一张图掌握 Linux 字符设备驱动架构!建议收藏

Posted 火山上的企鹅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一张图掌握 Linux 字符设备驱动架构!建议收藏相关的知识,希望对你有一定的参考价值。


面对疾风吧:

上面经过爆肝几夜总结的,建议保存下来再放大看,结合文章的话更香哦~


所有的热爱都要不遗余力,真正喜欢它便给它更高的优先级,和更多的时间吧!

Linux系统&驱动其它文章:     Linux


一. Linux 中字符设备驱动简介

Linux 中的有三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

其中字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的点灯到 I2C、SPI、音频等都属于字符设备驱动的类型。

块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。

所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。

网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。

一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

二. 字符设备驱动快速入门(超简单demo)

如果拿到一个新的代码,我觉得第一步应该先直接运行,让它跑起来,这样会有更直观的认识,即便是最底层的驱动, 咱们先上个最简单的字符驱动代码,不用管具体的含义。

1. demo

//test_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

MODULE_LICENSE("GPL");

dev_t dev;						//定义设备号
unsigned int major , minor ;	//定义主设备号和次设备号
struct cdev test_cdev;          //定义一个测试的cdev变量

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量,一般在 open 的时候将private_data 指向设备结构体。
 */
int test_open(struct inode *inode, struct file *filp)
{
    printk("call %s\\n", __func__);
    return 0;
}

int test_close(struct inode *inode, struct file *filp)
{
    printk("call %s\\n", __func__);
    return 0;
}

//3. 文件操作集合的绑定
struct file_operations test_fops=
{
    .owner = THIS_MODULE,
    .open  = test_open,
    .release = test_close,
};

//初始化函数,在加载时调用
int __init char_drv_init(void)
{
	/* 注册字符设备驱动 */
    //2. 创建设备号
    if(major) {										//定义了主设备号, 就是用静态分配
        dev = MKDEV(major, minor);					 //大部分驱动次设备号都选择 0
        register_chrdev_region(dev, 1, "test_drv");  //注册设备号,当驱动加载后  查看 cat /pro/devices
    }
    else { 											//没有定义设备号,则动态分配			
        alloc_chrdev_region(&dev,minor,1,"test_drv");	//申请设备号
        major = MAJOR(dev);							//获取分配号的主设备号
        minor = MINOR(devid); 						//获取分配号的次设备号
        printk("major=%d, minor =%d\\n", major, minor);				
    }

	//4. 注册cdev
    //4.1 初始化 cdev
    cdev_init(&test_cdev, &test_fops);
    
    //4.2 添加一个cdev (向 Linux 系统添加这个字符设备)
    cdev_add(&test_cdev, dev, 1);
    return 0;
}

//退出函数,在卸载时调用
void __exit char_drv_exit(void)
{
    /*注销cdev*/
    cdev_del(&test_cdev);
    /*注销设备号*/
    unregister_chrdev_region(dev, 1);
}

//1. 驱动模块加载和卸载的入口函数
module_init(char_drv_init);
module_exit(char_drv_exit);

用户测试代码:

//test_app.c
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
	 //fd :文件描述符
    int fd = 0;
   	//打开驱动文件 
    fd = open("/dev/myleds", O_RDWR);
    
    if(fd < 0)
    {
        perror("open failed");
        return -1;
    }

    printf("open myleds successed,using device...\\n");
    sleep(5);

    printf("end of using device,closing device\\n");
    //关闭设备
    close(fd);
    printf("exit from main\\n");
    return 0;
}

2. 代码编译

Makefile :

KERNELDIR := /home/chunn/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := test_drv.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1 行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径
:= 表示定义的变量不能修改(类似于const);
第 3 行,obj-m 表示将 test_drv.c 这个文件编译为 test_drv.ko 模块。
第 8 行,具体的编译命令,后面的 modules 表示编译模块,-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。M 表示模块源码目录,“make modules” 命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
 make //执行make

● 编译测试APP 和查看编译后文件:

arm-linux-gnueabihf-gcc test_app.c -o test_app

3. 加载驱动模块

insmod chrdevbase.ko

打印相关信息:

用 lsmod 查看下test_drv 模块:

运行了char_drv_init() , 且为自动分配的设备号,说明模块加载成功了!

4. 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/test_drv 这个设备节点文件:

mknod /dev/test_drv c 248 0

其中 “mknod” 是创建节点命令,“/dev/test_drv ” 是要创建的节点文件,“c” 表示这是个字符设备,“248” 是设备的主设备号,“0”是设备的次设备号。

创建完成以后就会存在 /dev/test_drv 这个文件,可以使用 “ls /dev/test_drv -l” 命令查看

5. APP设备文件操作

直接执行

./test_app


后续可以通过 APP 的 main 函数入口传入用户指令,就可以进行相关操作了如下:

./test_app /dev/test_drv xx

xx 为入口参数

6. 卸载驱动模块

rmmod chrdevbase.ko

应用大概就是这个流程:

三. 字符设备驱动开发流程介绍:

1. 驱动模块加载和卸载的入口函数

Linux 驱动有两种运行方式:
① 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
② 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。

在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用。
module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

2. 字符设备注册与注销

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成:

2.1 静态注册与注销

选出一个内核中未被使用的主设备,为我所用(主设备号为0~255)

查看内核中哪些主设备号已经被占用:

 1. cat /proc/devices
 2. vi Documentation\\devices.txt (具体分配的内容)
/ *
  * @ 作用:注册连续的多个设备号
  * @from: 起始设备号
  * @count:连续注册的个数
  * @name: 名称
  * @返回值:成功返回0
      失败返回<0的错误编号
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name) 

//作用:注销从from开始的连续count个设备号
void unregister_chrdev_region(dev_t from, unsigned count)
             

用户查看:

insmod char_drv.ko
 cat /proc/devices

2.2 动态注册

由内核帮我们挑一个未被使用的主设备号,和我们自己规定的次设备号拼成设备号

 /*
  *  @作用:注册连续多个设备号
  *  @dev, 注册成功的第一个设备号
  *  @baseminor, 起始次设备号
  *  @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)
                 

测试用例,两者都使用了:

//1. 创建设备号
if(major) {										//定义了主设备号, 就是用静态分配
    dev = MKDEV(major, minor);					 //大部分驱动次设备号都选择 0
    register_chrdev_region(dev, 1, "test_drv");  //注册设备号,当驱动加载后就生成 /dev/test_drv这个设备文件
}
else { 											//没有定义设备号,则动态分配			
    alloc_chrdev_region(&dev,minor,1,"test_drv");	//申请设备号
    major = MAJOR(dev);							//获取分配号的主设备号
    minor = MINOR(devid); 						//获取分配号的次设备号
    printk("major=%d, minor =%d\\n", major, minor);				
}

3. struct file_operations 文件操作集合

实现一个字符设备驱动, 实则就是实例化一个cdev,实例化一个cdev就是定义一个struct cdev 类型的变量,并做好初始化,其中关键就是是如何实现函数操作集合 file_operations ,它也是字符设备驱动开发过程中最主要的工作。

//文件操作集合的绑定
struct file_operations test_fops=
{
    .owner = THIS_MODULE,
    .open  = test_open,    
    .release = test_close,
    .read = xxx,
    .write = xxx,
    .unloched_ioctrl = xxx,
};

open、release对应应用层的open()、close()函数。实现比较简单,

其中read、write、unloched_ioctrl 函数的实现需要涉及到用户空间和内存空间的数据拷贝。

在Linux操作系统中,用户空间和内核空间是相互独立的。也就是说内核空间是不能直接访问用户空间内存地址,同理用户空间也不能直接访问内核空间内存地址。

如果想实现,将用户空间的数据拷贝到内核空间或将内核空间数据拷贝到用户空间,就必须借助内核给我们提供的接口来完成。

此部分后续文章再补充。

4. 注册cdev

定义好 file_operations 结构体,就可以通过函数cdev_init()、 cdev_add()注册字符设备驱动了。

	struct cdev test_cdev;          				//上文 定义的cdev变量

   //3.1. 初始化 cdev
   cdev_init(&test_cdev,  &test_fops);
   
   //3.2 添加一个cdev (向 Linux 系统添加这个字符设备)
   cdev_add(&test_cdev, dev, 1);      

cdev_init() 中最重要的是 绑定自定义的函数操作集合

cdev_add() 中最重要的是添加一个cdev (向 Linux 系统添加这个字符设备)

注意如果使用了函数register_chrdev(),就不用了执行上述操作,因为该函数已经实现了对cdev的封装。

5. 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/test_drv 这个设备节点文件:

mknod /dev/test_drv c 248 0

其中 “mknod” 是创建节点命令,“/dev/test_drv ” 是要创建的节点文件,“c”表示这是个字符设备,“248” 是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在 /dev/test_drv 这个文件

这样 APP程序 就可以直接对 /dev/test_drv 进行文件操作了。相当于/dev/test_drv 这个文件是 test_drv 设备在用户空间中的实现。 Linux 下一切皆文件,这个设备文件也当然是文件。

总的说来设备文件是用户空间访问内核空间驱动的介质,将对文件的访问变成了对特定cdev的访问(主设备号和次设号)

● 设备节点的自动创建

第一步 :通过宏class_create() 创建一个class类型的对象;

第二步: 导出我们的设备信息到用户空间 device_create()

这里只是提一下,后续有时间再详细介绍。

当然 Linux 字符驱动远远不止这些 ,如关于_Kobject 这一块还是一知半解,还有read 、write 等都未列出,路漫漫其修远~


【参考】

● 公众号:一口Linux ,作者土豆居士

● 公众号:老吴的嵌入式之旅,作者吴伟东Jack

● 《Linux设备驱动程序》

● 《深入Linux内核架构》

● 《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.1》

● Linux内核源码 4.1.15

《Linux 字符设备驱动结构(一)》


Linux系统&驱动其它文章:     Linux

以上是关于一张图掌握 Linux 字符设备驱动架构!建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

一张图掌握 Linux platform 平台设备驱动框架!建议收藏

一张图掌握 Linux platform 平台设备驱动框架!建议收藏

这都2021年了还不懂Linux?一张思维导图帮你理清思路!建议收藏!

这都2021年了还不懂Linux?一张思维导图帮你理清思路!建议收藏!

这都2021年了还不懂Linux?一张思维导图帮你理清思路!建议收藏!

这都2021年了还不懂Linux?一张思维导图帮你理清思路!建议收藏!