Cet article vous apporte des connaissances pertinentes sur l'analyse des numéros d'identification des processus sous Linux. Les processus Linux attribuent toujours un numéro pour les identifier de manière unique dans leur espace de noms. Ce numéro est appelé numéro d'identification du processus, ou PID en abrégé. Jetons un coup d'œil aux problèmes associés, j'espère qu'il sera utile à tout le monde.
Le code de cet article est extrait de la version 5.15.13 du noyau Linux.
Les processus Linux se voient toujours attribuer un numéro qui les identifie de manière unique dans leur espace de noms. Ce numéro est appelé numéro d'identification du processus, ou PID en abrégé. Chaque processus généré par un fork ou un clone se voit automatiquement attribuer une nouvelle valeur PID unique par le noyau.
En plus de la valeur caractéristique du PID, chaque processus a également d'autres identifiants. Il existe plusieurs types possibles : 1. Tous les processus dans un groupe de threads (dans un processus, utilisez le flag CLONE_THREAD pour appeler les différents contextes d'exécution du processus créé par clone, comme nous le verrons plus tard) Tous ont un ID de groupe de threads unifié ( TGID). Si le processus n'utilise pas de threads, son PID et son TGID sont les mêmes. Le processus principal d'un groupe de threads est appelé le chef de groupe. Le membre group_leader de task_struct de tous les threads créés via le clonage pointera vers l'instance task_struct du chef de groupe.
2. De plus, les processus indépendants peuvent être fusionnés en groupes de processus (à l'aide de l'appel système setpgrp). La valeur de l'attribut pgrp de task_struct des membres du groupe de processus est la même, c'est-à-dire le PID du chef du groupe de processus. Les groupes de processus simplifient l'opération d'envoi de signaux à tous les membres du groupe, ce qui est utile pour une variété d'applications de programmation système (voir la littérature sur la programmation système, telle que [SR05]). Notez que les processus redirigés sont inclus dans le même groupe de processus.
3. Plusieurs groupes de processus peuvent être fusionnés en une seule session. Tous les processus de la session ont le même ID de session, qui est stocké dans le membre de session de task_struct. Le SID peut être défini à l’aide de l’appel système setsid. Il peut être utilisé pour la programmation du terminal.
1.2. ID global et ID local1. L'ID global est un numéro d'identification unique dans le noyau lui-même et l'espace de noms initial. Le processus d'initialisation démarré lors du démarrage du système appartient à l'espace de noms initial. Pour chaque type d’ID, il existe un ID global donné dont l’unicité est garantie dans tout le système.
2. L'ID local appartient à un espace de noms spécifique et n'a pas de validité globale. Pour chaque type d'ID, ils sont valides dans l'espace de noms auquel ils appartiennent, mais des ID du même type et de la même valeur peuvent apparaître dans des espaces de noms différents.
1.3, implémentation de l'IDstruct task_struct {...pid_t pid;pid_t tgid;...}
Ces deux éléments sont. les deux types pid_t, qui sont définis par ce type est __kernel_pid_t, qui est défini séparément par chaque architecture. Généralement défini comme int, 232 identifiants différents peuvent être utilisés en même temps.
2. Gestion des PID
2.1. Méthode de représentation de l'espace de noms 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;
Chaque espace de noms PID a un processus, qui joue un rôle équivalent au processus d'initialisation global. L'un des objectifs de init est d'appeler wait4 sur le processus orphelin, et la variante d'initialisation locale de l'espace de noms doit également faire ce travail. child_reaper enregistre un pointeur vers la task_struct du processus.
parent est un pointeur vers l'espace de noms parent et le niveau représente la profondeur de l'espace de noms actuel dans la hiérarchie des espaces de noms. Le niveau de l'espace de noms initial est 0, le niveau de sous-espace de l'espace de noms est 1, le niveau de sous-espace de la couche suivante est 2, et ainsi de suite. Le calcul du niveau est important car les identifiants des espaces de noms avec des niveaux supérieurs sont visibles pour les espaces de noms avec des niveaux inférieurs. À partir d'un paramètre de niveau donné, le noyau peut déduire le nombre d'identifiants auxquels un processus sera associé.
2.2. Gestion du 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视频教程》
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!