計算機基礎-Socket
當時明月在,曾照彩雲歸。
簡介:計算機基礎-Socket
一、I/O 模型
一個輸入操作通常包括兩個階段:
- 等待資料準備好
- 從核心向程式複製資料
對於一個套接字上的輸入操作,第一步通常涉及等待資料從網路中到達。當所等待資料到達時,它被複制到核心中的某個緩衝區。第二步就是把資料從核心緩衝區複製到應用程式緩衝區。
Unix 有五種 I/O 模型:
- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 複用(select 和 poll)
- 訊號驅動式 I/O(SIGIO)
- 非同步 I/O(AIO)
阻塞式 I/O
應用程式被阻塞,直到資料從核心緩衝區複製到應用程式緩衝區中才返回。
應該注意到,在阻塞的過程中,其它應用程式還可以執行,因此阻塞不意味著整個作業系統都被阻塞。因為其它應用程式還可以執行,所以不消耗 CPU 時間,這種模型的 CPU 利用率會比較高。
下圖中,recvfrom() 用於接收 Socket 傳來的資料,並複製到應用程式的緩衝區 buf 中。這裡把 recvfrom() 當成系統呼叫。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
非阻塞式 I/O
應用程式執行系統呼叫之後,核心返回一個錯誤碼。應用程式可以繼續執行,但是需要不斷的執行系統呼叫來獲知 I/O 是否完成,這種方式稱為輪詢(polling)。
由於 CPU 要處理更多的系統呼叫,因此這種模型的 CPU 利用率比較低。
I/O 複用
使用 select 或者 poll 等待資料,並且可以等待多個套接字中的任何一個變為可讀。這一過程會被阻塞,當某一個套接字可讀時返回,之後再使用 recvfrom 把資料從核心複製到程式中。
它可以讓單個程式具有處理多個 I/O 事件的能力。又被稱為 Event Driven I/O,即事件驅動 I/O。
如果一個 Web 伺服器沒有 I/O 複用,那麼每一個 Socket 連線都需要建立一個執行緒去處理。如果同時有幾萬個連線,那麼就需要建立相同數量的執行緒。相比於多程式和多執行緒技術,I/O 複用不需要程式執行緒建立和切換的開銷,系統開銷更小。
訊號驅動 I/O
應用程式使用 sigaction 系統呼叫,核心立即返回,應用程式可以繼續執行,也就是說等待資料階段應用程式是非阻塞的。核心在資料到達時嚮應用程式傳送 SIGIO 訊號,應用程式收到之後在訊號處理程式中呼叫 recvfrom 將資料從核心複製到應用程式中。
相比於非阻塞式 I/O 的輪詢方式,訊號驅動 I/O 的 CPU 利用率更高。
非同步 I/O
應用程式執行 aio_read 系統呼叫會立即返回,應用程式可以繼續執行,不會被阻塞,核心會在所有操作完成之後嚮應用程式傳送訊號。
非同步 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 複用
select/poll/epoll 都是 I/O 多路複用的具體實現,select 出現的最早,之後是 poll,再是 epoll。
select
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 允許應用程式監視一組檔案描述符,等待一個或者多個描述符成為就緒狀態,從而完成 I/O 操作。
-
fd_set 使用陣列實現,陣列大小使用 FD_SETSIZE 定義,所以只能監聽少於 FD_SETSIZE 數量的描述符。有三種型別的描述符型別:readset、writeset、exceptset,分別對應讀、寫、異常條件的描述符集合。
-
timeout 為超時引數,呼叫 select 會一直阻塞直到有描述符的事件到達或者等待的時間超過 timeout。
-
成功呼叫返回結果大於 0,出錯返回結果為 -1,超時返回結果為 0。
1 fd_set fd_in, fd_out;
2 struct timeval tv;
3
4 // Reset the sets
5 FD_ZERO( &fd_in );
6 FD_ZERO( &fd_out );
7
8 // Monitor sock1 for input events
9 FD_SET( sock1, &fd_in );
10
11 // Monitor sock2 for output events
12 FD_SET( sock2, &fd_out );
13
14 // Find out which socket has the largest numeric value as select requires it
15 int largest_sock = sock1 > sock2 ? sock1 : sock2;
16
17 // Wait up to 10 seconds
18 tv.tv_sec = 10;
19 tv.tv_usec = 0;
20
21 // Call the select
22 int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
23
24 // Check if select actually succeed
25 if ( ret == -1 )
26 // report error and abort
27 else if ( ret == 0 )
28 // timeout; no event detected
29 else
30 {
31 if ( FD_ISSET( sock1, &fd_in ) )
32 // input event on sock1
33
34 if ( FD_ISSET( sock2, &fd_out ) )
35 // output event on sock2
36 }
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
poll 的功能與 select 類似,也是等待一組描述符中的一個成為就緒狀態。
poll 中的描述符是 pollfd 型別的陣列,pollfd 的定義如下:
1 struct pollfd {
2 int fd; /* file descriptor */
3 short events; /* requested events */
4 short revents; /* returned events */
5 };
1 // The structure for two events
2 struct pollfd fds[2];
3
4 // Monitor sock1 for input
5 fds[0].fd = sock1;
6 fds[0].events = POLLIN;
7
8 // Monitor sock2 for output
9 fds[1].fd = sock2;
10 fds[1].events = POLLOUT;
11
12 // Wait 10 seconds
13 int ret = poll( &fds, 2, 10000 );
14 // Check if poll actually succeed
15 if ( ret == -1 )
16 // report error and abort
17 else if ( ret == 0 )
18 // timeout; no event detected
19 else
20 {
21 // If we detect the event, zero it out so we can reuse the structure
22 if ( fds[0].revents & POLLIN )
23 fds[0].revents = 0;
24 // input event on sock1
25
26 if ( fds[1].revents & POLLOUT )
27 fds[1].revents = 0;
28 // output event on sock2
29 }
比較
1. 功能
select 和 poll 的功能基本相同,不過在一些實現細節上有所不同。
- select 會修改描述符,而 poll 不會;
- select 的描述符型別使用陣列實現,FD_SETSIZE 大小預設為 1024,因此預設只能監聽少於 1024 個描述符。如果要監聽更多描述符的話,需要修改 FD_SETSIZE 之後重新編譯;而 poll 沒有描述符數量的限制;
- poll 提供了更多的事件型別,並且對描述符的重複利用上比 select 高。
- 如果一個執行緒對某個描述符呼叫了 select 或者 poll,另一個執行緒關閉了該描述符,會導致呼叫結果不確定。
2. 速度
select 和 poll 速度都比較慢,每次呼叫都需要將全部描述符從應用程式緩衝區複製到核心緩衝區。
3. 可移植性
幾乎所有的系統都支援 select,但是隻有比較新的系統支援 poll。
epoll
1 int epoll_create(int size);
2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_ctl() 用於向核心註冊新的描述符或者是改變某個檔案描述符的狀態。已註冊的描述符在核心中會被維護在一棵紅黑樹上,通過回撥函式核心會將 I/O 準備好的描述符加入到一個連結串列中管理,程式呼叫 epoll_wait() 便可以得到事件完成的描述符。
從上面的描述可以看出,epoll 只需要將描述符從程式緩衝區向核心緩衝區拷貝一次,並且程式不需要通過輪詢來獲得事件完成的描述符。
epoll 僅適用於 Linux OS。
epoll 比 select 和 poll 更加靈活而且沒有描述符數量限制。
epoll 對多執行緒程式設計更有友好,一個執行緒呼叫了 epoll_wait() 另一個執行緒關閉了同一個描述符也不會產生像 select 和 poll 的不確定情況。
1 // Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
2 // The function argument is ignored (it was not before, but now it is), so put your favorite number here
3 int pollingfd = epoll_create( 0xCAFE );
4
5 if ( pollingfd < 0 )
6 // report error
7
8 // Initialize the epoll structure in case more members are added in future
9 struct epoll_event ev = { 0 };
10
11 // Associate the connection class instance with the event. You can associate anything
12 // you want, epoll does not use this information. We store a connection class pointer, pConnection1
13 ev.data.ptr = pConnection1;
14
15 // Monitor for input, and do not automatically rearm the descriptor after the event
16 ev.events = EPOLLIN | EPOLLONESHOT;
17 // Add the descriptor into the monitoring list. We can do it even if another thread is
18 // waiting in epoll_wait - the descriptor will be properly added
19 if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
20 // report error
21
22 // Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
23 struct epoll_event pevents[ 20 ];
24
25 // Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
26 int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
27 // Check if epoll actually succeed
28 if ( ret == -1 )
29 // report error and abort
30 else if ( ret == 0 )
31 // timeout; no event detected
32 else
33 {
34 // Check if any events detected
35 for ( int i = 0; i < ready; i++ )
36 {
37 if ( pevents[i].events & EPOLLIN )
38 {
39 // Get back our connection pointer
40 Connection * c = (Connection*) pevents[i].data.ptr;
41 c->handleReadEvent();
42 }
43 }
44 }
工作模式
epoll 的描述符事件有兩種觸發模式:LT(level trigger)和 ET(edge trigger)。
1. LT 模式
當 epoll_wait() 檢測到描述符事件到達時,將此事件通知程式,程式可以不立即處理該事件,下次呼叫 epoll_wait() 會再次通知程式。是預設的一種模式,並且同時支援 Blocking 和 No-Blocking。
2. ET 模式
和 LT 模式不同的是,通知之後程式必須立即處理事件,下次再呼叫 epoll_wait() 時不會再得到事件到達的通知。
很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。只支援 No-Blocking,以避免由於一個檔案控制程式碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。
應用場景
很容易產生一種錯覺認為只要用 epoll 就可以了,select 和 poll 都已經過時了,其實它們都有各自的使用場景。
1. select 應用場景
select 的 timeout 引數精度為微秒,而 poll 和 epoll 為毫秒,因此 select 更加適用於實時性要求比較高的場景,比如核反應堆的控制。
select 可移植性更好,幾乎被所有主流平臺所支援。
2. poll 應用場景
poll 沒有最大描述符數量的限制,如果平臺支援並且對實時性要求不高,應該使用 poll 而不是 select。
3. epoll 應用場景
只需要執行在 Linux 平臺上,有大量的描述符需要同時輪詢,並且這些連線最好是長連線。
需要同時監控小於 1000 個描述符,就沒有必要使用 epoll,因為這個應用場景下並不能體現 epoll 的優勢。
需要監控的描述符狀態變化多,而且都是非常短暫的,也沒有必要使用 epoll。因為 epoll 中的所有描述符都儲存在核心中,造成每次需要對描述符的狀態改變都需要通過 epoll_ctl() 進行系統呼叫,頻繁系統呼叫降低效率。並且 epoll 的描述符儲存在核心,不容易除錯。
當時明月在
曾照彩雲歸