一、fork 函数基础概念
作用:创建一个新进程(子进程),与父进程共享代码段,数据段采用写时拷贝(Copy-on-Write)机制。
特点:
- 调用一次,返回两次(父进程和子进程各返回一次)。
- 父子进程拥有独立的进程控制块(PCB)和虚拟地址空间,但初始时共享物理内存中的代码和数据(通过页表映射)。
二、fork 调用时操作系统的核心操作
1.分配内核资源
- 为子进程创建独立的 PCB(进程控制块)和虚拟地址空间页表,构建虚拟地址与物理地址的映射关系。
- 子进程的 PCB 中包含进程状态、优先级、文件描述符表等信息。
2.拷贝父进程数据结构
- 复制父进程的部分数据结构(如打开的文件描述符、进程环境变量等)到子进程的 PCB 中。
3.将子进程加入系统进程列表
- 将子进程的 PCB 添加到操作系统的进程调度队列中,等待 CPU 调度。
4.fork 返回与进程调度
- 父进程中 fork 返回子进程的 PID(非零值),子进程中 fork 返回 0。
- 调度器开始调度父子进程,两者可能以任意顺序执行。
三、代码示例:fork 的执行效果
#include <stdio.h>
#include <unistd.h>
int main() {
printf("pid: %d before!\n", getpid()); // 父进程执行一次
fork(); // 创建子进程,此后父子进程并行执行
printf("pid: %d after!\n", getpid()); // 父子进程各执行一次
return 0;
}
输出结果:
pid: X before! // X为父进程PID,仅打印一次
pid: X after! // 父进程打印
pid: Y after! // Y为子进程PID,子进程打印
解析:
fork()
调用前,只有父进程执行,打印before
。fork()
调用后,父子进程同时执行后续代码,因此after
会打印两次。
四、fork 函数的返回值
进程类型 | 返回值 | 含义 |
---|---|---|
父进程 | 子进程的 PID(>0) | 用于标识新创建的子进程 |
子进程 | 0 | 标识自身为子进程 |
失败 | -1 | 如系统进程数超限、内存不足等 |
使用场景:
通过判断返回值实现父子进程逻辑分流:
pid_t pid = fork();
if (pid < 0) {
perror("fork failed"); // 处理错误
} else if (pid == 0) {
// 子进程逻辑(如执行不同任务)
} else {
// 父进程逻辑(如等待子进程结束)
}
五、写时拷贝(Copy-on-Write, COW)技术
核心思想:父子进程初始时共享物理内存中的代码和数据,仅在需要修改时才复制副本,避免无意义的内存拷贝。
共享机制实现
- 父子进程的页表初始时指向相同的物理内存页,且页表权限设为只读(代码段默认只读,数据段临时设为只读)。
- 当任意进程尝试写入数据段时,触发缺页中断(页权限冲突)。
写时拷贝触发流程
- 操作系统检测到写操作,为写入方分配新的物理内存页。
- 将原有数据拷贝到新页,更新页表映射关系为可写,允许进程修改新页的数据。
- 代码段因权限固定为只读,若强行写入会触发段错误(Segmentation Fault),而非写时拷贝。
优势
- 减少内存占用:避免复制未修改的数据,尤其适用于大内存场景。
- 提升效率:延迟内存分配,仅在必要时执行拷贝。
六、fork 失败原因
- 系统进程数超限:内核维护的进程表条目或 PID 资源耗尽。
- 内存不足:无法为子进程分配必要的内核结构或用户空间内存。
- 通过判断
fork
返回值为 - 1 并调用perror
可获取具体错误信息。
七、多进程创建
代码示例:批量创建子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void runChild(){
int cnt=10;
while(cnt){
printf("I'm child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
--cnt;
}
exit(0);
}
int main(){
int n=5;
for(int i=0;i<n;i++){
pid_t id=fork();
if(id==0){
//child
runChild();
}
}
sleep(1000);
}
代码执行逻辑解析
1.父进程循环创建子进程
- 每次循环调用
fork()
时,父进程复制出一个子进程。 - 父进程行为:
fork()
返回子进程的 PID(正数),继续执行循环,创建下一个子进程。 - 子进程行为:
fork()
返回 0,进入if (id == 0)
分支,执行runChild()
函数,执行完毕后通过exit(0)
退出。
2.子进程独立运行
- 每个子进程执行
runChild()
时:- 每秒输出自身 PID(
getpid()
)和父进程 PID(getppid()
)。 - 循环 10 次后自动退出,避免成为僵尸进程。
- 每秒输出自身 PID(
3.父进程休眠保持存活
- 循环结束后,父进程通过
sleep(1000)
长时间休眠,确保子进程有足够时间运行。 - 若父进程提前退出,子进程会成为孤儿进程,被 init 进程(PID=1)接管,但此处通过休眠避免了这一情
运行结果观察与分析
通过命令实时查看进程信息
- 另开终端窗口,使用以下命令每秒刷新进程列表:
while :; do ps ajx | head -1 && ps ajx | grep createMulProcess | grep -v grep; sleep 1; done
- 输出示例:
PID 分配的动态特性
- 多次运行代码时,子进程 PID 后两位可能不连续(如第一次为 28、29、30、32、31,第二次为 28、29、30、31、32)。
- 原因:PID 由系统动态分配(基于 PID 池循环使用),与创建顺序无固定关联,体现了操作系统的资源管理机制。
多进程执行顺序的不确定性
调度器的作用
- 父子进程或兄弟进程的执行顺序由操作系统的 进程调度器 决定,无法预先确定。
- 新创建的进程会被放入 运行队列 等待调度,执行顺序取决于:
- 进程优先级(默认相同)。
- 调度算法(如轮转调度、优先级调度)。
- 系统负载和资源竞争情况。
示例场景
- 若子进程 1 先被调度,可能先输出;若子进程 3 先被调度,可能先输出,完全随机。
- 这一特性是多进程 / 多线程编程的核心难点之一,需通过同步机制(如信号量、管道)协调顺序。