国产成人精品18p,天天干成人网,无码专区狠狠躁天天躁,美女脱精光隐私扒开免费观看

Linux內核中怎么實(shí)現Percpu變量

發(fā)布時(shí)間:2021-08-11 11:57 來(lái)源:億速云 閱讀:0 作者:Leah 欄目: 系統運維 歡迎投稿:712375056

這篇文章給大家介紹Linux內核中怎么實(shí)現Percpu變量,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

所謂thread  local變量,就是對于同一個(gè)變量,每個(gè)線(xiàn)程都有自己的一份,對該變量的訪(fǎng)問(wèn)是線(xiàn)程隔離的,它們之間不會(huì )相互影響,所以也就不會(huì )有各種多線(xiàn)程問(wèn)題。

正確的使用thread local變量,能極大的簡(jiǎn)化多線(xiàn)程開(kāi)發(fā)。所以不管是c/c++/rust,還是java/c#等,都內置了對thread  local變量的支持。

但你知道嗎,不僅是在編程語(yǔ)言中,在linux內核中,也有一個(gè)類(lèi)似的機制,用來(lái)實(shí)現類(lèi)似的目的,它叫做percpu變量。

percpu變量,顧名思義,就是對于同一個(gè)變量,每個(gè)cpu都有自己的一份,它可以被用來(lái)存放一些cpu獨有的數據,比如cpu的id,cpu上正在運行的線(xiàn)程等等,因該機制可以非常方便的解決一些特定問(wèn)題,所以在內核編程中被廣泛使用。

好奇的你們肯定都在問(wèn),它是怎么實(shí)現的呢?

我們先不管細節,先來(lái)看一張圖,這樣從全局的角度來(lái)了解下它的實(shí)現。

從上圖中我們可以看到,各種源文件中通過(guò)DEFINE_PER_CPU的方式,定義了很多percpu變量,這些變量根據vmlinux.lds.S中的相關(guān)定義,會(huì )被linker聚合在一起,然后放到最終vmlinux文件的,一個(gè)名叫.data..percpu的section里。

這些變量的地址也是被特殊處理過(guò)的,它們從零開(kāi)始依次遞增,這樣一個(gè)變量的地址,就是該變量在整個(gè)vmlinux的.data..percpu區里的位置,有了這個(gè)位置,然后再知道某個(gè)cpu的percpu內存塊的起始地址,就可以很方便的計算出該cpu對應的該變量的運行時(shí)內存地址。

linux內核在啟動(dòng)時(shí),會(huì )先把vmlinux文件加載到內存中,然后根據cpu的個(gè)數,為每個(gè)cpu都分配一塊用于存放percpu變量的內存區域,之后把vmlinux中的.data..percpu  section里的內容,拷貝到各個(gè)cpu的percpu內存塊的static區域里,最后將各percpu內存塊的起始地址放到對應cpu的gs寄存器里。

到這里有關(guān)percpu變量的初始化工作就已經(jīng)結束了。

當我們在訪(fǎng)問(wèn)percpu變量時(shí),只需要將gs寄存器里的地址,加上我們想要訪(fǎng)問(wèn)的percpu變量的地址,就能得到在該cpu上,該percpu變量真實(shí)的內存地址。

有了這個(gè)地址,我們就可以方便的操作這個(gè)percpu變量了。

上圖中重點(diǎn)描述的是那些,在內核編譯期就已經(jīng)確定的percpu變量,這些變量是靜態(tài)的,是不會(huì )隨著(zhù)時(shí)間的推移而動(dòng)態(tài)的增加或減少的,所以它們在內核初始化時(shí),就直接被拷貝到了各個(gè)percpu內存塊的static區。

除了這種靜態(tài)percpu變量,還有另外兩種percpu變量。

其中一種是內核模塊中的靜態(tài)percpu變量,它雖然也是在編譯期就能確定的,但由于內核模塊動(dòng)態(tài)加載的特性,它不是完全靜態(tài)的,內核為這種percpu變量在percpu內存塊中單獨開(kāi)辟了一個(gè)區域,叫reserved區,當內核模塊被加載到內存時(shí),其靜態(tài)percpu變量就會(huì )在這個(gè)區域分配內存。

另外一種percpu變量就是純動(dòng)態(tài)的percpu變量,它是在運行時(shí)動(dòng)態(tài)分配的,它使用的內存是上圖中的dynamic區。

static區的大小是在編譯期就算好的,是固定不變的,reserved區也是固定不變的,但其大小是預估的,dynamic區是可以動(dòng)態(tài)增加的。

雖然這三種percpu變量的分配方式不同,但它們的內在機制本質(zhì)上都是一樣的,所以這里我們只講內核里的靜態(tài)percpu變量,對其他兩種方式感興趣的同學(xué),可以參考內核源碼自己研究下。

下面我們就用一個(gè)具體的例子,來(lái)看下percpu變量到底是怎么實(shí)現的。

上圖中的current表示要獲取當前線(xiàn)程對象,它其實(shí)是一個(gè)宏,具體定義如下:

由上可見(jiàn),current獲取的當前線(xiàn)程對象其實(shí)是一個(gè)名為current_task的percpu變量。

在get_current方法中,通過(guò)this_cpu_read_stable方法,獲取屬于當前cpu的current_task。

this_cpu_read_stable方法其實(shí)也是一個(gè)宏,它全部展開(kāi)后是下面這個(gè)樣子:

在這里,我們先不講宏展開(kāi)后各語(yǔ)句到底是什么意思,我們先跑個(gè)題。

讀過(guò)linux內核源碼的同學(xué)都知道,在linux內核中,宏使用的非常多,且比較復雜,如果我們對自己進(jìn)行宏展開(kāi)的正確性沒(méi)有信心的話(huà),可以使用下面我介紹的這個(gè)方式,使用它,你可以非常容易的得到任意文件宏展開(kāi)后的結果。

我們知道,一個(gè)程序的構建分為預處理、編譯、匯編、鏈接這些階段,而宏展開(kāi)就發(fā)生在預處理階段。

各個(gè)階段在完成后,一般都會(huì )生成一個(gè)臨時(shí)文件給下一階段使用,這些臨時(shí)文件默認是不會(huì )保存到磁盤(pán)上的,但我們可以通過(guò)指定一些參數,告知gcc幫我們保留下來(lái)這些臨時(shí)文件,這樣我們就可以查看各個(gè)階段的生成內容了。

依據該思路,我們只要在編譯比如上面的net/socket.c文件時(shí),加上這些參數,我們就能得到這些臨時(shí)文件,也就可以查看其預處理之后的宏展開(kāi)是什么樣子的了。

但是,如果只是為了查看單個(gè)文件的宏展開(kāi)后結果,就保存下整個(gè)內核中,所有源文件編譯時(shí)的臨時(shí)文件,這是非常耗時(shí)且不劃算的,那有沒(méi)有辦法可以想查看哪個(gè)文件的宏展開(kāi),就單獨編譯一次那個(gè)文件呢?

還真有。

其實(shí)說(shuō)起來(lái)該方法也很簡(jiǎn)單,我們只需要知道編譯某個(gè)文件時(shí)使用的編譯命令是什么,這樣當我們需要查看這個(gè)文件的宏展開(kāi)時(shí),再使用這個(gè)編譯命令,且加上一些特定的參數,再編譯一遍,這樣就能得到該文件編譯過(guò)程中,各階段的臨時(shí)文件了。

那如何找到編譯各個(gè)源文件時(shí)使用的命令呢?

這個(gè)內核其實(shí)已經(jīng)幫我們做好了。

當我們在編譯內核時(shí),內核中每個(gè)文件被編譯時(shí)使用的命令,都會(huì )保存到一個(gè)對應的臨時(shí)文件里,比如上面net/socket.c文件的編譯命令就保存在下面的文件里:

net/socket.c的編譯命令就是上圖中的第一行,從gcc開(kāi)始到該行結束的部分。

這個(gè)編譯命令夠復雜吧,但我們不用管,我們只用知道,使用該命令,就可以將net/socket.c編譯成net/socket.o。

現在我們在該命令的基礎上,加上-save-temps=obj參數,告知gcc在編譯時(shí)保留下各階段的臨時(shí)文件,具體操作流程如下:

由上可見(jiàn),加上-save-temps=obj參數后,該編譯過(guò)程多生成兩個(gè)文件,而net/socket.i就是gcc預處理之后的文件。

打開(kāi)net/socket.i,并找到我們需要的get_current方法:

看上圖中的選中部分,其內容和我們自己宏展開(kāi)后的結果,是完全一樣的。

這個(gè)方法還不錯吧。

當然,我們還可以通過(guò)反編譯的方式,進(jìn)一步確認下宏展開(kāi)后確實(shí)是這樣:

由上可見(jiàn),宏展開(kāi)后其實(shí)主要就是一條mov指令,其中current_task變量地址的值為0x16d00。

該指令的意思是,將gs寄存器里的地址,和current_task的地址相加,然后將相加后地址指向的內存空間里的值,移動(dòng)到rax里。

這個(gè)和我們上面提到的,percpu的實(shí)現機制是一致的。

好,我們回到上文中斷的部分,來(lái)繼續看下get_current方法里宏展開(kāi)后各語(yǔ)句的意思。

上文講到,get_current方法里的this_cpu_read_stable方法宏展開(kāi)后主要是一條asm語(yǔ)句,可能有些同學(xué)對該語(yǔ)句不太熟悉,它其實(shí)并不是c語(yǔ)言標準規范里的語(yǔ)法,而是gcc對c標準的擴展,通過(guò)asm語(yǔ)句,我們可以在c中直接執行匯編指令。

有關(guān)其詳細的語(yǔ)法規則,可以參考以下鏈接:

https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C

不關(guān)心細節的同學(xué)可以不用去看具體語(yǔ)法,我們只要知道該asm語(yǔ)句的意思是,獲取current_task的地址,將該地址與gs段寄存器里的基礎地址值相加,得到一個(gè)最終的地址,然后通過(guò)mov指令,將該最終地址指向的內存的值,放到pfo_val__變量里。

該指令執行完畢后,pfo_val__變量里存放的值,就是當前cpu執行的當前線(xiàn)程對象struct  task_struct的地址,也就是說(shuō),pfo_val__變量為當前正在執行的線(xiàn)程對象的指針。

那為什么通過(guò)這種方式,得到的就是當前cpu正在執行的當前線(xiàn)程對象的指針呢?

這個(gè)其實(shí)上文我們已經(jīng)講過(guò)了,關(guān)鍵點(diǎn)在于gs寄存器中存放的是當前cpu的percpu內存塊的起始地址,而current_task的地址表示的又是,current_task變量在任意percpu內存塊的位置,所以這兩個(gè)地址一相加,得到的自然就是當前cpu的current_task變量的當前值了。

理論上是如此,不過(guò)我們還是通過(guò)源碼角度再看下。

首先我們來(lái)看下current_task變量的定義:

DEFINE_PER_CPU還是一個(gè)宏,其展開(kāi)后如下:

在宏展開(kāi)后的變量定義中,最重要的是指定該變量的section為.data..percpu。

我們再看什么地方使用了這個(gè)section:

由上圖可見(jiàn),PERCPU_INPUT宏里使用了該section,而PERCPU_INPUT宏又被下面的PERCPU_VADDR宏使用。

我們再來(lái)看下PERCPU_VADDR宏在哪里使用:

由上可見(jiàn)PERCPU_VADDR宏又在vmlinux.lds.S文件中使用。

vmlinux.lds.S是一個(gè)鏈接腳本,在鏈接階段,linker會(huì )根據vmlinux.lds.S里的定義,把相同section的內核變量或方法,聚合起來(lái),放到最終輸出文件vmlinux的對應section里。

比如上面的PERCPU_VADDR宏就是說(shuō),把所有源文件中的屬于各種.data..percpu  section的變量提取出來(lái),然后依次放入到輸出文件vmlinux的.data..percpu的section中。

上圖中需要注意的是,在調用PERCPU_VADDR時(shí),傳入的vaddr參數是0,它表示vmlinux中.data..percpu  section里存放的變量地址是從0開(kāi)始,依次遞增的。

這個(gè)我們之前也說(shuō)過(guò),該地址是用來(lái)表示該變量在.data..percpu  section里的位置,也就是說(shuō),該地址表示的是該變量在運行時(shí)的,各cpu的percpu內存塊里的位置。

vmlinux里.data..percpu section存放的變量地址是從0開(kāi)始的,這個(gè)我們可以通過(guò)__per_cpu_start的值得到確認:

另一個(gè)需要注意的是,__per_cpu_load的地址值是正常的內核編譯地址,它用來(lái)指定,當vmlinux被加載到內存后,vmlinux里的.data..percpu  section所處內存的位置:

綜上可知,PERCPU_VADDR宏的作用是,將所有源文件中屬于各個(gè).data..percpu  section的變量聚合起來(lái),然后依次放到輸出文件vmlinux的.data..percpu  section中,且section中的變量地址是從0開(kāi)始的,這樣這些變量的地址就表示其所處的該section的位置。

另外,PERCPU_VADDR宏里還定義了三個(gè)地址值:

__per_cpu_load表示當vmlinux被加載到內存時(shí),vmlinux中的.data..percpu  section所處內存位置。__per_cpu_start的值是0。__per_cpu_end的值是vmlinux中的.data..percpu  section的結束地址。

這樣通過(guò)__per_cpu_load就可以知道當vmlinux被加載到內存時(shí),.data..percpu  section所處位置,通過(guò)__per_cpu_end - __per_cpu_start,就可以知道.data..percpu section的大小。

由上可見(jiàn),內核中的percpu變量占用內存大小差不多是170KiB。

到這里,有關(guān)percpu變量的所有準備工作都已做好,下面我們來(lái)看下,在內核vmlinux文件啟動(dòng)過(guò)程中,它是怎么利用這些信息,為各個(gè)cpu分配percpu內存塊,初始化內存塊數據,及設置內存塊地址到gs寄存器的。

通過(guò)搜索__per_cpu_load, __per_cpu_start,  __per_cpu_end我們可以知道,這些內存分配工作是在setup_per_cpu_areas方法里完成的:

該方法的文件路徑和大致樣子就如上圖所示,為了方便查看,我刪除了很多不必要的代碼。

由于該方法的邏輯非常復雜,這里我們就不詳細講解每行代碼了,只看些關(guān)鍵部分。

該方法及相關(guān)方法的主要作用是為每個(gè)cpu分配自己的percpu內存塊:

然后將vmlinux的.data..percpu section拷貝到各個(gè)cpu的percpu內存塊里:

這里的ai->static_size就是__per_cpu_end減去__per_cpu_start的值。

最后設置各cpu的percpu內存塊的起始地址值到各自cpu的gs寄存器里:

上圖中需要注意的是gs寄存器的設置方式,我們知道,在x86_64模式下,段寄存器CS, DS, ES,  SS基本上是不用了,FS和GS雖然還在用,但使用傳統的mov指令等方式設置FS和GS值,支持的地址空間只能到32位,如果想要支持到64位,必須通過(guò)寫(xiě)MSR的形式來(lái)完成。

這個(gè)在A(yíng)MD官方文檔里有詳細說(shuō)明:

在設置完gs寄存器的值后,我們再回頭來(lái)想想,內核是如何獲取當前cpu的current_task變量的地址值的呢:

mov %gs:0x16d00, %rax

現在這行代碼的意思你就完全明白了吧。

到這里,percpu部分的內容就已經(jīng)完全講完了,但有關(guān)如何獲取當前cpu正在運行的當前線(xiàn)程的current_task值,還有一點(diǎn)沒(méi)講到。

我們知道,一個(gè)cpu是可以運行多個(gè)線(xiàn)程的,如果想要讓current_task這個(gè)percpu變量,指向當前cpu的當前線(xiàn)程,那在線(xiàn)程切換的時(shí)候必須要更新一下current_task:

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。

亚洲AV一二三四区四色婷婷| 成人无码AV网站在线观看| 国产激情电影综合在线看| 麻豆AV一区二区三区| 人妻AV无码一区二区三区| 亚洲AV无码一区二区乱子伦AS|