- 什么是通信
1.数据传输:一个进程将数据传给另一个进程。
2.资源共享:多个进程共享同样的资源。
3通知事件:一个进程向另一个进程或一组进程发送信息,通知它发生了某种事件。
4.进程控制:有些进程希望控制其他进程 (如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它状态的改变。
- 为什么要有通信?
进程具有独立性,有时候我们需要多进程协同,来完成某种任务。
- 怎么办?
有两种通讯标准。
POSIX接口聚焦在让通信可以跨主机。
System V接口聚焦在本地通信。
System V比较少用,因为它无法跨主机,它诞生比较早,接口使用和文件无关。
- 通信方案
1.管道-基于文件系统
- 匿名管道
管道是Unix中最古老的进程间通信的形式。
从一个进程连接到另一个进程的一个数据流叫管道。
创建子进程时,文件不需要重新拷贝一份。
文件描述符表要拷贝一份。因为维护进程和文件之间的映射关系是进程找文件的,进程缺不了这个描述符表。拷贝下来的文件描述符表就能访问原先相同的文件。
如何理解通信本质问题?
1.操作系统直接或间接给通信双方的进程提供内存空间。
2.要通信的进程必须看到一份公共的资源。
不同的通信种类本质就是,上面所说的公共资源是操作系统中的哪一模块。
我们让两个不同的进程看到一份公共的资源,如果这个公共资源由文件系统提供,就叫管道通信。
如果公共资源由操作系统内System V对应的通信模块提供,就叫SystemV提供的通信方式。
如果提供的是一大块内存,就叫共享内存,共享内存如果是一个计数器,就叫信号量,如果是一个队列,就叫消息队列。
为什么说通信成本不低?
1.需要先让不同进程看到同一个资源。 (目前我们学习的是这个)
2还要通信传递信息。
文件内部一般包含两套资源
1.文件的操作方法
2.有属于自己的内核缓冲区。
所以父子进程能看到同一份内核资源,这个资源就是同一个文件的内核缓冲区,由文件系统提供。
我们把使用文件的方式进行父子进程之间的通信中被使用的内核资源文件叫管道文件。
进程间通信的数据不会刷新到磁盘,那样会降低效率。
不用访问磁盘,操作系统就能创建内核缓冲区。
进程间通信数据不刷新到磁盘。所以管道文件是内存级文件。
如何让两个进程看到同一个管道文件?
fork创建子进程完成的。
fork之后,子进程继承父进程文件描述符表,struct file文件地址是一样的。
内存级管道文件没有名字,父子进程看到同一个管道文件,是通过fork继承实现的,它俩是同一个文件描述符表,能看到同一个管道文件,所以这个管道没有名字,所以叫匿名管道。
一般而言,管道只能用来进行单向数据通信。
完成双向通信,可以建立两个管道,一个管道进行双向通信技术上回更复杂。
父进程以读和写的方式打开同一个文件,只有这样,读写文件描述符才会被子进程继承,让子进程也能看到读写端,后续能自由选择对应的通信方向。
但是直到现在,我们还没有通信,我们仅仅让不同进程看到同一个资源。
管道:父进程通过调用管道特定的系统调用,以读和写方式打开内存级文件,并通过fork创建子进程的方式,被子进程继承后,各种关闭各自读写端,进而形成的一个通信信道。
匿名管道能进行父子进程之间进程通信。
管道是父进程申请的。
第一步创建管道文件,打开读写端。
pipe函数用来创建管道文件。
它的参数pipefd是输出形参数,最大意义是调用pipe时,操作系统在操作系统对应的内部分别以读和写方式打开,填充对应进程的文件描述符表,把读端和写端文件描述符数组下标给pipe[2],此时以读方式和写方式分别打开了同一个文件。
创建失败返回-1。
操作系统把文件在此进程中的读入段与写入端文件描述符表写入这个数组。
vscode中选中内容后Ctrl+/进行加注释和去注释。
[0]是读取,[1]是写入。
0像嘴巴,是读取的,1像笔,是写的。
第二步,fork创建子进程。
写管道代码不能写文件描述符常数。
必须使用数组下标访问。
waitpid头文件
第三步,让子进程读取,父进程写入。 (反过来也行)
子进程进行写入,关闭读取文件描述符。父进程进行读取,关闭写入文件描述符。
进程退出,和这个进程有关的文件描述符就会被关闭。
为什么不能定义一个全局的缓冲区,子进程在缓冲区中写数据后,父进程就能看到?
因为存在写时拷贝。
下面的后面为什么不加1,因为以\0为字符串的结尾,管道文件不这么考虑。
父进程读取时因为是从文件转到c语言,所以最好减1.
下面展示一下进程通信例子的完整代码。
#include<iostream>
#include<cassert>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int n=pipe(fds);
assert(n==0);
pid_t id=fork();
assert(id>=0);
if(id==0)
{
//子进程的通信代码
close(fds[0]);//关闭读端
int cnt=0;
const char*s="我是子进程,我正在给你发信息";
while(true)
{
char buffer[1024];
cnt++;
snprintf(buffer,sizeof (buffer),"child->parent say: %s[%d][%d]",s,cnt,getpid());
// printf("%s\n",buffer);
write(fds[1],buffer,strlen(buffer));
sleep(1);
}
close(fds[1]);
exit(0);
}
//父进程的通信代码
close(fds[1]);//关闭写端
while(true)
{
char buffer[1024];
size_t s=read(fds[0],buffer,sizeof(buffer)-1);
if(s>0)buffer[s]=0;
cout<<"Get massage # " <<buffer<<"|my pid "<<getpid()<<endl;
//注意:父进程没有sleep(1)
}
close(fds[0]);
int m=waitpid(id,nullptr,0);
assert(id==m);
// cout<<"fds[0]:"<<fds[0]<<endl;
// cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
运行结果。
注意上述代码,中assert(id>=0),如果是id>0,那么它会杀掉子进程只运行父进程,因为在assert(id>0)中,assert会认为id==0是错的,所以程序会杀掉子进程。
操作系统直接或间接给通信双方的进程提供内存空间。提供了管道文件文件 。
- 管道特点。
子进程休眠时,父进程在读取
#include<iostream>
#include<cassert>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int n=pipe(fds);
assert(n==0);
pid_t id=fork();
assert(id>=0);
if(id==0)
{
//子进程的通信代码
close(fds[0]);//关闭读端
int cnt=0;
const char*s="我是子进程,我正在给你发信息";
while(true)
{
char buffer[1024];
cnt++;
snprintf(buffer,sizeof (buffer),"child->parent say: %s[%d][%d]",s,cnt,getpid());
// printf("%s\n",buffer);
write(fds[1],buffer,strlen(buffer));
sleep(10);
}
close(fds[1]);
exit(0);
}
//父进程的通信代码
close(fds[1]);//关闭写端
while(true)
{
char buffer[1024];
cout<<"AAAAAAAA"<<endl;
size_t s=read(fds[0],buffer,sizeof(buffer)-1);
cout<<"BBBBBBBB"<<endl;
if(s>0)buffer[s]=0;
cout<<"Get massage # " <<buffer<<"|my pid "<<getpid()<<endl;
//注意:父进程没有sleep(1)
}
close(fds[0]);
int m=waitpid(id,nullptr,0);
assert(id==m);
// cout<<"fds[0]:"<<fds[0]<<endl;
// cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
上述例子可以看出,子进程休眠10秒期间,父进程一直在读取。
如果管道中没有了数据,读端一直在读取的话,那么系统会默认阻塞正在读取的进程。
此时读端进程pcb会进入等待序列中。
- 而如果让子进程一直写,父进程不读呢?
#include<iostream>
#include<cassert>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int n=pipe(fds);
assert(n==0);
pid_t id=fork();
assert(id>=0);
if(id==0)
{
//子进程的通信代码
close(fds[0]);//关闭读端
int cnt=0;
const char*s="我是子进程,我正在给你发信息";
while(true)
{
char buffer[1024];
cnt++;
snprintf(buffer,sizeof (buffer),"child->parent say: %s[%d][%d]",s,cnt,getpid());
// printf("%s\n",buffer);
write(fds[1],buffer,strlen(buffer));
cout<<"count:"<<cnt<<endl;
}
close(fds[1]);
exit(0);
}
//父进程的通信代码
close(fds[1]);//关闭写端
while(true)
{
sleep(1000);
char buffer[1024];
cout<<"AAAAAAAA"<<endl;
size_t s=read(fds[0],buffer,sizeof(buffer)-1);
cout<<"BBBBBBBB"<<endl;
if(s>0)buffer[s]=0;
cout<<"Get massage # " <<buffer<<"|my pid "<<getpid()<<endl;
//注意:父进程没有sleep(1)
}
close(fds[0]);
int m=waitpid(id,nullptr,0);
assert(id==m);
// cout<<"fds[0]:"<<fds[0]<<endl;
// cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
管道是有空间的,此时管道会满,子进程就不会写入了。
- 而如果父进程改为2秒读一次
那么会出现这样的结果。
父进程没有按行读取,是因为它读取是按buffer的大小读取的,我们写入时并没有写换行,所以它就一下读取了buffer大小的数据。
- 而如果子进程写一次就把写端关掉会发生什么?
#include <iostream>
#include <cassert>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
pid_t id = fork();
assert(id >= 0);
if (id == 0)
{
// 子进程的通信代码
close(fds[0]); // 关闭读端
int cnt = 0;
const char *s = "我是子进程,我正在给你发信息";
while (true)
{
char buffer[1024];
cnt++;
snprintf(buffer, sizeof(buffer), "child->parent say: %s[%d][%d]\n", s, cnt, getpid());
// printf("%s\n",buffer);
write(fds[1], buffer, strlen(buffer));
cout << "count:" << cnt << endl;
break;
}
close(fds[1]);
cout<<"子进程关闭写端"<<endl;
exit(0);
}
// 父进程的通信代码
close(fds[1]); // 关闭写端
while (true)
{
char buffer[1024];
size_t s = read(fds[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{buffer[s] = 0;
cout << "Get massage # " << buffer << "|my pid " << getpid() << endl;
}
else if(s==0)
{
cout<<"NULL"<<endl;
break;
}
// 注意:父进程没有sleep(1)
}
close(fds[0]);
int m = waitpid(id, nullptr, 0);
assert(id == m);
// cout<<"fds[0]:"<<fds[0]<<endl;
// cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
如果不break,父进程的s会一直是0.
- 读写特征
如果子进程只写一次就退出,那么父进程只会读到这一次的数据。
如果读入关闭,操作系统会给写入端发信号,终止写入端。
- 为什么读关闭,写会停止,写关闭,读不停止,写关闭,读不停止不同样浪费资源吗?
读关闭了,写就没有意义了 ,而写关闭,之前写的数据是可以继续读取的。
此时子进程退出码是13。
- 管道特征
- 命名管道
命名管道是如何让不同进程看到同一个资源的?
可以让不同进程打开指定名称(路径+文件名)的同一个文件。
命名管道利用路径+文件名实现了唯一性。