进程间的通信—管道
1、管道的类型
管道包括三种:
- 普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
- 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
- 命名管道name_pipe: 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.
2、无名管道(也称管道)
管道是如何通信的:
- 管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。
- 一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
- 数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。
- 管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞,另一进程放入数据。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞,直到另一进程读出数据。
- 管道包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。
管道是如何创建的:
从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。
- 管道通信的实现细节
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面从而实现的。如下图
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
管道的读写:
管道实现的源代码在
fs/pipe.c
中,在pipe.c
中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()
和管道写函数pipe_wrtie()
。管道写函数通过将字节复制到 VFS索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
当写进程向管道中写入时,它利用标准的库函数
write()
,系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
- 内存中有足够的空间可容纳所有要写入的数据;
- 内存没有被读程序锁定。
如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
管道的linux函数原型及例程:
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd[0]用于读出数据,读取时必须关闭写入端,即close(pipefd[1]);
pipefd[1]用于写入数据,写入时必须关闭读取端,即close(pipefd[0])。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
void read_data(int *); //进程读函数
void write_data(int *); //进程写函数
int main(int argc,char *argv[])
{
int pipes[2],rc;
pid_t pid;
rc = pipe(pipes); //创建管道
if(rc == -1){
perror("\npipes\n");
exit(1);
}
pid = fork(); //创建进程
switch(pid){
case -1:
perror("\nfork\n");
exit(1);
case 0:
read_data(pipes); //相同的pipes
default:
write_data(pipes); //相同的pipes
}
return 0;
}
//进程读函数
void read_data(int pipes[])
{
int c,rc;
//由于此函数只负责读,因此将写描述关闭(资源宝贵)
close(pipes[1]);
//阻塞,等待从管道读取数据, int 转为 unsiged char 输出到终端
while( (rc = read(pipes[0],&c,1)) > 0 ){
putchar(c);
}
exit(0);
}
//进程写函数
void write_data(int pipes[])
{
int c,rc;
//关闭读描述字
close(pipes[0]);
while( (c=getchar()) > 0 ){
rc = write( pipes[1], &c, 1); //写入管道
if( rc == -1 ){
perror("Parent: write");
close(pipes[1]);
exit(1);
}
}
close(pipes[1]);
exit(0);
}
3、有名管道:
由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做有名管道(named PIPE)。
实现原理
FIFO (First in, First out)
为一种特殊的文件类型, 它在文件系统中有对应的路径。 当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,有名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。
有名管道的linux函数原型及例程:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
int mknod(const char *pathname, mode_t mode, dev_t dev);
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname :要创建文件的名称
mode :权限
int mknod(const char *pathname, mode_t mode, dev_t dev);
参数:
pathname :要创建文件的名称
mode :文件类型与权限
dev :该文件对应设备的设备号,只有当文件类型为 S_IFCHR 或 S_IFBLK 的时候该文件才有设备号,创建普通文件时传入0即可。
例:mknod(FIFO_FILE, S_IFIFO|0666, 0);
FIFO_FILE是一个字符指针,指向文件名,
S_IFIFO表示要创建一个FIFO文件,
0666表示该文件的权限是所有人可读可写,
0表示该文件不是一个设备文件。
创建
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void filecopy(FILE *,char *);
int main(void)
{
FILE *fp1;
long int i = 100000;
char buf[] = "I want to study Linux!\n";
char *file1 = "data.txt";
printf("begin!\n");
if((fp1 = fopen(file1,"a+")) == NULL )
printf("can't open %s\n",file1);
while(i--)
filecopy(fp1,buf);
fclose(fp1);
printf("over!\n");
return 0;
}
void filecopy(FILE *ifp,char *buf)
{
char c;
int i,j;
j = 0;
i = strlen(buf)-1;
while(i--){
putc(buf[j],ifp);
j++;
}
putc('\n',ifp);
}
写入
#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 = "my_fifo";
char *file1 = "data.txt";
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(file1, 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);
}
读取
#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 = "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);
}