Linux 內核:設備驅動模型(4)uevent與熱插拔


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盤自動掛載。

參考文章:

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/classdevice_name之間的那部分目錄稱為 subsystem。

也就是每個dev屬性文件所在的路徑都可表示為/sys/class/subsystem/<device_name>/dev

在 /dev目錄下創建相應的設備文件。

例如,cat /sys/class/tty/tty0/dev會得到4:0subsystemttydevice_nametty0

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM