Linux系统调用epoll与select使用详解

讲解关于epoll与select的使用,掌握多路I/O复用~
Views: 964
1 0
Read Time:3 Minute, 46 Second

在以前的项目中,经常会需要对File Descriptor(注意:I/O在Linux内形式上也为文件)进行监听,当其发生变化时我们可以监控到相关信息,之后会进行相关操作。那时候很喜欢通过select的系统调用来达成监听目的(因为写法固定,方便记忆),后来在看安卓系统InputFlinger内的EventHub时看到了关于epoll的使用,于是就仔细看了一下这个这个函数,对比了一下两者间的差异,那这篇文章就是我学习整理的关于这两个常用系统调用的一些笔记。

核心知识点:

  • select与epoll都可用于I/O多路复用
  • select内部采取轮询的方式获取文件描述符的状态信息,效率较低
  • epoll采用事件驱动型的通知方式,效率较高
  • select的监听存在上限,而epoll则没有(在系统资源允许的情况下)

首先来介绍一下select,select函数的原型为:int select(int nfds, fd_set readfds, fd_set writefds, fd_set * exceptfds,struct timeval *timeout);

参数解释:

nfds :该参数应该为 readfds 、 writefds 、 exceptfds 这三个fd集合中的最大值+1。

readfds :该fd集合表明我们需要监听可读的文件描述符的集合,如果这些文件中有一个文件可读,select语句则会返回一个大于0的值(表示文件可读),返回后清除掉其中不可读的文件描述符。如果无文件可读,则会根据timeout参数判断单次监听周期是否到期,到期返回0值,发生错误返回负值。在select正确返回后,我们可以进行正常的recv与read操作进行数据读取。

writefds :与readfds类似,不过这个集合是表明需要监听可写状态的文件描述符。在select返回后,会清除掉不可写的文件描述符,同样也可通过timeout参数设定超时时间。一般之后我们会通过send或者write进行数据写入。

exceptfds :这是一个可以用于某些特殊情况而可以排除在外的文件描述符集合。

timeout :该参数可以为select的监听设置一个超时时间。

除此之外,select的errno信息包含以下几个:

  • EBADF,表明文件描述符损坏
  • EINVAL,表明参数不合法
  • EINTER,表明接收到了中断信号(Sig=2,代表Interrupt)
  • ENOMEM,表明系统没有足够的内存

通常在我们使用select语句时,还会搭配其他函数进行使用,一般需要搭配FD_SET(int fd,fd_set *set),FD_ZERO(fd_set *set),FD_ISSET(int fd,fd_set *set)这三个函数。其中 FD_SET 函数用于将我们需要监听的文件描述符绑定到set这个文件描述符结合中, FD_ZERO 则用于在绑定前将set内所有的文件描述符给清除掉,该函数一般是初始化set时就需要进行调用。此外还有一个FD_ISSET函数,当select语句返回时,一般需要通过ISSET函数判定我们监听的文件描述符还在该set内。

这里放上一段代码,帮助大家理解:

    msKeyEventFd=open("/dev/input/event3",O_RDONLY) 
    if(msKeyEventFd<=0)
    {
        ALOGE("Open /dev/input/event3 error,error:%s",strerror(errno));
        goto ↓RETURN;
    }
    ALOGI("Load event dev file success, start handle event");
    for (;;)
    {
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;
        FD_ZERO(&fds);
        FD_SET(msKeyEventFd, &fds);
        FD_SET(msTouchEventFd, &fds);
        switch (select(msKeyEventFd + 1, &fds, NULL, NULL, &timeout))
        {
        case -1:
            ALOGE("%s select error:%s", __FUNCTION__, strerror(errno));
            break;
        case 0:
            //ALOGD("%s select timeout", __FUNCTION__);
            break;
        default:
            if (FD_ISSET(msKeyEventFd, &fds))
            {
                getInstance()->handleKeyEvent();
            }
            break;
        }
    }
RETURN:
    if (msKeyEventFd > 0)
    {
        close(msKeyEventFd);
        msKeyEventFd = -1;
    }

这里还需要注意一点,往往我们需要一直通过select进行,是需要循环体来实现的,在这里需要将 timeout 的时间设定放在循环体内。如果timeout全部设置为0,则select语句会立刻返回为0值,如果为空,则会一直阻塞直到监听的文件描述符准备好。与select类似的还有pselect,可以说pselect是select的衍生,两者在使用上可以进行以下形式上的转换:

使用pslect:

ready = pselect(nfds, &readfds, &writefds, &exceptfds,
timeout, &sigmask);

其等价于:

sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

通过 sigmask 参数,pselect可以捕获信号,以防止打断对文件描述符的监听,这在某些时刻是有必要的,比如安卓Vold进程发送中断信号给持有资源的进程时,而该进程恰好还需要监听某个文件,使用pselect就可以防止监听被打断。

这里还需要注意点,select能够监听的文件描述符最大为FD_SETSIZE(1024),这对于当前的很多应用来讲会导致一定程度上局限,而epoll是没有这个限制的。

接下来我们讲讲epoll,使用epoll需要引入头文件#include <sys/epoll.h>,epoll其实来源于poll函数,关于poll不在本文中赘述,感兴趣的朋友可以自行搜索相关资料。

epoll与select最大的不同在于,epoll监听I/O变化是由事件触发的,而select则是通过轮询去查看文件是否准备好的。如果在同一时刻,你需要监听多个文件描述符,select的轮询所耗费的时间是不可忽略的,而epoll则没有这个问题。

epoll的事件触发可以包含边缘触发与水平触发,表现在模拟电路上你可以理解为是受上升沿/下降沿触发还是受高电平/低电平触发

与select类似,使用epoll其实也有一定的流程规范,大致上包括:

  • epoll_create:创建一个epoll实例并返回一个文件描述符用于指代该实列
  • epoll_ctl:通过epoll_ctl向epoll实例中添加你需要监听/移除、或修改的文件描述符
  • epoll_wait:进入监听状态,直至监听的对象存在I/O事件,这个监听过程是阻塞式的

下面我们来讲讲这各个API的使用。

关于epoll_create,在实际使用过程中通常我们使用的是epoll_create1,其作为epoll_create的拓展,函数原型为:int epoll_create1(int flags);

这里的flags用于对该文件描述符做一些设定,一般我们使用O_CLOEXEC,设置该标志的意义在于,可以确保在多线程的操作下保证无用的文件描述符会被及时关闭,这一点很重要。

关于这一点,摘录一段博文:

我们考虑这样的情况:父进程fork出一个子进程,子进程是父进程的副本,获得父进程的数据空间、堆和栈的副本,当然也包括父进程打开的文件描述符

fork之后一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文、数据、堆和栈等,此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会在fork子进程后,在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。

但是在复杂系统中,有时我们fork出子进程时已经不知道打开了多少文件描述符了(包括socket句柄等),如果进行逐一清理难度很大。我们期望的是能在fork出子进程前、打开某个文件描述符时就指定好——这个文件描述符在我fork出子进程后、执行exec时就关闭。其实是有这样的方法的:即所谓的 close-on-exec。

在epoll_create调用成功后,会返回0值,如果调用失败则会返回-1,同时产生相应的errno,在此不做赘述。

之后我们需要通过epoll_ctl加入我们需要监听的文件描述符,其函数原型为:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

各参数解释如下:

epfd :由epoll_create创建得到的文件描述符

op :代表对文件描述所执行的操作,可用值包括:EPOLL_CTL_ADD,表示添加;EPOLL_CTL_DEL,表示删除;EPOLL_CTL_MOD,表示修改

fd :我们需要添加/移除或者修改的文件描述符

event :用于设定epoll触发的事件通知,其实是一个结构体,结构体如下:

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 */
 };

在这个结构体中,events规定了事件类型,包含EPOLLIN,表明有数据输入,可读;EPOLLOUT,表明可以进行数据输出,可写;EPOLLRDHUP,表明socket对方关闭连接;EPOLLET,表明采用边缘触发方式,此外还有很多,在此不做赘述,而 data 则包含了此次event事件下的具体数据内容。

在epoll_ctl设定完成之后,就可以利用epoll_wait进入监听状态了。其函数原型为:int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);其中 epfd 为epoll_create创建得到的fd, events 的定义与上述一致,在实际使用时这里通常是一个数组或者列表, maxevents 代表单次wait所获取到的最大event数量, timeout 与select类似,用于设定超时时间,该超时时间的时钟基准是CLOCK_MONOTONIC .这里需要注意的是,epoll的监听返回与select类似,即:I/O状态变化,或者收到中断信号,或者是超时,同样的我们可以使用epoll_pwait来处理收到中断信号的情况。

这里我们可以看看安卓InputFlinger中EventHub对epoll的使用。

EventHub::EventHub(void) :
        mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1), mControllerNumbers(),
        mOpeningDevices(nullptr), mClosingDevices(nullptr),
        mNeedToSendFinishedDeviceScan(false),
        mNeedToReopenDevices(false), mNeedToScanDevices(true),
        mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {
    acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);

    mEpollFd = epoll_create1(EPOLL_CLOEXEC);
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));

    mINotifyFd = inotify_init();
    mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
    LOG_ALWAYS_FATAL_IF(mInputWd < 0, "Could not register INotify for %s: %s",
            DEVICE_PATH, strerror(errno));
    if (isV4lScanningEnabled()) {
        mVideoWd = inotify_add_watch(mINotifyFd, VIDEO_DEVICE_PATH, IN_DELETE | IN_CREATE);
        LOG_ALWAYS_FATAL_IF(mVideoWd < 0, "Could not register INotify for %s: %s",
                VIDEO_DEVICE_PATH, strerror(errno));
    } else {
        mVideoWd = -1;
        ALOGI("Video device scanning disabled");
    }

    struct epoll_event eventItem;
    memset(&eventItem, 0, sizeof(eventItem));
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mINotifyFd;
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance.  errno=%d", errno);
        // service the timeout.
        mPendingEventIndex = 0;

        mLock.unlock(); // release lock before poll, must be before release_wake_lock
        release_wake_lock(WAKE_LOCK_ID);

        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);

        acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);
        mLock.lock(); // reacquire lock after poll, must be after acquire_wake_lock

好了,以上就是本篇博文的全部了,如果觉得这篇文章有用的话,不妨点赞转发分享一下吧,谢谢~

Happy
Happy
0 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

文章: 88

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CNCN