一文带你掌握Linux字符设备架构

一、Linux设备分类
Linux系统为了管理方便,将设备分成三种基本类型:
字符设备
块设备
网络设备
字符设备:
字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性 。字符设备驱动程序通常至少要实现open、close、read和write的系统调用 。
字符终端(/dev/console)和串口(/dev/ttyS0以及类似设备)就是两个字符设备,它们能很好的说明“流”这种抽象概念 。
字符设备可以通过文件节点来访问,比如/dev/tty1和/dev/lp0等 。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道 。然而,也存在具有数据区特性的字符设备,访问它们时可前后移动访问位置 。例如framebuffer就是这样的一个设备,app可以用mmap或lseek访问抓取的整个图像 。
在/dev下执行ls -l ,可以看到很多创建好的设备节点:

一文带你掌握Linux字符设备架构

文章插图
通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件 。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来 。
1.当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备) 。还会分配一个struct file结构体 。
【一文带你掌握Linux字符设备架构】 2.根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序 。这里以字符设备为例 。在Linux操作系统中每个字符设备有一个struct cdev结构体 。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口 。
3.找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中 。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中 。
4.任务完成,VFS层会给应用层返回一个文件描述符(fd) 。这个fd是和struct file结构体对应的 。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了 。
三、字符驱动相关函数分析
/** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev,remembering @fops,making it ready to add to the * system with cdev_add() 。*/void cdev_init(struct cdev *cdev,const struct file_operations *fops)功能: 初始化cdev结构体参数: @cdev cdev结构体地址 @fops 操作字符设备的函数接口地址返回值: 无
/** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success,a negative error code on failure. */ int register_chrdev_region(dev_t from,unsigned count,const char *name)功能: 注册一个范围()的设备号参数: @from 设备号 @count 注册的设备个数 @name 设备的名字返回值: 成功返回0,失败返回错误码(负数)
/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system,making it * live immediately. A negative error code is returned on failure. */int cdev_add(struct cdev *p,dev_t dev,unsigned count)功能: 添加一个字符设备到操作系统参数: @p cdev结构体地址 @dev 设备号 @count 次设备号个数返回值: 成功返回0,失败返回错误码(负数)
/** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system,possibly freeing the structure * itself. */void cdev_del(struct cdev *p)功能: 从系统中删除一个字符设备参数: @p cdev结构体地址返回值: 无
static inline int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)功能: 注册或者分配设备号,并注册fops到cdev结构体,如果major》0,功能为注册该主设备号,如果major=0,功能为动态分配主设备号 。参数: @major : 主设备号 @name : 设备名称,执行 cat /proc/devices显示的名称 @fops : 文件系统的接口指针返回值 如果major》0 成功返回0,失败返回负的错误码 如果major=0 成功返回主设备号,失败返回负的错误码
该函数实现了对cdev的初始化和注册的封装,所以调用该函数之后就不需要自己操作cdev了 。
相对的注销函数为unregister_chrdev
static inline void unregister_chrdev(unsigned int major,const char *name)
四、如何编写字符设备驱动

一文带你掌握Linux字符设备架构

文章插图
《1》应用层ioctl参数分析
int ioctl(int fd,int cmd,。..);参数:@fd:打开设备文件的时候获得文件描述符 @ cmd:第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一@第三个参数: “ 。..”在C语言中,很多时候都被理解成可变参数 。返回值 成功:0 失败:-1,同时设置errno
小贴士:
当我们通过ioctl调用驱动层xxx_ioctl的时候,有三种情况可供选择:1: 不传递数据给xxx_ioctl 2: 传递数据给xxx_ioctl,希望它最终能把数据写入设备(例如:设置串口的波特率)3: 调用xxxx_ioctl希望获取设备的硬件参数(例如:获取当前串口设备的波特率)这三种情况中,有些时候需要传递数据,有些时候不需要传递数据 。在C语言中,是无法实现函数重载的 。那怎么办?用“ 。..”来欺骗编译器了,“ 。..”本来的意思是传递多参数 。在这里的意思是带一个参数还是不带参数 。参数可以传递整型值,也可以传递某块内存的地址,内核接口函数必须根据实际情况提取对应的信息 。
《2》驱动层xxx_ioctl参数分析
long (*unlocked_ioctl) (struct file *file,unsigned int cmd,unsigned long arg);参数:@file: vfs层为打开字符设备文件的进程创建的结构体,用于存放文件的动态信息 @ cmd: 用户空间传递的命令,可以根据不同的命令做不同的事情@第三个参数: 用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),也可能是一个数值,也可能没值返回值 成功:0 失败:带错误码的负值
《3》如何确定cmd 的值 。
该值主要用于区分命令的类型,虽然我只需要传递任意一个整型值即可,但是我们尽量按照内核规范要求,充分利用这32bite的空间,如果大家都没有规矩,又如何能成方圆?
现在我就来看看,在Linux 内核中这个cmd是如何设计的吧!

一文带你掌握Linux字符设备架构 一文带你掌握Linux字符设备架构

文章插图

    推荐阅读