重点:信号意义、几种常见信号、signal、信号的阻塞和未决、sigprocmask、sigpending、sigaction、sigsuspend、竞争条件
1.信号
信号是软件中断,信号提供了一种处理异步事件的方法:产生信号的事件是随机出现的,需要告诉内核当什么信号发生时该执行什么操作。
定义在<signal.h>里(本机实际位置:/usr/include/bits/signum.h),形式:“#define 信号名 信号编号” ,如下图,不存在编号为0的信号。
信号处理动作:1.忽略;2.捕捉;3.执行系统默认动作(大多数是终止该进程)
几种常见信号及其产生原因:
SIGKILL和SIGSTOP两种信号不能被忽略也不能被捕捉
信号名 | 说明(产生原因) |
SIGABRT | 调用abort( ),异常终止 |
SIGALRM | 定时器超时alarm( ) |
SIGCHLD | 进程终止或停止时, 发送给其父进程 |
SIGCONT | 发送给需要继续运行, 但出于停止状态的进程 |
SIGHUP | 终端检测到连接断开, 则将此信号发送给控制 进程 |
SIGINT | 按中断键 Ctrl + C |
SIGKILL | 杀死任一进程 |
SIGQUIT | 按退出键 Ctrl + \ |
SIGSTOP | 停止一个进程 |
SIGTERM | kill命令发送 |
SIGTSTP | 停止信号,发送给前台 进程组的所有进程 |
SIGUSR1 | 用户定义的信号,可用 于应用程序 |
SIGUSR2 | 同SIGUSR1 |
SIGTTIN | 后台进程组的进程试图 读控制终端 |
SIGTTOUT | 后台进程组的进程试图 写控制终端 |
2.signal
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
成功:返回以前的信号配置;出错,返回SIG_ERR
第二个参数:SIG_IGN、SIG_DFL、或信号捕捉函数的函数名。
从函数定义可看出,对signal来说,不改变信号处理方式就不能确定信号的当前处理方式,sigaction可对此作出改善。
因为子进程在开始时复制了父进程的内存映像,所以子进程继承父进程的信号处理方式。
练习程序:
#include <stdio.h>
#include <signal.h>
static void sig_usr(int);
int main()
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("can't catch signal SIGUSR1\n");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("can't catch signal SIGUSR2\n");
for (; ;)
pause();//在接到信号前,一直挂起
return 0;
}
static void sig_usr(int signo)
{
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
if (signo == SIGUSR2)
printf("received SIGUSR2\n");
}
结果:
分析:
1.pause函数会导致进程在接到一个信号前,会一直处于挂起状态。
2.调用不带参数的kill,默认会发送SIGTERM信号,而对该信号的默认处理方式是终止该进程。
3.不可靠信号
不可靠指的是:
信号可能会丢失:一个信号发生了,但进程可能会不知道。
对信号控制能力差:无法阻塞信号
进程每次接收到信号对其进行处理时,随即将该信号的动作重置为默认值
不能关闭信号
4.中断的系统调用
低速系统调用和其他系统调用
低速系统调用:可能会使进程永远阻塞的一类系统调用
早起Unix:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR
为使应用程序不必处理被中断的系统调用,BSD引进了某些被中断系统调用的自动重启动。自动重启动的系统调用有:ioctl、read、readv、write、writev、wait、waitpid
自动重启动可能会引起问题,所以允许禁用此功能。
引入自动重启动理由是:不必每次对读、写系统调用进行是否出错的测试,如果是被中断的,则再调用读、写系统调用。
5.可重入函数
进程捕捉到信号并对其进行处理,进程正在执行的指令序列就被信号处理程序临时中断,首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续在捕捉到信号时进程正在执行的正常指令中返回。在信号处理程序中,不能判断捕捉到信号时进程在何处执行,这样不能保证在中断处理结束后能够正确返回到进程的执行指令中。为了保证进程在处理完中断后能够正确返回,需要保证调用的是可重入的函数。
不可重入函数包括:(1)使用静态数据结构,(2)调用malloc或free,(3)标准I/O函数。
信号处理程序中调用一个不可重入的函数,则结果是不可预测的。例如getpwnam函数是个不可重入的,因为其结果存放在静态存储单元中,信号处理程序调用后,返回给正常调用者的信息可能是被返回给信号处理程序的信息覆盖。
6.kill、 raise
kill向进程或进程组发送信号,raise向自身发送信号
#include <signal.h>
int kill(pid_t pid, int signo)
int raise(int signo)
返回值:成功,返回0;出错,返回-1
raise(signo)<=>kill(getpid(), signo)
kill的pid:
pid>0:发送给pid
pid<0:发送给|pid|
pid=0:发送给同一进程组所有进程
pid=-1:发送给有权限发送的所有进程
练习程序:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
static void sig_usr1(int signo)
{
printf("received signal SIGUSR1\n");
}
static void sig_chld(int signo)
{
printf("received signal SIGCHLD\n");
}
int main()
{
pid_t pid;
if ((pid = fork()) < 0)
{
printf("fork() error\n");
exit(-1);
}
if (pid == 0)
{
signal(SIGUSR1, sig_usr1);
raise(SIGUSR1);//子进程向自身发送信号
pause();//等待父进程发送信号
}
else
{
sleep(1);//等待子进程执行
signal(SIGCHLD, sig_chld);
kill(pid, SIGKILL);//杀死子进程
sleep(2);//等待子进程终止,捕捉信号SIGCHLD
}
exit(0);
}
结果:
分析都在代码注释里
7.alarm、 pause
#include <unistd.h>
//设置一个定时器
unsigned int alarm(unsigned int seconds);
返回值:0或以前设置的闹钟时间的余留秒数
若seconds=0,则取消以前设置的闹钟时间,余留秒数仍作为返回值
//使调用进程挂起,直到捕捉到一个信号
int pause(void);
返回值:-1,errno设置为EINTR
程序练习:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
{\
perror(m);\
exit(EXIT_FAILURE);\
}
void sig_alrm(int signo)
{
printf("received SIGALRM\n");
}
int main()
{
signal(SIGALRM, sig_alrm);
int time_origin = alarm(0);
alarm(2);
int time_now = alarm(5);
printf("origin time is: %d\n", time_origin);
printf("now time is: %d\n", time_now);
pause();
printf("after pause()\n");
exit(0);
}
结果:
8.信号集
信号集是一个能表示多个信号的数据类型,用sigset_t定义一个信号集
int sigemptyset(sigset_t *set);//初始化,清除所有信号
int sigfillset(sigset_t *set);//初始化,包括所有信号
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
//成功,返回0;出错,返回-1
int sigismember(sigset_t *set, int signo);
//若真,返回1;若假,返回0
所有应用程序在使用信号集之前,要对该信号集调用sigemptyset或sigfillset一次
程序练习:
#include <stdio.h>
#include <signal.h>
void printsig(const sigset_t *set)
{
//未列出全部信号
if (sigismember(set, SIGINT))
printf("SIGINT\n");
if (sigismember(set, SIGQUIT))
printf("SIGQUIT\n");
if (sigismember(set, SIGALRM))
printf("SIGALRM\n");
if (sigismember(set, SIGUSR1))
printf("SIGUSR1\n");
int i;
for (i = 1; i < NSIG; i++)//NSIG:最大信号编号
{
if (sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main()
{
sigset_t set;
sigemptyset(&set);
printsig(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGUSR1);
printsig(&set);
sigdelset(&set, SIGINT);
printsig(&set);
exit(0);
}
结果:
9.信号的阻塞和未决
参考文章:信号的阻塞和未决(写的很好)
实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
概括如下:
若sigprocmask将一个信号集设置为信号屏蔽字时,当该信号集中的信号发生后,在递送到信号处理程序过程中,因为被阻塞所以出于未决状态。此时,通过sigpending就可取出处于未决状态的信号集。
10.sigprocmask、 sigpending
//将信号集中的信号设为信号屏蔽字
int sigprocmask(int how, sigset_t *restrict set, sigset_t *restrict oset);
how:
SIG_BLOCK:set包含我们希望加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK:set包含我们希望从当前信号屏蔽字删除的信号,相当于mask=mask& ~set
SIG_SETMASK:设置当前信号屏蔽字为set,相当于mask=set
oset为非空指针,则当前信号屏蔽字通过oset返回
//取出处于未决状态的信号集
int sigpending(sigset_t *set);
成功,返回0;出错,返回-1
程序练习:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
//打印全部信号编号
static void print_sig(const sigset_t *set)
{
int i;
for (i = 1; i < NSIG; i++)
{
if (sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
//信号处理程序
static void sig_quit(int signo)
{
printf("caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
ERR_EXIT("signal error");
}
int main()
{
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
ERR_EXIT("signal error");
sigset_t newmask;
sigset_t oldmask;
sigset_t pendmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
sigpending(&pendmask);
print_sig(&pendmask);
sleep(5);//此期间产生退出信号
sigpending(&pendmask);
print_sig(&pendmask);
if (sigismember(&pendmask, SIGQUIT))
printf("SIGQUIT pending\n");
sigprocmask(SIG_SETMASK, &oldmask, NULL);
sleep(5);//再次产生退出信号
exit(0);
}
结果:
分析:
通过第一次调用sigprocmask后就调用sigpending来获取信号集并打印,可知,sigpending返回的是处于未决状态的信号集,而不是信号屏蔽字。只有一个信号产生了并被阻塞处于未决状态时,sigpending才返回该信号集
另:系统不会对信号进行排队,即阻塞期间产生多次SIGQUIT,但只向进程递送一次SIGQUIT
11.sigaction
//功能:检查或修改(或检查并修改)与指定信号相关联的处理动作,取代早期的signal函数
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
struct sigaction
{
void (*sa_handler)(int); //信号处理函数
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction)(int, siginfo_t *, void *);
};
sa_handler:信号捕捉函数的地址;
sa_mask:调用信号捕捉函数前,该信号集被加到信号屏蔽字中,从而在调用信号捕捉函数时,能阻塞某些信号。
sa_flags:SA_INTERRUPT:中断的系统调用不自动重启动;SA_RESTART:中断的系统调用自动重启动;......
sa_sigaction:替代的信号处理程序,一次只能使用sa_handler和sa_sigaction中的一个。
说明:同一信号多次发生,并不将它们加入队列;如:某种信号阻塞时发生了5次,解除阻塞后,信号处理函数只调用一次。
linux下默认是不自动重启动的。
用sigaction实现signal,程序如下:
#include <stdio.h>
#include <signal.h>
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func)
{
printf("now in my signal\n");
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
sigaction(signo, &act, &oact);
return(oact.sa_handler);
}
void sig_func(int signo)
{
printf("catched signal\n");
}
int main()
{
signal(SIGALRM, sig_func);
alarm(1);
pause();
exit(0);
}
结果:
12.sigsetjmp、 siglongjmp
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
若savemask非0,则sigsetjmp在env中保存了进程当前信号屏蔽字。当调用siglongjmp时,如果带非0的sigsetjmp已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
调用信号处理函数时,再次产生一个信号,并通过siglongjmp返回,程序如下:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <time.h>
#include <errno.h>
static void sig_usr1(int);
static void sig_alarm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump;
void pr_mask(const char* str);
int main()
{
signal(SIGUSR1, sig_usr1);
signal(SIGALRM, sig_alarm);
pr_mask("starting main:");
if (sigsetjmp(jmpbuf,1))
{
pr_mask("ending main:");
exit(0);
}
canjump = 1;
for (; ;)
pause();
}
static void sig_usr1(int signo)
{
time_t starttime;
if (canjump == 0)
return;
pr_mask("starting sig_usr1: ");
alarm(3);
sleep(5);
pr_mask("finishing sig_usr1: ");
canjump = 0;
siglongjmp(jmpbuf, 1);
}
static void sig_alarm(int signo)
{
pr_mask("in sig_alrm: ");
}
void pr_mask(const char *str)
{
sigset_t sigmask;
sigprocmask(0, NULL, &sigmask);
printf("%s", str);
if (sigismember(&sigmask, SIGINT))
printf(" SIGINT");
if (sigismember(&sigmask, SIGALRM))
printf(" SIGALRM");
if (sigismember(&sigmask, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigmask, SIGQUIT))
rintf(" SIGQUIT");
printf("\n");
}
结果:
分析:
1.打印信号屏蔽字pr_mask("starting main:"),无
2.调用sigsetjmp,在jmpbuf中保存进程当前的信号屏蔽字
3.发送SIGUSR1信号给进程
4.接收到信号,进入信号处理程序,打印此时的信号屏蔽字,pr_mask("starting sig_usr1");
注:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。 SIGUSR1
5. 3秒后,产生信号SIGALRM,进入另一个信号处理程序,打印此时信号屏蔽字pr_mask("in sig_alrm:");
注:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。 SIGUSR1 SIGALRM
6.返回上一层信号处理程序,打印信号屏蔽字,pr_mask("ending sig_usr1");
注:当从信号处理程序返回时,恢复原来的信号屏蔽字 SIGUSR1
7.调用siglongjmp,返回sigsetjmp位置,siglongjmp从jmpbuf中恢复之前的信号屏蔽字。打印此时信号屏蔽字 pr_mask("ending main:"),无
注:当从信号处理程序返回时,恢复原来的信号屏蔽字 无
本程序还利用了canjump为1从而确保sigsetjmp已经将当前的信号屏蔽字保存到jmpbuf中了;否则不执行信号处理程序,直接返回。
13.sigsuspend
sigsuspend将解除信号屏蔽字和使进程挂起结合成一个原子操作。这样可以解决竞争条件,见第14节。在原子操作中,先恢复信号屏蔽字,然后使进程休眠。进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
int sigsuspend(const sigset_t *sigmask);
返回值:-1,并将errno设置为EINTR
程序如下:
#include <stdio.h>
#include <signal.h>
static void pr_mask(const char *str);
static void sig_int(int signo);
int main()
{
signal(SIGINT, sig_int);
pr_mask("starting main: ");
sigset_t newmask, oldmask, waitmask;
sigemptyset(&newmask);
sigemptyset(&waitmask);
sigaddset(&newmask, SIGINT);
sigaddset(&waitmask, SIGUSR1);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
pr_mask("after newmask: ");
sigsuspend(&waitmask);//恢复之前的信号屏蔽字,现在的信号屏蔽字设置为SIGUSR1,进程休眠。调用sig_int后返回
pr_mask("after return from sigsuspend(): ");
sigprocmask(SIG_SETMASK, &oldmask, NULL);
pr_mask("ending main: ");
return 0;
}
static void sig_int(int signo)
{
pr_mask("in sig_int: ");
}
static void pr_mask(const char *str)
{
sigset_t sigmask;
sigprocmask(0, NULL, &sigmask);
printf("%s", str);
if (sigismember(&sigmask, SIGINT))
printf(" SIGINT");
if (sigismember(&sigmask, SIGALRM))
printf(" SIGALRM");
if (sigismember(&sigmask, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigmask, SIGQUIT))
printf(" SIGQUIT");
printf("\n");
}
结果:
分析:
调用sigsuspend(&waitmask)后,进程原先的信号屏蔽字(SIGINT)被恢复,现在的信号屏蔽字被设置为waitmask(SIGUSR1);
sigsuspend捕捉到一个信号并从信号处理程序返回时,sigsuspend返回。所以中断键引起sig_int被调用,sig_int返回时,sigsuspend返回,并将信号屏蔽字设置为之前的信号屏蔽字(SIGINT)
14.竞争条件和sleep函数
使用alarm和pause函数可实现sleep函数
程序如下:
#include <stdio.h>
#include <signal.h>
static void sig_alrm(int signo)
{
}
unsigned int sleep1(unsigned int seconds)
{
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
return(seconds);
alarm(seconds);
pause();
return(alarm(0));
}
int main()
{
printf("now sleep 3 seconds\n");
sleep1(3);
printf("end.\n");
exit(0);
}
该简单实现有3个问题:
1.如果在调用sleep1之前,调用者已设置了闹钟,则它被sleep1函数中的第一次alarm调用擦除
2.改程序中修改了对SIGALRM的配置
3.在第一次调用alarm和pause之间有一个竞争条件。在一个繁忙的系统中, 可能alarm在调用pause之前超时,并调用了信号处理程序。如果发生了这种情况,则在调用pause后,如果没有捕捉到其他信号,调用者将永远被挂起。虽然alarm的下一行就是pause,但无法保证pause在调用alarm后的seconds秒后一定会被调用,由于时序而导致的错误,叫做竞争条件
sleep:
unsigned int sleep(unsigned int seconds);
返回值:0或未休眠完的秒数
此函数使调用进程挂起直到满足下面两个条件之一:
(1)已经过了seconds所指定的墙上时钟时间
(2)调用进程捕捉到一个信号并从信号处理程序返回
使用alarm实现的sleep函数,该函数可靠的处理信号,避免了竞争条件,程序如下:
#include <stdio.h>
#include <signal.h>
static void sig_alrm(int signo)
{
}
unsigned int sleep(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(seconds);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
sigsuspend(&suspmask);
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
int main()
{
printf("sleep 5 seconds:\n");
int i;
for (i = 0; i < 5; i++)
{
sleep(1);
printf("%d\n", i+1);
}
printf("end\n");
return 0;
}
结果:
分析:
因为sigprocmask先将SIGALRM设置为信号屏蔽字,则就算调用alarm后内核转而去执行其他进程而超时,但由于被屏蔽,所以无法执行信号处理程序。而sigsuspend将解除信号屏蔽和挂起等待合为一个原子操作,则不可能存在解除信号后又去执行其他进程,而原来的进程一直处于等待的情况。所以能够确保解除信号后执行信号处理程序,然后一定会返回,不会使调用进程一直处于阻塞状态。sigprocmask和sigsuspend一起使用就解决了原来的sleep的pause后可能一直阻塞的问题。