相對于linux來說,udev還是一個新事物。然而,盡管它03年才出現,盡管它很低調(J),但它無疑已經成為linux下不可或缺的元件了。udev是什麼?它是如何實作的?最近研究Linux裝置管理時,花了一些時間去研究udev的實作。
udev是什麼?u 是指user space,dev是指device,udev是使用者空間的裝置驅動程式嗎?最初我也這樣認為,調試核心空間的程式要比調試使用者空間的程式複雜得多,核心空間的程式的BUG所引起的後果也嚴重得多,device driver是核心空間中所占比較最大的代碼,如果把這些device driver中硬體無關的代碼,從核心空間移動到使用者空間,自然是一個不錯的想法。
但我的想法并不正确,udev的文檔是這樣說的,
1. dynamic replacement for /dev。作為devfs的替代者,傳統的devfs不能動态配置設定major和minor的值,而major和minor非常有限,很快就會用完了。udev能夠像DHCP動态配置設定IP位址一樣去動态配置設定major和minor。
2. device naming。提供裝置命名持久化的機制。傳統裝置命名方式不具直覺性,像/dev/hda1這樣的名字肯定沒有boot_disk這樣的名字直覺。udev能夠像DNS解析域名一樣去給裝置指定一個有意義的名稱。
3. API to access info about current system devices 。提供了一組易用的API去操作sysfs,避免重複實作同樣的代碼,這沒有什麼好說的。
我們知道,使用者空間的程式與裝置通信的方法,主要有以下幾種方式,
1. 通過ioperm擷取操作IO端口的權限,然後用inb/inw/ inl/ outb/outw/outl等函數,避開裝置驅動程式,直接去操作IO端口。(沒有用過)
2. 用ioctl函數去操作/dev目錄下對應的裝置,這是裝置驅動程式提供的接口。像鍵盤、滑鼠和觸摸屏等輸入裝置一般都是這樣做的。
3. 用write/read/mmap去操作/dev目錄下對應的裝置,這也是裝置驅動程式提供的接口。像framebuffer等都是這樣做的。
上面的方法在大多數情況下,都可以正常工作,但是對于熱插撥(hotplug)的裝置,比如像U盤,就有點困難了,因為你不知道:什麼時候裝置插上了,什麼時候裝置拔掉了。這就是所謂的hotplug問題了。
處理hotplug傳統的方法是,在核心中執行一個稱為hotplug的程式,相關參數通過環境變量傳遞過來,再由hotplug通知其它關注hotplug事件的應用程式。這樣做不但效率低下,而且感覺也不那麼優雅。新的方法是采用NETLINK實作的,這是一種特殊類型的socket,專門用于核心空間與使用者空間的異步通信。下面的這個簡單的例子,可以監聽來自核心hotplug的事件。
#include <stdio .h> #include <stdlib.h> #include <string .h> #include <ctype .h> #include <sys/un.h> #include <sys/ioctl.h> #include <sys/socket .h> #include <linux/types.h> #include <linux/netlink.h> #include <errno .h> staticintinit_hotplug_sock(void ) { structsockaddr_nl snl ; constintbuffersize = 16 * 1024 * 1024; intretval ; memset(&snl, 0x00, sizeof(struct sockaddr_nl)); snl .nl_family = AF_NETLINK; snl.nl_pid = getpid (); snl .nl_groups = 1; inthotplug_sock= socket(PF_NETLINK, SOCK_DGRAM , NETLINK_KOBJECT_UEVENT); if(hotplug_sock == -1) { printf("error getting socket: %s", strerror(errno )); return -1; } setsockopt(hotplug_sock, SOL_SOCKET, SO_RCVBUFFORCE, &buffersize, sizeof(buffersize )); retval= bind(hotplug_sock, (structsockaddr*) &snl, sizeof(struct sockaddr_nl)); if(retval < 0) { printf("bind failed: %s", strerror(errno )); close(hotplug_sock ); hotplug_sock = -1; return -1; } returnhotplug_sock ; } #define UEVENT_BUFFER_SIZE 2048 intmain(intargc, char* argv []) { inthotplug_sock = init_hotplug_sock (); while (1) { charbuf[UEVENT_BUFFER_SIZE *2] = {0}; recv(hotplug_sock, &buf, sizeof(buf ), 0); printf("%s\n", buf ); } return 0; } |
編譯:
gcc -g hotplug.c -o hotplug_monitor
運作後插/拔U盤,可以看到:
[email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/usbdev2.2_ep00 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0 [email protected]/class/scsi_host/host2 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep81 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep02 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep83 [email protected]/class/usb_device/usbdev2.2 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/host2/target2:0:0/2:0:0:0 [email protected]/class/scsi_disk/2:0:0:0 [email protected]/block/sda [email protected]/block/sda/sda1 [email protected]/class/scsi_device/2:0:0:0 [email protected]/class/scsi_generic/sg0 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep81 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep02 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep83 [email protected]/class/scsi_generic/sg0 [email protected]/class/scsi_device/2:0:0:0 [email protected]/class/scsi_disk/2:0:0:0 [email protected]/block/sda/sda1 [email protected]/block/sda [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/host2/target2:0:0/2:0:0:0 [email protected]/class/scsi_host/host2 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0 [email protected]/class/usb_device/usbdev2.2 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1/usbdev2.2_ep00 [email protected]/devices/pci0000:00/0000:00:1d.1/usb2/2-1 |
udev的主體部分在udevd.c檔案中,它主要監控來自4個檔案描述符的事件/消息,并做出處理:
1. 來自用戶端的控制消息。這通常由udevcontrol指令通過位址為/org/kernel/udev/udevd的本地socket,向udevd發送的控制消息。其中消息類型有:
l UDEVD_CTRL_STOP_EXEC_QUEUE 停止處理消息隊列。
l UDEVD_CTRL_START_EXEC_QUEUE 開始處理消息隊列。
l UDEVD_CTRL_SET_LOG_LEVEL 設定LOG的級别。
l UDEVD_CTRL_SET_MAX_CHILDS 設定最大子程序數限制。好像沒有用。
l UDEVD_CTRL_SET_MAX_CHILDS_RUNNING 設定最大運作子程序數限制(周遊proc目錄下所有程序,根據session的值判斷)。
l UDEVD_CTRL_RELOAD_RULES 重新加載配置檔案。
2. 來自核心的hotplug事件。如果有事件來源于hotplug,它讀取該事件,建立一個udevd_uevent_msg對象,記錄目前的消息序列号,設定消息的狀态為EVENT_QUEUED,然後并放入running_list和exec_list兩個隊列中,稍後再進行處理。
3. 來自signal handler中的事件。signal handler是異步執行的,即使有signal産生,主程序的select并不會喚醒,為了喚醒主程序的select,它建立了一個管道,在signal handler中,向該管道寫入長度為1個子節的資料,這樣就可以喚醒主程序的select了。
4. 來自配置檔案變化的事件。udev通過檔案系統inotify功能,監控其配置檔案目錄/etc/udev/rules.d,一旦該目錄中檔案有變化,它就重新加載配置檔案。
其中最主要的事件,當然是來自核心的hotplug事件,如何處理這些事件是udev的關鍵。udev本身并不知道如何處理這些事件,也沒有必要知道,因為它隻實作機制,而不實作政策。事件的處理是由配置檔案決定的,這些配置檔案即所謂的rule。
關于rule的編寫方法可以參考《writing_udev_rules》,udev_rules.c實作了對規則的解析。
在規則中,可以讓外部應用程式處理某個事件,這有兩種方式,一種是直接執行指令,通常是讓modprobe去加載驅動程式,或者讓mount去加載分區。另外一種是通過本地socket發送消息給某個應用程式。
在udevd.c:udev_event_process函數中,我們可以看到,如果RUN參數以”socket:”開頭則認為是發到socket,否則認為是執行指定的程式。
下面的規則是執行指定程式:
60-pcmcia.rules: RUN+="/sbin/modprobe pcmcia"
下面的規則是通過socket發送消息:
90-hal.rules:RUN+="socket:/org/freedesktop/hal/udev_event"
hal正是我們下一步要關心的,接下來我會分析HAL的實作原理。
另外一篇見:http://hi.baidu.com/littertiger/blog/item/315d12dd8448fedc8c102918.html
kernel和udev間傳遞消息
kernel和udev間傳遞消息,比如add,remove等,是通過netlink進行。 netlink是個通用的機制,傳遞udev event隻是其中一個應用。
由于uevent是廣播的,是以寫個小程式很容易不會這些事件。
寫得比較匆忙,也很醜陋,呵呵。
#include <stdio.h>
#include <sys/socket.h>
#include <linux/netlink.h>
char buf[2048 + 512];
int main()
{
int bufsize = 16 * 1024 * 1024;
struct sockaddr_nl anl;
int res, i, sk;
anl.nl_family = AF_NETLINK;
anl.nl_pid = getpid();
anl.nl_groups = 1;
sk = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (sk == -1){
printf("socket create failed\n");
return sk;
}
setsockopt(sk, SOL_SOCKET, SO_RCVBUFFORCE, &bufsize, sizeof(bufsize));
res = bind(sk, &anl, sizeof(anl));
if (res == -1){
close(sk);
printf("socket bind failed\n");
return res;
}
while(1){
res = recv(sk, buf, sizeof(buf), 0);
printf("res:%d\n",res);
res = res > sizeof(buf) ? sizeof(buf) : res;
printf("begin\n");
for(i=0; i<res; i++)
putchar(buf );
printf("end\n");
}
return 0;
}
另外:
Udev的代碼樹裡的版本很多,我下載下傳的最新的版本是udev-117,配合2.6.21版本的核心能夠正常使用。網上很多文章介紹的可能都是稍微早期一些的版本,有些步驟包括udev的README文檔似乎描述的不是很準确。
基本上這個版本的udev需要注意的是,安裝時隻需要udevd,udevadm兩個檔案,其它必需的包括udevtrigger等隻是udevadm的一個符号連結。udevstart不是必需的。當然Udev.conf等配置檔案還是一樣。
2.2 啟動
你可以在啟動腳本中用udevd –d 參數啟動udev檔案系統的守護程序,然後使用udevtrigger将buildin的裝置驅動的節點建立出來,以後子產品插入移除時節點的管理會自動處理。
能夠正常加載udev的前提,基本包括如下操作:
Ø 設定路徑變量
Ø 加載sysfs檔案系統
Ø 加載一個基于ram的可寫的/dev目錄(其實,隻要提供一個可寫的目錄即可,目錄路徑本身也是可以配置的)
Ø /dev目錄下需要有已經建立好的 console節點和null節點
腳本類似:
# Set the path
PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PATH
# mount proc and devpts filesystem
/bin/mount -a
mknod /dev/console c 5 1
mknod /dev/null c 1 3
/sbin/udevd -d
/sbin/udevtrigger
Mount使用的fstab檔案類似:
none /tmp ramfs defaults 0 0
udev /dev ramfs defaults 0 0
none /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
當然,你的系統上可能還會需要預先建立一些其它的裝置節點,比如序列槽的ttySx 才能正常啟動shell,完成以上腳本的執行,那就要看具體情況了。
3 使用中的一些問題的思考
3.1 關于規則的多次比對
幫助文檔中說一個裝置可以被多條規則多次比對,不過,需要明确的一點是:
多次比對隻能添加多個Symlink,不能建立多個Name:
例如:
KERNEL=="mtdblock4", NAME+="mtdbb4"
KERNEL=="mtdblock4", NAME+="%k"
就隻會建立 /dev/mtdbb4 而不會建立/dev/mtdblock4
而類似:
KERNEL=="mtdblock4", NAME+="mtdbb4"
KERNEL=="mtdblock4", SYMLINK+="mtdbb4link"
是可以正常工作的。
3.2 關于udev.conf的文法
可能大家會發現,似乎沒有什麼詳細文檔描述udev.conf的寫法,實際上從udevd的代碼裡可以看出:
udev.conf檔案裡面隻會解析這三個參數:
udev_root 定義udev的目錄路徑
udev_rules 定義udev的規則檔案的目錄路徑
udev_log 定義log的級别
也許以後會添加一些别的配置參數?
4 基本工作原理方面的問題
這部分主要是分析了一下udev的source code,對一些自己關心的問題的了解
4.1 Udevd如何擷取核心的這些子產品動态變化的資訊
裝置節點的建立,是通過sysfs接口分析dev檔案取得裝置節點号,這個很顯而易見。那麼udevd是通過什麼機制來得知核心裡子產品的變化情況,如何得知裝置的插入移除情況呢?當然是通過hotplug機制了,那hotplug又是怎麼實作的?或者說核心是如何通知使用者空間一個事件的發生的呢?
答案是通過netlink socket通訊,在核心和使用者空間之間傳遞資訊。
核心調用kobject_uevent函數發送netlink message給使用者空間,這部分工作通常不需要驅動去自己處理,在統一裝置模型裡面,在子系統這一層面,已經将這部分代碼處理好了,包括在裝置對應的特定的Kobject建立和移除的時候都會發送相應add和remove消息,當然前提是你在核心中配置了hotplug的支援。
Netlink socket作為一種核心與使用者空間的通信方式,不僅僅用在hotplug機制中,同樣還應用在其它很多真正和網絡相關的核心子系統中。
Udevd通過标準的socket機制,建立socket連接配接來擷取核心廣播的uevent事件 并解析這些uevent事件。
4.2 Udevd如何監控規則檔案的變更
如果核心版本足夠新的話,在規則檔案發生變化的時候,udev也能夠自動的重新應用這些規則,這得益于核心的inotify機制, inotify是一種檔案系統的變化通知機制,如檔案增加、删除等事件可以立刻讓使用者态得知。
在udevd中,對inotify和udev的netlink socket檔案描述符都進行了select的等待操作。有事件發生以後再進一步處理。
4.3 Udevtrigger的工作機制?
運作udevd以後,使用udevtrigger的時候,會把核心中已經存在的裝置的節點建立出來,那麼他是怎麼做到這一點的? 分析udevtrigger的代碼可以看出:
udevtrigger通過向/sysfs 檔案系統下現有裝置的uevent節點寫"add"字元串,進而觸發uevent事件,使得udevd能夠接收到這些事件,并建立buildin的裝置驅動的裝置節點以及所有已經insmod的子產品的裝置節點。
是以,我們也可以手工用指令行來模拟這一過程:
/ # echo "add" > /sys/block/mtdblock2/uevent
/ #
/ # UEVENT[178.415520] add /block/mtdblock2 (block)
但是,進一步看代碼,你會發現,實際上,不管你往uevent裡面寫什麼,都會觸發add事件,這個從kernel内部對uevent屬性的實作函數可以看出來,預設的實作是:
static ssize_t store_uevent(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
kobject_uevent(&dev->kobj, KOBJ_ADD);
return count;
}
是以不管寫的内容是什麼,都是觸發add操作,真遺憾,我還想通過這個屬性實驗remove的操作。 不知道這樣限制的原因是什麼。
而udevstart的實作方式和udevtrigger就不同了,它基本上是重複實作了udevd裡面的機制,通過周遊sysfs,自己完成裝置節點的建立,不通過udevd來完成。
4.4 其它
Ø udevd建立每一個節點的時候,都會fork出一個新的程序來單獨完成這個節點的建立工作。
Ø Uevent_seqnum 用來辨別目前的uevent事件的序号(已經産生了多少uevent事件),你可以通過如下操作來檢視:
$ cat /sys/kernel/uevent_seqnum
2673