字符设备驱动
本文将介绍一个 linux 下字符设备驱动的简单实现,代码基于 Linux Device Driver 的 scull 驱动,介绍该字符设备的注册、打开、写入数据、读出数据等操作,所有示例代码可以在我的 Github 中找到。
字符设备的定义
Linux 对于设备是通过面向对象思想实现的,对于一个字符设备,通过数据结构 struct cdev 来描述。cdev 定义于 <linux/cdev.h> 中,其中最关键的是 file_operations 结构,它是实现字符设备的操作集。
|
|
我们实现的 scull 通过数据结构 struct scull_dev 表示,scull_dev类是继承 cdev类。因 C 语言没有定义面向对象的语法,在这里使用嵌套实现。这样就得到我们的设备模型 struct scull_dev,data 表示我们的硬件设备的起始地址,而 size 则表示这个硬件设备最大的存储空间。
|
|
下面列出了 cdev 的一些操作方法:
|
|
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
|
|
动态内存定义初始化:
|
|
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的需求而定。
字符设备的操作
scull_dev 的属性已经定义好了,接下来要定义其方法,即如何操作该设备。对于字符设备 Linux,Linux 使用 file_operations 结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个系统调用。
|
|
对于我们要实现的 scull,需要提供 open、write、read、lseek 等操作。
|
|
open
open 函数有两个参数,一个指向 inode 的指针,一个指向 file 的指针:
|
|
inode表示期望打开的设备节点,在其成员中有个一个指向字符设备cdev的指针i_cdev,我们可以通过该指针得到指向 scull 设备的指针(借助container_of宏实现)file用于表示将要打开的设备描述,包含期望打开文件的标志f_flags(读写标志)。我们需要使用指向设备的指针,初始化 file 成员private_data,该成员指向的是将要打开的设备。
|
|
release
release 函数参数与 open 相同:
|
|
由于我们要实现的 scull 设备一直存在于内存中,无需任何释放动作,因此只需要返回 0 即可。
|
|
write
wirte 函数用于向设备写入数据,其参数含义如下:
- 第一个参数表示打开的文件,即 open 中的 file
- 第二个参数是指向用户空间,期望写入数据的初始地址
- 第三个指针是期望写入的数据长度
- 第四个指针是当前写的位置,write 结束时,需要更新该指针指向的数据
- write 函数的返回值为实际写入数据的长度
|
|
由于驱动程序处在内核空间,内核空间是不能直接读取用户地址空间的数据,用户空间也不能读取内核地址空间的数据,我们需要借用函数 copy_from_user() ,从用户空间将数据拷贝的内核空间。
|
|
read
read 函数,用于读取设备中的数据,参数与 write 相类似,同样需要借助 copy_to_user() 函数,将数据从内核空间拷贝的用户空间。
|
|
下面是 scull 字符设备实现的 read 函数:
|
|
llseek
llseek函数,用于设置当前读写的位置
|
|
其函数参数含义如下:
- 第一个参数为 file
- 第二个参数为偏移量,
- 第三个参数是描述第二个参数偏移量的参考位置,只有三种有效值
- SEEK_SET(0,相对于文件起始位置的偏移量)
- SEEK_CUR(1,相对于当前所处位置的偏移量)
- SEEK_END(2,相对于文件末的偏移量)
|
|
字符设备注册销毁
scull_dev 设备的类已经构造完成,接下来就需要使用该类构建设备驱动模块。首先我们需要定义设备驱动模块的初始化函数 scull_init()。通过宏控 module_init 告诉内核指定模块的初始化函数。初始化函数需要完成如下操作:
设备号的申请与释放
一个字符设备或块设备都有一个主设备号和一个次设备号:
- 主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。
- 次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
内核通过设备号 dev_t来区分不同的设备,下面列出了操作设备号的一些宏:
|
|
linux 提供了一系列的函数来申请和释放主设备号和次设备号:
|
|
对应代码如下:
|
|
字符设备的注册
根据我们自定义的 scull_dev,对其初始化:动态分配一个 scull_dev 对象,初始化其成员 data 与 size
|
|
对于 cdev 成员,需要初始化其成员 ops,该成员是指向 struct file_operations,即包含了操作该设备方法的指针,这里采用的是动态初始化 cdev 方法:
|
|
接下来需要通过 cdev_add() 函数将 scull_dev 和它对应的设备号添加到设备列表中
|
|
自动创建设备节点
基于 udev 机制可以实现设备节点自动创建,在驱动用加入对 udev 的支持需要的工作是:
- 在驱动初始化的代码里调用
class_create为该设备创建一个struct class结构体 - 为每个设备调用
device_create创建对应的设备内核中定义的struct device结构体,并在 sysfs 中注册
加载模块的时候,用户空间中的 udev 会自动响应 device_create 函数,去 sysfs下寻找对应的类从而创建设备节点
|
|
对应的示例代码如下:
|
|
字符设备的销毁
scull_exit() 通过宏控 module_exit 告诉内核模块清除函数,清除函数需要将设备从设备列表中移除,释放动态分配的内存以及注销使用的设备号。
|
|
这里 clean_up_scull_data 释放 scull 字符设备申请的内存资源:
|
|
测试验证
使用以下 Makefile 文件编译好驱动程序 scull.ko
|
|
可以看到,在 /dev 目录下创建了 6 个字符设备,他们 major 相同,minor 不同:
|
|
编写一个测试的用户程序来读写字符设备:
|
|
编译运行,显示如下:
|
|
查看系统日志信息:
|
|
参考资料
-
No backlinks found.