什么是进程
开发者编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当运行该可执行文件后,它会被转载到内存中,接着 CPU 会执行程序中的每一条指令,这个运行中的程序就被称之为「进程」。
进程树
一个进程都是由其他进程创建出来的,每个进程都有各自的 PID,在 Linux
系统的进程之间存在继承关系,所有的进程都是1号进程(init进程)的子进程。可以通过
pstree
命令查看进程的继承关系。
系统中的每个进程必定会有一个父进程,在 Linux 操作系统中,可以在
/proc
文件系统中看到该进程对应的父进程 PID,也可以通过
ps -ef
命令查看。
Linux 进程涉及进程的所有操作都围绕进程描述符展开,就是 task_struct 结构体。
struct task_struct { |
进程终结
当进程通过 exit()
系统调用终结时,系统需要释放该进程所占有的所有资源,这个 syscall
可能是来自进程内部的
exit()
,也可能是来自外部的信号。调用结束后,此时该进程还没有完全从系统中消失,该进程的进程描述符依旧存在于系统中,存在的唯一目的就是向其父进程提供信息。
进程的收尾工作是交由其父进程完成的,父进程会通过 wait()
或者 waitpid()
来获取子进程的状态信息,从而释放该进程最后剩余的进程描述符、slab
缓存等,该调用会阻塞当前父进程,直到某个子进程退出。
孤儿进程
当一个父进程退出,而它的一个或者多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程会被 init 进程收养,并由 init进程对它们完成状态收集工作。
僵尸进程
一个进程 fork()
创建子进程,如果子进程退出,而父进程没有调用 wait()
或者
waitpid
获取子进程的状态信息,那么子进程的进程描述符仍然保留在操作系统中,这种进程被称为僵尸进程。
危害
僵尸进程虽然不占用内存空间,但是如果父进程不进行善后操作的话,那么子进程保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果有大量的僵尸进程,会导致创建新进程时没有可用的进程号。
相反,孤儿进程是没有父进程的进程,孤儿进程由 init 进程处理善后操作。因此孤儿进程并不会有什么危害。
解决
1)方案一:父进程通过 wait
和 waitpid
等函数等待子进程结束,但是这会导致父进程阻塞;
2)方案二:信号机制
子进程退出时向父进程发送 SIGCHILD
信号,父进程处理
SIGCHILD
信号,在信号处理函数中调用 wait
处理子进程。
3)方案三:fork 两次
原理是将进程变成孤儿进程,从而从 init进程来处理善后操作。具体操作:
- 父进程一次
fork()
后产生一个子进程,随后立即执行wait(NULL)
来等待子进程结束; - 子进程
fork()
产生孙子进程,随后立即exit(0)
,这样子进程顺利终止,此时父进程仅仅给子进程收尸,并不需要子进程的返回值,然后父进程继续执行。
此时孙子进程由于失去了它的父进程,成为了孤儿进程,将被转交给 init 进程托管。这样以来父进程与孙子进程无继承关系了,它们的父进程均为 init 进程。
4)方案四:kill 父进程
严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程,可以使用命令
ps -aux|grep Z
进行查找。kill
掉该进程之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被 init
进程接管,init 进程就会释放它们占用的系统进程表中的资源。
推荐阅读
- 孤儿进程orphaned与僵尸进程Zombie
- [从一道面试题来学习前台进程和后台进程、孤儿进程和僵尸进程 ](