Pid NameSpace浅分析
1.pid Namespace涉及的基本数据结构
linux通过命名空间管理进程pid,对于同一进程(同一个task_struct),在不同的命名空间中,看到的pid号不相同,每个pid命名空间有一套自己的pid管理方法,所以在不同的命名空间中调用getpid(),看到的pid号是不同的。pid命名空间是一个父子关系的结构,系统初始只有一个pid命名空间,后面如果在fork进程的时候,加上新建pid命名空间的选项,那么这个新的命名空间的父命名空间就是初始的那个命名空间,在这个命名空间fork出的进程,在子命名空间和父命名空间都有一个pid号相对应到这个task_struct上。
从上图中可以看出,假设namespace有3层,如果在Namespace2中fork进程,产生的进程task_struct,如果pid是6,那么在根Namespace1中pid就是6,在Namespace2中pid就是4(自己的一套分配方式,递增方式,如果进程号被占用,就使用下一个空闲的id号,后面重点会说到id号的分配),在Namespace6中fork子进程,因为Namespace6来源于Namespace3,所以子命名空间fork的进程,这个命名空间的父命名空间都会看到这个进程,每个父命名空间根据自己id分配的情况,做一个task_struct到内部id号的映射关系,然后在相应的命名空间中调用getpid会使用当前命名空间中的id号,而不是task_struct中的pid。所以pid命名空间的作用就是,1个task_struct,在不同的命名空间看到的pid是不一样的。
关于pid
namespace的管理,首先需要抽象出结构体pidNamespace:include/linux/pid_namespace.h
-
struct pid_namespace {
-
struct kref kref; //引用计数
-
struct pidmap pidmap[PIDMAP_ENTRIES]; //pid分配的bitmap,如果位为1,表示这个pid已经分配了
-
int last_pid; //记录上次分配的pid,理论上,当前分配的pid=last_pid+1
-
struct task_struct *child_reaper; //表示进程结束后,需要这个child_reaper进程对这个进程进行托管
-
struct kmem_cache *pid_cachep; //高速缓存,这个不太清楚,待这块分析源代码
-
unsigned int level; //记录这个pid
namespace的深度
-
struct pid_namespace *parent; //记录父pid
namespace
-
#ifdef CONFIG_PROC_FS
-
struct vfsmount *proc_mnt;
-
#endif
-
#ifdef CONFIG_BSD_PROCESS_ACCT
-
struct bsd_acct_struct *bacct;
-
#endif
-
};
-
这里比较重要的成员变量就是pidmap,它表示在这个pid命名空间的pid的分配情况,pidmap是个数组,每一位代表这个这个偏移量的pid是否分配出去,初始这个数组只有一个元素。
pidmap的结构:include/linux/pid_namespace.h
-
struct pidmap {
-
atomic_t nr_free;//表示这个bitmap还有多少位为0,就是说对应的pid没有被分配出去
-
void *page;//表示一段连续的内存空间,每位的0或1表示对应pid是否被分配
-
};
默认情况下pid最大是32768,那么默认正好是1页能保存下的pid使用情况,linux默认一页的大小是4k=4*1024*8位=32768,如果pid的最大值超过32768那么pidmap数组就用上了,多个pidmap就是为了pid限制大于32768来设计的。
child_reaper的作用见init进程对zombie进程的处理。这个child_reaper的作用就是当父进程先于子进程结束的时候,就把子进程的父进程更新为child_reaper。
整体的pid管理结构图:
一个进程对应一个task_struct,但是这个进程在多个namespace中都可以看见不同的pid,那么就需要一个表示pid的结构体。代码:include/linux/pid.h
-
struct pid
-
{
-
atomic_t count; //引用次数
-
unsigned int level;//这个pid的深度
-
/* lists of tasks that use this pid */
-
struct hlist_head tasks[PIDTYPE_MAX];//引用pid的task,看了很多的文章始终搞不清楚什么条件下,会分配同一个pid结构,看了fork中的一些逻辑,发现每次都是创建新的pid结构,这个有待研究
-
struct rcu_head rcu;
-
struct upid numbers[1];//这个task_struct在多个命名空间的显示。一个upid就是一个namespace的pid的表示。
-
};
-
这里最重要的成员变量就是numbers,它是个数组,表示一个task_struct在每个namespace的id(这个id就是getpid()所得到的值),number[0]表示最顶层的namespace,level=0,number[1]表示level=1,以此类推。
代码:include/linux/pid.h
-
struct upid {
-
/* Try to keep pid_chain in the same cacheline as nr for find_vpid * /
-
int nr; //表示命名空间中的标识
-
struct pid_namespace *ns; //命名空间
-
struct hlist_node pid_chain; //hash表中的端点
-
};
这里nr和ns成对出现,表示进程的在这个ns命名空间的pid为nr。管理这些pid结构,通常把他们防止在hash表中,pid_chain是hash结构中的一个节点,所以pid_chain就是hash表和数据之间的桥梁。这里linux内核中广泛的使用这种hash表,hash表中每个元素都是hlist_node,那么取得每个元素所代表的value,就要通过指针和结构体,来倒推value的指针。实现机理通过函数container_of 代码:include/linux/kernel.h
-
/**
-
* container_of - cast a member of a structure out to the containing structu re
-
* @ptr: the pointer to the member.
-
* @type: the type of the container struct this is embedded in.
-
* @member: the name of the member within the struct.
-
*
-
*/
-
#define container_of(ptr, type, member) ({ \
-
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
-
(type *)( (char *)__mptr - offsetof(type,member) );})
这里ptr是结构体type中的成员变量member的指针,这个函数的实际含义是通过ptr指针根据结构体中member的具体偏移量来得到type结构体的首地址,然后在强转成type的指针。这里typeof是GCC内建函数,offsetof是获得结构体中member变量的指针的偏移量。这样member变量的内存地址减去member的偏移量就可以获得结构体的指针。
遗留的问题:不知道什么情况会多个进程会公用一个pid结构。
2.pid的分配
fork进程的时候,需要为这个进程分配pid,应该根据这个namespace中pidmap的pid分配情况,分配适合的id,大体的过程就是根据当前namespace中的last_pid+1,然后参照pidmap中这位是否为1,如果为1证明当前last_pid+1已经被使用(导致这种情况是id被分配到了最大值,然后再重头选择id,之前的进程如果有还没结束,就会导致last_pid+1,不可用),这时需要找到比last_pid大的值,取离它最近的。如果找不到,则分配失败。
分配pid的函数:kernel/pid.c
-
static int alloc_pidmap(struct pid_namespace *pid_ns)
-
{
-
int i, offset, max_scan, pid, last = pid_ns->last_pid; //取出last_pid
-
struct pidmap *map;
-
-
pid = last + 1; //这里last+1,取得备选pid
-
//如果pid到了pidmax,那么重头开始寻找可用的pid,从RESERVED_PIDS开始,保留RESERVED_PIDS之前的pid号,默认300
-
if (pid >= pid_max)
-
pid = RESERVED_PIDS;
-
offset = pid & BITS_PER_PAGE_MASK; //取得掩码,获得pidmap的掩码(取余数)。
-
map = &pid_ns->pidmap[pid/BITS_PER_PAGE]; //根据pid获得pidmap
-
max_scan = (pid_max + BITS_PER_PAGE - 1)/BITS_PER_PAGE - !offset; //后面单独讲
-
for (i = 0; i <= max_scan; ++i) {
-
if (unlikely(!map->page)) { //如果这个pidmap没有分配内存重新分配
-
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;
-
}
-
//如果nr_free大于0表示map中还有空闲的pid的位
-
if (likely(atomic_read(&map->nr_free))) {
-
do {
-
-
//根据man->page基址,offset是偏移量,test_and_set_bit把offset位的值置为1,可以知道如果offset位如果是1,那么还是1,返回原来被set之前的值1,表示这位表示的pid已经被使用,如果返回0,表示之前这位表示的pid未被使用,同时将这位置为了1(这个函数的实现是,内嵌汇编,bts操作)返回0,表示这位未被使用
-
-
if (!test_and_set_bit(offset, map->page)) {
-
atomic_dec(&map->nr_free);//空闲计数减一
-
pid_ns->last_pid = pid; //重新设置last_pid
-
return pid;
-
}
-
//继续寻找offset之后,位为0的位置
-
offset = find_next_offset(map, offset);
-
//找到这个位置,根据map的序号和偏移量转换为pid
-
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.
-
*/
-
-
//这里循环停止会有多种条件,如果偏移量找到了这个pid_map的最后那么就停止查找了,因为已经到了这个map的最后一位了,那么应该从下一个pid_map开始寻找,如果分配的pid大于允许分配最大pid的值,就该从第一个map开始寻找之前可能已经结束的进程,空闲出来的位置
-
-
} while (offset < BITS_PER_PAGE && pid < pid_max &&
-
(i != max_scan || pid < last ||
-
!((last+1) & BITS_PER_PAGE_MASK)));
-
}
-
//如果当前的pid_map没有到最后一个pid_map,就继续寻找下一个pid_map,这时offset=0,重头开始寻找
-
if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) {
-
++map;
-
offset = 0;
-
} else {
-
//如果当前的pid_map到了最后一个pid_map,那么重头第一个pid_map开始寻找可用的pid,同时将offset设置成RESERVED_PIDS,RESERVED_PIDS之前的pid被保留了。
-
map = &pid_ns->pidmap[0];
-
offset = RESERVED_PIDS;
-
if (unlikely(last == offset))
-
break;
-
}
-
pid = mk_pid(pid_ns, map, offset);
-
}
-
return -1;
-
}
-
代码:
135 max_scan = (pid_max +
BITS_PER_PAGE - 1)/BITS_PER_PAGE - !offset;
这里max_scan代表最多去寻找几个pid_map,这里减去!offset的原因就是,如果offset为0,那么当前的pid_map不需要重新递归寻找掩码之前的空闲位置,因为掩码为0,没有再前面的位置了,如果掩码不为0,那么需要再次递归当前的pid_map,寻找掩码之前的位置的空闲位。
从上面的图看出来,如果last_pid位于第一个pid_map中的第三位,next就是第四位,那么max_scan=4,如果pid_map[1],pid_map[2]都没有空闲位,那么需要重新查找pid_map[0]中的空闲位,如果当前掩码是0,位于第一个pid_map,那么不需要回来查找pid_map[0]。
3.getpid函数的实现
getpid函数是获得当前进程id,如果线程调用这个函数,得到的是这个线程的task_group的pid,那么这个pid是当前namespace下的标识,并不是task_struct中的pid值。这个函数的具体实现在kernel/timer.c
-
SYSCALL_DEFINE0(getpid)
-
{
-
return task_tgid_vnr(current);
-
}
系统调用直接到了这里,task_tgid_vnr的实现:include/linux/sched.h
-
static inline pid_t task_tgid_vnr(struct task_struct*tsk)
-
{
-
return pid_vnr(task_tgid(tsk));
-
}
这里task_tgid(tsk)函数就是获得当前进程的task_group(进程的task_group就是它自己,线程的task_group是它的父进程,调用pthread_create的那个进程)的pid结构
-
static inline struct pid*task_tgid(struct task_struct*task)
-
{
-
return task->group_leader->pids[PIDTYPE_PID].pid;
-
}
获得pid结构,就应该根据当前namespace获得pid结构中对应的进程标识了,代码:kernel/pid.c
-
pid_t pid_vnr(struct pid*pid)
-
{
-
return pid_nr_ns(pid,current->nsproxy->pid_ns);
-
}
current->nsproxy->pid_ns就是当前pid_namespace
-
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){
-
//根据namespace的level深度获得upid结构,这里的upid->nr就是这个进程在这个namespace下的进程标识
-
upid=&pid->numbers[ns->level];
-
if(upid->ns==ns)
-
nr=upid->nr;
-
}
-
return nr;
-
}
总结:
pid命名空间可以把一个进程在不同的命名空间pid管理隔离开,使得每个命名空间都有自己的一套pid命名规则,在看以上的代码后,有疑问:什么情况下多个进程才会共用一个pid结构?希望大家给点建议
上面的问题,在pid Namespace续中解释了问题,多个进程共用一个pid结构的时机:父进程fork出子线程,然后子线程去调用exec,在这调用exec函数的过程中,首先子线程发信号使得父进程停止,子线程去attach父进程pid结构,最后再release
父进程,在段代码中,父进程和子线程会共用一个pid结构。
阅读(874) | 评论(0) | 转发(0) |