熱插拔(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盤給拔了,你會再次發現這個文件。這意味著什麼,意味著你可以輕而易舉地捕捉到設備加入/移出系統等事件,如果你的腳本足夠智能,那麼你就會想到很多很有創意的玩法對吧?
最後,對於PCI設備而言,Linux系統在啟動過程中會掃描系統中所有PCI設備,對發現的每一個設備都會調用device_add函數,正如你前面看到的那樣,udev將會被通知,它負責找到對應的驅動模塊並加載。當然,如果你願意,你也可以去捕捉這些事件。