信号(Signal)主要用来通知进程某个特定事件的发生,或者是让进程执行某个特定的处理函数。所以,信号可以说是进程控制的一部分。我们以普通的C语言程序(编译一个程序其实就是生成一个进程)为例:
在程序正常运行的过程中,当函数执行到exit,
会根据情况退出当前函数执行,或者退出整个进程。或者进程执行陷入到while(true)
循环当中,通过 Ctrl +C
通常会产生终端信号SIGINT
,我们就可以终止当前进程,退出循环。
信号可以来自终端(terminal)的键盘字符输入,比如control-C触发的SIGINIT
;也可以来自与硬件或软件有关的异常,比如应用程序访问了无效地址触发的SIGSEGV(segmentation fault)
,定时器到期触发的SIGALARM
等。这些信号都是由内核发送给进程的。
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
。
1、硬件方式
2、软件方式
简单介绍几种
kill 将信号sig 发送给pid 进程
killpg 发送信号sig 到pgrp 的所有进程中
raise 给当前进程发送信号sig
abort 给自己发送异常终止信号,(6.SIGABRO)终止并产生core文件
alarm 定时将产生SIGALRM信号给调用进程
当然只有5个响应方法怎么够呢,not fashion 于是sigaction()这个系统调用就上了,通过它可以给一个信号绑定一个函数来当作信号处理函数,你就可以在这个函数里面胡作非为了。可是你胡作非为了内核开发人员又感觉不爽了于是就设了两个信号你是改不了的,以显示他们不可动摇的地位,这两个信号就是9号SIGKILL和19号SIGSTOP,所以你也就不能定义Ctrl+c和Ctrl+z发送出来的信号的处理方式了。
用户进程对信号的响应方式:
信号在内核中一般有三种状态:
信号递达(Delivery)
:实际执行信号的处理动作称为信号递达。信号未决(Pending)
:信号从产生到递达之间的状态。信号阻塞(Block)
:被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意:
阻塞与忽略是不同的,只有信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以模拟成下图:
一个程序给另一个程序发了个短信,通过中国移不动或者中国联不通的网络,另一个程序的手机就收到了,一个信号就算发送成功了。
用于发送信号的函数有 raise、kill、 pthread kill、 sigqueue等
定义:
接收信号的任务是由内核代理
的,当内核接收到信号后,会将其放到对应进程的信号队列中(进程PCB主要维护了一个进程描述符,里面有着pid呀,进程状态,所以信号也存在里面),同时向进程发送一个中断,使其陷入内核态。
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结构
。
总结
信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号接收函数(signal()及sigaction())无关
,只与信号值有关
。
进程陷入内核态后,有两种场景会对信号进行检测
当发现有新信号时,便会进入下一步,信号的处理。
信号处理函数是运行在用户态
的:
内容备份拷贝到用户栈
上,并且修改指令寄存器(eip)将其指向信号处理函数执行相应的信号处理函数
信号捕捉
。检查是否还有其它信号未处理
。
所有信号都处理完成
,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip),将其指向中断前的运行位置,最后回到用户态继续执行进程。没有处理完,
如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行
至此,一个完整的信号处理流程便结束了,
分析1
除过以上的解释之外,你是否注意了那几个特殊的ABCD坐标
呢?没错,信号捕捉过程共发生了4次内核与用户态的切换
,其中3与4是自定义捕捉函数引起
的,可有可无。
分析2
除此之外,用户处理信号的时机为上图红色箭头
所示,即内核态切换到用户态之时,为什么要选此时?
1、kill():将信号sig 发送给pid 进程;
#include
int kill (pid_t pid, int sig);
返回值:
参数pid:
pid == 0
,则发送sig 给调用该函数所属group 里所有的进程;pid > 0
,将信号sig 发送给进程ID 为pid 的进程;pid < 0
,将信号发送给其他进程组ID 等于pid 的绝对值;失败原因:
#include
#include
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
int killpg (pid_t pgrp, int sig);
发送信号sig 到pgrp 的所有进程中;如果pgrp 为0,则发送sig 给当前调用该函数所属group里所有的进程;
3、raise 函数:给当前进程发送信号sig
#include
int raise (int sig);
返回值:
成功返回0,失败返回-1。
实例:
#include
#include
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
unsigned int alarm(unsigned int seconds);
返回值:
前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
参数
seconds
:表示设定的秒数,经过seconds
后,内核将给调用该函数的进程发送SIGALRM
信号。
实例:
#include
#include
#include
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
void abort(void);
类似于exit函数一样,abort函数总是成功的,因此没有返回值。
1、signal函数
#include
ypedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回值:
若成功则为指向前次处理程序的指针,若出错则为SIG_ERR。
参数说明:
signum
:信号的编号。handler
:是一个函数指针,表示接受此信号要执行的函数的地址。
SIG_IGN
:忽略这个信号SIG_DFL
:按照默认动作执行这个信号上面是alarm()已经简单说明,这里不举例了。
sigaction()
检查并改变信号动作(捕捉信号,指定处理动作),也就是可以读取和修改与指定信号相关联的处理动作。
#include
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
:表示执行默认动作,2、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
int sigqueue(pid_t pid, int sig, const union sigval value);
返回值:
参数说明:
pid
:进程IDsig
:要发送的信号value
:附加数据 union sigval
{
int sival_int;//只附加一个int数据
void *sival_ptr;//附加更多数据(附加一个地址)
};
//注:这是共用体、两者取其一
实例:
sigqueue程序向sigaction
程序发送SIGUSR1(用户自定义信号 默认处理:进程终止)
信号,并附带int型数据123
,sigaction
捕捉到信号后,打印出信号发送方的PID和附加的数据,然后退出。
sigaction.c文件
#include
#include
#include
#include
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
#include
#include //getpid()
#include
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