信号
Linux中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,信号在系统中的优先级是非常高的。
SIGKILL
:9号信号,无条件终止进程,不能被捕捉、阻塞和忽略。SIGSTOP
:19号信号,无条件暂停进程,不能被捕捉、阻塞和忽略。
信号相关函数
发送指定的信号到指定的进程。
#include <signal.h>
int kill(
pid_t pid, // 进程ID
int sig // 要发送的信号
);
给当前进程发送指定的信号。
#include <signal.h>
int raise(
int sig // 要发送的信号
);
给当前进程发送SIGABRT
信号。
#include <stdlib.h>
void abort(void);
给当前进程发送定时发送SIGALRM
信号。
#include <unistd.h>
unsigned int alarm(
unsigned int seconds // 倒计时seconds秒
);
// 返回值:大于0表示倒计时还剩多少秒,返回值为0表示倒计时完成,信号被发出
信号集
在进程控制块PCB中有两个非常重要的信号集:阻塞信号集、未决信号集。阻塞信号集和未决信号集在内核中的结构是相同的,封装成一个整形数组, 一共128个字节,1024个标志位,其中前31个标志位,对应Linux中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。
修改阻塞信号集
操作系统不允许我们直接对这两个信号集进行任何操作,而是需要自定义另外一个集合,借助信号集操作函数对信号集进行修改。关于阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。
#include <signal.h>
int sigprocmask(
int how,
const sigset_t *set, // 阻塞信号集与未决信号集, sigset_t = int[32], 128B = 1024b
sigset_t *oldset // 传出参数,将之前设置的阻塞信号集传出
);
// 返回值:函数调用成功返回0,调用失败返回-1
how:
SIG_BLOCK
:将参数 set 集合中的信号在阻塞信号集中阻塞SIG_UNBLOCK
:将参数 set 集合中的信号在阻塞信号集中解除阻塞SIG_SETMASK
:使用参数 set 集合中的数据覆盖内核的阻塞信号集数据
#include <signal.h>
// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位是0还是1
int sigismember(const sigset_t *set, int signum);
读未决信号集
未决信号集的写操作由内核完成,如果设置了某个信号阻塞,当这个信号产生之后,内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞,未决信号集中的信号随之被处理,内核再次修改未决信号集将该信号的状态修改为递达状态。
#include <signal.h>
int sigpending(
sigset_t *set // 传出参数, 传出未决信号集
);
示例
- 2号信号
SIGINT
:Ctrl+C - 3号信号
SIGQUIT
:Ctrl+\ - 9号信号
SIGKILL
:通过shell命令给进程发送信号kill -9 PID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
int main()
{
// kill -9 PID 得到该进程的ID
printf("此进程的PID=%d\n", getpid());
// 1. 初始化信号集
sigset_t myset;
sigemptyset(&myset);
// 设置阻塞的信号
sigaddset(&myset, SIGINT); // 2
sigaddset(&myset, SIGQUIT); // 3
sigaddset(&myset, SIGKILL); // 9 *经测试不能被阻塞*
// 2. 将初始化的信号集中的数据设置给内核
// 原来的阻塞信号集
sigset_t old;
sigprocmask(SIG_BLOCK, &myset, &old);
// 3. 进程保持运行
int i = 0;
while (1)
{
// 4. 读内核的未决信号集
sigset_t curset;
sigpending(&curset);
// 遍历这个信号集
for (int i = 1; i < 32; ++i)
{
// 判断某个信号在集合中对应的标志位是0还是1
// 下标从1开始
int ret = sigismember(&curset, i); // 未决信号集1 = 阻塞信号集1 + 信号发生
printf("%d", ret);
}
printf("\n");
sleep(2);
i++;
// 重新设置阻塞信号集,解除阻塞,防止程序退不出来
if (i == 20)
{
// sigprocmask(SIG_UNBLOCK, &myset, NULL);
sigprocmask(SIG_SETMASK, &old, NULL);
}
}
return 0;
}
信号捕获
Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略某些信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中的信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后的处理动作,当进程中对应的信号产生了,这个处理动作也就被调用了。
#include <signal.h>
sighandler_t signal(
int signum, // 需要捕捉的信号
sighandler_t handler // 信号捕捉到之后的处理动作 typedef void (*sighandler_t)(int);
);
struct sigaction
{
void (*sa_handler)(int); // 函数指针,指向信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 函数指针,指向信号处理函数
sigset_t sa_mask; // 在信号处理函数执行期间,临时屏蔽的信号。执行完毕后屏蔽解除。
int sa_flags;
void (*sa_restorer)(void); // 被废弃的成员
}
sa_flags
- 0:使用第一个函数指针
SA_RESTART
:使被信号打断的系统调用自动重新发起SA_SIGINFO
:使用第二个函数指针
#include <signal.h>
int sigaction(
int signum, // 要捕捉的信号
const struct sigaction *act, // 信号的处理动作
struct sigaction *oldact // 上一次信号处理动作,一般指定为NULL
);
// 返回值:函数调用成功返回0,失败返回-1
SIGCHLD信号
当子进程退出、暂停、从暂停恢复运行的时,子进程发送SIGCHLD
信号给父进程,但是父进程收到后默认忽略。我们可以在父进程中基于SIGCHLD
信号来回收子进程,因此需要在父进程中捕捉子进程发送过来的这个信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>
// 回收子进程处理函数
void recycle(int num)
{
printf("捕捉到的信号是: %d\n", num);
// 假设多个子进程同时退出,父进程同时收到了多个SIGCHLD信号,最终处理的时候只处理一次,因此出现僵尸进程
// 解决方案: 循环回收即可
while (1)
{
// 如果是阻塞回收就回不到父进程了
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid > 0)
{
printf("child died, pid = %d\n\n", pid);
}
else if (pid == 0)
{
// 没有死亡的子进程, 直接退出当前循环
break;
}
else if (pid == -1)
{
printf("所有子进程都回收完毕。\n");
break;
}
}
}
int main()
{
// 创建10个子进程
pid_t pid;
for (int i = 0; i < 10; ++i)
{
pid = fork();
if (pid == 0)
{
break;
}
}
// 子进程
if (pid == 0)
{
sleep(2); // 防止子进程退出太快
printf("我是子进程, pid = %d\n", getpid());
}
// 父进程
else if (pid > 0)
{
printf("我是父进程, pid = %d\n", getpid());
// 捕捉SIGCHDL信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = recycle;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 信号产生后, 当前进程优先处理信号, 之前的处理动作会暂停
// 信号处理完毕之后, 回到原来的暂停的位置继续运行
while (1)
{
sleep(20);
}
}
return 0;
}
参考:https://subingwen.cn/linux/signal/