1. 程式和進程 什麼是程式?什麼是進程? 程式是電腦存儲系統中的數據文件,如源代碼程式和可執行程式 進程是程式關於某個數據集合的一次運行活動,是程式執行後得到的一個實體 在當代操作系統中,進程是資源分配的基本單位 程式和進程有什麼聯繫? 沒有程式就沒有進程;但有了程式,未必就會有進程,如程式不運 ...
1. 程式和進程
什麼是程式?什麼是進程?
- 程式是電腦存儲系統中的數據文件,如源代碼程式和可執行程式
- 進程是程式關於某個數據集合的一次運行活動,是程式執行後得到的一個實體
- 在當代操作系統中,進程是資源分配的基本單位
程式和進程有什麼聯繫?
- 沒有程式就沒有進程;但有了程式,未必就會有進程,如程式不運行、程式本身是動態庫等
- 一個程式可能對應多個進程,如記事本程式多次運行,產生多個記事本進程
- 一個進程可能包含多個程式,如一個程式依賴多個動態庫,每個動態庫都是一個程式
2. 進程狀態
進程三態模型:就緒、阻塞、運行。
- 就緒:進程已經做好了一切準備,一旦得到CPU,就會開始運行
- 阻塞:進程正在等待某一事件發生(如共用資源被釋放、IO完成)而停止運行,在事件發生前,即使得到CPU也無法運行
- 運行:進程擁有CPU控制權,並正在運行
進程五態模型:與三態模型相比,多了新建、終止兩種狀態。
- 新建:進程還未創建完畢,不能被系統調度
- 終止:進程已結束運行,正在回收系統資源
3. 進程標識
每個進程都有一個非負整數的進程ID(pid),作為識別不同進程的唯一標識。
此外,每個進程還有一些其他標識符,包括父進程ID(ppid)、實際用戶ID(uid)、有效用戶ID(euid)、實際組ID(gid)、有效組ID(egid)。
#include <unistd>
pid_t getpid(); //返回值:調用進程的進程ID
pid_t getppid(); //返回值:調用進程的父進程ID
uid_t getuid(); //返回值:調用進程的實際用戶ID
uid_t geteuid(); //返回值:調用進程的有效用戶ID
uid_t getgid(); //返回值:調用進程的實際組ID
uid_t getegid(); //返回值:調用進程的有效組ID
4. 進程創建
一個現有的進程可以調用fork函數創建一個新進程,這個新進程叫做子進程,調用fork的進程叫做父進程。
- 子進程獲得父進程數據空間、堆、棧的副本
- 父進程和子進程共用代碼段、文件描述符和文件偏移量
#include <unistd.h>
pid_t fork(); //若成功:子進程返回0,父進程返回子進程ID;若出錯,返回-1
fork的特點為:一次調用,兩次返回。
- 父進程返回子進程ID的原因:父進程可以有多個子進程,但父進程不能通過函數獲得其所有子進程的ID,因為沒有這樣的函數
- 子進程返回0的原因:一個進程只會有一個父進程,並且子進程還可以調用getppid獲得其父進程ID
fork有以下兩種用法:
- 父進程和子進程同時執行不同的代碼段
- 子進程從fork返回後立即調用exec,執行另一個不同的程式
fork成功返回後,父進程和子進程繼續執行後面的代碼,但父子進程誰先執行,這點是不確定的。
#include <stdio.h>
#include <unistd.h>
int globvar = 10;
int main()
{
int var = 5;
pid_t pid = fork();
if (pid > 0)
{
sleep(2); //父進程休眠2秒,讓子進程先運行
}
else if (pid == 0)
{
globvar++;
var++;
}
printf("pid = %d, globvar = %d, var = %d\n", getpid(), globvar, var);
return 0;
}
5. 進程終止
正常終止方式:
- 從main函數return
- 調用exit()、_Exit()、_exit()(對於linux,後兩個函數是同義的)
- 進程的最後一個線程在其啟動常式中調用return或pthread_exit()
異常終止方式:
- 調用abort()以產生SIGABRT信號
- 進程接收到某些信號
- 進程的最後一個線程對pthread_cancel()請求做出響應
6. 避免僵屍進程
僵屍進程的產生與危害
- 一個已經終止、但是其父進程尚未對其進行善後處理的進程,稱為僵屍進程
- 子進程退出時,內核會釋放它占用的記憶體等資源,但是仍然保留了一些信息,如進程ID
- 內核為終止子進程保留的信息直到父進程調用wait或waitpid時才會釋放
- 如果父進程沒有調用wait或waitpid,那麼已經終止的子進程就會變成僵屍進程,其占用的進程ID會無法釋放
- 大量的僵屍進程可能會使系統沒有可用的進程ID,從而導致系統無法創建新進程
wait函數
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); //成功返回終止子進程ID,失敗返回-1
當在父進程中調用了wait時:
- 如果所有子進程都還在運行,則父進程阻塞
- 如果有任意子進程終止,則取得其終止狀態並立即返回
- 如果父進程沒有子進程,則立即出錯返回
如果wait的參數status不為NULL,那麼子進程的終止狀態就存放在它指向的記憶體中,如果不關心終止狀態,可以將status指定為NULL。
waitpid函數
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options); //成功返回終止子進程ID,失敗返回-1或0
waitpid的第二個參數status用法和wait一樣,但waitpid相比於wait的不同之處在於:
- 當
pid > 0
時,waitpid可以等待由pid指定的特定子進程 - 當
options == WNOHANG
時,若pid指定的子進程尚未終止,waitpid不會阻塞,而是立即返回0 - 當
pid == -1 && options == 0
時,waitpid等價於wait
雖然waitpid可以實現非阻塞版本的wait,但也存在一個缺陷:如果子進程在父進程waitpid(pid, NULL, WNOHANGE)
之後才終止,那麼即使父進程尚未結束,也不會給子進程收屍,也就是說,終止的子進程會一直處於僵屍進程狀態,直到父進程退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
sleep(2); //確保父進程執行waitpid時子進程還在休眠
printf("child %d exit\n", getpid());
exit(0);
}
if (waitpid(pid, NULL, WNOHANG) == 0)
{
printf("waitpid return before child exit\n");
}
sleep(300); //雖然waitpid不阻塞,但在父進程終止前,子進程pid會一直是僵屍進程
return 0;
}
如果既要保證父進程不阻塞等待子進程終止,也不希望子進程處於僵屍狀態直到父進程終止,可以採用調用兩次fork的訣竅,其核心思路為:
- 進程A調用fork產生子進程B,然後立即調用
waitpid(pid, NULL, 0)
,等待進程B終止 - 進程B再次調用fork產生子進程C,然後立即調用
exit(0)
終止(必須確保進程B在進程C之前終止) - 進程A隨即解除waitpid阻塞,對進程B收屍處理
- 由於父進程提前終止,進程C由init收養,其終止時也會由init收屍
- 此時,進程A和進程C就成為相互獨立、互不幹擾的兩個進程,兩者各司其職,分別執行不同的處理
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
if ((pid = fork()) > 0)
{
exit(0); //子進程B再次調用fork後立即終止,只留下孫子進程C
}
/*孫子進程C由init進程收養*/
sleep(2);
printf("I'm second child %d, my parent bacomes %d\n", getpid(), getppid());
exit(0);
}
printf("I'm parent %d\n", getpid());
if (waitpid(pid, NULL, 0) == pid) //父進程A立即調用waitpid等待子進程B終止
{
printf("first child %d exit\n", pid);
}
/*此時,進程A和進程C之間就沒有了繼承關係,兩者相互獨立,互不幹擾,各司其職*/
sleep(300);
return 0;
}
從上圖執行結果可以看出:
- 進程B(pid=9293)已經在進程列表中找不到了,說明已經被收屍處理了,沒有產生僵屍進程
- 進程C(pid=9294)也不在進程列表中了,說明其終止後由init收屍處理了,也沒有產生僵屍進程
7. exec函數族
exec函數及使用規則
上面提到過fork的兩種用法,其中一種是“子進程從fork返回後立即調用exec,執行另一個不同的程式”。
- 當進程調用exec函數時,其執行的程式將完全替換為exec指定的新程式,而新程式則從其main()開始執行。
- 因為exec並不創建新進程,所以替換前後的進程ID不會改變,exec只是用磁碟上的一個新程式替換了調用進程的代碼段、數據段和堆棧。
有7個不同的exec函數可供使用,它們統稱為exec函數族,可以根據需要調用這7個函數中的任意一個。
#include <unistd.h>
/*7個函數返回值均為:若成功,不返回;若失敗,返回-1*/
//以路徑名為參數
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/);
int execve(const char *path, char *const argv[], char *const envp[]);
//以文件名為參數
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
int execvp(const char *file, char *const argv[]);
//以文件描述符為參數
int fexecve(int fd, char *const argv[], char *const envp[]);
這些函數的命名是有規律的:exec[必選:l or v][可選:p or e](fexecve作為特例單獨拎出來)
這些函數的參數用於指定新程式的相關信息,分為3部分:可執行程式、命令行參數、環境變數,具體使用規則為:
【必選項l or v】
- 帶l,各個命令行參數必須以','間隔,最後一個命令行參數必須是NULL
- 帶v,需要將l後續的各個命令行參數(包括最後的NULL)構造成一個指針數組,然後以該指針數組作為參數
【可選項p】
- 不帶p,以可執行程式的路徑名path作為參數
- 帶p,以可執行程式的文件名file作為參數,如果file中包含/,則視作路徑名,否則從PATH環境變數指定的目錄中搜索可執行程式
【可選項e】
- 不帶e,複製調用進程的環境變數給新程式使用
- 帶e,需要傳遞環境變數給新程式使用
【fexecve特例】
- 尾碼ve含義和上面一樣
- 首碼f代表新程式由文件描述符fd指定
exec函數使用示例
/* filename - execl.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execl("/bin/echo", "echo", "executed by execl", NULL);
}
waitpid(pid, NULL, 0);
return 0;
}
/* filename - execv.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *execv_argv[] =
{
"echo",
"executed by execv",
NULL
};
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execv("/bin/echo", execv_argv);
}
waitpid(pid, NULL, 0);
return 0;
}
/* filename - execlp.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execlp("echo", "echo", "executed by execlp", NULL);
}
waitpid(pid, NULL, 0);
return 0;
}
/* filename - execvp.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *execvp_argv[] =
{
"echo",
"executed by execvp",
NULL
};
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execvp("echo", execvp_argv);
}
waitpid(pid, NULL, 0);
return 0;
}
/* filename - execle.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *env[] =
{
"PATH=/home/delphi",
"USER=execle",
NULL
};
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execle("/usr/bin/env", "env", NULL, env);
}
waitpid(pid, NULL, 0);
return 0;
}
/* filename - execve.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *execve_argv[] =
{
"env",
NULL
};
char *env[] =
{
"PATH=/home/delphi",
"USER=execve",
NULL
};
int main()
{
pid_t pid = fork();
if (pid == 0)
{
execve("/usr/bin/env", execve_argv, env);
}
waitpid(pid, NULL, 0);
return 0;
}
8. system函數
#include <stdlib.h>
int system(const char *command);
system返回值
在Unix系統中,system在其內部實現調用了fork、exec和waitpid,因此有3種返回值。
- 如果fork失敗,或者waitpid返回除EINTR之外的錯誤,則system返回-1,並且設置errno以指示錯誤類型
- 如果exec失敗,比如被信號中斷,或者command命令不存在,system返回127
- 如果fork、exec和waitpid都成功,system的返回值是shell的終止狀態,即command通過exit或return返回的值
下麵通過一個system的簡易實現,來幫助理解該函數的返回值。
int system(const char * cmdstring)
{
pid_t pid;
int status;
if (cmdstring == NULL)
{
return (1); //如果cmdstring為空,返回非零值,一般為1
}
if ((pid = fork()) < 0)
{
status = -1; //fork失敗,返回-1
}
else if (pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); // exec執行失敗返回127,註意exec只在失敗時才返回現在的進程,成功的話現在的進程就不存在啦~~
}
else //父進程
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno != EINTR)
{
status = -1; //如果waitpid被信號中斷,則返回-1
break;
}
}
}
return status; //如果waitpid成功,則返回子進程的返回狀態
}
仔細看完這個system函數的簡單實現,該函數的返回值就清晰了吧,那麼什麼時候system()返回0呢?答案是只在command命令返回0時。
system使用示例
#include <stdlib.h>
int main()
{
system("ls -l");
system("cat func.c");
system("gcc -o func.out func.c");
system("ls -l");
system("echo main.out begin system func.out");
system("./func.out");
return 0;
}