1. 程式人生 > 其它 >C多程序

C多程序

這篇文章主要是想針對多程序的建立和一些通訊手段來進行一下記錄

建立子程序

關於建立子程序的原型一般都是用的這個,直接fork,這個函式在父程序中呼叫,在父子程序中各有一個pid_t型別的返回值,父程序中得到的是子程序的ID,子程序中得到的是0值。當然呼叫失敗就是-1。

//建立程序,然後複製出另一份程序
#include <unistd.h>
pid_t fork();

根據不同的fork返回值,父子程序可以分出自己專屬的程式碼區域段。例子如下:

#include <stdio.h>
#include <unistd.h>

int i = 10;
int main() {
    pid_t pid;
    pid = fork();
    if (pid == 0) {
        i++;
        printf("I' m the subprocess.The i:%d\n", i);
    } else {
        i--;
        printf("I' m the parent process.The i:%d\n", i);
    }
    return 0;
}

一般來說,寫程式碼的理想狀態是最後的程式正常跑,更理想的就是完全不出錯,不過那個太理想了。比如多程序程式中,當父程序結束了,子程序沒有被父程序獲取狀態資訊,從而使得程序號依然保留在系統中,佔用系統定數的程序號;又比如父程序都結束運行了,子程序還在繼續跑,由init程序來接管。這兩種情況,前者被叫殭屍程序,後者被稱為孤兒程序(這個概念其實我挺犯迷糊,如果有衝突那就是你對,記得提點一聲)。所以,父程序在結束之前,要對子程序負責,要查詢子程序的結束狀態,並確保子程序跑完了才跑路。

wait一下

簡單的方案,就是父程序一直等,實現這個功能的函式原型如下:

#include <sys/wait.h>
pid_t wait(int *statloc);

//配合使用的巨集
WIFEXITED(statloc);						//子程序正常終止,返回非0值
WEXITSTATUS(statloc);						//子程序正常終止,返回退出碼
WIFSIGNALED(statloc);						//因為未捕獲訊號而終止,返回非0值
WTERMSIG(statloc);						//配合前一個巨集,返回訊號值
WIFSTOPPED(statloc);						//子程序意外終止,返回非0
WSTOPSIG(statloc);						//子程序意外終止,返回訊號值

上面函式的通用解讀就是,wait函式的呼叫會阻塞父程序,一直等著子程序跑完返回狀態資訊到statloc才對父程序放行。而對於子程序的結束資訊的解讀,就是上面對應的巨集來進行。不過wait的阻塞讓很多人不滿,所以他們實現了另一種wait:

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);

使用waitpid處理殭屍程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int status, i=0;
    pid = fork();
    if (pid == 0) {
        i--;
        printf("subprocess: %d\n", i);
        sleep(5);
        return 6;
    } else {
        //因為只有一個子程序,就不明確指定了
        while (!waitpid(-1, &status, WNOHANG)) {
            i++;
            printf("parent process, %d sec\n", i);
            sleep(1);
        }
        if (WIFEXITED(status))
            printf("Subprocess was ended and return a value :%d\n", WEXITSTATUS(status));
    }
    return 0;
}

程序間通訊

比較簡單的通訊方式,是建立管道,管道和socket套接字同屬系統資源,建立了管道,就是使得兩個管道在系統提供的記憶體進行通訊。實現的原型如下:

#include <unistd.h>
int pipe(int filedes[2]);

所謂管道,是有著兩個口子的,這裡的管道也一樣,filedes就是一個包含了兩個檔案描述符的陣列,一般傳入的這個引數是空的,函式呼叫結束後就成了新建立的管道的入口和出口。
嗯,所以這個管道的使用,其實就是這兩個描述符的使用,filedes陣列中,第一個是管道入口,第二個是管道出口,這個要注意。

#include <stdio.h>
#include <unistd.h>

int main() {

    pid_t pid;
    int fds[2];
    char str[20];
    pipe(fds);
    pid = fork();

    if (pid != 0) {
        write(fds[1], "balabala", sizeof("balabala"));
        printf("parent process.\n");
        sleep(3);
    } else {
        read(fds[0], str, 20);
        printf("subprocess, get mes: %s\n", str);
    }

    return 0;
}

例子是父程序傳送資訊,子程序接收資訊,實際上反過來也可以,不限定。但資訊放進管道,父子程序其實都可以讀取,就像寫了資訊在文字,誰都可以讀取。管道的單向只體現在它的資訊是從fds[1]進,fds[0]出。為了保證
資訊的受眾是對端從而實現雙方通訊,往往實現兩個管道,然後一個管道負責發,一個負責收,這樣就不需要預測執行流程。

管道是很便利,但它往往適用於關聯程序(像父子程序),想要無關聯的通訊還需要其他機制,比如下面的3種System V IPC。

System V IPC

針對共享資源的多程序訪問,這種獨佔式的訪問會引發大問題,誰先誰後無法控制,這種引發競爭的程式碼段,被稱為臨界區。對程序的同步,就是確保進入臨界區只有一個程序。

訊號量

它是一個特殊的整數值變數,只支援兩種操作,一個是取,一個是放,分別是P原語和V原語的解讀。因為針對多程序同步和多執行緒同步都有訊號量的概念,雖然語義一致,但實現不一樣,姑且把多程序間訊號量稱為訊號量,多執行緒間訊號量稱為POSIX訊號量。對於訊號量的初始化決定了其行為,但最常用的就是二進位制訊號量,用0和1來代表空置和佔用的意義。linux中的實現,往往在sys/sem.h標頭檔案中,三個系統呼叫設計成操作一組訊號量而不是單個訊號量,三個系統呼叫分別是semget、semop和semctl;而POSIX訊號量的實現都在semaphore.h標頭檔案中。

訊號量的建立

#include <sys/sem.h>

//申請訊號集,申請成功就返回訊號量標記值,失敗返回-1
int semget(key_t key, int num_sems, int sem_flags);

semget的引數key具有唯一性,num_sems則是申請的system V訊號量集的訊號量數,sem_flags制定了訊號量的讀寫許可權。在semget建立訊號量成功後,相關聯的核心資料結構體semid_ds也會被建立且初始化,具體儲存的資訊就是建立訊號量集的程序的使用者ID和組ID,以及訊號量集的訊號量數還有訊號量的讀寫許可權等。

訊號量賦初值

具體操作需要依賴semctl函式:

#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...)

sem_id,當然就是訊號量集的識別符號了,sem_num於訊號量集的意義就像下標之於陣列,是標記某某某訊號量,command則是執行的命令了。因為這裡要用它來賦初值,所以呼叫起來就是 semctl(sem_id, 0, SETVAL, sem_union),這個呼叫其實就是執行SETVAL指示的賦值操作,而sem_union就是攜帶著想要賦值給訊號量的初值。不過先不對這個結合體做過多闡述。系統瞭解一下semop先:

#include <sys/sem.h>
int semop(int sem_id, struct sembuf *semops, size_t num_sem_ops);

semop函式是對訊號量進行PV操作的關鍵,但具體如何改變要看傳參semops,也就是sembuf這種結構體

struct sembuf {
    unsigned short int sem_num;                  //對應訊號量在訊號量集中的索引
    short int sem_op;                            //指定操作型別
    short int sem_flag;                          //標誌位
};

可選值為正整型、0和負整型的sem_op以及可選值為IPC_NOWAIT和SEM_UNDO的sem_flag配合起來就決定了semop函式的呼叫結果。

共享記憶體

很容易理解的一個機制,就是一塊記憶體,程序間可以共享,它的實現都在sys/shm.h中,使用的函式包括shemget、shmat、shmdt、shmctl:

#include <sys/shm.h>

//建立共享記憶體或者獲取已存在的共享記憶體
int shmget(key_t key, size_t size, int shmflag);
//size,位元組為單位,指定記憶體的大小,獲取已存在的共享記憶體可以設定為0;
//shmflag,支援SHM_HUGETLB和SHM_NORESERVE,前者表示用“大頁面”來分配空間給共享記憶體,後者表示不為共享記憶體保留交換分割槽,這樣記憶體不足的時候繼續寫入就會發起SIGSEGV訊號

函式呼叫成功就返回共享記憶體的識別符號,失敗返回-1,然後同樣地,核心中有個相關的資料結構shmid_ds會被建立且初始化。在共享記憶體建立成功後,需要把它關聯到程序的地址空間中,用完了需要進行分離:

//關聯操作,返回共享記憶體被關聯到程序中的具體地址,失敗會返回(void*)-1
void *shmat(int shm_id, const void *shm_addr, int shmflag);
//分離原本關聯好的共享記憶體,成功就回0,失敗回-1
int shmdt(const void *shm_addr);

shmget成功呼叫返回的識別符號就可用於shm_id,shm_addr則是程序內指標,具體函式呼叫效果還是要看shmflag

  • shm_addr為NULL,關聯地址由系統選擇,這樣更加相容
  • shm_addr非空,shmflag沒有設定SHM_RND,共享記憶體關聯到shm_addr指向地址
  • shm_addr非空,shmflag設定了SHM_RND
    嗯,shmflag標誌位還可以設定SHM_RDONLY,表示程序只讀該共享記憶體,沒設定就讀寫都可(共享記憶體建立時就會設定讀寫許可權);SHM_REMAP,已經關聯呢,就重新關聯;SHM_EXEC,指定可讀

關於關聯成功和取消關聯關係,都會使得shmid_ds的核心資料發生變動,比如關聯成功:
shm_nattach加一、shm_lpid設定為呼叫程序的PID、shm_atime設定為當前時間
取消關聯成功,就:
shm_nattach減一、shm_lpid設為呼叫程序的PID、shm_dtime會設定成當前時間
這麼來看,其實關聯和非關聯都是一個記錄,看看什麼時候發生變動,變動的操作者是誰,至於區分開兩者就是前面的shm_nattach了。

嗯,和訊號量一樣,共享記憶體的關聯也是準備工作,要用還是要有個函式來進行呼叫,共享記憶體的就是shmctl,這個函式重點關注command引數,這個是具體如何用的關鍵:

int shmctl(int shm_id, int command, struct shmid_ds *buf);

關於command引數參見下表:

引數 意思 函式呼叫成功的返回值
IPC_STAT 共享記憶體相關的核心資料結構shmid_ds複製到buf中 0
IPC_SET buf的部分資料複製到共享記憶體相關的核心資料結構shmid_ds中,
重新整理shmid_ds.shm_ctime
0
IPC_RMID 標記上刪除,當最後一個程序用完呼叫shmdt分離後,共享記憶體
就被刪了
0
IPC_INFO 獲取共享記憶體的系統配置,存在轉換成shminfo結構體型別的buf中 核心中共享記憶體資訊陣列被使用項的最大index值
SHM_INFO 和IPC_INFO類似,但得到的是已分配的共享記憶體佔用的資源資訊
(嗯,這裡要把buf轉換成shm_info型)
同上
SHM_STAT 類似IPC_STAT,但此時shm_id是用來表示核心中共享記憶體資訊陣列的 核心共享記憶體資訊陣列索引為shm_id的識別符號
SHM_LOCK 禁止共享記憶體被移動到交換分割槽 0
SHM_UNLOCK 和上面的相反,允許共享記憶體被移動到交換分割槽 0

暫時先寫就這麼點吧,後面再來更新

一些相關的核心資料結構:

//system v訊號量
#include <sys/sem.h>

//描述IPC物件許可權
struct ipc_perm {
    key_t key;                      //鍵值
    uid_t uid;                      //持有者的有效使用者ID
    gid_t gid;                      //持有者的組ID
    uid_t cuid;                     //建立者的使用者ID
    gid_t cgid;                     //建立者的組ID
    mode_t mode;                    //訪問許可權
    ...
};

//system v訊號量的核心資料結構
struct semid_ds {
    struct ipc_perm sem_perm;            //重點關注訊號量的操作許可權
    unsigned long int sem_nsems;         //訊號量集的訊號量數
    time_t sem_otime;                    //最後一次呼叫semop時間
    time_t sem_ctime;                    //最後一次呼叫semctl時間
    ...
};


#include <sys/shm.h>
//共享記憶體的核心資料結構
struct shmid_ds {
    struct ipc_perm shm_perm;            //共享記憶體操作許可權
    size_t shm_segsz;                    //共享記憶體大小,以位元組為單位
    __time_t shm_atime;                  //對共享記憶體最後一次呼叫shmat的時間
    __time_t shm_dtime;                  //對共享記憶體最後一次呼叫shmdt的時間
    __time_t shm_ctime;                  //對共享記憶體最後一次呼叫shmctl的時間
    __pid_t shm_cpid;                    //建立者PID
    __pid_t shm_lpid;                    //最後一次執行shmat或者shmdt的程序PID
    ...
};


#include <sys/msg.h>
//訊息佇列的核心資料結構
struct msqid_ds {
    struct ipc_perm msg_perm;            //訊息佇列操作許可權
    time_t msg_stime;                    //最後一次呼叫msgsnd時間
    time_t msg_rtime;                    //最後一次呼叫msgrcv時間
    time_t msg_ctime;                    //最後一次被修改時間
    unsigned long __msg_cbytes;          //訊息佇列中已有的位元組數
    msgqnum_t msg_qnum;                  //訊息佇列已有訊息數
    msglen_t msg_qbytes;                 //訊息佇列允許的最大位元組數
    pid_t msg_lspid;                     //最後執行msgsnd的程序PID
    pid_t msg_lrpid;                     //最後執行msgrcv的程序PID
};