每天看三页《深入Linux内核架构》之Linux内核进程的表示及PID管理

本文详细阐述了Linux内核如何管理进程、线程、线程组、进程组和会话的ID,区分全局ID和局部ID的概念,并介绍了structpid和structupid数据结构在命名空间中的作用,以及如何通过这些结构体实现PID的索引和管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近大概用了两周时间弄清楚了Linux中进程、线程、线程组、进程组、会话的关系,以及内核管理各种ID的方式。最开心的是终于搞清楚了结构体struct pid的含义,在这里感谢优快云!并做一个总结,希望能够帮助和我当初有同样困惑的人。

下面是我学习过程中帮助很大的两个帖子,推荐一下:

(49条消息) linux父进程pid代码,Linux PID 一网打尽_设计圈的博客-优快云博客

(48条消息) Linux内核PID管理_LeoSoldOut的博客-优快云博客_linux pid

一. Linux内核中线程、进程、线程组、进程组、会话以及它们ID的关系

全局ID与局部ID:

首先来介绍一下全局ID与局部ID,它们分别属于不同的命名空间。全局ID是在内核本身和初始命名空间中的唯一ID号。对每个ID类型,都有一个给定的全局ID,保证在整个系统中是唯一的。局部ID属于某个特定的命名空间,不具备全局有效性,仅在所属的命名空间内部有效。类型相同、值也相同的ID可能出现在不同的命名空间中。

进程:传统上,操作系统下运行的应用程序、服务器以及其他程序都称为进程。每个进程都会被分配一个特定的ID号,用于在其命名空间中唯一地标识它们。该号码被称为进程ID号,简称PID。线程和进程都用内核对象struct task_struct 表示,该对象是CPU调度的实体。每个进程的全局PID记录在task_struct :: pid中。

线程:对进程执行clone命令可以新建出一个与该进程共享资源的线程,并为线程分配一个新的唯一的ID值。线程ID简称为TID。由于Linux使用进程来模拟线程,因此当struct task_struct用于表示线程时,其全局TID记录在task_struct :: pid中。

线程组:由同一个进程clone出的线程以及该进程,归属于同一个线程组。这个进程被称为线程组的主进程和组长(group leader)。线程组ID简称为TGID。属于同一个线程组的线程以及主进程,都有统一的TGID,该值等于线程group leader的PID。没有线程的进程其TGID与PID相等。全局TGID记录在task_struct :: tgid中。指向task_struct的线程组组长的指针,记录在task_struct :: group_leader中。

进程组:一些进程可以组成一个进程组。进程组ID简称为PGID。属于同一个进程组的线程与进程,都有统一的PGID,该值等于进程group leader的PID。全局PGID记录在task_struct :: signal::_pgrp中。

会话(session):一些进程组可以组成一个会话。会话ID简称为SID。属于同一个会话的线程与进程,都有统一的SID,该值等于会话group leader的PID。全局SID记录在task_struct :: signal::_session中。

大家可以看到,对于每个task_struct(即线程或进程),其全局PID/TID、全局TGID、全局PGID、全局SID在task_struct中都能够直接查询到。

但特定于任一非初始命名空间的局部ID实在太多了,存储在task_struct中会占用太多的资源。为实现局部ID的存储与索引,内核创建了两个额外的数据结构。这两个数据结构分别叫做struct pid与struct upid 。其中,struct pid是内核对PID的内部表示。struct upid表示特定的命名空间中可见的信息。下面展示了用于局部ID存储的数据结构们及其内在联系:

其中结构体struct pid_namespace用来表示命名空间,它的定义如下:

<sched.h>

struct pid_namespace
{
...
    struct task_struct *child_reaper ;//每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。child_reaper保存了指向该进程的tast_struct的指针。
...
    int level ;//当前命名空间在命名空间层次结构中的深度。初始命名空间为0,该命名空间的子空间level为1,以此类推。level较高的命名空间中的ID,对level较低的命名空间来说是可见的。
    struct pid_namespace *parent ;//指向父命名空间的指针
}

数据结构struct upid用于存储某特定命名空间下的可见信息。数据结构struct pid用于链接与该pid有关的task_struct(进程或线程)并存储该pid的所有局部ID号。这两个结构分别定义如下:

<pid.h>

struct upid
{
    int nr ; //局部进程号
    struct pid_namespace *ns ; //指向所代表命名空间的指针
    struct hlist_node pid_chain ; //散列表元素
}

struct pid
{
    atomic_t count;//引用计数,即,每新建一个指向该pid的指针,引用计数加一。
    /*  使用该pid的进程的列表  */   
    struct hlist_head tasks[PIDTYPE_MAX] ;/*PIDTYPE_MAX表示ID类型的数目,该数组共包括三个散列表                                             头,三个表头分别连接了:该PID对应的task_struct;若该PID
                                           为进程组长的话该进程组包括的task_struct;若该PID为会话
                                           组长的话该会话组包括的task_struct*/ 
    int level ;//可以看到该进程的命名空间的数目
    struct upid numbers[1] ;//每个数组项对应一个命名空间
}

task_struct用于表示一个进程或线程。

<sched.h>

struct task_struct{
...
/*   PID与PID散列表的联系   */
struct pid_link pids [PIDTYPE_MAX]; 
...
}

辅助数据结构pid_link可以将task_struct连接到表头在struct pid 中的散列表上。

<pid.h>

struct pid_link  
{
struct hlist_node node ;//用作散列表元素
struct pid *pid ;//指向进程所属的pid结构实例
}

上述数据结构们通过Linux内置的散列表(哈希表)实现了相互索引。为创建通用函数并能够将链表方便地嵌入各数据结构中,Linux内核使用的链表结构与普通链表稍有不同。为方便理解,建议首先看一下Linux中散列表的介绍,详见上一篇总结:

(48条消息) 每天看三页《深入Linux内核架构》——Linux内核中双链表与散列表的源码分析与使用说明_在浮沙筑小楼的博客-优快云博客

在上述结构中,有两组散列表:第一组: struct pid::tasks[i] 为散列表头, struct task_struct::pids[i]::node 为散列表元素(请注意两数组存在对应关系,需要取相同的项),两者形成散列表;第二组:struct pid_hash为散列表头, struct upid::pid_chain为散列表元素,两者形成散列表。 Linux内核通过这两对散列表,实现了特定命名空间的局部pid号与特定task_struct(进程或线程)间的相互索引。

关于第一组索引:

struct pid::tasks[PIDTYPE_MAX] 为一个具有PIDTYPE_MAX(3)项的struct hlist_head数组,每个数组项都是一个散列表头。需要注意的是,这三个散列表头,分别对应于PID_TYPE_PID、PID_TYPE_PGID、PID_TYPE_SID,即enum pid_type中的前三个枚举项。具体来说,对于hash链表pid::tasks[PID_TYPE_PID],理论上只有该pid本身的task_struct这一个节点。当这个pid作为所在进程组的group leader时,这个PID值同时也被作为该进程组的PGID,因此hash链表pid::tasks[PID_TYPE_PGID]上链接着该进程组中所有的task_struct。当这个pid作为所在会话的group leader时,这个PID值同时也被作为该会话的SID,因此hash链表pid::tasks[PID_TYPE_SID]上链接着该会话中所有的task_struct。

struct task_struct::pids[PIDTYPE_MAX]是一个具有PIDTYPE_MAX(3)项的struct pid_link类型数组,这三项也分别对应于PID_TYPE_PID、PID_TYPE_PGID、PID_TYPE_SID,即enum pid_type中的前三个枚举项。数组第一项:struct task_struct::pids[PID_TYPE_PID]用于链接本进程(或线程)的pid。前面提到过,struct pid::tasks中,表示进程(或线程)级别的链表头是struct pid::tasks[PID_TYPE_PID]。因此,链表元素struct task_struct::pids[PID_TYPE_PID]::hlist_node对应的的链表头在struct pid结构的pid::tasks[PID_TYPE_PID]处,请注意两个数组都要取第PID_TYPE_PID(0)项,存在着对应关系

相似的,struct task_struct::pids[PID_TYPE_PGID]项用于链接进程组长的pid。在struct pid中,来链接进程组成员的表头是pids[PID_TYPE_PGID]。因此,与链表元素struct task_struct::pids[PID_TYPE_PGID]::hlist_node相对应的链表头在struct pid结构的pid::tasks[PID_TYPE_PGID]处。同样的,struct task_struct::pids[PID_TYPE_SID]项用于链接会话组长的pid。在struct pid中,来链接会话组成员的表头是pids[PID_TYPE_SID]。因此,与链表元素struct task_struct::pids[PID_TYPE_SID]::hlist_node相对应的链表头在struct pid结构的pid::tasks[PID_TYPE_SID]处。

请注意,枚举类型中定义的ID类型不包括线程组ID! 这是因为线程组ID就是线程组组长的PID,而struct task_struct中直接存储了指向线程组组长的指针。

关于第二组索引:

struct task_struct::numbers 是一个upid实例的数组,每个数组项都对应于一个命名空间。该数组形式上只有一个数组项,如果一个进程只包含在全局命名空间中,那么确实如此。若有更多的命名空间,由于该数组位于结构的末尾,只要分配更多的内存空间,即可向数组添加附加的项。

pid_hash用作一个hlist_head数组。数组的元素数目取决于计算机的内存配置。pidhash_init用于计算恰当的容量并分配所需的内存。

pid_hash为链表头,对应的链表元素为struct task_struct::numbers[i]::pid_chain。

下面介绍一些用于pid索引的常用内置函数:

(1)Linux 内核定义了辅助函数find_pid_ns用于通过命名空间和局部ID索引对应的struct pid,函数形式为:

kernel/pid.c

struct pid * fastcall find_pid_ns(int nr, struct pid_namespace *ns)

(2)内置函数pid_task用于取出pid->tasks[type]散列表中的第一个task_struct实例。

(1)、(2)步骤可以通过辅助函数find_task_by_pid_type_ns完成:

kernel/pid.c

struct task_struct * find_task_by_pid_type_ns (int type, int nr, struct pid_namespace *ns)

{

return pid_task ( find_pid_ns(nr , ns), type) ;

}

内核源码中很多地方都需要find_task_by_pid,因为很多特定于进程的操作(例如,使用kill发送一个信号)都通过PID标识目标进程。

PID的生成:

内核使用一个大的位图来跟踪已经分配和仍然可用的PID。其中每个PID由一个比特标识。PID的值可通过对应比特在位图中的位置计算而来。我们可以使用内置函数实现PID的分配与释放。

kernel/pid.c

static int alloc_pidmap ( struct pid_namespace *pid_ns ) //用于分配一个PID
static fastcall void free_pidmap ( struct pid_namespace *pid_ns , int pid) //用于释放一个PID

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值