一、五种IO模型
1. 认识IO
什么是IO呢?
以前我们说scanf要等待数据就绪,数据就绪之后才会继续执行下面的代码,这就叫做IO 。还有read,recv,send,write这些函数,无论是接收数据还是发送数据,当条件不就绪时就会阻塞(即等待)。当接收缓冲区里有数据时才会拷贝,对于发送缓冲区来说也是如此。
所以,所谓IO = 等 + 拷贝。
那等,等的是什么呢?
等的是IO条件就绪。比如recv,当接收缓冲区里没有数据时就会阻塞,有数据时才会拷贝。
那什么是高效的IO呢?
比如:你和你舍友在操场跑步,你们比谁跑得快。那怎么判断谁跑得快呢?给你们一分钟的时间,一分钟之后谁跑的距离远谁就跑得快喽。
所以,评判一个人跑的快不快,是根据在单位时间内谁跑的距离远来决定的。
那么怎样判断IO的高效性呢?
和跑步是一样的,单位时间内拷贝更多的数据,IO的效率就高了。
那如何让你的IO变的高效呢?
减少等待的比重。
2. 五种IO模型
举个例子:钓鱼大家肯定都知道吧,虽然我们大多数人并没有钓过。
钓鱼 = 等 + 钓 。那什么是高效的钓鱼呢?等的比重非常低,单位时间内,钓鱼的效率高。
现在有5个人来这里钓鱼,他们拥着不同的身份。
第一位张三,他是一名新手,他钓鱼的方式是鱼漂不动,张三不动,他的眼睛一直死死的盯着鱼漂。(鱼漂就是一个彩色的布,用来判断有没有鱼上钩)
第二位李四,1年的钓鱼佬,他和张三一样也在钓鱼,但李四可不会一直盯着鱼漂,鱼儿没有上钩,李四就刷刷抖音,看看C语言了,做点其它的事情。
第三位王五,3年的钓鱼佬,他钓鱼的方式是用铃铛挂在鱼竿的顶部,他也会去做别的事情,比如看电影了什么的。只要铃铛响了就说明有鱼儿上钩了。
第四位赵六,是一名小有财气的主,他钓鱼一次性甩出100个鱼竿,来回检测,只要有一个鱼竿有动静就开始捞鱼。
第五位田七,这是一名非常有钱的主,开着奔驰来钓鱼,司机小王开着车,田七钓鱼不是为了钓鱼而钓鱼,而是为了吃鱼而钓鱼。但是突然之间公司有点事,必须回公司去处理问题,所以就让小王来帮他钓鱼。到时候他来接小王。
那么,在这5个人当中谁钓鱼的效率最高呢?
当然是赵六了,因为同一时间内,赵六能够钓到鱼的概率为100/104。赵六等的比重是最低的。
张三和李四钓鱼的效率有区别吗?
是没有的 。那他们之间的区别在哪里,区别在于张三是一直在等,而李四在等的同时还可以做其它事情。
张三这种模式就是阻塞IO,李四就是非阻塞式IO,王五属于信号驱动IO,赵六是多路复用(或多路转接IO),田七是异步IO。前四种可以统称为同步IO。
给异步IO下一个定义 :没有参与IO的任何过程,只是发起IO,最后只拿结果的叫做异步IO。
同步IO :凡是参与IO的等或者拷贝的任何一个或者两个过程,都叫做同步IO。
阻塞IO :在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
非阻塞IO :如果内核还未将数据准备好,系统调用仍然会直接返回,并返回EWOULDBLOCK错误码。
信号驱动IO :内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
数据准备好之后 ,OS会向目标进程发送SIGIO信号,通知进程可以开始IO了。不过这种方式不常用,有风险,因为信号驱动IO本质是通过改变bit的内容来达到目的的,有可能会失败,有风险。
IO多路转接 :能够同时等待多个文件描述符的就绪状态。
之所以说IO多路转接的效率高 ,是因为它一次性可以等待多个fd,等待的时间进行了重叠。
像select,poll,epoll函数就是专门用来IO多路转接的。
一次性等待多个fd,这样可以减少IO等待的比重,那IO的效率就高了。
C语言里的scanf函数,键盘不输入程序就卡住了,这就是阻塞,是因为读条件不就绪 。像以前学过的open,recv这些函数默认就是阻塞的。虽然它们有选项可以设置为非阻塞的,但是每个函数都要自己手动设置,这样太麻烦了。有没有接口一次性将所有的函数行为设置为非阻塞的,当然有了。
为了达到这个目的,就要更改OS里的标志位了 ,毕竟linux下一切皆文件,只要更改OS里文件里的标志位,将它设置为非阻塞的,即使应用层是阻塞式的,它的行为也是非阻塞的。

fd代表文件描述符,cmd代表设置的命令。成功了的话,它的返回值依赖于它的操作,失败返回-1并设置错误码。
cmd = F_DUPFD,复制一个现有的文件描述符。
cmd = F_GETFD或F_SETFD获得/设置文件描述符。
cmd = F_GETFL或F_SETFL获得/设置文件状态。
cmd = F_GETOWN或F_SETOWN获得/设置异步IO所有权。
cmd = F_GETLK或F_SETLK或F_SETLKW获得/设置记录锁。
写一份代码来验证一下。


这里果然是非阻塞的 ,只不过这里它报错了,它说资源暂时不可用。当进程非阻塞之后,资源不存在就会已出错的形式返回 ,那这种出错算真的"出错"吗?当然不算了。
那也就是说,read函数如果是非阻塞行为,那它出错的情况就不只是单纯的"出错"了。它包含多种情况,一种是真的出错了,另一种是其它情况导致出错了,被迫返回0(就像这里的资源未就绪)。那read函数的返回值就只有这三种情况,最后一种情况要如何区分呢?
这就不得不说到read函数的返回值了。

出错了,不仅会返回-1还会设置错误码。根据错误码就可以判断是不是真的出错了还是因为其它情况。
当函数调用失败以后,我们最关心的是什么呢?最关心的当然是为什么会出错了,所以出错之后会将错误码设置到errno里,errno是一个全局变量。
那也就是说errno就是一个共享资源了,如果是多进程的情况下,如何保证多进程访问errno的正确性 ?这个不用担心,因为多进程会写时拷贝。
那如果是多线程呢 ?该变量在多线程里是局部存储的,每个线程都有一份。
如果是因为资源没有准备就绪而导致的出错,errno就是11,表示没有临时资源可用。

还有一种情况,就是因为中断信号而导致返回值小于0,当进程刚执行到read函数的时候,因为信号中断而执行中断方法,执行完之后再从进程中断的位置开始执行 ,但是此时不会二次调用read函数了,就会读取出错。这个时候errno的值就是EINTR,表示因为中断而出错。


3. 多路转接select
select是什么呢?
select是一个多路复用,多路转接的一种具体方案。
那它的核心定位是什么呢?
是为了解决IO问题,提高IO效率 。通过一次等待多个fd,来达到提高IO效率的。
在等待的时间内,只要有任意的一个或多个fd就绪,就会通知用户。它是一种就绪事件通知机制。
除了select,还有poll,epoll。
select函数的参数和返回值。


select函数的返回值有三种情况:1.返回值大于0,代表有多少个fd就绪。2.等于0,超时,没有fd就绪,也没有报错。3.小于0,select函数调用出错,返回值为-1,并设置错误码。
接下来就来解析该函数的这些参数都分别代表的是什么含义吧。
select是一种多路转接的方案 ,一次可以等待多个fd,nfds代表的是等待的众多文件描述符中最大文件描述符值+1。
先看最后一个参数timeout,是一个指针,它的类型是struct timeval,可以看出来它就是一个结构体。

该结构体只有两个成员 ,第一个成员代表秒,第二个成员代表微秒。它是用来干嘛的呢 ?是用来设置等待时长的。
既然是解决IO的,那么就要有处理IO的方式,阻塞式,还是非阻塞式亦或者是其它方式的IO呢?
如果timeout为NULL,代表阻塞式等待,如果timeout指向的变量内容设置为{0,0},代表等待0秒,即非阻塞式等待,如果是{5,0}(也可以是其它值),代表在5秒内阻塞等待,5秒之后触发一次timeout,即成为非阻塞式等待,等到下一次再次调用select时,就会再等待5秒。
timeout是一个输入输出型参数。输入时代表等待的时长,输出时代表剩余多长时间。(有fd就绪之后就会通知用户)
最后就是中间的3个参数了,这三个参数的类型都是 fd_set(文件描述符集)。select一次可以等待多个fd,那它就要将等待的fd进行保存,那如何保存呢?
select是一种就绪事件通知机制,那调用select时应该要传递两种语义 :1.输入时,用户告诉内核要等待哪些fd。2.输出时,内核要告诉用户,哪些fd已经就绪。
那要如何保存fd呢 ?fd就是一个个整数,使用位图比较方便。以readfds为例,它是一个输入输出型参数 。输入时,用户告诉内核,内核要关心fd_set集合中哪些fd上面的读事件,比特位的位置代表文件描述符的编号,比特位的内容代表是否关心(1/0,1代表关心,0代表不关心)。
输出时,内核告诉用户,fd_set集合中,哪些fd上面的读事件就绪了,比特位的位置依然代表文件描述符编号,比特位的内容代表是否就绪(1/0,1代表就绪,0代表未就绪)。
对fd进行操作无非就是三种情况:1.读 2.写 3.异常 。所以,select函数有readfds,writefds,exceptfds三个参数,分别只对于读,写,异常进行操作。如果既读又写,那就分别给readfds,writefds参数进行设置,将比特位改为1。
select函数除了第一个参数以外,其余参数都是输入输出型参数。
那fd_set既然是一个结构体,也属于一种数据类型,它的大小自然也是固定的。那看一下fd_set。


该数组的类型其实就是一个long int类型的 。

该数组的大小是32,最多只能标识1024个fd。这也就意味着select能够管理的fd总数,是有上限的,最多只能管理1024个文件描述符。
fd_set既然是一个位图,那我们能直接对它进行操作吗?当然可以,不过这样做有风险,有可能我们自己设置会出错,所以不建议。可以用以下函数对其进行操作。

FD_CLR将指定的fd清除,FD_ISSET判断指定的fd是否在位图中,FD_SET将指定的fd设置到位图里,FD_ZERO将位图全部置为0。
4. select的特点,缺点
特点:
. select可监控的文件描述符是有上限的,取决于sizeof(fd_set)的值。
. 将fd加入select监控集的同时,还要在使用一个辅助数组保存select监控集中的fd。
这是由于select监控之前,需要用户将监控的fd进行设置,select之后内核会对位图进行更改,所以此时需要保存历史的fd。
缺点:
. 每次调用select,都需要手动设置fd集合,从接口使用角度来说非常不便。
. 每次调用select,都需要把fd集合从用户态拷贝到内核态。如果fd很多,这个开销会很大。
. 同时每次调用select都需要在内核遍历传递进来的所有fd。
因为select要监控多个fd,就需要进行遍历。
. select支持的文件描述符数量太小。
正因为select有这么多的缺点,所以提出了一种更好的多路转接解决方案 :poll。
5. poll
poll的定位和select一样,都是多路转接的一种方案 ,是一种事件就绪通知机制,是为了解决等的问题。


poll的返回值和select的返回值代表的意义是一样的 。poll的第三个参数timeout是一个输入参数,毫秒计时,0代表非阻塞,-1代表阻塞。
poll的第一个参数fds的类型是struct pollfd,是一个结构体。它是一个输入输出型参数。nfds代表的是poll能监控的文件描述符的个数。
我们可以把poll的前两个参数当做是一个数组来看。

调用poll,传递struct pollfd[],输入数组的时候,用户告诉内核,内核要关心哪些fd上的哪些事件。select监控fd事件时是通过readfds,writefds,exceptfds三个参数来决定的,那poll如何知道自己要关心哪些事件呢 ?通过struct pollfd中的第一个成员fd和第二个成员events来决定的。
那问题又来了,events是一个short类型呀,事件有读事件,写事件,异常事件...,这么多的事件,它怎么表示呢 ?和select一样,使用位图的方式来表示。
poll成功返回的时候 ,内核告诉用户,要关心的fd上面的哪些事件已经发生了。
那如何知道哪些事件已经发生了呢 ?通过fd和revents就可以知道了。fd代表的是文件描述符,revents也是一个位图,通过位图就可以知道哪些事件已经发生了。
events和revents的取值。


这些事件都是用大写的宏值来表示的,只有一个比特位为1的标志位。
poll主要解决了select的两个缺点 ,一个是文件描述符数量太少的问题,另一个是每次都需要手动的设置fd集合。
那它是如何解决的呢?一个是poll的参数nfds解决了select文件描述符太少的问题,通过传参解决。如果参数太小可以手动改大。而select的fd数量取决于OS(fd_set中的数组大小)。
那它又是如何解决手动设置fd集合的问题呢 ?因为select调用前需要用户手动设置位图来告诉内核要关心哪些fd上的哪些事件,调用后会修改位图,所以这导致了每次调用select都需要手动设置fd。而poll将输入输出参数(关心的事件,就绪的事件,即events,revents)进行了分离,从而解决了输入输出参数需要被重置的问题。
为什么要把poll的前两个参数当做是一个数组来看待呢 ?因为struct pollfd结构体一个变量只能表示一个fd,而poll作为多路复用,多路转接的具体方案,一次可以等待多个fd。所以有了nfds,一个结构体变量对应一个fd,nfds个结构体变量就对应nfds个fd,这不就相当于是一个数组吗!
以前我们说过文件描述符其实就是一个个数组下标,在一个进程中,打开的文件是在数组里存储的,给用户返回数组下标。那么,既然是一个数组,那它能够表示的文件描述符数量就是有限的,如果打开的文件太多,数组下标不够用了怎么办 ?如果文件描述符太多,静态数组空间有限,那么就会动态进行调整。在struct files_struct结构体里有一个成员会动态进行调整。
struct files_struct
{
struct fdtable* fdt;
}
struct fdtable
{
struct file** fd;//由它来动态申请
}
虽然poll相比于select要好一点,但是poll也有自己的问题,它的确解决了select文件描述符数量上的限制以及需要手动设置fd问题。但poll的底层依然需要对fd进行遍历。一旦fd过多,那么poll的遍历周期会变长,效率也会下降 。而多路转接方案还有一个epoll,是被公认为多路转接方案最好的方法。
6. epoll
epoll是为处理大量句柄而做了改进的poll。
句柄是一个抽象概念,是用来标识和访问操作系统资源的钥匙 。就像文件描述符,通过它就可以找到指定的文件。
亦或者是看电影的门票,有了门票才可以进入电影院。
那epoll是用来干什么的呢?它和select,poll一样都是多路转接的一种具体方案 ,是用来事件就绪通知的,一次可以等待多个fd。
现在对于epoll应该有了初步的认知了。那么接下来就要了解epoll的接口了,具体是如何对众多的fd进行监控的。


epoll_create的返回值是一个文件描述符,失败了返回-1,设置错误码。它的参数是被忽略的,不用管。


成功了,返回0,失败了返回-1设置错误码。
epoll_ctl函数是用来管理epoll实例中的文件描述符监控列表。
epfd就是epoll_create的返回值,op代表要执行什么操作,fd也代表文件描述符,event代表要对指定的文件描述符关心哪些事件。

这三个选项就是大写的宏值,代表要对指定的fd执行具体的操作,为op提供的选项 。EPOLL_CTL_ADD代表添加新的fd到监控列表,EPOLL_CTL_MOD代表修改已监控fd的事件设置,EPOLL_CTL_DEL从监控列表移除fd。

该函数有两个文件描述符epfd,fd,应该怎样理解呢?
epfd确实是一个文件描述符,但它是由epoll_create创建出来的,我们把它称为是一个epoll实例(epfd),它就是OS创建出来的一种数据结构的标识,就像文件描述符是用来标识一个文件的。
那这个epoll实例是干嘛的呢 ?epoll就是一个多路转接的方案,用来监控多个fd事件的。所以epoll实例就是用来管理众多fd的监控中心,而op,event参数都是为fd准备的,表明要关心哪些事件,对指定的文件描述符执行什么样的操作。
epoll实例就像一个学校一样,里面有着许多学生,学校就是用来管理学生的。


epoll_wait的返回值成功了就返回已经就绪的文件描述符的个数,没有就绪的文件描述符就返回0,出错了返回-1设置错误码。这一点和poll,select是一样的。
epfd依然是由epoll_create的返回值,timeout代表等待的时间。events和maxevents理解成一个输出型数组,它是用来内核告诉用户哪些fd上的哪些事件就绪的。
而epoll_ctl中的参数是用户要告诉内核要关心哪些fd上的哪些事件,执行什么样的操作。
epoll的原理:
我们在创建epoll模型时 ,OS会在底层创建一棵红黑树(默认是空的)。那如何描述红黑树节点呢 ?OS主要是由C语言写的,所以是用结构体来描述的。
struct rb_node
{
int fd;
int events;
int revents;
...
}
那红黑树表达的是什么意思呢 ?表达的是内核需要关心哪些fd上的哪些事件!那epoll都有哪些事件呢?

红黑树的一个节点就代表OS要关心的一个文件描述符上的事件,所以红黑树有多少个节点就说明有多少个fd要被关心。
那fd上的事件是谁要求OS要关心的 ?当然是用户了。可是红黑树是在OS内部的,用户是不能直接对红黑树进行操作的,所以要对外提供一些接口,方便用户对红黑树进行插入,修改,删除,灵活的对红黑树进行操作。
创建epoll模型的时候 ,OS不仅会创建一棵红黑树,还会创建一个就绪队列。它是一个队列,自然也有能够描述队列节点的结构体。
struct queue_node
{
int fd;
int revents;
...
}
那这个就绪队列表达的含义又是什么呢 ?红黑树是为了表达OS要关心哪些文件描述符上的哪些事件,而就绪队列是为了表达OS要关心的哪些文件描述符上的哪些事件已经就绪了。
比如红黑树节点中的3号文件描述符上的读事件需要被OS关心,3号文件描述符上的读事件就绪了,那么就会把3号文件描述符以及读事件链入到就绪队列里。
当fd被添加到epoll模型时,底层还会有回调机制,epoll模型中,回调机制被注册成一个函数(本来默认是空的,但是在epoll模型中被注册成了函数),当有文件描述符就绪时,该函数就会把fd和event形成一个"新节点",插入到就绪队列中。
这些所有的过程,操作都是由OS自动完成 ,我们把红黑树,就绪队列,回调机制三者合在一起叫做epoll模型。
那前面介绍的epoll函数分别在干什么呢?
epoll_create就是在创建epoll模型。
epoll_ctl就是用来对红黑树节点进行增删改操作的(op字段)。

红黑树节点是一对key->value的值,那该红黑树的键值是由谁来充当的呢 ?当然是fd了,文件描述符本来就是具有唯一性的。
那再想一下,为什么说epoll模型更好呢?这里的红黑树相当于poll里面的什么呢 ?红黑树存储的是文件描述符以及该文件描述符上的事件,而poll里面的数组不就充当了这个功能了吗!所以,这里的红黑树就相当于poll里面的数组,poll里面的数组需要我们自己去管理,增删改操作,而epoll里的红黑树却是OS里的,不需要我们自己来管理,进行增删改操作,这不就方便了许多。
那epoll_wait又是在干什么呢 ?epoll_wait就是在就绪队列里进行捞取就绪的文件描述符和事件。
那么在检测有没有事件就绪,还需要像select,poll一样去轮询检测吗 ?不需要了,只需要判断就绪队列是否为空就行了,O(1)的时间复杂度。
很明显,epoll_wait是从内核里的就绪队列里拿取数据,是内核到用户,哪个是用户到内核呢 ?只能是epoll_ctl,对红黑树进行增删改操作,将fd上的事件设置到内核里。
那回调机制是怎么把就绪的文件描述符及事件插入到就绪队列里的呢 ?还记得PCB吗,前面我们说过,PCB不仅属于双链表结构(以双链表的形式组织),它还属于队列结构,进程运行时在运行队列里,进程挂起时在阻塞队列里。一个PCB都可以既属于双链表又属于队列结构。那么红黑树的节点可不可以既属于红黑树又属于队列呢?当然可以了,所以,就绪队列里的节点和红黑树的节点是同一个节点,只不过将同一个节点链入到了不同的数据结构中。
回调机制会去检查红黑树上的哪些fd的哪些事件就绪了,然后再将这个节点链入到就绪队列里。这不就是激活节点了吗!
这就是epoll模型(epoll_create创建的数据结构)



在这里我们可以看到红黑树节点里只有颜色,左右指针,却并没有存储数据的描述 ,这是因为真正的红黑树节点是被内嵌进去的,所以,真正描述红黑树节点的数据结构是struct epitem。

struct eventpoll中的struct rb_root rbr就是指向struct epitem中的struct rb_node rbn,将来指针进行强转就可以访问struct epitem中的成员了。
而struct eventpoll中的struct list_head rdllist指向struct epitem中的struct list_head rdllink。这不就既属于红黑树又属于队列了吗!
struct epitem中的struct epoll_filefd ffd就是文件描述符,struct epoll_event event就是要关心的事件,unsigned int revents就是对就绪的事件进行通知。
那为什么epoll_create函数的返回值是一个文件描述符呢 ?既然它返回的是一个文件描述符,这已经是一个既定的事实了,这就说明在创建epoll模型的时候,还申请了一个struct file,struct file里的void* private_data就指向了struct eventpoll,符合linux下一切皆文件的思想。
7. LT和ET模式
epoll模型工作有两种模式:LT和ET。那什么是LT,ET模式呢?举两个例子来帮助大家理解。
比如说有一名学生小王住在6号公寓,房号601,他买了5个快递,当然了该公寓也不止只有小王一个人买快递了,学校里还有一个菜鸟驿站,驿站里有两名员工张三和李四。今天张三去给6号公寓买快递的学生派发包裹,于是张三推着个小车就去公寓楼下,小车里装着小王3个快递,给买了包裹的学生一个一个打电话,通知他们下来取快递,其他人都把快递取走了,就剩下小王了,第一次张三给小王打电话通知取快递,小王说好的,我等一会儿就取,结果别人都把包裹取走了,小王还没取,于是张三继续给小王打电话通知取快递,小王这个时候终于把游戏打完了,于是下来取快递,但是最后一个快递太大了,只能先拿两个快递上去,于是小王让张三等一会,自己马上下来取。结果小王上去之后,舍友让小王赶紧再开一局,张三就继续在楼下等着,这时候李四来了,也是给6号公寓里的小王派发快递的,李四拿着小王的两个快递,这时候张三和李四看见了对方,李四问张三,快递发完了没?张三说还没有,还有小王的一个快递,李四说这么巧,那你今天帮我把这两个快递派发了,我先下班了。于是张三的快递车里又新增了两个包裹,张三继续通知小王,让小王赶紧下来取快递,要不然自己就下班了,小王一听,赶紧和舍友一起下来把剩下的包裹取走了。这就叫做LT模式。
那什么是ET模式呢?
还是以上面的例子为背景,驿站有小王的5个快递,今天轮到李四给6号公寓派发快递了,依然只拿了小王的3个快递,李四来到楼下,给买了包裹的学生打电话,通知他们下来取快递,李四的做法和张三不一样,张三是只要快递没取走,就一直打电话通知,而李四只通知一次,如果不取走,李四就不再打电话通知了。依然还是其他学生把快递都取走了,只有小王没把快递取完,只取走了两个,最后一个快递太大了,小王让李四等一会,自己马上下来取。这个时候张三拿着小王剩下的两个快递来到了6号楼下,小王问李四,快递发完了没?李四说没有,还有小王的1个快递,张三说,我这里也有小王的两个快递,今天你帮我把它派发了吧。李四说没问题,昨天张三都帮他了,今天他帮一下张三,于是李四的快递车里从1个快递变成了3个快递,快递增多了。所以李四又给小王打电话,告诉小王你的快递又新增了两个,赶紧下来取吧。于是小王赶紧和他的舍友下来把剩下的快递取走了。这就是ET模式。
那简单总结一下什么是LT,什么是ET?
LT就是只要有快递你没取走就一直通知你,让你来取。
ET就是只通知一次,赶紧下来取快递。如果快递没取完,就不再通知。除非有新增快递,才会再次通知你。
那LT和ET两种模式,哪一种效率更高呢?当然是ET模式了,因为它只通知一次,如果不取可就不会再管你了,每一次通知都是有效通知。
这里我们对于LT和ET有了抽象的理解,那如何对应到epoll模型中呢?
epoll_wait是从就绪队列里拿取数据的,比如说5号文件描述符上的读事件就绪了,那epoll_wait就获取5号文件描述符上的事件,进行读取,如果数据没读取完,那该节点要不要继续存在就绪队列里呢 ?这就涉及到了LT和ET两种模式。
如果是LT,那么即使数据没读完,该节点也依然会存在于就绪队列里,可如果是ET模式,数据没读完,该节点就会被移走,下一次再也读不上来数据了,除非该文件描述符上的数据又增多了。
那就绪队列里有着许多文件描述符就绪了,如果一次不能把所有的事件拿走,怎么办呢?没关系,如果就绪队列里还有就绪事件,它还会进行获取。
提一个问题:LT相比于ET模式下,谁的IO效率高呢?
前面说过了,ET模式下的通知效率高,只要通知了,对端就要把数据拿走,否则就不管了。而ET模式下,数据是一种从无到有,从有到多的过程,才会通知一次,这就要求程序员必须一次性把本轮数据全部读取完毕,否则下一次可就读不到了。
所以,要如何保证一次性把本轮的全部数据拿走呢?举个例子,你爸身上有500块钱,今天你问你爸要100块钱,生病了要去看病,明天要100块钱,没衣服穿了,要买新衣服,你怎么知道你爸有钱呢?因为你问你爸要钱,你爸就给你就说明他有钱,如果有一天你问你爸要钱,你爸说没钱了,就说明你爸真没钱了,下一次你也不会再去找你爸要钱了。
同理,当我们recv读取数据的时候,你怎么知道缓冲区里还有没有数据可以读取呢?只需要通过recv的返回值就知道了,只要返回值大于0,就说明有数据读取,如果返回值为0,就说明没有数据可以读取了。
所以读取数据的时候,即使数据读取完毕,缓冲区里没有数据了,或者最后一次读取数据并没有读取到期望的值,都需要多读取一次,判断缓冲区里还有没有数据 。这个时候问题就来了,recv默认是阻塞的,如果缓冲区里没有数据了,那进程不就被卡住了,被挂起了吗!那服务器还如何去处理其它事件呢?
所以,ET模式下,必须把对应的文件描述符设置为非阻塞。目的是为了把本轮的数据读取完。所以ET的本质是倒逼着程序员,按照它的要求把数据读取完毕。
那为什么说ET的IO效率更高呢 ?首先,ET模式下必须一次性把本轮数据全部读取完(拷贝,从内核缓冲区拷贝到用户缓冲区),这就是ET模式下IO效率高的原因之一(IO = 等 + 拷贝)。其次,一次性读取大量数据,不就导致内核缓冲区的可利用的空间增大了吗!服务端读取数据完毕后,必须要给客户端进行ACK应答,ACK应答过程中不就可以在报文中的win窗口里给对端更新一个更大的窗口吗!那对端的滑动窗口变大了,发送的数据不就多了。IO效率不就高了。
那TCP协议里的PSH标志位在干嘛呢 ?在学习TCP协议时我们说过这个标志位 ,它就是要求对方内核把数据尽快交给上层。那它是怎么样让内核把数据交给上层呢 ?它不就是通过对应的fd上的读事件就绪来达到通知用户的目的,从而尽快取走内核里的数据吗!
那新的问题又来了,对端把数据发送过来到我的内核里,不就是读事件就绪了吗,为什么还要PSH标志位呢?
这就要说到缓冲区里的高低水位线了。如果发送过来的数据是一个一个字节的,每一次都要调用系统调用读取,是非常麻烦的,系统调用也是有成本的 ,所以这样做不妥。而高低水位线就是来解决这个问题的,假设低水位线是50个字节,如果缓冲区里的数据字节低于50个字节,那就不会通知用户读事件就绪,只有当大于50字节的时候才会通知用户。这样可以减少系统调用的次数,降低成本。
那怎么样理解PSH标志位和读事件就绪之间的区别呢 ?读事件是有界限的,只有大于低水位线才能触发读事件就绪,而PSH标志位则没有限制,只要缓冲区里有数据就可以通知用户读取,哪怕只有一个字节。
那LT好像也可以做ET的这些事情啊,ET不就是要一次性读取全部的数据吗,LT也可以实现啊!那LT和ET之间到底有什么区别呢?
ET会倒逼着程序员按照ET的要求读取数据,而LT不行,它没有对程序员起到约束的能力。这是它们之间本质的区别。
那在epoll模型中如何设计LT和ET模式呢?
在epoll模型中,默认就是LT模式的,要是想设置ET模式,只需要使用EPOLLET就可以了。