目录
0x01 信号
0x02 信号相关的函数
一、kill函数
二、alarm()函数
三、setitimer()
四、signal()
0x03 信号集
一、信号集的处理过程 编辑
二、关于信号集处理的函数
0x04 内核实现信号捕捉的过程
0x05 SIGCHLD信号
信号是Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时候也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件,处理完后就继续处理前面的事情。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号,比如输入Ctrl+C通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。
系统状态变化,比如alarm定时器到期将引出SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
运行kill命令或调用kill函数。
使用信号的两个主要目的:
让进程知道已经发生了一个特定的事情。
强迫进程执行它自己代码中的信号处理程序。
信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
查看系统定义的信号列表:kill -l
前31个信号为常规信号,其余为实时信号:
那么对于上述的信号描述可以看到如下:
SIGHUP 1 /* Hangup (POSIX). */ 终止进程 终端线路挂断
SIGINT 2 /* Interrupt (ANSI). */ 终止进程 中断进程 Ctrl+C
SIGQUIT 3 /* Quit (POSIX). */ 建立CORE文件终止进程,并且生成core文件 Ctrl+\
SIGILL 4 /* Illegal instruction (ANSI). */ 建立CORE文件,非法指令
SIGTRAP 5 /* Trace trap (POSIX). */ 建立CORE文件,跟踪自陷
SIGABRT 6 /* Abort (ANSI). */
SIGIOT 6 /* IOT trap (4.2 BSD). */ 建立CORE文件,执行I/O自陷
SIGBUS 7 /* BUS error (4.2 BSD). */ 建立CORE文件,总线错误
SIGFPE 8 /* Floating-point exception (ANSI). */ 建立CORE文件,浮点异常
SIGKILL 9 /* Kill, unblockable (POSIX). */ 终止进程 杀死进程
SIGUSR1 10 /* User-defined signal 1 (POSIX). */ 终止进程 用户定义信号1
SIGSEGV 11 /* Segmentation violation (ANSI). */ 建立CORE文件,段非法错误
SIGUSR2 12 /* User-defined signal 2 (POSIX). */ 终止进程 用户定义信号2
SIGPIPE 13 /* Broken pipe (POSIX). */ 终止进程 向一个没有读进程的管道写数据
SIGALARM 14 /* Alarm clock (POSIX). */ 终止进程 计时器到时
SIGTERM 15 /* Termination (ANSI). */ 终止进程 软件终止信号
SIGSTKFLT 16 /* Stack fault. */
SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
SIGCHLD 17 /* Child status has changed (POSIX). */ 忽略信号 当子进程停止或退出时通知父进程
SIGCONT 18 /* Continue (POSIX). */ 忽略信号 继续执行一个停止的进程
SIGSTOP 19 /* Stop, unblockable (POSIX). */ 停止进程 非终端来的停止信号
SIGTSTP 20 /* Keyboard stop (POSIX). */ 停止进程 终端来的停止信号 Ctrl+Z
SIGTTIN 21 /* Background read from tty (POSIX). */ 停止进程 后台进程读终端
SIGTTOU 22 /* Background write to tty (POSIX). */ 停止进程 后台进程写终端
SIGURG 23 /* Urgent condition on socket (4.2 BSD). */ 忽略信号 I/O紧急信号
SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */ 终止进程 CPU时限超时
SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */ 终止进程 文件长度过长
SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */ 终止进程 虚拟计时器到时
SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */ 终止进程 统计分布图用计时器到时
SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ 忽略信号 窗口大小发生变化
SIGPOLL SIGIO /* Pollable event occurred (System V). */
SIGIO 29 /* I/O now possible (4.2 BSD). */ 忽略信号 描述符上可以进行I/O
SIGPWR 30 /* Power failure restart (System V). */
SIGSYS 31 /* Bad system call. */
SIGUNUSED 31
对于从34~64的信号,其实它是LINUX的实时信号,他们并没有固定的含义,可以由用户自定义,他们的结果都是终止进程。
查看信号的详细信息:man 7 signal
信号的五种默认处理动作:
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
信号的几种状态:产生、未决、递达
SIGKILL和SIGSTOP信号不能被捕捉、阻塞或忽略,只能执行默认动作。
#include
那么其使用可以看如下代码:
#include
#include
#include
#include
#include
int main()
{pid_t pid = fork();if(pid==0){int i=0;for(i=0;i<5;i++){sleep(1);printf("child process\n");}}else if(pid>0){printf("parent process\n");sleep(2);printf("kill child process now\n");kill(pid,SIGINT);}return 0;
}
#include
下面这个代码验证了alarm函数的不阻塞性:
#include
#include
{int secOnds= alarm(5);printf("secOnds= %d\n",seconds); //0sleep(2);secOnds= alarm(2);printf("secOnds= %d\n",seconds); //3while(1){}return 0;
}
#include
那么下面我们实现一个过了三秒后,每隔两秒定时一次的定时器:
#include
#include
#include
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("clock start!!\n");if(ret == -1){perror("setitimer");exit(0);}getchar();return 0;
}
#include
那么我们就可以实现对上述定时信号的捕捉:
#include
#include
#include
#include
{printf("the signal nums is : %d\n",num);printf("xxxxxx\n");
}//过三秒中 每个两秒定时一次
int main()
{//注册信号捕捉//signal(SIGALRM,SIG_IGN); //可以实现定时重复执行,总是回到这一句,进程并不会终止//signal(SIGALRM,SIG_DFL); //会终止进程//指定回调函数 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("clock start!!\n");if(ret == -1){perror("setitimer");exit(0);}getchar();return 0;
}
那么这个alarm与setitimer的区别在于,alarm只能定时一次,而setitimer可以实现周期性的定时。setitimer如何实现周期性,这个时候就需要信号捕捉signal。但是这个signal函数需要使用sigaction来替换。具体是为什么,可以先学学信号集这个概念。
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t
。
在PCB中有两个非常重要的信号集,一个称为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的(也就是使用二进制位来进行实现)。但操作系统不允许我们直接对这两个信号集进行操作,而需自定义另一个集合,借助信号集操作函数来对PCB中的这两个信号集做修改。
信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送,由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
用户通过键盘Ctrl C,产生一个2号信号SIGINT(信号被创建)
信号产生但是没有被处理(处于一个未决状态)
在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集)
SIGINT信号,状态是被存储在第二个标志位上。(也就是理解为现在有一个64位的寄存器,对寄存器的值进行修改),这个标志位的值为0,表示信号并不是未决状态,当这个标志位的值为1时,说明信号处于未决状态。
未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集)进行比较。
阻塞信号集默认不阻塞任意一个信号,但是我们也可以设置为阻塞。然后不会去处理,所以我们就是操作这个地方。
如果想要阻塞某些信号,需要用户调用系统的API。
在处理的时候和这个阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了。
如果没有阻塞,那么这个信号就被处理。
如果阻塞了,这个信号继续处于未决状态,直到阻塞解除,这个信号就被处理。
int sigemptyset(sigset_t *set);- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0- 参数:set,传出参数,需要操作的信号集- 返回值:成功返回0, 失败返回-1int sigfillset(sigset_t *set);- 功能:将信号集中的所有的标志位置为1- 参数:set,传出参数,需要操作的信号集- 返回值:成功返回0, 失败返回-1int sigaddset(sigset_t *set, int signum);- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号- 参数:- set:传出参数,需要操作的信号集- signum:需要设置阻塞的那个信号- 返回值:成功返回0, 失败返回-1int sigdelset(sigset_t *set, int signum);- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号- 参数:- set:传出参数,需要操作的信号集- signum:需要设置不阻塞的那个信号- 返回值:成功返回0, 失败返回-1int sigismember(const sigset_t *set, int signum);- 功能:判断某个信号是否阻塞- 参数:- set:需要操作的信号集- signum:需要判断的那个信号- 返回值:1 : signum被阻塞0 : signum不阻塞-1 : 失败
以上的函数,都是对自定义的信号集进行操作,其使用可以看看如下函数:
#include
#include
{// 创建一个信号集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");}// 添加几个信号到信号集中sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);// 判断SIGINT是否在信号集中ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 判断SIGQUIT是否在信号集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}// 从信号集中删除一个信号sigdelset(&set, SIGQUIT);// 判断SIGQUIT是否在信号集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}return 0;
}
那么如果我们需要实现对内核信号集的修改,这个时候就需要一些系统调用函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)- 参数:- how : 如何对内核阻塞信号集进行处理SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(按位添加)假设内核中默认的阻塞信号集是mask, mask | setSIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞mask &= ~setSIG_SETMASK:覆盖内核中原来的值(直接替换)- set :已经初始化好的用户自定义的信号集- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL- 返回值:成功:0失败:-1设置错误号:EFAULT、EINVALint sigpending(sigset_t *set);- 功能:获取内核中的未决信号集- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
那么对于信号集,到目前为止,我们出现了未决信号集、阻塞信号集、自定义信号集,那么他们之间的关系到底是什么:
阻塞信号集和未决信号集在内核PCB中,因此我们无法操作,但是可以操作自定义信号集,然后将其通过函数映射给阻塞信号集来间接操作,信号集的本质,也就是位图。
我们现在先实现一个对内核的信号2与3进行处理,将其阻塞后使用自己的键盘来产生这些信号,并且把所有的常规信号的未决状态打印到屏幕:
#include
#include
#include
#include
}
这个如果没有使用num来解除阻塞,就需要进行kill -9来杀死。加入num后他会自动结束这个进程。
那么接下来是信号捕捉函数sigaction():
#include
那么改改上面所实现的定时器:
#include
#include
#include
#include
}// 过3秒以后,每隔2秒钟定时一次
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("setitimer");exit(0);}// getchar();while(1);return 0;
}
最好使用sigaction是因为它是基于posix的原则,在其他系统上的兼容性较强。、
SIGCHLD信号产生的条件:
以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号。但是他可以解决僵尸进程的问题,对于向wait这种函数,是阻塞的,还不如用信号来处理子进程的结束后的回收。
#include
#include
#include
#include
#include
#include
}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;
}