进程间的通信方式

文章目录

1.简单介绍

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

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

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

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

进程间通信分类

管道

  • 匿名管道pipe

  • 命名管道

    System V IPC

  • System V 消息队列

  • System V 共享内存

  • System V 信号量

    POSIX IPC

  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁

2.管道

2.1管道的基础概念

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

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

管道读写规则:

当没有数据可读时

  • 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匿名管道
c 复制代码
#include <unistd.h>
功能:创建一无名管道,管道是一种特殊的文件,它具有两个端点,一个用于写入数据,另一个用于读取数据。
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
    是一个包含两个整数的数组,用于接收管道的两个文件描述符
返回值:成功返回0,失败返回-1
匿名管道父子进程间通信的经典案例:
c 复制代码
使用例子:从键盘读取数据,写入管道,读取管道,写到屏幕
    
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

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来共享管道的原理

2.3命名管道
基本概念:

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

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

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

命名管道的创建:
c 复制代码
//命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ 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函数所创建的管道是不会随着进程结束而销毁的,我们需要手动销毁。

c 复制代码
//销毁命名管道所使用的函数

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

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

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

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

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

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

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

c 复制代码
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 函数重新将共享内存段挂载到当前进程的地址空间。
c 复制代码
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   
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
c 复制代码
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 <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#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);
}
c 复制代码
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;
}
c 复制代码
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 共享内存 基础介绍

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

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

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

4.一些基础概念的讲解

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

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

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

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

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

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

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

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

  1. 二进制信号量(Binary Semaphore):也称为互斥信号量,它的值只能为0或1。用于实现互斥访问共享资源,只允许一个进程或线程访问资源。
  2. 计数信号量(Counting Semaphore):它的值可以是任意非负整数。用于控制一定数量的资源,多个进程或线程可以同时访问资源,但受限于信号量的计数值。
相关推荐
jimy14 小时前
安卓里运行Linux
linux·运维·服务器
爱凤的小光5 小时前
Linux清理磁盘技巧---个人笔记
linux·运维
耗同学一米八6 小时前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
知星小度S7 小时前
系统核心解析:深入文件系统底层机制——Ext系列探秘:从磁盘结构到挂载链接的全链路解析
linux
2401_890443027 小时前
Linux 基础IO
linux·c语言
智慧地球(AI·Earth)8 小时前
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南
linux·ssh·ai编程
老王熬夜敲代码9 小时前
解决IP不够用的问题
linux·网络·笔记
zly35009 小时前
linux查看正在运行的nginx的当前工作目录(webroot)
linux·运维·nginx
QT 小鲜肉9 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
问道飞鱼10 小时前
【Linux知识】Linux 虚拟机磁盘扩缩容操作指南(按文件系统分类)
linux·运维·服务器·磁盘扩缩容