Linux 信号,僵尸进程,(面试)

时间:2022-10-29 16:46:17

1,信号有那些是不能被屏蔽的???(SIGKILL和SIGSTOP

2,僵尸进程是什嘛样子的,产生子进程的时候给其分配空间了之后,在变成僵尸进程之后,对这块空间是如何处理

  的,是将其全部释放掉???还是保留部分空间给(子进程的进程ID、终止状态以及资源利用信息(CPU时间,内

  存使用量),因为父进程可能会用到这些东西,????


信号(signal)是Linux进程间通信的一种机制,全称为软中断信号,也被称为软中断。信号本质上是在软件层次上对硬件中断机制的一种模拟。信号提供了一种处理异步事件的方法。每个信号的名字都以SIG字符开头,为正整型常量,定义在<signal.h>头文件中(实际上,实现将信号定义在内核头文件中,<signal.h>又包含该内核头文件,如:Linux 3.2.0将信号定义在<bits/signum.h>中,FreeBSD 8.0将信号定义在<sys/signal.h>中)


信号有3种处理方式:

(1) 忽略此信号。大多数信号都使用这种方式处理。但是有两种信号不能被忽略:SIGKILL和SIGSTOP。原因是这两种信号向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用、除0错误等),那进程的运行行为是未定义的。

(2) 捕捉信号。通知内核在某种信号发生时,调用一个用户函数来处理该信号。同样地,不能捕捉SIGKILL和SIGSTOP信号。

(3) 执行系统默认动作。对大多数信号而言,系统默认动作就是终止该信号。



与其他进程间通信方式(例如管道、共享内存等)相比,信号所能传递的信息比较粗糙,只是一个整数。但正是由于传递的信息量少,信号也便于管理和使用,可以用于系统管理相关的任务,例如通知进程终结、中止或者恢复等。

每种信号用一个整型常量宏表示,以SIG开头,比如SIGCHLD、SIGINT等,它们在系统头文件<signal.h>中定义。

信号由内核(kernel)管理,产生方式多种多样:
  • 可以由内核自身产生,比如出现硬件错误、内存读取错误,分母为0的除法等,内核需要通知相应进程。
  • 也可以由其他进程产生并发送给内核,再由内核传递给目标进程。

信号传递的过程:
  • 内核中针对每一个进程都有一个表来保存信号。
  • 当内核需要将信号传递给某个进程时,就在该进程对应的表中写入信号,这样就生成了信号。
  • 当该进程由用户态陷入内核态,再次切换到用户态之前,会查看表中的信号。如果有信号,进程就会首先执行信号对应的操作,此时叫做执行信号。
  • 从生成信号到将信号传递给对应进程这段时间,信号处于等待状态。
  • 我们可以编写代码,让进程阻塞(block)某些信号,也就是让这些信号始终处于等待的状态,直到进程取消阻塞(unblock)或者忽略信号。







可再入函数

进程捕捉到信号并对其进行处理时,正常执行的指令序列就会被中断,首先需要执行信号处理程序,之后则应该接着执行之前未完成的指令序列。但是在信号处理程序中,并不能判断捕捉到信号时进程执行到什么地方,如果进程正在执行malloc,而此时由于捕捉到信号而插入信号处理函数也要调用malloc,此时,由于malloc通常会为它所分配的存储区维护一个链表,而插入信号处理函数时,该进程正在修改链表,那么结果是进程环境遭到破坏,丢失重要信息。

Single Unix Specification说明了在信号处理程序中保证调用安全的函数,这些函数是可再入的,称为异步信号安全(async-signal safe)函数。除了可再入外,在信号处理期间,它会阻塞任何引起不一致的信号发送。

一般不可再入函数有如下特点:

(1)   使用静态数据结构;

(2)   调用malloc或free;

(3)   属于标准I/O函数,因为很多标准I/O库都以不可再入的方式使用了全局数据结构。

 

可靠信号术语

不可靠的信号是指信号在处理之前可能丢失。在发送一个信号给进程时,我们说向进程递送(delivery)了一个信号。在信号产生(generation)和递送之间的时间间隔内,称该信号是未决(pending)的。

如果进程采用“阻塞信号递送”(每个信号都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。进程可以调用sigprocmask来检测和更改当前信号屏蔽字。进程调用sigpending函数来判断哪些信号是设置为阻塞并处于pending状态),而且对该信号的处理是采用系统默认动作或者捕捉该信号,那么该信号将一直保持未决状态,直到进程对信号解除阻塞,或者对该信号的处理改为忽略。


信号种类

下表列出了一些常见信号:

Linux 信号,僵尸进程,(面试)


每种信号都会有一个默认动作。默认动作就是脚本或程序接收到该信号所做出的默认操作。常见的默认动作有终止进

程、退出程序、忽略信号、重启暂停的进程等,上表中也对部分默认动作进行了说明。



发送信号

有多种方式可以向程序或脚本发送信号,例如按下<Ctrl+C>组合键会发送SIGINT信号,终止当前进程。

Linux 信号,僵尸进程,(面试)
还可以通过 kill 命令发送信号,语法为:

kill -signal pid
signal为要发送的信号,可以是信号名称或数字;pid为接收信号的进程ID。例如:

kill -1 1001
将SIGHUP信号发送给进程ID为1001的程序,程序会终止执行

又如,强制杀死ID为1001的进程:

kill -9 1001

捕获信号

通常情况下,直接终止进程并不是我们所希望的。例如,按下<Ctrl+C>,进程被立即终止,不会清理创建的临时文件,带来系统垃圾,也不会保存正在进行的工作,导致需要重做。

可以通过编程来捕获这些信号,当终止信号出现时,可以先进行*和保存处理,再退出程序。

用户程序可以通过C/C++等代码捕获信号,这将在Linux C编程中进行讲解,这里仅介绍如果通过Linux命令捕获信号。

通过 trap 命令就可以捕获信号,语法为:

trap commands signals

commands为Linux系统命令或用户自定义命令;signals为要捕获的信号,可以为信号名称或数字。



捕获到信号后,可以有三种处理:

  • 执行一段脚本来做一些处理工作,例如清理临时文件;
  • 接受(恢复)信号的默认操作;
  • 忽略当前信号。

1) 清理临时文件

脚本捕获到终止信号后一个常见的动作就是清理临时文件。例如:
<span style="font-size:18px;">trap "rm -f $WORKDIR/work1$$ $WORKDIR/dataout$$; exit" 2</span>

当用户按下<Ctrl+C>后,脚本先清理临时文件 work1$$ 和 dataout$$ 再退出。

注意:exit 命令是必须的,否则脚本捕获到信号后会继续执行而不是退出。

修改上面的脚本,使接收到 SIGHUP 时进行同样的操作:
<span style="font-size:18px;">trap "rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit" 1 2</span>

几点注意:
  • 如果执行多个命令,需要将命令用引号包围;
  • 只有脚本执行到 trap 命令时才会捕获信号;
  • 再次接收到信号时还会执行同样的操作。

上面的脚本,执行到 trap 命令时就会替换 WORKDIR 和 $$ 的值。如果希望接收到 SIGHUP 或 SIGINT 信号时再替换其值,那么可以将命令放在单引号内,例如:

trap 'rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit' 1 2

2) 忽略信号

如果 trap 命令的 commands 为空,将会忽略接收到的信号,即不做任何处理,也不执行默认动作。例如:
$ trap '' 2
也可以同时忽略多个信号:
$ trap '' 1 2 3 15
注意:必须被引号包围,不能写成下面的形式:
$ trap  2

3) 恢复默认动作

如果希望改变信号的默认动作后再次恢复默认动作,那么省略 trap 命令的 commands 即可,例如:
$ trap 1 2
将恢复SIGHUP 和 SIGINT 信号的默认动作。




接下来谈谈僵尸进程:

比如进程采用exit()退出的时候,操作系统会进行一系列的处理工作,包括关闭打开的文件描述符、占用的内存等等,但是,操作系统也会为该进程保留少量的信息,比如进程ID号等信息,因而占用了系统的资源。在一种极端的情况下,档僵尸进程过多的时候,占用了大量的进程ID,系统将无法产生新的进程,相当于系统的资源被耗尽。


给进程设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时间获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)。

这里提到的这些信息等等都是将其写入到内核中的,该进程所占有的全部空间都已经释放掉了。。。。

如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(init进程将wait它们,从而去除僵尸状态)。

        但通常情况下,我们是不愿意留存僵尸进程的,它们占用内核中的空间,最终可能导致我们耗尽进程资源。那么为什么会产生僵尸进程以及如何避免产生僵尸进程呢?下边我将从这两个方面进行分析。

僵尸进程的原因

        我们知道,要在当前进程中生成一个子进程,一般需要调用fork这个系统调用,fork这个函数的特别之处在于一次调用,两次返回,一次返回到父进程中,一次返回到子进程中,我们可以通过返回值来判断其返回点:

pid_t child = fork();
if( child < 0 ) { //fork error.
perror("fork process fail.\n");
} else if( child ==0 ) { // in child process
printf(" fork succ, this run in child process\n ");
} else { // in parent process
printf(" this run in parent process\n ");
}

如果子进程先于父进程退出, 同时父进程又没有调用wait/waitpid,则该子进程将成为僵尸进程。通过ps命令,我们可以看到该进程的状态为Z(表示僵死),如图:

Linux 信号,僵尸进程,(面试)

让父进程休眠600s, 然后子进程先退出,我们就可以看到先退出的子进程成为僵尸进程了(进程状态为Z)


但是,但是:子进程如果处理的时间比较长的话,主进程会被挂起


避免产生僵尸进程:

  1. 父进程使用wait()或者waitpid()之类的函数等待子进程退出
  2. 父进程先产生一个子进程,然后子进程再产生一个孙子进程,子进程在孙子进程之前退出。
  3. 使用信号函数sigaction为SIGCHLD设置wait处理函数


我们知道了僵尸进程产生的原因,下边我们看看如何避免产生僵尸进程。


子进程的子进程的避免:

对于这样的情况可以采取连续fork()两次的方法。简而言之,首先父进程首先创建子进程,子进程创建孙子进程,由孙子进程处理事务,
而子进程再创建完孙子进程后,就退出。此时,孙子进程的父进程,也就是子进程退出了,因此孙子进程变为了一个孤儿进程,Linux进程处理
孤儿的进程的方式,是init进程接管孤儿进程,而init进程的子进程不会成为僵尸进程。

        一般,为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。如下代码所示:

void sig_chld( int signo ) {
pid_t pid;
int stat;
pid = wait(&stat);
printf( "child %d exit\n", pid );
return;
}

int main() {
signal(SIGCHLD, &sig_chld);
}

现在main函数中给SIGCHLD信号注册一个信号处理函数(sig_chld),然后在子进程退出的时候,内核递交一个SIGCHLD的时候就会被主进程捕获而进入信号处理函数sig_chld,然后再在sig_chld中调用wait,就可以清理退出的子进程。这样退出的子进程就不会成为僵尸进程。

        然后,即便我们捕获SIGCHLD信号并且调用wait来清理退出的进程,仍然不能彻底避免产生僵尸进程;我们来看一种特殊的情况:

        我们假设有一个client/server的程序,对于每一个连接过来的client,server都启动一个新的进程去处理来自这个client的请求。然后我们有一个client进程,在这个进程内,发起了多个到server的请求(假设5个),则server会fork 5个子进程来读取client输入并处理(同时,当客户端关闭套接字的时候,每个子进程都退出);当我们终止这个client进程的时候 ,内核将自动关闭所有由这个client进程打开的套接字,那么由这个client进程发起的5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个。server端接受到这5个FIN的时候,5个子进程基本在同一时刻终止。这就又导致差不多在同一时刻递交5个SIGCHLD信号给父进程,如图:

Linux 信号,僵尸进程,(面试)

正是这种同一信号多个实例的递交造成了我们即将查看的问题。       

我们首先运行服务器程序,然后运行客户端程序,运用ps命令看以看到服务器fork了5个子进程,如图:

Linux 信号,僵尸进程,(面试)

然后我们Ctrl+C终止客户端进程,在我机器上边测试,可以看到信号处理函数运行了3次,还剩下2个僵尸进程,如图:

Linux 信号,僵尸进程,(面试)

通过上边这个实验我们可以看出,建立信号处理函数并在其中调用wait并不足以防止出现僵尸进程,其原因在于:所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的 。 更为严重的是,本问题是不确定的,依赖于客户FIN到达服务器主机的时机,信号处理函数执行的次数并不确定。

       正确的解决办法是调用waitpid而不是wait,这个办法的方法为:信号处理函数中,在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,他告知waitpid在有尚未终止的子进程在运行时不要阻塞。(我们不能在循环内调用wait,因为没有办法防止wait在尚有未终止的子进程在运行时阻塞,wait将会阻塞到现有的子进程中第一个终止为止),下边的程序分别给出了这两种处理办法(func_wait, func_waitpid)。

Linux 信号,僵尸进程,(面试)



僵尸进程的深层次探究:


什么是僵尸进程

首先内核会释放终止进程(调用了exit系统调用)所使用的所有存储区,关闭所有打开的文件等,但内核为每一个终止子进程保存了一定量的信息。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。

而僵尸进程就是指:一个进程执行了exit系统调用退出,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态)的进程。

任何一个子进程(init除外)在exit后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号。


僵尸进程的目的?

设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。



如何避免僵尸进程?

  1. 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
  2. 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞waitpid可以通过传递WNOHANG使父进程不阻塞立即返回
  3. 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
  4. 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。


僵尸进程处理办法

1 wait()函数

#include <sys/types.h> 
#include <sys/wait.h>

pid_t wait(int *status);

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。 
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:

   pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  • wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
  • 返回的是子进程的PID,它通常是结束的子进程
  • 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
  • 如果status不是一个空指针,状态信息将被写入它指向的位置

可以上述的一些宏判断子进程的退出情况:

Linux 信号,僵尸进程,(面试)


2 waitpid()函数

#include <sys/types.h> 
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数:

status:如果不是空,会把状态信息写到它指向的位置,与wait一样

options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起

The value of options is an OR of zero or more  of  the  following  con- 
stants:

WNOHANG     return immediately if no child has exited.

WUNTRACED   also  return  if  a  child  has stopped (but not traced via 
            ptrace(2)).  Status for traced children which have  stopped 
            is provided even if this option is not specified.

WCONTINUED (since Linux 2.6.10) 
            also return if a stopped child has been resumed by delivery 
            of SIGCONT.

返回值:如果成功返回等待子进程的ID,失败返回-1


对于waitpid的p i d参数的解释与其值有关:

pid == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。

pid > 0 等待其进程I D与p i d相等的子进程。

pid == 0 等待其组I D等于调用进程的组I D的任一子进程。换句话说是与调用者进程同在一个组的进程。

pid < -1 等待其组I D等于p i d的绝对值的任一子进程

wait与waitpid区别:

  • 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
  • waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
  • 实际上wait函数是waitpid函数的一个特例。waitpid(-1, &status, 0);

 

示例:

如以下代码会创建100个子进程,但是父进程并未等待它们结束,所以在父进程退出前会有100个僵尸进程。

Linux 信号,僵尸进程,(面试)
#include <stdio.h>  
#include
<unistd.h>

int main() {

int i;
pid_t pid;

for(i=0; i<100; i++) {
pid
= fork();
if(pid == 0)
break;
}

if(pid>0) {
printf(
"press Enter to exit...");
getchar();
}

return 0;
}
Linux 信号,僵尸进程,(面试)

其中一个解决方法即是编写一个SIGCHLD信号处理程序来调用wait/waitpid来等待子进程返回。

 

Linux 信号,僵尸进程,(面试)
#include <stdio.h>  
#include
<unistd.h>
#include
<signal.h>
#include
<sys/types.h>
#include
<sys/wait.h>

void wait4children(int signo) {

int status;
wait(
&status);

}

int main() {

int i;
pid_t pid;

signal(SIGCHLD, wait4children);

for(i=0; i<100; i++) {
pid
= fork();
if(pid == 0)
break;
}

if(pid>0) {
printf(
"press Enter to exit...");
getchar();
}

return 0;
}
Linux 信号,僵尸进程,(面试)

但是通过运行程序发现还是会有僵尸进程,而且每次僵尸进程的数量都不定。这是为什么呢?其实主要是因为Linux的信号机制是不排队的,假如在某一时间段多个子进程退出后都会发出SIGCHLD信号,但父进程来不及一个一个地响应,所以最后父进程实际上只执行了一次信号处理函数。但执行一次信号处理函数只等待一个子进程退出,所以最后会有一些子进程依然是僵尸进程。

虽然这样但是有一点是明了的,就是收到SIGCHLD必然有子进程退出,而我们可以在信号处理函数里循环调用waitpid函数来等待所有的退出的子进程。至于为什么不用wait,主要原因是在wait在清理完所有僵尸进程后再次等待会阻塞。

 

所以最佳方案如下:

Linux 信号,僵尸进程,(面试)
#include <stdio.h>  
#include
<unistd.h>
#include
<signal.h>
#include
<errno.h>
#include
<sys/types.h>
#include
<sys/wait.h>

void wait4children(int signo) {
int status;
while(waitpid(-1, &status, WNOHANG) > 0);
}

int main() {

int i;
pid_t pid;

signal(SIGCHLD, wait4children);

for(i=0; i<100; i++) {
pid
= fork();
if(pid == 0)
break;
}

if(pid>0) {
printf(
"press Enter to exit...");
getchar();
}

return 0;
}
Linux 信号,僵尸进程,(面试)

这里使用waitpid而不是使用wait的原因在于:我们在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,它告诉waitpid在有尚未终止的子进程在运行时不要阻塞。我们不能在循环内调用wait,因为没有办法防止wait在正运行的子进程尚有未终止时阻塞。






本文转载:http://www.cnblogs.com/yuxingfirst/p/3165407.html

    :http://blog.chinaunix.net/uid-27064719-id-4757432.html

    :http://www.cnblogs.com/wuchanming/p/4020463.html