程序控制
- 1. 程序相關
-
- 1.1 CPU 與 MMU
-
- 1.1.1 CPU
- 1.1.2 MMU(記憶體管理單元)
- 1.3 程序控制塊PCB
- 2. 環境變量
-
- 2.1 常見環境變量
- 2.2 相關環境變量函數
-
- 2.2.1 getenv函數
- 2.2.2 setenv函數
- 2.2.1 unsetenv函數
- 3. 程序控制
-
- 3.1 程序ID相關函數
- 3.2 子程序建立
-
- 3.2.1 建立一個子程序
- 3.2.2 循環建立n各子程序
- 3.3 程序共享
- 3.4 gdb調試
- 4. exec函數族
-
- 4.1 execlp函數
- 4.2 execl函數
- 4.4 exec函數族一般規律
- 4.5 例子:将目前系統中的程序資訊列印到檔案中
- 5. 回收子程序
-
- 5.1 孤兒程序
- 5.2 僵屍程序
- 5.3 wait函數
- 5.4 waitpid函數
1. 程序相關
1.1 CPU 與 MMU
1.1.1 CPU
CPU 執行一條指令過程:預取器先從cache或者記憶體中取出一條指令,然後交給譯碼器分析,譯碼器分析該條指令需要用到哪個寄存器,并把相關資料存儲到對應的寄存器中,ALU對其進行運算,然後把資料回寫到寄存器中,最後再把資料放到緩沖區,然後由記憶體把資料傳輸到總線,再顯示到裝置上
1.1.2 MMU(記憶體管理單元)
MMU功能
- 虛拟記憶體與實體記憶體的映射
- 設定修改記憶體通路級别
虛拟位址:可用位址空間0-4G
假如虛拟位址使用了2KB,那麼映射到實體記憶體大小應該為4KB,因為一個page作為實體記憶體的最小機關,大小為4KB。
1.3 程序控制塊PCB
每個程序在核心中都有一個程序控制塊(PCB)來維護程序相關的資訊,Linux核心的程序控制塊是task_struct結構體。
/usr/src/linux-headers-3.16.0-30(可能不一樣)/include/linux/sched.h檔案中可以檢視struct task_struct 結構體定義。
其内部成員有很多,重點掌握以下部分即可:
- 程序id。系統中每個程序有唯一的id,在C語言中用pid_t類型表示,其實就是一個非負整數。
- 程序的狀态,有就緒、運作、挂起、停止等狀态。
- 程序切換時需要儲存和恢複的一些CPU寄存器。
- 描述虛拟位址空間的資訊。
- 描述控制終端的資訊。
- 目前工作目錄(Current Working Directory)。
- umask掩碼。
- 檔案描述符表,包含很多指向file結構體的指針。
- 和信号相關的資訊。
- 使用者id群組id。
- 會話(Session)和程序組。
- 程序可以使用的資源上限(Resource Limit)。
ulimit -a
2. 環境變量
環境變量,是指在作業系統中用來指定作業系統運作環境的一些參數。
通常具備以下特征:
- ① 字元串(本質)
- ② 有統一的格式:
名=值[:值]
- ③ 值用來描述程序環境資訊。
存儲形式:與指令行參數類似。char *[]數組,數組名environ,内部存儲字元串,NULL作為哨兵結尾。
使用形式:與指令行參數類似。
加載位置:與指令行參數類似。位于使用者區,高于stack的起始位置。
引入環境變量表:須聲明環境變量。
extern char ** environ;
2.1 常見環境變量
按照慣例,環境變量字元串都是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。
echo $SHELL
TERM
目前終端類型,在圖形界面終端下它的值通常是xterm,終端類型決定了一些程式的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字元終端一般不行。
LANG
語言和locale,決定了字元編碼以及時間、貨币等資訊的顯示格式。
HOME
目前使用者主目錄的路徑,很多程式需要在主目錄下儲存配置檔案,使得每個使用者在運作該程式時都有自己的一套配置。
練習:列印目前程序的所有環境變量。
#include <stdio.h>
#include <iostream>
using namespace std;
extern char **environ;//須聲明環境變量
int main(){
for(int i = 0;environ[i];i++){
cout << environ[i] << endl;
}
return 0;
}
2.2 相關環境變量函數
2.2.1 getenv函數
- 擷取環境變量值
- char *getenv(const char *name);
- 成功:傳回環境變量的值;
- 失敗:NULL (name不存在)
2.2.2 setenv函數
- 設定或添加環境變量的值
- int setenv(const char *name, const char *value, int overwrite);
- 成功:0;失敗:-1
- 參數overwrite取值:
- 1:覆寫原環境變量
- 0:不覆寫。(該參數常用于設定新環境變量,如:ABC = haha-day-night)
2.2.1 unsetenv函數
- 删除環境變量name的定義
- int unsetenv(const char *name);
- 成功:0;失敗:-1
- 注意事項:name不存在仍傳回0(成功),當name命名為"ABC="時則會出錯。
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;
int main(){
const char* name = "ABD";
char *val;
val = getenv(name);//擷取環境變量ABD的值,此時環境變量不存在,是以為空
if(val){
cout << name << ": " << val << endl;
}else{
cout << "環境變量不存在" << endl;
}
setenv(name,"day-day-up",1);//設定環境變量的值為day-day-up
val = getenv(name);
cout << name << ": " << val << endl;
int ret = unsetenv("ABCD");//删除環境變量的定義
cout << "環境變量ABCD 的值為:" << ret << endl;
ret = unsetenv("ABD");
cout << "環境變量ABD的值為:" << ret << endl;
cout << getenv(name) << endl;
return 0;
}
3. 程序控制
3.1 程序ID相關函數
函數 | 函數原型 | 說明 |
---|---|---|
getpid函數 | | 擷取目前程序ID |
getppid函數 | | 擷取目前程序的父程序ID |
getuid函數 | | 擷取目前程序實際使用者ID |
geteuid()函數 | | 擷取目前程序有效使用者ID |
getgid函數 | | 擷取目前程序使用使用者組ID |
getegid函數 | | 擷取目前程序有效使用者組ID |
區分一個函數是“系統函數”還是“庫函數”依據:
- 是否通路核心資料結構
- 是否通路外部硬體資源 二者有任一 → 系統函數;二者均無 → 庫函數
3.2 子程序建立
3.2.1 建立一個子程序
fork函數:建立一個子程序
pid_t fork(void);
- 失敗傳回-1;成功傳回兩個值:① 父程序傳回子程序的ID(非負) ②子程序傳回 0
- pid_t類型表示程序ID,但為了表示-1,它是有符号整型。(0不是有效程序ID,init最小,為1)
- 注意傳回值,不是fork函數能傳回兩個值,而是fork後,fork函數變為兩個,父子需各自傳回一個
#include <stdio.h>
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
cout << "testtesttesttest!!!" << endl;
pid_t pid = fork();
if(pid == -1){
perror("fork");
exit(1);
}else if(pid == 0){
cout << "此程序為子程序" << endl;
cout << "父程序pid為:" << getppid() << endl;
cout << "子程序pid為:" << getpid() << endl;
}else{
cout << "此程序為父程序" << endl;
cout << "父程序pid為:" << getppid() << endl;
cout << "子程序pid為:" << getpid() << endl;
sleep(1);
}
cout << "hellohellohello!!!" << endl;
return 0;
}
說明:
- testtesttest!!! 列印一次,而hellohellohello!!!列印了兩次,是因為fork()函數出現在testtesttest!!!之後,此句隻有父程序執行,而hellohellohello!!!在fork()函數之後,此時建立了一個子程序,父子程序各執行一次,是以出現兩次
- 目前程序的父程序為bash,如上所式
ps aux | grep 22865
3.2.2 循環建立n各子程序
使用
for(i = 0; i < n; i++) { fork(); }
建立的子程序:
- 當n為3時候,循環建立了(2^n)-1 個子程序,而不是N個子程序。
- 需要在循環的過程,保證子程序不再執行fork ,是以當(fork() == 0)時,子程序應該立即break;才正确。
//循環建立5個子程序
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main(){
cout << "建立5個子程序" << endl;
pid_t pid;
int i;
for(i = 0;i < 5;i++){
pid = fork();
if(pid == 0){//子程序就跳出此循環,防止為子程序建立程序
break;
}
}
if(i < 5){//子程序,break跳出後執行後續程式
sleep(i);//保證程序輸出的順序
cout << "I am " << i + 1 << " child fork!!!" << endl;
}else{ //父程序
sleep(i);
cout << "I am parent fork!!!" << endl;
}
return 0;
}
一次循環結束後,父程序會執行下一次循環,子程序會跳出循環執行循環後的内容,父子程序執行先後順序取決于誰先搶到CPU資源,并不能保證其執行順序(一般是父程序先執行,但是沒有理論依據),如下圖(注釋程式中sleep(i)後的運作結果,其中shell程序也參與cpu資源争奪)。
3.3 程序共享
父子程序遵循原則:讀時共享寫時複制
剛fork之後:
父子相同處: 全局變量(不能共享)、.data、.text、棧、堆、環境變量、使用者ID、宿主目錄、程序工作目錄、信号處理方式…
父子不同處:
- 1.程序ID
- 2.fork傳回值
- 3.父程序ID
- 4.程序運作時間
- 5.鬧鐘(定時器)
- 6.未決信号集
似乎,子程序複制了父程序0-3G使用者空間内容,以及父程序的PCB,但pid不同。真的每fork一個子程序都要将父程序的0-3G位址空間完全拷貝一份,然後在映射至實體記憶體嗎?
當然不是!父子程序間遵循讀時共享寫時複制的原則。這樣設計,無論子程序執行父程序的邏輯還是執行自己的邏輯都能節省記憶體開銷。
【重點】:父子程序共享:
- 檔案描述符(打開檔案的結構體)
- mmap建立的映射區 (程序間通信詳解)
3.4 gdb調試
使用gdb調試的時候,gdb隻能跟蹤一個程序。可以在fork函數調用之前,通過指令設定gdb調試工具跟蹤父程序或者是跟蹤子程序。預設跟蹤父程序。
-
指令設定gdb在fork之後跟蹤子程序。set follow-fork-mode child
-
設定跟蹤父程序。set follow-fork-mode parent
注意,一定要在fork函數調用之前設定才有效
4. exec函數族
fork建立子程序後執行的是和父程序相同的程式(但有可能執行不同的代碼分支),子程序往往要調用一種exec函數以執行另一個程式。當程序調用一種exec函數時,該程序的使用者空間代碼和資料完全被新程式替換,從新程式的啟動例程開始執行。調用exec并不建立新程序,是以調用exec前後該程序的id并未改變。
将目前程序的.text、.data替換為所要加載的程式的.text、.data,然後讓程序從新的.text第一條指令開始執行,但程序ID不變,換核不換殼。
其實有六種以exec開頭的函數,統稱exec函數:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
4.1 execlp函數
加載一個程序,借助PATH環境變量
int execlp(const char *file, const char *arg, ...);
- 成功:無傳回;失敗:-1
- 參數1:要加載的程式的名字。該函數需要配合PATH環境變量來使用,當PATH中所有目錄搜尋後沒有參數1則出錯傳回。
- 該函數通常用來調用系統程式。如:ls、date、cp、cat等指令。
//實作ls -la
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(2);
cout << "father fork" << endl;
}else{
//execlp函數
execlp("ls","ls","-l","-a",NULL);//實作ls -la
}
return 0;
}
4.2 execl函數
加載一個程序, 通過 路徑+程式名 來加載。可利用子程序執行自己寫的程式
int execl(const char *path, const char *arg, ...);
- 成功:無傳回;失敗:-1
- 對比execlp,如加載"ls"指令帶有-l,-a參數
- execlp(“ls”, “ls”, “-l”, “-a”, NULL); 使用程式名在PATH中搜尋。
- execl("/bin/ls", “ls”, “-l”, “-a”, NULL); 使用參數1給出的絕對路徑搜尋。
//實作ls -la
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(2);
cout << "father fork" << endl;
}else{
//execl函數
execl("/bin/ls","ls","-l","-a",NULL);//實作ls -la
//execl("./hello.cpp","hello",NULL);//運作hello.cpp檔案
}
return 0;
}
4.4 exec函數族一般規律
exec函數一旦調用成功即執行新的程式,不傳回。隻有失敗才傳回,錯誤值-1。是以通常我們直接在exec函數調用後直接調用perror()和exit(),無需if判斷。
- l (list) 指令行參數清單
- p (path) 搜素file時使用path變量
- v (vector) 使用指令行參數數組
-
e (environment) 使用環境變量數組,不使用程序原有的環境變量,設定新加載程式運作的環境變量
事實上,隻有execve是真正的系統調用,其它五個函數最終都調用execve,是以execve在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。
4.5 例子:将目前系統中的程序資訊列印到檔案中
指令行方式:
ps aux > 檔案
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main(){
int fd = open("out.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd < 0){
perror("open error");
exit(1);
}
dup2(fd,STDOUT_FILENO);//dup2(3,1),3相當于打開的檔案:out.txt,而1是标準輸出standout
execlp("ps","ps","aux",NULL);
return 0;
}
5. 回收子程序
5.1 孤兒程序
父程序先于子程序結束,則子程序成為孤兒程序,子程序的父程序成為init程序,稱為init程序領養孤兒程序。
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
cout << "I am parent,my pid is " << getpid() << endl;
sleep(8);
cout << "----------- parent going to die -------------"<< endl;
}else{
while(1){
cout << "I am child,my parent pid is " << getppid() << endl;
sleep(1);
}
}
return 0;
}
子程序每隔一秒列印一次,父程序在八秒後結束,此時子程序還沒結束,成為孤兒程序
5.2 僵屍程序
僵屍程序: 程序終止,父程序尚未回收,子程序殘留資源(PCB)存放于核心中,變成僵屍(Zombie)程序。
特别注意,僵屍程序是不能使用kill指令清除掉的。因為kill指令隻是用來終止程序的,而僵屍程序已經終止。
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
cout << "child ,my father pid is " << getppid() << endl;
sleep(8);
cout << "--------child die -------" << endl;
}else if(pid > 0){
while(1){
cout << "I am father,my pid is " << getpid() << " myson pid is " << pid << endl;
sleep(1);
}
}else{
perror("fork error");
exit(1);
}
return 0;
}
ps aux
5.3 wait函數
一個程序在終止時會關閉所有檔案描述符,釋放在使用者空間配置設定的記憶體,但它的PCB還保留着,核心在其中儲存了一些資訊:如果是正常終止則儲存着退出狀态,如果是異常終止則儲存着導緻該程序終止的信号是哪個。
這個程序的父程序可以調用wait或waitpid擷取這些資訊,然後徹底清除掉這個程序。我們知道一個程序的退出狀态可以在Shell中用特殊變量$?檢視,因為Shell是它的父程序,當它終止時Shell調用wait或waitpid得到它的退出狀态同時徹底清除掉這個程序。
父程序調用wait函數可以回收子程序終止資訊。該函數有三個功能:
- ① 阻塞等待子程序退出
- ② 回收子程序殘留資源
- ③ 擷取子程序結束狀态(退出原因)。
pid_t wait(int *status);
成功:清理掉的子程序ID;失敗:-1 (沒有子程序)
當程序終止時,作業系統的隐式回收機制會:1.關閉所有檔案描述符 2. 釋放使用者空間配置設定的記憶體。核心的PCB仍存在。其中儲存該程序的退出狀态。(正常終止→退出值;異常終止→終止信号)
可使用wait函數傳出參數status來儲存程序的退出狀态。借助宏函數來進一步判斷程序終止的具體原因。宏函數可分為如下三組:
1. WIFEXITED(status) 為非0 → 程序正常結束
WEXITSTATUS(status) 如上宏為真,使用此宏 → 擷取程序退出狀态 (exit的參數)
2. WIFSIGNALED(status) 為非0 → 程序異常終止
WTERMSIG(status) 如上宏為真,使用此宏 → 取得使程序終止的那個信号的編号。
3. WIFSTOPPED(status) 為非0 → 程序處于暫停狀态
WSTOPSIG(status) 如上宏為真,使用此宏 → 取得使程序暫停的那個信号的編号。
WIFCONTINUED(status) 為真 → 程序暫停後已經繼續運作
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;
int main(){
pid_t pid = fork();
int status;//wait(&status)
if(pid == 0){
cout << "child ,my father pid is " << getppid() << endl;
sleep(30);
cout << "--------child die -------" << endl;
exit(66);
}else if(pid > 0){
// pid_t wpid = wait(NULL); //一般回收
pid_t wpid = wait(&status);
if(wpid == -1){
perror("wait error");
exit(1);
}
//正常結束
if(WIFEXITED(status)){
cout << "child exit with " << WEXITSTATUS(status) << endl;
}
//異常終止
if(WIFSIGNALED(status)){
cout << "child killed by " << WTERMSIG(status) << endl;
}
while(1){
cout << "I am father,my pid is " << getpid() << " myson pid is " << pid << endl;
sleep(1);
}
}else{
perror("fork error");
exit(1);
}
return 0;
}
打開另一個終端視窗,手動發送信号 9 殺死子程序,此時子程序異常終止,傳回終止信号 9。
5.4 waitpid函數
作用同wait,但可指定pid程序清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options);
- 成功:傳回清理掉的子程序ID;
- 失敗:-1(無子程序)
特殊參數和傳回情況:
- 參數pid:
- 大于0: 回收指定ID的子程序
- -1 :回收任意子程序(相當于wait)
- 0 :回收和目前調用waitpid一個組的所有子程序
- 小于-1 :回收指定程序組内的任意子程序
- 參3
- WNOHANG,非阻塞回收(輪詢),且子程序正在運作。
- 0 : (wait)阻塞回收
- 傳回值
- 成功:pid
- 失敗:-1
-
傳回0 : 參數3為WNOHANG,且子程序正在運作
注意:一次wait或waitpid調用隻能清理一個子程序,清理多個子程序應使用循環。
waitpid(-1,NULL,0); 相當于 wait(NULL);