摘要:前面幾講手撕了網(wǎng)關(guān)服務器回顯服務器服務的代碼,但是這幾個一次只能監(jiān)聽一個文件描述符,因此性能非常原始低下。復用能使服務器同時監(jiān)聽多個文件描述符,是服務器性能提升的關(guān)鍵。表示要操作的文件描述符,指定操作類型,指定事件。
?本系列文章導航: 手把手寫C++服務器(0):專欄文章-匯總導航【更新中】?
前言: Linux中素有“萬物皆文件,一切皆IO”的說法。前面幾講手撕了CGI網(wǎng)關(guān)服務器、echo回顯服務器、discard服務的代碼,但是這幾個一次只能監(jiān)聽一個文件描述符,因此性能非常原始、低下。IO復用能使服務器同時監(jiān)聽多個文件描述符,是服務器性能提升的關(guān)鍵。雖然IO復用本身是阻塞的,但是和并發(fā)技術(shù)結(jié)合起來,再加上一點設計模式,一個高性能服務器的基石就基本搭建完成了。
目錄
2. 內(nèi)核態(tài)檢測文件描述符讀寫狀態(tài)的方式
強烈推薦看一下本系列的第25講《手把手寫C++服務器(25):萬物皆可文件之socket fd》
文件描述符(File descriptor)是計算機科學中的一個術(shù)語,是一個用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一個非負整數(shù)。實際上,它是一個索引值,指向內(nèi)核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
正在執(zhí)行的進程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢姡M程的阻塞是進程自身的一種主動行為,也因此只有處于運行態(tài)的進程(獲得了CPU資源),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當進程進入阻塞狀態(tài),是不占用CPU資源的。
緩存I/O又稱為標準I/O,大多數(shù)文件系統(tǒng)的默認I/O操作都是緩存I/O。在Linux的緩存I/O機制中,操作系統(tǒng)會將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,即數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應用程序的地址空間。
緩存 I/O 的缺點:
數(shù)據(jù)在傳輸過程中需要在應用程序地址空間和內(nèi)核進行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。
IO 多路復用是一種同步IO模型,實現(xiàn)一個線程可以監(jiān)視多個文件句柄;一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作;沒有文件句柄就緒就會阻塞應用程序,交出CPU。
這是最常用的簡單的IO模型。阻塞IO意味著當我們發(fā)起一次IO操作后一直等待成功或失敗之后才返回,在這期間程序不能做其它的事情。阻塞IO操作只能對單個文件描述符進行操作,詳見read或write。
我們在發(fā)起IO時,通過對文件描述符設置O_NONBLOCK flag來指定該文件描述符的IO操作為非阻塞。非阻塞IO通常發(fā)生在一個for循環(huán)當中,因為每次進行IO操作時要么IO操作成功,要么當IO操作會阻塞時返回錯誤EWOULDBLOCK/EAGAIN,然后再根據(jù)需要進行下一次的for循環(huán)操作,這種類似輪詢的方式會浪費很多不必要的CPU資源,是一種糟糕的設計。和阻塞IO一樣,非阻塞IO也是通過調(diào)用read或write來進行操作的,也只能對單個描述符進行操作。
IO多路復用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是類似的,但具體細節(jié)各有不同:首先都會對一組文件描述符進行相關(guān)事件的注冊,然后阻塞等待某些事件的發(fā)生或等待超時。IO多路復用都可以關(guān)注多個文件描述符,但對于這三種機制而言,不同數(shù)量級文件描述符對性能的影響是不同的,下面會詳細介紹。
信號驅(qū)動IO是利用信號機制,讓內(nèi)核告知應用程序文件描述符的相關(guān)事件。
但信號驅(qū)動IO在網(wǎng)絡編程的時候通常很少用到,因為在網(wǎng)絡環(huán)境中,和socket相關(guān)的讀寫事件太多了,比如下面的事件都會導致SIGIO信號的產(chǎn)生:
上面所有的這些都會產(chǎn)生SIGIO信號,但我們沒辦法在SIGIO對應的信號處理函數(shù)中區(qū)分上述不同的事件,SIGIO只應該在IO事件單一情況下使用,比如說用來監(jiān)聽端口的socket,因為只有客戶端發(fā)起新連接的時候才會產(chǎn)生SIGIO信號。
異步IO和信號驅(qū)動IO差不多,但它比信號驅(qū)動IO可以多做一步:相比信號驅(qū)動IO需要在程序中完成數(shù)據(jù)從用戶態(tài)到內(nèi)核態(tài)(或反方向)的拷貝,異步IO可以把拷貝這一步也幫我們完成之后才通知應用程序。我們使用?aio_read?來讀,aio_write?寫。
同步IO vs 異步IO
1. 同步IO指的是程序會一直阻塞到IO操作如read、write完成
2. 異步IO指的是IO操作不會阻塞當前程序的繼續(xù)執(zhí)行
所以根據(jù)這個定義,上面阻塞IO當然算是同步的IO,非阻塞IO也是同步IO,因為當文件操作符可用時我們還是需要阻塞的讀或?qū)?,同理IO多路復用和信號驅(qū)動IO也是同步IO,只有異步IO是完全完成了數(shù)據(jù)的拷貝之后才通知程序進行處理,沒有阻塞的數(shù)據(jù)讀寫過程。
select的作用是在一段指定的時間內(nèi),監(jiān)聽用戶感興趣的文件描述符上的可讀、可寫、異常等事件。函數(shù)原型如下:
#include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
readfds、writefds、exceptfds都是fd_set結(jié)構(gòu)體,timeout是timeval結(jié)構(gòu)體,這里詳解一下這兩個結(jié)構(gòu)體。
1、fd_set
fd_set結(jié)構(gòu)體定義比較復雜,涉及到位操作,比較復雜。所以通常用宏來訪問fd_set中的位。
#include FD_ZERO(fd_set* fdset); // 清除fdset中的所有位FD_SET(int fd, fd_set* fdset); // 設置fdset中的位FD_CLR(int fd, fd_set* fdset); // 清除fdset中的位int FD_ISSET(int fd, fd_set* fdset); // 測試fdset的位fd是否被設置
2、timeval
struct timeval { long tv_sec; // 秒數(shù) long tv_usec; // 微妙數(shù)};
綜上所述,我們一般的使用流程是:
根據(jù)使用流程,給出一個代碼示例:
#include #include #include #include #define TIMEOUT 5 /* select timeout in seconds */#define BUF_LEN 1024 /* read buffer in bytes */int main (void) { struct timeval tv; fd_set readfds; int ret; /* Wait on stdin for input. */ FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); /* Wait up to five seconds. */ tv.tv_sec = TIMEOUT; tv.tv_usec = 0; /* All right, now block! */ ret = select (STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); if (ret == ?1) { perror ("select"); return 1; } else if (!ret) { printf ("%d seconds elapsed./n", TIMEOUT); return 0; } /* * Is our file descriptor ready to read? * (It must be, as it was the only fd that * we provided and the call returned * nonzero, but we will humor ourselves.) */ if (FD_ISSET(STDIN_FILENO, &readfds)) { char buf[BUF_LEN+1]; int len; /* guaranteed to not block */ len = read (STDIN_FILENO, buf, BUF_LEN); if (len == ?1) { perror ("read"); return 1; } if (len) { buf[len] = "/0"; printf ("read: %s/n", buf); } return 0; } fprintf (stderr, "This should not happen!/n"); return 1; }
后面一講會給出一些實用的例子,有了select之后我們可以同時監(jiān)聽很多個請求,系統(tǒng)的處理能力大大增強了。
和select類似,在一定時間內(nèi)輪詢一定數(shù)量的文件描述符。
#include int poll(struct pollfd* fds, nfds_t nfds, int timeout);
但是和select不同的是,select需要用三組文件描述符,poll只有一個pollfd文件數(shù)組,數(shù)組中的每個元素都表示一個需要監(jiān)聽IO操作事件的文件描述符。而且我們只需要關(guān)心數(shù)組中events參數(shù),revents由內(nèi)核自動填充。
struct pollfd { int fd; // 文件描述符 short events; // 注冊的事件 short revents; // 實際發(fā)生的事件,由內(nèi)核填充 };
具體的事件類型參看手冊:https://man7.org/linux/man-pages/man2/poll.2.html
POLLIN There is data to read. POLLPRI There is some exceptional condition on the file descriptor. Possibilities include: ? There is out-of-band data on a TCP socket (see tcp(7)). ? A pseudoterminal master in packet mode has seen a state change on the slave (see ioctl_tty(2)). ? A cgroup.events file has been modified (see cgroups(7)). POLLOUT Writing is now possible, though a write larger than the available space in a socket or pipe will still block (unless O_NONBLOCK is set). POLLRDHUP (since Linux 2.6.17) Stream socket peer closed connection, or shut down writing half of connection. The _GNU_SOURCE feature test macro must be defined (before including any header files) in order to obtain this definition. POLLERR Error condition (only returned in revents; ignored in events). This bit is also set for a file descriptor referring to the write end of a pipe when the read end has been closed. POLLHUP Hang up (only returned in revents; ignored in events). Note that when reading from a channel such as a pipe or a stream socket, this event merely indicates that the peer closed its end of the channel. Subsequent reads from the channel will return 0 (end of file) only after all outstanding data in the channel has been consumed. POLLNVAL Invalid request: fd not open (only returned in revents; ignored in events). When compiling with _XOPEN_SOURCE defined, one also has the following, which convey no further information beyond the bits listed above: POLLRDNORM Equivalent to POLLIN. POLLRDBAND Priority band data can be read (generally unused on Linux). POLLWRNORM Equivalent to POLLOUT. POLLWRBAND Priority data may be written.
綜上所述,我們一般的使用流程是:
根據(jù)使用流程,給出一個代碼示例:
#include #include #include #define TIMEOUT 5 /* poll timeout, in seconds */int main (void) { struct pollfd fds[2]; int ret; /* watch stdin for input */ fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN; /* watch stdout for ability to write (almost always true) */ fds[1].fd = STDOUT_FILENO; fds[1].events = POLLOUT; /* All set, block! */ ret = poll (fds, 2, TIMEOUT * 1000); if (ret == ?1) { perror ("poll"); return 1; } if (!ret) { printf ("%d seconds elapsed./n", TIMEOUT); return 0; } if (fds[0].revents & POLLIN) printf ("stdin is readable/n"); if (fds[1].revents & POLLOUT) printf ("stdout is writable/n"); return 0; }
epoll是Linux特有的IO復用函數(shù),使用一組函數(shù)來完成任務,而不是單個函數(shù)。
epoll把用戶關(guān)心的文件描述符上的事件放在內(nèi)核的一個事件表中,不需要像select、poll那樣每次調(diào)用都要重復傳入文件描述符集或事件集。
epoll需要使用一個額外的文件描述符,來唯一標識內(nèi)核中的時間表,由epoll_create創(chuàng)建。
#include int epoll_create(int size); int epoll_create1(int flags); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
特別注意epoll_wait函數(shù)成功時返回就緒的文件描述符總數(shù)。select和poll返回文件描述符總數(shù)。
以尋找已經(jīng)就緒的文件描述符,舉個例子如下:
epoll_wait只需要遍歷返回的文件描述符,但是poll和select需要遍歷所有文件描述符
// pollint ret = poll(fds, MAX_EVENT_NUMBER, -1);// 必須遍歷所有已注冊的文件描述符for (int i = 0; i < MAX_EVENT_NUMBER; i++) { if (fds[i].revents & POLLIN) { int sockfd = fds[i].fd; }}// epoll_waitint ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);// 僅需要遍歷就緒的ret個文件描述符for (int i = 0; i < ret; i++) { int sockfd = events[i].data.fd;}
epoll監(jiān)控多個文件描述符的I/O事件。epoll支持邊緣觸發(fā)(edge trigger,ET)或水平觸發(fā)(level trigger,LT),通過epoll_wait等待I/O事件,如果當前沒有可用的事件則阻塞調(diào)用線程。
select和poll只支持LT工作模式,epoll的默認的工作模式是LT模式。
水平觸發(fā):
邊沿觸發(fā):
所以,邊沿觸發(fā)模式很大程度上降低了同一個epoll事件被重復觸發(fā)的次數(shù),所以效率更高。
#include #include #include #include #include #include #include #include #include #include #define MAXEVENTS 64static 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;}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;}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;}
select和poll的動作基本一致,只是poll采用鏈表來進行文件描述符的存儲,而select采用fd標注位來存放,所以select會受到最大連接數(shù)的限制,而poll不會。
select、poll、epoll雖然都會返回就緒的文件描述符數(shù)量。但是select和poll并不會明確指出是哪些文件描述符就緒,而epoll會。造成的區(qū)別就是,系統(tǒng)調(diào)用返回后,調(diào)用select和poll的程序需要遍歷監(jiān)聽的整個文件描述符找到是誰處于就緒,而epoll則直接處理即可。
select、poll都需要將有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)拷貝進內(nèi)核,最后再拷貝出來。而epoll創(chuàng)建的有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)本身就存于內(nèi)核態(tài)中。
select、poll采用輪詢的方式來檢查文件描述符是否處于就緒態(tài),而epoll采用回調(diào)機制。造成的結(jié)果就是,隨著fd的增加,select和poll的效率會線性降低,而epoll不會受到太大影響,除非活躍的socket很多。
epoll的邊緣觸發(fā)模式效率高,系統(tǒng)不會充斥大量不關(guān)心的就緒文件描述符。
雖然epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調(diào)。
這一講偏理論,主要講了Linux中三種IO復用。后面幾講會在這一講的基礎上,圍繞IO寫一些有趣的實戰(zhàn)demo,敬請期待。
參考
- https://blog.csdn.net/weixin_42145502/article/details/107320539?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163011698816780262548239%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163011698816780262548239&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-107320539.pc_v2_rank_blog_default&utm_term=IO%E5%A4%8D%E7%94%A8&spm=1018.2226.3001.4450
- 《Linux高性能服務器編程》
- https://juejin.cn/post/6882984260672847879
- https://zhuanlan.zhihu.com/p/115220699
- https://man7.org/linux/man-pages/man2/poll.2.html
- https://zhuanlan.zhihu.com/p/159135478
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/118798.html
摘要:當一個文件要被多個處理,那么一定要指定執(zhí)行的先后順序先執(zhí)行在執(zhí)行參考 webpack系列文章: 【W(wǎng)ebpack 性能優(yōu)化系列(2) - source-map】【W(wǎng)...
摘要:模塊什么是模塊什么是模塊化玩過游戲的朋友應該知道,一把裝配完整的步槍,一般是槍身消音器倍鏡握把槍托。更重要的是,其它大部分語言都支持模塊化。這一點與規(guī)范完全不同。模塊輸出的是值的緩存,不存在動態(tài)更新。 1.模塊 1.1 什么是模塊?什么是模塊化? 玩過FPS游戲的朋友應該知道,一把裝配完整的M4步槍,一般是槍身+消音器+倍鏡+握把+槍托。 如果把M4步槍看成是一個頁面的話,那么我們可以...
摘要:大家好,我是冰河有句話叫做投資啥都不如投資自己的回報率高。馬上就十一國慶假期了,給小伙伴們分享下,從小白程序員到大廠高級技術(shù)專家我看過哪些技術(shù)類書籍。 大家好,我是...
閱讀 2646·2021-11-23 09:51
閱讀 914·2021-09-24 10:37
閱讀 3632·2021-09-02 15:15
閱讀 1973·2019-08-30 13:03
閱讀 1894·2019-08-29 15:41
閱讀 2640·2019-08-29 14:12
閱讀 1437·2019-08-29 11:19
閱讀 3313·2019-08-26 13:39