进程间通信---管道
进程间通信
一般而言,不同的进程看到的那一份公共资源,是由操作系统提供的,任何一个进程的全局变量在另一个进程中都看不到,所以,进程之间要交换数据就需要通过内核,在内核中开辟一块缓冲区,进程一把数据从用户空间拷贝到缓冲区,进程二再从缓冲区把数据读走,内核提供的这种机制就叫做进程间通信 (IPC)。
![这里写图片描述][Image 1]
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信的分类:
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 读写锁
- 条件变量
今天我们主要来谈谈管道,管道是从一个进程连接到另一个进程的一个数据流
匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
调用pipe函数时在内核中开辟一块缓冲区,称为管道,创建成功就返回0,创建失败则返回-1。
管道有一个读端一个写端,通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端。
看几张图来理解一下管道的通信过程:
1. 父进程调用pipe开辟管道,得到两个文件描述符分别指向管道的读端和写端。
![这里写图片描述][Image 1]
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一个管道。
![这里写图片描述][Image 1]
3. 父进程关闭管道写端,子进程关闭管道读端。子进程可以往管道里写,父进程可以从管道里读。管道是用环形队列实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。
![这里写图片描述][Image 1]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2] = {0};
if(pipe(fd)==-1)
{
perror("pipe");
return 1;
}
pid_t pid = fork();
if(pid==-1)
{
perror("fork");
return 2;
}
else if(pid==0)
{
//child write
close(fd[0]);
char *msg="father,I am child";
int i = 5;
while(i--)
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
}
else
{
//father read
close(fd[1]);
char buf[1024];
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
printf("father: %s\n",buf);
}
else if(s==0)
{
printf("child quit!!!\n");
break;
}
else
{
perror("read");
return 3;
}
}
}
return 0;
}
![这里写图片描述][Image 1]
匿名管道的特点
- 只能用于具有亲缘关系的进程之间进行通信(常为父子)
- 管道的生命周期随进程
- 管道是半双工的,只能进行单向通信
- 管道自带同步机制
- 管道是面向字节流的
管道的读写规则
- 写端的文件描述符关闭,读端一直读
—–读到文件结尾返回0 - 写端的文件描述符没关闭,但写端也不写数据,而读端一直读
—–读端一直等待 - 写端一直写数据,读端不读,也不关闭它的文件描述符
—–写端写满后一直等待 - 写端写数据,而读端的文件描述符关闭
—–系统直接结束掉写进程(用13信号)
命名管道
匿名管道的一个不足之处是只能用于具有亲缘关系的进程间通信,而命名管道(FIFO)克服了这个缺点。
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
命名管道的创建
通过命令创建
$ mkfifo filename
![这里写图片描述][Image 1]
创建成功返回0,创建失败返回-1
参数pathname代表的是你创建出的管道名称,mode是这个管道具有的权限
来看例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
if(mkfifo("fifo",0644)==-1)
{
perror("mkfifo");
return 1;
}
return 0;
}
![这里写图片描述][Image 1]
命名管道和匿名管道除了打开和创建方式不同,其他语义基本相同
用命名管道实现client/server通信
(实现用户发送消息,服务器接受消息)
直接上代码
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
if(mkfifo("mypipe",0644)==-1)//创建命名管道
{
perror("mkfifo");
return 1;
}
int rfd = 0;
if((rfd = open("mypipe",O_RDONLY))==-1)//以只读方式打开管道
{
perror("open");
return 2;
}
char buf[1024];
while(1)
{
ssize_t s = read(rfd,buf,sizeof(buf)-1);//从管道中读数据
if(s>0)
{
buf[s-1]=0;
printf("client say:%s\n",buf);
}
else if(s==0)
{
printf("client quit!!!\n");
break;
}
else
{
perror("read");
return 3;
}
}
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int wfd = 0;
if((wfd = open("mypipe",O_WRONLY))==-1)//以只写方式打开管道
{
perror("open");
return 1;
}
char buf[1024];
while(1)
{
printf("Please Enter:");
//从标准输入读取数据
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
write(wfd,buf,strlen(buf));//将读取到的数据写进管道
}
else if(s==-1)
{
perror("write");
return 2;
}
}
return 0;
}
因为这里有两个文件需要编译,我在这里编写了一个Makefile来管理这两个文件
Makefile:
![这里写图片描述][Image 1]
测试结果:
1.编译通过后,我们先执行server创建出管道,如图:
(再开启一个终端来观察)
![这里写图片描述][Image 1]
2.再执行client开始传输数据
![这里写图片描述][Image 1]
这样我们就通过命名管道实现了两个毫不相关进程间的通信
Linux下管道的容量及管道的数据结构
1.查看管道的容量
![这里写图片描述][Image 1]
2.6标准版本的linux内核,pipe缓冲区是64KB,尽管命令ulimit -a看到管道大小是8块,这个大小是内核设定的管道缓冲区的大小。因为内核动态分配最大是16缓冲条目乘64K。
那么如何查看自己电脑上面管道的大小呢?
- 通过ulimit -a查看pipe size一次原子性写入8*512bytes=4096bytes
![这里写图片描述][Image 1] - 查看缓冲条目个数:cat /usr/src/kernels/2.6.32-431.el6.i686/include/linux/pipe_fs_i.h 文件
![这里写图片描述][Image 1]
接下来就可以计算自己电脑上管道容量的大小了:16*4096bytes=65536bytes
也就验证了 man 7 pipe中容量的大小:
![这里写图片描述][Image 1]
2.管道的数据结构
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和 VFS 的索引节点inode。通过将两个file结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
//inode结点信息结构
struct inode {
...
struct pipe_inode_info *i_pipe;
...
};
//管道缓冲区个数
#define PIPE_BUFFERS (16)
//管道缓存区对象结构
struct pipe_buffer {
struct page *page; //管道缓冲区页框的描述符地址
unsigned int offset, len; //页框内有效数据的当前位置,和有效数据的长度
struct pipe_buf_operations *ops; //管道缓存区方法表的地址
};
//管道信息结构
struct pipe_inode_info {
wait_queue_head_t wait; //管道等待队列
unsigned int nrbufs, curbuf;
//包含待读数据的缓冲区数和包含待读数据的第一个缓冲区的索引
struct pipe_buffer bufs[PIPE_BUFFERS]; //管道缓冲区描述符数组
struct page *tmp_page; //高速缓存区页框指针
unsigned int start; //当前管道缓存区读的位置
unsigned int readers; //读进程的标志,或编号
unsigned int writers; //写进程的标志,或编号
unsigned int waiting_writers; //在等待队列中睡眠的写进程的个数
unsigned int r_counter; //与readers类似,但当等待写入FIFO的进程是使用
unsigned int w_counter; //与writers类似,但当等待写入FIFO的进程时使用
struct fasync_struct *fasync_readers; //用于通过信号进行的异步I/O通知
struct fasync_struct *fasync_writers; //用于通过信号的异步I/O通知
};
[Image 1]:
还没有评论,来说两句吧...