Linux那些被你忽略的知識點-linux程序

前言

程堆疊、開啟的檔案描述符、訊號控制設定、 程序優先順序、程序組號等。例如,要是父程序打開了五個檔案,那麼子程序也有五個開啟的檔案,而且這些檔案的當前讀寫指標也停在相同的地方。

所以,這一步所做的是複製。這樣得到的子程序獨立於父程序,具有良好的併發性,但是二者之間的通訊需要透過專門的通訊機制。子程序所獨有的只有它的程序號、計時器等。使用fork函式的代價是很大的,這些開銷並不是所有的情況下都是必須的。比如某程序fork出一個子程序後,其子程序僅僅是為了呼叫exec執行另一個可執行檔案,那麼在fork過程中對於虛存空間的複製將是一個多餘的過程。

但由於現在Linux中是採取了copy-on-write(COW寫時複製)技術,為了降低開銷,fork最初並不會真的產生兩個不同的複製,因為在那個時候,大量的資料其實完全是一樣的。

寫時複製是在推遲真正的資料複製。若後來確實發生了寫入,那意味著parent和child的資料不一致了,於是產生複製動作,每個程序拿到屬於自己的那一份,這樣就可以降低系統呼叫的開銷。所以有了寫時複製後,vfork其實現意義就不大了。

在fork之後,子程序和父程序都會繼續執行fork呼叫之後的指令。子程序是父程序的副本。它將獲得父程序的資料空間,堆和棧的副本,這些都是副本,父子程序並不共享這部分的記憶體。也就是說,子程序對父程序中的同名變數進行修改並不會影響其在父程序中的值。但是父子程序又共享一些東西,簡單說來就是程式的正文段。正文段存放著由cpu執行的機器指令,通常是read-only的。

值得注意的是,子程序也是繼承父程序的緩衝區的(全緩衝,在填滿標準I/O緩衝區後,才進行實際的I/O操作;行緩衝,在遇到換行符時,標準I/O庫進行實際I/O 操作。),所以列印輸出時在不進行特殊處理的情況下會出現交叉列印。

有些列印函式,如printf,在輸出到螢幕上時是行緩衝,但是重定向輸出到檔案中卻變成了全緩衝。比如程式a。out中使用了printf進行輸出,如果a。out > log,則printf是按照全緩衝處理的。

vfork函式

定義:pid_t vfork(void);

vfork和fork的區別:

- vfork保證子程序先執行,父程序會被阻塞直到子程序呼叫exec或exit之後,父程序才可能被排程執行。

- vfork和fork一樣都建立一個子程序,但它並不將父程序的地址空間完全複製到子程序中,因為子程序會立即呼叫exec(或者exit),於是也就不訪問該地址空間。 相反,在子程序呼叫exec和exit之前,它在父程序的地址空間中執行,在exec之後子程序會有自己的程序地址空間。

注意:

在fork中用return語句是允許的,因為子程序是複製了一份資料。然而,在vfork中用return語句,因為父子程序共享地址空間,子程序return會引起彈棧,導致棧的破壞,也就是父程序不能夠繼續執行下去了。因此,在vfork中需要用exit()函式或者_exit()函式exec族函式。

clone函式

定義:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

系統呼叫fork()和vfork()是無引數的,而clone()則帶有引數。fork()是全部複製,vfork()是共享記憶體,而clone() 是則可以將父程序資源有選擇地複製給子程序,而沒有複製的資料結構則透過指標的複製讓子程序共享,具體要複製哪些資源給子程序,由引數列表中的 clone_flags來決定。另外,clone()返回的是子程序的pid。

CLONE_PARENT 建立的子程序的父程序是呼叫者的父程序,新程序與建立它的程序成了“兄弟”而不是“父子”

CLONE_FS 子程序與父程序共享相同的檔案系統,包括root、當前目錄、umask

CLONE_FILES 子程序與父程序共享相同的檔案描述符(file descriptor)表

CLONE_NEWNS 在新的namespace啟動子程序,namespace描述了程序的檔案hierarchy

CLONE_SIGHAND 子程序與父程序共享相同的訊號處理(signal handler)表

CLONE_PTRACE 若父程序被trace,子程序也被trace

CLONE_VFORK 父程序被掛起,直至子程序釋放虛擬記憶體資源

CLONE_VM 子程序與父程序運行於相同的記憶體空間

CLONE_PID 子程序在建立時PID與父程序一致

CLONE_THREAD Linux 2。4中增加以支援POSIX執行緒標準,子程序與父程序共享相同的執行緒群

例子:#include   #include   #include   #include   #include   #include   #include   int variable,fd;  int do_something(){ variable = 42;   printf(“in child process\n”);   close(fd);   return 0;  }  int main(int argc, char *argv[]){ void *child_stack;  char tempch;  variable = 9;  fd = open(“/test。txt”,O_RDONLY);  child_stack = (void *)malloc(16384);printf(“The variable was %d\n”,variable);  clone(do_something, child_stack+10000, CLONE_VM |CLONE_FILES,NULL);  sleep(3); /* 延時以便子程序完成關閉檔案操作、修改變數 */  printf(“The variable is now %d\n”,variable);  if(read(fd,&tempch,1) < 1){ perror(“File Read Error”);  exit(1);   }  printf(“We could read from the file\n”);  return 0;  } 執行結果: the value was 9  in child process  The variable is now 42  File Read Error

sleep函式

程序掛起指定的秒數,直到指定的時間用完或者收到訊號才解除掛起。注意:程序掛起指定的秒數後程序並不會立即執行,系統只是將此程序切換到了就緒態。

wait函式

工作原理:

父程序呼叫wait函式後阻塞,子程序結束時,系統向其父程序傳送SIGCHILD訊號。父程序被SIGCHILD訊號喚醒然後去回收殭屍子程序,父子程序之間是非同步的,SIGCHILD訊號機制就是為了解決父子程序之間的非同步通訊問題,讓父程序可以及時的去回收殭屍子程序。若父程序沒有任何子程序則wait返回錯誤。當前程序有可能有多個子程序,wait函式阻塞直到其中一個子程序結束wait就會返回,wait的返回值就可以用來判斷到底是哪一個子程序本次被回收了。

pid_t wait(int *status);功能:等待子程序終止,如果終止了,此函式會回收子程序的資源。呼叫wait函式的程序會掛起,直到它的一個子程序退出或收到一個不能被忽視的訊號時才被喚醒。  若呼叫程序沒有子程序或它的子程序已經結束,該函式立即返回。引數:函式返回時,引數status中包含子程序退出時的狀態資訊。子程序的退出資訊在一個int中包含了多個欄位,用宏定義可以取出其中的每個欄位。返回值:如果執行成功則返回子程序的程序號,出錯返回-1,失敗原因存於errno中。1、WIFEXITED(status) 這個宏用來指出子程序是否為正常退出的,如果是,它會返回一個非零值。2、WEXITSTATUS(status) 當WIFEXITED返回非零值時,我們可以用這個宏來提取子程序的返回值,如果子程序呼叫exit(5)退出,WEXITSTATUS(status) 就會返回5;如果子程序呼叫exit(7),WEXITSTATUS(status)就會返回7。請注意,如果程序不是正常退出的,也就是說, WIFEXITED返回0,這個值就毫無意義。

waitpid函式

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的絕對值。options提供了一些額外的選項來控制waitpid,引數option可以為0或可以用“|”運算子把它們連線起來使用,比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);如果我們不想使用它們,也可以把options設為0,如:ret=waitpid(-1,NULL,0);- WNOHANG 若pid指定的子程序沒有結束,則waitpid()函式返回0,不予以等待。若結束,則返回該子程序的ID。- WUNTRACED 若子程序進入暫停狀態,則馬上返回,但子程序的結束狀態不予以理會。WIFSTOPPED(status)宏確定返回值是否對應於一個暫停子程序。

注: wait就是經過包裝的waitpid。

static inline pid_t wait(int *wait_stat){return waitpid(-1,wait_stat,0);}

如果父程序屬於守護程序一類,開啟TCP套接字等待連結,每當有請求到來,便fork一個子程序傳輸資訊並自由退出。父程序並不關注子程序的退出狀態,是否正常都不影響今後的服務,但子程序變成殭屍程序便麻煩了,隨著時間的進行,殭屍程序一大堆,雖然佔用資源不多,且終究是個隱患。此時可以利用訊號處理方式進行非阻塞回收僵死子程序資源。

例子(子程序狀態改變會發送SIGCHLD訊號給父程序,父程序建立並回收多個子程序):

#include #include #include #include #include #include #include #include #define MY_PROCESS_COUNT 10 void child_catch(int signalNumber) {//子程序狀態發生改變時,核心對訊號作處理的回撥函式int w_status;pid_t w_pid;while ((w_pid = waitpid(-1, &w_status, WNOHANG)) != -1 && w_pid != 0) {if (WIFEXITED(w_status)) //判斷子程序是否正常退出printf(“——-catch pid %d,return value %d\n”, w_pid, WEXITSTATUS(w_status)); //列印子程序PID和子程序返回值}}int main(int argc, char **argv) {pid_t pid;int i;//在此處阻塞SIGCHLD訊號,防止訊號處理函式尚未註冊成功就有子程序結束sigset_t child_sigset;sigemptyset(&child_sigset); //將child_sigset每一位都設定為0sigaddset(&child_sigset, SIGCHLD); //新增SIGCHLD位sigprocmask(SIG_BLOCK, &child_sigset, NULL); //完成父程序阻塞SIGCHLD的設定 for (i = 0; i < MY_PROCESS_COUNT; i++) {//建立子程序,建立成功就跳出迴圈if ((pid = fork()) == 0)break;}if (MY_PROCESS_COUNT == i) { //括號內為父程序程式碼struct sigaction act; //訊號回撥函式使用的結構體act。sa_handler = child_catch;sigemptyset(&(act。sa_mask)); //設定執行訊號回撥函式時父程序的的訊號遮蔽字act。sa_flags = 0;sigaction(SIGCHLD, &act, NULL); //給SIGCHLD註冊訊號處理函式//解除SIGCHLD訊號的阻塞sigprocmask(SIG_UNBLOCK, &child_sigset, NULL);printf(“im PARENT ,my pid is %d\n”, getpid());while (1){ //父程序在這裡處理自己的相關事情,同時回收僵死子程序資源//父程序執行相關程式碼        }} else {//子程序執行程式碼printf(“im CHILD ,my pid is %d\n”, getpid());return i;}}

特殊程序

- 殭屍程序:程序已執行結束,但程序佔用的資源未被回收,這樣的程序稱為殭屍程序。子程序已執行結束,父程序未呼叫wait或者waitpid函式回收子程序的資源是 子程序變為殭屍程序的原因。

- 孤兒程序:父程序執行結束,但子程序未執行結束的子程序。(在bash終端上,父程序結束後即會釋放終端,子程序其實是在後臺執行的。)

- 守護程序:守護程序是個特殊的孤兒程序,這種程序脫離終端,在後臺執行。

exit函式

#include void exit(int status)引數:status是返回給父程序的引數(低8位有效)。

Linux那些被你忽略的知識點-linux程序

注:exit用於在程式執行的過程中隨時結束程式,exit的引數是返回給OS的。main函式結束時也會隱式地呼叫exit函式。exit函式執行時首先會執行由atexit()函式登記的函式,然後會做一些自身的清理工作,同時重新整理所有輸出流、關閉所有開啟的流並且關閉透過標準I/O函式tmpfile()建立的臨時檔案。exit是結束一個程序,它將刪除程序使用的記憶體空間,同時把錯誤資訊返回父程序;而return是返回函式值並退出函式。通常情況:exit(0)表示程式正常, exit(1)和exit(-1)表示程式異常退出,exit(2)表示表示系統找不到指定的檔案。在整個程式中,只要呼叫exit就結束(當前程序或者在main時候為整個程式)。

_exit函式#include void _exit(int status)引數:status是返回給父程序的引數(低8位有效)。

注:相比於exit函式,_exit函式是系統呼叫,而exit函式是庫函式。

exec函式族

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[]);

exec函式族的作用是根據指定的檔名找到可執行檔案,並用它來取代呼叫程序的內容,換句話說,就是在呼叫程序內部執行一個可執行檔案。這裡的可執行檔案既可以是二進位制檔案,也可以是任何Linux下可執行的指令碼檔案。

與一般情況不同,exec函式族的函式執行成功後不會返回,因為呼叫程序的實體,包括程式碼段,資料段和堆疊等都已經被新的內容取代,只留下程序ID等一些表面上的資訊仍保持原樣。只有呼叫失敗了,它們才會返回一個-1,從原程式的呼叫點接著往下執行。所以通常我們直接在exec函式呼叫後直接呼叫perror()和exit(),無需if判斷。

每當有程序認為自己不能為系統和使用者做出任何貢獻了,他就可以呼叫任何一個exec,讓自己以新的面貌重生;或者,更普遍的情況是,如果一個程序想執行另一個程式,它就可以fork出一個新程序,然後呼叫任何一個exec,這樣看起來就好像透過執行應用程式而產生了一個新程序一樣。

事實上第二種情況應用得很普遍,以至於Linux專門為其作了最佳化,我們知道,fork會將呼叫程序的所有內容原封不動的複製到新產生的子程序中去,這些複製的動作很消耗時間,而如果fork完之後我們馬上就呼叫exec,這些辛辛苦苦複製來的東西又會被立刻抹掉,這看起來非常不划算,於是人們設計了一種“寫時複製(copy-on-write)”技術,使得fork結束後並不立刻複製父程序的內容,而是到了真正實用的時候才複製,這樣如果下一條語句是exec,它就不會白白作無用功了,也就提高了效率。

帶l的exec函式這類函式有:execl,execlp,execle具體說明:表示後邊的引數以可變引數的形式給出且都以一個空指標結束。這裡特別要說明的是,程式名也是引數,所以第一個引數就是程式名。帶p的exec函式這類函式有:execlp,execvp具體說明:表示第一個引數無需給出具體的路徑,只需給出函式名即可,系統會在PATH環境變數中尋找所對應的程式,如果沒找到的話返回-1。帶v的exec函式這類函式有:execv,execvp具體說明:表示命令所需的引數以char *arg[]形式給出且arg最後一個元素必須是NULL帶e的exec函式這類函式有:execle具體說明:將環境變數傳遞給需要替換的程序,原來的環境變數不再起作用。

事實上,

只有execve是真正的系統呼叫,其它五個函式最終都呼叫execve,都是庫函式。

一個程序呼叫exec後,除了程序ID,程序還保留了下列特徵不變:父程序號、程序組號、控制終端、根目錄、當前工作目錄、程序訊號遮蔽集、未處理訊號,。。。

附1:

在 Linux 中程序和執行緒實際上都是用一個結構體 task_struct來表示一個執行任務的實體。程序建立呼叫fork 系統呼叫,而執行緒建立則是 pthread_create 方法,但是這兩個方法最終都會呼叫到 do_fork 來做具體的建立操作 ,區別就在於傳入的引數不同。Linux 實現執行緒的方式很巧妙,實際上根本沒有執行緒,它建立的就是程序,只不過透過引數指定多個程序之間共享某些資源(如虛擬記憶體、頁表、檔案描述符等),函式呼叫棧、暫存器等執行緒私有資料則獨立。

但是在其它提供了專門執行緒支援的系統中,則會在程序控制塊(PCB)中增加一個包含指向該程序所有執行緒的指標,然後再每個執行緒中再去包含自己獨佔的資源。這算是非常正統的實現方式了,比如 Windows 就是這樣乾的。但是相比之下 Linux 就顯得取巧很多,也很簡潔。

最後

Linux相關筆記。pdf文件可分享,需要私信【面試題】即可獲取

Linux那些被你忽略的知識點-linux程序