Linux 进程 PID 大家都知道,top
命令就可以很容易看到各个进程的 PID, 稍进一步top -H
,我们还能够看到各个线程的ID, 即TID。今天我们想深入到Linux Kernel, 看一看在 Kernel里PID的来龙去脉。
阅读本文 ,您可以了解到:
task_struct
上,它也是CPU调度的实体。 不管是进程PID还是线程TID,都是用task_struct::pid
表示;TGID
,它就等于这个主线程的TID,用task_struct::tgid
表示;thread_test
的进程,这个进程首先创建一个线程,然后再 fork出一个进程,具体代码可以参考这里, 然后我们来查看这个thread_test
相关的各种id信息:lw@lw-OptiPlex-7010:~$ ps -T -eo tid,pid,ppid,tgid,pgid,sid,comm | grep thread_test
tid pid ppid tgid pgid sid comm
1294928 1294928 1292449 1294928 1294928 1292449 thread_test (主进程)
1294929 1294928 1292449 1294928 1294928 1292449 thread_test (创建的线程)
1294933 1294933 1294928 1294933 1294928 1292449 thread_test (fork的新进程)
我们看到:
pid.png
我们以Linux Kernel 5.4.2 为例介绍
getpid
和getppid
来获取到,它们返回的pid_t
类型其实就是个int
类型;pid_namespace.png
为了表述方便 ,我们将PID Namespace简化为PN 从上图我们可以看到 : PN1-PID2, PN2-PID10和PN4-PID1它们都指向的是同一个task 1; PN1-PID3, PN2-PID8和PN5-PID1它们都指向的是同一个task 2;
由此我们可以得出一个结论:在Kernel中一个task的ID由两个元素
唯一确定 [pid namespace, processid id],在内核中用upid
表示:
struct upid {
int nr; // process id
struct pid_namespace *ns; // 所属的pid namespace
}
lw@lw-OptiPlex-7010:~/opensources/ExampleBank/thread_test$ echo $$
1292449
可以看到这个bash进程ID是 1292449,我们将这一层作为pid namespace 1;
b. 使用unshare
创建一个新的pid namespace, 并且启动一个新的bash进程:
```shell
lw@lw-OptiPlex-7010:~/opensources/ExampleBank/thread_test$ sudo unshare --pid --mount-proc --fork /bin/bash
root@lw-OptiPlex-7010:/home/lw/opensources/ExampleBank/thread_test# echo $$
1
```
可以看到当前bash进程ID是1, 是在一个新的pid namespace中,我们将这一层作为pid namespace 2;
c. 我们在pid namespace 2的bash中再创建一个新的pid namespace:
lw@lw-OptiPlex-7010:~/opensources/ExampleBank/thread_test$ sudo unshare --pid --mount-proc --fork /bin/bash
root@lw-OptiPlex-7010:/home/lw/opensources/ExampleBank/thread_test# echo $$
1
可以看到当前bash进程ID是1, 是在一个新的pid namespace中,我们将这一层作为pid namespace 3;
d. 我们在pid namespace 3的bash中在后台运行vim
root@lw-OptiPlex-7010:/home/lw/opensources/ExampleBank/thread_test# vim 1 &
[1] 8
可以看到这个vim在当前pid namespace 3中的pid是8;
e. 新开启一个bash, 我们查看一下在顶层pid namespace 1中的pid:
lw@lw-OptiPlex-7010:~$ pstree -pl | grep bash |grep unshare | grep -v grep
| |-bash(1292449)---sudo(1565473)---unshare(1565476)---bash(1565477)---sudo(1565555)---unshare(1565556)---bash(1565557)---vim(1565982)
可以看到在顶层pid namespace 1中: pid namespace 1中启动的bash的pid是1292449 pid namespace 2中启动的bash的pid是1565477 pid namespace 3中启动的bash的pid是1565557
f. 在顶层pid namespace 1的bash中查看vim 1
的pid 是 1565982:
lw@lw-OptiPlex-7010:~$ ps aux |grep 'vim 1' | grep -v grep
root 1565982 0.0 0.1 64980 19980 pts/2 T 18:56 0:00 vim 1
g. 在pid namespace 2的bash中查看vim 1
的pid 是 17:
//使用nsenter先进入pid namespace2, 这个 1565477 即为pid namespace 2中bash进程的PID
lw@lw-OptiPlex-7010:~$ sudo nsenter --pid --mount -t 1565477
root@lw-OptiPlex-7010:/# ps aux |grep 'vim 1' | grep -v grep
root 17 0.0 0.1 64980 19980 pts/2 T 18:56 0:00 vim 1
h. 在pid namespace 3的bash中查看vim 1
的pid 是 8:
//使用nsenter先进入pid namespace2
lw@lw-OptiPlex-7010:~$ sudo nsenter --pid --mount -t 1565557
root@lw-OptiPlex-7010:/# ps aux |grep 'vim 1' | grep -v grep
root 8 0.0 0.1 64980 19980 pts/2 T 18:56 0:00 vim 1
struct pid
表示, 它定义在incluse/linux/pid.h
中;
enum pid_type { PIDTYPE_PID, PIDTYPE_TGID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX, }; struct pid { refcount_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; /* wait queue for pidfd notifications */ wait_queue_head_t wait_pidfd; struct rcu_head rcu; struct upid numbers[1]; };
struct pid
时,numbers
会被扩展到level
个元素; 它用来容纳在每一层pid namespace中的 id;
tasks
是一个hash链表的数组,经过前面那么多的铺垫,我们知道了如下两件事:
在内核中进程的PID采用struct pid
结构体来表示,这就需要在struct pid
结构体中有成员可以保存这个task在各级PID Namespace中的对应的不同的进程ID:
struct pid
{
...
unsigned int level; //表示当前进程里在第几级pid namespace中创建
...
struct upid numbers[1]; //用于保存在每级pid namespace中的进程ID(struct upid)
};
struct pid
结构体的声明,您可能会觉得奇怪,明明说了numbers
数组保存每级pid namespace中的进程ID,但是为什么这个数据只有一个元素?
这个是Kernel里惯用的手法,结构体最后一个元素可以声明为动态数组,动态数组一般是在创建这个结构体时被动态分配内存。我们来看一下struct pid
结构体的创建和初始化,这要追溯到task的创建,最终会追溯到定义在kernel/fork.c
中的copy_process
函数,它的具体流程我们在这里不详述,只看相关的pid部分:
static __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args) { ... if (pid != &init_struct_pid) { pid = alloc_pid(p->nsproxy->pid_ns_for_children); if (IS_ERR(pid)) { retval = PTR_ERR(pid); goto bad_fork_cleanup_thread; } } ... }
下面我们来过一个alloc_pid
函数,它定义在 kernel/pid.c
中:
struct pid *alloc_pid(struct pid_namespace *ns) { //分配内存并构造struct pid pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); if (!pid) return ERR_PTR(retval); tmp = ns; pid->level = ns->level; }
kmem_cache_alloc
分配内存并构造struct pid,它使用父pid namespace的pid_cachep来分配内存,这个pid_cachep
是kmem_cache
类型 ,它在create_pid_namepsace
中被创建:
static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns, ▸ struct pid_namespace *parent_pid_ns) { ▸ struct pid_namespace *ns; //这里确定了当前pid namespace的level, 是父pid namespace level + 1 ▸ unsigned int level = parent_pid_ns->level + 1; ... //分配当前pid namespace结构体所需的内存 ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL); ... //创建上面所说的 kmem_cache, 将当前level作为参数传入 ns->pid_cachep = create_pid_cachep(level); ... }
create_pid_cachep
:
static struct kmem_cache *create_pid_cachep(unsigned int level) { ▸ /* Level 0 is init_pid_ns.pid_cachep */ ▸ struct kmem_cache **pkc = &pid_cache[level - 1]; ▸ struct kmem_cache *kc; ▸ char name[4 + 10 + 1]; ▸ unsigned int len; ▸ kc = READ_ONCE(*pkc); ▸ if (kc) ▸ ▸ return kc; ▸ snprintf(name, sizeof(name), "pid_%u", level + 1); //这里是关键,计算需要分配的内存,除了 sizeof(struct pid) //还需要加上 level * sizeof(struct upid), 这个就是上面提到的 //动态数据的大小,按level的大小来分配 ▸ len = sizeof(struct pid) + level * sizeof(struct upid); ▸ mutex_lock(&pid_caches_mutex); ▸ /* Name collision forces to do allocation under mutex. */ ▸ if (!*pkc) ▸ ▸ *pkc = kmem_cache_create(name, len, 0, SLAB_HWCACHE_ALIGN, 0); ▸ mutex_unlock(&pid_caches_mutex); ▸ /* current can fail, but someone else can succeed. */ ▸ return READ_ONCE(*pkc); }
到此,我们解决了struct pid
中numbers
的分配问题。
struct pid
和PID Namespace的关系pid_namespace_task.png
如上图所示:
一个task_strcut表示一个 进程或线程,其thread_pid
成员变量指向它的struct pid
结构体;
struct pid
结构体的numbers
数组有三个元素,分别表示在PID Namespace 1,2,3中的三个uid;
task
。进程,线程,主线程统统用task_struct
结构体表示;taks_struct
中有个成员变量struct signal_struct *signal
,这个struct signal_struct
也有个成员变量struct pid *pids[PIDTYPE_MAX];
,如果当前的task表示进程,那这个pids数组
用来存储PID, TGID, PGID, SID,其赋值是在copy_process
中进行:
//当前创建的p是进程 if (thread_group_leader(p)) { //新生成的pid就是这个线程组的TGID ▸ init_task_pid(p, PIDTYPE_TGID, pid); //新生成的task和current task是同属于一个进程组,有相同的PGID ▸ init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); //新生成的task和current task是同属于一个Session,有相同的session id ▸ init_task_pid(p, PIDTYPE_SID, task_session(current)); ▸ ... ▸ /* ▸ * Inherit has_child_subreaper flag under the same ▸ * tasklist_lock with adding child to the process tree ▸ * for propagate_has_child_subreaper optimization. ▸ */ ▸ ... ▸ attach_pid(p, PIDTYPE_TGID); ▸ attach_pid(p, PIDTYPE_PGID); ▸ attach_pid(p, PIDTYPE_SID); ▸ __this_cpu_inc(process_counts); }
我们来重点看下attach_pid
的实现:
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]); }
主要作用就是能过调用hlist_add_head_rcu
把当前task连接入pid->tasks对应的hash链表;
我们以PIDTYPE_PGID
来举例说明,同属于一个进程组的所有进程对应的taks_struct
都被链接到同一个hash链表上:
pgid.png
从上面的介绍我们知道PID还有一种类型是PIDTYEP_TGID
, 对应的是线程组,但是从代码实现来看同属一个线程组的所有线程并没有使用上一节中的pid::taks[PIDTYE_TGID]
和task_struct::pid_links[PIDTYPE_TGID]
来链接起来。
在copy_process
中有如下一段代码:
if (clone_flags & CLONE_THREAD) {
▸ p->exit_signal = -1;
▸ p->group_leader = current->group_leader;
▸ p->tgid = current->tgid;
}
其中group_leader
表示的是一个线程组的leader, 也就是一个进程的主线程。
在copy_process
中还有如下一段代码:
list_add_tail_rcu(&p->thread_group,
▸ ▸ &p->group_leader->thread_group);
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
这段代码就是将当前的线程对应的task_strcut
链接到其所在进程的主线程的thread_group
链表中,至此同属于同一个线程组的所有线程对应的task_struct
就全部链表在同一个双向链表中了,同时也使用thread_node
和signal->thread_head
将当前的thread都链表到了主线程的signal
中的thread_head
链表,通过它就可以很方便的遍历当前进程中的所有线程了:
tgid.png
关于PID的相关内容我们暂时就先讲到这里,希望可以帮大家理清一些基本概念,疏漏之处,望大家指证。