Linux 字符设备驱动
这部分主要讲Linux字符设备驱动程序的结构,解释主要组成部分的编程方法。
字符设备
字符设备:指只能一个byte一个byte读写的设备,不能随机读写数据,要按先后顺序。字符设备是面向流的设备,常见字符设备有鼠标、键盘、串口、终端、LED灯。
块设备:指可以从设备的任意位置读取一定长度数据的设备。常见块设备有磁盘、硬盘、U盘、SD卡等。
每个字符设备或块设备,都在/dev目录下有一个对应的设备文件。Linux APP可以通过这些设备文件(又称设备节点),来使用驱动程序操作字符设备和块设备。
字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系:
from Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析 | CSDN
Linux内核中,
- 使用cdev结构体描述字符设备;
- 通过成员dev_t定义设备号(主、次设备号),确定字符设备的唯一性;
- 通过成员file_operations定义字符设备驱动,为VFS(虚拟文件系统)提供接口函数,如open/close/read/write等。
Linux字符设备驱动中,
- 模块加载函数通过register_chrdev_region()或alloc_chrdev_region(),来静态或动态获取设备号;
- 通过cdev_init()建立cdev与file_opreations之间的连接,通过cdev_add()向系统添加一个cdev以完成注册;
- 模块卸载函数通过cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。
TIPS: register_chrdev 与 register_chrdev_region, alloc_chrdev_region有何区别?
register_chrdev 设备注册 + 设备号申请。register_chrdev_region和alloc_chrdev_region 设备号申请,设备注册由cdev_init + cdev_add完成。
register_chrdev() 支持一次注册一个设备,而且需要传入参数file_operations。默认写死注册的设备号范围0~255。释放字符设备时,使用unregister_chrdev()。但不必使用cdev_xxx系列操作。
register_chrdev_region() 支持一次注册多个设备号,不需要传入参数file_operations,在cdev_init()中绑定cdev与file_operations。释放设备号时,使用unregister_chrdev_region。register_chrdev_region需要搭配cdev_xxx系列操作使用。
alloc_chrdev_region() 与register_chrdev_region()的区别在于前者申请的设备号由系统决定,后者由调用者指定。
APP中访问设备驱动程序,
- 通过Linux系统调用,如open/close/read/write,调用file_operations中定义的接口函数。
Linux字符设备驱动结构
cdev结构体
Linux内核中,使用cdev结构体描述一个字符设备。cdev定义:
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属模块 */
struct file_operations *ops; /* 文件操作结构体 */
struct list_head list;
dev_t dev; /* 设备号 */
unsigned int count; /* 该设备关联的设备编号的数量 */
};
cdev结构体的dev_t成员定义设备号(32bit),其中高12bit为主设备号,低20bit为次设备号。
如何获取主次设备号,或dev_t?
- 从dev_t获得主设备号和次设备号
MAJOR(dev_t dev); // 主设备号
MINOR(dev_t dev); // 次设备号
- 通过主设备号、次设备号生成dev_t
MKDEV(int major, int minor); // 生成dev_t, 包含主次设备号信息
这几个宏定义如下:
#include <linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 高12bit为主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 低20bit为次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
内核提供一组函数用于操作cdev结构体:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
1)cdev_init 初始化cdev成员,最重要的是建立cdev和file_operations之间的连接
源码:
/**
* 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)
{
memset(cdev, 0, sizeof *cdev); /* 将整个结构体清零 */
INIT_LIST_HEAD(&cdev->list); /* 初始化list成员, 指向自身 */
kobject_init(&cdev->kobj, &ktype_cdev_default); /* 初始化kobj成员 */
cdev->ops = fops; /* 建立cdev和file_operations之间的连接 */
}
2)cdev_alloc 动态申请一个cdev内存
源码:
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); /* 动态申请一个cdev内存, GFP_KERNEL: 无内存可用时可休眠 */
if (p) {
INIT_LIST_HEAD(&p->list); /* 初始化list成员, 指向自身 */
kobject_init(&p->kobj, &ktype_cdev_dynamic); /* 初始化kobj成员 */
}
return p;
}
上面两个初始化函数,为何都没看到owner、dev、count 这3个成员的初始化?
对于owner成员,struct module类型对象,是内核对于一个模块的抽象。该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式初始化.owner = THIS_MODULE
对于dev和count成员,在cdev_add中才会赋值。
3)cdev_add 向内核添加一个cdev,完成字符设备的注册
这里需要提供参数dev(设备号)和count(该设备关联的设备编号的数量),直接赋值给cdev结构的dev和count成员。
/**
* 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)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p); /* 将cdev放入cdev_map中 */
if (error)
return error;
kobject_get(p->kobj.parent); /* 增加引用计数 */
return 0;
}
4)cdev_del 从内核删除一个cdev
/**
* 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)
{
cdev_unmap(p->dev, p->count); /* 将dev从cdev_map中擦除 */
kobject_put(&p->kobj); /* 减少引用计数 */
}
static void cdev_unmap(dev_t dev, unsigned count)
{
kobj_unmap(cdev_map, dev, count); /* 将dev从cdev_map中擦除 */
}
分配、释放设备号
分配设备号
调用cdev_add()向系统注册字符设备前,应先申请设备号。分配设备号有2种方法:
1)静态申请:register_chrdev_region
register_chrdev_region() 用于已知起始设备号的情况,向系统静态申请设备号(范围)。
要申请的设备号范围:[from, from + count)。
有些设备号已被Linux内核开发者分配掉了,具体分配内容可查看Documentation/devices.txt。
/**
* 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)
{
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)动态申请:alloc_chrdev_region
alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。
得到的设备号会放入第一个参数dev中。alloc_chrdev_region相比register_chrdev_region,优点:alloc_chrdev_region会自动避开设备号重复的冲突。
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
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;
}
检查:注册设备成功后,会在/proc/devices 添加字符设备名称。
因此,可以利用insmod命令加载设备驱动后,观察/proc/devices值,判断是否注册了设备。
# cat /proc/devices
释放设备号
在调用cdev_del()从系统注销字符设备后,unregister_chrdev_region()应该被调用以释放原先申请的设备号。
从系统反注册设备号,范围:[from, from + count)
/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0); /* 下一个设备号dev_t */
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); /* 反注册单个设备号, 并释放空间 */
}
}
file_operations结构体
file_operations 是设备驱动程序与APP交互的接口,其成员函数是字符设备驱动程序设计的主体,实际会在APP调用open/write/read/close等系统调用时被内核调用。
file_operations结构体定义:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
};
主要成员:
- llseek() 用来修改一个文件的当前读写位置,并将新位置返回,出错时,函数返回一个负值。
- read() 用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。对应用户空间read(2)。
- write() 向设备发送数据,成功时函数返回写入的字节数。如果未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。对应用户空间write(2)。
read和write返回0,暗示end-of-line(EOF)。
- unlocked_ioctl() 提供设备相关控制命令的实现(不是读,也不是写),成功时返回一个非负值。对应用户空间fcntl(2)应。
- mmap() 将设备内存映射到进程的虚拟地址空间,如果设备驱动未实现该函数,用户调用mmap()系统调用时返回-ENODEV。对应用户空间mmap(2)。与mmap对应的是unmap。
- open() 打开设备,用于初始化设备状态。用户空间调用open(2)时,设备驱动的open()被调用。驱动程序可以不实现该函数,设备打开操作永远成功。与open对应的是release。
- release() 释放设备资源。如果open()中有申请系统资源,则可以在release()中释放。对应用户空间close(2)。
- poll() 用于询问设备是否可以被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程阻塞。
- aio_read()/aio_write() 分别对与文件描述符对应的设备进行异步读、写操作。设备实现这2个函数后,用户空间可以对该设备文件描述符执行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系统调用进行读写。
字符设备驱动的组成
Linux中,字符设备驱动组成:字符设备驱动模块加载、卸载函数,字符设备驱动的file_operations结构体的成员函数。
字符设备驱动模块的加载、卸载函数
加载函数应该实现:1)设备号的申请;2)cdev的注册。
卸载函数应该实现:1)设备号的释放;2)cdev的注销。
典型的设备结构体、模块加载函数、卸载函数代码形式:
/* 设备结构体
struct xxx_dev_t = {
struct cdev cdev;
...
} xxx_dev;
*/
/* 设备驱动模块加载函数 */
static init __init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */
xxx_dev.cdev.owner = THIS_MODULE;
/* 获得字符设备号 */
if (xxx_major) {
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */
...
}
/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */
cdev_del(&xxx_dev.cdev); /* 注销设备 */
}
字符设备驱动的file_operations结构体的成员函数
file_operations的成员函数是字符设备驱动跟内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()/write/ioctl()。
典型字符设备驱动代码形式:
/* 读设备
* filp: 文件结构指针
* buf: 用户空间内存地址, 在内核空间不能直接读写
* count: 要读的字节数
* f_pos: 读的位置相对于文件开头的偏移
*/
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...); /* 将数据从内核空间拷贝到用户空间 */
...
}
/* 写设备 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_from_user(.... buf, ...); /* 将数据从用户空间拷贝到内核空间 */
...
}
/* ioctl函数 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch(cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
/* 不支持的命令 */
return -ENOTTY;
}
return 0;
}
copy_to_user和copy_from_user
注:用户空间不能直接访问内核空间的内存,所以要借助copy_to_user()将数据从内核空间拷贝到用户空间;
同样地,内核空间不能直接访问用户空间的内存,所以借助copy_from_user()将数据从用户空间拷贝到内核空间。
#include <linux/uaccess.h>
/* 用户 -> 内核 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
/* 内核 -> 用户 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
注:函数返回不能被复制的字节数。如果完全复制成功,返回0;如果失败,返回负值。
其源码如下:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(access_ok(VERIFY_READ, from, n))) /* 检查地址的合法性, from起始地址, 长度n */
n = __copy_from_user(to, from, n); /* 数据拷贝, 但不做地址合法性检查 */
else
memset(to, 0, n);
return n;
}
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(access_ok(VERIFY_WRITE, to, n))) /* 检查地址的合法性, to起始地址, 长度n */
n = __copy_to_user(to, from, n); /* 数据拷贝, 但不做地址合法性检查 */
return n;
}
likely:是宏定义,常用于编译器优化,告诉编译器分支大概率会发生。
access_ok(type, addr, size):内核空间可以访问用户空间的缓冲区,但访问之前需要用access_ok检查其合法性,以确定传入的缓冲区地址的确术语用户空间。
如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user()。
int val; /* 内核空间变量 */
...
get_user(val, (int* ) arg); /* 用户 -> 内核, arg 是用户空间地址 */
...
put_user(val, (int* ) arg); /* 内核 -> 用户, arg 是用户空间地址 */
copy_from_user函数中的__user宏是什么?
该宏表明背后的指针指向用户空间,实际上更多地充当了代码注释的功能。
#ifdef __CHECKER__
# define __user __attribute__((noderef, address_space(1)))
#else
# define __user
#endif
put_user和get_user
put_user(), get_user() 也有另外一个版本:__put_user(), __get_user()。区别在于__put_user()不用access_ok()检查地址的合法性,而put_user()会。通常,在调用__put_user()之前,会手动检查用户空间缓冲区。
get_user()和__get_user() 关系类似。
I/O控制函数unlocked_ioctl
I/O控制函数的cmd参数为事先定义的I/O控制命令,arg为对应于命令的参数。例如,对于串行设备,如果SET_BAUDRATE是设置波特率的命令,那arg就应该是波特率值。
字符设备驱动文件操作file_operations
字符设备驱动文件操作,通过定义file_operations实例,并将具体设备驱动函数赋值给file_operations成员来完成。
struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
.unlocked_ioctl = xxx_ioctl,
};
通过模块加载函数中调用cdev_init(&xxx_dev.cdev, &xxx_fops) 为cdev和fops建立连接。
参考
[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.
[2] https://blog.csdn.net/zqixiao_09/article/details/50839042