文章目录
IO的概念
在网络中通过网课设备向另外一段的输入设备进行输入,
IO=我们一定会因为一些条件而无法立即发送/无法立即接收(等也是IO的基本环节)+拷贝数据
高效的IO=拷贝数据(不等待),减少单位时间内等待的比重
IO 的话题
- 改变等待的方式
- 减少等的比重
但是操作系统是怎么知道当前网卡当中是有数据:
- 操作系统定期的轮询,
- 当中数据来到的时候,通过驱动提醒操作系统
我们的计算机是通过中断程序来做到的,8259,中断组件来实现的,等到外来设备来的时候,就会像CPU里面直接发送消息(数据不会发送,只是通过控制信号可以直接通知CPU)
CPU通过中断向量表,通过其中的方法进行对应的操作,而当网卡有数据,就是通知CPU去将数据从网卡里面 搬运到内存里面
中断.中断向量 中断向量表
首先我们先介绍一下中断
中断:所谓的中断就是指CPU 在正常运行程序的过程中由于内部/外部的事件的触发或因为程序预先的安排,引起CPU暂时的中断当前正在运行的程序,而转而去执行内部/外部事件,或者程序预先安排的事件的服务子程序,待中断服务子程序执行完毕之后,CPU 在返回被暂时中断的地方,(CPU接收到了信号暂时离开执行其他事情,完了之后再回来执行原本要执行的事情)
中断向量:中断服务程序的入口地址
中断向量表:把系统中所有的中断类型码及其对应的中断向量按照一定规律存放在一个区域内,这个区域就叫做中断向量表
底层数据到达时操作系统做了啥:
底层网卡有数据到达时候,此时硬中断通知操作系统,操作系统生成软中断对数据进行拷贝工作
硬中断 软中断
软中断
- 编程时出现的异常称为软中断(中断指令发出的)
- 软中断是通信之间用来模拟硬中断的一种信号通信方式
- 中断源发出中断请求或中断信号后,CPU或接收进程在适当的时机再自动进行中断处理或者完成软中断信号对应的功能
- 软中断是软件实现的中断,也就是程序运行时其他程序对他的中断,而硬中断时硬件实现的中断,是程序运行时设备对他的中断
硬中断
- 硬中断是外部事件引起的,具有随机性和突发性,
- 硬中断的中断响应周期,cpu需要发出中断回合信号,软中断的中断响应周期,不需要发送中断回合信号
- 硬中断是可以被屏蔽的,软中断无法被屏蔽掉
- 硬中断是的中断信号由中断控制器提供的,软中断的中断信号是直接发送的,无需用到中断控制器
中断
通常引入中断都是外设,系统当中数据准备就绪,需要拷贝到内存里面,需要硬件层面上的中断来完成
waitpid并没有使用中断
:本身就是一个软件,父子进程有直接的关系,子进程在退出的时候,可以根据PCB 找到父进程
高级IO为何高效
谈及高级IO为何高效,来讨论一下为何read和write低效
在之前的网络套接字代码编写的时候,我们可能使用的是read和write进行操作,使用这个还是的时候,我们在写入或者读取的时候进程都是阻塞的,此时这个就称为IO,尤其是在套接字的场景当中读取数据时候,不知道会被阻塞多久,一个进程没读完就不能进行其他的操作,我们因为只等待一个文件描述符,就在那里一直等待,所以就非常的低效率
如:read,write,recv,send,recvfrom,sendto,fopen,fread cin,cout,scanf,printf
高级IO的本质
因为调用了select poll,epoll进行等待的时候就可以等待批量的文件描述符,等待的事件重叠了,一次可以处理多个事情,所以就高效了
五种IO模型
我们使用一个例子来讲解五种IO模型
现在我们将钓鱼简化,过程可以分为等和钓两个步骤
钓鱼大佬们是怎么提高效率调到更多的鱼呢?
看看下面5个人的做法,谁最有可能调到最多的鱼儿
张三:阻塞式的钓鱼
李四:一边玩手机,时不时看看鱼鳔
王五:鱼竿带一个铃铛,忙自己的事情,等到铃铛响的时候再去钓鱼
赵六:用一大堆鱼竿,在岸边进行轮询检测,只要有一个鱼咬住钩就行了
田七:派人去钓鱼,调到了鱼就给田七打电话,钓鱼的桶就相当于一个缓冲区
答案是:赵六
- 张三的做法就是
阻塞
:类似我们之前调用recv,flags设置为0,直接就是阻塞式的等待,进程放入文件描述符中的等待队列,状态被设置为!R,一直到底部有数据,且高于低水位线,操作系统才将进程的状态设置为R,放入到运行队列中,等+拷贝 - 李四的做法是
非阻塞的检测轮询检测
,这个对CPU的消耗比较大,改变等待方式 - 王五的做法是
信号驱动IO
,当有IO 的时候就会发送SIGIO,这个做法很高效,王五只需要做自己的事情,在有信号时,才去处理,但是信号并不是当前立即就去处理,29号信号是一个普通信号,可能会发送很多次,但是王五只处理一次,所以在比较小型的同行才会使用 - 赵六:
多路转接式钓鱼
,每个鱼竿有反应的概率相同的情况,增加鱼竿的数目能让赵六将等待的事件叠加起来,很高效(select poll),鱼竿==fd - 田七:
异步IO
的思想,叫别人来帮忙钓鱼,不关系这个人怎么调到鱼的,但是帮助钓鱼的人可以采用前面的那些人的方法
高效IO 的本质
低效IO:在等待过程特别长的通信过程中,让等的时间占比特别高,此时IO大部分的时间都是在等待,所以效率就会很低,
高效IO:在一次IO中让等待的时间占比小,recv读不是立马有数据就去读取的,还要等数据超过低水位线,或者对端给我发送PSH字段才会通知上层将数据从内核拷贝数据到用户
我们的做法就是让一个中介,这个中介去执行等待,服务器只对其负责,当有客户端发起连接时,先告诉中介,中介将消息告诉服务器,服务器再执行操作,所以就不需要服务器去做无意义的等待,只要让这个中介去等待就行了
如何使用信号驱动IO
操作系统收到数据的时候,会给进程发送SIGIO,默认处理动作是忽略,我们可以注册SIGIO 的处理方法,当底层好了给我们发送信号,我们再一次调用read接收就可以了,简单IO用的多,复杂IO用的少,因为它是普通信号,只会记录一个信号,信号可能会丢失
同步IO vs 异步IO
同步IO和异步IO 的本质就是在数据拷贝的过程中,异步IO不关心数据的拷贝,只提供一个缓冲区,让OS在合适的时候拷贝到缓冲区里面,即拷贝的过程都是由OS来完成的,数据就绪的通知方案是由信号所决定的
类比:
家里来了客人,母亲在做菜,此时我是负责端菜的,我可以以同步IO的方式进行端菜,但时对于客人来说,不需要帮忙,对于客人来说就是异步的
总结
:同步IO需要用户在调用recv/read将数据拷贝到缓冲区,但是异步IO需要用户提前告知缓冲区,OS会选择合适的时机进行拷贝数据
为什么是内核收到数据
因为协议栈是自底向上收的,所以操作系统先收到,硬件,驱动,操作系统,用户,这个问题虽然简单,但是很关键,通常是我们硬件设备网卡检测到有数据的时候,然后中断拷贝到系统的缓冲区里面
多路IO转接高效的原因
如select,select的时候阻塞等待了多个文件描述符就绪,只要有一个准备好了,就会调用recv将数据读取上来,recv一次只能等待一个,因为它的参数只有一个,但是select后能保证recv不会被阻塞住了,
select只负责等待的环节,后续的recv就不会被阻塞了,因为数据已经就绪了,
但是细节还有很多,比如数据就绪我能够疯狂进行读取吗,或者我能读取一部分就不读了吗,下一次它还会送来给我读取吗
其
阻塞vs非阻塞
- 但进程阻塞接口直观看到进程卡住了,等待着某个事件就绪
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
fcntl
fcntl函数主要是用来操作文件描述符的
一个文件描述符,默认都是阻塞IO,fcntl可以让文件描述符为非阻塞的
但是骑士除了fcntl的方式,还有好几种方法,如open的时候,可以设置第二个参数为O_NONBLOCK,可以让打开的文件描述符就是非阻塞的,或者调用recv等接口的时候,设置flags 为O_NONBLOCK
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd值不同,后面追加的参数也不一样,fcntl函数有5个功能
复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
接口测试:
测试代码:默认read读取为非阻塞,每次往标准输入里面读取一个数据,默认缓冲区为空,便会卡住让我们输入数据
非阻塞等待
#include <iostream>
using namespace std;
#include <fcntl.h>
#include <unistd.h>
#include<errno.h>
void SetNonBlock(int fd)
{
//获取之前文件的状态
int fl=fcntl(fd,F_GETFD);
if(fl<0)
perror("fcntl");
//把文件描述符设置为非阻塞,设计标记
fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
int main()
{
//观察标准输入阻塞和非阻塞状态读取数据
char ch;
SetNonBlock(0);//给0设置为非阻塞
while (1)
{
sleep(1);
ssize_t s = read(0, &ch, 1);
if (s > 0)
{
printf("%c\n", ch); //读取成功
}
else if(s<0&&(errno==EAGAIN||errno==EWOULDBLOCK))//EAGIN和EWOULDBLOCK是一样的
{
//非阻塞读取,底层的数据没有就位
printf("数据没有就绪\n");
cout<<"continue"<<endl;
}
else if(errno==EINTR&&s<0)//读取被信号中断了,这个地方就是失败了
{
continue;
}
else
{
// cout << ch << endl;
cout<<s<<endl;
}
cout << "............." << endl;
}
return 0;
}
//当我们不输入的时候,就会卡住等待我们进行输入
//设置为非阻塞,当缓冲区里面没有数据的时候,read直接返回失败,ssize_t 是一个有符号整数,-1代表底层数据没有就绪,
//读取数据不算错误,而是一种通知,并且会设置errno为EAGAIN,(try again)表示底层数据没有准备好,下次再来的话,EWOULDBLOCK也同样的效果
//如果错误码是EINT表示阻塞等待时候被信号给阻塞掉了
在非阻塞的情况下,我们读取数据,如果数据没有就绪,系统是以出错的形式返回的(不是错误)
没有就绪和真正的错处,使用同样的方式标识的,如何进一步区分呢?errno=11()
EAGAIN(EWOULDBLOCK):给非阻塞用的,这两个是一样的,errno=11,底层没有就绪,try again
什么叫做等事件就绪,
IO事件就绪
- 读事件就绪,读缓冲区里面有数据,为了减少用户态内核态的过度切换,就让一次读取的数据足够多,水位线(低于就发)
- 写事件就绪,发送的缓冲区有足够的空间进行拷贝