前言:
第一节只是了解了字符设备驱动程序是以内核模块的形式存在的,学习了内核内核模块的加载过程,本节将对字符设备的驱动框架做一个学习!
文章目录
一、理论知识
1.Linux设备分类
字符设备、块设备、网络设备三大类,具体概念可以网上有很多。
2. cdev介绍
首先理解 创建的设备节点是一个具体的设备,但是对设备的操作是通过文件的形式。因为Linux中一切皆文件,所有设备的访问都是通过文件的形式。
Linux内核中,使用 struct cdev 来描述一个字符设备,这个结构体中会包含字符设备相关的信息、操作字符设备的打开、读写等接口函数(file_operations)。应用程序通过调用标准化的接口去访问设备,驱动程序负责将这些标准的调用映射到实际的硬件上。
2.1 cdev数据结构
源码目录:D:\imx6ull\imx-linux4.9.88\include\linux\cdev.h
struct cdev {
struct kobject kobj; //内嵌的内核对象
struct module *owner; //模块所有者
const struct file_operations *ops; //操作方法集(核心驱动程序)
struct list_head list; //字符设备链表
dev_t dev; //字符设备的设备号
unsigned int count; //设备数量
};
2.2 cdev操作函数
源码路径:D:\imx6ull\imx-linux4.9.88\fs\char_dev.c
(1)构造一个cdev结构体
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
(2)初始化结构体成员
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev); //初始化cdev
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; //重点理解 将cdev的 operations 和file的operations关联起来
}
注意:cdev->ops = fops是非常重要的一步
(3)将我们注册的设备号和具体的cdev设备绑定到一起。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
(4)从系统中移除一个cdev(字符设备)
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
3. 设备号
主设备号:
在写代码的时候,会把驱动程序和对应的主设备号注册到系统中,所以主设备号用来标识设备对应的驱动程序。
次设备号:
驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。
这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。
设备号的申请
/*dev:用于保存申请到的设备号
*baseminor:次设备号起始地址,一般为0
*count:要申请的设备数量
*name:设备名字*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
设备号的释放
/*from:要释放的设备号
count:设备数量*/
void unregister_chrdev_region(dev_t from, unsigned count)
设备节点和设备号的关系
chardevs 数组
如果分配了一个设备号,就会创建一个 struct char_device_struct 的对象,并将其添加到 chrdevs 中;
static struct char_device_struct {
struct char_device_struct *next; // 结构体指针
unsigned int major; // 主设备号
unsigned int baseminor; // 次设备起始号
int minorct; // 次备号个数
char name[64]; //设备名
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];// 只能挂255个字符主设备
主、次设备号的cdev的关系
注意:调用cdev_add()将设备号和cdev关联起来
4. 设备节点,驱动,硬件设备的关联
可以根据箭头寻找连接关系,理解整个关系,需要大概了解以下三个概念,这几个知识点都会在后续文章中补充。
4.1 inode结构体
概念:
VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。内核使用inode结构体在内核内部表示一个文件。
核心成员:知道inode是和cdev字符设备是有联系的就行。
struct inode {
dev_t i_rdev; //表示设备文件的结点,这个域实际上包含了设备号(通过它找到对应的设备)
struct cdev *i_cdev; //struct cdev是内核的一个内部结构,它是用来表示字符设备的。
... 省略
};
4.2 file结构体
概念:
核心:这个 struct file 是内核中的数据结构,不会出现在用户层程序中。
file结构体指示一个已经打开的文件(设备对应于设备文件),其实系统中的每个打开的文件在内核空间都有一个相应的struct file结构体,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数,直至文件被关闭。如果文件被关闭,内核就会释放相应的数据结构。
以上部分注释截图,就是说里面的每一个函数可以重新实现,然后由cdev->ops = fops关联起来。
4.3调用流程
理解以上三个概念,下面对整个系统做一个总结:
系统调用open打开一个字符设备的时候, 通过一系列调用最终会执行下面的函数。
int chrdev_open(struct inode * inode, struct file * filp) //两个参数都是上面介绍的
这个函数做个以下几个工作:
1.根据设备号(inode->i_rdev), 在字符设备驱动模型中查找我们编写的对应的驱动程序。
2.设置inode->i_cdev , 指向找到的cdev。
3.将inode添加到cdev->list 的链表中.
4. 使用cdev的ops 设置file对象的f_op.
5. 如果ops中定义了open方法,则调用该open方法.
执行完 chrdev_open()之后,file对象的f_op指向cdev的ops,因而之后对设备进行的read, write等操作,就会执行cdev的相应操作。
二、实验操作
1.驱动代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEMO_DRV_NAME "char_drv"
#define CHAR_DRV_CLASS "char_drv_class"
#define CHAR_DRV_DEVICE "char_drv_device"
static dev_t dev_num; //字符设备的设备号
static struct cdev char_dev; //字符设备的结构体cdev
static struct class *char_drv_class; //创建类
static struct device *char_drv_device; //类下面的设备
/*函数参照imx-linux4.9.88\include\linux\fs.h*/
static int char_dev_open(struct inode *inode, struct file *filp)
{
printk("char_dev_open!\n");
return 0;
}
static ssize_t char_dev_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
printk("char_dev_read!\n");
return 0;
}
static ssize_t char_dev_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
printk("char_dev_write!\n");
return 0;
}
static int char_dev_release(struct inode *inode, struct file *filp)
{
printk("char_dev_release!\n");
return 0;
}
static struct file_operations char_dev_opt = {
.owner = THIS_MODULE,
.open = char_dev_open,
.read = char_dev_read,
.write = char_dev_write,
.release = char_dev_release,
};
static int __init char_dev_init(void)
{
int ret ;
/*1. 动态申请设备号*/
ret = alloc_chrdev_region(&dev_num, 0, 1,DEMO_DRV_NAME);
if(ret != 0)
{
printk(KERN_INFO "failed to alloc dev_num\n");
return -1;
}
/*2. 初始化cdev结构体,主要是将cdev与文件操作结构体file_operations*/
cdev_init(&char_dev, &char_dev_opt);
/*3. 添加设备到内核*/
ret = cdev_add(&char_dev, dev_num, 1);
if(ret != 0)
{
printk(KERN_INFO "failed to cdev_add\n");
return -1;
}
// 4. 创建设备类
char_drv_class = class_create(THIS_MODULE, CHAR_DRV_CLASS);
if (!char_drv_class)
{
printk(KERN_INFO"class_create failed!\n");
return -1;
}
// 5.类下面创建设备节点
char_drv_device = device_create(char_drv_class, NULL, dev_num, NULL, CHAR_DRV_DEVICE);
if (IS_ERR(char_drv_device))
{
printk(KERN_INFO"device_create failed!\n");
return -1;
}
return 0;
}
static void __exit char_dev_exit(void)
{
printk(KERN_INFO "char dev_exit !!!");
cdev_del(&char_dev); //删除设备
unregister_chrdev_region(dev_num, 1); //释放设备号
device_destroy(char_drv_class, char_drv_device);// 删除设备类下的节点
class_destroy(char_drv_class);// 删除设备类
}
module_init(char_dev_init);
module_exit(char_dev_exit);
MODULE_AUTHOR("123");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("demo module");
2.Makefile
KERNEL_DIR := /home/book/100ask_imx6ull-sdk/Linux-4.9.88
arch = arm
CROSS_COMPILE = arm-linux-gnueabihf-
obj-m += char_drv_demo.o
all:
make -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
make -C $(KERNEL_DIR) M=$(CURDIR) clean
rm -rf modules.order
3.测试程序
三、遗留问题
参考链接:
https://www.cnblogs.com/chen-farsight/p/6177870.html
https://blog.csdn.net/Mculover666/article/details/123445472?spm=1001.2014.3001.5501