目錄
1. 介紹
1.1 進程間通信的目的
1.2 進程間通信的分類
2. 管道
2.1 什么是管道
2.2 匿名管道
2.2.1 接口
2.2.2 步驟--以父子進程通信為例
2.2.3?站在文件描述符角度-深度理解
2.2.4 管道代碼
2.2.5 讀寫特征
2.2.6?管道特征
2.3 命名管道
2.3.1?接口
2.3.2 代碼實現
?2.3.3 匿名管道和命名管道的區別
3.?system V共享內存
3.1 共享內存的原理?
3.2 步驟
3.3 系統接口
3.4 代碼
3.5 共享內存的優缺點
4.信號量
4.1 相關概念
4.2 信號量 -- 對資源的一種預定
4.3 系統接口
1. 介紹
1.1 進程間通信的目的
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件? ? ? ? ? ? ? ? ? ? ? ? (如進程終止 時要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望? ? ? ? ? ? ? ? ? ? ? 能夠攔截另一個進程的所有陷入和異常,并能夠及時知道它的狀態改變
- 有時候也需要多進程協同進行工作
如何理解進程間通信的本質問題呢?
- OS需要直接/間接的給通信雙方的進程提供"內存空間"
- 要通信的不同進程必須看到同一份公共資源
1.2 進程間通信的分類
- 管道
- SystemV(本文只討論共享內存) -- 讓通信過程可以跨主機
- POSIX -- 聚焦在本地通信
2. 管道
2.1 什么是管道
- 管道是 Unix 中最古老的進程間通信的形式
- 我們把從一個進程連接到另一個進程的一個數據流稱為一個 “ 管道 ”?
管道又分匿名管道和命名管道兩種。?
2.2 匿名管道
2.2.1 接口
#include <ustdio.h>
int pipe(int pipefd[2]);?
參數:piepfd[]為輸出型參數,pipefd[0]為讀文件描述符,pipefd[1]為寫文件描述符,若為其他的文件描述符使用,一般這兩個fd分別為3、4。返回值:創建成功返回0,失敗返回-1?
2.2.2 步驟--以父子進程通信為例
- 父進程利用pipe接口創建管道,分別以讀和寫打開一個文件?
- 父進程fork出子進程
- 父進程關閉fd[1],子進程關閉fd[0]
- 這樣父進程就可以往管道文件中寫數據,子進程從管道文件中讀數據,實現了父子進程的通信
注:管道一般是單向的,其實管道也是一個文件("內核級文件")--不需要進行磁盤IO(當然也可以用磁盤文件來實現這個管道操作,但是要進行磁盤讀取,太慢了)
?若是管道中沒有數據了,但是讀端還在讀,OS會直接阻塞當前正在讀取的進程。
2.2.3?站在文件描述符角度-深度理解
2.2.4 管道代碼
????????在這個代碼部分,可以實驗當讀快寫慢、讀慢寫快、只讀關閉、只寫關閉四種情況,這里只給出了只有讀關閉的情況
#include <iostream> #include <cstdio> #include <unistd.h> #include <cassert> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> #include <cstring> using namespace std; int main() { // 第一步:創建管道文件 int fds[2]; int n = pipe(fds); assert(n == 0); // 0 1 2應該是被占用的 _-> 3 4 cout << "fds[0]: " << fds[0] << endl; cout << "fds[1]: " << fds[1] << endl; // 第二步:fork pid_t id = fork(); assert(id >= 0); if(id == 0) { // 子進程的通信代碼 子進程寫入 close(fds[0]); // 通信代碼 // string msg = "hello, i am child!"; int cnt = 0; const char* s = "我是子進程,我正在給你發消息!"; while(1) { cnt++; char buffer[1024]; // 只有子進程能看到 snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid()); // 往文件中寫入數據 write(fds[1], buffer, strlen(buffer)); // sleep(50); // 細節 每隔一秒寫一次 // break; }cout << "子進程關閉寫端" << endl;close(fds[1]);exit(0);} // 父進程的通信代碼 父進程讀取close(fds[1]);while(1){char buffer[1024];// cout << "AAAAAAAAAAAAAAA" <<endl;// 父進程在這里阻塞等待ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);// cout << "BBBBBBBBBBBBBBB" <<endl;if(s > 0) {buffer[s] = 0;cout << " Get Message# " << buffer <<" | my pid: " << getpid() << endl;}else if(s == 0){cout << "read: " << s << endl;break;}// cout << "Get Message#" << buffer << " | my pid: " << getppid() << endl;// 細節:父進程可沒有進行sleep//sleep(5);// close(fds[0]);break;}close(fds[0]);int status = 0;cout << "父進程關閉讀端" << endl;n = waitpid(id, &status, 0);assert(n == id);cout << "pid->" << n << ":" << (status & 0x7F) << endl; // 信號為13:SIGPIPE 中止了寫入進程return 0;}
? ? ? ?由上面代碼結果可以看出,當讀關閉時,OS會終止寫端,給寫進程發送信號,終止寫端。寫進程收到13號信號
2.2.5 讀寫特征
- 讀快,寫慢 -- 讀進程會阻塞,等到管道中有數據時繼續讀取,子進程沒有寫入的那段時間,? ? ? ? ? ? ? ? ? ? ? ? ? 若管道中沒有數據時,父進程會在read處阻塞等待
- 讀慢,寫快 -- 寫進程正常寫數據,管道寫滿時,會在write處阻塞,讀進程就緒時,繼續讀取? ? ? ? ? ? ? ? ? ? ? ? ?數據
- 寫關閉 --?管道中的數據會被讀取完畢后返回EOF,此時?
read
?函數會返回0,最后等待子進程關? ? ? ? ? ? ? ? ? 閉讀端 - 讀關閉 -- OS會中止寫端,給寫端發送信號--13 SIGPIPE,終止寫端
2.2.6?管道特征
- 管道的生命周期隨進程
- 管道可以用來進行具有血緣關系的進程通信,常用于父子進程
- 管道是面向字節流的
- 單向通信 -- 半雙工通信
- 互斥與同步機制 -- 對共享資源進行保護額方案
2.3 命名管道
- 管道應用的一個限制就是只能在具有共同祖先(具有親緣關系)的進程間通信。
- 如果我們想在不相關的進程之間交換數據,可以使用FIFO文件來做這項工作,它經常被稱為命名管道。
- 命名管道是一種特殊類型的文件
- 在用命名管道實現兩個進程通信時,任意一個進程調用mkfifo創建命名管道即可
2.3.1?接口
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
參數:pathname:命名管道的路徑名? mode:管道權限
返回值:成功返回0;失敗返回-1,并設置errno來指示錯誤原因
int unlink(const char* pathname);? --在進程結束后,清理和刪除命名管道。
參數:命名管道的路徑名
返回值:成功返回0;失敗返回-1,并設置errno來指示錯誤原因
?命名管道可以從命令行上創建,命令行方法是使用下面這個命令:
mkfifo filename? # filename為命名管道文件名
2.3.2 代碼實現
#include <iostream>
#include "comm.hpp"
using namespace std;int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;cout << "server begin" << endl;int rfd =open(NAMED_PIPE, O_RDONLY); // 只讀方式打開cout << "server end" << endl; if(rfd < 0) { cout << "文件打開失敗!" << endl; exit(1); } // read char buffer[1024]; while(true) { ssize_t s = read(rfd, buffer, sizeof buffer - 1); if(s > 0) { buffer[s] = 0; std::cout << "client->server:" << buffer << endl; } else if(s == 0) { cout << "client quit, me too!" << endl; break; } else{ cout << "err string:" << strerror(errno) << endl; break; } }close(rfd);// sleep(10);removeFifo(NAMED_PIPE);return 0;
}
client.cc
#include <iostream>
#include "comm.hpp"
using namespace std; int main()
{ // 與server打開同一個文件 cout << "client begin" << endl; int wfd = open(NAMED_PIPE, O_WRONLY); cout << "client end" << endl; if(wfd < 0) { cout << "文件打開失敗!" << endl; exit(1); } // write char buffer[1024]; while(true) { cout << "Please Say# "; fgets(buffer, sizeof(buffer)-1, stdin); if(strlen(buffer) > 0) buffer[strlen(buffer)-1] = 0; ssize_t s = write(wfd, buffer, strlen(buffer)); assert(s == strlen(buffer)); (void)s; } close(wfd); return 0;
}
comm.hpp?
#pragma once #include <string>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <cassert>
#include <cerrno>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h> #define NAMED_PIPE "/tmp/mypipe.106" bool createFifo(const std::string& path)
{ umask(0); int n = mkfifo(path.c_str(), 0600); // 只允許擁有者通信 if(n == 0) return true; else{ std::cout << "erro" << errno << "err string: " << strerror(errno) << std::endl; return false; }
} void removeFifo(const std::string & path)
{ int n = unlink(path.c_str()); assert(n == 0); // debug有效,release里面就無效 (void)n; // 不想有警告
}
可以看到client可以向server端發送數據,server收到并打印到屏幕中,實驗結果如下圖所示:?
下圖為命名管道的信息:?
?
?2.3.3 匿名管道和命名管道的區別
- 匿名管道由pipe函數創建并打開。
- 命名管道由mkfifo函數創建,打開用open
- FIFO(命名管道)與pipe(匿名管道)之間唯一的區別在它們創建與打開的方式不同,一但這些工作完成之后,它們具有相同的語義。
3.?system V共享內存
3.1 共享內存的原理?
原理:是不同的進程通過各自的PCB和頁表訪問同一快共享內存
3.2 步驟
- 申請一塊空間
- 將創建好的內存映射(將進程和共享內存掛接)到不同的進程地址空間
- 若未來不想通信:取消進程和內存的映射關系--去關聯、釋放共享內存?
3.3 系統接口
#include <sys/ipc.h>
#include <sys.shm.h>
int shmget(key_t key, size_t size, int shmflg);
參數:key: 進行唯一性標識 -- 將key使用shmget設置進入共享內存屬性中,用來表示共享? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 內存在內核中的唯一性
? ? ? ? ? ?size:申請空間大小--一般為4KB的整數倍
? ? ? ? ? ?shmflg:IPC_CREAT--如果指定的共享內存不存在,創建;如果存在,獲取共享內存
? ? ? ? ? ? ? ? ? ? ? ? ? IPC_EXCL--無法單獨使用? 使用:IPC_CREAT|IPC_EXCL:如果不存在,? ? ? ? ? ? ? ? ? ? ? ? ? ? 創建--創建的一定是一個新的共享內存;存在則出錯返回,還可以通過其? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 設置共享內存的權限
返回值:成功返回標識符shmid;失敗,返回-1,與文件不同
key_t ftok(char* pathname, char proj_id); -- 使用給定路徑名命名的文件的標識(必須引用一個現有的,可訪問的文件)和proj_id的最低有效8位(必須是非零的)來生成key_t類型的System V IPC密鑰返回值:成功返回key_t值,失敗返回-1
解析:
? ? ? ? 創建共享內存時,如何保證其在系統中是唯一的?-- 通過參數key確定的,只要保證另一個要通信的進程看到相同的key值,通過在各個共享內存的屬性中查找相同的key,即可找到同一塊共享內存--通過相同的pathname和proj_id在不同的進程中調用ftok獲得相同的key。那么key在哪里呢? -- 在結構體struct stm中。
IPC資源的特征:共享內存的生命周期是隨OS的,不是隨進程的,若沒有對共享內存進行手動的刪除,那么該資源不會消失
查看IPC資源的命令:ipcs -m(共性內存)? /-q(消息隊列)/-s(信號量)
刪除IPC資源的執行:ipcrm -m shmid
操作共享內存
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
參數:shmid:shmget的返回值--要控制哪一個共享內存
????????? ?cmd:IPC_RMID -- 刪除共享內存段 誰創建誰刪除
? ? ? ? ? ? ? ? ? ? ? IPC_STAT -- 獲取共享內存屬性
? ? ? ? ? ? ? ? ? ? ? IPC_SET -- 設置共享內存屬性
? ? ? ? ? ?buf:
返回值:失敗返回-1
關聯進程
void* shmat(int shmid, const void* shmaddr, int shmflg);
參數:shmid:
????????? ?shmaddr:將共享內存映射到哪一個地址空間中,一般設為nullptr?核心自動選擇? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 一個地址
? ? ? ? ? ?shmflg:一般設置為0,讀寫權限
返回值:共享內存空間的起始地址
去關聯:并不是刪除共享內存,而是去除PCB和共享內存的映射關系int shmdt(const void* shmaddr);
參數:shmaddr-由shmat所返回的指針
返回值:失敗返回-1
3.4 代碼
// common.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_ #include <iostream>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h> #define PATHNAME "."
#define PROJ_ID 0x66
#define MAX_SIZE 4096 key_t getKey()
{ key_t k = ftok(PATHNAME, PROJ_ID); // 可以獲取同樣的key! if(k < 0) { // cin cout cerr -> stdin stdout stderr -> 0,1,2 std::cerr << errno << ":" << strerror(errno) << std::endl; exit(1); } return k;
} int getShmHelper(key_t k, int flags)
{ int shmId = shmget(k, MAX_SIZE, flags); if(shmId < 0) { std::cerr << errno << ":" << strerror(errno) << std::endl; exit(2); } return shmId;
}
// 給之后的進程獲取共享內存
int getShm(key_t k)
{return getShmHelper(k, IPC_CREAT/*可以設定為0*/);
}// 給第一個進程使用 創建共享內存
int creatShm(key_t k)
{return getShmHelper(k, IPC_EXCL | IPC_CREAT | 0600); // 0600為權限
}void *attachShm(int shmId)
{void *mem = shmat(shmId, nullptr, 0); // 64位系統 指針占8字節if((long long)mem == -1L){std::cerr << "shmat " << errno << ":" << strerror(errno) << std::endl;exit(3);}return mem;
}void detachShm(void *start)
{if(shmdt(start) == -1){std::cerr << errno << ":" << strerror(errno) << std::endl;}
}void delShm(int shmId)
{if(shmctl(shmId, IPC_RMID, nullptr) == -1){std::cerr << errno << ":" << strerror(errno) << std::endl;}
}#endif//shm_client.cc//
#include "common.hpp"
using namespace std; int main()
{ key_t k = getKey(); printf("0x%x\n", k); int shmId = getShm(k); printf("shmId:%d\n", shmId); // 關聯 char *start = (char*)attachShm(shmId); printf("sttach success, address start: %p\n", start); // 使用 const char* message = "hello server, 我是另一個進程,正在和你通信!"; pid_t id = getpid(); int cnt = 1; // char buffer[1024]; while(true) { sleep(1); // 直接將需要傳遞的信息寫在共享內存字符串中 省去了很多拷貝的過程 提高了傳輸信息的效率 snprintf(start, MAX_SIZE, "%s[pid:%d][消息編號:%d]", message, id, cnt++); // snprintf(buffer, sizeof(buffer), "%s[pid:%d][消息編號:%d]", message, id, cnt); // memcpy(start, buffer, strlen(buffer)+1); } // 去關聯 detachShm(start); // done return 0;
} /shm_server.cc///
#include "common.hpp"
using namespace std; int main()
{ key_t k = getKey(); printf("0x%x\n", k); // 申請共享內存 int shmId = creatShm(k); printf("shmId:%d\n", shmId); sleep(3); // 關聯 // 將共享內存看為一個字符串 char *start = (char*)attachShm(shmId); printf("sttach success, address start: %p\n", start); // 使用 while(true) { printf("client say: %s\n", start); sleep(1); } // 去關聯 detachShm(start); sleep(5); // 刪除共享內存 delShm(shmId); return 0;
}
上面的代碼我們看到的現象是:
通過共享內存的方式實現了進程間通信
3.5 共享內存的優缺點
優點:
- 共享內存是所有通信中最快的,大大減少數據的拷貝次數
缺點:
- 不會給我們進行同步和互斥,沒有對數據進行任何保護
問題--同樣的代碼,管道和共享內存方式實現各需要多少次拷貝??
4.信號量
4.1 相關概念
信號量?-- 本質是一個計數器,通常用來表示公共資源中,資源數量的多少問題
公共資源?-- 被多個進程同時訪問的資源,訪問沒有保護的公共資源時,可能會導致數據不一致? ? ? ? ? ? ? ? ? ? ? ? ?問題
為什么要讓不同進程看到同一份資源? -- 實現進程間的協同工作,但是進程是具有獨立性的,? ? ? 為了讓進程看到同一份資源,提出了進程間通信的方法,但是又帶來了新的問題--數據不一致問題
臨界資源:將被保護起來的公共資源稱為臨界資源,但是大部分的資源是獨立的,只有少量的屬于臨? ? ? ? ? ? ? ? ? ? ? 界資源,資源就是內存、文件、網絡等
臨界區:進程訪問臨界資源的代碼被稱為臨界區,與之對應的為非臨界區
保護公共資源:互斥、同步
原子性:要么不做,要做就做完,只有兩種狀態
4.2 信號量 -- 對資源的一種預定
為什么要有信號量?
設sem=20; sem--;// P操作,訪問公共資源;sem++;// V操作,釋放公共資源? --PV操作
所有的進程在訪問公共資源前都必須先申請sem信號量,前提是所有進程必須先看到同一個信號量,那么信號量本身就是公共資源--也要保證自己的安全--信號量++、--的操作是原子操作
如果一個信號量的初始值為1,二維信號量/互斥信號量
4.3 系統接口
頭文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
申請信號量
int semget(key_t key, int nsems, int semflg);
參數:? ?key:對申請的信號量進行唯一性標識
? ? ? ? ? ? ? nsems:申請幾個信號量,與信號量的值無關
? ? ? ? ? ? ? semflg:與共享內存的flag相同含義
返回值:成功返回信號量標識符semid,失敗返回-1
刪除信號量
int semctl(int semid, int semnum, int cmd,...);
參數:semid:信號量id,semget返回的值
? ? ? ? ? ?semnum:信號量集的下標
? ? ? ? ? ?cmd:IPC_RMID、IPC_STAT、IPC_SET
返回值:失敗返回-1?
操作信號量
int semop(int semid, struct sembuf* sops, unsigned nsops);
參數:semid:信號量id
???????????sops:
信號量的詳細操作會在多線程部分講解?