管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的方法。当进程创建管道时,每次都需要提供两个文件描述符来操作管道。其中一个对管道进行写操作,另一个对管道进行读操作。对管道的读写与一般的IO系统函数一致,使用write()函数写入数据,使用read()读出数据。
//创建管道
#include
int pipe(int filedes[2]);
//filedes是一个文件描述符的数组,用于保存管道返回的两个文件描述符。
//f[0]:读操作而创建和打开的。
//f[1]:写操作而创建和打开的。
//fd[1]的输出是fd[0]的输入。
//当函数执行成功时,返回0,失败时返回值为-1。
#include
#include
#include
#include
#include
int main(void)
{
int result = -1;
int fd[2],nbytes;
pid_t pid;
char string[] = "你好,管道";
char readbuffer[80];
int *write_fd = &fd[1];/*写文件描述符*/
int *read_fd = &fd[0];/*读文件描述符*/
result = pipe(fd);
if(-1 == result)
{
printf("建立管道失败\n");
return -1;
}
pid=fork();/*分叉出现*/
if(-1 == pid)
{
printf("fork进程失败\n");
return -1;
}
if(0 == pid)
{
close(*read_fd);
result = write(*write_fd,string,strlen(string));
return 0;
}
else
{
close(*write_fd);
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
return 0;
}
}
由于管道仅仅是将某个进程的输出和另一个进程的输入相邻的单向通信的方法,因此称其为"半双工"。
在shell中管道用"|"
表示,如下所示,管道的一种使用方式。
ls -l | grep *.c
把ls -l的输出当做"grep * .c
"的输入,管道在前 一 个进程 中建立输入通道 ,在后一 个进程建立输出通道,将数据从管道的左边传输到管道的右边,将Is -I
的输出通过管道传给 “grep *.c
”。
进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作。另一 个描述符对管道进行读操作。
如下图所示,显示了管道如何将两个进程通过内核连接起来,从图中可以看出这两个文件描述符是如何连接在一 起的。如果进程通过管道fda[0]
发送数据,它可以从fdb[0]
获得信息。
由于进程A和进程B都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。
这需要做好规划,两个进程都要做统一 的设置,在进程A中设置为读的管道描述符,在进程B中要设置为写;
反之亦然,并且要把不关心的管道端关掉。对管道的读写与一 般的IO 系统函数一 致,使用write()函数写入数据, read() 函数读出数据,某些特定的IO 操作管道是不支持的,例如偏移函数lseek()。
全双工管道是由两个半双工管道构成的,写入fd[1]的数据只能从fd[0]读出,写入fd[0]的数据只能从fd[1]读出。
int main(int argc, char **argv)
{
int fd[2], n;
char c;
pid_t childpid;
pipe(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);
}
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输 入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数实 现的操作是:创建一个管道, fork一个子进程,关闭未使用的管道端,执行一 个shell运行命令,然后等待命令终止。
#include
FILE *popen(const char *cmdstring, const char *type);
//type是"r",则返回的文件指针是可读的,是"w", 则是可写的。
//返回值:若成功,返回文件指针;若出错,返回NULL
int pclose(FILE *fp); //关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
// shell不能被执行,则pclose返回的终止状态与shell已执行exit一样。
//返回值:若成功,返回cmdstring的终止状态;若出错,返回-1
例:用popen向分页程序传送文件。
#include "apue.h"
#include
#define PAGER "${PAGER:-more}" /* 环境变量,或默认值 */
int main(int argc, char *argv[])
{
char line[MAXLINE];
FILE *fpin, *fpout;
if (argc != 2)
err_quit("usage: a.out " );
if ((fpin = fopen(argv[1], "r")) == NULL)
err_sys("can't open %s", argv[1]);
if ((fpout = popen(PAGER, "w")) == NULL)
err_sys("popen error");
/* copy argv[1] to pager */
while (fgets(line, MAXLINE, fpin) != NULL) {
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
}
if (ferror(fpin))
err_sys("fgets error");
if (pclose(fpout) == -1)
err_sys("pclose error");
exit(0);
}
管道没有名字,最大劣势是只能用于有一个共同祖先进程的各个进程之间。无法在无亲缘关系的两个进程间创建一个管道并将它用作IPC通道。
FIFO指代先进先出,FIFO类似管道,是一个单向数据流。不同于管道是,每个FIFO有一个路径名与之关联,从而允许无亲缘关系的进程访问同一个FIFO。FIFO也称有名管道。
命名管道的工作方式与普通的管道非常相似,但也有一些明显的区别。
有许多中方法可以创建命名管道。其中,可以直接用shell来完成。例:在目录**/tmp下建立一个名字为namedfifo的命名管道:
其中的属性中有一个p,表示这是一个管道。为了C语言创建FIFO。
用户可以使用mkfifo()
函数。
#include
#include
int mkfifo(const char *pathname,mode_t,mode);
int mkfifoat(int fd,cost char *path,mode_t mode);
//用mkfifo或者mkfifoat创建FIFO时,要用open来打开它。
//正常的文件I/O函数(如close、read、write和unlink)都需要FIFO。
mkfifoat函数和mkfifo
函数相似,但是mkfifoat
函数可以被用来在fd文件描述 符表示的目录相关的位置创建一个FIFO。像其他*at
函数一样,这里有3种情形:
AT_FDCWD
,则路径名以当前目录开始,mkfifoat和mkfifo类似。当一个进程打开一个FIFO一端时,如果FIFO的另一端没有打开,那么该进程会被阻塞。可以指定O_NONBLOCK
避免阻。
open一个FIFO时,非阻塞标志(O_NONBLOCK
)会产生下列影响:
O_NONBLOCK
,只读 open要阻塞到某个其他 进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读 而打开它为止。O_NONBLOCK
,则只读 open 立即返回。但是,如果没有进 程为读而打开一个FIFO,那么只写open将返回−1,并将errno设置成ENXIO
。下表对打开FIFO进行了总结:
对命名管道FIFO来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。
在FIFO中,必须使用一个 open()
函数来显式地建立连接到管道的通道。
一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一 直 “阻塞”,一直到其他进程打开该FIFO 并且向管道中写入数据。
这个阻塞动作反过来也是成立的,如果一 个进程打开一 个管道写入数据,当没有进程冲管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。
不希望在进行命名管道操作的时候发生阻塞,可以在 open()调用中使O_NONBLOCK
标志,以关闭默认的阻塞动作。
不希望多个进程所写的数据交叉,必须考虑原子操作。常量PIPE_BUF
说明了可被原子地写到FIFO的最大数据量。
shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间 临时文件。
客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和 服务器进程二者之间传递数据。
使用FIFO代替两个管道,实现无亲缘关系的客户和服务器,发送文件路径名和内容。
服务器程序:
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
void server(int, int);
int main(int argc, char **argv)
{
int readfd, writefd;
/* 创建两个FIFO;如果它们已经存在,则确定 */
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);
}
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE+1];
/* 从IPC通道读取路径名 */
if ( (n = Read(readfd, buff, MAXLINE)) == 0)
err_quit("end-of-file while reading pathname");
buff[n] = '\0'; /*null终止路径名 */
if ( (fd = open(buff, O_RDONLY)) < 0) {
/* 错误:必须告诉客户 */
snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n",
strerror(errno));
n = strlen(buff);
write(writefd, buff, n);
} else {
/*打开成功:将文件复制到IPC通道*/
while ( (n = read(fd, buff, MAXLINE)) > 0)
write(writefd, buff, n);
close(fd);
}
}
客户端程序:
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
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);
}
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE];
/* 读取路径名 */
Fgets(buff, MAXLINE, stdin);
len = strlen(buff); /* fgets()保证末尾为空字节 */
if (buff[len-1] == '\n')
len--; /* 从fgets()中删除换行符*/
/*写入IPC通道的路径名 */
Write(writefd, buff, len);
/* 从IPC读取,写入标准输出*/
while ( (n = Read(readfd, buff, MAXLINE)) > 0)
Write(STDOUT_FILENO, buff, n);
}
a.让客户端生成字节的FIFO路径名,然后将路径名作为请求消息的一部分传递给服务器。
b.客户端和服务器端约定一个构建客户端FIFO路径名的规则,然后客户端可以将构建自己的路径名所需的相关信息作为请求的一部分发送给服务器。(本例)。
每个客户端FIFO是从一个包含客户端的进程ID的路径名构成的模板(CLIENT_FIFO_TEMPLATE
)中构建的。在生成过程包含进程ID可以很容易地产生一个对各个客户端唯一的名称。
下图展示应用程序如何使用FIFO完成客户端和服务器进程之间的通信。
管道和FIFO中的数据是字节流,消息之间没有边界的。意味着当多条消息被递送到一个进程中时发送者和接收者必须要约定某种构规则来分隔消息。
每条消息使用诸如换行符的分隔字符结束。
在每条消息中包含一个大小固定的头,头中包含一个表示消息长度的字段,该字段指定了消息中剩余部分的长度。
使用固定长度的消息并让服务器总是读取这个大小固定的消息。(本例中)
如下图展示了三种技术,三种技术每条消息的总长度必须小于PIPE_BUF字节。
#include
#include
#include
#include "tlpi_hdr.h"
#define SERVER_FIFO "/tmp/seqnum_sv"
/* 服务器FIFO的知名名称*/
#define CLIENT_FIFO_TEMPLATE "/tmp/seqnum_cl.%ld"
/* 用于生成客户端FIFO名称的模板 */
#define CLIENT_FIFO_NAME_LEN (sizeof(CLIENT_FIFO_TEMPLATE) + 20)
/* 客户端FIFO路径名所需的空间 */
struct request { /*请求(客户端-->服务器) */
pid_t pid; /* 客户端PID */
int seqLen; /* 所需序列的长度 */
};
struct response { /* 响应(服务器-->客户端)*/
int seqNum; /*序列的开始 */
};
使用FIFO的迭代式服务器:
#include
int main(int argc, char *argv[])
{
int serverFd, dummyFd, clientFd;
char clientFifo[CLIENT_FIFO_NAME_LEN];
struct request req;
struct response resp;
int seqNum = 0; /* 服务器*/
/*创建著名的FIFO,并打开它进行读取 */
umask(0); /* 所以我们获得了想要的权限 */
if (mkfifo(SERVER_FIFO, S_IRUSR | S_IWUSR | S_IWGRP) == -1
&& errno != EEXIST)
errExit("mkfifo %s", SERVER_FIFO);
serverFd = open(SERVER_FIFO, O_RDONLY);
if (serverFd == -1)
errExit("open %s", SERVER_FIFO);
/* 打开一个额外的写描述符,这样我们就不会看到EOF*/
dummyFd = open(SERVER_FIFO, O_WRONLY);
if (dummyFd == -1)
errExit("open %s", SERVER_FIFO);
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) errExit("signal");
for (;;) { /* 读取请求并发送响应 */
if (read(serverFd, &req, sizeof(struct request))
!= sizeof(struct request)) {
fprintf(stderr, "Error reading request; discarding\n");
continue; /* 部分读取或错误 */
}
/* 打开客户端FIFO(以前由客户端创建) */
snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,
(long) req.pid);
clientFd = open(clientFifo, O_WRONLY);
if (clientFd == -1) { /* 打开失败,放弃客户端*/
errMsg("open %s", clientFifo);
continue;
}
/*送响应并关闭FIFO*/
resp.seqNum = seqNum;
if (write(clientFd, &resp, sizeof(struct response))
!= sizeof(struct response))
fprintf(stderr, "Error writing to FIFO %s\n", clientFifo);
if (close(clientFd) == -1)
errMsg("close");
seqNum += req.seqLen; /* 更新我们的序列号 */
}
}
客户端:
static char clientFifo[CLIENT_FIFO_NAME_LEN];
///*退出时调用以删除客户端FIFO*/
static void removeFifo(void)
{
unlink(clientFifo);
}
int main(int argc, char *argv[])
{
int serverFd, clientFd;
struct request req;
struct response resp;
if (argc > 1 && strcmp(argv[1], "--help") == 0)
usageErr("%s [seq-len]\n", argv[0]);
/* 创建我们的FIFO(在发送请求之前,避免竞争)*/
umask(0); /* 所以我们获得了想要的权限*/
//创建一个FIFO以从服务器接收响应。
snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,
(long) getpid());
if (mkfifo(clientFifo, S_IRUSR | S_IWUSR | S_IWGRP) == -1
&& errno != EEXIST)
errExit("mkfifo %s", clientFifo);
if (atexit(removeFifo) != 0)
errExit("atexit");
/* 构造请求消息,打开服务器FIFO,并发送消息 */
req.pid = getpid();
req.seqLen = (argc > 1) ? getInt(argv[1], GN_GT_0, "seq-len") : 1;
serverFd = open(SERVER_FIFO, O_WRONLY);
if (serverFd == -1)
errExit("open %s", SERVER_FIFO);
if (write(serverFd, &req, sizeof(struct request)) !=
sizeof(struct request))
fatal("Can't write to server");
/* 打开FIFO,读取并显示响应*/
clientFd = open(clientFifo, O_RDONLY);
if (clientFd == -1)
errExit("open %s", clientFifo);
if (read(clientFd, &resp, sizeof(struct response))
!= sizeof(struct response))
fatal("Can't read response from server");
printf("%d\n", resp.seqNum);
exit(EXIT_SUCCESS);
}
O_NONBLOCK
标记不仅会影响open语义,还会影响write和read函数。
需要修改一个已经打开的FIFO的O_NONBLOCK
标记的状态,具体存在几种场景:
使用O_NONBLOCK
打开一个FIFO但需要让后续的read和write函数调用在阻塞模式下运行。
需要启用从pipe返回的一个文件描述符的非阻塞模式。
需要切换一个文件描述符的O_NONBLOCK
设置的开启和关闭状态。
解决方法:使用fcntl()启用或禁用打开着的文件的O_NONBLOCK
状态标记,例:下面代码启用或禁用这个标记:
//启用这个标记:
int flags;
flags = fcntl(fd,F_GETFL);
flags = O_NONBLOCK;
fcntl(fd,F_SETFL,flags);
//或禁用这个标记:
flags = fcntl(fd,F_GETFL);
flags &=~O_NONBLOCK;
fcntl(fd,F_SETFL,flags);
下表对管道和FIFO上的read函数操作进行了总结,包括O_NONBLOCK
:
只有当没有数据并且写入端没有被打开时阻塞和非阻塞读取之间才存在差别,这时普通的read会被阻塞,而非阻塞read会失败并返回EAGAIN
错误。
当O_NONBLOCK
标记与PIPE_BUF
限制共同起作用时O_NONBLOCK
标记对象管
道或FIFO写入数据的影响会变得复制。
下表对write函数行为的总计:
当数据无法立即被传输时O_NONBLOCK
标记会导致在一个管道或FIFO上的write()失败(错误是EAGAIN)。
当一次写入的数据量超过PIPE_BUF
字节时,该写入操作无需是原子的。
管道进行写入操作的时候,当写入数据的数目小于 128K
时写入是非原子 的,如果把父进程中的两次写入字节数都改为 128K
, 可以发现:写入管道的数据量大于128K
字节时,缓冲区的数据将被连续地写入管道,直到数据全部写完为止,如果没有进程读数据,则一直阻塞。
例如,下面的代码为一个管道读写的例子。在成功建立管道后 ,子进程向管道 中写入数据,父进程从管道中读出数据。子进程一次写入 128K 个字节的数据 ,父进程每次读取10K字节的数据。当父进程没有数据可读的时候退出。
#include
#include
#include
#include
#include
#define K 1024
#define WRITELEN (128*K)
int main(void)
{
int result = -1;/*创建管道结果*/
int fd[2],nbytes;/*文件描述符,字符个数*/
pid_t pid;/*PID值*/
char string[WRITELEN] = "你好,管道";
char readbuffer[10*K];/*读缓冲区*/
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);/*建立管道*/
if(-1== result)
{
printf("建立管道失败\n");
return -1;
}
pid = fork();
if(-1 == pid)
{
printf("fork进程失败\n");
return -1;
}
if(0==pid)
{
int write_size = WRITELEN;
result = 0;
close(*read_fd);/*关闭读端*/
while(write_size >= 0)
{
result = write(*write_fd,string,write_size);
if(result >0)
{
write_size -=result;
printf("写入%d个数据,剩余%d个数据\n",result,write_size);
}
else
{
sleep(10);
}
}
return 0;
}
else/*父进程*/
{
close(*write_fd);
while(1)
{
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
if(nbytes <= 0)
{
printf("没有数据写入了\n");
break;
}
printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
}
}
return 0;
}
管道和FIFO唯一限制:
OPEN_MAX
:一个进程在任意时刻打开的最大描述符数。
PIPE_BUF
:可原子地写往一个管道或FIOF的最大数据量。
问题:当客户端给服务器发送请求,但从来不打开自己的FIFO来读,这时可能恶意的客户可以让服务器处于停顿状态,称为拒绝服务型攻击。
解决方法:特定操作设置超时时钟。