【Linux 驱动开发】三. 应用程序调用驱动过程分析

三. 应用程序调用驱动过程分析

  • [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. 操作寄存器的方式)

文章摘要:本文分析了 Linux应用程序调用驱动 的完整链路,重点介绍了内核中的关键数据结构及其交互关系。主要内容包括:1) 使用kzalloc进行设备对象内存分配时的注意事项;2) struct filestruct inodestruct 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;

补充要点:

  1. GFP 标志位选择
    • GFP_KERNEL 只能在进程上下文、且允许睡眠的场景使用。如果是在中断或原子上下文,需要使用 GFP_ATOMIC
  2. 错误路径与资源回收
    • 申请到的对象在模块卸载或错误路径中必须对应 kfree(led_dev) 释放。若使用 devm_kzalloc()(设备模型自动管理),则在 removecleanup 时可自动释放。
  3. 面向对象设计
    • 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_cdevi_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_cdevstruct 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;

若采用 miscdevicemisc_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

图示要点:

  1. 应用层调用 open 时,VFS 会根据路径找到对应 struct inode,并依据 inode->i_rdev 查询出已经注册的 struct cdev
  2. VFS 通过 inode->i_fop 把驱动提供的 file_operations 绑定到新创建的 struct file 对象上。
  3. .open() 常用于完成对象化封装(把驱动中的设备实例地址写入 filp->private_data),后续 .read/.write/.ioctl 直接从 private_data 里取回该实例,从而访问硬件寄存器。

图 2:cdevfile_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

图示要点:

  1. 初始化阶段 通过 kzalloccdev_initcdev_add(或 misc_register)把设备对象注册到内核字符设备框架中。
  2. 每一个 cdev 节点记录主次设备号及指向 file_operations 的指针,VFS 通过 dev_t 快速定位。
  3. 运行阶段 open 产生的 struct file 保存 f_opprivate_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 杂项设备驱动,本文提供了完整的实现流程。

相关推荐
yangSnowy2 小时前
Linux实用命令分析nginx系统日志文件
linux·运维·服务器
chengpei1472 小时前
Arduino环境下开发STM32
stm32·单片机·嵌入式硬件
三佛科技-134163842122 小时前
100V8A_HN0801雾化器加湿器MOS管关键特性
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
Jia ming2 小时前
ARM多核处理器缓存一致性全解析
arm开发·缓存
无级程序员2 小时前
clickhouse创建用户,登录出错的问题,code 516
linux·服务器·clickhouse
wkd_0072 小时前
【交叉编译 | arm版Ubuntu】arm版Ubuntu(飞腾平台)开发环境、交叉编译工具安装
linux·arm开发·ubuntu·aarch64-linux·arm交叉编译工具
菜鸟厚非3 小时前
如何在 Nginx 中配置 HTTPS - Linux
linux·nginx·https
biter00883 小时前
Ubuntu 上搜狗输入法突然“消失 / 只能英文”的排查与修复教程
linux·数据库·ubuntu
Embers(余烬矿)3 小时前
STM32 usb 设备描述符失败
stm32·单片机·嵌入式硬件