udev 入門:管理裝置事件的 Linux 子系統

2018-12-09 22:12:00

建立這樣一個指令碼,當指定的裝置插入時觸發你的計算機去做一個指定動作。

udev 是一個為你的計算機提供裝置事件的 Linux 子系統。通俗來講就是,當你的計算機上插入了像網絡卡、外接硬碟(包括 U 盤)、滑鼠、鍵盤、遊戲操縱桿和手柄、DVD-ROM 驅動器等等裝置時,程式碼能夠檢測到它們。這樣就能寫出很多可能非常有用的實用程式,而它已經很好了,普通使用者就可以寫出指令碼去做一些事情,比如當某個硬碟機插入時,執行某個任務。

這篇文章教你去如何寫一個由一些 udev 事件觸發的 udev 指令碼,比如插入了一個 U 盤。當你理解了 udev 的工作原理,你就可以用它去做各種事情,比如當一個遊戲手柄連線後載入一個指定的驅動程式,或者當你用於備份的驅動器連線後,自動執行備份工作。

一個初級的指令碼

使用 udev 的最佳方式是從一個小的程式碼塊開始。不要指望從一開始就寫出完整的指令碼,而是從最簡單的確認 udev 觸發了某些指定的事件開始。

對於你的指令碼,依據你的目標,並不是在任何情況下都能保證你親眼看到你的指令碼執行結果的,因此需要在你的指令碼紀錄檔中確認它成功觸發了。而紀錄檔檔案通常放在 /var 目錄下,但那個目錄通常是 root 使用者的領地。對於測試目的,可以使用 /tmp,它可以被普通使用者存取並且在重新啟動動後就被清除了。

開啟你喜歡的文字編輯器,然後輸入下面的簡單指令碼:

#!/usr/bin/bashecho $date > /tmp/udev.log

把這個指令碼放在 /usr/local/bin 或預設可執行路徑的位置中。將它命名為 trigger.sh,並執行 chmod +x 授予可執行許可權:

$ sudo mv trigger.sh /usr/local/bin$ sudo chmod +x /usr/local/bin/trigger.sh

這個指令碼沒有任何和 udev 有關的事情。當它執行時,這個指令碼將在檔案 /tmp/udev.log 中放入當前的時間戳。你可以自己測試一下這個指令碼:

$ /usr/local/bin/trigger.sh$ cat /tmp/udev.logTue Oct 31 01:05:28 NZDT 2035

接下來讓 udev 去觸發這個指令碼。

唯一裝置識別

為了讓你的指令碼能夠被一個裝置事件觸發,udev 必須要知道在什麼情況下呼叫該指令碼。在現實中,你可以通過它的顏色、製造商、以及插入到你的計算機這一事實來識別一個 U 盤。而你的計算機,它需要一系列不同的標準。

udev 通過序列號、製造商、以及提供商 ID 和產品 ID 號來識別裝置。由於現在你的 udev 指令碼還處於它的生命週期的早期階段,因此要盡可能地寬泛、非特定和包容。換句話說就是,你希望首先去捕獲盡可能多的有效 udev 事件來觸發你的指令碼。

使用 udevadm monitor 命令你可以實時利用 udev,並且可以看到當你插入不同裝置時發生了什麼。用 root 許可權試一試。

$ su# udevadm monitor

該監視函數輸出接收到的事件:

  • UDEV:在規則處理之後發出 udev 事件
  • KERNEL:核心傳送 uevent 事件

udevadm monitor 命令執行時,插入一個 U 盤,你將看到各種資訊在你的螢幕上捲動而出。注意那一個 ADD 事件的事件型別。這是你所需要的識別事件型別的一個好方法。

udevadm monitor 命令提供了許多很好的資訊,但是你可以使用 udevadm info 命令以更好看的格式來看到它,假如你知道你的 U 盤當前已經位於你的 /dev 樹。如果不在這個樹下,拔下它並重新插入,然後立即執行這個命令:

$ su -c 'dmesg | tail | fgrep -i sd*'

舉例來說,如果那個命令返回 sdb: sdb1,說明核心已經給你的 U 盤分配了 sdb 卷標。

或者,你可以使用 lsblk 命令去檢視所有連線到你的系統上的驅動器,包括它的大小和分割區。

現在,你的驅動器已經處於你的檔案系統中了,你可以使用下面的命令去檢視那個裝置的相關 udev 資訊:

# udevadm info -a -n /dev/sdb | less

這個命令將返回許多資訊。現在我們只關心資訊中的第一個塊。

你的任務是從 udev 的報告中找出能唯一標識那個裝置的部分,然後當計算機檢測到這些唯一屬性時,告訴 udev 去觸發你的指令碼。

udevadm info 命令處理一個(由裝置路徑指定的)裝置上的報告,接著“遍歷”父級裝置鏈。對於找到的大多數裝置,它以一個“鍵值對”格式輸出所有可能的屬性。你可以寫一個規則,從一個單個的父級裝置屬性上去匹配插入裝置的屬性。

looking at device '/devices/000:000/blah/blah//block/sdb':  KERNEL=="sdb"  SUBSYSTEM=="block"  DRIVER==""  ATTR{ro}=="0"  ATTR{size}=="125722368"  ATTR{stat}==" 2765 1537 5393"  ATTR{range}=="16"  ATTR{discard\_alignment}=="0"  ATTR{removable}=="1"  ATTR{blah}=="blah"

一個 udev 規則必須包含來自單個父級裝置的一個屬性。

父級屬性是描述一個裝置的最基本的東西,比如它是插入到一個物理埠的東西、或是一個容量多大的東西、或這是一個可移除的裝置。

由於 KERNEL 卷標 sdb 可能會由於分配給在它之前插入的其它驅動器而發生變化,因此捲標並不是一個 udev 規則的父級屬性的好選擇。但是,在做概念論證時你可以使用它。一個事件的最佳候選者是 SUBSYSTEM 屬性,它表示那個裝置是一個 “block” 系統裝置(也就是為什麼我們要使用 lsblk 命令來列出裝置的原因)。

/etc/udev/rules.d 目錄中開啟一個名為 80-local.rules 的檔案,然後輸入如下程式碼:

SUBSYSTEM=="block", ACTION=="add", RUN+="/usr/local/bin/trigger.sh"

儲存檔案,拔下你的測試 U 盤,然後重新啟動動系統。

等等,重新啟動動 Linux 機器?

理論上說,你只需要執行 udevadm control —reload 即可,它將重新載入所有規則,但是在我們實驗的現階段,最好要排除可能影響實驗結果的所有因素。udev 是非常複雜的,為了不讓你躺在床上整晚都在思考為什麼這個規則不能正常工作,是因為語法錯誤嗎?還是應該重新啟動動一下。所以,不管 POSIX 自負地告訴你過什麼,你都應該去重新啟動動一下。

當你的系統重新啟動動完畢之後,(使用 Ctl+Alt+F3 或類似快捷鍵)切換到一個文字控制台,並插入你的 U 盤。如果你執行了一個最新的核心,當你插入 U 盤後你或許可以看到一大堆輸出。如果看到一個錯誤資訊,比如 “Could not execute /usr/local/bin/trigger.sh”,或許是因為你忘了授予這個指令碼可執行的許可權。否則你將看到的是,一個裝置插入,它得到核心裝置分配的一些東西,等等。

現在,見證奇蹟的時刻到了。

$ cat /tmp/udev.logTue Oct 31 01:35:28 NZDT 2035

如果你在 /tmp/udev.log 中看到了最新的日期和時間,那麼說明 udev 已經成功觸發了你的指令碼。

改進規則做一些有用的事情

現在的問題是使用的規則太通用了。插入一個滑鼠、一個 U 盤、或某個人的 U 盤都將盲目地觸發這個指令碼。現在,我們開始專注於希望觸發你的指令碼的是確定的某個 U 盤。

實現上述目標的一種方式是使用提供商 ID 和產品 ID。你可以使用 lsusb 命令去得到這些數位。

$ lsusbBus 001 Device 002: ID 8087:0024 Slacker Corp. HubBus 002 Device 002: ID 8087:0024 Slacker Corp. HubBus 003 Device 005: ID 03f0:3307 TyCoon Corp.Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 hubBus 001 Device 003: ID 13d3:5165 SBo Networks

在這個例子中,TyCoon Corp 前面的 03f0:3307 就表示了提供商 ID 和產品 ID 的屬性。你也可以通過 udevadm info -a -n /dev/sdb | grep vendor 的輸出來檢視這些數位,但是從 lsusb 的輸出中可以很容易地一眼找到這些數位。

現在,可以在你的指令碼中包含這些屬性了。

SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", RUN+="/usr/local/bin/thumb.sh"

測試它(是的,為了確保不會有來自 udev 的影響因素,我們仍然建議先重新啟動一下),它應該會像前面一樣工作,現在,如果你插入一個不同公司製造的 U 盤(因為它們的提供商 ID 不一樣)、或插入一個滑鼠、或插入一個印表機,這個指令碼將不會被觸發。

繼續新增新屬性來進一步專注於你希望去觸發你的指令碼的那個唯一的 U 盤。使用 udevadm info -a -n /dev/sdb 命令,你可以找出像提供商名字、序列號、或產品名這樣的東西。

為了保證思路清晰,確保每次只新增一個新屬性。我們(和在網上看到的其他人)在 udev 規則中所遇到的大多數錯誤都是因為一次新增了太多的屬性,而奇怪為什麼不能正常工作了。逐個測試屬性是最安全的作法,這樣可以確保 udev 能夠成功識別到你的裝置。

安全

編寫 udev 規則當插入一個驅動器後自動去做一些事情,將帶來安全方面的擔憂。在我的機器上,我甚至都沒有開啟自動掛載功能,而基於本文的目的,當裝置插入時,指令碼和規則可以執行一些命令來做一些事情。

在這裡需要記住兩個事情。

  1. 聚焦於你的 udev 規則,當你真實地使用它們時,一旦讓規則發揮作用將觸發指令碼。執行一個指令碼去盲目地複製資料到你的計算上,或從你的計算機上複製出資料,是一個很糟糕的主意,因為有可能會遇到一個人拿著和你相同品牌的 U 盤插入到你的機器上的情況。
  2. 不要在寫了 udev 規則和指令碼後忘記了它們的存在。我知道哪個計算上有我的 udev 規則,這些機器一般是我的個人計算機,而不是那些我帶著去開會或辦公室工作的計算機。一台計算機的 “社交” 程度越高,它就越不能有 udev 規則存在於它上面,因為它將潛在地導致我的資料最終可能會出現在某個人的裝置、或某個人的資料中、或在我的裝置上出現惡意程式。

換句話說就是,隨著一個 GNU 系統提供了一個這麼強大的功能,你的任務是小心地如何使用它們的強大功能。如果你濫用它或不小心謹慎地使用它,最終將讓你出問題,它非常可能會導致可怕的問題。

現實中的 Udev

現在,你可以確認你的指令碼是由 udev 觸發的,那麼,可以將你的關注點轉到指令碼功能上了。到目前為止,這個指令碼是沒有用的,它除了記錄指令碼已經執行過了這一事實外,再沒有做更多的事情。

我使用 udev 去觸發我的 U 盤的 自動備份 。這個創意是,將我正在處理的文件的主副本儲存在我的 U 盤上(因為我隨身帶著它,這樣就可以隨時處理它),並且在我每次將 U 盤插入到那台機器上時,這些主文件將備份回我的計算機上。換句話說就是,我的計算機是備份驅動器,而產生的資料是移動的。原始碼是可用的,你可以隨意檢視 attachup 的程式碼,以進一步限制你的 udev 的測試範例。

雖然我使用 udev 最多的情況就是這個例子,但是 udev 能抓取很多的事件,像遊戲手柄(當連線遊戲手柄時,讓系統去載入 xboxdrv 模組)、攝像頭、麥克風(當指定的麥克風連線時用於去設定輸入),所以應該意識到,它能做的事情遠比這個範例要多。

我的備份系統的一個簡化版本是由兩個命令組成的一個過程:

SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", SYMLINK+="safety%n"SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", RUN+="/usr/local/bin/trigger.sh"

第一行使用屬性去檢測我的 U 盤,這在前面已經討論過了,接著在裝置樹中為我的 U 盤分配一個符號連結,給它分配的符號連線是 safety%n。這個 %n 是一個 udev 宏,它是核心分配給這個裝置的任意數位,比如 sdb1、sdb2、sdb3、等等。因此 %n 應該是 1 或 2 或 3。

這將在 dev 樹中建立一個符號連結,因此它不會干涉插入一個裝置的正常過程。這意味著,如果你在自動掛載裝置的桌面環境中使用它,將不會出現問題。

第二行執行這個指令碼。

我的備份指令碼如下:

#!/usr/bin/bashmount /dev/safety1 /mnt/hdsleep 2rsync -az /mnt/hd/ /home/seth/backups/ && umount /dev/safety1

這個指令碼使用符號連結,這將避免出現 udev 命名導致的意外情況(例如,假設一個命名為 DISK 的 U 盤已經插入到我的計算機上,而我插入的其它 U 盤恰好名字也是 DISK,那麼第二個 U 盤的卷標將被命名為 DISK_,這將導致我的指令碼不會正常執行),它在我喜歡的掛載點 /mnt/hd 上掛載了 safety1(驅動器的第一個分割區)。

一旦 safely 掛載之後,它將使用 rsync 將驅動器備份到我的備份資料夾(我真實使用的指令碼用的是 rdiff-backup,而你可以使用任何一個你喜歡的自動備份解決方案)。

udev 讓你的裝置你做主

udev 是一個非常靈活的系統,它可以讓你用其它系統很少敢提供給使用者的方式去定義規則和功能。學習它,使用它,去享受 POSIX 的強大吧。

本文內容來自 Slackermedia Handbook,它以 GNU Free Documentation License 1.3 許可證授權使用。