当前位置: 首页 > news >正文

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

书接上回,我们之前学习的文件系统编程都是在内存空间中的文件流(用户态文件缓冲区)内进行操作的,比如使用的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

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

相关文章:

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

书接上回&#xff0c;我们之前学习的文件系统编程都是在内存空间中的文件流&#xff08;用户态文件缓冲区&#xff09;内进行操作的&#xff0c;比如使用的fopen、fclose、fread和fwrite等等都是库函数&#xff0c;并没有用到内核态的功能&#xff08;实际上库函数中调用的是内…...

python基于django的留学生服务管理平台

留学服务管理平台的用户是系统最根本使用者&#xff0c;按需要分析系统包括三类用户&#xff1a;学生、教师、管理员。这三类用户对系统的需求简要如下。技术栈 后端&#xff1a;pythondjango 前端&#xff1a;vueCSSJavaScriptjQueryelementui 开发语言&#xff1a;Python 框架…...

C++ 之 Map

Map map 是C STL中的关联容器&#xff0c; key-Value键值对存储&#xff0c;一对一的映射关系。 内部结构采用的是红黑树&#xff0c; 它会实现对数据的自动排序 &#xff0c;所以map内部所有的数据都是有序的。 #include <map>定义的结构: map<数据类型1, 数据类型…...

MongoDB——centOS7环境Mongodb权限管理(图解版)

目录 一、MongDB权限概述1.1、MongDB权限概述1.2、MongDB权限列表 二、Mongodb权限管理示例2.1、创建账号2.1.1、创建管理员用户2.1.2、开启认证2.1.3、创建普通账号 一、MongDB权限概述 1.1、MongDB权限概述 mongodb是没有默认管理员账号&#xff0c;所以要先添加管理员账号…...

AndroidX项目接入穿山甲广告填坑

本文旨在记录初步接入过程遇到的一点点小问题&#xff0c;更详细的文档还是要参考官方文档。 一、项目版本 GradlePlugin&#xff1a;8.0.2 Gradle&#xff1a;8.0 Kotlin&#xff1a;1.7.20 compileSdk&#xff1a;34 compose-bom:2022.10.00 AS&#xff1a;2022.2.1 …...

汽车电子 - matlab - 用法

汽车电子 - matlab - 用法 面向对象 面向对象 % Man.m classdef Man < handle%UNTITLED2 此处显示有关此类的摘要% 此处显示详细说明properties(Accessprivate) %私有变量scoreendpropertiesageheightendmethodsfunction obj Man(inputArg1,inputArg2)%UNTITLED2 构造此…...

freefilesync文件同步软件

下载 下载链接 https://freefilesync.org/download.php 往下拉&#xff0c;看到下载的链接 下载windows版本 下载地址&#xff1a; https://freefilesync.org/download/FreeFileSync_13.0_Windows_Setup.exe 直接复制到浏览器中访问就能下载 安装 双击安装包&#xff0c;一路默…...

【2023】M1/M2 Mac 导入Flac音频到Pr的终极解决方案

介绍 原作者链接&#xff1a;https://github.com/fnordware/AdobeOgg 很早之前就发现了这个插件&#xff0c;超级好用&#xff0c;在windows上完全没有问题&#xff0c;可惜移植到mac就不行了&#xff08;然后我给作者发了一个Issue&#xff0c;后来就有大佬把m1的编译出来了&…...

C# 图解教程 第5版 —— 第6章 方法

文章目录 6.1 方法的结构6.2 方法体内部的代码执行6.3 局部变量6.3.1 类型推断和 var 关键字6.3.2 嵌套块中的局部变量 6.4 局部常量6.5 控制流6.6 方法调用&#xff08;*&#xff09;6.7 返回值&#xff08;*&#xff09;6.8 返回语句和 void 方法6.9 局部函数6.10 参数&#…...

283 移动零

解题思路&#xff1a; \qquad 适用双指针&#xff0c;l&#xff1a;最左边‘0’元素坐标&#xff1b;r&#xff1a;l右边第一个非零元素坐标。 \qquad 最初的思路&#xff1a;将l和r初始化为0&#xff0c;遍历数组nums若任意一个指针到达数组末尾时停止。若当前nums[l] 0则移…...

maven 编译.../maven-metadata.xml 报错

文章目录 问题解决 问题 突然编译报错: 解决 打开maven的里离线工作模式,感觉就是下载包到本地. 一个是在maven设置里面 或者直接在maven编译的窗口:...

【Rust笔记】Rust与Java交互-JNI模块编写-实践总结

近期工作中有Rust和Java互相调用需求&#xff0c;这篇文章主要介绍如何用Rust通过JNI和Java进行交互&#xff0c;还有记录一下开发过程中遇到的一些坑。 JNI简单来说是一套Java与其他语言互相调用的标准&#xff0c;主要是C语言&#xff0c;官方也提供了基于C的C接口。 既然是C…...

uniapp:幸运大转盘demo

<template><view class"index"><image src"../../static/img/158.png" mode"" class"banner"></image><view class"title">绿色积分加倍卡拿到手软</view><almost-lottery :lottery…...

android 13.0 通过系统自定义服务控制屏幕亮屏和灭屏操作

1.前言 在13.0的产品开发中, 需要提供亮屏和灭屏的接口在8.0以后系统对于屏幕亮灭屏做了限制,直接调用亮屏和灭屏的方法就调不到了,所有就需要通过增加自定义服务的功能,来实现 通过系统服务的方法来调用系统关于控制屏幕亮屏灭屏的相关操作 2.通过系统自定义服务控制屏幕…...

【SQL】新建库表时,报错attempt to write a readonly database

目录 1.问题背景 2.问题原因 3.解决方式 4.结果 windows64位 Navicat sql vscode c 1.问题背景 需求是这样&#xff1a; 希望在调用初始化数据库方法时&#xff0c;查看是否有名为【POCT_PROCESS_CONFIG】的数据库表&#xff0c;如果没有就新建 我的数据库格式是这样 …...

C++ --STL

STL STL&#xff08;Standard Template Library,标准模板库&#xff09;STL从广义上分为&#xff1a; 容器&#xff08;container&#xff09;算法 (algorithm)迭代器 (iterator) 容器 和 算法之间通过迭代器进行无缝连接。STL几乎所有的代码都采用模板类或者模板函数 1、ST…...

一卷到底,大明哥带你横扫 Netty

上一个死磕 Java 专栏【死磕 NIO】(当然写的不是很好&#xff0c;争取今年将它重写一遍)是**【死磕 Netty】**的铺垫&#xff0c;对于我们 Java 程序员而言&#xff0c;我们在实际开发过程一般都不会直接使用 Java NIO 作为我们的网络编程框架&#xff0c;因为写出一套高质量的…...

Python Opencv实践 - 车辆统计(1)读取视频,移除背景,做预处理

示例中的图像的腐蚀、膨胀和闭运算等需要根据具体视频进行实验得到最佳效果。代码仅供参考。 import cv2 as cv import numpy as np#读取视频文件 video cv.VideoCapture("../../SampleVideos/Traffic.mp4") FPS 10 DELAY int(1000 / FPS) kernel cv.getStructu…...

ROS-6.参数的使用

参数的使用 参数服务结构命令行的使用方式运行小海龟命令介绍查看参数获取参数值设置参数保存参数到文件从文件导入参数 通过程序操作创建节点修改cmake编译运行 参数服务结构 ros中存在参数服务管理服务&#xff0c;管理这所有参数&#xff0c;所有节点剋订阅和发布这些节点 …...

机器视觉在自动驾驶汽车中的应用与挑战

机器视觉在自动驾驶汽车中扮演着至关重要的角色&#xff0c;它使车辆能够感知和理解周围环境&#xff0c;以便自主驾驶。以下是机器视觉在自动驾驶汽车中的应用以及相关挑战&#xff1a; 应用&#xff1a; 障碍物检测与避让&#xff1a; 机器视觉系统可以检测和识别路上的障碍…...

欠拟合、过拟合及优化:岭回归

问题:训练数据训练的很好啊,误差也不大,为什么在测试集上面有问题呢? 当算法在某个数据集当中出现这种情况,可能就出现了过拟合现象。 1、 什么是过拟合与欠拟合 欠拟合 过拟合 分析 第一种情况:因为机器学习到的天鹅特征太少了,导致区分标准太粗糙,不能准确识别出天鹅…...

Mybatis学习笔记注解/xml映射/动态SQL%%%Mybatis教程

介绍 Mybatis 是一款优秀的持久层框架&#xff0c;用于简化 JDBC 的开发 MyBatis中文网 Mybatis 入门 快速入门 步骤 创建 SpringBoot 工程、数据库表 user、实体类 User引入 Mybatis 相关依赖&#xff0c;配置 Mybatis&#xff08;数据库连接信息&#xff09;编写 SQL 语…...

Git纯操作版 项目添加和提交、SSH keys添加、远程仓库控制、冲突解决、IDEA连接使用

Git 文章目录 Git项目简单克隆通用操作添加和提交回滚分支变基分支优选 远程项目推送认证抓取、拉取和冲突解决 IEDA类软件连接 最近学原理学的快头秃了&#xff0c;特此想出点不讲原理的纯操作版&#xff0c;不过还是放个图吧 项目简单克隆 git在本人日常中最重要的功能还是…...

使用OpenSSL生成自签证书

什么是OpenSSL OpenSSL是一个开源的软件库和工具套件&#xff0c;用于安全地处理网络数据传输中的加密、解密、安全套接层&#xff08;SSL&#xff09;以及传输层安全&#xff08;TLS&#xff09;协议等功能。它广泛应用于网站和互联网服务中&#xff0c;以确保数据传输的安全…...

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

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

机器人革命:脑洞大开的前沿机器人技术!

原创 | 文 BFT机器人 01 由生物启发的多模式移动形态机器人 在一个不断运动的世界中&#xff0c;一种新开发的名为M4&#xff08;多模式移动形态机器人&#xff09;的机器人展示了在包括滚动、飞行和行走在内的八种不同运动模式之间切换的能力。这款机器人由加州理工学院自主…...

微信小程序动态海报

参考文献&#xff1a; 微信小程序生成分享海报&#xff08;附带二维码生成&#xff09; - 简书 需求背景&#xff1a; 微信小程序固定图片&#xff0c;无法自动链接&#xff0c;分享页面内容 解决方案&#xff1a; 拆分海报内容&#xff0c;由以下几个组成 1、用户图像 …...

手写单例模式

一、单例模式的定义 定义&#xff1a; 确保一个类只有一个实例&#xff0c;并提供该实例的全局访问点。 这样做的好处是&#xff1a;有些实例&#xff0c;全局只需要一个就够了&#xff0c;使用单例模式就可以避免一个全局使用的类&#xff0c;频繁的创建与销毁&#xff0c;耗…...

介绍6种解决电脑找不到vcomp140.dll,无法继续执行代码的方法。

在编程和软件开发领域&#xff0c;我们经常会遇到各种错误和问题。其中&#xff0c;找不到vcomp140.dll文件导致无法继续执行代码是一个非常常见的问题。这个问题可能会影响到软件的正常运行&#xff0c;甚至导致整个项目延期。因此&#xff0c;我们需要找到解决方案来解决这个…...

mysql数据物理迁移

文章目录 一、mysql数据物理迁移1.1 物理迁移 一、mysql数据物理迁移 1.1 物理迁移 速度快&#xff0c;需要停机 进入数据库&#xff0c;查看数据存放位置&#xff1a; select datadir; 一般默认存放在/var/lib/mysql 停机数据库&#xff0c;防止有写入数据 systemctl stop …...