【Linux从青铜到王者】Linux进程间通信(一)——待完善
前言
本节重点:
- 进程间通信介绍。
- 管道。
- 消息队列(不涉及)。
- 共享内存。
- 信号量(网络时涉及)。
进程间通信介绍
1.进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2.进程间通信发展
管道。
System V进程间通信。
POSIX进程间通信
3.进程间通信分类
管道。
匿名管道pipe。
命名管道。
System V IPC
System V 消息队列。
System V 共享内存。
System V 信号量。
POSIX IPC
消息队列。
共享内存。
信号量。
互斥量。
条件变量。
读写锁。
4.为什么需要进程间通信?
上面那么多都是为了让进程之间通信,为什么不直接让一个进程发信息给另一个进程呢,或者一个进程直接去一个进程中获取对应的信息,这样不是还方便一点吗?
因为进程要保持独立性,如果一个进程可以直接被另一个进程之间获取,或者可以接受另一个进程的信息,那么就进程之间就没有独立性了,也可能导致进程之间彼此影响
上面那么多种操作的本质,其实都是为了一件事——必须让不同的进程看到同一份资源(由os提供)重点!!!
不同的进程看到同一份资源了,就可以对其进行写入和读取,进程也就可以通过该资源获得另一个进程所发的信息了,然后执行对应的任务
管道
什么是管道?
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
下面这个命令就是统计登录当前用户的个数
who和wc是两个不同的进程,who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
匿名管道
匿名管道只适用于父子进程之间的通信
原理:
- 父进程先创建管道文件,通过pcb里的inode找到管道的对应位置,并且读端和写端都打开
- 然后父进程通过fork创建子进程,由之前可得子进程的pcb中绝大部分信息和父进程保持一致,文件描述符表和inode也会被拷贝,所以此时子进程能找到管道文件,并且读端和写端也是打开着的
- 通过用户自主选择关闭每个进程的一个端口,就能实现通信了
注意:注意这里的通信是单向的,如果想要实现双向通信,那么可以建立两个管道
图解过程:
从内核角度来看
重点
- 因为管道只用于传输数据而执行不同的任务,就没有必要将其存到磁盘中而影响os效率,所以管道只存在于磁盘中
- 由上图我们可以发现管道的本质其实就是一个文件缓冲区
- 看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
pipe
创建匿名管道需要使用pipe函数 ,pipe函数调用成功时返回0,调用失败时返回-1。
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符
数组元素 含义
pipefd[0] 管道读端的文件描述符
pipefd[1] 管道写端的文件描述符
我们可以做个实验,子进程向管道里写入数据,父进程读取文件并打印,也顺便使用一下函数接口
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// child->write, father->read
// 使用pipe创建匿名管道
int fd[2] = {0};
if (pipe(fd) < 0)
{
perror("pipe fail");
exit(-1);
}
// 使用fork创建子进程 ,关闭子进程读端,向管道写入数据
pid_t id = fork();
if (id == 0)
{
// child
close(fd[0]);
const char *msg = "hello father, I am child...";
int count = 10;
while (count--)
{
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); // 子进程写入完毕,关闭文件
exit(0);
}
// 关闭父进程写端,创建buff数组,将管道文件读到buff中并打印
close(fd[1]);
char buff[64];
while (1)
{
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0)
{
buff[s] = '\0';
printf("child send to father:%s\n", buff);
}
else if (s == 0)
{
printf("read file end\n");
break;
}
else
{
printf("read error\n");
break;
}
}
// 读取完毕关闭文件,等待子进程退出
close(fd[0]);
waitpid(id,NULL,0);
return 0;
}
结果
pipe2
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项
1、当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
管道的特点
- 1、管道内部自带同步与互斥机制。
- 2、管道的生命周期随进程。
- 3、管道提供的是流式服务。
- 4、管道是半双工通信的。
管道的四种特殊情况
- 1.写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
- 2.读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
- 3.写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
- 4.读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会向写端进程发送信号(13号信号)杀掉
可以通过以下代码实现,也就是对上面代码略微修改一下即可
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
管道的大小
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败
可以使用ulimit -a命令,查看当前资源限制的设定
通过计算可得总共有4096 字节