前言
由於進程的獨立性,當我們要使兩個進程間進行數據交互的時候就得通過介質來實現。
進程間進行通信的介質我們把它們稱之爲進程間通信方式(IPC)
根據進程間通信場景的不同,操作系統爲用戶提供了幾種不同的進程間通信方式:管道、共享內存、消息隊列、信號量。
管道(數據傳輸)
管道是一種傳輸數據的媒介,所謂管道是操作系統在內核創建的一塊緩衝區域,進程只要能夠訪問到這塊緩衝區就能通過緩衝區實現相互通信。
同時操作系統會給進程一個操作句柄用於進程操作管道,在Linux下其實是給了進程兩個文件描述符,用於負責管道的讀和寫。
- 管道的本質:
內核中的一塊緩衝區
- 管道通信的原理:
讓多個進程都能訪問到同一塊緩衝區從而實現進程間通信
- 管道的特性:
1.管道自帶同步與互斥(保證多個進程操作管道不發生數據的二義性)
同步:通過條件判斷保證臨界資源訪問時序的合理性
互斥:通過同一時間的唯一訪問保證臨界資源訪問的安全性(原子操作)
2.管道的生命週期隨進程
3.管道實現方向可選擇的、單向的、有序的數據傳輸
4.管道提供字節流服務(有序,連接,可靠的字節流傳輸)
讀寫特性:
1.管道中沒有數據則讀端阻塞,管道中寫滿了數據則寫端阻塞
2.管道讀端被關閉繼續寫入會觸發異常發起SIGPIPE信號導致進程退出
3.寫端被關閉繼續讀完管道中的數據後返回0
注意:關閉讀端或寫端的時候,所有進程的讀端或寫端都得關閉
匿名管道
匿名管道顧名思義就是內核中的這塊緩衝區沒有標識符。
當前進程只能通過返回的操作句柄來操作匿名管道,其他進程無法找到匿名管道,所以匿名管道只能用於具有親緣關係的進程間通信。
- 匿名管道通信流程:
操作句柄保存在pcb當中,先創建管道再創建子進程,子進程通過複製父進程的方式獲取管道的操作句柄,進而訪問同一管道來實現進程間的通信。
- 匿名管道的創建:
int pipe(int pipefd[2]):具有兩個int型結點的數組的首地址,用於接收創建管道返回的操作句柄
創建一個匿名管道,向用戶通過參數pipefd返回管道的操作句柄。返回值 0(成功),-1(失敗)
pipefd[0]: 用於從管道讀取數據
pipefd[1]: 用於向管道寫入數據
- 匿名管道的實現:
int main(){
// 匿名管道只能用於具有親緣關係的進程間通信
// 通過子進程複製父進程的方式來獲取管道的操作句柄
// 創建管道一定要放到子進程之前
int pipefd[2] = {-1};
int ret = pipe(pipefd);
if(ret < 0){
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid == 0){
// 子進程從管道讀取數據
char buf[1024] = {0};
read(pipefd[0], buf, 1023);
printf("子進程讀到的數據:%s", buf);
}
else if(pid > 0){
// 父進程向管道寫入數據
char* ptr = "父進程寫入\n";
write(pipefd[1], ptr, strlen(ptr));
}
return 0;
}
命名管道
命名管道顧名思義就是內核中的這塊緩衝區有標識符。
- 命名管道的通信流程:
多個進程可以通過相同的標識符找到同一塊緩衝區,打開同一個管道文件,進而實現同一主機上任意進程間通信。
- 命名管道的創建:
mkfifo 創建命名管道(生成test.fifo的管道文件):int mkfifo(const char* pathname管道文件名稱,mode_t mode文件權限)
成功:返回 0 | 失敗:返回-1
- 命名管道的打開特性:
1.若管道文件以只讀的方式打開,則會阻塞,直到這個管道文件被以寫的方式打開。
2.若管道文件以只寫的方式打開,則會阻塞,直到這個管道文件被以讀的方式打開。
3.若管道文件以讀寫的方式打開,則不會阻塞。
- 命名管道的實現:
Fifowrite.c
int main(){
// 創建命名管道
umask(0);
int ret = mkfifo("./test.fifo", 0664);
if(ret < 0 && errno != EEXIST){
perror("mkfifo error");
return -1;
}
// 操作管道
int fd = open("./test.fifo", O_WRONLY);
if(fd < 0 ){
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
int cur = 0;
while(1){
char buf[1024] = {0};
sprintf(buf, "寫端寫入數據 [%d]", cur++);
write(fd, buf, strlen(buf));
printf("寫入數據成功\n");
sleep(1);
}
close(fd);
return 0;
}
Fiforead.c
int main(){
// 創建命名管道
umask(0);
int ret = mkfifo("./test.fifo", 0664);
if(ret < 0 && errno != EEXIST){
perror("mkfifo error");
return -1;
}
// 操作管道
int fd = open("./test.fifo", O_RDONLY);
if(fd < 0 ){
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
while(1){
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if(ret < 0){
perror("read error");
return -1;
}
else if(ret == 0){
perror("all write closed");
return -1;
}
printf("read buf:[%s]\n", buf);
}
close(fd);
return 0;
}
共享內存(數據共享)
- 共享內存的本質:
共享內存實際是在物理內存上開闢的一塊具有標識符的內存空間
- 共享內存的通信原理:
將這塊內存空間映射到進程的虛擬地址空間中,進程則可以通過虛擬地址進行訪問操作;多個進程映射同一塊物理內存,那麼多個進程訪問同一塊內存空間,進而實現進程間通信。
- 共享內存的特性:
1.共享內存生命週期隨內核
在物理內存上開闢,將信息儲存在內核中,生命不會隨進程結束而結束,內核重啓或手動釋放共享內存
2.最快的進程間通信方式
不涉及用戶態和內核態的兩次數據拷貝
3.共享內存操作是不安全的
不自帶同步和互斥,需要操作用戶進行控制
4.共享內存的寫入是一種針對地址的覆蓋式寫入
- 共享內存的通信流程:
Shmwrite.c
#define IPC_KEY 0x12345678
int main(){
// 1.創建共享內存
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if(shm_id < 0){
perror("shmget error");
return -1;
}
// 2.建立映射關係
void* shm_start = shmat(shm_id, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
// 3.進行內存操作
int cur = 0;
while(1){
sprintf(shm_start, "%s:%d", "寫入端寫入", ++cur);
sleep(1);
}
// 4.解除映射關係
shmdt(shm_start);
// 5.刪除共享內存
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
Shmread.c
#define IPC_KEY 0x12345678
int main(){
// 1.創建共享內存
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if(shm_id < 0){
perror("shmget error");
return -1;
}
// 2.建立映射關係
void* shm_start = shmat(shm_id, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
// 3.進行內存操作
while(1){
printf("[%s]\n", shm_start);
sleep(1);
}
// 4.解除映射關係
shmdt(shm_start);
// 5.刪除共享內存
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
刪除共享內存中值得注意:
共享內存並不會立即被刪除(因爲可能造成正在訪問的進程崩潰),而是將key修改爲0,表示這塊共享內存不再繼續接收映射連接,當這塊共享內存的映射連接數爲0時,則自動被釋放。
消息隊列(數據傳輸)
- 消息隊列的本質:
消息隊列是內核中的一個優先級隊列
- 消息隊列的通信原理:
多個進程通過向隊列中添加結點或者獲取結點實現通信
- 消息隊列的特性:
1.生命週期隨內核
2.自帶同步與互斥
3.數據傳輸自帶優先級(傳輸一個有類型(優先級)的數據塊)
信號量(進程控制)
- 信號量的本質:
內核當中的一個計數器+pcb等待隊列(對資源進行計數)
- 信號量的作用:
用於實現進程間的同步與互斥
同步的實現:
訪問前進行條件判斷
信號量是對資源的計數,通過計數判斷是否能夠獲取一個資源進行處理,若計數器<0則表示不能獲取(並且對計數器-1),需要等待(加入pcb隊列),並不保證獲取資源的安全性
這時若其他進程生產一個資源,則會對計數進行+1,若計數<=0則喚醒一個進程。 >0社麼都不幹
將pcb置爲可中斷休眠狀態加入隊列
互斥的實現:
同一時間的唯一訪問
起始爲1
通過不大於1的計數器,實現對臨界資源訪問狀態的標記,在訪問臨界資源之前先獲取信號量。
計數 - 1後:
若計數 <= 0則進程等待(將進程pcb加入隊列中);否則可以對臨界資源進行訪問(已經將臨界資源的狀態置爲不可訪問狀態,完畢之後,對計數+1,喚醒一個進程(將一個pcb出隊,置爲運行狀態)