はじめに
前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。こちらはLinuxカーネル4.1のメモリレイアウト(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモで説明しなかったvmalloc()の説明です。
カーネルのバージョンは4.1系です。
文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。
スラブアロケータ以外の動的メモリ確保
vmalloc関数
Linuxカーネルの動的メモリ確保手段にはページ単位で確保するバディシステムや、任意のサイズでメモリを確保するスラブアローケータがありますが、これらの他にもvmalloc関数があります。 vmalloc関数のインターフェースはmalloc(3)と同様で確保するサイズを指定します。vmalloc関数で確保したメモリ領域の解放にはvfree関数を使用します。
void *vmalloc(unsigned long size); void vfree(const void *addr);
非連続領域で使用する仮想アドレスの範囲はアーキテクチャによって違い、x86_64ではarch/x86/include/asm/pgtable_64_types.hにて以下のように32TiBの範囲が割当られています。
#define VMALLOC_START _AC(0xffffc90000000000, UL) #define VMALLOC_END _AC(0xffffe8ffffffffff, UL)
vmalloc関数はkmalloc関数との違い、物理的に連続していないページフレームよりメモリを確保します。kmalloc関数で2ページ分のメモリを確保する場合、2つのページフレームは物理的に連続しますが、vmalloc関数の場合は物理的に連続していません。どちらの場合もリニアアドレスとしては連続しています。このようにvmalloc関数でメモリを確保する場合、ページフレームは物理的に連続していないため、DMAなどで物理的に連続したアドレスを要求するような場合には使用できません。 また、kmalloc関数は要求したサイズに合うサイズのスラブキャッシュからメモリを確保していましたが、vmalloc関数は要求されたサイズを確保できる分のページフレームを確保します。そのため、16バイトのメモリを要求した場合でも1ページをメモリ確保用に使用します。
vmalloc関数で使用するデータ構造
vmalloc関数やioremap関数など、非連続メモリ領域からメモリを確保する場合に使用するデータ構造は主に2つあります。 vm_struct構造体はinclude/linux/vmalloc.hにて定義されており、割当を行ったページフレームや確保したメモリのサイズなど非連続メモリのデータにおける、実際のメモリ管理に関連するデータを扱います。ioremap関数の場合はphys_addrに物理アドレスを設定しますが、それ以外はリニアアドレスを使用します。
変数名 | 内容 |
---|---|
addr | 確保した領域の先頭のリニアアドレス |
size | 確保したメモリのサイズ |
flags | メモリを確保する時のフラグ。表_非連続メモリ領域の確保時に使用するフラグ参照 |
pages | リニアアドレスに割り当てたページフレームの配列 |
nr_pages | pagesで使用しているpage構造体数 |
phys_addr | 物理メモリのアドレス(ioremap関数の場合に使用) |
caller | 非連続メモリ領域を確保した関数のアドレス |
表_vm_struct構造体
非連続メモリ領域の確保時に使用するフラグは表_非連続メモリ領域の確保時に使用するフラグに示したフラグがあります。これらのフラグの定義はinclude/linux/vmalloc.hにあります。
変数名 | 内容 |
---|---|
VM_IOREMAP | ioremap関数でメモリを確保する場合に設定 |
VM_ALLOC | vmalloc関数でメモリを確保する場合に設定 |
VM_MAP | vmap関数でメモリを確保する場合に設定 |
VM_VPAGES | page構造体の配列作成時にvmalloc関数でメモリを確保した場合に設定 |
VM_UNINITIALIZED | メモリの確保途中を表すために設定 |
VM_NO_GUARD | ガード領域を使用しない場合に設定 |
表_非連続メモリ領域の確保時に使用するフラグ
vmap_area構造体は非連続メモリ領域に関するデータを扱います。この構造体のvm変数から、ページフレームなどを管理しているvm_struct構造体にアクセスできます。vmap_area構造体もvm_struct構造体と同じくinclude/linux/vmalloc.hにあります。
変数名 | 内容 |
---|---|
va_start | 開始アドレス |
va_and | 終了アドレス |
flags | この構造体の状態を表すフラグ。表_vmap_areaの状態フラグ参照 |
rb_node | vmap_area構造体をアドレスでソートした赤黒木 |
list | vmap_area構造体を管理する連結リスト |
purge_list | vmap_area構造体のリスト。vmap areaを解放する時に使用する |
vm | このvmap areaと関連しているvm_strcuct構造体 |
rcu_head | vmap_area構造体のメモリを解放するときに使用するRCU構造体 |
表_vmap_area構造体
非連続メモリ領域の確保や解放などではvmap_area構造体のflags変数に現在の処理状況を設定します。この内容は mm/vmalloc.cにて定義されています。
変数名 | 内容 |
---|---|
VM_LAZY_FREE | 使用していたvmap_area構造体を解放する時に設定 |
VM_LAZY_FREEING | vmap_areaa構造体解放処理が進んだ時に、VM_LAZY_FREEからこの状態に変更 |
VM_VM_AREA | vmalloc用にvmap_areaを設定した場合に設定 |
表_vmap_areaの状態フラグ
vmap_area構造体とvm_struct構造体は、vmap_area構造体のrb_node、list変数にて赤黒木と連結リストで結びついています。vmap_area構造体とvm_struct構造体は図_vmap_area構造体とvmstruct構造体の関連のようになります。
図_vmap_area構造体とvm_struct構造体の関連
vmallocの初期化
vmalloc関数を使用する前に、カーネルのブート処理の一環としてvmalloc_init関数で初期化を行います。vmalloc_init関数での初期化の前にvm_area_register_early関数で非連続領域の初期設定が終わっている必要があります。vm_area_register_early関数はブート中に非連続メモリ領域をリストに登録しておき、vmalloc_init関数でvmalloc関数で使用できるようにします。 vmalloc_init関数は最初に全cpuを対象にデータを初期化します。各cpuは連結リストのvmap_block_queueと、vfree関数実行時に使用するワークキューのvfree_deferredを持ちます。vmap_block_queueはリストの初期化、vfree_deferredはリストの初期化と、遅延実行時に呼ばれる関数(free_work関数)の登録を行います。
次に、vm_area_register_early関数で非連続メモリ領域がリストに登録されていた場合は、これらの領域用にvmap_area構造体を作成し、__insert_vmap_area関数で赤黒木のvmap_area_rootと、連結リストのvmap_area_listに登録します。 vmap_area_pcpu_holeにはvmallocが使用する領域の最後のアドレスを設定します。最後にvmap_initializedをtrueに設定して、vmap領域の初期化とマークします。
vmallocでのメモリ確保
vmalloc関数がメモリ確保のインターフェースですが、実際に処理を制御するのは__vmalloc_node_range関数です。vmalloc関数の処理の流れを図_vmallocのコールフローに示します。
vmalloc() (1) -> __vmalloc_node_flags() (2) -> __vmalloc_node() (3) -> __vmalloc_node_range() (4) -> __get_vm_area_node() (5) -> alloc_vmap_area() (6) -> setup_vmalloc_vm() (7) -> vmalloc_area_node() (8) -> remove_vm_area() (9) -> map_vm_area() (10) -> clear_vm_uninitialized_flag() (11)
図_vmallocのコールフロー
vmalloc関数によるメモリの確保では、最初に要求されたsizeをPAGE_SIZEの倍数に切り上げます。そして、確保するページ数を割り出します。そして、vmalloc関数で確保した領域を管理するためのデータとしてvm_struct構造体を使用するため、この構造体用にメモリを確保します。ここではスラブアロケータよりメモリを確保します。 次に、vmap_area構造体の設定を行います。この処理はalloc_vmap_area関数にて行います。まず、vmap_area構造体用にスラブアロケータよりメモリを確保します。 vmalloc関数によるメモリ確保ではガード領域として1ページ余分にページを確保します。この領域はプログラムのミスにより、本来のサイズを超えた位置への書き込みが行われた場合への対処です。この結果、PAGE_SIZE以下のメモリを確保する場合、「図_vmallocで確保するメモリ」のように2ページ分のページフレームを確保することになります。
図_vmallocで確保するメモリ
そして、vmalloc関数で確保するアドレス範囲を決定する処理を行います。この時、vmapキャッシュを使用の有無を選択します。 vmapキャッシュを使用するのは以下の条件の場合です。
- 以前にalloc_vmap_area関数が実行され、vmap探索用のキャッシュ変数のfree_vmap_cacheにデータが設定されている
以下のいずれかの条件を満たす場合にvmapキャッシュを使用しません。
- vmapキャッシュが設定されていない
- sizeが前回設定したcached_hole_sizeより小さい
- vstartが前回設定したcached_vstartより小さい
- alignが前回設定したalignより小さい
vmapキャッシュを設定しない場合は、cached_hole_sizeを0に、vmapキャッシュをNULLにします。 cached_vstartとcached_alignには、引数で受け取ったvstartとalignをそれぞれ設定します。この設定はvmapキャッシュの使用有無に関わらず行います。
そして、探索の第一弾目の処理に入ります。まず、vmapキャッシュを使用する場合、vmapキャッシュからvmap_area構造体を取得します。そして、このvmap_area構造体に設定されている終了アドレスを、引数のalignの境界に整列させます。 この計算結果のアドレスがvstartより小さい場合は、vmapキャッシュ使用なしとしてアドレスの探索をやり直します。アドレス+sizeの結果がオーバーフローする場合はエラーになります。取得した要素はfirst変数に代入します。
vmapキャッシュを使用しない場合は、vstartをalignの境界に整列させてアドレスを計算します。この場合も、アドレス+sizeの結果がオーバーフローする場合はエラーになります。次に、vmap_area構造体を管理している赤黒木のvmap_area_rootより、全vmpa_area構造体を調べていきます。ツリー構造の左右どちらに進むかは、取得したvmap_area構造体の終了アドレスにより変わります。終了アドレスがアドレスよりも小さい場合は右の要素を取得します。もし、終了アドレスがアドレスよりも大きい場合、まず、この要素をfirst変数に代入しておきます。そして、このvmap_area構造体の開始アドレスよりも大きければ探索はここで終了します。違った場合は左の要素を取得し、再度チェックを行います。これで、要素を全て探索するか、途中で探索を終了できるまで行います。ここでの赤黒木の探索は、startで指定されたアドレスよりも大きなアドレスがva_end変数に設定されているvmap_area構造体を探すことです。 この処理で、first変数が設定された場合は以下に続く処理を行います。見つからなければ、先のfoundラベルからの処理を行います。
キャッシュ未使用時に赤黒木の探索後にfirst変数がNULLで無い場合、もしくはキャッシュから探索した場合は以下の処理でアドレスの再設定を行います。
- アドレス+sizeがfirstの開始アドレスより大きく、引数で渡されたvend未満なら2へ
- アドレス+cached_hole_sizeがfirstの開始アドレスよりも小さい場合は、cached_hole_sizeを「firstの開始アドレス - アドレス」に設定
- firstの終了アドレスをalignの境界に合わせた結果をアドレスに代入
- アドレス+sizeがオーバーフローする場合はエラーにする
- firstがvmap_area構造体を管理するリスト(vmap_area_list)の最後の要素なら探索終了してループを抜ける
- firstのlist変数より、firstとリストでつながっている次の要素を取得し、1に戻る
上記のループを抜けた段階で"アドレス+size"がvendを超えている場合はエラーにします。 ここにたどり着くのは、上記のループを行った場合と、キャッシュ未使用時に、赤黒木の探索結果でfirst変数がNULLだった場合です。この場所にはfoundというgoto文のラベルが設定されていいます。 ここまで着た場合、アドレスを利用することができるので、呼び出し元に返却するvmap_area構造体のデータを設定します。開始アドレスはここまでで計算したアドレスを設定します。終了アドレスはアドレス+sizeになります。そして、このvmap_area構造体を__insert_vmap_area関数を使用して赤黒木のvmap_area_rootと、通常のリストのvmap_area_listへ登録します。最後に、設定を行ったvmap_area構造体のrb_nodeをvmapキャッシュのfree_vmap_cacheに設定します。ここまででvmap_area構造体の設定が完了になります。
リニアアドレスの確保はvmapキャッシュの有無などでコードは複雑になっていますが、シンプルなケースではファーストヒットにより割り当てるリニアアドレスを決定しています。
ここで、リニアアドレスを初めて割り当てるのにvmalloc関数を使用する場合を例に見てみます。
最初のリニアアドレス割当時にはvmapキャッシュは存在しませんので、赤黒木のノードを調べることになりますが、初回の場合は赤黒木にデータはないのでfoundラベルに移動することになります。 この時はaddrはリニアアドレス探索開始アドレスのVMALLOC_STARTが設定されています。結果として、vmap_area構造体のva_startとva_endは以下のようになります。
va->va_start = VMALLOC_START va->va_end = va->va_start + size
そして、vmapキャッシュにはこのvmap_area構造体が設定されます。
2回目のvmalloc関数呼び出し時にはvmapキャッシュが設定されています。よって、addrは前回割り当てたリニアアドレスの末端(va->va_end)が設定されます。そして、先に説明した、キャッシュから探索した場合の6ステップの処理を実行します。この場合、5ステップ目のfirst変数がリストの最後の要素(実際に1つしか要素が登録されていませんので、最初の要素でもあり最後の要素でもあります)となってループを抜けます。 この場合もfoundラベルの処理になりますので、以下のようにvmap_area構造体の要素を設定します。addrは前回割り当てたリニアアドレスの終端のアドレスです。
va->va_start = addr va->va_end = addr + size
この時のリニアアドレスの割当状態が図_リニアアドレスの割当です。
図_リニアアドレスの割当
alloc_vmap_area関数の処理が終わり、__get_vm_area_node関数に戻ったら最後にsetup_vmalloc_vm関数を実行します。 ここでは、vmap_area構造体とvm_struct構造体の設定を行います。vm_struct構造体にはアドレス、サイズなどを設定し、vmap_area構造体のvmメンバ変数にvm_struct構造体を設定します。この時にvm_struct構造体のflags変数にVM_UNINITIALIZEDを設定し、この領域はまだ初期化中とマークします。また、この時点でflags変数に対してVM_VM_AREAフラグも設定します。
次にページの割当と設定を行っていきます。まず、メモリを確保するsizeから必要なページフレーム数を計算します。この時点ではsizeは、呼び出し元が設定したサイズではなく、PAGE_SIZEの倍数になっています。 vmalloc関数ではページフレームは物理的に連続していないため、page構造体の配列にて管理する必要があります。 そして、page構造体を管理するためのメモリを確保しますが、この時に、確保するpage構造体の配列のサイズが1ページに収まらない場合、 __vmalloc_node関数を用いてメモリを確保します。1ページに収まる場合はスラブアローケータを利用します。__vmalloc_node関数は__vmalloc_node_range関数を呼び出します。 page構造体管理用の配列のメモリを確保できたら、バディシステムを利用してページフレームを1ページフレームずつ確保していきます。 ページフレームの確保を行ったら、map_vm_area関数にて、ページフレームをページテーブルに設定します。最初にアドレスからページグローバルディレクトリ(PGD)のインデックスを取得し、そこからページアッパーディレクトリ(PUD)、ページミドルディレクトリ(PMD)、ページテーブルエントリ(PTE)と設定していきます。 最後にvm_struct構造体のflagsに設定したVM_UNINITIALIZEDフラグを落としてvmalloc関数の処理は完了です。
vfree関数でのメモリ解放
vfree関数でメモリの解放を行う場合、割り込みコンテキスト中に呼ばれたかを確認します。もし、割り込みコンテキストだった場合は、解放処理を遅延させます。この場合、cpuに設定されているvfree_deferred構造体を取得し、この構造体に解放対象のアドレスを設定し、schedule_work関数でキューに登録して終了します。その後free_work()が呼ばれた時に__vunmap()を呼び出します。 割り込みコンテキストでない場合は__vunmap関数を使用してメモリの解放を行います。メモリの解放は__vunmapがメインの処理です。vfree関数のコールフローを図_vfreeのコールフローに示します。
vfree() (1) -> free_work() (2) -> __vunmap() (3) -> __vunmap() (4) -> remove_vm_area() (5) -> find_vmap_area() (6) -> free_unmap_vmap_area() (7)
図_vfreeのコールフロー
メモリを解放する場合、まずは解放するアドレスで使用しているvmap_area構造体を取得します。そして、vmap_area構造体からvm_struct構造体を取得し、vmap_area構造体からvm_struct構造体への参照を外します。また、vmap_area構造体のflagsからVM_AREAフラグを外して、未使用の状態として設定します。 そして、解放対象のアドレスで使用しているページテーブルを全てクリアします。クリアする順番はPGD、PUD、PMD、PTEの順番です。 次に、使用していた非連続メモリ領域の解放処理を行います。この処理は__purge_vmap_area_lazy関数にて行います。解放処理に入る前にvma_area構造体のflags変数にVM_LAZY_FREEフラグをセットして、これから解放するとマークします。また、vmap_area構造体に設定されているva_endからva_startの範囲から、ページ数を割り出し、vmap_lazy_nr変数に設定します。この変数は後ほど使用します。 __purge_vmap_area_lazy関数ではページを割り当てたvmap_area構造体を管理しているvmap_area_listを1要素ずつ辿りながら解放対象のvmap_area構造体を設定していきます。解放するのはvmap_area構造体のflags変数にVM_LAZY_FREEが設定されているものです。 vmap_area構造体に対する操作は以下の3処理です。
この時に解放するアドレスの範囲から解放するページフレーム数の計算と、解放するアドレスの範囲の再設定を行います。__purge_vmap_area_lazy関数はtry_purge_vmap_area_lazy関数から呼ばれますが、この時、解放するアドレスの範囲として、開始アドレスをULONG_MAX、終了アドレスを0で設定して関数を呼び出します。そして、ループ内で実際の開始・終了アドレスの設定を行います。 vmap_area構造体をvaとして、以下のように設定します。
- va->va_start < startなら開始アドレスをva->startのアドレスに変更
- va->va_end > endなら終了アドレスをva->endのアドレスに変更
ページ数はva->va_end - va->va_startの結果から計算します。
vmap_area_list内の要素に対して設定変更完了後にページ数が設定されている場合、以下の処理を行います。
- vmap_lazy_nrからループ内で数えたページ数を減らす
- start - endの範囲のアドレスを無効にするため、TLBをフラッシュ
- vmap_area構造体の解放
vmap_area構造体の解放処理は__free_vmap_area関数にて行います。この処理ではメモリの確保でvmap_area構造体を設定する時にvmapキャッシュを設定していた場合に、そのキャッシュを無効にします。キャッシュの無効化は2パータンあり、解放するvmap_area構造体の終了アドレスが、cached_vstartに設定されているアドレスより小さい場合はキャッシュをNULLに設定します。 そうでない場合、vmapキャッシュの赤黒木よりvmap_area構造体を取得します。そして、解放対象のvmap_area構造体とキャッシュのvmap_area構造体の開始アドレスを比較し、解放対象の開始アドレスがキャッシュの開始アドレスより小さければ、vmapキャッシュにはキャッシュが登録されている赤黒木のprev要素をvmapキャッシュに設定します。そうでない場合は最初に赤黒木より取得したvmap_area構造体をvmapキャッシュに設定します。 そして、解放対象のvmap_area構造体を赤黒木のvmap_area_rootや、RCUのリストから外します。 解放対象のvmap_area構造体の開始・終了アドレスがvmalloc関数が使用するアドレス範囲(VMALLOC_START - VMALLOC_END)にある時に、既存のvmap_area_per_cpu_holeに設定されているアドレスが、解放対象のvmap_area構造体の終了アドレスより小さい場合はvmap_arep_per_cpu_holeの値を、このvmap_area構造体の終了アドレスに更新します。 最後にvmap_area構造体をメモリを解放します。
その他の非連続メモリ領域からメモリを確保する関数
非連続領域からメモリを確保する関数は、vmalloc関数の他にvmap関数とioremap関数があります。
vmap関数
vmap関数はvmallocと同じくinclude/linux/vmalloc.hにて宣言されています。
extern void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot);
vmap関数は引数で渡されたpage構造体の配列pagesをリニアアドレスにマップします。この関数に渡すpageは非連続メモリ領域からアローケートしたページです。よって、リニアアドレスが連続していない複数個のpage構造体を連続したリニアアドレスにマップすることになります。 vmap関数の場合、vmalloc関数と同様に__get_vm_area_node関数でvmap_areaの設定を行います。この処理はvmalloc関数の場合と変わりません。この後、vmap関数の場合はページフレームはすでに確保されているので、ページフレームの確保は行わず、map_vm_area関数を使用してリニアアドレスとページフレームのマッピングを行います。
ioremap関数
ioremap関数は主にデバイスドライバが使用するための関数です。x86・x86_64環境では物理アドレス0xa000-0xffff(640KiB~1MiB)の範囲はISAデバイスが使用するために予約されていますが、それ以外のアドレス範囲については規定されていません。このような場合にデバイスの物理メモリ領域をカーネルのリニアアドレスにマッピングするためにioremap関数を使用します。
ioremap関数はアーキテクチャ毎に実装が違います。x86_64ではarch/x86/include/asm/io.hにて宣言されています。
static inline void __iomem *ioremap(resource_size_t offset, unsigned long size);
ioremap関数の処理のうち、メモリのマッピングを行う主要な関数は__ioremap_caller()です。この関数よりget_vma_area_caller関数が呼ばれ、さらに__get_vm_area_node関数を実行することで要求したサイズのリニアアドレス範囲を確保することができます。
/proc/vmallocinfoによるメモリ確保状況の確認
vmalloc領域の使用状況は/proc/vmallocinfoファイルで確認することができます。vmalloc関数やvmap関数などではメモリの確保時に__vmap_area関数にて連結リストのvmap_area_listにvmap_area構造体を登録していますので、cat時に表示する内容はこのリストを辿って、vmap_area構造体のデータを整形して表示します。このファイルをcatすると図_vmallocinfoのように出力されます。
表示内容は以下の通りです。
- 使用しているアドレスの範囲
- サイズ
- 呼び出し元の関数(機械語での呼び出し位置/機械語での関数のサイズ)
- 使用しているページフレーム数
- 物理アドレス
- メモリを確保した関数(vmalloc/vmap/ioremap/user(vmalloc_user関数)/vpages(__vmalloc_area_node関数)
- 使用しているNUMAノード(Nの後ろの数値がノードの番号、=の後ろの数字が使用しているページ数)
# cat /proc/vmallocinfo 0xffffc90000000000-0xffffc90000004000 16384 acpi_os_map_iomem+0xef/0x14e phys=bffdf000 ioremap 0xffffc90000004000-0xffffc90000805000 8392704 alloc_large_system_hash+0x160/0x236 pages=2048 vmalloc vpages N0=2048 0xffffc90000805000-0xffffc9000080a000 20480 alloc_large_system_hash+0x160/0x236 pages=4 vmalloc N0=4 0xffffc9000080a000-0xffffc90000c0b000 4198400 alloc_large_system_hash+0x160/0x236 pages=1024 vmalloc vpages N0=1024 0xffffc90000c0b000-0xffffc90000c0e000 12288 alloc_large_system_hash+0x160/0x236 pages=2 vmalloc N0=2 0xffffc90000c0e000-0xffffc90000c2f000 135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32 0xffffc90000c2f000-0xffffc90000c50000 135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32 0xffffc90000c50000-0xffffc90000c52000 8192 bpf_prog_alloc+0x39/0xb0 pages=1 vmalloc N0=1 0xffffc90000c52000-0xffffc90000c54000 8192 acpi_os_map_iomem+0xef/0x14e phys=fed00000 ioremap 0xffffc90000c54000-0xffffc90000cd5000 528384 alloc_large_system_hash+0x160/0x236 pages=128 vmalloc N0=128 0xffffc90000cd5000-0xffffc90000dd6000 1052672 alloc_large_system_hash+0x160/0x236 pages=256 vmalloc N0=256 0xffffc90000dd6000-0xffffc90000df7000 135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32 0xffffc90000df7000-0xffffc90000e18000 135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32 0xffffc90000e18000-0xffffc90000e29000 69632 alloc_large_system_hash+0x160/0x236 pages=16 vmalloc N0=16
図_vmalloinfo
- 作者: タトラエディット
- 出版社/メーカー: 技術評論社
- 発売日: 2019/04/27
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る