零基础Linux_18(进程间通信)共享内存+消息队列+信号量

目录

1. 共享内存

1.1 共享内存概念

1.2 系统函数shmget

1.2.1 key值和ftok

1.2.2 size+shmflg+返回值

1.3 系统调用shmctl

1.4 系统调用shmat和shmdt

1.5 共享内存进程间通信前期代码

1.6 共享内存进程间通信

2. 消息队列(了解)

3. 信号量(了解)

4. 笔试选择题

答案及解析

本篇完。


1. 共享内存

前一篇:

system V是一种进程间通信策略,它包括共享内存,消息队列以及信号量。


1.1 共享内存概念

        共享内存区是最快的IPC(进程间通信)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。共享内存也是由操作系统维护的共享资源。

 两个进程的PCB各自维护着一个进程地址空间。当两个进程要进行通信时:

  • 操作系统在内存中开辟一个内存块。
  • 通过两个进程的页表,将内存中的内存块映射到两个进程的进程地址空间中。
  • 此时两个进程便建立了连接。
  • 进行通信时,两个进程只需要访问自己的进程地址空间即可,操作系统会通过页表访问内存中的内存块。

所以说,共享内存就是让不同的进程,看到同一块内存块。

在维持通信关系中,还涉及到几个概念:

  • 挂接:将内存中创建好的内存块映射到进程的地址空间中。
  • 去关联:不想通信时,取消进程和内存的映射关系。

注意: 去关联后,共享内存仍然存在,只是和去关联的进程没有了映射关系。


1.2 系统函数shmget

该系统调用接口就是用来在内存中创建共享内存的。 

1.2.1 key值和ftok

关于共享内存,首先需要理解几件事情:

  • 共享内存是专门设计的IPC方式,是用来进行进程间通信的。
  • 所有想通信的进程都可以用共享内存的方式来通信。
  • 所以操作系统中注定不止一块共享内存。
  • 共享内存多了,就需要有一个标识,让要通信的进程找到正确的共享内存。

key值就是共享内存的标识,让想要通信的进程双方看到同一块公共资源。

        系统中既然存在很多个共享内存,操作系统势必要将它们管理起来,管理也是使用先描述再组织的方式。

管理共享内存并不是在管理内存块本身,而是在管理共享内存对应的结构体:

struct shm
{
	key_t key;
	size_t size;
	// ...
}

结构体类似与上面代码,包含许多共享内存的属性,最重要的是,结构体中有key值。

        每创建一个共享内存,就会创建一个结构体对象,并且赋一个不同的key值。

所以这个key值就代表着一块唯一的共享内存。

怎么保证这个key值是唯一的呢?从shmget函数的声明中可以看到,这个key值是我们传给操作系统的,也就是用我们传的key值来标定共享内存。

这就需要用到另一个函数ftok(),来生成一个独一无二的key值。

  • pathname:文件路径名,可以随意写,一般我们都写成当前路径"."。
  • proj_id:项目ID,同样可以自定义,但是不能为0。
  • 返回值:独一无二的key值。

        该函数会根据我们传的路径名和项目id值生成一个key值,具体实现是通过一些算法实现的,我们不需要在意,只需要得到key值就行。所以,在开辟共享内存之前,必须先使用ftok函数来生成一个独一无二的key值,这样才能保证我们内存块的标识是唯一的。当两个进程通过key值和共享内存挂接起来后,就可以进行通信了。

结论:key值就是用来标识共享内存的唯一性的。


1.2.2 size+shmflg+返回值

        size是用来指定开辟的共享内存是多大的,以字节为单位。 一般指定的大小是4KB的整数倍。也可以是任意值。

操作系统在开辟共享内存的时候是以4KB为单位的。每次开辟的共享内存,最小也是4KB的。

        假设我们指定4097字节大小的共享内存,但是在内存中实际开辟的共享内存是2*4KB的。但是在使用的时候只能使用4097字节的空间,剩下的空间用户无法使用,操作系统也不会用,就浪费掉了。所以,即使用不了那么大的空间,我们也要指定4KB的整数倍。

        shmflg是一个标志位,和之前使用的open用法相似,也是一个int类型的数据,根据比特位不同,用法也不同。

常用的两个选项:

  • IPC_CREAT:创建共享内存,如果不存在,创建新的,如果存在,获取相关信息。
  • IPC_EXCL:无法单独使用,必须与其他标志使用。
    IPC_CREAT | IPC_EXCL:创建共享内存,如果不存在,则创建,如果存在,错误返回。

        IPC_CREAT | IPC_EXCL是专门给用户使用的,就是为了保证创建的共享内存是一块新的内存块

返回值

  • 创建成功返回共享内存的标识符,注意不是key值。
  • 创建失败,返回-1。

        我们一般定义shmid接收返回值,shmid也是连续的小整数,它和文件描述符fd一样,也是让用户使用的。但是它和文件描述符代表的意义又不一样。这里创建linux_18目录写个测试代码:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h> // ftok
#include <sys/ipc.h> // shmget ftok
#include <sys/shm.h> // shmget
using namespace std;
#define PATH_NAME "/home/rtx2"
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍,多1字节也会多创建一页

int main()
{
    key_t key1 = ftok(PATH_NAME, 1);
    key_t key2 = ftok(PATH_NAME, 2);
    key_t key3 = ftok(PATH_NAME, 3);
    key_t key4 = ftok(PATH_NAME, 4);
    key_t key5 = ftok(PATH_NAME, 5);

    printf("key1: 0x%x\n", key1);
    printf("key2: 0x%x\n", key2);
    printf("key3: 0x%x\n", key3);
    printf("key4: 0x%x\n", key4);
    printf("key5: 0x%x\n", key5);

    int shmid1 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid2 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid3 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid4 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid5 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);

    cout << "shmid1: " << shmid1 << endl;
    cout << "shmid2: " << shmid2 << endl;
    cout << "shmid3: " << shmid3 << endl;
    cout << "shmid4: " << shmid4 << endl;
    cout << "shmid5: " << shmid5 << endl;

    return 0;
}

编译运行:

(因为多创建了几次所以从15开始了)

        为什么返回的不是key值,而是shmid呢?key值也是唯一的啊。在设计上是可以的,都是唯一的数字,用户在语言层和在系统使用的标识相同是没有问题的。

        如果写好了一份代码,代码中使用的是key值。当系统的底层发生了变化,key值也会变化。但是代码中的key值没有变化,此时继续使用原理的代码就会出错。所以说,用户层使用shmid而不是key值是为了让用户层和系统层解耦。

结论:shmid是供用户使用的共享内存标识符。shmget系统调用就是用来让通信双方获取同一块共享内存的。


1.3 系统调用shmctl

刚刚我们使用shmget时创建了五个共享内存,输入 ipcs -m

此时,进程早已经结束了,但是共享内存还是存在,没有随进程的结束而消失。

所以说,共享内存的生命周期随内核,不随进程

如果不想要这几个共享内存了,怎么删除呢?同样有指令:

  • 指令:ipcrm -m shimd
  • 功能:删除指定shimd标识的共享内存。

        此时所有的共享内存就被删除了,但是需要我们手动的一个个去删除。由于指令也是shell上运行的进程,也是属于用户层,所以操作共享内存时,使用的时shmid,而不是key值。用命令行的形式未免也太麻烦了,所以有系统调用shmctl也可以用来删除共享内存,而且是自动的。

  • shmid:获取共享内存后返回的标识符。
  • cmd:指定控制共享内存的方式。
  • buf:描述共享内存的数据结构指针。

cmd删除是最常用的选项:IPC_RMID

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h> // ftok
#include <sys/ipc.h> // shmget ftok
#include <sys/shm.h> // shmget
using namespace std;
#define PATH_NAME "/home/rtx2"
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍,多1字节也会多创建一页

int main()
{
    key_t key1 = ftok(PATH_NAME, 0x11);
    key_t key2 = ftok(PATH_NAME, 0x22);
    key_t key3 = ftok(PATH_NAME, 0x33);
    key_t key4 = ftok(PATH_NAME, 0x44);
    key_t key5 = ftok(PATH_NAME, 0x55);

    printf("key1: 0x%x\n", key1);
    printf("key2: 0x%x\n", key2);
    printf("key3: 0x%x\n", key3);
    printf("key4: 0x%x\n", key4);
    printf("key5: 0x%x\n", key5);

    int shmid1 = shmget(key1, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid2 = shmget(key2, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid3 = shmget(key3, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid4 = shmget(key4, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    int shmid5 = shmget(key5, SHM_SIZE, IPC_CREAT | IPC_EXCL);

    cout << "shmid1: " << shmid1 << endl;
    cout << "shmid2: " << shmid2 << endl;
    cout << "shmid3: " << shmid3 << endl;
    cout << "shmid4: " << shmid4 << endl;
    cout << "shmid5: " << shmid5 << endl;

    shmctl(shmid1, IPC_RMID, nullptr);
    shmctl(shmid2, IPC_RMID, nullptr);
    shmctl(shmid3, IPC_RMID, nullptr);
    shmctl(shmid4, IPC_RMID, nullptr);
    shmctl(shmid5, IPC_RMID, nullptr);

    return 0;
}

        key值和shmid也打印了,说明共享内存创建成功了,但是使用ipcs查看时,发现什么都没有,证明shmctl将创建的共享内存删除了。


1.4 系统调用shmat和shmdt

shmat是让进程和共享内存attach(挂接):

  • shmid:创建共享内存后返回的标识符。
  • shmaddr:指定共享内存映射到进程地址空间中的地址,可设置成nullptr,让系统自动设置。
  • shmflg:不用管它是什么,直接给0。
  • 返回值:共享内存映射到进程地址空间中的地址。不成功返回-1,但是是void*类型的。

如果要返回char*就要这样写:

char* shmaddr = (char*)shmat(shmid, nullptr, 0);

shmdt是让进程和共享内存去(取消)关联。发现和上面在同一手册。

  • shmaddr:要去关联的共享内存映射在进程地址空间中的地址。一般是shamddr的返回值。
  • 返回值:成功返回0,不成功返回-1。

nattch值表示和这块共享内存挂接的进程数量。

上面是还没有进程和其关联,所以是0,下面我们直接放一个类似上篇模拟的代码。


1.5 共享内存进程间通信前期代码

使用共享内存的方式,实现进程server和client之间的通信:

  • server负责创建共享内存,删除共享内存
  • server从共享内存中读数据。
  • client向共享内存中写数据。

shmget,shmctl,shmat,shmdt四个系统调用通信双方都会使用,而且会对返回值进行严格判断,所以我们将这些共用的代码放在一个头文件中。

关于后缀:(我们以后会演示.cc后缀和.xx,这和.cpp一样的,看到别人用时认识就可以了,这段可不看)

        在一开始,C++程序的源文件后缀名也是“.c”和“.h”,这与C语言程序源文件的后缀名完全一样。这就带来了一些问题,

        其中一个比较显著的问题就是编译器无法轻松的区分某个源文件里的程序究竟是使用C++编写的,还是C语言编写的。

        因此,后来C++程序源文件的后缀名不再使用“.c”了,有些使用“.C”后缀名以区别于C语言程序,还有些C++源文件则使用“.c++”,“.cc”,“.cxx”作为后缀名。

        不过,由于“.C”与“.c++”后缀名的适用性不够广,比如有的编译器不区分后缀名的大小写,以及有些系统不支持文件名使用“+”符号,这两种后缀名的C++源文件越来越少了。

        Dos 和 Windows 系统的编译器偏向使用“cpp”(c plus plus, c++)作为 C++ 源文件的后缀名,考虑到 Windows 系统的市场占有率极高,因此为了统一性,以“.cpp”作为源文件后缀名的C++程序项目相当多。

        再来看头文件,基本上和源文件的历史一致,曾经出现过后缀名为“.H”,“.h++”,".h"以及“.hxx”和“.hpp”的头文件。但是,与源文件不太一样的时,今天仍然有许多C++程序仍然使用“.h”后缀名的头文件。

        相信读者在一些“.h”后缀名的头文件里见到过下面这样的宏控制命令:

#ifdef__cplusplusextern"C"{#endif...#ifdef__cplusplus}#endif

        基本上,这样的头文件既可以被C语言程序包含,又可以被C++程序包含。有时,有些头文件不知道自己是否可以被包含在C语言程序的上下文中,甚至连后缀名都没有。

        另外,有些程序项目的源文件使用“.ii”,".ixx",“.ipp”作为后缀名,这类文件一般专用于提供内敛定义。还有些源文件使用“.txx”,“.tpp”,“.tpl” 作为后缀名,用于定义模板。这些源文件一般会被包含在头文件里,或者直接包含在程序项目的上下文中。

        编译器和工具通常并不关心源文件的后缀名,不过在默认状态下,编译器和工具会根据源文件的后缀名识别出代码就是是C语言还是C++语言,还是其他编程语言编写的。

        换句话说,如果编译器能够识别“.cpp”和“.cc”后缀名的源文件,那么使用哪一个都是可以的,因为它们都能告诉编译器该源文件代码是由C++语言编写的。

上面学的接口做通信前的准备工作是这样的:

Makefile

.PHONY:all
all:shmClient shmServer

shmClient:shmClient.cc
	g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmClient shmServer

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3

const std::string msg[] = {"Debug","Notice","Warning","Error"};

std::ostream& Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}
#endif

comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h> // ftok mkfifo open
#include <sys/ipc.h> // shmget ftok
#include <sys/shm.h> // shmget
#include <sys/stat.h> // mkfifo open
#include <fcntl.h> // mkfifo open
#include "Log.hpp"

using namespace std;

#define PATH_NAME "/home/rtx2"
#define PROJ_ID 0x66 // 随便写的
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍,多1字节也会多创建一页

shmServer.cc

#include "comm.hpp"

string TransToHex(key_t k) // ftok的返回值转为十六进制
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    // 1. 创建公共的Key值,传入参数相等,得到的返回值相等
    key_t k = ftok(PATH_NAME, PROJ_ID); // 在comm.hpp定义了的宏
    assert(k != -1); // 返回值是-1就创建失败

    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;

    // 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    // 第三个参数的两个选项,不存在就创建,存在就出错返回,保证创建的是新的共享内存
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << " shmid : " << shmid << endl;

    sleep(10);
    // 3. 将指定的共享内存,attach(挂接)到自己的地址空间
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;
    sleep(10);

    // 这里是通信的逻辑

    // 4. 将指定的共享内存,从自己的地址空间中去(取消)关联
    int n = shmdt(shmaddr); // 传入第3步的返回值(地址)
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;
    sleep(10);

    // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;
    return 0;
}

shmClient.cc

#include "comm.hpp"

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID); // 传入参数相等,得到的返回值相等
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0); // 第三个参数传入IPC_CREAT也行 creat
    if (shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;
    sleep(10);

    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
    sleep(10);

    //使用

    // 关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm success", Error) << " client key : " << k << endl;
    sleep(10);

    // client 不需要chmctl删除
    return 0;
}

开三个窗口,右边加上下一行的监控脚本,Server在左上先跑,3秒后Client在左下再跑:

while :; do ipcs -m; sleep 1; done

看到的就应该是Server跑之后nattch变为1,Client跑之后nattch变为2,然后去关联变1,再变0。

上面是跑完,然后监视拉上去的图,Server跑之后nattch变为1:(看右边类似进度条还是啥)

Client跑之后nattch变为2:

然后变为1,再变为0:

都退出后Ctrl C掉:


1.6 共享内存进程间通信

以前讲的代码区静态区什么的,栈区堆区中间有一块共享区:

共享区里的共享内存:

共享内存的优势:

考虑一个问题,通信双方一方写入,一方读取,采用管道和共享内存分别发生了几次数据拷贝。

管道:(一个箭头就是一次拷贝)

一共需要四次拷贝:键盘->写入端进程地址空间->管道->写出端进程地址空间->显示器

共享内存:

 一共需要两次拷贝:键盘->共享内存(写入写出端进程地址空间)->显示器

当通信的数据量非常大时,共享内存的方式大大减少了拷贝次数。


把前面测试的sleep都注释掉,加上通信代码,加上管道(访问控制)进行通信:

Makefile

.PHONY:all
all:shmClient shmServer

shmClient:shmClient.cc
	g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmClient shmServer

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3

const std::string msg[] = {"Debug","Notice","Warning","Error"};

std::ostream& Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}
#endif

comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h> // ftok mkfifo open
#include <sys/ipc.h> // shmget ftok
#include <sys/shm.h> // shmget
#include <sys/stat.h> // mkfifo open
#include <fcntl.h> // mkfifo open
#include "Log.hpp"

using namespace std;

#define PATH_NAME "/home/rtx2"
#define PROJ_ID 0x66 // 随便写的
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍,多1字节也会多创建一页

#define FIFO_NAME "./fifo"

class Init // 创建全局变量让它自己加载
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        Log("remove fifo success",Notice) << "\n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}

void Wait(int fd)
{
    Log("等待中...", Notice) << "\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}

void Signal(int fd) // 唤醒另一方过来读
{
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中...", Notice) << "\n";
}

void CloseFifo(int fd)
{
    close(fd);
}

shmServer.cc

#include "comm.hpp"

// 对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init;  // 放在comm.hpp会被构造两次,会报错

string TransToHex(key_t k) // ftok的返回值转为十六进制
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    // 1. 创建公共的Key值,传入参数相等,得到的返回值相等
    key_t k = ftok(PATH_NAME, PROJ_ID); // 在comm.hpp定义了的宏
    assert(k != -1); // 返回值是-1就创建失败

    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;

    // 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    // 第三个参数的两个选项,不存在就创建,存在就出错返回,保证创建的是新的共享内存
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << " shmid : " << shmid << endl;

    // sleep(10);
    // 3. 将指定的共享内存,attach(挂接)到自己的地址空间
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;
    // sleep(10);

    // 这里是通信的逻辑
    // 将共享内存当成一个大字符串 char buffer[SHM_SIZE];
    int fd = OpenFIFO(FIFO_NAME, READ); // 打开管道文件
    while (true)
    {
        Wait(fd); // 先等,直到被唤醒
        // 临界区
        printf("%s\n", shmaddr);
        if (strcmp(shmaddr, "quit") == 0)
        {
            break;
        }
        // sleep(1);
    }
    // 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
    //         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
    // 结论2: 共享内存缺乏访问控制,会带来并发问题 [这里创建管道文件的部分就是加上访问控制]

    // 4. 将指定的共享内存,从自己的地址空间中去(取消)关联
    int n = shmdt(shmaddr); // 传入第3步的返回值(地址)
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;
    // sleep(10);

    // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;
    return 0;
}

shmClient.cc

#include "comm.hpp"

int main()
{
    Log("child pid is : ", Debug) << getpid() << endl;
    key_t k = ftok(PATH_NAME, PROJ_ID); // 传入参数相等,得到的返回值相等
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0); // 第三个参数传入IPC_CREAT也行 creat
    if (shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    // 使用
    // client将共享内存看做一个char 类型的buffer
    while(true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE-1);
        if(s > 0)
        {
            shmaddr[s-1] = 0; // s-1是为了把输入quit回车的回车去掉
            Signal(fd); // 写出成功后唤醒对方
            if(strcmp(shmaddr,"quit") == 0) 
            {
                break;
            }
        }
    }
    CloseFifo(fd);

    // 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    // client 不需要chmctl删除
    return 0;
}

左边运行shmServer后,右边运行shmClient:

这就成功搞定了共享内存。


基于对于共享内存的理解:
        为了让进程间通信 -> 让不同的进程之间,看到同一份资源 -> 我们之前讲的所有的通信方式,本质都是优先解决一个问题:让不同的进程看到同一份资源。
        让不用的进程看到了同一份资源,比如共享内存,也带来了一些时序问题,造成数据不一致问题。一些概念:

  • ① 多个进程(执行流)看到的公共的一份资源:临界资源。
  • ② 自己的进程,访问临界资源的代码 :临界区。
  • ③ 为了更好地进行临界区的保护,可以让多执行流。在任何时刻,都只能有一个进程进入临界区,这就是互斥。
  • ④ 原子性:要么不做,要么做完,没有中间状态,就称之为原子性。

        所以多个执行流,互相运行的时候五相干扰, 主要是我们不加保护的访问了同样的资源(临界资源),在非临界区多个执行流互相是不影响的。


2. 消息队列(了解)

消息队列的公共资源是链表结构。
通信双方不会和消息队列进行挂接,而是像管道一样,访问内存中的消息队列。

  • 消息队列由操作系统维护,但是由通信的某一方创建和删除
  •  通信双方都需要获取到消息队列,和共享内存一样。

当发送方有数据发送时,将数据先打包成一个节点,然后尾插到内核中的消息队列中去。

当接收方接收数据时,从队列头部开始去找所需要的节点,然后进行解包得到数据。

  • 消息队列和普通队列不一样,不是严格按照先进先出的规则。
  • 读取方可以跳过队头寻找自己需要的数据。
  • 但是相同的数据,必须先读取靠近队头的。

        如上图中,当读取方需要的是香蕉,但是队头是苹果,此时就可以跳过苹果,读取香蕉,并且靠近队头的香蕉先被读取。

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
  • 特性方面:消息队列的IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

3. 信号量(了解)

信号量本质上就是资源计数器,能够保证多个进程之间访问临界资源,执行临界区代码。

  • 临界资源:多个进程都可以访问到的资源(例如:同一块内存)。
  • 临界区:访问临界资源时的代码,所在区域称之为临界区。

        如上图,将一大块公共资源划分成了多个小块的公共资源,假设一个有100个小块。

这个100就是信号量,它用来计数公共资源的个数。

        当进程想要访问某一小块资源的时候,首先要进行的就是申请信号量,一旦申请成功,信号量就会减1。

信号量申请成功进行减一的操作称为P操作。

        当这个进程访问完这块小资源后,需要将资源释放,这时候信号量就会加1。

是否资源后信号量进行加一的操作称为V操作。

当信号量为0的时候,进程就不能再申请了。

        此时再来看,所有进程在访问公共资源之前,都必须申请信号量,而申请信号量的前提是所有进程都能看到同一个信号量,所以这个信号量本身就是公共资源。既然信号量是公共资源,就必须在很多进程对它进行PV操作时保证自身的安全。

        试想,当一个进程正在申请,但是这个进程申请的比较慢,还有几个其他进程也在申请,但是申请的快。后面几个进程把信号量都申请完了,当第一个进程申请完成以后,发现信号量没了,此时就会出错。当然这是一种极端情况,在PV操作的时候,可能会因为时序问题,导致信号量有中间状态,从而导致数据不一致。

所以为了保证信号量的安全性:

  • 申请信号量->计数器减1->P操作->必须具有原子性。
  • 释放信号量->计数器加1->B操作->必须具有原子性。

        原子性:要么不做,要做就做完。也就是,信号量有互斥机制保护,当一个进程在申请信号量的时候,其他要申请信号量的进程处于阻塞状态。
        信号量的具体使用,在后面用到的时候会详细讲解,在这里只需要了解这些就可以。


4. 笔试选择题

1. 进程间通讯的方式中哪种的访问速度最快?

A.管道

B.消息队列

C.共享内存

D.套接字


2. 以下描述正确的有

A.使用ipcrm -m命令删除指定共享内存后,则会直接释放共享内存

B.使用ipcs -m命令删除指定共享内存后,则会直接释放共享内存

C.使用ipcrm -a选项可以删除所有进程间通信资源

D.使用ipcrm命令不指定选项可以删除所有进程间通信资源


3. 以下描述正确的有 

A.共享内存实现通信的原理是因为所有进程操作映射同一块物理内存

B.共享内存的操作是进程安全的

C.共享内存被删除后,则其它进程直接无法实现通信

D.所有进程与共享内存断开映射后,则共享内存自动被释放


4. 以下关于ipcrm命令描述正确的有

A.ipcrm命令不指定选项可以删除所有进程间通信

B.ipcrm -m命令可以删除共享内存

C.ipcrm -s命令可以删除共享内存

D.ipcrm -q命令可以删除管道


5. 以下关于ipc命令描述正确的有:

A.ipcs -m用于查看消息队列的信息

B.ipcs -q可以查看消息队列的信息

C.ipcrm -s可以查看共享内存的信息

D.ipcrm -q可以查看共享内存的信息


答案及解析

1. C

共享内存是将同一块物理内存映射到各个进程虚拟地址空间,可以直接通过虚拟地址访问,相较于其它方式少了两步内核态与用户态之间的数据拷贝因此速度最快

2. C

A和B:共享内存只有在当前映射连接数为0时才会被删除释放

3. A

A正确,共享内存的本质就是开辟一块物理内存,让多个进程映射同一块物理内存到自己的地址空间进行访问,实现数据共享的。

B错误,共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱

C错误,共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除

D错误,共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)

4. B

ipcrm 删除进程间通信资源

-m 针对共享内存的操作

-q 针对消息队列的操作

-s 针对信号量的操作

-a 针对所有资源的操作

根据以上理解分析:

A错误,ipcrm -a 选项是针对所有进程间通信资源

B正确

C错误,-s针对的是信号量

D错误,-q针对的是消息队列

5. B

ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源

-m 针对共享内存的操作

-q 针对消息队列的操作

-s 针对信号量的操作

-a 针对所有资源的操作


本篇完。

        重点介绍了system V通信策略的共享内存方式,包括它的原理及应用,至于消息队列和信号量仅做了解就行,在后面用到的时候会详细讲解。代码部分考察使用共享内存实现进程间通信, 理解共享内存原理, 考察shmget, shmat, shmdt, shmctl函数的使用, 理解共享内存的生命周期。要求:使用代码创建一个共享内存, 支持两个进程进行通信。

下一部分是进程信号的内容。(信号和信号量没有关系)

下一篇:零基础Linux_19(进程信号)产生信号+Core_Dump+保存信号。

(穿越回来复习顺便贴个下篇链接:零基础Linux_19(进程信号)产生信号+Core_Dump+保存信号_程序运行 发信号dump-CSDN博客

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值