[!NOTE]
本系列文章已進行全域去敏與匿名化處理,專案代號統一為DataGateway,內網 IP 統一為10.0.0.x。
昨天我們用 dd 創造了一個 .img 檔案,並且將它格式化為 FAT32,最後透過 ConfigFS 餵給了 Windows。Windows 現在開心得不得了,它看見了一個完整的磁碟區。
但是,身為這個 .img 檔案主人的 Raspberry Pi (Linux) 本身,卻不知道該怎麼把資料寫進這個 FAT32 空間!對 Linux 來說,它只是一個存在 /opt/gateway/usb_disk.img 裡面的龐大檔案而已,你沒辦法直接用 open() 寫入資料到這個「檔案的檔案系統」中。
這時候,我們需要一座橋樑,把這個實體檔案偽裝成一個區塊設備 (Block Device)。
這就是 Loop Device (迴圈設備) 發揮作用的時刻。
平時我們掛載隨身碟,通常是針對硬體節點,例如:mount /dev/sdb1 /mnt/usb
但現在我們手上沒有 /dev/sdb1,我們只有一個 usb_disk.img。
如果你直接執行 mount /opt/gateway/usb_disk.img /mnt/usb,在較新版本的 Linux 系統中,mount 其實非常聰明,它會在背景幫你搞定一切。但這背後的魔法,其實就是調用了 losetup。
在醫療邊緣設備的開發中,把邏輯交給 mount 的隱式行為是危險的。我們需要對系統資源有絕對的控制權,這包含明確知道我們佔用了哪個 Loop Device,以及在拔除隨身碟時能乾淨地回收資源。
Loop Device 是一個虛擬的 Block Device,通常位於 /dev/loop0, /dev/loop1 等位置。
它的作用很簡單:把讀寫這些 /dev/loopX 的操作,透明地轉發到某個實體的檔案上。
這就像是用一根水管 (Loop Device) 插入那個水缸 (img 檔案),然後你在水管的另一頭安裝一個水龍頭 (mount point),你轉開水龍頭流出來的水,其實就是水缸裡面的水。
在我們的 DataGateway 中,Python 程式啟動時,必須自己去掛載這個虛擬空間,才能開始把 Sensor 送來的資料寫入。
Linux 系統內建了多個 Loop Device,我們不該死寫 /dev/loop0,因為它可能已經被 Docker 或其他系統服務佔用了(例如 Ubuntu 的 snap 系統就吃了超多 loop device)。
我們應該讓系統自己分配:
# 尋找第一個未被使用的 loop device
LOOP_DEV=$(losetup -f)
echo "找到空閒設備:$LOOP_DEV"
# 可能會輸出 /dev/loop0
找到空閒的水管後,把它插進水缸裡:
sudo losetup $LOOP_DEV /opt/gateway/usb_disk.img
這時,/dev/loop0 就等同於那顆 FAT32 硬碟了。
有了 Block Device 後,我們就能用標準的 mount 指令把它掛載到指定的資料夾:
# 建立掛載點
sudo mkdir -p /mnt/usb_sync
# 將 FAT32 系統掛載起來,指定 uid 與 gid 為 pi,避免權限問題
sudo mount -t vfat -o uid=1000,gid=1000,umask=022 $LOOP_DEV /mnt/usb_sync
現在,Python 程式只要對 /mnt/usb_sync 寫入 data.csv,實際上就是在改變 usb_disk.img 裡面的二進位結構!
當不需要使用時,如果不正確地卸載,很容易導致檔案系統毀損。卸載必須是掛載的反向操作:
# 1. 卸載掛載點
sudo umount /mnt/usb_sync
# 2. 拔除水管 (釋放 Loop Device)
sudo losetup -d $LOOP_DEV
你可以用 losetup -a 來查看當前系統有哪些檔案被綁定在哪些 Loop Device 上,這在 Debug 時非常有用。
為了自動化,我們在 DataGateway 中包裝了一支 Python 腳本來處理這件事:
import subprocess
import os
def mount_virtual_disk(img_path, mount_point):
if not os.path.exists(mount_point):
os.makedirs(mount_point)
# 尋找空閒 loop device
loop_dev = subprocess.check_output(['losetup', '-f']).decode().strip()
# 綁定
subprocess.run(['sudo', 'losetup', loop_dev, img_path], check=True)
# 掛載
subprocess.run(['sudo', 'mount', '-t', 'vfat',
'-o', 'uid=1000,gid=1000',
loop_dev, mount_point], check=True)
return loop_dev
def unmount_virtual_disk(mount_point, loop_dev):
# 確保所有緩存寫入硬碟
subprocess.run(['sync'])
subprocess.run(['sudo', 'umount', mount_point], check=True)
subprocess.run(['sudo', 'losetup', '-d', loop_dev], check=True)
好啦,現在我們有兩端可以存取這個 usb_disk.img 了:
聽起來很完美對吧?設備產生資料 ➔ 透過 Loop Device 寫入 ➔ Windows 透過 USB 即時看見變化。
大錯特錯。
這將會引發非常恐怖的檔案系統災難。明天,我們將直面邊緣儲存領域中最棘手的難題:「雙腦分裂 (Split-Brain) 危機」。