linux下的进程通信

进程为什么需要通信呢?

虽然进程都有相对独立性,但是还是需要进行互相通信的,比如说QQ发消息、一个进程想要给另一个进程发送数据、几个进程之间想共享一份数据等等,这些都需要进程进行通信。、

进程通信的技术背景

  • 进程是有独立性的。虚拟地址空间+页表 保证进程运行的独立性(进程内核数据结构+进程的代码和数据)
  • 由于独立性的原因,通信成本会比较高

进程通信本质

  • 进程通信的前提,首先需要让不同的进程看到同一块“内存”(特定的结构组织的)
  • 同一块“内存”,不属于任意进程,是在进程的共享代码段。

进程通信分类

管道

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

# | 命令就是连接两个进程之间的管道命令
# who命令输出两行数据,交给wc -l 进程,统计行数,输出。
who | wc -l 

在这里插入图片描述

匿名管道pipe

功能:创建一个无名管道
int pipe(int fd [2])
参数:
fd:文件描述符数组,其中fd[0],为读端;fd[1]为写端。
返回值:成功返回0,失败返回错误代码。

使用fork(创建父子进程进行通信)来验证管道的原理:
fork之后:子进程会创建一份新的PCB,并且会复制一份父进程的文件描述符数组;像打开文件的信息,管道等都只共享的一份。

下面这段代码实现了,子进程写入管道,父进程从管道读出这一功能:

#include<iostream>
#include<unistd.h>
//#include <string.h>
#include<cstring>
#include<cerrno>
#include<cassert>
#include<cstdlib>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    //利用fork让两个不的进程看到同一份资源
    int pipefd[2] = {0};
    //1.创建匿名管道
    int n = pipe(pipefd);
    if(n<0)   //创建管道失败
    {
        std::cout<<"pipe error, "<<errno<<" : "<<strerror(errno)<<std::endl;
        return 1;
    }

    std::cout<<"pipefd[0]: "<<pipefd[0]<<std::endl;    //0读端
    std::cout<<"pipefd[1]: "<<pipefd[1]<<std::endl;   //1写端
    
    //2.  创建子进程
    pid_t id = fork();
    assert(id!=-1);

    if(id == 0)  //子进程
    {
        //3.关掉不需要得fd,实现让父进程读,子进程写得功能
        close(pipefd[0]);

        //4.开始通信 --结合某种场景
        int cnt = 0;
        while(true)
        {
            char x = 'X';
            write(pipefd[1],&x,1);
            std::cout<<"Cnt: "<<cnt++<<std::endl;
            sleep(1);
        }

        close(pipefd[1]);
        exit(0);
    }


    //父进程
    //3. 关闭不需要的fd,让父进程读,子进程写
    close(pipefd[1]);

    //4.开始通信
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        int n = read(pipefd[0], buffer, sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]='\0';
            std::cout<<"我是父进程,child give me messages:"<<buffer<<std::endl;
        }
        else if(n==0)
        {
            std::cout<<"我是父进程,读到了文件结尾"<<std::endl;
            break;
        }
        else{
            std::cout<<"我是父进程,读管道异常"<<std::endl;
            break;
        }
        sleep(2);
        if(cnt++>5) break;
    }

    close(pipefd[0]);


    int status = 0;
    waitpid(id, &status, 0);
    std::cout<<" sig: "<<(status &0x7f) << std::endl;
    


    sleep(20);



    return 0;
}

匿名管道原理

fork之后,子进程会复制一份父进程的文件描述符,指向父进程已经打开的文件资源等等。而用父进程创建匿名管道后,会在内存中,开辟一份空间,为父子进程之间提供通信,而父进程创建的这份资源会以fd的形式存在,父进程的打开文件表中。子进程复制后,子进程也能访问。
如下图:
在这里插入图片描述

管道特点

  1. 管道是用来进行具有血缘关系的进程进行进程见通信。—常用于父子通信
  2. 管道具有通过让进程间协同,提供了访问控制。
  • 写快,读慢,写满了不能再写了。
  • 写慢,读快,管道中没有数据的时候,读必须等待
  • 写关,读0,标识读到了文件结尾。
  • 读关,写继续写,OS终止写进程。
  1. 管道提供的是面向流式的通信服务,----面向字节流
  2. 管道是基于文件的,文件的生命周期是随进程的,管道的生命周期是随进程的
  3. 管道是单向通信,属于半双工通信的特殊情况,如果要进行全双工通信,需要创建两个管道。

命名管道

管道(匿名管道)应用的一个限制是只能在具有共同祖先的进程间通信。
但是,如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,这就是命名管道(一种特殊类型的文件)。

创建命名管道

  • 可以从命令行中创建管道:p开头即为管道文件

mkfifo [filename]

在这里插入图片描述

  • 可以使用函数创建命名管道:

int mkfifo(const char* filename, mode_t mode)
第一个参数是创建管道的路径
第二个参数是管道读写的权限,一般是0666

命名管道原理

首先命名管道是一个特殊的文件,因此它可以被打开到内存,但是不会将内存数据刷新到磁盘,该文件在系统中具有唯一路径,因此进程可以通过该路径找到管道文件,进行通信。
下面实现一段功能:父进程创建管道文件,然后从管道读消息,子进程从管道写消息。

//1.comm.hpp
#ifndef _COMM_H_
#define _COMM_H_

#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<cstring>
#include "Log.hpp"
using namespace std;



#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc";

#endif


//2. 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


//3.client.cxx
#include "comm.hpp"

int main()
{
    //1.获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd<0)
    {
        perror("open");
        exit(1);
    }

    //2.ipc过程
    string buffer;
    while (true)
    {
        cout<<"Please Enter Message Line :>";
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

    //3.关闭文件
    close(fd);
    return 0;
}

//4. server.cxx
#include "comm.hpp"
#include<sys/wait.h>


static void getMessage(int fd)
{
    char buffer[SIZE];
    while(true)
    {
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer)-1);

        if(s > 0)
        {
            cout<<"[ "<<getpid()<<" ]"<<"client sat>:"<<buffer<<endl;
        }
        else if(s == 0)
        {
            //end of file
            cerr<<"[ "<<getpid()<<" ]"<<"read end of file, client quit, server quit too!"<<endl;
            break;
        }
        else{
            //read error
            perror("read");
            break;
        }
    }
}


int main()
{
    //1.创建管道文件
    if(mkfifo(ipcPath.c_str(), MODE)<0)
    {
        perror("mkfifo");
        exit(1);
    }

    Log("管道文件创建成功", Debug)<<" step1"<<endl;

    //2.正常文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if(fd<0)
    {
        perror("open");
        exit(2);
    }

    Log("打开管道文件成功",Debug)<<" step 2"<<endl;


    int nums = 3;
    for(int i = 0; i < nums; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //3.编写正常的通信代码
            getMessage(fd);
            exit(1);
        }
    }

    for(int i = 0; i<nums;i++)
    {
        waitpid(-1, nullptr, 0);
    }

    //4.关闭文件
    close(fd);
    Log("关闭管道文件成功",Debug)<<" step 3"<<endl;
    unlink(ipcPath.c_str());   //通信完毕,删除管道文件
    Log("删除管道文件成功", Debug)<<" step 4"<<endl;


    return 0;
}

System V IPC

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程地址空间,这些进程间的数据传递不再涉及到内核,换句话说,进程不再通过执行进入内核的系统调用来传递彼此的数据。
如下图是 System V的通信方式:
在这里插入图片描述
该方式直接在内存里建立一块空间,供进程通信(进程AB都会在自己的页表上,建立好虚拟地址到物理地址的映射),因此只需访问自己空间的地址,就可以实现进程间通信。

管道与 System V的区别

管道对应的公共资源是文件,而文件是OS内核对应的数据结构,需要操作系统维护,因此需要系统调用来实现。而System V只需要在物理内存上申请一块空间,而内存是用户空间里的内容,用户可以不经过系统调用直接进行访问,直接进行内存级的读写即可。
但是共享内存的提供者是OS,因为OS要管理共享内存,因为OS要先描述再组织->共享内存 = 共享内存快+对应的共享内存的内核数据结构。 申请需要OS管理,但是申请完了之后,用户可以直接访问。

共享内存函数

ftok()

第0步
功能:生成一个唯一的key,供shmget使用生成共享内存段。
int ftok(const char* pathname, int proj_id);
参数:
pathname:必须是存在的、可访问的文件路径
proj_id:至少是8bit的非0数字(自己给定)
返回值:若生成成功,返回唯一的key值。失败返回-1

shmget()

1 .第一步
功能:用来创建共享内存
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段的名字(唯一id)
size:共享内存的大小
shmfg:由九个标志权限构成,他们的用法和创建文件open使用的mode模式标志一样
返回值:成功返回一个非负整数(该段共享内存段的标识码,类似于fd);失败返回-1

  • 说明shmfg参数的具体解释:
  • IPC_CREAT:创建共享内存,如果底层已经存在,获取已存在的id,并且返回;如果不存在,创建共享内存,并返回
  • IPC_EXCL: 单独使用无意义,和上面一起使用:若底层不存在,创建它并返回;若底层存在,出错返回。–>若返回成功的一定是一个全新的shm

shmat()

2.第二步
功能:将共享内存段连接到进程地址空间(在页表上生成映射)
void shmat(int shmid, const void shmaddr, int shmflg)
参数:
shmid:共享内存标识
shmaddr:指定连接的地址(一般填nullptr)
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存的第一个地址;失败返回-1

  • 说明shmflg:
  • shmaddr:为NULL,核心自动选择一个地址
  • shmaddr:不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
  • shmadd:不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg=SHM_RDONLY,标识该连接操作用作只读共享内存

shmdt()

第3步
功能:将共享内存点与当前进程脱离
int shmdt(const void* shmaddr);
参数:
shmaddr:由shmat返回的指针
返回值:成功返回0, 失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段!

shmctl()

第四步
功能:用来控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

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

删除共享内存

首先这种内存共享方式,当我们程序结束后,如果代码不主动删除,那么该内存不会被释放!
我们可以使用代码删除、也可以手动删除,手动删除使用如下命令即可。
注意:key只有在创建的时候才有,其他时候访问共享内存都是用shmid

在这里插入图片描述

System V 代码演示

该功能为:Server创建共享内存,并删除。Cilentt使用该共享内存传输数据。


// 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<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cassert>
#include"Log.hpp"

using namespace std; //不推荐

#define PATH_NAME "/home/xty"
#define PROJ_ID 0x66
#define SHM_SIZE 4096    //共享内存大小,最好是4096的整数倍




//shmClient.cc
#include "comm.hpp"

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if(k<0)
    {
        Log("creat key failed", Error)<<" client key : "<< k <<endl;
        exit(-1);
    }

    Log("creat key done", Debug)<<" client key : "<< k <<endl;

    //获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if(shmid < 0)
    {
        Log("create shm failed", Error) <<" client key : "<< k <<endl;
        exit(2);
    }
    Log(" create shm success", Debug) <<" 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", Debug) <<" client key : "<< k<<endl;
    sleep(10);


    // 使用

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

    sleep(10);


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


//shmServer.cc
#include "comm.hpp"

string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer, sizeof(buffer), "0x%x", k);
    return buffer;
}


int main()
{
    //1.创建公共的key值
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k!=-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.将指定的共享内存,挂接到自己的地址空间
    char * shmaddr = (char *)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid <<endl;
    sleep(10);

    //通信的逻辑...

    //4.将指定的共享内存,从自己的地址空间中 去 关联
    int n = shmdt(shmaddr);
    assert(n!=-1);
    (void)n;
    Log("deatch 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;
}




//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

管道和共享内存总结

管道通信的过程:由键盘->自己定义的缓冲区->进程A->write给内核缓冲区->内核缓冲区给管道文件->管道给内核缓冲区->read读到进程B处->自己定义的缓冲区->打印到屏幕。

共享内存通信:由键盘->自己定义的缓冲区->进程A->直接写入共享内存->进程B读贡献内存->到自己定义的缓冲区->屏幕。

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值