2011-11-11

Linux 的 udev 是個有趣又有用的玩具(3)

在這個單元,我們要開始運用 udev 呼叫 my_script 的時候,所傳過來的環境變數(environment variables)。其中最重要,而且每次都一定會設定的變數就是:SUBSYSTEM 及 ACTION。其他的許多變數常因 SUBSYSTEM 而異。

第一個單元裡,我們曾寫了一個範例程式,叫做 my_script,讓 Linux 系統每次發生 udev 事件時,都會發出一次聲響(sound effect)。以一個 USB thumb drive 做實驗,插入(insert)或拔除(unplug)時分別都會聽見約 6、7 次,以及 3 次聲響。也就是說,插入一個 USB 隨身碟時,系統上會產生許多次的 uevents。拔除時亦然。我們再複習一下這個簡單的掛勾機制(hook):

$ cat /etc/udev/rules.d/z99_test.rules
RUN+="/home/nobody/my_script"
$ ls -l ~/my_script
-rwxr-xr-x 1 nobody nobody 1056 Nov 22 03:04 /home/nobody/my_script

為了篩選出(match)方便與之掛勾的特定事件(uevent),我們必須更進一步追蹤這些事件的內幕。首先,把 my_script 改寫為以下 3 行:

#!/bin/sh
echo $SEQNUM $SUBSYSTEM $ACTION $DEVNAME >> /tmp/my_script.log
exit $?

先以 root 的權限把 logfile /tmp/my_script.log 的內容清空,再插入,或拔除手邊任何一個 USB 隨身碟。然後再看看在 /tmp/my_script.log 這個檔案裡面,我們到底收集到了那些情資。結果大概會很接近如下所示的內容:
2124 usb add /dev/1-6
2125 usb add
2126 scsi_host add
2127 usb_device add /dev/bus/usb/001/007
2134 bdi add
2128 scsi add
2129 scsi_disk add
2131 scsi change
2130 scsi_device add
2132 block add /dev/sda
2133 block add /dev/sda1
2136 scsi_disk remove
2135 scsi_device remove
2137 block remove /dev/sda1
2140 scsi remove
2143 usb_device remove /dev/bus/usb/001/007
2138 bdi remove
2141 scsi_host remove
2142 usb remove
2139 block remove /dev/sda
2144 usb remove /dev/1-6
其中第一個 field 是環境變數 SEQNUM 的值,可用來判別事件發生的先後順序,第二個是 SUBSYSTEM,第三個是 ACTION,第四個則為 DEVNAME 的值,其中,DEVNAME 有時候並未設定。細數後可知,插入 USB 的動作導致系統上一共產生了 11 次的 uevents,拔除時,則產生了 10 次的 uevents。在這眾多的 uevents 當中,特別適合用來唯一識別 USB 裝置插入(insert)及拔除(remove)動作的事件,也就是只發生過一次的事件可說是:
SUBSYSTEM==usb_device
ACTION==add
以及
SUBSYSTEM==usb_device
ACTION==remove

以下這個 my_script 示範如何限縮這個 script 的反應,只在插入或移除一個 USB device 時發出聲響,而且分別只響一次:

#!/bin/sh
# my_script example 3-1
LOGFILE=/tmp/my_script.log # 定義變數 LOGFILE
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # 把 stdout/stderr 導入 $LOGFILE
SND_USB_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav" # 定義聲音檔
SND_USB_REMOVE="/xp/WINDOWS/Media/chimes.wav" # 定義聲音檔

if [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "add" ]
then
    echo ""
    echo "=============================="
    date "+%G-%m-%dT%H:%M:%S %z"
    echo "$SUBSYSTEM $ACTION"
    echo "aplay -q \"$SND_USB_INSERT\""
    aplay -q "$SND_USB_INSERT"
elif [ "$SUBSYSTEM" = "usb_device" -a "$ACTION" = "remove" ]
then
    echo ""
    echo "=============================="
    date "+%G-%m-%dT%H:%M:%S %z"
    echo "$SUBSYSTEM $ACTION"
    echo "aplay -q \"$SND_USB_REMOVE\""
    aplay -q "$SND_USB_REMOVE"
fi
exit $?

編輯 my_script 的時候,要確認聲音檔是不是存在。編輯完畢之後,要記得做
$ chmod 755 ~/my_script
以上的 script 是說,當(if) SUBSYSTEM 這個變數的值等於(=)usb_device,而且(-a)當 變數 ACTION 的值為 add 的時候,就(then)執行一些 echo(1), date(1), aplay(1) 的指令;要不然的話當(elif)變數 SUBSYSTEM 的值為(=) usb_device,而且(-a)變數 ACTION 的值為(=)remove 時,就(then)執行另一批指令。請注意,方括號 [ 及 ] 跟 = 的前後,要有空白(space),變數(variable)的前後,也必須加上雙引號(double quotation marks),因為變數值可能會包含空白字元(embedded spaces)。

最後一個指令 "exit $?",是要結束這個 script 並把當時的執行結果傳回(return)給呼叫這個 script 的任何程式。想要省略掉的話,也沒什麼問題,然而我們必須平時就養成撰寫程式的正確習慣,在 UNIX 系統上,任何程式、函式都必須要有傳回值,就算是不知道有誰會去用它。我們不希望看到那一天,當有人突然去用它的時候,就必須面對噩運。這看似小事一件,然而缺乏持續性對小節的正確習慣或態度,我們對核四的運轉,為了自己的生存,必須反對到底。

做完以上的 script,只要隨手找個 USB 設備插入,或拔出,就都會聽到變數 $SND_USB_INSERT 或是 $SND_USB_REMOVE 所設定的聲效檔(sound file)。這是採用正面表列只當變數 SUBSYSTEM 內傳過來的值為 usb_device 時,才依變數 ACTION 的值是 add 或是 remove 來採取行動。

如果我們也希望這個程式對記憶卡以及 PCMCIA、CF 以及 SD 插槽也有所反應的話,則可修改程式,也檢查變數 SUBSYSTEM 是否為 "mmc" 或是 "pcmcia"。以同樣的方法可以得知,SD 卡插槽,以及 PCMCIA 插槽的插入與拔除動作可以:
SUBSYSTEM==mmc
ACTION==add

SUBSYSTEM==mmc
ACTION==remove

SUBSYSTEM==pcmcia
ACTION==add

SUBSYSTEM==pcmcia
ACTION==remove
可靠地識別。以下的另一個 my_script 範例則示範採用負面表列的邏輯結構,檢查變數 SUBSYSTEM 如果不是 "usb_device"、"mmc"、"pcmcia" 其中之一的話,即立刻結束程式(terminate),不然,則印出訊息,再判斷變數 ACTION 為 "add" 或 "remove" 分別作出對應的音效:

#!/bin/sh
# my_script example 3-2
LOGFILE=/tmp/my_script.log # 定義變數 LOGFILE
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # redirect stdout/stderr 到 LOGFILE
SND_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav" # 定義聲音檔
SND_REMOVE="/xp/WINDOWS/Media/chimes.wav" # 定義聲音檔

if [ "$SUBSYSTEM" != "usb_device" -a "$SUBSYSTEM" != "pcmcia" -a "$SUBSYSTEM" != "mmc" ]
then
    exit 0
fi

echo ""
echo "=============================="
date "+%G-%m-%dT%H:%M:%S %z"
echo "$SUBSYSTEM $ACTION"

if [ "$ACTION" = "add" ]
then
    echo "aplay -q \"$SND_INSERT\""
    aplay -q "$SND_INSERT"

elif [ "$ACTION" = "remove" ]
then
    echo "aplay -q \"$SND_REMOVE\""
    aplay -q "$SND_REMOVE"
fi
exit $?

編輯完畢、儲存檔案後,程式會立即生效。這時,無論是在 PCMCIA 插槽、CF 插槽、SD 插槽,還是 USB 插座插入或拔除週邊設備,電腦都會發出預設的音效,而且每個動作只會發出一次聲音,不像在第一單元那樣發出 6、7 次的聲響。

變數 ACTION 除了 "add" 跟 "remove" 兩個可能的值以外,還可能會是 "change"。這例如可能會發生於以下的情況:如果你使用的是一台含有內建電池的筆記型電腦(notebooks),那麼,在插上或拔掉外接電源線(AC power cord)時,udev 系統分別會產生兩種 uevents:
SUBSYSTEM=power_supply
ACTION=change
POWER_SUPPLY_ONLINE=1
以及
SUBSYSTEM=power_supply
ACTION=change
POWER_SUPPLY_ONLINE=0
我們現在就繼續改良以上的 my_script,讓它在插入跟拔掉電源線時,也會發出聲音信號:

#!/bin/sh
# my_script example 3-3
LOGFILE=/tmp/my_script.log # 定義變數 LOGFILE
exec 3>> $LOGFILE && exec >& 3 && exec 2>&1 # redirect stdout/stderr 到 LOGFILE
SND_INSERT="/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav" # 定義聲音檔
SND_REMOVE="/xp/WINDOWS/Media/chimes.wav" # 定義聲音檔

PRINT_DATE()
{
    echo ""
    echo "=============================="
    date "+%G-%m-%dT%H:%M:%S %z"
}

if [ "$SUBSYSTEM" != "usb_device" -a "$SUBSYSTEM" != "pcmcia" -a "$SUBSYSTEM" != "mmc" -a "$SUBSYSTEM" != "power_supply" ]
then
    exit 0
fi
if [ "$SUBSYSTEM" = "power_supply" -a "$ACTION" = "change" ]
then
    if [ "$POWER_SUPPLY_ONLINE" = "0" ]
    then
        PRINT_DATE
        echo "$SUBSYSTEM $ACTION OFFLINE"
        echo "aplay -q \"$SND_REMOVE\""
        aplay -q "$SND_REMOVE"
    elif  [ "$POWER_SUPPLY_ONLINE" = "1" ]
    then
        PRINT_DATE
        echo "$SUBSYSTEM $ACTION ONLINE"
        echo "aplay -q \"$SND_INSERT\""
        aplay -q "$SND_INSERT"
    fi
else
    if [ "$ACTION" = "add" ]
    then
        PRINT_DATE
        echo "$SUBSYSTEM $ACTION"
        echo "aplay -q \"$SND_INSERT\""
        aplay -q "$SND_INSERT"
    elif [ "$ACTION" = "remove" ]
    then
        PRINT_DATE
        echo "$SUBSYSTEM $ACTION"
        echo "aplay -q \"$SND_REMOVE\""
        aplay -q "$SND_REMOVE"
    fi
fi
exit $?

這個第三個版本,我們在程式的前面,定義了一個函數叫做 "PRINT_DATE ()",它是用來避免重複的程式碼(repeated codes),使得程式比較簡短易讀(readability)。

還記得我們把這個程式的所有 stdout 以及 stderr 都導向 $LOGFILE 了嗎?在指令行打:
$ less /tmp/my_script.log 
則會看到類似以下的文字紀錄(logs):

==============================
2011-11-11T05:02:52 +0800
pcmcia add
aplay -q "/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"

==============================
2011-11-11T05:03:01 +0800
pcmcia remove
aplay -q "/xp/WINDOWS/Media/chimes.wav"

==============================
2011-11-11T05:03:16 +0800
mmc add
aplay -q "/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"

==============================
2011-11-11T05:03:23 +0800
mmc remove
aplay -q "/xp/WINDOWS/Media/chimes.wav"

==============================
2011-11-11T07:24:26 +0800
power_supply change OFFLINE
aplay -q "/xp/WINDOWS/Media/chimes.wav"

==============================
2011-11-11T07:24:29 +0800
power_supply change ONLINE
aplay -q "/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"

==============================
2011-11-11T05:05:18 +0800
usb_device add
aplay -q "/xp/WINDOWS/Media/Windows XP Pop-up Blocked.wav"

==============================
2011-11-11T05:05:46 +0800
usb_device remove
aplay -q "/xp/WINDOWS/Media/chimes.wav"

本單元未完待續(this unit is yet to be continued)...

上一個單元:Linux 的 udev 是個有趣又有用的玩具(2)
下一個單元:Linux 的 udev 是個有趣又有用的玩具(4)

沒有留言:

張貼留言