C++並行編程

一、信號量

在學習信號量之前,我們必須先知道——Linux提供兩種信號量:

內核信號量,由內核控制路徑使用

用戶態進程使用的信號量,這種信號量又分爲POSIX信號量和SYSTEM V信號量。

POSIX信號量又分爲有名信號量和無名信號量 
有名信號量,其值保存在文件中, 所以它可以用於線程也可以用於進程間的同步。無名信號量,其值保存在內存中。

1.1 無名信號量接口函數

信號量的函數都以sem_開頭,線程中使用的基本信號量函數有4個,它們都聲明在頭文件semaphore.h中。

sem_init函數
該函數用於創建信號量,其原型如下:

sem_t  sem //sem信號量

下面傳輸的即爲&sem

int sem_init(sem_t *sem,int pshared,unsigned int value);


該函數初始化由sem指向的信號對象,設置它的共享選項,並給它一個初始的整數值。 
pshared控制信號量的類型,如果其值爲0,就表示這個信號量是當前進程的局部信號量,否則信號量就可以在多個進程之間共享,value爲sem的初始值。調用成功時返回0,失敗返回-1.

1.2 有名信號量接口函數

int sem_getvalue(sem_t *sem, int *sval);

sem_getvalue() 把 sem 指向的信號量當前值放置在 sval 指向的整數上。 如果有一個或多個進程或線程當前正在使用 sem_wait 等待信號量,POSIX.1-2001 允許返回兩種結果在 sval 裏:要麼返回 0;要麼返回一個負值,它的絕對等於當前正在 sem_wait裏阻塞的進程和線程數。Linux 選擇了前面的行爲(返回零)。

 

二、進程

2.1  fork()函數

 函數fork()

    所需頭文件:#include<sys/types.h>

                        #include<unistd.h>

    函數原型:pid_t fork()

    函數參數:無

    函數返回值:

          0    子進程

        >0    父進程,返回值爲創建出的子進程的PID

         -1    出錯

 

以發現子進程和父進程之間並沒有對各自的變量產生影響。

一般來說,fork之後父、子進程執行順序是不確定的,這取決於內核調度算法。進程之間實現同步需要進行進程通信

vfork與fork對比:

相同:

返回值相同

不同:

fork創建子進程,把父進程數據空間、堆和棧複製一份;vfork創建子進程,與父進程內存數據共享

vfork先保證子進程先執行,當子進程調用exit()或者exec後,父進程才往下執行

爲什麼需要vfork?

因爲用vfork時,一般都是緊接着調用exec,所以不會訪問父進程數據空間,也就不需要在把數據複製上花費時間了,因此vfork就是”爲了exec而生“的。

結束子進程的調用是exit()而不是return,如果你在vfork中return了,那麼,這就意味main()函數return了,注意因爲函數棧父子進程共享,所以整個程序的棧就跪了。

2.2 exec函數族

目的:讓子進程不執行父進程正在執行的程序

exec函數族提供了讓進程運行另一個程序的方法。exec函數族內的函數可以根據指定的文件名或目錄名找到可執行程序,並加載新的可執行程序,替換掉舊的代碼區、數據區、堆區、棧區與其他系統資源。這裏的可執行程序既可以是二進制文件,也可以是腳本文件。在執行exec函數族函數後,除了該進程的進程號PID,其他內容都被替換了。
-----------

 所需頭文件:#include<unistd.h>

    函數原型:

        //excel("/bin/ps","ps","-ef",NULL)
        //系統執行ps -ef,注意參數的寫法
        int execl(const char *path, const char *arg,…)


        //execlp("ps","ps","-ef",NULL)
        //第一個參數只需要寫ps即可,系統會根據環境變量自行尋找ps程序的位置
        int execlp(const char *file, const char *arg,…)
          
          
        //execle()函數將一個新的環境變量添加到子進程中
        // char *envp[]={"PATH=/tmp","USER=liyuge",NULL};
        //設定新的環境變量,注意使用NULL結尾
        //execle("/usr/bin/env","env",NULL,envp)

        int execle(const char *path, const char *arg,…, char *const envp[])

       

        //char *arg[]={"ps","-ef",NULL};execvp("ps",arg)
        //注意參數與execlp()函數的區別
        int execv(const char *path, char *const argv[])



        int execvp(const char *file, char *const argv[])




        //char *arg[]={"env",NULL};//設定參數向量表,注意使用NULL結尾
        //char *envp[]={"PATH=/tmp","USER=liyuge",NULL};
        //設定新的環境變量,注意使用NULL結尾
 
        int execve(const char *path, char *const argv[], char *const envp[])

        execl(完整的路徑名,列表……);

        execlp(文件名,列表……);

        execle(完整的路徑,列表……,環境變量的向量表)

 

        execv(完整的路徑名,向量表);

        execvp(文件名,向量表);

        execve(完整的路徑,向量表,環境變量的向量表)    //系統調用函數

    函數參數:

        path:文件路徑,使用該參數需要提供完整的文件路徑

        file:文件名,使用該參數無需提供完整的文件路徑,終端會自動根據$PATH的值查找文件路徑

        arg:以逐個列舉方式傳遞參數/命令行參數

        argv:以指針數組方式傳遞參數

        envp:環境變量數組

返回值:-1(通常情況下無返回值,當函數調用出錯纔有返回值-1)

區別1:參數傳遞方式(函數名含有l還是v)

exec函數族的函數傳參方式有兩種:逐個列舉指針數組

    若函數名內含有字母'l'(表示單詞list),則表示該函數是以逐個列舉的方式傳參,每個成員使用逗號分隔,其類型爲const char *arg,成員參數列表使用NULL結尾

    若函數名內含有字母'v'(表示單詞vector),則表示該函數是以指針數組的方式傳參,其類型爲char *const argv[],命令參數列表使用NULL結尾

區別2查找可執行文件方式(函數名是否有p

函數名內沒有字母'p',則形參爲path,表示我們在調用該函數時需要提供可執行程序的完整路徑信息

 若函數名內含有字母'p',則形參爲file,表示我們在調用該函數時只需給出文件名,系統會自動按照環境變量$PATH的內容來尋找可執行程序

區別3:是否指定環境變量(函數名是否有e

exec可以使用默認的環境變量,也可以給函數傳入具體的環境變量。其中:

    若函數名內沒有字母'e',則使用系統當前環境變量

    若函數名內含有字母'e'(表示單詞environment),則可以通過形參envp[]傳入當前進程使用的環境變量

 

exec函數族簡單命名規則如下:

    後綴    能力

    l        接收以逗號爲分隔的參數列表,列表以NULL作爲結束標誌

    v        接收一個以NULL結尾的字符串數組的指針

    p        提供文件的完整的路徑信息 或 通過$PATH查找文件

    e        使用系統當前環境變量 或 通過envp[]傳遞新的環境變量

 

execl("/bin/ps","ps","-ef",NULL) //子進程執行命令行參數ps -ef,注意參數的寫法,且需要使用NULL結尾

 

運行該程序會發現,子進程會運行ps -ef命令,這與我們在終端直接輸入ps -ef得到的結果是相同的。

注意我們在調用exec函數族的函數時,一定要加上錯誤判斷語句。當exec函數族函數執行失敗時,返回值爲-1,並且報告給內核錯誤碼,我們可以通過perror將這個錯誤碼的對應錯誤信息輸出。常見的exec函數族函數執行失敗的原因有:

    1.找不到文件或路徑

    2.參數列表arg、數組argv和環境變量數組列表envp未使用NULL指定結尾

    3.該文件沒有可執行權限


2.3 exit()與_exit()函數

   當我們需要結束一個進程的時候,我們可以使用exit()函數或_exit()函數來終止該進程。當程序運行到exit()函數或_exit()函數時,進程會無條件停止剩下的所有操作,並進行清理工作,最終將進程停止。

函數exit()

    所需頭文件:#include<stdlib.h>

    函數原型:

        void exit(int status)

    函數參數:

        status    表示讓進程結束時的狀態(會由主進程的wait();負責接收這個返回值【也可以不接收】-->類似函數的返回值),默認使用0表示正常結束

    返回值:無

exit()函數與_exit()函數用法類似,但是這兩個函數還是有很大的區別的:

    _exit()函數直接使進程停止運行,當調用_exit()函數時,內核會清除該進程的內存空間,並清除其在內核中的各種數據。

    exit()函數則在_exit()函數的基礎上進行了升級,在退出進程之間增加了若干工序。exit()函數在終止進程之前會檢測進程打開了哪些文件,並將緩衝區內容寫回文件。
   因此,exit()函數與_exit()函數最主要的區別就在於是否會將緩衝區數據保留並寫回。_exit()函數不會保留緩衝區數據,直接將緩衝區數據丟棄,直接終止進程運行;而exit()函數會將緩衝區內數據寫回,待緩衝區清空後再終止進程運行。

2.4 wait()函數與waitpid()函數

雖然子進程調用函數execv()之後擁有自己的內存空間,稱爲一個真正的進程,但由於子進程畢竟由父進程所創建,所以按照計算機技術中誰創建誰負責銷燬的慣例,父進程需要在子進程結束之後釋放子進程所佔用的系統資源。

爲實現上述目標,當子進程運行結束後,系統會向該子進程的父進程發出一個信息,請求父進程釋放子進程所佔用的系統資源。但是,父進程並沒有準確的把握一定結束於子進程結束之後,那麼爲了保證完成爲子進程釋放資源的任務,父進程應該調用系統調用wait()。

如果一個進程調用了系統調用wait(),那麼進程就立即進入等待狀態(也叫阻塞狀態),一直等到系統爲本進程發送一個消息。在處理父進程與子進程的關係上,那就是在等待某個子進程已經退出的信息;如果父進程得到了這個信息,父進程就會在處理子進程的“後事”之後纔會繼續運行。

wait()函數功能是:父進程一旦調用了wait就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷燬後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裏,直到有一個出現爲止。

如果父進程先於子進程結束進程,則子進程會因爲失去父進程而成爲“孤兒進程”。在Linux中,如果一個進程變成了“孤兒進程”,那麼這個進程將以系統在初始化時創建的init進程爲父進程。也就是說,Linux中的所有“孤兒進程”以init進程爲“養父”,init進程負責將“孤兒進程”結束後的資源釋放任務。

這裏區分一下殭屍進程和孤兒進程:

      孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作;
     殭屍進程:一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程(也就是進程爲中止狀態,僵死狀態)。


   Linix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到。這種機制就是: 在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,佔用的內存等。 但是仍然爲其保留一定的信息(包括進程號the process ID,退出狀態the termination status of the process,運行時間the amount of CPU time taken by the process等)。直到父進程通過wait / waitpid來取時才釋放。 但這樣就導致了問題,如果進程不調用wait / waitpid的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程. 此即爲殭屍進程的危害,應當避免。


 

使用wait()函數與waitpid()函數讓父進程回收子進程的系統資源,兩個函數的功能大致類似,waitpid()函數的功能要比wait()函數的功能更多。

函數wait()

    所需頭文件:#include<sys/types.h>

                        #include<sys/wait.h>

    函數原型:

        pid_t wait(int *status)

    函數參數:

        status    保存子進程結束時的狀態(由exit();返回的值)。使用地址傳遞,父進程獲得該變量。若無需獲得狀態,則參數設置爲NULL

    返回值:

        成功:已回收的子進程的PID

        失敗:-1

        

    函數waitpid()

    所需頭文件:#include<sys/types.h>

                        #include<sys/wait.h>

    函數原型:

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

    函數參數:

        pid        pid是一個整數,具體的數值含義爲:

            pid>0    回收PID等於參數pid的子進程

            pid==-1    回收任何一個子進程。此時同wait()

            pid==0    回收其組ID等於調用進程的組ID的任一子進程

            pid<-1    回收其組ID等於pid的絕對值的任一子進程

        status    同wait()

        options    

            0:同wait(),此時父進程會阻塞等待子進程退出

            WNOHANG:若指定的進程未結束,則立即返回0(不會等待子進程結束)

    返回值:

        >0        已經結束運行的子進程號

          0        使用WNOHANG選項且子進程未退出

         -1        錯誤

    當進程結束時,該進程會向它的父進程報告。wait()函數用於使父進程阻塞,直到父進程接收到一個它的子進程已經結束的信號爲止。如果該進程沒有子進程或所有子進程都已結束,則wait()函數會立即返回-1。

    waitpid()函數的功能與wait()函數一樣,不過waitpid()函數有若干選項,所以功能也比wait()函數更加強大。實際上,wait()函數只是waitpid()函數的一個特例而已,Linux內核總是調用waitpid()函數完成相應的功能。

wait(NULL)等價於waitpid(-1,NULL,0)。


 

三、共享內存

3.1 System V共享內存

概述

     共享內存是一種最爲高效的進程間通信方式,因爲進程可以直接讀寫內存,不需要任何數據的複製。爲了在多個進程間交換信息,內核專門留出了一塊內存區,這段內存區可以由需要訪問的進程將其映射到自己的私有地址空間。因此,進程就可以直接讀寫這一段內存區而不需要進行數據的複製,從而大大提高了效率。當然,由於多個進程共享一段內存,因此也需要依靠某種同步機制,如互斥鎖和信號量等。

共享內存使用步驟

 ①  創建共享內存。也就是從內存中獲得一段共享內存區域,這裏用到的函數是shmget();shmget()可以創建一個新的共享內存段或者取得一個既有共享內存段的標識符(其他進程創建的共享內存段),這個調用返回共享內存標誌符(shmid)

  ②  映射共享內存。也就是把這段創建的共享內存映射到具體的進程空間中,這裏使用的函數是shmat()。到這一步就可以使用這段共享內存了,也就是可以使用不帶緩衝的I/O讀寫命令對其進行操作,爲了引用這塊共享內存,程序需要使用由shmat()調用返回的addr值,它是一個指向進程的虛擬地址空間中該共享內存段起點的指針。

  ③  撤銷映射。使用完共享內存就需要撤銷,用到的函數是shmdt(),這個調用以後,進程就無法再引用這塊共享內存了。

  4 刪除共享內存段,只有噹噹前所有附加內存段的進程都與之分離之後內存段纔會銷燬,只有一個進程需要執行這一步。

3.2 函數說明

3.2.1 shmget函數

int shmget(key_t key, size_t size, int shmflg);
//shmflg 讀寫的權限
//函數返回共享內存段標識符(shmid)

 

    key標識共享內存的鍵值: 0/IPC_PRIVATE。 當key的取值爲IPC_PRIVATE,則函數shmget()將創建一塊新的共享內存;如果key的取值爲0,而參數shmflg中設置了IPC_PRIVATE這個標誌,則同樣將創建一塊新的共享內存。
    在IPC的通信模式下,不管是使用消息隊列還是共享內存,甚至是信號量,每個IPC的對象(object)都有唯一的名字,稱爲“鍵”(key)。通過“鍵”,進程能夠識別所用的對象。“鍵”與IPC對象的關係就如同文件名稱之於文件,通過文件名,進程能夠讀寫文件內的數據,甚至多個進程能夠共用一個文件。而在IPC的通訊模式下,通過“鍵”的使用也使得一個IPC對象能爲多個進程所共用。
    Linux系統中的所有表示System V中IPC對象的數據結構都包括一個ipc_perm結構,其中包含有IPC對象的鍵值,該鍵用於查找System V中IPC對象的引用標識符。如果不使用“鍵”,進程將無法存取IPC對象,因爲IPC對象並不存在於進程本身使用的內存中。

int size(單位字節Byte)
-----------------------------------------------
    size是要建立共享內存的長度。所有的內存分配操作都是以頁爲單位的。所以如果一段進程只申請一塊只有一個字節的內存,內存也會分配整整一頁(在i386機器中一頁的缺省大小PACE_SIZE=4096字節)這樣,新創建的共享內存的大小實際上是從size這個參數調整而來的頁面大小。即如果size爲1至4096,則實際申請到的共享內存大小爲4K(一頁);4097到8192,則實際申請到的共享內存大小爲8K(兩頁),依此類推。

 int shmflg
-----------------------------------------------
    shmflg主要和一些標誌有關。其中有效的包括IPC_CREAT和IPC_EXCL,它們的功能與open()的O_CREAT和O_EXCL相當。
    IPC_CREAT   如果共享內存不存在,則創建一個共享內存,否則打開操作。
    IPC_EXCL    只有在共享內存不存在的時候,新的共享內存才建立,否則就產生錯誤。

    如果單獨使用IPC_CREAT,shmget()函數要麼返回一個已經存在的共享內存的操作符,要麼返回一個新建的共享內存的標識符。如果將IPC_CREAT和IPC_EXCL標誌一起使用,shmget()將返回一個新建的共享內存的標識符;如果該共享內存已存在,或者返回-1。IPC_EXEL標誌本身並沒有太大的意義,但是和IPC_CREAT標誌一起使用可以用來保證所得的對象是新建的,而不是打開已有的對象。對於用戶的讀取和寫入許可指定SHM_RSHM_W,(SHM_R>3)和(SHM_W>3)是一組讀取和寫入許可,而(SHM_R>6)和(SHM_W>6)是全局讀取和寫入許可。

3.2.2 使用共享內存shmat()

 shmat()的地址函數結果是返回附加共享內存段的地址,開發人員可以向對待普通C指針那樣對待這個值。

3.2.3 分離共享內存段shmdt()函數

3.2.4 shmctl()函數

該函數完成對共享內存區的各種操作(主要刪除共享內存段落) 

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

//其中
struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
    };

The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):

struct ipc_perm {
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
};

該函數有三個命令: 
IPC_RMID:刪除 
IPC_SET:設置 
IPC_STAT:獲取

4 CPU核與進程或者線程綁定

cpu_set_t這個結構體類似於select中的fd_set,可以理解爲cpu集,也是通過約定好的宏來進行清除、設置以及判斷:

void CPU_ZERO (cpu_set_t *set); //初始化,設爲空
void CPU_SET (int cpu, cpu_set_t *set); //將某個cpu加入cpu集中
void CPU_CLR (int cpu, cpu_set_t *set); //將某個cpu從cpu集中移出
int CPU_ISSET (int cpu, const cpu_set_t *set); //判斷某個cpu是否已在cpu集中設置了

cpu集可以認爲是一個掩碼,每個設置的位都對應一個可以合法調度的 cpu,而未設置的位則對應一個不可調度的 CPU。換而言之,線程都被綁定了,只能在那些對應位被設置了的處理器上運行。通常,掩碼中的所有位都被置位了,也就是可以在所有的cpu中調度。

sched_setaffinity(俗稱進程親核性)系統調用,設置某進程(或線程)只能運行在某些cpu上

sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask) 

該函數設置進程爲pid的這個進程,讓它運行在mask所設定的CPU上.如果pid的值爲0,則表示指定的是當前進程,使當前進程運行在mask所設定的那些CPU上.第二個參數cpusetsize是mask所指定的數的長度.通常設定爲sizeof(cpu_set_t).如果當前pid所指定的進程此時沒有運行在mask所指定的任意一個CPU上,則該指定的進程會從其它CPU上遷移到mask的指定的一個CPU上運行. 
 

 

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