什么是信号(signal)?信号的处理机制?利用信号进行简单的进程通信过程设计(c++)?

信号(signal)

1、背景

在这里插入图片描述

信号(Signal)主要用来通知进程某个特定事件的发生,或者是让进程执行某个特定的处理函数。所以,信号可以说是进程控制的一部分。我们以普通的C语言程序(编译一个程序其实就是生成一个进程)为例:
在这里插入图片描述
在程序正常运行的过程中,当函数执行到exit,会根据情况退出当前函数执行,或者退出整个进程。或者进程执行陷入到while(true)循环当中,通过 Ctrl +C 通常会产生终端信号SIGINT,我们就可以终止当前进程,退出循环。

信号可以来自终端(terminal)的键盘字符输入,比如control-C触发的SIGINIT;也可以来自与硬件或软件有关的异常,比如应用程序访问了无效地址触发的SIGSEGV(segmentation fault),定时器到期触发的SIGALARM等。这些信号都是由内核发送给进程的。

2、定义

signal信号,又称为软中断信号,用来通知进程发生了异步事件

  • 进程之间可以互相通过系统调用kill发送软中断信号。
  • 内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
  • 信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
  • 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被 取消时才被传递给进程。

注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据

Linux 中定义了64中信号

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

目前Linux 中定义了64中信号,前期定了32种(1-31),后面的33种为实时信号(32-64

常用的几个信号说明:

	SIGINT:ctrl+c 终止信号
  SIGQUIT:ctrl+\ 终止信号
  SIGTSTP:ctrl+z 暂停信号
  SIGALRM:闹钟信号 收到此信号后定时结束,结束进程
  SIGCHLD:子进程状态改变,父进程收到信号
  SIGKILL:杀死信号

进程可以屏蔽掉大多数信号,除了SIGSTOP和SIGKILL

3、处理机制

3.1、信号来源

1、硬件方式

  • 当用户按某些终端键时,引发终端产生的信号,例如Ctrl +C 通常会产生终端信号SIGINT;
  • 硬件异常产生信号。除数为0、无效的内存引用等等,这些通常由硬件检测到,并将通知内核。然后内核会为正在运行的进程产生适当的信号。例如对执行一个无效内存引用产生SIGSEGV信号;

2、软件方式

简单介绍几种

kill  将信号sig 发送给pid 进程
killpg  发送信号sig 到pgrp 的所有进程中
raise   给当前进程发送信号sig
abort  给自己发送异常终止信号,(6.SIGABRO)终止并产生core文件
alarm  定时将产生SIGALRM信号给调用进程

3.2、信号的响应动作(前32种)

在这里插入图片描述
在这里插入图片描述
前32个信号都有默认的响应方式,分为以下5类:

  • Term 终止进程
  • Ign 忽略信号
  • Core 终止进程,并且产生core dump 文件
  • Stop 停止进程
  • Cont 如果进程处于stop状态,继续运行进程

当然只有5个响应方法怎么够呢,not fashion 于是sigaction()这个系统调用就上了,通过它可以给一个信号绑定一个函数来当作信号处理函数,你就可以在这个函数里面胡作非为了。可是你胡作非为了内核开发人员又感觉不爽了于是就设了两个信号你是改不了的,以显示他们不可动摇的地位,这两个信号就是9号SIGKILL和19号SIGSTOP,所以你也就不能定义Ctrl+c和Ctrl+z发送出来的信号的处理方式了。

用户进程对信号的响应方式:

  • 1,捕捉 (收到某个信号,做指定的动作,而不是做默认的)
  • 2,忽略 (收到某个信号,不做什么动作)
  • 3,阻塞 (收到某个信号,先做完当前事情,然后在响应信号)
  • 4,按照默认动作,SIGKILL,SIGSTOP不能被捕捉

3.3、信号在内核中的表示

信号在内核中一般有三种状态:

  • 1)信号递达(Delivery):实际执行信号的处理动作称为信号递达。
  • 2)信号未决(Pending):信号从产生到递达之间的状态。
  • 3)信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意:

阻塞与忽略是不同的,只有信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以模拟成下图:
在这里插入图片描述

  • block:是用来设置信号屏蔽的,如果里面对应的是1则表示该信号产生了,并且被屏蔽不能抵达,即如果block里面是1则,pending里面一定是1
  • pending:表示产生了一个信号。
  • handler表:是一个句柄函数指针,数组即可表示,下标表示信号编号,内容表示信号处理的动作,为NULL表示没有处理该信号。
  • 默认执行动作:表示这个信号一旦抵达根据默认执行动作的指示执行相关动作,如果用户自己设置了捕捉函数的话,则将执行用户指定的函数。

3.4、信号处理流程

在这里插入图片描述
在这里插入图片描述

3.4.0、信号发送

一个程序给另一个程序发了个短信,通过中国移不动或者中国联不通的网络,另一个程序的手机就收到了,一个信号就算发送成功了。

用于发送信号的函数有 raise、kill、 pthread kill、 sigqueue等

3.4.1、信号接收和存储(信号注册)

定义:
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中(进程PCB主要维护了一个进程描述符,里面有着pid呀,进程状态,所以信号也存在里面),同时向进程发送一个中断,使其陷入内核态。

  • 在linux里面每个进程都是按照进程描述符task_struct结构创建的,在进程描述符task_struct里面,有一项是Signal_Struct,在Signal_Strct这里面有一项list_head的描述符,在这里面有一个sigset_t表,定义了64种信号的所代表的含义。也就是说在每个进程中,都有一个表,里面存着各种信号所代表的含义。
  • 信号在进程中注册指的就是: 信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。
  • 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
  • pending 和 signal 是两个挂起信号队列,一个是私有的队列一个是共享的队列(这里不展开了)。主要关注是signal.
    在这里插入图片描述
    注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。

具体注册流程如下:

上面提到在进程的PCB表项中有一个软中断信号域,该域中每一位对应一个信号。内核给每一个进程发送软中断信号的方法,是在进程所在进程表项的信号域设置对应于该信号的位:

  • 如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程,否则仅设置进程表中信号域相应的位,而不唤醒进程。
  • 如果是发送给一个处于可运行状态的进程,则只设置相应的域即可。进程的task_struct结构中有关于本进程未决信号的数据成员,struct sigpending
struct sigpending{
		//每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
        struct sigqueue *head, *tail;//指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾
        
        sigset_t signal;//进程中所有未决信号集
};

struct sigqueue{
        struct sigqueue *next;
        siginfo_t info;
}

这里需要对两种信号就行说明:

  • 1、可靠信号

  • 当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"

  • 同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)

  • 信号值小于SIGRTMIN = 32的信号最多只注册一次

  • 2、不可靠信号

  • 当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"

  • 同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构

  • 信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册
    在这里插入图片描述

总结

信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号接收函数(signal()及sigaction())无关只与信号值有关

3.4.2、信号的检测

进程陷入内核态后,有两种场景会对信号进行检测

  • 进程从内核态返回到用户态前进行信号检测
  • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测

当发现有新信号时,便会进入下一步,信号的处理。

3.4.3、信号的查看与处理

信号处理函数是运行在用户态的:

  • 1、调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数
  • 2、接下来进程返回到用户态中,执行相应的信号处理函数
    • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为信号捕捉
  • 3、信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理
    • 所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip),将其指向中断前的运行位置,最后回到用户态继续执行进程。
    • 没有处理完,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行

至此,一个完整的信号处理流程便结束了,

3.4.4、简单实例过程说明

在这里插入图片描述
具体流程:

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

分析1
除过以上的解释之外,你是否注意了那几个特殊的ABCD坐标呢?没错,信号捕捉过程共发生了4次内核与用户态的切换,其中3与4是自定义捕捉函数引起的,可有可无。

分析2
除此之外,用户处理信号的时机为上图红色箭头所示,即内核态切换到用户态之时,为什么要选此时?

  • 信号不一定会被立即处理,操作系统不会为了处理一个信号而挂起当前正在运行的进程,这样产生的消耗太大(紧急信号【实时信号】可能会被立即处理)。
  • 操作系统选择在内核态切换到用户态的时候去处理信号,不要单独进行进程切换而浪费时间。
    • 但是有时候一个正在睡眠的进程突然收到信号,操作系统肯定不愿意切换当前正在运行的进程,预示就将该信号存在此进程的PCB的信号字段中,在合适的时候处理信号。

4、相关API函数

4.1、信号发送函数

1、kill():将信号sig 发送给pid 进程;

#include <signal.h>
 
int kill (pid_t pid, int sig);

返回值:

  • 如果失败,返回-1; 如果成功,返回0;

参数pid:

  • pid == 0,则发送sig 给调用该函数所属group 里所有的进程;
  • pid > 0,将信号sig 发送给进程ID 为pid 的进程;
  • pid < 0,将信号发送给其他进程组ID 等于pid 的绝对值;
  • pic == -1,将sig发送给发送进程有权限向它发送信号的系统上的所有进程;

失败原因:

  • 给定的信号无效(errno = EINVAL)
  • 发送权限不够( errno = EPERM )
  • 目标进程不存在( errno =ESRCH )
#include <stdio.h>
#include <signal.h>
static void usage(const char *proc)
{
	printf("Usage:%s sig pid\n",proc);//帮助手册
}
int main(int argc,char *argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		return 1;
	}
	int pid = atoi(argv[2]);//第三个命令行参数:进程的pid
	int sig =atoi(argv[1]);//第二个命令行参数:信号ID
	kill(pid,sig);
	return 0;
}

我们让死循环的程序在后台运行,其状态为R,当我们对其发送19号信号(暂停信号)时,由R状态变成了T状态,则证明模拟成功。
在这里插入图片描述

2、killpg 函数

#include <signal.h>
 
int killpg (pid_t pgrp, int sig);

发送信号sig 到pgrp 的所有进程中;如果pgrp 为0,则发送sig 给当前调用该函数所属group里所有的进程;

3、raise 函数:给当前进程发送信号sig

#include <signal.h>
 
int raise (int sig);

返回值:
成功返回0,失败返回-1。

实例:

#include <stdio.h>
#include <signal.h>
int count = 0;
void myhandler(int sig)
{
	printf("count:%d , sig:%d\n",count++,sig);
}
int main(int argc,char *argv[])
{
	signal(2,myhandler);
	while(1)
	{
		raise(2);
		sleep(1);
	}
	return 0;
}

在这里插入图片描述

4、alarm 函数
larm函数可以用来设置定时器,定时器超时将产生SIGALRM信号给调用进程。

#include <unistd.h>
 
unsigned int alarm(unsigned int seconds);

返回值:
前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。

参数
seconds:表示设定的秒数,经过seconds后,内核将给调用该函数的进程发送SIGALRM信号。

  • 如果seconds为0,则不再发送SIGALRM信号,最新一次调用alarm函数将取消之前一次的设定;
  • 如果seconds不为0,而上一次调用alarm还没有超时,那么将上次alarm 的余留值返回,并用新的alarm 代替上一次alarm;

实例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
	static int count = 0;
	printf("count:%d\n",count);
	if(++count < 5)
	{
		alarm(1);
	}
	else
	{
		printf("Complete...\n");
		exit(0);
	}
}
int main()
{
	signal(SIGALRM,handler);
	alarm(1);
	while(1){
		;
	}
	return 0;
}

在这里插入图片描述
我们使用signal函数设置了一个信号处理函数,只有进程收到一个SIGALRM信号,就异步调用该函数,中断main的while循环,当handler返回时,控制传递回main函数,它就从当初被信号到达时中断了的地方继续执行。

5、abort函数
abort可以使当前进程接收到信号而异常终止,但是abort会认为进程不安全。

#include <stdlib.h> 
void abort(void);

类似于exit函数一样,abort函数总是成功的,因此没有返回值。

4.2、信号捕捉函数(信号处理)

1、signal函数

#include <signal.h>
ypedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

返回值:
若成功则为指向前次处理程序的指针,若出错则为SIG_ERR。

参数说明:

  • signum:信号的编号。
  • handler:是一个函数指针,表示接受此信号要执行的函数的地址。
    • 1、SIG_IGN:忽略这个信号
    • 2、SIG_DFL:按照默认动作执行这个信号
    • 3、如果是一个函数,则是捕捉这个信号,收到这个信号的时候去执行这个函数。

上面是alarm()已经简单说明,这里不举例了。

sigaction()
检查并改变信号动作(捕捉信号,指定处理动作),也就是可以读取和修改与指定信号相关联的处理动作。

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

返回值:

成功返回0,失败返回-1

参数:

  • 1、signo:是指定信号的编号;
  • 2、act:信号动作结构体,用来登记对这个信号进行处理的动作。指针非空,则根据act修改该信号的处理动作。
 struct sigaction 
    {
     //不带参数的信号处理函数,默认执行这个不带参数的信号处理函数
       void     (*sa_handler)(int);   
       //带参的信号处理函数,如果想要使能这个信号处理函数, //需要设置一下sa_flags为SA_SIGINFO
       void     (*sa_sigaction)(int, siginfo_t *, void *);/
       
      //信号阻塞设置         
      //1,在信号处理函数执行的过程当中阻塞掉指定的信号,指定的信号过来将会被挂起,等函数结束后再执行               
      //2,sigset_t信号集合类型,参照sigprocmask中sigset的使用方式。                 
       sigset_t   sa_mask;        
                                                                                              
 	 //信号的操作标识,例如设置使用的是带参信号处理函数,还是不带参数的
       int        sa_flags;      
       void     (*sa_restorer)(void);    //被遗弃的设置
    };
  • 1、sa_handler:可以是常数SIG_DFL或者SIG_IGN,或者是一个信号处理函数名;
    • 赋值为常数SIG_IGN:表示忽略信号,
    • 赋值为常数SIG_DFL:表示执行默认动作,
    • 赋值为函数指针表示用自定义函数捕捉信号。
    • 该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这是一个回调函数,不是main函数调用,而是被系统所调用。
  • 2、sa_mask:是一个信号集,可以将信号加进进程的信号屏蔽字中。
    • 如果在调用信号处理函数时,除当前信号外,还希望屏蔽一些其他的信号,就可以使用sa_mask来屏蔽,仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。
  • 3、sa_flags:包含多个选项,一般设置为0即可。
//sa_falgs
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
  • 4、oact:用来保存原来的设置,如果是NULL则不保存。指针非空,则通过oact传出该信号原来的处理动作。

sigqueue():跟sigaction结合使用
将信号和数据发送到指定进程

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

返回值:

  • 成功:返回0,失败:返回-1,并设置error

参数说明:

  • pid:进程ID
  • sig:要发送的信号
  • value:附加数据
 union sigval 
       {
            int   sival_int;//只附加一个int数据
            void *sival_ptr;//附加更多数据(附加一个地址) 
        };
     //注:这是共用体、两者取其一

实例:
sigqueue程序向sigaction程序发送SIGUSR1(用户自定义信号 默认处理:进程终止)信号,并附带int型数据123sigaction捕捉到信号后,打印出信号发送方的PID和附加的数据,然后退出。

sigaction.c文件

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
 
 
void sigaction_handle(int signum, siginfo_t *info, void * ucontext)
{
 
	printf("info.si_pid=%d\n", info->si_pid);//打印信号发送方的pid
	printf("info.si_int=%d\n", info->si_int);//打印信号发送方的附加数据
	exit(0);
 
}
 
int main(void)
{
	int i;
	struct sigaction act, oldact;
	
	act.sa_flags = SA_SIGINFO;//使用带参数的信号处理函数
	act.sa_sigaction = sigaction_handle;
 
    //sigemptyset(&act.sa_mask);//清空原来集合
	//sigfillset(&act.sa_mask);//将所有信号添加到集合
 
	sigaction(SIGUSR1, &act, &oldact);//捕捉sigqueue发送的SIGUSR1信号
 
	for(i=0; i<30; i++)
	{
		printf("i=%d\n", i);
		sleep(1);
	}
	return 0;
}

sigqueue.c文件

#include <stdio.h>
#include <signal.h>
 
#include <sys/types.h>//getpid()
#include <unistd.h>
 
int main(int argc, const char *argv[])
{
	if(argc < 2)
	{
		printf("arg error\n");
		return -1;
	}
	union sigval value;
	value.sival_int = 123;
 
	int sig_num, pid;
	
	sscanf(argv[1], "%d", &pid);
	sigqueue(pid, SIGUSR1, value);//向指定的pid发送SIGUSR1信号
 
	printf("mypid=%d\n", getpid());//打印当前进程的ID号
 
	return 0;
}

在这里插入图片描述

参考

1、https://zhuanlan.zhihu.com/p/77598393
2、https://blog.csdn.net/shift_wwx/article/details/102549090
3、https://www.jianshu.com/p/4fd8e35a6580
4、https://blog.csdn.net/qq_41035588
5、https://blog.csdn.net/nanfeibuyi/article/details/81945408

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/107737137