1 基本概念
UNIX进程信号是经典的操作异步事件机制,每个信号有一个SIG开头的名字和正整数值,下面几种情况可以产生信号:
- 终端上用户按下Ctrl-C,产生SIGINT信号,默认终止前台进程组
- 硬件异常,如除零错误、非法内存访问
- kill(2)系统调用,向进程或进程组发送信号
- 软件条件,如alarm(2)时间到产生SIGALRM信号,向没有读进程的PIPE写数据产生SIGPIPE信号等
因为信号是异步事件,进程不能通过检测某个变量来检测信号,只能告诉操作系统当某个信号发生的时候怎么做,这个过程叫信号处理,有三种信号处理的方式:
- 忽略信号。信号发生时不做任何处理,但是SIGKILL和SIGSTOP不能忽略。
- 捕获信号。指定信号发生时执行某段代码。
- 默认处理。采取系统默认处理方式,大多数情况下是终止进程,如SIGINT,SIGTERM等。
2 早期的signal(2)系统调用
#include
void (*signal(int signo, void (*func)(int)))(int);
Returns: previous disposition of signal (see following) if OK, SIG_ERR on error
singal(2)有两个参数,第一个是信号值,第二个是信号新的处理函数,返回值是之前的信号处理函数或者SIG_ERR。其中信号处理函数是一个返回值为void的函数,唯一的参数是信号值。
通常func是信号处理函数,也可以是SIG_IGN,SIG_DFL。SIG_DFL指定使用默认处理,SIG_IGN忽略对应的信号。但是SIGKILL和SIGSTOP不能忽略也不能捕获,只能使用默认处理,前者终止进程,后者暂停进程直到收到SIGCONT信号,因为操作系统要保留通过这两个信号分别终止进程和暂停进程的能力。
2.1 早期实现中singal(2)有两个缺陷
一个是信号可能丢失,因为每次信号产生时系统把对应的信号处理函数回复为默认处理,以前的代码中常常在信号处理函数的第一步重新设置,如下所示:
int sig_int(); /* my signal handling function */
...
signal(SIGINT, sig_int); /* establish handler */
...
sig_int()
{
signal(SIGINT, sig_int); /* reestablish handler for next time */
... /* process the signal ... */
}
在SIGINT信号产生之后,sig_int()中的singal执行之前,SIGINT信号可能发生第二次,这时系统将SIGINT信号的处理方式设置为默认,导致进程终止,这样SIGINT信号就丢失了。
另一个缺陷是信号不能被阻塞。信号阻塞是指告诉操作系统,暂时不关心某些信号,但是还是记住信号发生了,等到关心这些信号的时候再通知进程。以前的代码通过下面的方式模拟信号阻塞:
int sig_int_flag; /* set nonzero when signal occurs */
main()
{
int sig_int(); /* my signal handling function */
...
signal(SIGINT, sig_int); /* establish handler */
...
while (sig_int_flag == 0)
pause(); /* go to sleep, waiting for signal */
...
}
sig_int()
{
signal(SIGINT, sig_int); /* reestablish handler for next time */
sig_int_flag = 1; /* set flag for main loop to examine */
}
可能出现这样的情况,在while中测试sig_int_flag之后调用pause()之前,SIGINT再次产生,系统把SIGINT的处理方式设置为默认,而且以后再也不发生,这样进程就一直睡眠下去。
3 可靠的信号机制
3.1 下面介绍一些相关术语:
- 信号被生成(generated):"1.基本概念"中提到的四种情况生成信号,操作系统在进程数据结构中设置相应的标志
- 信号传递(delivered)到进程:信号处理动作被执行
- 信号挂起(pending):从信号被生成到传递到进程的这段时间信号处于挂起状态
- 阻塞(blocking)信号的传递:如果信号被阻塞,而其进程处理方式是默认或者捕获,那么信号保持挂起状态,直到(a)解除阻塞该信号 或者 (b)改变信号处理方式为忽略
进程可以通过sigpending知道那些信号被阻塞和挂起
信号掩码(signal mask)决定那些信号被阻塞,通过sigprocmask可以设置。POSIX.1定义专门的数据结构sigset_t表示信号集合(signal set),信号掩码保存在信号集合中。
3.2 kill(2)和raise(3)系统调用
#include
int kill(pid_t pid, int signo);
int raise(int signo);
Both return: 0 if OK, 1 on error
kill(2)向指定的进程或进程组发送信号,raise(3)向本进程发送信号,等价于kill(getpid(),signo)。kill的pid有以下四种情况,分别对应不同的目标进程或进程组
- pid > 0 向进程ID为pid的进程发送信号
- pid == 0 向和本进程同组的所有进程发送信号,这些进程的进程组ID和本进程的进程组ID相同
- pid < 0 向进程组ID等于-pid的所有进程发送信号
- pid == 1 向所有进程发送信号
另外,不能向系统进程如init发送信号,而且发送信号要有权限,基本权限规则如下:
- root用户可以向任何进程发送任何信号
- 对于普通用户,发送者的EUID和接受者的EUID要相同
- SIGCONT特殊情况:进程可以向同一个会话(session)的其他进程发送SIGCONT信号
信号0是空(null)信号,如果信号值signo等于0,kill(2)可因用来检测某个进程是否存在,但由于UNIX循环使用PID,而且kill(2)返回时对应的进程可能已经不存在了,所以这种检测并不准确。
如果kill(2)成功生成信号,且经常没有阻塞该信号,那么信号会在kill(2)返回之前传递到进程。
3.3 alarm(2)和pause(3)
#include
unsigned int alarm(unsigned int seconds);
Returns: 0 or number of seconds until previously set alarm
int pause(void);
Returns: 1 with errno set to EINTR
alarm(2)设置seconds秒后给本进程产生SIGALRM信号。alarm(2)有几点需要注意的的地方:
- 每个进程只有一个alarm时钟,调用alarm(2)会取消上一个alarm并返回上次剩下的时间,设置新的alarm时钟,seconds之后产生SIGALRM信号
- 如果seconds == 0,取消alarm时钟并返回上次剩下的时间,不产生SIGALRM信号
- 当alarm时钟到期时,由内核产生SIGALRM信号,由于进程调度信号处理可能会有延时,因此alarm不能用作准确时间,而其它本身的时间精度只能到妙级
- SIGALRM信号的默认处理方式是终止进程,要在调用alarm(2)之前设置好信号处理函数
pause(2)直到某个信号处理函数执行完才返回,此时它返回1并设置erron变量为EINTR。
利用alarm(2)和pause(2)可以实现sleep(3),一些精彩的分析参见APUE的Section10.10中Figure10.7-10.9。利用alarm(2)和longjmp(3)可以实现I/O超时,参见APUE的Section10.10中Figure10.10-10.11,另外设置I/O超时还可以用select(2)或poll(2)实现。
3.4 sigprocmask(2)和sigpending(2)
信号集sigset_t的操作如下:
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
All four return: 0 if OK, 1 on error
int sigismember(const sigset_t *set, int signo);
Returns: 1 if true, 0 if false, 1 on error
sigprocmask(2)设置进程的信号掩码,oset返回原来的掩码
#include
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
Returns: 0 if OK, 1 on error
参数how决定具体的操作
- SIG_BLOCK 设置set对应的掩码,OR操作
- SIG_UNBLOCK 取消set对应的掩码,OR ~ 操作
- SIG_SETMASK 只设置set对应的掩码,赋值操作
sigpending(2)在set中返回进程挂起的信号集合
#include
int sigpending(sigset_t *set);
Returns: 0 if OK, 1 on error
3.5 sigaction(2)
#include
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
Returns: 0 if OK, 1 on error
sigaction(2)是signal(2)的替代,它实现可靠的信号机制,参数signo是信号值,act是设置的sigaction,oact是原来的sigaction。act和oact的类型是sigaction结构的指针,sigaction结构定义如下:
struct sigaction {
void (*sa_handler)(int); /* addr of signal handler, */
/* or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options, Figure 10.16 */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
其中sa_handler是和signal(2)参数类似的信号处理函数,sa_mask是处理该信号是屏蔽的信号集合,sa_flags是影响sigaction(2)行为的参数,sa_sigaction是额外的处理函数。
和signal(2)相比sigaction(2)提供了下面几种改进实现可靠的信号机制:
- 信号处理的时候系统不会自动将处理方式回复到默认处理
- 可以通过act.sa_mask制定需要屏蔽的信号集合,在信号处理调用之前系统将为进程阻塞sa_mask指定的信号,信号处理完成之后系统又自动恢复进程的信号屏蔽掩码。而且,系统自动在信号处理期间屏蔽当前处理的信号。
- 当sa_flags中设置了SA_SIGINFO时,调用sa_sigaction(int signo, siginfo_t *info, void *context);进行信号处理,后面两个参数提供更多的信息,如信号的发送者、信号相关的额外信息等
通过sigaction(2)可以实现signal(2),这也是很多系统选择的方式,下面是一种简单的实现:
Sigfunc *
signal_intr(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
更多的分析和sa_flags参数,sa_sigaction参数,参见APUE的Section10.14。
sigaction(2)设置的信号处理函数在执行过程中系统自动屏蔽当前信号,防止信号处理函数被当前信号中断而第二次调用同一个信号处理函数。在信号处理函数中,可能使用longjmp(2)直接跳转到main中,而不是返回,但是跳转之前当前信号被屏蔽,跳转之后信号可能仍然被屏蔽。siglongjmp和sigsetjmp解决这一问题,它们定义如下:
#include
int sigsetjmp(sigjmp_buf env, int savemask);
Returns: 0 if called directly, nonzero if returning from a call to siglongjmp
void siglongjmp(sigjmp_buf env, int val);
如果sigsetjmp(3)得savemask参数不等于0,对应的siglongjmp(3)跳转的时候自动恢复sigsetjmp(3)设置的进程信号掩码。
3.6 sigsuspend(2)
#include
int sigsuspend(const sigset_t *sigmask);
Returns: -1 with errno set to EINTR
sigsuspend(2)解除阻塞某些信号并且等待信号发生,信号处理函数返回的时候sigsuspend(2)返回-1并设置errno。值得的注意的是,解除阻塞信号和等待信号发生这两个动作是作为一个原子操作完成的。
在没有sigsuspend(2)的情况下,可能会选择下面的做法:
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/* block SIGINT and save current signal mask */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/* critical region of code */
/* reset signal mask, which unblocks SIGINT */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/* window is open */
pause(); /* wait for signal to occur */
/* continue processing */
这样做的问题在于,sigprocmask(2)之后pause(2)之前,信号可能产生,这样如果信号之后再也不产生,pause(2)就会一直阻塞下去。
利用sigsuspend(2)可以保护临界区被信号中断、实现全局变量检测、父子进程之间的同步等,具体的代码和分析参见APUE的Section10.16中Figure10.22-24。
3.7 abort(3), system(3), sleep(3)
#include
void abort(void);
This function never returns
abort(3)向本进程发出SIGABRT信号,SIGABRT信号的默认处理方式是终止进程,SIGABRT信号可以被捕获,但是信号处理函数返回以后进程仍然会被终止。如果进程处理函数可以通过调用exit, _exit, _Exit, longjmp或者siglongjmp不返回,前三种会终止进程,而最后两种不会。在进程被终止前,POSIX.1要求打开的I/O流像调用fclose(3)一样被关闭。
APUE的Section10.17中Figure10.25展示了一种POSIX.1 abort(2)的实现方式。
#include
int system(const char *cmd);
Returns: -1 on error, or exit status of execute /bin/sh -c 'cmd'
system(3)使用/bin/sh -c 'cmd'执行cmd指定的程序。POSIX.1要求system(3)忽略SIGINT和SIGQUIT信号,阻塞SIGCHLD信号。执行程序成功的时候,它的返回值其实是/bin/sh的返回值,大多是情况下和cmd的返回值是一致的,但是当cmd由于信号结束时也可能不同。
#include
unsigned int sleep(unsigned int seconds);
Returns: 0 or number of unslept seconds
sleep(3)阻塞进程直到遇到下面的情况:
- senocds指定的时间到,此时返回0
- 进程收到信号并从信号处理函数返回,此时返回剩下的秒数
3.8 Job Control信号
SIGCHLD 子进程暂停或终止
SIGSTOP 进程暂停
SIGCONT 进程恢复运行
SIGTSTP 交互式暂停(Ctrl-Z)
SIGTTIN 后台进程读控制台
SIGTTOU 后台进程写控制台
阅读(2928) | 评论(0) | 转发(0) |