前言:整個操作系統都在圍繞進程這一概念具體展開,所以對於進程的控制就顯得十分重要,這篇文章主要講述以下幾點:
1. 進程創建
2. 進程退出
3. 進程等待
4. 進程程序替換
進程創建
在操作系統中,對於父子進程的概念非常重要,必要linux自帶的bash,對於你在命令行輸入的一些指令,它是不會自己去處理你這些請求的,而是通過創建子進程去處理,它只需要知道子進程返回的消息就好了。
爲什麼要基於這樣的父子進程關係呢?試想一下,我們和操作系統打交道是通過shell內建的bash,如果用戶的什麼請求都經由bash去親自執行,那麼一個bash也不夠用啊,其次,如果一旦請求中出了問題,那麼bash掛掉的話,誰來幫我們向操作系統傳達我們的請求呢?
基於上面提出的種種問題,就引出進程創建子進程的必要性了。
進程的創建方式:
- pid_t fork(void);
- pid_t vfork(void);
認識fork函數
pid_t fork(void)
返回值:pid_t其實就是一個整型,typedef成pid_t只是爲了一眼看上去知道這是一個進程號
子進程中返回值爲0.
父進程中返回操作系統給子進程分配的pid號。
fork失敗返回-1
進程調用fork,當控制權限轉移到內核中的fork代碼後:
- 分配新的內存塊和數據結構給子進程
- 將父進程的大多數數據結構拷貝至子進程
- 添加子進程到系統進程列表
- fork返回,操作系統進行進程調度
當一個進程fork出一個子進程後,就有兩個二進制代碼相同的進程,並且運行到相同的地方,但每個進程都將開始執行自己的代碼。
如下:
int main()
{
printf("Before fork: pid is:%d\n",getpid())//getpid函數爲獲取進程pid
pid_t id = fork();
if(id < 0)
{
perror("fork error");
exit(1);
}
printf("After fork:pid is %d\n",getpid());
return 0;
}
這裏需要注意的是,先執行子進程還是父進程完全取決於操作系統的進程調度器決定。
fork失敗的原因:
- 系統中的進程數達到了上限
- 系統的內存不足
- 系統不支持,如Windows不支持fork
認識vfork函數
對於vfork來說,其他的都是fork函數用法一樣,只要記住最重要的兩個特性就好。
- 子進程一定先於父進程執行。
- 子進程調用exec或者exit之後父進程才能執行
進程終止
進程退出的場景:
- 代碼運行完,結果正確
- 代碼運行完,結果不正確
- 代碼異常終止
常見進程退出:
正常終止:
1. main函數返回
2. 調用exit函數
3. 調用_exit函數
exit函數和_exit函數的區別:
- exit會進行清理工作,如刷新緩衝區等,而exit直接退出程序
- _exit是系統調用,exit最終也會調用_exit。
異常終止
CTRL+C/kill -9
具體的程序退出部分,可見博客尾部的鏈接。
進程等待
進程等待是非常重要的,如果父進程對子進程不管不顧的話,那麼可能會產生殭屍進程,從而造成內存泄漏。
並且作爲父進程,創建子進程是讓它完成一些任務的,總要知道它返回的結果,完成的怎麼樣。
wait函數
pid_t wait(int *status)//阻塞式等待
返回值:成功返回等待進程的pid,失敗返回-1
參數:輸出型參數,獲取子進程的退出狀態,不關心可以爲NULL,該參數由操作系統初始化
status
所以查看的話,先查看低七位是否爲0 ,如果是0代表程序正常退出,可以查看高八位具體的退出碼。
如果低七爲不爲0,則代表信號終止,高八位就沒有意義了,可以查看低七位的具體信號。
core dump是指進程終止時所記錄的現場。
以下面的代碼爲實例:
#include<stdio.h>
#include<wait.h>
int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
int ret = wait(&status);
if(ret > 0 && (status&0x7f) == 0)
{
//success
printf("child exit code:%d\n",(status>>8));
}
else
{
//signal exit
printf("signal code:%d\n",(status>>8)&0xff);
}
}
else if(id == 0)
{
//child
sleep(3);
exit(5);//子進程的退出碼
}
else
{
perror("fork");
}
return 0;
}
當子進程正常退出時,會返回退出碼。
運行結果:
接下來,我們直接kill -9掉該進程,結果應該返回9號信號,看如下運行結果:
我的Ubuntu是最新的,本應該返回9,但是操作系統將9這個數字,對應成第九個信號的名稱顯示出來。
下面是Linux下的信號:
waitpid函數
pid_t waitpid(pid_t pid,int *status,int option)//如果最後一個參數設置了WNOHANG就是非阻塞式等待
返回值:
- 正常返回收集子進程的進程ID
- 如果設置了選項WNOHANG,而調用waitpid發現沒有已退出的子進程可以回收,則返回0,就是輪詢等待的意思
- 如果調用中出錯,返回-1,errno會被設置成相應的值以指示錯誤所在。
參數:
pid:
- pid = -1,等待任意一個子進程,與wait等效
- pid > 0,等待其進程ID與pid相等的進程
status:
- WIFEXITED(status):若爲正常終止子進程返回的狀態,則爲真(相當於上面的檢查低7位爲是否爲0)
- WEXITSTATUS(status):若WIFIXITED非零,提取子進程退出碼(相當於上面的查看高8位的退出碼)
option:
WNOHANG:若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待,若正常結束,則返回該進程的ID
需要注意的是:
- 如果子進程已經結束,調用wait/waitpid時,函數會直接返回,並且釋放資源,獲得子進程退出信息。(對應的場景就是子進程退出時,父進程在沉睡,如果這時父進程不予以處理子進程則會產生殭屍進程)
- 如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞
如果不存在該子進程,則立即出錯返回
如下代碼示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//no-blocking
if(ret == 0)
{
printf("child is running\n");
}
sleep(1);
}
while(ret == 0);
if(WIFEXITED(status) && ret == id)
{
printf("wait child 3s success,child return code is:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed,return\n");
return 1;
}
}
else if(id == 0)
{
//child
printf("child is run ,pid is:%d\n",getpid());
sleep(3);
exit(1);
}
else
{
printf("%s fork error\n",__FUNCTION__);
return 1;
}
return 0;
}
進程程序替換
替換原理
用fork創建子進程後執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程一般要調用一種exec函數以執行另一個程序。當進程調用exec函數族時,該進程的用戶空間代碼和數據完全被新的程序替換,從新程序的啓動例程開始執行,調用exec並不創建新進程,所以調用exec前後該進程的pid並未改變。
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[]);
函數解釋
- l(list):表示參數採用列表
- v(vector):表示參數採用數組
- p(path):帶p的函數會自動搜索環境變量PATH
- e(env):表示自己維護環境變量
需要特別注意的時,
- 如果exec函數族調用成功,則從新程序的啓動代碼開始,所以沒有返回值
- 如果調用失敗,則返回-1
代碼示例:
#include<stdio.h>
#include<unistd.h>
int main()
{
const *const argv[] = {"ls","-al",NULL};
char *const envp[] = {"PATH=/bin:/usr/bin",NULL};//環境變量
execl("/bin/ls","ls","-al".NULL);
//帶p的函數,不必再給出全路徑
execlp("ls","ls","-al",NULL);
//帶e的,需要自己配置環境變量
execle("ls","ls","-al".NULL,envp);
execv("/bin/ls",argv);
//帶p的,不需要給出全路徑
execvp("ls",argv);
//帶e的,需要自己配置環境變量
execve("/bin/ls",argv,envp);
exit(0);
}
雖然exec函數族有六個函數,但是隻有execve函數是系統調用,其他幾個函數最終都會調用execve函數。
基於程序替換,可以實現一個簡單的shell。
exit函數詳解http://blog.csdn.net/qq_36528114/article/details/71321390
實現一個簡單的shell:http://blog.csdn.net/qq_36528114/article/details/72582588