最全Linux驱动开发八股文(六)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询

———————————————————————————————————————————————————

6.2 新字符设备基本驱动框架

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在 Linux 下通过 udev(用户空间程序)实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。

1.设备文件系统

设备文件系统有devfs,mdev,udev这三种

  1. devfs, 一个基于内核的动态设备文件系统
  • devfs缺点(过时原因)
    • 不确定的设备映射
    • 没有足够的主/辅设备号
    • /dev目录下文件太多
    • 内核内存使用
  1. udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
  • udev和devfs的区别
    • 采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动
    • udev的Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
    • 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
  1. mdev,是udev的简化版本,是busybox中所带的程序,适合用在嵌入式系统

2.申请设备号

上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号

  • 动态申请设备号

    int alloc_chrdev_region(dev_t *dev, 
    						unsigned baseminor, 
    						unsigned count, 
    						const char *name)
    

    dev:保存申请到的设备号。 baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0 count: 要申请的设备号数量。 name:设备名字。

  • 静态申请设备号

    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    

    from - 要申请的起始设备号
    count - 设备号个数
    name - 设备号在内核中的名称 返回0申请成功,否则失败

  • 释放设备号

    void unregister_chrdev_region(dev_t from, unsigned count)
    

    from:要释放的设备号。 count: 表示从 from 开始,要释放的设备号数量。

  • 申请设备号模板

    //创建设备号 
    if (newchrled.major)   //定义了设备号就静态申请
    {		
    	newchrled.devid = MKDEV(newchrled.major, 0);
    	register_chrdev_region(newchrled.devid, 
    							NEWCHRLED_CNT, 
    							NEWCHRLED_NAME);
    } 
    else   //没有定义设备号就动态申请
    {		
     
    	alloc_chrdev_region(&newchrled.devid, 
    						0, 
    						NEWCHRLED_CNT, 
    						NEWCHRLED_NAME);//申请设备号 
    	newchrled.major = MAJOR(newchrled.devid);	//获取分配号的主设备号
    	newchrled.minor = MINOR(newchrled.devid);	// 获取分配号的次设备号
    }
    

3.注册字符设备

在 Linux 中使用 cdev 结构体表示一个字符设备

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;//操作函数集合
	struct list_head list;
	dev_t dev;//设备号
	unsigned int count;
};

在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t

  • 初始化cdev结构体变量

    void cdev_init(struct cdev *cdev, 
    				const struct file_operations *fops);
    

    struct cdev testcdev;
    //设备操作函数
    static struct file_operations test_fops = {
        .owner = THIS_MODULE,
        //其他具体的初始项 
    };
    testcdev.owner = THIS_MODULE;
    //初始化 cdev 结构体变量
    cdev_init(&testcdev, &test_fops); 
    
  • 将设备添加到内核

    cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备

    将cdev添加到内核同时绑定设备号。 其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    

    p - 要添加的cdev结构
    dev - 绑定的起始设备号
    count - 设备号个数

    cdev_add(&testcdev, devid, 1); //添加字符设备
    
  • 将设备从内核注销 卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

    void cdev_del(struct cdev *p);
    

    p - 要添加的cdev结构

    cdev_del(&testcdev); //删除 cdev
    

4.自动创建设备节点

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。

  • 创建一个class类

    struct class *class_create(struct module *owner, const char *name);
    
    • class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
    • 设备类名对应 /sys/class 目录的子目录名。
    • 返回值是个指向结构体 class 的指针,也就是创建的类。
  • 删除一个class类

    void class_destroy(struct class *cls); // cls要删除的类
    
  • 创建设备 还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。 成功会在 /dev 目录下生成设备文件。

    struct device *device_create(struct class *class,
        						struct device *parent,
        						dev_t devt,
        						void *drvdata,
    							const char *fmt, ...)
    

    *class——设备类指针, *parent——父设备指针, devt——设备号, *drvdata——额外数据, *fmt——设备文件名

  • 删除设备 卸载驱动的时候需要删除掉创建的设备

    void device_destroy(struct class *class, dev_t devt);
    

    class——设备所处的类 devt——设备号

5.文件私有数据

  • 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
  • 一个设备的所有属性信息将其做成一个结构体,
  • 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
  • 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};
struct newchrled_dev newchrled;	/* led设备 */
 
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}
 
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
	return 0;
}

6.新字符设备驱动程序框架

#define NEWCHR_CNT 1
#define NEWCHR_NAME "NEWCHR"
//内核缓存区
static char readbuf[100];						//读数据缓存
static char writebuf[100];						//写数据缓存
static char kerneldata[] = {"kernel data!"};	//测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) 	//宏定义寄存器映射地址
static void __iomem *GPIO_TEST;			// __iomem 类型的指针,指向映射后的虚拟空间首地址

/* newchr设备结构体 */
struct newchr_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};
 
struct newchrled_dev newchr;	/* newchr设备 */

//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp) 
{
	filp->private_data = &newchr; /* 设置私有数据 */
	return 0;
}
// 从设备读取数据 
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) 
{
	int retvalue = 0;
	unsigned char databuf[1];
	//读取私有数据
	struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
// 读取硬件寄存器
#if 0  
	//读取寄存器状态
	databuf[0] = readl(GPIO_TEST);
	retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else	
	//测试数据拷贝到读数据缓存中
    memcpy(readbuf , kerneldata , sizeof(kerneldata));  
    //内核中数据(读缓存)拷贝到用户空间
    retvalue = copy_to_user(buf , readbuf , cnt);
#endif

    if(retvalue == 0) printk("kernel senddate ok!\n");   
  	else printk("kernel senddate failed!\n");
    return 0;
}
//向设备写数据 
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt) 
{
	int retvalue = 0;
	//读取私有数据
	struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
//写硬件寄存器
#if 0
	writel(buf[0],GPIO_TEST);
//写内核缓存
#else
	//用户数据拷贝到内核空间(写缓存)
    retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
    if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
  	else printk("kernel recevdate failed!");
    return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp) 
{
	return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;
	//寄存器物理映射,物理地址映射到虚拟地址指针
	GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
	
	//申请设备号
    if(newchr.major)		//静态申请
    {
        newchr.devid = MKDEV(newchr.major , 0);
        register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);
    }else					//动态申请
    {
        alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);
        newchr.major = MAJOR(newchr.devid);
        newchr.minor = MINOR(newchr.devid);
    }   
    printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);

	//字符串设备初始化、注册添加到内核
	newchr.cdev.owner = THIS_MODULE;
    cdev_init(&newchr.cdev , &newchr_fops);
    cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);
	//创建设备类
    newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);
    if(IS_ERR(newchr.class))
    {
        return PTR_ERR(newchr.class);
    }
	//创建类的实例化设备 ,dev下面创建文件
    newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);
    if(IS_ERR(newchr.device))
    {
        return PTR_ERR(newchr.device);
    }
    return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
	//解除寄存器映射
	iounmap(GPIO_TEST);
	//删除cdev字符串设备
	cdev_del(&newchr.cdev);
	//释放设备号
    unregister_chrdev_region(newchr.devid , NEWCHR_CNT);
	//具体设备注销
    device_destroy(newchr.class, newchr.devid);
    //类注销
    class_destroy(newchr.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息
#嵌入式##秋招##八股文##Linux##linux驱动#
拉依达的Linux驱动八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

02-01 12:05
复旦大学 Java
腾讯的提前批大概率应该是没有笔试的,但是这个时候有相当部分的同学简历估计都没有准备好,没准备好的同学也不用急,大部分都是3月之后开,这个时候开的绝大多数都是神仙打架,问的东西也比较难,打算投递的同学也多看下计算机网络和操作系统,腾讯对这部分的知识问的比较多。另外多刷下牛客的热门题库,刷题注意刷ACM模式,和牛客的周赛题,腾讯有的部门会从这里面出原题。我是@程序员花海关注我,带你了解更多校招资讯!
程序员花海:还没有来得及准备的同学可以看下学习路线:https://www.nowcoder.com/discuss/824693499982315520?sourceSSR=users算法题:https://www.nowcoder.com/feed/main/detail/20e7a999fa04485b88340a274411ca0d?sourceSSR=users八股文:https://www.nowcoder.com/discuss/833102362771251200?sourceSSR=users简历书写方式:https://www.nowcoder.com/discuss/839907820706205696?sourceSSR=users都是以前在牛客发的文章~
软开人,秋招你打算投哪些...
点赞 评论 收藏
分享
秋招结束已经一段时间了 一直在忙着毕业的事情 浅浅总结一下自己的秋招经历吧~本人BG双非硕 后端选手 有一段小厂+腾讯暑期实习腾讯暑期转正loser秋招结束已经结束了有一段时间了总结一下秋招历程最大的感受就是秋招比起暑期更加卡学历秋招总共投了60多家吧一直面 一直挂也投了两家银行科技岗 都走到终面体检了都拒了(总体感觉本地的银行还是挺容易过的)可能本人更想去私企 并且银行也挺卷听说一直到11月就只有一家小厂的offer并签约当保底然后也突然被WXG捞了 本来都不对腾讯抱有希望了可能经过一整个秋招的面试积累吧 以及本人有ACM经历 WXG整体面试以做题偏多(一二面做了5道题 4道hard) 比较合自己胃口 差不多半个月就把五轮面试过了进入录用评估 但也一直没有结果到后面也陆陆续续有几家中厂也终面过泡池子一直到12月初华子给开了base杭州 14a因为华子公积金的原因 和小厂薪资上差距不大 所以也一直犹豫是否毁约签华子 但是内心也还对WXG抱有一丝幻想(虽然一直没有保温也没有任何消息)然后一直到12月中下旬 华子要求去现场签约了 但是WXG还是没有消息 然后就连续发邮件和打电话催了好多次 还是回复耐心等待直到华子签约那天 经过内心挣扎已经决定毁约签华子了 可能还是想平台更大一点吧 然后最戏剧性的一幕来了 就在我发毁约邮件没有5秒 WXG打电话开奖了 并且开奖也十分有诚意 最终还是没有签约成功华子 研究生期间也打了很多次华子的比赛还是对华子有感情的555整个秋招都是伴随着焦虑的 我认为自己也是秋招大部分人的画像 屡屡碰壁后不断怀疑自己 但是可能自己也比较幸运吧 但是也感谢自己在一次次陷入迷茫都没有放弃自己 还是一直努力背八股 刷题也祝各位牛友们共勉 就算暂时没有好的offer 不放弃一定会有好的结果的!!
点赞 评论 收藏
分享
评论
2
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务