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
};