select、poll、epoll的使用

select、poll、epoll的使用

首先我们从数据结构上的不同来看待linux给我们提供的用于监控fd是否处于读写状态的API之间的差异:

1. select

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

select允许程序监控三种类型的fd: 可读的fd、可写的fd、异常的fd,nfds=1+max(*readfds,*writefds,*exceptfds);

第五个参数是一个时间相关的结构体,若为NULL,则select函数执行检测后(无论fd是否做好IO的准备)立即返回,若非空且时间>0,函数会阻塞对应的时间才返回,这阻塞的时间就是在等待fd是否装备好进行对应的IO,一旦检测到事件发生则立即返回。这种反复地检测是否有IO事件发生的工作就叫做轮询。所以我们一般这么用这个接口:

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
    
    
    struct timeval waitTime = {
    
    0};
	fd_set readfds;
    char buffer[1024] = {
    
    0};
    
	FD_ZERO(&readfds);//先将文件描述符集合置空
	FD_SET(0,&readfds);//将代表标准输入的文件描述符0,加入到集合readfds中
    
	waitTime.tv_sec = 10;//阻塞10秒
    waitTime.tv_usec = 1;
  
	if (-1 != select(1,&readfds,NULL,NULL,&waitTime))
	{
    
    
		if (FD_ISSET(0,&readfds))//检测标准输入是否有可读事件
			read(0,buffer,1024);
			printf("\r\n\n read content:%s\r\n",buffer);
			printf("left time:\n\tleft sec:%ld,\n\tleft usec:%ld\n",waitTime.tv_sec,waitTime.tv_usec);//打印剩余时间
	}
	else
	{
    
    
		printf("select ret error:[%s]\r\n",strerror(errno));
	}
    
    return 0;
}

我们执行程序,在第8秒后往终端输入“select testing…”并回车,程序执行结果如下:

root@zyb-ubt:/home/zyb/CODE/LINUX_API# ./select_t
select testing............


 read content:select testing............

left time:
        left sec:2,
        left usec:624402
root@zyb-ubt:/home/zyb/CODE/LINUX_API#

可见,select在阻塞的第8秒检测到了可读的事件后返回,而程序员需要检测该事件是否发生在自己想要监控的文件上。

所以,select的核心数据结构是什么?是文件描述符集合,即fd_set,通过FD_SET可以将要监控的fd都加到该集合中,这样我们就能批量地监听fd,效率会更高效一点。

2. poll

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

//poll接口的核心数据结构
struct pollfd {
    
    
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

#define nfds_t usigned int

poll这个接口,功能大体上来说,和select一模一样,都是批量检测。
参数1是一个类型为struct pollfd的数组的首地址,参数2代表了该数组的成员个数,nfds的类型为unsiged int。

由select的接口我们可以知道,它只能检测三种事件:可读、可写、异常;而poll给每个fd提供一个请求监控事件位图events和一个检测到的事件的位图revents

POLL支持的具体事件:

POLLIN
    有数据可读
POLLPRI
    若fd是tcp的套接字,则表明此fd上有带外数据(优先级数据)可读
    
POLLOUT
    可写数据
    
POLLRDHUP (since Linux 2.6.17)
    面向连接的套接字的对端半关闭了写连接(只关闭写,不关闭读)
    
POLLERR
    fd上发生了错误条件(比如企图写一个已经关闭了读端的管道)。该状态只应该在返回事件中出现。
    
POLLHUP
    挂起;该状态只应该在返回事件中出现
        当读一个通道或者流套接字,此事件表明对端关闭了通道,在数据读完后继续读的话则返回0(文件结束标志)POLLNVAL
        无效的请求:fd未打开
        
若编译的时候开启了宏_XOPEN_SOURCE,则还有如下事件:
POLLRDNORM
        等同于POLLIN。
        
POLLRDBAND
        有优先级数据可读。
        
POLLWRNORM
        等同于POLLOUT。
        
POLLWRBAND
        可以写入优先级数据。

这样poll不仅能批量检测,还能详细地设置每个文件描述符的请求事件,也能通过返回的事件知道该fd上此刻发生的具体事件。而select的事件,相比之下粒度就粗糙了许多。

3. epoll

epoll,可以工作在两个模式:边缘触发、水平触发,此接口的核心数据结构是一个内核维护的实例,从用户态看来该实例就像两个链表:兴趣链、就绪链表

3.1 兴趣链

兴趣链是一个集合,是进程注册到监控中的文件描述符的集合。

3.2 就绪链

就绪链也是一个集合,它代表了能够进行I/O的fd的集合,所以理所当然,它是兴趣连集合中的一个子集。

3.3 相关API

int epoll_create(int size);//创建一个epoll实例,并返回引用该实例的fd
//在linux2.6.8以后size参数就没用了,但它又必须是一个>=0的值(为了向下监兼容)

int epoll_create1(int flags);//...
/*flag:EPOLL_CLOEXEC,执行时关闭。这是因为在多进程环境中,通过fork后的子进程共享附件的打开文件资源,那么就有可能造成父子进程共同操作一个文件导致未知结果。为了避免出现这种情况就要将文件设置为打开时关闭。*/

int close(int fd);/*当通过epoll_create(1)创建的epoll实例不再需要了,就调用close对其进行释放。
当该epoll涉及的所有的fd都被关闭,则该实例就被内核销毁并释放该资源。*/
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//向epoll实例中的兴趣链增加fd成员
  1. int epfd
    引用epoll实例的fd。

  2. int op
    (1) EPOLL_CTL_ADD
    向epoll实例的兴趣链中增加一个元素,该元素由文件描述符fd和相关的事件event组成。
    ​ (2) EPOLL_CTL_MOD
    ​ 改变epoll实例的兴趣链中,fd所关联的事件event
    ​ (3) EPOLL_CTL_DEL
    ​ 从epoll实例的兴趣链中移除fd所关联的事件event

  3. int fd

​ 想进行增、改、删操作的fd

  1. struct epoll_event *event
//具体结构
typedef union epoll_data {
    
    
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    
    
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

成员data为内核保存的数据,当内核检测到I/O就绪,就会把该数据返回。(为业务而生)

成员events,事件位图。

epoll支持的事件类型如下:

EPOLLIN
	  可读
EPOLLOUT
	  可写
EPOLLRDHUP (since Linux 2.6.17)
	  面向连接的套接字的对端半关闭了写连接(只关闭写,不关闭读)
EPOLLPRI
	  若fd是tcp的套接字,则表明此fd上有带外数据(优先级数据)可读
EPOLLERR
	 fd上发生了错误条件(比如企图写一个已经关闭了读端的管道)。该状态只会在返回事件中出现。

EPOLLHUP
	 挂起;该状态只应该在返回事件中出现
	 当读一个通道或者流套接字,此事件表明对端关闭了通道,在数据读完后继续读的话则返回0(文件结束标志)

EPOLLET
      请求以边沿触发的模式来监控fd;epoll的默认行为是水平触发的。此标志只用于event.events
          
EPOLLONESHOT (since Linux 2.6.2)
 	使用此标志,设置fd及其事件后,一旦该事件首次被内核返回后,就失效了,即一次性的事件通知。若要再次对该fd设置监听,则需要再次调用epoll_ctl来下发。
          
EPOLLWAKEUP (since Linux 3.5)
	 如果清除EPOLLONESHOT和EPOLLET并且该进程具有CAP_BLOCK_SUSPEND功能,请确保在此事件挂起或正在处理时,系统不会输入“挂起”或“休眠”。
从调用epoll_wait(2)返回该事件开始到对该同一个epoll(7)文件描述符进行下一次调用epoll_wait(2)的事件被视为“已处理”,该文件描述符的关闭,
使用EPOLL_CTL_DEL删除事件文件描述符,或使用EPOLL_CTL_MOD清除事件文件描述符的EPOLLWAKEUP。
另请参阅错误。

调用epoll_ctl();时,此标志是event.events字段的输入标志。
它永远不会由epoll_wait(2)返回。

EPOLLEXCLUSIVE (since Linux 4.5)
    为附加到目标文件描述符fd的epoll文件描述符设置排他唤醒模式。
当发生唤醒事件并且使用EPOLLEXCLUSIVE将多个epoll文件描述符附加到同一目标文件时,一个或多个epoll文件描述符将收到epoll_wait(2)事件。

在这种情况下(未设置EPOLLEXCLUSIVE时)是默认所有的epoll文件描述符都接收事件。
因此,EPOLLEXCLUSIVE在避免某些情况下的惊群问题方面很有用。

如果相同的文件描述符存在于多个epoll实例中,其中一些带有EPOLLEXCLUSIVE标志,而另一些没有,则事件将提供给所有未指定EPOLLEXCLUSIVE的epoll实例,以及至少一个指定了EPOLLEXCLUSIVE的epoll实例。

可以与EPOLLEXCLUSIVE一起指定以下值:EPOLLIN,EPOLLOUT,EPOLLWAKEUP和EPOLLET。
也可以指定EPOLLHUP和EPOLLERR,但这不是必需的:通常,无论是否在事件中指定了这些事件,都始终报告这些事件。
尝试在事件中指定其他值会产生错误EINVAL EPOLLEXCLUSIVE,只能在EPOLL_CTL_ADD操作中使用;

尝试将其与EPOLL_CTL_MOD一起使用会产生错误。
如果已使用epoll_ctl()设置了EPOLLEXCLUSIVE,则在同一epfd,fd对上的后续EPOLL_CTL_MOD会产生错误。
调用epoll_ctl()会在事件中指定EPOLLEXCLUSIVE并将目标文件描述符fd指定为epoll实例,这同样会失败。
在所有这些情况下,错误均为EINVAL。

当调用epoll_ctl();时,EPOLLEXCLUSIVE标志是event.events字段的输入标志。
它永远不会由epoll_wait(2)返回。
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
//等待I/O事件,即从就绪链中获取成员,直到获取到maxevents(>0)个事件。该接口是阻塞的。
int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,
                const sigset_t *sigmask);//直到返回要求的事件或者捕捉到某些信号就返回

边缘模式(ET)和水平模式(LT)

man手册是这样描述的,如下步骤:

1).在epoll实例中注册了一个rfd,此rfd是一个管道的读端。

2).该管道的写端向管道写入2kb的数据。

3).程序中调用epoll_ctl,且其返回了rfd可读。

4).管道从rfd中读取了1kb的数据。

5).程序再次对epoll_wait()的调用。

ET

若rfd注册到epoll实例中是以边缘触发的模式,则在第五个步骤程序可能会阻赛,即使管道的输入缓冲区中仍然存在可读数据。同时,对端也可能期望基于已发送的数据得到响应(这边阻赛,那边也等待响应,形成“死锁”)。

这是因为,边缘触发模式仅仅在注册的fd上发生了改变时才传递事件,然而,在第五个步骤后,程序却一直在等待已经存在于输入缓冲区的数据。(数据已经送来了,程序却不知道只能傻等)

在上面的例子中,一个可读事件由步骤2产生,在步骤3中该可读事件被消耗,但是在第4个步骤中数据并没有被完全读取,造成第5个步骤可能会永久阻赛。

如何避免?1)使用边缘触发模式的fd应该设置为非阻塞的,2)仅在read或write返回EAGAIN之后等待事件。

由此,ET可以帮助多个等待事件就绪的fd避免惊群。

LT

当使用水平触发模式时,epoll就像是一个更快的poll。

ET = LT+EPOLLONESHOT

总结

以上三个接口用的不多,使用过select和poll,epoll的使用更有考究,待后续有深刻的理解后再增加上。

猜你喜欢

转载自blog.csdn.net/dengwodaer/article/details/112505743
今日推荐