一. 進程創建
1.fork的概念與使用
在 Linux 中 fork 可以在一個進程中創建一個新的進程。這個新進程稱為子進程,原進程為父進程。使用前需要包含頭文件 #include <unistd.h> 。在調用 fork 函數時,子進程與父進程會共享數據和代碼,此時它們共用同一塊物理地址空間。但當子進程或父進程運行時,對數據進行了修改進行了寫入,此時系統將進行寫時拷貝,重新給子進程或者父進程申請物理地址。
fork 函數調用時父進程會返回子進程 pid (大于0),子進程會返回0。
二. 進程終止?
進程的退出場景分為三類,第一是代碼運行完畢并且結果正確,第二是代碼運行完畢但結果不正確,第三是代碼運行時異常終止。
在第一種和第二種情況下,我們可以通過退出碼來分辨,退出碼為0就是結果正確,不為0結果不正確。不同的退出碼代表不同的退出狀態。
?當我們想查看進程最近一次的退出碼時,輸入指令 echo $?? 可以查看到退出碼。
我們通常退出程序時以return exit 或 _exit 結尾終止進程。那么它們直接有什么區別呢?
return 返回通常為函數終止,但不一定代表結束,若在函數中return 返回后程序還會繼續運行。而exit 退出則代表程序結束了,此時 exit 會自動執行全局清理。_exit 與 exit 的區別在于 _exit 可以在任意位置結束程序,并且不會對全局進行清理。
int main() { printf("hello"); exit(0); } 運?結果: [root@localhost linux]# ./a.out hello[root@localhost linux]#int main() { printf("hello"); _exit(0); } 運?結果: [root@localhost linux]# ./a.out [root@localhost linux]#
?三. 進程等待
在進程中,子進程退出若父進程不進行管理,會導致子進程變成“僵尸進程”,造成內存泄漏。在進程等待中,我們用 wait 和 waitpid 這兩個函數。
3.1 wait與waitpid
我們在使用這兩個函數前要包含兩個頭文件。
#include<sys/types.h> #include<sys/wait.h>
pid_t wait(int* status)
其中status用于儲存 wait 中子進程的退出狀態,status需要是一個int類型,我們需要將他視作位圖,里面會存儲進程的退出碼,我們也可以傳遞NULL進去表示不關心退出狀態。若子進程成功退出,wait會返回子進程pid,若失敗則返回-1。
pid_t waitpid(pid_t pid,int *status,int options)
waitpid 內包含了三個參數,pid,status和options,這里的pid調用的是等待進程的pid,而status與上文一致,options分為阻塞等待和非阻塞等待(下文講解)。
3.2 阻塞與非阻塞等待
阻塞等待阻塞的是父進程,當父進程處于阻塞狀態時,不能進行其他的代碼操作,需要一直等待子進程完成任務后得到退出碼才能執行自己的代碼。而非阻塞等待的父進程可以在等待子進程時,運行父進程的代碼,我們可以通過循環的方式,限制多長時間對子進程進行訪問是否返回。
通過一個簡單的例子說明,我們在等待女友化妝,阻塞等待就是單純的等女友化完妝,而非阻塞等待意味著,我們可以在女友化妝時做一些其他的事情。
下面是進程阻塞方式的代碼:
此時options的參數為0.
int main()
{pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { //childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(257);}else {int status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("this is test for wait\n");if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is:%d.\n",WEXITSTATUS(status));}else {printf("wait child failed, return.\n");return 1;}}return 0;
}運?結果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
pid 返回了子進程的pid,并且等待了5秒。
四. 進程程序替換
4.1 替換原理
程序在 fork() 之后,父進程和子進程共用同一份代碼(可能執行不同的分支),若我們當前想讓子進程獨立執行一個全新的程序該如何操作呢?
這里我們會調用一種exec開頭的函數,他會將我們子進程的代碼進行替換,將需要更換的程序從磁盤里面拷貝下來。他不會產生新的進程,所以子進程的 pid 仍然保持不變,exec 只是對代碼段數據段進行了替換。
4.2 替換函數
一共有六中以 exec 開頭的函數:
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ...,char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
?4.2.1 execl
execl 參數有 path 和 arg,path 代表要替換程序在磁盤中的文件地址,arg 代表需要替換的功能。
execl("/bin/ls", "ls", "-l", NULL);
在文件 /bin/ls 下 ,調用 ls -l 這一功能,其中要在末尾添加上NULL,代表結束。
4.2.2 execlp
execlp 參數有 file 和 arg,file 可以根據命令查找到對應的文件,例如我們可以將 /bin/ls 文件改為 ls 也能實現同樣的功能。后面的 arg 代表要實現的功能。
execlp("ls", "ls", "-l", NULL);
4.2.3 execle
execle 與 execlp 的不同之處在于,它的代表著環境變量 env ,我們在調用時需要自己組裝環境變量,當然也可以使用系統環境變量。
我們可以對第三個參數 envp【】自行添加環境變量,例如 { "KEY1=VALUE1","KEY2=VALUE2"};
execle("ls", "ls", "-l", NULL, envp);
4.2.4 execv
我們將 execv 與 execl 進行對比,它們區別在于 “v” 傳遞的參數為數組,而 “l” 傳遞的參數為指針。
char *const argv[] = {"ls", "-l", NULL};
execv("/bin/ps", argv);
同樣的,在數組最后也要加上NULL表示終止。
4.2.5 execvp
結合上述對函數的解析,這個函數我們不難進行理解,“v” 代表傳遞的是數組(里面包含相應調用的功能),“p” 代表根據功能找到對應的文件地址。
char *const argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
4.2.6 execve
與 exec 相比多了 “e”和 “v” ,也就是需要添加數組和環境變量數組。
char *const argv[] = {"ls", "-l", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execve("/bin/ls", argv, envp);
總結:事實上,在操作系統中真正被調用的只有 execve 這個函數,其他函數都是對其進了封裝。因為在 man 手冊第二節存在execve ,而其他函數都在man 手冊第三節。本質是將 “l” 轉化為 “v” 將可變參數報錯帶數組中,將 “p” 轉化為 “e” 將環境變量轉化為數組。?