管道的概念
管道一般用于在shell中将一个进程的输出作为另一个进程的输入。比如
管道是一个字节流,只能按顺序读取任意大小的数据,不能使用fseek()随机读取。
从当前是空的管道内读取数据会阻塞,直到有数据写入管道,如果管道的写入端已关闭,读管道的进程读完管道内剩余的数据后将会读到end of file。
管道的数据传输是单向的。
往管道内写数据到PIPE_BUF个字节前肯定是原子操作。当写入数据大于PIPE_BUF时,内核会把数据分割成任意大小写入管道,当多个进程同时写入时,数据就可能混合。
管道写满后(Linux 2.6.11 后管道大小为65536),write()
会阻塞,直到read()
从管道删除部分数据。
管道可以让两个相关的进程进行通信,相关指的是两个进程有同一个祖先,因为管道的文件描述符是通过pipe()
返回的,通过继承在各个进程中传递。一个普遍的场景是,父进程创建管道,之后兄弟进程利用管道通信。
创建和使用管道
下面是例子,使用pipe()
创建管道后,调用fork()
,在父进程中往管道写数据,在另一端的子进程从管道中读出数据。父进程往管道写完数据后,会关闭管道,子进程在循环读出的过程中碰到EOF就退出循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| int pfd[2]; /* Pipe file descriptors */
char buf[BUF_SIZE];
ssize_t numRead;
if (pipe(pfd) == -1) /* Create the pipe, pfd[0] 读管道, pfd[1] 写管道*/
errExit("pipe");
switch (fork()) {
case -1:
errExit("fork");
case 0: /* Child - reads from pipe */
if (close(pfd[1]) == -1) /* Write end is unused */
errExit("close - child");
for (;;) { /* Read data from pipe, echo on stdout */
numRead = read(pfd[0], buf, BUF_SIZE);
if (numRead == -1)
errExit("read");
if (numRead == 0)
break; /* End-of-file */
if (write(STDOUT_FILENO, buf, numRead) != numRead)
fatal("child - partial/failed write");
}
write(STDOUT_FILENO, "\n", 1);
if (close(pfd[0]) == -1)
errExit("close");
_exit(EXIT_SUCCESS);
default: /* Parent - writes to pipe */
if (close(pfd[0]) == -1) /* Read end is unused */
errExit("close - parent");
if (write(pfd[1], "hello", 5) != 5)
fatal("parent - partial/failed write");
if (close(pfd[1]) == -1) /* Child will see EOF */
errExit("close");
wait(NULL); /* Wait for child to finish */
exit(EXIT_SUCCESS);
}
|
实现shell中的连接符,比如ls | wc -l
这样,主要操作就是将标准输入输出冲定向到管道,使用dup2()
复制文件描述符实现,然后在两端分别关闭重复的文件描述符。
1
2
3
4
| if (pfd[1] != STDOUT_FILENO) {
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
}
|
popen
popen()
用来执行一个shell命令,并且可以通过管道读取其输出或发给它一个输入。
1
2
3
4
5
| #include <stdio.h>
FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);
|
command
为想要执行的命令,mode
为r
或w
。popen()
返回的描述符只能用pclose()
关闭,而不能用fclose()
关闭,因为popen()
在执行过程中会创建两个进程,pclose()
会wait子进程,而使用fclose()
会产生僵尸进程。
管道和stdio缓冲
文件流是块缓冲的,所以stdio对待popen()
返回的管道也是块缓冲的,一般向管道写入时问题不大,我们可以fflush()刷新缓冲区,或者setbuf(fp, NULL)关闭stdio缓冲。但当我们作为管道的读取端时,就不太好控制了,只能看写入端的代码怎么写的了。这种情况可以使用伪终端,在伪终端的情况下,stdio缓冲为行缓冲。
FIFO 命名管道
1
2
3
4
| #include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// mode 指定权限
|
FIFO和管道的类似,只不过FIFO在文件系统中会有一个路径,所以也被叫做命名管道。
一般来说FIFO只会一个读进程和一个写进程,因此,当以只读打开一个FIFO时会阻塞,直到另一端的进程以只写打开FIFO。同样的,以只写打开一个FIFO也会阻塞至另一端以只读打开。
想要在open的时候不阻塞,最好使用O_NONBLOCK
标志。以此标志读FIFO时,若此时没有进程在写端打开,则马上成功结束调用,以此标志写FIFO时,若此时没有进程在对端读,则返回错误ENXIO。
有了O_NONBLOCK
,就可以在一个进程内打开一个FIFO的两端。其次能防止打开两个FIFO的进程发生死锁。
Author
xistor
LastMod
2020-12-01
(7f44e31)
License
CC BY-NC 2.0