当前阶段:


一、通过终端按键产生信号
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
#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;
}
#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 的心跳 | 驱动整个系统的定时机制 |
| 信号本质是异步通知 | 软件条件通过内核数据结构触发 |