信号的捕捉与模拟实现sleep函数

来源:互联网 发布:linux打开绝对路径 编辑:程序博客网 时间:2024/06/06 18:06

信号的捕捉:

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
信号的处理有三种方式。
那么它是在什么时候处理信号的呢?
我们来看一张图。
这里写图片描述
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的屏蔽