放置9年的SD卡會丟失資料嗎?

答案:會

找到一張十多年前的SD卡,不知道里面儲存的資料是否完好,先插上看看。

這是什麼卡?

$ system_profiler SPCardReaderDataType
Card Reader:

    Built in SD Card Reader:

      Vendor ID: 0x17a0
      Device ID: 0x9755
      Subsystem Vendor ID: 0x17a0
      Subsystem ID: 0x9755
      Revision: 0x0001
      Link Width: x1
      Link Speed: 5.0 GT/s

        SDHC Card (Class 10):

          Product Name: SDSL16G
          Manufacturer ID: 0x03
          Revision: 8.0
          Serial Number: 0x2587b27b
          Manufacturing Date: 2014-04
          Specification Version: 3.0
          Capacity: 15.93 GB (15,931,539,456 bytes)
          Removable Media: Yes
          BSD Name: disk4
          Partition Map Type: MBR (Master Boot Record)
          S.M.A.R.T. status: Verified
          Volumes:
            NO NAME:
              Capacity: 15.93 GB (15,929,966,592 bytes)
              File System: MS-DOS FAT32
              BSD Name: disk4s1
              Content: Windows_FAT_32
              Volume UUID: 60CDB9CD-9285-3884-BF60-8D9D3D90D7C2

生產日期2014-04,大機率是MLC顆粒,最後一次使用是2017年初。12年前生產,又吃了9年的灰之後,資料還完好嗎?

先嚐試直接在桌面裡把東西拷出來。

Finder會失敗,需要嘗試資料恢復了。

錯誤的嘗試

一開始直接使用了dd,但並不合適,因為dd在遇到錯誤時的策略不符合我們的要求:

$ diskutil unmount /Volumes/NO\ NAME/ # 先移除disk4上的所有掛載點
Volume NO NAME on disk4s1 unmounted

$ sudo gdd if=/dev/rdisk4 of=card.img bs=1M status=progress conv=noerror,sync
Written: 295 MB   30 MB, 315 MiB) copied, 9 s, 36.6 MB/s
gdd: error reading '/dev/rdisk4': Operation timed out
327+0 records in
327+0 records out
342884352 bytes (343 MB, 327 MiB) copied, 35.5718 s, 9.6 MB/s
Written: 374 MB   97 MB, 379 MiB) copied, 37 s, 10.7 MB/s
gdd: error reading '/dev/rdisk4': Operation timed out
402+1 records in
403+0 records out
422576128 bytes (423 MB, 403 MiB) copied, 63.8949 s, 6.6 MB/s
Written: 582 MB   16 MB, 587 MiB) copied, 102 s, 6.0 MB/s
gdd: error reading '/dev/rdisk4': Operation timed out
608+2 records in
610+0 records out
639631360 bytes (640 MB, 610 MiB) copied, 128.827 s, 5.0 MB/s
664797184 bytes (665 MB, 634 MiB) copied, 130 s, 5.1 MB/s
gdd: error reading '/dev/rdisk4': Operation timed out
631+3 records in
634+0 records out
...(餘下略)

此處使用GNU dd。conv=noerror,sync遇到讀取錯誤時不中斷,用零填充該塊之後繼續。

超時出現了40多次,每個超時的1MB塊都會被全部替換成全零的1MB。每個被丟棄的1MB中也許只有512B是真正損壞的,這樣的粒度顯然讓人無法接受。

使用ddrescue

這種情況下應該使用ddrescue,它會嘗試使用多種方式讀取,儘可能恢復資料:

$ sudo ddrescue -b512 /dev/rdisk4 card_rescue.img card_rescue.mapfile
GNU ddrescue 1.30
Press Ctrl-C to interrupt
     ipos:  859242 kB, non-trimmed:   524288 B,   current rate:    609 kB/s
     opos:  859242 kB, non-scraped:        0 B,   average rate:   2768 kB/s
non-tried:    9223 PB,  bad-sector:        0 B,     error rate:       0 B/s
  rescued:  858193 kB,   bad areas:          0,       run time:      5m 10s
pct rescued:    0.00%, read errors:          8, remaining time:         n/a
                               time since last successful read:          0s
Copying non-tried blocks... Pass 1 (forwards)^C
  Interrupted by user

macOS使用/dev/rdiskX代表訪問raw disk,繞過緩衝區。若在Linux上使用,請在ddrescue命令之後新增-d引數來繞過緩衝區。

512位元組是SD卡的最小粒度,-b512讓ddrescue按這個單位逐扇區重試,一個扇區讀不出來只會丟失512位元組,不會波及相鄰扇區。

有點問題,9223 PB代表它認為磁碟無限大,手動指定上限:

$ diskutil info /dev/disk4 # 先找到卡容量的精確值
...
   Disk Size:                 15.9 GB (15931539456 Bytes) (exactly 31116288 512-Byte-Units)
...

$ sudo ddrescue -b512 -s 15931539456 /dev/rdisk4 card_rescue.img card_rescue.mapfile
GNU ddrescue 1.30
Press Ctrl-C to interrupt
Initial status (read from mapfile)
(sizes limited to domain from 0 B to 15_558_144 KiB of 9_223_372_036_854_775_807 B)
rescued: 1125 MB, tried: 786432 B, bad-sector: 0 B, bad areas: 0

Current status
     ipos:   15931 MB, non-trimmed:    2752 kB,   current rate:  36700 kB/s
     opos:   15931 MB, non-scraped:        0 B,   average rate:   9239 kB/s
non-tried:    2752 kB,  bad-sector:        0 B,     error rate:       0 B/s
  rescued:   15926 MB,   bad areas:          0,       run time:     26m 39s
pct rescued:   99.96%, read errors:         30, remaining time:          1s
                               time since last successful read:          0s
Copying non-tried blocks... Pass 1 (forwards)
     ipos:  343277 kB, non-trimmed:    2752 kB,   current rate:   80659 B/s
     opos:  343277 kB, non-scraped:        0 B,   average rate:   9142 kB/s
non-tried:        0 B,  bad-sector:        0 B,     error rate:       0 B/s
  rescued:   15928 MB,   bad areas:          0,       run time:     26m 59s
pct rescued:   99.98%, read errors:         30, remaining time:          1s
                               time since last successful read:          0s
Copying non-tried blocks... Pass 2 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:    2969 B/s
     opos:    1983 MB, non-scraped:    33792 B,   average rate:   4393 kB/s
non-tried:        0 B,  bad-sector:    38912 B,     error rate:      25 B/s
  rescued:   15931 MB,   bad areas:         72,       run time:     56m 10s
pct rescued:   99.99%, read errors:        106, remaining time:         12s
                               time since last successful read:          0s
Trimming failed blocks... (forwards)          
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:   3442 kB/s
non-tried:        0 B,  bad-sector:    66560 B,     error rate:       0 B/s
  rescued:   15931 MB,   bad areas:         48,       run time:  1h 11m 41s
pct rescued:   99.99%, read errors:        160, remaining time:          0s
                               time since last successful read:      1m 45s
Scraping failed blocks... (forwards)
Finished

ddrescue允許Ctrl-C打斷,下一次重啟時會按照mapfile中的進度繼續。

ddrescue一輪結束,壞塊分散在48個不同位置,一共丟失正好65KB資料。這張卡實際只使用了2GB空間,而這48個壞塊全部分佈在這2GB有效資料上,一個可能的解釋是,這張卡從一開始就只寫過這麼一次,剩餘的容量從來沒有被寫入過,未寫入的區域仍處於快閃記憶體的擦除態(全0xFF),浮柵中沒有電荷可以洩漏,因此不會出現資料衰退。

算一下損壞率:65 KB / 2 GB = 3e-5,大約丟失0.003%。

我不確定這個數字是否符合MLC的預期,但說實話,我覺得這個數字沒有什麼很大的意義,丟失了就是丟失了,丟失了和沒丟失有本質的區別。

探索極限

有一些塊處於臨界狀態,多重複幾次有機率能讀出。

繼續讀其實沒有什麼很明顯的意義,對這張卡而言,每多讀一次都在read disturb,臨界狀態的塊會越來越不穩定,但我仍然想探索它的極限。

使用-r引數來指定重試次數,例如-r3重試三次:

sudo ddrescue -r3 -b512 -s 15931539456 /dev/rdisk4 card_rescue.img card_rescue.mapfile
GNU ddrescue 1.30
Press Ctrl-C to interrupt
Initial status (read from mapfile)
(sizes limited to domain from 0 B to 15_558_144 KiB of 9_223_372_036_854_775_807 B)
rescued: 15931 MB, tried: 66560 B, bad-sector: 66560 B, bad areas: 48

Current status
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       3 B/s
non-tried:        0 B,  bad-sector:    59392 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         44,       run time:     31m 21s
pct rescued:   99.99%, read errors:        116, remaining time:      1h 50m
                               time since last successful read:      2m  5s
Retrying bad sectors... Retry 1 (forwards)
     ipos:  343216 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  343216 kB, non-scraped:        0 B,   average rate:       3 B/s
non-tried:        0 B,  bad-sector:    53248 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         35,       run time:     58m 47s
pct rescued:   99.99%, read errors:        220, remaining time:      1h  8m
                               time since last successful read:      3m 35s
Retrying bad sectors... Retry 2 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       3 B/s
non-tried:        0 B,  bad-sector:    50688 B,     error rate:       0 B/s
  rescued:   15931 MB,   bad areas:         31,       run time:  1h 24m 28s
pct rescued:   99.99%, read errors:        319, remaining time:      7h  2m
                               time since last successful read:     14m  5s
Retrying bad sectors... Retry 3 (forwards) 
Finished

損壞資料減少到了約50KB,那麼,如果我們再繼續下去呢?能否收斂到一個“極限”?

當然,這麼做並不會讓奇蹟發生,我就是想測出一個實驗結果,以供後人參考。

來,再重複20次,接下來這段log會有點長:

$ sudo ddrescue -r20 -b512 -s 15931539456 /dev/rdisk4 card_rescue.img card_rescue.mapfile
GNU ddrescue 1.30
Press Ctrl-C to interrupt
Initial status (read from mapfile)
(sizes limited to domain from 0 B to 15_558_144 KiB of 9_223_372_036_854_775_807 B)
rescued: 15931 MB, tried: 50688 B, bad-sector: 50688 B, bad areas: 31

Current status
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       1 B/s
non-tried:        0 B,  bad-sector:    48128 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         29,       run time:     24m 14s
pct rescued:   99.99%, read errors:         94, remaining time:      4h 27m
                               time since last successful read:     10m 30s
Retrying bad sectors... Retry 1 (forwards)
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       1 B/s
non-tried:        0 B,  bad-sector:    46080 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:     47m 20s
pct rescued:   99.99%, read errors:        184, remaining time:      2h 33m
                               time since last successful read:      3m  5s
Retrying bad sectors... Retry 2 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       1 B/s
non-tried:        0 B,  bad-sector:    45056 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  1h  9m 41s
pct rescued:   99.99%, read errors:        272, remaining time:     12h 30m
                               time since last successful read:      4m 35s
Retrying bad sectors... Retry 3 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       1 B/s
non-tried:        0 B,  bad-sector:    44544 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  1h 31m 32s
pct rescued:   99.99%, read errors:        359, remaining time:         n/a
                               time since last successful read:          8m
Retrying bad sectors... Retry 4 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    44032 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  1h 53m  8s
pct rescued:   99.99%, read errors:        445, remaining time:         n/a
                               time since last successful read:      5m 31s
Retrying bad sectors... Retry 5 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    43520 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  2h 14m 34s
pct rescued:   99.99%, read errors:        530, remaining time:     12h  5m
                               time since last successful read:      8m 21s
Retrying bad sectors... Retry 6 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    43520 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  2h 35m 49s
pct rescued:   99.99%, read errors:        615, remaining time:         n/a
                               time since last successful read:     29m 36s
Retrying bad sectors... Retry 7 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    43008 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  2h 57m  5s
pct rescued:   99.99%, read errors:        699, remaining time:         n/a
                               time since last successful read:     15m  6s
Retrying bad sectors... Retry 8 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    43008 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  3h 18m  6s
pct rescued:   99.99%, read errors:        783, remaining time:         n/a
                               time since last successful read:     36m  7s
Retrying bad sectors... Retry 9 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    43008 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  3h 39m  7s
pct rescued:   99.99%, read errors:        867, remaining time:         n/a
                               time since last successful read:     57m  8s
Retrying bad sectors... Retry 10 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    42496 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:      4h  2s
pct rescued:   99.99%, read errors:        950, remaining time:     11h 48m
                               time since last successful read:      3m  5s
Retrying bad sectors... Retry 11 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    41984 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  4h 20m 38s
pct rescued:   99.99%, read errors:       1032, remaining time:         n/a
                               time since last successful read:     17m 20s
Retrying bad sectors... Retry 12 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    41472 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  4h 41m  9s
pct rescued:   99.99%, read errors:       1113, remaining time:         n/a
                               time since last successful read:     15m  6s
Retrying bad sectors... Retry 13 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    41472 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  5h  1m 25s
pct rescued:   99.99%, read errors:       1194, remaining time:         n/a
                               time since last successful read:     35m 22s
Retrying bad sectors... Retry 14 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    40960 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  5h 21m 41s
pct rescued:   99.99%, read errors:       1274, remaining time:         n/a
                               time since last successful read:     12m 21s
Retrying bad sectors... Retry 15 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    40960 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  5h 41m 42s
pct rescued:   99.99%, read errors:       1354, remaining time:         n/a
                               time since last successful read:     32m 22s
Retrying bad sectors... Retry 16 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    40448 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:  6h  1m 37s
pct rescued:   99.99%, read errors:       1433, remaining time:     11h 14m
                               time since last successful read:     12m 20s
Retrying bad sectors... Retry 17 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    39936 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  6h 21m 13s
pct rescued:   99.99%, read errors:       1511, remaining time:         n/a
                               time since last successful read:     15m 46s
Retrying bad sectors... Retry 18 (backwards)
     ipos:    1983 MB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:    1983 MB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    39936 B,     error rate:      34 B/s
  rescued:   15931 MB,   bad areas:         26,       run time:  6h 40m 44s
pct rescued:   99.99%, read errors:       1589, remaining time:         n/a
                               time since last successful read:     35m 17s
Retrying bad sectors... Retry 19 (forwards) 
     ipos:  640186 kB, non-trimmed:        0 B,   current rate:       0 B/s
     opos:  640186 kB, non-scraped:        0 B,   average rate:       0 B/s
non-tried:        0 B,  bad-sector:    39424 B,     error rate:       0 B/s
  rescued:   15931 MB,   bad areas:         25,       run time:      7h  5s
pct rescued:   99.99%, read errors:       1666, remaining time:         n/a
                               time since last successful read:     16m  1s
Retrying bad sectors... Retry 20 (backwards)
Finished

經過7小時的錘鍊,損壞資料量從50KB降低到了39KB,可喜可賀(大概吧)。但是,直到最後一回合都沒有出現我期望中的收斂,在最後一回合bad-sector又從39936 B減到了39424 B,不過再繼續下去也沒有什麼意思了,我已經在這裡浪費了7小時的人生。

記錄下這些實驗資料,讓我們格式化,重新出發吧,這張卡還能繼續發光發熱。

之前讀不出來的資料一般只是漏電導致無法辨認資料,而非物理壞塊,格式化之後這些塊還是可用的,但以防萬一,在重新投入使用之前還是先過一遍f3,確保整張卡都能正常讀寫。

卡沒有問題。全文完。