udev 入門:管理設備事件的 Linux 子系統
udev 是一個為你的計算機提供設備事件的 Linux 子系統。通俗來講就是,當你的計算機上插入了像網卡、外置硬碟(包括 U 盤)、滑鼠、鍵盤、遊戲操縱桿和手柄、DVD-ROM 驅動器等等設備時,代碼能夠檢測到它們。這樣就能寫出很多可能非常有用的實用程序,而它已經很好了,普通用戶就可以寫出腳本去做一些事情,比如當某個硬碟驅動器插入時,執行某個任務。
這篇文章教你去如何寫一個由一些 udev 事件觸發的 udev 腳本,比如插入了一個 U 盤。當你理解了 udev 的工作原理,你就可以用它去做各種事情,比如當一個遊戲手柄連接後載入一個指定的驅動程序,或者當你用於備份的驅動器連接後,自動執行備份工作。
一個初級的腳本
使用 udev 的最佳方式是從一個小的代碼塊開始。不要指望從一開始就寫出完整的腳本,而是從最簡單的確認 udev 觸發了某些指定的事件開始。
對於你的腳本,依據你的目標,並不是在任何情況下都能保證你親眼看到你的腳本運行結果的,因此需要在你的腳本日誌中確認它成功觸發了。而日誌文件通常放在 /var
目錄下,但那個目錄通常是 root 用戶的領地。對於測試目的,可以使用 /tmp
,它可以被普通用戶訪問並且在重啟動後就被清除了。
打開你喜歡的文本編輯器,然後輸入下面的簡單腳本:
#!/usr/bin/bash
echo $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.log
Tue 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.log
Tue Oct 31 01:35:28 NZDT 2035
如果你在 /tmp/udev.log
中看到了最新的日期和時間,那麼說明 udev 已經成功觸發了你的腳本。
改進規則做一些有用的事情
現在的問題是使用的規則太通用了。插入一個滑鼠、一個 U 盤、或某個人的 U 盤都將盲目地觸發這個腳本。現在,我們開始專註於希望觸發你的腳本的是確定的某個 U 盤。
實現上述目標的一種方式是使用提供商 ID 和產品 ID。你可以使用 lsusb
命令去得到這些數字。
$ lsusb
Bus 001 Device 002: ID 8087:0024 Slacker Corp. Hub
Bus 002 Device 002: ID 8087:0024 Slacker Corp. Hub
Bus 003 Device 005: ID 03f0:3307 TyCoon Corp.
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 hub
Bus 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 規則當插入一個驅動器後自動去做一些事情,將帶來安全方面的擔憂。在我的機器上,我甚至都沒有打開自動掛載功能,而基於本文的目的,當設備插入時,腳本和規則可以運行一些命令來做一些事情。
在這裡需要記住兩個事情。
- 聚焦於你的 udev 規則,當你真實地使用它們時,一旦讓規則發揮作用將觸髮腳本。執行一個腳本去盲目地複製數據到你的計算上,或從你的計算機上複製出數據,是一個很糟糕的主意,因為有可能會遇到一個人拿著和你相同品牌的 U 盤插入到你的機器上的情況。
- 不要在寫了 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/bash
mount /dev/safety1 /mnt/hd
sleep 2
rsync -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 許可證授權使用。
via: https://opensource.com/article/18/11/udev
作者:Seth Kenlon 選題:lujun9972 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive