Let's analyze the process ID number of Linux classic techniques

WBOY
Release: 2022-01-26 17:00:55
forward
2807 people have browsed it

This article brings you relevant knowledge about process ID number analysis in Linux. Linux processes always assign a number to uniquely identify them in their namespace. This number is called the process ID number, or PID for short. Let’s take a look at the related issues. I hope it will be helpful to everyone.

Let's analyze the process ID number of Linux classic techniques

 The code in this article is excerpted from Linux kernel version 5.15.13.

Linux processes are always assigned a number that uniquely identifies them within their namespace. This number is called the process ID number, or PID for short. Each process spawned by fork or clone is automatically assigned a new unique PID value by the kernel.

1. Process ID

1.1. Other IDs

In addition to the characteristic value of PID, each process also has other IDs. There are several possible types: 1. In a thread group (in a process, use the flag CLONE_THREAD to call the different execution contexts of the process created by clone, as we will see later) All processes in a have a unified thread group ID (TGID). If the process does not use threads, its PID and TGID are the same. The main process in a thread group is called the group leader. The group_leader member of the task_struct of all threads created through clone will point to the task_struct instance of the group leader.

2. In addition, independent processes can be merged into process groups (using the setpgrp system call). The pgrp attribute value of the task_struct of the process group members is the same, that is, the PID of the process group leader. Process groups simplify sending signals to all members of the group, which is useful for a variety of system programming applications (see the systems programming literature, for example [SR05]). Note that piped processes are included in the same process group.

3. Several process groups can be merged into one session. All processes in the session have the same session ID, which is stored in the session member of task_struct. The SID can be set using the setsid system call. It can be used for terminal programming.

1.2. Global ID and local ID

The name space increases the complexity of PID management. The PID namespace is organized hierarchically. When a new namespace is created, all PIDs in the namespace are visible to the parent namespace, but the child namespace cannot see the PIDs of the parent namespace. But this means that some processes have multiple PIDs, and wherever the process's namespace can be seen, it will be assigned a PID. This must be reflected in the data structure. We have to distinguish between local ID and global ID.

1. The global ID is a unique ID number in the kernel itself and the initial namespace. The init process started during system startup belongs to the initial namespace. For each ID type, there is a given global ID that is guaranteed to be unique throughout the system.

2. The local ID belongs to a specific namespace and does not have global validity. For each ID type, they are valid within the namespace to which they belong, but IDs of the same type and value may appear in different namespaces.

1.3. ID implementation

  The global PID and TGID are directly saved in the task_struct, which are the pid and tgid members of the task_struct respectively, in the sched.h file:

struct task_struct {...pid_t pid;pid_t tgid;...}
Copy after login

These two items are of type pid_t, which is defined as __kernel_pid_t, which is defined separately by each architecture. Usually defined as int, 232 different IDs can be used at the same time.

2. Managing PID

  A small subsystem called the PID allocator (pid allocator) is used to speed up the allocation of new IDs. In addition, the kernel needs to provide helper functions to implement the function of finding the task_struct of a process by ID and its type, as well as the function of converting the kernel representation of the ID and the value visible to user space.

2.1. Representation method of PID namespace

 There is the following definition in the pid_namespace.h file:

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;
Copy after login

 Each PID namespace has a process, which plays a The function is equivalent to the global init process. One purpose of init is to call wait4 on the orphan process, and the namespace-local init variant must also do this job. child_reaper saves a pointer to the task_struct of the process.

parent is a pointer to the parent namespace, and level represents the depth of the current namespace in the namespace hierarchy. The level of the initial namespace is 0, the subspace level of the namespace is 1, the subspace level of the next layer is 2, and so on. The calculation of level is important because IDs in namespaces with higher levels are visible to namespaces with lower levels. From a given level setting, the kernel can infer how many IDs a process will be associated with.

2.2. PID management

2.2.1. PID data structure

PID management revolves around two data Struct expansion: struct pid is the kernel's internal representation of PID, while struct upid represents the information visible in a specific namespace. The definitions of the two structures are in the file pid.h, as follows:

/*
 * 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];};
Copy after login

  对于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,};
Copy after login

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;...};
Copy after login

  将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]);}
Copy after login

  这里建立了双向连接: 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);}
Copy after login

相关推荐:《Linux视频教程

The above is the detailed content of Let's analyze the process ID number of Linux classic techniques. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:csdn.net
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template