为何所有CPU的IDTR值均相同?内核模块测试疑问
我编写了一个内核模块用于打印IDTR值,验证每个核心(包括超线程)是否拥有独立的IDT,还是同一核心共享IDT。但测试发现所有CPU的IDTR基址和限长值完全相同,看起来像是全局IDT被所有CPU共享。
为了在不同CPU上运行模块,我用taskset写了指定CPU的脚本。
模块代码
#include <linux/init.h> #include <linux/module.h> struct idt { u16 length; u64 base; } __attribute__((packed)) idt; void my_store_idt(struct idt *idt) { asm volatile ("sidt %0" : "=m"(*idt)); } static int __init mod_init(void) { my_store_idt(&idt); printk("cpu = %d, idtr.length = %u, idtr.base = %llu\n" ,get_cpu(), idt.length, idt.base); return 0; } static void __exit mod_exit(void) { printk("byeee\n"); } module_init(mod_init); module_exit(mod_exit); MODULE_LICENSE("GPL");
测试输出
[Sep15 00:52] cpu = 0, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001625] byeee [ +0.016648] cpu = 1, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001234] byeee [ +0.012360] cpu = 2, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001140] byeee [ +0.007094] cpu = 3, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001182] byeee [ +0.010665] cpu = 4, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001136] byeee [ +0.007931] cpu = 5, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001141] byeee [ +0.012969] cpu = 6, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001189] byeee [ +0.019079] cpu = 7, idtr.length = 4095, idtr.base = 18446741874686296064 [ +0.001226] byeee
测试脚本
#!/bin/bash for i in {0..7} do taskset -c $i insmod read_idtr.ko rmmod read_idtr done
我了解到每个处理器都应有独立的IDT,理论上至少每个核心的IDTR值应不同,但为何所有CPU的IDTR值完全一致?
1. Linux默认共享全局IDT的设计
Linux内核启动时会创建一个全局IDT,默认情况下所有CPU(包括超线程逻辑核)都会复用这个全局IDT,不会为每个CPU单独创建独立IDT。这是因为大部分中断处理逻辑是全局通用的,共享IDT可以减少内存开销,简化内核管理。
只有当特定CPU需要定制中断处理(比如实时场景、特定硬件驱动需求)时,内核才会为该CPU创建独立IDT,但这不是默认行为。
2. taskset的局限性导致误解
你用taskset -c $i insmod read_idtr.ko只是让insmod进程在指定CPU上执行,但内核模块的init函数执行时,内核可能会将其调度到其他CPU上——get_cpu()只是获取当前执行的CPU,但模块加载流程本身没有严格绑定到taskset指定的CPU。也就是说,你以为模块在指定CPU上运行,但实际上可能已经被内核调度走,导致读取的还是全局IDT的值。
3. 正确验证每个CPU的IDTR的方法
要准确读取每个CPU的IDTR,需要在模块中使用每CPU执行机制,强制在目标CPU上执行读取操作,而非依赖用户态的taskset。
改进后的模块代码:
#include <linux/init.h> #include <linux/module.h> #include <linux/smp.h> struct idt { u16 length; u64 base; } __attribute__((packed)); void my_store_idt(struct idt *idt) { asm volatile ("sidt %0" : "=m"(*idt)); } void read_idtr_on_cpu(void *data) { struct idt idt; my_store_idt(&idt); printk("cpu = %d, idtr.length = %u, idtr.base = 0x%llx\n", smp_processor_id(), idt.length, idt.base); } static int __init mod_init(void) { // 在所有CPU上执行read_idtr_on_cpu函数 on_each_cpu(read_idtr_on_cpu, NULL, 1); return 0; } static void __exit mod_exit(void) { printk("byeee\n"); } module_init(mod_init); module_exit(mod_exit); MODULE_LICENSE("GPL");
改进点说明:
- 使用
on_each_cpu()函数,强制让read_idtr_on_cpu在系统的每一个CPU上执行,确保读取对应CPU的IDTR。 - 用
smp_processor_id()获取当前执行的CPU ID,比get_cpu()更适合该场景(get_cpu()会禁止抢占,此处无需该操作)。 - 打印基址时用十六进制格式,更符合内核地址的阅读习惯。
对应的测试脚本可简化为:
#!/bin/bash insmod read_idtr.ko rmmod read_idtr
补充说明
即使使用改进后的代码,你大概率还是会看到所有CPU的IDTR值一致——这是Linux默认的全局IDT设计导致的。如果想要验证独立IDT的存在,可以尝试加载需要定制中断的驱动,或者手动修改内核配置启用特定CPU的独立IDT(需内核源码级修改)。
内容的提问来源于stack exchange,提问作者Syed Tayyab Farooq

