Linux:进程间通信

一、进程间通信的基本概念:

什么是进程间通信?我们都知道进程具有独立性,进程地址空间相互独立,每个进程都有各自的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,所以我们需要在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再把数据从内核缓冲区读走,内核提供的这种机制我们称之为进程间通信。

那么进程间通信如何来实现?实现进程间通信不能只有一种方式。那么下面我将列举出几种常见的进程间通信方式:

匿名管道、命名管道、共享内存、消息队列、信号。

管道是使用最简单的一种方式。下面我将从管道开始讲起。

二、匿名管道:

管道是一种最基本的ipc机制,匿名管道应用于有血缘关系的进程之间(不非得是父子进程,爷孙进程也可以),完成数据传递,那么如何创建一个管道?

提到创建管道,就不得不得到我们的pipe函数了。

cpp 复制代码
#include<unistd.h>
功能:创建一个匿名管道
int pipe(int fd[2]);
参数fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端。
返回值:如果成功则返回0,失败返回错误码。

fd0是读端, fd1是写端向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区

对于文件描述符,我这里再相对而言扩容一点知识:

cpp 复制代码
struct task_struct {
    // ... 进程的各种信息
    pid_t pid;                    // 进程ID
    struct files_struct *files;   // 关键:指向进程文件表
    // ...
};

首先,task_struct这个进程描述符里面有一个指针files,这个files指针指向了一个file_struct的结构体(也就是进程文件表),这个进程文件表,每个进程独有一份,而这个file_struct里面一个指针数组fd_array ,这个数组的每个对象是指向文件对象(也就是struct file)的指针,而我们所说的文件描述符,就是这个数组的下标。

所以:文件描述符的本质就是数组的下标,这个数组的基本元素是一个文件指针。

理解了上面的知识对应理解进程间通信绝对是相当于有了一个buff加持。

接着再谈匿名管道。

管道具有一下的特点:

1、管道的本质是一块内核缓冲区,上面我们提到了,这里不做过多的阐述。

2、由两个文件描述符引用,一个用来表示读端,一个用来表示写端。

3、规定从管道的写端流入管道,从读端流出管道。

4、管道的读端与写端默认都是阻塞的。

5、一般而言,进程退出,管道就进行释放,所以管道的生命周期随进程。

6、只能用于有公共祖先的进程之间进行通信。

7、管道是半双工的,数据只能从一个方向流到另一段,如果需要双方向通信,需要创建两个管道,来对应不同的方向。

8、一般而言,内核会对管道操作进行同步和互斥。

管道的原理:

管道的本质就是内核缓冲区,内部使用环形队列来实现。

默认缓冲区的大小为4k,可以使用ulimit-a的命令来获取大小。

父子进程使用匿名管道进行通信:

首先,我们使用pipe函数创建一个匿名管道,然后一般会fork一个字进程,然后通过我们创建出来的匿名管道来实现进程间消停,所以只要两个进程中存在血缘关系,这里的血缘关系不单指父子关系,只有具有公共的祖先都可以采用管道的方式来进行通信,父子进程就用相同的文件描述符,且指向同一管道,对于文件描述符,上述我做了详细的讲解,但是,没有血缘关系的进程不能获得pipe函数产生的两个文件描述符,所以就不能使用同一个管道进行通信。

第一步:

首先,父进程先创建管道:

第二步:父进程通过fork函数创建子进程:

第三步:父进程关闭读端,子进程关闭写端:

对于上述过程的总结:

首先,父进程通过pipe函数创建管道,得到两个文件描述符fd0,和fd1,分别指向管道的读端和写端。其次,父进程通过fork函数来创建子进程,那么子进程也就有了两个文件描述符,这两个文件描述符指向同一个管道,所以,父进程关闭管道读端,子进程关闭管道写端,父进程向管道内写入数据,那么子进程就可以从这个管道里面来读取数据,这样就实现了父子间的进程间通信,那么下面我们将用代码来验证这一说法:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <cstring>
#include <cstdio> 
#define SIZE 1024
using namespace std;

int main()
{
    // 先创建一个匿名管道
    int ret = -1;
    int fds[2];
    ret = pipe(fds);
    if (ret == -1)
    {
        perror("pipe");
        return 1;
    }
    
    // 创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork false");
        return 1;
    }
    else if(id == 0)
    {
        // 子进程负责读,关闭写端
        close(fds[1]);
        
        char buffer[SIZE];  // 添加buffer声明
        memset(buffer, 0, SIZE);  
        
        ret = read(fds[0], buffer, SIZE - 1);  // 留一个位置给字符串结束符
        if(ret > 0) {
            printf("child process buffer: %s\n", buffer);
        } else if(ret == 0) {
            printf("child: pipe closed by parent\n");
        } else {
            perror("child: read error");
        }
        
        // 关闭读端
        close(fds[0]);
        // 进程退出
        exit(0);
    }
    else
    {
        // 父进程负责写,关闭读端
        close(fds[0]);
        
        char buffer[SIZE];
        printf("parent: please input message: ");
        fgets(buffer, sizeof(buffer), stdin);
        
        // 去掉fgets可能读取的换行符
        size_t len = strlen(buffer);
        if(len > 0 && buffer[len-1] == '\n') {
            buffer[len-1] = '\0';
        }
        
        ret = write(fds[1], buffer, strlen(buffer));
        if(ret < 0) {
            perror("parent: write error");
        } else {
            printf("parent: sent %d bytes\n", ret);
        }
        
        // 关闭写端,让子进程知道写入结束
        close(fds[1]);
        
        // 等待子进程结束
        waitpid(id, nullptr, 0);
        printf("parent: child process exited\n");
    }
    
    return 0;
}

这样就完成了,父子进程之间的进程间通信。

需要注意的一点是,在进行读操作的时候,只要有数据,就能返回读出的字节数,但是如果没有数据:

在写端全部关闭的时候:

read会解除阻塞,返回0,表示这个文件读到了末尾。

读端没有全部关闭:

如果缓冲区写满了,write就进行阻塞,否则继续write。
进程间通信的前提是什么?是让两个不同的进程看到同一块资源,都看不到同一块资源或者说访问同一块资源,那么还怎么通信呢?

那么在匿名管道中,父子进程是如何看到同一块资源的?

通过继承,fork创建的子进程会赋值父进程的地址空间,包括打开的文件描述符,这意味着文件描述符会被复制到子进程中,指向同一个管道对象,管道在操作系统内核中维护一个缓冲区,文件描述符只是对这个缓冲区的引用,所以,父子进程可以通过继承的文件描述符访问同一个管道缓冲区,从而进行资源共享,也就是我们所说的看到同一块资源。

我们都知道,管道有一个读端和一个写端,那么如果写端不断写入,而读端不读取,会发生什么?

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;

int main()
{
    int fds[2];
    pipe(fds);

    if (fork() == 0)
    {
        // 子进程
        close(fds[1]);
        cout << "子进程:休眠中..." << endl;
        sleep(5);
        cout << "子进程:开始读取" << endl;
        char buf;
        read(fds[0], &buf, 1);  // 只读1个字节
        close(fds[0]);
        exit(0);
    }
    else
    {
        // 父进程
        close(fds[0]);
        
        // 创建一个大于管道缓冲区的大数据
        char* data = new char[100000];  // 100KB
        
        cout << "开始写入大量数据..." << endl;
        cout << "注意:当管道满时,下面的输出会卡住!" << endl;
        
        // 这个write调用会阻塞,因为数据量大于管道缓冲区
        write(fds[1], data, 100000);
        
        // 如果执行到这里,说明子进程开始读取了
        cout << "写入完成,子进程已开始读取" << endl;
        
        delete[] data;
        close(fds[1]);
        wait(nullptr);
    }

    return 0;
}

在上面的代码中,父进程也就是写端,尝试一次写入100000(100kb)字节的数据,但是管道缓冲区的大小是有限的,通常是64kb,所以当父进程写入的数据量超过缓冲区的大小的时候,写入操作会堵塞。子进程先休眠5秒,然后开始读取数据,在这5秒呢,父进程再写入过程中会因为管道满了而则色,所以当子进程休眠结束后并开始读取数据的时候,府进程的写入操作才能继续完成。

由此,我们可以得出如下结论:

写端在缓冲区满时会自动阻塞,等待读端读取数据,从而防止数据丢失。

言外之意,这个管道就像一根水管,在这根水管里面的水足够多的时候,要放点水出去,才能继续向里面注水。

那么如果将写端关闭,读端不断读取会发生什么现象呢?

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <cstring>
#include <cstdio>
#define SIZE 1024
using namespace std;

int main()
{
    // 先创建一个匿名管道
    int ret = -1;
    int fds[2];
    ret = pipe(fds);
    if (ret == -1)
    {
        perror("pipe");
        return 1;
    }

    // 创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork false");
        return 1;
    }
    else if (id == 0)
    {
        // 子进程负责读,关闭写端
        close(fds[1]);
        
        int read_count = 0;
        while (1)
        {
            char buffer[SIZE];
            memset(buffer, 0, SIZE);
            ret = read(fds[0], buffer, SIZE - 1);
            
            printf("child: read returned %d\n", ret);  // 打印返回值
            
            if (ret > 0)
            {
                printf("child process buffer: %s\n", buffer);
                read_count++;
            }
            else if (ret == 0)
            {
                printf("child: pipe closed by parent (read_count: %d)\n", read_count);
                break;
            }
            else
            {
                perror("child: read error");
                break;
            }
        }
        // 关闭读端
        close(fds[0]);
        // 进程退出
        exit(0);
    }
    else
    {
        // 父进程负责写,关闭读端
        close(fds[0]);
        
        // 只写入有限的数据
        for (int i = 0; i < 5; i++)
        {
            char buffer[SIZE];
            snprintf(buffer, SIZE, "Message %d from parent", i + 1);
            
            ret = write(fds[1], buffer, strlen(buffer));
            if (ret < 0)
            {
                perror("parent: write error");
            }
            else
            {
                printf("parent: sent %d bytes\n", ret);
            }
            sleep(1);  // 每秒写一次
        }

        cout << "父进程:关闭写端文件描述符" << endl;
        // 关闭写端,让子进程知道写入结束
        close(fds[1]);

        // 等待子进程结束
        waitpid(id, nullptr, 0);
        
        printf("parent: child process exited\n");
    }

    return 0;
}

当写端关闭后,读端的read调用会返回0,表示没有更多数据可读。

接着往下看。

如果子进程(也就是读端)进行休眠,那么父进程将做什么?父进程会进行等待,直到子进程休眠结束并开始读取数据。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <cstring>
#include <cstdio>
#define SIZE 1024
using namespace std;

int main()
{
    // 先创建一个匿名管道
    int ret = -1;
    int fds[2];
    ret = pipe(fds);
    if (ret == -1)
    {
        perror("pipe");
        return 1;
    }

    // 创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork false");
        return 1;
    }
    else if (id == 0)
    {
        // 子进程负责读,关闭写端
        close(fds[1]);
        
        cout << "子进程:开始休眠5秒..." << endl;
        sleep(5);  // 子进程休眠5秒
        cout << "子进程:休眠结束,开始读取数据" << endl;
        
        int count = 0;
        while (1)
        {
            char buffer[SIZE];
            memset(buffer, 0, SIZE);
            ret = read(fds[0], buffer, SIZE - 1);
            if (ret > 0)
            {
                printf("child process buffer: %s\n", buffer);
            }
            else if (ret == 0)
            {
                printf("child: pipe closed by parent\n");
                break;
            }
            else
            {
                perror("child: read error");
                break;
            }
        }
        // 关闭读端
        close(fds[0]);
        // 进程退出
        exit(0);
    }
    else
    {
        // 父进程负责写,关闭读端
        close(fds[0]);
        
        cout << "父进程:准备写入数据,但子进程在休眠..." << endl;
        
        // 父进程会阻塞在这里,因为子进程在休眠,没有读取数据
        // 当管道缓冲区满时,write会阻塞
        char buffer[SIZE] = "Hello from parent process!";
        ret = write(fds[1], buffer, strlen(buffer));
        if (ret < 0)
        {
            perror("parent: write error");
        }
        else
        {
            printf("parent: sent %d bytes\n", ret);
        }

        // 关闭写端,让子进程知道写入结束
        close(fds[1]);

        // 等待子进程结束
        waitpid(id, nullptr, 0);
        
        printf("parent: child process exited\n");
    }

    return 0;
}

在子进程休眠的这10秒中,父进程会一直进行等待,直到子进程结束休眠开始读取数据。

那么如何把这个阻塞改成非阻塞呢?

通过fcntl这个函数:

int fcntl(int fd,int cmd,...)

第一个参数就是文件描述符

第二个参数就是命令参数,比如F_GETFL - 获取文件状态标志,F_SETFL - 设置文件状态标志。

O_NONBLOCK - 非阻塞模式。

这个参数如何使用这里就不过多阐述了,可以通过man fcntl命令进行查看。

匿名管道有一个巨大的缺点,就是只能让两个有血缘关系的进程完成进程间通信,这有点太过于局限了,那么如果是没有血缘关系的进程呢?我们可以通过命名管道来实现通信。

三、命名管道:

什么是命名管道?FIFO常被称之为命名管道,FIFO是linux基础文件类型中的一种,我们可以通过ls-l来查看文件类型。

如何创建命名管道?创建命名管道的方式有两种,一种是直接使用命令,第二种是通过mkfifo函数来创建命名管道。

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

第一个参数:

pathname--管道文件路径

第二个参数:

mode--文件权限模式 比如0666(所以用户可读写) ,0644(用户可读写,其他用户只读), 0600(仅用户可读写)。

那么下面将用代码来实现一个简单的命名管道完成进程间通信。

cpp 复制代码
//createpipe.cc

#include <iostream>
#include <sys/stat.h>
#define PIPE_NAME "/tmp/myfifo"
using namespace std;
int main()
{
    int n = mkfifo(PIPE_NAME, 0666);
    if (n == -1)
    {
        perror("mkfifo false\n");
        exit(1);
    }
    else
    {
        cout << "创建FIFO文件成功" << PIPE_NAME << endl;
    }
    return 0;
}
cpp 复制代码
//客户端,负责写入数据

#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#define PIPE_NAME "/tmp/myfifo"
using namespace std;
int main()
{
    //这是一个客户端,用来写入数据,发送到服务器
  int wfd;
  cout<<"客户端:开始连接服务器..."<<endl;
  wfd=open(PIPE_NAME,O_WRONLY);
   cout<<"客户端:已经连接服务器,开始发送消息..."<<endl;
   char message[1024];
   fgets(message,sizeof(message),stdin);
   write(wfd,message,strlen(message)+1);
   cout<<"客户端:消息已经发送出去了"<<endl;
   close(wfd);
   return 0;
}
cpp 复制代码
//服务端,负责读取客户端发送的数据


#include<iostream>
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#define PIPE_NAME "/tmp/myfifo"
using namespace std;
int main()
{
  // 这是我们的服务端,用来读客户端写入的数据
  int rfd;
  char buffer[1024];
  // 创建命名管道
  if (mkfifo(PIPE_NAME, 0666) == -1)
  {
    // 如果管道已存在,可以继续使用
    if (errno != EEXIST)
    {
      perror("mkfifo");
      exit(1);
    }
  }
  cout << "我是服务端,我正在等待客户端连接" << endl;
  rfd = open(PIPE_NAME, O_RDONLY);
  cout << "客户端连接成功" << endl;
  cout << "开始读取数据" << endl;
  int ret = read(rfd, buffer, sizeof(buffer));
  cout<<"服务端收到"<<endl<<buffer<<endl;
  close(rfd);
  unlink(PIPE_NAME);
  return 0;
}

首先在createpipe里,我们创建了一个命名管道,然后通过readpipe.cc和writepipe.cc分别实现了读和写,readpipe.cc实现的是服务端,wirtepipe.cc实现的是客户端,客户端写数据,然后服务端来读取数据,这样就实现了一个简单的,通过命名管道的进程间通信。

那么下面,我们来看一下运行的结果:

客户端:

服务端:

如上就通过命名管道实现了进程间通信。

命名管道并不要求两个进程直接具有血缘关系,这是命名管道相比于匿名管道的优势所在。

四、共享内存:

进程是操作系统分配资源的基本单位,因此,不同的两个进程,它们的虚拟地址空间是不同的,因为要防止它们互相影响,不同进程能顺利进行通信的本质是不同的进程看到同一份资源,都看不到共同的资源,那么如何进行通信呢?System V IPC提供了共享内存的设施,可以让不同的进程看到一块资源。

下面将通过代码更好地展示这一点:

先创建共享内存,linux提供了shmget函数用来创建共享内存,该函数创建由键值key标识的共享内存块,并返回标识号。

下面我讲通过一段比较简单的代码来为大家展示,如何使用共享内存,在这里,我使用了生产者消费者模型,如果有学习过线程的朋友应该有所了解,这个模型我会在线程那里详细介绍。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <cstring>
using namespace std;
#define NUM 1024

// 定义共享内存这个结构体:
struct shared_mm
{
    int _flag; // 0:空 1:有新数据 2:传输结束
    char buffer[NUM];
};

enum//枚举类型,枚举错误退出码
{
    SHMAT_ERR = 1,
    SHMGET_ERR
};
class ShMemory
{
public:
    ShMemory(key_t key)
        : _key(key)
    {
        // 1、创建共享内存
        _shmid = shmget(key, sizeof(struct shared_mm), IPC_CREAT | IPC_EXCL | 0666);

        if (_shmid == -1)
        {
            // If exists, get existing
            _shmid = shmget(key, sizeof(struct shared_mm), 0);
            if (_shmid == -1)
            {
                std::cerr << "创建共享内存失败!" << std::endl;
                exit(SHMGET_ERR);
            }
        }
        //  2、返回一个虚拟地址空间
        _shmptr = (struct shared_mm *)shmat(_shmid, 0, 0);
        if (_shmptr == (void *)-1)
        {

            cout << "空间映射成虚拟地址失败!" << endl;
            exit(SHMAT_ERR);
        }

        // 走到这里说明共享内存创建成功了!!!
        memset(_shmptr->buffer, 0, NUM); // 清空缓冲区
        _shmptr->_flag = 0;
        // 初始化标记位为0
        cout << "共享内存创建成功" << endl;
    }
    ~ShMemory()
    {
        // 将共享内存从进程中分离
        if (_shmptr != nullptr && _shmptr != (void *)-1)
        {
            shmdt(_shmptr);
        }
        shmctl(_shmid, IPC_RMID, nullptr); // 删除共享内存
    }
    bool writedata(const char *data)
    {
        // 先从标记位来判断共享内存的状态
        if (_shmptr->_flag != 0)
        {
            // 说明此时共享内存不为空,不能进行数据写入
            return false;
        }
        // 走到这里说明此时共享内存为空,是不是就可以进行数据写入了?
        strncpy(_shmptr->buffer, data, NUM - 1); // 将传进来的参数拷贝到这个共享内存中
        _shmptr->buffer[NUM - 1] = '\0';         // 确保以\0结束
        _shmptr->_flag = 1;                      // 表示有此时共享内存有新数据了(我们刚刚写入的)
        return true;
    }
    char *readdata() // 从共享内存读取数据
    {
        // 什么时候才能读取数据?前提得是有新数据吧?换言之也就是标记位为1的时候
        if (_shmptr->_flag != 1)
        {
            return nullptr; // 没有新数据直接返回空指针即可
        }
        // 走到这里说明此时共享内存中有新数据!!!
        return _shmptr->buffer;
    }
    int getflag() // 返回标志位
    {
        return _shmptr->_flag;
    }
    void setflag(int flag)
    {
        _shmptr->_flag = flag; // 设置标志位
    }

private: // _表示为该类内部的成员变量
    int _shmid;
    key_t _key;
    struct shared_mm *_shmptr;
};

这个头文件中包含了一个类shMemory表示共享内存,里面有三个成员变量_shmid,_key,以及指向struct shared_mm结构的指针_shmptr。

在这个struct shared_mm结构中,我们有两个变量一个是缓冲区buffer,一个是_flag,这个_flag变量是为了实现同步机制的,0表示共享内存为空,1表示共享内存中有新的数据,2表示不再进行写入数据了,也就是完成通信了。

生产者进程代码如下:

cpp 复制代码
// 生产者
#include "SharedMemory.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;
int main(int argc, char *argv[])
{
    ShMemory shm(0x12345); // 固定k值 类实例化这个对象自动调用构造函数进行创建共享内存
    // 什么是生产者?产生数据呀
    if (argc != 2)
    {
        cout << "请输入./shmprod启动生产者" << endl;
    }
    cout << "生产者进程启动!" << endl;
    string input; // 从命令行输入的数据
    int  count=0;//表示第几次写入的数据
    while (true)
    {
        while (shm.getflag() == 1) // 循环等待消费者读取数据
        {
            // 只要进入这个循环说明此时共享内存中有新的数据,但是消费者并没有进行读取这个数据
            usleep(100000); // 等待100ms
            std::cout << "等待消费者读取..." << std::endl;
        }
        // 走到这里说明可以生产数据了
        cout << "#请输入数据:" << endl;
        getline(cin, input);
        if (input == "exit")
        {
            shm.setflag(2); // 设置标志位为2,表示退出了
            break;
        }
        string message = "消息#" + to_string(++count) + ": " + input;
        if (shm.writedata(message.c_str()))
        {
            cout << "成功写入:" << message << endl;
            sleep(2);
        }
        else
        {
            cout << "写入失败,共享内存忙" << endl;
        }
    }
    return 0;
}

消费者进程代码:

cpp 复制代码
// 消费者
#include "SharedMemory.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "请输入./shmcons启动消费者" << endl;
    }
    int count=0;//标记第几次收到的数据
    ShMemory shm(0x12345); // 使用相同的key
    cout << "消费者进程启动!" << endl;
    while (true)
    {
        // 是一个死循环
        if (shm.getflag() == 2)
        {
            cout << "收到终止信号,消费者退出" << endl;
            sleep(2);
        }
        // 走到这里说明没有退出,要读取数据
        char *data = shm.readdata();
        if (data != nullptr)
        {
            // 进入这里说明成功读取到了数据
            cout <<"["<<++count<<"]"<<"我收到了数据:" << data << endl;
            shm.setflag(0); // 把数据读取之后再把共享内存标志位设置为0,表示此时为空
        }
        else
        {
            // 说明没有读取到数据,要进行等待,等待生产者进程生产数据
            sleep(2); // 两秒钟打印一次
            cout << "等待生产者生产数据..." << endl;
        }
    }
    return 0;
}

对于上述代码,我都做了详细的注释,需要补充的就是,这份代码可以进一步进行改善,比如使用互斥量更好地实现进程同步。在这里我目的是为了给大家讲清楚这个共享内存的接口,就不做深入改善了!

在两个终端分别输入./shmproc与./shmcons就可以进行两个不同的进程通过这个共享内存进行通信!

使用ipcs命令就可以看到我们刚刚创建的共享内存,如果想把这个共享内存删除,可以使用ipcrm -m +对应共享内存的shmid就可以删除这个共享内存,这个shmid,你使用ipcs命令就可以查看到

至此进程间通信部分就写完了,后续部分可能会补充消息队列或者信号量的知识!

本文至此结束,如上述内容对您有所帮助可以点赞收藏,关注一下,如有错误可在评论区指出,本人会及时进行更正。

后续会持续更新linux相关的内容。

相关推荐
_wyt0015 小时前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
大树887 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush47 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5208 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz8 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工8 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
玖玥拾8 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
酣大智9 小时前
ARP代理--工作原理
运维·网络·arp·arp代理