Linux应用编程基础05-进程通信

进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。系统中的每一个进程都有 各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以同一个进程的不同模块 (譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等。

两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中,Linux 内核提供了多种进程间通信的机制

1、进程间的通信机制

Linux 内核提供了多种 IPC 机制,基本是从 UNIX 系统继承而来

  • UNIX IPC(古老的通信方式):管道、FIFO、信号
  • System V IPC(本地化进程间通信):信号量、消息队列、共享内存
  • POSIX IPC(网络中进程间通信):信号量、消息队列、共享内存、Socket

进程通信实现原理:通信进程之间能够访问同一个内存

2、管道

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

管道的本质是一个文件,一个进程以写方式打开文件,另一个进程以读方式打开,前面写完后面读,就实现了通信

2.1 匿名管道

一种特殊的管道,用于在具有亲缘关系的进程之间进行单向通信

创建匿名管道

c++ 复制代码
#include<unistd.h>

int pipe(int fd[2]); 

/*
* fd数组:两个文件标识符分别构成管道的两端,fd[0]读文件描述符 fd[1]写文件描述符
* flags: 管道标志位参数,可以是0或其他参数
*     -O_NONBLOCK:设置非阻塞模式,即读取和写入管道时不会阻塞进程
*     -O_CLOEXEC:设置在execve系统调用执行时关闭管道
* 返回值:成功返回0,失败返回-1,并设置errno
*/

父子进程通过匿名管道通信

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    int fd[2] = {0};
    int ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipe");
        exit(-1);
    }
    ret = fork();
    if (ret == 0) // 子进程写数据,关闭读端
    {
        close(fd[0]);
        int count = 5;
        const char *str = "Hello father, i am child.";
        while (count--)
        {
            write(fd[1], str, strlen(str));
            sleep(1);
        }
    }
    else if (ret > 0) // 父进程读数据,关闭写端
    {
        close(fd[1]);
        char buf[1024] = {0};
        while (1)
        {
            ssize_t size = read(fd[0], buf, 1024);
            if (size > 0)
                printf("buf:%s\n", buf);
            else
                break;
        }
    }
    else
    {
        perror("fork");
        exit(-1);
    }
    return 0;
}

linux命令|符号其实就是一个匿名管道,将前一个命令的输出,通过管道输入到后一命令的输入,完成一次通信

管道特点

1、单向通信

管道就像单行道,只允许数据单向流通,如果想要实现两个进程间相互进行通信,需要创建两条管道,管道1:父进程写,子进程读;管道2:子进程写,父进程读

2、管道的本质是文件

一个进程以写方式打开文件,另一个进程以读方式打开,前面写完后面读,就实现了通信

3、"血缘" 关系的进程

pipe 打开管道,并不清楚管道的名字等信息,这种管道称为 匿名管道,因此 匿名管道 只能用于有血缘关系的进程 IPC,因为 需要通过 fork 继承匿名管道信息

3、同步机制

当读端进行从管道中读取数据时,如果没有数据,则会阻塞,等待写端写入数据;如果读端正在读取,那么写端将会阻塞等待读端,因此 管道自带同步与互斥机制

匿名管道工作原理

父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的

为什么是半双工?

父进程调用fork函数成功后,父子进程不能同时保留读写文件描述符,需要关闭读或者写文件描述符。防止父子进程同时读写引发数据错误,因为只有一个管道,所以是半双工通信方式。如果想实现全双工,可以创建两个管道

为什么只能在亲缘关系的进程通信?

无亲缘关系进程文件表不能访问相同文件,进程间无法访问相同的pipe管道,所以不能通过无名管道进程通信。

2.2 命名管道

FIFO文件(也称为命名管道)是一种特殊类型的文件,在Linux中用于进程间通信。FIFO文件允许不相关的进程通过读取和写入相同的文件来进行通信。

特点:

  • FIFO文件位于文件系统中,可以像其他文件---样进行访问和管理
  • FIFO文件可以通过名称进行识别和引用,而不仅仅依赖于文件描述符
  • FIFO文件可以在不同的进程之间进行双向通信

使用mkfifo命令创建FIFO文件

使用mkfifo函数创建FIFO文件

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

/*
* pathname:创建命名管道文件时的路径+名字
* mode:创建命令管道文件时的权限
*/

两个独立进程通信

思路:创建服务端 server 和客户端 client 两个独立的进程,服务端 server 创建并以读的方式打开管道文件,客户端 client 以写的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭写端(服务端读端读取到 0 后也关闭并删除命名管道文件)

common.h

c 复制代码
#pragma once
const char* fifo_name = "./fifo";   //管道名

server.c

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char const *argv[])
{
    // 服务端(读数据)
    // 1.创建命名管道
    int ret = mkfifo(fifo_name, 0666);
    if (ret < 0)
    {
        perror("mkfifo");
        exit(-1);
    }
    // 2.以读的方式打开文件
    int rfd = open(fifo_name, O_RDONLY);
    if (rfd < 0)
    {
        perror("open");
        exit(-1);
    }
    // 3、读取数据
    while (1)
    {
        char buff[64];
        ssize_t n = read(rfd, buff, sizeof(buff) - 1);
        buff[n]='\0';

        if(n>0) 
            printf("Server get message: %s\n",buff);
        else if(n==0){
            printf("Client closed.\n");
            break;
        }else{
            perror("read");
            break;
        }
    }

    // 4.关闭、删除命名管道文件
    close(rfd);
    unlink(fifo_name);  

    return 0;
}

client.c

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"

int main(int argc, char const *argv[])
{
    // 客户端(写数据)
    // 1.以写的方式打开文件
    int wfd = open(fifo_name, O_WRONLY);
    if (wfd < 0)
    {
        perror("open");
        exit(-1);
    }
    // 2、写数据
    char buff[64];
    while (1)
    {
        printf("client send message: ");
        fgets(buff, sizeof(buff) - 1, stdin);
        buff[strlen(buff) - 1] = '\0'; // 去除 '\n'
        if (strcmp(buff, "exit") == 0)
        {
            printf("Client closed.\n");
            break;
        }
        write(wfd, buff, strlen(buff));
    }
    // 3.关闭文件
    close(wfd);

    return 0;
}

3、共享内存

System V 标准中一个比较成功的通信方式,特点就是非常快

原理:在物理内存中开辟一块公共区域,让两个不同的进程的虚拟地址同时对此空间建立映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行写入或读取,这块公共区域就是共享内存

3.1 共享内存相关知识

共享内存的数据结构

共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存

共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,因此 操作系统需要对共享内存的状态加以描述

shm 表示共享内存

c 复制代码
struct shmid_ds
{
    struct ipc_perm shm_perm;    /* operation perms */
    int shm_segsz;               /* size of segment (bytes) */
    __kernel_time_t shm_atime;   /* last attach time */
    __kernel_time_t shm_dtime;   /* last detach time */
    __kernel_time_t shm_ctime;   /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch;   /* no. of current attaches */
    unsigned short shm_unused;   /* compatibility */
    void *shm_unused2;           /* ditto - used by DIPC */
    void *shm_unused3;           /* unused */
};

其中 struct ipc_perm 中存储了 共享内存中的基本信息

c 复制代码
struct ipc_perm
{
    __kernel_key_t key; 
    __kernel_uid_t uid;
    __kernel_gid_t gid;
    __kernel_uid_t cuid;
    __kernel_gid_t cgid;
    __kernel_mode_t mode; 
    unsigned short seq;
};

创建共享内存

使用 shmget 函数创建

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

/*
* key:创建共享内存时的唯一 key 值,通过ftok函数计算获取(确保唯一)
* size:创建共享内存的大小,一般为 4096,与一个 PAGE 页大小相同,有利于提高 IO 效率
* shmflg:位图,可以设置共享内存的创建方式及创建权限
*     -IPC_CREAT 创建共享内存,如果存在,则使用已经存在的
*     -IPC_EXCL 配合 IPC_CREAT 使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败
*     -权限 因为共享内存也是文件,所以权限可设为文件的起始权限 0666
* 返回值:创建成功返回共享内存的 shmid,失败返回 -1
*/

注意:只有先让操作系统根据同一个 key 创建/打开 同一个共享内存,不同的进程才能看到同一份资源

创建共享内存 common.hpp

hpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号

const int gsize = 4096;
const mode_t mode = 0666;

//将十进制数转为十六进制数
string toHEX(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}

// 获取key
key_t getKey()
{
    // ftok函数计算key,根据不同的项目路径 + 项目编号 + 特殊的算法
    key_t key = ftok(PATHNAME, PROJID);
    if (key == -1)
    {
        // 失败,终止进程
        perror("ftok");
        exit(-1);
    }
    return key;
}

// 创建共享内存
int createShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}

// 获取共享内存
int getShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT);
}

int shmHelper(key_t key, size_t size, int flags)
{
    int shmid = shmget(key, size, flags);
    if (shmid == -1)
    {
        perror("shmget");
        exit(-1);
    }
    return shmid;
}

服务端进程 server.cpp

cpp 复制代码
#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);

    cout << "server key: " << toHEX(key) << endl;
    cout << "server shmid: " << shmid << endl;
    return 0;
}

客户端进程 client.cpp

cpp 复制代码
#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 客户端打开共享内存
    key_t key = getKey();
    int shmid = getShm(key, gsize);

    cout << "client key: " << toHEX(key) << endl;
    cout << "client shmid: " << shmid << endl;
    return 0;
}

创建出来的共享内存可以通过 ipcs -m 查看

进程关联

当进程与共享内存关联后,共享内存才会通过页表映射至进程的虚拟地址空间中的共享区中

需要使用 shmat 函数进行关联

c 复制代码
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

/*
* shmid:待关联的共享内存 id
* shmaddr:共享内存关联至进程共享区的地址,一般传NULL
* shmflg:进程对共享内存的读写属性,写0使用默认读写权限
* 返回值:关联后返回共享内存映射至共享区的起始地址, 一般通信数据为字符,所以可以将 shmat 的返回值强转为 char*
*/

服务端进程 server.cpp

cpp 复制代码
#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);
    
    // 关联共享内存
    char *start = (char *)shmat(shmid, NULL, 0);
    if ((void *)start == (void *)-1)
    {
        perror("shmat");
        shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放
        exit(-1);
    }

    // 挂接成功后,睡五秒再释放
    printf("start: %p\n", start);
    sleep(5);

    // 释放共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

客户端进程 client.cpp

cpp 复制代码
#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 客户端打开共享内存
    key_t key = getKey();
    int shmid = getShm(key, gsize);

    char *start = (char *)shmat(shmid, NULL, 0);
    if ((void *)start == (void *)-1)
    {
        perror("shmat");
        exit(1);
    }

    // 挂接成功后,睡三秒就结束
    printf("start: %p\n", start);
    sleep(3);

    return 0;
}

同一个共享内存,关联的两个进程中的映射地址不一样,因为这是虚拟地址

进程去关联

使用函数 shmdt

c 复制代码
 #include <sys/types.h>
 #include <sys/shm.h>

 int shmdt(const void *shmaddr);

注意:

共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程

共享内存被提前删除后,状态 status 变为 销毁 dest

共享内存控制

shmctl 函数

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

/*
* cmd: 控制共享内存的具体动作
*     -IPC_STAT 用于获取或设置所控制共享内存的数据结构
*     -IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值
*     -IPC_RMID 释放共享内存
* buf:共享内存的数据结构,当cmd参数为IPC_RMID可以不用设置
*/

通过 shmctl 获取共享内存的数据结构,并从中获取 pidkey

cpp 复制代码
#include <iostream>
#include "common.hpp"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);
    char *start = (char *)shmat(shmid, NULL, 0); 
    if ((void *)start == (void *)-1){
        perror("shmat");
        shmctl(shmid, IPC_RMID, NULL); 
        exit(1);
    }

    struct shmid_ds buf; // 共享内存的数据结构
    int n = shmctl(shmid, IPC_STAT, &buf);
    if (n == -1){
        perror("shmctl");
        shmctl(shmid, IPC_RMID, NULL); 
    }

    cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
    cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;

    shmdt(start); //去关联
    shmctl(shmid, IPC_RMID, NULL);// 释放共享内存
    return 0;
}

共享内存的大小

在上面的代码中,将共享内存的大小设为 4096 字节,即一个 PAGE 页的大小(4kb); 如果申请 4097 字节大小的共享内存,操作系统实际上会分配 8192 字节(8kb 的空间),但供共享内存使用的只有 4097 字节

因为操作系统为了避免因非法操作导致出现越界访问问题,所以会开辟 PAGE 页的整数倍大小空间,多开辟的空间不会给共享内存时,主要是用来检测是否出现了越界访问

共享内存 "快" 的原因

共享内存 IPC 快的秘籍在于 减少数据拷贝(IO),IO 是很慢、很影响效率的

比如使用管道通信:

  1. 从进程 A 中读取数据(IO)
  2. 打开管道,然后通过系统调用将数据写入管道(IO)
  3. 通过系统调用从管道读取数据(IO)
  4. 将读取到的数据输出至进程 B(IO)

但是共享内存,直接访问同一块区域进行数据读写

  1. 进程 A 直接将数据写入共享内存中
  2. 进程 B 直接从共享内存中读取数据

得益于共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程通信中,速度最快的

3.2 共享内存简单使用

当两个进程与同一块共享内存成功关联后,可以直接对该区域进行读写操作

为了使操作更加简洁,可以将 common.hpp 中的代码封装为一个类,创建、关联、去关联等操作一气呵成

common.hpp

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号

enum{SERVER = 0,CLIENT = 1};

class shm
{
public:
    shm(int id): _id(id){
        _key = getKey(); //获取 key

        // 根据不同的身份,创建 / 打开 共享内存
        if (_id == SERVER)
            _shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
        else
            _shmid = shmHelper(_key, gsize, IPC_CREAT);

        // 关联共享内存
        _start = shmat(_shmid, NULL, 0); // 关联
        if (_start == (void *)-1){
            perror("shmat");
            exit(-1);
        }
    }

    ~shm(){
        // 去关联
        int n = shmdt(_start);
        if (n == -1){
            perror("shmdt");
            exit(-1);
        }

        // 根据不同的身份,判断是否需要删除共享内存
        if (_id == SERVER)
            shmctl(_shmid, IPC_RMID, NULL);
    }

    key_t getKey() const{
        key_t key = ftok(PATHNAME, PROJID);
        if (key == -1){
            perror("ftok");
            exit(-1);
        }
        return key;
    }

    int getShmID() const{
        return _shmid;
    }

    void *getStart() const{
        return _start;
    }

protected:
    static const int gsize = 4096;
    static const mode_t mode = 0666;

    // 将十进制数转为十六进制数
    string toHEX(int x){
        char buffer[64];
        snprintf(buffer, sizeof buffer, "0x%x", x);
        return buffer;
    }

    // 共享内存助手
    int shmHelper(key_t key, size_t size, int flags){
        int shmid = shmget(key, size, flags);
        if (shmid == -1){
            perror("shmget");
            exit(2);
        }

        return shmid;
    }

private:
    key_t _key;
    int _shmid = 0;
    void *_start;
    int _id; // 身份标识符,用来区分服务端与客户端
};

使用:客户端向服务器发送信息,服务端接收信息并显示

服务端 server

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"

using namespace std;

int main()
{
    // 服务端
    shm s(SERVER);

    // 获取共享内存起始地址
    char *start = (char *)s.getStart();
    char *before = (char *)malloc(sizeof(char *) * (1024));
    strcpy(before, start);
    // 开始通信
    while (true)
    {
        if (strcmp(start, "exit") == 0) // 如果接收到 exit 退出
            break;

        if (strcmp(start, before) != 0) // 如果这一次的信息和上一次一样则不显示
        {
            cout << "server get: " << start << endl;
            strcpy(before, start);
        }
        sleep(1);
    }
    free(before);
    return 0;
}

客户端 client

cpp 复制代码
#include <iostream>
#include <string>
#include "common.hpp"

using namespace std;

int main()
{
    // 客户端
    shm c(CLIENT);

    // 获取共享内存起始地址
    char *start = (char *)c.getStart();
    string s;
    // 开始通信
    int n = 0;
    printf("client sent: \n");

    //写入26个字母后,终止客户端
    while (1)
    {
        getline(cin, s);
        strcpy(start, s.c_str());
        if ("exit" == s)
            break;
    }
    return 0;
}

3.3 配合命名管道完成通信

共享内存也存在缺点:没有同步和互斥机制,某个进程可能数据还没写完,就被读走,导致数据无法确保安全

可以利用其他通信方式,控制共享内存的写入与读取规则,比如使用命名管道,进程 A 写完数据后,才通知进程 B 读取,进程 B 读取后,才通知进程 A 写入

所需要资源:一块共享内存,两条命名管道

  • 一条管道负责 服务端写,客户端读
  • 一条管道则负责 服务端读,客户端写,间接实现 双向通知

common.hpp 将共享内存和命名管道的前置准备工作进行封装

hpp 复制代码
#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号

// 两条管道名
const char *fifo_name1 = "fifo1";
const char *fifo_name2 = "fifo2";

enum{SERVER = 0,CLIENT = 1};

class shm
{
public:
    shm(int id): _id(id){
        _key = getKey(); // 获取 key

        // 服务端:创建共享内存、创建、打开命名管道
        if (_id == SERVER){
            _shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);

            int n = mkfifo(fifo_name1, mode);
            if(n==-1){
                perror("mkfifo");
                exit(-1);
            }

            n = mkfifo(fifo_name2, mode);
            if(n==-1){
                perror("mkfifo");
                exit(-1);
            }
            // 服务端以写打开命名管道1,以读打开命名管道2
            _wfd = open(fifo_name1, O_WRONLY);
            _rfd = open(fifo_name2, O_RDONLY);
        }
        // 客户端端:打开共享内存、打开命名管道
        else{
            _shmid = shmHelper(_key, gsize, IPC_CREAT);

            // 客户端以读打开命名管道1,以写打开命名管道2
            _rfd = open(fifo_name1, O_RDONLY);
            _wfd = open(fifo_name2, O_WRONLY);
        }

        // 关联共享内存
        _start = shmat(_shmid, NULL, 0); // 关联
        if (_start == (void *)-1){
            perror("shmat");
            exit(1);
        }
    }

    ~shm(){
        // 关闭fd
        close(_wfd);
        close(_rfd);

        // 去关联
        int n = shmdt(_start);
        if (n == -1){
            perror("shmdt");
            exit(-1);
        }

        // 删除管道文件、删除共享内存
        if (_id == SERVER){
            unlink(fifo_name1);
            unlink(fifo_name2);
            shmctl(_shmid, IPC_RMID, NULL);
        }
    }

    key_t getKey() const{
        key_t key = ftok(PATHNAME, PROJID);
        if (key == -1){
            perror("ftok");
            exit(-1);
        }
        return key;
    }

    int getShmID() const{
        return _shmid;
    }

    void *getStart() const{
        return _start;
    }

    int getWFD() const{
        return _wfd;
    }

    int getRFD() const{
        return _rfd;
    }

protected:
    static const int gsize = 4096;
    static const mode_t mode = 0666;

    // 共享内存助手
    int shmHelper(key_t key, size_t size, int flags){
        int shmid = shmget(key, size, flags);
        if (shmid == -1){
            perror("shmget");
            exit(-1);
        }
        return shmid;
    }

private:
    key_t _key;
    int _shmid = 0;
    void *_start;
    int _wfd; // 写端 与 读端 fd
    int _rfd;
    int _id; // 身份标识符,用来区分服务端与客户端
};

server读端,开始先用管道2通知client,可以进行写入,然后从共享内存获取消息

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>

#include "common.hpp"

using namespace std;

int main(){
    // 服务端:读取
    shm s(SERVER);

    char *start = (char *)s.getStart();
    int wfd = s.getWFD();
    int rfd = s.getRFD();

    const char *str = "yes";

    // 这里服务端先启动,直接先向管道中发出 yes 的指令,表示客户端可以直接向共享内存写数据
    write(wfd, str, strlen(str));

    char buff[64];
    while (true){
    	// 等待客户端发出操作命令
        // 由于管道的同步机制,当读端进行从管道中读取数据时,如果没有数据,则会阻塞,等待写端写入数据
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = 0;
        if (n > 0){
            if (strcasecmp(str, buff) == 0){
                // 客户端允许服务端进行读取
                int i = 0;
                while (start[i]){
                    buff[i] = start[i];
                    i++;
                }
                buff[i] = 0;

                printf("server read: %s\n", buff);

                if (strcasecmp("exit", buff) == 0)
                    break;

                // 读取完成,通知客户端写入
                write(wfd, str, strlen(str));
            }
        }
        else if (n == 0)
            cerr << "客户端未从管道中读取到数据" << endl;
        else{
            cerr << "读取异常" << endl;
            break;
        }
    }

    return 0;
}

client写端,接收到server可以向共享内存写的通知后,写入消息到共享内存,然后使用管道1通知server读

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>

#include "common.hpp"

using namespace std;

int main(){
    // 客户端:写入
    shm c(CLIENT);

    char *start = (char *)c.getStart();
    int wfd = c.getWFD();
    int rfd = c.getRFD();

    const char *str = "yes";

    srand((size_t)time(NULL));

    char buff[64];
    while (true){
        // 等待服务端发出操作命令
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = 0;

        if (n > 0){
            if (strcasecmp(str, buff) == 0){
                printf("client write: ");
                fflush(stdout);

                fgets(buff, sizeof buff, stdin);
                int i = 0;
                while (buff[i] != '\n'){
                    start[i] = buff[i];
                    i++;
                }
                buff[i] = start[i] = 0;

                // 写入完成,通知服务端读取
                write(wfd, str, strlen(str));

                if (strcasecmp("exit", buff) == 0)
                    break;
            }
        }
        else if (n == 0)
            cerr << "客户端未从管道中读取到数据" << endl;
        
        else{
            cerr << "读取异常" << endl;
            break;
        }
    }

    return 0;
}

这里模拟实现的是客户端写,服务端读,如果想反转,更改读写逻辑即可,因为共享内存支持双向通信 因为是两条命名管道,刚开始都在等对方写入数据,所以必须由一方先出击,打破这种 无限等待 的破局,建议谁读取,谁就先通知,即在执行通信代码前,通知 写入方 可以写入数据了

4、消息队列

消息队列(Message Queuing)是一种比较特殊的通信方式,它不同于管道与共享内存那样借助一块空间进行数据读写,而是在系统中创建了一个队列,这个队列的节点就是数据块,包含类型和信息

因为消息队列使用起来比较麻烦,并且过于陈旧,现在已经较少使用了,所以这里就不详细讲解原理

消息队列的大部分接口都与共享内存近似,可快速上手消息队列

5、信号量

信号量(semaphore)一种特殊的工具,主要用于实现 同步和互斥

5.1 互斥相关概念

1、并发是指系统中同时存在多个独立的活动单元

  • 比如在多线程中,多个执行流可以同时执行代码,可以访问同一份共享资源

2、互斥是指同一时刻只允许一个活动单元使用共享资源

  • 即在任何一个时刻,都只允许一个执行流进行共享资源的访问(可以通过加锁实现)

3、临界资源临界区,多执行流环境中的共享资源就是临界资源,涉及临界资源操作的代码区间即临界区

  • 在多线程环境中,全局变量就是临界资源,对全局变量的修改、访问代码属于临界区

4、原子性:只允许存在成功和失败两种状态

  • 比如对变量的修改,要么修改成功,要么修改失败,不会存在修改一半被切走的状态

互斥 是为了解决临界资源在多执行流环境中的并发访问问题,需要借助互斥锁或信号量 等工具实现原子操作

5.2 工作原理

信号量是一个计数器,,它主要用于控制多个进程间或一个进程内的多个线程间对临界资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些临界资源,同时,进程也可以修改该标志,除了用于临界资源的访问控制外,还可用于进程同步

具体表现为:资源申请,计数器 -1,资源归还,计数器 +1,只有在计数器不为 0 的情况下,才能进行资源申请,可以设计二元信号量实现互斥

System V 中的 信号量操作比较麻烦,学习多线程时,也会使用 POSIX 中的信号量实现互斥,相比之下,POSIX 版的信号量操作要简单得多,同时应用也更为广泛

相关推荐
许白掰1 小时前
Linux入门篇学习——Linux 工具之 make 工具和 makefile 文件
linux·运维·服务器·前端·学习·编辑器
longze_75 小时前
Ubuntu连接不上网络问题(Network is unreachable)
linux·服务器·ubuntu
Dirschs5 小时前
【Ubuntu22.04安装ROS Noetic】
linux·ubuntu·ros
qianshanxue115 小时前
ubuntu 操作记录
linux
AmosTian8 小时前
【系统与工具】Linux——Linux简介、安装、简单使用
linux·运维·服务器
这我可不懂10 小时前
Python 项目快速部署到 Linux 服务器基础教程
linux·服务器·python
车车不吃香菇11 小时前
java idea 本地debug linux服务
java·linux·intellij-idea
tan77º11 小时前
【Linux网络编程】Socket - TCP
linux·网络·c++·tcp/ip
kfepiza12 小时前
Linux的`if test`和`if [ ]中括号`的取反语法比较 笔记250709
linux·服务器·笔记·bash
CodeWithMe12 小时前
【Note】《深入理解Linux内核》 第十九章:深入理解 Linux 进程通信机制
linux·运维·php