进程是具有独立性的,一个进程是无法看到另一个进程的代码和数据的,为了让进程间通信,要做的工作就是让不同的进程看到同一份“资源”。
任何进程通信手段需要解决的问题如下:
不同的进程间通信手段本质的区别就是让不同的进程看到同一份“资源”的方式不同。
匿名管道是一种以文件为媒介的通信方式,匿名管道是一个内存级别的文件,拥有和普通文件一样的缓冲区,但是操作系统不会将缓冲区刷新至外设,匿名管道虽然是文件,但是由于没有文件路径,进程是无法通过系统文件接口来操作的,因此匿名管道通常用于父子进程之间使用。
由于匿名管道没有文件路径,进程是无法通过系统文件接口来操作的特性,匿名管道必须通过父进程创建,子进程继承父进程文件描述符表的方式,使得不同的进程看到同一个文件:

由于匿名管道只支持单向通信,在使用匿名管道进行通信时,父进程必须分别以读方式和写方式打开管道文件,子进程继承了文件描述符表后,一方关闭读端,一方关闭写端进行通信。
注意: 如果父进程只以读方式或者写方式打开,子进程继承文件描述符表后,也是同样的方式,子进程自身无法打开该管道,因此导致无法通信。
Linux系统提供了创建匿名管道的系统接口pipe:
//pipe所在的头文件和声明
#include
int pipe(int pipefd[2]);
编写如下代码测试pipe接口:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
if (n < 0)//出错判断
{
cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
exit(1);
}
//创建子进程
pid_t id = fork();
assert(id != -1);//出错判断
//进行通信 -- 父进程进行读取,子进程进行写入
if (id == 0)
{
//子进程
close(pipefd[0]);
const string str = "hello world";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
write(pipefd[1], buffer, strlen(buffer));//向管道写入数据
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024];
while(1)
{
read(pipefd[0], buffer, sizeof(buffer) - 1);//从管道读取数据
cout << "我是父进程," << "child give me: " << buffer << endl;
}
close(pipefd[0]);
return 0;
}
编译代码运行查看结果:

从运行结果可以看出,建立管道后,父子进程就能够进行数据通信。
场景一: 如果管道内部的数据被读端读取完了,写端不写入,读端就只能等待
编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
if (n < 0)
{
cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
exit(1);
}
//创建子进程
pid_t id = fork();
assert(id != -1);
//进行通信 -- 父进程进行读取,子进程进行写入
if (id == 0)
{
//子进程
close(pipefd[0]);
const string str = "hello world";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
write(pipefd[1], buffer, strlen(buffer));
sleep(100); // --------- 模拟写入暂停 ---------
}
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024];
while(1)
{
read(pipefd[0], buffer, sizeof(buffer) - 1);
cout << "我是父进程," << "child give me: " << buffer << endl;
}
close(pipefd[0]);
return 0;
}
编译代码运行查看结果:

场景二: 如果管道内部的数据被写端写满了,读端不读取,写端无法继续写入
编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
if (n < 0)
{
cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
exit(1);
}
//创建子进程
pid_t id = fork();
assert(id != -1);
//进行通信 -- 父进程进行读取,子进程进行写入
if (id == 0)
{
//子进程
close(pipefd[0]);
const string str = "hello world";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
write(pipefd[1], buffer, strlen(buffer));
printf("cnt: %d\n", cnt); // --------- 显示写入过程 ---------
//sleep(100);
}
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024];
while(1)
{
sleep(100); // --------- 模拟读取暂停 ---------
read(pipefd[0], buffer, sizeof(buffer) - 1);
cout << "我是父进程," << "child give me: " << buffer << endl;
}
close(pipefd[0]);
return 0;
}
编译代码运行查看结果:

场景三: 写端关闭,读端读完了管道内部的数据时,再读就读到了文件的结尾。
编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
if (n < 0)
{
cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
exit(1);
}
//创建子进程
pid_t id = fork();
assert(id != -1);
//进行通信 -- 父进程进行读取,子进程进行写入
if (id == 0)
{
//子进程
close(pipefd[0]);
const string str = "hello world";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
write(pipefd[1], buffer, strlen(buffer));
printf("cnt: %d\n", cnt);
sleep(1);
if (cnt == 5) break; // --------- 写端关闭 ---------
}
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024];
while(1)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
cout << "我是父进程," << "child give me: " << buffer << endl;
}
else if (n == 0)// --------- 判断读取到文件末尾 ---------
{
cout << "读取完毕, 读到文件结尾" << endl;
break;
}
else
{
cout << "读取出错" << endl;
break;
}
}
close(pipefd[0]);
return 0;
}
编译代码运行查看结果:

**场景四:**写端一直写,读端关闭,操作系统会给写端发送13号信号终止进程。
编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
if (n < 0)
{
cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
exit(1);
}
//创建子进程
pid_t id = fork();
assert(id != -1);
//进行通信 -- 父进程进行读取,子进程进行写入
if (id == 0)
{
//子进程
close(pipefd[0]);
const string str = "hello world";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
write(pipefd[1], buffer, strlen(buffer));
printf("cnt: %d\n", cnt);
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024];
while(1)
{
int cnt = 0;
//sleep(100);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
cout << "我是父进程," << "child give me: " << buffer << endl;
}
else if (n == 0)
{
cout << "读取完毕, 读到文件结尾" << endl;
break;
}
else
{
cout << "读取出错" << endl;
break;
}
//sleep(100);
sleep(5);
break;// --------- 读端关闭 ---------
}
close(pipefd[0]);
int status = 0;
waitpid(id, &status, 0);
cout << "signal: " << (status & 0x7F) << endl;// --------- 回收子进程获取退出信号 ---------
sleep(3);
return 0;
}
编译代码运行查看结果:

在Linux下,管道(Pipe)的大小受到操作系统的限制。具体来说,管道的大小由内核参数PIPE_BUF定义,通常是4096个字节。
PIPE_BUF时,linux将保证写入的原子性。PIPE_BUF时,linux将不再保证写入的原子性。命名管道同样是内存级的文件,和匿名管道的区别就是命名管道可以在指定路径下创建,并且命名可以指定,因此命名管道可以给任何两个不同的进程用于通信。
Linux下使用mkfifo 指令就可以在指定路径下创建命名管道。

命名管道同样和匿名管道一样满足管道的协同场景:

写端尝试打开管道文件,没有读端,写端就会卡在打开文件这一步骤。

右侧读端开始会等待写端写入,后续关闭右侧读端,左侧写端进程直接被终止。
//mkfifo所在的头文件和声明
#include
#include
int mkfifo(const char *pathname, mode_t mode);
为了测试mkfifo接口编写代码进行测试,首先设置文件结构如下:

makefile文件内容如下:
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf client server
common.hpp主要用于让两个进程获取管道路径,具体内容如下:
#include
#include
#define NUM 1024
const std::string pipename = "./namepipe"; //管道的路径和管道名
mode_t mode = 0666; //创建管道的文件权限
client.cc作为写端输入数据,具体内容如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include "commn.hpp"
int main()
{
// 打开管道文件
int wfd = open(pipename.c_str(), O_WRONLY);
if (wfd < 0)
{
std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
exit(1);
}
//进行通信
while(true)
{
char buffer[NUM];
std::cout << "请输入内容:";
fgets(buffer, sizeof(buffer), stdin);//获取用户输入
buffer[strlen(buffer) - 1] = 0;
if (strcasecmp(buffer, "quit") == 0) break;//用户输入quit退出进程
ssize_t size = write(wfd, buffer, strlen(buffer));
assert(size >= 0);
(void)size;
}
close(wfd);
return 0;
}
server.cc作为读端用于接收写端的输入并打印,具体内容如下:
#include
#include
#include
#include
#include
#include
#include
#include "commn.hpp"
int main()
{
umask(0);
// 创建管道文件
int n = mkfifo(pipename.c_str(), mode);
if (n < 0)
{
std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create fifo file success" << std::endl;
// 以读方式打开管道文件
int rfd = open(pipename.c_str(), O_RDONLY);
if (rfd < 0)
{
std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
exit(2);
}
// 进行通信
while (true)
{
char buffer[NUM];
ssize_t size = read(rfd, buffer, sizeof(buffer) - 1);
buffer[size] = 0;
if (size > 0)
{
std::cout << "client send me :" << buffer << std::endl;//输出接收的信息
}
else if (size == 0)
{
std::cout << "client quit, me too!" << std::endl;
break;
}
else
{
std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
break;
}
}
close(rfd);
unlink(pipename.c_str()); // 删除文件
return 0;
}
编译代码运行查看结果:
