Linux系统编程:文件描述符以及IO多路复用

news2024/11/30 14:26:03

书接上回,我们之前学习的文件系统编程都是在内存空间中的文件流(用户态文件缓冲区)内进行操作的,比如使用的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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道1
  8     int fdw = open(argv[2],O_WRONLY); //只写方式打开管道2
  9     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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道1
  8     int fdr = open(argv[2],O_RDONLY); //只读方式打开管道2
  9     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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道1
  8     int fdw = open(argv[2],O_WRONLY); //只写方式打开管道2
  9     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         //第三个参数是填入写监听的监听集合地址,没有就填NULL
 19         //第四个参数是填入异常监听的监听集合地址,没有就填NULL
 20         //第五个参数是填超时时间,填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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道1
  8     int fdr = open(argv[2],O_RDONLY); //只读方式打开管道2
  9     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         //第三个参数是填入写监听的监听集合地址,没有就填NULL
 19         //第四个参数是填入异常监听的监听集合地址,没有就填NULL
 20         //第五个参数是填超时时间,填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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdr = open(argv[1],O_RDONLY); //使用open系统调用以只读方式打开管道1
  8     int fdw = open(argv[2],O_WRONLY); //只写方式打开管道2
  9     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         //第三个参数是填入写监听的监听集合地址,没有就填NULL
 19         //第四个参数是填入异常监听的监听集合地址,没有就填NULL
 20         //第五个参数是填超时时间,填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.pipe
  6     ARGS_CHECK(argc,3); //检查是否为三个命令行参数
  7     int fdw = open(argv[1],O_WRONLY); //使用open系统调用以只写方式打开管道1
  8     int fdr = open(argv[2],O_RDONLY); //只读方式打开管道2
  9     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         //第三个参数是填入写监听的监听集合地址,没有就填NULL
 19         //第四个参数是填入异常监听的监听集合地址,没有就填NULL
 20         //第五个参数是填超时时间,填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

在这里插入图片描述
等后面深入理解了再来填坑,感觉云里雾里的这节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1095693.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

hive排序

目录 order by (全局排序asc ,desc) sort by(reduce 内排序) Distribute by(分区排序) Cluster By&#xff08;当 distribute by 和 sorts by 字段相同时 &#xff0c;可以使用 &#xff09; order by (全局排序asc ,desc) INSERT OVERWRITE LOCAL DIRECTORY /home/test2 …

Ubuntu系统下配置安装区块链Hyperledger Fabric(新手小白篇)

有些安装过程比较简单的&#xff0c;不会详细赘述。主要还是集中在Hyperledger Fabric的配置上。 本篇主要介绍在Ubuntu系统上安装Hyperledger Fabric的过程。这里使用的Ubuntu&#xff1a;16.04 LTS。 1. Git安装 Git工具安装命令如下&#xff1a; sudo apt update sudo ap…

基于主动移频法与AFD孤岛检测的单相并网逆变器matlab仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 仿真模型 算法介绍 (1)仿真模型由单相电网、逆变器、滤波环节、PI控制器、PWM生成器、锁相环、AFD控制器s函数、测量模块等构成&#xff1b; (2)采用主动移频法(AFD)进行孤岛检测&#xff1b; (3)相应速度…

MATLAB——径向基神经网络预测程序

欢迎关注公众号“电击小子程高兴的MATLAB小屋” %% 学习目标&#xff1a;径向基神经网络 %% 可以以任意精度逼近任意连续函数 clear all; close all; P1:10; T[2.523 2.434 3.356 4.115 5.834 6.967 7.098 8.315 9.387 9.928]; netnewrbe(P,T,2); %建立精确的径向基…

语料库应用入门讲座

语料库应用入门讲座 引言 主要介绍语料库的概念、功能、意义和基础的方法。主要包括&#xff1a; 1. 什么是语料库&#xff1f;语料库有什么作用&#xff1f; 2. 语料库的分类有哪些&#xff1f; 3. 语料库有什么功能&#xff1f; 4. 常见的语料库工具有哪些&#xff1f; …

Spring源码解析——Spring事务是怎么通过AOP实现的?

正文 此篇文章需要有SpringAOP基础&#xff0c;知道AOP底层原理可以更好的理解Spring的事务处理。最全面的Java面试网站 自定义标签 对于Spring中事务功能的代码分析&#xff0c;我们首先从配置文件开始人手&#xff0c;在配置文件中有这样一个配置&#xff1a;<tx:annot…

vue3-admin-plus框架

拉取 git clone https://github.com/jzfai/vue3-admin-plus.git 安装pnpm npm -g i pnpm7.9.0 node版本&#xff08;&#xff09; 使用nvm改node版本为v16.10.0 v16.10.0 先安装依赖 pnpm install esbuild0.15.18 然后再安装其他依赖 pnpm i 直接安装依赖pnpm i会报错 这个错误…

如何用精准测试来搞垮团队?

测试行业每年会冒出来一些新鲜词&#xff1a;混沌工程、精准测试、AI测试…… 这些新概念、新技术让我们感到很焦虑&#xff0c;逼着自己去学习和了解这些新玩意&#xff0c;担心哪一天被淘汰掉。 以至于给我这样的错觉&#xff0c;当「回归测试」、「精准测试」这两个词摆在一…

【Java 进阶篇】JavaScript 事件详解

在本篇博客中&#xff0c;我们将深入探讨JavaScript事件&#xff0c;这是网页交互的核心。我们将从什么是事件开始&#xff0c;然后逐步介绍事件的类型、如何注册事件、事件处理程序、事件对象以及事件冒泡等相关内容。最终&#xff0c;我们将提供大量的示例代码来帮助您更好地…

【LeetCode】每日一题两数之和寻找正序数组的中位数找出字符串中第一个匹配项的下标在排序数组中查找元素的第一个和最后一个位置

主页点击直达&#xff1a;个人主页 我的小仓库&#xff1a;代码仓库 C语言偷着笑&#xff1a;C语言专栏 数据结构挨打小记&#xff1a;初阶数据结构专栏 Linux被操作记&#xff1a;Linux专栏 LeetCode刷题掉发记&#xff1a;LeetCode刷题 算法&#xff1a;算法专栏 C头…

二、K8S之Pods

Pod 一、概念 K8S作为一个容器编排管理工具&#xff0c;它可以自动化容器部署、容器扩展、容器负载均衡等任务&#xff0c;并提供容器的自愈能力等功能。在Kubernetes中&#xff0c;Pod是最基本的调度单元&#xff0c;它是一组共享存储和网络资源的容器集合&#xff0c;通常是…

Java集合类ArrayList的应用-杨辉三角的前n行

目录 一、题目 杨辉三角 二、题解 三、代码 四、总结 一、题目 题目链接&#xff1a;https://leetcode.cn/problems/pascals-triangle/description/ 杨辉三角 题目描述&#xff1a;给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨…

充换电企业开迈斯低成本提升线上应用稳定性的最佳实践

作者&#xff1a;开迈斯新能源 开迈斯新能源科技有限公司于 2019 年 5 月 16 日成立&#xff0c;目前合资股东分别为大众汽车&#xff08;中国&#xff09;投资有限公司、中国第一汽车股份有限公司、一汽-大众汽车有限公司[增资扩股将在取得适当监督&#xff08;包括反垄断&am…

vh、vw、vmin、vmax

1、分别是什么&#xff1f; vh:指屏幕可见视窗的高&#xff0c; vw:指屏幕可见视窗的宽&#xff0c; vmin:vh和vw之间选较小的值&#xff0c; vmax:vh和vw之间选较大的值。 2、和百分比的区别 百分比时基于父元素的宽高&#xff0c;而vh\vw\vmin\vmax基于屏幕可见视图的宽…

【MySQL JDBC】使用Java连接MySQL数据库

一、什么是JDBC&#xff1f; 理解API的概念 API&#xff1a;Application Programing Interface -- 应用程序编程接口写好一个程序&#xff0c;这个程序需要给别人提供哪些功能&#xff1f;这些功能就是通过一些 函数/类 这样的方式来提供的。例如 Random、Scanner、ArrayList..…

2.Python-用Flask框架创建一个简单的Web程序

怎么安装Flask框架 在终端输入以下命令&#xff1a; pip install flask 验证flask安装&#xff1a; flask --version 编写app.py文件 app文件py如下&#xff1a; #导入flask框架中的两个模块 #Flask允许创建一个Flask应用实例&#xff0c;处理路由、请求和响应等功能 #render…

zookeeper应用场景(二)

单机环境下可以利用jvm级别的锁&#xff0c;比如synchronized、Lock等来实现锁&#xff0c;如果是多机部署就需要一个共享数据存储区域来实现分布式锁 一、分布式锁实现方式 1、基于数据库实现分布式锁 可以用数据库唯一索引来实现 2、基于redis实现分布式锁 redis实现的分…

测试除了点点点,还有哪些内容呢?

今天和一个网友讨论了一下关于互联网行业中测试的情况&#xff0c;希望能够了解现在的互联网行业主要的测试工作内容。小编根据以往的工作经历和经验情况&#xff0c;来做一个总结和整理。 1、岗位分类 现在的岗位划分主要是分为两大类&#xff1a;测试工程师 和 测试开发工程…

1、验证1101序列(Moore)

题目要求&#xff1a; 用Moore型状态机验证1101序列。 题目描述&#xff1a; 用使用状态机验证1101序列&#xff0c;注意&#xff1a;允许重复子序列。如图 端口描述&#xff1a; module moore_1101(input clk,//时钟信号input clr,//reset复位信号&#xff0c;高电平有效in…

Netty 入门 — 亘古不变的Hello World

这篇文章我们正式开始学习 Netty&#xff0c;在入门之前我们还是需要了解什么是 Netty。 什么是 Netty 为什么很多人都推崇 Java boy 去研究 Netty&#xff1f;Netty 这么高大上&#xff0c;它到底是何方神圣&#xff1f; 用官方的话说&#xff1a;Netty 是一款异步的、基于事…