1. 程式人生 > >第一次作業:Linux的進程模型及CFS調度器算法分析

第一次作業:Linux的進程模型及CFS調度器算法分析

並行執行 pick 資源 virt 時鐘 程序代碼 processor 關系 mov

1. 關於進程

1.1. 進程的定義

進程:指在系統中能獨立運行並作為資源分配的基本單位,它是由一組機器指令、數據和堆棧等組成的,是一個能獨立運行的活動實體。

  1. 進程是程序的一次執行
  2. 進程是可以和別的計算並行執行
  3. 進程是程序在一個數據集合上運行的過程,它是系統進行資源分配和調度的一個獨立單位

1.2. 進程的特征

1.動態性:進程的實質是程序的一次執行過程,進程是動態產生,動態消亡的。
2.並發性:任何進程都可以同其他進程一起並發執行。
3.獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位。

4.異步性:由於進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可預知的速度向前推進。

2. 關於進程的組織

task_struct 是Linux內核的一種數據結構,它被裝在到RAM裏並且包含著進程的信息。每個進程都把它的信息放在 task_struct 這個數據結構中, task_struct 包含了以下內容:

標識符:描述本進程的唯一標識符,用來區別其他進程。

狀態:任務狀態,退出代碼,退出信號等。

優先級:相對於其他進程的優先級。

程序計數器:程序中即將被執行的下一條指令的地址。

內存指針:包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針。

上下文數據:進程執行時處理器的寄存器中的數據。

I/O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。

記賬信息:可以包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。

保存進程信息的數據結構叫做 task_struct ,並且可以在 include/linux/sched.h 裏找到它。所以運行在系統裏的進程都以 task_struct 鏈表的形式存在於內核中。

2.1. 進程狀態

2.1.1. 進程狀態

volatile long state;  
int exit_state;

2.1.2. state成員的可能取值

#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

2.1.3. 進程的各個狀態

TASK_RUNNING 表示進程正在執行或者處於準備執行的狀態
TASK_INTERRUPTIBLE 進程因為等待某些條件處於阻塞(掛起的狀態),一旦等待的條件成立,進程便會從該狀態轉化成就緒狀態
TASK_UNINTERRUPTIBLE 意思與TASK_INTERRUPTIBLE類似,但是我們傳遞任意信號等不能喚醒他們,只有它所等待的資源可用的時候,他才會被喚醒。
TASK_STOPPED 進程被停止執行
TASK_TRACED 進程被debugger等進程所監視。
EXIT_ZOMBIE 進程的執行被終止,但是其父進程還沒有使用wait()等系統調用來獲知它的終止信息,此時進程成為僵屍進程
EXIT_DEAD 進程被殺死,即進程的最終狀態。
TASK_KILLABLE 當進程處於這種可以終止的新睡眠狀態中,它的運行原理類似於 TASK_UNINTERRUPTIBLE,只不過可以響應致命信號

2.1.4. 狀態轉換圖

技術分享圖片

2.2. 進程標識符(pid)

2.2.1. 標識符定義

pid_t pid; //進程的標識符

2.2.2. 關於標識符

pid是 Linux 中在其命名空間中唯一標識進程而分配給它的一個號碼,稱做進程ID號,簡稱PID。

程序一運行系統就會自動分配給進程一個獨一無二的PID。進程中止後PID被系統回收,可能會被繼續分配給新運行的程序。

是暫時唯一的:進程中止後,這個號碼就會被回收,並可能被分配給另一個新進程。

2.3. 進程標記符

2.3.1. 標記符

unsigned int flags; /* per process flags, defined below */

flags反應進程的狀態信息,用於內核識別當前進程的狀態。

2.3.2. flags的取值範圍

#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_WQ_WORKER    0x00000020      /* I‘m a workqueue worker */
#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_NPROC_EXCEEDED 0x00001000    /* set_user noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH    0x00002000      /* if unset the fpu must be initialized before use */
#define PF_USED_ASYNC   0x00004000      /* used async_schedule*(), used by module init */
#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_MEMALLOC_NOIO 0x00080000     /* Allocating memory without IO involved */
#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_NO_SETAFFINITY 0x04000000    /* Userland is not allowed to meddle with cpus_allowed */
#define PF_MCE_EARLY    0x08000000      /* Early kill for mce process policy */
#define PF_MUTEX_TESTER 0x20000000      /* Thread belongs to the rt mutex tester */
#define PF_FREEZER_SKIP 0x40000000      /* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK 0x80000000      /* this thread called freeze_processes 

下面列出幾個常用的狀態。

狀態描述
PF_FORKNOEXEC 表示進程剛被創建,但還沒有執行
PF_SUPERPRIV 表示進程擁有超級用戶特權
PF_SIGNALED 表示進程被信號殺出
PF_EXITING 表示進程開始關閉

2.4. 表示進程親屬關系的成員

struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *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是該進程的”親生父親“,不管其是否被“寄養”;parent是該進程現在的父進程,有可能是”繼父“;這裏children指的是該進程孩子的鏈表,可以得到所有孩子的進程描述符,但是需使用list_for_each和list_entry,list_entry其實直接使用了container_of,同理,sibling該進程兄弟的鏈表,也就是其父親的所有孩子的鏈表。用法與children相似;struct task_struct *group_leader這個是主線程的進程描述符,也許你會奇怪,為什麽線程用進程描述符表示,因為linux並沒有單獨實現線程的相關結構體,只是用一個進程來代替線程,然後對其做一些特殊的處理;struct list_head thread_group;這個是該進程所有線程的鏈表。

3. 進程的調度

3.1. 完全公平的調度器CFS

CFS(完全公平調度器)是Linux內核2.6.23版本開始采用的進程調度器,它從RSDL/SD中吸取了完全公平的思想,不再跟蹤進程的睡眠時間,也不再企圖區分交互式進程。它將所有的進程都統一對待,這就是公平的含義。它的基本原理如下:設定一個調度周期( sched_latency_ns ),目標是為了讓每個進程在這個周期內至少有機會運行一次,也可以說就是每個進程等待CPU的時間最長不超過這個調度周期;然後根據進程的數量,所有進程平分這個調度周期內的CPU使用權,由於進程的優先級即nice值不同,分割調度周期的時候要加權;每個進程的累計運行時間保存在自己的vruntime字段裏,哪個進程的vruntime最小就獲得本輪運行的權利。CFS的算法和實現都相當簡單,眾多的測試表明其性能也非常優越。

SCHED_NOMALSCHED_BATCH 主要用於CFS調度。這幾個宏的定義可以在 include/linux/sched.h 中找到。文件 kernel/sched.c 包含了內核調度器及相關系統調用的實現。調度的核心函數為 sched.c 中的 schedule()schedule 函數封裝了內核調度的框架。細節實現上調用具體的調度算法類中的函數實現,如 kernel/sched_fair.ckernel/sched_rt.c 中的實現。

3.2. 進程調度的算法

在CFS中,當產生時鐘tick中斷時,sched.c中scheduler_tick()函數會被時鐘中斷(定時器timer的代碼)直接調用,我們調用它則是在禁用中斷時。註意在fork的代碼中,當修改父進程的時間片時,也會導致sched_tick的調用。sched_tick函數首先更新調度信息,然後調整當前進程在紅黑樹中的位置。調整完成後如果發現當前進程不再是最左邊的葉子,就標記need_resched標誌,中斷返回時就會調用scheduler()完成進程切換,否則當前進程繼續占用CPU。註意這與以前的調度器不同,以前是tick中斷導致時間片遞減,當時間片被用完時才觸發優先級調整並重新調度。sched_tick函數的代碼如下:

void scheduler_tick(void)  
{  
    int cpu = smp_processor_id();  
    struct rq *rq = cpu_rq(cpu);  
    struct task_struct *curr = rq->curr;  
  
    sched_clock_tick();  
  
    spin_lock(&rq->lock);  
    update_rq_clock(rq);  
    update_cpu_load(rq);  
    curr->sched_class->task_tick(rq, curr, 0);  
    spin_unlock(&rq->lock);  
  
    perf_event_task_tick(curr, cpu);  
  
#ifdef CONFIG_SMP  
    rq->idle_at_tick = idle_cpu(cpu);  
    trigger_load_balance(rq, cpu);  
#endif  
}  

它先獲取目前CPU上的運行隊列中的當前運行進程,更新runqueue級變量clock,然後通過sched_class中的接口名task_tick,調用CFS的tick處理函數task_tick_fair(),以處理時鐘中斷。我們看kernel/sched_fair.c中的CFS算法實現。

具體的調度類如下:

static const struct sched_class fair_sched_class = {  
    .next           = &idle_sched_class,  
    .enqueue_task       = enqueue_task_fair,  
    .dequeue_task       = dequeue_task_fair,  
    .yield_task     = yield_task_fair,  
  
    .check_preempt_curr = check_preempt_wakeup,  
  
    .pick_next_task     = pick_next_task_fair,  
    .put_prev_task      = put_prev_task_fair,  
  
#ifdef CONFIG_SMP  
    .select_task_rq     = select_task_rq_fair,  
  
    .load_balance       = load_balance_fair,  
    .move_one_task      = move_one_task_fair,  
    .rq_online      = rq_online_fair,  
    .rq_offline     = rq_offline_fair,  
  
    .task_waking        = task_waking_fair,  
#endif  
  
    .set_curr_task          = set_curr_task_fair,  
    .task_tick      = task_tick_fair,  
    .task_fork      = task_fork_fair,  
  
    .prio_changed       = prio_changed_fair,  
    .switched_to        = switched_to_fair,  
  
    .get_rr_interval    = get_rr_interval_fair,  
  
#ifdef CONFIG_FAIR_GROUP_SCHED  
    .task_move_group    = task_move_group_fair,  
#endif  
}; 

task_tick_fair函數用於輪詢調度類的中一個進程。實現如下:

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)  
{  
    struct cfs_rq *cfs_rq;  
    struct sched_entity *se = &curr->se;  
  
    for_each_sched_entity(se) {  /* 考慮了組調度 */  
        cfs_rq = cfs_rq_of(se);  
        entity_tick(cfs_rq, se, queued);  
    }  
}  

該函數獲取各層的調度實體,對每個調度實體獲取CFS運行隊列,調用entity_tick進程進行處理。kernel/sched_fair.c中的函數entity_tick源代碼如下:

static void  
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)  
{  
    /* 
     * Update run-time statistics of the ‘current‘. 
     */  
    update_curr(cfs_rq);  
  
#ifdef CONFIG_SCHED_HRTICK  
    /* 
     * queued ticks are scheduled to match the slice, so don‘t bother 
     * validating it and just reschedule. 
     */  
    if (queued) {  
        resched_task(rq_of(cfs_rq)->curr);  
        return;  
    }  
    /* 
     * don‘t let the period tick interfere with the hrtick preemption 
     */  
    if (!sched_feat(DOUBLE_TICK) &&  
            hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))  
        return;  
#endif  
  
    if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))  
        check_preempt_tick(cfs_rq, curr);  
}  

該函數用kernel/sched_fair.c:update_curr()更新當前進程的運行時統計信息,然後調用kernel/sched_fair.c:check_preempt_tick(),檢測是否需要重新調度,用下一個進程來搶占當前進程。update_curr()實現記賬功能,由系統定時器周期調用,實現如下:

static inline void  
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,  
          unsigned long delta_exec)  
{  
    unsigned long delta_exec_weighted;  
  
    schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));  
  
    curr->sum_exec_runtime += delta_exec; /* 總運行時間更新 */  
    schedstat_add(cfs_rq, exec_clock, delta_exec); /* 更新cfs_rq的exec_clock */  
    /* 用優先級和delta_exec來計算weighted,以用於更新vruntime */  
    delta_exec_weighted = calc_delta_fair(delta_exec, curr); 

4. 對操作系統進程模型的看法

操作系統(Operation System)從本質上說,並不是指我們平時看到的那些窗口、菜單、應用程序。那些只是天邊的浮雲。操作系統其實是隱藏後面,我們根本看不到的部分。操作系統一般來說,工作就是:進程管理、內存管理、文件管理、設備管理等等。操作系統中最核心的概念是進程, 進程也是並發程序設計中的一個最重要、 最基本的概念。進程是一個動態的過程, 即進程有生命周期, 它擁有資源, 是程序的執行過程, 其狀態是變化的。所謂的調度器,就是進程管理的一部分。

Linux一開始的調度器是復雜度為O(n)的始調度算法, 這個算法的缺點是當內核中有很多任務時,調度器本身就耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)調度器。然而,O(1)調度器又被另一個更優秀的調度器取代了,它就是CFS調度器Completely Fair Scheduler. 這個也是在2.6內核中引入的,具體為2.6.23,即從此版本開始,內核使用CFS作為它的默認調度器,O(1)調度器被拋棄了。但其實目前任何調度器算法都還無法滿足所有應用的需要,CFS也有一些負面的測試報告。相信隨著Linux的發展,還會有新的調度算法,我們拭目以待。

第一次作業:Linux的進程模型及CFS調度器算法分析