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

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

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

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

相关文章

python特别篇—github基本操作手册

一、开始使用 1.1 “Hello world” 1.1.1 github介绍 GitHub是一个基于Git版本控制系统的代码托管平台。它提供了一个在线的代码仓库&#xff0c;使开发者可以将自己的代码存储在云端&#xff0c;并与其他开发者进行协作。GitHub不仅仅是一个代码托管平台&#xff0c;还提供了…

HSN:微调预训练ViT用于目标检测和语义分割,华南理工和阿里巴巴联合提出

今天跟大家分享华南理工大学和阿里巴巴联合提出的将ViT模型用于下游任务的高效微调方法HSN&#xff0c;该方法在迁移学习、目标检测、实例分割、语义分割等多个下游任务中表现优秀&#xff0c;性能接近甚至在某些任务上超越全参数微调。 论文标题&#xff1a;Hierarchical Side…

uniapp 微信小程序 vue3.0+TS手写自定义封装步骤条(setup)

uniapp手写自定义步骤条&#xff08;setup&#xff09; 话不多说 先上效果图&#xff1a; setup.vue组件代码&#xff1a; <template><view class"stepBox"><viewclass"stepitem"v-for"(item, index) in stepList":key"i…

Sprint framework Day07:注解结合 xml 配置

前言 Spring注解结合XML配置是指在Spring应用中&#xff0c;使用注解和XML配置的方式来进行Bean的定义、依赖注入和其他配置。这种方式可以充分利用Spring框架的注解和XML配置两种不同的配置方式的特点。 在Spring框架中&#xff0c;我们可以使用注解来定义Bean&#xff0c;如…

《动手学深度学习 Pytorch版》 8.5 循环神经网络的从零开始实现

%matplotlib inline import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab d2l.load_data_time_machine(batch_size, num_steps) # 仍然使用时间机器数据集8.…

VSCode自定义代码块详解

第一步&#xff1a;点击文件-首选项-用户代码片段 第二步&#xff1a;选择代码块作用域的文件类型 类型一&#xff1a;全局作用域 这种类型的代码块是创建在vscode软件内部的文件。是跟随这当前安装的vscode这个软件的&#xff0c;不会随着项目的关闭而失效&#xff0c;会一直存…

Gpt-4多模态功能强势上线,景联文科技多模态数据采集标注服务等您来体验!

就在上个月&#xff0c;OpenAI 宣布对ChatGPT 进行重大更新&#xff0c;该模型不仅能够通过文字输入进行识别和分析&#xff0c;还能够通过语音、图像甚至视频等多种模态的输入来获取、识别、分析和输出信息。这一重要技术突破&#xff0c;将促进多模态自然语言处理的发展&…

Android位置服务和应用权限

Github:https://github.com/MADMAX110/Odometer 一、使用位置服务 之前的Odometer应用是显示一个随机数&#xff0c;现在要使用Android的位置服务返回走过的距离。 修改getDiatance方法使其返回走过的距离&#xff0c;为此要用Android的位置服务。这些服务允许你得到用户的当…

巧用正则表达式

文章目录 题目巧用正则表达式&#xff0c;题目将十进制转为16进制&#xff0c;可以采用Java的语法来表示 题目 巧用正则表达式&#xff0c;题目将十进制转为16进制&#xff0c;可以采用Java的语法来表示 String nInteger.toString(num,16); 那如何确定是否都是字母呢a-f呢&…

车载多源融合定位

终端硬件由两部分组成&#xff0c;组合导航处理板和地磁导航处理板。 组合导航处理板负责采集加速度计、陀螺、GNSS和轮速计等数据进行组合导航解算&#xff0c;差分数据通过6Q主板获取到后通过串口发送至组合导航处理板。地磁导航处理板负责地磁数据采集&#xff0c;保存至数…

Rxjava3 全新详解及常用操作符

简介 RxJava 是一个基于 Java 的响应式编程库&#xff0c;用于处理异步事件流和数据流。它是由 Netflix 开发并开源&#xff0c;现在广泛用于 Android 和 Java 后端开发。RxJava 提供了一种用于组合和处理异步数据的丰富工具集&#xff0c;它的核心思想是将数据流视为一系列事…

微信发红包(各种红包类型)-测试用例设计

微信发红包&#xff08;各种红包类型&#xff09;

总结10.15

项目进展 登陆注册&#xff0c;连接了数据库&#xff0c;找回密码写到了通过给邮箱发送验证码&#xff0c;然后重新输入密码 项目看法 之后俩天加紧把这个登陆注册这些搞完&#xff0c;注册用到的随机生成一个账号且不重复&#xff0c;且设置一个邮箱作为之后找回密码时候的…

CVPR 2023 | 数据驱动的解释对分布外数据具有鲁棒性吗?

论文链接&#xff1a; https://arxiv.org/abs/2303.16390 代码链接&#xff1a; https://github.com/tangli-udel/DRE 01. 研究背景&#xff1a;数据驱动的解释对分布外数据具有鲁棒性吗&#xff1f; 近年来&#xff0c;将黑盒机器学习&#xff08;ML&#xff09;模型用于高风…

CentOS 7 编译安装Boost

1、前提条件 linux平台/CentOS 7 下要编译安装Boost除gcc和gcc-c之外&#xff0c;还需要两个开发库&#xff1a;bzip2-devel 和python-devel &#xff0c;因此在安装前应该先保证这两个库已经安装。 安装指令: yum install bzip2 bzip2-devel bzip2-libs python-devel Cent…

zookeeper源码学习笔记(一)

一、缘起 1、CP还是AP 作为一个在大数据行业工作了7&#xff5e;8年的老兵&#xff0c;在被问到zookeeper和CAP时&#xff0c;竟然有些恍惚&#xff0c;AP还是CP&#xff1f; 看了一些博文&#xff0c;答案几乎都是CP&#xff1f; zookeeper的实现中&#xff0c;P是一定的&…

低代码提速应用开发

低代码介绍 低代码平台是指一种能够帮助企业快速交付业务应用的平台。自2000年以来&#xff0c;低代码市场一直充斥着40大大小小的各种玩家&#xff0c;比如国外的Appian、K2、Pega Systems、Salesforce和Ultimus&#xff0c;国内的H3 BPM。 2015年以后&#xff0c;这个市场更是…

2023年厦门市高等职业院校技能竞赛软件测试竞赛规程

2023年厦门市高等职业院校技能竞赛 软件测试竞赛规程 一、赛项名称 赛项名称&#xff1a;软件测试 竞赛形式&#xff1a;团体赛 赛项专业大类&#xff1a;电子信息 二、竞赛目的 &#xff08;一&#xff09;检验教学成效 本赛项竞赛内容以《国家职业教育改革实施方案》为设计方…

Docker逃逸---procfs文件挂载

一、产生原因 将宿主机/proc目录挂载进了容器&#xff0c;而该目录内的/proc/sys/kernel/core_pattern文件是负责进程奔溃时内存数据转储的&#xff0c;当第一个字符是| 管道符时&#xff0c;后面的部分会以命令行的方式进行解析并运行&#xff0c;攻击者可以将恶意文件写入该…

【Python数据分析工具】

文章目录 概要整体架构流程技术名词解释 概要 数据分析是一种通过收集、处理、分析和解释大量数据&#xff0c;以发现有价值信息、洞察趋势、制定决策并解决问题的过程。在现代科技和互联网的推动下&#xff0c;数据分析变得日益重要。它不仅仅是对数字和图表的简单解释&#…