APUE學習記錄——進程環境

進程相關概念

1、什麼是進程?
  通俗的說,進程是一個具有一定獨立功能的程序的一次運行活動。,對於Linux這種多任務操作系統來說,每一個運行者的程序就構成一個進程,可以用cat /proc/sys/kernel/pid_max命令查看系統支持的最大進程數,我在Ubuntu14.04中得到的結果是32768。
2、進程與程序的區別與聯繫
(1)進程是動態的,程序是靜態的
(2)一個進程只對應一個程序,一個程序可以對應多個進程;
(3)進程的生命週期是有限的,而程序則可以永久保存與磁盤中。
3、進程的特點:動態性、併發性、獨立性、異步性
動態性:由進程的概念可知,程序運行起來纔是進程,所以具有動態性
併發性:就是說在同一時間可以同時執行多個進程,這叫做併發性
獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源調度的獨立單位;
異步性:所謂異步性是指進程以不可預知的速度向前推進。每個進程何時執行,何時暫停,以怎樣的速度向前推進,何時完成等,都是不可預知的。
4、進程的三態——就緒、執行、阻塞
進程的三態
         圖一:進程執行過程的三態
進程執行過程:  
  如圖一所示,進程A被創建後進入就緒態,通過進程調度佔用CPU進入執行態,用完時間片後回到就緒態等待下一次被調度;
  在執行過程中如果遇到I/O請求(如訪問磁盤、文件)且得不到滿足(如磁盤、文件正在被進程B佔用),則進程A進入阻塞態,CPU被讓出來給其他進程使用,待B進程使用完磁盤、文件後,A進程便解除阻塞狀態去執行I/O操作,但由於進入阻塞時讓出了CPU,所以I/O完成後A進程只能回到就緒狀態了。

5、進程互斥、臨界資源、臨界區
進程互斥:指若干進程都要使用某一資源,但該資源在同一時刻最多允許一個進程使用(臨界資源),這時其他進程必須等待,直到佔用該資源者釋放了該資源爲止。
臨界資源:操作系統中將同一時刻只允許一個進程訪問的資源稱爲臨界資源。
臨界區:進程中訪問臨界資源的那段程序代碼稱爲臨界區。爲實現對臨界資源的互斥訪問,應保證諸進程互斥地進入各自的臨界區。
6、進程同步
  一組進程按一定的順序執行的過程稱爲進程間的同步,具有同步關係的這組進程稱爲合作進程。
7、進程調度
  在任何時刻,一個CPU上運行的進程只能有一個,但系統中就緒的進程有很多,所以操作系統系統按一定算法,從一組待運行的進程中選出一個來佔有CPU運行,這就是進程調度。
  常見的調度算法有:先來先服務、短進程優先、高優先級優先、時間片輪轉法
8、調度時機
  按調度時機,調度分爲搶佔式調度非搶佔式調度
  舉個例子,A、B兩個進程,A優先級高B優先級低,B進程正在運行,此時A進程請求佔用CPU,由於A的優先級高,搶佔式調度會強制使B進程結束使A進程運行,而非搶佔式調度會等B進程執行完畢再使A進程執行。

進程環境

1、main()函數與命令行參數

int main(int argc, char *argv[]);

  argc是命令行參數的數目,argv是指向各個命令行參數的指針構成的指針數組,其中,argv[0]一定指向程序名,argv[argc]是一個空指針。
  我們在使用 shell 命令的時候經常使用各種參數, 如gcc main.c -o main、ls -l,shell可以把這些參數傳遞給程序,具體就是傳遞給main函數的argc和argv了。
  舉一個例子,實現shell的echo命令(PS:shell的echo並不回顯argv[0])
  

/*
**程序說明:實現echo命令——將命令行參數回顯到標準輸出
*/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int i = 0;

    if (argc < 2)
    {
        printf("\n");
        exit(0);
    }

    for (i = 1; argv[i] != NULL; i++)
    /*或者for (i = 1; i < argc; i++)*/
        printf("%s ", argv[i]);

    printf("\n");

    exit(0);
}

  運行結果,說明命令行參數的確傳遞給了main函數。
這裏寫圖片描述
2、進程終止(termination)
  Linux系統有8種方式可以使進程終止,其中5種正常終止,3種異常終止。
  5種正常終止:
  (1)從main函數返回
  (2)調用exit
  (3)調用_exit或_Exit
  (4)最後一個線程從其啓動例程返回
  (5)從最後一個線程調用pthread_exit
  3種異常終止:
  (6)調用abort
  (7)接到一個信號
  (8)最後一個線程對取消請求做出響應
  我們暫且只討論前三種,其他的等學到信號和線程時再說。
  (1)從main函數返回:在main()中使用return語句返回一個int值給調用者,返回0表示正常終止,其他表示異常終止。
  在說明(2)(3)之前我們先看一下這三個函數的原型:
  

#include <stdlib>
void exit(int status);
void _Exit(int status);

#include <unistd.h>
void _exit(int status);

  首先,_exit和_Exit是系統調用,會立即結束程序;調用exit會先執行一些處理操作(如調用終止處理程序、沖洗I/O流等等),然後調用_exit或_Exit結束程序;
  其次,三個函數都帶有一個整形參數status,我們稱其爲退出狀態(exit status),取值範圍爲0-255,每一個取值都對應着一種退出狀態;
  最後,在main函數中調用exit(ststus)等價於調用return status。
3、atexit

#include <stdlib.h>
int atexit(void (*func)(void));

               /*返回值:若成功,返回0;若失敗,返回非0*/

  用atexit註冊過的函數會在調用exit時以註冊順序的逆序被調用,被註冊的函數稱爲“終止處理程序”(exit handler)
  “註冊”這個詞語聽起來可能不太好懂,其實很簡單啦,就是把終止處理程序的函數名(也就是函數地址)作爲參數傳遞給atexit,但是,終止處理程序必須是無參無返回值的
  舉個例子說明順序註冊逆序調用是怎麼一回事:
  

/*
**程序說明:使用atexit函數
*/

#include <stdio.h>
#include <stdlib.h>

/*
**聲明兩個終止處理函數my_atexit1和my_atexit2
*/
static void my_atexit1(void);
static void my_atexit2(void);

int main(int argc, char *argv[])
{

    if (atexit(my_atexit2) != 0)  /*這裏只是註冊,並不執行終止處理程序*/
        printf("can't register my_atexit2\n");
    else 
        printf("register my_atexit2 first time done\n");

    if (atexit(my_atexit1) != 0)
        printf("can't register my_atexit1\n");
    else 
        printf("register my_atexit1 first time done\n");

    if (atexit(my_atexit1) != 0)
        printf("can't register my_atexit1\n");
    else 
        printf("register my_atexit1 second time done\n");  

    printf("main is done and before exit\n");

    exit(0);
}

static void my_atexit1(void)
{
    printf("my_atexit1\n");
}

static void my_atexit2(void)
{
    printf("my_atexit2\n");
}

  編譯並運行結果,大家應該明白了吧!
這裏寫圖片描述
  注意終止處理程序每註冊一次,就會被調用一次,my_exit1註冊了兩次,所以有兩條打印語句。
4、環境表與環境變量
  每個程序都收到一張環境表,環境表是一個字符指針數組,其每個元素都指向一個形如name=value的環境字符串,而全局變量extern char **environ又指向環境表這個字符指針數組,environ叫做環境指針。這樣,環境指針、環境表、環境字符串構成了環境,如圖二所示:
  這裏寫圖片描述
                圖二 由5個環境字符串組成的環境
  1)用environ指針查看整個環境,可以用如下方式:  

#include <stdio.h>

extern char **environ;  /*全局變量,環境指針*/

int main()
{
    int i;

    /*
    **查看整個環境,必須使用environ指針
    */
    for(i = 0; environ[i] != NULL; i++)
        printf("environ[%d]: %s\n", i, environ[i]);

    return 0;
}

運行部分結果:
這裏寫圖片描述
  2)getenv從環境中取名字爲name的環境變量的值,返回指向value的指針

#include <stdlib.h>
char *getenv(const char *name);

          /*返回值:若找到,返回與name關聯的value指針;否則,返回NULL*/

  3)putenv、setenv、unsetenv修改環境變量

#include <stdlib.h>
int putenv(char *str);
             /*返回值:若成功,返回0;若失敗,返回非0*/
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
            /*兩個函數返回值:若成功,返回0;若失敗,返回-1*/

putenv:用“name=value”形式的字符串,添加或修改環境表,如果name已存在,則先刪除原來定義,然後用新值替換,如putenv(“PATH=/usr/apue”);

setenv:將name設置爲value,這裏分兩種情況:
a) name不存在,直接添加新的環境變量。
b) name已存在:若rewrite爲真,就用 value 覆蓋 name 原來的值;若rewrite爲假,則保留 name 原來的值。

unsetenv:刪除name的定義,若name不存在也不算出錯。
2)、3)部分測試代碼:

#include <stdio.h>
#include <stdlib.h>

int main()
{      
    /*
    **查看指定環境變量的值
    */  
    printf("HOME = %s\n", getenv("HOME"));  
    printf("PATH = %s\n", getenv("PATH"));        
    printf("PWD = %s\n", getenv("PWD"));

    /*
    **修改環境變量
    */
    putenv("PWD=/home/");                     /*注意=兩邊不能有空格*/
    printf("PWD = %s\n", getenv("PWD"));      /*putenv直接用新值替換*/ 

    setenv("PATH", "/user/local/bin", 0);
    printf("PATH = %s\n", getenv("PATH"));    /*rewrite=0,不用新值替換*/ 

    setenv("PATH", "/user/local", 1);
    printf("PATH = %s\n", getenv("PATH"));    /*rewrite=1,用新值替換*/

    unsetenv("PWD");
    if (!getenv("PWD"))
        printf("PWD is deleted\n");           /*PWD環境變量被刪除*/

    return 0;
}

運行結果:
這裏寫圖片描述
  關於修改環境變量要注意一點,我們所做的修改只在當前進程和修改之後產生的子進程有效,而不能影響父進程(通常爲shell)的環境,也就是說你這個程序運行結束,你做的修改就全部無效,這是用env命令查看可發現所有的環境變量都已回到初始的樣子。  
5、C程序的存儲空間佈局 
一個C程序從低地址到高地址依次是正文段(代碼段、text段)、初始化數據段(data段)、未初始化數據段(bss段)、堆(heap)、棧(stack)這幾個部分組成,一種典型的存儲空間佈局如圖三所示:
這裏寫圖片描述
              圖三 典型的存儲空間安排
正文段(text段):通常是可共享的、只讀的,對於32位Intel x86處理器的Linux,正文段一定從0x08048000開始。代碼塊外const修飾的只讀變量存放在此位置。
初始化數據段(data段):存放已初始化的變量,有兩張情況:
a)代碼塊外已初始化的變量
b)代碼塊內用static修飾的已初始化變量
未初始化數據段(bss段):存放未初始化數據,也有兩種情況:
a)代碼塊外未初始化的變量
b)代碼塊內用static修飾的未初始化變量
堆(heap):我們經常用malloc、calloc進行動態存儲分配,分配的空間就在堆中進行。
棧(stack):存放代碼塊內沒有用ststic修飾的自動變量等數據,代碼塊內const修飾的只讀變量也存放在棧中。
  以上各個段都存放哪些數據大家一定要牢記,最好是在理解了const和static的基礎上記憶,效果更好。
  既然提到了const和static,那就順便複習下這兩個關鍵字(PS:這兩個關鍵字的含義在可是經常出現在面試中哦
  const的作用:
  1)const是作爲類型的附加修飾符來使用的,用const修飾的變量必須在聲明時初始化,而且該變量是隻讀的,既它的值不能被更改;
  2)提高代碼可讀性和可維護性。關鍵字const可以爲讀代碼的人傳達非常有用的信息,可以讓讀者很清楚的知道這些變量的值不能被修改,同時也爲代碼的維護提供了方便。
  3)const也提供給編譯器一些有用的信息,一旦不小心修改了這些變量的值,編譯器會報錯。
  static的作用:
  在C語言中:
  1)static用於所有函數體外的全局變量聲明,表明該變量只能在聲明它的源文件中使用;
  2)static用於函數的定義,表明該函數只能在聲明它的源文件中使用;
  3)static用於函數體內的局部變量聲明,其作用域爲該函數體,表明該變量的內存只在第一次調用該函數時分配一次,其值在下次調用時仍維持上次的值,而且一直到整個程序運行結束才銷燬。
  在C++中除了上面3條,還有下面2條:
  4)在類中的static成員變量屬於整個類所擁有,對類的所有對象只有一份拷貝;
  5)在類中的static成員函數屬於整個類所擁有,這個函數不接收this指針,因而只能訪問類的static成員變量。
  堆向高內存地址生長,棧向低內存地址生長,堆頂和棧底之間有一塊很大的未用空間,叫做虛地址空間,當堆和棧的空間不夠用時,就分別從兩個方向向其擴展。
6、存儲空間分配 
  和存儲空間分配相關的函數:

#include <stdlib.h>

/*分配指定字節數的存儲區,存儲區中初始值不確定*/
void *malloc(size_t size);

/*
**爲指定數量指定長度的對象分配空間,
**空間中每一位都初始化爲0。
**但不保證此0與空指針的NULL或浮點數0相同
*/
void *calloc(size_t nobj, size_t size);

/*增加或減少以前分配區(ptr指向的地址)的長度,新增區域初始值不確定*/
void *realloc(void *ptr, size_t newsize);

/*申請的空間用完後必須釋放,否則會造成內存泄露*/
void free(void *ptr);

7、函數setjmp和longjmp 
  這兩個函數的功能是跨越函數(或者說跨越棧幀)進行跳轉,它們在處理深層次嵌套函數的返回是非常有用的。
  舉個例子:假如有1000個嵌套的for循環,在最後一層嵌套中出錯要返回,正常情況需要一層一層的返回,這顯然是費時的,而有了這兩個函數,我們就可直接跳轉到最外層for循環之外,是不是很方便呢?
  讓我們看下這兩個函數的原型:
  

#include <setjmp.h>
int setjmp(jmp_buf env);
             /*返回值;若直接調用,返回0;若從longjmp返回,則爲非0*/
void longjmp(jmp_buf env, int val);

  這兩個函數是相互配合使用的,我們一般先調用setjmp這個函數來設置跳轉點,此時函數返回值是0,它的參數env是一個特殊類型jump_buf,通常定義爲全局變量。
  調用longjmp可以跳轉到setjmp所在的位置,此時setjmp會再執行一次,但這次返回值就不是0了,而是longjmp的第二個參數val,val的值可以自己設置,它的作用是判斷究竟是從哪個位置跳轉回來的,所以 setjmp下面一般跟着一組if…else if或者switch來根據不同的返回值做不同的處理。
  特殊的,如果val = 0,則 setjmp的返回值是 1,這樣規定是爲了避免跳轉出現死循環。
  舉個例子:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static jmp_buf jmpbuffer;
int ret;

static void f1();
static void f2();
static void f3();
static void f4();

int main()
{
    ret = setjmp(jmpbuffer);   //設置跳轉點

    if (ret == 0)  //第一跳
    {
        printf("first jump: from main()\n");
        printf("Begin: main() call f1()\n");   
        f1();      
    }   
    else           //第二跳
    {
        printf("second jump: from f%d()\n", ret);
    }

    return 0;
}

static void f1()
{
    printf("Begin: f1() call f2()\n");
    f2();
    printf("End: f1 call f2()\n");
}

static void f2()
{
    printf("Begin: f2()() call f3()\n");
    f3();
    printf("End: f2 call f3()\n");
}

static void f3()
{
    printf("Begin: f3()() call f4()\n");
    f4();
    printf("End: f3 call f4()\n");
}

static void f4()
{
    longjmp(jmpbuffer, 4);
}

運行結果:
這裏寫圖片描述


結尾:進程的相關概念和進程環境暫且大概就是這些了,目前我的理解還比較淺,還寫不出多麼深刻的內容,也只能把書上的知識加以總結,以後有了更深的理解後,再來對這篇博客內容進行修改。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章