目录
1 Linux字符设备驱动结构
1.1 cdev结构体
1.1.1 dev_t dev
1.1.2 struct file_operations
1.2 Linux设备驱动的组成
1.2.1 字符设备驱动模块的加载与卸载
register_chrdev()与register_chrdev_region()
1.2.2 file_operations中的成员函数
1.2.3 用户空间与内核空间的内存复制
1.3 字符设备驱动的结构
1.4 字符设备节点创建
Linux内核中使用cdev结构体来描述一个字符设备。
1 struct cdev {
2 struct kobject kobj; // 内嵌的kobject对象
3 struct module *owner; // 所属模块
4 const struct file_operations *ops; // 文件操作结构体
5 struct list_head list; //linux内核所维护的链表指针
6 dev_t dev; //设备号
7 unsigned int count; //设备数目
8 };
dev_t dev是设备号,为32位,高12位是主设备号,低20位是次设备号。因此 Linux系统中主设备号范围为 0~4095。在include/linux/kdev_t.h 中提供了几个关于设备号的操作函数:
1 #define MINORBITS 20 // 表示次设备号位数,一共是 20 位。
2 #define MINORMASK ((1U << MINORBITS) - 1) // 表示次设备号掩码
3
4 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //获得主设备号
5 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //获得此设备号
6 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //通过主次设备号生成dev_t
file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会被应用程序调用Linux系统调用如open()/read()/write()/close()等时,最终被内核调用。
struct file_operations {
struct module *owner; // owner表示拥有该结构体的模块的指针,基本都是THIS_MODULE。
loff_t (*llseek) (struct file *, loff_t, int); // 用来修改一个文件的当前读写位置,将新位置返回,出错返回负值
ssize_t (*read) (struct file *, char __user *, size_t, loff_t*); // 从设备读取数据,成功返回读取的字节数,失败返回负值。对应用户空间的read、fread函数
ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *); // 成功返回写入的字节数,失败返回-EINVAL。对应用户空间的write和fwrite函数
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); // 异步读linux4.0及之前是aio_read
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); // 异步写linux4.0及之前是aio_write
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct*); /* poll轮询函数,用来查询设备是否可以进行非阻塞的读写,当询问条件未触发时,用户空间的select和poll函数会阻塞*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsignedlong); /* 对应ioctl和fcntl函数,在32位操作系统上运行32位的程序会使用unlocked_ioctl,而64位操作系统上运行32位程序会对应compat_ioctl */
long (*compat_ioctl) (struct file *, unsigned int, unsignedlong);
int (*mmap) (struct file *, struct vm_area_struct *); // 将设备内存映射到进程的虚拟地址空间,对应用户程序的mmap函数
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *); // 当用户空间调用系统调用open打开设备文件时,设备驱动的open函数会被执行,当然驱动程序可以不实现这个函数,此时,设备的打开操作默认永远成功。
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); // 对应close函数
int (*fsync) (struct file *, loff_t, loff_t, int datasync); /* 用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。 */
int (*aio_fsync) (struct kiocb *, int datasync); /*与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据 */
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
}
在字符设备驱动模块的加载函数中应实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
Linux内核的编码习惯是为设备定义一个设备相关的结构体,该结构体应包含设备所涉及的cdev、私有数据及锁等信息。如下:
#define DEV_NAME "xxxxx" // 定义设备名
/* 设备自定义结构体 */
struct xxx_dev_t {
struct cdev cdev;
......
};
static int xxx_major = 0; // 定义一个主设备号,0用于动态分配设备号,大于零用于静态分配设备号
static struct xxx_dev_t xxx_cdev;
/* 设备驱动模块加载函数 */
static int __init xxx_init(void)
{
.......
cdev_init(&xxx_dev.cdev, &xxx_fops); // 初始化cdev
xxx_dev.owner = THIS_MODULE;
dev_t xxx_dev_no; // 定义一个设备号
/* 获取字符设备号 */
if (xxx_major)
{
xxx_dev_no = MKDEV(xxx_major,0);
/* 静态分配设备号 */
register_chrdev_region(xxx_dev_no, 1, DEV_NAME); // 是register_chrdev()的升级版
} else {
/* : alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
* @description : 动态分配设备号
* @param – dev : 传出参数,用来保存申请到的设备号。
* @param - baseminor: 次设备号起始地址,一般 baseminor 为 0,也就是说次设备号从 0 开始。申请多个设备号时用
* @param - count : 要申请的设备号数量。
* @param - name : 设备名字
* @return : 0 成功;其他 失败
*/
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
xxx_major=MAJOR(xxx_dev_no);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); // 注册设备
......
}
/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
/* : unregister_chrdev_regio(dev_t from, unsigned count)
* @description : 释放设备号
* @param – from : 需要释放的设备号。
* @param - count : 表示从 from 开始,要释放的设备号数量。
* @return : 无
*/
unregister_chrdev_region(xxx_dev_no, 1); // 释放设备号
cdev_del(&xxx_dev.cdev); // 注销cdev设备
}
register_chrdev()与register_chrdev_region()
register_chrdev()函数不适用cdev结构体,老版本一直使用此函数,而2.6内核才出现了cdev结构体。
/* : register_chrdev * @description : 字符设备注册函数,一般放在驱动模块的入口函数modlue_init * @param – major : 主设备号,Linux 下每个设备都有一个主设备号和次设备号 * @param - name : 设备名字,指向一串字符串。即调用注册函数生成/dev/name * @param - fops : 结构体 file_operations 类型指针,指向设备的操作函数集合变量 * @return : 0 成功;其他 失败 */ static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) /* : unregister_chrdev * @description : 字符设备注销函数,一般放在驱动模块的出口函数modlue_exit * @param – major : 主设备号,Linux 下每个设备都有一个主设备号和次设备号 * @param - name : 设备名字,指向一串字符串。 * @return : 无 */ static inline void unregister_chrdev(unsigned int major, const char *name)
调用register_chrdev()不能指定次设备号,那么函数内部会在内核中连续注册256个次设备号,也就是所有的次设备号都会被占用,而在大多数情况下都不会用到这么多次设备号,所以会造成极大的资源浪费。
源码解析:register_chrdev深入分析 - 程序员大本营 (pianshen.com)
/* : xxx_read
* @description : 从设备读文件,对应read函数
* @param – filp : 要打开的设备文件(文件描述符)
* @param – buf : 返回给用户空间的数据缓冲区
* @param – cnt : 要读取的数据长度
* @param – offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
ssize_t xxx_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
......
copy_to_usr(buf, ..., ...);
......
}
/* :xxx_write
* @description : 向设备写数据,对应write函数
* @param - pfilp_t : 设备文件,表示打开的文件描述符
* @param - pusr_buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - pofft_t : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
ssize_t xxx_write(struct file *pfilp_t,const char __user *pusr_buf,
size_t cnt, loff_t *pofft_t)
{
......
copy_from_usr(..., buf, ...);
......
}
/* :xxx_open
* @description : 打开设备,对应open函数
* @param – inode : 传递给驱动的 inode
* @param - pfilp_t : 设备文件, file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
int xxx_open(struct inode *pinode_t, struct file *pfilp_t)
{
......
return 0;
}
/* :xxx_release
* @description : 关闭/释放设备,对应close函数
* @param - pfilp_t : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
int xxx_release(struct inode *pinode_t, struct file *pfilp_t)
{
// 用户实现具体功能
return 0;
}
/* : xxx_ioctl
* @description : 驱动实现ioctl,获取用户态发送的cmd
* @param – pfilp_t : 设备文件
* @param - cmd : 用户态发送给内核的cmd
* @param - arg : 用户态发给内核的额外的参数
* @return : 0 成功;其他 失败
*/
long xxx_ioctl(struct file *pfilp_t, unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case xxx:
{
......
break;
}
case xxx:
{
......
break;
}
default:
{
......
return -ENOTTY;
}
}
return 0;
}
static struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = xxx_ioctl, //指定驱动函数对应系统调用的ioctl
.open = xxx_open, //指定驱动函数对应系统调用的open
.release = xxx_release, //指定驱动函数对应系统调用的close
.write = xxx_write, //指定驱动函数对应系统调用的write
.read = xxx_read, //指定驱动函数对应系统调用的read
};
用户空间不能直接访问内核的内存,因此需要借助copy_to_usr()和copy_from_usr()函数。
/* : copy_to_user
* @description : 内核空间将数据拷贝到用户空间的“buf”
* @param – to : 目标地址,这个地址是用户空间接收数据的地址,即buf;
* @param - from : 源地址,为内核空间需要发送的数据的地址
* @param - count : 拷贝的字节数
* @return : 0 成功;负值 失败
*/
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
// 即copy_to_user(buf, const void *from, unsigned long count)
/* : copy_from_user
* @description : 用户空间的“buf”拷贝到内核空间
* @param – to : 目标地址,这个地址是内核空间接收数据的地址
* @param - from : 源地址,为用户空间需要发送的数据的地址,“buf”
* @param - count : 拷贝的字节数
* @return : 0 成功;负值 失败
*/
unsigned long copy_from_user(void *to, void __user *from, unsigned long count);
// 即copy_to_user(void *to, buf, unsigned long count)
__user是一个宏,表明其后的指针指向用户空间/include/linux/compiler.h中有如下定义:
#ifdef __CHECKER__
# define __user __attribute__((noderef, address_space(1)))
#else
# define __user
#endif
如果内核空间与用户空间只传递简单的值(int、long、char等),可以使用更简单的get_user()和put_user()函数。
int var; // 内核空间整型变量
get_user(var, (int *)arg); // 用户--->内核,arg是用户空间地址
put_user(val, (int *)arg); // 内核--->用户,arg是用户空间地址
无论是copy_to_user()/copy_from_user()还是get_user()/put_user()在函数内部都会执行一个合法性检测。具体就是:内核虽然可以访问用户空间的缓冲区,但在访问之前会检查其合法性,通过access_ok(type, addr, size)函数进行判断,来确定待访问的空间是否属于用户空间。
以copy_to_user函数源码举例:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_sleep();
BUG_ON((long) n < 0);
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
}
EXPORT_SYMBOL(copy_to_user);
如果使用__put_user和__get_user函数不会检测用户空间的合法性,需要自己进行合法性检测,这两者与put_user和get_user的区别就是后两者会自动进行合法性检测,而前者不会。
在完成上述 1.2 的字符设备驱动组成后,将写好的模块.c进行编译,生成.ko文件,.ko文件移植到开发板后,再执行相关命令:
insmod xxx.ko // 加载模块
lsmod // 查看当前系统已挂载的模块
mkmod /dev/xxx c major minor // 创建设备节点,c表示字符设备,major主设备号,minor次设备号
rmmod xxx.ko // 卸载模块