一、命名管道
1. 命名管道的原理
有了匿名管道,理解命名管道就非常簡單了。
對于普通文件而言,兩個進程打開同一個文件,OS是不會將文件加載兩次的,這兩個進程都會指向同一個文件,那么,也就享有同一份 inode 和 文件緩沖區
。這在一定層面上也是實現了進程之間共享數據。但是,普通文件是需要向磁盤上進行刷新的。
所以,才有了一種特殊的文件:管道,管道(包含匿名管道和命名管道)是不需要向磁盤上進行刷新的,命名管道是文件系統中的一種特殊類型文件。管道就是解決進程之間共享數據的問題的。OS會為每個進程分配一個 struct file 結構體的,但是它們指向同一個 struct pipe_inode_info ,這個結構體里包含環形緩沖區,這就保證了不同的進程之間可以訪問同一份數據
。
2. 命名管道的操作
//命令
mkfifo //創建命名管道
可以看到,fifo 文件的大小是 0,這也說明了數據是不會刷新到磁盤上的。
//系統調用
//mode代表權限
//成功返回0,失敗返回-1
int mkfifo(const char* pathname, mode_t mode);//創建命名管道
//成功返回0,失敗返回-1
int unlink(const char* pathname); //刪除命名管道
3. 命名管道的實現
.
client.cpp
#include "NamePipe.hpp"int main()
{NamePipe name_pipe(fifoname);name_pipe.OpenForWrite();std::string line;while (true){std::cout << "Please Enter# ";std::getline(std::cin, line);name_pipe.Write(line);}name_pipe.Close();return 0;
}
.
common.hpp
#ifndef __COMMON_HPP__
#define __COMMON_HPP__#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>std::string fifoname = "fifo";
mode_t mode = 0666;#define SIZE 128#endif
.
NamePipe.hpp
#include "common.hpp"const int gdefaultfd = -1;
class NamePipe
{
public:NamePipe(std::string &name, int fd = gdefaultfd) : _name(name), _fd(fd){}bool Create(){// server 創建管道int n = mkfifo(fifoname.c_str(), mode);if (n < 0){perror("mkfifo fail");return false;}return true;}bool OpenForRead(){// 命名管道的操作特點:打開一端,另一端沒打開的時候,open會阻塞_fd = open(fifoname.c_str(), O_RDONLY);if (_fd < 0){perror("server open fail");return false;}return true;}bool OpenForWrite(){_fd = open(fifoname.c_str(), O_WRONLY);if (_fd < 0){perror("client open fail");return false;}return true;}bool Read(std::string *out){char buffer[SIZE] = {0};ssize_t num = read(_fd, buffer, sizeof(buffer) - 1);if (num > 0){buffer[num] = 0;// std::cout 也有緩沖區,需要刷新*out = buffer;}else if (num == 0){// client quit,read讀到文件末尾,返回0return false;}else{return false;}return true;}void Write(const std::string &in){write(_fd, in.c_str(), in.size());}void Close(){close(_fd);}void Remove(){unlink(fifoname.c_str()); // 刪除管道文件}~NamePipe(){}private:std::string _name;int _fd;
};
.
server.hpp
#include "NamePipe.hpp"int main()
{NamePipe name_pipe(fifoname);name_pipe.Create();name_pipe.OpenForRead();std::cout << "open file success" << std::endl;std::string message;while (true){bool res = name_pipe.Read(&message);if(!res)break;std::cout << "Client Say@ " << message << std::endl;}name_pipe.Close();name_pipe.Remove();return 0;
}
.
Makefile
.PHONY:all
all:client serverclient:client.cppg++ -o $@ $^ -g -std=c++11
server:server.cppg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f clientrm -f server
命名管道主要解決毫無關系的進程之間,進行文件級進程通信。
匿名管道和命名管道剩下的特點是一樣的。
server 和 client 之所以能夠看到同一份資源,是因為它們打開的是同一份文件,該文件路徑相同,inode相同,所以能夠共享數據
。
看到這里,大家應該明白了,匿名管道和命名管道是非常相似的,匿名管道是內存級文件,是內存模擬出來的,而命名管道是磁盤上的一種特殊文件類型,是有名字的。正是因為有名字,所以才叫做命名管道
。
二、System V共享內存
1. System V的原理
還記得動態庫的加載嗎?進程會把自己依賴的動態庫加載到物理內存里,通過頁表建立虛擬地址和物理地址的映射關系,而動態庫就被映射到了虛擬地址空間中的共享區中
。
那么,一個進程需要的動態庫會被映射到自己的共享區中,另一個進程也需要這個動態庫呢?那么,這個進程也會把動態庫映射到自己的共享區中,OS不會將這個動態庫加載兩次,兩個進程用的是同一個動態庫
。這從某種意義上來說,不就是OS給動態庫開辟了一塊物理內存,這塊物理內存被映射到了進程虛擬地址空間中的共享區上嗎。這不就是兩個進程之間共享內存了嘛!
所以,我們想實現共享內存,就必須
1.創建共享內存
。
2.建立虛擬地址和物理地址的映射關系
。
3.刪除共享內存
。
動態庫的詳細加載過程請看這篇文章:從ELF到進程間通信:剖析Linux程序的加載與交互機制。
假設進程A,進程B之間通過共享內存通信,如果進程C,D也需要通信呢,許多進程之間都需要共享內存通信呢?那么,是不是會有許多共享內存,OS要不要進行管理呢?肯定是要的。先描述再組織。
2. System V的操作
//key表示共享內存段的唯一鍵值(類似文件名)
//size是共享內存段的大小,一般建議是4KB的整數倍
//shmflg 創建方式和訪問權限
//成功返回共享內存標識符,失敗返回-1(類似于文件描述符fd)
//shmflg的選項,介紹兩個
//IPC_CREAT,單獨使用,共享內存不存在,則創建,已存在,直接獲取
//IPC_EXCL,不能單獨使用
//一起使用,共享內存不存在,則創建,已存在,返回出錯
int shmget(key_t key, size_t size, int shmflg)//創建或獲取一個共享內存
//這兩個參數是可以隨便寫的
//成功返回 key,失敗返回-1
key_t ftok(const char* pathname, int proj_id)//形成唯一的鍵值,傳入shmget函數中
注:共享內存的生命周期不隨進程,隨內核
。
所以,共享內存需要我們自己手動釋放資源
。可以使用以下命令查看共享內存的信息以及刪除共享內存。
ipcs -m //查看所有共享內存段的信息
ipcrm -m shmid //刪除指定的共享內存段
可以看到,第一次共享內存是創建出來的,第二次就創建失敗,這也證明了共享內存的生命周期不隨進程。當刪除了指定的共享內存,就可以再次創建共享內存了。
但這種刪除共享內存的方式還是太麻煩了,我們希望進程結束時就刪除掉共享內存。
文件 = 文件屬性 + 文件內容
。
共享內存 = 內存塊 + 共享內存的 struct(shm的屬性)
。
//shmid就是共享內存標識符
//cmd可以使用IPC_RMID,表示立即刪除
//buf設置為NULL
//0表示成功, -1表示失敗
int shmctl(int shmid, int cmd, struct shmid_ds* buf); //對共享內存的屬性做操作
//shmaddr 設置為 nullptr,讓內核自動選擇映射的虛擬地址
//shmflg 設置為 0,缺省,繼承共享內存的權限
//成功了返回共享內存映射到當前進程虛擬地址空間的具體地址,失敗返回-1
void* shmat(int shmid, const void* shmaddr, int shmflg); //將共享內存與虛擬地址進行映射
這是為什么呢?要想掛接成功,就必須對共享內存設置權限才可以
。
可以看到,映射成功了,并且共享內存有了權限,nattch 掛接的進程數量也變為了 1。
在刪除共享內存之前,虛擬地址空間依然和共享內存有著映射關系,但這并不是我們想要的,所以,在刪除共享內存之前,需要先將虛擬地址空間和共享內存去關聯,然后再刪除共享內存
。
//成功返回0,失敗返回-1
int shmdt(const void* shmaddr); //將共享內存與虛擬地址空間進行去關聯
我們說過,OS內會有許多共享內存,所以對于共享內存是需要進行管理的,在用戶層面上提供了一種結構體來描述共享內存。我們來看一下。
我們可以獲取里面的屬性看一看。不過在此之前,需要了解 shmctl
函數的一個選項。
共享內存的特征總結:
1.生命周期隨內核
。
2.共享內存是 IPC 中速度最快的(因為共享內存寫入數據和讀取數據不需要使用系統調用,只需要使用指針就可以完成,系統調用也是有成本的)
。
3.共享內存沒有同步互斥機制,來對多個進程的訪問進行協同
。
3. 共享內存的實現
.
client.cpp
#include "Shm.hpp"int main()
{SharedMemory shm;shm.Get();// sleep(5);shm.Attach();// sleep(5);// 通信shm.SetZero();char ch = 'A';int cnt = 10;while(cnt--){std::cout << "client 開始寫入" << std::endl;shm.AddChar(ch);ch++;sleep(1);}shm.Detach();// sleep(5);return 0;
}
.
Shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define gdefaultsize 4096std::string gpathname = ".";
int gproj_id = 0x66;class SharedMemory
{
private:bool CreateHelper(int flags){// 形成唯一的鍵值_key = ftok(gpathname.c_str(), gproj_id);if (_key < 0){perror("ftok fail");return false;}printf("形成鍵值成功:0x%x\n", _key);// 獲取共享內存_shmid = shmget(_key, _size, flags); // 創建全新的共享內存if (_shmid < 0){perror("shmget fail");return false;}printf("shmid:%d\n", _shmid);return true;}public:SharedMemory(size_t size = gdefaultsize) : _key(0), _size(size), _shmid(-1),_start_addr(nullptr), _windex(0), _rindex(0),_num(nullptr), _data_start(nullptr){}bool Create(){return CreateHelper(IPC_CREAT | IPC_EXCL | 0666);}bool Get(){return CreateHelper(IPC_CREAT);}bool RemoveShm(){// 刪除共享內存int n = shmctl(_shmid, IPC_RMID, NULL);if (n < 0){perror("shmctl fail");return false;}std::cout << "刪除shm成功" << std::endl;return true;}bool Attach(){// 將共享內存映射到虛擬地址空間中//共享內存需要權限才能掛接在指定的進程_start_addr = shmat(_shmid, nullptr, 0);if ((long long)_start_addr == -1){perror("shmat fail");return false;}_num = (int *)_start_addr;_data_start = (char *)_start_addr + sizeof(int);std::cout << "共享內存映射到進程的虛擬地址空間中" << std::endl;return true;}bool Detach(){// 共享內存與虛擬地址空間進行去關聯int n = shmdt(_start_addr);if (n < 0){perror("Detach fail");return false;}std::cout << "虛擬地址空間與共享內存進行去關聯" << std::endl;return true;}void SetZero(){*_num = 0;}void AddChar(char ch){if (*_num == _size - sizeof(int))return;((char *)_data_start)[_windex++] = ch;_data_start[_windex] = 0;_windex %= (_size - sizeof(int));(*_num)++;// std::cout << "Debug: " << _data_start[_windex - 1] << " _num = " << *_num << std::endl;}void PopChar(char *out){// std::cout << " _num = " << *_num;if (*_num == 0)return;*out = _data_start[_rindex++];_rindex %= (_size - sizeof(int));(*_num)--;// std::cout << "client read: " << _data_start[_rindex - 1] << std::endl;}void PrintAttr(){struct shmid_ds ds;int n = shmctl(_shmid, IPC_STAT, &ds);if(n < 0){perror("PrintAttr shmctl fail");return;}printf("key:0x%x\n", ds.shm_perm.__key);printf("shm_atime:%ld\n",ds.shm_atime);printf("shm_nattch:%ld\n",ds.shm_nattch);}int GetCount(){return *_num;}~SharedMemory(){}private:key_t _key;size_t _size;int _shmid;void *_start_addr;int *_num;char *_data_start;int _windex;int _rindex;
};#endif
.
server.cpp
#include "Shm.hpp"int main()
{SharedMemory shm;shm.Create();// sleep(5);shm.Attach();shm.PrintAttr();sleep(2);// 通信char ch;while (true){if(!shm.GetCount()) break;shm.PopChar(&ch);printf("server getchar:%c\n", ch);sleep(1);}shm.Detach();// sleep(5);shm.RemoveShm();// sleep(5);return 0;
}
.
Makefile
.PHONY:all
all:client serverclient:client.cppg++ -o $@ $^ -g -std=c++11
server:server.cppg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f clientrm -f server
今天的文章分享到此結束,覺得不錯的給個一鍵三連吧。