Linux 内核 ERESTARTSYS 与可重启系统调用:从信号中断到 Restart Block 的深度工程剖析
引言:一个被工程师忽视 20 年的核心机制
假设你正在编写一个高性能网络服务器,使用 epoll_wait() 等待事件。此时一个 SIGALRM 信号到达——你的进程被中断,但 epoll_wait 竟然"自动恢复"了!你知道这是怎么发生的吗?如果换成 futex(FUTEX_WAIT),同样的场景结果可能完全不同。为什么?
这背后的核心机制就是 Linux 内核的 可重启系统调用(restartable syscalls),它通过 ERESTARTSYS 错误码及一系列相关的 restart 语义,优雅地协调了"系统调用中途被信号打断"和"信号处理完后应该接着干什么"之间的矛盾。这个机制自 Linux 2.6 时代引入,至今仍然是内核中最容易被误解、也最容易制造 Heisenbug 的子系统之一。
本文将从内核源码出发,完整拆解:
- 信号中断系统调用时内核的 restart 决策链路
- ERESTARTSYS / ERESTARTNOINTR / ERESTARTNOHAND 三种语义的精确差异
- Restart Block 机制在 nanosleep、clock_nanosleep 等场景中的精确恢复
- SA_RESTART flag 如何与内核 restart 协同工作
- 常见 ERESTARTSYS Heisenbug 的根因模式与调试技巧
- 在 io_uring、eBPF、容器场景下 restart 行为的新变化
一、信号打断系统调用时发生了什么
1.1 系统调用路径上的信号检查
理解 restart 机制的第一步,是理解系统调用路径上信号何时被处理。在 Linux x86-64 架构下,系统调用从 entry_SYSCALL_64 进入内核,其最简略的伪代码路径如下:
entry_SYSCALL_64:
; 1. 保存用户态寄存器到 pt_regs
; 2. 调用对应的 sys_xxx() 处理函数
; 3. syscall_exit_to_user_mode_work() — 此处检查 _TIF_WORK_MASK
; ├─ _TIF_NEED_RESCHED → schedule()
; ├─ _TIF_SIGPENDING → do_signal()
; ├─ _TIF_UPROBE → uprobe_notify_resume()
; └─ ...
; 4. 恢复到用户态
关键点在第 3 步。内核不会在每个系统调用执行中间检查信号(那样性能太糟),而是在"即将返回用户态"这个边界上进行 pending signal 检查。如果在系统调用阻塞期间pending了一个信号,控制权转移到 do_signal()。
1.2 do_signal 如何干预系统调用返回值
do_signal() 执行后,如果信号没有终止进程(即不是 SIGKILL / SIGSTOP / SIGSEGV 等致命信号),它就面临一个关键问题:刚才在跑的系统调用怎么办?
这时,取决于具体系统调用的实现,内核会做三件事之一:
- 直接返回
ERESTARTSYS(值为 512):告诉用户态"请检查信号,处理完后把这个系统调用重新执行一遍"。 - 返回
EINTR(值为 4):告诉用户态"系统调用被信号打断了,自己做决定"。 - 不做任何特殊事情:系统调用已经成功完成或返回非 EINTR 错误(系统调用实际上不受影响)。
二、ERESTARTSYS 家族的三种语义
当系统调用路径上的 do_signal() 发现系统调用需要重启时,它会设置返回值码。这个码决定了重启的方式:
| 错误码十进制值 | 宏名称 | 含义 | 典型使用场景 |
|---|---|---|---|
| 512 | ERESTARTSYS |
用户态应重启系统调用(仅当 SA_RESTART 时自动重启) | read, write, wait4, futex, nanosleep |
| 513 | ERESTARTNOINTR |
无条件重启系统调用,用户态看到的是重新启动 | restart_syscall() 专用 |
| 514 | ERESTARTNOHAND |
除非用户态显式注册 SA_RETERM,否则重启 | pause, rt_sigsuspend, sigtimedwait |
2.1 ERESTARTSYS:最高频、最 tricky
这是最常见的 restart 码。内核实际传入系统用户态的 rax 寄存器值为 -ERESTARTSYS(即 -512)。在 glibc 中,这一层封装的 syscall wrapper 会做如下处理:
// glibc internal_syscall6 的简化逻辑
long result = raw_syscall(...);
if (result == -ERESTARTSYS) {
if (sigaction_with_SA_RESTART) {
// 直接恢复用户态到系统调用入口,重新执行 syscall 指令
regs->pc = syscall_instruction_address;
return; // 用户态不会感觉到这次中断
} else if (signal_handler_logged) {
// 信号的 handler 已经被调用了,但我们仍要重启
// 这会导致特殊语义:handler 执行完后,系统调用返回 -EINTR
// 但同时设置 TIF_NEED_RESCHED / TIF_SIGPENDING,让下次系统调用时再试
return -EINTR;
} else {
return -EINTR;
}
}
2.2 ERESTARTNOINTR:用户态不可见的 restart
ERESTARTNOINTR 是"不应该让用户态看到中断"的语义。典型场景:pause() 系统调用,它的语义就是"被信号唤醒"。在旧版内核中,pause() 使用 schedule() 让 CPU,如果被信号打断,内核直接设置 rax = -ERESTARTNOINTR,然后在返回用户态前自动恢复系统调用状态。
效果:用户态永远看不到 pause() 执行了一半被信号打断,它只会看到 pause() 返回 -1 且 errno=EINTR(因为 pause 被信号唤醒本身就是要结束它)。
2.3 ERESTARTNOHAND:SA_RESTART 的另一种表达
ERESTARTNOHAND 等价于 ERESTARTSYS,但它还有一个附加语义:如果 handler 执行期间又收到了另一个信号,当前 handler 本身也要返回后重新开始。它比 ERESTARTSYS 更"可重试"。
三、Restart Block:精确的系统调用时间恢复
3.1 问题的引入:nanosleep 为什么需要完整重启?
假设有以下调用:
// 要求睡眠 10 秒
struct timespec req = { .tv_sec = 10, .tv_nsec = 0 };
nanosleep(&req, &rem);
如果睡眠进行到第 2 秒时收到 SIGALRM,信号处理完后我们希望:
不是从头开始睡 10 秒,而是把剩余的 8 秒补回来!
这就是 Restart Block 机制。它存储了系统调用原始的精确的剩余参数,以便在重启时继续工作而非从头开始。
3.2 内核实现:restart_block 结构
在 Linux 5.0+ 内核中,每个 task_struct 增加了一个 restart_block 结构:
// include/linux/restart_block.h
struct restart_block {
long (*fn)(struct restart_block *);
union {
struct {
u64 exponent;
u64 remainder;
} futex;
struct {
clockid_t which_clock;
int flags;
u64 expires;
u64 remaining;
} nanosleep;
struct {
int nr_segments;
struct iovec *iovs;
} ...;
};
};
对于 nanosleep(),内核在 hrtimer_nanosleep() 中设置:
// kernel/time/hrtimer.c
long hrtimer_nanosleep(struct timespec64 *rqtu)
{
struct restart_block *restart = ¤t->restart_block;
restart->nanosleep.expires = hrtimer_get_expire(...);
restart->fn = hrtimer_nanosleep_restart;
// 进入 hrtimer 等待...
// 如果被信号打断,设置返回值为 -ERESTART_RESTARTBLOCK
// 而不是普通的 -ERESTARTSYS
}
3.3 ERESTART_RESTARTBLOCK:"特殊重启"标记
当内核决定使用 restart_block 时:
- 系统调用返回
-ERESTART_RESTARTBLOCK(值是 516) - glibc/POSIX 层识别这个码,调用
restart_syscall()(系统调用号随架构不同) restart_syscall()恢复current->restart_block.fn等参数,重新进入内核- 内核调用
restart->fn(restart),例如hrtimer_nanosleep_restart(),从剩余时间开始继续睡
这种机制确保了 nanosleep、clock_nanosleep、poll(ppoll)、epoll_pwait、futex(FUTEX_WAIT) 等长时间阻塞调用在被信号中断后能够精确恢复剩余时间。
3.4 用户态看不到 restart_block 的奥秘
用户态程序通常不需要感知 restart_block 的存在——glibc 通过 restart_syscall 系统调用自动帮你处理了。但在以下场景中会出问题:
- 直接内联
syscall指令:如果你的程序不走 glibc 直接内联 syscall(比如某些 Rust runtime),你需要自己处理ERESTART_RESTARTBLOCK。 - io_uring 与 futex:当 io_uring 使用
IORING_OP_FUTEX_WAIT时,restart 机制不走用户态 syscall 路径,完全由内核处理。 - vDSO 中的 time calls:
clock_gettime()等调用走 vDSO(用户态直接读内存页),不经过内核,自然也不会出现 ERESTARTSYS。
四、SA_RESTART 标志:用户态与内核的协议
4.1 sigaction 的 SA_RESTART
当用户通过 sigaction() 注册信号处理函数时,可以设置 SA_RESTART 标志(signal(7) 的手册中标注为"restart"):
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sigaction(SIGALRM, &sa, NULL);
这个标志的实质作用:告诉内核"当我返回信号处理函数后,如果系统调用被 ERESTARTSYS 标记了,请自动恢复它,不要让我看到 EINTR"。
4.2 哪些系统调用默认有 SA_RESTART?
signal(7) 手册列出了影响系统调用的完整分类:
| 类别 | 行为 | 系统调用 |
|---|---|---|
| 自动重启(如果 SA_RESTART) | 如果设置了 SA_RESTART,内核自动重启;否则返回 EINTR | read, write, wait4, poll, ioctl 等 |
| 永不返回 EINTR | 系统调用不可被信号中断 | getpid, gettimeofday, clock_gettime(vDSO) |
| 总是返回 EINTR | SA_RESTART 对它们无效 | pause, sigsuspend, sigwaitinfo |
| 由 restart_block 恢复 | 用户态通过 restart_syscall() 恢复 |
nanosleep, clock_nanosleep, ppoll, pselect |
4.3 一个最容易踩坑的场景:ppoll 与 epoll_pwait
让我们看一个真实场景:
// 场景:我们想等待一个 fd 可读,同时设置了 SIGCHLD handler + SA_RESTART
struct pollfd pfd = { .fd = fd, .events = POLLIN };
struct timespec timeout = { .tv_sec = 5, .tv_nsec = 0 };
// 用 ppoll(而不是 poll)—— 它支持精确的信号掩码控制
int ret = ppoll(&pfd, 1, &timeout, &orig_mask);
如果此时 SIGCHLD 到达:
- ppoll 会被信号中断,设置 restart_block。
- 内核返回 -ERESTART_RESTARTBLOCK。
- 如果设置了 SA_RESTART,glibc 调用 restart_syscall(),恢复剩余 timeout。
- 如果没设置 SA_RESTART,glibc 返回 -1、errno=EINTR。
注意:这是 ppoll vs poll 的核心区别。poll 不支持精确恢复剩余时间(每次重启都是重新等 5 秒),而 ppoll 通过 restart_block 精确恢复。
五、Futex 的 restart 语义:一个独立于信号的世界
5.1 futex(FUTEX_WAIT) 的特殊性
futex 是最基本的内核同步原语,FUTEX_WAIT 的语义是"等待某个 uaddr == val"。它的 restart 行为不遵守 SA_RESTART 规则:
// FUTEX_WAIT 被信号打断时:
// 1. 信号被递送,handler 执行
// 2. futex 返回 -EINTR(不是 -ERESTARTSYS)
// 即使设置了 SA_RESTART,FUTEX_WAIT 也返回 -EINTR
原因:futex 的 FUTEX_WAIT 语义要求用户态在返回后重新检查 uaddr 的值。因为返回时 uaddr 可能已经改变了(另一个线程执行了 FUTEX_WAKE),简单重启 FUTEX_WAIT 的等待逻辑是错误的。
5.2 glibc mutex 如何处理 EINTR
C11 的 mtx_lock() 和 pthreads 的 pthread_mutex_lock() 内部调用 futex(FUTEX_WAIT)。当它返回 -EINTR 时,glibc 的 NPTL 实现自动重试(忽略 -EINTR,因为 FUTEX_WAIT 在成功时绝不会返回 EINTR——要么唤醒,要么真出错)。
但如果你在手工编写 futex-based lock,你需要:
int ret;
do {
ret = futex_wait(uaddr, expected);
} while (ret == -1 && errno == EINTR);
// 因为 futex 的 FUTEX_WAIT 只要返回了就是:要么 WAKE,要么 EINTR
// (不会因竞争失败返回,只有因为信号返回)
5.3 FUTEX_LOCK_PI / FUTEX_LOCK_ROBUST
FUTEX_LOCK_PI(优先级继承锁)和 robust futex 有不同的 restart 语义:
FUTEX_LOCK_PI:在旧内核中完全不响应信号(永远阻塞,不能中断)。Linux 5.14+ 允许通过FUTEX_LOCK_PI2实现可中断。FUTEX_WAIT_REQUEUE_PI:用于将 lock 从普通 mutex 重新排队到 PI mutex,它的 restart 行为取决于内核版本。
六、常见 Heisenbug 模式与调试技巧
6.1 Bug 模式一:意外丢失 EINTR 导致 IO 饥饿
// 错误代码:包装了 read() 的 safe_read 函数
ssize_t safe_read(int fd, void *buf, size_t len) {
ssize_t n;
do {
n = read(fd, buf, len); // 假设 read 被设置了 SA_RESTART
} while (n == -1 && errno == EINTR);
return n;
}
// 问题:如果 read 是慢IO(如终端、管道、Unix socket),且设置了 SA_RESTART,
// read 在被信号打断时会自动重启,返回的不是 -1/EINTR,而是成功返回部分数据。
// 此时 safe_read 的 "重试" 逻辑永远不会触发——但如果信号发生足够频繁,
// 整个应用会因为 read 返回 0(因为实际读到了 EOF 但用户以为被信号打断)
// 而陷入"假 EOF"。
修复方案:如果明确需要打断 read,不应依赖 SA_RESTART,而是使用 sigtimedwait + EINTR 显式处理打断。
6.2 Bug 模式二:nanosleep 的"虚假重启"导致 CPU Burn
// 场景:高精度精确定时器
void timer_loop() {
struct timespec sleep_time = {0, 1000000}; // 1ms
while (running) {
do_work();
nanosleep(&sleep_time, NULL); // 没有设置 SA_RESTART
}
}
// 问题:如果没有设置 SA_RESTART,每次信号到来后 nanosleep 返回 -EINTR
// 循环立即重启,导致 do_work() 被连续调用数万次/秒,CPU 爆炸。
修复方案:需要判断是否严格需要绝对时间语义,如果不需要,可以接受 sleep 被缩短;如果需要,设置 SA_RESTART 或使用 clock_nanosleep 的绝对时间模式。
6.3 Bug 模式三:多线程 + ERESTARTSYS 的 race condition
// 线程 A 正在执行一个 SA_RESTART system call:read(fd)
// 线程 B 通过 pthread_kill(A, SIGUSR1) 发送信号
//
// 线程 A 的 read 被打断,内核检查 current->restart_block
// 如果此时线程 A 的 fd 已经被线程 C close() 了...
// 重启 read 时 fd 已失效 -> EBADF
修复方案:多线程场景中不要混用 SA_RESTART 和 close() + 共享 fd。使用 dup() 或 epoll + shutdown 模式。
6.4 调试技巧一:strace 观察 restart 行为
strace 是观察 ERESTARTSYS 最锐利的工具:
strace -e trace=nanosleep,ppoll,read -p <PID>
典型的输出对比:
# 自动重启(有 SA_RESTART):
ppoll([{fd=3, events=POLLIN}], 1, {tv_sec=10, tv_nsec=0}, NULL, 8) = ? ESTART_RESTARTBLOCK (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, ...} ---
restart_syscall(<... resuming interrupted ppoll ...>) = 0
# 无自动重启(无 SA_RESTART):
ppoll([{fd=3, events=POLLIN}], 1, {tv_sec=10, tv_nsec=0}, NULL, 8) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, ...} ---
ppoll(...) = -1 EINTR (Interrupted system call)
注意看 restart_syscall 的出现与否——这就是自动重启的标志。
6.5 调试技巧二:通过 /proc//syscall 实时观察
# 查看当前系统调用状态
cat /proc/<pid>/syscall
# 输出:1 0x3 0x7f.... 0x1 0x0 0x0 ...
# 含义:syscall_number arg0 arg1 ...
# 当处于 ERESTARTSYS 状态时,这里会显示正在被重启的系统调用信息
七、容器与 ERESTARTSYS:新场景下的复杂交互
7.1 PID Namespace 对信号递送的影响
在容器化场景中,SIGKILL、SIGSTOP 仍有特权,但普通信号的递送受 pid namespace 和 seccomp 双重约束:
- 内核路径
do_signal()中,signal delivery 的 namespace 检查发生在prepare_signal()阶段。 - 如果容器内进程发送了它认为"合适"的信号,但 namespace 映射失败,可能导致进程被"静默跳过"信号。
- 对方观察到的现象:系统调用没有被信号打断——所以 restart 机制"永远不会被触发"。
7.2 Seccomp 会对系统调用返回值做什么?
SECCOMP_RET_TRACE 和 SECCOMP_RET_TRAP 拦截系统调用时,会替换返回值。对于 restart 语义的影响:
SECCOMP_RET_TRAP:默认发送SIGSYS+ 系统调用执行不会发生。因此没有 restart 问题。SECCOMP_RET_TRACE:tracer 可以修改系统调用号和参数。如果 tracer 决定"模拟"某个行为,它需要自己处理 restart 语义——这是大部分 seccomp tracer 没有正确实现的点。SECCOMP_RET_ERRNO:直接返回指定 errno,系统调用不会被执行。
7.3 io_uring 中的 restart
当 io_uring 在 SQPOLL 模式下执行,io_uring 线程会遇到自己的 restart 行为:
IORING_OP_LINK_TIMEOUT内部使用内核hrtimer+ktimeout,被 signal 中断时可能触发ERESTARTSYS。- 在
IORING_SETUP_SQPOLL下,内核线程处理 SQE 时如果是futex_wait_event且被信号打断,会自动重启(因为 kernel thread 没有用户态 SA_RESTART 的概念)。
八、内核源码精读:restart 决策路径
8.1 ksuspend 中的 restart_block 实现
以 hrtimer_nanosleep 为例,看内核如何设置 restart_block:
// kernel/time/hrtimer.c
static long hrtimer_nanosleep_restart(struct restart_block *restart)
{
enum hrtimer_mode mode = HRTIMER_MODE_ABS;
return __hrtimer_nanosleep(&restart->nanosleep.expires,
mode, restart->nanosleep.which_clock);
}
long hrtimer_nanosleep(ktime_t expires, const enum hrtimer_mode mode,
const clockid_t clockid)
{
struct restart_block *restart = ¤t->restart_block;
restart->nanosleep.expires = expires;
restart->nanosleep.which_clock = clockid;
restart->fn = hrtimer_nanosleep_restart;
// 进入 hrtimer 阻塞等待
// ...
// 如果被信号打断(in case of signal arrival):
if (!task_state_alive(current)) {
return -ERESTARTNOHAND; // 不应该发生:连信号都没有收到
}
// 标准路径:返回给用户的 ERESTART_RESTARTBLOCK
// 表明需要从 restart_block.fn 恢复
return -ERESTART_RESTARTBLOCK;
}
8.2 pthread_cond_wait 如何实现"不可中断的等
pthread_cond_wait() 底层的 futex 调用故意设置 ~SA_RESTART,这是因为它被要求返回 EINTR 时 pthread 自身不允许重启。
// nptl/pthread_cond_wait.c (glibc)
// 内部使用 futex_wait_bitset 等系统级原语
// 这些系统调用被内核标记为"不可中断"类别
九、实战场景:一个正确的可重入信号处理框架
下面给出一个生产级别的处理框架,正确应对所有 restart 场景:
// 处理信号驱动的应用框架
#include <signal.h>
#include <time.h>
#include <poll.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
static volatile sig_atomic_t g_signaled = 0;
static volatile sig_atomic_t g_sig_code = 0;
void signal_handler(int sig) {
g_signaled = 1;
g_sig_code = sig;
// 注意:signal_handler 中不执行复杂操作!
// 特别注意:不要调用非 async-signal-safe 函数(如 fprintf, malloc)
}
// 封装一个支持超时 + 精确恢复的 poll_wait
int poll_wait_with_timeout(int fd, int events, int timeout_ms) {
struct pollfd pfd = { .fd = fd, .events = events };
struct timespec ts = {
.tv_sec = timeout_ms / 1000,
.tv_nsec = (timeout_ms % 1000) * 1000000L
};
int saved_errno;
int ret;
do {
// 使用 ppoll 自动利用 restart_block 精确恢复剩余时间
ret = ppoll(&pfd, 1, &ts, NULL);
saved_errno = errno;
// 检查是否因信号打断
if (ret == -1 && errno == EINTR) {
if (g_signaled) {
// 可以选择立即退出或继续等待
// 这里演示"信号后继续等待"
g_signaled = 0;
// 重新计算剩余时间
// (实际场景中需要记录起始时间)
}
}
} while (ret == -1 && errno == EINTR);
return ret;
}
// nanosleep 的精确版本:即使 EINTR 也能睡满指定时长
int robust_nanosleep(time_t sec, long nsec) {
struct timespec req = { .tv_sec = sec, .tv_nsec = nsec };
struct timespec rem;
int ret;
do {
ret = clock_nanosleep(CLOCK_MONOTONIC, 0, &req, &rem);
// CLOCK_MONOTONIC 不 care SA_RESTART,直接返回 EINTR
if (ret == EINTR) {
// 信号导致中断,重新睡剩余部分
req = rem;
}
} while (ret == EINTR);
return (ret == 0) ? 0 : -1;
}
int main() {
// 注册 SIGINT/SIGTERM 的处理函数(不带 SA_RESTART,以便我们能主动决定重启)
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler;
sa.sa_flags = SA_RESTART; // 我们设置 SA_RESTART,让大部分调用自动重启
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
// 但 SIGIO(或 SIGALRM)我们不设置 SA_RESTART,让它手动控制
// ...
return 0;
}
十、前沿:restart 机制在子系统中的演进
10.1 io_uring 的 async_cancel
io_uring 引入了自己的异步取消语义(IORING_ASYNC_CANCEL),它提供了比 ERESTARTSYS 更细粒度的控制:
IORING_ASYNC_CANCEL_FD:取消所有针对特定 fd 的 opIORING_ASYNC_CANCEL_ANY:取消所有 opIORING_ASYNC_CANCEL_FD_FIXED:针对注册文件的取消
io_uring 的 cancel 在底层调用 io_cancel_cb(),遍历 io-wq worker 和 uring wait queues。对于正常的系统调用 restart 机制,io_uring 是 "走自己的路"。
10.2 io_uring 的 futex 操作(IORING_OP_FUTEX_WAIT)
io_uring 在 5.17+ 支持 IORING_OP_FUTEX_WAIT。这个操作的信号响应与传统的 futex(FUTEX_WAIT) 系统调用略有不同:
- 传统 futex wait:信号打断 → 返回 EINTR,用户态负责决定恢复。
- io_uring FUTEX_WAIT:如果同时提交了 linked timeout,信号打断后 timeout 也跟着取消,由 uring 内核态协调,用户态通过 CQE 结果感知(
-EINTR)。
10.3 Rust async 生态对 ERESTARTSYS 的处理
Rust 的 tokio 系列 async runtime 中,syscall wrapper 层需要小心处理:
tokio::net::TcpStream::poll_read内部调用read()(通过miocrate)。- 如果
read()返回-ERESTARTSYS,glibc/ld_quirks 自动重启,Rust 代码看到的是最终成功或 EINTR。 - 但如果 runtime 直接用
syscall指令(不走 glibc),需要自己处理。
Tokio 目前的做法是在 runtime builder 阶段显式注册 signal handler 时不设置 SA_RESTART,然后在 poll 循环中显式处理 EINTR。
十一、总结与工程建议
回顾 ERESTARTSYS 与可重启系统调用的核心知识体系:
关键理解:
ERESTARTSYS是内核对系统调用 interrupt 后的三种回复方式之一,对应不同的语义。SA_RESTART标志是用户态告诉内核"遇到 ERESTARTSYS 请隐藏它"的协议。ERESTART_RESTARTBLOCK用于要求精确恢复剩余时间/进度的系统调用,通过restart_block结构实现。futex(FUTEX_WAIT)是特殊的:永不自动重启。
工程建议:
- 总是对长时间阻塞调用(
epoll_wait、nanosleep、nanosleep)显式处理EINTR,不要假设SA_RESTART会自动帮你搞定。 - 使用 ppoll 而不是 poll,使用 clock_nanosleep 而不是 nanosleep。
- 在使用 futex-based 同步原语时,记得循环处理
EINTR。 - 在 signal handlers 中只使用 async-signal-safe 函数,并且不要对 restart 行为做任何假设。
- 如果你在写 io_uring 应用,请了解其不同于传统 syscall 路径的 cancel/restart 语义。
- 调试 restart 相关 bug 时,优先用 strace 观察
restart_syscall系统调用的出现。
被信号中断不可怕。可怕的是不知道信号中断后内核会做什么——这才是 ERESTARTSYS 机制的核心价值:它提供了一个确定性的协议,让内核和用户态能够对"是否重启系统调用"达成正确的一致。正确使用这个协议,是写出健壮系统程序的基础。
参考文档
- Linux kernel source:
kernel/signal.c,kernel/time/hrtimer.c,kernel/futex/core.c man 7 signal,man 2 restart_syscall,man 2 nanosleep,man 2 futex- Linux kernel documentation:
Documentation/driver-api/io_uring.rst - glibc source:
sysdeps/unix/sysv/linux/, nptl/pthread_cond_wait.c

发表评论 取消回复