1
0

Linux で DMA Bufferを mmap した時に CPU Cacheが無効になる場合がある (Linux Kernel の Cache 問題の扱い)

Last updated at Posted at 2024-08-02

はじめに

Linux では DMA Bufferを mmap した時に、ある条件が揃うと CPU Cache が無効になり、パフォーマンスが極端に落ちる場合があります。そこで、何故そのようなことが起こるのか説明します。少し長くなるので、次のように記事を幾つかに分けて投稿します。

  1. はじめに
  2. Cache Coherence 問題
  3. Cache Aliasing 問題
  4. Linux Kernel の Cache 問題の扱い (この記事)
  5. Linux では Cache Coherence Hardware を持っていないとDMA Buffer をmmap する際に CPU Cache が無効になる
  6. Raspberry Pi の例
  7. RISC-V CPU の注意点
  8. 所感

1〜3は、コンピューターアーキテクチャの基本的な事項を、簡単に説明したものです。すでにご存じの方は読み飛ばしてください。

4 はこれらの問題を Linux Kernel 内でどのように扱っているかを説明します。

5 がこれらの記事群の結論です。結論だけ知りたい方はここだけ読んでください。

この記事では「Linux で DMA Bufferを mmap した時に CPU Cacheが無効になる場合がある」原因である Cache Coherence 問題と Cache Aliasing 問題を Linux Kernel がどう扱っているかを説明します。

Linux Kernel の Cache 問題の扱い

dma-direct

Linux Kernel では DMA Buffer の管理は DMA Mapping API が窓口になっています。DMA Mapping API はあくまでも API(Application Programming Interface) なので、呼び出される関数の名前や引数の型、変数の名前や型を規定したものです。実際にどのように実装されているかは歴代の Linux Kernel によって移り変わっています。

この DMA Mapping API の実装ですが、Linux Kernel 5.0 から大きな変更が行われました。それまでは、アーキテクチャ毎に個別に実装されており、それらを関数テーブルを介して直接呼び出していました。

Linux Kernel 5.0 以降は dma-direct と呼ばれる新しい実装が導入されました。これは一度 dma-direct が DMA Mapping API の共通の処理を請け負い、どうしてもアーキテクチャに依存するところだけを呼び出すようにしたものです。

ただし、Linux Kernel 5.0 の時点ですべてのアーキテクチャが dma-direct に統合されたわけではありません。arm(32bit) だけが Linux Kernel 5.0 での dma-direct の統合に間に合わずに、Linux Kernel 6.0 以降に統合されました。

この記事で説明する内容は、Linux Kernel 6.1.97 の dma-direct を対象にしています。

dev_is_dma_coherent の紹介

まずは Linux Kernel で Cache Coherence 問題と Cache Aliasing 問題を扱う際にとても重要な役目を持つインライン関数を紹介します。それが dev_is_dma_coherent() です。

dev_is_dma_coherent() は次のように定義されています。

https://elixir.bootlin.com/linux/v6.1.97/source/include/linux/dma-map-ops.h#L267
#if defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_DEVICE) || \
        defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU) || \
        defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU_ALL)
extern bool dma_default_coherent;
static inline bool dev_is_dma_coherent(struct device *dev)
{
        return dev->dma_coherent;
}
#else
static inline bool dev_is_dma_coherent(struct device *dev)
{
        return true;
}
#endif /* CONFIG_ARCH_HAS_DMA_COHERENCE_H */

この定義を見てもわかる通り、dev_is_dma_coherent() は2種類に実装方法があります。この実装方法の違いはアーキテクチャによってだいたい次のように分かれています。

  • struct device の dma_cohrent フィールドの値を返すように定義
    • arc, arm, arm64,m68k, sh 等の主に組み込み系のプロセッサ
    • riscv (linux 6.0 以降)
  • 常に true を返すように定義
    • x86, ia64 等 PC 用のプロセッサ
    • riscv (linux 5.19 以前)

dev_is_dma_coherent の役割その1(Cache Coherence 問題)

『Linux で DMA Bufferを mmap した時に CPU Cacheが無効になる場合がある (Cache Coherence 問題の解決方法)』でソフトウェアでなんとかする方法を説明しましたが、実際に例をあげて説明します。

Linux Kernel では一般的に、DMA Buffer にCPUがアクセスする時と DMA によって DEVICE がアクセスする時を明確に区別するために特定の DMA Mapping API が呼ばれます。それが dma_sync_single_for_cpu() とか dma_sync_single_for_device() です。例えば次のように使われます。

static ssize_t device_file_read(struct file* file, char __user* buff, size_t count, loff_t* ppos)
{        struct object* this = file->private_data;
        int             result = 0;
        size_t         xfer_size;
        size_t         remain_size;
        dma_addr_t  phys_addr;
        void*         virt_addr;
        phys_addr = this->phys_addr + *ppos;
        virt_addr = this->virt_addr + *ppos;
        xfer_size = (*ppos + count >= this->size) ? this->size - *ppos : count;
         
        dma_sync_single_for_cpu(this->dev, phys_addr, xfer_size, DMA_FROM_DEVICE);
        if ((remain_size = copy_to_user(buff, virt_addr, xfer_size)) != 0) {
                result = 0;
                goto return_unlock;
        }
         dma_sync_single_for_device(this->dev, phys_addr, xfer_size, DMA_FROM_DEVICE);
         :
         (中略)
         :
         return result;
}

この例では、copy_to_user() によって DMA Buffer の内容をユーザー空間に転送する前に dma_sync_single_for_cpu() を実行して、これ以降は CPU が DMA Buffer をアクセスすることを明示的に宣言しています。また、DMA Buffer の内容をユーザー空間に転送した後に dma_sync_single_sync_for_device() を実行して、これ以降は CPU は DMA Buffer にアクセスしないことを明示的に宣言しています。

次に dma_sync_single_for_device() をみてみましょう。

https://elixir.bootlin.com/linux/v6.1.97/source/kernel/dma/mapping.c#L342
void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
                size_t size, enum dma_data_direction dir)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        BUG_ON(!valid_dma_direction(dir));
        if (dma_map_direct(dev, ops))
                dma_direct_sync_single_for_device(dev, addr, size, dir);
        else if (ops->sync_single_for_device)
                ops->sync_single_for_device(dev, addr, size, dir);
        debug_dma_sync_single_for_device(dev, addr, size, dir);
}
EXPORT_SYMBOL(dma_sync_single_for_device);

dma-direct では dma_direct_sync_single_for_device() が呼ばれます。そして dma_direct_sync_single_for_device() は次のようになっています。

https://elixir.bootlin.com/linux/v6.1.97/source/kernel/dma/direct.h#L55
static inline void dma_direct_sync_single_for_device(struct device *dev,
                 dma_addr_t addr, size_t size, enum dma_data_direction dir)
{
        phys_addr_t paddr = dma_to_phys(dev, addr);
        if (unlikely(is_swiotlb_buffer(dev, paddr)))
                swiotlb_sync_single_for_device(dev, paddr, size, dir);
        if (!dev_is_dma_coherent(dev))
                arch_sync_dma_for_device(paddr, size, dir);
}

ここでは、dev_is_dma_coherent() が true を返す場合は何もしていません。しかし dev_is_dma_coherent() が false の場合は、 arch_sync_dma_for_device() が呼ばれています。この arch_sync_dma_for_device() はアーキテクチャに実装されていて、主に Cache Flush 操作を行います。

dma_single_sync_for_cpu() の場合もほぼ同様に dev_is_dma_coherent() が false の場合は、arch_sync_dma_for_cpu() が呼ばれ、主に Cache Invalidiate 操作が行われます。

つまり dev_is_dma_coherent() の役割の一つは、次のように推測できます。

  • dev_is_dma_coherent() が true の場合
    • 事実: sync 時には何もしない
    • 推測: このデバイスの Cache Coherence 問題はハードウェアでなんとかする(または Cache が無効になっている)事を示している
  • dev_is_dma_coherent() が false の場合
    • 事実: sync 時に Cache の Flush/Invalidiate を行っている
    • 推測: このデバイスの Cache Coherence 問題はソフトウェアでなんとかする事を示している

dev_is_dma_coherent の役割その2(Cache Aliasing 問題)

Linux Kernel では Cache Aliasing 問題はキャッシュを使わないことで対処しています。というのも、[参考]でも紹介していますが、ページカラーリングは実装コストが高すぎて効果に見合わないことを明言されています。また、ページサイズを大きくすることは、汎用 OS としては好ましくないからでしょう。

そこで DMA Buffer に仮想アドレスを割り当てる手順を追っていきましょう。

まず、DMA Mapping API では、 DMA Buffer に仮想アドレスを割り当てる際に窓口となる関数 dma_mmap_attrs() が次のように定義されています。

https://elixir.bootlin.com/linux/v6.1.97/source/kernel/dma/mapping.c#L457
int dma_mmap_attrs(struct device *dev, struct vm_area_struct *vma,
                void *cpu_addr, dma_addr_t dma_addr, size_t size,
                unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        if (dma_alloc_direct(dev, ops))
                return dma_direct_mmap(dev, vma, cpu_addr, dma_addr, size,
                                               attrs);
        if (!ops->mmap)
                return -ENXIO;
        return ops->mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
}
EXPORT_SYMBOL(dma_mmap_attrs);

dma-direct では dma_direct_mmap() が呼ばれます。そして dma_direct_mmap() は次のようになっています。

https://elixir.bootlin.com/linux/v6.1.97/source/kernel/dma/direct.c#L556
int dma_direct_mmap(struct device *dev, struct vm_area_struct *vma,
                void *cpu_addr, dma_addr_t dma_addr, size_t size,
                unsigned long attrs)
{
        unsigned long user_count = vma_pages(vma);
        unsigned long count = PAGE_ALIGN(size) >> PAGE_SHIFT;
        unsigned long pfn = PHYS_PFN(dma_to_phys(dev, dma_addr));
        int ret = -ENXIO;
        vma->vm_page_prot = dma_pgprot(dev, vma->vm_page_prot, attrs);
        if (force_dma_unencrypted(dev))
                vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
        if (dma_mmap_from_dev_coherent(dev, vma, cpu_addr, size, &ret))
                return ret;
        if (dma_mmap_from_global_coherent(vma, cpu_addr, size, &ret))
                return ret;
        if (vma->vm_pgoff >= count || user_count > count - vma->vm_pgoff)
                return -ENXIO;
        return remap_pfn_range(vma, vma->vm_start, pfn + vma->vm_pgoff,
                        user_count << PAGE_SHIFT, vma->vm_page_prot);
}

ここで重要なのが、vma->vm_page_prot です。これは MMU に渡す各種属性を設定します。そして属性にはキャッシュを有効にするか否かも含まれます。

そして、vma->vm_page_prot の値を決定しているのが dma_pgprot() です。dma_pgprot() は次のように定義されています。

https://elixir.bootlin.com/linux/v6.1.97/source/kernel/dma/mapping.c#L415
pgprot_t dma_pgprot(struct device *dev, pgprot_t prot, unsigned long attrs)
{
        if (dev_is_dma_coherent(dev))
                return prot;
#ifdef CONFIG_ARCH_HAS_DMA_WRITE_COMBINE
        if (attrs & DMA_ATTR_WRITE_COMBINE)
                return pgprot_writecombine(prot);
#endif
        return pgprot_dmacoherent(prot);
}

ここでも dev_is_dma_coherent() が登場しています。 dev_is_dma_coherent() が true の場合は何もしていませんが、 false の場合は pgprot_dmacoherent() の値を返しています。ここで pgprot_dmacoherent() というのは、アーキテクチャ毎に定義されたマクロですが、どのアーキテクチャの場合もキャッシュを無効にする値を返しています。

すなわち dev_dma_coherent() は次のように DMA Buffer に仮想アドレスを割り当てる際に、 キャッシュを有効にするか無効にするかを決定する役割があると思われます。

  • dev_is_dma_coherent() が true の場合
    • 事実: mmap時には何もしない(キャッシュの設定は変更しない)
    • 推測: このデバイスは Cache Aliasing 問題は起きないことを示す
  • dev_is_dma_coherent() が false の場合
    • 事実: mmap 時にキャッシュを無効にしている
    • 推測: このデバイスは Cache Aliasing 問題が起こりうることを示す

まとめ

この記事では、「Linux で DMA Bufferを mmap した時に CPU Cacheが無効になる場合がある」原因である Cache Coherence 問題と Cache Aliasing 問題を Linux Kernel がどう扱っているかを説明しました。そして、その際の判定に使われる dev_is_dma_cohrent() について紹介しました。

dev_is_dma_coherent() Linux Kernel での対応
Cache Coherence 問題 Cache Aliasing 問題
true ハードウェアで解決 キャッシュを有効にする
false ソフトウェアで解決 キャッシュを無効にする

このように使われている dev_is_dma_coherent() ですが、実は dev_is_dma_coherent() には大きな問題点があります。次の記事 『Linux では Cache Coherence Hardware を持っていないとDMA Buffer をmmap する際に CPU Cache が無効になる』では、その問題点について説明します。

参考

ページカラーリングに関する Linux Kernel Newsgroup での議論

Linux Kernel の DMA Mapping API に関する資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0