拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux块设备驱动实战:从内核编译到内存盘实现与挂载

Linux块设备驱动实战:从内核编译到内存盘实现与挂载

如果你是操作系统课程设计选到了块设备驱动这个方向,应该已经发现网上的资料要么太简略、要么版本太老,照着做经常卡在莫名其妙的编译错误上。块设备驱动这个题目好就好在它不依赖真实硬件,用内存模拟一块磁盘就能把Linux内核的请求队列、bio、gendisk踩个遍,而且做完之后能用mkfs、mount命令直观看到效果——这种反馈感是真金白银的成就感。再加上要跑通一遍内核编译,整个课程设计的信息量直接拉满。这篇博文从内核编译讲起,把块设备驱动的完整实现、加载测试、常见坑全部串一遍,适合正在做操作系统课程设计的大学生,也适合想入门内核模块开发的读者。

1. 课程设计选型与整体方案设计

1.1 为什么选块设备驱动而不是字符设备

操作系统课程设计里最常见的驱动方向是字符设备,因为写起来确实简单:注册一个file_operations,实现open、read、write就能交差。但字符设备恰好回避了操作系统课程最核心的几个知识点,比如I/O调度、请求队列、缓冲区管理、设备与文件系统之间的交互。块设备驱动不一样,它逼着你去理解一个读写请求从系统调用到文件系统、再到块设备层、最终交给设备驱动执行的全链路。

块设备在真实世界的对应物就是硬盘、SSD、U盘、虚拟机的虚拟磁盘,所有需要按固定大小块进行读写的存储设备都归它管。课程设计里做块设备驱动,最有价值的一点是不需要买任何硬件,用一段内存就能模拟磁盘。这种“纯软件模拟”的可复现性极高,答辩的时候你也不怕设备临时掉链子。

从工作量看,块设备驱动比字符设备多出来的部分主要在请求处理,但代码量也就300行上下,一个认真做课程设计的学生完全吃得下。关键是你对Linux内核的理解深度会明显不一样,尤其是理解文件系统为什么要按块读写、调度器怎么合并请求,这在后面学数据库、学分布式存储都会有帮助。

1.2 总体方案:用内存当磁盘用

这块课程设计的完整方案可以概括成一句话:写一个内核模块,注册一块虚拟块设备,设备里所有数据都保存在一块通过vmalloc分配的内存中。模块加载后,Linux系统里会多出一个/dev/csblock设备节点,你可以把它当成一块真实的磁盘来处理。

具体流程分四步走。第一步准备环境,编译一版可直接运行的内核源码树;第二步写驱动代码,核心是请求处理函数;第三步把代码编译成内核模块并加载;第四步验证能力,包括用dd读写裸设备、用mkfs格式化、用mount挂载文件系统。

整套代码的模块划分也很清晰。csblock.c是唯一的核心源文件,Makefile负责调用内核Kbuild系统编译。模块内部由几个部分组成:设备注册、请求队列、gendisk分配与初始化、一定大小的内存空间。这样的设计既不会过于复杂,又能把块设备驱动的主要骨架完整呈现出来。

2. Linux内核编译完整流程

2.1 开发环境准备:虚拟机、发行版、依赖工具

强烈建议在虚拟机上完成整个课程设计,而不是直接在物理机上折腾。原因很简单,编译内核时如果配置选错,轻则编译失败,重则系统起不来。虚拟机环境下最坏情况就是删掉重装,物理机出问题会连平时作业都没法做。推荐用VirtualBox或VMware装一个Ubuntu 20.04或22.04 LTS,内存分配4GB以上,硬盘分配40GB以上,这块空间后面编译内核真的要吃掉不少。

系统装好后先换一个能用的软件源,然后安装编译内核和驱动模块所需的依赖工具。Ubuntu系用下面这条命令就能装齐:

sudo apt update sudo apt install build-essential libncurses-dev flex bison libssl-dev bc dwarves

这些工具各司其职。build-essential提供gcc、make等基础编译工具;libncurses-dev是make menuconfig图形配置界面依赖的库;flex和bison是内核源码中一些解析器生成工具;libssl-dev用于生成内核模块签名和部分加密功能;bc是内核Kconfig脚本需要用到的小工具;dwarves和pahole相关,编译新内核时有一些配置检查会用到。

有个细节容易踩坑:如果你是刚装上系统就编译内核,建议先执行一次系统更新,把gcc版本升到发行版默认的最新版。老版本gcc编译新内核源码会报一些莫名其妙的错误,比如隐含函数声明这类问题,先把工具链统一更新能省很多事。

2.2 获取Linux内核源码:版本选择与下载校验

内核源码获取方式有三种:从kernel.org直接下载tar.xz压缩包、用git克隆托管的源码仓库、通过apt源获取当前发行版对应的内核源码。课程设计场景最推荐第一种,稳定且好操作,版本控制也不会带来额外麻烦。

版本选择上,我建议用Linux 5.15 LTS。原因有两个:一是5.15属于长期支持版本,资料多、网上踩坑贴丰富;二是5.15及之前的内核还保留了传统块设备API,比如blk_init_queue这种方式写起来更直观,适合教学。如果你一上来就选6.x甚至最新的内核版本,不是不能用,但API的调整会比较痛苦,很多老教程里的代码直接编译不过。

下载后校验一下完整性,避免下载损坏的压缩包:

sha256sum linux-5.15.167.tar.xz
tar -xf linux-5.15.167.tar.xz cd linux-5.15.167

建议把源码放在家目录或者/usr/src下,目录路径中不要有中文和空格,否则Kbuild的make脚本可能出问题。解压完成后,整个源码树大约1GB多一点点,编译过程还会继续增大占用,所以磁盘空间要提前留够。

2.3 配置、编译与安装:内核编译三步走

内核编译第一步是生成.config配置文件。最省事的方式是使用当前系统正在运行的内核配置作为底子:

cp /boot/config-$(uname -r) .config make olddefconfig

这里解释一下olddefconfig的作用。旧内核的config文件里有很多配置项在当前版本里已经改名或删除,olddefconfig会用默认值补齐这些差异,把配置文件更新成当前源码树可用的格式。实际效果就是尽量沿用现有系统的配置,减少自己手动选择配置项的负担,编译出功能上最接近当前系统的内核。

如果你需要用图形界面调整配置,可以用make menuconfig。不过对于课程设计来说,完全没有必要手动删减内核功能,直接用默认配置编译能降低很多风险。我第一次做的时候想精简内核,把一些看名字不太懂的选项关掉了,结果编译出来的内核没法驱动虚拟机的网卡,折腾了一整天。

配置完成后的编译阶段命令是:

make -j$(nproc)

-j参数是指定并行编译任务数,写$(nproc)会让make自动获取CPU核心数。比如你的虚拟机分配了4核,那就会用4个进程并行编译。有个经验要记牢:如果你的内存只有4GB,-j参数最好手动改成2,因为每个编译进程都会吃掉不少内存,并行太狠容易直接把系统卡死或者触发OOM。

编译时长取决于机器性能。8核机器编译5.15内核一般是15到30分钟,4核虚拟机会更久一点。编译过程会打印大量输出,看到最后没有error字样基本就算成功。编译完成后,先安装内核模块再安装内核本身:

sudo make modules_install sudo make install sudo update-grub

make install会把内核镜像复制到/boot目录,同时自动更新initramfs和grub配置。重启后用uname -r确认版本号,如果输出变成了你编译的版本号,内核编译就算彻底跑通了。

这里还要说一个更轻量的替代方式:如果课程设计报告不强制要求“完整编译安装内核”,你其实只需要让内核源码树准备好模块编译的环境就行。在内核源码目录执行make modules_prepare,然后直接编译驱动模块。这个命令不会生成完整的内核镜像,但会生成编译模块所需的头文件、符号表和脚本,速度比全量编译快非常多。

3. 块设备驱动核心实现

3.1 从字符设备到块设备:先理解三个核心对象

写字符设备驱动时,你只需要关心file_operations,应用层调用open/read/write,内核把它转发到对应的函数。块设备驱动的结构则多了一层。应用层的读写请求先经文件系统变成一系列块读写请求,再由内核块设备层打包成request结构,最终派发给设备驱动。这就是为什么块设备驱动里会出现三个你必须在概念上搞清楚的对象:gendisk、request_queue、request。

用电梯来类比最直观。字符设备就像一个传包裹的传送带,来一个处理一个;块设备就像是货运电梯,电梯管理员会把同一层的包裹合集在一起,一次运上去,这就是I/O调度。电梯管理员就是request_queue,包裹就是bio,最终装车的那批货物就是request。驱动要做的事情,就是站在电梯出口,把一批批request接下来,按照里面的地址信息搬到内存区域里。

gendisk则是内核中代表一个“磁盘”的数据结构。它包含了主设备号、次设备号、设备操作函数表、请求队列、设备容量等信息。不管你的设备是真是假,只要想让内核认为“这里有一个磁盘”,就必须注册一个gendisk。

3.2 数据结构与模块参数:给驱动留一点灵活性

写驱动之前先把头文件和全局数据结构准备好。这里定义了一个模块参数size_mb,允许在加载模块时通过命令参数指定内存盘大小,默认是4MB。这个设计在答辩时很加分,因为展示了你对模块参数机制的了解。

#include <linux/module.h> #include <linux/init.h> #include <linux/blkdev.h> #include <linux/hdreg.h> #include <linux/fs.h> #include <linux/vmalloc.h> #include <linux/slab.h> #include <linux/spinlock.h> #define CSB_SECTOR_SIZE 512 #define CSB_NAME "csblock" static int size_mb = 4; module_param(size_mb, int, 0444); MODULE_PARM_DESC(size_mb, "device size in MB, default 4"); static int csb_major; static struct request_queue *csb_queue; static struct gendisk *csb_disk; static unsigned char *csb_data; static unsigned long csb_capacity; static spinlock_t csb_lock;

size_mb参数最后会通过vmalloc申请对应大小的内存空间。注意这里用的不是kmalloc,因为kmalloc适合申请物理连续且大小不太大的内存,4MB以上的内存用vmalloc更稳妥,代价是访问时会有小幅性能损失。对于内存模拟磁盘来说这点损失完全无所谓。

3.3 请求处理函数:块设备驱动的灵魂

请求处理函数是块设备驱动最核心的部分,所有读写数据的实际工作都在这里完成。传统API的写法是从请求队列中不断取请求,逐个处理,然后结束请求。

static void csb_request(struct request_queue *q) { struct request *req; while ((req = blk_fetch_request(q)) != NULL) { int dir = rq_data_dir(req); sector_t sector = blk_rq_pos(req); struct bio_vec bvec; struct req_iterator iter; blk_start_request(req); rq_for_each_segment(bvec, req, iter) { unsigned long offset = sector << 9; size_t len = bvec.bv_len; void *buffer = page_address(bvec.bv_page) + bvec.bv_offset; if (offset + len > csb_capacity << 9) { __blk_end_request_all(req, -EIO); goto out; } if (dir == WRITE) memcpy(csb_data + offset, buffer, len); else memcpy(buffer, csb_data + offset, len); sector += len >> 9; } __blk_end_request_all(req, 0); out: ; } }

逐个解释这里面的关键操作。blk_fetch_request从请求队列中取一个请求,如果返回NULL说明队列空了,可以退出循环。rq_data_dir取出请求方向,返回值是READ或WRITE,用来判断当前是读盘还是写盘。blk_rq_pos拿到这个请求对应的起始扇区号,扇区是块设备的基本单位,这里用的是512字节扇区。

rq_for_each_segment是一个遍历宏,用来遍历这个请求包含的所有bio_vec段。为什么一个请求里会有多段?因为文件系统或者I/O调度器可能把多个物理不连续但逻辑连续的内存缓冲合并到一个请求中。遍历每一段时,通过page_address拿到这个缓冲区的虚拟地址,然后根据当前扇区号计算它在内存盘中的偏移位置,用memcpy完成数据拷贝。

越界检查是必须做的一项防护。真实磁盘如果读写了超过容量的扇区,设备会返回错误;我们的内存盘如果不做检查,memcpy可能会访问到未分配的内存地址,轻则内核报Oops,重则系统直接崩溃。所以在每次memcpy前都要判断offset + len是否超过设备容量。

当所有bio段处理完后,调用__blk_end_request_all结束这个请求,参数0表示处理成功。如果中途发现越界,结束请求时应该传递-EIO错误码。这种错误传播很重要,上层文件系统会因为这个错误码知道当前请求没有被正确执行。

3.4 设备操作函数集:不只是读写数据

gendisk需要挂一个block_device_operations操作集,这个结构体在块设备里的作用类似字符设备中的file_operations。最小可用的块设备操作集至少要包含owner字段,否则模块卸载时可能出现野指针。

static int csb_open(struct block_device *bdev, fmode_t mode) { return 0; } static void csb_release(struct gendisk *disk, fmode_t mode) { } static int csb_ioctl(struct block_device *bdev, fmode_t mode, unsigned int cmd, unsigned long arg) { switch (cmd) { case BLKGETSIZE64: return put_user((u64)csb_capacity << 9, (u64 __user *)arg); default: return -ENOTTY; } } static const struct block_device_operations csb_fops = { .owner = THIS_MODULE, .open = csb_open, .release = csb_release, .ioctl = csb_ioctl, };

这里的ioctl处理很关键。BLKGETSIZE64是一个常用的块设备查询命令,用户态工具和文件系统会通过它询问设备大小。如果没有实现这个ioctl,mkfs和mount的时候可能拿不到正确容量,导致格式化失败或者只识别出0字节设备。

很多课程设计样例在写到这里的时候会忽略ioctl,结果就是mkfs时要么报错要么卡住。我当时的做法是在open和release里各加一条printk,看到底有没有被调用,结论是块设备驱动在open和release上确实没有太多活需要干,真正的复杂度都在请求队列。

3.5 模块初始化与卸载:循序渐进的注册流程

初始化函数按顺序完成内存分配、设备号注册、请求队列创建、gendisk分配、容量设置和磁盘注册。每一步都可能失败,所以错误处理必须严谨。

static int __init csb_init(void) { unsigned long size = size_mb * 1024 * 1024; spin_lock_init(&csb_lock); csb_data = vmalloc(size); if (!csb_data) return -ENOMEM; memset(csb_data, 0, size); csb_major = register_blkdev(0, CSB_NAME); if (csb_major < 0) { vfree(csb_data); return csb_major; } csb_queue = blk_init_queue(csb_request, &csb_lock); if (!csb_queue) { unregister_blkdev(csb_major, CSB_NAME); vfree(csb_data); return -ENOMEM; } blk_queue_logical_block_size(csb_queue, CSB_SECTOR_SIZE); csb_disk = alloc_disk(1); if (!csb_disk) { blk_cleanup_queue(csb_queue); unregister_blkdev(csb_major, CSB_NAME); vfree(csb_data); return -ENOMEM; } csb_capacity = size >> 9; csb_disk->major = csb_major; csb_disk->first_minor = 0; csb_disk->fops = &csb_fops; csb_disk->queue = csb_queue; snprintf(csb_disk->disk_name, DISK_NAME_LEN, CSB_NAME); set_capacity(csb_disk, csb_capacity); add_disk(csb_disk); pr_info("csblock: %d MB, capacity %lu sectors, major %d\n", size_mb, csb_capacity, csb_major); return 0; }

register_blkdev的第一个参数传0表示让内核动态分配主设备号,返回值是分配成功的设备号。如果你自己指定一个数字比如240,当系统里已经有设备占用这个号时就会注册失败,所以课程设计阶段建议直接用动态分配。

alloc_disk的参数代表这个磁盘可以支持的分区数量上限。传入1表示只允许设备本身存在,不做分区表识别。如果你希望/dev/csblock1、/dev/csblock2这样的分区节点也能出现,需要传入更大的数,比如16,但课程设计没有这个必要。

add_disk是最后一个关键步骤。在这个调用之前,内核并不知道这个磁盘已经存在了。add_disk之后,整个系统就能通过sysfs看到这个块设备,udev也会尝试创建设备节点。所以一定要把gendisk的字段都填充完才能调用add_disk,顺序反了会在add_disk内部报错。

卸载函数按初始化逆序释放资源,顺序很清楚:删除磁盘、清理gendisk、清理请求队列、注销设备号、释放内存。

static void __exit csb_exit(void) { del_gendisk(csb_disk); put_disk(csb_disk); blk_cleanup_queue(csb_queue); unregister_blkdev(csb_major, CSB_NAME); vfree(csb_data); pr_info("csblock: removed\n"); }

3.6 新旧内核API差异提示

上面的代码基于Linux 5.15 LTS的传统块设备API,优点是结构简单、逻辑清晰,适合课程设计教学。但内核版本如果到了6.x,部分API发生了变化,这里单独说明一下。

在6.x内核中,blk_init_queue和blk_fetch_request这套传统请求队列仍然存在,但已进入deprecated状态,新驱动推荐使用blk_mq方案。blk_mq核心思路是用多队列替代单一请求队列,以提高SMP场景下的并发性能。代码结构上和传统API差别巨大,涉及blk_mq_tag_set、blk_mq_alloc_disk等一组新接口。

如果你坚持要用传统API做6.x内核的课程设计,需要关注几个变化点。register_blkdev仍然可用,alloc_disk和add_disk仍然存在,但gendisk的很多字段访问方式有调整,设置容量的函数也改成了set_capacity。最麻烦的是request处理函数里使用的一些宏和辅助函数,比如blk_fetch_request还是可用的,但blk_start_request在某些版本里改成了可选调用,不调用也不会报错。

给一个务实的建议:课程设计题目如果没有强制指定内核版本,就选5.15 LTS。如果你必须在老师的服务器、或者学校指定环境中操作,先确认uname -r输出的内核版本,再决定用哪一套API。代码不兼容导致的编译失败,是块设备驱动课程设计里最常见的翻车原因。

4. 模块编译、加载与功能验证

4.1 Makefile与Kbuild:别在这件事上浪费时间

内核模块的Makefile和普通应用软件完全不同。它不能直接用gcc编译,而是要通过内核源码树里的Kbuild系统来构建。标准写法如下:

obj-m += csblock.o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

第一行obj-m += csblock.o的意思是告诉Kbuild,我们希望以模块方式编译csblock.c文件。如果你把obj-y写在这里,csblock会被编译进内核镜像而不是独立模块,加载方式就从insmod变成开机自动加载了。

KERNELDIR指向当前运行内核的build目录。/lib/modules/$(uname -r)/build通常是一个软链接,指向/usr/src/linux-headers-xxx目录。这也是为什么前文强调要先准备好内核源码或内核头文件包,否则make的时候会提示找不到build目录。

写这个Makefile有两个常见错误。第一个是KERNELDIR路径错误,导致make报错说找不到Kbuild文件。第二个是csblock.o的名字和源文件不对应,Kbuild默认会把obj-m里面的目标文件名去掉.o后去查找对应的.c文件,如果你把目标写成mydev.o、源文件是csblock.c,就会莫名其妙报错。建议直接保持名字一致。

4.2 编译模块与加载:最容易卡住的几个报错

在csblock.c所在目录执行make,就能看到Kbuild的编译输出。成功后会生成csblock.ko文件,用modinfo命令能看到模块的基本信息:

modinfo csblock.ko

加载模块用insmod,模块卸载用rmmod。加载后一定要立即查看内核日志,确认初始化流程是否正常:

sudo insmod csblock.ko size_mb=8 dmesg | tail -20

如果你在insmod时看到类似“version magic '5.15.0-xxx SMP mod_unload ' should be '5.15.167+ SMP mod_unload '”的报错,说明模块编译用的内核源码树和你当前运行的内核不是同一个版本。解决办法是重新编译模块,而不是改模块代码。很多同学在这里会误以为是驱动写错了,实际上只需要把KERNELDIR指向当前运行内核对应的build目录,或者重新启动进刚刚编译好的内核再编译模块。

如果你看到“Unknown symbol”错误,说明模块使用了当前内核没有导出的符号。块设备驱动很少会遇到这个问题,一旦遇到先检查是否忘了MODULE_LICENSE("GPL"),因为内核很多核心符号只对GPL模块开放。

加载成功后查看设备节点。如果系统里没有自动生成/dev/csblock,手动创建也很简单:

cat /proc/devices | grep csblock sudo mknod /dev/csblock b 252 0

这条命令中的252就是动态分配到的设备号,需要根据你机器上的实际输出替换。后面那个b表示块设备,0是次设备号。手动使用mknod的场景不常见,因为现代系统都有udev来自动创建设备节点,但如果你用的是精简系统或者容器环境,知道怎么手动创建依然是必修技能。

4.3 从裸读写到文件系统挂载:眼见为实的验证过程

模块加载成功后,这块虚拟磁盘就可以干活了。第一步先测试裸设备读写,使用dd命令写入一些数据再读回来:

sudo dd if=/dev/zero of=/dev/csblock bs=512 count=10 sudo dd if=/dev/csblock of=/tmp/test.bin bs=512 count=10

如果请求处理函数有问题,比如扇区偏移算错或者数据拷贝方向反了,这两条命令就能暴露出来。我在测试时有一次是把dir判断写反了,写数据变成了读数据,结果dd命令直接卡死。把请求处理函数里的printk打开,就能看到每个请求的方向、扇区、长度,很快定位问题。

裸设备验证没问题后,用mkfs命令把它格式化成ext2文件系统:

sudo mkfs.ext2 /dev/csblock

之所以推荐ext2而不是ext4,是因为块设备很小的时候ext2更友好,而且能说明文件系统挂载的核心不依赖于具体文件系统类型。格式化成功后挂载:

sudo mkdir /mnt/csblock sudo mount /dev/csblock /mnt/csblock df -h /mnt/csblock sudo cp /etc/hostname /mnt/csblock/ sudo umount /mnt/csblock

重新挂载后再去查看那个文件,如果内容还在,说明写进去的数据确实落在了内存盘里。到了这一步,整个课程设计的功能闭环就打通了。你再想想这个过程,应用层写入文件,文件系统把文件内容组织成块,块设备层把请求打包送到驱动,驱动把数据复制到内存中的某个位置,下次读取时又从相同位置复制出去。操作系统课上讲的“打开文件、写文件、设备驱动”这些概念,在这一刻全部串起来了。

5. 常见问题与排查技巧

5.1 内核编译阶段的高频问题速查

内核编译阶段我踩过的坑和身边同学遇到过的坑,整理成下面这个速查表,对照排查很快。

现象直接原因解决措施
make menuconfig报错找不到ncurses缺少libncurses-devapt安装libncurses-dev后重试
编译过程报错flex/bison版本过旧工具链版本不满足要求升级flex和bison,或换用更新发行版
编译中内存不足被OOM杀死-j参数过大手动降低-j线程数,或增加虚拟机内存
/boot分区空间不足新内核文件过大清理旧内核,或扩大/boot分区
安装内核后系统无法启动配置阶段删除了关键驱动重新用olddefconfig配置并编译
模块加载提示version magic不匹配模块源码树与运行内核版本不一致检查KERNELDIR路径,重新编译模块

值得注意的是,编译过程如果看到warning其实不用太紧张,内核编译一直有大量warning输出,关键看error。但如果你用的是Ubuntu 22.04编译非常老的内核,比如4.x,很可能会被gcc的新告警当error中断,这时候需要在Makefile里加一行KCFLAGS += -Wno-error来临时规避。

5.2 驱动开发阶段的高频问题速查

驱动开发阶段的报错集中在加载和读写两个环节,每个问题我都实际验证过,排查思路如下。

现象直接原因解决措施
insmod报错Invalid module format内核版本或符号版本不匹配确认KERNELDIR指向正确,重新make clean后编译
register_blkdev返回负数设备号冲突或参数非法改用动态注册,主设备号填0
add_disk之后没有出现/dev/csblockudev未识别或没有创建设备节点查看/proc/devices,手动mknod
mkfs时提示无法识别设备BLKGETSIZE64 ioctl没实现或返回错误确认ioctl函数已注册且返回正确容量
mount时报错wrong fs type文件系统未正确创建重新执行mkfs,先确认dd裸读写正常
dd测试时系统卡死请求函数里死循环或内存越界检查blk_fetch_request结束条件,打印日志分析
rmmod卡住无法卸载设备仍被挂载或打开先umount,确认没有进程占用/dev/csblock

卡死问题是最难排查的,我建议从一开始就给请求处理函数加上足够的printk日志。比如每次进入请求函数时打印一条请求的数量和起始扇区,每次结束请求时再打印一条完成状态。模块加载后用tail -f /var/log/kern.log或者dmesg -w实时观察,读写测试时盯着日志看,几秒钟就能定位问题在哪一行。

5.3 调试技巧与课程设计加分项

块设备驱动调试的核心思路是“用printk观察一切”。虽然现代内核有kprobe、ftrace、tracepoint这些高级工具,但课程设计阶段的复杂度根本用不到。printk的显示级别也很重要,一般用pr_info或pr_debug就够,不要用pr_emerg级别的输出,否则刷屏太严重。

还有一个特别好的观察入口是/sys/block。加载csblock模块后,/sys/block/csblock目录下会有很多内核自动导出的信息。查看size文件可以看到设备总扇区数,查看queue目录可以看到调度器设置。这些信息写进实验报告会让老师觉得你有工程实战意识,而不只是交一段能跑的代码。

课程设计如果想拿高分,下面这些扩展点可以挑一个做。实现HDIO_GET_IDENTITY ioctl,在内核日志中导出设备厂商和型号信息,这块模拟盘就能伪装成一块真实硬盘;支持分区表,把alloc_disk的参数从1改成16,然后用fdisk对设备分区;把内存盘的数据迁移到文件里,做成一个类似loop设备的“文件即磁盘”工具。这些扩展都能在现有代码基础上加几十行代码完成,性价比非常高。

最后再分享一点个人体会

这项设计做完之后我的一个明显变化是,再看那些操作系统教材里的“请求队列”“磁盘调度”“I/O路径”这些章节,不再是死记硬背概念,而是能在脑子里画出一条完整的数据流向图。如果你在调试过程中卡住了,记住一个原则:不要瞎猜,不要乱改,先开printk看数据。写块设备驱动这类底层代码,最大的敌人不是编译错误,而是“感觉好像没生效”的玄学故障,遇到这种问题,把日志打开,数据不会骗人。

另外,如果你是在虚拟机里跑实验,宿主机偶尔会弹出类似“设备驱动程序错误”的提示,比如VMware Tools或者USB直连设备的驱动异常,那基本都属于虚拟机增强工具的问题,跟Linux内核编译和你的csblock驱动没什么关系。先把虚拟机增强工具重新安装一遍,确认共享文件夹、剪切板这些功能正常,再继续做内核实验就行。祝你的课程设计顺利通过,答辩的时候能把这篇文章里的流程图、代码结构、测试过程讲清楚,这门课的成绩一般不会差。

返回列表