摘要:前面幾講手撕了網關服務器回顯服務器服務的代碼,但是這幾個一次只能監聽一個文件描述符,因此性能非常原始低下。復用能使服務器同時監聽多個文件描述符,是服務器性能提升的關鍵。表示要操作的文件描述符,指定操作類型,指定事件。
?本系列文章導航: 手把手寫C++服務器(0):專欄文章-匯總導航【更新中】?
前言: Linux中素有“萬物皆文件,一切皆IO”的說法。前面幾講手撕了CGI網關服務器、echo回顯服務器、discard服務的代碼,但是這幾個一次只能監聽一個文件描述符,因此性能非常原始、低下。IO復用能使服務器同時監聽多個文件描述符,是服務器性能提升的關鍵。雖然IO復用本身是阻塞的,但是和并發技術結合起來,再加上一點設計模式,一個高性能服務器的基石就基本搭建完成了。
目錄
強烈推薦看一下本系列的第25講《手把手寫C++服務器(25):萬物皆可文件之socket fd》
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統。
正在執行的進程,由于期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態的進程(獲得了CPU資源),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的。
緩存I/O又稱為標準I/O,大多數文件系統的默認I/O操作都是緩存I/O。在Linux的緩存I/O機制中,操作系統會將I/O的數據緩存在文件系統的頁緩存中,即數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。
緩存 I/O 的缺點:
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。
IO 多路復用是一種同步IO模型,實現一個線程可以監視多個文件句柄;一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作;沒有文件句柄就緒就會阻塞應用程序,交出CPU。
這是最常用的簡單的IO模型。阻塞IO意味著當我們發起一次IO操作后一直等待成功或失敗之后才返回,在這期間程序不能做其它的事情。阻塞IO操作只能對單個文件描述符進行操作,詳見read或write。
我們在發起IO時,通過對文件描述符設置O_NONBLOCK flag來指定該文件描述符的IO操作為非阻塞。非阻塞IO通常發生在一個for循環當中,因為每次進行IO操作時要么IO操作成功,要么當IO操作會阻塞時返回錯誤EWOULDBLOCK/EAGAIN,然后再根據需要進行下一次的for循環操作,這種類似輪詢的方式會浪費很多不必要的CPU資源,是一種糟糕的設計。和阻塞IO一樣,非阻塞IO也是通過調用read或write來進行操作的,也只能對單個描述符進行操作。
IO多路復用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是類似的,但具體細節各有不同:首先都會對一組文件描述符進行相關事件的注冊,然后阻塞等待某些事件的發生或等待超時。IO多路復用都可以關注多個文件描述符,但對于這三種機制而言,不同數量級文件描述符對性能的影響是不同的,下面會詳細介紹。
信號驅動IO是利用信號機制,讓內核告知應用程序文件描述符的相關事件。
但信號驅動IO在網絡編程的時候通常很少用到,因為在網絡環境中,和socket相關的讀寫事件太多了,比如下面的事件都會導致SIGIO信號的產生:
上面所有的這些都會產生SIGIO信號,但我們沒辦法在SIGIO對應的信號處理函數中區分上述不同的事件,SIGIO只應該在IO事件單一情況下使用,比如說用來監聽端口的socket,因為只有客戶端發起新連接的時候才會產生SIGIO信號。
異步IO和信號驅動IO差不多,但它比信號驅動IO可以多做一步:相比信號驅動IO需要在程序中完成數據從用戶態到內核態(或反方向)的拷貝,異步IO可以把拷貝這一步也幫我們完成之后才通知應用程序。我們使用?aio_read?來讀,aio_write?寫。
同步IO vs 異步IO
1. 同步IO指的是程序會一直阻塞到IO操作如read、write完成
2. 異步IO指的是IO操作不會阻塞當前程序的繼續執行
所以根據這個定義,上面阻塞IO當然算是同步的IO,非阻塞IO也是同步IO,因為當文件操作符可用時我們還是需要阻塞的讀或寫,同理IO多路復用和信號驅動IO也是同步IO,只有異步IO是完全完成了數據的拷貝之后才通知程序進行處理,沒有阻塞的數據讀寫過程。
select的作用是在一段指定的時間內,監聽用戶感興趣的文件描述符上的可讀、可寫、異常等事件。函數原型如下:
#include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
readfds、writefds、exceptfds都是fd_set結構體,timeout是timeval結構體,這里詳解一下這兩個結構體。
1、fd_set
fd_set結構體定義比較復雜,涉及到位操作,比較復雜。所以通常用宏來訪問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; // 秒數 long tv_usec; // 微妙數};
綜上所述,我們一般的使用流程是:
根據使用流程,給出一個代碼示例:
#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之后我們可以同時監聽很多個請求,系統的處理能力大大增強了。
和select類似,在一定時間內輪詢一定數量的文件描述符。
#include int poll(struct pollfd* fds, nfds_t nfds, int timeout);
但是和select不同的是,select需要用三組文件描述符,poll只有一個pollfd文件數組,數組中的每個元素都表示一個需要監聽IO操作事件的文件描述符。而且我們只需要關心數組中events參數,revents由內核自動填充。
struct pollfd { int fd; // 文件描述符 short events; // 注冊的事件 short revents; // 實際發生的事件,由內核填充 };
具體的事件類型參看手冊: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.
綜上所述,我們一般的使用流程是:
根據使用流程,給出一個代碼示例:
#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復用函數,使用一組函數來完成任務,而不是單個函數。
epoll把用戶關心的文件描述符上的事件放在內核的一個事件表中,不需要像select、poll那樣每次調用都要重復傳入文件描述符集或事件集。
epoll需要使用一個額外的文件描述符,來唯一標識內核中的時間表,由epoll_create創建。
#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函數成功時返回就緒的文件描述符總數。select和poll返回文件描述符總數。
以尋找已經就緒的文件描述符,舉個例子如下:
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監控多個文件描述符的I/O事件。epoll支持邊緣觸發(edge trigger,ET)或水平觸發(level trigger,LT),通過epoll_wait等待I/O事件,如果當前沒有可用的事件則阻塞調用線程。
select和poll只支持LT工作模式,epoll的默認的工作模式是LT模式。
水平觸發:
邊沿觸發:
所以,邊沿觸發模式很大程度上降低了同一個epoll事件被重復觸發的次數,所以效率更高。
#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會受到最大連接數的限制,而poll不會。
select、poll、epoll雖然都會返回就緒的文件描述符數量。但是select和poll并不會明確指出是哪些文件描述符就緒,而epoll會。造成的區別就是,系統調用返回后,調用select和poll的程序需要遍歷監聽的整個文件描述符找到是誰處于就緒,而epoll則直接處理即可。
select、poll都需要將有關文件描述符的數據結構拷貝進內核,最后再拷貝出來。而epoll創建的有關文件描述符的數據結構本身就存于內核態中。
select、poll采用輪詢的方式來檢查文件描述符是否處于就緒態,而epoll采用回調機制。造成的結果就是,隨著fd的增加,select和poll的效率會線性降低,而epoll不會受到太大影響,除非活躍的socket很多。
epoll的邊緣觸發模式效率高,系統不會充斥大量不關心的就緒文件描述符。
雖然epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
這一講偏理論,主要講了Linux中三種IO復用。后面幾講會在這一講的基礎上,圍繞IO寫一些有趣的實戰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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/118798.html
摘要:當一個文件要被多個處理,那么一定要指定執行的先后順序先執行在執行參考 webpack系列文章: 【Webpack 性能優化系列(2) - source-map】【W...
摘要:模塊什么是模塊什么是模塊化玩過游戲的朋友應該知道,一把裝配完整的步槍,一般是槍身消音器倍鏡握把槍托。更重要的是,其它大部分語言都支持模塊化。這一點與規范完全不同。模塊輸出的是值的緩存,不存在動態更新。 1.模塊 1.1 什么是模塊?什么是模塊化? 玩過FPS游戲的朋友應該知道,一把裝配完整的M4步槍,一般是槍身+消音器+倍鏡+握把+槍托。 如果把M4步槍看成是一個頁面的話,那么我們可以...
摘要:大家好,我是冰河有句話叫做投資啥都不如投資自己的回報率高。馬上就十一國慶假期了,給小伙伴們分享下,從小白程序員到大廠高級技術專家我看過哪些技術類書籍。 大家好,我是...
閱讀 2627·2021-11-23 09:51
閱讀 860·2021-09-24 10:37
閱讀 3612·2021-09-02 15:15
閱讀 1962·2019-08-30 13:03
閱讀 1881·2019-08-29 15:41
閱讀 2624·2019-08-29 14:12
閱讀 1424·2019-08-29 11:19
閱讀 3301·2019-08-26 13:39