1. 程式人生 > >Linux系統程式設計_程序

Linux系統程式設計_程序

  • 程式 vs. 程序:
    程式,是指編譯好的二進位制檔案,是靜態概念,在磁碟上,不佔用系統資源(cpu、記憶體、開啟的檔案、裝置、鎖….);
    程序,是一個抽象的動態概念,與作業系統原理聯絡緊密。程序是活躍的程式,佔用系統資源。在記憶體中執行。(程式執行起來,產生一個程序)
    程式 → 劇本(紙) 程序 → 戲(舞臺、演員、燈光、道具…)
    同一個劇本可以在多個舞臺同時上演。同樣,同一個程式也可以載入為不同的程序(彼此之間互不影響)
    如:同時開兩個終端。各自都有一個bash但彼此ID不同。

  • MMU(Memory Management Unit,記憶體管理單元):CPU內部的MMU完成從虛擬記憶體到實體記憶體的對映過程。MMU以4k大小的頁為單位。

  • PCB(程序控制塊):在核心區,每個程序僅此一份。PCB中的常見包含的內容需瞭解。

程式執行的四級流水示意圖:
這裡寫圖片描述

MMU的作用示意圖:
這裡寫圖片描述

  • 程序四狀態:就緒、執行、掛起\阻塞、停止。
    這裡寫圖片描述

  • 環境變數:在作業系統中用來指定作業系統執行環境的一些引數。

程序控制

  • 建立程序:fork函式,父程序的fork函式返回子程序的id,子程序的fork返回0。注意返回值,不是fork函式能返回兩個值,而是fork後,fork函式變為兩個,父子需各自返回一個。注意:雖然返回的子程序也擁有了一份和父程序一樣的程式,但是在子程序中,該程式是從該子程序被建立處,即fork函式返回處,繼續往後執行的,前面的程式部分不再執行。
  • 迴圈建立指定數量的子程序並區分每個子程序:
    注意:除去父程序外,第ifork()後,共有2^i-1個子程序被創建出來,因為第一次fork後就有不止一個程序在執行這個程式。
//迴圈建立5個子程序並打印出每個子程序的程序ID
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    pid_t pid;

    int i;
    for(i = 0; i < 5; i++)  //出口1,父程序專用出口
        if
((pid = fork()) == 0) break; //出口2,子程序出口,i不自增 if(5 == i){ sleep(n); printf("I am parent, pid = %d\n", getpid()); } else { sleep(i); printf("I'm %dth child, pid = %d\n", i+1, getpid()); } return 0; }
  • 程序共享:
    (1)讀時共享寫時複製原則:對於父子程序的相同部分內容是適用的,這樣以免相同的內容通過MMU被重複對映到記憶體中不同的空間,便於節省記憶體。不過該原則指出,在寫時就要複製一份相同的內容給子程序了,使得父子程序各自修改自己的內容。此處,有一個關於程式中全域性變數會干擾父子程序內容的誤區。事實上,寫時複製已經明確了,若是要修改父或子程序的內容,就會複製一份給子程序,從此修改一方的內容就不會再對另一方有任何影響,不管修改的是什麼內容,包括全域性變數。
    (2)注意,上述原則僅適用於父子程序間的使用者空間資料,而父子程序的核心空間資料,即PCB的內容,經MMU對映到實際物理空間中是同一塊內容,這給父子程序間通訊打下了基礎。
    (3)父子程序間相同和不同的內容:
    父子相同處: 全域性變數、.data、.text、棧、堆、環境變數、使用者ID、宿主目錄、程序工作目錄、訊號處理方式…
    父子不同處: 1.程序ID 2.fork返回值 3.父程序ID 4.程序執行時間 5.鬧鐘(定時器) 6.未決訊號集
    【重點】:父子程序共享:1. 檔案描述符(開啟檔案的結構體) 2. mmap建立的對映區
    特別的,fork之後父程序先執行還是子程序先執行不確定。取決於核心所使用的排程演算法。
    (4)多程序gdb除錯:
set follow-fork-mode parent //跟蹤父程序(預設)
set follow-fork-mode child //跟蹤子程序
//注意,上述兩條指令要在fork函式呼叫之前設定!

(5)exec函式族:在程式執行期間執行另外一個程式,讓子程序和父程序完全區分開來,執行內容與父程序無關,徹底獨立出來,但子程序id不變。可理解為子程序中,執行exec函式族中任一函式後,子程序覆蓋了原來複制自父程序的程式碼段和資料段。
exec函式一旦呼叫成功即執行新的程式,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函式呼叫後直接呼叫perror()和exit(),無需if判斷。

回收子程序

  • 孤兒程序:父程序先於子程序結束,則子程序成為孤兒程序。孤兒子程序的父程序自動變為init程序,稱為init程序領養孤兒程序。
  • 殭屍程序:程序終止,父程序尚未回收,子程序殘留資源(PCB)存放於核心中,變成殭屍(Zombie)程序。
    特別注意,殭屍程序是不能使用kill命令清除掉的。因為kill命令只是用來終止程序的,而殭屍程序已經終止。那用什麼辦法可清除掉殭屍程序呢?殺死父程序即可,這樣一來該殭屍程序就被劃歸init程序的孤兒院並被回收。
  • 回收子程序:
    (1)wait函式,pid_t wait(int *status),返回值是應該回收的子程序ID,引數是應該獲取的子程序退出狀態,注意這個引數是一個傳出引數,若不關心子程序退出狀態,那麼給wait函式傳NULL。注意:用int型別變數status來存子程序狀態也是通過點陣圖(bitmap)的方式,所以接下來獲取子程序退出的訊號要藉助巨集函式,這一點類似於前面的檔案操作。
    父程序呼叫wait函式可以回收子程序終止資訊。該函式有三個功能:
    ① 阻塞等待子程序退出
    ② 回收子程序殘留資源
    ③ 獲取子程序結束狀態(退出原因)。
//利用wait函式回收子程序並獲得其退出狀態
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid, wpid;
    int status;

    pid = fork();
    if(pid == -1) {
        perror("fork error");
        exit(1);
    } else if(pid == 0) {
        //sleep(100);
        printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid());
        return 20;
    } else if(pid > 0) {
        sleep(1);
        printf("I am parent, pid = %d, ppid = %d\n", getpid(), getppid());
        wpid = wait(&status);
        printf("wpid = %d\n", wpid);

        if(WIFEXITED(status)) { //子程序正常退出
            printf("exit with %d\n", WEXITSTATUS(status));
        } else if(WIFSIGNALED(status)) { //子程序非正常退出
            printf("killed by %d\n", WTERMSIG(status));
        }
    }

    return 0;
}

(2)waitpid函式:作用同wait,但可指定pid程序清理,可以不阻塞。

pid_t waitpid(pid_t pid, int *status, in options);
//返回值:
成功:返回清理掉的子程序ID;
失敗:-1(無子程序)
特殊引數和返回情況:
    對於引數pid: 
        > 0 回收指定ID的子程序  
        -1 回收任意子程序(相當於wait0 回收和當前呼叫waitpid一個組的所有子程序
        < -1 回收指定程序組內的任意子程序
返回0:引數3:options為WNOHANG,且子程序正在執行。

注意:一次wait或waitpid呼叫只能清理一個子程序,清理多個子程序應使用迴圈。