目录
进程间通信的定义
进程间通信(Inter-Process Communication ---- IPC)的定义:不同进程之间交换数据、传递信息、协同工作的机制。
为什么要有IPC?
因为每个进程具有独立性,进程之间是相互隔离的。
进程间通信的目的
数据传输:一个进程需要将它的数据发送个另一个进程。(一个进程生成数据交给另一个进程来处理数据)
资源共享:多个进程之间共享同样的资源。(多个进程通过共享内存,同时读取同一份配置数据,避免数据拷贝)
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,使其执行某种回应。(子进程退出时,通过信号通知父进程,让父进程回收资源)
进程控制:有些进程希望完全控制另一个进程的执行(例如调试进程),来监视它的运行过程。
进程间通信的发展
管道 IPC -> System V IPC -> POSIX IPC
进程间通信的分类
管道 IPC:匿名管道 ---- 命名管道
System V IPC:System V ---- 消息队列 ---- System V 共享内存 ---- System V 信号量
POSIX IPC:消息队列 ---- 共享内存 ---- 信号量 ---- 互斥量 ---- 条件变量 ---- 读写锁
进程间通信的方式
单工:单向固定传输,只能一方发、一方收,无法反向。(例如信号)
半双工:双方均可收发,同一时刻只能单向传输。(例如管道)
全双工:双方同时收发,双向并行传输。(例如Socket)
管道
管道的定义:内核级维护的一块环形缓冲区,用来进行数据传输。
通信方式:半双工。
匿名管道
匿名管道的定义:Linux内核维护的无名环形缓冲区,属于半双工通信,仅用来具有血缘关系(常用于父子进程)的进程进行进程间通信,无磁盘文件实体,进程结束管道自动销毁。
匿名管道的创建
pipe -> 创建一个匿名管道
int pipe(int pipefd[2]);
pipefd[0]:表示读端 pipe[1]:表示写端
成功:返回 0 失败:返回 -1
管道实现父子进程通信的原理
fork之前

fork之后

父子进程间单向管道通信的最终状态 ---- 关闭多余管道端(父写子读或父读子写)

代码演示
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
void Read(int rfd)
{
// 子进程读取数据
while (1)
{
char buf[1024];
int n = read(rfd, buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = 0;
printf("父进程说:%s\n", buf);
}
else if (n == 0)
{
printf("子进程正常退出\n");
break;
}
else
{
printf("子进程异常读取");
break;
}
}
}
void Write(int wfd)
{
// 父进程写入数据
int cnt = 5;
int i = 0;
char* buf[5] = {"hello world", "hello linux", "wait", "quit", "haha"};
while (cnt--)
{
int n = write(wfd, buf[i], strlen(buf[i]));
++i;
sleep(1);
}
}
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
perror("make pipe error");
exit(1);
}
pid_t id = fork();
if (id < 0)
{
perror("fork error");
exit(1);
}
else if (id == 0)
{
close(pipefd[1]);
Read(pipefd[0]);
close(pipefd[0]);
exit(0);
}
close(pipefd[0]);
Write(pipefd[1]);
close(pipefd[1]);
waitpid(id, NULL, 0);
return 0;
}
管道读写规则
-
读端快,写端慢 ---- 读端进程阻塞在read调用内部,知道写端进行写入数据
-
读端慢,写端快 ---- 管道是有容量大小的,管道被写满时,写端进程阻塞在write调用内部,等待读端进行读取,由于管道是环状结构,一旦读端把数据读取,则该数据所占用的空间可以被读端进行覆盖写入
-
读端关闭,写端没关 ---- 写段进程进行write调用内部会产生信号SIGPIPE,从而被操作系统终止
-
读端没关,写段关闭 ---- 读端进程会读到管道有效数据的末尾,再次调用read则返回0
匿名管道的特点
1.只能用于具有血缘关系的进程进行进程间通信。通常的做法:一个匿名管道由一个进程创建,然后该进程fork,此后父、子进程之间通过匿名管道进行通信。
-
管道的生命周期随着进程(原因:读端和写端的所有文件描述符都被关闭,管道释放。)
-
内核会对管道操作进行同步与互斥
-
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道。
-
管道是面向字节流的
命名管道
命名管道(FIFO)的定义:一种以文件形式存在于文件系统中的特殊文件(文件类型为 p),支持任意进程(有无血缘关系的进程)进行进程间通信,属于半双工通信。
本质特性:磁盘上存在,内核会为其维护环形缓冲区,但缓冲区里面的内容不会刷新到磁盘上。
命名管道的特点、读写规则、原理与匿名管道基本一致。
命名管道的创建和删除
命令行创建命名管道
mkfifo fifo
prw-rw-r-- 1 lt lt 0 May 23 21:43 fifo|
命令行删除命名管道
rm fifo
unlink fifo
可执行程序创建命名管道
int mkfifo(const char *filename,mode_t mode);
可执行程序删除命名管道
int unlink(const char *pathname);
命名管道的打开规则
以读的方式打开:打开之前没有其他进程以写的方式打开 -> 阻塞到open调用内部直到有其他进程以写的方式打开
以写的方式打开:打开之前没有其他进程以读的方式打开 -> 阻塞到open调用内部直到有其他进程以读的方式打开
那么谁先阻塞结束呢?
核心结论:后到达匹配条件的一方先结束阻塞
-
读端先 open 阻塞,后续写端执行 open,写端先成功返回,然后读端阻塞解除返回
-
写端先 open 阻塞,后续读端执行 open,读端先成功返回,然后写端阻塞解除返回
举例:
进程 A 只读打开 FIFO→阻塞
进程 B 只写打开 FIFO→B 先结束阻塞返回,A 同步唤醒结束阻塞
代码实现 ---- 用命名管道实现server 和 client 进程间通信
cpp
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
umask(0);
// 创建命名管道
int n = mkfifo("fifo", 0664);
if(n < 0)
{
perror("make fifo error");
exit(1);
}
// 读数据
int fd = open("fifo", O_RDONLY);
while(true)
{
char buf[1024];
int n = read(fd, buf, sizeof(buf) - 1);
if(n > 0)
{
buf[n] = 0;
std::cout << "client say:" << buf << std::endl;
}
else if(n == 0)
{
std::cout << "client 已退出,并且数据已读取完毕" << std::endl;
break;
}
else
{
std::cout << "读取异常" << std::endl;
break;
}
}
close(fd);
unlink("fifo");
return 0;
}
cpp
#include <iostream>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
// 写数据
int fd = open("fifo", O_WRONLY);
while(true)
{
std::cout << "Prease Input #:";
std::string buf;
std::getline(std::cin, buf);
write(fd, buf.c_str(), buf.size());
}
close(fd);
return 0;
}
代码运行结果
