Linux IPC(共享内存)

让两个不同的进程看到同一个内存块(存在于物理内存当中)实现共享内存,看到同一份资源。

代码

//comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
//  IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!

#define PATHNAME "."
#define PROJID 0x6666//保证客户端和服务端可以看到同一份资源

const int gsize = 4096; //共享内存的大小以page页(4KB) 为单位的

key_t getKey()//获得shmget的第一个参数key值
{
    key_t k = ftok(PATHNAME, PROJID);
    if(k == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k;
}

string toHex(int x)//将key值转换成为16进制的字符串
{
    char buffer[64];
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}

static int createShmHelper(key_t k, int size, int flag)//加上static是为了让函数只在本文件里面有效,该函数创建的原因是createShm和getShm两个函数是比较相似的,所以统一一下。
{
{
    int shmid = shmget(k, gsize, flag);
    if(shmid == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int createShm(key_t k, int size)//服务器创建共享内存
{
    umask(0);
    return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);//0666是创建共享内存时设置的权限操作,通过ipcs -m可以查看perms字段即可。
}

int getShm(key_t k, int size)//客户端接收共享内存
{
    return createShmHelper(k, size, IPC_CREAT);
}

char* attachShm(int shmid)
{
    char *start = (char*)shmat(shmid, nullptr, 0);//未来想将共享内存当成字符串来使用,所以强转称为char*,挂接到哪里由os来指定,第三个参数权限默认为0。
    return start;
}

void detachShm(char *start)
{
    int n = shmdt(start);
    assert(n != -1);
    (void)n;
}

void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);//IPC_RMID是用来对shmid进行删除操作
    assert(n != -1);
    (void)n;
}
//下面的init类是对上面函数的封装,其实我们共享内存创建好了之后我们想要拿到的就是共享内存的其实地址,所以该类里面只需提供getstart这个函数,不用提供别的冗余的函数了。
#define SERVER 1
#define CLIENT 0
class Init
{
public:
    Init(int t):type(t)
    {
        key_t k = getKey();
        if(type == SERVER) shmid = createShm(k, gsize);
        else shmid = getShm(k, gsize);
        start = attachShm(shmid);
    }
    char *getStart(){ return start; }
    ~Init()
    {
        detachShm(start);
        if(type == SERVER) delShm(shmid);
    }
private:
    char *start;
    int type; //server or client
    int shmid;
};
#endif
//server.cc
 #include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(SERVER);
    char *start = init.getStart();
    
    //我们在通信的时候,没有使用任何接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了。  
    //因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信速度最快的。
    //共享内存没有任何的保护机制,其实就是同步互斥,这也是共享内存快速的另一个原因,现象是:client不加载的时候,我们运行server会发现server依旧会不断地读取共享内存的数据,只不过字符串置零读取的都是空而已。
    //但是对于之前的管道,当client没有向管道中发送数据的时候对应的服务端也不会去读。
    //为什么共享内存没有任何的保护机制呢?问题在于管道是通过系统接口(write和read)通信的,系统接口底层实现还干了许多事情,比如说检测管道里面是否由空间、是否有数据等等。但是共享内存是直接通信的。
    int n = 0;
    while(n <= 30)
    {
        cout <<"client -> server# "<< start << endl;
        sleep(1);
        n++;
    }



    // //1. 创建key
    // key_t k = getKey();
    // cout << "server key: " << toHex(k) << endl;

    // //2. 创建共享内存
    // int shmid = createShm(k, gsize);
    // cout << "server shmid: " << shmid << endl;

    // sleep(3);

    // //3. 将自己和共享内存关联起来
    // char* start = attachShm(shmid);

    // sleep(20);

    // // 通信代码在这里!

    // // 4.  将自己和共享内存去关联
    // detachShm(start);

    // sleep(3);

    // struct shmid_ds ds;
    // int n = shmctl(shmid, IPC_STAT, &ds);//IPC_STAT是将shmid对应的struct shmid_ds结构体的内容拷贝到ds指向的空间里面。这也说明了共享内存除了开辟对应的空间,我们还需要创立内核数据结构,里面包含了共享内存的各种属性信息。 
    // if(n != -1)
    // {
    //     cout << "perm: " << toHex(ds.shm_perm.__key) << endl;//我需要对共享内存拥有读权限才可以查看这些字段的内容,所以在创建共享内存的时候我们需要进行一个权限的操作。
    //     cout << "creater pid: " << ds.shm_cpid  << " : " << getpid() << endl;
    // }

    // ?. 删除共享内存
    //delShm(shmid);
    return 0;
}
//client.cc
#include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(CLIENT);
    char *start = init.getStart();
    char c = 'A';

    while(c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c - 'A'] = '\0';
        sleep(1);
    }


    // key_t k = getKey();
    // cout << "client key: " << toHex(k) << endl;

    // int shmid = getShm(k, gsize);
    // cout << "client shmid: " << shmid << endl;

    // //3. 将自己和共享内存关联起来
    // char* start = attachShm(shmid);

    // sleep(15);
    
    // // 4.  将自己和共享内存去关联
    // detachShm(start);


    return 0;
}

int shmget(key_t key, size_t size, int shmflg):获取共享内存函数,key值、

key_t ftok(const char *pathname, int proj_id):结合路径和项目id帮我们形成一个冲突概率非常低的key值

void *shmat(int shmid, const void *shmaddr, int shmflg):将共享内存和进程进行关联,进程创建共享内存也有可能时为别的进程创建的。shmaddr是说你将共享内存放到虚拟地址的那一块位置,其实这个值我们并不能直到,所以就直接设置成nullptr,让os自己去决定。 shmflg设置为0默认权限是可读可写。

int shmdt(const void *shmaddr):去关联,shmaddr就是shmat函数对应的返回值,卸载就是将映射关系去掉。 

系统种可以用shm共享内存进行通信,是不是只能有一对进程来使用共享内存实现通信?当让可以,  所以在任何时刻可能会有多个共享内存被用来进行通信。所以系统中一定会同时存在很多shm, 最终操作系统一定要整体管理所有的共享内存,操作系统如何管理多个shm,那就是先描述再组织。所以共享内存不只是只要在内存中开辟空间即可,系统也要为了管理shm,构建对应的描述共享内存的结构体对象struct shm。  共享内存=共享内存的内核数据结构(struct shm)+真正开辟的空间。两个进程要进行通信,一定有一个式创建共享内存,一个是获取共享内存。服务端创建共享内存最好创建全新的。

创建共享内存的进程退出之后,但是我们发现共享内存还存在。通过ipcs -m(ipc是进程间通信inter process communication),-m是只看共享内存部分,我们可以查看已经存在的共享内存,根据key 和shmid可以看到进程退出依旧存在。perms是permission权限的缩写。nattch说明的是有几个进程和当前共享内存相关联。

ipcrm -m shmid:删除共享内存,shimid指代对应共享内存的shmid值。由上面知道每次共享内存操作完成之后删除共享内存是非常有必要的。共享内存的生命周期不随进程,随os。进程挂掉了,只要进程没有显式删掉共享内存,共享内存会一直存在,直到用指令、或者os的接口删掉共享内存,否则共享内存一直存在, 直到os重启方可。所以这也证明了共享内存和文件系统是两套机制,文件系统进程释放文件也会自动释放。

int shmctl(int shmid, int cmd, struct shmid_ds *buf):系统接口来删除共享内存,和上面的指令删除是不一样的。cmd是你想对当前共享内存做哪种操作。

一般进程间的通信要经过第一次拷贝从外设读到用户的buffer缓冲区,第二次拷贝从用户缓冲区write发送到管道当中,第三次拷贝read从管道中读到用户的buffer缓冲区中,第四次拷贝就是buffer刷新到显示器上。共享内存是第一次拷贝将数据放到共享内存当中,第二次直接中共享内存中读取数据就两次。

nattch选项是可以看到有几个进程和当前的共享内存相关联

共享内存的生命周期不随进程,而是随os。也就是如果创建共享内存了,哪怕该进程推出了共享内存还是存在,解决方法就是:1、用指令进行删除;2、调用系统调用接口。

信号量

我们把大家都能看到的资源叫做公共资源。任何一个时刻,都只允许一个执行流在进形共享资源的访问叫做加锁。将任何一个时刻都只允许一个执行流进行共享资源访问叫做互斥。我们把任何一个时刻只允许一个执行流进行访问的共享资源叫做临界资源。临界资源是通过代码访问的,凡是访问临界资源的代码叫做临界区。 通过代码进行加锁实际上是对临界区进行保护的,进临界区之前做加锁,出临界区做解锁就可以了。原子性:要么不做,要么做完,只有两种确定状态的属性。

信号量和信号灯的本质就是一个描述资源数量的计数器,有点像int count。任何一个执行流,像访问临界资源中的额一个子资源的时候,不能直接访问,得先申请信号资源,只要申请信号量成功,我们未来一定能够拿到一个子资源,此时count--。进入临界区,访问临界资源。当我们离开的时候,释放信号量资源,相当于让count++,只要将计数器增加,就表示将我们对应的资源进行了归还。所有的进程都必须先看到信号量,所以信号量就是共享资源,而信号量又是共享资源,信号量是用来保护共享资源的,此时信号量自己成了共享资源,那信号量保护人家(其它共享资源),那谁来保护信号量,这就要求信号量经过设计必须保证自己的++和--操作时原子性的,也就是要么不加减,要么加减工作已经完成了。我们将申请信号量称为p操作,释放信号量称为v操作。

信号量是资源的一种预定机制,信号量一定是另一种共享资源,为了保证自身操作的安全,它的操作必须是原子性的,它的操作又叫做原子性的pv操作。那么两个进程,能看到同一个int count吗?可以,但是我们想让不同的进程看到同一个计数器,不能发生写实拷贝,否则这就不是同一个计数器了,如果a进程有a进程的计数器,b进程有b进程的计数器  ,彼此之间互不影响,这是不合理的,所以我们将信号量归类为进程间的通信,信号量是不以传送数据为目的而用来协同两个进程工作的一种通信方案,也属于进程间通信的一种。如果计数器是1呢?那么现在很多进程都想要访问该资源,当其中一个进程申请到该信号量的时候,将计数器减减成为0,此时别的进程就不发在申请资源了,这就完成了二元信号互斥功能。互斥的本质就是就是将临界资源独立进行使用。

信号量的相关接口(认识一下就行)

int semget(key_t key, int nsems, int semflg):获取信号量函数,nsems指的是信号量集也就是允许一次申请多个信号量,多个信号量和单个信号量是几是两码事。sem是semaphore的意思。

查看信号量的指令:ipc -s

删除信号量的指令:ipcrm -s semid

int semctl(int semid, int semnum, int cmd, ...):因为一次允许我们创建多个信号量,semnum是对哪一个信号量进行操作。

int semop(int semid, struct sembuf *sops, unsigned nsops):对指定的信号进行pv操作

理解ipc

system V系统的ipc有消息队列、共享内存、信号量三种方式,如何对其进行管理呢?操作系统管理这些ipc资源并不是分开管理的,先描述,分别描述成为struct shmid_ds、struct semid_ds、struct msqid_ds结构体,再通过一个数组struct ipc_perm* ipc_id_arr[]来将内核中所有的ipc资源统一以数组的方式进行管理。结构体的地址和结构体首元素的地址在树值上是一样的。如果我们想通过该数组访问某个ipc中的除了struct ipc_perm的其它元素,可以通过((struct shmid_ds*)pc_id_arr[n])->other这种强转方式来实现访问,这样虽然ipc之间的数据结构存在差异,但是我们依然实现了统一的管理。而这就是多态,其中struct ipc_perm perm是基类

Linux系统提供了各种系统调用API用于进程之间的通信:    无名管道PIPE    命名管道FIFO    消息队列    共享内存    信号量    文件锁    信号signal....其中还包括system V和POSIX 两种接口标准,除此之外,Linux系统自身还扩展了自己的一套API接口用于进程间通信,比如signalfd、timerfd、eventfd等。本视频教程为《Linux系统编程》第05期,本期课程将会带领大家学习Linux下将近15种进程间通信IPC工具的使用,了解它们的通信机制、编程实例、使用场景、内核中的实现以及各自的优缺点。本课程会提供PDF版本的PPT课件和代码,学员购买课程后可到课程主页自行下载嵌入式自学路线指导图:------------------------------------------------------------------------------------------------------                   《嵌入式工程师自我修养》嵌入式自学系列教程                                          作者:王利涛------------------------------------------------------------------------------------------------------一线嵌入式工程师精心打造,嵌入式学习路线六步走: 第 1 步:Linux三剑客零基础玩转Linux+UbuntuGit零基础实战:Linux开发技能标配vim从入门到精通基础篇:零基础学习vim基本命令vim从入门到精通定制篇:使用插件打造嵌入式开发IDEmakefile工程实践基础篇:从零开始一步一步写项目的Makefilemakefile工程实践第2季:使用Autotools自动生成Makefile软件调试基础理论printf打印技巧Linux内核日志与打印使用QEMU搭建u-boot+Linux+NFS嵌入式开发环境第 2 步:C语言嵌入式Linux高级编程第1期:C语言进阶学习路线指南第2期:计算机架构与ARM汇编程序设计第3期:程序的编译、链接和运行原理第4期:堆栈内存管理第6期:数据存储与指针第7期:嵌入式数据结构与Linux内核的OOP思想第8期:C语言的模块化编程第9期:CPU和操作系统入门      搞内核驱动开发、光会C语言是不行的!      你还需要学习的有很多,包括:计算机体系架构、ARM汇编、程序的编译链接运行原理、CPU和操作系统原理、堆栈内存管理、指针、linux内核中的面向对象思想、嵌入式系统架构、C语言的模块化编程.....第 3 步:Linux系统编程第00期:Linux系统编程入门第01期:揭开文件系统的神秘面纱第02期:文件I/O编程实战第03期:I/O缓存与内存映射第04期:打通进程与终端的任督二脉第05期:进程间通信-------------------we are here!‍    第 4 步:Linux内核编程‍    练乾坤大挪移,会不会九阳神功,是一道坎。搞驱动内核开发,懂不懂内核也是一道坎。第 5 步:嵌入式驱动开发    芯片原理、datasheet、硬件电路、调试手段、总线协议、内核机制、框架流程....6 步:项目实战    嵌入式、嵌入式人工智能、物联网、智能家居...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值