1、概述
管道是最初Unix IPC形式,他们的存在没有名字,只能用于具有血缘关系的进程之间的通信(如父子进程)。这一点随着FIFO的引入得到改正,FIFO有时称为有名管道(named pipe)。
2、一个简单客户端-服务器例子
客户端从标准输入(stdin)读进一个路径名,并把它写入IPC通道,服务器从该IPC通道读出这个路径名,并尝试打开其文件内容来读。如果服务器能打开文件,它就读取其内容,并写回另外一个IPC通道,客户端随后从该IPC通道中读取内容,并写到标准输出(stdout).如果服务器无法读取路径指定的文件内容,则返回一个错误信息。
3、 管道
#include <unistd.h>
int pipe(int fd[2]);
pipe()函数返回两个文件描述符, fd[0]用来读,fd[1]用来写。
宏S_ISPIPE可用来确定一个文件描述符是管道还是FIFO, 它的唯一参数是stat结构中的st_mode成员。对于管道来说通过fstat函数获取。
pipe尽管是在单个进程中创建,但是很少在单个进程中使用,一般是用来父子进程之间的通信的,下面是一个常用的情景。
父进程首先通过pipe创建管道,然后通过fork函数创建一个子进程,这样父子经常的管道就指向了相同的内容,他们可以根据自己的需要关闭其中一个描述符,如父进程只负责写fd[1](关闭读fd[0]), 子进程只负责读fd[0](关闭写fd[1])。
如图,这样就可以实现进程间的单向通信。
上面这种方式实现的都是半双工的(即单向通信的), 如果需要实现双向通信,则需要创建两个管道。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
void client(int rd, int wr);
void server(int rd, int wr);
int main(int argc, char *argv[])
{
int fd1[2], fd2[2];
pid_t childpid;
int ret;
ret = pipe(fd1);
ret |= pipe(fd2);
if (ret < 0) {
fprintf(stderr, "pipe errr\n");
return -1;
}
if ((childpid = fork()) == 0) {
/* child process */
close(fd1[1]);
close(fd2[0]);
server(fd1[0], fd2[1]);
exit(0); // 子进程结束之后,父进程不知道,没有回收资源,编程僵尸进程
} else if (childpid < 0) {
fprintf(stderr, "fork err\n");
return -1;
}
/* parent */
close(fd1[0]);
close(fd2[1]);
client(fd2[0], fd1[1]);
waitpid(childpid, NULL, 0); // 父进程结束需要回收子进程的资源,不然会变成孤儿进程
exit(0);
}
#define MAXLINE 1024
void client(int rd, int wr)
{
size_t len;
ssize_t n = 0;
char buf[MAXLINE] = {0};
fgets(buf, MAXLINE, stdin);
len = strlen(buf);
if (buf[len -1] == '\n') {
len--;
}
write(wr, buf, len);
while ((n = read(rd, buf, MAXLINE)) > 0) {
write(STDOUT_FILENO, buf, n);
}
}
void server(int rd, int wr)
{
int fd;
ssize_t n;
char buf[MAXLINE] = {0};
if ((n = read(rd, buf, MAXLINE)) == 0) {
fprintf(stderr, "read file path fail\n");
exit(1);
}
buf[n] = '\0';
if ((fd = open(buf, 0, O_RDONLY)) < 0) {
snprintf(buf + n, sizeof(buf) - n, "cannot open %s\n", strerror(errno));
n = strlen(buf);
write(wr, buf, n);
} else {
while ((n = read(fd, buf, MAXLINE)) > 0) {
write(wr, buf, n);
}
close(fd);
}
}
3、全双工管道
socketpair函数:
//NAME
// socketpair - create a pair of connected sockets
//
//SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
//On success, zero is returned. On error, -1 is returned
全双工的可能实现如下图,隐含的意思是,整个管道只存在一个缓冲区,协议管道的任何数据都添加到缓冲区的末尾,从管道读取都是从缓冲区的开头读取。
但是这样的实现是有问题的,实际上sockerpair的实现的全双工是使用两个半双工管道实现的,写入fd[1]的只能从fd[0]读取, 从fd[0]写入的只能从fd[1]读取。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
int main(int argc, char *argv)
{
int fd[2];
int n;
char c;
pid_t childpid;
if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, fd) < 0) {
fprintf(stderr, "socketpair fail\n");
exit(-1);
}
if ((childpid = fork()) < 0) {
fprintf(stderr, "fork fail\n");
close(fd[0]);
close(fd[1]);
exit(-1);
} else if (childpid == 0) {
/* child process */
sleep(3);
if ((n = read(fd[0], &c, 1)) != 1) {
fprintf(stderr, "child read return %d\n", n);
exit(-2);
}
fprintf(stderr, "child read %c\n", c);
write(fd[0], "c", 1);
exit(0);
}
/* parent process */
write(fd[1], "p", 1);
if ((n = read(fd[1], &c, 1)) != 1) { // block here wait child write back
fprintf(stderr, "child read return %d\n", n);
exit(-1);
}
fprintf(stderr, "parent read %c\n", c);
exit(0);
}
4、popen和pclose函数
标准I/O函数提供了popen函数,它创建了一个管道并启动另外一个进程,该进程要么从该管道读取标准输入,要么往该管道写入标准输出。
//NAME
// popen, pclose - pipe stream to or from a process
//SYNOPSIS
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen函数调用进程和所指定的命令之间创建一个管道,由popen返回的值是一个FILE指针,该指针用于输入或者输出,取决于type.
- 如果type为r, 则调用进程读取command的标准输出。
- 如果type为w, 则调用进程写到command的标准输入。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXLINE 1024
int main(int argc, char *argv[])
{
size_t n;
char buf[MAXLINE], cmd[MAXLINE + 5];
FILE *fp = NULL;
if (argc != 2) {
fprintf(stderr, "usage: ./a.out <filename>\n");
exit(-1);
}
snprintf(cmd, sizeof(cmd), "cat %s", argv[1]);
if ((fp = popen(cmd, "r")) == NULL) {
fprintf(stderr, "popen: %s fail\n", cmd);
exit(-2);
}
while ( fgets(buf, MAXLINE, fp) != NULL) {
fputs(buf, stdout);
}
pclose(fp);
exit(0);
}
5、FIFO
管道是没有名字的,这就限制了它只能在有血缘关系的进程之间使用,这就引入了一个有名管道fifo,它可以在没有关系的进程之间通信。
FIFO由mkfifo函数创建。
//NAME
// mkfifo, mkfifoat - make a FIFO special file (a named pipe)
//SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname: 是一个普通的unix路径名,他是该FIFO的名字。
mode: 指定该文件权限位,同open函数,定义在<sys/stat.h>头文件中。
mkfifo函数已经隐含指定了O_CREAT | O_EXCL,也就是说要么创建成功,要么返回一个EEXIST错误。如果不希望创建一个新的FIFO, 就改为调用open函数而不是mkfifo函数,要调用或者创建一个FIFO,应先调用mkfifo函数,再检查是否返回EEXIST错误,若返回该错误则调用open函数。
6、管道和FIFO的额外属性
设置为非阻塞状态:
1. 调用open时可指定O_NONBLOCK标志。
writefd = open(FIFO1, O_WRONLY | O_NONBLOCK);
2. 如果一个描述符已经打开,可以调用fcntl函数设置为非阻塞。对于管道来说必须使用这种技术,因为管道没有open调用,在pipe调用中也无法指定O_NONBLOCK标志。
int flags;
if ((flags = fcntl(fd, F_GETFL, 0)) < 0) {
printf("fail\n");
exit(-1);
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags ) < 0) {
printf("fail\n");
exit(-2);
}
注意:设置属性的时候不能删除其他属性设置,所有要先获取,然后在原来的基础上做操作。