Linux进程ID的内核管理

系统 1803 0

在进程创建的时候,Linux系统会分配一个号码给当前这个进程,这个号码在进程所在的命名空间中是唯一的,但在其它的兄弟进程中,这个号码就不是唯一的了,也就是对于全局的命名空间来说,这个号不是全局唯一。这个号码就是进程的ID号,简称为PID。

一,进程号数据结构表示

这个PID被保存在进程的结构表示task_struct中。

    struct task_struct{
....
     pid_t pid;
     pid_t tgid;
.....
};
  
这两个结构都是pid_t,这个结构是是体系结构相关的,在X86下它的定义是int,也就是可以同时使用的最大的ID数为int的取值范围。tgid是线程组ID,因为在Linux中线程也是进程,线程组的ID其实就是主线程的PID。

    typedef int	__kernel_pid_t;
typedef __kernel_pid_t pid_t;

  

二,管理进程ID

因为这些都是和具体的PID命名空间相关的,先了解一下PID的命名空间结构。
      struct pid_namespace {
	struct kref kref;
	struct pidmap pidmap[PIDMAP_ENTRIES];
	int last_pid;
	struct task_struct *child_reaper;
	struct kmem_cache *pid_cachep;
	int level;
	struct pid_namespace *parent;
#ifdef CONFIG_PROC_FS
	struct vfsmount *proc_mnt;
#endif
};
    
这个结构用于实现很多的功能,如保证分配唯一的PID,在这里只关心这几个成员:
  1. child_reaper。每个PID命名空间都有一个和全局空间中init进程一样的进程,该进程实现了对命名空间中的孤儿进程进行wait4操作。child_reaper保存了该进程的指针。
  2. parent。指向父命名空间的指针。
  3. level。表示当前的命名空间在命名空间层次结构中的深度,第一个命名空间,就是全局的命名空间level值为0。level较高的命名空间对于level较低的命名空间是可见的,其实命名空间是采用在低level命名空间中给高level命名空间中的PID建立映射,所以对其是可见的。
为了实现命名空间可见性,建立两个数据结构:struct pid用于内核对PID的内部表示,而struct upid表示特定的命名空间中可见的信息。定义如下:
        struct upid {
	/* Try to keep pid_chain in the same cacheline as nr for find_pid */
	int nr;   //表示ID的数值,这个就是命名空间内所可见的PID数值。
	struct pid_namespace *ns; // 该ID的命名空间指针。
	struct hlist_node pid_chain; // 将所有的upid实例都保存在一散列溢出链表上。
};

struct pid
{
	atomic_t count;  // 引用计数
	/* lists of tasks that use this pid */ //
	struct hlist_head tasks[PIDTYPE_MAX]; // 每个数组项都是一个散列表头。
	struct rcu_head rcu; // 
	int level;
	struct upid numbers[1];
};
      
PIDTYPE_MAX表示进程ID的类型,如下:
        enum pid_type
{
	PIDTYPE_PID,进程ID
	PIDTYPE_PGID,进程组ID
	PIDTYPE_SID,会话组ID
	PIDTYPE_MAX
};
      
在网上找的一张图片,说明这些数据结构的关系:
这个图也不是那么容易看明白,图中一共有三个部分,说明如下:
  1. struct pid是进程ID在内核结构的一个表示。但有可能会出现多个task_struct实例共享同一个进程ID,这就需要将所有共享进程ID的task_struct都以一种方式和struct pid关联起来,这里就是struct pid结构中的tasks数组,对应多个散列链表的头部,那是不是struct task_struct结构中应该提供链表接点呢,就是这样的。
                struct task_struct{
    ......
        struct pid_link pids[PIDTYPE_MAX];
    ......
    };
    struct pid_link{
                
                
                
                
                //定义于pid.h文件中。
        struct hlist_node node;
        struct pid *pid;
    }
              
    这个结构中,pid指向进程所属的pid结构,而node用作散列表接点元素。上文中的图中的mode,应为node。这样的话,图的右上角部分就容易理解了。再往下
  2. struct pid结构中有一个level域,表示当前命名空间的层次,这个值可以表示当前可以看到该进程的命名空间的数目,比如当前只有一个命名空间,则表示进程对一个命名空间是可见的,如果这个命名空间有一个子命名空间,那么子命名空间的level值应该是1,这个时候表示子命名空间的进程对2个命名空间可见:自身和父命名空间。struct pid为了实现这个,在结构中定义了numbers域,定义只有一个,因为大多数情况是这样的。但如果有更多的话,也没事,为什么呢?因为这个numbers域在结构的末尾,所以只要添加,就可以根据level的值读出来。只要分配空间了,就不会有数组溢出了。所以数组的每一个元素都是对应于在某一层次的命名空间中的struct upid表示,这正是PID在特定命名空间中可见的信息。
  3. 现在说到右下角了,这是在指定的命名空间中查找对应于指定PID的struct pid结构实例。因为我们在某一命名空间对PID可见的信息是struct upid,那么要根据这个struct upid找到struct pid。
    为了实现这个,内核使用散列表,在pid.c文件中定义:
                static struct hlist_head *pid_hash;
              
    这是一个数组,数组的大小取决于当前计算机的内存大小,调用pidhash_init函数计算合适的容量并分配内存
                void __init pidhash_init(void)
    {
    	int i, pidhash_size;
    	unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT);
    
    	pidhash_shift = max(4, fls(megabytes * 4));
    	pidhash_shift = min(12, pidhash_shift);
    	pidhash_size = 1 << pidhash_shift;
    
    	printk("PID hash table entries: %d (order: %d, %Zd bytes)\n",
    		pidhash_size, pidhash_shift,
    		pidhash_size * sizeof(struct hlist_head));
    
    	pid_hash = alloc_bootmem(pidhash_size *	sizeof(*(pid_hash)));
    	if (!pid_hash)
    		panic("Could not alloc pidhash!\n");
    	for (i = 0; i < pidhash_size; i++)
    		INIT_HLIST_HEAD(&pid_hash[i]);
    }
              
    这个函数在内核启动的时候,也就是start_kernel中,就会被调用。
    在这之后,可以根据struct upid中nr的值和命名空间的地址进行散列。
三,进程task_struct和PID结构struct pid关联
      int fastcall attach_pid(struct task_struct *task, enum pid_type type,
		struct pid *pid)
{
	struct pid_link *link;

	link = &task->pids[type];
	link->pid = pid;
	hlist_add_head_rcu(&link->node, &pid->tasks[type]);

	return 0;
}
    
这个比较好理解,就是将struct pid附加到struct task_struct上去。这样就建立了一个双向的连接:task_struct可以通过task_struct->pids[type]->pid访问struct pid实例。而从struct pid实例开始的话,可以遍历tasks[type]散列表找到task_struct实例。

四,实现
在管理这些结构时,主要是对下面几个问题比较着重:
  1. 根据一个局部的进程ID和对应的命名空间,查找所对应的task_struct实例。
  2. 如果已知task_struct和进程类型,命名空间,如何找到命名空间里的进程ID,就是struct upid结构的nr值。
对于第一个问题,先根据局部的进程ID和对应的命名空间找到struct pid实例,然后再确定task_struct实例。
      struct pid * fastcall 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;
}
EXPORT_SYMBOL_GPL(find_pid_ns);
    
通过前文说的pid_hash散列数组,对于container_of的实现,可以参看博文【Li nux内核的Container_of机制 】。在找到struct pid之后,再确定struct task_struct就容易了。
内核封装了一个函数,用于一步完成这个操作:
      struct task_struct *find_task_by_pid_type_ns(int type, int nr,
		struct pid_namespace *ns)
{
	return pid_task(find_pid_ns(nr, ns), type);
}
    
pid_task用于根据struct pid和type,找到具体的struct task_struct,这里也用到了Container_of机制。
      struct task_struct * fastcall pid_task(struct pid *pid, enum pid_type type)
{
	struct task_struct *result = NULL;
	if (pid) {
		struct hlist_node *first;
		first = rcu_dereference(pid->tasks[type].first);
		if (first)
			result = hlist_entry(first, struct task_struct, pids[(type)].node);
	}
	return result;
}
    
再回到第二个问题,第二个问题就比较容易了,根据struct task_struct和type,可以很容易的找到struct pid,只需要
      struct pid* _pid = task->pids[type].pid;
    
就可以了,然后再直接遍历_pid->numbers就可以得到匹配的命名空间所在struct upid结构,进而获取nr的值。

五,生成唯一的进程ID
上面讨论的是内核如果对进程的ID进行管理,内核肯定还会负责生成一些PID,并且保证这些PID都是全局唯一的。
为了知道哪些ID已经分配,内核使用了一个大位图,位图中的每个比特表示一个PID,这样就很明白了,PID的值和比特位的位置是唯一对应的。这样一来,分配一个没有使用的PID,就相当于在位图中寻找第一个为0的比特,然后将其置1。
      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];
	max_scan = (pid_max + BITS_PER_PAGE - 1)/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)
				kfree(page);
			else
				map->page = page;
			spin_unlock_irq(&pidmap_lock);
			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);
					pid_ns->last_pid = pid;
					return pid;
				}
				offset = find_next_offset(map, offset);
				pid = mk_pid(pid_ns, map, offset);
			/*
			 * find_next_offset() found a bit, the pid from it
			 * is in-bounds, and if we fell back to the last
			 * bitmap block and the final block was the same
			 * as the starting point, pid is before last_pid.
			 */
			} while (offset < BITS_PER_PAGE && pid < pid_max &&
					(i != max_scan || pid < last ||
					    !((last+1) & BITS_PER_PAGE_MASK)));
		}
		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;
}
    
释放一个PID操作
      static fastcall void free_pidmap(struct pid_namespace *pid_ns, int pid)
{
	struct pidmap *map = pid_ns->pidmap + pid / BITS_PER_PAGE;
	int offset = pid & BITS_PER_PAGE_MASK;

	clear_bit(offset, map->page);
	atomic_inc(&map->nr_free);
}
    
另外,上面讨论知道,在建立一个新的进程时,进程需要在多个命名空间中是可见的,对于每个对其可见的命名空间,都需要生成一个局部的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);
	if (!pid)
		goto out;

	tmp = ns;
	for (i = ns->level; i >= 0; i--) {//这里将对其可见的所有的命名空间都生成一个局部的ID
		nr = alloc_pidmap(tmp);
		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]);

	spin_lock_irq(&pidmap_lock);
	for (i = ns->level; i >= 0; i--) {//更新struct pid,对每个struct upid,都将其置于散列表上。
		upid = &pid->numbers[i];
		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:
	for (i++; i <= ns->level; i++)
		free_pidmap(pid->numbers[i].ns, pid->numbers[i].nr);

	kmem_cache_free(ns->pid_cachep, pid);
	pid = NULL;
	goto out;
}
    
这个函数很简单,处理的功能也比较单一。

六,小结
再回过来看内核进程,这里一个进程类型,其实在内核中进程不是只有一个PID这个特征的,还有其它的ID,像上文涉及的tgid就是其中一个,可能有以下几种类型:
  1. 线程组ID,就是TGID。在一个进程没有使用线程之前,TGID和PID是相等的。线程组的主进程,就是第一个创建线程的进程。每个”线程“都包含组长的task_struct实例
              struct task_struct{
    .......
           struct task_struct *group_leader;	/* threadgroup leader */
    .......
    };
            
  2. 由独立进程合并的进程组。每个task_struct结构都包含进程组长的PID信息,这个数据存储于task_struct->signal->__pgrp中。
  3. 几个进程组可以合并成一个会话,会话中的所有进程都有同样的会话ID。
  4. 全局PID。
  5. 局部PID。这是由于引进命名空间导致的,因为命名空间中的所有PID对父命名空间都是可见的,但子命名空间无法看到父命名空间的PID。这就要求,某些进程可能有多个PID,因为可以看到该进程的命名空间都为为其分配一个PID。

Linux进程ID的内核管理


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论