這篇文章主要是想針對多進程的創建和一些通信手段來進行一下記錄 創建子進程 關於創建子進程的原型一般都是用的這個,直接fork,這個函數在父進程中調用,在父子進程中各有一個pid_t類型的返回值,父進程中得到的是子進程的ID,子進程中得到的是0值。當然調用失敗就是-1。 //創建進程,然後複製出另一份 ...
這篇文章主要是想針對多進程的創建和一些通信手段來進行一下記錄
創建子進程
關於創建子進程的原型一般都是用的這個,直接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
};