半雙工管道、全雙工管道、FIFO
UDS,管道
同一臺主機的兩個進程之間的 IPC,套接字和 STREAMS 是僅有的支持不同主機上兩個進程之間 IPC 的兩種形式
本章討論經(jīng)典的 IPC:管道、FIFO、消息隊列、信號量 以及共享存儲
管道
局限性:歷史上半雙工,數(shù)據(jù)單向;在公共祖先的兩個進程間使用,通常父子進程使用
FIFO沒有第二種限制,uds沒有這兩種限制
每當(dāng)在管道中鍵入一個命令序列, 讓 shell 執(zhí)行時,shell 都會為每一條命令單獨創(chuàng)建一個進程,然后用管道將前一條命令進程的標 準輸出與后一條命令的標準輸入相連接
管道是通過調(diào)用 pipe 函數(shù)創(chuàng)建的
#include <unistd.h>
int pipe(int fd[2]);
fd[0]讀 <- fd[1]寫
fd[1]的輸出是fd[0]的輸入
單個進程中的管道幾乎沒有任何用處
管道連接著一個寫端的進程,一個讀端的進程;讀一個寫端的管道,寫一個讀端的管道
popen和pcloose
創(chuàng)建一個管道,fork 一個子進程,關(guān)閉未使用的管道端,執(zhí)行一個 shell 運行命令,然后等待命令終止。
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);
popen 的r模式: 子進程cmd的標準輸出連接到父進程的文件指針
popen 的w模式:父進程的文件指針連接到子進程的cmd的標準輸入
協(xié)同進程
當(dāng)一個過濾程序既產(chǎn)生某個過濾程序的輸入,又讀取該過濾程序的輸出時,它就變成了協(xié)同進程
兩個pipe實現(xiàn)可以雙工
FIFO
前面用的其實是未命名管道,只能兩個相關(guān)進程使用,如父子進程,而FIFO是命名管道,不相關(guān)進程也能通信
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
int mkfifoat(int fd,const char *path,mode_t mode);
mode與open的mode一樣
mkfifoat 函數(shù)可以被用來在 fd 文件描述符表 示的目錄相關(guān)的位置創(chuàng)建一個 FIFO
FIFO的用途:
- shell 命令使用 FIFO 將數(shù)據(jù)從一條管道傳送到另一條時,無需創(chuàng)建中間臨時文件。
- 客戶進程-服務(wù)器進程應(yīng)用程序中,F(xiàn)IFO 用作匯聚點,在客戶進程和服務(wù)器進程二者之 間傳遞數(shù)據(jù)
XSI IPC
有 3 種稱作 XSI IPC 的 IPC:消息隊列,信號量,共享存儲器
XSI IPC 函數(shù)是緊密地基于 System V 的 IPC 函數(shù)的
標識符和鍵
與文件描述符不同,IPC 標識符不是小的整數(shù)。當(dāng)一個 IPC 結(jié)構(gòu)被創(chuàng)建,然后又被 刪除時,與這種結(jié)構(gòu)相關(guān)的標識符連續(xù)加 1,直至達到一個整型數(shù)的最大正值,然后又回轉(zhuǎn)到 0
有多種方法使客戶進程和服務(wù)器進程在同一 IPC 結(jié)構(gòu)上匯聚
1,2,3;1,2都有明顯缺點,3是基于2,客服2的缺點
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
客戶進程和服務(wù)器進程認同一個路徑名和項目 ID(項目 ID 是 0~255 之 接著,調(diào)用函數(shù) ftok 將這兩個值變換為一個鍵。
權(quán)限結(jié)構(gòu)
struct ipc_perm{
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
...
}
| 權(quán)限 | 位 |
|---|---|
| 用戶讀 | 0400 |
| 用戶寫(更改) | 0200 |
| 組讀 | 0040 |
| 組寫(更改) | 0020 |
| 其他讀 | 0004 |
| 其他寫(更改) | 0002 |
結(jié)構(gòu)限制
優(yōu)缺點
管道和FIFO在最后一個引用進程終止后,會刪除數(shù)據(jù),但是3中IPC不會
這些 IPC 結(jié)構(gòu)在文件系統(tǒng)中沒有名字,需要內(nèi)核增加十幾個全新的系統(tǒng)調(diào)用,如msgget,semget,shmget
消息隊列
每個消息都由 3 部分組成:一個正的長整型類型的字段、一個非負的長度 (nbytes)以及實際數(shù)據(jù)字節(jié)數(shù)(對應(yīng)于長度)。
消息總是放在隊列尾端
消息隊列是消息的鏈接表,存儲在內(nèi)核中,由消息隊列標識符標識
msgget 用于創(chuàng)建一個新隊列或打開一個現(xiàn)有隊列
msgsnd 將新消息添加到隊列尾端
msgrcv 用于從隊列中取消息
#include <sys/msg.h>
int msgget(key_t key,int flag);
msgget((0x123 + 1), IPC_CREAT | 0666)
int msgctl(int msgid,int cmd,struct msgid_ds *buf);
每個隊列都有一個msgid_ds
struct msgid_ds{
struct ipc_perm msg_perm;
msggnum_t msg_gnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
time_t msg_stime;
time_t msg_rtime;
time_t msg_ctime;
...
}
msgctl 函數(shù)對隊列執(zhí)行多種操作(類似的還有semctl,shmctl)
cmd:也可用于信號量和共享存儲
IPC_STAT 取此隊列的 msqid_ds 結(jié)構(gòu),并將它存放在 buf 指向的結(jié)構(gòu)中
IPC_SET 將字段 msg_perm.uid、msg_perm.gid、msg_perm.mode 和 msg_qbytes從 buf 指向的結(jié)構(gòu)復(fù)制到與這個隊列相關(guān)的 msqid_ds 結(jié)構(gòu)中
IPC_RMID 從系統(tǒng)中刪除該消息隊列以及仍在該隊列中的所有數(shù)據(jù)。這種刪除立即生效
#include <sys/msg.h>
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);
ptr 就是一個指向 mymesg 結(jié)構(gòu)的指針
struct mymesg{
long mtype;
char mtext[512];
}
對刪除消息隊列的處理不是很完善,沒有維護引用計數(shù)器
ptr 參數(shù)指向一個長整型數(shù),它包含了正的整型消息類型,其后緊接著的是消息數(shù)據(jù)(若 nbytes 是 0,則無消息數(shù)據(jù))
參數(shù) flag 的值可以指定為 IPC_NOWAIT。這類似于文件 I/O 的非阻塞 I/O 標志
當(dāng) msgsnd 返回成功時,消息隊列相關(guān)的 msqid_ds 結(jié)構(gòu)會隨之更新,表明調(diào)用的進程 ID (msg_lspid)、調(diào)用的時間(msg_stime)以及隊列中新增的消息(msg_qnum)
msgrcv 從隊列中取用消息
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
flag 中設(shè)置了 MSG_NOERROR 位,則該消息會被截斷;flag 值指定為 IPC_NOWAIT,使操作不阻塞
參數(shù) type 可以指定想要哪一種消息
msgrcv 成功執(zhí)行時,內(nèi)核會更新與該消息隊列相關(guān)聯(lián)的 msgid_ds 結(jié)構(gòu),以指示調(diào)用者的進程 ID(msg_lrpid)和調(diào)用時間(msg_rtime),并指示隊列中的消息數(shù)減少了 1 個(msg_qnum)
消息隊列與全雙工管道的時間比較
客戶進程和服務(wù)器進程之間的雙向數(shù)據(jù)流:
可以使用消息隊列或全雙工管道
可以使全雙工管道可用,而某些平臺通過 pipe 函數(shù)提供全雙工管道
考慮到使用消息隊列時遇到的問題(見 15.6.4 節(jié)),我們得出的 結(jié)論是,在新的應(yīng)用程序中不應(yīng)當(dāng)再使用它們
信號量
信號量與已經(jīng)介紹過的 IPC 機構(gòu)(管道、FIFO 以及消息列隊)不同。它是一個計數(shù)器,用 于為多個進程提供對共享數(shù)據(jù)對象的訪問。
若此信號量的值為正,則進程可以使用該資源。在這種情況下,進程會將信號量值減 1, 表示它使用了一個資源單位
否則,若此信號量的值為 0,則進程進入休眠狀態(tài),直至信號量值大于 0。進程被喚醒 后
當(dāng)進程不再使用由一個信號量控制的共享資源時,該信號量值增 1。如果有進程正在休眠等 待此信號量,則喚醒它們
內(nèi)核為每個信號量集合維護著一個 semid_ds 結(jié)構(gòu)
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
...
}
每個信號量由一個無名結(jié)構(gòu)表示
struct{
unsigned short semval;
pid_t ssempid;
unsigned short semncnt;
unsigned short semzcnt;
...
}
調(diào)用函數(shù) semget 來獲得一個信號量 ID
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
初始化 ipc_perm 結(jié)構(gòu)。該結(jié)構(gòu)中的 mode 成員被設(shè)置為 flag 中的 相應(yīng)權(quán)限位
nsems 是該集合中的信號量數(shù)。如果是創(chuàng)建新集合(一般在服務(wù)器進程中),則必須指定 nsems。 如果是引用現(xiàn)有集合(一個客戶進程),則將 nsems 指定為 0。
cmd:10種
函數(shù) semop 自動執(zhí)行信號量集合上的操作數(shù)組
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
參數(shù) semoparray 是一個指針,它指向一個由 sembuf 結(jié)構(gòu)表示的信號量操作數(shù)組
參數(shù) nops 規(guī)定該數(shù)組中操作的數(shù)量(元素數(shù))。
struct sembuf{
unsigned short sem_num;
short sem_op;//對集合中每個成員的操作由相應(yīng)的 sem_op 值規(guī)定
short sem_flg;//IPC_NOWAIT, SEM_UNDO
}
sem_op 為正值,這對應(yīng)于進程釋放的占用的資源數(shù),需要加到信號量的值上;如果指定了 undo 標志,則也從該進程的此信號量調(diào)整值中減去 sem_op
sem_op 為負值,信號量的值大于等于 sem_op 的絕對值,從信號量值中減去 sem_op 的絕對值;如果指定了 undo 標志,則 sem_op 的絕對值也 加到該進程的此信號量調(diào)整值上
正負兩者操作其實是一個道理
sem_op 為 0,這表示調(diào)用進程希望等待到該信號量值變成 0
exit 時的信號量調(diào)整
正如前面提到的,如果在進程終止時,它占用了經(jīng)由信號量分配的資源,那么就會成為一個 問題。
信號量、記錄鎖和互斥量的時間比較
共享存儲
信號量用于同步共享存儲訪問
在多個進程將同一個文件映射到它們的地址空間 的時候。
XSI 共享存儲和內(nèi)存映射的文件的不同之處在于,前者沒有相關(guān)的文件。
XSI 共享存儲 段是內(nèi)存的匿名段
內(nèi)核為每個共享存儲段維護著一個結(jié)構(gòu)
struct shmid_ds{
struct ipc_perm shm_perm;
size_t shm_segsz;
pid_t shm_lpid;
pid_t shm_cpid;
shmatt_t shm_nattch;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
...
}
調(diào)用的第一個函數(shù)通常是 shmget,它獲得一個共享存儲標識符
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
mode 按 flag 中的相應(yīng)權(quán)限位 設(shè)置
shmctl 函數(shù)對共享存儲段執(zhí)行多種操作
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
Cmd:5種命令
一旦創(chuàng)建了一個共享存儲段,進程就可調(diào)用 shmat 將其連接到它的地址空間中
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
int shmdt(const void *addr);
如果 addr 為 0,則此段連接到由內(nèi)核選擇的第一個可用地址上。這是推薦的使用方式
flag 中指定了 SHM_RDONLY 位,則以只讀方式連接此段,否則以讀寫方式連接此段
shmat 的返回值是該段所連接的實際地址,如果出錯則返回?1;成功,內(nèi)核將使與該共享存儲段相關(guān)的 shmid_ds 結(jié)構(gòu)中的 shm_nattch 計數(shù)器值加 1
共享存儲段緊靠在 棧之下
POSIX信號量
POSIX 信號量接口意在解決 XSI 信號量接口的幾個缺陷:
- 更高性能的實現(xiàn)
- 沒有信號量集,更加簡單
- 操作能繼續(xù)正常工作直到該信號量的最后一次引用被釋放;XSI 信號量被刪除時,使用這個 信號量標識符的操作會失敗
POSIX 信號量有兩種形式:命名的和未命名的
調(diào)用 sem_open 函數(shù)來創(chuàng)建一個新的命名信號量或者使用一個現(xiàn)有信號量
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode,unsigned int value */ );
int sem_close(sem_t *sem);
當(dāng)使用一個現(xiàn)有的命名信號量時,我們僅僅指定兩個參數(shù):信號量的名字和 oflag 參數(shù)的 0 值
當(dāng)這個 oflag 參數(shù)有 O_CREAT 標志集時,如果命名信號量不存在,則創(chuàng)建一個新的。如果它 已經(jīng)存在,則會被使用,但是不會有額外的初始化發(fā)生
當(dāng)我們指定 O_CREAT 標志時,需要提供兩個額外的參數(shù)。mode 參數(shù)指定誰可以訪問信號量。 mode 的取值和打開文件的權(quán)限位相同
在創(chuàng)建信號量時,value 參數(shù)用來指定信號量的初始值。它的取值是 0~SEM_VALUE_MAX
如果我們想確保創(chuàng)建的是信號量,可以設(shè)置 oflag 參數(shù)為 O_CREAT|O_EXCL。如果信號量已 經(jīng)存在,會導(dǎo)致 sem_open 失敗
可以使用 sem_unlink 函數(shù)來銷毀一個命名信號量
#include <semaphore.h>
int sem_unlink(const char *name);
sem_unlink 函數(shù)刪除信號量的名字。如果沒有打開的信號量引用,則該信號量會被銷毀。 否則,銷毀將延遲到最后一個打開的引用關(guān)閉
可以使用 sem_wait 或者 sem_trywait 函數(shù)來實現(xiàn)信號量的減 1 操作
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_timedwait(sem_t *restrict sem,const struct timespec *restrict tsptr);
使用 sem_wait 函數(shù)時,如果信號量計數(shù)是 0 就會發(fā)生阻塞。直到成功使信號量減 1 或者被 信號中斷時才返回
可以調(diào)用 sem_post 函數(shù)使信號量值增 1
#include <semaphore.h>
int sem_post(sem_t *sem);
當(dāng)我們想在單個進程中使用 POSIX 信號量時,使用未命名信號量更容易。這僅僅改變創(chuàng)建和 銷毀信號量的方式??梢哉{(diào)用 sem_init 函數(shù)來創(chuàng)建一個未命名的信號量。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
pshared 參數(shù)表明是否在多個進程中使用信號量。如果是,將其設(shè)置成一個非 0 值。value 參 數(shù)指定了信號量的初始值。
如果要在兩個進程之間使用信號量,需要確保 sem 參數(shù)指向兩個進程之間共享的內(nèi)存范圍
對未命名信號量的使用已經(jīng)完成時,可以調(diào)用 sem_destroy 函數(shù)丟棄它
調(diào)用 sem_destroy 后,不能再使用任何帶有 sem 的信號量函數(shù),除非通過調(diào)用 sem_init 重新初始化它
sem_getvalue 函數(shù)可以用來檢索信號量值