Linux C/C++编程之(十六)进程及进程控制
文章目錄
- 一、概述
- 二、基礎知識
- 1. 進程相關概念
- 2. 環境變量
- 3. 相關函數
- 三、練習
一、概述
二、基礎知識
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回收多個子進程:
三、練習
總結
以上是生活随笔為你收集整理的Linux C/C++编程之(十六)进程及进程控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows下安装HDFView
- 下一篇: 2022迅雷ios版下载beta