20、进程编程基础
進程的基本概念
進程是操作系統設計的核心概念,Multics的設計者在20世紀60年代首次使用了"進程"這個術語,比作業更通用一點。目前存在很多關于進程的定義,例如:
1、一個正在執行的程序。
2、計算機中正在運行的程序的一個實例。
3、可以分配給處理器并由處理器執行的一個實體。
4.由單一的順序的執行線程、一個當前狀態和一組相關的系統資源所描述的活動單元。
也可以把進程是為由一組元素組成的實體,進程的兩個基本元素是程序代碼和代碼相關聯的數據集。假設處理器開始執行程序代碼,那我們把這個執行的實體成為進程。進程執行時,任意給定一個時間,進程都可以用以下元素為唯一表征:
- 標識符:跟這個進程相關地唯一標識符,用來區別其他進程
- 狀態:如果進程正在執行,那么進程處于運行態
- 優先級:相對于其他進程的優先級
- 程序計數器:程序中即將被執行的下一條指令的地址
- 內存地址:包括程序代碼和進程相關數據的指針,還有和其他進程共享內存塊的指針
- 上下文數據:進程執行時處理器的寄存器中的數據
- I/O狀態信息:包括顯式的I/O請求、分配給進程的I/O設備和被進程使用的文件列表
- 記賬信息
上述列表信息被存放在一個稱為進程控制塊的數據結構中,該控制塊由操作系統創建和管理。
比較有意義的一點是,進程控制塊包含充分的信息,這樣當進程被中斷時,操作系統會把程序計數器和處理器寄存器(上下文數據)保存到進程控制塊中的相應位置,進程狀態也被改變為其他值,例如阻塞態或就緒態。現在操作系統可以自由地把其他進程置為運行態,把其他進程的程序計數器和進程上下文數據加載到處理器寄存器,這樣其他進程就可以開始執行。
為什么設計了進程??
設計出一個能夠協調各種不同活動的系統軟件是非常困難的。在任何時刻都有許多作業在運行中,每個作業都包括要求按照順序執行的很多步驟,因此分析時間的序列組合是不可能的。由于缺乏能夠在所有活動中進行協調和合作的系統級的方法,程序員只能基于他們對操作系統所控制的環境的理解,采用自己的特殊方法。然而這種方法是很脆弱的,尤其對于一些程序設計中的小錯誤,因為這些錯誤只有在很少見的時間序列發生時才會出現。由于需要從應用程序軟件錯誤和硬件錯誤中區分出這些錯誤,因而診斷工作是很困難的。及時檢測出錯誤,也很難確定原因,因為很難在線錯誤產生的精確場景。一般而言,產生這類錯誤的4個主要原因如下:
1.不正確同步?
2.失敗互斥。?
3.不確定的程序操作?
4.死鎖
解決這些問題需要一種系統級別的方法監控處理器中不同程序的執行。進程的概念為此提供了基礎。?因此進程可以看做是由三部分組成的:?
1.一段可以執行的程序?
2.程序所需要的相關數據?
3.程序的執行上下文
?
?執行上下文是進程的重重之中。執行上下文(execution context)?又稱作進程狀態(process state),是操作系統用來管理和控制進程所需的內部數據。這種內部信息和進程是分開的,因為操作系統信息不允許被進程直接訪問。上下文包括操作系統管理進程以及處理器正確執行進程所需要的所有信息。包括了各種處理器寄存器的內容,如程序計數器和數據寄存器。它還包括操作系統使用的信息,如進程優先級以及進程是否在等待特定?I/O事件的完成。
圖中兩個進程A 和B ,存在于內存中某部分。也就是說給每個進程(包含程序、數據和上下文信息)分配一塊存儲器區域,并且在由操作系統建立和維護的進程表中進行記錄。進程表中包含記錄每個進程的表現,表項內容包括指向包含進程的存儲塊地址的指針,還包括該進程的部分或全部執行上下文。指向上下文的其余部分存放在別處,可能和進程自己保存在一起,通常也可能保存在內存里一塊獨立的區域中。進程索引寄存器(process index register)?包含當前正在控制處理器的進程在進程表中的索引。程序計數器指向該進程中下一條待執行的指令。基址寄存器(base register)?和界限寄存器(limit register)?定義了該進程所占據的存儲器區域:基址寄存器中保存了該存儲器區域的開始地址,界限寄存器中保存了該區域的大小(以字節或字為單位)。程序計數器和所有的數據引用相對于基址寄存器被解釋,并且不能超過界限寄存器中的值,這就可以保護內部進程間不會相互干涉。(解決了互斥的問題)
圖中進程索引寄存器表明進程B正在執行。以前執行的進程被臨時中斷,在A中斷的同時,所有的寄存器的內容被記錄在它執行上下文環境中,以后操作系統就可以執行進程切換,恢復進程A的執行。進程切換過程包括保存B的上下文和恢復A的上下文。當在程序計數器中載入指向A的程序區域的值時,進程A自動恢復執行。
因此進程被當做數據結構來實現。一個進程可以是正在執行,也可是等待執行。任何時候整個進程狀態都包含在它的執行上下文環境中。這個結構使得可以開發功能強大的技術,以確保在進程中進行協調和合作。在操作系統中可能會設計和并入一些新的功能(優先級,linux中nice值。)這可以通過擴展上下文環境以包括支持這些特征的新信息。
?
兩狀態進程模型?
操作系統的基本職責是控制進程的執行。這包括確定交替執行的方式和給進程分配資源在設計控制進程的程序時,第一步就是描述進程所表現出的行為。
在任何時刻,一個進程要么正在執行,要么沒有執行,因而可以構造最簡單的模型。一個進程可以處于以下兩種狀態之一:運行態或未運行態。當操作系統創建一個新的進程時,它將該進程運行態加入到系統中,操作系統知道這個進程是存在的,并且正在等待執行機會。當前正在運行的進程時不時的被中斷,操作系統中的分派器部分將選擇一個新進程運行。前一個進程從運行態轉換到未運行狀態,另外一個集成轉換到運行態。
從這個簡單的模型可以意識到操作系統的一些設計元素。必須用某種方式來表示每個進程,使得操作系統能夠跟蹤它,也就是說,必須有一些與進程相關的信息,包括進程在內存中的當前狀態和位置,即進程控制塊。未運行的進程必須保持在某種類型的隊列中,并等待它們的執行時機。結構中有一個隊列,隊列中的每一項都指向某個特定進程的指針,或隊列可以由數據塊構成的鏈表組成,每個數據塊表示一個進程。
因此可以用該隊列圖描述分派器的行為。被中斷的進程轉移到等待進程隊列中,或者,如果進程已經結束或取消,則被銷毀(離開系統)。在任何一種情況下,分派器均從隊列中選擇一個進程來執行。
?五狀態模型
如果所有的進程都做好了執行準備。隊列是先進先出(first-in-first-out)的表,對于可運行的進程處理器以一種輪轉(round-robin)方式操作(依次給隊列中的每個進程一定的執行時間,然后進程返回隊列,阻塞情況除外)。但是存在著一些非運行狀態但已經就緒等待執行的進程,而同時存在另外的一些處于阻塞狀態等待I/O操作結束的進程。因此,如果使用單個隊列,分派器不能只考慮隊列中最老的進程,相反,他應該掃描這個列表,查找那些被阻塞且在隊列中時間最長的進程。解決這種情況的一種比較自然的方法是將非運行狀態分成兩個狀態:就緒(ready)和阻塞(blocked),此外應該增加兩個已經證明很有用的狀態。
五狀態模型中的五種狀態具體含義如下:
運行態:進程正在被執行(本章中,假設計算機只有一個處理器,因此一次最多只有一個進程處于這個狀態)。
就緒態:進程做好了準備,只要有機會就可運行。
阻塞態:進程在某些事件發生前不能執行,例如IO操作完成。
新建態:剛剛創建的進程,操作系統還沒有把它加入到可執行進程組中,通常是進程塊已經創建但還沒有加載到內存中的新進程。
退出態:操作系統從可執行進程組中釋放出的進程,或者因為它自身停止了,或者是因為某種原因被取消。
新建態和退出態對進程管理是非常有用的。新建態對于剛剛定義的進程,例如,如果一位新用戶試圖登錄到分時系統中,或者新的一批作業被提交執行,那么操作系統可以分兩步定義新進程:首先,操作系統執行一些必須的輔助工作,將標識符關聯到進程中,分配和創建管理進程所需要的所有表,此時,進程處于新建態,這意味著操作系統已經執行了創建進程的必要動作,但還沒有執行進程;其次,操作系統將所需要的關于該進程的信息保存到內存的進程表中,但進程自身還未進入內存,就是說即將執行的程序代碼不在內存中,也沒有為這個程序相關的數據分配空間。當進程處于新建態時,程序保留在外存中,通常是磁盤中。
進程退出也分為兩步:(1)當進程到達一個自然結束點,由于出現不可恢復的錯誤而被取消時,或當具有相應權限的另一個進程取消該進程時,進程被終止。終止的進程轉換到退出態,此時程序不再被執行,與程序相關的信息被操作系統臨時保留起來,這給輔助程序或支持程序提供了獲取所需信息的時間。(2)為了分析性能和利用率,一個實用程序可能需要提取進程的歷史信息,一旦這些程序提取了所需要的信息,操作系統就不再需要任何于該進程相關的數據,該進程將從系統中被刪掉。
進程狀態切換
導致進程狀態轉換的事件類型有很多,其中常見的有以下8種。
1、空——>新建
通常有四個事情會導致一個進程的創建:
- 批處理環境中,響應作業提交時會創建進程。
- 交互環境中,當一個新用戶試圖登錄時會創建進程。
- 現有進程派生新進程。基于模塊話的考慮,或者為了開發并行性,用戶程序可以指示創建多個進程。
- 操作系統代表應用程序創建進程,例如當用戶請求打印一個文件時,操作系統會創建一個管理打印的進程,進而請求進程可以繼續執行,與完成打印任務的時間無關。
2、新建——>就緒
操作系統尊卑在接納一個進程時,把一個進程從新建態轉換到就緒態。大多數系統會基于現有的進程數或分配給現有進程的虛擬數量設置一些限制,以確保不會因為活躍的進程過多而導致系統性能下降。
3、就緒——>運行
需要選擇一個新進程運行時,操作系統會選擇一個處于就緒態的進程,這是調度器或分派器的工作。
4、運行——>就緒
運行態轉換為就緒態最常見的原因是:正在運行的進程到達了“允許不中斷執行"的最大時間段,實際上所有多道程序操作系統都實行了這類時間限定。這類轉換還有很多其他原因,例如操作系統給不同進程分配不同的優先級,優先級高的進程搶占優先級低的進程,但這不試用于所有操作系統。還有一種情況是:進程自愿釋放對處理器的控制,例如一個周期行的進行記賬和維護的后臺進程。
5、運行——>阻塞
如果操作系統被進程請求必須等待某些事件,那么該進程則進入阻塞態。對操作系統的請求通常以系統調用的形式發出。例如:進程可能請求操作系統的一個服務,但操作系統無法立即予以服務,即請求一個無法立即得到的資源,如文件或虛擬內存中的共享區域;或者也可能需要進行某種初始化的工作,如IO操作所遇到的情況,并且只有在該初始化完成后才能繼續執行。當進程相互通信,一個進程等待另一個進程提供輸入時,或者等待來自另一個進程提供輸入時,或者等待來自另一個進程的信息時,進程都可能被阻塞。
6、阻塞——>就緒
當所等待的事情發生后,處于阻塞態的進程轉換到就緒態。
7、就緒——>退出
為了清楚,圖中并未表示這種轉換。在某些系統中,父進程可以在任何時刻終止一個子進程。如果一個父進程終止,那么與該父進程相關的所有子進程都將被終止。
8、阻塞——>退出
阻塞態轉換為退出態于就緒態轉換為退出態類似。
進程啟動
從磁盤加載到Linux系統內存中并被執行,一個程序大致經過7個階段,如圖所示:
C函數總是從函數main()開始執行,其函數原型如下:
int main(int argc,char *argv[]);其中,argc是命令行參數的數目;參數argv是指向命令行參數構成的數組。
當內核執行C程序是,在調用函數main()之前會先調用一個特殊的啟動例程。可執行程序文件將此啟動例程制定為程序的起始地址——這是由連接器設置的,而連接器則由C編譯器調用。啟動例程從內核取得命令行參數和環境變量值,然后為調用main()做好準備。
進程終止
終止進程的方式有8種,其中5中為正常終止:
(1)從函數main()主動返回;
(2)調用函數exit();
(3)調用函數_exit()或Exit();
(4)最后一個線程從啟動例程返回;
(5)最后一個線程調用函數pthread_exit()。
異常終止有3種:
(1)調用函數abort()。
(2)接到一個信號。
(3)最后一個線程對線程取消做出響應。
?
函數_exit()和_Exit()立即進入內核;函數exit()則先進行一些清理,然后返回內核。其函數原型如下:
?
#include<unistd.h>void _exit(int status);#inlcude <stdlib.h>void _Exit(int status); void exit(int status);由于歷史原因,函數exit()總是執行一個標準IO的清理關閉操作,對于所有打開流調用函數fclose(),這造成輸出緩沖區中的所有數據都被沖洗(寫入到文件中)。
?
這3個推出函數都帶一個整型參數,稱為終止狀態。Linux系統Shell可以檢查進程終止的狀態。如果調用函數時不帶終止狀態,或函數main()執行了一個無返回值的return語句,或函數main()沒有聲明返回類型為整型,并且函數main()執行到最后一條語句時返回(隱式返回),那么該進程的終止狀態為0。
?
進程的內核空間
程序在內存中大致分為5個部分,如下圖所示:
可以看到一個進程地址空間的主要成分為:
-
正文:這是整個用戶空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執行機器碼),通常,正文段是可共享的,所以即使是頻繁執行的程序在存儲器中也只需要一個副本。此外,程序端通常是只讀的,以防止程序意外修改其指令。
-
已初始化數據段:這里存放的是初始化過的全局變量
-
未初始化數據段:此段又稱為BSS段,這里存放的是未初始化的全局變量,在程序開始執行之前,內核將此段數據初始化為0或空指針。
-
堆(heap):堆是用于存放進程運行中被動態分配的內存段,它的大小并不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
-
棧:棧是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味著在數據段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,并且待到調用結束后,函數的返回值也會被存放回棧中。由于棧的先進先出特點,所以棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。
-
命令行參數和環境變量:用戶調用的最底層。
下面以一個實例完成對內存的探究:
?
#include<stdio.h>#include<malloc.h>#include<unistd.h>int bss_var;//未初始化變量int data_var0=1;//初始化變量int main(int argc,char **argv){printf("below are addresses of types of process's mem\n");printf("Text location:\n");printf("\tAddress of main(Code Segment):%p\n",main);//獲取main的地址 printf("____________________________\n");int stack_var0=2;//棧變量 printf("Stack Location:\n");printf("\tInitial end of stack:%p\n",&stack_var0);int stack_var1=3;printf("\tnew end of stack:%p\n",&stack_var1);printf("____________________________\n");printf("Data Location:\n");printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);//初始化數據段地址static int data_var1=4;printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);printf("____________________________\n");printf("BSS Location:\n");printf("\tAddress of bss_var:%p\n",&bss_var);//未初始化數據段地址 printf("____________________________\n");char *b = sbrk((ptrdiff_t)0);//保存在堆區的內存指針 printf("Heap Location:\n");printf("\tInitial end of heap:%p\n",b);brk(b+4);//brk通過傳遞的addr來重新設置program break,brk和sbrk主要的工作是實現虛擬內存到內存的映射. b=sbrk((ptrdiff_t)0);//而sbrk用來增加heap,增加的大小通過參數increment決定,返回增加大小前的heap的program break,如果increment為0則返回program break。 printf("\tNew end of heap:%p\n",b);return 0;}?
編譯運行結果如下:
?
參考資料
進程詳解
進程描述和控制
Linux內存管理
brk和sbrk及內存分配函數相關
理解brk和sbrk
操作系統之面試常考
malloc內存分配過程詳解
推薦書目
操作系統:精髓與設計原理(原書第6版)
轉載于:https://www.cnblogs.com/noticeable/p/8490657.html
總結
- 上一篇: C/C++语言中闭包的探究及比较
- 下一篇: 起底车载互联市场:产业市场庞大,但产品鱼