0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

概述一下Linux內(nèi)核搶占的圖景

嵌入式悅翔園 ? 來源:老吳嵌入式 ? 2023-04-18 11:50 ? 次閱讀

本文以內(nèi)核搶占為引子,概述一下 Linux 搶占的圖景。

我盡量避開細節(jié)問題和源碼分析。????

什么是內(nèi)核搶占?

別急,咱們慢慢來。

先理解搶占 (preemption) 這個概念

involuntarily suspending a running process is called preemption

奪取一個進程的 cpu 使用權(quán)的行為就叫做搶占。

根據(jù)是否可以支持搶占,多任務操作系統(tǒng) (multitasking operating system) 分為 2 類

1、cooperative multitasking os

這種 os,進程會一直運行直到它自愿停下來。這種自愿停止運行自己的行為稱為 yielding。協(xié)作式多任務系統(tǒng),一聽就知道這是一個烏托邦式的系統(tǒng),只有當所有進程都很 nice 并樂意經(jīng)常 yielding 時,系統(tǒng)才能正常工作。如果某個進程太傻或者太壞,系統(tǒng)很快就完蛋了。

2、preemptive multitasking os

這種 os,會有一個調(diào)度器 (scheduler,其實就是一段用于調(diào)度進程的程序),scheduler 決定進程何時停止運行以及新進程何時開始運行。當一個進程的 cpu 使用權(quán)被 scheduler 分配給另一個進程時,就稱前一個進程被搶占了。

你可以把 sheduler 想象成非常智能的交警,交警按照一定的交通規(guī)則、當前的交通狀況以及車輛的優(yōu)先級 (救護車之類的),決定了哪些車可以行駛、哪些車要停下來等待。

很明顯,現(xiàn)階段,preemptive os 優(yōu)于 cooperative os。所以 Linux 被設計成 preemptive。

搶占的核心操作包括 2 個步驟

1、從用戶態(tài)陷入到內(nèi)核態(tài) (trap kernel),3 個路徑:

a. 系統(tǒng)調(diào)用,本質(zhì)是 soft interrupt,通常就是一條硬件指令 (x86 的 int 0x80)。

b. 硬件中斷,最典型的就是會周期性發(fā)生的 timer 中斷,或者其他各種外設中斷.

c. exception,例如 page fault、div 0。

37d7ec96-dd9b-11ed-bfe3-dac502259ad0.jpg

點擊查看大圖

2、陷入到內(nèi)核態(tài)后,在合適的時機下,調(diào)用 sheduler 選出一個最重要的進程,如果被選中的不是當前正在運行的進程的話,就會執(zhí)行 context switch 切換到新的進程。

根據(jù)搶占時機點的不同,搶占分為 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 里,搶占是需要非常高的權(quán)限的事情,user-space 沒權(quán)利也不應該干這事。

2、kernel preemption

Linux 2.6 之前是不支持內(nèi)核搶占的。這意味著當處于用戶空間的進程請求內(nèi)核服務時,在該進程阻塞(進入睡眠)等待某事(通常是 I/O)或系統(tǒng)調(diào)用完成之前,不能調(diào)度其他進程。支持內(nèi)核搶占意味著當一個進程在內(nèi)核里運行時,另一個進程可以搶占第一個進程并被允許運行,即使第一個進程尚未完成其在內(nèi)核里的工作。

37f98ca2-dd9b-11ed-bfe3-dac502259ad0.png

支持內(nèi)核搶占 vs 不支持內(nèi)核搶占

舉個例子:

3822e3f4-dd9b-11ed-bfe3-dac502259ad0.png

點擊查看大圖

在上圖中,進程 A 已經(jīng)通過系統(tǒng)調(diào)用進入內(nèi)核,也許是對設備或文件的 write() 調(diào)用。內(nèi)核代表進程 A 執(zhí)行時,具有更高優(yōu)先級的進程 B 被中斷喚醒。內(nèi)核搶占進程 A 并將 CPU 分配給進程 B,即使進程 A 既沒有阻塞也沒有完成其在內(nèi)核里的工作。

內(nèi)核搶占的時機:

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)延遲和吞吐量之間進行權(quán)衡。

并不是說內(nèi)核搶占就是絕對的好,使用什么搶占機制最優(yōu)是跟你的應用場景掛鉤的。如果不是為了滿足用戶,內(nèi)核其實是完全不想進行進程切換的,因為每一次 context switch,都會有 overhead,這些 overhead 就是對 cpu 的浪費,意味著吞吐量的下降。

但是,如果你想要系統(tǒng)的響應性好一點,就得盡量多的允許搶占的發(fā)生,這是 Linux 作為一個通用操作系統(tǒng)所必須支持的。當你的系統(tǒng)做到隨時都可以發(fā)生搶占時,系統(tǒng)的響應性就會非常好。

為了讓用戶根據(jù)自己的需求進行配置,Linux 提供了 3 種 Preemption Model。

383a799c-dd9b-11ed-bfe3-dac502259ad0.png

CONFIG_PREEMPT_NONE=y:不允許內(nèi)核搶占,吞吐量最大的 Model,一般用于 Server 系統(tǒng)。

3850f6f4-dd9b-11ed-bfe3-dac502259ad0.png

CONFIG_PREEMPT_VOLUNTARY=y:在一些耗時較長的內(nèi)核代碼中主動調(diào)用cond_resched()讓出CPU,對吞吐量有輕微影響,但是系統(tǒng)響應會稍微快一些。

3873f58c-dd9b-11ed-bfe3-dac502259ad0.png

CONFIG_PREEMPT=y:除了處于持有 spinlock 時的 critical section,其他時候都允許內(nèi)核搶占,響應速度進一步提升,吞吐量進一步下降,一般用于 Desktop / Embedded 系統(tǒng)。

38900ea2-dd9b-11ed-bfe3-dac502259ad0.png

另外,還有一個沒有合并進主線內(nèi)核的 Model: CONFIG_PREEMPT_RT,這個模式幾乎將所有的 spinlock 都換成了 preemptable mutex,只剩下一些極其核心的地方仍然用禁止搶占的 spinlock,所以基本可以認為是隨時可被搶占。

38b0a8c4-dd9b-11ed-bfe3-dac502259ad0.png

搶占前的檢查

這里的檢查是同時針對所有的 preemption 的。如果你理解了前面的 4 種 preempiton model 的話,應該能感覺到其實是不用太嚴格區(qū)分 user / kernel preemption,所有搶占的作用和性質(zhì)都一樣:降低 lantency,完全可以將它們一視同仁。

搶占的發(fā)生要同時滿足兩個條件

需要搶占;

能搶占;

1、是否需要搶占?

判斷是否需要搶占的依據(jù)是:thread_info 的成員 flags 是否設置了 TIF_NEED_RESCHED 標志位。

相關(guān)的 API:

set_tsk_need_resched() 用于設置該 flag。

tif_need_resched() 被用來判斷該 flag 是否置位。

resched_curr(struct rq *rq),標記當前 runqueue 需要搶占。

2、是否能搶占?

搶占發(fā)生的前提是要確保此次搶占是安全的 (preempt-safe)。什么才是 preempt-safe:不產(chǎn)生 race condition / deadlock。

值得注意的是,只有 kernel preemption 才有被禁止的可能,而 user preemption 總是被允許,因此這時馬上就要返回 user space 了,肯定是處于一個可搶占的狀態(tài)了。

在引入內(nèi)核搶占機制的同時引入了為 thread_info 添加了新的成員:preempt_count ,用來保證搶占的安全性,獲取鎖時會增加 preempt_count,釋放鎖時則會減少。搶占前會檢查 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。

什么場景會設置需要搶占 (TIF_NEED_RESCHED = 1)

通過 grep resched_curr 可以找出大多數(shù)標記搶占的場景。

下面列舉的是幾個我比較關(guān)心的場景。

1、周期性的時鐘中斷

時鐘中斷處理函數(shù)會調(diào)用 scheduler_tick(),它通過調(diào)度類(scheduling class) 的 task_tick 方法 檢查進程的時間片是否耗盡,如果耗盡則標記需要搶占:

//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、喚醒進程的時候

當進程被喚醒的時候,如果優(yōu)先級高于 CPU 上的當前進程,就會觸發(fā)搶占。相應的內(nèi)核代碼中,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;
}
}
}
[...]
}

參數(shù) "p" 指向被喚醒進程,"rq" 代表搶占的 CPU。如果 p 的調(diào)度類和 rq 當前的調(diào)度類相同,則調(diào)用 rq 當前的調(diào)度類的 check_preempt_curr() (例如 cfs 的 check_preempt_wakeup()) 來判斷是否要標記需要搶占。

如果 p 的調(diào)度類 > rq 當前的調(diào)度類,則用 resched_curr() 標記需要搶占,反之,則不標記。

3、新進程創(chuàng)建的時候

如果新進程的優(yōu)先級高于 CPU 上的當前進程,會需要觸發(fā)搶占。相應的代碼是 sched_fork(),它再通過調(diào)度類的 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 值導致優(yōu)先級高于 CPU 上的當前進程,也要標記需要搶占,代碼見 set_user_nice()。

//kernel/sched/core.c
voidset_user_nice(structtask_struct*p,longnice)
{
[...]
//Ifthetaskincreaseditspriorityorisrunningandlowereditspriority,thenrescheduleitsCPU
if(delta0&&task_running(rq,p)))
resched_curr(rq);
}

還有很多場景,這里就不一一列舉了。

什么場景下要禁止內(nèi)核搶占 (preempt_count > 0)

有幾種場景是明確需要關(guān)閉內(nèi)核搶占的。

1、訪問 Per-CPU data structures 的時候

看下面這個例子:

structthis_needs_lockingtux[NR_CPUS];
tux[smp_processor_id()]=some_value;
/*taskispreemptedhere...*/
something=tux[smp_processor_id()];

如果搶占發(fā)生在注釋所在的那一行,當進程再次被調(diào)度時,smp_processor_id() 值可能已經(jīng)發(fā)生變化了,這種場景下需要通過禁止內(nèi)核搶占來做到 preempt safe。

2、訪問 CPU state 的時候

這個很好理解,你正在操作 CPU 相關(guān)的寄存器以進行 context switch 時,肯定是不能再允許搶占。

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 的時候

支持內(nèi)核搶占,這意味著進程有可能與被搶占的進程在相同的 critical section 中運行。為防止這種情況,當持有自旋鎖時,要禁止內(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)用和中斷返回用戶空間的時候:

它們都是在 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)核空間的時候:

//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)核恢復為可搶占的時候:

前面列舉了集中關(guān)閉搶占的場景,當離開這些場景時,會恢復內(nèi)核搶占。

例如 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();//使能搶占時,如果需要,就會執(zhí)行搶占
}

//include/linux/preempt.h
#definepreempt_enable()
do{
barrier();
if(unlikely(preempt_count_dec_and_test()))
__preempt_schedule();
}while(0)

內(nèi)核顯式地要求調(diào)度的時候:

內(nèi)核里有大量的地方會顯式地要求進行調(diào)度,最常見的是:cond_resched() 和 sleep()類函數(shù),它們最終都會調(diào)用到 __schedule()。

內(nèi)核阻塞的時候:

例如 mutex,sem,waitqueue 獲取不到資源,或者是等待 IO。這種情況下進程會將自己的狀態(tài)從TASK_RUNNING 修改為 TASK_INTERRUPTIBLE,然后調(diào)用 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();//會調(diào)用schedule();
[...]
}






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5294

    瀏覽量

    119814
  • LINUX內(nèi)核
    +關(guān)注

    關(guān)注

    1

    文章

    316

    瀏覽量

    21608
  • CFS
    CFS
    +關(guān)注

    關(guān)注

    0

    文章

    7

    瀏覽量

    9044
  • 調(diào)度器
    +關(guān)注

    關(guān)注

    0

    文章

    98

    瀏覽量

    5232

原文標題:內(nèi)核搶占,讓世界變得更美好 | Linux 內(nèi)核

文章出處:【微信號:嵌入式悅翔園,微信公眾號:嵌入式悅翔園】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    文搞懂Linux內(nèi)核鏈表

    hello 大家好,今天給大家介紹一下linux 內(nèi)核鏈表的分析,在寫這篇文章前,筆者自己以前也只是停留在應用層面,沒有深究其中的細節(jié),很多也是理解的不是很透徹。寫完此文后,發(fā)現(xiàn)對鏈表的理解更加深刻了。很多現(xiàn)代計算機的思想在
    發(fā)表于 11-14 09:17 ?1051次閱讀

    Linux內(nèi)核搶占和用戶搶占的概念和區(qū)別

    本文詳解了Linux內(nèi)核搶占實現(xiàn)機制。首先介紹了內(nèi)核搶占和用戶搶占的概念和區(qū)別,接著分析了不可
    發(fā)表于 08-05 08:18

    詳解Linux內(nèi)核搶占實現(xiàn)機制

    本文詳解了Linux內(nèi)核搶占實現(xiàn)機制。首先介紹了內(nèi)核搶占和用戶搶占的概念和區(qū)別,接著分析了不可
    發(fā)表于 08-06 06:16

    linux 5.4.31為例來介紹一下linux內(nèi)核目錄結(jié)構(gòu)

    ,它是Linux內(nèi)核概述和編譯命令說明。readme的說明更加針對X86等通用的平臺,對于某些特殊的體系結(jié)構(gòu),可能有些特殊的地方。內(nèi)核源碼很復雜,包含多級目錄,形成
    發(fā)表于 02-16 07:30

    記錄一下Linux設備模型學習歷程

    Linux設備模型學習筆記1KobjectKobject, Kset和KtypeUeventsysfs文件系統(tǒng)wowo這里寫的很好了:內(nèi)核等看wowo寫的很有幫助我寫一下我的理解。記錄一下
    發(fā)表于 02-17 06:05

    介紹一下Linux內(nèi)核編譯和更新的操作流程

    。由于官方?jīng)]有提高最新Linux內(nèi)核版本的燒寫固件,為了解決些比較嚴重的bug,需要自行編譯Linux內(nèi)核進行更新,接下來就介紹
    發(fā)表于 06-21 09:58

    如何自行編譯Linux內(nèi)核的詳細資料概述

    ,你能很輕易地安裝實驗版本的或者指定版本的內(nèi)核(比如針對音頻產(chǎn)品的實時內(nèi)核)。 考慮一下,既然升級內(nèi)核如此容易,為什么你不愿意自行編譯
    的頭像 發(fā)表于 05-27 10:12 ?3180次閱讀
    如何自行編譯<b class='flag-5'>一</b>個<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>的詳細資料<b class='flag-5'>概述</b>

    Linux內(nèi)核搶占相關(guān)的基礎(chǔ)知識

    今天要分享的是搶占相關(guān)的基礎(chǔ)知識。本文以內(nèi)核搶占為引子,概述一下 Linux
    的頭像 發(fā)表于 11-09 16:48 ?2002次閱讀

    STM32MP157 Linux系統(tǒng)移植開發(fā)篇7:Linux內(nèi)核目錄結(jié)構(gòu)詳解

    ,它是Linux內(nèi)核概述和編譯命令說明。readme的說明更加針對X86等通用的平臺,對于某些特殊的體系結(jié)構(gòu),可能有些特殊的地方。內(nèi)核源碼很復雜,包含多級目錄,形成
    發(fā)表于 12-17 18:29 ?10次下載
    STM32MP157 <b class='flag-5'>Linux</b>系統(tǒng)移植開發(fā)篇7:<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>目錄結(jié)構(gòu)詳解

    小編科普一下Linux內(nèi)核中常用的C語言技巧

    Linux內(nèi)核采用的是GCC編譯器,GCC編譯器除了支持ANSI C,還支持GNU C。在Linux內(nèi)核中,許多地方都使用了GNU C語言的擴展特性,如typeof、__attribu
    的頭像 發(fā)表于 02-08 11:51 ?669次閱讀

    介紹一下linux內(nèi)核比較優(yōu)秀的調(diào)試方式KGDB

    printf相信學過C語言的同志再熟悉不過了,然而在linux內(nèi)核開發(fā)中有種非常簡潔的日志輸出函數(shù)叫-printk。
    的頭像 發(fā)表于 03-08 13:45 ?1762次閱讀

    文搞懂Linux系統(tǒng)內(nèi)核的重要性

    今天我要跟大家分享一下Linux內(nèi)核的重要性。內(nèi)核就像Linux系統(tǒng)運行的大心臟,對系統(tǒng)的運行起到了至關(guān)重要的作用。那么
    的頭像 發(fā)表于 03-24 15:16 ?868次閱讀
    <b class='flag-5'>一</b>文搞懂<b class='flag-5'>Linux</b>系統(tǒng)<b class='flag-5'>內(nèi)核</b>的重要性

    介紹一下Linux內(nèi)核中的各種鎖

    Linux內(nèi)核中有許多不同類型的鎖,它們都可以用來保護關(guān)鍵資源,以避免多個線程或進程之間發(fā)生競爭條件,從而保護系統(tǒng)的穩(wěn)定性和可靠性。
    的頭像 發(fā)表于 05-16 14:13 ?5091次閱讀

    操作系統(tǒng)中搶占式和非搶占內(nèi)核的區(qū)別

    操作系統(tǒng)般分為搶占內(nèi)核和非搶占內(nèi)核,通常RTOS都是搶占
    的頭像 發(fā)表于 05-29 10:47 ?1671次閱讀
    操作系統(tǒng)中<b class='flag-5'>搶占</b>式和非<b class='flag-5'>搶占</b>式<b class='flag-5'>內(nèi)核</b>的區(qū)別

    Linux系統(tǒng)內(nèi)核概述

    Linux 內(nèi)核Linux 操作系統(tǒng)的主要組件,也是計算機硬件與其進程之間的核心接口。它負責兩者之間的通信,還要盡可能高效地管理資源。
    發(fā)表于 06-09 09:29 ?580次閱讀
    <b class='flag-5'>Linux</b>系統(tǒng)<b class='flag-5'>內(nèi)核</b><b class='flag-5'>概述</b>