一、信号
1.1什么是信号
信号是 Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断.转而处理某一个突发事件。
发往进程的信号多数都是来自于内核,引发内核为进程产生信号的各类事件如下:
1.对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入ctrl+c通常是给进程发送一个中断信号
2.硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者内存越界
3.系统状态发生变化,比如alarm定时器到期,将引发SIGALRM信号,进程执行的CPU时间超限,或者该进程某个子程序退出都会引发信号。
4.运行KILL命令或者调用KILL函数。
1.2信号的主要目的和特点
目的:
---让进程知道已经发生了一个特定的事情。
---强迫进程执行它自己代码中的信号处理程序
特点:
---简单
---不能携带大量信息
---满足某个特定条件才发送
---优先级比较高
kill -l // 查看系统定义的信号列表
1.3 LINUX系统信号一览
注意:其中Core文件是保存进程异常退出的错误信息。
信号的5中默认处理动作 | |
Term | 终止进程 |
Ign | 当前进程忽略掉这个信号 |
Core | 终止进程,并且生成Core文件 |
Stop | 暂停当前进程 |
Cont | 继续执行当前被暂停的进程 |
注意:9号信号SIGKILL和19号信号SIGSTOP不能被捕捉,忽略或者阻塞,且只能执行默认动作
编号 | 信号名称 | 对应事件 | 默认动作 |
1 | SIGHUP |
用户退出shell时,由该shell启动的所有进程将收到这个信号 |
Term |
2 | SIGINT |
当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 |
Term |
3 | SIGQUIT |
用户按下<Ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号 |
Term |
4 | SIGILL |
CPU检测到某进程执行了非法指令 |
Core |
5 | SIGTRAP |
该信号由断点指令或其他 trap指令产生 |
Core |
6 | SIGABRT |
调用abort函数时产生该信号 |
Core |
7 | SIGBUS |
非法访问内存地址,包括内存对齐出错 |
Core |
8 | SIGFPE |
在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 |
Core |
9 | SIGKILL |
无条件终止进程。该信号不能被忽略,处理和阻塞 |
Term(可以杀死任何进程) |
10 | SIGUSR1 |
用户定义的信号。即程序员可以在程序中定义并使用该信号 |
Term |
11 | SIGSEGV |
指示进程进行了无效内存访问(段错误) |
Core |
12 | SIGUSR2 |
另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 |
Term |
13 | SIGPIPE |
Broken pipe,向一个没有读端的管道写数据 |
Term |
14 | SIGALRM |
定时器超时,超时的时间由系统调用alarm设置 |
Term |
15 | SIGTERM |
程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 |
Term |
16 | SIGSTKFLT |
Linux早期版本出现的信号,现仍保留向后兼容 |
Term |
17 | SIGCHLD |
子进程结束时,父进程会收到这个信号 |
Ign |
18 | SIGCONT |
如果进程已停止,则使其继续运行 |
Cont/Ign(如果进程已运行) |
19 | SIGSTOP |
停止进程的执行。信号不能被忽略,处理和阻塞 |
Stop |
20 | SIGTSTP |
停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 |
Stop |
21 | SIGTTIN |
后台进程读终端控制台 |
Stop |
22 | SIGTTOU |
该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 |
Stop |
23 | SIGURG |
套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 |
Ign |
24 | SIGXCPU |
进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程 |
Term |
25 | SIGXFSZ |
超过文件的最大长度设置 |
Term |
26 | SIGVTALRM |
虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 |
Term |
27 | SIGPROF |
类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 |
Term |
28 | SIGWINCH |
窗口变化大小时发出 |
Ign |
29 | SIGIO |
此信号向进程指示发出了一个异步Io事件 |
Ign |
30 | SIGWR | 关机 | Term |
31 | SIGSYS |
无效的系统调用 |
Core |
34-64 |
SIGRTMIN ~ SIGRTMAX |
LINUX的实时信号,它们没有固定的含义(可以由用户自定义) |
Term |
1.4 core文件的产生与使用
要让系统能够保存core文件,首先要将core文件存储空间改为1024(最大值)或者unlimited(不限制),使用ulimit -a命令进行查看,ulimit -c 1024进行修改
使用以下代码(使用未初始化指针),可以生成core文件。
#include<stdio.h> #include<string.h> int main(){ char * buf; strcpy(buf, "hello"); return 0; }
在编译时加上-g参数,调试时输入core-file 名字,即可查看core文件内容,内容如下:
可以看出是因为使用了野指针产生了段错误。
二、信号相关的函数
2.1 kill/raise/abort
功能和实例如下:
/* #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); -作用:给任何进程或者进程组发送任何信号 -参数: pid:进程id >0:发给对应进程 ==0:给当前组发 ==-1:发送给每一个有权限接收的进程 <-1:取绝对值为进程组id sig:信号宏值(或者编号) -返回值: -成功:0 -失败:-1 int raise(int sig); -功能:给当前进程发送信号 -参数: -sig:信号宏值(或者编号) -返回值: -成功:0 -失败:非0 void abort(void); -功能:发送SIGABRT信号发给当前进程,目的是杀死当前进程 */ #include <sys/mman.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include<stdio.h> #include <fcntl.h> #include<string.h> #include <signal.h> int main(){ pid_t pid = fork(); //子进程 if(pid==0){ for(int i=0;i<5;++i){ printf("child process: %d\n", i); sleep(1); } } //父进程 else if (pid>0){ printf("parent process\n"); sleep(2); //杀死子进程 printf("kill child\n"); kill(pid, SIGINT); } }
2.2 alarm
请注意:程序运行的实际时间=内核时间+用户时间+消耗的时间, 文件I/O操作是比较浪费时间的
/* #include <unistd.h> unsigned int alarm(unsigned int seconds); -功能:设置定时器,倒计时为0时,会给当前进程发送SIGALARM信号,不阻塞 -参数: -seconds:多少秒,为0即定时器无效,可以用来取消一个定时器 -返回值: 之前有定时器:返回上一次设置alarm倒计时剩余的时间 之前无定时器:0 SIGALARM默认终止当前进程,每个进程有且仅有一个定时器 */ #include <sys/mman.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include<stdio.h> #include <fcntl.h> #include<string.h> #include <signal.h> int main(){ int sec = alarm(5); printf("sec:%d\n", sec); //0 sleep(2); sec = alarm(4); //不阻塞 printf("sec:%d\n", sec); //3 while(1){ sleep(1); printf("倒计时中\n"); } return 0; }
2.3 setitimer
与alarm函数的一次性不同,setitimer可以帮助我们完成周期性的操作,这里介绍函数本身
/* #include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); -功能:设置定时器,可以替代alarm函数,精度可以达到微秒,且可以实现周期性的定时 -参数: -which:宏值,指定itimer使用的定时方式 ITIMER_REAL :真实时间,时间到达发送SIGALRM ITIMER_VIRTUAL :虚拟的时间(用户时间),时间到发送SIGVTALRM ITIMER_PROF :用户时间+内核时间,时间到发送SIGPROF -new_value:设置定时器属性 struct itimerval { //定时器结构体 struct timeval it_interval; //每个阶段的时间,间隔时间 struct timeval it_value; //延迟多长时间执行 }; struct timeval { //时间的结构体 time_t tv_sec; //秒数 suseconds_t tv_usec; //微秒 }; -old_value:记录上一次定时的时间参数,传出参数,用不上传递NULL -返回值: -成功:0 -失败:-1 */ #include <sys/time.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include<stdio.h> #include<string.h> #include <signal.h> //过3秒以后,每隔两秒钟定时一次 int main(){ struct itimerval new_value; //设置间隔时间 new_value.it_interval.tv_sec = 2; new_value.it_interval.tv_usec = 0; //设置延迟时间,3秒之后发送第一次信号 new_value.it_value.tv_sec = 3; new_value.it_value.tv_usec = 0; int ret = setitimer(ITIMER_REAL, &new_value, NULL); printf("定时器开始了....\n"); if(ret==-1){ perror("itimer"); exit(0); } getchar(); return 0; }
2.4 signal信号捕捉函数
回调数函:非程序员调用,在信号产生时由内核调用,使用signal函数实现周期定时
/* #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 请注意:SIGKILL,SIKSTOP不能被捕捉和忽略 作用:设置某个信号的捕捉行为 参数: signum: 要被捕捉的信号 handler: SIG_IGN :被忽略 SIG_DFL :使用默认动作 -回调数函:捕捉到信号后如何处理信号,函数指针是实现回调的手段,将函数名放在其中就行 返回值: -成功:返回上一次信号处理函数地址 -失败:返回SIG_ERR */ #include <sys/time.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include<stdio.h> #include<string.h> #include <signal.h> void myalarm(int num); //过3秒以后,每隔两秒钟定时一次 int main(){ //在信号开始前注册信号捕捉 //void (*sighandler_t)(int) 函数指针(*),返回值void,参数为Int, int 为捕捉到信号的值 signal(SIGALRM, myalarm); struct itimerval new_value; //设置间隔时间 new_value.it_interval.tv_sec = 2; new_value.it_interval.tv_usec = 0; //设置延迟时间,3秒之后发送第一次信号 new_value.it_value.tv_sec = 3; new_value.it_value.tv_usec = 0; int ret = setitimer(ITIMER_REAL, &new_value, NULL); printf("定时器开始了....\n"); if(ret==-1){ perror("itimer"); exit(0); } getchar(); return 0; } void myalarm(int num){ printf("捕捉到了信号,编号是:%d\n", num); printf("xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); }
三、信号集
3.1什么是信号集和信号集的作用
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t。在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
3.2 和自定义信号集相关的系统调用
/* 以下信号集对自定义信号集进行操作 int sigemptyset(sigset_t *set); -功能:清空信号集中的数据,将信号集中的所有标志位置0 -参数:传出参数set,将需要操作的set清空为0传出 -返回值: -成功:0 -失败:-1 int sigfillset(sigset_t *set); -功能:清空信号集中的数据,将信号集中的所有标志位置1 -参数:传出参数set,将需要操作的set设置为1传出 -返回值: -成功:0 -失败:-1 int sigaddset(sigset_t *set, int signum); -功能:设置信号集中的一个信号对应的标志位为1,表示阻塞这个信号 -参数: set:传出参数,需要操作的信号集 signum:信号编号 -返回值: -成功:0 -失败:-1 int sigdelset(sigset_t *set, signum); -功能:设置信号集中的一个信号对应的标志位为0,表示不阻塞这个信号 -参数: set:传出参数,需要操作的信号集 signum:信号编号 -返回值: -成功:0 -失败:-1 int sigismember(const sigset_t *set, int signum); -功能:判断某个信号是否阻塞 -参数: set:需要操作的信号集,非传出参数 signum:信号编号 -返回值: -1:表示signum被阻塞 -0:表示signum不阻塞 --1:调用失败 */ #include <signal.h> #include<stdio.h> int main(){ //创建一个信号集 sigset_t set; //初始化信号集 sigemptyset(&set); //判断SIGINT是否阻塞,是否在信号集中 int ret = sigismember(&set, SIGINT); if(ret==0){ printf("SIGINT不阻塞\n"); } else if (ret==1){ printf("SIGINT阻塞\n"); } //添加几个信号 sigaddset(&set, SIGINT); sigaddset(&set, SIGQUIT); //判断SIGINT是否阻塞,是否在信号集中 ret = sigismember(&set, SIGQUIT); if(ret==0){ printf("SIGQUIT不阻塞\n"); } else if (ret==1){ printf("SIGQUIT阻塞\n"); } //从信号集删除一个信号 sigdelset(&set, SIGQUIT); ret = sigismember(&set, SIGQUIT); if(ret==0){ printf("SIGQUIT不阻塞\n"); } else if (ret==1){ printf("SIGQUIT阻塞\n"); } }
3.3用自定义信号集操作内核阻塞信号集,并读取未决信号集
/* #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); -功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换) -参数: -how:如何对内核阻塞信号集进行处理 SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(mask | set) SIG_UNBLOCK:根据用户设置的数据,对内核的数据进行解除阻塞(mask &= ~set) -set:用户初始化好的自定义信号集 -oldset:保存设置之前内核中阻塞信号集状态,可以为NULL -返回值: -成功:0 -失败:-1 int sigpending(sigset_t *set) -功能:获取内核中的未决信号集 -参数:set,传出参数,保存内核未决信号集 */ //下面程序为把所有常规信号的未决状态打印到屏幕上(1-31) //设置某些信号为阻塞,通过键盘产生信号 #include <signal.h> #include<stdio.h> #include<stdlib.h> #include <unistd.h> int main(){ //设置2,3号信号阻塞 sigset_t set; sigemptyset(&set); //将2,3号信号阻塞 sigaddset(&set, SIGQUIT); sigaddset(&set, SIGINT); //修改内核中的阻塞信号集 sigprocmask(SIG_BLOCK, &set, NULL); int num = 0; while(1){ //获取当前未决信号集数据 sigset_t pendingset; sigemptyset(&pendingset); sigpending(&pendingset); //遍历前32位 for(int i = 1;i<=32;++i){ if(sigismember(&pendingset, i)==1){ printf("1"); } else if(sigismember(&pendingset, i)==0){ printf("0"); } else{ perror("sigismember"); exit(0); } } sleep(1); ++num; printf("\n"); if(num==10){ //解除阻塞 sigprocmask(SIG_UNBLOCK, &set, NULL); } } return 0; }
3.4 sigaction信号捕捉函数
/* #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); -功能:检查或者改变信号的处理。信号捕捉 -参数: signum:需要捕捉的信号编号(宏) act:捕捉到信号后相应的处理动作 oldact:上一次的设置,一般不适用,传递NULL 返回值: 成功:0 失败:-1 其中struct sigaction: struct sigaction { //函数指针,指向的是捕捉到信号后的处理函数 void (*sa_handler)(int); //不常用 void (*sa_sigaction)(int, siginfo_t *, void *); //设置的临时阻塞信号集,信号捕捉函数执行过程中,阻塞某些信号 sigset_t sa_mask; //sa_flags,使用哪一个信号处理函数对信号进行处理,常用: \\ 0:使用sa_handler \\ SA_SIGINFO:使用sa_sigaction int sa_flags; //已废弃,传递NULL void (*sa_restorer)(void); }; */ #include <sys/time.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include<stdio.h> #include<string.h> #include <signal.h> void myalarm(int num); //过3秒以后,每隔两秒钟定时一次 int main(){ //在信号开始前注册信号捕捉 struct sigaction act; act.sa_flags=0; act.sa_handler=myalarm; sigemptyset(&act.sa_mask);//情况临时信号集 sigaction(SIGALRM, &act,NULL); struct itimerval new_value; //设置间隔时间 new_value.it_interval.tv_sec = 2; new_value.it_interval.tv_usec = 0; //设置延迟时间,3秒之后发送第一次信号 new_value.it_value.tv_sec = 3; new_value.it_value.tv_usec = 0; int ret = setitimer(ITIMER_REAL, &new_value, NULL); printf("定时器开始了....\n"); if(ret==-1){ perror("itimer"); exit(0); } //这里不能用getchar()进行阻塞,sigaction会中断getchar导致程序向下运行。 while(1); return 0; } void myalarm(int num){ printf("捕捉到了信号,编号是:%d\n", num); printf("xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); }
3.5 内核实现信号捕捉的过程
注意:使用sigaction时,在进行信号处理函数时,会生产临时的阻塞信号集。当同一信号在执行回调函数时被接收,会被阻塞在未决信号集中,且未决信号集只能记录一次,其余的信号就被丢弃。
3.6 SIGCHLD信号
捕捉SIGCHLD信号,解决僵尸进程问题
/* SIGCHLD信号产生的3个条件: 1.子进程结束 2.子进程暂停了 3.子进程继续运行 都会给父进程发送该信号,父进程默认忽略该信号。 使用SIGCHLD信号解决僵尸进程的问题。 */ #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <sys/wait.h> void myFun(int num) { printf("捕捉到的信号 :%d\n", num); // 回收子进程PCB的资源 // while(1) { // wait(NULL); // } while(1) { int ret = waitpid(-1, NULL, WNOHANG); if(ret > 0) { printf("child die , pid = %d\n", ret); } else if(ret == 0) { // 说明还有子进程或者 break; } else if(ret == -1) { // 没有子进程 break; } } } int main() { // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉 sigset_t set; sigemptyset(&set); sigaddset(&set, SIGCHLD); sigprocmask(SIG_BLOCK, &set, NULL); // 创建一些子进程 pid_t pid; for(int i = 0; i < 20; i++) { pid = fork(); if(pid == 0) { break; } } if(pid > 0) { // 父进程 // 捕捉子进程死亡时发送的SIGCHLD信号 struct sigaction act; act.sa_flags = 0; act.sa_handler = myFun; sigemptyset(&act.sa_mask); sigaction(SIGCHLD, &act, NULL); // 注册完信号捕捉以后,解除阻塞 sigprocmask(SIG_UNBLOCK, &set, NULL); while(1) { printf("parent process pid : %d\n", getpid()); sleep(2); } } else if( pid == 0) { // 子进程 printf("child process pid : %d\n", getpid()); } return 0; }
这里要注意在父进程注册信号集完成前阻塞SIGCHLD信号,避免出现段错误。