信号
背景知识:
信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分。
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信 号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号:
①与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
②与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
③与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
④与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
⑤在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。
⑥与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。
⑦跟踪进程执行的信号。
相关函数和头文件:
①signal.h
系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:
void (*signal(int signum, void (*handler)(int)))(int);
在使用该调用的进程中加入以下头文件:
#include <signal.h>
②sigaction函数
int sigaction(int signo,const struct sigaction *restrict act,struct sigaction *restrict oact);
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性.第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。
结构sigaction定义如下:
struct sigaction{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flag;
void (*sa_sigaction)(int,siginfo_t *,void *);
};
sa_handler字段包含一个信号捕捉函数的地址
sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加进进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。
sa_flag是一个选项,主要理解两个:
SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_RESTART 由此信号中断的系统调用会自动重启
SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
最后一个参数是一个替代的信号处理程序,当设置SA_SIGINFO时才会用他。
③sigemptyset
函数初始化信号集合set,将set 设置为空。
④信号所传递的每一个整数都被赋予了特殊的意义,并有一个信号名对应该整数。常见的信号有SIGINT, SIGQUIT, SIGCONT, SIGTSTP, SIGALRM等。这些都是信号的名字。你可以通过$man 7 signal来查阅更多的信号。上面几个信号中,
SIGINT 当键盘按下CTRL+C从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是中断 (INTERRUPT) 该进程。
SIGQUIT 当键盘按下CTRL+\从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是退出 (QUIT) 该进程。
SIGTSTP 当键盘按下CTRL+Z从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是暂停 (STOP) 该进程。
SIG_DFL:默认信号处理程序
SIG_IGN:忽略信号的处理程序
SIGCONT 用于通知暂停的进程继续。
SIGALRM 起到定时器的作用,通常是程序在一定的时间之后才生成该信号。
⑤kill()
原型:int kill(pid_t pid,int signo)
作用:该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程;sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
⑥pause()
阻塞进程,直到有信号产生
1、接收SIGINT信号,始终执行自定义处理
任务描述:
- 打印字符串,忽略ctrl+c的中断信号
main.c:
#include<signal.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> struct sigaction act; void doit(int sig) { printf("ha ha you can't close me\n"); } int main(void) { act.sa_handler=doit; sigemptyset(&act.sa_mask); act.sa_flags=0; if(sigaction(SIGINT,&act,NULL)!=0){ perror("sigaction error\n"); exit(1); } while(1){ printf("hello,world\n"); sleep(1); } return 0; }
2、接收SIGINT信号,执行一次自定义处理后,恢复默认行为
任务描述:
- 打印字符串,第一次按ctrl+c没有用,第二次按生效
main.c:
#include<signal.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> struct sigaction act; void doit(int sig) { printf("ha ha you can't close me\n"); act.sa_handler=SIG_DFL; sigaction(SIGINT,&act,NULL); } int main(void) { struct sigaction act; act.sa_handler=doit; sigemptyset(&act.sa_mask); act.sa_flags=0; if(sigaction(SIGINT,&act,NULL)!=0){ perror("sigaction error\n"); exit(1); } while(1){ printf("hello,world\n"); sleep(1); } return 0; }
3、模拟时钟
任务描述:
- 在5秒内,如果没有接受到其他信号,则提示时间到了
main.c:
#include<signal.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> struct sigaction act; void doit(int sig) { printf("It's time to get up\n"); } int main(void) { act.sa_handler=doit; sigemptyset(&act.sa_mask); act.sa_flags=0; if(sigaction(SIGALRM,&act,NULL)!=0){ perror("sigaction error\n"); exit(1); } pid_t pid; pid=fork(); if(pid<0){ printf("error\n"); return -1; } if(pid==0){ sleep(5); kill(getppid(),SIGALRM); } else{ printf("get a signal\n"); pause(); } return 0; }
4、带有超时功能的read
任务描述:
- 在5秒内输入字符串,否则提示超时
相关知识:
①setjmp.h是C标准函数库中提供“非本地跳转”的头文件:控制流偏离了通常的子程序调用与返回序列。互补的两个函数setjmp与longjmp提供了这种功能。
setjmp/longjmp的典型用途是异常处理机制的实现:利用longjmp恢复程序或线程的状态,甚至可以跳过栈中多层的函数调用。
在信号处理机制中,进程在检查收到的信号,会从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是通过使用setjmp和longjmp来实现的。setjmp将保存的上下文载入用户空间,并继续在旧的上下文中继续执行。这就是说,进程执行一个系统调用,当因为资源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将现在的上下文切换成原先通过setjmp调用保存在进程用户区的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。另一种用在多线程程序中同步访问的方法是使用互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。
②int setjmp(jmp_buf env)
建立本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处。这个子程序[保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值。
void longjmp(jmp_buf env, int value) 恢复env所指的缓冲区中的程序调用环境上下文,env所指缓冲区的内容是由setjmp子程序[1]调用所保存。value的值从longjmp传递给setjmp。longjmp完成后,程序从对应的setjmp调用处继续执行,如同setjmp调用刚刚完成。如果value传递给longjmp零值,setjmp的返回值为1;否则,setjmp的返回值为value。
setjmp保存当前的环境(即程序的状态)到平台相关的一个数据结构 (jmp_buf),该数据结构在随后程序执行的某一点可被 longjmp用于恢复程序的状态到setjmp调用所保存到jmp_buf时的原样。这一过程可以认为是"跳转"回setjmp所保存的程序执行状态。setjmp的返回值指出控制是正常到达该点还是通过调用longjmp恢复到该点。因此有编程的惯用法: if( setjmp(x) ){/* handle longjmp(x) */}。
jmp_buf 数组类型,例如struct int[16]或struct __jmp_buf_tag[3],用于保存恢复调用环境所需的信息。
③alarm也称为闹钟函数,它可以再进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。当在调用alarm()前已经设置了一个闹钟,那么我们可以调用alarm(0)来取消此闹钟,并返回剩余时间。
函数原型
unsigned int alarm(unsigned int seconds)
函数参数
seconds:指定秒数
函数返回值
成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
出错:-1
main.c:
#include<stdio.h> // perror #include<unistd.h> // read(), write(), alarm() #include<signal.h> // signal() #include<stdlib.h> // exit() #include<setjmp.h> // setjmp(), lonjmp() #define MAXLINE 80 struct sigaction act; jmp_buf myjmp; void sig_alrm(int sig) { longjmp(myjmp,1); exit(0); } int main(void) { int n; char a[MAXLINE]; act.sa_handler=sig_alrm; sigemptyset(&act.sa_mask); act.sa_flags=0; if(sigaction(SIGALRM,&act,NULL)!=0){ perror("sigaction error\n"); exit(1); } //setjmp一开始默认是0 if (setjmp(myjmp) != 0){ printf("read timeout\n"); exit(0); } alarm(5);//指定时间到时发送SIGSLARM信号,若5秒内执行到了alarm(0)则取消发送alarm信号,否则进入子函数的longjmp返回1到setjmp,退出整个程序 printf("please input something in 5 seconds\n"); n=read(0,a,sizeof(a)); alarm(0); printf("your input string is :\n"); write(1,a,n); exit(0); }
5、屏蔽SIGINT信号,当来SIGINT信号后,恢复所有信号的正常处理
任务描述:
- sigsuspend屏蔽SIGINT信号,并挂起进程,然后SIGQUIT信号来之后开始正常处理
相关知识:
sigsuspend()
函数原型
int sigsuspend(const sigset_t *mask);
作用
用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。也就是说,进程执行到sigsuspend时,sigsuspend并不会立刻返回,进程处于TASK_INTERRUPTIBLE状态并立刻放弃CPU,等待UNBLOCK(mask之外的)信号的唤醒。进程在接收到UNBLOCK(mask之外)信号后,调用处理函数,然后把现在的信号集还原为原来的,sigsuspend返回,进程恢复执行。
返回值
sigsuspend返回后将恢复调用之前的的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR.
main.c:
#include<signal.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> struct sigaction act,act2; void sig_int(int sig) { act2.sa_handler=SIG_DFL; sigaction(SIGINT,&act2,NULL); } void sig_quit(int sig) { act.sa_handler=SIG_DFL; sigaction(SIGQUIT,&act,NULL); } int main(void) { sigset_t newmask; sigemptyset(&newmask); act.sa_handler=sig_quit; sigemptyset(&act.sa_mask); act.sa_flags=0; act2.sa_handler=sig_int; sigemptyset(&act2.sa_mask); act2.sa_flags=0; if(sigaction(SIGINT,&act2,NULL)!=0){ perror("sigaction sigint error\n"); exit(0); } if(sigaction(SIGQUIT,&act,NULL)!=0){ perror("sigaction sigquit error\n"); exit(0); } sigaddset(&newmask, SIGINT);//SIGINT信号加入到new信号集中 printf("get signal\n"); sigsuspend(&newmask); printf("\n"); while(1){ printf("hello\n"); sleep(1); } return 0; }
6、将程序阻塞期间产生的信号推迟到阻塞之后处理
任务描述:
- slee 5s期间产生SIGQUIT不立即处理,5s后再处理,处理要先自定义打印消息,然后恢复默认设置
- 然后将屏蔽信号恢复,sleep 10s,这期间产生的SIGQUIT信号,将直接执行
相关知识:
①sigemptyset
函数初始化信号集合set,将set 设置为空。
②信号屏蔽字是指一个进程中当前阻塞而不能够递送给该进程的信号集。信号集则是一个能表示多个信号的集合的一种数据类型,为sigset_t。
与信号集设置相关的函数有如下几个:
* 下列四个函数成功返回0,出错返回-1
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
* 下面函数若真返回1,若假返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);
③int sigaddset(sigset_t *set,int signum);
sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里。
④sigprocmask
功能:
检测或更改信号屏蔽字
头文件:
#include <signal.h>
函数原型:
int sigprocmask(int how,const sigsett_t *set,sigset_t *oldset);
参数:
how 操作方式
set 信号集
oldest
返回值:
若成功返回0,若出错返回-1。
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。sigprocmask是最为关键的一个函数。在使用之前要先设置好信号集合set。这个函数的作用是将指定的信号集合set加入到进程的信号阻塞集合之中去,如果提供了oldset那么当前的进程信号阻塞集合将会保存在oldset里面。参数how决定函数的操作方式。
SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。
SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。
SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。
main.c:
#include<signal.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> struct sigaction act; void doit(int sig) { printf("caught sigquit\n"); act.sa_handler=SIG_DFL; sigemptyset(&act.sa_mask); act.sa_flags=0; //接收SIGQUIT信号并执行doit函数 if(sigaction(SIGQUIT,&act,0)<0){ perror("recover sigquiut error\n"); exit(1); } } int main(void) { sigset_t newmask,oldmask,pendmask; act.sa_handler=doit; sigemptyset(&act.sa_mask); act.sa_flags=0; //接受到SIGQUIT信号则执行doit函数 if(sigaction(SIGQUIT,&act,NULL)!=0){ perror("sigaction error\n"); exit(0); } sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT);//SIGINT信号加入到newmask信号集中 //将newmask信号集中的信号设为阻塞信号 if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0){ perror("sigprocmask error\n"); exit(1); } printf("block SIGQUIT 5s\n") ; sleep(5); //查看阻塞的信号 if(sigpending(&pendmask)<0){ perror("sigpending error\n"); exit(1); } //查看阻塞的信号中有没有SIGQUIT if(sigismember(&pendmask,SIGQUIT)) printf("SIGQUIT pending\n"); printf("sigquit unblocked\n"); //将阻塞的信号释放 if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0){ perror("set oldmask error\n"); exit(1); } printf("program will sleep 10s,during these SIGQUIT won't be blocked\n"); sleep(10); printf("done\n"); return 0; }