Linux字符设备驱动

news2024/12/23 12:26:29

前言

代码结构简单,旨在用最简单的原理理解最主要的框架逻辑,细节需要自行延伸。 -----------------学习的基础底层逻辑

基础步骤

开发linux内核驱动需要以下4个步骤:

  • 编写驱动代码
  • 编写makefile
  • 编译和加载驱动
  • 编写应用程序测试驱动

由于硬件设备各式各样,有了设备驱动程序,应用程序就可以不用在意设备的具体细节,而方便地与外部设备进行通信。从外部设备读取数据,或是将数据写入外部设备,即对设备进行控制。

设备驱动程序框架

  1. module_init 是 linux kernel 绝大多数模块的起始点。
  2. 我们所熟悉的应用程序都是从一个 main() 函数开始运行的,而与应用程序不同,内核模块的起始就是 module_init() 标记的函数 。
  3. module_init 是一个宏,它的参数就是模块自行定义的“起始函数”。这个函数使用 module_init 标记后,就会在内核初始化阶段,“自动”运行。
  4. 无论模块是编译进内核镜像,还是以ko的形式加载,都是从这里开始运行。
  5. 有开始就有结束,与 module_init 对应的就是 module_exit 。module_exit 负责进行一些和init反向的活动。

设备的种类繁多,所以设备的驱动程序也是各式各样的,SVR4是UNIX操作系统的一种内核标准。
规范有以下部分:

  • 驱动程序与内核的接口:是通过数据结构file_opration完成的
  • 驱动程序与设备的接口:描述了驱动程序如何与设备交互,这与具体的设备是密切相关的。
  • 驱动程序与系统引导的接口:驱动程序对设备进行初始化

接下来使用mknod命令创建设备文件结点,然后用chmod命令修改权限为777。此时设备就可以使用了。

/proc/devices与/dev的不同之处

  • 在/proc/devices下,显示的是驱动程序生成的设备及其主设备号。其中主设备号可用来让mknod作为参数。
  • 在/dev下的设备是mknod生成的设备,其中,用户通过使用/dev下的设备名来使用设备。

上层应用如何调用底层驱动

  1. 应用层的程序open(“/dev/xxx”,mode,flags)打开设备文件,进入内核中,即虚拟文件系统中。
  2. VFS层的设备文件有对应的struct inode,其中包含该设备对应的设备号,设备类型,返回的设备的结构体。
  3. 在驱动层中,根据设备类型和设备号就可以找到对应的设备驱动的结构体,用i_cdev保存。该结构体中有很重要的一个操作函数接口file_operations。
  4. 在打开设备文件时,会分配一个struct file,将操作函数接口的地址保存在该结构体中。
  5. VFS层 向应用层返回一个fd,fd是和struct file相对应,这样,应用层可以通过fd调用操作函数,即通过驱动层调用硬件设备了。

代码

起始函数

module_init(hello_init);

下面的函数的主要工作是:

  • 注册驱动
  • 申请资源
  • 节点创建
int hello_init(void)//三件事情
{
    
    devNum = MKDEV(reg_major, reg_minor);
    if(OK == register_chrdev_region(devNum, subDevNum, "helloworld"))//驱动注册
    {
        printk(KERN_EMERG"register_chrdev_region ok \n"); 
    }else {
    printk(KERN_EMERG"register_chrdev_region error n");
        return ERROR;
    }
    printk(KERN_EMERG" hello driver init \n");
    gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);//资源申请
    gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);//资源申请
    gFile->open = hello_open;
    gFile->read = hello_read;
    gFile->write = hello_write;
    gFile->owner = THIS_MODULE;
    cdev_init(gDev, gFile);
    cdev_add(gDev, devNum, 3);//把它添加到系统中去
    return 0;
}

驱动注册
将驱动信息注册完毕后,如果有匹配的设备接入,本驱动的 probe 就会被调用。
原生代码里有很多进一步对 module_init 封装的宏,其实做的也就是注册驱动这件事。

申请资源&创建节点

  • 并非所有的模块都是驱动模块,又有一些纯软件功能的模块,例如class、bus管理等。

  • 这类模块在其实函数中,则不需要注册驱动,而是按照本模块的需求,申请资源,创建节点等。

  • 需要注意的是,所有在 module_init 里做的动作,都需要在 module_exit 中做反向操作,避免资源泄漏。

  • MKDEV 功能:将主设备号和次设备号转换成dev_t类型

  • register_chrdev_region(devNum, subDevNum, “helloworld”):驱动注册,第一个参数表示设备号,第二个参数表示注册的此设备数目,第三个表示设备名称。

  • kzalloc:资源申请

  • 钩子函数挂钩:gFile->open = hello_open

  • cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接

  • cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中

钩子函数的几个实现:没有实现任何功能,只是为了让框架更明显

 int hello_open(struct inode *p, struct file *f)
{
    printk(KERN_EMERG"hello_open\r\n");
    return 0;
}
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_write\r\n");
    return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_read\r\n");      
    return 0;
}

几个重要的数据结构:
内核中每个字符设备都对应一个 cdev 结构的变量:
struct cdev

struct cdev {
struct kobject kobj;          // 每个cdev 都是一个 kobject
struct module *owner;       // 指向实现驱动的模块
const struct file_operations *ops;   // 操纵这个字符设备文件的方法
struct list_head list;       // 与cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev;                  // 起始设备编号
unsigned int count;       // 设备范围号大小
};

struct file_operations

struct file_operations { 
    struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES 
    loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置 
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据 
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作 
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作 
    int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL 
    unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入 
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令 
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl 
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替 
    int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
    int (*open) (struct inode *, struct file *); //打开 
    int (*flush) (struct file *, fl_owner_t id); 
    int (*release) (struct inode *, struct file *); //关闭 
    int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据 
    int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据 
    int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化 
    int (*lock) (struct file *, int, struct file_lock *); 
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 
    int (*check_flags)(int); 
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 
    int (*setlease)(struct file *, long, struct file_lock **); 
};

完整代码

驱动代码:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>//file_operations结构体
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>

#define BUFFER_MAX    (10)
#define OK            (0)
#define ERROR         (-1)

struct cdev *gDev;//代表字符设备的数据结构
struct file_operations *gFile;//struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带.该驱动程序的核心。它给出了对文件操作函数的定义。当然,具体的实现函数是留给驱动程序编写的
dev_t  devNum;//设备文件的设备号
unsigned int subDevNum = 1;
int reg_major  =  232;    
int reg_minor =   0;
char *buffer;
int flag = 0;
int hello_open(struct inode *p, struct file *f)
{
    printk(KERN_EMERG"hello_open\r\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_write\r\n");
    return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_read\r\n");      
    return 0;
}
int hello_init(void)//三件事情注册驱动,申请资源,节点创建
{
    
    devNum = MKDEV(reg_major, reg_minor);//将主设备号和次设备号转换成dev_t类型
    if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){
        printk(KERN_EMERG"register_chrdev_region ok \n"); 
    }else {
    printk(KERN_EMERG"register_chrdev_region error n");
        return ERROR;
    }
    printk(KERN_EMERG" hello driver init \n");
    gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
    gFile->open = hello_open;
    gFile->read = hello_read;
    gFile->write = hello_write;
    gFile->owner = THIS_MODULE;
    cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接
    cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中
    return 0;
}

void __exit hello_exit(void)
{
    cdev_del(gDev);
    unregister_chrdev_region(devNum, subDevNum);
    kfree(gDev);
    kfree(gFile);
    return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

编译驱动的Makefile:

ifneq ($(KERNELRELEASE),)
obj-m := helloDev.o
else
PWD := $(shell pwd)
#KDIR:= /lib/modules/4.4.0-31-generic/build
KDIR := /lib/modules/`uname -r`/build
all:
	make -C $(KDIR) M=$(PWD)
clean:	
	rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif

使用make命令编译结果:
在这里插入图片描述
ls -l 如下:
在这里插入图片描述

加载进内核:
在这里插入图片描述
想要控制驱动就需要添加设备节点:mknod /dev/hello c 232 0
在这里插入图片描述

应用层代码:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>

#define DATA_NUM    (64)
int main(int argc, char *argv[])
{
    int fd, i;
    int r_len, w_len;
    fd_set fdset;
    char buf[DATA_NUM]="hello world";
    memset(buf,0,DATA_NUM);
    fd = open("/dev/hello", O_RDWR);
	printf("%d\r\n",fd);
    if(-1 == fd) {
      	perror("open file error\r\n");
		return -1;
    }	
	else {
		printf("open successe\r\n");
	}
    w_len = write(fd,buf, DATA_NUM);
    r_len = read(fd, buf, DATA_NUM);
    printf("%d %d\r\n", w_len, r_len);
    printf("%s\r\n",buf);
    return 0;
}

编译:

gcc test.c -o test.o

结果:
在这里插入图片描述
执行test测试驱动:
在这里插入图片描述
再次执行dmesg查看驱动输出,发现驱动里的hell_open, hello_write, hello_read被依次调用了:
在这里插入图片描述

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

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

相关文章

Android9.0 系统Framework发送通知流程分析

1.前言 在android 9.0的系统rom定制化开发中,在systemui中一个重要的内容就是系统通知的展示,在状态栏展示系统发送通知的图标,而在 系统下拉通知栏中展示接收到的系统发送过来的通知,所以说对系统framework中发送通知的流程分析很重要,接下来就来分析下系统 通知从frame…

开发攻城狮必备的Linux虚拟机搭建指南|原创

hi&#xff0c;我是阿笠&#xff01; 这篇文章主要面对的是不常搭建Linux操作系统环境的开发同学&#xff0c;文中介绍了基本操作步骤并且提供了相关云盘资源&#xff0c;都是为了节约时间&#xff01; 因为从我自身来讲&#xff0c;作为一名后端开发&#xff0c;经常需要练习一…

c#笔记-内置类型

内置类型 内置类型是一些有关键字表示的类型。关键字具有非常高的优先级&#xff0c;可以让你在没有别的配置的情况下&#xff0c; 只要用的是c#就可以使用。这也意味着这些类型是非常重要&#xff0c;或是基本的东西。 整数&#xff1a;byte, sbyte, short, ushort, int, ui…

【Python入门】搭建开发环境-安装Pycharm开发工具

前言 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于Python零基础入门系列&#xff0c;本专栏主要内容为Python基础语法、判断、循环语句、函…

【数据结构】线性表之单链表(讲解实现——带动图理解)

文章目录 单链表单链表主体结构单链表操作函数介绍单链表操作函数实现单链表的初始化&#xff1a;打印函数单链表插入函数&#xff1a;头插尾插指定结点后插入和查找函数单链表结点之前插入数据 单链表删除函数头删尾删指定结点后删除指定结点删除 销毁单链表 文件分类test.cLi…

【STM32】基础知识 第十课 CubeMx

【STM32】基础知识 第十课 CubeMx STM32 CubeMX 简介安装 JAVACubeMX 安装新建 STM32 CubeMX 工程步骤新建工程时钟模块配置GPIO 配置生成源码 main.c STM32 CubeMX 简介 CubeMX (全称 STM32CubeMX) 是 ST 公司推出的一款用于 STM32 微控制器配置的图形化工具. 它能帮助开发者…

「Bug」解决办法:Could not switchto this profil,无法使用节点的解决方法,彻底解决

♥️作者&#xff1a;白日参商 &#x1f935;‍♂️个人主页&#xff1a;白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识&#xff0c;和大家一起努力呀&#xff01;&#xff01;&#xff01; &#x1f388;&#x1f388;加油&#xff01; 加油&#xff01…

二十五、OSPF高级技术——开销值、虚链路、邻居建立、LSA、静默接口

文章目录 调试指令&#xff08;三张表&#xff09;1、邻居表&#xff1a;dis ospf peer brief2、拓扑表&#xff08;链路状态数据库&#xff09;&#xff1a;dis ospf lsdb3、路由表&#xff1a;dis ip routing-table 一、OSPF 开销值/度量值&#xff08;cost&#xff09;1、co…

Python基础合集 练习15(内置函数 匿名函数)

匿名函数 以lambda开头表示这是匿名函数&#xff0c;之后的x,y是函数参数 def sub(a,b): return a-b print(sub(10,3)) print(lambda x,y:x-y) sublambda x,y:x-y print(sub(8,4)) def game(math,chinese,english): “”" 功能&#xff1a;计算三科的成绩 math&#xf…

谈谈多线程的上线文切换

大家好&#xff0c;我是易安&#xff01; 我们知道&#xff0c;在并发程序中&#xff0c;并不是启动更多的线程就能让程序最大限度地并发执行。线程数量设置太小&#xff0c;会导致程序不能充分地利用系统资源&#xff1b;线程数量设置太大&#xff0c;又可能带来资源的过度竞争…

【C++】隐式转换与explicit关键字、运算符及其重载、this关键字

C隐式转换与explicit关键字 隐式构造函数 隐含的意思是不会明确告诉你要做什么 隐式转换 C允许编译器对代码执行一次隐式转换&#xff0c;而不需要使用casr强制转换 例1 #include <iostream> #include <string>class Entity { private:std::string m_Name;in…

13 SQL——数值函数

1 ceil() 数值向上取整&#xff08;前提是小数位不是0&#xff09; select ceil(1.2);2 floor() 数值向下取整&#xff08;前提是小数位不是0&#xff09;select floor(1.8);3 mod() 取&#xff08;x%y&#xff09;的模运算&#xff08;求余数运算&#xff09; select …

10. hr 综合面试题汇总

10. hr 综合面试题汇总 C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅) 本章讲解知识点 1.1 HR心理复盘1.2 HR常问问题——学校的表现怎么样啊?1.3 HR常问问题——了解我们公司吗?1.4 HR常问问题——个人情况1.5 HR常问问题——业余生活1.6 HR常问问题——薪资待…

【源码角度】为什么AQS这样设计

AQS&#xff08;AbstractQueuedSynchronizer&#xff0c;抽象同步队列器&#xff09;是 一个基于 FIFO的双端队列。它分为独占模式和共享模式&#xff0c;本文主要围绕独占模式进行讲解&#xff0c;共享模式的原理和独占模式相似&#xff0c;最后会提一嘴。 场景代入 其实AQS模…

云计算基础(持续更新)

文章目录 云计算云计算的定义第1关&#xff1a;云计算定义第2关&#xff1a;云计算的基本原理 云计算出现的背景第1关&#xff1a;云计算出现的背景第2关&#xff1a;云计算的特征第3关&#xff1a;云计算的优势与劣势 虚拟化的类型第1关&#xff1a;虚拟化的定义第2关&#xf…

第六章结构型模式—代理模式

文章目录 代理模式解决的问题概念结构 静态代理动态代理织入的概念JDK 动态代理JDK 动态代理分析 CGLIB 动态代理 三种代理的对比JDK 和 CGLIB 的区别动态代理和静态代理的对比代理模式的优缺点使用场景 结构型模式描述如何将类或对象按某种布局组成更大的结构&#xff0c;有以…

浅谈springboot启动过程

1. 知识回顾 为了后文方便&#xff0c;我们先来回顾一下spring的一些核心概念。 spring最核心的功能无非是ioc容器&#xff0c;这个容器里管理着各种bean。ioc容器反映在java类上就是spring的核心类ApplicationContext。ApplicationContext有众多的子接口和子类&#xff0c;不…

SAP重复制造入门到放弃系列之基本配置

目录 前言 主要配置清单&#xff1a; REM参数文件&#xff1a; 计划订单计划参数文件 维护输入项参数 维护行选择 确认和物流信息系统全局设置 定义确认处理 操作方法表 其他 前言 重复制造中的配置步骤包括创建重复制造配置文件、为运行计划数量&#xff08;计划订单&a…

Excel技能之打印,19+技巧超省纸

颜色太多&#xff0c;重新打印。 没有边框&#xff0c;重新打印。 少了几列&#xff0c;重新打印。 整个工作表打印出来&#xff0c;拿剪刀把自己需要的数据剪下来&#xff0c;用胶水贴到另一张新的A4纸。 你上班打印资料&#xff0c;浪费了多少纸&#xff0c;认真算一下&…

PyCharm with Remote Interpreters

文章目录 一: 利用 Docker 镜像搭建1.编写 Dockerfile2.构建并配置 Remote Interpreters3.结论0.FAQ 二: 利用 SSH Docker 镜像搭建1.编写 Dockerfile2.构建并运行3.构建镜像4.运行容器5.验证并配置0.FAQ 起因是需要在 M2 芯片的 Mac 上调试基于 Python3.6.1 构建的程序. 而 M…