IO access and Interrupts
实验目的:
- 与外围设备通信
- 实现中断处理程序
- 将中断与进程上下文同步
外围设备通过 Read/Write 其寄存器进行控制:
- 通常,设备具有多个寄存器,可以在内存地址空间或 I/O 地址空间中的连续地址访问这些寄存器
- 连接到 I/O 总线的每个设备都有一组 I/O 地址,称为 I/O 端口
- I/O 端口可以映射到物理内存地址,以便处理器可以通过直接与内存配合使用的指令与设备通信
- 为简单起见,我们将直接使用 I/O 端口(不映射到物理内存地址)与物理设备进行通信
每个器件的 I/O 端口被结构化为一组专用寄存器,以提供统一的编程接口,大多数设备将具有以下类型的寄存器:
- Control registers:接收设备命令
- Status registers:包含有关设备内部状态的信息
- Input registers:从设备中获取数据 - Read
- Output registers:在其中写入数据并传输给设备 - Write
Accessing the hardware
在 Linux 中,I/O 端口访问在所有体系结构上实现,并且可以使用多个 API
在访问 I/O 端口之前,首先必须请求访问它们,以确保只有一个用户在使用:
1 |
|
first
:IO 端口的基地址n
:IO 端口占用的范围name
:使用这段 IO 地址的设备名
要释放保留区域 resource
,必须使用以下函数:
1 | void release_region(unsigned long start, unsigned long n); |
使用案例如下:
1 |
|
所有端口请求都可以通过文件从用户空间看到:/proc/ioports
1 | root@qemux86:~ |
驱动程序获得所需的 I/O 端口范围后,可以在这些端口上执行读取或写入操作:
1 | unsigned inb(int port); /* reads one byte (8 bits) from port */ |
- 读取出来的字符并不是 ASCII,而是注册表值 scancode
- 我们只需要在按下时选择代码,然后解码 ASCII 字符
- PS:键盘 “按下时” 和 “松开时” 是两个不同的 scancode,后面的
is_key_press
用于展示这个特点
Interrupt handling
与其他资源一样,驱动程序必须先访问 Interrupt handling 中断处理程序,然后才能使用它,并在执行结束时释放它:
1 |
|
- 中断处理程序函数在中断上下文中执行,这意味着无法调用阻塞 API
- 必须避免在中断处理程序中执行大量工作,而是在需要时使用延迟工作
中断处理程序函数的签名:
1 | irqreturn_t (*handler)(int irq_no, void *dev_id); |
irq_no
:中断编号irqerturn_t
:标识返回信息- IRQ_NONE:中断不适用于此设备(共享中断)
- IRQ_HANDLED:中断可以直接在中断上下文中处理
- IRQ_WAKE_THREAD:计划进程上下文处理函数的运行
实例如下:
1 |
|
有关系统中断的信息和统计信息可以在 /proc/interrupt
或 /proc/stat
中找到
1 | root@qemux86:~ |
Locking
由于中断处理程序在中断上下文中运行,因此可以执行的操作受到限制:
- 无法访问用户空间内存
- 无法调用阻塞函数,因此不能使用互斥锁(中断发生时,程序会把当前进程的上下文保存到内核栈上,称为中断帧,如果在中断中发生阻塞,
schedule
新调用的进程很可能会破坏中断帧),其实这是为了实现中断嵌套所付出的代价 - 使用自旋锁进行同步也很棘手(如果所使用的自旋锁,已被正在运行的处理程序中断的进程获取,则可能导致死锁)
在某些情况下,设备驱动程序必须使用中断进行同步(例如,当数据在中断处理程序和进程上下文或下半部分处理程序之间共享时),在这些情况下,有必要停用中断并使用自旋锁:
1 | void spin_lock_irqsave (spinlock_t * lock, unsigned long flags); /* 保存中断的当前状态,禁止本地中断,获取指定的锁 */ |
为了使用在进程上下文和中断处理例程之间共享的资源,将按如下方式使用上述功能:
1 | static spinlock_t lock; |
- 因为系统硬中断
kbd_interrupt_handle
在任何时候都可以发生(内核会直接抢占原来的进程,从而执行硬中断) - 如果在
my_access
中拿了自旋锁之后被kbd_interrupt_handle
抢占,就会发生死锁 - 如果不在
kbd_interrupt_handle
中加锁,又可能会破坏共享数据(例如:在引用指针之前置空了指针) - 因此需要使用
spin_lock_irqsave
在加锁的同时禁止硬中断
Exercises
要解决练习,您需要执行以下步骤:
- 从模板准备 skeletons
- 构建模块
- 将模块复制到虚拟机
- 启动 VM 并在 VM 中测试模块
1 | make clean |
直接看完整代码:
1 |
|
- 注意:当
insmod
这个驱动程序时可能会报错
1 | root@qemux86:~ |
- 这是因为键盘 IO 已经有对应的驱动了:
1 | root@qemux86:~ |
- 键盘 I/O 端口是在引导期间由内核注册的,我们将无法删除关联的模块
- 因此,我们需要欺骗内核并注册 0x61 和 0x65 端口
1 | root@qemux86:~/skels/interrupts |
- 当中断注册完成以后,可以在
/proc/interrupts
中进行查看:
1 | root@qemux86:~/skels/interrupts |
- 通过如下命令可以在虚拟机中打开键盘:(用来查看
kbd_interrupt_handler
是否正确)
1 | QEMU_DISPLAY=gtk make boot |
- 被这个多线程和锁搞得头痛,有时候莫名其妙陷入死锁,尝试使用
printk
打印时还报错,看了答案以后进行了大刀阔斧的修改才解决了问题 - 另外
i8042_read_data
实际上是使用了系统自带的键盘 IO 端口(程序中自己申请的 IO 端口其实就是花架子,不影响程序流程) - 在程序输入输出的地方都需要用锁,但是如果在程序拿到锁的情况下发生中断,中断中也要拿同一个锁的情况下就会发生问题(硬件中断在任何时候都会发生)
1 | spin_lock_irqsave(&data->lock,flag); |
- 所以使用
spin_lock_irqsave
在加锁的同时禁止中断
仔细对比了答案以后,又分析了一下之前死锁的原因,感觉问题应该出在 cat
中:
kbd_open
正常执行kbd_read
有返回但是无限循环kbd_release
根本不会执行,程序卡死在kbd_read
中
我之前的代码是这样的:
1 | static ssize_t kbd_read(struct file *file, char __user *user_buffer, |
- 这个函数有个很明显的问题,就是 Read 的返回值不对(刚开始也没考虑这些问题)
- 把它修改为以下代码后就没有问题了:
1 | static ssize_t kbd_read(struct file *file, char __user *user_buffer, |