简 述: 在上一篇中,讲解了 Linux 的系统中基本信号概念入门。这里就说一下两个重要的概念,系统内核里面的 未决信号集 和 阻塞信号集 的状态关系,以及处于用户区域的 自定义的信号集 ,如何处理这三者之间的关系。和 Linux 中的信号捕捉 ,以及捕捉函数相关函数相关的使用。
- 自定义信号集:
- sigaddset() //将指定信号置为 1,添加到自定义集中
- sigdelset() //将指定信号置为 0,添加到自定义集中
- sigemptyset() //将所有信号置为 0,清空
- sigfillset() //将所有信号置为 1,填充
- sigismember() //判断指定信号是否存在,是否为 1
- 系统信号集:
- sigprocmask() //将自定义信号集设置给阻塞信号集。
- sigprocmask() //读取当前信号的未决信号集。参数为输出参数,内核将未决信号集写入 set
- 信号捕捉:
- signal() //实现信号捕捉的功能;最简单使用一个函数。
- sigaction() //同上,多一个额外功能,运行期间能够临时 屏蔽指定信号
[TOC]
本文初发于 “偕臧的小站“,同步转载于此。
编程环境:
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
未决信号集:
- 概念: 没有被当前进程处理的信号
阻塞信号集:
- 概念: 将某个信号放到阻塞信号集中,这个信号就不会被进程处理;当阻塞解除后,信号就会被处理。
绘画一个草图理解一下这几则之间的关系,当一个程序跑起来之后,会生成一个进程,有自己虚拟地址控件,然后在内核区域的 PCB 中,有着未决信号集合阻塞信号集两个,被系统内核所控制,包括其和自定义信号集的类型都是一样的。左侧的数值是系统信号 1-64(或更多)对应的,然后格子里面的是该 n 号信号对应的值,值为 0 或 1;当发送一个 2 号信号在未决信号集里面时候,系统就会去阻塞信号集里面照片 2 号信号的值,若是为 1,表示阻塞,不作处理,让其一直待在未决信号集中;若是为 0,则将 2 号信号在未决信号集中的值修改为 1,且对出对应的处理。
若是要修改阻塞信号集合里面的值,只能够先赋值号自顶一个信号集,然后再讲自定义的这个值设置到系统的阻塞信号中。
自定义信号集:
查看 man 文档,里面常见的 自定义信号集 的接口函数为如下:
int sigaddset(sigset_t *set, int signo); //将指定信号置为 1,添加到自定义集中
int sigdelset(sigset_t *set, int signo); //将指定信号置为 0,添加到自定义集中
int sigemptyset(sigset_t *set); //将所有信号置为 0,清空
int sigfillset(sigset_t *set); //将所有信号置为 1,填充
int sigismember(const sigset_t *set, int signo); //判断指定信号是否存在,是否为 1
sigprocmask() 函数:
- 作用: 将自定义信号集设置给阻塞信号集。
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
- 参数:
- how: 假设当前屏蔽的信号屏蔽字符为 mask;
SIG_BLOCK
: 相当于 mask = mask | set (set 为需要屏蔽的信号集)SIG_UNBLOCK
: 相当于 mask = mask & ~set (set 为需要解除屏蔽的信号集)SIG_SETMASK
: 相当于 mask = set (set 为用于替代原始屏蔽集的新屏蔽集)
- how: 假设当前屏蔽的信号屏蔽字符为 mask;
sigpending() 函数:
- 作用: 读取当前信号的未决信号集。参数为输出参数,内核将未决信号集写入 set
int sigpending(sigset_t *set);
写一个小的例子:
编写一个小的例子来使用一下,设置阻塞信号集,并把所有常规的信号的未决状态打印到终端。
代码实现:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/time.h> #include <signal.h> int main(int argc, char *argv[]) { while (true) { sigset_t pendest; sigpending(&pendest); for (int i = 0; i < 64; i++) { //每隔一秒,对系统信号阻塞集的信号做一次校验 if (sigismember(&pendest, i)) printf("1"); else printf("0"); } printf("\n"); sleep(1); } return 0; }
运行效果:
这里的 1-64 号信号,在系统的阻塞信号集中,都是 0,说明该进程没有设置信号阻塞。
信号捕捉:
在 Linux 中, 系统会释放信号,然后内核又会根据这个信号对对应的进程做相应的动作。然后对于信号捕捉常用有如下函数的使用。
其中分为简单的 signal()
函数,和 sigaction()
函数,这两个都能够捕捉指定的信号,也比较常使用
signal() 函数:
实现信号捕捉的功能;最简单一个函数。
typedef void (*sig_t) (int);
sig_t signal(int sig, sig_t func);
这个函数比较特别,当然,也可以简写为
void (* signal(int sig, void (*func)(int));)(int);
这种复杂形式的。- 第一行是一个函数指针,作为后面的函数的回调函数使用。
- 第二行是真正的信号捕捉函数调用
代码例子:
写一个例子,捕捉信号
SIGINT
函数,键盘按下ctrl + c
会发射此信号,然后保证此进程没有终止或者死亡的情况下,捕捉这个函数,做出自定义的行为,具体的在 func() 函数里面实现。#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/time.h> #include <signal.h> void func(int no); //用作回调函数,给 signal() 调用 int main(int argc, char *argv[]) { signal(SIGINT, func); //设置信号捕捉函数, 捕捉 ctrl + c while (true) { printf("Keep the thread running for the non-death state.\n"); sleep(1); } return 0; } void func(int no) { printf("捕捉的信号为: %d\n", no); }
运行效果:
Unix 中的 sigaction() 函数:
捕捉信号的函数,比上面使用略微复杂点,但是有一个额外的功能,可以在程序运行期间,临时屏蔽指定的信号。
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
/* union for signal handlers */
union __sigaction_u {
void (*__sa_handler)(int);
void (*__sa_sigaction)(int, struct __siginfo *,
void *);
};
/* Signal vector template for Kernel user boundary */
struct __sigaction {
union __sigaction_u __sigaction_u; /* signal handler */
void (*sa_tramp)(void *, int, int, siginfo_t *, void *); //已被废弃的参数,不填
sigset_t sa_mask; /* signal mask to apply */
int sa_flags; /* see signal options below */ //在信号处理函数执行中,是 "临时" 屏蔽指定信号
};
/* if SA_SIGINFO is set, sa_sigaction is to be used instead of sa_handler. */
#define sa_handler __sigaction_u.__sa_handler
#define sa_sigaction __sigaction_u.__sa_sigaction
//--------------------------------------------------------------------
//__sigaction() 函数也可以 改为使用 sigaction() 函数
/*
* Signal vector "template" used in sigaction call.
*/
struct sigaction {
union __sigaction_u __sigaction_u; /* signal handler */
sigset_t sa_mask; /* signal mask to apply */
int sa_flags; /* see signal options below */
};
- 参数:
- sig: 捕捉的信号
- act: 新的,将要执行的自定义动作的设定操作
- oact: 被设置之前的旧的设定的操作吗,一般不需要,传入 NULL
restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int *restrict ptr, ptr 指向的内存单元只能被 ptr 访问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针(野指针)。这个关键字只能在C99标准的C程序里使用,C++程序不支持,restrict的起源最早可以追溯到Fortran。
写一个例子:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/time.h> #include <signal.h> void func(int no); //用作回调函数 int main(int argc, char *argv[]) { __sigaction_u sigactu; sigactu.__sa_handler = func; //另一个变量 sigactu.__sa_sigaction 不用赋值 struct sigaction act; act.sa_flags = 0; //通常给 0 act.__sigaction_u = sigactu; sigemptyset(&act.sa_mask); //清空 自定义信号集 sigaddset(&act.sa_mask, SIGQUIT); //向指定 系统阻塞集 中写入 自定义的信号集,添加需要屏蔽的信号(ctrl + 反斜杠 触发) sigaction(SIGINT, &act, NULL); while (true) { printf("Keep the thread running for the non-death state.\n"); sleep(1); }; } void func(int no) { printf("捕捉的信号为: %d\n", no); sleep(4); printf("醒了\n"); }
分析运行:
这里故意运行两次:
第一次: 先按下
ctrl + c
发射 2 号信号SIGINT
, 等待其收到信号后,执行 3s 后 ,将 func() 内容跑完,再次按下ctrl + \
发射其他信号SIGINT
,系统收到后,程序立即死亡(此时该SIGINT
已经过了临死屏蔽的状态)第二次: 先按下
ctrl + c
发射 2 号信号SIGINT
, 等待其收到信号后,执行不到 3s ,func() 内容还没跑完,立即按下ctrl + \
发射其他信号SIGINT
,系统收到后,程序没有立即死亡(此时该SIGINT
正处于临死屏蔽的状态), 而是过了几秒钟在死亡。运行效果:
Linux 中的 sigaction() 函数:
Linux 中的该函数,和 Unix 有点不一致,但主要填写的四个参数是一样的,这是不变的;
这里贴出 Linux 下的函数原型,可以和 Unix 下的 sigaction()
比较一下:
下载地址:
欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。