设备管理
- 用设备控制器屏蔽设备差异
- I/O 设备多种多样, 通过设备控制器范文设备( 类似代理商 )
- 控制器像小电脑, 有芯片和寄存器, CPU 可通过读写寄存器访问设备
- I/O 设备可分为两类: 块设备, 信息存于块中, 有自己的地址, 例如硬盘; 字符设备, 信息为字节流, 无法寻址, 例如鼠标.
- 块设备控制器有缓冲区, 数据缓冲区即内存映射 I/O; 控制器寄存器有 I/O 端口, 可通过汇编指令操作.
- 如何通知设备操作已完成:
- 轮询检测控制器寄存器的状态标志位
- 中断通知, 通过硬件中断控制器通知 CPU; ( 而软中断是在代码中调用 INT, 可触发系统调用 )
- DMA 功能, 设备在 CPU 不参与下, 自行完成内存读写; 由 DMA 协调控制磁盘控制器, 并发送中断通知 CPU 操作完成
- 驱动程序屏蔽设备控制器差异
- 设备控制器不属于操作系统的一部分; 而驱动程序属于, 可以被内核代码调用.
- 驱动程序应有统一的接口, 中断处理也在驱动里完成
- 驱动初始化时, 注册中断处理函数; 中断统一出发 do_IRQ, 其找到注册的中断处理函数并执行
-
对于块设备, 驱动与文件系统之间需要通用设备层; 通用设备层实现与块设备相关的通用逻辑, 位于与设备无关的操作
- 用文件系统接口屏蔽驱动程序的差异
- 统一设备名称, 设备在 /dev/ 下有特殊设备文件, 其有 inode 但不关联存储介质数据, 只建立与驱动的连接; /dev/ 是在 devtmpfs 文件系统下, c→字符设备文件, b→块设备文件; 设备号: 主设备号(驱动程序), 次设备号(相应的单元); 可对设备文件使用文件的操作命令
- 添加新设备, 需要安装驱动( Linux 中即加载一个内核模块 ), 用 lsmod 查看加载的内核模块, 可通过 insmod 安装; 有了驱动, 可用 mkmod 在 /dev/ 下创建设备文件.
- 或 /sys/sysfs 中是实际设备数的反映
- /sys/devices 所有设备层次结构
- /sys/dev char block 中用设备号链接到 /sys/devices 中
- /sys/block 所有块设备
- 守护进程 udev
- 内核检测到新设备插入, 或创建 kobject 对象, 通过 sysfs 展现给用户, 并发送热插拔消息, udev 监听到消息并在 /dev/ 中创建设备文件
- ioctl 可用于配置和修改设备信息.
字符设备
内核模块
- 驱动程序的内核模块,以 ko 的文件形式存在,可以通过 insmod 加载到内核中
- 一个内核模块应该由以下几部分组成
- 头文件部分:include <linux/module.h> 及 <linux/init.h>
- 定义以内科模块处理逻辑的函数,如开、关、读写及响应中断。
- 定义一个 file_operations 接口,使得对上层接口统一
- 定义整个模块的初始化和退出函数
- 调用 module_init 和 module_exit,分别指向上面两个初始化函数和退出函数
- 声明一下 lisense,调用 MODULE_LICENSE
打开字符设备
- 打开字符设备
- 注册字符设备:通过 insmod 加载进内核
- 调用 __register_chrdev_region
- 注册设备的主次设备号和名称
- 初始化 cdev 结构体,将其 ops 成员指向设备定义的 file_operations
- 调用 cdev_add 将设备添加到内核中的 cdev_map,统一管理字符设备
- 创建设备文件:通过 mknod 在 /dev 下面创建一个设备文件
- 找到设备文件所在的文件夹,然后为这个新创建的设备文件创建一个 dentry,用于关联文件和 inode
- 创建特殊 inode,用于关联设备(还可关联FIFO文件、socket等)
- 打开设备文件:调用 inode 的 open 函数
- 如果 cdev 还没有关联,从 cdev_map 中找到 cdev 并关联
- 找到 cdev 的 file_operations,将其设置给文件描述符
-
调用设备驱动程序的 file_operations 的 open 函数,真正打开设备
- 注册字符设备:通过 insmod 加载进内核
写入字符设备
- 写入字符设备
- 调用文件系统标准接口 write,参数为设备的文件描述符
-
由于已经将 file_operations 替换成了设备的,所以会直接调用设备定义的 write(多态)
IOCTL控制设备
- 发送 IOCTL 信令控制设备
- cmd 组成(32位):
- 最低 8 位为 NR,是命令号;
- 然后 8 位是 TYPE,是类型;
- 然后 14 位是参数的大小;
- 最高 2 位是 DIR,是方向,表示写入、读出,还是读写。
- 有对应的宏方便操作 cmd
- 调用 do_vfs_ioctl,分支判断 cmd 执行对应操作,分为以下几种
- 默认定义好的 cmd,执行系统默认操作
- 普通文件,调用 file_ioctl
- 其他文件调用 vfs_ioctl
-
vfs_ioctl 内部还是会直接调用设备定义的 cmd 对应的接收函数,里面对不同 cmd 执行不同操作
- cmd 组成(32位):
设备中断处理
- 设备中断处理
- 定义中断处理函数:irq_handler_t
- 函数入参
- int irq:中断信号
- void * dev_id:通用指针,主要用于区分同一个中断处理函数对于不同设备的处理
- 返回值
- IRQ_NONE:设备不是中断接收者
- IRQ_HANDLED:处理完了的中断
- IRQ_WAKE_THREAD:有一个进程正在等待这个中断,中断处理完了,应该唤醒它
- 很多中断处理程序将整个中断要做的事情分成两部分,称为上半部和下半部,或者成为关键处理部分和延迟处理部分。在中断处理函数中,仅仅处理关键部分,完成了就将中断信号打开,使得新的中断可以进来,需要比较长时间处理的部分,也即延迟部分,往往通过工作队列等方式慢慢处理。
- 函数入参
- 注册中断处理函数:request_irq
- 函数入参
- unsigned int irq 是中断信号
- irq_handler_t handler 是中断处理函数
- unsigned long flags 是一些标识位
- const char *name 是设备名称
- void *dev 这个通用指针应该和中断处理函数的 void *dev 相对应
- 初始化描述中断的结构体 irq_desc,其中 struct irqaction,用于表示处理这个中断的动作,irqaction 都有以下成员
- 中断处理函数 handler
- void *dev_id 为设备 id
- irq 为中断信号
- next 为指向下一个 action 的链表指针
- 如果中断处理函数在单独的线程运行,则有 thread_fn 是线程的执行函数,thread 是线程的 task_struct
- irpaction 的存储数据结构通过宏 CONFIG_SPARSE_IRQ 配置
- 如果为连续下标则使用数组
- 如果为不连续下标则使用基数树
- irq 并不是真正的、物理的中断信号,而是一个抽象的、虚拟的中断信号
- 内部调用 request_threaded_irq->__setup_irq
- 查找 irq_desc 是否已经有 irqaction
- irq 有一个 next 的参数,如果已经有同类的 action,则将其挂在链表末尾
- 如果设定了以单独的线程运行中断处理函数,setup_irq_thread 就会创建这个内核线程,wake_up_process 会唤醒它
- 函数入参
- 中断处理流程
- 外部设备给中断控制器发送物理中断信号
- 中断控制器将物理中断信号转换成为中断向量 interrupt vector,发给各个 CPU
- 每个 CPU 都会有一个中断向量表,根据 interrupt vector 调用一个 IRQ 处理函数。注意这里的 IRQ 处理函数还不是咱们上面指定的 irq_handler_t,到这一层还是 CPU 硬件的要求
-
在 IRQ 处理函数中,将 interrupt vector 转化为抽象中断层的中断信号 irq,调用中断信号 irq 对应的中断描述结构里面的 irq_handler_t
- 定义中断处理函数:irq_handler_t
- 硬件 中断处理
- CPU 能够处理的中断总共 256 个,用宏 NR_VECTOR 或者 FIRST_SYSTEM_VECTOR 表示
- CPU 硬件要求每一个 CPU 都有一个中断向量表 idt_table,通过 load_idt 加载,里面记录着每一个中断对应的处理函数
- 中断被分为几个部分
- 0 到 31 的前 32 位是系统陷入或者系统异常,这些错误无法屏蔽,一定要处理;中断向量表中已经填好了前 32 位,外加一位 32 位系统调用
- 其他的都是用于设备中断
- 硬件中断的处理函数是 do_IRQ 进行统一处理,在这里会让中断向量,通过 vector_irq 映射为 irq_desc
- 找到注册的中断处理 action 并执行
块设备
初始化
- 所有的块设备被一个 map 结构管理从 dev_t 到 gendisk 的映射;
- 所有的 block_device 表示的设备或者分区都在 bdev 文件系统的 inode 列表中;
- mknod 创建出来的块设备文件在 devtemfs 文件系统里面,特殊 inode 里面有块设备号;
- mount 一个块设备上的文件系统,调用这个文件系统的 mount 接口;
- 通过按照 /dev/xxx 在文件系统 devtmpfs 文件系统上搜索到特殊 inode,得到块设备号;
- 根据特殊 inode 里面的 dev_t 在 bdev 文件系统里面找到 inode;
- 根据 bdev 文件系统上的 inode 找到对应的 block_device,根据 dev_t 在 map 中找到 gendisk,将两者关联起来;
- 找到 block_device 后打开设备,调用和 block_device 关联的 gendisk 里面的 block_device_operations 打开设备;
- 创建被 mount 的文件系统的 super_block。
IO操作
- 对于块设备的 I/O 操作分为两种,一种是直接 I/O,另一种是缓存 I/O。无论是哪种 I/O,最终都会调用 submit_bio 提交块设备 I/O 请求。
- 对于每一种块设备,都有一个 gendisk 表示这个设备,它有一个请求队列,这个队列是一系列的 request 对象。
-
每个 request 对象里面包含多个 BIO 对象,指向 page_cache。所谓的写入块设备,I/O 就是将 page_cache 里面的数据写入硬盘。
- 对于请求队列来讲,还有两个函数,一个函数叫 make_request_fn 函数,用于将请求放入队列。
-
submit_bio 会调用 generic_make_request,然后调用这个函数。另一个函数往往在设备驱动程序里实现,我们叫 request_fn 函数,它用于从队列里面取出请求来,写入外部设备。