sys_ioperm 函数详解

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 = &current->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 的 TSSper_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_maxio_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;

变量含义

  1. bitmap_base : 指向包含起始位的 unsigned long 单元的指针

    • base / BITS_PER_LONG: 计算起始位所在的 long 单元索引
    • 例如:如果 BITS_PER_LONG = 32base = 50,则 bitmap_base = bitmap + 1(第 2 个 long)
  2. low_index : 起始位在当前 long 单元内的偏移(0 到 BITS_PER_LONG-1)

    • base & (BITS_PER_LONG-1): 等价于 base % BITS_PER_LONG
    • 例如:base = 50BITS_PER_LONG = 32,则 low_index = 50 & 31 = 18
  3. length: 从起始位到结束位的总长度(包括起始位所在 long 的剩余部分)

    • length = low_index + extent
    • 例如:low_index = 18extent = 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

详细分析

  1. 创建起始掩码

    c 复制代码
    mask = (~0UL << low_index);
    • ~0UL: 全 1 的 unsigned long(例如:0xFFFFFFFF)
    • << low_index: 左移 low_index 位,低位补 0
    • 结果:从 low_index 位开始到最高位都是 1
    • 例如:low_index = 18mask = 0xFFFFC000(32位系统)
  2. 处理范围完全在一个 long 内的情况

    c 复制代码
    if (length < BITS_PER_LONG)
        mask &= ~(~0UL << length);
    • 如果整个范围都在第一个 long 内,需要截断掩码
    • ~(~0UL << length): 创建从 0 到 length-1 位全为 1 的掩码
    • mask &= ...: 只保留从 low_indexlength-1 的位
    • 例如:low_index = 18length = 25,最终 mask 只覆盖位 18-24
  3. 应用掩码

    c 复制代码
    if (new_value)
        *bitmap_base++ |= mask;   // 设置为 1
    else
        *bitmap_base++ &= ~mask;  // 设置为 0
    • 使用按位或(|=)设置位为 1
    • 使用按位与取反(&= ~mask)清除位(设置为 0)
    • bitmap_base++: 处理完后移动到下一个 long 单元
  4. 更新剩余长度

    c 复制代码
    length -= BITS_PER_LONG;
    • 减去已处理的位数(一个完整的 long

示例 1base = 18extent = 10BITS_PER_LONG = 32

  • low_index = 18
  • length = 18 + 10 = 28 < 32
  • mask = (~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 单元(一个或多个)

详细分析

  1. 创建完整掩码

    c 复制代码
    mask = (new_value ? ~0UL : 0UL);
    • 如果 new_value = 1mask = ~0UL(全 1)
    • 如果 new_value = 0mask = 0UL(全 0)
  2. 批量设置完整的 long 单元

    c 复制代码
    while (length >= BITS_PER_LONG) {
        *bitmap_base++ = mask;
        length -= BITS_PER_LONG;
    }
    • 直接赋值整个 long 单元,效率最高
    • 循环处理所有完整的 long 单元
    • 每处理一个,length 减少 BITS_PER_LONG

示例 2base = 18extent = 100BITS_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 单元

详细分析

  1. 创建末尾掩码

    c 复制代码
    mask = ~(~0UL << length);
    • ~0UL << length: 从 length 位开始到最高位全为 1
    • ~(...): 取反,得到从 0 到 length-1 位全为 1 的掩码
    • 例如:length = 10mask = 0x000003FF(32位系统,覆盖低 10 位)
  2. 应用掩码

    c 复制代码
    if (new_value)
        *bitmap_base++ |= mask;   // 设置为 1
    else
        *bitmap_base++ &= ~mask;  // 设置为 0
    • 与情况 1 相同的逻辑,但这里是从 0 位开始

示例 3base = 0extent = 10BITS_PER_LONG = 32

  • low_index = 0,情况 1 不执行
  • length = 10 < 32,情况 2 不执行
  • 情况 3 处理:mask = 0x000003FF,设置位 0-9

完整示例

假设 BITS_PER_LONG = 32(32位系统),设置 base = 18extent = 100new_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. 情况 1

    • low_index = 18
    • mask = (~0UL << 18) = 0xFFFFC000(位 18-31 为 1)
    • *bitmap_base++ &= ~mask:清除位 18-31
    • length = 118 - 32 = 86
  2. 情况 2

    • mask = 0UL(全 0)
    • 循环 2 次:long[1] = 0long[2] = 0
    • length = 86 - 64 = 22
  3. 情况 3

    • mask = ~(~0UL << 22) = 0x003FFFFF(低 22 位为 1)
    • *bitmap_base++ &= ~mask:清除位 96-117

位操作技巧总结

  1. 创建从位置 n 开始的掩码~0UL << n
  2. 创建到位置 n 结束的掩码~(~0UL << (n+1))
  3. 创建从 m 到 n 的掩码(~0UL << m) & ~(~0UL << (n+1))
  4. 设置位为 1*ptr |= mask
  5. 设置位为 0*ptr &= ~mask

性能优化

  • 情况 2 使用直接赋值 :对于完整的 long 单元,直接赋值比逐位操作快得多
  • 减少循环次数:三种情况分别处理,避免在循环中判断边界
  • 位操作高效:使用位运算而非逐位循环,充分利用 CPU 的位操作指令

边界情况处理

  • extent = 0length = low_index,只执行情况 1(如果 low_index != 0
  • 完全对齐 :如果 low_index = 0extentBITS_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  // 延迟加载标志

工作流程总结

  1. 验证参数:检查端口范围是否有效
  2. 权限检查 :开启权限需要 CAP_SYS_RAWIO
  3. 延迟初始化:首次调用时分配位图内存
  4. 禁用抢占:确保操作的原子性
  5. 更新位图:修改线程的 I/O 权限位图
  6. 计算范围:确定需要更新的最小范围
  7. 延迟加载:设置标志,让 CPU 在下次 I/O 操作时更新 TSS
  8. 恢复抢占:完成操作

使用场景

sys_ioperm 主要用于需要直接访问硬件 I/O 端口的程序,例如:

  • 设备驱动程序开发
  • 硬件调试工具
  • 低级别的硬件控制程序
  • 嵌入式系统开发

安全考虑

  1. 权限控制 :只有具有 CAP_SYS_RAWIO 能力的进程才能开启 I/O 访问
  2. 范围限制:端口范围被限制在 0-65535
  3. 进程隔离:每个进程/线程有自己独立的 I/O 位图
  4. 默认禁止:默认情况下所有 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 权限位图机制
相关推荐
Dovis(誓平步青云)1 小时前
《从内核视角看 Linux:环形缓冲区 + 线程池的生产消费模型实现》
linux·运维·服务器
Cincoze-Johnny1 小时前
Linux系统-应用问题全面剖析Ⅳ:德承工控机MD-3000在Ubuntu操作系统下[TPM功能]设置教程
linux·运维·ubuntu
默|笙1 小时前
【Linux】进程(1)
linux·运维·服务器
某林2121 小时前
基于ROS2与EKF的四轮差速机器人里程计精度优化:解决建图漂移与重影问题
linux·stm32·嵌入式硬件·slam·智能小车
dragoooon341 小时前
[Linux网络基础——Lesson7.「传输层协议 UDP 与 TCP」]
linux·网络·udp
红袜子i1 小时前
解决 Ubuntu 中 apt-get update 因架构配置混乱导致的更新失败问题
linux·ubuntu·架构
Xの哲學1 小时前
Linux 分段卸载技术深度剖析
linux·服务器·网络·架构·边缘计算
Charles Shan1 小时前
Mac上的linux虚拟机踩坑日记
linux·macos
zengshitang5201 小时前
ACRN 实战应用:在一台电脑上同时安装Windows10、Ubuntu22.04、Ubuntu PREEMPT_RT实时系统并流畅运行
linux·运维·ubuntu