The NetBSD Project
”Of course it runs NetBSD”
本文翻译自:http://wiki.netbsd.org/tutorials/kqueue_tutorial/
NetBSD Wiki/tutorials/kqueue tutorial
目录
I、简介
II、kqueue API
- kevent 数据结构
- pair
- flags
- EV_SET()宏
2.kqueue()
3.kevent()
a.timeout
4.总结
III.例子
1.一个定时器例子
2.一个原始的TCP客户端例子
IV.参考文献
简介
本文档的目的是向程序员介绍kqueue的使用方法,而不是提供一个完整而详尽的文档。
Kqueue为应用程序提供了一个标准的API,用于注册他们感兴趣的各种事件和条件,并以有效的方式通知他们。它的设计是可伸缩,灵活,可靠和准确的。
Kqueue API
kevent数据结构
kevent结构体是这样的:
struct kevent
{
uintptr_t ident; /* identifier for this event */ 事件标识
short filter; /* filter for event */ 监听事件的类型,如EVFILT_READ,EVFILT_WRITE,EVFILT_TIMER等
u_short flags; /* action flags for kqueue */事件操作类型,如EV_ADD,EV_ENABLE,EV_DELETE等
u_int fflags; /* filter flag value */
intptr_t data; /* filter data value */
void *udata; /* opaque user data identifier */可携带的任意用户数据
};
pair
kevent 由一对<ident, filter>进行标识。标识符ident可以是一个描述符 (文件、套接字、流)、进程ID或信号量, 这取决于我们要监视的内容。筛选器filter标识用于处理相应事件的内核筛选器。有一些预先定义的系统筛选器 (如 EVFILT_READ 或 EVFILT_WRITE),当有可进行读取或写入操作的数据时,可分别被触发。
例如,如果我们想在socket有数据可读取时收到通知,我们必须指定一个<sckfd,EVFILLE_READ>形式的kevent,其中sckfd是与套接字关联的描述符。如果我们监视一个进程的活动,我们需要一个<pid,EVFILT_PROC>元组。需要注意的是,一个kqueue只能有一个<ident,filter>相同的kevent。
flags
在设计好一个kevent之后,我们应该决定是否将它添加到我们的kqueue中。为此,我们通过设置flags成员为EV_ADD。我们也可以通过设置EV_DELETE或EV_DISABLE来删除或仅仅只是禁用一个已存在的kevent。
可以通过或运算将期望的值组合在一起。例如,EV_ADD|EV_ENABLE|EV_ONESHOT将被解释为“添加这个事件,使能它并且只在第一次发生时被触发,在用户将事件从kqueue中取出之后将其删除。”
反过来,如果希望检查一个kevent中某个flag是否被置位,我们可以通过将kevent.flag与期望的值进行“与运算”。例如:
if(myevent.flags & EV_ERROR)
{
/*handle errors*/
}
EV_SET()宏
EV_SET()宏是为了便于初始化kevent结构体。我们暂时不祥述其余的kevent成员,相反我们先来看看当我们需要监视一个套接字是否有等待读取的数据的情况:
kevent ev;
EV_SET(&ev,sckfd,EVFILT_READ,EV_ADD,0,0,0);
如果我们要监视N个套接字的集合,我们应该这样写:
kevent ev[N];
int i;
for(i=0;i<N;i++)
{
EV_SET(&ev[i],sckfd[i],EVFILE_READ,EV_ADD,0,0,0);
}
kqueue
kqueue包含了我们所有趣的事件。因此,首先,我们必须创建一个新的kqueue,我们使用下面的代码执行此操作:
int kq;
if((q=kqueue())= = -1)
{
perror(“kqueue”);
exit(EXIT_FAIURE);
}
kevent(2)
此时kqueue是空的,为了用一个events集合来填充它,我们使用kevent(2)函数。给个系统函数传入我们前面构造的事件数组,它并不立即返回直到至少接收到一个事件(或者一个与之关联的超时时间被耗尽)。这个函数返回接收到的事件列表数量并且将它们的信息保存到另一个kevent结构体组成的数组中。
kevent chlist[N]; /*我们要监视的事件*/
kevent evlist[N]; /*已触发的事件*/
int nev,I;
/*用我们感兴趣的事件填充chlist*/
/*……*/
/*无限循环*/
for(;;)
{
nev=kevent(kq,chlist,N,evlist,N,NULL); /*无限期阻塞*/
if(nev= =-1)
{
perror(“kevent()”);
exit(EXIT_FAIURE);
}
else if(nev>0)
{
for(i=0;i<nev;i++)
{
/*处理事件*/
}
}
}
timeout
有时候为kevent()设置一个有限的阻塞上限时间是有用的。那样的话无论有没有事件被触发它都会返回,为此我们需要一个timespec结构体,它在sys/time.h中被定义:
struct timespec
{
time_t tv_sec; /*秒数*/
long tv_nsec; /*纳秒数*/
};
上面的代码将成下面的这样:
kevent chlist[N]; /*我们要监视的事件*/
kevent evlist[N]; /*已触发的事件*/
struct timespec tmout={5,0}; /*最多阻塞5秒0纳秒*/
int nev,I;
/*用我们感兴趣的事件填充chlist*/
/*……*/
/*无限循环*/
for(;;)
{
nev=kevent(kq,chlist,N,evlist,N,&tmout); /*无限期阻塞*/
if(nev= =-1)
{
perror(“kevent()”);
exit(EXIT_FAIURE);
}
else if(nev= =0)
{
/*超时处理*/
}
else if(nev>0)
{
for(i=0;i<nev;i++)
{
/*处理事件*/
}
}
}
注意,如果使用一个值为0但不为NULL的timespec结构体,kevent()将立即返回,从而使性能降低到一个纯epoll方式的级别。
总结
总而言之, kqueue 框架的工作方式如下:
示例
一个定时器例子
下面的代码将设置一个每5秒触发一次kevent的定时器。一旦触发,进程将fork并且子进程将执行data(1)命令。
#include <sys/event.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> /* for strerror() */
#include <unistd.h>
/* function prototypes */
/*函数原型*/
void diep(const char *s);
int main(void)
{
struct kevent change; /* event we want to monitor,我们想要监视的事件*/
struct kevent event; /* event that was triggered,已触发的事件*/
pid_t pid;
int kq, nev;
/* create a new kernel event queue */
/*创建一个新的内核事件队列*/
if ((kq = kqueue()) == -1)
diep("kqueue()");
/* initalise kevent structure */
/*初始化kevent结构体,5000为超时时间(单位毫秒)*/
EV_SET(&change, 1, EVFILT_TIMER, EV_ADD | EV_ENABLE, 0, 5000, 0);
/* loop forever */
/*无限循环*/
for (;;)
{
nev = kevent(kq, &change, 1, &event, 1, NULL);
if (nev < 0)
diep("kevent()");
else if (nev > 0)
{
if (event.flags & EV_ERROR)
{
/* report any error */
/*报告任何错误*/
fprintf(stderr, "EV_ERROR: %s\n", strerror(event.data));
exit(EXIT_FAILURE);
}
if ((pid = fork()) < 0) /* fork error */
diep("fork()");
else if (pid == 0) /* child */
if (execlp("date", "date", (char *)0) < 0)
diep("execlp()");
}
}
close(kq);
return EXIT_SUCCESS;
}
void diep(const char *s)
{
perror(s);
exit(EXIT_FAILURE);
}
编译和运行:
$ gcc -o ktimer ktimer.c -Wall -W -Wextra -ansi -pedantic
$ ./ktimer
Tue Mar 20 15:48:16 EET 2007
Tue Mar 20 15:48:21 EET 2007
Tue Mar 20 15:48:26 EET 2007
Tue Mar 20 15:48:31 EET 2007
^C
一个原始TCP客户端例子
我们将用kqueue框架实现一个原始的TCP客户端。每当主机向套接字发送数据时,我们通过标准输出流把它打印出来。同样, 当用户在标准输入流中键入内容时, 我们将通过套接字将其发送到主机。基本上, 我们需要监控以下事件:
1、套接字中输入的任何主机数据
2、标准输入流中输入的任何用户数据
#include <sys/event.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE 1024
/* 函数原型 */
void diep(const char *s);
int tcpopen(const char *host, int port);
void sendbuftosck(int sckfd, const char *buf, int len);
int main(int argc, char *argv[])
{
struct kevent chlist[2];/* 我们要监视的事件 */
struct kevent evlist[2];/* 触发的事件 */
char buf[BUFSIZE];
int sckfd, kq, nev, i;
/* 检查参数数量 */
if (argc != 3)
{
fprintf(stderr, "usage: %s host port\n", argv[0]);
exit(EXIT_FAILURE);
}
/* 打开一个链接(host,port)pair */
sckfd = tcpopen(argv[1], atoi(argv[2]));
/* 创建一个新的内核事件队列 */
if ((kq = kqueue()) == -1)
diep("kqueue()");
/* 初始化kevent结构体 */
EV_SET(&chlist[0], sckfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
EV_SET(&chlist[1], fileno(stdin), EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
/* 无限循环 */
for (;;)
{
nev = kevent(kq, chlist, 2, evlist, 2, NULL);
if (nev < 0)
{
diep("kevent()");
}
else if (nev > 0)
{
if (evlist[0].flags & EV_EOF)/* 读取socket关闭指示 */
{
exit(EXIT_FAILURE);
}
for (i = 0; i < nev; i++)
{
if (evlist[i].flags & EV_ERROR)
{
/* 报告错误 */
fprintf(stderr, "EV_ERROR: %s\n", strerror(evlist[i].data));
exit(EXIT_FAILURE);
}
if (evlist[i].ident == sckfd)
{
/* 我们从host接收到数据 */
memset(buf, 0, BUFSIZE);
if (read(sckfd, buf, BUFSIZE) < 0)
{
diep("read()");
}
fputs(buf, stdout);
}
else if (evlist[i].ident == fileno(stdin))
{
/* stdin中有数据输入 */
memset(buf, 0, BUFSIZE);
fgets(buf, BUFSIZE, stdin);
sendbuftosck(sckfd, buf, strlen(buf));
}
}
}
}
close(kq);
return EXIT_SUCCESS;
}
void diep(const char *s)
{
perror(s);
exit(EXIT_FAILURE);
}
int tcpopen(const char *host, int port)
{
struct sockaddr_in server;
struct hostent *hp;
int sckfd;
if ((hp = gethostbyname(host)) == NULL)
{
diep("gethostbyname()");
}
/* 译者注:此处Linux系统应使用AF_INET,PF_INET用于BSD */
if ((sckfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
diep("socket()");
}
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr = *((struct in_addr *)hp->h_addr);
memset(&(server.sin_zero), 0, 8);
if (connect(sckfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) < 0)
{
diep("connect()");
}
return sckfd;
}
void sendbuftosck(int sckfd, const char *buf, int len)
{
int bytessent, pos;
pos = 0;
do
{
if ((bytessent = send(sckfd, buf + pos, len - pos, 0)) < 0)
{
diep("send()");
}
pos += bytessent;
}while(bytessent > 0);
}
编译和运行:
$ gcc -o kclient kclient.c -Wall -W -Wextra -ansi -pedantic
$ ./kclient irc.freenode.net 7000
NOTICE AUTH :*** Looking up your hostname...
NOTICE AUTH :*** Found your hostname, welcome back
NOTICE AUTH :*** Checking ident
NOTICE AUTH :*** No identd (auth) response
_USER guest tolmoon tolsun :Ronnie Reagan
NICK Wiz_
:herbert.freenode.net 001 Wiz :Welcome to the freenode IRC Network Wiz
^C
(斜体是我们输入的内容)
参考文献
1. kqueue, kevent NetBSD Manual Pages
http://netbsd.gw.com/cgi-bin/man-cgi?kqueue++NetBSD-current
2. Kqueue: A generic and scalable event notification facility (pdf)
http://people.freebsd.org/~jlemon/papers/kqueue.pdf
3. kqueue slides
http://people.freebsd.org/~jlemon/kqueue_slides/
4. The Julipedia: An example of kqueue
http://julipedia.blogspot.com/2004/10/example-of-kqueue.html