linux c語言 fork() 和 exec 函數的簡介和用法
假如我們在編寫1個c程序時想調用1個shell腳本或者執行1段 bash shell命令, 應該如何實現呢?
其實在<stdlib.h> 這個頭文件中包含了1個調用shell命令或者腳本的函數 system();直接把 shell命令作為參數傳入 system函數就可以了, 的確很方便. 關於system 有一段這樣的介紹: system 執行時內部會自動啟用fork() 新建1個進程, 效率沒有直接使用fork() 和 exec函數高.
那么這篇文章其實就是介紹一下fork() 和 exec函數的用法, 以及如何使用它們來替代system函數.
1. fork() 函數
1.1 fork() 函數的作用
一般來講, 我們編寫1個普通的c程序, 運行這個程序直到程序結束, 系統只會分配1個pid給這個程序, 也就就說, 系統里只會有一條關於這個程序的進程.
但是執行了fork() 這個函數就不同了.
fork 這個英文單詞在英文里是"分叉"意思, fork() 這個函數作用也很符合這個意思. 它的作用是復制當前進程(包括進程在內存里的堆棧數據)為1個新的鏡像. 然后這個新的鏡像和舊的進程同時執行下去. 相當於本來1個進程, 遇到fork() 函數后就分叉成兩個進程同時執行了. 而且這兩個進程是互不影響
參考下面這個小程序:
- int fork_3(){
- printf("it's the main process step 1!!\n\n");
- fork();
- printf("step2 after fork() !!\n\n");
- int i; scanf("%d",&i); //prevent exiting
- return 0;
- }
在這個函數里, 共有兩條printf語句, 但是執行執行時則打出了3行信息. 如下圖:
為什么呢, 因為fork()函數將這個程序分叉了啊, 見下面的圖解:

可以見到程序在fork()函數執行時都只有1條主進程, 所以 step 1 會被打印輸出1次.
執行 fork()函數后, 程序分叉成為了兩個進程, 1個是原來的主進程, 另1個是新的子進程, 它們都會執行fork() 函數后面的代碼, 所以 step2 會被 兩條進程分別打印輸出各一次, 屏幕上就總共3條printf 語句了!
可以見到這個函數最后面我用了 scanf()函數來防止程序退出, 這時查看系統的進程, 就會發現兩個相同名字的進程:
如上圖, pid 8808 那個就是主進程了, 而 pid 8809那個就是子進程啊, 因為它的parent pid是 8808啊!
需要注意的是, 假如沒有做特殊處理, 子進程會一直存在, 即使fork_3()函數被調用完成, 子進程會和主程序一樣,返回調用fork_3() 函數的上一級函數繼續執行, 直到整個程序退出.
可以看出, 假如fork_3() 被執行2次, 主程序就會分叉兩次, 最終變成4個進程, 是不是有點危險. 所以上面所謂的特殊處理很重要啊!
1.2 區別分主程序和子程序.
實際應用中, 單純讓程序分叉意義不大, 我們新增一個子程序, 很可能是為了讓子進程單獨執行一段代碼. 實現與主進程不同的功能.
要實現上面所說的功能, 實際上就是讓子進程和主進程執行不同的代碼啊.
所以fork() 實際上有返回值, 而且在兩條進程中的返回值是不同的, 在主進程里 fork()函數會返回主進程的pid, 而在子進程里會返回0! 所以我們可以根據fork() 的返回值來判斷進程到底是哪個進程, 就可以利用if 語句來執行不同的代碼了!
如下面這個小程序fork_1():
- int fork_1(){
- int childpid;
- int i;
- if (fork() == 0){
- //child process
- for (i=1; i<=8; i++){
- printf("This is child process\n");
- }
- }else{
- //parent process
- for(i=1; i<=8; i++){
- printf("This is parent process\n");
- }
- }
- printf("step2 after fork() !!\n\n");
- }
我對fork() 函數的返回值進行了判斷, 如果 返回值是0, 我就讓認為它是子進程, 否則是主程序. 那么我就可以讓這兩條進程輸出不同的信息了.
輸出信息如下圖:

可以見到 子程序和主程序分別輸出了8條不同的信息, 但是它們並不是規則交替輸出的, 因為它們兩條進程是互相平行影響的, 誰的手快就在屏幕上先輸出, 每次運行的結果都有可能不同哦.
下面是圖解:

由圖解知兩條進程都對fork()返回值執行判斷, 在if 判斷語句中分別執行各自的代碼. 但是if判斷完成后, 還是會回各自執行接下來的代碼. 所以 step2 還是輸出了2次.
1.4 使用exit() 函數令子進程在if 判斷內結束.
參考上面的函數, 雖然使用if 對 fork() 的返回值進行判斷, 實現了子進程和 主進程在if判斷的范圍內執行了不同的代碼, 但是就如上面的流程圖, 一旦if執行完成, 他們還是會各自執行后面的代碼.
通常這不是我們期望的, 我們更多時會希望子進程執行一段特別的代碼后就讓他結束, 后面的代碼讓主程序執行就行了.
這個實現起來很簡單, 在子程序的if 條件內最后加上exit() 函數就ok了.
將上面的fork_1()函數修改一下, 加上exit語句:
- int fork_1(){
- int childpid;
- int i;
- if (fork() == 0){
- //child process
- for (i=1; i<=8; i++){
- printf("This is child process\n");
- }
- exit(0);
- }else{
- //parent process
- for(i=1; i<=8; i++){
- printf("This is parent process\n");
- }
- }
- printf("step2 after fork() !!\n\n");
- }
再看看輸出:

可以見到, step2只輸出1次了, 這是因為子程序在 if條件內結束了啊, 一旦 if 判斷成, 就只剩下1個主進程執行下面的代碼了, 這正是我們想要的!
注意: exit() 函數在 stdlib.h 頭文件內
流程圖:

1.4 使用wait() 函數主程序等子程序執行完成(退出)后再執行.
由上面例子得知, 主程序和子程序的執行次序是隨機的, 但是實際情況下, 通常我們希望子進程執行后, 才繼續執行主進程.
例如對於上面的fork_1()函數, 我想先輸出子進程的8個 "This is child process" 然后再輸出 8個 主進程"This is parent process", 改如何做?
wait()函數就提供了這個功能, 在if 條件內的 主進程呢部分內 加上wait() 函數, 就可以讓主進程執行fork()函數時先hold 住, 等子進程退出后再執行, 通常會配合子進程的exit()函數一同使用.
我將fork_1()函數修改一下, 添加了wait()語句:
- int fork_1(){
- int childpid;
- int i;
- if (fork() == 0){
- //child process
- for (i=1; i<=8; i++){
- printf("This is child process\n");
- }
- exit(0);
- }else{
- //parent process
- wait();
- for(i=1; i<=8; i++){
- printf("This is parent process\n");
- }
- }
- printf("step2 after fork() !!\n\n");
- }
輸出:

見到這時的屏幕輸出就很有規律了!
其實wait() 函數還有1個功能, 就是可以接收1個 pid_t(在unistd.h內,其實就是Int啦) 指針類型參數, 給這個參數賦上子進程退出前的系統pid值
流程圖:

2. exec 函數組
需要注意的是exec並不是1個函數, 其實它只是一組函數的統稱, 它包括下面6個函數:
- #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[]);
可以見到這6個函數名字不同, 而且他們用於接受的參數也不同.
實際上他們的功能都是差不多的, 因為要用於接受不同的參數所以要用不同的名字區分它們, 畢竟c語言沒有函數重載的功能嘛..
但是實際上它們的命名是有規律的:
exec[l or v][p][e]
exec函數里的參數可以分成3個部分, 執行文件部分, 命令參數部分, 環境變量部分.
例如我要執行1個命令 ls -l /home/gateman
執行文件部分就是 "/usr/bin/ls"
命令參賽部分就是 "ls","-l","/home/gateman",NULL 見到是以ls開頭 每1個空格都必須分開成2個部分, 而且以NULL結尾的啊.
環境變量部分, 這是1個數組,最后的元素必須是NULL 例如 char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};
好了說下命名規則:
e后續, 參數必須帶環境變量部分, 環境變零部分參數會成為執行exec函數期間的環境變量, 比較少用
l 后續, 命令參數部分必須以"," 相隔, 最后1個命令參數必須是NULL
v 后續, 命令參數部分必須是1個以NULL結尾的字符串指針數組的頭部指針. 例如char * pstr就是1個字符串的指針, char * pstr[] 就是數組了, 分別指向各個字符串.
p后續, 執行文件部分可以不帶路徑, exec函數會在$PATH中找
還有1個注意的是, exec函數會取代執行它的進程, 也就是說, 一旦exec函數執行成功, 它就不會返回了, 進程結束. 但是如果exec函數執行失敗, 它會返回失敗的信息, 而且進程繼續執行后面的代碼!
通常exec會放在fork() 函數的子進程部分, 來替代子進程執行啦, 執行成功后子程序就會消失, 但是執行失敗的話, 必須用exit()函數來讓子進程退出!
下面是各個例子:
2.1 execv 函數
- int childpid;
- int i;
- if (fork() == 0){
- //child process
- char * execv_str[] = {"echo", "executed by execv",NULL};
- if (execv("/usr/bin/echo",execv_str) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execv done\n\n");
- }
2.2 execvp 函數
- if (fork() == 0){
- //child process
- char * execvp_str[] = {"echo", "executed by execvp",">>", "~/abc.txt",NULL};
- if (execvp("echo",execvp_str) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execvp done\n\n");
- }
2.3 execve 函數
- if (fork() == 0){
- //child process
- char * execve_str[] = {"env",NULL};
- char * env[] = {"PATH=/tmp", "USER=lei", "STATUS=testing", NULL};
- if (execve("/usr/bin/env",execve_str,env) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execve done\n\n");
- }
2.4 execl 函數
- if (fork() == 0){
- //child process
- if (execl("/usr/bin/echo","echo","executed by execl" ,NULL) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execv done\n\n");
- }
2.5 execlp 函數
- if (fork() == 0){
- //child process
- if (execlp("echo","echo","executed by execlp" ,NULL) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execlp done\n\n");
- }
2.6 execle 函數
- if (fork() == 0){
- //child process
- char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};
- if (execle("/usr/bin/env","env",NULL,env) <0){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait(&childpid);
- printf("execle done\n\n");
- }
輸出:

3. fork() 和exec 函數與system()函數比較
見到上面execvp函數的輸出. 你會發現 exec函數只是系統調用, 它是不支持管線處理的
而system()函數是支持的. 他的內部會自動fork() 1個子進程,但是效率沒有fork() 和 exec配合使用好.
但是exec 支持執行腳本. 所以不需要管線處理的命令或者腳本可以利用fork() 和 exec函數來執行.
4. 利用 fwrite() ,fork() 和exec 函數 替代system()函數.
上面講過了, 雖然exec函數不支持管線, 而且命令參數復雜, 但是它支持執行腳本啊, 所以我們可以使用fwrite將 有管線處理的命令寫入1個腳本中, 然后利用exec函數來執行這個腳本.
下面會編寫1個base_exec(char *) 函數, 接收1個字符串參數, 然后執行它.
這里只會大概寫出這個函數的邏輯步驟:
1. 利用getuid函數獲得當前的pid, 然后利用pid獲得當前唯一的文件名, 避免因為相同程序同時執行發生沖突!
2. 利用fwrite函數在 /tmp/下面 建立1個上面文件名的腳本文件. 因為/tmp/ 任何用戶都可以讀寫啊
3. 把命令參數寫入腳本
4. 利用fork() 和 exec() 執行這個腳本
5. 有需要的話當exec執行完, 記錄日志.
下面就是i代碼:
頭文件:
base_exec.h
- #ifndef __BASE_EXEC_H_
- #define __BASE_EXEC_H_
- int base_exec(char *) ;
- #endif /* BASE_EXEC_H_ */
源文件:
base_exec.c
- #include "base_exec.h"
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <time.h>
- #define LOGFILE "/home/gateman/logs/c_exec.log"
- int base_exec(char * pcmd){
- FILE * pf;
- pid_t pid = getpid();
- char pfilename[20];
- sprintf(pfilename, "/tmp/base_exec%d.sh",pid);
- pf=fopen(pfilename,"w"); //w is overwrite, a is add
- if (NULL == pf){
- printf("fail to open the file base_exec.sh!!!\n");
- return -1;
- }
- fwrite("#!/bin/bash\n", 12, 1, pf);
- fwrite(pcmd, strlen(pcmd),1, pf);
- fwrite("\n", 1,1, pf);
- fclose(pf);
- if (fork() ==0 ){
- //child processj
- char * execv_str[] = {"bash", pfilename, NULL};
- if (execv("/bin/bash",execv_str) < 0){
- perror("fail to execv");
- exit(-1);
- }
- }else{
- //current process
- wait();
- pf=fopen(LOGFILE,"a");
- if (NULL == pf){
- printf("fail to open the logfile !!!\n");
- return -1;
- }
- time_t t;
- struct tm * ptm;
- time(&t);
- ptm = gmtime(&t);
- char cstr[24];
- sprintf (cstr, "time: %4d-%02d-%02d %02d:%02d:%02d\n", 1900+ptm->tm_year,ptm->tm_mon,ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
- fwrite(cstr, strlen(cstr),1, pf);
- int uid = getuid();
- sprintf(cstr, "uid: %d\ncommand:\n",uid);
- fwrite(cstr, strlen(cstr),1, pf);
- fwrite(pcmd, strlen(pcmd),1, pf);
- fwrite("\n\n\n", 3,1, pf);
- fclose(pf);
- remove(pfilename);
- return 0;
- }
- return 0;
- }

