進程的相關知識是操作系統一個重要的模塊。在理解進程概念同時,還需了解如何控制進程。對於進程控制,通常分成1.進程創建 (fork函數) 2.進程等待(wait系列) 3.進程替換(exec系列) 4.進程退出(exit系列,return)四個方面。在大致熟悉進程控制之后,便可基於此 ,來模擬使用一個簡單的myshell,實現簡單的命令解析。
在此之前,先來簡單回顧進程控制一些基本方法
進程控制
(1)進程創建
進程創建一般通過fork來實現,(關於fork,前面有本人一點小小總結: 戳=> ,這里不再贅述)。
(2)進程退出
通常 進程從1. 從main返回 2. 調用exit 3. _exit 是正常終止(可以通過 echo $? 查看進程退出碼) 也可能異常退出。
大部分情況下進程會return退出,return n等同於執行exit(n),因為調用main的運行時函數會將main的返回值當做 exit的參數。而關於exit和_exit函數:
1._exit
#include <unistd.h> void _exit(int status); 參數:status 定義了進程的終止狀態,父進程通過wait來獲取該值
說明:雖然status是int,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端執行$?發現返回值是255。
2.exit
#include <unistd.h> void exit(int status);
exit最后也會調用exit, 但在調用 exit之前,還做了其他工作:
·1. 執行用戶通過 atexit或on_exit定義的清理函數。
·2. 關閉所有打開的流,所有的緩存數據均被寫⼊入
·3. 調用_exit
(3)進程等待
由於子進程退出,父進程如果不管不顧,就可能造成‘僵屍進程’的問題,進而造成內存泄
漏。
另外,進程一旦變成僵屍狀態,那就刀槍不入,就連kill -9 也無能為力,因為誰也沒有
辦法殺死一個已經死去的進程。
最后,父進程派給子進程的任務完成的如何,我們是需要知道。如,子進程運行完成,結果對還是不
對,或者是否正常退出。父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息。
關於等待方法,有如下wait系列:
1.wait
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status); 返回值: 成功返回被等待進程pid,失敗返回-1。 參數: 輸出型參數,獲取⼦子進程退出狀態,不關⼼心則可以設置成為NULL
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。
·如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,並且釋放資源,獲得子進程
退出信息。
·如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。
·如果不存在該子進程,則⽴立即出錯返回。
總結:父進程阻塞在wait,子進程退出后繼續執行
關於退出狀態獲取:
wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。
如果傳遞NULL,表示不關心子進程的退出狀態信息。否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位)
(4)進程替換
替換原理:
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec並不創建新進程,所以調用exec前后該進程的id並未改變。
替換函數exec系列,共6種,
char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); // 帶p的,可以使⽤用環境變量PATH,⽆無需寫全路徑 execlp("ps", "ps", "-ef", NULL); // 帶e的,需要⾃自⼰己組裝環境變量 execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); // 帶p的,可以使⽤用環境變量PATH,⽆無需寫全路徑 execvp("ps", argv); // 帶e的,需要⾃自⼰己組裝環境變量 execve("/bin/ps", argv, envp);
參數解釋: ·這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。 ·如果調用出錯則返回-1 ·所以exec函數只有出錯的返回值而沒有成功的返回值。
模擬實現進程創建函數process_create
基於進程控制的理解,我們可以來簡單模擬實現一個進程的創建函數process_create。
process_create(pid_t* pid, void* func, void* arg) 參數: func回調函數,就是子進程執行的入口函數 arg是傳遞給func回調函數的參數.
該函數將fork和wait函數封裝起來,然后用創建出來的子進程去回調func函數,完成func函數功能。
/************************************************************************* > File Name: pro_create.c > Author: tp > Mail: > Created Time: Wed 13 Jun 2018 10:04:21 PM CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> typedef struct Stu{ char a[32]; int age; }Stu; typedef void (*FUNC_NOARG)(); typedef int (*FUNC_ARG)(void*); int print_info(void* arg) { Stu* p= (Stu *)arg; printf( "%s : %d\n", p->a, p->age); return 0; } void say_hi( ) { printf( "hello\n"); } void process_create(pid_t *pid, void *func, void *arg) { pid_t id = fork( ); if( id < 0) perror(" fork"),exit( 1); else if( id == 0) { //child if(arg != NULL) //傳入參數不為NULL { FUNC_ARG callback = (FUNC_ARG)func; int ret = callback(arg); if( ret != 0) //模擬判斷回調是否成功 (wait) { printf("執行回調函數有錯誤\n"); exit(1); } } else { FUNC_NOARG callback = (FUNC_NOARG)func; callback(); } exit(0); } else //father { *pid = wait(NULL); } } int main( ) { pid_t id; int* p = (int* )malloc(sizeof(int)); *p = 10; Stu s={"張全蛋", 30}; process_create(&id, ( void*)print_info, &s); printf("pid=%d\n", (int)id); process_create(&id, ( void*)say_hi, NULL); printf("pid=%d\n", (int)id); return 0; }
總結:通過上面程序可以感受函數與進程之間的相似性。 一個C程序有很多函數組成。一個函數可以調用另外一個函數,同時傳遞給它一些參數。被調用的函數執行一定的操作,然后返回一個值。每個函數都有他的局部變量,不同的函數通過call/return系統進行通信。exec/exit就像call/return一樣。這種通過參數和返回值在擁有私有數據的函數間通信的模式是結構化程序設計的基礎。系統是很鼓勵將這種應用於程序之內的模式擴展到程序之間去的。
一個C程序可以fork/exec另一個程序,並傳給它一些參數。這個被調用的程序執行一定的操作,然后通過exit(n)來返回值。調用它的進程可以通過wait(&ret)來獲取exit的返回值。
模擬實現——簡單shell
完成大致思路: shell建立一個新的進程,然后在那個進程中運行一個程序(如完成ls操作)然后等待那個進程執行結束。然后shell便可讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 並等待這個進程結束。所以要寫一個shell,需要循環以下過程:
1. 獲取命令行 2. 解析命令行 3. 建立一個子進程(fork) 4. 替換子進程(execvp) 5. 父進程等待子進程退出(wait)
①完成基本的命令
如簡單的ls -l, mkdir ..等。由於是使用fork出來的一個子進程,再通過exec系列函數來單純將進程地址空間替換來執行完成的命令,這樣的方式不能直接解析完成 > 、| 和 cd 、su .., 等一些帶系統權限的命令。這里我去添加了它的重定向功能,其它功能,例如 “|”管道命令操作, 可以基於管道操作pipe函數創建出一個管道來實現進程通信。若感興趣,讀者可以再自行添加。也歡迎來一起討論!!
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <ctype.h> #include <sys/wait.h> void do_exe(char* buf, char* argv[]) //加載程序 { pid_t pid = fork(); if(pid == 0)//子進程 { execvp(buf, argv); perror("fork"); //執行到此,說明execvp未執行成功,fork失敗 exit(1); } wait(NULL); //等待子進程死亡, 回收 } //對命令進行解析 void do_parse(char* buf){ char* argv[8] = {}; //將buf中的命令以‘ ’為分界存入指針數組中 int argc = 0; int status = 0; //一個新的字符串 for(int i =0; buf[i] != 0; ++i){ if(status ==0 && !isspace(buf[i])){ argv[argc++] = buf +i; status = 1; } else if(isspace(buf[i])){ status = 0; buf[i] = 0; } } argv[argc] = NULL; do_exe(buf, argv); } int main(void) { // char* argv[] = {"ls", "-lah", NULL}; // execvp("ls", argv);//替換地址空間,實則將原進程的代碼段,數據段進行替換,並未創建新的進程出來。 char buf[1024] = {}; while(1) { printf("my shell#"); memset(buf, 0x00, sizeof(buf)); //[^\n]匹配除\n以外的所有字符,*用於抑制轉換 //scanf成功返回輸入的項數 while(scanf("%[^\n]%*c", buf) == 0) { //為0表示只輸入了換行 printf("my shell#"); while(getchar() != '\n'); //到獲得了一個‘\n' } do_parse(buf); } return 0; }
②添加重定向功能
對於其中的重定向功能可以通過文件操作和dup函數來模擬實現。
改良版:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <ctype.h> #include <sys/wait.h> void do_exe(char* buf, char* argv[]) //加載程序 { pid_t pid = fork(); if(pid == 0)//子進程 { //尋找重定向標志 > for( int i =0; argv[i] != NULL; ++i) { if(strcmp(argv[i], ">") == 0) { if(argv[i+1] == NULL) //> 后面未帶參數 perror("command '>'[option]?"),exit( 1); argv[i] = NULL; //根據解析命令參數,創建/打開一文件 int fd =open(argv[i+1], O_RDWR|O_CREAT|O_TRUNC, 0664); if(fd == -1)perror("open"),exit( 1); //重定向操作 dup2(fd, 1); //dup2(oldfd, newfd); close(fd); } } execvp(buf, argv); perror("fork"); //執行到此,說明execvp未執行成功,fork失敗 exit(1); } wait(NULL); //等待子進程死亡, 回收 } //對命令進行解析 void do_parse(char* buf){ char* argv[8] = {}; //將buf中的命令以‘ ’為分界存入指針數組中 int argc = 0; int status = 0; //一個新的字符串 for(int i =0; buf[i] != 0; ++i){ if(status ==0 && !isspace(buf[i])){ argv[argc++] = buf +i; status = 1; } else if(isspace(buf[i])){ status = 0; buf[i] = 0; } } argv[argc] = NULL; do_exe(buf, argv); } int main(void) { // char* argv[] = {"ls", "-lah", NULL}; // execvp("ls", argv);//替換地址空間,實則將原進程的代碼段,數據段進行替換,並未創建新的進程出來。 char buf[1024] = {}; while(1) { printf("my shell#"); memset(buf, 0x00, sizeof(buf)); //[^\n]匹配除\n以外的所有字符,*用於抑制轉換 //scanf成功返回輸入的項數 while(scanf("%[^\n]%*c", buf) == 0) { //為0表示只輸入了換行 printf("my shell#"); while(getchar() != '\n'); //到獲得了一個‘\n' } do_parse(buf); } return 0; }
驗證: