• 进程间通信,SystemV共享内存


    进程通信IPC(Inter-Process-Communication)

    宏观上理解进程通信

    1. 计算机是模拟人的行为逻辑,而进程非常像人,从创建到最终的销毁。

    2. 以普遍性论述,人与人之间可以通过声音传递信息,一方通过声音发送信息,一方接受信息并通过声音进行反馈。而我们发现声音这种介质是不独属于某个人的,是所有人共享的(感觉声音还是有点抽象,哈哈哈~)。当我们将这种交流逻辑映到射进程中,进行2个或者多个进程之间的交互或者通信,我们需要确定一种不独属性2个进程的介质,而计算机中能满足这个要求的就是内存。因此那些进程间的通信介质:匿名管道,命名管道,共享内存,消息队列,信号量等本质上都是内存的不同存在形式。同时人与人之间的交流,也是遵守规则的,典型的就是:请说普通话。因此进程间的通信依据不同的通信介质必然要设置不同的规则,以保证通信的高效与安全。。最终,一个进程将信息放到这个介质里面,另外一个进程通过这个介质拿到信息,处理后返回到介质中,–这就是进程间的通信宏观上的行为逻辑。

    3. 进程是具有独立性的,因此共享的内存必然不是进程提供的,只能是OS提供的。因此进程间通信的本质:由OS提供公开资源用以进程间通信,并加以管理的行为。

    4. 共享的资源可以以文件、队列、原始内存形式出现。

    5. 如果把网络也看做是一种介质,它也必然有它的规则。

    进程间通信目的

    • 数据传输:一个进程需要将它的数据发送给另一个进程
    • 资源共享:多个进程之间共享同样的资源。
    • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
    • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

    管道

    管道本质是一种特殊的文件,向文件写入内容时,不会立刻映射到磁盘上。

    理论

    在这里插入图片描述

    1. 从上面的图中我们发现,子进程拷贝父进程的文件指针数组,完成了对同一份文件的访问,同时又保证的进程的独立性。因此文件可以说是一个完美的共享介质。
    2. 如果我们让文件指针数组中有2个分别以写和读方式打开的文件指针,父进程在进行业务逻辑时,关闭读操作,只进行写操作,子进程关闭写操作只进行读操作。这就完成了单向通信----也就是管道。

    匿名管道

    1. 匿名管道是OS内核提供的文件,不需要名字来标识这个文件,只供有血缘的进程使用
    2. 命名管道是用户通过系统接口调用创建的一种文件,所以进程都可见。

    匿名管道的生成

     #include 
     int pipe(int fildes[2]);
    
    • 1
    • 2
    1. filde是一个输出参数,默认fildes[0]为读端,fildes[1]写端

    2. 成功返回0,否则-1

    匿名管道特点

    1. 管道是一个单向通信的通信信道,本质上是一种文件,采用引用计数的被OS管理着它的生命周期。
    2. 管道是面向字节流
    3. 管道的2个对象必须具有血缘关系:父子进程,兄弟进程等,因为文件指针数组是可以被继承
    4. 管道自带同步机制,原子性写入
    5. 管道要求写端和读端必须是不用进程,同时在使用管道的时候,必须保证写端和读端都运行。

    命名管道

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwFLUxzA-1667445179814)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101182514832.png)]

    1. 在磁盘中:路径+文件名,是唯一能标识文件的,无论是绝对路径还是相对路径
    2. 如果进程client和server能同时使用同一个文件,同时这个文件的数据不会立即刷新到磁盘上,那么这个文件就可以作为进程client和server的共享资源,也就是管道。
    3. 这个文件是我们用户层确定的,文件名是自己定义的,因此称为命名管道
    4. 从client和server可以看出,命名管道的2个对象是可以不具有血缘关系的,因为管道名和路径都对任意一个进程为可视化。
    5. 对命名管道的操作,就变成了对文件的操作,只是这个数据不会刷新到磁盘上,被悬挂起来了。
    6. 对命名管道的观察,是可以看到管道的同步等特点的。

    命名管道的生成

    #include 
    #include 
    int mkfifo(const char *pathname, mode_t mode);
    nt main()
    {
        // int mkfifo(const char *pathname, mode_t mode);
        //mode 是要受umask:mode&(~umask)
        if(mkfifo(MY_FIFO,666)<0)
        {
            perror("mkfifo failed");
            return -1;
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. pathname:路径+命名管道

    2. mode:初始权限,因为受umask影响,最终:mode&(~umask)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-85rFlEOy-1667445179817)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101185945442.png)]

    命名管道的代码

    client

    #include "./comm.h"
    int main()
    {
        int fd = open("./fifo", O_WRONLY | O_TRUNC);
        if (fd < 0)
        {
            perror("O_WRONLY failed");
            exit(1);
        }
        while(1)
        {
            printf("请输入#:");
            fflush(stdout);
            char tmp[100]={0};
            read(0,tmp,sizeof(tmp)-1);
            write(fd, tmp, strlen(tmp));
    
        }
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    server

    #include "./comm.h"
    int main()
    {
        //创建命名管道
        // int mkfifo(const char *pathname, mode_t mode);
        // mode 是要受umask:mode&(~umask)
        // if (mkfifo(MY_FIFO, 0666) < 0)
        // {
        //     perror("mkfifo is failed");
        //     return -1;
        // }
    
        int fd = open("./fifo", O_RDONLY);
        if (fd < 0)
        {
            perror("O_RDONLY  failed");
    
            exit(1);
        }
        while (1)
        {
            char data[100] = {0};
            int ret = read(fd, data, sizeof(data) - 1);
            if (ret > 0)
            {
                //去掉读取的\n
                data[strlen(data) - 1] = '\0';
                //进程控制
                if (strcmp(data, "run") == 0)
                {
                    if (fork() == 0)
                    {
                        execl("/usr/bin/sl", "sl", NULL);
                        exit(1);
                    }
                    waitpid(-1, NULL, 0);
                }
                else if (strcmp(data, "show") == 0)
                {
                    if (fork() == 0)
                    {
                        execlp("ls", "ls", "-al", NULL);
                        exit(1);
    
                    }
                    waitpid(-1, NULL, 0);
                }
                else
                {
                    printf("%s\n", data);
                }
            }
            else if (ret == 0)
            {
                printf(" read end\n");
                break;
            }
            else
            {
                break;
            }
        }
        close(fd);
    
        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

    comm.h

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define MY_FIFO "./fifo"
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    管道的大小

    管道是具有一定大小的,在linux是64Kb

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YtFqX33y-1667445179819)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101145237522.png)]

    同步机制

    1. 当管道写满的时候,只有管道被读走4Kb后,才能唤醒写操作。—,读的慢,写端等读端,只不过这个等具有原子性,必须留出4Kb空间才能唤醒
    2. 当管道没有数据的时候,写的慢,读端会等写端这样可以有效的避免的数据丢失与覆盖问题。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6FXSenH-1667445179821)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101150210392.png)]

    1. 当写端关闭的时候,读端也会同步关闭

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KvwM3OJV-1667445179824)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101152044910-1667287245811-1.png)]

    1. 当读端关闭时,写端也会同步关闭,子进程被关闭是通过信号

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cbq0pkyF-1667445179825)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101154731137.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3S5dapTT-1667445179827)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101154832391-1667288913584-3.png)]

    System V

    除了管道这种基于文件进行的通信,还有很多通信,而这些通信是个人,组织OS设置的规则来帮助进程通信。在历史发展中,System V和POSIX标准成为了主流。

    System V中有System V 共享内存(shm),System V 消息队列(msq),System V 信号量(sem) 。这里重点介绍共享内存shm

    System V 共享内存

    理论

    1. 当OS开辟一块内存,进程A和进程B都可以访问,这块内存就可以成为进程通信的介质,即共享内存

    2. 一个系统中,必然可以有多个共享内存,而OS是不信任任何人的,因此OS必须亲自去管理这些共享内存,OS要想管理好这块共享内存,必然先用结构体描述这块内存,最后通过特定的数据结构去组织这些结构体,最终OS管理共享内存块就变成了对结构体的增删查改。

    3. 同时,不同的进程都要与这个共享内存交互,必然需要一个能唯一标识这块共享内存的标识符,方便定位这块内存,这和文件描述符非常像。

    4. 因此System V为了管理好共享内存,需要解决:生成共享内存,进程和共享内存挂接,进程和共享内存去关联,共享内存的释放4个问题,也就是4个系统接口。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIilEF8Z-1667445179828)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101222819770.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zJg1SJ6h-1667445179830)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221101214843656.png)]

    共享内存数据结构

    以下只是用户层的结构体信息,内核会在其基础添加一些信息

    struct shmid_ds {
        struct ipc_perm shm_perm; /* operation perms */
        int shm_segsz; /* size of segment (bytes) */
        __kernel_time_t shm_atime; /* last attach time */
        __kernel_time_t shm_dtime; /* last detach time */
        __kernel_time_t shm_ctime; /* last change time */
        __kernel_ipc_pid_t shm_cpid; /* pid of creator */
        __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
        unsigned short shm_nattch; /* no. of current attaches */
        unsigned short shm_unused; /* compatibility */
        void *shm_unused2; /* ditto - used by DIPC */
        void *shm_unused3; /* unused */
    };
    struct ipc_perm {
        key_t          __key;    /* Key supplied to shmget(2) */
        uid_t          uid;      /* Effective UID of owner */
        gid_t          gid;      /* Effective GID of owner */
        uid_t          cuid;     /* Effective UID of creator */
        gid_t          cgid;     /* Effective GID of creator */
        unsigned short mode;     /* Permissions + SHM_DEST and
                                               SHM_LOCKED flags */
        unsigned short __seq;    /* Sequence number */
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    共享内存的特点

    1. 共享内存是所有进程间通信速度最快的,因此共享内存申请好后,进程与shm一旦挂接,即页表中与相应的物理内存建立映射关系,进程可以直接使用/看到这块共享内存就像动态开辟内存的malloc/new函数一样。
    2. 管道不同与共享内存的:它会有拷贝的时间消耗:从用户层数据拷贝到文件内核缓冲区,从文件内核缓冲区到用户层。
    3. 共享内存不存在任何同步或互斥机制,这也就导致执行流紊乱问题,需要加锁(目前不会)。

    生成共享内存(get shm)

    #include 
    #include 
    int shmget(key_t key, size_t size, int shmflg);
    
    • 1
    • 2
    • 3
    1. return

    创建成功返回共享内存的标识符

    1. key
    1. 这个key是为了帮助创建标识共享内存id的一个种子,可以自己确定,但是为了防止污染OS内核的某些标识符,使用系统接口 ftok来获得key。本质是利用路径+文件指向文件的唯一性.
    #include 
    #include 
    key_t ftok(const char *pathname, int proj_id);
    
    • 1
    • 2
    • 3

    return

    1. key-t 为有符号整数int
    2. 成功返回key,失败返回-1

    pathname

    路径+文件名可以唯一标识一个文件,文件必须存在

    proj_id

    自己设置的一个int数

    1. size

    共享内存在内核中的申请基本单位为一页:4096bite。用户申请时,编译器总会向上取整。即用户:4097,OS:4096*2

    1. shmflg
    1. 一种标记位:IPC_CREAT,IPC_EXCL

    在这里插入图片描述

    1. 如果单独使用IPC_CREAT或者flag=0:OS会创建一个共享内存,如果创建的共享内存已经存在,则直接返回已经存在的共享内存id。

    2. IPC_EXCL单独使用没有意义,一般和IPC_CREAT搭配使用IPC_CREAT|IPC_EXCL:创建一个新的共享内存,如果和系统内核重复,就返回错误-1。

    3. 即如果创建是用IPC_CREAT|IPC_EXCL,使用时用IPC_CREAT

    挂接共享内存(attach shm)

    只需要了解会挂接就行,shmaddr和 shmflg不需要了解太多,如有需要请man指令

    #include 
    #include 
    void *shmat(int shmid, const void *shmaddr, int shmflg);
     //进程挂接共享内存
        char *retp = (char *)shmat(shmid, NULL, 0);
        if (retp == NULL)
        {
            perror("shmat failed");
            exit(1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    挂接失效(detache shm)

    #include 
    #include 
    int shmdt(const void *shmaddr);
    int ret = shmdt(retp);
        if (ret < 0)
        {
            perror("shmdt failed");
            exit(1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    销毁共享内存(destory shm)

    共享内存的生命周期是随OS内核的,只能通过调用系统接口才能销毁共享内存,或者内核OS重启。

    #include 
    #include 
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    • 1
    • 2
    • 3

    return

    成功操作共享内存返回0,失败返回-1

    shmid

    共享内存的标识符

    cmd

    操作指令:IPC_STAT,IPC_SET,IPC_RMID.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sXxgE3k8-1667445179834)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221102224421026.png)]

    buf

    用于辅助完成cmd指令

    ipcs -m

    显示已申请的共享内存的信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mdoOWPY-1667445179835)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221103093200288.png)]

    ipcrm -m +shmid

    1. 只有有挂接数为0时,删除shmid对应的共享内存才能成功。
    2. 命令也会调用shmctl进行销毁

    共享内存与全局变量

    全局变量虽然可以被子进程继承,但是会发生“写时拷贝‘,而共享内存不会。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDTfTrwl-1667445179837)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221103090730632.png)]

    共享内存代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0j5XbAfy-1667445179838)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221103111245088.png)]

    comm.h

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PATHNAME "./"
    #define PROJ_ID  0x66
    #define SIZE 4097
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    client.cpp

    #include "./comm.h"
    int main()
    {
    
        key_t key = ftok(PATHNAME, PROJ_ID);
    
        if (key < 0)
        {
            perror("ftok failed");
            exit(1);
        }
        printf("key =%d\n", key);
    
        //挂接
        int shmid = shmget(key, SIZE, IPC_CREAT);
        if (shmid < 0)
        {
            perror("shmid failed");
            exit(1);
        }
        //进程挂接共享内存
        char *retp = (char *)shmat(shmid, NULL, 0);
        if (retp == NULL)
        {
            perror("shmat failed");
            exit(1);
        }
        //业务逻辑
        char ch = 'A';
        while (ch<'Z')
        {
            retp[ch - 'A'] = ch;
            ch++;
            retp[ch - 'A'] = 0; //放置\0
            sleep(1);
        }
    
        //用户层不需要删除共享内存,只需要与共享内存失去挂接关系即可
        int ret = shmdt(retp);
        if (ret < 0)
        {
            perror("shmdt failed");
            exit(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

    server.cpp

    #include "./comm.h"
    int main()
    {
    
        key_t key = ftok(PATHNAME, PROJ_ID);
        if (key < 0)
        {
            perror("ftok failed");
            exit(1);
        }
        // printf("key =%d\n", key);
        //umask(0);
        
        // int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL|0666);//8进制表示权限
        //创建全新的shm,如果和系统已经存在的ID冲突,就错误退出
        int shmid = shmget(key, SIZE, IPC_CREAT);//8进制表示权限
        if (shmid < 0)
        {
            perror("shmget failed");
            exit(1);
        }
        printf("shmid=%d\n",shmid);
        
        //进程挂接共享内存
        char *retp = (char *)shmat(shmid, NULL, 0);
        if (retp == NULL)
        {
            perror("shmat failed");
            exit(1);
        }
        //业务逻辑
    
        while(1)
        {
            printf("%s\n",retp);
            sleep(1);
        }
    
        // 共享内存的删除
        //sleep(5);
        //删除
        
        shmctl(shmid,IPC_RMID,NULL);
        printf("shmid: %d delete success\n",shmid);
        //sleep(5);
        
        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

    IPC资源的共性

    1. 每一个描述IPC资源的结构体的第一个成员都是ipc_perm,结构非常类似

    2. OS采用ipc_perm指针数组 (ipc_perm*arr)的方式,将每个IPC资源的首地址存储在这个数组中,当需要使用某个IPC资源时,通过强制转换的方式即

      (shmid_ds *)arr[i]->shm的属性来对IPC资源进行管理,这也就是为什么在用户层我们获得的shmid是0,1,2,3,4…

    3. 这样的设计,OS就能以统一的视角/方式去管理各种IPC资源

    在这里插入图片描述

    System V 消息队列(msq)

    1. 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
    2. 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
    3. 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
    #include 
    #include 
    #include 
    int semget(key_t key, int nsems, int semflg);
     int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    System V 信号量

    #include 
    #include 
    #include 
    int semget(key_t key, int nsems, int semflg);
    int semctl(int semid, int semnum, int cmd, ...);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    临界资源

    1. 凡是能够被多个执行流访问的资源就是临界资源,也就是多个执行流共享同一个资源
    2. 典型的就是屏幕,它能被多个进程使用。另外进程通信的介质:管道,共享内存,消息队列。信号量

    临界区

    进程代码很多,用来访问临界资源的代码称为临界区。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1YwstfkU-1667445179841)(./%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.assets/image-20221103104200299.png)]

    互斥

    在任意一个时刻,只能允许一个执行流进入临界资源,执行自己的临界区。

  • 相关阅读:
    dubbo功能非常完善,好用的功能你又知道哪些?
    详解Python中的序列化(简单易懂版)
    解决报错TypeError:unsupported operand type(s) for +: ‘NoneType‘ and ‘str‘
    QT creator与VS2019 QT加载模块方法
    2、计划任务不显示UI的问题
    Windows取证——登录过的用户名、新建的用户名和访问的网址文件(墨者学院)
    SRTT-110VDC-4H-C时间继电器
    JAVA xml格式转为java对象
    Fluent Facede Pattern(外观模式)
    10、MyBatis的缓存
  • 原文地址:https://blog.csdn.net/qq_55439426/article/details/127666090