天天看點

Linux C多程序程式設計基礎程序的關系程序的狀态程序描述符(程序控制塊)程序辨別符Linux程序的使用者Linux程序操作

關于程序概念相關的内容請打開連結,本文所介紹的是程序的POSIX标準。

程序的關系

       Linux中的所有程序都是互相聯系的,程序之間的的從屬關系有父/子關系和兄/弟關系。

       Linux核心建立了程序标号為0以及程序标号為1的程序。其中PID為1的程序是初始化程序init,Linux中的所有程序都是由其衍生而來的,在shell下執行程式啟動的程序則是shell程序的子程序。程序可以再啟動一個或多個程序,這樣就形成了一顆程序樹,每個程序都是樹中的一個節點,其中樹根就是初始化程序init。

       按程序的從屬關系,程序分為以下幾種(假設一個程序為P):

       ·祖先程序p_opptr(original parent):指向建立了程序P的程序的程序描述符,如果父程序不存在(例如已被銷毀或從父程序中退出),則指向程序init的程序描述符。是以當一個shell使用者啟動了一個背景程序并從shell退出的時候,這個背景程序将變成init的子程序。

       ·父程序p_pptr(parent):指向建立了程序P的程序的程序描述符,其值通常來說和p_opptr一緻,但也可能不同。

       ·子程序p_cptr(child):指向由程序P建立的程序中年齡最小(即建立時間最晚)的程序的程序描述符,即上一次建立的程序。

       ·兄程序p_osptr(older sibling):指向與P程序同屬于一個父程序,但建立時間比P程序早的程序程序描述符。

       ·弟程序p_ysptr(younger sibling):與兄程序相對,指向與P程序同屬于一個父程序,但建立時間比P程序較晚的程序。

程序的狀态

       程序在其生存周期内可能處于以下狀态中,且一個程序在同一時刻隻能位于其中一個狀态:

       ·可運作狀态(TASK_RUNNING):占用處理器執行或準備執行。

       ·可中斷的等待狀态(TASK_INTERRUPTIBLE):程序被挂起或睡眠,當滿足某些條件時才退出這種等待狀态。這些條件包括:硬體中斷、等待的資源被釋放、傳遞一個信号燈,退出等待狀态後會回到可運作态。

       ·不可中斷的等待狀态(TASK_UNINTERRUPTIBLE):和上一個狀态相似,差別是當接收到信号時不能退出這個等待狀态。

       ·暫停狀态(TASK_STOPPING):程序的運作被暫停,通常來說是接收到SIGSTOP、SIGTTIN或者SIGTTOU信号後。如果一個程序被另外一個程序監控時,任何信号都可以把這個程序置于TASK_STOPPEN狀态。

       ·僵屍狀态(TASK_ZOMBIE):程序的執行已經被終止,但父程序還沒有wait系列系統調用已傳回的相應資訊,此時核心不能丢棄與該程序有關的資料,因為父程序可能還需要這些資料。

       程序在這幾種狀态之間互相轉化,但對于使用者而言是透明的,這個切換的過程常被稱為程序排程。程序是一個随執行過程不斷變化的實體,和程式要包含指令和資料一樣,程序也包含程式計數器和所有處理器寄存器的值,同時它的堆棧中存儲着參數、傳回位址以及變量之類的臨時資料。在多處理機作業系統中,程序之間除了從屬關系以外相對獨立,如果系統中某個程序崩潰,不會影響到其餘程序,每個程序運作在各自的虛拟位址空間中,通過一定的通信機制,它們之間才能發生聯系。

程序描述符(程序控制塊)

       為了對程序進行管理,Linux核心必須了解每個程序目前的執行狀态,這些狀态包括程序的優先級、運作狀态、配置設定的位址空間等。為了達到這個目的,Linux核心提供了一個結構體task_struct來描述程序(或者說表示程序實體)。

struct task_struct {
    volatile long state;    //程序運作時的狀态,-1表示不可運作,0表示可運作,大于0表示已停止
    unsigned int falgs;     //flags是程序目前的狀态辨別
                            //0x00000002表示程序正在被建立
                            //0x00000004表示程序正準備退出
                            //0x00000040表示程序被fork,但沒有執行exec
                            //0x00000400表示此程序由于其他程序發送相關信号而被殺死
    unsigned int rt_priority    //程序的優先級
    truct list_head tasks;
    struct mm_struct *mm;    //記憶體的使用情況
    int exit_state;
    int exit_code, exit_signal;
    pid_t pid;    //Process ID
    pid_t tgid;    //程序組号
    struct task_struct *real_parent;    //該程序的建立者,即“親生父親”
    struct task_struct *parent;    //該程序現在的父程序,有可能是“繼父”
    struct list_head children;    //指向該程序孩子的連結清單,可以得到所有子程序的程序描述符
    struct list_head sibling;    //指向該程序兄弟的連結清單,也就是其父程序的所有子程序
    struct task_struct *group_leader;    //程序組的組長
    struct list_head thread_group;    //該程序所有線程的連結清單
    time_t utime, stime;    //處理器相關的時間參數
    struct timespec start_time;    //程序啟動時間
    struct timespec real_start_time;    //與上一條類似
    char comm[TASK_COMM_LEN];
    int link_count, total_link_count;    //檔案系統資訊計數
    struct thread_struct thread;
    struct fs_struct *fs;    //特定處理器下的狀态
    struct files_struct *files;    //檔案系統相關資訊結構體

    //打開檔案相關資訊結構體
    struct signal_struct *signal;
    struct sighand_struct *sighand;

    //松弛時間值,用來規定select()和epoll()的逾時時間,機關是納秒
    unsigned long timer_slack_ns;
    unsigned long default_timer_slack_ns;
};
           

程序辨別符

       程序辨別符(Process ID)是程序描述符中最重要的組成部分,用于辨別和對應唯一的程序。

       Linux核心使用了一個資料類型pid_t來存放程序辨別符,這個資料類型實質上是一個機器相關的無符号整數(類似于size_t)。PID通常被順序編号,且PID是可以重複使用的。當一個程序被回收之後,過一段時間,其辨別符又可以被再次使用。為了和16位處理器架構的應用系統相相容,在Linux核心上通常允許使用的程序辨別符是0~32767。

       在Linux中,有如下幾個特殊的程序辨別符所對應的程序:

       ·PID0:對應的是交換程序(swapper),實際上并不存在,其用于執行多程序的調用(子程序傳回0作為判斷父子程序的标志)。

       ·PID1:初始化程序(init),在自舉過程結束時由核心調用,其對應的檔案是/sbin/init,負責Linux的啟動工作,這個程序在系統運作過程中是不會終止的(守護程序),可以說目前作業系統中的所有程序都是由這個程序衍生而來的。

       ·PID2:可能對應頁守護程序(pagedaemon),用于虛拟存儲系統的分頁操作。

Linux程序的使用者

       與檔案類似的是,程序也有對應的實際使用者ID、實際組ID、有效使用者ID、有效組ID。對于這些使用者而言每個程序同樣存在一個相應的辨別符,Linux提供了相應的函數用于擷取這些辨別符,對其标準調用格式說明如下:

#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void);    //擷取實際使用者ID
uid_t geteuid(void);   //擷取有效使用者ID
gid_t getgid(void);    //擷取實際組ID
gid_t getegid(void);   //擷取有效組ID
           

Linux程序操作

建立程序

       POSIX标準定義了程序建立函數fork和vfork以建立一個新程序,被建立的新程序稱為目前執行該建立函數程序的子程序。

fork

       fork函數實質是一個系統調用,其作用是建立一個新的程序,當一個程序調用它完成後就出現兩個幾乎一模一樣的程序,其中由fork建立的新程序被稱為子程序,而原來的程序稱為父程序。子程序是父程序的一個拷貝,即子程序從父程序得到了資料段和堆棧段的拷貝,這些需要配置設定新的記憶體;而對于隻讀的代碼段,通常使用共享記憶體的方式通路。

       使用者通常在有如下需求的時候使用fork函數:

       ·一個程序希望複制自身,進而使得父子程序能同時執行不同段的代碼,通常來說這種應用會涉及網絡服務:父程序等待遠端的一個請求或應答,當收到這個請求或者應答的時候調用fork建立一個子程序來完成處理,而自己繼續等待遠端的請求或應答。

       ·程序想執行另外一個程式,例如在shell中調用使用者所生成的應用程式。

#include <unistd.h>

pid_t fork(void);
           

       fork函數沒有參數,它被調用一次,但是傳回兩次:

       ·對于父程序而言:函數的傳回值是子程序的程序辨別符(PID)。

       ·對于子程序而言:函數的傳回值是0。一個程序隻會有一個父程序,是以子程序總是可以調用getppid以獲得父程序的辨別符,是以不需要在這裡傳回父程序的程序辨別符。

       ·如果出錯,傳回值為“-1”。

資料空間共享

       當fork函數傳回後,子程序和父程序都從調用fork函數的下一條語句開始執行,但是父程序或子程序哪個先執行是随機的,這個取決于具體的排程算法。

       通常來說,fork所建立的子程序将會從父程序中拷貝父程序的資料空間、堆空間和棧空間,并且和父程序一起共享正文段,需要注意的是子程序所拷貝的僅僅是一個副本,和父程序的相應部分是完全獨立的。

vfork

       在使用fork函數建立一個程序後,可以不使用exec系列函數來執行新的程式,如果要執行新的程式則必須調用exec系列函數。在這種情況下可以使用vfork函數,該函數在建立完一個新的程序後自動實作exec系列函數的功能。

#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void); //子程序為0,父程序不為0
           

       fork與vfork之間的差別如下:

       ·fork要拷貝父程序的資料段,而vfork不需要完全拷貝父程序的資料段,在子程序沒有調用exec系列函數或exit函數之前,子程序與父程序共享資料段。

       ·vfork函數會自動調用exec系列函數去執行另一個程式。

       ·fork不對父子程序的執行次序進行任何限制,而在vfork中,子程序先運作,父程序挂起,直到子程序調用了exec系列函數或exit之後,父子程序的執行次序才不再有任何限制。

執行程序

       在Linux中可以調用fork函數來建立一個子程序,該子程序幾乎複制了父程序的全部内容,但是如果需要在子程序中執行一些自定義動作,則需要調用exec函數族。

       當調用exec系列函數的時候,該程序執行的程式被立即替換為新的程式,而新程式則從main函數開始執行,并用它來取代原調用程序的正文段、資料段、堆和棧,但其程序辨別符和程序描述符是不會改變的。

       在Linux中通常會在如下兩種情況下調用exec函數族:

       ·當程序不能再為系統和使用者做出任何貢獻時,就可以調用exec函數族讓自己“重生”。

       ·如果一個程序想執行另一個程式,那麼它就可以調用fork函數建立一個程序,然後調用exec函數族中的一個函數,這樣看起來就像通過執行應用程式而産生了一個正文段、資料段等都與其父程序不同的全新程序。

#include <unistd.h>

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 execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
           

       上述exec函數族的參數說明如下:

       ·path:可執行目标檔案的路徑名

       ·file:可執行目标檔案的檔案名

       ·arg:目标檔案的路徑名

       ·argv:一個字元指針數組,由它指出該目标程式使用的指令行參數表,按照約定第一個字元指針指向與path或file相同的字元串,最後一個指針指向一個空字元串,其餘的指向該程式執行時所帶的指令行參數。

       ·envp:與argv一樣也是一個字元指針數組,由它指出該目标程式執行時的程序環境,它也以一個空字元串結束。

       execl、execle、execlp這三個函數用于表示指令行參數的一般方式是:

char *arg0, char *arg1, ..., char *argn, (char *)0
           

退出程序

       當一個程序執行完成後必須要退出,退出時核心會進行一系列操作,包括釋放緩沖區等。通常來說Linux的應用程式代碼會調用exit系列函數來退出一個程序。

#include <stdlib.h>
#include <unistd.h>

void exit(int status);
void _exit(int status);
void _Exit(int status);
           

       exit系列函數沒有傳回值,其使用一個稱為終止狀态(exit status)的整形變量作為參數,Linux核心會對這個終止狀态進行檢查,當異常終止時,Linux核心會直接産生一個終止狀态字,描述異常終止的原因,可以通過wait或者waitpid函數來獲得終止狀态字。父程序也可以通過檢查終止狀态來獲得子程序的狀态。如果main函數的傳回值定義為整型并且main函數正常執行到最後一條語句傳回,則終止狀态是0。

       _exit與exit的差別:

       _exit:直接使程序停止運作,清除其占用的記憶體空間,并清除其在核心中的各種資料結構。

       exit:在_exit的基礎上做了一些包裝,在執行退出之前加了若幹道程式。如調用前要檢查檔案的打開情況,把檔案緩沖區中的内容寫回檔案(清理I/O緩沖)。

       當一個程序退出時,可能存在以下兩種狀态:

       ·其父程序恰好因忙于其他事務暫時不能接收子程序的終止狀态,如果此時子程序完全消失,那麼當父程序處理完其他事務想檢查子程序的情況時就沒有可用的資訊了。是以Linux核心為每個已退出的程序保留一定的資訊,一般至少包含程序辨別符、終止狀态字、程序處理器時間等資訊。父程序可以通過調用wait或waitpid得到相應的資訊,在此之後,Linux核心再将這些資料釋放。通常把這種已經結束,但其父程序尚未檢查其終止狀态的程序稱為僵屍程序。

       ·如果父程序可能先于子程序結束,此時init程序就會自動成為該子程序的父程序。

       由以上可知,當調用exit系列函數或者return函數傳回時,其實程序并沒有真正的完全消失,其還在繼續占用部分資源。如果這種僵屍程序過多,就會大大影響系統的性能。

銷毀程序

       當一個程序使用exit系列函數退出時,會在記憶體中保留部分資料以供父程序查詢,同時也會産生一個終止狀态字,然後Linux核心會發出一個SIGCHLD信号以通知父程序。因為子程序的結束對于父程序是異步的,是以這個SIGCHLD信号對于父程序也是異步的,父程序可以不響應。

       父程序對于退出後的子程序的預設狀态是不處理的,這樣會導緻系統中的僵屍程序浪費了系統資源,此時應該調用wait或waitpid函數對這些僵屍程序進行處理。

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
           

       在調用wait或waitpid之後可能存在如下三種情況:

       ·如果該父程序的所有子程序都還在運作,則阻塞父程序自身以等待子程序的運作結束。

       ·如果有一個子程序已經結束,則父程序取得該子程序的終止狀态,并且立即傳回。

       ·如果該父程序沒有任何子程序,則立即出錯傳回。

wait函數

       如果wait函數調用成功則傳回子程序的辨別符,如果失敗則傳回-1,其中參數status是一個整型指針,可以用于存放子程序的終止狀态,也可以定義為一個空指針。

       wiat函數與waitpid函數不同,在有一個子程序終止之前,wait函數讓父程序阻塞以等待子程序退出,而waitpid有一個參數可以讓父程序不阻塞。并且在一個父程序有多個子程序的情況下,如果其中有一個子程序退出則會傳回該子程序的程序辨別符。

wait函數傳回的宏

說明
WIFEXITED(status) 當子程序正常結束時傳回為真
WIFSIGNALED((status) 當子程序異常結束時傳回為真
WEXITSTATUS(status) 當WIFEXITED(status)為真時調用,傳回狀态字的低8位
WTERMSIG(status) 當WIFSIGNALED(status)為真時調用,傳回引起狀态終止的信号代号

waitpid函數

       在使用wait函數時,如果父程序的任何一個子程序傳回則wait函數傳回,而waitpid函數可以通過參數來指定需要等待的子程序。waitpid函數的參數pid用于對子程序進行相應的篩選:

       ·pid>0:隻等待PID為pid的子程序,不管其他已經有多少子程序結束退出了,隻要指定的子程序還沒有結束,waitpid就一直等待下去。

       ·pid=-1:等待任何一個子程序退出,沒有任何限制,此時waitpid等價于wait。

       ·pid<-1:等待一個指定程序組中的任何子程序,這個程序組的ID等于pid的絕對值。

       waitpid函數的參數options用于進一步控制waitpid函數的操作,其可以是0,也可以是WNOHANG和WUNTRACED兩個選項之一,或者是使用“|”符号連接配接的“或”操作。對這兩個選項的定義如下:

       ·WNOHANG:如果由pid指定的子程序并不是立即可用的,則waitpid函數不阻塞,此時傳回“0”。

       ·WUNTRACED:如果某實作支援作業控制,而由pid指定的任意子程序已經處于暫停狀态,并且未報告過,則傳回其狀态。

       總體而言,waitpid函數提供了wait函數所沒有的三個功能:

       ·能夠等待一個指定的程序結束。

       ·能夠不阻塞父程序獲得子程序的狀态。

       ·支援作業控制。

繼續閱讀