文章目錄
標題http://www.osplay.org/modules/article/view.article.php?7
- 思考
如果想讓核心啟動過程中自動載入某個模組該怎麼做呢?最容易想到的方法就是到/etc/init.d/中添加一個啟動指令碼,然後在/etc/rcN.d/目錄下建立一個符號連結,這個連結的名字以S開頭,這核心啟動時,就會自動運行這個指令碼了,這樣就可以在指令碼中使用modprobe來實現自動載入。但是我們發現,核心中載入了許多硬體裝置的驅動,而搜尋/etc目錄,卻沒有發現任何指令碼負責載入這些硬體裝置驅動程式的模組。那麼這些模組又是如何被載入的呢?
ID, SubVendor ID的裝置提供服務。以PCI裝置為例,它是通過一個pci_device_id的資料結構來實現這個功能的。例如:RTL8139的pci_device_id定義為:
static struct pci_device_id rtl8139_pci_tbl[] = {
{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
......
}
MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);
上面的資訊說明,凡是Verdon ID為0x10EC, Device ID為0x8139, 0x8138的PCI裝置(SubVendor ID和SubDeviceID為PCI_ANY_ID,表示不限制。),都可以使用這個驅動程式(8139too)。
alias pci:v000010ECd00008138sv*sd*bc*sc*i* 8139too
alias pci:v000010ECd00008139sv*sd*bc*sc*i* 8139too
......
v後面的000010EC說明其Vendor ID為10EC,d後面的00008138說明Device ID為8139,而sv,和sd為SubVendor ID和SubDevice ID,後面的星號表示任意匹配。
另外在/lib/modules/uname-r/modules.dep檔案中還儲存這模組之間的依賴關係,其內容如下:
(這裡省去了路徑資訊。)
8139too.ko:mii.ko
options,這裡面的各種匯流排,你只能夠選擇Y或N,而不能選擇M.),並且為每一個裝置建立一個裝置對象。每一個匯流排對象有一個kset對象,每一個裝置對象嵌入了一個kobject對象,kobject串連在kset對象上,這樣匯流排和匯流排之間,匯流排和裝置裝置之間就組織成一顆樹狀結構。當匯流排驅動程式為掃描到的裝置建立裝置對象時,會初始化kobject對象,並把它串連到裝置樹中,同時會調用kobject_uevent()把這個(添加新裝置的)事件,以及相關資訊(包括裝置的VendorID,DeviceID等資訊。)通過netlink發送到使用者態中。在使用者態的udevd檢測到這個事件,就可以根據這些資訊,開啟/lib/modules/uname-r/modules.alias檔案,根據
alias pci:v000010ECd00008138sv*sd*bc*sc*i* 8139too
得知這個新掃描到的裝置驅動模組為8139too。於是modprobe就知道要載入8139too這個模組了,同時modprobe根據 modules.dep檔案發現,8139too依賴於mii.ko,如果mii.ko沒有載入,modprobe就先載入mii.ko,接著再載入 8139too.ko。
實驗
在你的shell中,運行:
# ps aux | grep udevd
root 25063 ...... /sbin/udevd --daemon
我們得到udevd的進程ID為25063,現在結束這個進程:
# kill -9 25063
然後跟蹤udevd,在shell中運行:
# strace -f /sbin/udevd --daemon
這時,我們看到udevd的輸出如下:
......
close(8) = 0
munmap(0xb7f8c000, 4096) = 0
select(7, [3 4 5 6], NULL, NULL, NULL
我們發現udevd在這裡被阻塞在select()函數中。
select函數原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
第一個參數:nfds表示最大的檔案描述符號,這裡為7(明明是6 ?)。
第二個參數:readfds為讀檔案描述符集合,這裡為3,4,5,6.
第三個參數:writefds為寫檔案描述符集合,這裡為NULL。
第四個參數:exceptfds為異常檔案描述符集合,這裡為NULL。
第五個參數:timeout指定逾時時間,這裡為NULL。
select函數的作用是:如果readfds中的任何一個檔案有資料可讀,或者witefds中的任何一個檔案可以寫入,或者exceptfds中的任何一個檔案出現異常時,就返回。否則阻塞當前進程,直到上訴條件滿足,或者因阻塞時間超過了timeout指定的時間,當前進程被喚醒,select返回。
所以,在這裡udevd等待3,4,5,6這幾個檔案有資料可讀,才會被喚醒。現在,到shell中運行:
# ps aux | grep udevd
root 27615 ...... strace -o /tmp/udevd.debug -f /sbin/udevd --daemon
root 27617 ...... /sbin/udevd --daemon
udevd的進程id為27617,現在我們來看看select等待的幾個檔案:
# cd /proc/27615/fd
# ls -l
udevd的標準輸入,標準輸出,標準錯誤全部為/dev/null.
0 -> /dev/null
1 -> /dev/null
2 -> /dev/null
udevd在下面這幾個檔案上等待。
3 -> /inotify
4 -> socket:[331468]
5 -> socket:[331469]
6 -> pipe:[331470]
7 -> pipe:[331470]
由於不方便在運行中插入一塊8139的網卡,因此現在我們以一個隨身碟來做實驗,當你插入一個隨身碟後,你將會看到strace的輸出,從它的輸出可以看到 udevd在select返回後,調用了modprobe載入驅動模組,並調用了sys_mknod,在dev目錄下建立了相應的節點。
execve("/sbin/modprobe", ["/sbin/modprobe", "-Q", "usb:v05ACp1301d0100dc00dsc00dp00"...]
......
mknod("/dev/sdb", S_IFBLK|0660, makedev(8, 16)) = 0
......
這裡modprobe的參數"usb:v05AC..."對應modules.alias中的某個模組。
可以通過udevmonitor來查看核心通過netlink發送給udevd的訊息,在shell中運行:
# udevmonitor --env
然後再插入隨身碟,就會看到相關的發送給udevd的訊息。
== 核心處理過程 ==:
int pci_bus_add_device(struct pci_dev *dev)
{
int retval;
retval = device_add(&dev->dev);
......
return 0;
}
device_add()代碼如下:
int device_add(struct device *dev)
{
struct device *parent = NULL;
dev = get_device(dev);
......
error = bus_add_device(dev);
if (error)
goto BusError;
kobject_uevent(&dev->kobj, KOBJ_ADD);
......
}
device_add()在準備好相關資料結構後,會調用kobject_uevent(),把這個訊息發送到使用者空間的udevd。
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[])
{
struct kobj_uevent_env *env;
const char *action_string = kobject_actions[action];
const char *devpath = NULL;
const char *subsystem;
struct kobject *top_kobj;
struct kset *kset;
struct kset_uevent_ops *uevent_ops;
u64 seq;
int i = 0;
int retval = 0;
......
/* default keys */
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
goto exit;
/* keys passed in from the caller */
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, envp_ext[i]);
if (retval)
goto exit;
}
}
......
/* 通過netlink發送訊息,這樣使用者態的udevd進程就會從select()函數返回,並做相應的處理。 */
#if defined(CONFIG_NET)
/* send netlink message */
if (uevent_sock) {
struct sk_buff *skb;
size_t len;
/* allocate message with the maximum possible size */
len = strlen(action_string) + strlen(devpath) + 2;
skb = alloc_skb(len + env->buflen, GFP_KERNEL);
if (skb) {
char *scratch;
/* add header */
scratch = skb_put(skb, len);
sprintf(scratch, "%s@%s", action_string, devpath);
/* copy keys to our continuous event payload buffer */
for (i = 0; i < env->envp_idx; i++) {
len = strlen(env->envp[i]) + 1;
scratch = skb_put(skb, len);
strcpy(scratch, env->envp[i]);
}
NETLINK_CB(skb).dst_group = 1;
netlink_broadcast(uevent_sock, skb, 0, 1, GFP_KERNEL);
}
}
#endif
......
return retval;
}
思考
現在我們知道/dev目錄下的裝置檔案是由 udevd負責建立的,但是在核心啟動過程中,需要mount一個根目錄,通常我們的根目錄是在硬碟上,比如:/dev/sda1,但是硬碟對應的驅動程式沒有載入前,/dev/sda1是不存在的, 如果沒有/dev/sda1,就不能通過mount /dev/sda1 /來掛載根目錄。另一方面udevd是一個可執行檔,如果連硬碟驅動程式到沒有載入,根目錄都不存在,udevd就不能運行。如果udevd不能運行,那麼就不會自動載入磁碟驅動程式,也就不能自動建立/dev/sda1。這不是死結了嗎?那麼你的Linux是怎麼啟動的呢?
參考資料: Essential Linux Device Drivers