1、 linux设备驱动是以内核模块的方式而存在的,在具体的驱动开发中将驱动编译为模块具有很到的工程意义。因为如果将正在开发中的驱动编译如内核,而开发过程中会不断修改驱动代码,则需要不断的编译和重启Linux,但是如果直接编译为模块则只需要rmmod与insmod即可,开发效率大大提高。
2、 linux系统的用户空间编程有两种方法,即通过linux API和通过C库函数访问文件。用户空间看不到设备驱动,能看到的只有设备对应的文件,因此文件编程即是用户空间的设备编程。
Linux按照功能对文件系统的目录进行了良好的规划。/dev是设备文件存放的目录,devfs和udev分别是linux2.4和linux2.6设备生成的设备文件节点方法,前者运行于内核空间后者运行于用户空间。linux2.6设备通过一些列的数据结构定义了设备模型,设备模型与sysfs文件系统中的目录和文件存在一种对应关系,udev可以利用sysfs中记录的信息定义规则并提取主次设备号动态创建/dev文件设备。3、 字符设备是三大设备(字符设备、块设备、网络设备)中较简单的一类设备,器驱动程序中完成的主要功能是初始化、添加和删除dev结构体,申请和释放设备号,以及填充file_operations结构体中的操作函数,实现file_operations结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。
4、 并发态和竞态广泛存在,中断屏蔽、原子操作、自旋锁和信号量都是解决并发问题的机制。中断屏蔽很少被单独使用,院子操作只针对整型进行,因此自旋锁和信号量应用最为广泛。
自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小。信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁和信号量,它允许多个执行单元对共享资源并读开发。5、 阻塞与非阻塞访问时I/O操作的两种不同的模式,前者在I/O操作不可进行时会让进程睡眠。
在设备驱动中阻塞I/O一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞I/O也可以借助轮询函数来查询设备是否立即被访问,用户空间调用select()函数和poll()接口,设备驱动提供poll()函数。设备驱动的poll()函数本身不会阻塞,但是poll()和select()系统调用则会阻塞的等待文件描述符至少一个可访问或超时。6、 使用信号量可以实现设备驱动与用户程序之间的异步通知,总体而言,设备驱动和用户空间要分别完成以下工作:用户空间设置文件的拥有者、FASYNC标志及捕获信号,内核空间响应对文件对文件的拥有者、FASYNC标志的设置,并在资源可获得是释放信号。
linux2.6内核包含对AIO的支持为用户空间提供统一的异步I/O接口。在AIO中,信号和回调函数是实现内核空间对用户空间应用程序通知的两种机制。7、 硬中断是是外部设备对CPU的中断、软中断通常是硬中断服务程序对内核的中断,而信号则是有内核(或其他进程)对某个进程的中断。
linux内核中断处理分为两个半部,顶半部处理紧急硬件操作,底半部处理不紧急的耗时操作。Tasklet和工作队列都是调度底半部的良好机制,tasklet基于软中断实现。内核定时器也依靠软中断实现。内核中的延时是忙等待或者睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延时时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的。8、 Malloc()的内存一定要被free(),否则会造成内存泄露。理想情况下,malloc()与free()应该成对出现,即谁申请就由谁释放。
外设可处于CPU的内存空间或者I/O空间,除X86外嵌入式处理器一般只存在内存空间。在Linux系统中,为I/O内存和I/O端口的访问提供了一套统一的方法,访问流程一般为“申请资源—>映射—>访问—>去映射—>释放资源”。对于有MMU的处理器而言,linux系统的内部布局比较复杂,可直接映射的物理内存称为常规内存,超出部分为高端内存。Kmalloc()和__get_free_pages()申请的内存在物理上连续,而vmalloc()申请的内存在物理上不连续。DMA操作可能导致Cache的不一致问题,因此,对于DMA缓冲,应该用dma_alloc_coherent()等方法申请。在DMA操作中涉及总线地址、物理地址和虚拟地址等概念,区分这3类地址非常重要。Linux内核中对DMA通道的申请和释放采用了和中断类似的方法。9、 TTY设备驱动的主体工作围绕tty_driver这个结构体的成员函数展开,主要实现其中的数据发送和接收流程以及tty设备线路设置函数。
针对串口,内核实现了内核核心层,这个层实现了串口设备通用的tty_driver。因此,串口设备驱动的主体工作从tty_driver移动到uart_driver。10、块设备的I/O操作方式与字符设备存在较大不同,因而引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的是“请求”,字符设备的I/O操作则是直接进行不绕弯,块设备的I/O操作会排队和整合。
驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心是请求处理函数或“制造请求”函数。尽管块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不在包含读写一类的成员函数,而只是包含打开、释放及I/O控制等与具体读写无关的函数。块设备驱动的结构相当复杂,但幸运的是,块设备不像字符设备那样包罗万象,它通常就是存储设备,而且驱动的主体已经由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及的工作往往只是编写少量的与硬件直接交互的代码。11、linux I2C驱动体系结构相当复杂,它主要分为3个部分组成,即I2C核心、I2C总线驱动和I2C设备驱动。I2C核心是2C总线驱动和I2C设备驱动的中间枢纽,它以通用的、与平台无关的接口实现了I2C设备与适配器的沟通。I2C总线驱动填充i2c_adapter和i2c_algorithm结构体,I2C设备驱动填充i2c_driver和i2c_client结构体。
另外,系统中的i2c-dev.c文件定义的主设备号为89的设备可以方便的给用程序提供读写I2C设备寄存器的能力,使得工程师大多时候不需要为具体的I2C设备驱动定义文件操作接口。工程师在计I2C设备驱动程序的时候,并不一定要遵守linux I2C驱动体系结构,完全可以把它当做一个普通的字符设备来处理。
12、在音频设备驱动中,几乎必须使用DMA,而DMA缓冲区会被分成一个一个的段,每次DMA操作进行期中的一段。
13、帧缓冲设备是一种典型的字符设备,它统一了显存,将显示缓冲区直接映射到用户空间。帧缓冲设备驱动file_operations中VFS接口函数由fbmem.c文件统一实现。这样,驱动工程师的工作重点将是实现针对特定设备fb_info中的fb_ops的成员函数,另外,理解并灵活的修改fb_info中的var和fix参数非常关键。fb_info中的var参数直接和LCD控制器的硬件设置以及LCD屏幕对应。