字符驱动设备的另一种写法
字符驱动设备的另一种写法
零、常用函数、宏汇总
-
设备号相关
1
2
3
4
5
6 -
内核空间、用户空间之间的数据传递
内核空间–>用户空间
1
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
- to:用户空间指针
- from:内核空间指针
- n:从内核空间向用户空间拷贝数据的字节数
- 成功返回0,失败返回失败数目
用户空间–>内核空间
1
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
- to:内核空间指针
- from:用户空间指针
- n:从用户空间向内核空间拷贝数据的字节数
- 成功返回0,失败返回失败数目
一、register_chrdev
的缺点
之前注册字符设备用的如下函数注册字符设备驱动:
1 | register_chrdev(unsigned int major, const char *name,const struct file_operations *fops); |
但其实这个函数是linux版本2.4之前的注册方式,它的使用流程:
- 确定一个主设备号
- 构造一个
file_operations
结构体,传给该函数(本质是将该结构体放在chrdevs
数组中) - 调用
register_chrdev
函数将字符设备注册到系统中
当读写字符设备的时候,就会根据主设备号从chrdevs
数组中取出相应的结构体,并调用相应的处理函数。
缺点:
-
每注册一个字符设备,会连续注册256个次设备号,使它们绑定在同一个file_operations操作方法结构体上,会浪费很多资源。
-
最多只能有255个设备
改进后的优点:
- 通过一定区间长度的次设备号将file_operations操作方法结构体的数量限制住
- 通过主设备号和次设备号来查找设备,其中次设备号20位,主设备号12位,理论上最大支持4G个驱动程序
二、全新字符驱动设备的注册方法
在2.4版本后,内核里就加入了以下几个函数也可以来实现注册字符设备:
1、register_chrdev_region
指定设备编号时,采用该函数进行静态注册一个字符设备
1 | int register_chrdev_region(dev_t from, unsigned count, const char *name); |
from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
**count:**需要连续注册的次设备编号个数,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上
**name:**字符设备名称
当返回值小于0,表示注册失败
2、alloc_chrdev_region
未指定设备编号时,采用该函数进行动态注册一个字符设备,内核会返回申请到的设备号
1 | int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name); |
dev: 存放起始设备编号的指针,当注册成功, *dev就会等于分配到的起始设备编号,可以通过MAJOR()
和MINNOR()
宏来提取主次设备号
**baseminor:**次设备号基地址,也就是起始次设备号
**count:**需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2,表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上
**name:**字符设备名称
当返回值小于0,表示注册失败
3、cdev_init
初始化cdev
字符设备结构体,并将file_operations结构体放入cdev->ops
里。一般该函数用在驱动程序的入口函数中
1 | void cdev_init(struct cdev *cdev, const struct file_operations *fops); |
其中字符设备结构体cdev
的成员如下所示:
1 | struct cdev { |
其中可以通过MAJOR(cdev->dev)
,MINOR(cdev->dev)
来提取主次设备号,亦可以通过MKDEV(major,minor)
来将主设备号major和次设备号minor转换成dev_t类型变量dev
4、cdev_add
根据 cdev
字符设备结构体变量中的内容来向 Linux 系统添加字符设备。一般该函数用在驱动程序的入口函数中
1 | /*将cdev结构体添加到系统中,并将dev(注册好的设备编号)放入cdev-> dev里, count(次设备编号个数,即要添加的设备数量)放入cdev->count里*/ |
5、cdev_del
从 Linux 内核中删除相应的字符设备结构体。一般该函数用在驱动程序的出口函数中
1 | void cdev_del(struct cdev *p); |
6、unregister_chrdev_region
注销字符设备
1 | void unregister_chrdev_region(dev_t from, unsigned count); |
from: 注销的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
**count:**需要连续注销的次设备编号个数,比如: 起始次设备号为0,baseminor=100,表示注销掉0~99的次设备号
三、设备节点的自动创建
udev
是一个用户程序,在 Linux 下通过 udev
来实现设备文件的创建与删除, udev
可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。使用busybox 构建根文件系统的时候,busybox 会创建一个 udev
的简化版本——mdev
一般来说,在嵌入式系统中,热插拔事件也由 mdev 管理。在初始化脚本/etc/init.d/rcS
文件中加入如下语句可以将热插拔事件交由 mdev 来管理 :
1 | echo /sbin/mdev > /proc/sys/kernel/hotplug |
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add
函数后面添加自动创建设备节点相关代码
创建类
创建一个 class 类(其实就是个结构体),通过宏创建:
1 |
- owner:一般为
THIS_MODULE
- name:类名字
- 返回值:指向结构体 class 的指针
删除类
1 | void class_destroy(struct class *cls); |
创建设备
在类下创建一个设备就能够实现自动创建一个设备节点,使用 device_create
函数在类下面创建设备:
1 | int device_create(struct class *class, struct device *parent, dev_t devt, const char *fmt, ...) |
- class:设备要创建哪个类下面
- parent:父设备,一般为NULL
- devt:设备号
- fmt:设备名字(若fmt=xxx,则会生成/dev/xxx设备文件)
- 返回值:创建好的设备
删除设备
1 | void device_destroy(struct class *class, dev_t devt) |
- classs:要删除的设备所处的类
- devt:要删除的设备号
四、编写全新的字符驱动设备
通过调用上面的函数,构造两个不同的file_operations操作结构体,
次设备号0~1对应第一个file_operations,
次设备号2~3对应第二个file_operations,
然后在/dev/下,通过次设备号(0~4)创建5个设备节点, 利用应用程序打开这5个文件,看有什么现象
驱动代码
1 |
|
测试代码
1 |
|
运行测试
如下所示,挂载驱动后,通过 ls /dev/hello* -l
,看到创建了5个字符设备节点
1 | ls /dev/hello* -l |
接下来开始测试驱动,如下所示,
打开/dev/hello0时,调用的是驱动代码的操作结构体hello1_fops里的.open(),
打开/dev/hello2时,调用的是驱动代码的操作结构体hello2_fops里的.open(),
打开/dev/hello4时,打开无效,因为在驱动代码里没有分配次设备号4的操作结构体,
1 | ./20th_chrdev_region_test /dev/hello0 |