APUE学习:信号

来源:互联网 发布:大圣传 知乎 编辑:程序博客网 时间:2024/05/20 01:46

信号机制是一个经典的进程异步机制。
Linux信号机制的基本流程:

  1. 用户程序为一个信号注册一个信号处理函数,例如SIGQUIT注册了一个sig_hander函数
  2. 因为某些原因,进程从用户态切换到内核态
  3. 从内核态要返回到用户态时,内核会去检测有没有给该进程传递一个SIGQUIT信号,如果有会在用户态下面去执行对应的信号处理函数sig_hander
  4. sig_hander执行完毕之后会自动执行特殊的系统调用sigreturn再次进入内核态。
  5. 如果没有新的信号传递过来,这次在返回用户态就恢复到上次中断的地方之后继续执行。

信号传递的过程
这里写图片描述

一些概念

当产生一个信号之后,注意信号是跟进程相关联的。所以信号产生之后,内核需要把这个信号交给进程去处理。

进程处理信号基本上有三种方式:
1.忽略该信号。但是注意有两种信号不可以忽略。例如SIGKILL与SIGSTOP。
2.捕捉该信号。捕捉该信号之后,激活进程准备好的信号处理函数,把这个信号交给这个信号处理函数去处理。
3.捕捉该信号但是让默认的信号处理函数去处理。

注意点:
1)另外需要注意的是,进程有一个信号阻塞队列。阻塞不同于忽略。阻塞表示的是我接受该信号,但是目前却不想处理该信号。
对于处于阻塞状态的信号一般有两种方式去处理。
1)忽略这个处于阻塞状态的信号
2)解除阻塞状态后,调用处理该信号的信号处理函数去处理。

2)未决信号
与阻塞不同,阻塞指的是阻止这个信号被处理,即不让改信号的信号处理函数去处理它。未决指的是信号产生到被处理之前的这段时间。

signal函数

#include <signal.h>void (*signal(int signo, void(*func)(int)))(int);

说明:
signal函数会返回之前的信号处理函数,如果成功的话。
失败就设置error为SIG_ERR
里面的func有三种情况:
1.SIG_IGN:忽略该信号
2.SIG_DFL:使用默认的信号处理函数
3.我们自定义的信号处理函数

注意点:

  • signal函数的一个弱点是,如果我们想要知道以前的信号处理函数,那么只有通过改变当前的信号处理函数。
  • 进程创建:当一个进程调用fork()生成子进程之后,子进程会保留父进程的信号处理方式。
  • 程序启动:一个程序启动之后,所有的信号状态都是默认或者忽略的。通常程序启动之后都是默认。但是如果一个进程设置了一个信号为忽略的,那么在这个进程调用exec之后,该信号就是忽略的了。但是如果进程把该信号设置为捕捉,那么在调用exec之后,该信号的处理方式就是系统默认的了。

对于程序启动的解释:
1.如果父进程中对于一个信号注册了一个自己的信号处理函数,那么父进程fork之后,子进程也会保留这个信号处理函数。
2.但是如果在子进程中调用exec后,除非在父进程中对于信号的处理方式是默认或者忽略,否则子进程会把该信号的处理方式修改为默认方式处理。

不可靠信号

  • 早期版本的一个问题是:对于一个信号,我们调用了信号处理函数之后,对于这个信号的信号处理函数我们会进行复位。即将该信号的信号处理函数设置为默认的动作。
  • 早期版本的另一问题是:我们不可以如果我们不想一个信号发生,我们不可以关闭该信号。

注意点:
还有一点要记住的是:从信号产生到调用信号处理函数来处理该信号是需要一个时间的。

中断的系统调用

即系统调用是可以被信号中断的,比如慢速系统调用。系统调用会在被信号打断之后返回,并且设置error变量为EINTR

  • 对于被中断的系统调用,一般有3中处理方式:

    • 人为重启被中断的系统调用
    • 安装信号时设置SA_RESTART属性
    • 忽略信号
  • 低速系统调用: read , write, open, pause, ioctl, interprocess communication

ioctl, read, readv, write, writev, wait, waitpid这些系统调用都会在被打断之后重启。但是如果我们不想他们重启,那么就可以对于相应的信号设置对应的处理过程。

信号中断与慢系统调用

可重入函数

UNIX定义的可重入函数:
这里写图片描述
这些可重入函数会阻塞那些可能造成不连贯的信号。

注意点:

  • 对于可重入函数,我们需要注意的是: 系统调用肯能影响errno的值,所以我们在调用信号处理函数时,最好先保存errno的值。
  • 对于longjmp以及siglongjmp来说,如果主例程在以非可重入方式更新数据时,这时产生了信号,并且在信号中我们调用了longjmp与siglongjmp,这两个函数会造成我们中断更新数据这个步骤。所以在信号处理函数中不要使用longjmp与siglongjmp

可靠信号术语与语义

几个术语

  • 信号产生:当信号产生时,内核通常把进程表中相应字段的标识设置好。
  • 信号delivered:指的是信号被信号处理函数接受了。
  • 信号pending:指的是信号产生到delivered这一段时间。
  • 信号blocking:指的是进程有权阻塞信号。阻塞的过程是:进程有block mask,这个mask记录了要阻塞的进程。在信号产生后,如果进程对于该信号不是忽略的,那么就接受这个信号,然后进程根据自己的mask决定这个信号是不是去信号阻塞队列中去。对于处于阻塞队列的信号,我们可以决定解除阻塞或者忽略它。
  • 信号集:表示的是信号的集合
  • 信号屏蔽字:表示的是进程想要屏蔽的信号的集合,一般用位操作。

注意点

  • 如果一个信号已经被进程阻塞了,那么如果继续传递该信号,那么进程可能把产生的信号丢弃掉,除非它是实时信号,会把它排队。
  • 如果有多个signal产生并且deliver给进程的话,POSIX建议将最新的deliver给进程

信号集处理函数

因为信号的个数可能超过int类型的位数,为了跨平台,所以建议使用下面的函数去处理信号集。

#include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signo);int segdelset(sigset_t *set, int signo);int sigismember(const sigset_t *set, int signo);

alarm与pause 函数

alarm

unsigned intalarm(unsigned int seconds);

说明:
alarm函数在用来判断低速系统调用时,要注意他与系统调用之间的竞争关系。也就是说注意alarm的计时到时,但是系统调用却没有使用完的问题。
alarm接受一个sec,返回以前的alarm还剩余的时间或者返回0.
注意一个进程只有一个alarm。alarm(0)会注销掉进程设置的闹钟。
记住现代操作系统都是多任务的。注意线程安全等。

注意点:
见书上的3个编程例子
主要问题是
1.我们记得要保留以前留下的时间,因为一个进程只会有一个计时。
2.alarm与其他系统调用结合使用时,可能会有alarm已经返回,而其他系统调用还没开始执行的问题。
3.另外不要在信号处理函数中使用longjmp,而是要用siglongjmp代替,因为longjmp没有定义跳出信号处理函数时,信号屏蔽字的处理方法。

信号集

信号集是用来表示多个信号的一个集合。因为系统中信号的数量会比一个int类型大,所以定义一个sigset_t类型,来定义信号集。本质上还是用位来表示。

sigprocmask函数

int sigprocmask(int how,                const sigset_t *restrict set,                sigset_t *restrict oset);

根据how的值的不同,新的mask的值也不同。
通过oset,来存储旧的mask。
具体定义见书上。

注意点

sigprocmask只能用于单线程环境。
在call sigprocmask之后,如果有处于unblocked状态的信号在排队,那么在sigprocmask返回之前,至少有一个信号会被传递给进程,即被信号处理函数处理。
注意点:
需要说明的是,在sigprocmask的函数体内应该就会解除信号的阻塞,这样信号就是unblocked and pending状态了,对于有处于unblocked and pending状态的信号,对于这样的信号,至少有一个在sigprocmask返回之前就要去调用信号处理函数去处理它。
这就是Figure 10.15中为什么在第二次调用sigprocmask之后,先输出QUIT的信号处理的printf,再去输出后面的printf(“SIGQUIT unblocked”),因为在sigprocmask函数体内就把QUIT变成了unbloced状态,使得QUIT是一个unblocked and pending的信号,所以在sigprocmask函数返回之前就要调用QUIT的信号处理函数。

sigpending函数

#include <signal.h>int sigpending(sigset_t *set); //成功返回0,否则返回-1

获取正在处于blocked并且在排队的signal,将他们放在set中。

sigaction

ing sigaction(int signo, const struct sigaction *restrict act,              struct sigaction *restrict oact);struct sigaction{    union    {        void (*sa_handler)(int);        void (*sa_sigaction)(int, siginfo_t *, void *);    }    sigset_t sa_mask;    int sa_flags;};

说明
1.在现代系统中,使用sigaction,因为sa_flags为0时,会自动保存对于该信号的信号处理函数,而早期系统中调用signal之后,该信号的处理方式就变为了默认。并且使用sigaction之后,在信号处理函数中我们会屏蔽掉该信号,即自动把该信号加入信号屏蔽字,而在我的系统上,signal并不会自动把该信号加入信号屏蔽字。
2.sa_flags的取值及说明:
这里写图片描述
这里写图片描述
3.如果sa_flags使用了SA_SIGINFO,那么信号处理函数使用的是sa_sigaction,而不是sa_handler。

注意点

sigaction的运行过程:
先使用signal_hander注册信号处理函数,然后在使用信号处理函数之前,将进程的mask设置为sa_mask。接着再信号处理函数返回之后,把mask设置为原先的值。这样在调用一个signal_handler前,我们可以阻塞任意的信号。
需要特别注意的是:如果一个信号已经在被delivered,那么这个信号会被放到该进程的信号屏蔽字中去。所以对于非实时信号的多次产生,我们可能只会响应一次。

需要注意的是:当sa_flags设置为SA_SIGINFO时,信号注册信号处理函数时,使用的是那个alternate handler,也就是说,当有一个信号被捕捉到了,我们调用的信号处理函数由sa_sigaction来指示,而不是 sa_handler来指示。

sigsetjmp和siglongjmp函数

longjump的问题

对于在signal handler中使用longjmp与setjmp的问题:
这个问题在于longjmp使用之后会跳出当前的栈空间,返回到主例程中去。这样可能会造成在一个signal handler中设置的信号屏蔽字依然在主例程中有效(我们前面就说了,对于一个delivered的信号,操作系统会把该信号屏蔽掉,这样我们直接跳出栈空间就会出现一些不可预见的事情)
总结:
信号处理的一个关键问题:就是信号屏蔽字在什么时候去设置,什么时候释放,防止出现不可预料的信号屏蔽字的产生。

ing sigsetjmp(sigjmp_buf env, int savemask);void siglongjmp(sigjmp_buf env, int val);

说明:
sigsetjmp会保存当前的堆栈信息到env中,如果savemask不为0,那么当前环境的signal mask也会被保存到env中去。这样当siglongjmp被调用时,他会检测他的env,如果他的env是被一个sigsetjmp设置,且这个sigsetjmp的savemask不是0,那么siglongjmp就会从env中取到以前的堆栈信息,包括以前的signal mask,并且还原他们。
而setjmp与longjmp并没有说明signal mask的还原问题。
所以对于信号处理问题要使用sigsetjmp 与 siglongjmp

sigsuspend函数

为什么引入sigsuspend

因为sigsuspend是一个原子操作,在我们需要解除信号的屏蔽,并且等待一个信号处理函数的返回时,我们就需要它。没有它,解除操作与等待操作就会有一个时间窗口,在这个窗口可能会出现信号的丢失。

例子:
这里写图片描述
红框中的代码的问题:
这里出现的问题就是:
如果有一个信号在pause()调用之前就delivered,而且以后也不会出现了,那么就不会获得这个信号了。并且pause就会获得别的信号。
造成这个问题的原因就是sigprocmask与pause连在一起不是一个整体操作。所以我想要在block一个信号之后,在unblock他并且去处理他就要调用sigsuspend(),这相当于将sigprocmask与pause组合成了一个原语操作

sigsuspend

#include <signal.h>int sigsuspend(const sigset_t *sigmask); //返回-1,并将errno设置为EINTR

sigsuspend函数:
调用它时,它将当前的信号屏蔽在该为sigmask,并且阻塞进程,直到有一个信号被捕捉到了,或者有一个终止该进程的信号发生了。如果一个信号被捕捉并且信号处理函数返回了,sigsuspend才返回,并且会把信号屏蔽字设置为调用sigsuspend之前的值。
注意sigsuspend总是返回-1,并且把errno设置为ENTR。表示一个被中断的系统调用。

这个函数就比较有效的解决了上面信号丢失的问题。比如我先用sigprocmask来设置信号屏蔽字,并且保持旧的信号屏蔽字,然后执行critical section,执行完毕之后我想把在block and pending 的信号取出来给unblock掉,那么就可以调用sigsuspend来重新设定信号屏蔽字,从而unblock我们要unblock的信号,这个时候sigsuspend会等待直到有一个信号处理函数返回,从而防止了上面程序中的丢失信号问题。在sigsuspend返回之后,我们就可以调用sigprocmask来将信号屏蔽字设定为以前的值。

sigsuspend编程模型

1.一般是先用sigprocmask设定好屏蔽字,然后调用sigsuspend解除一些屏蔽字,并且测试什么信号到了,如果是我们的信号捕捉到了,那么就可以返回,然后使用sigprocmask来恢复以前的屏蔽字。
sigsuspend一般用于要等待特定信号的捕捉。

abort函数

void abort(void)

说明:

注意abort函数永不返回。

它的作用是:在进程终止之前,由abort来执行所需要的清理操作。
需要注意的一些东西:
1.abort并不理会进程对它这个信号的阻塞与忽略
2.abort会产生SIGABRT信号,对于这个信号我们可以使用信号捕捉函数来处理,当是在信号处理函数处理完毕返回之后,abort也不会返回。
3.但是在信号处理函数中若是调用exit(),_exti(), _Exit(), siglongjmp或longjmp,那么进程就可以避免掉abort的不会返回。
4.abort并不会理会block与ignore操作。
5.如果abort调用终止进程,则他对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。

abort函数要做什么

注意理解abort()
首先我们要知道abort()需要什么。
1.abort()不能让进程忽略SIGABRT
2.abort()不能让进程阻塞SIGABRT
3.如果采用默认方式使用SIGABRT,那么在abort中要fflush()
4.abort()会给进程发SIGABRT信号,进程是可以使用信号处理函数来处理SIGABRT这个信号的。
5.如果该信号处理函数正常返回了,那么应该返回到abort()中,因为是在abort()中发这个信号的,但是注意到abort()函数并不会返回,他的目的是让进程使用abort()退出,并且在退出之前做一些清理工作,所以从信号处理函数返回后,abort要把SIGABRT这个信号的信号处理函数设置为默认值,并且去除对SIGABRT这个信号的阻塞(因为我们可能在信号处理函数中又把他个屏蔽了,然后再去给进程发SIGABRT这个信号。
6.如果该信号处理函数不是正常返回,例如调用了exit(), _exit(), _Exit(), longjmp, setlongjmp()等就会跳出abort(),不受abort影响。
7.最后一句不会执行,因为ABRT的默认操作就是退出。

需要注意的一些内容就是:
注意多任务的环境下,编程的细节的处理。即这里的第5点。

system函数

POSIX要求 system 忽略SIGINT与SIGQUIT,并且要阻塞SIGCHLD

1.为什么要阻塞SIGCHILD呢
因为system一般会调用fork() and exec(),也就是说会有一个子进程产生,那么我们希望子进程结束后,由我们,而不是fork来处理子进程,所以在system中要阻塞SIGCHILD。
2.为什么要忽略SIGINT与SIGQUIT
因为使用fork() and exec()后,这些进程都是在一个进程组中,这样我们传递一个QUIT之后,在前台的进程组中所有进程都会收到QUIT信号,但是我们只是想让跟我们交互的那个进程来接受QUIT,所以在system中我们要忽略SIGQUIT与SIGINT

sleep函数

unsigned int sleep(unsigned int seconds);

sleep 会让进程suspend,直到
1.wall clock time 到时
2.有一个信号被进程捕捉,并且信号处理函数返回。此时返回剩余的时间。

sigqueue

int sigqueue(pid_t pid, int signo, const union sigval value)

如果想要queue一个信号,那么要做3件事:
1.给sa_flags加上SA_SIGINFO
2.signal handler设定为struct sigaction中的sa_sigaction,而不是sa_handler
3.使用sigqueue函数传递消息

sigqueue函数与kill函数类似,但是我们只能给一个进程传送消息,而kill可以一次性给多个进程传送消息。

总结

1.Linux的信号机制为的是解决进程异步的问题。
2.理解Linux的信号传递过程。
3.Linux信号编程需要注意的问题:
什么时候阻塞信号,什么时候解除阻塞,怎样处理特定的信号,如何去改变信号处理函数,信号屏蔽字是怎么改变的,可靠与不可靠信号,可重入函数,系统调用如何被信号中断,如何防止信号的丢失,什么是竞争条件。以及一些信号编程模型。注意改变之后恢复以前内容。
4.注意Linux是一个多任务多用户的系统,所以要注意一些全局变量的改变,以及代码是不是原子性会产生什么问题,这些都要注意。

[参考]

信号传递过程

0 0
原创粉丝点击