星期一, 9月 03, 2012

為 Das U-Boot 新增網路服務

最近被交代要在 u-boot 上實作 DHCP server。雖然單純的寫一個簡單的 DHCP server 其實蠻容易的,只要完成 "DORA" (Discover, Offer, Request, Ack) 即可,sourceforge 上也有一些程式碼可以參考,例如 Simple Embedded DHCP 等等,但是要在 U-Boot 上實作就不是那麼直接了,因為在 U-Boot 環境中沒有 OS 的協助,所以必須依照 U-Boot 的架構從零開始。

今天完成了可運作的 DHCPD 實作,雖然還有一些細節需要再檢驗 U-Boot 原始碼來做些確認和釐清,但大致上的重點應該都有抓到,所以紀錄一下以供以後參考。不過因為我的目標 U-Boot 版本是大約 2005 年釋出的 v1.1.4,雖然大原則相同,但一些細節可能會有所不同,例如定義一些函式、字串、巨集的檔案可能會有所不同等等。


U-Boot 網路相關的核心功能與函式,主要是在 <U-BOOT>/net/net.c 中實作,並且在 <U-BOOT>/include/net.h 中定義許多相關全域變數。基本上要實作任何網路相關功能,大概至少要引入 net.h 和 common.h:

#include <net.h>
#include <common.h>
此外,若實作的服務是原本 U-Boot 中沒有的服務,那多半還需要在 net.h 的 proto_t 中定義新的項目。

U-Boot 中的網路功能,主要是透過 net.c 中的 int NetLoop(proto_t protocol) 來執行。當 NetLoop() 函式開始執行時,會根據傳入的 protocol 作該協定相關的初始化工作,然後再呼叫 net_check_prereq(protocol) 來做該服務相關的先決條件檢查,如果檢查通過的話,通常就會執行該相關服務的起始函式,接著進入無限迴圈,在每次環圈執行中檢查全域變數 NetState 來決定接下來是繼續執行迴圈還是結束 NetLoop() 的執行。

在這樣的架構下,要實作一個新的服務(EX. MYSERVICE),需要至少實作以下的部分:
  • 至少需要實作新服務的起始函式 MyServiceStart()、封包處理回呼函式 MyServiceHandler()、逾時處理回呼函式 MyServiceTimeout() 等,其中除了 MyServiceHandler() 的原型會有輸入參數以外,其餘兩個函式都可以沒有輸出輸入參數。另外,通常還會實作一個封包建構和傳送函式 MyServiceSend(),把封包建構的程式碼由 MyServiceHandler() 中分離出來,方便修改除錯。此外,因為大部分的函式都沒有使用輸入參數來做參數傳遞,因此通常還會建立一個該服務的狀態全域變數 MyServiceState 來追蹤新服務的執行階段。
  • 在 <U-BOOT>/include/net.h 的 proto_t enum 中新增該服務
  • 修改 <U-BOOT>/net/net.c 的 NetLoop() 中協定初始化的 switch() {...},加入新服務的初始化程式碼,例如由環境變數中把 local IP 填入 NetOurIP 全域變數等等。
  • 修改 net_check_prereq() 函式,加入新服務的檢查。
  • 修改 NetLoop() 中的 swtich(net_check_prereq(protocol)) {...},在 "case 0:" 的部分,加上新服務在通過檢查後的後續初始化程式碼,並在最後呼叫新服務的 MyServiceStart() 實際起始新服務。

至於新服務的3個基本函式要包含怎樣的內容呢?大致上來說,MyServiceStart() 需要處理以下工作:
  • 開始接收封包前的其他初始化工作。
  • 使用 NetSetTimeout(TIMEOUT, MyServiceTimeout()) 來註冊新服務的逾時時間門檻以及逾時處理回呼函式。
  • 使用 NetSetHandler(MyServiceHandler()) 來註冊新服務的封包接收處理回呼函式。

MyServiceHandler() 則需要負責接收到封包後的處理,例如封包內容的解讀、根據收到的封包更新 MyServiceState 狀態變數、呼叫 MyServiceSend() 來建構和發送回應封包、根據服務的執行階段設定 NetState 全域變數來結束 NetLoop() 的執行或呼叫 NetStartAgain() 重新初始化 NetLoop() 的執行等等。

至於 MyServiceTimeout() 中,則是處理逾時的狀況,例如顯示除錯訊息後,把 NetState 設定為 NETLOOP_FAIL 讓 NetLoop() 結束執行等等。不過要注意一點的是,如果在接收到有效封包後沒有在 MyServiceHandler() 中呼叫 NetSetTimeout(0, (thand_f *)0) 取消逾時回呼的註冊,或者是重新設定 Timeout 時限值 (即 NetSetTimeout() 的第一個參數)的話,逾時的計算會繼續執行,所以如果一開始的 Timeout 值設定得很短,然後又忘了在之後重設 Timeout 的話,很可能會發生服務執行到一半莫名其妙被中斷的 bug。

除了實作這3個基本函式以外,通常還會把封包建構和傳送的程式碼,集中在 MyServiceSend() 函式中。在 U-Boot 中要建構封包,幾乎是必須從零開始。net.c 中有提供兩個封包傳送函式,發送網路層封包的 NetSendPacket() 函式,以及發送傳輸層 UDP 封包的 NetSendUDPPacket() 函式,需要視新服務所用協定的封包種類做選擇。封包的建構通常是宣告一個 uchar *pkt 指標後,由 pkt=NetTxPacket 取得外送封包的指標,接著就是逐步加上 ethernet header、IP header、負載協定封包的檔頭與資料等等,填值時需注意適當使用 htons()、htonl() 等函式來處理 byte order 的問題。

到此為止,新服務的實作大致完成了,但是如果想要在 U-Boot 的互動式命令列介面中能夠用指令來執行這個新服務,則還需要實作一個新的 U-Boot 命令列指令。

U-Boot 的命令列指令的實作,都集中在 <U-BOOT>/common/cmd_*.c 中,並且要在 <U-BOOT>/include/cmd_confdefs.h 中定義該指令的設定用旗標值的巨集,以及修改相關的 Makefile 中把新指令和新服務的 .o 檔加入連結的目標中。

要實作一個新的命令列指令 myservice,大致上需要修改以下幾個地方:
  • 在 <U-BOOT>/include/cmd_confdefs.h 中定義 CFG_CMD_MYSERVICE 巨集。U-Boot 在設定命令列介面要包含哪些指令的 CONFIG_COMMAND 巨集時,會使用這個巨集的旗標值作 OR 運算,來決定要包含的指令有哪些。如果找不到適當的值的話,就只好取代已經被定義但應該用不到的指令的旗標值。
  • 在 <U-BOOT>/common/cmd_net.c 中,實作 do_myservice() 函式,並使用 U_BOOT_CMD(myservice, 1, 1, do_myservice, "description", "Help Message") 巨集在指令表中註冊 myservice 指令與對應的 do_myservice() 處理函式。至於 do_myservice() 處理函式的定義,最簡單的方式就是直接呼叫 cmd_net.c 中定義的 netboot_common() 函式,該函式會在完成參數分析之後,呼叫 NetLoop() 函式開始執行新服務。

假如所有實作都正確,且 CONFIG_COMMAND 巨集中有啟用 CFG_CMD_MYSERVICE 的話,在進入 U-Boot 的命令列模式後,使用 help 指令,應該會看到 myservice 指令也在其中。

大致上要實作一個新的網路服務,並且要新增一個命令列指令來啟動該服務的注意事項就是這些,剩下的就是一些實際實作時隨情況而定的細節,以及不斷的除錯直到新服務可以順利執行了。過幾天如果時間許可,也許會寫一個簡單的 echo service 來紀錄整個實作的細節。

繼續閱讀全文

星期四, 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 熱插拔。


繼續閱讀全文

星期六, 4月 07, 2012

使用 Shell Script 進行 URL 編/解碼

一段時間之前,我寫了個利用 GNU coreutils 中的 "printf" 程式來進行 URL 解碼的 script 叫作 "urldecode"。今天本來想寫個使用 tinyurl.com 來產生短網址的 script,不過這樣的 script 必須要用到 URL 編碼來把傳給 tinyurl 的網址資料先編碼才行。在查了維基百科的 "Percent-encoding" 頁面之後,我寫了個用來做 URL 編碼的 script 叫 "urlencode


首先來看解碼的部份。解碼是相對簡單的事情,因為 "printf" 接受 "\xHH" 這樣的格式字串("HH" 是一位或兩位數的16位元數值),所以只要先把編碼字串中的 '%' 換成 "\x" 字串後,再整個丟給 "printf" 即可。以下是我的實作:


#!/bin/bash
#
# urldecode - decoding the URL-encoded string
#
# (C)2010 Shang-Feng Yang <storm_DOT_sfyang_AT_gmail_DOT_com>
#
# License: GPLv3

ENC_STR=$@
[ "${ENC_STR}x" == "x" ] && {
TMP_STR="$(cat - | sed -e 's/%/\\x/g')"
} || {
TMP_STR="$(echo ${ENC_STR} | sed -e 's/%/\\x/g')"
}
PRINTF=/usr/bin/printf
exec ${PRINTF} "${TMP_STR}\n"

"urldecode" 程式接受由標準輸入串流中或從命令參數中傳入目標字串。不過這個程式有個明顯的問題,那就是因為是把前處理過的字串整個丟給 "printf" 當作格式字串,所以當輸入字串太長時,程式會發生錯誤。

至於編碼的部份就相對比較麻煩了。我最初的想法是掃描輸入字串找出保留字元,把這些字元轉換後,再代換進輸入字串中。為了做字元轉換,我寫了個叫作 "char2hex" 的 script 來把目標字元轉換成對應的 ASCII 碼的 16 進位數值:


#!/bin/bash
#
# char2hex - returning the hexadecimal value of the given characters
#
# (C)2012 Shang-Feng Yang <storm_DOT_sfyang_AT_gmail_DOT_com>
#
# License: GPLv3

function usage() {
echo -e "Usage:\n"
echo -e "\t$(basename $0) CHARACTER(S)_TO_CONVERT\n"
}

CHAR=$1

[ "x${CHAR}" == "x" ] && { usage; exit 1; }

echo -n "${CHAR}" | od -A n -t x1 | tr -d ' '

這個 script 的運作方式相當直觀,唯一需要稍微提一下的是 "echo" 需要加上 '-n' 選項的原因。"echo" 預設會在輸出的最後加上一個換行字元,所以如果不加 '-n' 選項的話,輸出的結果在結尾會多個 "0a" 在後面。加上 '-n' 選項會關閉這個預設行為。

這種方法似乎相當簡單,但是實際上實作可能就不是那麼容易了。也許是我太笨想不到解法,不過像是光要從輸入字串中挑出保留字元,我就想不到有什麼簡單又不會因為大幅度增加 I/O 導致程式跑得很沒效率的方法。對像我這種懶惰的人來說,為了這個不是主要目的的問題想破頭,顯然不是個可行的辦法。

在看過維基百科 "Percent-encoding" 頁面的 "Percent-encoding reserved characters" 和 "Percent-encoding the percent character" 兩個段落後,發現必須進行編碼的保留字元其實並不多,所以最直接簡單的辦法是使用相對無腦的『列表法』來實作:


#!/bin/bash
#
# urlencode - escaping the reserved characters using URL-encoding
#
# (C)2012 Shang-Feng Yang <storm_DOT_sfyang_AT_gmail_DOT_com>
#
# License: GPLv3

STR=$@
[ "${STR}x" == "x" ] && { STR="$(cat -)"; }

echo ${STR} | sed -e 's| |%20|g' \
-e 's|!|%21|g' \
-e 's|#|%23|g' \
-e 's|\$|%24|g' \
-e 's|%|%25|g' \
-e 's|&|%26|g' \
-e "s|'|%27|g" \
-e 's|(|%28|g' \
-e 's|)|%29|g' \
-e 's|*|%2A|g' \
-e 's|+|%2B|g' \
-e 's|,|%2C|g' \
-e 's|/|%2F|g' \
-e 's|:|%3A|g' \
-e 's|;|%3B|g' \
-e 's|=|%3D|g' \
-e 's|?|%3F|g' \
-e 's|@|%40|g' \
-e 's|\[|%5B|g' \
-e 's|]|%5D|g'

"urlencode" 的內容相當簡單不需要多做解釋。這個 script 同樣可以使用 STDIN 資料流和命令列參數的方式來傳入輸入字串。下面是使用這兩個 script 的一些範例:


$ urlencode http://en.wikipedia.org/wiki/Percent-encoding
http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPercent-encoding
$ echo 'http://en.wikipedia.org/wiki/Percent-encoding' |urlencode
http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPercent-encoding
$ urldecode $(urlencode http://en.wikipedia.org/wiki/Percent-encoding)
http://en.wikipedia.org/wiki/Percent-encoding
$ urlencode http://en.wikipedia.org/wiki/Percent-encoding |urldecode
http://en.wikipedia.org/wiki/Percent-encoding

註:由於應 blogger 要求更新了 blogger 底層的 template,所以上面的程式碼和終端機區塊的排版有點問題...我未來如果有足夠動力的話也許會去改 CSS 把這個問題修好...

繼續閱讀全文