Linux内核中current_thread_info函数的实现原理是怎样的?

摘要:Linux 3.2 current_thread_info 函数 前言 current_thread_info, 这个函数在内核中, 经常被用于访问当前CPU正在运行的任务, 那么它的底层是怎么实现的呢? 这是我阅读 LKD 遇到的第一个难
Linux 3.2 current_thread_info 函数 前言 current_thread_info, 这个函数在内核中, 经常被用于访问当前CPU正在运行的任务, 那么它的底层是怎么实现的呢? 这是我阅读 LKD 遇到的第一个难点, 也是我第一次体会到 "纸上得来终觉浅, 绝知此事要躬行" 的点. 关于 Linux 3.2 进程模型, 在 copy_process 中已有记载. 1.让我们来看看, LKD 对此是怎么写的 LKD对此的描述如下 对, 不就是获取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_BASE 和 MSR_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