應用程式設計基礎課第一講:程式設計基礎知識
本人從事linux下web程式設計多年,最近有幸給組內同學做培訓,希望能給大家介紹下自己這些年在應用程式設計方面的經驗,今天先給大家介紹下一些程式設計方面的需要掌握的基礎知識:
作業系統介紹
先來看一個unix系統的架構圖:
從內向外,unix系統架構分為:
- 核心:控制硬體資源,提供應用程式執行的環境
- 系統呼叫:核心的程式設計介面
- shell和庫函式:為應用程式提供程式設計、執行介面
- 應用程式:我們自己編寫的程式
系統呼叫和庫函式
應用程式可以呼叫系統呼叫和庫函式,很多庫函式都會呼叫系統呼叫,用下面這幅圖來展示二者的區別:
上面說過,核心用於控制硬體資源。例如從磁碟上讀寫檔案,相當於需要控制硬碟這個硬體做IO操作,這個事情需要核心來做,那如何告訴核心要做這個事情呢?系統呼叫就是幹這個事情的,它是核心暴露出來的一組程式設計介面,通過呼叫這個介面來執行核心中的程式碼。
庫函式不是核心程式碼,是更高一層的功能封裝。如我們常用的 printf
函式,我們可以呼叫這個函式輸出內容到顯示器上,但控制顯示器的輸出是核心做的事情,系統呼叫提供的是 write
方法, printf
相當於封裝了 write
這個系統呼叫,給應用程式提供了一個更加友好的操作方式。
總結下,系統呼叫相當於核心程式碼的呼叫入口,庫函式是對應用程式要使用的功能的一層友好的封裝。
使用者態和核心態
程式在執行時會有使用者態和核心態的區別,請見下圖:
簡單一句話,程式執行時,如果執行的是我們編寫的應用程式的程式碼,這些程式碼就是執行在使用者態的;當代碼中呼叫了系統呼叫後,接下來核心中的程式碼就會執行,核心中的程式碼就是執行在核心態的。
程式是如何執行的
我們用最經典的 hello world
來說明下程式是如何執行的。
程式原始碼 hello.c
:
#include <stdio.h> int main() { printf("Hello, world!\n"); return 0; }
首先,我們開發的 hello.c
儲存在磁碟上,我們首先編譯它,得到可執行檔案 hello
:
ligang@vm-xubuntu ~/tmp/c $ gcc -o hello hello.c ligang@vm-xubuntu ~/tmp/c $ ll total 16 -rwxrwxr-x 1 ligang ligang 8296 9月15 16:07 hello -rw-rw-r-- 1 ligang ligang80 9月15 16:05 hello.c
這一步編譯,實際上經歷瞭如下處理流程:
當我們執行hello程式時,效果如下:
ligang@vm-xubuntu ~/tmp/c $ ./hello Hello, world
實際執行流程如下:
首先,我們通過鍵盤輸入 ./hello
,shell讀取每個字元到暫存器中,然後儲存到主存中:
當我們輸入 enter
後,shell知道我們完成了命令輸入,將hello這個程式從磁碟載入到主存:
使用DMA(direct memory access)技術,資料從磁碟直接載入到主存中,避免了通過CPU傳輸。
當代碼載入到主存後,CPU開始執行程式指令,這些指令拷貝 hello, world\n
字串從記憶體到暫存器,然後傳輸到顯示裝置,顯示裝置負責顯示結果:
程序、執行緒、協程
我們先來看下一個經典的程式在記憶體中的佈局:
-
text
段就是我們的程式程式碼 -
initialized data
中是我們程式碼中明確初始化的一些全域性變數、靜態變數 -
bss
段中放的是程式碼中沒有初始化的全域性變數、靜態變數 -
heap
是我們程式中動態申請的記憶體空間,例如呼叫malloc
,這個空間很大 -
stack
中的空間由程式中的區域性變臉和函式呼叫使用
程序
什麼是程序呢?我理解程序就是執行中的程式在作業系統中的一個具象化的表現。
每個程序在記憶體中都擁有上面那些佈局,而且都是獨立的,不共享。
執行緒
那麼什麼是執行緒呢?我自己理解,每個執行緒代表了一個獨立的執行流。
舉個例子,hello那個程式中,如果printf語句要執行10次,那麼這10次可以一個挨著一個順序來執行,這是一個執行流;
也可以建立10個執行流,每個都執行一次printf,如果我們有至少10個cpu核,這樣就可以做到10個printf並行執行。
所以,執行緒是作業系統排程的最小單元。
同一程序的多執行緒間,棧空間(stack)是相互獨立的,而其它資料是共享的,所以高效能的多執行緒程式設計首先要解決的就是鎖的問題。
協程
一些現代的程式語言會有這個概念,它又是什麼呢?
我理解,協程是一個使用者態的概念。
執行緒是由作業系統核心進行排程的,我們無法干預,協程是使用者態程式,相當於應用程式自己進行了排程。
因為它是使用者態程式,所以相當於多個協程會執行在一個執行緒中。
要注意的是,只有核心對執行緒的排程才能夠利用cpu的多核資源,讓程式做到並行,所以在一個執行緒中的多個協程,是無法做到並行的。
父子程序間的共享
我們來看一張圖:
這張圖描述的是當我們在程式中用 open
這個系統呼叫開啟一個檔案時,作業系統中維護的相關資料結構,這裡最重要的一個說明,就是最左面 process table entry
部分可以理解為是使用者態程式中的,其它的 file table entry
和 v-node table entry
是核心中的。
當我們使用 fork
建立新程序後,使用者態的內容都會複製一份給子程序,而核心中的部分會保持不變,如下圖:
所以,父子程序相當於可以通過相同的檔案描述符來共享核心中的資料結構。這個特性,就是未來我們做服務的平滑重啟的最重要的一點。
結束語
上面這些,都是筆者程式設計這些年,覺得非常受用的基礎知識。正確的認識這些知識,很多問題你都可以自己想明白了。
筆者也是在不斷學習中,如果有錯誤的地方,還望指正,我們共同進步,謝謝!
參考
APUE: ofollow,noindex" target="_blank">https://book.douban.com/subject/1788421/
深入理解計算機系統: https://book.douban.com/subject/5407246/