参考资料:《Linux驱动开发入门与实战》
字符驱动开发思维导图:
1、构建设备号dev_t
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
定义设备号:dev_t devno;
查看主设备号: cat /proc/devices
查看当前设备的主次设备号: ls -l /dev 内核为我们提供了几个方便操作的宏实现dev_t:
1.1 通过major和minor构建设备号
int major = 100;
int minor= 0;
dev_t devno = MKDEV(int major,int minor);
注:这只是构建设备号。并未注册,需要调用register_chrdev_region 静态申请;
1.2 从设备号中提取主次设备号
int major = MAJOR(dev_t devno);
int minor = MINOR(dev_t devno);
2、分配设备号
2.1 静态分配设备号
函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name);
头文件:#include <linux/cdev.h>
函数功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增加);
参数:
from:要分配的设备号范围的起始值。一般只提供from的主设备号,from的次设备号通常被设置为0;
count:需要申请的连续设备号的个数;
name:和该范围编号关联的设备名称,该名称不能超过64字节;
返回值:成功时返回0,失败时返回一个负的错误码,并且不能为字符设备分配设备号。
源代码如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
2.2 动态分配设备号
函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
头文件:#include <linux/cdev.h>
函数功能:申请使用从from开始的count 个设备号(主设备号不变,次设备号增加);
参数:
dev:dev作为输出参数,在函数成功返回后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回的第 一个设备号;
baseminor:要申请的第一个次设备号;
count:次设备的个数;
name:和该范围编号关联的设备名称,该名称不能超过64字节;
返回值:成功返回0,失败返回错误码;
源代码如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
2.3 申请设备号举例
int major = 100;
dev_t devno;
if(major)
{
devno= MKDEV(major, 0);
ret = register_chrdev_region(devno, 1, "xxx_dev");
if(ret != 0)
{
printk("[%s] %s %d : failed!\n", __FILE__, __func__, __LINE__);
return -1;
}
}
else
{
ret = alloc_chrdev_region(&devno, 0, 1, "xxx_dev");
if(ret != 0)
{
printk("[%s] %s %d : failed!\n", __FILE__, __func__, __LINE__);
return -1;
}
}
2.4 注销设备号
void unregister_chrdev_region(dev_t from, unsigned count);
from表示要释放的设备号,count表示从from开始要释放的设备号的个数。
3、分配cdev
3.1 静态分配cdev
struct cdev xxx_cdev;
3.2 动态分配cdev
法一:
struct cdev *xxx_pcdev = cdev_alloc();
xxx_pcdev->owner = THIS_MODULE;
xxx_pcdev->ops = &xxx_fops;
法二:
struct cdev *xxx_pcdev = kmalloc(sizeof(struct cdev), GFP_KERNEL);
xxx_pcdev->owner = THIS_MODULE;
补充:
kmalloc函数原型和头文件:
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
3.3 静态分配cdev与动态分配cdev的区别
1)struct cdev xxx_cdev; xxx_cdev一般是定义为全局变量,在数据段中分配内存;cdev_alloc和kmalloc分配的内存处于堆中;如果对struct cdev做了封装,且封装后的结构体较大,那么会在数据段中占很大内存;
2)cdev_alloc函数更简洁,但不适合struct cdev xxx_cdev封装后的自定义cdev结构体。见cdev_alloc函数定义。
4、初始化cdev
cdev_init()函数的定义;
函数原型:void cdev_init(struct cdev *, const struct file_operations *);
头文件:#include <linux/cdev.h>
函数功能:对struct cdev结构体做初始化,最重要的就是建立cdev和file_operations之间的连接;
参数:
struct cdev *:已定义的cdev结构体地址;
const struct file_operations *:已定义的file_operations结构体地址;
返回值:
源代码:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
函数解析:
(1) 将整个结构体清零;
(2) 初始化list成员使其指向自身;
(3) 初始化kobj成员;
(4) 初始化ops成员;
5、注册cdev
函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned count);
头文件:#include <linux/cdev.h>
函数功能:该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了;
参数:
p:已定义的cdev结构体地址;
dev:第一个设备号;
返回值:成功时返回0,失败时返回一个负的错误码。
源代码:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
6、向内核注销一个struct cdev结构
函数原型:void cdev_del(struct cdev *p);
头文件:#include <linux/cdev.h>
函数功能:该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了,同时释放cdev 占用的内存;
参数:已定义的cdev结构体地址;
返回值:无
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
其中 cdev_unmap()调用kobj_unmap()来释放cdev_map散列表中的对象。kobject_put()释放cdev结构本身。
7. 字符设备驱动编码实例
struct _xxx_dev /* 自定义设备结构体 */
{
struct cdev cdev; /* cdev结构体 */
... /* 特定设备的数据,可自定义 */
}xxx_dev;
static int __init xxx_init(void) /*模块加载函数*/
{
...
/* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请 */
if(xxx_major)
result = register_chrdev_region(xxx_devno, 1, "DEV_NAME");
else
{
result = alloc_chrdev_region(&xxx_devno, 0, 1, "DEV_NAME");
xxx_major = MAJOR(xxx_devno); /* 获得申请的主设备号 */
}
/* 初始化cdev结构体,并传递file_operation结构体指针 */
cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev结构体 */
xxx_dev.cdev.owner = THIS_MODULE; /* 指定所属模块 */
err = cdev_add(&xxx_dev.cdev, xxx_devno, 1); /* 注册设备 */
}
static void __exit xxx_exit(void) /*模块卸载函数*/
{
cdev_del(&xxx_dev.cdev); /* 注销cdev */
unregister_chrdev_region(xxx_devno, 1); /* 释放设备号 */
}
8、创建设备文件
8.1.1 使用mknod手工创建
利用cat /proc/devices查看申请到的设备名,设备号。
mknod filename type major minor
8.1.2 自动创建设备节点
利用udev/mdev来实现设备文件的自动创建,首先应保证支持udev/mdev。
在驱动中主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。
内核中定义的struct class结构体,一个struct class结构体类型变量对应一个类,内核同时提供了class_create()函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create()函数来在/dev目录下创建相应的设备节点。
这样,加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sys下寻找对应的类从而创建设备节点。
8.2 struct class 结构体
struct class定义在头文件include/linux/device.h中,结构体定义如下:
struct class{
const char *name;
struct module *owner;
nbsp;struct kset subsys;
struct list_head devices;
struct list_head interfaces;
struct kset class_dirs;
struct semaphore sem; /* locks children, devices, interfaces */
struct class_attribute *class_attrs;
struct device_attribute *dev_attrs;
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
void (*class_release)(struct class *class);
void (*dev_release)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
};
8.3 创建设备类class_create()
函数原型:struct class *class_create(struct module *owner, const char *name);
头文件:#include <linux/device.h>
函数功能:创建一个设备类;
参数:
owner:通常为THIS_MODULE;
name:要创建的设备类的名称;
返回值:可以定义一个struct class的指针变量cls接受返回值,然后通过IS_ERR(cls)判断是否失败,如果成功,这个宏会返回0,失败返回非0值。可以使用PTR_ERR(cls)来获得失败返回的错误码。
源代码如下:
class_create()在/drivers/base/class.c中实现:
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
struct class *class_create(struct module *owner, const char *name)
{
struct class *cls;
int retval;
cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}
cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;
retval = class_register(cls);
if (retval)
goto error;
return cls;
error:
kfree(cls);
return ERR_PTR(retval);
}
8.4 创建设备节点device_create()
函数原型:struct device *device_create(struct class *cls, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
头文件:#include <linux/device.h>
函数功能:用于动态的建立逻辑设备,并对新的逻辑设备类进行相应初始化,将其与函数的第一个参数所代表的逻辑类关联起来,然后将此逻辑设备加到linux内核系统的设备驱动程序模型中。函数能够自动在/sys/devices/virtual目录下创建新的逻辑设备目录,在/dev目录下创建于逻辑类对应的设备文件
参数:
第一个参数指定所要创建的设备所从属的类;
第二个参数是这个设备的父设备,如果没有就指定为NULL;
第三个参数是设备号;
第四个参数 void *drvdata: void类型的指针,代表回调函数的输入参数,通常填NULL。
第五个参数 const char *fmt: 逻辑设备的设备名,即在目录 /sys/devices/virtual创建的逻辑设备目录的目录名;
返回值:可以定义一个struct class的指针变量cls接受返回值,然后通过IS_ERR(cls)判断是否失败,如果成功,这个宏会返回0,失败返回非0值。可以使用PTR_ERR(cls)来获得失败返回的错误码。
源代码如下:
device_create(…)函数在/drivers/base/core.c中实现:
/**
* device_create - creates a device and registers it with sysfs
* @class: pointer to the struct class that this device should be registered to
* @parent: pointer to the parent struct device of this new device, if any
* @devt: the dev_t for the char device to be added
* @fmt: string for the device's name
*
* This function can be used by char device classes. A struct device
* will be created in sysfs, registered to the specified class.
*
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct device is passed in, the newly created
* struct device will be a child of that device in sysfs.
* The pointer to the struct device will be returned from the call.
* Any further sysfs files that might be required can be created using this
* pointer.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, NULL, fmt, vargs);
va_end(vargs);
return dev;
}
8.5 删除设备类class_destroy()
函数原型:void class_destroy(struct class *cls);
头文件:#include <linux/device.h>
函数功能:用于在模块卸载时删除类;
参数:
第一个参数指定所要删除的设备类;
返回值:无。
源代码如下:
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
8.6 移除设备device_destroy()
函数原型:void device_destroy(struct class *dev, dev_t devt);
头文件:#include <linux/device.h>
函数功能:用于从linux内核系统设备驱动程序模型中移除一个设备,并删除/sys/devices/virtual目录下对应的设备目录及/dev/目录下对应的设备文件;
参数:
第一个参数指定所要创建的设备所从属的类;
第二个参数是设备号;
返回值:无。
8.7 自动创建设备节点的步骤
1)调用class_create()函数创建设备类;可在/sys/class/目录下查看创建的设备类;
2)调用device_create()函数创建设备节点;这一步的作用是导出设备信息到用户空间;可在/sys/class/类/设备名/ 目录下看到设备号等信息。
3)在模块卸载时,先调用device_destroy()移除设备,再调用class_destroy()移除设备类;
//加载:
xxx_cls = class_create(THIS_MODULE, "xxx_class");
if(IS_ERR(xxx_cls))
{
ret = PTR_ERR(xxx_cls);
goto err_class_create;
}
xxx_device = device_create(xxx_cls, NULL, devno, NULL, "xxx_dev");
if(IS_ERR(xxx_device))
{
ret = PTR_ERR(xxx_device);
goto err_device_create;
}
err_device_create:
device_destroy(xxx_device, devno);
err_class_create:
class_destroy(xxx_cls);
// 卸载:
device_destroy(xxx_device, devno);
class_destroy(xxx_cls);
9、file_operations操作函数集合中的函数实现
struct file_operations在linux/fs.h这个文件里面被定义,以下操作函数在struct _file_operations结构体中定义。
9.1
/* 打开 */
int (*open) (struct inode * inode , struct file * filp );
(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.与open()函数对应的是release()函数。
9.2
/* 关闭 */
int (*release) (struct inode *, struct file *);
release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
9.3
/* 从设备中同步读取数据 */
函数原型:ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
函数功能:从设备中同步读取数据;
参数:
filp:待读取信息的目标文件file结构体指针;
buf:对应放置信息的缓冲区(即用户空间内存地址);
count:要读取的信息长度;
f_pos:为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值;
返回值:成功实际读取的字节数,失败返回负值(-EINVAL);
9.4
/* 向设备发送数据 */
函数原型:ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
函数功能:向设备发送数据;
参数:
filp:待操作的设备文件file结构体指针;
buf:要写入文件的信息缓冲区;
count:要写入信息的长度;
f_pos:为当前的偏移位置,这个值通常是用来判断写文件是否越界;
返回值:成功实际写入的字节数,失败返回负值(-EINVAL);
备注:对文件进行读的操作read和对文件进行写的操作write均为阻塞操作;
9.5
/* 执行设备I/O控制命令 */
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针.如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的.因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.)
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表.如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.
(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示;如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).
9.6 用户空间和内核空间数据相互拷贝
9.6.1 说明:
在Linux操作系统中,用户空间和内核空间使相互独立的。也就是说内核空间式不能直接访问用户空间内存地址,同理用户空间也不能直接访问内核空间的地址。
如果要实现将用户空间的数据拷贝到内核空间或将内核空间的数据拷贝到用户空间,就必须借助内核给我们提供的接口来完成。
9.6.2 从用户空间拷贝数据到内核空间
头文件:#include <asm/uaccess.h>
函数原型:unsigned long copy_from_user(void *to, const void *from, unsigned long n);
函数功能:从用户空间拷贝数据到内核空间;
参数:
to:内核空间地址;
from:用户空间地址;
n:数据大小;
返回值:成功返回0,失败返回未拷贝的字节数;
源代码:
copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_sleep();
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else
memset(to, 0, n);
return n;
}
9.6.3 从内核空间拷贝数据到用户空间
头文件:#include <asm/uaccess.h>
函数原型:unsigned long copy_to_user(void *to, const void *from, unsigned long n)
函数功能:从内核空间拷贝数据到用户空间;
参数:
to:用户空间地址;
from:内核空间地址;
n:数据大小;
返回值:成功返回0,失败返回未拷贝的字节数;
备注:__user是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址;
源代码:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
}
10. file_operations结构体及其成员函数实现实例
/* 读函数
struct file *filp: 指向打开的文件的结构体指针。
const char __user *buf: 用户空间地址,该地址不能在驱动程序中直接写入,需要使用copy_to_user写入。
size_t size: 要写入的字节数。
loff_t *ppos: 读写的位置。
*/
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
...
if(size > 8)
copy_to_user(buf, ..., ...);/* 当数据较大时,使用copy_to_user效率较高 */
else
put_user(..., buf); /* 当数据较小时,使用put_user效率较高 */
...
}
/*
写函数
struct file *filp: 指向打开的文件的结构体指针。
const char __user *buf: 用户空间地址,该地址不能在驱动程序中直接读取,需要使用copy_from_user读取。
size_t size: 要读取的字节数。
loff_t *ppos: 读写的位置。
*/
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
...
if(size > 8)
copy_from_user(..., buf, ...);/* 当数据较大时,使用copy_to_user效率较高 */
else
get_user(..., buf);
}
/*
ioctl设备控制函数
struct file *filp: 指向打开的文件的结构体指针
unsigned int cmd: 事先定义的I/O控制命令
unsigned long arg: 对应cmd命令的参数
*/
static long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch(cmd)
{
case xxx_cmd1:
... /* 执行命令1的操作 */
break;
case xxx_cmd2:
... /* 执行命令2的操作 */
break;
...
default:
return -EINVAL; /* 驱动不支持该命令时,返回无效的命令 */
}
return 0;
}
/* 文件操作结构体 */
static const struct file_operations xxx_fops =
{
.owner = THIS_MODULE, /* 模块引用,任何时候都赋值THIS_MODULE */
.read = xxx_read, /* 指定设备的读函数 */
.write = xxx_write, /* 指定设备的写函数 */
.ioctl = xxx_ioctl, /* 指定设备的控制函数 */
};