星期四, 8月 30, 2012

Linux 2.6.13+ 的嵌入式系統上實現 USB 熱插拔

某個我習慣用來做紀錄的 BBS 系統因為硬體故障,所以暫時無法使用,所以還是寫篇文章紀錄起來好了。

最近因為工作上的需要接觸了些嵌入式系統的開發工作。基於設計需求,必須讓系統支援 USB 裝置的熱插拔。如果只是單純的 USB thumb drive 的熱插拔其實問題不大,但是如果接上去的裝置是個 USB 的記憶卡讀卡機的話,狀況就複雜了些。為了解決讀卡機的熱插拔問題,花了點時間研究了一下以 udev 為基礎的熱插拔機制到底是怎麼運作的。


以 Ubuntu Linux 為例,Kernel 2.6.13+ 的 Linux 系統目前的熱插拔機制,主要是靠 kernel 的 udev 機制,搭配 udev 軟體套件和 udisks 套件,來動態管理 /dev 下的裝置檔案以及實現 USB 熱插拔。根據這份有點稍微過時的文件,目前 kernel 的 uevent 介面,提供了兩種由 user space 實現熱插拔的機制,一個是透過在 /sys/kernel/uevent_helper (或 /proc/sys/kernel/hotplug) 設定一個 user space 的 helper 程式,在 uevent 觸發時執行;另一個則是透過 netlink 由 user space daemon 來接收 uevent。

在像是 Ubuntu Linux 之類的 Desktop 系統上,目前常見的熱插拔,是 udevd 透過 netlink 接收 uevent 介面送出的熱插拔事件,然後 udevd 根據 rule 分別在 /dev 下建立對應的裝置檔案或觸發其他程式,例如透過 dbus 通知 GNOME 之類的桌面環境有新裝置出現等等。在嵌入式系統上,最常見的替代方案,則是使用 busybox 中的 mdev 當 uevent_helper 來取代 udevd。mdev 在 uevent 觸發執行時,會根據 /etc/mdev.conf 中設定的規則來建立和管理 /dev 下的裝置檔案,也能用來呼叫其他的程式進行像是自動掛載等動作。不過如果插入的裝置是 USB 讀卡機的話,整個狀況又不太一樣了。

USB 讀卡機如果在接上 USB 插槽前,讀卡機中沒有記憶卡的話,插上去之後透過 uevent_helper 介面可以發現 kernel 偵測到 raw device,但是之後再插入記憶卡的話,從 dmesg 的輸出中會看到 kernel 完全沒有任何反應。使用一個簡單的 shell script 取代 mdev 來檢視整個 uevent 觸發的狀況,以 env 把 uevent 傳入的環境變數內容全部紀錄下來,可以發現確實在讀卡機插入 USB 插槽時,會觸發一連串的 uevent,但是之後插卡時則完全沒有任何 uevent 發生。使用 kernel 的 usbmon 搭配 debugfs 監測 USB 控制器的通訊狀態,可以發現問題出在讀卡機在記憶卡插上去之後,並沒有和 USB Host 有任何通訊產生。

那到底 Desktop 系統中的記憶卡熱插拔又是怎樣辦到的呢?檢查 udev 的 rule 可以發現,udevd 根據這些 rule,由 vendor ID 和 product ID 等資訊,以及透過 usb_id 等輔助程式查詢,確認是讀卡機類型的裝置之後,會設定 ID_DRIVE_FLASH_* 等環境變數,通知 udisks 這個裝置是讀卡機裝置,然後再由 udisks 定時對讀卡機的 raw device 做讀取,也就是做所謂的 polling 的動作。當記憶卡插入讀卡機後,polling 的動作會讓 kernel 偵測到有 partition 裝置,並又觸發後續的 uevent。

因此,要在嵌入式裝置上實現讀卡機的熱插拔,最直接的方式自然是實作類似於 udevd 和 udisks 的裝置類型判斷以及裝置 polling 的機制。不過問題是要移植 udisks 和 usb_id 等輔助程式到資源有限的嵌入式系統上,似乎不是個理想的做法。因此我的變通辦法是,透過 mdev 在 sd[a-z] 的 raw device 建立時呼叫一個輔助判別程式,這個判別程式則是使用 uevent 的 DEVPATH 中的 SCSI host、channel、ID、LUN 等資訊,在 /proc/scsi/scsi 的裝置列表中找到對應的裝置內容,並比對 "Model" 資訊來判斷插入的 raw device 是否是讀卡機,如果不是的話,則串聯呼叫自動掛載程式;如果發現是讀卡機的話,則啟動另一個 poller daemon 程式對該裝置定時做 polling 的動作,來處理卡片插入和移除的事件,直到裝置移除時,才由輔助判別程式通知 poller daemon 裝置已移除,讓 daemon 結束執行。

這樣的解決方案也許不是最佳方案,但是應該是相對簡單的解決方法,至少在目標系統上,我使用 busybox 的 ash,用 shell script 就實作了驗證這個想法用的 tester 程式和 pollerd 程式,成功的以軟體的方式達成 USB 熱插拔。

6 則留言:

bubble 提到...

版大你好:
最近也在處理讀卡機熱插拔問題, 但版大的 shell script 是怎麼寫的, 我試著寫, 但還是沒反應...求助...

Shang-Feng Yang 提到...

@bubble:

因為我不知道你是怎麼寫的,所以我實在也不知道要怎麼幫你。基本的 USB thumb drive 或 USB 外接硬碟等的熱插拔,只要 kernel 的 USB 支援、USB mass storage 支援、以及 udev 支援有正確的開啟的話,依照 busybox 的 mdev 提供的文件 mdev.txt 和 mdev_fat.conf 設定範例,稍做調整後應該不難實作出來。我的文章的重點是放在 USB 讀卡機的熱插拔上,如果你不是要實作 USB 讀卡機的熱插拔的話,那應該不太需要實作我文章中提到的輔助判別程式以及 poller daemon。

Bubble 提到...

版大:

謝謝, 我也是做 USB 讀卡機的熱插拔, 但我已試出來了, 感謝~~~

Bubble 提到...

版大:
雖我已可自動斷記憶卡是否插入, 您文中所提的, 如何判斷是否為讀卡機, 我找不到一般USB裝置和讀卡機的不同? 請問您是如何判斷是否為讀卡機?

Shang-Feng Yang 提到...

@Bubble:

判斷是否為讀卡機的方法,最理想的方式是用類似 udev 的方式,建立一個讀卡機的 iVendor:iProduct 的資料庫。

不過我的作法是抄了點捷徑,像我文章中提到的,我是使用 /proc/scsi/scsi 中的 Model 資訊來判斷。至於要確認 /proc/scsi/scsi 中的哪一個裝置是你的目標裝置的話,其實可以從 uevent 傳來的 DEVPATH 中的 SCSI 裝置編號來看。

在 SUBSYSTEM=block 且 DEVNAME=sda 的 uevent,若 DEVPATH=/devices/platform/someplatform-ehci.0/usb1/1-1/1-1:1.0/host5/target5:0:0/5:0:0:0/block/sda,從 target5:0:0 和 block 之間的部份,可以得到該裝置對應的 SCSI host、Channel、ID、LUN,因此就可以用這個資訊來判斷 /proc/scsi/scsi 中哪個項目是對應到這個裝置的。

大致上就是這樣,不知道有沒有回答到你的問題呢?

Bubble 提到...

感謝,

我想我知道該如何測試了, 謝謝!