1. 程式人生 > >嵌入式Linux驅動筆記(十六)------裝置驅動模型(kobject、kset、ktype)

嵌入式Linux驅動筆記(十六)------裝置驅動模型(kobject、kset、ktype)

你好!這裡是風箏的部落格,

歡迎和我一起交流。

前幾天去面試,被問到Linux裝置驅動模型這個問題,沒答好,回來後惡補知識,找了些資料,希望下次能答出個滿意答案。

Linux早期時候,一個驅動對應一個裝置,也就對應一個硬體地址,那當有兩個一樣的裝置的時候,就要寫兩個驅動,顯然是不合理的。應該是從Linux2.5開始,就引入了device-bus-driver模型。
其中裝置驅動模型主要結構分為kset、kobject、ktype。

kset是同類型kobject物件的集合,可以說是一個容器。
kobject是匯流排、驅動、裝置的三種物件的一個基類,實現公共介面。
ktype,記錄了kobject物件的一些屬性。

裝置驅動模型的核心即是kobject,是為了管理日益增多的裝置,使得裝置在底層都具體統一的介面。他與sysfs檔案系統緊密相連,每個註冊的kobject都對應sysfs檔案系統中的一個目錄。為了直觀管理,統一存放的路徑,使用了kset。但是僅僅有這些目錄沒有意義,這兩個結構體只能表示出裝置的層次關係,所以基本不單獨使用,會嵌入到更大的結構體中,(如希望在驅動目錄下能看到掛在該總線上的各種驅動,而在裝置目錄下能看到掛在該匯流排的各種裝置,就將kobject嵌入到描述裝置以及驅動的結構體中,這樣每次註冊裝置或驅動,都會在sys目錄下有描述
放上一個經典的圖:
這裡寫圖片描述
這個圖其實還漏了一個ktype,kobject都應該包含一個ktype。

Linux裝置模型的目的是:為核心建立起一個統一的裝置模型,從而有一個對系統結構的一般性抽象描述。

我們可以先看下一個小的測試程式:

#include <linux/device.h>  
#include <linux/module.h>  
#include <linux/init.h>  
#include <linux/sysfs.h>  
#include <linux/kernel.h>  
#include <linux/stat.h>  
#include <linux/slab.h>  
#include <linux/string.h>
static struct kset * my_kset; struct test_kobj { int number; struct kobject kobj;/*嵌入更大的結構體*/ }; static struct test_kobj * test1; static struct attribute my_attr = { .name = "name", .mode = S_IRWXUGO, }; /*attribute陣列*/ static struct attribute *my_attrs[] = { &my_attr, NULL, /*最後必須為NULL*/ }; static ssize_t kobject_attr_show(struct kobject *kobj, struct attribute *attr, char *buf) { struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj); ssize_t count = 0; printk("kobject 's number is %d\n", obj->number); printk("kobject 's name is "); count = sprintf(buf, "%s\n", kobject_name(kobj) ); return count; } static ssize_t kobject_attr_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count) { struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj); sscanf(buf, "%d", &obj->number); printk("%s\n", __FUNCTION__); return count; } static struct sysfs_ops my_sys_ops = { .show = kobject_attr_show, .store = kobject_attr_store, }; void kobject_release(struct kobject *kobj) { struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj); kfree(obj); printk("%s\n", __FUNCTION__); } static struct kobj_type my_ktype = { .release = kobject_release, .sysfs_ops = &my_sys_ops, .default_attrs = my_attrs, }; static int __init kobject_init_test(void) { int error; my_kset = kset_create_and_add("kobject_test", NULL, NULL); if (!my_kset) { goto out; } test1 = kzalloc(sizeof(struct test_kobj), GFP_KERNEL); if (!test1) { kset_unregister(my_kset); return -ENOMEM; } test1->number= 1; error = kobject_init_and_add(&test1->kobj, &my_ktype, &my_kset->kobj, "test1"); if(error){ kobject_put(&test1->kobj); goto out; } printk("%s success.\n", __FUNCTION__); return 0; out: printk("%s failed!\n", __FUNCTION__); return -1; } static void __exit kobject_exit_test(void) { kobject_del(&test1->kobj); kobject_put(&test1->kobj); kset_unregister(my_kset); printk("%s\n", __FUNCTION__); } module_init(kobject_init_test); module_exit(kobject_exit_test); MODULE_DESCRIPTION("kobject test"); MODULE_LICENSE("GPL");

這裡寫圖片描述

可以看到,我們在使用kobject、kset、ktype結構,就在sysfs虛擬檔案系統下建立(通過kset_create_and_add和kobject_init_and_add函式)了一些子目錄(kobject_test)和屬性檔案。kset和kobject都可以創建出目錄,但是kset的目錄下存放kobject目錄,kobject下存放屬性檔案(可以對屬性檔案進行讀寫操作,如上圖name屬性檔案,而且kobject目錄下也可以存放kobject目錄,只需parent指向它即可)。
這個小程式沒看懂?沒關係,先看下面的分析:

我們對著Linux kernel原始碼分析下,可以下看看三個結構體的成員:

struct kset {
    struct list_head list;//包含kobject的連結串列
    spinlock_t list_lock;//在訪問連結串列時加鎖
    struct kobject kobj;//嵌入的kobject 
    const struct kset_uevent_ops *uevent_ops;//對發往使用者空間的uevent的處理,如熱拔插
};
struct kobject {
    const char      *name;//名字
    struct list_head    entry;//連線到kset建立層次結構
    struct kobject      *parent;//指向父節點,面向物件的層次架構
    struct kset     *kset;//指向所屬的kset 
    struct kobj_type    *ktype;//屬性檔案 
    struct kernfs_node  *sd; /* sysfs directory entry */
    struct kref     kref;//引用計數
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
    struct delayed_work release;
#endif
    unsigned int state_initialized:1;//初始化狀態
    unsigned int state_in_sysfs:1;//是否處在sysfs下了
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1;
};
struct kobj_type {
    void (*release)(struct kobject *kobj);/*用於釋放kobject佔用的資源*/ 
    const struct sysfs_ops *sysfs_ops;/*提供實現以下屬性的方法*/ 
    struct attribute **default_attrs;/*用於儲存型別屬性列表(指標的指標)*/  
    const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
    const void *(*namespace)(struct kobject *kobj);
};

其實說到裝置驅動模型,很容易想到platform,之前我們也說過:嵌入式Linux驅動學習筆記(五)——學習platform裝置驅動
那我們現在就來具體分析這個吧:
init/main.c裡:

kernel_init
    ->kernel_init_freeable
        ->do_basic_setup
            ->driver_init

這是driver_init函式:

void __init driver_init(void)
{
    /* These are the core pieces */
    devtmpfs_init();
    devices_init();/*device、dev目錄*/
    buses_init();/*bus目錄*/
    classes_init();/*class目錄*/
    firmware_init();/*firmware目錄*/
    hypervisor_init();/*hypervisor目錄*/
    /* These are also core pieces, but must come after the
     * core core pieces.
     */
    platform_bus_init();
    cpu_dev_init();
    memory_dev_init();
    container_dev_init();
    of_core_init();
}

我們看下devices_init函式:

int __init devices_init(void)
{
    devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
    if (!devices_kset)
        return -ENOMEM;
    dev_kobj = kobject_create_and_add("dev", NULL);
    if (!dev_kobj)
        goto dev_kobj_err;
    sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
    if (!sysfs_dev_block_kobj)
        goto block_kobj_err;
    sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
    if (!sysfs_dev_char_kobj)
        goto char_kobj_err;

    return 0;
 char_kobj_err:
    kobject_put(sysfs_dev_block_kobj);//刪除
 block_kobj_err:
    kobject_put(dev_kobj);
 dev_kobj_err:
    kset_unregister(devices_kset);
    return -ENOMEM;
}

這裡面呼叫kset_create_and_add建立kset並返回給devices_kset,注意這裡的devices_kset,可以說是/sys下最大的boss之一了,所有的物理裝置都會在device目錄下管理,/sys/device/目錄是核心對系統中所有裝置的分層次表達模型,儲存了系統所有的裝置。
然後呼叫kobject_create_and_add函式在/sys/目錄下建立dev目錄,/sys/dev目錄下維護一個按照字元裝置和塊裝置的主次號碼(major:minor)連結到真實裝置(/sys/devices)的符號連結檔案,應用程式通過對這些檔案的讀寫和控制,可以訪問實際的裝置。
最後再以dev_kobj為父節點,在/sys/dev/目錄下建立block和char目錄。

這裡我們先看kobject_create_and_add函式,再分析kset_create_and_add函式:

struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{
    struct kobject *kobj;
    int retval;

    kobj = kobject_create();
    if (!kobj)
        return NULL;

    retval = kobject_add(kobj, parent, "%s", name);
    /*忽略部分無關程式碼*/
    return kobj;
}

其實裡面函式也沒啥,先建立kobject,初始化它,再新增,沒啥好說的。
倒是除了kobject_create_and_add函式,還有一個類似的函式:kobject_init_and_add。
kobject_init_and_add傳入一個kobject指標和kobj_type指標,然後進行初始化
kobject_create_and_add建立一個kobject變數,並返回其指標,它不用傳入kobj_type指標

在kset_create_and_add函式裡也會用到kobject,所以我們現在來分析下kset_create_and_add函式:

struct kset *kset_create_and_add(const char *name,
                 const struct kset_uevent_ops *uevent_ops,
                 struct kobject *parent_kobj)
{
    struct kset *kset;
    int error;

    kset = kset_create(name, uevent_ops, parent_kobj);
    if (!kset)
        return NULL;
    error = kset_register(kset);
    if (error) {
        kfree(kset);
        return NULL;
    }
    return kset;
}

裡面就是具體的建立和註冊kset了。
先說建立函式:

static struct kset *kset_create(const char *name,
                const struct kset_uevent_ops *uevent_ops,
                struct kobject *parent_kobj)
{
    struct kset *kset;
    int retval;

    kset = kzalloc(sizeof(*kset), GFP_KERNEL);//分配kset空間
    if (!kset)
        return NULL;//失敗就返回
    retval = kobject_set_name(&kset->kobj, "%s", name);//設定kset的名字,也即內嵌kobject的名字
    if (retval) {
        kfree(kset);
        return NULL;
    }
    kset->uevent_ops = uevent_ops;//kset屬性操作
    kset->kobj.parent = parent_kobj;//設定其parent 
    kset->kobj.ktype = &kset_ktype;//ktype指定為kset_ktype 
    kset->kobj.kset = NULL;

    return kset;
}

可以看出kset_create函式內容為:
1)呼叫kobject_set_name函式設定kobject的名稱
2)設定kobject的uevent_ops、parent為傳入的形參uevent_ops、parent_kobj
3)設定kobject的ktype為系統定義好的ktype變數
4)設定kobject的所屬kset為NULL,意思是kobject所屬的kset就是kset本身,因為kset結構體包含了一個kobject成員。

這裡需要一個注意的,就是ktype 這個結構,即kset_ktype

static struct kobj_type kset_ktype = {
    .sysfs_ops  = &kobj_sysfs_ops,
    .release = kset_release,
};

這裡填充了一個釋放函式,每個kobject必須有一個釋放函式,並且這個kobject必須保持直到這個釋放函式被呼叫到。如果這個條件不能被滿足,則這個程式碼是有缺陷的。注意,假如你忘了提供釋放函式,核心會提出警告的;不要嘗試提供一個空的釋放函式來消除這個警告,你會受到kobject維護者的無情嘲笑。
至於kobj_sysfs_ops,則是關於讀寫操作相關的操作集:

static const struct sysfs_ops sysfs_ops = {
    .show   = show,
    .store  = store,
};

讀檔案時,會呼叫到.show的回撥函式。
寫檔案時,會呼叫到.store的回撥函式。

看完了建立函式,接下來是註冊函式:

int kset_register(struct kset *k)
{
    int err;
    if (!k)
        return -EINVAL;

    kset_init(k);//初始化kset
    err = kobject_add_internal(&k->kobj);/*初始化kobject,建立對應的sys目錄*/  
    if (err)
        return err;
    kobject_uevent(&k->kobj, KOBJ_ADD);
    return 0;
}

kset_init函式主要是對kset初始化,會將初始化引用計數器(即kobj->kref)為1(當計數器引用計數沒到0之前不可以被釋放)。接著初始化entry連結串列結點,用於與所屬的kset的list成員組成連結串列(INIT_LIST_HEAD(&kobj->entry)),以及一些引數的賦值。最後,還初始化以list成員為頭結點的連結串列,它和子kobject的entry成員組成連結串列(INIT_LIST_HEAD(&k->list))。

kobject_add_internal函式就是關鍵的kobject函數了:

static int kobject_add_internal(struct kobject *kobj)
{
    int error = 0;
    struct kobject *parent;

    if (!kobj)
        return -ENOENT;

    if (!kobj->name || !kobj->name[0]) {//如果kobject的名字為空.退出 
        WARN(1, "kobject: (%p): attempted to be registered with empty "
             "name!\n", kobj);
        return -EINVAL;
    }

    parent = kobject_get(kobj->parent);//如果kobj-parent為真,則增加kobj->kref計數,即父節點的引用計數
    /* join kset if set, use it as parent if we do not already have one */
    if (kobj->kset) {
        if (!parent)
            parent = kobject_get(&kobj->kset->kobj);//如果parent父節點為NULL那麼就用kobj->kset->kobj作其父節點,並增加其引用計數
        kobj_kset_join(kobj);//把kobj的entry成員新增到kobj->kset>list的尾部,現在的層次就是kobj->kset->list指向kobj->entry
        kobj->parent = parent;
    }
    /*刪除了部分除錯內容*/
    error = create_dir(kobj);//利用kobj建立目錄和屬性檔案,其中會判斷,如果parent為NULL那麼就在sysfs_root_kn下建立
    if (error) {
        /*刪除了部分內容*/
    } else
        kobj->state_in_sysfs = 1;//如果建立成功。將state_in_sysfs建為1。表示該object已經在sysfs中了

    return error;
}

kobject_add_internal函式內容在註釋裡都寫好了,可以概括為:
1)如果kobject的parent成員為NULL,則把它指向kset的kobject成員。
2)如果kobject的kset成員不為NULL,它會呼叫kobj_kset_join函式把kobject的entry成員新增到kset的list連結串列中
3)最後呼叫create_dir函式建立sys目錄

註冊函式裡最後一個呼叫就是kobject_uevent函數了,應該是關於熱拔插機制的,這不是我們現在關心的內容。
好了,經過上面的折騰,就會在/sys/目錄下建立一個devices目錄。

接下來繼續回到文章開頭進入到的devices_init函式:

void __init driver_init(void)
{
    /* These are the core pieces */
    devtmpfs_init();
    devices_init();/*device、dev目錄*/
    buses_init();/*bus目錄*/
    classes_init();/*class目錄*/
    firmware_init();/*firmware目錄*/
    hypervisor_init();/*hypervisor目錄*/
    /* These are also core pieces, but must come after the
     * core core pieces.
     */
    platform_bus_init();
    cpu_dev_init();
    memory_dev_init();
    container_dev_init();
    of_core_init();
}

我們之前分析的是devices_init函式,其實接下來幾個函式都是一樣的,在/sys/目錄下建立各個目錄。
只需要記住
devices_kset對應/sys/devices目錄
bus_kset對應/sys/bus目錄
devices_kset對應/sys/devices目錄
system_kset對應/sys/devices/system目錄
class_kset對應/sys/class目錄
firmware_kobj對應/sys/firmware目錄
hypervisor_kobj對應/sys/hypervisor目錄

接下來看下platform_bus_init函式
也就是我們之前用的platform匯流排了!!
在driver/base/platform.c檔案:

struct bus_type platform_bus_type = {
    .name       = "platform",
    .dev_groups = platform_dev_groups,
    .match      = platform_match,//各種關鍵字匹配
    .uevent     = platform_uevent,
    .pm     = &platform_dev_pm_ops,
};
struct device platform_bus = {
    .init_name  = "platform",
};

int __init platform_bus_init(void)
{
    int error;

    early_platform_cleanup();

    error = device_register(&platform_bus);
    if (error)
        return error;
    error =  bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);
    of_platform_register_reconfig_notifier();
    return error;
}

這裡,device_register就是在/sys/device/目錄下建立platform

int device_register(struct device *dev)
{
    device_initialize(dev);
    return device_add(dev);
}

其實也就包含兩個函式,一個初始化,一個新增:

void device_initialize(struct device *dev)
{
    dev->kobj.kset = devices_kset;//設定裝置的kobject所屬集合,devices_kset即對應/sys/devices/ 
    kobject_init(&dev->kobj, &device_ktype);//初始化裝置的kobject 
    INIT_LIST_HEAD(&dev->dma_pools);//初始化裝置的DMA池,用於傳遞大資料
    mutex_init(&dev->mutex);
    lockdep_set_novalidate_class(&dev->mutex);
    spin_lock_init(&dev->devres_lock);//初始化自旋鎖,用於同步子裝置連結串列 
    INIT_LIST_HEAD(&dev->devres_head);//初始化子裝置連結串列頭
    device_pm_init(dev);
    set_dev_node(dev, -1);
#ifdef CONFIG_GENERIC_MSI_IRQ
    INIT_LIST_HEAD(&dev->msi_list);
#endif
}

註釋都寫好了,看下device_add函式:

int device_add(struct device *dev)
{
    struct device *parent = NULL;
    struct kobject *kobj;
    struct class_interface *class_intf;
    int error = -EINVAL;
    struct kobject *glue_dir = NULL;

    dev = get_device(dev);//增加裝置的kobject的引用計數
    if (!dev)
        goto done;

    if (!dev->p) {
        error = device_private_init(dev);//初始化dev的私有成員,及其連結串列操作函式
        if (error)
            goto done;
    }

    if (dev->init_name) {//儲存裝置名,以後需要獲取時使用dev_name函式獲取
        dev_set_name(dev, "%s", dev->init_name);
        dev->init_name = NULL;
    }

    /* subsystems can specify simple device enumeration */
    if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
        dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);

    if (!dev_name(dev)) {
        error = -EINVAL;
        goto name_error;
    }

    pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

    parent = get_device(dev->parent);//返回父節點,增加父節點引用計數,如果沒有返回NULL 
    kobj = get_device_parent(dev, parent);//以上層devices為準重設dev->kobj.parent  
    if (kobj)
        dev->kobj.parent = kobj;

    /* use parent numa_node */
    if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
        set_dev_node(dev, dev_to_node(parent));

    /* first, register with generic layer. */
    /* we require the name to be set before, and pass NULL */
    error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);//設定dev->kobj的名字和父物件,並建立相應目錄 
    if (error) {
        glue_dir = get_glue_dir(dev);
        goto Error;
    }

    /* notify platform of device entry */
    if (platform_notify)
        platform_notify(dev);

    error = device_create_file(dev, &dev_attr_uevent);//建立uevent屬性檔案  
    if (error)
        goto attrError;

    error = device_add_class_symlinks(dev);
    if (error)
        goto SymlinkError;
    error = device_add_attrs(dev);
    if (error)
        goto AttrsError;
    error = bus_add_device(dev);
    if (error)
        goto BusError;
    error = dpm_sysfs_add(dev);
    if (error)
        goto DPMError;
    device_pm_add(dev);

    if (MAJOR(dev->devt)) {
        error = device_create_file(dev, &dev_attr_dev);//在sys下產生dev屬性檔案 
        if (error)
            goto DevAttrError;

        error = device_create_sys_dev_entry(dev);//在/sys/dev目錄建立對裝置的軟連結
        if (error)
            goto SysEntryError;

        devtmpfs_create_node(dev);
    }

    /* Notify clients of device addition.  This call must come
     * after dpm_sysfs_add() and before kobject_uevent().
     */
    if (dev->bus)
        blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
                         BUS_NOTIFY_ADD_DEVICE, dev);

    kobject_uevent(&dev->kobj, KOBJ_ADD);//向用戶空間發出KOBJ_ADD 事件 
    bus_probe_device(dev);//檢測驅動中有無適合的裝置進行匹配,現在只添加了裝置,還沒有載入驅動,所以不會進行匹配 
    if (parent)
        klist_add_tail(&dev->p->knode_parent,
                   &parent->p->klist_children);//把該裝置的節點掛到其父節點的連結串列  

    if (dev->class) {
        mutex_lock(&dev->class->p->mutex);
        /* tie the class to the device */
        klist_add_tail(&dev->knode_class,
                   &dev->class->p->klist_devices);

        /* notify any interfaces that the device is here */
        list_for_each_entry(class_intf,
                    &dev->class->p->interfaces, node)
            if (class_intf->add_dev)
                class_intf->add_dev(dev, class_intf);
        mutex_unlock(&dev->class->p->mutex);
    }
    /*省略部分error內容*/
}

device_add函式是比較重要的,註釋基本都寫好了,可以概括為:
1)增加kobj->kref計數
2)初始化dev的私有成員
3)設定裝置名稱
4)增加父節點引用計數
5)將dev->kobj新增到dev->kobj.parent對應目錄下
6)dev->kobj下建立屬性檔案
7)在/sys/dev目錄建立對裝置的軟連結
8)驅動檢測

最後,我們接著看 bus_register(&platform_bus_type);
篇幅有點長了,函式我就寫點重要的即可

int bus_register(struct bus_type *bus)
{
    int retval;
    struct subsys_private *priv;
    struct lock_class_key *key = &bus->lock_key;

    priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->bus = bus;
    bus->p = priv;

    BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

    retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
    if (retval)
        goto out;

    priv->subsys.kobj.kset = bus_kset;
    priv->subsys.kobj.ktype = &bus_ktype;
    priv->drivers_autoprobe = 1;

    retval = kset_register(&priv->subsys);
    if (retval)
        goto out;

    retval = bus_create_file(bus, &bus_attr_uevent);
    if (retval)
        goto bus_uevent_fail;

    priv->devices_kset = kset_create_and_add("devices", NULL,
                         &priv->subsys.kobj);
    if (!priv->devices_kset) {
        retval = -ENOMEM;
        goto bus_devices_fail;
    }

    priv->drivers_kset = kset_create_and_add("drivers", NULL,
                         &priv->subsys.kobj);
    if (!priv->drivers_kset) {
        retval = -ENOMEM;
        goto bus_drivers_fail;
    }
    /*後面的省略*/
}

再次強調:
priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
這裡設定了所屬的kset和ktype。
ktype結構體裡包含了sysfs_ops結構體,裡面就是對檔案的讀寫操作:

static const struct sysfs_ops bus_sysfs_ops = {
    .show   = bus_attr_show,//讀檔案
    .store  = bus_attr_store,//寫檔案
};

最後,bus_register函式裡還呼叫了kset_create_and_add函式在/sys/platform/目錄下建立devices和drivers目錄,裡面存放我們platform平臺下注冊的裝置和驅動。

好了,到此,我們就來再次小小歸納下
*在kset下還可能會有更深的kset
*kset包含一個或多個kobject,方便管理
*kobject並不一定需要kset
*kobject下有屬性檔案,·向用戶層提供了表示和操作這個 kobject 的屬性特徵的介面
*kobject 下還有一些符號連結檔案,指向其它的 kobject

現在,是不是對裝置驅動模型有了更為直觀的認識?現在回頭看看文章開頭的小程式,是不是輕而易舉的理解了呢?