设备的操作函数如果比喻是桩的话(性质类似于设备操作函数的函数,在一些场合被称为桩函数),则:
驱动实现设备操作函数 ----------- 做桩
insmod调用的init函数主要作用 --------- 钉桩
rmmod调用的exitt函数主要作用 --------- 拔桩
应用层通过系统调用函数间接调用这些设备操作函数 ------- 用桩
内核中记录文件元信息的结构体
struct inode
{
//....
dev_t i_rdev;//设备号
struct cdev *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
//....
}
/*
1. 内核中每个该结构体对象对应着一个实际文件,一对一
2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建
3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)
*/
读写文件内容过程中用到的一些控制性数据组合而成的对象------文件操作引擎(文件操控器)
struct file
{
//...
mode_t f_mode;//不同用户的操作权限,驱动一般不用
loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用
unsigned int f_flags;//open时的第二个参数flags存放在此,驱动中常用
struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用
void *private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
//...
};
/*
1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作
2. open同一个文件多次,每次open都会创建一个该类型的对象
3. 文件描述符数组中存放的地址指向该类型的对象
4. 每个文件描述符都对应一个struct file对象的地址
*/
驱动实现端:
具体如下:
我们将每个设备都在内核中抽象成一个struct cdev对象,他们之间使用hash链表连接起来,所以就可以把他们看成是一个一个的节点,每个cdev对象都有一个唯一的设备号devno,想要驱动这些设备就需要在驱动程序中调用各种函数来实现,这些函数被放在其对应的操作函数集中,也就是图中的*fops,他们是整个硬件驱动的核心,这些函数的内容是根据所操作的硬件要满足的不同需求自己编写的,你想要实现什么功能,就像这个操作函数集中添加对应的函数即可,他们的具体使用请看下面的介绍。
驱动使用端:
具体流程如下:
当驱动程序调用open read等函数时,内核会根据其操作的设备号在链表中查找是否有对应的inode对象存在,这个对象有两个成员,一个是设备号,另一个是这个设备号在内核中所对应的抽象出来的设备对象,如果没有则创建一个新节点,并将其设备号赋值给成员devno,有的话则直接通过devno寻找到对应的inode对象,然后就可以的到对应的i_cdev对象的地址,也就是我们抽象出来的设备的地址,然后再通过这个地址找到cdev对象中的fops成员也就是操作函数集,然后系统会创建一个struct file对象,并将这个操作函数集设置到file对象中f_op成员当中去,然后将创建好的struct file对象的地址填到对应进程的文件描述符数组当中并返回给open函数,也就其文件描述符,然后我们就可以对返回的文件描述符所对应的设备进行操作了。
int (*open) (struct inode *, struct file *); //打开设备
/*
指向函数一般用来对设备进行硬件上的初始化,对于一些简单的设备该函数只需要return 0,对应open系统调用,是open系统调用函数实现过程中调用的函数,
*/
int (*release) (struct inode *, struct file *); //关闭设备
/*
,指向函数一般用来对设备进行硬件上的关闭操作,对于一些简单的设备该函数只需要return 0,对应close系统调用,是close系统调用函数实现过程中调用的函数
*/
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备
/*
指向函数用来将设备产生的数据读到用户空间,对应read系统调用,是read系统调用函数实现过程中调用的函数
*/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //写设备
/*
指向函数用来将用户空间的数据写进设备,对应write系统调用,是write系统调用函数实现过程中调用的函数
*/
loff_t (*llseek) (struct file *, loff_t, int); //数据操作位置的定位
/*
指向函数用来获取或设置设备数据的开始操作位置(位置指示器),对应lseek系统调用,是lseek系统调用函数实现过程中调用的函数
*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
/*
指向函数用来获取、设置设备一些属性或设备的工作方式等非数据读写操作,对应ioctl系统调用,是ioctl系统调用函数实现过程中调用的函数
*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);//POLL机制,实现对设备的多路复用方式的访问
/*
指向函数用来协助多路复用机制完成对本设备可读、可写数据的监控,对应select、poll、epoll_wait系统调用,是select、poll、epoll_wait系统调用函数实现过程中调用的函数
*/
int (*fasync) (int, struct file *, int); //信号驱动
/*
指向函数用来创建信号驱动机制的引擎,对应fcntl系统调用的FASYNC标记设置,是fcntl系统调用函数FASYNC标记设置过程中调用的函数
*/
struct cdev
{
struct kobject kobj;//表示该类型实体是一种内核对象
struct module *owner;//填THIS_MODULE,表示该字符设备从属于哪个内核模块
const struct file_operations *ops;//指向空间存放着针对该设备的各种操作函数地址
struct list_head list;//链表指针域
dev_t dev;//设备号
unsigned int count;//设备数量
};
自己定义的结构体中必须有一个成员为 struct cdev cdev,两种方法定义一个设备:
1.直接定义:定义结构体全局变量
struct cdev mydev;//自定义字符驱动结构体
2.动态申请:
struct cdev * cdev_alloc()
void cdev_init(struct cdev *cdev,const struct file_operations *fops)
struct file_operations
{
struct module *owner; //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
int (*open) (struct inode *, struct file *); //打开设备
int (*release) (struct inode *, struct file *); //关闭设备
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //写设备
loff_t (*llseek) (struct file *, loff_t, int); //定位
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
unsigned int (*poll) (struct file *, struct poll_table_struct *); //POLL机制,实现多路复用的支持
int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
int (*fasync) (int, struct file *, int); //信号驱动
//......
};
该对象各个函数指针成员都对应相应的系统调用函数,应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数:
一般定义一个struct file_operations类型的全局变量并用自己实现各种操作函数名对其进行初始化,其初始化格式较为特殊,具体如下:
struct file_operations myops = {
.owner = THIS_MODULE,//该结构体对象属于哪个内核模块
.open = mychar_open,//打开设备
.release = mychar_close,//关闭设备
.read = mychar_read,//读操作
.write = mychar_write,//写操作
};
这个结构体名为myops,他的成员为我们所要实现的各种操作,上方结构体实现了open、close、read、write这四个函数,等号右边为这几个函数的具体实现,这个是需要自己写的,命名也是根据你要实现的功能去命名而不是固定的。
字符设备驱动开发步骤:
将字符设备添加到内核使用int cdev_add(struct cdev *p,dev_t dev,unsigned int count)函数,具体如下:
将字符设备从内核中移除使用void cdev_del(struct cdev *p)函数,具体如下:
验证操作步骤:
编写驱动代码mychar.c
make生成ko文件
insmod内核模块
查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
编写app验证驱动(testmychar_app.c)
编译运行app,dmesg命令查看内核打印信息
#include
#include
#include
#include
#include
#define BUF_LEN 100
int major = 11;//主设备号
int minor = 0; //次设备号
int mychar_num = 1;//设备数量
struct cdev mydev;//自定义字符驱动结构体
int mychar_open(struct inode *pnode,struct file *pflie){
printk("open \n");
return 0;
}
int mychar_close(struct inode *pnode,struct file *pflie){
printk("close \n");
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,//该结构体对象属于哪个内核模块
.open = mychar_open,//打开设备
.release = mychar_close,//关闭设备
};
int __init mychar_init(void){
/* 将主设备号和次设备号组合成32位完整的设备号*/
dev_t devno = MKDEV(major,minor);
int ret = 0;
//申请设备号,方式为手动申请
ret = register_chrdev_region(devno,mychar_num,"mychar");
//手动申请失败则使用动态申请
if(ret != 0){
ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
if(ret != 0){
printk("get chrdev failed\n");
return -1;
}
major = MAJOR(devno);//自动申请的主设备号需要重新赋值
}
//给struct cdev对象制定操作函数集
cdev_init(&mydev,&myops);
//将其添加到内核对应的数据结构里
mydev.owner = THIS_MODULE;
//将指定的字符设备添加到内核中
cdev_add(&mydev,devno,mychar_num);
}
void __exit mychar_exit(void){
//获取设备的32位设备号
dev_t devno = MKDEV(major,minor);
//移除对应的设备
cdev_del(&mydev);
//将申请的设备号释放
unregister_chrdev_region(devno,mychar_num);
}
module_init(mychar_init);
module_exit(mychar_exit);