如果你是操作系统课程设计选到了块设备驱动这个方向应该已经发现网上的资料要么太简略、要么版本太老照着做经常卡在莫名其妙的编译错误上。块设备驱动这个题目好就好在它不依赖真实硬件用内存模拟一块磁盘就能把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.xztar -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-grubmake 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是一个常用的块设备查询命令用户态工具和文件系统会通过它询问设备大小。如果没有实现这个ioctlmkfs和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_mb8 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 bs512 count10 sudo dd if/dev/csblock of/tmp/test.bin bs512 count10如果请求处理函数有问题比如扇区偏移算错或者数据拷贝方向反了这两条命令就能暴露出来。我在测试时有一次是把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返回负数设备号冲突或参数非法改用动态注册主设备号填0add_disk之后没有出现/dev/csblockudev未识别或没有创建设备节点查看/proc/devices手动mknodmkfs时提示无法识别设备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驱动没什么关系。先把虚拟机增强工具重新安装一遍确认共享文件夹、剪切板这些功能正常再继续做内核实验就行。祝你的课程设计顺利通过答辩的时候能把这篇文章里的流程图、代码结构、测试过程讲清楚这门课的成绩一般不会差。