• 【Linux】进程间通信 | 共享内存 | 信号量


    上篇Linux的博客是有关管道的,今日就让我们继续康康进程间通信的另外一种方法:共享内存

    完整代码详见我的gitee仓库 👇

    https://gitee.com/ewait/raspberry-practice/tree/master/code/22-11-12_systemV

    1.啥是共享内存

    进程间通信的基本方式,就是让两个进程看到同一份资源。

    共享内存的方式,通过系统接口开辟一段内存,再让多个进程去访问这块内存,就能同时看到一份资源。

    image-20221112090042941

    这里贴出之前动态库博客中的图,共享内存的方式和该图展示的方式类似。进程需要调用系统接口,将已经开辟好的共享内存映射到自己的页表中,以实现访问。

    这里就出现了一个问题:

    • 操作系统的接口怎么知道进程要的是那一块共享内存?即共享内存是怎么标识的?

    要知道,之前我们打开文件、开辟管道等等,都是具有唯一的文件路径来标识文件的。如果按以前的想法:打开文件->系统返回文件的文件描述符,共享内存则应该是开辟共享内存->系统返回共享内存的编号

    • 这就出现了问题!

    假设进程A开辟了一段共享内存,系统返回了编号123,那么进程A要怎么让其他想使用这块共享内存进行通信的进程,知道它开辟的共享内存编号是123呢?总不能开个管道告诉它吧?那岂不是多此一举😂

    QQ图片20220502222002

    所以,共享内存的编号其实和命名管道一样,是由用户手动在代码中指定的。只要进程使用这个编号去获取共享内存,他们就能获取到同一份!


    2.相关接口

    说完了基本概念,现在让我们来康康它的使用

    2.1 ftok

    ftok - convert a pathname and a project identifier to a System V IPC key

    #include 
    #include 
    key_t ftok(const char *pathname, int proj_id);
    
    • 1
    • 2
    • 3

    前面提到了,共享内存的key是我们自己指定的。Linux系统给定了ftok接口,将用户提供的pathname工作路径,以及proj_id项目编号转换为一个共享内存的key(其实就是int类型)

    image-20221112132307366

    只要我们的工作路径和项目编号传的是一样的,那么它返回的key就是一样的!

    2.2 shmget

    shmget - allocates a System V shared memory segment

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

    参数分别为key值,共享内存的大小,以及创建共享内存的方式。

    key值需要通过 ftok函数获取;

    其中共享内存的大小最好设置为4kb的整数倍,因为操作系统IO的基本单位是4KB。如果你申请了不是4的整数倍的字节,比如15个字节,其还是会申请16个字节(4个页)交给你,而其中有1kb的内存你是无法使用的,即造成了内存浪费😥

    创建共享内存的shmflg:

    • IPC_CREAT:创建共享内存。如果存在则获取,如果不存在则创建后获取
    • IPC_EXCL:必须配合IPC_CREAT使用,如果不存在指定的共享内存,就进行创建;如果该共享内存存在,则出错返回(即保证获取到的共享内存一定是当前进程创建的,是一个新的共享内存)

    返回值是一个共享内存的标识符

    RETURN VALUE
           On success, a valid shared memory identifier is returned.  On errir, -1 is returned, and errno is set to indicate the error.
    
    • 1
    • 2

    这些工作都是操作系统做的。其内核中有专门的管理单元来判断一个共享内存是否存在,以及何时被创建、被使用、被什么进程绑定等等…

    命令行键入man shmctl,可以看到下面的内核结构

    struct shmid_ds {
        struct ipc_perm shm_perm;    /* Ownership and permissions */
        size_t          shm_segsz;   /* Size of segment (bytes) */
        time_t          shm_atime;   /* Last attach time */
        time_t          shm_dtime;   /* Last detach time */
        time_t          shm_ctime;   /* Last change time */
        pid_t           shm_cpid;    /* PID of creator */
        pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        shmatt_t        shm_nattch;  /* No. of current attaches */
        ...
    };
    
    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

    共享内存要被管理,其内核结构中一定有一个唯一的key值来标识该共享内存,即和文件的inode一样

    key_t     __key; //共享内存的唯一标识符,由用户在shmget中提供
    
    • 1

    关于key为何要让用户提供,已经在上面做出过解释👉 回顾一下


    2.3 shmat/shmdt

    at其实是attach绑定的缩写,这个接口的作用是将一个共享内存和我们当前的进程绑定。

    其实就是将这个共享内存映射到进程的页表中(堆栈之间)

    shmat, shmdt - System V shared memory operations

    #include 
    #include 
    
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    int shmdt(const void *shmaddr);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一共有两个函数,分别为at和dt,用于绑定/解绑共享内存

    shmat的三个参数如下

    • shmid:为shmget的返回值
    • shmaddr:指定共享内存连接到当前进程中的地址位置。通常为空,表示让系统来选择共享内存的地址。
    • shmflg:如果指定了SHM_RDONLY位,则以只读方式连接此段;否则以读写的方式连接此段;通常设置为0

    调用成功的时候,返回指向共享内存第一个字节的指针;出错返回-1

    • shmdt的参数为shmat正确调用时的返回值

    以下是man手册中对这两个函数返回值的描述👇

    RETURN VALUE
           On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set  to
           indicate the cause of the error.
    
           On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.4 shmctl

    这个函数可以用于操作我们的共享内存

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

    其中cmd的参数有下面几种

    • IPC_RMID 删除该共享内存

    • IPC_STATshmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值

    • IPC_SET 如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

    最后一个buf参数是一个指向shmid_ds结构的指针,一般设为NULL

    The buf argument is a pointer to a shmid_ds structure
    
    • 1

    shmid_ds的基本结构如下

    struct shmid_ds
    {
        uid_t shm_perm.uid;
        uid_t shm_perm.gid;
        mode_t shm_perm.mode;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以删除为例,其操作如下

    shmctl(shmid, IPC_RMID, NULL);//删除shmid的共享内存
    
    • 1

    2.5 ipcs命令

    先来康康几个ipcs命令的选项,其中我们要用到的是-m查看共享内存

    ipcs -c #查看消息队列/共享内存/信号量
    ipcs -s #单独查看信号量
    ipcs -q #单独查看消息队列
    ipcs -m #单独查看共享内存
    
    • 1
    • 2
    • 3
    • 4

    执行了之后,会列出当前操作系统中开辟的共享内存,以及它们的基本信息

    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m 
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 1          muxue      0          1024       0    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里的key和我们使用ftok获取到的key值是一样的,只不过我们打印的时候是十进制,操作系统列出来的为十六进制

    我们可以使用ipcrm -m 共享内存的shmid来删除共享内存

    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 1          muxue      0          1024       0                       
    
    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcrm -m 1
    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    可以看到我们自己创建的共享内存已经被删除了


    消息队列/信号量的接口

    消息队列和信号量的接口和共享内存很相似

    消息队列用的不多,信号量的难度很高!😂

    //消息队列相关接口
    msgget //获取
    msgctl //操作
    msgsnd //发送信息
    msgrcv 
    
    //信号量
    semget
    semctl
    semop
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ipcrm

    这个命令可以用与删除ipc资源,包括共享内存

    ipcrm -m shmid #删除共享内存
    
    • 1

    但是,当我们尝试用该命令删除一个正在被使用的共享内存时,它并不会被立即删除(立即删除会影响进程运行)

    此时执行删除,在共享内存的status列会出现dest;观察结果,当进程结束的时候,这个共享内存会被直接删除(进程内部并没有调用shmctl接口)

    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 21         muxue      666        1024       2                       
    
    [muxue@bt-7274:~/git]$ ipcrm -m 21
    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x00000000 21         muxue      666        1024       2          dest         
    
    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    相比之下,如果不执行ipcrm命令+进程内部不调用shmctl接口,这个共享内存就会一直存在

    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 22         muxue      666        1024       0                       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结论:使用ipcrm -m命令删除共享内存之后,其共享内存不一定会立即释放。如果有进程关联了该共享内存,则会在进程去关联之后释放

    3.使用

    3.1 创建并获取

    //头文件实在太多,为了博客篇幅,这里省略了
    #define NUM 1024
    #define PROJ_ID 0x20
    #define PATH_NAME "/home/muxue/git/linux/code/22-11-12_systemV"
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if(key < 0)
        {
            cerr <<"ftok: "<< strerror(errno) << endl;
            exit(1);//key获取错误直接退出程序
        }
        return key;
    }
    
    int main()
    {
        key_t key = CreateKey();
    
        int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
    
        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

    File exists

    这里会发现,第一次运行代码的时候,程序成功获取了共享内存;但是第二次运行的时候,却报错说File exists(文件存在)

    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
    shmget: 1
    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
    shmget err: File exists
    
    • 1
    • 2
    • 3
    • 4

    这是因为共享内存的声明周期是随内核的。即只要这个共享内存不被删除,他就会一直存在,直到内核因为某种原因释放掉它,亦或者操作系统关机

    通过上面提到的ipcrm -m shmid 命令删除共享内存,才能重新运行代码获取新的共享内存

    为了避免这个问题,应该在进程结束后使用shmctl接口删除共享内存

    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
    shmget success: 2
    [muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 2          muxue      0          1024       0                       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    设置权限值

    默认情况下,我们创建的共享内存的perms是0,代表没有用户能访问这个共享内存。所以在创建的时候,我们需要在flag里面直接或上这个共享内存的权限值

    代码如下👇

    int main()
    {
        key_t key = CreateKey();
    
        int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
    
        sleep(5);
    
        shmctl(id,IPC_RMID,nullptr);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这时候创建的共享内存就有正确的权限值了

    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 4          muxue      666        1024       0    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.2 挂接/取消挂接

    //关联共享内存
    char *str = (char*)shmat(id, nullptr, 0);
    
    • 1
    • 2

    因为shmat函数的返回值是一个void*指针,我们可以以使用malloc一样的方式使来挂接共享内存。随后对这个内存的操作就是正常的指针操作了!

    同样的,另外一个进程也需要用同样的方式挂接共享内存,才能读取到相同的数据

    [muxue@bt-7274:~/git]$ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x20011ac8 4          muxue      666        1024       1   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    挂接成功后,可以发现nattch的值从0变为1

    取消/删除

    取消挂接的方式很简单,直接把shmat的返回值传入即可

    shmdt(str);//取消挂接
    
    • 1

    如果是服务端,则还需要在取消挂接之后,删除共享内存。避免下次程序运行的时候,无法通过key获取到新的共享内存

    shmctl(id,IPC_RMID,nullptr);//删除共享内存
    
    • 1

    3.3 写入内容

    因为共享内存本质就是一个内存,其和malloc出来的内存都是一样的,直接使用即可

    这里还是用一个服务端和一个客户端来进行演示

    //server.cpp
    #include "Mykey.hpp"
    
    int main()
    {
        //获取key值
        key_t key = CreateKey();
        //创建共享内存
        int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
    
        sleep(2);
        //关联共享内存
        char *str = (char*)shmat(id, nullptr, 0);
        printf("[server] shmat success\n");
        //读取数据,sleep(1)
        int i=0;
        while(i<=40)
        {
            printf("[%03d] %s\n",i,str);
            i++;
            sleep(1);
        }
    
        //去关联
        shmdt(str);//shmat的返回值
        printf("[server] shmdt(str)\n");
        //删除共享内存
        shmctl(id,IPC_RMID,nullptr);
        printf("[server] exit\n");
        return 0;
    }
    
    //client.cpp
    #include "Mykey.hpp"
    
    int main()
    {
        //获取key值
        key_t key = CreateKey();
        //获取共享内存
        int id = shmget(key, NUM, IPC_CREAT);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
    
        sleep(2);
        //关联共享内存
        char *str = (char*)shmat(id, nullptr, 0);
        printf("[client] shmat success\n");
        //写入数据
        int i=0;
        while(i<26)
        {
            char base = 'A';
            str[i] = base+i;
            str[i+1] = '\0';
            printf("write times: %02d\n",i);
            i++;
            sleep(1);
        }
    
    
        //去关联
        shmdt(str);//shmat的返回值
        printf("[client] shmdt & exit\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
    • 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

    跑起来之后,客户端向共享内存中写入数据(注意控制\0)服务端进行读取。这便实现了我们进程之间的通信

    image-20221113091741600

    不过我们发现,客户端已经停止写入之后,服务端还是在不停的读取。如果我们不控制while循环的话,其会一直这么读取下去

    image-20221113091847579

    这便牵扯出共享内存的一个特性了

    共享内存没有访问控制

    在管道的博客中提到,管道是有访问控制的进程通信方式,写端没有写入数据的时候,读端会在read中进行等待

    而共享内存因为我们是直接像操作一个malloc出来的空间一样访问,没有使用任何系统接口(相比之下管道需要使用read/write)所以操作系统没有办法帮我们进行访问控制

    也正是因为没有等待,共享内存是进程中通信中最快的一种方式

    通过管道进行共享内存的控制

    既然共享内存没有访问控制,那么我们可以利用管道来让控制共享内存的读写

    • 写端写完后,将完成信号写入管道,由读端读取
    • 读端从管道中获取到信号后,访问共享内存读出内容
    • 如果写端没有写好,读端就会在管道read内部等待

    你可能会说,那为何不直接用管道通信呢?

    • 管道仅作访问控制,只需要一个int乃至一个char类型即可;
    • 相比直接管道通信,内存的方式更好控制(毕竟使用内存的方式和使用指针一样,管道还需要文件操作)
    • 读取很长一串数据的时候,共享内存的速度优势能体现出来

    以下是完整代码👇

    //mykey.hpp
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define NUM 1024
    #define PROJ_ID 0x20
    #define PATH_NAME "/home/muxue/git/linux/code/22-11-12_systemV"
    #define FIFO_FILE "sc.pipe"
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if(key < 0)
        {
            cerr <<"ftok: "<< strerror(errno) << endl;
            exit(1);//key获取错误直接退出程序
        }
        return key;
    }
    
    void CreateFifo()
    {
        umask(0);
        if(mkfifo(FIFO_FILE, 0666) < 0)
        {
            cerr << "fifo: " << strerror(errno) << endl;
            exit(2);
        }
    }
    //打开管道文件
    int Open(int flags)
    {
        return open(FIFO_FILE, flags);
    }
    //让读端通过管道等待
    ssize_t Wait(int fd)
    {
        char val = 0;
        //如果写端没有写入,其就会在read中等待
        ssize_t s = read(fd, &val, sizeof(val));
        return s;
    }
    //发送完成信息
    int Signal(int fd)
    {
        char sig = 'g';
        write(fd, &sig, sizeof(sig));
    }
    
    //server.cpp
    #include "Mykey.hpp"
    
    int main()
    {
        //创建管道
        CreateFifo();
        //获取key值
        key_t key = CreateKey();
        //创建共享内存
        int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
        //获取管道
        int fd = Open(O_RDONLY);
        cout << "open fifo success: " << fd << endl;
        sleep(2);
        //关联共享内存
        char *str = (char*)shmat(id, nullptr, 0);
        printf("[server] shmat success\n");
        //读取数据
        int i=0;
        while(i<=40)
        {
            ssize_t ret = Wait(fd);//通过管道等待
            if(ret!=0)
            {
                printf("[%03d] %s\n",i,str);
                i++;
                sleep(1);
            }
            else
            {
                cout<<"[server] wait finish, break" << endl;
                break;
            }
        }
    
        //去关联
        shmdt(str);//shmat的返回值
        printf("[server] shmdt(str)\n");
        //删除共享内存
        shmctl(id,IPC_RMID,nullptr);
        close(fd);
        unlink(FIFO_FILE);
        printf("[server] exit\n");
        return 0;
    }
    
    //client.cpp
    #include "Mykey.hpp"
    
    int main()
    {
        //获取key值
        key_t key = CreateKey();
        //获取共享内存
        int id = shmget(key, NUM, IPC_CREAT);
        if(id<0)
        {
            cerr<< "shmget err: " << strerror(errno) << endl; 
            return 1;
        }
        cout << "shmget success: " << id << endl;
    
        //获取管道
        int fd = Open(O_WRONLY);
        cout << "open fifo success: " << fd << endl;
        sleep(2);
        //关联共享内存
        char *str = (char*)shmat(id, nullptr, 0);
        printf("[client] shmat success\n");
        //写入数据
        int i=0;
        while(i<26)
        {
            char base = 'A';
            str[i] = base+i;
            str[i+1] = '\0';
            printf("write times: %02d\n",i);
            i++;
            Signal(fd);
            sleep(1);
        }
    
    
        //去关联
        shmdt(str);//shmat的返回值
        printf("[client] shmdt & exit\n");
        close(fd);
        printf("[client] close fifo\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
    • 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
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160

    运行结果

    管道控制了之后,当客户端退出的时候,管道也不会继续读取,而是在read内等待

    image-20221113104949379

    如果客户端最后关闭了管道的写段,服务器端就会直接退出。这样我们就实现了通过管道控制共享内存的读写👍

    image-20221113111731418


    4.相关概念

    4.0 临界资源

    能被多个进程看到的资源,被称为临界资源

    如果不对临界资源进行访问控制,进程对该资源的访问就是乱序的(比如父子进程向显示器打印内容)可能会因为数据交叉导致乱码、数据不可用等情况

    以此可见,显示器、管道、共享内存都是临界资源

    • 管道是有访问控制的临界资源

    进程访问临界资源的代码,称为临界区

    • 一个进程中,并不是所有的代码都在访问临界资源。如管道中,其实只有read/write接口在访问临界资源

    互斥:任何时刻,只允许一个进程访问临界资源

    原子性:一件事情只有做完/没做两种状态,没有中间状态

    下面对信号量的概念进行讲解~只用基本理解即可

    4.1 信号量

    信号量是对临界资源的控制方式之一,其本质是一个计数器

    • 信号量保证不会有多余的进程连接到这份临界资源
    • 还需要保证每一个进程的能够访问到临界资源的不同位置(根据上层业务决定)

    信号量根据情况的不同分为两种:

    • 二元信号量(互斥状态,当进程使用的时候为1,没有进程使用的时候为0)
    • 多元信号量(常规)

    如果一个进程想访问由信号量控制的临界资源,必须先申请信号量。申请成功,就一定能访问到这个临界资源中的一部分(或者全部)

    原子性的说明

    先来想想,我们对一个变量+1/-1需要做什么工作:

    • 将这个变量从内存中拿到CPU的寄存器中
    • 在寄存器中完成加减操作
    • 放回内存

    这其中是有很多个中间状态的,设该变量初始值为100

    • 假设一个进程A拿走了这个变量,放入CPU的寄存器
    • 另外一个进程B也来拿走了这个变量
    • 此时A和B拿到的都是100
    • A对该变量进行了循环--操作,最终该变量变成了50,将其放回内存
    • B对该变量-1,将其放回内存
    • 最终导致A对变量的操作被B覆盖,出现了变量不统一的情况

    而我们的信号量为了保证能够正确的控制进程的访问,其就必须维护自身的原子性!不能有中间状态

    QQ图片20220424132543

    说人话就是,如果进程A在访问信号量,进程B来了,信号量应该拒绝B的访问,直到A访问结束。不能让B中途插入访问,从而导致可能的数据不统一

    共享内存同样可以通过信号量进行访问控制

    改变信号量的值

    int semop(int semid, struct sembuf *sops, size_t nops);
    
    • 1

    功能: 操作信号量,P V 操作

    参数:

    • semid 为信号量集的标识符;
    • sops 指向进行操作的结构体数组的首地址;
    • nsops 指出将要进行操作的信号的个数;

    返回值: 成功返回0,出错返回-1

    RETURN VALUE
           If successful semop() and semtimedop() return 0; otherwise they return -1 with errno indicating the error.
    
    • 1
    • 2

    因为信号量的操作比较复杂,所以这里就木有演示😝

    4.2 扩展 mmap

    这部分仅供参考,可能有错误😥部分资料参考

    前面贴出过IPC资源的内核结构,它们都有一个共同的特点:第一个成员都相同

    struct shmid_ds {
        struct ipc_perm shm_perm;    /* Ownership and permissions */
        size_t          shm_segsz;   /* Size of segment (bytes) */
        time_t          shm_atime;   /* Last attach time */
        time_t          shm_dtime;   /* Last detach time */
        time_t          shm_ctime;   /* Last change time */
        pid_t           shm_cpid;    /* PID of creator */
        pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        shmatt_t        shm_nattch;  /* No. of current attaches */
        ...
    };
    
    struct semid_ds {
        struct ipc_perm sem_perm;  /* Ownership and permissions */
        time_t          sem_otime; /* Last semop time */
        time_t          sem_ctime; /* Last change time */
        unsigned long   sem_nsems; /* No. of semaphores in set */
    };
    
    struct msqid_ds {
        struct ipc_perm msg_perm;     /* Ownership and permissions */
        time_t          msg_stime;    /* Time of last msgsnd(2) */
        time_t          msg_rtime;    /* Time of last msgrcv(2) */
        time_t          msg_ctime;    /* Time of last change */
        unsigned long   __msg_cbytes; /* Current number of bytes in
                                                    queue (nonstandard) */
        msgqnum_t       msg_qnum;     /* Current number of messages
                                                    in queue */
        msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                    allowed in queue */
        pid_t           msg_lspid;    /* PID of last msgsnd(2) */
        pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
    };
    
    • 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

    它们的第一个成员都是一个struct ipc_perm,其中包含了一个信号量的基本信息

    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

    而内核中对IPC资源的管理,是通过一个数组进行的。我们所获取的shmid,和文件描述符一样,都是一个数组的下标

    其中我在测试的时候,便发现了一点:我们每一次获取的新的共享内存,它的编号都会+1,而不像文件描述符一样,提供第一个没有被使用的下标

    struct ipc_ids {
        int in_use;//说明已分配的资源个数
        int max_id;//在使用的最大的位置索引
        unsigned short seq;//下一个分配的位置序列号
        unsigned short seq_max;//最大位置使用序列
        struct semaphore sem; //保护 ipc_ids的信号量
        struct ipc_id_ary nullentry;//如果IPC资源无法初始化,则entries字段指向伪数据结构
        struct ipc_id_ary* entries;//指向资源ipc_id_ary数据结构的指针
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在内核中,struct ipc_id_ary* entries是一个指向所有ipc_perm指针数组。其能够通过该数组找到我们对于id(下标)的资源,对其进行访问

    struct ipc_id_ary
    {
            int size;
            struct kern_ipc_perm *p[0];//指针数组
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20221113163918931

    那你可能想问了,这里只是第一个元素啊?那如果我想访问shmid_ds结构的其他成员,岂不是没有办法访问了?

    要是这么想,就还是太年轻了😂

    (strcut shmid_ds*)
    
    • 1

    我们只需要对这个指针进行强转,就能直接访问其他成员!

    这是因为:C语言中,结构体第一个元素的地址,和结构体整体的地址是一样的!

    指针的类型会限制这个指针访问元素的能力,只要我们进行强转,其就能直接访问父结构体的其他成员!

    这是一种切片的思想

    用这种办法,可以用统一的规则在内核中管理不同的IPC资源,没有必要再为每一个IPC资源建立一个单独的数组来管理。

    QQ图片20220419103136

    不得不说,linus大佬是真的牛逼!


    结语

    关于共享内存的操作到这里就OVER了!

    最后还了解了一些内核设计上的小妙招,不得不说,真的牛批~

    如果本文有什么问题,欢迎在评论区提出

    QQ图片20220527185356

  • 相关阅读:
    百度率先在元宇宙举办大会,李彦宏:中国迎来AI黄金十年
    面试官:Spring Boot 是否可以使用 XML 配置?如果可以的话怎么配置
    二进制逻辑运算和基本门电路
    leetcode day12 相同的树
    公司监控员工电脑用什么软件?应该怎么选?
    MySQL之索引
    新闻稿写作的具体要求是什么?
    算法数据结构体系学习 第十一节
    改造 Kubernetes 自定义调度器
    【Windows】手动配置静态IPv4
  • 原文地址:https://blog.csdn.net/muxuen/article/details/127834035