Zev 逐梦,成长!

《Linux设备驱动程序》读书笔记

2017-10-17

《Linux设备驱动程序》读书笔记

第一章 设备驱动程序简介

  • 设备驱动程序的任务:将标准化的调用映射到作用于实际硬件的操作上,使得驱动程序独立于内核
  • 机制:需要提供什么功能;策略:如何使用这些功能
  • 驱动程序在于提供机制而不是策略
  • 编写访问硬件的内核代码时,不要给用户强加任何特定策略
  • 驱动程序应该处理如何使硬件可用的问题,而将如何使用硬件的问题留给上层应用程序
  • 驱动程序设计主要考虑的因素
    • 提供给用户尽量多的选项
    • 编写驱动程序要占用的时间
    • 尽量保持程序简单而不至于错误丛生
  • 许多设备驱动程序是同用户程序一起发行的
  • 内核功能划分
    • 进程管理
    • 内存管理
    • 文件系统
    • 设备控制
    • 网络功能
  • 模块:可在运行时添加到内核中的代码,包括但不限于设备驱动程序
  • Linux将设备分为三种类型,每个模块通常实现为其中的一种
    • 字符设备:能够像字节流(类似文件)一样被访问的设备;字符设备能够通过文件系统节点访问;与普通文件相比,大多数字符设备是一个只能顺序访问的数据通道
    • 块设备:进行I/O操作时一次只能传输一个或多个完成的块,每个块可以包含多个字节;能够通过文件系统节点访问
    • 网络接口:能够和其他主机交换数据的设备;由于不是面向流的设备,因此通过一个唯一的名字访问网络接口,但是在文件系统中不存在对应的节点
  • 安全问题
    • 只有具有特定权限的用户才能够加载模块到内核
    • 应当尽量避免在驱动程序代码中实现安全策略,安全策略最好由内核高层实现
    • 避免因为开发人员自身原因而引入安全问题:比如缓冲区溢出
    • 任何从用户进程得到的输入只有经过内核严格验证后才能使用
    • 任何从内核得到的内存都必须在提供给用户进程或者设备之前清零,防止信息泄露
    • 如果设备要解释和分析发送给它的数据,就必须确保用户不能将破坏系统的任何东西发送给它
    • 如果某些操作会影响整个系统,则应将此类操作限于特权用户
    • 小心使用第三方的软件
    • 内核可以编译为不支持模块方式;也可以通过权能机制禁止系统启动后加载内核模块

第二章 构造和运行模块

  • printk:具有默认优先级的消息可能不会输出在控制台上,因此需要指定高优先级;与printf相比缺乏对浮点数的支持
  • 内核模块与应用程序对比:
    • 每个内核模块的工作方式和事件驱动有点类似,模块在初始化函数中告诉内核它能完成什么工作,在退出函数中清除初始化函数做的一切
    • 模块仅仅被链接到内核,因此它能够调用的函数仅仅是由内核导出的那些函数
    • 没有任何函数库会和模块链接,因此模块源文件中不能包含通常的头文件,大多数相关的头文件在include/linux/和include/asm/下
    • 一个内核错误即使不影响整个系统,也至少会杀死当前进程
    • 模块运行在内核空间,应用程序运行在用户空间
  • 现代处理器都至少具有两个保护级别,在Unix中,内核运行在最高级别,此时称为内核态;应用程序运行在最低级别,称为用户态
  • 通常将运行模式称作内核空间和用户空间。这两个术语不仅说明两种模式具有不同的优先权等级,而且还说明每个模式都有自己的内存映射,也就是自己的地址空间
  • 每当应用程序执行系统调用或者被硬件中断挂起时,Unix将运行模式从用户空间切换到内核空间;执行系统调用的内核代码运行在进程上下文中,而处理硬件中断的内核代码和进程是异步的,与任何一个特定的进程无关
    • 执行系统调用时,用户空间的进程需要传递很多变量和参数给内核空间,内核也要保存用户进程的寄存器和变量,以便系统调用结束后继续执行进程,这些变量和参数称为进程上下文
    • 硬件的一些变量和参数也要传递给内核,这些变量和参数称为中断上下文
    • 当内核处于进程上下文是可抢占的,因为进程上下文存放在每个进程的内核栈中;当内核处于中断上下文是不可抢占的,因为每个处理器只有一个中断栈
  • 通常来说,一个驱动程序要执行两类任务:模块中的某些函数作为系统调用的一部分执行;而其他函数则负责中断处理
  • 内核编程需要考虑并发问题
    • 可能有多个进程同时使用同个驱动程序
    • 中断处理程序在异步运行着,可能会中断驱动程序
    • 有一些软件抽象(比如内核定时器)也在异步运行着
    • Linux支持SMP,可能同时有多个处理器运行着同个驱动程序
    • 内核是可抢占的
  • Linux内核代码包括驱动程序代码都必须是可重入的
  • 可重入:若一个子程序可以在任意时刻被中断然后操作系统调度执行另一段代码,这段代码又调用了该子程序不会出错,则称其为可重入
    • 可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的;按照后进先出的顺序执行完成
    • 线程安全强调多线程执行时能够正确地处理对共享资源的使用,一般通过加锁的方式实现;先获得锁的线程先执行完成
  • 可重入的条件:可重入函数使用的所有变量都保存在函数的栈上
    • 不能含有全局非常量数据
    • 不能返回全局非常量数据的地址
    • 只能处理由调用者提供的数据
    • 不能依赖于单实例模式资源的锁
    • 调用的函数也必须是可重入的
  • 可重入的内核不仅仅包含可重入的代码,也包含不可重入的代码,例如访问共享资源的代码就是不可重入的
  • 内核栈非常小,当需要创建大的结构体时应该采用动态分配的方式
  • 在内核API中具有双下划线的函数通常是接口的底层组件,应该谨慎使用
  • 内核代码不能实现浮点运算,因为代价太大
  • 因为内核源码对编译器做了大量假定,在编译内核之前通过查看内核源码目录下的Documentation/Changes查看对编译工具的要求
  • 在内核源码中,有且只有系统调用的名字前会带有sys_前缀
  • modprobe会将当前模块依赖的其他模块一起加载到内核,但是insmod不会这么做
  • rmmod从内核中移除模块
  • lsmod列出加载到内核中的所有模块
  • 模块代码必须针对要链接的每个版本的内核重新编译:在构造模块的过程中会在模块中加入当前内核的相关信息
  • 当模块装入内核后,模块导出的符号都会成为内核符号表的一部分
  • 模块层叠技术(Module Stacking):一个模块导出的符号被另一个模块使用
  • modprobe是处理层叠模块的一个实用工具。但是其只能从标准的已安装模块目录中搜索需要装入的模块
  • 在模块中导出的符号必须是全局的,存放在可执行文件的ELF段中
    • EXPORT_SYMBOL(name);
    • EXPORT_SYMBOL(name);
  • 如果一个模块没有显示地指明使用的许可证,那么就会被认为是Proprietary(专有)许可证
  • 初始化函数一般为static
  • 被__init和__init_data修饰函数和变量会在初始化完成后被扔掉以释放内存,在其他情况下调用都是非法的
  • 被__exit和__exit_data修饰的函数和变量仅用于模块卸载,在其他情况下调用都是非法的。如果内核不允许卸载模块,那么这些函数和变量都会被丢弃
  • 如果一个模块未定义清除函数,则内核不允许卸载该模块
  • 在初始化函数中注册设施时可能会失败,因此要做好错误恢复工作,释放已经成功注册的资源
    • 在追求效率的代码中使用goto来进行错误处理是最佳选择
    • 如果注册了很多设施,为了提高代码复用,可以调用cleanup函数来完成错误恢复工作,但此时的cleanup函数不能被__exit修饰
  • 在初始化函数还在运行的时候,内核完全有可能会调用已经注册的设施,因此要注册一个设施之前要确保这个设施已经初始化完成
  • 如果成功注册的设施已经被内核所使用,但是初始化函数失败了,那么就要等待内核的对设施的操作完成后在撤销已经注册的设施
  • module_param:用于声明模块参数,必须放在函数体以外,通常在源文件头部;所有模块参数都应该给定默认值

第三章 字符设备驱动程序

  • 一般来说,一个主设备号对应一个驱动程序,一个次设备号对应具体一个设备
  • 分配和释放设备编号
    • int register_chrdev_region(dev_t first, unsigned int count, char *name);
    • int alloc_chardev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);,内核会分配一个恰当的主设备编号
    • void unregister_chardev_region(dev_t first, unsigned int count);,一般在清除函数中调用
  • 如果采用动态分配主设备号的方式,就无法预先创建对应的设备文件,因此需要写一个脚本根据分配到的主设备号创建设备文件,在卸载模块时删除设备文件
  • 内核在调用驱动程序的read和write方法前已经检查了权限,因此不必在方法内部检查权限
  • 驱动程序涉及的数据结构
    • inode:文件在内核中的表示形式,一个文件只有一个inode
    • file:文件描述符,一个文件可能对应多个文件描述符
    • file_operations:封装了文件的操作,每个file结构中都包含一个fops
  • 字符设备注册
    • 获得cdev结构体
      • 动态方式:struct cdev *cdev_alloc(void);之后还要手动设置fops
      • 静态方式:一定要调用void cdev_init(struct cdev *cdev, struct file_operations *fops);进行初始化
      • 无论哪种方式都要设置模块的所有者owner
    • 注册设备
      • int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
      • 可能会注册失败,但是一旦注册成功内核就有可能马上使用这个设备,因此在设备准备就绪前不要注册
    • 移除设备
      • void cdev_del(struct cdev *dev);
      • 移除设备之后不能再访问dev结构体了
  • open方法进行的主要操作:
    • 检查设备特定的错误
    • 如果设备是首次打开,则对其进行初始化
    • 如果有必要,更新f_op指针
    • 分配并填写置于flip->private_data里的数据结构
  • release方法进行的主要操作:
    • 释放由open分配的,保存在filp->private_data中的所有内容
    • 在最后一次关闭操作时关闭设备
    • 每个file结构维护着引用计数器,只有在最后一次close调用时才会调用release
  • kfree只能够释放由kmalloc分配的内存,kfree允许接收NULL
  • 内核不能够直接引用用户空间的指针
    • 用户空间指针指向的内存可能无法被映射到内核空间
    • 因为用户空间的内存是分页的,在内核中访问可能缺页,将导致该系统调用的进程死亡
    • 盲目使用用户空间提供的指针,可能导致系统出现后门
  • 内核提供了访问用户空间的函数
    • unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
    • unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
    • 两个函数可能会导致睡眠,驱动程序要保证可重入性
    • 两个函数会检查指针的有效性,如果在拷贝过程中遇到无效地址,则仅仅复制部分数据
    • 如果不需要检查指针的有效性,则可以使用__copy_to_user和__copy_from_user
  • pwrite和pread系统调用不会修改文件的位置,它们会丢弃掉驱动程序对*offp所做的修改
  • read和write出错时返回错误码(负值),否则返回实际传输的字节数;如果数据传输过程中发生了错误,驱动程序应该先返回成功传输的字节数,将错误状态保存到下一次调用时返回
    • 用户空间程序需要通过查看errno变量来获取错误状态
  • read方法的返回值规则
    • 等于read系统调用的count参数,说明传输成功完成
    • 为正数但是小于count,说明只有部分数据传输成功;一般情况下程序会重新读取数据;可以利用这个规则简短代码
    • 为0说明到达文件尾
    • 为负值说明发生了错误,具体的错误码定义在linux/errno.h中
  • write方法的返回值规则
    • 等于count,说明传输成功完成
    • 为正数但是小于count,说明只有部分数据传输成功;一般情况下程序会重新读取数据;可以利用这个规则简短代码
    • 为0意味着什么也没写入
    • 为负值说明发生了错误,具体的错误码定义在linux/errno.h中
  • 向量化的读写系统调用readv和writev,如果驱动程序没有实现readv和writev方法,那么内核会通过read和write方法模拟它们,但是效率有所降低

第四章 调试技术

  • 通过配置内核可以开启内核的调试功能
  • 通过打印调试
    • printk(KERN_DEBUG “message\n”);//KERN_DEBUG会被替换成一个字符串,预处理器会将两个字符串拼接在一起,所以中间是一个空格而不是逗号
    • printk只有遇到newline符号时才会将消息刷新到控制台
    • 日志优先级会被宏展开成”<num>“的形式,num的取值为0-7,数字越小优先级越高
    • 未指定优先级的printk采用默认优先级DEFAULT_MESSAGE_LOGLEVEL
    • 无论console_loglevel设定为何值,dmesg都会显示所有日志级别的log
    • /dev/console:可以在启动时设置为制定的虚拟控制台,默认为/dev/tty0
    • /dev/tty0:指向当前的虚拟控制台
    • /dev/tty[1-n]:表示不同的虚拟控制台
    • /dev/tty:表示当前的tty设备,包括虚拟控制台,伪终端(例如ssh或者xterm)
    • 可以通过ioctl来重定向控制台消息
    • printk将日志写到一个循环缓冲区中,然后唤醒正在等待日志的进程,当循环缓冲区满时,会覆盖旧的日志
    • 在可能会重复的日志的printk前调用printk_ratelimit来限制打印的速度
    • 打印设备编号
      • int print_dev_t(char*, dev_t);
      • char *format_dev_t(char*, dev_t)
  • 通过查询调试
    • 使用/proc文件
      • 实现int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
      • 创建/proc文件:struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void * data);
      • 删除/proc文件:remove_proc_entry(const char *name, struct proc_dir_entry *parent);
    • 使用/proc文件的缺点
      • 没有引用计数,在删除文件的时候可能文件正在被使用
      • 内核在创建文件时不会检查重名问题,因此可能存在重名文件
    • seq_file接口:使用原来的/proc接口传输大文件不是那么方便
    • 通过create_proc_entry创建/proc文件
    • 为刚创建的/proc文件设置file_operations,只要自己实现open操作即可,在open函数中调用seq_open设置seq_operations
    • 实现seq_operations中的start,show,next,stop方法
    • 通过remove_proc_entry删除/proc文件
    • ioctl方法
      • 不要求将数据分割成不超过一个页面大小
    • 在调试被禁用时,用来取得信息的命令仍然可以保留在驱动程序中
  • 通过监视调试
    • strace用于监视从用户空间发出的所有系统调用
    • strace从内核中接收信息,因此无论程序在编译时是否加入了-g选项,都可以被跟踪
    • strace也可以跟踪一个正在运行的进程
  • 调试系统故障
    • 某些oops可能会导致kernel panic
    • 一般驱动程序出现问题只会导致正在使用驱动程序的进程被终止,内核会在进程被终止时调用其打开的所有设备的close方法关闭设备
    • 显示oops错误信息由失效处理函数(arch/*/kernel/traps.c)的printk语句产生
    • opps中比较有用的信息:EIP(出错指令地址);调用栈;slab毒剂值
    • 系统挂起:对于单处理器不可抢占的内核来说,如果内核代码进入死循环,就会停止调度,系统不会再响应任何动作
      • 通过插入schedule调用可以防止死循环,但是会引入代码重入问题,可以通过加入适当的锁解决重入问题,但是驱动程序持有自旋锁时不能调用schedule
    • 如果驱动程序进入了死循环但是系统仍然正常运行,可以考虑使用内核剖析功能
    • 在复现系统挂起故障时,将所有的磁盘以只读的方式挂载、不挂载或者通过NFS访问的方式来避免破坏文件系统或者使文件系统处于不一致状态
  • 调试器和相关工具:一步步跟踪代码非常耗时,应该尽量避免
    • 使用gdb:只能print变量的信息
      • 使用命令gdb /usr/src/linux/vmlinux /proc/kcore启动gdb
      • vmlinux是未压缩的linux内核
      • vmlinuz是经过压缩的linux内核,有用于小内核的zImage和大内核的bzImage两种
    • 通过core-file /proc/kcore命令刷新gdb缓存,但是读取新的数据时不需要刷新缓存
    • 调试内核时,gdb不能够修改内核变量;不能设置断点;不能单步跟踪内核函数
    • 打开CONFIG_DEBUG_INFO使gdb能够使用内核的符号信息
    • kdb补丁,可以获得更多调试功能
    • kgdb补丁,不像前两种方式,kgdb运行调试内核的系统和运行调试器的系统是隔离的
    • 用户模式的Linux(UML):使得linux内核成为一个运行在linux系统之上的、独立的用户模式的进程,缺点是无法访问主机系统的硬件
    • Linux跟踪工具包
    • 动态探测

第五章 并发和竞态

  • 避免竞态的基本原理
    • 只要可能,就应该避免资源的共享(全局变量不是资源共享的唯一方式,只要将一个指针传递给了内核的其他部分就可能产生)
    • 硬规则:确保一次只有一个执行线程可操作共享资源
    • 在对象尙不能正确工作时,不能将其对内核可用
  • 临界区:在任意给定的时刻,代码只能被一个线程执行
  • down_interruptible是down的可中断版本,作为通常的规则,我们应该总是使用可中断的操作,非中断操作是建立不可杀进程的好方法
  • 如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放信号量
  • 如果down_interruptible被用户中断,可以选择返回
    • -ERESTARTSYS:如果能够撤销任何用户可见的修改,系统调用就能够正确重试
    • -EINTR:如果不能撤销
  • rwsem:写者优先?
  • completion:相比信号量是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成
    • 注意wait_for_completion执行一个非中断等待
    • 典型使用是模块退出时的内核线程终止
  • 自旋锁:一个自旋锁是一个互斥设备,它只有锁定和解锁两个值
    • 实际自旋锁的实现与具体的架构有关
    • 虽然自旋锁最初是为了SMP而设计的,但是在抢占式的单处理器系统上仍需要实现正确的锁定。在非抢占式的单处理器系统上自旋锁被优化为不做什么
    • 所有自旋锁等待在本质上都是不可中断的
    • 使用自旋锁的规则:
      • 核心规则:任何拥有自旋锁的代码都必须是原子的,不能够睡眠
      • 只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止,甚至在单处理器系统上
      • 在拥有锁时避免睡眠是很难做到的,因为许多内核函数可以睡眠但是没有很好的文档说明:比如在内核空间和用户空间复制数据、需要分配内存的任何操作kmalloc等,因此在编写自旋锁下的代码时必须注意调用的每个函数
      • 如果和中断例程通过自旋锁共享资源,我们在持有自旋锁时就要禁止本地中断
      • 持有自旋锁的时间尽可能短
    • 必须在同一个函数中调用spin_lock_irqsave和spin_unlock_irqrestore,否则代码在某些架构上会出现问题
  • rwlock_t:读者写者自旋锁,和rwsem类似,写者优先?
  • 锁陷阱
    • 不明确的规则:提供给外部调用的函数必须显式地获取锁;因为信号量和自旋锁都不能够重复获得,所以内部静态函数可以假定已经获得了锁
    • 锁的顺序规则:首先获取自己的局部锁,再获取内核更加中心的锁;必须先获取信号量再获取自旋锁
    • 细粒度锁和粗粒度锁
      • 大内核锁使得整个内核成为一个临界区,任何时刻只有一个CPU能够执行内核代码,降低了SMP的效率
    • 我们应该在最初使用粗粒度的锁,除非有真正的原因相信竞争会导致问题。我们需要抑制过早考虑优化的欲望,因为真正的性能约束通常出现在非预期的情况
    • 可以通过lockmeter补丁查看锁竞争导致的性能下降
  • 除了锁以外的办法
    • 免锁算法:只有一个生产者和一个消费者时,可以构造免锁的循环缓冲区,内核有一个通用的循环缓冲区在<linux/kfifo.h>
    • 原子变量:如果共享的资源恰好是一个整数,注意多个原子变量的操作不是原子的
    • 原子位操作:大多数现代代码不会使用位操作来管理一个锁变量
    • seqlock:读取访问通过获得一个整数值进入临界区,在退出时该整数值会和当前值比较,如果不同则重新进行操作;写入锁是自旋锁实现的
    • RCU锁:在正确使用时可以获得较高的性能

第六章 高级字符驱动程序操作

  • ioctl(int fd, unsigned long cmd, ...);后面的可选参数通常为char*类型或者整形
    • 虽然很多内核开发者都吐槽ioctl,但是对于设备的操作来说,它还是一个最直接的选择
    • 对于驱动程序来说,无论用户传递的可选参数是什么类型,看到的都是unsigned long类型
    • 要恰当地选择ioctl命令编号,避免与系统预定义的命令冲突
    • 从任何一个系统调用返回时,正的的返回值是受保护的,而负值则被认为是一个错误,并被用来设置用户空间的errno变量
    • switch语句的default返回值,POSIX标准规定返回-ENOTTY,然而返回-EINVAL是很普遍的做法
    • 为了向后兼容,fcntl保留了下来,它和ioctl很类似
    • 使用ioctl参数,如果是指针类型时就必须小心了
      • 使用copy_to_user和copy_from_user
    • 但是考虑到ioctl一般传输的是小数据,为了效率可以使用access_ok搭配put_user,__put_user,get_user,__get_user使用
      • 通过capable()检查用户的特权级别,POSIX权能机制细分了root特权,避免suid的问题
  • 非ioctl的设备控制
    • 通过写入控制序列的方式非常适合那种不传输数据只响应命令的设备,比如机器人
  • 阻塞型I/O
    • 休眠:当进程进入休眠状态时,会被标记为一种特殊状态并从调度器的运行队列中移走,直到某些情况下修改了这个状态,进程才会在任意CPU上调度
    • 安全休眠规则:
      • 永远不要在原子上下文中休眠:不能在拥有自旋锁、seqlock、RCU锁时休眠,如果禁止了中断,也不能休眠,拥有信号量时可以休眠
    • 对唤醒之后的状态不能作任何假定,因此必须检查等待的条件是否真正为真
    • 简单休眠wait_event:如果使用可中断版本,当被中断时,驱动程序应该返回-ERESTARTSYS
    • 约定的做法是使用wake_up唤醒wait_event,wake_up_interruptible唤醒wait_event_interruptible
  • 阻塞和非阻塞型的操作
    • 驱动程序通过查看flip->f_flags中的O_NONBLOCK标志决定是否是非阻塞I/O,默认该标志是清除的,O_NDELAY是它的别名,用户只能在open文件时指定
    • Unix标准语义的阻塞型操作
      • read调用在没有数据时会被阻塞,一旦有数据哪怕少于count指定的数目也要立即返回
    • write调用在没有缓冲空间时会被阻塞,一旦有空间哪怕少于count指定的数目也要立即返回
    • 在驱动程序中实现输出缓冲区可以提高性能,这得益于减少了上下文切换和用户级/内核级转换次数
    • 如果是非阻塞I/O,在数据没有准备好时驱动程序立即返回-EAGAIN
    • 只有read、write和open操作会受到O_NONBLOCK标志的影响,非阻塞的open操作会在设备初始化需要很长一段时间时返回-EAGAIN
  • 在驱动程序内部,阻塞在read调用的进程会在数据到来时被唤醒:通常硬件会发出一个中断来通知这个事件,作为中断处理的一部分,驱动程序会唤醒等待的进程
  • 高级休眠
    • 进程如何休眠
      • 分配并初始化一个wait_queue_t结构,将其加入到对应的等待队列中
    • 设置进程的状态,将其标记为不可运行状态TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE
      • 可以通过set_current_state修改状态,但是注意修改状态只会影响调度器在下一次调度时对该进程的调度方式,不会马上使进程进入休眠状态
    • 检查等待条件是否为真,因为此时进程还没有休眠,如果条件为真,进程可以直接被唤醒,不会丢失唤醒信号
    • 调用schedule()让出CPU
    • 被唤醒后,如果是从schedule返回就不需要设置进程的状态,如果没有进入休眠就被唤醒就要修改进程的状态为TASK_RUNNING。然后将进程从等待队列中移走
    • 手工休眠
      • 通过DEFINE_WAIT()建立等待队列入口
    • 通过prepare_to_wait()加入等待队列
    • 判断等待条件是否为真,如果确实需要休眠调用schedule
    • 调用finish_wait()执行清理工作:设置进程的状态以及从等待队列中移除
    • 调用signal_pending()判断是否接收到信号
    • 独占等待:为了解决现实世界的“疯狂兽群行为”,wake_up会唤醒所有等待进程,大家一起蜂拥而上,最终只有一条进程获得资源,其余继续休眠~
      • 等待队列入口设置了WQ_FLAG_EXCLUSIVE标志时,则会被添加到等待队列尾部,没有这个标志的会被添加到头部
    • 调用wake_up时会唤醒第一个具有该标志的进程之后停止唤醒其他进程,这意味着所有的非独占等待的进程都会被唤醒
    • 执行独占等待通过prepare_to_wait,wait_event及其变种无法执行独占等待,prepare_to_wait_exclusive可以帮我们设置等待队列入口项的独占标志位
    • 唤醒的相关细节:将进程标志设置为TASK_RUNNING,根据具体的唤醒函数决定是否进行上下文切换
      • wait_up()和wait_up_interruptible()如果不是处于原子上下文中,函数在返回前会让唤醒的一个或多个进程被调度
    • 永远不要使用sleep_on,其将会被移除内核
  • poll、select和epoll:允许进程决定是否可以对一个或多个打开文件做非阻塞的读取或者写入。这些调用会阻塞进程,直到至少其中一个文件可以读取或者写入
    • 驱动程序需要实现poll方法
      • 在一个或多个可以指示poll状态变化的等待队列上调用poll_wait,如果当前没有文件可以用来执行I/O,将阻塞当前进程
    • 返回一个描述操作是否可以无阻塞执行的位掩码
    • 与read和write的交互

写在最后

通过这段时间的学习,我明白了linux驱动开发的基本流程:实现硬件对应的设备文件的各种操作,如open,read,write,release等,并且通过加锁防止出现各种同步问题。因为如今驱动开发的需求比较小, 所以我在了解了驱动的基本原理后决定先暂停对驱动的学习。期待下次与你的相会。 Zev 2017/10/17


Comments

Content