文章目录
进程间通信:Inter-Process Communication,简写做 IPC
一、匿名管道
1. 创建管道 pipe
头文件:
#include <unistd.h>
#include <fcntl.h>
int pipe(int pipefd[2])
输出型参数:
- 带回两个 文件描述符
- 0 下标为 读端,助记:0 - -> 嘴巴 - -> 读书
- 1 下标为 写端,助记:1 - -> 笔 - -> 写东西
返回值
- 成功返回 0,失败返回 -1
2. 管道的特点
-
单向通行(管道是半双工的一种特殊情况)
-
管道的本质是文件,因为 fd 的生命周期随进程,所以管道的生命周期也是随进程的
-
管道通信,通常用来进行具有 “血缘关系”的进程,进行进程件通信。常用于 父子间通信 - - 使用 pipe 打开管道(匿名管道,并不清楚管道的名字)
-
在管道通信中,写入的次数 和 读取的次数,不是严格匹配的。读写没有强相关 - - 字节流
管道 是面向 字节流 的 -
管道具有一定的协同能力,让 reader 和 writer 能够按照一定的步骤进行通信(管道自带 同步机制)
3. 四种场景
-
如果我们 read 读取完毕所有管道的数据,如果对方不发,我们就只能等待
-
如果我们 write 端将管道写满了,就不能写啦
-
如果关闭了写端,读取完毕管道数据,再读,就会 read 返回 0,表明读到了文件结尾
-
写端一直再写,读端关闭,没有意义。OS 不会维护无意义、低效率、或者浪费资源的事。故,OS 会通过信号,来终止进程 13)SIGPIPE
二、命名管道
让不同的进程通过 文件路径 + 文件名 看到同一个文件并打开,就是看到了同一个资源,也即具备了进程通信的前提。
命名管道也被称为 FIFO 文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
1. Linux命令:mkfifo (创建 命名管道)
mkfifo [管道名]
:创建命名管道
创建出来的文件类型以 p
开头
-:普通文件
d:目录文件(document)
l:链接文件(link)
p:管道文件(pipeline)
2. 函数 mkfifo
头文件:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数 pathname:
- 文件路径
参数 mode:
- 创建文件的权限,这里的权限会被系统的umask影响,所以我们若不想被系统的默认掩码影响,可以使用 函数
umask(0)
返回值:
- 成功返回 0,失败返回 -1
3. 函数 unlink(删除 命名管道)
头文件:
#include <unistd.h>
int unlink(const char *path);
参数 pathname:
- 需要删除的文件路径
返回值:
- 成功返回 0,失败返回 -1
4. 函数 open(打开 FIFO 文件)
与打开其他文件一样,FIFO 文件也可以使用 open 调用来打开。注意,mkfifo 函数只是创建一个 FIFO 文件,要使用命名管道还需要将其打开。
需要注意的是一下两点:
-
就是程序不能以 O_RDWR 模式打开 FIFO 文件进行读写操作,而其行为也未明确定义。因为如一个管道以读/写方式打开,进程就会读回自己的输出,而我们通常使用 FIFO 只是为了单向的数据传递。
-
就是传递给 open 调用的是要是 FIFO 文件的路径名,而不是别的属性的文件。
打开FIFO文件通常有四种方式:
open(const char *path, O_RDONLY); // 1
open(const char *path, O_RDONLY | O_NONBLOCK); // 2
open(const char *path, O_WRONLY); // 3
open(const char *path, O_WRONLY | O_NONBLOCK); // 4
在 open 函数的调用的第二个参数中,选项 O_NONBLOCK 表示非阻塞,加上这个选项后,表示 open 调用是非阻塞的,如果没有这个选项,则表示 open 调用是阻塞的。
open调用的阻塞是什么一回事呢?
-
对于以只读方式(O_RDONLY)打开的 FIFO 文件,如果 open 调用是阻塞的(即第二个参数为 O_RDONLY),除非有一个进程以写方式打开同一个 FIFO,否则它不会返回;如果 open 调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个 FIFO 文件,open 调用将成功并立即返回。
-
对于以只写方式(O_WRONLY)打开的 FIFO 文件,如果 open 调用是阻塞的(即第二个参数为 O_WRONLY),open 调用将被阻塞,直到有一个进程以只读方式打开同一个 FIFO 文件为止;如果 open 调用是非阻塞的(即第二个参数为 O_WRONLY | O_NONBLOCK),open 总会立即返回,但如果没有其他进程以只读方式打开同一个 FIFO 文件,open 调用将返回 -1,并且 FIFO 也不会被打开。
5. 命名管道代码案例
举例实现:利用命名管道,实现两个进程的连接。client 端输入,server 端同步实现接收。
makefile
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -lncurses -std=c++11
.PHONY:clean
clean:
rm -f server client
comm.hpp
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
server.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
// 1. 创建管道文件,一次创建即可
umask(0); // 这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "管道文件已创建" << std::endl;
// 2. 让服务端直接开启管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0 )
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "成功打开管道文件,开始 ipc(进程间通信)" << std::endl;
// 3. 正常通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
//std::cout << "client# " << buffer << std::endl;
printf("%c", buffer[0]);
fflush(stdout);
}
else if(n == 0)
{
std::cout << "client 已经退出,我是 server,也退出了" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
// 删除管道文件
unlink(fifoname.c_str());
return 0;
}
client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;
int main()
{
//1. 不需创建管道文件,只需要打开对应的文件即可(文件就是两个进程看到的相同的资源)
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
cerr << errno << ":" << strerror(errno) << endl;
return 1;
}
// 2. 开始通信
char buffer[NUM];
while(true)
{
// version(1)回车发送
// cout << "请输入你的消息# ";
// char *msg = fgets(buffer, sizeof(buffer), stdin);
// assert(msg);
// (void)msg;
// version(2)不用进行回车就可以确认输入
system("stty raw"); // 使终端驱动处于一次一字符模式
int c = getchar();
system("stty -raw"); // 使终端驱动回到一次一行模式
//buffer[strlen(buffer) - 1] = 0; // 解决输入的\n
//if(strcasecmp(buffer, "quit") == 0) break; // 写端退出,读端就也一起退出了
// strcasecmp 忽略大小写的字符串比较(带 n 是strncasecmp)
ssize_t n = write(wfd, (char*)&c, sizeof(char));
assert(n >= 0);
(void)n;
}
close(wfd);
return 0;
}
🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~