Linux:进程间通信

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

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

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

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

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

二、匿名管道:

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

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

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

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

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

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函数创建管道,得到两个文件描述符fd[0],和fd[1],分别指向管道的读端和写端。其次,父进程通过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相关的内容。

相关推荐
10岁的博客2 小时前
二维差分算法高效解靶场问题
java·服务器·算法
lwhdjbcjdjd3 小时前
Nginx与Tomcat协作处理流程及数据流向
运维·nginx·tomcat
CIb0la3 小时前
安卓16系统升级后(Google pixel 8/8pro 9/9pro xl 10/10pro xl)救砖及Root方法
android·运维·生活
灵晔君3 小时前
C++标准模板库(STL)——list的模拟实现
c++·list
呉師傅4 小时前
国产化操作系统---银河麒麟安装步骤(简单介绍)
运维·网络·windows·单片机·电脑
Justinyh4 小时前
1、CUDA 编程基础
c++·人工智能
Code Warrior4 小时前
【Linux】应用层协议HTTP
linux·网络·网络协议·http
Hello,C++!4 小时前
linux下libcurl的https简单例子
linux·数据库·https
white-persist5 小时前
差异功能定位解析:C语言与C++(区别在哪里?)
java·c语言·开发语言·网络·c++·安全·信息可视化