IO多路复用——select、poll和epoll

select

#include 

int select(int maxfdp1, fd_set *restrict readfds,
            fd_set *restrict writefds, fd_set *restrict exceptfds,
            struct timeval *restrict tvptr);
参数描述

select 的第一个参数 maxfdp1 的意思是“最大文件描述符编号值加 1”。考虑所有 3 个描述符集合,在这 3 个描述符集合中找到最大描述符编号值,然后加 1,就是第一个参数。

第二个到第四个参数 readfdswritefdsexceptfds 是指向描述符集合的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集合存储在一个 fd_set 数据类型中。这个数据类型是由实现选择的,它可以为每一个可能的描述符保持一位。我们可以认为它只是一个很大的字节数组。

这个 fd_set 类型的描述符集合可以用以下的函数来操作,向集合中增加或者删除一个关心的描述符。

#include 
int FD_ISSET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);

最后一个参数 tvptr 表示愿意等待的时间长度。如果超过这个时间,则不再等待。

返回值

select 有三个可能的返回值:

(1)返回 -1 表示出错。例如,在指定的描述符一个都没准备好的时候收到了一个信号。此情况下,一个描述符集都不修改。

(2)返回 0 表示没有描述符准备好。超时发生时会产生这样的状况。此时,所有描述符集都会置 0。

(3)一个正的返回值说明了已经准备好的描述符数。

poll

#include 
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

struct pollfd {
    int fd;
    short events;
    short revents;
};
参数描述

poll 并未分别为每一个条件(可读、可写和异常)构造一个描述符集,而是构造一个 pollfd 结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。

其中前两个参数就是指定了我们所感兴趣的描述符和其对应的事件以及数组的长度。第三个参数依旧是超时时间。

详细来说,poolfd 的第一个成员表示了要监听的描述符,第二个成员为我们关心的是每个描述符的哪些事件。返回时,revents 成员由内核设置,用于说明每个描述符发生了哪些事件。poll 并没有更改 events 成员。这与 select 不同,不通过修改其参数指示哪个描述符已经准备好了。

epoll

创建 epoll 实例
int epoll_create1(int flags);

flags:当前版本只支持 EPOLL_CLOEXEC 标志
我们也可以通过 epoll_create(int size) 这个函数来创建 epoll 实例,只不过这个函数中的 size 在 2.6.277 内核开始就不必要了:

SYSCALL_DEFINE1(epoll_create, int, size) {
    if (size <= 0)
        return -EINVAL;
    return sys_epoll_create1(0);
}
管理 epoll 事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:epoll 实例的 fd
  • op:操作标志
  • fd:监控对象的 fd
  • event:事件的内容

op 可以有三个值:

  • EPOLL_CTL_ADD : 添加监听的事件
  • EPOLL_CTL_DEL : 删除监听的事件
  • EPOLL_CTL_MOD : 修改监听的事件
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是一个联合体,能够存储fd或其它数据,我们需要根据自己的需求定制。events表示监控的事件的集合,是一个状态值,通过状态位来表示,可以设置如下事件:

  • EPOLLERR : 文件上发上了一个错误。这个事件是一直监控的,即使没有明确指定
  • EPOLLHUP : 文件被挂断。这个事件是一直监控的,即使没有明确指定
  • EPOLLRDHUP : 对端关闭连接或者shutdown写入半连接
  • EPOLLET : 开启边缘触发,默认的是水平触发,所以我们并未看到EPOLLLT
  • EPOLLONESHOT : 一个事件发生并读取后,文件自动不再监控
  • EPOLLIN : 文件可读
  • EPOLLPRI : 文件有紧急数据可读
  • EPOLLOUT : 文件可写
  • EPOLLWAKEUP : 如果EPOLLONESHOT和EPOLLET清除了,并且进程拥有CAP_BLOCK_SUSPEND权限,那么这个标志能够保证事件在挂起或者处理的时候,系统不会挂起或休眠
等待 epoll 事件的发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd : epoll实例的fd
  • events : 储存事件的数组首地址
  • maxevents : 最大事件的数量
  • timeout : 等待的最长时间

LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。听起来到时挺玄乎的,那么怎么区分这个Level和Edge呢?很简单,0->1这种类型的事件就是Edge,而Level则正好相反,1->1这种类型就是,由此可见,当缓冲区有数据可取的时候,ET会触发一次事件,之后就不会再触发,而LT只要我们没有取完缓冲区的数据,就会一直触发。

附上Demo:

#define MAX_EVENTS 10
struct epoll_event  ev, events[MAX_EVENTS];
int         listen_sock, conn_sock, nfds, epollfd;


/* Code to set up listening socket, 'listen_sock',
 * (socket(), bind(), listen()) omitted */

epollfd = epoll_create1( 0 );
if ( epollfd == -1 )
{
    perror( "epoll_create1" );
    exit( EXIT_FAILURE );
}

ev.events   = EPOLLIN;
ev.data.fd  = listen_sock;
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, listen_sock, &ev ) == -1 )
{
    perror( "epoll_ctl: listen_sock" );
    exit( EXIT_FAILURE );
}

for (;; )
{
    nfds = epoll_wait( epollfd, events, MAX_EVENTS, -1 );
    if ( nfds == -1 )
    {
        perror( "epoll_wait" );
        exit( EXIT_FAILURE );
    }

    for ( n = 0; n < nfds; ++n )
    {
        if ( events[n].data.fd == listen_sock )
        {
            conn_sock = accept( listen_sock,
                        (struct sockaddr *) &local, &addrlen );
            if ( conn_sock == -1 )
            {
                perror( "accept" );
                exit( EXIT_FAILURE );
            }
            setnonblocking( conn_sock );
            ev.events   = EPOLLIN | EPOLLET;
            ev.data.fd  = conn_sock;
            if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, conn_sock,
                    &ev ) == -1 )
            {
                perror( "epoll_ctl: conn_sock" );
                exit( EXIT_FAILURE );
            }
        } else {
            do_use_fd( events[n].data.fd );
        }
    }
}

作者:大呀大帝国
链接:https://www.jianshu.com/p/ee381d365a29
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

参考

[1] 大话 Select、Poll、Epoll

[2] Linux epoll 详解

[3] Epoll 的使用详解

[4] epoll内核源码详解+自己总结的流程

你可能感兴趣的:(操作系统)