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;
}