이 문서에서는 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. 여러 프로세스 그룹을 하나의 세션으로 병합할 수 있습니다. 세션의 모든 프로세스는 task_struct의 세션 멤버에 저장된 동일한 세션 ID를 갖습니다. SID는 setid 시스템 호출을 사용하여 설정할 수 있습니다. 터미널 프로그래밍에 사용할 수 있습니다.
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;...}
이 두 항목은 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의 목적 중 하나는 고아 프로세스에서 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!