信号机制是一个经典的进程异步机制。
Linux信号机制的基本流程:
当产生一个信号之后,注意信号是跟进程相关联的。所以信号产生之后,内核需要把这个信号交给进程去处理。
进程处理信号基本上有三种方式:
1.忽略该信号。但是注意有两种信号不可以忽略。例如SIGKILL与SIGSTOP。
2.捕捉该信号。捕捉该信号之后,激活进程准备好的信号处理函数,把这个信号交给这个信号处理函数去处理。
3.捕捉该信号但是让默认的信号处理函数去处理。
注意点:
1)另外需要注意的是,进程有一个信号阻塞队列。阻塞不同于忽略。阻塞表示的是我接受该信号,但是目前却不想处理该信号。
对于处于阻塞状态的信号一般有两种方式去处理。
1)忽略这个处于阻塞状态的信号
2)解除阻塞状态后,调用处理该信号的信号处理函数去处理。
2)未决信号
与阻塞不同,阻塞指的是阻止这个信号被处理,即不让改信号的信号处理函数去处理它。未决指的是信号产生到被处理之前的这段时间。
#include
void (*signal(int signo, void(*func)(int)))(int);
说明:
signal函数会返回之前的信号处理函数,如果成功的话。
失败就设置error为SIG_ERR
里面的func有三种情况:
1.SIG_IGN:忽略该信号
2.SIG_DFL:使用默认的信号处理函数
3.我们自定义的信号处理函数
注意点:
对于程序启动的解释:
1.如果父进程中对于一个信号注册了一个自己的信号处理函数,那么父进程fork之后,子进程也会保留这个信号处理函数。
2.但是如果在子进程中调用exec后,除非在父进程中对于信号的处理方式是默认或者忽略,否则子进程会把该信号的处理方式修改为默认方式处理。
注意点:
还有一点要记住的是:从信号产生到调用信号处理函数来处理该信号是需要一个时间的。
即系统调用是可以被信号中断的,比如慢速系统调用。系统调用会在被信号打断之后返回,并且设置error变量为EINTR
对于被中断的系统调用,一般有3中处理方式:
低速系统调用: read , write, open, pause, ioctl, interprocess communication
ioctl, read, readv, write, writev, wait, waitpid这些系统调用都会在被打断之后重启。但是如果我们不想他们重启,那么就可以对于相应的信号设置对应的处理过程。
信号中断与慢系统调用
UNIX定义的可重入函数:
这些可重入函数会阻塞那些可能造成不连贯的信号。
注意点:
因为信号的个数可能超过int类型的位数,为了跨平台,所以建议使用下面的函数去处理信号集。
#include
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);
unsigned int
alarm(unsigned int seconds);
说明:
alarm函数在用来判断低速系统调用时,要注意他与系统调用之间的竞争关系。也就是说注意alarm的计时到时,但是系统调用却没有使用完的问题。
alarm接受一个sec,返回以前的alarm还剩余的时间或者返回0.
注意一个进程只有一个alarm。alarm(0)会注销掉进程设置的闹钟。
记住现代操作系统都是多任务的。注意线程安全等。
注意点:
见书上的3个编程例子
主要问题是
1.我们记得要保留以前留下的时间,因为一个进程只会有一个计时。
2.alarm与其他系统调用结合使用时,可能会有alarm已经返回,而其他系统调用还没开始执行的问题。
3.另外不要在信号处理函数中使用longjmp,而是要用siglongjmp代替,因为longjmp没有定义跳出信号处理函数时,信号屏蔽字的处理方法。
信号集是用来表示多个信号的一个集合。因为系统中信号的数量会比一个int类型大,所以定义一个sigset_t类型,来定义信号集。本质上还是用位来表示。
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的信号处理函数。
#include
int sigpending(sigset_t *set); //成功返回0,否则返回-1
获取正在处于blocked并且在排队的signal,将他们放在set中。
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来指示。
对于在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是一个原子操作,在我们需要解除信号的屏蔽,并且等待一个信号处理函数的返回时,我们就需要它。没有它,解除操作与等待操作就会有一个时间窗口,在这个窗口可能会出现信号的丢失。
例子:
红框中的代码的问题:
这里出现的问题就是:
如果有一个信号在pause()调用之前就delivered,而且以后也不会出现了,那么就不会获得这个信号了。并且pause就会获得别的信号。
造成这个问题的原因就是sigprocmask与pause连在一起不是一个整体操作。所以我想要在block一个信号之后,在unblock他并且去处理他就要调用sigsuspend(),这相当于将sigprocmask与pause组合成了一个原语操作
#include
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来将信号屏蔽字设定为以前的值。
1.一般是先用sigprocmask设定好屏蔽字,然后调用sigsuspend解除一些屏蔽字,并且测试什么信号到了,如果是我们的信号捕捉到了,那么就可以返回,然后使用sigprocmask来恢复以前的屏蔽字。
sigsuspend一般用于要等待特定信号的捕捉。
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()需要什么。
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点。
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
unsigned int sleep(unsigned int seconds);
sleep 会让进程suspend,直到
1.wall clock time 到时
2.有一个信号被进程捕捉,并且信号处理函数返回。此时返回剩余的时间。
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是一个多任务多用户的系统,所以要注意一些全局变量的改变,以及代码是不是原子性会产生什么问题,这些都要注意。
信号传递过程