一、信号的概念 说道信号大多数人可能会想到在公路上的红绿灯,在我们linux也有着信号的概念。那么linux下的信号到底是怎么回事呢?我们还是今天就看看linux下的信号。 信号其实是一种软件中断,它为程序提供了处理异步事件的方法。异步事件就是事件可能会在任何时间内发生,很多重要的而程序都有对信号的处理,在linux下我们是通过命令 kill -l来查看系统中所有的信号列表和它们的信号编号。 我们可以看到31~34之间是没有信号的,我们把1~31号信号称为普通信号,把34~64号信号称为实时信号。所有的信号都包含在signal.h中,且都定义成正整数常量,也就是它们的信号编号。 二、信号的产生形式 linux下的信号一般有四种 产生方式: 1、来自键盘的信号 用户在终端按下某些按键时,终端驱动程序会发送信号给前台的进程(使前台进程终止的信号)。ctrl+c发送SIGINT信号、ctrl+\发送SIGQUIT信号、ctrl+z发送SIGTSTP信号。SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,SIGTSTP信号使前台进程停止。 2、硬件异常产生信号 除数为0、无效的内存引用对应的SIGSEGV信号,硬件异常产生信号不同,一旦硬件异常产生,那么它会一直存在,直到程序被终止位置,所以处理硬件异常信号一般都采用终止程序的方法。 3、通过系统调用向进程发送信号 进程可以通过在shell下运行kill指令来对某个进程发送信号,kill指令是kill系统调用的一个接口。
int kill(pid_t id,int signo); //向进程id发送signo信号 int raise(int signo); //自己给自己发送signo信号返回值:成功返回的是0,失败返回的是-1。 4、由软件条件产生信号
#include <unisted.h> unsigned int alarm(unsigned int seconds); //告诉操作系统seconds时间后给自己发送一个SIGALRM信号返回值:返回0或者以前设定的闹钟还余下的秒数 三、信号的处理方式 产生了信号,当然也要对信号进行处理。下面来看一下linux中信号的处理方式: 1、信号忽略 忽略此信号,大多数信号都可以采用这种方式进行处理,除了9( SIGKILL )和19(SIGSTOP )信号。因为这两种信号都直接向内核提供了进程终止和停止的可靠办法。SIGKILL还有硬件异常信号我们最好不要忽略,因为硬件异常一旦产生如果不进行处理就会一直存在。 2、信号捕捉 进程要通知内核在某种信号产生时,需要调用一个用户函数,在用户函数中,用户可以自己定义信号处理的方式,在Linux下我们不能捕捉SIGKILL信号和SIGSTOP信号。 用户自定义动作 3、执行默认动作 一般情况下是终止该进程
四、信号中的处理函数 1、signal函数(指定信号的处理方式)
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);signum: 参数为信号名或者信号的编号。 handler: 为指向返回值为void,参数为int的函数指针指针,或者是SIG_IGN或SIG_DFL的宏定义。 在signal.h的头文件中,上面的宏定义被定义为以下:
#define SIG_ERR (void(*)()) -1 #define SIG_DFL (void(*)()) 0 #define SIG_IGN (void(*)()) 1SIG_DFL指定信号处理的方式为默认方式,SIG_IGN指定信号处理的方式为忽略。 2、alarm函数(闹钟函数)
unsigned int alarm(unsigned int seconds);
alarm函数相当于一个闹钟,它可以为进程注册闹钟时间( 例如使用alarm(5)可以为进程注册5秒钟的闹钟时间,5秒后会产生SIGALRM信号 )。如果在调用alarm函数时,之前已经为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值所取代; 使用alarm(0)可以取消以前所注册的闹钟,并返回之前注册的闹钟的剩余时间。 3、pause函数
int pause(void);
pause函数是检测进程有没有从信号处理函数中返回,只有当执行了一个信号处理程序并从其返回时,pause才返回,否则pause将一直挂起调用进程。 当进程执行了信号处理程序时,pause返回-1,并将errno设置为EINTR。
五、Core Dump Core Dump:称为核心转储,即当一个进程异常退出之前,将进程的用户空间内存数据全部以文件方式保存到磁盘上,文件名则称作是core,方便gdb的调试。 Post-mortem Debug(事后调试):进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因。 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中),如下: 默认下系统分配core文件大小为0,是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全,并且若一个较大程序每次运行都要产生一个core文件,很可能将硬盘空间占用完。 我们可以用一下命令进行更改: ulimit -c 1024 //设置core文件大小上限为1024字节 现在我们写一个代码(除0出错的代码),看一下core文件 代码如下:有一个code dumped 我们可以看到有一个core类型的文件,文件还是挺大的。 我们可以使用core文件在GDB中调试,来找错误。 直接在gdb中打开那个core文件,core-file core.2306 就会直接告诉你出错的地方。 六、信号阻塞和信号的处理机制 我们先看一下信号的状态: 信号的状态: 信号递达(Delivery): 我们将实际执行信号的处理动作称为信号递达。 信号未决(Pending): 我们将信号从产生到递达之间的状态称为信号未决。 信号阻塞(Block): 进程可以选择阻塞某个信号,也可以理解为屏蔽某个信号。 注意: 阻塞和忽略是不同的,阻塞是进程没有收到该信号,而忽略是进程收到信号后的一种处理方式。 信号未决和信号阻塞的状态都被组织在两张位图中的,即位置表示信号编号,如果在pending表中该位置为1则表示被pending为0表示尚未被pending,如果在block表中则表示是否被block。 每个进程中操作系统都会为其分配一整套的block表,pending表以及handler表 进程将收到的信号存放在PCB中,PCB中有三个与上面三个状态相对应的位图表。(用于表示信号的状态,状态只有是与否两个概念,我们中0,1来表示方便且节省空间,所以用位图。) 每个信号都在阻塞表(block)和未决表(pending)中有一个0或1的状态。还有一个handler表,类似于一个函数指针数组,每个指针都指向指定信号的处理方式。 linux下的信号处理机制 如图: 内核是如何产生信号的?内核又是在什么时候产生信号的呢? 信号发送给进程之后,进程将收到的信号存放在pcb的某些字段中,信号是在由内核态切换到用户态的时候来处理信号的。 内核可因为执行当前主控流程的某条指令是因为中断,异常或者系统调用进入内核态。 内核处理完异常准备返回用户态之前,会检查当前进程的PCB中是否有递达的信号。若有,根据处理方式又可分为三种情况 : 1、忽略,将pending表中的信号位置由1置0,返回用户态 2、执行默认动作,默认动作一般为终止程序。此时不返回,执行终止进程流程。 3、执行自定义动作(信号捕捉)。 信号的处理动作是用户自定义的动作,内核会先切换到用户态执行信号处理函数,信号处理函数返回时执行系统调用sigreturn再次返回内核态。然后再从内核态返回用户态从上次异常或中断的地方继续执行。此种方式最为繁琐,共有四次内核用户之间的切换。
七、信号集 我们知道信号在pending表中只是一个bit为标志(0或1)我我们以相同的31个信号的Block和Pending标志,而这就是信号集(sigset_t) 信号集的操作函数:
#include <signal.h> int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使set包含所有信号 int sigfillset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对 表示该信号集的有效信号包括系统支持的所有信号 int sigaddset(sigset_t *set, int signum); //将一个信号添加到已经存在的信号集中 int sigdelset(sigset_t *set, int signum); //从已有信号集中删除一个信号 int sigismember(const sigset_t *set, int signum); //测试信号集是否包含signum信号1、函数sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
该函数用来规定当前阻塞而不能递达给进程的信号集。调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。 参数: how参数指定了这个函数工作的方式,有三种情况: 1、SIG_BLOCK: 当前的信号屏蔽字与参数set指针指向的信号屏蔽字组成并集,构成新的信号屏蔽字。set包含了希望阻塞的新信号。 2、SIG_UNBLOCK: 当前的信号屏蔽字与参数set指针指向的信号屏蔽字补集的交集,构成新的信号屏蔽字。set包含了希望希望解除阻塞的信号。 3、SIG_SETMASK: 将当前进程的信号屏蔽字设置成set所指向的值 set指针:指向一个合适的信号屏蔽字 oldset指针:当我们修改了当前的信号屏蔽字之后,需要保存之前的信号屏蔽字,以便回复之前的工作状态。 2、函数sigpending
int sigpending(sigset_t *set);
返回当前信号的pengding信号集。通过参数指针set返回。 3、函数sigaction
nt sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数的功能是检查或修改指定信号的处理动作。 参数: signum为指定信号,act为一个结构体指针,注意这个结构体的名字与本函数的名字相同,但他们是两个概念,不要混淆。 oldact。 若act非空,表示要修改指定信号的处理动作;当oldact指针为空的时候,由oldact返回上一个信号的动作 4、函数pause pause函数功能是使当前进程挂起,直到有信号递达。 如果信号的处理动作是默认终止,则程序终止,pause没有机会返回。 如果是忽略,则进程继续处于挂起状态。 如果是执行用户自定信号处理函数,则pause返回-1。error设置为EINTR,所以pause只有出错返回。EINTR表示信号被中断。
八、代码展示
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/types.h> void myhander(int sig) { printf("pid## %d receiving sig## %d\n",getpid(),sig); } void PrintPending(sigset_t set) { int i=1; for(;i<32;++i) { if(sigismember(&set,i)) { printf("1 "); } else { printf("0 "); } } printf("\n"); } int main() { sigset_t s; sigemptyset(&s); sigaddset(&s,2); sigset_t oldset; sigprocmask(SIG_SETMASK,&s,&oldset); signal(2,myhander); int count=0; while(1) { sigset_t s1; sigprocmask(0,NULL,&s1); printf("block list:"); PrintPending(s1); sigset_t p; sigpending(&p); printf("pend list:"); PrintPending(p); if(count==10) { sigprocmask(SIG_SETMASK,&oldset,&s); } sleep(1); ++count; } return 0; }(1)在前10秒count<=10时,2号信号被阻塞,此时block表与pending表依次为: 在前10秒因为2号信号被阻塞,若这时键盘发送Ctrl-C,2号信号不会递达(不执行自定义捕捉函数),其会先保持在未决状态,此时block表与pending表依次为: pending表的2号位置为1 我们继续发送,效果还是一样的 2号还是1 10s发送之后,阻塞取消,信号发送成功。此时我们再发2号信号结果还是0。