在做的一些事情,查阅跳转到了这个issue,发现一些有趣的思考,随手记录一下:https://github.com/SerenityOS/serenity/issues/26463
about SerenityOS belike:
引言
在 SerenityOS 的 GitHub 仓库中,Zig 项目的开发者提出了一个有趣的技术提案——为 SerenityOS 的 libc 实现一套健壮的信号量机制。
这个提案背后涉及到一个经典的操作系统问题:当进程异常终止时,如何防止系统资源泄露?
这是一个很有趣的议题,触及了进程间通信(IPC)、资源管理和系统健壮性
对这部分 感兴趣的可以跳转去年写的前文,大概有二十多篇,随手在这里放几篇吧,对这部分不太了解的可以当作前置知识来看,以便更好的理解下面的问题讨论:
- [Linux#40][线程] 线程控制 | 多线程
- [Linux][OS][详解信号的产生]
- 【Linux】详解自定义Shell管道 | 构建简易进程池
- 【Linux】重定向 | 为什么说”一切皆文件?
- 【Linux详解】进程地址空间
- 【Linux详解】冯诺依曼架构 | 操作系统设计 | 斯坦福经典项目Pintos
…more, 如果对这部分有所了解,那我们直接来看这个议题
问题:GNU Jobserver 的致命缺陷
什么是 Jobserver?
想象一下这样的场景:在编译一个大型项目,make工具会同时启动多个gcc进程来并行编译。如果不加限制,可能会同时启动几十个编译器进程,导致:
- CPU 过度竞争,频繁上下文切换
- 内存耗尽
- 系统响应变慢
Jobserver(作业服务器)就是为了解决这个问题而生的——它本质上是一个跨进程的线程池,通过"令牌"(token)机制来限制同时执行的任务数量。
GNU Jobserver 的设计缺陷
GNU 设计的 Jobserver 协议有一个致命问题:
进程 A 获取令牌 → 开始编译 → 突然崩溃 💥此时,令牌永远不会被归还
如果多个进程都这样崩溃,所有令牌都会泄露,导致系统死锁——剩余的进程永远在等待永远不会到来的令牌。
这就像图书馆借书系统:如果借书的人突然"消失",书永远不会被归还,其他人就永远借不到这本书了。
Zig 项目的解决方案:System V 信号量的SEM_UNDO
Zig 团队开发的新协议采用了一个巧妙的机制:System V 信号量的SEM_UNDO标志。
工作原理
// 进程获取令牌时semop(semid,&op,1);// 带 SEM_UNDO 标志// 内核记录:进程 PID=1234 对信号量做了 -1 操作// 进程崩溃时// 内核自动执行:将信号量 +1,撤销之前的操作核心思想:让os在进程退出时自动清理资源,就像 RAII(资源获取即初始化)在 C++ 中的作用一样。
为什么不直接用 System V IPC?
虽然 System V 信号量能解决问题,但它有明显的缺点:
- API 设计古老:诞生于 1980 年代,接口复杂难用
- 已被视为过时:现代系统更倾向于
POSIX 信号量 - 维护负担重:为一个过时的 API 维护完整实现不划算
🎢System V IPC vs. POSIX 信号量
System V IPC
- 传统UNIX进程通信机制,包含消息队列、共享内存、信号量。
- 信号量操作复杂(需
semctl、semop等系统调用),基于内核对象标识符(如semid)。 - 跨进程稳定性强,但接口冗余,可能残留未释放资源。
POSIX 信号量
- 现代标准,轻量级,接口简洁(
sem_wait、sem_post等)。 - 支持线程级同步,提供命名/未命名信号量,资源自动释放。
- 性能更高,但部分旧系统兼容性有限。
所以 提案者建议为 SerenityOS 设计一套现代化的健壮信号量 API。
提案:扩展 POSIX 信号量 API
新增 API 设计
/* 类似 sem_post,但进程退出时操作会被系统自动撤销 */intsem_post_robust(sem_t*);/* 类似 sem_wait,但进程退出时操作会被系统自动撤销 */intsem_wait_robust(sem_t*);intsem_trywait_robust(sem_t*);intsem_timedwait_robust(sem_t*,conststructtimespec*abstime);实现思路
基础版本(需要系统调用)
用户空间 内核空间 │ │ ├─ sem_wait_robust()│ │ │ └─> syscall ───────────────>├─ 更新信号量值:-1 ├─ 记录调整值:adjustment[PID][SEM] = -1 │ 进程退出时 │ └─ 自动执行:sem_value += adjustment[PID][SEM]优点:实现简单,逻辑清晰
缺点:每次操作都需要系统调用,性能开销较大
高级版本(用户态优化)
为了避免频繁的系统调用,可以采用用户态快速路径:
// 1. 进程启动时告诉内核:adjustment 变量的内存地址register_adjustment_variable(&my_adjustment,semaphore_id);// 2. 用户态直接操作voidsem_wait_robust_fast(sem_t*sem){atomic_dec(&sem->value);// 原子操作减少信号量atomic_inc(&my_adjustment);// 记录调整值// 内核会在进程退出时读取 my_adjustment 并应用}挑战:信号中断的竞态条件
时间线:T1:atomic_dec(&sem->value)← 信号量已更新T2:【收到信号,进程被杀死】 ← adjustment 还未更新!T3:atomic_inc(&my_adjustment)← 永远不会执行解决方案:
- 屏蔽信号:在关键区间禁止信号中断(类似自旋锁)
- 内核检测:内核检查进程退出时的 PC(程序计数器),判断是否在关键代码区间
- 接受系统调用:对于 Serenity 这样的新系统,先实现基础版本即可
分析
与 POSIX 健壮互斥锁的对比
POSIX 已经有类似的概念:健壮互斥锁(Robust Mutex)
pthread_mutexattr_tattr;pthread_mutexattr_setrobust(&attr,PTHREAD_MUTEX_ROBUST);pthread_mutex_init(&mutex,&attr);// 如果持有锁的线程死亡,下一个 pthread_mutex_lock() 会返回 EOWNERDEAD区别:
- 互斥锁:需要显式处理
EOWNERDEAD错误 - 健壮信号量:自动恢复,对用户透明
内核实现
// 内核数据结构structprocess{pid_tpid;structsemaphore_adjustment{sem_t*semaphore;intadjustment;}adjustments[MAX_SEMS];};// 进程退出时的清理逻辑voidprocess_exit_cleanup(structprocess*proc){for(inti=0;i<MAX_SEMS;i++){if(proc->adjustments[i].semaphore){// 原子地恢复信号量值atomic_add(&proc->adjustments[i].semaphore->value,proc->adjustments[i].adjustment);}}}应用场景
1. 构建系统(Build Systems)
# Makefile 中使用 jobservermake-j8 --jobserver-auth=fifo:/tmp/jobserver2. 分布式任务调度
- CI/CD 系统中的并行测试
- 渲染农场(Render Farm)的任务分配
- 数据库连接池管理
3. 嵌入式系统
- 实时操作系统中的资源配额管理
- 多核处理器的负载均衡
为什么这对 SerenityOS 很重要?
SerenityOS 是一个从零开始构建的现代操作系统,有机会:
- 避免历史包袱:不需要兼容 System V IPC 的复杂 API
- 设计更优雅的接口:基于 POSIX 标准但加入现代特性
- 展示技术创新:成为其他系统的参考实现
总结
提案本质上在解决一个经典的操作系统问题:如何在分布式环境中实现健壮的资源管理。
核心思想:
- 将资源清理的责任从用户态转移到内核态
- 利用操作系统的进程生命周期管理能力
- 在性能和健壮性之间找到平衡
启示:
- 好的 API 设计需要平衡易用性、性能和健壮性
- 操作系统原语的选择会深刻影响上层应用的架构
- 有时候"老技术"(如 System V IPC)中蕴含的设计智慧值得借鉴
对于 SerenityOS 这样的现代操作系统项目,这个提案提供了一个好的机会——在吸取历史经验的基础上,设计出更加优雅和高效的系统接口。
参考:
- Robust Jobserver Protocol Specification
- POSIX.1-2017:
pthread_mutexattr_setrobust() - Linux Manual:
semop(2)和SEM_UNDO标志