第102章 ユーザーによるオブジェクト管理を前提にするカスタムページアロケーター

 この項目ではユーザーにファイルマッピングしたページを返すアロケーターの検討をします。

 プラットフォームはページは最低でも 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