Linux C/C++編程之(十六)進程及進程控制

一、概述

在這裏插入圖片描述

二、基礎知識

1. 進程相關概念

1)程序和進程

程序,是指編譯好的二進制文件,在磁盤上,不佔用系統資源(cpu、內存、打開的文件、設備、鎖…)

進程,是一個抽象的概念,與操作系統原理聯繫緊密。進程是活躍(運行起來的)的程序,佔用系統資源,在內存中執行。(程序運行起來,產生一個進程)。

程序 → 劇本(紙) ,進程 → 戲(舞臺、演員、燈光、道具…),同一個劇本可以在多個舞臺同時上演。同樣,同一個程序也可以加載爲不同的進程(彼此之間互不影響)

如:同時開兩個終端。各自都有一個bash,但彼此ID不同。

進程和線程的區別:
在這裏插入圖片描述
阮一峯大佬的文章

  • CPU是工廠、
  • CPU時間片是電力資源、
  • 進程是車間、
  • 線程是車間工人~

2)併發

併發,在操作系統中,一個時間段中有多個進程都處於已啓動運行到運行完畢之間的狀態。但,任一個時刻點上仍只有一個進程在運行。理論依據:時鐘中斷

例如,當下,我們使用計算機時可以邊聽音樂邊聊天邊上網。 若籠統的將他們均看做一個進程的話,爲什麼可以同時運行呢,因爲併發。
在這裏插入圖片描述

併發和並行的區別:
在這裏插入圖片描述
Erlang 之父 Joe Armstrong 用一張5歲小孩都能看懂的圖解釋了併發與並行的區別

  • 併發是兩個隊列交替使用一臺咖啡機,
  • 並行是兩個隊列同時使用兩臺咖啡機,
  • 串行是一個隊列使用一臺咖啡機,

3)單道程序設計

所有進程一個一個排隊執行。若A阻塞,B只能等待,即使CPU處於空閒狀態。而在人機交互時阻塞的出現時必然的。所有這種模型在系統資源利用上及其不合理,在計算機發展歷史上存在不久,大部分便被淘汰了。

4)多道程序設計

在計算機內存中同時存放幾道相互獨立的程序,它們在管理程序控制之下,相互穿插的運行。多道程序設計必須有硬件基礎作爲保證。

時鐘中斷 即爲多道程序設計模型的理論基礎。 併發時,任意進程在執行期間都不希望放棄cpu。因此係統需要一種強制讓進程讓出cpu資源的手段。時鐘中斷有硬件基礎作爲保障,對進程而言不可抗拒。 操作系統中的中斷處理函數,來負責調度程序執行。

在多道程序設計模型中,多個進程輪流使用CPU (分時複用CPU資源)。而當下常見CPU爲納秒級,1秒可以執行大約10億條指令(1s = 1000ms, 1ms = 1000us, 1us = 1000ns)。由於人眼的反應速度是毫秒級,所以看似同時在運行。

實質上,併發是宏觀並行,微觀串行(僞並行)!推動了計算機蓬勃發展,將人類引入了多媒體時代。

5)CPU和MMU
在這裏插入圖片描述程序中用到的所有的內存都是虛擬內存,但是虛擬內存在計算機中是不實際存在的,存儲的數據都是存儲在物理內存中。
在這裏插入圖片描述6)進程控制塊PCB

每個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是task_struct結構體。

  • 進程id。系統中每個進程有唯一的id,在C語言中用pid_t類型表示,其實就是一個非負整數。
  • 進程的狀態,有初始化、就緒、運行、掛起、停止等狀態。
  • 進程切換時需要保存和恢復的一些CPU寄存器的值。
  • 描述虛擬地址空間的信息。
  • 描述控制終端的信息。
  • 當前工作目錄(Current Working Directory)。
  • umask掩碼。
  • 文件描述符表,包含很多指向已經打開的文件的file結構體的指針的一個數組。 (注:pcb中有一根指針,指針存儲的是文件描述符表的首地址)
  • 和信號相關的信息。
  • 用戶id和組id。
  • 會話(Session)和進程組。
  • 進程可以使用的資源上限(Resource Limit)。

在這裏插入圖片描述
ulimit -a 列出所有當前資源極限

7)進程狀態

進程基本的狀態有5種。分別爲初始態,就緒態,運行態,掛起態與終止態。細分可以分成七種狀態:
在這裏插入圖片描述

2. 環境變量

(1)定義:

環境變量,是指在操作系統中用來指定操作系統運行環境的一些參數。

  • 通常具備以下特徵:
    • ① 字符串(本質)
    • ② 有統一的格式:名=值[:值]
    • ③ 值用來描述進程環境信息。
  • 存儲形式:與命令行參數類似。char *[]數組,數組名environ,內部存儲字符串,NULL作爲哨兵結尾。
  • 使用形式:與命令行參數類似。
  • 加載位置:與命令行參數類似。位於用戶區,高於stack的起始位置。
  • 引入環境變量表:須聲明環境變量。extern char ** environ;

(2)常見環境變量:

按照慣例,環境變量字符串都是name=value這樣的形式,大多數name由大寫字母加下劃線組成,一般把name的部分叫做環境變量,value的部分則是環境變量的值。環境變量定義了進程的運行環境,一些比較重要的環境變量的含義如下:

  • PATH

可執行文件的搜索路徑。ls命令也是一個程序,執行它不需要提供完整的路徑名/bin/ls,然而通常我們執行當前目錄下的程序a.out卻需要提供完整的路徑名./a.out,這是因爲PATH環境變量的值裏面包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環境變量的值可以包含多個目錄,用:號隔開。在Shell中用echo命令可以查看這個環境變量的值:$ echo $PATH

  • SHELL

當前Shell,它的值通常是/bin/bash。

  • TERM

當前終端類型,在圖形界面終端下它的值通常是xterm,終端類型決定了一些程序的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字符終端一般不行。

  • LANG

語言和locale,決定了字符編碼以及時間、貨幣等信息的顯示格式。

  • HOME

當前用戶主目錄的路徑,很多程序需要在主目錄下保存配置文件,使得每個用戶在運行該程序時都有自己的一套配置。

3. 相關函數

1)getenv

  • 函數作用:獲取當前進程環境變量
  • 頭文件
    在這裏插入圖片描述
    參數說明:
  • name環境變量名

返回值

  • 成功:指向環境變值得指針
  • 失敗:返回NULL

在這裏插入圖片描述
在這裏插入圖片描述

2)setenv

  • 函數作用:設置環境變量。
  • 頭文件:
    在這裏插入圖片描述
    參數說明:
  • name 環境變量名
  • value 要設置的環境變量值
  • overwrite取值: 1:覆蓋原環境變量。0:不覆蓋

返回值:

  • 成功:0;
  • 失敗:-1

3)unsetenv

  • 函數作用:刪除環境變量name的定義
  • 頭文件:
    在這裏插入圖片描述
    參數說明:
  • name 環境變量名

返回值

  • 成功:0;
  • 失敗:-1

注意:name不存在仍返回0(成功),當name命名爲"ABC="時則會出錯。因爲“=”是構成環境變量中的一個組成部分。

1)fork

  • 函數作用:創建子進程
  • 頭文件
    在這裏插入圖片描述
    返回值
  • 成功:兩次返回,父進程返回子進程的id,子進程返回0
  • 失敗:返回-1給父進程,設置errno

在這裏插入圖片描述
在這裏插入圖片描述
結果分析:爲何會打印兩次begin?

這是由於 printf("Begin ..."); 執行之後並不會打印到屏幕,而是在緩衝區,因此fork之後子進程在執行 printf("End ...\n"); 遇到\n則全部打印出來。

如果修改爲 printf("Begin …\n");(在遇到\n時會將緩衝區內容打印到屏幕。)則子進程不會打印begin…
在這裏插入圖片描述
2)getpid與getppid

  • 函數作用:獲取進程id
  • 頭文件
    在這裏插入圖片描述

返回值:

  • getpid 獲得當前進程的pid,getppid獲取當前進程父進程的pid。
    在這裏插入圖片描述
    在這裏插入圖片描述在這裏插入圖片描述

(1)查看進程信息:

  • init進程是所有進程的祖先。

  • ps命令:

    • ps aux
    • ps ajx —可以追溯進程之間的血緣關係
  • kill命令:

    • SIGKILL/9 信號
    • kill -SIGKILL pid
    • kill -9 pid

(2)循環創建n個子進程:
一次fork函數調用可以創建一個子進程, 那麼創建N個子進程應該怎樣實現呢?
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述> 執行結果:

總共產生4個進程,但是本來想產生2個,因此將代碼中的break打開,在fork進程之後,將子進程退出。
在這裏插入圖片描述在這裏插入圖片描述 從上圖可以很清晰的看到,當n爲2時候,循環創建了(2^n)-1個子進程,而不是n的子進程。需要在循環的過程,保證子進程不再執行fork ,因此當 (fork() == 0)時,子進程應該立即 break; 才正確。

如何修改成預期創建兩個線程?
將代碼中的break解註釋,當爲子線程的時候直接退出。

重點:通過該練習掌握框架,循環創建n個子進程,使用循環因子i對創建的子進程加以區分。

(3)進程先創建先退出?
在這裏插入圖片描述
在這裏插入圖片描述

3)getuid

  • uid_t getuid(void); --> 獲取當前進程實際用戶ID
  • uid_t geteuid(void); --> 獲取當前進程有效用戶ID
    在這裏插入圖片描述

4)getgid

  • gid_t getgid(void); --> 獲取當前進程使用用戶組ID
  • gid_t getegid(void); --> 獲取當前進程有效用戶組ID
    在這裏插入圖片描述

父子進程之間在fork後,有哪些相同,那些相異之處呢?

剛fork之後:

  • 父子相同處(0-3G的用戶區及3-4G的內核區大部分): 全局變量、.data、.text、棧、堆、環境變量、用戶ID、宿主目錄、進程工作目錄、信號處理方式…
  • 父子不同處(3-4G中的內核區的PCB區): 1.進程ID 2.fork返回值 3.父進程ID 4.進程運行時間 5.鬧鐘(定時器)(定時器是以進程爲單位進行分配,每個進程有且僅有一個) 6.未決信號集。

似乎,子進程複製了父進程0-3G用戶空間內容,以及父進程的PCB,但pid不同真的每fork一個子進程都要將父進程的0-3G地址空間完全拷貝一份,然後在映射至物理內存嗎?

  • 當然不是!
  • 父子進程間遵循 讀時共享寫時複製 的原則(針對的是物理地址)。這樣設計,無論子進程執行父進程的邏輯還是執行自己的邏輯都能節省內存開銷。

練習:編寫程序測試,父子進程是否共享全局變?
在這裏插入圖片描述
在這裏插入圖片描述
結論:父子進程不共享全局變量。
父子進程共享:1. 文件描述符(打開文件的結構體) 2. mmap建立的映射區 (進程間通信詳解)
特別的,fork之後父進程先執行還是子進程先執行不確定,取決於內核所使用的調度算法,即隨機爭奪。

在這裏插入圖片描述如上圖:如果有一個全局變量 i = 5,當fork出子進程之後,此時父子進程指向同一片物理內存,父子進程讀到的 i = 5,但是當子進程或者父進程去修改全局變量(i = 10),則此時系統會開闢一片新內存,則父子進程的 i 就不是同一個值。

這樣做爲了減少系統開銷,也就是 讀時共享,寫時複製。

5)gdb調試

使用gdb調試的時候,gdb只能跟蹤一個進程。可以在fork函數調用之前,通過指令設置gdb調試工具跟蹤父進程或者是跟蹤子進程。默認跟蹤父進程。

set follow-fork-mode child 		命令設置gdb在fork之後跟蹤子進程
set follow-fork-mode parent 	設置跟蹤父進程

注意,一定要在fork函數調用之前設置纔有效。

6)exec函數族

fork創建子進程後執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啓動例程開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。

將當前進程的.text、.data替換爲所要加載的程序的.text、.data,然後讓進程從新的.text第一條指令開始執行,但進程ID不變,換核不換殼。也就是調用完exec函數中的命令之後,原來函數後面的代碼就不會執行。

其實有六種以exec開頭的函數,統稱exec函數:

在這裏插入圖片描述7)execlp

參數說明:

  • file 需要加載的程序的名字
  • arg 一般是程序名
  • … 參數名,可變參數

返回值:

  • 成功:無返回
  • 失敗:-1

注意:p是PATH的縮寫, execlp加載一個進程,藉助PATH環境變量 (不用寫該命令的絕對路徑,會到當前進程的環境變量中去找),當PATH中所有目錄搜索後沒有參數1則出錯返回。

該函數通常用來調用系統程序。如:ls、date、cp、cat等命令。比如:execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在PATH中搜索。

注意:int execlp(const char *file, const char *arg, ...); ---> int execlp(const char *file, const char *argv[]); 因此 arg 就相當於第一個參數(argv[0])。
在這裏插入圖片描述
在這裏插入圖片描述
7)execl函數

其中 l 是 list 的縮寫,基本同execlp函數,只是該函數在加載程序式,需要寫絕對路徑。

比如:execl("/bin/ls", “ls”, “-l”, “-F”, NULL); 使用參數1給出的絕對路徑搜索。
在這裏插入圖片描述
在這裏插入圖片描述
8)execvp函數

加載一個進程,使用自定義環境變量env

  • 變參形式: 1)… 2)argv[] (main函數也是變參函數,形式上等同於 int main(int argc, char *argv0, …))

  • 變參終止條件:1)NULL結尾 2)固參指定

execvp與execlp參數形式不同,原理一致。

9)exec函數族一般規律

exec函數一旦調用成功即執行新的程序,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函數調用後直接調用perror()和exit(),無需if判斷。

  • l (list) 命令行參數列表
  • p (path) 搜素file時使用path變量
  • v (vector) 使用命令行參數數組
  • e (environment) 使用環境變量數組,不使用進程原有的環境變量,設置新加載程序運行的環境變量

事實上,只有execve是真正的系統調用,其它五個函數最終都調用execve,所以execve在man手冊第2節,其它函數在man手冊第3節。

這些函數之間的關係如下圖所示。
在這裏插入圖片描述
10)回收子進程

  • 孤兒進程:父進程先於子進程結束,則子進程成爲孤兒進程,子進程的父進程成爲init進程,稱爲init進程領養孤兒進程。

  • 殭屍進程:子進程終止,父進程尚未回收,子進程殘留資源(PCB)存放於內核中,變成殭屍(Zombie)進程。

特別注意,殭屍進程是不能使用kill命令清除掉的。因爲kill命令只是用來終止進程的,而殭屍進程已經終止。

思考!用什麼辦法可清除掉殭屍進程呢?

  • 方一:wait函數。
  • 方二:殺死他的父進程使其變成孤兒進程,進而被系統處理。

孤兒進程:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
殭屍進程:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
11)wait

一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留着,內核在其中保存了一些信息:如果是正常終止則保存着退出狀態,如果是異常終止則保存着導致該進程終止的信號是哪個。這個進程的父進程可以調用wait或waitpid獲取這些信息,然後徹底清除掉這個進程。我們知道一個進程的退出狀態可以在Shell中用特殊變量$?查看,因爲Shell是它的父進程,當它終止時Shell調用wait或waitpid得到它的退出狀態同時徹底清除掉這個進程。

父進程調用wait函數可以回收子進程終止信息。該函數有三個功能:

  • 阻塞等待子進程退出
  • 回收子進程殘留資源
  • 獲取子進程結束狀態(退出原因)

當進程終止時,操作系統的隱式回收機制會:1.關閉所有文件描述符 2. 釋放用戶空間分配的內存。內核的PCB仍存在。其中保存該進程的退出狀態。(正常終止→退出值;異常終止→終止信號)

可使用wait函數傳出參數status來保存進程的退出狀態(status只是一個整型變量,不能很精確的描述出狀態),因此需要藉助宏函數來進一步判斷進程終止的具體原因。宏函數可分爲如下三組:

  • WIFEXITED(status) 爲非0 → 進程正常結束
    WEXITSTATUS(status) 如上宏爲真,使用此宏 → 獲取進程退出狀態 (exit的參數)
  • WIFSIGNALED(status) 爲非0 → 進程異常終止
    WTERMSIG(status) 如上宏爲真,使用此宏 → 取得使進程終止的那個信號的編號。
  • WIFSTOPPED(status) 爲非0 → 進程處於暫停狀態
    WSTOPSIG(status) 如上宏爲真,使用此宏 → 取得使進程暫停的那個信號的編號。
    WIFCONTINUED(status) 爲真 → 進程暫停後已經繼續運行

  • wait 函數作用:1)阻塞等待 2)回收子進程資源 3)查看死亡原因
  • 頭文件
    在這裏插入圖片描述
    參數說明:
  • status傳出參數,用來獲取子進程退出的狀態。

返回值:

  • 成功:返回終止的子進程pid
  • 失敗:返回-1,設置errno

子進程的死亡原因:

  • 正常死亡 WIFEXITED,如果WIFEXITED爲真,使用WEXITSTATUS得到退出狀態。
  • 非正常死亡WIFSIGNALED,如果WIFSIGNALED爲真,使用WTERMSIG得到信號。

wait回收子進程:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述wait查看子進程死亡原因:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
wait回收多個子進程:
在這裏插入圖片描述
在這裏插入圖片描述

12)waitpid

  • 作用同wait,但可指定pid進程清理,可以不阻塞。

參數說明:

  • pid:
    • < -1 組id
    • -1 回收任意
    • 0 回收和調用進程組id相同組內的子進程
    • >0 回收指定的pid
  • options
    • 0與wait形同,也會阻塞
    • WNOHANG 如果當前沒有子進程退出的,會立即返回。

返回值:

  • 如果設置了WNOHANG,那麼如果沒有子進程退出,返回0。
  • 如果有子進程退出,返回退出子進程的pid
  • 失敗: 返回-1(沒有子進程),設置errno

注意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用循環。

waitpid回收子進程:
在這裏插入圖片描述
在這裏插入圖片描述
waitpid回收多個子進程:
在這裏插入圖片描述
在這裏插入圖片描述

三、練習

  1. 父進程fork 3 個子進程,三個子進程一個調用ps命令, 一個調用自定義程序1(正常),一個調用自定義程序2(會出段錯誤)。父進程使用waitpid對其子進程進行回收?

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述

  1. 驗證子進程是否共享文件描述符,子進程負責寫入數據,父進程負責讀數據?
    在這裏插入圖片描述
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章