Linux进程间通信(2)-信号

164 阅读7分钟

信号

  1. 软件层级上对中断机制的一种模拟,实现异步
  2. 信号四要素:man 7 signal
  • 编号:不同发行版本可能信号不同
  • 名称:在指定信号中推荐使用
  • 事件:定义发生信号的动作,如 SIGSEGV 由非法内存访问触发
  • 默认处理动作
    • Term
    • Ign:ignore,SIGCHLD就是默认忽略
    • Core:终止进程,并产生core文件
    • Stop:挂起进程,如SIGSTOP
    • Cont:continue,如SIGCONT,继续执行挂起进程
  1. 特别:只能执行默认动作
  • SIGKILL:kill -9 进程号产生并发送,强制停止进程
  • SIGSTOP:挂起进程,CTRL+z能发送给shell前台应用
  1. 进程不会去主动检测信号

  2. SIGKILL和SIGSTOP直接在内核态终止/挂起进程

  3. 其他信号处理都是从内核态转向用户态的时候自动进行,所以进程如果一直在用户态运行,是感知不到信号到来

  4. 用户态转内核态:

  • 系统调用

  • 事件片用完

  • 硬件中断:如外设完成I/O操作

  • 异常:如缺页异常或除0错误

  1. SIGKILL是用来强制停止进程的,但是无法处理僵尸进程,因为僵尸进程已经“死”了,只是父进程还存在,僵尸进程的PCB留着等待父进程读取状态
  1. 生命周期

PCB中存在变量专门管理信号,生命周期本质上就是内核操纵这些变量(未决位图pending、阻塞位图block、处理函数指针列数组handler,信号队列(实时信号才会重复出现))

  • 此处位图就是使用变量的位来表示信号,比如0位则是SIGHUP,若位为1则代表有此信号
  • 实时信号(34-64)使用队列存储,避免信号丢失,而普通信号(1-31)仅记录最后一次触发
  • 产生
    • 硬件:如CTRL+C
    • 软件:用户自定义或出现非法指令之类的
  • 未决
    • 信号产生到处理之前
    • 信号产生时,内核通过目标进程的 PID 找到其 PCB,修改 pending 位图
    • 如果被阻塞,则会一直处于未决状态
    • SIGKILL 和 SIGSTOP 不能被阻塞或忽略,内核会直接处理
  • 递达
    • 执行处理信号动作,默认或自定义
    • 从内核态到用户态时,先检测是否有信号,若有,查不在阻塞位图的未决信号,然后调用处理函数 image.png
    • 处理函数若是默认动作,则直接在内核态完成,若是用户自定义,则转到用户态执行,然后转回内核态,再跳转回进程代码 image.png

信号产生(软件)

使用API产生信号,并将信号注册到指定进程中,注册见信号未决部分

1.Kill

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

功能:向指定进程(或进程组)发送信号。
参数

  • pid > 0:发送给特定进程。
  • pid = 0:发送给当前进程组所有进程。
  • pid = -1:发送给所有有权限的进程。

权限检查:非root用户无法向其他用户的进程发送信号(除非进程属于同一组或权限放宽)。
进程组广播kill(0, SIGTERM)会向当前进程组所有成员发送SIGTERM
内核流程:调用 sys_kill() → kill_something_info() → 更新目标进程的 pending 位图

2.raise

#include <signal.h>
int raise(int sig);

功能:向当前进程发送信号(等价于 kill(getpid(), sig)

3.abort

#include <stdlib.h>
void abort(void);

功能:强制终止当前进程,产生 SIGABRT 信号并生成 core 文件

4.alarm闹钟

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

功能:设置单次定时器,到期发送 SIGALRM, 默认动作Term,返回剩余的时间
注意:每个进程只会有一个计时器,再次调用覆盖之前的计时器,alarm(0)取消当前计时,无论进程状态,都会一直计时

5.setitimer定时器

6.sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

功能:发送带附加数据的实时信号(34-64)
参数

  • value:可携带整型或指针数据(通过 siginfo_t 结构传递) 

内核流程:将信号加入目标进程的 sigqueue 队列,支持信号排队

信号未决

注册

注册就是在传递,“快递员”是内核,本质上是在操作未决位图和sigqueue,注意未决位图用户不能写,只能读取

  1. 注册过程(非可靠信号、普通信号、1-31号):

  • 在未决位图中对应比特位置1

  • 若sigqueue中没有此信号节点,则添加(保证只能有一个)

  1. 注册过程(可靠信号、实时信号、31之后的信号)

  • 在未决位图中对应比特位置1
  • 直接将此信号节点加入sigqueue(信号可以叠加)

阻塞

阻塞位图并不会干扰信号注册,但是会阻塞(延迟)信号递达 阻塞位图用户可操作

位图相关函数

给未决位图和阻塞位图提供操作,sigset_t则代表位图

1. 位图(信号集)操作

下面函数就是对set进行操作(除最后一个)

#include<signal.h>

int sigemptyset(sigset_t *set);   //set集合置空
int sigfillset(sigset_t *set);    //所有信号加入set集合
int sigaddset(sigset_t *set,int signo); //信号加入set集合
int sigdelset(sigset_t *set,int signo); //set集合移除信号
int sigismember(const sigset_t *set, int signo);//判断信号是否存在

sigprocmask阻塞位图操作

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  1. how,该做什么样的操作
  • SIG_BLOCK:向阻塞位图添加set信号集,block(new) = block(old) | set
  • SIG_UNBLOCK:向阻塞位图删除set信号集,block(new)= block(old) & (~set)
  • SIG_SETMASK:用set信号集替换阻塞位图,block(new)= set
  1. set:用来设置阻塞位图
  • 对set的操作要使用上面的操作
  1. oldset:原来的阻塞位图

sigpending未决位图读取

int sigpending(sigset_t *set);
  • 读取未决位图
  • 参数set就是用来接受输出的

信号递达

此处就是开始处理信号,调用信号处理函数
信号捕获:如果处理函数是用户自定义函数,在递达时就调用这个函数,执行此函数期间,默认阻塞该信号

用户自定义处理函数

signal

typedef void (*sighandler_t)(int);  
sighandler_t signal(int signum, sighandler_t handler);  

功能 :注册信号处理函数
参数

  1. signum:更改的信号值
  2. handler:函数指针,要更改的动作是什么
  • SIG_IGN:忽略该型号
  • SIG_DFL:执行系统默认
  • 自定义函数名
    注意:不推荐,不同Linux版本有不同的行为

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

功能:更灵活的信号处理函数注册,支持实时信号和数据传递。

参数

  1. signum:信号编号。
  2. act:新处理方式(struct sigaction)。
  3. oldact:旧处理方式(可置 NULL)。

结构体 sigaction

C
struct sigaction {
    void (*sa_handler)(int);        // 处理函数
    sigset_t sa_mask;                // 处理函数执行期间阻塞的信号位图,临时屏蔽一些信号
    int sa_flags;                    // 标志位
    //保存的值0,在处理信号的时候,调用sa_handler保存的函数
    //SA_SIGINFO,OS在处理信号的时候,调用的就是sa_sigaction函数指针当中 
    void (*sa_sigaction)(int, siginfo_t *, void *); // 带数据的处理函数
};

特殊信号汇总

  1. SIGKILL/SIGSTOP只能使用默认动作,不可捕获、忽略
  2. SIGCHLD:并不是只有终止时才产生

只要状态改变就产生该信号

  • 子进程终止时
  • 子进程收到SIGSTOP信号停止时
  • 子进程处于停止态,收到SIGCONT后唤醒时
  1. 僵尸进程处理:

若父进程没有显式调用wait/waitpid且没有显式设置处理函数,则会导致僵尸进程

  • 若父进程通过 signal(SIGCHLD, SIG_IGN) 显式忽略该信号,内核会直接回收子进程资源,避免僵尸进程
  • 自定义处理函数:
void handler(int sig) { 
    while (waitpid(-1, NULL, WNOHANG) > 0); // 循环回收所有终止的子进程 
} 
signal(SIGCHLD, handler);

一篇文章彻底搞定信号