WDS1期第12课 字符设备驱动框架 围绕cdev


模块是linux内核进行组件管理的一种方式,
驱动是基于模块进行注册和注销的,
不单单是字符设备,块设备驱动和网络设备驱动都是基于模块进行加载和卸载的。

字符设备(字符为单位,读写同步)
I/O传输过程中以字符为单位进行传输,
用户对字符设备发出读/写请求时,实际的使件读/写操作一般紧接着发生块设备。
eg:LCD 鼠标 键盘 触摸屏 串口
块设备(以块为单位,异步)
与字符相反,它的数据传输以块(内存缓冲)为单位传输用户对块设备读,硬件上的读/写操作不会紧接着发生,即用户请求和硬件操作是异步的
eg:磁盘类、闪存类等设备都封装成块设备

设备文件:字符设备 和块设备有设备文件,网络设备没有设备文件。

内核统一管理设备,所以编写内核驱动程序需要遵循内核的一些规则。内核将字符设备/块设备/网络设备抽象成对应的结构体

一、 描述字符设备的结构体(cdev)

在include/linux/cdev.h中定义,有两个成员很重要dev,ops,

struct cdev {
	struct kobject kobj; 				// 设备模型相关 
	struct module *owner;				// 设备模型相关,赋值THIS_MODULE,设备驱动属于哪个模块
	const struct file_operations *ops;  // 函数指针,操作方法集
	struct list_head list;				// 内核通过这个list管理设备
	dev_t dev;							// 设备号
	unsigned int count;					// 设备个数,一个驱动 驱动了多个设备
};
1. 设备号 (dev_t dev)

用来标识设备,唯一。
32位无符号整型,12位主设备 + 20位次设备(根内核版本有关吧)
在/usr/include/linux/kdev_t.h中,

#define MAJOR(dev)	((dev)>>8)				// 提取主设备号
#define MINOR(dev)	((dev) & 0xff)			// 提取次设备号
#define MKDEV(ma,mi)	((ma)<<8 | (mi))	// 根据 主次设备号 合成 设备号
2. 操作方法集(file_operations *ops)

提供给应用层的一些操作方法集,open write read

/*
 * NOTE:
 * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
 * can be called without the big kernel lock held in all filesystems.
 */
struct file_operations {
	...
	struct module *owner;
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	int (*open) (struct inode *, struct file *);
	int (*release) (struct inode *, struct file *);
	...
};

二、 字符设备驱动框架用到的一些API

这些东西都是在内核模块框架下的。

0. 分配设备号
0.1 分配设备号,注册设备号
  1. 自动分配设备号
    /*********************************************
    功能:分配设备号
    参数:dev		设备号指针,分配到的设备号就从指针传出
    	 baseminor  次设备号起始,一般从0开始
    	 count		次设备个数
    	 name		设备号对应的名字
    返回值:成功返回0,失败返回负数错误码
    **********************************************/
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
    			const char *name)
    
  2. 指定设备号
    /*********************************************
    功能:分配设备号
    参数:from		需要程序员指定的设备号,MKDEV(major, minor),起始在次设备号中
    	 count		次设备个数
    	 name		设备号对应的名字
    返回值:成功返回0,失败返回负数错误码
    **********************************************/
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    
0.2 注销设备号
/*********************************************
功能:从内核中注销设备号
参数:from		需要注销的设备号
	 count		次设备个数
	 name		设备号对应的名字
返回值:void
**********************************************/
void unregister_chrdev_region(dev_t from, unsigned count);
1. 为cdev结构体分配内存空间

在fs/char_dev.c中,cdev_alloc为cdev结构体分配内存空间,

/*********************************************
功能:为cdev结构体分配空间
参数:void
返回值:分配成功返回分配得到的结构体指针
	   失败返回NULL
**********************************************/
struct cdev *cdev_alloc(void)

可能由于内存不够 导致分配失败,一定要处理分配失败的情况,并且如果要返回一定要把前面分配到的资源释放掉,包含多层调用一定要查清楚分配了哪些资源,然后依次释放。

2. 初始化cdev结构体

在fs/char_dev.c中,

/*********************************************
功能:初始化结构体
参数:
	cdev	cdev结构体指针
	fops	操作方法集指针
返回值:void
**********************************************/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3. 添加(注册)字符设备到内核

注册字符设备中,由内核统一管理,在fs/char_dev.c中,

/*********************************************
功能:添加(注册)字符设备到内核中
参数:
	p		cdev结构体指针
	dev		设备号
	count	设备个数
返回值:
	int 成功返回0,失败返回错误码-ENOMEM
**********************************************/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  1. cdev_add返回值的情况
    在drivers/base/map.c中定义kobj_map函数,成功返回0,失败返回错误码
    kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p)
    { 
    	...
    	p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
    
    	if (p == NULL)
    		return -ENOMEM; // #define	ENOMEM	12	/* Out of memory */
    	...
    		return 0;
    }
    
  2. 第二参数dev设备号
    设备号内核统一管理,属于内核的资源,设备号资源,需要申请设备号资源
4. 删除(注销)字符设备

在不用设备时,一定要注销该设备,应该在内核模块的卸载函数中,

/*********************************************
功能:从内核中删除(注销)字符设备
参数:p	cdev结构体指针
返回值:void
**********************************************/
void cdev_del(struct cdev *p)

三、 编写字符设备驱动框架

所有驱动都是基于内核模块去完成的,内核模块三要素:
入口函数
出口函数
GPL协议声明

驱动注册时,就要申请cdev内存,执行初始化,申请设备号等;
当卸载驱动时就要去释放内存资源,注销设备号。

1. module_demo.c文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>  // 操作方法集
#include <linux/cdev.h> 

#define BASEMINOR   0
#define COUNT       3
#define NAME        "chrdev_demo"

// 保存申请到的设备号
dev_t devno; 
// 抽象的字符设备结构体
struct cdev *cdevp;

// 操作方法集内的函数指针(函数)定义
int demo_open(struct inode *inode, struct file *filep)
{
    printk("---%s---%s---%d", __FILE__, __func__, __LINE__);
    return 0;
}
int demo_release(struct inode *inode, struct file *filep)
{
    printk("---%s---%s---%d", __FILE__, __func__, __LINE__);
    return 0;
}
// 定义操作方法集结构体,并部分初始化
struct file_operations fops = { 
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .release = demo_release,
};

static int __init module_demo_init(void)
{
    int ret = 0; // 用于保存分配到的设备号

    // 0. 获取设备号 alloc_chrdev_region 自动获取设备号
    ret = alloc_chrdev_region(&devno, BASEMINOR, COUNT, NAME);
    if(ret < 0)
    {
        printk(KERN_ERR "alloc_chrdev_region failed.\n");
        goto err1_alloc_chrdev_region;
    }
    printk(KERN_INFO "major = %d\n", MAJOR(devno)); // 主设备号是内核分配的,次设备是自己指定,提取主设备号打印
    // 1. 分配内存 cdev_alloc
    cdevp = cdev_alloc();
    if(cdevp == NULL)
    {
        printk(KERN_ERR "cdev_alloc failed.\n");
        ret = -ENOMEM;     // #define	ENOMEM		12	/* Out of memory */
        goto err2_cdev_alloc;
    }
    // 2. 初始化cdev结构体 cdev_init
    cdev_init(cdevp, &fops);
    // 3. 注册设备到内核 cdev_add
    ret = cdev_add(cdevp, devno, COUNT);
    if(ret < 0)
    {
        printk(KERN_ERR "cdev_add failed.\n");
        goto err2_cdev_alloc;
    }
    printk("---%s---%s---%d", __FILE__, __func__, __LINE__); // 打印此行说明前面成功
    return 0;                                                // 成功之后就不能执行下面的err释放资源步骤

    // 释放资源,申请资源的倒序
    err2_cdev_alloc:
        unregister_chrdev_region(devno, COUNT);
    err1_alloc_chrdev_region:
        return ret;
}

static void __exit module_demo_exit(void)
{
    // 注销设备号
    unregister_chrdev_region(devno, COUNT); 
    // 注销字符设备
    cdev_del(cdevp);
    printk("---%s---%s---%d", __FILE__, __func__, __LINE__); // 打印此行说明注销成功
}

module_init(module_demo_init);
module_exit(module_demo_exit);

MODULE_LICENSE("GPL");
2. Makefile文件
KERNDIR := /lib/modules/$(shell uname -r)/build # 系统内核的路径
PWD := $(shell pwd)		# 执行pwd命令并把结果赋给PWD

obj-m := module_demo.o  # .ko的生成依赖于.o,.o默认依赖.c

all:
	make -C $(KERNDIR) M=$(PWD) modules

clean:
	make -C $(KERNDIR) M=$(PWD) clean

插入内核模块module_demo.ko到内核,查看内核日志,卸载内核模块,查看内核日志,
在这里插入图片描述重新分配得到的设备号根之前的一样,说明上次释放资源成功,
在这里插入图片描述

三个重要的结构体

1. inode
struct inode {
	...
	dev_t			i_rdev;
	struct cdev		*i_cdev;
	...
}
2.fops
struct file_operations {
	...
	struct module *owner;
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	int (*open) (struct inode *, struct file *);
	int (*release) (struct inode *, struct file *);
	...
};
3. filep
struct file {
	...

	mode_t			f_mode;
	loff_t			f_pos;
	unsigned int 		f_flags;
	const struct file_operations	*f_op; 
	void			*private_data;
	...
};

inode结构体将设备号dev_t和字符设备抽象结构cdev联系起来,字符设备抽象结构cdev将设备号dev_t和操作方法集fops联系起来,
cdev_init函数用操作方法集fops去初始化字符设备抽象结构cdev(绑定驱动程序到cdev)
参考牛人的博客
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值