sys_ioperm 函数详解
概述
sys_ioperm 是 Linux 内核中的一个系统调用,用于修改当前任务的 I/O 端口权限位图。该函数允许用户空间程序控制对特定 I/O 端口的访问权限,这是 x86 架构下硬件 I/O 端口访问控制的核心机制。
函数原型
c
asmlinkage long sys_ioperm(unsigned long from, unsigned long num, int turn_on)
参数说明
- from: 起始 I/O 端口号(0-65535)
- num: 要修改的连续端口数量
- turn_on : 权限标志
1(非零): 允许访问指定的 I/O 端口0: 禁止访问指定的 I/O 端口
返回值
0: 成功-EINVAL: 参数无效(端口范围超出或溢出)-EPERM: 权限不足(需要CAP_SYS_RAWIO权限才能开启 I/O 访问)-ENOMEM: 内存分配失败
代码实现分析
1. 变量声明
c
unsigned long i, max_long, bytes, bytes_updated;
struct thread_struct * t = ¤t->thread;
struct tss_struct * tss;
unsigned long *bitmap;
t: 指向当前任务的线程结构体,包含 I/O 位图指针和最大字节数tss: 任务状态段(Task State Segment),x86 架构中用于存储任务状态信息bitmap: I/O 权限位图指针
2. 参数验证
c
if ((from + num <= from) || (from + num > IO_BITMAP_BITS))
return -EINVAL;
验证逻辑:
from + num <= from: 检测整数溢出(如果from + num小于等于from,说明发生了溢出)from + num > IO_BITMAP_BITS: 确保端口范围不超过最大支持值(65536)
IO_BITMAP_BITS 定义为 65536,对应 x86 架构支持的 16 位 I/O 地址空间(0x0000-0xFFFF)。
3. 权限检查
c
if (turn_on && !capable(CAP_SYS_RAWIO))
return -EPERM;
权限要求:
- 只有当
turn_on为非零(开启 I/O 访问权限)时,才需要检查权限 - 需要
CAP_SYS_RAWIO能力(通常只有 root 用户拥有) - 关闭 I/O 访问权限不需要特殊权限(任何进程都可以关闭自己的 I/O 权限)
4. 延迟初始化 I/O 位图
c
if (!t->io_bitmap_ptr) {
bitmap = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
if (!bitmap)
return -ENOMEM;
memset(bitmap, 0xff, IO_BITMAP_BYTES);
t->io_bitmap_ptr = bitmap;
}
延迟初始化策略:
- 只有在第一次调用
ioperm()时才分配 I/O 位图 - 这样做的原因是:
ioperm()的调用频率远低于clone(),延迟初始化可以节省内存 - 初始值
0xff表示所有位都设置为 1,即默认禁止所有 I/O 端口访问 - 在 I/O 位图中,
1表示禁止访问,0表示允许访问(这是 x86 硬件的要求)
内存大小:
IO_BITMAP_BYTES = IO_BITMAP_BITS / 8 = 65536 / 8 = 8192字节(8KB)
5. 禁用抢占并获取 TSS
c
tss = &per_cpu(init_tss, get_cpu());
关键操作:
get_cpu(): 禁用内核抢占并返回当前 CPU 编号per_cpu(init_tss, cpu): 获取当前 CPU 的 TSS 结构- 必须禁用抢占 ,因为后续操作需要保证
io_bitmap_max值与位图内容的一致性
为什么必须禁用抢占?详细分析:
禁用抢占是确保 I/O 权限位图更新操作原子性和一致性的关键机制。主要原因如下:
1. 多步骤操作的原子性要求
sys_ioperm 函数执行了一个多步骤的更新过程:
c
// 步骤1: 修改位图内容
set_bitmap(t->io_bitmap_ptr, from, num, !turn_on);
// 步骤2: 重新计算最大有效字节数
max_long = 0;
for (i = 0; i < IO_BITMAP_LONGS; i++)
if (t->io_bitmap_ptr[i] != ~0UL)
max_long = i;
bytes = (max_long + 1) * sizeof(long);
// 步骤3: 更新 io_bitmap_max
t->io_bitmap_max = bytes;
// 步骤4: 设置延迟加载标志
tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET_LAZY;
这些步骤必须作为一个原子操作完成。如果在这四个步骤之间发生抢占,可能导致严重的数据不一致问题。
2. io_bitmap_max 与位图内容的一致性
io_bitmap_max 字段表示位图中实际有效的最大字节数,它必须准确反映位图的当前状态:
- 如果
io_bitmap_max过大:可能包含无效的位图数据,导致错误的 I/O 权限判断 - 如果
io_bitmap_max过小:可能遗漏有效的位图数据,导致某些端口的权限设置失效
不一致场景示例:
假设在步骤1和步骤2之间发生抢占:
c
// 线程A执行:
set_bitmap(t->io_bitmap_ptr, 0, 100, 0); // 允许端口 0-99
// [此时发生抢占,切换到线程B]
// 线程B可能修改了位图或 io_bitmap_max
// [切换回线程A]
max_long = 0; // 基于可能已被修改的位图计算
t->io_bitmap_max = bytes; // 可能设置错误的值
结果:io_bitmap_max 可能反映的是线程B修改后的状态,而不是线程A期望的状态。
3. CPU 绑定的重要性
get_cpu() 不仅禁用抢占,还确保整个操作在同一个 CPU 上执行:
- TSS 是每 CPU 数据结构:每个 CPU 都有自己的 TSS(Task State Segment)
- 必须操作当前 CPU 的 TSS :
per_cpu(init_tss, get_cpu())确保获取的是当前 CPU 的 TSS - 抢占可能导致 CPU 切换:如果被抢占后在其他 CPU 上恢复执行,会访问错误的 TSS
CPU 切换场景:
c
// 在 CPU 0 上执行
tss = &per_cpu(init_tss, 0); // 获取 CPU 0 的 TSS
// [发生抢占,任务被调度到 CPU 1]
// 如果继续使用原来的 tss 指针,将访问错误的 TSS!
4. TSS 更新的同步问题
TSS 中的 io_bitmap_max 和 io_bitmap 必须与线程的 thread_struct 中的对应字段保持同步。如果操作被中断:
- 部分更新 :可能只更新了位图,但没有更新
io_bitmap_max - 延迟加载标志错误 :
io_bitmap_base可能被设置为错误的值 - 竞态条件:其他线程或中断处理程序可能同时访问这些字段
5. 安全性考虑
I/O 权限位图直接控制硬件访问权限,不一致可能导致:
- 权限提升:本应禁止的端口被允许访问
- 权限降级:本应允许的端口被错误禁止
- 系统不稳定:错误的硬件访问可能导致系统崩溃
6. get_cpu() 和 put_cpu() 的机制
c
#define get_cpu() ({ preempt_disable(); smp_processor_id(); })
#define put_cpu() preempt_enable()
preempt_disable(): 增加抢占计数器,禁止内核抢占smp_processor_id(): 返回当前 CPU 编号(在禁用抢占后,CPU 编号不会改变)preempt_enable(): 减少抢占计数器,恢复内核抢占
关键保证:
- 在
get_cpu()和put_cpu()之间,当前任务不会被抢占 - 当前任务始终在同一个 CPU 上执行
- 可以安全地访问每 CPU 数据(如 TSS)
7. 实际代码中的保护范围
从代码可以看出,禁用抢占保护了整个关键区域:
c
tss = &per_cpu(init_tss, get_cpu()); // 开始保护
set_bitmap(t->io_bitmap_ptr, from, num, !turn_on); // 修改位图
// ... 计算 max_long 和 bytes ...
t->io_bitmap_max = bytes; // 更新最大值
tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET_LAZY; // 设置延迟标志
put_cpu(); // 结束保护
这个保护范围确保了:
- 位图修改和
io_bitmap_max更新是原子的 - 所有操作都在同一个 CPU 上完成
- TSS 访问的一致性
总结 :禁用抢占是 Linux 内核中保护关键数据结构和确保操作原子性的标准机制。在 sys_ioperm 中,它确保了 I/O 权限位图更新的完整性和安全性,防止了可能导致安全漏洞或系统不稳定的竞态条件。
6. 更新位图
c
set_bitmap(t->io_bitmap_ptr, from, num, !turn_on);
重要细节:
- 注意参数是
!turn_on(取反) - 因为位图中
1表示禁止,0表示允许 - 所以当
turn_on = 1(允许访问)时,需要将位设置为0 - 当
turn_on = 0(禁止访问)时,需要将位设置为1
set_bitmap 函数详细解析
set_bitmap 是一个静态辅助函数,用于在位图中设置连续的位。该函数需要处理位图可能跨越多个 unsigned long 单元的情况。
函数原型
20:51:arch/i386/kernel/ioport.c
static void set_bitmap(unsigned long *bitmap, unsigned int base, unsigned int extent, int new_value)
参数说明:
bitmap: 指向位图数组的指针(每个元素是unsigned long)base: 起始位的位置(从 0 开始)extent: 要设置的连续位的数量new_value: 要设置的值(1 或 0)
函数作用 :将位图中从 base 开始的 extent 个连续位设置为 new_value。
关键变量初始化
c
unsigned long mask;
unsigned long *bitmap_base = bitmap + (base / BITS_PER_LONG);
unsigned int low_index = base & (BITS_PER_LONG-1);
int length = low_index + extent;
变量含义:
-
bitmap_base: 指向包含起始位的unsigned long单元的指针base / BITS_PER_LONG: 计算起始位所在的long单元索引- 例如:如果
BITS_PER_LONG = 32,base = 50,则bitmap_base = bitmap + 1(第 2 个 long)
-
low_index: 起始位在当前long单元内的偏移(0 到 BITS_PER_LONG-1)base & (BITS_PER_LONG-1): 等价于base % BITS_PER_LONG- 例如:
base = 50,BITS_PER_LONG = 32,则low_index = 50 & 31 = 18
-
length: 从起始位到结束位的总长度(包括起始位所在 long 的剩余部分)length = low_index + extent- 例如:
low_index = 18,extent = 20,则length = 38
处理逻辑:三种情况
由于位图以 unsigned long 为单位存储,设置连续的位可能涉及三种情况:
情况 1:处理起始部分(不对齐的部分)
c
if (low_index != 0) {
mask = (~0UL << low_index);
if (length < BITS_PER_LONG)
mask &= ~(~0UL << length);
if (new_value)
*bitmap_base++ |= mask;
else
*bitmap_base++ &= ~mask;
length -= BITS_PER_LONG;
}
适用场景 :起始位不在 long 单元的边界上(low_index != 0)
详细分析:
-
创建起始掩码:
cmask = (~0UL << low_index);~0UL: 全 1 的unsigned long(例如:0xFFFFFFFF)<< low_index: 左移low_index位,低位补 0- 结果:从
low_index位开始到最高位都是 1 - 例如:
low_index = 18,mask = 0xFFFFC000(32位系统)
-
处理范围完全在一个 long 内的情况:
cif (length < BITS_PER_LONG) mask &= ~(~0UL << length);- 如果整个范围都在第一个
long内,需要截断掩码 ~(~0UL << length): 创建从 0 到length-1位全为 1 的掩码mask &= ...: 只保留从low_index到length-1的位- 例如:
low_index = 18,length = 25,最终mask只覆盖位 18-24
- 如果整个范围都在第一个
-
应用掩码:
cif (new_value) *bitmap_base++ |= mask; // 设置为 1 else *bitmap_base++ &= ~mask; // 设置为 0- 使用按位或(
|=)设置位为 1 - 使用按位与取反(
&= ~mask)清除位(设置为 0) bitmap_base++: 处理完后移动到下一个long单元
- 使用按位或(
-
更新剩余长度:
clength -= BITS_PER_LONG;- 减去已处理的位数(一个完整的
long)
- 减去已处理的位数(一个完整的
示例 1 :base = 18,extent = 10,BITS_PER_LONG = 32
low_index = 18length = 18 + 10 = 28 < 32mask = (~0UL << 18) & ~(~0UL << 28) = 0x03FC0000(覆盖位 18-27)- 只执行情况 1,情况 2 和 3 不执行
情况 2:处理中间完整的 long 单元
c
mask = (new_value ? ~0UL : 0UL);
while (length >= BITS_PER_LONG) {
*bitmap_base++ = mask;
length -= BITS_PER_LONG;
}
适用场景 :处理完整的 long 单元(一个或多个)
详细分析:
-
创建完整掩码:
cmask = (new_value ? ~0UL : 0UL);- 如果
new_value = 1,mask = ~0UL(全 1) - 如果
new_value = 0,mask = 0UL(全 0)
- 如果
-
批量设置完整的 long 单元:
cwhile (length >= BITS_PER_LONG) { *bitmap_base++ = mask; length -= BITS_PER_LONG; }- 直接赋值整个
long单元,效率最高 - 循环处理所有完整的
long单元 - 每处理一个,
length减少BITS_PER_LONG
- 直接赋值整个
示例 2 :base = 18,extent = 100,BITS_PER_LONG = 32
- 情况 1 处理:位 18-31(14 位),
length = 18 + 100 - 32 = 86 - 情况 2 处理:2 个完整的
long(位 32-63 和 64-95),length = 86 - 64 = 22 - 情况 3 处理:位 96-117(22 位)
情况 3:处理末尾部分
c
if (length > 0) {
mask = ~(~0UL << length);
if (new_value)
*bitmap_base++ |= mask;
else
*bitmap_base++ &= ~mask;
}
适用场景 :处理最后一个不完整的 long 单元
详细分析:
-
创建末尾掩码:
cmask = ~(~0UL << length);~0UL << length: 从length位开始到最高位全为 1~(...): 取反,得到从 0 到length-1位全为 1 的掩码- 例如:
length = 10,mask = 0x000003FF(32位系统,覆盖低 10 位)
-
应用掩码:
cif (new_value) *bitmap_base++ |= mask; // 设置为 1 else *bitmap_base++ &= ~mask; // 设置为 0- 与情况 1 相同的逻辑,但这里是从 0 位开始
示例 3 :base = 0,extent = 10,BITS_PER_LONG = 32
low_index = 0,情况 1 不执行length = 10 < 32,情况 2 不执行- 情况 3 处理:
mask = 0x000003FF,设置位 0-9
完整示例
假设 BITS_PER_LONG = 32(32位系统),设置 base = 18,extent = 100,new_value = 0(允许访问):
位图布局(每个数字代表一个位):
long[0]: [0-31] ← 情况1处理位 18-31
long[1]: [32-63] ← 情况2处理(完整)
long[2]: [64-95] ← 情况2处理(完整)
long[3]: [96-127] ← 情况3处理位 96-117
执行过程:
-
情况 1:
low_index = 18mask = (~0UL << 18) = 0xFFFFC000(位 18-31 为 1)*bitmap_base++ &= ~mask:清除位 18-31length = 118 - 32 = 86
-
情况 2:
mask = 0UL(全 0)- 循环 2 次:
long[1] = 0,long[2] = 0 length = 86 - 64 = 22
-
情况 3:
mask = ~(~0UL << 22) = 0x003FFFFF(低 22 位为 1)*bitmap_base++ &= ~mask:清除位 96-117
位操作技巧总结
- 创建从位置 n 开始的掩码 :
~0UL << n - 创建到位置 n 结束的掩码 :
~(~0UL << (n+1)) - 创建从 m 到 n 的掩码 :
(~0UL << m) & ~(~0UL << (n+1)) - 设置位为 1 :
*ptr |= mask - 设置位为 0 :
*ptr &= ~mask
性能优化
- 情况 2 使用直接赋值 :对于完整的
long单元,直接赋值比逐位操作快得多 - 减少循环次数:三种情况分别处理,避免在循环中判断边界
- 位操作高效:使用位运算而非逐位循环,充分利用 CPU 的位操作指令
边界情况处理
extent = 0:length = low_index,只执行情况 1(如果low_index != 0)- 完全对齐 :如果
low_index = 0且extent是BITS_PER_LONG的倍数,只执行情况 2 - 单 long 内 :如果整个范围在一个
long内,只执行情况 1 或情况 3
该函数设计巧妙,能够高效处理任意起始位置和长度的位设置操作,同时保持代码简洁和性能优化。
7. 计算最大有效字节数
c
max_long = 0;
for (i = 0; i < IO_BITMAP_LONGS; i++)
if (t->io_bitmap_ptr[i] != ~0UL)
max_long = i;
bytes = (max_long + 1) * sizeof(long);
bytes_updated = max(bytes, t->io_bitmap_max);
t->io_bitmap_max = bytes;
优化策略:
- 查找最后一个不全为
~0UL(全 1,即全禁止)的long单元 - 这样可以只更新 TSS 中必要的部分,而不是整个 8KB 位图
bytes_updated用于确保更新时覆盖之前可能更大的范围
为什么需要这个优化:
- TSS 中的
io_bitmap需要与线程的位图保持同步 - 但不需要每次都复制整个 8KB,只需要复制有效部分
8. 设置延迟加载标志
c
tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET_LAZY;
延迟加载机制:
INVALID_IO_BITMAP_OFFSET_LAZY = 0x9000(特殊值)- 当 CPU 检测到这个值时,会在下一次 I/O 操作时触发异常
- 异常处理程序会重新加载正确的位图到 TSS
- 这是一种"懒加载"(lazy loading)优化,避免每次修改都立即更新 TSS
为什么使用延迟加载:
- TSS 更新是相对昂贵的操作
- 通过延迟加载,可以将多个
ioperm()调用的更新合并到一次 TSS 更新中
9. 恢复抢占
c
put_cpu();
return 0;
put_cpu(): 恢复内核抢占- 返回 0 表示成功
相关数据结构
thread_struct
c
struct thread_struct {
// ... 其他字段 ...
/* IO permissions */
unsigned long *io_bitmap_ptr; // 指向 I/O 权限位图的指针
unsigned long io_bitmap_max; // 位图中有效的最大字节数
};
tss_struct
c
struct tss_struct {
// ... 其他字段 ...
unsigned short trace, io_bitmap_base; // I/O 位图在 TSS 中的偏移
unsigned long io_bitmap[IO_BITMAP_LONGS + 1]; // I/O 权限位图
unsigned long io_bitmap_max; // 位图的最大有效字节数
struct thread_struct *io_bitmap_owner; // 拥有此位图的线程
};
注意 :io_bitmap 数组大小是 IO_BITMAP_LONGS + 1,额外的 1 是因为 CPU 会访问位图末尾之后的一个字节,这个字节必须全为 1。
相关常量定义
c
#define IO_BITMAP_BITS 65536 // 支持的 I/O 端口总数
#define IO_BITMAP_BYTES (IO_BITMAP_BITS/8) // 位图字节数:8192
#define IO_BITMAP_LONGS (IO_BITMAP_BYTES/sizeof(long)) // long 单元数
#define IO_BITMAP_OFFSET offsetof(struct tss_struct,io_bitmap) // 位图在 TSS 中的偏移
#define INVALID_IO_BITMAP_OFFSET 0x8000 // 无效位图偏移(禁用 I/O 位图)
#define INVALID_IO_BITMAP_OFFSET_LAZY 0x9000 // 延迟加载标志
工作流程总结
- 验证参数:检查端口范围是否有效
- 权限检查 :开启权限需要
CAP_SYS_RAWIO - 延迟初始化:首次调用时分配位图内存
- 禁用抢占:确保操作的原子性
- 更新位图:修改线程的 I/O 权限位图
- 计算范围:确定需要更新的最小范围
- 延迟加载:设置标志,让 CPU 在下次 I/O 操作时更新 TSS
- 恢复抢占:完成操作
使用场景
sys_ioperm 主要用于需要直接访问硬件 I/O 端口的程序,例如:
- 设备驱动程序开发
- 硬件调试工具
- 低级别的硬件控制程序
- 嵌入式系统开发
安全考虑
- 权限控制 :只有具有
CAP_SYS_RAWIO能力的进程才能开启 I/O 访问 - 范围限制:端口范围被限制在 0-65535
- 进程隔离:每个进程/线程有自己独立的 I/O 位图
- 默认禁止:默认情况下所有 I/O 端口都是禁止访问的
与 sys_iopl 的区别
- sys_ioperm: 用于控制特定 I/O 端口范围的访问权限(0-0x3FF),使用位图机制
- sys_iopl: 用于控制 I/O 特权级别(IOPL),影响所有 I/O 端口的访问,通过修改 EFLAGS 寄存器实现
sys_iopl 适用于需要访问超过 0x3FF 范围的端口,因为为所有 65536 个端口维护位图需要 8KB 内存,开销较大。
参考资料
- 源代码位置:
arch/i386/kernel/ioport.c - 相关头文件:
include/asm-i386/processor.h - Intel x86 架构手册:I/O 权限位图机制