书接上回,我们之前学习的文件系统编程都是在内存空间中的文件流(用户态文件缓冲区)内进行操作的,比如使用的fopen、fclose、fread和fwrite等等都是库函数,并没有用到内核态的功能(实际上库函数中调用的是内核态的功能,库函数是内核调用的封装),而库函数间接调用内核功能的话就会造成性能的损失,所以我们考虑直接在内核态调用内核功能,即不带缓冲的文件IO操作。
不带缓冲的文件IO(不带用户态缓冲区,即文件流FILE)
我们直接对在内核内存中的struct file文件对象进行操作,因为它与硬件直接对应。
而在struct file文件对象中有一个缓冲区,这个缓冲区中的内容真正的直接对接硬件,这个缓冲区称为内核文件缓冲区。现在我们对文件进行操作就直接操作这块内核文件缓冲区,逻辑上相当于直接和硬件进行通信。
但是struct file不好管理,因为这是由内核控制的,用户无法随意操作内核的地址空间,这不安全。我们想要找到某个文件对象,但是又不能知道其具体的地址,所以肯定会有一个“中间人”的存在,我们将要操作的请求交给该中间人,由它来帮我们操作内核的地址空间。
可以考虑用一个指针数组,而每一个数组元素就存储了每一个文件对象的首地址,这个时候我们要找到某一个文件对象我们就只需要找到该文件对象在数组中的下标位置即可,并不需要知道它真实的内存地址的值,操作系统内核会处理下标位置到真实内存地址的转换。
即用户用的是一个个整数,而操作系统用的是一个真实的地址。
明显这个整数不能为一个负数,因为数组下标不可能为负数啊。所以这样的非负整数的一个数组下标的集合,就是Linux系统中的文件描述符。
文件描述符
文件描述符形式上是一个非负整数,作用是用来访问某个具体的文件对象。
但是这个整数呢有三个数是已经天生就被用掉了,分别0、1、2:0表示标准输入,1表示标准输出、2表示标准错误。
open系统调用
open 系统调用的简单测试:
测试一下多属性open系统调用传入参数:
open常用属性
read和write读写文件系统调用
read系统调用
read系统调用的一些细节,不过这些在man手册中都是写的比较清楚的:
count应当是申请内存的大小,read之前注意要先清空buf。
write系统调用
简单的测试:
ftruncate系统调用:文件截断
简单测试:
ftruncate系统调用的最大意义就是用来创建一个固定大小的文件。
用在什么地方呢?内存映射mmap。
内存映射mmap
mmap系统调用
简要说明:
对应的测试代码:
lseek系统调用
这个系统调用和之前学的一个fseek很像,其实功能完全一样,只不过操作的位置不一样:
很明显lseek操作的是内核区的文件对象缓冲区,而fseek操作的是目录流里的缓冲区。
dup系统调用:文件描述符的复制
有名管道named pipe/FIFO文件
这是一种进程间通信机制在文件系统中的映射,即操作管道和操作文件是一样的,如用open 打开管道。
传输方式:
1、单工方式:A->B
2、半双工:能从A->B 也能B->A,但是不能同时双向通信
3、全双工:能同时双向通信
而管道至少是半双工通信,不同OS的设计不一样。
使用makefifo创建一个管道:
可以看到创建的管道的大小为0,这意味着管道只能用来通信并不能拿来存数据,所以用任何命令去访问它都是错误的(比如vim)。
删除管道的话就跟普通的删除文件是一样的:
我们现在往里面写一个hello数据:
可以看见此时当前进程被阻塞,因为写进去的数据还没有被读取。
此时我新开一个进程,进行读取操作再返回回来就可以看见原进程就被释放了:
这就是简单的进程间通信。
用系统调用操作管道
之前说过管道本质也就是文件,所以我们可以通过文件的系统调用来操作管道,写一个读程序和一个写程序:
运行时依然可以得到我们之前使用管道命令的效果,值得注意的是当一个进程打开(open)了管道一端的时候,如果对端未被打开,进程就会处于阻塞状态,直到对端被另一个进程打开(并不论读先还是写先)。
那怎么实现全双工通信?用两根管道即可实现(注意可能产生死锁问题)。
我们可以来试一下这个事情,简单写一个两个进程聊天的程序。
实现简易进程聊天程序
首先建立两根管道1.pipe和2.pipe来实现全双工通信:
然后写两个c程序,采用系统调用的方式来实现聊天程序:
chat1.c :
1 #include <43func.h>2 3 int main(int argc,char* argc[]){4 5 //./chat1 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道18 int fdw = open(argv[2],O_WRONLY); //只写方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 while(1){ //永真循环一直维持对话的进行12 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息13 read(fdr,buf,sizeof(buf));//采用read系统调用读取管道1内的信息14 puts(buf); //打印管道1中来自进程2的消息15 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据16 read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中17 write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道2 18 } 19 }
chat2.c:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 5 //./chat2 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道18 int fdr = open(argv[2],O_RDONLY); //只读方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 while(1){ //永真循环一直维持对话的进行12 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息13 read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中14 write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道1 15 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据16 read(fdr,buf,sizeof(buf));//采用read系统调用读取管道2内的信息17 puts(buf); //打印管道1中来自进程2的消息 18 }19 }
现在我们开启两个程序:
我们让chat2发送nihao:
此时可以看见chat1接收到了消息,我们再让chat2发送chilem:
可以发现我们的chat1并没有收到其消息,当我们让chat1回复niyehao后,才能收到chilem的消息:
上面情况的发生也就意味着我们的这个聊天是串行执行的,是一问一答的形式,这是为什么呢?
这其实和我们的代码逻辑有关系,我们代码中的循环执行两个事情,读管道和读标准输入(读标准输入也就是为了写管道),是有先后顺序的,要么先读管道再读输入要么先读输入再读管道,所以一问一答串行执行时并没有问题。但当假设管道一端为程序A连续朝一个管道里面发送多条数据(读输入操作)而另一端B程序没来得及读取该管道消息时就会导致A端的读管道操作被阻塞(因为B端即然没读管道数据自然也就不会执行读输入也就是写管道的操作,那A端的读管道也会因为管道没有数据而被停滞)。
怎么解决?就是让串行变成并行即可,而这个方法就叫IO多路复用,这是现代服务器的基础:
IO多路复用(超级重点)
IO多路复用的思路
我们将上面的串行执行具象话,用投简历的过程来模拟一下:
串行执行:我一开始给百度投简历,然后给阿里投简历,然后给腾讯投简历,然后我必须得等百度的意向,它录取我了或者没录取我拿到结果之后我才能去看阿里对我的结果,这就是串行执行,但很明显这很蠢,因为如果阿里或者腾讯先给我offer了我是不是就可以先跳过百度拿到后两家的offer呢?当然应该是可以的,所以这是串行执行的弊端。
并行执行:此时我们会采用一个中间人或者说用一个小助手,由它来管理我的所有意向消息,即这三家的意向都会发给该小助手,我只要去找小助手就能知道这几家有哪一家给我发意向了没,这样就能变串行被并行,也就是IO多路复用的思路。
IO多路复用的实现:select系统调用
记得使用的时候要加头文件嗷。
我们前面所提到的中间人或者说小助手,就是指的select系统调用中的 fd_set 监听集合。
对于监听集合要使用的话,首先要创建 fd_set,也就是给其分配内存。
第二步是设置合适的监听(也就是要监听的都有哪些公司),相关的函数有上面的fd_zero用来清空监听集合,fd_set用来将某一个会产生阻塞行为的文件描述符加入监听。
第三步调用select函数让进程阻塞,当监听的文件描述符中有任何一个满足条件的时候select才会就绪。
最后使用FD_ISSET函数轮流询问所有监听的文件描述符是否就绪,如果有就绪的话就可以读取该文件描述符,这样读取就不会引发阻塞了。
我们使用上述IO多路复用的知识来改造我们的简易聊天程序:
chat1.c:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 5 //./chat1 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道18 int fdw = open(argv[2],O_WRONLY); //只写方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 fd_set rdset; //设置监听集合,这里是从栈上开辟的内存 12 while(1){ //永真循环一直维持对话的进行13 FD_ZERO(&rdset);//清空监听集合/*为什么需要清空监听集合重新再监听这是由于select函数的设计导致的,在select函数的参数列表中明显其参数并没有用const修饰起来这意味着我们传入的参数和最后函数执行完所返回的参数结果有可能并不一致,所以我们才需要重新设置监听集合*/14 FD_SET(STDIN_FILENO,&rdset); //将标准输入加入监听15 FD_SET(fdr,&rdset); //将管道读操作加入监听16 //第一个参数是固定用法,集合中哪个文件描述符的值大就填其+1的值进去,这表示将所有描述符监听起来17 //第二个参数是填入读监听的监听集合地址18 //第三个参数是填入写监听的监听集合地址,没有就填NULL19 //第四个参数是填入异常监听的监听集合地址,没有就填NULL20 //第五个参数是填超时时间,填NULL表示永久等待,永不超时21 select(fdr+1,&rdset,NULL,NULL,NULL);22 //开始轮询看被监听集合中是否有文件描述符所描述的文件来数据了23 if(FD_ISSET(fdr,&rdset)){//如果管道来数据了 24 puts("msg from pipe");25 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息26 read(fdr,buf,sizeof(buf));//采用read系统调用读取管道1内的信息27 puts(buf); //打印管道1中来自进程2的消息28 }29 if(FD_ISSET(STDIN_FILENO,&rdset)){30 puts("msg from stdin");31 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据32 read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中33 write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道2 34 }35 }36 }
chat2.c:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 5 //./chat2 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道18 int fdr = open(argv[2],O_RDONLY); //只读方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 fd_set rdset; 12 while(1){ //永真循环一直维持对话的进行13 FD_ZERO(&rdset);//清空监听集合14 FD_SET(STDIN_FILENO,&rdset); //将标准输入加入监听15 FD_SET(fdr,&rdset); //将管道读操作加入监听16 //第一个参数是固定用法,集合中哪个文件描述符的值大就填其+1的值进去,这表示将所有描述符监听起来17 //第二个参数是填入读监听的监听集合地址18 //第三个参数是填入写监听的监听集合地址,没有就填NULL19 //第四个参数是填入异常监听的监听集合地址,没有就填NULL20 //第五个参数是填超时时间,填NULL表示永久等待,永不超时21 select(fdr+1,&rdset,NULL,NULL,NULL);22 //开始轮询看被监听集合中是否有文件描述符所描述的文件来数据了23 if(FD_ISSET(fdr,&rdset)){//如果管道来数据了 24 puts("msg from pipe");25 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息26 read(fdr,buf,sizeof(buf));//采用read系统调用读取管道1内的信息27 puts(buf); //打印管道1中来自进程2的消息28 }29 if(FD_ISSET(STDIN_FILENO,&rdset)){30 puts("msg from stdin");31 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据32 read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中33 write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道2 34 }35 }36 }
测试:
现在我们让chat2随意的向chat1发送下消息:
可以看见不会再被阻塞了,让chat1向chat2发消息也一样:
完美解决。
简易进程聊天程序的关闭
关闭程序我们都知道直接ctrl+c即可,但是其实这是有问题的,因为ctrl+c是直接杀死进程,此时一个进程结束而另一个进程还没有,那么就会产生一些离谱的情况,这里我们杀死chat1:
因为图片只能显示静态的东西,实际上下面的进程是一直在重复打印msg from pipe 的。
这意味着一方进程被杀死,另一方进程就陷入了死循环。
另外从图中结合代码也可以看到是因为管道一直就绪导致的死循环,我们来分析一下可能出现死循环的原因:
1、写端先关闭,那么读端read会读到EOF文件终止符(文件终止符有一样算要读的数据嗷),这其实意味着读端是就绪的,那么select也就可以就绪,且管道又没有关掉,那么就陷入死循环喽。
解决办法是读到文件终止符时,我们就终止读取就好了。
另外使用ctrl+c终止进程的方式太过粗暴,我们可以使用ctrl+d,该操作会向标准输入输入一个文件终止符EOF,更推荐这种方式,所以改造后的代码如下:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 5 //./chat1 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道18 int fdw = open(argv[2],O_WRONLY); //只写方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 fd_set rdset; //设置监听集合,这里是从栈上开辟的内存 12 while(1){ //永真循环一直维持对话的进行13 FD_ZERO(&rdset);//清空监听集合/*为什么需要清空监听集合重新再监听这是由于select函数的设计导致的,在select函数的参数列表中明显其参数并没有用const修饰起来这意味着我们传入的参数和最后函数执行完所返回的参数结果有可能并不一致,所以我们才需要重新设置监听集合*/14 FD_SET(STDIN_FILENO,&rdset); //将标准输入加入监听15 FD_SET(fdr,&rdset); //将管道读操作加入监听16 //第一个参数是固定用法,集合中哪个文件描述符的值大就填其+1的值进去,这表示将所有描述符监听起来17 //第二个参数是填入读监听的监听集合地址18 //第三个参数是填入写监听的监听集合地址,没有就填NULL19 //第四个参数是填入异常监听的监听集合地址,没有就填NULL20 //第五个参数是填超时时间,填NULL表示永久等待,永不超时21 select(fdr+1,&rdset,NULL,NULL,NULL);22 //开始轮询看被监听集合中是否有文件描述符所描述的文件来数据了23 if(FD_ISSET(fdr,&rdset)){//如果管道来数据了 24 puts("msg from pipe");25 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息26 int ret = read(fdr,buf,sizeof(buf));//采用read系统调用读取管道1内的信息27 if(ret == 0){ //如果读到文件终止符,我们就终止进程读取28 printf("end!\n"); 29 break;30 }27 puts(buf); //打印管道1中来自进程2的消息28 }29 if(FD_ISSET(STDIN_FILENO,&rdset)){30 puts("msg from stdin");31 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据32 int ret = read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中33 if(ret == 0){ //表示标准输入输入了一个文件终止符write(fdw,"nishigehaoren",13);break;}write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道2 34 }35 }36 }
chat2.c也要改:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 5 //./chat2 1.pipe 2.pipe6 ARGS_CHECK(argc,3); //检查是否为三个命令行参数7 int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道18 int fdr = open(argv[2],O_RDONLY); //只读方式打开管道29 puts("pipe open"); //标识管道打开10 char buf[4096] = {0}; //存储数据的缓冲数组 11 fd_set rdset; 12 while(1){ //永真循环一直维持对话的进行13 FD_ZERO(&rdset);//清空监听集合14 FD_SET(STDIN_FILENO,&rdset); //将标准输入加入监听15 FD_SET(fdr,&rdset); //将管道读操作加入监听16 //第一个参数是固定用法,集合中哪个文件描述符的值大就填其+1的值进去,这表示将所有描述符监听起来17 //第二个参数是填入读监听的监听集合地址18 //第三个参数是填入写监听的监听集合地址,没有就填NULL19 //第四个参数是填入异常监听的监听集合地址,没有就填NULL20 //第五个参数是填超时时间,填NULL表示永久等待,永不超时21 select(fdr+1,&rdset,NULL,NULL,NULL);22 //开始轮询看被监听集合中是否有文件描述符所描述的文件来数据了23 if(FD_ISSET(fdr,&rdset)){//如果管道来数据了 24 puts("msg from pipe");25 memset(buf,0,sizeof(buf));//每次对话结束都刷新一下数组信息26 int ret = read(fdr,buf,sizeof(buf));//采用read系统调用读取管道1内的信息27 if(ret == 0){printf("end!\n");break;}puts(buf); //打印管道1中来自进程2的消息28 }29 if(FD_ISSET(STDIN_FILENO,&rdset)){30 puts("msg from stdin");31 memset(buf,0,sizeof(buf)); //刷新数组信息好接下来使用其来进行写数据32 int ret = read(STDIN_FILENO,buf,sizeof(buf)); //使用read系统调用从标准输入读取控制台上的内容存储进buf数组中33 if(ret == 0){write(fdw,"nishigehaoren",13);break;}write(fdw,buf,strlen(buf)); //使用write系统调用将buf数组中存在的数据写进管道2 34 }35 }36 }
再来测试:
可以看见此时杀掉其中一个进程,另一个就直接结束了,上面是用ctrl+c结束的程序,也可以用ctrl+d,因为我们改写过代码的。
可能出现的异常的情况除了上面说的第一种,还有一种:
2、读端先关闭,写端继续write,此时进程会收到一个SIGPIPE信号然后直接崩溃。
不过这里暂时没什么影响,后面学到网络时会有影响,那个时候再重点说这个崩溃原理。
接下来我们解决会话超时的问题,因为我们有需要让select 不永久的一直轮询等待的情况,这就需要解决其超时所带来的一系列问题。
select的超时
之前提过,在select函数的最后一个参数,其实就是用来设置超时用的,其结构体结构如下:
time_t是秒,suseconds_t是微秒。
我们可以使用select的返回值来区分超时导致的就绪,上图是timeout的简单使用。
关于管道
非阻塞方式操控管道文件
学习过之前的知识我们知道,一个管道一定会关联两个文件,一端读一端写,当我们在一端write的时候,这部分数据会被送到另一端的读缓冲区中等待被读取,但是如果不读取的话那么read缓冲区的数据就会堆积起来,一直堆积到写缓冲区知道写缓冲区被填满再也写不进数据为止:
之前我们都是以阻塞的方式来实现的管道通信,现在我们可以使用非阻塞的方式来尝试通信的操作,用非阻塞方式其实只要指定open 的打开方式为O_RDWR即可,现在我们open不会阻塞所以可以先open一个再open一个,所以现在也就不强调哪一段是读端哪一端是写端了,一般由用户自己去决定哪一端是读哪一端是写,这样我们就可以在一个进程中把管道的两端给打开出来,相当于左手一个对讲机右手一个对讲机自己和自己说话。
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 ARGS_CHECK(argc,2);5 int fdr = open(argv[1],O_RDWR);//非阻塞方式打开人为设定的读端6 int fdw = open(argv[1],O_RDWR);//非阻塞方式打开人为设定的写端7 puts("pipe open");8 char buf[4096];9 int cnt = 0;10 while(1){ 11 printf("cnt = %d\n",cnt++);12 write(fdw,buf,sizeof(buf));13 }14 }
运行结果:
可以看见缓冲区中是可以写入一些数据的,但是如果读端一直不读取数据的话则会阻塞起来造成写阻塞,也就是我们刚刚说的情况。
同理,当缓冲区为空时进行数据的读取就会陷进读阻塞的情况中。
除了上面两种基本的情况,还有几种情况,比如先读管道再写管道,如果管道为空则先读的时候就被阻塞那么写管道自然也被阻塞,就成了永久阻塞,交换顺序先写管道再读管道,如果读的速度比写的速度要快那么数据先被读完了的话读一样会阻塞那写管道也就进行不了,一样会阻塞,如何解决这些种种问题?我们依然可以用IO多路复用的思路来解决。
使用select同时监听读和写
测试代码如下:
1 #include <43func.h>2 3 int main(int argc,char* argv[]){4 ARGS_CHECK(argc,2);5 int fdr = open(argv[1],O_RDWR);//非阻塞方式打开人为设定的读端6 int fdw = open(argv[1],O_RDWR);//非阻塞方式打开人为设定的写端7 puts("pipe open"); 8 char buf[4096];9 fd_set rdset;10 fd_set wrset;11 int cnt = 0;12 while(1){13 FD_ZERO(&rdset);14 FD_SET(fdr,&rdset);15 FD_ZERO(&wrset);16 FD_SET(fdw,&wrset);17 select(fdw+1,&rdset,&wrset,NULL,NULL);18 if(FD_ISSET(fdr,&rdset)){19 printf("read cnt = %d\n",cnt++);20 read(fdr,buf,2048);21 }22 if(FD_ISSET(fdw,&wrset)){23 printf("write cnt = %d\n",cnt++);24 write(fdw,buf,4096);25 }26 sleep(1); 27 }28 }
~
运行结果:
可以看见一开始管道读写缓冲区内没有数据,所以读管道肯定会阻塞那么写管道肯定先执行,即上面先执行write cnt = 0 ;
然后有数据了之后读管道就不会被阻塞了开始读数据,然后二者交替进行。
当进行到一定时间,因为每回写入4096字节却只读取2048个字节,时间长了写空间肯定是会满的,此时写进程就被堵塞,当读取一定空间之后才会继续读,具体表现为一开始是二者交替进行,后面就变成了读的频次要高于写的频次,但确实是可以永久执行下去的。
但如果我们稍微修改一下代码就会出问题,我们将上面的代码的写入管道的数据大小改成4097,理论上不应该会有影响,但实际上会时该程序永久阻塞。
为什么?
首先我们可以使用ulimit -a查看unix系统的所有限定大小的设定,其中就包含pipe管道文件的大小:
从上图的pipe size可以知道管道的读写缓冲区大小为512字节*8等于4096个字节大小,因为select认为写就绪的条件是写缓冲区为空,多空为空,上图已经说过是4096个字节,也就是说当写缓冲区有4096个空字节时说明该缓冲区为空,这个时候才能往里面写入数据。
当我们改成4097时,我们想象如下场景,现在的管道和暂存区全部都填满了,还剩下一个写缓冲区是空的,这个时候select认为写管道应该是就绪的,因为正好有4096个字节,但此时我们往里面写的时候却是写入4097个空间,但是此时多了一个字节写不进去,写不进去就永远阻塞在这里了。
也就是实际上的写入行为和select认为的就绪行为之间有差异造成的问题,所以我们写管道的时候填的数据大小不能超过4096,小于4096是可以的,但是大于4096的话就可能会造成永久阻塞。
select实现原理
我也听的不是很懂,先记录一下老师的笔记等后面学会了再来详细解释一下。
调用select
等后面深入理解了再来填坑,感觉云里雾里的这节。