這篇文章主要是想針對多行程的創建和一些通信手段來進行一下記錄
創建子行程
關于創建子行程的原型一般都是用的這個,直接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
};
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/538891.html
標籤:其他
上一篇:升級到win11 22h2的體驗
下一篇:一文讀懂野指標
