【Linux】进程信号(一):信号的快速认识与五种产生方式

文章目录

    • Linux进程信号(一):信号的快速认识与五种产生方式
    • 一、信号快速认识
      • [1.1 生活角度的信号](#1.1 生活角度的信号)
      • [1.2 技术应用角度的信号](#1.2 技术应用角度的信号)
        • [1.2.1 一个样例](#1.2.1 一个样例)
        • [1.2.2 signal系统函数](#1.2.2 signal系统函数)
      • [1.3 信号概念](#1.3 信号概念)
        • [1.3.1 查看信号](#1.3.1 查看信号)
        • [1.3.2 信号处理](#1.3.2 信号处理)
    • 二、产生信号
      • [2.1 通过终端按键产生信号](#2.1 通过终端按键产生信号)
        • [2.1.1 基本操作](#2.1.1 基本操作)
        • [2.1.2 理解OS如何得知键盘有数据](#2.1.2 理解OS如何得知键盘有数据)
        • [2.1.3 初步理解信号起源](#2.1.3 初步理解信号起源)
      • [2.2 调用系统命令向进程发信号](#2.2 调用系统命令向进程发信号)
      • [2.3 使用函数产生信号](#2.3 使用函数产生信号)
        • [2.3.1 kill函数](#2.3.1 kill函数)
        • [2.3.2 raise函数](#2.3.2 raise函数)
        • [2.3.3 abort函数](#2.3.3 abort函数)
      • [2.4 由软件条件产生信号](#2.4 由软件条件产生信号)
        • [2.4.1 基本alarm验证 - 体会IO效率问题](#2.4.1 基本alarm验证 - 体会IO效率问题)
        • [2.4.2 设置重复闹钟](#2.4.2 设置重复闹钟)
        • [2.4.3 如何理解软件条件](#2.4.3 如何理解软件条件)
        • [2.4.4 如何简单快速理解系统闹钟](#2.4.4 如何简单快速理解系统闹钟)
      • [2.5 硬件异常产生信号](#2.5 硬件异常产生信号)
        • [2.5.1 模拟除0](#2.5.1 模拟除0)
        • [2.5.2 模拟野指针](#2.5.2 模拟野指针)
        • [2.5.3 子进程退出core dump](#2.5.3 子进程退出core dump)
        • [2.5.4 Core Dump](#2.5.4 Core Dump)
      • [2.6 总结思考](#2.6 总结思考)
    • 三、本篇总结
    • 附录:常用信号速查表

Linux进程信号(一):信号的快速认识与五种产生方式

💬 欢迎讨论 :前面我们学习了进程间通信的管道、共享内存等机制,它们主要用于进程间的数据传输。但有没有一种更轻量级的通信方式,可以让一个进程"通知"另一个进程发生了某个事件呢?答案是信号!信号是Linux中最古老、最经典的进程间通信方式,也是理解操作系统运行机制的重要窗口。本篇将带你从生活例子出发,深入理解信号的本质和五种产生方式。

👍 点赞、收藏与分享:这篇文章包含大量代码实验、图示说明,以及对信号机制的深入剖析,内容循序渐进且实用,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:建议按顺序阅读,从生活例子理解概念,再通过代码实验验证原理。


一、信号快速认识

1.1 生活角度的信号

在学习技术概念之前,我们先从生活中的例子理解"信号"的本质。

场景:网购快递

假设你在网上买了很多件商品,正在等待不同商品的快递到来:

bash 复制代码
1. 识别快递
   即便快递还没到,你也知道快递来了该怎么处理
   → 你能"识别"不同的快递

2. 记住快递
   快递员到了楼下,你正在打游戏,5分钟后才能去取
   → 在这5分钟内,你知道有快递要取,但没有立即执行
   → 你"记住了"有一个快递要去取

3. 处理快递
   时间合适后,你下楼拿到快递,开始处理:
   ① 默认动作:开心地打开,使用商品
   ② 自定义动作:是零食,要送给女朋友
   ③ 忽略:扔在床头,继续开一把游戏

4. 异步性
   快递到来的整个过程,对你来说是异步的
   你不能准确断定快递员什么时候给你打电话

映射到信号机制:

生活场景 信号机制 说明
进程 接收和处理信号的主体
快递员 操作系统 发送信号的实体
快递 信号 通知进程发生了某个事件
打电话通知 信号产生 OS向进程发送信号
记住要取快递 信号保存 进程记录收到的信号
下楼取快递 信号递达 进程处理信号
处理方式 信号捕捉 默认/忽略/自定义

📌 基本结论:

bash 复制代码
1. 怎么识别信号?
   → 识别信号是内置的,由内核程序员实现

2. 信号的处理方法什么时候准备好?
   → 在信号产生之前,处理方法就已经准备好了

3. 信号产生后立即处理吗?
   → 不一定,可能正在做优先级更高的事情
   → 会在"合适的时候"处理

4. 信号处理的三个阶段:
   信号到来 → 信号保存 → 信号处理

5. 信号处理的三种方式:
   a. 默认动作
   b. 忽略信号
   c. 自定义捕捉

1.2 技术应用角度的信号

1.2.1 一个样例

最常见的信号:Ctrl+C

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

int main()
{
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ g++ sig.cc -o sig
$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C                    ← 按下Ctrl+C,进程退出

发生了什么?

bash 复制代码
时间线:
t1: 用户输入命令,Shell启动前台进程
t2: 进程运行中,用户按下Ctrl+C
t3: 键盘硬件产生硬件中断
t4: OS捕获中断,解释为SIGINT信号(2号)
t5: OS向前台进程发送SIGINT信号
t6: 进程收到信号,执行默认处理动作:退出

完整流程图:

bash 复制代码
用户           键盘           OS              进程
 │              │              │               │
 ├─按Ctrl+C──→ │              │               │
 │              ├─硬件中断──→ │               │
 │              │              ├─解释为SIGINT │
 │              │              ├─发送信号───→ │
 │              │              │               ├─收到2号信号
 │              │              │               ├─执行默认动作
 │              │              │               └─退出
1.2.2 signal系统函数

函数原型:

c 复制代码
#include <signal.h>

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

// 参数说明:
//   signum:信号编号(后面解释)
//   handler:函数指针,表示信号的处理动作
//            当收到对应的信号,就回调执行handler方法
//
// 返回值:
//   成功:返回之前的信号处理函数
//   失败:返回SIG_ERR

开始测试:

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

void handler(int signumber)
{
    std::cout << "我是: " << getpid() 
              << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    
    // 修改2号信号的处理动作为自定义函数handler
    signal(SIGINT/*2*/, handler);
    
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2    ← 捕捉到信号,但没有退出
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!

📌 思考:

  1. 这里进程为什么不退出?

    • 因为我们修改了2号信号的处理动作
    • 原来的默认动作是"终止进程"
    • 现在的动作是"调用handler函数"
    • handler函数只是打印信息,不退出
  2. 这个例子能说明哪些问题?

    • 信号处理是进程自己处理的
    • 信号的处理方式可以被修改
    • signal函数设置的是"处理方法",而不是立即调用
  3. 请结合生活例子解释信号处理过程?

    • 进程就是你
    • 操作系统就是快递员
    • 信号就是快递
    • 发信号的过程就是给你打电话
    • handler函数就是你的处理方式

📌 注意:

bash 复制代码
重要提示:
1. signal函数仅仅是设置信号的捕捉行为,
   并不是直接调用处理动作

2. 如果信号没有产生,设置的捕捉函数永远也不会被调用!

3. Ctrl-C产生的信号只能发给前台进程
   - 一个命令后面加&可以放到后台运行
   - Shell可以同时运行一个前台进程和任意多个后台进程
   - 只有前台进程才能接到Ctrl-C这种控制键产生的信号

4. 信号相对于进程的控制流程来说是异步的
   - 用户随时可能按下Ctrl-C
   - 进程的用户空间代码执行到任何地方都有可能收到信号

渗透:& 和 nohup

bash 复制代码
# 后台运行进程
$ ./sig &
[1] 12345
我是进程: 12345
I am a process, I am waiting signal!
...
$ # Shell立即返回,可以输入新命令
$ # 按Ctrl+C不会杀死后台进程

# nohup:忽略挂断信号
$ nohup ./sig &
[1] 12346
nohup: ignoring input and appending output to 'nohup.out'
$ # 即使关闭终端,进程也会继续运行

1.3 信号概念

定义:

信号是进程之间事件异步通知的一种方式,属于软中断。

1.3.1 查看信号

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到:

c 复制代码
#define SIGINT  2    // 中断信号(Ctrl+C)

查看所有信号:

bash 复制代码
$ man 7 signal

Signal     Value     Action   Comment
────────────────────────────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction
SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating-point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
SIGCHLD   20,17,18    Ign     Child stopped or terminated
SIGCONT   19,18,25    Cont    Continue if stopped
SIGSTOP   17,19,23    Stop    Stop process
SIGTSTP   18,20,24    Stop    Stop typed at terminal
...

字段说明:

字段 含义
Signal 信号名称
Value 信号编号
Action 默认处理动作
Comment 信号说明

默认动作:

动作 含义
Term 终止进程
Core 终止进程并产生core dump
Ign 忽略信号
Stop 暂停进程
Cont 继续执行被暂停的进程

📌 注意:

bash 复制代码
编号34以上的是实时信号,本章只讨论编号34以下的信号。
1.3.2 信号处理

可选的处理动作有以下三种:

1. 忽略此信号(SIG_IGN)

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

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    
    signal(SIGINT/*2*/, SIG_IGN);  // 设置忽略信号
    
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ ./sig
我是进程: 212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C^C^C^C^C  ← 输入Ctrl+C毫无反应
I am a process, I am waiting signal!

2. 执行该信号的默认处理动作(SIG_DFL)

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

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    
    signal(SIGINT/*2*/, SIG_DFL);  // 恢复默认处理动作
    
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ ./sig
我是进程: 212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C  ← 输入Ctrl+C,进程退出(默认动作)

3. 提供一个信号处理函数(自定义捕捉)

cpp 复制代码
// 就是1.2.2节的样例
void handler(int signumber)
{
    std::cout << "我是: " << getpid() 
              << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    signal(SIGINT, handler);  // 自定义捕捉
    // ...
}

注意看源码:

c 复制代码
#define SIG_DFL ((__sighandler_t) 0)  /* Default action. */
#define SIG_IGN ((__sighandler_t) 1)  /* Ignore signal. */

/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);

// SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型

二、产生信号

上面的内容让我们快速认识了信号,下面我们从理论和实操两个层面,详细学习信号的产生方式

2.1 通过终端按键产生信号

2.1.1 基本操作

1. Ctrl+C(SIGINT,2号信号)

前面已经验证过,这里不再重复。

2. Ctrl+\(SIGQUIT,3号信号)

SIGQUIT可以发送终止信号并生成core dump文件(后面详谈)。

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

void handler(int signumber)
{
    std::cout << "我是: " << getpid() 
              << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    
    signal(SIGQUIT/*3*/, handler);  // 捕捉3号信号
    
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ ./sig
我是进程: 213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\我是: 213056, 我获得了一个信号: 3  ← 按Ctrl+\

# 如果注释掉signal(SIGQUIT, handler);
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\Quit  ← 按Ctrl+\,进程退出并产生core dump

3. Ctrl+Z(SIGTSTP,20号信号)

SIGTSTP可以发送停止信号,将当前前台进程挂起到后台。

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

void handler(int signumber)
{
    std::cout << "我是: " << getpid() 
              << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    
    signal(SIGTSTP/*20*/, handler);  // 捕捉20号信号
    
    while(true) {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
bash 复制代码
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z我是: 213552, 我获得了一个信号: 20  ← 按Ctrl+Z

# 如果注释掉signal(SIGTSTP, handler);
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+  Stopped                 ./sig  ← 进程被挂起

$ jobs
[1]+  Stopped                 ./sig

$ fg  ← 将后台进程调到前台继续执行
I am a process, I am waiting signal!
2.1.2 理解OS如何得知键盘有数据

键盘输入的完整流程:

bash 复制代码
用户按键
    │
    ├─ 键盘硬件检测到按键
    ├─ 键盘控制器生成扫描码
    ├─ 通过中断控制器(8259A)向CPU发送中断请求
    ├─ CPU暂停当前指令,查询中断向量表
    ├─ 跳转到键盘中断处理程序
    │
    └─ OS中断处理程序:
        ├─ 读取键盘扫描码
        ├─ 解释为ASCII字符
        ├─ 检查是否为特殊按键(Ctrl+C等)
        ├─ 如果是特殊按键,生成对应信号
        └─ 发送信号给前台进程

硬件中断机制:

bash 复制代码
外部设备(键盘)
      │
      ├─硬件中断──→ 中断控制器(8259A)
                        │
                        ├─发送中断请求──→ CPU
                                           │
                                           ├─查询中断向量表
                                           ├─执行中断处理程序
                                           └─返回用户程序
2.1.3 初步理解信号起源

📌 重要理解:

bash 复制代码
信号的本质:
- 信号是从纯软件角度,模拟硬件中断的行为
- 硬件中断是发给CPU
- 信号是发给进程

相似性:
- 都是异步通知机制
- 都有处理程序
- 都可以被屏蔽/阻塞

不同点:
- 硬件中断:硬件层面,CPU响应
- 信号:软件层面,进程响应

对比图:

bash 复制代码
硬件中断                        信号
────────                        ────
外部设备产生                    OS/进程/键盘产生
发给CPU                         发给进程
中断向量表                      信号处理表
中断处理程序                    信号处理函数
可被屏蔽(IF标志)                可被阻塞(block表)

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

示例代码:

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

int main()
{
    while(true) {
        sleep(1);
    }
    return 0;
}
bash 复制代码
# step 1: 编译
$ g++ sig.cc -o sig

# step 2: 后台运行
$ ./sig &
[1] 213784

# step 3: 查看进程
$ ps ajx | head -1 && ps ajx | grep sig
PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
211805 213784 213784 211805 pts/0  213792 S     1002     0:00 ./sig

使用kill命令发送信号:

首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号:

bash 复制代码
$ kill -SIGSEGV 213784
$ # 多按一次回车
[1]+  Segmentation fault      ./sig

📌 注意:

bash 复制代码
1. 213784是sig进程的pid

2. 之所以要再次回车才显示"Segmentation fault",
   是因为在进程终止前已经回到Shell提示符等待输入下一条命令,
   Shell不希望错误信息和用户的输入交错在一起,
   所以等用户输入命令之后才显示

3. 指定发送某种信号的kill命令可以有多种写法:
   kill -SIGSEGV 213784
   kill -11 213784        ← 11是信号SIGSEGV的编号
   kill -SEGV 213784

4. 以往遇到的段错误都是由非法内存访问产生的,
   而这个程序本身没错,给它发SIGSEGV也能产生段错误

2.3 使用函数产生信号

2.3.1 kill函数

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。

函数原型:

c 复制代码
NAME
       kill - send signal to a process

SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);

RETURN VALUE
       On success (at least one signal was sent), zero is returned.
       On error, -1 is returned, and errno is set appropriately.

样例:实现自己的kill命令

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

// 用法: mykill -signumber pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
        return 1;
    }
    
    int number = std::stoi(argv[1] + 1);  // 去掉'-'
    pid_t pid = std::stoi(argv[2]);
    
    int n = kill(pid, number);
    if(n < 0)
    {
        perror("kill");
        return 2;
    }
    
    std::cout << "发送信号 " << number << " 给进程 " << pid << " 成功" << std::endl;
    return 0;
}
bash 复制代码
$ g++ mykill.cc -o mykill

# 启动一个测试进程
$ ./sig &
[1] 214123

# 使用我们自己的mykill发送信号
$ ./mykill -2 214123
发送信号 2 给进程 214123 成功
[1]+  Terminated              ./sig

$ ./mykill -9 214123
发送信号 9 给进程 214123 成功
2.3.2 raise函数

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

函数原型:

c 复制代码
NAME
       raise - send a signal to the caller

SYNOPSIS
       #include <signal.h>

       int raise(int sig);

RETURN VALUE
       raise() returns 0 on success, and nonzero for failure.

样例:

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

void handler(int signumber)
{
    // 整个代码就只有这一处打印
    std::cout << "获取了一个信号: " << signumber << std::endl;
}

int main()
{
    signal(2, handler);  // 先对2号信号进行捕捉
    
    // 每隔1秒,自己给自己发送2号信号
    while(true)
    {
        sleep(1);
        raise(2);
    }
    return 0;
}
bash 复制代码
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
^C

📌 理解:

bash 复制代码
raise(sig) 等价于 kill(getpid(), sig)

raise函数的优点:
- 代码更简洁
- 不需要获取进程ID
- 语义更清晰(给自己发信号)
2.3.3 abort函数

abort函数使当前进程接收到SIGABRT信号而异常终止。

函数原型:

c 复制代码
NAME
       abort - cause abnormal process termination

SYNOPSIS
       #include <stdlib.h>

       void abort(void);

RETURN VALUE
       The abort() function never returns.
       // 就像exit函数一样,abort函数总是会成功的,所以没有返回值

样例:

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

void handler(int signumber)
{
    // 整个代码就只有这一处打印
    std::cout << "获取了一个信号: " << signumber << std::endl;
}

int main()
{
    signal(SIGABRT, handler);
    
    while(true)
    {
        sleep(1);
        abort();
    }
    return 0;
}
bash 复制代码
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号: 6  ← 实验可以得知,abort给自己发送的是固定6号信号
Aborted                ← 虽然捕捉了,但是还是要退出

# 注释掉signal(SIGABRT, handler);
$ ./Abort
Aborted

📌 注意:

bash 复制代码
abort()的特殊性:
1. 固定发送SIGABRT(6号)信号
2. 即使捕捉了信号,进程最终还是会退出
3. 用于程序异常终止,产生core dump
4. 常用于断言(assert)失败时调用

2.4 由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在"管道"中已经介绍过了。本节主要介绍alarm函数和SIGALRM信号。

alarm函数原型:

c 复制代码
NAME
       alarm - set an alarm clock for delivery of a signal

SYNOPSIS
       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);

RETURN VALUE
       alarm() returns the number of seconds remaining until any previously
       scheduled alarm was due to be delivered, or zero if there was no
       previously scheduled alarm.

📌 说明:

bash 复制代码
1. 调用alarm函数可以设定一个闹钟,
   也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号

2. SIGALRM信号的默认处理动作是终止当前进程

3. 返回值是0或者是以前设定的闹钟时间还余下的秒数
   - 打个比方:某人要小睡一觉,设定闹钟为30分钟后响,
     20分钟后被人吵醒了,还想多睡一会儿,
     于是重新设定闹钟为15分钟后响,
     "以前设定的闹钟时间还余下的时间"就是10分钟

4. 如果seconds值为0,表示取消以前设定的闹钟,
   函数的返回值仍然是以前设定的闹钟时间还余下的秒数
2.4.1 基本alarm验证 - 体会IO效率问题

程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。

实验1:IO多

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

int main()
{
    int count = 0;
    alarm(1);  // 1秒后发送SIGALRM信号,默认终止进程
    
    while(true)
    {
        std::cout << "count : " << count << std::endl;
        count++;
    }
    return 0;
}
bash 复制代码
$ g++ alarm.cc -o alarm
$ ./alarm
count : 0
count : 1
count : 2
...
count : 107148
count  ← 1秒后被信号终止

实验2:IO少

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

int count = 0;

void handler(int signumber)
{
    std::cout << "count : " << count << std::endl;
    exit(0);
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    
    while(true)
    {
        count++;  // 只计数,不打印
    }
    return 0;
}
bash 复制代码
$ g++ alarm.cc -o alarm
$ ./alarm
count : 492333713  ← 1秒后打印计数,数值巨大!

📌 结论:

bash 复制代码
1. 闹钟会响一次,默认终止进程

2. 有IO效率低
   - 实验1:1秒内只数了10万次
   - 实验2:1秒内数了4亿多次
   - 差距:约4000倍!

3. IO操作(如std::cout)非常耗时
   - 涉及系统调用
   - 涉及设备驱动
   - 涉及硬件操作

4. 这个实验说明了为什么:
   "程序大部分时间都在等待IO"
2.4.2 设置重复闹钟

代码样例:

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

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

int gcount = 0;
std::vector<func_t> gfuncs;

void handler(int signo)
{
    // 可以在这里执行多个任务
    for(auto &f : gfuncs)
    {
        f();
    }
    
    std::cout << "gcount : " << gcount << std::endl;
    
    int n = alarm(1);  // 重设闹钟,会返回上一次闹钟的剩余时间
    std::cout << "剩余时间 : " << n << std::endl;
}

int main()
{
    // 可以注册多个任务函数
    // gfuncs.push_back([](){ std::cout << "我是一个内核刷新操作" << std::endl; });
    // gfuncs.push_back([](){ std::cout << "我是一个检测进程时间片的操作" << std::endl; });
    // gfuncs.push_back([](){ std::cout << "我是一个内存管理操作" << std::endl; });
    
    std::cout << "我的进程pid是: " << getpid() << std::endl;
    
    alarm(1);  // 一次性的闹钟,超时alarm会自动被取消
    signal(SIGALRM, handler);
    
    while(true)
    {
        pause();  // 阻塞等待信号
        std::cout << "我醒来了..." << std::endl;
        gcount++;
    }
    return 0;
}

pause函数:

c 复制代码
NAME
       pause - wait for signal

SYNOPSIS
       #include <unistd.h>

       int pause(void);

DESCRIPTION
       pause() causes the calling process (or thread) to sleep until a
       signal is delivered that either terminates the process or causes
       the invocation of a signal-catching function.

RETURN VALUE
       pause() returns only when a signal was caught and the signal-
       catching function returned.  In this case, pause() returns -1,
       and errno is set to EINTR.

运行测试:

bash 复制代码
# 窗口1
$ ./alarm
我的进程pid是: 216982
我醒来了...
gcount : 0
剩余时间 : 0
我醒来了...
gcount : 1
剩余时间 : 0

# 窗口2: 提前唤醒它
$ kill -14 216982

# 窗口1继续显示:
我醒来了...
gcount : 2
剩余时间 : 13  ← 提前唤醒,剩余时间
我醒来了...
gcount : 3
剩余时间 : 0

📌 结论:

bash 复制代码
1. 闹钟设置一次,起效一次

2. 重复设置的方法:
   在handler中再次调用alarm(1)

3. 如果提前唤醒(比如发送其他信号),
   返回值是剩余的秒数

4. alarm(0)可以取消闹钟
2.4.3 如何理解软件条件

定义:

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。

包括但不限于:

bash 复制代码
1. 定时器超时
   - alarm函数设定的时间到达
   - 产生SIGALRM信号

2. 软件异常
   - 向已关闭的管道写数据
   - 产生SIGPIPE信号

3. 子进程状态变化
   - 子进程退出
   - 产生SIGCHLD信号

4. 其他软件事件
   - 用户自定义条件
   - 产生SIGUSR1/SIGUSR2信号

与硬件异常的区别:

类型 触发方式 例子 信号
硬件异常 CPU检测到 除0、野指针 SIGFPE、SIGSEGV
软件条件 OS判断 定时器、管道 SIGALRM、SIGPIPE
2.4.4 如何简单快速理解系统闹钟

系统闹钟的本质是OS必须自身具有定时功能,并能让用户设置这种定时功能。

内核定时器数据结构:

c 复制代码
struct timer_list {
    struct list_head entry;
    unsigned long expires;         // 超时时间
    void (*function)(unsigned long);  // 处理方法
    unsigned long data;
    struct tvec_t_base_s *base;
};

简化理解(堆结构):

bash 复制代码
                  [最小堆]
                     │
           ┌─────────┼─────────┐
           │         │         │
      timer1     timer2     timer3
    expires=10  expires=15  expires=20
    handler1    handler2    handler3

时间推进:
t=0:  启动定时器
t=10: timer1超时,调用handler1
t=15: timer2超时,调用handler2
t=20: timer3超时,调用handler3

📌 注意:

bash 复制代码
实际内核使用的是"时间轮"(timing wheel)的数据结构,
比堆更高效,但原理类似。

我们这里为了简单理解,可以把它想象成堆结构。

2.5 硬件异常产生信号

硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。

例子:

bash 复制代码
1. 除以0:
   CPU的运算单元产生异常
   → 内核将异常解释为SIGFPE信号发送给进程

2. 非法内存访问:
   MMU(内存管理单元)产生异常
   → 内核将异常解释为SIGSEGV信号发送给进程
2.5.1 模拟除0
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

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

int main()
{
    signal(SIGFPE, handler);  // 8号信号
    sleep(1);
    
    int a = 10;
    a除0操作
    
    while(1);
    return 0;
}
bash 复制代码
$ gcc sig.c -o sig
$ ./sig
catch a sig : 8
catch a sig : 8
catch a sig : 8
catch a sig : 8
...  ← 一直产生8号信号

📌 思考:为什么一直有8号信号产生?

bash 复制代码
原因分析:
1. CPU执行除0指令时,运算单元检测到异常

2. CPU中有状态寄存器,记录了异常状态标记位

3. OS检查状态寄存器,发现异常存在

4. OS调用对应的异常处理方法(发送信号)

5. 但是!我们并没有清理现场:
   - 没有清理内存
   - 没有关闭进程打开的文件
   - 没有切换进程
   - CPU寄存器内容还保留着

6. 所以除0异常会一直存在,信号就一直发出

正确的处理方式:

cpp 复制代码
void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
    exit(1);  // 退出进程,清理现场
}
2.5.2 模拟野指针
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

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

int main()
{
    signal(SIGSEGV, handler);  // 11号信号
    sleep(1);
    
    int *p = NULL;
    *p = 100;  // 野指针解引用
    
    while(1);
    return 0;
}
bash 复制代码
# 默认行为
$ gcc sig.c -o sig
$ ./sig
Segmentation fault (core dumped)

# 捕捉行为
$ ./sig  ← 启用handler
catch a sig : 11
catch a sig : 11
catch a sig : 11
...  ← 一直产生11号信号

原理同除0,不再赘述。

2.5.3 子进程退出core dump
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
    if(fork() == 0)
    {
        // 子进程
        sleep(1);
        int a = 10;
        a /= 0;  // 触发SIGFPE
        exit(0);
    }
    
    // 父进程
    int status = 0;
    waitpid(-1, &status, 0);
    
    printf("exit signal: %d, core dump: %d\n", 
           status & 0x7F,        // 低7位是信号编号
           (status >> 7) & 1);   // 第8位是core dump标志
    
    return 0;
}
bash 复制代码
$ ./sig
exit signal: 8, core dump: 1

status的位图结构:

bash 复制代码
status(int类型,32位):
┌─────┬─────┬─────────────────┬─────────────────┐
│ ... │core │ exit signal(7位)│ exit code(8位)  │
│     │dump │                 │                 │
└─────┴─────┴─────────────────┴─────────────────┘
  高位   第8位   低7位(0-6)      次低8位(8-15)

如果进程被信号终止:
- 低7位:信号编号
- 第8位:是否产生core dump
- 次低8位:无意义

如果进程正常退出:
- 低7位:0
- 次低8位:exit code
2.5.4 Core Dump

什么是Core Dump?

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。

作用:

bash 复制代码
1. 事后调试(Post-mortem Debug)
   - 进程异常终止通常是因为有Bug
   - 比如非法内存访问导致段错误
   - 事后可以用调试器检查core文件以查清错误原因

2. 保留现场
   - core文件保存了进程的完整状态
   - 包括所有变量的值
   - 包括调用栈信息

设置core文件大小:

bash 复制代码
# 查看当前限制
$ ulimit -a
core file size          (blocks, -c) 0  ← 默认不允许产生core文件
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
...

# 设置允许产生core文件(最大1024KB)
$ ulimit -c 1024

$ ulimit -a
core file size          (blocks, -c) 1024  ← 已修改
...

为什么默认是0?

bash 复制代码
安全原因:
- core文件中可能包含用户密码等敏感信息
- 默认不允许产生,防止信息泄露

开发调试阶段:
- 可以用ulimit命令改变这个限制
- 允许产生core文件用于调试

测试core dump:

cpp 复制代码
#include <stdio.h>

int main()
{
    int *p = NULL;
    *p = 100;  // 触发段错误
    return 0;
}
bash 复制代码
$ ulimit -c 1024
$ gcc sig.c -o sig -g  ← -g选项产生调试信息
$ ./sig
Segmentation fault (core dumped)  ← 产生了core文件

$ ls -lh core
-rw------- 1 user user 180K Jan 23 10:30 core

# 使用gdb调试core文件
$ gdb sig core
GNU gdb (GDB) 8.1
...
Core was generated by `./sig'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400526 in main () at sig.c:6
6           *p = 100;

(gdb) bt  ← 查看调用栈
#0  0x0000000000400526 in main () at sig.c:6

(gdb) p p  ← 查看变量p的值
$1 = (int *) 0x0  ← 确实是空指针

(gdb) quit

2.6 总结思考

📌 重要思考题:

  1. 上面所说的所有信号产生,最终都要由OS来执行,为什么?

    • OS是进程的管理者
    • 只有OS能修改进程的PCB
    • 信号的本质是修改进程PCB中的pending位图
  2. 信号的处理是否是立即处理的?

    • 不是立即处理
    • 在"合适的时候"处理
    • 具体时机:从内核态返回用户态时检查pending
  3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?

    • 是的,需要记录
    • 记录在PCB的pending位图中
  4. 记录在哪里最合适呢?

    • PCB(进程控制块)
    • 因为PCB是内核管理进程的核心数据结构
    • 下一篇会详细讲解
  5. 一个进程在没有收到信号的时候,能否知道自己应该对合法信号作何处理呢?

    • 处理方法在信号产生之前就已经准备好了
    • 记录在PCB的handler数组中
  6. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

    • 下一篇详细讲解

五种信号产生方式总结:

方式 函数/操作 信号 特点
终端按键 Ctrl+C//Z SIGINT/SIGQUIT/SIGTSTP 硬件中断触发
系统命令 kill命令 任意信号 用户主动发送
系统函数 kill/raise/abort 任意/当前/SIGABRT 编程控制
软件条件 alarm/管道 SIGALRM/SIGPIPE 软件状态触发
硬件异常 除0/野指针 SIGFPE/SIGSEGV CPU检测异常

三、本篇总结

本篇我们从生活例子出发,深入学习了信号的概念和五种产生方式。

核心知识点:

  1. 信号的本质

    • 进程间事件异步通知的一种方式
    • 软件模拟硬件中断的机制
    • 三个阶段:信号到来 → 信号保存 → 信号处理
  2. 信号处理的三种方式

    • 默认动作(SIG_DFL)
    • 忽略信号(SIG_IGN)
    • 自定义捕捉(handler函数)
  3. 五种信号产生方式

    • 终端按键(Ctrl+C//Z)
    • 系统命令(kill)
    • 系统函数(kill/raise/abort)
    • 软件条件(alarm/管道)
    • 硬件异常(除0/野指针)
  4. 重要理解

    • 信号处理不是立即的,在合适的时候
    • 信号处理方法在信号产生前就准备好了
    • 信号相对于进程是异步的
    • IO操作非常耗时,影响程序效率

完整流程图:

bash 复制代码
信号产生                 信号保存                 信号递达
────────                ────────                ────────
键盘/函数/              PCB的pending            从内核态返回用户态时
异常/条件               位图记录信号            检查pending和block
    │                       │                       │
    ├──────────────────────→├──────────────────────→│
    │                       │                       │
    │                   如果被阻塞                  │
    │                   暂不处理                 检查handler
    │                       │                   ────────
    │                   如果未阻塞               │ 默认  │
    │                   等待递达                 │ 忽略  │
    │                       │                   │ 自定义 │
    └───────────────────────┴───────────────────┴───────→
                                                  执行处理动作

下一篇预告:

在下一篇中,我们将深入学习:

bash 复制代码
1. 信号在内核中如何保存?
   - PCB中的pending位图
   - PCB中的block位图
   - PCB中的handler数组

2. 信号集操作函数
   - sigemptyset/sigfillset
   - sigaddset/sigdelset
   - sigismember
   - sigprocmask
   - sigpending

3. 实验验证
   - 打印pending位图
   - 阻塞信号
   - 解除阻塞
   - 观察信号递达

💬 总结:通过本篇的学习,我们从生活例子理解了信号的本质,掌握了五种信号产生方式,并通过大量代码实验验证了信号的基本行为。信号机制是Linux进程间通信的重要方式,也是理解操作系统运行机制的重要窗口。下一篇我们将深入内核,看看信号是如何被保存和管理的。

👍 点赞、收藏与分享:如果这篇文章对你有帮助,请点赞、收藏并分享给更多需要的人!有任何问题欢迎在评论区讨论。

附录:常用信号速查表

信号名 编号 默认动作 说明 产生方式
SIGHUP 1 Term 终端挂断 终端关闭
SIGINT 2 Term 终端中断 Ctrl+C
SIGQUIT 3 Core 终端退出 Ctrl+\
SIGILL 4 Core 非法指令 CPU异常
SIGABRT 6 Core 异常终止 abort()
SIGFPE 8 Core 浮点异常 除0
SIGKILL 9 Term 强制终止 kill -9
SIGSEGV 11 Core 段错误 野指针
SIGPIPE 13 Term 管道破裂 写关闭的管道
SIGALRM 14 Term 定时器 alarm()
SIGTERM 15 Term 终止信号 kill默认
SIGCHLD 17 Ign 子进程状态变化 子进程退出
SIGCONT 18 Cont 继续执行 fg/bg
SIGSTOP 19 Stop 停止执行 kill -19
SIGTSTP 20 Stop 终端停止 Ctrl+Z

📌 注意:

  • Term:终止进程
  • Core:终止进程并产生core dump
  • Ign:忽略信号
  • Stop:暂停进程
  • Cont:继续执行被暂停的进程
相关推荐
赵民勇2 小时前
使用GSocketService创建Socket服务详解
linux·服务器
刹那间的回眸x.y2 小时前
Jenkins学习
运维·学习·jenkins
啊阿狸不会拉杆2 小时前
《计算机操作系统》第六章-输入输出系统
java·开发语言·c++·人工智能·嵌入式硬件·os·计算机操作系统
欧洵.2 小时前
HTTP协议详解Fiddler的安装与使用
网络·网络协议·http
线束线缆组件品替网2 小时前
Stewart Connector RJ45 以太网线缆高速接口设计解析
服务器·网络·人工智能·音视频·硬件工程·材料工程
潇冉沐晴2 小时前
div3 970个人笔记
c++·笔记·算法
王老师青少年编程2 小时前
2023年12月GESP真题及题解(C++八级): 奖品分配
c++·题解·真题·gesp·csp·八级·奖品分配
凯子坚持 c2 小时前
Qt常用控件指南(3)
运维·服务器
我不是程序员yy2 小时前
常见网络故障排查思路:从 DNS 到 TCP,一步步定位问题
网络