- 什么是通信?
- 为什么要有通信?
- 如何实现?
- 管道通信
- 匿名管道
- pipe系统调用
- 读写特征
- 管道的特征:
什么是通信?
进程具有独立性,我们现在的进程间需要通信,那么这个成本一定不低
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
为什么要有通信?
有时候我们需要多进程协同操作,比如:
cat file.txt | grep hello'
两个进程通过管道的方式协同操作
如何理解通信的本质问题?
- 操作系统OS需要直接或间接的给通信双方的进程提供“内存空间”,这个“内存空间”肯定不是属于通信双方的,因为进程具有独立性,如果要存在,要么是一方的要么就是另一方的,所以这个“内存空间”,肯定是由“第三方”也就是OS提供的
- 要通信的进程,必须看到一份公共的资源
不同的通信种类:
本质就是:上面所说的资源,是由OS中的哪一个模块提供的。
不同的进程实现通信,第一条件就是需要先让不同的进程看到同一份资源(我们主要学习的就是这个)
如何实现?
进程间通信分类,标准方法:
- POSIX – 通信过程可以跨主机
- System V – 聚焦在本地通信
管道通信
跟上面两种不一样,管道是基于文件系统的通信方式:
- 匿名管道
- 命名管道
什么是管道?
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道"
匿名管道
由父进程fork创建子进程来使得两个进程看到同一个管道文件,这种管道叫做匿名管道
匿名管道它是在内存中创建的一段缓冲区,可以用于在两个进程之间传递数据。由于匿名管道是在内存中创建的,因此不需要与磁盘进行IO。
使用匿名管道进行进程间通信时,数据是从一个进程中写入管道,然后从另一个进程中读取。这个过程是通过操作系统内核来实现的,因此不需要涉及磁盘IO操作。操作系统会负责将数据从一个进程的地址空间复制到管道缓冲区中,并将数据从管道缓冲区复制到另一个进程的地址空间中。
匿名管道是一种半双工的通信机制,即只能在一个方向上传递数据。如果需要进行双向通信,需要创建两个管道。匿名管道目前能用于父子进程之间的通信
父进程需要分别以读和写打开同一个文件,是因为管道是一个特殊的文件,它只存在于内存中,没有对应的磁盘文件。管道是单向的,所以需要使用两个文件描述符来实现双向通信。父进程需要以读模式打开管道的读取端,以便能够从管道读取数据;同时,父进程也需要以写模式打开管道的写入端,以便能够向管道写入数据。因此,父进程需要分别以读和写模式打开同一个文件,来实现对管道的读写操作。
pipe系统调用
pipe的用法:
#include <iostream>
#include <cassert>
#include <unistd.h>
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);
assert(n==0);
//[0]是读取
//[1]是写入
cout<<"fds[0]:"<<fds[0]<<endl;
cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
在Linux中,每个进程都有一个内核空间和一个用户空间。管道在内核空间中实现,而进程在用户空间中实现。当一个进程调用pipe()函数时,内核会创建一个管道,并返回两个文件描述符,一个是读取端的文件描述符,一个是写入端的文件描述符。这两个文件描述符都是指向内核中的管道,而不是指向文件系统中的文件。
当一个进程想要向管道中写入数据时,它将数据写入到写入端的文件描述符中。这个数据将被存储在管道中,等待其他进程读取。当一个进程想要从管道中读取数据时,它从读取端的文件描述符中读取数据。这个读取操作会从管道中删除一个数据,其他数据会向前移动。
在Linux中,管道的实现是通过环形缓冲区来实现的。当数据被写入管道时,它会被存储在环形缓冲区中。当数据被读取时,它会从环形缓冲区中删除。如果缓冲区已满,写入操作将会阻塞,直到有一些数据被读取出来。如果缓冲区为空,读取操作将会阻塞,直到有一些数据被写入进来。
管道通信代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
// 父进程进行读取,子进程进行写入
int main()
{
// 第一步:创建管道文件,打开读写端
int fds[2];
int n = pipe(fds);
assert(n == 0);
// 第二步: fork
pid_t id = fork();
assert(id >= 0);
if (id == 0)
{
// 子进程进行写入,那么它就要关闭读取端
close(fds[0]);
// 子进程的通信代码
const char *s = "我是子进程,我正在给你发消息";
int cnt = 0;
while (true)
{
cnt++;
char buffer[1024]; // 只有子进程能看到!
snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());
write(fds[1], buffer, strlen(buffer));
cout << "count: " << cnt << endl;
sleep(1); //细节,我每隔1s写一次
}
exit(0);
}
// 父进程进行读取,那么它就要关闭写入端
close(fds[1]);
// 父进程的通信代码
while (true)
{
char buffer[1024];
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
}
// 细节:父进程可没有进行sleep
}
n = waitpid(id, nullptr, 0);
assert(n == id);
close(fds[0]);
return 0;
}
输出结果:
上面的代码子进程全程没有往显示器打印消息,打印的消息全是父进程在做,这种通信方式也就叫管道通信
为什么我们在使用的时候是需要用close(fds[0]);
这种方式去关闭,而不是直接以数字为关闭符号,因为虽然我们知道012是以及被标准输入输出错误提前占领,我们接下来就是使用的34号位,但是我们不能够确定前面没有创建过文件,而将位置已经占了,所以我们最好用fds[0]
这种方式
如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
管道是一段固定大小的缓冲区,如果写端满了的时候,再写会阻塞,等对方进行读取!而此时要读取是直接将整个缓冲区有的全读取,不是一行一行读取
我们在读取sizeof(buffer) - 1/strlen(buffer)
计算长度大小的时候有时候减1有时候不减有时候加1有时候不加,是因为C语言到文件系统,文件系统不需要以\0
结尾,所以我们不需要多加1传入\0
,而文件到C语言,我们需要多留出一个位置来存放\0
读写特征
- 读慢,写快
- 读快,写慢
- 写关闭,读到0(终止符号)截至
- 读关闭,OS会给进程发送信号,终止写端
验证4:我们使子进程一直写,然后父进程只写一次后关闭自己的读端,我们观察一下进程的退出:
//上面代码不用更改,只需要子进程改为不睡眠一直写
close(fds[0]);
cout << "父进程关闭读端" << endl;
int status = 0;
n = waitpid(id, &status, 0);
assert(n == id);
cout <<"pid->"<< n << " : "<< (status & 0x7F) << endl;
[AMY@VM-12-15-centos lesson_14]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
我们可以发现,当父进程关闭读端,子进程会被终止,且其终止信号为:SIGPIPE
管道的特征:
- 管道的生命周期是随进程。 进程退出,管道在内核中所对应的资源就释放
- 管道可以用来进行具有血缘关系的进程之间进行通信,常用与父子通信
- 管道是面向字节流的(网络)
- 半双工–单向通信(特殊概念)
- 互斥与同步机制–对共享资源进行保护的方案
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀