目錄
一、了解IO模型
(一)異步IO和同步IO
(二)五種IO快速回顧
二、IO多路復用
(一)IO 多路復用模型
(二)select 實現原理
(三)poll 實現原理
(四)epoll 實現原理
(五)總結
三、總結
參考推薦閱讀
干貨分享,感謝您的閱讀!
在現代計算機系統中,輸入/輸出(I/O)操作的效率直接影響到整體性能與用戶體驗。無論是大型服務器處理海量請求,還是嵌入式設備與傳感器的實時數據交互,了解 I/O 模型及其多路復用機制顯得尤為重要。本文將深入探討 I/O 的五種主要模型,包括阻塞、非阻塞、同步、異步以及信號驅動 I/O,解析它們的工作原理與適用場景。同時,我們還將聚焦多路復用技術的核心概念與實現方式,揭示其在提升并發處理能力、降低資源消耗中的關鍵作用。通過對這些基礎理論的深入理解,您將能夠更有效地設計與優化高性能應用程序,推動技術的不斷進步與創新。
一、了解IO模型
I/O 模型是指在進行輸入輸出操作時,操作系統和應用程序之間如何進行交互的方式。
(一)異步IO和同步IO
上圖中,阻塞式I/O、非阻塞式I/O、I/O復用、信號驅動式I/O 在操作系統層面都是同步IO,它們都會阻塞在數據從內核空間復制到用戶空間的緩沖區;異步IO模型在兩個階段都不會阻塞調用進程,在操作系統層面實現真正的異步IO。
同步 I/O:
- 同步 I/O 模型是指進程發起一個 I/O 操作后,必須等待這個操作完成才能進行下一步操作。在這種模型下,當應用程序發起一個 I/O 請求時,它會被阻塞,直到操作系統將數據從內核空間復制到用戶空間的緩沖區中,然后應用程序才能繼續執行。
- 阻塞式 I/O、非阻塞式 I/O、I/O 復用、信號驅動式 I/O 這些模型都屬于同步 I/O,因為它們都需要應用程序等待 I/O 操作完成才能進行下一步操作。
異步 I/O:
- 異步 I/O 模型則不同,當應用程序發起一個 I/O 請求后,它可以立即繼續執行其他任務,不需要等待操作完成。當 I/O 操作完成后,操作系統會通知應用程序。
- 異步 I/O 模型確實能夠在兩個階段都不阻塞調用進程,因為應用程序發起請求后就可以繼續執行其他任務,而不必等待數據從內核空間復制到用戶空間的緩沖區中。
- 異步 I/O 模型通常需要操作系統或硬件設備的支持,以便在 I/O 操作完成時通知應用程序。通常涉及到事件驅動的編程模式,比如回調函數或事件循環。異步 I/O 通常用于處理大量的并發連接或需要高性能的應用程序中。
(二)五種IO快速回顧
模型 | 描述 | 適用場景 |
---|---|---|
阻塞式 I/O | 應用程序發起 I/O 請求后被阻塞,直到操作完成。 | 單任務環境,簡單應用程序,對于少量連接或低并發的應用。 |
非阻塞式 I/O | 應用程序發起 I/O 請求后繼續執行,但需要通過輪詢等方式檢查操作是否完成。 | 單任務環境,需要處理多個 I/O 事件,但需要謹慎處理輪詢造成的 CPU 消耗。 |
I/O 復用 | 允許一個進程同時監視多個文件描述符的 I/O 事件,當某個文件描述符準備好時通知應用程序。 | 需要同時處理多個 I/O 事件的場景,如網絡服務器。 |
信號驅動式 I/O | 將信號處理函數與文件描述符關聯,當文件描述符準備好進行 I/O 操作時,觸發相應的信號處理函數。 | 某些需要提高應用程序性能的情況,但在處理復雜 I/O 事件時可能變得復雜。 |
異步 I/O | 應用程序發起 I/O 請求后立即返回,當操作完成時,操作系統通知應用程序。 | 需要高性能和高并發的應用程序,可以在兩個階段都不阻塞調用進程。 |
二、IO多路復用
(一)IO 多路復用模型
在使用多路復用模型時,通常會使用像 select()
、poll()
、epoll()
這樣的系統調用。這些調用允許應用程序同時監視多個文件描述符,等待其中任何一個文件描述符就緒(即有數據可讀或可寫)。當文件描述符就緒時,這些系統調用會返回,并告知應用程序哪些文件描述符已經就緒。
然后,應用程序可以進一步操作就緒的文件描述符,比如調用 recvfrom()
函數將數據從內核空間拷貝到用戶空間。盡管這個階段仍然是阻塞的,但因為在 select()
或其他多路復用系統調用中已經知道哪些文件描述符有數據可讀,因此整體上效率會有很大的提升。
(二)select 實現原理
select()
函數是一個用于多路復用 I/O 的系統調用,它允許一個進程監視多個文件描述符的狀態,以確定它們是否處于可讀、可寫或異常狀態。其基本原理是在內核中檢查指定的文件描述符,并在其中任何一個文件描述符就緒時返回。
具體地,select()
函數參數分析如下:
- nfds:要檢查的文件描述符的數量,即最大文件描述符加一。
- readfds:指向包含要檢查是否可讀的文件描述符集合的指針。
- writefds:指向包含要檢查是否可寫的文件描述符集合的指針。
- exceptfds:指向包含要檢查是否有異常情況的文件描述符集合的指針。
- timeout:超時時間,指定
select()
調用的最長等待時間,當為NULL
時表示永遠等待。
調用 select()
后,內核會遍歷傳入的文件描述符集合,檢查它們的狀態。如果有任何一個文件描述符就緒(可讀、可寫或異常),select()
就會返回。返回后,可以通過檢查相應的文件描述符集合來確定哪些文件描述符處于就緒狀態。
在實際使用中,通常會使用 FD_SET()
、FD_CLR()
、FD_ISSET()
等宏來設置和檢查文件描述符集合。
具體可以看下以下代碼中的標注:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define MAXCLINE 5 // 最大連接數
#define MAXBUF 1024 // 緩沖區大小int main(void)
{int sock_fd, i, ret;int fd[MAXCLINE]; // 存放連接的文件描述符隊列struct sockaddr_in server_addr, client_addr;socklen_t sin_size = sizeof(struct sockaddr_in);char buf[MAXBUF];// 創建 socketsock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd < 0) {perror("socket");exit(EXIT_FAILURE);}// 綁定 socketserver_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);server_addr.sin_addr.s_addr = INADDR_ANY;memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind");exit(EXIT_FAILURE);}// 監聽 socketif (listen(sock_fd, 5) < 0) {perror("listen");exit(EXIT_FAILURE);}// 接受連接,并將連接的文件描述符存入數組for (i = 0; i < MAXCLINE; i++) {fd[i] = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);if (fd[i] < 0) {perror("accept");exit(EXIT_FAILURE);}}// 循環監聽連接上的數據是否到達while (1) {fd_set fdsr; // 用于存放需要監聽的文件描述符集合FD_ZERO(&fdsr); // 清空文件描述符集合// 將要監聽的文件描述符加入集合int max = 0;for (i = 0; i < MAXCLINE; i++) {FD_SET(fd[i], &fdsr);if (fd[i] > max)max = fd[i];}// 調用 select 進行多路復用ret = select(max + 1, &fdsr, NULL, NULL, NULL);if (ret < 0) {perror("select");exit(EXIT_FAILURE);}// 遍歷文件描述符集合,判斷哪些連接有數據到達for (i = 0; i < MAXCLINE; i++) {if (FD_ISSET(fd[i], &fdsr)) { // 文件描述符有數據到達ret = recv(fd[i], buf, sizeof(buf), 0);if (ret < 0) {perror("recv");exit(EXIT_FAILURE);} else if (ret == 0) { // 對方關閉連接printf("Connection closed by client.\n");close(fd[i]);FD_CLR(fd[i], &fdsr); // 從文件描述符集合中清除} else {buf[ret] = '\0';printf("Received message from client %d: %s\n", i + 1, buf);}}}}return 0;
}
select 的執行過程
在服務器進程 A 啟動的時候,要監聽的連接的 socket 文件描述符是 3、4、5,如果這三個連接均沒有數據到達網卡,則進程 A 會讓出 CPU,進入阻塞狀態,同時會將進程 A 的進程描述符和被喚醒時用到的回調函數組成等待隊列項加入到 socket 對象 3、4、5 的進程等待隊列中,注意,這時 select 調用時fdsr 文件描述符集會從用戶空間拷貝到內核空間,如下圖所示:
當網卡接收到數據,然后網卡通過中斷信號通知 CPU 有數據到達,執行中斷程序,中斷程序主要做了兩件事:
- 將網絡數據寫入到對應 socket 的數據接收隊列里面;
- 喚醒隊列中的等待進程 A,重新將進程 A 放入 CPU 的運行隊列中;
假設連接 3、5 有數據到達網卡,注意,這時 select 調用結束時,fdsr 文件描述符集會從內核空間拷貝到用戶空間:
(三)poll 實現原理
poll()
函數是一種多路復用 I/O 模型,與 select()
類似,允許一個進程監視多個文件描述符,等待其中任何一個文件描述符就緒(即有數據可讀或可寫):
-
準備文件描述符數組:在調用
poll()
函數之前,需要準備一個struct pollfd
類型的數組,數組的每個元素對應一個待監視的文件描述符。結構體中包含文件描述符的值以及所關心的事件,如可讀、可寫等。 -
調用
poll()
:一旦文件描述符數組準備好,程序會調用poll()
函數,將數組和數組中元素的個數傳遞給它。poll()
函數會在這些文件描述符中的任何一個就緒時返回。 -
檢查返回值:一旦
poll()
函數返回,程序會檢查返回值,以確定哪些文件描述符已經就緒。通常返回值表示就緒文件描述符的個數。 -
處理就緒文件描述符:程序員可以遍歷檢查每個文件描述符對應的
pollfd
結構體,以確定哪些文件描述符處于就緒狀態。然后可以執行相應的讀取或寫入操作。
在內核層面,poll()
函數的實現通常會使用輪詢機制,檢查每個文件描述符是否已經就緒。不同于 select()
,poll()
傳遞的是指向結構體數組的指針,因此無需復制文件描述符集合到內核空間,這在一定程度上減少了開銷。
與 select()
不同的是,poll()
沒有最大文件描述符數的限制,因為它使用了結構體數組而不是位圖來表示文件描述符。因此poll()
在處理大量文件描述符時,通常比 select()
更有效率。
使用 poll()
函數簡單實現多路復用 I/O:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <poll.h>#define MAX_CLIENTS 5
#define PORT 8888int main() {int i, ret, sockfd, newsockfd;struct sockaddr_in serv_addr, cli_addr;socklen_t addrlen = sizeof(struct sockaddr_in);struct pollfd fds[MAX_CLIENTS + 1]; // +1 for the listening socket// Create socketsockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}// Set up server addressmemset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(PORT);// Bind socketif (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind");exit(EXIT_FAILURE);}// Listen for incoming connectionsif (listen(sockfd, MAX_CLIENTS) < 0) {perror("listen");exit(EXIT_FAILURE);}// Initialize pollfd structuresfds[0].fd = sockfd;fds[0].events = POLLIN;for (i = 1; i <= MAX_CLIENTS; i++) {fds[i].fd = -1;}while (1) {// Call pollret = poll(fds, MAX_CLIENTS + 1, -1);if (ret < 0) {perror("poll");exit(EXIT_FAILURE);}// Check for new connectionif (fds[0].revents & POLLIN) {newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, &addrlen);if (newsockfd < 0) {perror("accept");exit(EXIT_FAILURE);}// Add new client to fds arrayfor (i = 1; i <= MAX_CLIENTS; i++) {if (fds[i].fd == -1) {fds[i].fd = newsockfd;fds[i].events = POLLIN;break;}}if (i > MAX_CLIENTS) {fprintf(stderr, "Too many clients\n");close(newsockfd);}if (--ret <= 0) continue;}// Check for data from clientsfor (i = 1; i <= MAX_CLIENTS; i++) {if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {char buffer[1024];ret = recv(fds[i].fd, buffer, sizeof(buffer), 0);if (ret < 0) {perror("recv");close(fds[i].fd);fds[i].fd = -1;} else if (ret == 0) {printf("Client disconnected\n");close(fds[i].fd);fds[i].fd = -1;} else {printf("Received message from client %d: %s\n", i, buffer);}if (--ret <= 0) break;}}}close(sockfd);return 0;
}
- 在初始化
pollfd
結構體數組之后,通過poll()
函數等待任何一個文件描述符就緒。 - 如果監聽套接字(
sockfd
)上有新連接到來,poll()
函數會檢測到并調用accept()
接受連接,然后將新的客戶端套接字添加到pollfd
數組中。 - 如果某個客戶端套接字上有數據到達,
poll()
函數同樣會檢測到并調用recv()
接收數據,并進行相應的處理。
(四)epoll 實現原理
epoll
是 Linux 系統提供的一種高效的多路復用 I/O 模型,相比于 select
和 poll
,它在處理大量連接時有更好的性能表現。
數據結構:
epoll
使用了三種主要數據結構:epoll_create()
創建的 epoll 實例,epoll_ctl()
用于修改監聽的文件描述符集合,以及epoll_wait()
用于等待就緒事件的函數。- 內核維護了一個紅黑樹(rbtree),用于存儲需要監聽的文件描述符。這個樹的節點是一個
epitem
結構體,包含文件描述符、事件類型等信息。 - 另外,內核還維護了一個鏈表,用于存儲當前就緒的事件,這個鏈表的節點是
rdllink
結構體。
epoll 實例:
- 使用
epoll_create()
創建一個 epoll 實例,返回一個文件描述符,這個描述符代表了一個 epoll 對象。 - epoll 實例是一個文件描述符,通過對這個描述符進行操作,可以控制對哪些文件描述符進行監聽以及對就緒事件的處理。
添加和刪除文件描述符:
- 使用
epoll_ctl()
函數向 epoll 實例中添加或刪除要監聽的文件描述符。 - 添加文件描述符時,會創建一個
epitem
結構體,將其插入到紅黑樹中。 - 刪除文件描述符時,會將對應的
epitem
結構體從紅黑樹中移除。
等待就緒事件:
- 使用
epoll_wait()
函數等待文件描述符的就緒事件。 - 內核會遍歷紅黑樹,檢查哪些文件描述符已經就緒,將其加入到就緒鏈表中。
- 用戶空間可以通過
epoll_wait()
返回的事件列表來獲取就緒的文件描述符,并進行相應的處理。
總的來說,epoll
的實現原理涉及了三個主要的數據結構:紅黑樹、鏈表和 epoll
實例。它通過將文件描述符的就緒事件保存在內核空間的數據結構中,避免了 select
和 poll
中頻繁的內存復制操作,從而提高了處理大量連接時的效率。
epoll 的基本用法是:
int main(void) {struct epoll_event events[5];int epfd = epoll_create(10); // 創建一個 epoll 對象......for(i = 0; i < 5; i++){static struct epoll_event ev;.....ev.data.fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);ev.events = EPOLLIN;epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); // 向 epoll 對象中添加要管理的連接}while(1){nfds = epoll_wait(epfd, events, 5, 10000); // 等待其管理的連接上的 IO 事件for(i=0; i<nfds; i++){......read(events[i].data.fd, buff, MAXBUF)}}
主要涉及到三個函數:
// 創建一個 eventpoll 內核對象
int epoll_create(int size);
// 將連接到socket對象添加到 eventpoll 對象上,epoll_event是要監聽的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待連接 socket 的數據是否到達
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epoll_create
epoll_ctl
epoll_wait
(五)總結
下面是使用 select
、poll
和 epoll
實現 IO 多路復用的簡單對比分析:
特點 | select | poll | epoll |
---|---|---|---|
文件描述符數量限制 | 通常受限于文件描述符數量 | 通常受限于文件描述符數量 | 沒有顯著限制 |
內核空間數據結構 | 使用位圖表示文件描述符集合 | 使用數組表示文件描述符集合 | 使用紅黑樹存儲文件描述符 |
內存復制開銷 | 需要將文件描述符集合復制到內核空間 | 需要將文件描述符集合復制到內核空間 | 無需將文件描述符集合復制到內核空間 |
時間復雜度 | O(n) | O(n) | O(log n) |
適用場景 | 文件描述符數量少且不頻繁變化 | 文件描述符數量少且不頻繁變化 | 文件描述符數量大且頻繁變化 |
并發連接處理性能 | 性能較差 | 性能較好 | 性能最佳 |
從上表中可以看出:
select
和poll
的效率都受到文件描述符數量的限制,并且需要將文件描述符集合復制到內核空間,因此在處理大量連接時效率較低。epoll
利用了紅黑樹來存儲文件描述符,避免了內存復制的開銷,并且在文件描述符數量較大且頻繁變化時性能最佳。- 因此,對于高并發的網絡應用,通常選擇
epoll
來實現 IO 多路復用,而select
和poll
則適用于文件描述符數量較少且不頻繁變化的情況。
三、總結
在本文中,我們探討了 I/O 操作的五種主要模型及其在現代計算機系統中的應用,分別是阻塞 I/O、非阻塞 I/O、同步 I/O、異步 I/O 和信號驅動 I/O。通過分析這些模型的特點與適用場景,我們了解到每種模型都有其獨特的優勢與局限性,開發者可以根據具體需求選擇最合適的 I/O 方案。
此外,我們深入討論了多路復用技術,包括 select、poll 和 epoll 等實現方式,揭示了它們在處理高并發請求中的重要性。多路復用不僅能夠有效減少系統資源的浪費,還能夠提升 I/O 操作的響應速度,使得高性能應用得以順暢運行。
通過對 I/O 模型與多路復用技術的深入理解,開發者將能夠更好地優化應用程序,提升整體性能,為用戶提供更加流暢的體驗。隨著技術的不斷發展,掌握這些基礎知識將為您在計算機科學與軟件開發領域的進一步探索打下堅實的基礎。
參考推薦閱讀
徹底搞懂IO模型:五種IO模型透徹分析 | 駿馬金龍
高性能IO模型分析-IO模型簡介(一) - 知乎
https://blog.51cto.com/u_15287666/4917767
IO模型:BIO、NIO、AIO的解析與應用-百度開發者中心
談談你對IO多路復用機制的理解-io 多路復用
https://www.cnblogs.com/yrxing/p/14143644.html
https://www.cnblogs.com/88223100/p/Deeply-learn-the-implementation-principle-of-IO-multiplexing-select_poll_epoll.html
你管這破玩意叫 IO 多路復用?-阿里云開發者社區
select - 徹底搞懂IO多路復用 - 個人文章 - SegmentFault 思否