1.進程的組織
1.1進程定義
百度百科中,進程定義如下:進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。
一個進程在CPU上運行可以有兩種運行模式(進程狀態):用戶模式和內核模式。如果當前運行的是用戶程序(用戶代碼),那么對應進程就處於用戶模式(用戶態),如果出現系統調用或者發生中斷,那么對應進程就處於內核模式(核心態)。
在Linux操作系統下,我們通過ps指令實現進程的查看,如下:
1.2進程的組織
Linux進程通過一個task_struct結構體描述,在linux/sched.h中定義,通過理解該結構,可更清楚的理解linux進程模型。包含進程所有信息的task_struct數據結構是比較龐大的,但是該數據結構本身並不復雜,我們將它的所有域按其功能可做如下划分:
- 進程狀態(State)
- 進程調度信息(Scheduling Information)
- 各種標識符(Identifiers)
- 進程通信有關信息(IPC:Inter_Process Communication)
- 時間和定時器信息(Times and Timers)
- 進程鏈接信息(Links)
- 文件系統信息(File System)
- 虛擬內存信息(Virtual Memory)
- 頁面管理信息(page)
- 對稱多處理器(SMP)信息
- 和處理器相關的環境(上下文)信息(Processor Specific Context)
- 其他信息
1.3標識符(Identifiers)
進程標識符在task_struct類中如下定義:
pid_t pid;//內核中用以標識進程的id pid_t tgid;//用來實現線程機制
struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; }; * What is struct pid? * * A struct pid is the kernel's internal notion of a process identifier. * It refers to individual tasks, process groups, and sessions. While * there are processes attached to it the struct pid lives in a hash * table, so it and then the processes that it refers to can be found * quickly from the numeric pid value. The attached processes may be * quickly accessed by following pointers from struct pid.
每個進程有進程標識符、用戶標識符、組標識符,如下:
域名 | 含義 |
Pid | 進程標識符 |
Uid、gid | 用戶標識符、組標識符 |
Euid、egid | 有效用戶標識符、有效組標識符 |
Suid、sgid | 備份用戶標識符、備份組標識符 |
Fsuid、fsgid | 文件系統用戶標識符、文件系統組標識符 |
不管對內核還是普通用戶來說,怎么用一種簡單的方式識別不同的進程呢?這就引入了進程標識符(PID:process identifier),每個進程都有一個唯一的標識符,內核通過這個標識符來識別不同的進程,同時,進程標識符PID也是內核提供給用戶程序的接口,用戶程序通過PID對進程發號施令。PID是32位的無符號整數,它被順序編號:新創建進程的PID通常是前一個進程的PID加1。然而,為了與16位硬件平台的傳統Linux系統保持兼容,在Linux上允許的最大PID號是32767,當內核在系統中創建第32768個進程時,就必須重新開始使用已閑置的PID號。
2..進程狀態
為了對進程從產生到消亡的整個過程進行跟蹤和描述,就需要定義各種進程的各種狀態並制定相應的狀態轉換策略,以此來控制進程的運行。
Linux系統中,進程狀態在 task_struct
中定義如下:
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 #define TASK_STATE_MAX 512
對進程每個狀態簡析如下:
- TASK_RUNNING (可運行狀態):處於這種狀態的進程,要么正在運行、要么正准備運行。正在運行的進程就是當前進程(由current所指向的進程),而准備運行的進程只要得到CPU就可以立即投入運行,CPU是這些進程唯一等待的系統資源。
- TASK_INTERRUPTIBLE(可中斷的等待狀態):表示進程被阻塞(睡眠),直到某個條件達成,進程的狀態就被設置為TASK_RUNNING。處於該狀態的進程正在等待某個事件(event)或某個資源,而被掛起。對應的task_struct結構被放入對應事件的等待隊列中。處於可中斷等待態的進程可以被信號(外部中斷觸發或者其他進程觸發)喚醒,如果收到信號,該進程就從等待狀態進入可運行狀態,並且加入到運行隊列中,等待被調度。
- TASK_UNINTERRUPTIBLE(不可中斷的等待狀態):該狀態與 TASK_INTERRUPTIBLE 狀態類似,也表示進程被阻塞,處於睡眠狀態。當進程等待的某些條件被滿足了之后,內核也會將該進程的狀態設置為 TASK_RUNNING。但是,處於這個狀態下的進程不能在接收到某個信號之后立即被喚醒。這時該狀態與 TASK_INTERRUPTIBLE 狀態唯一的區別。
- __TASK_STOPPED(暫停狀態):此時的進程暫時停止運行來接受某種特殊處理。通常當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信號后就處於這種狀態。例如,正接受調試的進程就處於這種狀態。
- __TASK_TRACED(跟蹤狀態):當前進程正在被另一個進程所監視。
- EXIT_ZOMBIE(僵死狀態):進程雖然已經終止,但由於某種原因,父進程還沒有執行wait()系統調用,終止進程的信息也還沒有回收。顧名思義,處於該狀態的進程就是死進程,這種進程實際上是系統中的垃圾,必須進行相應處理以釋放其占用的資源。
- EXIT_DEAD:一個進程的最終狀態。
以下是LINUX進程間狀態轉換和內核調用圖解
3.進程的調度
3.1 進程調度概述
內存中保存了對每個進程的唯一描述,,並通過若干結構與其他進程連接起來。調度器面對的情形就是這樣, 其任務是在程序之間共享CPU時間, 創造並行執行的錯覺, 該任務分為兩個不同的部分,,其中一個涉及調度策略, 另外一個涉及上下文切換。
3.2Linux調度器的發展
一開始的調度器是復雜度為O(n)的始調度算法(實際上每次會遍歷所有任務,所以復雜度為O(n)),這個算法的缺點是當內核中有很多任務時,調度器本身就會耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)調度器。可沒過多久,一個優秀的調度器就被開發出來了,它就是CFS調度器Completely Fair Scheduler。 這個是在2.6內核中引入的,具體為2.6.23,即從此版本開始,內核使用CFS作為它的默認調度器,O(1)調度器被拋棄了。
3.3 O(1)調度算法
3.3.1優先級數組
O(1)算法的一個核心數據結構即為prio_array結構體。該結構體中有一個用來表示進程動態優先級的數組queue,它包括了每一種優先級進程所形成的鏈表。
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40) typedef struct prio_array prio_array_t; struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; };
因為進程優先級的最大值為139,因此MAX_PRIO的最大值取140(詳細的是,普通進程使用100到139的優先級。實時進程使用0到99的優先級)。因此,queue數組中包括140個可執行狀態的進程鏈表,每一條優先級鏈表上的進程都具有同樣的優先級,而不同進程鏈表上的進程都擁有不同的優先級。除此之外,prio_array結構中還包含一個優先級位圖bitmap。該位圖使用一個位(bit)來代表一個優先級,而140個優先級最少須要5個32位來表示,因此BITMAP_SIZE的值取5。起初。該位圖中的全部位都被置0,當某個優先級的進程處於可執行狀態時。該優先級所相應的位就被置1。因此,O(1)算法中查找系統最高的優先級就轉化成查找優先級位圖中第一個被置1的位。與2.4內核中依次比較每一個進程的優先級不同,因為進程優先級個數是定值,因此查找最佳優先級的時間恆定。它不會像曾經的方法那樣受可運行進程數量的影響。假設確定了優先級。那么選取下一個進程就簡單了,僅僅需在queue數組中相應的鏈表上選取一個進程就可以。
3.3.2活動進程和過期進程
當處於執行態的進程用完時間片后就會處於就緒態。此時調度程序再從就緒態的進程中選取一個作為即將要執行的進程。而在詳細Linux內核中,就緒態和執行態統一稱為可執行態(TASK_RUNNING)。
對於系統內處於可執行狀態的進程,我們能夠分為三類。首先是正處於執行狀態的那個進程;其次,有一部分處於可執行狀態的進程則還沒實用完他們的時間片。他們等待被執行;剩下的進程已經用完了自己的時間片,在其它進程沒實用完它們的時間片之前,他們不能再被執行。據此,我們將進程分為兩類,活動進程,那些還沒實用完時間片的進程。過期進程。那些已經用完時間片的進程。因此,調度程序的工作就是在活動進程集合中選取一個最佳優先級的進程,假設該進程時間片恰好用完,就將該進程放入過期進程集合中。
關於可執行隊列和兩個優先級數組的關系可參考以下的圖:
3.4全然公平(CFS)調度算法
3.4.1概述
CFS 調度程序使用安撫(appeasement)策略確保公平性。當某個任務進入運行隊列后,將記錄當前時間,當某個進程等待 CPU 時,將對這個進程的 wait_runtime 值加一個數,這個數取決於運行隊列當前的進程數。當執行這些計算時,也將考慮不同任務的優先級值。 將這個任務調度到 CPU 后,它的 wait_runtime 值開始遞減,當這個值遞減到其他任務成為紅黑樹的最左側任務時,當前任務將被搶占。通過這種方式,CFS 努力實現一種理想 狀態,即 wait_runtime 值為 0。
3.4.2CFS調度器類fair_sched_class
CFS完全公平調度器的調度器類叫fair_sched_class, 其定義在kernel/sched/fair.c,line 8521, 它是我們熟知的是struct sched_class調度器類類型, 將我們的CFS調度器與一些特定的函數關聯起來.
/* * All the scheduling class methods: */ const struct sched_class fair_sched_class = { .next = &idle_sched_class, /* 下個優先級的調度類, 所有的調度類通過next鏈接在一個鏈表中*/ .enqueue_task = enqueue_task_fair, .dequeue_task = dequeue_task_fair, .yield_task = yield_task_fair, .yield_to_task = yield_to_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, .migrate_task_rq = migrate_task_rq_fair, .rq_online = rq_online_fair, .rq_offline = rq_offline_fair, .task_waking = task_waking_fair, .task_dead = task_dead_fair, .set_cpus_allowed = set_cpus_allowed_common, #endif .set_curr_task = set_curr_task_fair, .task_tick = task_tick_fair, .task_fork = task_fork_fair, .prio_changed = prio_changed_fair, .switched_from = switched_from_fair, .switched_to = switched_to_fair, .get_rr_interval = get_rr_interval_fair, .update_curr = update_curr_fair, #ifdef CONFIG_FAIR_GROUP_SCHED .task_move_group = task_move_group_fair, #endif };
下面就這調度器類成員進行介紹:
- enqueue_task:向就緒隊列中添加一個進程, 某個任務進入可運行狀態時,該函數將得到調用。它將調度實體(進程)放入紅黑樹中,並對 nr_running 變量加 1。
- dequeue_task:將一個進程從就就緒隊列中刪除, 當某個任務退出可運行狀態時調用該函數,它將從紅黑樹中去掉對應的調度實體,並從 nr_running 變量中減 1。
- yield_task:在進程想要資源放棄對處理器的控制權的時, 可使用在sched_yield系統調用, 會調用內核API yield_task完成此工作. compat_yield sysctl 關閉的情況下,該函數實際上執行先出隊后入隊;在這種情況下,它將調度實體放在紅黑樹的最右端。
- check_preempt_curr:該函數將檢查當前運行的任務是否被搶占。在實際搶占正在運行的任務之前,CFS 調度程序模塊將執行公平性測試。這將驅動喚醒式(wakeup)搶占。
- pick_next_task:該函數選擇接下來要運行的最合適的進程。
- put_prev_task:用另一個進程代替當前運行的進程。
- set_curr_task:當任務修改其調度類或修改其任務組時,將調用這個函數。
- task_tick:在每次激活周期調度器時, 由周期性調度器調用, 該函數通常調用自 time tick 函數;它可能引起進程切換。這將驅動運行時(running)搶占。
- task_new:內核調度程序為調度模塊提供了管理新任務啟動的機會, 用於建立fork系統調用和調度器之間的關聯, 每次新進程建立后, 則用new_task通知調度器, CFS 調度模塊使用它進行組調度,而用於實時任務的調度模塊則不會使用這個函數。
3.4.3CFS的就緒隊列
就緒隊列是全局調度器許多操作的起點, 但是進程並不是由就緒隊列直接管理的, 調度管理是各個調度器的職責, 因此在各個就緒隊列中嵌入了特定調度類的子就緒隊列(cfs的頂級調度就隊列struct cfs_rq, 實時調度類的就緒隊列struct rt_rq和deadline調度類的就緒隊列struct dl_rq)。
/* CFS-related fields in a runqueue */ /* CFS調度的運行隊列,每個CPU的rq會包含一個cfs_rq,而每個組調度的sched_entity也會有自己的一個cfs_rq隊列 */ struct cfs_rq { /* CFS運行隊列中所有進程的總負載 */ struct load_weight load; /* * nr_running: cfs_rq中調度實體數量 * h_nr_running: 只對進程組有效,其下所有進程組中cfs_rq的nr_running之和 */ unsigned int nr_running, h_nr_running; u64 exec_clock; /* * 當前CFS隊列上最小運行時間,單調遞增 * 兩種情況下更新該值: * 1、更新當前運行任務的累計運行時間時 * 2、當任務從隊列刪除去,如任務睡眠或退出,這時候會查看剩下的任務的vruntime是否大於min_vruntime,如果是則更新該值。 */ u64 min_vruntime; #ifndef CONFIG_64BIT u64 min_vruntime_copy; #endif /* 該紅黑樹的root */ struct rb_root tasks_timeline; /* 下一個調度結點(紅黑樹最左邊結點,最左邊結點就是下個調度實體) */ struct rb_node *rb_leftmost; /* * 'curr' points to currently running entity on this cfs_rq. * It is set to NULL otherwise (i.e when none are currently running). * curr: 當前正在運行的sched_entity(對於組雖然它不會在cpu上運行,但是當它的下層有一個task在cpu上運行,那么它所在的cfs_rq就把它當做是該cfs_rq上當前正在運行的sched_entity) * next: 表示有些進程急需運行,即使不遵從CFS調度也必須運行它,調度時會檢查是否next需要調度,有就調度next * * skip: 略過進程(不會選擇skip指定的進程調度) */ struct sched_entity *curr, *next, *last, *skip; #ifdef CONFIG_SCHED_DEBUG unsigned int nr_spread_over; #endif #ifdef CONFIG_SMP /* * CFS load tracking */ struct sched_avg avg; u64 runnable_load_sum; unsigned long runnable_load_avg; #ifdef CONFIG_FAIR_GROUP_SCHED unsigned long tg_load_avg_contrib; #endif atomic_long_t removed_load_avg, removed_util_avg; #ifndef CONFIG_64BIT u64 load_last_update_time_copy; #endif #ifdef CONFIG_FAIR_GROUP_SCHED /* * h_load = weight * f(tg) * * Where f(tg) is the recursive weight fraction assigned to * this group. */ unsigned long h_load; u64 last_h_load_update; struct sched_entity *h_load_next; #endif /* CONFIG_FAIR_GROUP_SCHED */ #endif /* CONFIG_SMP */ #ifdef CONFIG_FAIR_GROUP_SCHED /* 所屬於的CPU rq */ struct rq *rq; /* cpu runqueue to which this cfs_rq is attached */ /* * leaf cfs_rqs are those that hold tasks (lowest schedulable entity in * a hierarchy). Non-leaf lrqs hold other higher schedulable entities * (like users, containers etc.) * * leaf_cfs_rq_list ties together list of leaf cfs_rq's in a cpu. This * list is used during load balance. */ int on_list; struct list_head leaf_cfs_rq_list; /* 擁有該CFS運行隊列的進程組 */ struct task_group *tg; /* group that "owns" this runqueue */ #ifdef CONFIG_CFS_BANDWIDTH int runtime_enabled; u64 runtime_expires; s64 runtime_remaining; u64 throttled_clock, throttled_clock_task; u64 throttled_clock_task_time; int throttled, throttle_count; struct list_head throttled_list; #endif /* CONFIG_CFS_BANDWIDTH */ #endif /* CONFIG_FAIR_GROUP_SCHED */ };
成員描述:
- nr_running:隊列上可運行進程的數目。
- load:就緒隊列上可運行進程的累計負荷權重。
- min_vruntime:跟蹤記錄隊列上所有進程的最小虛擬運行時間. 這個值是實現與就緒隊列相關的虛擬時鍾的基礎。
- tasks_timeline:用於在按時間排序的紅黑樹中管理所有進程。
- rb_leftmost:總是設置為指向紅黑樹最左邊的節點, 即需要被調度的進程. 該值其實可以可以通過病例紅黑樹獲得, 但是將這個值存儲下來可以減少搜索紅黑樹花費的平均時間。
- curr:當前正在運行的sched_entity(對於組雖然它不會在cpu上運行,但是當它的下層有一個task在cpu上運行,那么它所在的cfs_rq就把它當做是該cfs_rq上當前正在運行的sched_entity。
- next:表示有些進程急需運行,即使不遵從CFS調度也必須運行它,調度時會檢查是否next需要調度,有就調度next。
- skip:略過進程(不會選擇skip指定的進程調度)。
3.4.4紅黑樹
紅黑樹 (Red–black tree) 是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構。在 CFS 調度器中將 sched_entity 存儲在以時間為順序的紅黑樹中,vruntime 最低的進程存儲在樹的左側,vruntime 最高的進程存儲在樹的右側。 為了公平,調度器每次都會選取紅黑樹最左端的節點調度以便保持公平性。這樣,整顆紅黑樹最左側的進程就被給予時間運行了,樹的內容從右側遷移到左側以保持公平。同時,紅黑樹的時間復雜度為 O(log n),可以快速高效地執行插入或是刪除操作。如下圖:
4.小結
進程是一種數據結構,進程的引入為了使程序在多道程序環境下能並發執行。盡管進程只是計算機操作系統中很小的一方面,但它卻為了整個系統運行提供了極大地幫助。我們完全可以將進程比擬成我們現今紛繁復雜的世界,我們人類便是一個個程序,如何更好的調整社會朝着安穩和諧快速發展,這是我們要努力的。在如今計算機越來越復雜的情況下,我們急需對進程進行更深入的研究。
5.參考資料
1.https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin
2.http://blog.chinaunix.net/uid-24203478-id-3130713.html
3.https://blog.csdn.net/jinkang_zhao/article/details/71367924
4.https://blog.csdn.net/gatieme/article/details/51701149
5.https://blog.csdn.net/gatieme/article/details/52067518