文章目录
1.进程间通信
1.1 进程间通讯概述
- 进程是一个独立的资源分配单元,不同进程之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
- 每个进程各自有不同的
进程地址空间
,所有进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区
,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区读走数据,内核提供的这种机制就是进程间通信(IPC),管道
是最基本的IPC机制。
1.2 进程间通信目的
数据传输
:一个进程需要将它的数据发送给另一个进程资源共享
:多个进程之间共享同样的资源。通知事件
:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制
:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.3 进程间通信分类
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
2. 管道
2.1 概述
- 所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,就像现实中的水管,水就像数据。(连接进程,相当于在进程间连接一个通路,用来传递信息)
- Linux下⼀切皆⽂件”,所以我们可以把管道看做是文件,是服务于管道通信的特殊文件,而管道通信是一种通信方式。
管道通信原理图:
2.2 匿名管道
2.2.1 概述
- 仅仅适用于
具有亲缘关系
(如父子进程)的进程间通信,因为匿名管道无法被其他进程找到
,也就无法通信,所以只能通过子进程复制父进程的方法,让子进程能够访问到相同的管道来实现通信。
2.2.2 创建匿名管道
#include <unistd.h>
int pipe(int fd[2])
参数
fd:文件描述符
- fd[0]:表示读端
- fd[1]:表示写端
返回值
成功返回0,失败返回错误代码。
2.2.3 基本过程
2.2.4 内核角度了解实质
- 在linux中,管道的实现并没有专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个索引节点又指向一个物理页面而实现。如下图所示:
2.2.5代码实现原理
- 父进程调用
pipe函数
创建管道,得到两个文件描述符fd[0]、fd[1]
指向管道的读端和写端
。 - 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
父进程关闭管道写端,子进程关闭管道读端
。父进程可以从管道中读数据,子进程将从管道中写入数据。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
2.2.6代码实现
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define SIZE 128
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id=fork();
if(id==0)
{
close(pipefd[0]);
const char* msg="Hello World!\n";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
}
else
{
close(pipefd[1]);
char buff[SIZE];
while(1)
{
ssize_t s=read(pipefd[0],buff,sizeof(buff)-1);
if(s>0)
{
buff[s]=0;
printf("fathrt get message:%s\n",buff);
sleep(1);
}
}
}
return 0;
}
运行结果:
2.2.7 常见读写行为
- 使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
-
如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么
管道中剩余的数据都被读取后,再次read会返回0,
就像读到文件末尾一样。 -
如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么
管道中剩余的数据都被读取后,再次read会阻塞
,直到管道中有数据可读了才读取数据并返回。 -
如果
所有指向管道读端的文件描述符都关闭了
(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。 -
如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在
管道被写满时再次write会阻塞
,直到管道中有空位置了才写入数据并返回。
2.2.8 管道特点
- 只能用于
具有共同祖先的进程(具有亲缘关系的进程)之间进行通信
;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。 - 管道提供
流式服务
- 一般而言,
进程退出,管道释放
,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行
同步与互斥
- 管道是
半双工
的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
2.3 命名管道(FIFO)
2.3.1 概述
- 命名管道在某种程度上可以看做是匿名管道 ,但他
打破了匿名管道只能在有血缘关系的进程间的通信
。命名管道之所以可以实现进程间通信在于通过同一个路径名而看到同一份资源
,这份资源以FIFO的文件形式存在于文件系统中。 - 它作为特殊的设备文件存在于文件系统中。因此,在进程中可以使用
open()和close()函数打开和关闭命名管道
。
2.3.2 创建命名管道
程序里创建,相关函数:
#include <sys/tyoes.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
参数:
- pathname是一个文件的路径名,是创建的一个命名管道的文件名;
- 参数mode是指文件的权限,文件权限取决于(mode&~umask)的值。
返回值:
- 返回值:若成功则返回0,否则返回-1
2.3.3代码实现
//server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#define FIFO_FILE "./fifo"
int main()
{
umask(0);
if(mkfifo(FIFO_FILE ,0666)== -1)
{
perror("mkfifo error!\n");
return 1;
}
int fd=open(FIFO_FILE,O_RDONLY);
if(fd >=0)
{
char buf[64];
while(1)
{
ssize_t s = read(fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("client message is:%s",buf);
}
else if(s==0)
{
perror("client quit!\n");
break;
}
else
{
perror("read error!");
break;
}
}
}
return 0;
}
// client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FIFO_FILE "./fifo"
int main()
{
int fd=open(FIFO_FILE,O_WRONLY);
if(fd >=0)
{
char buf[64];
while(1)
{
printf("Please enter message:");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
write(fd,buf,s);
}
}
}
return 0;
}
运行结果:
2.3.4 运行总结
- 有运行结果可知实现了
client 和 server 之间的通信
,在client上输入的内容即在server上显示
。 - 且由图可得
client和server之间是两个没有任何关系的进程
,他们的pid 和 ppid都不相同
。 - 正是因为是两个不同的进程,所以才
打破了匿名管道只有在具有亲缘关系间通信的束缚
。
2.3.5 命令行创建命名管道
命名管道可以从命令行上创建:
$ mkfifo filename
2.3.6 命令行命名管道通信
2.3.7 运行总结
mkfifo 不仅是一个函数,还是一条命令
,不仅可以在函数内创建管道,还可以在命令行上创建命名管道,实现两个命令进程之间的通信。
2.4 匿名/命名管道的区别
- 匿名管道由
pipe函数
创建并打开。 - 命名管道由
mkfifo函数创建
,打开用open。 - FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。