精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

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

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

3天內不再提示

linux內核中percpu變量的實現

Linux閱碼場 ? 來源:Linuxer ? 作者:Linuxer ? 2021-01-04 13:39 ? 次閱讀

我們在使用各種編程語言進行多線程編程時,經常會用到thread local變量。

所謂thread local變量,就是對于同一個變量,每個線程都有自己的一份,對該變量的訪問是線程隔離的,它們之間不會相互影響,所以也就不會有各種多線程問題。

正確的使用thread local變量,能極大的簡化多線程開發。所以不管是c/c++/rust,還是java/c#等,都內置了對thread local變量的支持。

但你知道嗎,不僅是在編程語言中,在linux內核中,也有一個類似的機制,用來實現類似的目的,它叫做percpu變量。

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

好奇的你們肯定都在問,它是怎么實現的呢?

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

095e34cc-4e4d-11eb-8b86-12bb97331649.png

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

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

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

到這里有關percpu變量的初始化工作就已經結束了。

當我們在訪問percpu變量時,只需要將gs寄存器里的地址,加上我們想要訪問的percpu變量的地址,就能得到在該cpu上,該percpu變量真實的內存地址。

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

上圖中重點描述的是那些,在內核編譯期就已經確定的percpu變量,這些變量是靜態的,是不會隨著時間的推移而動態的增加或減少的,所以它們在內核初始化時,就直接被拷貝到了各個percpu內存塊的static區。

除了這種靜態percpu變量,還有另外兩種percpu變量。

其中一種是內核模塊中的靜態percpu變量,它雖然也是在編譯期就能確定的,但由于內核模塊動態加載的特性,它不是完全靜態的,內核為這種percpu變量在percpu內存塊中單獨開辟了一個區域,叫reserved區,當內核模塊被加載到內存時,其靜態percpu變量就會在這個區域分配內存。

另外一種percpu變量就是純動態的percpu變量,它是在運行時動態分配的,它使用的內存是上圖中的dynamic區。

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

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

下面我們就用一個具體的例子,來看下percpu變量到底是怎么實現的。

098e7696-4e4d-11eb-8b86-12bb97331649.png

上圖中的current表示要獲取當前線程對象,它其實是一個宏,具體定義如下:

09d1f36c-4e4d-11eb-8b86-12bb97331649.png

由上可見,current獲取的當前線程對象其實是一個名為current_task的percpu變量。

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

this_cpu_read_stable方法其實也是一個宏,它全部展開后是下面這個樣子:

0a0caba6-4e4d-11eb-8b86-12bb97331649.png

在這里,我們先不講宏展開后各語句到底是什么意思,我們先跑個題。 讀過linux內核源碼的同學都知道,在linux內核中,宏使用的非常多,且比較復雜,如果我們對自己進行宏展開的正確性沒有信心的話,可以使用下面我介紹的這個方式,使用它,你可以非常容易的得到任意文件宏展開后的結果。

我們知道,一個程序的構建分為預處理、編譯、匯編、鏈接這些階段,而宏展開就發生在預處理階段。 各個階段在完成后,一般都會生成一個臨時文件給下一階段使用,這些臨時文件默認是不會保存到磁盤上的,但我們可以通過指定一些參數,告知gcc幫我們保留下來這些臨時文件,這樣我們就可以查看各個階段的生成內容了。

依據該思路,我們只要在編譯比如上面的net/socket.c文件時,加上這些參數,我們就能得到這些臨時文件,也就可以查看其預處理之后的宏展開是什么樣子的了。 但是,如果只是為了查看單個文件的宏展開后結果,就保存下整個內核中,所有源文件編譯時的臨時文件,這是非常耗時且不劃算的,那有沒有辦法可以想查看哪個文件的宏展開,就單獨編譯一次那個文件呢? 還真有。

其實說起來該方法也很簡單,我們只需要知道編譯某個文件時使用的編譯命令是什么,這樣當我們需要查看這個文件的宏展開時,再使用這個編譯命令,且加上一些特定的參數,再編譯一遍,這樣就能得到該文件編譯過程中,各階段的臨時文件了。 那如何找到編譯各個源文件時使用的命令呢?

這個內核其實已經幫我們做好了。 當我們在編譯內核時,內核中每個文件被編譯時使用的命令,都會保存到一個對應的臨時文件里,比如上面net/socket.c文件的編譯命令就保存在下面的文件里:

0a40bcc0-4e4d-11eb-8b86-12bb97331649.png

net/socket.c的編譯命令就是上圖中的第一行,從gcc開始到該行結束的部分。 這個編譯命令夠復雜吧,但我們不用管,我們只用知道,使用該命令,就可以將net/socket.c編譯成net/socket.o。 現在我們在該命令的基礎上,加上-save-temps=obj參數,告知gcc在編譯時保留下各階段的臨時文件,具體操作流程如下:

0a7fdec8-4e4d-11eb-8b86-12bb97331649.png

由上可見,加上-save-temps=obj參數后,該編譯過程多生成兩個文件,而net/socket.i就是gcc預處理之后的文件。 打開net/socket.i,并找到我們需要的get_current方法:

0ac1897c-4e4d-11eb-8b86-12bb97331649.png

看上圖中的選中部分,其內容和我們自己宏展開后的結果,是完全一樣的。 這個方法還不錯吧。 當然,我們還可以通過反編譯的方式,進一步確認下宏展開后確實是這樣:

0aecae04-4e4d-11eb-8b86-12bb97331649.png

由上可見,宏展開后其實主要就是一條mov指令,其中current_task變量地址的值為0x16d00。 該指令的意思是,將gs寄存器里的地址,和current_task的地址相加,然后將相加后地址指向的內存空間里的值,移動到rax里。 這個和我們上面提到的,percpu的實現機制是一致的。 好,我們回到上文中斷的部分,來繼續看下get_current方法里宏展開后各語句的意思。

上文講到,get_current方法里的this_cpu_read_stable方法宏展開后主要是一條asm語句,可能有些同學對該語句不太熟悉,它其實并不是c語言標準規范里的語法,而是gcc對c標準的擴展,通過asm語句,我們可以在c中直接執行匯編指令。 有關其詳細的語法規則,可以參考以下鏈接: https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C

不關心細節的同學可以不用去看具體語法,我們只要知道該asm語句的意思是,獲取current_task的地址,將該地址與gs段寄存器里的基礎地址值相加,得到一個最終的地址,然后通過mov指令,將該最終地址指向的內存的值,放到pfo_val__變量里。 該指令執行完畢后,pfo_val__變量里存放的值,就是當前cpu執行的當前線程對象struct task_struct的地址,也就是說,pfo_val__變量為當前正在執行的線程對象的指針。

那為什么通過這種方式,得到的就是當前cpu正在執行的當前線程對象的指針呢? 這個其實上文我們已經講過了,關鍵點在于gs寄存器中存放的是當前cpu的percpu內存塊的起始地址,而current_task的地址表示的又是,current_task變量在任意percpu內存塊的位置,所以這兩個地址一相加,得到的自然就是當前cpu的current_task變量的當前值了。 理論上是如此,不過我們還是通過源碼角度再看下。 首先我們來看下current_task變量的定義:

0b59deb6-4e4d-11eb-8b86-12bb97331649.png

DEFINE_PER_CPU還是一個宏,其展開后如下:

0b89d1de-4e4d-11eb-8b86-12bb97331649.png

在宏展開后的變量定義中,最重要的是指定該變量的section為.data..percpu。 我們再看什么地方使用了這個section:

0bb45e36-4e4d-11eb-8b86-12bb97331649.png

由上圖可見,PERCPU_INPUT宏里使用了該section,而PERCPU_INPUT宏又被下面的PERCPU_VADDR宏使用。 我們再來看下PERCPU_VADDR宏在哪里使用:

0c2ee5fc-4e4d-11eb-8b86-12bb97331649.png

由上可見PERCPU_VADDR宏又在vmlinux.lds.S文件中使用。 vmlinux.lds.S是一個鏈接腳本,在鏈接階段,linker會根據vmlinux.lds.S里的定義,把相同section的內核變量或方法,聚合起來,放到最終輸出文件vmlinux的對應section里。 比如上面的PERCPU_VADDR宏就是說,把所有源文件中的屬于各種.data..percpu section的變量提取出來,然后依次放入到輸出文件vmlinux的.data..percpu的section中。

上圖中需要注意的是,在調用PERCPU_VADDR時,傳入的vaddr參數是0,它表示vmlinux中.data..percpu section里存放的變量地址是從0開始,依次遞增的。 這個我們之前也說過,該地址是用來表示該變量在.data..percpu section里的位置,也就是說,該地址表示的是該變量在運行時的,各cpu的percpu內存塊里的位置。 vmlinux里.data..percpu section存放的變量地址是從0開始的,這個我們可以通過__per_cpu_start的值得到確認:

0c512b58-4e4d-11eb-8b86-12bb97331649.png

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

0c75ebdc-4e4d-11eb-8b86-12bb97331649.png

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

另外,PERCPU_VADDR宏里還定義了三個地址值: __per_cpu_load表示當vmlinux被加載到內存時,vmlinux中的.data..percpu section所處內存位置。 __per_cpu_start的值是0。 __per_cpu_end的值是vmlinux中的.data..percpu section的結束地址。 這樣通過__per_cpu_load就可以知道當vmlinux被加載到內存時,.data..percpu section所處位置,通過__per_cpu_end -__per_cpu_start,就可以知道.data..percpu section的大小。

0c958636-4e4d-11eb-8b86-12bb97331649.png

由上可見,內核中的percpu變量占用內存大小差不多是170KiB。 到這里,有關percpu變量的所有準備工作都已做好,下面我們來看下,在內核vmlinux文件啟動過程中,它是怎么利用這些信息,為各個cpu分配percpu內存塊,初始化內存塊數據,及設置內存塊地址到gs寄存器的。 通過搜索__per_cpu_load,__per_cpu_start,__per_cpu_end我們可以知道,這些內存分配工作是在setup_per_cpu_areas方法里完成的:

0cc992aa-4e4d-11eb-8b86-12bb97331649.png

該方法的文件路徑和大致樣子就如上圖所示,為了方便查看,我刪除了很多不必要的代碼。 由于該方法的邏輯非常復雜,這里我們就不詳細講解每行代碼了,只看些關鍵部分。 該方法及相關方法的主要作用是為每個cpu分配自己的percpu內存塊:

0d00dcc4-4e4d-11eb-8b86-12bb97331649.png

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

0d2962ca-4e4d-11eb-8b86-12bb97331649.png

這里的ai->static_size就是__per_cpu_end減去__per_cpu_start的值。 最后設置各cpu的percpu內存塊的起始地址值到各自cpu的gs寄存器里:

0d5d891a-4e4d-11eb-8b86-12bb97331649.png

上圖中需要注意的是gs寄存器的設置方式,我們知道,在x86_64模式下,段寄存器CS, DS, ES, SS基本上是不用了,FS和GS雖然還在用,但使用傳統的mov指令等方式設置FS和GS值,支持的地址空間只能到32位,如果想要支持到64位,必須通過寫MSR的形式來完成。 這個在AMD官方文檔里有詳細說明:

0d7ce4a4-4e4d-11eb-8b86-12bb97331649.png

在設置完gs寄存器的值后,我們再回頭來想想,內核是如何獲取當前cpu的current_task變量的地址值的呢: mov %gs:0x16d00, %rax 現在這行代碼的意思你就完全明白了吧。 到這里,percpu部分的內容就已經完全講完了,但有關如何獲取當前cpu正在運行的當前線程的current_task值,還有一點沒講到。 我們知道,一個cpu是可以運行多個線程的,如果想要讓current_task這個percpu變量,指向當前cpu的當前線程,那在線程切換的時候必須要更新一下current_task:

0da628fa-4e4d-11eb-8b86-12bb97331649.png

如上。 現在,有關percpu變量的知識,你是否已經完全了解了呢,如果還有疑問,可以再去看看文章開始我畫的那張圖,或者給我留言,我們可以一起討論。

責任編輯:xj

原文標題:一張圖看懂linux內核中percpu變量的實現

文章出處:【微信公眾號:Linuxer】歡迎添加關注!文章轉載請注明出處。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 內核
    +關注

    關注

    3

    文章

    1336

    瀏覽量

    40082
  • Linux
    +關注

    關注

    87

    文章

    11123

    瀏覽量

    207886
  • 變量
    +關注

    關注

    0

    文章

    607

    瀏覽量

    28257

原文標題:一張圖看懂linux內核中percpu變量的實現

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    linux驅動程序如何加載進內核

    Linux系統,驅動程序是內核與硬件設備之間的橋梁。它們允許內核與硬件設備進行通信,從而實現對硬件設備的控制和管理。 驅動程序的編寫 驅
    的頭像 發表于 08-30 15:02 ?187次閱讀

    Linux內核測試技術

    內核測試技術是實現這一目標的關鍵手段。本文將詳細介紹 Linux 內核測試的各種技術,包括單元測試、集成測試、功能測試和性能測試等,并討論不同測試方法的優缺點及其適用場景。
    的頭像 發表于 08-13 13:42 ?242次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>測試技術

    Linux內核的頁面分配機制

    Linux內核是如何分配出頁面的,如果我們站在CPU的角度去看這個問題,CPU能分配出來的頁面是以物理頁面為單位的。也就是我們計算機中常講的分頁機制。本文就看下Linux
    的頭像 發表于 08-07 15:51 ?143次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b><b class='flag-5'>中</b>的頁面分配機制

    歡創播報 華為宣布鴻蒙內核已超越Linux內核

    1 華為宣布鴻蒙內核已超越Linux內核 ? 6月21日,在華為開發者大會上, HarmonyOS NEXT(鴻蒙NEXT)——真正獨立于安卓和iOS的鴻蒙操作系統,正式登場。這是HarmonyOS
    的頭像 發表于 06-27 11:30 ?565次閱讀

    使用 PREEMPT_RT 在 Ubuntu 構建實時 Linux 內核

    盟通技術干貨構建實時Linux內核簡介盟通技術干貨Motrotech如果需要在Linux實現實時計算性能,進而有效地將
    的頭像 發表于 04-12 08:36 ?1400次閱讀
    使用 PREEMPT_RT 在 Ubuntu <b class='flag-5'>中</b>構建實時 <b class='flag-5'>Linux</b> <b class='flag-5'>內核</b>

    Linux系統設置環境變量的方法和技巧

    Linux中環境變量是一種保存有關系統環境配置的信息的對象。它們被廣泛用于存儲有關系統操作的信息比如路徑、文件名等。通過合理配置環境變量我們可以方便地訪問和執行各種命令和程序。
    的頭像 發表于 02-01 11:09 ?1523次閱讀
    <b class='flag-5'>Linux</b>系統<b class='flag-5'>中</b>設置環境<b class='flag-5'>變量</b>的方法和技巧

    C++在Linux內核開發從爭議到成熟

    Linux 內核郵件列表中一篇已有六年歷史的老帖近日再次引發激烈討論 —— 主題是建議將 Linux 內核的開發語言從 C 轉換為更現代的 C++。
    的頭像 發表于 01-31 14:11 ?486次閱讀
    C++在<b class='flag-5'>Linux</b><b class='flag-5'>內核</b>開發<b class='flag-5'>中</b>從爭議到成熟

    Linux環境變量配置方法

    想必大家平時工作也會配置Linux的環境變量,但是可能也僅僅是為解決某些工具的運行環境,對于Linux環境變量本身的配置學問還沒深入了解。
    的頭像 發表于 01-04 09:51 ?411次閱讀

    Linux內核RCU的用法

    Linux內核,RCU最常見的用途是替換讀寫鎖。在20世紀90年代初期,Paul在實現通用RCU之前,實現了一種輕量級的讀寫鎖。后來,為
    的頭像 發表于 12-27 09:56 ?1421次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b><b class='flag-5'>中</b>RCU的用法

    獲取Linux內核源碼的方法

    (ELF1/ELF1S開發板及顯示屏)Linux內核是操作系統中最核心的部分,它負責管理計算機硬件資源,并提供對應用程序和其他系統組件的訪問接口,控制著計算機的內存、處理器、設備驅動程序和文件系統等
    的頭像 發表于 12-13 09:49 ?532次閱讀
    獲取<b class='flag-5'>Linux</b><b class='flag-5'>內核</b>源碼的方法

    Linux內核自解壓過程分析

    uboot完成系統引導以后,執行環境變量bootm的命令;即,將Linux內核調入內存并調用do_bootm函數啟動
    的頭像 發表于 12-08 14:00 ?703次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>自解壓過程分析

    內核的電源管理

    之前介紹的電源管理機制基本都是在Linux實現的,可以看到很復雜,各種框架,明明一個操作非要轉來轉去,而且在內核里面實現,跟
    的頭像 發表于 11-29 09:33 ?699次閱讀
    微<b class='flag-5'>內核</b><b class='flag-5'>中</b>的電源管理

    Linux內核UDP收包為什么效率低

    包效率真的很低,這是為什么?有沒有辦法去嘗試著優化?而不是動不動就DPDK。 我們從最開始說起。 Linux內核作為一個通用操作系統內核,脫胎于UNIX那一套現代操作系統理論。 但一開始不知道怎么回事將網絡協議棧的
    的頭像 發表于 11-13 10:38 ?385次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>UDP收包為什么效率低

    如何優化Linux內核UDP收包效率低

    真的很低,這是為什么?有沒有辦法去嘗試著優化?而不是動不動就DPDK。 我們從最開始說起。 Linux內核作為一個通用操作系統內核,脫胎于UNIX那一套現代操作系統理論。 但一開始不知道怎么回事將網絡協議棧的
    的頭像 發表于 11-10 10:51 ?465次閱讀
    如何優化<b class='flag-5'>Linux</b><b class='flag-5'>內核</b>UDP收包效率低

    Linux內核時鐘系統和定時器實現

    Linux內核時鐘系統和定時器實現 Linux 2.6.16之前,內核只支持低精度時鐘,內核定時
    的頭像 發表于 11-09 09:12 ?1109次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>時鐘系統和定時器<b class='flag-5'>實現</b>