开始看老宋的字符设备驱动结构,把主要内容罗列一下,因为是菜鸟,所以只能看见什么都记下来。
在linux字符设备驱动中主要有下面的几大部分组成。
一、字符设备驱动模块加载与卸载函数
1.定义:①字符设备的家在函数应该实现设备号的申请和cdev的注册。
②字符设备的卸载函数实现设备号的释放和cdev的注销。
关于cdev的具体概念在另一篇文章中进行介绍。
2.在大的工程中都会有设备相关的结构体,主要包括设备涉及的cdev、私有数据以及信号量等信息。
设备结构体模型如下:struct xxxx_dev_t
{ struct cdev cdev;
..................
}
驱动模块的加载函数如下:
static int memdev_init (void)
{
int result;
int i;
/*使用这个宏可以通过主设备号和次设备号生成dev_t,dev_t注册进cdev结构体(在开始处)*/
dev_t devno = MKDEV(mem_major, 0);
//MKDEV(mem_major, 0) 设备号
//静态申请设备号
if (mem_major) //判断mem_major是否大于零,但是在宏中已经定义了它就是大于0的。
{
result = int register_chrdev_region(devno,2, "memdev")
/*三个参数的含义:devno 希望申请使用的设备号
2 希望申请适用的设备数目
"memdev" 设备名(体现在/proc/devices)*/
}
//如果动态申请失败,就开始动态申请设备号
else
{
result = int alloc_chrdev_region(&devno, 0, 2,"memdev")
/*四个参数的含义:&devno 分配到的设备号
0 起始的次设备号
2 需要分配的设备数目
"memdev" 设备名(体现在/proc/devices)*/
mem_major = MAJOR(devno); //宏定义的函数,提取主设备号。
}
//申请失败。
if (result < 0)
return result;
/*初始化cdev结构*/
//在前面已经静态的分配了cdev,所以此处不必再分配cdev的空间了。
//cdev的初始化函数(老谢的源程序中没有定义cdev的类型,不知道为什么?)
//cdev函数的原型:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
cdev_init(&cdev, &mem_fops);//cdev_init 用于初始化cdev成员,并建立cdev和file_operations的连接。
//两个参数的含义:&cdev 待初始化的cdev结构
//&mem_fops 设备对应的操作函数集
//注册进来的两个函数(没太懂)
cdev.owner = THIS_MODULE;//拥有该结构体模块的指针。
cdev.ops = &mem_fops;
//添加设备注册
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS); //执行成功以后,驱动程序就在内核中注册好了。
/*三个参数的含义:&cdev 待添加到内核的字符设备结构
MKDEV(mem_major, 0) 设备号
MEMDEV_NR_DEVS 添加的设备个数
/* 为设备描述结构分配内存*/
//为设备的描述结构分配内存。
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) //头文件中定义有设备描述结构体
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev)); //清零的操作
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE; //这是4K的大小,宏中有定义。使用4K的内存来模拟一个设备。
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);//分配出4K的内存来给data指针。
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;
//goto到达的位置
fail_malloc:
unregister_chrdev_region(devno, 1);
return result; //鬼知道这玩意整哪去了。。
}
上述的驱动模块加载函数包含了一部分为设备分配内存的部分,因为这个字符设备的驱动主要的将一块内存来虚拟成我们所要驱动的字符设备。
驱动模块的卸载函数如下:
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}
二、设备驱动中的file_operations 结构体成员函数
file_operations 结构体成员函数是字符设备驱动与内核的接口,是用户空间对linux进行系统调用的最终落实者。在file_operations 结构体中最最常见的三类函数。
①设备驱动读函数:
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
/*函数参数: struct file *filp(open的时候系统产生的结构,根据inode来的)
char __user *buf 存储读取数据的空间
size_t size 读取的大小 size_t应该就是typedef unsigned int的类型
loff_t *ppos 当前文件指针的位置*/
{
unsigned long p = *ppos;//保存下文件指针的当前位置
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
//这个在文件的打开中已经定义了。
//private_data是一个空指针用来接收各种数据的传递
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE)
//MEMDEV_SIZE是设备的大小,所以读取的大小绝对不可以大于设备的大小
return 0;
if (count > MEMDEV_SIZE - p)
//假如读到的内容比剩下的还多,那么只能让你读到剩下的大小。count = MEMDEV_SIZE - p;
count = MEMDEV_SIZE - p;
/*读数据到用户空间*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count; //ppos,读写指针的位置
ret = count; //返回实际读到的数据量
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
return ret;
}
设备驱动读函数的原型:static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
在设备驱动读函数中,filp是文件的结构体指针,buf是用户空间内存的地址,该地址在内核空间是不能够直接读写的(老谢说的对),count是要读的字节数,ppos是读的位置相对于文件开头的偏移。
②设备驱动写函数:
static ssize_t mem_write (struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
//和读函数时一个道理,读函数和写函数都不能够自己活得设备号,所以都要借助于打开函数。
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}
设备驱动写函数的构造和设备驱动读函数的构造基本相同。
备注:由于内核空间和用户空间不能相互访问,在设备驱动读函数和设备驱动写函数中分别使用到了copy_to_user和copy_from_user,to_usrer函数完成了内核空间到用户空间的拷贝,from_usrer完成了用户空间到内核空间的拷贝。如果要复制的类型是简单的内存类型,那么可以使用put_user 和 get_user。(个人觉得这块老谢没有说清)
③设备驱动文件打开函数:
int mem_open(struct inode *inode, struct file *filp)//参数含义 inode 存储文件物理信息的结构(包含设备号) file结构{ struct mem_dev *dev; //宏中有定义 /*获取次设备号*/ int num = MINOR(inode->i_rdev); //如果获取的次设备号大于需要的,那么将会出错。 if (num >= MEMDEV_NR_DEVS) return -ENODEV; dev = &mem_devp[num]; //这是一个设备号的指针数组。 /*将设备描述结构指针赋值给文件私有数据指针*/ filp->private_data = dev; //在read中用到了这个地方 //这个dev是从dev = &mem_devp[num];里拿到的。 //之所以从这里拿是因为文件读取函数并不能获得设备的次设备号 return 0;}设备驱动写函数的构造和设备驱动读函数的构造基本相同。备注:由于内核空间和用户空间不能相互访问,在设备驱动读函数和设备驱动写函数中分别使用到了copy_to_user和copy_from_user,to_usrer函数完成了内核空间到用户空间的拷贝,from_usrer完成了用户空间到内核空间的拷贝。如果要复制的类型是简单的内存类型,那么可以使用put_user 和 get_user。(个人觉得这块老谢没有说清) int mem_open(struct inode *inode, struct file *filp)
//参数含义 inode 存储文件物理信息的结构(包含设备号) file结构
{
struct mem_dev *dev; //宏中有定义
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
//如果获取的次设备号大于需要的,那么将会出错。
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num]; //这是一个设备号的指针数组。
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev; //在read中用到了这个地方
//这个dev是从dev = &mem_devp[num];里拿到的。
//之所以从这里拿是因为文件读取函数并不能获得设备的次设备号
return 0;
}
这一部分是文件打开函数的代码,具体的需要注意的都有注释说明。
三、最后别忘了还有几个小部分,但是少了也不行
①一个作者(可有可无),一个要告诉内核自己编写的东西是遵守开源协议的,不会对内核造成污染。MODULE_AUTHOR("XXX");MODULE_LICENSE("GPL");
②必须有这两个宏定义的函数模块装载与卸载要用到的:
module_init(memdev_init); //这里指定的就是整个程序的入口
module_exit(memdev_exit);
装载时(insmod),调用module_init(...)中说明的函数。
卸载时(rmmod),调用module_exit(...)中说明的函数。
另附字符设备驱动结构图一张