「Linux 系统编程」fork() 创建子进程
1 fork() 函数
在 Linux 系统编程中,进程创建是核心概念之一。一个进程(父进程)可以通过 fork()
函数 创建出一个与自己几乎完全相同的副本(子进程)。
1.1 函数原型
1 |
|
1.2 工作原理
调用 fork()
函数后,操作系统会 创建一个新的进程,这个新进程是调用者进程(父进程)的一个完整副本(子进程)。
-
复制进程上下文:操作系统会为子进程 分配一个新的进程标识符(PID),然后复制父进程的文本段、数据段、堆栈段以及进程环境(如打开的文件描述符、信号处理程序等)到子进程中。
-
读时共享、写时复制:
-
在
fork()
刚成功时,或者 只涉及读的操作,父子进程的物理内存页是共享的(读时共享)。 -
只有当任一进程试图修改这些内存页时,涉及写的操作,操作系统才会为该进程复制一份独立的副本(写时复制)。
-
-
返回值分流:
fork()
函数会 返回两次。-
在 父进程 中,
fork()
函数返回 新创建子进程的 PID(一个大于 0 的正整数)。 -
在 子进程 中,
fork()
函数返回0
。表示创建子进程成功。 -
如果创建失败(例如系统进程数达到上限),则返回
-1
给父进程。
-
可以通过判断返回值,决定后续是执行父进程的代码还是子进程的代码。
1.3 getpid() 与 getppid()
在处理进程时,经常需要获取进程的标识符(PID):
pid_t getpid(void)
:返回 当前进程的 PID。
pid_t getppid(void)
:返回 当前进程的父进程的 PID(PPID)。
1.4 创建子进程示例
1 |
|
进程调度是随机的,父进程和子进程异步执行,它们的输出顺序不确定,可能会交错出现。
2 循环创建多个子进程
2.1 错误示例
如果直接在循环中调用 fork()
函数,实际上会创建出 个子进程:
1 |
|
上面的代码会产生指数级增长的进程。每次 fork()
后,父进程 和 刚创建的子进程 都会继续执行下一次循环。
假设 n = 3
,上面代码实际循环创建子进程的过程如下:
2.2 正确示例
正确的做法是在 fork()
后通过 判断返回值来终止子进程的循环:
1 |
|
3 父子进程相同和不同的内容
特性 | 父子进程相同点 | 父子进程不同点 |
---|---|---|
代码段 | 在 fork() 瞬间完全相同 |
之后可以执行不同的代码路径(通过 if-else 判断) |
数据段、堆、栈 | 在 fork() 瞬间拥有相同的内容 |
之后由于 写时复制,对变量的修改相互独立 |
环境变量 | 继承父进程的环境 | 可独立修改(如使用 setenv ),不影响对方 |
文件描述符 | 共享 相同的打开文件表项。文件偏移指针会相互影响 | 拥有独立的文件描述符编号集合,但指向相同的内核文件对象 |
信号处理 | 继承父进程的信号处理设置(如 ignore, handler) | 可以独立设置新的信号处理方式 |
进程属性 | - | PID、PPID、进程运行时间、未决信号集等不同 |
锁状态 | - | 子进程不会继承父进程通过 fcntl 创建的锁 |
共享的文件描述符:
如果父进程打开了一个文件,然后调用 fork()
,父子进程可以对同一个文件进行读写。它们的操作会相互影响文件偏移指针。例如,如果父进程写入 “Hello”,子进程写入 “World”,那么文件内容将是 “HelloWorld” 而不是随机的覆盖。