Linux内核5.4下SD卡驱动开发实战:从mmc_blk_probe到块设备注册全流程解析
Linux内核5.4下SD卡驱动开发实战:从mmc_blk_probe到块设备注册全流程解析
最近在调试一块基于i.MX8MP的工控板,SD卡死活识别不出来,dmesg里只看到一堆MMC控制器初始化的信息,但就是没有/dev/mmcblk0这个设备节点出现。折腾了两天,从硬件焊接检查到设备树配置,最后才发现问题出在驱动层——mmc_blk_probe函数里对卡类型的一个判断逻辑没走对。这次经历让我意识到,对于嵌入式Linux开发者而言,仅仅知道SD卡驱动“能用”是远远不够的,必须深入到MMC子系统的核心,理解从卡片探测到块设备诞生的每一个细节,才能在遇到问题时精准定位,甚至进行定制化开发。
这篇文章,我就结合Linux 5.4内核源码,带你完整走一遍SD卡块设备创建的“生命之旅”。我们不会停留在概念层面,而是像解刨一样,逐行分析mmc_blk_probe、mmc_blk_alloc、mmc_add_disk这几个核心函数,看看request_queue、gendisk这些关键数据结构是如何被组装起来的。无论你是正在为项目定制存储驱动,还是单纯想深入理解Linux块设备子系统,相信这篇实战解析都能给你带来实实在在的收获。
1. 基石:Linux块设备驱动模型再认识
在深入MMC之前,我们有必要快速回顾一下Linux内核中块设备驱动的通用框架。这就像盖房子前先看蓝图,理解了整体结构,再看具体模块(比如SD卡驱动)的实现就会清晰很多。
简单来说,当上层文件系统(比如Ext4)想要读写一块硬盘时,它不会直接操作硬件。这个请求会经历一个精妙的“流水线”:
- 通用块层(Generic Block Layer):将读写请求封装成一个或多个
bio结构体。bio描述了要传输的数据在内存中的位置(页)和磁盘上的位置(扇区)。 - I/O调度层(I/O Scheduler Layer):接收
bio,并将其封装成request结构体,然后放入一个叫request_queue的队列中。调度器在这里发挥关键作用,它可能会对多个请求进行合并(相邻扇区的请求合为一个)和排序(电梯算法,减少磁头移动),以优化磁盘访问性能。 - 块设备驱动层:这是驱动开发者主要工作的层面。驱动会提供这个
request_queue,并设置好处理队列中请求的回调函数。驱动从队列中取出request,将其翻译成硬件能理解的命令(对于SD卡,就是MMC命令),通过主机控制器发送出去。
那么,系统如何知道有一个块设备存在呢?答案就是gendisk(通用磁盘)结构体。你可以把它看作是一个块设备在内核中的“身份证”和“操作手册”。它包含了设备容量、主次设备号、指向request_queue的指针,以及一个非常重要的结构体——block_device_operations。这个结构体里定义了一系列文件操作类似的方法,比如open、release、ioctl等,是驱动与VFS(虚拟文件系统)交互的接口。
为了更直观,我们用一个表格来对比这几个核心数据结构的分工:
| 数据结构 | 所属层次 | 核心职责 | 类比 |
|---|---|---|---|
struct bio | 通用块层 | 描述一个I/O请求的“数据段”,包含内存页和磁盘扇区映射。 | 快递包裹里的“货物清单”。 |
struct request | I/O调度层 | 封装一个或多个bio,代表一个待处理的I/O操作单元。 | 一个待派送的“快递包裹”。 |
struct request_queue | I/O调度层 / 驱动层 | 管理request的队列,包含调度策略和驱动处理函数。 | 快递公司的“分拣中心”和“派送规则”。 |
struct gendisk | 驱动层 | 代表一个块设备(或分区),关联request_queue和操作集。 | 快递网点的“营业执照”和“服务手册”。 |
struct block_device_operations | 驱动层 | 定义块设备的文件操作接口(open/ioctl等)。 | 服务手册里的“标准操作流程”。 |
对于SD卡(属于MMC设备家族)而言,它的驱动位于drivers/mmc/core/block.c,是上述通用模型的一个具体实现。MMC子系统在request_queue之上又封装了自己的struct mmc_queue,在request之上封装了struct mmc_queue_req,以处理MMC协议特有的命令、响应和数据传输。
2. 入口:mmc_blk_probe()的职责与探路
当内核检测到一个MMC设备(比如SD卡插入)时,MMC核心层会为其创建一个struct mmc_card对象,并将其“注册”到MMC总线(mmc_bus)上。这触发了总线上的设备与驱动匹配过程。我们的主角——mmc_driver,正是在此时登场。
static struct mmc_driver mmc_driver = {
.drv = {
.name = "mmcblk",
.pm = &mmc_blk_pm_ops,
},
.probe = mmc_blk_probe,
.remove = mmc_blk_remove,
.shutdown = mmc_blk_shutdown,
};
mmc_blk_probe就是这个驱动的探测函数,也是SD卡块设备创建流程的总入口。它的参数是匹配成功的mmc_card。这个函数就像一位严谨的工厂质检员,工作流程清晰:
第一步,资格检查。 不是所有插到MMC槽里的卡都是存储卡(比如还有SDIO功能的Wi-Fi卡)。所以mmc_blk_probe第一件事就是检查卡片的命令集(csd.cmdclass)是否支持块读取(CCC_BLOCK_READ)。如果不支持,直接返回-ENODEV,表示“这不是我要驱动的设备”。我开头提到的调试问题,就是模拟的SDIO信号让驱动误判了卡片类型,导致这一步就失败了。
第二步,创建工作队列。 为卡片分配一个用于完成事务的工作队列(complete_wq)。MMC命令的发送和响应是异步的,这个队列用于处理命令完成后的后续工作。
第三步,核心资源分配与初始化。 调用mmc_blk_alloc(card)。这是整个流程的核心步骤,它负责创建并初始化驱动所需的几乎所有核心数据结构:mmc_blk_data、mmc_queue、request_queue和gendisk。我们将在下一节深入剖析。
第四步,处理物理分区。 对于像eMMC这样的设备,可能会有多个物理分区(比如用户区、RPMB、boot分区)。mmc_blk_alloc_parts会为这些额外分区创建对应的mmc_blk_data。对于普通的SD卡,通常只有一个用户数据区,这一步可以简单略过。
第五步,关联与注册。 将上一步创建的mmc_blk_data通过dev_set_drvdata关联到mmc_card设备上。最后,调用mmc_add_disk(md),将准备好的gendisk正式注册到内核的块设备子系统中。至此,用户空间才能在/dev目录下看到mmcblk0这样的设备节点。
提示:
mmc_blk_probe的返回值至关重要。返回0表示成功,块设备创建;返回负的错误码则意味着失败,驱动加载会中止。在调试时,可以在这里增加pr_debug打印,跟踪每个阶段的执行情况。
整个mmc_blk_probe的函数体,实际上是一个清晰的资源申请和管理链条,任何一环出错都需要妥善地回滚(goto out)释放已申请的资源,防止内存泄漏。
3. 核心构造:mmc_blk_alloc()如何搭建驱动骨架
如果说mmc_blk_probe是总指挥,那么mmc_blk_alloc就是负责搭建所有基础设施的工程师。它接收一个mmc_card,最终返回一个完全初始化好的struct mmc_blk_data *。这个结构体是MMC块设备驱动的中央控制单元,几乎所有关键部件都挂在它下面。
让我们跟着代码,看看它具体做了什么:
1. 计算设备容量
首先,它需要知道这张卡有多大。容量信息存储在卡的CSD(Card Specific Data)或EXT_CSD寄存器中。对于SD卡,使用CSD中的capacity字段,并考虑read_blkbits(读取块大小,通常是9,代表512字节)进行换算,最终得到以512字节扇区为单位的size。这个size后面会被设置到gendisk中。
2. 分配设备号
通过ida_simple_get从mmc_blk_ida这个ID分配器中获取一个空闲的次设备号索引(devidx)。这决定了你的设备会是mmcblk0、mmcblk1中的哪一个数字。
3. 分配并初始化mmc_blk_data 这是内存分配的基础步骤。之后,开始填充这个结构体的各个字段:
area_type: 标识区域类型(主数据区、RPMB、Boot等)。read_only: 根据卡写保护开关和主机能力判断是否为只读。- 关键操作:分配gendisk。调用
alloc_disk(perdev_minors)。perdev_minors决定了每个物理设备支持多少个分区(比如是8,则支持mmcblk0p1-p8)。
4. 初始化请求队列(mmc_init_queue)
这是最复杂也最关键的一步。mmc_init_queue函数做了两件大事:
- 创建并设置
struct request_queue:通过blk_mq_init_queue初始化一个多队列块设备请求队列。这里关联了mmc_mq_ops,它定义了请求如何被驱动处理(queue_rq)、初始化(init_request)和完成(complete)等操作。 - 启动请求处理线程:虽然代码片段未完全展示,但在
mmc_setup_queue中,会为这个队列创建内核线程(或使用工作队列),专门负责从request_queue中取出请求,并将其转换为MMC命令序列下发。
5. 设置gendisk
将前面准备好的各个部件组装到gendisk上:
- 设置主设备号为
MMC_BLOCK_MAJOR(固定为179)。 - 设置次设备号基值:
devidx * perdev_minors。 - 设置操作集
fops为&mmc_bdops(定义在block.c中的标准块设备操作)。 - 最关键的一步:将
gendisk->queue指向刚刚创建的request_queue(md->queue.queue)。至此,文件系统的I/O请求才能通过这个gendisk找到对应的处理队列。 - 设置磁盘名称,如
mmcblk0。 - 调用
set_capacity(md->disk, size),设置磁盘容量。
为了更清晰地展示mmc_blk_data如何组织这些结构,我们可以看下面的关系图(用文字描述):
struct mmc_blk_data (md)
|
|---> struct gendisk *disk
| |---> major = 179
| |---> first_minor = (devidx * perdev_minors)
| |---> queue = &(md->queue.queue) // 链接到请求队列
| `---> fops = &mmc_bdops
|
`---> struct mmc_queue queue
|---> struct request_queue *queue // 实际的请求队列
|---> struct mmc_card *card // 反向指向对应的卡
`---> tag_set, lock等成员
mmc_blk_alloc执行成功后,一个功能完整的块设备驱动“骨架”就搭建完毕了,只差最后向内核“报备”注册。
4. 临门一脚:mmc_add_disk()完成块设备注册
经过mmc_blk_alloc的精心准备,所有的数据结构都已就位。mmc_add_disk的工作相对单纯,但却是从内核驱动空间到用户设备节点可见的临门一脚。
它的核心代码只有一行,却重若千钧:
device_add_disk(md->parent, md->disk, NULL);
这个device_add_disk函数是块设备子系统的核心API之一。它主要完成以下工作:
- 将gendisk添加到系统:内核会为这个
gendisk在/sys/block/下创建对应的属性文件。 - 创建设备节点:根据
gendisk中的主次设备号,在/dev目录下创建设备文件(例如/dev/mmcblk0)。这是通过devtmpfs或udev规则触发的。 - 扫描分区表:如果磁盘没有设置
GENHD_FL_NO_PART_SCAN标志,内核会尝试读取设备的前几个扇区,解析MBR或GPT分区表,并为每个分区创建对应的gendisk(分区),在/dev下生成如mmcblk0p1、mmcblk0p2这样的设备节点。
在mmc_add_disk中,除了调用device_add_disk,还会为磁盘创建一些特殊的sysfs属性文件,例如force_ro(强制只读)和ro_lock_until_next_power_on(eMMC的boot分区写保护),让用户空间可以动态调整设备的某些属性。
注意:
device_add_disk的调用意味着设备立刻对系统可见。如果驱动在probe函数中还有未完成的初始化工作(比如硬件自检),需要确保在调用它之前全部完成,否则可能导致用户空间在设备未就绪时发起I/O,引发错误。
当mmc_add_disk成功返回,整个SD卡块设备的创建流程就宣告完成。用户现在可以通过fdisk -l /dev/mmcblk0查看磁盘信息,或者用mount命令将其挂载到文件系统树中进行读写操作。
5. 实战调试:当SD卡驱动不工作时如何排查
理解了理论流程,我们最终要服务于实战。当你的开发板插入SD卡后,/dev下没有出现预期的设备节点时,可以按照以下步骤进行排查,这些步骤本身就是对上述流程的逆向验证:
1. 确认硬件与控制器驱动
首先,确保不是硬件问题。检查dmesg日志,搜索MMC主机控制器是否成功初始化。你应该能看到类似这样的信息:
mmc0: new high speed SDHC card at address aaaa
mmcblk0: mmc0:aaaa SL16G 14.8 GiB
如果没有,问题可能出在设备树配置、时钟、电源或引脚复用上。确保主机控制器驱动(如sdhci-esdhc-imx)已正确加载并probe成功。
2. 检查mmc_blk驱动是否加载
使用lsmod | grep mmc_block查看MMC块设备驱动模块是否已加载。如果没有,尝试modprobe mmc_block手动加载。也可以检查/sys/bus/mmc/drivers/mmcblk/目录下是否绑定了你的卡设备。
3. 深入驱动内部添加调试信息
如果控制器有日志但块设备没创建,问题很可能出在mmc_blk_probe流程中。最有效的方法是在关键函数添加pr_err或dev_dbg打印。
- 在
mmc_blk_probe入口处打印:确认函数是否被调用,以及传入的card指针是否有效。// 临时在drivers/mmc/core/block.c的mmc_blk_probe函数开头添加 dev_info(&card->dev, "mmc_blk_probe entered for card at %pn", card); - 检查CCC_BLOCK_READ判断:这是第一个“拦路虎”。可以打印
card->csd.cmdclass的值,确认卡片是否真的支持块操作。 - 跟踪
mmc_blk_alloc的返回值:在调用mmc_blk_alloc后检查其返回值是否为错误指针(IS_ERR(md)),并打印错误码。 - 检查
mmc_add_disk:确认它是否被调用以及返回值。
4. 关注资源分配失败
mmc_blk_alloc中可能失败的点:
ida_simple_get失败:可能设备号耗尽(max_devices限制)。alloc_disk失败:内存不足。mmc_init_queue失败:可能是DMA内存分配问题,或者blk_mq_alloc_tag_set失败(标签集是块多队列的关键资源)。
5. 使用动态调试(Dynamic Debug) 对于生产或调试内核,频繁修改代码并重新编译不现实。可以启用内核的动态调试功能。
# 启用mmc_block驱动所有文件的动态调试信息
echo 'file drivers/mmc/core/* +p' > /sys/kernel/debug/dynamic_debug/control
# 然后重新插拔SD卡,查看dmesg输出
这会在控制台打印出该驱动内所有pr_debug级别的信息,无需重新编译内核。
6. 检查内核配置 确保内核配置中相关选项已开启:
CONFIG_MMC_BLOCK=y (或=m)
CONFIG_MMC_SDHCI=y (根据你的主机控制器选择)
并且没有其他配置冲突。
我解决之前那个问题,就是在mmc_blk_probe开头加了打印,发现函数根本没被调用。然后回溯发现,是MMC核心层在识别卡类型时,因为某个时序问题,将我的SD卡错误地识别为了MMC_TYPE_SD_COMBO(SDIO复合卡),导致它匹配到了另一个驱动(mmc_sdio驱动),而mmcblk驱动则因为不匹配而被跳过。调整了主机控制器的识别时序参数后,问题得以解决。
这个过程充分说明,对驱动创建流程的透彻理解,是快速定位和解决复杂嵌入式系统问题的基石。它让你知道该在哪里下断点,该查看哪个变量,该怀疑哪个环节,而不是盲目地四处尝试。
更多推荐
所有评论(0)