《Linux设备驱动程序》笔记

第一章–设备驱动程序简介(P9)

什么是Linux设备驱动程序(P9)

linux驱动处于OS和硬件之间。
驱动给OS提供一组设备驱动接口函数(包括open,close,read,ioctl等)

为什么研究编写Linux驱动程序(P9)

  • 新硬件问世或硬件过时
  • 个人用户要了解一些驱动程序知识才能访问设备
  • 硬件厂商通过提供Linux驱动程序能为自己的产品带来潜在用户群
  • Linux开源,驱动程序源码可以在大量用户中迅速流传

设备驱动程序的作用(P10)

  1. 驱动的作用在于提供机制,而不是提供策略(驱动灵活)
    • 机制:需要提供什么功能
    • 策略:如何使用这些功能
  2. 介于应用程序和实际设备之间,设计驱动时综合考虑三方面:给用户提供尽可能多的选项,编写驱动占用的时间,尽量保持程序简单而不至于错误丛生
  3. 不带策略的驱动的典型特征:同时支持同步和异步,能被多次打开,充分利用硬件特性,不具备用来“简化任务”的或提供与策略相关的软件层

内核功能划分(P12)

  1. 进程管理:在单个或多个CPU上实现了多个进程的抽象
  2. 内存管理:内核的不同部分在和内存管理子系统交互时使用一组函数调用
  3. 文件系统:Linux支持多种文件系统类型,也就是在物理介质上组织数据的不同方式
  4. 设备控制:除了处理器和内存等少数对象外,所有设备控制操作都由驱动完成
  5. 网络功能:大部分网络操作和具体进程无关。系统负责在应用程序和网络接口之间传递数据包,并根据网络活动控制程序的执行。所有的路由和地址解析问题都由内核处理。

设备和模块的分类(P14)

  1. 字符模块:能像字节流(类似文件)一样被访问的设备。字符设备驱动通常至少要实现open,close,read和write系统调用。
  2. 块模块:每次只能传输一个或多个完整的块(512KB或更大),和字符驱动相比具有完全不同的接口。
  3. 网络模块:网络接口可以是硬件设备,也可能是个纯软件设备(回环接口loopback)。只负责发送和接受数据包,eth0在文件系统中不存在对应的节点

驱动安全策略(P16)

  • 系统中所有安全检查都是由内核代码进行的,系统调用init_module会检查调用进程是否具有将模块装载到内核的权利。
  • 尽量避免在驱动代码中实现安全策略。由驱动本身完成的相关安全检查:能影响全局资源的设备操作(设置中断线),可能会破坏硬件(装载固件),影响其他用户(给磁盘驱动器设置默认的块尺寸),这些都只能由root执行

第二章–构造和运行模块(P21)

核心模块与应用程序的对比(P24)

  • 内核模块都是事件驱动的,模块只是预先注册自己以便服务于将来的某个请求。
  • 内核模块能调用的函数仅仅是由内核导出的那些函数,而不存在任何可链接的函数库。printk函数缺少对浮点数的支持。
  • 处理错误的方式不同,应用程序中的段错误是无害的,总是可以使用调试器跟踪到源代码中的问题所在,而一个内核错误即使不影响整个系统,也至少会杀死当前进程。

用户空间和内核空间(P25)

  • OS必须负责程序的独立操作并保护资源不受非法访问,在CPU中实现不同的操作模式(级别)。
  • 当应用程序执行系统调用或者被硬件中断挂起时(两者由驱动代码提供),Unix将执行模式从用户空间切换到内核空间。执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,能访问进程地址空间的所有数据,而处理硬件中断的内核代码和进程是异步的,与特定进程无关。

内核中的并发(P26)

原因:

  1. Linux系统中通常正在运行多个并发进程,并且可能有多个进程同时使用我们的驱动程序。
  2. 大多数设备能够中断处理器,而且中断处理程序可能在驱动正试图处理其他任务时被调用。
  3. 有一些软件抽象(如内核定时器)也在异步运行着。
  4. Linux还可以运行对称多处理器(SMP)系统上,可能同时有不止一个CPU运行着驱动。
  5. 内核代码是可抢占的,即使在单处理器系统上也存在类似多处理器系统的并发问题。

版本依赖(P31)

  • 加载模块时会将模块与当前内核树中的vermagic.o链接,用来检查模块和正在运行的内核的兼容性。

内核符号表(P33)

  • 模块被装入内核后,它所导出的任何符号都会变成内核符号表的一部分。
  • EXPORT_SYMBOL_GPL(name)导出的模块只能被GPL许可证下的模块使用。这个宏将被扩展为一个全局变量的声明,该变量在模块可执行文件的ELF段中保存。

初始化和关闭(P36)

1
2
3
4
static int __init record_init_module(void)
static void __exit record_exit_module(void)
module_init(record_init_module);
module_exit(record_exit_module);

__init表示仅在初始化使用;在模块装载结束后,模块装载器会将初始化函数扔掉。

只要你的模块依赖关系正确(可以通过depmod设置),你就可以通过modprobe 命令加载而不需要知道具体模块文件位置

1
2
3
4
5
6
#define __init __attribute__((__section__(".init.text")) __cold
将作用的函数或数据放入指定名为".init.text"的输入段。
当内核启动完毕后,.init.text段中的内存会被释放掉供其他使用。

输入段和输出段是相对于要生成最终的elf或binary时的Link过程说的,Link过程的输入大都是由源代码编绎生成的
目标文件.o,那么这些.o文件中包含的段相对link过程来说就是输入段,而Link的输出一般是可执行文件elf或库等

只有内核未被配置为支持热拔插设备时,__devinit和__devinitdata才会被翻译为__init和__initdata(用于数据定义)

能够注册的设施包括:串口,杂项设备,sysfs入口,/proc文件,可执行域和线路规程(line discipline)

如果一个模块未定义清除函数,则内核不允许卸载该模块。

初始化过程中的错误处理(P37)

错误恢复的处理有时使用goto语句比较有效。
Linux中,错误编码是定义在中的负整数。

模块装载竞争(P40)

在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施。

模块参数化(P40)

1
2
module_param(bugfile, charp, S_IRUGO);
module_param_array(name, type, num, perm)

如果我们需要的类型不在内核支持的模块参数类型里,模块代码中的钩子可让我们来定义这些类型。

所有的模块参数都应该给定一个默认值,insmod只会在用户明确设置了参数的值的情况下才会改变参数的值。

最后一个参数perm是访问许可值,它用来控制谁能访问sysfs中对模块参数的表述:

  • 0表示不会有对应的sysfs入口项;否则模块参数会在/sys/module中出现,并设置为给定的访问许可。
  • S_IRUGO,任何人均可读取该参数,但不能修改
  • S_IRUGO|S_IWUSR,允许root用户修改该参数。

在用户空间编写驱动程序(P42)

用户空间驱动程序的优缺点

第三章–字符设备驱动程序(P46)

主设备号和次设备号(P47)

  • 对字符设备的访问是通过文件系统内的设备名称进行的。那些名称被称为特殊文件、设备文件,或者简单称之为文件系统树的节点。通常位于/dev目录、
  • ls - l的修改日期前用逗号分隔的两个数,主设备号, 从设备号
  • 尽管现代的Linux内核允许多个驱动程序共享主设备号,但大部分是“一个主设备号对应一个驱动程序”。次设备号用来后的一个指向内核设备的直接指针。
  • dev_t(在中定义)用来保存设备编号,12+20位。使用中定义的宏获得:
    • MAJOR(dev_t dev)
    • MINOR(dev_t dev)
    • MKDEV(int major, int minor)

分配和释放设备编号(P49)

  • 静态获得设备编号:在中,int register_chrdev_region(dev_t first, unsigned int count, char* name),first为分配的设备编号的起始值,常为0,count为连续设备的个数,name为设备名称
  • 动态分配主设备号:alloc_chrdev_region
  • 释放设备编号:unregister_chrdev_region

动态分配主设备号(P50)

  • 动态分配的缺点:由于分配的主设备号不能保证始终不变,所以无法预先创建设备节点。但分配完设备号可从/proc/devices中读取
  • scull_load, scull_init

一些重要的数据结构(53P)

  • 大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是:file_operations,file,inode
  • file_operations(中)结构用来将驱动程序操作连接到设备编号,其中包含每个打开的文件和一组函数关联
  • file_operations结构或指向这类结构的指针称为fops,结构中的每个字段都必须指向驱动中实现特定操作的函数,对于不支持的操作字段置为NULL。
  • file_operations结构的字段介绍:

    1. owner 避免在模块的操作正在被使用时卸载该模块
    2. llseek 设置为NULL,调用seek将以不可预期的方式修改file结构中的位置计数器。
    3. read 设置为NULL,调用read将出错并返回-EINVAL
    4. aio_read 初始化一个异步的读取操作,设置为NULL将通过read(同步)处理
    5. write
    6. readdir 仅用于读取目录,只对文件系统有用
    7. poll 是poll,epoll,select这三个系统调用的后端实现
    8. ioctl 提供了一种执行设备特定命令(如格式化软盘)的方法。
    9. mmap 将设备内存映射到进程地址空间
    10. open 若为NULL,打开永远成功,但系统不会通知驱动
    11. flush 调用发生在进程关闭设备文件描述符副本的时候,它执行(并等待)设备上尚未完结的操作。
    12. release file结构被释放时调用,并不是每次调用close时都会被调用
  • file结构(在中)代表一个打开的文件,在open时创建,通常将该指针称为filp

  • inode结构在内部表示文件。对单个文件,可能有许多个表示打开的文件描述符的file结构,但是都指向单个inode结构。struct cdev是表示字符设备的内核的内部结构。用iminor和imajor来获取主从设备号,而不是直接操作i_rdev

字符设备的注册(P60)

1
2
3
4
5
6
7
8
9
10
11
12
<linux/cdev.h>
运行时获取一个独立的cdev结构:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;

将cdev结构嵌入到自己的设备特定结构中:
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);

cdev_del(&scull_devices[i].cdev);

open和release

open

  • 检查设备错误(设备未就绪)
  • 更新f_op指针,填写filp->private_data

release

  • 释放filp->private_data中的内容
  • 最后一次close时被调用
  • dup和fork系统调用都会在不掉用open的情况下创建已打开文件的副本。
  • flush方法在应用程序每次调用close时都会被调用

read和write

积累

  • Unix图形显示器的管理分为X服务器+窗口和会话管理器
  • 软驱的驱动程序不带策略,它的作用是将磁盘表示为一个连续的数据块阵列。系统高层提供策略
  • 许多驱动是同用户程序一起发行的:比如,用来调整并口打印机驱动程序工作方式的tunelp程序;作为PCMCIA驱动程序包一部分的图形化cardctl工具。还会有一个客户程序库,它提供不必在驱动本身实现的功能
  • 不同进程之间的通信方式:信号,管道,进程间通信原语
  • 磁盘可以格式化为符合Linux标准的ext3文件系统,也可以格式化为常用的FAT文件系统或者其他种类
  • 字符终端(/dev/console)和串口(/dev/ttys0以及类似设备)是字符设备。字符设备可以通过文件系统节点来访问,比如/dev/tty1和/dev/lp0。
  • 偶数编号的内核版本(如2.6.x)是正式发行的稳定版本,而奇数编号的版本(如2.7.x)则是开发过程中的一个快照,将很快被下一个开发版本不同。
  • 如何将内核内存映射到用户空间(即mmap系统调用),如何将用户内存映射到内核空间(即get_user_pages),如何将这两种内存映射到设备空间(执行DMA操作)
  • 竞态问题:不同的执行顺序导致不同的,非预期行为发生。
  • current是一个执行struct task_struct的指针,current隐藏在内核栈中,便于current被频繁引用。
  • 内核具有很小的栈,可能只有一个4096字节大小的页那么小,我们自己的函数必须和整个内核空间调用链一同共享这个栈。
  • insmod依赖于定义在kernel/module.c中的一个系统调用,函数sys_init_module给模块分配内核内存(vmalloc负责内存分配)
  • 系统调用的名字前都带有sys_前缀
  • modprobe也用来将模块加载到内核,但若模块引用了内核中不存在的符号时,它会在当前模块搜索路径(标准的已安装模块目录)中查找定义了这些符号的其他模块,并将这些模块也装载到内核。而insmod则会失败,提示”unresolved symbols”。
  • lsmod读取sysfs虚拟文件系统中/proc/modules虚拟文件
  • 查看系统日志文件(/var/log/messages或者系统配置使用的文件)
  • IA-32(Intel Architecture 32 bit),常被称为i386,x86-32,x86。
  • SCSI接口:系统级接口的独立处理器标准。SCSI使驱动器和计算机内部安装的SCSI适配器进行通信。
  • 通过sysfs访问设备信息,在/sys目录下反映了这个机器的系统状况。sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。
  • 不应该将非kmalloc返回的指针传递个kfree,但是将NULL传递给kfree是合法的