目录
一、进程间通信的目的:
在学习通信间进程前我们要明白进程间通信的目的意义是是什么,从而方便我们在学习的过程中对我们所学的知识有一个清晰的认知
1.传输数据
将一个进程的数据发送给另一个进程
2.资源共享
让多个进程共享所需要的资源
3.通知事件
一个进程向另一个进程发送消息,通知它发生了某种事件(如进程终止时要通知父进程)
4.进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
二、进程间通信的种类
1.管道
- 匿名管道
- 命名管道
2.System V
- System V 消息队列
- System V 共享内存
- System V 信号量
3.POSIX
- 消息队列
- 共享内存
- 信号量
- 互斥锁
- 条件变量
- 读写锁
在本章中主要讲解管道通信
三、什么是管道
- 管道是操作系统中最古老的进程间通信的方式
- 我们把一个进程连接到另一个进程的一个数据流称为一个“管道”
四、匿名管道(共同祖先的进程之间)
1.匿名管道的使用
#include <unistd.h>
int pipe(int fd[2]);
匿名管道的功能是创建一个无名管道
- fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
- 返回值:成功返回0,失败返回错误代码
如果上述看的不太明白的话可以去看看这篇文章,里面有对文件描述符及数组的详细讲解
https://blog.youkuaiyun.com/liuty0125/article/details/140828056?spm=1001.2014.3001.5502
2.匿名管道举例
1.先创建管道, 进而创建子进程, 父子进程使用管道进行通信
父进程向管道当中写“i am father”,
子进程从管道当中读出内容, 并且打印到标准输出
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<string>
using namespace std;
int main()
{
int fd[2];
int f=pipe(fd);
char buf[100];
const char* fa="I am father";
int len=strlen(fa);
if(f==-1)
{
perror("make pipe");
exit(1);
}
int pid = fork();
if(pid==-1)
{
perror("fork failed");
return 1;
}
if(pid>0)//父进程
{
write(fd[1],fa,len);
}
if(pid==0)
{
read(fd[0],buf,len);
printf("%s",buf);
}
return 0;
}
2.从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include<sys/types.h>
#include<string>
int main(void)
{
int fds[2];
char buf[100];
int len;
if (pipe(fds) == -1)
perror("make pipe"), exit(1);
// read from stdin
while (fgets(buf, 100, stdin)) {
len = strlen(buf);
// write into pipe
if (write(fds[1], buf, len) != len) {
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if ((len = read(fds[0], buf, 100)) == -1) {
perror("read from pipe");
break;
}
// write to stdout
if (write(1, buf, len) != len) {
perror("write to stdout");
break;
}
}
}
3.匿名管道的原理
我们知道,进程将一个文件以读方式打开一次会产生一个FILE*,再以写方式打开一次,也会产生一个FILE*,但是这两个文件流指针指向的是同一个缓存区
内容只有一份基于文件的,让不同进程看到同一份资源的通信方式,叫做管道,管道只能被设计成为单向通信
如:ajx | ps | … 这中间的管道是兄弟关系
我们可以让父子进程都打开这个文件,再让父进程关闭读端,子进程关闭写端,这样就形成了一条由父进程通向子进程的管道,反之则形成由子进程通向父进程的管道
在这个地方其实有个bug:
通过父进程创建多个子进程,用管道文件连接父进程与各个子进程,产生的管道文件有多个读端,因为每个子进程的文件描述符表都会和创建他的父进程的文件描述符表一样,父进程的读端没有关闭,所以与接连创建的各个子进程的管道会带有上个父进程的读端
解决方案1:最后一个管道一定只有一个读写端,可以从后往前删
解决方案2:每次创建管道文件时,记录父进程的fd,然后每次创建时通过记录的上个父进程的文件描述符关闭不需要的fd
4.管道特点
- 匿名管道只能用于共同祖先的进程之间进行通信,一般都是一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用改管道
- 一般而言,匿名管道的生命周期随进程,进程退出,管道释放
- 管道是半双工的,数据只能向一个方向流动,需要双方通信时,则需要建立起两个管道
5.管道的读写规则
管道读写操作的行为会根据是否设置了非阻塞标志(O_NONBLOCK)以及管道的状态(是否有数据可读、是否已满等)有所不同
1. 当管道内没有数据可读时
- O_NONBLOCK 未设置(阻塞模式):
read调用阻塞,即进程暂停执行,一直等到有数据来到为止
- O_NONBLOCK 已设置(非阻塞模式):
read调用返回-1,errno值为EAGAIN
2.当管道满的时候
- O_NONBLOCK 未设置(阻塞模式):
write调用阻塞,直到有进程读走数据
- O_NONBLOCK 已设置(非阻塞模式):
调用返回-1,errno值为EAGAIN
3.管道端被关闭
- 所有管道的写端对应的文件描述符被关闭:
read 调用会返回 0,表示已经到达文件末尾(EOF),没有更多的数据可以读取
- 所有管道的读端对应的文件描述符被关闭:
write 调用会产生 SIGPIPE 信号,默认情况下,这会导致进程终止。可以通过捕获 SIGPIPE 信号来避免进程终止
4.数据写入的原子性
- 当要写入的数据量不大于 PIPE_BUF 时:
Linux 保证写入的原子性,即要么全部写入成功,要么完全不写入。这有助于避免数据在管道中被分割成多个部分,从而保证了数据的完整性。
- 当要写入的数据量大于 PIPE_BUF 时:
Linux 不再保证写入的原子性,数据可能会被分割成多个部分写入管道。这可能导致读端在读取数据时,需要多次调用 read 才能获取完整的数据。
五、命名管道(让两个不相干的进程通信)
1.命名管道的概念与原理
学习了上面的匿名管道,我们知道匿名管道其实是有缺陷的,它只能在具有共同祖先(具有亲缘关系)的进程间通信,那如果我们想在不相关的进程之间交换数据呢?
想要在两个不相干进程之间进行通信,我们的目的是让不同进程看到同一份资源
比如进程A通过文件描述符表打开一个文件,会为文件创建一个struct FILE,并通过文件缓冲区与磁盘进行交互
进程B也通过文件描述符打开同一个文件,也会为文件再创建一个struct FILE,但会使用同一个缓冲区,指向同一个内存空间,因此我们就可以通过这段内存空间进行通信
不过我们只是需要两个进程之间进行通信,并不需要把打开的文件内容写到缓冲区中,因此这里的文件是一个特殊的文件,也就是我们下面说的命名管道
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
- 怎么保证两个进程打开的是同一个文件?
只需要保证文件的路径与文件名一样即可,因为路径具有唯一性且同一路径下不能有同名文件
2.命名管道的创建与删除
1.在命令行上创建
- 使用
mkfifo
filename
p开头为管道文件
2.在程序中创建
int mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
3.删除文件
使用unlink()删除文件,可以用于析构函数
unlink(filename);
也就是说,想让两个不相干的进程通信,只需通过mkfifo创建一个管道文件,一个进程写,一个进程读即可
在程序中,管道文件只能创建一个,一个进程创建后,另一个进程只需直接操作就行,不需要再创建管道文件
管道中的通信都是基于文件的,可以说管道通信是基于文件的二次创新
如果写端没打开,而去先打开读端,读端open时会阻塞,直到把写端打开,读端才会返回
3.匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
4.命名管道的打开规则
- 为读而打开FIFO
O_NONBLOCK 未设置(阻塞模式):
如果当前没有进程为写而打开该FIFO,则打开操作会阻塞,直到有进程以写模式打开它。这意味着,如果FIFO是空的,并且没有写端打开,那么尝试以读模式打开FIFO的进程将被挂起,直到另一个进程以写模式打开它
O_NONBLOCK 已设置(非阻塞模式):
如果当前没有进程为写而打开该FIFO,则打开操作会立即成功,但是后续的read操作可能会失败,并返回-1,同时设置errno为EAGAIN(表示资源暂时不可用,可以在以后重试)。
- 为写而打开FIFO
O_NONBLOCK 未设置(阻塞模式):
如果当前没有进程为读而打开该FIFO,则打开操作会阻塞,直到有进程以读模式打开它。这意味着,如果FIFO没有读端,那么尝试以写模式打开FIFO的进程将被挂起,直到另一个进程以读模式打开它
O_NONBLOCK 已设置(非阻塞模式):
如果当前没有进程为读而打开该FIFO,则打开操作会立即失败,并返回-1,同时设置errno为ENXIO(表示设备不存在或没有这样的设备文件)。
在非阻塞模式下,如果没有读端可用,写端不能立即打开FIFO。
5.命名管道举例(附完整代码)
创建出来的命名管道可以供两个进程通信
进程A 向管道当中写 “i am process A”
进程B 从管道当中读 并且打印到标准输出
1.makefile文件
.PHONY:all
all:w r
w:wFifo.cpp
g++ -o $@ $^ -std=c++11
r:rFifo.cpp
g++ -o $@ $^ -std=c++11
PHONY:clean
clean:
rm -f r w
2.wFifo.cpp
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <stdio.h>
#include<unistd.h>
void errS(const char* s)
{
perror(s);
exit(EXIT_FAILURE);
}
int main()
{
const char* s="/home/xiaoliu/code/Fifo/__fifo";
char a[1024]="i am process A";
mkfifo(s,0666);
int wfd=open(s,O_WRONLY);
if(wfd < 0)
{
errS("open");
}
int w=write(wfd,a,sizeof(a-1));
if(w<0)
{
errS("write");
}
return 0;
}
3.rFifo.cpp
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <stdio.h>
#include<unistd.h>
#include <iostream>
void errS(const char* s)
{
perror(s);
exit(EXIT_FAILURE);
}
int main()
{
const char* s="/home/xiaoliu/code/Fifo/__fifo";
char buf[1024];
int rfd=open(s,O_RDONLY);
if(rfd<0)
{
errS("open");
}
int n = read(rfd,buf,sizeof(buf)-1);
if(n>0)
{
buf[n]='\0';
std::cout<<"A said: "<<buf;
}
else if(n==0)
{
std::cout<<"A didn't speak";
}
else
{
std::cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
}
return 0;
}
六、关于匿名管道与命名管道的细节问题
匿名管道
- 进程之间可以通过地址访问进行相互通行吗?
错误,进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)
- 所有的进程间通信都是通过内核中的缓冲区实现的吗?
错误,除了内核中的缓冲区之外还有文件以及网络通信的方式可以实现
- 管道的容量仅受磁盘容量大小限制吗?
错误,管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质
- 一个管道只能有一个读进程或写进程对其操作吗?
错误,多个进程只要能够访问同一管道就可以实现通信,不限于读写个数
- 程对管道进行读操作和写操作都可能被阻塞吗?
正确,管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥
- 管道的本质是内核中的一块缓冲区?
正确,管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信
命名管道
- .命名管道可以用于同一主机上的任意进程间通信?
正确,命名管道可用于同一主机上的任意进程间通信
- 向命名管道中写入的数据越多,则管道文件越大?
错误,管道的通信本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区,不是磁盘文件
- 多个进程在通过管道通信时,删除管道文件则无法继续通信?
错误,管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
- 命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区?
正确
- .命名管道在磁盘空间足够的情况下可以持续写入数据?
错误,管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
- 若以只读或只写的方式打开命名管道时,则会阻塞?
正确,当一个进程尝试以只读方式打开命名管道时,如果该管道尚未被任何进程以写方式打开,则打开操作会阻塞,阻塞将持续到另一个进程以写方式打开该管道为止。此时,读进程可以继续进行,并尝试从管道中读取数据。反之亦然