熱插拔
1.熱插拔:就是帶電插拔,即允許用戶在不關閉系統,不切斷電源的情況下拆卸或安裝硬盤,板卡等設備。熱插拔是內核和用戶空間之間,通過調用用戶空間程序實現交互來實現的,當內核發生了某種熱拔插事件時,內核就會調用用戶空間的程序來實現交互。熱插拔機制有devfs、udev和mdev,devfs如今已經不再使用。嵌入式設備上一般使用mdev,X86上一般用udev,當然嵌入式設備上也可以用udev,mdev是udev的簡化版本。udev是基于netlink機制實現的,通過udevd守護進程監聽內核發送的uevent事件來執行相應的熱插拔操作。而mdev是基于uevent helper機制,內核產生的uevent會調用uevent_helper所指的用戶程序medv來執行熱拔插動作。
2.int kobject_uevent(struct kobject *kobj, enum kobject_action action);函數可用來在內核中向用戶空間發送設備事件通知uevent,觸發用戶態的udev/mdev等設備管理工具響應設備狀態變化,該函數執行成功返回0。其中,kobj是關聯的內核對象指針,代表觸發事件的設備或子系統;action是發生的事件類型,包括下圖所示的幾種事件:
udevadm是Linux系統中用于管理和調試udev設備管理器的核心命令行工具,它允許用戶直接與udev交互,查詢設備信息、觸發事件、監控設備變動或調試規則。使用方法為:
例如可以使用udevadm monitor命令監聽所有內核設備事件,示例如下:
kobject_uevent函數向用戶態發送事件時會調用kobject_uevent_env函數,如下圖:
kobject_uevent_env函數可用來發送帶有環境變量數據的事件。kobject_uevent_env函數會根據事件的類型進行對應的操作,但是這一流程是借助kset來實現的(uevent是通過netlink socket發送給用戶空間的應用程序的,而netlink socket是基于kset的),所以發送事件的kobject必須屬于某個kset,否則會導致事件發送失敗,如下圖:
如上圖所示,在獲取到發送事件的kobject所屬的kset以及該kset的事件操作uevent_ops后,kobject_uevent_env函數依次執行這些操作,如下圖:
最終kobject_uevent_env函數會廣播要發送的事件,以便用戶空間的應用程序可以接收并處理這些事件(對應udev)。另外如果定義了CONFIG_UEVENT_HELPER則會調用用戶空間的uevent_helper程序(可將其設置成mdev)來處理uevent事件,如下圖:
3.kset->uevent_ops中定義了三個函數,如下圖:
其中,filter函數用于過濾,即當一個kobject想要向用戶空間發送uevent時,由filter函數決定這個uevent是否應該被發送;name函數用于為uevent生成一個特定的名稱字符串,這個名稱會被添加到uevent的環境變量中,幫助用戶空間應用程序識別事件來源;uevent函數來填充或修改發送到用戶空間的uevent消息中的環境變量。一個示例如下圖:
4.Linux提供了多種方式實現內核和用戶空間的數據交換,比如系統調用、sysfs等,但是這些通信機制均為單工通信機制。而netlink是基于socket通信機制,具有雙工通信的特點,可以很好的滿足內核和用戶空間的數據交換。因為netlink是基于socket通信機制,所以需要在用戶空間使用socket接口實現。首先介紹幾個函數:
- int socket(int domain, int type, int protocol):用于創建套接字。其中domain表示所用協議,使用netlink機制時將其設置為AF_NETLINK;type表示套接字的類型,指定通信的方式和特性,使用netlink機制時將其設置為SOCK_RAW;protocol表示套接字使用的協議,通常設置為0,讓系統自動選擇適當的協議,在接收uevent時可將其設置為NETLINK_KOBJECT_UEVENT;該函數調用成功返回創建的套接字對應的文件描述符,失敗返回-1并設置errno
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):用于將創建的套接字與指定的地址結構綁定。其中sockfd為套接字對應的文件描述符;addr為傳入參數,在接收uevent時通常使用sockaddr_nl結構體(強轉為sockaddr類型),這個結構體成員包括協議族(這里應為AF_NETLINK),當前進程PID;addrlen為addr的長度;該函數綁定成功返回0,失敗返回-1并設置errno
- ssize_t recv(int sockfd, void *buf, size_t len, int flags):用于接收內核發出的uevent事件。注意與一般網絡編程不同,在netlink中是不用調用listen函數的,可以直接使用recv函數進行接收。其中sockfd為套接字對應的文件描述符;buf指向接收數據的緩沖區;len指定要讀取的數據的字節數;flags指定一些標志用于控制如何接收數據,通常設置為0;成功情況下該函數返回實際讀取到的字節數
netlink需要在用戶空間循環讀取內核發來的uevent,下圖是一個例子(可參考訊為Linux驅動視頻第十期P5):
5.對于uevent helper機制,要想在kobject_uevent_env函數中調用用戶空間的uevent_helper程序來處理uevent事件,則需要定義CONFIG_UEVENT_HELPER,并且需要定義uevent_helper的路徑(即CONFIG_UEVENT_HELPER_PATH的值),如下圖:
有以下幾種配置方法(可參考訊為Linux驅動視頻第十期P6):
- 在編譯內核時直接配置CONFIG_UEVENT_HELPER_PATH:make menuconfig打開圖形化配置界面后,選中Device Drivers->Generic Driver Options->Support for uevent helper后配置path to uevent helper,即配置uevent_helper的路徑(例如可將其設置為/sbin/mdev)
- make menuconfig打開圖形化配置界面后,依次選中:Device Drivers->Generic Driver Options->Support for uevent helper(這一步是打開宏定義CONFIG_UEVENT_HELPER)、File systems->Pseudo fllesystems->/proc file system support、File systems->Pseudo fllesystems-> Sysctl support(/proc/sys)、Networking support。選中上述幾個配置之后,就可以通過命令echo /sbin/mdev > /sys/kernel/uevent_helper對uevent_helper進行設置,或通過命令echo /sbin/mdev > /proc/sys/kernel/hotplug對uevent_helper進行設置(這兩種設置方法實際就是通過對屬性文件進行讀寫實現的)
一個簡單的mdev程序如下圖所示(可參考訊為Linux驅動視頻第十期P7):
需要注意的是,kobject_uevent_env函數中調用的call_usermodehelper_exec函數是一個在內核空間中調用用戶空間程序的函數,該函數執行用戶空間程序時,將其作為子進程運行,并將其標準輸入、標準輸出和標準錯誤輸出重定向到相應的文件描迷符。因此如果在用戶空間程序中使用printf打印信息,這些信息將被輸出到標準輸出文件描述符(文件描述符1),而不是終端。因此需要在調用call_usermodehelper_exec時將標準輸出重定向到終端,這樣才可以在終端上看到printf輸出的信息。
6.實現U盤熱插拔的幾個步驟,采用udev(可參考訊為Linux驅動視頻第十期P8):
- 首先需要在編譯源碼時配置所使用的Linux系統支持udev,例如對于buildroot文件系統,執行make menuconfig之后將System configuration->/dev management設置為Dynamic using devtmpfs + eudev表示使用udev
- 啟動系統后在/etc/udev/rules.d/目錄下創建一個001.rules文件(若沒有rules.d/目錄則創建),其中001表示第一個規則文件,.rules是固定后綴。向在001.rules文件寫入以下內容:
第一行表示當新增一個usb設備,執行/etc/udev/rules.d/usb/usb-add.sh腳本文件,并傳入參數sd[a-z][0-9],第二行表示當移除一個usb設備,執行/etc/udev/rules.d/usb/usb-remove.sh腳本文件KERNEL=="sd[a-z][0-9]",SUBSYSTEM=="block",ACTION=="add",RUN+="/etc/udev/rules.d/usb/usb-add.sh %k" SUBSYSTEM=="block",ACTION=="remove",RUN+="/etc/udev/rules.d/usb/usb-remove.sh"
- 分別創建/etc/udev/rules.d/usb/usb-add.sh和/etc/udev/rules.d/usb/usb-remove.sh文件,分別寫入以下內容:
#!/bin/sh/bin/mount -t vfat /dev/$1 /mnt sync
#!/bin/shsync /bin/unmount -l /mnt
還可以在/lib/udev/rules.d/目錄下創建規則文件,但是/etc/udev/rules.d/比/lib/udev/rules.d的優先級高。TF卡的udev熱插拔實現方式和U盤類似,只是U盤的節點名格式為sd[a-z][0-9],而TF卡的節點名格式為mmcblk[0-9]p[0-9]。采用mdev實現U盤和TF卡的熱插拔步驟與udev類似,可參考訊為Linux驅動視頻第十期P10、P11。
7.USBmount是一個用于自動掛載USB存儲設備的工具,它可以在Linux系統中自動掛載插入的USB存儲設備并在設備拔出時自動卸載。USBmount的工作原理是通過udev監視USB設備的插拔事件,并在檢測到設備插入時自動掛載設備,檢測到設備拔出時自動卸載設備。USBmount不需要手動掛載或卸載USB存儲設備,因此可以方便地在嵌入式系統中使用(USBmount使用方式可參考訊為Linux驅動視頻第十期P12)。