Linux下USB 3.0移动硬盘读写错误问题分析

为了解决 Chromebook 上自带 SSD 空间不足的问题,之前我在淘宝上购入了一个绿帆 F200 USB 3.0 移动硬盘盒,该硬盘盒使用的是 JMicron JMS567 这款使用还比较广泛的 SATA 6.0Gbps to USB 3.0 桥接芯片,准备配上 N 年前的神船笔记本淘汰下来的 2.5 寸硬盘给 Chromebook 使用,这样我就可以在 Chromebook 上安装的 Crouton Ubuntu 系统里无碍的使用各种编译开发环境了。

移动硬盘问题说明

首先我在宏碁 W700 Windows 10 平板上接上此移动硬盘建了一个 NTFS 分区并做各种读写拷贝大文件之类的测试没有任何问题;但是我把移动硬盘换到 Chromebook 上之后发现移动硬盘上新建的 ext4 分区读写会报错,无法做任何拷贝文件操作,先看看 Linux kernel 看到的 USB 3.0 移动硬盘的信息:

[ 9934.612465] usb 2-1: new SuperSpeed USB device number 2 using xhci_hcd
[ 9934.624741] usb 2-1: New USB device found, idVendor=152d, idProduct=0567
[ 9934.624768] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 9934.624787] usb 2-1: Product: lvfan USB3.0 storage
[ 9934.624802] usb 2-1: Manufacturer: JMicron
[ 9934.624817] usb 2-1: SerialNumber: 3224043AA81395
[ 9934.626473] scsi2 : usb-storage 2-1:1.0
[ 9935.627536] scsi 2:0:0:0: Direct-Access     lvfan                     0117 PQ: 0 ANSI: 6
[ 9935.629174] sd 2:0:0:0: [sdb] Spinning up disk...
[ 9936.629940] ...ready
[ 9938.634171] sd 2:0:0:0: [sdb] 312581808 512-byte logical blocks: (160 GB/149 GiB)
[ 9938.634552] sd 2:0:0:0: [sdb] Write Protect is off
[ 9938.634580] sd 2:0:0:0: [sdb] Mode Sense: 47 00 10 08
[ 9938.634955] sd 2:0:0:0: [sdb] Write cache: enabled, read cache: enabled, supports DPO and FUA
[ 9938.659685]  sdb: sdb1 sdb2
[ 9938.661548] sd 2:0:0:0: [sdb] Attached SCSI disk

这个是拷贝文件出错时候的内核日志:

[ 9910.412002] EXT4-fs (sdb2): mounted filesystem with ordered data mode. Opts: (null)
[ 9918.500515] sd 2:0:0:0: [sdb] Invalid command failure
[ 9918.500530] sd 2:0:0:0: [sdb]  
[ 9918.500537] Result: hostbyte=DID_OK driverbyte=DRIVER_SENSE
[ 9918.500547] sd 2:0:0:0: [sdb]  
[ 9918.500554] Sense Key : Illegal Request [current] 
[ 9918.500570] sd 2:0:0:0: [sdb]  
[ 9918.500577] Add. Sense: Invalid field in cdb
[ 9918.500589] sd 2:0:0:0: [sdb] CDB: 
[ 9918.500596] Write(10): 2a 08 04 58 18 30 00 00 08 00
[ 9918.500641] end_request: critical target error, dev sdb, sector 72882224
[ 9918.500654] end_request: critical target error, dev sdb, sector 72882224
[ 9918.500695] Aborting journal on device sdb2-8.
[ 9922.109415] EXT4-fs error (device sdb2): ext4_journal_start_sb:349: Detected aborted journal
[ 9922.109439] EXT4-fs (sdb2): Remounting filesystem read-only

看起来是报某个扇区写操作出错,那么我先在 Chromebook 上用 dd 命令直接写对应报错的扇区:

chronos@localhost ~ $ sudo dd if=/dev/zero of=/dev/sdb bs=512 count=10 seek=72882224
10+0 records in
10+0 records out
5120 bytes (5.1 kB) copied, 0.0185829 s, 276 kB/s

可以看到写之前报错的扇区其实是没有问题的,而且我使用的 SATA 笔记本硬盘之前经过 mhdd 扫描测试没有发现坏道之类的。

无奈我删掉这个 ext4 分区,重新创建了 ext3 分区并格式化拷贝文件但还是存在类似的错误,最后更换为 ext2 或者 FAT32 文件系统才能正常写入文件,这个就非常奇怪了,接着我把移动硬盘换到 Remix OS PC 版 4.0.9 kernel 下挂载 ext4 读写还是有一样的问题,这样我们就必须要具体分析上面的报错信息了。

读写错误问题分析

从上面的 kernel 日志中可以看到报错的 SCSI CDB 是:2a 08 04 58 18 30 00 00 08 00,而 0x2A 是 SCSI WRITE 10 命令,参考 SCSI 协议可以知道其命令格式为:

SCSI WRITE 10命令

我们可以从上图知道 04 58 18 30 是写操作的实际扇区地址(也就是上面报错的 72882224 扇区),08 是写命令的标志位表示这个写命令启用了 FUA

这里我对 FUA(Force Unit Access)做个简单的介绍:

  • 由于在 Linux 操作系统中从上层到下层多个地方都存在着缓存:page cache(vfs cache)-> HBA/RAID卡自带的 cache -> 磁盘自身的缓存,这样文件系统在写日志等关键的操作时就需要保证写入的数据被真实写到磁盘等物理介质中,而不是存留在各层 cache 里,这样可以防止系统掉电等情况导致数据不一致。

  • Linux kernel 中比较新的文件系统像 ext4、xfs、reiserfs 等则引入了 barrier 特性,遇到带 barrier 标志的写请求(或者 fsync 刷新请求)的时候必须保证之前的所有请求都已经写入到物理介质才能继续。老的 SCSI 或 SAS 磁盘支持通过 SYNCHRONIZE CACHE 命令刷新缓存数据,只不过这样会有额外的影响;新的 kernel 中对于支持 FUA 的设备则是通过发送带 FUA 标志的写请求来实现。

  • SCSI CDB 中的 FUA 标志位如果被置上则表示该请求必须通过访问物理介质实现,如果是带 FUA 的写命令就表示写到物理介质命令才算完成,带 FUA 的读命令则表示直接从物理介质读取绕过缓存。

这样分析下来基本就可以推断出原因了:

由于 ext4、ext3 等文件系统在 kernel 中是默认启用和日志(journal)和 barrier 的,拷贝文件时需要更新文件系统日志,从最上面 kernel 报上来的移动硬盘信息显示是支持 FUA 的,如此日志的写入就是通过发送带 FUA 的写命令实现,然而我的 USB 3.0 移动硬盘响应带 FUA 标志的写命令出现问题导致 ext4、ext3 文件系统变为只读状态无法写入;而 ext2 和 FAT32 文件系统则是完全不带日志的,这样拷贝文件反而就没有问题。

提示

有关我的移动硬盘使用的 JMicron JMS567 桥接芯片的详细信息请参考 JMicron 技术文档

解决方案

如果要顺利使用 ext4 分区,解决方案也有几种,一般的用户建议参考第二种解决方案,下面简单说明下:

禁用 ext4 文件系统日志

这是比较简单粗暴的解决方案,ext4 文件系统相对 ext3 其中一个改进就是可以手工关闭日志,先卸载已挂载的 ext4 文件系统,然后运行下面的命令:

sudo tune2fs -O ^has_journal /dev/sdb2

这样就可以直接关闭 ext4 文件系统默认启用的日志功能,日志被禁用之后基本就类似 ext2 不需要专门发送带 FUA 的写请求了;当然这里我不建议这样做,因为日志功能对于移动硬盘这种热插拔频繁的设备还是相当有用的,可以减少文件出错或丢失的可能。

关闭 ext4 文件系统 barrier

对于不愿意修改或者编译 Linux kernel 的用户这是比较好的解决方案咯,只是关闭 ext4 文件系统 barrier 特性可以保留最有用的日志功能,虽然相比默认启用 barrier 的情况仍然会有一点导致数据不一致的可能,但好处是修改之后就算把移动硬盘接到其它 Linux 机器上基本也能起作用,这样就比较划算了,具体运行下面的命令:

sudo tune2fs -o nobarrier /dev/sdb2

然后重新挂载 ext4 分区,一般的 Linux 系统都会在挂载该文件系统时自动禁用 barrier,Chromebook 上自带文件管理器的自动挂载磁盘功能实测有效。

修改 kernel 禁用移动硬盘 FUA

这种解决方案比较适合对编译 kernel 比较熟悉而且相对追求完美(^_^)的用户,不过相对的是需要依赖使用的 Linux 主机 kernel。

由于只禁用 ext4 文件系统的 barrier 还不能完全保证不会有什么别的地方需要用到带 FUA 的读写命令,如果能够修改 kernel 直接让我的这款移动硬盘设备报到系统中时就显示为不支持 FUA,这样就算遇到例如使用 barrier 等场合也可以通过 SYNCHRONIZE CACHE 命令来解决。

Linux kernel USB mass storage 驱动中内置了一个 unusual USB 设备列表,里面包含各种用户已经发现的有问题的 USB 设备及对应处理标志,该列表由 kernel 源代码中的 drivers/usb/storage/unusual_devs.h 文件负责维护。

我们可以先检查下 Chromebook 当前使用的 3.8.11 版本内核和 Remix OS PC 版使用的 4.0.9 版本内核源代码,却都能看到已经有用户报告的我的移动硬盘使用的 JMicron JMS567 芯片的 FUA 问题了:

UNUSUAL_DEV(  0x152d, 0x0567, 0x0114, 0x0114,
		"JMicron",
		"USB to ATA/ATAPI Bridge",
		USB_SC_DEVICE, USB_PR_DEVICE, NULL,
		US_FL_BROKEN_FUA ),

这个稍微有点奇怪,kernel 碰到带 US_FL_BROKEN_FUA 标志的 USB 设备会自动禁用 FUA,按说报上来的移动硬盘设备应该显示为不支持 FUA 的;不过稍微看下就发现上面的 UNUSUAL_DEV 条目限制了有问题的 USB 设备的 BCD 码固定为 0x0114,我们可以通过 lsusb -v 命令确认下移动硬盘设备的详细 USB 信息:

Bus 002 Device 004: ID 152d:0567 JMicron Technology Corp. / JMicron USA Technology Corp. 
Couldn't open device, some information will be missing
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               3.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0         9
  idVendor           0x152d JMicron Technology Corp. / JMicron USA Technology Corp.
  idProduct          0x0567 
  bcdDevice            1.17

可以看到移动硬盘的 USB 设备 BCD 码是 1.17 也就是 0x0117,这样 kernel 就不会认为我这款绿帆移动硬盘是有问题的 USB 设备进而做特殊处理了。

同样我们可以看看最新的 Linux 4.5-rc7 版本 kernel 中异常 USB 设备列表中的 JMS567 条目:

UNUSUAL_DEV(  0x152d, 0x0567, 0x0114, 0x0116,
		"JMicron",
		"USB to ATA/ATAPI Bridge",
		USB_SC_DEVICE, USB_PR_DEVICE, NULL,
		US_FL_BROKEN_FUA ),

稍微有点无奈最新的 Linux kernel 中的 BCD 码范围是从 0x0114 到 0x0116,还是不包含我这款移动硬盘。

解决步骤也就比较简单了,检出对应的内核源代码,修改 drivers/usb/storage/unusual_devs.h 文件可以把 JMS567 USB 设备的 BCD 码范围改为 0x0114 到 0x0117,也可以彻底点直接改成 0x0000 到 0x9999。

unusual_devs.h 文件修改完成后替换 usb-storage 模块并重新启动,顺利的话就可以看到报上来的 SCSI 磁盘设备显示为:

[ 7205.597952] sd 2:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA

提示

由于 Chromebook 和 Remix OS 中的 usb-storage 驱动都是直接集成在 kernel 中的,无法只编译 usb-storage 模块进行简单替换,因此需要完整编译出 kernel bzImage 直接替换。


这样 ext4 文件系统的日志还有 barrier 特性就能愉快的继续使用了,祝各位玩的开心~~~。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*