1、select
1.1 当调用select 时,Linux都做了什么?
使用select 的应用程序用多路复用器,把我们想要监听的文件描述符分成三类(可读,可写,异常)一次性全部传给Linux内核,然后内核轮询所有文件描述符,监视其上的就绪事件,经过给定时长后,返回就绪事件的个数。
应用程序拿到返回值后,要自己遍历所有文件描述符,找出哪些被内核标记为有事件就绪。
当应用程序想再次使用select 查询就绪事件时,要再次把文件描述符传给内核,也就是上述过程全部重新来一遍。所以select 其实是非常低效的。
1.2 使用select 的细节 及 注意事项
select 使用一组数字表示socket文件描述符集合,从而实现多路复用。select 的核心是如下函数:
#include<sys/select.h>
int selece(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
上面说的 “一组数字”,就是函数中的第2,3,4个参数,参数类型是fd_set 结构体类型(用typedef 起的别名),别看他是个结构体,其实结构体里面仅仅定义了一个long 类型的整数。这个整数转化成二进制后 的每一位标记一个文件描述符,文件描述符的值为几,他就是二进制数中的第几位,该位的0、1值在调用select 前后有不同的含义。
- 调用
select 前,该位的0,1表示是否要让select 监视其对应socket文件描述符的事件,1表示要监视,0表示不用监视 - 调用
select 后,该位的0,1表示其对应的socket文件描述符上的事件是否发生,1表示发生了,0表示没发生
查看这些位的值要使用位操作,很麻烦,不过有现成的函数可以调用:
#include<sys/select.h>
FD_ZERO(&set); 将set的所有位置0,如set在内存中占8位则将set置为00000000
FD_SET(0, &set); 将set的第0位置1,如set原来是00000000,则现在变为100000000,这样fd==1的文件描述字就被加进set中了
FD_CLR(4, &set); 将set的第4位置0,如set原来是10001000,则现在变为10000000,这样fd==4的文件描述字就被从set中清除了
FD_ISSET(5, &set); 测试set的第5位是否为1,如果原来set是10000100,则返回非零,表明fd==5的文件描述符在set中,否则返回0
select 参数说明:
- 第一个参数
nfds 指定了被监听的文件描述符的总数,咱就设置为select 监听的所有文件描述符中的最大值+1,因为文件描述符的值是从0开始往上计数的。 - 第2,3,4个都是要监听的文件描述符集合,分别要监听可读,可写,异常事件。
select 能监视的文件描述符最大数量由函数第2,3,4个参数的数组大小决定。 - 最后一个参数限定
select 函数的超时时间,他是一个指针,指向结构体:
struct timeval
{
long tv_sec; 秒数
long tv_usec; 微秒数
};
使用指针是因为,内核会修改该结构体,告诉应用程序select 实际等待了多久。函数调用失败时,其值不确定。 传入时有两种特殊情况: ——把tv_sec 和 tv_usec 都设置为0,则select 非阻塞,调用后立即返回。 ——给timeout 传递NULL ,则·select 阻塞,直到某个文件描述符就绪。
具体什么情况才算就绪?
可写的就绪情况一般是:发送缓冲区不满。
实例程序:
这是一个服务器程序,使用select同时处理普通数据 和 带外数据。
这是一个客户端程序,使用select实现 非阻塞 connect。
2、poll
2.1 当调用poll 时,Linux做了什么?
类似于select ,poll 也使用多路复用器将想要监听的文件描述符集合传给内核,由内核遍历所有的文件描述符,并监视其事件,最终返回被触发事件的个数。
不同点是:
poll 的调用接口更简单一些,他没有把监控的事件分为三类(可读,可写,异常),而是统一用一个变量来表示poll 能够监视的事件种类远不止三种,下面会列出poll 不需要在调用前做预处理,因为内核每次修改的是pollfd 结构体的revents 成员,而events 和fd 成员保持不变。
2.2 使用poll 的细节 及 注意事项
#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds,int timeout);
参数说明:
① fds
fds 结构体数组就是我们传入的要监听的文件描述符集合,他的每个元素都包含 文件描述符 、监听的事件 监听结果 这3个信息,结构体其定义如下:
struct pollfd
{
int fd; 文件描述符
short events; 注册的事件
short revents; 实际发生的事件,由内核填充
};
events 可以是一系列事件的按位或: 需要提醒的是POLLRDHUP 事件,他为我们区分 调用recv 接收到的是用户数据还是关闭连接的请求提供了一个更简单的方式(之前我们要根据recv 的返回值区分)。此外,使用该事件时必须在代码最开始出定义_GUN_SOURCE ,如下所示:
#define _GUN_SOURCE 1
revents :我们用这个值判断到第那些事件被触发了。使用时就是用进行与运算,如revents & POLLIN 就是判断可读事件是否被触发。
② nfds
指定被监听事件集合的大小,没太多说的,其定义如下:
typedef unsigned long int nfds_t;
③ timeout
这个参数很重要,其含义被下面的epoll 沿用。它指定poll 的超时值,单位是毫秒。 ——当其值为-1时,poll 将永远阻塞,直到监听到事件发生。 ——当其值为0时,poll 调用立即返回。
2.3 实例程序
poll 的实例程序是一个聊天室程序,分为服务端和客户端两部分。多个客户端可以连接到同一个服务器,当一个客户端向服务器发送消息时,该消息会被转发给除发送端外的其他客户端,其他客户端收到该消息并输出到标准输出。
这是一个聊天室程序,分为客户端和服务器两部分,服务器使用epoll同时处理客户端的连接 和 客户输入数据。
3、epoll
epoll 的核心就是三个函数:
#include<sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epollfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epollfd, struct epoll_event* events, int maxevent, int timeout);
这三个函数不能死记硬背,要记住调用他们时应用程序及Linux内核都做了什么,才能更好地掌握epoll 的用法。
3.1 调用三个函数时,Linux都做了什么?
当调用epoll_create 函数时,Linux内核中创建一张内核事件表,用于后续注册我们想要监听的时间,该函数返回一个文件描述符,他唯一标识内核事件表。
而调用epoll_ctl 函数可以在内核事件表上注册我们想要监听的事件,当然喽,也可以注销(删除)和修改这些事件。
调用epoll_wait 函数会开始监听内核事件表上的所有时间,只要有注册的事件发生,就会将其添加到返回值的一员。规定的监听时长结束后,将发生的事件返回。这时,应用程序就可以直接通过返回值进行数据的读写。
除了了解以上执行过程以外,还应该了解epoll 的实现原理。实现epoll 的核心是红黑树。内核中创建的内核事件表其实就是一颗红黑树。我们往内核事件表中注册的每个事件,都对应红黑树中的一个节点。
那么,注册在内核事件表中的事件是怎么被触发的呢? 当网卡上收到数据时,会发生中断,提醒CPU去处理这批数据,在处理时,事件就会被触发。也就是中断间接触发事件。(这个描述极其简单,实际的过程远不止这样,而且涉及到的对象也不止网卡、CPU,这里大概意会就行了。)
3.2 三个函数的一些细节 及 注意事项
① epoll_create
size 参数不起作用,他告诉内核事件表需要多大。随便传一个正整数即可。
② epoll_ctl
int epoll_ctl(int epollfd, int op, int fd, struct epoll_event* event);
第二个参数op :
operator,操作。也就是本次epoll_ctl 调用要执行什么操作。共有三种取值:
EPOLL_CTL_ADD 注册(添加)事件
EPOLL_CTL_MOD 修改事件
EPOLL_CTL_DEL 注销(删除)事件
第三个参数就是要监视的socket 文件描述符。
第四个参数event :
第四个参数是一个指针,指向一个epoll_event 结构体,结构体定义如下:
struct epoll_event
{
uint32_t events; epoll 事件
epoll_data_t data; 保存用户数据的结构体
};
typedef union epoll_data
{
void* ptr;
int fd; 这个值最常用,他指定事件从属的 socket 文件描述符
uint32_t u32;
uint64_t u64;
};
epoll_event 的events 成员需要注意。他描述事件类型,epoll 支持的事件类型和poll 基本相同。但是表示epoll 事件类型的宏要在poll 对应的宏前加上E 。epoll 有两个额外的事件类型:
EPOLLET
EPOLLONESHOT
他们对于epoll 的功耗小运作非常关键,下面有使用到他们的实例程序。
返回值:
成功返回0,失败返回-1并设置errno 。
③ epoll_wait
int epoll_wait(int epollfd, struct epoll_event* events, int maxevent, int timeout);
返回值:
成功返回就绪事件的个数,失败返回-1并设置errno 。
第二个参数events :
第二个参数是epoll_event 结构体数组,数组的每个元素都是就绪的事件。在实际应用中,我们通常结合该函数的返回值与本参数遍历并处理所有就绪的事件。
第三个参数maxevents :
指定最多监听多少个事件,其值必须大于0。
第四个参数timeout :
该参数的含义与poll 接口的timeout 参数相同。实际应用中,通常设置为-1 ,阻塞的调用该函数。
3.3 LT 和 ET 模式
ET 模式是epoll 的另一个独特且强悍的特性。ET 模式的存在进一步提升了epoll 的性能。
① LT 模式
LT (Level Tigger,电平触发)模式是epoll 的默认工作模式。该模式下,当epoll_wait 检测到某事件被触发时,若应用程序不去处理该事件,那么下次调用epoll_wait 函数还会再次向应用程序报告该事件,直到事件被处理。
② ET 模式
ET (Edge Tigger,边沿触发)模式下,当epoll_wait 检测到某事件被触发时,不管应用程序处不处理,下次调用epoll_wait 都不会再此报告该事件。也就是说,程序员只有一个选择,那就是在第一次报告该事件时就处理。这变向降低了同一个事件被重复出发的次数(每次出发都要从内核拷贝该事件),因此效率更高。
实际使用ET 模式时要注意:
- 当在
ET 模式下处理某个被触发的事件时,你处理一遍可能没处理完(如:可读事件),所以要在while(1) 里面处理事件,从而确保你确实处理完了这个事件。(下面有这种情况的实例程序) - 使用
ET 模式必须将文件描述符设置为非阻塞的。因为,上一条要求在while(1) 里处理事件,那么在最后一次循环中,事件一定已被处理完了,如果socket是阻塞的,那么程序就会一直阻塞在这里。
这是一个服务器程序,包含ET,LT两种工作模式。
3.4 EPOLLONESHOT 事件
EPOLLONSHOT 事件在多线程程序中使用。在多线程成程序中,即使使用ET 模式,一个socket上的某事件还是可能触发多次,比如,一个线程读取完某个socket 上的数据后,开始处理这些数据,而在数据处理的过程中EPOLLIN 事件再次被新来的数据触发,此时另外一个线程被唤醒来读取这些新的数据。于是,就出现多个线程同时操作一个socket的局面。这时,需要使用EPOLLONESHOT 避免这种情况。
对于注册了EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl 函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程不可能操作该socket。但是,注册了EPOLLONESHOT 事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket 上的EPOLLONESHOT 事件,以确保这个socket下一次可读时,其EPOLLIN 事件能被触发。
使用EPOLLONESHOT 时要注意:
- 也要使用
while(1) 确保某个被触发的事件被处理完毕 - 也要设置socket文件描述符为非阻塞的
- 处理完一个时间后,一定要记得重新设置
EPOLLONESHOT 事件 - 可以同时使用
ET 模式和EPOLLONESHOT 事件,这两个没有任何冲突,且功能上也没有任何冲突。
这是一个服务器程序,使用了EPOLLONESHOT事件。
3.5 其他实例程序
这是一个客户端程序,使用 epoll 实现同时处理一个端口的TCP和UDP服务。
4、三组I/O服用函数的比较
这里重点说一下实现原理: poll 和 select 都是轮询的方式,内核每次都扫描整个注册的文件描述符集合;而epoll_wait 采用回调方式,内核检测到就绪的文件描述符时,触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪队列,最后返回给用户。
这也就引出了epoll 的一个缺点,当事件触发比较频繁时,回调函数也会被频繁触发,此时效率就未必比select 或 poll 高了。所以epoll 的最佳使用情景是:连接数量多,但活跃的连接数量少。
|