Linux程序管理
程序的定義與特徵
程序的定義
程式:是靜態的,就是個存放在磁碟裡的可執行檔案,就是一系列的指令集合。
程序:是動態的,是程式的一次執行過程。
程序的組成--PCB
- 一個程式檔案
program
,只是一堆待執行的程式碼和部分待處理的資料,他們只有被載入到記憶體中,然後讓CPU逐條執行其程式碼,根據程式碼做出相應的動作,才形成一個真正“活的”、動態的程序process
,因此程序是一個動態變化的過程,而程式檔案只是這一系列動作的原始藍本,是一個靜態的劇本。
當程序被建立時,作業系統會為該程序分配一個** 唯一的 ** , ** 不重複 ** 的“身份證號”--PID。
- 程序識別符號(process identifier)
用於區分系統中的其他程序,這個PID也是Linux核心提供給使用者訪問程序的一個介面,使用者可以透過PID控制程序。
- PCB --
ProcessControl Block
ELF格式的程式被執行時,核心中實際上產生了一個叫
task_struct{}
的結構 體來表示這個程序。程序是一個“活動的實體”,這個活動的實體從一開始誕生就需要各種 各樣的資源以便於生存下去,比如記憶體資源、CPU資源、檔案、訊號、各種鎖資源等,所 有這些東西都是動態變化的,這些資訊都被事無鉅細地一一記錄在結構體task_struct
之 中,所以這個結構體也常常被成為程序控制塊(PCB,即Process Control Block
)。該結構體被定義在一個名稱叫做sched.h
的標頭檔案中。** PCB是程序存在的唯一標誌 **
-
程序是作業系統分配資源的基本單位!
-
**作業系統是以程序為單位來分配系統資源的,比如記憶體空間、CPU使用權等。 **
-
**執行緒是作業系統排程資源的最小單位! **
-
程序包含執行緒!
- PCB是給作業系統用的。
- 程式段和資料段是給程序自己用的。
程序的特徵
- 併發性:併發性指的是多個程序實體可以同時存在於記憶體中,並在一段時間內同時執行。這是程序的重要特徵,也是作業系統的關鍵特性之一。例如,多個應用程式可以同時執行,共享計算機資源。
- 動態性:程序的實質是程式的一次執行過程。因此,程序是動態產生和動態消亡的。每個程序都有自己的生命週期,從建立到終止。
- 獨立性:程序實體是一個獨立執行、獨立分配資源和獨立接受排程的基本單位。每個程序都有自己的記憶體空間、暫存器和其他資源。這使得程序之間相互隔離,不會相互干擾。
- 非同步性:程序按照各自獨立的、不可預知的速度向前推進。這意味著不同程序之間的執行是非同步的,它們不會嚴格按照某個固定的順序執行。例如,一個程序可能在等待使用者輸入時暫停,而另一個程序繼續執行
Linux 系統檢視程序pid
的shell命令
ps -ef
ps -aux
檢視所有使用者的相關程序的所有訊息
程序的狀態與轉換
- 新建態(New):程序剛剛被建立,但還沒有被作業系統排程執行。在這個狀態下,作業系統會為程序分配必要的資源,例如記憶體空間和識別符號。
- 就緒態(Ready):程序已經準備好執行,等待被作業系統排程到CPU上執行。在這個狀態下,程序已經被新增到可執行程序佇列中,但還沒有獲得CPU的使用權。
- 執行態(Running):程序正在被CPU執行。它佔有處理器,其程式正在執行。在單處理機系統中,只有一個程序處於執行狀態;在多處理機系統中,可能有多個程序處於執行狀態。
- 阻塞態(Blocked):程序因為某些原因(例如等待I/O操作完成、等待資源等)而暫時無法繼續執行,被阻塞。在這個狀態下,程序不會佔用CPU資源,但會等待外部事件的發生。
- 可中斷等待狀態:程序可以被訊號量或中斷喚醒,一旦資源有效,程序會立即進入就緒狀態。
- 不可中斷等待狀態:程序不能被訊號量或中斷喚醒,只有當它申請的資源有效時才能被喚醒。這種狀態通常用於核心中某些場景,例如磁碟讀寫時的DMA操作。
- 終止態(Terminated):程序因某種原因而中止執行,佔有的所有資源將被回收。
- 系統對其不再予以理睬,也稱為“僵死狀態”。程序則成為殭屍程序。
程序控制
程序控制是對系統中所有程序從建立、執行到撤銷的全過程實行有效的管理和控制。
程序控制一般是由作業系統核心的相應程式(原語)來實現。通常,作業系統核心執行在系統態。
- 原語
原語是由若干條指令組成的,用於完成特定功能的,具有原子性(不可分割)的子程式。它與一般過程的區別:它們是原子操作(Action Operation)為保證操作的正確性,原語在執行期間是不可被中斷的。因此,規定在執行原語操作時要遮蔽中斷,以保證原語操作的不可分割性。
用於程序控制過程中的原語有:
- 建立原語(Create)、撤銷原語(Termination)
- 阻塞原語(Block)、 喚醒原語(Wakeup)
- 掛起原語(Suspend)、 啟用原語(Active)
程序的建立
函式名稱 | fork() |
---|---|
標頭檔案 | #include <unistd.h> |
原型 | pid_t fork(void); |
返回值 | - 成功:在父程序中返回大於0的正整數,表示子程序的PID。 - 成功:在子程序中返回0。 - 失敗:返回-1,表示建立子程序失敗。 |
備註 | - fork() 執行成功後,會生成一個新的子程序。- 在新的子程序中, fork() 返回值為0。- 在原來的父程序中, fork() 返回值為大於0的正整數,即子程序的PID。 |
-
fork()
函式的作用:fork()
會使得程序本身被複制,類似細胞分裂。因此,被建立出來的子程序和父程序幾乎是一模一樣的。但需要注意的是,子程序並不是100%父程序的影印件,具體關係如下:- 父子程序的以下屬性在建立之初完全一樣,子程序相當於搞了一份複製品:
- 實際 UID 和 GID,以及有效 UID 和 GID。
- 所有環境變數。
- 程序組 ID 和會話 ID。
- 當前工作路徑(除非使用
chdir()
進行修改)。 - 開啟的檔案。
- 訊號響應函式。
- 整個記憶體空間,包括棧、堆、資料段、程式碼段、標準 I/O 的緩衝區等等。
- 而以下屬性,父子程序是不一樣的:
- 程序號 PID。PID 是身份證號碼,哪怕親如父子,也要區分開。
- 記錄鎖。如果父程序對某檔案加了鎖,子程序不會繼承這把鎖。
- 掛起的訊號。這些訊號是所謂的“懸而未決”的訊號,等待著程序的響應,子程序也不會繼承這些訊號。
- 父子程序的以下屬性在建立之初完全一樣,子程序相當於搞了一份複製品:
-
子程序的執行:
- 子程序會從
fork()
返回值後的下一條邏輯語句開始執行。這樣就避免了不斷呼叫fork()
而產生無限子孫的悖論。
- 子程序會從
-
父子程序的關係:
- 父子程序是相互平等的:他們的執行次序是隨機的,或者說他們是併發執行的,除非使用特殊機制來同步他們,否則你不能判斷他們的執行究竟誰先誰後。
- 父子程序是相互獨立的:由於子程序完整地複製了父程序的記憶體空間,因此從記憶體空間的角度看,他們是相互獨立、互不影響的。
- 獲取當前程序PID的函式介面
getpid()
,獲取當前程序的父程序PID的函式介面是getppid()
示例:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(int argc, char const *argv[]) { int fd = 3; // 父程序的資料段 //建立子程序,呼叫fork函式的時候就已經建立子程序 pid_t child_pid = fork(); //透過fork函式的返回值分析父程序 or 子程序 if (child_pid > 0) { //說明是父程序的程序空間 printf("my is parent,my pid = %d,my child pid = %d\n",getpid(),child_pid); } else if( child_pid == 0) { //說明是子程序的程序空間 printf("my is child,my pid = %d,my parent pid = %d\n",getpid(),getppid()); } else { printf("child process fork error\n"); return -1; } return 0; }
由於父子程序的併發性,以上程式的執行效果是不一定的。
程序的撤銷
功能 | 等待子程序結束 |
---|---|
標頭檔案 | #include <sys/wait.h> |
原型 | pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc, int options); |
引數 | - pid :- 小於-1:等待組ID的絕對值為pid的程序組中的任一子程序。 - -1:等待任一子程序。 - 0:等待呼叫者所在程序組中的任一子程序。 - 大於0:等待程序組ID為pid的子程序。 - stat_loc :子程序退出狀態。- options :- WCONTINUED :報告任一從暫停態出來且從未報告過的子程序的狀態。- WNOHANG :非阻塞等待。- WUNTRACED :報告任一當前處於暫停態且從未報告過的子程序的狀態。 |
返回值 | - wait() :- 成功:退出的子程序PID。 - 失敗:-1。 - waitpid() :- 成功:狀態發生改變的子程序PID(如果 WNOHANG 被設定,且由pid指定的程序存在但狀態尚未發生改變,則返回0)。- 失敗:-1。 |
備註 | 如果不需要獲取子程序的退出狀態,stat_loc 可以設定為NULL 。 |
所謂的退出狀態不是退出值,退出狀態包括了退出值。如果使用以上兩個函式成功獲取了子程序的退出狀態,則可以使用以下宏來進一步解析
宏 | 含義 |
---|---|
WIFEXITED(status) |
如果子程序正常退出,則該宏為真。 |
WEXITSTATUS(status) |
如果子程序正常退出,則該宏將獲取子程序的退出值。 |
WIFSIGNALED(status) |
如果子程序被訊號殺死,則該宏為真。 |
WTERMSIG(status) |
如果子程序被訊號殺死,則該宏將獲取導致他死亡的訊號值。 |
WCOREDUMP(status) |
如果子程序被訊號殺死且生成核心轉儲檔案(coredump ),則該宏為真。 |
WIFSTOPPED(status) |
如果子程序的被訊號暫停,且option中WUNTRACED 已經被設定時,則該宏為真。 |
WSTOPSIG(status) |
如果WIFSTOPPED(status) 為真,則該宏將獲取導致子程序暫停的訊號值。 |
WIFCONTINUED(status) |
如果子程序被訊號SIGCONT 重新置為就緒態,該宏為真。 |
status是一個出參,由作業系統為其賦值,使用者可以傳遞NULL值表示不關心,而如果傳入引數,作業系統就會根據該引數,將子程序的退出資訊反饋給父程序,由status最終被賦予的值來體現。 |
可以看出,不論是正常退出還是異常退出,status的高8個位元位(只討論低16個位元位)都表示子程序的退出碼,而這個退出碼一般是return的返回值或者exit的引數;正常退出時,status的低8個位元位為全0;而異常退出時,其第8個位元位則為core dump標誌位,用來標誌是否會有core dump檔案產生,而低7個位元位則是退出訊號。
可以透過位運算判斷是否正常退出,是否產生core dump檔案。
示例:
child_elf.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
printf("[%d]:yep,I am the child\n", (int)getpid());
exit(0);
}
wait.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/wait.h> // 包含 wait 函式的宣告
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main(int argc, char** argv) {
pid_t x = fork();
if (x == 0) { // 子程序,執行指定程式 child_elf
execl("./child_elf", "child_elf", NULL);
}
if (x > 0) { // 父程序,使用 wait() 阻塞等待子程序的退出
int status;
wait(&status);
if (WIFEXITED(status)) { // 判斷子程序是否正常退出
printf("child exit normally, exit value: %hhu\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { // 判斷子程序是否被訊號殺死
printf("child killed by signal: %u\n", WTERMSIG(status));
}
}
return 0;
}
父程序使用 wait()
阻塞等待子程序的退出狀態。如果子程序正常退出,我們獲取其退出值;如果子程序被訊號殺死,我們獲取導致其死亡的訊號值。
程序的執行
也可以叫程式的替換。
程序程式替換與fork不同,它並不會建立新的程序,而是該程序的使用者空間程式碼和資料完全被新程式替換,從新程式的啟動例程開始執行。替換前後的程序號並未改變。
功能 | 在程序中載入新的程式檔案或者指令碼,覆蓋原有程式碼,重新執行 |
---|---|
標頭檔案 | <unistd.h> |
原型 | int execl(const char *path, const char *arg, ...); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, ..., char *const envp[]); int execlp(const char *file, const char *arg, ...); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); |
引數 | - path :即將被載入執行的ELF檔案或指令碼的路徑- file :即將被載入執行的ELF檔案或指令碼的名字- arg :以列表方式羅列的ELF檔案或指令碼的引數- argv :以陣列方式組織的ELF檔案或指令碼的引數- envp :使用者自定義的環境變數陣列 |
返回值 | 成功不返回 失敗返回 -1 |
備註 | 1. 函式名帶字母 l 意味著其引數以列表(list)的方式提供。2. 函式名帶字母 v 意味著其引數以向量(vector)陣列的方式提供。3. 函式名帶字母 p 意味著會利用環境變數 PATH 來找尋指定的執行檔案。4. 函式名帶字母 e 意味著使用者提供自定義的環境變數。 |
功能 | 在子程序中執行一個 shell 命令,然後返回 |
---|---|
標頭檔案 | <stdlib.h> |
原型 | int system(const char *command); |
引數 | - command :要執行的 shell 命令字串。如果 command 為 NULL ,則返回 shell 是否存在的狀態 |
返回值 | - 如果 command 為 NULL :- 返回 0:沒有可用的 shell - 返回 非 0 值:有可用的 shell - 如果 command 不為 NULL :- 成功:返回 shell 的退出狀態碼 - 失敗:返回 -1,並設定 errno |
備註 | 1. system 函式使用 /bin/sh 來執行命令。2. 在呼叫 system 之前,會忽略 SIGINT 和 SIGQUIT 訊號,並且阻塞 SIGCHLD 訊號。3. 如果在 system 呼叫期間捕獲到訊號,shell 的退出狀態可能會受到影響。 |
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
// 建立子程序,呼叫fork函式的時候就已經建立子程序
pid_t child_pid = fork();
// 透過fork函式的返回值分析父程序或子程序
if (child_pid > 0) {
// 父程序的程序空間
printf("my is parent, my pid = %d, my child pid = %d\n", getpid(), child_pid);
wait(NULL); // 會阻塞,當第一個子程序狀態改變時,該函式可以解除阻塞
} else if (child_pid == 0) {
// 子程序的程序空間
printf("my is child, my pid = %d, my parent pid = %d\n", getpid(), getppid());
// 打算讓子程序載入新的程式碼段和資料段,也就是讓子程序執行新的可執行檔案
//execl("./demo","demo",NULL);
system("./demo"); // 使用system呼叫執行新的可執行檔案 demo
} else {
// fork失敗
printf("child process fork error\n");
return -1;
}
return 0;
}
程序的終止
功能 | 退出本程序 |
---|---|
標頭檔案 | <unistd.h> <stdlib.h> |
原型 | void _exit(int status); void exit(int status); |
引數 | status :子程序的退出值- 如果子程序正常退出,則 status 一般為0。- 如果子程序異常退出,則 status 一般為非0。 |
返回值 | 不返回 |
備註 | - exit() 函式退出時會自動沖洗(flush)標準IO緩衝區的殘留資料到核心,如果程序註冊了退出處理函式,還會自動執行這些函式。- _exit() 函式直接退出,不執行任何清理操作。- 除了\n(換行)以及exit()函式會重新整理緩衝區之外,也可以呼叫 fflush() 來強制重新整理緩衝區 |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int status;
printf("父程序開始執行。\n");
// 建立子程序
pid_t child_pid = fork();
if (child_pid == -1) {
// 建立子程序失敗
perror("fork");
return 1;
} else if (child_pid == 0) {
// 子程序
printf("子程序正在執行,PID:%d\n", getpid());
// 模擬一個錯誤條件
int result = 5 / 0; // 這會導致除以零異常
// 檢查除法是否成功
if (result != 0) {
// 如果發生錯誤,以狀態碼 1 退出
printf("子程序遇到錯誤。\n");
exit(1);
} else {
// 如果成功,以狀態碼 0 退出
printf("子程序成功執行。\n");
exit(0);
}
} else {
// 父程序
printf("父程序等待子程序結束,子程序PID:%d\n", child_pid);
wait(&status); // 等待子程序結束
if (WIFEXITED(status)) {
printf("子程序以狀態碼 %d 正常退出。\n", WEXITSTATUS(status));
} else {
printf("子程序未正常退出。\n");
}
printf("父程序結束。\n");
}
return 0;
}
錯誤處理 (補充)
fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
fprintf
: 這個函式用於向流中寫入格式化的輸出。在這個例子中,它將格式化的訊息寫入標準錯誤流(stderr
)。"open user.txt fail, error = %d, %s\n"
: 這是格式字串。它包含了將要插入字串的佔位符。具體來說:%d
是一個整數值的佔位符(在這個例子中,是errno
的值)。%s
是一個字串值的佔位符(在這個例子中,是strerror(errno)
的結果)。
errno
: 這個全域性變數儲存了最後一個失敗的系統呼叫的錯誤程式碼。當發生錯誤時,系統呼叫和庫函式會設定這個變數。strerror(errno)
: 這個函式接受一個錯誤號(如errno
),並返回一個人類可讀的描述錯誤的字串。它將錯誤程式碼轉換為有意義的錯誤訊息。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[]) {
// 1. 開啟檔案
int file_fd = open("./user.txt", O_RDWR | O_CREAT);
if (file_fd == -1) {
fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
return -1;
}
// 2. 對檔案進行寫入
write(file_fd, "hello world", 11);
lseek(file_fd,0,SEEK_SET); //將游標偏移到開頭
// 3. 從文字中讀取資料
char recv_buf[11] = {0};
read(file_fd, recv_buf, 11);
printf("read from user.txt data is [%s]\n", recv_buf);
// 4. 關閉檔案
close(file_fd);
return 0;
}