Linux驱动开发基础(Hello驱动)

news2024/9/20 6:54:52

所学内容来自百问网

目录

1. 文件在内核中的表示

2. 打开字符设备节点时,内核中也有对应的struct file

3. 编写驱动程序步骤

4. 相关知识点

4.1 涉及函数解析

4.2 module_init/module_exit的实现

4.3 register_chrdev的内部实现

4.4 class_destroy/device_create 浅析

5. 示例代码

5.1 驱动代码

5.2 应用代码

5.3 Makefile

5.4 效果


1. 文件在内核中的表示

APP 打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于APP 的每一个文件句柄,在内核里面都有一个“struct file”与之对应。

使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):

int open(const char *pathname, int flags, mode_t mode); 

去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。

2. 打开字符设备节点时,内核中也有对应的struct file

注意这个结构体中的结构体:struct file_operations *f_op,这是由驱 动程序提供的。

结构体struct file_operations的定义如下:

3. 编写驱动程序步骤

1.确定主设备号,也可以让内核分配

2.定义自己的file_operations结构体

3.实现对应的drv_open/drv_read/drv_write 等函数,填入file_operations结构体

4.把file_operations 结构体告诉内核:register_chrdev

5.得有一个入口函数:安装驱动程序时,就会去调用这个入口函数

6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev

7.其他完善:提供设备信息,自动创建设备节点:class_create, device_create

4. 相关知识点

4.1 涉及函数解析

  • 将数据从内核空间复制到用户空间

    • unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

    • void __user *to:指向用户空间的指针,表示数据复制的目标地址

    • const void *from:指向内核空间的指针,表示数据复制的源地址

    • unsigned long n:要复制的字节数

    • 返回值:函数返回未能复制的字节数。如果返回值为0,则表示全部复制成功

  • 将数据从用户空间复制到内核空间

    • unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

    • void *to:指向内核空间的指针,表示数据复制的目标地址

    • const void __user *from:指向用户空间的指针,表示数据复制的源地址

    • unsigned long n:要复制的字节数

    • 返回值:函数返回未能复制的字节数。如果返回值为0,则表示全部复制成功

  • 从文件中读取数据到用户空间缓冲区

    • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

    • struct file *:指向file结构体的指针,代表要读取的文件

    • char __user *:指向用户空间缓冲区的指针,用于存储从文件中读取的数据

    • size_t:要读取的字节数

    • loff_t *:指向文件中的偏移量的指针,表示从文件的哪个位置开始读取

    • 返回值:返回实际读取的字节数。如果读取失败,返回负值

  • 将用户空间缓冲区的数据写入文件

    • ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    • struct file *:指向file结构体的指针,代表要写入的文件

    • const char __user *:指向用户空间缓冲区的指针,包含要写入文件的数据

    • size_t:要写入的字节数

    • loff_t *:指向文件中的偏移量的指针,表示从文件的哪个位置开始写入

    • 返回值:返回实际写入的字节数。如果写入失败,返回负值

  • 打开文件,并初始化file结构体

    • int (*open) (struct inode *, struct file *);

    • struct inode *:指向inode结构体的指针,代表要打开的文件的元数据

    • struct file *:指向file结构体的指针,用于存储打开文件的相关信息

    • 返回值:如果打开成功,返回0;如果失败,返回负值

  • 释放文件资源,包括关闭文件、释放内存等

    • int (*release) (struct inode *, struct file *);

    • struct inode *:指向inode结构体的指针,代表要释放的文件的元数据

    • struct file *:指向file结构体的指针,包含要释放的文件的相关信息

    • 返回值:如果释放成功,返回0;如果失败,返回负值

4.2 module_init/module_exit的实现

一个驱动程序有入口函数、出口函数,代码如下:

module_init(hello_init); 
module_exit(hello_exit); 

驱动程序可以被编进内核里,也可以被编译为ko文件后手工加载。对于这 两种形式,“module_init/module_exit”这2个宏是不一样的。在内核文件 “include\linux\module.h”中可以看到这2个宏:

module_init(initfn) 宏用于声明模块初始化函数。当模块被加载到内核时,initfn函数会被自动调用。这个函数通常用于执行模块所需的任何初始化任务,比如注册设备、分配内存、初始化数据结构等。

module_exit(exitfn)宏用于声明模块退出函数。当模块从内核中卸载时,exitfn函数会被自动调用。这个函数通常用于执行模块卸载前的任何清理任务,比如注销设备、释放内存、清理数据结构等。

4.3 register_chrdev的内部实现

register_chrdev函数源码如下:

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 
{ 
    return __register_chrdev(major, 0, 256, name, fops); 
} 

它调用__register_chrdev函数,这个函数的代码精简如下:

01 int __register_chrdev(unsigned int major, unsigned int baseminor, 
02                       unsigned int count, const char *name, 
03                       const struct file_operations *fops) 
04 { 
05     struct char_device_struct *cd; 
06     struct cdev *cdev; 
07     int err = -ENOMEM; 
08  
09     cd = __register_chrdev_region(major, baseminor, count, name); 
10  
11     cdev = cdev_alloc(); 
12  
13     cdev->owner = fops->owner; 
14     cdev->ops = fops; 
15     kobject_set_name(&cdev->kobj, "%s", name); 
16  
17     err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); 
18 } 

这个函数主要的代码是第09行、第11~15行、第17行。

第09行,调用__register_chrdev_region函数来“注册字符设备的区域”, 它仅仅是查看设备号(major, baseminor)到(major, baseminor+count-1) 有没有被占用,如果未被占用的话,就使用这块区域。

内核中存在着一个chrdevs数组:

static struct char_device_struct { 
    struct char_device_struct *next;  
    unsigned int major; 
    unsigned int baseminor; 
    int minorct; 
    char name[64]; 
    struct cdev *cdev;      /* will die */ 
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; 

去访问它的时候,并不是直接使用主设备号major来确定数组项,而是使用如下函数来确定数组项:

/* index in the above */ 
static inline int major_to_index(unsigned major) 
{ 
    return major % CHRDEV_MAJOR_HASH_SIZE; 
} 

上述代码中,CHRDEV_MAJOR_HASH_SIZE等于255。比如主设备号1、256, 都会使用chardevs[1]。chardevs[1]是一个链表,链表里有多个 char_device_struct结构体,某个结构体表示主设备号为1的设备,某个结构 体表示主设备号为256的设备。

chardevs的结构图如图1.6所示:

由此可见:

1.chrdevs[i]数组项是一个链表头

链表里每一个元素都是一个char_device_struct结构体,每个元素表示 一个驱动程序。

char_device_struct结构体内容如下:

struct char_device_struct { 
    struct char_device_struct *next; 
    unsigned int major; 
    unsigned int baseminor; 
    int minorct; 
    char name[64]; 
    struct cdev *cdev;      /* will die */ 
} 

它指定了主设备号major、次设备号baseminor、个数minorct,在cdev 中含有file_operations结构体。

char_device_struct结构体的含义是:主次设备号为(major, baseminor)、(major, baseminor+1)、(major, baseminor+2)、(major, baseminor+ minorct-1)的这些设备,都使用同一个file_operations来操作。

2.在图1.6中,chardevs[1]中有3个驱动程序

第1个char_device_struct结构体对应主次设备号(1, 0)、(1, 1),这 是第1个驱动程序。

第2个char_device_struct结构体对应主次设备号(1, 2)、(1, 2)、……、 (1, 11),这是第2个驱动程序。

第3个char_device_struct结构体对应主次设备号(256, 0),这是第3 个驱动程序。

第11~15行分配一个cdev结构体,并设置它:它含有file_operations 结构体。

第17行调用cdev_add把cdev结构体注册进内核里,cdev_add函数代码 如下:

01 int cdev_add(struct cdev *p, dev_t dev, unsigned count) 
02 { 
03     int error; 
04  
05     p->dev = dev; 
06     p->count = count; 
07  
08     error = kobj_map(cdev_map, dev, count, NULL, 
09              exact_match, exact_lock, p); 
10     if (error)  
11         return error; 
12  
13     kobject_get(p->kobj.parent); 
14  
15     return 0; 
16 } 

这个函数涉及kobj的操作,这是一个通用的链表操作函数。它的作用是: 把cdev结构体放入cdev_map链表中,对应的索引值是“dev”到“dev+count 1”。以后可以从cdev_map链表中快速地使用索引值取出对应的cdev。

比如执行以下代码:

err = cdev_add(cdev, MKDEV(1, 2), 10);

其中的MKDEV(1,2)构造出一个整数“1<<8 | 2”,即0x102;上述代码将 cdev放入cdev_map链表中,对应的索引值是0x102到0x10c(即0x102+10)。 以后根据这10个数值(0x102、0x103、0x104、……、0x10c)中任意一个,都 可以快速地从cdev_map链表中取出cdev结构体。

APP打开某个字符设备节点时,进入内核。在内核里根据字符设备节点的主、 次设备号,计算出一个数值(major<<8 | minor,即inode->i_rdev),然后使 用这个数值从cdev_map中快速得到cdev,再从cdev中得到file_operations 结构体。

关键函数如下:

在打开文件的过程中,可以看到并未涉及chrdevs,都是使用cdev_map。 所以可以看到在chrdevs的定义中看到如下注释:

4.4 class_destroy/device_create 浅析

驱动程序的核心是 file_operations 结构体:分配、设置、注册它。 “class_destroy/device_create”函数知识起一些辅助作用:在/sys目录下 创建一些目录、文件,这样Linux系统中的APP(比如udev、mdev)就可以根据 这些目录或文件来创建设备节点。

以下代码将会在“/sys/class”目录下创建一个子目录“hello_class”

hello_class = class_create(THIS_MODULE, "hello_class"); 

以下代码将会在“/sys/class/hello_class”目录下创建一个文件 “hello”:

device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); 

5. 示例代码

5.1 驱动代码

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
​
//1.确定主设备号,也可以让内核分配
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a,b) (a < b ? a : b)
//3.实现对应的drv_open/drv_read/drv_write 等函数,填入file_operations结构体
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    ret = copy_to_user(buf, kernel_buf, MIN(1024,size));
    return MIN(1024,size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    ret = copy_from_user(kernel_buf, buf, MIN(1024,size));
    return MIN(1024,size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    return 0;
}
​
//2.定义自己的file_operations结构体
static struct file_operations hello_drv = {
    .owner = THIS_MODULE,
    .open = hello_drv_open,
    .read = hello_drv_read,
    .write = hello_drv_write,
    .release = hello_drv_close,
};
​
//4.把file_operations 结构体告诉内核:register_chrdev
//5.得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
    int err;
    major = register_chrdev(0,"hello",&hello_drv);
​
    hello_class = class_create(THIS_MODULE, "hello_class");
    err = PTR_ERR(hello_class);
    if(IS_ERR(hello_class))
    {
        unregister_chrdev(major,"hello");
        return -1;
    }
​
    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
    return 0;
}
//6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
static void __exit hello_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    device_destroy(hello_class, MKDEV(major, 0));
    class_destroy(hello_class);
    unregister_chrdev(major, "hello");
}
​
//7.其他完善:提供设备信息,自动创建设备节点:class_create,  device_create
module_init(hello_init);
module_exit(hello_exit);
// 声明许可证类型
MODULE_LICENSE("GPL");

5.2 应用代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
​
/*
    ./hello_drv_test -w abc
    ./hello_drv_test -r
*/
int main(int argc,char **argv)
{
    int fd;
    char buf[1024];
    int len;
​
    // 判断参数
    if(argc < 2)
    {
        printf("Usage: %s -w <string>\n",argv[0]);
        printf("       %s -r\n",argv[0]);
        return -1;
    }
​
    // 打开文件
    fd = open("/dev/hello",O_RDWR);
    if(fd == -1)
    {
        printf("can not open file /dev/hello\n");
        return -1;
    }
​
    // 写文件或读文件
    if((strcmp(argv[1],"-w") == 0) && (argc == 3))
    {
        len = strlen(argv[2]) + 1;
        len = len < 1024 ? len : 1024;
        write(fd,argv[2],len);
    }else
    {
        len = read(fd,buf,1024);
        buf[1023] = '\0';
        printf("APP read : %s\n",buf);
    }
​
    close(fd);
    return 0;
}

5.3 Makefile

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册
​
// 此处使用的是IMX6U_6ULL开发板
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
​
all:
    make -C $(KERN_DIR) M=`pwd` modules 
    $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 
​
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order
    rm -f hello_drv_test
​
obj-m   += hello_drv.o

5.4 效果

linux:

开发板:

装载驱动

查看驱动是否转载成功

执行程序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2038505.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(Jmeter、Fiddler)脚本转换Loadrunner脚本

背景:公司政治任务、各种体系文档要留档,但有些不在体系内的工具生成的脚本需要转化到体系内以备留档。 一、Loadrunner代理设置 开始录制配置: Record->Remote Application via LoadRunner Proxy LoadRrunner Proxy listens on port-> 8889 (系统建立出入站规则…

解析防蠕动交叉导轨的防蠕动机制

随着工业自动化的不断发展&#xff0c;对机械导轨系统的精度和稳定性要求越来越高。防蠕动交叉导轨作为一种新型导轨系统&#xff0c;能够有效提高设备的运行精度和稳定性&#xff0c;降低维护成本。 蠕动现象通常发生在导轨负载超出其额定范围、表面粗糙度不足或润滑不良等情况…

Python 实现 Excel 文件操作的技术性详解

目录 一、引言 二、Excel 文件格式及库的选择 2.1 Excel 文件格式 2.2 库的选择 三、安装必要的库 四、使用 openpyxl 读取 Excel 文件 4.1 基本步骤 4.2 实战案例 五、使用 pandas 读取 Excel 文件 5.1 基本步骤 5.2 实战案例 六、写入 Excel 文件 6.1 使用 …

【每日刷题】Day100

【每日刷题】Day100 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 【模板】堆_牛客题霸_牛客网 (nowcoder.com) 2. 【模板】链表_牛客题霸_牛客网 (nowcoder.com) 3…

Linux系统移植——开发板烧写

目录&#xff1a; 目录&#xff1a; 一、什么是EMMC分区&#xff1f; 1.1 eMMC分区 1.2 分区的管理 二、相关命令介绍&#xff1a; 2.1 mmc 2.1.1 主要功能 2.1.2 示例用法 2.2 fdisk 2.2.1 基本功能 2.2.2 交互模式常用命令 2.2.3 注意事项 三、U-BOOT烧写 3.1 mmc命令 3.2 f…

24小时监控识别潜在的非安全生产隐患的明厨亮灶开源了。

明厨亮灶视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。AI技术可以24小时…

【JS】旋涡数组

前言 实现如下图的旋涡数组&#xff0c;简单理解为遇到拐点自动拐弯&#xff0c;否则一直沿当前方向推进。 封装一个函数接收两个参数分别为行数以及列数&#xff0c;实现该旋涡数组。 思路 二维数组&#xff0c;初始填充0分别记录水平和垂直方向的坐标&#xff0c;并根据步…

学习记录702@计算机组成原理之计算机硬件组成细化

运算器 ACC是累加器ALU是逻辑和算数运算单元MQ是乘商寄存器 X是操作数寄存器各部分存储的值 控制器 PC是程序计数器&#xff0c;用来存放下一条应该执行程序的地址&#xff0c;与MAR地址寄存器相连&#xff0c;找到下一个要执行的程序的地址。IR指令寄存器&#xff0c;用来…

怎么利用XML发送视频彩信

传统的短信推广主要以文字为主&#xff0c;用户接收到的信息往往显得单调乏味。而视频彩信则不同&#xff0c;它结合了视频和音频的优势&#xff0c;通过生动的画面和悦耳的音乐&#xff0c;给用户带来强烈的视听冲击&#xff0c;从而极大地提高了用户的吸引力。 XML成功返回示…

某通电子文档安全管理系统 CDGAuthoriseTempletService1接口SQL注入漏洞复现 [附POC]

文章目录 某通电子文档安全管理系统 CDGAuthoriseTempletService1接口SQL注入漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现0x06 修复建议某通电子文档安全管理系统 CDGAuthoriseTempletService1接口SQL注入漏…

聚鼎科技:现在开一家装饰画店铺究竟受欢迎吗

在这个时代&#xff0c;装饰画不仅仅是墙上的一抹色彩&#xff0c;它已经成为人们情感寄托、审美追求的象征。那么&#xff0c;现在开一家装饰画店铺究竟受欢迎吗? “家&#xff0c;是一幅画。”这句话道出了现代人对于家的期待与向往。在这个快节奏的时代&#xff0c;人们渴望…

8.13 哈希表中等 128 Longest Consecutive Sequence 138 Copy List with Random Pointer

128 Longest Consecutive Sequence class Solution { public:int longestConsecutive(vector<int>& nums) {//无序array整数数组&#xff0c;返回最长的连续的序列长度&#xff0c;首先这些数不按顺序//时间复杂度O(n)//使用哈希表&#xff0c;先存后遍历---->no …

web页面的性能测试

背景 测试大模型主要web页面的性能 使用工具 通过google自带的lighthouse测试页面的性能 各个参考指标 First Contentful Paint(FCP):测量在用户导航到页面后浏览器呈现第一段 DOM 内容所花费的时间。页面上的图像、非白色<canvas>元素和 SVG 被视为 DOM 内容&#…

C++STL之string类:基本使用及模拟实现

目录 一&#xff0c;前言 为什么要学习string类 C语言中的字符串 C中的字符串 STL(Standard Template Library) 里面的 string 类 二&#xff0c;string类的基本使用 文档的阅读 常见接口的基本使用 1&#xff0c;构造函数(constructor) 2&#xff0c;拷贝构造(copy …

程序员喜欢的7个免费公共API

本文将介绍七个程序员们喜爱的免费公共API&#xff0c;它们覆盖了从天气信息、翻译到数据分析、游戏等多个领域。这些API不仅易于使用&#xff0c;而且功能全面&#xff0c;能够帮助开发者快速实现项目需求&#xff0c;无论是个人学习、小项目开发还是商业应用&#xff0c;都能…

诸葛io孔淼:聚焦区域性银行,新一代自主可控的埋点分析平台

近日&#xff0c;由金科创新社主办的2024金融科技创新发展论坛顺利召开&#xff0c;诸葛智能创始人孔淼受邀出席并发表演讲&#xff0c;他表示&#xff0c;区域性银行需要构建新一代的数智化营销体系&#xff0c;驱动营销效率与经营效能增长。 孔淼指出&#xff0c;中小银行应…

Flink UDF注意幂等性,防止重复调用时出问题

编写Flink UDF 要注意幂等性&#xff0c;尤其不能直接修改入参&#xff01;理论上是一条数据只会执行一次UDF&#xff0c;但是实际执行过程中可能会对一条数据执行多次UDF&#xff0c;引起意想不到的问题。 例如有如下SQL&#xff1a; CREATE VIEW tmp_view AS SELECT a.…

Processing圆圈随鼠标运动

一.案例代码如下&#xff1a; import processing.pdf.*; import java.util.Calendar; boolean savePDF false; float tileCount 20; color circleColor color(0); int circleAlpha 180; int actRandomSeed 0; void setup(){ size(600,600); } void draw(){ if(savePD…

Python(TensorFlow)衍射光学层卷积算法模拟(英伟达GPU)

&#x1f3af;要点 &#x1f3af;衍射光学卷积算法模拟 | &#x1f3af;模拟或数字电子计算之前加入一层光学计算 | &#x1f3af;前馈卷积神经网络计算成像系统对输入图像进行分类 | &#x1f3af;相位掩模利用线性空间不变成像系统执行固有卷积 &#x1f4dc;用例 Python非…

【Unity/网络】Unity和内网穿透的网络测试 —— 以聊天室为例

这两天在做那个CodeMonky的胡闹厨房的案例&#xff0c;一直困扰我的是关于Lobby和Relay的相关网络服务&#xff0c;需要挂加速器并且延迟不低&#xff0c;所以我一直在寻找一些其他替代方案&#xff0c;想起来之前做一个UEC的网络枪战时做过一个内网穿透的方法&#xff0c;所以…