您好,登錄后才能下訂單哦!
這篇文章主要介紹“Unix/Linux接口實例分析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Unix/Linux接口實例分析”文章能幫助大家解決問題。
阻塞型的網絡編程接口
幾乎所有的程序員***次接觸到的網絡編程都是從 listen()、send()、recv()等接口開始的。使用這些接口可以很方便的構建服務器 /客戶機的模型。
我們假設希望建立一個簡單的服務器程序,實現向單個客戶機提供類似于“一問一答”的內容服務。
圖 1. 簡單的一問一答的服務器 /客戶機模型
我們注意到,大部分的 socket接口都是阻塞型的。所謂阻塞型接口是指系統調用(一般是 IO接口)不返回調用結果并讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。
實際上,除非特別指定,幾乎所有的 IO接口 (包括 socket 接口 )都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用 send()的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網絡請求。這給多客戶機、多業務邏輯的網絡編程帶來了挑戰。這時,很多程序員可能會選擇多線程的方式來解決這個問題。
多線程服務器程序
應對多客戶機的網絡應用,最簡單的解決方式是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。
具體使用多進程還是多線程,并沒有一個特定的模式。傳統意義上,進程的開銷要遠遠大于線程,所以,如果需要同時為較多的客戶機提供服務,則不推薦使用多進程;如果單個服務執行體需要消耗較多的 CPU 資源,譬如需要進行大規模或長時間的數據運算或文件訪問,則進程較為安全。通常,使用 pthread_create () 創建新線程,fork() 創建新進程。
我們假設對上述的服務器 / 客戶機模型,提出更高的要求,即讓服務器同時為多個客戶機提供一問一答的服務。于是有了如下的模型。
圖 2. 多線程服務器模型
在上述的線程 / 時間圖例中,主線程持續等待客戶端的連接請求,如果有連接,則創建新線程,并在新線程中提供為前例同樣的問答服務。
很多初學者可能不明白為何一個 socket 可以 accept 多次。實際上,socket 的設計者可能特意為多客戶機的情況留下了伏筆,讓 accept() 能夠返回一個新的 socket。下面是 accept 接口的原型:
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
輸入參數 s 是從 socket(),bind() 和 listen() 中沿用下來的 socket 句柄值。執行完 bind() 和 listen() 后,操作系統已經開始在指定的端口處監聽所有的連接請求,如果有請求,則將該連接請求加入請求隊列。調用 accept() 接口正是從 socket s 的請求隊列抽取***個連接信息,創建一個與 s 同類的新的 socket 返回句柄。新的 socket 句柄即是后續 read() 和 recv() 的輸入參數。如果請求隊列當前沒有請求,則 accept() 將進入阻塞狀態直到有請求進入隊列。
上述多線程的服務器模型似乎***的解決了為多個客戶機提供問答服務的要求,但其實并不盡然。如果要同時響應成百上千路的連接請求,則無論多線程還是多進程都會嚴重占據系統資源,降低系統對外界響應效率,而線程與進程本身也更容易進入假死狀態。
很多程序員可能會考慮使用“線程池”或“連接池”。“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,并讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,盡量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如 websphere、tomcat 和各種數據庫等。
但是,“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用 IO 接口帶來的資源占用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應并不比沒有池的時候效果好多少。所以使用“池” 必須考慮其面臨的響應規模,并根據響應規模調整“池”的大小。
對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。
總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型并不是***方案。下一章我們將討論用非阻塞接口來嘗試解決這個問題。
使用select()接口的基于事件驅動的服務器模型
大部分 Unix/Linux 都支持 select 函數,該函數用于探測多個文件句柄的狀態變化。下面給出 select 接口的原型:
FD_ZERO(int fd, fd_set* fds) FD_SET(int fd, fd_set* fds) FD_ISSET(int fd, fd_set* fds) FD_CLR(int fd, fd_set* fds) int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
這里,fd_set 類型可以簡單的理解為按 bit 位標記句柄的隊列,例如要在某 fd_set 中標記一個值為 16 的句柄,則該 fd_set 的第 16 個 bit 位被標記為 1。具體的置位、驗證可使用 FD_SET、FD_ISSET 等宏實現。在 select() 函數中,readfds、writefds 和 exceptfds 同時作為輸入參數和輸出參數。如果輸入的 readfds 標記了 16 號句柄,則 select() 將檢測 16 號句柄是否可讀。在 select() 返回后,可以通過檢查 readfds 有否標記 16 號句柄,來判斷該“可讀”事件是否發生。另外,用戶可以設置 timeout 時間。
下面將重新模擬上例中從多個客戶端接收數據的模型。
圖4.使用select()的接收數據模型
上述模型只是描述了使用 select() 接口同時從多個客戶端接收數據的過程;由于 select() 接口可以同時對多個句柄進行讀狀態、寫狀態和錯誤狀態的探測,所以可以很容易構建為多個客戶端提供獨立問答服務的服務器系統。
圖5.使用select()接口的基于事件驅動的服務器模型
這里需要指出的是,客戶端的一個 connect() 操作,將在服務器端激發一個“可讀事件”,所以 select() 也能探測來自客戶端的 connect() 行為。
上述模型中,最關鍵的地方是如何動態維護 select() 的三個參數 readfds、writefds 和 exceptfds。作為輸入參數,readfds 應該標記所有的需要探測的“可讀事件”的句柄,其中永遠包括那個探測 connect() 的那個“母”句柄;同時,writefds 和 exceptfds 應該標記所有需要探測的“可寫事件”和“錯誤事件”的句柄 ( 使用 FD_SET() 標記 )。
作為輸出參數,readfds、writefds 和 exceptfds 中的保存了 select() 捕捉到的所有事件的句柄值。程序員需要檢查的所有的標記位 ( 使用 FD_ISSET() 檢查 ),以確定到底哪些句柄發生了事件。
上述模型主要模擬的是“一問一答”的服務流程,所以,如果 select() 發現某句柄捕捉到了“可讀事件”,服務器程序應及時做 recv() 操作,并根據接收到的數據準備好待發送數據,并將對應的句柄值加入 writefds,準備下一次的“可寫事件”的 select() 探測。同樣,如果 select() 發現某句柄捕捉到“可寫事件”,則程序應及時做 send() 操作,并準備好下一次的“可讀事件”探測準備。下圖描述的是上述模型中的一個執行周期。
圖6. 一個執行周期
這種模型的特征在于每一個執行周期都會探測一次或一組事件,一個特定的事件會觸發某個特定的響應。我們可以將這種模型歸類為“事件驅動模型”。
相比其他模型,使用 select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務。如果試圖建立一個簡單的事件驅動的服務器程序,這個模型有一定的參考價值。
但這個模型依舊有著很多問題。
首先,select() 接口并不是實現“事件驅動”的***選擇。因為當需要探測的句柄值較大時,select() 接口本身需要消耗大量時間去輪詢各個句柄。很多操作系統提供了更為高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要實現更高效的服務器程序,類似 epoll 這樣的接口更被推薦。遺憾的是不同的操作系統***的 epoll 接口有很大差異,所以使用類似于 epoll 的接口實現具有較好跨平臺能力的服務器會比較困難。
其次,該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。如下例,龐大的執行體 1 的將直接導致響應事件 2 的執行體遲遲得不到執行,并在很大程度上降低了事件探測的及時性。
圖7. 龐大的執行體對使用select()的事件驅動模型的影響
幸運的是,有很多高效的事件驅動庫可以屏蔽上述的困難,常見的事件驅動庫有 libevent 庫,還有作為 libevent 替代者的 libev 庫。這些庫會根據操作系統的特點選擇最合適的事件探測接口,并且加入了信號 (signal) 等技術以支持異步響應,這使得這些庫成為構建事件驅動模型的不二選擇。下章將介紹如何使用 libev 庫替換 select 或 epoll 接口,實現高效穩定的服務器模型。
使用事件驅動庫libev的服務器模型
Libev 是一種高性能事件循環 / 事件驅動庫。作為 libevent 的替代作品,其***個版本發布與 2007 年 11 月。Libev 的設計者聲稱 libev 擁有更快的速度,更小的體積,更多功能等優勢,這些優勢在很多測評中得到了證明。正因為其良好的性能,很多系統開始使用 libev 庫。本章將介紹如何使用 Libev 實現提供問答服務的服務器。
(事實上,現存的事件循環 / 事件驅動庫有很多,作者也無意推薦讀者一定使用 libev 庫,而只是為了說明事件驅動模型給網絡服務器編程帶來的便利和好處。大部分的事件驅動庫都有著與 libev 庫相類似的接口,只要明白大致的原理,即可靈活挑選合適的庫。)
與前章的模型類似,libev 同樣需要循環探測事件是否產生。Libev 的循環體用 ev_loop 結構來表達,并用 ev_loop( ) 來啟動。
void ev_loop( ev_loop* loop, int flags )
Libev 支持八種事件類型,其中包括 IO 事件。一個 IO 事件用 ev_io 來表征,并用 ev_io_init() 函數來初始化:
void ev_io_init(ev_io *io, callback, int fd, int events)
初始化內容包括回調函數 callback,被探測的句柄 fd 和需要探測的事件,EV_READ 表“可讀事件”,EV_WRITE 表“可寫事件”。
現在,用戶需要做的僅僅是在合適的時候,將某些 ev_io 從 ev_loop 加入或剔除。一旦加入,下個循環即會檢查 ev_io 所指定的事件有否發生;如果該事件被探測到,則 ev_loop 會自動執行 ev_io 的回調函數 callback();如果 ev_io 被注銷,則不再檢測對應事件。
無論某 ev_loop 啟動與否,都可以對其添加或刪除一個或多個 ev_io,添加刪除的接口是 ev_io_start() 和 ev_io_stop()。
void ev_io_start( ev_loop *loop, ev_io* io ) void ev_io_stop( EV_A_* )
由此,我們可以容易得出如下的“一問一答”的服務器模型。由于沒有考慮服務器端主動終止連接機制,所以各個連接可以維持任意時間,客戶端可以自由選擇退出時機。
圖8. 使用libev庫的服務器模型
上述模型可以接受任意多個連接,且為各個連接提供完全獨立的問答服務。借助 libev 提供的事件循環 / 事件驅動接口,上述模型有機會具備其他模型不能提供的高效率、低資源占用、穩定性好和編寫簡單等特點。
由于傳統的 web 服務器,ftp 服務器及其他網絡應用程序都具有“一問一答”的通訊邏輯,所以上述使用 libev 庫的“一問一答”模型對構建類似的服務器程序具有參考價值;另外,對于需要實現遠程監視或遠程遙控的應用程序,上述模型同樣提供了一個可行的實現方案。
關于“Unix/Linux接口實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。