本文以內核搶占為引子,概述一下 Linux 搶占的圖景。
我盡量避開細節問題和源碼分析。????
什么是內核搶占?
別急,咱們慢慢來。
先理解搶占 (preemption) 這個概念:
involuntarily suspending a running process is called preemption
奪取一個進程的 cpu 使用權的行為就叫做搶占。
根據是否可以支持搶占,多任務操作系統 (multitasking operating system) 分為 2 類:
1、cooperative multitasking os
這種 os,進程會一直運行直到它自愿停下來。這種自愿停止運行自己的行為稱為 yielding。協作式多任務系統,一聽就知道這是一個烏托邦式的系統,只有當所有進程都很 nice 并樂意經常 yielding 時,系統才能正常工作。如果某個進程太傻或者太壞,系統很快就完蛋了。
2、preemptive multitasking os
這種 os,會有一個調度器 (scheduler,其實就是一段用于調度進程的程序),scheduler 決定進程何時停止運行以及新進程何時開始運行。當一個進程的 cpu 使用權被 scheduler 分配給另一個進程時,就稱前一個進程被搶占了。
你可以把 sheduler 想象成非常智能的交警,交警按照一定的交通規則、當前的交通狀況以及車輛的優先級 (救護車之類的),決定了哪些車可以行駛、哪些車要停下來等待。
很明顯,現階段,preemptive os 優于 cooperative os。所以 Linux 被設計成 preemptive。
搶占的核心操作包括 2 個步驟:
1、從用戶態陷入到內核態 (trap kernel),3 個路徑:
a. 系統調用,本質是 soft interrupt,通常就是一條硬件指令 (x86 的 int 0x80)。
b. 硬件中斷,最典型的就是會周期性發生的 timer 中斷,或者其他各種外設中斷.
c. exception,例如 page fault、div 0。
點擊查看大圖
2、陷入到內核態后,在合適的時機下,調用 sheduler 選出一個最重要的進程,如果被選中的不是當前正在運行的進程的話,就會執行 context switch 切換到新的進程。
根據搶占時機點的不同,搶占分為 2 種類型:
1、user preemption
這里的 user 并不是指在 user-space 里進行搶占,而是指在返回 user-space 前進行搶占,具體的:
When returning to user-space from a system call
When returning to user-space from an interrupt handler
即從 system call 和 interrupt handler 返回到 user-space 前進行搶占,這時仍然是在 kernel-space 里,搶占是需要非常高的權限的事情,user-space 沒權利也不應該干這事。
2、kernel preemption
Linux 2.6 之前是不支持內核搶占的。這意味著當處于用戶空間的進程請求內核服務時,在該進程阻塞(進入睡眠)等待某事(通常是 I/O)或系統調用完成之前,不能調度其他進程。支持內核搶占意味著當一個進程在內核里運行時,另一個進程可以搶占第一個進程并被允許運行,即使第一個進程尚未完成其在內核里的工作。
支持內核搶占 vs 不支持內核搶占
舉個例子:
點擊查看大圖
在上圖中,進程 A 已經通過系統調用進入內核,也許是對設備或文件的 write() 調用。內核代表進程 A 執行時,具有更高優先級的進程 B 被中斷喚醒。內核搶占進程 A 并將 CPU 分配給進程 B,即使進程 A 既沒有阻塞也沒有完成其在內核里的工作。
內核搶占的時機:
When an interrupt handler exits, before returning to kernel-space
When kernel code becomes preemptible again
If a task in the kernel explicitly calls schedule()
If a task in the kernel blocks (which results in a call to schedule() )
為什么要引入內核搶占?
根本原因:
trade-offs between latency and throughput
在系統延遲和吞吐量之間進行權衡。
并不是說內核搶占就是絕對的好,使用什么搶占機制最優是跟你的應用場景掛鉤的。如果不是為了滿足用戶,內核其實是完全不想進行進程切換的,因為每一次 context switch,都會有 overhead,這些 overhead 就是對 cpu 的浪費,意味著吞吐量的下降。
但是,如果你想要系統的響應性好一點,就得盡量多的允許搶占的發生,這是 Linux 作為一個通用操作系統所必須支持的。當你的系統做到隨時都可以發生搶占時,系統的響應性就會非常好。
為了讓用戶根據自己的需求進行配置,Linux 提供了 3 種 Preemption Model。
CONFIG_PREEMPT_NONE=y:不允許內核搶占,吞吐量最大的 Model,一般用于 Server 系統。
CONFIG_PREEMPT_VOLUNTARY=y:在一些耗時較長的內核代碼中主動調用cond_resched()讓出CPU,對吞吐量有輕微影響,但是系統響應會稍微快一些。
CONFIG_PREEMPT=y:除了處于持有 spinlock 時的 critical section,其他時候都允許內核搶占,響應速度進一步提升,吞吐量進一步下降,一般用于 Desktop / Embedded 系統。
另外,還有一個沒有合并進主線內核的 Model: CONFIG_PREEMPT_RT,這個模式幾乎將所有的 spinlock 都換成了 preemptable mutex,只剩下一些極其核心的地方仍然用禁止搶占的 spinlock,所以基本可以認為是隨時可被搶占。
搶占前的檢查
這里的檢查是同時針對所有的 preemption 的。如果你理解了前面的 4 種 preempiton model 的話,應該能感覺到其實是不用太嚴格區分 user / kernel preemption,所有搶占的作用和性質都一樣:降低 lantency,完全可以將它們一視同仁。
搶占的發生要同時滿足兩個條件:
需要搶占;
能搶占;
1、是否需要搶占?
判斷是否需要搶占的依據是:thread_info 的成員 flags 是否設置了 TIF_NEED_RESCHED 標志位。
相關的 API:
set_tsk_need_resched() 用于設置該 flag。
tif_need_resched() 被用來判斷該 flag 是否置位。
resched_curr(struct rq *rq),標記當前 runqueue 需要搶占。
2、是否能搶占?
搶占發生的前提是要確保此次搶占是安全的 (preempt-safe)。什么才是 preempt-safe:不產生 race condition / deadlock。
值得注意的是,只有 kernel preemption 才有被禁止的可能,而 user preemption 總是被允許,因此這時馬上就要返回 user space 了,肯定是處于一個可搶占的狀態了。
在引入內核搶占機制的同時引入了為 thread_info 添加了新的成員:preempt_count ,用來保證搶占的安全性,獲取鎖時會增加 preempt_count,釋放鎖時則會減少。搶占前會檢查 preempt_count 是否為 0,為 0 才允許搶占。
相關的 API:
preempt_enable(),使能內核搶占,可嵌套調用。
preempt_disable(),關閉內核搶占,可嵌套調用。
preempt_count(),返回 preempt_count。
什么場景會設置需要搶占 (TIF_NEED_RESCHED = 1)
通過 grep resched_curr 可以找出大多數標記搶占的場景。
下面列舉的是幾個我比較關心的場景。
1、周期性的時鐘中斷
時鐘中斷處理函數會調用 scheduler_tick(),它通過調度類(scheduling class) 的 task_tick 方法 檢查進程的時間片是否耗盡,如果耗盡則標記需要搶占:
//kernel/sched/core.c voidscheduler_tick(void) { [...] curr->sched_class->task_tick(rq,curr,0); [...] }
Linux 的調度策略被封裝成調度類,例如 CFS、Real-Time。CFS 調度類的 task_tick() 如下:
//kernel/sched/fair.c task_tick_fair() ->entity_tick() ->resched_curr(rq_of(cfs_rq));
2、喚醒進程的時候
當進程被喚醒的時候,如果優先級高于 CPU 上的當前進程,就會觸發搶占。相應的內核代碼中,try_to_wake_up() 最終通過 check_preempt_curr() 檢查是否標記需要搶占:
//kernel/sched/core.c voidcheck_preempt_curr(structrq*rq,structtask_struct*p,intflags) { conststructsched_class*class; if(p->sched_class==rq->curr->sched_class){ rq->curr->sched_class->check_preempt_curr(rq,p,flags); }else{ for_each_class(class){ if(class==rq->curr->sched_class) break; if(class==p->sched_class){ resched_curr(rq); break; } } } [...] }
參數 "p" 指向被喚醒進程,"rq" 代表搶占的 CPU。如果 p 的調度類和 rq 當前的調度類相同,則調用 rq 當前的調度類的 check_preempt_curr() (例如 cfs 的 check_preempt_wakeup()) 來判斷是否要標記需要搶占。
如果 p 的調度類 > rq 當前的調度類,則用 resched_curr() 標記需要搶占,反之,則不標記。
3、新進程創建的時候
如果新進程的優先級高于 CPU 上的當前進程,會需要觸發搶占。相應的代碼是 sched_fork(),它再通過調度類的 task_fork() 標記需要搶占:
//kernel/sched/core.c intsched_fork(unsignedlongclone_flags,structtask_struct*p) { [...] if(p->sched_class->task_fork) p->sched_class->task_fork(p); [...] } //kernel/sched/fair.c staticvoidtask_fork_fair(structtask_struct*p) { [...] if(sysctl_sched_child_runs_first&&curr&&entity_before(curr,se)){ resched_curr(rq); } [...] }
4、進程修改 nice 值的時候
如果修改進程 nice 值導致優先級高于 CPU 上的當前進程,也要標記需要搶占,代碼見 set_user_nice()。
//kernel/sched/core.c voidset_user_nice(structtask_struct*p,longnice) { [...] //Ifthetaskincreaseditspriorityorisrunningandlowereditspriority,thenrescheduleitsCPU if(delta0?||?(delta?>0&&task_running(rq,p))) resched_curr(rq); }
還有很多場景,這里就不一一列舉了。
什么場景下要禁止內核搶占 (preempt_count > 0)
有幾種場景是明確需要關閉內核搶占的。
1、訪問 Per-CPU data structures 的時候
看下面這個例子:
structthis_needs_lockingtux[NR_CPUS]; tux[smp_processor_id()]=some_value; /*taskispreemptedhere...*/ something=tux[smp_processor_id()];
如果搶占發生在注釋所在的那一行,當進程再次被調度時,smp_processor_id() 值可能已經發生變化了,這種場景下需要通過禁止內核搶占來做到 preempt safe。
2、訪問 CPU state 的時候
這個很好理解,你正在操作 CPU 相關的寄存器以進行 context switch 時,肯定是不能再允許搶占。
asmlinkage__visiblevoid__schedschedule(void) { structtask_struct*tsk=current; sched_submit_work(tsk); do{ //調度前禁止內核搶占 preempt_disable(); __schedule(false); sched_preempt_enable_no_resched(); }while(need_resched()); sched_update_worker(tsk); }
3、持有 spinlock 的時候
支持內核搶占,這意味著進程有可能與被搶占的進程在相同的 critical section 中運行。為防止這種情況,當持有自旋鎖時,要禁止內核搶占。
staticinlinevoid__raw_spin_lock(raw_spinlock_t*lock) { preempt_disable(); spin_acquire(&lock->dep_map,0,0,_RET_IP_); LOCK_CONTENDED(lock,do_raw_spin_trylock,do_raw_spin_lock); }
還有很多場景,這里就不一一列舉了。
真正執行搶占的地方
這部分是 platform 相關的,下面以 ARM64 Linux-5.4 為例,快速看下執行搶占的具體代碼。
執行 user preemption
系統調用和中斷返回用戶空間的時候:
它們都是在 ret_to_user() 里判斷是否執行用戶搶占。
//arch/arm64/kernel/entry.S ret_to_user()//返回到用戶空間 work_pending() do_notify_resume() schedule() //arch/arm64/kernel/signal.c asmlinkagevoiddo_notify_resume(structpt_regs*regs, unsignedlongthread_flags) { do{ [...] //檢查是否要需要調度 if(thread_flags&_TIF_NEED_RESCHED){ local_daif_restore(DAIF_PROCCTX_NOIRQ); schedule(); }else{ [...] }while(thread_flags&_TIF_WORK_MASK); }
執行 kernel preemption
中斷返回內核空間的時候:
//arch/arm64/kernel/entry.S el1_irq irq_handler arm64_preempt_schedule_irq preempt_schedule_irq __schedule(true) //kernel/sched/core.c /*Thisistheentrypointtoschedule()fromkernelpreemption*/ asmlinkage__visiblevoid__schedpreempt_schedule_irq(void) { [...] do{ preempt_disable(); local_irq_enable(); __schedule(true); local_irq_disable(); sched_preempt_enable_no_resched(); }while(need_resched()); exception_exit(prev_state); }
內核恢復為可搶占的時候:
前面列舉了集中關閉搶占的場景,當離開這些場景時,會恢復內核搶占。
例如 spinlock unlock 時:
staticinlinevoid__raw_spin_unlock(raw_spinlock_t*lock) { spin_release(&lock->dep_map,1,_RET_IP_); do_raw_spin_unlock(lock); preempt_enable();//使能搶占時,如果需要,就會執行搶占 } //include/linux/preempt.h #definepreempt_enable() do{ barrier(); if(unlikely(preempt_count_dec_and_test())) __preempt_schedule(); }while(0)
內核顯式地要求調度的時候:
內核里有大量的地方會顯式地要求進行調度,最常見的是:cond_resched() 和 sleep()類函數,它們最終都會調用到 __schedule()。
內核阻塞的時候:
例如 mutex,sem,waitqueue 獲取不到資源,或者是等待 IO。這種情況下進程會將自己的狀態從TASK_RUNNING 修改為 TASK_INTERRUPTIBLE,然后調用 schedule() 主動讓出 CPU 并等待喚醒。
//block/blk-core.c staticstructrequest*get_request(structrequest_queue*q,intop, intop_flags,structbio*bio, gfp_tgfp_mask) { [...] prepare_to_wait_exclusive(&rl->wait[is_sync],&wait, TASK_UNINTERRUPTIBLE); io_schedule();//會調用schedule(); [...] }
審核編輯:劉清
-
寄存器
+關注
關注
31文章
5357瀏覽量
120681 -
LINUX內核
+關注
關注
1文章
316瀏覽量
21675 -
CFS
+關注
關注
0文章
7瀏覽量
9058 -
調度器
+關注
關注
0文章
98瀏覽量
5261
原文標題:內核搶占,讓世界變得更美好 | Linux 內核
文章出處:【微信號:嵌入式悅翔園,微信公眾號:嵌入式悅翔園】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論