• linux系统编程专题(六) 系统调用之文件系统


    介绍linux系统编程文件系统相关知识点

    一、文件存储

    1.1、inode (文件属性)

    inode为文件是否存在的标志,本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位 置…也叫作文件属性管理结构,大多数的 inode 都存储在磁盘上。少量常用、近期使用的 inode 会被缓存到内存中。

    所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。

    inode

    查看文件信息使用stat命令

    stat lseek.c 
    
    • 1
    image-20220909154540861

    可以看到Inode编号与连接数Links

    1.2、dentry (目录项)

    目录项,其本质依然是结构体,重要成员变量有两个 {文件名,inode,…},而文件内 容(data)保存在磁盘盘块中。

    inode

    1.3、硬链接

    由于linux下的文件是通过索引节点(Inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配inode。inode指向了物理硬盘的一个区块,事实上文件系统会维护一个引用计数,只要有文件指向这个区块,它就不会从硬盘上消失。每添加一个硬链接,文件的链接数就加1。

    1、创建tmp文件

    touch tmp
    
    • 1

    2、查看链接数:ls -lrth tmp,发现链接数为1

    ls -lrth tmp
    
    • 1
    -rw-r--r-- 1 root root 0 Sep  9 16:36 tmp
    
    • 1

    3、用ln命令来建立硬链接:ln tmp tmphard

    ln tmp tmphard
    
    • 1

    4、查看链接数与inode:ls -ilrth tmp*,发现inode一样,且链接数都是2

    ls -ilrth tmp*
    
    • 1
    1063415 -rw-r--r-- 2 root root 0 Sep  9 16:36 tmphard
    1063415 -rw-r--r-- 2 root root 0 Sep  9 16:36 tmp
    
    • 1
    • 2

    分析:在创建链接前,tmp显示的链接数目为1,创建链接后对比:

    • tmp 和tmphard 的链接数目都变为2;
    • tmp 和tmphard 在inode号是一样的;
    • tmp 和tmphard 显示的文件大小也是一样;

    可见进行了ln命令的操作结果:

    tmp 和tmphard 是同一个文件的两个名字,它们具有同样的索引节点号和文件属性,建立文件tmp的硬链接,就是为tmp的文件索引节点在当前目录上建立一个新指针。

    5、删除

    你可以删除其中任何一个,每次只会删除一个指针,链接数同时减一,只有将所有指向文件内容的指针,也即链接数减为0时,内核才会把文件内容从磁盘上删除。

    rm tmp
    ls -ilrth tmp*
    
    • 1
    • 2
    1063415 -rw-r--r-- 1 root root 0 Sep  9 16:36 tmphard
    
    • 1

    硬链接缺点:尽管硬链接节省空间,也是Linux系统整合文件系统的传统方式,但是存在以下不足之处:

    1. 不可以在不同文件系统的文件间建立链接

    电脑与u盘间就不能创建硬链接

    1. 只有超级用户才可以为目录创建硬链接。

    1.4、软链接

    软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。

    软链接的inode所指向的内容实际上是保存了一个绝对路径,当用户访问这个文件时,系统会自动将其替换成其所指的文件路径。类似于Windows 的快捷方式。

    用ln -s命令来建立软链接:ln -s tmp tmpsoft

    ln -s tmp tmpsoft
    
    • 1

    创建后查看连接数

    ls -ilrth tmp*
    
    • 1
    1063415 -rw-r--r-- 2 root root 0 Sep  9 17:00 tmphard
    1063415 -rw-r--r-- 2 root root 0 Sep  9 17:00 tmp
    1063416 lrwxrwxrwx 1 root root 3 Sep  9 17:01 tmpsoft -> tmp
    
    • 1
    • 2
    • 3

    如果移除源文件tmp,再查看就会报错

    image-20220909171512696

    软链接缺点:

    • 因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;
    • 它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。

    二、函数调用

    2.1、stat (查看文件信息)

    作用:查看文件信息

    1、脚本指令stat

    查看文件信息使用stat命令

    stat tmphard
    
    • 1
    root@VM-4-5-ubuntu:~/gccTest# stat tmphard 
      File: tmphard
      Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
    Device: fc02h/64514d	Inode: 1063415     Links: 2
    Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2022-09-09 17:00:40.270413164 +0800
    Modify: 2022-09-09 17:00:40.270413164 +0800
    Change: 2022-09-09 17:14:08.279544985 +0800
     Birth: -
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2、通过man page查询stat用法

    Man Page第二章是系统调用

    man 2 stat
    
    • 1

    可以看到用法说明,以及struct state的说明

    image-20220913111749681

    3、函数stat

    编程里面也可以通过stat函数获取这些信息

    获取文件属性,从inode结构体中获取,同 stat file

    函数int stat(const char *path, struct stat *buf);
    参数path: 文件路径
    buf:(传出参数) 存放文件属性,inode结构体指针。
    返回值成功: 0
    失败: -1 errno
    应用场景获取文件大小: buf.st_size
    获取文件类型: buf.st_mode
    获取文件权限: buf.st_mode

    stat 结构体

    struct stat  
    {   
        dev_t       st_dev;     /* ID of device containing file -文件所在设备的ID*/  
        ino_t       st_ino;     /* inode number -inode节点号*/    
        mode_t      st_mode;    /* protection -保护模式?*/    
        nlink_t     st_nlink;   /* number of hard links -链向此文件的连接数(硬连接)*/    
        uid_t       st_uid;     /* user ID of owner -user id*/    
        gid_t       st_gid;     /* group ID of owner - group id*/    
        dev_t       st_rdev;    /* device ID (if special file) -设备号,针对设备文件*/    
        off_t       st_size;    /* total size, in bytes -文件大小,字节为单位*/    
        blksize_t   st_blksize; /* blocksize for filesystem I/O -系统块的大小*/    
        blkcnt_t    st_blocks;  /* number of blocks allocated -文件所占块数*/    
        time_t      st_atime;   /* time of last access -最近存取时间*/    
        time_t      st_mtime;   /* time of last modification -最近修改时间*/    
        time_t      st_ctime;   /* time of last status change - */    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    示例mystat.c 获取文件大小

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        struct stat sbuf;
    
        int ret = stat(argv[1], &sbuf);
        if (ret == -1) {
            perror("stat eror");
            exit(1);
        }
    	
    	printf("file size: %ld\n", sbuf.st_size);    
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    运行

    ./a.out lseek.c 
    
    • 1
    file size: 735
    
    • 1

    2.2、lstat (查看文件信息)

    类似于stat,差别是这个不会穿透查询,即可以查询软链接文件

    1、lstat用法示例

    示例mylstat.c 判断文件类型

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        struct stat sb;
    
        int ret = lstat(argv[1], &sb);
        if (ret == -1) {
            perror("stat eror");
            exit(1);
        }
        
        if (S_ISREG(sb.st_mode)) {
            printf("It's a regular\n");
        } else if (S_ISDIR(sb.st_mode)) {
            printf("It's a dir\n");
        } else if (S_ISFIFO(sb.st_mode)) {
            printf("It's a pipe\n");
        } else if (S_ISLNK(sb.st_mode)) {
            printf("it's a sym link\n");
        }
    
    	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

    运行结果

    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmp
    It's a regular
    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmpsoft 
    it's a sym link
    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmphard 
    It's a regular
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果是stat,即把lstat这行改为stat

    int ret = lstat(argv[1], &sb);
    int ret = stat(argv[1], &sb);
    
    • 1
    • 2
    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmp
    It's a regular
    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmphard 
    It's a regular
    root@VM-4-5-ubuntu:~/gccTest# ./a.out tmpsoft 
    It's a regular
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、穿透符号链接

    • stat 会,stat 会拿到符号链接指向那个文件或目录的属性
    • 类似穿透现象还有 cat vim(实现基于系统调用)
    • lstat 不会,不想穿透符号就用 lstat

    文件类型判断方法,使用宏函数:

    • S_ISLNK(st_mode):是否是一个连接.
    • S_ISREG(st_mode):是否是一个常规文件.
    • S_ISDIR(st_mode):是否是一个目录
    • S_ISCHR(st_mode):是否是一个字符设备.
    • S_ISBLK(st_mode):是否是一个块设备
    • S_ISFIFO(st_mode):是否是一个FIFO文件.
    • S_ISSOCK(st_mode):是否是一个SOCKET文件

    2.3、link && unlink (建立硬链接)

    1、回顾ln命令

    回顾:用ln命令来建立硬链接:ln tmp tmphard

    ln tmp tmphard
    
    • 1

    2、系统调用link && unlink

    使用ln命令实际上也是调用的系统调用,那么如何通过代码的方式实现创建硬链接

    为已经存在的文件创建目录项(硬链接)

    函数int link(const char *oldpath, const char *newpath);
    参数oldpath: 旧的路径
    newpath:新的路径
    返回值成功:0
    失败:-1 设置 errno 为相应值

    删除一个文件的目录项

    函数int unlink(const char *pathname);
    参数pathname:文件路径
    返回值成功:0
    失败:-1 设置 errno 为相应值

    3、link && unlink实现改名操作

    示例:实现 mv 命令的改名操作 myMv.c

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        link(argv[1], argv[2]);
        unlink(argv[1]);
    		return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    image-20220914091756990

    Linux下删除文件的机制:不断将st_nlink -1,直至减到0为止。无目录项对应的文件,将会被操作系统择机释放。(具体时间由系统内部调度算法决定),

    因此,我们删除文件,从某种意义上说,只是让文件具备了被释放的条件。

    4、隐式回收概念

    当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统 的这一特性称之为隐式回收系统资源。

    2.4、getcwd

    获取进程当前工作目录

    注意:这个是卷 3的标库函数

    char *getcwd(char *buf, size_t size);

    成功:buf 中保存当前进程工作目录位置。失败返回 NULL。

    示例

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        char path[256];
        char *ptr = getcwd(path, sizeof(path));
        if (NULL == ptr) {
            perror("getcwd error");
            exit(-1);
        }
        printf("Current working directory: %s\n", path);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果

    Current working directory: /Users/zhanglei/CLionProjects/MyCLanguage/cmake-build-debug

    2.5、chdir

    改变当前进程的工作目录

    int chdir(const char *path);

    成功:0;失败:-1 设置 errno 为相应值

    示例

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        // set
        char path1[256]="/Users/zhanglei/CLionProjects/MyCLanguage";
        int result = chdir(path1);
        if (result == -1) {
            perror("chdir error");
            exit(-1);
        }
    
        // get
        char path[256];
        char *ptr = getcwd(path, sizeof(path));
        if (NULL == ptr) {
            perror("getcwd error");
            exit(-1);
        }
        printf("Current working directory: %s\n", path);
        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

    运行结果

    Current working directory: /Users/zhanglei/CLionProjects/MyCLanguage

    2.6、文件、目录权限

    注意:目录文件也是“文件”。其文件内容是该目录下所有子文件的目录项 dentry。 可以尝试用 vim 打开一个目录。

    rwx
    文件文件的内容可以被查看 cat、more、less…内容可以被修改 vi、> …可以运行产生一个进程 ./文件名
    目录目录可以被浏览 ls、tree…创建、删除、修改文件 mv、touch、mkdir…可以被打开、进入 cd
    vim gcctest
    
    • 1
    image-20220914110026163

    2.7、opendir && closedir && readdir

    注意:这3个是卷 3的标库函数

    1、opendir

    根据传入的目录名打开一个目录 (库函数) DIR * 类似于 FILE *

    DIR *opendir(const char *name);

    成功返回指向该目录结构体指针,失败返回 NULL 参数支持相对路径、绝对路径两种方式:

    例如:打开当前目录:

    1 getcwd() , opendir()

    2 opendir(“.”);

    2、closedir

    关闭打开的目录

    int closedir(DIR *dirp);

    成功:0;失败:-1 设置 errno 为相应值

    3、readdir

    读取目录 (库函数)

    struct dirent *readdir(DIR *dirp);

    成功,返回目录项结构体指针;失败返回NULL设置errno 为相应值(需注意返回值,读取数据结束时也返回 NULL 值,所以应借助 errno 进一步加以区分)

    struct dirent
    {
       long d_ino; /* inode number 索引节点号 */
       off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
       unsigned short d_reclen; /* length of this d_name 文件名长 */
       unsigned char d_type; /* the type of d_name 文件类型 */
       char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长255字符 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    由于\0结束符暂居一个字符的位置,所以文件名最长254个字符

    4、示例:实现简单的 ls 功能

    myls.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        DIR * dp;
        struct dirent *sdp;
    
        dp = opendir(argv[1]);
        if (dp == NULL) {
            perror("opendir error");
            exit(1);
        }
    
        while ((sdp = readdir(dp)) != NULL) {
            printf("%s\t", sdp->d_name);
        }
        printf("\n");
    
        closedir(dp);
    
    	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

    运行结果

    root@VM-4-5-ubuntu:~/gccTest# ./a.out ./
    ..	tmpsoft	.	lseek2.c	mystat.c	dynamiclib	block_readtty.c	mylstat.c	nonblock_readtty.c	tmp	myMv.c	tmphard	a.out	myls.c	staticlib	gcctest	read_cmp_getc.c	test.c	
    
    • 1
    • 2

    5、示例:递归遍历目录

    查询指定目录,递归列出目录中文件,同时显示文件大小。ls-R.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void isFile(char *name);
    
    // 打开目录读取,处理目录
    void read_dir(char *dir, void (*func)(char *)) {
        char path[256];
        DIR *dp;
        struct dirent *sdp;
    
        dp = opendir(dir);
        if (dp == NULL) {
            perror("opendir error");
            return;
        }
        // 读取目录项
        while ((sdp = readdir(dp)) != NULL) {
            if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
                continue;
            }
            //fprintf();
            // 目录项本身不可访问, 拼接. 目录/目录项
            // /mnt/jason   a.txt
            sprintf(path, "%s/%s", dir, sdp->d_name);
    
            // 判断文件类型,目录递归进入,文件显示名字/大小
            //isFile(path);
            (*func)(path);
    //        func(path);//由于函数名称就是个指针,这样写也ok
        }
    
        closedir(dp);
    }
    
    void isFile(char *name) {
    
        int ret = 0;
        struct stat sb;
    
        // 获取文件属性, 判断文件类型
        ret = stat(name, &sb);
        if (ret == -1) {
            perror("stat error");
            return;
        }
        // 是目录文件
        if (S_ISDIR(sb.st_mode)) {
            read_dir(name, isFile);
        }
        // 是普通文件, 显示名字/大小
        printf("%10s\t\t%lld\n", name, sb.st_size);
    }
    
    
    int main(int argc, char *argv[]) {
        // 判断命令行参数
        if (argc == 1) {
            isFile(".");
        } else {
            isFile(argv[1]);
        }
    
        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
    • 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

    运行结果

    image-20220914151839078

    2.8、重定向

    1、命令行重定向

    命令行重定向:cat a > b

    root@VM-4-5-ubuntu:~/gccTest# cat test.txt 
    新建文本文档
    root@VM-4-5-ubuntu:~/gccTest# cat test.txt > test2.txt
    root@VM-4-5-ubuntu:~/gccTest# cat test2.txt 
    新建文本文档
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、dup

    如果使用系统调用,用函数的方式如何实现?

    dup:duplicate英文缩写

    功能:文件描述符拷贝。 使用现有的文件描述符,拷贝生成一个新的文件描述符,且函数调用前后这个两个文件描述符指向同一文件。

    int dup(int oldfd);

    成功:返回一个新文件描述符;失败:-1 设置 errno 为相应值

    3、dup2

    功能:文件描述符拷贝。重定向文件描述符指向。

    通过该函数可实现命令行“重定向”功能。使得原来指向某文件的文件描述符,指向其 他指定文件。

    int dup2(int oldfd, int newfd);

    成功:返回一个新文件描述符;如果 oldfd 有效,则返回的文件描述符与 oldfd 指向同一文件。

    失败:如果 oldfd 无效,调用失败,关闭 newfd。返回-1,同时设置 errno 为相应值。

    dup2.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        int fd1 = open(argv[1], O_RDWR);       // 012  --- 3
    
        int fd2 = open(argv[2], O_RDWR);       // 012  --- 3
    
        int fdret = dup2(fd1, fd2);     // 返回 新文件描述符fd2
        printf("fdret = %d\n", fdret);
    
        int ret = write(fd2, "1234567", 7); // 写入 fd1 指向的文件
        printf("ret = %d\n", ret);
    
        dup2(fd1, STDOUT_FILENO);       // 将屏幕输入,重定向给 fd1所指向的文件.
    
        printf("-----------------------------886\n");
    
    	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

    4、fcntl

    int fcntl(int fd, int cmd, ....);
    cmd: F_DUPFD

    当 fcntl 的第二个参数为 F_DUPFD 时, 它的作用是根据一个已有的文件描述符,复制 生成一个新的文件描述符。此时,fcntl 相当于 dup 和 dup2 函数。

    参 3 指定为 0 时,因为 0 号文件描述符已经被占用。所以函数自动用一个最小可用文件 描述符。

    参 3 指定为 9 时,如果该文件描述符未被占用,则返回 9。否则,返回大于 9 的可用文件描述符。

    fcntl 实现dup描述符fcntl_dup.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[]){
    	int fd1 = open(argv[1], O_RDWR);
    	printf("fd1 = %d\n", fd1);
    	
    	int newfd = fcntl(fd1, F_DUPFD, 0);
    	printf("newfd = %d\n", newfd);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果

    root@VM-4-5-ubuntu:~/gccTest# gcc fcntl_dup.c 
    root@VM-4-5-ubuntu:~/gccTest# ./a.out ls-R.c 
    fd1 = 3
    newfd = 4
    
    • 1
    • 2
    • 3
    • 4

    回顾open函数

    函数int open(char *pathname, int flags)
    参数pathname:要打开的文件路径名
    flags:文件打开方式,#include
    返回值成功: 打开文件所得到对应的文件描述符(整数)
    失败: -1, 设置errno

    这里打开得到的文件描述符为3,fcntl重定向为0,由于0-3都被系统占了,所以输出最小描述符为4

  • 相关阅读:
    金仓数据库KingbaseES客户端应用参考手册--17. vacuumdb
    Redis 线程模型和工作流程
    万宾科技智能井盖传感器特点介绍
    8086与8088
    CRC16计算FC(博途SCL语言)
    以太坊合并后展望与机构DeFi的未来
    创建springboot(五)文章发表项目
    《uni-app》表单组件-Picker组件
    C# RestoreFormer 图像修复
    C#:实现数据挖掘之决策树ID3算法(附完整源码)
  • 原文地址:https://blog.csdn.net/liuxingyuzaixian/article/details/126846570