当前位置: 首页 > news >正文

【奔跑吧!Linux 内核(第二版)】第6章:简单的字符设备驱动(一)

笨叔 陈悦. 奔跑吧 Linux 内核(第2版) [M]. 北京: 人民邮电出版社, 2020.

文章目录

  • 从一个简单的字符设备开始
    • 一个简单的字符设备
  • 字符设备驱动详解
    • 字符设备驱动的抽象(cdev 数据结构)
    • 设备节点
    • file_operations 数据结构
  • 演示
  • 源码
    • *驱动程序*
    • *Makefile*
    • *用户空间程序*

为了写好一个设备驱动程序,你需要具备如下知识技能:

  1. 了解 Linux 内核字符设备驱动的架构。其中包括了解 Linux 字符设备驱动是如何组织的,应用程序是如何与驱动交互的。
  2. 了解 Linux 内核字符设备驱动相关的 API。其中涉及字符设备的相关基础知识,如字符设备的描述、设备号的管理、file_operations 的实现、ioctl 交互的设计和 Linux 设备模型的管理等。
  3. 了解 Linux 内核内存管理的 API。设备驱动不可避免地需要和内存打交道,如设备里的数据需要和用户程序交互,设备需要做 DMA 操作等。常见的应用场景有很多,如设备的内存需要映射到用户空间,然后和用户空间中的程序做交互,这就会用到 mmap 这个 API。
  4. 了解 Linux 内核中管理中断的 API。因为几乎所有的设备都支持中断模式,所以中断程序是设备驱动中不可或缺的部分。我们需要了解和熟悉 Linux 内核提供的中断管理相关的接口函数,例如,如何注册中断、如何编写中断处理程序等。
  5. 了解 Linux 内核中同步和锁等相关的 API。因为 Linux 是多进程、多用户的操作系统,而且支持内核抢占,所以进程间同步变得很复杂,即使是编写简单的字符设备驱动,也需要考虑同步和竞争的问题。
  6. 了解所要编写驱动的芯片原理。

从一个简单的字符设备开始

字符设备驱动开发流程:

  1. (.init)分配设备号(手动或自动,int register_chrdev_region(dev_t from, unsigned count, const char *name); 或者 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name););
  2. (.init)分配 cdev 结构体(静态或动态,static struct cdev mydemo_cdev; 或者 struct mydemo_cdev = cdev_alloc(););
  3. (.init)初始化 cdev 结构体,建立设备与驱动操作方法集 file_operations 之间的连接关系(void cdev_init(mydemo_cdev, const struct file_operations *fops););
  4. (.init)注册 cdev 到系统中(int cdev_add(mydemo_cdev , dev_t dev, unsigned count); );
  5. 实现 file_operations 中的操作函数;
  6. 创建设备文件(mknod或udev机制创建);
  7. 编写用户空间程序,以使用设备。

一个简单的字符设备

模块初始化部分:

static int __init simple_char_init(void)
{int ret;ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);if(ret){printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if(!demo_cdev){printk("cdev_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, count);if(ret){printk("cdev_add failed\n");goto cdev_fail;}printk("succeeded register char device: %s\n", DEMO_NAME);printk("Major number=%d, minor number=%d\n", MAJOR(dev), MINOR(dev));return 0;
cdev_fail:cdev_del(demo_cdev);unregister_chrdev:unregister_chrdev_region(dev, count);return ret;
}static void __exit simple_char_exit(void)
{printk("removing device\n");if(demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, count);
}

file_operations 核心函数:

static int demodrv_open(struct inode *inode, struct file *file)
{int major = MAJOR(inode->i_rdev);int minor = MINOR(inode->i_rdev);printk("%s: major=%d, minor=%d\n", __func__, major, minor);return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{return 0;
}static ssize_t demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{printk("%s enter\n", __func__);return 0;
}static ssize_t demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{printk("%s enter\n", __func__);return 0;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write
};

字符设备驱动详解

字符设备驱动的抽象(cdev 数据结构)

cdev结构体定义在Linux内核头文件<linux/cdev.h>中,其完整定义如下:

struct cdev {struct kobject kobj;           // 内嵌的内核对象struct module *owner;          // 指向拥有该设备的模块指针const struct file_operations *ops; // 文件操作结构体指针struct list_head list;         // 用于将设备加入内核链表中dev_t dev;                     // 设备号(主设备号+次设备号)unsigned int count;            // 设备数量
};
  • ​​kobj​​:表示一个内核对象,用于实现内核对象的通用功能,如引用计数、sysfs接口支持等。它是cdev在内核中注册为对象的基础。
  • owner​​:指向拥有该字符设备的内核模块的指针,通常设置为THIS_MODULE。这个成员可以防止设备的方法正在被使用时,设备所在模块被意外卸载。
  • ops​​:指向file_operations结构体的指针,该结构体定义了字符设备的各种操作函数,如open、read、write等。这是字符设备驱动与内核交互的核心接口。
  • ​​list​​:用于将cdev结构体链接到一个内核链表中,便于内核管理多个字符设备或设备实例。
  • dev​​:设备号,为32位整数,其中高12位表示主设备号,低20位表示次设备号。主设备号标识设备类型,次设备号区分同类型的不同设备。
  • count​​:表示与该设备关联的设备号范围,通常为1。当驱动程序管理多个次设备时,count表示次设备号的数量。

申请到设备号后,一般可以通过如下宏获取主设备号和此设备号。

#define MAJOR(dev)    ((dev) >> 20)     // 提取主设备号(高12位)
#define MINOR(dev)    ((dev) & 0xFFFFF) // 提取次设备号(低20位)
#define MKDEV(ma, mi) ((ma) << 20 | (mi)) // 生成设备号

设备节点

Linux 系统中有一条原则 —— “万物皆文件”。设备节点也算特殊的文件,成为设备文件,是连接内核空间驱动和用户空间应用程序的桥梁。如果应用程序想使用驱动提供的服务或者操作设备,那么需要通过访问设备文件来完成。设备文件使得用户程序操作硬件设备就像操作普通文件一样方便。

设备节点的生成方式有两种:

  1. mknod 命令:mknod filename type major minor,type: c/d
  2. udev 机制。udev 是一个在用户空间中使用的工具,它能够根据系统中硬件设备的状态动态地更新设备节点,包括设备节点的创建、删除等。这种机制必须联合 sysfs 和 tmpfs 来实现,sysfs 为 udev 提供设备入口和 uevent 通道,tmpfs 为 udev 设备文件提供存放空间。

file_operations 数据结构

struct file_operations {struct module *owner;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 (*read_iter)(struct kiocb *, struct iov_iter *);ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);int (*iterate)(struct file *, struct dir_context *);int (*iterate_shared)(struct file *, struct dir_context *);__poll_t (*poll)(struct file *, struct poll_table_struct *);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);long (*compat_ioctl)(struct file *, unsigned int, unsigned long);int (*mmap)(struct file *, struct vm_area_struct *);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*open)(struct inode *, struct file *);int (*flush)(struct file *, fl_owner_t id);int (*release)(struct inode *, struct file *);int (*fsync)(struct file *, loff_t, loff_t, int datasync);int (*fasync)(int, struct file *, int);int (*lock)(struct file *, int, struct file_lock *);ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);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 **, void **);int (*fallocate)(struct file *, int, loff_t, loff_t);void (*show_fdinfo)(struct seq_file *, struct file *);unsigned long (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);int (*remap_file_range)(struct file *, loff_t, struct file *, loff_t, loff_t, unsigned int);int (*fadvise)(struct file *, loff_t, loff_t, int);
};

关键成员说明:

  • owner​​指向拥有该结构的模块指针(通常为 THIS_MODULE),用于模块引用计数管理。
  • read/write​​同步读写接口,需使用 copy_to_user/copy_from_user安全访问用户空间缓冲区。
  • read_iter/write_iter​​异步读写接口(替代旧版 aio_read/aio_write),支持零拷贝操作。
  • unlocked_ioctl​​设备控制命令入口,无需大内核锁,参数直接传递用户空间数据。
  • mmap​​将设备内存映射到用户空间,常用于帧缓冲区等场景。
  • poll​​实现非阻塞 I/O,返回设备可读写状态(如 POLLIN)。
  • open/release​​设备打开/关闭时的资源初始化和清理入口。

演示

1.编译设备驱动程序并加载驱动模块。

在这里插入图片描述

通过 dmesg 命令可以获取到系统为驱动程序所分配的设备号,亦可以通过查看 /proc/devices 虚拟文件系统中的 devices 节点信息获取设备号(主设备号:240),如下所示:

在这里插入图片描述
2.创建设备节点文件——搭建用户空间和内核空间的桥梁。

在这里插入图片描述

3.在用户空间设计测试程序。

通过操作第2步创建好的/dev/demo_drv文件,实现对设备驱动程序的控制。

在这里插入图片描述

源码

驱动程序

simple_char.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>#define DEMO_NAME "my_demo_dev"
static dev_t dev;
static struct cdev *demo_cdev;
static signed count = 1;static int demodrv_open(struct inode *inode, struct file *file)
{int major = MAJOR(inode->i_rdev);int minor = MINOR(inode->i_rdev);printk("%s: major=%d, minor=%d\n", __func__, major, minor);return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{return 0;
}static ssize_t demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{printk("%s enter\n", __func__);return 0;
}static ssize_t demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{printk("%s enter\n", __func__);return 0;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write
};static int __init simple_char_init(void)
{int ret;ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);if(ret){printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if(!demo_cdev){printk("cdev_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, count);if(ret){printk("cdev_add failed\n");goto cdev_fail;}printk("succeeded register char device: %s\n", DEMO_NAME);printk("Major number=%d, minor number=%d\n", MAJOR(dev), MINOR(dev));return 0;
cdev_fail:cdev_del(demo_cdev);unregister_chrdev:unregister_chrdev_region(dev, count);return ret;
}static void __exit simple_char_exit(void)
{printk("removing device\n");if(demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, count);
}module_init(simple_char_init);
module_exit(simple_char_exit);MODULE_AUTHOR("JJM");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simple character device");

Makefile

BASEINCLUDE ?= /lib/modules/`uname -r`/buildmydemo-objs := simple_char.oobj-m := mydemo.oall:$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modulesclean:$(MAKE) -C $(BASEINCLUDE) M=$(PWD) cleanrm -f *.ko

用户空间程序

test.c

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>#define DEMO_DEV_NAME "/dev/demo_drv"int main()
{char buffer[64];int fd;fd = open(DEMO_DEV_NAME, O_RDONLY);if(fd < 0){printf("open device %s failed\n", DEMO_DEV_NAME);return -1;}read(fd, buffer, 64);close(fd);return 0;
}
http://www.dtcms.com/a/319836.html

相关文章:

  • 解决 Nginx 反代中 proxy_ssl_name 环境变量失效问题:网页能打开但登录失败
  • 3深度学习Pytorch-神经网络--全连接神经网络、数据准备(构建数据类Dataset、TensorDataset 和数据加载器DataLoader)
  • TCP 如何保证可靠性
  • Linux openssl、openssh 升级 保留旧版本
  • 【插件式微服务架构系统分享】之 解耦至上:gateway 网关与APISIX 网关的不同分工
  • React 为什么要自定义 Hooks?
  • 一文解读“Performance面板”前端性能优化工具基础用法!
  • 顺序表——C语言
  • FPGA学习笔记——VGA静态字符的显示(寄存器)
  • SOMGAN:利用自组织映射提高生成对抗网络的模式探索能力
  • 国内PCB批量厂家推荐
  • Linux 文件IO与标准IO的区别解析
  • wordpress安装环境推荐php8.0+mysql5.7
  • Linux Docker 新手入门:一文学会配置镜像加速器
  • 谷歌推出Genie3:世界模型的ChatGPT时刻?
  • 8.3.1 注册服务中心Etcd
  • MHA架构实战
  • 我在学习spring 项目构建时遇到问题,解决JDK最低版本只有17的问题
  • 佳文赏读 || 冶炼钒钛矿高炉操作炉型计算模拟研究
  • 【大前端】vite忽略指定前缀的静态资源
  • 移动端 WebView 登录异常与会话恢复问题全流程排查指南
  • 灯哥FOC笔记-----(1)无刷电机概论和硬件控制原理
  • 高精度惯性导航IMU供应商价格选型
  • 面向制造业的 AI Agent 架构与实战:任务驱动的智能协同新路径
  • 常见命令-资源查看-iostat命令实践
  • React 中 Context(上下文)介绍
  • ReAct Agent 原生代码实现(纯Python实现)
  • OpenAI推出开源GPT-oss-120b与GPT-oss-20b突破性大模型,支持商用与灵活部署!
  • 使用RestTemplate发送与接收http/https请求
  • 告别YAML,在SpringBoot中用数据库配置替代配置文件