《Linux系統程式設計訓練營》7_程式建立大盤點

發表於2023-09-24

vfork 與程式建立

程式建立回顧
int create_process(char *path, char *args[], char *env[])
{
    int ret = fork();

    if (ret == 0) {
        execve(path, args, env);
    }
    
    return ret;
}
問題:程式建立是否只能依賴於 fork() 和 execve() 函式?

再論程式建立

  • fork() 透過完整複製當前程式的方式建立新程式
  • execve() 根據引數覆蓋程式資料(一個不留)

image.png

pid_t vfork(void);

  • vfork() 用於建立子程式,然而不會複製父程式空間中的資料
  • vfork() 建立的子程式直接使用父程式空間(沒有完整獨立的程式空間)
  • vfork() 建立的子程式對資料(變數)的修改會直接反饋到父程式中
  • vfork() 是為了 execve() 系統呼叫而設計

下面的程式執行後會發生什麼?

vfork.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t pid = 0;
    int var = 88;

    printf("parent = %d\n", getpid());

    if ((pid = vfork()) < 0) {
        printf("vfork error\n");
    }
    else if (pid == 0) {
        printf("pid = %d, var = %d\n", getpid(), var);
        var++;
        printf("pid = %d, var = %d\n", getpid(), var);
        return 0;
    }

    printf("parent = %d, var = %d\n", getpid(), var);

    return 0;
}
parent = 52385
pid = 52386, var = 88
pid = 52386, var = 89
parent = 52385, var = -661685664
Segmentation fault (core dumped)

vfork() 深度分析

  • 子程式使用父程式的資料空間
    image.png

vfork() 要點分析

  • vfork() 成功後,父程式將等待子程式結束
  • 子程式可以使用父程式的資料(堆,棧,全域性)
  • 子程式可以從建立點呼叫其它函式,但不要從建立點返回

    • 當 子程式執行流 回到建立點 / 需要結束 時,使用 _exit(0) 系統呼叫
    • 如果使用 return 0 那麼將破壞棧結構,導致後續父程式執行出錯

當子程式呼叫其它函式及被呼叫函式返回時,不會破壞原有的棧空間

image.png

在上述程式碼中,子程式使用 return 結束自己時導致了棧回收。當子程式結束,父程式開始執行時,棧幀已經被銷燬,出現執行錯誤。

image.png

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t pid = 0;
    int var = 88;

    printf("parent = %d\n", getpid());

    if ((pid = vfork()) < 0) {
        printf("vfork error\n");
    }
    else if (pid == 0) {
        printf("pid = %d, var = %d\n", getpid(), var);
        var++;
        printf("pid = %d, var = %d\n", getpid(), var);
        // return 0;  /* distroy parent stack frame */
        _exit(0);
    }

    printf("parent = %d, var = %d\n", getpid(), var);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out 
parent = 52539
pid = 52540, var = 88
pid = 52540, var = 89
parent = 52539, var = 89

fork 與 vfork 的選擇

個人思考:vfork 弊大於利,使用時需要格外小心避免奇奇怪怪的問題,fork 雖然效率低,但更容易被使用

fork() 的現代最佳化

  • Copy-on-Write 技術

    • 多個任務訪問同一資源,在寫入操作修改資源時,複製資源的原始副本
  • fork() 引入 Copy-on-Write 之後,父子程式共享相同的程式空間

    • 當父程式或子程式的其中之一修改記憶體資料,則實時複製程式空間
    • fork() + execve() ←→ vfork() + execve()

Linux的fork()系統呼叫會建立一個新的程式,但是該程式與父程式共享相同的記憶體對映。當程式呼叫exec()函式時,該程式的記憶體對映會被替換為新的程式的記憶體對映,但是這個操作並不會導致程式的複製。實際上,寫時複製(Copy-on-Write)機制會延遲對共享記憶體的複製,直到其中一個程式試圖對共享記憶體進行寫操作時才會進行復制。這樣可以減少記憶體的使用和複製的開銷。

fork出來子程式之後,父子程式哪個先排程直接決定了是否需要複製的問題?核心一般會先排程子程式,因為很多情況下子程式是要馬上執行exec,而避免無用的複製。如果父程式先排程很可能寫共享頁面,會產生“寫時複製”的無用功。所以,一般是子程式先排程。

程式設計實驗 fork & vfork

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int create_process(char *path, char *const args[], char *const env[], int wait)
{
    int ret = fork();

    if (ret == 0) {
        if (execve(path, args, env) == -1) {
            exit(-1);
        }
    }

    if (wait && ret) {
        waitpid(ret, &ret, 0);
    }

    return ret;
}

int main(int argc, char *argv[])
{
    char *target = argv[1];
    char *const ps_argv[] = {target, NULL};
    char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Delphi", NULL};

    int result = 0;

    if (argc < 2) exit(-1);

    printf("current : %d\n", getpid());

    result = create_process(target, ps_argv, ps_envp, 1);

    printf("result = %d\n", result);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out ./helloword.out 
current : 54739
Hello Word!
result = 0

exec 與 system 簡介

exec 函式家族

#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *file, char *const argv[], char *const envp[]);
l(list):引數地址列表,以空指標結尾
v(vector):存有各引數地址的指標陣列的地址
p(path):按 PATH 環境變數指定的目錄搜尋可執行檔案。
e(environment):存有環境變數字串地址的指標陣列的地址。

exec 函式族裝入並執行可執行程式 path/file,並將引數 arg0 ( arg1, arg2, argv[], envp[] ) 傳遞給此程式。

exec 函式族與一般的函式不同,exec 函式族中的函式執行成功後不會返回,而且,exec 函式族下面的程式碼執行不到。只有呼叫失敗了,它們才會返回 -1,失敗後從原程式的呼叫點接著往下執行。
path:要執行的程式路徑。可以是絕對路徑或者是相對路徑。在execv、execve、execl和execle這4個函式中,使用帶路徑名的檔名作為引數。
file:要執行的程式名稱。如果該引數中包含“/”字元,則視為路徑名直接執行;否則視為單獨的檔名,系統將根據PATH環境變數指定的路徑順序搜尋指定的檔案。
argv:命令列引數的向量陣列。
envp:帶有該引數的exec函式可以在呼叫時指定一個環境變數陣列。其他不帶該引數的exec函式則使用呼叫程式的環境變數。
arg:程式的第0個引數,即程式名自身。相當於argv[O]。
…:命令列引數列表。呼叫相應程式時有多少命令列引數,就需要有多少個輸入引數項。注意:在使用此類函式時,在所有命令列引數的最後應該增加一個空的引數項(NULL),表明命令列引數結束。
返回值:一1表明呼叫exec失敗,無返回表明呼叫成功。
函式使用檔名使用路徑名使用引數列表(函式出現字母l)使用 argv(函式出現字母v)指定環境變數
execl
execlp
execle
execv
execvp
execve

#include <stdio.h>
#include <unistd.h>

int main(int argc, char* argv[])
{   
    char pids[32] = {0};
    char* const ps_argv[] = {"pstree", "-A", "-p", "-s", pids, NULL};
    char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Delphi", NULL};
    
    sprintf(pids, "%d", getpid());
    
    execl("/bin/pstree", "pstree", "-A", "-p", "-s", pids, NULL);
    execlp("pstree", "pstree", "-A", "-p", "-s", pids, NULL);
    execle("/bin/pstree", "pstree", "-A", "-p", "-s", pids, NULL, ps_envp);
    execv("/bin/pstree", ps_argv);
    execvp("pstree", ps_argv);
    execve("/bin/pstree", ps_argv, ps_envp);
    
    return 0;
}

程式建立庫函式

  • #include <stdlib.h>
  • int system(const char *command);

    • 引數, 程式名及程式引數 (如:pstree -A -p -s $$)
    • 返回值,程式退出狀態
system 在 linux 中的實現(system 首先建立 shell 程式,功能強大,但效率較低)
system()會呼叫fork()產生子程式,由子程式來呼叫/bin/sh-c string來執行引數string字串所代表的命令,此命令執行完後隨即返回原呼叫的程式。

int system(const char * cmdstring)
{
    pid_t pid;
    int status;

    if(cmdstring == NULL){  
         return (1);
    }

    if((pid = fork())<0){
            status = -1;
    }else if(pid = 0){
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        -exit(127);                                  // 子程式正常執行則不會執行此語句
    }else{
        while(waitpid(pid, &status, 0) < 0){         // 父程式等待子程式結束 !!
            if(errno != EINTER){
                status = -1;
                break;
            }
        }
    }
    return status;
}

image.png

程式設計實驗

system.sh
echo "Hello world from shell"
a=1
b=1
c=$(($a + $b))
echo "c = $c"
system.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int result = 0;

    printf("current : %d\n", getpid());

    result = system("pstree -A -p -s $$");  // $$ shell 標識,表示當前程式 pid

    printf("result : %d\n", result);

    result = system("./system.sh");

    printf("result : %d\n", result);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ chmod 777 ./system.sh 
tiansong@tiansong:~/Desktop/linux$ ./system.sh 
Hello world from shell
c = 2

tiansong@tiansong:~/Desktop/linux$ gcc system.c
tiansong@tiansong:~/Desktop/linux$ ./a.out 
current : 55370
systemd(1)---sh(2545)---node(2555)---node(2738)---bash(12984)---a.out(55370)---pstree(55371)
result : 0
Hello world from shell
c = 2
result : 0

相關文章