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()
根據引數覆蓋程式資料(一個不留)
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() 深度分析
- 子程式使用父程式的資料空間
vfork() 要點分析
- vfork() 成功後,
父程式將等待子程式結束
- 子程式可以使用父程式的資料(堆,棧,全域性)
子程式可以從建立點呼叫其它函式,但不要從建立點返回
- 當 子程式執行流 回到建立點 / 需要結束 時,使用
_exit(0)
系統呼叫 - 如果使用 return 0 那麼將破壞棧結構,導致後續父程式執行出錯
- 當 子程式執行流 回到建立點 / 需要結束 時,使用
當子程式呼叫其它函式及被呼叫函式返回時,不會破壞原有的棧空間
在上述程式碼中,子程式使用 return
結束自己時導致了棧回收。當子程式結束,父程式開始執行時,棧幀已經被銷燬,出現執行錯誤。
#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;
}
程式設計實驗
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