dd
dd 是一個核心工具,其主要用途是複製文件,並可在複製過程中選擇性地對文件進行轉換。
與cp類似,默認情況下dd會按位複製文件,但還提供了更底層的 I/O 流控制功能。
dd在執行複製操作過程中不會有任何輸出。要監控操作進度,可添加status=progress選項。dd等磁碟操作類命令時應格外小心,錯誤使用可能會不可逆轉地破壞數據。dd是GNU coreutils包的一部分。該軟體包中的其它工具,可參見核心工具。
dd是一個簡單而又功能多樣的強大工具。其可逐塊從源(source)到目標(destination)複製內容,而不關心源或目標的文件系統類型、作業系統。可通過live 環境(如 Live CD)便捷的在任何計算機上使用dd。
dd時應格外小心,避免破壞其它數據。使用時必須注意輸入文件(if=)、輸出文件(of=)的順序,切勿顛倒!務必確保目標設備或分區(of=)的大於等於源(if=)的大小。
若要將硬碟/dev/sda上的第一個分區克隆到硬碟/dev/sdb的第一個分區上:
# dd if=/dev/sda1 of=/dev/sdb1 bs=64K conv=noerror,sync status=progress
of=中指定的輸出分區(sdb1)存在。否則,dd會在根文件系統下創建同名文件並寫入,而不是寫入到目標分區中。若要將硬碟/dev/sda上的所有內容克隆到另一個硬碟/dev/sdb上:
# dd if=/dev/sda of=/dev/sdb bs=64K conv=noerror,sync status=progress
這將克隆整個硬碟,包括分區表、引導加載程序、所有分區、UUID 和數據。
-
bs=用於設置塊大小,默認為512位元組。512位元組是自20世紀80年代初以來的硬碟的「經典」塊大小。但在現代硬碟上,這並非最佳設置。建議使用64K、128K等更大的值。另外,請詳細閱讀下文的說明,該選項不僅設置「塊大小」,還影響讀取錯誤的處理方式。詳情請參閱[1]、[2]。 -
noerror表示當遇到讀取錯誤時,忽略錯誤繼續操作。在默認情況下,dd將在遇到任何錯誤時停止。 -
sync表示當遇到讀取錯誤時,用零填充輸入塊的末尾,以保持數據偏移量同步(若認為輸入設備可能產生讀取錯誤,建議閱讀下文關於sync在遇到讀取錯誤時的行為的詳細解釋)。 -
status=progress顯示傳輸統計信息,可用於估算操作完成時間。
實際上,dd存在「輸入塊大小」(IBS)和「輸出塊大小」(OBS)。
當設置bs時,將同時設置IBS和OBS。在通常情況下,若設置塊大小為1MiB,dd將從輸入文件讀取1024×1024位元組,並寫入輸出文件。但若發生讀取錯誤,則讀取會提前終止。
在使用noerror,sync選項時,一種常見的錯誤看法是,設置bs為1MiB,若讀取過程中發生錯誤,dd只會將發生錯誤的磁碟塊(例如,大小可能為512位元組)用零填充,而1MiB塊的其它部分仍然包含正確的數據;但實際上,根據文檔,dd會先嘗試從輸入文件讀取IBS個字節,一旦發生讀取錯誤,讀取就立刻停止,之後用零將輸入塊填充到OBS個字節,再寫入輸出文件。這意味著,對於磁碟而言,若在當前1MiB數據塊的頭部便發生讀取錯誤(例如,1個512位元組的扇區無法讀取),則將用零填充整個1MiB數據塊,而不是只填充發生錯誤的扇區。若用ERROR表示錯誤,則12ERROR89會變成128900000,而不是120000089。
若確定磁碟沒有錯誤,則可以使用更大的塊大小以大幅提高複製速度。例如,在一個普通的 Celeron 2.7 GHz系統上,將bs從512改為64K,複製速度從35 MB/s提高到了120 MB/s。但需要注意,若發生讀取錯誤,源磁碟上一個扇區的讀取錯誤最終會成為目標磁碟上一整個塊的錯誤,即一個512位元組的讀取錯誤可能會導致整個64 KiB的輸出塊被填充零。
- 在克隆後,若要為ext2/3/4文件系統重新生成UUID,以確保UUID的唯一性,可對每個需要調整的分區執行
tune2fs /dev/sdXY -U random。對於交換分區,需要執行mkswap -U random /dev/sdXY。 - 在克隆GPT磁碟後,可使用sgdisk來隨機化磁碟和分區的GUID,以保證其唯一性。
- 使用
dd克隆磁碟導致的分區表更改不會自動被內核註冊。要在不重啟的情況下通知內核檢查更改,可使用類似partprobe(GNU Parted 的一部分)的工具。
參見Fdisk#備份和恢復分區表、GPT_fdisk#備份和恢復分區表。
首先,從 live 介質啟動,並確保要備份的源硬碟上沒有任何分區被掛載。
然後,掛載用於備份的外部硬碟,備份源硬碟到鏡像中:
# dd if=/dev/sda conv=sync,noerror bs=64M status=progress | lz4 -z > /path/to/backup.img.lz4
如果需要(例如,當生成的文件將存儲在FAT32等單文件大小限制較小的文件系統上時),可將磁碟鏡像分割成多個部分(另見split(1)):
# dd if=/dev/sda conv=sync,noerror bs=64M status=progress | lz4 -z | split -a3 -b2G - /path/to/backup.img.lz4
如果本地沒有足夠的磁碟空間用於存儲鏡像文件,可以通過ssh發送鏡像:
# dd if=/dev/sda conv=sync,noerror bs=64M status=progress | lz4 -z | ssh user@local dd of=backup.img.lz4
最後,保存有關源磁碟物理結構的額外信息(其中最重要的是扇區大小)。這些信息可用於解析鏡像中的分區表。
# fdisk -l /dev/sda > /path/to/list_fdisk.info
要從鏡像文件中恢復系統:
# lz4 -dc /path/to/backup.img.lz4 | dd of=/dev/sda status=progress
若鏡像文件使用分卷方式儲存,改用以下命令:
# cat /path/to/backup.img.lz4* | lz4 -dc | dd of=/dev/sda status=progress
在對磁碟進行更改之前,可能希望備份設備的分區表和分區方案。此外,還可以通過備份與恢復的方式將相同的分區布局複製到多個設備。
MBR存儲在磁碟的前512位元組中,由4個部分組成:
- 前440位元組包含引導加載程序代碼(引導加載程序)。
- 接下來的6位元組包含磁碟簽名。
- 再接下來的64位元組包含分區表(4個條目,每個16位元組,每個條目對應一個主分區)。
- 最後2位元組包含引導簽名。
要將MBR保存到mbr_file.img鏡像中:
# dd if=/dev/sdX of=/path/to/mbr_file.img bs=512 count=1
此外,也可以從完整磁碟備份鏡像中提取MBR:
# dd if=/path/to/disk.img of=/path/to/mbr_file.img bs=512 count=1
要將備份的MBR恢復到磁碟上(注意,這將銷毀現有的分區表,導致磁碟上所有數據無法訪問):
# dd if=/path/to/mbr_file.img of=/dev/sdX bs=512 count=1
若只需要恢復引導加載程序代碼部分,而不恢復主分區表,則只需恢復MBR的前 440 字節:
# dd if=/path/to/mbr_file.img of=/dev/sdX bs=440 count=1
若要只恢復主分區表,不恢復引導加載程序代碼部分,使用:
# dd if=/path/to/mbr_file.img of=/dev/sdX bs=1 skip=446 count=64
要清除MBR中引導程序加載程序的代碼(可能在需要完全重新安裝另一作業系統時有用),只需將前440位元組清零:
# dd if=/dev/zero of=/dev/sdX bs=440 count=1
雖然dd(1)支持一些其他常用工具中不支持的獨特功能,但其與其它核心工具的命令行語法差距較大,許多默認行為並不符合預期,且在應用於特定場景時容易出錯。因此,若可能,推薦使用在特定方面更易使用的其它工具。
不過,由於dd是核心工具,默認安裝於Arch和其它眾多系統上。因此,對於不便在系統上安裝新軟體包的情況,可能仍需使用dd進行操作,而不能使用其它更好的替代品。
為了同時涵蓋上述兩個方面,本節聚焦於dd(1)的獨特功能。這些功能在其它常用工具中很少見。本節的形式類似於Pacman/各軟體包管理器命令對應關係,同時給出使用dd實現與使用其它工具實現的示例,但減少了其它工具的示例的數量,以聚焦於dd的功能。其它工具的示例將在「提示」框中通過實際指令或偽代碼體現。
在自動化的shell腳本中,經常使用dd作為二進制文件修補程序。儘管其功能相比專用工具有限,但支持:
- 通過
seek選項,在輸出文件特定偏移量處開始寫入。 - 覆蓋輸出文件中的特定部分,但不將輸出文件截斷到寫入結束的位置(使用
conv=notrunc選項)。
在下例中,使用dd修改了符合cpio(5) § Portable ASCII Format格式的歸檔文件中第一個成員的時間戳,該時間戳起始於文件的第49(十六進制0x30)字節:
$ touch a-randomly-chosen-file $ bsdtar -cf example-modify-ts.cpio --format odc -- a-randomly-chosen-file
$ printf '%011o' "$(date -d "2019-12-21 00:00:00" +%s)" | dd conv=notrunc of=example-modify-ts.cpio seek=48 oflag=seek_bytes
seek_bytes 輸出標誌,用於以字節而不是塊為單位,指定寫入輸出文件的偏移量。dd外,還可以考慮使用支持在shell打開的文件描述符上調用lseek(2)的shell。若有以下需求:
那麼需要讓shell先打開文件描述符,在文件描述符上執行定位操作,並最終連接到相應程序的輸出端。「相應程序」可以是上面提到的使用splice(2)系統調用的工具、不進行fork的shell內建命令(例如:zshmodules(1) § sysseek):
$ zsh
$ local +xr openToWriteFD
$ zmodload zsh/system
$ sysopen -wu openToWriteFD example-modify-ts.cpio
$ sysseek -u $openToWriteFD 48
$ printf '%011o' "$(date -d "2019-12-21 00:00:00" +%s)" >&${openToWriteFD}
...
$ : finally close the fd {openToWriteFD}>&-
VFAT文件系統的卷標位於偏移量0x047處,長度為11位元組,卷標長度小於11位元組的,由ASCII空格填充到11位元組。要讀取存儲在鏡像文件中的VFAT文件系統的卷標:
$ truncate -s 36M empty-hole.img $ mkfs.fat -F 32 -n LabelMe empty-hole.img
$ dd iflag=skip_bytes,count_bytes count=11 skip=$((0x047)) if=empty-hole.img | sed -e 's% *$%%'
curl不支持進行定位/跳過操作。此時可使用socat(1),其支持對輸入文件(包括塊設備,但不包括管道和字符設備)進行這些定位操作,但不如curl常用:$ socat -u -,seek=$((0x047)),readbytes=11 - < empty-hole.img | sed -e 's% *$%%'
在下例中,若輸出端長時間阻塞,則輸入端將長時間等待,導致建立的TCP連接持續時間過長。此時可以在兩個命令之間「拼接」一個dd。dd的輸出塊大小應明顯大於輸入,同時需要小於可用內存容量:
$ curl -qgsfL http://example.org/mirrors/ftp.archlinux.org/mirrored.md5deep | dd ibs=128k obs=200M | <一个逐行对输入文件中存储的路径信息执行镜像,但不会先缓冲整个输入文件的低效镜像脚本>
dd在開始複製操作之前會截斷輸出文件。在數據流式 shell 腳本中,可以使用dd來限制管道命令可能消耗的數據總長度。例如,以流式方式使用shell腳本函數檢查ustar頭部塊tar(5) § POSIX ustar Archives):
count選項的參數中使用B後綴是GNU coreutils v9.1中新引入的功能,其效果與count_bytes輸入標誌相同。注意,在不使用B後綴時,count指定的是塊數量,而不是字節數量,即使附加了其它後綴(例如,k)。例如,count=256k表示複製262144個輸入塊而不是262144位元組。hexdump-field() {
set -o pipefail
printf '%s[%d]:\n' $1 $2
dd count=${2}B status=none | hexdump -e $2'/1 "%3.2x"' -e '" | " '$2'/1 "%_p" "\n"'
}
inspect-tar-header-block() {
local -a hdrstack=(
name 100
mode 8
uid 8
gid 8
size 12
mtime 12
checksum 8
typeflag 1
linkname 100
magic 6
version 2
uname 32
gname 32
devmajor 8
devminor 8
prefix 155
pad 12
)
set - ${hdrstack[@]}
while test $# -gt 0; do
hexdump-field $1 $2 || return
shift 2
done
}
$ bsdtar -cf - /dev/tty /dev/null 2>&- | dd count=1 skip=1 status=none | inspect-tar-header-block
dd的輸出端是一個管道,且該管道連接到的程序使用splice(2)。那麼,作為替代方案,可以考慮使用:
- 內置lseek(2)定位功能的shell(見前之前中的替代方案)。
- 使用類Bourne shell(例如bash),通過使用xxd(1) § s在shell打開的文件描述符上進行一次性lseek(2)操作,
ls -l /proc/self/fd查看已分配的文件描述符):$ bash
$ exec 9<dummy-but-rather-large.img $ xxd -g 0 -l 0 -s $((0x47ffff)) <&9 $ pv -qSs 104857601200 <&9 | <一個處理大量數據但不會按需限制讀取長度,也不支持按偏移量讀取的程序> $ exec 9<&-
不推薦直接將dd用於該用途。其它常用工具參見U_盤安裝介質#使用基本命令行工具。
dd,建議使用dd_rescue(1) § W替代。若塊設備中已經存儲了舊版本的鏡像內容,在寫入新版本的鏡像時,其能避免不必要的寫入操作。若dd在嘗試進行讀取操作時,一個完整的輸入塊暫時不可用,則輸出文件的最終的大小可能比要求複製的大小要小。如文檔所述:
- 此外,如果未指定
conv數據轉換操作數(即本文中提到過的選項),從輸入讀取到的內容會立即被複製到輸出,即使讀取到的數據量小於塊大小。
在Linux上,底層的read(2)系統調用在從管道(pipe(7))或某些設備文件(例如/dev/urandom、/dev/random,由於底層內核設備驅動程序硬編碼的限制)讀取時,可能會在讀取到部分數據,數據量還未達到參數指定的值時便提前返回(即「部分讀取」)。這導致在結合使用bs和count=n選項時,複製的數據總大小小於預期。其中count=n指定了要複製到輸出的輸入塊的數量,但每個輸入塊的大小可能小於bs字節。
在發生部分讀取時,dd可能顯示警告,但不保證一定會顯示警告:
dd: warning: partial read (X bytes); suggest iflag=fullblock
解決方案是依照警告的提示,在dd命令中添加iflag=fullblock輸入選項。例如,要創建一個大小為40MiB、填充了隨機數據的新文件:
$ dd if=/dev/urandom of=new-file-filled-by-urandom.bin bs=40M count=1 iflag=fullblock
當從管道讀取時,iflag=fullblock的一個替代方案是將bs設置為linux/limits.h中定義的PIPE_BUF常量值,以使得pipe(7) I/O成為原子操作。例如,若要創建總長度為5MiB,填充了隨機字母數字字符串的文本文件:
$ LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | dd of=passtext-5m.txt bs=4k count=1280
若輸出文件不是管道,可使用ibs、obs選項分別為輸入管道和輸出文件設置塊大小。例如,由於輸出文件存儲在磁碟上,可設置更大的塊大小,以提高效率:
$ LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | dd of=passtext-5m.txt ibs=4k obs=64k count=1280
PIPE_BUF常量定義的值,可能就已經是最佳的了。如果在寫入輸出時遇到錯誤(例如收到SIGPIPE信號、發生介質故障或網絡塊設備意外斷開),命令顯示的總傳輸字節數可能會大於實際值(「部分寫入」)。如下例所示,其中第二個dd從輸入中讀取的字節數顯然不會超過512200位元組,但第一個dd實例仍然報告了複製了512400位元組:
$ yes 'x' | dd bs=4096 count=512400B | dd ibs=1 count=512200 status=none >/dev/null 125+1 records in 125+1 records out 512400 bytes (512 kB, 500 KiB) copied, 10.7137 s, 47.8 kB/s
對於類似上述例子的情況,當要恢復中斷的傳輸時,建議以已經完整複製的輸出塊的數量為依據(125+1 records out中「+」號前面的數字,即125)。
iflag=fullblock選項後,部分I/O塊數(「+」號後面的數字仍大於1,則說明部分I/O發生了不止一次。在這種情況下,為可靠地恢復傳輸進度,建議:
- 改用ddrescue重新進行傳輸,以更靈活地處理可能存在故障的介質上發生的部分讀取。
- 若網絡連接不穩定且需要寫入網絡設備,使用dd_rescue(1)以直接I/O方式重新進行傳輸。
- 避免向可能發生故障的介質寫入。