Linux驱动基本概念(内核态、用户态、模块、加载、卸载、设备注册、字符设备)
驱动开发基础知识
文章目录
- 驱动开发基础知识
- I 基本概念
- 1 什么是驱动程序
- 2 驱动的作用与功能
- 3 内核空间与用户空间
- II 一个简单的驱动程序
- III 驱动程序的基本组成
- 1 驱动程序的组成
- 2 驱动加载流程
- 3 驱动卸载流程
- 4 设备操作实现
- 5 关键注意事项
- 6 完整生命周期示例
- IV 附录:测试示例驱动的应用程序
- 1 使用示例
- 2 代码
I 基本概念
1 什么是驱动程序
- 设备驱动是操作系统内核中用于管理和控制硬件的特殊软件,它是设备和操作系统之间的桥梁。
- 向上为用户空间提供操作设备的接口(如系统调用或设备文件),向下直接管理硬件,实现设备的读写、配置等功能。
- 驱动可以是内核模块(动态加载/卸载)或编译进内核,运行在特权模式下,负责抽象硬件细节,使应用程序无需关心底层差异。
2 驱动的作用与功能
-
初始化硬件设备、提供设备访问接口、处理设备中断、管理数据传输、实现电源管理功能…
-
核心功能:
- 初始化硬件设备(寄存器配置、资源分配)
- 提供标准设备访问接口(实现file_operations结构体)
- 处理设备中断和DMA传输
- 管理设备电源状态(如休眠唤醒)
- 维护设备状态和数据缓冲区
3 内核空间与用户空间
-
内核空间:
- 驱动程序运行的特权模式,可直接访问硬件和所有内存。
- 使用内核专用函数(如
kmalloc
、printk
),无法使用标准库(如libc
)。 - 代码错误可能导致系统崩溃(如空指针访问)。
- 通过
copy_to_user
/copy_from_user
与用户空间交换数据。 - 遵循GPL协议,调试困难(可用printk/ftrace)
-
用户空间:
- 应用程序运行的受限模式,通过系统调用(如
open
、read
)间接访问硬件。 - 使用标准库(如
glibc
),内存访问受 MMU 保护。 - 进程崩溃通常不影响系统稳定性。
- 开发灵活,可使用各种调试工具
- 应用程序运行的受限模式,通过系统调用(如
-
两者区别:
- 内存隔离:用户空间进程有独立地址空间,内核空间共享同一地址。
- 权限级别:内核代码可执行特权指令(如开关中断),用户代码受限。
- 稳定性要求:内核代码需更严格的错误处理(如资源释放)。
- 开发约束:内核模块需遵循 GPL 协议,调试工具受限(如无 gdb)。
- 执行上下文:内核代码可能运行在中断上下文,不能睡眠
II 一个简单的驱动程序
#include <linux/module.h>#include <linux/fs.h>#include <linux/errno.h>#include <linux/miscdevice.h>#include <linux/kernel.h>#include <linux/major.h>#include <linux/mutex.h>#include <linux/proc_fs.h>#include <linux/seq_file.h>#include <linux/stat.h>#include <linux/init.h>#include <linux/device.h>#include <linux/tty.h>#include <linux/kmod.h>#include <linux/gfp.h>#define DEVICE_NAME "hello_driver"#define SIZE(x,y) (x < y ? x : y) // Return the smaller of two valuesstatic int major = 0; // Major number for the devicestatic char hello_buf[1024]; // Buffer for storing datastatic struct class *hello_class; // Device class structure// Called when device is openedstatic int hello_open(struct inode *node, struct file *file){printk(KERN_INFO "%s : open called !\n", __FUNCTION__);return 0;}// Called when device is closedstatic int hello_close(struct inode *node, struct file *file){printk(KERN_INFO "%s : close called !\n", __FUNCTION__);return 0;}// Called when reading from devicestatic ssize_t hello_read(struct file *file, char __user *buf, size_t size, loff_t *offset){int err ;printk(KERN_INFO "%s : read called !\n", __FUNCTION__);err = copy_to_user(buf, hello_buf, SIZE(1024, size)); // Copy data to user space{if(err != 0){printk(KERN_ERR "%s: copy_to_user error!\n", __FUNCTION__);return -EFAULT; // Return bad address error}}return SIZE(1024, size); // Return number of bytes read}// Called when writing to devicestatic ssize_t hello_write(struct file *file, const char __user *buf, size_t size, loff_t *offset){int err ;printk(KERN_INFO "%s : open called !\n", __FUNCTION__);err = copy_from_user(hello_buf, buf, SIZE(1024, size)); // Copy data from user space{if(err != 0){printk(KERN_ERR "%s: copy_from_user error!\n", __FUNCTION__);return -EFAULT; // Return bad address error}}return SIZE(1024, size); // Return number of bytes written}// File operations structurestatic struct file_operations hello_drv = {.owner = THIS_MODULE, // Owner module.read = hello_read, // Read operation.write = hello_write, // Write operation.open = hello_open, // Open operation.release = hello_close, // Close operation};// Module initialization functionstatic int __init hello_init(void){int err = 0;major = register_chrdev(major, DEVICE_NAME, &hello_drv); // Register character deviceif(major < 0){printk(KERN_ERR "%s: register_chardev error!\n", __FUNCTION__);return major;}hello_class = class_create(THIS_MODULE, "hello_class"); // Create device classPTR_ERR(hello_class);if(IS_ERR(hello_class)){unregister_chrdev(major, DEVICE_NAME); // Cleanup if errorreturn -1;}device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); // Create device nodeprintk(KERN_INFO "registe char dev %s(%d) suucssfully\n",DEVICE_NAME, major);return 0;}// Module exit functionstatic void __exit hello_exit(void){device_destroy(hello_class,MKDEV(major,0)); // Remove device nodeunregister_chrdev(major, DEVICE_NAME); // Unregister character deviceprintk(KERN_INFO "device %s(%d) unregisted\n", DEVICE_NAME, major);}module_init(hello_init);module_exit(hello_exit);MODULE_AUTHOR("fison");MODULE_DESCRIPTION("hello");MODULE_LICENSE("GPL");
III 驱动程序的基本组成
1 驱动程序的组成
Linux 驱动程序通常包含以下核心部分:
- 头文件:包含必要的内核头文件(如
linux/module.h
、linux/fs.h
等)。 - 设备名称与参数:定义设备名称、主设备号、缓冲区等(如 DEVICE_NAME、major、hello_buf)。
- 文件操作结构体(
file_operations
):- 定义设备支持的操作(如 open、read、write、release)。
- 每个操作对应一个回调函数(如
hello_open
、hello_read
)。 - 必须指定
.owner = THIS_MODULE
以防止模块被意外卸载。
- 初始化与退出函数:
module_init(hello_init)
:注册驱动加载时的初始化函数。module_exit(hello_exit)
:注册驱动卸载时的清理函数。- 必须正确处理错误路径的资源释放。
- 模块信息:通过宏声明作者、描述、许可证(如
MODULE_LICENSE("GPL")
)。 - 同步机制:必要时添加互斥锁(
mutex
)或自旋锁(spinlock
)保护共享资源。
2 驱动加载流程
- 注册字符设备:
- 调用
register_chrdev(major, DEVICE_NAME, &hello_drv)
注册设备。 - 内核动态分配主设备号(若
major=0
),或使用指定的主设备号。 - 必须检查返回值,失败时返回错误码。
- 调用
- 创建设备类:
- 使用
class_create(THIS_MODULE, "hello_class")
创建设备类。 - 检查返回值是否有效(
IS_ERR
),失败时需注销已注册的设备。
- 使用
- 创建设备节点:
- 调用
device_create()
在/dev
下生成设备文件(如/dev/hello
)。 - 设备节点权限默认为
0666
,可通过udev
规则修改。
- 调用
- 资源初始化:
- 初始化缓冲区、锁等资源。
- 日志输出:
- 使用
printk(KERN_INFO ...)
打印加载成功信息。 - 错误路径需使用
KERN_ERR
级别日志。
- 使用
3 驱动卸载流程
- 销毁设备节点:
- 调用
device_destroy(hello_class, MKDEV(major, 0))
移除设备文件。
- 调用
- 注销字符设备:
- 调用
unregister_chrdev(major, DEVICE_NAME)
释放主设备号。
- 调用
- 销毁设备类:
- 调用
class_destroy(hello_class)
清理设备类。
- 调用
- 日志输出:
- 使用
printk(KERN_INFO ...)
打印卸载成功信息。
- 使用
4 设备操作实现
open
:- 设备打开时调用,通常进行初始化或资源分配。
- 示例中仅打印日志(
printk(KERN_INFO "%s : open called !\n", __FUNCTION__)
)。
read
:- 使用
copy_to_user(buf, hello_buf, SIZE(1024, size))
将内核缓冲区数据复制到用户空间。 - 检查返回值(
if(err != 0)
),错误时返回-EFAULT
。 - 返回实际读取的字节数(
SIZE(1024, size)
)。
- 使用
write
:- 使用
copy_from_user(hello_buf, buf, SIZE(1024, size))
将用户数据复制到内核缓冲区。 - 类似地处理错误(
-EFAULT
)。 - 返回实际写入的字节数。
- 使用
release
:- 设备关闭时调用,通常释放资源。
- 示例中仅返回0。
5 关键注意事项
- 错误处理:
- 所有内核函数调用必须检查返回值。
- 初始化失败时需反向释放已申请的资源。
- 并发控制:
- 若多进程访问设备,需添加互斥锁(
mutex
)保护共享资源(如hello_buf
)。
- 若多进程访问设备,需添加互斥锁(
- 缓冲区安全:
- 确保
copy_to/from_user
的size
不超过缓冲区大小(示例中通过SIZE(1024, size)
限制)。
- 确保
- 日志级别:
- 使用
KERN_INFO
记录正常操作,KERN_ERR
记录错误。
- 使用
6 完整生命周期示例
加载: insmod hello_driver.ko → hello_init() → register_chrdev() → class_create() → device_create()使用: 用户程序通过 /dev/hello 调用 open/read/write/release卸载: rmmod hello_driver → hello_exit() → device_destroy() → unregister_chrdev() → class_destroy()
IV 附录:测试示例驱动的应用程序
1 使用示例
(1)写入数据到设备hello
./main -w "the message you want to write to the device hello"
(2)从设备hello中读取数据
./main -r
2 代码
/** Simple program to interact with a character device "/dev/hello"* Supports reading from and writing to the device*/#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <stdio.h>#include <string.h>int main(int argc, char *argv[]){// Check for minimum required argumentsif(argc < 2){printf("Usage:%s <-r(read)/-w(write)> <string>\n", argv[0]);return -1;}// Open the device file with read/write permissionsint fd = open("/dev/hello", O_RDWR);if(fd == -1){perror("open");return -1;}char buf[1024]; // Buffer for read operationsint len; // Length variable for read/write operations// Handle read operationif(strcmp(argv[1], "-r") == 0){len = read(fd, buf, 1024);if(len < 0){perror("read");close(fd);return -1;}buf[1023] = '\0'; // Ensure null terminationprintf("read %d bytes from device hello:%s\n", len, buf);}// Handle write operationif(strcmp(argv[1], "-w") == 0){if(argv[2] == NULL){printf("nothing to write\n");close(fd);return -1;}// Calculate write length (including null terminator)len = strlen(argv[2]) + 1;len = len < 1024 ? len : 1024; // Cap at buffer sizelen = write(fd, argv[2], len);if(len < 0 ){perror("write");close(fd);return -1;} printf("write %d bytes to device hello\n", len);}close(fd); // Clean up file descriptorreturn 0;}len = write(fd, argv[2], len);if(len < 0 ){perror("write");close(fd);return -1;} printf("write %d bytes to device hello\n", len);}close(fd); // Clean up file descriptorreturn 0;}