博客 / 詳情

返回

[LKD/Linux 內核] 關於對 current_thread_info 的一點研究

Linux 3.2 current_thread_info 函數

前言

current_thread_info, 這個函數在內核中, 經常被用於訪問當前CPU正在運行的任務, 那麼它的底層是怎麼實現的呢?

這是我閲讀 LKD 遇到的第一個難點, 也是我第一次體會到 "紙上得來終覺淺, 絕知此事要躬行" 的點.

關於 Linux 3.2 進程模型, 在 copy_process 中已有記載.

1.讓我們來看看, LKD 對此是怎麼寫的

LKD對此的描述如下
cti

對, 不就是獲取RSP, 然後去掉13位嗎? 這有什麼難的, 那不是隻需要 rsp & ~(8192-1) 不就好了嗎?

帶着這個思路, 我打開了 thread_info.h...

2.但是, Linux 3.2 的源代碼呢?

然而, 在Linux 3.2中, 代碼是這樣寫的

static inline struct thread_info *current_thread_info(void) {
    struct thread_info *ti;
    ti = (void *)(percpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE);
	return ti;
}

相信不止是我有這樣的感受吧:

這什麼鬼? 這percpu又是什麼鬼? 為什麼還要加加減減的? 和我在書上看到的完全不一樣啊!

別急, 我們先拆分一下這段代碼, 讓它更清晰易懂:

static inline struct thread_info *current_thread_info(void) {
    void* kstack = (void *)percpu_read_stable(kernel_stack);
    struct thread_info *ti;
    ti = kstack + KERNEL_STACK_OFFSET - THREAD_SIZE;
	return ti;
}

3. percpu 機制

3.1 percpu 含義

percpu, 顧名思義, 每個cpu.

眾所周知, 現代的 CPU 其實就是一個大公司, 每個核心相當於每個牛馬, 操作系統相當於主管.

那我們這些在玩黑公司: 打工的牛馬, 也有自己的隱私, 也就是説, 一個牛馬不能訪問其他牛馬獨有的資料和文件, 保證數據安全. 同時, 公司也有一些數據是公共的, 每個人都可以訪問.

對, cpu核心也是一樣的, cpu核心也有屬於自己的數據, 和每個核心都能訪問到的公共數據.

那問題來了, cpu核心怎麼知道哪些數據是自己的, 哪些數據是公共的呢? 這些數據存儲在哪? 如何保證隔離?

3.2 x86_64的分段模式

在 x86 中, 段寄存器存儲的是段選擇子. 那你可能會想, x86_64 就是 x86 的擴展嘛. 那分段也總和x86一樣吧.

N O!

x86_64的長模式, 可謂是差不多快把分段這玩意給廢了, 主要用的是平坦模型+分頁模式.

更具體的來説, x86_64強制CS, DS, ES三個段寄存器的值為0(當然, 還有一種情況不是0, 那就是 x86 兼容模式. 向下兼容這塊沒得説).

FS GS 存儲的值仍然是段選擇子(當然, 允許是0), 但是在長模式下, 段選擇子僅僅用於檢查特權級, 它的基址字段是不起作用的.

那麼, 在長模式下, CPU 怎麼計算地址呢?

段寄存器是CS DS ES的情況下, 計算地址的時候直接忽略掉這些段寄存器. 然而 FS GS 寄存器的情況有些不同.

每個CPU核心 (注意每個, 下面要考) 中有個區域叫 MSR, 這個區域中有兩個字段分別叫做 MSR_GS_BASEMSR_FS_BASE, CPU 在計算基址的時候, 會加上這兩個字段存儲的值, 也就是説假設有如下代碼

mov ecx, 0xC0000101
mov eax,0x10
mov edx,0x0
wrmsr
;以上是操作 MSR 寄存器的彙編代碼, 將 MSR_GS_BASE 的值設置為 0x00000010.
mov rax,qword gs:[0x1234]

那 CPU 會從 0x00001244 處獲取數據.

3.3 percpu_read_stable 的含義

OK, 現在讓我們看看這個函數. 這個函數的作用就是讀取每個CPU獨有的變量.

讓我們看看 percpu_read_stable 宏展開時候的樣子

({ typeof(kernel_stack) pfo_ret__; switch (sizeof(kernel_stack)) { case 1: asm("mov" "b ""%%""gs"":" "%P" "1"",%0" : "=q" (pfo_ret__) : "p" (&(kernel_stack))); break; case 2: asm("mov" "w ""%%""gs"":" "%P" "1"",%0" : "=r" (pfo_ret__) : "p" (&(kernel_stack))); break; case 4: asm("mov" "l ""%%""gs"":" "%P" "1"",%0" : "=r" (pfo_ret__) : "p" (&(kernel_stack))); break; case 8: asm("mov" "q ""%%""gs"":" "%P" "1"",%0" : "=r" (pfo_ret__) : "p" (&(kernel_stack))); break; default: __bad_percpu_size(); } pfo_ret__; })

嚇哭了, 然而, 實際上, 翻譯成人話, 這段代碼就在幹這件事:

mov rax,qword gs:[var]

對, 發現了嗎? 它實際上就是引用gs寄存器上的數據. 那麼根據上面講的, 引用gs寄存器, 實際上是讀取了對應CPU的MSR_GS_BASE, 然後加上offset.

誒, 對應CPU? 那也就是説... 每個CPU的MSR_GS_BASE是可以不同的?

BINGO!

所以, 我們把每個 CPU 核心的 MSR_GS_BASE 都設置成不同的值, 設立不同的 GS 基址, 讓不同的CPU訪問不同的內存, 那豈不是就可以做到每個CPU的數據隔離了嗎?

對, Linux 就是這樣乾的. offset就是變量偏移. 這就是 percpu_read_stable 的原理.

回到這段代碼, 因為每個 CPU 都需要執行內核任務, 所以 Linux 為每個 CPU 核心都分配了一個內核棧, 這個棧屬於 CPU 的私有數據.

CPU要是想知道當前的運行任務的話, 只需要獲取內核棧頂的 thread_info 儲存的值就可以.

在 Linux 中, kernel_stack記錄該cpu的內核棧起始點的位置(具體見下文), 所以, percpu_read_stable(kernel_stack) 其實就是獲取它:

static inline struct thread_info *current_thread_info(void) {
//...
    void* kstack = MSR_GS_OFFSET + kernel_stack;
//...
}

4.後續的操作呢?

4.1 x86_64 的特權級壓棧機制

在 x86_64 中, 要是進行特權級切換, 那麼就必須往內核棧壓入 5 個寄存器 SS,RSP,RFLAGS,CS,RIP, 用於保存當前 CPU 狀態.

所以, 棧底其實還預留了 40 字節, 用於保存切換特權級前的CPU狀態的, 而並不是直接存儲的 thread_info.

由此, 我們可以構造出棧模型了

高地址 (棧底)  +----------------------------+ <--- kernel_stack (TSS 中記錄的值)
              |      SS (8 bytes)          |
              |     RSP (8 bytes)          |
              |  RFLAGS (8 bytes)          |
              |      CS (8 bytes)          |
              |     RIP (8 bytes)          | 
              +----------------------------+ <--- 棧起始點(kernel_stack變量)
              |                            |
              |      內核運行時的棧空間      |
              |      (向下增長)             |
              |             |              |
              |             v              |
              |                            |
              +----------------------------+ <--- thread_info (ti) 放在最底部
低地址 (棧頂)  +----------------------------+ <--- (kernel_stack - THREAD_SIZE) 

4.2 後面的那加加減減

#define KERNEL_STACK_OFFSET (5*8) 
//現在知道5*8怎麼來了吧
#define THREAD_SIZE 8192 
//內核棧大小
static inline struct thread_info *current_thread_info(void) {
    //...
    ti = kstack + KERNEL_STACK_OFFSET - THREAD_SIZE;
    //...
}

kstack 是棧起始點的位置, THREAD_SIZE 是棧大小, 所以我們先通過 kstack - THREAD_SIZE 獲取棧底的位置.
然後, 我們再加上KERNEL_STACK_OFFSET, 就是棧頂的位置了, 也是 thread_info 的位置.

The End

所以, 總體的代碼是這樣的

static inline struct thread_info *current_thread_info(void) {
    void* kstack = (void *)percpu_read_stable(kernel_stack);
    struct thread_info *ti;
    ti = kstack + KERNEL_STACK_OFFSET - THREAD_SIZE;
	return ti;
}

本期隨筆寫到這, 感謝大家的觀看哦~萌新初涉 Linux 內核, 有錯誤也請多多指正~

版權聲明: 本文采用 CC BY-NC-SA 4.0 許可協議。轉載請註明出處!
作者: Sudo-su-Bash (Alien-Bash)
發佈時間: 2026-02-18
原文鏈接: https://www.cnblogs.com/SudosuBash/p/19622204

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.