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

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

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


文章目录

  • 1.啥是共享内存?
  • 2.相关接口
  • 2.1 ftok
  • 2.2 shmget
  • 2.3 shmat/shmdt
  • 2.4 shmctl
  • 2.5 ipcs命令
  • 消息队列/信号量的接口
  • ipcrm
  • 3.使用
  • 3.1 创建并获取
  • File exists
  • 设置权限值
  • 3.2 挂接/取消挂接
  • 取消/删除
  • 3.3 写入内容
  • 共享内存没有访问控制
  • 通过管道进行共享内存的控制
  • 运行结果
  • 4.相关概念
  • 4.0 临界资源
  • 4.1 信号量
  • 原子性的说明
  • 改变信号量的值
  • 4.2 扩展 mmap
  • 结语


1.啥是共享内存?

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

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

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

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

这里就出现了一个问题:

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

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

  • 这就出现了问题!

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

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


2.相关接口

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

2.1 ftok

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

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

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

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

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

2.2 shmget

shmget - allocates a System V shared memory segment

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数分别为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.

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

命令行键入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 */
};

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

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

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


2.3 shmat/shmdt

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

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

shmat, shmdt - System V shared memory operations

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

一共有两个函数,分别为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.

2.4 shmctl

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

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

其中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

shmid_ds的基本结构如下

struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

以删除为例,其操作如下

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

2.5 ipcs命令

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

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

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

[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

这里的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

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


消息队列/信号量的接口

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

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

//消息队列相关接口
msgget //获取
msgctl //操作
msgsnd //发送信息
msgrcv 

//信号量
semget
semctl
semop

ipcrm

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

ipcrm -m shmid #删除共享内存

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

此时执行删除,在共享内存的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

相比之下,如果不执行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

结论:使用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;
}

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

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

通过上面提到的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

设置权限值

默认情况下,我们创建的共享内存的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;
}

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

[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

3.2 挂接/取消挂接

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

因为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

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

取消/删除

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

shmdt(str);//取消挂接

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

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

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;
}

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

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

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

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

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

共享内存没有访问控制

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

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

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

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

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

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

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

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

以下是完整代码👇

//mykey.hpp
#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
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;
}

运行结果

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

【Linux】进程间通信 | 共享内存 | 信号量_服务器_05

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

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


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覆盖,出现了变量不统一的情况

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

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

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

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

改变信号量的值

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

功能: 操作信号量,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.

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

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) */
};

它们的第一个成员都是一个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 */
};

而内核中对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数据结构的指针
};

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

struct ipc_id_ary
{
        int size;
        struct kern_ipc_perm *p[0];//指针数组
};

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

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

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

(strcut shmid_ds*)

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

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

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

这是一种切片的思想

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

【Linux】进程间通信 | 共享内存 | 信号量_运维_09

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


结语

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

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

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

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