日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

内核进程切换实现分析

發(fā)布時間:2024/1/17 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 内核进程切换实现分析 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

當我們在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)用或異常返回到用戶空間時, 從異常處理程序返回到用戶空間時.

1 static void __sched __schedule(void) 2 { 3 ??? struct task_struct *prev, *next; 4 ??? unsigned long *switch_count; 5 ??? struct rq *rq; 6 ??? int cpu; 7 need_resched: 8 ??? //關(guān)閉搶占, current_thread_info->preempt_count-- 9 ??? preempt_disable(); 10 ??? //獲取線程所在cpu 11 ??? cpu = smp_processor_id(); 12 ??? rq = cpu_rq(cpu); 13 ??? rcu_note_context_switch(cpu); 14 ??? prev = rq->curr; 15 ??? schedule_debug(prev); 16 ??? if (sched_feat(HRTICK)) 17 ??????? hrtick_clear(rq); 18 ??? raw_spin_lock_irq(&rq->lock); 19 ??? switch_count = &prev->nivcsw; 20 ??? //如果進程停止且支持搶占 21 ??? if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { 22 ??????? //判斷是否有信號掛起 23 ??????? if (unlikely(signal_pending_state(prev->state, prev))) { 24 ??????????? prev->state = TASK_RUNNING; 25 ??????? } else { 26 ??????????? //沒有信號掛起則將當前進程踢出運行隊列 27 ??????????? deactivate_task(rq, prev, DEQUEUE_SLEEP); 28 ??????????? prev->on_rq = 0; 29 ??????????? //如果該進程屬于一個工作隊列, 判斷是否需要喚醒另一個進程 30 ??????????? if (prev->flags & PF_WQ_WORKER) { 31 ??????????????? struct task_struct *to_wakeup; 32 ??????????????? to_wakeup = wq_worker_sleeping(prev, cpu); 33 ??????????????? if (to_wakeup) 34 ??????????????????? try_to_wake_up_local(to_wakeup); 35 ??????????? } 36 ??????? } 37 ??????? switch_count = &prev->nvcsw; 38 ??? } 39 ??? //sched_class.pre_schedule 40 ??? pre_schedule(rq, prev); 41 ??? if (unlikely(!rq->nr_running)) 42 ??????? idle_balance(cpu, rq); 43 ??? //sched_class.put_prev_task 44 ??? put_prev_task(rq, prev); 45 ??? //sched_class.pick_next_task 46 ??? next = pick_next_task(rq); 47 ??? //清除需要調(diào)度標記 48 ??? clear_tsk_need_resched(prev); 49 ??? rq->skip_clock_update = 0; 50 ??? //如果next不等于prev即調(diào)度到其它進程 51 ??? if (likely(prev != next)) { 52 ??????? rq->nr_switches++; 53 ??????? rq->curr = next; 54 ??????? ++*switch_count; 55 ??????? //進程上下文切換, 過程中會解鎖rq->lock 56 ??????? context_switch(rq, prev, next); 57 ??????? /* 58 ???????? * The context switch have flipped the stack from under us 59 ???????? * and restored the local variables which were saved when 60 ???????? * this task called schedule() in the past. prev == current 61 ???????? * is still correct, but it can be moved to another cpu/rq. 62 ???????? */ 63 ??????? cpu = smp_processor_id(); 64 ??????? rq = cpu_rq(cpu); 65 ??? } else 66 ??????? raw_spin_unlock_irq(&rq->lock); 67 ??? post_schedule(rq); 68 ??? //恢復搶占, current_thread_info->preempt_count++ 69 ??? sched_preempt_enable_no_resched(); 70 ??? //如線程標記位TIF_NEED_RESCHED仍存在繼續(xù)調(diào)度 71 ??? if (need_resched()) 72 ??????? goto need_resched; 73 }

?

__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)核空間無需切換, 用戶進程需切換), 恢復寄存器與堆棧等.

1 static inline void context_switch(struct rq *rq, \ 2 ??? struct task_struct *prev, struct task_struct *next) 3 { 4 ??? struct mm_struct *mm, *oldmm; 5 ??? //準備進程切換 6 ??? //sched_info_switch記錄調(diào)度前后兩個進程在調(diào)度時刻的時間信息 7 ??? //prepare_lock_switch將調(diào)度后進程next->on_cpu置位(對于SMP芯片) 8 ??? prepare_task_switch(rq, prev, next); 9 ??? mm = next->mm; 10 ??? oldmm = prev->active_mm; 11 ??? arch_start_context_switch(prev); 12 ??? /** 13 ???? * 切換內(nèi)存空間, mm為調(diào)度后進程的內(nèi)存空間, oldmm為調(diào)度前進程正在使用的內(nèi)存空間 14 ???? * 對于內(nèi)核線程沒有私有內(nèi)存空間, mm為空, 此時無需切換內(nèi)存空間 15 ???? * 所以復用之前的mm, 同時tlb進入lazy mode, 如果為用戶進程(mm非空)則需刷新mm 16 ???? * 17 ??? **/ 18 ??? if (!mm) { 19 ??????? next->active_mm = oldmm; 20 ??????? atomic_inc(&oldmm->mm_count); 21 ??????? enter_lazy_tlb(oldmm, next); 22 ??? } else 23 ??????? switch_mm(oldmm, mm, next); 24 ??? /** 25 ???? * 內(nèi)核線程沒有自己的內(nèi)存空間, 需要引用其它進程的內(nèi)存空間 26 ???? * 但當該線程還在運行時不可能自己減少對該mm的引用 27 ???? * 因此需要緩存該mm, 在調(diào)度到新進程后再減少引用 28 ???? * 這就是rq->prev_mm作用, 其減少見finish_task_switch() 29 ???? * 30 ??? **/ 31 ??? if (!prev->mm) { 32 ??????? prev->active_mm = NULL; 33 ??????? rq->prev_mm = oldmm; 34 ??? } 35 ??? /** 36 ???? * 釋放運行隊列鎖 37 ???? * 通常釋放鎖操作在switch_to()之后, 但在部分平臺上需要解鎖執(zhí)行上下文切換 38 ???? * 此時就需要定義__ARCH_WANT_UNLOCKED_CTXSW, 具體參見scheduler/sched-arch.txt 39 ???? * 40 ??? **/ 41 #ifndef __ARCH_WANT_UNLOCKED_CTXSW 42 ??? spin_release(&rq->lock.dep_map, 1, _THIS_IP_); 43 #endif 44 ??? context_tracking_task_switch(prev, next); 45 ??? //切換寄存器狀態(tài)與棧, 實際調(diào)用__switch_to(defined in arch/arm/kernel/entry-armv.S) 46 ??? switch_to(prev, next, prev); 47 ??? barrier(); 48 ??? //當前運行的cpu可能不是之前的cpu, 所以需要重新獲取當前的runqueue 49 ??? finish_task_switch(this_rq(), prev); 50 }

?

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()完成上下文切換.

1 static void finish_task_switch(struct rq *rq, \ 2 ??? struct task_struct *prev) __releases(rq->lock) 3 { 4 ??? struct mm_struct *mm = rq->prev_mm; 5 ??? long prev_state; 6 ??? //清空prev_mm 7 ??? rq->prev_mm = NULL; 8 ??? /** 9 ???? * 源碼注釋已經(jīng)指出進程在退出時會最后一次調(diào)度schedule(), 調(diào)度不會返回 10 ???? * 測試進程狀態(tài)是否為TASK_DEAD必須在獲取運行隊列鎖時 11 ???? * 否則退出的進程可能被調(diào)度到另一CPU上, 在那個CPU上退出導致兩次減少引用計數(shù) 12 ???? * 13 ??? **/ 14 ??? prev_state = prev->state; 15 ??? vtime_task_switch(prev); 16 ??? finish_arch_switch(prev); 17 ??? perf_event_task_sched_in(prev, current); 18 ??? //finish_lock_switch()中執(zhí)行解鎖 19 ??? //注意此處解鎖不一定與__schedule()中為同一把鎖, 因為此時可能切換了CPU 20 ??? finish_lock_switch(rq, prev); 21 ??? //如果之前切換內(nèi)存空間時處于禁止中斷狀態(tài)則推遲到此處切換內(nèi)存空間 22 ??? finish_arch_post_lock_switch(); 23 ??? fire_sched_in_preempt_notifiers(current); 24 ??? //注意此處drop的是rq->prev_mm, rq->prev_mm是在__schedule()中被賦值為prev->active_mm 25 ??? //如此保證了盡管內(nèi)核線程沒有內(nèi)存空間, 但仍能正常使用mm 26 ??? if (mm) 27 ??????? mmdrop(mm); 28 ??? //釋放task_struct 29 ??? if (unlikely(prev_state == TASK_DEAD)) { 30 ??????? kprobe_flush_task(prev); 31 ??????? put_task_struct(prev); 32 ??? } 33 ??? tick_nohz_task_switch(current); 34 }

?

至此完成進程上下文切換, 重新回到__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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。