Linux进程信号详解(二):信号产生

当前阶段:

一、通过终端按键产生信号

1.1 基本操作

  • Ctrl+C → SIGINT
  • Ctrl+\ → SIGQUIT 可以发送终止信号
  • Ctrl + Z - > SIGSTP 可以发送停止信号,将当前前台进程挂起到后台

设置所有信号都可以自定义捕捉 :

1.2 理解OS如何得知键盘有数据

1.3 初步理解信号起源

  • 信号其实是从纯软件角度,模拟硬件中断的行为
  • 只不过硬件中断是发给CPU,而信号是发送给进程
  • 两者都有相似性,但是层级不同

二、调用系统命令向进程发信号

kill命令可以给指定进程发送信号。

复制代码
# 方式1:通过信号宏定义发送
kill -信号名 进程PID
# 方式2:通过信号编号发送(推荐,更通用)
kill -信号编号 进程PID
# 示例:向PID为213784的进程发送SIGSEGV(11号)信号
kill -SIGSEGV 213784
kill -11 213784

示例:先运行一个后台死循环进程

复制代码
// deadloop.cc
#include <iostream>
#include <unistd.h>

int main()
{
    while(true) {
        sleep(1);
    }
}

g++ deadloop.cc -o deadloop
./deadloop &          # 后台运行
ps aux | grep deadloop  # 查看PID,假设是213784
kill -SIGSEGV 213784   # 发送段错误信号

进程会收到SIGSEGV(11号信号),默认动作是终止并产生core dump。

三、使用函数产生信号

3.1 kill() 函数

kill命令的底层实现,可向任意进程(需有对应权限)发送任意信号。

复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  • 作用:给指定进程发送信号。

  • 返回值:成功返回0,失败返回-1。

Makefile

复制代码
.PHONY:all
all:testSig mykill
testSig:testSig.cc
	g++ -o $@ $^ -std=c++11
mykill:mykill.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f testSig mykill

mykill.cc

复制代码
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>

// ./mykill signumber pid
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "./mykill signumber pid" << std::endl;
        return 1;
    }
    int signum = std::stoi(argv[1]);
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signum);
    if (n == 0)
    {
        std::cout << "send " << signum << " to " << target << " success." << std::endl;
    }
    return 0;
}

testSig.cc

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
    signal(SIGINT,handlerSig);
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world, " << cnt++  << ", pid: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

3.2 raise() 函数

自己给自己发信号,底层是调用kill(getpid(), sig)实现。

复制代码
#include <signal.h>
int raise(int sig);
  • 作用:给当前进程自己发送信号。

  • 返回值:成功0,失败非0。

9 号信号无法被自定义捕捉

把9号信号跳过去 , 查看是否还有其他信号无法被自定义捕捉

把9号和 19号 信号跳过去 , 查看是否还有其他信号无法被自定义捕捉

没有了 , 就只有 9号和 19 号无法自定义捕捉

3.3 abort() 函数

强制让进程异常终止,无论是否捕捉 SIGABRT 信号,进程最终都会终止(捕捉后会先执行自定义逻辑,再终止)。

复制代码
#include <stdlib.h>
void abort(void);

关键特性:无返回值,总是会成功,进程最终一定会异常终止。

四、硬件异常产生信号

4.1 除零异常 → SIGFPE

复制代码
// divzero.cc
#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    // signal(SIGFPE, handler);   // 取消注释可捕捉
    int a = 10;
    a /= 0;      // 除零错误
    while(1);
    return 0;
}

默认情况下,进程会收到SIGFPE(8号信号)并终止。如果捕捉了该信号,会一直打印"catch a sig : 8" ------ 为什么一直打印?因为CPU的状态寄存器中除零标志位没有被清除,每次从异常处理返回后,再次执行除零指令又会触发异常,形成循环。

4.2 野指针 → SIGSEGV

复制代码
// segv.cc
#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    // signal(SIGSEGV, handler);
    int *p = NULL;
    *p = 100;     // 向NULL地址写入,非法内存访问
    while(1);
    return 0;
}

默认产生段错误(SIGSEGV,11号信号)。捕捉后也会一直打印,因为MMU异常状态未清除。

核心结论 :C/C++中的除零、野指针等异常,在系统层面都是通过信号来通知进程的。

4.3 硬件异常与信号的对应关系

异常类型 典型代码 产生的信号 信号值 默认行为
除零错误 a /= 0; SIGFPE 8 终止 + Core
野指针/段错误 *p = 100; // p=nullptr SIGSEGV 11 终止 + Core
非法指令 执行损坏的二进制代码 SIGILL 4 终止 + Core
总线错误 内存对齐错误 SIGBUS 7 终止 + Core
浮点异常 浮点溢出/下溢 SIGFPE 8 终止 + Core
断点调试 int 3 指令 SIGTRAP 5 终止 + Core

Core****表示会产生 core dump 文件,用于调试分析。

4.4 核心问题:OS 怎么知道硬件异常了?

前置知识:信号全部都是由操作系统发送的 。

硬件层面的检测机制

关键组件:CPU 寄存器与状态

组件 作用
EFLAGS 标志寄存器 记录运算结果状态(溢出、零标志、符号标志等)
CR0 控制寄存器 控制 CPU 操作模式,包含异常使能位
CR2 寄存器 页故障时保存导致故障的线性地址
CR3 寄存器 保存页目录表物理地址(MMU 使用)
IDT 中断描述符表 存储异常处理程序的入口地址

4.5 操作系统如何识别"谁"犯了错?

当前进程上下文

五、由软件条件产生信号

当 Linux 内核检测到特定软件条件满足 时,会自动向相关进程发送信号,属于内核主动触发 。常见的软件条件有:管道破裂(SIGPIPE)定时器超时(SIGALRM) 等,主要介绍alarm 函数 + SIGALRM 信号

5.1 基本alarm验证 - 体会IO效率问题

复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 作用:告诉内核在seconds秒后给当前进程发送SIGALRM信号(默认终止进程)。

  • 返回值:0或以前设置的闹钟剩余秒数。

  • 如果seconds=0,表示取消以前的闹钟。

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号" << sig << std::endl;
    exit(13);
}

int main()
{
    for(int i = 1 ; i< 32 ; i++)
        signal(i,handlerSig);

    alarm(1); //设定1s闹钟,1s 以后,当前进程会收到一个信号

    int cnt = 0;
    while(true)
    {
        std::cout << "count: " << cnt++ << std::endl;
    }


    return 0;
}

我们把代码进行修改一下:

5.2 pause() 系统调用详解

复制代码
#include <unistd.h>

int pause(void);
特性 说明
功能 使进程进入可中断睡眠状态,直到收到任何信号
返回值 总是返回 -1,errno 设置为 EINTR(被信号中断)
特点 进程完全阻塞,不消耗 CPU 资源
使用场景 等待信号驱动的事件循环

为什么用 pause() 而不是空循环?

复制代码
// ❌ 错误:忙等待,浪费 CPU
while (true) {
    // 空转,CPU 占用 100%
}

// ✅ 正确:阻塞等待,不消耗 CPU
while (true) {
    pause();  // 进程挂起,收到信号才唤醒
}

5.3 设置重复闹钟

问题alarm() 是一次性的,如何每隔1秒执行一次任务?

解决方案:在信号处理函数中重新设置闹钟

复制代码
#include <unistd.h>
#include <signal.h>
#include <iostream>

void handlerSig(int sig) {
    std::cout << "获得了一个信号: " << sig << std::endl;
    
    // 关键:重新设置闹钟,实现周期性
    alarm(1);  // 再设1秒后的闹钟
}

int main() {
    signal(SIGALRM, handlerSig);
    alarm(1);  // 首次设置
    
    while (true) {
        pause();  // 每次信号到来后重新暂停
    }
    
    return 0;
}
复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>

struct task_struct
{
    pid_t id;
    int count; //时间片
};

std::vector<task_struct> task_list; //遍历一下,拿到时间片最少的来运行~


////////////func/////////////////
void Sched()
{
    std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{
    std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
    std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}

////////////////////////////////

using func_t = std::function<void()>;
std::vector<func_t> funcs;

//////每隔一秒完成一些任务///////
void handlerSig(int sig)
{
    std::cout << "#################################" << std::endl;
    for(auto f : funcs)
        f();
    std::cout << "#################################" << std::endl;
    alarm(1);
}

int main()
{
    funcs.push_back(Sched);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);
    signal(SIGALRM, handlerSig);
    alarm(1); //1s,0.00000000000001s -> os会以高频的频率处理我们的工作

    while (true)//这就是操作系统!也是牛马!
    {
        pause(); // 循环式的pause
    }
    return 0;
}

5.4 内核中的闹钟管理机制

5.4.1 OS 如何管理大量闹钟?

5.4.2 闹钟的数据结构

复制代码
// Linux 内核中的定时器结构(简化版)
struct timer_list {
    struct list_head entry;       // 链表节点,用于组织
    unsigned long expires;        // 过期时间(绝对时间,jiffies)
    
    void (*function)(unsigned long);  // 到期回调函数
    unsigned long data;           // 回调函数参数
    
    struct tvec_t_base_s *base;   // 所属定时器基座
};
字段 含义
expires 过期时间点(以 jiffies 为单位)
function 到期时执行的函数(发送 SIGALRM)
data 传递给回调函数的参数(通常是进程信息)

5.4.3 组织方式:最小堆(Min-Heap)

  • 父节点过期时间 < 子节点过期时间
  • 根节点永远是最近要到期的闹钟
  • 插入、删除、查看最小值 :O(log n)

5.3.4 内核定时器管理流程

要点 说明
alarm() 是一次性的 响过后自动失效,需要手动重新设置
pause() 是高效的 进程阻塞不消耗 CPU,等待信号唤醒
周期性定时 = 信号处理中重新 alarm() 递归设置实现循环触发
内核用最小堆管理闹钟 高效找到最近到期的定时器
时钟中断是 OS 的心跳 驱动整个系统的定时机制
信号本质是异步通知 软件条件通过内核数据结构触发
相关推荐
chxii2 小时前
Nginx性能优化-压缩(返回头报文介绍)
运维·nginx·性能优化
Bert.Cai2 小时前
Linux cd命令详解
linux·运维
n 55!w !1082 小时前
IP-vlan实验报告
服务器·网络·tcp/ip
扑火的小飞蛾3 小时前
Kali Linux 安装 OpenClaw 详细教程
linux·运维·服务器
PrDf22Iw83 小时前
CPU ↔ DRAM(内存总线)的可持续数据传输带宽
java·运维·网络
王琦03183 小时前
第二次作业
linux·运维·服务器
Bert.Cai3 小时前
Linux mkdir命令详解
linux·运维
超绝振刀怪3 小时前
【Linux进程状态:僵尸进程、孤儿进程和调度基础】
linux·僵尸进程·孤儿进程·进程状态
chenglin0164 小时前
AI服务的可观测性与运维
运维·人工智能