回顾部分
同步通信 VS 异步通信
同步就是发出一个调用时,在没有得到结果之前,该调用就不返回。调用者会主动等待调用的结果。
异步就是调用者发出调用之后,调用直接返回了,没有结果。后续有了结果,会以状态、信号等方式通知调用者。
阻塞 VS 非阻塞
阻塞就是指调用结果返回之前,当前线程被挂起,直到返回结果。
非阻塞就是指不能立刻返回结果之前,该调用不会阻塞当前线程。
select
概念:
是用来监视多个文件描述符状态变化的函数。程序会停在select这里等待,直到被监视的文件描述符有一个或者多个发生状态变化。
函数原型:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
函数返回值:
- 成功则返回⽂文件描述词状态已改变的个数。
- 返回0代表在描述词状态改变前已超过timeout时间,没有返回。
- 错误时返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout
的值变成不可预测。
**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的全部位
理解select的执行过程:
也就是理解底层实现原理。select底层是fd_set,fd_set长度为字节1,每一个bit对应一个文件描述符fd,那么一个字节fd_set可以最大对应8个fd。
select的特点:
1、可监视的文件描述符最多个数就是fd_set的大小sizeof(fd_set);如果一个服务器sizeof(fd_set) = 512,每个bit表示一个文件描述符,支持的最大文件描述符就是512*8=4096。
2、将fd加入到select监视集。使用一个array保存放到select监视集中的fd。一是select返回后,array作为源数据和fdset进行fd_set进行判断。二是select返回后会把以前加入的,但没有事件发生的fd清空。每次开始select前重新从array中取得fd逐一扫描,取到fd的最大值,用于select的第一个参数。
select的缺点:
1、调用select前,要把fd集合从用户态拷贝到内核态。
2、每次调用select都需要在内核遍历传递进来的所有fd。
3、select支持的文件描述符大小数量有限。
poll
函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数说明:
-
fds是⼀个poll函数监听的结构列表。每一个元素中, 包含了三部分内容: 文件描述符,监听的事件集
合, 返回的事件集合。 -
nfds表示fds数组的⻓长度。
-
timeout表⽰示poll函数的超时时间, 单位是毫秒。
返回值:
- 小于0, 表示出错。
- 等于0, 表示poll函数等待超时。
- 大于0, 表示poll由于监听的文件描述符就绪而返回。
poll的优点:
select使用三个位图表示,poll使用pollfd的指针实现。
1、pollfd结构包含监视的event和发生的event。接口使用方便。
2、poll没有文件描述符数量限制,但过多性能也会下降。
poll的缺点:
前提:fd增多时
1、和select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
2、每次调用poll要将大量的pollfd结构从用户态拷贝到内核中。
3、连接大量客户端在一个时刻可能只有很少的处于就绪状态,那么文件描述符数量增多,效率也会下降。
epoll
几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll_create
int epoll_create(int size);
创建一个epoll句柄。注意用完要close()关闭。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
select是告诉内核要监听什么类型的事件。epoll先注册需要监听的事件类型。
函数参数
1、epoll句柄。
2、动作(用三个宏表示:注册新的fd到epfd、修改fd监听事件、从epfd中删除一个fd)。
3、需要监听的fd。
4、告诉内核需要监听什么事件(可读、可写、发生错误、被挂断)。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * event, int maxevents, int timeout);
收集在epoll监视的事件中已经发送的事件。
如果函数调用成功,返回对应I/O上已准备好的⽂文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。
epoll的工作原理
底层实现:一颗红黑树,一张准备就绪句柄链表,少量的内核cache。
为什么epoll高效
epoll比select/poll的优越:
epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll的两种模式(ET & LT)
LT:
水平触发模式是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
ET:
边缘触发模式是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。
LT和ET优缺点:
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。