内核进程切换实现分析
當我們在linux編寫用戶態(tài)程序時并不需要考慮進程間是如何切換的, 即使當我們編寫驅(qū)動程序時也只需調(diào)用一些阻塞接口來讓渡cpu. 但是cpu究竟是如何切換進程的, 在進程切換過程中需要做什么, 今天我們通過分析內(nèi)核schedule()的實現(xiàn)來看下內(nèi)核是如何完成進程切換的.
先看下幾個相關(guān)的數(shù)據(jù)結(jié)構(gòu):
1 struct thread_info { 2 ??? unsigned long flags; 3 ??? /** 4 ???? * 搶占標記, 為0可搶占, 大于0不能搶占, 小于0出錯 5 ???? * preempt_disable()/preempt_enable()會修改該值 6 ???? * 同時也是被搶占計數(shù), preempt_count的結(jié)構(gòu)可見include/linux/hardirq.h中描述 7 ???? * 最低字節(jié)為搶占計數(shù), 第二字節(jié)為軟中斷計數(shù), 16-25位(10位)為硬中斷計數(shù) 8 ???? * 26位為不可屏蔽中斷(NMI)標記, 27位為不可搶占標記 9 ???? * 針對preempt_count的判斷宏都在include/linux/hardirq.h中 10 ???? * 11 ??? **/ 12 ??? int preempt_count; 13 ??? //break地址限制 14 ??? mm_segment_t addr_limit; 15 ??? //task結(jié)構(gòu)體 16 ??? struct task_struct *task; 17 ??? struct exec_domain *exec_domain; 18 ??? //線程所在cpu號, 通過raw_smp_processor_id()獲取 19 ??? __u32 cpu; 20 ??? //保存協(xié)處理器狀態(tài), __switch_to()中修改 21 ??? __u32 cpu_domain; 22 ??? //保存寄存器狀態(tài), __switch_to()假定cpu_context緊跟在cpu_domain之后 23 ??? struct cpu_context_save cpu_context; 24 ??? __u32 syscall; 25 ??? __u8 used_cp[16]; 26 ??? unsigned long tp_value; 27 #ifdef CONFIG_CRUNCH 28 ??? struct crunch_state crunchstate; 29 #endif 30 ??? union fp_state fpstate __attribute__((aligned(8))); 31 ??? union vfp_state vfpstate; 32 #ifdef CONFIG_ARM_THUMBEE 33 ??? unsigned long thumbee_state; 34 #endif 35 ??? struct restart_block restart_block; 36 }; 37 struct task_struct { 38 ??? //任務狀態(tài), 0為可運行, -1為不可運行, 大于0為停止 39 ??? volatile long state; 40 ??? void *stack; 41 ??? //引用計數(shù) 42 ??? atomic_t usage; 43 ??? //進程標記狀態(tài)位, 本文用到的是TIF_NEED_RESCHED 44 ??? unsigned int flags; 45 ??? unsigned int ptrace; 46 #ifdef CONFIG_SMP 47 ??? struct llist_node wake_entry; 48 ??? //該進程在被調(diào)度到時是否在另一cpu上運行, 僅SMP芯片判斷 49 ??? //prepare_lock_switch中置位, finish_lock_switch中清零 50 ??? int on_cpu; 51 #endif 52 ??? //是否在運行隊列(runqueue)中 53 ??? int on_rq; 54 ??? int prio, static_prio, normal_prio; 55 ??? unsigned int rt_priority; 56 ??? const struct sched_class *sched_class; 57 ??? struct sched_entity se; 58 ??? struct sched_rt_entity rt; 59 #ifdef CONFIG_CGROUP_SCHED 60 ??? struct task_group *sched_task_group; 61 #endif 62 #ifdef CONFIG_PREEMPT_NOTIFIERS 63 ??? struct hlist_head preempt_notifiers; 64 #endif 65 ??? unsigned int policy; 66 ??? int nr_cpus_allowed; 67 ??? //cpu位圖, 表明task能在哪些cpu上運行, 系統(tǒng)調(diào)用sched_setaffinity會修改該值 68 ??? cpumask_t cpus_allowed; 69 #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) 70 ??? //該進程調(diào)度狀態(tài), 記錄進程被調(diào)度到的時間信息, 在sched_info_arrive中修改 71 ??? struct sched_info sched_info; 72 #endif 73 ??? /** 74 ???? * mm為進程內(nèi)存管理結(jié)構(gòu)體, active_mm為當前使用的內(nèi)存管理結(jié)構(gòu)體 75 ???? * 內(nèi)核線程沒有自己的內(nèi)存空間(內(nèi)核空間共有), 所以它的mm為空 76 ???? * 但內(nèi)核線程仍需要一個內(nèi)存管理結(jié)構(gòu)體來管理內(nèi)存(即active_mm的作用) 77 ???? * 進程則同時存在mm與active_mm, 且兩者相等(否則訪問用戶空間會出錯) 78 ???? * 79 ??? **/ 80 ??? struct mm_struct *mm, *active_mm; 81 ??? //進程上下文切換次數(shù) 82 ??? unsigned long nvcsw, nivcsw; 83 ??? ...... 84 };?
首先來分析調(diào)度管理的入口, 內(nèi)核調(diào)度的通用接口是schedule()(defined in kernel/sched/core.c).
1 asmlinkage void __sched schedule(void) 2 { 3 ??? struct task_struct *tsk = current; 4 ??? sched_submit_work(tsk); 5 ??? __schedule(); 6 }?
current即get_current()(defined in include/asm-generic/current.h), 后者為current_thread_info()->task. current_thread_info()是內(nèi)聯(lián)函數(shù)(defined in arch/arm/include/asm/thread_info.h): THREAD_SIZE大小為8K, 即內(nèi)核假定線程棧向下8K對齊處為thread_info, 通過它索引task_struct.
1 static inline struct thread_info *current_thread_info(void) 2 { 3 ??? register unsigned long sp asm ("sp"); 4 ??? return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); 5 }?
獲取task后判斷當前task是否需要刷新IO隊列, 然后執(zhí)行實際的調(diào)度函數(shù)__schedule().
__schedule()的注釋詳細指出了調(diào)度發(fā)生的時機:
1. 發(fā)生阻塞時, 比如互斥鎖, 信號量, 等待隊列等.
2. 當中斷或用戶態(tài)返回時檢查到TIF_NEED_RESCHED標記位. 為切換不同task, 調(diào)度器會在時間中斷scheduler_tick()中設置標記位.
3. 喚醒不會真正進入schedule(), 它們僅僅在運行隊列中增加一個task. 如果新增的task優(yōu)先于當前的task那么喚醒函數(shù)將置位TIF_NEED_RESCHED, schedule()將最可能在以下情況執(zhí)行:
如果內(nèi)核是開啟搶占的(CONFIG_PREEMPT), 在系統(tǒng)調(diào)用或異常上下文執(zhí)行preempt_enable()之后(最快可能在wake_up()中spin_unlock()之后); 在中斷上下文, 從中斷處理程序返回開啟搶占后.
如果內(nèi)核未開啟搶占, 那么在cond_resched()調(diào)用, 直接調(diào)用schedule(), 從系統(tǒng)調(diào)用或異常返回到用戶空間時, 從異常處理程序返回到用戶空間時.
?
__schedule()中首先關(guān)閉該線程的搶占(current_thread_info->preempt_count自減), 獲取線程所在cpu(current_thread_info->cpu), 再獲取對應cpu的rq. 此處先看下rq的結(jié)構(gòu)(defined in kernel/sched/sched.h). cpu_rq()(defined in kernel/sched/sched.h)是一個復雜的宏, 用于獲取對應cpu的rq結(jié)構(gòu). 來看下它的定義(以SMP芯片為例):
1 #define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) 2 #define per_cpu(var, cpu) (*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))?
SHIFT_PERCPU_PTR()是對特定平臺的宏, 一般平臺上即直接取地址加偏移. 偏移不一定是線性的, 所以用per_cpu_offset()宏獲取(其實質(zhì)是個數(shù)組, 在setup_per_cpu_areas()中初始化). 再看下runqueues(defined in kernel/sched/core.c)又是如何定義的.
1 DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues); 2 #define DEFINE_PER_CPU_SHARED_ALIGNED(type, name) \ 3 ??? DEFINE_PER_CPU_SECTION(type, name, PER_CPU_SHARED_ALIGNED_SECTION) \ 4 ??? ____cacheline_aligned_in_smp 5 #define DEFINE_PER_CPU_SECTION(type, name, sec) \ 6 ??? __PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \ 7 ??? __typeof__(type) name?
這樣就都串起來了, runqueues是struct rq的數(shù)組, 各個cpu通過偏移獲取對應結(jié)構(gòu)體地址.
開始調(diào)度前首先要獲取運行隊列rq的自旋鎖. 調(diào)度器算法是由pre_schedule(), put_prev_task(), pick_next_task()與post_schedule()實現(xiàn)的. 這幾個接口都是當前進程調(diào)度器類型結(jié)構(gòu)體(sched_class)的回調(diào). 關(guān)于調(diào)度器模型的分析以后有空分析, 此處先略過.
回到__schedule(), 再得到調(diào)度后的進程后先清除調(diào)度前的進程的調(diào)度標記, 判斷調(diào)度前后進程是否不同, 不同則執(zhí)行上下文切換的工作, context_switch()(defined in kernel/sched/core.c)是為了恢復到調(diào)度后進程的環(huán)境, 包括TLB(內(nèi)核線程僅訪問內(nèi)核空間無需切換, 用戶進程需切換), 恢復寄存器與堆棧等.
?
context_switch()中最主要的兩個函數(shù)是switch_mm()與switch_to(), 前者切換mm與TLB后者切換寄存器與棧. switch_mm()以后有空詳述, 先看下switch_to(), 其調(diào)用的__switch_to()(defined in arch/arm/kernel/entry-armv.S)是匯編函數(shù):
1 #define switch_to(prev,next,last) \ 2 do { \ 3 ??? last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \ 4 } while (0) 5 ENTRY(__switch_to) 6 UNWIND(.fnstart) 7 UNWIND(.cantunwind) 8 ??? add ip, r1, #TI_CPU_SAVE 9 ??? ldr r3, [r2, #TI_TP_VALUE] 10 ??? ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) 11 ??? THUMB( stmia ip!, {r4 - sl, fp} ) 12 ??? THUMB( str sp, [ip], #4 ) 13 ??? THUMB( str lr, [ip], #4 ) 14 #ifdef CONFIG_CPU_USE_DOMAINS 15 ??? ldr r6, [r2, #TI_CPU_DOMAIN] 16 #endif 17 ??? set_tls r3, r4, r5 18 #if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP) 19 ??? ldr r7, [r2, #TI_TASK] 20 ??? ldr r8, =__stack_chk_guard 21 ??? ldr r7, [r7, #TSK_STACK_CANARY] 22 #endif 23 #ifdef CONFIG_CPU_USE_DOMAINS 24 ??? mcr p15, 0, r6, c3, c0, 0 25 #endif 26 ??? mov r5, r0 27 ??? add r4, r2, #TI_CPU_SAVE 28 ??? ldr r0, =thread_notify_head 29 ??? mov r1, #THREAD_NOTIFY_SWITCH 30 ??? bl atomic_notifier_call_chain 31 #if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP) 32 ??? str r7, [r8] 33 #endif 34 ??? THUMB( mov ip, r4 ) 35 ??? mov r0, r5 36 ??? ARM( ldmia r4, {r4 - sl, fp, sp, pc} ) 37 ??? THUMB( ldmia ip!, {r4 - sl, fp} ) 38 ??? THUMB( ldr sp, [ip], #4 ) 39 ??? THUMB( ldr pc, [ip] ) 40 UNWIND(.fnend) 41 ENDPROC(__switch_to)?
進入函數(shù)調(diào)用時R0與R1分別為調(diào)度前進程的task_struct與thread_info, R2為調(diào)度后進程的thread_info. 函數(shù)首先將當前寄存器中R4-R15(除IP與LR外)全部保存到調(diào)度前進程的thread_info.cpu_context(IP指向的地址)中, 然后恢復TLS(thread local store). set_tls(defined in rch/arm/include/asm/tls.h)用于內(nèi)核向glibc傳遞TLS地址.
1 .macro set_tls_software, tp, tmp1, tmp2 2 ??? mov \tmp1, #0xffff0fff 3 ??? str \tp, [\tmp1, #-15] @ set TLS value at 0xffff0ff0 4 .endm?
在ARM_V7平臺上使用set_tls_software宏, 即將調(diào)度后進程的thread_info.tp_value(R3的值)保存在0xFFFF0FF0, 這是內(nèi)核為glibc獲取TLS專門預留的地址.
恢復TLS后還需恢復協(xié)處理器, 同樣是從thread_info.cpu_domain(R6的值)中獲取. 然后調(diào)用回調(diào)通知鏈, 入?yún)⒎謩e是thread_notify_head, THREAD_NOTIFY_SWITCH, 調(diào)度后進程的thread_info. 看了下這里注冊回調(diào)的都是與架構(gòu)強相關(guān)的代碼: mm, fp, vfp和cp這幾類寄存器的修改. 因為與架構(gòu)強相關(guān), 先不分析了, 以后有空再看.
最后從調(diào)度后進程的thread_info.cpu_context(R4指向的地址)恢復寄存器. 想想為什么不一起保存/恢復R0-R3, IP和LR?
注意此處! 當PC被出棧后, 就切換到調(diào)度后進程代碼執(zhí)行了, 即switch_to()是不返回的函數(shù). 既然是不返回的函數(shù), 后面的代碼是干什么的呢? 自然是線程恢復運行時的恢復代碼了. 當調(diào)度前的進程恢復(即通過__switch_to出棧PC恢復之前執(zhí)行代碼地址)后繼續(xù)執(zhí)行context_switch(). 此時進程身份已經(jīng)互換了, 之前調(diào)度出去的進程作為被調(diào)度到的進程(但是代碼中的prev與next還是沒有改變, 因為寄存器與棧仍為之前的狀態(tài)), 而當前被調(diào)度出去的進程可能是之前調(diào)度到的進程, 也可能是第三個進程. 且此時程序運行在哪個CPU上也是不確定的. 所以先做內(nèi)存屏障, 然后調(diào)用finish_task_switch()完成上下文切換.
?
至此完成進程上下文切換, 重新回到__schedule(), 執(zhí)行post_schedule()完成清理工作, 恢復該進程可搶占狀態(tài), 判斷當前線程是否需要調(diào)度, 如果需要再走一遍流程.
關(guān)于調(diào)度器的代碼我們將在以后具體分析, 如果感興趣也可以看下內(nèi)核文檔中對調(diào)度器的說明(在Documentation/scheduler目錄下), 這里稍稍翻譯下(主要是針對cgroup使用的補充比較有價值).
1. sched-arch.txt
討論與架構(gòu)相關(guān)的調(diào)度策略. 進程上下文切換中運行隊列的自旋鎖處理: 一般要求在握有rq->lock情況下調(diào)用switch_to. 在有些情況下(比如在進程上下文切換時有喚醒操作, 見arch/ia64/include/asm/system.h為例)switch_to需要獲取鎖, 此時調(diào)度器需要保證無鎖時調(diào)用switch_to. 在這種情況下需要定義__ARCH_WANT_UNLOCKED_CTXSW(一般與switch_to定義在一起).
2. sched-bwc.txt
討論SCHED_NORMAL策略的進程的帶寬控制. CFS帶寬控制需要配置CONFIG_FAIR_GROUP_SCHED. 帶寬控制允許進程組指定使用一個周期與占比, 對于給定的周期(以毫秒計算), 進程組最多允許使用占比長度的CPU時間, 當進程組執(zhí)行超過其限制的時間進程將不得再執(zhí)行直到下一個周期到來.
周期與占比通過CPU子系統(tǒng)cgroupfs管理. cpu.cfs_quota_us為一個周期內(nèi)總的可運行時間(以毫秒計算), cpu.cfs_period_us為一個周期的長度(以毫秒計算), cpu.stat為調(diào)節(jié)策略. 默認值cpu.cfs_period_us=1000ms, cpu.cfs_period_us=-1. -1表明進程組沒有帶寬限制, 向其寫任何合法值將開啟帶寬限制, 最小的限制為1ms, 最大的限制為1s, 向其寫任何負數(shù)將取消帶寬限制并將進程組恢復到無約束狀態(tài).
可以通過/proc/sys/kernel/sched_cfs_bandwidth_slice_us(默認5ms)獲取調(diào)度時間片長度.
進程組的帶寬策略可通過cpu.stat的3個成員獲取: nr_periods nr_throttled throttled_time.
存在兩種情況導致進程被節(jié)制獲取CPU: a. 它完全耗盡一個周期中的占比 b. 它的父進程完全耗盡一個周期中的占比. 出現(xiàn)情況b時, 盡管子進程存在運行時間但它仍不能獲取CPU直到它的父親的運行時間刷新.
3. sched-design-CFS.txt
CFS即completely fair scheduler, 自2.6.23后引入, 用于替換之前的SCHED_OTHER代碼. CFS設計目的是基于真實的硬件建立理想的, 精確的多任務CPU模型. 理想的多任務CPU即可以精確的按相同速度執(zhí)行每一個任務, 比如在兩個任務的CPU上每個任務可以獲取一半性能. 真實的硬件中我們同時僅能運行一個任務, 所以我們引入虛擬運行時間的概念. 任務的虛擬運行時間表明在理想的多任務CPU上任務下一次執(zhí)行時間, 實際應用中任務的虛擬運行時間即其真實的運行時間.
CFS中的虛擬運行時間通過跟蹤每個task的p->se.vruntime值, 借此它可以精確衡量每個task的期望CPU時間. CFS選擇task的邏輯是基于p->se.vruntime值: 它總是嘗試運行擁有最小值的task.
CFS設計上不使用傳統(tǒng)的runqueue, 而是使用基于時間的紅黑樹建立一個未來任務執(zhí)行的時間線. 同時它也維護rq->cfs.min_vruntime值, 該值是單調(diào)增長的值, 用來跟蹤runqueue中最小的vruntime. runqueue中所有運行進程的總數(shù)通過rq->cgs.load值統(tǒng)計, 它是該runqueue上所有排隊的task的權(quán)重的綜合. CFS維護這一個時間排序的紅黑樹, 樹上所有可運行進程都以p->se.vruntime為鍵值排序, CFS選擇最左的task執(zhí)行. 隨著系統(tǒng)持續(xù)運行, 執(zhí)行過的task被放到樹的右側(cè), 這給所有task一個機會成為最左的task并獲取CPU時間. 總結(jié)CFS工作流程: 當一個運行的進程執(zhí)行調(diào)度或因時間片到達而被調(diào)度, 該任務的CPU使用值(p->se.vruntime)將加上它剛剛在CPU上消耗的時間. 當p->se.vruntime足夠高到另一個任務成為最左子樹的任務時(再加上一小段緩沖以保證不會發(fā)生頻繁的來回調(diào)度), 那么新的最左子樹的任務被選中.
CFS使用ns來計算, 它不依賴jiffies或HZ. 因此CFS沒有其它調(diào)度中有的時間片的概念. CFS只有一個可調(diào)整參數(shù): /proc/sys/kernel/sched_min_granularity_ns用于調(diào)整工作負載(從桌面模式到服務器模式).
調(diào)度策略:
1. SCHED_NORMAL(傳統(tǒng)叫法SCHED_OTHER)用于普通task.
2. SCHED_BATCH不像通常task一樣經(jīng)常發(fā)生搶占, 因此允許task運行更久, 更好利用cache.
3. SCHED_IDLE比nice值19更弱, 但它不是一個真正的idle時間調(diào)度器, 可以避免優(yōu)先級反轉(zhuǎn)的問題.
SCHED_FIFO/SCHED_RR在sched/rt.c中實現(xiàn)并遵循POSIX規(guī)范.
調(diào)度器類型的實現(xiàn)通過sched_class結(jié)構(gòu), 它包含以下回調(diào):
enqueue_task(): 當task進入可運行狀態(tài)時調(diào)用, 將task放入紅黑樹中并增加nr_running值.
dequeue_task(): 當task不再可運行時調(diào)用, 將task從紅黑樹中移除并減少nr_running值.
yield_task(): 將task移除后再加入紅黑樹.
check_preempt_curr(): 檢測進入可運行狀態(tài)的task是否可搶占當前運行的task.
pick_next_task(): 選擇最合適的task運行.
set_curr_task(): 當task改變調(diào)度器類型或改變?nèi)蝿战M.
task_tick(): 通常由時間函數(shù)調(diào)用, 可能會導致進程切換, 這會導致運行時搶占.
通常情況下調(diào)度器針對單獨task操作, 但也可以針對任務組進行操作.
CONFIG_CGROUP_SCHED允許task分組并將CPU時間公平的分給這些組.
CONFIG_RT_GROUP_SCHED允許分組實時task.
CONFIG_FAIR_GROUP_SCHED允許分組CFS task.
以上選項需要定義CONFIG_CGROUPS, 并使用cgroup創(chuàng)建組進程, 具體見Documentation/cgroups/cgroups.txt.
舉例創(chuàng)建task組:
# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/cpu
# mount -t cgroup -ocpu none /sys/fs/cgroup/cpu
# cd /sys/fs/cgroup/cpu
# mkdir multimedia
# mkdir browser
# echo 2048 > multimedia/cpu.shares
# echo 1024 > browser/cpu.shares
# echo <firefox_pid> > browser/tasks
# echo <movie_player_pid> > multimedia/tasks
5. sched-nice-design.txt
由于舊版調(diào)度器nice值與時間片相關(guān), 而時間片單位由HZ決定, 最小時間片為1/HZ. 在100HZ系統(tǒng)上nice值為19的進程僅占用1個jiffy. 但在1000HZ系統(tǒng)上其僅運行1ms, 導致系統(tǒng)頻繁的調(diào)度. 因此在1000HZ系統(tǒng)上nice值19的進程使用5ms時間片.
7.sched-stats.txt
查看調(diào)度器狀態(tài): cat /proc/schedstat 參數(shù)太多, 不寫了.
查看每個進程調(diào)度狀態(tài): cat /proc/<pid>/schedstat 分別為CPU占用時間, 在runqueue中等待時間, 獲取時間片次數(shù).
?
轉(zhuǎn)載于:https://www.cnblogs.com/Five100Miles/p/8644993.html
總結(jié)
以上是生活随笔為你收集整理的内核进程切换实现分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis Cluster高可用(HA)
- 下一篇: 20165211 2017-2018-2