什么时候socket可读
- socket内核中,接受缓冲区的字节数大于等于低水位标志SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于0;
- TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读,则返回 0;
- 侦听 socket 上有新的连接请求;
- socket 上有未处理的错误。
什么时候socket可写
- socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0;
- socket 的写操作被关闭(调用了 close 或者 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
- socket 使⽤非阻塞 connect 连接成功或失败之后;
select
select缺点
- 每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
- 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
- select 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能。
poll
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
fds:指向结构体数组的首指针,每个数组元素都是一个struct pollfd结构。
**nfds:**参数fds结构体数组的长度
struct pollfd 结构体定义如下:
struct pollfd {
int fd; /* 待检测事件的 fd */
short events; /* 关心的事件组合 */
short revents; /* 检测后的得到的事件类型 */
};
poll 与 select 相比具有如下优点:
- poll 不要求开发者计算最大文件描述符加 1 的大小;
- 相比于 select,poll 在处理大数目的文件描述符的时候速度更快;
- poll 没有最大连接数的限制,原因是它是基于链表来存储文件描述符的;
- 在调用 poll 函数时,只需要对参数进行一次设置就好了。
poll 函数存在的一些缺点:
- 在调用 poll 函数时,不管有没有有意义,大量的 fd 的数组被整体在用户态和内核地址空间之间复制;
- 与 select 函数一样,poll 函数返回后,需要遍历 fd 集合来获取就绪的 fd,这样会使性能下降;
- 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
epoll
- 使用epoll_create()创建epollfd;
#include <sys/epoll.h>
int epoll_create(int size);
- 将我们需要检测事件的其他 fd 绑定到这个 epollfd 上,或者修改一个已经绑定上去的 fd 的事件类型,或者在不需要时将 fd 从 epollfd 上解绑,这都可以使用 epoll_ctl 函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
- 参数 epfd 即上文提到的 epollfd
- 参数 op,操作类型,取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分别表示向 epollfd 上添加、修改和移除一个其他 fd,当取值是 EPOLL_CTL_DEL,第四个参数 event 忽略不计,可以设置为 NULL
- 参数 fd,即需要被操作的 fd;
- 参数 event,这是一个 epoll_event 结构体的地址,epoll_event 结构体定义如下:
struct epoll_event
{
uint32_t events; /* 需要检测的 fd 事件,取值与 poll 函数一样 */
epoll_data_t data; /* 用户自定义数据 */
};
epoll_data_t
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- 函数返回值:epoll_ctl调用成功返回事件的fd数目,0表示超时,调用失败返回-1;
- 使用epoll_wait检测事件
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数的形式和 poll 函数很类似,参数 events 是一个 epoll_event 结构数组的首地址,这是一个输出参数,函数调用成功后,events 中存放的是与就绪事件相关 epoll_event 结构体数组;参数 maxevents 是数组元素的个数;timeout 是超时时间,单位是毫秒,如果设置为 0,epoll_wait 会立即返回。
返回值:成功返回有就绪事件的fd数目;如果超时返回0,调用失败返回-1;
epoll_wait的使用示例:
while (true)
{
epoll_event epoll_events[1024];
int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0)
{
//被信号中断
if (errno == EINTR)
continue;
//出错,退出
break;
}
else if (n == 0)
{
//超时,继续
continue;
}
for (size_t i = 0; i < n; ++i)
{
// 处理可读事件
if (epoll_events[i].events & POLLIN)
{
}
// 处理可写事件
else if (epoll_events[i].events & POLLOUT)
{
}
//处理出错事件
else if (epoll_events[i].events & POLLERR)
{
}
}
}
epoll_wait 与 poll 的区别
epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);而 poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。
epoll为什么能支持百万句柄:
1. 不用重复传递。我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
2. 在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
3. 极其高效的原因:
这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。(注:好好理解这句话!)
从上面这句可以看出,epoll的基础就是回调呀!
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll为什么要有ET模式
poll和select一样,是设计用来实现单线程多路复用的。属于很老的系统接口。
很多人对epoll的使用存在误区,认为epoll只是一个增强型的poll,而且大量使用LT模式。这就违背了epoll的设计初衷了。
epoll就是要解决高并发环境下IO效率急剧下降的问题,因此增加了边缘触发(Edge Trig)模式。什么是边缘触发呢?比如说本机正在通过一个TCP连接进行通信,对方发来了一个数据包,这个时候操作系统会把收到数据这个事件通知给接收进程。这个事情操作系统只做一次,所以叫边缘触发。接收进程只会收到一次通知。
为什么ET模式可以提高IO效率呢?
用户在调用epoll_wait时,ET模式产生的事件只会报告一次。不管epoll管理的连接有多少,epoll_wait都会在常数时间内返回。而使用LT模式时,epoll_wait会去遍历所有连接的状态,只要某个连接的接收缓冲区中还有数据,epoll_wait就会报告。可以看出在LT模式下,epoll管理的连接越多,遍历所需的时间就越长(呈线性增长趋势),IO效率就越低。当有成千上万个连接时,LT模式就不可用了。
因此,我们用就是要用ET模式,否则达不到提高效率的目的。如果连接不太多(几十个到几百个)其实还不如用多线程阻塞模式去处理,这样可以降低设计难度,效率上也没有太大的损失。
epoll为什么高效(相比select)
-
仅从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
-
此外,内核使用了slab机制,为epoll提供了快速的数据结构:在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
-
epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。