僵尸进程详解

来源:互联网 发布:java class类 编辑:程序博客网 时间:2024/06/05 16:41

        //xk> 铺垫1:fork()和exec

        Unix中派生一个新的进程的唯一方法是fork()函数(有些系统可能提供它的各种变体)。父进程中打开的所有描述符在fork()之后由子进程分享。

        存放在硬盘上的程序文件能够被Unix执行的唯一途径是:由一个现有进程调用exec函数中的某一个(exec函数有6个,作用相同,调用参数不同)。exec不会创建一个新的进程,而是用程序文件替换掉当前进程(即调用exec的进程,又叫调用进程)映像,而且该新程序通常从main()开始执行,进程ID不变,并且默认情况下调用进程打开的描述符跨exec继续保持打开(可以使用fcntl设置FD_CLOEXEC描述符标志禁止)。

        因为exec会替换掉原来的进程映像,所以常常是先fork(),然后在子进程中调用exec。

        //xk> 铺垫2:main()和exit()

        C程序总是从main()开始执行,当内核使用一个exec函数执行C程序时,会先调用一个启动例程,启动例程从内核取得命令行参数和环境变量值。在通过C源程序创建可执行文件时,连接器会将系统启动例程指定为程序的起始地址。

        启动例程通常用汇编语言编写,表示成C语言的形式则为:

exit(main(argc, argv));使得从main()返回后立即调用exit()。exit()主要做两个事情:1. 执行标准I/O库的清理关闭操作。为所有打开流调用fclose(),这会造成所有缓冲的输出数据都被flush到文件上。2. 返回exit status. 通常就是main()的返回值。

        //xk> 铺垫3:进程终止

        有8种方法终止进程。

        5种正常终止:

        1. 从main()返回。

        2. 调用exit(). 执行一些清理处理(调用执行各atexit()设定的终止处理程序,关闭所有标准I/O流等),然后进入内核,应该还是调用了_exit()系统调用。

        3. 调用_exit()或_Exit(). 不执行清理操作立即进入内核。_exit()是由POSIX.1说明的,_Exit()和exit()是由ISO C说明的。而内核对_exit()系统调用的处理是释放进程所拥有的资源并向父进程发送SIGCHLD信号(默认处理是忽略)。

        4. 最后一个线程从其启动例程返回。

        5. 最后一个线程调用pthread_exit().

        3种异常终止:

        1. 调用abort(). 此函数将SIGABRT信号发送给调用进程。

        2. 接到一个信号并终止。对多数信号,如果进程没有捕捉它,通常进程将会立即终止,并将进程在内存中的映像生成为核心转储文件core放在当前目录下。

        3. 最后一个线程对取消请求做出响应。线程可以被同一进程中的其它线程cancel掉。

        //xk> 铺垫4:Unix信号一般是不排队的

        当引发信号的事件发生时,内核会产生一个信号,然后递送给相应的进程,这通常是由内核在进程表中设置一个某种形式的标志,总之内核做这些动作是需要时间的,在信号产生和递送之间的时间间隔内,称信号是未决的(pending)。并且,进程还可以设置信号递送阻塞,如果为该进程产生了一个选择为阻塞的信号,而且对该信号的处理是系统默认动作或捕捉该信号,则内核将此信号保持为未决状态,直到进程对该信号解除了阻塞或者对此信号的动作改为忽略。

        如果信号成功递送到进程之前,这种信号发生了多次,会怎么样呢?大多数UNIX并不对信号排队,即系统只会递送该信号一次。支持POSIX.1实时扩展的系统才会递送该信号多次。

        //xk> 好,下面开始

        //xk>----------------------------------------------------------------

        在很多场景下需要用到fork()来派生一个子进程,这时需要注意僵尸进程的问题。

        //xk> 僵尸进程的产生

        我们一般认为,进程终止后系统会自动回收分配给该进程的所有资源。然而,当一个子进程终止时,它其实是处于“僵而不死”的状态,因为父进程可能需要检测子进程终止的退出码。所以子进程的执行流程虽然结束,但它仍然存在于系统中,进程表中代表子进程的表项还没有释放,直到父进程也正常终止或父进程调用wait()/waitpid()检查了已终止子进程的退出状态。如果父进程异常终止,则子进程自动把PID为1的进程(即init进程)作为自己的父进程,僵尸进程会一直保留在进程表中直到被init进程发现并释放。

        //xk> 僵尸进程的处理

        在父进程中可以通过调用wait()来让父进程等待子进程的结束并检查子进程的exit status。如果子进程尚未结束,则父进程会被阻塞,等待子进程的结束。如果子进程已经结束,则会超度已经僵死的子进程,释放资源。

        如果不希望父进程被阻塞咋办呢?子进程结束时会向父进程发送SIGCHLD信号,可以把wait()调用放到SIGCHID信号的信号处理函数中。

        但是小心!建立一个信号处理函数并在其中调用wait()并不足以防止产生僵尸进程。考虑下面的情景:

        因为Unix进程比较轻量级,在Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。现在假设在一个客户进程中打开n个socket描述符都与这样的并发服务器建立TCP连接(模拟服务器负载很大时多个客户几乎同时建立连接或断开连接),那么服务器就会fork出n个子进程来响应这n个连接请求。当客户进程终止时,n个socket描述符几乎同时关闭,每个socket描述符关闭时都会发出FIN报文通知服务器客户端不会再向socket写数据了。假设服务器子进程的处理是接收客户端发来的数据,直到收到FIN报文则程序执行结束,那么n个服务器子进程几乎同时结束,几乎同时向并发服务器父进程发送SIGCHLD信号。悲剧的是,Unix信号一般是不排队的,所以这么做很容易造成一些服务器子进程终止的SIGCHLD信号丢失,这些子进程不会被父进程wait()到,也就变成了僵尸进程。

        正确的解决办法是调用waitpid(). 相比wait()这个函数有两点强大的地方:1. 可以指定子进程id号。2. 可以设置WNOHANG选项让父进程在指定的子进程尚未终止时不要阻塞。

        针对上面设定的情景,并发服务器程序可以隔一段时间用waitpid()遍历一次所有的子进程,清掉僵尸子进程。注意必须设置WNOHANG选项,并发服务器被阻塞是不可忍受的。

原创粉丝点击