盒子
盒子
文章目录
  1. Overview
  2. Introduction
  3. Pre-internal
    1. PID namespace
    2. Overview of struct task_struct
      1. design
        1. 一个进程对应一个struct task_struct
        2. 进程区分了id类型
        3. 增加了pid namespaces的struct task_struct
      2. real_parent vs parent
  4. Internal
    1. getpid & gettid & getpgrp & getpgid & getsid
    2. setsid
  5. References

linux下的进程关系与pid namespace

Overview

Linux使用struct task_struct数据结构来关联所有与进程有关的数据和结构,定义了pidtgidpgidsid四种进程类型。

Note:

  • pid —— Process identifier,进程标识符。
  • tid —— Thread identifier,线程标识符。
  • tgid —— Thread group identifier,线程组标识符。
  • pgid —— Process group identifier,进程组标识符。
  • sid —— Session identifier,会话标识符。

在v4.12中,与四种进程类型有关的系统调用有以下8个:

getpid gettid
getppid getpgrp
getpgid setpgid
getsid setsid

Introduction

  • pid —— 在特定PID namespaces中,Linux使用pid唯一标识一个进程。使用fork / clone创建一个进程均会被分配一个新的且唯一的pid
  • tid —— Linux的线程由Native POSIX Thread Library(NPTL)实现,本质上是用进程模拟线程,因此内核中并没有tid这种数据,它由pid表示。线程是通过带有CLONE_THREADflag参数clone建立的进程。
  • tgid —— 同一个进程通过clone建立了的线程处于同一个线程组,该线程组的ID叫做tgid,线程组组长的tgid与其pid相同。若一个进程没有线程,则其tgidpid也相同。
  • pgid —— 进程组是一个或多个进程的集合,每个进程都属一个进程组,拥有相同的pgid。进程组组长(process group leader)的pid是进程组的ID,用以识别进程,可通过setpgrp为进程设置pgid
  • sid —— 几个进程组使用setsid可以合并成为一个会话组,会话组内的所有进程拥有相同的sid

    Pre-internal

    PID namespace

    PID namespaces隔离了进程ID,指的是不同的进程在不同的PID namespaces可以有着相同的PID。这个特性是在主机之间迁移容器的先决条件;只有一个PID namespaces的时候,在保持PID不变(保持PID不变是一种需求)的情况下将其迁移到另一个主机可能会失败,因为目标节点上可能存在相同PID的进程。

PID namespaces具有层级顺序,低级的PID namespaces对高级的不可见。一个PID namespaces可以具有多个child PID namespaces,每一个PID namespaces都可以看做parent PID namespaces的一个局部视图。一个新的PID namespaces B从当前PID namespaces A被创建后,处于当前PID namespaces A的进程可以看到B中的所有进程,但反之不成立。

PID Namespaces

新的PID namespaces通过带有CLONE_NEWPIDflag参数的clone系统调用创建。新namespace种第一个进程的PID是1,它是这个namespace的init进程,属于此namespace的孤儿进程将会被它收养;若这个进程死亡,则整个namespace都会被中止。

Note: reference to 123

Overview of struct task_struct

design

一个进程对应一个struct task_struct

不考虑进程之间的关系、命名空间,仅仅是一个pid对应一个struct task_struct,可以设计如下数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct task_struct {
...
struct pid_link pids;
...
};

struct pid_link {
struct hlist_node node;
struct pid *pid;
};

struct pid {
struct list_head tasks; // 指回pid_link的node
int nr; // pid
struct hlist_node pid_chain; // pid hash table node
};

能用下图描述:
task_struct design 1

上图中,

  • pid_hash[] —— 是一个hash表的结构,根据struct pidnr值哈希到其某个表项,若有多个nr值对应到同一个表项,则使用散列表法解决冲突。利用链表的container_of机制13可以利用tasks反向得到task_struct
  • pid_map —— 是一个位图(bitmap),是用来唯一分配pid值的结构。

这种设计可以达到:

  1. 快速地给新进程在可见的命名空间内分配一个唯一的pid
  2. 快速地利用task_struct找到pid
  3. 快速地利用pid反向得到task_struct

进程区分了id类型

考虑到进程/线程之间的复杂关系,原来的struct task_struct中的pid_link需要增加几项,用以指向到其组长进程的pid,相应的struct pid也需要增加几项用以链接那些以该pid为组长的所有进程组组内进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum pid_type {
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};

struct task_struct {
...
struct pid_link pids[PIDTYPE_MAX];
struct task_struct *group_leader; // thread group leader

struct pid_links pids[PIDTYPE_MAX];
...
};

struct pid_link {
struct hlist_node node;
struct pid *pid;
};

struct pid {
struct list_head tasks[PIDTYPE_MAX]; // 指回pid_link的node
int nr; // pid
struct hlist_node pid_chain; // pid hash table node
};

新的结构设计示意图如下:
task_struct design 2

增加了pid namespacesstruct task_struct

在第二种情形下再增加pid namespaces,同一个进程在不同的pid namespaces下有不同的pid,因此新的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum pid_type {
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};

struct task_struct {
...
struct pid_link pids[PIDTYPE_MAX];
struct task_struct *group_leader; // thread group leader

struct pid_links pids[PIDTYPE_MAX];
...
};

struct pid_link {
struct hlist_node node;
struct pid *pid;
};

struct pid {
unsigned int level; // 表示pid namespaces level
struct list_head tasks[PIDTYPE_MAX]; // 指回pid_link的node
struct upid numbers[1]; // 指向特定命名空间的upid
};

struct upid {
struct hlist_node pid_chain; // pid hash table node
int nr; // pid
struct pid_namespace *ns; // 该进程所属的命名空间
};

最终成了这样:
task_struct design 3

Note:upidunique pid的缩写。

real_parent vs parent

struct task_struct中有俩parent

1
2
3
4
5
6
7
8
9
struct task_struct {
...
/* Real parent process: */
struct task_struct __rcu *real_parent;

/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
...
}

Wikipedia中的parent process中有提到456,大意是:

Linux内核的process与POSIX threads差异极其小,对于每个进程/线程来说都有两种类型的parent process,分别为real_parentparentReal_parent仅仅是使用clone创建子进程/线程的的进程,对创建出来的进程/线程并没有控制权,而parent进程才是在子进程/线程中止的时候接收SIGCHLD的进程。通常情况下,他们的值是相同的;但对于POSIX threads来说,他们的值可能会有差异。

Internal

getpid & gettid & getpgrp & getpgid & getsid

在linux v4.12的include/linux/pid.h中定义了五种与进程/线程有关的id类型:

1
2
3
4
5
6
7
8
9
enum pid_type
{
PIDTYPE_PID, // pid
PIDTYPE_PGID, // pgid
PIDTYPE_SID, // sid
PIDTYPE_MAX,
/* only valid to __task_pid_nr_ns() */
__PIDTYPE_TGID // tgid
};

实际上,与进程有关的各类id都是group_leader的id,与线程有关的tid才是线程所属进程的pid。至于__PIDTYPE_TGID,它有着如下做法:

1
2
3
4
5
if (type != PIDTYPE_PID) {
if (type == __PIDTYPE_TGID)
type = PIDTYPE_PID;
task = task->group_leader;
}

因此,源码中有了以下的注释(thread group idprocess group leader pid

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* sys_getpid - return the thread group id of the current process
*
* Note, despite the name, this returns the tgid not the pid. The tgid and
* the pid are identical unless CLONE_THREAD was specified on clone() in
* which case the tgid is the same in all threads of the same group.
*/
SYSCALL_DEFINE0(getpid)
...

/* Thread ID - the internal kernel "pid" */
SYSCALL_DEFINE0(gettid)
...

下图展示了一个进程fork一次、每个进程再pthread_create两次的大致关系8

pid & tid & grp & sid

setsid

setsid的实现中,有这么一行代码:

1
proc_clear_tty(group_leader);

它的作用使脱离终端。《那些永不消逝的进程》7一文对此有相应的解释,梗概如下:

忽略SIGHUP信号的子进程不会因父进程的退出而退出,这是通过nohup运行的子程序可以在后台保持运行、不随终端退出而中止的原理。但是,为什么忽略了SIGHUP信号的子进程就不会随着父进程的结束而消逝?在什么样的场景下,一个进程会收到SIGHUP信号呢?这与Linux系统中描述进程关系的两个术语进程组(process group)会话(session)有关。

在早期Unix的设计中,每当有一个终端(terminal)通过某一tty来访问服务器,一个包含login shell进程的进程组就会被建立起来,所有在该shell中被建立的进程都会自动隶属于同一进程组之下,同时该tty也会被设置成该进程组下所有进程共有的终端控制器(controlling terminal)。但是,进程组对控制终端缺乏有效的管理手段、所有进程无差别地共享控制终端等的设计带来了许多弊端。因此作业控制(job control)的概念被提了出来,会话的设计也被引入,简单地说:新的设计将控制终端(tty或pty)的访问和控制完全置于了会话的管理之下。

SIGHUP是当终端连接中断或关闭时,直接或间接地被发送给会话中的所有进程组,一般进程对于该信号的默认处理方式也同样是终结自己。但是,nohup通过屏蔽SIGHUP的方式来实现守护进程并不理想,会出现缺少控制终端(SIGHUP不再起作用)、守护进程的工作目录无法umount等一系列问题。

Glibc把实现守护进程所需的工作封装到了daemon函数中,通过fork创建出的子进程执行setsid、同时父进程退出来实现,并且执行setsid成功的进程不仅成为新的会话组长和新的进程组长,还不会关联任何终端。

TODD:Question —— 为什么当前进程为process group leader的时候要使调用失败?

1
2
if (pid_task(sid, PIDTYPE_PGID))
goto out;

References