【Linux】select、poll和epoll详解

        关于I/O多路复用技术以及这三者是什么,这里我就不做过多的讲解了,如有不懂是什么的读者,请参考:【Linux】select,poll和epoll_linux epoll poll select-CSDN博客

一、select

        select函数的作用是检测一组socket中某个或某几个是否有 “ 事件 ” 就绪,这里的“事件”一般分为如下三类:

  • 读事件就绪
  1. socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于 0;
  2. TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读返回 0 值 ;
  3. 侦听 socket 上有新的连接请求;
  4. socket 上有未处理的错误。
  • 写事件就绪
  1. socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于 0;
  2. socket 的写操作被关闭(调用了 close 或 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
  3. socket 使⽤非阻塞 connect 连接成功或失败时。
  • 异常事件就绪

        socket上收到外带数据。

函数签名如下:

int select(int nfds, 
		   fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout);

参数说明:

  • 参数 nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数检测事件的 fd 中的最大 fd 值加 1。

  • 参数 readfds,需要监听可读事件的 fd 集合。

  • 参数 writefds,需要监听可写事件的 fd 集合。

  • 参数 exceptfds,需要监听异常事件 fd 集合。

readfdswritefds 和 exceptfds 类型都是 fd_set,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中:

/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;

/* Some versions of <linux/posix_types.h> define this macros.  */
#undef  __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT.  */
#define __NFDBITS       (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d)     ((d) / __NFDBITS)
#define __FD_MASK(d)    ((__fd_mask) 1 << ((d) % __NFDBITS))

/* fd_set for select and pselect.  */
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
	// 在我的centOS 7.0 系统中的值:
	// __FD_SETSIZE = 1024
	//__NFDBITS = 64
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];	 
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

/* Maximum number of file descriptors in `fd_set'.  */
#define FD_SETSIZE              __FD_SETSIZE

我们假设未定义宏__USE_XOPEN,将上面的代码整理一下:

typedef struct
{ 
    long int __fds_bits[16];	 
} fd_set;

将一个 fd 添加到 fd_set 这个集合中需要使用 FD_SET 宏,其定义如下:

void FD_SET(int fd, fd_set *set);

fd_set 数组的定义是:

  typedef struct
{ 
      long int __fds_bits[16];	//可以看成是128 bit的数组 
} fd_set;

        __fds_bits 是 long int 型数组,long int 占 8 个字节,每个字节 8 bit,每个 bit 对应一个 fd 的事件状态,0 表示无事件,1 表示有事件,数组长度是 16,因此一共可以表示 8 * 8 * 16 = 1024 个 fd 的状态, 这是 select 函数支持的最大 fd 数量。

        __FD_SET(d, set) 的实际操作是:

__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)

        __FD_ELT (d) 确定的是某个 fd(这里用 d 表示)在数组 __fds_bits 中的下标位置,计算方法是计算 fd 与 __NFDBITS 求商(__NFDBITS 值是 64 (8 * 8))。

        __FD_MASK 计算对应 fd 在对应 bit 位置上的值,计算方法是先与 __NFDBITS 求余得到 n,然后执行 1 << n,即左移 n 位,然后将值设置到对应的 bit 上( |= 操作)。

        举个例子,假设现在 fd 的 值是 57,那么:

  FD_SET(57, set)
  实际调用:
  __fds_bits[__FD_ELT(57)] |= __FD_MASK(57)
  即
  __fds_bits[57 / 64] |= (1 << (57 % 64))
  即
  __fds_bits[0] |= (0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000)(二进制)
  即数组下标为0的元素(64 bit)中第 57 bit被置为 1(0~63位)

        Linux 系统上往 fd_set 集合中添加新的 fd 时决定这个 fd 在 __fds_bits 数组的位置的实现使用的是位图法(bitmap),Windows 系统上添加 fd 至 fd_set 的实现则是依次从数组第 0 个位置开始往后递增。

        也就是说 FD_SET 宏的原理本质上是向一个有 1024 个连续 bit (共计 64 个字节)的内存的某个 bit 上设置一个标志。

        同理,如果我们需要从 fd_set 上删除一个 fd,即对应的 bit 位置 0,我们可以使用 FD_CLR,其定义如下:

void FD_CLR(int fd, fd_set *set);

        如果,我们需要将 fd_set 中所有的 fd 都清掉,即将所有 bit 位置 0,可以使用宏 FD_ZERO

void FD_ZERO(fd_set *set);

        当 select 函数返回时, 我们使用 FD_ISSET 宏来判断某个 fd 是否有我们关心的事件,FD_ISSET 宏的定义如下:

int  FD_ISSET(int fd, fd_set *set);

        这就是与 select 函数相关的几个宏的原理。示意图如下:

        参数 timeout,超时时间,即在这个参数设定的时间内检测这些 fd 的事件,超过这个时间后 select 函数将立即返回。这是一个 timeval 类型结构体,其定义如下:

struct timeval 
{
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};
/**
* select函数示例,server端, select_server.cpp
*/
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>

//自定义代表无效fd的值
#define INVALID_FD -1

int main(int argc, char* argv[])
{
  //创建一个侦听socket
  int listenfd = socket(AF_INET, SOCK_STREAM, 0);
  if (listenfd == INVALID_FD)
  {
      std::cout << "create listen socket error." << std::endl;
      return -1;
  }

  //初始化服务器地址
  struct sockaddr_in bindaddr;
  bindaddr.sin_family = AF_INET;
  bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  bindaddr.sin_port = htons(3000);
  if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
  {
      std::cout << "bind listen socket error." << std::endl;
  	close(listenfd);
      return -1;
  }
  
  //启动侦听
  if (listen(listenfd, SOMAXCONN) == -1)
  {
      std::cout << "listen error." << std::endl;
  	close(listenfd);
      return -1;
  }
  
  //存储客户端socket的数组
  std::vector<int> clientfds;
  int maxfd;
  
  while (true) 
  {	
  	fd_set readset;
  	FD_ZERO(&readset);
  	
  	//将侦听socket加入到待检测的可读事件中去
  	FD_SET(listenfd, &readset);
  	
  	maxfd = listenfd;
  	//将客户端fd加入到待检测的可读事件中去
  	int clientfdslength = clientfds.size();
  	for (int i = 0; i < clientfdslength; ++i)
  	{
  		if (clientfds[i] != INVALID_FD)
  		{
  			FD_SET(clientfds[i], &readset);

  			if (maxfd < clientfds[i])
  				maxfd = clientfds[i];
  		}
  	}
  	
  	timeval tm;
  	tm.tv_sec = 1;
  	tm.tv_usec = 0;
  	//暂且只检测可读事件,不检测可写和异常事件
  	int ret = select(maxfd + 1, &readset, NULL, NULL, &tm);
  	if (ret == -1)
  	{
  		//出错,退出程序。
  		if (errno != EINTR)
  			break;
  	}
  	else if (ret == 0)
  	{
  		//select 函数超时,下次继续
  		continue;
  	} 
  	else
      {
  		//检测到某个socket有事件
  		if (FD_ISSET(listenfd, &readset))
  		{
  			//侦听socket的可读事件,则表明有新的连接到来
  			struct sockaddr_in clientaddr;
  			socklen_t clientaddrlen = sizeof(clientaddr);
  			//4. 接受客户端连接
  			int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
  			if (clientfd == INVALID_FD)					
  			{         	
  				//接受连接出错,退出程序
  				break;
  			}
  			
  			//只接受连接,不调用recv收取任何数据
  			std:: cout << "accept a client connection, fd: " << clientfd << std::endl;
  			clientfds.push_back(clientfd);
  		} 
  		else 
  		{
  			//假设对端发来的数据长度不超过63个字符
  			char recvbuf[64];
  			int clientfdslength = clientfds.size();
  			for (int i = 0; i < clientfdslength; ++i)
  			{
  				if (clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i], &readset))
  				{				
  					memset(recvbuf, 0, sizeof(recvbuf));
  					//非侦听socket,则接收数据
  					int length = recv(clientfds[i], recvbuf, 64, 0);
  					if (length <= 0)
  					{
  						//收取数据出错了
  						std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl;							
  						close(clientfds[i]);
  						//不直接删除该元素,将该位置的元素置位INVALID_FD
  						clientfds[i] = INVALID_FD;
  						continue;
  					}
  					
  					std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl;					
  				}
  			}
  			
  		}
  	}
  }
  
  //关闭所有客户端socket
  int clientfdslength = clientfds.size();
  for (int i = 0; i < clientfdslength; ++i)
  {
  	if (clientfds[i] != INVALID_FD)
  	{
  		close(clientfds[i]);
  	}
  }
  
  //关闭侦听socket
  close(listenfd);
  
  return 0;
}

关于上述代码在实际开发中有几个需要注意的事项,这里逐一来说明一下:

  1. 正如上文介绍 select 函数相关宏所介绍的,select 函数调用前后可能会修改 readfds、writefds 和 exceptfds 这三个集合中的内容,所以如果读者想下次调用 select 函数时复用这些 fd_set 变量,记得在下次调用前使用 FD_ZERO 将 fd_set 清零,然后再调用 FD_SET 将需要检测事件的 fd 重新添加到 fd_set。

  2. select 函数也会修改 timeval 结构体的值,这也要求我们如果想复用这个变量,必须给 timeval 变量重新设置值。

    上面的例子中在调用 select 函数一次之后,变量 tv 的值也被修改了。具体修改成多少,得看系统的表现。当然这种特性却不是跨平台的,在 Linux 系统中是这样的,而在其他操作系统上却不一定是这样(Windows 上就不会修改这个结构体的值),这点在 Linux man 手册 select 函数的说明中说的很清楚。

  3. select 函数的 timeval 结构体的 tv_sec 和 tv_usec 如果两个值设置为 0,即检测事件总时间设置为 0,其行为是 select 会检测一下相关集合中的 fd,如果没有需要的事件,则立即返回

Linux select 函数的缺点也是显而易见的:

  • 每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
  • 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
  • select 函数在每次调用之前都要对传入参数进行重新设定,这样做也比较麻烦。

在 Linux 平台上,select 函数的实现是利用 poll 函数的,有兴趣的读者可以查找一下相关的资料来阅读一下。关于 poll 函数的使用,接下来我们会介绍。

二、poll函数用法

        poll 函数用于检测一组文件描述符(File Descriptor, fd)上的可读可写和出错事件,其函数签名如下:

#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

参数解释:

  • fds:指向一个结构体数组的首个元素的指针,每个数组元素都是一个 struct pollfd 结构,用于指定检测某个给定的 fd 的条件;

  • nfds:参数 fds 结构体数组的长度,nfds_t 本质上是 unsigned long int,其定义如下:

typedef unsigned long int nfds_t;
  • timeout:表示 poll 函数的超时时间,单位为毫秒。

        struct pollfd 结构体定义如下:

struct pollfd {
    int   fd;         /* 待检测事件的 fd       */
    short events;     /* 关心的事件组合        */
    short revents;    /* 检测后的得到的事件类型  */
};

        struct pollfd的 events 字段是由开发者来设置,告诉内核我们关注什么事件,而 revents 字段是 poll 函数返回时内核设置的,用以说明该 fd 发生了什么事件。events 和 revents 一般有如下取值:

事件宏事件描述是否可以作为输入(events)是否可以作为输出(revents)
POLLIN数据可读(包括普通数据&优先数据)
POLLOUT数据可写(普通数据&优先数据)
POLLRDNORM等同于 POLLIN
POLLRDBAND优先级带数据可读(一般用于 Linux 系统)
POLLPRI高优先级数据可读,例如 TCP 带外数据
POLLWRNORM等同于 POLLOUT
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对端关闭,或者关闭了写操作,由 GNU 引入
POPPHUP挂起
POLLERR错误
POLLNVAL文件描述符没有打开

        poll 检测一组 fd 上的可读可写和出错事件的概念与前文介绍 select 的事件含义一样,这里就不再赘述。poll 与 select 相比具有如下优点:

  • poll 不要求开发者计算最大文件描述符加 1 的大小;
  • 相比于 selectpoll 在处理大数目的文件描述符的时候速度更快;
  • poll 没有最大连接数的限制,原因是它是基于链表来存储的;
  • 在调用 poll 函数时,只需要对参数进行一次设置就好了。
/**
 * 演示 poll 函数的用法,poll_server.cpp
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>

//无效fd标记
#define INVALID_FD  -1

int main(int argc, char *argv[])
{
    //创建一个侦听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == INVALID_FD)
    {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    //将侦听socket设置为非阻塞的
    int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
    {
        close(listenfd);
        std::cout << "set listenfd to nonblock error." << std::endl;
        return -1;
    }

    //复用地址和端口号
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *) &on, sizeof(on));

    //初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *) &bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    //启动侦听
    if (listen(listenfd, SOMAXCONN) == -1)
    {
        std::cout << "listen error." << std::endl;
        close(listenfd);
        return -1;
    }

    std::vector<pollfd> fds;
    pollfd listen_fd_info;
    listen_fd_info.fd = listenfd;
    listen_fd_info.events = POLLIN;
    listen_fd_info.revents = 0;
    fds.push_back(listen_fd_info);

    //是否存在无效的fd标志
    bool exist_invalid_fd;
    int n;
    while (true)
    {
        exist_invalid_fd = false;
        n = poll(&fds[0], fds.size(), 1000);
        if (n < 0)
        {
            //被信号中断
            if (errno == EINTR)
                continue;

            //出错,退出
            break;
        }
        else if (n == 0)
        {
            //超时,继续
            continue;
        }

        for (size_t i = 0; i < fds.size(); ++i)
        {
            // 事件可读
            if (fds[i].revents & POLLIN)
            {
                if (fds[i].fd == listenfd)
                {
                    //侦听socket,接受新连接
                    struct sockaddr_in clientaddr;
                    socklen_t clientaddrlen = sizeof(clientaddr);
                    //接受客户端连接, 并加入到fds集合中
                    int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen);
                    if (clientfd != -1)
                    {
                        //将客户端socket设置为非阻塞的
                        int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
                        int newSocketFlag = oldSocketFlag | O_NONBLOCK;
                        if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
                        {
                            close(clientfd);
                            std::cout << "set clientfd to nonblock error." << std::endl;
                        }
                        else
                        {
                            struct pollfd client_fd_info;
                            client_fd_info.fd = clientfd;
                            client_fd_info.events = POLLIN;
                            client_fd_info.revents = 0;
                            fds.push_back(client_fd_info);
                            std::cout << "new client accepted, clientfd: " << clientfd << std::endl;
                        }
                    }
                }
                else
                {
                    //普通clientfd,收取数据
                    char buf[64] = {0};
                    int m = recv(fds[i].fd, buf, 64, 0);
                    if (m <= 0)
                    {
                        if (errno != EINTR && errno != EWOULDBLOCK)
                        {
                            //出错或对端关闭了连接,关闭对应的clientfd,并设置无效标志位
                            for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++iter)
                            {
                                if (iter->fd == fds[i].fd)
                                {
                                    std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl;
                                    close(fds[i].fd);
                                    iter->fd = INVALID_FD;
                                    exist_invalid_fd = true;
                                    break;
                                }
                            }
                        }
                    }
                    else
                    {
                        std::cout << "recv from client: " << buf << ", clientfd: " << fds[i].fd << std::endl;
                    }
                }
            } else if (fds[i].revents & POLLERR)
            {
                //TODO: 暂且不处理
            }

        }// end  outer-for-loop

        if (exist_invalid_fd)
        {
            //统一清理无效的fd
            for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end();)
            {
                if (iter->fd == INVALID_FD)
                    iter = fds.erase(iter);
                else
                    ++iter;
            }
        }
    }// end  while-loop


    //关闭所有socket
    for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++iter)
        close(iter->fd);

    return 0;
}

        通过上面的示例代码,我们也能看出 poll 函数存在的一些缺点:

  • 在调用 poll 函数时,不管有没有有意义,大量的 fd 的数组被整体在用户态和内核地址空间之间复制;
  • 与 select 函数一样,poll 函数返回后,需要遍历 fd 集合来获取就绪的 fd,这样会使性能下降;
  • 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

三、epoll模型

        综合 select 和 poll 的一些优缺点,Linux 从内核 2.6 版本开始引入了更高效的 epoll 模型,本节我们来详细介绍 epoll 模型。

        要想使用 epoll 模型,必须先需要创建一个 epollfd,这需要使用 epoll_create 函数去创建:

#include <sys/epoll.h>

int epoll_create(int size);

        参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。

        有了 epollfd 之后,我们需要将我们需要检测事件的其他 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_ADDEPOLL_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_ctl 调用成功返回 0,调用失败返回 -1,我们可以通过 errno 错误码获取具体的错误原因。

        epoll_event 结构体的 data 字段的类型是 epoll_data_t,我们可以利用这个字段设置一个自己的自定义数据,它本质上是一个 Union 对象,在 64 位操作系统中其大小是 8 字节,其定义如下:

typedef union epoll_data
{
    void*		 ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

        创建了 epollfd,设置好某个 fd 上需要检测事件并将该 fd 绑定到 epollfd 上去后,我们就可以调用 epoll_wait 检测事件了,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 会立即返回。

        当 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 & EPOLLIN)
        {
        		// 处理可读事件
        }
        else if (epoll_events[i].events & EPOLLOUT)
        {
        		// 处理可写事件
        }
        else if (epoll_events[i].events & EPOLLERR)
        {
        		//处理出错事件
        }
    }
}

        通过前面介绍 poll 与 epoll_wait 函数的介绍,我们可以发现:

        epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);而 poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。

        当然,这并不意味着,poll 函数的效率不如 epoll_wait,一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,epoll_wait 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。

四、LT模式和ET模式

        与 poll 的事件宏相比,epoll 新增了一个事件宏 EPOLLET,这就是所谓的边缘触发模式Edge Trigger,ET),而默认的模式我们称为水平触发模式Level Trigger,LT)。这两种模式的区别在于:

  • 对于水平触发模式,一个事件只要有,就会一直触发;
  • 对于边缘触发模式,只有一个事件从无到有才会触发。

        这两个词汇来自电学术语,你可以将 fd 上有数据认为是高电平,没有数据认为是低电平,将 fd 可写认为是高电平,fd 不可写认为是低电平。那么水平模式的触发条件是状态处于高电平,而边缘模式的触发条件是新来一次电信号将当前状态变为高电平,即:

        以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 EPOLLIN 事件;而对于边缘模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 EPOLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 EPOLLOUT 事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值