目录
1. VSCode环境安装
下载可以在官网直接下载,慢的话可以直接复制这个链接:
安装很简单,可以自己安装一下,不行就搜一搜。
1.1 使用VSCode
VSCode至少是一个编辑器,和Vim一样。
刚打开应该是英文的,点击左边应用搜索chinese,直接安装简体中文就行。
在桌面创建一个空文件夹,然后在VSCode中点击打开文件夹,选择桌面,选择刚创建的文件夹打开就行。然后这里选择新建文件:
这里新建test.c文件,此时右下角可能会有安装C/C++的插件等,安装就行。
(左下角设置可以修改字体大小)写一段代码,Ctrl s保存:
再新建一个test.cpp文件,写一段代码并Ctrl s保存,然后顺便创建一个文件夹试试:
也可以创建其它语言的文件,此时桌面刚才新建的文件夹就有了在VSCode创建的东西:
运行是运行不了的,这里远程链接我们前面使用的Linux机器(也可以用其它方法)。
1.2 远程链接到Linux机器
在应用里搜索Remote并安装,滑下来你可以看到步骤:
这里按F1键,选中Remote-SSH Add,输入ssh 你的用户名@你的公网IP(xshell登录可以看)
回车后按第一行就配置好了,这里可以关掉重新启动VSCode,然后左边的远程资源管理器就出现了你刚才链接的机器。
这里右键机器,然后选择上面一行的在当前窗口链接,然后选择Linux,输入刚才用户名密码
然后这里打钩了就是链接好了:
这里打开xshll,在平时写代码的路径创建一个TestVSCode目录:
点击左上角的资源管理器,新建文件夹,默认就是我们在xshell的路径:
选择TestVSCode目录,确定,选择Linux,然后输密码就行,
这里连接好了,新建一个test.cpp文件,写点代码,Ctrl S保存:
此时在xshell就自动同步我们的文件和代码了,编译运行试试:
你也可以在VSCode下 Ctrl ~ 调出终端输入命令:
成功成功。
1.3 VSCode调试
C++相关的插件刚才应该也一带安装了,没安装的这里打钩都能安装了:
这里安装下GDB Debug,然后在左边类似小爬虫的图标上点击这里:
然后选择GDB Debug调试器:
这个小方括号里面的内容都可以删掉了,然后点击左边的添加配置,选择第一行什么gdb 启动:
然后只需要改这一行:"program": "输入程序名称,例如 ${workspaceFolder}/a.out",
改成:"program": "${workspaceFolder}/test_gdb",
Ctrl 保存,在终端生成test_gdb调试文件,运行:
然后在点小爬虫左上角的开始调试(F5)就行,快捷键都和VS2022差不多的:
断点也能直接打,左边变量啥的都能看到,但是VSCode的调试有时会很卡,所以不常用,
据此,VSCode的简单使用就讲完了。
2. 进程间通讯介绍
IPC(Inter-Process Communication,进程间通讯)
2.1 进程间通讯的概念和意义
什么是进程间通信?:
进程间通信是两个或者多个进程之间进行通信,行为如下:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
为什么要进行通信?:
在之前讲过 "进程之间是具有独立性" 的,如果进程间想交互数据,成本会非常高。因为独立性之本质即 "封闭",进程们你封闭你的我封闭我的,那么进程间的交流就很难。
- 进程间的通信说白了就是 "数据交互",在操作系统中,进程是独立运行的程序,它们之间需要相互协作完成任务。进程间通信的目的是为了实现进程之间的数据共享、协作和同步,从而提高系统的效率和可靠性。
很多场景下我们需要多进程进行协同处理一件事情。
不要以为,进程独立了就是彻底独立,有时我们需要双方能够进行一定程序的信息交互。
2.2 进程间通讯的策略和本质
如何进行进程通信?:
当前主要是通过三种策略来实现进程间通信的,每一种策略下都有很多种通信方式:
① 管道:通过文件系统通信。
匿名管道
命名管道
② System Ⅴ:聚焦在本地通信。
共享内存
消息队列
信号量
③ POSIX:让通信可以跨主机。
共享内存
消息队列
信号量
互斥量
条件变量
读写锁
进程通信的本质是什么?:
进程是相互独立的,所以进程之间的通信成本肯定不低。
为了进程在通信的时候,既能满足进程之间的独立性,又能够到达通信的目的,那么进程之间通信的地点就不能在两个进程中。
一个进程将自己的数据交给另一个进程,并且还要等待另一个进程的应答,这样一来,这个进程将不独立了,受到了另一个进程的影响,与进程的独立性矛盾。
所以,两个进程进行通信的地点必须是由第三方提供的,第三方只能是操作系统。操作系统提供的这个地点被我们称为:公共资源。公共资源有了,还必须让要通信的进程都看到这一份公共资源,此时要通信的进程将有了通信的前提。之后就是进程通信,也就是访问这块公共资源的数据。
之所以有不同的通信方式,是因为公共资源的种类不一,如果公共资源是一块内存,那么通信方式就叫做共享内存,如果公共资源是一个文件,也就是struct file结构体,那么就叫做管道。
3. 管道
3.1 管道介绍
什么是管道?管道是 Unix 系统中最古老的 进程间通信IPC 形式,
一个进程连接到另一个进程的数据流称为管道 (Pipe)。
我们来回忆一下文件系统:
父进程打开一个文件,操作系统在内存上创建一个struct file结构体对象,里面包含文件的各种属性,以及对磁盘文件的操作方法。每个struct file对象中还有一个内核缓冲区,这个缓冲区中可以存放数据。
当子进程创建的时候,父进程的文件描述符表会被子进程继承下去,所以此时子进程在相同的fd处也指向父进程打开的文件。文件描述符表一个进程维护一个,但是struct file结构体对象在内存中只有一个,由操作系统维护。此时,父子进程将看到了同一份公共资源,也就是操作系统在内存中维护的struct file对象,并且父子进程也都和这份资源建立了连接。此时父子进程通信的基础有了,它们就可以通信了。
父进程向文件中写内容,写完后继续干自己的事,并不破坏父进程的独立性。
子进程向文件中读内容,读完后继续干自己的事,并不破坏子进程的独立性。
这样一读一写,父子进程将完成了一次进程间通信。而我们又知道,对文件进行IO操作时,由于需要访问硬盘,所以速度非常的慢,而且我们发现,父子间进行通信,磁盘中文件的内容并不重要,重要的是父进程写了什么,子进程又读到了什么。
此时操作系统为了提高效率,就关闭了内存中struct file和硬盘中文件进行IO的通道。父进程写数据写到了struct file的内核缓冲区中。子进程读数据从struct file的内核缓冲区中读取。此时,父子间通信仍然正常进行,并且效率还非常的高,而且还没有影响进程的独立性。而这种不进行IO的文件叫做内存级文件。
这种由文件系统提供公共资源的进程间通信,就叫做管道。
两个进程就通过管道建立起了连接,并且可以进程进程之间的通信。而管道又分为匿名管道和命名管道。
3.2 匿名管道介绍
- 匿名管道:顾名思义,就是没有名字的文件(struct file)。
- 匿名管道只能用于父子进程间通信,或者由一个父进程创建的兄弟进程之间进行通信。
现在我们知道了匿名管道就是没有名字的文件,通过管道进行通信时,只需要通信双方打开同一个文件就可以。
我们通过系统调用open打开文件的时候,会指定打开方式,是读还是写。当父进程以写方式打开一个文件的时候,创建的子进程会继承父进程的一切。此时子进程也是以写的方式打开的这个文件。
既然是通信,势必有一方在写,一方在读,而现在父子双方都是以写的方式打开,它们怎么进行通信呢?:父进程以读和写的方式打开同一份文件两次(看下面图)。
这样一来,创建子进程后,父子进程都可以对管道进行读和写,它们就可以进行通信了,上面的问题就解决了。之所以命名为管道,那么就有和管道类似的性质。在生活中,我们对水管,它的流向只能是单向的,管道也一样,通过管道建立的通信只能进行单向数据通信。
如上图,假设父进程对管道写,子进程对管道读。
为了防止父进程对管道进行误读,以及子进程对管道进行误写,破坏通信规则。将父进程的读端关闭,将子进程的写端关闭,使用系统调用close(fd)。
此时,父子进程之间的单向数据通信就建立起来了,下一步就可以进行通信了。如果想进行双向通信,可以建立两个管道。
3.3 匿名管道示例代码
示例:从键盘读取数据,写入管道,读取管道,写到屏幕。
这里在rtx2目录下新建一个linux_17目录,在里面新建一个pipe目录,在VSCode写代码:
前面弄好了VSCode(没弄好用Vim写也行),打开VSCode打开上面的pipe文件夹,链接,输普通用户的密码,写个Makefile:
3.3.1 建立管道的pipe
上面都是理论上的,具体到代码中是如何建立管道的呢?
既然是操作系统中的文件系统提供的公共资源,当然是用系统调用来建立管道了。
- 原型:int pipe(int pipefd[2]);
- 头文件:unistd.h
- 功能:创建一个匿名管道
- 形参:int pipefd[2]是一个输出型参数,一个数组,该数组只有两个元素,下标分别为0和1。下标为0的元素表示的是管道读端的文件描述符fd。下标为1的元素表示的是管道写端的文件描述符fd。这里巧记:pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
- 返回值:返回0,管道创建成功。返回-1,管道创建失败,并将错误码自动写入errno中。
使用系统调用pipe,直接就会得到两个fd,并且放入父进程的文件描述符表中,不用打开内存级文件两次。那么,父进创建管道以后,得到的两个文件描述符是多少呢?根据前面所学,是3和4吗?我们代码中来看,mypipe.cpp:
#include <iostream>
#include <cerrno> // C++包C语言头文件常用的方法,和.h效果一样
#include <cstring>
#include <unistd.h> // pipe
using namespace std;
int main()
{
int pipefd[2];
int ret = pipe(pipefd); // 一.创建管道
if(ret < 0)
{
cerr << errno << ": " << strerror(errno) << endl;
}
cout << "pipefd[0]: " << pipefd[0] << endl; // 3
cout << "pipefd[1]: " << pipefd[1] << endl; // 4
return 0;
}
可以看到,创建管道后返回的两个fd值,果然是3和4,因为0,1,2分别被stdin,stdout,stderr占用。知道了如何使用系统调用创建管道以后,接下来就创建子进程,然后关闭不需要的端口了,原理已经清楚,直接看代码。
#include <iostream>
#include <cerrno> // C++包C语言头文件常用的方法,和.h效果一样
#include <cstring>
#include <cassert>
#include <unistd.h> // pipe + close
using namespace std;
int main()
{
int pipefd[2];
int ret = pipe(pipefd); // 一.创建管道
if(ret < 0)
{
cerr << errno << ": " << strerror(errno) << endl;
}
// cout << "pipefd[0]: " << pipefd[0] << endl; // 3
// cout << "pipefd[1]: " << pipefd[1] << endl; // 4
pid_t id = fork(); // 二.创建子进程
assert(id != -1);
if (id == 0) // 子进程,读,关闭写
{
close(pipefd[1]);
// 通信
close(pipefd[0]);
exit(0);
}
close(pipefd[0]); // 父进程,写,关闭读
// 通信
close(pipefd[1]);
return 0;
}
此时在代码层面上, 父子双方就已经建立了连接了,接下来就是通信数据了。
子进程读,代码:
父进程写,代码:
3.3.2 匿名管道完整代码
#include <iostream>
#include <cerrno> // C++包C语言头文件常用的方法,和.h效果一样
#include <cstring>
#include <cassert>
#include <unistd.h> // pipe + close + read + write
#include <sys/types.h> // waitpid两个头文件
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2];
int ret = pipe(pipefd); // 一.创建管道
if(ret < 0)
{
cerr << errno << ": " << strerror(errno) << endl;
}
// cout << "pipefd[0]: " << pipefd[0] << endl; // 3
// cout << "pipefd[1]: " << pipefd[1] << endl; // 4
pid_t id = fork(); // 二.创建子进程
assert(id != -1);
if (id == 0) // 子进程,读,关闭写
{
close(pipefd[1]);
// 三. 子进程读
char buffer[1024 * 8];
while (true)
{
// sleep(10);
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // read读
if (s > 0) // 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
{
buffer[s] = 0;
cout << "child get a message[" << getpid() << "] Father# " << buffer << endl;
}
else if (s == 0) // 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾
{
cout << "writer quit(father), me quit" << endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
close(pipefd[0]); // 父进程,写,关闭读
// 四. 父进程写
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024 * 8];
while (true)
{
//构建一个变化的字符串
snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count++);
write(pipefd[1], send_buffer, strlen(send_buffer)); // 写入
sleep(1);
cout << count << endl;
if (count == 5)
{
cout << "writer quit(father)" << endl;
break;
}
}
close(pipefd[1]);
pid_t ret_id = waitpid(id, nullptr, 0);
cout << "id : " << id << " ret_id: " << ret << endl;
assert(ret_id > 0); // 断言只在debug起效
(void)ret_id; // 只是证明ret_id被使用过
return 0;
}
这就实现了简单的匿名管道。
可以自己改代码试一试管道的读取特征:
场景 | 特征 |
---|---|
读取慢,写入快 | 写入端阻塞在write处 |
读取快,写入慢 | 读取端阻塞在read处 |
读取端关闭 | 操作系统终结写端 |
写入端关闭 | 读取端read返回0 |
管道之所以有这样的读取特征,其实是为了对管道中的数据进行保护,这种方式称为互斥,后面会详细讲解这一概念。
匿名管道本身也有它自己的特征,如下:
- 管道的生命周期随进程的结束而结束,当所有进程都关闭该管道的文件描述符时,管道被销毁。
- 管道可以用来进行具有血缘关系的进程之间进行通信,常用于父子进程通信。
- 管道是半双工的通信方式(单向通信)
- 管道是面向字节流的(在网络部分讲解)。
- 管道有互斥与同步机制对共享资源进行保护(以后讲解)。
3.3.3 匿名管道模拟进程池
这里在linux_17目录下创建ProcessPool目录,在VSCode打开,在里面写代码,
直接放匿名管道模拟进程池的代码了,可以自己跟着注释读一遍,也可以跟着写一写:
Makefile:
ProcessPool:ProcessPool.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f ProcessPool
ProcessPool.cpp:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp" // 发任务的文件,.hpp -> .h和.cpp写一起
#define PROCESS_NUM 5 // 创建的子进程数目
using namespace std;
int waitCommand(int waitFd, bool& quit) //如果对方不发,我们就阻塞
{
uint32_t command = 0; // uint32_t四个字节
ssize_t s = read(waitFd, &command, sizeof(command)); // 期望读取四个字节
if (s == 0) // 读到0让子进程退出
{
quit = true;
return -1;
}
assert(s == sizeof(uint32_t)); // 不是四个字节就报错
return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command) // 通过文件描述符,向哪一个文件发什么命令
{ // who给哪个进程,这个进程的id
write(fd, &command, sizeof(command));
cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}
int main()
{
// 代码中关于fd的处理,有一个小问题,不影响我们使用,但是你能找到吗??
load();
vector<pair<pid_t, int>> slots; // 存放子进程pid和子进程写端id(pipefd)
vector<int> deleteFd; // 存放要删除的子进程写端fd(不删除也不会出问题)
for (int i = 0; i < PROCESS_NUM; i++) // 先创建多个进程
{
int pipefd[2] = { 0 };
int ret = pipe(pipefd); // 创建管道
assert(ret == 0); // 等于0才创建成功
(void)ret;
pid_t id = fork();
assert(id != -1);
if (id == 0) // 子进程,进行读取
{
close(pipefd[1]); // 关闭写端
for (int i = 0; i < deleteFd.size(); i++) // 关闭所以继承下来的写端fd
{
close(deleteFd[i]);
}
while (true)
{
// 等命令
bool quit = false; // 默认不退出
int command = waitCommand(pipefd[0], quit); // 如果对方不发,我们就阻塞
if (quit) // 读到0就退出关闭所有进程
{
break;
}
if (command >= 0 && command < handlerSize()) // 执行对应的命令
{ // handlerSize任务方法的个数
callbacks[command]();
}
else
{
cout << "非法command: " << command << endl;
}
}
exit(1);
}
close(pipefd[0]); // 父进程,进行写入,关闭读端
slots.push_back(pair<pid_t, int>(id, pipefd[1])); // 把此次循环得到的子进程id和子进程写端的id保存
deleteFd.push_back(pipefd[1]); // 把要被继承下去的子进程写端fd保存起来
}
// 父进程均衡地派发任务(单机版的负载均衡)
srand((unsigned long)time(nullptr) ^ getpid() ^ 2335643123L); // 仅仅让数据源更随机
while (true)
{
// 选择一个任务
int command = rand() % handlerSize();
// 选择一个进程 ,采用随机数的方式,选择进程来完成任务,随机数方式的负载均衡
int choice = rand() % slots.size();
// 把任务给指定的进程
sendAndWakeup(slots[choice].first, slots[choice].second, command);
sleep(1);
//int select; // 手动版
//int command;
//cout << "############################################" << endl;
//cout << "# 1. show funcitons 2.send command #" << endl;
//cout << "############################################" << endl;
//cout << "Please Select> ";
//cin >> select;
//if (select == 1)
//{
// showHandler();
//}
//else if (select == 2)
//{
// cout << "Enter Your Command: ";
// cin >> command; // 选择任务
// int choice = rand() % slots.size(); // 选择进程
// sendAndWakeup(slots[choice].first, slots[choice].second, command); // 把任务给指定的进程
//}
//else // 选择错误
//{
//}
}
for (const auto& slot : slots) // 关闭fd, 所有的子进程都会退出
{
close(slot.second);
}
for (const auto& slot : slots) // 回收所有的子进程信息
{
waitpid(slot.first, nullptr, 0);
}
}
Task.hpp:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unistd.h>
#include <functional>
typedef std::function<void()> func;
std::vector<func> callbacks; // 存放若干个回调
std::unordered_map<int, std::string> desc; // 查看有多少方法用的
void readMySQL()
{
std::cout << "sub process[" << getpid() << " ] 执行访问数据库的任务\n" << std::endl;
}
void execuleUrl()
{
std::cout << "sub process[" << getpid() << " ] 执行url解析\n" << std::endl;
}
void cal()
{
std::cout << "sub process[" << getpid() << " ] 执行加密任务\n" << std::endl;
}
void save()
{
std::cout << "sub process[" << getpid() << " ] 执行数据持久化任务\n" << std::endl;
}
void load() // 操作表,先插入描述再插入方法,下标就对齐了
{
desc.insert({ callbacks.size(), "readMySQL: 读取数据库" });
callbacks.push_back(readMySQL);
desc.insert({ callbacks.size(), "execuleUrl: 进行url解析" });
callbacks.push_back(execuleUrl);
desc.insert({ callbacks.size(), "cal: 进行加密计算" });
callbacks.push_back(cal);
desc.insert({ callbacks.size(), "save: 进行数据的文件保存" });
callbacks.push_back(save);
}
void showHandler() // 查看有多少方法
{
for (const auto& iter : desc)
{
std::cout << iter.first << "\t" << iter.second << std::endl; // \t制表符
}
}
int handlerSize() // 直接返回有多少个任务的方法
{
return callbacks.size();
}
编译运行:
3.4 命名管道介绍
3.4.1 命名管道概念
命名管道:顾名思义,有名字的管道(内存级文件)。
根据前面的学习,我们知道,父子进程间使用匿名管道的方式进行通信,是通过子进程继承父进程的方式来实现,而且匿名匿名管道常用于父子进程直接,或者由血缘关系的进程直接。
那么,如果两个进程毫无关系呢?此时就不能继承了,那这两个进程如何建立连接呢?
还是采用管道的方式,但是这个管道是有名字的管道,这样一来,两个进程就可以打开同一个管道文件建立连接。
还是这张图,此时内存中的struct file在磁盘上有对应文件的,如上图中的fifo.ipc文件。
3.4.2 mkfifo和unlink小实验
创建命名管道指令,man mkfifo:
- 指令:mkfifo 文件名
- 功能:创建命名管道文件
FIFO(first in first out)因为管道是单向通信的,这里在linux_17目录下创建fifo目录,进入并建立一个命名管道:
此时就成功地建立了一个命名管道,可以发现它的(文件类型)权限前面的字母是p(pipe),而目录的文件类型是d(directory)。命名管道文件类型是p,而且该文件还有inode,说明在磁盘上是真实存在的。
当磁盘中有了命名管道文件以后,两个进程将可以通过这个管道文件进行通信了,步骤和匿名管道非常相似。一个进程以写方式打开管道文件,另一个进程以读端方式打开管道文件。
此时两个进程将建立了连接,然后将可以进行通信了。我们知道,进程间通信的前提是,要通信的进程能够看到同一份公共资源,那么命名管道是如何做到这一点的呢?:
让不同的进程打开指定路径下同一个管道文件。
往name_pipe写点东西:
此时发现类似堵塞住了? 此时处于的就是阻塞状态,它需要被另一个进程读取:
此时就相当于完成了两个进程之间的通讯。
你可以通过unlink或者rm删掉命名管道(效果是差不多的),man unlink
3.5 命名管道示例代码
3.5.1 系统调用的mkfifo和unlink
可以在shell中通过命令的方式创建管道文件,两个进程直接去使用它。也可以像文件一样,在进程中创建管道文件,此时就需要用到系统调用。man 3 mkfifo:
- 第一个形参:管道文件的名字
- 第二个形参:创建管道文件的权限
- 返回值:0表示创建成功,-1表示创建失败。
在进程中删除管道文件:man 2 unlink:
写个测试代码:
如果带上unlink:
此时命名管道在进程里打开,也在进程里关闭了。
3.5.2 命名管道示例完整代码:
在VSCode打开上面的fifo文件夹,在里面写代码,
Makefile:
.PHONY:all
all:client mutiServer
client:client.cpp
g++ -o $@ $^ -std=c++11
mutiServer:server.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client mutiServer
comm.hpp:(一些头文件和宏和一个路径)
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "Log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
#endif
Log.hpp:(打印日志,不加也行)
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] ={"Debug", "Notice", "Warning", "Error"};
std::ostream& Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
server.cpp:(服务端)
#include "comm.hpp"
static void getMessage(int fd) // 读取信息
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0) // 读取成功
{
cout << "[" << getpid() << "] " << "client say> " << buffer << endl;
}
else if (s == 0) // 文件结尾end of file
{
cerr << "[" << getpid() << "] " << "read end of file, clien quit, server quit too" << endl;
break;
}
else // 读取失败
{
perror("read");
break;
}
}
}
int main()
{
if (mkfifo(ipcPath.c_str(), MODE) != 0)// 1. 创建管道文件
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
int fd = open(ipcPath.c_str(), O_RDONLY); // 2. 正常的文件操作
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", Debug) << " step 2" << endl;
int nums = 3;
for (int i = 0; i < nums; i++)
{
pid_t id = fork();
if (id == 0) // 3. 编写正常的通信代码
{
getMessage(fd); // 读取信息
exit(1);
}
}
for (int i = 0; i < nums; i++)
{
waitpid(-1, nullptr, 0);
}
close(fd); // 4. 关闭文件
Log("关闭管道文件成功", Debug) << " step 3" << endl;
unlink(ipcPath.c_str()); // 通信完毕,就删除文件
Log("删除管道文件成功", Debug) << " step 4" << endl;
return 0;
}
client.cpp:(用户端)
#include "comm.hpp"
int main()
{
int fd = open(ipcPath.c_str(), O_WRONLY); // 1. 获取管道文件
if (fd < 0)
{
perror("open");
exit(1);
}
string buffer; // 2. ipc过程
while (true)
{
cout << "Please Enter Message Line : ";
getline(cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
close(fd); // 3. 关闭
return 0;
}
make之后先运行服务端再运行客户端,就能实现类似微信发信息的功能(客户端Ctrl C关闭):
这样就实现了命名管道的通信方式,建议自己写一遍代码测试一下。
4. 笔试选择题
1. 以下描述正确的有:
A.进程之间可以直接通过地址访问进行相互通信
B.进程之间不可以直接通过地址访问进行相互通信
C.所有的进程间通信都是通过内核中的缓冲区实现的
D.以上都是错误的
2. 以下选项属于进程间通信的是()[多选]
A.管道
B.套接字
C.内存
D.消息队列
3. 下列关于管道(Pipe)通信的叙述中,正确的是()
A.一个管道可以实现双向数据传输
B.管道的容量仅受磁盘容量大小限制
C.进程对管道进行读操作和写操作都可能被阻塞
D.一个管道只能有一个读进程或一个写进程对其操作
4. 以下关于管道的描述中,正确的是 [多选]
A.匿名管道可以用于任意进程间通信
B.匿名管道只能用于具有亲缘关系的进程间通信
C.在创建子进程之后也可以通过创建匿名管道实现父子进程间通信
D.必须在创建子进程之前创建匿名管道才能实现父子进程间通信
5. 以下关于管道的描述中错误的是 [多选]
A.可以通过int pipe(int pipefd[2])接口创建匿名管道,其中pipefd[0]用于从管道中读取数据
B.可以通过int pipe(int pipefd[2])接口创建匿名管道,其中pipefd[0]用于向管道中写入数据
C.若在所有进程中将管道的写端关闭,则从管道中读取数据时会返回-1;
D.管道的本质是内核中的一块缓冲区;
6. 以下关于管道描述正确的有:
A.命名管道可以用于同一主机上的任意进程间通信
B.向命名管道中写入的数据越多,则管道文件越大
C.若以只读的方式打开命名管道时,则打开操作会报错
D.命名管道可以实现双向通信
7. 以下关于管道描述正确的有:
A.命名管道和匿名管道的区别在于命名管道是通过普通文件实现的
B.命名管道在磁盘空间足够的情况下可以持续写入数据
C.多个进程在通过管道通信时,删除管道文件则无法继续通信
D.命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区
答案及解析
1. B
A错误: 进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)
B正确
C错误: 除了内核中的缓冲区之外还有文件以及网络通信的方式可以实现D
2. ABD
典型进程间通信方式:管道,共享内存,消息队列,信号量。 除此之外还有网络通信,以及文件等多种方式
C选项,这里的内存太过宽泛,并没有特指某种技术,错误。
3. C
A.一个管道可以实现双向数据传输
B.管道的容量仅受磁盘容量大小限制
C.进程对管道进行读操作和写操作都可能被阻塞
D.一个管道只能有一个读进程或一个写进程对其操作
4. ABD
A.匿名管道可以用于任意进程间通信
B.匿名管道只能用于具有亲缘关系的进程间通信
C.在创建子进程之后也可以通过创建匿名管道实现父子进程间通信
D.必须在创建子进程之前创建匿名管道才能实现父子进程间通信
5. BC
- 管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
- 使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
- 管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。
- 在同步的提现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出
根据以上管道理解分析:A正确,B错误,C错误,D正确
6. A
- 匿名管道只能用于具有亲缘关系的进程间通信,命名管道可用于同一主机上的任意进程间通信
- 管道的通信本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区
- 管道是半双工通信,是可以选择方向的单向通信
- 命名管道打开特性为,若以只读方式打开文件,则会阻塞,直到管道被以写的方式打开,反之亦然
7. D
A错误, 管道的本质是内核中的缓冲区,命名管道文件是缓冲区的标识
B错误, 管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
C错误, 管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
D正确
再就是自己实现匿名管道和命名管道。
本篇完。
下一篇:零基础Linux_18(进程间通信)共享内存+消息队列+信号量。
(穿越回来复习顺便贴个下篇链接:零基础Linux_18(进程间通信)共享内存+消息队列+信号量-优快云博客)