心宇

FDF24DCED8C6738F8313A6D3B073C3F4

首页 管理

一、什么是信号?

Signal:

信号

是 Unix/Linux 中一种经典的:

异步事件通知机制。

它主要用于:

通知进程或线程:某个事件已经发生。

例如:

Ctrl + C
      ↓
SIGINT
      ↓
Process
      ↓
执行默认动作 / Signal Handler

又例如:

非法内存访问
      ↓
SIGSEGV
      ↓
Process

或者:

kill -TERM PID
      ↓
SIGTERM
      ↓
Process

因此 Signal 的核心不是:

“传输大量数据。”

而是:

异步通知某个事件已经发生。


二、Signal 与其他 IPC 的区别

例如消息队列:

Process A
   ↓
Message
   ↓
Process B

重点是:

传递消息数据

共享内存:

Process A
   ↓
Shared Memory
   ↑
Process B

重点是:

共享大量数据

而 Signal:

Process A
   ↓
SIGUSR1
   ↓
Process B

重点是:

事件通知

因此可以简单记:

Message Queue
→ 传消息

Shared Memory
→ 共享数据

Semaphore
→ 同步 / 互斥

Signal
→ 异步事件通知

三、Linux 中有哪些信号?

可以使用:

kill -l

查看当前系统支持的信号。

常见信号:

SIGHUP
SIGINT
SIGQUIT
SIGILL
SIGABRT
SIGFPE
SIGKILL
SIGSEGV
SIGPIPE
SIGALRM
SIGTERM
SIGCHLD
SIGCONT
SIGSTOP
SIGTSTP
SIGUSR1
SIGUSR2
...

不同 CPU 架构和系统环境中,部分信号编号可能存在差异。

因此:

程序设计中通常应该优先使用 SIGINT、SIGTERM 这样的符号名称,而不是死记数字。


四、信号的完整生命周期

一个信号从产生到最终处理,可以概括成:

Signal Generation
信号产生
      ↓
Signal Pending
信号未决
      ↓
检查 Signal Mask
      ↓
是否被 Block?
   ┌──┴──┐
  YES    NO
   │      │
   │      ▼
   │   Delivery
   │   信号递达
   │      ↓
   │   Disposition
   │   信号处理
   │
   └→ 暂时保持 Pending

因此最重要的几个概念:

Generation
→ 信号产生

Pending
→ 信号已经产生,但尚未完成递达处理

Blocked
→ 当前暂时阻塞该信号递达

Delivery
→ 信号真正递达目标执行流

Disposition
→ 信号递达后如何处理

五、Blocked 和 Ignored 不是一回事

这是原笔记中最需要纠正的一点。

不能写成:

“进程可以阻塞信号、忽略信号,导致信号无法抵达。”

因为:

Blocked

和:

Ignored

是两个完全不同的概念。


六、Blocked:阻塞信号

Blocked 表示:

这个信号暂时不要递达给我。

例如:

SIGINT 产生
   ↓
SIGINT 当前被 Block
   ↓
Pending
   ↓
等待解除 Block
   ↓
Delivery

因此 Block 通常是:

暂时延迟处理。

不是简单把信号删除。


七、Ignored:忽略信号

Ignored:

SIG_IGN

表示:

进程对这个信号的 disposition 设置为忽略。

即信号到来时:

Signal
  ↓
SIG_IGN
  ↓
不执行默认动作
不执行自定义 Handler

所以:

Blocked
→ 暂时不递达

Ignored
→ 处理策略就是忽略

二者不能混淆。


八、信号产生的常见方式

Signal 可以通过很多方式产生。

常见来源可以分为:

① 终端事件

② 硬件异常 / CPU Exception

③ Shell 命令

④ 程序调用系统接口

⑤ Kernel 事件

九、终端产生信号

例如:

Ctrl + C

通常由终端驱动向前台进程组发送:

SIGINT

即:

Ctrl + C
    ↓
Terminal Driver
    ↓
SIGINT
    ↓
Foreground Process Group

注意:

Ctrl+C 本身不是硬件直接向某个进程发送 SIGINT。

更准确地说,是终端输入机制识别控制字符,然后由内核终端驱动向前台进程组产生相应信号。


十、Ctrl + Z

例如:

Ctrl + Z

通常产生:

SIGTSTP

用于暂停前台作业。

例如:

sleep 100

然后:

Ctrl + Z

Shell 可能显示:

Stopped

十一、CPU / 硬件异常产生信号

例如程序非法访问内存:

int *p = NULL;
*p = 100;

可能导致:

Memory Access Fault
      ↓
Kernel
      ↓
SIGSEGV
      ↓
Process

又例如某些算术异常可能产生:

SIGFPE

因此信号也可以是:

Kernel 将同步异常转换成对进程的信号通知。


十二、kill 命令

Shell 中可以:

kill -TERM 1234

向 PID 1234 发送:

SIGTERM

例如:

kill -INT 1234

发送:

SIGINT

十三、kill 默认不是 SIGKILL

这是非常常见的误区。

执行:

kill 1234

默认发送:

SIGTERM

不是:

SIGKILL

只有:

kill -KILL 1234

或者常见写法:

kill -9 1234

才是发送:

SIGKILL

十四、kill() 函数

程序中可以调用:

#include <signal.h>

int kill(pid_t pid, int sig);

例如:

kill(1234, SIGTERM);

表示:

向指定目标发送 SIGTERM。

kill() 不仅能针对单个 PID,在不同 pid 参数形式下还可以作用于:

进程
进程组
调用者所在进程组
满足条件的一组进程

所以它比“给指定进程发送信号”这个描述更广。


十五、raise()

函数:

int raise(int sig);

例如:

raise(SIGUSR1);

用于:

向调用者自身产生指定信号。

可以简单理解:

Current Process / Thread
       │
       │ raise(SIGUSR1)
       ▼
     SIGUSR1
       │
       └────→ 自己

十六、alarm()

函数:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

例如:

alarm(5);

表示:

安排一个 alarm,在大约指定秒数之后向当前进程产生 SIGALRM。

所以:

alarm(5)
   ↓
约 5 秒以后
   ↓
SIGALRM
   ↓
Process

十七、alarm() 不等于“程序还能运行几秒”

你原来的描述:

alarm(5) 表示当前程序还能运行 5 秒。

不够准确。

准确来说:

alarm(5)

只是:

安排 5 秒后产生 SIGALRM。

程序最终会不会终止,要看:

SIGALRM 的 disposition

如果没有修改处理方式:

SIGALRM 默认动作
→ Terminate

所以看起来像:

5 秒后程序终止

但如果安装 Handler:

signal(SIGALRM, handler);

那么:

5 秒
 ↓
SIGALRM
 ↓
handler()

程序不一定终止。


十八、alarm() 的返回值

例如:

unsigned int left = alarm(5);

返回值并不是:

“新设置的 5 秒倒计时剩余时间。”

而是:

之前已经存在的 alarm 还剩多少秒。

例如:

alarm(10);

sleep(3);

unsigned int left = alarm(5);

第二次调用:

取消 / 替换原来的 10 秒 alarm

返回的 left 大约是:

7

然后重新安排:

新的 5 秒 alarm

十九、alarm(0)

执行:

alarm(0);

表示:

取消当前尚未触发的 alarm。

返回:

之前 alarm 剩余的秒数

所以:

alarm(N)
→ 设置 / 替换 alarm

alarm(0)
→ 取消 alarm

二十、pause()

函数:

int pause(void);

作用:

挂起当前调用线程,直到捕获到一个信号并且对应的信号处理函数返回,或者信号导致进程终止。

可以概念化:

Process
  │
  │ pause()
  ▼
Sleep / Wait
  │
  │ Signal
  ▼
Handler
  │
  ▼
pause() 被中断

如果信号 Handler 执行后返回:

pause()

返回:

-1

并通常设置:

errno = EINTR

二十一、signal()

可以使用:

#include <signal.h>

signal(SIGINT, handler);

修改进程对某个信号的 disposition。

例如:

void handler(int sig)
{
    write(STDOUT_FILENO, "SIGINT\n", 7);
}

int main(void)
{
    signal(SIGINT, handler);

    while (1)
        pause();
}

当:

Ctrl + C

产生 SIGINT 后:

SIGINT
 ↓
handler()

而不是执行 SIGINT 的默认终止动作。


二十二、信号的三种基本处理方式

一个信号递达以后,通常存在三种 disposition:

① Default Action

② Ignore

③ Catch / Handler

即:

SIG_DFL
→ 默认处理


SIG_IGN
→ 忽略


handler
→ 执行自定义 Signal Handler

二十三、默认动作

不同信号的默认动作不同。

可能包括:

Terminate
→ 终止进程

Core Dump
→ 终止并可能产生 core dump

Stop
→ 停止进程

Continue
→ 恢复进程

Ignore
→ 默认忽略

所以不能认为:

“收到 Signal 就一定终止。”


二十四、SIGKILL 和 SIGSTOP 很特殊

两个特别重要的信号:

SIGKILL

SIGSTOP

不能:

Catch
Block
Ignore

也就是说,程序不能安装 Handler 来“拦住”它们。

例如:

kill -9 PID

发送:

SIGKILL

目标进程无法通过普通 signal handler 拒绝这个信号。


二十五、signal() 与 sigaction()

虽然:

signal()

简单方便,但现代 POSIX 程序通常更推荐:

sigaction()

因为它能够更精确地控制:

Signal Handler
Signal Mask
Flags
Signal Behavior

例如:

struct sigaction sa = {0};

sa.sa_handler = handler;

sigemptyset(&sa.sa_mask);

sigaction(SIGINT, &sa, NULL);

因此可以记:

signal()
→ 简单接口

sigaction()
→ 更完整、更可控的 POSIX 信号处理接口

二十六、Signal Set

Linux/POSIX 提供:

sigset_t

表示:

Signal Set(信号集合)。

它可以包含:

SIGINT
SIGTERM
SIGUSR1
SIGUSR2
...

注意:

sigset_t 本身只是一个“信号集合数据结构”。

创建一个集合:

sigset_t set;

此时通常还需要初始化。


二十七、sigemptyset()

函数:

sigemptyset(&set);

作用是:

把 set 这个信号集合初始化为空集合。

不是:

“把当前进程的信号集合清空。”

这是原笔记中的重要错误。

即:

sigemptyset(&set)
       ↓
修改变量 set

并不会直接修改当前进程/线程真正的 Signal Mask。


二十八、sigfillset()

如果:

sigemptyset(&set);

表示:

set = {}

那么:

sigfillset(&set);

表示:

将可表示的信号加入这个集合。

概念上:

set = {很多可表示 Signal}

二十九、sigaddset()

向集合中加入信号:

sigaddset(&set, SIGINT);

例如:

sigset_t set;

sigemptyset(&set);

sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);

得到概念上的:

set = {
    SIGINT,
    SIGTERM
}

三十、sigdelset()

删除:

sigdelset(&set, SIGINT);

三十一、sigismember()

判断:

sigismember(&set, SIGINT);

可以检查:

SIGINT 是否存在于 set

三十二、Signal Mask

每个执行线程都有:

Signal Mask

也叫:

Blocked Signal Mask

用于决定:

哪些信号暂时不允许递达。

例如:

Signal Mask

SIGINT   → Blocked
SIGUSR1  → Blocked
SIGTERM  → Unblocked

如果 SIGINT 此时产生:

SIGINT
 ↓
Blocked?
 ↓ YES
Pending

等解除阻塞以后才有机会递达。


三十三、sigprocmask()

单线程程序中经常使用:

sigprocmask()

修改调用线程的 Signal Mask。

函数:

int sigprocmask(
    int how,
    const sigset_t *set,
    sigset_t *oldset
);

例如:

sigprocmask(
    SIG_BLOCK,
    &set,
    NULL
);

三十四、SIG_BLOCK

例如:

sigemptyset(&set);
sigaddset(&set, SIGINT);

sigprocmask(
    SIG_BLOCK,
    &set,
    NULL
);

表示:

将 set 中的信号加入当前 Signal Mask。

因此:

SIGINT
→ Blocked

三十五、SIG_UNBLOCK

解除:

sigprocmask(
    SIG_UNBLOCK,
    &set,
    NULL
);

表示:

从 Signal Mask 中移除 set 中指定的信号。

如果 SIGINT 此时已经:

Pending

那么解除阻塞后:

SIGINT
   ↓
可以 Delivery

三十六、SIG_SETMASK

例如:

sigprocmask(
    SIG_SETMASK,
    &set,
    NULL
);

表示:

直接使用 set 替换当前的 Signal Mask。

因此三种操作:

SIG_BLOCK
→ 添加阻塞项

SIG_UNBLOCK
→ 删除阻塞项

SIG_SETMASK
→ 整体替换 Mask

所以不能简单说:

“sigprocmask 就是把自定义集合替换系统集合。”

只有:

SIG_SETMASK

才是替换。


三十七、查看 Pending Signals

可以使用:

sigpending()

例如:

sigset_t pending;

sigpending(&pending);

然后:

sigismember(
    &pending,
    SIGINT
);

判断:

SIGINT 是否处于 Pending 状态

三十八、普通信号为什么可能“合并”?

传统标准信号通常:

不排队保存每一次重复发生。

例如:

SIGINT 被 Block

此时连续产生:

SIGINT
SIGINT
SIGINT
SIGINT
SIGINT

Pending 状态可能只是:

SIGINT = Pending

而不是:

SIGINT Pending Count = 5

因此解除阻塞后,通常不会因为期间发生 5 次就保证调用 Handler 5 次。

可以理解:

普通 Signal Pending
更像:

“这个信号至少发生过一次”

而不是:

“这个信号精确发生了 N 次”

三十九、不要简单称现代 Linux 普通信号为“不可靠信号”

历史 Unix 文献中确实存在:

Reliable Signals
Unreliable Signals

这样的术语。

但是在现代 POSIX/Linux 中,更准确的学习方式是区分:

Standard Signals
        vs
Real-Time Signals

核心差异之一就是:

Standard Signal
→ 同一种 Pending Signal 通常不排队


Real-Time Signal
→ 支持排队

因此比简单说:

1~31 不可靠
34~64 可靠

更加准确。


四十、Standard Signals

Linux 中传统标准信号包括:

SIGINT
SIGTERM
SIGSEGV
SIGCHLD
SIGUSR1
SIGUSR2
...

它们的重要特点之一:

同一种标准信号在尚未递达期间重复产生时,通常不会为每一次都保存独立队列项。

例如:

Block SIGUSR1

kill -USR1 PID
kill -USR1 PID
kill -USR1 PID
kill -USR1 PID

Unblock SIGUSR1

不能期待 Handler:

一定执行 4 次

四十一、Real-Time Signals

Linux/POSIX 还提供:

Real-Time Signals

范围通常使用:

SIGRTMIN
~
SIGRTMAX

而不是在程序中硬编码:

34
~
64

原因之一是:

实际可用实时信号编号范围可能受到 libc / threading implementation 等因素影响。

因此程序应该:

SIGRTMIN
SIGRTMIN + 1
...
SIGRTMAX

而不是死记固定数字。


四十二、Real-Time Signal 支持排队

例如某个 Real-Time Signal 被阻塞期间产生:

RT Signal
RT Signal
RT Signal
RT Signal

它们可以排队:

Pending Queue

RT Signal
RT Signal
RT Signal
RT Signal

解除阻塞以后,可以按相应规则依次递达。

因此:

Standard Signals
→ 同种信号通常不排队

Real-Time Signals
→ 可以排队

这是最重要的区别之一。


四十三、sigqueue()

实时信号还可以配合:

sigqueue()

发送额外数据。

例如:

union sigval value;

value.sival_int = 100;

sigqueue(
    pid,
    SIGRTMIN,
    value
);

接收端配合:

sigaction
+
SA_SIGINFO

可以取得发送者和附带值等信息。

所以 Real-Time Signal 比普通 Signal 提供了更丰富的排队和信息传递能力。


四十四、Signal Handler 是异步执行的

假设程序正在:

main()
   ↓
funcA()
   ↓
funcB()

突然 SIGINT 递达:

funcB()
  │
  │ Signal
  ▼
Signal Handler
  │
  │ return
  ▼
恢复原来的执行流

因此 Signal Handler:

可能在程序正常执行过程中的某个时刻异步插入执行。

这也是信号编程比较复杂的原因。


四十五、Signal Handler 里不能随便调用所有函数

由于 Handler 是异步执行的,所以:

不是所有 libc 函数都可以安全地在 Signal Handler 中调用。

例如很多复杂库函数不是:

Async-Signal-Safe

因此 Handler 中应该尽量只调用 POSIX 明确保证 async-signal-safe 的函数。

例如常见:

write()

而不要想当然地在 Handler 中大量执行:

printf()
malloc()
free()

等复杂操作。

一个常见设计是:

Signal Handler
      ↓
只做最少工作
      ↓
设置 flag / write pipe
      ↓
正常程序逻辑再处理真正任务

四十六、信号与系统调用 EINTR

假设进程正在阻塞:

read()

此时信号递达并执行 Handler。

某些情况下系统调用可能:

被 Signal 中断

然后返回:

-1

并设置:

errno = EINTR

因此 Linux 系统编程中经常看到:

if (errno == EINTR) {
    ...
}

sigaction() 中的某些 flags,例如:

SA_RESTART

还会影响部分系统调用在信号之后是否自动重新开始。


四十七、进程和线程中的 Signal

到了多线程程序,信号模型会更加复杂。

因为信号存在:

Process-Directed Signal

Thread-Directed Signal

有些信号针对整个进程产生,然后由符合条件的线程接收。

有些信号则明确针对某个线程。


四十八、每个线程有自己的 Signal Mask

在多线程程序中:

每个线程拥有自己的 Signal Mask。

例如:

Process

Thread A
Mask:
SIGUSR1 Blocked


Thread B
Mask:
SIGUSR1 Unblocked

因此进程级 SIGUSR1 到来时,内核会根据相关规则选择能够接收它的线程。


四十九、pthread_sigmask()

多线程 POSIX 程序中,通常使用:

pthread_sigmask()

来管理线程的 Signal Mask。

例如:

pthread_sigmask(
    SIG_BLOCK,
    &set,
    NULL
);

而不是把多线程场景简单理解为:

整个进程只有一个统一 Signal Mask

五十、同步等待信号

除了安装异步 Handler,还可以:

sigwait()

同步等待指定信号。

例如多线程服务器中一种常见设计:

所有工作线程
    ↓
Block 某些 Signal

专门的 Signal Thread
    ↓
sigwait()
    ↓
集中处理信号

这样可以减少复杂的异步 Signal Handler 逻辑。


五十一、Signal 和条件变量不是一回事

你原笔记最后写:

“线程 A 不满足条件就等待,线程 B 满足条件以后通知 A,这就是条件变量。”

这个概念本身基本方向正确,但它属于:

线程同步

而不是本章 Unix Signal。

条件变量:

pthread_cond_t

通常和:

pthread_mutex_t

配合使用。

例如:

Thread A
   │
条件不满足
   ↓
pthread_cond_wait()
   ↓
Wait


Thread B
   │
修改共享状态
   ↓
pthread_cond_signal()
   ↓
Wake Thread A

这里的:

pthread_cond_signal()

虽然函数名也有 signal:

但它和 Unix/Linux 的 SIGINT、SIGTERM、SIGUSR1 等 Signal 完全不是同一种机制。


五十二、Signal 与 Condition Variable 对比

Unix Signal

SIGINT
SIGTERM
SIGUSR1
SIGALRM
...

用途:
进程 / 线程异步事件通知

API:
kill()
raise()
sigaction()
sigprocmask()
sigwait()
...


Condition Variable

pthread_cond_t

用途:
线程之间等待某个共享状态变化

API:
pthread_cond_wait()
pthread_cond_signal()
pthread_cond_broadcast()

一定不要因为:

signal

这个英文单词相同,就把两套机制混在一起。


五十三、条件变量的正确基本模型

假设:

ready = false

Thread A:

lock(mutex)

while (!ready)
    pthread_cond_wait(...)

处理数据

unlock(mutex)

Thread B:

lock(mutex)

ready = true

pthread_cond_signal(...)

unlock(mutex)

这里:

Condition Variable

本身不是:

“保存条件”。

真正的条件通常是:

ready
queue_not_empty
buffer_has_space
...

Condition Variable 的作用是:

让线程高效地等待共享状态发生变化,并由其他线程通知等待者重新检查条件。

这应该单独放在线程同步章节学习。


五十四、常用 Signal 总结

SIGINT
→ Ctrl+C 常见产生的中断信号

SIGTERM
→ 请求进程正常终止
→ kill 默认发送

SIGKILL
→ 强制终止
→ 不能 catch / block / ignore

SIGSTOP
→ 强制停止
→ 不能 catch / block / ignore

SIGTSTP
→ Ctrl+Z 常见产生

SIGCONT
→ 继续执行

SIGSEGV
→ 非法内存访问等导致

SIGALRM
→ alarm() 定时到期

SIGCHLD
→ 子进程状态发生相关变化时通知父进程

SIGUSR1
SIGUSR2
→ 用户程序自定义用途

五十五、Signal API 总结

最值得记住:

signal()
→ 简单修改 Signal Disposition


sigaction()
→ 推荐的、更完整的 Signal Handler 配置


kill()
→ 向进程 / 进程组等目标发送 Signal


raise()
→ 给自己产生 Signal


alarm()
→ 定时产生 SIGALRM


pause()
→ 等待 Signal


sigemptyset()
→ 初始化空 Signal Set


sigfillset()
→ 初始化满 Signal Set


sigaddset()
→ 加入 Signal


sigdelset()
→ 删除 Signal


sigismember()
→ 判断 Signal 是否在 Set


sigprocmask()
→ 修改 Signal Mask


sigpending()
→ 查看 Pending Signal


sigwait()
→ 同步等待 Signal


pthread_sigmask()
→ 多线程程序管理线程 Signal Mask

五十六、信号状态完整模型

可以把一个信号理解成:

            Signal Generated
                   │
                   ▼
              Is Blocked?
              /          \
            YES           NO
             │             │
             ▼             ▼
          Pending       Delivery
             │             │
             │             ▼
             │        Disposition
             │         /    |    \
             │        /     |     \
             │       ▼      ▼      ▼
             │    Default Ignore Handler
             │
             │
      Signal Unblocked
             │
             └────────────→ Delivery

其中:

Standard Signal
→ 同一种 Pending Signal 通常不会逐次排队

Real-Time Signal
→ 支持排队

五十七、Linux Signal 的整体知识体系

最终可以整理成:

Linux Signal
│
├── Signal Generation
│   ├── Terminal
│   ├── Hardware / CPU Exception
│   ├── Kernel Event
│   ├── kill()
│   ├── raise()
│   └── alarm()
│
├── Signal State
│   ├── Pending
│   ├── Blocked
│   └── Delivered
│
├── Signal Disposition
│   ├── SIG_DFL
│   ├── SIG_IGN
│   └── Handler
│
├── Signal Set
│   ├── sigemptyset()
│   ├── sigfillset()
│   ├── sigaddset()
│   └── sigdelset()
│
├── Signal Mask
│   ├── SIG_BLOCK
│   ├── SIG_UNBLOCK
│   └── SIG_SETMASK
│
├── Standard Signals
│   └── 同种 Pending 通常不排队
│
└── Real-Time Signals
    └── 支持排队

五十八、一句话总结

Linux Signal 是一种异步事件通知机制。信号产生后,如果当前被阻塞,则会进入 Pending 状态,待解除阻塞后再递达;信号递达后,根据该信号的 disposition 执行默认动作、忽略或调用自定义 Handler。sigaction() 用于配置更可靠、可控的信号处理方式,sigset_t 用于描述信号集合,sigprocmask() / pthread_sigmask() 用于管理阻塞掩码。传统 Standard Signal 的同种未决实例通常不会逐次排队,而 Real-Time Signal 支持排队。SIGKILL 和 SIGSTOP 是特殊信号,不能被捕获、阻塞或忽略。

posted on 2026-10-01 08:24  心宇16969  阅读(23)  评论(0)    收藏  举报