select、poll、epoll的使用方法 和 使用场景

1、select

1.1 当调用select时,Linux都做了什么?

使用select的应用程序用多路复用器,把我们想要监听的文件描述符分成三类(可读,可写,异常)一次性全部传给Linux内核,然后内核轮询所有文件描述符,监视其上的就绪事件,经过给定时长后,返回就绪事件的个数。

应用程序拿到返回值后,要自己遍历所有文件描述符,找出哪些被内核标记为有事件就绪。

当应用程序想再次使用select查询就绪事件时,要再次把文件描述符传给内核,也就是上述过程全部重新来一遍。所以select其实是非常低效的。

1.2 使用select的细节 及 注意事项

select使用一组数字表示socket文件描述符集合,从而实现多路复用。select的核心是如下函数:

#include
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
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_sectv_usec都设置为0,则select非阻塞,调用后立即返回。
——给timeout传递NULL,则·select阻塞,直到某个文件描述符就绪。

具体什么情况才算就绪?

select、poll、epoll的使用方法 和 使用场景_第1张图片
可写的就绪情况一般是:发送缓冲区不满

实例程序:

这是一个服务器程序,使用select同时处理普通数据 和 带外数据。

这是一个客户端程序,使用select实现 非阻塞 connect。

2、poll

2.1 当调用poll时,Linux做了什么?

类似于selectpoll也使用多路复用器将想要监听的文件描述符集合传给内核,由内核遍历所有的文件描述符,并监视其事件,最终返回被触发事件的个数

不同点是:

  • poll的调用接口更简单一些,他没有把监控的事件分为三类(可读,可写,异常),而是统一用一个变量来表示
  • poll能够监视的事件种类远不止三种,下面会列出
  • poll不需要在调用前做预处理,因为内核每次修改的是pollfd结构体的revents成员,而eventsfd成员保持不变。

2.2 使用poll的细节 及 注意事项

#include
int poll(struct pollfd* fds, nfds_t nfds,int timeout);
参数说明:
fds

fds结构体数组就是我们传入的要监听的文件描述符集合,他的每个元素都包含 文件描述符监听的事件 监听结果这3个信息,结构体其定义如下:

struct pollfd
{
	int fd;			文件描述符
	short events;	注册的事件
	short revents;	实际发生的事件,由内核填充
};
  • events可以是一系列事件的按位或:select、poll、epoll的使用方法 和 使用场景_第2张图片
    select、poll、epoll的使用方法 和 使用场景_第3张图片
    需要提醒的是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
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_eventevents成员需要注意。他描述事件类型,epoll支持的事件类型和poll基本相同。但是表示epoll事件类型的宏要在poll对应的宏前加上Eepoll有两个额外的事件类型:

EPOLLET
EPOLLONESHOT

他们对于epoll的功耗小运作非常关键,下面有使用到他们的实例程序。

select、poll、epoll的使用方法 和 使用场景_第4张图片

返回值:

成功返回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 LTET模式

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服用函数的比较

select、poll、epoll的使用方法 和 使用场景_第5张图片
这里重点说一下实现原理:
pollselect都是轮询的方式,内核每次都扫描整个注册的文件描述符集合;而epoll_wait采用回调方式,内核检测到就绪的文件描述符时,触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪队列,最后返回给用户。

这也就引出了epoll的一个缺点,当事件触发比较频繁时,回调函数也会被频繁触发,此时效率就未必比selectpoll高了。所以epoll的最佳使用情景是:连接数量多,但活跃的连接数量少。

你可能感兴趣的:(Linux网络编程,Linux,服务器)