GNU/Linux >> Znalost Linux >  >> Linux

Ovladač zařízení linuxového jádra na DMA ze zařízení do paměti uživatelského prostoru

Jsem zmatený směrem k implementaci. Chci...

Při navrhování ovladače zvažte aplikaci.
Jaká je povaha pohybu dat, frekvence, velikost a co dalšího se může v systému dít?

Je tradiční API pro čtení/zápis dostačující? Je přímé mapování zařízení do uživatelského prostoru v pořádku? Je žádoucí reflektivní (semi-koherentní) sdílená paměť?

Ruční manipulace s daty (čtení/zápis) je docela dobrá volba, pokud se data hodí k dobrému pochopení. Použití VM pro všeobecné použití a čtení/zápis může být dostačující s vloženou kopií. Přímé mapování necachovatelných přístupů k periferii je pohodlné, ale může být neohrabané. Pokud je přístup relativně řídkým pohybem velkých bloků, může mít smysl použít běžnou paměť, mít pin disku, překládat adresy, DMA a uvolnit stránky. Jako optimalizaci lze stránky (možná obrovské) předem připnout a přeložit; jednotka pak dokáže rozpoznat připravenou paměť a vyhnout se složitosti dynamického překladu. Pokud existuje mnoho malých I/O operací, má smysl mít disk běžet asynchronně. Pokud je důležitá elegance, lze příznak nečisté stránky virtuálního počítače použít k automatické identifikaci toho, co je třeba přesunout, a volání (meta_sync()) lze použít k vyprázdnění stránek. Možná kombinace výše uvedených funguje...

Lidé se příliš často nedívají na větší problém, než se začnou vrtat do detailů. Často stačí ta nejjednodušší řešení. Trocha úsilí při konstrukci behaviorálního modelu může pomoci určit, jaké API je vhodnější.


Ve skutečnosti právě pracuji na úplně stejné věci a jdu do ioctl() trasa. Obecnou myšlenkou je, aby uživatelský prostor alokoval vyrovnávací paměť, která bude použita pro přenos DMA, a ioctl() se použije k předání velikosti a adresy této vyrovnávací paměti ovladači zařízení. Ovladač pak použije seznamy scatter-gather spolu s rozhraním DMA API pro přenos dat přímo do a ze zařízení a vyrovnávací paměti uživatelského prostoru.

Implementační strategie, kterou používám, je ioctl() v ovladači zadá smyčku, která je DMA vyrovnávací pamětí uživatelského prostoru v kouscích 256k (což je hardwarově stanovený limit pro počet rozptylových/shromážděných záznamů, které dokáže zpracovat). Toto je izolováno uvnitř funkce, která blokuje, dokud není každý přenos dokončen (viz níže). Když jsou přeneseny všechny bajty nebo funkce inkrementálního přenosu vrátí chybu ioctl() ukončí a vrátí se do uživatelského prostoru

Pseudokód pro ioctl()

/*serialize all DMA transfers to/from the device*/
if (mutex_lock_interruptible( &device_ptr->mtx ) )
    return -EINTR;

chunk_data = (unsigned long) user_space_addr;
while( *transferred < total_bytes && !ret ) {
    chunk_bytes = total_bytes - *transferred;
    if (chunk_bytes > HW_DMA_MAX)
        chunk_bytes = HW_DMA_MAX; /* 256kb limit imposed by my device */
    ret = transfer_chunk(device_ptr, chunk_data, chunk_bytes, transferred);
    chunk_data += chunk_bytes;
    chunk_offset += chunk_bytes;
}

mutex_unlock(&device_ptr->mtx);

Pseudokód pro funkci přírůstkového přenosu:

/*Assuming the userspace pointer is passed as an unsigned long, */
/*calculate the first,last, and number of pages being transferred via*/

first_page = (udata & PAGE_MASK) >> PAGE_SHIFT;
last_page = ((udata+nbytes-1) & PAGE_MASK) >> PAGE_SHIFT;
first_page_offset = udata & PAGE_MASK;
npages = last_page - first_page + 1;

/* Ensure that all userspace pages are locked in memory for the */
/* duration of the DMA transfer */

down_read(&current->mm->mmap_sem);
ret = get_user_pages(current,
                     current->mm,
                     udata,
                     npages,
                     is_writing_to_userspace,
                     0,
                     &pages_array,
                     NULL);
up_read(&current->mm->mmap_sem);

/* Map a scatter-gather list to point at the userspace pages */

/*first*/
sg_set_page(&sglist[0], pages_array[0], PAGE_SIZE - fp_offset, fp_offset);

/*middle*/
for(i=1; i < npages-1; i++)
    sg_set_page(&sglist[i], pages_array[i], PAGE_SIZE, 0);

/*last*/
if (npages > 1) {
    sg_set_page(&sglist[npages-1], pages_array[npages-1],
        nbytes - (PAGE_SIZE - fp_offset) - ((npages-2)*PAGE_SIZE), 0);
}

/* Do the hardware specific thing to give it the scatter-gather list
   and tell it to start the DMA transfer */

/* Wait for the DMA transfer to complete */
ret = wait_event_interruptible_timeout( &device_ptr->dma_wait, 
         &device_ptr->flag_dma_done, HZ*2 );

if (ret == 0)
    /* DMA operation timed out */
else if (ret == -ERESTARTSYS )
    /* DMA operation interrupted by signal */
else {
    /* DMA success */
    *transferred += nbytes;
    return 0;
}

Obsluha přerušení je výjimečně krátká:

/* Do hardware specific thing to make the device happy */

/* Wake the thread waiting for this DMA operation to complete */
device_ptr->flag_dma_done = 1;
wake_up_interruptible(device_ptr->dma_wait);

Vezměte prosím na vědomí, že se jedná pouze o obecný přístup, na tomto ovladači pracuji posledních několik týdnů a ještě ho musím skutečně otestovat... Takže prosím, nepovažujte tento pseudo kód za evangelium a nezapomeňte jej zdvojnásobit zkontrolujte veškerou logiku a parametry;-).


V určitém okamžiku jsem chtěl umožnit aplikaci v uživatelském prostoru alokovat vyrovnávací paměti DMA a namapovat je do uživatelského prostoru a získat fyzickou adresu, abych mohl ovládat své zařízení a provádět transakce DMA (mastering sběrnice) zcela z uživatelského prostoru, zcela obcházení linuxového jádra. Použil jsem ale trochu jiný přístup. Nejprve jsem začal s minimálním modulem jádra, který inicializoval/zjišťoval PCIe zařízení a vytvářel znakové zařízení. Tento ovladač pak umožnil aplikaci v uživatelském prostoru dělat dvě věci:

  1. Namapujte I/O panel zařízení PCIe do uživatelského prostoru pomocí remap_pfn_range() funkce.
  2. Přidělte a uvolněte vyrovnávací paměti DMA, namapujte je do uživatelského prostoru a předejte fyzickou adresu sběrnice aplikaci v uživatelském prostoru.

V podstatě se scvrkává na vlastní implementaci mmap() volání (ačkoli file_operations ). Jeden pro I/O bar je snadný:

struct vm_operations_struct a2gx_bar_vma_ops = {
};

static int a2gx_cdev_mmap_bar2(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    size_t size;

    size = vma->vm_end - vma->vm_start;
    if (size != 134217728)
        return -EIO;

    dev = filp->private_data;
    vma->vm_ops = &a2gx_bar_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = dev;

    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(dev->bar2),
                        size, vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

A další, který přiděluje vyrovnávací paměti DMA pomocí pci_alloc_consistent() je trochu složitější:

static void a2gx_dma_vma_close(struct vm_area_struct *vma)
{
    struct a2gx_dma_buf *buf;
    struct a2gx_dev *dev;

    buf = vma->vm_private_data;
    dev = buf->priv_data;

    pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr, buf->dma_addr);
    buf->cpu_addr = NULL; /* Mark this buffer data structure as unused/free */
}

struct vm_operations_struct a2gx_dma_vma_ops = {
    .close = a2gx_dma_vma_close
};

static int a2gx_cdev_mmap_dma(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    struct a2gx_dma_buf *buf;
    size_t size;
    unsigned int i;

    /* Obtain a pointer to our device structure and calculate the size
       of the requested DMA buffer */
    dev = filp->private_data;
    size = vma->vm_end - vma->vm_start;

    if (size < sizeof(unsigned long))
        return -EINVAL; /* Something fishy is happening */

    /* Find a structure where we can store extra information about this
       buffer to be able to release it later. */
    for (i = 0; i < A2GX_DMA_BUF_MAX; ++i) {
        buf = &dev->dma_buf[i];
        if (buf->cpu_addr == NULL)
            break;
    }

    if (buf->cpu_addr != NULL)
        return -ENOBUFS; /* Oops, hit the limit of allowed number of
                            allocated buffers. Change A2GX_DMA_BUF_MAX and
                            recompile? */

    /* Allocate consistent memory that can be used for DMA transactions */
    buf->cpu_addr = pci_alloc_consistent(dev->pci_dev, size, &buf->dma_addr);
    if (buf->cpu_addr == NULL)
        return -ENOMEM; /* Out of juice */

    /* There is no way to pass extra information to the user. And I am too lazy
       to implement this mmap() call using ioctl(). So we simply tell the user
       the bus address of this buffer by copying it to the allocated buffer
       itself. Hacks, hacks everywhere. */
    memcpy(buf->cpu_addr, &buf->dma_addr, sizeof(buf->dma_addr));

    buf->size = size;
    buf->priv_data = dev;
    vma->vm_ops = &a2gx_dma_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = buf;

    /*
     * Map this DMA buffer into user space.
     */
    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(buf->cpu_addr),
                        size, vma->vm_page_prot))
    {
        /* Out of luck, rollback... */
        pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr,
                            buf->dma_addr);
        buf->cpu_addr = NULL;
        return -EAGAIN;
    }

    return 0; /* All good! */
}

Jakmile jsou na místě, aplikace v uživatelském prostoru může dělat v podstatě vše – ovládat zařízení čtením/zápisem z/do I/O registrů, přidělovat a uvolňovat vyrovnávací paměti DMA libovolné velikosti a nechat zařízení provádět transakce DMA. Jedinou chybějící částí je obsluha přerušení. Prováděl jsem dotazování v uživatelském prostoru, vypaloval jsem CPU a měl jsem vypnutá přerušení.

Doufám, že to pomůže. Hodně štěstí!


V zásadě máte správnou představu:ve verzi 2.1 můžete pouze nechat uživatelský prostor alokovat jakoukoli starou paměť. Chcete to zarovnat podle stránky, takže posix_memalign() je užitečné rozhraní API.

Pak nechejte uživatelský prostor nějak předat virtuální adresu uživatelského prostoru a velikost tohoto bufferu; ioctl() je dobrý rychlý a špinavý způsob, jak toho dosáhnout. V jádře alokujte pole bufferů odpovídající velikosti struct page* -- user_buf_size/PAGE_SIZE záznamy -- a použijte get_user_pages() získat seznam struct page* pro vyrovnávací paměť uživatelského prostoru.

Jakmile to budete mít, můžete alokovat pole struct scatterlist která má stejnou velikost jako vaše pole stránek a prochází seznamem stránek s sg_set_page() . Po nastavení seznamu sg uděláte dma_map_sg() na poli scatterlist a pak můžete získat sg_dma_address a sg_dma_len pro každý záznam v bodovém seznamu (všimněte si, že musíte použít návratovou hodnotu dma_map_sg() protože můžete skončit s menším počtem namapovaných položek, protože se věci mohou sloučit pomocí mapovacího kódu DMA).

To vám dává všechny adresy sběrnice, které se mají předat vašemu zařízení, a pak můžete spustit DMA a čekat na to, jak chcete. Schéma založené na read(), které máte, je pravděpodobně v pořádku.

Můžete se podívat na drivers/infiniband/core/umem.c, konkrétně ib_umem_get() , pro některé kódy, které vytvářejí toto mapování, i když obecnost, se kterou se tento kód musí vypořádat, může být trochu matoucí.

Alternativně, pokud vaše zařízení nezvládá rozptylové/shromážděné seznamy příliš dobře a chcete souvislou paměť, můžete použít get_free_pages() alokovat fyzicky souvislou vyrovnávací paměť a použít dma_map_page() na tom. Chcete-li poskytnout uživatelskému prostoru přístup k této paměti, váš ovladač potřebuje implementovat mmap namísto ioctl, jak je popsáno výše.


Linux
  1. Proč jsem přešel z Macu na Linux

  2. Linuxové jádro:5 nejlepších inovací

  3. Linux – Jak linuxové jádro zná hlavní a vedlejší čísla zařízení?

  1. Spuštění funkce uživatelského prostoru z prostoru jádra

  2. Jak načíst moduly jádra Linuxu z kódu C?

  3. Jak mohu rezervovat blok paměti z jádra Linuxu?

  1. Jak nainstalovat ovladač zařízení v systému Linux

  2. Jak přistupovat (pokud je to možné) k prostoru jádra z uživatelského prostoru?

  3. K čemu je mít část jádra ve virtuálním paměťovém prostoru linuxových procesů?