[Linux]进程间通信–管道
进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程 。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
实现进程间通信的原理
进程是具有独立性的,一个进程是无法看到另一个进程的代码和数据的,为了让进程间通信,要做的工作就是让不同的进程看到同一份“资源”。
任何进程通信手段需要解决的问题如下:
- 让不同的进程看到同一份“资源”
- 让一方进行读取,另一方进行写入
不同的进程间通信手段本质的区别就是让不同的进程看到同一份“资源”的方式不同。
匿名管道
匿名管道是一种以文件为媒介的通信方式,匿名管道是一个内存级别的文件,拥有和普通文件一样的缓冲区,但是操作系统不会将缓冲区刷新至外设,匿名管道虽然是文件,但是由于没有文件路径,进程是无法通过系统文件接口来操作的,因此匿名管道通常用于父子进程之间使用。
匿名管道的通信原理
由于匿名管道没有文件路径,进程是无法通过系统文件接口来操作的特性,匿名管道必须通过父进程创建,子进程继承父进程文件描述符表的方式,使得不同的进程看到同一个文件:
由于匿名管道只支持单向通信,在使用匿名管道进行通信时,父进程必须分别以读方式和写方式打开管道文件,子进程继承了文件描述符表后,一方关闭读端,一方关闭写端进行通信。
注意: 如果父进程只以读方式或者写方式打开,子进程继承文件描述符表后,也是同样的方式,子进程自身无法打开该管道,因此导致无法通信。
系统接口
Linux系统提供了创建匿名管道的系统接口pipe
:
//pipe所在的头文件和声明
#include <unistd.h>
int pipe(int pipefd[2]);
- pipefd为输出型参数,用于接收以读方式和写方式打开管道的文件描述符。
- pipefd[0]获取读端文件描述符,pipefd[1]获取写端文件描述符。
- 成功返回0,失败返回-1,错误码被设置。
编写如下代码测试pipe
接口:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
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 <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
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 <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
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 <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
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 <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
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 <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname参数 – 创建命名管道的路径
- mode参数 – 创建命名管道的文件权限
- 成功返回0,失败返回-1,错误码被设置。
为了测试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 <iostream>
#include <string>
#define NUM 1024
const std::string pipename = "./namepipe"; //管道的路径和管道名
mode_t mode = 0666; //创建管道的文件权限
client.cc
作为写端输入数据,具体内容如下:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <cassert>
#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 <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#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;
}
编译代码运行查看结果:文章来源:https://www.toymoban.com/news/detail-708894.html
文章来源地址https://www.toymoban.com/news/detail-708894.html
到了这里,关于[Linux]进程间通信--管道的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!