信号的捕捉:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
信号的处理有三种方式。
那么它是在什么时候处理信号的呢?
我们来看一张图。
0,一张图,两半,上为用户态(运行态),下面为内核态(管理态)。
1, 上图为信号的捕捉,处理流程。
2,图中3,4 是为了处理用户自定义的句柄。
3,图中有4个内核与用户的切换。
4,用户处理信号的时机:从内核态切回用户态时。
mysleep
在模拟实现sleep之前先介绍三个函数。
一,sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。
调用成功则返回0,出错则 返回- 1。
signo是指定信号的编号。
若act指针非空,则根据act修改该信号的处理动作。
若oact指针非空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction)(int, siginfo_t *, void*);
}
1,关于sa_handler
将sa_handler赋值为常数SIG _IGN传给sigaction表示忽略信号,
赋值为常数SIG_DFL 表示执行系统默认动作,
赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册 了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信 号的编号,这样就可以用同一个函数处理多种信号。
显然,这也是一个回调函数,不是被main 函数调用,而是被系统所调用。
2,当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产 生,那么它会被阻塞到当前处理结束为止。(防止多次信号)
3,如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则 用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号 屏蔽字。
二:pause
#include <unistd.h>
int pause(void);
pause函数使调用进程挂起直到有信号递达。
如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;
如果信号的处理动作是忽略,则进程继续处于挂起状态,pause 不返回;
如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为 EINTR, 所以pause只有出错的返回值。错误码 EINTR表示“被信号中断”。
三:alarm用法:
alarm和SIGALRM
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
作用:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发 SIGALRM信号, 该信号的默认处理动作是终止当前进程。
返回值:这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
四,模拟实现mysleep
1,main函数调用mysleep函数,后者调用sigaction注册了SIGALRM信号的处理函数 sig_alrm。
2,调用alarm(nsecs)设定闹钟。
3,调用pause等待,内核切换到别的进程运行。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void myhandler(int sec)
{}
int mysleep(int second)
{
int ret;
int test = 100; //这句为了验证pause 干了什么。
sigset_t sig; //前两句符合C语言编程规范,把需要定义的变量放前面。
struct sigaction new, old; //设置两个处理动作,为后面的注册处理函数而设值的。
new.sa_handler = myhandler; //设置结构体(内读到信号时所)调用处理方式(注意指针类型)。
new.sa_flags = 0; //默认给0
sigemptyset(&sig); //初始化信号集,因为不需要屏蔽任何信号。
new.sa_mask = sig; // (然后把这个不屏蔽任何信号的 )信号集给这个结构体内
sigaction(SIGALRM, &new, &old); //注册 处理函数动作
alarm(second); //设置一个闹钟,秒数已经给定,继续跑,在结束发信号SIGALRM
test = pause(); //跑到这一步,程序就挂起了,就需要等待信号。
printf("%d\n", test);// -1,如果不给结束的信号,处理动作向下执行。证明这里接受的是SIGALRM
ret = alarm(0); /*取消闹钟。//在这个程序里面,取消这个闹钟是可以跑通的,但是如果我们在其他地方加代码呢?比如说在有闹钟的同时,我们在跑其他的代码,闹钟的异常会引起很多问题,为了编写优良,我们是需要加上取消闹钟的。*/
sigaction(SIGALRM, &old, NULL);//恢复默认的处理动作。
return ret; //0
}
int main()
{
while(1)
{
printf("oh,no,wait me 3s more!\n");
mysleep(3);
}
return 0;
}
五:advance_mysleep
question:
系统频繁的切换进程的时候是很容易出现错误的!
如果有一个优先级更高的线程在 我们这个程序的alarm和pause 之间跑起来呢?
这个线程里面有一个时间很长的alarm, 我们这个程序的闹钟超时了(但是在此期间并没有响应),内核发送SIGALRM信号给这个进程,处于未决状态。
优先级高的线程执行结束,内核要调度回这个进程开始执行。SIGALRM信号递达,执行处理函数之后再次进入内核; 返回这个进程的主控制流。我们的SIGALRM刚出来就处理了。然后再pause,这个时候就是机场等一艘轮船。
也就是说 虽然alarm(times)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(times)之后的secs秒之内被调用。如果接受不到他的信号那么它将永远挂起。
由于异步事件在任何时候都有可能发生 (这里的异步事件指出现更优 先级的进程),如果写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件。
改善:
在调用pause之前屏蔽SIGALRM信号使它不能提前递达就可以了。
但是你屏蔽了就要解除,解除后如果再等待,也可能发生异步的问题。
引入函数:
sigsuspend(包含了pause的挂起等待功能,同时解决了竞态条件的问题),在存在时序问题的场合下都应该调用sigsuspend而不是pause。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
和pause一样,sigsuspend没有成功返回值,只有一个执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定。
可以通过sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字回复为原来的值。如果原来对信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。 (恢复原状态)
1、调用sigprocmask(SIG_BLOCK, &newmask, &omask);屏蔽SIGALRM
2、调用sigsuspend(&smask);解除对SIGALRM的屏蔽,然后挂起等待
3、SIGALRM递达后suspend返回,自动回复原来的屏蔽字,也就是再次屏蔽SIGALRM
4、调用sigprocmask(SIG_SETMASK, &omask, NULL);再次解除对SIGALRM的屏蔽