這篇文章為大家帶來了linux中進程ID號分析的相關知識,Linux進程總是會分配一個號碼用於在其命名空間中唯一地標識它們。該號碼被稱為進程ID號,簡稱PID,以下就一起來看相關問題,希望對大家有幫助。
本文中的程式碼摘自 Linux核心5.15.13版本。
Linux程序總是會分配一個號碼用於在其命名空間中唯一地標識它們。該號碼被稱為進程ID號,簡稱PID。用fork或clone產生的每個進程都由核心自動地分配了一個新的唯一的PID值。
# 每個行程除了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系統呼叫設定。它可以用於終端程式設計。
名空間增加了PID管理的複雜度。 PID命名空間依層級組織。在建立一個新的命名空間時,該命名空間中的所有PID對父命名空間都是可見的,但子命名空間無法看到父命名空間的PID。但這意味著某些進程具有多個PID,凡可以看到該進程的命名空間,都會為其分配一個PID。這必須反映在資料結構中。我們必須區分局部ID和全域ID。
1、全域ID是在核心本身和初始命名空間中的唯一ID號,在系統啟動期間開始的init進程即屬於初始命名空間。對每個ID類型,都有一個給定的全域ID,保證在整個系統中是唯一的。
2、 局部ID屬於某個特定的命名空間,不具備全域效度。對每個ID類型,它們在所屬的命名空間內部有效,但類型相同、值也相同的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 allocator)用來加速新ID的分配。此外,核心需要提供輔助函數,以實現透過ID及其類型查找進程的task_struct的功能,以及將ID的核心表示形式和用戶空間可見的數值進行轉換的功能。
在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。
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,};
一个进程可能在多个命名空间中可见,而其在各个命名空间中的局部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中文網其他相關文章!