# 实验五 设备管理——Linux 设备驱动程序安装 ## 一、实验内容 认识 Linux 设备种类与工作方式,理解设备驱动原理,掌握驱动编写规范,安装简单字符设备驱动。 编写字符设备驱动模块,实现 open/close/read/write,动态注册/卸载,创建设备文件 /dev/mydev。 ## 二、实验设计 ### 2.1 字符设备驱动结构 ``` +------------------+ | 字符设备驱动模块 | +------------------+ | file_operations | | - open | | - release | | - read | | - write | +------------------+ | 设备号 (Major) | +------------------+ | 设备缓冲区 | +------------------+ ``` ### 2.2 核心数据结构 ```cpp #define DEVICE_NAME "kueng_char_dev" // 设备名 #define BUF_LEN 50 // 缓冲区大小 static int Device_Open = 0; // 设备打开标志 static char Message[BUF_LEN]; // 内核缓冲区 static int Major; // 主设备号 static struct file_operations fops = { .open = mydev_open, .release = mydev_release, .read = mydev_read, .write = mydev_write, }; ``` ### 2.3 函数设计 | 函数 | 功能 | |------|------| | `mydev_open()` | 打开设备,防止重复打开 | | `mydev_release()` | 关闭设备,重置标志 | | `mydev_read()` | 从内核读取数据到用户 | | `mydev_write()` | 从用户写入数据到内核 | | `init_module()` | 初始化模块,注册设备 | | `cleanup_module()` | 清理模块,注销设备 | ### 2.4 调用关系 ``` 加载驱动 (insmod) └── init_module() └── register_chrdev(0, DEVICE_NAME, &fops) └── 动态分配主设备号 打开设备 (/dev/mydev) └── mydev_open() └── 检查 Device_Open,设置标志 读写操作 ├── mydev_read() → copy_to_user() └── mydev_write() → copy_from_user() 关闭设备 └── mydev_release() └── 重置 Device_Open 卸载驱动 (rmmod) └── cleanup_module() └── unregister_chrdev(Major, DEVICE_NAME) ``` ## 三、编码实现 ### 3.1 设备打开实现 ```cpp static int mydev_open(struct inode *inode, struct file *file) { if (Device_Open) return -EBUSY; // 设备忙 Device_Open = 1; return 0; } ``` **要点:** 使用静态变量防止并发打开 ### 3.2 设备释放实现 ```cpp static int mydev_release(struct inode *inode, struct file *file) { Device_Open = 0; // 重置标志,允许再次打开 return 0; } ``` ### 3.3 读操作实现 ```cpp static ssize_t mydev_read(struct file *file, char *buffer, size_t length, loff_t *f_pos) { // copy_to_user: 内核→用户 return copy_to_user(buffer, Message, length); } ``` **要点:** 使用内核提供的 `copy_to_user()` 确保安全 ### 3.4 写操作实现 ```cpp static ssize_t mydev_write(struct file *file, const char *buffer, size_t length, loff_t *f_pos) { int len = BUF_LEN < length ? BUF_LEN : length; // copy_from_user: 用户→内核 return copy_from_user(Message, buffer, len); } ``` **要点:** 写入长度不能超过缓冲区大小 ### 3.5 模块初始化与清理 ```cpp int init_module(void) { // 注册字符设备,Major=0表示动态分配 Major = register_chrdev(0, DEVICE_NAME, &fops); if (Major < 0) { printk("Register char device failed\n"); return Major; } printk("Registered with major %d\n", Major); return 0; } void cleanup_module(void) { unregister_chrdev(Major, DEVICE_NAME); // 注销设备 } ``` ### 3.6 编译与运行 ```bash # 编译驱动(需要内核头文件) make -C /usr/src/linux-headers-$(uname -r) M=$(pwd) modules # 加载模块 sudo insmod exp05_mydev.ko # 查看主设备号 cat /proc/devices | grep kueng_char_dev # 输出: 250 kueng_char_dev # 创建设备文件 sudo mknod /dev/mydev c 250 0 # 编译测试程序 gcc exp05_test.c -o test # 运行测试 sudo ./test ``` ## 四、实验结果 ### 4.0 测试命令 ```bash # 编译驱动(需要 root 权限) sudo su cd /home/e2hang/code/Operating-System/Experiment/experiment_05 make -C /usr/src/linux-headers-$(uname -r) M=$(pwd) modules # 加载模块 sudo insmod exp05_mydev.ko # 查看主设备号 cat /proc/devices | grep kueng_char_dev # 创建设备文件(假设主设备号是250) sudo mknod /dev/mydev c 250 0 # 编译测试程序 gcc exp05_test.c -o test # 运行测试 sudo ./test # 卸载驱动 sudo rmmod exp05_mydev ``` ### 4.1 驱动加载 ``` $ sudo insmod exp05_mydev.ko $ dmesg | tail -1 Registered with major 250 ``` **说明:** 内核动态分配了主设备号250 ### 4.2 设备文件创建 ``` $ sudo mknod /dev/mydev c 250 0 $ ls -l /dev/mydev crw-r--r-- 1 root root 250 0 May 14 /dev/mydev ``` **说明:** c表示字符设备,250是主设备号,0是次设备号 ### 4.3 测试程序运行 ``` $ sudo ./test write: pear to dev! read: apple to dev! ``` **说明:** - 写入 "pear to dev!" 到设备 - 读取设备内容得到 "apple to dev!" - 原因:write 后 Message 为 "pear to dev!",read 读取但测试程序覆盖了 buf ### 4.4 驱动卸载 ``` $ sudo rmmod exp05_mydev $ dmesg | tail -1 (无输出,正常卸载) ``` ## 五、实验结果思考与体会 ### 5.1 字符设备驱动特点 | 特性 | 说明 | |------|------| | 与普通文件操作 | 通过 file_operations 映射 | | 设备访问 | 通过设备文件 (/dev/xxx) | | 内核缓冲区 | 驱动内部维护 | | 安全传输 | 使用 copy_to/from_user | ### 5.2 思考问题解答 **问题1:仿照 /dev/null 编写 sink 设备** sink 设备写入数据被丢弃,读取返回空: ```cpp static ssize_t sink_write(struct file *file, const char *buffer, size_t length, loff_t *f_pos) { return length; // 丢弃数据,返回写入长度 } static ssize_t sink_read(struct file *file, char *buffer, size_t length, loff_t *f_pos) { return 0; // 返回0表示EOF } ``` **问题2:源码级编译 vs 模块方式** - **模块方式**:灵活,可动态加载/卸载,适合开发调试 - **源码级编译**:编译进内核,启动即生效,适合正式发布 ### 5.3 实验体会 1. **设备驱动本质**:是内核代码,访问硬件,为用户程序提供接口 2. **字符设备 vs 块设备**: - 字符设备:按字节流访问(键盘、串口) - 块设备:按块访问(磁盘) 3. **file_operations 结构**:将系统调用映射到驱动函数 4. **用户/内核数据交换**:必须使用 copy_to_user / copy_from_user,防止内核崩溃 5. **设备号管理**:主设备号标识设备类型,次设备号标识具体设备实例 6. **模块编程**:了解内核模块的基本框架,为后续深入学习驱动开发打下基础