この記事では、Linux でのプロセス ID 番号分析に関する関連知識を提供します。Linux プロセスは常に、名前空間内でプロセスを一意に識別するための番号を割り当てます。この番号はプロセス ID 番号 (略して PID) と呼ばれます。関連する問題を見てみましょう。皆さんの参考になれば幸いです。
この記事のコードは、Linux カーネル バージョン 5.15.13 から抜粋したものです。
Linux プロセスには、名前空間内でプロセスを一意に識別する番号が常に割り当てられます。この番号はプロセス ID 番号 (略して PID) と呼ばれます。フォークまたはクローンによって生成された各プロセスには、カーネルによって新しい一意の PID 値が自動的に割り当てられます。
各プロセスは、PID の特性値に加えて、その他の ID を持ちます。考えられるタイプはいくつかあります。 1. スレッド グループ内 (後で説明するように、プロセス内で CLONE_THREAD フラグを使用して、クローンによって作成されたプロセスのさまざまな実行コンテキストを呼び出します) 内のすべてのプロセスは、統一されたスレッド グループ ID を持ちます。 (TGID)。プロセスがスレッドを使用しない場合、その PID と TGID は同じです。スレッド グループのメイン プロセスはグループ リーダーと呼ばれます。クローンによって作成されたすべてのスレッドの task_struct の group_leader メンバーは、グループ リーダーの task_struct インスタンスを指します。
2. さらに、独立したプロセスをプロセス グループにマージすることができます (setpgrp システム コールを使用)。プロセスグループメンバーのtask_structのpgrp属性値は同じ、つまりプロセスグループリーダーのPIDとなります。プロセス グループにより、グループのすべてのメンバーへの信号の送信が簡素化され、さまざまなシステム プログラミング アプリケーションに役立ちます ([SR05] などのシステム プログラミングの文献を参照)。パイプされたプロセスは同じプロセス グループに含まれることに注意してください。
3. 複数のプロセス グループを 1 つのセッションにマージできます。セッション内のすべてのプロセスは同じセッション ID を持ち、それは task_struct のセッション メンバーに保存されます。 SID は、setsid システム コールを使用して設定できます。端末プログラミングに使用できます。
1.2. グローバル ID とローカル ID1. グローバル ID はカーネル本体と初期名前空間内で一意の ID 番号であり、システム起動時に起動される init プロセスは初期名前空間に属します。 ID タイプごとに、システム全体で一意であることが保証される特定のグローバル ID があります。
2. ローカル ID は特定の名前空間に属しており、グローバルな有効性を持ちません。各 ID タイプは、それが属するネームスペース内で有効ですが、同じタイプと値の ID が異なるネームスペースに出現する可能性があります。
1.3. ID の実装struct task_struct {...pid_t pid;pid_t tgid;...}
これら 2 つの項目は pid_t 型で、__kernel_pid_t として定義され、各アーキテクチャによって個別に定義されます。通常は int として定義され、232 個の異なる ID を同時に使用できます。
2. PID の管理
2.1. PID 名前空間の表現方法
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 の目的の 1 つは、孤立プロセスで wait4 を呼び出すことであり、名前空間ローカルの init バリアントもこのジョブを実行する必要があります。 child_reaper は、プロセスの task_struct へのポインターを保存します。
parent は親名前空間へのポインタであり、level は名前空間階層内の現在の名前空間の深さを表します。最初の名前空間のレベルは 0、名前空間の部分空間レベルは 1、次の層の部分空間レベルは 2 などとなります。より高いレベルの名前空間の ID は、より低いレベルの名前空間にも表示されるため、レベルの計算は重要です。カーネルは、特定のレベル設定から、プロセスに関連付けられる ID の数を推測できます。
2.2. PID 管理/* * 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,};
一个进程可能在多个命名空间中可见,而其在各个命名空间中的局部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中的散列表上。
假如已经分配了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,本质上就等同于寻找位图中第一个值为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; } /* * ENOMEM is not the most obvious choice especially for the case * where the child subreaper has already exited and the pid * namespace denies the creation of any new processes. But ENOMEM * is what we have exposed to userspace for a long time and it is * documented behavior for pid namespaces. So we can't easily * change it even if there were an error code better suited. */ retval = -ENOMEM; get_pid_ns(ns); refcount_set(&pid->count, 1); spin_lock_init(&pid->lock); for (type = 0; type < PIDTYPE_MAX; ++type) INIT_HLIST_HEAD(&pid->tasks[type]); init_waitqueue_head(&pid->wait_pidfd); INIT_HLIST_HEAD(&pid->inodes); upid = pid->numbers + ns->level; spin_lock_irq(&pidmap_lock); if (!(ns->pid_allocated & PIDNS_ADDING)) goto out_unlock; for ( ; upid >= pid->numbers; --upid) { /* Make the PID visible to find_pid_ns. */ idr_replace(&upid->ns->idr, pid, upid->nr); 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);}
相关推荐:《Linux视频教程》
以上がLinuxの古典的な手法のプロセスID番号を解析してみようの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。