1. 程式人生 > >C語言的輸入輸出模型

C語言的輸入輸出模型

其實對於計算機器的理解,難點是為什麼會有這個模型的建立,也就是模型建立的實際意義,另外還有一點,這個模型的形象化表示,如果能理解這個模型的形象化表示,就可以更深刻的理解模型。
網路或者書籍往往不能這樣去解釋模型的意義以及模型的形象化表示,所以學習計算機的初期,你或許會有點進步,但是學到最後往往會很迷茫,這時候有個導師幫助你顯得尤為重要。

個人以前並沒有深入去研究輸入輸出模型,但是,網路程式設計卻使用了這個模型,導致在做資料傳輸的時候各種迷茫,只能退到C語言的層面,瞭解C語言的基本輸入輸出模型,因為soket描述符其實就是一個檔案描述符,但是這個描述符有其特殊的意義,但是,這個描述符的輸入輸出模型和一般性的檔案描述符都是類似的。

先給出模型圖,稍後再解釋這個模型圖:
這裡寫圖片描述

這篇文章會很長,因為需要解釋的東西很多,希望大家有耐心的讀下去,我保證所有的內容都是些基本的概念(但是我不保證概念的正確性,畢竟這是個人見解,又沒有做事實論證,我的目的是你能夠理解輸入輸出模型形象化的表示,至於理解的過程因人而異),有些概念如果有疑問,請留言,我會直接測試一下。如果可以的話,我會對概念進行測試。使用的作業系統是centos6/centos7(核心是linux, 不使用windows的原因是我個人正在研究《UNIX網路程式設計》)。

首先,大部分的輸入輸出模型就是上圖所展示的,部分輸入輸出模型是上圖的簡化,其餘的我暫時沒有概念。

我們先來描述這個模型圖,然後解釋每個組成部分的意義(這些都是個人的見解,不一定對,但是絕對有參考價值)。

“裝置”其實就是檔案描述符所指定的東西,這個“裝置”對應LINUX作業系統就是檔案(一切皆檔案),對應實際的物理裝置就是硬碟、網絡卡、鍵盤、顯示器等等。我們為了理解上的方便,暫時定義這個“裝置”就是“檔案”。

“輸出緩衝區”與“輸入緩衝區”這部分對應作業系統來說一般就是“記憶體”這個物理裝置,但是,我們不關心他的具體形式,我們關心的是,“程式碼”部分可以直接對這部區域進行讀或寫,那麼問題來了,“程式碼”部分能不能直接對“裝置”讀或寫呢?答案是肯定的。既然可以直接讀寫“裝置”,為什麼還要有“輸入輸出緩衝區”呢?這個我後文進行闡述,這也是整個模型理解起來最困難的地方。

“程式碼”部分就是我們C語言編輯的部分,這部分對於程式設計師來說是可見的部分。

“A”過程是把“裝置”的內容寫入到“輸入緩衝區”,對應的作業系統API是“read”(低階I/O)。

“B”過程是把“輸入緩衝區”的內容寫入“程式碼”,對應的標準庫API是“getc”等(高階I/O)

“C”過程是把“程式碼”的內容寫入到“輸出緩衝區 ”,對應的標準庫API是“putc”等(高階I/O)

“D”過程是把“輸出緩衝區”寫入到“裝置”, 對應的作業系統API是“write”(低階I/O)

對於實際的實現,“輸入輸出緩衝區”部分還有一些特性,我們忽略這個特性,我們簡單的模擬一下,程式碼是如何實現讀的操作的,我們以標準輸入stdin做為例子。
我們在終端部分輸入一個內容,當我們按下回車的時候,作業系統喚醒程式碼的讀取程式碼(比如我們正在等待getc),假設我們的輸入時“abcd回車”,然後這個內容就首先會寫入到輸入緩衝區(A過程),假設輸入緩衝區足夠的大,那麼“abcd回車”就會全部寫入到“輸入緩衝區”,接著就會寫入到程式碼(B過程),這時候我們的程式碼緊緊消費了一個字元“a”,輸入緩衝區還有“bcd回車”等,如果我們再次呼叫getc,我們的緊緊會執行B過程,這時候的getc也不會阻塞,因為輸入緩衝區是有資料的而且是足夠的,如果我們持續消費,導致輸入緩衝區為空,我們再次使用getc函式,這時候getc函式就會阻塞,等待喚醒,整個過程就會重複。(個人沒有程式碼實踐,可以的話最好使用程式碼實驗一下)

stdout擁有類似的過程,但是這還是有些不一樣,C過程的部分就是putc所做的工作,D過程是write控制的,這部分一般會有兩個問題,D過程是否可能會阻塞?答案是肯定的,D過程可能會阻塞的,比如網絡卡裝置的寫,如果使用TCP協議,TCP協議是有流量控制的,這時候D過程就會阻塞。D過程什麼時候執行?一般有兩種方式,一種是自動的,比如設定一個條件,當“輸出緩衝區”滿的時候,執行D過程,還有一種方式是手動的,比如我們可以執行fflush()方法,或者執行fclose()方法,這些方法就會執行D過程。

如果能夠理解上面所說的流程方式,我在說下,我對“輸入輸出緩衝區”的理解,這部分有幾個好處:

  1. 我們可以遮蔽掉低階I/O的實現,低階I/O的實現依賴作業系統本身核心的實現,所以如果能夠遮蔽這部分的差異,我們可以很容易寫出可移植的程式。
  2. 我們可以使用這部分的內容實現“行”讀取的行為,對於計算機而言是沒有“行”這個概念,有了這部分,我們可以定義“行”的概念,然後解析緩衝區的內容,返回一個“行”。

這裡有個設計性的問題,如果我們想遮蔽裝置的差異性,而裝置之間又有某些相同的特性(比如都是關係資料庫),那麼我們就可以利用上面的模型,建立一個“緩衝區”層,這樣程式碼部分只針對“緩衝區”層進行設計。另外,我們還會發現,當我們使用getc的時候,我們還是需要一個檔案描述符號,而檔案描述符是針對裝置的,也就是,不論我們怎麼設計程式碼,上層的程式碼必須告訴底層我需要從什麼裝置讀或者寫操作,我們可以使用類似“檔案描述符”的概念去執行這個通知的操作,但是,你還是必須告訴底層,你到底想幹什麼才行。“緩衝區”層只是遮蔽具體的讀或寫的具體方式,但是沒有遮蔽掉裝置本身的差異性。

這部分的內容對於《UNIX網路程式設計》很有參考意義。因為對於網絡卡的讀或者寫,我們是沒有“緩衝區”層的,標準庫並沒有支援這部分內容,所以我們只能依賴A和D過程。另外,網路程式設計的I/O輸入裝置往往不是單一的,可能是標準輸入裝置和網絡卡裝置同時操作(互動性的程式),這時候,我們往往不能使用標準庫的函式,因為“緩衝區”層對於不是核心的任務,如果有這一層可能會增加程式碼的複雜度。簡單的說,如果你想寫一個“讀行”這個行為,其實是很困難的,因為你不能把使用緩衝層,就只能從裝置依次讀取單個字元,而這個過程是低效的,從裝置讀我們往往使用塊的方式。