赞
踩
目录
官⽹下载地址:https://code.visualstudio.com/Download
下载太慢,推荐下载链接:
https://vscode.cdn.azure.cn/stable/30d9c6cd9483b2cc586687151bcbcd635f373630/VSCodeUser
然后一直 next 安装即可
安装完成后在如下地方,继续安装插件即可
- Remote - SSH - 远程登录Linux
- C/C++ - 必装
- C/C++ Extension Pack - C/C++扩展包,下载直接安装,它包含了 vscode 编写 C/C++ ⼯程需要的插件(C/C++、C/C++ Themes、CMake、CMake Tools和Better C++ Syntax等),和以前⽐不需要⼀个个找了。
- C/C++ Themes - 主题设置,插件⾥⾯可以点击设置
- Chinese (Simplified) (简体中⽂)
- vscode-icons - 改变编辑器⾥⾯的⽂件图标
- filesize - 左下⻆显⽰源⽂件⼤⼩的插件
- Include AutoComplete - ⾃动头⽂件包含
- GBKtoUTF8 - ⾃动将 GBK 转换为 UTF8
安装如下软件后,对照 README 操作,输入主机号即可
可以在我的电脑的如下路径下查看 config ,检查连接
记事本查看
连接完成后,打开需要的文件夹即可
使用小tips:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高
通信可以用于:1. 基本数据 2. 发送命令 3. 某种协调 4. 通知 ...很多场景下需要多个进程协同工作来完成要求。例如
两个进程cat和grep协同工作,将log.txt文件中带有hello的文字显示出来。
当前主要是通过三种策略来实现进程间通信的:
管道:通过文件系统通信。
System Ⅴ:聚焦在本地通信。
POSIX:让通信可以跨主机。
每一种策略下都有很多种通信方式,在这篇文章中将详细讲解管道策略的通信方式。
a. 进程间通信的本质:必须让不同的进程看到同一份“资源”
b. 资源?特定形式的内存空间
c. 这个资源谁提供?一般是操作系统,
为什么不是我们两个进程中的一个呢?假设一个进程提供,这个资源属于谁?
这个进程独有,破坏进程独立性,所以要借用第三方空间
d. 我们进程访问这个空间,进行通信,本质就是访问操作系统!
进程代表的就是用户,资源从创建,使用(一般),释放--系统调用接口!
从底层设计,从接口设计,都要由操作系统独立设计,一般操作系统,会有一个独立的通信模块--隶属于文件系统--IPC 通信模块
定制标准 -- 进程间通信是有标准的 -- system V && posix
e. 基于文件级别的通信方式 --管道
父子间通信仍然正常进行,并且效率还非常的高,而且还没有影响进程的独立性。而这种不进行IO(不刷到磁盘),就是内存级文件
这种由文件系统提供公共资源的进程间通信,就叫做管道。
匿名管道:没有名字的文件(struct file)。
匿名管道用于父子间通信,或者由一个父创建的兄弟进程(必须有“血缘“)之间进行通信
现在我们知道了匿名管道就是没有名字的文件,通过管道进行通信时,只需要通信双方打开同一个文件就可以。
我们通过系统调用open打开文件的时候,会指定打开方式,是读还是写。
既然是通信,势必有一方在写,一方在读,而现在父子双方都是以写的方式打开,它们怎么进行通信呢?
父进程以读和写的方式打开同一份文件两次。可以理解为管道就是文件,有缓冲区,由父子进程引入管道文件
父进程拷贝创建出子进程,PCB 是肯定要的,files_struct 指向同一文件
父子进程公用文件实现了,那么对于读写的控制是怎么实现的呢?
只想让父子进程进行单向通信!所以称为管道通信。把文件这套拿过来,用就是为了简单
我们想让子进程进行写入,父进程进行读取
细节图
如果我要进行双向通信呢?多个管道
如果文件没有任何关系,可以用我们刚刚讲的原理进行通信呢?
不能,必须是父子关系/兄弟关系/爷孙关系...进程之间需要有血缘关系,常用于父子,才能继承一套 file_struct。制作如下动图
sum: 至此,通信了吗??没有。建立通信信道--为什么这么费劲?--因为进程具有独立性,要操作系统忙活才能建立出信道,通信是有成本的!
上面都是理论上的,具体到代码中是如何建立管道的呢?既然是操作系统中的文件系统提供的公共资源,当然是用系统调用来建立管道了。
man 2 pipe
int pipefd[2]
是一个输出型参数,表示一个数组,该数组有两个元素,下标分别为0和1。fd
。(0像张嘴读书)fd
。(1像拿笔写字)通过系统调用 pipe
,可以直接获得这两个文件描述符,并将其添加到父进程的文件描述符表中,无需两次打开内存级别的文件。
int
类型的整数,用来反馈管道创建的情况。errno
中。那么,父进程创建管道后,得到的两个文件描述符是多少呢?是3和4吗?我们可以通过代码来验证这一点。
输出型参数:文件的描述符数字带出来,让用户使用-->3,4,因为0,1,2分别被stdin,stdout,stderr占用。
下面代码示例展示了如何通过管道(pipe)在父子进程之间进行通信。为了更好地理解代码,我们将其拆分开来并逐段解释每部分的含义。
- #include <unistd.h> // 包含pipe, fork, waitpid, write, read等函数的头文件
- #include <sys/types.h> // 包含pid_t类型的头文件
- #include <string> // 包含std::string类的头文件
- #include <iostream> // 包含std::cout, std::endl等输入输出流的头文件
- #include <cstdlib> // 包含exit函数和一些其他通用函数的头文件
这些头文件提供了程序中使用的函数和类型的声明。
- const int NUM = 100; // 定义缓冲区大小
- const int N = 2; // 定义管道数组的大小
这两行代码定义了一些常量,NUM
是缓冲区的大小,N
是管道数组的大小。
- void Writer(int wfd) {
- std::string s = "hello, I am child";
- pid_t self = getpid();
- int number = 0;
-
- char buffer[NUM]; // 定义一个缓冲区
-
- while (true) {
- sleep(1);
-
- // 构建发送字符串
- snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
- // 发送/写入给父进程, system call
- write(wfd, buffer, strlen(buffer) + 1); // 注意这里需要加1来包括'\0'
-
- std::cout << number << std::endl;
-
- if (number >= 5) break;
- }
- }
这个函数Writer
用于子进程向父进程写入数据。wfd
是写端文件描述符,s
是发送的字符串,self
是子进程ID,number
是一个计数器。每次循环中,构建一个包含字符串、子进程ID和计数器的消息,然后写入管道。每写一次,计数器加一,循环5次后退出。
- void Reader(int rfd) {
- char buffer[NUM];
-
- while (true) {
- buffer[0] = 0;
- ssize_t n = read(rfd, buffer, sizeof(buffer));
- if (n > 0) {
- buffer[n] = 0; // 终止字符串
- std::cout << "father get a message[" << getpid() << "]# " << buffer << std::endl;
- } else if (n == 0) {
- std::cout << "father read file done!" << std::endl;
- break;
- } else {
- std::perror("Error reading from pipe"); // 打印错误信息
- break;
- }
- }
- }
这个函数Reader
用于父进程从管道读取数据。rfd
是读端文件描述符。每次循环中,尝试从管道读取数据,如果成功读取到数据,将其打印出来。如果读取到文件结尾或发生错误,则退出循环。
主函数中:
pipefd
数组包含两个文件描述符,一个用于读,一个用于写。fork()
创建子进程。fork()
失败,打印错误信息并退出。Writer
函数写数据,写完后关闭写端并退出。Reader
函数读数据,等待子进程结束,关闭读端。- int main() {
- int pipefd[N] = {0}; // 管道文件描述符
- int n = pipe(pipefd);
- if (n < 0) {
- std::perror("Error creating pipe");
- return 1;
- }
-
- pid_t id = fork(); // 创建子进程
- if (id < 0) {
- std::perror("Error forking");
- return 2;
- }
- if (id == 0) { // 子进程
- close(pipefd[0]); // 关闭读端
-
- Writer(pipefd[1]); // 写入数据
-
- close(pipefd[1]); // 关闭写端
- exit(0);
- } else { // 父进程
- close(pipefd[1]); // 关闭写端
-
- Reader(pipefd[0]); // 读取数据
-
- pid_t rid = waitpid(id, nullptr, 0);//回收子进程
- if (rid < 0) {
- std::perror("Error waiting for child process");
- return 3;
- }
-
- close(pipefd[0]); // 关闭读端
- }
-
- std::cout << "Program finished." << std::endl;
- return 0;
- }
回忆点:
return 2
的设置 : 如果 fork()
失败,程序将返回 2
。这里的数字 2
是一个约定的返回值,用于表示 fork()
调用失败。通常情况下,不同的错误情况会使用不同的返回值来区分。pipefd[1]
) 并随后调用 exit(0)
是为了:确保子进程只执行写操作,一旦写操作完成,子进程就退出waitpid
函数来等待子进程的结束,结束后父进程一定要实现资源回收
waitpid
是一个系统调用,用于等待指定子进程的终止状态。- 参数
id
是子进程的 PID(进程标识符)。- 参数
nullptr
表示不关心子进程的状态信息。- 参数
0
表示默认的行为,即等待任何子进程的终止。
运行:
批注:
buffer[0] = 0;
- 清空字符串,标志数组 buffer
作为字符串使用。snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
- 使用安全的格式化函数,防止缓冲区溢出。snprintf
参数设计简洁说明:
buffer
:目标字符数组,用于存储格式化后的字符串。sizeof(buffer)
:指定缓冲区大小,防止溢出。"%s-%d-%d"
:定义字符串格式,包含字符串、两个整数和一个分隔符。s.c_str()
:提供要插入的字符串。self
:插入的第一个整数。number++
:使用并递增的第二个整数。完整代码:
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <string>
- #include <iostream>
- #include <cstdlib>
- #include <cstdio> // 包含snprintf和perror的头文件
-
- const int NUM = 100; // 缓冲区大小
- const int N = 2; // 管道数组的大小
- using namespace std;
-
- void Write(int wfd) {
- string s = "hello, I am child";
- int number = 0;
- char buffer[NUM]; // 定义一个缓冲区
- while (true) {
- sleep(1);
- snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), getpid(), number++);
- // 构建字符串
- write(wfd, buffer, sizeof(buffer)); // 实现传参写入
- cout << number << endl; // 打印计数
- if (number >= 5) break;
- }
- }
-
- void Reader(int rfd) {
- char buffer[NUM];
- while (true) {
- buffer[0] = 0;
- ssize_t n = read(rfd, buffer, sizeof(buffer)); // 读取缓冲区
- if (n > 0) {
- buffer[n] = 0; // 终止字符串,打印
- cout << "father get a message[" << getpid() << "]# " << buffer << endl;
- } else if (n == 0) {
- cout << "father read file done!" << endl;
- break;
- } else {
- perror("Error reading from pipe"); // 打印错误信息
- break;
- }
- }
- }
-
- int main() {
- int pipefd[N] = {0}; // 管道数组描述符
- int n = pipe(pipefd);
- if (n < 0) {
- perror("Error pipe");
- return 1;
- }
-
- pid_t id = fork(); // 创建子进程
- if (id < 0) {
- perror("Error forking");
- return 2; // 回忆点1
- }
- if (id == 0) {
- // 子进程
- close(pipefd[0]);
- Write(pipefd[1]); // 传入写端
- close(pipefd[1]);
- exit(0); // 回忆点2
- } else {
- // 父进程
- close(pipefd[1]);
- Reader(pipefd[0]);
-
- // 回忆点3
- // 等待回收
- pid_t rid = waitpid(id, NULL, 0);
- if (rid < 0) {
- perror("Error waiting for child process");
- return 3;
- }
-
- close(pipefd[0]); // 关闭读端
- }
-
- cout << "Program finished." << endl;
- return 0;
- }
必须要调用 write read,系统调用,防止群众当中有坏人,操作系统起到了一个媒介的作用
note:
管道的 5 大特征
1. 具有血缘关系的进程,进行进程间通信
2. 管道只能单向通信
3. 管道是基于文件的,而文件的生命周期是随进程的!例如:不需要我们去关 0 1 2
下面特性之后会详细解释:
4. 父子进程是会进程协同的,同步与互斥的---保护管道文件的数据安全
那父进程休眠,子进程呢?
不读的时候,一瞬间就把管道写满了,可以发现:管道是有固定大小的
5. 管道是面向字节流的
可能写端写了十次一百次,读端都不管,看着就是一个一个字节的,上面提到管道有固定大小,那么是多少?
验证管道的固定大小: 使用 ulimit -a
命令可以查看当前用户进程的资源限制,其中包括管道的大小。管道大小在输出中通常标记为 pipe size
。
ulimit -a | grep "pipe size"
查看 Linux 发行版和版本: 使用 cat /etc/redhat-release
命令可以查看 Red Hat 或 CentOS 系统的发行版和版本信息。
cat /etc/redhat-release
测试管道大小: 要测试管道大小是否为 64 KB,可以使用 dd
命令尝试写入一个大于 64 KB 的文件到管道,并观察是否能够成功写入。
dd if=/dev/zero of=/dev/null bs=65536 count=1 2>&1 | grep records
如果管道大小确实是 64 KB,那么上面的命令应该能成功写入 64 KB 数据。
查找 pipe_buf
大小: pipe_buf
的大小定义在内核头文件中,可以通过查看内核源码或使用 grep
命令在内核头文件中搜索。制
grep -R "define PIPE_BUF" /usr/include/linux
或者,如果你有内核源码树,可以在源码中搜索:
grep -R "define PIPE_BUF" /path/to/linux-source-code/include/linux
进行原子性读取测试: 为了确保在不间断的情况下完整读取 “hello world” 字符串,并保证原子性,可以创建一个管道,并在一个进程中写入字符串,在另一个进程中读取。
管道的四种情况
eg. 父进程等待子进程(Z)退出,测试 n=0;
4. 写端是正常写入,读端关闭了的情况呢?敬请期待下篇分解~
一些小思考:
1. 一个进程中可以开两个 pipe:
- 一个进程可以创建多个管道,每个管道都有独立的读写端。
2. read 读到文件尾,write 一写就会更新到文件头:
- 管道是循环缓冲区,写入数据会覆盖最早的数据,read 指针会自动跳到新的数据开始处。
3. 覆盖写,但为什么不会读取垃圾信息?read 指针也在移动:
- 管道确保了写入和读取的原子性,每次读取都是连续的数据块,不会读取到被覆盖的旧数据。
拓展:
管道(pipe)是一种在进程间进行单向通信的机制。
为什么管道文件无法实现“写了就马上读到”:
总结来说,管道不是立即写入即读取的同步机制,而是一个提供了有序、缓冲的通信方式,其设计考虑了数据的完整性和进程间的同步。如果需要立即写入后读取,可能需要考虑其他通信机制,如消息队列或信号量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。