第一次作業:Linux 2.6.32的進程模型與調度器分析
1.前言
本文分析的是Linux 2.6.32版的進程模型以及調度器分析。在線查看 源碼下載
本文主要討論以下幾個問題:
什麽是進程?進程是如何產生的?進程都有那些?
在操作系統中,進程是如何被管理以及它們是怎樣被調用的?
2.進程模型
2.1進程的概念
在我的理解中,一個程序就相當於一個進程,程序的啟動意味著產生了一個新的進程,程序的關閉也就意味著一個進程的消亡。
那麽專業定義應該是:
在計算中,進程是正在執行的計算機程序的一個實例。 它包含程序代碼及其當前活動。 根據操作系統(OS),一個進程可能由多個並行執行指令的執行線程組成。
進程不是空洞的,其實我們也是可以看到的
比如在Linux下它是這樣的:
在Windows下它是這樣的:
2.2進程的產生
了解了什麽是進程之後,那麽一個進程他是怎麽產生的呢?在這裏我們要知道的是,在Linux系統中,所有進程之間都有著直接或間接地聯系,每個進程都有其父進程,也可能有零個或多個子進程。擁有同一父進程的所有進程具有兄弟關系,其中有兩個特殊的進程,分別是swapper,init。
swapper:
swapper也叫0號進程或者idle進程,他是所有進程的祖先,它是在 Linux 的初始化階段從無到有的創建的一個內核線程。這個祖先進程使用靜態分配的數據結構。
init :
init也叫1號進程它由進程0創建的內核線程執行init() 函數,init() 一次完成內核的初始化。init()調用execve()系統調用裝入可執行程序init ,結果 ,init 內核線程變成一個普通的進程,且擁有自己的每個進程內核數據結構。在系統關閉之前,init 進程一直存活,因為它創建和監控在操作系統外層執行的所有進程的活動。
在Linux中我們可以通過pstree查看進程樹模型:
2.3進程的分類
Linux把進程區分為實時進程和非實時進程(普通進程), 其中非實時進程進一步劃分為交互式進程和批處理進程。
類型 |
描述 |
實例 |
交互式進程(interactive process) |
此類進程經常與用戶進行交互,,因此需要花費很多時間等待鍵盤和鼠標操作。當接受了用戶的輸入後, 進程必須很快被喚醒, 否則用戶會感覺系統反應遲鈍 |
shell, 文本編輯程序和圖形應用程序 |
批處理進程(batch process) |
此類進程不必與用戶交互, 因此經常在後臺運行. 因為這樣的進程不必很快相應, 因此常受到調度程序的怠慢 |
程序語言的編譯程序, 數據庫搜索引擎以及科學計算 |
實時進程(real-time process) |
這些進程由很強的調度需要, 這樣的進程絕不會被低優先級的進程阻塞. 並且他們的響應時間要盡可能的短 |
視頻音頻應用程序, 機器人控制程序以及從物理傳感器上收集數據的程序 |
2.4進程標識符
知道進程的產生之後,很自然的會聯想到在同一個時段系統運行的進程那麽多,操作系統要怎麽樣對這麽多的進程進行有效的管理呢?為了有效的管理在Linux中運用了一個task_struct的數據結構對一個進程做了一個完整的描述。
Linux中對進程的描述多達300行代碼,定義了非常的成員,大致可分為以下幾個部分:
-進程狀態(State)
-進程調度信息(Scheduling Information)
-各種標識符(Identifiers)
-進程通信有關信息(IPC)
-時間和定時器信息(Times and Timers)
-進程鏈接信息(Links)
-文件系統信息(File System)
-虛擬內存信息(Virtual Memory)
-頁面管理信息(page)
-對稱多處理器(SMP)信息
-和處理器相關的環境(上下文)信息(Processor Specific Context)
-其它信息
所以接下來將對task_struct結構中的一小部分成員進行分析。
2.4.1親屬關系
在前面講到,進程在Linux中是層次結構的,所有進程之間都有著直接或間接地聯系,每個進程都有其父進程,也可能有零個或多個子進程。擁有同一父進程的所有進程具有兄弟關系。那麽在task_struct中它是這麽被定義的:
/* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */ struct task_struct *real_parent; /* real parent process */ struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */ /* * children/sibling forms the list of my natural children */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent‘s children list */ struct task_struct *group_leader; /* threadgroup leader */
字段 | 描述 |
real_parent | 指向其父進程,如果創建它的父進程不再存在,則指向PID為1的init進程 |
parent | 指向其父進程,當它終止時,必須向它的父進程發送信號。它的值通常與real_parent相同 |
children | 表示鏈表的頭部,鏈表中的所有元素都是它的子進程 |
sibling | 用於把當前進程插入到兄弟鏈表中 |
group_leader | 指向其所在進程組的領頭進程 |
註:這裏的real_parent相當於是親爹,parent呢是幹爹,大部分情況下親爹幹爹是一個人,同ps命令看到的是幹爹,什麽時候親爹幹爹不一樣的?當親爹死了,但是呢又得有一個父進程,比如1號進程就會被當成父進程。
2.4.2標識符
pid_t pid;
pid_t tgid;
在Linux中,操作系統為了更好地區分內核中每一個進程或者是每一個用戶,引入了標識符的概念。每一個進程有進程標識符,用戶標識符,組標識符等等。那麽在這裏我們著重地談一下進程標識符PID。PID是32位的無符號整數,它被順序編號,新創建進程的PID通常是前一個進程的PID加1。當一個活躍的程序結束後,再次開啟他的pid將被賦予不同於上一次的值。Linux中pid規定最大為32767,當要創建第32768個pid時系統將啟用正在閑置的pid。
域名 | 含義 |
Pid | 進程標識符 |
Uid、gid | 用戶標識符、組標識符 |
Euid、egid | 有效用戶標識符、有效組標識符 |
Suid、sgid | 有效用戶標識符、有效組標識符 |
Fsuid、fsgid | 文件系統用戶標識符、文件系統組標識符 |
2.4.3進程標記
unsigned int flags; /* per process flags, defined below */
進程標記是反應進程狀態的信息,但不是運行狀態,用於內核識別進程當前的狀態,以便下一步操作,flags成員的可能取值如下:
/* * Per process flags */ #define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */ /* Not implemented yet, only for 486*/ #define PF_STARTING 0x00000002 /* being created */ #define PF_EXITING 0x00000004 /* getting shut down */ #define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */ #define PF_VCPU 0x00000010 /* I‘m a virtual CPU */ #define PF_FORKNOEXEC 0x00000040 /* forked but didn‘t exec */ #define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */ #define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ #define PF_DUMPCORE 0x00000200 /* dumped core */ #define PF_SIGNALED 0x00000400 /* killed by a signal */ #define PF_MEMALLOC 0x00000800 /* Allocating memory */ #define PF_FLUSHER 0x00001000 /* responsible for disk writeback */ #define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */ #define PF_FREEZING 0x00004000 /* freeze in progress. do not account to load */ #define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */ #define PF_FROZEN 0x00010000 /* frozen for system suspend */ #define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */ #define PF_KSWAPD 0x00040000 /* I am kswapd */ #define PF_OOM_ORIGIN 0x00080000 /* Allocating much memory to others */ #define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */ #define PF_KTHREAD 0x00200000 /* I am a kernel thread */ #define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */ #define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */ #define PF_SPREAD_PAGE 0x01000000 /* Spread page cache over cpuset */ #define PF_SPREAD_SLAB 0x02000000 /* Spread some slab caches over cpuset */ #define PF_THREAD_BOUND 0x04000000 /* Thread bound to specific cpu */ #define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */ #define PF_MEMPOLICY 0x10000000 /* Non-default NUMA mempolicy */ #define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */ #define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezeable */ #define PF_FREEZER_NOSIG 0x80000000 /* Freezer won‘t send signals to it */
2.4.4進程的優先級
進程的優先級是一種針對cpu占用的Qos機制,優先級高占有cpu時間高,優先級低的占有cpu時間低,甚至高優先級進程可以搶占低優先級進程占用cpu。
在談優先級之前不得不先說"nice"值,nice值它是反應一個進程“優先級”狀態的值,其取值範圍是-20至19,一共40個級別,默認取中間值0。這個值越小,表示進程”優先級”越高,而值越大“優先級”越低,也即負值表示高優先級,正值表示低優先級。也許你一開始你會記混,但其實從名字上我們就很好的理解這個機制,在英語中,如果我們形容一個人nice,那一般說明這個人的人緣比較好。什麽樣的人人緣好?往往是謙讓、有禮貌的人。比如,你跟一個nice的人一起去吃午飯,點了兩個一樣的飯,先上了一份後,nice的那位一般都會說:“你先吃你先吃!”,但是如果另一份上的很晚,那麽這位nice的人就要餓著了。這說明什麽?越nice的人越不會去搶占資源,而越不nice的人就越會搶占。這就是nice值大小的含義,nice值越低,說明進程越不nice,搶占cpu的能力就越強,也就意味著優先級就越高。
優先級也分為多種,如下圖:
它們在task_struct中是這樣定義的:
int prio, static_prio, normal_prio; unsigned int rt_priority;
/*
* static_prio = MAX_RT_PRIO + nice + 20
* rt_priority = 0~MAX_RT_PRIO-1
* prio = 0~MAX_PRIO-1 其中 0~MAX_RT_PRIO-1 (MAX_RT_PRIO 定義為100)屬於實時進程範圍,MAX_RT_PRIO~MX_PRIO-1屬於非實時進程。數值越大,表示進程優先級越小。
* normal_prio 由靜態優先級以及調度決策決定
*/
它們的取值定義是:
/* * Priority of a process goes from 0..MAX_PRIO-1, valid RT * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority * values are inverted: lower p->prio value means higher priority. * * The MAX_USER_RT_PRIO value allows the actual maximum * RT priority to be separate from the value exported to * user-space. This allows kernel threads to set their * priority to a value higher than any user task. Note: * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO. */ #define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40) #define DEFAULT_PRIO (MAX_RT_PRIO + 20)
2.5進程狀態及轉換
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
他可能的取值有:
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32 /* in tsk->state again */ #define TASK_DEAD 64 #define TASK_WAKEKILL 128 #define TASK_WAKING 256 /* Convenience macros for the sake of set_task_state */ #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) /* Convenience macros for the sake of wake_up */ #define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE) #define TASK_ALL (TASK_NORMAL | __TASK_STOPPED | __TASK_TRACED) /* get_task_state() */ #define TASK_REPORT (TASK_RUNNING | TASK_INTERRUPTIBLE | \ TASK_UNINTERRUPTIBLE | __TASK_STOPPED | __TASK_TRACED)
2.5.1五個互斥狀態
state域能夠取5個互為排斥的值。系統中的每個進程都必然處於以上所列進程狀態中的一種。
- 運行狀態(TASK_RUNNING):表示進程要麽正在執行,要麽正要準備執行(已經就緒),正在等待cpu時間片的調度。
- 可中斷睡眠狀態(TASK_INTERRUPTIBLE):進程因為等待一些條件而被掛起(阻塞)而所處的狀態。一旦等待的條件成立,進程就會從該狀態(阻塞)迅速轉化成為就緒狀態TASK_RUNNING。
- 不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE):與TASK_INTERRUPTIBLE類似,除了不能通過接受一個信號來喚醒以外,對於處於TASK_UNINTERRUPIBLE狀態的進程,哪怕我們傳遞一個信號或者有一個外部中斷都不能喚醒他們。只有它所等待的資源可用的時候,他才會被喚醒。
- 暫停狀態(TASK_STOPPED):進程被暫停執行,當進程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信號之後就會進入該狀態。
- 追蹤狀態(TASK_TRACED):表示進程被debugger等進程監視,進程執行被調試程序所暫停,當一個進程被另外的進程所監視,每一個信號都會讓進程進入該狀態。
2.5.2兩個終止狀態
- 僵屍狀態(EXIT_ZOMBIE):進程的執行被終止,但是其父進程還沒有使用wait()等系統調用來獲知它的終止信息,此時進程成為僵屍進程。
- 死亡狀態(EXIT_DEAD):當子進程死亡,父進程調用wait(),進入最終死亡狀態。
2.5.3狀態轉換圖
3.進程調度
3.1什麽是進程調度
進程調度器,其一般原理是, 按所需分配的CPU計算能力, 向系統中每個進程提供最大的公正性, 而同時又要考慮不同的任務優先級;或者從另外一個角度上說, 他試圖確保沒有進程被虧待。
3.2調度器的演變
一開始的調度器是復雜度為O(n)O(n)的始調度算法(實際上每次會遍歷所有任務,所以復雜度為O(n)), 這個算法的缺點是當內核中有很多任務時,調度器本身就會耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)O(1)調度器
然而,linux是集全球很多程序員的聰明才智而發展起來的超級內核,沒有最好,只有更好,在O(1)O(1)調度器風光了沒幾天就又被另一個更優秀的調度器取代了,它就是CFS調度器Completely Fair Scheduler. 這個也是在2.6內核中引入的,具體為2.6.23,即從此版本開始,內核使用CFS作為它的默認調度器,O(1)O(1)調度器被拋棄了, 其實CFS的發展也是經歷了很多階段,最早期的樓梯算法(SD), 後來逐步對SD算法進行改進出RSDL(Rotating Staircase Deadline Scheduler), 這個算法已經是”完全公平”的雛形了, 直至CFS是最終被內核采納的調度器, 它從RSDL/SD中吸取了完全公平的思想,不再跟蹤進程的睡眠時間,也不再企圖區分交互式進程。它將所有的進程都統一對待,這就是公平的含義。CFS的算法和實現都相當簡單,眾多的測試表明其性能也非常優越。
字段 |
版本 |
O(n) |
linux-0.11~2.4 |
O(1) |
linux-2.5 |
CFS |
linux2.6~至今 |
3.3CFS算法(完全公平調度算法)
CFS思路很簡單,就是根據各個進程的權重分配運行時間進程的運行時間計算公式為:
分配給進程的運行時間 = 調度周期 * 進程權重 / 所有進程權重之和 (公式1)
調度周期很好理解,就是將所有處於TASK_RUNNING態進程都調度一遍的時間,差不多相當於O(1)調度算法中運行隊列和過期隊列切換一次的時間(對O(1)調度算法看得不是很熟,如有錯誤還望各位大蝦指出)。舉個例子,比如只有兩個進程A, B,權重分別為1和2,調度周期設為30ms,那麽分配給A的CPU時間為:30ms * (1/(1+2)) = 10ms;而B的CPU時間為:30ms * (2/(1+2)) = 20ms。那麽在這30ms中A將運行10ms,B將運行20ms。
公平怎麽體現呢?它們的運行時間並不一樣阿?這就需要我們接下來要說到的虛擬運行時間的概念了
3.3.1虛擬運行時間(vruntime)
CFS定義了一種新的模型,它給cfs_rq(cfs的run queue)中的每一個進程安排一個虛擬時鐘,vruntime。如果一個進程得以執行,隨著時間的增長(也就是一個個tick的到來),其vruntime將不斷增大。沒有得到執行的進程vruntime不變。而調度器總是選擇vruntime跑得最慢的那個進程來執行。這就是所謂的“完全公平”。為了區別不同優先級的進程,優先級高的進程vruntime增長得慢,以至於它可能得到更多的運行機會。
3.3.2調度實體(sched entiy)
sched_entity是調度實體描述,描述可被調度的對象:
struct sched_entity { struct load_weight load; /* for load-balancing */ struct rb_node run_node; struct list_head group_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 last_wakeup; u64 avg_overlap; u64 nr_migrations; u64 start_runtime; u64 avg_wakeup; u64 avg_running; #ifdef CONFIG_SCHEDSTATS u64 wait_start; u64 wait_max; u64 wait_count; u64 wait_sum; u64 iowait_count; u64 iowait_sum; u64 sleep_start; u64 sleep_max; s64 sum_sleep_runtime; u64 block_start; u64 block_max; u64 exec_max; u64 slice_max; u64 nr_migrations_cold; u64 nr_failed_migrations_affine; u64 nr_failed_migrations_running; u64 nr_failed_migrations_hot; u64 nr_forced_migrations; u64 nr_forced2_migrations; u64 nr_wakeups; u64 nr_wakeups_sync; u64 nr_wakeups_migrate; u64 nr_wakeups_local; u64 nr_wakeups_remote; u64 nr_wakeups_affine; u64 nr_wakeups_affine_attempts; u64 nr_wakeups_passive; u64 nr_wakeups_idle; #endif #ifdef CONFIG_FAIR_GROUP_SCHED struct sched_entity *parent; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; #endif };
3.3.3紅黑樹
與之前的 Linux 調度器不同,它沒有將任務維護在運行隊列中,CFS 維護了一個以時間為順序的紅黑樹(參見下圖)。 紅黑樹 是一個樹,具有很多有趣、有用的屬性。首先,它是自平衡的,這意味著樹上沒有路徑比任何其他路徑長兩倍以上。 第二,樹上的運行按 O(log n) 時間發生(其中 n 是樹中節點的數量)。這意味著您可以快速高效地插入或刪除任務。
任務存儲在以時間為順序的紅黑樹中(由 sched_entity 對象表示),對處理器需求最多的任務 (最低虛擬運行時)存儲在樹的左側,處理器需求最少的任務(最高虛擬運行時)存儲在樹的右側。 為了公平,調度器先選取紅黑樹最左端的節點調度為下一個以便保持公平性。任務通過將其運行時間添加到虛擬運行時, 說明其占用 CPU 的時間,然後如果可運行,再插回到樹中。這樣,樹左側的任務就被給予時間運行了,樹的內容從右側遷移到左側以保持公平。 因此,每個可運行的任務都會追趕其他任務以維持整個可運行任務集合的執行平衡。
3.3.4CFS 組調度
CFS 另一個有趣的地方是組調度 概念(在 2.6.24 內核中引入)。組調度是另一種為調度帶來公平性的方式,尤其是在處理產生很多其他任務的任務時。 假設一個產生了很多任務的服務器要並行化進入的連接。不是所有任務都會被統一公平對待, CFS 引入了組來處理這種行為。產生任務的服務器進程在整個組中(在一個層次結構中)共享它們的虛擬運行時,而單個任務維持其自己獨立的虛擬運行時。這樣單個任務會收到與組大致相同的調度時間。
4.體會
對於 Linux 技術而言,惟一不變的就是永恒的變化。CFS 是 2.6 Linux 調度器; 或許今後就會是另一個新的調度器或一套可以被靜態或動態調用的調度器。 CFS、RSDL 以及內核背後的進程中還有很多深刻的含義與秘密,等待我們去探索。
5.參考資料
https://blog.csdn.net/yiyeguzhou100/article/details/50994232
https://blog.csdn.net/gatieme/article/details/51702662
https://www.cnblogs.com/yysblog/archive/2012/11/20/2779120.html
第一次作業:Linux 2.6.32的進程模型與調度器分析