文章目录
一、什么是信号
1.信号的定义
信号相当于软件中断,并具有如下特征
- 进程虽然现在没有受到信号,但是进程一定知道如何识别信号和做出相应反应。程序员在设计进程的时候已经内置了处理方案,信号是进程中特有的特征。
- 进程在收到信号时可能不会立即处理,因为有优先级更高的事情。如果这样需要先把信号记录下来,等合适的时候再处理。
- 进程处理信号有三种方式:(1)默认行为:终止进程,暂停,继续运行等。(2)自定义行为,由我们自己编写。这种方式也称为捕捉一个信号(3)忽略信号。
2.信号的记录
在学习进程时候,我们应该了解了每个进程都对应一个task_struct(PCB),在这个task_struct中,记录着进程的各种信息,各种信息中同样也包括信号的记录。信号在task_struct中是以位图的方式记录的,task_struct有变量signal,可以把它的类型理解成无符号整数。比特位的位置为信号编号,比特位的内容为是否收到信号,假如收到6号信号就会把第六个比特位置1。我们可以通过下面的命令查看信号编号。
命令:kill -l 功能:查看系统定义的信号列表
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
- 31号之前为普通信号,34号之后为实时信号
3.信号的发送
进程收到信号,本质上是进程task_struct中的信号位图被改了。那么谁有权限改进程内的东西呢?答案当然是OS,操作系统是进程的管理者,拥有绝对的权限。所以信号发送的本质就是操作系统修改了进程的信号位图,进程根据修改后位图的值做出相应处理。
二、信号捕获
1.signal函数
1.函数功能
设置某一信号的对应动作
2.函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
3.参数说明
第一个参数signum:指明了所要处理的信号类型的编号,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler:描述了与信号关联的动作,它可以取三种值。
参数handler取值:
- SIG_IGN :表示忽略该信号
- SIG_DFL:表示恢复对信号的系统默认处理
- sighandler_t类型的函数指针 :此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为sig的信号时,就执行handler 所指定的函数。(int)signum(信号类型编号)是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行handler 所指定的函数。当handler 所指定的函数执行结束后,控制权返回进程被中断的那一点继续执行。
4.返回值
返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
注意:
- 有些信号是不能被捕捉的,因为如果所有的信号都被捕捉,那么操作系统就再也没有办法杀死进程了。
- 信号会打断阻塞的系统调用,例如sleep,write,open,read等
5.代码演示
例如:程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。该信号关联的动作为杀死该进程,我们修改其关联动作
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <signal.h>
4 #include <unistd.h>
5 void signal_fun(int signum)
6 {
7 printf("输入信号的编号为:%d\n",signum);
8 }
9 int main()
10 {
11
12 signal(SIGINT,signal_fun);
13
14 while(1)
15 {
16 write(1,"*",1);
17 sleep(1);
18 }
19 exit(0);
20 }
运行结果如下
注意这时Ctrl-C已经不能够终止程序运行,其关联功能为输出一段字符串
三、信号响应过程
1.信号响应流程
1.内核为每个进程给了两个位图,一个为mask(默认为1),一个为pending(默认为0)
2.当内核态转到用户态时,检测是否有信号,用mask & pending得到结果,如果没有接到信号,那么结果为32位的0,此时通过内核中保存的地址返回原进程。
3.当有一个信号来时,pending中对应位置变为1,当一个中断到来时,在内核中保存原进程的地址等待调度,当调度到时,从内核态转为用户态,此时由于pengding& mask的值不全为0,所以也就发现了信号,也就能够执行signal了。
4.此时替换内核中地址,将要执行的signal参数中的函数地址装进,设置mask和pengding的对应位为0
5.执行完signal中的函数后,将内核中的地址换为之前函数的地址,设置mask为1.最后通过将内核态转为用户态执行之前的进程。
思考:1.如何忽略一个信号?
在signal中有一个参数设置为忽略模式后,在信号对应的位置的mask设置为0,即可永久屏蔽信号
2.信号从收到到相应有一个不可忽略的时延
响应是在内核态转用户态时候发生的,所以需要等有中断或者系统调用进入内核,再由内核态到用户态的过程中进行信号响应。
也就是说:对一个进程发送一个信号以后,其实并没有硬中断发生,只是简单把信号挂载到目标进程的信号 pending 队列上去,信号真正得到执行的时机是进程执行完异常/中断返回到用户态的时刻。 让信号看起来是一个异步中断的关键就是,正常的用户进程是会频繁的在用户态和内核态之间切换(这种切换包括:系统调用、缺页异常、系统中断…),所以信号能很快的能得到执行。
3.标准信号
- 标准信号会丢失
因为如果一次性发送1W个信号的话,位图的同一个位置只有一个1存在,也就是只能在中断后,在内核转向用户态的时候响应一次
2.标准信号的响应没有严格的先后顺序
如果有多个标准信号都需要响应则先响应哪个都可以
四、信号中的常用函数
1.kill()函数
1.函数功能
kill命令:kill 进程pid 信号编号
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
2.kill()函数原型
1
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signo);
signo为信号编号。成功返回0,失败返回-1。
2.raise()函数
1.函数功能
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
2.函数原型
#include <signal.h>
> int raise(int signo);
成功返回0,失败返回-1。
相当于kill(getpid(),signo);
3.abort()函数
函数功能:
向自己发送6号信号SIGABRT(退出指令)
函数原型
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
注意:SIGABRT可以被捕捉,但是捕捉之后依然会让进程终止,这就是SIGABRT的特点
- kill(),raise(),abort(0)都是调用系统函数向进程发信号
4.alarm()函数
函数功能
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。我们也可以指定其处理动作
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm的返回值是0或者是剩余秒数。如果闹钟被提前唤醒,返回值为剩余秒数,否则是0。
alarm()函数在计时时间,不会重新装填;也就是说如果使用signal()函数指定SIGALRM信号处理动作,也只能执行一次,如果想多次执行SIGALRM信号的处理动作(即执行摸某个函数),可以按照函数实现:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <signal.h>
5 void test(int a)
6 {
7 printf("信号编号为%d\n",a);
8 alarm(2);
9 }
10 int main()
11 {
12 alarm(1);
13 signal(SIGALRM,test);
14 while(1);
15 }
开始进入test函数为1s,之后每2s进入一次test函数 ,类似setitimer()函数
5.setitimer()函数
函数功能:
setitimer也可以创建定时器,和alarm函数相比,最显著的区别就是它可以指定到微秒,而且可以循环发送
函数原型
#include <sys/time.h>
int setitimer(int which,
const struct itimerval *new_value,
struct itimerval *old_value);
参数说明
- 1、which参数用来设置定时器类型,可选的值为
(1)ITIMER_REAL : 设置定时器以系统真实所花费的时间来计时,运行指定时间后发送SIGALRM信号。
(2)ITIMER_VIRTUAL : 设置进程在用户空间中执行时,时间计数减少。运行指定时间后发送SIGVTALRM信号
(3)ITIMER_PROF : 设置进程在内核空间中执行时,时间计数减少。运行指定时间后发送SIGPROF信号。
-
2、new_value参数 : 用来对定时器的定时时间进行设置
-
3、old_value参数 :很少使用,常常设置为NULL。它是用来存储上一次setitimer调用时设置的new_value值。
itimerval结构体:
struct timeval
{
time_t tv_sec; /* 秒*/
suseconds_t tv_usec; /* 微秒(10^(-6))*/
};
struct itimerval
{
struct timeval it_interval; /* 再次产生定时器信号的时间 */
struct timeval it_value; /* 首次产生定时器信号的时间 */
};
settimer的机制:先对it_value以which参数设置的方式倒计时,当it_value为零时就会发送信号。然后it_value会被重置为it_interval的值。最后重新开始新一轮的定时,按照这种方式一直循环下去。
注意:
1、it_value的值为0,将不会发送信号。因此要想能发送信号,it_value的值得大于0
2、it_interval的值为0,定时器只会发送一次信号,即只能延时,不能定时。
- 代码演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
void f1()
{
printf("time is coming\n");
}
int main()
{
struct itimerval time;
time.it_value.tv_sec=3;
time.it_value.tv_usec=0;//第一次产生定时时间为1s
time.it_interval.tv_sec=0;
time.it_interval.tv_usec=500000;//再次产生定时时间为500000us=500ms=0.5s
signal(SIGALRM,f1);
setitimer(ITIMER_REAL,&time,NULL);
while(1)
{
write(1,"*",1);
sleep(1);
}
}
运行结果为:
7.信号集处理函数
sigset_t:信号集,用来描述信号的集合,linux 所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。
- 函数功能
设定我们需要处理的信号,将其至于一个集合之中
- 函数原型
#include <signal.h>
int sigemptyset(sigset_t *set); 清空信号集,将信号集全部置0
int sigfillset(sigset_t *set); 将信号集全部置1
int sigaddset(sigset_t *set, int signo) 把信号集的某一个信号位置1
int sigdelset(sigset_t *set, int signo); 把信号集的某一个信号位置0;
int sigismember(const sigset_t *set, int signo); 判断这个信号集的某个信号位是否置位
注意
在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。上面四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
- 信号的表示:
实际执行信号的处理动作(3种)称为信号递达;
信号从产生到递达之间的状态,叫做信号未决;
进程可以选择阻塞某个信号;
被阻塞的信号产生时,将保持在未决状态,直至进程取消对该信号的阻塞,才执行递达的动作;
注意:阻塞和忽略是不同的。只要信号阻塞就不会被递达;而忽略是信号在递达之后的一种处理方式。
- 信号在内核中的表示
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针(handler)表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直至信号递达才清除该标志。操作系统向进程发送信号就是将pending位图中的该信号对应状态位由0变为1。
注意:在Linux下,如果进程解除某信号的阻塞之前,该信号产生了很多次,它的处理方法是:若是常规信号,在递达之前多次产生只计一次;若是实时信号,在递达之前产生多次则可以放在一个队列里。下面提到的信号都是常规信号。
- 信号集
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次。同样的,阻塞标志也是这样表示的。所以阻塞和未决标志我们可以采用相同的数据类型sigset_t来存储,sigget_t称为信号集。
- 信号屏蔽字
数据类型sigset_t可以表示每个信号的“有效”、“无效”状态。在未决信号集中,“有效”、“无效”表示该信号是否处于未决状态;在阻塞信号集中,“有效”、“无效”表示该信号是否被阻塞。阻塞信号集也叫做当前进程的信号屏蔽字。
调用sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。
8.sigprocmask()函数
- 函数功能
读取或更改进程的信号屏蔽字(阻塞信号集)
- 函数原型:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 参数说明
首先,若oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
若set是空指针,则不改变该进程的信号屏蔽字,how的值也无意义了。
How参数 | 说明 |
---|---|
SIG_BLOCK | 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的的并集,set包含了我们希望阻塞的附加信号 |
SIG_UNBLOCK | 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号补集的交集,set包含了我们希望解除阻塞的信号 |
SIG_SETMASK | 该进程新的信号屏蔽字将被set指向的信号集的值代替 |
下面表格说明了how可选用的值。注意,不能阻塞SIGKILL和SIGSTOP信号。
- 返回值
返回值:成功返回0,出错返回-1
- 代码演示
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <signal.h>
4 #include <unistd.h>
5 void signal_fun(int signum)
6 {
7 printf("输入信号的编号为:%d\n",signum);
8 }
9 int main()
10 {
11 int i,j;
12 sigset_t set;
13 sigemptyset(&set);
14 signal(SIGINT,signal_fun);
15 sigaddset(&set,SIGINT);
16 for(j=0;j<100;j++)
17 {
18 sigprocmask(SIG_BLOCK,&set,NULL);
19 for(i=0;i<5;i++)
20 {
21 write(1,"*",1);
22 sleep(1);
23 }
24
25 sigprocmask(SIG_UNBLOCK,&set,NULL);
26 }
27 exit(0);
28 }
9. sigpending()函数
函数功能
该函数的作用是将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,成功调用返回0,否则返回-1,并设置errno表明错误原因。
函数原型
#include <signal.h>
int sigpending(sigset_t *set);
返回值
成功:0;失败:-1,设置 errno
10.sigsuspend()函数
- 函数功能:
sigsuspend() 函数可以更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。
使用这种技术可以保护不希望由信号中断的代码临界区。
如果希望对一个信号解除阻塞,然后pause等待以前被阻塞的信号发生,那么必须使用 sigsuspend() 函数
pause() 函数无法达成上述目的
- 函数原型
#include <signal.h>
int sigsuspend(const sigset_t *mask);
参数说明:
*mask: 设置新的mask阻塞或解除阻塞当前进程
返回值
sigsuspend返回后将恢复调用之前的的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR.
注意
进程执行到sigsuspend时,sigsuspend并不会立刻返回,进程处于TASK_INTERRUPTIBLE状态并立刻放弃CPU,
等待UNBLOCK(mask之外的)信号的唤醒。进程在接收到UNBLOCK(mask之外)信号后,调用处理函数,然后还原信号集,sigsuspend返回,进程恢复执行。
- 与pause函数区别
如果在等待信号阶段,发送信号给程序,那么 pause()函数退出,处理信号
如果在解除阻塞之前发送信号,信号一被解除,立马执行信号处理函数,执行完毕后,回来执行 pause()函数,程序被卡住
为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由 sigsuspend() 函数提供的。
11.sigaction()函数
- 函数原型
int sigaction(int signum,
const struct sigaction *act,
struct sigaction *oldact);
- 参数说明
signum 参数指出要捕获的信号类型,act 参数指定新的信号处理方式,oldact 参数输出先前信号的处理方式(如果不为 NULL
的话)。
- struct sigaction 结构体介绍
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
- sa_handler:此参数和 signal() 的参数 handler 相同,代表新的信号处理函数
- sa_sigaction:另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。当 sa_flags 成员的值包含了SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler作为信号处理函数。在某些系统中,成员 sa_handler 与 sa_sigaction 被放在联合体中,因此使用时不要同时设置。
- sa_mask:成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。
- sa_flags:用来设置信号处理的其他相关操作,下列的数值可用:
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。 SA_RESTART:使被信号打断的系统调用自动重新发起。
SA_NODEFER:一般情况下,当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER
标记,那么在该信号处理函数运行时,内核将不会阻塞该信号。即在信号处理函数执行期间仍能发出这个信号。
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。 re_restorer
成员则是一个已经废弃的数据域,不要使用。
五、实时信号
1.实时信号位置
1-31为标准信号
34-64为实时信号
2.标准信号与实时信号的区别
每个进程拥有一个信号等待队列。在 task_struct 中有一个 struct sigpending pending
域,就是进程的信号等待队列。
当向一个进程发送信号时,信号会先被送入进程的信号等待队列,然后等到进程被调度到去处理信号的时候,会从信号等待队列中依次取出信号进行处理。
- 标准信号不能排队,而实时信号可以排队:
假设进程屏蔽了一个标准信号,当给它连续发送多个相同的标准信号,则只有第一个被放入进程的信号等待队列中,后续的都被丢弃。
假设进程屏蔽了一个实时信号,当给它连续发送多个相同的实时信号,则所有的信号都被放入进程的信号接收队列中。
- 也就是说:
1.标准信号会丢失,实时信号不会丢失
2.标准信号没有严格的响应顺序,实时信号到来会进行排队,有响应顺序 信号排队