Redis 是cs架构(服务端-客户端),典型的一对多的服务器应用程序。多个客户通过网络与Redis服务器进行通信。那么在linux环境中是使用epoll(我们也 只讨论linux环境的,便于学习)。
通过使用I/O多路复用技术, redis 服务器使用单线程单进程的方式处理命令请求,并与多个客户端连接进行网络通讯。
redis的网络框架和Muduo是很相似的,若是弄懂了Muduo后在来看Redis的网络部分,那是很轻松的。这里推荐下我写的cppServer程序,有很详细的章节解说。0.仿造muduo,实现linux服务器开发思路
看懂这个后,很有助于看懂Muduo和Redis的网络部分。
//server.c
int main(int argc, char **argv) {
//省略很多.......
initServer();
//省略很多.......
//server.el 类型是struct aeEventLoop*
aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}
目前,main()函数中关于网络的主要就这几个函数,其他的先省略了。
而要介绍这几个函数,需要先介绍一些数据结构才行。
这里就介绍aeEventLoop,aeFileEvent,aeTimeEvent,aeFiredEvent4个结构体(都在ae.h文件中)。
aeEventLoop是重中之重,带出其他三个结构体。
这个就类似我写的程序里面的EventLoop。
aeEventLoop是Reactor模型的具体抽象,把网络读写事件和时间事件(定时器任务)可以统一到一起处理。
在Redis中是使用链表存储定时器任务的。timeEventHead表示的是定时器链表的头结点。而由于每次定时器任务结点都是从链表头部插入的,所以timeEventHead记录的是最晚插入的结点。(因为是直接插入放在头结点,所有对链表进行排序,所以头结点不一定是最早超时的任务)
beforesleep和aftersleep是每次事件循坏之前和之后需要执行的回调函数。(即是调用epoll_wait()前后)
apidata封装来具体的IO多路复用的系统调用。linux主要有select、poll、epoll,在Redis代码文件中分别对应ae.select.cc、ae_evport.cc、ae_epoll.cc。(这里我们只分析epoll)。
//ae.h
// 事件循环
typedef struct aeEventLoop {
int maxfd; //目前已注册的最大文件描述符fd
int setsize; //能注册的最大描述符数
long long timeEventNextId; //下一个要注册的时间事件id
time_t lastTime; //最后一次执行时间事件的时间
aeFileEvent *events; //是数组,已注册的文件事件 (就是IO event)
aeFiredEvent *fired; //数组,已就绪的文件事件
aeTimeEvent *timeEventHead; //定时器链表的头结点
int stop; //eventLoop的开关
void *apidata; //多路复用库的私有数据(epollfd相关的数据)
aeBeforeSleepProc *beforesleep;//在处理事件前要执行的回调函数(即是在执行epoll_wait()之前)
aeBeforeSleepProc *aftersleep;//在处理事件后要执行的回调函数(即是在执行epoll_wait()之后)
int flags; //设置的标识位
} aeEventLoop;
//文件事件结构
typedef struct aeFileEvent {
//监听事件类型,即是关注的事件
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc; //读事件的回调函数
aeFileProc *wfileProc; //写事件的回调函数
void *clientData;
} aeFileEvent;
//已就绪的事件
typedef struct aeFiredEvent {
int fd; //已就绪文件描述符
int mask; //事件类型
} aeFiredEvent;
要说说回调函数的类型。
//ae.h
//回调函数类型
//用c++11表示的话 using aeFileProc=std::function;
//IO读写事件回调函数
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
//定时器事件回调函数
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
//删除定时事件的回调函数
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
//进入循环等待之前的回调函数
typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop);
//ae.h
//时间事件结构
typedef struct aeTimeEvent {
long long id; //时间事件的唯一标识符,自增
//事件的到达时间(即是执行时间)
long when_sec; /* seconds */
long when_ms; /* milliseconds */
//事件处理函数 (到期执行的回调函数)
aeTimeProc *timeProc;
//事件释放函数 (回调函数)
aeEventFinalizerProc *finalizerProc;
void *clientData; //多路复用库的私有数据
//双向链表
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
int refcount; //以防止计时器事件在递归时间事件调用中释放
} aeTimeEvent;
那么,我们接着看回main()函数中的关于网络的部分。
initServer()函数
void initServer(void) {
//省略了很多不相关的代码.......
//创建evEventLoop ,也可以说是创建epoll
//server.el是aeEventLoop*类型
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
//ipfd是服务器端fd,是数组,因为有IPV4和IPV4,
listenToPort(server.port,server.ipfd,&server.ipfd_count);//函数内部调用sokcet(),bind(),listen()...
for (j = 0; j < server.ipfd_count; j++) {
aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL);
}
}
其主要4步:
1.分配内存给eventloop;
2.eventloop的成员进行初始化,
3.创建epollfd(调用epoll_create());
4.初始化要关注的事件类型,初始化阶段什么时间也没有关注。
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
int i;
//1.分配内存 zmalloc()函数是Redis封装的函数,可以理解为是malloc()
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
//创建数组,可见数组的长度就是setsize
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
//2.初始化
eventLoop->setsize = setsize;
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
eventLoop->flags = 0;
//3.创建epollfd
if (aeApiCreate(eventLoop) == -1) goto err;
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
/*
* 可以看到数组长度就是setsize,同时创建之后将每一个event的mask属性置为AE_NONE(即是0),
*对于eventLoop->events数组来说,fd就是这个数组的下标。
*例如,当程序刚刚启动时候,创建监听套接字,按照标准规定,该fd的值为3。此时就直接在
eventLoop->events下标为3的元素中存放相应event数据。
*不过也基于文件描述符的这些特点,意味着events数组的前三位一定不会有相应的fd赋值。
*/
//4.初始化,什么事件也没有关注,mask=AE_NONE
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
err:
if (eventLoop) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
}
return NULL;
}
epoll的封装都在ae_epoll.c文件中。这个是封装了I/O多路复用。
主要是封装一些epoll的函数(epoll_create,epoll_ctl)。
aeEventLoop中的apidata在epoll中表示为aeApiState。结构体aeApiState中的epfd为epoll的fd,events表示接受事件循环epoll_wait返回的触发读写的网络事件。
调用epoll_create()得到epfd,并将aeApiState数据赋值给eventLoop->apidata。
重置eventLoop->apidata的events的大小。
调用epeoll_ctl(),根据mask对fd进行添加或修改。
调用epoll_ctl(),对fd进行删除。
调用epoll_wait(),得到已就绪的events,并将已就绪的events赋值给eventLoop->fired。
//ae_epoll.c
typedef struct aeApiState {
int epfd; //这个是epoll_create()返回来的fd
struct epoll_event *events; //用在epoll_wait(int epfd,epoll_event* events)函数内的参数
} aeApiState;
//创建epollfd,调用epoll_create()
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
eventLoop->apidata = state;//epoll的数据aeApiState
return 0;
}
//重置events的大小
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);
return 0;
}
//添加fd到epoll上,调用epoll_ctl()
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}
}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
//赋给已就绪的fired事件
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
创建好EventLoop后,需要进行服务器的初始化,调用socket(),bind()等等操作。
bindaddr是个元素是char*的数组,元素是服务器需要绑定的ip。
为了便于分析,假定server.bindaddr_count是0,那么IPv4和IPv6都需要绑定。
//in server.c
//调用例子 listenToPort(server.port,server.ipfd,&server.ipfd_count)
int listenToPort(int port, int *fds, int *count) {
int j;
if (server.bindaddr_count == 0) server.bindaddr[0] = NULL;
for (j = 0; j < server.bindaddr_count || j == 0; j++) {
if (server.bindaddr[j] == NULL) {
/* Bind * for both IPv6 and IPv4, we enter here only if
* server.bindaddr_count == 0. */
fds[*count] = anetTcp6Server(server.neterr,port,NULL,
server.tcp_backlog);
if (fds[*count] != ANET_ERR) {
anetNonBlock(NULL,fds[*count]); //设置非阻塞
(*count)++;
}
if (*count == 1) {
/* Bind the IPv4 address as well. */
fds[*count] = anetTcpServer(server.neterr,port,NULL,
server.tcp_backlog);
if (fds[*count] != ANET_ERR) {
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
}
}
if (fds[*count] == ANET_ERR) {
//省略一些错误处理和打印日志
return C_ERR;
}
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
return C_OK;
}
这里需要调用anetTcpServer()函数和anetTcp6Server()函数。
//in anet.c
int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{
return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}
int anetTcp6Server(char *err, int port, char *bindaddr, int backlog)
{
return _anetTcpServer(err, port, bindaddr, AF_INET6, backlog);
}
static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
int s = -1, rv;
char _port[6]; /* strlen("65535") */
struct addrinfo hints, *servinfo, *p;
snprintf(_port,6,"%d",port);
memset(&hints,0,sizeof(hints));
hints.ai_family = af;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; /* No effect if bindaddr != NULL */
if ((rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)) != 0) {
anetSetError(err, "%s", gai_strerror(rv));
return ANET_ERR;
}
/* getaddrinfo() returns a list of address structures.
Try each address until we successfully bind(2).
If socket(2) (or bind(2)) fails, we (close the socket
and) try the next address. */
//其中有绑定一个成功就会跳出for循环的
for (p = servinfo; p != NULL; p = p->ai_next) {
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) //创建sockdfd
continue;
if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;
if (anetSetReuseAddr(err,s) == ANET_ERR) goto error; //设置端口服用
if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR; //绑定并开始监听
goto end;
}
if (p == NULL) {
anetSetError(err, "unable to bind socket, errno: %d", errno);
goto error;
}
error:
if (s != -1) close(s);
s = ANET_ERR;
end:
freeaddrinfo(servinfo);
return s;
}
绑定并进行listenn()监听后,就到了aeCreateFileEvent(),进行事件注册,添加FileEvent到eventLoop中。
事件一共有两类:
IO事件的注册通过aeApiAddEvent函数将套接字及其事件处理函数注册到epoll中。
//in ae.c
//根据mask参数的值, 创建文件事件
// 监听fd 文件的状态,
//当fd可用时,执行proc函数(即是回调函数)
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];
if (aeApiAddEvent(eventLoop, fd, mask) == -1)//使用epoll的话即是调用epoll_ctl
return AE_ERR;
//设置文件事件类型,以及事件的处理器(即是设置回调函数)
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
if (fd > eventLoop->maxfd) //若符合,则更新maxfd
eventLoop->maxfd = fd;
return AE_OK;
}
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{
if (fd >= eventLoop->setsize) return;
aeFileEvent *fe = &eventLoop->events[fd];
if (fe->mask == AE_NONE) return;
/* We want to always remove AE_BARRIER if set when AE_WRITABLE
* is removed. */
if (mask & AE_WRITABLE) mask |= AE_BARRIER;
aeApiDelEvent(eventLoop, fd, mask); //调用epoll_ctlr(del)
fe->mask = fe->mask & (~mask);
if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
/* Update the max fd */
int j;
for (j = eventLoop->maxfd-1; j >= 0; j--) //判断最大的fd的mask
if (eventLoop->events[j].mask != AE_NONE) break;
eventLoop->maxfd = j;
}
}
//创建时间事件
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
long long id = eventLoop->timeEventNextId++; //更新时间计数器,这个id是时间事件的唯一标识符
aeTimeEvent *te;
te = zmalloc(sizeof(*te));
if (te == NULL) return AE_ERR;
te->id = id;
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms); //设置到期时间
//设置事件处理器,即是设置回调函数
te->timeProc = proc;
te->finalizerProc = finalizerProc;
//设置私有数据
te->clientData = clientData;
te->prev = NULL;
te->next = eventLoop->timeEventHead; //将新事件放入表头
te->refcount = 0;
if (te->next)
te->next->prev = te;
eventLoop->timeEventHead = te;
return id;
}
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
aeTimeEvent *te = eventLoop->timeEventHead; //这是个链表
while(te) { //需要从头找到尾
if (te->id == id) {
te->id = AE_DELETED_EVENT_ID;
return AE_OK;
}
te = te->next;
}
return AE_ERR; /* NO event with the specified ID found */
}
那么现在,通过 aeCreateFileEvent(server.el,server.ipfd[j],AE_READABLE,acceptTcpHandler,NULL);将服务器的监听fd和其事件回调函数acceptTcpHandler注册到epoll中,并监听读事件。(acceptTcpHandler函数先不细讲,这个函数肯定是有调用accept()的)
启动aeMain函数阻塞等待事件发生,并处理。
//文件事件 1
#define AE_FILE_EVENTS (1<<0)
//时间事件 2
#define AE_TIME_EVENTS (1<<1)
//文件事件和时间事件 4
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
//不阻塞等待标识 8
#define AE_DONT_WAIT (1<<2)
#define AE_CALL_BEFORE_SLEEP (1<<3)
#define AE_CALL_AFTER_SLEEP (1<<4)
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
该函数就是一个while()循环,循环内部是aeProcessEvents函数。
下面注重来看下aeProcessEvents,第一个参数是要处理的事件驱动框架,第二个参数是要处理的事件类型,在aeMain中表示处理包括IO事件和时间事件在内的所有事件以及回调前置函数beforesleep和回调后置函数aftersleep。
aeProcessEvents的主要步骤:
回调函数beforsleep和aftersleep可以先省略不关注的。因为目前其还没有用途,先知道有这两个回调函数就行。
1.计算epoll_wait()需要的阻塞时间。
如果是设置了AF_DONT_WAIT,那就是不阻塞,epoll_wait()的超时时间就设置为0。如有定时器任务,那么其阻塞时间即是定时器的最早超时时间,这样可以防止定时器任务等待过久。
若是没有定时器任务,那就永远等待下去,直到有事件被触发。
2.执行beforsleep
在epoll_wait()阻塞之前执行一些任务,防止因为阻塞时间过长而无法执行,或者执行一些准备工作。
3.epoll_wait等待事件发生。
4.执行aftersleep。
5.处理发生的IO事件。
根据发生的事件类型来调用对应的回调函数。若是AE_READABLE类型调用rfileProc,若是AE_WRITABLE类型调用wfileProc。一般来说,先处理AE_READABLE类型事件,该类事件一般为客户端连接或者命令,然后处理AE_WRITABLE类型事件向客户端发送响应对于客户端的回复,一般在beforesleep中就会执行完成)
6.处理时间事件。若该时间事件时周期性的,执行完后会再添加到时间事件链表的。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
//没有时间事件和文件事件,退出
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
//1.计算epoll_wait()需要的阻塞时间
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) //表示有时间事件且需要阻塞,epoll_wait()设置的时间就不是0
shortest = aeSearchNearestTimer(eventLoop); //找到最近的定时器超时时间
if (shortest) {
long now_sec, now_ms;
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
//定时器的到期时间,毫秒数
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
if (ms > 0) {
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
} else {
//不需要阻塞
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
//再次确认
if (eventLoop->flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
}
//2.执行beforesleep
if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
eventLoop->beforesleep(eventLoop); //在休眠前执行(即是在epoll_wait()前)
/* Call the multiplexing API, will return only on timeout or when
* some event fires. */
//3.执行epoll_wait
numevents = aeApiPoll(eventLoop, tvp);
//4.执行aftersleep
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);//在休眠后执行(即是在epoll_wait()之后)
//5.逐个处理触发的事件
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask; //这个是epoll_wait()返回的触发事件类型
int fd = eventLoop->fired[j].fd;
int fired = 0; /* Number of events fired for current fd. */
int invert = fe->mask & AE_BARRIER;
//触发可读事件
if (!invert && fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
}
//可写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
/* If we have to invert the call, fire the readable event now
* after the writable one. */
if (invert) {
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
if ((fe->mask & mask & AE_READABLE) &&
(!fired || fe->wfileProc != fe->rfileProc))
{
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
processed++;
}
}
//6.处理时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}