文章目录
1.字符设备基础知识
1.1 字符设备的结构分析
如下图是字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
如上图
在linux内核中:
(1)使用cdev结构体来描述字符设备
(2)通过cdev结构体中的dev_t成员来定义设备号(分为主,次设备号)以确定字符设备的唯一性
(3)通过cdev结构体中的file_operations来定义字符设备驱动提供给VFS的接口函数
在Linux字符设备驱动中:
(1)模块加载可通过静态或者动态来获取设备号
(2)通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
(3)模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号
1.2 cdev 结构体解析
在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
内核给出的操作struct cdev结构的接口主要有以下几个:
1. void cdev_init(struct cdev *, const struct file_operations *);
cdev_init()该函数主要对struct cdev结构体做初始化,最重要的就是建立cdev 和 file_operations之间的连接:
2. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
该函数向内核注册一个struct cdev结构和设备号对应和该设备关联的设备编号的数量。
这里还需提供两个参数:
(1)第一个设备号 dev,
(2)和该设备关联的设备编号的数量。
这两个参数直接赋值给struct cdev 的dev成员和count成员。
3. void cdev_del(struct cdev *p);
该函数向内核注销一个struct cdev结构,告诉内核由struct cdev *p代表的字符设备已经不可以使用了。
1.3 设备号相应操作
主设备号和次设备号(二者一起为设备号):
字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
2.分析加载函数代码和创建设备号节点
2.1 字符驱动源码
#include<linux/init.h>
#include<linux/module.h>
#include<linux/types.h>
#include<linux/fs.h>
#include<linux/mm.h>
#include<linux/sched.h>
#include<linux/cdev.h>
#include<asm/io.h>
#include<asm/switch_to.h>
#include<asm/uaccess.h>
#include<linux/kernel.h>
#include <linux/slab.h> //头文件
MODULE_LICENSE("GPL"); //许可证信息
#define MYCDEV_MAJOR 231 //定义一个主设备号
#define MYCDEV_SIZE 1000 //定义缓存变量数组的大小
char * kernel_buf; //定义一个内核缓存数据指针
int mycdev_major;
int mycdev_minor;
struct cdev cdev; //定义一个cdev结构体
static int mycdev_open(struct inode *inode, struct file *fp)
{
return 0;
}
static int mycdev_release(struct inode *inode, struct file *fp)
{
return 0;
}
static ssize_t mycdev_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{
if(size > MYCDEV_SIZE) //判断是否要读取的大小大于缓存数组的大小
size = MYCDEV_SIZE; //如果上面的if成立将缓存数组的数据赋值给读取的大小
if(copy_to_user(buf,kernel_buf,size) != 0) //如果操作失败返回值不等于0
{
printk("read error!\n"); //打印出读取失败的串口信息
return -1;
}
printk("reader:%d bytes was read...\n",size);
return size; //返回读取的大小
}
static ssize_t mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
if(size > MYCDEV_SIZE) //判断是否要读取的大小大于缓存数组的大小
size = MYCDEV_SIZE; //如果上面的if成立将缓存数组的数据赋值给读取的大小
if(copy_from_user(kernel_buf, buf, size) != 0) //如果操作失败返回值不等于0
{
printk("write error!\n"); //打印出写入失败的串口信息
return -1;
}
printk("write:%d bytes was write...\n",size);
return size; //返回读取的大小
}
static const struct file_operations mycdev_fops = //文件操作结构体
{
.owner = THIS_MODULE,
.read = mycdev_read,
.write = mycdev_write,
.open = mycdev_open,
.release = mycdev_release,
};
static int __init mycdev_init(void) //加载函数
{
int ret;
dev_t devno; //定义一个设备变量
printk("mycdev module is starting..\n");
// 自动分配设备编号。分配的地址,第一个从设备号(可以传从设备号),连续申请从设备号的编号数量,设备名称
ret = alloc_chrdev_region(&devno, 0, 1, "my_cdev");
// 初始化cdev,传入文件操作结构体信息
cdev_init(&cdev, &mycdev_fops);
cdev.owner = THIS_MODULE; //所属模块赋值
// 串口打印出驱动加载开始
// 注册到内核,cdev数据,设备号(起始),次设备数
cdev_add(&cdev, devno , 1);
if(ret < 0)
{
printk("register failed..\n"); //串口打印出失败信息
return ret;
}
printk("register success..\n");
// 在内核区域申请一块内存,用来放缓存数据kernel_buf
kernel_buf = kzalloc(sizeof(MYCDEV_SIZE), GFP_KERNEL);
if (NULL==kernel_buf)
{
return -1;
}
return 0;
}
static void __exit mycdev_exit(void) //卸载函数
{
printk("mycdev module is leaving..\n");
// 注销cdev
cdev_del(&cdev);
// 释放内存
kfree(kernel_buf);
// 释放设备号
unregister_chrdev_region(devno, 1);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
2.2分析驱动加载函数代码
上面的源代码通过make编译后将编译生成的.ko文件加载进内核
.ko文件加载到内核后会执行驱动代码中的加载函数
加载函数代码如下
static int __init mycdev_init(void) //加载函数
{
int ret; //定义一个设备注册判断变量
dev_t devno; //定义一个设备变量
printk("mycdev module is starting..\n"); //向串口打印提示信息
// 自动分配设备编号。分配的地址,第一个从设备号(可以传从设备号),连续申请从设备号的编号数量,设备名称
ret = alloc_chrdev_region(&devno, 0, 1, "my_cdev");
// 初始化cdev,传入文件操作结构体信息
cdev_init(&cdev, &mycdev_fops);
cdev.owner = THIS_MODULE; //所属模块赋值
// 注册到内核,cdev数据,设备号(起始),次设备数
cdev_add(&cdev, devno , 1);
if(ret < 0)
{
printk("register failed..\n"); //串口打印出失败信息
return ret;
}
printk("register success..\n");
// 在内核区域申请一块内存,用来放缓存数据kernel_buf
kernel_buf = kzalloc(sizeof(MYCDEV_SIZE), GFP_KERNEL);
if (NULL==kernel_buf)
{
return -1;
}
return 0;
}
上面的加载代码主要分下面几部分
(1)变量的定义和申请一个动态设备号
(2)将file_operation与cdev关联在一起
(3)然后将系统自动分配的设备号和cdev结构体向内核注册
(4)向内核区域申请一块内存空间来放入缓存数据
(5)打印提示信息
下面我主要对申请设备号,cdev结构体和向内核注册,申请内存部分进行分析
1.申请设备号
第一步先定义一个设备号变量devno;
dev_t devno; //定义一个设备号变量devno
第二步使用alloc_chrdev_region()函数进行动态申请设备号
ret = alloc_chrdev_region(&devno, 0, 1, "my_cdev");
备注:&devno是第一步定义的设备号变量的地址(系统动态分配设备号得到的设备号数据将传到这个地址上),0是指的是第一个从设备号,1是指要申请的从设备的个数,"my_cdev"指设备的名字 ,ret是返回值如果返回值小于0表示动态申请设备号失败相反动态申请设备号成功。
2.cdev结构体和向内核注册
第一步通过cdev_init()函数将初始化cdev结构体和file_opreration与cdev设备结构体关联起来
cdev_init(&cdev, &mycdev_fops);
备注:&cdev是驱动源码开始定义cdev结构体的全局变量的地址,&mycdev_fops是驱动源码中定义的操作文件结构体的地址
第二步 给cdev结构体中owner成员所属模块赋值
cdev.owner = THIS_MODULE;
第三步通过cdev_add()函数将cdev结构体和设备号向内核注册
cdev_add(&cdev, devno , 1);
备注:&cdev是驱动源码开始定义cdev结构体的全局变量的地址,devno是上面自动分配的设备号函数的输出变量,1表示要注册的从设备数
3.申请内存
使用kzalloc函数在内核区域申请一块内存,用来存放缓存数据
kernel_buf = kzalloc(sizeof(MYCDEV_SIZE), GFP_KERNEL);
备注:sizeof(MYCDEV_SIZE)表示要申请的空间大小,GFP_KERNEL是权限标准位,kernel_buf是存放kzalloc()函数的返回值(kzalloc()函数如果申请内存成功会将该内存的头地址返回。申请失败的话kernel_buf将会是NULL)
2.3创建字符设备节点
字符驱动的加载函数运行将cdev结构体和设备号向内核注册后接下来需要创建设备节点来供用户使用
1.查看系统自动分配的字符设备主设备号
命令行输入:cat /proc/devices //查看/proc/devices文本
如下图所示Character devices对应的是字符设备号, my_cdev在驱动源码中向内核注册的设备名字 ,250是内核自动分配的主设备号
- 在dev目录下创建字符设备节点
(1)使用mknod命令出创建字符设备节点 格式 mknod 设备名 c/b 主设备号 从设备号 -m 777
(c/b分别表示字符设备和块设备 主设备号如上面查看/proc/devices文本得出 从设备号是在驱动代码中写入的 -m 777 表示修改该设备节点的权限)
命令行输入:sudo mknod my_cdev c 250 0 -m 777 //创建字符设备节点和修改设备节点的权限
备注:可以使用chmod命令修改设备节点权限
使用格式:chmod 777 filename //修改该文件的权限
(2)使用ls -l 命令查看创建的字符节点如下图
备注:crwxrwxrwx c表示是字符设备 rwx表示权限 250表示主设备号 0表示从设备号
3.实现函数代码分析和调试程序
3.1实现函数的分析
为了达到对内存的操作实现了read write 函数下面对他们进行分析
static ssize_t mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
if(size > MYCDEV_SIZE) //判断是否要读取的大小大于缓存数组的大小
size = MYCDEV_SIZE; //如果上面的if成立将缓存数组的数据赋值给读取的大小
if(copy_from_user(kernel_buf, buf, size) != 0) //如果操作失败返回值不等于0
{
printk("write error!\n"); //打印出写入失败的串口信息
return -1;
}
printk("write:%d bytes was write...\n",size);
return size; //返回读取的大小
}
eg:mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)中
*fp指的是文件结构体指针 *buf用户空间内存地址指针 size 要写入的字节数 *pos 读取的位置相对于文件开头的偏移
由于用户空间不能直接访问内核空间的内存,因此借助了函数**copy_from_user()完成用户空间缓冲区到内核空间的复制,以及copy_to_user()**完成内核空间到用户空间缓冲区的复制
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。如果复制失败,则返回负值
在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员,函数如下:对应的具体设备驱动函数名赋值给file_operations的成file_opreations结构体的成员
static const struct file_operations mycdev_fops = //文件操作结构体
{
.owner = THIS_MODULE,
.read = mycdev_read,
.write = mycdev_write,
.open = mycdev_open,
.release = mycdev_release,
}
```c
## 3.2调试代码
1.编写用户态测试程序
```c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int testdev;
int i,ret;
char buf[11]; //定义一个数组可以存放从内核中读取到的数据
testdev = open("/dev/my_cdev",O_RDWR);//打开/dev/my_cdev设备文件
if(testdev == -1) //如果返回值是-1表示打开设备文件失败
{
printf("connot open file..\n");
exit(1);
}
if((ret = read(testdev,buf,11)) <11)//read()函数向内核中读取的缓存数据
{
printf("read error!\n");
exit(1);
}
for(i=0;i<11;i++)
{
printf("%c",buf[i]); //打印出从内核读取的数据
}
printf("\n");
close(testdev); //关闭设备文件
return 0;
}
2.运行测试程序
在运行测试程序之前先对驱动设备进行写入缓存数据
命令行输入:echo "hello world"> /dev/my_cdev //将hello world 写入内核的缓存数据
将上面的测试用户态程序编译后执行 效果如下图
4.卸载驱动函数分析
4.1分析模块卸载函数
通过rmmod命令将驱动卸载 内核会执行驱动代码中对应的卸载函数
static void __exit mycdev_exit(void) //卸载函数
{
printk("mycdev module is leaving..\n");
// 注销cdev
cdev_del(&cdev);
// 释放内存
kfree(kernel_buf);
// 释放设备号
unregister_chrdev_region(devno, 1);
}
1.cdev_del()函数
cdev_del(&cdev); //注销内存
&cdev是驱动源码中cdev结构体地址 cdev_del()函数的作用就是向内核注销cdev结构体。
2.kfree()函数
kfree(kernel_buf); //释放内存
kernel_buf是指向内存的指针 kfree()函数函数的作用就是释放在本驱动申请的内存(避免占用浪费资源)
3.unregister_chrdev_region()函数
unregister_chrdev_region(devno, 1); //释放设备
egister_chrdev_region(devno, 1);
}
1.cdev_del()函数
cdev_del(&cdev); //注销内存
&cdev是驱动源码中cdev结构体地址 cdev_del()函数的作用就是向内核注销cdev结构体。
2.kfree()函数
kfree(kernel_buf); //释放内存
kernel_buf是指向内存的指针 kfree()函数函数的作用就是释放在本驱动申请的内存(避免占用浪费资源)
3.unregister_chrdev_region()函数
unregister_chrdev_region(devno, 1); //释放设备号