目录
什么是IO?
I/O(input/output)即输入和输出,在冯诺依曼体系结构中,将数据从输入设备拷贝到内存就即输入,将数据从内存拷贝到输出设备就即输出

OS如何得知外设中有数据可读取?
输入就是操作系统将数据从外设拷贝到内存的过程,操作系统一定要通过某种方法得知特定外设上是否有数据就绪
注意:CPU不直接和外设交互指的是在数据层面上,而外设其实是可以直接将某些控制信号发送给CPU中的某些控制器的。
OS如何处理从网卡中读取到的数据包?
操作系统任何时刻都可能会收到大量的数据包,因此操作系统须将这些数据包管理起来。所谓的管理即"先描述,再组织",在内核中有一个结构sk_buff,该结构就是用来管理和控制接收或发送数据包的信息
简化版的sk_buff结构:

当操作系统从网卡中读取到一个数据包后,会将该数据依次交给链路层、网络层、传输层、应用层进行解包和分用,最终将数据包中的数据交给了上层用户,那对应到这个sk_buff结构来说具体是如何进行数据包的解包和分用的呢?

发送数据时对数据进行封装也是同样的道理,即依次在数据前面拷贝上对应的报头。应用层以下,数据包在进行封装和解包的过程中,本质数据的存储位置是没有发生变化的,实际只是在用不同的指针对数据进行操作
但内核中的sk_buff并不像上面那样简单:
什么是高效的IO?
IO主要分为两步:
任何IO的过程,都包含"等"和"拷贝"这两个步骤,但在实际的应用场景中"等"消耗的时间往往比"拷贝"消耗的时间多,因此要让IO变得高效,最核心的办法就是尽量减少"等"的时间
IO的过程其实和钓鱼是非常类似的
下面给出五个人的钓鱼方式:
张三、李四、王五的钓鱼效率是否一样?为什么?
张三、李四、王五的钓鱼效率本质上是一样的
因此张三、李四、王五三个人的钓鱼的效率是一样的,只是等鱼上钩的方式不同而已,张三是死等,李四是定期检测浮漂,而王五是通过铃铛来判断是否有鱼上钩
问题是钓鱼效率是否是一样的,而不是问整体谁做的事最多,若说整体做事情的量的话,那一定是王五做得最多,李四次之,张三最少
张三、李四、王五它们三个人分别和赵六比较,谁的钓鱼效率更高?
赵六是四个人中钓鱼效率最高的,因为赵六同时在等多个鱼竿上有鱼上钩,因此在单位时间内,赵六的鱼竿有鱼上钩的概率是最大的
而高效的钓鱼就是要减少单位时间内"等"的时间,增加"拷贝"的时间,所以说赵六的钓鱼效率是这四个人中最高的
赵六的钓鱼效率之所以高,是因为赵六一次等待多个鱼竿上的鱼上钩,可以将"等"的时间进行重叠
如何看待田七的这种钓鱼方式?
田七让司机帮自己钓鱼,自己开车去做其他事情去了,此时这个司机具体怎么钓鱼已经不重要了,他可以模仿张三、李四、王五、赵六任何一个人的钓鱼方式进行钓鱼
最重要的是田七本人并没有参与整个钓鱼的过程,只是发起了钓鱼的任务,而真正钓鱼的是司机,田七在司机钓鱼期间可能在做任何其他事情,若将钓鱼看作是一种IO的话,那田七的这种钓鱼方式即异步IO
而对于张三、李四、王五、赵六而言,都需要自己等鱼上钩,当鱼上钩后又需要自己把鱼从河里钓上来,对应到IO中就是需要自行进行数据的拷贝,因此他们四个人的钓鱼方式即同步IO
五种IO模型
实际这五个人的钓鱼方式分别对应的就是五种IO模型。
通过这里的钓鱼例子可以看到发现,阻塞IO、非阻塞IO和信号驱动IO本质上是不能提高IO的效率的,但非阻塞IO和信号驱动IO能提高整体做事的效率
其中,这个钓鱼场景中的各个事物都能与IO中的相关概念对应起来,比如这里钓鱼的河对应就是内核,这里每一个人都是进程或线程,鱼竿对应的就是文件描述符或套接字,装鱼的桶对应的就是用户缓冲区
阻塞IO即在内核将数据准备好前,系统调用会一直等待

阻塞IO是最常见的IO模型,套接字默认都是阻塞方式
以阻塞方式进行IO操作的进程或线程,在"等"和"拷贝"期间都不会返回,在用户看来就是阻塞了,因此被称为阻塞IO
非阻塞IO:若内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码

非阻塞IO需要程序员以循环的方式反复尝试读写文件描述符,即轮询,这对CPU是较大的浪费,一般只有特定场景下使用
阻塞IO和非阻塞IO的区别在于,阻塞IO当数据没有就绪时,后续检测数据是否就绪的工作是由操作系统发起的,而非阻塞IO当数据没有就绪时,后续检测数据是否就绪的工作是由用户发起的
信号驱动IO:当内核将数据准备好后,使用SIGIO信号通知应用程序进行IO操作

当底层数据就绪的时候会向当前进程或线程递交SIGIO信号,因此可以通过signal或sigaction函数将SIGIO的信号处理程序自定义为需要进行的IO操作,当底层数据就绪时就会自动执行对应的IO操作
信号的产生是异步的,但信号驱动IO是同步IO的一种
判断一个IO过程是同步的还是异步的,本质就是看当前进程或线程是否需要参与IO过程,若参与即为同步IO,否则为异步IO
IO多路转接也被称为IO多路复用,能够同时等待多个文件描述符的就绪状态

IO多路转接的思想:
IO多路转接就像是帮人排队的黄牛,因为多路转接接口实际并没有进行数据拷贝。排队黄牛可以一次帮多个人排队,此时就将多个人排队的时间进行了重叠
异步IO:由内核在数据拷贝完成时,通知应用程序

同步和异步关注的是消息通信机制
为什么非阻塞IO在没有得到结果之前就返回了?
同步通信、同步与互斥
在多进程和多线程中有同步与互斥的概念,但是这里的同步通信和进程或线程之间的同步是不相干的概念
因此当看到"同步"这个词的时候,需先明确这个"同步"是同步通信的同步,还是同步与互斥的同步
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态
非阻塞IO,记录锁,系统V流机制,I/O多路转接(I/O多路复用),readv和writev函数以及存储映射IO(mmap),统称为高级IO
系统中大部分的接口都是阻塞式接口,如使用read函数从标准输入中读取数据
- #include
- #include
- #include
- using namespace std;
-
- int main()
- {
- char buffer[1024];
- while (true){
- ssize_t size = read(0, buffer, sizeof(buffer)-1);
- if (size < 0){
- cerr << "read error" << endl;
- break;
- }
- else {
- buffer[size] = '\0';
- cout << "echo# " << buffer << endl;
- }
- }
- return 0;
- }
程序运行后,若不进行输入操作,该进程就会阻塞,根本原因就是因为此时底层数据不就绪,因此read函数需进行阻塞等待

一旦进行输入操作,此时read函数就会检测到底层数据就绪,然后将数据读取到从内核拷贝到程序员传入的buffer数组中,并且将读取到的数据输出到显示器上面,最后就看到了输入的字符串

打开文件时默认都是以阻塞的方式打开的,若要以非阻塞的方式打开某个文件,需在使用open函数打开文件时携带O_NONBLOCK或O_NDELAY选项,此时就能够以非阻塞的方式打开文件

fcntl函数
int fcntl(int fd, int cmd, ... /* arg */);
参数说明:
fcntl函数常用的5种功能与其对应的cmd取值如下:
返回值说明:
实现SetNonBlock函数
可定义一个函数,该函数用于将指定的文件描述符设置为非阻塞状态
- #include
- #include
- #include
- #include
- using namespace std;
-
- bool SetNonBloack(int fd)
- {
- int fl = fcntl(fd, F_GETFL);//获取该fd对应的文件读写标志位
- if(fl < 0) return false;
- fcntl(fd, F_SETFL, fl | O_NONBLOCK);//设置非阻塞
- return true;
- }
-
- int main()
- {
- SetNonBloack(0);
- char buffer[1024];
- while(true)
- {
- sleep(1);
- errno = 0;
- ssize_t size = read(0,buffer,sizeof(buffer) - 1);
- if(size > 0) {
- buffer[size - 1] = 0;
- cout << "echo# " << buffer << " errno[success]:" << errno << " errString:" << strerror(errno) <
- }
- else {
- if(errno == EWOULDBLOCK || errno == EAGAIN) {
- cout << "当前0号fd数据没有就绪,请待会再进行尝试" << endl;
- continue;
- }
- else if(errno == EINTR) {
- cout << "当前IO可能被信号中断,请再次尝试" << endl;
- continue;
- }
- else {//差错处理
- cout << "read error" << " errno:" << errno << " errString:" << strerror(errno) <
- }
- }
- }
- return 0;
- }
- 当read函数以非阻塞方式读取标准输入时,若底层数据不就绪,那么read函数就会立即返回,并且是以出错的形式返回的,此时的错误码会被设置为 EAGAIN 或 EWOULDBLOCK
- 因此在以非阻塞方式读取数据时,若调用read函数时得到的返回值是-1,此时还需通过错误码进一步进行判断,若错误码的值是EAGAIN或EWOULDBLOCK,说明本次调用read函数出错是因为底层数据还没有就绪,因此后续还应该继续调用read函数进行轮询检测数据是否就绪,当数据继续时再进行数据的读取
- 调用read函数在读取到数据前可能被其他信号中断,此时read函数也会以出错的形式返回,此时的错误码会被设置为EINTR,应重新执行read函数进行数据的读取
因此在以非阻塞的方式读取数据时,若调用read函数读取到的返回值为-1,此时并不应该直接认为read函数在底层读取数据时出错,而应该继续判断错误码,若错误码的值为EAGAIN、EWOULDBLOCK 或 EINTR则应该继续调用read函数再次进行读取
-
相关阅读:
攻防世界题目练习——Web引导模式(四)(持续更新)
第一章 计算机系统体系结构
Docker 安装mysql8.x
纽约联储测试CBDC以改善MogaFX外汇交易
csdn,是时候说再见!
如何用度量数据驱动代码评审的改善
一个 println 竟然比 volatile 还好使?
idea提交git项目,提交代码 点击commit一闪而过,没有反应的解决办法
java的Exception.getMessage为null
Day10力扣打卡
-
原文地址:https://blog.csdn.net/GG_Bruse/article/details/130817818