进程具有独立性,我们现在的进程间需要通信,那么这个成本一定不低
有时候我们需要多进程协同操作,比如:
cat file.txt | grep hello'
如何理解通信的本质问题?
不同的通信种类:
本质就是:上面所说的资源,是由OS中的哪一个模块提供的。
不同的进程实现通信,第一条件就是需要先让不同的进程看到同一份资源(我们主要学习的就是这个)
进程间通信分类,标准方法:
跟上面两种不一样,管道是基于文件系统的通信方式:
什么是管道?
由父进程fork创建子进程来使得两个进程看到同一个管道文件,这种管道叫做匿名管道
匿名管道它是在内存中创建的一段缓冲区,可以用于在两个进程之间传递数据。由于匿名管道是在内存中创建的,因此不需要与磁盘进行IO。
使用匿名管道进行进程间通信时,数据是从一个进程中写入管道,然后从另一个进程中读取。这个过程是通过操作系统内核来实现的,因此不需要涉及磁盘IO操作。操作系统会负责将数据从一个进程的地址空间复制到管道缓冲区中,并将数据从管道缓冲区复制到另一个进程的地址空间中。
匿名管道是一种半双工的通信机制,即只能在一个方向上传递数据。如果需要进行双向通信,需要创建两个管道。匿名管道目前能用于父子进程之间的通信
父进程需要分别以读和写打开同一个文件,是因为管道是一个特殊的文件,它只存在于内存中,没有对应的磁盘文件。管道是单向的,所以需要使用两个文件描述符来实现双向通信。父进程需要以读模式打开管道的读取端,以便能够从管道读取数据;同时,父进程也需要以写模式打开管道的写入端,以便能够向管道写入数据。因此,父进程需要分别以读和写模式打开同一个文件,来实现对管道的读写操作。
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
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
验证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
如有错误或者不清楚的地方欢迎私信或者评论指出