进程间的通信方式

文章目录

      • 1.简单介绍
      • 2.管道
          • 2.1管道的基础概念
            • **管道读写规则**:
            • **管道特点**
          • 2.2匿名管道
            • 匿名管道父子进程间通信的经典案例:
          • 2.3命名管道
            • 基本概念:
            • 命名管道的创建:
            • 命名管道的打开规则:
            • 匿名管道与普通管道的区别
            • **例子:用命名管道实现server&client通信**
      • 3.system v ipc
          • 3.1system v 共享内存 基础介绍
          • 3.2system v 共享内存常用接口介绍
          • 3.3system v 信息队列 以及信号量
      • 4.一些基础概念的讲解

1.简单介绍

在计算机中,进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间交换数据或信息的机制。

首先我们通过之前的学习知道进程是具有独立性的,其交互数据成本非常高,所以通信这一机制就诞生了。

通信的本质其实就是由os参与,提供一份所有通信进程能看到的公共资源。

进程间通信的方式非常多以下是对这些方式的梳理以及分类:

进程间通信分类

管道

  • 匿名管道pipe

  • 命名管道

    System V IPC

  • System V 消息队列

  • System V 共享内存

  • System V 信号量

    POSIX IPC

  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁

2.管道

2.1管道的基础概念

管道是Unix中最古老的进程间通信的形式。

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

进程间的通信方式_第1张图片

管道读写规则:

当没有数据可读时

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程

退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点
  • 管道提供流式服务

  • 一般而言,进程退出,匿名管道释放,所以匿名管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

进程间的通信方式_第2张图片

2.2匿名管道
#include 
功能:创建一无名管道,管道是一种特殊的文件,它具有两个端点,一个用于写入数据,另一个用于读取数据。
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
    是一个包含两个整数的数组,用于接收管道的两个文件描述符
返回值:成功返回0,失败返回-1
匿名管道父子进程间通信的经典案例:
使用例子:从键盘读取数据,写入管道,读取管道,写到屏幕
    
#include 
#include 
#include 
#include 
#include 

int main()
{
    int pipe_fd[2] = {0};

    if(pipe(pipe_fd) < 0){
        perror("pipe");
        return 1;
    }
    printf("%d, %d\n", pipe_fd[0], pipe_fd[1]);

    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return 2;
    }
    else if(id == 0) { //write
        //child
        close(pipe_fd[0]);

        //const char *msg = "hello parent, I am child";

        char c = 'x';
        int count = 0;
        //while(count){
        while(1){
            write(pipe_fd[1], &c, 1); //strlen(msg) + 1??
         //   sleep(1);
            count++;
            printf("write: %d\n", count);
        }

        close(pipe_fd[1]);
        exit(0);
    }
    else{              //read
        //parent
        close(pipe_fd[1]);

        char buffer[64];
        while(1){
            sleep(100);
            buffer[0] = 0;
            ssize_t size = read(pipe_fd[0], buffer, sizeof(buffer)-1);
            if(size > 0){
                buffer[size] = 0;
                printf("parent get messge from child# %s\n", buffer);
            }
            else if(size == 0){
                printf("pipe file close, child quit!\n");
                break;
            }
            else{
                //TODO
                break;
            }
        }

        int status = 0;
        if(waitpid(id, &status,0) > 0){
            printf("child quit, wait success!, sig: %d\n", status&0x7F);
        }
        close(pipe_fd[0]);
    }
    return 0;
}

对上端代码用fork来共享管道的原理

进程间的通信方式_第3张图片

2.3命名管道
基本概念:

匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件

命名管道的创建:
//命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
//命名管道也可以从程序里创建,相关函数有:
    int mkfifo(const char *filename,mode_t mode);
//filename:一个指向要创建的命名管道的路径和名称的字符串。它是一个以 null 结尾的字符数组。

//mode:用于指定创建的命名管道的权限(访问权限和文件类型)。mode 是一个 mode_t 类型的参数,通常使用八进制表示的权限值。可以使用一些预定义的常量(如 S_IRUSR、S_IWUSR、S_IRGRP 等)来设置权限。

//创建命名管道
int main(int argc, char *argv[])
{
 mkfifo("p2", 0644);
 return 0;
}
命名管道的打开规则:

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

匿名管道与普通管道的区别

匿名管道由pipe函数创建并打开(使用了pipe函数默认该描述符对应的文件打开了)。

命名管道由mkfififo函数创建,打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完

成之后,它们具有相同的语义。

tips:

通过 mkfifo 函数创建的命名管道在文件系统中持久存在,与创建它的进程无关。

使用 pipe 函数创建的管道是进程特有的,与创建它的进程相关联。当创建管道的进程结束时,管道会被自动关闭和销毁。而mkfifo函数所创建的管道是不会随着进程结束而销毁的,我们需要手动销毁。

//销毁命名管道所使用的函数

int unlink(const char *pathname);
pathname 是一个指向要删除的文件的路径和名称的字符串。它是一个以 null 结尾的字符数组。
    
//unlink 函数执行以下操作:

1.首先,它会检查指定路径的文件是否存在。如果文件不存在,unlink 函数会返回一个错误码,并不执行任何其他操作。

2.如果文件存在,并且调用进程对该文件具有足够的权限,unlink 函数将从文件系统中删除该文件。删除文件会将文件从目录中删除,并释放文件所占用的磁盘空间。

3.注意,删除文件并不会关闭已打开的文件描述符。如果有进程仍然持有对该文件的打开描述符,文件将继续存在于文件系统中,直到所有打开的描述符关闭。

4.当调用进程成功删除文件时,unlink 函数返回 0。如果出现错误,比如权限不足或指定的文件路径无效,unlink 函数返回 -1,并设置相应的错误码,可以通过 errno 全局变量获取错误信息。

    //删除命名管道文件并不会自动关闭已打开的管道文件描述符。在删除命名管道文件之前,需要确保所有打开的文件描述符都已关闭,以免导致资源泄漏或其他问题。
例子:用命名管道实现server&client通信

例子中使用的一些函数的解析:

1.
    mode_t umask(mode_t mask);
umask 函数接受一个参数 mask,它是一个无符号整数类型 mode_t 的值,表示要设置的权限掩码。mode_t 是一个用于表示文件权限和文件类型的数据类型。

2.
    key_t ftok(const char *pathname, int proj_id);
    用于生成一个唯一的键值(key)用于标识一个共享资源,例如共享内存、信号量或消息队列。
    pathname:一个存在的文件路径名,可以是任意合法的文件路径。
    proj_id:一个用户定义的整数,用于区分不同的共享资源。通常取值为一个非负整数。  
    生成的键值可以用于创建或访问共享资源,例如在调用 shmget 函数创建共享内存时使用。
 3.
    int shmdt(const void *shmaddr);      
    shmdt 函数用于将共享内存从当前进程的地址空间中分离,即解除共享内存的挂载。
    shmdt 函数并不会删除共享内存段,只是将其与当前进程分离。
    在调用 shmdt 函数之后,当前进程将无法再直接访问共享内存中的数据。如果需要重新访问共享内存,必须使用 shmat 函数重新将共享内存段挂载到当前进程的地址空间。
Log.hpp(一个简单的日志记录功能的实现)
    
#ifndef _LOG_H_
#define _LOG_H_

#include 
#include 

#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   
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
comm.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

using namespace std; //不推荐

#define PATH_NAME "/home/whb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍

#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);
}
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);
    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;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0) break;
        }
    }

    CloseFifo(fd);
    // char a = 'a';
    // for(; a <= 'z'; a++)
    // {
    //     shmaddr[a-'a'] = a;
    //     // 我们是每一次都向shmaddr[共享内存的起始地址]写入
    //     // snprintf(shmaddr, SHM_SIZE - 1,\
    //     //     "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
    //     //     getpid(), a);
    //     sleep(5);
    // }

    // strcpy(shmaddr, "quit");

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

    // client 要不要chmctl删除呢?不需要!!

    return 0;
}
shmServer.cc
 
#include "comm.hpp"

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

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

    // 这里就是通信的逻辑了
    // 将共享内存当成一个大字符串
    // char buffer[SHM_SIZE];
    // 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
    //         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
    // 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】
    
    int fd = OpenFIFO(FIFO_NAME, READ);
    for(;;)
    {
        Wait(fd);

        // 临界区
        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
        // sleep(1);
    }
    // 4. 将指定的共享内存,从自己的地址空间中去关联
    int n = shmdt(shmaddr);
    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;

    CloseFifo(fd);
    return 0;
}    

3.system v ipc

3.1system v 共享内存 基础介绍

进程间的通信方式_第4张图片

实际上,system v本质也是创建虚拟内存进而映射到实际内存上,不过跟上面管道不同的是,进程间信息交互的处理不在由内核系统那一套来处理,而是由操作系统直接处理,因此其是最快的ipc模式。

3.2system v 共享内存常用接口介绍
功能:用来创建共享内存
原型
    int shmget(key_t key, size_t size, int shmflg);
参数
    key:这个共享内存段名字
    size:共享内存大小
    shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
功能:将共享内存段连接到进程地址空间
原型
    void *shmat(int shmid, const void *shmaddr, int shmflg);参数
    shmid: 共享内存标识
    shmaddr:指定连接的地址
    shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
功能:将共享内存段与当前进程脱离
原型
    int shmdt(const void *shmaddr);
参数
    shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
功能:用于控制共享内存
原型
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数
    shmid:由shmget返回的共享内存标识码
    cmd:将要采取的动作(有三个可取值)
    buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

进程间的通信方式_第5张图片

3.3system v 信息队列 以及信号量

这一块学有余力的同学再去详细了解,这里便不在一一呈现了。

4.一些基础概念的讲解

为了之后获取更好的阅读文章体验,我将提前把一些经常出现的基础概念在此总结一下,强烈介意第一次打基础的同学阅读。

当多个进程对同一份资源进行来回操作时,因为时序问题,可能会造成数据不一致的问题,为了解决这一问题,我们将定制一套基础方案,下面先对其中涉及到的专有名词的含义进行讲解。

临界资源:多个进程执行流看到的公用的一份资源

临界区:进程访问临界资源的代码

互斥性:为了更好的临界区的保护,可以让各执行流在同一时刻只有一个进程进入临界区

原子性:一件事情要么不做,要么做完,没有中间状态

信号量:信号量是一种用于实现进程间同步和互斥的机制。可以将信号量看作是一个计数器,它的值可以被多个进程或线程修改和读取。信号量的值表示可用的资源数量或某种条件的状态。

主要有两种类型的信号量:

  1. 二进制信号量(Binary Semaphore):也称为互斥信号量,它的值只能为0或1。用于实现互斥访问共享资源,只允许一个进程或线程访问资源。
  2. 计数信号量(Counting Semaphore):它的值可以是任意非负整数。用于控制一定数量的资源,多个进程或线程可以同时访问资源,但受限于信号量的计数值。

你可能感兴趣的:(linux)