管道
管道是最初的IPC形式。管道的根本局限在于没有名字,从而只能由具有亲缘关系的进程使用。管道和FIFO都是使用read和write函数访问的(因为FIFO也是一种文件类型)。
创建管道
#include <unistd.h>
int pipe(int fd[2]);
返回值:成功,0;出错,-1
返回的是两个文件描述符:fd[0],用于读;fd[1],用于写。
管道是半双工的,即单向的,只提供一个方向的数据流。所以:
管道用于一个进程内部时,可通过fd[1]来写数据,然后其再从fd[0]来读。这样玩没什么意思,毕竟管道是要实现IPC的
管道用于多个进程时(例如两个进程间),若只创建了一个管道,则只能一个进程写,另一个进程读,不能双向通信,这也没什么意思。
要实现两个进程间双向通信,则需要两个管道。
在shell中输入以下命令:
who | sort | lp
则创建了3个进程,2个管道
例1:
使用管道实现客户端-服务器通行。父进程作为客户端,子进程作为服务器,客户端向服务器发送一个文件的路径名,服务器打开该文件,读取文件内容,发送给客户端。
思考:
这是两个进程间双向通信的问题,所以需要两个管道:客户端通过一个管道发送路径名,服务器通过另一个管道发回文件内容。父进程创建两个管道,然后再fork子进程,父进程的所有打开的文件描述符都会被复制到子进程中,关系图如下:
程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#define MAXLINE 4096 //缓冲区大小
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
void server(int readfd, int writefd);
void client(int readfd, int writefd);
int main(int argc, char *argv[])
{
//创建两个管道
int fd1[2], fd2[2];
if (pipe(fd1) != 0)
ERR_EXIT("pipe() error");
if (pipe(fd2) != 0)
ERR_EXIT("pipe() error");
//设置管道方向
pid_t childpid;
if ((childpid = fork()) < 0)
ERR_EXIT("fork() error");
if (childpid == 0)//子进程
{
close(fd1[1]);
close(fd2[0]);
server(fd1[0], fd2[1]);//读取发送过来的路径名,发送文件内容给父进程
exit(0);
}
close(fd1[0]);
close(fd2[1]);
client(fd2[0], fd1[1]);//发送路径名给子进程,读取发送过来的文件内容
waitpid(childpid, NULL, 0);//等待子进程终止,回收子进程资源
exit(0);
}
//客户端程序
void client(int readfd, int writefd)
{
char buf[MAXLINE];//设置缓冲区
fgets(buf, MAXLINE, stdin);//从标准输入读路径名
size_t len = strlen(buf);
if (buf[len - 1] == '\n')//去掉路径名末尾的换行符
len--;
if (write(writefd, buf, len) != len)//将路径名写入管道
ERR_EXIT("write() error");
ssize_t n;
while((n = read(readfd, buf, MAXLINE)) > 0)//读取管道(子进程发送过来的文件内容)
write(STDOUT_FILENO, buf, n);
}
//服务器程序
void server(int readfd, int writefd)
{
ssize_t n;
char buf[MAXLINE + 1];
if ((n = read(readfd, buf, MAXLINE)) == 0)//从管道读取路径名
ERR_EXIT("read() error");
buf[n] = '\0';
//打开文件
int fd;
if ((fd = open(buf, O_RDONLY)) < 0)
{
snprintf(buf + n, sizeof(buf) - n, ": can't open, %s\n", strerror(errno));
n = strlen(buf);
write(writefd, buf, n);
}
else
{ //读取文件内容,写入管道
while ((n = read(fd, buf, MAXLINE)) > 0)
write(writefd, buf, n);
close(fd);
}
}
结果:
分析:
1.在使用管道时,要将不用的文件描述符关掉。
2.重点函数:pipe、fork、waitpid、fgets、strlen、read、write、snprintf
全双工管道
全双工管道是双向的,提供两个方向的数据流,管道的任一一端既可以读也可以写,一个全双工管道是由两个半双工管道实现的,如下图所示:
例2:
用一个全双工管道代替两个半双工管道实现例1
程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/socket.h>
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
int main()
{
pid_t pid;
int fd[2];
char c;
ssize_t n;
socketpair(AF_UNIX, SOCK_STREAM, 0, fd);//创建全双工管道
if ((pid = fork()) < 0)
ERR_EXIT("fork() error");
if (pid == 0)
{
//子进程
sleep(3);
if ((n = read(fd[0], &c, 1)) != 1)
ERR_EXIT("child read() error");
printf("child read %c\n", c);
write(fd[0], "c", 1);
exit(0);
}
write(fd[1], "p", 1);
if ((n = read(fd[1], &c, 1)) != 1)
ERR_EXIT("parent read() error");
printf("parent read %c\n", c);
exit(0);
}
结果:
分析:
1.用socketpair(AF_UNIX, SOCK_STREAM, 0, fd)创建全双工管道2.一个进程只能用一个描述符,如子进程不管读还是写,只能用fd[0]
popen、 pclose
popen创建一个管道,并启动一个进程,通过管道读该启动进程的标准输出,或写到该进程的标准输入
#include <stdio.h>
FILE *popen(const char *command, const char *type);
返回值:成功,文件指针;失败,NULL
int pclose(FILE *stream);
popen:若 type = “r”,则调用进程读command的标准输出;若 type = “w”,则调用进程写到command的标准输入。
例3:从标准输入读入一个目录名,通过popen启动另一个进程ls,ls列出该目录下的文件,标准输出作为管道的输入。
程序:
#include <stdio.h>
#include <unistd.h>
#define MAXLINE 4096
int main(int argc, char *argv[])
{
char buff[MAXLINE], command[MAXLINE];
FILE *fp;
fgets(buff, MAXLINE, stdin);//读标准输入
size_t len = strlen(buff);
if (buff[len -1] == '\n')
len--;
snprintf(command, sizeof(command), "ls %s", buff);//设置另一个命令
fp = popen(command, "r");//启动进程
while (fgets(buff, MAXLINE, fp) != NULL)//读启动进程的标准输入
fputs(buff, stdout);
pclose(fp);
exit(0);
}
结果:
分析:
1.标准输入里面是个目录的路径,则popen启动的例程相当于 ls /home/zxin/unp/ch4,结果通过管道读出到文件指针。
FIFO(命名管道)
FIFO类似于管道,但最重要的是FIFO有名字,每个FIFO有一个路径名,因此可以用于无亲缘关系的进程之间。FIFO指代先进先出(first in, first out),它也是半双工(单向)的。
创建FIFO
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:成功,0;出错,-1
理解:pathname:一个路径名,是创建的FIFO的名字。mode:文件权限位,可取S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。
mkfifo默认的创建方式是O_CREAT | O_EXCL,所以,若FIFO已存在,则错误,此时errno = EEXIST。
和管道使用区别:通过pipe创建管道后,即可read或write该管道;但对FIFO来说,通过mkfifo创建FIFO后,要先open该FIFO,才能read或者write。区别原因在于fd[0]、fd[1]是文件描述符,而FIFO是文件。
例4:通过FIFO来重写例1,server和client函数不用改,只用改main函数就行
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#define MAXLINE 4096 //缓冲区大小
#define FIFO1 "/home/zxin/unp/ch4/fifo.1"
#define FIFO2 "/home/zxin/unp/ch4/fifo.2"
#define FIFO_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
void server(int readfd, int writefd);
void client(int readfd, int writefd);
int main(int argc, char *argv[])
{
pid_t pid;
int readfd, writefd;
if (mkfifo(FIFO1, FIFO_MODE) < 0)
ERR_EXIT("mkfifo() error");
if (mkfifo(FIFO2, FIFO_MODE) < 0)
ERR_EXIT("mkfifo() error");
if ((pid = fork()) < 0)
ERR_EXIT("fork() error");
if (pid == 0)
{
readfd = open(FIFO1, O_RDONLY);//打开FIFO1
writefd = open(FIFO2, O_WRONLY);//打开FIFO2
server(readfd, writefd);
exit(0);
}
writefd = open(FIFO1, O_WRONLY);//打开FIFO1
readfd = open(FIFO2, O_RDONLY);//打开FIFO2
client(readfd, writefd);
waitpid(pid, NULL, 0);
unlink(FIFO1);
unlink(FIFO2);
exit(0);
}
void client(int readfd, int writefd)
{
char buf[MAXLINE];//设置缓冲区
fgets(buf, MAXLINE, stdin);//从标准输入读路径名
size_t len = strlen(buf);
if (buf[len - 1] == '\n')//去掉路径名末尾的换行符
len--;
if (write(writefd, buf, len) != len)//将路径名写入管道
ERR_EXIT("write() error");
ssize_t n;
while((n = read(readfd, buf, MAXLINE)) > 0)//读取管道(子进程发送过来的文件内容)
write(STDOUT_FILENO, buf, n);
}
void server(int readfd, int writefd)
{
ssize_t n;
char buf[MAXLINE + 1];
if ((n = read(readfd, buf, MAXLINE)) == 0)//从管道读取路径名
ERR_EXIT("read() error");
buf[n] = '\0';
//打开文件
int fd;
if ((fd = open(buf, O_RDONLY)) < 0)
{
snprintf(buf + n, sizeof(buf) - n, ": can't open, %s\n", strerror(errno));
n = strlen(buf);
write(writefd, buf, n);
}
else
{ //读取文件内容,写入管道
while ((n = read(fd, buf, MAXLINE)) > 0)
write(writefd, buf, n);
close(fd);
}
}
结果:
例5:FIFO和管道最大区别在于FIFO可用于无亲缘关系直接的进程,所以再改变下例1,分别创建两个无亲缘关系的进程,并通过FIFO进行通信,它们之间关系如下:
程序:
公共头文件:
#ifndef _FIFO_IPC_H_H
#define _FIFO_IPC_H_H
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
#define FIFO1 "/home/zxin/unp/ch4/fifo_ipc/fifo.1"
#define FIFO2 "/home/zxin/unp/ch4/fifo_ipc/fifo.2"
#define FIFO_MODE (S_IRUSR | S_IWUSR | S_IRGRP |S_IROTH)
服务器程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include "fifo.h"
#define MAXLINE 4096 //缓冲区大小
void server(int readfd, int writefd);
int main(int argc, char *argv[])
{
pid_t pid;
int readfd, writefd;
if (mkfifo(FIFO1, FIFO_MODE) < 0)//创建FIFO1
ERR_EXIT("mkfifo() error");
if (mkfifo(FIFO2, FIFO_MODE) < 0)//创建FIFO2
ERR_EXIT("mkfifo() error");
readfd = open(FIFO1, O_RDONLY);
writefd = open(FIFO2, O_WRONLY);
server(readfd, writefd);
exit(0);
}
void server(int readfd, int writefd)
{
ssize_t n;
char buf[MAXLINE + 1];
if ((n = read(readfd, buf, MAXLINE)) == 0)//从管道读取路径名
ERR_EXIT("read() error");
buf[n] = '\0';
//打开文件
int fd;
if ((fd = open(buf, O_RDONLY)) < 0)
{
snprintf(buf + n, sizeof(buf) - n, ": can't open, %s\n", strerror(errno));
n = strlen(buf);
write(writefd, buf, n);
}
else
{ //读取文件内容,写入管道
while ((n = read(fd, buf, MAXLINE)) > 0)
write(writefd, buf, n);
close(fd);
}
}
客户端程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include "fifo.h"
#define MAXLINE 4096 //缓冲区大小
void client(int readfd, int writefd);
int main(int argc, char *argv[])
{
pid_t pid;
int readfd, writefd;
writefd = open(FIFO1, O_WRONLY);
readfd = open(FIFO2, O_RDONLY);
client(readfd, writefd);
unlink(FIFO1);
unlink(FIFO2);
exit(0);
}
void client(int readfd, int writefd)
{
char buf[MAXLINE];//设置缓冲区
fgets(buf, MAXLINE, stdin);//从标准输入读路径名
size_t len = strlen(buf);
if (buf[len - 1] == '\n')//去掉路径名末尾的换行符
len--;
if (write(writefd, buf, len) != len)//将路径名写入管道
ERR_EXIT("write() error");
ssize_t n;
while((n = read(readfd, buf, MAXLINE)) > 0)//读取管道(子进程发送过来的文件内容)
write(STDOUT_FILENO, buf, n);
}
结果:
例6:下面实现的是一个服务器,多个客户端之间的通信。
这个模型的问题是服务器收到要打开文件的路径名并读取文件内容后,服务器如何知道要发给哪个客户端?解决的办法就是:服务器以一个大家都知道的路径名创建一个服务器管道,所有客户端要发送的路径名都从该管道发送给服务器,但每个客户端发送的路径名里面包含有自己的进程ID。每个客户端都创建一个客户端管道,服务器从路径名里判断读完的内容该写到哪个客户端管道里。过程如下图所示:
程序:
公共头文件:
#ifndef _FIFO_IPC_H_H
#define _FIFO_IPC_H_H
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
#define SERVER_FIFO "/home/zxin/unp/ch4/fifo_lots/serverfifo"
#define FIFO_MODE (S_IRUSR | S_IWUSR | S_IRGRP |S_IROTH)
#define MAXLINE 4096
#endif
客户端程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include "fifo.h"
int main()
{
char fifoname[MAXLINE], buff[MAXLINE];
pid_t pid;
pid = getpid();
int writefifo;
//设置读取数据管道
snprintf(fifoname, MAXLINE, "/home/zxin/unp/ch4/fifo_lots/fifo.%ld", (long)pid);//管道名
if (mkfifo(fifoname, FIFO_MODE) < 0)//创建管道
ERR_EXIT("mkfifo() error");
//设置要写给服务器的内容
snprintf(buff, MAXLINE, "%ld ", (long)pid);//进程ID号
size_t len = strlen(buff);
char *ptr = buff + len;
fgets(ptr, MAXLINE - len, stdin);//从标准输入读取文件的路径名
//将请求写给服务器
if ((writefifo = open(SERVER_FIFO, O_WRONLY)) < 0)
ERR_EXIT("client open SERVER_FIFO error");
len = strlen(buff);
write(writefifo, buff, len);
//读取服务器发回的数据
int readfifo = open(fifoname, O_RDONLY);
ssize_t n;
while ((n = read(readfifo, buff, MAXLINE)) > 0)
write(STDOUT_FILENO, buff, n);
//清除管道
close(readfifo);
unlink(fifoname);
exit(0);
}
服务器程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include "fifo.h"
int main()
{
char buff[MAXLINE];
char fifoname[MAXLINE];
ssize_t n;
char *ptr;
pid_t pid;
int fd;
int writefifo;
size_t len;
//创建服务器管道
if (mkfifo(SERVER_FIFO, FIFO_MODE) < 0)
ERR_EXIT("mkfifo() error");
int readfifo = open(SERVER_FIFO, O_RDONLY);
int dummyfd = open(SERVER_FIFO, O_WRONLY);//避免管道被重复打开,不用
//读取客户端的请求,发回数据
while ((n = read(readfifo, buff, MAXLINE)) > 0)
{
//读取进程ID、文件路径名
if (buff[n - 1] == '\n')
n--;
buff[n] = '\0';
printf("%s\n", buff);
if ((ptr = strchr(buff, ' ')) == NULL)//定位到空格位置
continue;
*ptr++ = 0;//现在ptr指向文件路径名
pid = atol(buff);//读取进程ID
snprintf(fifoname, MAXLINE, "/home/zxin/unp/ch4/fifo_lots/fifo.%ld", (long)pid);//读取管道名
writefifo = open(fifoname, O_WRONLY);//打开管道,以写回数据
//读取文件内容
if ((fd = open(ptr, O_RDONLY)) < 0)//打开客户端请求的文件
{
//若打开失败,则写回错误信息
snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n", strerror(errno));
len = strlen(ptr);
write(writefifo, ptr, n);
close(writefifo);
}
else
{
//读取文件内容并写回管道
while ((n = read(fd, buff, MAXLINE)) > 0)
write(writefifo, buff, n);
close(fd);
close(writefifo);
}
}
exit(0);
}
结果:
分析:
重点函数:strchr、atol、snprintf
管道和FIFO的读写规则
管道和FIFO默认是阻塞的,可以有以下两种方法设置成非阻塞
1.调用open时指定O_NONBLOCK标识
writefd = open(FIFO1, O_WRONLY | O_NONBLOCK);
2.如果一个描述符已经打开,则可通过fcntl来启用O_NONBLOCK标识
int flags = fcntl(fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
注:不能简单的通过fcntl(fd, F_SETFL, O_NONBLOCK)设置
参考文章:管道的读写规则
当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
系统加于管道和FIFO的唯一限制为:
OPEN_MAX:一个进程在任一时刻能够打开的最大描述符数
PIPE_BUF:可原子地写往一个管道或FIFO的最大数据量 定义在<limits.h>中