信号是软件中断,很多比较重要的应用程序都需要处理信号,信号提供了一种处理异步事件的方法。
有很多条件可以产生信号:
当用户按某些终端键时,引发终端产生的信号。
硬件异常产生信号。
进程调用kill(2)函数可以将任意信号发送给另一个进程或进程组。
用户可以调用kill(1)命令将信号发送给其他进程。
当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。
当某个信号产生,我们可以有以下3中方式处理:
1. 忽略此信号。但是有两种信号不能被忽略,它们是SIGKILL和SIGSTOP。这两个信号不能被忽略的原因是:它们向内核和超级用户提供了是进程终止或停止的可靠方法。
2. 捕捉信号。调用一个用户函数。
3. 执行系统默认动作。
unix系统信号机制最键的接口是signal函数。
#include <signal.h>
void (*signal(int signo , void(*func)(int)))(int));
//返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR
signo参数是信号名。func值是常量SIG_IGN(忽略此信号)、SIG_DFL(系统默认动作)、或当接到此信号后要调用的函数的地址(信号处理函数signal handler或信号捕捉函数signal-catching function)。
signal函数原型太复杂了,如果使用下面的typedef,则可使其简单一些。
typedef void Sigfunc(int);
然后可将signal函数原型写成:
Sigfunc signal(int , Sigfunc );
这个typedef包括在apue.h中,系统不自带。
这是一个简单的程序,它捕捉两个用户定义的信号并打印信号编号。
#include "apue.h"
static void sig_usr(int); //one handler for both signals
int main()
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("cna`t catch SIGUSR1");
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can`t catch SIGUSR2");
for( ; ; )
pause();
}
static void sig_usr(int signo) //argument is signal number
{
if(signo == SIGUSR1)
printf("received SIGUSR1\n");
else if(signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n",signo);
}
当执行一个程序时,所有信号的状态都是系统默认或忽略。exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc,被信号中断,这时会发生什么?
Single UNIX Specification说明了在信号处理程序中保证调用安全的函数。这些函数是可重入的并被称为异步信号安全的(async-signal safe)。除了可重入以外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。
这段程序从信号处理程序my_alarm调用非可重入函数getpwnam,而my_alarm每秒钟都被调用一次。
#include "apue.h"
#include <pwd.h>
static void my_alarm(int signo)
{
struct passwd *rootptr;
printf("in signal handler\n");
if((rootptr = getpwnam("root")) == NULL)
err_sys("getpwnam(root)error");
alarm(1);
}
int main()
{
struct passwd *ptr;
signal(SIGALARM, my_alarm);
alarm(1);
for(;;)
{
if((ptr = getpwnam("sar")) == NULL)
err_sys("getpwnam error");
if(strcmp(ptr->pw_name, "sar") != 0)
printf("return value corrupted!, pw_name = %s\n",
ptr->pw_name);
}
}
当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。
每一个进程都有一个信号屏蔽字 (signal mask),它规定了当前要阻塞递送到该进程的信号集。
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参数有以下4种不同的情况:
pid > 0 将该信号发送给进程ID为pid的进程。
pid = 0 发送给与发送进程同一进程组的所有进程,而且发送线程具有权限向这些进程发送信号。
pid < 0 以绝对值来计算pid
pid == -1 将该信号发送给发送进程有权限向它们发送信号的所有进程。
alarm函数可以设置一个定时器,在将来的某个时刻该定时器会超时。当定时器超时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0或以前设置的闹钟时间的预留秒数
参数seconds的值是产生信号SIGALRM需要经过的时钟秒数。当这一时刻达到时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。
每个进程只能有一个闹钟时间。
如果有以前注册的尚未超时的闹钟时间,而且本次调用的seconds值是0,则取消以前的闹钟时间,其余值仍作为alarm函数的返回值。
pause函数使调用进程挂起直至捕捉到一个信号。
#include <unistd.h>
int pause(void);
//返回值:-1,errno设置为EINTR
只有执行了一个信号处理程序并从其返回时,pause才返回。
使用longjmp ,带时间限制调用read
#include "apue.h"
#include <setjmp.h>
static void sig_alrm(int);
static jmp_buf env_alrm;
int main()
{
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, lin, 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);
}
#include <signal.h>
int sigemptyset(sigset_t *set); //清空
int sigfillset(sigset_t *set); //填满
int sigaddset(sigset_t *set, int signo); //增加
int sigdelset(sigset_t *set, int signo); //删除
//4个函数返回值:若成功,返回0;若出错,返回-1
int sigismember(const sigset_t *set, int signo);//判断
//返回值:若真,返回1;若假,返回0
调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const siget_t *restrict set,
siget_t *restrict oset);
//返回值:若成功,返回0;若出错,返回-1
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
how | 说明 |
---|---|
SIG_BLOCK | 当前信号屏蔽字加上set |
SIG_UNBLOCK | 当前信号屏蔽字去掉set |
SIG_SETMASK | set替换当前信号屏蔽字 |
sigpending函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回。
#include <signal.h>
int sigpending(sigset_t *set);
//返回值:若成功,返回0;若出错,返回-1
sigaction函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act
,struct sigaction *restrict oact);
//返回值:若成功,返回0;若出错,返回-1
其中,参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。如果oact指针非空,则系统经由oact指针返回该信号的上一个动作。
此函数使用下列结构:
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字段包含一个信号捕捉函数的地址(不是常亮SIG_IGN或SIG_DFL),则sa_mask字段说明雷了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。
act结构的sa_flags字段指定对信号进行处理的各个选项。
sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。对于sa_sigaction字段和sa_handler字段两者,实现可能使用同一存储区,所以应用只能一次使用这两个字段中的一个。
通常,按下列方式调用信号处理程序:
void handler(int signo);
但是,如果设置了AS_SIGINFO标志,那么按下列方式调用信号处理程序:
void handler(int signo, siginfo_t *info, void *context);
siginfo结构包含了信号产生原因的有关信息
struct siginfo{
int si_signo; /*signal number*/
int si_errno; /*if nonzero,errno value from<errno.h>*/
int si_code; /*additional info (depends on signal)*/
pid_t si_pid; /*sending process ID*/
uid_t si_uid; /*sending process real user ID*/
void *si_addr; /*addres that caused the fault*/
int si_status; /*exit value or signal number*/
union sigval si_value;/*application-specific value*/
/*possibly other fields also*/
}
union sigval
{
int sival_int;
void *sival_ptr;
}
用sigaction实现的signal函数
#include "apue.h"
/* Reliable version of signal(),using POSIX sigaction().*/
signal(int signo , Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act .sa_mask);
act.sa_flags = 0;
if(signo == SIGALRM){
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else
act.sa_flags |= SA_RESTART;
if(sigaction(signo, &act, &oact) < 0)
return SIG_ERR;
return(oact.sa_handler);
}
非局部转移的setjmp和longjmp函数。
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);
这两个函数和setjmp、longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
信号屏蔽、sigsetjmp和siglongjmp实例
#include "apue.h"
#include <setjmp.h>
#include <time.h>
static void sig_usr1(int);
static void sig_alrm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump;
int main(void)
{
if(signal(SIGUSR1, sig_usr1) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if(signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
pr_mask("starting main: "); /*Figure 10.14*/
if(sigsetjmp(jmpbuf, 1)){
pr_mask("ending main: ");
exit(0);
}
canjump = 1; /*now sigsetjmp() is OK*/
for(;;)
pause();
}
static void sig_usr1(int signo)
{
time_t starttime;
if(canjump == 0)
return; /*unexpected signal, ignore*/
pr_mask("starting sig_usr1: ");
alarm(3); /*SIGALRM in 3 seconds*/
starttime = time(NULL);
for(;;) /*busy wait for 5 seconds*/
if(time(NULL) > starttime + 5)
break;
pr_mask("finishing sig_usr1:");
canjump = 0;
siglongjmp(jmpbuf, 1); /*jump back to main,don`t return*/
}
static void sig_alrm(int signo)
{
pr_mask("in sig_alrm:");
}
目的是保护代码临界区,使其不被特定信号中断
在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由
sigsuspend函数提供的
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//返回值:-1,并将errno设置为EINTR
进程的信号屏蔽字设置为sigmask指向的值。在捕捉到一个信号或发生了一个终止该进程的信号之前,该进程挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
保护临界区不被信号中断
#include "apue.h"
static void sig_int(int)
int main(void)
{
sigset_t newmask , oldmask , waitmask;
pr_mask("program start:");
if(signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/* * Block SIGINT and save current signal mask. */
if(sigpromask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/* *Critical region of code. */
pr_mask("in critical region:");
/* *Pause, allowing all signals excecpt SIGUSR1. */
if(sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend:");
/* * Reset signal mask which unblocks SIGINT. */
if(sigpromask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/* * And continue processing··· */
pr_mask("program exit: ");
exit(0);
}
static void sig_int(int signo)
{
pr_mask("\nin sig_int:");
}
利用sigsusupend还可以实现等待一个信号处理程序设置一个全局变量。
可以用用心实现父,子进程之间的同步
如果在等待信号期间希望调用其他系统函数可以使用多线程,可专门安排一个线程处理信号。
abort函数的功能是使程序异常终止
#include <stdlib.h>
void abort(void);
//此函数不返回值
此函数将SIGABRT信号发送给调用进程(进程不应该忽略此信号)。
abort()函数导致所有的流被关闭和冲洗。
在程序中执行一串命令字符串。
注意system的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串进程的终止状态。在信号方面是最后要加上一个信号编号。
仅当shell本身异常终止时,system的返回值才报告一个异常终止。
在编写使用system函数的程序时,一定要正确地解释返回值,如果直接调用fork、exec和wait,终止状态与调用system是不同的。
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
//返回值:0或为休眠完的秒数
此函数使调用进程挂起直到满足下面两个条件之一。
1. 已经过了seconds所指定的墙上时钟时间。
2. 调用进程捕捉到一个信号并从信号处理程序返回。
nanosleep函数与sleep函数类似,但提供了纳秒级的精度
#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
//返回值:若休眠到要求的时间,返回0;若出错,返回-1
reqpt参数用秒和纳秒指定需要休眠的时间长度。如果某个信号中断了休眠间隔,程序并没有终止,remtp参数指向的timespec结构就会被设置为未休眠完的时间长度。如果对为休眠完的时间并不感兴趣,可以把该参数置为NULL;
随着多个系统时钟的引入,需要使用相对于特定时钟的延迟时间来挂起调用线程。clock_nanosleep函数提供了这种功能
#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *reqtp,
struct timespec *remtp);
//返回值:若休眠要求的时间,返回0;如出错,返回错误号
clock_id参数指定了计算延迟时间基于的时钟。flags参数用于控制延迟是相对的还是绝对的。flags为0时表示休眠时间是相对的(希望休眠的时间长度),如果flags值设置为TIMER_ABSTIME,表示休眠时间是绝对的(希望休眠到时钟到达某个特定的时间)。
注意,除了出错返回,调用clock_nanosleep和调用nanosleep的效果是相同的。使用相对休眠的问题是有些应用对休眠长度有精度要求,相对休眠时间会导致实际休眠时间比要求的长。
使用排队信号必须做以下几个操作:
1. 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有给出这个标志,信号会延迟,但信号是否进入队里要取决于具体实现。
2. 在sigaction结构的sa_sigaction成员中提供信号处理程序,实现可能允许用户使用sa_handler字段,但不能获取sigqueue函数发送出来的额外信息。
3. 使用sigqueue函数发送信号。
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
//返回值:若成功,返回0;若出错,返回-1
sigqueue函数只能把信号发送给单个进程,可以使用value参数向信号处理程序传递整数和指针值,除此之外,sigqueue函数与kill函数类似。
信号不能被无限排队,达到相应的限制(SIGQUEUE_MAX)以后,sigqueue就会失败,将errno设为EAGAIN。
随着实时信号的增强,引入了用于应用程序的独立信号集。这些信号的编号在SIGRTMIN~SIGRTMAX之间,包括这个限制值。注意,这些信号的默认行为是终止进程。
可以使用psignal函数可移植地打印与信号编号对应的字符串
#include <signal.h>
void psignal(int signo, const char *msg);
字符串msg输出到标准错误文件,后面跟随一个冒号和一个空格,再后面对该信号的说明,最后是一个换行符。如果msg为NULL,只有信号说明部分输出到标准错误文件。
如果在sigaciton信号处理程序中有siginfo结构,可以使用psiginfo函数打印信号信息。
#include <signal.h>
void psiginfo(const siginfo_t *info, const char *msg);
如果只需要信号的字符描述部分,也不需要把它写到标准错误文件中,可以使用strsignal函数。
#include <string.h>
char *strsignal(int signo);
//返回值:指向描述该信号的字符串的指针
完