Linux进程间通信

进程间通信介绍

首先进程是具有独立性的,要让两个不同的进程,进行通信,前提是:先让两个进程,看到同一份资源,这份资源及不能属于进程A也不能属于进程B,所以只能有操作系统直接或间接提供,然后让一方写入,一方读取完成通信过程,至于通信的目的与后续工作,要结合后续具体场景。

管道

首先Linu下一切皆文件,管道也一样。管道文件也有缓冲区,和磁盘文件一样。但是管道是一个操作系统提供的内存文件,它并不需要再将自己的有效内容刷新到磁盘当中,这种文件也称之为匿名文件。最后我们关闭它操作系统会直接把结构释放掉。

匿名管道

在这里插入图片描述

站在文件描述符角度-深度理解管道

  1. 父进程创建管道
    在这里插入图片描述
  2. 父进程fork出子进程
    在这里插入图片描述
  3. 父进程关闭fd[0],子进程关闭fd[1]
    在这里插入图片描述

父进程以读和写的方式打开文件,那么fork之后它的读端和写端都能够被子进程继承下去。然后形成通信的信道,那么我们要形成单向通信的信道,就必须得关闭特定的读写端。

#include <iostream>
#include <string>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
    //让不同的进程看到同一份资源
    //任何一种进程间通信,一定要先保证不同的进程看到同一份资源
    int pipefd[2] = {0};
    //1.创建管道
    int n = pipe(pipefd);
    if(n == -1)
    {
        std::cout << "pipe调用失败," << errno << ":" << strerror(errno)<< std::endl;
        return 1;
    }
    std::cout << "pipefd[0]" << ":" << pipefd[0]<< std::endl;//读端
    std::cout << "pipefd[1]" << ":" << pipefd[1]<< std::endl;//写端
    //2.创建子进程
    pid_t id = fork();
    if(id == -1)
    {
        std::cout << "fork失败," << errno << ":" << strerror(errno)<< std::endl;
        return 1;
    }
    if(id == 0)
    {
        //子进程
        //3.关闭不需要的fd,让父进程读取,让子进程写入
        close(pipefd[0]);
        //4.开始通信---结合具体场景
        std::string namestr = "hello,我是子进程";
        int cnt = 1;
        char buffer[1024] = {0};
        while(true)
        {
            snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的PID: %d\n",namestr.c_str(), ++cnt, getpid());
            write(pipefd[1],buffer,strlen(buffer));
            sleep(1);
        }

        close(pipefd[1]);
        exit(0);
    }
    //父进程
    //3.关闭不需要的fd,让父进程读取,让子进程写入
    close(pipefd[1]);
    //4.开始通信---结合具体场景
    char buffer[1024] = {0};
    while(true)
    {
        int n = read(pipefd[0],buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout<< "我是父进程,读取的内容是:"<< buffer << std::endl;
        }
        else if(n == 0)//表示read读取到了文件结尾了
        {
            std::cout<<"我是父进程,读取到文件结尾"<<std::endl;
            break;
        }
        else // read返回-1,表示出错了
        {
            std::cout<<"我是父进程,读取出错了"<<std::endl;
            break;
        }
    }
    close(pipefd[0]);
    int status = 0;
    waitpid(id, &status, 0);
    std::cout << "sig: " << (status & 0x7F) << std::endl;
    return 0;
}

管道的特点:

  • 单向通信,如果想双向通信就在加一个管道。管道是半双工的。
  • 管道的本质是文件,因为文件描述的生命周期随进程,所以管道的生命周期是随进程的。
  • 管道通信,通常用来进行具有"血缘”关系的进程,进行进程间通信。常用与父子通信—原理还是fork之后的继承。pipe打开的管道,并不清楚管道的名称,所以称为匿名管道。
  • 在管道通信中,写入的次数,和读取的次数,不是严格匹配的读写次数的多少没有强相关----表现字节流。
  • 具有一定的协同能力,让reader和writer能够按照一-定的步骤进行通信-----自带同步机制。

管道的几种特殊场景

  • 如果read读取完了管道内所有数据,如果对方不写入,就只能等待。
  • 如果writer端将管道写满了,管道也是文件,文件就有固定大小,所以就不能写了。
  • 如果关闭了写端,读取完毕管道数据,在读就会read返回0,表示读到了文件结尾。
  • 写端一直写,读端关闭,OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程。13号信号。
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

注意:
在管道中写入的数据,只要读取了,那么读取过的数据就会被 “删除”。这里的删除,实际上是这些数据被读取过了,它就无效了,意味着下次在写入的时候可以拷贝覆盖。

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件,命名管道是内存级文件,不会刷盘。

当两个不同的进程,打开同一个文件时,操作系统不会维护两个一样的结构体(struct file)。当新进程想打开其他文件,新进程做的第一件事情,是在所有已经打开的文件列表里去找这个文件是否已经被打开了,如果没有被打开,就创建一个结构体(struct file),如果打开了他就直接把对应的结构体(struct file)对象的地址填入到当前进程的文件描述符表里,而对应的struct file里面它包含一个叫做int ret的引用计数器就会加加。

命令行上创建命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo fifo

在这里插入图片描述

函数创建命名管道

命名管道也可以从程序里创建,相关函数有:
在这里插入图片描述
comm.hpp

#pragma once
#include <iostream>
#include <string>
#define NUM 1024

const std::string fifoname = "./fifo";//要创建的文件名
uint32_t mode = 0666; //创建的文件的权限

server.cc

#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "comm.hpp"
int main()
{
    // 1. 创建管道文件,我们今天只需要一次创建
    umask(0);//将当前进程的umask 设置为0,这个设置并不影响系统的默认配置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(),mode);
    if(n == -1)
    {
        std::cerr << "mkfifo 创建命名管道失败" << errno << " : " << strerror(errno)<< std::endl;
        exit(-1);
    }
    std::cout << "创建 fifo 文件成功" << std::endl;
    // 2. 让服务端直接开启管道文件
    int fd = open(fifoname.c_str(),  O_RDONLY);
    if(fd == -1 )
    {
        std::cerr << "open 打开文件失败" << errno << " : " << strerror(errno) << std::endl;
        return 2;
    }
    std::cout << "打开fifo成功, 开始通信" << std::endl;
    // 3. 正常通信
    char buffer[NUM]= {0};
    while(true)
    {
        buffer[0] = 0;
        ssize_t r = read(fd, buffer, sizeof(buffer) - 1);
        if(r > 0)
        {
            buffer[r] = 0;
            std::cout << "client# " << buffer << std::endl;
        }
        else if( r == 0)
        {
            std::cout<< "读取到文件结束" << std::endl;
            break;
        }
        else
        {
            std::cerr <<"文件读取错误:" <<errno << " : " << strerror(errno) << std::endl;
            break;
        }
    }
    // 关闭不要的fd
    close(fd);

    unlink(fifoname.c_str());//删除创建的管道文件
   return 0;
}

client.cc

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "comm.hpp"
int main()
{
    //1. 不需创建管道文件,我只需要打开对应的文件即可!
    int fd = open(fifoname.c_str(), O_WRONLY);
    if(fd == -1 )
    {
        std::cerr << "open 打开文件失败" << errno << " : " << strerror(errno) << std::endl;
        return 2;
    }

    // 可以进行常规通信了
    char buffer[NUM];
    while(true)
    {
        std::cout << "请输入你的消息# ";
        char *msg = fgets(buffer, sizeof(buffer), stdin);
        buffer[strlen(buffer) - 1] = 0;
        // abcde\n\0
        // 012345
        if(strcasecmp(buffer, "quit") == 0) break;

        ssize_t n = write(fd, buffer, strlen(buffer));
        
    }
    close(fd);
    return 0;
}

运行结果
在这里插入图片描述
在这里插入图片描述
匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义

system V共享内存

共享内存的原理

先创建出一块物理内存,然后再通过一定的接口将物理内存的地址映射到我们两个要通信进程,各自的地址空间当中,那么最终我们返回地理空间时。进程A和进程B,它们使用时直接就用的是虚拟地址,经过页表转化不就可以访问到我们的物理内存当中特定的同一块内存了吗?所以进程间通信的前提,我们一直在强调,叫做让不同的进程得先看到同一份资源,那么这份同样的资源,是一个内存块,那么我们把它称之为共享内存。
在这里插入图片描述
注意:匿名管道和命名管道是通过文件来实现通信的,共享内存是通过内存块让不同的进程实现通信。共享内存允许多个进程进行通信。

共享内存函数

shmget

在这里插入图片描述

创建方式说明
IPC_CREAT创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT l IPC_EXCL创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 ---- 如果创建成功,对应的共享内存(shmget的返回值),一定是最新的!
ftok

在这里插入图片描述
在这里插入图片描述

在Linux中,ipcs查看进程间通信方式的信息,包括共享内存,消息队列,信号。可以携带一下选项

  • q: 只查看系统消息队列信息
  • m: 只查看系统共享内存信息
  • s: 只查看系统信号量信息
ipcs -m命令的查看共享内存的列头解释
key操作系统识别共享内存的唯一标识
shmid用户层识别共享内存的id标识
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

shmat

在这里插入图片描述

shmdt

在这里插入图片描述

shmctl

在这里插入图片描述

cmd的三种权限说明
IPC_STAT把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid ds数据结构中给出的值
IPC_RMID删除共享内存段

指令ipcrm -m shmid释放共享内存为shmid的内存。

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <assert.h>
#include <unistd.h>

#define PATHNAME "."
#define PROJID 0x6667

//共享内存的大小是以PAGE页(4KB)为单位的,开辟空间时以4KB为单位开辟。
const int gsize = 4096; //暂时
key_t getFtok()
{
    key_t k = ftok(PATHNAME, PROJID);//保证生成的值是一样的。
    if(-1 == k)
    {
        std::cerr << "调用ftok函数失败" << "error: " << errno << " : " << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

std::string toHex(int x)
{
    char buffer[64] = {0};
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

static int createShmHelper(key_t k, int size, int flag)
{
    int shmid = shmget(k, size, flag);
    if(-1 == shmid)
    {
        std::cerr << "调用shmid函数失败" << "error: " << errno << " : " << strerror(errno) << std::endl;
        exit(2);
    }

    return shmid;
}
//创建共享内存
int createShm(key_t k, int size)
{
    umask(0);
    //IPC_CREAT | IPC_EXCL | 0666 创建新的共享内存并将共享内存设置权限(这里的权限与文件权限一样)
    return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
    
}
//获取共享内存
int getShm(key_t k, int size)
{
    return createShmHelper(k, size, IPC_CREAT);
}

//链接共享内存
char* attachShm(int shmid)
{
    char *start = (char*)shmat(shmid,nullptr,0);
    //shmat 与 malloc 的
    return start;
}
//脱离共享内存的链接
void detachShm(void *start)
{
    int n = shmdt(start);
    assert(n != -1);
    (void)n;
}
//删除共享内存
//1. 函数删除
//2. 指令删除
void delShm(int shmid)
{
    //shmid_ds * temp; 如果你想看这个结构体的话,就定义一个结构体传入就行,不想看设置为nullptr就行
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
}

#define SERVER 1
#define CLIENT 0

struct Init
{
public:
    Init(int type)
        :_type(type)
    {
        key_t k = getFtok();
        if(type == SERVER)
        {
            shmid = createShm(k,gsize);
        }
        else
        {
            shmid = getShm(k, gsize);
        }
       start = attachShm(shmid);
    }
    char *getStart()
    { 
        return (char*)start; 
    }

    ~Init()
    {
        detachShm(start);
        if(_type == SERVER) 
            delShm(shmid);
    }
private:
    int shmid;
    void* start;
    int _type; //server or client
};
#endif

shmserver.cc

#include "comm.hpp"
int main()
{
    // //1. 创建key
    // key_t k = getFtok();
    // std::cout << "server key: " << toHex(k) << std::endl;

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

    // char *start = attachShm(shmid);
    // sleep(15);
    // detachShm((void*)start);

    // delShm(shmid);

    Init init(SERVER);

    // start 就已经执行了共享内存的起始空间
    char *start = init.getStart();

    int n = 0;
    // 我们在通信的时候,没有使用任何接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程 直接看到了!
    // 因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的
    // 共享内存没有任何的保护机制(同步互斥) -- 为什么?管道通过系统接口通信,共享内存直接通信
    while(n <= 30)
    {
        std::cout <<"client -> server# "<< start << std::endl;
        sleep(1);
        n++;
    }
    return 0;
}

shmclient.cc

#include "comm.hpp"
int main()
{
    // key_t k = getFtok();
    // std::cout << "client key: " << toHex(k) << std::endl;

    // int shmid = getShm(k, gsize);
    // std::cout << "client shmid: " << shmid << std::endl;
    // char *start = attachShm(shmid);
    // sleep(10);
    // detachShm((void*)start);

    Init init(CLIENT);
    // start 就已经执行了共享内存的起始空间
    char *start = init.getStart();
    char c = 'A';

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

共享内存与管道文件通信速度的比较

在这里插入图片描述
在考虑外设的情况下,首先是管道文件要经历4次拷贝,进程从外设(键盘,网络)中读取到数据,写入到语言(C, C++,…)缓冲区中(一次拷贝),按语言缓冲区的刷新策略,刷新到管道文件中(二次拷贝),然后另一进程再将管道文件中的数据拷贝到语言(C, C++,…)缓冲区中(三次拷贝),在拷贝到外设中进行读取(四次拷贝)。其次是共享内存要经历2次拷贝,进程从外设(键盘,网络)中读取到数据写入到虚拟地址空间,虚拟地会通过页表进行映射到物理地址空间完成拷贝(一次拷贝),然后另一进程直接进行读取就行,在这之中虚拟地会通过页表进行映射到物理地址空间,找到数据,然后将数据拷贝到外设中(二次拷贝)。

共享内存和管道不一样,如果写端没有写,读端可以一直读。共享内存在并发访问的时候没有进行任何保护。共享内存,没有同步和互斥。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梦乘着风去远航

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

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

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

打赏作者

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

抵扣说明:

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

余额充值