この項目ではユーザーにファイルマッピングしたページを返すアロケーターの検討をします。
プラットフォームはページは最低でも 4096 バイトぐらいになると考えてください。
さらに割り当てられる 2 番目に小さいサイズは 8192 バイトというように、ページサイズの倍数しか割り当てられないです。
14 #define PAGE_SIZE 4096 15 #define PAGE_ROUND_UP(x) ( x + PAGE_SIZE-1) & (~(PAGE_SIZE-1))
PAGE_ROUND_UP マクロを使って次のページサイズをチェックしてみると良いでしょう。
ユーザーアプリケーション側にこれを返す場合は、オブジェクトのライフタイムの管理はユーザーアプリケーションでやってもらうしかなさそうです。
つまり construct() や destroy() は実装することがあっても、たぶん使わないと思います。
では実装例を見てみましょうかね。
1 #include <memory> 2 #include <string> 3 #include <map> 4 #include <cerrno> 5 #include <vector> 6 #include <cmath> 7 #include <cstdio> 8 #include <fcntl.h> 9 #include <sys/mman.h> 10 #include <sys/stat.h> 11 #include <sys/types.h> 12 #include <cstring> 13 #include <unistd.h> 14 15 #define PAGE_SIZE 4096 16 #define PAGE_ROUND_UP(x) ( x + PAGE_SIZE-1) & (~(PAGE_SIZE-1)) 17 18 class shared_mmap_allocator { 19 20 typedef std::size_t size_type; 21 typedef std::map<void*, size_type> size_map_t; 22 typedef std::map<size_type, std::vector<void*>> free_list_t; 23 24 public: 25 26 shared_mmap_allocator() = delete; 27 shared_mmap_allocator(int fd) : M_fd_(fd) 28 {} 29 30 ~shared_mmap_allocator() { 31 for(auto& addr : M_size_map_){ 32 std::printf("%p\n",addr.first); 33 if(munmap(addr.first,addr.second) == -1){ 34 std::perror("munmap failed"); 35 } 36 } 37 close(M_fd_); 38 } 39 40 void* allocate(size_t n) { 41 42 size_t page_multiple = n * M_page_size_; 43 std::printf("page_multiple = %lu\n",page_multiple); 44 45 auto& free_chunks = M_free_list_[page_multiple]; 46 if (!free_chunks.empty()) { 47 void* pblock = free_chunks.back(); 48 free_chunks.pop_back(); 49 return pblock; 50 } 51 52 struct stat sb; 53 char buf[page_multiple]; 54 std::memset(buf,0,page_multiple); 55 56 off_t seek_end_offset = lseek(M_fd_, 0, SEEK_END); //ファイルの終端 57 58 write(M_fd_,buf,page_multiple); 59 if(fstat(M_fd_,&sb) == -1){ 60 std::perror("stat"); 61 exit(1); 62 } 63 64 lseek(M_fd_,-page_multiple,SEEK_END); 65 66 void* pblock = mmap( 67 NULL, 68 sb.st_size, 69 PROT_READ | PROT_WRITE, 70 MAP_SHARED, 71 M_fd_, 72 seek_end_offset); //ファイル終端からマッピング開始 73 if(pblock == MAP_FAILED){ 74 std::perror("mmap"); 75 exit(1); 76 } 77 M_size_map_[pblock] = page_multiple; 78 return pblock; 79 } 80 81 void deallocate(void* p, size_t ) { 82 auto chunk_size = M_size_map_[(void*)p]; 83 M_free_list_[chunk_size].push_back((void*)p); 84 } 85 86 private: 87 const int M_page_size_ = sysconf(_SC_PAGE_SIZE); 88 int M_fd_; 89 size_map_t M_size_map_; 90 free_list_t M_free_list_; 91 }; 92 93 94 template<typename T> 95 class block_allocator { 96 using value_type = T; 97 using pointer = T*; 98 using const_pointer = const T*; 99 using reference = T&; 100 using const_reference = const T&; 101 using size_type = std::size_t; 102 using difference_type = off_t; 103 104 public: 105 template <class U> 106 struct rebind { 107 typedef block_allocator<U> other; 108 }; 109 110 static block_allocator* create_allocator(std::string filename) { 111 int fd = open(filename.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644); 112 if (fd == -1) { 113 return nullptr; 114 } 115 return new block_allocator(fd); 116 } 117 118 block_allocator(int fd) : M_shared_mmap_allocator_(fd) 119 {} 120 121 template <class U> 122 block_allocator(const block_allocator<U>& ){} 123 124 pointer allocate(size_t n) { 125 return reinterpret_cast<pointer>(M_shared_mmap_allocator_.allocate(n)); 126 } 127 128 void deallocate(void *p, size_t n){ 129 M_shared_mmap_allocator_.deallocate(p,n); 130 } 131 132 void construct(pointer p, const_reference val) { 133 new ((void*)p) T(val); 134 } 135 136 void destroy(pointer p) { p->~T(); } 137 138 template <class U, class... Args> 139 void construct(U* p, Args&&... args) { 140 ::new ((void*)p) U(std::forward<Args>(args)...); 141 } 142 143 template <class U> 144 void destroy(U* p) { 145 p->~U(); 146 } 147 148 private: 149 shared_mmap_allocator M_shared_mmap_allocator_; 150 }; 151 152 int main() 153 { 154 block_allocator<void*>* ba = block_allocator<void*>::create_allocator("abc.txt"); 155 void* ptr = ba->allocate(2); 156 char* iptr = static_cast<char*>(ptr); 157 for(int i = 0; i < 8; ++i){ 158 iptr[i] = i; 159 } 160 iptr[8] = '\0'; 161 ba->deallocate(ptr,2); 162 void* nptr; 163 nptr = ba->allocate(1); 164 char* cptr = static_cast<char*>(nptr); 165 for(int i = 0; i < 8; ++i) { 166 cptr[i] = 10 + i; 167 } 168 ba->deallocate(nptr,1); 169 delete ba; 170 171 int fd = open("abc.txt",O_RDONLY); 172 char buf[10000]; 173 read(fd,buf,10000); 174 175 for(int i = 0; i < 8; ++i){ 176 std::printf("buf[%d] = %d\n",i,buf[i]); 177 } 178 for(int i = 8192; i < 8200; ++i){ 179 std::printf("buf[%d] = %d\n",i,buf[i]); 180 } 181 return 0; 182 }
ビルドと実行結果.
$ g++ main.cpp -std=c++17 -g $ ./a.out page_multiple = 8192 page_multiple = 4096 0x7f0efa80a000 0x7f0efa80d000 buf[0] = 0 buf[1] = 1 buf[2] = 2 buf[3] = 3 buf[4] = 4 buf[5] = 5 buf[6] = 6 buf[7] = 7 buf[8192] = 10 buf[8193] = 11 buf[8194] = 12 buf[8195] = 13 buf[8196] = 14 buf[8197] = 15 buf[8198] = 16 buf[8199] = 17
まずはユーザーインターフェースと内部実装のクラスは分けることにします。
インスタンスについてはデフォルトコンストラクターは使わずに create_allocator() を作って行います。
94 template<typename T> 95 class block_allocator { // 中略 110 static block_allocator* create_allocator(std::string filename) { 111 int fd = open(filename.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644); 112 if (fd == -1) { 113 return nullptr; 114 } 115 return new block_allocator(fd); 116 } 117 118 block_allocator(int fd) : M_shared_mmap_allocator_(fd) 119 {} // 中略 148 private: 149 shared_mmap_allocator M_shared_mmap_allocator_; 150 };
fd は open() 関数で取得したファイル記述子となります。
後は block_allocator クラステンプレートのインスタンスから、shared_mmap_allocator の関数を呼び出す感じです。
shared_mmap_allocator は以下のようにコピーコンストラクターだけ定義しておきます。
18 class shared_mmap_allocator { 19 20 typedef std::size_t size_type; 21 typedef std::map<void*, size_type> size_map_t; 22 typedef std::map<size_type, std::vector<void*>> free_list_t; 23 24 public: 25 26 shared_mmap_allocator() = delete; 27 shared_mmap_allocator(int fd) : M_fd_(fd) 28 {} // 中略 86 private: 87 const int M_page_size_ = sysconf(_SC_PAGE_SIZE); 88 int M_fd_; 89 size_map_t M_size_map_; 90 free_list_t M_free_list_; 91 };
このアロケーターはページをユーザーに返すのでブロックサイズではなく、ページサイズ M_page_size_ を基準にメモリーの割り当てをします。
87 const int M_page_size_ = sysconf(_SC_PAGE_SIZE);
sysconf() 関数によってシステムのページサイズを取得しておきます。
割り当てサイズはこのページサイズの倍数にするために使います。
では allocate() 関数を見てみましょう。
40 void* allocate(size_t n) { 41 42 size_t page_multiple = n * M_page_size_; 43 std::printf("page_multiple = %lu\n",page_multiple); 44 45 auto& free_chunks = M_free_list_[page_multiple]; 46 if (!free_chunks.empty()) { 47 void* pblock = free_chunks.back(); 48 free_chunks.pop_back(); 49 return pblock; 50 } 51 52 struct stat sb; 53 char buf[page_multiple]; 54 std::memset(buf,0,page_multiple); 55 56 off_t seek_end_offset = lseek(M_fd_, 0, SEEK_END); //ファイルの終端 57 58 write(M_fd_,buf,page_multiple); 59 if(fstat(M_fd_,&sb) == -1){ 60 std::perror("stat"); 61 exit(1); 62 } 63 64 lseek(M_fd_,-page_multiple,SEEK_END); 65 66 void* pblock = mmap( 67 NULL, 68 sb.st_size, 69 PROT_READ | PROT_WRITE, 70 MAP_SHARED, 71 M_fd_, 72 seek_end_offset); //ファイル終端からマッピング開始 73 if(pblock == MAP_FAILED){ 74 std::perror("mmap"); 75 exit(1); 76 } 77 M_size_map_[pblock] = page_multiple; 78 return pblock; 79 } 80 81 void deallocate(void* p, size_t ) { 82 auto chunk_size = M_size_map_[(void*)p]; 83 M_free_list_[chunk_size].push_back((void*)p); 84 }
page_multiple はページサイズの倍数となる割り当てサイズです。
52 struct stat sb; 53 char buf[page_multiple]; 54 std::memset(buf,0,page_multiple);
ここでは要素が 0 の buf[] 配列を初期化していますね。
これはファイルを初期化するためのバッファーとして、すぐ後に使います。
56 off_t seek_end_offset = lseek(M_fd_, 0, SEEK_END); //ファイルの終端
seek_end_offset はファイルの終端オフセット(位置)のことですね。
なぜファイルの終端の位置が知りたいかというと、そこに新たにファイルマッピングをするからです。
それでオフセットはあることはあるんですが、キレイな状態でマッピングをするために write() 関数で buf[] を書き込みます。
58 write(M_fd_,buf,page_multiple); 59 if(fstat(M_fd_,&sb) == -1){ 60 std::perror("stat"); 61 exit(1); 62 } 63 64 lseek(M_fd_,-page_multiple,SEEK_END);
書き込み後にすぐにファイルオフセットを lseek() 関数で巻き戻します。
mmap() をしてファイルを書き込む段階で seek_end_offset にしておきます。
66 void* pblock = mmap( 67 NULL, 68 sb.st_size, 69 PROT_READ | PROT_WRITE, 70 MAP_SHARED, 71 M_fd_, 72 seek_end_offset); //ファイル終端からマッピング開始 73 if(pblock == MAP_FAILED){ 74 std::perror("mmap"); 75 exit(1); 76 }
mmap() の引数は前の項目で説明している通りですが、マッピングの開始点は seek_end_offset になります。
それで動作は以下のコードで確認します。
152 int main() 153 { 154 block_allocator<void*>* ba = block_allocator<void*>::create_allocator("abc.txt"); 155 void* ptr = ba->allocate(2); 156 char* iptr = static_cast<char*>(ptr); 157 for(int i = 0; i < 8; ++i){ 158 iptr[i] = i; 159 } 160 iptr[8] = '\0'; 161 ba->deallocate(ptr,2); 162 void* nptr; 163 nptr = ba->allocate(1); 164 char* cptr = static_cast<char*>(nptr); 165 for(int i = 0; i < 8; ++i) { 166 cptr[i] = 10 + i; 167 } 168 ba->deallocate(nptr,1); 169 delete ba; 170 171 int fd = open("abc.txt",O_RDONLY); 172 char buf[10000]; 173 read(fd,buf,10000); 174 175 for(int i = 0; i < 8; ++i){ 176 std::printf("buf[%d] = %d\n",i,buf[i]); 177 } 178 for(int i = 8192; i < 8200; ++i){ 179 std::printf("buf[%d] = %d\n",i,buf[i]); 180 } 181 return 0; 182 }
考え方としては create_allocator() 関数で abc.txt というファイルを開き、ファイル記述子を shared_mmap_allocator クラステンプレートのコンストラクターの引数として使います。
154 block_allocator<void*>* ba = block_allocator<void*>::create_allocator("abc.txt");
次にページ 2 個分をポインター ptr 割り当てます。
そして iptr という文字を指すポインターにキャストして、データを設定します。
155 void* ptr = ba->allocate(2); 156 char* iptr = static_cast<char*>(ptr); 157 for(int i = 0; i < 8; ++i){ 158 iptr[i] = i; 159 }
さらにページ 1 個分をポインター nptr に割り当てます。
これもキャストをしてデータを設定します。
162 void* nptr; 163 nptr = ba->allocate(1); 164 char* cptr = static_cast<char*>(nptr); 165 for(int i = 0; i < 8; ++i) { 166 cptr[i] = 10 + i; 167 }
アロケーターのインスタンスは delete で解放します。
169 delete ba;
これによってデストラクターがコールされます。
30 ~shared_mmap_allocator() { 31 for(auto& addr : M_size_map_){ 32 std::printf("%p\n",addr.first); 33 if(munmap(addr.first,addr.second) == -1){ 34 std::perror("munmap failed"); 35 } 36 } 37 close(M_fd_); 38 }
割り当て済みのデータを M_size_map_ から取り出して munmap でマップを解除します。
解除時にはマッピングした領域に対して変更したデータが反映されます。
ついでですがアドレスを出力し、ファイル記述子をクローズします。
0x7f0efa80a000 0x7f0efa80d000
最後にファイルを読みこんで、ファイルに対してデータが更新されたかチェックします。
171 int fd = open("abc.txt",O_RDONLY); 172 char buf[10000]; 173 read(fd,buf,10000); 174 175 for(int i = 0; i < 8; ++i){ 176 std::printf("buf[%d] = %d\n",i,buf[i]); 177 } 178 for(int i = 8192; i < 8200; ++i){ 179 std::printf("buf[%d] = %d\n",i,buf[i]); 180 }
これの出力は以下の通りです。
buf[0] = 0 buf[1] = 1 buf[2] = 2 buf[3] = 3 buf[4] = 4 buf[5] = 5 buf[6] = 6 buf[7] = 7 buf[8192] = 10 buf[8193] = 11 buf[8194] = 12 buf[8195] = 13 buf[8196] = 14 buf[8197] = 15 buf[8198] = 16 buf[8199] = 17
このようにメモリー内で設定した情報がファイルに反映されています。
しかしユーザーアプリケーション側の方が複雑な気がするので、インターフェースとしてはいまいちです。
実のところ、設計面では明らかに直せる点もあるので、読者さんも時間があったら直して見ると良い練習になるかもしれないですね。
まあ筆者は時間ないっすけどね…
(´・ω・`)
Copyright 2018-2019, by Masaki Komatsu