为什么要有进程间通信?
原因有以下几点:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间需要共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信(IPC,Interprocess Communication)是指在不同进程之间传输数据和交换信息的一种机制。它允许多个进程在同一操作系统中同时运行,并实现彼此之间的协作。
如何进行进程间通信?
我们知道,进程之间是具有独立性的。那么进行进程间通信的前提就是:让不同的进程,看到同一份资源。并且这个资源应是某种形式的内存空间,而提供资源的人只能是操作系统!
进程间通信的分类
进程间通信的方式共有两大类:本地通信和网络通信。今天我们主要介绍本地通信,网络通信我们后面再说。本地通信是同一台主机,同一个操作系统,不同进程之间通信。
管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
who和wc是两个不同的命令,执行时成为两个进程,这两个进程为兄弟关系。
匿名管道
匿名管道的原理
管道只能进行单向通信
- 父进程创建管道时需要将rw同时打开,然后在创建子进程,是为了让子进程也能看到这个管道,利用的是子进程会继承父进程相关资源的特性。如果先创建子进程,父进程在打开rw端,子进程就看不到这个管道了。如果父进程只将 r 或者 w 打开,子进程继承的就只是 r 或 w ,此时的父子进程要么都是读,要么都是写。就无法通信,管道就创建失败了。
- 创建管道成功之后,需要将父子进程特定的文件描述符关闭。如果不关闭就容易造成 fd泄露 或者 误操作 等问题
我们创建的这个管道,没有路径,如果想创建直接在内核里创建就是了,没有名字,所以叫做匿名管道!
接口验证
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
一般是3和4
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
// 验证pipe接口
#include <iostream>
#include <unistd.h>
int main()
{
int fds[2] ={0};
int n = pipe(fds); //fds:输出型参数
if(n == 0)
{
std::cout <<"fds[0]:"<< fds[0] << std::endl; // 3
std::cout <<"fds[1]:"<< fds[1] << std::endl; // 4
}
return 0;
}
代码展示
// 子进程向父进程发送信息
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 1. 创建管道
int fds[2] = {0};
int n = pipe(fds);
if(n != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if(id < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if(id == 0)
{
// 子进程
// 3. 关闭不需要的fd,关闭read
close(fds[0]);
int cnt = 0;
while(true)
{
string message = "hello world, hello ";
message += to_string(getpid());
message += ",";
message += to_string(cnt);
// 直接通过系统调用write向fds[1]中写入
::write(fds[1], message.c_str(), message.size());
cnt++;
sleep(1);
}
exit(0);
}
else
{
// 父进程
// 3. 关闭不需要的fd, 关闭write
close(fds[1]);
char buffer[1024];
while(true)
{
// 通过系统调用read,从fds[0]中读取数据
// 如果n等于0,代表读到了文件末尾
ssize_t n = ::read(fds[0], buffer, 1024);
if(n > 0)
{
buffer[1024] = 0;
cout << "child -> father, message: " << buffer << endl;
}
}
pid_t rid = waitpid(id, nullptr, 0);
cout << "father wait child success:" << rid << endl;
}
return 0;
}
匿名管道的特性
1. 面向字节流!
2. 用来进行具有血缘关系的进程,进行IPC,常用于父子。
3. 文件的生命周期,随进程!管道也随进程!
4. 单向数据通信。
5. 管道自带同步互斥等保护机制(保护共享资源)!
命名管道
命名管道是一种进程间通信(IPC)机制,运行不同进程之间进行可靠的、单向或双向的数据通信。
命名管道的基本原理
- 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件,两个进程通过命名管道的文件名(同一个文件系统)打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
- 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
- FIFO是真实存在的。为什么?因为为我们需要使用这个文件名 + 路径来标识它资源的唯一性。
- 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的占位符,但这个占位符的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中,它们都只使用内核文件缓冲区。
命名管道的创建
$ mkfifo fifo//使用该命令来创建一个命名管道
接口验证
代码级创建有名管道-mkfifo,本质是创建文件!
- mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。- mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
- mkfifo函数创建成功返回0,创建失败返回-1
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
//将权限掩码设为0, 这样我们就可以自己设置我们所需要的权限
umask(0);
int n = ::mkfifo("new_Fifo", 0666);
if(n < 0)
{
std::cerr << "mkfifo error" << std::endl;
return 1;
}
std::cout << "mkfifo success" << std::endl;
return 0;
}
#include <unistd.h>
// 删除上面代码中的管道文件new_Fifo
int main()
{
int n = ::unlink("new_Fifo");
if(n < 0)
{
std::cerr << "unlink error" << std::endl;
return 1;
}
std::cerr << "unlink succeed" << std::endl;
return 0;
}
代码展示
利用命名管道对server和client通信
// server-服务器代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
// 创建管道文件
if(::mkfifo("mypipe", 0644) < 0)
ERR_EXIT("mkfifo");
// 以读打开管道文件
int rfd = ::open("mypipe", O_RDONLY);
if(rfd < 0)
ERR_EXIT("open");
char buf[1024];
while(1)
{
buf[0] = 0;
printf("Please wait...\n");
// 从管道文件中读取sizeof(buf) - 1个数据到buf里
ssize_t s = ::read(rfd, buf, sizeof(buf) - 1);
if(s > 0 )
{
buf[s] = 0;
printf("client say# %s\n", buf);
}
else if(s == 0) // 表示读到了文件末尾,即客户端关闭
{
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else
ERR_EXIT("read");
}
// 关闭管道文件
::close(rfd);
return 0;
}
// client-客户端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
// 以写方式打开管道文件
int wfd = ::open("mypipe", O_WRONLY);
if(wfd < 0)
ERR_EXIT("open");
char buf[1024];
while(1)
{
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);
// 从 标准输入 中读取sizeof(buf) - 1个数据到buf里
ssize_t s = ::read(0, buf, sizeof(buf)-1);
if(s > 0 )
{
buf[s] = 0;
// 把从标准输入中读的数据写入管道文件
::write(wfd, buf, strlen(buf));
}
else if(s <= 0)
ERR_EXIT("read");
}
// 关闭管道文件
::close(wfd);
return 0;
}
管道IO操作的四种情况
1. 管道为空 && 管道正常,read会阻塞[read是一个系统调用]
2. 管道为满 && 管道正常,write会阻塞[write也是一个系统调用]
3. 管道写端关闭 && 读端继续,读端读到0,表示读到文件结尾
4. 管道写端正常 && 读端关闭,OS会直接杀掉写入的进程!