1. 信号的机制

进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号之后不管执行到程序的什么位置,都要暂停执行去处理信号,处理完信号之后再继续执行。
每个进程收到的所有信号都是由内核发送的。

进程A给进程B发送信号示意图:

1.1. 信号的状态

  信号有三种状态:产生、未决、递达。   

信号的产生:
(1) 按键产生,如:Ctrl+c、Ctrl+z等。
(2) 系统调用产生,如:kill、raise、abort。
(3) 软件条件产生,如:定时器alarm。
(4) 硬件异常条件,如:非法访问内存(段错误)、除0(浮点数除外)、内存对齐出错(总线错误)。
(5) 命令产生,如:kill命令。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
递达:递送并且到达进程。

1.2. 信号的处理方式

(1)执行默认动作。
(2)忽略信号(丢弃不处理)。
(3)捕捉信号(调用用户的自定义的处理函数)。

1.3. 信号的特质

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。(注:表示PCB的task_struct结构体定义在:/usr/src/linux-headers-4.4.0-97/include/linux/sched.h:1390)

1.4. 阻塞信号集和未决信号集

阻塞信号集中保存的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号集中的某些信号,这些信号需要暂时被阻塞,不予处理。
信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态;若是信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。

1.5. 信号的四要素(通过man 7 signal查看)

1.5.1. 信号的编号

  使用kill -l命令可以查看当前系统有哪些信号,不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。

1.5.2. 信号的名称

1.5.3. 产生信号的事件

1.5.4. 信号的默认处理动作:

  	Term:终止进程
  	Ign:忽略信号 (默认即时对该种信号忽略操作)
  	Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
  	Stop:停止(暂停)进程
  	Cont:继续运行进程

注意:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

2. 信号相关函数

2.1. signal函数

  • 函数作用:注册信号捕捉信号。

  • 函数原型:

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    
  • 函数参数:
    signum:信号编号
    handler:信号处理函数

2.2. kill函数/命令

  • 描述:给指定进程发送指定信号

  • kill命令:kill -SIGKILL 进程PID

  • kill函数原型:

    int kill(pid_t pid, int sig);
    
  • 函数返回值:成功返回0,失败返回-1,设置errno值。

  • 函数参数:
    sig信号参数:尽量使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
    pid参数:

    • pid>0:发送给指定的进程。
    • pid=0:发送信号给调用kill函数的进程同一进程组的所有进程。
    • pid<-1:取|pid|发给对应进程组。
    • pid=-1:发送给进程有权限发送的系统中所有进程。
      (进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。)

2.3. raise函数、abort函数

raise:

  • 函数描述:给当前进程发送指定信号(自己给自己发)

  • 函数原型:

    int raise(int sig);
    
  • 函数返回值:成功返回0,失败返回非0值。

  • 函数拓展:raise(signo) == kill(getpid(), signo);

abort:

  • 函数描述:给自己发送异常终止信号(6)SIGANRT),并产生core文件。

  • 函数原型:

    void abort(void);
    
  • 函数拓展:abort() == kill(getpid(), SIGABRT);

2.4. alarm函数

  • 函数原型:

    unsigned int alarm(unsigned int seconds); 
    
  • 函数描述:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。

  • 函数返回值:返回0或剩余的秒数,无失败。
    如:

  • 注意:alarm使用的是自然定时法,与进程状态无关,就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。取消定时器:alarm(0),返回旧闹钟余下秒数

2.5. setitimer函数

  • 函数原型:

    int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
    
  • 函数描述:设置定时器,可代替alarm函数,精度微妙us,可以实现周期定时。

  • 函数返回值:成功返回0,失败返回-1,并设置errno值。

  • 函数参数:
    which:指定定时方式:

    • 自然定时:ITIMER_REAL -> 14)SIGALRM 计算自然时间
    • 虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26)SIGVTALRM 只计算进程占用CPU的时间
    • 运行时计时(用户+内核):ITIMER_PROF -> 27)SIGPROF 计算占用CPU及执行系统调用的时间

new_value:struct itimerval,负责设定timeout时间

  • itimerval.it_value:设定第一次执行function所延迟的秒数。

  • itimerval.it_interval: 设定以后每几秒执行function。

        struct itimerval { 
      struct timerval it_interval; // 闹钟触发周期
      struct timerval it_value; // 闹钟触发时间
    }; 
    struct timeval { 
      long tv_sec; 			// 秒
      long tv_usec; 			// 微秒
    

    }

old_value:存放旧的timeout值,一般指定为NULL。

3. 信号集

3.1. 未决信号集和阻塞信号集的关系

阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。

以SIGINT为例说明未决信号集阻塞信号集的关系:

当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:

  • 如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;
  • 如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。

当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。

3.2. 信号集相关函数

由于信号属于内核中的一块区域,用户不能直接操作内核空间,为此内核提供了一些信号集相关的接口函数,使用这些函数,用户就可以完成对信号集的相关操作。
信号集是一个能表示多个信号的数据类型,sigset_t set ,set即为一个信号集。

        int sigemptyset(sigset_t *set);
  函数说明:将某个信号集清0,初始化信号集	 	
  函数返回值:成功:0;失败:-1,设置errno

        int sigfillset(sigset_t *set);
  函数说明:将某个信号集置1,即把该信号集中所有位置为1。
  函数返回值:成功:0;失败:-1,设置errno

        int sigaddset(sigset_t *set, int signum);	
  函数说明:将某个信号加入信号集合中
  函数返回值:成功:0;失败:-1,设置errno

        int sigdelset(sigset_t *set, int signum);		
  函数说明:将某个信号从集合中删除  	
  函数返回值:成功:0;失败:-1,设置errno

        int sigismember(const sigset_t *set, int signum);
  函数说明:判断某个信号是否在信号集中
  函数返回值:在:1;不在:0;出错:-1,设置errno

        sigprocmask函数
  函数说明:用来屏蔽信号,解除屏蔽也使用该函数。将本地信号集中的信号加入到内核中的阻塞信号集中。
  **特别注意,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。**
  函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  函数返回值:成功:0;失败:-1,设置errno

  函数参数:
  how参数取值:假设当前的信号屏蔽字为mask
  SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
  SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
  SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
  set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
  oldset:传出参数,保存旧的信号屏蔽字。

  sigpending函数
  函数原型:int sigpending(sigset_t *set);	   
  函数说明:读取当前进程的未决信号集
  函数参数:set传出参数
  函数返回值:成功:0;失败:-1,设置errno

  //信号集相关函数测试
  #include <stdio.h>
  #include <stdlib.h>
  #include <sys/types.h>
  #include <unistd.h>
  #include <signal.h>

  //信号处理函数
  void sighandler(int signo)
  {
  	printf("signo==[%d]\n", signo);
  }

  int main()
  {

  	//注册信号处理函数
  	signal(SIGINT, sighandler);
  	signal(SIGQUIT, sighandler);

  	//定义信号集变量
  	sigset_t set;
  	sigset_t oldset;

  	//初始化信号集
  	sigemptyset(&set);
  	sigemptyset(&oldset);

  	//将SIGINT SIGQUIT加入到set集合中
  	sigaddset(&set, SIGINT);	
  	sigaddset(&set, SIGQUIT);	

  	//将set集合中的SIGINT SIGQUIT信号加入到阻塞信号集中
  	//sigprocmask(SIG_BLOCK, &set, NULL);
  	sigprocmask(SIG_BLOCK, &set, &oldset);

  	int i=0;
  	int j=1;
  	sigset_t pend;

  	while(1)
  	{
  		//获取未决信号集
  		sigemptyset(&pend);
  		sigpending(&pend);
  
  		for(i=1; i<32; i++)
  		{
  			if(sigismember(&pend, i)==1)
  			{
  				printf("1");
  			}
  			else
  			{
  				 printf("0");
  			}
  		}	
  		printf("\n");      
  
  		//循环10次解除对SIGINT SIGQUIT信号的阻塞
  		if(j++%10==0)
  		{
  			//sigprocmask(SIG_UNBLOCK, &set, NULL);	
  			sigprocmask(SIG_SETMASK, &oldset, NULL);
  		}
  	      	else
  		{
  			sigprocmask(SIG_BLOCK, &set, NULL);
  		}
  
  		sleep(1);
  	}		

  	return 0;
  }

4.信号捕捉函数

4.1. signal函数

4.2. sigaction函数

  int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 函数说明:注册一个信号处理函数

  • 函数参数:

    • signum:捕捉的信号

    • act:传入参数,新的处理方式

    • oldact:传出参数,旧的处理方式

          struct sigaction {
           void  (*sa_handler)(int);	// 信号处理函数
           void  (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数
           sigset_t  sa_mask; //信号处理函数执行期间需要阻塞的信号
           int      sa_flags; //通常为0,表示使用默认标识
           void     (*sa_restorer)(void);
          };
      
  • 总结:

    • sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
    • sa_mask: 用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
    • sa_flags:通常设置为0,使用默认属性。
    • sa_restorer:已不再使用

5. 内核实现信号捕捉的过程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

  1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
  2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
  3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
  4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
  6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

6. SIGCHILD信号

6.1 产生SIGCHILD信号的条件

  • 子进程结束的时候
  • 子进程收到SIGSTOP信号
  • 当子进程停止时,收到SIGCONT信号

6.2 SIGCHILD信号的作用

子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号后可以对子进程进行回收。
使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。

父进程回收子进程:

  //父进程使用SIGCHLD信号完成对子进程的回收
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <sys/types.h>
  #include <unistd.h>
  #include <signal.h>
  #include <sys/wait.h>

  //SIGCHLD信号处理函数
  void waitchild(int signo)
  {
  	pid_t wpid;

  	while(1)
  	{
  		wpid = waitpid(-1, NULL, WNOHANG);
  		if(wpid>0)
  		{
  			printf("child is quit, wpid==[%d]\n", wpid);
  		}
  		else if(wpid==0)
  		{
  			printf("child is living, wpid==[%d]\n", wpid);
  			break;
  		}
  		else if(wpid==-1)
  		{
  			printf("no child is living, wpid==[%d]\n", wpid);
  			break;
  		}
  	}
  }

  int main(int argc, char *argv[])
  {
  	pid_t pid;
  	int i = 0;

  	//将SIGCHLD信号阻塞
  	sigset_t mask;
  	sigemptyset(&mask);
  	sigaddset(&mask, SIGCHLD);
  	sigprocmask(SIG_BLOCK, &mask, NULL);

  	for(i=0; i<3; i++)
  	{
  		pid = fork();
  		if(pid<0)
  		{
  			perror("fork error");
  			return -1;
  		}
  		else if(pid>0)
  		{
  			printf("father process, pid==[%d], child pid==[%d]\n", getpid(), pid);	
  		}
  		else
  		{
  			printf("child process, father pid==[%d], pid==[%d]\n", getppid(), getpid());
  			break;
  		}
  	}

  	if(i==0)
  	{
  		printf("the first child, pid==[%d]\n", getpid());
  	}      

  	if(i==1)
  	{
  		printf("the second child, pid==[%d]\n", getpid());
  	}

  	if(i==2)
  	{
  		printf("the third child, pid==[%d]\n", getpid());
  	}

  	if(i==3)
  	{
  		printf("the father, pid==[%d]\n", getpid());

  		//注册SIGCHLD信号处理函数
  		struct sigaction act;
  		act.sa_handler = waitchild;
  		sigemptyset(&act.sa_mask);
  		act.sa_flags = 0;
  		sleep(5);
  		sigaction(SIGCHLD, &act, NULL);

  		//完成SIGCHLD信号的注册后, 解除对SIGCHLD信号的阻塞
  		sigprocmask(SIG_UNBLOCK, &mask, NULL);

  		while(1)
  		{
  			sleep(1);
  		}
  	}

  	return 0;
  }

注意点:

  • 有可能还未完成信号处理函数的注册三个子进程都退出了。
    • 解决办法:可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后在解除阻塞。
  • 当SIGCHLD信号函数处理期间, SIGCHLD信号若再次产生是被阻塞的,而且若产生了多次, 则该信号只会被处理一次, 这样可能会产生僵尸进程。
    • 解决办法: 可以在信号处理函数里面使用while(1)循环回收, 这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而可以避免产生僵尸进程。