08.linux内核驱动架构

.ko文件的数据组织形式是elf格式(可重定位目标文件,executeable and linkable format)。如下图(忽略了program header table),摘自《深入linux设备驱动程序内核机制》p19

字符设备的 file_operations 直接由字符设备驱动提供,file_operations 也正是字符设备驱动的核心。
块设备有两种访问方法。一种是不通过文件系统直接访问裸设备,linux内核实现了统一的 def_blk_fops这一file_operations(fs/block_dev.c),当运行类似 dd if=/dev/sdb1 of=sdb1.img 时,linux正是走的这条path把整个裸分区复制到sdb.img。另一种是通过文件系统访问块设备,具体文件系统会实现 file_operations,负责把针对文件的读写转换为针对原始扇区的读写,块设备驱动是看不到 file_operations的。(摘自《linux设备驱动开发详解 基于linux4.0》P111)

<下面好几个操作都是 module_init 中应该做的>

WHAT happens when 分配设备号??

register_chrdev_region() 和 alloc_chrdev_region()两个函数用于为字符设备分配设备号,内部均调用 _register_chrdev_region(),传入 baseminor 和 count,将返回一个dev_t类型的设备号。_register_chrdev_region()使用 chrdevs[] (全局量)记录并管理设备号,使用major作为key哈希得到 hash_value 作为 index,然后遍历冲突链,查找设备号是否已经被占用(即major相同,minor有重叠),没有重叠则新建 char_device_struct 作为node插入链表中。因此总体来说,chrdev[] 存在的意义在于快速查看所要分配的设备号有没有已经被占用。设备号干嘛用?linux 以后多次将其用作 hash表的key!(《深入linux设备驱动内核机制》P69)

WHAT happens when cdev_add()??

cdev_add() 根据设备的设备号(调用add时提供)将struct cdev 哈希入 kobj_map 表,其中 cdev_map 是一个全局指针,在 chrdev_init() 时初始化。因此只要有 设备号,配合cdev_map指向的hash表,拿到设备相关的结构体轻而易举。我们实现的file_operations当然就在 cdev 里面啦。

WHAT happens when mknod()??

mknod()会调用 sys_mknod() 创建设备节点。设备节点最关键作用在于记录设备号、提供默认fops。流程如下(ext3 文件系统):
1、sys_mknod() 首先在根目录 / 下搜索 "dev"目录对应的 inode编号, sys_mknod可以通过 ext3文件系统会提供的某种映射机制通过inode编号获取(dev的)inode在内存中的实际地址。
2、sys_mknod调用 dev对应的 inode->i_op (ext3_dir_inode_operations)->mknod(ext3_mknod,用于创建特殊文件inode,eg.设备文件、命名管道、套接字)生成一个新的 inode(vfs相关)。
3、ext3_mknod()会调用init_special_inode()记录传下来的设备号(新 inode 的 i_rdev)并初始化 此 inode的 i_fop 成员(字符设备-->def_chr_fops, 块设备-->def_blk_fops,  fifo & sock 设备xxx)!  def_chr_fops提供了默认的 open (chrdev_open)操作,相当关键。def_blk_fops则是块设备一套操作原始扇区的函数集。

WHAT happens when open(/dev/xxx,flags,mode)??

open()函数通过调用sys_open()函数进入内核空间。open()-->sys_open()-->do_sys_open()完成下面动作:
1、alloc_fd()分配一个未使用的fd(作为index)
2、do_filp_open()查找/dev/xxx对应的inode(mknod时创建的,这一步还涉及到 vfs目录项那些东西),malloc struct file 并将地址填充到 进程维护的 struct file*pfd[]数组的第fd个成员中,继续填充 struct file 的 f_op 成员为 inode->i_fop 并调用 open() (即mknod时填充的那个 chrdev_open函数)
3、chrdev_open()首先通过 inode中记录的设备号在  cdev_map 指向的hash表中找到对应的设备 cdev, 并使用 inode->i_cdev记录(下次就不用去hash表查找 cdev 了)。然后使用cdev中的 file_operations (驱动程序作者实现) 覆盖 struct file 的 f_op 成员 并调用 open()。
4、后面再调用 read()、write()、ioctl() 时,通过fd找到 struct file,其f_op成员就对应了驱动作者所实现的 read()、write()、ioctl()了。

IOCTL 函数

当我们在驱动中实现了unlocked_ioctl()函数,并打开了设备后,用户层ioctl()即可通过系统调用最终调用到驱动中的ioctl函数(过程中可能会将用户层传进来的fd转换为struct file *)。
ioctl主要用来在用户空间和驱动模块之间以 (cmd, arg) 的形式传递控制信息,系统提供了一套规则用于确保编码出来的cmd在系统范围内具有唯一性(同时识别此cmd是否是本module定义的,此cmd是否是有效的)

copy_from_user()
copy_to_user()
本质还是memcpy(),不同之处在于添加了参数的检查,缺页异常的处理以及其他一些可能错误的修复。

字符设备 IO 模型

字符设备主要功能是用来实现IO操作(反映到用户层即 read() / write() 等相关操作),但设备读写速度与CPU读写速度存在差异,因此根据同步与异步、阻塞与非阻塞将字符设备IO模型分为四类。
同步与异步是指:发出一个功能调用,在没有得到结果前要不要返回?如果不管怎样,只要函数调用返回了,就一定要给我一个结果,那就是同步操作。阻塞与非阻塞是指:当设备暂时无法给出有效数据时(如read请求时设备缓冲区还没有有效数据或wirte请求时数据时缓冲区仍为满),是挂起等待?还是直接返回并报告这一现状?
 
同步阻塞IO:最常用,指驱动程序read/write方法中,当用户层发起read/write请求、但设备尚未ready时,wait_event_interruptible()挂起等待直到请求完成再返回。可以使用 等待队列 来实现(见 linux学习 1.7 linux内核同步)
 
同步非阻塞IO:指驱动程序read/write方法,当用户层使用 O_NONBLOCK 标志发起read/write请求、但设备尚未ready时,直接返回错误码报告 "设备尚未ready" 这一事件(直接返回 -EAGAIN即可,所以阻塞和非阻塞式 read/write其实是可以合二为一的,因为用户会用flags指示驱动程序使用哪一种)。
 
异步阻塞IO:驱动程序中实现poll方法时会用到,poll函数返回只代表设置完成,IO操作会阻塞在一组设备文件描述符上,当其中的某些设备描述符代表的设备读/写操作ready时,阻塞状态解除,用户便可以完成读写请求。原理如右(深入linux设备驱动内核机制 p253)
 
异步非阻塞IO:多用在块设备和网络设备的IO模型,读写请求被放在请求队列中由设备在后台异步完成,当设备完成请求时将通过信号或回调函数通知用户程序。

总线 设备 与驱动

sysfs,基于RAM的文件系统,取代了proc文件系统。
总线、设备和驱动是在kobject、kset等内核基础设施的基础上架构起来

参考:

《linux内核设计与实现》
《深入linux设备驱动程序内核机制》

猜你喜欢

转载自blog.csdn.net/ddddfang/article/details/88946100