Linux下的进程(五):信号 - HeavyYuan/A-CD-Record-Management-System GitHub Wiki
信号是软件中断。
信号提供了一种处理异步事件的方法。常见的如:
键盘输入ctrl + c,(终端)给前台进程发送SIGINT信号,终止了进程运行;
键盘键入ctrl + z ,终端发送SIGTSPT给前台进程,挂起进程。
每个信号都有一个编号和名字。
编号是从1开始的正整数常量(不存在编号为0的信号)
名字是以SIG打头字符,如SIGINT,SIGTSPT,SIGQUIT
给进程发送信号,可以指定信号编号,也可以指定信号名
kill -INT/-2 pid
- 用户在终端输入某些组合键(如:CTRL + C),引发终端产生信号
- 硬件异常产生信号(由硬件检测到),如除数为0,无效的内存引用。
SIGSEGV信号让进程产生Segmentation fault
- kill函数或命令向任意进程发送信号
- 达到既定的软件条件后,通过发送信号的方式通知其他进程
- 忽略信号。SIGKILL和SIGSTOP不能忽略(系统限制)
- 执行系统默认动作。大多数信号的默认处理是终止该进程
- 捕捉信号。SIGKILL和SIGSTOP不能捕捉(系统限制)
中断的系统调用: 早期的UNIX系统中,系统调用被信号中断后不在继续执行
自动重启动的系统调用: 被信号中断的系统调用,在信号处理程序运行完成后,会自动重启动继续运行。
可重入函数: 函数A在执行过程中,被信号中断,而信号处理程序中也会执行函数A,并且不会出现任何问题。
则,函数A是可重入函数 ,也称这种函数为异步信号安全的(async-singal safe)
未决(Pending): 已经产生(generation),但是还没有递送(delivery)给进程的信号,此种状态称之为Pending.
sigpending函数可以获取Pending状态的信号
信号集(Signal Set): 表示多个信号的集合,对应数据类型为sigset_t
对信号集操作的函数有:sigemptyset,sigfillset,sigaddset,sigdelset,sigismember
信号屏蔽字(Signal Mask): 是一个信号集合,该集合内的信号都会被阻塞而不被递送给进程
sigprocmask函数可以返回当前信号屏蔽字和设置信号屏蔽字
信号的应用可以实现多进程控制,进程协作等能力。
这可以帮助用户实现更为人性化的功能,但是也增加了一下复杂度,以下问题需要在信号应用中作细致考虑
一个实例是sleep函数的实现,一个实例是临界代码的保护
-
#include <stdio.h> #include <signal.h> #include <unistd.h> static void sig_alrm(int signo) { /* nothing to do, just return to wake up the pause */ } unsigned int sleep1(unsigned int seconds) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(seconds); alarm(seconds); /* start the timer */ pause(); /* next caught signal wakes us up */ return(alarm(0)); /* turn off timer, return unslept time */ }
alarm和pause之间存在竞争条件。
在一个繁忙的系统中,可能存在alarm在pause之前就超时,并调用信号处理程序。
如果后续没有捕捉到其他信号,程序将永远被挂起。
解决办法是引入setjmp和longjmp,如下:
#include <setjmp.h> #include <signal.h> #include <unistd.h> #include <stdio.h> static jmp_buf env_alrm; static void sig_alrm(int signo) { longjmp(env_alrm, 1); } unsigned int sleep2(unsigned int seconds) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(seconds); if (setjmp(env_alrm) == 0) { alarm(seconds); /* start the timer */ pause(); /* next caught signal wakes us up */ } return(alarm(0)); /* turn off timer, return unslept time */ }
即使出现sleep1的情况,longjmp返回后不再满足执行pause的条件,sleep2会返回。
保护临界代码不受信号中断的影响
int main()
{
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset($newmask, SIGINT);
/*block SIGINT and save current signal mask*/
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_says("SIG_BLOCK error");
/*Critical region of code*/
/*restore signal mask, which unblocks SIGINT*/
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_says("SIG_SETMASK error");
pause(); /*wait for signal to occur*/
/*continue processing*/
}
第二个sigprocmask解除对SIGINT的阻塞,其和pause之间存在竞争条件。
如果在解除阻塞时 (还没有解除)到 执行pause前,信号到达,此时信号会被丢弃。
而后续可能不会再有信号到达,因此会永远pause下去。
解决办法是引入sigsuspend函数,该函数在一个原子操作中先恢复信号屏蔽字,然后是进程进入休眠,直到捕捉到
一个信号,该函数返回。上述代码修改成:
int main()
{
sigset_t waitmask;
...
/*Pause , allowing all signals except signals in `waitmask`*/
if(sigsuspend(&waitmask) != -1)
err_says("sigsuspend error");
/*restore signal mask, which unblocks SIGINT*/
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_says("SIG_SETMASK error");
}
sigsuspend
函数返回后,其将信号屏蔽字恢复成调用它之前的值。
sigsuspend
函数的局限性在于,无法处理在等待信号期间希望调用其他系统函数的场景,其只适用于在等待信号
期间希望去休眠的场景。
在问题一中,sleep2用到setjmp
和longjmp
来解决条件竞争导致的进程永久pause问题。
但setjmp
和longjmp
带来另外的问题是,在有多个信号处理程序在执行时,xxxjmp
实现的信号处理程序的执行,
会中断其他信号处理程序的执行,让其他信号处理程序提早结束。如:
#include "apue.h"
unsigned int sleep2(unsigned int);
static void sig_int(int);
int
main(void)
{
unsigned int unslept;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
unslept = sleep2(5);
printf("sleep2 returned: %u\n", unslept);
exit(0);
}
static void
sig_int(int signo)
{
int i, j;
volatile int k;
/*
* Tune these loops to run for more than 5 seconds
* on whatever system this test program is run.
*/
printf("\nsig_int starting\n");
for (i = 0; i < 300000; i++)
for (j = 0; j < 4000; j++)
k += i * j;
printf("sig_int finished\n");
}
sleep2的返回打断了sig_int的执行,使其没有执行第2个printf.
问题存在于sleep2的实现中用到了xxxjmp
非局部跳转函数,sleep的可靠实现如下:
#include "apue.h"
static void
sig_alrm(int signo)
{
/* nothing to do, just returning wakes up sigsuspend() */
}
unsigned int
sleep(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
/* set our handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* block SIGALRM and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(seconds);
suspmask = oldmask;
/* make sure SIGALRM isn't blocked */
sigdelset(&suspmask, SIGALRM);
/* wait for any signal to be caught */
sigsuspend(&suspmask);
/* some signal has been caught, SIGALRM is now blocked */
unslept = alarm(0);
/* reset previous action */
sigaction(SIGALRM, &oldact, NULL);
/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}
其摒弃了非局部跳转(xxxjmp
),所以不存在对其他信号处理程序有任何影响。
但程序没有处理与以前设置的闹钟的交互作用(POSIX.1没有显示定义交互标准)(P269有自定义的处理:取小值)
在应用慢速系统调用时,需要对其执行时间做限制,并且在时间到时中断系统调用的执行。
但是有些系统调用时自动重启动的,此时有如下解决方案:
-
非局部跳转函数
xxxjmp
#include "apue.h" #include <setjmp.h> static void sig_alrm(int); static jmp_buf env_alrm; int main(void) { int n; char line[MAXLINE]; if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); if (setjmp(env_alrm) != 0) err_quit("read timeout"); alarm(10); if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) err_sys("read error"); alarm(0); write(STDOUT_FILENO, line, n); exit(0); } static void sig_alrm(int signo) { longjmp(env_alrm, 1); }
信号递送后,直接回跳到
err_sys("read timeout")
-
sigaction函数参数feature
#include <signal.h> int sigaction(int sigon,const struct sigaction *restrict act, struct sigaction *restrict oact) struct sigaction{ void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; /*signal options*/ void (*sa_sigaction)(int, siginfo_t *, void *); } if(sa_flag & SA_INTERRUPT == 1) /*则由此信号中断的系统调用不自动重启动*/
在执行了业务逻辑之后,应该恢复原来的信号环境
这里的信号环境包括(可能没有覆盖完全):
-
对一个或多个信号的处理方式,执行业务逻辑后要恢复原貌
如:在业务逻辑中对SIGINT做了捕获,那在业务逻辑之后要恢复其原来的处理方式。
-
信号屏蔽字,执行业务逻辑后要将信号屏蔽字恢复到原貌
对于第1点,sigaction
较signal
更方便和效率。
对于第2点,sigprocmask
对信号屏蔽字的管理非常充分
另,在信号场景中有非局部跳转时,应该用
sigsetjmp(sigjmp_buf env, int savemask)
和siglongjmp(sigjmp_buf env, int val)
以上函数会确保调用前后的信号屏蔽字保持不变。
sigaction
等接口对信号由更强的控制力度。本节总结这些接口功能和特性。
#include <signal.h>
int sigaction(int sigon,const struct sigaction *restrict act,
struct sigaction *restrict oact)
struct sigaction{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags; /*signal options*/
void (*sa_sigaction)(int, siginfo_t *, void *);
}
功能:检查或修改(或检查并修改)于指定信号相关联的处理动作。
#include <signal.>
int sigemptyset(sigset_t *set); //初始化信号集set,清除其中的所有信号
int sigfillset(sigset_t *set); //初始化信号集set,使其包括所有信号
int sigaddset(sigset_t *set, int signo); //添加信号signo到集合set中
int sigdelset(sigset_t *set, int signo); //从set中删除信号signo
4个函数返回值:成功返回0,出错返回-1;
int sigismember(sigset_t *set, int signo);
返回值:真返回1,假返回0;
/*-------------------------------------------------------------*/
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
返回值:成功返回0,出错返回-1
/*
oset非空,则返回当前信号屏蔽字存于oset.
set非空,how = SIG_BLOCK, 屏蔽set指向的信号集合
how = SIG_UNBLOCK, 解除屏蔽set指向的信号集合
how = SIG_SETMASK, 设置进程新的信号屏蔽字为set指向的值
*/
/*sigpromask的常规用法是:*/
sigset_t newmask,oldmask;
sigaddset(&newmask, SIGINT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask); //屏蔽并且保存当前屏蔽字
...
sigprocmask(SIG_SETMASK, &oldmask,NULL); //恢复原来的屏蔽字
/*----------------------------------------------------------------------------*/
int sigdepending(sigset_t *set); //获取depending状态的信号,并添加到set中
返回值:成功返回0,出错返回-1
/*通常用法*/
sigset_t pendmask;
if(sigpending(&pendmask) < 0)
printf("sigpending error\n");
if(sigismember(&pendmask,SIGQUIT))
printf("SIGQUIT pending\n");
/*-------------------------------------------------------------------------------------*/
int sigsuspend(const sigset_t *sigmask); //恢复信号屏蔽字,并进入休眠,直到收到一个信号
返回值:-1,并将error设置为EINTR
/*通常用法*/
sigset_t waitmask;
...
/*Pause, allowing all signals except ones in waitmask*/
if(sigsuspend(&waitmask) != -1)
printf("sigsuspend error");
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
返回值:成功返回0,出错返回-1
在激活信号排队特性后,需要用`sigqueue`来发送信号该单个进程