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