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;
}
这里还需要注意一点,往往我们需要一直通过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其实也有一定的流程规范,大致上包括:
下面我们来讲讲这各个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
好了,以上就是本篇博文的全部了,如果觉得这篇文章有用的话,不妨点赞转发分享一下吧,谢谢~