UFS逻辑单元管理,听起来像是一个只在芯片原厂FAE或者存储方案公司里才会被认真研究的冷门话题,但我在实际项目中遇到的工程师,十个里有八个都是被“为什么同一个UFS芯片,在不同板子上分出来的盘不一样”或者“为什么系统分区老是写坏”这类问题绊住之后,才开始回头补逻辑单元的课。UFS叫Universal Flash Storage,是JEDEC定义的通用闪存存储标准,手机上用得最多,平板、车载、服务器引导盘里也越来越常见。而逻辑单元(Logical Unit),是UFS设备内部能被主机端单独寻址和操作的存储实体,你平时看到的sda1、userdata分区,本质上都是逻辑单元在操作系统里的投影。
这篇内容我按自己的经验整理,从UFS设备的基本结构、逻辑单元的类型划分,到查询、配置、TRIM这些实操命令,最后再聊一些踩过坑才明白的排障思路。不管你是刚接触UFS的嵌入式工程师,还是在做存储方案选型的产品经理,应该都能在里面找到能直接用的东西。
1. UFS 逻辑单元是什么,为什么重要
1.1 UFS设备的基本构成
一个UFS设备,从物理上看就是一颗SoC加几片NAND闪存封装在一起。控制器负责FTL、坏块管理、ECC校验和磨损均衡,这些工作在设备内部完成,主机端根本不关心闪存物理块是怎么分布的。主机通过UFS主机控制器接口(UFSHCI)发出命令,经过UniPro和M-PHY物理链路到达UFS设备,整个数据交换过程由UPIU(UFS Protocol Information Unit,协议信息单元)来承载。UPIU里有一个LUN字段,专门指向目标逻辑单元。
很多人第一次看到UFS协议栈会觉得复杂,但换个角度想就通了:这一整套东西,本质上就是把一套SCSI命令集,塞进了为闪存而优化的通信链路里。UFS设备对外表现出的,是一块SCSI磁盘;磁盘内部又进一步划分出若干可独立操作的逻辑单元。主机没法直接访问闪存物理地址,只能通过逻辑单元的LBA块地址来读写。
1.2 从SCSI继承的“逻辑单元”概念
SCSI体系里有一种经典寻址模型:一个Target设备下可以挂多个LUN,主机通过LUN号区分不同的逻辑单元。UFS直接把这套东西继承了下来,只是把物理链路从并行SCSI换成了M-PHY,把传输协议改成了UPIU,命令集则沿用SPC/SBC标准的SCSI命令。所以,你如果会调试SCSI硬盘,那UFS的很多命令操作会让你感到熟悉,只是底层包了一层UFS特有的事务机制。
这也是为什么很多Linux工具,比如sg3_utils、lsscsi,可以直接用在UFS设备上。它们本来是为SCSI设备写的,但UFS在软件层面就是按SCSI磁盘的样子暴露给系统的。做嵌入式开发的朋友应该深有体会,板上跑起来之后,/dev/sda这个节点背后的设备,很可能就是一颗UFS存储芯片。
1.3 逻辑单元如何影响日常存储
我见过不少踩坑案例,最后都归结到没理解逻辑单元的分隔作用。第一,逻辑单元提供了天然的物理隔离能力,同一个UFS芯片可以切出系统区、用户数据区、缓存区、OTA区,区与区之间互不干扰。第二,不同逻辑单元可以设置不同属性,比如系统区设置为只读或永久写保护,用户区保持可写并开启TRIM,固件区设置为增强区域来保障随机写性能。第三,UFS支持多队列和并发访问,主机可以同时向不同LUN发出读写命令,合理规划逻辑单元,能让设备在多线程场景下跑出更好的实际带宽。
换句话说,逻辑单元不是简单的“分区表里的分区”,它背后关联着设备描述符、配置描述符、容量分配、性能属性和安全机制。管理好逻辑单元,才能在一个容量固定的UFS芯片上做出稳定、高效、可维护的存储方案。
2. UFS 逻辑单元的类型与属性配置
2.1 通用LU、引导LU与RPMB的职责划分
UFS设备里常见的逻辑单元分几类。通用LU最常见,就是给操作系统装数据用的,一般有多个,每个可以独立设置容量和属性。引导LU最多3个,早期规范里叫Boot LU A/B/C,主要放引导代码。手机在启动阶段,SoC从引导LU里读启动镜像,等内核接管以后再切到通用LU上的userdata分区。很多设备平时会把引导LU隐藏掉,防止系统或者误操作把引导数据擦掉。
RPMB(Replay Protected Memory Block)是一种带安全认证功能的逻辑单元,用来存钥匙、计数器这类敏感数据。它靠HMAC签名和重放计数机制保证写操作来自合法主机,读操作也要经过认证,所以RPMB不是随便发个READ命令就能读的。如果你在项目里碰到RPMB报错,多半不是存储芯片坏了,而是主机侧的安全密钥或计数器状态没对齐。
除了这三种,有些厂商还会把固件更新区单独做成一个LU,让升级固件时不会碰到用户数据区。所以你要是真拿到一颗UFS设备,请先别急着往里写数据,把它的LU类型和分布梳理清楚再动手,省得后面给自己挖坑。
2.2 LU描述符与配置描述符
每个逻辑单元都有自己的描述符,叫做LU描述符(Unit Descriptor),里面记录着LUN号、LU类型、逻辑块大小、总块数(容量)、写保护状态、增强区域属性等信息。而整个设备层面还有一个配置描述符(Configuration Descriptor),里面定义了设备启用了哪些LU、每个LU的起始LBA范围、大小和类型。这两个描述符,可以说是逻辑单元管理的“总纲”。
我每次拿到新UFS设备,第一件事就是读取配置描述符,把里面的字节流解析成表格。你会看到设备版本、每个LU的使能位、起始地址、容量、类型代码。这一步能避免很多低级错误,比如两块LU容量重叠,或者某个LU根本没启用但系统还在尝试挂载。配置描述符是可以写的,修改之后设备会重新加载配置,但这个过程必须配合设备复位或重新上电,否则新配置不生效。
2.3 写保护、增强区域等实用属性
逻辑单元的属性里,有几个在实际项目里非常关键。
写保护属性分两种:永久写保护和电源周期写保护。永久写保护一旦设置就没办法取消,适合出厂固件、密钥区;电源周期写保护是断电后失效,适合临时要防误写的场景。增强区域(Enhanced Area)则是一些UFS设备支持的容量类型,它内部可能针对随机写做了优化,适合放需要频繁更新的数据。很多方案把用户数据区、应用缓存区设成增强区域,把系统区、备份区放在普通区,这样既保证体验又控制成本。
逻辑单元还有块大小这个属性,常见的是4096字节逻辑块。这个大小直接影响主机侧命令的参数计算,比如LBA和传输长度的单位。很多人在调读写时发现“地址怎么对不上”,其实就是因为把逻辑块大小默认当成了512字节去换算。拿到设备后,第一步还是老老实实读一遍每个LU的属性,别猜。
3. LU 管理实操:识别、查询与命令
3.1 系统层面识别UFS设备的逻辑单元
在Linux系统里,UFS设备初始化成功后,dmesg里会出现与ufshcd类似的日志,然后系统会注册一个SCSI磁盘设备。用lsscsi或者sg_scan查看,能看到厂商名、产品名、固件版本,还有对应的LUN编号。要是UFS设备里有多个通用LU,系统可能就会注册出多个块设备,也可能是一个块设备带多个分区,具体取决于配置描述符和分区表。
Android手机上更常见的是用adb方式查看。连进设备后,在/sys/block/下能看到sda、sdb等节点,再往下翻device目录,能读到UFS相关的型号和LUN信息。厂商调试工具一般也会提供读配置描述符和LU属性的入口,这些工具在量产测试阶段几乎是必需品。真要手动操作,Linux下的sg3_utils套装就是最趁手的利器。
3.2 查询与配置:QUERY REQUEST 使用流程
UFS协议里,专门有一个叫Query Request的机制,用来读写描述符、属性和标志位。想了解设备支持哪些LU、每个LU什么情况,就靠它。
查询逻辑单元信息的基本流程是这样:
- 主机侧构造一个Command UPIU,命令操作码设为Query Request。
- 在查询请求中指明查询功能(比如Read Descriptor)、描述符类型(比如Configuration Descriptor或Unit Descriptor)、目标LUN。
- 把UPIU发到UFS设备,设备处理完返回Response UPIU,数据内容在Data In阶段带回来。
- 把返回的字节流按描述符格式解析出来,得到各个LU的容量、类型、属性。
读取只是第一步,要是想改配置,使用Write Descriptor功能,把修改后的整段描述符写回设备。写完后一定记得让设备复位或者重新上电,让固件重新加载配置。我第一次改配置描述符时,改完没复位就急着读写,结果设备返回的全是保留值,折腾了半天才发现是没复位。这个坑,希望你们别踩。
3.3 日常管理常用SCSI命令集合
下面这张表是我在调试UFS设备时最常用到的SCSI命令,都和逻辑单元管理有关。
| 操作目标 | SCSI命令 | 典型用途 |
|---|---|---|
| 基础识别 | INQUIRY | 获取厂商、产品型号、固件版本 |
| 容量查询 | READ CAPACITY(10/16) | 获取某LU的逻辑块数量和块大小 |
| 数据读写 | READ(10)、WRITE(10) | 按LBA对某个LU执行读写 |
| 缓存同步 | SYNCHRONIZE CACHE(10) | 把整机缓存中的数据刷到介质 |
| 空间释放 | UNMAP | 对应TRIM操作,标记无效LBA |
| 设备启停 | START STOP UNIT | 让设备进入/退出低功耗状态 |
READ CAPACITY有个细节值得提,它返回的“最后一块LBA”和“块大小”是后续所有读写的基础。换算地址时,LBA从0开始算,实际可寻址范围是0到返回的LBA值。如果你看到某个LU的容量只有标称值的一半,先别怀疑芯片缩水,多半是有其他LU占用了额外空间,或者Boot/RPMB区域被误映射了出来。
3.4 一次完整的TRIM/UNMAP操作过程
很多人在搜“UFS有没有TRIM命令”的时候,其实问的就是UNMAP。UFS设备是支持TRIM的,它对应的SCSI命令就是UNMAP。操作系统层面的fstrim,最终也是通过UNMAP把不再使用的LBA告诉UFS设备。
我来拆解一次UNMAP操作的完整流程。假设我们的通用LU块大小是4096字节,要释放的逻辑块范围是从LBA 1024开始的128个块。
构造UNMAP命令时,CDB里需要带上参数块长度,后面是Unmap LBA表,表里每条记录包含起始LBA和块数量。具体到字节上,一个简化的例子是这样的:
操作码: 0x42 分配长度: 本命令参数块总字节数 Unmap LBA记录: 起始LBA: 0x0000000000000400 块数量: 0x00000080主机把这条命令封装成Command UPIU发下去,UFS设备根据这些LBA信息把对应逻辑块标记为可回收。之后设备的垃圾回收机制会把这些块释放出来,变成后续写入可用的空闲块。
从实际维护角度看,如果你想给Linux系统下的UFS分区执行TRIM,直接使用fstrim -v /挂载点就能触发。跑一次之后,可以看到它报告释放了多少字节。要是这个值长期接近0,说明文件系统层可能没怎么产生无效数据,或者驱动没有正确透传UNMAP。生产环境中,UFS设备的TRIM策略和GC策略配合得好不好,直接影响长期写入性能和寿命,值得专门压测。
4. 逻辑单元管理常见问题与排查技巧
4.1 逻辑单元识别不到或容量显示异常
这一类问题在项目中遇到得最多,原因通常是几个方向。
首先检查UFS设备有没有成功初始化,链路层没训练成功时,主机根本看不到任何LUN,这个问题和逻辑单元本身关系不大,更多在PHY、供电和参考时钟上。其次是看配置描述符里每个LU的使能位和容量字段,是不是把引导LU和RPMB也算进通用容量了。我见过一台设备的userdata分区始终比预期小2GB,查到最后发现是配置描述符里某块区域被显式映射给了一个隐藏LU,导致通用LU的实际可用空间被压缩。
还有一个容易忽略的点:某些UFS设备在出厂时默认只对外开放一个或多达几个通用LU,真要开放更多LU,需要修改配置描述符并复位设备。系统侧如果找不到第二个LU,先确认硬件设备是否真的把一个以上的LU映射出来了。
4.2 写性能下降与GC/TRIM的关系
写性能越来越差,是UFS使用一段时间后的常见现象。硬件上闪存的垃圾回收(GC)如果长期压力大,写入延迟就会明显升高。这里的核心逻辑是:文件系统删除文件后,对应LBA如果没有通过UNMAP告知UFS设备,设备内部就不知道这些页已经无效,后续GC还要把它们搬来搬去,白白增加写放大,拉低性能。
解决办法就是定期执行TRIM。Android系统里其实有后台的fstrim机制,很多Linux发行版也有每周定时任务。但有些定制系统为了省电,把这个服务关掉了,就会出现在持续使用一段时间后越来越卡的情况。项目阶段压测时,可以对比开TRIM和不开TRIM两轮长时间写测试,通常能明显看到不同。
另外,在GC压力峰值期间,如果主机还在发大量写请求,部分UFS固件会返回BUSY或者需要主机重试。日志里如果反复出现命令超时或重试,最好给GC留一点空闲时间,或者调整固件的GC策略参数,具体能调什么取决于厂商开放程度。
4.3 RPMB安全单元使用失败的排查思路
RPMB这块报错,通常不是芯片坏了,而是主机侧安全上下文没准备好。最常见的情况是RPMB里的密钥没有写进去,主机在执行写操作时返回认证失败。RPMB密钥的写入是一次性的,需要在安全启动流程或产线初始化阶段完成,之后主机侧和芯片侧要用同一个密钥来算HMAC。
RPMB读操作也会校验MAC,如果主机侧的counter值和设备侧不一致,就会出现计数器校验错误。多次认证失败会导致设备侧的验证尝试计数增长,有些设备还会因此在等待一段时间后才能重试。遇到这类问题时,我的排查顺序是:确认密钥是否已经写入、确认nonce和counter是否单独累加、确认MAC计算方式是否符合规范。RPMB不像普通LU,直接发个命令就能看到数据,必须先建立信任关系。
4.4 不同操作系统与平台的实现差异
同样一颗UFS芯片,在Linux、Android、Windows和车机平台上的表现方式差别很大。Linux和Android基本走SCSI块设备层,可以通过sg3_utils、fstrim这些标准工具来操作。Windows对UFS的通用支持没那么标准,主板上挂UFS设备的情况也比较少,更多是从USB读卡器或转接器去访问,能看到的命令集取决于驱动实现。
车机或者服务器平台,UFS往往用来当启动盘或者关键数据盘,固件升级流程和相关工具一般是厂商定制。不同平台间最需要注意的是LU编号和分区表映射关系。Android通常用GPT分区表直接管理通用LU,而有些嵌入式系统会绕过分区表,直接按固定LBA操作。换平台后如果沿用旧的上层软件,很容易出容量和地址错位的事故。
5. 一些踩坑后总结的实操建议
先说一个我自己的习惯:拿到一块UFS设备,不管新板子还是返修板,先完整读一遍配置描述符和各个LU的描述符,把它们整理成表格放在项目文档里。这一步看着麻烦,但能省下后面好几天的定位时间。毕竟设备实际呈现在系统里的样子,完全取决于这些描述符里的内容。
修改配置描述符之后,记得一定要执行设备复位或重新上电,否则新配置可能还是旧状态在起作用。我一度以为UFS不支持修改LU配置,后来发现自己只是少了复位这一步。量产阶段如果每个板子都要重配LU,还需要确认一遍配置写入和固化流程,防止个别板子配置没保存成功就流入后续测试。
不要把引导LU或者RPMB当成普通存储区随意写入。引导LU被写坏,系统直接起不来;RPMB密钥被覆盖,安全启动和支付功能都会崩,这不是通过软件能轻易修复的。尤其是RPMB,一旦密钥初始化异常,有些主控要换片才能恢复。所以做测试脚本时,务必把这两个区域排除在随机写和压力写之外。
TRIM的启用不能只看系统里有没有fstrim命令,还要确认UFS驱动真的把UNMAP命令透传下去了。用systrace或者厂商日志观察,能确认LBA释放请求是不是发到了设备侧。多轮压测后再看写入性能,是最直接的验证方式。
最后再分享一点:UFS逻辑单元管理不难,难的是把规范里的描述符字段和实际板子的表现对应起来。多备份几份不同厂商、不同容量UFS设备的配置描述符做对比,你会发现规律非常明显。我在实际项目里就是靠这些对比,快速判断出某个逻辑单元到底是容量被吃掉了,还是属性配置错了。这套方法,试过就知道多好用。