1. 程式人生 > >linux c語言 fork() 和 exec 函式的簡介和用法

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