网络服务器传统上是使用每个连接单独的进程或线程实现的。对于需要同时处理大量客户端的高性能应用程序,这种方法将无法正常工作,因为诸如资源使用和上下文切换时间等因素会影响一次处理多个客户端的能力。一种替代方法是在单个线程中执行非阻塞I / O,以及一些准备通知方法,它可以告诉您何时可以在套接字上读取或写入更多数据。
本文介绍了Linux的epoll(7)工具,它是Linux中最好的准备通知工具。我们将在C中编写一个完整的TCP服务器实现的示例代码。我假设你有C编程经验,知道如何在Linux上编译和运行程序,并且可以读取使用的各种C函数的联机帮助页。
epoll在Linux 2.6中引入,并且在其他类UNIX操作系统中不可用。它提供类似于select(2)和poll(2)功能的设施:
select(2)可以一次监视多达FD_SETSIZE 几个描述符,通常在libc的编译时确定的数量很少。
poll(2)没有一次可以监视的描述符的固定限制,但除了其他事情,即使我们每次都要对所有传递的描述符执行线性扫描,以检查准备通知,O( n)慢
epoll没有这样的固定限制,不执行任何线性扫描。因此,它能够更好地执行并处理更多的事件。
epoll实例由epoll_create(2)或epoll_create1(2)(它们采用不同的参数)创建,返回一个epoll实例。epoll_ctl(2)用于添加/删除要在epoll实例上观看的描述符。要等待观看集上的事件,使用epoll_wait(2),阻塞直到事件可用。请查看他们的联机帮助页以获取更多信息。
当描述符被添加到epoll实例时,它们可以以两种模式添加:电平触发和边沿触发。当您使用电平触发模式,并且数据可用于阅读时,epoll_wait(2)将始终以就绪事件返回。如果您没有完全读取数据,并再次在epoll实例上调用epoll_wait(2),则会再次返回一个ready事件,因为数据可用。在边缘触发模式下,您只会有一次准备就绪。如果您没有完全读取数据,并且再次在epoll实例上调用epoll_wait(2),则会阻塞,因为准备事件已经被传递。
您传递给epoll_ctl(2)的epoll事件结构如下所示。在每个描述符被观看时,您可以将整数或指针作为用户数据。
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
我们现在写代码 我们将实现一个微小的TCP服务器,打印在标准输出上发送到套接字的所有内容。我们将首先编写一个创建并绑定TCP套接字的函数create_and_bind():
static int
create_and_bind (char *port)
{
struct addrinfo hints;
struct addrinfo *result, *rp;
int s, sfd;
memset (&hints, 0, sizeof (struct addrinfo));
hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
hints.ai_flags = AI_PASSIVE; /* All interfaces */
s = getaddrinfo (NULL, port, &hints, &result);
if (s != 0)
{
fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
return -1;
}
for (rp = result; rp != NULL; rp = rp->ai_next)
{
sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sfd == -1)
continue;
s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
if (s == 0)
{
/* We managed to bind successfully! */
break;
}
close (sfd);
}
if (rp == NULL)
{
fprintf (stderr, "Could not bind\n");
return -1;
}
freeaddrinfo (result);
return sfd;
}
create_and_bind()包含用于获取IPv4或IPv6套接字的便携式方法的标准代码块。它接受一个port参数作为一个字符串,哪里argv[1]可以传递。所述的getaddrinfo(3)函数返回一束addrinfo结构中result,这是与在通过提示兼容hints 参数。该addrinfo结构是这样的:
struct addrinfo
{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
我们逐个走过结构,尝试使用它们创建套接字,直到我们能够创建和绑定套接字。如果我们成功,create_and_bind()返回套接字描述符。如果不成功,则返回-1。
接下来,我们来写一个函数来使套接字无阻塞。make_socket_non_blocking()O_NONBLOCK在sfd参数中传递的描述符设置标志:
static int
make_socket_non_blocking (int sfd)
{
int flags, s;
flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
}
return 0;
}
现在,关于包含事件循环的程序的main()函数。这是程序的大部分:
#define MAXEVENTS 64
int
main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events;
if (argc != 2)
{
fprintf (stderr, "Usage: %s [port]\n", argv[0]);
exit (EXIT_FAILURE);
}
sfd = create_and_bind (argv[1]);
if (sfd == -1)
abort ();
s = make_socket_non_blocking (sfd);
if (s == -1)
abort ();
s = listen (sfd, SOMAXCONN);
if (s == -1)
{
perror ("listen");
abort ();
}
efd = epoll_create1 (0);
if (efd == -1)
{
perror ("epoll_create");
abort ();
}
event.data.fd = sfd;
event.events = EPOLLIN | EPOLLET;
s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1)
{
perror ("epoll_ctl");
abort ();
}
/* Buffer where events are returned */
events = calloc (MAXEVENTS, sizeof event);
/* The event loop */
while (1)
{
int n, i;
n = epoll_wait (efd, events, MAXEVENTS, -1);
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN)))
{
/* An error has occured on this fd, or the socket is not
ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
}
else if (sfd == events[i].data.fd)
{
/* We have a notification on the listening socket, which
means one or more incoming connections. */
while (1)
{
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept (sfd, &in_addr, &in_len);
if (infd == -1)
{
if ((errno == EAGAIN) ||
(errno == EWOULDBLOCK))
{
/* We have processed all incoming
connections. */
break;
}
else
{
perror ("accept");
break;
}
}
s = getnameinfo (&in_addr, in_len,
hbuf, sizeof hbuf,
sbuf, sizeof sbuf,
NI_NUMERICHOST | NI_NUMERICSERV);
if (s == 0)
{
printf("Accepted connection on descriptor %d "
"(host=%s, port=%s)\n", infd, hbuf, sbuf);
}
/* Make the incoming socket non-blocking and add it to the
list of fds to monitor. */
s = make_socket_non_blocking (infd);
if (s == -1)
abort ();
event.data.fd = infd;
event.events = EPOLLIN | EPOLLET;
s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
if (s == -1)
{
perror ("epoll_ctl");
abort ();
}
}
continue;
}
else
{
/* We have data on the fd waiting to be read. Read and
display it. We must read whatever data is available
completely, as we are running in edge-triggered mode
and won't get a notification again for the same
data. */
int done = 0;
while (1)
{
ssize_t count;
char buf[512];
count = read (events[i].data.fd, buf, sizeof buf);
if (count == -1)
{
/* If errno == EAGAIN, that means we have read all
data. So go back to the main loop. */
if (errno != EAGAIN)
{
perror ("read");
done = 1;
}
break;
}
else if (count == 0)
{
/* End of file. The remote has closed the
connection. */
done = 1;
break;
}
/* Write the buffer to standard output */
s = write (1, buf, count);
if (s == -1)
{
perror ("write");
abort ();
}
}
if (done)
{
printf ("Closed connection on descriptor %d\n",
events[i].data.fd);
/* Closing the descriptor will make epoll remove it
from the set of descriptors which are monitored. */
close (events[i].data.fd);
}
}
}
}
free (events);
close (sfd);
return EXIT_SUCCESS;
}
main()首先调用create_and_bind()来设置套接字。然后使套接字无阻塞,然后调用listen(2)。然后,它创建一个epoll实例efd,它添加了监听套接字sfd以在边缘触发模式下监视输入事件。
外部while循环是主要事件循环。它调用epoll_wait(2),其中线程保持阻塞等待事件。当事件可用时,epoll_wait(2)返回events参数中的事件,这是一堆epoll_event结构。
efd当我们添加新的传入连接以观看时,epoll实例在事件循环中不断更新,并在其死亡时删除现有连接。
当事件可用时,它们可以有三种类型:
错误:当发生错误情况或事件不是有关可读取数据的通知时,我们只需关闭相关描述符。关闭描述符会自动从被监视的epoll实例集中删除它efd。
新连接:当侦听描述符sfd准备好读取时,意味着一个或多个新的连接到达。虽然有新的连接,接受(2)连接,打印关于它的消息,使传入插座不阻塞,并将其添加到被监视的epoll实例集efd。
客户端数据:当数据可用于任何客户描述符的读取时,我们使用read(2)在内部while循环中读取512字节的数据。这是因为我们必须读取现在可用的所有数据,因为我们不会得到关于它的更多事件,因为描述符是在边缘触发模式下观看的。读取的数据使用write(2)写入stdout(fd = 1 )。如果read(2)返回0,则表示EOF,我们可以关闭客户端的连接。如果返回-1,并 errno设置为EAGAIN,则表示已读取此事件的所有数据,我们可以返回到主循环。
就是这样。它绕过一个循环,在观察集中添加和删除描述符。
下载epoll-example.c 程序。
Update1:级别和边缘触发的定义被错误地反转(尽管代码是正确的)。被Reddit用户bodski注意到了。文章已经被更正了。在发布之前,我应该有证明。道歉,谢谢你指出错误。:)
Update2:代码已被修改为运行 accept(2),直到它表示将阻止,以便如果多个连接到达,我们接受所有这些。被Reddit用户cpitchford注意到了。谢谢你的意见。:)