一、select
select是系统提供的一个多路转接接口。
- select系统调用可以让我们的程序同时监视多个文件描述符上的事件是否就绪。
- select的核心工作就是,当监视多个文件描述符中有一个或者多个事件就绪时,select才会成功返回并将对应的文件描述符的就绪事件告知调用者。
select函数
select函数
select函数原型如下:
int select(int fds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数说明:
- nfds:需要监视的文件描述符中,最大的文件描述符值+1。
- readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
- writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
- exceptfds:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。
参数timeout的取值:
- NULL/nullptr:select调用后进行阻塞等待,知道被监视的某个文件描述符上的某个事件就绪。
- 0:select调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
- 特定的时间值:select调用后在指定时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。
返回值说明:
- 如果函数调用成功,则返回有事件就绪的文件描述符的个数。
- 如果timeout时间耗尽,则返回0。
- 如果函数调用失败,则返回-1,同时错误码会被设置。
select调用失败时,错误码可能被设置为:
EBADF
:文件描述符为无效的或该文件已经被关闭。EINTR
: 此调用被信号所中断。EINVAL
:参数nfds为负值。ENOMEM
:核心内存不足。
fd_set
结构
fd_set结构与sigset_t结构类似,fd_set本质也是一个位图,用位图中所对应的位来表示要监视的文件描述符。
调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作。
如下:
void FD_CLR(int fd, fd_set *set); //用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); //用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); //用来清除描述词组set的全部位
timeval
结构
传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段事件长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒。
select基本工作流程
如果我们要实现一个简单的select服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么这个select服务器的工作流程应该是这样的:
- 先初始化服务器,完成套接字的创建、绑定和监听。
- 定义一个fd_array数组用于保存监听套接字和已经与客户端建立连接的套接字,刚开始时就将监听套接字添加到fd_array数组当中。
- 然后服务器开始循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作。
- 每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将fd_array当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪。
- 当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds当中,此时我们就能够得知哪些文件描述符的读事件就绪了,并对这些文件描述符进行对应的操作。
- 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fd_array数组当中。
- 如果读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
- 当然,服务器与客户端建立连接的套接字读事件就绪,也可能是因为客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从fd_array数组当中清除,因为下一次不需要再监视该文件描述符的读事件了。
说明一下:
- 因为传入select函数的readfds、writefds和exceptfds都是输入输出型参数,当select函数返回时这些参数当中的值已经被修改了,因此每次调用select函数时都需要对其进行重新设置,timeout也是类似的道理。
- 因为每次调用select函数之前都需要对readfds进行重新设置,所以需要定义一个fd_array数组保存与客户端已经建立的若干连接和监听套接字,实际fd_array数组当中的文件描述符就是需要让select监视读事件的文件描述符。
- 我们的select服务器只是读取客户端发来的数据,因此只需要让select帮我们监视特定文件描述符的读事件,如果要同时让select帮我们监视特定文件描述符的读事件和写事件,则需要分别定义readfds和writefds,并定义两个数组分别保存需要被监视读事件和写事件的文件描述符,便于每次调用select函数前对readfds和writefds进行重新设置。
- 服务器刚开始运行时,fd_array数组当中只有监听套接字,因此select第一次调用时只需要监视监听套接字的读事件是否就绪,但每次调用accept获取到新连接后,都会将新连接对应的套接字添加到fd_array当中,因此后续select调用时就需要监视监听套接字和若干连接套接字的读事件是否就绪。
- 由于调用select时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历fd_array对readfds进行重新设置时,还需要记录最大文件描述符值。
select的优缺点
select的优点
- 可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由accept、read、write等接口来完成,这些接口在进行IO操作时不会被阻塞。
- select同时等待多个文件描述符,因此可以将“等”的时间重叠,提高了IO效率。
当然,这也是所有多路转接接口的优点。
select的缺点
- 每次调用select,都需要手动设置fd集合,从接口使用的角度来说也非常不方便。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
- select可监控的文件描述符数量太少。
select可监控的文件描述符的个数
调用select函数时传入的readfds、writefds以及exceptfds都是fd_set结构,fd_set结构本质是一个位图,它用每一个比特位来标记一个文件描述符,因此select可监控的文件描述符个数是取决于fd_set的比特位个数的。
通过以下代码可以看到fd_set类型有多少个比特位。
这里我们可以看到,其实select可监控的文件描述符个数也就是1024个。
一个进程能打开的文件描述符个数
进程控制块task_struct当中有一个files指针,该指针指向一个struct files_struct结构,进程的文件描述符表fd_array就存储在该结构中,其中文件描述符表fd_array的大小定义为NR_OPEN_DEFAULT
,NR_OPEN_DEFAULT
的值实际就是32。
但并不意味着一个进程最多只能打开32个文件描述符,进程能打开的文件描述符实际上是可以扩展的,比如我们当前使用的云服务器默认就是把进程能打开的文件描述符设置的很高的,通过 ulimit -a
命令就可以看到进程能打开的文件描述符上限。
因此select可监控的文件描述符个数太少是一个很大的问题,比如select可监控的文件描述符个数是1024,除去其中一个监听套接字,那么select服务器最多只能连接1023个客户端。
select的适用场景
多路转接接口select、poll和epoll,需要在一定的场景下使用,如果场景选择的不适宜,可能会适得其反。
- 多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃。因为少量连接比较活跃,也就意味着几乎所有的连接在进行IO操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高IO效率。
- 对于多连接中大部分连接都很活跃的场景,其实并不适合使用多路转接。因为每个连接都很活跃,也就意味着任何时刻每个连接上的事件基本都是就绪的,此时根本不需要动用多路转接接口来帮我们进行等待,毕竟使用多路转接接口也是需要花费系统的时间和空间资源的。
多连接中只有少量连接是比较活跃的,比如聊天工具,我们登录QQ后大部分时间其实是没有聊天的,此时服务器端不可能调用一个read函数阻塞等待读事件就绪。
多连接中大部分连接都很活跃,比如企业当中进行数据备份时,两台服务器之间不断在交互数据,这时的连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了。
二、poll
poll也是系统提供的一个多路转接接口。
- poll系统调用也可以让我们的程序同时监控多个文件描述符上的事件是否就绪,和select的定位是一样的,适用场景也一样。
poll函数
poll函数的原型如下:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数说明:
- fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合。
- nfds:表示fds数组的长度。
- timeout:表示poll函数的超时时间,单位是毫秒(ms)
参数timeout的取值:
- -1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:poll调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测后都会立即返回。
- 特定的时间值:poll调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后poll进行超时返回。
返回值说明:
- 如果函数调用成功,则返回有事件就绪的文件描述符个数。
- 如果timeout时间耗尽,则返回0.
- 如果函数调用失败,则返回-1,同时错误码会被设置。
poll调用失败时,错误码可能会被设置为:
EFAULT
:fds数组不包含在调用程序的地址空间中。EINTR
:此调用被信号所中断。EINVAL
:nfds值超过RLIMIT_NOFILE值。ENOMEM
:核心内存不足。
struct pollfd
结构
struct pollfd结构当中包含三个成员:
- fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0。
- events:需要监视该文件描述符上的哪些事情。
- revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。
events和revents的取值:
这些取值实际都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
- 因此在调用poll函数之前,可以通过或运算符将要监听的事件添加到events成员当中。
- 在poll函数返回后,可以通过与运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。
poll的优缺点
poll的优点
- struct pollfd 结构当中包含了 events 和 revents,相当于将select的输入输出型参数进行分离,因此在每次调用poll之前,不需要像select一样重新对参数进行设置。
- poll可监控的文件描述符数量没有限制。
- 当然,poll也可以同时等待多个文件描述符,能够提高IO效率。
说明一下:
- fds数组的大小是可以继续增大的,poll函数能够帮你监视多少个文件描述符是由传入poll函数的第二个参数决定的。
- 而fd_set类型只有1024个比特位,因此select函数最多只能监视1024个文件描述符。
poll的缺点
- 和select函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
- 每次调用poll,都需要把大量的struct pollfd结构从用户态拷贝到内核态,这个开销也会随着poll监视的文件描述符数目的增多而增大。
- 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
三、epoll
epoll系统调用提供的一个多路转接接口。
- epoll系统调用也可以让我们的程序同时监听多个文件描述符上的事件是否就绪,与select和poll的定位是一样的,适用场景也相同。
- epoll在命名上比epoll多了一个e,这个e可以理解成是extend,epoll就是为了同时处理大量文件描述符而改进的poll。
- epoll在2.5.4内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll相关系统调用
epoll有三个相关的系统调用,分别是epoll_create
、epoll_ctl
和epoll_wait
。
epoll_create函数
epoll_create函数用于创建一个epoll模型,该函数的原型如下:
int epoll_create(int size);
参数说明:
- size:自从Linux2.6.8后,size参数是被忽略的,但是size值必须设置为大于0的值。
返回值说明:
- epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置。
注意:当不在使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。
epoll_ctl函数
epoll_ctl函数用于向指定的epoll模型中注册事件,该函数的函数原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数说明:
- epfd:指定的epoll模型。
- op:表示具体的动作,用三个宏来表示。
- fd:需要监视的文件描述符。
- event:需要监视该文件描述符上的哪些事件。
第二个参数op的取值有以下三种:
EPOLL_CTL_ADD
:注册新的文件描述符到指定的epoll模型中。EPOLL_CTL_MOD
:修改已经注册的文件描述符的监听事件。EPOLL_CTL_DEL
:从epoll模型中删除指定的文件描述符。
返回值说明:
- 函数调用成功返回0,调用失败返回-1,同时错误码会被设置。
第四个参数对应的struct epoll_event结构如下:
struct epoll_event 结构中有两个成员,第一个成员events表示的是需要监听的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符。
events的常用取值如下:
EPOLLIN
:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。EPOLLOUT
:表示对应的文件描述符可以写。EPOLLPRI
:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。EPOLLERR
:表示对应的文件描述符发送错误。EPOLLHUP
:表示对应的文件描述符被挂断,即对端将文件描述符关闭了。EPOLLET
:将epoll的工作方式设置为边缘触发(Edge Triggered)模式。EPOLLONESHOT
:只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中。
这些取值实际也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
epoll_wait函数
epoll_wait函数用于收集监视的事件中已经就绪的事件,该函数的函数原型如下:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数说明:
- epfd:指定的epoll模型。
- events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
- maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
- timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。
参数timeout的取值:
- -1:epoll_wait调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:epoll_wait调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,epoll_wait检测后都会立即返回。
- 特定的时间值:epoll_wait调用后在直到的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后epoll_wait进行超时返回。
返回值说明:
- 如果函数调用成功,则返回有事件就绪的文件描述符个数。
- 如果timeout时间耗尽,则返回0。
- 如果函数调用失败,则返回-1,同时错误码会被设置。
epoll_wait调用失败时,错误码可能被设置为:
EBADF
:传入的epoll模型对应的文件描述符无效。EFAULT
:events指向的数组空间无法通过写入权限访问。EINTR
:此调用被信号所中断。EINVAL
:epfd不是一个epoll模型对应的文件描述符,或传入的maxevents值小于等于0。
epoll工作原理
红黑树和就绪队列
当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll
结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员rbr和rdlist与epoll的使用方式密切相关。
struct eventpoll{
...
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
struct rb_root rbr;
//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
- epoll模型当中的红黑树本质就是告诉内核,需要监视哪些文件描述符上的哪些事件,调用epoll_ctl函数实际就是在对这颗红黑树进行对应的增删改操作。
- epoll模型当中的就绪队列本质就是告诉内核,哪些文件描述符上的哪些事件已经就绪了,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件。
在epoll中,对于每一个事件都会有一个对应的epitem
结构体,红黑树和就绪队列当中的节点当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件。
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
- 对于epitem结构当中rbn成员来说,ffd与event的含义是,需要监视ffd上的event事件是否就绪。
- 对于epitem结构当中的的rdlink成员来说,ffd与event的含义是,ffd上的event事件已经就绪了。
说明一下:
- 红黑树是一种二叉搜索树,因此必须有键值key,而这里的文件描述符就天然的可以作为红黑树的key值。
- 调用epoll_ctl向红黑树当中新增节点时,如果设置了
EPOLLONESHOT
选项,当监听完这次事件后,如果还需要继续监听该文件描述符则需要重新将其添加到epoll模型中,本质就是当设置了EPOLLONESHOT选项事件就绪时,操作系统会自动将其从红黑树当中删除。 - 而如果调用epoll_ctl向红黑树当中新增节点时没有设置EPOLLONESHOT,那么该节点插入红黑树后就一直存在,除非用户调用epoll_ctl将该节点从红黑树当中删除。
回调机制
所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫ep_poll_callback。
- 对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担。
- 而对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中。
- 当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可。
采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理。
说明一下:
- 只有添加到红黑树当中的事件才会与底层建立回调方法,因此只有当红黑树当中对应的事件就绪时,才会执行对应的回调方法将其添加到就绪队列当中。
- 当不断有监视的事件就绪时,会不断调用回调方法向就绪队列当中插入节点,而上层也会不断调用epoll_wait函数从就绪队列当中获取节点,这是典型的生产者消费者模型。
- 由于就绪队列可能会被多个执行流同时访问,因此必须要使用互斥锁对其进行保护,eventpoll结构当中的lock和mtx就是用于保护临界资源的,因此epoll本身是线程安全的。
- eventpoll结构当中的wq(wait queue)就是等待队列,当多个执行流想要同时访问同一个epoll模型时,就需要在该等待队列下进行等待。
epoll三部曲
总结一下,epoll的使用过程就是三部曲:
- 调用epoll_create创建一个epoll模型。
- 调用epoll_ctl,将要监控的文件描述符进行注册。
- 调用epoll_wait,等待文件描述符就绪。
epoll的优点
- 接口使用方便,虽然拆分成了三个函数,但是反而使用起来更方便高效。
- 数据拷贝轻量:只有在新增监视事件的时候调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要重新将需要监视的事件从用户拷贝到内核。此外,调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝操作。
- 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。调用epoll_wait时直接访问就绪队列就知道哪些文件描述符就绪的时间复杂度是O(1),因为本质只需要判断就绪队列是否为空即可。
- 没有数量限制,监视的文件描述符数目无上限,只要内存允许,就可以一直向红黑树当中新增节点。
注意:
- 有人说epoll中使用了内存映射机制,内核可以直接将底层就绪队列通过mmap的方式映射到用户态,此时用户就可以直接读取到内核中就绪队列当中的数据,避免了内存拷贝的额外性能开销
- 这种说法是错误的,实际操作系统并没有做任何映射机制,因为操作系统是不相信任何人的,操作系统不会让用户进程直接访问到内核的数据的,用户只能通过系统调用来获取内核的数据。
- 因此用户要获取内核当中的数据,势必还是需要将内核的数据拷贝到用户空间。
与select和poll的不同之处
- 在使用select和poll时,都需要借助第三方数组来维护历史上的文件描述符以及需要监视的事件,这个第三方数组是由自己维护的,对该数组的增删改操作都需要用户自己来进行。
- 而是用epoll时,不需要用户自己维护所谓的第三方数组,epoll底层的红黑树就充当了这个第三方数组的功能,并且该红黑树的增删改操作都是由内核维护的,用户只需要调用epoll_ctl让内核对该红黑树进行对应的操作即可。
- 在使用多路转接接口时,数据流都有两个方向,一个是用户告知内核,一个是内核告知用户。select和poll将这两件事情都交给了同一个函数来完成,而epoll在接口层面上就将这两件事进行了分离,epoll通过调用epoll_ctl完成用户告知内核,通过调用epoll_wait完成了内核告知用户。
epoll工作方式
epoll有两种工作方式,分别是水平触发工作模式和边缘触发工作模式。
水平触发(
LT
,Level Triggered)
- 只要底层有事件就绪,epoll就会一直通知用户。
- 就像数字电路当中的高电平触发一样,只要一直处于高电平,则会一直触发。
epoll默认状态下就是LT工作模式。
- 由于在LT工作模式下,只要底层有事件就绪就会一直通知用户,因此当epoll检测到底层读事件就绪时,可以不立即进行处理,或者只处理一部分,因为只要底层数据没有处理完,下一次epoll还会通知用户事件就绪。
- select和poll其实就是工作在LT模式下的。
- 支持阻塞读写和非阻塞读写。
边缘触发(
ET
,Edge Triggered)
- 只要底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户。
- 就像数字电路当中的上升沿触发一样,只有当电平由低变高的那一瞬间才会触发。
如果要将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET选项。
- 由于在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,因此当epoll检测到底层读事件就绪时,必须立即进行处理,而且必须全部处理完毕,因为有可能此后底层再也没有事件就绪,那么epoll就再也不会通知用户进行事件处理,此时没有处理完的数据就相当于丢失了。
- ET工作模式下epoll通知用户的次数一般比LT少,因此ET的性能一般比LT性能更高,Nginx就是默认采用ET模式使用epoll的。
- 只支持非阻塞的读写。
ET工作模式下应该如何进行读写
因为在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了。
因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入。
- 当底层读事件就绪时,循环调用recv函数进行读取,直到某次调用recv读取时,实际读取到的字节数小于期望读取的字节数,则说明本次底层数据已经读取完毕了。
- 但有可能最后一次调用recv读取时,刚好实际读取的字节数和期望读取的字节数相等,但此时底层数据也恰好读取完毕了,如果我们再调用recv函数进行读取,那么recv就会因为底层没有数据而被阻塞住。
- 而这里的阻塞是非常严重的,就比如我们这里写的服务器都是单进程的服务器,如果recv被阻塞住,并且此后该数据也不就绪,那么就相当于我们的服务器挂掉了,因此在ET工作模式下循环调用recv函数进行读取时,必须对应的文件描述符设置为非阻塞状态。
- 调用send函数写数据时也是同样的道理,需要循环调用send函数进行数据写入,并且必须将对应的文件描述符设置为非阻塞状态。
强调:ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的,不是可选的。
对比LT和ET
- 在ET模式下,一个文件描述符就绪之后,用户不会反复收到通知,看起来比LT更高效,但如果在LT模式下能够做到每次都将就绪的文件描述符立即全部处理,不让操作系统反复通知用户的话,其实LT和ET的性能也是一样的。
- 此外,ET的编程难度比LT更高。