1 管道
作为进程之间的通信手段之一,本质就是内核中的一块有限的缓冲区(64K字节,不同操作系统不一样),而且独立于文件系统,自己构成一个只存在于内存中的文件系统(可追踪内核的pipe.c存在于文件系统目录下)。其特点如下。
1)半双工通信,同一时间数据只能从一端流向另一端;
2)分为匿名管道和有名管道。而且对于匿名管道,只能用于具有共同祖先的进程之间通信,即父子进程或者父孙进程等有关系的进程之间通信,有名管道则没有此限制;
3)填充的是文件描述符数组,其中fd[0] 表示读端,fd[1]表示写端。其在创建子进程前后的关系如下图所示。
2 通用编程方式
2.1 匿名管道
如下表所示,需要注意的就是,父子进程可以关闭管道无用的端口。
1 | //假设用例是子进程通过管道发送给父进程 | 2 | // 子进程需要关闭读端,父进程需要关闭写端 | 3 | int main(int argc, char *argv[]) | 4 | { | 5 | int pipefd[2]; | 6 | if (pipe(pipefd) == -1) | 7 | ERR_EXIT("pipe error"); | 8 | pid_t pid; | 9 | pid = fork(); | 10 | if (pid == -1) | 11 | ERR_EXIT("fork error"); | 12 | if (pid == 0) | 13 | { | 14 | close(pipefd[0]); | 15 | write(pipefd[1], "hello", 5); | 16 | close(pipefd[1]); | 17 | exit(EXIT_SUCCESS); | 18 | } | 19 | close(pipefd[1]); | 20 | char buf[10] = {0}; | 21 | read(pipefd[0], buf, 10); | 22 | printf("buf=%s\n", buf); | 23 | return 0; | 24 | } | |
2.2 有名管道
简单示例,如下表所示。注意打开方式的不同,除了下示的open打开一个已经存在的管道文件外,还可以通过mkfifo函数创建,事实上它也是一个命令。
其用法说明如下。
1)有名管道,是一种特殊的文件类型,即通过ls可以查看到其指定为“p”,除此之外还有六种类型,共计七种,d-目录文件,l-符号链接文件,s-套接字文件,b-块设备文件,c-字符设备文件,“-”表示普通文件。
2) 打开方式有所不同,有可能会阻塞。没错,打开时就阻塞。具体可参见下表,注意区分O_NONBLOCK使能后,对应的错误。
| O_NONBLOCK disable | O_NONBLOCK enable |
READ | 阻塞,直至有程序以写打开该管道 | 返回成功 |
WRITE | 阻塞,直至有程序以读打开该管道 | 返回失败,错误码为ENXIO |
3)示例程序
1 | int main(int argc, char *argv[]) | 2 | { | 3 | int fd; | 4 | /* fd = open("p1", O_RDONLY);*/ | 5 | fd = open("p1", O_RDONLY | O_NONBLOCK); | 6 | | 7 | if (fd == -1) | 8 | ERR_EXIT("open error"); | 9 | | 10 | printf("open succ\n"); | 11 | return 0; | 12 | } | |
2.3 对比
主要体现在两点,一是打开方式,匿名管道不可能会发生阻塞;二是用途,匿名管道不能用于不具备亲缘关系的进程之间通信。
3 读写规则
若管道的写端不存在,继续读该管道时,认为已经读到了末尾,返回0;
若管道的读端不存在,继续写该管道时,返回错误;
[可见系统对写采取0容忍的态度 ]
若管道不存在可读数据,那么在非阻塞模式下返回-1,且errno等于EAGAIN,而在阻塞模式下则会继续阻塞直至有数据;
若管道已经满时,那么在非阻塞模式下同样返回-1,且errno等于EAGAIN,在阻塞模式下则继续阻塞,直至有空间可写;
当写入的数据大于PIPEBUF(在文章一开始处已经介绍了管道本质就是内核的一块缓冲区大小为PIPEBUF 64K),不会保证写入的原子性。如下代码所示。
1 | int main(void) | 2 | { | 3 | char a[TEST_SIZE]; | 4 | char b[TEST_SIZE]; | 5 | char c[TEST_SIZE]; | 6 | | 7 | memset(a, 'A', sizeof(a)); | 8 | memset(b, 'B', sizeof(b)); | 9 | memset(c, 'C', sizeof(c)); | 10 | | 11 | int pipefd[2]; | 12 | | 13 | int ret = pipe(pipefd); | 14 | if (ret == -1) | 15 | ERR_EXIT("pipe error"); | 16 | | 17 | pid_t pid; | 18 | pid = fork(); | 19 | if (pid == 0)//第一个子进程 | 20 | { | 21 | close(pipefd[0]); | 22 | ret = write(pipefd[1], a, sizeof(a)); | 23 | printf("apid=%d write %d bytes to pipe\n", getpid(), ret); | 24 | exit(0); | 25 | } | 26 | | 27 | pid = fork(); | 28 | | 29 | | 30 | if (pid == 0)//第二个子进程 | 31 | { | 32 | close(pipefd[0]); | 33 | ret = write(pipefd[1], b, sizeof(b)); | 34 | printf("bpid=%d write %d bytes to pipe\n", getpid(), ret); | 35 | exit(0); | 36 | } | 37 | | 38 | pid = fork(); | 39 | | 40 | | 41 | if (pid == 0)//第三个子进程 | 42 | { | 43 | close(pipefd[0]); | 44 | ret = write(pipefd[1], c, sizeof(c)); | 45 | printf("bpid=%d write %d bytes to pipe\n", getpid(), ret); | 46 | exit(0); | 47 | } | 48 | | 49 | | 50 | close(pipefd[1]); | 51 | | 52 | sleep(1); | 53 | int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); | 54 | char buf[1024*4] = {0}; | 55 | int n = 1; | 56 | while (1) | 57 | { | 58 | ret = read(pipefd[0], buf, sizeof(buf)); | 59 | if (ret == 0) | 60 | break; | 61 | printf("n=%02d pid=%d read %d bytes from pipe buf[4095]=%c\n", n++, getpid(), ret, buf[4095]); | 62 | write(fd, buf, ret); | 63 | | 64 | } | 65 | return 0; | 66 | } | |
其结果,如下图所示。“A”、“B”、“C”完全是乱序输出的,并不是按顺序,写完A再写B和C的。