-
信号概述
- 信号有时也称为软件中断,是事件发生时对进程的通知机制,他只在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
- 信号通常源于内核,引发内核为进程产生信号的各类时间如下:
- 对于前台进程,用户可以通过输入特殊终端字符来给它发送信号。比如Ctrl+C通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应的信号给相关进程。比如执行了一条异常的机器语言指令,或者引用了无法访问的内存区域。
- 系统状态变化,比如alarm定时器到期将引起SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
- 运行kill命令或调用kill函数
- 使用信号的主要目的:
- 让进程知道已经发生了一个特地的事情
- 强迫进程执行她自己代码中的信号处理程序
- 信号的特点:
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
- 优先级比较高
- 查看系统的信号:
kill -l
-
Linux信号一览表
编号 信号名称 对应事件 默认动作 1 SIGHUP 用户退出shell时,该shell启动的所有进程将收到这个信号 终止进程 2 SIGINT 当用户按下<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的进程发出此信号 终止进程 3 SIGQUIT 当用户按下<Ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的进程发出此信号 终止进程 4 SIGILL CPU检查到某进程执行了非法指令 终止进程并产生core文件 5 SIGTRAP 该信号由断点指令或其他trap指令产生 终止进程并产生core文件 6 SIGABRT 调用abort函数时产生该信号 终止进程并产生core文件 7 SIGBUS 非法访问内存地址,包括内存对齐出错 终止进程并产生core文件 8 SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 终止进程并产生core文件 9 SIGKILL 无条件终止程序。该信号不能别忽略、处理和阻塞 终止进程,可以杀死任何进程 10 SIGUSR1 用户定义的信号。即程序员可以在程序中定义并使用该信号 终止进程 11 SIGSEGV 表示进程进行了无效内存访问(段错误) 终止进程并产生core文件 12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 终止进程 13 SIGPIPE Broken pipe,向一个没有读端的管道写数据 终止进程 14 SIGALRM 定时器超时,超时的时间由系统调用alarm设置 终止程序 15 SIGTERM 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来表示程序正常退出。执行shell命令kill时,缺省产生这个信号 终止程序 16 SIGSTKFLT Linux早期版本出现的信号,先仍保留向后兼容 终止进程 17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略这个信号 18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略 19 SIGSTOP 停止(暂停)进程的执行。信号不能被忽略、处理和阻塞 为终止进程 20 SIGTSTP 停止终端交互进程的运行。按下<Ctrl+Z>组合键时发出这个信号 暂停进程 21 SIGTTIN 后台进程读终端控制台 暂停进程 22 SIGTTOU 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 暂停进程 23 SIGGURG 套接字上有紧急数据时,向当前正在运行的进程发出信号,报告有紧急数据到达。如网络带外数据到达 忽略该信号 24 SIGXCPU 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程 终止进程 25 SIGXFSZ 超过文件的最大长度设置 终止进程 26 SIGVTALRM 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 终止进程 27 SIGPROF 类似于SIGVTALRM,它不光包括该进程占用CPU时间,还包括执行系统调用时间 终止进程 28 SIGWINCH 窗口变化大小时发出 忽略该信号 29 SIGIO 此信号向进程指示发出了一个异步IO事件 忽略该信号 30 SIGPWR 关机 终止进程 31 SIGSYS 无效的系统调用 终止进程并产生core文件 34~64 SIGRTMIN~SIGRTMAX Linux的实时信号,它们没有固定的含义(可以由用户自定义) 终止进程 -
信号的5种默认处理动作
- 查看信号的详细信息:
man 7 signal
- 信号的5种默认处理动作
- Term 终止进程
- Ign 当前进程忽略掉这个信号
- Core 终止进程并生成一个Core文件,Core文件用于保存进程异常退出的错误信息
- Stop 暂停当前进程
- Cont 继续执行当前被暂停的进程
- 信号的几种状态:产生、未决、递达
- SIGKILL和SIGSTOP信号不能被捕捉、阻塞或忽略,只能执行默认动作
- 查看信号的详细信息:
-
信号相关的函数
-
kill函数
#include<signal.h> int kill(pid_t pid,int sig);
-
功能:给任何的进程或进程组 pid,发送任何的信号 sig
-
参数:
-pid:需要发送给进程的id
pid>0:将信号发送给指定的进程
pid=0:将信号发送给当前的进程组
pid=-1:将信号发送给每一个有权限接收这个信号的进程
pid<-1:这个pid等于某个进程组的ID取反
-sig:需要发送给的信号的编号或时宏值,0表示不发送任何信号
父进程中利用kill函数杀死子进程的程序:
#include<stdio.h> #include<sys/types.h> #include<signal.h> #include<unistd.h> int main(){ pid_t pid=fork(); if(pid>0){ //父进程 printf("parent process\n"); sleep(2); printf("kill child process\n"); kill(pid,SIGINT); }else if(pid==0){ //子进程 for(int i=0;i<5;++i){ printf("child process\n"); sleep(1); } } return 0; }
-
-
raise函数
#include<signal.h> int raise(int sig);
-
功能:给当前进程发送信号
-
参数:-sig:要发送的信号
-
返回值:
成功返回0
失败返回非0
-
-
abort函数
#include<signal.h> int abort(void);
- 功能:发送SIGABRT信号给当前进程,杀死当前进程
-
alarm函数
#include<unistd.h> unsigned int alarm(unsigned int seconds);
-
功能:设置定时器。调用函数开始倒计时,当倒计时为0时,函数会给当前进程发送一个SIGALARM信号。该函数是不阻塞的。
SIGALARM:默认终止当前的进程,每个进程有且仅有一个定时器。如果多次调用alarm函数,前面调用的函数设置的时间会失效。
-
参数:-seconds:倒计时时长,单位:秒。如果参数为0,定时器无效可以通过alarm(0)取消定时器。
-
返回值:
如果前面没有调用此函数,则返回0;
如果前面调用过该函数,则返回前一个倒计时剩余时间。
#include<stdio.h> #include<unistd.h> int main(){ printf("start...\n"); unsigned int seconds=alarm(5); //这里返回0 printf("seconds=%d",seconds); sleep(2); seconds=alarm(2); printf("seconds=%d",seconds); //这里返回3 while(1){} return 0; } /* 程序运行结果: seconds=0 seconds=3(2秒后输出) CLOCK(4秒后输出) */
-
-
setitimer函数
#include<sys/time.h> int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
-
功能:设置定时器,可以替代alarm函数。精度为us,可以实现周期性的定时。
-
参数:
-which:定时器以什么时间计时
ITIMER_REAL:真实时间,时间到达发送SIGALRM信号(常用)
ITIMER_VIRTUAL:用户时间,时间到达发送SIGVTALRM信号
ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达发送SIGPROF信号
-new_value:设置定时器的属性
//定时器结构体 struct itimerval{ struct timerval it_interval; //每个阶段的时间,间隔时间 struct timerval it_value; //延迟多长时间执行定时器 }; //时间结构体 struct timerval{ time_t tv_sec; //秒数 suseconds tv_usec; //微秒 }
-old_value:记录上一次的定时的时间参数,一般不使用,指定为NULL
-
返回值:
成功:返回0
失败:返回-1,并设置errno
-
-
-
-
信号捕获
-
signal函数
#include<signal.h> typedef void (*sighandler_t) (int); sighandler_t signal(int signum,sighandler_t handler);
-
功能:设置某个信号的捕捉行为
SIGKILL和SIGSTOP不能被捕捉,也不能被忽略
-
参数:
-signum:要捕捉的信号
-handler:捕捉到的信号要如何处理
-SIG_IGN:忽略信号
-SIG_DFL:使用信号默认的行为
-回调函数:这个函数是由内核调用的,程序员只负责写,定义捕捉到的信号如何处理信号。
typedef void (*sighandler_t) (int);
-
返回值:
成功:返回上一次注册的信号处理函数的地址。第一次调用返回NULL。
失败:返回SIG_ERR,设置错误号
-
#include<sys/types.h> #include<signal.h> #include<stdio.h> #include<stdlib.h> #include<sys/time.h> void myalarm(int num){ printf("捕捉到的信号的编号是:%d\n",num); printf("xxxxxxx\n"); } int main(){ //注册信号捕捉 //信号捕捉一定要在设置定时器之前 //void (*sighandler_t)(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("setitimer"); exit(0); } getchar(); return 0; }
-
sigaction函数
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
-
功能:检查或者改变信号的处理。信号捕捉
-
参数:
-signum:需要捕捉的信号的编号或宏值
-act:捕捉到信号之后的处理动作
-oldact:上一次信号捕捉相关的设置,一般设置为NULL
-
返回值:成功返回0,失败返回-1
struct sigaction{ void (*sa_handler)(int);//函数指针,指向的函数就是信号捕捉到之后的处理函数 void (*sa_sigaction)(int,siginfo *,void *);//不常用 sigset_t sa_mask;//临时阻塞信号集,信号捕捉函数执行过程中,临时阻塞信号 int sa_flags;/*使用哪一个信号处理对捕捉到的信号进行处理 *这个值可以是0,表示sa_handler; *也可以是SA_SIGINFO,表示使用sa_sigaction */ void (*sa_restorer)(void);//已经废弃了 }
-
-
-
信号集
-
信号集是一个用来管理和操作信号的数据结构,它是多个信号的集合,其系统数据结构类型为
sigset_t
-
在PCB中有两个非常重要的信号集。一个是“阻塞信号集”,另一个是“未决信号集"。这两个信号集都是内核使用位图机制实现的,但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行操作。
-
信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间
-
信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号的产生
-
信号的阻塞就是让系统暂时保留信号,留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
-
未决信号集和阻塞信号集的工作原理
-
用户通过键盘Ctrl+C产生2号信号SIGINT(信号被创建)
-
此时信号刚产生,但还未被处理(即未决状态)
-内核中会将所有未处理的信号存储在一个集合中(未决信号集)
-SIGINT信号会被存储在第二个标志位上
-如果这个标志位的值位0,表示信号已被处理,不是未决状态
-如果这个标志为的值位1,表示信号未被处理,处于未决状态
-
处于未决状态的信号是需要被处理的,在处理之前需要和另一个信号集(阻塞信号集)进行比较
-阻塞信号及默认不阻塞任何信号
-如果想要阻塞某个信号需要调用系统API
-
在处理的时候和阻塞信号集中的标志位进行比较,看是不是对该信号设置阻塞了
-如果没有阻塞,这个信号就会被处理
-如果设置了阻塞,那么这个信号继续处于未决状态,直到阻塞解除,这个信号就被处理
-
-
相关函数(对自定义的信号集进行操作)
头文件
#include<signal.h>
(1)sigemptyset函数
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有标志位置设置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0,失败返回-1
(2)sigfillset函数
int sigfillset(sigset_t *set);
- 功能:将信号集中所有的标志位置1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0,失败返回-1
(3)sigaddset函数
int sigaddset(sigset_t *set,int signum);
-
功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
-
参数:
-set:传出参数,需要操作的信号集
-signum:需要设置阻塞的那个信号
-
返回值:成功返回0,失败返回-1
(4)sigdelset函数
int sigdelset(sigset_t *set,int signum);
-
功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
-
参数:
-set:传出参数,需要操作的信号集
-signum:需要设置不阻塞的那个信号
-
返回值:成功返回0,失败返回-1
(5)sigismember函数
int sigismember(const sigset_t *set,int signum);
-
功能:判断某个信号是否阻塞
-
参数:
-set:需要操作的信号集
-signum:需要判断的那个信号
-
返回值:
1: signum被阻塞
0: signum不被阻塞
-1:调用失败
#include<signal.h> #include<stdlib.h> #include<stdio.h> int main(){ //创建一个信号集 sigset_t set; //清空信号集的内容 sigemptyset(&set); //判断SIGINT是否在信号集中set的 int ret=sigismember(&set,SIGINT); if(ret==0){ printf("SIGINT不阻塞\n"); }else if(ret==1){ printf("SIGINT阻塞\n"); }else{ perror("sigismember"); exit(0); } //添加几个信号到信号集中的 sigaddset(&set,SIGINT); sigaddset(&set,SIGQUIT); //判断SIGINT是否在信号集中set的 ret=sigismember(&set,SIGINT); if(ret==0){ printf("SIGINT不阻塞\n"); }else if(ret==1){ printf("SIGINT阻塞\n"); }else{ perror("sigismember"); exit(0); } //判断SIGQUIT是否在信号集中set的 ret=sigismember(&set,SIGQUIT); if(ret==0){ printf("SIGINT不阻塞\n"); }else if(ret==1){ printf("SIGINT阻塞\n"); }else{ perror("sigismember"); exit(0); } sigdelset(&set,SIGQUIT); //判断SIGQUIT是否在信号集中set的 ret=sigismember(&set,SIGQUIT); if(ret==0){ printf("SIGINT不阻塞\n"); }else if(ret==1){ printf("SIGINT阻塞\n"); }else{ perror("sigismember"); exit(0); } return 0; }
(6)sigprocmask函数
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
-
功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
-
参数:
-how:如何对内核阻塞信号金进行处理
SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变。假设内核中默认的阻塞信号是mask,mask | set
SIG_UNBLOCK:根据用于设置的数据,对内核中的数据进行接触阻塞。相当于mask &= ~set
SIG_SETMASK:覆盖内核中原来的值
-set:已经初始化好的用户自定义数据集
-oldset:保存设置之前内核中的阻塞信号集的状态,可以是NULL
-
返回值:
成功返回0;
失败返回-1,并设置错误号:EFAULT、EINVAL
(7)sigpending函数
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存内核中未决信号集的信息
- 返回值:成功返回0,失败返回-1
-
-
内核实现信号捕捉的过程
-
SIGCHLD信号
- SIGCHLD信号产生的条件:
- 子进程终止时
- 子进程收到SIGSTOP信号停止时
- 子进程处于停止状态,接收到SIGCONT后唤醒时
- 使用SIGCHLD信号可以解决僵尸进程的问题
- SIGCHLD信号产生的条件:
利用SIGCHLD解决僵尸进程的例子:
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
void myFun(int num){
printf("捕捉到的信号:%d\n",num);
//回收子进程PCB的资源
// 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 id:%d\n",getpid());
sleep(1);
}
}else if(pid==0){
//子进程
printf("child process pid:%d\n",getpid());
}
return 0;
}