如何正确实现PID/TID缓存,规避fork场景下的异常问题?
gettid缓存与fork行为差异的优化方案探讨
考虑以下代码:
#define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> #include <stdio.h> int main() { pid_t tid = gettid(); printf("tid: %d", tid); int ret = syscall(SYS_fork, 0); printf("tid: %d", gettid()); }
- 在现代(
>=2.25)glibc环境下,上述代码会输出三行内容,包含两个不同的值。 - 而在musl环境下,上述代码会输出三行完全相同的内容。
这种差异源于缓存机制。libc的实现会缓存这类值以避免系统调用的开销,fork/clone包装器负责更新这些缓存值,但如果用户绕过这些包装器,就会导致意外结果。
getpid的手册页中有一个专门章节介绍相关历史:
从glibc 2.3.4到glibc 2.24版本,getpid()的glibc包装函数会缓存PID,目的是避免进程重复调用getpid()时产生额外的系统调用。通常这种缓存是透明的,但它的正确运行依赖于fork(2)、vfork(2)和clone(2)的包装函数支持:如果应用通过syscall(2)绕过glibc的这些包装函数调用上述系统调用,那么子进程中调用getpid()会返回错误的值(准确来说,会返回父进程的PID)。此外,即使通过glibc包装函数调用clone(2),也存在getpid()返回错误值的情况(相关案例讨论可参考clone(2)的BUGS章节)。而且,缓存代码的复杂性多年来一直是glibc内部一些BUG的根源。
我想知道是否存在一种正确实现缓存的方式。鉴于MADV_WIPEONFORK可以指示内核在fork时清除特定页面,我们能否构建如下结构:
struct ProcessLocalStorage { OnceFlag once; /* zero represents uninitialized */ pid_t pid; pid_t getpid() { callonce(&this->once, []{ pid = syscall(SYS_getpid); }); return pid; } } *pls = mmap(...); /* hinted by MADV_WIPEONFORK */ pid_t gettid() { static thread_local pid_t pid = 0; static thread_local pid_t tid = 0; pid_t real_pid = pls->getpid(); if (pid != real_pid) { pid = real_pid; tid = syscall(SYS_gettid); } return tid; }
我能想到的一个问题是用户自行调用SYS_clone却未正确设置TLS,但我认为可以说明用户不应假设libc在这种场景下仍能正常工作。
内容的提问来源于stack exchange,提问作者Schrodinger ZHU
相关产品推荐
相关产品推荐

