天天看點

Linux 系統應用程式設計——程序基礎

一、Linux下多任務機制的介紹

         Linux有一特性是多任務,多任務處理是指使用者可以在同一時間内運作多個應用程式,每個正在執行的應用程式被稱為一個任務。

         多任務作業系統使用某種排程(shedule)政策(由核心來執行)支援多個任務并發執行。事實上,(單核)處理器在某一時刻隻能執行一個任務。每個任務建立時被配置設定時間片(幾十到上百毫秒),任務執行(占用CPU)時,時間片遞減。作業系統會在目前任務的時間片用完時排程執行其他任務。由于任務會頻繁地切換執行,是以給使用者多個任務運作的感覺。是以可以說,多任務由“時間片 + 輪換”來實作。多任務作業系統通常有三個基本概念:任務、程序和線程,現在,我們先學習程序:

程序的基本概念

        程序是指一個具有獨立功能的程式在某個資料集合上的動态執行過程,它是作業系統進行資源配置設定和排程的基本單元。簡單的說,程序是一個程式的一次執行的過程。

        程序具有并發性、動态性、互動性和獨立性等主要特性。

        程序和程式是有本質差別的:

1)程式( program )是一段靜态的代碼,是儲存在非易失性存儲器(磁盤)上的指令和資料的有序集合,沒有任何執行的概念;

2)程序( process )是一個動态的概念,它是程式的一次執行過程(在RAM上執行),包括了動态建立、排程、執行和消亡的整個過程,它是程式執行和資源管理的最小機關。

Linux 系統應用程式設計——程式基礎

這裡,我們可以看到,程序由兩部分組成:記憶體位址空間 + task_struct ,task_struct 下面我們會講到,記憶體位址空間就是我們程式在記憶體中運作(程序)時所開辟的4GB虛拟位址空間,用于存放代碼段、資料段、堆、棧等;

從作業系統的角度看,程序是程式執行時相關資源的總稱。當程序結束時,所有資源被作業系統回收。

         Linux系統中主要包括下面幾種類型的過程:

1)互動式程序;

2)批處理程序;

3)守護程序;

Linux下的程序結構

       程序不但包括程式的指令和資料,而且包括程式計數器和處理器的所有寄存器以及存儲臨時資料的程序堆棧。

       因為Linux是一個多任務的作業系統,是以其他的程序必須等到作業系統将處理器的使用權配置設定給自己之後才能運作。當正在運作的程序需要等待其他的系統資源時,Linux核心将取得處理器的控制權,按照某種排程算法将處理器配置設定給某個等待執行的程序。

        在上面介紹程式和程序的差別時,我們看到程序除了記憶體位址空間以外,還有個結構體task_struct,核心将所有程序存放在雙向循環連結清單(程序連結清單)中,連結清單的每一項就是這個結構體task_struct,稱為程序控制塊的結構。該結構包含了與一個程序相關的所有資訊,在linux核心目錄下<include / Linux / sched.h>檔案中定義。task_struct核心結構比較大,它能完整地描述一個程序,如程序的狀态、程序的基本資訊、程序标示符、記憶體的相關資訊、父程序相關資訊、與程序相關的終端資訊、目前工作目錄、打開的檔案資訊,所接收的信号資訊等。

下面詳細講解task_struct結構中最為重要的兩個域:stat (程序狀态) 和 pid (程序标示符)。

1、程序狀态

Linux中的程序有以下幾種主要狀态:運作狀态、可中斷的阻塞狀态、不可中斷的阻塞狀态、暫停狀态、僵死狀态、消亡狀态,它們之間的轉換關系如下:

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、硬體通過觸發信号,導緻核心調用中斷處理程式,進入核心空間。這個過程中,硬體的一些變量和參數也要傳遞給核心,核心通過這些參數進行中斷處理。所謂的“中斷上下文”,其實也可以看作就是硬體傳遞過來的這些參數和核心需要儲存的一些其他環境(主要是目前被打斷執行的程序環境)。

   一個程式我們可以從兩種角度去分析。其一就是它的靜态結構,其二就是動态過程。下圖表示了使用者态和核心态直接的關系(靜态的角度來觀察程式)

Linux 系統應用程式設計——程式基礎

二、程序程式設計

1、fork() 函數

        在Linux 中建立一個新程序的方法是使用fork() 函數。fork() 函數最大的特性就是執行一次傳回兩個值。

函數原型如下:

所需頭檔案

#include <sys/types.h> //提供類型pid_t定義

#include <unistd.h>

函數原型

pid_t  fork(void)

函數傳回值

0 :子程序

子程序PID(大于0的整數):父程序

-1 :出錯

fork() 函數用于從已存在的程序中建立一個新程序。新程序稱為子程序,而原程序稱為父程序。具體fork()函數究竟做了什麼,我們先看這張圖:

Linux 系統應用程式設計——程式基礎

這裡我們可以看到,使用fork () 函數得到的子程序是父程序的一個複制品,它從父程序處繼承了整個程序的位址空間(注意:子程序有其獨立的位址空間,隻是複制了父程序位址空間裡的内容),包括程序上下文、代碼段、程序堆棧、記憶體資訊、打開的檔案描述符、信号處理函數、程序優先級等。而子程序所獨有的隻是它的程序号、資源使用和計時器等。

      因為子程序幾乎是父程序的完全複制,是以父子程序會運作同一個程式,這裡,兩個程序都會從PC位置往下執行;如何區分它們呢?父子程序一個很重要的差別是, fork()傳回值不同。父程序中傳回值是子程序的程序号,而子程序中傳回0;是以在上圖中,兩個程序會通過判斷PID來選擇執行的語句。

       注意:子程序沒有執行fork() 函數,而是從fork() 函數調用的下一條語句開始執行。

下面,寫一個fork()程式,來加深對fork()的了解:

Linux 系統應用程式設計——程式基礎

#include <stdio.h>  

#include <stdlib.h>  

#include <unistd.h>  

#include <sys/types.h>  

int global = 22;  

int main(void)  

{  

    int test = 0,stat;  

    pid_t pid;  

    pid = fork();  

    if(pid < 0)  

    {  

        perror("fork");  

        return -1;  

    }  

    else if(pid == 0)  

        {  

            global++;  

            test++;  

            printf("global = %d test = %d Child,my PID is %d\n",global,test,getpid());  

            exit(0);  

        }  

        else  

            global += 2;  

            test += 2;  

            printf("global = %d test = %d Parent,my PID is %d\n",global,test,getpid());  

}  

執行結果如下:

Linux 系統應用程式設計——程式基礎

從結果我們可以發現幾個問題:

1)最後一行光标在閃,是程式沒執行完嗎?第三行中子程序列印前是bash,這是什麼原因呢?

其實我們這裡執行的程式中有三個程序在執行:父程序、子程序、bash。從列印結果中我們可以看到父程序先執行完,然後是bash ,最後子程序執行完,這裡的光标其實是bash的。是以,我們可以發現:父程序、子程序誰先運作時不可知的,誰先運作有核心排程來确定;

2)從列印結果中,可以看出父子程序列印出了各自的程序号和對應變量的值,顯然global和test在父子程序間是獨立的,其各自的操作不會對對方的值有影響;

2、exec 函數族

        fork() 函數用于建立一個子程序,該子程序幾乎指派了父程序的全部内容。我們能否讓子程序執行一個新的程式呢?exec 函數族就提供了一個在程序中執行裡一個程式的辦法。它可以根據指定的檔案名或目錄找到可執行檔案,并用它來取代目前程序的資料段、代碼段和堆棧段。在執行完之後,目前程序除了程序号外,其他的内容都被替換掉了。是以,如果一個程序想執行另一個程式,那麼它就可以調用fork() 函數建立一個程序,然後調用exec家族中的任意一個函數,這樣看起來就像執行應用程式而産生了一個新程序。

      Linux 中并沒有exec() 函數,而是有6個以 exec 開頭的函數,下面是函數文法:

#include <stdio.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 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[] 中傳遞目前程序所使用的環境變量;

Linux 系統應用程式設計——程式基礎

exev使用示例:

Linux 系統應用程式設計——程式基礎

int main()  

//調用execlp 函數,相當于調用了 "ps -ef"指令  

    if(execlp("ps","ps","-ef",NULL) < 0) //這裡"ps"為filename "ps" 為argv[0] "-ef"為argv[1],NULL為參數結束标志  

        perror("execlp error");  

    return 0;  

Linux 系統應用程式設計——程式基礎

fs@ubuntu:~/qiang/process/exec$ ./execlp   

UID        PID  PPID  C STIME TTY          TIME CMD  

root         1     0  0 13:48 ?        00:00:01 /sbin/init  

root         2     0  0 13:48 ?        00:00:00 [kthreadd]  

...  

root      5300     2  0 20:49 ?        00:00:00 [kworker/0:2]  

root      5351     2  0 20:54 ?        00:00:00 [kworker/0:0]  

fs        5371  2797  0 20:56 pts/0    00:00:00 ps -ef  

fs@ubuntu:~/qiang/process/exec$   

如果我們使用execvp,則

Linux 系統應用程式設計——程式基礎

char *argv[] = {"ps","-ef",NULL};  

execvp("ps",argv);  

3、exit() 和_exit()

1) exit()和_exit()函數說明

   exit()和_exit() 都是用來終止程序的。當程式執行到exit()或_exit() 時,程序會無條件的停止剩下的所有操作,清除各種資料結構,并終止本程序的運作。但是,這兩個函數是有差別的:

Linux 系統應用程式設計——程式基礎

可以看出exit() 是庫函數,而_exit() 是系統調用;

_exit() 函數的作用最為簡單:直接使程序終止運作,清除其使用的記憶體空間,并銷毀其在核心中的各種資料結構;exit() 函數則在這些基礎上作了一些包裝,在執行退出之前加了若幹道工序。

二者函數描述如下:

exit()  :#include <stdlib.h>

_exit():#include <unistd.h>

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 <sys/types.h>

#includ <sys/wait.h>

pid_t wait(int *status)

函數參數

status是一個整型指針,指向的對象用來儲存子程序退出時的狀态

status 若為空,表示忽略子程序退出時的狀态;

status 若不為空,表示儲存子程序退出時的狀态;

另外,子程序的結束狀态可有Linux 中的一些特定的宏來測宏。

成功:子程序的程序号

失敗: -1

附:檢查wait 所傳回的終止狀态的宏

WIFEXTED (status) :若為正常終止子程序傳回的狀态,則為真。對于這種情況可執行 WEXITSTATUS (status) ,取子程序傳送給exit 參數的低八位;

(首先判斷子程序是否正常死亡,異常死亡是不會運作到exit()的,這時分析status 是無意義的;)

wait() 會回收任一一個先死亡的子程序;

下面看一個程式,wait() 與exit()的使用

Linux 系統應用程式設計——程式基礎

#include <sys/wait.h>  

int main(int argc, char **argv)  

    printf("parent[pid=%d] is born\n", getpid());  

    if (-1 == (pid = fork())) {  

        perror("fork error");  

    if (pid == 0){  

        printf("child[pid=%d] is born\n", getpid());  

        sleep(20);  

        printf("child is over\n");  

        exit(123); //return 123;   

    else{  

        pid_t pid_w;  

        int status;  

        printf("parent started to wait...\n");  

        pid_w = wait(&status);  

        printf("parent wait returned\n");  

        if (pid_w < 0) {  

            perror("wait error");  

            return 1;  

        if (WIFEXITED(status)) {   

            status = WEXITSTATUS(status);  

            printf("wait returns with pid = %d. return status is %d\n", pid_w, status);  

        } else {  

            printf("wait returns with pid = %d. the child is terminated abnormally\n", pid_w);  

//      while(1);  

        printf("father is over\n");  

        return 0;  

Linux 系統應用程式設計——程式基礎

fs@ubuntu:~/qiang/process/wait$ ./wait  

parent[pid=5603] is born  

parent started to wait...  

child[pid=5604] is born  

child is over  

parent wait returned  

wait returns with pid = 5604. return status is 123  

father is over  

fs@ubuntu:~/qiang/process/wait$   

waitpid() 函數

       waitpid() 函數和wait() 的作用是完全相同的,但waitpid 多出了兩個可有使用者控制的參數pid 和 options,進而為使用者程式設計提供了一種更為靈活的方式。waitpid 可以用來等待指定的程序,可以使程序不挂起而立刻傳回。

wait(&status);等價于waitpid(-1, &status, 0);

其函數類型如下:

#include <sys/types.h> /* 提供類型pid_t的定義 */

#include <sys/wait.h>

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   : 出錯

示例如下:

Linux 系統應用程式設計——程式基礎

    pid_t pc, pr;  

    pc = fork();  

    if(pc < 0) /* 如果fork出錯 */  

        printf("Error occured on forking\n");  

    else if(pc == 0) /* 如果是子程序 */  

        sleep(10); /* 睡眠10秒 */  

        exit(0);  

    /* 如果是父程序 */  

    do{  

        pr = waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG參數,waitpid不會在這裡等待 */  

        if(pr == 0) /* 如果沒有收集到子程序 */  

            printf("No child exited\n");  

            sleep(1);  

    }while(pr == 0); /* 沒有收集到子程序,就回去繼續嘗試 */  

    if(pr == pc)  

        printf("successfully get child %d\n", pr);  

    else  

        printf("some error occured\n");  

Linux 系統應用程式設計——程式基礎

fs@ubuntu:~/qiang/wait$ ./waitpid   

No child exited  

successfully get child 17144  

fs@ubuntu:~/qiang/wait$  

父程序經過10次失敗的嘗試之後,終于收集到了退出的子程序。