linux緩衝區概念梳理

緩衝區

我們在linux的開發過程中經常會接觸到緩衝的概念。緩衝一般與輸入輸出聯繫在一起,因爲我們知道I/O設備的速度與內存和CPU的速度往往有好幾個數量級的差異。如果在某一個進程執行時,涉及到I/O相關的操作,等待I/O的操作完全執行完成再繼續執行,會讓CPU和內存在大量時候都處於空閒的狀態。故而需要引入緩衝區這一中間層,相當於CPU,內存 與 I/O設備之間的緩衝地帶。對應用程序而言,將數據放入到緩衝區,就認爲大功告成,後續將緩衝區中的數據具體交付到I/O設備,是由內核或者C庫來負責調度完成的。這一中間層可以大大提高應用程序的執行效率。

具體而言,緩衝區也有很多分類,根據目標的I/O設備來區分,可以有文件緩衝,以及網絡緩衝,這兩塊也是所有I/O操作中最爲重要的;根據緩衝區所在的層次分,可以有用戶態緩衝,以及內核態的緩衝,而對於不同的I/O設備而言,不同的層次的緩衝的具體含義又是不盡相同的;而因爲緩衝區是與I/O設備相關的,自然又涉及到輸入緩衝與輸出緩衝,一般而言輸入緩衝基本由內核來進行管理,我們涉及更多的一般是輸出緩衝。

文件緩衝區

用戶態文件緩衝

我們先從文件緩衝開始說。我個人是嵌入式工程師,下文一般以C來舉例,有讀過《unix環境高級編程》的人會記得,前幾章會先後介紹兩類文件的輸入輸出方式,一是直接I/O,也就是read/write系統調用,二是帶緩衝區的I/O(也叫STD I/O),也就是fread/fwrite,我們在helloworld中接觸的 printf和sscanf當然也屬於這一類,只是限定了I/O的對象是終端標準輸入輸出。fread和fwrite由標準C庫實現,這裏的帶緩衝區,自然是指用戶態的緩衝區。

標準緩衝存在的意義,是因爲write和read屬於系統調用,每次執行都需要從用戶態陷入內核態,而這是需要一定的開銷的,如果一次陷入,只拷貝幾個字節,下次也是幾個字節,就會有比較大的一個內核態和用戶態切換的時鐘浪費。而帶緩衝區的的I/O,對文件描述符fd進行了封裝,得到了FILE類型的句柄,對寫入的數據進行管理。

這裏的緩衝方式有三種,分別是全緩衝,行緩衝和無緩衝,可以由setbuf和setvbuf系統調用通過改變fd對應的屬性進行設置。

無緩衝顧名思義,與write是一樣的,stderr標準錯誤默認使用的是無緩衝。行緩衝是在遇到換行符\n時刷入緩衝區。全緩衝執行刷入緩衝區的情況有三種,一是用戶態緩衝區被填滿,二是fflush調用沖刷緩衝區,三是fclose調用關閉FILE句柄。緩衝區大小由 stdio.h 頭文件中的宏 BUFSIZ 定義,如果希望查看它的大小,包含頭文件,直接輸出它的值即可,默認是8192字節。此即爲文件緩衝在用戶態下的輸出緩衝的基本特性。

內核態文件緩衝

而不管是通過write還是fwrite將數據寫入到文件,仍舊是要經過一層內核的緩衝的,這層就是文件緩衝的內核緩衝區。我們通過free命令可以看到兩塊內容,一個是cache,一個是buffer。buffer就是寫緩衝,爲了解決寫磁盤的效率。寫時會先寫到buffer塊中,再定期地sync,也就是同步數據到磁盤的塊上。這同時減少磁盤的擦寫次數。而cache的存在自然是爲了提高讀磁盤中文件的效率,會按照一定的策略緩存磁盤上應用程序的頁到內存中,根據臨時局部性原理提高讀取文件的效率。在某些時候,我們可以手動調用sync,將內存buffer中的文件落到磁盤上,避免在某些時候異常掉電或者其他原因導致的數據不一致性,linux系統中的update的系統守護進程會週期性地(一般每隔30秒)調用sync函數,另外在系統關機時自然也是會調用sync的。爲了讓應用程序保證數據一致性的處理,內核針對內核緩衝區的控制,提供了3個系統調用,sync,fsync和fdatasync,比如在數據庫操作中,事務操作在實現中需要嚴格保證在返回時數據已經可靠落盤,並要應對掉電異常等場景,就會調用fsync或者fdatasync對寫入到內核中的數據進行強制同步。此即爲文件緩衝在內核態下的輸出緩衝的基本特性。

網絡緩衝

與文件緩衝的核心區別

與文件平起平坐的輸入輸出設備,自然是網絡設備。在介紹網絡緩衝區時,我們優先介紹,網絡緩衝在內核層面的緩衝區,因爲在用戶態層面,網絡緩衝並沒有像文件緩衝那樣,提供一套通用的輸入輸出緩衝機制。絕大部分用戶態下的網絡緩衝機制,是主流的一些網絡庫各自獨立實現的,這我會在之後簡單介紹幾種策略。

內核態網絡緩衝

針對網絡緩衝在內核層面的輸入輸出緩衝區,其實更爲準確的說法應該是發送緩衝區和接收緩衝區。因爲這裏的輸入輸出設備,一般指代的是已經與對端建立連接TCP四元組套接字。

我們知道TCP有滑動窗口控制協議,會控制目前在途的TCP分節的數量,而不是用戶態給多少,內核態就一股腦兒地發多少,與此同時,一段時間內的吞吐量還受到對端處理程序的處理能力的限制,如果一直處於忙碌狀態,則無法從接收緩衝區中將數據包接收到內核態中進行處理,發送這端的套接字如果緩衝區也已經填滿,就會在用戶態下呈現爲無法發送的狀態。即使假定對端處理能力非常強,網絡設備(網卡)的發送速率一般是有限的,比如目前常見家用網絡設備的帶寬的是100Mbps和1000Mbps。用戶態填入到內核緩衝區的數據被髮送出去的速度,也是受這個限制的,不僅如此,一臺主機上有幾十個套接字已經建立連接是非常常見的,而網卡一般只有一張,故而某個特定的socket四元組還需要與所有的的套接字 網卡的輸入輸出帶寬。故而必然會需要內核緩衝區的存在,讓用戶態調用send調用給下來的,但是還沒有辦法發送出去的數據暫存下來,這裏緩衝區設計的初衷可能與 涉及到文件的磁盤I/O爲了效率和提高磁盤讀寫壽命的初衷是不同的。

關於網絡緩衝區的配置,在系統整體層面或者是單個套接字層面,都有默認的經驗值。針對單個套接字的發送和接收緩衝區的大小,可以通過 setsockopt接口和 SO_SNDBUF選項,以及SO_RCVBUF選項來控制。

阻塞與非阻塞的區別

網絡緩衝區與文件緩衝區一樣,是爲了應對I/O設備與內存和CPU的速度不匹配而存在,在網絡內核緩衝區已滿的情況下,顯然用戶態是無法繼續寫入的,這與文件緩衝區不同,但很相似。在針對網絡套接字描述符而言,在內核已滿的情況,就存在兩種不同的場景,阻塞與非阻塞,阻塞顧名思義就是陷入到內核態,等待到緩衝區空閒後,將要寫入到的內容拷貝到內核的空閒緩衝區,而非阻塞則是在內核緩衝區已滿時直接返回,EAGAINBUF,指示發送緩衝區已滿,相當於套接字不可寫,以等待用戶態下的程序後續的處理。而對於文件描述符而言,就我所知是不存在這種限制的。

用戶態下的網絡緩衝區

對於有大量請求需要發送的場景,顯然會出現大量緩衝區滿的情況,如果此時套接字爲阻塞,則程序會經常處於阻塞的狀態,執行效率顯然會大大下降。故而在我的接觸的網絡庫,比如libevent庫和mqtt的庫,都提供了針對非阻塞套接字的異步的發送方式。即針對網絡套接字在用戶態下的緩衝區。

這裏以MQTT爲例,MQTT中的發送爲MQTT_Asyncsend,在調用後會立即返回,具體的套接字發送流程爲內部封裝,實現時將待發送的地址和長度加入到隊列中,在套接字可寫時,從發送隊列的頭部取出節點,將內容寫入到套接字,如果寫入完成,則將節點移出待發送隊列,並清理之前留存的緩衝區。在不可寫時,則通過多路複用等待套接字可操作,或轉而進行其他線程的處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章