8.2 管道
8.2.1 管道概述
本书在第2章中介绍“ps”的命令时提到过管道,当时指出了管道是Linux中一种很重要的通信方式,它是把一个程序的输出直接连接到另一个程序的输入,这里仍以第2章中的“ps –ef | grep ntp”为例,描述管道的通信过程,如图8.2所示。
图8.2 管道的通信过程
管道是Linux中进程间通信的一种方式。这里所说的管道主要指无名管道,它具有如下特点。
n 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。
n 它是一个半双工的通信模式,具有固定的读端和写端。
n 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中。
8.2.2 管道系统调用
1.管道创建与关闭说明
管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1],其中fds[0]固定用于读管道,而fd[1]固定用于写管道,如图8.3所示,这样就构成了一个半双工的通道。
图8.3 Linux中管道与文件描述符的关系
管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close()函数逐个关闭各个文件描述符。
注意 |
当一个管道共享多对文件描述符时,若将其中的一对读写文件描述符都删除,则该管道就失效。 |
2.管道创建函数
创建管道可以通过调用pipe()来实现,表8.1列出了pipe()函数的语法要点。
表8.1 pipe()函数语法要点
所需头文件 |
#include <unistd.h> |
函数原型 |
int pipe(int fd[2]) |
函数传入值 |
fd[2]:管道的两个文件描述符,之后就可以直接操作这两个文件描述符 |
函数返回值 |
成功:0 |
出错:-1 |
3.管道读写说明
用pipe()函数创建的管道两端处于一个进程中,由于管道是主要用于在不同进程间通信的,因此这在实际应用中没有太大意义。实际上,通常先是创建一个管道,再通过fork()函数创建一子进程,该子进程会继承父进程所创建的管道,这时,父子进程管道的文件描述符对应关系如图8.4所示。
此时的关系看似非常复杂,实际上却已经给不同进程之间的读写创造了很好的条件。父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可。例如在图8.5中将父进程的写端fd[1]和子进程的读端fd[0]关闭。此时,父子进程之间就建立起了一条“子进程写入父进程读取”的通道。
图8.4 父子进程管道的文件描述符对应关系 图8.5 关闭父进程fd[1]和子进程fd[0]
同样,也可以关闭父进程的fd[0]和子进程的fd[1],这样就可以建立一条“父进程写入子进程读取”的通道。另外,父进程还可以创建多个子进程,各个子进程都继承了相应的fd[0]和fd[1],这时,只需要关闭相应端口就可以建立其各子进程之间的通道。
想一想 |
为什么无名管道只能在具有亲缘关系的进程之间建立? |
4.管道使用实例
在本例中,首先创建管道,之后父进程使用fork()函数创建子进程,之后通过关闭父进程的读描述符和子进程的写描述符,建立起它们之间的管道通信。
/* pipe.c */
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2];
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 创建管道 */
if (pipe(pipe_fd) < 0)
{
printf("pipe create errorn");
exit(1);
}
/* 创建一子进程 */
if ((pid = fork()) == 0)
{
/* 子进程关闭写描述符,并通过使子进程暂停1s等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 */
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停1s等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent wrote %d bytes : '%s'n", real_write, data);
}
/*关闭父进程写描述符*/
close(pipe_fd[1]);
/*收集子进程退出信息*/
waitpid(pid, NULL, 0);
exit(0);
}
}
将该程序交叉编译,下载到开发板上的运行结果如下所示:
$ ./pipe
Parent wrote 17 bytes : 'Pipe Test Program'
17 bytes read from the pipe is 'Pipe Test Program'
5.管道读写注意点
n 只有在管道的读端存在时,向管道写入数据才有意义。否则,向管道写入数据的进程将收到内核传来的SIGPIPE信号(通常为Broken pipe错误)。
n 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
n 父子进程在运行时,它们的先后次序并不能保证,因此,在这里为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用sleep()函数,当然这种调用不是很好的解决方法,在后面学到进程之间的同步与互斥机制之后,请读者自行修改本小节的实例程序。
8.2.4 标准流管道
1.标准流管道函数说明
与Linux的文件操作中有基于文件流的标准I/O操作一样,管道的操作也支持基于文件流的模式。这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的“另一个进程”也就是一个可以进行一定操作的可执行文件,例如,用户执行“ls -l”或者自己编写的程序“./pipe”等。由于这一类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数popen()中完成。它所完成的工作有以下几步。
n 创建一个管道。
n fork()一个子进程。
n 在父子进程中关闭不需要的文件描述符。
n 执行exec函数族调用。
n 执行函数中所指定的命令。
这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处,例如,它不如前面管道创建的函数那样灵活多样,并且用popen()创建的管道必须使用标准I/O函数进行操作,但不能使用前面的read()、write()一类不带缓冲的I/O函数。
与之相对应,关闭用popen()创建的流管道必须使用函数pclose()来关闭该管道流。该函数关闭标准I/O流,并等待命令执行结束。
2.函数格式
popen()和pclose()函数格式如表8.2和表8.3所示。
表8.2 popen()函数语法要点
所需头文件 |
#include <stdio.h> |
|
函数原型 |
FILE *popen(const char *command, const char *type) |
|
函数传入值 |
command:指向的是一个以null结束符结尾的字符串,这个字符串包含一个shell命令,并被送到/bin/sh以-c参数执行,即由shell来执行 |
|
type: |
“r”:文件指针连接到command的标准输出,即该命令的结果产生输出 |
|
函数返回值 |
成功:文件流指针 |
|
出错:-1 |
表8.3 pclose()函数语法要点
所需头文件 |
#include <stdio.h> |
函数原型 |
int pclose(FILE *stream) |
函数传入值 |
stream:要关闭的文件流 |
函数返回值 |
成功:返回由popen()所执行的进程的退出码 |
出错:-1 |
3.函数使用实例
在该实例中,使用popen()来执行“ps -ef”命令。可以看出,popen()函数的使用能够使程序变得短小精悍。
/* standard_pipe.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main()
{
FILE *fp;
char *cmd = "ps -ef";
char buf[BUFSIZE];
/*调用popen()函数执行相应的命令*/
if ((fp = popen(cmd, "r")) == NULL)
{
printf("Popen errorn");
exit(1);
}
while ((fgets(buf, BUFSIZE, fp)) != NULL)
{
printf("%s",buf);
}
pclose(fp);
exit(0);
}
下面是该程序在目标板上的执行结果。
$ ./standard_pipe
PID TTY Uid Size State Command
1 root 1832 S init
2 root 0 S [keventd]
3 root 0 S [ksoftirqd_CPU0]
……
74 root 1284 S ./standard_pipe
75 root 1836 S sh -c ps -ef
76 root 2020 R ps –ef
8.2.5 FIFO
1.有名管道说明
前面介绍的管道是无名管道,它只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可以使互不相关的两个进程实现彼此通信。该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操作。
有名管道的创建可以使用函数mkfifo(),该函数类似文件中的open()操作,可以指定管道的路径和打开的模式。
小知识 |
用户还可以在命令行使用“mknod 管道名 p”来创建有名管道。 |
在创建管道成功之后,就可以使用open()、read()和write()这些函数了。与普通文件的开发设置一样,对于为读而打开的管道可在open()中设置O_RDONLY,对于为写而打开的管道可在open()中设置O_WRONLY,在这里与普通文件不同的是阻塞问题。由于普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在open()函数中设定为O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行讨论。
(1)对于读进程。
n 若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有数据写入。
n 若该管道是非阻塞打开,则不论FIFO内是否有数据,读进程都会立即执行读操作。即如果FIFO内没有数据,则读函数将立刻返回0。
(2)对于写进程。
n 若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。
n 若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。
2.mkfifo()函数格式
表8.4列出了mkfifo()函数的语法要点。
表8.4 mkfifo()函数语法要点
所需头文件 |
#include <sys/types.h> |
|
函数原型 |
int mkfifo(const char *filename,mode_t mode) |
|
函数传入值 |
filename:要创建的管道 |
|
函数传入值 |
mode: |
O_RDONLY:读管道 |
O_WRONLY:写管道 |
||
O_RDWR:读写管道 |
||
O_NONBLOCK:非阻塞 |
||
函数传入值 |
mode: |
O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限 |
O_EXCL:如果使用O_CREAT时文件存在,那么可返回错误消息。这一参数可测试文件是否存在 |
||
函数返回值 |
成功:0 |
|
出错:-1 |
表8.5再对FIFO相关的出错信息做一归纳,以方便用户查错。
表8.5 FIFO相关的出错信息
EACCESS |
参数filename所指定的目录路径无可执行的权限 |
EEXIST |
参数filename所指定的文件已存在 |
ENAMETOOLONG |
参数filename的路径名称太长 |
ENOENT |
参数filename包含的目录不存在 |
ENOSPC |
文件系统的剩余空间不足 |
ENOTDIR |
参数filename路径中的目录存在但却非真正的目录 |
EROFS |
参数filename指定的文件存在于只读文件系统内 |
3.使用实例
下面的实例包含了两个程序,一个用于读管道,另一个用于写管道。其中在读管道的程序里创建管道,并且作为main()函数里的参数由用户输入要写入的内容。读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式。
以下是写管道的程序:
/* fifo_write.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo" /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /*定义在于limits.h中*/
int main(int argc, char * argv[]) /*参数为即将写入的字符串*/
{
int fd;
char buff[MAX_BUFFER_SIZE];
int nwrite;
if(argc <= 1)
{
printf("Usage: ./fifo_write stringn");
exit(1);
}
sscanf(argv[1], "%s", buff);
/* 以只写阻塞方式打开FIFO管道 */
fd = open(MYFIFO, O_WRONLY);
if (fd == -1)
{
printf("Open fifo file errorn");
exit(1);
}
/*向管道中写入字符串*/
if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Write '%s' to FIFOn", buff);
}
close(fd);
exit(0);
}
以下是读管道程序:
/*fifo_read.c*/
(头文件和宏定义同fifo_write.c)
int main()
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
/* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo filen");
exit(1);
}
}
/* 以只读阻塞方式打开有名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1)
{
printf("Open fifo file errorn");
exit(1);
}
while (1)
{
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' from FIFOn", buff);
}
}
close(fd);
exit(0);
}
为了能够较好地观察运行结果,需要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。读管道进程在建立管道之后就开始循环地从管道里读出内容,如果没有数据可读,则一直阻塞到写管道进程向管道写入数据。在启动了写管道程序后,读进程能够从管道里读出用户的输入内容,程序运行结果如下所示。
终端一:
$ ./fifo_read
Read 'FIFO' from FIFO
Read 'Test' from FIFO
Read 'Program' from FIFO
……
终端二:
$ ./fifo_write FIFO
Write 'FIFO' to FIFO
$ ./fifo_write Test
Write 'Test' to FIFO
$ ./fifo_write Program
Write 'Program' to FIFO
……