进程间通信之管道篇

来源:互联网 发布:孙子定理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',表示该文件是一个管道。

 

 

1 0
原创粉丝点击