Linux 內核:設備驅動模型(4)uevent與熱插拔
背景
我們簡單回顧一下Linux的設備驅動模型(Linux Device Driver Model,LDDM):
1、在《sysfs與kobject基類》中,kobject的3大功能中包括了用戶空間事件投遞。
2、在《driver-bus-device與probe》中,我們知道在驅動/設備的添加或者移除事件時,會同步投遞對應的事件到用戶空間,而且這個動作是通過uevent來完成的。
當時出於學習的需要,我們並沒有詳細的說明。現在我們就來分析:
1、uevent機制以及 mdev 如何自動創建設備節點
2、實現自己想要的一些功能,比如U盤自動掛載。
參考文章:
- https://blog.csdn.net/newdye/article/details/77774070
- http://www.wowotech.net/device_model/uevent.html
- https://blog.csdn.net/u012066426/article/details/51917369
- https://blog.csdn.net/sandwich125/article/details/80580597
- https://blog.csdn.net/lizuobin2/article/details/51534385
uevent
uevent是kobject的一部分,用於在kobject狀態發生改變時,例如增加、移除等,通知用戶空間程序。用戶空間程序收到這樣的事件后,會做相應的處理。
uevent( user space event)是 內核與用戶空間的一種基於netlink機制通信機制,主要用於設備驅動模型,常用於設備的熱插拔。
例如:
U盤插入后,USB相關的驅動軟件會動態創建用於表示該U盤的device結構(相應的也包括其中的kobject),並告知用戶空間程序,為該U盤動態的創建/dev/目錄下的設備節點;
更進一步,可以通知其它的應用程序,將該U盤設備mount到系統中,從而動態的支持該設備。

uevent的機制是比較簡單的,設備模型中任何設備有事件需要上報時,會觸發uevent提供的接口。uevent模塊准備好上報事件的格式后,可以通過兩個途徑把事件上報到用戶空間:一種是通過kmod模塊,直接調用用戶空間的可執行文件;另一種是通過netlink通信機制,將事件從內核空間傳遞給用戶空間。
其中:
-
netlink是一種socket,專門用來進行內核空間和用戶空間的通信;
-
kmod是管理內核模塊的工具集,類似busybox,我們熟悉的lsmod,insmod等是指向kmod的鏈接。
uevent有幾個核心的數據結構,按照慣例,先獨立分析各個核心類,然后通過類之間的關系全面了解uevent機制
核心結構
kobject_action與事件類型
// include/linux/kobject.h
/*
* The actions here must match the index to the string array
* in lib/kobject_uevent.c
*
* Do not add new actions here without checking with the driver-core
* maintainers. Action strings are not meant to express subsystem
* or device specific properties. In most cases you want to send a
* kobject_uevent_env(kobj, KOBJ_CHANGE, env) with additional event
* specific variables added to the event environment.
*/
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
// lib/kobject_uevent.c
/* the strings here must match the enum in include/linux/kobject.h */
static const char *kobject_actions[] = {
[KOBJ_ADD] = "add",
[KOBJ_REMOVE] = "remove",
[KOBJ_CHANGE] = "change",
[KOBJ_MOVE] = "move",
[KOBJ_ONLINE] = "online",
[KOBJ_OFFLINE] = "offline",
};
kobject_action定義了event的類型,包括:
| action | 意義 |
|---|---|
ADD/REMOVE |
kobject(或上層數據結構)的添加/移除事件。 |
ONLINE/OFFLINE |
kobject(或上層數據結構)的上線/下線事件,其實是是否使能。 |
CHANGE |
kobject(或上層數據結構)的狀態或者內容發生改變。 |
MOVE |
kobject(或上層數據結構)更改名稱或者更改parent(意味着在sysfs中更改了目錄結構)。 |
CHANGE |
如果設備驅動需要上報的事件不再上面事件的范圍內,或者是自定義的事件,可以使用該event,並攜帶相應的參數 |
kobj_uevent_env與用戶環境
// include/linux/kobject.h
#define UEVENT_NUM_ENVP 32 /* number of env pointers */
#define UEVENT_BUFFER_SIZE 2048 /* buffer for the variables */
struct kobj_uevent_env {
// 指針數組,用於保存每個環境變量
char *envp[UEVENT_NUM_ENVP];
// 用於訪問 環境變量指針 數組下標
int envp_idx;
// 保存環境變量的buffer與長度
char buf[UEVENT_BUFFER_SIZE];
int buflen;
};
前面有提到過,在通過kmod向用戶空間上報event事件時,會直接執行用戶空間的可執行文件。
而在Linux系統中,可執行文件的執行,依賴於環境變量,因此kobj_uevent_env用於組織此次事件上報時的環境變量。
argv,argv[0]存儲uevent_helper的值,uevent_helper的內容是由內核配置項CONFIG_UEVENT_HELPER_PATH決定的,該配置項指定了一個用戶空間程序(或者腳本),用於解析上報的uevent,例如"/sbin/hotplug”。
可以這樣理解,uevent模塊通過kmod上報Uevent時,會通過call_usermodehelper函數,調用用戶空間的可執行文件(或者腳本,簡稱uevent helper )處理該event。而該uevent helper的路徑保存在uevent_helper數組中。對於uevent_helper還有一點要注意,在編譯內核時,通過CONFIG_UEVENT_HELPER_PATH配置項,靜態指定uevent helper的方式,會為每個event fork一個進程,隨着內核支持的設備數量的增多,這種方式在系統啟動時將會是致命的(可以導致內存溢出等),現在內核不再推薦使用該方式。
因此內核編譯時,需要把該配置項留空。在系統啟動后,大部分的設備已經ready,可以根據需要,重新指定一個uevent helper,以便檢測系統運行過程中的熱拔插事件。這可以通過把helper的路徑寫入到"/sys/kernel/uevent_helper”文件中實現。
實際上,內核通過sysfs文件系統的形式,將uevent_helper數組開放到用戶空間,供用戶空間程序修改訪問。argv[1]存儲了本kobj_uevent_env的buf指針,argv[2]一般為NULL。
kset_uevent_ops與策略
// include/linux/kobject.h
struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
前面在分析kset的時候,有一個屬性uevent_ops就是kobj_uevent_ops結構。
filter
當任何kobject需要上報uevent時,它所屬的kset可以通過該接口過濾,阻止不希望上報的event,從而達到從整體上管理的目的。
name
該接口可以返回kset的名稱。如果一個kset沒有合法的名稱,則其下的所有Kobject將不允許上報uvent
uevent
當任何kobject需要上報uevent時,它所屬的kset可以通過該接口統一為這些event添加環境變量。
因為很多時候上報uevent時的環境變量都是相同的,因此可以由kset統一處理,就不需要讓每個kobject獨自添加了。
三者的關系
當設備加載或卸載時,是怎么通過這幾個uevent的核心類通知用戶空間的呢?
通過前面的分析,大家應該知道,設備加載或卸載最直觀的體現在/sys下目錄的變化,/sys下的目錄和kobject是對應的,因此還得從kobject說起。
kobject_uevent(&class_dev->kobj, KOBJ_ADD);
kobject_uevent_env(kobj, action, NULL);
// action_string = "add";
action_string = action_to_string(action);
/* 分配、保存環境變量的內存 */
/* environment values */
buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
/* 設置環境變量 */
nvp [i++] = scratch;
scratch += sprintf(scratch, "ACTION=%s", action_string) + 1;
envp [i++] = scratch;
scratch += sprintf (scratch, "DEVPATH=%s", devpath) + 1;
envp [i++] = scratch;
scratch += sprintf(scratch, "SUBSYSTEM=%s", subsystem) + 1;
/* 調用應用程序:比如mdev */
/* 在/etc/init.d/rcS 中的echo /sbin/mdev > /proc/sys/kernel/hotplug指定了應用程序*/
argv [0] = uevent_helper; // = "/sbin/mdev"
argv [1] = (char *)subsystem;
argv [2] = NULL;
call_usermodehelper (argv[0], argv, envp, 0);
發送事件
我們以之前注冊驅動的時候,發送的事件為例。
// drivers/base/driver.c
int driver_register(struct device_driver *drv)
{
// ...
// 將事件發送到用戶空間
kobject_uevent(&drv->p->kobj, KOBJ_ADD);
return ret;
}
kobject找到自己的kset,通過kobject_uevent函數將事件發送到用戶空間,但是實際上發送事件的動作由調用函數kobject_uevent_env實現。
// lib/kobject_uevent.c
/**
* kobject_uevent - notify userspace by sending an uevent
*
* @action: action that is happening
* @kobj: struct kobject that the action is happening to
*
* Returns 0 if kobject_uevent() is completed with success or the
* corresponding error when it fails.
*/
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
EXPORT_SYMBOL_GPL(kobject_uevent);
kobject_uevent_env
主要做了這些事情:
1、獲取整理了與即將發送的事件相關的環境變量,如ACTION、DEVPATH和SUBSYSTE等。
2、發送事件到用戶空間。
/**
* kobject_uevent_env - send an uevent with environmental data
*
* @action: action that is happening
* @kobj: struct kobject that the action is happening to
* @envp_ext: pointer to environmental data
*
* Returns 0 if kobject_uevent_env() is completed with success or the
* corresponding error when it fails.
*/
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;
const struct kset_uevent_ops *uevent_ops;
int i = 0;
int retval = 0;
#ifdef CONFIG_NET
struct uevent_sock *ue_sk;
#endif
/* 1、找到對應的對象 */
/* search the kset we belong to */
// ...
/* 2、判斷是否要過跳過發送事件 */
/* skip the event, if uevent_suppress is set*/
// ...
/* skip the event, if the filter returns zero. */
// ...
/* originating subsystem */
/* 通過uevent_ops->name函數取得子系統名,如果uevent_ops->name為NULL,則使用kset.kobj.name做為子系統名。
事實上,一個kset就是一個所謂的“subsystem”。
*/
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);
else
subsystem = kobject_name(&kset->kobj);
/* 3、處理事件*/
/* environment buffer */
// ...
/* complete object path */
// ...
/* default keys */
// ...
/* keys passed in from the caller */
// ...
/* let the kset specific function add its stuff */
// ...
/*
* Mark "add" and "remove" events in the object to ensure proper
* events to userspace during automatic cleanup. ...
*/
// ...
/* we will send an event, so request a new sequence number */
/*4、與用戶空間交互 */
#if defined(CONFIG_NET)
/* send netlink message */
// ...
#endif
/* call uevent_helper, usually only enabled during early boot */
// ...
/* 5、回收資源 */
exit:
kfree(devpath);
kfree(env);
return retval;
}
EXPORT_SYMBOL_GPL(kobject_uevent_env);
找到頂層的對象
找到對象所屬的頂級集合(kset)、頂級父對象(top_kobj)、以及頂級對應的uevent_ops:
/* 如果kobject 不屬於一個Kset,則向上查找到,直到找到一個屬於kset的kobject為止 */
struct kobject *top_kobj;
struct kset *kset;
const struct kset_uevent_ops *uevent_ops;
/* search the kset we belong to */
top_kobj = kobj;
while (!top_kobj->kset && top_kobj->parent)
top_kobj = top_kobj->parent;
if (!top_kobj->kset) {
pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
"without kset!\n", kobject_name(kobj), kobj,
__func__);
return -EINVAL;
}
// 找到 kobj 的 kset,並使用event的操作方法
kset = top_kobj->kset;
uevent_ops = kset->uevent_ops;
判斷是否要過跳過發送事件
判斷分為2個層次:
1、這個kobj 是否允許上報事件
/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
2、kset是否允許發送事件(通過filter)。這里以bus為例。
/* skip the event, if the filter returns zero. */
if (uevent_ops && uevent_ops->filter)
if (!uevent_ops->filter(kset, kobj)) {
// 說明kobj希望發送的uevent被頂層kset過濾掉了,不再發送
pr_debug("kobject: '%s' (%p): %s: filter function "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
/////////////////////////////
// drivers/base/bus.c
static int bus_uevent_filter(struct kset *kset, struct kobject *kobj)
{
struct kobj_type *ktype = get_ktype(kobj);
if (ktype == &bus_ktype)
return 1;
return 0;
}
static const struct kset_uevent_ops bus_uevent_ops = {
.filter = bus_uevent_filter,
};
int __init buses_init(void)
{
bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
// ...
return 0;
}
處理事件
/* environment buffer */
env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
if (!env)
return -ENOMEM;
/* complete object path */
// 獲取Path 也就是kobj的路徑 /sys/devices/xxx
devpath = kobject_get_path(kobj, GFP_KERNEL);
/* default keys */
// 將ACTION、DEVPATH、SUBSYSTEM三個默認環境變量添加到env中
retval = add_uevent_var(env, "ACTION=%s", action_string);
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
/* keys passed in from the caller */
// 額外的變量信息(由調用者提供,在bus.c中是NULL)
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, "%s", envp_ext[i]);
}
}
/* let the kset specific function add its stuff */
// kset可以通過uevent_ops->uevent完成自己特定的功能
if (uevent_ops && uevent_ops->uevent) {
retval = uevent_ops->uevent(kset, kobj, env);
}
/*
* Mark "add" and "remove" events in the object to ensure proper
* events to userspace during automatic cleanup. If the object did
* send an "add" event, "remove" will automatically generated by
* the core, if not already done by the caller.
*/
// 如果action是KOBJ_ADD, 設置state_add_uevent_sent為1。
// 如果action是KOBJ_REMOVE,設置state_remove_uevent_sent為1。
// 作用:確保在自動清理期間向用戶空間發送正確的事件。
if (action == KOBJ_ADD)
kobj->state_add_uevent_sent = 1;
else if (action == KOBJ_REMOVE)
kobj->state_remove_uevent_sent = 1;
// 將SEQNUM環境變量添加到env中,代表 熱插拔事件的順序號.
// 順序號是一個 64-位 數, 它每次產生熱插拔事件都遞增.
// 這允許用戶空間以內核產生它們的順序來排序熱插拔事件, 因為對一個用戶空 間程序可能亂序運行.
/* we will send an event, so request a new sequence number */
retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
與用戶空間交互
熱插拔(hotplug)是指當有設備插入或撥出系統時,內核可以檢測到這種狀態變化,並通知用戶空間加載或移除該設備對應的驅動程序模塊。
在Linux系統上內核有兩種機制可以通知用戶空間執行加載或移除操作,一種是udev,另一種是/sbin/hotplug;
在Linux發展的早期,只有/sbin/hotplug,實際上是基於內核中的call_usermodehelper函數實現的,它能從內核空間啟動一個用戶空間程序。
隨着內核的發展,出現了udev機制並逐漸取代了/sbin/hotplug。udev的實現基於內核中的網絡機制,它通過創建標准的socket接口來監聽來自內核的網絡廣播包,並對接收到的包進行分析處理。
在Linux中,有兩種方式完成向用戶空間廣播當前kset對象中的uevent事件:
- 通過udev的方式向用戶空間廣播當前kset對象中的uevent事件。
- 另外一種方式是在內核空間啟動一個用戶空間進程/sbin/hotplug,通過給該進程傳遞內核設定的環境變量的方式來通知用戶空間kset對象中的uevent事件
通過udev的方式
mutex_lock(&uevent_sock_mutex);
#if defined(CONFIG_NET)
/* send netlink message */
list_for_each_entry(ue_sk, &uevent_sock_list, list) {
struct sock *uevent_sock = ue_sk->sk;
struct sk_buff *skb;
size_t len;
if (!netlink_has_listeners(uevent_sock, 1))
continue;
/* 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;
retval = netlink_broadcast_filtered(uevent_sock, skb,
0, 1, GFP_KERNEL,
kobj_bcast_filter,
kobj);
/* ENOBUFS should be handled in userspace */
if (retval == -ENOBUFS || retval == -ESRCH)
retval = 0;
} else
retval = -ENOMEM;
}
#endif
mutex_unlock(&uevent_sock_mutex);
通過設置並啟動hotplug進程
/* 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=/");
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
// 調用用戶空間程序,程序名 argv[0], 並把環境變量當作參數傳遞過去
retval = call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
}
如何在Linux內核中執行某些用戶態程序或系統命令?
- 在用戶態中,可以通過
execve()實現; - 在內核態,則可以通過
call_usermodehelpere()實現該功能。
如果您查閱了上述函數的源碼實現,就可以發現
call_usermodehelper()與execve系統調用最終都會會執行do_execve()。
uevent_helper常用環境變量:
| 環境變量 | 說明 |
|---|---|
| ACTION | 對應kobject_action定義的kobject動作,不過是將枚舉轉換成了字符串 |
| DEVPATH | 被創建或刪除的kobject在sysfs中的路徑 |
| SEQNUM | 熱插拔事件,使程序可以區分熱插拔事件 |
| SUBSYSTEM | 描述子系統的字符串,與class中的name對應。 |
誰是uevent_helper
剛剛我們說了,kobject_uevent_env會指定uevent_helper來並執行。搜索源碼以后發現uevent_helper的默認值是/sbin/hotplug":
// lib/kobject_uevent.c
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
// .config (默認值)
CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug"
但文件系統中找不到hotplug。
通過在if (uevent_helper[0])添加一句printk("uevent_helper is %s\n", uevent_helper );
日志如下:
uevent_helper is /sbin/hotplug
uevent_helper is /sbin/hotplug
// ...
uevent_helper is /sbin/mdev
uevent_helper is /sbin/mdev
看到沒,剛開始確實是/sbin/hotplug,但后來就變成了/sbin/mdev。
結論:在系統啟動后,大部分的設備已經ready,可以根據需要,重新指定一個uevent helper,以便檢測系統運行過程中的熱拔插事件。
可以通過把helper的路徑寫入到/sys/kernel/uevent_helper文件中實現。
有的資料說是將mdev加到
/proc/sys/kernel/hotplug_helper,其實這兩個是一樣的,但為了確保proc子系統只提供給進程使用,因此新的系統應該優先使用sys子系統
例如:
# /etc/init.d/rcS
echo /sbin/mdev > /sys/kernel/hotplug # 重新指定了處理uevent的上層應用程序
實際上,內核通過sysfs文件系統的形式,將uevent_helper數組開放到用戶空間,供用戶空間程序修改訪問。
在早期版本的內核中,
uevent helper是通過CONFIG_UEVENT_HELPER_PATH配置項來靜態指定uevent helper。但這種方式會為每個event fork一個進程,隨着內核支持的設備數量的增多,這種方式在系統啟動時將會是致命的(存在內存溢出的風險等)。現在不推薦使用這種方式,因此內核編譯時,需要把該配置項留空。
至於為什么用戶空間能夠修改uevent_helper,實際上是由"kernel/ksysfs.c”實現的,這里不再詳細描述。
mdev
概述
熟悉linux驅動程序編寫的人都知道,需要在/dev下建立設備文件,但是如果用LDDM來寫驅動程序可能就看不到熟悉的mknod,modprobe等了,這些操作並非消失了,而是由其他機制代替人工做了。
大家都知道創建設備節點的工作是在用戶空間進行的,為什么不能由驅動直接創建呢?
試想,如果創建設備由驅動程序來做,驅動位於內核層,如果由其負責這個任務,那么驅動就得知道它要創建的設備名。
簡單的字符驅動還好,如果是USB等可插拔的設備,驅動怎么知道自己要創建什么設備名呢?
有人說可以寫明一套規則。確實如此,但如果把這套規則放到應用層,由應用程序開發人員去明確這個規則(mdev正是這樣做的),會不會更好?
因為是應用程序直接編程訪問這個設備名對應的設備驅動的。所以設備驅動不應該直接負責設備文件的創建。
用戶層創建設備文件也有兩種方法:
- 用戶在shell中使用mknod命令創建設備文件,同時傳入設備名和設備號。這應該是大家最熟悉的一種方法,但是這種人工的做法,很不科學。它只是一種演示的方法,不適於作為工程方法。
- 利用設備驅動模型來輔助創建設備文件(這也是設備模型的作用之一)。
udev和mdev就是使用設備驅動模型來自動創建設備文件的。
- udev是構建在linux的sysfs之上的,是一個用戶程序,它能夠根據系統中的硬件設備的狀態動態更新設備文件。
- mdev是busybox自帶的一個簡化版的udev,它比udev占用的內存更小,因此更適合嵌入式系統的應用。
udev和mdev都依賴uevent機制,個人理解,udev使用netlink機制,mdev使用kmod機制。
在分析kobj_uevent_env的argv成員是已經提到了,kmod最終會調用用戶程序,即uevent_helper處理uevent消息,在嵌入式中,mdev通常就是uevent_helper程序。
我們接下來介紹mdev,udev等的原理也是一樣的。
mdev是busybox提供的一個工具,用在嵌入式系統中,相當於簡化版的udev,作用是在系統啟動和熱插拔或動態加載驅動程序時, 自動創建設備節點。
在加載驅動過程中,根據驅動程序,在/dev下自動創建設備節點。
文檔說明
# docs/mdev.txt
Mdev has two primary uses: initial population and dynamic updates. Both
require sysfs support in the kernel and have it mounted at /sys. For dynamic
updates, you also need to have hotplugging enabled in your kernel.
Here's a typical code snippet from the init script:
[0] mount -t proc proc /proc
[1] mount -t sysfs sysfs /sys
[2] echo /sbin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s
Alternatively, without procfs the above becomes:
[1] mount -t sysfs sysfs /sys
[2] sysctl -w kernel.hotplug=/sbin/mdev
[3] mdev -s
Of course, a more "full" setup would entail executing this before the previous
code snippet:
[4] mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev
[5] mkdir /dev/pts
[6] mount -t devpts devpts /dev/pts
The simple explanation here is that [1] you need to have /sys mounted before
executing mdev. Then you [2] instruct the kernel to execute /sbin/mdev whenever
a device is added or removed so that the device node can be created or destroyed.
Then you [3] seed /dev with all the device nodes that were created while the system
was booting.
For the "full" setup, you want to [4] make sure /dev is a tmpfs filesystem
(assuming you're running out of flash). Then you want to [5] create the
/dev/pts mount point and finally [6] mount the devpts filesystem on it.
mdev -s
執行mdev -s命令時,mdev掃描/sys/class/block(塊設備保存在/sys/block)目錄下的dev屬性文件。
從內核2.6.25版本以后,塊設備不再保存於
/sys/block目錄下。mdev掃描/sys/block是為了實現兼容歷史版本的早期驅動。
由於dev屬性文件以”major:minor”形式保存設備編號,因此mdev能夠從該dev 屬性文件中獲取到設備編號;
並以包含該dev屬性文件的目錄名稱作為設備名 device_name,即:包含dev屬性文件的目錄稱為device_name,
而/sys/class和device_name之間的那部分目錄稱為 subsystem。
也就是每個dev屬性文件所在的路徑都可表示為
/sys/class/subsystem/<device_name>/dev
在 /dev目錄下創建相應的設備文件。
例如,cat /sys/class/tty/tty0/dev會得到4:0,subsystem為tty、device_name為tty0。
uevent調用的mdev
當mdev因uevnet事件(以前叫hotplug事件)被調用時,mdev通過由uevent事件傳遞給它的環境變量獲取到:引起該uevent 事件的設備action及該設備所在的路徑device path。
然后判斷引起該uevent事件的action是什么:
- 若該action是add,即有新設備加入到系統中,不管該設備是虛擬設備還是實際物理設備,mdev都會通過device path路徑下的dev屬性文件獲取到設備編號,然后以device path路徑最后一個目錄(即包含該dev屬性文件的目錄)作為設備名,在/dev目錄下創建相應的設備文件。
- 若該action是remove,即設備已從系統中移除,則刪除/dev目錄下以device path路徑最后一個目錄名稱作為文件名的設備文件。
- 如果該action既不是add也不是remove,mdev則什么都不做。
由上面可知,如果我們想在設備加入到系統中或從系統中移除時,由mdev自動地創建和刪除設備文件,那么就必須做到以下三點:
1、在/sys/class 的某一subsystem目錄下,創建一個以設備名device_name作為名稱的目錄
2、並且在該device_name目錄下還必須包含一個 dev屬性文件,該dev屬性文件以”major:minor\n”形式輸出設備編號。
那么,實際上,mdev做了什么呢?
來看一下 busybox的源碼,版本:May 2021 -- BusyBox 1.33.1 (stable)
mdev_main
由於新版本的busybox比較復雜,我們看一個比較老的版本
// util-linux/mdev.c
int mdev_main(int argc UNUSED_PARAM, char **argv)
{
// ...
xchdir("/dev"); // 先把目錄改變到/dev下
if (argv[1] && strcmp(argv[1], "-s") == 0) { // 在文件系統啟動的時候會調用 mdev -s,創建所有驅動設備節點
putenv((char*)"ACTION=add"); // mdev -s 的動作是創建設備節點,所以為add
if (access("/sys/class/block", F_OK) != 0) { // 當/sys/class/block目錄不存在時,才掃描/sys/block
/* Scan obsolete /sys/block only if /sys/class/block
* doesn't exist. Otherwise we'll have dupes.
* Also, do not complain if it doesn't exist.
* Some people configure kernel to have no blockdevs.
*/
recursive_action("/sys/block",
ACTION_RECURSE | ACTION_FOLLOWLINKS | ACTION_QUIET,
fileAction, dirAction, temp, 0);
}
/*
* 這個函數是遞歸函數,它會掃描/sys/class目錄下的所有文件,如果發現dev文件,將按照
* /etc/mdev.conf文件進行相應的配置。如果沒有配置文件,那么直接創建設備節點
* 最終調用的創建函數是 make_device
*/
recursive_action("/sys/class",
ACTION_RECURSE | ACTION_FOLLOWLINKS,
fileAction, dirAction, temp, 0);
} else{
// 獲得環境變量,環境變量是內核在調用mdev之前設置的
env_devname = getenv("DEVNAME"); /* can be NULL */
G.subsystem = getenv("SUBSYSTEM");
action = getenv("ACTION");
env_devpath = getenv("DEVPATH");
snprintf(temp, PATH_MAX, "/sys%s", env_devpath);
make_device(env_devname, temp, op);
}
}
由以上代碼分析可知,無論對於何種操作,最后都是調用make_device
make_device
make_device最終完成了創建/移除驅動節點並執行指定的命令的操作。
/* mknod in /dev based on a path like "/sys/block/hda/hda1"
* NB1: path parameter needs to have SCRATCH_SIZE scratch bytes
* after NUL, but we promise to not mangle it (IOW: to restore NUL if needed).
* NB2: "mdev -s" may call us many times, do not leak memory/fds!
*
* device_name = $DEVNAME (may be NULL)
* path = /sys/$DEVPATH
*/
static void make_device(char *device_name, char *path, int operation)
{
int major, minor, type, len;
//path_end指定path結尾處
char *path_end = path + strlen(path);
/* Try to read major/minor string. Note that the kernel puts \n after
* the data, so we don't need to worry about null terminating the string
* because sscanf() will stop at the first nondigit, which \n is.
* We also depend on path having writeable space after it.
*/
/* 讀取 主/次設備號 */
major = -1;
if (operation == OP_add) {
// 往path結尾處拷貝“/dev”,這時path=/sys/class/test/test_dev/dev
strcpy(path_end, "/dev");
// 打開並讀取/sys/class/test/test_dev/dev
len = open_read_close(path, path_end + 1, SCRATCH_SIZE - 1);
*path_end = '\0';
if (len < 1) {
if (!ENABLE_FEATURE_MDEV_EXEC)
return;
/* no "dev" file, but we can still run scripts
* based on device name */
// 通過sscanf從/sys/class/test/test_dev/dev獲得主次設備號
// 因為 cat /sys/class/test/test_dev/dev 能夠得到 '主設備號:次設備號' 這樣子的結果
} else if (sscanf(path_end + 1, "%u:%u", &major, &minor) == 2) {
dbg1("dev %u,%u", major, minor);
} else {
major = -1;
}
}
/* else: for delete, -1 still deletes the node, but < -1 suppresses that */
/* Determine device name */
// ...
/* Determine device type */
// ...
#if ENABLE_FEATURE_MDEV_CONF
// 如果 /etc/mdev.conf 有這個配置文件的話,根據配置文件的規則來 創建設備節點 並執行一些命令
// ...
#endif
for (;;) {
const char *str_to_match;
regmatch_t off[1 + 9 * ENABLE_FEATURE_MDEV_RENAME_REGEXP];
char *command;
char *alias;
char aliaslink = aliaslink; /* for compiler */
char *node_name;
const struct rule *rule;
str_to_match = device_name;
rule = next_rule();
#if ENABLE_FEATURE_MDEV_CONF
// ...
#endif
/* Build alias name */
alias = NULL;
if (ENABLE_FEATURE_MDEV_RENAME && rule->ren_mov) {
// ...
}
dbg3("alias:'%s'", alias);
// 解析命令
command = NULL;
IF_FEATURE_MDEV_EXEC(command = rule->r_cmd;)
if (command) {
/* Are we running this command now?
* Run @cmd on create, $cmd on delete, *cmd on any
*/
if ((command[0] == '@' && operation == OP_add)
|| (command[0] == '$' && operation == OP_remove)
|| (command[0] == '*')
) {
command++;
} else {
command = NULL;
}
}
dbg3("command:'%s'", command);
// ...
// 如果動作是 ADD ,則在 /dev/ 中 創建節點
if (operation == OP_add && major >= 0) {
// ...
if (mknod(node_name, rule->mode | type, makedev(major, minor)) && errno != EEXIST)
bb_perror_msg("can't create '%s'", node_name);
// ...
}
// 如果命令存在,則 執行命令
if (ENABLE_FEATURE_MDEV_EXEC && command) {
/* setenv will leak memory, use putenv/unsetenv/free */
char *s = xasprintf("%s=%s", "MDEV", node_name);
putenv(s);
dbg1("running: %s", command);
if (system(command) == -1)
bb_perror_msg("can't run '%s'", command);
bb_unsetenv_and_free(s);
}
// 如果動作是REMOVE ,則在 /dev/ 中 移除節點
if (operation == OP_remove && major >= -1) {
if (ENABLE_FEATURE_MDEV_RENAME && alias) {
if (aliaslink == '>') {
dbg1("unlink: %s", device_name);
unlink(device_name);
}
}
dbg1("unlink: %s", node_name);
unlink(node_name);
}
/* We found matching line.
* Stop unless it was prefixed with '-'
*/
if (!ENABLE_FEATURE_MDEV_CONF || !rule->keep_matching)
break;
} /* for (;;) */
}
調試log
附上一次其他網友調試時打印出來的環境變量(基於platform device)。
env[0] ACTION=add
env[1] DEVPATH=/devices/platform/myled
env[2] SUBSYSTEM=platform
env[3] MAJOR=251
env[4] MINOR=0
env[5] DEVNAME=myled
env[6] MODALIAS=platform:myled
env[7] SEQNUM=642
env[8] HOME=/
env[9] PATH=/sbin:/bin:/usr/sbin:/usr/bin
附錄:基於LDDM的設備
什么都不依賴的單純設備,它的父設備是NULL,會在出現在 /sys/devices/目錄下
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/device.h>
static int first_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
return 0;
}
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 這是一個宏,推向編譯模塊時自動創建的__this_module變量 */
.open = first_drv_open,
.write = first_drv_write,
};
struct device dev = {
.init_name = "my_first_drv",
.devt = MKDEV(major, 0),
};
int major;
static int first_drv_init(void)
{
major = register_chrdev(0, "first", &first_drv_fops);
device_register(&dev);
return 0;
}
static void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); // 卸載
iounmap(gpbcon);
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
測試:
# ls /sys/devices/
my_first_drv platform system virtual
# ls /sys/devices/my_first_drv/
dev uevent
附錄:mdev.conf 文檔
作者@韋東山, 介紹了如何 編寫/etc/mdev.conf。
格式
<device regex> <uid>:<gid> <octal permissions> [<@|$|*> <command>]
- device regex:正則表達式,表示哪一個設備
- uid: owner
- gid: 組ID
- octal permissions:以八進制表示的屬性
- @:創建設備節點之后執行命令
- $:刪除設備節點之前執行命令
- *: 創建設備節點之后 和 刪除設備節點之前 執行命令
- command:要執行的命令
范例
前提:韋東山老師寫了個驅動,有 led led1 led2 led3 這四個設備。
寫法1
指定4個設備,全部設為 777權限
leds 0:0 777
led1 0:0 777
led2 0:0 777
led3 0:0 777
寫法2
基於正則表達式
leds?[123]? 0:0 777
寫法3
在2的基礎上,指定在 設備創建后,執行腳本
leds?[123]? 0:0 777 @ echo create /dev/$MDEV > /dev/console
寫法4
類似3,但是使用了環境變量$ACTION
leds?[123]? 0:0 777 * if [ $ACTION = "add" ]; then echo create /dev/$MDEV > /dev/console; else echo remove /dev/$MDEV > /dev/console; fi
寫法5
將命令寫到文件中執行
leds?[123]? 0:0 777 * /bin/add_remove_led.sh
腳本的內容是:
#!/bin/sh
if [ $ACTION = "add" ];
then
echo create /dev/$MDEV > /dev/console;
else
echo remove /dev/$MDEV > /dev/console;
fi
