读写两端的文件描述符初始的属性是阻塞属性,所以如果我们希望匿名管道的读写两端为非阻塞属性的时候,就需要自己设置非阻塞属性,此时需要用到fcntl函数。
fcntl函数原型:
int fcntl(int fd, int cmd, … /* arg */ );
参数:可变参数列表
fd
:待要操作的文件描述符,对匿名管道来说,就是管道的读写两端的文件描述符
cmd
:告诉fcntl函数做什么操作,有两个可选项:
F_GETFL
:获取
文件描述符的属性信息F_SETFL
:设置
文件描述符的属性信息,要设置的新的属性放到可变参数列表当中。要注意设置属性的时候是直接替换
原有属性,并不是在原有属性上面追加属性,设置多个属性的时候,用按位或
的方式连接各属性。返回值:
我们创建一个管道来看一下管道两端读写文件描述符的属性信息:
#include
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 //创建管道
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0){
9 perror("pipe");
10 return 0;
11 }
12
13 int flag = fcntl(fd[0],F_GETFL);
14 printf("read: flag = %d.\n",flag);
15
16
17 flag = fcntl(fd[1],F_GETFL);
18 printf("write: flag = %d.\n",flag);
19 return 0;
20 }
~
执行结果:
[jxy@VM-4-2-centos nonblock]$ ./nonblock
read: flag = 0.
write: flag = 1.
在上述代码的基础上,我们设置管道读端文件描述符为非阻塞属性:
#include
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 //创建管道
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0){
9 perror("pipe");
10 return 0;
11 }
12 //获取读端文件描述符的属性信息
13 int flag = fcntl(fd[0],F_GETFL);
14 printf("before flag = %d.\n",flag);
15 //设置fd[0]的文件描述符为非阻塞属性
16 ret = fcntl(fd[0],F_SETFL,flag | O_NONBLOCK);
17 //获取此时的读端文件描述符的属性信息
18 flag = fcntl(fd[0],F_GETFL);
19 printf("after flag:%d.\n",flag);
20 return 0;
21 }
执行结果:
[jxy@VM-4-2-centos nonblock]$ ./nonblock
before flag = 0.
after flag:2048.
上述两端代码的运行结果不难看出,读端文件描述符的属性为0,写端文件描述符的属性为1,那为什么属性是0,为什么是1呢?还有为什么flag |和O_NONBLOCK之间要用或来连接呢?
我们在操作系统内核中查找O_NONBLOCK,可以看出,它的值转化为十进制就是2048.
#define O_NONBLOCK 00004000
//是一个八进制数字
读端文件描述符的属性信息是0,代表着O_RDONLY;
写端文件描述符的属性信息的1,代表着O_WRONLY.
那为什么文件描述符的属性信息需要用按位或的方式进行设置呢?
因为文件描述符的属性信息在操作系统内核当中是用比特位表示的。
我们以下面这个为例:
flag的值是0,O_NONBLOCK的值是2048
flag | O_NONBLOCK
按位或得出的结果是2048。
操作系统内核当中大量的在使用位图
,有两个明显的优点就是:位操作快、节省内存空间。
系统接口当中,文件打开方式的宏,在内核当中的使用方式是位图,比如O_RDONLY、O_CREAT、O_WRONLY等等。
看到这里,可能会有疑问,为什么文件描述符会有属性呢?它不是一个数字吗?
我们在上一篇提到,文件描述符是指针数组的下标,而且该指针数组存放的指针是指向的是文件的结构体,我们可以把图再拿来看看。
所以文件描述符的属性其实是指的是struct file{…}里面存储的文件的一些属性,比如该文件是否可读,是否可执行,是否可写等等。
一共有下面这四种情况:
首先就是要创建管道,其次再创建子进程,再设置匿名管道读端文件描述符为非阻塞属性,父进程进行读,子进程进行写。
此时我们需要关心的就是父进程的读端和子进程的写端,所以我们首先需要把不需要的文件描述符的端口关掉,也就是把父进程的写端和子进程的读端关闭了。
我们让子进程一直不退出,模拟出子进程的写端一直打开。
#include
#include
#include
int main(){
//创建管道
int fd[2];
int ret = pipe(fd);
if(ret<0){
perror("pipe");
return 0;
}
//创建子进程
ret = fork();
if(ret < 0){
perror("fork");
return 0;
}else if(ret == 0){
//child
close(fd[0]);
}else{
//father
close(fd[1]);
}
return 0;
}
执行结果:
[jxy@VM-4-2-centos pipe_nonblock]$ ./nonblock
read: Resource temporarily unavailable
r_size:-1.
执行完需要注意的是,父进程退出之后子进程依然在sleep中,子进程就变成了孤儿进程,需要kill掉。
写不关闭,一直读,读端调用read函数之后,返回值为-1,error设置为EAGAIN。
在上述代码的基础上,将子进程的写端关闭,再让父进程去读,为了保证父进程去读的时候,子进程的写端一定是关闭的,所以在进入父进程之后,我们让父进程先sleep一秒。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 //创建管道
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret<0){
9 perror("pipe");
10 return 0;
11 }
12
13 //创建子进程
14 ret = fork();
15 if(ret < 0){
16 perror("fork");
17 return 0;
18 }else if(ret == 0){
19 //child
20 close(fd[0]);
21 close(fd[1]);
22 while(1){
23 sleep(1);
24 }
25 }else{
26 //father
27 sleep(1);
28 close(fd[1]);
29 int flag = fcntl(fd[0],F_GETFL);
30 fcntl(fd[0],F_SETFL,flag | O_NONBLOCK);
31 char buf[1024] = {0};
32 ssize_t r_size = read(fd[0],buf,sizeof(buf)-1);
33 perror("read");
W> 34 printf("r_size:%d.\n",r_size);
35 }
36 return 0;
37 }
执行结果:
[jxy@VM-4-2-centos pipe_nonblock]$ ./nonblock
read: Success
r_size:0.
此时,管道没有进程去写入了,相当于为空,父进程再去读,就什么也读不到了。
写关闭,一直读,父进程读端read函数返回0,表示什么也没有读到。
我们设置父进程进行读,子进程进行写,首先和上面一样,关闭父进程的写端和子进程的读端,之后子进程以自己进行写,父进程一直进行读。
代码如下:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 int fd[2];
6 int ret = pipe(fd);
7 if(ret < 0){
8 perror("pipe");
9 return 0;
10 }
11
12 ret = fork();
13 if(ret < 0){
14 perror("fork");
15 return 0;
16 }else if(ret == 0){
17 //child 写
18 close(fd[0]);
19 int flag = fcntl(fd[1],F_GETFL);
20 fcntl(fd[1],F_SETFL,flag | O_NONBLOCK);
21 int count = 0;
22 while(1){
23 int ret = write(fd[1],"a",1);
24 if(ret < 0){
25 perror("write");
26 break;
27 }
28 count++;
29 printf("count=%d\n",count);
30 }
31 }else{
32 //father 读
33 close(fd[1]);
34 while(1){
35 sleep(1);
36 //模拟一直读
37 }
38 }
39 return 0;
40 }
执行结果:
读不关闭,一直写,当把管道写满之后,则在调用write,就会返回-1
我们设置父进程进行读,子进程进行写,然后再关闭父进程的读端,此时父子进程的读端就全部关闭了。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 int fd[2];
6 int ret = pipe(fd);
7 if(ret < 0){
8 perror("pipe");
9 return 0;
10 }
11
12 ret = fork();
13 if(ret < 0){
14 perror("fork");
15 return 0;
16 }else if(ret == 0){
17 //child 写
18 sleep(1);
19 close(fd[0]);
20 int flag = fcntl(fd[1],F_GETFL);
21 fcntl(fd[1],F_SETFL,flag | O_NONBLOCK);
22 int count = 0;
23 while(1){
24 int ret = write(fd[1],"a",1);
25 if(ret < 0){
26 perror("write");
27 break;
28 }
29 count++;
30 printf("count=%d\n",count);
31 }
32 }else{
33 //father 读
34 close(fd[1]);
35 close(fd[0]);
36 while(1){
37 sleep(1);
38 }
39 }
40 return 0;
41 }
执行程序后,我们发现,子进程变成了僵尸进程。
[jxy@VM-4-2-centos nonblock]$ ps aux | grep nonblock
jxy 17875 0.0 0.0 4212 352 pts/1 S+ 16:51 0:00 ./nonblock
jxy 17876 0.0 0.0 0 0 pts/1 Z+ 16:51 0:00 [nonblock] <defunct>
jxy 17923 0.0 0.0 112816 984 pts/2 S+ 16:51 0:00 grep --color=auto nonblock
管道读端关闭,但一直往管道中,写端调用write进行写的时候,就会发生崩溃,本质上是因为读端关闭,写端的进程就会收到SIGPIPE信号,导致写端进程崩溃,就和水管是一样的,堵住一头,再想往水管里面一直注水,那水管就会破裂了。