進程創建
在Linux中我們使用fork函數創建新進程:
fork函數
fork函數是Linux中的一個系統調用,用于創建一個新的進程,創建的新進程是原來進程的子進程
返回值:如果子進程創建失敗,返回值是-1。如果子進程創建成功,對于父進程而言,fork的返回值是子進程的pid(子進程的進程號);對于子進程而言,fork的返回值是0。
讓父子進程分別打印fork函數的返回值和自己的PID:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{pid_t pid; //parent_idpid_t cid; //child_pidprintf("Before fork Process id: %d\n", getpid());cid = fork();printf("After fork, Process id: %d\n", getpid());printf("fork_function return: %d\n", cid);sleep(5);return 0;
}
實驗結果:
調用fork后,內核中的fork代碼會執行:
? 分配新的內存塊和內核數據結構給?進程
? 將?進程部分數據結構內容拷???進程
? 將?進程添加到系統進程列表當中
? fork返回,調度器開始調度
子進程被創建好后,父子進程會并發執行后續代碼
父子進程內存空間獨立
上文說到,fork函數會把父進程的代碼和數據拷貝給子進程,以此來保證進程之間的獨立性,但實際上,為了節省內存空間和提高運行效率,只有當子進程的代碼和數據發生變化時,才會為子進程開辟一塊內存空間,并將父進程的代碼和數據拷貝給子進程,這被稱為寫時拷貝
而在寫時拷貝之前,父子進程的虛擬地址相同,并且通過頁表映射后指向同一處物理地址
而在寫時拷貝之后,父子進程的虛擬地址仍然相同,只是會映射到不同的物理地址
進程終止
進程終?的本質是釋放進程申請的相關內核數據結構和對應的數據和代碼。
進程退出的三種情況:
1.代碼運?完畢,結果正確
2.代碼運?完畢,結果不正確
3. 代碼異常終?
為了判斷進程終止是哪種情況,進程在結束時會將退出碼和退出信號返回給父進程,其中,退出信號用于判斷進程是否異常,退出碼用于判斷進程運行結果是否正確
如何獲取退出碼和退出信號,我們在進程等待部分再做介紹,這里先介紹常見的退出碼:
可以使?strerror函數來獲取退出碼對應的描述。
返回不同的退出碼:
1.使用 return n
2.使用void exit(int status)或void _exit(int status),二者區別如下:
return n和exit(n)等效
進程等待
進程等待的目的:
1.檢查子進程的任務執行
2.避免產生僵尸進程
進程等待的方法
wait函數
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待進程 pid ,失敗返回-1 。
參數:
status:輸出型參數,獲取?進程退出碼?, 不關?則可以設置成為 NULL
一個父進程可能有多個子進程,而wait函數等待的是任意子進程
因此,推薦使用waitpid函數:
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
當正常返回的時候 waitpid 返回收集到的?進程的進程 ID ;
如果設置了選項 WNOHANG, ?調?中 waitpid 發現沒有已退出的?進程可收集 , 則返回 0 ;
如果調?中出錯 , 則返回-1, 這時 errno 會被設置成相應的值以指?錯誤所在;
參數:
pid :
Pid=-1, 等待任?個?進程。與 wait 等效。
Pid>0. 等待其進程 ID 與 pid 相等的?進程。
status: 輸出型參數,為了便于調用,可以使用以下兩個宏:
WIFEXITED(status): 若為正常終??進程返回的狀態,則為真。(查看進程是否是正常退出) WEXITSTATUS(status): 若 WIFEXITED ?零,提取?進程退出碼。(查看進程的退出碼)
options:
默認為 0 ,表?阻塞等待
WNOHANG: 非阻塞等待,若 pid 指定的?進程沒有結束,則 waitpid() 函數返回 0 ,不予等待。若正常結束,則返回該?進程的 ID 。
status
status用于記錄子進程的退出信號和退出碼:
前7位為退出信號,第8位為core dump標志(不關心),9~16位為退出碼
若退出信號非0,表明進程異常
若退出信號為0,表明進程正常運行
若退出碼非0,表明運行結果錯誤
若退出碼非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;pd_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S printf("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;}
父進程阻塞等待成功
雖然返回的是257,但由于退出碼只取8位,所以返回1:
非阻塞輪詢等待:
void handler() {printf("臨時任務\n");sleep(1);}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(1);} else {int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG); //?阻塞式等待if (ret == 0) {printf("child is running\n");}handler();} while (ret == 0);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;}
可以看到父進程一邊執行自己的“臨時任務”,一邊詢問子進程是否結束,直到子進程結束,等待結束:
進程替換
我們之前雖然用fork創建了子進程,但是子進程和父進程執行的還是同樣的代碼,很多時候我們創建子進程就是為了去專門執行某一任務,這就需要用到進程替換,進程替換是通過特定的接?,把磁盤上另外的?個程序的代碼和數據加載到調?原先進程的地址空間中。
替換原理
?fork創建?進程后執?的是和?進程相同的程序(但有可能執?不同的代碼分?),?進程往往要調??種exec函數以執?另?個程序。當進程調??種exec函數時,該進程的??空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執?。調?exec并不創建新進程,所以調?exec前后該進程的id并未改變。
替換函數
#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[]);
這些函數如果調?成功則加載新的程序從啟動代碼開始執?,不再返回;如果調?出錯則返回-1?
這里把子進程替換為一個能夠讀入并執行命令的程序:
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{pid_t id=fork();if(id==0){ char** myargv=&argv[1]; execvp(myargv[0],myargv);}waitpid(id,NULL,0);return 0;
}
可以看到子進程可以收集并執行命令