1. 管道
所有式样的Unix都提供管道。它由pipe函数创建,提供一个单路(单向)数据流
#include <unistd.h>
int pipe(int fd[2]);
// 返回:若成功则为0,若出错则为-1
该函数返回两个文件描述符: fd[0]和fd[1]。前者打开来读,后者打开来写。
例子:
父进程(客户端)将文件名写入管道,子进程(服务器)读取数据(文件名)并打开对应文件,将文件中数据写入管道,父进程读取管道中数据。
图解:
代码:
//mainpipe.c
#include "unpipc.h"
void client(int, int);
void server(int, int);
int main(int argc, char **argv)
{
int pipe1[2], pipe2[2];
pid_t childpid;
Pipe(pipe1); /* create two pipes */
Pipe(pipe2);
if ( (childpid = Fork()) == 0)
{
/* child */
Close(pipe1[1]);
Close(pipe2[0]);
server(pipe1[0], pipe2[1]);
exit(0);
}
/* parent */
Close(pipe1[0]);
Close(pipe2[1]);
client(pipe2[0], pipe1[1]);
Waitpid(childpid, NULL, 0); /* wait for child to terminate */
exit(0);
}
//client.c
#include "unpipc.h"
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE];
/* read pathname */
Fgets(buff, MAXLINE, stdin);
len = strlen(buff); /* fgets() guarantees null byte at end */
if (buff[len-1] == '\n')
len--; /* delete newline from fgets() */
/* write pathname to IPC channel */
Write(writefd, buff, len);
/* read from IPC, write to standard output */
while ( (n = Read(readfd, buff, MAXLINE)) > 0)
Write(STDOUT_FILENO, buff, n);
}
#include "unpipc.h"
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE+1];
/* read pathname from IPC channel */
if ( (n = Read(readfd, buff, MAXLINE)) == 0)
err_quit("end-of-file while reading pathname");
buff[n] = '\0'; /* null terminate pathname */
if ( (fd = open(buff, O_RDONLY)) < 0)
{
/* error: must tell client */
snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n", strerror(errno));
n = strlen(buff);
Write(writefd, buff, n);
}
else
{
/* open succeeded: copy file to IPC channel */
while ( (n = Read(fd, buff, MAXLINE)) > 0)
Write(writefd, buff, n);
Close(fd);
}
}
t1.txt 文件内容
执行结果:
2. 全双工管道
socketpair()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int d, int type, int protocol, int sv[2]);
//函数用于创建一对无名的、相互连接的套接子。
如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中。
基本用法:
1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。
例子:
// fduplex.c
#include "unpipc.h"
int main(int argc, char **argv)
{
int fd[2], n;
char c;
pid_t childpid;
socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
if ( (childpid = Fork()) == 0)
{
/* child */
sleep(3);
if ( (n = Read(fd[0], &c, 1)) != 1)
err_quit("child: read returned %d", n);
printf("child read %c\n", c);
Write(fd[0], "c", 1);
exit(0);
}
/* parent */
Write(fd[1], "p", 1);
if ( (n = Read(fd[1], &c, 1)) != 1)
err_quit("parent: read returned %d", n);
printf("parent read %c\n", c);
exit(0);
}
结果:
3. popen 和 pclose 函数
作为另一个关于管道的例子,标准 I/O 函数库提供了popen函数,它创建一个管道并启动另外一个进程,该进程要么从该管道读出标准输入,要么往该管道写入标准输出。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
// 返回:若成功则为文件指针,若出错则为 NULL
int pclose(FILE *stream);
// 返问:若成功则为shell的终止状态,若出错则为 -1
其中command是一个shell命令行。它是由sh程序(通常为Bourne shell)处理的,因此PATH环境变量可用于定位command。 popen在调用进程和所指定的命令之间创建一个管道。由popen返回的值是一个标准I/O FILE指针,该指针或者用于输入,或者用于输出,具体取决于字符串type。
- 如果type为r,那么调用进程读进command的标准输出。
- 如果type为w,那么调用进程写到command的标准输入。
pclose函数关闭由popen创建的标准I/0流(stream ),等待其中的命令终止,然后返回shell的终止状态。
例子:
#include "unpipc.h"
int main(int argc, char **argv)
{
size_t n;
char buff[MAXLINE], command[MAXLINE];
FILE *fp;
/* read pathname */
Fgets(buff, MAXLINE, stdin);
n = strlen(buff); /* fgets() guarantees null byte at end */
if (buff[n-1] == '\n')
n--; /* delete newline from fgets() */
snprintf(command, sizeof(command), "cat %s", buff);
fp = Popen(command, "r");
/* copy from pipe to standard output */
while (Fgets(buff, MAXLINE, fp) != NULL)
Fputs(buff, stdout);
Pclose(fp);
exit(0);
}
运行结果:
./mainpopen
t1.txt
执行之后成功打印出 t1.txt文件中内容
4. FIFO
管道没有名字,因此它们的最大劣势是只能用于有一个共同祖先进程的各个进程之间。我们无法在无亲缘关系的两个进程间创建一个管道并将它用作IPC通道(不考虑描述符传递)。
FIFO指代先进先出(first in, first out), Unix中的FIFO类似于管道。它是一个单向(半双工)数据流。不同于管道的是,每个FIFO有一个路径名与之关联,从而允许无亲缘关系的进程访问同一个FIFO, FIFO也称为有名管道(named pipe)。
FIFO由mkfifo函数创建。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:pathname是一个普通的Unix路径名,它是该FIFO的名字。
mode参数指定文件权限位,类似于open的第二个参数。
返回:若成功则为0,若出错则为-1
mkfifo函数已隐含指定0_CREAT | 0_EXCL。也就是说,它要么创建一个新的FIFO,要么返回一个EEXIST错误(如果所指定名字的FIFO已经存在),如果不希望创建一个新的FIFO,那就改为调用open而不是mkfifo。要打开一个已存在的FIFO或创建一个新的FIFO,应先调用mkfifo,再检查它是否返回EEXIST错误,若返回该错误则改为调用open。
mkfifo命令也能创建FIFO,可以从shell脚本或命令行中使用它。
在创建出一个FIFO后,它必须或者打开来读,或者打开来写,所用的可以是open函数,也可以是某个标准 I/O 打开函数,例如fopen。 FIFO不能打开来既读又写,因为它是半双工的。对管道或FIFO的write总是往末尾添加数据,对它们的read则总是从开头返回数据。如果对管道或FIFO调用lseek,那就返回ESPIPE错误。
例子:
// mainfifo.c
#include "unpipc.h"
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
void client(int, int);
int server(int, int);
int main(int argc, char **argv)
{
int readfd, writefd;
pid_t childpid;
/* create two FIFOs; OK if they already exist */
if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
err_sys("can't create %s", FIFO1);
if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
{
unlink(FIFO1);
err_sys("can't create %s", FIFO2);
}
if ( (childpid = Fork()) == 0)
{
/* child */
readfd = Open(FIFO1, O_RDONLY, 0);
writefd = Open(FIFO2, O_WRONLY, 0);
server(readfd, writefd);
exit(0);
}
/* parent */
writefd = Open(FIFO1, O_WRONLY, 0);
readfd = Open(FIFO2, O_RDONLY, 0);
client(readfd, writefd);
Waitpid(childpid, NULL, 0); /* wait for child to terminate */
Close(readfd);
Close(writefd);
Unlink(FIFO1);
Unlink(FIFO2);
exit(0);
}
运行结果:
4.2 例子:无亲缘关系的客户与服务器
头文件:
// fifo.h
#include "unpipc.h"
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
服务器:
// server_main.c
#include "fifo.h"
void server(int, int);
int main(int argc, char **argv)
{
int readfd, writefd;
/* create two FIFOs; OK if they already exist */
if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
err_sys("can't create %s", FIFO1);
if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
{
unlink(FIFO1);
err_sys("can't create %s", FIFO2);
}
readfd = Open(FIFO1, O_RDONLY, 0);
writefd = Open(FIFO2, O_WRONLY, 0);
server(readfd, writefd);
exit(0);
}
客户端:
#include "fifo.h"
void client(int, int);
int main(int argc, char **argv)
{
int readfd, writefd;
writefd = Open(FIFO1, O_WRONLY, 0);
readfd = Open(FIFO2, O_RDONLY, 0);
client(readfd, writefd);
Close(readfd);
Close(writefd);
Unlink(FIFO1);
Unlink(FIFO2);
exit(0);
}
运行:
方法一:先运行服务器,在运行客户端
方法二:只启动客户端,服务器由客户端通过fork和exec来激活。
5、小结
管道和FIFO是许多应用程序的基本构建模块。管道普遍用于shell中,不过也可以从程序中使用,往往是用于从子进程向父进程回传信息。使用管道时涉及的某些代码(pipe、 fork、close、 exec和waitpid)可通过使用popen和pclose来避免,由它们处理具体细节并激活一个shell。
FIFO与管道类似,但它们是用 mkfifo 创建的,之后需用open打开。打开管道时必须小心,因为有许多规则制约着open的阻塞与否。