1. Linux驱动层次在嵌入式Linux开发中驱动是连接硬件与用户空间的桥梁。理解驱动的层次结构是学习字符设备驱动的基础。Linux驱动整体可分为三个层次应用层用户空间的应用程序通过标准系统调用如 open、read、write、ioctl访问设备。内核层包括虚拟文件系统VFS、字符设备驱动、块设备驱动、网络设备驱动等负责具体硬件操作。硬件层实际的物理设备如 LED、按键、传感器、串口等。应用层通过系统调用进入内核VFS 根据设备类型将请求分发到对应的驱动接口驱动最终操作硬件寄存器完成数据读写。字符设备驱动位于内核层是嵌入式开发中最常见、最基础的驱动类型。2. 设备号相关知识2.1 设备号定义在Linux中每个设备在内核中都通过一个唯一的设备号来标识。设备号是一个dev_t类型的无符号32位整数由两部分组成主设备号和次设备号。主设备号major高12位用于标识设备对应的驱动程序即区分设备类型。次设备号minor低20位用于标识同一个驱动程序管理下的不同设备实例。例如一个系统中有两个串口设备它们共用同一个主设备号对应同一个串口驱动但次设备号不同分别为0和1从而区分不同的串口。2.2 主设备号与次设备号主设备号决定了设备由哪个驱动程序处理次设备号则用于区分同一驱动下的多个设备。内核通过dev_t类型统一管理设备号并提供一组宏来拆分和组合主次设备号。设备号在内核中具有全局唯一性申请设备号时需要避免与已有设备号冲突。常见的设备号分配方式有两种静态分配手动指定和动态分配由内核自动分配。2.3 设备号的数据结构与操作设备号在内核中使用dev_t类型表示定义在linux/types.h中。它是一个32位无符号整数其中高12位为主设备号低20位为次设备号。typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t;内核在linux/kdev_t.h提供了一组宏用于操作设备号开发者无需关心具体的位运算细节。MAJOR(dev)从dev_t中提取主设备号。MINOR(dev)从dev_t中提取次设备号。MKDEV(major, minor)由主设备号和次设备号组合生成dev_t设备号。#define MINORBITS 20 #define MINORMASK ((1U MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) MINORMASK)) #define MKDEV(ma, mi) (((ma) MINORBITS) | (mi))使用示例dev_t devno MKDEV(200, 0); // 主设备号200次设备号0 int major MAJOR(devno); // major 200 int minor MINOR(devno); // minor 03. 设备号相关函数3.1 静态申请静态申请是指开发者手动指定主设备号调用register_chrdev_region函数向内核申请设备号。这种方式适用于已知设备号未被占用的情况常用于教学或固定设备号的场景。int register_chrdev_region(dev_t first, unsigned int count, const char *name);first要申请的设备号起始值通常使用MKDEV(major, minor)生成。count连续申请的设备号个数。name设备名称显示在/proc/devices中。返回值成功返回0失败返回负的错误码。示例dev_t devno MKDEV(200, 0); int ret register_chrdev_region(devno, 1, my_char_dev); if (ret 0) { printk(register_chrdev_region failed\n); return ret; }3.2 动态申请动态申请由内核自动分配一个可用的主设备号开发者无需关心设备号是否冲突。使用alloc_chrdev_region函数适用于大多数实际项目。int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);dev输出参数内核分配的设备号。firstminor起始次设备号通常为0。count申请的设备号个数。name设备名称。返回值成功返回0失败返回负的错误码。示例dev_t devno; int ret alloc_chrdev_region(devno, 0, 1, my_char_dev); if (ret 0) { printk(alloc_chrdev_region failed\n); return ret; } int major MAJOR(devno); // 获取自动分配的主设备号3.3 设备号释放设备卸载时必须释放已申请的设备号使用unregister_chrdev_region函数。void unregister_chrdev_region(dev_t first, unsigned int count);first要释放的设备号起始值通常使用MKDEV(major, minor)生成与申请时的起始设备号保持一致。count要释放的设备号个数必须与申请时的个数一致。返回值无返回值void。示例unregister_chrdev_region(devno, 1);设备号释放通常在驱动卸载函数exit中调用与申请函数成对出现避免资源泄漏。4. struct cdev 字符设备在Linux内核中字符设备使用struct cdev结构体表示定义在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内核对象用于设备模型管理一般由内核自动维护开发者无需直接操作。owner指向所属模块通常设置为THIS_MODULE用于模块引用计数管理。ops指向struct file_operations即设备操作函数集用户空间的系统调用最终映射到这些函数。list链表节点用于将cdev挂接到内核的设备链表中由内核维护。dev设备号类型为dev_t标识该字符设备在内核中的唯一编号。count设备号个数表示该cdev管理的连续设备号数量通常为1。开发者通常将struct cdev嵌入到自定义的设备结构体中便于管理设备私有数据。5. 字符设备操作函数内核提供了一组函数用于初始化和注册struct cdev下面逐一说明每个函数的原型、参数含义和返回值。cdev_init —— 初始化 cdev 结构体void cdev_init(struct cdev *cdev, const struct file_operations *fops);cdev指向要初始化的struct cdev结构体指针。fops指向struct file_operations操作函数集的指针将设备操作函数绑定到该cdev。返回值无返回值void。cdev_init用于初始化一个静态分配的cdev结构体主要完成两件事将ops字段指向传入的fops并将owner字段设置为THIS_MODULE。初始化后仍需手动设置owner字段或依赖默认值。cdev_add —— 注册字符设备int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);cdev指向已初始化或动态分配的struct cdev结构体指针。dev设备号类型为dev_t通常由MKDEV(major, minor)生成或由alloc_chrdev_region动态分配。count该设备管理的连续设备号个数通常为1。返回值成功返回0失败返回负的错误码。cdev_add将cdev注册到内核并把设备号dev与cdev关联起来。调用成功后用户空间即可通过设备节点访问该设备。若注册失败需要释放已申请的设备号。cdev_del —— 注销字符设备void cdev_del(struct cdev *cdev);cdev指向要注销的struct cdev结构体指针。返回值无返回值void。cdev_del从内核中移除该字符设备通常在驱动卸载函数exit中调用。调用后设备节点将无法再访问但设备号仍需通过unregister_chrdev_region单独释放。cdev_alloc —— 动态分配 cdev 结构体struct cdev *cdev_alloc(void);参数无。返回值成功返回指向新分配的struct cdev指针失败返回NULL。cdev_alloc在堆上动态分配一个struct cdev结构体并返回指针适用于需要动态创建设备的场景。与cdev_init不同cdev_alloc不需要预先定义静态的cdev变量而是由内核通过kzalloc在堆上分配内存因此更适合在设备数量不确定或需要运行时动态创建多个字符设备的场景。使用cdev_alloc时需要注意以下几点分配成功后仍需调用cdev_init或手动设置ops和owner字段将操作函数集绑定到该cdev。注册设备时调用cdev_add将设备号与动态分配的cdev关联。卸载设备时先调用cdev_del注销设备再调用kfree释放动态分配的内存避免内存泄漏。示例struct cdev *p_cdev; /* 动态分配 cdev 结构体 */ p_cdev cdev_alloc(); if (p_cdev NULL) { printk(cdev_alloc failed\n); return -ENOMEM; } /* 绑定操作函数集 */ p_cdev-ops my_fops; p_cdev-owner THIS_MODULE; /* 注册字符设备 */ ret cdev_add(p_cdev, devno, 1); if (ret 0) { printk(cdev_add failed\n); kfree(p_cdev); unregister_chrdev_region(devno, 1); return ret; }在模块卸载时需要成对调用注销与释放函数cdev_del(p_cdev); kfree(p_cdev); unregister_chrdev_region(devno, 1);cdev_put —— 释放 cdev 结构体引用void cdev_put(struct cdev *p);p指向要释放引用的struct cdev结构体指针。返回值无返回值void。cdev_put用于释放对struct cdev的引用计数通常在动态分配cdev且不再需要该设备时调用。与cdev_del不同cdev_del负责从内核中注销设备而cdev_put负责释放引用计数二者需要配合使用。使用cdev_put时需要注意以下几点只有通过cdev_alloc动态分配的cdev才需要调用cdev_put释放引用静态分配的cdev无需调用。调用cdev_put后不能再访问该cdev结构体否则可能导致悬空指针问题。在模块卸载时应先调用cdev_del注销设备再调用cdev_put释放引用最后调用kfree释放内存。示例/* 注销设备 */ cdev_del(p_cdev); /* 释放 cdev 引用 */ cdev_put(p_cdev); /* 释放动态分配的内存 */ kfree(p_cdev); unregister_chrdev_region(devno, 1);下面给出一个完整的初始化与注册示例struct cdev my_cdev; cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; int ret cdev_add(my_cdev, devno, 1); if (ret 0) { printk(cdev_add failed\n); unregister_chrdev_region(devno, 1); return ret; }在模块卸载时需要成对调用注销函数cdev_del(my_cdev); unregister_chrdev_region(devno, 1);6. 字符设备驱动核心接口struct file_operations是字符设备驱动的核心接口定义在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 *); 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 *, 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 (*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 **); };常用成员说明open打开设备时调用通常用于初始化硬件或分配资源。release关闭设备时调用用于释放资源。read从设备读取数据。write向设备写入数据。unlocked_ioctl设备控制命令处理函数。llseek文件读写位置偏移。用户空间的open、read、write、close、ioctl系统调用会通过 VFS 最终调用到file_operations中对应的函数。7. 字符设备驱动实现编写一个完整的字符设备驱动通常遵循以下流程定义设备结构体包含struct cdev和私有数据。实现file_operations中的操作函数open、read、write、release、ioctl 等。在模块入口函数中申请设备号、初始化cdev、注册设备。在模块出口函数中注销设备、删除cdev、释放设备号。编写 Makefile编译并加载模块创建设备节点。创建设备节点是驱动加载后、用户空间访问设备前的必要步骤。常用的创建设备节点方式有两种手动创建mknod和自动创建借助设备模型。方式一手动创建mknod手动创建是最直接的方式通过mknod命令在/dev目录下建立设备节点需要指定设备类型、主设备号和次设备号。sudo mknod /dev/my_char_dev c major 0c表示字符设备character device。major主设备号可通过cat /proc/devices | grep my_char_dev查询。0次设备号通常从 0 开始。手动创建方式简单直接但需要开发者手动查询设备号并执行命令且设备节点不会随驱动卸载而自动删除需要手动清理。方式二自动创建借助设备模型自动创建方式利用 Linux 设备模型在驱动中通过class_create和device_create接口让内核在驱动加载时自动在/dev目录下创建设备节点驱动卸载时自动删除无需手动干预。class_create —— 创建设备类struct class *class_create(struct module *owner, const char *name);owner指向所属模块通常设置为THIS_MODULE用于模块引用计数管理。name设备类的名称会在/sys/class目录下创建对应的类目录。返回值成功返回指向struct class的指针失败返回错误指针需用IS_ERR判断。class_create用于在内核中创建一个设备类该类是设备模型中的一个节点用于组织和管理同类型的设备。创建成功后会在/sys/class下生成一个以name命名的目录后续通过device_create创建的设备节点都会挂载到该类下。device_create —— 创建设备节点struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);cls指向class_create创建的设备类指针。parent父设备指针通常设置为NULL表示无父设备。devt设备号类型为dev_t由MKDEV生成或alloc_chrdev_region动态分配。drvdata设备私有数据指针通常设置为NULL。fmt设备节点名称的格式化字符串支持可变参数如my_char_dev或my_char_dev%d。返回值成功返回指向struct device的指针失败返回错误指针需用IS_ERR判断。device_create会在/dev目录下自动创建设备节点并将该设备挂载到cls指定的类下。内核会根据devt中的主设备号和次设备号自动生成对应的设备节点无需手动执行mknod命令。使用class_create和device_create时需要注意以下几点调用class_create后必须检查返回值使用IS_ERR和PTR_ERR处理错误避免空指针访问。device_create的fmt参数支持格式化字符串可以动态生成设备节点名称便于管理多个同类型设备。在模块卸载时必须先调用device_destroy销毁设备节点再调用class_destroy销毁设备类顺序不能颠倒。设备节点名称不能与/dev下已有节点冲突否则创建会失败。static struct class *my_class; /* 在模块入口函数中 */ my_class class_create(THIS_MODULE, my_char_class); if (IS_ERR(my_class)) { printk(class_create failed\n); return PTR_ERR(my_class); } device_create(my_class, NULL, devno, NULL, my_char_dev); /* 在模块出口函数中 */ device_destroy(my_class, devno); class_destroy(my_class);自动创建方式需要包含linux/device.h头文件并在驱动中维护class和device的生命周期。这种方式更适合实际项目设备节点随驱动加载自动出现、卸载自动消失用户体验更好。两种方式的对比总结如下手动创建操作简单适合调试和教学场景需要手动查询设备号节点不会自动清理。自动创建驱动加载自动生成节点、卸载自动删除适合实际项目需要在驱动中额外实现class_create和device_create逻辑。7.1 完整示例下面给出一个完整的字符设备驱动示例实现简单的数据读写功能。#include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEVICE_NAME my_char_dev #define BUF_SIZE 128 static int major 0; static dev_t devno; static struct cdev my_cdev; static char kernel_buf[BUF_SIZE]; static int my_open(struct inode *inode, struct file *filp) { printk(my_open called\n); return 0; } static int my_release(struct inode *inode, struct file *filp) { printk(my_release called\n); return 0; } static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { int ret; size_t len count BUF_SIZE ? count : BUF_SIZE; ret copy_to_user(buf, kernel_buf, len); if (ret 0) { printk(copy_to_user failed\n); return ret; } return len; } static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { int ret; size_t len count BUF_SIZE ? count : BUF_SIZE; ret copy_from_user(kernel_buf, buf, len); if (ret 0) { printk(copy_from_user failed\n); return ret; } return len; } static struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .release my_release, .read my_read, .write my_write, }; static int __init my_init(void) { int ret; /* 1. 动态申请设备号 */ ret alloc_chrdev_region(devno, 0, 1, DEVICE_NAME); if (ret 0) { printk(alloc_chrdev_region failed\n); return ret; } major MAJOR(devno); printk(alloc device number: major%d minor%d\n, major, MINOR(devno)); /* 2. 初始化 cdev 并注册 */ cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, devno, 1); if (ret 0) { printk(cdev_add failed\n); unregister_chrdev_region(devno, 1); return ret; } printk(my_char_dev driver init success\n); return 0; } static void __exit my_exit(void) { /* 1. 注销设备 / cdev_del(my_cdev); / 2. 释放设备号 */ unregister_chrdev_region(devno, 1); printk(my_char_dev driver exit\n); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple char device driver);7.2 Makefile 示例obj-m : my_char_dev.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean7.3 加载与测试# 编译模块 make 加载模块 sudo insmod my_char_dev.ko 查看设备号 cat /proc/devices | grep my_char_dev 创建设备节点major 替换为实际主设备号 sudo mknod /dev/my_char_dev c major 0 测试读写 echo hello /dev/my_char_dev cat /dev/my_char_dev 卸载模块 sudo rmmod my_char_dev8. 元数据结构体说明8.1 struct inodestruct inode是内核中描述文件元数据的结构体定义在linux/fs.h中。每个文件包括设备文件在内核中都有一个对应的inode用于保存文件的属性信息如文件类型、权限、大小、时间戳等。对于设备文件inode中保存了设备号信息通过i_rdev字段可以获取设备号。当用户空间打开设备节点时VFS 会找到对应的inode并从中取出设备号进而定位到对应的字符设备驱动。struct inode中与设备驱动开发密切相关的常用字段如下i_rdev设备号类型为dev_t用于标识该文件对应的设备。i_cdev指向struct cdev的指针用于字符设备驱动中建立inode与cdev的关联。i_mode文件类型与权限位用于区分普通文件、目录、设备文件等。i_uid/i_gid文件所有者的用户 ID 和组 ID。i_size文件大小对于设备文件通常为 0。i_atime/i_mtime/i_ctime访问时间、修改时间和状态变更时间。在字符设备驱动的open函数中通常通过container_of宏从inode-i_cdev获取自定义设备结构体从而访问设备私有数据。示例代码如下static int my_open(struct inode *inode, struct file *filp) { struct my_dev *dev; /* 通过 inode 获取 cdev再通过 container_of 获取自定义设备结构体 */ dev container_of(inode-i_cdev, struct my_dev, cdev); filp-private_data dev; return 0; }8.2 struct filestruct file是内核中描述一个已打开文件实例的结构体定义在linux/fs.h中。每当用户空间调用open打开一个文件或设备节点时内核就会创建一个新的struct file实例用于保存该次打开的状态信息。同一个文件可以被多次打开每次打开都会对应一个独立的struct file但它们共享同一个inode。struct file中与设备驱动开发密切相关的常用字段如下f_mode文件打开模式包含读、写、追加等标志用于判断设备是否以只读或只写方式打开。f_flags文件状态标志如O_NONBLOCK、O_APPEND等驱动可通过该字段判断是否支持非阻塞操作。f_pos当前读写位置偏移量类型为loff_t用于记录文件读写的当前位置。f_op指向struct file_operations的指针即该文件对应的操作函数集。private_data私有数据指针驱动可在open函数中将其指向自定义设备结构体供后续read、write等函数使用。f_count引用计数表示该文件实例被引用的次数内核通过它管理文件对象的生命周期。在驱动开发中private_data是最常用的字段之一。通常在open函数中将自定义设备结构体赋值给filp-private_data在read、write、release等函数中再取出使用从而避免使用全局变量提高代码的可维护性。示例代码如下static int my_open(struct inode *inode, struct file *filp) { struct my_dev *dev; dev container_of(inode-i_cdev, struct my_dev, cdev); filp-private_data dev; /* 保存设备私有数据 */ return 0; } static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct my_dev *dev filp-private_data; /* 取出设备私有数据 */ /* 使用 dev 进行数据读取操作 */ return 0; }8.3 联系与区别struct inode与struct file是内核文件系统中两个既相互关联又职责不同的核心结构体理解它们的区别对驱动开发至关重要。生命周期不同inode在文件创建时产生在文件被删除时销毁生命周期与文件本身一致file在每次open时创建在close时销毁生命周期与一次打开操作一致。对应关系不同一个inode可以对应多个file同一文件被多次打开一个file只能对应一个inode。保存内容不同inode保存文件的静态属性类型、权限、设备号等file保存一次打开操作的动态状态读写位置、打开标志、私有数据等。驱动中的角色不同inode主要用于在open时获取设备号和cdevfile则贯穿read、write、release等整个操作过程用于传递设备私有数据。在字符设备驱动中open函数同时接收inode和file两个参数正是为了完成从inode获取设备信息、再通过file保存设备私有数据的衔接过程。理解这两个结构体的分工是编写高质量字符设备驱动的基础。9. 用户/内核空间数据交换在字符设备驱动中用户空间的应用程序与内核空间的驱动之间经常需要交换数据。由于用户空间和内核空间使用不同的地址空间内核不能直接访问用户空间的指针必须通过内核提供的专用函数来完成数据的拷贝。常用的数据交换函数有四个copy_to_user、copy_from_user、get_user和put_user。copy_to_user —— 从内核空间拷贝数据到用户空间unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);to目标地址即用户空间的缓冲区指针类型为void __user *。from源地址即内核空间的缓冲区指针类型为const void *。n要拷贝的字节数。返回值成功返回0失败返回未拷贝完成的字节数即剩余未拷贝的字节数。copy_to_user用于将内核空间的数据拷贝到用户空间通常在驱动的read函数中调用把设备读取到的数据返回给用户程序。该函数内部会检查用户空间地址的合法性并在拷贝前进行必要的安全检查避免内核因非法地址而崩溃。示例static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { int ret; size_t len count BUF_SIZE ? count : BUF_SIZE; /* 将内核缓冲区 kernel_buf 中的数据拷贝到用户空间 buf */ ret copy_to_user(buf, kernel_buf, len); if (ret ! 0) { printk(copy_to_user failed, %d bytes not copied\n, ret); return -EFAULT; } return len; }copy_from_user —— 从用户空间拷贝数据到内核空间unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);to目标地址即内核空间的缓冲区指针类型为void *。from源地址即用户空间的缓冲区指针类型为const void __user *。n要拷贝的字节数。返回值成功返回0失败返回未拷贝完成的字节数。copy_from_user用于将用户空间的数据拷贝到内核空间通常在驱动的write函数中调用把用户程序写入的数据保存到内核缓冲区。与copy_to_user一样该函数也会对用户空间地址进行合法性检查。示例static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { int ret; size_t len count BUF_SIZE ? count : BUF_SIZE; /* 将用户空间 buf 中的数据拷贝到内核缓冲区 kernel_buf */ ret copy_from_user(kernel_buf, buf, len); if (ret ! 0) { printk(copy_from_user failed, %d bytes not copied\n, ret); return -EFAULT; } return len; }get_user —— 从用户空间读取单个数据int get_user(type *val, const void __user *addr);val指向内核空间变量的指针用于保存从用户空间读取到的数据。addr用户空间的源地址指针。返回值成功返回0失败返回负的错误码如-EFAULT。get_user用于从用户空间读取一个简单的数据类型如int、char、long等适用于读取单个变量或少量数据的场景。与copy_from_user相比get_user更轻量适合读取固定大小的标量数据常用于ioctl命令中读取用户传入的参数。示例int value; int ret; /* 从用户空间读取一个 int 类型的数据 */ ret get_user(value, (int __user *)arg); if (ret ! 0) { printk(get_user failed\n); return ret; } printk(read value from user: %d\n, value);put_user —— 向用户空间写入单个数据int put_user(type val, void __user *addr);val要写入用户空间的数据值类型为标量类型如int、char、long等。addr用户空间的目标地址指针。返回值成功返回0失败返回负的错误码如-EFAULT。put_user用于向用户空间写入一个简单的数据类型适用于返回单个变量或少量数据的场景。与copy_to_user相比put_user更轻量适合返回固定大小的标量数据常用于ioctl命令中向用户返回结果。示例int result 100; int ret; /* 将内核中的 result 值写入用户空间 */ ret put_user(result, (int __user *)arg); if (ret ! 0) { printk(put_user failed\n); return ret; }四个函数的对比总结如下copy_to_user批量拷贝从内核到用户适合传输大量数据用于read函数。copy_from_user批量拷贝从用户到内核适合传输大量数据用于write函数。get_user单值读取从用户到内核适合读取单个标量常用于ioctl。put_user单值写入从内核到用户适合返回单个标量常用于ioctl。在使用这四个函数时需要注意以下几点所有涉及用户空间地址的指针都必须使用__user标记便于内核工具检查地址空间的正确性。拷贝前应检查用户传入的缓冲区大小避免越界访问导致内核崩溃。批量拷贝函数返回的是未拷贝的字节数判断失败时应检查返回值是否为0而不是判断返回值是否小于0。单值读写函数返回的是错误码判断失败时应检查返回值是否为0。在ioctl中传递结构体等复杂数据时应使用copy_to_user和copy_from_user而不是get_user和put_user。10. 总结本文系统梳理了Linux平台下Linux 字符设备驱动开发的核心知识从驱动层次、设备号管理、struct cdev结构体到file_operations接口、驱动实现流程、元数据结构体以及用户/内核空间数据交换形成了完整的知识闭环。回顾全文需要重点掌握以下几个关键点驱动层次字符设备驱动位于内核层通过 VFS 承接用户空间系统调用最终操作硬件寄存器。设备号管理dev_t由主设备号高12位和次设备号低20位组成静态申请用register_chrdev_region动态申请用alloc_chrdev_region卸载时必须用unregister_chrdev_region成对释放。cdev 生命周期cdev_init初始化、cdev_add注册、cdev_del注销动态分配的cdev还需配合cdev_put和kfree释放资源。核心接口struct file_operations是驱动与用户空间交互的桥梁open、read、write、release、unlocked_ioctl是最常用的成员。设备节点手动mknod适合调试借助class_create和device_create自动创建更适合实际项目。数据交换批量数据用copy_to_user/copy_from_user单值数据用get_user/put_user务必检查返回值并正确使用__user标记。在实际开发中建议优先采用动态申请设备号 自动创建设备节点的方式并严格遵循「申请与释放成对、注册与注销成对」的原则避免资源泄漏。理解inode与file的分工以及private_data的传递机制是写出结构清晰、可维护性强的字符设备驱动的关键。掌握以上内容后即可在此基础上进一步学习阻塞与非阻塞 IO、中断处理、并发与同步等进阶主题。