一、Linux下多任務機制的介紹
Linux有一特性是多任務,多任務處理是指用戶可以在同一時間內運行多個應用程序,每個正在執行的應用程序被稱為一個任務。
多任務操作系統使用某種調度(shedule)策略(由內核來執行)支持多個任務並發執行。事實上,(單核)處理器在某一時刻只能執行一個任務。每個任務創建時被分配時間片(幾十到上百毫秒),任務執行(占用CPU)時,時間片遞減。操作系統會在當前任務的時間片用完時調度執行其他任務。由於任務會頻繁地切換執行,因此給用戶多個任務運行的感覺。所以可以說,多任務由“時間片 + 輪換”來實現。多任務操作系統通常有三個基本概念:任務、進程和線程,現在,我們先學習進程:
進程的基本概念
進程是指一個具有獨立功能的程序在某個數據集合上的動態執行過程,它是操作系統進行資源分配和調度的基本單元。簡單的說,進程是一個程序的一次執行的過程。
進程具有並發性、動態性、交互性和獨立性等主要特性。
進程和程序是有本質區別的:
1)程序( program )是一段靜態的代碼,是保存在非易失性存儲器(磁盤)上的指令和數據的有序集合,沒有任何執行的概念;
2)進程( process )是一個動態的概念,它是程序的一次執行過程(在RAM上執行),包括了動態創建、調度、執行和消亡的整個過程,它是程序執行和資源管理的最小單位。
這裡,我們可以看到,進程由兩部分組成:內存地址空間 + task_struct ,task_struct 下面我們會講到,內存地址空間就是我們程序在內存中運行(進程)時所開辟的4GB虛擬地址空間,用於存放代碼段、數據段、堆、棧等;
從操作系統的角度看,進程是程序執行時相關資源的總稱。當進程結束時,所有資源被操作系統回收。
Linux系統中主要包括下面幾種類型的過程:
1)交互式進程;
2)批處理進程;
3)守護進程;
Linux下的進程結構
進程不但包括程序的指令和數據,而且包括程序計數器和處理器的所有寄存器以及存儲臨時數據的進程堆棧。
因為Linux是一個多任務的操作系統,所以其他的進程必須等到操作系統將處理器的使用權分配給自己之後才能運行。當正在運行的進程需要等待其他的系統資源時,Linux內核將取得處理器的控制權,按照某種調度算法將處理器分配給某個等待執行的進程。
在上面介紹程序和進程的區別時,我們看到進程除了內存地址空間以外,還有個結構體task_struct,內核將所有進程存放在雙向循環鏈表(進程鏈表)中,鏈表的每一項就是這個結構體task_struct,稱為進程控制塊的結構。該結構包含了與一個進程相關的所有信息,在linux內核目錄下文件中定義。task_struct內核結構比較大,它能完整地描述一個進程,如進程的狀態、進程的基本信息、進程標示符、內存的相關信息、父進程相關信息、與進程相關的終端信息、當前工作目錄、打開的文件信息,所接收的信號信息等。
下面詳細講解task_struct結構中最為重要的兩個域:stat (進程狀態) 和 pid (進程標示符)。
1、進程狀態
Linux中的進程有以下幾種主要狀態:運行狀態、可中斷的阻塞狀態、不可中斷的阻塞狀態、暫停狀態、僵死狀態、消亡狀態,它們之間的轉換關系如下:
1)運行態(TASK_RUNNING):進程當前正在運行,或者正在運行隊列中等待調度(排隊中);
2)等待態_可中斷(TASK_INTERRUPTIBLE):進程處於阻塞(睡眠sleep,主動放棄CPU)狀態,正在等待某些事件發生或能夠占用某些資源。處於這種狀態下的進程可以被信號中斷。接收到信號或被顯式地喚醒呼叫(如調用wake_up系列宏:wake_up、wake_up_interruptible等)喚醒之後,進程將轉變為運行態,繼續排隊等待調度;
3)登台態_不可中斷(TASK_UNINTERRUPTIBLE):此進程狀態類似於可中斷的阻塞狀態(TASK_INTERRUPTIBLE),只是他不會處理信號,把信號傳遞到這種狀態下的進程不能改變它的狀態,即不可被信號所中斷,不能被隨便調度。在一些特定的情況下(進程必須等待,知道某些不能被中斷的事件發生),這種狀態是很有用的。只有在它等待的事件發生時,進程才被顯示地喚醒呼叫喚醒,進程將轉變為運行態,繼續排隊等待調度;
4)停止態(TASK_STOPPED),即暫停狀態,進程的執行被暫停,當進程受到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信號,就會進入暫停狀態,知道收到繼續執行信號,轉變為運行態,繼續排隊等待調度;
5)僵屍態(EXIT_ZOMBIE):子進程運行結束,父進程尚未使用wait 函數族(如使用wait()函數)等系統調用來回收退出狀態。處於該狀態下的子進程已經放棄了幾乎所有的內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中(即task_struct)保留一個位置,記載該進程的退出信息供其父進程收集。即進程結束後,內存地址空間被釋放、task_struct 成員被釋放,但task_struct 這個空殼還存在,它就是僵屍,這個僵屍我們用kill 是殺不掉的。所以,一般在子進程結束後,我們會對其進行回收。回收的方法有三種:
1)誰創建誰回收,即用父進程來回收;
2)父進程不回收,通知內核來回收;
3)由init 進程來回收,當父進程先死掉,子進程成為孤兒進程,由 init 進程來認養;
當這三種條件都不滿足時,比如父進程不去回收子進程,自己卻未死掉,僵屍便會出現,這是非常棘手的,可以通過殺死父進程來殺死僵屍(不推薦使用);
2、進程標識符
Linux內核通過唯一的進程標示符PID來標識每個進程。PID存放在task_strcut 的pid字段中。當系統啟動後,內核通常作為某一個進程的代表。一個指向task_struct 的宏 current 用來記錄正在運行的進程。current 進程作為進程描述符結構指針的形式出現在內核代碼中,例如,current->pid 表示處理器正在執行的進程的PID。當系統需要查看所有的進程時,則調用 for_each_process(宏),這將比系統搜索數組的速度要快的多。
在Linux 中獲得當前進程的進程號(PID)和父進程號 (PPID) 的系統調用函數分別為 getpid() 和 getppid() 。
3、進程的模式
進程的執行模式分別為用戶模式和內核模式。
在CPU的所有指令中,有一些指令是非常危險的,如果錯用,將導致整個系統崩潰。比如:清內存、設置時鐘等。如果所有的程序都能使用這些指令,那麼你的系統一天死機n回就不足為奇了。所以,CPU將指令分為特權指令和非特權指令,對於那些危險的指令,只允許操作系統及其相關模塊使用,普通的應用程序只能使用那些不會造成災難的指令。Intel的CPU將特權級別分為4個級別:RING0,RING1,RING2,RING3。
linux的內核是一個有機的整體。每一個用戶進程運行時都有一份內核的拷貝,每當用戶進程使用系統調用時,都自動地將運行模式從用戶態轉為內核態,此時進程在內核的地址空間中運行。
當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核運行態(或簡稱為內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每個進程都有自己的內核棧。
當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而突然被中斷程序中斷時,此時用戶程序也可以象征性地稱為處於進程的內核態。因為中斷處理程序將使用當前進程的內核棧。這與處於內核態的進程的狀態有些類似。
內核態與用戶態是操作系統的兩種運行級別,跟intel cpu沒有必然的聯系, 如上所提到的intel cpu提供Ring0-Ring3四種級別的運行模式,Ring0級別最高,Ring3最低。Linux使用了Ring3級別運行用戶態,Ring0作為 內核態,沒有使用Ring1和Ring2。Ring3狀態不能訪問Ring0的地址空間,包括代碼和數據。Linux進程的4GB地址空間,3G-4G部 分大家是共享的,是內核態的地址空間,這裡存放在整個內核的代碼和所有的內核模塊,以及內核所維護的數據。用戶運行一個程序,該程序所創建的進程開始是運行在用戶態的,如果要執行文件操作,網絡數據發送等操作,必須通過write,send等系統調用,這些系統調用會調用內核中的代碼來完成操作,這時,必須切換到Ring0,然後進入3GB-4GB中的內核地址空間去執行這些代碼完成操作,完成後,切換回Ring3,回到用戶態。這樣,用戶態的程序就不能 隨意操作內核地址空間,具有一定的安全保護作用。
處理器總處於以下狀態中的一種:
1、內核態,運行於進程上下文,內核代表進程運行於內核空間;
2、內核態,運行於中斷上下文,內核代表硬件運行於內核空間;
3、用戶態,運行於用戶空間。
從用戶空間到內核空間有兩種觸發手段:
1、用戶空間的應用程序,通過系統調用,進入內核空間。這個時候用戶空間的進程要傳遞很多變量、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存器值、變量等。所謂的“進程上下文”,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變量和寄存器值和當時的環境等。
2、硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理。所謂的“中斷上下文”,其實也可以看作就是硬件傳遞過來的這些參數和內核需要保存的一些其他環境(主要是當前被打斷執行的進程環境)。
一個程序我們可以從兩種角度去分析。其一就是它的靜態結構,其二就是動態過程。下圖表示了用戶態和內核態直接的關系(靜態的角度來觀察程序)
二、進程編程
1、fork() 函數
在Linux 中創建一個新進程的方法是使用fork() 函數。fork() 函數最大的特性就是執行一次返回兩個值。
函數原型如下:
所需頭文件#include //提供類型pid_t定義
#include
函數原型pid_t fork(void)
函數返回值0 :子進程
子進程PID(大於0的整數):父進程
-1 :出錯
fork() 函數用於從已存在的進程中創建一個新進程。新進程稱為子進程,而原進程稱為父進程。具體fork()函數究竟做了什麼,我們先看這張圖:
這裡我們可以看到,使用fork () 函數得到的子進程是父進程的一個復制品,它從父進程處繼承了整個進程的地址空間(注意:子進程有其獨立的地址空間,只是復制了父進程地址空間裡的內容),包括進程上下文、代碼段、進程堆棧、內存信息、打開的文件描述符、信號處理函數、進程優先級等。而子進程所獨有的只是它的進程號、資源使用和計時器等。
因為子進程幾乎是父進程的完全復制,所以父子進程會運行同一個程序,這裡,兩個進程都會從PC位置往下執行;如何區分它們呢?父子進程一個很重要的區別是, fork()返回值不同。父進程中返回值是子進程的進程號,而子進程中返回0;所以在上圖中,兩個進程會通過判斷PID來選擇執行的語句。
注意:子進程沒有執行fork() 函數,而是從fork() 函數調用的下一條語句開始執行。
下面,寫一個fork()程序,來加深對fork()的理解:
view plaincopy
#include
#include
#include
#include
intglobal=22;
intmain(void)
{
inttest=0,stat;
pid_tpid;
pid=fork();
if(pid<0)
{
perror("fork");
return-1;
}
elseif(pid==0)
{
global++;
test++;
printf("global=%dtest=%dChild,myPIDis%d\n",global,test,getpid());
exit(0);
}
else
{
global+=2;
test+=2;
printf("global=%dtest=%dParent,myPIDis%d\n",global,test,getpid());
exit(0);
}
}
執行結果如下:
從結果我們可以發現幾個問題:
1)最後一行光標在閃,是程序沒執行完嗎?第三行中子進程打印前是bash,這是什麼原因呢?
其實我們這裡執行的程序中有三個進程在執行:父進程、子進程、bash。從打印結果中我們可以看到父進程先執行完,然後是bash ,最後子進程執行完,這裡的光標其實是bash的。所以,我們可以發現:父進程、子進程誰先運行時不可知的,誰先運行有內核調度來確定;
2)從打印結果中,可以看出父子進程打印出了各自的進程號和對應變量的值,顯然global和test在父子進程間是獨立的,其各自的操作不會對對方的值有影響;
2、exec 函數族
fork() 函數用於創建一個子進程,該子進程幾乎賦值了父進程的全部內容。我們能否讓子進程執行一個新的程序呢?exec 函數族就提供了一個在進程中執行裡一個程序的辦法。它可以根據指定的文件名或目錄找到可執行文件,並用它來取代當前進程的數據段、代碼段和堆棧段。在執行完之後,當前進程除了進程號外,其他的內容都被替換掉了。所以,如果一個進程想執行另一個程序,那麼它就可以調用fork() 函數創建一個進程,然後調用exec家族中的任意一個函數,這樣看起來就像執行應用程序而產生了一個新進程。
Linux 中並沒有exec() 函數,而是有6個以 exec 開頭的函數,下面是函數語法:
所需頭文件#include
函數原型int execl (const char *path,const char *arg,...);
int execv (const char *path, char *const argv[]);
int execle (const char *path,const char *arg,....,char *const envp[]);
int execve(const char *path, char const *argv[],char *const envp[]);
int execlp (const char *file,const char *arg,...);
int execvp (const char *file, char *const argv[]);
函數返回值-1;出錯
exec 函數族使用區別
1)可執行文件查找方式
表中的前四個函數的查找方式都是指定完整的文件目錄路徑,而最後兩個函數(以p 結尾的函數)可以只給出文件名,系統會自動從環境變量“$PATH”所包含的路徑中進行查找。
2)參數表傳遞方式
兩種方式:逐個列舉或是將所喲參數通過指針數組傳遞
以函數名的第五位字母來區分,字母為" l ”(list) 的表示逐個列舉的方式;字母為"v "(vertor) 的表示將所有參數構成指針數組傳遞,其語法為 char *const argv[]
3)環境變量的使用
exec 函數族可以默認使用系統的環境變量,也可以傳入指定的環境變量,這裡,以"e" (Enviromen) 結尾的兩個函數execle 、execve 就可以在 envp[] 中傳遞當前進程所使用的環境變量;
exev使用示例:
view plaincopy
#include
#include
intmain()
{
//調用execlp函數,相當於調用了"ps-ef"命令
if(execlp("ps","ps","-ef",NULL)<0)//這裡"ps"為filename"ps"為argv[0]"-ef"為argv[1],NULL為參數結束標志
{
perror("execlperror");
return-1;
}
return0;
}
執行結果如下:
view plaincopy
fs@ubuntu:~/qiang/process/exec$./execlp
UIDPIDPPIDCSTIMETTYTIMECMD
root10013:48?00:00:01/sbin/init
root20013:48?00:00:00[kthreadd]
...
root53002020:49?00:00:00[kworker/0:2]
root53512020:54?00:00:00[kworker/0:0]
fs53712797020:56pts/000:00:00ps-ef
fs@ubuntu:~/qiang/process/exec$
如果我們使用execvp,則
view plaincopy
char*argv[]={"ps","-ef",NULL};
execvp("ps",argv);
3、exit() 和_exit()
1) exit()和_exit()函數說明
exit()和_exit() 都是用來終止進程的。當程序執行到exit()或_exit() 時,進程會無條件的停止剩下的所有操作,清除各種數據結構,並終止本進程的運行。但是,這兩個函數是有區別的:
可以看出exit() 是庫函數,而_exit() 是系統調用;
_exit() 函數的作用最為簡單:直接使進程終止運行,清除其使用的內存空間,並銷毀其在內核中的各種數據結構;exit() 函數則在這些基礎上作了一些包裝,在執行退出之前加了若干道工序。
二者函數描述如下:
所需頭文件exit() :#include
_exit():#include
函數原型exit() :void exit(int status);
_exit():void _exit(int status);
函數傳入值status 是一個整形的參數,可以利用這個參數傳遞進程結束時的狀態。
通常0表示正常結束;其他的數值表示出現了錯誤,進程非正常結束。在
實際編程時,可以用wait 系統調用接收子進程的返回值,進行相應的處理。
其實,在main函數內執行return 語句,也會使進程正常終止;
exit(status) 執行完會將終止狀態(status)傳給內核,內核會將status傳給父進程的wait(&status),wait()會提取status,並分析;
4、wait 和waitpid()
wait() 函數
調用該函數使進程阻塞,直到任一個子進程結束或者是該進程接受到了一個信號為止。如果該進程沒有子進程或其子進程已經結束。wait 函數會立即返回。函數描述如下:
所需頭文件#include
#includ
函數原型pid_t wait(int *status)
函數參數status是一個整型指針,指向的對象用來保存子進程退出時的狀態
status 若為空,表示忽略子進程退出時的狀態;
status 若不為空,表示保存子進程退出時的狀態;
另外,子進程的結束狀態可有Linux 中的一些特定的宏來測宏。
函數返回值成功:子進程的進程號
失敗: -1
附:檢查wait 所返回的終止狀態的宏
WIFEXTED (status):若為正常終止子進程返回的狀態,則為真。對於這種情況可執行 WEXITSTATUS (status) ,取子進程傳送給exit 參數的低八位;
(首先判斷子進程是否正常死亡,異常死亡是不會運行到exit()的,這時分析status 是無意義的;)
wait() 會回收任一一個先死亡的子進程;
下面看一個程序,wait() 與exit()的使用
view plaincopy
#include
#include
#include
#include
#include
intmain(intargc,char**argv)
{
pid_tpid;
printf("parent[pid=%d]isborn\n",getpid());
if(-1==(pid=fork())){
perror("forkerror");
return-1;
}
if(pid==0){
printf("child[pid=%d]isborn\n",getpid());
sleep(20);
printf("childisover\n");
exit(123);//return123;
}
else{
pid_tpid_w;
intstatus;
printf("parentstartedtowait...\n");
pid_w=wait(&status);
printf("parentwaitreturned\n");
if(pid_w<0){
perror("waiterror");
return1;
}
if(WIFEXITED(status)){
status=WEXITSTATUS(status);
printf("waitreturnswithpid=%d.returnstatusis%d\n",pid_w,status);
}else{
printf("waitreturnswithpid=%d.thechildisterminatedabnormally\n",pid_w);
}
//while(1);
printf("fatherisover\n");
return0;
}
}
執行結果如下:
view plaincopy
fs@ubuntu:~/qiang/process/wait$./wait
parent[pid=5603]isborn
parentstartedtowait...
child[pid=5604]isborn
childisover
parentwaitreturned
waitreturnswithpid=5604.returnstatusis123
fatherisover
fs@ubuntu:~/qiang/process/wait$
waitpid() 函數
waitpid() 函數和wait() 的作用是完全相同的,但waitpid 多出了兩個可有用戶控制的參數pid 和 options,從而為用戶編程提供了一種更為靈活的方式。waitpid 可以用來等待指定的進程,可以使進程不掛起而立刻返回。
wait(&status);等價於waitpid(-1, &status, 0);
其函數類型如下:
所需頭文件#include /* 提供類型pid_t的定義 */
#include
函數原型pid_t waitpid(pid_t pid,int *status,int options)
函數傳入值pid > 0 時,只等待進程ID等於pid的子進程,不管其它已經有多少子進程運行結束退出了,
只要指定的子進程還沒有結束,waitpid就會一直等下去。
pid = -1時,等待任何一個子進程退出,沒有任何限制,
此時waitpid和wait的作用一模一樣。
pid = 0 時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,
waitpid不會對它做任何理睬。
pid < -1時,等待一個指定進程組中的任何子進程,這個進程組的ID等於pid的絕對值。
函數傳入值WNOHANG 如果沒有任何已經結束的子進程則馬上返回, 不予以等待;
WUNTRACED 如果子進程進入暫停執行情況則馬上返回,但結束狀態不予以理會;
0 : 同wait() ,阻塞父進程,直到指定的子進程退出;
函數返回值> 0 :已經結束運行的子進程的進程號;
0 : 使用選項WNOHANG 且沒有子進程退出
-1 : 出錯
示例如下:
view plaincopy
#include
#include
#include
#include
#include
intmain()
{
pid_tpc,pr;
pc=fork();
if(pc<0)/*如果fork出錯*/
printf("Erroroccuredonforking\n");
elseif(pc==0)/*如果是子進程*/
{
sleep(10);/*睡眠10秒*/
exit(0);
}
/*如果是父進程*/
do{
pr=waitpid(pc,NULL,WNOHANG);/*使用了WNOHANG參數,waitpid不會在這裡等待*/
if(pr==0)/*如果沒有收集到子進程*/
{
printf("Nochildexited\n");
sleep(1);
}
}while(pr==0);/*沒有收集到子進程,就回去繼續嘗試*/
if(pr==pc)
printf("successfullygetchild%d\n",pr);
else
printf("someerroroccured\n");
}
執行結果如下:
view plaincopy
fs@ubuntu:~/qiang/wait$./waitpid
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
Nochildexited
successfullygetchild17144
fs@ubuntu:~/qiang/wait$ 父進程經過10次失敗的嘗試之後,終於收集到了退出的子進程。