字符设备驱动程序

Linux 驱动程序开发概述

①应用程序、库、内核、驱动程序的关系

以点亮一个 LED 为例,这 4 层软件的协作关系如下所示:

  1. 应用程序使用库提供的 open 函数打开代表 LED 的设备文件。
  2. 库根据 open 函数传入的参数执行“swi”指令,这条指令会引起 CPU 异常,进入内核。
  3. 内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序。
  4. 应用程序得到文件句柄后,使用库提供的 write 或 ioclt 函数发出控制命令。
  5. 库根据 write 或 ioclt 函数传入的参数执行“swi”指令,这条指令会引起 CPU 异常,进入内核。
  6. 内核的异常处理函数根据这些参数调用驱动程序的相关函数,点亮 LED。

②驱动程序的分类和开发步骤

1. 字符设备(Character Device)

核心特征:以字节流为单位读写,类似文件访问,无强制缓冲区要求(可自行实现缓冲区提升效率)。

接口实现:驱动需实现open、close、read、write等系统调用,应用层通过/dev目录下的设备文件(如/dev/ttySAC0)访问。

典型例子:串口设备,数据收发以单个字节为单位进行。

2. 块设备(Block Device)

核心特征:数据以固定块(或页)为单位存储(如NAND Flash按页存储)。

用户层接口:与字符设备一致,通过设备文件(如/dev/mtdblock0、/dev/hda1)调用标准系统调用,用户无感知差异

特殊之处

  • 硬件操作逻辑:需先将用户数据组织成块再读写,或从设备读块数据后提取用户所需内容。
  • 数据格式与内核接口:块设备需支持文件系统格式,驱动除面向用户层接口外,还需向内核提供底层接口,支持文件系统挂载(mount)。

块设备本身仅负责 “按固定大小块存储原始数据”(类似仓库的货架格子,只存东西不分类别),但杂乱的数据块无法被用户识别(不知道哪个块对应哪个文件)。

文件系统就是 “整理规则”:定义数据块的组织方式(如文件名、权限、存储位置、目录结构),比如 EXT4、FAT32 就是不同的规则。

挂载的本质是 “将文件系统规则绑定到块设备”,让内核知道 “如何解读这个块设备里的数据”。而驱动的内核层接口就是 “绑定的关键”:文件系统需要通过驱动提供的底层接口,实现对块设备的读写控制(如读取超级块、分配数据块),没有这些接口,文件系统无法操作块设备,自然无法挂载。

3. 网络接口(Network Interface)

核心特征:兼具字符/块设备部分特点,无法归入前两类,数据以非固定大小的报文/包/帧为单位传输。

访问方式:分配唯一名称(如eth0),但/dev目录下无对应节点;通信不依赖open、readwrite,而是通过内核/库提供的数据包传输专用函数。

标准开发流程

  1. 硬件调研:查看原理图、设备数据手册,明确硬件操作逻辑(如寄存器地址、读写时序)。

  2. 模板选型:内核中寻找相近驱动作为模板(优先复用现有框架),特殊场景需从零开发。

  3. 初始化实现:向内核注册驱动,使内核能通过应用层传入的文件名匹配到对应驱动。

  4. 操作函数设计:实现核心系统调用对应的函数(如open、close、read、write),封装硬件操作逻辑。

  5. 中断服务(可选):若设备支持中断(如按键、串口),编写中断服务程序,处理硬件中断事件。

  6. 编译与加载:将驱动编译进内核,或通过insmod命令动态加载(调试阶段优先动态加载)。

  7. 测试验证:编写应用程序调用设备文件,测试驱动功能完整性、稳定性及异常场景适配性。

③驱动程序的加载和卸载

可以将驱动程序静态编译进内核中,也可以将它作为模块在使用时再加载。

当使用 insmod 加载模块时,模块的初始化函数被调用,它用来向内核注册驱动程序;当使用 rmmod 卸载模块时,模块的清除函数被调用。在驱动代码中,这两个函数要么取固定的名字:init_module 和 cleanup_module,要么使用以下两行来标记它们(假设初始化函数、清除函数为 my_init 和 my_cleanup)。

作用
module_init()告诉内核:模块加载时,调用哪个函数
module_exit()告诉内核:模块卸载时,调用哪个函数
module_init(my_init); module_exit(my_cleanup);

字符设备驱动程序开发

对于每个系统调用,驱动程序中都有一个与之对应的函数。对于字符设备驱动程序,这些函数集合在一个 file_operations 类型的数据结构中。file_operations 结构在 Linux 内核include/linux/fs.h 文件中定义。

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 (*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); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 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 *); 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); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *); 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 (*dir_notify)(struct file *filp, unsigned long arg); 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); };

当应用程序使用 open 函数打开某个设备时,设备驱动程序的 file_operations 结构中的open 成员就会被调用;当应用程序使用 read、write、ioctl 等函数读写、控制设备时,驱动程序的 file_operations 结构中的相应成员(read、write、ioctl 等)就会被调用。从这个角度来说,编写字符设备驱动程序就是为具体硬件的 file_operations 结构编写各个函数(并不需要全部实现 file_operations 结构中的成员)。
那么,当应用程序通过 open、read、write 等系统调用访问某个设备文件时,Linux 系统怎么知道去调用哪个驱动程序的 file_operations 结构中的 open、read、write 等成员呢?

(1)设备文件有主/次设备号。
设备文件分为字符设备、块设备,比如 PC 机上的串口属于字符设备,硬盘属于块设备。在 PC 上运行命令“ls /dev/ttyS0 /dev/hda1-l”可以看到:

brw-rw---- 1 root disk 3, 1 Jan 30 2003 /dev/hda1 crw-rw---- 1 root uucp 4, 64 Jan 30 2003 /dev/ttyS0

“brw-rw----”中的“b”表示/dev/hda1 是个块设备,它的主设备号为 3,次设备号为 1;
“crw-rw----”中的“c”表示/dev/ttyS0 是个块设备,它的主设备号为 4,次设备号为 64。
(2)模块初始化时,将主设备号与 file_operations 结构一起向内核注册。
驱动程序有一个初始化函数,在安装驱动程序时会调用它。在初始化函数中,会将驱动程序的 file_operations 结构连同其主设备号一起向内核进行注册。对于字符设备使用如下以下函数进行注册:

int register_chrdev(unsigned int major, const char * name, struct file_ operations *fops);

这样,应用程序操作设备文件时,Linux 系统就会根据设备文件的类型(是字符设备还是块设备)、主设备号找到在内核中注册的 file_operations 结构(对于块设备为 block_device_
operations 结构),次设备号供驱动程序自身用来分辨它是同类设备中的第几个。
编写字符驱动程序的过程大概如下。
(1)编写驱动程序初始化函数。
进行必要的初始化,包括硬件初始化(也可以放其他地方)、向内核注册驱动程序等。
(2)构造 file_operations 结构中要用到的各个成员函数。
实际的驱动程序当然比上述两个步骤复杂,但这两个步骤已经可以让我们编写比较简单的驱动程序,比如 LED 控制。

LED 驱动程序代码分析

下面按照函数调用的顺序进行讲解,模块的初始化函数和卸载函数如下:

86 /* 87 * 执行“insmod s3c24xx_leds.ko”命令时就会调用这个函数 88 */ 89 static int __init s3c24xx_leds_init(void) 90 { 91 int ret; 92 93 /* 注册字符设备驱动程序 94 * 参数为主设备号、设备名字、file_operations 结构; 95 * 这样,主设备号就和具体的 file_operations 结构联系起来了, 96 * 操作主设备为 LED_MAJOR 的设备文件时,就会调用 s3c24xx_leds_fops 中的相关成 员函数 97 * LED_MAJOR 可以设为 0,表示由内核自动分配主设备号 98 */ 99 ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops); 100 if (ret < 0) { 101 printk(DEVICE_NAME " can't register major number\n"); 102 return ret; 103 } 104 105 printk(DEVICE_NAME " initialized\n"); 106 return 0; 107 } 108 109 /* 110 * 执行“rmmod s3c24xx_leds.ko”命令时就会调用这个函数 111 */ 112 static void __exit s3c24xx_leds_exit(void) 113 { 114 /* 卸载驱动程序 */ 115 unregister_chrdev(LED_MAJOR, DEVICE_NAME); 116 } 117 118 /* 这两行指定驱动程序的初始化函数和卸载函数 */ 119 module_init(s3c24xx_leds_init); 120 module_exit(s3c24xx_leds_exit); 121

第 119、120 两行用来指明装载、卸载模块时所调用的函数。也可以不使用这两行,但是需要将这两个函数的名字改为 init_module、cleanup_module。
执行“insmod s3c24xx_leds.ko”命令时就会调用 s3c24xx_leds_init 函数,这个函数核心的代
码只有第 99 行。它调用 register_chrdev 函数向内核注册驱动程序:将主设备号 LED_MAJOR 与
file_operations 结构 s3c24xx_leds_fops 联系起来。以后应用程序操作主设备号为 LED_MAJOR 的设备文件时,比如 open、read、write、ioctl,s3c24xx_leds_fops 中的相应成员函数就会被调用。但是,s3c24xx_leds_fops 中并不需要全部实现这些函数,用到哪个就实现哪个。
执行“rmmod s3c24xx_leds.ko”命令时就会调用 s3c24xx_leds_exit 函数,它进而调用
unregister_chrdev 函数卸载驱动程序,它的功能与 register_chrdev 函数相反。
s3c24xx_leds_init、s3c24xx_leds_exit 函数前的“_ _init”、“_ _exit”只有在将驱动程序静
态链接进内核时才有意义。前者表示 s3c24xx_leds_init 函数的代码被放在“.init.text”段中,这个段在使用一次后被释放(这可以节省内存);后者表示 s3c24xx_leds_exit 函数的代码被放在“.exit.data”段中,在连接内核时这个段没有使用,因为不可能卸载静态键接的驱动程序。

下面来看看 s3c24xx_leds_fops 的组成。

76 /* 这个结构是字符设备驱动程序的核心 77 * 当应用程序操作设备文件时所调用的 open、read、write 等函数, 78 * 最终会调用这个结构中的对应函数 79 */ 80 static struct file_operations s3c24xx_leds_fops = { 81 .owner = THIS_MODULE, /* 这是一个宏,指向编译模块时自动创建的_ _this_ module 变量 */ 82 .open = s3c24xx_leds_open, 83 .ioctl = s3c24xx_leds_ioctl, 84 }; 85

第 81 行的宏 THIS_MODULE 在 include/linux/module.h 中定义如下,_ _this_module 变量在编译模块时自动创建,无需关注这点。
#define THIS_MODULE (&_ _this_module)
file_operations 类型的 s3c24xx_leds_fops 结构是驱动中最重要的数据结构,编写字符设备驱动程序的主要工作也是填充其中的各个成员。比如本驱动程序中用到 open、ioctl 成员被设为 s3c24xx_leds_open、s3c24xx_leds_ioctl 函数,前者用来初始化 LED 所用的 GPIO 引脚,后者用来根据用户传入的参数设置 GPIO 的输出电平。

s3c24xx_leds_open 函数的代码如下:

33 /* 应用程序对设备文件/dev/leds 执行 open()时, 34 * 就会调用 s3c24xx_leds_open 函数 35 */ 36 static int s3c24xx_leds_open(struct inode *inode, struct file *file) 37 { 38 int i; 39 40 for (i = 0; i < 4; i++) { 41 // 设置 GPIO 引脚的功能:本驱动中 LED 所涉及的 GPIO 引脚设为输出功能 42 s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]); 43 } 44 return 0; 45 } 46

在应用程序执行 open(“/dev/leds”,...)系统调用时,s3c24xx_leds_open 函数将被调用。它用来将 LED 所涉及的 GPIO 引脚设为输出功能。不在模块的初始化函数中进行这些设置的原因是:虽然加载了模块,但是这个模块却不一定会被用到,就是说这些引脚不一定用于这些用途,它们可能在其他模块中另作他用。所以,在使用时才去设置它,我们把对引脚的初始化放在 open 操作中。第 42 行的 s3c2410_gpio_cfgpin 函数是内核里实现的,它被用来选择引脚的功能。其实现原理就是设置 GPIO 的控制寄存器。
s3c24xx_leds_ioctl 函数的代码如下:

47 /* 应用程序对设备文件/dev/leds 执行 ioclt()时, 48 * 就会调用 s3c24xx_leds_ioctl 函数 49 */ 50 static int s3c24xx_leds_ioctl( 51 struct inode *inode, 52 struct file *file, 53 unsigned int cmd, 54 unsigned long arg) 55 { 56 if (arg > 4) { 57 return -EINVAL; 58 } 59 60 switch(cmd) { 61 case IOCTL_LED_ON: 62 // 设置指定引脚的输出电平为 0 63 s3c2410_gpio_setpin(led_table[arg], 0); 64 return 0; 65 66 case IOCTL_LED_OFF: 67 // 设置指定引脚的输出电平为 1 68 s3c2410_gpio_setpin(led_table[arg], 1); 69 return 0; 70 71 default: 72 return -EINVAL; 73 } 74 } 75

应用程序执行系统调用 ioclt(fd, cmd, arg)时(fd 是前面执行 open 系统调用时返回的文件句柄),s3c24xx_leds_ioctl 函数将被调用。第 63、68 行根据传入的 cmd、arg 参数调用 s3c2410_gpio_setpin 函数,来设置引脚的输出电平:输出 0 时点亮 LED,输出 1 时熄灭 LED。
s3c2410_gpio_setpin 函数也是内核中实现的,它通过 GPIO 的数据寄存器来设置输出电平。

驱动程序测试

06 #define IOCTL_LED_ON 0 07 #define IOCTL_LED_OFF 1 … 16 int main(int argc, char **argv) 17 { … 24 fd = open("/dev/leds", 0); // 打开设备 … 30 led_no = strtoul(argv[1], 0, 0) - 1; // 操作哪个 LED? … 34 if (!strcmp(argv[2], "on")) { 35 ioctl(fd, IOCTL_LED_ON, led_no); // 点亮它 36 } else if (!strcmp(argv[2], "off")) { 37 ioctl(fd, IOCTL_LED_OFF, led_no); // 熄灭它 38 } else { 39 goto err; 40 } … 50 }

其中的 open、ioclt 最终会调用驱动程序中的 s3c24xx_leds_open、s3c24xx_leds_ioctl函数。

📌-1 说明:

  • argv[1]是命令行参数(如1

  • 用户习惯用1、2、3…

  • 驱动中一般用0、1、2…

  • 所以要-1

最终使用以下命令点灯灭灯:

# led_test 1 on # led_test 1 off

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

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

相关文章

testing

今天是2026-01-23,是我注册博客园的第一天,让我们写个博客测试一下吧,好了,拜拜!

两个 Docker 容器如何通信?Docker 网络问题完整踩坑与解决指南

NebulaGraph Studio 连接失败&#xff1f;Docker 网络问题完整踩坑与解决指南&#xff08;小白友好&#xff09; 一、问题背景 我在本地使用 Docker 部署 NebulaGraph 集群&#xff0c;同时使用 Nebula Graph Studio&#xff08;Web UI&#xff09; 进行可视化管理。 Nebula Gr…

芒格的“避免失败“原则在前沿科技投资中的重要性

芒格的"避免失败"原则在前沿科技投资中的重要性关键词&#xff1a;芒格、避免失败原则、前沿科技投资、风险控制、投资策略摘要&#xff1a;本文深入探讨了芒格的“避免失败”原则在前沿科技投资领域的重要性。通过对该原则的背景介绍&#xff0c;阐述其核心概念及与…

关与短链接API,其中稳定无毒的少之又少。

关与短链接API,其中稳定无毒的少之又少。关与短链接,一直想自己开发,开发一个也不是很难,后来想了又想,自己搭建需要购买服务器和域名。 于是就在网上查找关于稳定的API接口,有各种各样的,其中稳定无毒的少之又少…

数据结构——冒泡排序 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

机械制造ToB企业获客困境与数字化解决方案架构深度解析

在当今高度竞争的工业市场中&#xff0c;机械制造类ToB&#xff08;企业对企业&#xff09;企业正面临着一场深刻的获客模式变革。传统的销售路径严重依赖线下展会、行业人脉和经销商渠道&#xff0c;这种模式在信息透明度极高的数字化时代&#xff0c;其局限性日益凸显&#x…

Java毕设项目:基于springboot的二次元商品商城系统(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

Java计算机毕设之基于SpringBoot + Vue的电子产品手机数码销售系统基于springboot的电子产品电子外设销售系统(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

【毕业设计】基于springboot的二次元商品商城系统(源码+文档+远程调试,全bao定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

【课程设计/毕业设计】基于springboot的电子产品销售系统基于springboot的电子产品电子外设销售系统【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

【课程设计/毕业设计】基于SpringBoot与Vue的动漫周边商场系统设计与实现基于springboot的二次元商品商城系统【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

MyEMS开源能源管理系统:赋能生物化学农药及微生物农药制造业绿色低碳转型

各位读者&#xff0c;大家好&#xff01;我今天要给大家介绍的是MyEMS开源能源管理系统。在当下双碳政策的大环境下&#xff0c;生物化学农药及微生物农药制造业面临着巨大的绿色低碳转型压力。而MyEMS开源能源管理系统正是赋能该行业转型的利器。本次演讲将围绕六个方面展开。…

开源驱动零碳实践:MyEMS 赋能零碳工厂建设的核心路径

在 “双碳” 目标纵深推进的背景下&#xff0c;零碳工厂建设已成为工业领域绿色转型的核心载体。《关于开展零碳工厂建设工作的指导意见》&#xff08;以下简称《指导意见》&#xff09;明确提出 “坚持因业施策、创新引领、稳妥有序&#xff0c;推动绿色化与智能化深度融合” …

服装加工ERP系统是什么?它能为企业带来哪些效率提升?

服装加工ERP系统如何提升企业整体运营效率 服装加工ERP系统能够通过多种方式提升企业整体运营效率。首先&#xff0c;系统能够有效地整合信息流、物流和资金流&#xff0c;减少信息孤岛。这意味着各个部门可以顺畅沟通&#xff0c;减少重复劳动。同时&#xff0c;艾格文服装ERP…

【工具变量】全国地级市文旅融合DID数据集(2008-2025年)

一、数据简介 本数据以周春波等&#xff08;2025&#xff09;《文旅融合政策与旅游业发展&#xff1a;效应评估与机制检验》的研究框架为参考&#xff0c;构建文旅融合政策DID虚拟变量。文旅融合并非文化与旅游的简单叠加&#xff0c;而是指文化要素与旅游活动在产业、空间、功…

10399_基于SpringBoot的智慧养老院管理系统

1、项目包含 项目源码、项目文档、数据库脚本、软件工具等资料; 带你从零开始部署运行本套系统。 2、技术说明 后端:SpringBoot 前端:VUE 数据库:MySql 开发工具:JDK1.8及以上 + Eclipse + MySQL + Maven 本项目涉…

诺斯罗普·格鲁曼加速辐射测试

激光等离子体加速器技术的应用&#xff0c;使得用于太空和核设施的微电子器件的测试速度提升至原来的十倍。在国防高级研究计划局&#xff08;DARPA&#xff09;的“单粒子效应辐射测试先进源计划”&#xff08;ASSERT&#xff09;框架下&#xff0c;诺斯罗普格鲁曼公司为应用于…

代码源挑战赛 Round 47

比赛链接 A 简单题 B 在题目条件下,如果 \(|s| = 1\) 则答案为 \(1\),否则判一下最大的数字。 C 显然可以对于每一位分别考虑,记 \(\operatorname {dist} (x, y)\) 表示 \(x, y\) 在模 \(26\) 意义下的距离,数字串…

26年1月23算法

基础算法 看ACWING崩溃一天 快速排序点击查看代码 #include<iostream> using namespace std; const int N = 1e6+10; int n; int q[N];void quick_sort(int q[], int l, int r){// 递归终止条件:区间长度小于等…

编辑标题

目录一、LoRA rank 的含义二、rank 太低 → 学不到细节三、rank 太高 → 容易记住训练集(过拟合)四、从训练指标判断 rank 是否合适五、可用面试回答模板 明白,这个问题涉及 LoRA 的核心机制:低秩矩阵注入 + 参数量…