进程间通信之管道篇
来源:互联网 发布:孙子定理c语言代码 编辑:程序博客网 时间:2024/05/16 10:55
进程间通信之管道篇
一:管道实现原理
在 Linux 中, 管道是一种使用非常频繁的通信机制。 从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现为:
· 限制管道的大小。 实际上, 管道是一个固定大小的缓冲区。 在 Linux 中, 该缓冲区的大小为 1 页, 即 4K 字节, 使得它的大小不象文件那样不加检验地增长。 使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞, 等待某些数据被读取, 以便腾出足够的空间供 write()调用写。
· 读取进程也可能工作得比写进程快。 当所有当前进程数据已被读取时, 管道变空。 当这种情况发生时, 一个随后的 read()调用将默认地被阻塞, 等待某些数据被写入,这解决了 read()调用返回文件结束的问题。
注意: 从管道读数据是一次性操作,数据一旦被读, 它就从管道中被抛弃,释放空间以便写更多的数据。
1. 管道的结构
在 Linux 中, 管道的实现并没有使用专门的数据结构, 而是借助了文件系统的 file结构和 VFS 的索引节点 inode。 通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如图 7.1 所示。
图 7.1 中有两个 file 数据结构, 但它们定义文件操作例程地址是不同的, 其中一个是向管道中写入数据的例程地址, 而另一个是从管道中读出数据的例程地址。 这样, 用户程序的系统调用仍然是通常的文件操作, 而内核却利用这种抽象机制实现了管道这一特殊操作。
2.管道的读写
管道实现的源代码在 fs/pipe.c 中, 在 pipe.c 中有很多函数, 其中有两个函数比较重要, 即管道读函数 pipe_read()和管道写函数 pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据, 而管道读函数则通过复制物理内存中的字节而读出数据。 当然, 内核必须利用一定的机制同步对管道的访问, 为此, 内核使用了锁、 等待队列和信号。当写进程向管道中写入时, 它利用标准的库函数 write(), 系统根据库函数传递的文件描述符, 可找到该文件的 file 结构。 file 结构中指定了用来进行写操作的函数( 即写入函数) 地址, 于是, 内核调用该函数完成写操作。 写入函数在向内存中写入数据之前, 必须首先检查 VFS 索引节点中的信息, 同时满足如下条件时, 才能进行实际的内存复制工作
·内存中有足够的空间可容纳所有要写入的数据;
·内存没有被读程序锁定。
如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数
据到内存。 否则,写入进程就休眠在 VFS 索引节点的等待队列中, 接下来, 内核将调用调度程序, 而调度程序会选择其他进程运行。 写入进程实际处于可中断的等待状态, 当内存中有足够的空间可以容纳写入数据, 或内存被解锁时, 读取进程会唤醒写入进程, 这时, 写入进程将接收到信号。 当数据写入内存之后, 内存被解锁, 而所有休眠在索引节点的读取进程会被唤醒。管道的读取过程和写入过程类似。 但是, 进程可以在没有数据或内存被锁定时立即返回错误信息, 而不是阻塞该进程, 这依赖于文件或管道的打开模式。 反之, 进程可以休眠在索引节点的等待队列中等待写入进程写入数据。 当所有的进程完成了管道操作之后, 管道的索引节点被丢弃, 而共享数据页也被释放。因为管道的实现涉及很多文件的操作,因此,当读者学完有关文件系统的内容后来读pipe.c 中的代码, 你会觉得并不难理解。
二:管道应用
管道是利用 pipe()系统调用而不是利用 open()系统调用建立的。pipe()调用的原型是:int pipe(int fd[2])
我们看到, 有两个文件描述符与管道结合在一起,一个文件描述符用于管道的 read()
端, 一个文件描述符用于管道的 write()端。 由于一个函数调用不能返回两个值, pipe()的参数是指向两个元素的整型数组的指针, 它将由调用两个所要求的文件描述符填入。fd[0]元素将含有管道 read()端的文件描述符, 而 fd[1]含有管道 write()端的文件描述符。系统可根据 fd[0]和 fd[1]分别找到对应的 file 结构。在第 8 章我们会描述 pipe() 系统调用的实现机制。注意, 在 pipe 的参数中, 没有路径名, 这表明, 创建管道并不象创建文件一样, 要为它创建一个目录连接。 这样做的好处是, 其它现存的进程无法得到该管道的文件描述符,从而不能访问它。 那么, 两个进程如何使用一个管道来通信呢?
我们知道, fork()和 exec()系统调用可以保证文件描述符的复制品既可供双亲进程使用, 也可供它的子女进程使用。 也就是说, 一个进程用 pipe()系统调用创建管道, 然后用fork()调用创建一个或多个进程,那么, 管道的文件描述符将可供所有这些进程使用。pipe()系统调用的具体实现将在下一章介绍。这里更明确的含义是: 一个普通的管道仅可供具有共同祖先的两个进程之间共享, 并且这个祖先必须已经建立了供它们使用的管道。
注意: 在管道中的数据始终以和写数据相同的次序来进行读,这表示 lseek()系统调用
对管道不起作用。
下面给出在两个进程之间设置和使用管道的简单程序:
int main(void)
{
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, world!\n";
char readbuffer[80];
pipe(fd);
if((childpid = fork()) == -1)
{
printf("Error:fork");
exit(1);
}
if(childpid == 0) /* 子进程是管道的写进程 */
{
close(fd[0]); /*关闭管道的读端 */
write(fd[1], string, strlen(string));
exit(0);
}
else /* 父进程是管道的读进程 */
{
close(fd[1]); /*关闭管道的写端 */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
printf("Received string: %s", readbuffer);
}
return(0);
}
以上都是匿名管道的应用。
下面介绍命名管道的实现:
一、什么是命名管道
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
二、创建命名管道
我们可以使用两下函数之一来创建一个命名管道,他们的原型如下:
[cpp] view plain copy print?
1. #include <sys/types.h>
2. #include <sys/stat.h>
3. int mkfifo(const char *filename, mode_t mode);
4. int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。
mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。
三、访问命名管道
1、打开FIFO文件
与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。
但是有两点要注意,1、就是程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。2、就是传递给open调用的是FIFO的路径名,而不是正常的文件。
打开FIFO文件通常有四种方式,
[cpp] view plain copy print?
1. open(const char *path, O_RDONLY);//1
2. open(const char *path, O_RDONLY | O_NONBLOCK);//2
3. open(const char *path, O_WRONLY);//3
4. open(const char *path, O_WRONLY | O_NONBLOCK);//4
在open函数的调用的第二个参数中,你看到一个陌生的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的。
open调用的阻塞是什么一回事呢?很简单,对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。
四、使用FIFO实现进程间的通信
说了这么多,下面就用一个例子程序来说明一下,两个进程如何通过FIFO实现通信吧。这里有两个源文件,一个fifowrite.c,它在需要时创建管道,然后向管道写入数据,数据由文件Data.txt提供,大小为10M,内容全是字符‘0’。另一个源文件为fiforead.c,它从FIFO中读取数据,并把读到的数据保存到另一个文件DataFormFIFO.txt中。为了让程序更加简洁,忽略了有些函数调用是否成功的检查。
fifowrite.c的源代码如下:
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
int main()
{
const char *fifo_name = "/tmp/my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
const int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[PIPE_BUF + 1];
if(access(fifo_name, F_OK) == -1)
{
//管道文件不存在
//创建命名管道
res = mkfifo(fifo_name, 0777);
if(res != 0)
{
fprintf(stderr, "Could not create fifo %s\n", fifo_name);
exit(EXIT_FAILURE);
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
//以只写阻塞方式打开FIFO文件,以只读方式打开数据文件
pipe_fd = open(fifo_name, open_mode);
data_fd = open("Data.txt", O_RDONLY);
printf("Process %d result %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
int bytes_read = 0;
//向数据文件读取数据
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
while(bytes_read > 0)
{
//向FIFO文件写数据
res = write(pipe_fd, buffer, bytes_read);
if(res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
//累加写的字节数,并继续读取数据
bytes_sent += res;
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
}
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}
源文件fiforead.c的代码如下:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <string.h>
int main()
{
const char *fifo_name = "/tmp/my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
int open_mode = O_RDONLY;
char buffer[PIPE_BUF + 1];
int bytes_read = 0;
int bytes_write = 0;
//清空缓冲数组
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
//以只读阻塞方式打开管道文件,注意与fifowrite.c文件中的FIFO同名
pipe_fd = open(fifo_name, open_mode);
//以只写方式创建保存数据的文件
data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);
printf("Process %d result %d\n",getpid(), pipe_fd);
if(pipe_fd != -1)
{
do
{
//读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中
res = read(pipe_fd, buffer, PIPE_BUF);
bytes_write = write(data_fd, buffer, res);
bytes_read += res;
}while(res > 0);
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}
两个程序都使用阻塞模式的FIFO,为了让大家更清楚地看清楚阻塞究竟是怎么一回事,首先我们运行fifowrite.exe,并把它放到后台去运行。这时调用jobs命令,可以看到它确实在后台运行着,过了5秒后,再调用jobs命令,可以看到进程fifowrite.exe还没有结束,它还在继续运行。因为fifowrite.exe进程的open调用是阻塞的,在fiforead.exe还没有运行时,也就没有其他的进程以读方式打开同一个FIFO,所以它就一直在等待,open被阻塞,没有返回。然后,当我们进程fiforead.exe运行时(为了查看性能,在time命令中运行),fifowrite.exe中的open调用返回,进程开始继续工作,然后结束进程。而fiforead.exe的open调用虽然也是阻塞模式,但是fifowrite.exe早已运行,即早有另一个进程以写方式打开同一个FIFO,所以open调用立即返回。
从time中的输出来看,管道的传递效率是非常高的,因为fiforead.exe既要读取数据,还要写数据到文件DataFormFIFO.txt中,10M的数据只用了0.1秒多一点。
此外,如果此时,你在shell中输入如下命令,ls -l /tmp/my_fifo,可以看到如下结果:
证明FIFO文件确实是存在于文件系统中的文件,文件属性的第一个字符为‘p',表示该文件是一个管道。
- 进程间通信之管道篇
- 进程间通信之管道fifo篇
- linux进程间通信之管道篇
- 进程间通信之管道篇
- 进程间通信之管道篇
- [进程通信] 进程间通信 之 管道
- 进程间通信之管道
- 进程间通信之 管道
- 进程间通信 之 管道
- 进程间通信之管道
- 进程间通信之管道
- 进程间通信之 管道
- 进程间通信之管道
- 进程间通信之管道
- 进程间通信之管道
- 进程间通信之管道
- 进程间通信之管道
- 进程间通信之管道
- kmeans++算法
- myeclipse中自带的tomcat在安装文件中的具体位置
- VMware虚拟机与主机的三大网络连接模式
- iOS图片压缩处理
- Android圆角图片和圆形图片实现
- 进程间通信之管道篇
- Java中值传递与引用传递的区别
- Mac OS X、Linux和Windows设备连接至SFTP服务器的方法
- java.lang.NoSuchMethodError: javax.persistence.Table.indexes()[Ljavax/persistence/Index;
- Days37RecyclerView
- mysql-proxy实现读写分离
- go语言的控制语句
- 单引号和双引号的区别
- 一行代码集成今日头条效果