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

從行程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)

分享創造快樂