Files
2026-06-25 00:09:09 +08:00

6.6 KiB
Raw Permalink Blame History

实验五 设备管理——Linux 设备驱动程序安装

一、实验内容

认识 Linux 设备种类与工作方式,理解设备驱动原理,掌握驱动编写规范,安装简单字符设备驱动。

编写字符设备驱动模块,实现 open/close/read/write动态注册/卸载,创建设备文件 /dev/mydev。

二、实验设计

2.1 字符设备驱动结构

+------------------+
|  字符设备驱动模块  |
+------------------+
|  file_operations  |
|  - open          |
|  - release       |
|  - read          |
|  - write         |
+------------------+
|  设备号 (Major)   |
+------------------+
|  设备缓冲区       |
+------------------+

2.2 核心数据结构

#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 设备打开实现

static int mydev_open(struct inode *inode, struct file *file) {
    if (Device_Open) return -EBUSY;  // 设备忙
    Device_Open = 1;
    return 0;
}

要点: 使用静态变量防止并发打开

3.2 设备释放实现

static int mydev_release(struct inode *inode, struct file *file) {
    Device_Open = 0;  // 重置标志,允许再次打开
    return 0;
}

3.3 读操作实现

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 写操作实现

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 模块初始化与清理

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 编译与运行

# 编译驱动(需要内核头文件)
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 测试命令

# 编译驱动(需要 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 设备写入数据被丢弃,读取返回空:

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. 模块编程:了解内核模块的基本框架,为后续深入学习驱动开发打下基础