首页 > 系统 > Linux > 正文

Linux的进程ID号如何实现

2024-08-27 23:55:06
字体:
来源:转载
供稿:网友
        这篇文章主要介绍“Linux的进程ID号怎么实现”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Linux的进程ID号怎么实现”文章能帮助大家解决问题。
 
  本文中的代码摘自 Linux内核5.15.13版本。
 
  Linux进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程ID号,简称PID。用fork或clone产生的每个进程都由内核自动地分配了一个新的唯一的PID值。
 
一、进程ID
1.1、其他ID
  每个进程除了PID这个特征值之外,还有其他的ID。有下列几种可能的类型
 
  1、 处于某个线程组(在一个进程中,以标志CLONE_THREAD来调用clone建立的该进程的不同的执行上下文,我们在后文会看到)中的所有进程都有统一的线程组ID( TGID)。如果进程没有使用线程,则其PID和TGID相同。线程组中的主进程被称作组长( group leader)。通过clone创建的所有线程的task_struct的group_leader成员,会指向组长的task_struct实例。
 
  2、另外,独立进程可以合并成进程组(使用setpgrp系统调用)。进程组成员的task_struct的pgrp属性值都是相同的,即进程组组长的PID。进程组简化了向组的所有成员发送信号的操作,这对于各种系统程序设计应用(参见系统程序设计方面的文献,例如[ SR05])是有用的。请注意,用管道连接的进程包含在同一个进程组中。
 
  3、 几个进程组可以合并成一个会话。会话中的所有进程都有同样的会话ID,保存在task_struct的session成员中。 SID可以使用setsid系统调用设置。它可以用于终端程序设计。
 
1.2、全局ID和局部ID
  名空间增加了PID管理的复杂性。 PID命名空间按层次组织。在建立一个新的命名空间时,该命名空间中的所有PID对父命名空间都是可见的,但子命名空间无法看到父命名空间的PID。但这意味着某些进程具有多个PID,凡可以看到该进程的命名空间,都会为其分配一个PID。 这必须反映在数据结构中。我们必须区分局部ID和全局ID。
 
  1、 全局ID是在内核本身和初始命名空间中的唯一ID号,在系统启动期间开始的init进程即属于初始命名空间。对每个ID类型,都有一个给定的全局ID,保证在整个系统中是唯一的。
 
  2、 局部ID属于某个特定的命名空间,不具备全局有效性。对每个ID类型,它们在所属的命名空间内部有效,但类型相同、值也相同的ID可能出现在不同的命名空间中。
 
1.3、ID实现
  全局PID和TGID直接保存在task_struct中,分别是task_struct的pid和tgid成员,在sched.h文件里:
 
struct task_struct {...pid_t pid;pid_t tgid;...}
  这两项都是pid_t类型,该类型定义为__kernel_pid_t,后者由各个体系结构分别定义。通常定义为int,即可以同时使用232个不同的ID。
 
二、管理PID
  一个小型的子系统称之为PID分配器( pid allocator)用于加速新ID的分配。此外,内核需要提供辅助函数,以实现通过ID及其类型查找进程的task_struct的功能,以及将ID的内核表示形式和用户空间可见的数值进行转换的功能。
 
2.1、PID命名空间的表示方式
  在pid_namespace.h文件内有如下定义:
 
struct pid_namespace {
struct idr idr;
struct rcu_head rcu;
unsigned int pid_allocated;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;#ifdef CONFIG_BSD_PROCESS_ACCT
struct fs_pin *bacct;#endif
struct user_namespace *user_ns;
struct ucounts *ucounts;
int reboot; /* group exit code if this pidns was rebooted */
struct ns_common ns;} __randomize_layout;
  每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。 init的一个目的是对孤儿进程调用wait4,命名空间局部的init变体也必须完成该工作。 child_reaper保存了指向该进程的task_struct的指针。
 
  parent是指向父命名空间的指针, level表示当前命名空间在命名空间层次结构中的深度。初始命名空间的level为0,该命名空间的子空间level为1,下一层的子空间level为2,依次递推。level的计算比较重要,因为level较高的命名空间中的ID,对level较低的命名空间来说是可见的。从给定的level设置,内核即可推断进程会关联到多少个ID。
 
2.2、PID的管理
2.2.1、PID的数据结构
  PID的管理围绕两个数据结构展开: struct pid是内核对PID的内部表示,而struct upid则表示特定的命名空间中可见的信息。两个结构的定义在文件pid.h内,分别如下:
 
/*
 * What is struct pid?
 *
 * A struct pid is the kernel's internal notion of a process identifier.
 * It refers to inpidual tasks, process groups, and sessions.  While
 * there are processes attached to it the struct pid lives in a hash
 * table, so it and then the processes that it refers to can be found
 * quickly from the numeric pid value.  The attached processes may be
 * quickly accessed by following pointers from struct pid.
 *
 * Storing pid_t values in the kernel and referring to them later has a
 * problem.  The process originally with that pid may have exited and the
 * pid allocator wrapped, and another process could have come along
 * and been assigned that pid.
 *
 * Referring to user space processes by holding a reference to struct
 * task_struct has a problem.  When the user space process exits
 * the now useless task_struct is still kept.  A task_struct plus a
 * stack consumes around 10K of low kernel memory.  More precisely
 * this is THREAD_SIZE + sizeof(struct task_struct).  By comparison
 * a struct pid is about 64 bytes.
 *
 * Holding a reference to struct pid solves both of these problems.
 * It is small so holding a reference does not consume a lot of
 * resources, and since a new struct pid is allocated when the numeric pid
 * value is reused (when pids wrap around) we don't mistakenly refer to new
 * processes.
 *//*
 * struct upid is used to get the id of the struct pid, as it is
 * seen in particular namespace. Later the struct pid is found with
 * find_pid_ns() using the int nr and struct pid_namespace *ns.
 */struct upid {
int nr;
struct pid_namespace *ns;};struct pid{
refcount_t count;
unsigned int level;
spinlock_t lock;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct hlist_head inodes;
/* wait queue for pidfd notifications */
wait_queue_head_t wait_pidfd;
struct rcu_head rcu;
struct upid numbers[1];};
  对于struct upid, nr表示ID的数值, ns是指向该ID所属的命名空间的指针。所有的upid实例都保存在一个散列表中。 pid_chain用内核的标准方法实现了散列溢出链表。struct pid的定义首先是一个引用计数器count。 tasks是一个数组,每个数组项都是一个散列表头,对应于一个ID类型。这样做是必要的,因为一个ID可能用于几个进程。所有共享同一给定ID的task_struct实例,都通过该列表连接起来。 PIDTYPE_MAX表示ID类型的数目:
 
enum pid_type{
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,};
2.2.2、PID与进程的联系
  一个进程可能在多个命名空间中可见,而其在各个命名空间中的局部ID各不相同。 level表示可以看到该进程的命名空间的数目(换言之,即包含该进程的命名空间在命名空间层次结构中的深度),而numbers是一个upid实例的数组,每个数组项都对应于一个命名空间。注意该数组形式上只有一个数组项,如果一个进程只包含在全局命名空间中,那么确实如此。由于该数组位于结构的末尾,因此只要分配更多的内存空间,即可向数组添加附加的项。
 
  由于所有共享同一ID的task_struct实例都按进程存储在一个散列表中,因此需要在struct task_struct中增加一个散列表元素在sched.h文件内进程的结构头定义内有
 
struct task_struct {...
/* PID/PID hash table linkage. */
struct pid *thread_pid;
struct hlist_node pid_links[PIDTYPE_MAX];
struct list_head thread_group;
struct list_head thread_node;...};
  将task_struct连接到表头在pid_links中的散列表上。
 
2.2.3、查找PID
  假如已经分配了struct pid的一个新实例,并设置用于给定的ID类型。它会如下附加到task_struct,在kernel/pid.c文件内:
 
static struct pid **task_pid_ptr(struct task_struct *task, enum pid_type type){
return (type == PIDTYPE_PID) ?
&task->thread_pid :
&task->signal->pids[type];}/*
 * attach_pid() must be called with the tasklist_lock write-held.
 */void attach_pid(struct task_struct *task, enum pid_type type){
struct pid *pid = *task_pid_ptr(task, type);
hlist_add_head_rcu(&task->pid_links[type], &pid->tasks[type]);}
  这里建立了双向连接: task_struct可以通过task_struct->pids[type]->pid访问pid实例。而从pid实例开始,可以遍历tasks[type]散列表找到task_struct。 hlist_add_head_rcu是遍历散列表的标准函数。
 
三、生成唯一的PID
  除了管理PID之外,内核还负责提供机制来生成唯一的PID。为跟踪已经分配和仍然可用的PID,内核使用一个大的位图,其中每个PID由一个比特标识。 PID的值可通过对应比特在位图中的位置计算而来。因此,分配一个空闲的PID,本质上就等同于寻找位图中第一个值为0的比特,接下来将该比特设置为1。反之,释放一个PID可通过将对应的比特从1切换为0来实现。在建立一个新进程时,进程可能在多个命名空间中是可见的。对每个这样的命名空间,都需要生成一个局部PID。这是在alloc_pid中处理的,在文件kernel/pid.c内有:
 
struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
      size_t set_tid_size){
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid;
int retval = -ENOMEM;
 
/*
 * set_tid_size contains the size of the set_tid array. Starting at
 * the most nested currently active PID namespace it tells alloc_pid()
 * which PID to set for a process in that most nested PID namespace
 * up to set_tid_size PID namespaces. It does not have to set the PID
 * for a process in all nested PID namespaces but set_tid_size must
 * never be greater than the current ns->level + 1.
 */
if (set_tid_size > ns->level + 1)
return ERR_PTR(-EINVAL);
 
pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
if (!pid)
return ERR_PTR(retval);
 
tmp = ns;
pid->level = ns->level;
 
for (i = ns->level; i >= 0; i--) {
int tid = 0;
 
if (set_tid_size) {
tid = set_tid[ns->level - i];
 
retval = -EINVAL;
if (tid < 1 || tid >= pid_max)
goto out_free;
/*
 * Also fail if a PID != 1 is requested and
 * no PID 1 exists.
 */
if (tid != 1 && !tmp->child_reaper)
goto out_free;
retval = -EPERM;
if (!checkpoint_restore_ns_capable(tmp->user_ns))
goto out_free;
set_tid_size--;
}
 
idr_preload(GFP_KERNEL);
spin_lock_irq(&pidmap_lock);
 
if (tid) {
nr = idr_alloc(&tmp->idr, NULL, tid,
       tid + 1, GFP_ATOMIC);
/*
 * If ENOSPC is returned it means that the PID is
 * alreay in use. Return EEXIST in that case.
 */
if (nr == -ENOSPC)
nr = -EEXIST;
} else {
int pid_min = 1;
/*
 * init really needs pid 1, but after reaching the
 * maximum wrap back to RESERVED_PIDS
 */
if (idr_get_cursor(&tmp->idr) > RESERVED_PIDS)
pid_min = RESERVED_PIDS;
 
/*
 * Store a null pointer so find_pid_ns does not find
 * a partially initialized PID (see below).
 */
nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
      pid_max, GFP_ATOMIC);
}
spin_unlock_irq(&pidmap_lock);
idr_preload_end();
 
if (nr < 0) {
retval = (nr == -ENOSPC) ? -EAGAIN : nr;
goto out_free;
}
 
pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
}
  upid->ns->pid_allocated++;
}
spin_unlock_irq(&pidmap_lock);
 
return pid;out_unlock:
spin_unlock_irq(&pidmap_lock);
put_pid_ns(ns);out_free:
spin_lock_irq(&pidmap_lock);
while (++i <= ns->level) {
upid = pid->numbers + i;
idr_remove(&upid->ns->idr, upid->nr);
}
 
/* On failure to allocate the first pid, reset the state */
if (ns->pid_allocated == PIDNS_ADDING)
idr_set_cursor(&ns->idr, 0);
 
spin_unlock_irq(&pidmap_lock);
 
kmem_cache_free(ns->pid_cachep, pid);
return ERR_PTR(retval);}

(编辑:武林网)

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表