写在转载之前的:
在nginx源码src/event/ngx_event.h中涉及了好几种网络模型:
windows select
windows IOCP
select
poll
epoll
devpoll
kqueue
eventport
废弃的:
glibc aio
rtsig
可以说不同的系统,使用的网络模型不尽相同,是时候对它们总结一下了。
这里介绍两种机制,它们跟select和poll这两个函数具备类似的特性。
/dev/poll接口
Solaris上名为/dev/poll的特殊文件提供了一个可扩展的轮询大量描述符的方法。select和poll存在的一个问题是,每次调用它们都得传递待查询的文件描述符。轮询设备能在调用之间维持状态,因此轮询进程可以预先设置好待查询描述符的列表,然后进入一个循环等待事件发生,每次循环回来时不必再次设置该列表。
打开/dev/poll之后,轮询进程必须先初始化一个pollfd结构(即poll函数使用的结构,不过本机制不使用其中的revents成员)数组,再调用write往/dev/poll设备上写这个结构数组以把它传递给内核,然后执行DP_POLL命令阻塞自身以等待事件发生。
传递给ioctl调用的结构如下:
struct dvpoll {
struct pollfd *dp_fds;
int dp_nfds;
int dp_timeout;
};
其中dp_fds成员指向一个缓冲区,供ioctl在返回时存放一个pollfd结构数组。dp_nfds成员指定该缓冲区。ioctl调用将一直阻塞到任何一个被轮询描述符上发生所关心的事件,或者流逝时间超过经由dp_timeout成员指定的毫秒数为止。dp_timeout指定为0将导致ioctl立即返回,从而提供了使用本接口的非阻塞手段。dp_timeout指定为-1表示没有超时设置。
以下利用/dev/poll机制对函数str_cli进行改编。
#include "unp.h"
#include
void
str_cli(FILE *fp, int sockfd)
{
int stdineof;
char buf[MAXLINE];
int n;
int wfd;
struct pollfd pollfd[2];
struct dvpoll dopoll;
int i;
int result;
wfd = open("/dev/poll", O_RDWR, 0);
pollfd[0].fd = fileno(fp);
pollfd[0].events = POLLIN;
pollfd[0].revents = 0;
pollfd[1].fd = sockfd;
pollfd[1].events = POLLIN;
pollfd[1].revents = 0;
write(wfd, pollfd, sizeof(struct pollfd) * 2);
stdineof = 0;
for ( ; ; ) {
dopoll.dp_timeout = -1;
dopoll.dp_nfds = 2;
dopoll.dp_fds = pollfd;
result = ioctl(wfd, DP_POLL, &dopoll);
for (i = 0; i < result; i++) {
if (dopoll.dp_fds[i].fd == sockfd) {
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return;
else
err_quit("str_cli: server terminated prematurely");
}
write(fileno(stdout), buf, n);
}
else {
if ((n = read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
shutdown(sockfd, SHUT_WR);
continue;
}
write(sockfd, buf, n);
}
}
}
}
kqueue接口
FreeBSD引入了kqueue接口。本接口允许进程向内核注册描述所关注kqueue事件的事件过滤器。事件除了与select所关注类似的文件I/O和超时外,还有异步I/O、文件修改通知(例如文件被删除或修改时发出的通知)、进程跟踪(例如进程调用exit或fork时发出的通知)和信号处理。kqueue接口有如下两个函数和一个宏。
#include
#include
#include
int kqueue(void);
int kevent(int kq,const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout);
void EV_SET(struct kevent *kev,uintptr_t ident, short filter,
u_short flags, u_int fflags, intptr_t data, void *udata);
kqueue函数返回一个新的kqueue描述符,用于后续的kevent调用中。kevent函数既用于注册所关注的事件,也用于确定是否有所关注的事件发生。changelist和nchanges这两个参数给出对所关注事件做出的更改,若无更改则分别取值NULL和0。如果nchanges不为0,kevent函数就执行changelist数组中所请求的每个事件过滤器更改。其条件已经触发的任何事件(包括刚在changelist中增设的那些事件)由kevent函数通过eventlist参数返回,它指向一个由nevents个元素构成的kevent结构数组。kevent函数在eventlist中返回的事件数目作为函数返回值返回,0表示超时。超时通过timeout参数设置,其处理类似select:NULL阻塞进程,非0值timespec指定明确的超时值,0值timespec执行非阻塞事件检查。注意,kevent使用的timespec结构不同于select使用的timeval结构,前者的分辨率为纳秒,后者的分辨率为微秒。
kevent结构在头文件中定义:
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags
intptr_t data;
void *udata;
};
其中的flags成员在调用时指定过滤器更改行为,在返回时额外给出条件,如下图所示。
filter成员指定的过滤器类型如下图所示。
以下利用kqueue机制对函数str_cli进行改编。
void
str_cli(FILE *fp, int sockfd)
{
int kq, i, n, nev, stdineof = 0, isfile;
char buf[MAXLINE];
struct kevent kev[2];
struct timespec ts;
struct stat st;
isfile = ((fstat(fileno(fp), &st) == 0) &&
(st.st_mode & S_IFMT) == S_IFREG);
EV_SET(&kev[0], fileno(fp), EVFILT_READ, EV_ADD, 0, 0, NULL);
EV_SET(&kev[1], sockfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kq = kqueue();
ts.tv_sec = ts.tv_nsec = 0;
kevent(kq, kev, 2, NULL, 0, &ts);
for ( ; ; ) {
nev = kevent(kq, NULL, 0, kev, 2, NULL);
for (i = 0; i < nev; i++) {
if (kev[i].ident == sockfd) {
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return;
else
err_quit("str_cli: server terminated prematurely");
}
write(fileno(stdout), buf, n);
}
if (kev[i].ident == fileno(fp)) {
n = read(fileno(fp), buf, MAXLINE);
if (n > 0)
write(sockfd, buf, n);
if (n == 0 || (isfile && n == kev[i].data)) {
stdineof = 1;
shutdown(sockfd, SHUT_WR);
kev[i].flags = EV_DELETE;
kevent(kq, &kev[i], 1, NULL, 0, &ts);
continue;
}
}
}
}
}