当前位置: 首页 > news >正文

Linux IPC-管道

前言

前面我们已经对进程概念、进程控制做了介绍!本期开始将来介绍进程的最后一章进程间通信,即如何让两个进程进行通信!本博客主要介绍管道!

本期内容介绍

• 初识进程间通信

• 管道

一、初识进程间通信

1、进程间通信的概念

进程间通信(Inter-Process Communication,简称IPC是指在不同进程之间传输数据或信号的技术!

2、进程间通信的目的

进程之间也是需要某种协同的,但是进行协同的前提是得双方进行通信。通信是要传递数据的,根据数据的类别,可以将通信目的分为:

• 数据传输 :一个进程需要将自己的数据发送给另一个进程;

• 资源共享 :多进程之间共享同样的资源

• 通知事件 :一个进程需要给另一个进程发送消息,通知他(他们)发生了某种事件(例如,进程终止要统治父进程)

• 进程控制 :有些进程希望完全控制另一个进程执行(入Debug进程),此时控制进程希望能够拦截另一个进程的所有的陷入和异常,并能够及时知道他的状态改变!

3、进程间通信的本质

进程间通信的本质,就是让不同的进程看到同一个 "资源";

这个资源是由OS分配的,本质就是一段内存

根据前面的介绍我们知道,进程之间是具有独立性的,这也就注定了其他进程是无法直接获取到其他进程的数据的!那如何在不破坏进程独立性的前提下,让不同的进程进行通信呢?其实很简单,让他们看到同一份 "资源" 就可以了!而进程是由操作系统管理的,所以这个"资源" 也一定是由操作系统提供的!所以,如果要实现进程间通信是必须要有成本的

由于,这个资源是由操作系统创建的,根据前面的介绍,操作系统不相信任何人,所以他一定会给我们提供相关的系统调用接口操作!

4、进程间通信的分类

而根据OS创建的共享资源的不同,操作系统提供的系统调用接口也就不同,即进程间通信的种类也就不同!可以将通信种类分为三类:

• 管道(pipe)

        • 匿名管道

        • 命名管道

• System V IPC

         • System V 消息队列

         • System V 共享内存

         • System V 信号量

• POSIX IPC

       • 消息队列

       • 共享内存

       • 信号量

       • 互斥量

       • 条件变量

       • 读写锁

二、管道

1、管道概念

• 管道是Unix种最古老的进程间通信的形式!

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

• 管道分为匿名管道命名管道

2、管道的原理

• 站在内核角度深度理解管道

我们知道在进程打开一个文件时,OS会为其创建一个struct file对象,以及创建一张文件按描符表(files_struct)用其中的fd_array数组记录打开了那些文件!并会为每一个struct file创建内核级缓冲区!最后当I\O结束后,将内核缓冲区写到磁盘!


如果两次用不同的方式打开同一个文件呢(第一次以r方式打开,第二次以w方式打开)?

我们根据上面及以前的介绍知道,第一次方式打开会先创建struct file对象,然后将内容加载到内核缓冲区!而由于打开的方式不一样,OS会再创建一个struct  file对象,因为是同一个文件对象且第一次已经加再到缓冲区了,所以第二次打开仅仅是让以w打开的struct file对象指向文件的属性和缓冲区,不会在重复的加载一份了

如果让当前以不同方式打开同一个文件的进程创建一个子进程呢?

因为进程具有独立性,所以子进程会有自己的PCB, files_struct等,这些内核数据结构的内容大部分继承与父进程,但是并不会将虚拟文件系统的那部分再给子进程拷贝一份了,原因是进程具有独立性,但是虚拟文件系统没有说有独立性!所以此时,父子进程形成了浅拷贝(文件系统内每个struct file对象的引用计数都是2),也就是此时父子进程指向同一块空间,即两个不同的进程看到了同一块资源!

把这种基于虚拟文件系统让多个进程看到同一份资源的文件(包含缓冲区)叫做管道文件!

管道只允许单向通信,即要么父一直给子发,要么子一直给父发!如果管道进行双向通信会出现信息紊乱的情况!其实这就像我们的水管,你要么A端流向B端,要么从B端流向A端。你不可能说两端同时相向流入!所以,此时我们就可以将上述的父子进程,中的其中一个打开文件的struct file对象给关掉了:

因为管道文件只是用来通信的,不需要向磁盘刷新,所以,此时就得重新设计管道的接口了,得把想磁盘刷新的功能给去掉,只需要让OS开辟一个内存级的缓冲区即可!

这样就可以实现不同进程通过管道进行单向通信了 !

• 理解以前不能理解的现象

OK,介绍完这里我们也就能理解以前的几个现象了!

进程会默认打开0\1\2,他是如如何做到的?

父进程bash默认打开了,我们平时所有的进程都是bash的子进程,都继承了父进程的文件描述表

为什么父子进程会向同一个显示器打印数据呢?

因为父进程默认打开了0、1、2,子进程继承了下来,所以他两指向同一个显示器文件,写的时候当然就写到同一个显示器喽~!

• 为什么我们子进程close等的时候,不影响父进程的使用呢?

因为在struct file对象内部会存在一个引用计数,记录了与多少个进程指向当前的struct file,只有当引用计数是1的时候执行close再会真正的关闭!因为父子进程同时指向,所以引用计数一定大于1,所以子进程close只是让引用计数减1,并没有真的关闭!

• 站在文件描述符的角度理解管道

• 两个进程在通信时父进程会先创建管道,本质就是以读和写的方式打开了同一个文件!所以有两个文件文件描述符fd(详细的后面使用介绍参数时介绍)!

• 父进程再去创建子进程,让他们保证看到同一份资源!

• 关掉父子进程各自不需要的文件描述符,然后就可以单向的通信了!

看完这个更加直观的文件描述符原理,我们可以看,不同进程要想进行通信首先是要申请系统的内存资源,即进程间通信是有成本的!

3、匿名管道

在创建时没有与磁盘中的文件进行关联的管道文件(没有名字),叫做匿名管道。

• 匿名管道的系统调用接口

OK,了解了原理,接下来我们来认识一下OS提供的系统调用接口:

函数的原型和返回值都介绍了,但是这个参数是啥呢?我们下面就来介绍一下pipe的参数:

pipefd[2]数组是一个输出型参数!目的是为了方便用户使用!

其中,OS默认:pipefd[0]表示读端;pipefd[1]表示写端;当然这个只是OS默认的,你也可以改成pipefd[0]表示写端;pipefd[1]表读端

OK,介绍完了接口,那就可以使用喽:我们可以让父进程读取,子进程写入:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string>const int size = 1024;std::string GetOtherMessage()
{static int cnt = 0;//消息编号std::string id = std::to_string(cnt++);//将消息编号转为字符串pid_t self_id = getpid();//获取当前进程的pidstd::string prs_id = std::to_string(self_id);//将消息编号、pid拼接在一起std::string message = "msg_id: " + id + "  my pid: " + prs_id;return message;
}// 子进程写入
void ChildProcessWrite(int wfd)
{std::string msg = "father, i am your child....";while (1){std::string info = msg + GetOtherMessage();write(wfd, info.c_str(), info.size());sleep(1);}
}// 父进程读取
void FatherProcessRead(int rfd)
{char buffer[size];while (1){ssize_t n = read(rfd, buffer, sizeof(buffer) -1);if(n > 0){buffer[n] = '\0';std::cout << "Son, i get: " << buffer << std::endl;sleep(1);}else if(n < 0){std::cerr << "read error" << std::endl;break;}else{std::cerr << "Writer haved quit..., return val is: " << n << std::endl;break;}}
}int main()
{// 创建管道int pipefd[2] = {0};int n = pipe(pipefd);// 检查是否创建成功if (n != 0){std::cerr << "errno: " << errno << "errmsg: " << strerror(errno) << std::endl;return -1; // 管道创建失败}// 创建子进程pid_t id = fork();// child -> writeif (id == 0){std::cout << "我是子进程, 正在关闭不需要的fd, 要准备发消息了..." << std::endl;close(pipefd[0]);             // 关闭不需要的读端ChildProcessWrite(pipefd[1]); // 子进程写入close(pipefd[1]);             // 通信完毕,关闭写端exit(0);}// father -> readstd::cout << "我是父进程, 正在关闭不需要的fd, 要准备接收消息了~~~" << std::endl;close(pipefd[1]);             // 关闭写端FatherProcessRead(pipefd[0]); // 父进程读取close(pipefd[0]);             // 通信完毕,关闭读端// 等待子进程退出int status = 0;                      // 获取子进程的退出信息pid_t rid = waitpid(id, &status, 0); // 阻塞等待子进程退出if (rid > 0){std::cout << "wait success" << std::endl;std::cout << "quit sig is: " << (status & 0x7f) << std::endl;std::cout << "quit code is: " << ((status >> 8) & 0xff) << std::endl;}std::cout << "father quit..." << std::endl;return 0;
}

OK,我们再写一个makefile方便编译运行

Test_pipe:Test_pipe.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm Test_pipe

OK,看一下效果:

我们可以看到父进程读取到的,和子进程写入的一样!另外,由于我的某里云是体验的服务器,马上过期了,所以用的是root,一般情况不建议使用root!!

• 管道的机制

• 如果写进程没有关闭,管道内空的,读进程会被阻塞,等管道有数据了,读进程才会被唤醒!

OK,我们可以让写入端不写,让读取端一直读:

• 管道被写满了,但是读进程就是不读且没有关闭,写进程会被阻塞,等到度进程读了后写进程才可以,继续进行写入!

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <stdlib.h>
#include <sys/wait.h>void Writer(int wfd)
{char c = 'a';size_t cnt = 0;while (1){write(wfd, &c, 1);std::cout << "pipsize: " << ++cnt << "  -> " << c << std::endl;if (c == 'z' + 1){c = 'a';}c++;}
}void Reader(int rfd)
{char buffer[1024];while (1){int n = read(rfd, buffer, 1023);if (n > 0){buffer[n] = '\0';std::cout << "Son, i get:  " << buffer << std::endl;sleep(1);}}
}int main()
{// 打开管道int pipefd[2] = {0};int n = pipe(pipefd);if (n != 0){std::cerr << "errno: " << errno << "err_msg: " << strerror(errno) << std::endl;}// 创建子进程pid_t id = fork();if (id == 0){std::cout << "我是子进程, 正在关闭不需要的fd, 要准备发消息了..." << std::endl;close(pipefd[0]); // 关闭读端Writer(pipefd[1]);close(pipefd[1]); // 通信结束}// 父进程//  father -> readstd::cout << "我是父进程, 正在关闭不需要的fd, 要准备接收消息了~~~" << std::endl;close(pipefd[1]); // 关闭写端;sleep(10);Reader(pipefd[0]);close(pipefd[0]); // 通信完毕,关闭读端// 等待子进程退出int status = 0;                      // 获取子进程的退出信息pid_t rid = waitpid(id, &status, 0); // 阻塞等待子进程退出if (rid > 0){std::cout << "wait success" << std::endl;std::cout << "quit sig is: " << (status & 0x7f) << std::endl;std::cout << "quit code is: " << ((status >> 8) & 0xff) << std::endl;}return 0;
}

我们此时可以每次向管道写入一个字符,读进程先休眠10秒不读取,此时写进程当写满管道后就阻塞了:

这里卡在了65536字符处,这其实就是我们当前管道的大小, 65536Byte就是64KB

当10秒过后,父进程可以读取了:

我们发现这怎么读的数据连在一块了?其实这是管道的一个特征:管道是面向字节流的!

什么意思呢?简单说就是,读进程读的时候管道有多少就读多少,而读到的这些可能是写进程写了好多次的结果!但是读进程不管,在他看来就是一个个的字符;这就像我们一个水管一样,我们一打开可能是另一端灌入多次的结果,但是我们一次就都拿到了!至于用户怎么区分那是用户的事情!

等管道中的内容被读进程拿走了后,写进程就可以被唤醒继续写入了:

• 读端正常读,写端关闭,读端read就会返回0,表明读到了文件(pipe)结尾;

• 写端正常写入,读端关闭,操作系统会向正在写入的进程发送13号信号终止。

void Writer(int wfd)
{char c = 'a';size_t cnt = 0;while (1){write(wfd, &c, 1);std::cout << "pipsize: " << ++cnt << "  -> " << c << std::endl;if (c == 'z'){c = 'a';}c++;}
}void Reader(int rfd)
{char buffer[1024];int cnt = 5;while (cnt--){int n = read(rfd, buffer, 1024);if (n > 0){buffer[n] = '\0';std::cout << "Son, i get:  " << buffer << std::endl;sleep(1);}else if (n == 0){std::cerr << "读取到了文件的结尾" << std::endl;break;}else{std::cout << "读取错误" << std::endl;break;}}
}

这里写端一直写入,读端读取5次后结束

• 管道的特征

• 匿名管道只能进行具有血缘关系的进程之间通信,常用于父子间进程通信

此时就是爷孙进程通信了!

• 管道内部自带进程同步机制,多执行流的时候,具有明显的顺序性!

其实有了上面的机制介绍,这一点很好理解,当管道写满了就不能写了,只能让读进程来执行喽!同理,没有读端就不读了,写段就写喽!

• 管道文件的生命周期是随进程的

• 管道文件在通信的时候是面向字节流的,即读取和写入不是一一匹配的

• 管道文件的通信模式是,一种特殊的半双工模式(就是通信是单向的)

这里唯一要解释的一个点是,半双工模式。后面介绍网络的时候还会介绍全双工模式以及单工模式!OK,这里就简单的举个例子理解一下:半双工就是对讲机对方说的时候你不能说;全双工就是你打游戏时,队友问候你的同时你也可以问候队友!单工模式:就是接收方只能接收,发送方只能发送!例如广播等;

匿名管道的应用
1、命令行执行的

我们以前就在Linux指令那一期介绍过,说 | 这个叫管道,其实它就是我们刚刚介绍的匿名管道!我们但是说,可以将他左端的处理结果和水流一样直接到右边执行!OK,可以举个例子:

 while :; do ps axj | head -1 && ps axj | grep Test_pipe | grep -v grep; sleep 1; done

我们以前写的这个脚本就是组好的例子!以及统计当前机器的用户数:

who | wc -l
2、进程池

我们可以通过上面的介绍,实现一个进程池!什么是进程池?我们以前了解过内存池,内存池是为了减少系统开辟内存的花销,直接提前一次性开辟很多内存,等用的时候直接用就可以了!这里进程池也是一样的,我们为了减少系统穿甲进程的花销,可以预先创建很多的进程等待有任务的时候直接从进程池指派进程执行即可!

如何做呢?我们进程池无非就是想节约资源,再用的时候效率高一点!那我们的大思路就是:

1、先创建一批进程并为其创建对应的管道

2、父进程通过管道,控制子进程执行任务

3、等执行完任务了,关闭管道,以及等待子进程结束

上面的思路细分一下就是,

• 可以将子进程和管道用类进行先描述,然后用数组进行组织,以后父进程对子进程和对应管道的管理就变成了对数组的管理!

• 父进程有任务时,找一个空闲的管道,向管道写入操作码,子进程读取到操作码后就去执行相应的任务!

• 等执行完了,先把对应的写端关闭,因为管道的特性,当写端关闭读端,读端就会读到文件的结尾!然后我们再去把子进程给一一等待即可!

上面的思路就是这张图:

processpool.cc 

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"class Channel
{
public:Channel(int wfd, pid_t subprocessid, const std::string &channelname): _wfd(wfd), _subprocessid(subprocessid), _channelname(channelname){}~Channel() {}int getWfd(){return _wfd;}pid_t getSubprocessId(){return _subprocessid;}std::string getChannerlName(){return _channelname;}void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait: " << rid << " success!" << std::endl;}}private:int _wfd;pid_t _subprocessid;std::string _channelname;
};// 创建信道和子进程
void CreaterChannelAndSub(std::vector<Channel> *channels, int num, task_t task)
{for (int i = 0; i < num; i++){// 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0){std::cerr << "errno: " << errno << "errmsg: " << strerror(errno) << std::endl;exit(-1);}// 创建子进程pid_t id = fork();if (id == 0){// child -> readclose(pipefd[1]); // 关掉不需要的fddup2(pipefd[0], 0);task();close(pipefd[0]); // 通信结束exit(0);}// fatherclose(pipefd[0]);std::string channel_name = "channel_id: " + std::to_string(i);channels->push_back(Channel(pipefd[1], id, channel_name));}
}// 获取信道
int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}// 给对应的信道发操作码
void SendTaskCommand(Channel &channel, int task_code)
{write(channel.getWfd(), &task_code, sizeof(task_code));
}// 控制进程一次
void CtrlProcessonce(std::vector<Channel> &channels)
{sleep(1);int task_code = SelectTask();                        // 选择一个任务码int channel_index = NextChannel(channels.size());    // 选择一个信道SendTaskCommand(channels[channel_index], task_code); // 发送std::cout << std::endl;std::cout << "task_code: " << task_code << "  channel : " << channels[channel_index].getChannerlName() << "  sub_pid: "<< channels[channel_index].getSubprocessId() << std::endl;
}// 控制进程
void CtrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times == -1){while (true){CtrlProcessonce(channels);}}else{while (times--){CtrlProcessonce(channels);}}
}// 关闭信道和等待子进程退出
void CleanUpChannel(std::vector<Channel> &channels)
{//  关闭所有的写端for (auto &channel : channels){channel.CloseChannel();}// 等待子进程for (auto &channel : channels){channel.Wait();}
}int main(int argc, char *argv[])
{// 判断是否命令行参数合法if (argc != 2){std::cerr << "command error" << std::endl;return -1;}// 获取进程池的数量int num = std::stoi(argv[1]);std::vector<Channel> channels;LoadTask(); // 加载任务// 1、创建信道和子进程CreaterChannelAndSub(&channels, num, work);// 2、通过channel控制子进程// a.选择任务 b.选择一个进程CtrlProcess(channels, 5);// 3、关闭管道和子进程CleanUpChannel(channels);// test// for (auto &channel : channels)// {//     std::cout << "----------------------------------------------" << std::endl;//     std::cout << "channel_id: " << channel.getSubprocessId() \//     << "  channel_name: " << channel.getChannerlName() << "  Sub_pid: " \//     << channel.getWfd() << std::endl;// }// sleep(100);return 0;
}

Task.hpp

#pragma once
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <unistd.h>#define NUM 3typedef void (*task_t)(); // 函数指针类型// 下载
void DownLoad()
{std::cout << "i am DownLoad task" << std::endl;
}// 输出
void Print()
{std::cout << "i am Print task" << std::endl;
}// 刷新
void Flush()
{std::cout << "i am Flush task" << std::endl;
}task_t tasks[NUM]; // 创建任务的指针指针数组
// 加载任务
void LoadTask()
{srand(time(nullptr) ^ 177777777);tasks[0] = Print;tasks[1] = DownLoad;tasks[2] = Flush;
}// 执行任务
void ExcuteTask(int num)
{if (num > 2 || num < 0)return;tasks[num]();
}int SelectTask()
{return rand() % NUM;
}// 子进程执行的任务
void work()
{while (true){int command = 0;int n = read(0, &command, sizeof command);if (n == sizeof(command)){std::cout << "pid is: " << getpid() << "task: " << std::endl;ExcuteTask(command);std::cout << "hehe" << std::endl;}else if (n == 0){std::cout << "sub process :" << getpid() << " is quit...." << std::endl;break;}else{std::cerr << "error" << std::endl;break;}}
}

4、命名管道

我们上面介绍了,匿名管道!它适用于具有血缘关系的进程间通信!现在我们的需求是,要让两个毫不相关的进程通信,该如何做呢?此时匿名管道就不行了,得用命名管道!

• 什么是命名管道?

让不相关的进程之间交换数据的管道文件叫做命名管道

• 在命令行创建一个命名管道

我们可以使用 mkfifo 指令在命令行创建一个命名管道:

例如:

此时,echocat就是两个不同的进程,我们可以看到通过mkfifo创建的命名管道myfifo可以进行通信了!如果我们不想要了,直接rm删掉或这可以用unlink删除:

• mkfifo函数创建命名管道

除了上述的在命令行用指令创建命名外也可以,在代码中自主的利用函数mkfifo创建:

当然,你如果不想要了,也可以用函数unlink给删除掉:

OK,我们写一个代码用一下:我们让client端写,让server端读:

named_pipe.hpp

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>#define User 1
#define Creater 2
#define Default -1
#define Read O_RDONLY
#define Write O_WRONLY
#define SIZE 4096const std::string comm_path = "./myfifo"; // 通信文件的路径class NamedPipe
{
private:bool OpenNamePipe(int mode) // 打开文件{_fd = open(_path_name.c_str(), mode);if (_fd < 0){return false;}return true;}public:NamedPipe(const std::string &path, int who) // 构造: _path_name(path), _id(who), _fd(Default){if (who == Creater){int n = mkfifo(path.c_str(), 0666);if (n != 0){perror("mkfifo");exit(-1);}std::cout << "creater namedpipe success" << std::endl;}}~NamedPipe() // 析构{if (_id == Creater){int res = unlink(_path_name.c_str());if (res != 0){perror("unlink");}std::cout << "creater free named pipe" << std::endl;}if (_fd != Default)close(_fd);}bool OPenForRead() // 以读的方式打开{return OpenNamePipe(Read);}bool OPenForWrite() // 以写的方式打开{return OpenNamePipe(Write);}int ReadNamePipe(std::string *out) // 读管道{char buffer[SIZE];int n = read(_fd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;*out = buffer;}return n;}int WriteNamePipe(const std::string &in) // 写管道{return write(_fd, in.c_str(), in.size());}private:std::string _path_name; // 管道文件的路径int _id;                // 操作者int _fd;                // 文件描述符
};

server.cc

#include "named_pipe.hpp"void ReadMessage(NamedPipe &fifo)
{while (true){std::string message;int n = fifo.ReadNamePipe(&message);if (n > 0){std::cout << "Client Say> " << message << std::endl;}else if (n == 0){std::cout << "Client Quit, Server Too" << std::endl;break;}else{std::cout << "fifo.ReadNamePipe Error" << std::endl;break;}}
}// read
int main()
{NamedPipe fifo(comm_path, Creater);if (fifo.OPenForRead()){// 读文件ReadMessage(fifo);}return 0;
}

client.cc

#include "named_pipe.hpp"// 写一次
void WriteMessageOnce(NamedPipe &fifo)
{std::string msg;std::cout << "Please Enter> " << std::endl;std::getline(std::cin, msg);fifo.WriteNamePipe(msg);
}// 写消息
void WriteMessage(NamedPipe &fifo, int times = -1)
{if (times == -1){while (true){WriteMessageOnce(fifo);}}else{while (times--){WriteMessageOnce(fifo);}}
}// write
int main()
{NamedPipe fifo(comm_path, User);if (fifo.OPenForWrite()){// 写消息WriteMessage(fifo);}return 0;
}

makefile

.PHONY:all
all:client serverclient:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf client server

看一下效果:

• 命名管道的原理

命名管道的原理和上面介绍的管道原理是一样的,上面那之所以没有说是匿名还是命名的原因是他们的原理是一样的!只不过一个是没有名字的,一个是有名字的,但是他们的底层原理一致!

5、匿名管道和命名管道的区别

• 匿名管道由pipe函数创建而成

• 命名管道由mkfifo函数创建,打开用open

• FIFO(命名管道)和pipe(匿名管道)之间的唯一区别就是他们的创建和打开方式不同,一旦这些工作完成之后他们具有相同的语义!

• 匿名管道用于具有血缘关系的进程之间,而命名管道用于两个毫不相干的进程

OK,本期分享就到这里,我是cp,期待与你的下次相遇!

结束语:编程路上,勤奋为翼,无畏前行!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 探索AI与社交的交汇点:看Facebook如何引领智能化革命
  • SSM伊犁旅游攻略网站—计算机毕业设计源码15961
  • 简短而精辟: 用什么样的约束思路能提高 (LLM) 成绩?
  • spring boot3.x快速入门
  • 代码规范 —— 数据库规范
  • 麦田物语第二十天
  • JVM知识总结(CMS收集器)
  • Element学习(表格组件、分页组件)(2)
  • openwrt编译Dockerfile
  • TV-L1光流算法流程
  • C++ SQL ORM
  • 苹果离线打包机配置和打包
  • Typora v1.9.5解锁版下载、安装教程 (轻便简洁的Markdown编辑器)
  • android手动绘制矩形框
  • Spring Boot实战:拦截器
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • Android组件 - 收藏集 - 掘金
  • Map集合、散列表、红黑树介绍
  • MySQL数据库运维之数据恢复
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • PAT A1120
  • 创建一个Struts2项目maven 方式
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 工作踩坑系列——https访问遇到“已阻止载入混合活动内容”
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 前端路由实现-history
  • 前端学习笔记之原型——一张图说明`prototype`和`__proto__`的区别
  • 驱动程序原理
  • 设计模式走一遍---观察者模式
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 使用 5W1H 写出高可读的 Git Commit Message
  • 物联网链路协议
  • 新书推荐|Windows黑客编程技术详解
  • 译米田引理
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • (k8s)Kubernetes 从0到1容器编排之旅
  • (第二周)效能测试
  • (附源码)spring boot车辆管理系统 毕业设计 031034
  • (免费领源码)python#django#mysql校园校园宿舍管理系统84831-计算机毕业设计项目选题推荐
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (四)鸿鹄云架构一服务注册中心
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • ****三次握手和四次挥手
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .NET Core 中插件式开发实现
  • .NET Framework与.NET Framework SDK有什么不同?
  • .net6Api后台+uniapp导出Excel
  • [ C++ ] STL---stack与queue
  • [20171102]视图v$session中process字段含义
  • [Android]一个简单使用Handler做Timer的例子
  • [BUG] Authentication Error
  • [C#]winform部署yolov5-onnx模型
  • [C/C++]数据结构 堆的详解
  • [C++]二叉搜索树