[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
Pid Namespace 详解
By pwl999 of TinyLab.org Feb 02, 2021
pid 解析
在 Linux 下获取 pid,会发现有好几种类似的 id(pid、tgid、pgid、sid),这几种的区别在哪里呢?各自的使用场景和用法是什么呢?下面我们就来一一解析。
在 kernel 中,进程 process 是最小的调度单位(对应数据结构 task_struct)。Linux 下没有单独线程的概念,只有轻量级进程的概念,如果一个进程它和其他进程共享进程空间 mm 和文件句柄 fd 等一些资源,那它就是轻量级进程相当于线程 thread。多个轻量级进程组成了一个线程组(thread group),线程组中第一个创建的轻量级进程称之为 group_leader,其中的每一个轻量级进程(thread)拥有自己独立的 pid,所有的轻量级进程共享同一个线程组 tgid(即 group_leader 的 pid)。
除了上述 pid 和 tgid 两种,还有更大的进程集合。在系统启动以后,用户使用系统首先需要使用创建会话 session,我们一般看到的 tty(比如键盘、屏幕的 tty1~tty7,或者网络连接的虚拟 tty 即 pty),一个 tty 对应一个 session。在当前 tty 中创建的所有进程都共享一个 sid(即 leader 的 pid)。
在一个 session 中,还存在着前后进程组的概念,这是大型机时为了多个用户使用同一个 tty 来引申出的概念,在一个 session 下可以创建多个 job,每个 job 称为一个进程组(process group),只能有一个前台进程组可以有多个后台进程组。进程组的所有进程都共享一个 pgid(即 leader 的pid)。
相关的数据结构如下:
item | member |
---|---|
进程 process (pid) | task_struct->pid //pid_t |
task_struct->pids[PIDTYPE_PID]->pid //struct pid * | |
线程组 thread group (tgid) | task_struct->tgid //pid_t |
task_struct->group_leader //struct task_struct * | |
task_struct->signal->leader_pid //struct pid * | |
进程组 process group (pgid) | task_struct->pids[PIDTYPE_PGID]->pid //struct pid * |
会话组 session (sid) |
struct pid_link
细心的同学已经发现,保存 pid 有两种结构:一个是保存 pid number 的 pid_t
;另外是一个结构体 struct pid
,它有一个数组 ->numbers[]
可以保存多个 pid number,还有一个进程链表数组 tasks[]
。
设计成两个数据结构的原因如下:
- 因为多个 namespace 的存在,需要在每个名空间都一个 pid number,所以我们需要一个 pid number 数组。而
task_struct->pid
和task_struct->tgid
仅仅保存了第 0 层名空间的 pid number。 - 需要建立起 task_struct 和各种 pid 之间的双向查询关系,所以需要一些链表结构。
另外,为了简化这些复杂的关系,把 struct pid
结构从 task_struct
中抽离出来,而 task_struct
中使用的是 struct pid_link
类型的成员。
下面具体分析每个场景下,pid 的链接关系。
process
每一个进程 process(包括轻量级进程/thread)创建时,都会创建一个对应的 struct pid
数据结构,struct pid
创建了一个 pid number 数组,在每一层名空间中都分配了一个 pid number。
process 的 struct task_struct
和 struct pid
数据结构之间的双向查询关系:
direction | description |
---|---|
task_struct → pid | 通过 task->pids[PIDTYPE_PID].pid 指针指向 struct pid |
pid → task_struct | 通过 pid->tasks[PIDTYPE_PID] 链表找到 task_struct,理论上该链表只有一个成员 |
thread group
我们看到线程组其实是一个异类,就它没有使用 pid_link
而是用了一堆的私有结构。为什么会形成这种局面?估计是历史原因造成的。
普通轻量级进程(线程)和线程组leader线程之间的双向查询关系:
direction | description |
---|---|
thread → group leader | 普通线程 task->group_leader 存放线程组 leader 线程的 task_struct 结构 |
普通线程 task->signal->leader_pid 存放线程组 leader 线程的 struct pid | |
group leader → thread | 线程组 leader 线程的 task->thread_group 链表,链接了本线程组所有线程的 task_struct |
遍历线程组的实例代码:
while (t) {
t = next_thread(t);
}
process group
我们来看看进程组的关系图,进程组也是使用 pid_link
来进行链接的。每个进程的进程组 pgid 指向同一个 leader,需要注意反向由进程组 leader 查询进程时的只能查询到线程组 leader,因为只把线程组 leader 链接到一起,而线程组下的普通线程由线程组 leader 自己来组织。
线程组 leader 和进程组 leader 之间的双向查询关系:
direction | description |
---|---|
thread group leader → process group leader | 线程组learder的task->pids[PIDTYPE_PGID].pid指针指向进程组leader的struct pid |
process group leader → thread group leader | 进程组leader的pid->tasks[PIDTYPE_PGID]链表链接了所有线程组learder的task_struct结构 |
遍历进程组的实例代码:
struct pid *pgrp;
struct task_struct *p = NULL;
do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
...
} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
#define do_each_pid_task(pid, type, task) \
do { \
if ((pid) != NULL) \
hlist_for_each_entry_rcu((task), \
&(pid)->tasks[type], pids[type].node) {
/*
* Both old and new leaders may be attached to
* the same pid in the middle of de_thread().
*/
#define while_each_pid_task(pid, type, task) \
if (type == PIDTYPE_PID) \
break; \
} \
} while (0)
session
我们来看看会话的关系图,会话也是使用 pid_link
来进行链接的。每个进程的会话 sid 指向同一个 leader,需要注意反向由会话 leader 查询进程时的只能查询到线程组 leader,因为只把线程组 leader 链接到一起,而线程组下的普通线程由线程组 leader 自己来组织。
线程组 leader 和会话 leader 之间的双向查询关系:
direction | description |
---|---|
thread group leader → process group leader | 线程组learder的task->pids[PIDTYPE_SID].pid指针指向会话leader的struct pid |
process group leader → thread group leader | 会话leader的pid->tasks[PIDTYPE_SID]链表链接了所有线程组learder的task_struct结构 |
遍历会话的实例代码:
struct pid *session
struct task_struct *p;
do_each_pid_task(session, PIDTYPE_SID, p) {
...
} while_each_pid_task(session, PIDTYPE_SID, p);
pid 的初始化
在进程的创建的时候,有对各种 pid 的初始化:
_do_fork() → copy_process():
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
/* (1) 分配一个新的task_struct结构,并且拷贝父进程task中所有内容
所以接下来如果没有给新task_struct中的成员赋新值,那么它的值就是和父进程一样的
*/
p = dup_task_struct(current, node);
/* (2) 拷贝名空间 */
retval = copy_namespaces(clone_flags, p);
/* (3) 在名空间中分配一个`struct pid *`结构 */
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;
}
}
/* (4.1) 取出`struct pid`中最顶层名空间的pid number,赋值给task_struct->pid
task_struct->pid相当于是一个快捷方式,取最顶层的pid number不用每次去查询struct pid->numbers[].nr数组
*/
p->pid = pid_nr(pid);
/* (4.2) 线程组tgid的赋值,创建线程时:
group_leader等于父进程的group_leader
tgid等于父进程的tgid
*/
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
/* (4.3) 线程组tgid的赋值,创建进程(group_leader)时:
group_leader等于本进程的group_leader
tgid等于本进程的tgid
*/
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
p->tgid = p->pid;
}
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
/* (5.1) 赋值新结构task_struct->pids[PIDTYPE_PID].pid,将新分配的`struct pid`结构赋值给它 */
init_task_pid(p, PIDTYPE_PID, pid);
/* (5.2) 如果本进程是线程组的group leader进行pgid和sid的新赋值和链接操作
如果本进程是线程组中一个普通线程,它的pgid和sid从group leader复制继承,并且不会加入到pgid和sid的链表当中
*/
if (thread_group_leader(p)) {
/* (5.2.1) 赋值新结构task_struct->pids[PIDTYPE_PGID].pid,
将当前进程的所在线程组的group_leader的pgid赋值给他:task->group_leader->pids[PIDTYPE_PGID].pid
*/
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
/* (5.2.2) 赋值新结构task_struct->pids[PIDTYPE_SID].pid,
将当前进程的所在线程组的group_leader的sid赋值给他:task->group_leader->pids[PIDTYPE_SID].pid
*/
init_task_pid(p, PIDTYPE_SID, task_session(current));
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
/* (5.2.3) 给线程组group_leader的leader_pid赋值`struct pid`结构
p->signal保存的是这个线程组公共的信号
p->pending保存的是每个进程私有的信号
*/
p->signal->leader_pid = pid;
p->signal->tty = tty_kref_get(current->signal->tty);
/*
* Inherit has_child_subreaper flag under the same
* tasklist_lock with adding child to the process tree
* for propagate_has_child_subreaper optimization.
*/
p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
p->real_parent->signal->is_child_subreaper;
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
/* (5.2.4) 当前是线程组的group_leader
将新进程的task_struct结构加入到进程所在pgid的pid->tasks[PIDTYPE_PGID]链表当中
*/
attach_pid(p, PIDTYPE_PGID);
/* (5.2.5) 当前是线程组的group_leader
将新进程的task_struct结构加入到进程所在pgid的pid->tasks[PIDTYPE_SID]链表当中
*/
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
atomic_inc(¤t->signal->sigcnt);
/* (5.3.1) 将线程加入group_leader的thread_group链表 */
list_add_tail_rcu(&p->thread_group,
&p->group_leader->thread_group);
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
}
/* (5.4) 将新进程的task_struct结构加入到pid->tasks[PIDTYPE_PID]链表当中
*/
attach_pid(p, PIDTYPE_PID);
nr_threads++;
}
}
pid namespace
Namespace 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。Linux 内核支持的 namespaces 如下:
名称 | 宏定义 | 隔离内容 |
---|---|---|
Cgroup | CLONE_NEWCGROUP | Cgroup root directory (since Linux 4.6) |
IPC | CLONE_NEWIPC | System V IPC, POSIX message queues (since Linux 2.6.19) |
Network | CLONE_NEWNET | Network devices, stacks, ports, etc. (since Linux 2.6.24) |
Mount | CLONE_NEWNS | Mount points (since Linux 2.4.19) |
PID | CLONE_NEWPID | Process IDs (since Linux 2.6.24) |
User | CLONE_NEWUSER | User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8) |
UTS | CLONE_NEWUTS | Hostname and NIS domain name (since Linux 2.6.19) |
上图可以看到 pid namespace 的组织结构,pid namespace 使用父子关系组成了树形。在分配新 pid 结构,会从当前的 pid ns(task->nsproxy->pid_ns_for_children->pid_cachep
) 中分配一个 struct pid
结构,struct pid
是一个数组包含了向上的所有 pid ns,在每个 pid ns 中都分配了一个 pid number。
注意事项:
当 unshare PID namespace 时,调用进程会为它的子进程分配一个新的 PID Namespace,但是调用进程本身不会被移到新的 Namespace 中。而且调用进程第一个创建的子进程在新 Namespace 中的 PID 为 1,并成为新 Namespace 中的 init 进程。setns() 系统调用也是类似的,调用者进程并不会进入新的 PID Namespace,而是随后创建的子进程会进入。
为什么创建其他的 Namespace 时 unshare() 和 setns() 会直接进入新的 Namespace,而唯独 PID Namespace 不是如此呢?
因为调用 getpid() 函数得到的 PID 是根据调用者所在的 PID Namespace 而决定返回哪个 PID,进入新的 PID namespace 会导致 PID 产生变化。而对用户态的程序和库函数来说,他们都认为进程的 PID 是一个常量,PID 的变化会引起这些进程奔溃。换句话说,一旦程序进程创建以后,那么它的 PID namespace 的关系就确定下来了,进程不会变更他们对应的 PID namespace。
下面从三个 API(clone/setns/unshare)切入来解读。
clone(CLONE_NEWPID)
我们来分析系统调用 clone()
对 CLONE_NEWPID
的处理:
__do_fork() → copy_process() → copy_namespaces() → create_new_namespaces() → copy_pid_ns():
static struct nsproxy *create_new_namespaces(unsigned long flags,
struct task_struct *tsk, struct user_namespace *user_ns,
struct fs_struct *new_fs)
{
struct nsproxy *new_nsp;
int err;
/* (1) 分配一个进程的名空间代理 */
new_nsp = create_nsproxy();
if (!new_nsp)
return ERR_PTR(-ENOMEM);
new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
if (IS_ERR(new_nsp->mnt_ns)) {
err = PTR_ERR(new_nsp->mnt_ns);
goto out_ns;
}
new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
if (IS_ERR(new_nsp->uts_ns)) {
err = PTR_ERR(new_nsp->uts_ns);
goto out_uts;
}
new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
if (IS_ERR(new_nsp->ipc_ns)) {
err = PTR_ERR(new_nsp->ipc_ns);
goto out_ipc;
}
/* (2) 根据flags,判断当前pid名空间是引用旧的,还是创建一个新的
*/
new_nsp->pid_ns_for_children =
copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
if (IS_ERR(new_nsp->pid_ns_for_children)) {
err = PTR_ERR(new_nsp->pid_ns_for_children);
goto out_pid;
}
new_nsp->cgroup_ns = copy_cgroup_ns(flags, user_ns,
tsk->nsproxy->cgroup_ns);
if (IS_ERR(new_nsp->cgroup_ns)) {
err = PTR_ERR(new_nsp->cgroup_ns);
goto out_cgroup;
}
new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
if (IS_ERR(new_nsp->net_ns)) {
err = PTR_ERR(new_nsp->net_ns);
goto out_net;
}
return new_nsp;
}
↓
struct pid_namespace *copy_pid_ns(unsigned long flags,
struct user_namespace *user_ns, struct pid_namespace *old_ns)
{
/* (2.1) 如果没有设置CLONE_NEWPID标志,引用旧的pid_namespace */
if (!(flags & CLONE_NEWPID))
return get_pid_ns(old_ns);
if (task_active_pid_ns(current) != old_ns)
return ERR_PTR(-EINVAL);
/* (2.2) 如果设置了CLONE_NEWPID标志,创建一个新的pid_namespace并引用 */
return create_pid_namespace(user_ns, old_ns);
}
↓
static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns,
struct pid_namespace *parent_pid_ns)
{
struct pid_namespace *ns;
/* (2.2.1) 在父名空间的基础上,将level加1 */
unsigned int level = parent_pid_ns->level + 1;
struct ucounts *ucounts;
int err;
err = -EINVAL;
if (!in_userns(parent_pid_ns->user_ns, user_ns))
goto out;
err = -ENOSPC;
if (level > MAX_PID_NS_LEVEL)
goto out;
ucounts = inc_pid_namespaces(user_ns);
if (!ucounts)
goto out;
err = -ENOMEM;
ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
if (ns == NULL)
goto out_dec;
idr_init(&ns->idr);
/* (2.2.2) 根据名空间level层级,创建对应的struct pid的slub内存池
因为level不一样,struct pid中包含的pid number数组大小也不一样,所以实际struct pid大小是根据level动态变化的
*/
ns->pid_cachep = create_pid_cachep(level + 1);
if (ns->pid_cachep == NULL)
goto out_free_idr;
err = ns_alloc_inum(&ns->ns);
if (err)
goto out_free_idr;
ns->ns.ops = &pidns_operations;
kref_init(&ns->kref);
ns->level = level;
ns->parent = get_pid_ns(parent_pid_ns);
ns->user_ns = get_user_ns(user_ns);
ns->ucounts = ucounts;
ns->pid_allocated = PIDNS_ADDING;
INIT_WORK(&ns->proc_work, proc_cleanup_work);
return ns;
out_free_idr:
idr_destroy(&ns->idr);
kmem_cache_free(pid_ns_cachep, ns);
out_dec:
dec_pid_namespaces(ucounts);
out:
return ERR_PTR(err);
}
进程的 pid_namespace 存储在 task->nsproxy->pid_ns_for_children
中,在进程创建时就会从对应的名空间中分配 struct pid
结构。因为 struct pid
的 number 数组大小和名空间 level 是一致的,所以在每个 level 层级名空间,都会分配一个 pid number。
copy_process()
{
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;
}
}
}
↓
struct pid *alloc_pid(struct pid_namespace *ns)
{
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid;
int retval = -ENOMEM;
/* (1) 从名空间中分配一个对应的struct pid数据 */
pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
if (!pid)
return ERR_PTR(retval);
tmp = ns;
pid->level = ns->level;
/* (2) 在每个层级名空间,分配一个对应的pid number,存储到struct pid中的number数组 */
for (i = ns->level; i >= 0; i--) {
int pid_min = 1;
idr_preload(GFP_KERNEL);
spin_lock_irq(&pidmap_lock);
/*
* 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).
*/
/* (2.1) 分配pid number */
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;
}
if (unlikely(is_child_reaper(pid))) {
if (pid_ns_prepare_proc(ns))
goto out_free;
}
...
}
setns(CLONE_NEWPID)
SYSCALL_DEFINE2(setns, int, fd, int, nstype)
{
struct task_struct *tsk = current;
struct nsproxy *new_nsproxy;
struct file *file;
struct ns_common *ns;
int err;
/* (1) 根据fd找到对应的名空间 */
file = proc_ns_fget(fd);
if (IS_ERR(file))
return PTR_ERR(file);
err = -EINVAL;
ns = get_proc_ns(file_inode(file));
if (nstype && (ns->ops->type != nstype))
goto out;
/* (1) 创建新的进程名空间代理结构 */
new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs);
if (IS_ERR(new_nsproxy)) {
err = PTR_ERR(new_nsproxy);
goto out;
}
/* (2) 安装新的进程名空间代理 */
err = ns->ops->install(new_nsproxy, ns);
if (err) {
free_nsproxy(new_nsproxy);
goto out;
}
/* (3) 把进程名空间代理切换成新的
需要注意的是pid_namespace的特殊性,只有在子进程创建时才会生效
*/
switch_task_namespaces(tsk, new_nsproxy);
perf_event_namespaces(tsk);
out:
fput(file);
return err;
}
unshare(CLONE_NEWPID)
SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags)
{
err = unshare_nsproxy_namespaces(unshare_flags, &new_nsproxy,
new_cred, new_fs);
}
↓
int unshare_nsproxy_namespaces(unsigned long unshare_flags,
struct nsproxy **new_nsp, struct cred *new_cred, struct fs_struct *new_fs)
{
struct user_namespace *user_ns;
int err = 0;
if (!(unshare_flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWNET | CLONE_NEWPID | CLONE_NEWCGROUP)))
return 0;
user_ns = new_cred ? new_cred->user_ns : current_user_ns();
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
*new_nsp = create_new_namespaces(unshare_flags, current, user_ns,
new_fs ? new_fs : current->fs);
if (IS_ERR(*new_nsp)) {
err = PTR_ERR(*new_nsp);
goto out;
}
out:
return err;
}
相关函数
- 获取当前进程的 pid
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
↓
static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
return __task_pid_nr_ns(tsk, __PIDTYPE_TGID, NULL);
}
↓
pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type,
struct pid_namespace *ns)
{
pid_t nr = 0;
rcu_read_lock();
/* (1) 如果ns为NULL,则ns为当前名空间 */
if (!ns)
ns = task_active_pid_ns(current);
if (likely(pid_alive(task))) {
if (type != PIDTYPE_PID) {
if (type == __PIDTYPE_TGID)
type = PIDTYPE_PID;
task = task->group_leader;
}
/* (1) 根据type找到对应的struct pid结构:task->pids[type].pid
根据名空间的level,找到struct pid中对应名空间的pid number
*/
nr = pid_nr_ns(rcu_dereference(task->pids[type].pid), ns);
}
rcu_read_unlock();
return nr;
}
↓
pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
struct upid *upid;
pid_t nr = 0;
if (pid && ns->level <= pid->level) {
upid = &pid->numbers[ns->level];
if (upid->ns == ns)
nr = upid->nr;
}
return nr;
}
参考文档
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 | 微信打赏 | |
请作者喝杯咖啡吧 |