天天看點

udev的實作原理

相對于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 

繼續閱讀