Unix网络编程(16):管道和FIFO
1. 管道
#include <unistd.h>
// 成功返回0,出错返回-1
int pipe(int fd[2]);
该函数返回两个文件描述符 $fd[0]$ 和 $fd[1]$ ,前者用于读,后者用于写。宏 $S_-IFFIFO$ 可用于确定一个描述符是文件、管道还是FIFO
,它的唯一参数是 $stat$ 结构的 $st_-mode$ 成员,计算结果或为真 ( 非零值 ),或为假 ( $0$ )。对于管道来说,这个 $stat$ 结构是由 $fstat$ 函数填写的;对于FIFO
来说,这个结构是由 $fstat$ 、$lstat$ 或 $stat$ 函数填写的。
管道的典型用途是为两个不同的进程 ( 父进程和子进程 ) 提供进程间通信。首先,由一个进程 ( 父进程 ) 创建一个管道,然后调用 $fork$ 派生一个副本 ( 子进程 )。接着,父进程关闭这个管道的读端 ( $fd[0]$ ),子进程关闭管道的写端 ( $fd[1]$ ),这样就在父子进程间建立了一个单向数据流。我们在Unix
shell
中输入如下命令时:
who | sort
将会创建一个管道,$who$ 进程通过输出端将数据发送给 $sort$ 进程,$sort$ 进程通过读入端读入 $who$ 进程发送的数据。
管道是单向的,如果需要双向传输数据,我们必须创建两个管道,每个方向一个:
- 创建管道 $fd1$ 和管道 $fd2$ ,并 $fork$ 一个子进程;
- 父进程关闭 $fd1[0]$ 和 $fd2[1]$ ;
- 子进程关闭 $fd1[1]$ 和 $fd2[0]$ 。
#include "unpipc.h"
void client(int, int), server(int, int);
int main(int argc, char **argv) {
int pipe1[2], pipe2[2];
pid_t childpid;
Pipe(pipe1);
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]);
clinet(pipe2[0], pipe1[1]);
Waitpid(childpid, NULL, 0); // wait for child to terminate
exit(0);
}
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);
}
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);
}
}
子进程在往管道写入最终数据后调用 $exit$ 终止,随后变为僵尸进程 ( $zombie$ ):自身终止但父进程仍在运行且未等待该子进程。子进程终止时,内核会给父进程发送一个 $SIGCHILD$ 信号,但是父进程忽略了这个信号。随后父进程调用 $waitpid$ 取得子进程的终止状态。如果父进程没有调用 $waitpid$ 就终止,那么子进程变为孤儿进程,其父进程将变为 $init$ 进程,由 $init$ 进程获取其终止状态。
2. popen
和pclose
函数
#include <stdio.h>
// 成功为文件指针,出错为NULL
FILE *popen(const char *command, cosnt char *type);
// 成功为shell终止状态,出错为-1
int pclose(FILE *stream);
$popen$ 函数创建一个管道并启动另外一个进程,该进程要么从该管道读出标准输入,要么往该管道写入标准输出。$command$ 是一个shell
命令,$type$ 可以是 $r$ 或者 $w$ ,分别对应着读入 $command$ 的标准输出和写到 $command$ 的标准输入。$pclose$ 函数则负责关闭由 $popen$ 创建的标准I/O
流 $stream$ ,等待命令终止并返回shell
终止状态。
#inlcude "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);
}
3. FIFO
FIFO
指先进先出 ( $first$ $in$ $first$ $out$ ),类似于管道,是一个单向的数据流。不同于管道的是,每个FIFO
都有一个路径名与之关联,从而允许无亲缘关系的进程访问同一个FIFO
,也被称为有名管道 ( $named$ $pipe$ )。
#include <sys/types.h>
#include <sys/stat.h>
// 成功返回0,出错返回-1
int mkfifo(const char *pathname, mode_t mode);
$pathname$ 是一个普通的Unix
路径名,是FIFO
的名字。$mode$ 指定文件权限,类似于 $open$ 的第二个参数,定义在 <$sys/stat.h$> 头文件中。
常值 | 说明 |
---|---|
$S_-IRUSR$ | 用户 ( 属主 ) 读 |
$S_-IWUSR$ | 用户 ( 属主 ) 写 |
$S_-IRGRP$ | ( 属 ) 组成员读 |
$S_-IWGRP$ | ( 属 ) 组成员写 |
$S_-IROTH$ | 其他用户读 |
$S_-IWOTH$ | 其他用户写 |
$mkfifo$ 隐式指定 $O_-CREAT$ | $O_-EXCL$ ,意味着要么创建一个新的FIFO
,要么返回 $EEXIST$ 错误。如果不希望创建一个新的FIFO
,那么应该调用 $open$ 或者 $fopen$ 。由于FIFO
是半双工的,所以不能以读写方式打开,并且对其的 $lseek$ 调用也会报错,返回 $ESPIPE$ 错误。我们定义默认的FIFO
权限如下:
#include "unpipc.h"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#include "unpipc.h"
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
void client(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);
}
没有正确使用FIFO
的程序会产生问题,例如上述代码中调换父进程的两个 $open$ 的顺序,程序就会被阻塞,因为在以读方式打开FIFO
的时候,没有其他进程以写方式打开相同的FIFO
,从而就会阻塞。
4. 管道和FIFO
的额外属性
当前操作 | 目标管道或FIFO 的模式 |
返回 | |
---|---|---|---|
阻塞 ( 默认 ) | $O_-NONBLOCK$ 设置 | ||
$open$ FIFO 只读 |
写模式打开的FIFO |
成功 | 成功 |
非写模式打开的FIFO |
阻塞直到FIFO 被以写模式打开 |
成功 | |
$open$ FIFO 只写 |
读模式打开的FIFO |
成功 | 成功 |
非读模式打开的FIFO |
阻塞直到FIFO 被以读模式打开
| 返回 $ENXIO$ 错误 | |
$read$ 空管道或FIFO |
写模式打开的管道或FIFO |
阻塞直到管道或FIFO 中有数据 / 管道或FIFO 不再以写模式打开 |
返回 $EAGAIN$ 错误 |
非写模式打开的管道或FIFO |
$read$ 返回 $0$ | $read$ 返回 $0$ | |
$write$ 管道或FIFO |
读模式打开的管道或FIFO |
见后续 | 见后续 |
非读模式打开的管道或FIFO |
给线程产生 $SIGPIPE$ | 给线程产生 $SIGPIPE$ |
描述符可以通过 $open$ 或者 $fcntl$ 来设置为非阻塞式。对于管道或FIFO
的读写,有以下规则:
- 如果请求读的数据量大于现有可用数据量,那么只会返回这些可用的数据;
- 如果请求写的字节数小于或等于 $PIPE_-BUF$ ( 一个
POSIX
限制值 ),那么 $write$ 操作是原子的; - $O_-NONBLOCK$ 标志的设置对 $write$ 操作的原子性没有影响;
- 在非阻塞模式下,如果待写字节数小于等于 $PIPE_-BUF$ :
- 如果该管道或
FIFO
中有足够的空间,写入所有数据; - 如果该管道或
FIFO
中没有足够的空间,立即返回 $EAGAIN$ 错误;
- 如果该管道或
- 在非阻塞模式下,如果待写字节数大于 $PIPE_-BUF$ :
- 如果该管道或
FIFO
中至少有 $1$ 字节空间,那么内核将尽可能写入剩余空间,并将写入字节数作为返回值; - 如果该管道或
FIFO
已满,返回 $EAGAIN$ 错误;
- 如果该管道或
- 向一个以非读模式打开的管道或
FIFO
写,内核将产生 $SIGPIPE$ 信号;- 如果调用进程没有捕获也没有忽略 $SIGPIPE$ ,那么默认行为是终止进程;
- 如果调用进程忽略了 $SIGPIPE$ ,或者捕获并从信号处理程序中返回,$write$ 将返回 $EPIPE$ 错误。
5. 字节流与消息
到此为止的所有例子都是使用的字节流I/O
模型,这是Unix
的原生I/O
模型。这种模型不存在记录边界,读写操作不会检查数据。有时候应用希望对所发送的数据加上某种结构,读出者可以通过结构确认消息边界。有三种方式可以实现这个目的:
- 带内特殊终止序列。许多
Unix
应用程序使用换行符来分隔消息。写进程会给每个消息添加一个换行符,读进程则每次读出一行。这种方式也要求对一般数据中出现的分隔符转义处理; - 显式长度。每个记录前标记长度;
- 每次连接一个记录。应用通过关闭与对端的连接来指示一个记录的结束。
我们也可以给消息增加一些结构。
#include "unpipc.h"
#define MAXMESGDATA (PIPE_BUF - 2 * sizeof(long))
#define MESGHDRSIZE (sizeof(struct mymesg) - MAXMESGDATA)
struct mymesg {
long mesg_len; // # bytes in mesg_data, can be 0
long mesg_type; // message type, must be > 0
char mesg_data[MAXMESGDATA];
};
ssize_t mesg_send(int, struct mymesg *);
ssize_t mesg_recv(int, struct mymesg *);
ssize_t mesg_send(int fd, struct mymesg *mptr) {
return write(fd, mptr, MESGHDRSIZE + mptr->mesg_len);
}
ssize_t mesg_recv(int fd, struct mymesg *mptr) {
size_t len;
ssize_t n;
// read message header first, to get len of data that follows
if ((n = Read(fd, mptr, MESGHDRSIZE)) == 0)
return 0; // end of file
else if (n != MESGHDRSIZE)
err_quit("message header: expected %d, got %d", MESGHDRSIZE, n);
if ((len = mptr->mesg_len) > 0)
if ((n = Read(fd, mptr->mesg_data, len)) != len)
err_quit("message data: expected %d, got %d", len, n);
return len;
}
6. 管道和FIFO
限制
系统加于管道和FIFO
的唯一限制为:
- $OPEN_-MAX$ :一个进程可以打开的最大描述符数;
- $PIPE_-BUF$ :可以原子地往管道或
FIFO
写入的最大数据量。
$OPEN_-MAX$ 的值可以通过 $sysconf$ 得到,$PIPE_-BUF$ 的值可以通过 $pathconf$ 或 $fpathconf$ 得到。
#include "unpipc.h"
int main(int argc, char **argv) {
if (argc != 2)
err_quit("usage: pipeconf <pathname>");
printf("PIPE_BUF = %ld, OPEN_MAX = %ld\n",
Pathconf(argv[1], _PC_PIPE_BUF), Sysconf(_SC_OPEN_MAX));
exit(0);
}