Linux系统编程(四)——signal信号处理
Linux中的信号处理以及信号处理函数
目录
0x01 信号
-
信号是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信号不能被捕捉、阻塞或忽略,只能执行默认动作。
0x02 信号相关的函数
一、kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 功能:给任何的进程或进程组pidpid,发送任何信号sig
- 参数:
- sig:需要发送信号的编号或者宏值,0表示不发送任何信号。
- pid:
- >0 将信号发送给指定的进程
- =0 将信号发送给当前的进程组
- =-1 将信号发送给每一个有权限接收这个信号的进程
< -1 这个pid=某个进程组的ID取反(-12345)-》(12345)
kill(getppid(),9);
kill(getpid(),9);
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig:要发送的信号
-返回值
0 成功
非0 失败
kill(getpid(),sig);
void abort(void);
- 功能:发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(),SIGABRT);
那么其使用可以看如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <signal.h>
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;
}
二、alarm()函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-功能:设置定时器(闹钟),函数调用开始倒计时,倒计时为0时,函数会给当前的进程发送信号:SIGALARM
-参数:
seconds:倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
可用于取消一个定时器,通过alarm(0)。
-返回值:
之前没有定时器,返回0
之前有定时器,返回之前的定时器剩余的时间
-SIGALARM:默认终止当前的进程,每一个进程都有且只有一个唯一的定时器。
alarm(10); -> 0
过了一秒
alarm(5);//直接覆盖 倒计时五秒 -> 9
alarm(100)该函数不阻塞。
下面这个代码验证了alarm函数的不阻塞性:
#include <unistd.h>
#include <stdio.h>
int main()
{
int seconds = alarm(5);
printf("seconds = %d\n",seconds); //0
sleep(2);
seconds = alarm(2);
printf("seconds = %d\n",seconds); //3
while(1)
{
}
return 0;
}
三、setitimer()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器,可以替代alarm函数,精度比alarm()高,精度为us,可实现周期性定时。
- 参数:
- which:定时器以什么时间计时
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 <stdio.h>
#include <stdlib.h>
//过三秒中 每个两秒定时一次
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;
}
四、signal()
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:
设置某个信号的捕捉行为
- 参数:
signum:要捕捉的信号(写宏值,在不同的操作系统上不一样)
handler:捕捉到信号要如何处理
- SIG_INGN:忽略信号
- SIG_DFL:信号默认的行为
- 回调函数:这个函数是内核调用的,程序员只负责写。捕捉到信号后如何去处理信号。
回调函数:
需要程序员实现,提前准备号的,函数的类型根据实际需求,看函数指针定义。
不是程序员调用,当信号产生由内核调用。
函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置。
- 返回值:
成功:返回上一次注册的信号处理函数的地址,第一个调用返回NULL
失败:返回SIG_ERR,设置错误号
- 注意:SIGKILL SIGSTOP不能被捕捉,不能被忽略
那么我们就可以实现对上述定时信号的捕捉:
#include <sys/time.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void myalarm(int num)
{
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来替换。具体是为什么,可以先学学信号集这个概念。
0x03 信号集
-
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为
sigset_t
。 -
在PCB中有两个非常重要的信号集,一个称为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的(也就是使用二进制位来进行实现)。但操作系统不允许我们直接对这两个信号集进行操作,而需自定义另一个集合,借助信号集操作函数来对PCB中的这两个信号集做修改。
-
信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
-
信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
-
信号的阻塞就是让系统暂时保留信号留待以后发送,由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
一、信号集的处理过程
-
用户通过键盘Ctrl C,产生一个2号信号SIGINT(信号被创建)
-
信号产生但是没有被处理(处于一个未决状态)
-
在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集)
-
SIGINT信号,状态是被存储在第二个标志位上。(也就是理解为现在有一个64位的寄存器,对寄存器的值进行修改),这个标志位的值为0,表示信号并不是未决状态,当这个标志位的值为1时,说明信号处于未决状态。
-
-
未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集)进行比较。
-
阻塞信号集默认不阻塞任意一个信号,但是我们也可以设置为阻塞。然后不会去处理,所以我们就是操作这个地方。
-
如果想要阻塞某些信号,需要用户调用系统的API。
-
-
在处理的时候和这个阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了。
-
如果没有阻塞,那么这个信号就被处理。
-
如果阻塞了,这个信号继续处于未决状态,直到阻塞解除,这个信号就被处理。
-
二、关于信号集处理的函数
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int 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 是否在信号集 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 | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值(直接替换)
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
成功:0
失败:-1
设置错误号:EFAULT、EINVAL
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
那么对于信号集,到目前为止,我们出现了未决信号集、阻塞信号集、自定义信号集,那么他们之间的关系到底是什么:
阻塞信号集和未决信号集在内核PCB中,因此我们无法操作,但是可以操作自定义信号集,然后将其通过函数映射给阻塞信号集来间接操作,信号集的本质,也就是位图。
我们现在先实现一个对内核的信号2与3进行处理,将其阻塞后使用自己的键盘来产生这些信号,并且把所有的常规信号的未决状态打印到屏幕:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
for(int i = 1; i <= 31; i++) {
if(sigismember(&pendingset, i) == 1) {
printf("1");
}else if(sigismember(&pendingset, i) == 0) {
printf("0");
}else {
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
if(num == 10) {
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
这个如果没有使用num来解除阻塞,就需要进行kill -9来杀死。加入num后他会自动结束这个进程。
那么接下来是信号捕捉函数sigaction():
#include <signal.h>
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_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
// 被废弃掉了
void (*sa_restorer)(void);
};
那么改改上面所实现的定时器:
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过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的原则,在其他系统上的兼容性较强。、
0x04 内核实现信号捕捉的过程
0x05 SIGCHLD信号
SIGCHLD信号产生的条件:
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接收到SIGCONT后唤醒时
以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号。但是他可以解决僵尸进程的问题,对于向wait这种函数,是阻塞的,还不如用信号来处理子进程的结束后的回收。
#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); //未决信号集只能接收一个 其他为阻塞 所以用wait不可以处理所有子进程,所以需要写一个死循环
//但是这样你的父进程无法处理其他的事情
// }
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;
}
更多推荐
所有评论(0)