Unix 信号大全
信号的概念:
信号,当进程调用a b o r t函数时产生这种信号。S I G A L R M是闹钟信号,当由a l a r m函数设置的时
间已经超过后产生此信号。 V 7有1 5种不同的信号, S V R 4和4 . 3 + B S D均有3 1种不同的信号。
在头文件< s i g n a l . h >中,这些信号都被定义为正整数(信号编号) 。没有一个信号其编号为
0。在1 0 . 9节中将会看到 k i l l函数,对信号编号0有特殊的应用。P O S I X . 1将此种信号编号值称为
空信号。
信号的分类:
在信号表中我们可以看到1-31 的信号为传统Unix支持的信号,是不可靠的信号(非实时的),编号32-64为后来扩充的信号,是可靠的信号(实时的)。不可靠和可靠的信号的区别在于前者不支持排队,可能造成信号的丢失,而后者不会。
很多条件可以产生一个信号。
• 当用户按某些终端键时,产生信号。在终端上按D E L E T E键通常产生中断信号(S I G I N T) 。
这是停止一个已失去控制程序的方法。 (第11章将说明此信号可被映射为终端上的任一字符。 )
• 硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将
其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无
效存储访问的进程产生一个 S I G S E G V。
• 进程用k i l l ( 2 )函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号进程
和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
• 用户可用 k i l l ( 1 )命令将信号发送给其他进程。此程序是 k i l l函数的界面。常用此命令终止
一个失控的后台进程。
• 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬
件产生条件(如被 0除) ,而是软件条件。例如 S I G U R G (在网络连接上传来非规定波特率的数
据) 、S I G P I P E (在管道的读进程已终止后一个进程写此管道 ),以及S I G A L R M (进程所设置的闹
钟时间已经超时 )。
信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能只是测
试一个变量 (例如e r r n o )来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请
执行下列操作” 。
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。
(1) 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。
它们是:S I G K I L L和S I G S TO P。这两种信号不能被忽略的原因是:它们向超级用户提供一种使
进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问
或除以0) ,则进程的行为是未定义的。
(2) 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用
户函数中,可执行用户希望对这种事件进行的处理。例如,若编写一个命令解释器,当用户用
键盘产生中断信号时,很可能希望返回到程序的主循环,终止系统正在为该用户执行的命令。
如果捕捉到S I G C H L D信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用 w a i t p i d
以取得该子进程的进程I D以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要
为S I G T E R M信号编写一个信号捕捉函数以清除临时文件( k i l l命令传送的系统默认信号是终止
信号) 。
(3) 执行系统默认动作。表 1 0 - 1给出了对每一种信号的系统默认动作。注意,对大多数信
号的系统默认动作是终止该进程。
信号的处理机制
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应与该信号(内核通过在进程的struct task_struct 结构中的信号域中设置对应位其来实现相一个进程发送信号)。
如果信号发送给一个正在谁秒的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不是唤醒进程。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时,所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完信号。内核处理一个进程收到的软中断是在该进程的上下文中,因此,进程必须处于运行状态。处理信号有三种类型:进程接收到信号后推出:进程忽略该信号;进程收到信号后执行用户自定义的使用系统调用signal()注册函数。当进程接收到一个它忽略的信号时,进程丢去该信号,就像从来没有收到该信号似的,而继续运行。如果进程收到一个要捕获的信号,那么进程返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回再弹出栈顶时,才返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核状态下运行的话,用户就可以获取任何权限)
信号的处理
signal function :
要对一个信号进行处理,就需要给出信号发生时系统所调用的处理函数。可以为一个特定的信号(除去无法捕捉的SIGKILL和SIGSTOP信号)注册相应的处理函数。如果正在运行的程序源代码里注册了针对某一个特定信号的处理程序,不论当时程序执行到何处,一旦进程接到收到该信号,相应的调用就会发生。所以说信号的发送和处理是异步的。
通过调用signal()函数来注册某个特定信号的处理程序,它的函数原型如图下:
#include
void ( *signal(int signum,void (*handler)(int))) (int)
返回:如果成功则返回以前的信号处理配置,若出错则为SIG_ERR。
参数signum表示所注册函数针对的信号,其取值为上面的信号宏定义。handler的取值是:(a)常数SIG_IGN,或者(b)常数SIG_DEL,或(c)当接到此信号后要调用的函数的地址。如果指定SIG_IGN,则向内核表示忽略此信号。(注意有两个信号SIGKILL和SIGSTOP不能忽略)如果指定SIG_DEL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,我们称此为捕捉此信号,并称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。
如果查看系统的头文件,则可能会找到下列形式的说明:
#define SIG_ERR (void (*) () ) -1
#define SIG_DEL (void(*)())0
#define SIG_IGN (void(*)()) 1
这些常数可以表示指向函数的指针,该函数需要一个整形参数,而且无返回值。signal的第二个参数及其返回值就可用它们表示。这些常数所使用的三个值不一定要是-1,0,和1,但他们必须要是三个值而决不能是任何一可说明函数的地址。大多数UNIX系统使用上面所示的值。
程序:
EG1:捕捉终端键入“Ctrl+c”时产生的SIGINT信号。
source code :
#include
#include
#include
void SignHandler(int iSignNum)
{
printf("Capture signal number : %d ", iSignNum);
}
int main(void)
{
signal(SIGINT,SignHandler);
while(1)
sleep(1);
return 0 ;
}
EG 2:忽略终端键入“Ctrl+c”时产生的SIGINT信号
#include
#include
int main(void)
{
int i =0;
signal (SIGINT,SIG_IGN);
while(1)
{
printf("i = %d ",i++);
sleep(1);
}
exit(0);
}
EG2 :接收信号的默认处理方,接收默认处理就相当于没有写信号处理程序。
#include
#include
int main(void)
{
signal(SIGINT,SIG_DEF);
while(1)
sleep(1);
return 0 ;
}
EG3 :一个程序中注册多个新号
#include
#include
#include
#include
void sigroutine(int dunno )
{
switch (dunno)
{
case 1: printf("Capture SIGHUP signal,the signal number is %d ",dunno); break ;
case 2: printf("Capture SIGINT signal,the signal number is %d ",dunno); break ;
case 3 :printf("Capture SIGQUIT signal,the signal number is %d ",dunno); break;
}
return ;
}
int main(int argc ,char *argv[])
{
printf("process ID is %d ",getpid());
if(signal(SIGHUP,sigroutine) == SIG_ERR)
{
printf("Could"t register signal handler for SIGHUP! ");
}
if(signal(SIGINT,sigroutine) == SIG_ERR)
{
printf("Couldn"t register signal handler for SIGINT! ");
}
if(signal(SIGQUIT,sigroutine)==SIG_ERR)
{
printf("Couldn"t register signal handler for SIGQUIT! ");
}
while(1)
sleep(1);
return 0 ;
}
EG4
#include
#include
#include
static void sig_usr(int) ;//one handler for both signals
int main(void)
{
if(signal(SIGUSR1, sig_usr)==SIG_ERR)
printf("can"t catch SIGUSR1");
if(signal(SIGUSR2, sig_usr) == SIG_ERR )
printf("can"t catch SIGUSR2");
for (;;)
pause() ;
}
static void sig_usr(int signo)
{
if(signo == SIGUSR1)
printf("received SIGUSR1 ");
else if (signo == SIGUSR2)
printf("received SIGUSR2 ");
else
printf("received signal %d ",signal);
return 0 ;
}
Program Analysis:
我们使该程序在后台运行,并且用 k i l l ( 1 )命令将信号送给它。注意,在 U N I X中,杀死( k i l l )这个术语是不恰当的。k i l l ( 1 )命令和k i l l ( 2 )函数只是将一个信号送给一个进程或进程组。该信号是否终止该进程则取决于该信号的类型,以及该进程是否安排了捕捉该信号。
$ a.out & 在后台启动进程
[1] 4720 作业控制s h e l l打印作业号和进程I D
$ kill -USR1 4720 向该进程发送 S I G U S R 1
received SIGUSR1
$ kill -USR2 4720 向该进程发送 S I G U S R 2
received SIGUSR2
$ kill 4720 向该进程发送 S I G T E R M
[1] + Terminated a.out &
当向该进程发送 S I G T E R M信号后,该进程就终止,因为它不捕捉此信号,而对此信号的系统
默认动作是终止。
sigaction function:
Linux还提供了另外一个功能更为强大的信号处理机制--sigaction系统调用。sigaction函数的功能是检查或者修改(或两者)与指定信号相关联的处理动作,此函数可以完全替代signal函数。sigaction函数原型如下:
#include
int sigaction(int signum ,const struct sigaction *act,struct sigaction *old);
返回:若成功则返回0 ,若出错返回-1.
参数signum为需要捕捉的信号。参数act是一个结构体,里面包含信号处理函数的地址,处理方式等信息。参数oldact是一个传出参数,sigaction函数调用成功后,oldact里面包含以前对signum信号的处理方式的信息。
结构体struct sigaction(注意,名称与函数sigaction相同)的原型为:
struct sigaction{
void (*sa_handler)(int) ; // 老类型的信号处理函数指针
void (*sa_sigaction)(int,siginfo_t *,void *);//新类型的信号处理函数指针
sigset_t sa_mask ; //将要被阻塞的信号
int sa_flags ; //信号处理方式掩码
void (*sa_restore)(void);//保留,不使用
}
sigaction函数不但可以实现signal函数的功能,而且还可以提供更详细的信息,确切了解进程接收到信号时所发生的具体细节。程序EG 5 显示了sigaction函数的功能,当终端没有产生SIGINT或SIGQUIT(ctrl+)信号时,程序能很好地执行read()函数,即读入终端输入的字符串;当SIGINT或SIGQUIT信号产生时,进程被信号中断,read出错退出了。
EG 6:
#include
#include
#include
#include
int g_iSeq = 0 ;
void SignHandlerNew (int iSignNo , siginfo_t *plnfo , void *pReserved)
{
int iSeq = g_iSeq++ ;
printf("%d enter SignHndlerNew , signo: %d.s ",iSeq, iSignNo) ;
sleep(3);
printf("%d Leave SignHandlerNew , signo : %d ",iSeq ,iSignNo );
}
int main(int argc ,char * argv [] )
{
char szBuf [20] ; //
int iRet;
struct sigaction act ;//signal process struct
act.sa_sigaction = SignHandlerNew ;//process function
act.sa_flags=SA_SIGINFO ; //表明信号处理函数由sa_sigaction指定
sigemptyset(&act.sa_mask);//信号集处理函数,将act.sa_mask所指向的信号集清空,即不包含任何信号
sigaction(SIGINT,&act,NULL) ; //注册SIGINT 信号
sigaction(SIGQUIT,&act,NULL) ;//注册SIGQUIT 信号
do{
iRet =read(STDIN_FILENO,szBuf,sizeof(szBuf)-1) ;//从标准输入读入数据
if(iRet<0)
{
perror("read fail");
break;
}
szBuf[iRet] =0 ;
printf("Get :%s",szBuf);//打印终端输入的字符串
}while(strcmp(szBuf,"quit ")!=0);//输入“quit”时退出出寻根
return 0 ;
}
program output :
[root@localhost 10]# ./sigaction
zz
Get :zz
^C
0 enter SignHndlerNew , signo: 2
^
1 enter SignHndlerNew , signo: 3
1 Leave SignHandlerNew , signo : 3
0 Leave SignHandlerNew , signo : 2
read fail: Interrupted system call
信号的发送
1:kill function
kill function用于给某一个给定进程或进程组发送信号,函数原型如下:
#include
#include
int kill(pid_t pid , int signum);
reback value:success 0 , fail -1
参数pid表示kill函数发送信号对象的进程或进程组号,其取值有几种情况,如下表:signum表示发送的信号
Pid取值及对应含义
Pid >0 将信号发送给进程号为pid的进程
Pid =0 将信号发送给和目前进程相同进程组的所有进程
Pid <0 &&pid!=1 向进程组id为pid绝对值的进程组中的所有进程发送信号
Pid =-1 除发送进程自身外,向所有进程ID大于1的进程发送信号(POSIX.1未定义此情况)
Signum是信号值,当为0时(即是空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限只能向属于同一个session(会话)或者同一个用户的进程发送信号)。
Kill()最常用于pid>0时的信号发送,调用成功返回0;否则,返回-1.
Eg7演示了父进程利用kill()函数向其子进程传送一个SIGABRT信号,使子进程非正常结束。
#include
#include
#include
#include
#include
int main(int argc ,char * argv[])
{
pid_t pid ;
int status;
if( !(pid = fork()))
{
printf("Hi I am child process! ");
sleep(10);
printf("Hi I am child process,again1 ");
return ;
}else {
printf("send signal to child process(%d) ",pid);
sleep(1);
if( kill(pid,SIGABRT) == -1 )
{
printf("kill failed ");
}
wait(&status);
if(WIFSIGNALED(status))
printf("child process receive signal %d ",WTERMSIG(status));
}
return 0 ;
}
程序运行结果分析:
Program output :
send signal to child process(27794)
Hi I am child process!
child process receive signal 6
Why doesn‘t output “Hi I am child process,again1”, that reason is that the subprocess has been end due to having received the SIGABRT signal sent by its parent process , so 子进程睡眠醒来的时候(或者被唤醒)的时候就被”夭折”了.
2:raise function
Raise function used send the signal to itself , the function prototype is as follows :
#include
#include
Int raise (int signum) ;
Function return : success 0 , fail -1 ;
Argument : signum is the signal sent
Raise 函数使用简单,下面的程序利用raise函数向自身的进程发送了一个SIGABRT信号,这会使得进程非正常结束。
EG8:
#include
#include
#include
int main(void )
{
printf("Hello , I like Linux c programs! ");
if(raise(SIGABRT) == -1 )
{
//send failly then exit
printf("raise failed! ");
return 0 ;
//exit(1);
}else{
printf("raise successfully! ");
}
//if the process exits abnormally , this sentende does not be executed
printf("Hello , I like C programs ,again! ");
return 0 ;
}
Program output:
Hello , I like Linux c programs!
Aborted (core dumped)
Program analysis:
Because the program received the SIGABRT signal sent by itself , it would exit immediately
3:sigqueue function
Sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,通常与函数sigaction配合使用。the function prototype is as follows:
#include
#include
int sigqueue(pid_t pid , int signum ,const union sigval val);
Function return :access 0 , error -1
Sigqueue 的第一个参数pid是指定接收信号的进程ID,第二个参数signum确定即将发送的信号,第三个参数是一个联合数据结构union sigval ,指定了信号传递的参数,即通常所说的4个字节值,define is as follows:
Typedef union sigval{
int sigval_int ;
Void *sival_ptr;//指向要传处理哈hdeepin-music-playe递的信号参数
}sigval_t ;
Sigqueue()比kill()传递更多的附加信息,但是sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signum = 0 ,将执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权可向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信号会拷贝到3参数信号处理函数(3信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针,在稍后的例子中读者可以清楚地看到)的siginfo_t结构中,这样信号处理函数就是处理这些信息了。由于sigqueue系统调用支持发送带参数的信号,所以比kill()系统调用的功能要灵活和强大得多。
Eg9演示了进程给自己发送信号SIGUSR1,并且带上附加信息(一个字符串数据)。Code is as follows:
#include
#include
#include
void SigHandler(int signo, siginfo_t *info,void *context);
int main(void)
{
//define struct containing information processing
struct sigaction sigAct ;
//signal processing function shows designated by sa_sigaction
sigAct.sa_flags = SA_SIGINFO ;
//specify the signal processing function
sigAct.sa_sigaction = SigHandler;
//register signal
if(sigaction(SIGUSR1,&sigAct,NULL) == -1)
{
printf("sigaction failed! ");
return 1;
}
//define the third argument of sigqueue function
sigval_t val ;
//将传递的信息参数--一个字符串数据
char pMsg[] = "I like Linux c program!";
val.sival_ptr = pMsg ;
//调用sigqueue向自身发送SIGUSR1信号,并携带一个4个字节的联合数据结构
if(sigqueue(getpid(),SIGUSR1,val) == -1 )
{
printf("sigqueue failed ");
return 1 ;
}
sleep(3);
return 0 ;
}
void SigHandler(int signo,siginfo_t *info,void *context)
{
char *pMsg = (char *) info->si_value.sival_ptr ;
printf("Receive signal number :%d ",signo);
printf("Receive Message : %s ",pMsg);
}
Program output:
Receive signal number :10
Receive Message : I like Linux c program!
Program analysis
4:alarm function
Alarm function专门为 SIGALRM信号而设定,使系统在一定时间之后发送信号。函数原型如下:
#include
Unsigned int alarm (unsigned int seconds) ;
Return value: 如果调用alarm之前,进程中已经设置了闹钟时间,则返回一个闹钟时间的剩余时间,否则返回0.
参数seconds指定了下一次发送信号的时间,即在当期时间的seconds妙后,向进程本身发送SIGALARM 信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm调用都将无效。如果参数seconds为0 ,那么进程内将不再包含任何闹钟时间。
程序10演示了alarm函数的用法,将alarm的时间参数设为5妙钟,5秒钟之后将调用信号的处理函数。
EG10:
#include
#include
#include
void Handler()
{
printf("Hello , I like Linux c Programs! ");
}
int main(void)
{
int i = 0 ;
signal(SIGALRM,Handler);
alarm(6);
while(i++<7)
{
printf("Sleeping %d ",i);
sleep(1);
}
return 0 ;
}
Program output:
Sleeping 1
Sleeping 2
Sleeping 3
Sleeping 4
Sleeping 5
Sleeping 6
Hello , I like Linux c Programs!
Sleeping 7
Program Analysis:
Ignore
5:setitimer function
setitimer 函数同alarm函数一样,也可以用于使系统在某一时刻发出信号,但是他可以更加精确地控制程序。函数如下:
#include
int setitimer(int which, const struct itimerval *val ,struct itimerval *olvalue) ;
Return value: success 0 , fail -1
setitimer的第一个参数which指定定时器类型,setitimer比alarm功能强大,支持3种类型的定时器。参数value和oldvalue为指向时间参数的结构体指针,itimerval结构原型如下:
struct itimerval{
struct timeval it_interval ;//计时器重启动的间歇值
struct timeval it_value;//计数器安装后首先启动的初始值
}
成员it_interval和it_value又是timeval类型的结构体:
struct timeval {
long tv_sec ;//时间的秒数部分
long tv_usec; //时间的微妙(1/1000000)部分
};
setitimer()将value指向的结构体设置为计时器的当前值如果oldvalue不是null,将返回计时器原有值。
EG 11 :
#include
#include
#include
#include
#include
static void showDate()
{
struct timeval tp ;
struct tm *tm ;
// gettimeofday function can get the current system time ()
gettimeofday(&tp,NULL) ;
// localtime get local current time and day
tm = localtime(&tp.tv_sec) ;
// print second summary from Unix jiyuan to current
printf("Now The Seconds Is as follows:");
printf("sec = %ld ",tp.tv_sec);
// print setble
//printf("usec = %ld ",tp.tv_usec);
// print the current local time and day
// printf("Now The Time Is :%d-%d-%d %d:%d ",tm->tm_year+1990,tm->tm_mon+1,tm->tm_mday,tm->tm_hour,tm->tm_min,tm->tm_sec);
}
static void ElsfTimer( int signo )//signal process function
{
struct timeval tp ;
struct tm *tm ;
printf(" signo : %d ",signo);
// gettimeofday function can get the current system time ()
gettimeofday(&tp,NULL) ;
// localtime get local current time and day
tm = localtime(&tp.tv_sec) ;
// print second summary from Unix jiyuan to current
printf("sec = %ld ",tp.tv_sec);
// print setble
printf("usec = %ld ",tp.tv_usec);
// print the current local time and day
printf("Now The Time Is :%d-%d-%d %d:%d ",tm->tm_year+1990,tm->tm_mon+1,tm->tm_mday,tm->tm_hour,tm->tm_min,tm->tm_sec);
}
static void InitTime (int tv_sec ,int tv_usec)
{
struct itimerval value ; // define time argument struct value
// register SIGALRM and signal handler function ElsfTimer()
signal(SIGALRM,ElsfTimer);
value.it_value.tv_sec = tv_sec ;
value.it_value.tv_usec = tv_usec ;
value.it_interval.tv_sec = tv_sec -1 ;
value.it_interval.tv_usec =tv_usec ;
// setitimer sent signal , timer type is ITIMER_REAL
setitimer(ITIMER_REAL,&value,NULL) ;
}
int main( void )
{
// call InitTimer subfunction , real argument is set tv_sec 5, subtle is set 0
showDate();
InitTime(3,0) ;
while(1) ;
return 0 ;
}
Program Output :
Now The Seconds Is as follows:sec = 1362234384
signo : 14
sec = 1362234387 usec = 319855
Now The Time Is :2103-3-2 22:26
signo : 14
sec = 1362234389 usec = 319771
Now The Time Is :2103-3-2 22:26
signo : 14
sec = 1362234391 usec = 319768
Now The Time Is :2103-3-2 22:26
signo : 14
sec = 1362234393 usec = 319838
Now The Time Is :2103-3-2 22:26
Program Analysis:
注意:struct timeval it_interval(计时器重启动的间歇值)和struct timeval it_value(计数器安装后首先启动的初始值)的区别
信号的阻塞
在linux的信号控制中,有时候即不希望进程在接受到信号时立即中断进程的执行,也不希望该信号完全被忽略,而是延时一段时间再去调用相关的信号处理函数。这中操作就是通过阻塞信号的方法来实现的。信号阻塞的系统调用主要是sigprocmask()和sigsuspend()函数。
到底信号阻塞是在什么时候呢?
A(产生信号)_________未决(去查找屏蔽表是否被屏蔽)________>传递_______________>B(设置位)____________>做相应的动作。
信号集
我们需要有一个能表示多个信号——信号集( signal set)的数据类型。将在 s i g p r o c m a s k(下一节中说明)这样的函数中使用这种数据类型,以告诉内核不允许发生该信号集中的信号。
如前所述,信号种类数目可能超过一个整型量所包含的位数,所以一般而言,不能用整型量中的一位代表一种信号。 POSIX.1定义数据类型 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
四个函数返回:若成功则为 0,若出错则为- 1
int sigismember(const sigset_sett, *i n t signo) ;
返回:若真则为 1,若假则为 0
函数sigemptyset初始化由set指向的信号集,使排除其中所有信号。函数 sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用s i g e m p t y s e t或s i g f i l l s e t一次。这是因为 C编译程序将不赋初值的外部和静态度量都初始化为 0,而这是否与给定系统上信号集的实现相对应并不清楚。一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函数 sigaddset将一个信号添加到现存集中, sigdelset则从信号集中删除一个信号。对所有以信号集作为参数的函数,都向其传送信号集地址。
实例
如果实现的信号数目少于一个整型量所包含的位数,则可用一位代表一个信号的方法实现信号集。例如,大多数 4 . 3 + B S D实现中有 3 1种信号和 3 2位整型。 s i g e m p t y s e t和s i g f i l l s e t这两个函数可以在头文件中实现为宏:
#define sigemptyset(ptr)
#define sigfillset(ptr)
( *(ptr) = 0)
( *(ptr) = ̃(sigset_t)0, 0 )
注意,除了设置对应信号集中各信号的位外, sigfillset必须返回0,所以使用逗号算符,它将逗号算符后的值作为表达式的值返回。
使用这种实现, sigaddset打开一位, sigdelset则关闭一位,sigismember测试一指定位。因为没有信号编号值为 0,所以从信号编号中减 1以得到要处理的位的位编号数。程序 10-9可实现这些功能。
An implementation of sigaddset, sigdelset, and sigismember
#include
#include
#define SIGBAD(signo) ((signo) <= 0 || (signo) >= NSIG)
Int sigaddset(sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL; return(-1);
}
*set |= 1 << (signo - 1);
return(0);
}
Int sigdelset(sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL; return(-1);
}
*set &= ~(1 << (signo - 1));
return(0);
}
Int sigismember(const sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL; return(-1);
}
return((*set & (1 << (signo - 1))) != 0);
}
然后该进程再睡眠 5秒钟。如果在此期间再产生退出信号,那么它就会使该进程终止,因为在上次捕捉到该信号时,已将其处理方式设置为默认动作。此时如果键入终端退出字符 Ctrl-,则输出“Q U I T ( c o r e d u m p )”信息,表示进程因接到 S I G Q U I T而终止,但是在 c o r e文件中保存了与进程有关的信息(该信息是由shell发现其子进程异常终止时打印的 )。
源程序:
int main(void)
{
sigset_t newmask , oldmask ,pendingmask ;
if(signal(SIGQUIT,sig_quit) == SIG_ERR)
{
printf("can"t catch SIGUIT");
}
sigemptyset(&newmask);
sigaddset(&newmask,SIGQUIT);
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
{
printf("SIG_BLOCK error ");
}
sleep(5);
if(sigpending(&pendingmask)<0)
{
printf("sigpending error");
}
if( sigismember(&pendingmask,SIGQUIT))
{
printf(" SIGQUIT pending ");
}
if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
{
printf("SIG_SETmASK error");
}
printf("SIGQUIT unblocked ");
sleep(5);
exit(0);
}
static void sig_quit(int signo)
{
printf("caught SIGQUIT ");
return ;
}
程序输出:
注意,在第二次运行该程序时,在进程睡眠期间使 SIGQUIT信号产生了10次,但是解除了对该信号的阻塞后,只向进程传送一次 SIGQUIT。从中可以看出在此系统上没有将信号进行排队。
Sigprocmask函数用于检测或更改(或两者)进程的信号掩码(signalmask)。信号掩码是由被阻塞的发送给当前进程的信号组成的信号集。
函数sigaction中设置的被阻塞信号集合只是针对要处理的信号,例如:
Struct sigaction act ;
Sigemptyset(&act.sa_mask);
Sigaddset(&act.sa_mask,SIGQUIT);
Sigaction(SIGINT,&act,NULL);
表示只有在处理信号SIGINT时,才阻塞信号SIGQUIT
而函数sigprocmask是全程阻塞,在sigpromask中设置了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。Sigprocmask的函数原型如下:
#include
Int sigprocmask(int how ,const sigset_t *set,sigset_t *oldset);
Return value: success 0 , fail -1
参数set和oldset是sigset_t 类型的指针,用于表示所指向的信号集。Set指向一个信号集时,参数how表示sigprocmask函数将如何对set所指向信号集以及信号掩码进程操作,其取值及对应函数功能见下表。当set为NULL 时,how的取值无效。当oldset不为NULL 时,函数sigpromask将进程当前的信号掩码返回给oldset。
How取值 对应函数功能
SIG_BLOCKS 该进程新的信号屏蔽字是其当前信号屏蔽字和 s e t指向信号集的并集。 s e t包含了我们希望阻塞的附加信号
SIG_UNBLOCKS 该进程新的信号屏蔽字是其当前信号屏蔽字和 s e t所指向信号集的交集。 s e t包含了我们希望解除阻塞的信号
SIG_SETMASK 该进程新的信号屏蔽是 set指向的值
下面的程序主要屏蔽掉SIGINT(虽然注册了SIGINT 和SIGQUIT 两个信号), 而只能捕捉到SIGQUIT信号。
EG 12 :
sigsetjmp和siglongjmp函数
在讨论sigsetjmp 和 siglongjmp 函数前 我们先看setjmp 和 sigjmp 函数
#include
#include
#include
#include
#define TOK_ADD 5
#define MAXLINE 10
void do_line(char *);
void cmd_add(void);
int get_token(void);
int main( int argc ,char * argv[])
{
char line[MAXLINE];
while(fgets(line,MAXLINE,stdin)!=NULL)
{
do_line(line);
}
exit(0);
}
char *tok_ptr ;
void do_line(char *ptr)
{
int cmd ;
tok_ptr = ptr ;
while((cmd = get_token())>0)
{
switch(cmd){
case TOK_ADD :
cmd_add();
break;
}
}
}
void cmd_add(void)
{
int token;
token = get_token();
}
int get_token(void)
{
}
程序7-3在读命令、确定命令的类型、然后调用相应栈顶函数处理每一条命令这类程序中是非常典型的。图 7-4显
示了调用了cmd_add之后栈的大致使用情况。
自动变量的存储单元在每个函数的栈桢中。数组 line 在m a i n的栈帧中,整型 c m d在d o _ l i n e的栈帧中,整型token在cmd_add的栈帧中。
如上所述,这种形式的栈安排是非常典型的,但并不要求非如此不可。栈并不一定要向低地址方向扩充。某些系统对栈并没有提供特殊的硬件支持,此时一个 C实现可能要用连接表实现栈帧。
在编写如程序 7 - 3这样的程序中经常会遇到的一个问题是,如何处理非致命性的错误。例如,若 c m d _ a d d函数发现一个错误,譬如说一个无效的数,那么它可能先打印一个出错消息,然后希望忽略输入行的余下部分,返回 main函数并读下一输入行。用 C语言比较难做到这一点。(在本例中, cmd_add函数只比 main低两个层次,在有些程序中往往低五或更多层次。)如果我们不得不以检查返回值的方法逐层返回,那就会变得很麻烦。
解决这种问题的方法就是使用非局部跳转—— setjmp和longjmp函数。非局部表示这不是在一个函数内的普通的 C语言g o t o语句,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的一个函数中。
#include
int setjmp(jmp_buf env);
Returns: 0 if called directly, nonzero if returning from a call to longjmp
void longjmp(jmp_buf env, int val);
在希望返回到的位置调用 setjmp,在本例中,此位置在 main函数中。因为我们直接调用该函数,所以其返回值为 0。setjmp的参数env是一个特殊类型 jmp_buf。这一数据类型是某种形式的数组,其中存放在调用 longjmp时能用来恢复栈状态的所有信息。一般, env变量是个全局变量,因为需从另一个函数中引用它。当检查到一个错误时,例如在 cmd_add函数中,则以两个参数调用 longjmp函数。第一个就是在调用 setjmp时所用的env;第二个 val,是个非0值,它成为从setjmp处返回的值。使用第二个参数的原因是对于一个 s e t j m p可以有多个 l o n g j m p。例如,可以在 c m d _ a d d中以v a l为1调用longjmp,也可在get_token中以val为2调用longjmp。在main函数中,setjmp的返回值就会是1或2,通过测试返回值就可判断是从 cmd_add还是从get_token来的longjmp。再回到程序实例中,表 7 - 1中给出了经修改过后的 m a i n和c m d _ a d d函数 (其他两个函数,do_line和get_token未经更改)。
上面的程序不好理解:
看下面的:
#include
#include
jmp_buf jmpbuffer ;
int i = 0 ;
int bb()
{
longjmp(jmpbuffer,3);
}
void aa()
{
bb();
}
int main ()
{
printf("+++++ ");
printf("%d ",setjmp(jmpbuffer));
if(i++>9)
return 0;
aa();
printf("----- ");
return 0 ;
}
看输出:
macos@macos-R453-R403:~/program/Unix/10$ ./a.out
+++++
0 3 3 3 3 3 3 3 3 3 3 macos@macos-R453-R403:~/program/Unix/10$ clear
这次就容易理解了吧
下面我们就回归主题吧》》
在信号处理程序中经常调用l o n g j m p函数以返回到程序的主循环中,而不是从该处理程序返回。确实, ANSI C 标准说明一个信号处理程序可以返回或者调用 a b o r t、e x i t或l o n g j m p。调用l o n g j m p时有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断此信号处理程序。如果用longjmp跳出此信号处理程序,则对此进程的信号屏蔽字会发生什么呢 ?为了允许两种形式并存, POSIX.1并没有说明 setjmp和longjmp对信号屏蔽字的作用,而是定义了两个新函数 sigsetjmp和siglongjmp。在信号处理程序中作非局部转移时应当使用这两个函数。
#include
int sigsetjmp(sigjmp_buf env, int savemask);
Returns: 0 if called directly, nonzero if returning from a call to siglongjmp
返回:若直接调用则为 0,若从s i g l o n g j m p调用返回则为非 0
这两个函数和 setjmp,longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果 savemask非0,则s i g s e t j m p在e n v中保存进程的当前信号屏蔽字。调用 s i g l o n g j m p时,如果带非 0 s a v e m a s k的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
实例
程序显示了在信号处理程序被调用时,系统所设置的信号屏蔽字如何自动地包括刚被捕捉到的信号。它也例示了如何使用 sigsetjmp和siglongjmp函数。
EG12
文件一:
#include
#include
#include
#include
void pr_mask(const char *str)
{
sigset_t sigset ;
int errno_save ;
errno_save = errno ;
if(sigprocmask(0,NULL,&sigset)<0)
printf("sigpromask error ");
printf("%s",str);
if(sigismember(&sigset,SIGINT)) printf("SIGINT ");
if(sigismember(&sigset,SIGQUIT)) printf("SIGQUIT ");
if(sigismember(&sigset,SIGUSR1)) printf("SIGUSR1 ");
if(sigismember(&sigset,SIGALRM)) printf("SIGALRM ");
printf(" ");
errno = errno_save ;
}
文件二:
#include
#include
#include
#include"pr_mask.c"
#include
#include
static void sig_usrl(int),sig_alrm(int);
static sigjmp_buf jmpbuf ;
static volatile sig_atomic_t canjump ;
int main(int argc , char * agrv [] )
{
if( signal(SIGUSR1,sig_usrl) == SIG_ERR )
printf("signal(SIGUSR1) error ");
if( signal(SIGALRM ,sig_alrm) == SIG_ERR )
printf("signal(SIGALRM) error");
pr_mask("staring main: ");
if(sigsetjmp(jmpbuf,1)){
pr_mask("ending main:");
return 0 ;
}
canjump =1 ;
for(;;)
pause();
}
static void sig_usrl (int signo )
{ time_t starttime ;
if(canjump ==0 )
return 0 ;
pr_mask("starting sig_usrl: ");
alarm(3);
starttime = time(NULL);
for(;;)
if(time(NULL)>starttime+5)
break;
pr_mask("finishing sig_usrl : ");
canjump = 0 ;
siglongjmp(jmpbuf,1);
}
static void sig_alrm(int signo)
{
pr_mask("in sig_alrm ");
}
此程序例示了另一种技术,只要在信号处理程序中调用 siglongjmp就应使用这种技术。在调用s i g s e t j m p之后将变量c a n j u m p设置为非 0。在信号处理程序中检测此变量,仅当它为非 0值时才调用siglongjmp。这提供了一种保护机制,使得若在 jmpbuf(跳转缓存)尚未由sigsetjmp初始化时调用信号处理程序,则不执行其处理动作就返回(在本程序中, siglongjmp之后程序很快就结束,但是在较大的程序中,在 siglongjmp之后,信号处理程序可能仍旧被设置)。在一般的C代码中(不是信号处理程序),对于longjmp并不需要这种保护措施。但是,因为信号可能在任何时候发生,所以在信号处理程序中,需要这种保护措施。在程序中使用了数据类型 sig_atomic_t,它是由ANSI C定义的在写时不会被中断的变量类型。它意味着这种变量在具有虚存的系统上不会跨越页的边界,可以用一条机器指令对其进行存取。对于这种类型的变量总是包括 A N S I类型修饰符 v o l a t i l e,其原因是:该变量将由两个不同的控制线——main函数和异步执行的信号处理程序存取。
图1 0 - 1显示了此程序的执行时间顺序。可将图 1 0 - 1分成三部分:左面部分 (对应于 m a i n ),中间部分 ( s i g _ u s r 1 )和右面部分( s i g _ a l r m )。在进程执行左面部分时,信号屏蔽字是 0 (没有信号是阻塞的 )。而执行中间部分时,其信号屏蔽字是 S I G U S R 1。执行右面部分时,信号屏蔽字是SIGUSR1|SIGALRM。
执行程序10-14得到下面的输出:
$ a.out &
在后台启动进程
starting main:
[1]
531
$ kill -USR1 531 作业控制 shell打印其进程 I D
starting sig_usr1: SIGUSR1 向该进程发送 S I G U S R 1
$ in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
[1] + Done a.out & 键入回车
该输出与我们所期望的相同:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字。当从信号处理程序返回时,原来的屏蔽字被恢复。另外, s i g l o n g j m p恢复了由sigsetjmp所保存的信号屏蔽字。
sigsuspend函数
用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止.也就是说,sigsuspend后,进程就挂在那里,等待着开放的信号的唤醒。系统在接收到信号后,马上就把现在的信号集还原为原来的,然后调用处理函数。
上面已经说明,更改进程的信号屏蔽字可以阻塞或解除阻塞所选择的信号。使用这种技术可以保护不希望由信号中断的代码临界区。
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
pause();
如果希望对一个信号解除阻塞,然后 pause以等待以前被阻塞的信号发生,则又将如何呢? 假定信号是SIGINT,实现这一点的一种不正确的方法是:如果在解除对 SIGINT的阻塞和pause之间发生了SIGINT信号,则此信号被丢失。这是早期的不可靠信号机制的另一个问题。为了纠正此问题,需要在一个原子操作中实现恢复信号屏蔽字,然后使进程睡眠,这种功能是由sigsuspend函数所提供的。
#include
int sigsuspend(const sigset_t *sigmask);
Returns: 1 with errno set to EINTR
进程的信号屏蔽字设置为由 s i g m a s k指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程也被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用 sigsuspend之前的值。注意,此函数没有成功返回值。如果它返回到调用者,则总是返回- 1,并且errno设置为EINTR(表示一个被中断的系统调用)。
实例程序EG13显示了保护临界区,使其不被指定的信号中断的正确方法。
EG13
文件一:
#include
#include
#include
#include
void pr_mask(const char *str)
{
sigset_t sigset ;
int errno_save ;
errno_save = errno ;
if(sigprocmask(0,NULL,&sigset)<0)
printf("sigpromask error ");
printf("%s",str);
if(sigismember(&sigset,SIGINT)) printf("SIGINT ");
if(sigismember(&sigset,SIGQUIT)) printf("SIGQUIT ");
if(sigismember(&sigset,SIGUSR1)) printf("SIGUSR1 ");
if(sigismember(&sigset,SIGALRM)) printf("SIGALRM ");
if(sigismember(&sigset,SIGTERM)) printf("SIGTERM ");
printf(" ");
errno = errno_save ;
}
文件二
#include
#include
#include"pr_mask.c"
static void sig_int (int);
int main( int argc ,char *argv[] )
{
sigset_t newmask , oldmask , zeromask ;
//pr_mask("program start : ");
// re4gister signal processing function
if(signal(SIGINT , sig_int ) == SIG_ERR )
printf("signal (SIGINT) error");
sigemptyset(&zeromask) ;
sigemptyset(&newmask);
sigaddset(&newmask,SIGINT) ;
if(sigprocmask(SIG_BLOCK,&newmask , &oldmask)< 0 )
printf("SIG_BLOCK error ");
pr_mask("in critical region: ");
if(sigsuspend(&zeromask) != -1 )
printf("sigsuspend error ");
pr_mask("after return from sigsuspend : ");
if( sigprocmask(SIG_SETMASK,&oldmask,NULL) < 0 )
printf("SIG_SETMASK error ");
return 0 ;
}
static void sig_int(int signo ){
pr_mask (" in sig_int: ");
return ;
}
程序输出:
in critical region: SIGINT
^C
in sig_int: SIGINT
after return from sigsuspend : SIGINT
程序分析:
从中可见,在sigsuspend返回时,它将信号屏蔽字恢复为调用它之前的值。
实例
sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。程序 10-16用于捕捉中断信号和退出信号,但是希望只有在捕捉到退出信号时再继续执行 main程序。此程序的样本输出是:
macos@macos-R453-R403:~/program/Unix/10$ ./sigsuspend2
^C
^Cinterrupt
^Cinterrupt
^Cinterrupt
^interrupt
SIG_QUIT
EG14:
#include
#include
volatile sig_atomic_t quitflag ;//set nonzero by signal handler
int main(void)
{
void sig_int(int);
sigset_t newmask , oldmask ,zeromask ;
if(signal(SIGINT,sig_int) == SIG_ERR)
{
printf("signal (SIGINT)error") ;
}
if(signal(SIGQUIT,sig_int) == SIG_ERR)
{
printf("signal(SIGQUIT) error");
}
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask,SIGQUIT);
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
printf("SIG_BLOCK error");
while(quitflag == 0)
sigsuspend(&zeromask);
quitflag = 0 ;
if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
printf("SIG_SETMASK error") ;
return 0 ;
}
void sig_int(int signo)
{
if(signo==SIGINT)
printf(" interrupt");
else if(signo == SIGQUIT)
{
quitflag =1 ; // set flag for main loop
printf(" SIG_QUIT ") ;
}
return ;
}
Abort 函数
此函数将SIGABRT信号发送给调用进程。进程不应忽略此信号。
ANSI C要求若捕捉到此信号而且相应信号处理程序返回, a b o r t仍不会返回到其调用者。
如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用 e x i t、_ e x i t、l o n g j m p或siglongjmp。(10.15节讨论了longjmp和siglongjmp之间的区别。)POSIX.1也说明abort覆盖了进程对此信号的阻塞和忽略。
让进程捕捉 S I G A B RT的意图是:在进程终止之前由其执行所需的清除操作。如果进程并不在信号处理程序中终止自己, POSIX.1说明当信号处理程序返回时,abort终止该进程。ANSI C对此函数的规格说明将这一问题留由实现决定,而不管输出流是否刷新以及不管临时文件(见 5 . 1 3节)是否删除。P O S I X . 1的要求则进了一步,它要求如果 a b o r t调用终止进程,则它应该有对所有打开的标准 I/O流调用fclose的效果。但是如果abort调用并不终止进程,则它对打开流也不应有影响。正如我们将在后面所看到的,这种要求很难实现。
计时器与信号
睡眠函数
Unsigned int sleep (unsigned int seconds) ;
Void usleep (unsigned long usec) ;
事实上,sleep睡眠函数的内部是用信号机制进行处理的,用到的函数有:
Unsigned int alarm(unsigned int seconds)
Int pause (void);
Alarm函数告知自身进程,是进程在seconds秒后自动产生一个SIGALRM信号,而pause函数用于将自身进行挂起,直到有信号发送时才从pause返回。
Eg15:
#include
#include
#include
void SignHandler(int iSignNo)
{
printf("Signal:%d ",iSignNo) ;
}
int main(void)
{
signal(SIGALRM,SignHandler);
alarm(3);
printf("Before pause(). ");
pause();
printf("After pause(). ");
return 0 ;
}
时钟处理
Linux 系统为每个进程维护3个计时器,分别是真实计时器,虚拟计时器和实用计时器。
真实计时器计算的是程序运行的实际时间
虚拟计时器计算的是程序运行在用户太时所消耗的时间(可以为是实际实际时间剪掉系统调用和程序睡眠所消耗的时间)
实用计时器的是程序处于用户态和处于内核所消耗的时间之和。
用指定的初始间隔和重复间隔时间为进程设置好一个计时器后,该计时器就会定时地向进程发送时钟信号。3和计时器发送的时钟信号分别为:SIGALRM , SIGVTALRM和SIGPROF。
用到的函数有getitimer和setitimer。
Int getitimer(int which ,struct itimerval *value);
Int setitimer(int which ,const struct itimerval *value,struct itimer val *ovalue);
Return : success 0 ,fail -1
Getitimer 用于获取计时器的设置。参数which 用于指定计时器类型,可选项为ITIMER_REAL(真实计时器),
ITIMER_VITUAL(虚拟计时器),ITIMER_PROF(实用计时器)。Value为一结构体的传出参数,用于传出该计时器的初始间隔时间和重复间隔时间。
Setitimer用于设置计时器。
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。