linux程序管理

小小的番茄發表於2024-12-01

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是程序存在的唯一標誌 **

image-20240525091741801

  • 程序是作業系統分配資源的基本單位!

  • **作業系統是以程序為單位來分配系統資源的,比如記憶體空間、CPU使用權等。 **

  • **執行緒是作業系統排程資源的最小單位! **

  • 程序包含執行緒!

image-20240526135129498

  • PCB是給作業系統用的。
  • 程式段和資料段是給程序自己用的。

程序的特徵

  1. 併發性:併發性指的是多個程序實體可以同時存在於記憶體中,並在一段時間內同時執行。這是程序的重要特徵,也是作業系統的關鍵特性之一。例如,多個應用程式可以同時執行,共享計算機資源。
  2. 動態性:程序的實質是程式的一次執行過程。因此,程序是動態產生和動態消亡的。每個程序都有自己的生命週期,從建立到終止。
  3. 獨立性:程序實體是一個獨立執行、獨立分配資源和獨立接受排程的基本單位。每個程序都有自己的記憶體空間、暫存器和其他資源。這使得程序之間相互隔離,不會相互干擾。
  4. 非同步性:程序按照各自獨立的、不可預知的速度向前推進。這意味著不同程序之間的執行是非同步的,它們不會嚴格按照某個固定的順序執行。例如,一個程序可能在等待使用者輸入時暫停,而另一個程序繼續執行

image-20240526140000676

Linux 系統檢視程序pid的shell命令

ps -ef ps -aux 檢視所有使用者的相關程序的所有訊息

程序的狀態與轉換

image-20240526114252952

  1. 新建態(New):程序剛剛被建立,但還沒有被作業系統排程執行。在這個狀態下,作業系統會為程序分配必要的資源,例如記憶體空間和識別符號。
  2. 就緒態(Ready):程序已經準備好執行,等待被作業系統排程到CPU上執行。在這個狀態下,程序已經被新增到可執行程序佇列中,但還沒有獲得CPU的使用權。
  3. 執行態(Running):程序正在被CPU執行。它佔有處理器,其程式正在執行。在單處理機系統中,只有一個程序處於執行狀態;在多處理機系統中,可能有多個程序處於執行狀態。
  4. 阻塞態(Blocked):程序因為某些原因(例如等待I/O操作完成、等待資源等)而暫時無法繼續執行,被阻塞。在這個狀態下,程序不會佔用CPU資源,但會等待外部事件的發生。
    • 可中斷等待狀態:程序可以被訊號量或中斷喚醒,一旦資源有效,程序會立即進入就緒狀態。
    • 不可中斷等待狀態:程序不能被訊號量或中斷喚醒,只有當它申請的資源有效時才能被喚醒。這種狀態通常用於核心中某些場景,例如磁碟讀寫時的DMA操作。
  5. 終止態(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。
  1. fork() 函式的作用

    fork()會使得程序本身被複制,類似細胞分裂。因此,被建立出來的子程序和父程序幾乎是一模一樣的。但需要注意的是,子程序並不是100%父程序的影印件,具體關係如下:

    • 父子程序的以下屬性在建立之初完全一樣,子程序相當於搞了一份複製品:
      • 實際 UID 和 GID,以及有效 UID 和 GID。
      • 所有環境變數。
      • 程序組 ID 和會話 ID。
      • 當前工作路徑(除非使用 chdir() 進行修改)。
      • 開啟的檔案。
      • 訊號響應函式。
      • 整個記憶體空間,包括棧、堆、資料段、程式碼段、標準 I/O 的緩衝區等等。
    • 而以下屬性,父子程序是不一樣的:
      • 程序號 PID。PID 是身份證號碼,哪怕親如父子,也要區分開。
      • 記錄鎖。如果父程序對某檔案加了鎖,子程序不會繼承這把鎖。
      • 掛起的訊號。這些訊號是所謂的“懸而未決”的訊號,等待著程序的響應,子程序也不會繼承這些訊號。
  2. 子程序的執行

    • 子程序會從 fork() 返回值後的下一條邏輯語句開始執行。這樣就避免了不斷呼叫 fork() 而產生無限子孫的悖論。
  3. 父子程序的關係

    • 父子程序是相互平等的:他們的執行次序是隨機的,或者說他們是併發執行的,除非使用特殊機制來同步他們,否則你不能判斷他們的執行究竟誰先誰後。
    • 父子程序是相互獨立的:由於子程序完整地複製了父程序的記憶體空間,因此從記憶體空間的角度看,他們是相互獨立、互不影響的。
    • 獲取當前程序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;
    
    }
    

    image-20240526205547592

    由於父子程序的併發性,以上程式的執行效果是不一定的。

程序的撤銷

功能 等待子程序結束
標頭檔案 #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最終被賦予的值來體現。

image-20240526223251646

image-20240526223312076

可以看出,不論是正常退出還是異常退出,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() 阻塞等待子程序的退出狀態。如果子程序正常退出,我們獲取其退出值;如果子程序被訊號殺死,我們獲取導致其死亡的訊號值。

image-20240526224752916

程序的執行

也可以叫程式的替換。

程序程式替換與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 命令字串。如果 commandNULL,則返回 shell 是否存在的狀態
返回值 - 如果 commandNULL
- 返回 0:沒有可用的 shell
- 返回 非 0 值:有可用的 shell
- 如果 command 不為 NULL
- 成功:返回 shell 的退出狀態碼
- 失敗:返回 -1,並設定 errno
備註 1. system 函式使用 /bin/sh 來執行命令。
2. 在呼叫 system 之前,會忽略 SIGINTSIGQUIT 訊號,並且阻塞 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;
}

image-20240526235400702

錯誤處理 (補充)

fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
  1. fprintf: 這個函式用於向流中寫入格式化的輸出。在這個例子中,它將格式化的訊息寫入標準錯誤流(stderr)。
  2. "open user.txt fail, error = %d, %s\n": 這是格式字串。它包含了將要插入字串的佔位符。具體來說:
    • %d 是一個整數值的佔位符(在這個例子中,是 errno 的值)。
    • %s 是一個字串值的佔位符(在這個例子中,是 strerror(errno) 的結果)。
  3. errno: 這個全域性變數儲存了最後一個失敗的系統呼叫的錯誤程式碼。當發生錯誤時,系統呼叫和庫函式會設定這個變數。
  4. 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;
}

image-20240526231054451

相關文章