转载者注:此篇转载内容是两篇博文的汇总,Kqueue用例部分增加csdn 《春夜喜雨》的demo用例分析,转者认为更容易让读者理解和认知。
常见IO复用技术对比
Wiki上的解释:
kqueue 是一种可扩展的事件通知接口。2000 年 7 月发布的 FreeBSD 4.1 中首次引入了 kqueue,随后也被 NetBSD、OpenBSD、macOS 等操作系统支持。
kqueue 在内核与用户空间之间充当输入输出事件的管线。因此在事件循环的迭代中,进行一次 kevent(2) 系统调用不仅可以接收未决事件,还可以修改事件过滤器。
简单解释,Kqueue是unix系统上高效的IO多路复用技术(常见的io复用有select、poll、epoll、kqueue等等,其中epoll为Linux系统独有,kqueue则在众多unix系统中存在)。
-
为什么要有IO多路复用?
阻塞I/O模式下,一个线程只能处理一个流的I/O事件(比如用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪,如果数据没有就绪,则会一直在那等待,直到数据就绪,当数据就绪之后,便将数据拷贝到用户线程)。
所以,如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。 -
select/poll 的弊端
考虑非阻塞忙轮询的I/O方式,我们发现同时处理多个流,使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。
select/poll是通过轮询的方法来获得就绪的状态,调用select/poll后就阻塞住,直到有就绪的文件描述符,或者超时,或者被中断。返回值是就绪的文件描述符的个数,需要遍历作为参数传入的文件描述符的位域或数组获得文件描述符。 -
Kqueue和Epoll的优势
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知用户空间程序。此时我们对这些流的操作都是有意义的。关于 epoll 的应用参考连接。
kqueue与epoll非常相似,在注册一批文件描述符到 kqueue 以后,当其中的描述符状态发生变化时,kqueue将一次性通知应用程序哪些描述符可读、可写或出错了(即产生事件Event)。
kqueue 支持的event很多,文件句柄事件,信号,异步io事件,子进程状态事件,支持微秒的计时器事件等。
以下转载原文内容
简介
本文档的目的是向程序员介绍kqueue的使用方法,而不是提供一个完整而详尽的文档。
Kqueue为应用程序提供了一个标准的API,用于注册他们感兴趣的各种事件和条件,并以有效的方式通知他们。它的设计是可伸缩,灵活,可靠和准确的。
Kqueue API
kevent 由一对<ident, filter>进行标识。标识符ident可以是一个描述符 (文件、套接字、流)、进程ID或信号量, 这取决于我们要监视的内容。筛选器filter标识用于处理相应事件的内核筛选器。有一些预先定义的系统筛选器 (如 EVFILT_READ 或 EVFILT_WRITE),当有可进行读取或写入操作的数据时,可分别被触发。
例如,如果我们想在socket有数据可读取时收到通知,我们必须指定一个<sckfd,EVFILLE_READ>形式的kevent,其中sckfd是与套接字关联的描述符。如果我们监视一个进程的活动,我们需要一个<pid,EVFILT_PROC>元组。需要注意的是,一个kqueue只能有一个<ident,filter>相同的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 */可携带的任意用户数据
};
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);
}
宏定义如下
#define EV_SET(kevp, a, b, c, d, e, f) do { \
struct kevent *__kevp__ = (kevp); \
__kevp__->ident = (a); \
__kevp__->filter = (b); \
__kevp__->flags = (c); \
__kevp__->fflags = (d); \
__kevp__->data = (e); \
__kevp__->udata = (f); \
} while(0)
kqueue
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; /*纳秒数*/
};
示例: 一个定时器例子
下面的代码将设置一个每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
实例:一个原始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
————————————————
版权声明:本文为CSDN博主「Segment fault」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Namcodream521/article/details/83032615