Linux内核:进程基础之进程ID与管理

时间:2022-11-24 14:55:39

Linux系统中的每一个进程都有一个唯一的标示——PID(Process ID),也叫进程ID。给每一进程设置一个标示的主要作用便是方便Linux系统管理数量庞大的进程,例如进程间的通信、判断子父进程、通过进程ID找到进程的task_struct等等,都需要使用到我们的进程ID。我们已经知道每一个进程都会有一个唯一的task_struct结构体来关联该进程相关的数据和结构体。Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。所以毫不例外,我们的PID也应该会在task_struct结构体中会有关联,但它到底是怎么和进程的PID关联起来的呢?同时由于PID命名空间的存在,以及线程组、进程组、会话组等等组织的存在,进程ID又是如何进行区分和表示的呢?下面我将一一对这些疑问进行解答。

命名空间

在Linux系统中,之所以要引入命名空间,其根本目的是可以通过命名空间,从系统层面对进程进行管理。例如我们上面提到的进程ID,由于所有的进程都是通过PID进行标示的,那么在系统中肯定会对这些PID进行统一全局的管理。对于一些有着相同属性的进程,我们可以将其划分到同一个容器中,但这并不意味着每一个进程只能属于一个命名空间。解释这个问题,我们可以从进程的task_struct结构体关联其命名空间开始讲起。在task_struct结构体中包含有一个nsproxy结构体体的指针,其指向了自己进程所属于的nsproxy结构体。

struct task_struct {
          .....
      struct nsproxy *nsproxy;
          .....
}

那么这个nsproxy指针就是指向其所在的nsproxy结构体。那我们现在来看一看nsproxy结构体。

struct nsproxy {
	atomic_t count; // 关联该结构体的task_struct数量
	struct uts_namespace *uts_ns; //uts命名空间
	struct ipc_namespace *ipc_ns; //ipc命名空间
	struct mnt_namespace *mnt_ns; //mnt命名空间
	struct pid_namespace *pid_ns; //pid命名空间
	struct net 	     *net_ns; //网络相关的命名空间的数量
}
从上面的nsproxy结构体中我们可以看到,其里面包含各种命名空间的指针,而由于task_struct结构体指向的是nsproxy,所以毫无疑问,该task_struct是属于nsproxy里面指向的各种类型的命名空间。在Linux的官方文档中,我们还看到了这样一句话:The nsproxy is shared by tasks which share all namespaces. As soon as a single namespace is cloned or unshared, the nsproxy is copied. 说明只要有一个task_struct结构体的命名空间与现有的nsproxy不一样或者是有新的namespace生成,那么整个nsproxy结构体都将会被重新拷贝一份。这样我们就可以看到如下的一个关系框图。

Linux内核:进程基础之进程ID与管理
由上图可以看到,只要有进程的用户命名空间与其他的nsproxy结构体不同,那么就会重新生成一个nsproxy结构体。从这个图我们也看出了,同一个进程,并不只属于一种类型的命名空间。


PID命名空间

因为这里我是讲进程的ID管理,所以下面我们着重关注PID命名空间。命名空间的出现,增加了PID管理的复杂性,但是却能极大提高管理的效率。PID命名空间是按照层次组织的,当我们在新复制一个进程时,都会设置是否新建一个命名空间的标志位。新建的命名空间中的所有PID对父命名空间都是可见的,但是子命名空间却无法看到父命名空间。为什么会这样呢,原来在建立新的命名空间的时候,新命名空间的所有PID都会在父命名空间建立一个它的映射,有了这个映射,父命名空间就可以看到子命名空间的PID啦。父子命名空间的层次结构图如下:

Linux内核:进程基础之进程ID与管理

由于有了这种父子命名空间的出现,所以也就有了PID命名空间的所属层级了,由上图可知处在最高位的是level 0,然后下面的命名空间level会逐级增加。同时由于这种层级的出现,也便有了全局ID和局部ID之分。

      1.  全局ID处于层级顶层的ID,也就是level0的命名空间中的ID,全局ID具有唯一性。

      2.  局部ID只属于一个命名空间,其局部ID也只有在该命名空间里面有效。

全局ID保存在task_struct结构体的pid变量中(除此还有TGID也是直接保存在task_struct中)。


PID的管理

在内核中,不光定义有PID,还有一些其它类型的ID,通过这些类型的ID能更加好的管理我们的进程。除了PID,主要的ID还有:

  • TGID:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程(因为这种进程没有独立的地址空间,与他的父进程共享一个地址空间,在Linux内核中没有明显区分进程与线程。所以我们所有自己独立的地址空间就叫进程,与别的进程共享地址空间就叫线程。),它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGIDPID也相同。
  • PGID:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID
  • SID:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID

有了这多类型的进程ID,那么内核肯定需要找到一种方法用来管理所有的命名空间以及不同类型的进程ID(这也是Linux内核最管理进程最为核心的地方,也是比较难理解的地方)。

要管理这些命名空间和不同类型的ID,我们就需要互相连接的数据结构以及辅助函数。

首先我们介绍数据结构


相关数据结构

pid_namespace结构体

pid_namespace结构体相当于一个PID命名空间的实例,其关联了与该命名空间中的所有结构体和相关函数,该结构体定义在include\linux\PID_namespace.h中定义。 

struct pid_namespace {  
    struct kref kref;  
    struct pidmap pidmap[PIDMAP_ENTRIES]; //定义了一个数组来表示pidmap,这说明pidmap的个数不止一个,pidmap里面当然是存放pid的啦,后面会将如何分配。  
    int last_pid;                        //上一次分配的pid的值,之所以要纪录这个,后面会讲到  
    struct task_struct *child_reaper;  //指向该命名空间的init进程的task_struct结构体  
    struct kmem_cache *pid_cachep;    //高速缓存,目前还没有仔细研究其作用。  
    unsigned int level;              //该pid命名空间所处于的层级  
    struct pid_namespace *parent;   //指向父命名空间  
#ifdef CONFIG_PROC_FS  
    struct vfsmount *proc_mnt;  
#endif  
#ifdef CONFIG_BSD_PROCESS_ACCT  
    struct bsd_acct_struct *bacct;  
#endif  
}  

所以每当新生成一个pid_namespace,便会生成一个新的pid_namespace结构体。系统要了解该命名空间的信息,只需要获取该命名空间的pid_namesapce结构体就行了。


pidupid结构体

由上面我们知道,每一个进程都会属于某一个PID命名空间,同时该命名空间会给其一个PID号,如果该命名空间不是level0的命名空间,那么该ID便是局部PID,我们上面已经讲过。同时每一个进程都会有一个属于它的PID(局部PID)、PGID(进程组组长ID)、SID(会话组ID)这里的GPID(线程组ID,属于进程内部的线程ID)直接在task_struct结构体中指定,所以这里不予讨论。那如何对这些歌ID进行管理呢?所以pid结构体便应运而生。

pid结构体在include\linux\Pid.h文件中定义

struct pid  
{  
    atomic_t count;    //引用计数器,用于记录有多少个task_struct结构体关联到这个pid结构体(只有进程组的pid结构体或者是会话组的pid结构体才会被其他task_struct结构体关联),后面会讲到  
    unsigned int level;   //这个PID的深度,该变量的值和其所处的命名空间结构体中的level的值是相等的。  
    /* lists of tasks that use this pid */  
    struct hlist_head tasks[PIDTYPE_MAX];  //一个散列表头的数组,其包含了不同类型ID的散列表头,当然后无疑问tasks[0]只有一个元素。系统正是通过这些散列表来找到处于同一组的task_struct,这个后面我会单独写文章讲。  
    struct rcu_head rcu;  
    struct upid numbers[1]; //这个upid记录着该task_struct在不同level下的ID号以及该level下的命名空间地址。  
};  

pid结构体的定义,我们可以看到,其里面包含了一个upid的数组,该数组会记录与之关联的task_struct在不同命名空间下的局部ID号(映射的ID)以及其命名空间的地址。upid结构体与pid结构体在同一文件中定义。

struct upid {  
    /* Try to keep pid_chain in the same cacheline as nr for find_vpid */  
    int nr;                    //该命名空间的ID号(根据level的级别,可能是局部ID号,也可能是全局ID号)  
    struct pid_namespace *ns; //该命名空间pid_namespace结构体的地址。  
    struct hlist_node pid_chain; //用于链接该命名空间下的所有upid。  
};

没生成一个task_struct就会同时生成一个与其关联的pid结构体,那么在task_struct结构体中又如何关联pid结构体以及其进程组组长的pid结构体、会话组组长的pid结构体呢,我们先来看看task_struct中是如何定义的。

struct task_struct{  
            
             ....  
       struct pid_link pids[PIDTYPE_MAX]; 
       struct task_struct *group_leader; 
             ....  
  
}  

我们看到在task_struct中定义了一个pid_link类型的数组,该数组就是用来记录与其关联的pid结构体的地址,进程组组长的pid结构体地址,会话组组长的pid结构体地址。

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

有了上面对于pid_namespace结构体、pid结构体、pid_link结构体的讲解,我们现在就可以给出整个pid命名空间各结构关系图了。

Linux内核:进程基础之进程ID与管理

针对上图,这里主要说明以下几点

  1. 这里的A进程是处于该进程组的组长,所以进程B和进程C的pids[1]的pid指针都是指向其进程组组长所关联的pid结构体。
  2. 同时由于进程组A是进程组组长,所以其所关联的pid结构体的task[1]指针将作为进程组散列表的散列表头(hlist_head),其进程组成员的task_struct的pids[1]的node都将链接在散列表头的后面。
  3. 这里假设进程A 处在level为2的PID命名空间当中,那么就会有进程A的pid结构体的upid[0]、upid[1]分别保存其映射在level为0和level为1的一些基本信息,例如其映射在该命名空间的PID号,该命名空间的pid_namespace结构体的地址,以及pid_chain链表成员。
  4. 这里要注意pid_hash[],这个数组里面都是hlist_head指针,pid_hash[]是在内核初始化时调用pidhash_init()函数(该函数会指定pid_hash数组的长度以及初始化)里初始化的,pid_hash[]数组主要是用来链接所有的upid的pid_chain,但这里需要注意的是,给定一个upid,它应该处于pid_hash[]数组的哪一个成员(pid_hash[i])为链表头的散列表中呢?这个时候我们便需要一个通用的索引函数pid_hashfn(nr, ns),该函数的作用就是主要给定该命名空间下的局部ID号nr,以及命名空间的地址空间,就会返回一个索引号,该索引号也就是pid_hash[i]中的这个i的值。


相关辅助函数


hlist_add_head_rcu函数

该函数定义在include\linux\Rculist.h文件中
static inline void hlist_add_head_rcu(struct hlist_node *n,
					struct hlist_head *h)
{
	struct hlist_node *first = h->first;

	n->next = first;
	n->pprev = &h->first;
	rcu_assign_pointer(hlist_first_rcu(h), n);
	if (first)
		first->pprev = &n->next;
}

主要作用是对于给定的hlist_node指针和hlist_head指针,将hlist_node加入以hlist_head为链表头的散列表中。这个函数会在linux内核中的其他函数经常用到,例如attach_pid函数、alloc_pid函数。


pidhash_init函数

该函数定义在kernel\Pid.h文件中
void __init pidhash_init(void)
{
	int i, pidhash_size;

	pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
					   HASH_EARLY | HASH_SMALL,
					   &pidhash_shift, NULL, 4096);
	pidhash_size = 1 << pidhash_shift;

	for (i = 0; i < pidhash_size; i++)
		INIT_HLIST_HEAD(&pid_hash[i]);
}
内核初始化时会自动调用该函数为pid_hash[]散列表分配空间,上面已经讲过这个函数了。


task_pid函数

该函数在include\linux\Sched.h文件中定义
static inline struct pid *task_pid(struct task_struct *task)
{
	return task->pids[PIDTYPE_PID].pid;
}
该函数的作用是对于给定的task_struct指针,找到与其关联的pid结构体的指针。类似这种函数的还有:

task_tgid函数

static inline struct pid *task_tgid(struct task_struct *task)
{
	return task->group_leader->pids[PIDTYPE_PID].pid;
}
用于找到该进程的线程组组长的pid结构体指针,注意这里用到的是group_leader,因为group_leader存放的是该线程组组长的task_struct结构体的指针,如果该进程只有一个线程组,那么这个也便是指向他自己,同时其tgid与pid的ID值相等。所以也就能够理解为什么pids数组里面的索引号是PIDTYPE_PID了。

task_pgrp函数

static inline struct pid *task_pgrp(struct task_struct *task)
{
	return task->group_leader->pids[PIDTYPE_PGID].pid;
}
该函数的作用用于找到进程组长的PID结构体指针,这里用到的也是其线程组长group_leader指针,因为线程组组长所在的进程组无疑与其所在进程是一样的。

task_session函数

static inline struct pid *task_session(struct task_struct *task)
{
	return task->group_leader->pids[PIDTYPE_SID].pid;
}

该函数的作用用于找到会话组组长的PID结构体指针,与上面的task_pgrp函数一样,也是用到了group_leader指针。


pid_nr_ns函数

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;
	} //由于父命名空间可以看到子命名空间的pid,而反之就不可以,所以这里有 <span style="font-family: Arial, Helvetica, sans-serif;">ns->level <= pid->level

	return nr;
}
这个函数的作用就是给出某一个进程的pid结构体指针,以及一个命名空间结构体的指针,然后找到其在该命名空间的局部PID号。但是这里需要注意的是,由于pid结构体中保存了该进程所处的level层级,所以在获取其nr值之前,必须要判断是否给出的命名空间的层级小于pid的level层级。


find_pid_ns函数

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
	struct hlist_node *elem;
	struct upid *pnr;

	hlist_for_each_entry_rcu(pnr, elem,
			&pid_hash[pid_hashfn(nr, ns)], pid_chain)
		if (pnr->nr == nr && pnr->ns == ns)
			return container_of(pnr, struct pid,
					numbers[ns->level]);

	return NULL;
}
这个函数的主要作用就是给定pid号(一定要是该命名空间的pid号)和命名空间指针,从而找到其pid指针。注意这里使用到了container_of机制,给出upid的地址,便可以获取pid结构体的地址。

pid_task函数

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
	struct task_struct *result = NULL;
	if (pid) {
		struct hlist_node *first;
		first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
					      rcu_read_lock_held() ||
					      lockdep_tasklist_lock_is_held());
		if (first)
			result = hlist_entry(first, struct task_struct, pids[(type)].node);
	}
	return result;
}
这个函数的作用就是给定pid结构体的指针,以及pid_type类型,就可以返回其所关联的task_struct指针。这里的主要机制还是使用到了pid的task[PIDTYPE]作为散列表头,然后去找满足散列表的第一个task_struct的指针。当然如果type类型为PIDTYPE_PID时,由于只有一个node与其关联,所以找到的task_struct指针肯定是与pid绑定的task_struct指针。

生成唯一的PID结构体

当我们在fork一个进程时,不仅要复制父进程的task_struct,当然还要生成一个与该task_struct进程绑定的pid结构体了。既然要生成pid结构体,那么肯定就要生成全局ID号,以及在不同层级的PID命名空间下的局部ID。那么这些全局ID号和局部ID号是不是随便生成的呢?或者说是可以不限量的给定ID号。答案当然是否定的,因为在名一个命名空间下,都会定义一个pidmap结构体类型的数组,这个数组当然存放的都是一个个的pidmap,每一个pidmap相当于一个大的位图,用来跟踪已经分配和仍然可用的PID,每一个PID由一个比特位来表示,一个位置的比特位为0,则表示该位置的PID可用,为1便表示该位置的PID已经分配了。系统会通过其pidmap数组的索引号以及其所在pidmap中的位置来确定PID的值。

首先我们来看一下pidmap结构体的定义

struct pidmap {
       atomic_t nr_free;  //该pidmap可用的pid的数量
       void *page;   //表示一段连续的内存空间,如果某一位为0则表示该位的pid号没有被分配出去。
};
由上面的定义可以看出pidmap结构体主要有两部分组成,第一是记录pidmap中为0位的数量,另外一个就是记录一段连续空间的起始地址。

下面我重点讲解两个重要的函数

alloc_pid函数

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;

	pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); //为pid结构体分配空间。
	if (!pid)
		goto out;

	tmp = ns;
	for (i = ns->level; i >= 0; i--) {
		nr = alloc_pidmap(tmp); //该函数用于获取pid号
		if (nr < 0)
			goto out_free;

		pid->numbers[i].nr = nr;
		pid->numbers[i].ns = tmp;
		tmp = tmp->parent; //指向父命名空间。
	}

	get_pid_ns(ns);
	pid->level = ns->level;
	atomic_set(&pid->count, 1);
	for (type = 0; type < PIDTYPE_MAX; ++type)
		INIT_HLIST_HEAD(&pid->tasks[type]);

	upid = pid->numbers + ns->level;
	spin_lock_irq(&pidmap_lock);
	for ( ; upid >= pid->numbers; --upid) //将pid的upid链接进pid_hash[]索引表中
		hlist_add_head_rcu(&upid->pid_chain,
				&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
	spin_unlock_irq(&pidmap_lock);

out:
	return pid;

out_free:
	while (++i <= ns->level)
		free_pidmap(pid->numbers + i);

	kmem_cache_free(ns->pid_cachep, pid);
	pid = NULL;
	goto out;
}
这个函数的作用就是生成一个pid结构体,并且根据pid所在的命名空间,建立其在父命名空间的映射ID号,当然这些ID号都是需要在各级命名空间的pidmap中申请的,最后返回其pid结构体的指针。上面的函数使用到了alloc_pidmap函数,该函数用于获取在某一个命名空间的ID号。


alloc_pidmap函数

static int alloc_pidmap(struct pid_namespace *pid_ns)
{
	int i, offset, max_scan, pid, last = pid_ns->last_pid;
	struct pidmap *map;

	pid = last + 1;
	if (pid >= pid_max)
		pid = RESERVED_PIDS;
	offset = pid & BITS_PER_PAGE_MASK;
	map = &pid_ns->pidmap[pid/BITS_PER_PAGE];
	/*
	 * If last_pid points into the middle of the map->page we
	 * want to scan this bitmap block twice, the second time
	 * we start with offset == 0 (or RESERVED_PIDS).
	 */
	max_scan = DIV_ROUND_UP(pid_max, BITS_PER_PAGE) - !offset;
	for (i = 0; i <= max_scan; ++i) {
		if (unlikely(!map->page)) {
			void *page = kzalloc(PAGE_SIZE, GFP_KERNEL); //分配内存空间
			/*
			 * Free the page if someone raced with us
			 * installing it:
			 */
			spin_lock_irq(&pidmap_lock);
			if (!map->page) {
				map->page = page;
				page = NULL;
			}
			spin_unlock_irq(&pidmap_lock);
			kfree(page);
			if (unlikely(!map->page))
				break;
		}
		if (likely(atomic_read(&map->nr_free))) {
			do {
				if (!test_and_set_bit(offset, map->page)) {
					atomic_dec(&map->nr_free);
					set_last_pid(pid_ns, last, pid);
					return pid;
				}
				offset = find_next_offset(map, offset);
				pid = mk_pid(pid_ns, map, offset);
			} while (offset < BITS_PER_PAGE && pid < pid_max);
		}
		if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) {
			++map;
			offset = 0;
		} else {
			map = &pid_ns->pidmap[0];
			offset = RESERVED_PIDS;
			if (unlikely(last == offset))
				break;
		}
		pid = mk_pid(pid_ns, map, offset);
	}
	return -1;
}