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$ 进程发送的数据。
        管道是单向的,如果需要双向传输数据,我们必须创建两个管道,每个方向一个:

  1. 创建管道 $fd1$ 和管道 $fd2$ ,并 $fork$ 一个子进程;
  2. 父进程关闭 $fd1[0]$ 和 $fd2[1]$ ;
  3. 子进程关闭 $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. popenpclose函数

#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的读写,有以下规则:

5. 字节流与消息

        到此为止的所有例子都是使用的字节流I/O模型,这是Unix的原生I/O模型。这种模型不存在记录边界,读写操作不会检查数据。有时候应用希望对所发送的数据加上某种结构,读出者可以通过结构确认消息边界。有三种方式可以实现这个目的:

  1. 带内特殊终止序列。许多Unix应用程序使用换行符来分隔消息。写进程会给每个消息添加一个换行符,读进程则每次读出一行。这种方式也要求对一般数据中出现的分隔符转义处理;
  2. 显式长度。每个记录前标记长度;
  3. 每次连接一个记录。应用通过关闭与对端的连接来指示一个记录的结束。

        我们也可以给消息增加一些结构。

#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$ 的值可以通过 $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);
}

Unix网络编程(16):管道和FIFO