通常的網絡服務器實現, 是對每一個連接使用一個單獨的線程或進程。對高性能應用而言,由於需要同時處理非常多的客戶請求, 所以這種方式並不能工作得很好,因爲諸如資源使用和上下文切換所需的時間影響了在一時間內對多個客戶端進行處理。另一個可選的途徑是在一個單獨的線程裏採用非阻塞的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() 首先調用 create_and_bind()來新建一個socket。然後將其設置爲非阻塞,再調用 listen (2)。之後,我們新建一個epoll實例inefd,並將監聽套接字sfd以採用邊沿觸發的方式加入它,用以監聽輸入事件。
在外面的while循環是主要的事件循環。它調用epoll_wait(2),它所在線程以阻塞的方式來等待事件的到來。當事件就緒,epoll_wait(2)在其epoll_event類型的參數中返回相應的事件。
當我們添加新的傳入連接,當他們終止時我們刪除現有的連接,epoll 的實例 inefdis 的事件循環不斷更新。
當事件的狀態爲可用的時候,他們有以下三種類型:
- 錯誤:當錯誤情況發生時,或者事件是不是一個有關數據可以被讀取的通知,我們只需關閉相關的描述符。關閉描述符會自動移除其 epoll instanceefd。
- 新的連接:當監聽到 descriptorsfdis 已經準備好用於讀取的時候,這意味着已經到達一個或多個新的連接。當有新連接時,accept(2)連接,打印關於連接的信息,使傳入的 socket 不被阻斷,並將其添加到 epoll instanceefd 監聽事件。
- 客戶端數據:當數據在客戶端描述符上爲可讀狀態,我們在 read(2) 中使用 while 循環來讀去存儲在512位數據塊中的數據。這是因爲我們現在要讀取所有可用的數據,在 edge-triggered 模式下,我們不會進一步獲取事件描述符。使用 write(2) 將讀取的數據被寫入到 stdout (fd=1),如果 read(2) 返回 0,這意味着到達了一個 EOF(End of File),這時我們就可以斷開與客戶端的連接。如果 read(2) 返回 -1,anderrnois 設置爲 EAGAIN,這意味着該事件所有的數據已讀完,我們可以返回主循環了。