深入源碼分析Linux進程模型
1. 前言(實驗內容)
- 操作系統是怎麽組織進程的
- 進程狀態如何轉換(給出進程狀態轉換圖)
- 進程是如何調度的
- 談談自己對該操作系統進程模型的看法
2.關於進程
(1)定義:
進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。進程的概念主要有兩點:第一,進程是一個實體。每一個進程都有它自己的地址空間,一般情況下,包擴文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲處理器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲著活動過程調用的指令和本地變量。第二,進程是一個“執行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時(操作系統執行之),它才能成為一個活動的實體,我們稱其為進程。
(2)進程的特征:
- 動態性:進程的實質是程序在多道程序系統中的一次執行過程,進程是動態產生、消亡的;
- 並發性:任何進程都可以同其他進程一起並發執行;
- 獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位;
- 異步性:由於進程間的相互制約,使得進程具有執行的間斷性,即進程按各自獨立的、不可預知的速度向前推進;
(3)進程與程序、線程的區別:
在面向進程設計的系統(如早期的UNIX,Linux 2.4及更早的版本)中,進程是程序的基本執行實體;在面向線程設計的系統(如當代多數操作系統、Linux 2.6及更新的版本)中,進程本身不是基本運行單位,而是線程的容器。簡單地來說,進程與程序是動態與靜止的區別,進程與程序是多對一的,同樣,線程與進程也是多對一的。
3.1 操作系統是如何組織進程的
在Linux系統中, 進程在/linux/include/linux/sched.h
頭文件中被定義為task_struct
, 它是一個結構體, 一個它的實例化即為一個進程, task_struct
由許多元素構成, 下面列舉一些重要的元素進行分析。
- 標識符:與進程相關的唯一標識符,用來區別正在執行的進程和其他進程。
- 狀態:描述進程的狀態,因為進程有掛起,阻塞,運行等好幾個狀態,所以都有個標識符來記錄進程的執行狀態。
- 優先級:如果有好幾個進程正在執行,就涉及到進程被執行的先後順序的問題,這和進程優先級這個標識符有關。
- 程序計數器:程序中即將被執行的下一條指令的地址。
- 內存指針:程序代碼和進程相關數據的指針。
- 上下文數據:進程執行時處理器的寄存器中的數據。
- I/O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表等。
- 記賬信息:包括處理器的時間總和,記賬號等等。
3.1 進程狀態(STATE)
在task_struct
結構體中, 定義進程的狀態語句為
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
valatile
關鍵字的作用是確保本條指令不會因編譯器的優化而省略, 且要求每次直接讀值, 這樣保證了對進程實時訪問的穩定性。
進程在/linux/include/linux/sched.h
頭文件中我們可以找到state
的可能取值如下
/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* the task exiting. Confusing, but this way
* modifying one set can‘t modify the other one by
* mistake.
*/
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_NONINTERACTIVE 64
define TASK_DEAD 128
根據state
後面的註釋, 可以得到當state<0時,表示此進程是處於不可運行的狀態, 當state=0時, 表示此進程正處於運行狀態, 當state>0時, 表示此進程處於停止運行狀態。
以下列舉一些state的常用取值
| 狀態 | 描述 |
| :---------------------- | :----------------------------------------------------------- |
| 0(TASK_RUNNING) | 進程處於正在運行或者準備運行的狀態中 |
| 1(TASK_INTERRUPTIBLE) | 進程處於可中斷睡眠狀態, 可通過信號喚醒 |
| 2(TASK_UNINTERRUPTIBLE) | 進程處於不可中斷睡眠狀態, 不可通過信號進行喚醒 |
| 4( TASK_STOPPED) | 進程被停止執行 |
| 8( TASK_TRACED) | 進程被監視 |
| 16( EXIT_ZOMBIE) | 僵屍狀態進程, 表示進程被終止, 但是其父程序還未獲取其被終止的信息。 |
| 32(EXIT_DEAD) | 進程死亡, 此狀態為進程的最終狀態 |
3.2 進程標識符(PID)
c pid_t pid; /*進程的唯一表示*/ pid_t tgid; /*進程組的標識符*/
在Linux系統中,一個線程組中的所有線程使用和該線程組的領頭線程(該組中的第一個輕量級進程)相同的PID,並被存放在tgid成員中。只有線程組的領頭線程的pid成員才會被設置為與tgid相同的值。註意,getpid()系統調用返回的是當前進程的tgid值而不是pid值。(線程是程序運行的最小單位,進程是程序運行的基本單位。)
3.3 進程的標記(FLAGS)
unsigned int flags; /* per process flags, defined below */
反應進程狀態的信息,但不是運行狀態,用於內核識別進程當前的狀態,以備下一步操作
flags成員的可能取值如下,這些宏以PF(ProcessFlag)開頭
/*
* 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_FORKNOEXEC 0x00000040 /* forked but didn‘t exec */
#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_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_SWAPOFF 0x00080000 /* I am in swapoff */
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */
#define PF_BORROWED_MM 0x00200000 /* I am a kthread doing use_mm */
#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_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 */
3.4 進程之間的關系
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->parent->pid)
*/
struct task_struct *real_parent; /* real parent process (when being debugged) */
struct task_struct *parent; /* parent process */
/*
* children/sibling forms the list of my children plus the
* tasks I‘m ptracing.
*/
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 */
在Linux系統中,所有進程之間都有著直接或間接地聯系,每個進程都有其父進程,也可能有零個或多個子進程。擁有同一父進程的所有進程具有兄弟關系。
real_parent指向其父進程,如果創建它的父進程不再存在,則指向PID為1的init進程。 parent指向其父進程,當它終止時,必須向它的父進程發送信號。它的值通常與 real_parent相同。 children表示鏈表的頭部,鏈表中的所有元素都是它的子進程(進程的子進程鏈表)。 sibling用於把當前進程插入到兄弟鏈表中(進程的兄弟鏈表)。 group_leader指向其所在進程組的領頭進程。
3.5 進程調度
3.5.1 優先級
int prio, static_prio, normal_prio;
unsigned int rt_priority;
/*
prio: 用於保存動態優先級
static_prio: 用於保存靜態優先級, 可以通過nice系統調用來修改
normal_prio: 它的值取決於靜態優先級和調度策略
priort_priority: 用於保存實時優先級
*/
3.5.2 調度策略
unsigned int policy;
cpumask_t cpus_allowed;
/*
policy: 表示進程的調度策略
cpus_allowed: 用於控制進程可以在哪個處理器上運行
*/
policy
表示進程調度策略, 目前主要有以下五種策略
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0 //按優先級進行調度
#define SCHED_FIFO 1 //先進先出的調度算法
#define SCHED_RR 2 //時間片輪轉的調度算法
#define SCHED_BATCH 3 //用於非交互的處理機消耗型的進程
#define SCHED_IDLE 5//系統負載很低時的調度算法
字段 描述 所在調度器類 SCHED_NORMAL (也叫SCHED_OTHER)用於普通進程,通過CFS調度器實現。SCHED_BATCH用於非交互的處理器消耗型進程。SCHED_IDLE是在系統負載很低時使用 CFS SCHED_FIFO 先入先出調度算法(實時調度策略),相同優先級的任務先到先服務,高優先級的任務可以搶占低優先級的任務 RT SCHED_RR 輪流調度算法(實時調度策略),後 者提供 Roound-Robin 語義,采用時間片,相同優先級的任務當用完時間片會被放到隊列尾部,以保證公平性,同樣,高優先級的任務可以搶占低優先級的任務。不同要求的實時任務可以根據需要用sched_setscheduler()API 設置策略 RT SCHED_BATCH SCHED_NORMAL普通進程策略的分化版本。采用分時策略,根據動態優先級(可用nice()API設置),分配 CPU 運算資源。註意:這類進程比上述兩類實時進程優先級低,換言之,在有實時進程存在時,實時進程優先調度。但針對吞吐量優化 CFS SCHED_IDLE 優先級最低,在系統空閑時才跑這類進程(如利用閑散計算機資源跑地外文明搜索,蛋白質結構分析等任務,是此調度策略的適用者) CFS
3.6 進程的地址空間
進程都擁有自己的資源,這些資源指的就是進程的地址空間,每個進程都有著自己的地址空間,在task_struct中,有關進程地址空間的定義如下:
struct mm_struct *mm, *active_mm;
/*
mm: 進程所擁有的用戶空間內存描述符,內核線程無的mm為NULL
active_mm: active_mm指向進程運行時所使用的內存描述符, 對於普通進程而言,這兩個指針變量的值相同。但是內核線程kernel thread是沒有進程地址空間的,所以內核線程的tsk->mm域是空(NULL)。但是內核必須知道用戶空間包含了什麽,因此它的active_mm成員被初始化為前一個運行進程的active_mm值。
*/
如果當前內核線程被調度之前運行的也是另外一個內核線程時候,那麽其mm和avtive_mm都是NULL
以上即為操作系統是怎麽組織進程的一些分析, 有了這些作為基礎, 我們就可以進行下一步的分析
4.進程狀態之間是如何轉換的
關於linux進程狀態(STATE)的定義, 取值以及描述都在進程狀態中進行了詳細的分析, 這裏就不做過多的贅述。
下面給出進程的各種狀態之間是如何進行互相轉換的關系圖:
5.進程是如何進行調度的
5.1 與進程調度有關的數據結構
在了解進程是如何進行調度之前, 我們需要先了解一些與進程調度有關的數據結構。
5.1.1 可運行隊列(runqueue)
在/kernel/sched.c
文件下, 可運行隊列被定義為struct rq
, 每一個CPU都會擁有一個struct rq
, 它主要被用來存儲一些基本的用於調度的信息, 包括及時調度和CFS調度。在Linux kernel 2.6中, struct rq
是一個非常重要的數據結構, 接下來我們介紹一下它的部分重要字段:
/* 選取出部分字段做註釋 */
//runqueue的自旋鎖,當對runqueue進行操作的時候,需要對其加鎖。由於每個CPU都有一個runqueue,這樣會大大減少競爭的機會
spinlock_t lock;
// 此變量是用來記錄active array中最早用完時間片的時間
unsigned long expired_timestamp;
//記錄該CPU上就緒進程總數,是active array和expired array進程總數和
unsigned long nr_running;
// 記錄該CPU運行以來發生的進程切換次數
unsigned long long nr_switches;
// 記錄該CPU不可中斷狀態進程的個數
unsigned long nr_uninterruptible;
// 這部分是rq的最最最重要的部分, 我將在下面仔細分析它們
struct prio_array *active, *expired, arrays[2];
5.1.2 優先級數組(prio_array)
Linux kernel 2.6版本中, 在rq中多加了兩個按優先級排序的數組active array
和expired array
。
這兩個隊列的結構是struct prio_array
, 它被定義在/kernel/sched.c
中, 其數據結構為:
struct prio_array {
unsigned int nr_active; //
DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */
/*開辟MAX_PRIO + 1個bit的空間, 當某一個優先級的task正處於TASK_RUNNING狀態時, 其優先級對應的二進制位將會被標記為1, 因此當你需要找此時需要運行的最高的優先級時, 只需要找到bitmap的哪一位被標記為1了即可*/
struct list_head queue[MAX_PRIO]; // 每一個優先級都有一個list頭
};
Active array
表示的是CPU選擇執行的運行進程隊列, 在這個隊列裏的進程都有時間片剩余, *active
指針總是指向它。
Expired array
則是用來存放在Active array
中使用完時間片的進程, *expired指針總是指向它。
一旦在active array
裏面的某一個普通進程的時間片使用完了, 調度器將重新計算該進程的時間片與優先級, 並將它從active array
中刪除, 插入到expired array
中的相應的優先級隊列中 。
當active array內的所有task都用完了時間片, 這時只需要將*active
與*expired
這兩個指針交換下, 即可切換運行隊列。
5.1.3 調度器主函數(schedule())
schedule
函數存在/kernel/sched.c
中, 是Linux kernel很重要的一個函數, 它的作用是用來挑選出下一個應該執行的進程, 並且完成進程的切換工作, 是進程調度的主要執行者。
5.2 調度算法(O(1)算法)
5.2.1 介紹O(1)算法
何為O(1)算法: 該算法總能夠在有限的時間內選出優先級最高的進程然後執行, 而不管系統中有多少個可運行的進程, 因此命名為O(1)算法。
5.2.2 O(1)算法的原理
在前面我們提到了兩個按優先級排序的數組active array
和expired array
, 這兩個數組是實現O(1)算法的關鍵所在。
O(1)調度算法每次都是選取在active array數組中且優先級最高的進程來運行。
那麽該算法如何找到優先級最高的進程呢? 大家還記得前面prio_array
內的DECLARE_BITMAP(bitmap, MAX_PRIO+1);
字段嗎?這裏它就發揮出作用了(詳情看代碼註釋), 這裏只要找到bitmap
內哪一個位被設置為了1, 即可得到當前系統所運行的task的優先級(idx, 通過sehed_find_first_bit()方法實現), 接下來找到idx所對應的進程鏈表(queue), queue內的所有進程都是目前可運行的並且擁有最高優先級的進程, 接著依次執行這些進程,。
該過程定義在schedule
函數中, 主要代碼如下:
struct task_struct *prev, *next;
struct list_head *queue;
struct prio_array *array;
int idx;
prev = current;
array = rq->active;
idx = sehed_find_first_bit(array->bitmap); //找到位圖中第一個不為0的位的序號
queue = array->queue + idx; //得到對應的隊列鏈表頭
next = list_entry(queue->next, struct task_struct, run_list); //得到進程描述符
if (prev != next) //如果選出的進程和當前進程不是同一個,則交換上下文
context_switch();
6. 對該操作系統進程模型的看法
很多年前就有人說Linux必定會取代Windows,已經過去這麽多年了,就我所知道的是使用Windows越來越多,放棄Linux的也越來越多。很簡單,從桌面端來說,我認為Linux是不能戰勝Windows的,Windows是由有積極進取心的商業公司生產出來的,漂亮、迷人、方便。而Linux不是技術不行,而是這種東西做出來,基本上沒有人欣賞,旁人很難理解的。但Linux有它的優點,就憑這個我支持。可它不做改變的話,就只能保持著這個狀態,在一堆狂熱的職業或業余的程序員中間流傳著。
7. 參考資料
- https://blog.csdn.net/bit_clearoff/article/details/54292300
- https://blog.csdn.net/qq_29503203/article/details/54618275
- https://blog.csdn.net/gatieme/article/details/51383272
- https://blog.csdn.net/bullbat/article/details/7160246
- https://blog.csdn.net/liuxiaowu19911121/article/details/47010721
- Linux kernel 2.6 源碼下載鏈接
深入源碼分析Linux進程模型