进程的创建
fork
fork函数用来创建一个子进程
子进程获得父进程的栈、数据段、堆和执行文本段的拷贝
内存节约
需要注意的是,执行文本段其实就是代码段,这个段是父子进程共享的,换句话说,虚拟的进程空间各自有一份,但是指向的物理空间共享一份
还有,对于父进程数据段、堆段和栈段中的各页,内核采用写时复制的技术,简单来说,就是刚开始,父子进程指向的都是一样的,并没有复制,当子进程要修改时,内核才会对这些要修改的页进行拷贝,这个时候,对这些页的修改就不会影响对方了
这两种技术,都是为了节省空间,单纯的复制还是有点浪费了
文件共享
执行fork,子进程会获得父进程的文件描述符的副本,但是指向的文件是一样的,所以父子进程间共享文件偏移量和打开文件状态标志,换句话说,对文件的修改会通知对方
如果不想共享,可以使用以下两种办法
- wait(),让父进程等待子进程结束后,在执行
- 令父子进程使用不同的文件描述符,各自立即关闭不在使用的描述符
竞争条件
fork创建进程后,我们是没办法知道哪一个进程会优先执行,即获得cpu的使用权,这取决于操作系统的进程调度
可以使用信号量、文件锁等方法,不过一般来说是默认父进程为fork之后优先调度的对象
使用方法
fork返回值为-1,表示失败,返回值为0,表示是子进程,剩下的是父进程
switch (fork()) {
case -1:
/* 失败 */
case 0:
/* 子进程 */
default:
/* 父进程 */
}
还有需要注意的是,父子进程的代码段最开始是一样的,然后都是从fork函数的返回点开始执行,也就是说他们都会进入case的情况,然后根据各自的fork的返回值进入不同的case内执行,如果中途没有退出,那么他们都会顺利执行完整个程序,这点很重要
常用的做法还有子进程和exec函数结合,要注意的是当使用exec函数加载新程序时,会替换掉原有的进程映像,即将新程序加载到当前进程的内存空间中,并替换文本段、数据段等等,仅保留进程ID、文件描述符表等属性,然后当这个新程序结束后,也就是遇到exit或者main函数执行完毕,父进程仍可以用wait函数去捕捉返回状态,不过需要注意的是,这个新程序的返回点不是exec函数调用处,因为文本段都被替换了,已经和父进程没什么太多关系了
还有,如果对子进程有信号处理器函数,也会失效,因为文本段已经不一样了
vfork
vfork()因为如下两个特性而更具效率,这也是其与 fork()的区别所在。
无需为子进程复制虚拟内存页或页表。相反,子进程共享父进程的内存,直至其功执行了 exec()或是调用_exit()退出。
在子进程调用 exec()或_exit()之前,将暂停执行父进程。
vfork创建进程后,执行的顺序是确定的,只有等子进程结束,父进程才能执行
并且,vfork创建出的子进程是和父进程共享内存的,但仍存在一些情况,子进程的修改不会影响父进程,比如说对子进程的文件描述符进行操作(如果指向的是同一个打开文件句柄,那还是会有影响的),不过情况很少就对了
需要注意的是,尽管子进程也会有自己的文件流标,但是文件流对应着底层的一个或多个文件描述符,对文件流的操作可能会影响
wait
系统调用 wai(t &status)的目的有二:其一,如果子进程尚未调用 exit()止,那么 wait()会挂起父进程直至子进程终止;其二,子进程的终止状态通过 wait()的 status 参数返回。
execve
系统调用 execve(pathname,argv,envp)加载一个新程序(路径名为pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存。这将丢弃现存的程文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行(execing)一个新程序。
进程的终止
_exit
_exit()的 status 参数定义了进程的终止状态(termination status),父进可调用 wait()以获取该状态。虽然将其定义为 int 类型,但仅有低 8 位可为父进程所用。按照惯例,终止状态为 0 表示进程“功成身退”,而非 0 值则表示进程因异常而退出。
- EXIT_FAILURE 是1,表示非正常终止
- EXIT_SUCCESS 是0,表示正常终止
需要注意的是_exit(-1)传递给父进程的状态码是255, 因为status仅有低8位有效,即0-255, -1会被视为补码形式
exit
exit()会执行的动作如下
- 调用退出处理程序(通过 atexit()和 on_exit()注册的函数),其 执行顺序与册顺序相反 (就像是栈一样)
- 刷新 stdio 流缓冲区。
- 使用由 status 提供的值执行_exit()系统调用
程序的另一种终止方法是从 main()函数中返回(return),或者或明或暗地一直执行到main()函数的结尾处1。执行 return n 等同于执行对 exit(n)的调用,因为调用 main()的运行时函数会将 main()的返回值作为 exit()的参数。
退出处理程序
退出处理程序执行的顺序与注册顺序 相反
孤儿进程和僵尸进程
孤儿进程 (Orphan Process)
定义:
孤儿进程是指某个子进程在其父进程尚未终止的情况下,父进程先行结束。在这种情况下,孤儿进程会被操作系统的 init
进程(通常是进程ID为1的进程)收养。
特征:
- 收养机制:当一个父进程结束时,所有未结束的子进程会被
init
进程收养。init
进程会负责这些孤儿进程的管理。 - 继续运行:孤儿进程不会因为父进程的结束而终止,它们会继续执行,直到它们自己完成或被其他机制终止。
- 资源管理:
init
进程会监控孤儿进程并在其结束后进行适当的清理和资源释放。
僵尸进程 (Zombie Process)
定义:
僵尸进程是指一个已经终止的子进程,它的父进程尚未调用 wait
或 waitpid
来收集其返回状态和资源信息。虽然子进程已经完成执行,但在进程表中仍然有其记录,因此它被称为“僵尸”。
特征:
- 存在状态:僵尸进程在操作系统中仍然保留着一些信息(如进程ID、退出状态等),以便父进程在调用
wait
时可以获取这些信息。 - 资源占用:僵尸进程不再占用 CPU 时间,持有的资源会进行释放,但它仍占用系统的进程表项,直到父进程处理它的状态。
- 无法杀死:僵尸进程无法通过
kill
命令终止,因为它们已经完成了执行。要清除僵尸进程,必须通过其父进程调用wait
或waitpid
。
进程结束
一个进程的真正结束需要完成以下几个步骤:
- 执行完毕:进程的代码执行完成。
- 返回状态:进程通过系统调用(如
exit
)返回状态码。 - 资源释放:操作系统需要清理与该进程相关的所有资源,如内存、打开的文件等。这一过程包括:
- 释放分配给进程的内存。
- 关闭打开的文件描述符。
- 更新进程表,移除该进程的记录。
小结
孤儿进程和僵尸进程的管理对于操作系统的稳定性和性能至关重要。在开发多进程应用时,需要注意如何处理子进程的生命周期,以避免出现僵尸进程。同时,设计父进程的退出策略时也要考虑到孤儿进程的管理。
总结
不论程序是正常终止或者异常终止,内核都会执行多个清理步骤,包括关闭所有打开的文件描述符、文件流、目录流等等
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义