mc_echo进程模型
mc_echo的进程模型的图例上一篇博客已经给出;主进程用于分发请求,调度进程用于调整处理进程的数目以及干涉主进程的请求分发,处理进程处理具体的请求。
mc_echo采用的是多进程的模型,处理进程使用复用IO的技术来处理来自客户端的各个请求,服务器在启动或重启时会根据配置文件的参数预先创建处理进程的一个进程池,当请求数目过多时mc_echo会增加进程池中的进程数目,当请求过少时会减少进程池中的进程数目。
mc_echo的event机制
mc_echo的复用IO使用kqueue,devpoll,epoll或者poll来实现,mc_echo采用autoconf来探测运行环境,一般而言freebsd使用kqueue,sunos使用devpoll,linux使用epoll,其他的unix系统使用poll。mc_echo的event机制采用如下结构体来实现:
#define MC_POLLFD_SIZE (sizeof(struct mc_pollfd))
typedef struct mc_pollfd *mc_pollfd;
struct mc_pollfd{
int fd;
short revents;
};
#define MC_EVENT_SIZE (sizeof(struct mc_event))
typedef struct mc_event *mc_event;
struct mc_event{
...
int (*free)(mc_event evp);
int (*poll)(mc_event evp, int timeout);
int (*add)(mc_event evp, int fd, int flags);
int (*del)(mc_event evp, int fd, int flags);
int (*traverse)(mc_event evp, mc_pollfd pollp);
};
struct mc_event是event机制最重要的结构,该结构体提供了一个调用各种不同事件函数的接口,以下一一解释每个函数的作用:
free: 当不再需要mc_event时调用该函数做一些清理
poll: 调用该函数后会陷入睡眠,直至超过指定的时间或者等待事件中的任一事件发现后被唤醒,函数返回-1或者事件数目
add: 增加一个事件
del: 删除一个事件
traverse: 该函数在poll返回后调用,依次遍历所有发生的事件,事件的信息存放在struct mc_pollfd中。
接下来以devpoll为例讲诉mc_echo的event实现,之所以不讲解epoll或者kqueue的实现,是因为devpoll的资料较少,而且很多人对devpoll的接口有一定的误解,所以此处以devpoll为入口讲解mc_echo的event。首先给出devpoll的poll函数实现:
int
mc_event_devpoll_poll(mc_event p, int timeout)
{
int n;
struct dvpoll event;
event.dp_fds = evp->fds;
event.dp_nfds = evp->nfds;
event.dp_timeout = timeout;
retry:
if((n = ioctl(p->fd, DP_POLL, &event)) == -1 && errno == EINTR)
goto retry;
p->offset = 0;
return (p->events = n);
}
devpoll使用ioctl来等待事件发生,ioctl成功返回事件数目,失败返回-1。struct dvpoll event是devpoll所用的结构体,用于指明需要等待的事件。
devpoll的add和del函数如下:
int
mc_event_devpoll_add(mc_event p, int fd, int flags)
{
struct pollfd event;
if(p->nfds >= p->maxfds)
return -1;
mc_memset(&event, 0, sizeof(event));
event.fd = fd;
if(flags & MC_EVENT_IN)
event.events |= POLLIN;
if(flags & MC_EVENT_OUT)
event.events |= POLLOUT;
if(mc_write(p->fd, &event, sizeof(event)) == -1)
return -1;
p->nfds++;
return 0;
}
int
mc_event_devpoll_del(mc_event p, int fd, int flags)
{
struct pollfd event;
mc_memset(&event, 0, sizeof(event));
event.fd = fd;
event.events = POLLREMOVE;
if(mc_write(p->fd, &event, sizeof(event)) == -1)
return -1;
p->nfds--;
return 0;
}
devpoll添加和删除事件的方式比较奇怪,采用的调用wirte系统调用的方式来增减事件,值得注意的是devpoll可以一次性注册n个事件,具体的办法是写入一个struct pollfd数组到对应的文件描述符。另外对于一个文件描述符而言,如果需要修改等待事件的性质,比如从等待可读变为等待可写,则需要移除事件后重新添加,devpoll不提供改变事件属性的函数(据我所知是没有)。
devpoll的遍历函数如下:
int
mc_event_devpoll_traverse(mc_event p, mc_pollfd pollp)
{
struct pollfd tm;
struct pollfd *pp;
while(p->offset < p->events){
pp = &p->fds[p->offset++];
pollp->fd = pp->fd;
tm.fd = pp->fd;
tm.events = tm.revents = 0;
retry:
switch(ioctl(p->fd, DP_ISPOLLED, &tm)){
case -1:
if(errno == EINTR)
got retry;
continue;
case 1:
if(tm.revents & (POLLHUP | POLLERR)){
pollp->revents = MC_EVENT_ERR;
}else if(tm.revents & POLLIN){
pollp->revents = MC_EVENT_IN;
}else if(tm.revents & POLLOUT){
pollp->revents = MC_EVENT_OUT;
}else
pollp->revents = 0;
return 0;
default:
continue;
}
}
return -1;
}
devpoll最最重要的是revents是不应该使用的,devpoll并不会在struct pollfd的revents中写入事件的状态。网上有一些代码会直接使用revents变量,这是不可取的,虽然这么做可能是有效的。最好的办法是使用ioctl来获取对应事件的状态,当然一般使用devpoll的代码并不用去关注事件的状态,只要知道对应的文件描述符存在即可。上面的traverse的代码提供了一个相对而言比较标准的devpoll的获取事件状态的办法,如果你需要使用devpoll可以参考一下。