linux内核pid名字空间

发布于:2023-01-04 ⋅ 阅读:(356) ⋅ 点赞:(0)

最近看《深入理解linux内核架构》中pid名字空间一块,略有感悟。这里开门见山,直接来讲pid名字空间是如何实现的,以及如何使用。先看一幅图,图中描述了所使用的数据结构和他们的关系。

 

首先强调图中的几个比较容易搞混的字段。看task_struct中的struct pid_link *pids数组中,每个元素里有一个struct hlist_node node字段。也就是说,可以利用这个字段把task_struct加入到一个哈希表中。再看右边的struct pid结构中的struct hlist_head tasks[]数组,明显这个hlist_head,也就是表头,这个地方的每个数组元素都可以是一个链表。由于这里数据结构比较多,所以一定要注意是一个内结点,还是一个链表的头结点。

图中绿线(无箭头)表示头结点把中间结点链接成了一个链。而蓝色单箭头线表示,是只有一个指向目标结点的指针。蓝色双箭头线表示是双向链表。

为什么要搞这么复杂的结构呢?因为满足一些需求。 搞pid名字空间就是为了把一些进程隔离开来,让他们以为他们独占了整个系统,在虚拟化等中有应用。 还有,内核中经常需要由PID的数字值得到进程task_struct。(struct hlist_head *pid_hash的作用)。 需要由PID数字值得到进程组ID,会话ID。 当一个会话结束了,需要终止属于这个会话的所有进程。也就是要由会话ID找到所有属于这个会话的进程task_struct。(看看struct pid中有个SID字段,这里链接着属于这个PID的所有会话进程)。 linux用轻量级进程模拟线程,所以需要由线程组长ID找到所有属于这个进程的线程ID。

新创建一个进程是怎么影响名字空间的?

新建进程时的调用栈如下图。可以看出这是在创建内核线程。创建用户线程是调用fork()系统调用,这fork里也调用了do_fork(),也就是图中的do_fork(). do_fork中调用了copy_process函数,在这个函数中子进程复制了父进程的task_struct,其中就复制了task_struct中的struct pid_link pids[]数组。也就是说这时子进程与父进程有一样的PID,SID,GID。不过不用担心,这时子进程没有运行,在copy_process后边会重新给pids[PID].pid赋值。

在kernel/fork.c:copy_process函数中,依次调用了下列函数: 

p = dup_task_struct(current);复制了父进程的PID,GID,SID。
if (pid != &init_struct_pid) {
	retval = -ENOMEM;
	pid = alloc_pid(p->nsproxy->pid_ns);//分配新的PID,包括在每个名字空间中看到的不同PID。
	if (!pid)
		goto bad_fork_cleanup_io;
	if (clone_flags & CLONE_NEWPID) {
		retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
		if (retval < 0)
			goto bad_fork_free_pid;
	}
}
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
	p->tgid = current->tgid;
//自己成了新的进程组长,但是在真正获取进程组ID时:task->group_leader->pids[PIDTYPE_PGID];
p->group_leader = p; 
INIT_LIST_HEAD(&p->thread_group);
if (likely(p->pid)) {
	list_add_tail(&p->sibling, &p->real_parent->children);
	tracehook_finish_clone(p, clone_flags, trace);
	if (thread_group_leader(p)) {
		if (clone_flags & CLONE_NEWPID)
			p->nsproxy->pid_ns->child_reaper = p;

			p->signal->leader_pid = pid;
			tty_kref_put(p->signal->tty);
			p->signal->tty =tty_kref_get(current->signal->tty);
			attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
				
			**//加入会话PID的task[PIDTYPE_SID]链表。**
			attach_pid(p, PIDTYPE_SID, task_session(current));
			list_add_tail_rcu(&p->tasks, &init_task.tasks);
			__get_cpu_var(process_counts)++;
		}
		**//将自己加入struct pid的tasks[PIDTYPE_PID]链表。**
		attach_pid(p, PIDTYPE_PID, pid);
		nr_threads++;
	}

 

每个进程有一个stuct pid结构,在这个结构中,tasks[]数组中有三个链表,第一个是PIDTYPE_PID链表,这个链表中只有一个进程x,也就是说每个进程有唯一一个struct PID. 第二个是PIDTYPE_PGID,这个链表中可能多于一个进程,多个进程可能属于同一个进程组,由此可见,组长进程退出后,这个struct结点不应该释放,直到进程组中所有进程退出。第三个是PIDTYPE_SID,这个与PGID一样,属于同一个会话的进程都在这个链表中。有了这些知识,上面的问题都可以迎刃而解了。

分配struct pid

下面代码在kernel/pid.c中,在分配struct 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;
	//从slab中获得struct pid结构
	pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
	if (!pid)
		goto out;
	tmp = ns;
	for (i = ns->level; i >= 0; i--) {
		nr = alloc_pidmap(tmp);//从pid名字空间的位图中分配一个数字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); //引用计数。计数为0就释放struct pid结构。
	
}

最初的struct pid是如何建立起来的?

一开始,内核用静态变量初始化了一个struct pid结构给swap(0号进程)使用。还静态初始化了一个pid名字空间。

这是在kernel/pid.ck上定义的。
struct pid_namespace init_pid_ns = {
	.kref = {
		.refcount       = ATOMIC_INIT(2),
	},
	.pidmap = {
		[ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
	},
	.last_pid = 0,
	.level = 0,
	.child_reaper = &init_task, //每个pid名字空间有一个init进程,用来收养孤儿进程
};
//全局pid_hash中。链接的是struct upid结构,哈希值是由nr(pid)和ns(pid所在名字空间)算出来的.由pid和ns就能得到struct upid结构,再用container_of得到struct pid结构,再从struct pid结构中tasks[]获取task_struct。
static struct hlist_head *pid_hash; 
static unsigned int pidhash_shift = 4;
struct pid init_struct_pid = INIT_STRUCT_PID;//一个全局的初始pid结构。也是系统中最初始的PID。其他PID都是由alloc_pid函数从slab中分配的。

下面代码在include/linux/init_task.h中:
extern struct nsproxy init_nsproxy;//名字空间根结点,静态定义
#define INIT_NSPROXY(nsproxy) {						\

}

#define INIT_STRUCT_PID {						\
	.count 		= ATOMIC_INIT(1),				\
	.tasks		= {						\
	下边三个字段就是初始PID的初始化。可以看到0号进程是自己的组长,会话组长。
		**{ .first = &init_task.pids[PIDTYPE_PID].node },		\
		{ .first = &init_task.pids[PIDTYPE_PGID].node },	\
		{ .first = &init_task.pids[PIDTYPE_SID].node },		\**
	},								\
	.rcu		= RCU_HEAD_INIT,				\
	.level		= 0,						\
	.numbers	= { {						\
		.nr		= 0,					\
		**.ns		= &init_pid_ns,**				\
		.pid_chain	= { .next = NULL, .pprev = NULL },	\
	}, }								\
}

#define INIT_PID_LINK(type) 					\
{								\
	.node = {						\
		.next = NULL,					\
		.pprev = &init_struct_pid.tasks[type].first,	\
	},							\
	.pid = &init_struct_pid,				\
}

/*
 *  INIT_TASK is used to set up the first task table, touch at
 * your own risk!. Base=0, limit=0x1fffff (=2MB)
 */
#define INIT_TASK(tsk)	\初始化第一个进程
{									\
	
	**将自己加入struct pid的tasks字段中。**
	.pids = {							\
		[PIDTYPE_PID]  = INIT_PID_LINK(PIDTYPE_PID),		\
		[PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID),		\
		[PIDTYPE_SID]  = INIT_PID_LINK(PIDTYPE_SID),		\
	},								\
}

下面代码在arch/x86/kernel/init_task.c中:
/*
 * Initial task structure.
 *
 * All other task structs will be allocated on slabs in fork.c
 */
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

 

本文含有隐藏内容,请 开通VIP 后查看