重提信号概念
前一篇中提到了信号的概念,并且对信号的产生及一些属性有了一定的认识,这一次,要从更加深入的角度看待信号。
之前提到过,当我的进程在接收到信号之前,就已经知道了,当我接收到某种信号之后就要发生某一项动作,换句话说,在进程内部,一定存在这某种结构,将这些信息都记录了下来,很明显,对于进程而言,这些信息都会保存在它的 PCB 当中。
首先我们来认识这样几个概念:
信号递达(Delivery):执行信号的处理动作;
信号未决(Pending):信号从产生到递达之间的状态;
阻塞(Block):被阻塞的信号被保存在未决状态,直到解除阻塞之后,才会执行递达动作。只要信号阻塞就永远不会递达;
忽略 (Ignore):忽略完全不同于阻塞,忽略是在递达之后可选的一种动作;
这样的几个概念显得有点太过笼统,这里截取了一张信号在 PCB 中的示意图,如下:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决状态,直到信号递达清除该标志位。
Linux 为了节省内存空间,在设计的时候,使用了类似位图的结构,只留出一个 bit 的大小,分别用 0 和 1 表示阻塞或者未决状态,那么对于上图,我们就可以看到三张表:阻塞表,未决表,handler 表。
对于阻塞表,该位为 0,表示进程对该信号不阻塞,为 1 表示对该信号阻塞;
对于未决表,该位为 0,表示该信号没有产生,为 1 表示该信号已经发生;
handler 表就类似我们之前提到过的信号处理函数 signal(),用来表示对于某一信号的处理方式。
1、pending 表和 block 表之间没有任何关系。信号的产生是异步的,对于进程而言完全随机,而阻塞状态是该进程对某一信号所做的限制;
2、信号的发生,对于进程而言,只是将该进程 PCB 中的 pending 表中的对应位置 1,其他的操作和信号就不再有任何直接关系,这就解释了在信号来临之前进程就已知了某个信号对应的动作;
3、在 Linux 下,由于这里只是通过一个 bit 位来存储信息,所以在信号递达之前,信号发生多次只记一次。当然,更严格意义上说,常规信号是这样的,对于实时信号(34~64 号信号),在递达之前,多次产生的信号会保存到某个队列当中,实时信号暂时不在我们的讨论范围之内。
4、任何一个信号,都不会是被立刻递达,这个后面解释。
由于阻塞标志和未决标志都是用一个 bit 位来表示,因此对于 Linux,引入了一个用户类型, 两种标志都可以使用数据类型来存储,称为。因此就有了阻塞信号集和未决信号集。阻塞信号集又叫做信号屏蔽字(有没有很熟悉的感觉)。
信号集操作函数
信号集操作函数,顾名思义,就是对上面的几种信号进行操作,之前我们提到的信号操作函数,实际上就是在更改这里的 pending 表,因此,我们这里提到的信号集操作函数,可以查询和修改阻塞信号集中的数据,对于 pending 表中的数据,这里只提供了查看的函数接口。具体函数声明如下:
- // 信号集操作函数
- #include
- int sigemptyset(sigset_t *set);
- # 初始化,清零所有信号对应的bit位
- int sigfillset(sigset_t *set);
- # 对所有信号的bit位置1
- int sigaddset(sigset_t *set, int signum);
- # 将指定信号bit位置1
- int sigdelset(sigset_t *set, int signum);
- # 将指定信号bit位清零
- 以上四个函数,成功返回0,失败返回-1
- int sigismember(const sigset_t *set, int signum);
- # 判断一个信号集的有效信号中,是否包含某个信号
- # 包含返回1, 不包含返回-1
- // 屏蔽信号集操作函数(写)
- int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- # 读取或更改进程中的信号屏蔽字(阻塞信号集)
- # 成功返回0, 失败返回-1
- # 参数1,how有三种定义
- SIG_BLOCK:添加对应位,mask = mask| set
- SIG_UNBLOCK:清零对应位mask&~set
- SIG_SETMASK:设置对于位mask=set
- # 参数2,设置的SIG值
- # 参数3, 输出型参数,用来获取修改之前的信号屏蔽字
当我们调用 sigprocmask 对某些信号解除屏蔽之后,在该函数返回之前,至少有一个信号被递达
- // 未决信号集操作函数(读)
- #include
- int sigpending(sigset_t *set);
- # 输出型参数,将pending列表通过set传回
- # 成功返回0,失败返回-1
说了这么多,下面通过代码做一简单验证。(以 SIGINT 信号为例)
- #include
- #include
- void printfPending(sigset_t *pending)
- {
- int i = 1;
- for(;i <= 31; i++)
- {
- if(sigismember(pending, i)){
- printf("1");
- }
- else{
- printf("0");
- }
- }
- printf("\n");
- }
- int main()
- {
- sigset_t block, oblock, pending;
- sigemptyset(&block);
- sigaddset(&block, SIGINT); // 设置block值
- sigprocmask(SIG_SETMASK, &block, &oblock); // 设置信号屏蔽字
- while(1){
- sleep(1);
- sigpending(&pending);
- printfPending(&pending); // 获取pending值
- }
- printf("hello world\n");
- return 0;
- }
因为 SIGINT 信号对应的操作是 ctrl+c,但上面将 SIGINT 信号设置为屏蔽状态,因此,当我们输入 ctrl+c 之后并没有立即终止该进程,我们看到的第二为 pending 值由 0 变为 1。如下图:
接下来将代码做一简单调整,我们设置 10 秒之后,信号屏蔽字被自动清零,为了防止 ctrl+c 将信号终止,所以这里 SIGINT 信号执行自定义行为,代码如下:
- #include
- #include
- void printfPending(sigset_t *pending)
- {
- int i = 1;
- for(;i <= 31; i++){
- if(sigismember(pending, i)){
- printf("1");
- }
- else{
- printf("0");
- }
- }
- printf("\n");
- }
- void runSig(int i)
- {
- printf("run SIGINT\n");
- }
- int main()
- {
- signal(SIGINT, runSig);
- sigset_t block, oblock, pending;
- sigemptyset(&block);
- sigaddset(&block, SIGINT);
- sigprocmask(SIG_SETMASK, &block, &oblock);
- int count = 0;
- while(1)
- {
- if(count == 10)
- {
- sigdelset(&block, SIGINT);
- sigprocmask(SIG_SETMASK, &block, &oblock);
- }
- sleep(1);
- sigpending(&pending);
- printfPending(&pending);
- count++;
- }
- printf("hello world\n");
- return 0;
- }
运行行结果如下:
由于这里已经设置了自定义 SIGINT 的动作,因此,即使 10 秒之后,ctrl+c 也不会终止进程
信号捕捉
信号捕捉的过程
关于信号捕捉,其实前面一直在说,我们把对信号的自定义行为称为信号捕捉。对信号的处理有三种,忽略,默认,捕捉。
前两种算是比较简单的。站在操作系统的角度,忽略信号其实要做的就是将 pending 中的 1 改为 0 即可,不需要其他操作;对于默认动作,大部分的默认动作的最终结果都是终止进程,先有个简单简单认识,接下来看捕捉状态下的情况,看下面这张图:
①:发生了外部终端,或者遇到了陷阱、异常,这个时候,会由用户态切换到内核态处理该异常;
②:内核处理完成异常之后,在返回用户态执行原代码之前,会检查该进程的 PCB 中有无未处理的信号(内核会在内核态切换到用户态的过程中检查有无未处理的信号);
③:这时发现了存在未处理的信号,不受阻塞,而且该信号的处理动作是捕捉的,就会切换到用户态去执行自定义的函数(因为这个函数是用户定义的,如果不切换用户,由内核态直接去执行,是不安全的);
④:在执行完自定义的信号处理函数之后,会受到系统调用再次切换到内核态;
⑤:再次进行检查,然后返回到用户态,从上次被中断的地方继续向下执行。
这就是捕捉的整个过程,一共发生了四次用户态到内核态之间的转化,这时候再看我们的忽略动作,当执行的第三步之后,发现该动作是忽略,于是在内核态直接将 pending 中的对应位清零,直接返回用户态终端的地方继续执行。对于默认动作,由于通常会终止进程,所以在内核态将对应位的 pending 值改 0 之后,同时销毁 PCB,直接结束进程。(这个过程还是挺重要的)
sigaction() 函数
sigaction 函数可以设置和读取与指定信号相关联的动作,与 signal 函数功能类似,函数声明与注释如下:
- #include <signal.h>
- int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- # 成功返回0, 失败返回-1
- # 参数1,信号编号
- # 参数2,若act非空,按照结构体中的信息修改处理动作
- # 参数3,输出型参数,若非空,获取原来的struct结构。
- 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有三种,SIG_DFL表示默认动作;SIG_IGN表示忽略信号;为函数指针,表示执行捕捉动作
- # sa_mask表示当正在对该信号动作时,除了当前信号被屏蔽之外,还需要屏蔽的其他信号
- # sa_flags这里直接设置为0即可,暂不关心
- # 其他两个参数这里也暂不关心
这里给出测试代码:
- #include <stdio.h>
- #include <signal.h>
- void IntRun(int i)
- {
- printf("my sigaction is running\n");
- }
- int main()
- {
- struct sigaction act, oact;
- act.sa_handler = IntRun;
- sigemptyset(&act.sa_mask);
- act.sa_flags = 0;
- sigaction(SIGINT, &act, &oact);
- while(1){
- sleep(1);
- printf("hello world\n");
- }
- return 0;
- }
输出结果如图:
sigaction 与 signal 函数功能类似,这里只介绍用法,不在多说。
pause() 函数
首先给出 pause 函数的定义
- #include <unistd.h>
- int pause(void);
函数定义特别简单,pause 函数的功能是将调用进程挂起,直到有信号递达。
如果到达的信号是将进程终止,那么进程直接结束,来不及返回;
如果到达信号被忽略,则继续挂起,无返回值;
如果调用动作是捕捉,那么调用信号处理函数之后,pause 返回 - 1,同时设置 errno 为 EINTR(被信号中断)。
可见,pause 函数,只有当出错的情况下才会有返回值,这点和 exec 函数类似。
接下来,让我们写一段小代码,使用 alarm 函数和 pause 函数写一个自己的 sleeep 函数,函数名为 mysleep。
实现原理:利用了 pause 函数的特性,会将进程挂起,直到有捕获(catch)的行为,才会将 pause 函数终止。利用 alarm 函数定时,闹钟时间到达之后,会调用自定义函数,发生捕获行为,导致 pause 函数终止,从而实现了 sleep 的功能。
这里给出了 signal 函数和 sigaction 函数版本的,两者基本一致,不同之处在于 sigaction 需要设置的参数较多。代码如下:
- #include <stdio.h>
- #include <unistd.h>
- #include <signal.h>
- void run_alarm(int i)
- {}
- /*
- // signal版本
- size_t mysleep(size_t second)
- {
- signal(SIGALRM, run_alarm);
- alarm(second);
- pause();
- int ret = alarm(0);
- return ret;
- }
- */
- // sigaction版本
- size_t mysleep(size_t second)
- {
- struct sigaction act, oact;
- act.sa_handler = run_alarm;
- sigemptyset(&act.sa_mask);
- act.sa_flags = 0;
- sigaction(SIGALRM, &act, &oact);
- alarm(second);
- pause();
- int ret = alarm(0);
- sigaction(SIGALRM, &oact, NULL);
- return ret;
- }
- int main()
- {
- while(1){
- mysleep(2);
- printf("this is mysleep\n");
- }
- return 0;
- }
可重入函数
可重入函数的概念其实很好理解。有些函数,如果重入不会导致出错或不安全的话,我们把这些函数叫做可重入函数,反之,叫做不可重入函数。
举个例子,当我们对一个链表进行插入的时候,中途收到一个信号,该信号执行自定义动作,该动作也是在该结点处插入一个新节点,就会造成下图所示的情况,最终的 2 号结点并没有被插入,这就是所说的不可重入函数
问题来了,很容易可以发现,这个和线程安全有着很大的相似之处,都是由于重入导致的问题,这里做以简单区分。
区别:
1、前提不同:线程安全是在多线程情况下产生的,可重入函数可以是在单线程下由信号的捕获产生的的重入
2、范围不同:线程安全不一定可重入,可重入函数一定满足线程安全
3、对临界资源加锁可以实现线程安全,但依旧是不可重入的,因为加锁只能防止多线程的情况,单一线程的情况不一定安全。
4、线程安全要求不同线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响。
可重入函数的几点必要条件
1、不在函数内部使用静态或全局数据 ;
2、不返回静态或全局数据,所有数据都由函数的调用者提供;
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;
4、不调用不可重入函数;
------muhuizz 整理
来源: http://www.bubuko.com/infodetail-1957138.html