上一篇讲述了字符设备驱动的设备号怎么创建,现在来讲讲内核中是怎么实现字符驱动的。
在Linux内核中cdev就是用来描述字符设备的结构体。
之前说明用户空间查找字符驱动是使用设备号,而在内核空间肯定也是通过和用户空间一样的设备号来标识字符设备驱动。
如图所示,cdev主要关心的是dev_t和file_operations。dev_t就是设备号,file_operations是操作函数集合,相当于字符设备提供的接口函数。
字符设备驱动模型
下面我们就按照字符驱动模型来一一讲解。
设备号的静态分配和动态分配参考上一篇文章(设备号申请)。
要使用字符设备驱动就需要先向内核中添加一个cdev结构体表示来声明一个字符设备驱动。
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
我们使用cdev_init函数来初始化cdev。
//初始化cdev,为cdev提供操作函数集合
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
然后向使用cdev_add函数内核中添加cdev。
//将cdev添加到内核,还会为cdev绑定设备号
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
参数:
p - 要添加的cdev结构
dev - 起始设备号
count - 设备号个数
返回0表示成功,非0表示失败
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。
简单来说就是你想字符设备驱动实现什么功能就要写好相应的函数,并把函数地址赋值给file_operations中对应的成员,没有赋值的成员都被gcc初始化为NULL。
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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
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);
};
我们在实现操作函数时,函数结构要和file_operations中的是一致的。挑选几个常用的操作函数来做一下示例。
int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\n");
return 0;
}
ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_read!\n");
return 0;
}
ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_write!\n");
return 0;
}
long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter cdd_ioctl!\n");
return 0;
}
int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\n");
return 0;
}
//声明操作函数集合
struct file_operations cdd_fops = {
.owner = THIS_MODULE,//拥有该结构的模块计数,一般为THIS_MODULE
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl,//ioctl接口
.release = cdd_release,//对应用户close接口
};
上面有多个函数参数中有file结构体和inode结构体,接下来还要介绍一下这两个结构体。
file:
struct file结构体定义在linux/fs.h头文件中。file结构体是用于描述进程与文件的关系和操作的,代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。struct file的指针通常被命名为file或filp,file结构有以下几个重要的成员:
struct file{
mode_t fmode; /*文件模式,如FMODE_READ,FMODE_WRITE*/
loff_t f_pos; /*loff_t 是一个64位的数,需要时,须强制转换为32位*/
unsigned int f_flags; /*文件标志,如:O_NONBLOCK*/
struct file_operations *f_op;
void *private_data; /*非常重要,用于存放转换后的设备描述结构指针*/
};
inode:
文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(sector)。每个扇区储存512字节操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的源信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件源信息的区域就叫做inode。
我们有往内核添加cdev的函数,自然也有从内核中删除cdev的函数cdev_del。
void cdev_del(struct cdev *p);
然后就是设备号的注销也请参考上一篇文章 (设备号申请)。
一个完整的字符设备驱动就完成了。我们来写一个实例,练习一下。
驱动程序cdd.c
#include
#include
#include
#include
//主设备号
#define CDD_MAJOR 220
//起始次设备号
#define CDD_MINOR 0
//设备号个数
#define CDD_COUNT 1
//设备号
dev_t dev;
//声明cdev
struct cdev cdd_cdev;
/*
inode是文件的节点结构,用来存储文件静态信息
文件创建时,内核中就会有一个inode结构
file结构记录的是文件打开的信息
文件被打开时内核就会创建一个file结构
*/
int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\n");
return 0;
}
ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_read!\n");
return 0;
}
ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_write!\n");
return 0;
}
long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter cdd_ioctl!\n");
return 0;
}
int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\n");
return 0;
}
//声明操作函数集合
struct file_operations cdd_fops = {
.owner = THIS_MODULE,
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl,//ioctl接口
.release = cdd_release,//对应用户close接口
};
//加载函数
int cdd_init(void)
{
int ret;
#if 1
//构造设备号
dev = MKDEV(CDD_MAJOR, CDD_MINOR);
// 1.静态申请设备号
ret = register_chrdev_region(dev, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("register_chrdev_region failed!\n");
return ret;
}
#else
// 2.动态申请设备号
ret = alloc_chrdev_region(&dev, CDD_MINOR, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("alloc_chrdev_region failed!\n");
return ret;
}
#endif
// 2.注册cdev
//初始化
cdev_init(&cdd_cdev, &cdd_fops);
//将cdev添加到内核
ret = cdev_add(&cdd_cdev, dev, CDD_COUNT);
if(ret<0){
//注销设备号
unregister_chrdev_region(dev, CDD_COUNT);
printk("cdev_add failed!\n");
return ret;
}
printk("cdev_add success!\n");
return 0;
}
//卸载函数
void cdd_exit(void)
{
cdev_del(&cdd_cdev);
//注销设备号
unregister_chrdev_region(dev, CDD_COUNT);
}
//声明为模块的入口和出口
module_init(cdd_init);
module_exit(cdd_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("xin");//作者
MODULE_VERSION("1.0");//版本
MODULE_DESCRIPTION("charactor driver!");//描述信息
应用测试程序test.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd;
char ch = 0;
char buf[10] = {0};
fd = open("/dev/cdd",O_RDWR);
if(fd==-1){
perror("open");
exit(-1);
}
printf("open successed!fd = %d\n",fd);
while(1){
ch = getchar();
getchar();
if(ch=='q')
break;
switch(ch){
case 'r':
read(fd,buf,0);
break;
case 'w':
write(fd,buf,0);
break;
default:
printf("error input!\n");
break;
}
sleep(1);
}
close(fd);
return 0;
}
Makefile
ifeq ($(KERNELRELEASE),)
#内核源代码路径
KERNELDIR ?= /home/xin/6818GEC/kernel
#交叉编译器路径
CROSS_PATH := /home/xin/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
#模块源代码路径
PWD := $(shell pwd)
default:
$(MAKE) CROSS_COMPILE=$(CROSS_PATH) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.ko *.mod .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
else
#obj-m表示编译生成可加载模块,obj-y表示直接将模块编译进内核。
obj-m := cdd.o
endif
Makefile中 内核源码路径和交叉编译器路径都是根据自己的情况修改,具体请看Linux驱动交叉编译。
另外加载驱动模块(insmod xxx.ko)之后还需要创建设备文件,格式为:
mknod 设备文件路径 文件类型 主设备号 次设备号
例如 mknod /dev/cdd c 220 0
详情请看设备文件的两种创建方式。
敲完命令后会在/dev下生成一个目录
应用测试cdd_test
以上就是字符驱动设备cdev的介绍和使用,有任何疑问都欢迎在评论区讨论。