☰
从RMB位到dm-linear:USB存储设备固定为本地磁盘的工程实践
2026/11/1 1:09:07 网站建设 项目流程

简介:USB_LocalDisk 面向仍在使用 Windows XP、需要让普通 U 盘被系统识别为本地磁盘的技术用户,核心用途是突破可移动介质限制,从而在 XP 内对 U 盘进行多分区管理与测试。压缩包共 6 个文件,约 7KB,包含 C 源码、资源脚本 rc、驱动信息文件 inf、系统驱动文件 sys、makefile 及 sources 目录,覆盖从源码到驱动安装所需的完整结构,便于理解设备识别逻辑并按需调整。已有 723 人学习下载,说明其在老系统兼容、驱动修改与 U 盘分区场景中具有一定参考价值。读者可据此获得一套可直接研究的驱动样例、INF 配置范例与编译组织文件,用于学习 Windows 设备驱动识别、可移动磁盘与本地磁盘差异,以及旧平台下 U 盘多分区问题的排查思路。

1. USB_LocalDisk 的核心:把物理上可拔下的设备表达成本地磁盘

USB_LocalDisk 这个命名背后,是一个被反复追问的真实场景:移动硬盘插在工控机上,盘符明明出现了,业务软件却报「找不到本地磁盘」;授权模块按固定盘序列号绑定,U 盘一律被判定为可移动设备而拒绝加载。很多人第一反应是「USB 设备天生就是可移动的,改不了」,这个结论是错的。主机是否把一块盘当作可移动设备,取决于设备在 INQUIRY 命令里回报的一个标志位,以及系统存储栈怎么解释这个位。抓住这条线索,设备侧固件、主机侧映射层、文件系统层三个位置都能动手。这篇笔记面向需要把 USB 存储设备稳定接入业务系统的人,从协议字节讲到能直接跑的代码和脚本,也把几种看起来能用、实际会导致故障的做法说清楚。

2. 拆解 USB 大容量存储:从枚举到 CBW/CSW 的三层结构

2.1 插入之后到底发生了什么:接口类 0x08 与 BOT 协议

设备插入主机,主机先读设备描述符,再读配置描述符,逐层遍历接口。大容量存储设备的识别标志在接口描述符里:bInterfaceClass = 0x08 表示大容量存储类,bInterfaceSubClass = 0x06 表示使用 SCSI 透明命令集,bInterfaceProtocol = 0x50 表示 Bulk-Only Transport,也就是常说的 BOT。同一个 U 盘也可能同时暴露 0x62 的 UAS 接口,UAS 走 USB Attached SCSI,队列深度更高、吞吐更好,但抓包和调试的复杂度高一截。做兼容性优先的工具时,我一般锁定 BOT,原因是它的时序完全同步:一条命令发出去,等数据,等状态,三步走完才发下一条,出问题时你能清楚知道卡在哪一步。

BOT 的端点是两个 Bulk 端点,OUT 端点负责下发命令块和数据写入,IN 端点负责上行数据和状态。命令块叫 CBW,固定 31 字节;状态块叫 CSW,固定 13 字节。一次完整的传输是:主机把 CBW 写到 OUT 端点,设备解析出 SCSI 命令;如果这条命令带数据阶段,双方按 CBW 里声明的方向和长度搬运数据;最后设备把 CSW 写回 IN 端点,里面带着这条命令的成败状态和残余字节数。整个流程没有中断、没有异步回调,任何一步读错长度,后面的字节流就会错位,表现出来就是「命令发出去没反应」,这是新手最容易遇到的第一个故障。

为什么 BOT 里长度错位后果这么严重?因为 Bulk 端点本质上是字节流,没有消息边界。主机和设备的约定完全依赖 CBW 里的 dCBWDataTransferLength 和 CSW 里的 dCSWDataResidue 这两个字段来对齐。你声明要读 512 字节却只读了 36 字节,剩下的 476 字节还躺在设备的发送缓冲里,下一条命令的 CSW 就会被你当成数据读走,签名对不上,整个通道进入不可恢复的状态,只能重新插拔。所以写传输层代码时,每个长度字段都要逐字节核对。

2.2 CBW 的 31 字节与 CSW 的 13 字节:逐字段对照

协议文档里这两张表值得打印出来贴墙上。CBW 的所有多字节字段采用小端字节序,这一点和后面 SCSI 命令块内部的大端形成鲜明对比,是我见过最多人出错的地方。

偏移长度字段说明
04dCBWSignature固定 0x43425355,小端存储后字节序列为 55 53 42 43
44dCBWTag主机自增编号,CSW 必须原样带回
84dCBWDataTransferLength数据阶段字节数,无数据阶段填 0
121bmCBWFlagsbit7 为 1 表示数据从设备流向主机
131bCBWLUN低 4 位有效,单 LUN 设备填 0
141bCBWCBLength命令块有效长度,取值 1 到 16
1516CBWCBSCSI 命令块本体,不足 16 字节补 0

CSW 结构简单得多:前 4 字节签名固定 0x53425355,接着 4 字节是回带的 tag,再 4 字节是数据残余量,最后 1 字节是状态码。状态码 0 表示命令成功,1 表示失败、需要发 REQUEST SENSE 取具体错误码,2 表示相位错误,通常意味着主机在数据阶段的方向或长度和设备理解的不一致。看到状态 2 不要急着改代码逻辑,先回头核对 CBW 的 flags 位,十有八九是方向搞反了。

下面这段代码把一段原始 CBW 字节拆开,调试抓包时把它贴进脚本里能省很多时间:

import struct def parse_cbw(raw: bytes) -> dict: """把 31 字节 CBW 拆成可读字段,用于对照抓包结果""" assert len(raw) == 31, f"CBW 长度应为 31,实际 {len(raw)}" sign, tag, dlen, flags, lun, cblen = struct.unpack('<IIIBBB', raw[:15]) # 小端解析,与 USB 侧字节序一致 return { "signature": hex(sign), # 应为 0x43425355 "tag": tag, # 与 CSW 里的 tag 比对 "data_len": dlen, # 数据阶段期望字节数 "dir_in": bool(flags & 0x80), # True 表示设备到主机 "lun": lun & 0x0F, "cdb_len": cblen & 0x1F, "cdb": raw[15:15 + (cblen & 0x1F)].hex(), }

struct.unpack('<IIIBBB', ...)里的<强制小端且不做对齐填充,正好覆盖前 15 个字节。lun & 0x0F和cblen & 0x1F的掩码不能省,某些设备固件会把高位写成脏数据,不掩码就会算出长度为负或者 LUN 越界。cdb字段按实际有效长度截取,剩余补零部分不参与解析,这样对照抓包工具时能直接肉眼比对。

2.3 最小 SCSI 命令集:五条命令覆盖识别、容量与读写

SCSI 命令块里的大多数字段是大端,和 CBW 头部相反。这五条命令覆盖了从识别到读写的全部基础动作,先把它们跑通,再考虑写缓存、TRIM 之类的高级特性。

操作码命令CDB 长度作用
0x00TEST UNIT READY6探测设备是否就绪,不返回数据
0x03REQUEST SENSE6取上一条失败命令的具体错误码
0x12INQUIRY6取设备厂商、型号,以及关键的 RMB 位
0x25READ CAPACITY(10)10取最后 LBA 和块大小
0x28READ(10)10按 LBA 读块数据
0x2AWRITE(10)10按 LBA 写块数据
0x35SYNCHRONIZE CACHE(10)10把设备缓存刷到介质

INQUIRY 的分配长度字段是两字节大端,请求 36 字节标准响应就写 0x0024。READ CAPACITY(10) 的 LBA 字段全填 0,返回 8 字节,前 4 字节是最后可寻址 LBA,后 4 字节是块大小,两个都是大端。READ(10) 的传输长度字段是块数量而非字节数,这一点经常被搞混,按字节数填进去会读出几十倍的数据,然后通道直接堵死。REBOOT 之后设备侧缓存的那些元数据也会变,所以每次重新枚举都要重跑一遍容量查询,不要缓存上一次的结果。

选择 BOT 加这组命令的组合,还有个现实理由:几乎所有 U 盘、读卡器、移动硬盘盒的固件都完整支持这套最小集,兼容性风险最低。反过来,如果你追求吞吐去用 UAS 或者带队列的 SCSI,就必须处理乱序完成、多命令并发,调试成本翻几倍,而这条工具链的目标是「把盘认对、把数据读出来」,不是压榨 IOPS。

3. 用 PyUSB 手写 BOT 传输:读出一个扇区的完整链路

3.1 找到那个接口:按类 0x08、子类 0x06、协议 0x50 过滤

系统里有键盘、鼠标、串口设备,直接按第一个找到的设备操作必然出错。遍历所有 USB 设备,只看接口描述符三元组匹配的组合,才是一块存储设备。下面的函数返回设备对象和接口号,接口号在后续 claim 接口时要用:

import usb.core BOT_CLASS, BOT_SUBCLASS, BOT_PROTO = 0x08, 0x06, 0x50 def find_bot_devices(): """返回 [(设备对象, 接口号), ...],只保留 BOT 大容量存储接口""" found = [] for dev in usb.core.find(find_all=True): try: for cfg in dev: for intf in cfg: if (intf.bInterfaceClass == BOT_CLASS and intf.bInterfaceSubClass == BOT_SUBCLASS and intf.bInterfaceProtocol == BOT_PROTO): found.append((dev, intf.bInterfaceNumber)) except usb.core.USBError: continue # 某些设备的配置描述符读取会失败,跳过 return found

usb.core.find(find_all=True)会枚举总线上所有可访问设备,某些复合设备在读配置描述符时会抛出权限或管道错误,用 try 包住并跳过是常规做法,否则一个坏设备会让整个扫描中断。bInterfaceNumber要记下来,因为后面claim_interface必须传对,传错了抛出的错误信息很含糊,容易误判成设备不支持。

3.2 封装 CBW 发送与 CSW 校验

下面这段是传输层核心。三个方法分别负责发命令、读数据、验状态,把状态校验放在独立方法里是刻意的,因为状态码 1 和 2 的处理路径完全不同:1 要追发 REQUEST SENSE,2 要重置通道。

import struct import usb.util CBW_SIGN, CSW_SIGN = 0x43425355, 0x53425355 class BotDisk: def __init__(self, dev, intf_num, lun=0, timeout=5000, block=512): self.dev, self.lun = dev, lun self.timeout, self.block = timeout, block self.tag = 0 cfg = dev.get_active_configuration() intf = cfg[(intf_num, 0)] # (接口号, 备用设置) self.ep_out = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction( e.bEndpointAddress) == usb.util.ENDPOINT_OUT) self.ep_in = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction( e.bEndpointAddress) == usb.util.ENDPOINT_IN) usb.util.claim_interface(dev, intf_num) # 独占该接口 def _send_cbw(self, cdb, data_len, dir_in): self.tag = (self.tag + 1) & 0xFFFFFFFF flags = 0x80 if dir_in else 0x00 raw = struct.pack('<IIIBBB', CBW_SIGN, self.tag, data_len, flags, self.lun, len(cdb)) raw += cdb.ljust(16, b'\x00') # CDB 补齐到 16 字节 assert len(raw) == 31 self.ep_out.write(raw, self.timeout) return self.tag def _read_csw(self, tag): raw = bytes(self.ep_in.read(13, self.timeout)) sign, rtag, residue, status = struct.unpack('<IIIB', raw) if sign != CSW_SIGN: raise IOError(f'CSW 签名异常: {hex(sign)},通道已错位') if rtag != tag: raise IOError(f'CSW tag 不匹配: 期望 {tag} 实际 {rtag}') return status, residue def command(self, cdb, data_len=0, dir_in=True, payload=b''): tag = self._send_cbw(cdb, data_len, dir_in) data = b'' if data_len and dir_in: data = bytes(self.ep_in.read(data_len, self.timeout)) elif data_len and not dir_in: self.ep_out.write(payload, self.timeout) status, residue = self._read_csw(tag) return status, residue, data

timeout按毫秒计,5000 是个保守值,机械硬盘从休眠唤醒可能要几秒,设太短会误报超时。block默认 512,但 4Kn 盘要按 READ CAPACITY 返回的值改,这直接影响后面的读写长度。claim_interface在 Linux 上会触发内核驱动解绑,导致系统里原有的盘符消失,这是有意的副作用,工具用完要 release 掉。_read_csw里对签名和 tag 的双重校验不能省,只验签名的话,遇到复用 tag 的固件会漏掉错位。

3.3 读出 MBR 的 0x55AA 并核对容量

把三条命令串起来,就能确认这块盘到底是真能读,还是只是枚举成功。先 INQUIRY 拿型号,再 READ CAPACITY 拿几何信息,最后读 0 号扇区看引导签名:

def inquiry(self): cdb = bytes([0x12, 0x00, 0x00, 0x00, 0x24, 0x00]) # 分配 36 字节 status, _, data = self.command(cdb, 36, True) if status != 0: raise IOError('INQUIRY 返回失败状态') return data def read_capacity10(self): cdb = bytes([0x25] + [0x00] * 9) status, _, data = self.command(cdb, 8, True) last_lba, blk = struct.unpack('>II', data) # 大端 return last_lba + 1, blk # 总块数, 块大小 def read10(self, lba, count=1): cdb = struct.pack('>BBIHBB', 0x28, 0x00, lba, count, 0x00, 0x00) status, _, data = self.command(cdb, count * self.block, True) if status != 0: raise IOError(f'READ(10) 失败,LBA={lba}') return data

INQUIRY 返回 36 字节,第 0 字节是外设类型,0x00 表示直接访问块设备;第 1 字节的 bit7 就是后面要重点处理的 RMB 位;第 8 到 15 字节是厂商字符串,第 16 到 31 字节是型号字符串,都是空格右填充的 ASCII。>II的大端解析和 CBW 的小端形成对照,写代码时把这两处放在相邻位置看,能有效减少出错。

调通之后建议立刻做一次完整验证:用 read_capacity10 拿到的总块数乘以块大小,和系统里显示的容量比对,差值应该为零;再读第 0 块,检查最后两字节是不是 0x55 0xAA。两个检查都过,说明整条链路真的通了,而不只是设备枚举成功。这时候再去改 RMB 位或者做映射,才有意义。

4. 把可移动变成固定:RMB 位与三条落地路径

4.1 根源就在 INQUIRY 响应的第 2 个字节

主机判断一块盘是不是可移动介质,最直接的依据是 SCSI INQUIRY 标准响应里的 RMB 位。这个位在响应数据的第 1 字节(从 0 开始计数)的最高位,值为 1 表示可移动介质,值为 0 表示固定介质。绝大多数 U 盘固件把它置 1,移动硬盘盒固件通常置 0,所以同样是 USB 接口,移动硬盘在系统里往往显示为固定盘,U 盘显示为可移动盘。这不是 USB 协议规定的,是设备固件自己声明的。

响应偏移内容与本主题的关系
0外设类型,0x00 为块设备判断是否为磁盘
1bit7 = RMB 可移动位决定主机是否按可移动处理
8-15厂商标识,8 字节 ASCII授权绑定常用
16-31产品标识,16 字节 ASCII型号识别
32-35固件版本,4 字节 ASCII兼容性排查

用第 3 章的 inquiry 方法拿到 36 字节后,取data[1] & 0x80就能判断当前状态。这一步没有任何猜测成分,是判断「能不能通过改固件解决」的唯一入口。RMB 为 1 且设备固件可控,改固件最干净;RMB 为 0 但系统仍显示可移动,问题在主机侧驱动或策略层,改固件没用。这两条分支必须分开走,混在一起排查会浪费大量时间。

4.2 设备侧:把 mass_storage 的 removable 属性置 0

如果设备是自研的嵌入式板卡,用 USB 设备控制器模拟成 U 盘,那么 RMB 位由 gadget 驱动的一个属性直接控制。以 configfs 方式配置为例,核心是把removable写成 0,并挂一个后端文件或块设备作为存储介质:

#!/bin/bash set -e G=/sys/kernel/config/usb_gadget/usbdisk mkdir -p "$G" echo 0x1234 > "$G/idVendor" # 示例 VID,量产必须换成自己申请的 echo 0x5678 > "$G/idProduct" echo 0x0200 > "$G/bcdUSB" mkdir -p "$G/strings/0x409" echo "SN20240001" > "$G/strings/0x409/serialnumber" echo "Example Corp" > "$G/strings/0x409/manufacturer" echo "USB Local Disk" > "$G/strings/0x409/product" mkdir -p "$G/configs/c.1/strings/0x409" echo "cfg1" > "$G/configs/c.1/strings/0x409/configuration" mkdir -p "$G/functions/mass_storage.0" echo 0 > "$G/functions/mass_storage.0/removable" # 关键:声明为固定介质 echo 1 > "$G/functions/mass_storage.0/lun.0/ro" # 只读,防误写 echo /srv/disk.img > "$G/functions/mass_storage.0/lun.0/file" ln -sf "$G/functions/mass_storage.0" "$G/configs/c.1/" ls /sys/class/udc | head -n1 > "$G/UDC" # 绑定到第一个可用控制器

removable属性直接对应 INQUIRY 响应里的 RMB 位,写 0 之后主机拿到的就是固定介质声明,这是最正统的做法。ro置 1 表示后端介质只读,主机侧看到写保护,适合做配置下发盘;需要可写就去掉这一行。lun.0/file指向一个普通文件时,驱动会把它当成分区镜像导出,容量等于文件大小;也可以指向一个块设备节点,那种情况下要在绑定前确保该设备没被挂载,否则会报忙。

UDC那一行是把 gadget 绑定到具体的 USB 设备控制器,一个板子上可能有多个控制器,ls /sys/class/udc列出来的名字各不相同。绑定成功后主机端会重新枚举,几秒内出现盘符。如果绑定时报No such device,通常是控制器名字写错或者已经被别的 gadget 占用,先解绑再重试。

改完别急着收工,务必回主机上重新跑一次 INQUIRY,确认data[1] & 0x80变成 0。只看系统里显示「本地磁盘」不够,因为部分系统会缓存设备属性,显示和实际声明可能不一致,而业务软件读的往往是底层声明。

4.3 主机侧兜底:dm-linear 与 loop 把 USB 分区映射成固定块设备

设备固件改不了的时候,只能在主机侧做一层映射。Linux 上的思路很直接:USB 分区本身还是可移动的,但把它映射成一个 device-mapper 设备或者 loop 设备,这两个类型的块设备在 sysfs 里removable都是 0,上层软件看到的就是固定盘。

#!/bin/bash # 方案一:dm-linear 把 USB 分区原样映射成 dm 设备 DEV=/dev/sdb1 SECTORS=$(blockdev --getsz "$DEV") dmsetup create usbdisk --table "0 $SECTORS linear $DEV 0" cat /sys/block/dm-0/removable # 输出 0 # 方案二:把 U 盘上的镜像文件挂成 loop 设备 truncate -s 8G /mnt/usb/disk.img LOOP=$(losetup --find --show /mnt/usb/disk.img) cat /sys/block/$(basename $LOOP)/removable # 输出 0

dm-linear 是零拷贝映射,读写直接落到物理分区上,性能损耗极小,代价是 USB 一拔,dm 设备立刻进入错误状态,上层文件系统可能直接崩掉。loop 方案多一层文件系统,掉盘时镜像文件可能损坏,但恢复时至少还能从备份重建。两者都不是银弹,选哪个取决于业务能不能容忍掉盘,而不是性能。

Windows 侧没有这么轻量的等价命令。常见做法是写一个 StorPort 虚拟微端口驱动,创建固定类型虚拟磁盘,再把读写请求转发到 USB 设备;或者用镜像文件挂载成虚拟磁盘,把镜像放在 U 盘上。前者工作量大、要签名,后者多一层容器、掉盘风险更高。如果目标只是让某个软件认到固定盘,先确认它读的是磁盘类型还是卷序列号,很多时候把业务改成读卷序列号比做驱动划算得多。

5. 排查清单:枚举不到、容量错、写入掉盘的 5 个真实坑

5.1 现象:PyUSB 报设备忙,或者插上 U 盘后系统盘符直接消失

原因几乎固定:在 Windows 上,PyUSB 依赖 libusb 后端,要访问设备必须先把系统自带的大容量存储驱动替换成 WinUSB 或 libusbK,替换的瞬间设备从存储栈里消失,盘符自然没了;在 Linux 上则是claim_interface触发内核 usb-storage 驱动解绑,效果一样。

解决方式分两种场景。做只读分析时,优先在不影响业务的环境里操作,或者用设备侧抓包工具,别在正在跑业务的主机上抢接口。确实要在主机上跑工具,就在用完以后显式调用usb.util.release_interface和usb.util.dispose_resources,再触发一次重新枚举让系统驱动重新绑定。这条坑的本质是「一个接口同一时刻只能有一个驱动」,不是库的缺陷,提前设计好释放流程就不会翻车。

5.2 现象:CSW 状态码 1,READ(10) 反复失败

先别改命令参数,第一件事是发 REQUEST SENSE 把具体错误码取回来。常见的是 0x21 逻辑块地址越界,说明 LBA 算错了,或者用了 512 字节块去访问 4096 字节块的盘;另一个常见的是 0x28 介质已更换,多见于读卡器,插拔卡后没重新做 TEST UNIT READY。

请求感知的 CDB 是03 00 00 00 12 00,分配长度 18 字节。返回数据里第 2 字节是 sense key,第 12 字节是附加感知码,第 13 字节是附加感知码限定符。三个字节组合起来查表,能定位到具体原因。把这段逻辑封装成失败自动追查,比盲目重试有效得多。

5.3 现象:RMB 位已经改成 0,系统里还是显示「可移动磁盘」

原因有两层。第一层是系统缓存:设备属性在枚举时读取并缓存,改固件后必须完全断电重新插入,热重启不一定清得掉。第二层是驱动层:某些 USB 存储驱动的 INF 文件里把设备类型固定声明成可移动,这种情况下哪怕 INQUIRY 报 RMB 为 0,最终呈现仍然是可移动。

排查顺序是先确认 INQUIRY 的实际返回值,再去看系统里的设备属性。两者不一致,问题就在驱动或策略层,继续改固件是白费力气。这时候要么换驱动,要么改用 4.3 节的映射方案绕开。我见过最冤的一种情况是改了固件但没重新烧写,设备跑的还是旧镜像,对着代码查了半天。

5.4 现象:超过 2TB 的盘只能读到前面一小段

READ CAPACITY(10) 返回的最后 LBA 字段只有 4 字节,无符号 32 位,最大寻址到 2TiB(按 512 字节块算)。盘容量超过这个界限时,这条命令返回的是 0xFFFFFFFF,你按它算出来的总块数就是错的,读到边界就报错或者绕回。

解法是改用 READ CAPACITY(16),操作码 0x9E,服务动作码 0x10,返回 32 字节,最后 LBA 是 8 字节。读写命令同步换成 READ(16) 操作码 0x88 和 WRITE(16) 操作码 0x8A,LBA 字段 8 字节。切换判断条件很简单:READ CAPACITY(10) 返回的最后 LBA 等于 0xFFFFFFFF 就说明溢出了,必须降级到 16 字节版本。CDB 长度也从 10 变成 16,struct.pack的格式串要跟着改,这是最容易漏的一步。

5.5 现象:写完数据拔盘,文件系统报错或者文件内容残缺

设备侧有写缓存,WRITE(10) 返回成功只代表数据进了设备的内存缓冲,不代表落到了闪存颗粒上。主机侧以为写完了,直接断电或者拔线,缓冲区里的数据就丢了,文件系统元数据和实际数据对不上,轻则文件损坏,重则整个分区不可挂载。

解决方式是每次批量写完后显式发 SYNCHRONIZE CACHE(10),操作码 0x35,CDB 十字节全零即可。这条命令会阻塞到设备把缓存刷完才返回 CSW,耗时可能几百毫秒,但能换来数据安全。另一种做法是 WRITE(10) 的 CDB 第 1 字节 bit3 置 1,也就是 FUA 强制单元访问,每条写命令都等到落盘才返回,代价是写性能下降明显。批量写入用前者,关键少量数据用后者,按场景选。

6. 量产前怎么验:顺序写读比对与固定盘符规则

改完 RMB 位或者做完映射,只验证「系统显示本地磁盘」是不够的,那只是声明层面的变化。真正要验的是数据通路的完整性:整盘顺序写、整盘读回、逐块比对,再叠加一次非正常断电测试。我一般用下面这个脚本跑一遍,块大小取 1MiB,写到容量的九成,留出余量:

#!/bin/bash DEV=/dev/mapper/usbdisk BLK=1M TOTAL=$(blockdev --getsz "$DEV") LIMIT=$(( TOTAL / 2048 * 9 / 10 )) # 扇区数换算成 MiB 后取九成 for i in $(seq 0 $((LIMIT - 1))); do # 用块号派生的固定模式填充,便于读回时定位出错块 head -c 1048576 /dev/zero | tr '\0' "$(printf '\\%03o' $((i % 256)))" \ | dd of="$DEV" bs=$BLK seek=$i count=1 conv=fsync status=none done sync blockdev --flushbufs "$DEV" # 触发设备侧缓存刷新 for i in $(seq 0 $((LIMIT - 1))); do GOT=$(dd if="$DEV" bs=$BLK skip=$i count=1 status=none | od -An -tu1 -N1 | tr -d ' ') [ "$GOT" = "$((i % 256))" ] || echo "块 $i 校验失败,读到 $GOT" done

conv=fsync保证每个 dd 结束都刷一次页缓存,blockdev --flushbufs再补一次设备侧刷新,两级都做到才能保证读回的是介质上的真实内容。块号对 256 取模生成填充字节,读回时只需要看首字节就能定位到具体哪一块出错,比整盘哈希更容易定位问题区域。脚本跑完输出的失败行数应该为零,出现任何一行都说明这条链路在某个 LBA 区间有问题,通常是坏块或者映射偏移。

盘符稳定性是另一半工作。dm 设备的编号在重启后会变,直接写死/dev/dm-0迟早出事。用 udev 规则按 dm 名称建固定符号链接:

# /etc/udev/rules.d/72-usbdisk.rules KERNEL=="dm-*", ATTR{dm/name}=="usbdisk", SYMLINK+="usb_localdisk", MODE="0660"

ATTR而不是ATTRS,因为dm/name

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询