Help us understand the problem. What is going on with this article?

Linuxの仮想 - 物理アドレスと、カーネル空間の概要(その2)

More than 5 years have passed since last update.

前置き

前回の資料は、Linuxもくもく会という勉強会で会社帰りに秋葉原でソース読んで投稿しました。
この資料も大部分はLinuxもくもく会で書きました。

前回のあらましと今回見ていくところ

前回、カーネル空間はvmalloc向けと思われるメモリ領域を境に仮想アドレス - 物理アドレスのハンドリングが異なりそうだというところを見てきました。

さて、今回はvmalloc()以降の仮想 - 物理アドレスの変換はどうなっているのか、ここを見てみましょう。

vmallocを歩いてみる

vmallocということは、それっぽいソースを探せば良いや。えい。ということでmm/vmalloc.cを読みます。
すると、以下の関数が目に止まります。

mm/vmalloc.c
unsigned long vmalloc_to_pfn(const void *vmalloc_addr)
{
  return page_to_pfn(vmalloc_to_page(vmalloc_addr));
}
EXPORT_SYMBOL(vmalloc_to_pfn);

vmalloc_to_page()がページ構造体のアドレスを返し、それをpage_to_pfnで物理フレーム番号に変換するのだろう。それではvmalloc_to_page()を見てみましょう。

mm/vmalloc.c
struct page *vmalloc_to_page(const void *vmalloc_addr)
{
  unsigned long addr = (unsigned long) vmalloc_addr;
  struct page *page = NULL;
  pgd_t *pgd = pgd_offset_k(addr);

  /*
   * XXX we might need to change this if we add VIRTUAL_BUG_ON for
   * architectures that do not vmalloc module space
   */
  VIRTUAL_BUG_ON(!is_vmalloc_or_module_addr(vmalloc_addr));

  if (!pgd_none(*pgd)) {
    pud_t *pud = pud_offset(pgd, addr);
    if (!pud_none(*pud)) {
      pmd_t *pmd = pmd_offset(pud, addr);
      if (!pmd_none(*pmd)) {
        pte_t *ptep, pte;

        ptep = pte_offset_map(pmd, addr);
        pte = *ptep;
        if (pte_present(pte))
          page = pte_page(pte);
        pte_unmap(ptep);
      }
    }
  }
  return page;
}
EXPORT_SYMBOL(vmalloc_to_page);

噂には聞いていましたが、壮観ですね。
データ構造としては、PGD - PUD - PMD - PTEの4段階構成のページテーブルになっているように見えます。

ページテーブルということを踏まえると、仮想アドレスを分割して、分割することによって生成した値をインデックスとして、4段のテーブルを下り、最後のPTEにかかれている物理アドレスを取得すると思われます。
(x86のページテーブルはまさにこうなっていますね。)

さて、実際のところはどうなっているのか、それを見ましょう。

x86(32bit)のPGE以下のデータの使われ方

結論から
言うと、以下のような構成になっています。

(x86(32bit/PAEなし)
linux_page_trans_x86_32_nopae.jpg

(x86(32bit/PAEあり)
linux_page_trans_x86_32_pae.jpg

つまり、x86アーキテクチャのページテーブルそのまんまです。
何と言うことはない、ページテーブルを見て、仮想アドレスと物理アドレスの変換をしているだけです。
(良い子のみんなは、Intel SDMに目を通してみよう)

「では、使われていないテーブル(PUDとか)はどう扱っているんだ?」という人は、この文書の最後にAppendixとして概要を載せましたので、参考にしてください。

本文では、途中は省略し、pteのところだけ見てみましょう。

include/asm/pgtable.h
#define pte_val(x)  native_pte_val(x)
/* 略 */
static inline unsigned long pte_pfn(pte_t pte)
{
  return (pte_val(pte) & PTE_PFN_MASK) >> PAGE_SHIFT;
}
/* 略 */
#define pte_page(pte) pfn_to_page(pte_pfn(pte))

要するに、PTEの中身から物理アドレスに該当するところを抜き出しています。
ちなみに、残りの定義は以下のとおりです。

arch/x86/include/asm/pgtable_types.h
/* PTE_PFN_MASK extracts the PFN from a (pte|pmd|pud|pgd)val_t */
#define PTE_PFN_MASK    ((pteval_t)PHYSICAL_PAGE_MASK)
static inline pteval_t native_pte_val(pte_t pte)
{
  return pte.pte;
}
arch/x86/include/asm/page_types.h
#define PAGE_SHIFT  12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))

#define __PHYSICAL_MASK   ((phys_addr_t)((1ULL << __PHYSICAL_MASK_SHIFT) - 1    ))
/* 略 */
#define PHYSICAL_PAGE_MASK  (((signed long)PAGE_MASK) & __PHYSICAL_MASK)
arch/x86/include/asm/page_32_types.h
#ifdef CONFIG_X86_PAE
/* 44=32+12, the limit we can fit into an unsigned long pfn */
#define __PHYSICAL_MASK_SHIFT 44
#define __VIRTUAL_MASK_SHIFT  32

#else  /* !CONFIG_X86_PAE */
#define __PHYSICAL_MASK_SHIFT 32
#define __VIRTUAL_MASK_SHIFT  32
#endif  /* CONFIG_X86_PAE */

ここまで見たとおり、x86の場合は、そのままこのデータ構造が生かされていますね。
PTEの中身を見る箇所などは、CPUアーキテクチャ依存の処理になっていますので、他のCPUアーキテクチャでもこのデータ構造を使えるわけです。
(もちろん、アーキテクチャの中には多少無理矢理感が出てしまうものもあるのでしょうか?ARMもページテーブル方式なので、このデータ形式でも無理はないかと思います)

まるっとCPUアーキテクチャの処理とデータ構造が分離されたBSDと異なるこの実装、新鮮です。
* 念の為に書きますが、どちらの設計もありだと思いますし、優劣をつける気は
ありません。

何となく仮想記憶実装の雰囲気が少しずつつかめてきました。

次回の予定

次回は、メモリマップといえばこのシステムコール、mmap()を読んでみる予定です。

Appendix

最初は、pgd_offset_k()を見てみましょう。

arch/x86/include/asm/pgtable.h
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

/*
 * pgd_offset() returns a (pgd_t *)
 * pgd_index() is used get the offset into the pgd page's array of  t's;
 */
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))
/*
 * a shortcut which implies the use of the kernel's pgd, instead
 * of a process's
 */
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))

struct mmの中のpgdがプロセスのアドレス空間を表現するページディレクトリ(Linuxの実装だとPGD)を指していること、そして、カーネルのアドレス空間を表すstruct mmはどうやらinit_mmのようだということが読み取れます。

次に、以下のヘッダを見ましょう。

arch/x86/include/asm/pgtable_32_types.h
#ifdef CONFIG_X86_PAE
# include <asm/pgtable-3level_types.h>
# define PMD_SIZE (1UL << PMD_SHIFT)
# define PMD_MASK (~(PMD_SIZE - 1))
#else
# include <asm/pgtable-2level_types.h>
#endif

ここから、仮想アドレスが32bitであっても、PAEが有効な場合とそうでない場合とでテーブルの作りが違うことが伺えます。(x86アーキテクチャの仕様上、それは当然であります。)

PGDの場合は、
c:arch/x86/include/asm/pgtable.h
646 #define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

PGDIR_SHIFTとPTRS_PER_PGDは以下のとおりです。

arch/x86/include/asm/pgtable-3level_types.h
#define PGDIR_SHIFT 30
#define PTRS_PER_PGD  4
arch/x86/include/asm/pgtable-2level_types.h
#define PGDIR_SHIFT 22
#define PTRS_PER_PGD  1024
arch/x86/include/asm/pgtable.h
static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
  return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}
/* 略 */
static inline unsigned long pud_index(unsigned long address)
{
  return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
  return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}
arch/x86/include/asm/pgtable.h
#define pgd_val(x)  native_pgd_val(x)
arch/x86/include/asm/pgtable_types.h
static inline pgdval_t native_pgd_val(pgd_t pgd)
{
  return pgd.pgd;
}

つまり、pgdの中身をそのまま返しているだけです。
あれ、grepしてみると、x86のpgtable_64_types.hには定義があるが、x86(32bit)向けの定義がない!と思ったあなた。実はpudを使っていないアーキテクチャの場合、
include/asm-generic/pgtable-nopud.h内の定義が使われます。

include/asm-generic/pgtable-nopud.h
/*
 * Having the pud type consist of a pgd gets the size right, and allows
 * us to conceptually access the pgd entry that this pud is folded into
 * without casting.
 */
typedef struct { pgd_t pgd; } pud_t;

#define PUD_SHIFT PGDIR_SHIFT
#define PTRS_PER_PUD  1
#define PUD_SIZE    (1UL << PUD_SHIFT)
#define PUD_MASK    (~(PUD_SIZE-1))

PUDについても同じ要領となります。(気になる方はソース追ってみてください。)

akachochin
(I'm software engineer in Japan.) おじさんだけど、お仕事ではNetBSD/Linuxカーネルのソースコード読んでる時間が7割程度。エラくはなれないけど、それなりに上々かな。趣味はお名前の通り、居酒屋めぐり。場末の飲み屋でHoppy飲むのが至福です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした