久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

xfs文件系統(tǒng):格式化以及掛載

 HK123COM 2018-06-26

引言

情景:
  <源碼> linux:3.14.56 xfsprogs:3.2.0
  <命令> mkfs.xfs -f /dev/[sdx] ; mount /dev/[sdx]; umount /dev/[sdx]

如情景所示,,來分析分析,mkfs.xfs mount 以及umount操作都做了些什么事情,。下述內(nèi)容均為本人隨意跟蹤,,看到那說到哪!僅作參考,。


MKFS.XFS

本例格式化xfs文件系統(tǒng),,有關(guān)xfs文件系統(tǒng)就不介紹了。倒是首先要分析mkfs.xfs命令,先得獲取相應(yīng)的源碼,,源碼獲取可到github搜索xfsprogs,,之前找了一個util-linux的包,編譯后才發(fā)覺不支持mkfs.xfs,,其它的文件系統(tǒng)倒還支持一些的,,可見xfs還是比較“特殊”的,值得了解一下,。好吧,,總之下載即可。

  • mkfs.xfs -f /dev/sdc
    • 源碼分析
      1,、進入 xfs_mkfs.c文件主函數(shù)main。對于一個提供給用戶使用的終端命令(或說控制接口),,它做的事情無非就是在接受用戶指令后,,對相應(yīng)指令參數(shù)進行解析,而后下發(fā)給后端進/線程做進一步處理,,當然最終會返回信息給當前主進程,,處理完事務(wù)后(同步的話),進程也就退出了,。
... ...
while ((c = getopt(argc, argv, "b:d:i:l:L:m:n:KNp:qr:s:CfV")) != EOF) {
        switch (c) {
        case 'C':
        case 'f':
            force_overwrite = 1;
            break;
... ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

跟蹤force_overwrite標志:

    memset(&ft, 0, sizeof(ft));                                ----/1/
    get_topology(&xi, &ft, force_overwrite);        ----/2/
  • 1
  • 2

/1/ 首先初始化以ft為首地址的fs_topology結(jié)構(gòu)體,,那么不妨先來看看這個結(jié)構(gòu)體的內(nèi)容有哪些:

/*
 * Device topology information.
 */
struct fs_topology {
    int    dsunit;        /* stripe unit - data subvolume:*/
    int    dswidth;    /* stripe width - data subvolume */
    int    rtswidth;    /* stripe width - rt subvolume */
    int    lsectorsize;    /* logical sector size &*/
    int    psectorsize;    /* physical sector size */
    int    sectoralign;    //扇區(qū)對齊標志:該標志位置位1時,要求處理的塊大小與扇區(qū)大小相同(老linux版本中),,因而通常不會置位,。
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

/2/ 若ENABLE_BLKID有定義,即安裝有blkid源碼,,即可執(zhí)行blkid命令,,獲取設(shè)備的拓撲結(jié)構(gòu)如下:

static void get_topology(
    libxfs_init_t        *xi,
    struct fs_topology    *ft,
    int            force_overwrite)
{
    if (!xi->disfile) {                                        ----/2.1/
        const char *dfile = xi->volname ? xi->volname : xi->dname;

        blkid_get_topology(dfile, &ft->dsunit, &ft->dswidth,                ----/2.2/
                   &ft->lsectorsize, &ft->psectorsize,
                   force_overwrite);
    }

    if (xi->rtname && !xi->risfile) {
        int dummy;

        blkid_get_topology(xi->rtname, &dummy, &ft->rtswidth,
                   &dummy, &dummy, force_overwrite);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

/2.1/ libxfs_init_t函數(shù)memset初始化為0,其成員變量disfile可知其初始化為0,,又當前mkfs.xfs命令未加-d選項,,則進入該分支處理。下面分支同理,。
/2.2/ 處理獲取設(shè)備拓撲結(jié)構(gòu),,這里不好追蹤就不展開了,關(guān)鍵函數(shù)定義實現(xiàn)在blkid命令源碼中如:當前函數(shù)中的blkid_new_probe_from_filename,。

/2// 若ENABLE_BLKID無定義,,獲取設(shè)備的拓撲結(jié)構(gòu):

static void get_topology(
    libxfs_init_t        *xi,
    struct fs_topology    *ft,
    int            force_overwrite)
{

    char *dfile = xi->volname ? xi->volname : xi->dname;
    int bsz = BBSIZE;

    if (!xi->disfile) {
        int fd;
        long long dummy;

        get_subvol_stripe_wrapper(dfile, SVTYPE_DATA,                ----/2.1//
                &ft->dsunit, &ft->dswidth, &ft->sectoralign);
        fd = open(dfile, O_RDONLY);
        /* If this fails we just fall back to BBSIZE */
        if (fd >= 0) {
            platform_findsizes(dfile, fd, &dummy, &bsz);
            close(fd);
        }
    }

    ft->lsectorsize = bsz;
    ft->psectorsize = bsz;

    if (xi->rtname && !xi->risfile) {
        int dummy1;

        get_subvol_stripe_wrapper(dfile, SVTYPE_RT, &dummy1,
                      &ft->rtswidth, &dummy1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

/2.1// 不妨進入該函數(shù):

void
get_subvol_stripe_wrapper(
    char        *dev,
    sv_type_t    type,
    int        *sunit,
    int        *swidth,
    int        *sectalign)
{
    struct stat64    sb;

    if (dev == NULL)
        return;

    if (stat64(dev, &sb)) {
        fprintf(stderr, _("Cannot stat %s: %s\n"),
            dev, strerror(errno));
        exit(1);
    }

    if (  dm_get_subvol_stripe(dev, type, sunit, swidth, sectalign, &sb))
        return;
    if (  md_get_subvol_stripe(dev, type, sunit, swidth, sectalign, &sb))        ----/2.1.1//
        return;
    if ( lvm_get_subvol_stripe(dev, type, sunit, swidth, sectalign, &sb))
        return;
    if ( xvm_get_subvol_stripe(dev, type, sunit, swidth, sectalign, &sb))
        return;
    if (evms_get_subvol_stripe(dev, type, sunit, swidth, sectalign, &sb))
        return;
    //可添加其它類型的設(shè)備驅(qū)動,信息獲取如上格式
    /* ... add new device drivers here */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

/2.1.1// 分析它即可,,其它的同理,。顧名思義,即獲取Multiple Devices相關(guān)的東西,,也就是有關(guān)矩陣raid條帶化的信息,,因為mkfs.xfs命令未加-d選項,,故而獲取的是創(chuàng)建md設(shè)備時指定的條帶信息。進入該函數(shù)可知,,主要是對其參數(shù)進行賦值,。
返回main主函數(shù),調(diào)用libxfs_init函數(shù)進行xfs文件系統(tǒng)的初始化,,主要關(guān)注一下函數(shù):
 1,、radix_tree_init
   基樹初始化,主要用于內(nèi)存管理,,該樹為典型的字典類型結(jié)構(gòu)(有待研究)
 2,、libxfs_device_open
   打開一個設(shè)備并且獲取其設(shè)備號,即便不是真的設(shè)備亦返回一個偽設(shè)備號
 3,、cache_init
   初始化緩存,,返回cache結(jié)構(gòu)體
 4、manage_zones
   manage_zones函數(shù)實現(xiàn)釋放xfs分區(qū)或生成xfs分區(qū),。而生成啟動xfs目錄結(jié)構(gòu)調(diào)用的是xfs_dir_startup函數(shù),,該函數(shù)定義在linux源碼lib庫中(可使得生成目錄下”.”以及“..”文件)

回到main函數(shù),進入判斷force_overwrite標志位的分支結(jié)構(gòu),,調(diào)用zero_old_xfs_structures函數(shù),,函數(shù)將置0初始化各個次要AG的sb超級塊結(jié)構(gòu),通常128M至4T左右容量設(shè)的備,,4個AG用于管理磁盤空間,。當然若是raid陣列的話就另當別論了,相同容量需創(chuàng)建更多的AG,。AG超級塊結(jié)構(gòu)參考:http://www./archives/2012/01/648

回到main函數(shù),,調(diào)用libxfs_mount —-/3/,該函數(shù)主要就是對結(jié)構(gòu)體進行填充,,不過卻是非常重要的,,不妨來看看:

/*
 * Mount structure initialization, provides a filled-in xfs_mount_t
 * such that the numerous XFS_* macros can be used.  If dev is zero,
 * no IO will be performed (no size checks, read root inodes).
 */
xfs_mount_t *                ----/3.1/
libxfs_mount(
    xfs_mount_t    *mp,
    xfs_sb_t    *sb,
    dev_t        dev,
    dev_t        logdev,
    dev_t        rtdev,
    int        flags)
{
    、,、,、
    xfs_sb_mount_common(mp, sb);                ----/3.2/
    、,、,、
    xfs_dir_mount(mp);                ----/3.3/
    、,、,、
    libxfs_readbuf                ----/3.4/
    、、,、
    libxfs_putbuf                ----/3.5/
    ,、、,、
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

/3.1/ 首先要理解的是xfs_mount_t這個結(jié)構(gòu)體類型,,我們定義了一個用戶層面上的xfs_mount結(jié)構(gòu)體,同時方便了使用以XFS_*為前綴的宏,。即該函數(shù)填充xfs_umount結(jié)構(gòu)體,,并將其返回,以供用戶進程訪問,,也就是mkfs.xfs了,。(注:即便設(shè)備上的文件系統(tǒng)格式化了,在未mount掛載前也是不能直接訪問的),。而在進程使用完后,,即在main函數(shù)最后,會調(diào)用libxfs_umount釋放占用的資源(可理解xfs_mount為中間變量),。
/3.2/ 而該函數(shù)使用xfs_sb超級塊的信息,建立了各類通用的mount掛載域
/3.3/ 顧名思義,,
/3.4/ 填充好xfs_mount信息后,,將其讀取到相應(yīng)的緩存區(qū)xfs_buf中
/3.5/ 而libxfs_putbuf –> cache_node_put,該函數(shù)將xfs_buf類型強制轉(zhuǎn)化成cache_node類型,,并添加至cache下發(fā)鏈表,。最終使得以完成設(shè)備文件系統(tǒng)的格式化。

最后回到main函數(shù),,調(diào)用libxfs_getsb,,同理該函數(shù)同樣讀取xfs_buf緩存中的內(nèi)容,若讀取出來的超級塊信息與寫入的相同,,則標識文件系統(tǒng)格式化成功,。到此,進程mkfs.xfs的整個執(zhí)行流程也算是結(jié)束了,,當然中間有很多東西都無視了(呵呵)

MOUNT

即將格式化xfs的磁盤掛載到相應(yīng)目錄

  • mount /dev/[sdx] /mnt
    • 源碼分析
      首先應(yīng)用層shell實現(xiàn)系統(tǒng)調(diào)用sys_mount,,而在本linux源碼版本:調(diào)用SYSCALL_DEFINE5,申明定義在include/linux/syscall.h中,。至于為什么使用這種方式定義sys_mount,,實質(zhì)上是為在64位的內(nèi)核上實現(xiàn)調(diào)用32位的系統(tǒng)調(diào)用(有待研究)。
    asmlinkage long sys_mount(char __user *dev_name, char __user *dir_name,        ----/0/
                char __user *type, unsigned long flags,
                void __user *data);
  • 1
  • 2
  • 3

函數(shù)sys_mount由SYSCALL_DEFINE5(5:代表參數(shù)個數(shù))定義:

#define SYSCALL_DEFINE0(sname)                        SYSCALL_METADATA(_##sname, 0);                    asmlinkage long sys_##sname(void)

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)                    SYSCALL_METADATA(sname, x, __VA_ARGS__)                __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...)                        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))            __attribute__((alias(__stringify(SyS##name))));            static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))        {                                        long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));            __MAP(x,__SC_TEST,__VA_ARGS__);                        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));            return ret;                            }                                    static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

在內(nèi)核中,,結(jié)構(gòu)體以及宏函數(shù)的使用可謂出神入化,。有關(guān)這里的宏函數(shù)定義如何實現(xiàn)的,暫不展開,不好表達,!

其實如下示:compat_sys_mount函數(shù)定義與上述SYSCALL_DEFINE5非常相似,,而與sys_mount也似乎存在著某種聯(lián)系(有待研究):

    /include/uapi/asm-generic/unistd.h:
    #define __NR_mount 40
    __SC_COMP(__NR_mount, sys_mount, compat_sys_mount)
  • 1
  • 2
  • 3

/0/ 上述的asmlinkage標識著調(diào)用函數(shù)參數(shù)的傳遞方式。若加該標識標識使用堆棧,,缺省時使用的是寄存器傳遞函數(shù)參數(shù),。如:匯編中調(diào)用c函數(shù),且使用堆棧傳遞函數(shù),,則在定c義函數(shù)時,,需在函數(shù)前添加宏asmlinkage。需要注意的是內(nèi)核只在系統(tǒng)調(diào)用時使用該宏,,原因:一是,,普通內(nèi)核函數(shù)調(diào)用寄存器傳參比堆棧傳參快很多,想想就是無需解釋,。二是:對于系統(tǒng)調(diào)用時使用堆棧傳參必然有其的合理性,。有關(guān)第二點說到的合理性,不妨稍作解釋,,就不展開了:
  1,,涉及到用戶態(tài)與內(nèi)核態(tài)的轉(zhuǎn)換,轉(zhuǎn)換過程實現(xiàn)需系統(tǒng)調(diào)用,。也就是說系統(tǒng)調(diào)用函數(shù)既需訪問用戶態(tài)資源,,與此同時訪問內(nèi)核資源。而內(nèi)核與用戶態(tài)的資源組織方式(可以這么說吧?。┛墒遣煌?,如:地址空間的映射方式就不同。
  2,、有人說堆棧的信息來源不就是來自寄存器的copy嗎,,我想其實兩者都能實現(xiàn)參數(shù)的傳遞,其本質(zhì)不都是要實現(xiàn)壓棧,,現(xiàn)場保護嗎,。只不過用戶態(tài)與內(nèi)核態(tài)的轉(zhuǎn)換,或說既要實現(xiàn)兩者之間的通訊,,又要實現(xiàn)兩者的隔離,。而畢竟內(nèi)核態(tài)的東西使用用戶態(tài)堆棧訪問方式相當于做了一些隔離。在此可能真的犧牲了一些傳參效率吧?。ê呛牵?。

下面不妨進入SYSCALL_DEFINE5函數(shù)定義,看看它做了些什么:

SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
        char __user *, type, unsigned long, flags, void __user *, data)
{
    int ret;
    char *kernel_type;
    struct filename *kernel_dir;
    char *kernel_dev;
    unsigned long data_page;

    ret = copy_mount_string(type, &kernel_type);                ----/1/
    if (ret < 0)
        goto out_type;

    kernel_dir = getname(dir_name);
    if (IS_ERR(kernel_dir)) {
        ret = PTR_ERR(kernel_dir);
        goto out_dir;
    }

    ret = copy_mount_string(dev_name, &kernel_dev);                
    if (ret < 0)
        goto out_dev;

    ret = copy_mount_options(data, &data_page);                ----/2/
    if (ret < 0)
        goto out_data;

    ret = do_mount(kernel_dev, kernel_dir->name, kernel_type, flags,                ----/3/
        (void *) data_page);

    free_page(data_page);
out_data:
    kfree(kernel_dev);
out_dev:
    putname(kernel_dir);
out_dir:
    kfree(kernel_type);
out_type:
    return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

/1/ 該函數(shù)將mount掛載的選項字符串內(nèi)核的一個空閑頁中,,如下示:

int copy_mount_string(const void __user *data, char **where)
{
    char *tmp;

    if (!data) {
        *where = NULL;
        return 0;
    }
    //成功返回指向內(nèi)核PAGE大小數(shù)據(jù)頁指針
    tmp = strndup_user(data, PAGE_SIZE);        ----/1.1/
    if (IS_ERR(tmp))
        return PTR_ERR(tmp);

    *where = tmp;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

/1.1/ 該函數(shù)最終調(diào)用copy_from_user函數(shù),,實現(xiàn)選項內(nèi)容拷貝到指定空閑頁中,。需注意的是,該函數(shù)可睡眠,,可能導(dǎo)致缺頁情況,。其中睡眠當然是針對當前進程而言的,若進程在copy_from_user或copy_to_user前被剝奪執(zhí)行權(quán)限,,而又在此期間觸發(fā)了交換條件(參照:虛擬內(nèi)存實現(xiàn)原理,,交換通常發(fā)生在內(nèi)存資源緊張時),則系統(tǒng)會將進程執(zhí)行所需的相應(yīng)資源從內(nèi)存交換到硬盤中,。之后當進程獲得執(zhí)行權(quán)限時,,由于硬盤的資源只有在要使用的時候才加載到內(nèi)存中(linux機制),因而即便是立即就加載數(shù)據(jù)到內(nèi)存,,進程也需阻塞睡眠等待,。還有種情況就是,剛要加載數(shù)據(jù)時,,進程又睡眠了,,連數(shù)據(jù)加載操作都做不了。

/2/ 較新的內(nèi)核版本均使用該函數(shù),,該函數(shù)與copy_mount_string函數(shù)不同之處在于:

int copy_mount_options(const void __user * data, unsigned long *where)
{
    int i;
    unsigned long page;
    unsigned long size;

    *where = 0;
    if (!data)
        return 0;

    if (!(page = __get_free_page(GFP_KERNEL)))    ----/2.1/
        return -ENOMEM;

    /* We only care that *some* data at the address the user
     * gave us is valid.  Just in case, we'll zero
     * the remainder of the page.
     */
    /* copy_from_user cannot cross TASK_SIZE ! */  

    size = TASK_SIZE - (unsigned long)data;    ----/2.2/
    if (size > PAGE_SIZE)
        size = PAGE_SIZE;

    i = size - exact_copy_from_user((void *)page, data, size);    ----/2.3/
    if (!i) {
        free_page(page);
        return -EFAULT;
    }
    if (i != PAGE_SIZE)
        memset((char *)page + i, 0, PAGE_SIZE - i);    ----/2.4/
    *where = page;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

/2.1/ 函數(shù)最終調(diào)用__get_free_pages函數(shù),,返回一個32位的非高端頁內(nèi)存地址
/2.2/ 拷貝用戶態(tài)數(shù)據(jù)不能超過TASK_SIZE(3G),超過3G已是屬于內(nèi)核態(tài)數(shù)據(jù)區(qū)域,,故使用TASK_SIZE減出需拷貝的數(shù)據(jù)大小,,更安全。
/2.3/ 調(diào)用exact_copy_from_user函數(shù),,較之于copy_from_user(返回值成功:0 失敗:失敗字節(jié)個數(shù)),,最終能返回精確的copy數(shù)據(jù)大小,。
/2.4/ 當拷貝數(shù)不足PAGE數(shù)據(jù)字節(jié)大小時,將未使用的頁面數(shù)據(jù)區(qū)域設(shè)置為零

/3/ 該函數(shù)mount實現(xiàn)掛載文件系統(tǒng)的主戰(zhàn)場,,不妨進入該函數(shù),,看看都做了些什么?

/*
 * Flags is a 32-bit value that allows up to 31 non-fs dependent flags to
 * be given to the mount() call (ie: read-only, no-dev, no-suid etc).
 *
 * data is a (void *) that can point to any structure up to
 * PAGE_SIZE-1 bytes, which can contain arbitrary fs-dependent
 * information (or be NULL).
 *
 * Pre-0.97 versions of mount() didn't have a flags word.
 * When the flags word was introduced its top half was required
 * to have the magic value 0xC0ED, and this remained so until 2.4.0-test9.
 * Therefore, if this magic number is present, it carries no information
 * and must be discarded.
 */
long do_mount(const char *dev_name, const char *dir_name,
        const char *type_page, unsigned long flags, void *data_page)
{
    struct path path;
    int retval = 0;
    int mnt_flags = 0;

    /* Discard magic */
    if ((flags & MS_MGC_MSK) == MS_MGC_VAL)        ----/3.1/
        flags &= ~MS_MGC_MSK;

    /* Basic sanity checks */

    if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE))        ----/3.2/
        return -EINVAL;

    if (data_page)
        ((char *)data_page)[PAGE_SIZE - 1] = 0;

    /* ... and get the mountpoint */
    retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);        ----/3.3/
    if (retval)
        return retval;

    retval = security_sb_mount(dev_name, &path,        ----/3.4/
                   type_page, flags, data_page);
    if (!retval && !may_mount())
        retval = -EPERM;
    if (retval)
        goto dput_out;

    /* Default to relatime unless overriden */
    if (!(flags & MS_NOATIME))
        mnt_flags |= MNT_RELATIME;

    /* Separate the per-mountpoint flags */
    if (flags & MS_NOSUID)
        mnt_flags |= MNT_NOSUID;
    if (flags & MS_NODEV)
        mnt_flags |= MNT_NODEV;
    if (flags & MS_NOEXEC)
        mnt_flags |= MNT_NOEXEC;
    if (flags & MS_NOATIME)
        mnt_flags |= MNT_NOATIME;
    if (flags & MS_NODIRATIME)
        mnt_flags |= MNT_NODIRATIME;
    if (flags & MS_STRICTATIME)
        mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME);
    if (flags & MS_RDONLY)
        mnt_flags |= MNT_READONLY;

    /* The default atime for remount is preservation */
    if ((flags & MS_REMOUNT) &&
        ((flags & (MS_NOATIME | MS_NODIRATIME | MS_RELATIME |
               MS_STRICTATIME)) == 0)) {
        mnt_flags &= ~MNT_ATIME_MASK;
        mnt_flags |= path.mnt->mnt_flags & MNT_ATIME_MASK;
    }

    flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE | MS_BORN |
           MS_NOATIME | MS_NODIRATIME | MS_RELATIME| MS_KERNMOUNT |
           MS_STRICTATIME);

    if (flags & MS_REMOUNT)
        retval = do_remount(&path, flags & ~MS_REMOUNT, mnt_flags,
                    data_page);
    else if (flags & MS_BIND)
        retval = do_loopback(&path, dev_name, flags & MS_REC);
    else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
        retval = do_change_type(&path, flags);
    else if (flags & MS_MOVE)
        retval = do_move_mount(&path, dev_name);
    else
        retval = do_new_mount(&path, type_page, flags, mnt_flags,            ----/4.5/
                      dev_name, data_page);
dput_out:
    path_put(&path);
    return retval;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94

/3.1/ 如該函數(shù)注釋所訴:這些magic數(shù)使用于早些版本(2.4.0-test9以前),,即無視MS_MGC_MSK與MS_MGC_VAL作用,,通過從flags中獲取,取反后寫入flags,,呵呵,,意圖很明顯啊,!,。
/3.2/ 對于該函數(shù)memchr,,就不展開了,它的作用是:在dir_name指向的內(nèi)存區(qū)域中的前PAGE大小個字節(jié)中查找int=0的字符null,,當?shù)谝淮斡龅皆撟址麜r停止查找,,成功返回指向字符的指針;否則返回NULL,。其實就是做了一下參數(shù)的檢查,,看指定數(shù)據(jù)內(nèi)容是否存在。
/3.3/ 接下來,,進入kern_path函數(shù):

int kern_path(const char *name, unsigned int flags, struct path *path)
{
    struct nameidata nd;
    int res = do_path_lookup(AT_FDCWD, name, flags, &nd);        ----/4.3.1/
    if (!res)
        *path = nd.path;                ----/4.3.2/
    return res;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

/3.3.2/ 從kern_path和該行可知,,函數(shù)只要實現(xiàn)將用戶態(tài)path路徑賦值到內(nèi)核態(tài)路徑,以便后續(xù)再內(nèi)核態(tài)的操作,。當然除此之外,,要做的事情就是審核path的有效性了。如:對于某一塊設(shè)備/dev/[sdx]而言,,系統(tǒng)調(diào)用mount操作最終實現(xiàn)將一個可訪問的塊設(shè)備(該設(shè)備)安裝到一個可訪問的節(jié)點,,即設(shè)備安裝前就是要可訪問的,也就是說在掛載之前,,/dev/sdx已經(jīng)與內(nèi)核建立過相關(guān)的聯(lián)系(nameidata結(jié)構(gòu)體),,而如上示/1/就將對其進行審核。
/3.3.1/ 該函數(shù)do_path_lookup –> filename_lookup,,不妨來看看filename_lookup函數(shù):

/* dfd為AT_FDCWD:指示操作應(yīng)在當前目錄
*  name為/dev/[sdx]
*  flags為LOOKUP_FOLLOW
*  nd為局部變量且尚未初始化
*/
static int filename_lookup(int dfd, struct filename *name,
                unsigned int flags, struct nameidata *nd)
{
    int retval = path_lookupat(dfd, name->name, flags | LOOKUP_RCU, nd);        ----/4.3.1/
    if (unlikely(retval == -ECHILD))  //等價于if (retval == -ECHILD)
        retval = path_lookupat(dfd, name->name, flags, nd);
    if (unlikely(retval == -ESTALE))
        retval = path_lookupat(dfd, name->name,
                        flags | LOOKUP_REVAL, nd);

    if (likely(!retval))
        audit_inode(name, nd->path.dentry, flags & LOOKUP_PARENT);        ----/4.3.2/
    return retval;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

/3.3.1/ 路徑查找函數(shù),,若/dev/[sdx]塊設(shè)備存在,path_lookupat函數(shù)返回值retval=0,,使nd有效(一系列操作將其初始化為正真的設(shè)備并與/dev/[sdx]比較該設(shè)備是否存在),。該函數(shù)返回0,do_path_lookup返回值ret為0,,kern_path返回值為0
/3.3.2/ if (likely(!retval)) 等價于if (!retval),,即進入分支對inode掛載節(jié)點進行審核,使得最終kern_path獲取掛載點(nd的dentry域),。

/3.4/ 安全性檢查

最終返回到do_mount,,而后根據(jù)參數(shù)flags的值來決定調(diào)用:do_remount do_loopback do_change_type do_move_mount do_new_mount其中的某一函數(shù)。下面以do_new_mount來簡單介紹一下:
/3.5/ 進入該函數(shù):

/*
* 在用戶空間創(chuàng)建一個新的掛載點并且將其添加到命名空間樹
*
 * create a new mount for userspace and request it to be added into the
 * namespace's tree
 */
static int do_new_mount(struct path *path, const char *fstype, int flags,
            int mnt_flags, const char *name, void *data)
{
    struct file_system_type *type;
    struct user_namespace *user_ns = current->nsproxy->mnt_ns->user_ns;
    struct vfsmount *mnt;
    int err;

    if (!fstype)
        return -EINVAL;

    /*get_fs_type --> __get_fs_type --> find_filesystem,,同時調(diào)用try_module_get,,遞增模塊module結(jié)構(gòu)體全局計數(shù),若是沒有找到則調(diào)用request_module來注冊新的文件系統(tǒng)到file_systems鏈中,,有關(guān)request_module如何實現(xiàn)模塊注冊,,這里涉及到驅(qū)動層,就不展開了,。*/
    type = get_fs_type(fstype);        ----/4.5.1/
    if (!type)
        return -ENODEV;

    if (user_ns != &init_user_ns) {
        if (!(type->fs_flags & FS_USERNS_MOUNT)) {
            put_filesystem(type);
            return -EPERM;
        }
        /* Only in special cases allow devices from mounts
         * created outside the initial user namespace.
         */
        if (!(type->fs_flags & FS_USERNS_DEV_MOUNT)) {
            flags |= MS_NODEV;
            mnt_flags |= MNT_NODEV | MNT_LOCK_NODEV;
        }
    }

    /*vfs_kern_mount --> mount_fs --> mount :該函數(shù)為mount統(tǒng)一路口
    * 當前格式化文件系統(tǒng)為xfs,,有  .mount    = xfs_fs_mount  (fs/xfs_super.c)
    * 故type->mount   -->  xfs_fs_mount  
    */

    mnt = vfs_kern_mount(type, flags, name, data);        ---->/4.5.2/
    if (!IS_ERR(mnt) && (type->fs_flags & FS_HAS_SUBTYPE) &&
        !mnt->mnt_sb->s_subtype)
        mnt = fs_set_subtype(mnt, fstype);

    put_filesystem(type);                ----/4.5.3/
    if (IS_ERR(mnt))
        return PTR_ERR(mnt);

    err = do_add_mount(real_mount(mnt), path, mnt_flags);        ----/4.5.4/
    if (err)
        mntput(mnt);
    return err;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

/3.5.1/ 該查看內(nèi)核是否注冊了參數(shù)fstype所指的文件系統(tǒng),,get_fs_type會將參數(shù)fstype字符串跟內(nèi)核鏈表中所有已經(jīng)注冊的文件系統(tǒng)結(jié)構(gòu)體file_system_type的name成員向比較,若已注冊則返回file_system_type結(jié)構(gòu)體,。
/3.5.2/ 該函數(shù)成功返回vfsmount結(jié)構(gòu)體,,內(nèi)核中代表掛載文件系統(tǒng)的vfsmountt結(jié)構(gòu)體填充成功。
/3.5.3/ 該函數(shù)執(zhí)行在vfs_kern_mount函數(shù)后,,將文件系統(tǒng)模塊使用量減1,。
/3.5.4/ 將新掛載的文件系統(tǒng)(由vfsmount表示)添加到系統(tǒng)的命名空間結(jié)構(gòu)體的已掛載文件系統(tǒng)鏈表中,命名空間是指系統(tǒng)中以掛載文件系統(tǒng)樹,,每個進程的PCB中都有namespace成員來表示該進程的命名空間,,大多數(shù)的進程共享同一個命名空間,所以如果在一個進程中將磁盤掛載到系統(tǒng)中,,在另一個進程也是可以看到的,,這就是由命名空間來實現(xiàn)的。vfsmount添加到相應(yīng)的namespace中的vfsmount鏈表成功后do_new_mount返回,。

這樣從sys_mount到vfs_kern_mount到mount(有些版本為get_sb)再到具體的文件系統(tǒng)層調(diào)用xfs_fs_mount進行處理,,處理完層層返回,內(nèi)核也就實現(xiàn)了指定文件系統(tǒng)的磁盤掛載到指定目錄,??傮w感覺在這個過程中內(nèi)核主要實現(xiàn)vfsmount結(jié)構(gòu)體的填充,而這個結(jié)構(gòu)體中最重要的是super_block的填充,,然后將vfsmount添加到相應(yīng)進程PCB的namespace成員所指向的namespace結(jié)構(gòu)體中,,大部分進程都指向這個namespace,所以掛載后對大部分進程都是可見的,。

UMOUNT

即卸載格式化xfs文件系統(tǒng)的掛載點

  • umount /dev/sdc /mnt
    • 源碼分析
      系統(tǒng)調(diào)用sys_umount,,而該函數(shù)使用SYSCALL_DEFINE2定義實現(xiàn)。首先不妨看看整個系統(tǒng)調(diào)用,,umount實現(xiàn)的調(diào)用流程:
    sys_umount()  --> SYSCALL_DEFINE2()

    -->do_umount --> security_sb_umount --> sb_umount --> mq_put_mnt --> kern_unmount 
SYSCALL_DEFINE2()
-->do_umount 
        --> security_sb_umount  //安全檢查
        --> umount_begin (統(tǒng)一接口)
           --> fuse_umount_begin(fs/fuse/inode.c) --> fuse_abort_conn --> ... ...
                --> mq_put_mnt 
                    --> kern_unmount 
                        --> mntput
                            --> mntput_no_expire()  //  -->cleanup_mnt() (有些linux版本有該函數(shù),,做多一次封裝后繼續(xù)往下調(diào)用)
                                --> deactivate_super()
                                    --> deactivate_locked_super()
                                        --> kill_sb()   //統(tǒng)一入口   若為xfs文件系統(tǒng) :static struct file_system_type xfs_fs_type
則  .kill_sb = kill_block_super  (在fs/xfs/xfs_super.c中申明,注:在此有聲明,,但定義在fs/super.c中)
                                                --> kill_block_super()
                                                        --> generic_shutdown_super() 
                                                                --> put_super()  同理上述  映射至 ==> xfs_fs_put_super
                                                                        --> xfs_fs_put_super()
                                                                                --> xfs_unmountfs()
                                                                                        --> xfs_ail_push_all_sync()
                                                                                                --> schedule()  //進程調(diào)度的主體程序
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

一般來說,獲取資源會比釋放資源的處理過程要復(fù)雜一些的,,那么可以說umount處理流程會簡易一些嗎,?進到SYSCALL_DEFINE2函數(shù)中去看看吧!

SYSCALL_DEFINE2(umount, char __user *, name, int, flags)
{
    struct path path;
    struct mount *mnt;
    int retval;
    int lookup_flags = 0;

    if (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))
        return -EINVAL;

    if (!may_mount())
        return -EPERM;

    if (!(flags & UMOUNT_NOFOLLOW))
        lookup_flags |= LOOKUP_FOLLOW;

    retval = user_path_mountpoint_at(AT_FDCWD, name, lookup_flags, &path);        ----/1/
    if (retval)
        goto out;
    mnt = real_mount(path.mnt);                ----/2/
    retval = -EINVAL;
    if (path.dentry != path.mnt->mnt_root)
        goto dput_and_out;
    if (!check_mnt(mnt))
        goto dput_and_out;
    if (mnt->mnt.mnt_flags & MNT_LOCKED)
        goto dput_and_out;
    retval = -EPERM;
    //capable函數(shù)做用戶權(quán)限檢查
    if (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))
        goto dput_and_out;

    retval = do_umount(mnt, flags);                ----/3/
dput_and_out:
    /* we mustn't call path_put() as that would clear mnt_expiry_mark */
    dput(path.dentry);
    mntput_no_expire(mnt);
out:
    return retval;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

/1/ 查找掛載點
/2/ real_mount(path.mnt) –> container_of(mnt, struct mount, mnt)有關(guān)container_of宏,,該宏可利用結(jié)構(gòu)體中的某一成員變量的首地址計算出整個結(jié)構(gòu)變量的地址,。在這里,我們想給mount結(jié)構(gòu)的指針變量賦值,,即可通過使用path結(jié)構(gòu)體中定義的指針變量path.mnt作為real_mount參數(shù),。上述第一個mnt為mount結(jié)構(gòu)體中的一個vfsmount類型成員變量的首地址,,而第二個mnt其實是mount結(jié)構(gòu)體成員變量(類型當然也是為vfsmount)。
/3/ 該函數(shù)時umount的主戰(zhàn)場:

static int do_umount(struct mount *mnt, int flags)
{
    struct super_block *sb = mnt->mnt.mnt_sb;
    int retval;

    retval = security_sb_umount(&mnt->mnt, flags);                ----/1.1/
    if (retval)
        return retval;

    /*
     * Allow userspace to request a mountpoint be expired rather than
     * unmounting unconditionally. Unmount only happens if:
     *  (1) the mark is already set (the mark is cleared by mntput())
     *  (2) the usage count == 1 [parent vfsmount] + 1 [sys_umount]
     */
    if (flags & MNT_EXPIRE) {                                        ----/1.2/
        if (&mnt->mnt == current->fs->root.mnt ||
            flags & (MNT_FORCE | MNT_DETACH))
            return -EINVAL;                                                ----/1.3/

        /*
         * probably don't strictly need the lock here if we examined
         * all race cases, but it's a slowpath.
         */
        //說那么多,,其實就是說最好是加把鎖唄,!
        lock_mount_hash();
        if (mnt_get_count(mnt) != 2) {
            unlock_mount_hash();
            return -EBUSY;                                ----/1.4/
        }
        unlock_mount_hash();

        if (!xchg(&mnt->mnt_expiry_mark, 1))  
            return -EAGAIN;                                ----/1.5/
    }

    /*
     * If we may have to abort operations to get out of this
     * mount, and they will themselves hold resources we must
     * allow the fs to do things. In the Unix tradition of
     * 'Gee thats tricky lets do it in userspace' the umount_begin
     * might fail to complete on the first run through as other tasks
     * must return, and the like. Thats for the mount program to worry
     * about for the moment.
     */

    if (flags & MNT_FORCE && sb->s_op->umount_begin) {
        sb->s_op->umount_begin(sb);                                                ----/1.6/
    }

    /*
     * No sense to grab the lock for this test, but test itself looks
     * somewhat bogus. Suggestions for better replacement?
     * Ho-hum... In principle, we might treat that as umount + switch
     * to rootfs. GC would eventually take care of the old vfsmount.
     * Actually it makes sense, especially if rootfs would contain a
     * /reboot - static binary that would close all descriptors and
     * call reboot(9). Then init(8) could umount root and exec /reboot.
     */
    if (&mnt->mnt == current->fs->root.mnt && !(flags & MNT_DETACH)) {
        /*
         * Special case for "unmounting" root ...
         * we just try to remount it readonly.
         */
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        down_write(&sb->s_umount);
        if (!(sb->s_flags & MS_RDONLY))
            retval = do_remount_sb(sb, MS_RDONLY, NULL, 0);
        up_write(&sb->s_umount);
        return retval;
    }

    namespace_lock();
    lock_mount_hash();
    event++;

    if (flags & MNT_DETACH) {
        if (!list_empty(&mnt->mnt_list))
            umount_tree(mnt, 2);
        retval = 0;
    } else {
        shrink_submounts(mnt);
        retval = -EBUSY;
        if (!propagate_mount_busy(mnt, 2)) {
            if (!list_empty(&mnt->mnt_list))
                umount_tree(mnt, 1);
            retval = 0;
        }
    }
    unlock_mount_hash();
    namespace_unlock();
    return retval;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94

/1.1/ 函數(shù)do_umount –> security_sb_umount –> sb_umount –> selinux_umount 顧名思義,是在安全的模式下做一些操作,,那該函數(shù)是用來實現(xiàn)在安全模式下完成umoun卸載的任務(wù)嗎,?分析可知,其實這個函數(shù)只是通過獲取一些信息(參照:superblock_has_perm函數(shù))進而來評估,,在當前環(huán)境能否順利的完成umount操作,。若不能則返回retval>0的數(shù),umount任務(wù)取消,。反之,,繼續(xù)。
/1.2/ 進入該分支說明MNT_EXPIRE置位為1了,,即掛載時效到期(掛載時間到期感覺不好理解,,把它當做普通標志位理解即可)
/1.3/ 中途返回說明umount失敗,這里表示:若當前掛載點知進程的根目錄或某進程使用強制手段卸載節(jié)點,,再或是MNT_DETACH置位(該參數(shù)置位的話,,將不會立即執(zhí)行umount操作,會等掛載點退出忙碌狀態(tài)時再操作),,均立即返回操作失敗,。
/1.4/ 檢查vfsmount的引用計數(shù)是否為2 若不為2則umount失敗立即返回,計數(shù)2代表的是當前vfsmount結(jié)構(gòu)的父vfsmount結(jié)構(gòu)體,,以及sys_mmount()對本對象的引用,,而簡單的可以理解就是:在文件系統(tǒng)卸載的時候不能再有額外的引用,想想也是,!
/1.5/ 設(shè)置vfsmount對象mnt_expiry_mark字段為1
/1.6/ umount_begin該函數(shù)正式開始處理umount事務(wù),。umount_begin –> fuse_abort_conn 該函數(shù)用于終結(jié)所有與掛載點的鏈接如:IO鏈接等

void fuse_abort_conn(struct fuse_conn *fc)
{
    spin_lock(&fc->lock);
    if (fc->connected) {
        fc->connected = 0;
        fc->blocked = 0;
        fc->initialized = 1;
        end_io_requests(fc);
        end_queued_requests(fc);
        end_polls(fc);
        wake_up_all(&fc->waitq);
        wake_up_all(&fc->blocked_waitq);
        kill_fasync(&fc->fasync, SIGIO, POLL_IN);
    }
    spin_unlock(&fc->lock);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

總結(jié)

到此告一段落,其中有很多地方還有待學(xué)習(xí),,

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,謹防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多