深入解析:[Linux]学习笔记系列 -- [kernel]pid
title: pid
categories:
- linux
- kernel
tags: - linux
- kernel
abbrlink: 5550c4ed
date: 2025-10-03 09:01:49
文章目录

https://github.com/wdfk-prog/linux-study
kernel/pid.c PID管理(PID Management) 实现PID虚拟化与生命周期控制
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/pid.c 中的代码及其核心数据结构 struct pid 是为了解决在现代Linux内核中PID(进程标识符)的虚拟化和生命周期管理问题而诞生的。
在早期内核中,PID仅仅是 task_struct(进程描述符)中的一个整型成员(pid_t)。这种设计简单直接,但在面临以下新需求时暴露了严重局限性:
- 容器化和隔离:这是最主要的驱动力。为了实现容器(如Docker、LXC),必须让容器内的进程拥有自己独立的PID空间。例如,容器内的第一个进程应该看到自己的PID是1,而不是它在主机(Host)上的某个高位PID。旧的全局唯一PID模型无法实现这一点。
- 检查点/恢复(Checkpoint/Restore):像CRIU这样的项目需要能够“冻结”一个正在运行的进程及其所有状态,并在稍后或另一台机器上“解冻”它。如果PID只是一个全局数字,那么恢复时很可能原来的PID已经被其他进程占用,导致恢复失败。
- ID类型的统一管理:一个进程不仅有进程ID(PID),还有线程组ID(TGID,即主线程的PID)、进程组ID(PGID)和会话ID(SID)。在旧模型中,这些ID需要分别管理。
为了解决这些问题,内核需要将PID这个“标识符”本身从一个简单的数值,抽象成一个独立的、可被管理的内核对象。struct pid 和 pid.c 中的管理逻辑应运而生。
它的发展经历了哪些重要的里程碑或版本迭代?
pid.c 的演进是Linux进程模型现代化的核心部分。
struct pid的引入:这是最根本的变革。内核不再直接在task_struct中存储PID数值,而是存储一个指向struct pid对象的指针。这个struct pid成为了PID的“实体”,它被独立分配、拥有自己的生命周期(通过引用计数管理),并且可以被多个进程或ID类型共享。- PID命名空间(PID Namespaces)的实现:在
struct pid抽象的基础上,内核实现了PID虚拟化。struct pid结构被扩展,使其能够存储一个PID在多个不同命名空间中的数值。一个进程因此可以同时拥有一个在主机上可见的PID和一个在容器内可见的PID。 - 高效的PID哈希表:为了能根据一个在特定命名空间中的PID数值快速查找到对应的
task_struct(例如kill(pid, ...)系统调用),内核实现了一个全局的PID哈希表。pid.c中的代码负责管理这个哈希表,实现PID的快速注册和查找。 - 统一ID类型:
struct pid的设计使其可以被PID、TGID、PGID和SID共用。task_struct中有多个指向struct pid的指针,但这些指针可能指向同一个struct pid实例(例如,一个单线程的进程组长,其PID、TGID和PGID可能都由同一个struct pid对象表示),这提高了效率和代码复用。
目前该技术的社区活跃度和主流应用情况如何?
pid.c 是Linux内核进程调度和管理子系统的绝对核心,其代码非常稳定和成熟。
- 主流应用:它是所有容器技术(Docker、Kubernetes、LXC等)能够存在的基础。没有
pid.c实现的PID虚拟化,就没有现代容器隔离。此外,Linux系统上所有的进程创建、销毁、信号发送、进程组管理等日常操作,都无时无刻不在依赖pid.c提供的功能。
核心原理与设计
它的核心工作原理是什么?
pid.c 的核心是围绕 struct pid 对象及其在命名空间中的转换和查找。
核心数据结构
struct pid:kref:一个引用计数器。当一个task_struct指向它,或者它被用于PGID等,计数就会增加。当计数归零时,该struct pid对象被销毁。tasks:一个包含三个链表头的数组,分别用于PID、PGID和SID。这使得可以从一个struct pid对象反向查找到所有使用它的task_struct。numbers:这是实现PID虚拟化的关键。它是一个struct upid数组,每个upid包含两部分:一个PID数值(nr)和一个指向该数值所属的命名空间(struct pid_namespace *ns)的指针。一个struct pid对象可以包含它在各级嵌套命名空间中的所有不同数值。
PID的分配与关联:
- 当通过
fork()创建新进程时,内核会调用alloc_pid()。 alloc_pid()会在当前进程所在的PID命名空间中找到一个未使用的PID数值。- 然后,它会创建一个新的
struct pid对象,用找到的PID数值和命名空间指针来初始化其numbers数组的第一个元素。如果存在父命名空间,也会一并填充。 - 最后,新的
task_struct会保存一个指向这个新创建的struct pid对象的指针。
- 当通过
PID的查找与转换:
- 当用户空间程序执行如
kill(1234, SIGKILL)的操作时,内核需要将数值1234翻译成一个具体的进程。 - 内核会获取当前进程所在的PID命名空间。
- 然后调用
find_pid_ns(),它利用PID哈希表,在指定的命名空间中查找数值为1234的struct pid对象。 - 一旦找到了
struct pid对象,内核就可以通过其tasks链表轻松找到对应的task_struct,然后执行发送信号的操作。
- 当用户空间程序执行如
它的主要优势体现在哪些方面?
- 隔离性:完美地实现了PID的虚拟化,是容器技术的基础。
- 高效查找:通过哈希表,使得从PID数值到进程实体的查找操作平均时间复杂度为 O(1)。
- 清晰的生命周期:通过引用计数,精确地管理PID对象的生命周期,避免了资源泄漏和过早释放。
- 统一和抽象:将不同类型的ID(PID/TGID/PGID/SID)统一用
struct pid来表示,简化了内核设计。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 复杂性增加:相比于简单的全局计数器,
pid.c的逻辑(命名空间、哈希表、引用计数)要复杂得多,给内核开发者带来了更高的理解成本。 - 内存开销:每个PID(以及PGID/SID)都对应一个内核对象,这比只存储一个整数有更多的内存开销。但在现代系统中,这种为了功能和隔离性付出的开销是完全值得的。
- PID耗尽:虽然
pid_max可调,但在一个PID命名空间内,PID的数量是有限的。在创建和销毁大量短生命周期进程的容器环境中,可能会发生PID耗尽的问题。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
pid.c 的机制是内核内建的,不是一个可选方案,而是所有涉及进程ID操作的唯一实现方式。
- 创建容器:
clone()系统调用使用CLONE_NEWPID标志时,内核会创建一个新的PID命名空间,并为新进程在新空间中分配PID 1。这一切都由pid.c和nsproxy.c等协同完成。 - 发送信号:
kill()系统调用依赖pid.c的查找机制,将用户传入的、在调用者命名空间中有效的PID,准确无误地定位到目标进程。 - 进程组和会话管理:
setsid()、setpgid()等调用会创建或修改进程的会日志和会话ID,其底层实现涉及到创建新的struct pid对象或改变task_struct的指针。 - 所有常规的进程创建和销毁:
fork()、vfork()、clone()和exit()都深度依赖pid.c的分配和释放逻辑。
是否有不推荐使用该技术的场景?为什么?
此问题不适用。因为pid.c是内核进程模型不可或缺的一部分,无法被“不使用”或“替换”。
对比分析
请将其 与 其他相似技术 进行详细对比。
最恰当的对比是将其与它所取代的旧的、扁平化的PID模型进行比较。
| 特性 | 现代 struct pid 模型 (kernel/pid.c) | 旧的 pid_t in task_struct 模型 |
|---|---|---|
| 数据结构 | PID是一个独立的、引用计数的内核对象 (struct pid)。 | PID是 task_struct 中的一个简单整型成员。 |
| 命名空间支持 | 原生支持。一个struct pid可以包含在多个命名空间中的不同数值。 | 完全不支持。PID是全局唯一的。 |
| 查找机制 | 基于哈希表,在指定命名空间内快速查找。 | 通常是线性扫描一个全局的进程数组。 |
| 生命周期 | 由引用计数管理,独立于task_struct。 | 与 task_struct 的生命周期完全绑定。 |
| ID类型管理 | 统一。PID, TGID, PGID, SID 都可以由struct pid对象表示。 | 分散。每个ID类型都需要在task_struct中有一个独立的整型字段。 |
| 支持的场景 | 容器、检查点/恢复 (CRIU)、复杂的进程组管理。 | 仅支持传统的、单一系统的进程管理。 |
| 复杂性 | 高 | 低 |
| 性能 | 查找快,但分配/释放有对象管理开销。 | 查找慢(随进程数增加),但分配/释放只是递增计数器。 |
kernel/pid.c
init_pid_ns
/*
* This controls the default maximum pid allocated to a process
*/
#define PID_MAX_DEFAULT (IS_ENABLED(CONFIG_BASE_SMALL) ? 0x1000 : 0x8000)
/*
* PID 映射页面开始时为 NULL,它们在首次使用时被分配,并且永远不会被释放。
* 这样,低 pid_max 值不会导致分配大量位图,但方案可以扩展到最多 400 万个 PID,运行时。
*/
struct pid_namespace init_pid_ns = {
.ns.count = REFCOUNT_INIT(2),
.idr = IDR_INIT(init_pid_ns.idr),
.pid_allocated = PIDNS_ADDING,
.level = 0,
.child_reaper = &init_task,
.user_ns = &init_user_ns,
.ns.inum = PROC_PID_INIT_INO,
#ifdef CONFIG_PID_NS
.ns.ops = &pidns_operations,
#endif
.pid_max = PID_MAX_DEFAULT,
#if defined(CONFIG_SYSCTL) && defined(CONFIG_MEMFD_CREATE)
.memfd_noexec_scope = MEMFD_NOEXEC_SCOPE_EXEC,
#endif
};
EXPORT_SYMBOL_GPL(init_pid_ns);
pid_idr_init
/*
* 最多 400 万个 PID 应该足够一段时间。
* [注意:PID/TID 限制为 2^30 ~= 10 亿,见 FUTEX_TID_MASK。
*/
#define PID_MAX_LIMIT (IS_ENABLED(CONFIG_BASE_SMALL) ? PAGE_SIZE * 8 : \
(sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))
/*
* struct upid 用于获取 struct PID 的 ID,因为它在特定命名空间中可以看到。
* 稍后使用 int nr 和 struct pid_namespace *ns 通过 find_pid_ns() 找到结构 pid。
*/
#define RESERVED_PIDS 300
static int pid_max_min = RESERVED_PIDS + 1;
static int pid_max_max = PID_MAX_LIMIT;
void __init pid_idr_init(void)
{
/* 确认没有人做过任何愚蠢的事情: */
BUILD_BUG_ON(PID_MAX_LIMIT >= PIDNS_ADDING);
/* 基于 CPU 数量的 Bump Default 和 Minimum pid_max */
init_pid_ns.pid_max = min(pid_max_max, max_t(int, init_pid_ns.pid_max,
PIDS_PER_CPU_DEFAULT * num_possible_cpus()));
pid_max_min = max_t(int, pid_max_min,
PIDS_PER_CPU_MIN * num_possible_cpus());
pr_info("pid_max: default: %u minimum: %u\n", init_pid_ns.pid_max, pid_max_min);
/* 初始化 PID 分配器 */
idr_init(&init_pid_ns.idr);
init_pid_ns.pid_cachep = kmem_cache_create("pid",
struct_size_t(struct pid, numbers, 1),
__alignof__(struct pid),
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT,
NULL);
}
浙公网安备 33010602011771号