Linux篇第十一篇——进程间通信(管道+system V共享内存)
Posted 呆呆兽学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux篇第十一篇——进程间通信(管道+system V共享内存)相关的知识,希望对你有一定的参考价值。
⭐️ 本篇博客要给大家介绍一些关于进程间通信的一下知识。Linux下进程通信常见的几种方式,例如管道、共享内存等。
目录
🌏介绍
概念: 进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。IPC方法包括管道(PIPE)、消息排队、旗语、共用内存以及套接字(socket)(本篇博客只介绍共享内存和管道两种)。
通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知某些或某个进程发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
如何实现通信?
要让两个不同的进程实现通信,前提条件是让它们看到同一份资源。所以要想办法让他们看到同一份资源,就需要采取一些手段,可以分为下面几种
通信方式分类:
- 管道
- 匿名管道pipe
- 命名管道
- System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
🌏管道
🌲认识管道
概念: 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。它的特点是单向传输数据的,先进先出。
管道相信大家之前都知道一些。我们之前也会用到管道命令‘|’。例如:cat file.txt | head -1。
cat是一个进程,这个进程先处理,然后将处理后得到的标准输出到管道中,再由head进程通过标准输入将管道中的数据读出,再进行处理。
🌲匿名管道
概念: 匿名管道用于进程之间通信,这两个进程需要具有亲缘关系(父子进程等)。
🍯创建匿名管道——pipe
这里介绍一个系统调用接口——pipe。
功能: 创建一个匿名管道
函数原型:#include <unistd> int pipe(int pipefd[2])
参数:
fd:文件描述符数组,这是一个输出型参数,fd[0]表示读端,fd[1]表示写端
返回值:
创建管道成功返回0,失败返回-1
匿名管道创建原理:
调用pipe函数后,OS会在fd_array数组中分配两个文件描述符给管道,一个是读,一个是写,并把这两个文件描述符放到用户传进来的数组中,fd[0]代表管道读端,fd[1]代表管道写端。这样一个管道就创建好了。
实例演示:
实例1: 观察fd[0]和fd[1]的值
#include <stdio.h>
#include <unistd.h>
int main()
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
// 管道创建失败
perror("make piep");
exit(-1);
// 成功返回0
// pipefd[0] 代表读端
// pipefd[1] 代表写端
printf("fd[0]:%d, fd[1]:%d\\n", pipefd[0], pipefd[1]);
return0;
代码运行结果: 显然,pipefd这个数组里面放的是两个文件描述符
实例2: 尝试使用管道读写数据
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
// 管道创建失败
perror("make piep");
exit(-1);
char buf[64] = "hello world";
// 写数据
write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));
// 读数据
buf[0] = 0;// 清空buf
ssize_t s = read(pipefd[0], buf, 11);
buf[s] = '\\0';
printf("%s\\n", buf);
return 0;
代码运行结果如下: 可以看出,管道也可以读写数据,和文件使用方法是一致的
上面介绍的都是关于管道如何创建,接下来就要介绍如何使用管道进行通信。
🍯管道的本质
Linux下一切皆文件,看待管道,其实时可以像看待文件一样。且管道和文件使用方法是一致的。管道的生命周期随进程。
🍯使用匿名管道进行通信
匿名管道是提供给有亲缘关系两个进程进行通信的。所以我们可以在创建管道之后通过fork函数创建子进程,这样父子进程就看到同一份资源,且父子进程都有这个管道的读写文件描述符。我们可以关闭父进程的读端,关闭子进程的写端,这样子进程往管道里面写数据,父进程往管道里面读数据,这样两个进程就可以实现通信了。
具体过程如下:
-
父进程创建管道
-
foek创建子进程
-
关闭父进程的写端,子进程的读端
实例演示: 子进程每隔1秒往管道里面写数据,父进程每隔1秒往管道里读数据
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
// 管道创建失败
perror("make piep");
exit(-1);
pid_t id = fork();
if (id < 0)
perror("fork failed");
exit(-1);
else if (id == 0)
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
//int count = 0;
// 写数据
while (1)
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\\n");
sleep(1);
else
// parent
close(pipefd[1]);
char buf[64];
while (1)
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0)
buf[s] = '\\0';// 字符串后放一个'\\0'
printf("father get message:%s", buf);
else if (s == 0)
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\\n ");
sleep(1);
return 0;
代码运行结果如下:
🍯匿名管道的读写规则
在这里我们分四种情况来进行研究:
- 写端速度小于读端速度,管道大部分时间内为空,即读条件不满足 让子进程每5秒写一次,父进程每1秒读一次,观察现象
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
// 管道创建失败
perror("make piep");
exit(-1);
pid_t id = fork();
if (id < 0)
perror("fork failed");
exit(-1);
else if (id == 0)
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
//int count = 0;
// 写数据
while (1)
ssize_t s = write(pipefd[1], msg, strlen(msg));
sleep(5);// 管道大部分时间是空的,读条件不满足时,读端处于阻塞状态
printf("child is sending message...\\n");
else
// parent
close(pipefd[1]);
char buf[64];
//int count = 0;
while (1)
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0)
buf[s] = '\\0';// 字符串后放一个'\\0'
printf("father get message:%s", buf);
else if (s == 0)
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\\n ");
sleep(1);
return 0;
代码运行结果如下: 读端处于阻塞
总结: 当读条件不满足时,读端进程会处于阻塞,从task_struct会从运行队列调到等待队列,知道有数据来,才会转移到运行队列中。
- 写端速度大于读端速度,管道大部分时间内是满的,即写调整不满足 让子进程一直,父进程每5秒读一次,观察现象
这里这方核心代码,在上面那个例子的代码进行了一定改造
pid_t id = fork();
if (id < 0)
perror("fork failed");
exit(-1);
else if (id == 0)
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
//int count = 0;
// 写数据
while (1)
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\\n");
else
// parent
close(pipefd[1]);
char buf[64];
//int count = 0;
while (1)
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0)
buf[s] = '\\0';// 字符串后放一个'\\0'
printf("father get message:%s", buf);
else if (s == 0)
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\\n ");
sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
代码运行结果如下: 写端写了一会后,管道满了,此时写端处于阻塞状态
总结: 当写条件不满足时,写端处于阻塞状态
- 关闭写端 让写端先写5秒,然后关闭写端,观察现象
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
int count = 0;
// 写数据
while (1)
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\\n");
printf("CHILD: %d\\n", count++);
if (count == 5)
close(pipefd[1]);
exit(-1);
sleep(1);
// parent
close(pipefd[1]);
char buf[64];
while (1)
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0)
buf[s] = '\\0';// 字符串后放一个'\\0'
printf("father get message:%s", buf);
sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
else if (s == 0)
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\\n ");
代码运行结果如下: 3s后,关闭写端,读端会读到文件结尾
总结: 如果关闭写端,读端进程会读到文件结尾
- 关闭读端 5秒后关闭读端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
// 管道创建失败
perror("make piep");
exit(-1);
pid_t id = fork();
if (id < 0)
perror("fork failed");
exit(-1);
else if (id == 0)
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
// int count = 0;
// 写数据
while (1)
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\\n");
sleep(1);
else
// parent
close(pipefd[1]);
char buf[64];
int count = 0;
while (1)
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0)
buf[s] = '\\0';// 字符串后放一个'\\0'
printf("father get message:%s", buf);
//sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
else if (s == 0)
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\\n ");
sleep(1);
if (count++ == 3)
close(pipefd[0]);// 读端关闭文件描述符,写端进程后序会被操作系统直接杀掉,没有进程读,写时没有意义的
break;
int status;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
// 等待成功
printf("child exit singal is %d\\n", status&0x7f);
else
// 等待失败
perror("wait failed");
exit(-1);
return 0;
代码运行结果如下: 可以看出,关闭读端后,子进程收到操作系统发送的13号信号(SIGPIPE)杀死
总结: 读端关闭,写端进程会被操作系统发送信号杀死。
为什么写端进程会被OS杀死?
操作系统不做任何浪费空间和低效的事情,如果读端关闭,那么写还有什么意义呢?所以操作系统会通过信号把写端进程干掉
读写规则总结:
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值以上是关于Linux篇第十一篇——进程间通信(管道+system V共享内存)的主要内容,如果未能解决你的问题,请参考以下文章