進程控制總結
- 1 進程創建的三種方式
- fork
- vfrok
- clone
- 2 進程終止
- 進程正常退出
- return
- exit
- _exit
- 進程異常退出
- 進程收到某個信號,而該信號使進程終止
- abort
- 3 進程等待
- 進程等待的方法
- wait
- waitpid
- 4 進程替換
- 替換原理
- 替換函數
- 制作一個簡單的shell
1 進程創建的三種方式
參考文章:
https://zhuanlan.zhihu.com/p/498427466?utm_source=wechat_session&utm_medium=social&utm_oi=977698418977746944&utm_campaign=shareopn
https://blog.csdn.net/gogokongyin/article/details/51178257
在linux中主要提供了fork、vfork、clone三個進程創建方法。在Linux源碼中,這三個調用的執行過程是執行fork()、vfork()、clone()時,通過一個系統調用表映射到sys_fork()、sys_vfork()和sys_clone(),再在這三個函數中去調用do_fork()去做具體的創建進程工作。
fork
fork創建一個進程時,復制出來的子進程有自己的task_struct結構體和pid,然后復制父進程其他所有的資源。
例如,要是父進程打開了五個文件,那么子進程也有五個打開的文件,而且這些文件的當前讀寫指針也停在相同的地方。
這樣得到的子進程獨立于父進程,具有良好的并發性。但是子進程需要復制父進程很多資源,所以fork是一個開銷很大的系統調用,這些開銷并不是所有的情況下都是必須的,比如某進程fork出一個子進程,其子進程僅僅是為了調用exec執行另一個可執行文件,那么fork過程對于虛擬空間的復制將是一個多余的過程。
但由于現在Linux采取了copy-on-write(寫時復制)技術,fork最初不會真的產生兩個不同的拷貝。寫時復制是在推遲真正的數據拷貝,若后來確實發生了寫入,那意味著父進程和子進程的數據不一致了,就需要產生復制動作,每個進程拿到屬于自己的那一份。所以有了寫時復制后,vfork其實現意義就不大了。
fork調用一次,返回兩個值,對于父進程,返回的是子進程的pid值,對于子進程,返回的是0 。 在fork之后,子進程和fork都會繼續執行fork調用之后的指令。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main(void)
{int a=5,b=2;pid_t pid;pid = fork();if(pid==0){/*這是子進程 */a = a-4;printf("child process PID = %d,a=%d,b=%d\n",getpid(),a,b);}else if(pid >0){/* 這是父進程 */printf("parent process PID = %d, a=%d,b=%d\n",getpid(),a,b);}else{perror("fork error");exit(1);}return 0;
}
可見,子進程中將變量a的值該為1,而進程中則保持不變。
vfrok
vfork系統調用不同于fork,用vfork創建的子進程與父進程共享地址空間,也就是說子進程完全運行在父進程的地址空間上,如果這時子進程修改了某個變量,這將影響父進程。
因此,如果fork的例程改用vfork的話,那么兩次打印a、b的值是相同的,所在地址也是相同的。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main(void)
{int a=5,b=2;pid_t pid;pid = vfork();if(pid==0){/*這是子進程 */a = a-4;printf("child process PID = %d,a=%d,b=%d\n",getpid(),a,b);exit(0);}else if(pid >0){/* 這是父進程 */printf("parent process PID = %d, a=%d,b=%d\n",getpid(),a,b);}else{perror("fork error");exit(1);}return 0;
}
但此處有一點要注意的是,用vfork創建的子進程必須先調用exit()來結束,否則子進程將不能結束,fork則不存在這個情況。
vfork也是在父進程中返回子進程的進程號,在子進程中返回0,用vfork創建子進程后,父進程會被阻塞直到子進程調用exec(exec將一個新的可執行文件載入到地址空間并執行)或exit。vfork的好處是在子進程被創建后往往僅僅是為了調用exec執行另一個程序,因為它就不會對父進程的地址空間由任何引用,因此通過vfork共享內存可以減少不必要的開銷。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main(void)
{int a=5,b=2;pid_t pid;pid = fork();if(pid==0){/*這是子進程 */if(execl("./vfork_example","example",NULL)<0){perror("exec error");exit(1);}}else if(pid >0){/* 這是父進程 */printf("parent process a=%d,b=%d,the address a = %p ,b=%p\n",a,b,&a,&b);}else{perror("vfork error");exit(1);}return 0;
}
vfork_example.c
#include <stdio.h>
#include <unistd.h>
int main(void)
{int a=1,b=2;sleep(3);printf("child process,a=%d,b=%d,the address a =%p,b =%p\n",a,b,&a,&b);return 0;
}
子進程調用了exec,父進程會繼續執行,子進程sleep(3),所以父進程會提前結束。
clone
系統調用fork()和vfork()是無參數的,而clone()則帶有參數。fork()是全部復制,vfork()是共享內存,而clone是可以將父進程資源有選擇地復制給子進程,而沒有復制的數據結構則通過指針的復制讓子進程共享,具體要復制那些資源給子進程,由參數列表中的clone_flags來決定。
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, void *newtls, pid_t *ctid */ );
fn為函數指針,此指針指向一個函數體,即想要創建進程的靜態程序(我們知道進程的4要素,這個就是指向程序的指針,就是所謂的“劇本", );child_stack為給子進程分配系統堆棧的指針(在linux下系統堆棧空間是2頁面,就是8K的內存,其中在這塊內存中,低地址上放入了值,這個值就是進程控制塊task_struct的值);arg就是傳給子進程的參數一般為(0);flags為要復制資源的標志,描述你需要從父進程繼承那些資源(是資源復制還是共享,在這里設置參數:
下面是flags可以取的值
標志 | 含義 |
---|---|
CLONE_PARENT | 創建的子進程的父進程是調用者的父進程,新進程與創建它的進程成了“兄弟”而不是“父子” |
CLONE_FS | 子進程與父進程共享相同的文件系統,包括root、當前目錄、umask |
CLONE_FILES | 子進程與父進程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 在新的namespace啟動子進程,namespace描述了進程的文件hierarchy |
CLONE_SIGHAND | 子進程與父進程共享相同的信號處理(signal handler)表 |
CLONE_PTRACE | 若父進程被trace,子進程也被trace |
CLONE_VFORK | 父進程被掛起,直至子進程釋放虛擬內存資源 |
CLONE_VM | 子進程與父進程運行于相同的內存空間 |
CLONE_PID | 子進程在創建時PID與父進程一致 |
CLONE_THREAD | Linux 2.4中增加以支持POSIX線程標準,子進程與父進程共享相同的線程群 |
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>int variable,fd;int do_something(void*arg)
{variable = 42;printf("in child process\n");close(fd);return 0;
}int main(void)
{void *child_stack;char tempch;variable = 9;fd = open("./test.txt",O_RDONLY);child_stack=(void *)malloc(16384);printf("The varibale is %d\n",variable);clone(do_something,child_stack,CLONE_VM|CLONE_FILES,NULL);sleep(3);printf("The variable is now %d\n",variable);if(read(fd,&tempch,1)<1){perror("file read error");exit(1);}printf("we could read from the file\n");return 0;
}
我們在clone指定了CLONE_VM和CLONE_FILES,所以子進程與父進程共享相同的文件描述符(file descriptor)表以及子進程與父進程運行于相同的內存空間,所以會出現上述情況。
2 進程終止
參考文章:
https://zhuanlan.zhihu.com/p/435709371
https://zhuanlan.zhihu.com/p/63424197
進程正常退出
return
在main函數中使用return退出進程。return num等同于exit(num),所做的事可以看下面的exit介紹。
exit
exit函數可以在代碼中任何位置使進程退出,并且exit在退出進程前還會做一系列工作:
- 調用用戶通過atexit或on_exit定義的函數
- 關閉所有打開的流,所有的緩存數據均被刷新
- 調用_exit函數終止進程。
#include <stdio.h>
#include <stdlib.h>void show()
{printf("hello world");exit(1);
}
int main(void)
{show();return 0;
}
終止進程前會將緩沖區當中的數據輸出。
_exit
_exit函數也可以在代碼中的任何地方退出進程,但是_exit函數會直接終止進程,并不會在退出進程前會做任何收尾工作。
我們將上面代碼中的exit函數改成_exit函數,運行會沒有輸出。
進程異常退出
進程收到某個信號,而該信號使進程終止
例如,在進程運行過程中向進程發生kill -9信號使得進程異常退出,或是使用Ctrl+C使得進程異常退出等。
abort
調用abort()函數,會使進程異常終止。
3 進程等待
https://zhuanlan.zhihu.com/p/435709371
進程等待的方法
wait
pid_t wait(int* status);
等待任意子進程退出,status保存子進程的退出碼。所以父進程會被阻塞,直到子進程退出。WEXITSTATUS(status)宏可以獲取子進程的退出值。
on success, returns the process ID of the terminated child; on error, -1 is returned.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main(void)
{pid_t pid = fork();if(pid==0){int count = 10;while(count--){printf("Child process : PID = %d; PPID : %d\n",getpid(),getppid());sleep(1);}}else if(pid>0){int status;pid_t ret = wait(&status);if(ret>0){printf("wati child success \n");printf("child process pid = %d,return status=%d\n",ret,WEXITSTATUS(status));}}else{printf("fork error\n");exit(1);}exit(0);
}
waitpid
函數原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);
參數含義:
pid:
< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.-1 meaning wait for any child process.0 meaning wait for any child process whose process group ID is equal to that of the calling process.> 0 meaning wait for the child whose process ID is equal to the value of pid.
options的值是下面0個或多個或(OR)值
- WNOHANG (wait no hung): 即使沒有子進程退出,它也會立即返回,直接返回0,不會像wait那樣永遠等下去。
- WUNTRACED :用于調試。
如果孩子已經停止(但沒有通過 ptrace(2) 跟蹤),也會返回。 即使未指定此選項,也會提供已停止的跟蹤子項的狀態。
state和wait一樣。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main(void)
{pid_t pid = fork();if(pid==0){int count = 10;while(count--){printf("Child process : PID = %d; PPID : %d\n",getpid(),getppid());sleep(1);}}else if(pid>0){int status;pid_t ret = waitpid(pid,&status,0);if(ret>0){printf("wati child success \n");printf("child process pid = %d,return status=%d\n",ret,WEXITSTATUS(status));}}else{printf("fork error\n");exit(1);}exit(0);
}
運行的結果和wait一樣。
4 進程替換
原文鏈接:
https://zhuanlan.zhihu.com/p/435709371
替換原理
用fork創建子進程后,子進程執行的是和父進程相同的程序(但有可能執行不同的代碼分支),如想讓子進程執行另一個程序,往往需要調用一種exec函數。
當進程調用exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,并從新程序的啟動代碼開始執行。
-
當進程程序被替換后,有沒有創建新的進程?
進程程序被替換之后,該進程對應的PCB、進程地址空間 以及頁表等數據結構都沒法發生改變,只是進程在物理內存當中的數據和代碼發生了改變,所有并沒有創建新的進程,而且進程程序替換前后該進程的pid并沒發生改變。 -
子進程進行進程程序替換后,會影響父進程的代碼和數據嗎?
子進程剛被創建時,與父進程共享代碼和數據,但當子進程需要進行進程程序替換時,也就意味著子進程需要對其數據和代碼進行寫入操作,這時便需要將父子進程共享的代碼和數據進行寫時拷貝,此后父子進程的代碼和數據也就分離了,因此子進程進行程序替換后不會影響父進程的代碼和數據。
替換函數
替換函數有六種以exec開頭的函數,它們統稱為exec函數。
int execl(const char *path, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execve(const char *file, char *const argv[],char *const envp[]);
exec函數的后綴含義如下:
- l(list):表示參數采用列表的形式
- v(vector):表示參數采用數組的形式
- p(path):表示能自動搜素環境變量PATH,進行程序查找
- e(env):表示可以傳入自己設置的環境變量。
事實上,只有execve才是真正的系統調用,其它五個函數最終都是調用的execve,所以execve在man手冊的第2節,而其它五個函數在man手冊的第3節,也就是說其他五個函數實際上是對系統調用execve進行了封裝,以滿足不同用戶的不同調用場景的。
制作一個簡單的shell
shell也就是命令行解釋器,其運行原理就是:當有命令需要執行時,shell創建子進程,讓子進程執行命令,而shell只需等待子進程退出即可。
其實shell需要執行的邏輯非常簡單,其只需循環執行以下步驟:
- 獲取命令行。
- 解析命令行。
- 創建子進程。
- 替換子進程。
- 等待子進程退出。
其中,創建子進程使用fork函數,替換子進程使用exec系列函數,等待子進程使用wait或者waitpid函數。
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個數
int main()
{char cmd[LEN]; //存儲命令char* myargv[NUM]; //存儲命令拆分后的結果char hostname[32]; //主機名char pwd[128]; //當前目錄while (1){//獲取命令提示信息struct passwd* pass = getpwuid(getuid());gethostname(hostname, sizeof(hostname)-1);getcwd(pwd, sizeof(pwd)-1);int len = strlen(pwd);char* p = pwd + len - 1;while (*p != '/'){p--;}p++;//打印命令提示信息printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);//讀取命令fgets(cmd, LEN, stdin);cmd[strlen(cmd) - 1] = '\0';//拆分命令myargv[0] = strtok(cmd, " ");int i = 1;while (myargv[i] = strtok(NULL, " ")){i++;}pid_t id = fork(); //創建子進程執行命令if (id == 0){//childexecvp(myargv[0], myargv); //child進行程序替換exit(1); //替換失敗的退出碼設置為1}//shellint status = 0;pid_t ret = waitpid(id, &status, 0); //shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出碼}}return 0;
}
說明:
當執行./myshell命令后,便是我們自己實現的shell在進行命令行解釋,我們自己實現的shell在子進程退出后都打印了子進程的退出碼,我們可以根據這一點來區分我們當前使用的是Linux操作系統的shell還是我們自己實現的shell