在之前的Linux進程概念中提到了一些關於進程的創建。進程的創建通過fork函數創建子進程。
進程終止
進程終止顧名思義,進程結束。但是結束有各種可能,可能成功,可能失敗,也可能異常。
進程常見退出方法
正常退出
1、從main返回
2、調用exit
3、_exit
異常退出
ctrl + c
exit函數
#include <unistd.h>
void _exit(int status);
參數:status 定義了進程的終止狀態,父進程通過wait來獲取該值
說明:雖然status是int,但是僅有低8位可以被父進程所用。
所以_exit(-1)時,在終端執行$?發現返回值 是255。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(){
int pid = fork();
if(pid < 0){
perror("fork error");
printf("fork error:%s\n",strerror(errno));
}
int i = 0;
for( ;i < 256; i++){
printf("errno:[%s]\n",strerror(i));
}
printf("hello bit");
sleep(1);
_exit(257);
}
在這之中調用的是_exit函數,關於這兩個函數,並沒有什麼太大的區別,exit()
就是對-exit
進行的一個分裝。通過man手冊可以詳細瞭解。
_ exit()的退出是簡單粗暴的,exit()還做了一些其他的工作。可自行查看
他們倆的共同點就是都會關閉文件描述符,都會清空內存,但是exit還會額外地清空輸入輸出流緩存,移除臨時創建的文件,調用註冊好的出口函數等等。
進程等待
爲什麼要進行進程等待?
之前寫過殭屍進程的危害,一個殭屍進程對內存的資源泄漏是積少成多的。一個兩個倒沒啥,但是如果多了將無法再創建新的進程,並且就連最強大的殺死進程kill -9
也無法將其殺死,所以進程等待的必要性是很重要的。當父進程創建一個子進程時,通過進程等待,回收子進程資源,獲取子進程的退出信息,而不讓其保存在操作系統中。
總的來說:因爲父進程不知道子進程什麼時候退出,因此只能在子進程進程創建之後調用wait(),進行進程等待。因爲調用wait就是一直在等待子進程的退出。
進程等待的方法
wait()接口是一個阻塞函數,功能是等待子進程退出,如果子進程沒有退出,一直等待到有子進程退出
#include <type.h>
#include <wait.h>
pid_t wait(int *status);
函數的返回值:成功返回等待的pid,失敗返回-1
參數:輸出型參數,獲取子進程的狀態,不關心則可以設置爲NULL
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。
第二種wait還是一種封裝方式。只不過需要討論以下status的參數獲取。
status是輸出型參數,由操作系統填充。status的使用只在低16位上
從圖片可以看出status低八位中的低七位保存異常的情況,高八位保存的是我們的數據
/*進程等待-避免產生建時進程demo*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
sleep(5);
exit(255);//此時返回的是1
}
int status;
while (waitpid(pid, &status, WNOHANG) == 0) {
printf("no exit~~~smoking~~\n");
sleep(1);
}
//低7位爲0則正常退出
if (!(status & 0x7f)) {
printf("child exit code:%d\n", (status >> 8) & 0xff);
}
//man 2 wait 自行查看這個WIFEXITED的參數返回設置
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
while(1) {
printf("打麻將~~~\n");
sleep(1);
}
return 0;
}
但是waitpid第三個參數可以將waitpid設置爲非阻塞,沒有子進程退出則立即報錯返回0.
阻塞:爲了完成某個功能發起調用,如果當前不具備完成條件,一直等待,直到完成後返回
非阻塞:爲了完成某個功能發起調用,如果當前不具備完成條件,直接報錯返回
當自己進程沒有退出正在執行sleep函數時,waitpid()一直在運行等待子進程的退出。退出成功後,輸出此時status此時右移到低八位的數值-----就是exit()的返回值。然後判斷子進程是否正常退出,如果非正常退出將有退出信號。
進程替換
父進程創建了子進程,子進程如果總是做着和父進程相同的工作,那麼子進程沒有太大的意義。所以這時候可以調用exec函數來執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啓動例程開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。
也可以這樣認爲:替換的是程序所運行的程序,將另一段程序加載到內存中,通過頁表將原先進程的映射關係,重新建立到新程序在內存中的地址,相當於替換了進程所運行的程序以及所要處理的數據,因此,替換了代碼段,重新初始化數據段。
替換進程正在運行的程序(替換代碼段以及運行數據,更行頁表,從main函數重新運行)
替換函數
通過exec函數羣來實現
execve是系統調用接口,以下是函數羣
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 execvpe(const char *file, char *const argv[], char *const envp[]);
execl(ls,ls,-a,-l,-i,null) 參數是平鋪賦予
execv(/bin/ls,argv) 參數是通過參數數組賦予argv[0]=ls,argv[1]=-a,argv[2]=-l…argv[4]=NULL
execl與execv區別:參數的賦予是以指針數組賦予還是以不定參數形式賦予
最後必須將null寫入!!!
最後必須將null寫入!!!
最後必須將null寫入!!!
有無p的區別:是否自動到PATH所制定的路徑下找程序文件
有無e的區別:是否自定義環境變量的區別
如果帶e的話將當前進程的環境變量也將會發生改變。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
printf("hello world\n");
execl("./env","env","-l",NULL);//誰調用就對誰進行程序替換
//如果 execl() 執行成功,下面執行不到,因爲當前進程已經被執行的 ./env 替換了
//所以執行之後一直都是第一個printf循環執行
char *env[32];
env[0] = "MYENV=10000";
env[1] = NULL;
extern char **environ;//保存環境變量
execle("./env","env","-l",NULL,environ);
printf("hello world~~\n");
return 0;
}
exec 只是用另一個新程序替換了當前進程的正文、數據、堆和棧段(進程替換)
不斷的強調這句話因爲,exec的替換的理解:我們可以想象爲我們在當前內存空間中執行的程序替換到一個新的內存空間中。程序將在新的空間中執行
這裏的理解感覺有些繁瑣,但是進程替換之後,執行成功之後不會返回,而且exec函數族下面的代碼執行不到,只有調用失敗了,纔會返回-1,失敗後從原程序的調用點接着往下執行。
從物理角度來理解:將進程的虛擬地址空間所映射在物理內存的區域進行改變,改編成另一個程序中在內存中的位置,更新頁表信息,重新初始化虛擬地址空間,爲了讓進程運行另一個程序
自主minishell的實現
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <unistd.h>
#include <ctype.h>
int main(){
while(1){
printf("[liuyucheng@localhost]$ ");
fflush(stdout);
char buf[1024] = {0};
if(scanf("%[^\n]%*c", buf) != 1){
getchar();
continue;
}
//取出空白字符,獲取程序名稱和參數
char *argv[32];//將自己定義的緩衝區的字符放入該數組中
int argc = 0;
char *ptr = buf;
while(*ptr != '\0'){
//若參數c爲空格字符,則返回TRUE,否則返回NULL(0)。
if(!isspace(*ptr)){
argv[argc++] = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
}else{
*ptr = '\0';
ptr++;
}
}
argv[argc] = NULL;
int pid = fork();
//創建子進程
if(pid < 0){
exit(-1);//退出返回-1
}
else if(pid == 0){//如果子進程創建成功
execvp(argv[0], argv);//程序替換,此時argv保存了指令的字符串,替換到argv[0]中,這樣execvp自動在當前PATH路徑下尋找指令,例如ls,ll
exit(0);
}
wait(NULL);//等待子進程退出
}
return 0;
}
如圖則爲程序運行。不過cd指令不行。不能跳到另一個文件夾中。
將所學的幾個進程都用到了,很有幫助。