歡迎光臨
每天分享高質量文章

從行程PID到容器PID Namespace的演變及實現

前言

這幾晚在看行程相關的內核原理,正好看到了pid這塊,看起來不是很複雜,但是引入了pid namespace後增加了一些資料結構,看起來不是那麼清晰了,參考了Linux內核架構這本書,看完後感覺還沒有理解。所以就在網上找了一些文章參考,其中我發現了一篇質量相當不錯的文章,為什麼說質量不錯呢主要是因為筆者在博文中並沒有亂貼代碼一桶,也沒有按照常規的代碼分析,而是以一種追蹤溯源的方法還原了整個pid的框架,讀了這篇文章後感覺甚好,因此有了本文,本文算不上原創,只是在此基礎上將自己的理解重新進行了梳理,相關的圖表進行了重繪,加入了一些資料結構的含義表述。關於這篇文章的鏈接可以參考附錄A


PID框架的設計

一個框架的設計會考慮很多因素,相信分析過Linux內核的讀者來說會發現,內核的大量資料結構被哈希表和鏈錶鏈接起來,最最主要的目的就是在於查找。可想而知一個好的框架,應該要考慮到檢索速度,還有考慮功能的劃分。那麼在PID框架中,需要考慮以下幾個因素.

  • 如何通過task_struct快速找到對應的pid

  • 如何通過pid快速找到對應的task_struct

  • 如何快速的分配一個唯一的pid

這些都是PID框架設計的時候需要考慮的一些基本的因素。也正是這些因素將PID框架設計的愈加複雜。

原始的PID框架

先考慮的簡單一點,一個行程對應一個pid

struct task_struct
{    
..
   pid_t pid;    

   …..

}

是不是很easy,回到上文,看看是否符合PID框架的設計原則,通過task_struct找到pid,很方便,但是通過pid找到task_struct怎麼辦呢?好吧,基於現在的這種結構肯定是無法滿足需求的,那就繼續改進吧。
註: 以上的這種設計來自與linux 2.4內核的設計


引入hlist和pid位圖

struct task_struct *pidhash[PIDHASH_SZ];


這樣就很方便了,再看看PID框架設計的一些因素是否都滿足了,如何分配一個唯一的pid呢,連續遞增?,那麼前面分配的行程如果結束了,那麼分配的pid就需要回收掉,直到分配到PID的最大值,然後從頭再繼續。好吧,這或許是個辦法,但是是不是需要標記一下那些pid可用呢?到此為此這看起來似乎是個解決方案,但是考慮到這個方案是要放進內核,開發linux的那幫家伙肯定會想近一切辦法進行優化的,的確如此,他們使用了pid位圖,但是基本思想沒有變,同樣需要標記pid是否可用,只不過使用pid位圖的方式更加節約記憶體.想象一下,通過將每一位設置為0或者是1,可以用來表示是否可用,第1位的0和1用來表示pid為1是否可用,以此類推.到此為此一個看似還不錯的pid框架設計完成了,下圖是目前整個框架的整體效果.


引入PID型別後的PID框架

熟悉linux的讀者應該知道一個行程不光光只有一個行程pid,還會有行程組id,還有會話id,(關於行程組和會話請參考(行程之間的關係)[])那麼引入pid型別後,框架變成了下麵這個樣子,

struct task_struct
{
   ….
   pid_t pid;
   pid_t session;    
struct task_struct *group_leader;
   ….
}
struct signal{
   ….
   pid_t __pgrp;
   ….
}
來自於kernel
2.6.24

對於行程組id來說,信號需要知道這這個id,通過這個id,可以實現對一組行程進行控制,所以這個id出現在了signal這個結構體中.所以直到現在來說框架還不是那麼複雜,但是有一個需要明確的就是無論是session id還是group id其實都不占用pid的資源,因為session id是和領導行程組的組id相同,而group id則是和這個行程組中的領導行程的pid相同.


引入行程PID命名空間後的PID框架

隨著內核不斷的添加新的內核特性,尤其是PID Namespace機制的引入,這導致PID存在命名空間的概念,並且命名空間還有層級的概念存在,高級別的可以被低級別的看到,這就導致高級別的行程有多個PID,比如說在預設命名空間下,創建了一個新的命名空間,占且叫做level1,預設命名空間這裡稱之為level0,在level1中運行了一個行程在level1中這個行程的pid為1,因為高級別的pid namespace需要被低級別的pid namespace所看見,所以這個行程在level0中會有另外一個pid,為xxx.套用上面說到的pid位圖的概念,可想而知,對於每一個pid namespace來說都應該有一個pidmap,上文中提到的level1行程有兩個pid一個是1,另一個是xxx,其中pid為1是在level1中的pidmap進行分配的,pid為xxx則是在level0的pidmap中分配的. 下麵這幅圖是整個pidnamespace的一個框架


.引入了PID命名空間後,一個pid就不僅僅是一個數值那麼簡單了,還要包含這個pid所在的命名空間,父命名空間,命名空間多對應的pidmap,命名空間的pid等等.因此內核對pid做了一個封裝,封裝成struct pid,一個名為pid的結構體,下麵是其定義:

enum pid_type
{
   PIDTYPE_PID,
   PIDTYPE_PGID,
   PIDTYPE_SID,
   PIDTYPE_MAX
};
struct pid
{    
unsigned int level; //這個pid所在的層級
   
/* lists of tasks that use this pid */
   
struct hlist_head tasks[PIDTYPE_MAX];

//一個hash表,又三個表頭,分別是pid表頭,行程組id表頭,會話id表頭,後面再具體介紹
   
struct upid numbers[1];

//這個pid對應的命名空間,一個pid不僅要包含當前的pid,還有包含父命名空間,預設大小為1,所以就處於根命名空間中};


struct upid {               //包裝命名空間所抽象出來的一個結構體
   
int nr;                 //pid在該命名空間中的pid數值
   
struct pid_namespace *ns;       //對應的命名空間
   
struct hlist_node pid_chain;    //通過pidhash將一個pid對應的所有的命名空間連接起來.};

struct pid_namespace {    

   struct kref kref;    

   struct pidmap pidmap[PIDMAP_ENTRIES];  

//上文說到的,一個pid命名空間應該有其獨立的pidmap
   
int last_pid;               //上次分配的pid
   
unsigned int nr_hashed;
   
struct task_struct *child_reaper;  

//這個pid命名空間對應的init行程,因為如果父行程掛了需要找養父啊,這裡指明瞭該去找誰
   
struct kmem_cache *pid_cachep;    unsigned int level;         //所在的命名空間層次
   
struct pid_namespace *parent;    //父命名空間,構建命名空間的層次關係
   
struct user_namespace *user_ns;    struct work_struct proc_work;
   kgid_t pid_gid;    
int hide_pid;    int reboot; /* group exit code if this pidns was rebooted */
   
unsigned int proc_inum;
};
//上面還有一些複雜的成員,這裡的討論占且用不到


引入了pid namespace後,的確變得很複雜了,多了很多看不懂的資料結構.行程如何和struct pid關聯起來呢,內核為了統一管理pid,行程組id,會話id,將這三類id,進行了整合,也就是現在task_struct要和三個struct pid關聯,還要區分struct pid的型別.所以內核又引入了中間結構將task_struct和pid進行了1:3的關聯.其結構如下:

struct pid_link
{    
struct hlist_node node;    struct pid *pid;
};
struct task_struct
{
   ………….
   pid_t pid;    
struct pid_link pids[PIDTYPE_MAX];
   ………….
}
struct pid
{    
unsigned int level;

   //這個pid所在的層級
   
/* lists of tasks that use this pid */
   
struct hlist_head tasks[PIDTYPE_MAX];

   //一個hash表,又三個表頭,分別是pid表頭,行程組id表頭,會話id表頭,用於和task_struct進行關聯
   
struct upid numbers[1];

   //這個pid對應的命名空間,一個pid不僅要包含當前的pid,還有包含父命名空間,預設大小為1,所以就處於根命名空間中};

使用pid的tasks hash表和task_struct中pids結構中的hlist_node關聯起來了.


到此為止一個看起來已經比較完善的pid框架構建完成了,整個框架的效果如下:


行程PID相關的API分析

獲取pid結構

static inline struct pid *task_pid(struct task_struct *task)
{    
return task->pids[PIDTYPE_PID].pid;
}


獲取pid結構中的某一個名字空間的pid數值

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{    
struct upid *upid;
   pid_t nr =
0;    

   //判斷傳入的pid namespace層級是否符合要求
   
if (pid && ns->level <= pid->level) {
       upid = &pid-;>numbers[ns->level];

   //去到對應pid namespace的strut upid結構
       
if (upid->ns == ns) //判斷命名空間是否一致
           nr = upid->nr;
//獲取pid數值
   }    
return nr;
}


如何分配一個pid

struct pid *alloc_pid(struct pid_namespace *ns)

//pid分配要依賴與pid namespace,也就是說這個pid是屬於哪個pid namespace
{
   struct pid *pid;
   enum pid_type
type;
   int i, nr;
   struct pid_namespace *tmp;
   struct upid *upid;
   //分配一個pid結構
   pid = kmem_cache_alloc(ns->pid_cachep,
GFP_KERNEL);  

   if (!pid)
       goto out;

   tmp = ns;
   pid->level = ns->level; //初始化level

   //遞迴到上面的層級進行pid的分配和初始化
   for (i = ns->level; i >=
0; i–) {
       nr = alloc_pidmap(tmp);

//從當前pid namespace開始知道全域性pid namespace,每一個層級都分配一個pid        if (nr < 0)
           goto out_free;
       pid->numbers[i].nr = nr; //初始化upid結構
       pid->numbers[i].ns = tmp;
       tmp = tmp->parent; //遞迴到父親pid namespace
   }    
if (unlikely(is_child_reaper(pid))) {  

//如果是init行程需要做一些設定,為其準備proc目錄        

       if (pid_ns_prepare_proc(ns))
           goto out_free;
   }

   get_pid_ns(ns);
   atomic_set(&pid-;>count,
1);
   for (
type = 0; type < PIDTYPE_MAX; ++type)  

//初始化pid中的hlist結構
       
INIT_HLIST_HEAD(&pid-;>tasks[type]);

   upid = pid->numbers + ns->level;  

//定位到當前namespace的upid結構
   spin_lock_irq(&pidmap;_lock);    

if (!(ns->nr_hashed & PIDNS_HASH_ADDING))
       goto out_unlock;
for ( ; upid >= pid->numbers;
–upid) {
    hlist_add_head_rcu(&upid-;>pid_chain,
               &pid;_hash[pid_hashfn(upid->nr, upid->ns)]);

               //建立pid_hash,讓pid和pid namespace關聯起來
       upid->ns->nr_hashed++;
   }
   spin_unlock_irq(&pidmap;_lock);
out:
   return pid;
out_unlock:
   spin_unlock_irq(&pidmap;_lock);
   put_pid_ns(ns);
out_free:
   while (++i <= ns->level)
       free_pidmap(pid->numbers + i);

   kmem_cache_free(ns->pid_cachep, pid);
   pid = NULL;
   goto out;
}

附錄

附錄A 參考鏈接

http://www.cnblogs.com/hazir/p/linux_kernel_pid.htm

赞(0)

分享創造快樂