当前位置: 首页 > news >正文

[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) 不就好了吗?

带着这个思路, 我打开了 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 内核, 有错误也请多多指正~

http://www.jsqmd.com/news/390789/

相关文章:

  • 神经符号整合的可解释推荐系统推理
  • 2026年水利水电工程施工企业综合评测与网址导航 - 2026年企业推荐榜
  • 2026年开年驻马店全屋定制家具制造商选择指南 - 2026年企业推荐榜
  • Java Web 毕业生实习与就业管理系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】
  • 移动开发领域 MVP 模式的在线旅游应用开发与预订
  • 提升Python运行速度的几款工具
  • SpringBoot+Vue 民宿管理系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • 基于SpringBoot+Vue的银行账目账户管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • SpringBoot+Vue 银行账目账户管理系统平台完整项目源码+SQL脚本+接口文档【Java Web毕设】
  • 存在即对话:方见华对话本体论与世毫九理论体系的形式证明与工程实现V1.0
  • Blinkist - Lucky by Design
  • linux系统开启全局代理脚本,通过虚拟tun卡实现
  • 2026年唐山选矿设备服务商实力评估与精选推荐 - 2026年企业推荐榜
  • 百度AI数据治理案例:架构师如何解决数据延迟问题?
  • 良品率90%以上的AI:是原画师的“斩杀线”还是新纪元的起跑线?
  • 2026年家用不锈钢浴室柜深度评测与品牌甄选指南 - 2026年企业推荐榜
  • 实战分享:如何为PB级大数据实施高效脱敏处理
  • 2026年安徽旧房翻新市场深度解析与优质局改全改公司推荐 - 2026年企业推荐榜
  • 武汉重型货架品牌评测:2026年Q1如何选择高效仓储伙伴? - 2026年企业推荐榜
  • 基于领码SPARK融合平台的省级二轮土地承包延包再延长30年管理平台解决方案:从0到1避坑指南(附完整代码)
  • 2026年家电清洗服务专业度评测:如何选择靠谱服务商? - 2026年企业推荐榜
  • 人类责任主义:AI治理作为创新的源头,人机共生作为意义的归宿 ——基于DOS模型的系统化建构
  • 人类责任主义:AI治理作为创新
  • 分布式系统:分布式文件系统
  • Zookeeper在大数据领域的集群搭建与配置
  • 阜阳侵权纠纷法律服务选择指南:三维度评估与三家律所深度解析 - 2026年企业推荐榜
  • 主成分分析 – 实战教程
  • 大数据领域 ETL 的架构设计与最佳实践
  • 【2025最新】基于SpringBoot+Vue的针对老年人景区订票系统管理系统源码+MyBatis+MySQL
  • 企业级汽车租赁系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】