管道

每个进程各自有不同的用户地址空间,都是独立运行,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程把数据从用户空间拷到内核缓冲区,另一个进程再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信
我们常用 ‘|’ 符号来用一个命令来处理另一个命令的显示结果
cat test.c | less
这个过程本质上是创建cat进程传入参数test.c然后这个进程从用户态切换到内核态,将数据拷贝到内核中的缓冲区中,然后less进程切换到内核态取走缓冲区中的数据,返回用户态并处理数据。这个内核中的缓冲区就叫管道
管道的生命周期随进程,进程结束以后,相应的管道也会自动释放。有同步互斥机制、面向字节流的通信、半双工通信

匿名管道

系统调用
int pipe(int pipefd[2]);
fd为输出型参数,传入一个空数组,返回文件描述符数组,fd[0]表示读端fd[1]表示写端,成功返回0,失败返回错误代码-1

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
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

int main() {
int fd[2];
int ret = pipe(fd);
if (ret < 0) {
perror("creat pipe error");
return 1;
}

pid_t id = fork();
if (id < 0) {
perror("fork error");
return 1;
} else if (id == 0) {
// 子进程写数据,关闭读端
close(fd[0]);
while (1) {
char mesg[] = "I am child";
write(fd[1], mesg, strlen(mesg));
printf("child write:%s\n", mesg);
sleep(1);
}
close(fd[1]);
} else {
// 父进程读数据,关闭写端
close(fd[1]);
while (1) {
char mesg[1024];
read(fd[0], mesg, 1023);
printf("father read:%s\n", mesg);
}
waitpid(id, NULL, 0);
close(fd[0]);
}
return 0;
}

gif1
父进程创建fd[2]数组,子进程fork后,fork了fd中的文件描述符,因此他们可以共同访问内核中的缓冲区(管道),也就是说这种方式仅适用与具有亲缘关系的进程
img1

命名管道

由于匿名管道只可用于具亲缘关系的进程间通信,所以我们要实现非亲缘进程间的通信就需要给这个内核中的缓冲区起个名字,使进程可以通过管道名字访问到这个内核中的缓冲区,进而实现进程间通信

可以通过mkfifo命令直接创建管道文件(此文件本质还是内核中的缓冲区)
mkfifo filename
或者通过库函数
int mkfifo(const char *filename,mode_t mode);
第一个参数为所要创建管道的全路径名,第二个参数为创建管道的权限,这里需要注意的是,该值会受到umask值的影响,所以最好先将umask的值设置为000

打开规则

open打开的时候可以通过参数O_NONBLOCK设置非阻塞

若只读打开:
阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK: 若没有进程为写而打开该FIFO,则立即返回成功

若只写打开:
阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK: 若没有进程为读而打开该FIFO,则立刻返回失败,错误码为ENXIO

演示代码

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
// fifo_write.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
const char *file = "./tmp.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if (ret < 0) {
// 如果文件不是因为已经存在而报错,则退出
if (errno != EEXIST) {
perror("mkfifo error");
return -1;
}
}
int fd = open(file, O_WRONLY);
if (fd < 0) {
perror("open error");
return -1;
}
while(1) {
char buf[1024] = {0};
scanf("%s", buf);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
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
// fifo_read.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
const char *file = "./tmp.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if (ret < 0) {
// 如果文件不是因为已经存在而报错,则退出
if (errno != EEXIST) {
perror("mkfifo error");
return -1;
}
}
int fd = open(file, O_RDONLY);
if (fd < 0) {
perror("open error");
return -1;
}
while(1) {
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if (ret < 0) {
perror("read error");
return -1;
} else if (ret == 0) {
printf("write closed\n");
return -1;
}
printf("buf:[%s]\n", buf);
}
close(fd);
return 0;
}

管道读写规则

如果管道所有的写端都关闭,那么read循环读取完数据后返回0
如果写端没全部关闭,但是又不写入数据,则读端一直阻塞等待,等写端写入数据后读取

如果管道所有的读端都关闭,执行write时会产生SIGPIPE信号,导致write进程退出,进程异常终止
如果读端没全部关闭,但是缓冲区已经满了,则写端一直阻塞等待,等有空间在写入

若设置O_NONBLOCK则当管道满时或没有数据可读时返回-1,设置errno值为EAGAIN

当要写入的数据量不大于PIPE_BUF时(ulimit -a查看),linux将保证写入的原子性

那么系统分配的管道大小是多少呢?我们可以通过命令
cat /proc/sys/kernel/msgmax查看

从内核看管道

进程向管道中写数据本质上就是创建一块内核空间并且将这个空间的inode信息放入f_inode中,将f_op设置为对这块内存的操作(读写等)然后将若干信息组织起来形成file结构体,并且将这个结构体的指针添加到进程的文件描述符数组中,这是进程就可以操作这个块内核空间了,另一个进程若想进行通信,就要使用相同的f_inode而f_op可以不同

利用管道实现minishell

https://github.com/Ranjiahao/Linux/tree/master/MiniShell