Linux系统编程(4)进程基础与 fork 函数详解 📅 发布时间:2026/8/18 4:30:03 👁 浏览次数: 前言本文整理自 Linux 系统编程中关于进程基本概念、fork 函数、进程状态、父子进程关系以及多进程编程的学习笔记与代码示例。内容涵盖理论知识、API 用法、经典试题解析和完整带注释的代码适合用于回顾复习。一、什么是进程1. 进程的定义进程正在进行中的程序是程序的一次执行过程也可以说是程序的一个实例。程序a.out存放在硬盘上是静态的。当我们执行./a.out时硬盘上的程序被加载到内存中运行起来就变成了进程——动态的。程序每运行一次都对应一个新的进程。2. 进程需要用到的资源进程在运行过程中需要用到内存和CPU这两样就是进程需要用到的核心资源。3. 进程的组成程序 数据 代码 data bss text 进程 PCB [栈 | 堆 | data | bss | text]C 语言中程序加载到内存后对应5 个区区域说明栈stack局部变量、函数调用信息堆heap动态分配的内存mallocdata全局区中已初始化的数据如int b 1;bss全局区中未初始化的数据如static int a;text代码区存放程序指令此外还有一个重要的结构——PCBProcess Control Block进程控制块用于保存进程运行过程中的动态信息如 PID、进程状态、寄存器值等。4. 为什么需要进程早期计算机如纸带机、单片机 51一次只能运行一个任务。现代操作系统需要多任务同时运行这就需要进程机制。通过时间片轮转等 CPU 资源调度算法实现宏观上并行看起来多个程序同时在运行。微观上串行同一时刻 CPU 只在执行一个进程。并发与并行是两个不同的概念。二、进程的状态1. 查看进程状态的命令ps aux | grep a.out # 查看进程的 PID 号及状态 ps -elf | grep a.out # 查看进程的 PPID 号和 PID 号PIDProcess ID进程 ID。PPIDParent Process ID父进程 ID。2. 终止进程kill -信号编号 进程的PID常用信号9) SIGKILL强制杀死进程。19) SIGSTOP暂停进程。18) SIGCONT继续运行被暂停的进程。3. 进程状态码状态码含义D不可中断睡眠通常是 I/O 等待R运行中或在运行队列中S可中断睡眠等待事件完成T被作业控制信号停止t被调试器在跟踪过程中停止W分页中2.6.xx 内核后已无效X死亡基本不会被看到Z僵尸进程已终止但未被父进程回收BSD 格式下还可能显示附加字符高优先级N低优先级L页面被锁定在内存中s会话领导者l多线程在前台进程组中4. 进程树Linux 系统中通过进程树来管理所有进程类似族谱/家谱存在父子关系。早期系统第一个进程为init。现代系统第一个进程为systemd。三、fork 函数——创建子进程1. 函数原型#include sys/types.h #include unistd.h pid_t fork(void);功能创建子进程。参数无。返回值父进程中返回子进程的 PID大于 0。子进程中返回 0。失败返回 -1同时errno被设置。2. 重要注意事项fork 之后谁先运行运行顺序是不确定的取决于进程调度算法时间片轮转、先来先服务等。fork 之后父子进程的关系父子进程拥有独立的 4G 内存空间32 位操作系统。子进程拥有自己的数据段父进程拥有自己的数据段。相互之间不会影响——fork 之后两个进程各自独立运行。四、代码示例附详细注释示例 1fork 基本用法——父子进程分别输出#include stdio.h #include sys/types.h #include unistd.h int main(int argc, const char *argv[]) { /* 调用 fork 创建子进程 * fork 的返回值有三种情况 * 0 在父进程中返回值是子进程的 PID * 0在子进程中 * 0 创建失败 */ pid_t pid fork(); /* 如果 fork 返回值小于 0说明创建子进程失败 */ if (pid 0) { perror(fork fail); // 打印错误信息 return -1; } /* pid 0说明当前是父进程 */ if (pid 0) { printf(father hello world!\n); } /* pid 0说明当前是子进程 */ else if (pid 0) { printf(child hello world!\n); } /* 这行代码父子进程都会执行到 * 因为 fork 之后父子进程各自独立运行 * 都会从 fork 之后继续往下执行 */ printf(common hello world!\n); sleep(1); return 0; }运行结果father hello world! common hello world! child hello world! common hello world!可以看到father和child由各自进程分别打印而common被打印了两次——因为父子进程都会执行到if-else之后的代码。示例 2父子进程各自死循环#include stdio.h #include sys/types.h #include unistd.h int main(int argc, const char *argv[]) { /* 调用 fork 创建子进程 */ pid_t pid fork(); /* 创建失败 */ if (pid 0) { perror(fork fail); return -1; } /* 父进程pid 0进入无限循环打印父进程信息 */ if (pid 0) { while (1) { /* getpid() 获取当前进程的 PID */ printf(father%d hello world!\n, getpid()); sleep(1); // 每隔 1 秒打印一次 } } /* 子进程pid 0进入无限循环打印子进程信息 */ else if (pid 0) { while (1) { printf(child%d hello world!\n, getpid()); sleep(1); // 每隔 1 秒打印一次 } } /* 这行代码不会被执行到 * 因为父子进程都进入了各自的无限循环 * 不会走到这里 */ printf(common hello world!\n); sleep(1); return 0; }运行结果交替输出father6037 hello world! child6038 hello world! father6037 hello world! child6038 hello world! ...注意最后一行printf(common hello world!\n)永远不会被执行因为父子进程都在各自的while(1)循环中无法跳出来。示例 3for 循环中多次 fork#include stdio.h #include unistd.h int main(int argc, const char *argv[]) { int i 0; /* 循环 3 次每次调用 fork() * 第 1 次 fork1 个进程变成 2 个 * 第 2 次 fork2 个进程各 fork 一次变成 4 个 * 第 3 次 fork4 个进程各 fork 一次变成 8 个 * 最终总共 2^3 8 个进程 * 按下回车后全部退出 */ for (i 0; i 3; i) { fork(); } getchar(); // 等待输入方便用 ps 命令观察进程数 return 0; }分析循环 3 次每次所有已存在的进程都会 fork 一次最终产生2^3 8 个进程。示例 4父子进程的内存独立性验证#include stdio.h #include sys/types.h #include unistd.h #include stdlib.h /* 全局变量位于 data 段已初始化 */ int b 1; int main(int argc, const char *argv[]) { /* 静态局部变量位于 data 段已初始化 */ static int a 0; /* 动态分配的内存位于堆区 */ int *p malloc(sizeof(int)); *p 2; /* fork 创建子进程 */ pid_t pid fork(); if (pid 0) { perror(fork fail); return -1; } /* 父进程每次对 a、b、*p 各加 1 */ if (pid 0) { while (1) { a; b; (*p); printf(father%d a %d b %d *p %d\n, getpid(), a, b, *p); sleep(1); } } /* 子进程每次对 a、b、*p 各加 2 */ else if (pid 0) { while (1) { a 2; b 2; (*p) 2; printf(child%d a %d b %d *p %d\n, getpid(), a, b, *p); sleep(1); } } return 0; }运行结果father6037 a 1 b 2 *p 3 child6038 a 2 b 3 *p 4 father6037 a 2 b 3 *p 4 child6038 a 4 b 5 *p 6 father6037 a 3 b 4 *p 5 child6038 a 6 b 7 *p 8 ...核心结论fork 之后子进程会复制父进程的内存空间包括全局变量b、静态变量a、堆区的*p。但从 fork 那一刻起父子进程拥有各自独立的副本互不影响父进程对a加 1子进程对a加 2各自独立变化。全局变量、静态变量、堆区数据在 fork 后都是独立的。示例 5经典试题fork() fork() || fork()#include stdio.h #include unistd.h int main(int argc, const char *argv[]) { /* 经典 fork 面试题 * 表达式展开为三个 fork 调用编号 (1)(2)(3) * * 进程关系分析 * 原始进程 6037 执行 fork(1) * 6037 (pid0) → 继续执行 fork(2) * 6037 (pid0) → 为真短路跳过(3)... 但 || 需要继续 * 实际上 优先级高于 ||表达式为 * (fork(1) fork(2)) || fork(3) * 6038 (pid0) → 为假短路执行 fork(3) * * 最终产生 5 个进程6037, 6038, 6039, 6040, 6041 */ fork() fork() || fork(); /* 所有进程都进入无限循环方便 ps 查看 */ while (1) sleep(1); return 0; }详细分析过程表达式: fork(1) fork(2) || fork(3) (由于 优先级高于 ||等价于 (fork(1) fork(2)) || fork(3)) 原始进程 6037 执行 fork(1): ├── 6037 (返回 0为真) │ ├── 继续执行 fork(2): │ │ ├── 6037 (返回 0为真) │ │ │ → (真 真) 真|| 短路不执行 fork(3) │ │ └── 6040 (返回 0为假) │ │ → (真 假) 假|| 继续执行 fork(3): │ │ ├── 6040 (返回 0) → 真 │ │ └── 6041 (返回 0) → 假 │ └── ... └── 6038 (返回 0为假) → (假 ...) 假短路跳过 fork(2)|| 继续执行 fork(3): ├── 6038 (返回 0) → 真 └── 6039 (返回 0) → 假 最终进程6037, 6038, 6039, 6040, 6041 → 共 5 个进程示例 6动态创建 n 个子进程#include stdio.h #include unistd.h int main(int argc, const char *argv[]) { int n 0; printf(Input a num:); scanf(%d, n); // 从键盘读取要创建的子进程数量 int i 0; pid_t pid; /* 循环 n 次创建子进程 * * 关键技巧父进程 continue 继续循环创建下一个子进程 * 子进程 break 跳出循环不再参与 fork * 这样保证每个子进程只由原始父进程创建不会产生孙进程 * 最终结果1 个父进程 n 个子进程 */ for (i 0; i n; i) { pid fork(); if (pid 0) { /* 当前是父进程继续下一轮循环创建下一个子进程 */ continue; } else if (pid 0) { /* 当前是子进程跳出循环不再 fork */ break; } } /* fork 循环结束后通过 pid 的值区分父进程和子进程 */ if (pid 0) { /* pid 0 说明是父进程 */ printf(f %d\n, getpid()); } else { /* pid 0 说明是子进程 */ printf(c %d\n, getpid()); } getchar(); // 消耗缓冲区中的换行符 getchar(); // 等待用户输入方便观察 return 0; }运行结果输入 3 时Input a num:3 f 6037 c 6038 c 6039 c 60401 个父进程打印f3 个子进程分别打印c。示例 7无限循环 hello world用于观察进程#include stdio.h int main(int argc, const char *argv[]) { /* 一个简单的无限循环程序 * 用于配合 ps、top、kill 等命令观察进程状态 * 可以用 ps aux | grep a.out 查看其 PID 和状态 * 可以用 kill -9 PID 强制结束 */ while (1) { printf(hello world!\n); } return 0; }示例 8简易 ls 命令实现目录操作 文件属性综合#include stdio.h #include sys/types.h #include sys/stat.h #include unistd.h #include pwd.h #include grp.h #include time.h #include dirent.h /* 打印单个文件的详细信息类似 ls -l 的输出格式 */ int do_ls_file(const char *filename) { struct stat st; /* stat() 获取文件属性失败返回 -1 */ if (stat(filename, st) 0) { perror(stat fail); return -1; } /* 1. 文件类型通过 st_mode 与 S_IFMT 掩码做按位与判断 */ switch (st.st_mode S_IFMT) { case S_IFSOCK: putchar(s); break; // 套接字 case S_IFLNK: putchar(l); break; // 符号链接 case S_IFREG: putchar(-); break; // 普通文件 case S_IFBLK: putchar(b); break; // 块设备 case S_IFDIR: putchar(d); break; // 目录 case S_IFCHR: putchar(c); break; // 字符设备 case S_IFIFO: putchar(p); break; // 管道 } /* 2. 权限9 个权限位分别判断 所有者/组/其他 的 读/写/执行 */ st.st_mode S_IRUSR ? putchar(r) : putchar(-); // 所有者读 st.st_mode S_IWUSR ? putchar(w) : putchar(-); // 所有者写 st.st_mode S_IXUSR ? putchar(x) : putchar(-); // 所有者执行 st.st_mode S_IRGRP ? putchar(r) : putchar(-); // 组读 st.st_mode S_IWGRP ? putchar(w) : putchar(-); // 组写 st.st_mode S_IXGRP ? putchar(x) : putchar(-); // 组执行 st.st_mode S_IROTH ? putchar(r) : putchar(-); // 其他读 st.st_mode S_IWOTH ? putchar(w) : putchar(-); // 其他写 st.st_mode S_IXOTH ? putchar(x) : putchar(-); // 其他执行 /* 3. 硬链接数 */ printf( %-3ld , st.st_nlink); /* 4. 用户名和组名通过 UID/GID 查询对应名称 */ printf( %-10s , getpwuid(st.st_uid)-pw_name); printf( %-10s , getgrgid(st.st_gid)-gr_name); /* 5. 文件大小字节 */ printf( %-10ld , st.st_size); /* 6. 最后修改时间将 time_t 转换为 struct tm 格式化输出 */ struct tm *ptm localtime(st.st_mtime); printf(%02d月 %02d %02d:%02d , ptm-tm_mon 1, ptm-tm_mday, ptm-tm_hour, ptm-tm_min); /* 7. 文件名 */ printf( %s \n, filename); return 0; } /* 遍历目录中所有文件逐个调用 do_ls_file 打印详细信息 */ int do_ls_dir(const char *dirname) { if (dirname NULL) return -1; /* 先切换到目标目录 */ if (chdir(dirname) 0) { perror(chdir fail); return -1; } /* 打开目录 */ DIR *dp opendir(dirname); if (dp NULL) { perror(opendir fail); return -1; } /* 循环读取目录中的每个条目 */ while (1) { struct dirent *pdir readdir(dp); if (pdir NULL) // 读取完毕 break; /* 对每个文件调用 do_ls_file 打印详细信息 */ do_ls_file(pdir-d_name); } /* 关闭目录流 */ closedir(dp); return 0; } int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s dir or filename\n, argv[0]); return -1; } struct stat st; if (stat(argv[1], st) 0) { perror(stat fail); return -1; } /* 判断参数是目录还是普通文件 * 目录 → 遍历目录下所有文件 * 普通文件 → 直接打印该文件信息 */ if (st.st_mode S_IFDIR) do_ls_dir(argv[1]); // 是目录遍历并打印 else do_ls_file(argv[1]); // 是文件直接打印 return 0; }五、进程的执行方式子进程创建好之后有两种执行方式1. 子承父业——fork 后直接执行fork 之后子进程复制父进程和父进程做同样的任务。类似于12306 节假日增开窗口每个窗口做同样的事。淘宝双11增开服务器每台服务器处理同样的请求。2. 独立创业——fork exec 函数族子进程调用exec系列函数替换当前进程的代码段去执行一个全新的程序。bash (PID: 4433) | fork() | ├── 父进程 bash 继续等待 | └── 子进程 bash exec(a.out) → 变成 a.out 进程Shellbash就是这么工作的每次你在终端输入命令bash 先 fork 一个子进程子进程再 exec 成你输入的命令程序。六、僵尸进程与孤儿进程1. 僵尸进程Zombie场景子进程先结束但父进程没有回收它的资源没有收尸。子进程退出后其 PCB 信息仍然保留在系统中等待父进程读取。如果父进程一直不调用wait()/waitpid()回收子进程就变成僵尸状态Z 状态。僵尸进程有危害占用系统进程表资源需要专门处理。2. 孤儿进程Orphan场景父进程先结束但子进程还在运行。子进程失去父进程成为孤儿进程。Linux 会自动将孤儿进程交由init 进程现代系统为 systemd收养。被收养的子进程会变成后台进程。七、总结本文覆盖了以下核心知识点知识点关键内容进程的概念进程是程序的一次执行过程由 PCB 内存五区组成进程的状态R运行、S睡眠、Z僵尸、T停止等进程树所有进程形成树状结构最早的进程为 systemdfork 函数创建子进程父进程返回子进程 PID子进程返回 0父子进程内存fork 后拥有独立的内存空间互不影响多次 fork循环 n 次 fork 产生 2^n 个进程动态创建 n 个子进程父进程 continue、子进程 break 的技巧fork exec子进程执行全新程序独立创业僵尸进程子进程已死但父进程未回收状态为 Z孤由进程父进程已死子进程被 init/systemd 收养掌握 fork 函数的用法和父子进程的关系是理解 Linux 多任务编程的基础。建议结合代码动手练习尤其是示例 4内存独立性和示例 5经典试题加深对 fork 机制的理解。