信号(1)
信号的基本概念
-
信号是事件发生时对进程的通知机制。或者说是软中断(硬中断的软件模拟),收到信号的时间是无法准确知道。
-
信号分为两个类型,通过
kill -l
可以查看系统中所有信号(前面数字是信号的编号也是后面宏的值)1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
其中1~31号信号是标准信号,剩下的称为实时信号
实时信号主要作用是弥补标准信号的使用限制的!对比如下:
-
实时信号的信号范围有所扩大,可应用于应用程序自定义的目的。而标准信号给用户自定义使用的信号只有两个,SIGUSR1和SIGUSR2
-
对实时信号所采用的是队列化管理,比如将某一个实时信号多次发送给一个进程,那么会多次传递信号。反之,如果给一个进程多次发送标准信号(假如此时进程正在处理该信号,后续又来了相同的信号),信号最多会出现2次!
-
发送实时信号时,可以为信号指定伴随数据(整数或者指针)
union sigval { int sival_int; void *sival_ptr; };
-
不同的实时信号的传递顺序可以得到保障。顺序如下:
- 如果是不同的信号同时处于等待状态,那么会率先传递最小编号的信号
- 如果是相同的信号同时处于等待状态,那么传递顺序按照信号发送来的顺序保持一致
-
对于开发人员使用实时信号时需要注意的是:不同的系统的实时信号的编号可能不同,所以代码中填写实时信号的编号,不是一个好的做法,最好的做法直接写宏名
-
-
信号因某些事件而产生,信号产生后,会于稍后被传递给某一个进程,而进程也会采取某些措施来相应信号。在产生和到达期间,信号处于等待(pending)状态。
-
通常一旦系统将要调度该进程运行,等待的信号会马上送达,或者此时进程正在运行,则会立刻传递该信号!
-
进程收到信号后,有如下默认操作(操作为其中之一)(可以通过man 7 signal查看详细英文介绍):
- 忽略信号
- 终止进程(异常终止,非主动调用exit())
- 产生core,同时终止进程(产生的core文件可以使用gdb调试,查看进程终止时的状态)
- 停止进程
- 恢复停止的进程,继续执行
-
当然进程收到信号后,除了可以按照上面五种信号的默认行为处理之外,还可以自定义改变信号到达时的处理行为。程序可以对信号设置的行为如下:
- 采用默认行为。适用于撤销之前对信号的行为做的修改、恢复其默认处理场景
- 忽略信号。适用于默认行为为终止进程的信号(可以通过man 7 signal查看不同信号的默认行为)。
- 执行信号处理程序(自定义的函数)
-
一些常见的信号描述
名称 信号值 描述 默认行为 SIGINT 2 终端中断,如ctrl C term SIGQUIT 3 终端退出,如ctrl \ core SIGBUS 7 发生内存访问错误 core SIGFPE 8 算数异常(如除0操作) core SIGKILL 9 "必杀"信号(该信号不可以被捕捉设置) term SIGSEGV 11 【非常常见】对内存的无效引用(如引用的页不存在、
更新只读内存、用户态访问内核态内存、解引用未初始化的指针等)core SIGPIPE 13 写读端关闭的管道时产生的错误 term SIGALRM 14 使用alarm或者setitimer设置的定时器到期 term SIGSTOP 19 必停信号(该信号不可以被捕捉设置) stop
信号的设置
-
有两个函数可以改变信号的设置,分别是signal()和sigaction()函数。
- sigaction函数具备signal函数所不具备的功能,但是signal函数使用方式简单
- signal函数不同系统的实现方式可能不太相同,所以它的移植性得不到保证,所以要追求可移植性,首选sigaction函数
-
signal()函数
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); //signum:需要设置的信号的编号 //handler:是一个函数指针,指向信号处理函数(注意信号处理函数的返回值void与参数int类型) //返回值:成功返回之前的信号处理(函数指针),失败返回SIG_ERR,并置错误码 //SIG_ERR的定义:#define SIG_ERR ((__sighandler_t) -1)
代码示例:
#include <stdio.h> #include <signal.h> void newHandler(int sig) { /* 对信号做处理 */ } int main(int argc, char **argv) { void (*oldHandler)(int); //对SIGINT信号进行捕捉并处理,处理行为在newHandler函数中定义 oldHandler = signal(SIGINT, newHandler); if(oldHandler == SIG_ERR){ printf("error\n"); return -1; } /* 此处假如对SIGINT信号设置为其它处理方式,那么可以在 * 处理完成后恢复旧的处理方式 */ //恢复信号的旧的行为 signal(SIGINT, oldHandler); //按照信号的默认行为处理 /* signal(SIGINT, SIG_DFL); */ //忽略该信号 /* signal(SIGINT, SIG_IGN); */ return 0; }
-
程序在执行的过程中,如果收到了信号,那么就会打断主程序,内核转而执行信号的处理函数,当信号处理函数执行完成后,主程序会在打断的位置恢复执行。
-
代码示例1:捕捉2号信号
#include <stdio.h> #include <signal.h> #include <unistd.h> void sigHandler(int sig) { printf("sig %d is comming\n", sig); } int main(int argc, char **argv) { if(SIG_ERR == signal(SIGINT, sigHandler)){ printf("error\n"); return -1; } //使用pause函数等待signal函数捕捉信号,并执行信号处理函数 //当信号处理函数执行完成后pause函数才会返回,失败返回-1,并置错误码 pause(); return 0; }
代码示例2:同时捕捉2、3号信号,只捕捉两次
#include <stdio.h> #include <signal.h> #include <unistd.h> void sigHandler(int sig) { printf("sig %d is comming\n", sig); } int main(int argc, char **argv) { if(SIG_ERR == signal(SIGINT, sigHandler)){ printf("error\n"); return -1; } if(SIG_ERR == signal(SIGQUIT, sigHandler)){ printf("error\n"); return -1; } //使用pause函数等待signal函数捕捉信号,并执行信号处理函数 //当信号处理函数执行完成后pause函数才会返回,失败返回-1,并置错误码 pause(); pause(); return 0; } //想要给进程发送2、3号信号,很简单ctrl c发送2号信号、ctrl \发送3号信号, //或者使用kill命令kill -2 pid, kill -3 pid; 查看本进程pid也很简单,pidof + 进程名, //或者ps -e|grep + 进程名
发送信号
-
上面的例子中提到主动给某个进程发送信号可以使用kill命令完成。其实还有一个kill()函数可以实现发送信号
int kill(pid_t pid, int sig); //sig:指定要发送给pid的信号编号
kill函数的pid有四种填法:
- pid > 0:发送信号给pid指定的进程
- pid = 0:发送信号给调用进程同组的每一个进程,包含调用者本身
- pid = -1:给系统中除init和本身两个进程外的所有进程发送信号(慎用),该信号也可以称为广播信号
- pid < -1:会向组id等于该pid绝对值的进程组内所有下属进程发送信号
代码示例:
#include <signal.h> #include <stdlib.h> int main(int argc, char **argv) { //给进程发送2号信号,进程pid通过传参传入 kill(atoi(argv[1]), 2); //给本进程所属的进程组内的每一个进程发送2号信号, //可以通过ps -ajx命令查看进程组id kill(0, 2); //向组id = |-128|的进程组所有进程发送2号信号 kill(-128, 2); //给系统中除init和本身两个进程外的所有进程发送2号信号 kill(-1, 2); return 0; }
-
如果kill函数的sig参数填0,表示发送空信号给指定进程,如果发送失败就证明目标进程不存在!
-
使用raise()函数发送信号
int raise(int sig); //raise() returns 0 on success, and nonzero for failure
该函数可以给调用者自身(进程或者线程)发送信号。
在单线程程序中,调用raise函数类似于调用kill函数如下操作:
kill(getpid(), sig);
在多线程程序中,调用raise函数等效操作如下:
pthread_kill(pthread_self(), sig);
-
使用killpg()函数向某一个进程组所有成员发送信号
int killpg(int pgrp, int sig);
调用killpg()函数相对于kill函数的如下操作:
kill(-pgrp, sig)
如果pgrp的值为0,那么会向调用者所属的进程组所有的进程发送信号。
显示信号的描述
每一个信号都有一串与它相关的打印说明,这写描述保存在sys_siglist数组中。除sys_siglist数组外还有两个函数也可以获取信号的描述,分别是:
-
#include <string.h> char *strsignal(int sig); //sig:需要获取信息的信号 //成功返回一个char*,指向对该信号描述的字符串,失败返回NULL
-
void psignal(int sig, const char *s); //s:用户给定的字符串,后面跟一个冒号,随后是对应sig的描述
三种方式代码示例:
#include <stdio.h>
#include <signal.h>
#include <string.h>
int main(int argc, char **argv)
{
//1.使用数组打印的方式(不推荐)
printf("mode1: %s\n", sys_siglist[2]);
//2.使用strsignal方式
char *p = strsignal(2);
printf("mode2: %s\n", p);
//3.使用psignal的方式
psignal(2, "mode3");
return 0;
}
//运行结果
mode1: Interrupt
mode2: Interrupt
mode3: Interrupt
信号第一部分整理完成,未完待续,敬请期待第二部分!
本人能力有限,如有错误望各位大佬不吝指正,原创不易,转载请注明出处!