1. 程式人生 > >深入源碼分析Linux進程模型

深入源碼分析Linux進程模型

encode https int signed mirror 動態分配 工作 starting 非交互

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 arrayexpired 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 arrayexpired 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進程模型