Linux开发-->驱动开发-->字符设备驱动框架(2)

文章目录

1、字符设备实现的两套接口

Linux 字符设备驱动有两套标准实现接口,对应内核不同阶段的设计:

1、传统旧接口:基于 register_chrdev + 手动创建设备文件(静态分配设备号)

2、新内核标准接口:基于 cdev 结构体 + 动态设备号 + udev 自动创建设备文件。

现在的驱动开发多使用新接口。

2、现代Linux内核实现字符设备步骤

1、初始化file_operations 结构体

2、申请设备号

3、初始化c_dev结构体

4、注册c_dev结构体

5、创建设备类

6、创建设备

7、释放设备

8、释放设备类

9、销毁内核c_dev

10、释放设备号

3、虚拟设备文件实验

创建虚拟字符设备,read时返回数字字符,每次递增。write 设置值 。open 初始化为0。

1、代码实现

c 复制代码
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h> 
#include <linux/string.h>
#include <linux/module.h>
#include <linux/uaccess.h>
//定义缓存
uint8_t kernal_buff[128] = {0};
uint8_t write_buff[128] = {0};
uint8_t read_buff[128] = {0};

//定义操作函数
static int vc_open (struct inode * node , struct file * fp)
{
    printk(KERN_INFO"call vc_open\r\n");
    kernal_buff[0] = 1;
    return 0;
}

static int vc_close (struct inode * node, struct file * fp)
{
    printk(KERN_INFO"call vc_close\r\n");
    return 0;
}

static ssize_t vc_read (struct file * fp, char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    printk(KERN_INFO"vc_read input parments * fp = 0x%x, user buff * = 0x%x ,size = %d,loff_t = %d\n ",
    fp,buff,cnt,*offset);
    strncpy(read_buff,kernal_buff,sizeof(kernal_buff));
    retvalue = copy_to_user(buff,read_buff,strlen(read_buff)<cnt?sizeof(read_buff):cnt);
    if(retvalue == 0)
    {
        printk(KERN_INFO"kernal vc senddata OK... \r\n");
        *offset += strlen(read_buff)<cnt?sizeof(read_buff):cnt;
        if(*offset > 128)
        {
            return 0;
        }
        if(kernal_buff[0] == 255)
        {
            kernal_buff[0] == 0;
        } 
        else{
            kernal_buff[0] += 1;
        }
        return strlen(read_buff)<cnt?strlen(read_buff):cnt;
        // return 0;
    }
    else
    {
        printk(KERN_ERR"kernal vc senddata FAILED... \r\n");
        return -EFAULT;
    }

}

static ssize_t vc_write (struct file * fp, const char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    int loop;
    printk(KERN_INFO"======>>>> write parament cnt:%zu\r\n",cnt);
    retvalue = copy_from_user(write_buff,buff,((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff))));
    if(retvalue == 0)
    {
        printk(KERN_INFO"vc_write %d byte\n",retvalue);
         *offset += strlen(read_buff)<cnt?sizeof(read_buff):cnt;
        if(*offset > 128)
        {
            return 0;
        }
        for(loop = 0;loop < retvalue;loop++)
        {
            printk(KERN_INFO"0x%02x ",write_buff[loop]);
        }
        printk(KERN_INFO"\n");
        kernal_buff[0] = write_buff[0];
        return ((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff)));
    }
    else
    {
        printk(KERN_ERR"vc_write err\n");
        return -EFAULT;
    }
  
}


//定义操作函数结构体
static struct file_operations vc_operations =
{
    .open = vc_open,
    .release = vc_close,
    .read = vc_read,
    .write = vc_write,
    .owner = THIS_MODULE
};
//定义字符设备结构体
static struct cdev my_cdev = {0};
//定义设备类指针
static struct class * my_class = NULL;
//定义设备号
static dev_t my_dev_t = 0;
//定义设备名称
static char const * const  vc_name = "vc_char";

//init 函数
static int __init vc_init(void)
{
    int res = 0;
    //申请设备号
    res = alloc_chrdev_region(&my_dev_t,0,1,vc_name);
    if(res != 0)
    {
        printk(KERN_ERR"vc_init alloc_chrdev_region err\n");
        return -EFAULT;
    }
    else
    {
        printk(KERN_INFO"vc_init alloc_chrdev_region dev_t = 0x%x\n",my_dev_t);
    }
    //初始化字符设备结构体
    cdev_init(&my_cdev,&vc_operations);
    //向内核注册字符设备
    my_cdev.owner = THIS_MODULE;
    res = cdev_add(&my_cdev,my_dev_t,1);
     if(res != 0)
    {
        printk(KERN_ERR"vc_init cdev_add err\n");
        //释放设备号
        unregister_chrdev_region(my_dev_t,1);
        return -EFAULT;
    }
    else
    {
        printk(KERN_INFO"vc_init cdev_add success\n");
    }

    //创建设备类
    my_class =  class_create(THIS_MODULE,vc_name);
    if (IS_ERR(my_class)) {
        int err = PTR_ERR(my_class); 
        printk(KERN_ERR "vc_init class_create failed! Error: %d\n", err);
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_t,1);
        return err; 
    }
    printk(KERN_INFO "vc_init class_create successfuly! \n");
    //创建设备文件
    struct device * vc_ddev = device_create(my_class,NULL,my_dev_t,NULL,vc_name);
     if (IS_ERR(vc_ddev)) {
        int err = PTR_ERR(vc_ddev); 
        printk(KERN_ERR "vc_init device_create failed! Error: %d\n", err);
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_t,1);
        return err; 
    }
    printk(KERN_INFO "vc_init device_create successfuly! \n");
    return 0;
}


//exit 函数
static void __exit vc_exit(void)
{
    //释放设备文件
    device_destroy(my_class,my_dev_t);
    //释放设备类
    class_destroy(my_class);
    //释放字符设备
    cdev_del(&my_cdev);
    //释放设备号
    unregister_chrdev_region(my_dev_t,1);
    printk(KERN_INFO "vc_exit successfuly! \n");
}

//注册
module_init(vc_init);
module_exit(vc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoMing 10086@xx.com");

2、编译

1、Mmakfile

c 复制代码
KERNELDIR := /home/wangju/linux/linux
CURRENT_PATH := $(shell pwd) 
obj-m := my_dev.o 
  
build: kernel_modules 
   
kernel_modules:   
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules 
clean:   
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean 

在此makefile中,默认目标为build,依赖kernel_modules 。kernel_modules 目标的执行是跳转到Linux内核源码目录 (-C),然后为内核顶层Makefile指定外部模块路径,modules 参数告诉内核顶层Makefile是编译模块。

2、编译

c 复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

指定了架构和交叉编译器前缀

3、编译结果

3、应用程序

1、代码

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>



static char buff[128] = {0};

#define dev_path "/dev/vc_char"
int main()
{
    int res = 0;
    int fp = open(dev_path,O_RDWR);
    if(fp <= 0)
    {
        printf("open error\n");
        return 0;
    }
    char rc ;
    while(1)
    {
        rc = fgetc(stdin) ;
        if(rc == '1')
        {
            res = read(fp,buff,1);
            if(res <= 0)
            {
                printf("read failed\n");
            }
            else
            {
                printf("read valus = %d\n",buff[0]);
            }
        }
        else if(rc == '2')
        {
            char wr = 10;
            res = write(fp,&wr,1);
            if(res <= 0)
            {
                printf("write failed\n");
            }
            else
            {
                printf("write valus = %d\n",wr);
            }
        }
          else if(rc == '3')
        {
            break;
        }
        else
        {
            printf("involed\n");
        }

    }
    close(fp);
    return 0;
}

2、编译

bash 复制代码
arm-linux-gnueabihf-gcc vc_app.c  -o vc_app

3、代码效果

应用层代码打开模块的设备文件vc_char,从键盘接收输入。输入1,读数据,内核每次++。输入2,写数据,设置内核数据为10.

4、效果检验

1、将应用程序和模块程序发送至板卡

2、加载模块

加载模块有两个命令 insmod 和 modprobe

  1. insmod(简单粗暴)
    作用:手动加载指定路径的驱动模块用法:必须给 完整路径 / 相对路径
    不能解决依赖问题
    卸载使用 insmod → rmmod
  2. modprobe(智能高级)
    作用:从 系统目录 /lib/modules/xxx/ 加载模块用法:直接写 模块名,不用路径、不用 .ko 后缀
    可以自动加载依赖模块
    卸载使用 modprobe → modprobe -r
  3. lsmod
    作用:查看当前系统已经加载了哪些内核模块
  4. dmesg
    查看内核打印信息(驱动里的 printk 全在这)

3、启动应用程序

可以看到,对设备文件的读写都是按照设计的思路进行的。

4、查看内核输出

4、设备驱动在内核模块的缓存如何实现?

  1. kmem_cache 对内核对象的频繁申请释放
    创建缓存:kmem_cache_create()
    分配对象:kmem_cache_alloc()
    释放对象:kmem_cache_free()
    销毁缓存:kmem_cache_destroy()
  2. kmalloc
    和用户空间的malloc一样的效果
  3. kzalloc
    和kmalloc一样,不过会清零
  4. vmalloc
    申请内存不连续的虚拟空间。
  5. 全局静态数组
    如果模块是直接编译进内核,则占用内核镜像的 .data/.bss段
    如果是insmod动态加载的,则通过SLAB动态分配。

内核模块的空间,如果动态申请,一遍都是通过SLAB分配静态缓存池的对象。
如果是静态声明的,模块编译进内核就是内核镜像的静态数据段。动态加载就是通过vmalloc申请的动态空间,最后还是通过SLAB分配的。

5、设备文件的阻塞、非阻塞、定时读写如何实现

简单概述:在模块的read和write时判断是否是非阻塞读写(标志不一样),做不同处理。

非阻塞:直接返回读写状态

阻塞:将调用read和write的进程挂入阻塞队列(内部私有队列),条件满足时唤醒进程。

是否阻塞的标志保存在 struct file中,这是每个进程独立的,在调用read、write时会作为参数传入模块的read和write。

6、驱动真实设备要做的工作

1、相关概念

  1. mmu的物理地址和虚拟地址的转换
  2. I/O内存及其读写

2、LED实例代码

c 复制代码
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h> 
#include <linux/string.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h> 
#include <linux/errno.h>
//定义缓存
uint8_t kernal_buff[128] = {0};
uint8_t write_buff[128] = {0};
uint8_t read_buff[128] = {0};


#define LEDOFF   0           /* 关灯  */ 
#define LEDON    1          /* 开灯  */ 
 /* 寄存器物理地址 */ 
#define CCM_CCGR1_BASE               (0X020C406C)     
#define SW_MUX_GPIO1_IO03_BASE      (0X020E0068) 
#define SW_PAD_GPIO1_IO03_BASE      (0X020E02F4) 
#define GPIO1_DR_BASE                (0X0209C000) 
#define GPIO1_GDIR_BASE              (0X0209C004)

/* 映射后的寄存器虚拟地址指针 */ 
static void __iomem *IMX6U_CCM_CCGR1; 
static void __iomem *SW_MUX_GPIO1_IO03; 
static void __iomem *SW_PAD_GPIO1_IO03; 
static void __iomem *GPIO1_DR; 
static void __iomem *GPIO1_GDIR; 




void led_switch(u8 sta) 
{ 
    u32 val = 0; 
    if(sta == LEDON) { 
        val = readl(GPIO1_DR); 
        val &= ~(1 << 3);    
        writel(val, GPIO1_DR); 
    }else if(sta == LEDOFF) { 
        val = readl(GPIO1_DR); 
        val|= (1 << 3);  
        writel(val, GPIO1_DR); 
    }    
}

//定义操作函数
static int vc_open (struct inode * node , struct file * fp)
{
    printk(KERN_INFO"call led_open\r\n");
    // kernal_buff[0] = 1;
    return 0;
}

static int vc_close (struct inode * node, struct file * fp)
{
    printk(KERN_INFO"call led_close\r\n");
    return 0;
}

static ssize_t vc_read (struct file * fp, char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    u32 val = 0;
    printk(KERN_INFO"led_read input parments * fp = 0x%x, user buff * = 0x%x ,size = %d,loff_t = %d\n ",
    fp,buff,cnt,*offset);
    //读取数据
    val = readl(GPIO1_DR); 
    val &= (1 << 3); 
    val = val >> 3;
    if(val == 0)
    {
        //led open
        read_buff[0] = 1;
    }
    else if(val == 1)
    {
        //led 关闭
        read_buff[0] = 0;
    }
    else
    {
        //led 未知状态
        read_buff[0] = 2;
    }
    printk(KERN_INFO"led read value = %d \n",val);
    strncpy(read_buff,kernal_buff,sizeof(kernal_buff));
    retvalue = copy_to_user(buff,read_buff,strlen(read_buff)<cnt?sizeof(read_buff):cnt);
    if(retvalue == 0)
    {
        printk(KERN_INFO"kernal led senddata OK... \r\n");
        return 1;
    }
    else
    {
        printk(KERN_ERR"kernal vc senddata FAILED... \r\n");
        return -EFAULT;
    }

}

static ssize_t vc_write (struct file * fp, const char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    int loop;
    printk(KERN_INFO"======>>>> write parament cnt:%zu\r\n",cnt);
    retvalue = copy_from_user(write_buff,buff,((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff))));
    if(retvalue == 0)
    {
        if(write_buff[0] == 1)
        {
            led_switch(LEDON);
        }
        else
        {
            led_switch(LEDOFF);
        }
        return 1;
    }
    else
    {
        printk(KERN_ERR"vc_write err\n");
        return -EFAULT;
    }
  
}


//定义操作函数结构体
static struct file_operations vc_operations =
{
    .open = vc_open,
    .release = vc_close,
    .read = vc_read,
    .write = vc_write,
    .owner = THIS_MODULE
};
//定义字符设备结构体
static struct cdev my_cdev = {0};
//定义设备类指针
static struct class * my_class = NULL;
//定义设备号
static dev_t my_dev_t = 0;
//定义设备名称
static char const * const  vc_name = "led";

//init 函数
static int __init vc_init(void)
{
    int res = 0;
    int retvalue = 0; 
    u32 val = 0; 
    //初始化LED

    //寄存器映射
    /* 初始化LED */ 
    /* 1、寄存器地址映射 */ 
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); 
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); 
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); 
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); 
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
    /* 2、使能GPIO1时钟 */ 
    val = readl(IMX6U_CCM_CCGR1); 
    val &= ~(3 << 26);  /* 清除以前的设置 */ 
    val |= (3 << 26);   /* 设置新值 */ 
    writel(val, IMX6U_CCM_CCGR1); 
    /* 3、IO复用*/
    writel(5, SW_MUX_GPIO1_IO03); 
    writel(0x10B0, SW_PAD_GPIO1_IO03); 
    /* 4、 输出模式*/
    val = readl(GPIO1_GDIR); 
    val &= ~(1 << 3);   /* 清除以前的设置 */ 
    val |= (1 << 3);    /* 设置为输出 */
    writel(val, GPIO1_GDIR);
    /* 5、 关闭LED*/
    val = readl(GPIO1_DR); 
    val |= (1 << 3);     
    writel(val, GPIO1_DR); 

    //申请设备号
    res = alloc_chrdev_region(&my_dev_t,0,1,vc_name);
    if(res != 0)
    {
        printk(KERN_ERR"led_init alloc_chrdev_region err\n");
        return -EFAULT;
    }
    else
    {
        printk(KERN_INFO"led_init alloc_chrdev_region dev_t = 0x%x\n",my_dev_t);
    }
    //初始化字符设备结构体
    cdev_init(&my_cdev,&vc_operations);
    //向内核注册字符设备
    my_cdev.owner = THIS_MODULE;
    res = cdev_add(&my_cdev,my_dev_t,1);
     if(res != 0)
    {
        printk(KERN_ERR"led_init cdev_add err\n");
        //释放设备号
        unregister_chrdev_region(my_dev_t,1);
        return -EFAULT;
    }
    else
    {
        printk(KERN_INFO"led_init cdev_add success\n");
    }

    //创建设备类
    my_class =  class_create(THIS_MODULE,vc_name);
    if (IS_ERR(my_class)) {
        int err = PTR_ERR(my_class); 
        printk(KERN_ERR "led_init class_create failed! Error: %d\n", err);
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_t,1);
        return err; 
    }
    printk(KERN_INFO "led_init class_create successfuly! \n");
    //创建设备文件
    struct device * vc_ddev = device_create(my_class,NULL,my_dev_t,NULL,vc_name);
     if (IS_ERR(vc_ddev)) {
        int err = PTR_ERR(vc_ddev); 
        printk(KERN_ERR "led_init device_create failed! Error: %d\n", err);
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_t,1);
        return err; 
    }
    printk(KERN_INFO "led_init device_create successfuly! \n");
    return 0;
}


//exit 函数
static void __exit vc_exit(void)
{
    //释放内存映射
    iounmap(IMX6U_CCM_CCGR1); 
    iounmap(SW_MUX_GPIO1_IO03);  
    iounmap(SW_PAD_GPIO1_IO03); 
    iounmap(GPIO1_DR); 
    iounmap(GPIO1_GDIR); 
    //释放设备文件
    device_destroy(my_class,my_dev_t);
    //释放设备类
    class_destroy(my_class);
    //释放字符设备
    cdev_del(&my_cdev);
    //释放设备号
    unregister_chrdev_region(my_dev_t,1);
    printk(KERN_INFO "led_exit successfuly! \n");
}

//注册
module_init(vc_init);
module_exit(vc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoMing 10086@xx.com");

问题汇总

1、read 和 write的偏移值参数问题

open后,文件描述符指向一个struct file,保存该进程中对对用文件操作的标志,是否阻塞、偏移量等。

普通文件:内核会自动帮你维护偏移量,驱动不用管

字符设备:偏移量内核也会自动保存,但不会自动更新 **,必须由你驱动自己去更新 **

对于cat命令,他只有驱动read返回0时才会停止(认为读完了),如果字符设备的read没有做处理就会疯狂的read(认为没有读取完)。cat等 系统命令是针对普通文件的命令。

所以,为了让字符设备可以使用cat等命令应该在read和write时对偏移量和返回值做处理。

2、用户调用read/write等API的过程

1、app调用read

2、系统read API(glibc实现)将系统调用号、read参数放入cpu通用寄存器

3、read触发中断

4、cpu进入内核态

5、进入中断处理函数sys_read,从寄存器获取调用号

6、找到对应的执行函数,获取参数,调用对应函数

7、中断处理结束

8、CPU返回用户态,read API返回。

9、用户程序继续执行。

这就是内核代替用户执行的过程,我认为这里的内核是指 CPU内核态 + 内核中注册的对应函数两部分组成。在整个过程中CPU执行的都是用应用进程,没有发生进程的切换。所以内核只是一段可执行代码,哪个CPU加载内核代码,都可以执行。内核存在于内存,不在CPU。

应为参数要存到寄存器中,32或64。所以API参数位指针或者int等都可以,但是传入一个结构体,明显是不行的,只能传入指针,直接访问内存。

3、字符设备定时获取数据

使用内核定时器或者内核线程。但是他们的创建和销毁也是在模块的init函数中完成的。所以他们还是以模块的形式加载。

相关推荐
不一样的故事1261 天前
禁止访问 是 SVN 标准 403 权限拒绝错误
运维·安全·自动化
我想成为你噶叻叻猪1 天前
imx6ull板子ping不通ubuntu
linux·运维·ubuntu
桌面运维家1 天前
校园机房vDisk IDV云桌面Linux更新部署方案
linux·运维·服务器
2601_949194261 天前
Redis的安装教程(Windows+Linux)【超详细】
linux·数据库·redis
傻啦嘿哟1 天前
Python 文件批量处理:重命名/备份/同步运维实战指南
linux·数据库·github
cyber_两只龙宝1 天前
【Oracle】 Oracle之SQL的子查询
linux·运维·数据库·sql·云原生·oracle
遇印记1 天前
网络运维DDos攻击
运维·网络·ddos
司南-70491 天前
opencode环境搭 并 配置自定义BASE URL
linux·运维·服务器·人工智能
无巧不成书02181 天前
Rust开发环境完全指南:Windows/Linux双平台配置与实战
linux·windows·rust·gnu·msvc·mingw-w64安装·镜像配置
Little At Air1 天前
C++stack模拟实现
linux·开发语言·c++·算法