如何使用epoll? 一个C语言的简单例子
程序员文章站
2024-02-01 16:31:04
...
* How to use epoll? A complete example in C 译文
通常的网络服务器实现, 是对每一个连接使用一个单独的线程或进程。对高性能应用而言,由于需要同时处理非常多的客户请求, 所以这种方式并不能工作得很好,因为诸如资源使用和上下文切换所需的时间影响了在一时间内对多个客户端进行处理。另一个可选的途径是在一个单独的线程里采用非阻塞的I/O, 这样当可以从一个socket中读取或写入更多数据时,由一些已经准备就绪的通知方式来告知我们。
这篇文章介绍Linux 的 epoll方法, 它是Linux上最好的就绪通知方式。我们会写一个用C语言的TCP服务器的完全实现的简单程序。假设你已有C编程的经验,知道在Linux 下编译和运行程序, 并且会用 manpages 来查看所使用的 C 函数。
epoll 是在 Linux 2.6 才引进的,而且它并不适用于其它 Unix-like 系统。它提供了一个与select 和 poll 函数相似的功能:
+ select 可以在某一时间监视最大达到 FD_SETSIZE 数量的文件描述符, 通常是由在 libc 编译时指定的一个比较小的数字。
+ poll 在同一时间能够监视的文件描述符数量并没有受到限制,即使除了其它因素,更加的是我们必须在每一次都扫描所有通过的描述符来检查其是否存在己就绪通知,它的时间复杂度为 O(n) ,是缓慢的。
epoll 没有以上所示的限制,并且不用执行线性扫描。因此, 它能有更高的执行效率且可以处理大数量的事件。
一个 epoll 实例可以通过返加epoll 实例的 epoll_create 或者 epoll_create1 函数来创建。 epoll_ctl 是用来在epoll实例中 添加/删除 被监视的文件描述符的。 epoll_wait是用来等待所监听描述符事件的,它会阻塞到事件到达。 可以在 manpages上查看更多信息。
当描述符被添加到epoll实例中, 有两种添加模式: level triggered(级别触发) 和 edge triggered(边沿触发) 。 当使用 level triggered 模式并且数据就绪待读, epoll_wait总是会返加就绪事件。如果你没有将数据读取完, 并且调用epoll_wait 在epoll 实例上再次监听这个描述符, 由于还有数据是可读的,它会再次返回。在 edge triggered 模式时, 你只会得一次就绪通知。 如果你没有瘵数据读完, 并且再次在 epoll实例上调用 epoll_wait , 由于就绪事件已经被发送所以它会阻塞。
传递到 epoll_ctl 的epoll事件结构体如下所示。对每一个被监听的描述符,你可以关联到一个整数或一个作为用户数据的指针。
马上实践写代码。我们会实现一个小的TCP服务器,它会将所有SOCKET上收到的数据输出到标准输出。 首先写一个 create_and_bind() 函数,它创建并绑定一个TCP socket.
create_and_bind函数包含了一种可移植方式来获取IPv4或IPv6套接字的标准代码段。它接受一个port的字符串参数,port是从argv\[1\]中传入的。其中,getaddrinfo函数返回一群addrinfo到result,其中它们跟传入的hints参数是兼容的。 addrinfo结构体如下:
我们依次遍历这些结构体并用其来创建结构体,直到我们可以同时创建和绑定到socket。如果我们成功,create_and_bind() 会返回一个socket描述符。失败则返回 -1.
接下来,我们写一个用来设置socket为非阻塞的函数。 make_socket_non_blocking() 设置 O_NONBLOCK 标志给传入的sfd描述符参数。
接下来的main()函数中,它包含有一个事件循环。 下面是代码:
关于main函数里就不多说了。
下载 epoll-example.c 程序。
通常的网络服务器实现, 是对每一个连接使用一个单独的线程或进程。对高性能应用而言,由于需要同时处理非常多的客户请求, 所以这种方式并不能工作得很好,因为诸如资源使用和上下文切换所需的时间影响了在一时间内对多个客户端进行处理。另一个可选的途径是在一个单独的线程里采用非阻塞的I/O, 这样当可以从一个socket中读取或写入更多数据时,由一些已经准备就绪的通知方式来告知我们。
这篇文章介绍Linux 的 epoll方法, 它是Linux上最好的就绪通知方式。我们会写一个用C语言的TCP服务器的完全实现的简单程序。假设你已有C编程的经验,知道在Linux 下编译和运行程序, 并且会用 manpages 来查看所使用的 C 函数。
epoll 是在 Linux 2.6 才引进的,而且它并不适用于其它 Unix-like 系统。它提供了一个与select 和 poll 函数相似的功能:
+ select 可以在某一时间监视最大达到 FD_SETSIZE 数量的文件描述符, 通常是由在 libc 编译时指定的一个比较小的数字。
+ poll 在同一时间能够监视的文件描述符数量并没有受到限制,即使除了其它因素,更加的是我们必须在每一次都扫描所有通过的描述符来检查其是否存在己就绪通知,它的时间复杂度为 O(n) ,是缓慢的。
epoll 没有以上所示的限制,并且不用执行线性扫描。因此, 它能有更高的执行效率且可以处理大数量的事件。
一个 epoll 实例可以通过返加epoll 实例的 epoll_create 或者 epoll_create1 函数来创建。 epoll_ctl 是用来在epoll实例中 添加/删除 被监视的文件描述符的。 epoll_wait是用来等待所监听描述符事件的,它会阻塞到事件到达。 可以在 manpages上查看更多信息。
当描述符被添加到epoll实例中, 有两种添加模式: level triggered(级别触发) 和 edge triggered(边沿触发) 。 当使用 level triggered 模式并且数据就绪待读, epoll_wait总是会返加就绪事件。如果你没有将数据读取完, 并且调用epoll_wait 在epoll 实例上再次监听这个描述符, 由于还有数据是可读的,它会再次返回。在 edge triggered 模式时, 你只会得一次就绪通知。 如果你没有瘵数据读完, 并且再次在 epoll实例上调用 epoll_wait , 由于就绪事件已经被发送所以它会阻塞。
传递到 epoll_ctl 的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服务器,它会将所有SOCKET上收到的数据输出到标准输出。 首先写一个 create_and_bind() 函数,它创建并绑定一个TCP socket.
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的字符串参数,port是从argv\[1\]中传入的。其中,getaddrinfo函数返回一群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; };
我们依次遍历这些结构体并用其来创建结构体,直到我们可以同时创建和绑定到socket。如果我们成功,create_and_bind() 会返回一个socket描述符。失败则返回 -1.
接下来,我们写一个用来设置socket为非阻塞的函数。 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函数里就不多说了。
下载 epoll-example.c 程序。