第一次作業:Linux的進程模型及CFS調度器算法分析
1. 關於進程
1.1. 進程的定義
進程:指在系統中能獨立運行並作為資源分配的基本單位,它是由一組機器指令、數據和堆棧等組成的,是一個能獨立運行的活動實體。
- 進程是程序的一次執行
- 進程是可以和別的計算並行執行
- 進程是程序在一個數據集合上運行的過程,它是系統進行資源分配和調度的一個獨立單位
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_NOMAL 和 SCHED_BATCH 主要用於CFS調度。這幾個宏的定義可以在 include/linux/sched.h 中找到。文件 kernel/sched.c 包含了內核調度器及相關系統調用的實現。調度的核心函數為 sched.c 中的 schedule() , schedule 函數封裝了內核調度的框架。細節實現上調用具體的調度算法類中的函數實現,如 kernel/sched_fair.c 或 kernel/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調度器算法分析