前面看了LInux PCI設備初始化,看得有點暈,就轉手整理下之前寫的筆記,同時休息一下!!~(@^_^@)~
這片文章是之前寫的,其中參考了某些大牛們的博客!!
PID框架的設計
一個框架的設計會考慮很多因素,相信分析過Linux內核的讀者來說會發現,內核的大量數據結構被哈希表和鏈表鏈接起來,最最主要的目的就是在於查找。可想而知一個好的框架,應該要考慮到檢索速度,還有考慮功能的划分。那么在PID框架中,需要考慮以下幾個因素.
-
如何通過task_struct快速找到對應的pid
-
如何通過pid快速找到對應的task_struct
-
如何快速的分配一個唯一的pid
這些都是PID框架設計的時候需要考慮的一些基本的因素。也正是這些因素將PID框架設計的愈加復雜。
原始的PID框架
先考慮的簡單一點,一個進程對應一個pid
struct task_struct {
.....
pid_t pid;
.....
}
引入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 namespace帶來的好處遠不止於此,其中最重要的就是對輕量級虛擬化的支持。當前炙手可熱的docker就是基於Linux 內核中命名空間的原理。有了PID命名空間,我們就可以保證進程的隔離性,至少在進程的角度,可以按照namespace的方式去組織,不同namespace的進程互不干擾。這也是筆者要分析下底層虛擬化支持的初衷之一!
言歸正傳:
引入進程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的一個框架
可以看到這里出現了一個層次結構,即一個命名空間可以有其子命名空間,子命名空間中的進程同樣會出現在父命名空間中,只是進程ID是不同的。看PID的結構:
1 struct pid 2 { 3 atomic_t count; 4 unsigned int level; 5 /* lists of tasks that use this pid */ 6 struct hlist_head tasks[PIDTYPE_MAX]; 7 struct rcu_head rcu; 8 struct upid numbers[1]; 9 };
count表示這個PID的引用計數,同一個PID結構可以為多個進程所共享;
level表示該PID所在的層級;
tasks是一個HASH數組,每一項都是一個鏈表頭。分別是PID鏈表頭,進程組ID表頭,會話ID表頭;
rcu用於保護指針引用;
numbers是一個UPID數組,記錄對應層級的命名空間中的UPID,所以可以想到,該PID處於第幾層,那么這個數組應該有幾項(當然都是從0開始)。
看下UPID結構:
1 struct upid { 2 /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ 3 int nr; 4 struct pid_namespace *ns; 5 struct hlist_node pid_chain; 6 };
UPID相比之下就簡單的多,或者說這才是真正的PID。
nr表示ID號;
namespace指向該UPID所在命名空間的namespace結構;
pid_chain是一個鏈表,系統會把UPID 的nr和namespace的ns經過某種hash,得到在一個全局的Hash數組中的下標,此UPID便會加入到對應下標的鏈表中。
看下進程中對於PID是如何應用的:
1 struct task_struct{ 2 ... 3 struct pid_link pids[PIDTYPE_MAX]; 4 ... 5 }
進程中包含一個pid_link結構的數組,我們還是先看一個pid_link結構:
1 struct pid_link 2 { 3 struct hlist_node node; 4 struct pid *pid; 5 };
node作為一個節點加入到所有引用同一PID結構的進程鏈表中;
pid指向該進程引用的PID結構。
也許到這里還是顯得關系有點紊亂,那么看一下下面的圖:
沒錯,這個圖是我畫的!!哈哈,可花了我不少時間了,希望對大家理解進程結構和PID以及UPID之間的關系有所幫助。貼出這個圖突然發現沒有什么好解釋的了,各種關系圖中已經表明。不過需要注意的是最下面的UPID我僅僅用一個框框代表了,其實是Numbers指向UPID結構,而讓UPID加入到鏈表中的是結構中的pid_chain;還有一個問題就是UPID那一組鏈表並不是表示同一PID下的所有UPID,根據內核源代碼,這里只是處理沖突的一種方式,而這里畫成鏈表僅僅是為了表示這里是以鏈表存在的。
下面說下PID位圖:
每一個namespace都對應一個map,用於分配pid號。這里包含兩個成員,nr_free表示可用的ID號數量,第二個是一個指向一個頁面的指針。這么設計有其自身的合理性。默認情況下page指向一個頁面,而一個頁面是4kb的大小,即4096個字節,也就是4096*8bit,每一位表示一個pid號,一個頁面就可以表示32768個進程ID,普通情況下絕對是夠了,即使特殊情況不夠,那么還可以動態擴展,這就是其合理性所在。
1 struct pidmap { 2 atomic_t nr_free; 3 void *page; 4 };
這里是pid_namespace結構:
1 struct pid_namespace { 2 struct kref kref; 3 struct pidmap pidmap[PIDMAP_ENTRIES];//命名空間對應的PID位圖 4 int last_pid;//上次分配的PID,便於下次分配 5 unsigned int nr_hashed; 6 struct task_struct *child_reaper; 7 struct kmem_cache *pid_cachep; 8 unsigned int level;//命名空間所在層級 9 struct pid_namespace *parent;//指向父命名空間的指針 10 #ifdef CONFIG_PROC_FS 11 struct vfsmount *proc_mnt; 12 struct dentry *proc_self; 13 #endif 14 #ifdef CONFIG_BSD_PROCESS_ACCT 15 struct bsd_acct_struct *bacct; 16 #endif 17 struct user_namespace *user_ns; 18 struct work_struct proc_work; 19 kgid_t pid_gid; 20 int hide_pid; 21 int reboot; /* group exit code if this pidns was rebooted */ 22 unsigned int proc_inum; 23 };
下一節會結合LInux內核源代碼分析下PID的具體分配情況