linux--多进程开发(7) 信号、相关函数、信号集及其操作和捕获、SIGCHLD解决僵尸进程
信号是什么
是在软件层面上对中断机制的模拟,是异步通信的方式
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件
信号通常是由内核发往终端的,引发内核产生信号的各类事件如下:
- 对于前台进程,用户可通过输入特殊的终端字符例如CTRL+C来给进程发送中断信号
- 对于硬件发生异常,即硬件检测到错误条件并通知内核进而发送信号给进程。例如被除数为0
- 系统状态变化,alarm定时器到期引起SIGALRM信号等
- 运行kill
使用信号的目的:
- 让进程知道已经发生了一个特定的事情
- 强迫进程执行它自己代码中的信号处理程序
信号的特点:
- 简单
- 不能携带大量信息(所以信号时可以通信的,但不适合)
- 满足特定条件才能发送
- 优先级比较高
信号的三种状态:产生、未决、递达
-
信号的
未决
是一种状态,指的是从信号的产生到信号被处理前的这一段时间 -
信号的
阻塞
是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作 (就是说阻塞之后能用了还是会继续用到)
kill -l 查看系统定义的32个信号列表
信号总览
信号的默认处理动作(5种):
- Term:终止进程
- Ign:当前进程忽略掉这个信号
- Core:终止进程,并生成一个core文件(错误信息就可以在这个地方看)
- STop:暂停当前进程
- Cont:继续执行当前进程
SIGKILL和
SIGSTOP` 信号不能被捕捉、阻塞或者忽略,只能执行默认动作
总共62个信号,红色信号熟练掌握并记住其编号:
core文件的作用
当进程异常终止的时候会生成core文件,当然生成core文件需要前置准备,例如通过ulimit -a
查看core file size的大小,如果为0就设置ulimit -c core-size
然后在编译的时候加上-g生成可调试文件,再gdb这个可执行文件,使用core-file core就可以看到其信息:
信号相关函数
kill 很万能,可以实现raise和abort
int kill(pid_t pid, int sig);
- 使用
man 2 kill
查看帮助 - 功能:给任何的进程或者进程组
pid
,发送任何的信号sig
- 参数
pid
> 0
: 将信号发送给指定的进程= 0
: 将信号发送给当前的进程组= -1
: 将信号发送给每一个有权限接收这个信号的进程< -1
: 这个pid=某个进程组的ID取反
sig
: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
- 返回值:0成功,-1失败
raise 给当前进程发送信号
- 使用
man 3 raise
查看帮助 - 参数:
sig
: 要发送的信号 - 返回值:0成功,非0失败
abort 给当前进程发送杀死信号
- 使用
man 3 abort
查看帮助 - 功能: 发送
SIGABRT
信号给当前的进程,**杀死当前进程
alarm函数&setitimer
区别:alarm
只能定一次时,setitimer
可以周期性定时
unsigned int alarm(unsigned int seconds);
- 使用
man 2 alarm
查看帮助 - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:
SIGALARM
- 参数:
seconds
,倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。如果要取消一个定时器,通过alarm(0)
- 返回值: 之前没有定时器,返回0;之前有定时器,返回之前的定时器剩余的时间
SIGALARM
:默认终止当前的进程,每一个进程都有且只有唯一的一个定时器
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时,即**函数不阻塞
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
- 使用
man 2 setitimer
查看帮助 - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数
which
: 定时器以什么时间计时ITIMER_REAL
: 真实时间,时间到达,发送SIGALRM
(常用)ITIMER_VIRTUAL
: 用户时间,时间到达,发送SIGVTALRM
ITIMER_PROF
: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送SIGPROF
new_value
: 设置定时器的属性,传入一个结构体指针old_value
:记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:成功 0,失败 -1 并设置错误号
srtuct itimerval的结构:
struct itimerval { // 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
// 过it_value秒后,每隔it_interval秒定时一次
使用的时候直接初始化:
struct itimerval new_value;
new_value.it_interval.tv_sec = 2 设置每2秒定时一次
new_value.it_interval.tv_usec = 0 设置每2秒+0微妙定时一次
...
信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
- 在 PCB 中有两个非常重要的信号集。一个称之为
阻塞信号集
,另一个称之为未决信号集
。这两个信号集都是内核使用位图机制来实现的(也就是那个0和1的按位与或)。 - 但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改
阻塞信号集与非阻塞信号集模拟流程说明
- 用户通过键盘
Ctrl + C
, 产生2号信号SIGINT
(信号被创建) - 信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
SIGINT
信号状态被存储在第二个标志位上- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
- 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较。在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 阻塞信号集默认不阻塞任何的信号,如果没有阻塞,这个信号就被处理
- ,如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处。如果想要阻塞某些信号需要用户调用系统的API处理,如何处理看下面操作
对于操作自定义信号集函数
用户不能对系统默认的信号集进行直接操作,但是可以访问,或者是用自定义的信号集对它简介操作。类似类里面的隐私成员变量的保护。
总共有5个
sigempty
清空信号集、sigfillset
设置信号集全为1、sigaddset
添加某个为1、sigdelset
设置信号集某个信号为0、sigismember
判断信号集某个信号为1否
* 使用`man 3 sigemptyset`查看帮助
*
* `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 : 失败
对于操作内核信号集函数
sigprocmask让用户可以通过自己设置的自定义信号集对内核信号集进行操作,也只能操作内核的阻塞信号集
* 使用`man 2 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
*
* `int sigpending(sigset_t *set);`
* 使用`man 2 sigpending`查看帮助
* 功能:获取内核中的未决信号集
* 参数:set,传出参数,保存的是内核中的未决信号集中的信息
* 返回值:成功返回0, 失败返回-1
eg:组合使用案例:
// 设置自定义信号集
sigset_t set;
// 清空信号集
sigemptyset(&set);
// 设置2 3号信号阻塞
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
信号捕捉相关函数
signal和sigaction区别
- 参数区别
- 版本区别,
signal
在不同版本Linux中,行为不一致,所以推荐使用sigaction
(ubutun
下两者一致)
`int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);`
* 使用`man 2 sigaction`查看帮助
* 功能:检查或者改变信号的处理,即信号捕捉
* 参数
* `signum` : 需要捕捉的信号的编号或者宏值(信号的名称)
* `act` :捕捉到信号之后的处理动作
* `oldact` : 上一次对信号捕捉相关的设置,一般不使用,设置为NULL
* 返回值:成功返回0, 失败返回-1
其中第二个act就是捕捉之后要处理的动作,是一个结构体,其结构如下:
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);
};
eg:
#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;
}
内核捕获信号的过程
先由用户区执行指令出现中断等进入系统内核,内核就处理异常,然后处理当前进程中可以递送的信号,如果信号的处理是用户自定义的动作(函数)那么就返回到用户模式执行信号处理函数,然后再次进入内核,内核从上次主控制流程中断的地方继续向下执行。
利用捕捉SIGCHLD信号解决僵尸进程问题
有三种情况子进程会发送SIGCHLD给父进程:
- 子进程结束
- 子进程暂停
- 子进程继续运行
主要思想:那么利用子进程结束父进程接收该信号,进而由内核对该信号进行自定义的信号处理如waitpid来释放子进程,从而解决僵尸进程问题。
一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环,因此这里在处理函数alarm里面用了while,并判断返回ret的状态决定是否继续释放还是退出。
可能会出现段错误(不一定能复现)
- 原因:在捕获信号注册前,子进程已经执行完
- 解决办法:在产生子进程之前,提前设置好内核信号阻塞集,将SIGCHLD阻塞住,等子进程创建完毕,再在父进程里面创建信号捕捉,最后清空阻塞集。这样无论子进程运行的再快,也没关系,大不了一次就一次同时处理已经结束的N个子进程而已。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
// 回收子进程PCB的资源
// 因为可能多个子进程同时死了,所以使用while循环
// 不使用wait是因为会造成阻塞,父进程不能继续
// 使用waitpid可以设置非阻塞
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) {
// 父进程
// 使用sigaction捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while (1) {
printf("parent process : %d\n", getpid());
sleep(2);
}
} else {
// 子进程
printf("child process : %d\n", getpid());
}
return 0;
}