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 等致命信号),它就面临一个关键问题:刚才在跑的系统调用怎么办?

这时,取决于具体系统调用的实现,内核会做三件事之一:

  1. 直接返回 ERESTARTSYS(值为 512):告诉用户态"请检查信号,处理完后把这个系统调用重新执行一遍"。
  2. 返回 EINTR(值为 4):告诉用户态"系统调用被信号打断了,自己做决定"。
  3. 不做任何特殊事情:系统调用已经成功完成或返回非 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 = &current->restart_block;
    restart->nanosleep.expires = hrtimer_get_expire(...);
    restart->fn = hrtimer_nanosleep_restart;

    // 进入 hrtimer 等待...
    // 如果被信号打断,设置返回值为 -ERESTART_RESTARTBLOCK
    // 而不是普通的 -ERESTARTSYS
}

3.3 ERESTART_RESTARTBLOCK:"特殊重启"标记

当内核决定使用 restart_block 时:

  1. 系统调用返回 -ERESTART_RESTARTBLOCK(值是 516)
  2. glibc/POSIX 层识别这个码,调用 restart_syscall()(系统调用号随架构不同)
  3. restart_syscall() 恢复 current->restart_block.fn 等参数,重新进入内核
  4. 内核调用 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 = &current->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 的 op
  • IORING_ASYNC_CANCEL_ANY:取消所有 op
  • IORING_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()(通过 mio crate)。
  • 如果 read() 返回 -ERESTARTSYS,glibc/ld_quirks 自动重启,Rust 代码看到的是最终成功或 EINTR。
  • 但如果 runtime 直接用 syscall 指令(不走 glibc),需要自己处理。

Tokio 目前的做法是在 runtime builder 阶段显式注册 signal handler 时不设置 SA_RESTART,然后在 poll 循环中显式处理 EINTR。


十一、总结与工程建议

回顾 ERESTARTSYS 与可重启系统调用的核心知识体系:

关键理解:

  1. ERESTARTSYS 是内核对系统调用 interrupt 后的三种回复方式之一,对应不同的语义。
  2. SA_RESTART 标志是用户态告诉内核"遇到 ERESTARTSYS 请隐藏它"的协议。
  3. ERESTART_RESTARTBLOCK 用于要求精确恢复剩余时间/进度的系统调用,通过 restart_block 结构实现。
  4. futex(FUTEX_WAIT) 是特殊的:永不自动重启。

工程建议:

  1. 总是对长时间阻塞调用(epoll_wait、nanosleep、nanosleep)显式处理 EINTR,不要假设 SA_RESTART 会自动帮你搞定。
  2. 使用 ppoll 而不是 poll,使用 clock_nanosleep 而不是 nanosleep。
  3. 在使用 futex-based 同步原语时,记得循环处理 EINTR。
  4. 在 signal handlers 中只使用 async-signal-safe 函数,并且不要对 restart 行为做任何假设。
  5. 如果你在写 io_uring 应用,请了解其不同于传统 syscall 路径的 cancel/restart 语义。
  6. 调试 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
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部