(整合)Linux下的多程式程式設計

YHQ-Fish發表於2020-10-23

一、程式

1、程式的定義

程式(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。在早期面向程式設計的計算機結構中,程式是程式的基本執行實體;在當代面向執行緒設計的計算機結構中,程式是執行緒的容器。程式是指令、資料及其組織形式的描述,程式是程式的實體。

2、程式的概念

程式是作業系統中資源分配的最小單位,而執行緒是排程的最小單位。
一個程式,主要包含三個元素:

1. 一個可以執行的程式   
2. 和該程式相關聯的全部資料(包括變數,記憶體空間,緩衝區等等)
3. 程式的執行上下文(execution context)

程式是程式的一個具體實現,每當我們執行一個程式時,對於作業系統來講就建立了一個程式,程式是執行程式的過程,同一個程式可以執行多次,每次都可以在記憶體中開闢獨立的空間來裝載,從而產生多個程式。不同的程式還可以擁有各自獨立的IO介面。作業系統的一個重要功能就是為程式提供方便,比如說為程式分配記憶體空間,管理程式的相關資訊等等。

程式和程式的區別: 程式是靜態的,它是一些儲存在磁碟上得指令的有序集合,沒有任何執行的概念。 程式是一個動態的概念,它是程式執行的過程,包括建立、排程和消亡。

3、程式的特徵

1.動態性:程式的實質是程式在多道程式系統中的一次執行過程,程式是動態產生,動態消亡的。

2.併發性:任何程式都可以同其他程式一起併發執行。

3.獨立性:程式是一個能獨立執行的基本單位,同時也是系統分配資源和排程的獨立單位;

4.非同步性:由於程式間的相互制約,使程式具有執行的間斷性,即程式按各自獨立的、不可預知的速度向前推進。

5.結構特徵:程式由程式、資料和程式控制塊三部分組成。

多個不同的程式可以包含相同的程式:一個程式在不同的資料集裡就構成不同的程式,能得到不同的結果;但是執行過程中,程式不能發生改變。

4、程式切換

進行程式切換就是從正在執行的程式中收回處理器,然後再使待執行程式來佔用處理器。

這裡所說的從某個程式收回處理器,實質上就是把程式存放在處理器的暫存器中的中間資料找個地方存起來,從而把處理器的暫存器騰出來讓其他程式使用。那麼被中止執行程式的中間資料存在何處好呢?當然這個地方應該是程式的私有堆疊。

讓程式來佔用處理器,實質上是把某個程式存放在私有堆疊中暫存器的資料(前一次本程式被中止時的中間資料)再恢復到處理器的暫存器中去,並把待執行程式的斷點送入處理器的程式指標PC,於是待執行程式就開始被處理器執行了,也就是這個程式已經佔有處理器的使用權了。

在切換時,一個程式儲存在處理器各暫存器中的中間資料叫做程式的上下文,所以程式的切換實質上就是被中止執行程式與待執行程式上下文的切換。在程式未佔用處理器時,程式的上下文是儲存在程式的私有堆疊中的。

5、程式控制塊

程式在作業系統中都有一個戶口,用於表示這個程式。這個戶口作業系統被稱為程式控制塊PCB(process control block),在Linux中具體實現是 task_struct資料結構,它記錄了一下幾個型別的資訊:

程式描述資訊:
程式識別符號用於唯一的標識一個程式(pid,ppid)
程式控制資訊:
程式當前狀態 //如這個程式處於可執行狀態,休眠,掛起等。
程式優先順序
程式開始地址
各種計時資訊
通訊資訊

資源資訊:
佔用記憶體大小及管理用資料結構指標
交換區相關資訊
I/O裝置號、緩衝、裝置相關的數結構
檔案系統相關指標
資源的限制和許可權

現場保護資訊(cpu進行程式切換時):
暫存器
PC
程式狀態字PSW
棧指標

對於作業系統來說PCB即找到整個過程。

在這裡插入圖片描述

6、程式識別符號

inux核心通過唯一的程式識別符號PID來標識每個程式。PID存放程式描述符的pid欄位中,新建立的PID通常是前一個程式的PID加1,不過PID的值有上限。當系統啟動後,核心通常作為一個程式的代表。一個指向task_struct的巨集current用來記錄正在執行的程式。current經常作為程式描述符結構指標的形式出現在核心程式碼中,例如,current->pid表示處理器正在執行程式的PID。

每一個程式都有一個非負整形表示的唯一程式ID。雖然程式ID總是唯一的,但是可以重用。
當一個程式終止,其之前被分配的程式ID就可以再次被使用。

手冊檔案 man getpid / man getuid / man getgid

7、Linux下程式的結構

在Linux作業系統中,程式在記憶體裡有三部分的資料,就是“資料段”、“堆疊段”和“程式碼段”。

簡單的說“程式碼段”,顧名思義,就是存放了程式程式碼的資料,假如機器中有數個程式執行相同的一個程式,那麼它們就可以使用同一個程式碼段。

堆疊段存放的就是子程式的返回地址、子程式的引數以及程式的區域性變數。而資料段則存放程式的全域性變數,常數以及動態資料分配的資料空間(比如用malloc之類的函式取得的空間)。

8、寫入時拷貝(Copy-on-write)機制

傳統fork:直接把當前執行緒資料直接全部複製給新建立的程式。這種實現過於簡單並且效率低下,因為它拷貝的資料或許可以共享。更糟糕的是,如果新程式打算立即執行一個新的映像(執行exec),那麼所有的拷貝都將前功盡棄。

如今fork的寫時拷貝技術:所以Linux的fork()使用寫時拷貝(copy-on-write COW)頁實現。寫時拷貝是一種可以推遲甚至避免拷貝資料的技術。核心此時並不複製整個程式的地址空間,而是讓父子程式共享同一個地址空間。只用在需要寫入的時候才會複製地址空間,從而使各個進行擁有各自的地址空間。也就是說,資源的複製是在需要寫入的時候才會進行,在此之前,只有以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下—例如,fork()後立即執行exec(),地址空間就無需被複制了fork()的實際開銷就是複製父程式的頁表(使子程式虛擬空間和父程式一樣,物理空間共用一個)以及給子程式建立一個程式描述符。在一般情況下,程式建立後都會馬上執行一個可執行的檔案,這種優化,可以避免拷貝大量根本就不會被使用的資料,導致寫時複製技術很牛逼,這就回答了程式是如何被建立的。

在這裡插入圖片描述
核心fork()時並不複製整個程式地址空間,而是讓父子程式共享一個地址空間,只有在需要寫入時,資料才會被複制,從而使各個程式擁有各自的拷貝資料。
也就是說,只有在需要寫入的時候才複製資源,在此之前,以只讀方式共享。

寫時複製技術:核心只為新生成的子程式建立虛擬空間結構,它們複製於父程式的虛擬空間結構,但是不為這些段分配實體記憶體,它們共享父程式的物理空間,當父子程式中有更改相應的段的行為發生時,再為子程式相應的段分配物理空間。在這裡插入圖片描述
vfork的做法更加簡單粗暴,核心連子程式的虛擬地址空間也不建立了,直接共享了父程式的虛擬空間,當然了,這種做法就順水推舟的共享了父程式的物理空間
在這裡插入圖片描述

二、函式

1、獲取PID函式

所需標頭檔案:

#include <sys/types.h>
#include <unistd.h>

函式原型:

pid_t getpid(void) ; //獲取目前呼叫程式的程式ID
pid_t getppid(void) ; //獲取目前呼叫程式的父程式ID
uid_t getuid(void) ; //獲取目前呼叫程式的實際使用者ID
gid_t getgid(void) ; //獲取目前呼叫程式的實際組ID
//函式引數: 無
//函式說明及返回值:許多程式利用取到的id來建立臨時檔案, 以避免臨時檔案相同帶來的問題。

2、建立程式 fork() 函式

所需標頭檔案:

#include<unistd.h>  
#include<sys/types.h>   

函式原型:

pid_t fork(void);
//若成功呼叫一次則返回兩個值,子程式返回0,父程式返回子程式ID;否則,出錯返回-1
//pid_t 是一個巨集定義,其實質是int 被定義在#include

fork函式被呼叫一次,但返回兩次。兩次返回的唯一區別是子程式的返回值是0,而父程式的返回值則是新子程式的程式ID。

將子程式ID返回給父程式的理由是:因為一個程式的子程式可以有多個,但是沒有一個函式能使一個程式可以獲得其所有子程式的程式ID。

fork使子程式獲得返回值為0的原因:一個程式只會有一個父程式,所以子程式總是可以呼叫getppid用來獲得其父程式的程式ID。

子程式和父程式繼續執行fork呼叫之後的指令(原因在文章的後面部分會解釋)。子程式是父程式的副本。子程式可以獲得父程式的資料空間、堆和棧的副本,但是父子程式並不共享這些儲存空間,父子程式共享這些正文段。

示例程式碼

暫無

在fork函式執行完畢後,如果建立新程式成功,則出現兩個程式,一個是子程式,一個是父程式。在子程式中,fork函式返回0,在父程式中,fork返回新建立子程式的程式ID。我們可以通過fork返回的值來判斷當前程式是子程式還是父程式。解釋一下pid的值為什麼在父子程式中不同。“其實就相當於連結串列,程式形成了連結串列,父程式的pid(p 意味point)指向子程式的程式id, 因為子程式沒有子程式,所以其pid為0.

fork出錯可能有兩種原因:
1.當前的程式數已經達到了系統規定的上限,這時errno的值被設定為EAGAIN。
2.系統記憶體不足,這時errno的值被設定為ENOMEM。

建立新程式成功後,系統中出現兩個基本完全相同的程式,子程式從父程式處繼承了整個程式的地址空間,包括程式上下文、程式碼段、程式堆疊、記憶體資訊、開啟的檔案描述符、訊號控制設定、程式優先順序、程式組號、當前工作目錄、根目錄、資源限制和控制終端, 父子程式不共享這些儲存空間部分,父子程式共享正文段。這兩個程式執行沒有固定的先後順序,哪個程式先執行要看系統的程式排程策略。

注意以下幾點

1.在Linux系統中建立程式有兩種方式:一是由作業系統建立,二是由父程式建立程式(通常為子程式)。系統呼叫函式fork()是建立一個新程式的唯一方式,當然vfork()也可以建立程式,但是實際上其還是呼叫了fork()函式。fork()函式是Linux系統中一個比較特殊的函式,其一次呼叫會有兩個返回值。

2.呼叫fork()之後,父程式與子程式的執行順序是我們無法確定的(即排程程式使用CPU),意識到這一點極為重要,因為在一些設計不好的程式中會導致資源競爭,從而出現不可預知的問題。

3.fork產生子程式的表現就是它會返回2次,一次返回0,順序執行下面的程式碼。這是子程式。一次返回子程式的pid,也順序執行下面的程式碼,這是父程式。

4.程式建立成功之後,父程式以及子程式都從fork() 之後開始執行,知識pid不同。fork語句可以看成將程式切為A、B兩個部分。(在fork()成功之後,子程式獲取到了父程式的所有變數、環境變數、程式計數器的當前空間和值)。

一般來說,fork()成功之後,父程式與子程式的執行順序是不確定的。這取決於核心所使用的排程演算法,如果要求父子程式相互同步,則要求某種形式的程式間通訊。

3、建立程式 vfork() 函式

所需標頭檔案:

#include<unistd.h>  
#include<sys/types.h>   

函式原型:

pid_t vfork(void);
//若成功呼叫一次則返回兩個值,子程式返回0,父程式返回子程式ID;否則,出錯返回-1
//pid_t 是一個巨集定義,其實質是int 被定義在#include

也用於建立一個程式,返回值與fork()相同。

fork()與vfork()的異同

執行次序:
fork():對父子程式的排程室由排程器決定的;
vfork():是先呼叫子程式,等子程式的exit(1)被呼叫後,再呼叫父程式;

對資料段的影響:
fork():父子程式不共享一段地址空間,修改子程式,父程式的內容並不會受 影響。

vfork():在子程式呼叫exit之前,它在父程式的空間中執行,也就是說會更改 父程式的資料段、棧和堆。。即共享程式碼區和資料區,且地址和內容都是一 樣的。

示例程式碼

暫無

版權宣告:本博所提供的內容均為網際網路整理而來,僅供學習參考,如有侵犯您的版權,請聯絡博主刪除。

參考資料來自(感謝):
https://www.jianshu.com/p/2007a20fdef8
https://www.cnblogs.com/CodingUniversal/p/7396671.html
https://www.cnblogs.com/tgycoder/p/5263644.html
https://www.cnblogs.com/lengender-12/p/7054896.html
https://blog.csdn.net/u010710458/article/details/79617395
https://blog.csdn.net/jobbofhe/article/details/82192092
https://www.jianshu.com/p/2007a20fdef8

相關文章