三. 应用程序调用驱动过程分析
- [1. 面向对象编程](#1. 面向对象编程)
-
- [1.1 kzalloc 的使用注意点](#1.1 kzalloc 的使用注意点)
- [2. 应用调用驱动过程分析](#2. 应用调用驱动过程分析)
-
- [2.1 了解内核中相关的几个结构体](#2.1 了解内核中相关的几个结构体)
- [2.2 应用调用驱动过程](#2.2 应用调用驱动过程)
- [3. 杂项设备](#3. 杂项设备)
-
- [3.1 杂项设备对象类型](#3.1 杂项设备对象类型)
- [3.2 杂项设备对象注册和注销](#3.2 杂项设备对象注册和注销)
- [3.3 杂项设备驱动实例---led驱动](#3.3 杂项设备驱动实例---led驱动)
- [4. iotcl 的实现](#4. iotcl 的实现)
- [5. 操作寄存器的方式](#5. 操作寄存器的方式)
-
- [5.1 通过直接位运算](#5.1 通过直接位运算)
- [5.2 内核提供的函数](#5.2 内核提供的函数)
文章摘要:本文分析了 Linux应用程序调用驱动 的完整链路,重点介绍了内核中的关键数据结构及其交互关系。主要内容包括:1) 使用kzalloc进行设备对象内存分配时的注意事项;2) struct file、struct inode和struct cdev等核心结构体的作用解析,特别是 file_operations 作为VFS与驱动的桥梁功能;3) 面向对象编程在驱动开发中的应用,包括通过 container_of 获取设备对象和 private_data 的使用方法。文章通过代码示例展示了从用户空间 open() 到硬件寄存器访问的完整调用路径。
在"应用调用驱动"的链路中,这里使用一条清晰的执行链条串联上下文:
用户态应用 →
open()→ VFS →struct inode / struct file→ 驱动file_operations→ 设备对象 → 具体硬件寄存器。
后文的所有代码、数据结构与 API 都围绕这条路径展开。
1. 面向对象编程
c
static inline void *kzalloc(size_t size, gfp_t gfp) //申请物理空间连续,且被初始化的一块内存空间
//参数1 ----- 要申请的空间大小
//参数2 ----- 标志位:GFP_KERNEL
//返回值 ----成功:申请到的空间地址,失败:NULL
例如:
//设计设备对象类型
struct mp157_led{
unsigned int major;
struct class * clz;
struct device * dev;
unsigned int * mode;
unsigned int * odr;
};
struct mp157_led *led_dev;
// 0,实例化全局设备对象
led_dev = kzalloc(sizeof(*led_dev), GFP_KERNEL);
if(!led_dev){
printk("kzalloc error");
return -ENOMEM;
}
1.1 kzalloc 的使用注意点
c
led_dev = kzalloc(sizeof(*led_dev), GFP_KERNEL);
if (!led_dev)
return -ENOMEM;
补充要点:
- GFP 标志位选择
GFP_KERNEL只能在进程上下文、且允许睡眠的场景使用。如果是在中断或原子上下文,需要使用GFP_ATOMIC。
- 错误路径与资源回收
- 申请到的对象在模块卸载或错误路径中必须对应
kfree(led_dev)释放。若使用devm_kzalloc()(设备模型自动管理),则在remove或cleanup时可自动释放。
- 申请到的对象在模块卸载或错误路径中必须对应
- 面向对象设计
struct mp157_led同时承载"状态"和"方法",建议在open时将其保存在filp->private_data中,便于read/write/ioctl直接取得设备实例。
c
static int led_drv_open(struct inode *inode, struct file *filp)
{
struct mp157_led *led = container_of(
inode->i_cdev, struct mp157_led, cdev);
filp->private_data = led;
return 0;
}
使用
container_of既可以保持对象封装,又能从struct cdev回溯到真正的设备对象。
2. 应用调用驱动过程分析
2.1 了解内核中相关的几个结构体
1》struct file ------- //进程打开文件时,被动态创建
c
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
void *private_data;
struct address_space *f_mapping;
errseq_t f_wb_err;
}
struct file 补充常用字段的作用:
f_inode:指向底层的struct inode,用于获取设备的i_cdev、i_rdev等信息。f_op:指向驱动注册的struct file_operations,是连接 VFS 与驱动的桥梁。private_data:驱动自定义指针,是"对象化"的关键。open时通常设置为设备对象,release时清理。
2》 struct inode ------ 描述文件属性信息,当创建文件时,同时会在内核中创建该结构体
c
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
/* Stat data, not accessed from path walking */
unsigned long i_ino;
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec64 i_atime;
struct timespec64 i_mtime;
struct timespec64 i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits;
u8 i_write_hint;
blkcnt_t i_blocks;
/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
void *i_private; /* fs or device private pointer */
};
struct inode 补充常用字段的作用:
- 字符设备的
inode->i_rdev使用MKDEV(major, minor)保存设备号。 - 驱动初始化时通过
cdev_init()/cdev_add()将inode->i_cdev与struct cdev关联,从而在open被调用时能顺利查找到设备对象。
3》struct cdev ------- //申请设备号时,被创建,用于管理驱动
c
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
若采用 miscdevice,misc_register() 内部已经帮我们完成了 cdev 的注册流程,所以不必再次调用下方 API:
c
cdev_init(&led_dev->cdev, &led_fops);
led_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&led_dev->cdev, devt, 1);
4》 设备操作对象类型 ------ 在驱动程序中需要实现的结构体
c
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 (*iopoll)(struct kiocb *kiocb, bool spin);
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 mmap_supported_flags;
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);
}
2.2 应用调用驱动过程
为便于理解,这里用两幅流程图把"应用 → VFS → 驱动"这一条链路串起来,并在图下给出要点说明,帮助大家可以对照代码理解每一步发生了什么。
整体流程:
1. 模块初始化
kzalloc() 创建设备对象并初始化寄存器映射。
misc_register() 或 cdev_add() 完成字符设备注册,内核记录 dev_t ↔ cdev ↔ file_operations 的对应关系。
2. 应用调用 open
VFS 根据路径找到 inode,并由 inode 的 dev_t 找到 cdev。
VFS 创建 struct file,把 cdev->ops 赋给 filp->f_op,再回调 .open()。
3. open 回调中对象化封装
使用 container_of(inode->i_cdev, struct mp157_led, cdev) 找到设备实例。
设置 filp->private_data = led_dev,必要时初始化硬件(如打开时钟、配置 GPIO 模式)。
4. read / write / ioctl
都从 filp->private_data 取回设备实例,对寄存器做 readl / writel 或其他操作。
根据不同命令码(LED_ALL_ON 等)执行对应动作,保持良好错误处理(-EINVAL、-EFAULT 等)。
5. release/模块退出
.release() 里可清理设备状态或引用计数。
模块退出时 misc_deregister() + iounmap() + kfree(),确保资源释放完整。
借助这两张图和文字说明,可以使得整条"应用层调用 → VFS 分发 → 驱动回调 → 操作硬件"的链路更加直观清晰。
图 1:open/read/write 调用链梳理

c
flowchart LR
subgraph User["用户空间(应用进程)"]
A["用户代码\nfd = open(\"/dev/led01\", O_RDWR)"]
B["read/write/ioctl 调用"]
end
subgraph VFS["内核空间:VFS 层"]
C["sys_open()/sys_read()/sys_write()"]
D["struct inode\n(包含 dev_t、i_fop 等)"]
E["struct file\n(运行时创建,保存 f_op、private_data 等)"]
end
subgraph Driver["内核空间:驱动实现"]
F["const struct file_operations led_fops\n.open = led_drv_open\n.read = led_drv_read\n.write = led_drv_write\n.unlocked_ioctl = led_drv_ioctl"]
G["led_drv_open()\n→ 将设备实例绑定到 filp->private_data"]
H["led_drv_read()/write()/ioctl\n→ 通过 private_data 访问硬件寄存器"]
end
A --> C
C --> D
D -->|"根据 dev_t 定位"| F
C -->|"创建 struct file, 关联 f_op"| E
E -->|"f_op->open"| G
B -->|"f_op->read/write/ioctl"| H
图示要点:
- 应用层调用
open时,VFS 会根据路径找到对应struct inode,并依据inode->i_rdev查询出已经注册的struct cdev。 - VFS 通过
inode->i_fop把驱动提供的file_operations绑定到新创建的struct file对象上。 .open()常用于完成对象化封装(把驱动中的设备实例地址写入filp->private_data),后续.read/.write/.ioctl直接从private_data里取回该实例,从而访问硬件寄存器。
图 2:cdev、file_operations 与设备实例的关系

c
flowchart LR
subgraph Register["驱动初始化(模块加载)"]
I["kzalloc() 申请 struct mp157_led"]
J["cdev_init(&led->cdev, &led_fops)"]
K["cdev_add(&led->cdev, dev_t, 1) 或 misc_register()"]
end
subgraph Kernel["内核字符设备核心"]
L["cdev 链表\n(主次设备号为键)"]
M["struct cdev\n·owner\n·ops\n·dev_t"]
end
subgraph Ops["驱动方法表"]
N["const struct file_operations led_fops"]
O[".open = led_drv_open\n.write = led_drv_write\n.release = led_drv_release ..."]
end
subgraph Runtime["运行时(应用 open/read/...)"]
P["struct file\nf_op = led_fops\nprivate_data = led_dev"]
Q["struct mp157_led(设备对象)\n·寄存器映射地址\n·杂项设备对象\n·同步原语"]
end
I --> J --> K --> L --> M
M -.->|"ops = fops 指针"| N --> O
M -->|"对应 dev_t"| P -->|"通过 private_data\n访问设备状态"| Q
图示要点:
- 初始化阶段 通过
kzalloc、cdev_init、cdev_add(或misc_register)把设备对象注册到内核字符设备框架中。 - 每一个
cdev节点记录主次设备号及指向file_operations的指针,VFS 通过dev_t快速定位。 - 运行阶段
open产生的struct file保存f_op与private_data,实现"结构体即对象"的封装;随后的.read/.write/.ioctl都只需对filp->private_data做类型转换即可操作硬件。
3. 杂项设备
3.1 杂项设备对象类型
杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。
在 Linux 内核的 include/linux 目录下有 Miscdevice.h 文件,要把自己定义的 misc device 从设备定义在这里。
其实是因为这些字符设备不符合预先确定的字符设备范畴, 所有这些设备采用主设备号为10 ,一起归于 misc device,其实 misc_register 就是用主设备号10调用 register_chrdev() 的。
也就是说,杂项设备其实也就是特殊的字符设备,可自动生成设备节点。
c
struct miscdevice {
int minor; //次设备号
const char *name; //设备结点名称
const struct file_operations *fops; //指向设备操作对象的指针
struct list_head list;
struct device *parent; //父类
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
3.2 杂项设备对象注册和注销
c
extern int misc_register(struct miscdevice *misc);
//当调用注册函数时,内核申请设备号,并创建设备结点
extern void misc_deregister(struct miscdevice *misc);
3.3 杂项设备驱动实例---led驱动
c
//全局设备对象类型
struct mp157_led{
struct miscdevice misc; //定义杂项设备对象
unsigned int * mode;
unsigned int * odr;
};
struct mp157_led *led_dev;
static int __init led_drv_init(void)
{
int ret;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//1,申请全局设备对象空间 ---同时分配杂项设备对象空间
led_dev = kzalloc(sizeof(*led_dev), GFP_KERNEL);
if(!led_dev){
printk("kzalloc error");
return -ENOMEM;
}
//2,初始化杂项设备对象
led_dev->misc.fops = &led_fops; //设备操作对象地址
led_dev->misc.minor = 7; //次设备号
led_dev->misc.name = "led01"; //设备结点名称
//3,注册杂项设备对象
ret = misc_register(&led_dev->misc);
if(ret < 0){
printk("misc_register error\n");
goto err_kfree;
}
//4,硬件初始化
led_dev->mode = ioremap(GPIOZ,24);
if(!led_dev->mode){
printk("ioremap error\n");
ret = PTR_ERR(led_dev->mode);
goto err_misc_deregister;
}
led_dev->odr = led_dev->mode + 5;
return 0;
err_misc_deregister:
misc_deregister(&led_dev->misc);
err_kfree:
kfree(led_dev);
return ret;
}
4. iotcl 的实现
c
应用层:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
//参数1 ---- 文件描述符
//参数2 ---- 请求
//变参 ---- 根据参数2决定变参的类型和个数
----------------------------------------------------
内核驱动:
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch(cmd){
case 常量1:
break;
case 常量2:
break;
....
}
}
cmd为应用层传递的命令,在驱动中需要定义命令,一般有两种方法:
方法一:直接用整数定义命令
#define LED_ALL_ON 0x1234
#define LED_ALL_OFF 0x5678
方法二:用内核提供的算法
#define _IO(type,nr)
#define _IOR(type,nr,size)
#define _IOW(type,nr,size)
//参数1 ----表示设备标志
//参数2 ----命令编号
//参数3 ----ioctl的第三个参数的类型
例如:
long led_drv_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
int shift = args + 4;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
switch(cmd){
case LED_ALL_ON:
*led_dev->odr |= 0x7 << 5;
break;
case LED_ALL_OFF:
*led_dev->odr &= ~(0x7 << 5);
break;
case LED_NUM_ON:
if(args != 1 && args !=2 && args != 3)
return -EAGAIN;
else
*led_dev->odr |= 0x1 << shift;
break;
case LED_NUM_OFF:
if(args != 1 && args !=2 && args != 3)
return -EAGAIN;
else
*led_dev->odr &= ~(0x1 << shift);
break;
default:
printk("unknow cmd\n");
return -EINVAL;
}
return 0;
}
5. 操作寄存器的方式
5.1 通过直接位运算
c
//将gpio设置为输出模式
*led_dev->mode &= ~(0x3f<<10);
*led_dev->mode |= 0x15 << 10;
5.2 内核提供的函数
c
//向指定的内存地址写数据(寄存器映射的虚拟地址)
static inline void writeb(u8 value, volatile void __iomem *addr) //写1一个字节数据
static inline void writew(u16 value, volatile void __iomem *addr) //写2一个字节数据
static inline void writel(u32 value, volatile void __iomem *addr) //写4一个字节数据
static inline void writeq(u64 value, volatile void __iomem *addr) //写8一个字节数据
//从指定内存中读数据(寄存器映射的虚拟地址)
static inline u8 readb(const volatile void __iomem *addr) //读1一个字节数据
static inline u16 readw(const volatile void __iomem *addr) //读2一个字节数据
static inline u32 readl(const volatile void __iomem *addr) //读4一个字节数据
static inline u64 readq(const volatile void __iomem *addr) //读8一个字节数据
例如:
int led_drv_open(struct inode *inode, struct file *filp)
{
u32 value;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将gpio设置为输出模式
value = readl(led_dev->mode);
value &= ~(0x3f<<10);
value |= 0x15 << 10;
writel(value, led_dev->mode);
return 0;
}
ssize_t led_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *flags)
{
int ret;
int value;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将应用数据转为内核数据
ret = copy_from_user(&value, buf, size);
if(ret > 0){
printk("copy_from_user error\n");
return -EINVAL;
}
//判断应用传递的数据 1---开灯,0 --- 关灯
if(value){
//开灯
//*led_dev->odr |= 0x7 << 5;
writel(readl(led_dev->odr) |0x7 << 5,led_dev->odr);
}else{
//关灯
//*led_dev->odr &= ~(0x7 << 5);
writel(readl(led_dev->odr) & ~(0x7 << 5),led_dev->odr);
}
return size;
}
练习:实现扩展板上的led(温馨提示:需要先打开时钟):

c
#define RCC_AHB4 (0x50000000 + 0xa28)
#define GPIOE_BASE 0x50006000
#define GPIOE_SIZE 24
//在模块加载函数中---映射寄存器地址
led_dev->moder = ioremap(GPIOE_BASE,GPIOE_SIZE);
if(!led_dev->moder){
printk("ioremap error\n");
ret = -ENOMEM;
goto err_misc_deregister;
}
led_dev->otyper = led_dev->moder + 1;
led_dev->odr = led_dev->moder + 5;
//映射时钟寄存器地址
led_dev->rcc_ahb4 = ioremap(RCC_AHB4, 4);
//实现open函数
int led_drv_open(struct inode *inode, struct file *filp)
{
printk("---------- ^_^ %s-------------\n",__FUNCTION__);
//打开时钟
*led_dev->rcc_ahb4 |= 0x1 << 4;
//1,将引脚gpioz_5,6,7设置为输出模式
*led_dev->moder &= ~(0x3<<20);
*led_dev->moder |= 0x1<<20;
//设置开漏输出
//writel(readl(led_dev->otyper) | 0x1<<10,led_dev->otyper);
//推挽输出
writel(readl(led_dev->otyper) & ~(0x1<<10),led_dev->otyper);
return 0;
}
主线流程:
kzalloc初始化设备对象 → 注册miscdevice→ 硬件映射 →open/read/write/ioctl操作 → 模块卸载时释放资源。核心关键步骤:VFS 结构体之间的关系、面向对象封装、寄存器读写、ioctl 编码、错误处理。
工具链与调试:
dmesg,strace,hexdump,devmem, 逻辑分析仪等辅助工具。进阶方向:设备树、LED 子系统、
devm_*资源管理、sysfs属性、自定义poll/fasync、PM 支持。
本篇博客的逻辑链路:从内核对象模型入手,讲解驱动的封装方法 和 应用层调用链条;同时完善资源管理、ioctl 编码、并发同步与卸载流程,希望读者能直接按照文章搭建并运行一个功能完备的 LED 杂项设备驱动,本文提供了完整的实现流程。