USB Gadget FunctionFS初始化流程详解:f_fs.c内核实现剖析
做USB gadget开发或者搞过Android系统移植的大概率都跟FunctionFS打过照面。最典型的场景是内核里有一个usb gadget控制器你想让用户态程序直接控制这个设备对电脑/手机呈现出来的USB功能比如把某个用户态协议栈包装成一个串口、一个MTP设备或者一个自定义vendor设备。这时候FunctionFS就是那条把用户态和内核USB协议栈缝在一起的线而drivers/usb/gadget/function/f_fs.c就是这条线在内核侧的全部实现。这个文件里最值得先吃透的就是初始化流程。从模块加载、文件系统挂载、ep0节点创建到用户态往ep0写描述符触发状态机迁移再到gadget bind流程配合这条链路牵涉VFS、USB gadget、configfs、用户态ABI好几个层面很容易让新手看懵。这篇东西就围绕f_fs.c的初始化流程来拆把每一步是在干什么、为什么这么干、出问题怎么排查一次说清楚。1. 先定位f_fs.c 在整个USB gadget体系里到底承担什么1.1 用一句话说清FunctionFS的设计价值普通USB gadget开发开发者要么在drivers/usb/gadget/function/里写一个内核态function比如f_serial.c、f_mass_storage.c要么用configfs把现成function组合成复合设备。但很多场景下真正想实现的USB逻辑不是在“内核态”里做的而是在用户态程序里做比如用户态要实现一套私有协议交互这逻辑写在用户态显然开发效率更高、调试更方便。FunctionFS就是为这个场景设计的它在内核里提供一个文件系统接口用户态程序通过mount -t functionfs拿到可以在用户态文件读写操作访问的端点文件往这些文件里写数据数据就会从USB总线上发出去USB主机发过来的数据你能从文件里读出来。相当于内核只负责把USB协议栈翻译成文件读写语义业务逻辑全留给你在用户态自己玩。这也是f_fs.c整个初始化的总目标把“一个可mount的文件系统”、“一组USB端点的文件抽象”、“一个和gadget生命周期挂钩的function实例”这三样东西统一管理起来。1.2 初始化流程不是一条线而是三条线在并发f_fs.c的初始化我初看的时候最容易被绕晕的地方在于它不是“一个函数从头执行到尾”的单一流程而是三条互相耦合的初始化线。第一条是文件系统侧模块加载时注册functionfs文件系统mount时创建ffs_data对象在VFS层准备好ep0节点。第二条是gadget侧通过configfs或者传统的usb_add_function把f_fs实例绑定到UDC控制器上执行ffs_func_bind此时才算真正拿到端点、分配请求。第三条是用户态侧用户程序打开ep0按FunctionFS ABI的要求先写设备/配置描述符再写字符串描述符一步步把状态从“描述符未就绪”推到“设备可用”。三条线之间有严格的等待关系但又不是简单的同步。比如Gadget侧可以先bind、用户态后写描述符也可以用户态先把描述符全部写完gadget侧再绑上来两种时序代码都接受。这就是为什么f_fs.c里大量使用状态标志、互斥锁和等待队列来协调三方进度。后面我拆解的时候会反复提到struct ffs_data里的状态标志它就是这三条线的“共享内存”。1.3 先记住这几个关键结构和函数后面不会迷路struct ffs_data整个FunctionFS实例的“总账本”状态机、设备描述符指针、端点文件数组、gadget引用全在这里。struct ffs_functionUSB gadget function的封装里面包含struct usb_function、指向ffs_data的指针、以及bind之后申请到的一组struct ffs_ep。ffs_mod_init模块入口注册文件系统和USB function。ffs_sb_fill挂载时填充超级块初始化ffs_data并创建ep0节点。ffs_ep0_write/ffs_ep0_read用户态与内核态交互的核心入口写描述符、读事件都在这里。ffs_func_bindgadget绑定阶段的初始化入口把function实例和实际端点接起来。后续所有内容基本都是围绕这几个函数和结构展开的。2. 模块加载阶段内核在 ffs_mod_init 里悄悄做的三件事2.1 注册文件系统让 mount 有法可依内核模块加载时会走到module_init(ffs_mod_init)。这个函数本身不长但它干的活一个比一个重要。第一件事就是register_filesystem(ffs_fs_type)把名叫functionfs的文件系统注册进内核VFS层。我们平时在板子上执行mount -t functionfs myfs /sys/kernel/config/usb_gadget/g1/functions/ffs.myfs/Android常见用法内核正是在mod_init阶段注册的ffs_fs_type才能识别这个文件系统类型。ffs_fs_type里的关键字段包括文件系统名、mount回调和umount回调等。mount回调在较新内核里指向ffs_fs_mount这个函数会负责构造一个struct file_system_type级别的挂载然后实际初始化工作交给ffs_sb_fill。这里有一个很容易忽略但很关键的细节FunctionFS的mount方式与普通文件系统不同它天然设计成需要以-t functionfs的方式挂载到一个由configfs创建的function目录上。为什么要这么设计因为一个gadget设备可以有多个USB function也就意味着可能有多个FunctionFS实例。这时每个实例需要有独立的ffs_data而mount就是创建独立实例的自然入口。同一个functionfs类型可以被mount到不同目录内核各自维护一份ffs_data。2.2 注册USB function让上层配置框架能找到它mod_init的第二件事是usb_function_register(ffs_function)。ffs_function在f_fs.c里是一个struct usb_function_type不同版本内核里类型名可能略有差异它的作用是描述“FunctionFS这一类USB function的能力”。注册进去之后configfs或者传统的legacy gadget框架才知道内核里有这么一类function才能在配置阶段为它分配实例、调用bind。打个比方如果你把USB gadget配置比作“组装一台电脑”usb_function_register就相当于向市场登记了“PowerFS这个品牌的内存条可以用”后面configfs像内存插槽一样构造function时就能从这个品牌里选一个已经登记过的型号。如果没有这一步后面无论configfs里怎么创建ffs.*节点内核都找不到对应的function实现。2.3 创建debugfs目录给排错留一个观察窗口mod_init最后还会创建debugfs相关目录具体路径一般是/sys/kernel/debug/usb_ffs/下面再按FunctionFS实例名分目录。这个目录在正常运行时内容很简单但它存在的意义是让开发者不用开ftrace就能快速确认某个FunctionFS实例是否注册成功。我在实际调试时经常先用ls /sys/kernel/debug/usb_ffs/看目录下有没有对应实例名来判断module加载是否正常、configfs的function节点是否创建成功。在早期内核版本里如果没开CONFIG_DEBUG_FS这段逻辑会被编译器剔除目录自然看不到这不代表functionfs不可用别把这个当故障误判了。mon_init阶段还有不少锁和缓存初始化但宏观上记住这三件事就够了注册文件系统、注册USB function、创建debugfs。这三样分别服务于后面挂载、bind、调试三条线。3. 挂载瞬间ffs_sb_fill 与 ffs_data 的完整出生过程3.1 从 mount 到 sb_fill 的调用链在Linux里mount -t functionfs ...最终会走到ffs_sb_fill。这个函数是初始化流程里第一个“大动作”它要完成超级块、根目录、ep0节点、ffs_data初始化的全套准备工作。标准的调用链是ffs_fs_mount - mount_nodev(ffs_sb_fill) - ffs_sb_fill(sb, desc, size)ffs_sb_fill会先取出struct ffs_data *ffs这个ffs要么是mount参数里带过来的已有实例在configfs绑定场景下往往从function配置处传入要么是刚通过ffs_data_new新建的。注意这里的“要么”背后就是那两种挂载时序一种是先mount再bind另一种是configfs创建function后function实例里已经持有了一个ffs_datamount时直接复用。这个函数的核心工作可以拆成三步初始化/补充ffs_data里的基础结构互斥锁、自旋锁、等待队列、引用计数等。设置超级块的操作函数集以及根目录inode。创建ep0节点并把ep0的dentry存在ffs-ep0里inode的私有数据指向ffs。3.2 ffs_data这个初始化流程里的“总账本”struct ffs_data是这个文件里最重要的结构初始化流程里几乎所有状态都沉淀在这里。理解它的字段基本就等于理解了初始化流程的一半。几个关键字段的关系是这样的state当前状态机位置决定了用户态现在可以干什么、内核期待什么。flags一些辅助标志位比如是否已经有人打开了ep0、是否有I/O在途等。ep0/epfilesep0文件dentry和端点文件数组。ep0在mount时创建而epfiles里的其它端点文件要等描述符就绪之后才会创建这是ABI特意设计的。ffs-descs/ffs-raw_descs/ffs-string_tab解析后的设备/配置描述符、原始描述符数据、字符串表。用户态写入的原始二进制就存在这里供bind时检索端点。ffs-gadget/ffs-func绑定的gadget和function实例bind时填入。实操时值得留意的是ffs_data里几乎所有字段都是随着初始化流程推进逐步填起来的没有任何一个函数能一口气把它全初始化完。我在代码里找初始化相关逻辑时如果只看ffs_data_new会漏掉一大半ffs_data_new只负责零初始化和一部分基础设施真正的关键字段都在ffs_sb_fill、ffs_ep0_write、ffs_func_bind这几个不同阶段填入。读代码时别指望“找到初始化函数就万岁”要顺着状态机一条链追下去。3.3 ep0 与 epfiles 的准备用户态的入口从哪来FunctionFS的用户态入口分两种文件ep0和ep。ep0在mount时由ffs_sb_fill创建它是用户态和内核态交互的控制通道。用户程序通过open(ep0)取得文件描述符然后write设备描述符、配置描述符、字符串描述符read事件对一个正常function来说ep0就是控制信息的必经之路。对应的file_operations是ffs_ep0_operations其中write接口ffs_ep0_write是状态机的主要驱动力。而epx这类端点文件在mount阶段并不会创建。它们的创建被推迟到了描述符解析完成、状态进入ACTIVE之后由ffs_epfiles_create批量创建。为什么这么晚因为只有读到用户态写下来的端点描述符内核才知道这个function到底配置了几个端点、每个端点是IN还是OUT、用了什么传输类型才能决定创建几个文件、每个文件的属性是什么。所以你去翻代码会发现ffs_epfiles_create的调用位置不在mount路径而在状态机推进到“描述符就绪”的路径上。对使用者来说这就意味着挂载成功不代表端点文件齐全。你要是脚本里写完mount立刻去ls找ep1大概率是空的必须等用户态程序完成描述符写入之后文件才会出现。很多初学FunctionFS的人在这个地方被绊倒过。4. 状态机驱动用户态写入才是真正让初始化“往下走”的引擎4.1 状态定义的完整图景f_fs.c里的初始化不是由内核单方面推进的而是用户态每往ep0写一段数据状态才向前跳一格。这些状态定义在include/uapi/linux/usb/functionfs.h中它们不仅是内核里的枚举也是用户态ABI的一部分。宏观上可以分成三组描述符准备阶段FFS_READ_DESCRIPTORS等待用户态写入设备/配置描述符、FFS_READ_STRINGS等待写入字符串描述符。设备可用阶段FFS_ACTIVE表示描述符和字符串都解析完成设备可以被bind和枚举。生命周期收尾阶段FFS_BOUND、FFS_CLOSING、FFS_DEACTIVATED等处理设备解绑、ep0关闭、错误中断等情况。对于初始化流程来说最核心的路径就是FFS_READ_DESCRIPTORS - FFS_READ_STRINGS - FFS_ACTIVE。这一条链全部由ffs_ep0_write驱动。4.2 第一步写 descriptors触发设备描述符解析用户态程序打开ep0后首先要做的不是读写业务数据而是按照FunctionFS ABI把一个包含魔数的数据块和一个完整的“设备描述符配置描述符集合”写进去。这里有个容易忽略的点FunctionFS设计了一套魔术字协议用来区分用户态写的是描述符段还是字符串段。每次write的前4字节是魔数内核会根据魔数确定当前数据段的类型再调用不同的解析函数。这个细节完美规避了接口歧义同样是往ep0写数据内核不用靠状态猜你写的是什么魔数本身就带着语义。描述符解析阶段的内核处理大致流程是用户态write(ep0, desc, len)进入ffs_ep0_write。解析魔数确定这是描述符数据调用__ffs_data_got_descs。逐个校验描述符设备描述符里类型、长度对不对配置描述符集合里的配置头、接口描述符、端点描述符是否齐全。解析完成后把原始描述符数据暂存到ffs-raw_descs解析出端点信息供后续创建ep文件时使用。状态切换到FFS_READ_STRINGS等待下一轮写入。如果中途任何一个描述符校验失败内核会返回错误状态机原地不动。这个阶段我踩过最多的坑是设备描述符里的bMaxPacketSize0写得和控制器能力不匹配或者配置描述符的wTotalLength统计少了端点描述符长度都会导致校验不过。4.3 第二步写 strings完成状态推进到 ACTIVE描述符写完后用户态还需要把字符串描述符写进去即使不需要字符串也必须写一个符合ABI的最小数据段把状态从FFS_READ_STRINGS推过去。这一步对应的内核函数是__ffs_data_got_strings它会解析字符串索引、语言ID和UTF-16编码的字符串内容存到ffs-string_tab里。字符串解析完成后状态迁移到FFS_ACTIVE紧接着内核会做几件收尾工作调用ffs_epfiles_create根据之前解析出的端点描述符批量创建ep1、ep2等设备文件。调用ffs_data_opened/ready相关的回调唤醒那些在等待设备就绪的用户态程序。把ffs_data标记为“可以对外提供端点服务”。到这一步用户态视角的初始化基本完成你可以去ls看到端点文件可以open它们收发数据。但注意这并不代表USB主机侧已经枚举成功枚举还依赖gadget侧的bind和UDC的状态也就是下一章要讲的内容。4.4 初始化完成后状态机还会继续变吗初始化完成后状态机并不就此静止。gadget bind之后状态可能进入FFS_BOUND主机和设备断开时可能进入FFS_DEACTIVATED用户态关闭ep0时进入FFS_CLOSING。理解初始化流程的价值就在于后面这些“非初始化”状态的处理逻辑全部依赖初始化阶段建立好的数据结构和引用关系。比如FFS_BOUND状态表示ffs_data已经和具体的USB function绑定在一起。这时候如果用户态想重新写描述符内核会严格拒绝因为设备已经运行起来了。反过来如果主机枚举所需的数据还没准备好bind可能成功而枚举却不成功握手会卡在FFS_ACTIVE到FFS_BOUND的转换边沿。这个状态机时序是排查“f_fs设备插上没反应”这类问题的核心地图。5. gadget 侧的 bind初始化流程的另一半拼图5.1 bind 的触发时机与前提前面说的都是用户态驱动的那条初始化线。真正让FunctionFS“接入USB总线”的是ffs_func_bind。它由gadget框架在device配置被激活时调用不管你是用configfs配好gadget后执行UDC绑定还是legacy USB gadget直接在驱动里usb_add_function最终都会走到这里的bind回调。bind的必要前提是configfs里已经有一个ffs.name类型的function节点并且mount的时候把对应的ffs_data关联到了这个function实例上。我在项目里看到的做法通常是在configfs创建gadget目录、配置目录。在functions/下创建ffs.myfunc得到function目录。mount -t functionfs myfunc /sys/kernel/config/usb_gadget/g1/functions/ffs.myfunc/。用户态程序打开这个目录下的ep0开始初始化交互。把所有function配置好之后把gadget关联到UDC控制器触发bind。第3步mount的作用本质上就是把ffs_data挂到第2步创建的function实例上让后续bind时能通过function实例找到ffs_data。顺序可以变但这个关联关系必须建立。5.2 bind 内部做了哪些初始化动作ffs_func_bind内部做的事情比名字看起来要多得多主要集中在“把逻辑端点描述符映射到真实端点”上。流程大致是通过function实例拿到ffs_data确认引用关系和状态合法性。遍历之前解析好的描述符里的端点信息对每一个逻辑端点调用usb_ep_autoconfig之类的接口从UDC控制器上申请一个物理端点。为每个申请到的端点分配struct ffs_ep初始化对应的请求缓冲区。分配并初始化用于USB控制传输的ep0请求setup请求这个请求用于处理主机的控制命令比如GET_DESCRIPTOR。把整理好的描述符集合、端点数组挂在struct usb_function的对应字段上方便后面的set_alt、disable等回调使用。如果你的FunctionFS设备里有多个接口比如一个CDC ECM再加一个自定义Bulk接口bind阶段会反复遍历配置描述符确保每个接口里的每个端点都被正确映射。这个阶段最常见的错误是端点数量超过UDC控制器的实际能力导致usb_ep_autoconfig失败返回NULL整个bind直接失败。5.3 bind 与用户态状态机的先后关系我前面反复强调这两条初始化线是解耦的。bind时ffs_data的状态可能是最初始的FFS_READ_DESCRIPTORS也可能是已经就绪的FFS_ACTIVE。两种情况内核都能处理如果bind时描述符还没写好ffs_func_bind会先完成所有物理端点映射但由于描述符还没解析完整一些依赖最终状态的操作会延迟到用户态写完之后执行驱动会通过等待队列和状态回调通知彼此。如果bind时描述符已经就绪ffs_func_bind会直接读取已经解析好的端点信息一步到位完成绑定。这个设计对实际部署很有意义开机阶段可以先把gadget全部配置好用户态程序晚点起来也不影响只要最终所有description都写好设备就能正常枚举。反过来用户态程序先跑起来写了描述符gadget再bind同样成立。理解了这种双向时序调试时起码不会一看到“先有鸡还是先有蛋”的顺序差异就慌。6. 初始化相关常见失败场景与排查实录6.1 ep0 打开失败或者 read 不到数据现象用户态程序openep0失败或者成功打开后read一直返回空/阻塞。先说open失败第一步不是看FunctionFS的代码而是确认挂载目录环境对不对。很多新手把mount的源目录和open目录搞混或者在gadget的function目录没有正确关联ffs_data的情况下强行open自然拿不到ep0。read不到数据则要看设备当前状态。ep0的read事件依赖设备和主机之间有控制传输交互如果gadget还没bind到UDC或者说UDC没有被真正关联主机侧根本看不到这个设备自然也就不会有任何控制请求过来。这种时候先查/sys/kernel/config/usb_gadget/g1/UDC是否有值。6.2 descriptor 校验一直过不去写描述符阶段返回-EINVAL十有八九是f_fs.c里内置的校验没过。这类问题排查我建议先写一个最小可用的描述符集合跑通后再加功能。最小集合里设备描述符和配置描述符都要字段完整wTotalLength必须和实际长度一致。我在实际项目里吃过一次亏配置描述符集合包含一个接口关联描述符IAD但我在构造时只算了接口描述符的部分没把IAD的长度算进去结果FunctionFS解析后多检查了好几层才发现长度不匹配。另一个常见问题是字符串段写法不对。FunctionFS对字符串段的数据格式很严格语言ID、字符串数量、字符串块长度都要按ABI来。建议直接参考Documentation/usb/functionfs.txt里附带的用户态示例去对照写别自己拍脑袋造格式。6.3 状态机卡在 READ_DESCRIPTORS 不动如果你在代码里调试或者打印日志发现状态一直停在FFS_READ_DESCRIPTORS大概率是ep0的write调用压根没走到“完整写一段描述符”的路径。常见原因有两个一是用户态程序只写了部分描述符就停了二是写入的数据缺少必要的魔数头被内核当成非法数据拒绝了而用户态又没检查write返回值错误被吞掉。这种问题最直接的定位方法是在ffs_ep0_write入口和__ffs_data_got_descs入口加打印对比一下write是否到达、解析是否成功。不要一上来就猜是状态机逻辑bug绝大多数情况是用户态协议栈实现不完整。6.4 排查初始化问题我惯用的三板斧第一板斧是看dmesg。f_fs.c里很多关键路径都有pr_debug或dev_dbg级别的日志如果内核开启了对应调试宏能非常直观地看到mount、bind、状态切换的时序。我在调试时一般会开dyndbg给f_fs.c单独开调试echo file drivers/usb/gadget/function/f_fs.c p /sys/kernel/debug/dynamic_debug/control第二板斧是用tracepoint看状态机。如果用的内核版本比较新USB gadget层已经有tracepoint重点关注usb_gadget、configfs和function相关的trace点能还原整套初始化时序图。没有tracepoint也可以用ftrace的function_graph直接把ffs_ep0_write、ffs_sb_fill、ffs_func_bind这几个函数挂上观察调用栈。第三板斧是写一个最小用户态复现程序。不要一上来就跑完整业务只做mount、open ep0、写一份最小描述符、写字符串然后看内核日志和文件节点变化。这样能把“FunctionFS初始化问题”和“业务逻辑问题”快速切分。我做USB自定义function时这种最小复现程序几乎就是调试期的标配成本很低但效率极高。最后再分享一个习惯调试FunctionFS初始化时尽量把用户态程序的write返回值、errno、以及每次write的字节数都打印出来。因为FunctionFS对用户态写入的时机和数据完整性非常敏感很多初始化失败不是内核bug而是用户态某个write少了4字节或者write被信号打断后没有重新提交完整数据。这些细节一旦在日志里暴露出来定位基本就是分钟级的事。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →