在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
