Physical Address:
ChongQing,China.
WebSite:
在以前的项目中,经常会需要对File Descriptor(注意:I/O在Linux内形式上也为文件)进行监听,当其发生变化时我们可以监控到相关信息,之后会进行相关操作。那时候很喜欢通过select的系统调用来达成监听目的(因为写法固定,方便记忆),后来在看安卓系统InputFlinger内的EventHub时看到了关于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信息包含以下几个:
通常在我们使用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;}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);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);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其实也有一定的流程规范,大致上包括:
下面我们来讲讲这各个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 */};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 = <strong><span style="color: var(--theme-palette-color-1, #FB7258);" class="ugb-highlight">epoll_create1</span></strong>(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 = <strong><span style="color: var(--theme-palette-color-1, #FB7258);" class="ugb-highlight">epoll_ctl</span></strong>(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance. errno=%d", errno);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_lockrelease_wake_lock(WAKE_LOCK_ID);int pollResult = <strong><span style="color: var(--theme-palette-color-1, #FB7258);" class="ugb-highlight">epoll_wait</span></strong>(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// 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
好了,以上就是本篇博文的全部了,如果觉得这篇文章有用的话,不妨点赞转发分享一下吧,谢谢~