5.输入输出设备

设备管理

  • 用设备控制器屏蔽设备差异
    • I/O 设备多种多样, 通过设备控制器范文设备( 类似代理商 )
    • 控制器像小电脑, 有芯片和寄存器, CPU 可通过读写寄存器访问设备
    • I/O 设备可分为两类: 块设备, 信息存于块中, 有自己的地址, 例如硬盘; 字符设备, 信息为字节流, 无法寻址, 例如鼠标.
    • 块设备控制器有缓冲区, 数据缓冲区即内存映射 I/O; 控制器寄存器有 I/O 端口, 可通过汇编指令操作.
    • 如何通知设备操作已完成:
      • 轮询检测控制器寄存器的状态标志位
      • 中断通知, 通过硬件中断控制器通知 CPU; ( 而软中断是在代码中调用 INT, 可触发系统调用 )
    • DMA 功能, 设备在 CPU 不参与下, 自行完成内存读写; 由 DMA 协调控制磁盘控制器, 并发送中断通知 CPU 操作完成
  • 驱动程序屏蔽设备控制器差异
    • 设备控制器不属于操作系统的一部分; 而驱动程序属于, 可以被内核代码调用.
    • 驱动程序应有统一的接口, 中断处理也在驱动里完成
    • 驱动初始化时, 注册中断处理函数; 中断统一出发 do_IRQ, 其找到注册的中断处理函数并执行
    • 对于块设备, 驱动与文件系统之间需要通用设备层; 通用设备层实现与块设备相关的通用逻辑, 位于与设备无关的操作


      image.png
  • 用文件系统接口屏蔽驱动程序的差异
    • 统一设备名称, 设备在 /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 可用于配置和修改设备信息.
image.png

字符设备

内核模块

  • 驱动程序的内核模块,以 ko 的文件形式存在,可以通过 insmod 加载到内核中
  • 一个内核模块应该由以下几部分组成
    • 头文件部分:include
    • 定义以内科模块处理逻辑的函数,如开、关、读写及响应中断。
    • 定义一个 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 函数,真正打开设备


        image.png

写入字符设备

  • 写入字符设备
    • 调用文件系统标准接口 write,参数为设备的文件描述符
    • 由于已经将 file_operations 替换成了设备的,所以会直接调用设备定义的 write(多态)


      image.png

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 执行不同操作


        image.png

设备中断处理

  • 设备中断处理
    • 定义中断处理函数: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


        image.png
- 硬件 中断处理
    - 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 并执行
image.png

块设备

初始化

  • 所有的块设备被一个 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。
image.png

IO操作

  • 对于块设备的 I/O 操作分为两种,一种是直接 I/O,另一种是缓存 I/O。无论是哪种 I/O,最终都会调用 submit_bio 提交块设备 I/O 请求。
  • 对于每一种块设备,都有一个 gendisk 表示这个设备,它有一个请求队列,这个队列是一系列的 request 对象。
  • 每个 request 对象里面包含多个 BIO 对象,指向 page_cache。所谓的写入块设备,I/O 就是将 page_cache 里面的数据写入硬盘。


    image.png
  • 对于请求队列来讲,还有两个函数,一个函数叫 make_request_fn 函数,用于将请求放入队列。
  • submit_bio 会调用 generic_make_request,然后调用这个函数。另一个函数往往在设备驱动程序里实现,我们叫 request_fn 函数,它用于从队列里面取出请求来,写入外部设备。


    image.png

你可能感兴趣的:(5.输入输出设备)