exec 系列(execve 與同伴)
fork() 給你同一個程式的第二份副本——但通常你想要新行程去跑一個完全不同的程式(你 fork 了你的 shell,可是你想跑 ls,不是又一個 shell)。這正是 exec 在做的事:它把目前的行程拿來,把它正在跑的一切——程式碼、資料、堆積、堆疊——換成從一個可執行檔載入的全新程式。行程保留它的身分(同樣的 PID、同樣開著的檔案),但它所跑的程式被整批換掉。想像一位演員保有自己的化妝間(行程),卻換上一套完全不同的戲服與劇本(程式映像)。
具體來說,像 execve("/bin/ls", argv, envp) 這樣的呼叫告訴核心:丟掉這個行程目前的定址空間,把 /bin/ls 那個程式載入一個全新映像,設好引數列(argv)與環境(envp),再從新程式的進入點開始執行。關鍵又令人意外的事實:當 exec 成功時它不會回傳。在舊程式裡沒有「exec 之後」,因為舊程式在這個行程裡已不復存在——它的記憶體被丟掉了。成功的 exec() 之後的程式碼,只有在 exec 失敗時(例如找不到檔案)才會跑,這正是為什麼你通常會在呼叫後面緊接著看到一個 perror() 與 exit。「系列」這部分——execl、execlp、execv、execvp、execve、execve——只是同一概念外面不同的方便包裝,差別在你怎麼傳引數(清單還是陣列),以及它們會不會去 PATH 裡搜尋程式。
為何重要:exec 是每個程式如何被啟動的後半。固定的身分(同樣的 PID)正是讓父行程在子行程變成另一個程式之後,仍能追蹤並等待它的原因。exec 也是環境被跨越邊界傳入新程式的地方,以及新程式的 argv[0] 被設定的地方。理解 exec 是替換映像——而非啟動一個分開的新行程——正是讓 fork 接 exec 模式豁然開朗的關鍵。
char *argv[] = { "ls", "-l", NULL }; execvp("ls", argv); perror("execvp"); _exit(127); —— 如果 execvp 成功,這個行程現在正在跑 ls,perror 那行永遠不會執行;如果跑到了 perror,表示 exec 失敗了,我們回報錯誤並結束。
成功的 exec() 之後的程式碼永遠不會跑——跑到它就表示 exec 失敗了。
exec 替換的是程式,但保留行程:同樣的 PID,而且(除非標了 close-on-exec)同樣開著的檔案描述符會延續過去。它不會建立子行程——要得到新行程又跑新程式,你得先 fork、再 exec。