switch (childPid = fork()) { case-1: errExit("fork"); case0: /* Child: immediately exits to become zombie */ printf("Child (PID=%ld) exiting\n", (long) getpid()); _exit(EXIT_SUCCESS); default: /* Parent */ sleep(3); /* Give child a chance to start and exit */ snprintf(cmd, CMD_SIZE, "ps | grep %s", basename(argv[0])); cmd[CMD_SIZE - 1] = '\0'; /* Ensure string is null-terminated */ system(cmd); /* View zombie child */
/* Now send the "sure kill" signal to the zombie */ if (kill(childPid, SIGKILL) == -1) errMsg("kill"); sleep(3); /* Give child a chance to react to signal */ printf("After sending SIGKILL to zombie (PID=%ld):\n", (long) childPid); system(cmd); /* View zombie child again */
有时,应用程序需要在进程终止时自动执行一些操作。考虑这样一个例子:一个应用程序库,如果在进程的生命周期中被使用,需要在进程退出时自动执行一些清理操作。由于该库无法控制进程何时以及如何退出,也不能强制主程序在退出前调用库特定的清理函数,因此无法保证清理一定会发生。在这种情况下,一种方法是使用退出处理程序(exit
handler)(较老的 System V 手册使用术语“程序终止例程”)。
staticvoidatexitFunc1(void){ printf("atexit function 1 called\n"); } staticvoidatexitFunc2(void){ printf("atexit function 2 called\n"); } staticvoidonexitFunc(int exitStatus, void *arg){ printf("on_exit function called: status=%d, arg=%ld\n", exitStatus, (long) arg); }
intmain(int argc, char *argv[]){ if (on_exit(onexitFunc, (void *) 10) != 0) fatal("on_exit 1"); if (atexit(atexitFunc1) != 0) fatal("atexit 1"); if (atexit(atexitFunc2) != 0) fatal("atexit 2"); if (on_exit(onexitFunc, (void *) 20) != 0) fatal("on_exit 2"); exit(2); }
当我们运行这个程序时,会看到以下输出:
1 2 3 4 5
$ ./exit_handlers on_exit function called: status=2, arg=20 atexit function 2 called atexit function 1 called on_exit function called: status=2, arg=10
Linux 拥有一个独特的线程实现。对 Linux
内核而言,没有“线程”的概念。Linux
将所有线程实现为标准进程。Linux
内核并不提供任何特殊的调度语义或数据结构来表示线程。相反,一个线程仅仅是一个与其他进程共享某些资源的进程。每个线程都有一个唯一的
task_struct,并且在内核看来就是一个普通的进程——线程只是恰巧与其他进程共享了资源(例如地址空间)。
这种线程实现方法与 Microsoft Windows 或 Sun Solaris
等操作系统形成了巨大对比,这些操作系统在内核中提供了对线程的明确支持(有时称线程为轻量级进程
(Lightweight Processes))。“轻量级进程”这个名字概括了 Linux
与其他系统在哲学上的差异。对这些其他操作系统而言,线程是一种抽象,旨在提供比笨重进程更轻量、更快速的执行单元。而对
Linux
而言,线程仅仅是进程之间共享资源的一种方式(而进程本身已经相当轻量)¹⁰。
例如,假设一个包含四个线程的进程。在具有显式线程支持的系统中,可能会存在一个进程描述符,该描述符又指向四个不同的线程。进程描述符描述共享资源,如地址空间或打开的文件。线程则描述它们独自拥有的资源。相反,在
Linux 中,简单地存在四个进程,因而有四个普通的 task_struct
结构。这四个进程被设置为共享某些资源。结果非常优雅。
¹⁰ 例如,可以对比 benchmark 一下 Linux
的进程创建时间与其他操作系统的进程(甚至线程!)创建时间。结果对 Linux
有利。
这篇发表于 2013 年 2 月的文章讨论了 Linux 内核中 IDR 子系统
API 的一次重大简化改革,由开发者 Tejun Heo 主导。IDR
机制用于高效分配和管理整数 ID(例如设备名、POSIX 定时器 ID 等),其旧
API 因其复杂性和潜在的竞争条件而闻名。
核心问题:旧 API 的缺陷 1.
两步分配:需要先调用 idr_pre_get()
预分配内存(可休眠),再调用 idr_get_new() 获取
ID(可原子上下文)。 2.
必须重试循环:idr_get_new()
可能因预分配内存被其他 CPU 耗尽而失败(返回
-EAGAIN),要求调用者编写冗长且易错的循环重试代码。 3.
全局资源竞争:idr_pre_get()
预分配的内存是全局的,多个 CPU 竞争时,后执行的
idr_get_new()
可能因资源不足而失败,迫使代码退出原子上下文进行重试,这条路径往往缺乏测试。
解决方案:新 API 的改进 Tejun Heo
引入了三个新函数来简化流程: 1.
idr_preload(gfp_t gfp_mask): 为当前 CPU
预分配内存,并禁用抢占以防止预分配的内存被偷。 2.
idr_alloc(...): 单次调用即可完成 ID
分配和关联。它接受 ID
范围参数,并仅在真正需要时(未预分配或预分配不足)才使用
gfp_mask 分配内存。它只会在内存分配彻底失败时报错,消除了对
-EAGAIN 的重试循环需求。 3. idr_preload_end():
在 idr_alloc 后调用,重新启用抢占。
关键优势: *
更简单:消除了遍布内核的百余处重复、易错的样板代码。 *
更可靠:通过每 CPU
预分配和禁用抢占,基本消除了在原子上下文中因资源竞争而失败的需要。 *
更灵活:idr_alloc 可以指定 ID
范围,并且如果能在进程上下文调用,甚至可以完全省略
idr_preload/idr_preload_end。
社区反应: 尽管大部分开发者接受了这个改动(给出了
Acked-by),但 Eric Biederman 表达了强烈反对,认为新 API 的
idr_preload
像是一种难以理解的“魔法”。然而,文章作者(Jonathan
Corbet)预测,新 API
带来的巨大简化优势将使其最终被内核社区接受。
这篇文章记录了一个经典的内核优化案例:通过巧妙的设计(利用每 CPU
数据和禁用抢占)将一个复杂、易错、充满竞争条件的旧接口,重构为一个简洁、可靠、高效的新接口。尽管存在一些争议,但简化并提升广泛使用的底层
API
的价值是极其巨大的,这很可能是新方案最终被采纳的原因。这正是
Linux 内核持续演进的一个缩影。