Linux 命名空间
在linux中,命名空间主要提供一种轻量级的资源虚拟,可以从不同方面来查看系统的全局属性,不同命名空间可以互相不干扰,为进程的一部分嗯。例如:同一个进程pid,可以被多个进程共享使用,可以查看同一用户的所有资源消费情况等等。
总体定义
定义如下:
- struct nsproxy {
- atomit_t count;//指向同一个nsproxy的进程个数
- struct uts_namespace *uts_ns;//运行的内核
- struct ipc_namespace *ipc_ns;//进程通信ipc
- struct mnt_namespace *pid_ns;//文件系统
- struct user_namespace *user_ns;//用户的资源限制信息
- struct net *net_ns;//网络
- }
在创建进程(fork,clone系统调用)时,需要设置一些标志来指明命名空间的创建与否
- #define CLONE_NEWUTS 0x04000000
- #define CLONE_NEWIPC 0x08000000
- #define CLONE_NEWUSER 0x10000000
- #define CLONE_NEWPID 0x20000000
- #define CLONE_NEWNET 0x40000000
同时在每个任务的定义中也包含命名空间的相关域:
- struct task_struct{
- …..
- //指针形式,这样可以被多个进程共享,修改一个命名空间,其它进程就可见了
- struct nsproxy *nsproxy;
- …....
- }
值得注意的是,命名空间需要在编译其间进行选择,如果没有指明,则所有的属性都是全局的,即只存在一个命名空间,全局命名空间为init_proxy,指向每个子系统的对象:
- struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy);
- #define INIT_NSPROXY(nsproxy) {
- .pid_ns = &init_pid_ns, \
- .count = ATOMIC_INIT(1), \
- .uts_ns = &init_uts_ns, \
- .mnt_ns = NULL, \
- INIT_NET_NS(net_ns), \
- INIT_IPC_NS(ipc_ns), \
- .user_ns = &init_user_ns, \
- }
相对应的系统调用有unshare,可以将父子进程的命名空间进行分开或共享。
UTS命名空间
定义如下:
- struct uts_namespace {
- struct kref kref;//使用计数
- struct new_utsname name;
- }
- struct new_utsname
- {
- char sysname[65];//系统名称
- char nodename[65];//主机名
- char release[65];//内核版本号
- char version[65];//内核版本日期
- char machine[65];//体系结构
- char domainname[65];
- }
测试结果如下:
- sys:Linux
- node:ubuntu-laptop
- release:2.6.32-33-generic
- version:#68-Ubuntu SMP Fri Jun 17 16:25:19 UTC 2011
- machine:i686
- domain:(none)
init进程初始化时,utsname赋值如下:
- struct uts_namespace init_uts_ns = {
- .kref = {
- .refcount = ATOMIC_INIT(2),
- },
- .name = {
- .sysname = UTS_SYSNAME,
- .nodename = UTS_NODENAME,
- .release = UTS_RELEASE,
- .version = UTS_VERSION,
- .machine = UTS_MACHINE,
- .domainname = UTS_DOMAINNAME,
- },
- }
这些宏常量定义在内核的各个地方,有些通过编译内核形成的,如UTS_RElEASE,定义在文件
<utsrelease.h>中。有些域可以进行修改,但是有些域不能修改,如UTS_SYSNAME只能是Linux,而UTS_NODENAME则可以进行修改。
User namespace
当需要一个新用户命名空间时,当前用户命名空间就会被复制,并与当前任务的nsproxy相关联,声明如下:
- struct user_namespace {
- struct kref kref;
- struct hlist_head uidhash_table[UIDHASH_SZ];
- struct user_struct *root_user;
- }
其中root_user用于记录单个用户资源情况,而uidhash_table将所有user_struct连接起来.
- struct user_struct {
- atomic_t __count;//该结构体引用计数
- atomic_t processes;//该用户拥有的进程数量
- atomic_t sigpending;//该用户拥有的悬而未决的信号量数目
- ….
- unsigned long locked_shm;//锁住的共享页个数
- //hash表所维护的信息
- struct hlist_node uidhash_node;
- uid_t uid;
- …...
- }
当设置新的用户时,用户命名空间创建如下:
- static struct user_namespace *clone_user_ns(struct user_namespace *old_ns)
- {
- struct user_namespace *ns;
- struct user_struct *new_user;
- int n;
-
- ns = kmalloc(sizeof(struct user_namespace),GFP_KERNEL);
- if(!ns)
- return ERR_PTR(-ENOMEM);
-
- kref_init(&ns->kref);
-
- for(n=0; n< UIDHASH_SZ;++n)
- INIT_HLIST_HEAD(ns->uidhash_table+n);
-
- /*insert into new root user*/
- ns->root_user = alloc_uid(ns,0);
- if(!ns->root_user) {
- kfree(ns);
- return ERR_PTR(-ENOMEM);
- }
- new_user = alloc_uid(ns,current->uid);
- if(!new_user) {
- free_uid(ns->root_user);
- kfree(ns);
- return ERR_PTR(-ENOMEM);
- }
- switch(new_user);
- return ns;
- }
命名空间增加了pid管理的复杂性,pid命名空间按照树型层次化管理。当一个新的命名空间创建,该命名空间内所有的pid都对其父命名空间可见,但是子命名空间却不能看到父命名空间中pid,这样就意味着有些任务包含有多个pid:父命名空间,子命名空间。这样就产生了两种类型的pid:
Global pid:内核本身包含有效的pid,init任务中可见的全局唯一的pid,也就是系统唯一的pid。
Local pid:在该命名空间内部的pid,全局不是唯一的,在不同的命名空间中,可能存在相同的pid。
task的pid_namespace描述
task_struct 中结构体部分结构如下:
- struct task_struct {
- …..
- pid_t pid;
- pid_t tgid;
- struct task_struct *group_leader;
- struct list_head thread_group;
- …..
- }
而session id和进程组id没有直接包含在task_struct内部,而是存放在signal结构体中
- struct signal_struct {
- ….....
- struct task_struct *curr_target;
- union {
- pid_t pgrp __deprecated;
- pid_t __pgrp;
- };
- union {
- pid_t session __deprecated;
- pid_t __session;
- };
- ….....
- }
对应的修改函数有:set_task_session,set_task_pgrp。
pid的管理
对应的pid_namespace定义如下:
- [include/linux/pid_namespace.h]
- //每个命名空间都包含一个所有pid的分配图,系统中各个命名空间互不交叉
- struct pid_namespace {
- struct kref kref;//进程的引用计数
- struct pidmap pidmap[PID_ENTRIES];//pid分配图
- int last_pid;
- struct task_struct *child_reaper;//当前命名空间中waitpid角色的task
- struct kmem_cache *pid_cachep;
- int level;//当前pid_namespace的深度
- struct pid_namespace *parent;//父pid_namespace
- }
init_ns定义如下:
- [kernel/pid.c]
- struct pid_namespace init_pid_ns = {
- .kref = {
- .refcount = ATOMIC_INIT(2),
- },
- //可用pid:4KB 4*1024*8=32768
- .pidmap = {
- .pidmap = {
- [ 0 … PIDMAP_ENTRIES – 1] = {
- ATOMIC_INIT(BITS_PER_PAGE),NULL}
- },
- .last_pid = 0,
- .level = 0,
- .child_reaper = &init_task,//init_task监听该命名空间的所有task
- }
pid的管理主要集中在两个数据结构:struct pid为内核形式的pid,struct upid代表在特定命名空间中可见的信息,定义如下:
- [include/linux/pid.h]
- struct upid
- {
- int nr;//真正的pid值
- struct pid_namespace *ns;//该nr属于哪个pid_namespace
- struct hlist_node pid_chain;//所有upid的hash链表 find_pid
- }
- //一个pid可以属于不同的级别,每一级别又包含一个upid
- struct pid
- {
- atomic_t count;//引用计数
- struct hlist_head tasks[PIDTYPE_MAX];//该pid被使用的task链表
- struct rcu_head rcu;//互斥访问
- int level;//该pid所能到达的最大深度
- struct upid numbers[1];//每一层次(level)的upid
- }
一个进程可以在多个命名空间中,但是每个命名空间的local id却不相同,numbers则表示每一层level的upid实例,这里的数组只包含一个元素,如果系统只有一个进程,这是可行的,但如果包含多个进程的话,就需要进行分配更多的空间,这个放在结构体中最后一个元素就是方便扩容。
其中PIDTYPE_MAX定义如下:
- enum pid_type
- {
- PIDTYPE_PID,
- PIDTYPE_PGID,
- PIDTYPE_SID,
- PIDTYPE_MAX
- }
线程组id没有包含在内,因为它与thread group leader的pid一样,没有必要放在里面。
一个任务可以包含多个命名空间,task_struct的结构体中显示如下:
- struct task_struct {
- …...
- struct pid_link pids[PIDTYPE_MAX];
- …...
- }
pid_link就是链接所有的pid:
- struct pid_link {
- struct hlist_node node;//由于每个task包含多个pid(多个命名空间可见),指向的是自己
- struct pid *pid;
- }
而实现upid中的数值nr到pid对象的hash映射如下:
- static struct hlist_head *pid_hash;//双向hash链表
- pid_hash是一个hlist_head数组,大小根据机器的内存配置,从16到4096,初始化代码如下:
- [kernel/pid.c]
- void __init pidhash_init(void)
- {
- int i, pidhash_size;
- //#define PAGE_SHIFT 12
- unsigned long megabytes = nr_kernel_pages >> (20-PAGE_SHIFT);
-
- pidhash_shift = max(4,fls(megabytes * 4));
- pidhash_shift = min(12,pidhash_shift);
- //16项到4096项
- pidhash_size = 1 << pidhash_shift;
-
- ….....
- pid_hash = alloc_bootmem(pidhash_size*sizeof(*(pid_hash)));
- if(!pid_hash)
- panic(“Could not alloc pidhash”);
- for(i=0;i< pidhash_size;++i)
- INIT_HLIST_HEAD(&pid_hash[i]);
- }
当struct pid已经分配,需要链接到具体task中时,执行代码如下:
- int fastcall attach_pid(struct task_struct *task,enum pid_type type,
- struct pid *pid)
- {
- struct pid_link *link;
- //从指定的task[type]中取出函数指针
- link = &task->pids[type];
- //将task赋值pid
- link ->pid = pid;
- //将该task放入pid的tasks链表中
- hlist_add_head_rcu(&link->node,&pid->tasks[type]);
- return 0;
- }
内核提供了大量的函数来进行pid到task之间的映射管理及维护,主要包括两部分:
1.指定局部数值pid,对应的命名空间,查找到对应的进程。
这种情况主要有以下几种情况:
1.获取与pid相关联的task,task_pid,task_tgid,task_pgrp,task_session用于不同类型的ID。
- //每一task有四种不同类型的type,每一种type中包含有一个pid
- static inline struct pid *task_pid(struct task_struct *task)
- {
- return task->pids[PIDTYPE_PID].pid;
- }
其它的也与上面的类似。
2.通过pid和命名空间namespace来查找进程的pid_t
- [kernel/pid.c]
- pid_t pid_nr_ns(struct pid *pid,struct pid_namespace *ns)
- {
- struct upid *upid;
- pid_t nr = 0;
- //指定的命名空间深度必须比pid高
- if(pid && ns->level <= pid->level)
- {
- upid = & pid->numbers[pid->level];
- if(upid->ns == ns)
- nr = upid->nr;
- }
- return nr;
- }
内核还采用了另外的方法来访问进程的pid:
1.pid_vnr从id所属的命名空间中返回局部pid,如:
- pid_t task_pid_vnr(struct task_struct*);
- pid_t task_tgid_vnr(struct task_struct*);
- pid_t task_pgrp_vnr(struct task_struct*);
- pid_t task_session_vnr(struct task_struct*);
这些函数都是通过pid_nr_ns来实现的
2.pid_nr从init进程中获取全局pi
这两个函数实际都通过指明level级别(0表示全局)调用了函数pid_nr_ns,它则是通过nr_ns系列函数来进行访问,如:
- pid_t task_pid_nr_ns(struct task_struct *,struct pid_namespace *);
- pid_t task_tgid_nr_ns(struct task_struct *,struct pid_namespace*);
- pid_t task_pgrp_nr_ns(struct task_struct*,struct pid_namespace*);
- pid_t task_session_nr_ns(struct task_struct *,struct pid_namespace *);
这些函数也是通过pid_nr_ns来实现的。
另外,还可以通过pid中的数值id-nr和命名空间来获取对应的pid,如下:
- struct pid *fastcall find_pid_ns(int nr,struct pid_namespace *ns)
- {
- struct hlist_node *elem;
- struct upid *pnr;
- //通过nr进行hash查找到对应的struct upid,pid_hash为全局唯一
- hlist_for_each_entry_rcu(pnr,elem,
- &pid_hash[pid_hashfn(nr,ns)],pid_chain)
- if(pnr->nr == ns)
- //通过pid中的成员变量upid来查找对应的struct pid
- return container_of(pnr,struct pid, numbers[ns->level]);
- }
2.指定一个进程,id类型,及命名空间,查找到对应的进程pid,如find_task系列函数:
- [kernel/pid.c]
- struct task_struct *find_task_by_pid_type_ns(int type,int nr,
- struct pid_namespace *ns);
- struct task_struct *find_task_by_pid(pid_t nr);
- struct task_struct *find_task_by_vpid(pid_t vnr);
- struct task_struct *find_task_by_pid_ns(pid_t nr,struct namespace *ns);
这些函数都是通过find_task_by_pid_type_ns来实现的
- struct task_struct *find_task_by_pid_type_ns(int type,int nr,
- struct pid_namspace *ns)
- {
- return pid_task(find_pid_ns(nr,ns),type);
- }
而pid_task实现如下:
- struct task_struct *fastcall pid_task(struct pid *pid,enum pid_type type)
- {
- struct task_struct *result = NULL;
- if(pid){
- struct hlist_node *first;
- //获取指定type的task_struct
- first = rcu_dereference(pid->tasks[type].node);
- if(first)
- //获取first中的pid_link域的pids[(type)].node值
- //因为pid_link中node域就是自己所以就直接获取,
- //那里指向自己的hlist_head就是为了满足这里的统一
- result = hlist_entry(first,struct task_struct,pids[(type)].node)
- }
- return result;
- }
pid的分配
为了记录pid的分配与释放情况,内核使用了一张pid位图,可以从pid位图中的位置来获取对应的pid值,同时将pid值从0改为1,相反释放时只需修改1为0
- 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;
- //从pid中获取具体位数的偏移量,位图即每一位一个pid
- 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;++i)
- {
- //如果内存页没有分配就分配一项
- if(unlikely(!map->page)) {
- void *page = kzalloc(PAGE_SIZE,GFP_KERNEL);
- 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);
- }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
- pid = mk_pid(pid_ns,map,offset);
- }
- return -1;
- }
参考资料
linux2.6.24内核源码
professional Linux architecture
understanding linux kernel