程序員成長之旅 ——進程控制
代碼實現進程創建、等待、終止
進程創建
(1) fork函數創建進程
fork調用格式如下:
#include<unistd.h>
pid_t pid = fork();
返回值:子進程返回0,父進程返回子進程的pid,創建失敗返回-1
代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
printf("before: pid is %d\n",getpid());
if((pid = fork()) == -1)//fork()子進程返回0,父進程返回子進程pid
{
perror("fork");
exit(1);
}
printf("after:pid is %d,fork return is %d\n",getpid(),pid);
sleep(1);
return 0;
}
運行結果如下:
從上面可看出fork之前父進程獨立運行,fork之後一起運行,但是誰先運行這是不確定的。
(2) vfork創建進程
fork與vfork使用的區別:
- vfork創建的子進程與父進程共享地址空間,fork的子進程具有獨立的地址空間
- vfork保證子進程先運行,在它調用exec或exit之後父進程纔可能被調度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int flag = 100;
pid_t pid = vfork();
if(pid == -1)
{
perror("vfork()");
exit(1);
}
if(pid == 0)//child
{
flag = 200;
printf("child flag is %d\n",flag);
exit(0);
}
else//parent
{
printf("parent flag is %d\n",flag);
}
return 0;
}
可以看到:子進程改變了父進程的變量值,因爲子進程在父進程的地址空間中運行。
我們再來用fork驗證一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int flag = 100;
pid_t pid = fork();
if(pid == -1)
{
perror("fork()");
exit(1);
}
if(pid == 0)//child
{
flag = 200;
printf("child flag is %d\n",flag);
exit(0);
}
else//parent
{
printf("parent flag is %d\n",flag);
}
return 0;
}
發現父進程並沒有被修改,這是因爲子進程有自己獨立的地址空間。
進程終止
進程退出的三種場景
代碼運行完畢,結果對,正常退出
代碼運行完畢,結果不對,正常退出
代碼異常退出
進程常見的退出方法
- 正常終止(可以用echo $?查看進程退出碼)
從main函數返回;調用exit;_exit;- 異常退出
Ctrl + c 信號終止
終止進程場景演示如下
- 從main函數返回終止進程場景演示如下:
#include <stdio.h>
int main()
{
printf("hello lpf\n");
return 0;
}
運行該程序,可以查看程序退出碼爲0
將main函數中的0改爲9,再運行可以看出程序的退出碼則爲9
結論:
main函數中的return語句返回的即爲程序的退出碼,其它函數中的return語句返回值並不是
echo $?僅能查看最近一條命令的退出碼
退出碼0表示程序正常退出結果正確
- 調用_exit函數終止進程的場景演示如下
_exit函數使用方法如下:
#include<stdlib.h>
void _exit(int status);
參數:status定義了進程的中止狀態,父進程通過wait來獲取該值
注意:雖然status類型是int,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端查看退出是255
#include <stdio.h>
#include <unistd.h>
void test()
{
_exit(1);
}
int main()
{
printf("hello lpf\n");
test();
return 0;
}
結論:_exit函數會直接中止程序,無論該函數在主函數還是調用函數中
- 調用exit函數終止進程場景如下
#include<unistd.h>
void exit(int status);
exit函數的實現最後也會調用_exit函數,但在調用_exit函數之前,還會做其他工作( 比如刷新緩衝區 ),所以它和_exit函數不太一樣,它其實不是直接中止程序的。
#include <stdio.h>
#include <stdlib.h>
void test()
{
exit(1);
}
int main()
{
printf("hello lpf");
test();
return 0;
}
運行程序,會發現同樣的代碼,只是將_exit換成了exit。printf中的語句就被輸出來了
return是一種更常見的退出進程方法。執行return n等同於執行exit(n),因爲調用main的運行時函數會將main的返
回值當做 exit的參數。
進程等待
進程等待的方式一般有兩種:
阻塞式等待:子進程沒有退出時,父進程一直等待子進程的退出(常見於調用函數時)
非阻塞式等待:採取輪詢式訪問,條件不滿足時,父進程會返回去做其他事
(1)wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status)
返回值:成功返回被等待進程的pid,失敗返回-1
參數status:輸出型參數,獲取子進程退出狀態,不關心子進程的退出原因時,可設置爲NULL
代碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h> //exit
int main()
{
pid_t id = fork();
if(id == 0)//child
{
int count = 5;
while(count--)
{
printf("hello %d,pid is %d\n",count,getpid());
sleep(2);
}
}
else if(id > 0)//father
{
int st;
int ret = wait(&st);
if(ret>0 && (st&0X7F)==0)//正常退出(status參數的低8位未收到任何信號)
{
printf("child exit code:%d\n",(st>>8)&0XFF);//讓st的次低8位與8個比特位均爲1按位與,得到退出碼
}
else if(ret > 0)
{
printf("sig code:%d\n",st&0X7F);//讓st的低7位分別與1按位與,得到終止信號
}
}
else
{
perror("fork");
exit(1);
}
return 0;
}
當程序自己執行完後,程序正常退出,收到0號退出碼。
在另一個在另一個終端kill該進程,程序異常退出,收到15號SIGTERM程序結束信號。與SIGKILL不同的是該信號可以被阻塞和處理,通常用來要求程序正常退出。
下面測試的是,利用9號信號殺死該進程,程序異常退出,收到9號信號。
(2)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:
WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。
#include <stdio.h>
#include <unistd.h>//fork
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)//child
{
printf("child is run,pid is %d\n",getpid());
sleep(5);
exit(3);
}
else//parent
{
int status = 0;
pid_t ret = waitpid(-1,&status,0);//阻塞式等待
printf("I am waiting\n");
if(WIFEXITED(status) && ret == id)//WIFEXITED(status)爲真且正常返回才進入該條件語句
{
printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看進程的退出碼
}
else
{
printf("wait child failed,return \n");
return 2;
}
}
return 0;
}
讓程序運行,自己退出。因爲子進程用exit(3)退出,所以SEXITSTATUS(status)非零,提取出子進程的退出碼3。
當我在另一個終端下,kill掉該進程,子進程異常退出,所以WIFEXITED(status)不爲真,所以父進程中走else語句,輸出結果如下:
以下是waitpid函數的非阻塞式等待實現:
#include <stdio.h>
#include <unistd.h>//fork
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)//child
{
printf("child is run,pid is %d\n",getpid());
sleep(10);
exit(3);
}
else//parent
{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//非阻塞式等待
if(ret == 0)//無退出子進程可收集
{
printf("child is running\n");
}
sleep(1);
}while(ret == 0);//無退出子進程時,會一直詢問(輪詢式訪問),這裏1s詢問一次
if(WIFEXITED(status) && ret == id)//WIFEXITED(status)爲真且正常返回才進入該條件語句
{
printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看進程的退出碼
}
else
{
printf("wait child failed,return \n");
return 2;
}
}
return 0;
}
子進程正常退出結果
換終端將子進程異常終止,運行結果如下
(3)獲取子進程status
- wait和waitpid,均有一個status參數,該參數是一個輸出型參數,由操作系統填充;
- 若傳NULL,表示不關心子進程的退出狀態信息;
- 否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程
- status不能當做簡單整型看待,可以當做位圖來看待,具體如下圖(這裏只有低16位比特位):
由圖可知,當程序正常終止時,status的位圖的低8位不會收到任何信號,保持全0狀態,此時位圖的次低8位則保存着程序的退出碼。一旦低8位收到信號,則表示程序被信號殺死即異常終止。此時位圖的低8位保存程序的終止信號,次低8位不會用。不過需要注意的是,實際上用來保存收到終止信號的只有7個比特位,因爲低8位中有一個比特位是core dump標誌。
coredump標誌 : 核心轉儲標誌
在程序異常退出時,保存程序的堆棧信息,方便事後調試 (核心轉儲通常默認是關閉的: 安全隱患 和 空間佔用)
迷你自主shell的編寫
- 獲取命令行
- 解析命令行
- 建立一個子進程
- 替換子進程
- 父進程等待子進程退出
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/wait.h>
int main()
{
while(1){
printf("[lpf@localhost]# ");
fflush(stdout);
char buf[1024] = {0};
fgets(buf,1023,stdin);//從標準輸入獲取用戶敲擊的命令
buf[strlen(buf) - 1] = '\0';
printf("cmd:[%s]\n",buf);
//解析
int argc = 0;
char *argv[32] = {NULL};
char *ptr = buf;
while(*ptr != '\0'){
if(*ptr != ' ')
{
argv[argc++] = ptr;
while(*ptr != ' ' && *ptr != '\0'){
ptr++;
}
*ptr = '\0';
}
ptr++;
}
argv[argc] = NULL;
int i;
for(i = 0;i < argc;i++)
{
printf("[%s]\n",argv[i]);
}
int pid = fork();
if(pid == 0)
{
execvp(argv[0],argv);
exit(0);
}
waitpid(-1,NULL,0);
}
return 0;
}
封裝fork/wait等操作,編寫函數process_create ( pid_t* pid, void* func, void* arg ),func回調函數就是子進程執行的入口函數,arg是傳遞給func回調函數的參數
代碼:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
void func(char *arg[])//子進程要執行的函數
{
execvp(arg[1],arg+1);//因爲在輸入的時候,argv裏的第一個參數是可執行程序的名字,不是要執行的參數,所以要從argv裏的第二個參數
//比如要輸入:./process_create ls -l
//argv[0] --->./process
//argc[1] --->ls
//argc[2] --->-l
exit(1);
}
typedef void(*FUNC)(char**);
void process_creak(pid_t* pid,void* func,char* arg[])
{
*pid = fork();//創建一個進程
if(*pid == 0)
{
//子進程
FUNC funct = (FUNC)(func);
(*funct)(arg);
}else
if(pid > 0)
{
//父進程
wait(NULL);
}else
{
//創建進程失敗
perror("fork");
exit(1);
}
}
int main(int argc,char* argv[])
{
// ./process_creak 要執行的參數列表
// 比如: ./process_creak ls -l
if(argc == 1)
{
fprintf(stderr,"usage:%s 參數\n",argv[0]);
exit(1);
}
pid_t pid;
process_creak(&pid,func,argv);
}
popen/system這兩個函數和fork的區別.
1.popen函數
(1)函數原型
#include<stdio.h>
FILE *popen(const char *command,const char *type);
int pclose(FILE *stream);
創建一個管道用於進程間通信,並調用shell,因爲管道被定義爲單向的。所以 type 參數只能定義成只讀或者只寫, 結果流也相應的是隻讀或只寫。
(2)函數功能
popen()會調用fork()產生子進程,然後從子進程中調用/bin/sh -c來執行參數command的指令。這個進程必須由pclose關閉。
(3)參數
command :一個字符串指針, 指向一個以NULL爲結束符的字符串。 這個字符串包含一個shell命令, 這個命令被送到 /bin/sh 以-c參數 執行, 即由 shell 來執行。
type :一個指向以NULL爲結束符的字符串指針,用“r”代表讀,“w“代表寫。依照type值, popen( )會建立管道連到子進程的標準輸出設備或標準輸入設備,然後返回一個文件指針。隨後進程便可利用此文件指針來讀取子進程的輸出設備或是寫入到子進程的標準輸入設備中。
(4)返回值
成功則返回文件指針,否則返回NULL,錯誤原因存於errno中。
編寫代碼如下:(將replace.c文件的內容輸出到屏幕)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* file = NULL;
char buf[1024] = {0};
file = popen("cat replace.c","r");
if(file == NULL)
{
perror("popen");
exit(1);
}
while(fgets(buf,1024,file) != NULL)
{
fprintf(stdout,"%s",buf);
}
pclose(file);
return 0;
}
2.system函數
(1)函數原型
#include<stdlib.h>
int system(const char *command)
(2)函數功能
system( )會調用fork( )產生子進程,由子進程來調用/bin/sh-c string來執行參數string字符串所代表的命令。此命令執行完後隨即返回原調用的進程。在調用system( )期間SIGCHLD 信號會被暫時擱置,SIGINT和SIGQUIT 信號則會被忽略。調用/bin/sh來執行參數指定的命令,/bin/sh 一般是一個軟連接,指向某個具體的shell。
實際上system()函數執行了三步操作:
fork一個子進程;
在子進程中調用exec函數去執行command;
在父進程中調用wait去等待子進程結束。
(3)返回值
若 exec 執行成功,即 command 順利執行,則返回 command 通過 exit 或 return 的返回值。(注意 :command 順利執行不代表執行成功,當參數中存在文件時,不論這個文件存不存在,command 都順利執行) ;
若exec執行失敗,即command沒有順利執行,比如被信號中斷,或者command命令根本不存在, 返回 127 ;
若command 爲 NULL, 則 system 返回非 0 值;
對於fork失敗,system()函數返回-1。注意:判斷一個system函數調用shell是否正常結束的標誌是status != -1;(WIFEXITED(status))非零且WEXITSTATUS(status) == 0
代碼
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int status = 0;
status = system("ls -l");
if(status == -1)
{
perror("system");
exit(1);
}
if(WIFEXITED(status) != 0)//正常退出
{
if(WEXITSTATUS(status) == 0)//操作正確
{
printf("run success\n");
}
else
{
printf("run failed,exit code is %d\n",WEXITSTATUS(status));
}
}
else//異常退出
{
printf("sig code is %d\n",WEXITSTATUS(status));
}
}
3.區別
(1)system 在執行期間,調用進程會一直等待 shell 命令執行完成(waitpid),但是 popen 無需等待 shell 命令執行完成就返回。
(2)popen 函數執行完畢後必須調用 pclose 來對所創建的子進程進行回收,否則會造成殭屍進程的情況。
(3)open 沒有屏蔽 SIGCHLD ,如果在調用時屏蔽了 SIGCHLD ,且在 popen 和 pclose 之間調用進程又創建了其他子進程並調用進程註冊了 SIGCHLD 來處理子進程的回收工作,那麼這個回收工作會一直阻塞到 pclose 調用。