熱插拔
有 2 個不同角度來看待熱插拔:
從內核角度看,熱插拔是在硬件、內核和內核驅動之間的交互。
從用戶角度看,熱插拔是內核和用戶空間之間,通過調用用戶空間程序(如hotplug、udev 和 mdev)的交互。 當需要通知用戶內核發生了某種熱插拔事件時,內核才調用這個用戶空間程序。
現在的計算機系統,要求 Linux 內核能夠在硬件從系統中增刪時,可靠穩定地運行。這就對設備驅動作者增加了壓力,因為在他們必須處理一個毫無征兆地突然出現或消失的設備。
熱插拔工具
當用戶向系統添加或刪除設備時,內核會產生一個熱插拔事件,並在 /proc/sys/kernel/hotplug 文件里查找處理設備連接的用戶空間程序。這個用戶空間程序主要有
hotplug:這個程序是一個典型的 bash 腳本, 只傳遞執行權給一系列位於 /etc/hot-plug.d/ 目錄樹的程序。hotplug 腳本搜索所有的有 .hotplug 后綴的可能對這個事件進行處理的程序並調用它們, 並傳遞給它們許多不同的已經被內核設置的環境變量。(基本已被淘汰,具體內容請參閱《LDD3》)
《
UDEV Primer》(英文),地址:
http://webpages.charter.net/decibelshelp/LinuxHelp_UDEVPrimer.html
《udev規則編寫》(luofuchong翻譯),地址:
http://www.cnitblog.com/luofuchong/archive/2007/12/18/37831.html
《udev-FAQ 中文翻譯》地址:
http://gnawux.bokee.com/3225765.html
在《LFS》中也有介紹udev的使用,很值得參考!下載地址:
http://lfs.osuosl.org/lfs/downloads/stable/
|
因為hotplug現在也在被慢慢地淘汰,udev不再依賴hotplug了,所以這里不再介紹;
udev較mdev復雜,不太適合嵌入式使用。(本人也有做udev的實驗,交叉編譯是通過了,但是使用上有問題,沒有實現其功能。也許是我的文件系統沒做好,以后有時間再研究和寫記錄。有成功高人的通知一聲,交流一下經驗。^_^謝謝!);
mdev簡單易用,比較適合嵌入式系統,實驗成功。以下詳細介紹mdev的使用。
================================
設備節點的創建,是通過sysfs接口分析dev文檔取得設備節點號,這個很顯而易見。那么udevd是通過什么機制來得知內核里模塊的變化情況,如何得知設備的插入移除情況呢?當然是通過hotplug機制了,那 hotplug又是怎么實現的?或說內核是如何通知用戶空間一個事件的發生的呢?
答案是通過netlink socket通訊,在內核和用戶空間之間傳遞信息。
內核調用kobject_uevent函數發送netlink message給用戶空間,這部分工作通常無需驅動去自己處理,在統一設備模型里面,在子系統這一層面,已將這部分代碼處理好了,包括在設備對應的特定的 Kobject創建和移除的時候都會發送相應add和remove消息,當然前提是您在內核中配置了hotplug的支持。
Netlink socket作為一種內核和用戶空間的通信方式,不但僅用在hotplug機制中,同樣還應用在其他很多真正和網絡相關的內核子系統中。
Udevd通過標准的socket機制,創建socket連接來獲取內核廣播的uevent事件 並解析這些uevent事件
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來完成。
udevd創建每一個節點的時候,都會fork出一個新的進程來單獨完成這個節點的創建工作。
Uevent_seqnum 用來標識當前的uevent事件的序號(已產生了多少uevent事件),您能夠通過如下操作來查看:
$ cat /sys/kernel/uevent_seqnum
2673
udev的工作原理 當系統內核發現安裝或者卸載了某一個硬件設備時,內核會執行hotplug,以便讓hotplug去安裝或卸載該硬件的驅動程序;hotplug在處理完硬件的驅動程序后,就會去呼叫執行udevd,以便讓udevd可以產生或者刪除硬件的設備文件。 接着udevd會通過libsysfs讀取sys文件系統,以便取得該硬件設備的信息;然后再向namedev查詢該外部設備的設備文件信息,例如文件的名稱、權限等。最后,udevd就依據上述的結果,在/dev/目錄中自動建立該外部設備的設備文件,同時在/etc/udev/rules.d下檢查有無針對該設備的使用權限
====================
1.kobject, ktype, kset
kobject代表sysfs中的目錄。
ktype代表kobject的類型,主要包含release函數和attr的讀寫函數。比如,所有的bus都有同一個bus_type;所有的class都有同一個class_type。
kset包含了subsystem概念,kset本身也是一個kobject,所以里面包含了一個kobject對象。另外,kset中包含kset_uevent_ops,里面主要定義了三個函數
int (*filter)(struct kset *kset, struct kobject *kobj);
const char *(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env);
這三個函數都與uevent相關。filter用於判斷uevent是否要發出去。name用於得到subsystem的名字。uevent用於填充env變量。
2.uevent內核部分
uevent是sysfs向用戶空間發出的消息。比如,device_add函數中,會調用kobject_uevent(&dev->kobj, KOBJ_ADD); 這里kobj是發消息的kobj,KOBJ_ADD是發出的事件。uevent的事件在kobject_action中定義:
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
kobject_uevent_env:
由kobject的parent向上查找,直到找到一個kobject包含kset。
如果kset中有filter函數,調用filter函數,看看是否需要過濾uevent消息。
如果kset中有name函數,調用name函數得到subsystem的名字;否則,subsystem的名字是kset中kobject的名字。
分配一個kobj_uevent_env,並開始填充env環境變量:
增加環境變量ACTION=<action name>
增加環境變量DEVPATH=<kobj’s path>
增加環境變量SUBSYSTEM=<subsystem name>
增加環境變量kobject_uevent_env中參數envp_ext指定的環境變量。
調用kset的uevent函數,這個函數會繼續填充環境變量。
增加環境變量SEQNUM=<seq>,這里seq是靜態變量,每次累加。
調用netlink發送uevent消息。
調用uevent_helper,最終轉換成對用戶空間sbin/mdev的調用。
3.uevent用戶空間部分
uevent的用戶空間程序有兩個,一個是udev,一個是mdev。
udev通過netlink監聽uevent消息,它能完成兩個功能:
1.自動加載模塊
2.根據uevent消息在dev目錄下添加、刪除設備節點。
另一個是mdev,mdev在busybox的代碼包中能找到,它通過上節提到的uevent_helper函數被調用。
下面簡要介紹udev的模塊自動加載過程:
etc目錄下有一個uevent規則文件/etc/udev/rules.d/50-udev.rules
udev程序收到uevent消息后,在這個規則文件里匹配,如果匹配成功,則執行這個匹配定義的shell命令。例如,規則文件里有這么一行:
ACTION=="add", SUBSYSTEM=="?*", ENV{MODALIAS}=="?*", RUN+="/sbin/modprobe $env{MODALIAS}"
所以,當收到uevent的add事件后,shell能自動加載在MODALIAS中定義的模塊。
mdev的模塊自動加載過程與之類似,它的配置文件在/etc/mdev.conf中。例如:
$MODALIAS=.* 0:0 660 @modprobe "$MODALIAS"
這條規則指的是:當收到的環境變量中含有MODALIAS,那么加載MODALIAS代表的模塊。
mdev的詳細說明在busybox的docs/mdev.txt中。
4.uevent在設備驅動模型中的應用
在sys目錄下有一個子目錄devices,代表一個kset。
創建設備時,調用的device_initialize函數中,默認會把kset設置成devices_kset,即devices子目錄代表的kset。
devices_kset中設置了uevent操作集device_uevent_ops。
static struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};
dev_uevent_filter中,主要是規定了要想發送uevent,dev必須有class或者bus。
dev_uevent_name中,返回dev的class或者bus的名字。
dev_uevent函數:
如果dev有設備號,添加環境變量MAJOR與MINOR。
如果dev->type有值,設置DEVTYPE=<dev->type->name>。
如果dev->driver,設置DRIVER=<dev->driver->name>。
如果有bus,調用bus的uevent函數。
如果有class,調用class的uevent函數。
如果有dev->type,調用dev->type->uevent函數。
一般在bus的uevent函數中,都會添加MODALIAS環境變量,設置成dev的名字。這樣,uevent傳到用戶空間后,就可以通過對MODALIAS的匹配自動加載模塊。這樣的bus例子有platform和I2C等等。
==========================
熱插拔(hotplug,打這個詞的時候我常常想到熱干面)不一定非要指類似U盤那樣的插入拔出,此處的熱插拔廣義上講,是指一個設備加入系統,內核如何通知用戶空間。舉個簡單的例子,如果你的電腦中有塊PCI網卡,針對該網卡的驅動程序以內核模塊的形式被編譯(obj-m),那么Linux系統在啟動過程中是如何自動加載該網卡的驅動模塊呢?大家都知道現在udev負責干這事,其實除了udev,還可以有其他的手法,你自己就可以這樣做。
我們先討論udev,udev最關鍵的東西是當系統發現一個設備時,它要能夠被通知該事件,一旦它知道了這件事,那么余下的事情就都好說了,無非是個如何查找模塊並加載的過程。所以我們看到,這里的關鍵是熱插拔事件的通知機制。Linux的設備模型為此提供了非常完美的支持,其原理其實發源於kset這一層,對此在《深入Linux設備驅動程序內核機制》一書中有詳細的描述,雖然這部分看起來蠻復雜,貌似挺能嚇唬住一些新手,其實說白了,要點就是通過sysfs建立關系,溝通內核與用戶空間,然后就是uevent,也就是下面要說的熱插拔事件。
當然設備驅動程序一般不會和這些太底層的kobject/kset家伙打交道,因為更高層次的device,bus和driver把kobject/kset那一層的細節實現都給封裝了起來。所以設備熱插拔的uevent事件最終的源頭來自於device_add,本帖這里肯定不會討論device與driver如何綁定那一攤子事情。下面看看device_add的源碼,是如何實現uevent機制的:
- <drivers/base/core.c>
- int device_add(struct device *dev)
- {
- ...
- kobject_uevent(&dev->kobj, KOBJ_ADD);
- ...
- }
熱插拔的核心實現就那一個函數調用,這里device_add對應的是KOBJ_ADD,那么移除設備自然對應KOBJ_REMOVE了。kobject_uevent函數最終調用的是kobject_uevent_env,后者才是真正干事的伙計。
下面給出kobject_uevent_env函數的核心框架:
- int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
- char *envp_ext[])
- {
- ...
- #if defined(CONFIG_NET)
- /* send netlink message */
- ...
- #endif
- /* call uevent_helper, usually only enabled during early boot */
- if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
- char *argv [3];
- argv [0] = uevent_helper;
- argv [1] = (char *)subsystem;
- argv [2] = NULL;
- retval = add_uevent_var(env, "HOME=/");
- if (retval)
- goto exit;
- retval = add_uevent_var(env,
- "PATH=/sbin:/bin:/usr/sbin:/usr/bin");
- if (retval)
- goto exit;
- retval = call_usermodehelper(argv[0], argv,
- env->envp, UMH_WAIT_EXEC);
- }
- ...
- }
怎么樣,夠簡潔吧,其實看實際的代碼比這要郁悶地多,不過骨架清晰就行了。代碼中的netlink message就不用多說了吧,給udev發通知用(有時間的話可以分析分析udev的代碼)。本帖重點討論后半段的if (uevent_helper[0] && !kobj_usermode_filter(kobj))代碼,這里的核心調用是call_usermodehelper,這個函數最有意思的地方就在於在內核空間調用用戶空間的程序,它的詳細實現機制在書中已經講得很多,這里就不再贅述了。call_usermodehelper在kobject_uevent_env函數中要調用的用戶空間程序由uevent_helper[0]來指定,所以如果我們能控制這個uevent_helper[0],就能接收到設備加入系統移出系統等事件。那個if中的kobj_usermode_filter條件一般都會滿足(除非這是個特別注意個人隱私的設備,那就不好說了,人家偷偷加入系統就是不想讓你知道你也沒有辦法,但是udev還是能知道的)。
下面看看uevent_helper[0]來自何處:
- <lib/kobject_uevent.c>
- char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
貌似要通過內核配置來指定,我看了一下我系統中Linux目錄下的.config文件,找到了下面這行:
- <linux-3.1.6/.config>
- #
- # Generic Driver Options
- #
- CONFIG_UEVENT_HELPER_PATH=""
丫的,居然沒指定,那么uevent_helper[0]="",這樣的話我們在kobject_uevent_env函數中的那個if語句就沒法滿足了,看來要重新配置再編譯內核了。不過想想sysfs這么強大,內核開發的那幫人好歹給留個用戶空間的接口出來吧,一查看還真有:
<kernel/ksysfs.c>
- static ssize_t uevent_helper_store(struct kobject *kobj,
- struct kobj_attribute *attr,
- const char *buf, size_t count)
- {
- if (count+1 > UEVENT_HELPER_PATH_LEN)
- return -ENOENT;
- memcpy(uevent_helper, buf, count);
- uevent_helper[count] = '\0';
- if (count && uevent_helper[count-1] == '\n')
- uevent_helper[count-1] = '\0';
- return count;
- }
尼瑪,爽得簡直是一塌糊塗,雖然俺那台馬力強勁的機器編個全新的內核不過幾分鍾的事情,但是哪里有上面這個方法爽啊。馬上進入到/sys/kernel目錄下ls一把,截屏如下(點擊放大):
有個uevent_helper文件不是?那么我們現在可以把我們用戶空間的程序給打進去了,我打算做個最簡單的腳本/sbin/myhotplug,這個腳本只干一件事,在/home/dennis目錄下生成一個hotplug文件:
</sbin/myhotplug>
- #!/bin/sh
- cd /home/dennis
- touch hotplug
然后把這個腳本程序的文件名給打入到內核空間的uevent_helper[0]上:
- root@build-server:/sys/kernel# echo "/sbin/myhotplug" > uevent_helper
- root@build-server:/sys/kernel# cat uevent_helper
- /sbin/myhotplug
好了,現在檢查一下你的/home/dennis目錄下面有沒有hotplug這個文件,有的話就刪掉,否則怎么知道是新生成的呢。現在,找個U盤插到你的電腦里,然后再看一下/home/dennis目錄,有個hotplug文件對吧?如果你現在刪除這個文件,再把U盤給拔了,你會再次發現這個文件。這意味着什么,意味着你可以輕而易舉地捕捉到設備加入/移出系統等事件,如果你的腳本足夠智能,那么你就會想到很多很有創意的玩法對吧?