进程间通信——IPC(Linux)

进程间通信

  • 前言
  • 一、管道
    • 1. 管道原理
    • 2. 匿名管道
      • ①理解匿名管道
      • ②创建匿名管道——pipe
      • ③模拟实现进程池——管道
    • 3. 命名管道
      • ①理解命名管道
      • ②使用命名管道——mkfifo
        • 拓展 —— 日志
        • 俩无关进程通信
    • 3. 小结
      • ①管道总结
      • ②拓展命令和接口
  • 二、System V
    • 1. 共享内存
      • ①原理
      • ②使用共享内存
        • 接口介绍
        • 使用
      • ③小结
    • 2. 消息队列
    • 3. 信号量
      • ①引入
      • ②原理
    • 4. 总结

前言

1. 概念

进程间通信(IPC):不同进程之间进行数据交换和信息传递的过程。

  • 因为进程独立性的原因,所以进程之间通信是有成本的。
    成本:需要通过特定的机制来实现,这个过程会增加额外的开销和复杂性。还有数据传输、同步、互斥等方面的开销。

2. 目的

其实概念就是IPC存在的目的。
进一步解释:

  • 数据传输:一个进程需要把数据传输到另一个进程。
  • 资源共享:多个进程共享同一个资源
  • 通知:一个进程通知另一个(不限于一个)发生了某些事件。(eg:子进程退出,要通知父进程)
  • 进程控制:控制进程希望拦截被控制进程的异常,并及时知道它的状态改变
  • 协同:… (eg:信号量)

3. 怎么做

  • 让不同的进程看到同一份“资源” —— 特定形式的内存空间
  • “资源”一般是由OS提供。因为如果是通信双方的进程提供,那这一份资源本质就是这个进程独有的,会破坏进程间的独立性
  • 进程通信本质就是访问OS,进程代表的就是用户。 “资源”的 创建 -> 使用 -> 释放 ——>都离不开system call (因为群众有坏人,OS不放心把底层暴露出来)。

4. 分类

  • system call -> 从底层设计,接口设计,都要由OS设计。一般OS会有独立的通信模块(IPC通信模块)——> 隶属文件系统
  • 既然出现通信,就要制定标准,进程间通信的标准:
    • System V(本机通信)
    • POSIX (网络方便)

本文先介绍的是System V和基于文件级别的通信方式——管道
System V IPC:

  • System V 共享内存(主讲)
  • System V 消息队列(介绍原理)
  • System V 信号量(介绍原理)

一、管道

因为设计是数据从一端进去从另一端出去,是一种单向的通信方式,所以命名为管道

1. 管道原理

管道:基于文件级别的通信方式
原理图:介绍如何基于文件
基于文件的进程间通信

再叙述一下:

  1. 文件本身是不会继承下去的,被子进程继承的是文件描述符,所以通过文件描述符父子(具有血缘关系的)进程就可以对同一份资源进行访问。这就是管道的前提条件
  2. 因为进程间通信的数据基本不需要落盘,所以内存级文件通信提高了效率,减少拷贝和与外设的交互

注:因为和文件有关,这里涉及文件的知识就不再介绍,想要了解可以看另一篇文章IO Linux

2. 匿名管道

①理解匿名管道

匿名管道:用于父子进程、兄弟进程,具有亲缘关系的进程之间通信。管道是一种单向通信方式,数据只能在一个方向上流动。

简化图:
管道原理简化图

匿名管道的特殊性导致:

  1. 只能用于共同祖先的进程(具有亲缘关系)之间进行通信。通常一个管道由一个进程创建,然后fork,此后父子进程就都可以使用该管道。
  2. 不能双向通信,所以想要双向通信,再建立一条匿名管道
  3. 到这里只是让通信进程看到了同一份“资源”,还没有进行通信。所以因为进程独立性,进程间通信是需要成本的。

注:
数据为脏:无论读写都要先把数据加载到内存中。如果进行修改,在内存中修改好,此时内存中的数据和磁盘上的数据不一致,此时内存中的就是脏数据

②创建匿名管道——pipe

系统调用介绍:pipe

头文件:
	#include <unistd.h>

函数声明:
	int pipe(int pipefd[2]);

函数参数:
	pipefd[2]:两个整型元素的数组。输出型参数:这两个元素带出来的是管道的文件描述符。
	pipefd[0]:读端的文件描述符
	pipefd[1]:写端的文件描述符

返回值:
	1. 成功返回0
	2. 失败返回-1,错误码被设置

输出型参数pipefd[2]:根据上面匿名管道的简化图,可以看到想要建立任取一方读或写的单向通信。就需要在父进程时,以读和写两种方式打开管道文件然后fork,子进程继承这两种方式,然后根据需要再关闭通信进程对应一端。

使用系统调用——pipe:

介绍下面这串代码的逻辑:
创建管道 -> 子进程继承,然后关闭子进程的读端和父进程的写端。子进程每秒写一条信息,父进程读。对子进程的写和父进程的读都进行封装,读写方法我会单独拿出来验证管道的四种状态

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cerrno>

using namespace std;

#define SIZE 1024


// child -> w
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t child_pid = getpid();
    int number = 0;  //展现数据是变化的

    char buffer[SIZE];
    while (true)
    {
        buffer[0] = 0; // 字符串情况,告诉阅读代码的人,这个数组被当成字符串使用
        // 通过上面三个数据构建字符串
        snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), child_pid, number++);

        // 发送/写入给父进程信息 system call
        write(wfd, buffer, strlen(buffer));
        sleep(1); //一秒写一条信息
    }
}

// father -> r
void Reader(int rfd)
{
    char buffer[SIZE];
    while (true)
    {
        buffer[0] = 0; // 告诉阅读代码的人,这个数组被当成字符串使用
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if (n < 0)
            break;
        else if (n == 0)
        {
            printf("read file done!\n");
            break;
        }
        else
        {
            buffer[n] = 0; // 因为是字符串,所以读出来要注意
            cout << "father get a message[" << getpid() << "]#" << buffer << endl;
        }
    }
}

int main()
{
	//创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n < 0)
        exit(errno);  //创建失败直接退出

    // child -> w,  father -> r
    pid_t id = fork();
    if (id < 0)
        exit(errno);
    else if (id == 0)
    {
        // child 关闭读端
        close(pipefd[0]);
        // 写方法
        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);
    // 读方法
    Reader(pipefd[0]);

    // wait child process
    pid_t rid = waitpid(id, nullptr, 0);
    if (rid < 0)
        exit(errno);
    close(pipefd[0]);
    return 0;
}

运行结果:
运行结果

管道的四种情况:

  1. 读写端正常 ,写方法改成从键盘输入,其余代码不变

改变写方法代码:

void Writer(int wfd)
{
    pid_t child_pid = getpid();

    char buffer[SIZE];
    while (true)
    {
        buffer[0] = 0; 
        cout << "Please Enter@";
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strlen(buffer) - 1] = '\0';      //把换行符去掉

        // 发送/写入给父进程信息 system call
        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}

实验结果:
运行结果
现象:

  • 在子进程输入数据然后向父进程发送消息,父进程进行读取,再一次写的时候,上一次信息清空(调整位置),父进程读取新内容
  • 子进程写完之后父进程读取,因此子进程未写之前,父进程读端处于阻塞状态
  1. 读写端正常,写端写满

改变读写方法代码:

void Writer(int wfd)
{
    int cnt = 0;
    while(true)
    {
        cout << cnt++ << endl;
        write(wfd, "c", 1);
    } 
}
void Reader(int rfd)
{
    while(true)
    {
        sleep(5);
        break;
    }
}

实验结果:
实验结果
现象:

  • 写端写满,写端陷入阻塞,管道写入了65536字节,即64KB
  • 不同内核,可能有一定差别
  1. 读端正常,写端关闭

改变写方法代码:

void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t child_pid = getpid();
    int cnt = 3;
    char buffer[SIZE];
    while (cnt)
    {
        buffer[0] = 0; 
        snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), child_pid, cnt--);
        write(wfd, buffer, strlen(buffer));

		//读三次之后关闭文件描述符
        if(cnt == 0)
        {
            close(wfd);
            cout << "wfd close!!" << endl;
        }
        sleep(1);
    }
}

实验结果:
实验结果
现象:

  • 正常读写三次之后,写端关闭,读端会返回0,表面读到文件尾,不会被阻塞
  1. 写端正常,读端关闭

改变读方法代码,并且在父进程等待处获取子进程退出状态

void Reader(int rfd)
{
    char buffer[SIZE];
    int cnt = 3;
    while (cnt)
    {
        buffer[0] = 0; // 告诉阅读代码的人,这个数组被当成字符串使用
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if (n < 0)
            break;
        else if (n == 0)
        {
            printf("read file done!\n");
            break;
        }
        else
        {
            buffer[n] = 0; // 因为是字符串,所以读出来要注意
            cout << "father get a message[" << getpid() << "]#" << buffer << endl;
        }

        cnt--;
        if(cnt == 0)
        {
            close(rfd);
        }
    }
}



	//在main函数最后多加了几行的内容 ——> 获取子进程退出状态
	int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid < 0)
        exit(errno);
    cout << "wait success pid:" << id;
    cout << " exit code:" << ((status >> 8)&0xFF) << " exit signal:" << (status&0x7F) << endl;

实验结果:
实验结果
现象:

  • 首先我们没有对写代码做任何更改,只是读端被我们读了三次关闭了。结果OS杀掉正在写入的进程,子进程异常退出。
  • 父进程等待子进程,获取的子进程退出信息,退出码没有问题,退出信号是13号信号
    13号信号

③模拟实现进程池——管道

注:我们可以使用管道模拟实现进程池

由父进程创建一批进程,父子进程之间建立管道,当父进程有任务时,给子进程发送信号,让指定的子进程去完成该任务。进程池代码实现链接:本质上这个进程池的实现,体现了OS底层的模式:先描述再组织的思想。首先创建一个结构体进行描述,然后使用C++的容器进行管理。
简易图解:
图解
每次有任务时,父进程只需要把任务码通过管道发送给空闲的子进程。(判断是否是空闲的子进程,可以在结构体中添加一些属性,然后派送任务的时候进行遍历,判断该进程是否空闲)

3. 命名管道

①理解命名管道

命名管道:命名管道是一种特殊的管道,可以在无关的进程之间进行通信。命名管道是一种有名字的通信方式,可以通过文件系统中的路径来访问。

②使用命名管道——mkfifo

  1. 命名管道可以直接使用命令创建
    mkfifo [filename]
  2. 也可以使用系统调用接口mkfifo

注:

  1. Linux下,创建一个命名管道(也称为FIFO)时,无论用户设置的权限是什么,最终创建出来的命名管道的权限都会被内核自动修改为0666(即所有用户都有读写权限)。
  2. 管道创建出来,属于内存级文件,内容不会进行落盘,所以管道不占用磁盘空间
拓展 —— 日志

日志简单介绍:
日志信息

日志类实现的一些函数、宏和结构体介绍:
time、localtime、struct tm、snprintf、可变参数(va_list、va_start、va_end)、vsnprintf
注:可变参数,至少要有一个具体参数

//time —— 返回时间戳
头文件:
	#include <time.h>

函数声明:
	time_t time(time_t *t);

参数:
	t:输出型参数,放的是当前的时间戳

函数返回值:
	1. 成功返回时间戳
	2. 失败返回-1,错误码被设置
注:使用时,参数设为nullptr即可

time使用:
#include <iostream>
#include <time.h>

int main()
{
    time_t t1  = 0;
    time_t t2 = time(&t1);
    std::cout << "t1:" << t1 << " t2:" << t2 << std::endl;
    return 0;
}

//output:
//t1:1710156476 t2:1710156476

//
//localtime 和 struct tm
头文件:
	#include <time.h>

函数声明:
	struct tm *localtime(const time_t *timep);

参数:
	timep:指向要转换成本地时间的时间戳

返回值:
	1. 成功返回结构体指针(struct tm),包含了转换后的本地之间信息。
	2. 失败返回nullptr
		struct tm定义:
		struct tm 
		{
		    int tm_sec;   // 秒,范围为 0-59
		    int tm_min;   // 分,范围为 0-59
		    int tm_hour;  // 时,范围为 0-23
		    int tm_mday;  // 一个月中的日期,范围为 1-31
		    int tm_mon;   // 月份,范围为 0-11
		    int tm_year;  // 年份,从 1900 年开始
		    int tm_wday;  // 一周中的天数,范围为 0-6 (0 表示周日)
		    int tm_yday;  // 一年中的天数,范围为 0-365
		    int tm_isdst; // 夏令时标识符,负数表示不可确定,0 表示不使用夏令时,正数表示使用夏令时
		};
localtime使用:
#include <iostream>
#include <time.h>
#include <cstdio>

int main()
{
    time_t t = time(nullptr);
    struct tm *ctime = localtime(&t);
    printf("%d-%d-%d %d:%d:%d\n", ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
             ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
    return 0;
}

//output:
// 2024-3-11 19:47:31

/
//snprintf
头文件:
	#include <stdio.h>

函数声明:
	int snprintf(char *str, size_t size, const char *format, ...);

参数:
	1. 区别sprintf,snprintf限制输出的字符数
	2. 将可变参数(...)按照format格式化成字符串,然后复制到str中
	3. size:要写入的大小,超过size会被截断,最终写size-1大小,末尾添加'\0'

返回值:
	1. 成功返回想要写入str的长度,而不是实际写入的长度
	2. 失败返回负值

使用:
#include <cstdio>
 
int main()
{
    char buffer[50];
    const char* s = "runoobcom";
 
    int j = snprintf(buffer, 6, "%s\n", s);
 
    printf("string:%s\ncharacter count = %d\n", buffer, j);
 
    return 0;
}

// string:
// runoo
// character count = 10

/
//可变参数va_list、va_start(ap, last_arg)、va_end(ap)、va_arg(ap,type)
头文件:
	#include <stdarg.h>

四个宏:
	1. va_list:实际就是char*类型
	2. va_start:初始化可变参数列表,ap是va_list类型的变量,last_arg是最后一个固定参数的名称(可变参数裂变前一个参数)。目的是为了将ap指向可变参数列表中第一个参数
	3. va_end:结束可变参数裂变访问,ap置为nullptr
	4. va_arg:获取可变参数列表中下一个参数,ap是一个va_list类型的变量,type是下一个参数的类型。返回值是type类型的值,并将ap指向下一个参数

可变参数使用:
int sum(int n, ...)
{
    va_list s;   // va_list <==> char*
    va_start(s, n);
    int sum = 0;
    while (n--)
    {
        sum += va_arg(s, int);
    }
    va_end(s); // s=NULL
    return sum;
}
int main()
{
    int c = sum(4, 1, 2, 3, 4);
    int d = sum(5, 1, 2, 3, 4, 5);
    cout << "c = " << c << ", d = " << d <<endl;
    return 0;
}
//output: c = 10, d = 15

/
//vsnprintf
头文件:
	#include <stdio.h>

函数声明:
	int vsnprintf(char *str, size_t size, const char *format, va_list ap);

参数:
	1. str:用于存储格式化后的字符串
	2. size:字符数组大小
	3. format:类似printf函数中的格式化字符串
	4. ap:va_list类型的参数列表,包含要格式化的数据

返回值:
	1. 成功返回想要写入str的长度,而不是实际写入的长度
	2. 失败返回负值

vsnprintf使用:
#include <cstdio>
#include <stdarg.h>

void my_vsnprintf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    char buffer[100]; 
    int n = vsnprintf(buffer, sizeof(buffer), format, args);

    va_end(args);
    printf("string:%s return val:%d\n", buffer, n);
}

int main() 
{
    my_vsnprintf("Hello, %s Size: %d", "world", 42);
    return 0;
}

//output:
// string:Hello, world Size: 42 return val:21

实现一个简单的日志类:
文件名:log.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <cstdarg>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// 定义日志等级
#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

// 输出方式
#define Screen 1
#define OneFile 2
#define SortFile 3

#define LOG_MODE 0666
#define SIZE 1024

// 输出到一个文件的文件名   输出到多个文件时,可以加日志等级作为后缀,进行区分
#define LogFile "log.txt"

class Log
{
public:
    Log(int printMethod = Screen, std::string path = "./log/")
        : _printMethod(printMethod), _path(path)
    {}

    // 改变输出方式,使用者设置
    void Enable(int method)
    {
        _printMethod = method;
    }

    // 根据等级转字符串
    std::string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Waring:
            return "Waring";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void PrintLog(int level, const std::string &logtxt)
    {
        switch (_printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case OneFile:
            PrintOneFile(LogFile, logtxt);
            break;
        case SortFile:
            PrintSortFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    // 写在一个文件中
    void PrintOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = _path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, LOG_MODE);
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

	//根据日志等级不同,写在多个文件中
    void PrintSortFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += '.';
        filename += LevelToString(level);
        PrintOneFile(filename, logtxt);
    }

    // 可变参数
    void operator()(int level, const char *format, ...)
    {
        // 获取时间
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", LevelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // std::cout << logtxt << std::endl;
        PrintLog(level, logtxt);
    }

    ~Log()
    {}

private:
    int _printMethod;
    std::string _path;
};
俩无关进程通信

mkfifo接口介绍:

头文件:
	#include <sys/types.h>
    #include <sys/stat.h>

函数声明:
	int mkfifo(const char *pathname, mode_t mode);

函数参数:
	1. pathname:要创建的所在路径+管道名
	2. mode:所创建管道的权限
	
返回值:
	1. 成功返回0
	2. 失败返回-1,错误码被设置

使用: 实现两个无关的进程间的通信,使用命名管道,同时引用我们上面刚刚实现的日志类

首先要先看到同一份资源:
comm.hpp

#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <iostream>

//创建的管道文件名 + 权限
#define FIFO_FILE "./myfifo"
#define MODE 0666
#define MAX_SIZE 1024

//程序退出码都封装在枚举中
enum exit
{
    FIFO_CREAT_ERR = 1,
    FIFO_DESTROY_ERR,
    FIFO_OPEN_ERR,
    FIFO_READ_ERR,
    FIFO_WRITE_ERR
};


//把创建管道和销毁管道,封装成一个类
class InitPipe
{
public:
    //创建管道
    InitPipe()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }
    }

    // 销毁管道
    ~InitPipe()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DESTROY_ERR);
        }
    }
};

服务端:server.cc

#include "comm.hpp"
#include "log.hpp"

int main()
{
    // 借用命名管道类,实现创建和销毁管道
    InitPipe Init;

	//使用日志类定义对象,并选择输出方式
    Log log;
    log.Enable(OneFile);
    
    log(Info, "already creat pipe success!");
    
    // 打开管道 以读的方式
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        log(Fatal, "error string:%s, errno code:%d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

	//这里主要是为了测试日志类,下面这四条语句
    log(Info, "server already open fifo_file, error string:%s, errno code:%d", strerror(errno), errno);
    log(Waring, "server already open fifo_file, error string:%s, errno code:%d", strerror(errno), errno);
    log(Fatal, "server already open fifo_file, error string:%s, errno code:%d", strerror(errno), errno);
    log(Debug, "server already open fifo_file, error string:%s, errno code:%d", strerror(errno), errno);

    // 开始通信
    while (true)
    {
        char buffer[MAX_SIZE] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if (x > 0)
        {
            buffer[x] = 0;
            std::cout << "client say:" << buffer << std::endl;
        }
        else if (x == 0)
        {
            log(Debug, "client quit, server too!error string:%s, errno code:%d", strerror(errno), errno);
            break;
        }
        else
        {
            perror("read");
            exit(FIFO_READ_ERR);
        }
    }

    close(fd);
    return 0;
}

客户端:client.cc

#include "comm.hpp"
#include "log.hpp"

int main()
{
    // 服务端创建好管道,客户端打开即可
    // 打开管道
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }
    log(Info, "client already open fifo_file!");

    std::string line;
    // 通信
    while (true)
    {
        std::cout << "Please Enter@ ";
        getline(std::cin, line);
        int w = write(fd, line.c_str(), line.size());
        if(w < 0)
        {
            perror("write");
            exit(FIFO_WRITE_ERR);
        }
    }

    close(fd);
    return 0;
}

3. 小结

①管道总结

  1. 管道的四种状态:
    • 读写端正常,管道为空,读端阻塞
    • 读写端正常,管道被写满,写端阻塞
    • 读端正常,写端关闭,读端就会读到0,表示读到文件结尾,不会被阻塞
    • 写端正常,读端关闭,OS就会杀掉正在写入的进程,使用13号信号杀掉
  2. 管道的特征:
    • 进程间会进行协同:内核会对管道操作进行同步和互斥——保护管道文件的数据安全
    • 管道是面向字节流的
  3. 匿名管道和命名管道的区别:
    • 匿名管道由pipe接口创建,并打开,后续要关闭相应的文件描述符。命名管道由mkfifo接口创建,打开要使用open,要决定用读还是写的方式打开
    • 匿名管道只有具有亲缘关系的进程才能进行通信。命名管道,无关的进程间也可以通信
    • 管道只能单向通信
    • 管道是基于文件的,文件的声明周期是随进程的,所以管道的生命周期随进程。(匿名管道的声明周期随进程,命名管道的声明周期要显示的删除)
  4. 原子性:
    • 当要写入的数据量小于PIPE_BUF时,Linux保证写入的原子性
    • 当要写入的数据量大于PIPE_BUF时,Linux不再保证写入的原子性

注:
PIPE_BUF——大概意思就是向管道写入的内容小于PIPE_BUF就是原子的,PIPE_BUF的大小是4KB。
原子性: 是一个操作要么完全执行成功,要么完全不执行 PIPE_BUF

②拓展命令和接口

  1. ulimit -a OS对一些重要资源的限制
    ulimit -a

  2. 写代码时最好有一套规范,参数传参规范:
    - 输入型参数: const &
    - 输出型参数: *
    - 输入输出型参数:&

  3. unlink删除文件,不论是管道文件还是普通文件,软链接和硬链接等都可以删除。

  4. gettimeofday也可以获取时间

二、System V

1. 共享内存

共享内存是最快的IPC形式,允许多个进程共享同一块内存区域。进程可以直接读写共享内存中的数据,不再涉及到内核(不需要执行内核的系统调用来传递彼此的数据),也无需进行数据拷贝。

①原理

共享内存看到同一份资源的原理
图解:
图解

  1. 上图的内容可以理解为创建共享内存,让进程拿到这块内存。用完还是要和进程地址空间去关联,然后释放共享内存。这些操作只能由OS来做 —— 系统调用
  2. OS中肯定不止一份共享内存,所以OS就要对共享内存进行管理——先描述,再组织(后面对System V总结时,介绍其内核结构体)
  3. 共享内存映射到进程地址空间的共享区,并且会把映射的起始虚拟位置返回给上层用户
  4. 因为这块内存空间是映射到不同进程的进程地址空间内,可以直接使用(都以为是自己的),通信时不需要更多的拷贝。
  5. 共享内存的生命周期是随内核的,需要手动关闭,如果忘记也是内存泄漏

②使用共享内存

接口介绍

1. shmget和ftok: 共享内存的创建

/ftok///
头文件:
	#include <sys/types.h>
	#include <sys/ipc.h>

函数声明:
	key_t ftok(const char *pathname, int proj_id);

参数:
	1. pathname:指向路径(现有,可访问的路径)的字符串
	2. proj_id:自定义整数

返回值:
	1. 成功,返回key_t类型的唯一键值
	2. 失败,返回-1,错误码被设置


/shmget///
头文件:
	#include <sys/ipc.h>
    #include <sys/shm.h>

函数声明:
	int shmget(key_t key, size_t size, int shmflg);

参数:
	1. key:共享内存的内核标识符,通常使用ftok生成。(保证了让不同进程获得同一个共享内存)
	2. size:开辟共享内存的大小(单位字节)
	3. shmflg:创建方式
		IPC_CREAT(单独使用):没有就创建,有就返回
		IPC_CREAT | IPC_EXIT:有就出错返回,没有就创建。(保证是新创建的共享内存)
		IPC_EXIT:不单独使用
		在最后可以直接给共享内存|一个权限
返回值:
	1. 成功,返回共享内存的标识符
	2. 失败,返回-1,错误码被设置

上述两个接口的介绍:

  1. ftok:
    • ftok函数本质就是一套算法,将pathname转换成一个唯一的整数值,然后和proj_id再组合,生成唯一键值,用于创建、获取IPC对象的标识符。
    • key能让不同进程进行唯一性标识,第一个进程通过key(这个唯一键值)创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存
    • key类似路径(具有唯一性)
      注:生成的值可能存在冲突的风险。
  2. shmget:
    • 通过ftok接口获取的key,创建一个新的共享内存或者获取一个已存在的共享内存的shmid
    • shmid就是shmget的返回值,其是为了用户接下来控制共享内存。

上述两个函数的返回值对比:shmid和key
图解

测试shmget和ftok接口:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>

// 为了获取key值设置的两个参数 -> ftok
const std::string pathname = "/home/kpl_2023";
const int proj_id = 0x32231;

// 开辟共享内存的大小
// 共享内存的大小建议4096的整数倍
#define SIZE 4096

// 创建共享内存的权限.实际权限可能在OS内核中被限制和管理
#define SHM_MODE 0666

int main()
{

    // 获取key,用来创建共享内存
    key_t key = ftok(pathname.c_str(), proj_id);   // key_t 实际就是 int
    if (key < 0)
    {
        perror("ftok");
        exit(errno);
    }
    printf("get key success! key:0x%x\n", key);
    
    // 创建共享内存
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | SHM_MODE); //权限直接在最后的位置 |上即可
    if (shmid < 0)
    {
        perror("shmget");
        exit(errno);
    }
    printf("Create shm success! shmid:%d\n", shmid);

    return 0;
}

注:

  1. 查看系统中共享内存的命令
    ipcs -m
  2. 删除系统中共享内存的命令
    ipcrm -m [shmid]

测试结果:
运行结果

2. shmat和shmdt: 共享内存操作

/shmat///
头文件:
	#include <sys/types.h>
	#include <sys/shm.h>

函数声明:
	void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:
	1. shmid:共享内存标识(shmget成功的返回值)
	2. shmaddr:指定连接的地址(一般设置成nullptr,由OS自己分配。如果设置了,并且该被挂接的地址空间位置又被占用,返回-13. shmflg:进程映射共享内存的方式,一般设置成0。两个可能取值:
		SHM_RND:这可以提高内存访问的效率。
		SHM_RDONLY:进程只能读取共享内存中的数据,而不能修改数据。
		shmaddr不为nullptr且shmflg设置SHM_RND标记,则连接地址会自动向下调整SHMLBA的整数倍。

返回值:
	1. 成功,返回共享内存映射到进程地址空间的地址
	2. 失败,返回-1,错误码被设置

/shmdt///
头文件:
	#include <sys/types.h>
	#include <sys/shm.h>

函数声明:
	int shmdt(const void *shmaddr);

参数:
	shmaddr:共享内存映射到进程地址空间的地址

返回值:
	1. 成功返回0
	2. 失败返回-1,错误码被设置

上述两个接口的作用

  1. shmat:将共享内存段连接到进程地址空间
  2. shadt:将共享内存段与当前进程脱离

3. shmctl: 共享内存控制

头文件:
	#include <sys/ipc.h>
    #include <sys/shm.h>

函数声明:
	int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:
	1. shmid:共享内存标识(shmget成功的返回值)
	2. cmd:要控制的动作:IPC_STAT、IPC_SET、IPC_RMID
		IPC_STAT:获取共享内存的状态信息,放在buf中
		IPC_SET:在进程有足够的权限下,把共享内存当前的管理值设置为shmid_ds数据结构中给出的值
		IPC_RMID:删除共享内存段
	3. buf:根据选项不同可以是输入型参数也可以是输出型参数,指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:
	1. 成功,返回0
	2. 失败返回-1,错误码被设置	

可以删除共享内存、获取共享内存的信息

使用

两个进程间的通信,使用共享内存

首先创建同一份资源:
comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>


// 为了获取key值
const std::string pathname = "/home/kpl_2023";
const int proj_id = 0x32231;

// 开辟共享内存的大小
// 共享内存的大小简易4096的整数倍
#define SIZE 4096

// 创建共享内存的权限
#define SHM_MODE 0666

enum exit
{
    SHM_FTOK_ERR = 1,
    SHM_SHMGET_ERR,
    SHM_SHMAT_ERR,
    SHM_SHMDT_ERR,
    SHM_SHMCTL_IPC_RMID_ERR,

    FIFO_CREATE_ERR = 10,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

// 获取key
key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id); // key_t 实际就是 int
    if (key < 0)
    {
        perror("ftok");
        exit(SHM_FTOK_ERR);
    }
    return key;
}

// 获取shmid
int GetShareMemHelper(int flag)
{
    key_t key = GetKey();
    int shmid = shmget(key, SIZE, flag);
    if (shmid < 0)
    {
        perror("shmget");
        exit(SHM_SHMGET_ERR);
    }

    return shmid;
}

// 给服务端的接口
int CreatShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | SHM_MODE);
}

// 给客户端的接口
int GetShm()
{
    return GetShareMemHelper(IPC_CREAT);
}

#endif

进程1:
share_mema.cc

#include "comm.hpp"

int main()
{
    //创建共享内存
    int shmid = CreatShm();

    // shm挂接到进程地址空间 ->  返回的时挂接在进程地址空间处的地址
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if (*shmaddr < 0)
    {
        perror("shamt");
        exit(SHM_SHMAT_ERR);
    }


    // OS提供的结构体
    struct shmid_ds shmds;
    // 通信
    while (true)
    {
        std::cout << "client say@ " << shmaddr << std::endl; // 直接访问地址即可
        sleep(1);

        shmctl(shmid, IPC_STAT, &shmds);
        std::cout << "shm size: " << shmds.shm_segsz << std::endl;
        std::cout << "shm nattch: " << shmds.shm_nattch << std::endl;
        printf("shm key: 0x%x\n", shmds.shm_perm.__key);
    }

    // 去挂接
    int shmdtid = shmdt(shmaddr);
    if (shmdtid < 0)
    {
        perror("shmdt");
        exit(SHM_SHMDT_ERR);
    }
    // 删除共享内存
    int rmid = shmctl(shmid, IPC_RMID, nullptr);
    if (rmid < 0)
    {
        perror("shmctl::IPC_RMID");
        exit(SHM_SHMCTL_IPC_RMID_ERR);
    }

    return 0;
}

进程2:
share_memb.cc

#include "comm.hpp"

// 客户端不能先运行,因为我们没有让它创建共享内存,所以没有设置权限,
//  所以shmat挂接不上会出现段错误

int main()
{
    //获取共享内存shmid
    int shmid = GetShm();

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if (*shmaddr < 0)
    {
        perror("shamt");
        exit(SHM_SHMAT_ERR);
    }

    std::cout << "shmat success !!!" << std::endl;

    // 通信
    while (true)
    {
        std::cout << "Please Enter@ ";
        fgets(shmaddr, SIZE, stdin);
    }

    // 去挂接
    int shmdtid = shmdt(shmaddr);
    if (shmdtid < 0)
    {
        perror("shmdt");
        exit(SHM_SHMDT_ERR);
    }
    return 0;
}

③小结

  1. 共享内存没有同步互斥之类的保护机制
  2. 共享内存是所有进程间通信中,速度最快的,拷贝少
  3. 共享内存内部的数据,由用户自己维护

2. 消息队列

  1. 消息队列是一种消息传递机制。消息队列可以实现进程之间的异步通信。
  2. 允许不同的进程,向内核中发送带类型的数据块,接收者进程接收的数据块可以有不同类型。
  3. 看到同一份资源:不同进程看到同一个队列(内核中)
  4. 消息的生命周期是随内核的,需要手动删除,如果忘记也是内存泄漏

原理:
图解

内核中,OS管理着消息队列,进程(用户)想要使用这块资源,OS一定会提供系统调用给用户。

3. 信号量

信号量是一种用于进程同步和互斥的机制,可以用来解决进程之间的竞争条件和临界区问题。
信号量的生命周期是随内核的,需要手动删除,如果忘记也是内存泄漏

信号量是进程间通信的原因:

  1. 通信不仅仅是数据的交互,互相协同也是
  2. 信号量可以被所有通信进程看到

①引入

  1. 数据不一致问题: A、B进程看到同一份资源(共享资源),如果不加保护,会导致数据不一致问题。eg:共享内存
  2. 互斥: 任何时刻只允许一个执行流访问共享资源。加锁——互斥访问
  3. 临界资源: 共享资源,任何时刻只允许一个执行流访问——一般是内存空间
  4. 临界区: 访问临界资源的代码

②原理

1. 理解信号量

信号量的本质就是一把计数器(描述临界资源中资源数量的多少)
图解
信号量解释:

  1. 所以申请计数器成功,那就代表还有资源,具有访问资源的权限
  2. 申请计数器资源,并不表示我(执行流)访问要的资源了,而是对资源的预定机制
  3. 计数器可以有效保证进入这一份资源(共享资源)的执行流数量

所以每个执行流,想要访问共享资源的一部分时,不能直接访问,而是要先申请计数器资源,而这个计数器就是信号量

2. 二元信号量(锁)

当我们把上面那个临界资源当成一个整体,那么计数器cnt就是1。这就只能一个执行流访问,当再来执行流时计数器是0,就不能再有执行流访问了——互斥
所以,我们把值只能为1、0两态的计数器叫做二元信号量——本质就是一个锁

3. 共享资源——信号量:

  1. 要访问临界资源,就得先申请信号量(计数器)资源,它保护着临界资源。但是计数器也被很多执行流共享,所以也是共享资源,那也得被保护。
  2. 申请计数器本质就是对计数器减一,但是虽然在C语言上计数器减一是一条语句,但是转成汇编就是三条(也可能多条)汇编。进程在运行时随时被切换,所以这减一操作是不安全的
  3. 所以为了保护这个信号量资源,在底层做了工作
    • P操作:申请信号量,本质是对计数器减一
    • V操作:释放资源,释放信号量,本质是对计数器加一

结论: 信号量的PV操作都是原子的——要么不做,要么做完

4. 总结

1. 共享内存、消息队列和信号量的接口:
接口

观察发现三者的接口相似,原因:System V标准规定
所以:

  1. 消息队列
    • 查看系统中的消息队列的命令
      ipcs -q
    • 删除系统中消息队列的命令
      ipcrm -q [msgid]
  2. 信号量
    • 查看系统中信号量的命令
      ipcs -s
    • 删除系统中信号量的命令
      ipcs -s [semid]
  1. IPC在内核中的数据结构设计:

图解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/451988.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

9、设计模式之组合模式(Composite)

一、什么是组合模式 组合模式也成为整体部分模式&#xff0c;是一种结构型设计模式。它将对象组合成树形的层次结构&#xff0c;用来表示“整体-部分”的关系。通过组合模式&#xff0c;我们可以使用相同的方式处理单个对象和多个对象组合。 二、角色组成 组件&#xff08;Com…

Unity URP 如何写基础的几何着色器

这是使用几何着色器在点中心生成一个点并根据这个点把原本的面片分成三个三角形的操作。 对于几何着色器构造相对简单&#xff0c;网上的信息也相对较多&#xff0c;需要注意的点就是需要提供一个新的数据结构供几何着色器输出&#xff0c;因为几何着色器在顶点之后&#xff0…

properties文件和yml文件的区别以及文件优先级

properties文件和yml文件的区别 yml是按照缩进关系&#xff0c;而properties用"."来表示关系springboot默认生成的是properties文件当properties文件和yml文件都存在时&#xff0c;properties文件的优先级更高。 properties文件的样式 yml文件的样式 文件优先级 r…

使用 Jenkins 和 Spinnaker 构建 Kubernetes CI/CD

无论您是新手还是持续集成和持续交付以及容器化领域的经验丰富&#xff0c;本文都将为您提供设置 Spinnaker 以满足您的软件应用程序交付需求的基本知识。 了解 Jenkins、Spinnaker 和 Kubernetes Kubernetes 和 Jenkins 是两个强大的工具&#xff0c;它们相互配合&#xff0…

新加坡大带宽服务器托管优势

在数字化快速发展的今天&#xff0c;服务器托管成为企业拓展业务、提高服务质量的关键环节。而新加坡作为一个国际性的金融、贸易和科技创新中心&#xff0c;其大带宽服务器托管服务在全球范围内享有盛誉。本文将为您科普新加坡大带宽服务器托管的诸多优势。 首先&#xff0c;新…

第十五届蓝桥杯(Web 应用开发)模拟赛 3 期-大学组(被题目描述坑惨了)

目录 1.创意广告牌 2.原子化css 3.神秘咒语 4.朋友圈 5.美食蛋白揭秘 6.营业状态变更 7.小说阅读器 8.冰岛人 9.这是一个”浏览器“ 10.趣味加密解密 总结 1.创意广告牌 这个题目不多说了&#xff0c;只要知道这些css应该都能写出来&#xff0c;不会的平时多查查文…

Docker部署ChatGLM3、One API、FastGPT

创建并运行chatglm3容器 docker run --name chatglm3 -p 8000:8000 registry.cn-hangzhou.aliyuncs.com/ryyan/chatglm.cpp:chatglm3-q5_1 创建并运行one-api容器 (其中挂载路径 D:\one-api 可以选择你自己喜欢的目录) docker run --name oneapi -d -p 3000:3000 -e TZAsia…

k8s基本使用(namespace,pod增删查)-持续更新中

目录 1. 查看Namespace 2. 创建Namespace 2.1 使用纯命令行创建 2.2 编写yaml文件创建 3. 删除Namespace 3.1 使用纯命令行删除 3.2 使用yaml文件删除 二、Pod 1. 查看pod 1.1 查看默认空间的pod 1.2 查看指定空间的pod 1.3 查看全部pod 1.4 查看pod更多信息 1…

HUAWEI 华为交换机 配置 MAC 地址漂移检测示例

组网需求 如 图 2-17 所示&#xff0c;网络中两台 LSW 间网线误接形成了网络环路&#xff0c;引起 MAC 地址发生漂 移、MAC 地址表震荡。 为了能够及时检测网络中出现的环路&#xff0c;可以在 Switch 上配置 MAC 地址漂移检测功能&#xff0c; 通过检测是否发生MAC 地址漂移…

Python in Visual Studio Code 2024年3月发布

排版&#xff1a;Alan Wang 我们很高兴地宣布 2024 年 3 月发布适用于 Visual Studio Code 的 Python 和 Jupyter 扩展&#xff01; 此版本包括以下公告&#xff1a; 新的“Add Imports”代码操作设置调试 Django 或 Flask 应用时自动启动浏览器Python REPL 的 Shell 集成对本…

Python学习:基本数据类型

Python3 命令行参数 Python 提供了 getopt 模块来获取命令行参数。 %> python test.py arg1 arg2 arg3Python 中也可以所用 sys 的 sys.argv 来获取命令行参数&#xff1a; sys.argv 是命令行参数列表。 len(sys.argv) 计算命令行参数个数。 sys.argv[0] 表示脚本名。tes…

docker ENTRYPOINT [“sh“,“-c“,“java“,“-jar“,“Hello.jar“] 启动失败问题分析

因为没系统的学过linux语法&#xff0c;所以才会产生如下疑问。大佬请跳过。 问题&#xff1a;当在dockerfile里面配置 ENTRYPOINT ["sh","-c","java","-jar","Hello.jar"] &#xff0c;启动对应容器时会无法正常运行&…

剑指offer经典题目整理(四)

一、树的子结构 1.链接 树的子结构_牛客题霸_牛客网 (nowcoder.com) 2.描述 给两颗二叉树A B&#xff0c;判断B是不是A的子结构 3.思路 将问题拆解开来&#xff0c;首先是找到a树中子结构的位置&#xff0c;然后是判断是否相同&#xff0c;也就是说&#xff0c;我们需要去…

【小白学机器学习8】统计里的自由度

目录 1 自由度 /degree of freedom / df 1.1 物理学的自由度&#xff08;摘自网上 ^&#xff09; 1.2 数学里的自由度 1.2.2 统计里的自由度 1.2.3 需要补充线性代数的&#xff0c;用线性代数来理解自由度 1.3 统计学里自由度的定义 1.4 自由度的公式 1.5 线性回归公式…

stm32使用时钟生成PWM时调用__HAL_TIM_SetAutoreload导致PWM消失处理

stm32使用时钟生成PWM时调用__HAL_TIM_SetAutoreload导致PWM消失处理 这一个是配置的时候没有使用影子寄存器导致的, 如果加载的Autoreload的值比原来的这一个值小, 这是会出现一个问题, 如果计数器里面的值记为Count, 如果改变的时候New_Autoreload < Count < Old_Auto…

结构体的增删查改

结构体&#xff0c;是为了解决生活中的一些不方便利用c语言自带数据类型来表示的问题。例如表示一个学生&#xff0c;那么学生这个个体假如用c语言自带数据类型怎么表示呢。可以使用名字&#xff0c;也就是字符数组&#xff1b;也可以使用学号&#xff0c;也就是int类型。但是这…

Hadoop学习2:完全分布集群搭建

文章目录 Fully-Distributed Operation&#xff08;完全分布模式&#xff09; 重点机器环境同步集群规划配置文件修改以及同步步骤0&#xff1a;下面其他步骤涉及修改配置以这里为准&#xff08;要不然部署使用过程会有很多问题&#xff09;通用配置&#xff08;三台节点机器&a…

工具-百度云盘服务-身份认证

目标 通过百度网盘API的方式去获取网盘中的文件&#xff0c;要实现这的第一步就是需要获取网盘的权限。资料(参考) 如果期望应用访问用户的网盘文件&#xff0c;则需要经过用户同意&#xff0c;这个流程被称为“授权”。百度网盘开放平台基于 OAuth2.0 接入授权。OAuth2.0 是…

配置中心概述

目录 一、配置中心的定义 二、配置中心的工作 三、配置中心的作用 四、SpringBoot中的配置文件 一、配置中心的定义 配置中心就是用来管理项目当中所有配置的系统&#xff0c;也是微服务系统当中不可或缺的一部分。 nacos是一个更易于构建云原生应用的动态服务发现、配置管理…

服务器被大流量攻击怎么办?如何防御攻击?

随着网络的发展&#xff0c;我们所遇到的安全挑战也越来越多。尤其是近年来&#xff0c;网络攻击频发&#xff0c;许多互联网企业深受其扰。为了不影响自身业务的稳定运行&#xff0c;许多企业都在想方设法的寻求解决方案&#xff0c;防止服务器被攻击而影响业务发展。下面我们…