• 零基础学Linux内核之设备驱动篇(6)_字符设备_实验篇1


    零基础学Linux内核系列文章目录

    前置知识篇
    1. 进程
    2. 线程
    进程间通信篇
    1. IPC概述
    2. 信号
    3. 消息传递
    4. 同步
    5. 共享内存区
    编译相关篇
    1. GCC编译
    2. 静态链接与动态链接
    3. makefile入门基础
    设备驱动篇
    1. 设备驱动概述
    2. 内核模块_理论篇
    3. 内核模块_实验篇
    4. 字符设备_理论篇1
    5. 字符设备_理论篇2
    5. 字符设备_实验篇1


    一、前言

    本节主要介绍如何使用一个驱动进行支持多个设备,以及使用宏定义container_of,将结构体的某个成员的地址,转换为该结构体的地址。


    二、前置条件


    三、本文参考资料

    《 [野火]i.MX Linux开发实战指南》
    百度


    四、正文部分

    4.1 驱动实验

    4.1.1 前置知识

    字符设备驱动程序是 以内核模块的形式存在

    要向系统注册一个新的字符设备,需要这几样东西:

    1. 定义字符设备结构体chr_dev
      static struct cdev chr_dev;
      
      • 1
    2. 定义字符设备的设备号
      static dev_t devno;
      
      • 1
    3. 定义文件操作结构体file_operations
      static struct file_operations  chr_dev_fops = 
      {
      	.owner = THIS_MODULE,
      	.open = chr_dev_open,
      	.release = chr_dev_release,
      	.write = chr_dev_write,
      	.read = chr_dev_read,
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    4.1.2 驱动框架(模板)

    #include 
    #include 
    #include 
    /* 其她头文件...... */
    
    /* 一些驱动函数 */
    static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    
    }
    
    static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
    {
    
    }
    
    static int xxx_open (struct inode *node, struct file *file)
    {
    
    }
    
    static int xxx_close (struct inode *node, struct file *file)
    {
    
    }
    /* 其它驱动函数...... */
    
    /* 定义自己的驱动结构体 */
    staticstruct file_operations xxx_drv = {
      .owner	 = THIS_MODULE,
      .open    = xxx_open,
      .read    = xxx_read,
      .write   = xxx_write,
      .release = xxx_close,
      /* 其它程序......... */
    };
    
    /* 驱动入口函数 */
    staticint __init xxx_init(void)
    {
    
    }
    
    /* 驱动出口函数 */
    staticvoid __exit hello_exit(void)
    {
    
    }
    
    /* 模块注册与卸载函数 */
    module_init(xxx_init);
    module_exit(xxx_exit);
    
    /* 模块许可证(必选项) */
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    4.1.3 内核模块加载函数

    在这里插入图片描述

    1. 采用动态分配的方式,获取设备编号devno,次设备号为0,设备名称为DEV_NAME(可通过命令cat /proc/devices查看),DEV_CNT为当前申请设备编号个数

      alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME)
      
      • 1
    2. 关联字符设备结构体cdev与文件操作结构体file_operations

      cdev_init(&chr_dev, &chr_dev_fops);
      
      • 1
    3. 添加设备至cdev_map散列表中

      cdev_add(&chr_dev, devno, DEV_CNT);
      
      • 1

    4.1.4 内核模块卸载函数

    在这里插入图片描述

    1. 注销设备号
      unregister_chrdev_region(devno, DEV_CNT);
      
      • 1
    2. 移除字符设备
      cdev_del(&chr_dev);
      
      • 1

    4.1.5 文件操作方式的实现

    1. 写操作
      static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
      {
      	unsigned long p = *ppos;                           //变量p记录了当前文件的读写位置
      	int ret;
      	int tmp = count ;
      	if(p > BUFF_SIZE)                                         //如果超过了数据缓冲区的大小(128字节)的话,直接返回0。
      		return 0;                                              //并且如果要读写的数据个数超过了数据缓冲区剩余的内容的话,则只读取剩余的内容。
      	if(tmp > BUFF_SIZE - p)
      		tmp = BUFF_SIZE - p;
      	ret = copy_from_user(vbuf, buf, tmp);   //使用copy_from_user从用户空间拷贝tmp个字节的数据到数据缓冲区中
      	*ppos += tmp;                                             //同时让文件的读写位置偏移同样的字节数。
      	return tmp;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    2. 读操作
      static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
      {
      	unsigned long p = *ppos;
      	int ret;
      	int tmp = count ;
      	if (p >= BUFF_SIZE)
      		return 0;
      	if (tmp > BUFF_SIZE - p)
      		tmp = BUFF_SIZE - p;
      	ret = copy_to_user(buf, vbuf+p, tmp);
      	*ppos +=tmp;
      	return tmp;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    4.1.6 主函数

    #include 
    #include 
    #include 
    #include 
    char *wbuf = "Hello World\n";
    char rbuf[128];
    int main(void)
    {
    	printf("EmbedCharDev test\n");
    	//打开文件
    	int fd = open("/dev/chrdev", O_RDWR);
    	//写入数据
    	write(fd, wbuf, strlen(wbuf));
    	//写入完毕,关闭文件
    	close(fd);
    	//打开文件
    	fd = open("/dev/chrdev", O_RDWR);
    	//读取文件内容
    	read(fd, rbuf, 128);
    	//打印读取的内容
    	printf("The content : %s", rbuf);
    	//读取完毕,关闭文件
    	close(fd);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

     

    4.2 单驱动支持多设备

    在Linux内核中,主设备号用于 标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务

    但是,次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的

    如何能够用一个驱动程序去控制各种设备呢?

    很明显,首先,我们可以根据次设备号,来区分各种设备;
    其次,就是前文提到过的file结构体的私有数据成员private_data

    我们可以通过该成员来做文章,不难想到为什么只有open函数和close函数的形参才有file结构体,
    因为驱动程序第一个执行的是操作就是open,通过open函数就可以控制我们想要驱动的底层硬件。

    一个驱动支持多个设备的具体实现方式的重点在于如何运用file的私有数据成员。

    • 第一种方法是通过将各自的数据缓冲区放到该成员中,在读写函数的时候,直接就可以对相应的数据缓冲区进行操作;
    • 第二种方法则是通过将我们的数据缓冲区和字符设备结构体封装到一起,
      由于文件结构体inode的成员i_cdev保存了对应字符设备结构体, 使用container_of宏便可以获得封装后的结构体的地址,进而得到相应的数据缓冲区。

    4.2.1 方式一 管理各种的数据缓冲区

    #define DEV_NAME "EmbedCharDev"
    #define DEV_CNT (2)
    #define BUFF_SIZE 128
    /* 定义字符设备的设备号 */
    static dev_t devno;
    /* 定义字符设备结构体chr_dev */
    static struct cdev chr_dev;
    /* 数据缓冲区 */
    static char vbuf1[BUFF_SIZE];
    static char vbuf2[BUFF_SIZE];
    
    static int chr_dev_open(struct inode *inode, struct file *filp)
    {
    	printk("\nopen\n ");
    	/* 获取该设备文件的次设备号,使用private_data指向各自的数据缓冲区 */
    	switch (MINOR(inode->i_rdev)) {
    		case 0 : {
    			filp->private_data = vbuf1;
    			break;
    		}
    		case 1 : {
    			filp->private_data = vbuf2;
    			break;
    		}
    	}
    	return 0;
    }
    
    static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
    {
    	unsigned long p = *ppos;
    	int ret;
    	char *vbuf = filp->private_data;
    	int tmp = count ;
    	if (p > BUFF_SIZE)
    		return 0;
    	if (tmp > BUFF_SIZE - p)
    		tmp = BUFF_SIZE - p;
    	ret = copy_from_user(vbuf, buf, tmp);
    	*ppos += tmp;
    	return tmp;
    }
    
    static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
    {
    	unsigned long p = *ppos;
    	int ret;
    	int tmp = count ;
    	char *vbuf = filp->private_data;
    	if (p >= BUFF_SIZE)
    		return 0;
    	if (tmp > BUFF_SIZE - p)
    		tmp = BUFF_SIZE - p;
    	ret = copy_to_user(buf, vbuf+p, tmp);
    	*ppos +=tmp;
    	return tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    4.2.2 方式二 i_cdev变量

    /*虚拟字符设备*/
    struct chr_dev {
    	struct cdev dev;
    	char vbuf[BUFF_SIZE];
    };
    /* 字符设备1 */
    static struct chr_dev vcdev1;
    /* 字符设备2 */
    static struct chr_dev vcdev2;
    
    static int __init chrdev_init(void)
    {
    	int ret;
    	printk("4 chrdev init\n");
    	ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    	if (ret < 0)
    		goto alloc_err;
    	/* 关联第一个设备:vdev1 */
    	cdev_init(&vcdev1.dev, &chr_dev_fops);
    	ret = cdev_add(&vcdev1.dev, devno+0, 1);
    	if (ret < 0) {
    		printk("fail to add vcdev1 ");
    		goto add_err1;
    	}
    	/* 关联第二个设备:vdev2 */
    	cdev_init(&vcdev2.dev, &chr_dev_fops);
    	ret = cdev_add(&vcdev2.dev, devno+1, 1);
    	if (ret < 0) {
    		printk("fail to add vcdev2 ");
    		goto add_err2;
    	}
    	return 0;
    	
    	/* 若虚拟设备2添加失败,则需要把虚拟设备1移除,再将申请的设备号注销 */
    	add_err2:
    	cdev_del(&(vcdev1.dev));
    	
    	/* 当虚拟设备1添加失败时,直接返回的时候,只需要注销申请到的设备号即可 */
    	add_err1:
    	unregister_chrdev_region(devno, DEV_CNT);
    	alloc_err:
    	return ret;
    }
    
    static void __exit chrdev_exit(void)
    {
    	printk("chrdev exit\n");
    	unregister_chrdev_region(devno, DEV_CNT);
    	cdev_del(&(vcdev1.dev));
    	cdev_del(&(vcdev2.dev));
    }
    
    static int chr_dev_open(struct inode *inode, struct file *filp)
    {
    	printk("open\n");
    	filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
    	return 0;
    }
    static int chr_dev_release(struct inode *inode, struct file *filp)
    {
    	printk("release\n");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    我们知道inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体, 我们要如何能够得到虚拟设备的数据缓冲区呢?
    为此,Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址
    该宏需要三个参数,分别是代表结构体成员的真实地址结构体的类型以及结构体成员的名字
    在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。
    假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev利用container_of宏, 我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了
    ==> 回顾一下inode与cdev结构体

    dev_t == u32
    
    struct cdev {
    	struct kobject kobj;
    	struct module *owner;
    	const struct file_operations *ops;
    	struct list_head list;
    	dev_t dev;
    	unsigned int count;
    };
    
    struct inode {
    	...
    	dev_t			i_rdev;
    	...
    	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
    	...
    	union {
    		struct pipe_inode_info	*i_pipe;
    		struct block_device	*i_bdev;
    		struct cdev		*i_cdev;
    		char			*i_link;
    		unsigned		i_dir_seq;
    	};
    	__u32			i_generation;
    	void			*i_private; /* fs or device private pointer */
    } __randomize_layout;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    最后读写操作的相关实现如下:

    static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
    {
    	unsigned long p = *ppos;
    	int ret;
    	/* 获取文件的私有数据 */
    	struct chr_dev *dev = filp->private_data;
    	char *vbuf = dev->vbuf;
    	int tmp = count ;
    	if (p > BUFF_SIZE)
    		return 0;
    	if (tmp > BUFF_SIZE - p)
    		tmp = BUFF_SIZE - p;
    	ret = copy_from_user(vbuf, buf, tmp);
    	*ppos += tmp;
    	return tmp;
    }
    
    static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
    {
    	unsigned long p = *ppos;
    	int ret;
    	int tmp = count ;
    	/* 获取文件的私有数据 */
    	struct chr_dev *dev = filp->private_data;
    	char *vbuf = dev->vbuf;
    	if (p >= BUFF_SIZE)
    		return 0;
    	if (tmp > BUFF_SIZE - p)
    		tmp = BUFF_SIZE - p;
    	ret = copy_to_user(buf, vbuf+p, tmp);
    	*ppos +=tmp;
    	return tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

     


    五、总结

    在Linux内核中,主设备号用于 标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。但是,次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。
    通过将我们的数据缓冲区和字符设备结构体封装到一起,由于文件结构体inode的成员i_cdev保存了对应字符设备结构体, 使用container_of宏便可以获得封装后的结构体的地址,进而得到相应的数据缓冲区。

    我们知道inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体, 我们要如何能够得到虚拟设备的数据缓冲区呢?
    为此,Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。
    该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。
    在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。
    假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev,利用container_of宏, 我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了。

  • 相关阅读:
    地理社交网络中基于多目标组合优化的空间感知影响力联合最大化
    【C++】从入门到精通第四弹——命名空间
    基于SpringBoot的超市管理系统
    【系统架构设计师】第四章 计算机网络
    限流与代理网关集成调研及应用
    java使用poi生成excel
    c++初阶-----STL---string的模拟实现
    计算机丢失MSVCP140.dll的解决方法分享
    华为云企业应用上云解决方案,为企业排忧解难
    JAVA上门家政服务系统源码微信小程序+微信公众号+APP+H5
  • 原文地址:https://blog.csdn.net/qq_38211182/article/details/126572801