1. 什么是system V
System V IPC(Interprocess Communication,進程間通信)是Unix系統中一種經典的進程間通信機制,由AT&T在System V.2版本中引入,并廣泛應用于Linux等現代操作系統中。它通過三種核心機制實現進程間的同步、數據傳遞和資源共享。
在Linux中支持了這種標準,專門設計了IPC通信模塊。
2. 共享內存原理
這里我們先來宏觀的認識一下什么是共享內存。假設有倆個進程ab,如果它們需要進行進程間通信,除了我們之前說的命名管道,還可以通過共享內存的方式實現。我們之前再談動態庫加載原理的時候說過進程的地址空間的堆棧之間有一塊共享區,用來記錄動態庫的虛擬地址。其實,在這塊空間中,還用一片區域->共享內存區。進程ab在物理內存中申請同一塊區域,然后各自通過頁表映射建立物理內存和共享內存區之間的關系。至此,兩個進程就可以看到同一份資源了!以上就是我們對共享內存的宏觀認識,還有許多細節沒有說。
> 我們剛剛所說的所有工作都涉及內核數據結構和磁盤,這些工作都由操作系統完成,我們使用相應的系統調用完成上面的工作。
> 在實際情況中,可能有多組進程在進行進程間通信,那勢必有多個共享內存,有的是被創建,有的正在打開,有的正在關閉……所以,在內核中勢必也會有描述共享內存的結構體對象,也勢必會有管理這些共享內存的內核數據結構!
3. shmget[share memory get]
shmget是我們用來獲取共享內存的接口:
> 參數size是用來設置我們創建共享內存的大小的,很好理解。
> 第三個參數是共享內存標記位, 有兩個選項:IPC_CREAT、IPC_EXCL。
IPC_CREAT:只帶這一個選項表示,如果目標共享內存不存在則創建并打開共享內存,否則就直接打開已近存在的目標共享內存。
IPC_EXCL:該選項單獨使用無任何意義,必須和IPC_CREAT一起使用,使用是表示如果目標內存不存在則創建并打開該共享內存,如果存在,則會直接報錯!從解釋上來看該選項是保證我們創建一個全新的共享內存。
> 不過,這里還有許多問題:我們怎么知道一個共享內存到底是否存在,并且如何保證兩個進程打開同一個共享內存呢?
> 這就由第一個參數key來標識共享內存的唯一性了!這個key不是由內核直接生成,而是讓用戶來構建并傳入給操作系統的。這是為什么呢???
> 假如,進程a在內存中創建了一個共享內存區,內核直接生成對應放id給管理該區域的結構體對象。進程b需要和進程a進行進程間通信,那么進程b就需要拿到這個id來找到同一塊共享內存。但是,id也是數據,如果進程a可以把id給到進程b,它們不就已經可以進程間通信了嗎!!!所以,由內核自己生成是做不到共享內存的!
> 但是,如果這兩個進程其中一方在創建共享內存時就做好約定,在用戶層規定一個key,傳給操作系統,讓這個key來唯一標記這個共享內存。有朝一日,另一個進程就可以通過這個key找到目標共享內存來完成進程間通信!
> 其實,這個key原理不就是我們用路徑和管道文件名找同一個管道文件一樣的嗎。
> 好了,明白了上面的原理,我們就來說說key如何給定。理論上來說,這個key我們可以隨便給,但是,為了減少key之間的沖突,系統給了我們一個生成key的接口ftok()。
這個接口會根據用戶傳入的字符串和id整合形成一個key并返回給用戶,正常情況下,我們也是使用這個接口來生成key。?這里有一個問題:為什么操作系統不這樣設置呢->提供一個系統調用,在內核遍歷已有的key,生成一個不存在的key返回給用戶,這樣做不是更好嗎?但是,如果其他用戶同時生成key,這些key會不會一樣呢?
> 最后,我們再來說一下shmget的返回值:
?如果一個共享內存被創建成功,會返回一個整形來唯一標記這個共享內存。這個整形是給用戶看到的,而key則是給操作系統來尋找共享內存的。這就好比我們之前學文件時,打開一個文件時,系統通過file*的指針找到文件,卻返回一個整形文件描述符fd給用戶使用。
4. 共享內存demon代碼編寫
4.1 預備代碼的編寫過程
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const std::string gpathname = ".";
int gproj_id = 0x66;
int g_size = 1024;
int g_default_id=-1;// 共享內存
class shm
{
public:shm() : _shm_id(g_default_id), _size(g_size) {}// 創建共享內存void create_shm(){// 獲取keykey_t k = ftok(gpathname.c_str(), gproj_id);if (k < 0){// 獲取key失敗ERR_EXIT("ftok");}printf("key:0x%0x\n", k);// 創建共享內存_shm_id = shmget(k, _size, IPC_CREAT | IPC_EXCL);if (_shm_id < 0){// 創建共享內存失敗ERR_EXIT("shmget");}printf("_shm_id:%d\n", _shm_id);}~shm() {}private:int _shm_id;int _size;
};
./server運行之后,我們果然創建共享內存成功了,也看到了相應的key和id。?
不過,當我們結束進程,再次運行時,卻發現共享內存創建失敗了。這說明之前的共享內存并沒有隨進程的結束而釋放,事實上,共享內存的生命周期是隨系統的,我們關閉系統重啟之后,共享內存便釋放了。
我們可以用命令:ipcs? -m 來查看系統內的共享內存情況。【ipcs即查看系統進程間通信資源,-m則標識查看共享內存部分】。
如果我們想要刪除一個共享內存資源,可用命令ipsrm -m +shmid?
接下來,我們來看看代碼級別怎么刪除共享內存資源的,這里我們再認識一個接口shmctl:?
該系統調用是用來控制共享內存資源的,包括刪除,查看共享內存資源屬性等。第二個參數用來控制我們想要對共享內存做和控制,而第三個參數我們暫時用不到,直接設為nullptr即可。
要想釋放目標共享內存,我們僅需將第二個參數設為IPC_RMID即可。好了,下面我們就來實現destroy接口。
// 釋放共享內存資源void destroy(){if (_shm_id == g_default_id){// 沒有創建共享內存資源,無需釋放return;}int n = shmctl(_shm_id, IPC_RMID, nullptr);if (n < 0){ERR_EXIT("shmctl");}printf("共享內存釋放成功! shmid->%d\n", _shm_id);}
?有了上面的鋪墊,我們就可以來學習如何使用共享內存來完成進程間通信了。首先,我們需要將我們創建的共享內存和我們的進程關聯【本質就是讓物理地址和進程虛擬地址空間完成映射】。這由系統完成,所以由對應的系統調用幫我們完成工作->shmat【at->attach】:
第一個參數很好理解,第二個參數用來設置固定虛擬地址映射,在我們應用層開發一般不需要關心這個參數,設為nullptr即可,第三個參數是用來標識我們的共享內存資源的權限的,我們一般設置為0,則系統會使用缺省權限->只讀和只寫。最后,如果成功完成內存和進程地址空間的關聯,則該接口會返回系統選定虛擬地址映射共享內存的起始虛擬地址,這個返回值和我們使用的malloc返回值是一樣的,使用上也沒什么區別,我們這樣就很好理解了。
好了,接下來我們就設計一個接口來完成共享內存和進程的關聯:
//進程和共享內存完成關聯void attach(){_start_addr=shmat(_shm_id,nullptr,0);if((long long)_start_addr<0){ERR_EXIT("shmat");}printf("進程關聯成功!_start_addr->%p\n",_start_addr);}
誒?為什么我們在關聯共享內存時報錯了呢,這個報錯說明我們的行為被決絕了。其實,共享內存和文件在某些方面有些類似。共享內存也有對應的讀寫執權限,所以我們在創建共享內存時,需要設定相應的權限,做法也是和文件權限一樣。
?執行結果:
現在,服務端創建對應的共享內存并完成地址空間的映射。那么,客戶端也需要相應的接口獲取同一份共享內存并進行地址空間的映射。創建和刪除共享內存的工作在客戶端也就不需要做了!
具體做法如下圖所示:
我們做完以上工作之后,就可以開始進程間通信了。做法非常簡單,將我們返回的虛擬地址用指針指向,把共享內存當做長字符串來使用就可以了。
4.2 共享內存優缺點?
最后,我們再來總結一下:我們發現在使用共享內存實現進程間通信的讀寫的時候,并沒有使用系統調用,而我們在使用管道的時候,切切實實的使用了系統調用。很好理解,因為共享內存的讀寫是在進程的虛擬地址空間上面操作的【這屬于用戶層】,所以共享區屬于用戶層可以讓用戶直接使用,而管道讀寫則是在內核文件緩沖區完成的,這屬于系統層。
共享區也是因此有以下的優點:
> 共享區是進程間通信中,速度最快的方式:映射之后,內容直接被雙方進程看到,不需要進行系統調用來獲取和寫入。
不過,共享內存機制的優點也是犧牲了很多換來的:第一點就是共享內存沒有像管道那樣的同步機制,服務端開啟后就一直讀,如果客戶端不開啟呢??而管道文件一方開始讀后,如果另一方沒有寫,則讀取放就會同步阻塞并不開始讀取,知道另一方寫入。另一點就是共享內存沒有保護機制,這里的保護不是指對共享內存的保護,而是對讀寫數據的保護。如果寫入端對自己寫入的內容在讀取時有特定的讀取要求【比如一次讀多少字節,一次讀一整句話等等】,目前的共享內存就無能為力了,因為讀取方不停的讀,就無所謂讀取格式的要求了,它也正因此是進程間通信中最快的方式。
那么,我們目前有沒有什么方案可以將我們的數據保護起來呢?當然有了,我們可以同時用管道把這兩個進程關聯起來。而這個管道的作用就是用來做服務端等待wait,客戶端喚醒wake!假設客戶端自己寫入兩個字符后,服務端成對的讀取打印結果。那我在客戶端寫入成對字符后,通過管道寫入來喚醒【喚醒的方式隨便啦】服務端,服務端通過是否收到管道信號來決定是否讀取共享內存中的數據。如此一來,我們就通過管道,講我們的共享內存的讀寫控制起來了!話不多說,直接上代碼!
客戶端和服務端之間的通信邏輯:
?結果如下:
4.3 共享內存去關聯?
上面,我們在釋放共享內存的時候是直接shmctl刪除的,但是我們再刪除之前還遺漏了一步,就是去關聯!在釋放共享內存之前,我們還應該將關聯該共享內存的所有進程去關聯。這里我們使用shmdt接口即可,使用也很簡單,傳入虛擬地址空間的其實地址即可。
?修改以下部分即可:
4.3 共享內存大小問題?
然后,我們再來說一下創建共享內存時的大小問題,實際上,共享內存的大小一定是4kb的整數倍的 。但是如果我們創建大小時,給定的不是4kb的整數被,那么系統則會向上取整開辟對應大小的共享內存空間。不過,我們用ipcs -m 命令查看到的大小還是我們自己定義的大小!
4.4 獲取共享內存屬性
在最后的最后,我們再來見一見共享內存是如何被系統管理起來的。
內核中,有一種結構體對象shmid_ds,該結構體對象記錄了共享內存的一些屬性,其中有一個結構體ipc_perm里面記錄了共享內存的key!
?我們使用shmctl傳入IPC_STAT參數即可查看到先關的屬性!
5. 源碼?
5.1 comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define FIFO_FILE "fifo"
#define FIFO_PATH "."const std::string gpathname = ".";
int gproj_id = 0x66;
int g_size = 4097;
int g_default_id = -1;
int gmode = 0666;#define USER "user"
#define CREATER "creater"#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)
5.2 Shm.hpp
#include "comm.hpp"// 共享內存
class shm
{
private:// 創建共享內存void create_help(int flag){// 創建共享內存// _shm_id = shmget(k, _size, IPC_CREAT | IPC_EXCL | gmode);_shm_id = shmget(_key, _size, flag);if (_shm_id < 0){// 創建共享內存失敗ERR_EXIT("shmget");}printf("共享內存創建成功!_shm_id->%d\n", _shm_id);}// 創建共享內存void create_shm(){create_help(IPC_CREAT | IPC_EXCL | gmode);}// 獲取共享內存void get_shm(){create_help(IPC_CREAT);}// 釋放共享內存資源void destroy(){if (_shm_id == g_default_id){// 沒有創建共享內存資源,無需釋放return;}// 去關聯detach();if (_user_type == CREATER){int n = shmctl(_shm_id, IPC_RMID, nullptr);if (n < 0){ERR_EXIT("shmctl");}printf("共享內存釋放成功! shmid->%d\n", _shm_id);}}// 進程和共享內存去關聯void detach(){int n = shmdt(_start_addr);if (n < 0)ERR_EXIT("shmdt");printf("共享內存去關聯成功!\n");}// 進程和共享內存完成關聯void attach(){_start_addr = shmat(_shm_id, nullptr, 0);if ((long long)_start_addr < 0){ERR_EXIT("shmat");}printf("進程關聯成功!_start_addr->%p\n", _start_addr);}public:shm(const std::string &pathname, int projid, const std::string &user_type): _shm_id(g_default_id),_size(g_size),_user_type(user_type){// 獲取key_key = ftok(pathname.c_str(), projid);if (_key < 0){// 獲取key失敗ERR_EXIT("ftok");}printf("成功獲取key!key->0x%0x\n", _key);if (user_type == CREATER)create_shm();else if (user_type == USER)get_shm();else{}attach();}~shm(){if (_user_type == CREATER)destroy();}void *get_start_addr(){return _start_addr;}int get_size(){return _size;}// 獲取共享內存相關屬性void get_property(){struct shmid_ds tmp;int n = shmctl(_shm_id, IPC_STAT, &tmp);if (n < 0)ERR_EXIT("shmctl");printf("shm_segsz->%ld\n", tmp.shm_segsz);printf("key->0x%0x\n", tmp.shm_perm.__key);}private:key_t _key;int _shm_id;int _size;void *_start_addr;std::string _user_type;
};
5.3 name_fifo.hpp
#include "comm.hpp"// 命名管道類
class name_fifo
{
public:// 構造函數name_fifo(const std::string &path, const std::string &name): _path(path),_name(name){umask(0);_fifo_name = _path + "/" + _name;// 創建命名管道int n = mkfifo(_fifo_name.c_str(), 0666);if (n < 0){// 命名管道創建失敗// perror("命名管道創建失敗\n");ERR_EXIT("mkfifo");}else{std::cout << "管道創建成功!" << std::endl;}}// 析構函數~name_fifo(){// 刪除管道文件int n = unlink(_fifo_name.c_str());if (n < 0){// perror("刪除管道文件失敗!\n");ERR_EXIT("unlink");}else{std::cout << "管道刪除成功!" << std::endl;}}private:std::string _path;std::string _name;std::string _fifo_name;
};// 命名管道操作類
class fifo_opre
{
public:fifo_opre(const std::string &path, const std::string &name): _path(path),_name(name),_fd(-1){_fifo_name = _path + "/" + _name;}void open_for_read(){// 創建成功->服務端打開管道->讀取內容_fd = open(_fifo_name.c_str(), O_RDONLY);if (_fd < 0){// 命名管道打開失敗// perror("命名管道打開失敗\n");ERR_EXIT("open");}else{std::cout << "管道打開成功!" << std::endl;}}void open_for_write(){// 打開管道文件_fd = open(_fifo_name.c_str(), O_WRONLY);if (_fd < 0){// 命名管道打開失敗// perror("命名管道打開失敗\n");ERR_EXIT("open");}else{std::cout << "管道打開成功!" << std::endl;}}bool wait(){char c;int num = read(_fd, &c, 1);if (num > 0)return true;return false;}bool wake(){char c = 'a';int num = write(_fd, &c, 1);if (num > 0)return true;return false;}void Close(){if (_fd > 0)close(_fd);}~fifo_opre(){}private:std::string _path;std::string _name;std::string _fifo_name;int _fd;
};
5.4 server.cc
#include "comm.hpp"
#include "Shm.hpp"
#include "name_fifo.hpp"int main()
{// 服務端先創建關聯共享內存shm s(gpathname, gproj_id, CREATER);// // 創建命名管道// name_fifo nf(FIFO_PATH, FIFO_FILE);// char *mem = (char *)s.get_start_addr();// // 命名管道讀取端// fifo_opre fifo_reader(FIFO_PATH, FIFO_FILE);// fifo_reader.open_for_read();// // 服務端讀取// while (true)// {// // 如果等待成功則讀取// if (fifo_reader.wait())// {// printf("%s\n", mem);// }// else// break;// }// // 關閉命名管道// fifo_reader.Close();s.get_property();return 0;
}
5.5 client.cc
#include "comm.hpp"
#include "Shm.hpp"
#include "name_fifo.hpp"int main()
{// 客戶端獲取共享內存shm s(gpathname, gproj_id, USER);char *mem = (char *)s.get_start_addr();// 客戶端打開管道寫入喚醒fifo_opre fifo_writer(FIFO_PATH, FIFO_FILE);fifo_writer.open_for_write();int index = 0;for (char c = 'A'; c <= 'Z'; index += 2, c++){sleep(1);mem[index] = c;mem[index + 1] = c;fifo_writer.wake();}return 0;
}
5.6 Makefile
.PHPNY:all
all:client server
client:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server fifo