本篇文章给大家带来了linux中进程id号分析的相关知识,linux进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程id号,简称pid,下面就一起来看一下相关问题,希望对大家有帮助。
本文中的代码摘自 linux内核5.15.13版本。
linux进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程id号,简称pid。用fork或clone产生的每个进程都由内核自动地分配了一个新的唯一的pid值。
一、进程id1.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; } /* * 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号的详细内容。