1. 进程间通信介绍
1-1 进程间通信目的
• 数据传输:一个进程需要将它的数据发送给另一个进程
• 资源共享:多个进程之间共享同样的资源。
• 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
• 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1-2 进程间通信发展
• 管道
• System V进程间通信
• POSIX进程间通信
2. 管道
什么是管道
• 管道是Unix中最古老的进程间通信的形式。
• 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
进程间通信的前提是让不同的进程看到同一份资源
父子进程向同一标准输出个打印本质就是显示器文件被共享了
管道的定义:
管道是一个基于文件系统的一个内存级的单向通信文件,主要用来进程间通信(IPC)
3. 匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
3-1代码实例
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fd[2] = {0};
// 创建匿名管道
int ret = pipe(fd);
if (ret != 0)
perror("pipe:");
pid_t id = fork();
if (id < 0)
perror("fork:");
else if (id == 0)
{
// child write
// 子进程关闭管道读端
close(fd[0]);
int cnt = 0;
while (cnt < 10)
{
sleep(1);
char c = 'a' + cnt;
cnt++;
write(fd[1], &c, 1);
}
}
else
{
// father read
// 父进程关闭管道写端
close(fd[1]);
while (1)
{
char c;
ssize_t n = read(fd[0], &c, 1);
if (n < 0)
perror("read error:");
else if (n > 0)
{
cout << "父进程写:" << c << endl;
}
else if (n == 0)
{
// 文件读完
cout << "文件读完" << endl;
break;
}
}
}
// // 休眠5秒后,父进程关闭读端,看子进程会如何(此时子进程仍然在写)
// sleep(5);
// // 父进程关闭读端,此时管道只有子进程的写端
// close(fd[0]);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
int exitcode = (status >> 8) & 0xFF;
int exitsig = (status & 0x7F); // 13
cout << "wait success, rid: " << rid << ", exit code: " << exitcode << ", exitsig: " << exitsig << endl;
}
return 0;
}
3-2 用 fork 来共享管道原理
匿名管道是一个内存级的文件,没有名字,不在磁盘上,因此其文件缓冲区上的内容就不需要向磁盘刷新
父进程fork后子进程继承了父进程的匿名管道,此时父子进程可以看到同一份资源
3-3 站在文件描述符角度-深度理解管道
父进程关闭fd[0],子进程关闭fd[1],此时就形成了一个单向通信的管道
父子进程关闭对应的不需要读写端是为了防止误触发
3-4读写规则
1. 单向通信
管道是半双工的,数据只能在一个方向上流动。一个进程写,另一个进程读。如果需要双向通信,必须创建两个管道。
2. 内核缓冲区
管道拥有一个由内核维护的缓冲区。写入者将数据写入这个缓冲区,读取者从同一个缓冲区读取数据。一旦数据被读取,它就会从管道中消失。缓冲区有大小限制。在 Linux 上,默认通常是64KB。
3. 原子性
当要写入的数据量小于等于 PIPE_BUE(Linux 上通常是 4096 字节)时,写入操作是原子的。
这意味着多个进程同时向同一个管道写入小于PIPE_BUE 的数据块时,这些数据块不会相互穿插,保证了数据的完整性。
如果写入的数据量大于 PIPE_BUE,那么内核可能会将数据与其它进程写入的数据交织在一起,即非原子操作。
4. 生命周期
管道的生命周期与其关联的进程绑定。当所有指向管道读端和写端的文件描述符都被关闭后,管道会被内核销毁,其占用的资源也会被释放
3-5验证管道通信的4种情况
• 读正常,读端不读&&写满
情况描述
- 写进程一直写使管道缓冲区已满
- 写进程尝试继续写入
- 读进程正常,不关闭读端但也不读取数据
结果
- 写进程阻塞,直到读进程读取数据腾出缓冲区空间
- 管道有最大容量限制(通常64KB),即写进程一次最多向文件缓冲区中写入64KB的数据
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fd[2] = {0};
// 创建匿名管道
int ret = pipe(fd);
if (ret != 0)
perror("pipe:");
pid_t id = fork();
if (id < 0)
perror("fork:");
else if (id == 0)
{
// child write
// 子进程关闭管道读端
close(fd[0]);
int cnt = 0;
while (true)
{
//sleep(1);
int c = cnt;
cnt++;
write(fd[1], &c, 1);
cout<<"子进程写:"<<c<<endl;
}
}
else
{
// father read
// 父进程关闭管道写端
close(fd[1]);
//父进程正常,但不关闭读端也不读取数据
}
return 0;
}
write写入的数据是字节流形式的,即一个一个字符
可以看到,子进程从0开始一直写入数据,直到写到65535时阻塞到那里 65536/1024=64
• 写正常&&读空
情况描述
- 管道缓冲区为空
- 读进程尝试读取
- 写进程正常写入(但尚未写入)
结果
- 读进程阻塞,直到写进程写入数据
#include <unistd.h>
#include <stdio.h>
#include<stdlib.h>
int main() {
int fd[2];
pipe(fd);
if (fork() == 0) {
// 子进程 - 读进程(立即读取)
close(fd[1]);
char buf[100];
printf("Reading from empty pipe...\n");
int n = read(fd[0], buf, sizeof(buf));
printf("Read %d bytes\n", n);
exit(0);
} else {
// 父进程 - 写进程(延迟写入)
close(fd[0]);
sleep(3); // 延迟3秒写入
write(fd[1], "Data", 5);
printf("Data written\n");
}
return 0;
}
• 写关闭&&读正常
情况描述
- 所有写端文件描述符已关闭
- 读进程继续读取
结果
- 读进程读取完缓冲区所有数据后,read返回0(文件结束)
- 不会阻塞等待新数据
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<stdlib.h>
using namespace std;
int main()
{
int fd[2] = {0};
// 创建匿名管道
int ret = pipe(fd);
if (ret != 0)
perror("pipe:");
pid_t id = fork();
if (id < 0)
perror("fork:");
else if (id == 0)
{
// child write
// 子进程关闭管道读端
close(fd[0]);
int cnt = 0;
while (cnt<10)
{
//子进程写
char c = 'a'+cnt;
cnt++;
write(fd[1], &c, 1);
}
//子进程退出,写端关闭
exit(0);
}
else
{
// father read
// 父进程关闭管道写端
close(fd[1]);
while (1)
{
char c;
ssize_t n = read(fd[0], &c, 1);
if (n < 0)
perror("read error:");
else if (n > 0)
{
cout << "子进程写:" << c << endl;
}
else if (n == 0)
{
// 文件读完
cout << "文件读完" <<"read返回值:"<<n<< endl;
break;
}
}
}
return 0;
}
• 读关闭&&写正常
情况描述
- 所有读端文件描述符已关闭
- 写进程继续写入
结果
- 写进程收到 SIGPIPE 信号(默认终止进程)
- 如果忽略SIGPIPE,write返回-1,errno=EPIPE
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fd[2] = {0};
// 创建匿名管道
int ret = pipe(fd);
if (ret != 0)
perror("pipe:");
pid_t id = fork();
if (id < 0)
perror("fork:");
else if (id == 0)
{
// child write
// 子进程关闭管道读端
close(fd[0]);
int cnt = 0;
while (cnt < 10)
{
sleep(1);
char c = 'a' + cnt;
cnt++;
write(fd[1], &c, 1);
}
}
else
{
// father read
// 父进程关闭管道写端
close(fd[1]);
}
// 休眠5秒后,父进程关闭读端,看子进程会如何(此时子进程仍然在写)
sleep(5);
// 父进程关闭读端,此时管道只有子进程的写端
close(fd[0]);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
int exitcode = (status >> 8) & 0xFF;
int exitsig = (status & 0x7F); // 13号信号SIGPIPE 信号
cout << "wait success, rid: " << rid << ", exit code: " << exitcode << ", exitsig: " << exitsig << endl;
}
return 0;
}
注意:匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;
通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
4. 命名管道
• 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
• 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道:是一种特殊的文件类型,它在文件系统中有一个文件名,但不存储实际数据在磁盘上。数据仍然在内核的缓冲区中流动。
4-1 创建一个命名管道
• 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
4-2 匿名管道与命名管道的区别
• 匿名管道由pipe函数创建并打开。
• 命名管道由mkfifo函数创建,打开用open
• FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道主要解决毫无关系的进程之间文件级通信的问题
4-3用命名管道实现server&client通信
//server.cpp
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>
#include<cstring>
using namespace std;
int main()
{
int ret=mkfifo("np",0666);
if(ret<0)
{
perror("mkfifo:");
return 1;
}
int fd=open("np",O_WRONLY);
if(fd<0)
{
perror("open:");
return 1;
}
char a[100]={0};
int n = 0;
while((n = read(0,a,100)) > 0)
{
a[n]=0;
//server写
write(fd,a,n);
}
return 0;
}
//client.cpp
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>
#include<cstring>
using namespace std;
int main()
{
int fd=open("np",O_RDONLY);
if(fd<0)
{
perror("open:");
return 1;
}
char a[100]={0};
int n = 0;
//client读
while((n = read(fd,a,100)) > 0)
{
a[n]=0;
cout<<a;
}
return 0;
}
//Makefile
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -std=c++11 -g
client:client.cpp
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f client
rm -f server
到此,进程间通信:管道就讲完了,怎么样,是不是感觉大脑里面多了很多新知识。
如果觉得博主讲的还可以的话,就请大家多多支持博主,收藏加关注,追更不迷路
如果觉得博主哪里讲的不到位或是有疏漏,还请大家多多指出,博主一定会加以改正
博语小屋将持续为您推出文章