一句话区别:
- 信号(Signal):通知进程"发生了一件事"(异步通知机制)。
- 信号量(Semaphore):控制多个线程/进程"谁可以使用资源"(同步互斥机制)。
一、信号(Signal)
1. 什么是信号?
信号(Signal)是 Linux 内核向进程发送的一种异步通知机制。
它用于通知进程:
- 用户按下 Ctrl+C
- 子进程退出
- 定时器超时
- 非法访问内存
- 管道断开
可以理解为:
信号 = 中断 + 通知
例如:
Ctrl + C
│
▼
Linux Kernel
│
发送 SIGINT
│
▼
进程收到信号
Kernel(内核)是计算机操作系统最核心的程序,主要负责管理硬件资源、充当软件与硬件的桥梁以及控制系统安全。
2. 信号的特点
- 异步发生
- 不携带大量数据(普通信号只有编号)
- 可以打断程序当前执行流程
- 每个信号都有编号
- 大多数信号可以被捕获(signal/sigaction)
3. 常见信号
| 信号 | 编号 | 含义 |
|---|---|---|
| SIGHUP | 1 | 终端断开 |
| SIGINT | 2 | Ctrl+C |
| SIGQUIT | 3 | Ctrl+\ |
| SIGILL | 4 | 非法指令 |
| SIGABRT | 6 | abort():非正常退出 |
| SIGFPE | 8 | 除零异常 |
| SIGKILL | 9 | 强制结束(不能捕获) |
| SIGSEGV | 11 | 段错误 |
| SIGPIPE | 13 | 管道断开 |
| SIGALRM | 14 | alarm() 定时器 |
| SIGTERM | 15 | 正常结束 |
| SIGCHLD | 17 | 子进程结束 |
| SIGCONT | 18 | 继续运行 |
| SIGSTOP | 19 | 暂停(不能捕获) |
| SIGTSTP | 20 | Ctrl+Z |
查看:
kill -l
flowchart TD
A[程序运行] --> B{收到什么信号?}
B -->|Ctrl+C| C[SIGINT<br/>退出]
B -->|Ctrl+\\| D[SIGQUIT<br/>退出+Core]
B -->|Ctrl+Z| E[SIGTSTP<br/>暂停]
B -->|kill| F[SIGTERM<br/>正常退出]
B -->|kill -9| G[SIGKILL<br/>强制退出]
B -->|kill -STOP| H[SIGSTOP<br/>强制暂停]
H -->|kill -CONT| I[SIGCONT<br/>继续]
E -->|fg/bg| I
B -->|除0| J[SIGFPE]
B -->|非法内存| K[SIGSEGV]
B -->|非法指令| L[SIGILL]
B -->|abort| M[SIGABRT]
B -->|alarm到时| N[SIGALRM]
B -->|终端关闭| O[SIGHUP]
B -->|子进程结束| P[SIGCHLD]
B -->|写入断开的管道/Socket| Q[SIGPIPE]
4. 信号产生方式
(1)键盘产生
例如:
Ctrl+C
发送:
SIGINT
用户
│
│ Ctrl+C
▼
终端
│
▼
Kernel
│
▼
进程
(2)kill命令
例如:
kill -15 pid
或者
kill -9 pid
(3)系统调用
例如:
kill(pid,SIGUSR1);
或者
raise(SIGINT);
当前进程主动给自己发送一个 SIGINT 信号。
(4)硬件异常
例如:
int a=1/0;
产生:
SIGFPE
访问非法地址:
SIGSEGV
(5)软件条件
例如:
alarm(5);
五秒后:
SIGALRM
5. 信号处理方式
收到信号以后,有三种处理方式:
graph TD
A[收到信号] --> B{查询信号处理动作}
B -->|SIG_DFL| C[默认处理<br/>Default]
B -->|SIG_IGN| D[忽略<br/>Ignore]
B -->|User Handler| E[用户捕获<br/>Catch]
默认处理
例如:
SIGTERM
默认:
退出程序
忽略
例如:
signal(SIGINT,SIG_IGN);
按 Ctrl+C:
没有反应
捕获
例如:
void handler(int sig)
{
printf("收到信号%d\n",sig);
}
signal(SIGINT,handler);
收到:
SIGINT
执行:
handler()
6. signal()
#include <signal.h>
signal(SIGINT,handler);
作用:
安装一个信号处理函数。
流程:
flowchart TD
A[收到 SIGINT] --> B[Kernel: 检查处理动作]
B --> C{是否为自定义 handler?}
C -- 是 --> D[Kernel: 保存用户态上下文]
D --> E[Kernel: 切换到信号栈]
E --> F[执行 handler]
F --> G[执行 sigreturn 系统调用]
G --> H[Kernel: 恢复用户态上下文]
H --> I[返回程序继续执行]
C -- 否 --> J[执行默认动作/忽略]
7. sigaction()
推荐使用。
比 signal() 更安全。
struct sigaction act;
act.sa_handler=handler;
sigaction(SIGINT,&act,NULL);
优势:
- 更稳定
- 可以设置屏蔽信号
- 可以设置更多属性
8. 信号屏蔽
有时候:
不希望现在处理信号。
可以:
先屏蔽
以后再处理
流程:
flowchart LR
A[收到 SIGINT]
B[加入Pending]
C[解除屏蔽]
D[执行handler]
A --> B --> C --> D
常见函数:
sigprocmask()
**<font style="color:rgb(34, 34, 34);">sigprocmask</font>** 是用于检查或修改当前线程信号屏蔽字(Signal Mask)的系统调用,其核心作用是控制哪些信号在当前时刻被“阻塞”(即暂时不递送给进程),从而实现对信号处理的精确时序控制。
9. 未决信号(Pending)
收到信号:
并不会马上执行
可能:
Pending
等待解除屏蔽。
流程:
产生信号
│
▼
Pending
│
解除Mask
▼
执行Handler
10. 信号生命周期
flowchart LR
A[产生 Signal]
B[Pending]
C[是否屏蔽?]
A --> B
B --> C
C -- 是 --> B
C -- 否 --> D[Delivery]
D --> E[Handler]
二、信号量(Semaphore)
1. 什么是信号量?
信号量(Semaphore)用于:
同步多个线程/进程对共享资源的访问。
它本质上就是:
一个整数
谁拿到资源:
--
释放资源:
++
2. 为什么需要信号量?
例如:
厕所只有3个坑位。
来了10个人。
坑位:3
A ✔
B ✔
C ✔
D 等待
E 等待
...
只有出来一个:
++
等待的人:
--
进入。
3. 信号量分类
二值信号量
只有:
0
1
作用:
类似:
Mutex
计数信号量
例如:
5
表示:
有5个资源
4. P操作 和 V操作
经典名称:
P()
申请资源。
sem_wait()
value--
如果:
value<0
阻塞。
V()
释放资源。
sem_post()
value++
唤醒等待线程。
图解
假设:
Semaphore=3
sequenceDiagram
participant S as Semaphore (初始值=2)
participant A as 线程A
participant B as 线程B
participant C as 线程C
participant D as 线程D
Note over S: 初始值 = 2
A->>S: P()
S-->>S: 值: 2 → 1
Note right of A: ✅ 获取成功,继续执行
B->>S: P()
S-->>S: 值: 1 → 0
Note right of B: ✅ 获取成功,继续执行
C->>S: P()
S-->>S: 值: 0 → -1
Note right of C: ⏳ 信号量 < 0,进入等待队列
D->>S: P()
S-->>S: 值: -1 → -2
Note right of D: ⏳ 信号量 < 0,进入等待队列
Note over S: 当前状态:2个线程运行,2个线程等待
线程A:
V()
Semaphore=0
↓
线程D 被唤醒
5. Linux POSIX信号量
头文件:
#include <semaphore.h>
创建:
sem_t sem;
初始化:
sem_init(&sem,0,3);
表示:
三个资源
等待:
sem_wait(&sem);
相当于:
P()
释放:
sem_post(&sem);
相当于:
V()
销毁:
sem_destroy(&sem);
6. 工作流程
flowchart TD
A[线程请求资源]
B{Semaphore > 0 ?}
C[sem--]
D[进入临界区]
E[阻塞等待]
F[sem_post]
G[sem++]
H[唤醒等待线程]
A --> B
B -- Yes --> C
C --> D
D --> F
F --> G
G --> H
B -- No --> E
H --> D
7. 信号量实现线程同步
例如:
生产者:
生产一个数据
↓
sem_post()
消费者:
sem_wait()
↓
消费数据
流程:
flowchart LR
Producer --> Buffer
Producer --> sem_post
sem_post --> Semaphore
Consumer --> sem_wait
sem_wait --> Buffer
8. 信号量实现互斥
初始化:
Semaphore=1
线程A
wait()
↓
进入临界区
此时:
Semaphore=0
线程B:
wait()
↓
阻塞
线程A:
post()
线程B:
继续执行
9. 信号量 VS 互斥锁
| 对比项 | 信号量 | 互斥锁 |
|---|---|---|
| 本质 | 资源计数器 | 加锁对象 |
| 值 | 可以大于1 | 只能锁/解锁 |
| 可控制资源数 | ✔ | ✘ |
| 谁释放 | 任意线程 | 必须加锁线程 |
| 是否适合同步 | ✔ | 一般 |
| 是否适合互斥 | ✔ | ✔(最佳) |
三、信号 与 信号量 区别
| 项目 | 信号 Signal | 信号量 Semaphore |
|---|---|---|
| 中文 | 信号 | 信号量 |
| 本质 | 异步通知 | 同步机制 |
| 谁发送 | Kernel | 程序员 |
| 是否携带资源 | ✘ | ✔ |
| 是否控制资源 | ✘ | ✔ |
| 是否会中断程序 | ✔ | ✘ |
| 是否用于线程同步 | ✘ | ✔ |
| 是否用于进程通信 | ✔ | ✔ |
四、整体关系图
graph TD
A[Linux 进程/线程同步与通信]
A --> B[Signal 信号]
A --> C[Semaphore 信号量]
%% Signal
B --> B1[本质]
B1 --> B11[异步通知]
B1 --> B12[软件中断]
B --> B2[产生方式]
B2 --> B21[kill]
B2 --> B22[raise]
B2 --> B23[alarm]
B2 --> B24[Ctrl+C]
B --> B3[分类]
B3 --> B31[不可靠信号]
B3 --> B32[可靠信号]
B --> B4[处理方式]
B4 --> B41[默认]
B4 --> B42[忽略]
B4 --> B43[捕获]
B --> B5[常用API]
B5 --> B51[signal]
B5 --> B52[sigaction]
B5 --> B53[kill]
B5 --> B54[sigprocmask]
%% Semaphore
C --> C1[本质]
C1 --> C11[同步]
C1 --> C12[互斥]
C1 --> C13[资源计数]
C --> C2[类型]
C2 --> C21[无名信号量]
C2 --> C22[有名信号量]
C --> C3[P/V操作]
C3 --> C31[sem_wait]
C3 --> C32[sem_post]
C3 --> C33[sem_trywait]
C --> C4[常用API]
C4 --> C41[sem_init]
C4 --> C42[sem_destroy]
C4 --> C43[sem_open]
C4 --> C44[sem_close]
C4 --> C45[sem_unlink]
C --> C5[应用]
C5 --> C51[线程同步]
C5 --> C52[进程同步]
C5 --> C53[生产者消费者]
C5 --> C54[资源池]
五、面试高频考点
信号(Signal)
- 什么是信号?有哪些特点?
- 常见信号有哪些?(SIGINT、SIGKILL、SIGTERM、SIGCHLD、SIGSEGV)
- signal() 与 sigaction() 有什么区别?
- SIGKILL 为什么不能捕获?
- SIGSTOP 为什么不能忽略?
- 什么是信号屏蔽(Mask)?
- 什么是未决信号(Pending)?
- kill()、raise()、alarm() 有什么作用?
信号量(Semaphore)
- 什么是 P 操作和 V 操作?
- sem_wait() 和 sem_post() 的作用?
- 二值信号量和计数信号量有什么区别?
- 信号量如何实现线程同步?
- 信号量如何实现互斥?
- 信号量和互斥锁有什么区别?
- 生产者消费者模型为什么适合使用信号量?