web-dev-qa-db-fra.com

Pilote de périphérique du noyau Linux à DMA d'un appareil dans la mémoire de l'espace utilisateur

Je souhaite transférer aussi rapidement que possible les données d'un périphérique matériel PCIe activé pour DMA.

Q: Comment combiner "entrées/sorties directes vers l'espace utilisateur avec/et/via un transfert DMA"

  1. En lisant LDD3, il semble que je doive effectuer différents types d’opérations IO !? 

    dma_alloc_coherent me donne l'adresse physique que je peux transmettre au périphérique matériel . Mais il faudrait que setup get_user_pages et effectuer un appel de type copy_to_user à la fin du transfert. Cela semble un gaspillage, demandant au périphérique de DMA dans la mémoire du noyau (jouant le rôle de tampon), puis de le transférer à nouveau dans l'espace utilisateur . LDD3 p453: /* Only now is it safe to access the buffer, copy to user, etc. */

  2. Ce que je veux idéalement, c’est un souvenir qui

    • Je peux utiliser dans l'espace utilisateur (peut-être demander à un pilote via un appel ioctl de créer une mémoire/un tampon DMA?) 
    • Je peux obtenir une adresse physique à transmettre au périphérique, de sorte que tout l'espace utilisateur doit être lu par le pilote 
    • la méthode de lecture activerait le transfert DMA, bloquerait l'attente de l'interruption complète de DMA et libérerait ensuite l'espace utilisateur lu (l'espace utilisateur est maintenant sûr d'utiliser/de lire la mémoire).

Ai-je besoin de mappages en continu sur une seule page, de mappage de configuration et de tampons d'espace utilisateur mappés avec get_user_pagesdma_map_page?

Mon code jusqu'à présent met en place get_user_pages à l'adresse donnée à partir de l'espace utilisateur (j'appelle cela la partie Direct I/O). Ensuite, dma_map_page avec une page de get_user_pages. Je donne à l'appareil la valeur de retour de dma_map_page comme adresse de transfert physique DMA.

J'utilise certains modules du noyau comme référence: drivers_scsi_st.c et drivers-net-sh_eth.c. Je regarderais le code infiniband, mais je ne peux pas trouver lequel est le plus fondamental!

Merci d'avance.

27
Ian Vaughan

En fait, je travaille sur exactement la même chose en ce moment et je vais sur la route ioctl(). L'idée générale est que l'espace utilisateur alloue le tampon qui sera utilisé pour le transfert DMA et un ioctl() sera utilisé pour transmettre la taille et l'adresse de ce tampon au pilote de périphérique. Le pilote utilisera ensuite des listes de diffusion et de l'API DMA de transmission en continu pour transférer des données directement vers et depuis le périphérique et le tampon d'espace utilisateur.

La stratégie d'implémentation que j'utilise est que la ioctl() du pilote entre dans une boucle qui est le tampon d'espace utilisateur de DMA en morceaux de 256 k (ce qui est la limite imposée par le matériel pour le nombre d'entrées scatter/rassembl qu'il peut gérer). Ceci est isolé dans une fonction qui bloque jusqu'à ce que chaque transfert soit terminé (voir ci-dessous). Lorsque tous les octets sont transférés ou que la fonction de transfert incrémentiel renvoie une erreur, la ioctl() se ferme et retourne à l'espace utilisateur.

Pseudo-code pour la 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);

Pseudo-code pour la fonction de transfert incrémental:

/*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;
}

Le gestionnaire d'interruptions est exceptionnellement bref:

/* 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);

Veuillez noter qu'il ne s'agit que d'une approche générale. Je travaille sur ce pilote depuis quelques semaines et je ne l'ai pas encore testé. Alors, s'il vous plaît, ne considérez pas ce pseudo code comme un évangile et veillez à le doubler. vérifiez toute la logique et les paramètres ;-).

14
Rakis

Vous avez fondamentalement la bonne idée: en 2.1, vous pouvez simplement laisser l’espace utilisateur allouer n’importe quelle mémoire ancienne. Vous voulez qu'elle soit alignée sur les pages, donc posix_memalign() est une API pratique à utiliser.

Ensuite, faites passer l'espace utilisateur dans l'adresse virtuelle de l'espace utilisateur et la taille de ce tampon d'une manière ou d'une autre; ioctl () est un bon moyen rapide et sale de le faire. Dans le noyau, allouez un tableau de tampons de taille appropriée, constitué de struct page* - user_buf_size/PAGE_SIZE entrées - et utilisez get_user_pages() pour obtenir une liste de struct page * pour le tampon d'espace utilisateur.

Une fois que vous avez cela, vous pouvez allouer un tableau de struct scatterlist qui a la même taille que votre tableau de page et parcourir la liste des pages faisant sg_set_page(). Une fois la liste sg configurée, vous créez dma_map_sg() dans le tableau de scatterlist, puis vous pouvez obtenir le sg_dma_address et le sg_dma_len pour chaque entrée de la liste de scatter (notez que vous devez utiliser la valeur de retour de dma_map_sg() car vous risquez de vous retrouver avec moins de cartes entrées car les éléments pourraient être fusionnés par le code de mappage DMA).

Cela vous donne toutes les adresses de bus à transmettre à votre appareil, puis vous pouvez déclencher le DMA et attendre comme vous le souhaitez. Le schéma que vous avez basé sur read () est probablement bon.

Vous pouvez vous référer à drivers/infiniband/core/umem.c, plus précisément à ib_umem_get(), pour du code qui construit ce mappage, bien que la généralité que ce code doit traiter puisse le rendre un peu déroutant.

Sinon, si votre appareil ne gère pas trop les listes de dispersion/collecte et que vous souhaitez une mémoire contiguë, vous pouvez utiliser get_free_pages() pour allouer un tampon physiquement contigu et utiliser dma_map_page() pour cela. Pour permettre à cet espace utilisateur d'accéder à cette mémoire, votre pilote doit simplement implémenter une méthode mmap à la place de ioctl, comme décrit ci-dessus.

12
Roland

À un moment donné, j'ai voulu autoriser une application d'espace utilisateur à allouer DMA des mémoires tampons et à la mapper à l'espace utilisateur, ainsi qu'à obtenir l'adresse physique permettant de contrôler mon périphérique et d'effectuer DMA transactions (maîtrise du bus) entièrement à partir de l'espace utilisateur, en contournant totalement le noyau Linux. J'ai utilisé une approche un peu différente cependant. J'ai d'abord commencé avec un module de noyau minimal qui initialisait/vérifiait le périphérique PCIe et créait un périphérique de caractères. Ce pilote a ensuite permis à une application utilisateur de faire deux choses:

  1. Mappez la barre d'E/S du périphérique PCIe dans l'espace utilisateur à l'aide de la fonction remap_pfn_range().
  2. Allouer et libérer DMA - tampons, les mapper à l'espace utilisateur et transmettre une adresse de bus physique à une application d'espace utilisateur.

En gros, il s’agit d’une implémentation personnalisée de l’appel mmap() (bien que file_operations). Un pour I/O bar est facile:

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;
}

Et un autre qui alloue DMA les tampons utilisant pci_alloc_consistent() est un peu plus compliqué:

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! */
}

Une fois que ceux-ci sont en place, les applications d’espace utilisateur peuvent pratiquement tout faire - contrôler le périphérique en lisant/écrivant de/vers des registres d’E/S, allouer et libérer DMA - tampons de taille arbitraire, et laisser le périphérique s’exécuter DMA transactions. La seule partie manquante est la gestion des interruptions. J'étais en train d'interroger dans l'espace utilisateur, de graver mon processeur et de désactiver les interruptions.

J'espère que ça aide. Bonne chance!

6
user405725

Je suis confus avec la direction à mettre en œuvre. Je veux...

Considérez l'application lors de la conception d'un pilote.
Quelle est la nature des mouvements de données, leur fréquence, leur taille et quoi d’autre pourrait se passer dans le système?

L'API traditionnelle en lecture/écriture est-elle suffisante? Le mappage direct du périphérique dans l'espace utilisateur est-il OK? Une mémoire partagée réfléchissante (semi-cohérente) est-elle souhaitable?

Manipuler manuellement des données (lecture/écriture) est une très bonne option si les données se prêtent à une bonne compréhension. L'utilisation d'une utilisation générale VM et de lecture/écriture peut suffire avec une copie en ligne. La cartographie directe des accès non cachables au périphérique est pratique, mais peut être lourde. Si l'accès correspond au mouvement relativement peu fréquent de grands blocs, il peut être judicieux d'utiliser de la mémoire ordinaire, d'avoir la broche de lecteur, de traduire les adresses, DMA et de libérer les pages. En optimisation, les pages (peut-être énormes) peuvent être pré-épinglées et traduites; le lecteur peut alors reconnaître la mémoire préparée et éviter les complexités de la traduction dynamique. S'il y a beaucoup de petites opérations d'E/S, il est logique de faire fonctionner le lecteur de manière asynchrone. Si l'élégance est importante, l'indicateur de page sale VM peut être utilisé pour identifier automatiquement ce qui doit être déplacé et un appel (meta_sync ()) peut être utilisé pour vider les pages. Peut-être un mélange des travaux ci-dessus ...

Trop souvent, les gens ne se penchent pas sur le problème dans son ensemble avant d’entrer dans les détails. Les solutions les plus simples suffisent souvent. Un petit effort pour construire un modèle comportemental peut aider à déterminer quelle API est préférable.

1
fbp
first_page_offset = udata & PAGE_MASK; 

Cela semble faux. Ce devrait être soit:

first_page_offset = udata & ~PAGE_MASK;

ou 

first_page_offset = udata & (PAGE_SIZE - 1)
0
Suman

Il est à noter que le pilote avec support Scatter-Gather DMA et l’allocation de mémoire utilisateur sont les plus efficaces et les plus performants. Cependant, si nous n’avons pas besoin de hautes performances ou si nous voulons développer un pilote dans des conditions simplifiées, nous pouvons utiliser certaines astuces.

Abandonnez le design zéro copie. Il convient de prendre en compte le moment où le débit de données n’est pas trop important. Dans une telle conception, les données peuvent être copiées à l’utilisateur par copy_to_user(user_buffer, kernel_dma_buffer, count); User_buffer peut être, par exemple, un argument de tampon dans l’implémentation d’un appel système read () de caractères. Nous devons encore nous occuper de l'allocation kernel_dma_buffer. Il pourrait par mémoire obtenu de dma_alloc_coherent() appeler par exemple.

Une autre astuce consiste à limiter la mémoire système au moment du démarrage, puis à l’utiliser comme énorme tampon DMA contigu. Ceci est particulièrement utile lors du développement de pilotes et de contrôleurs FPGA DMA et plutôt déconseillé dans les environnements de production. Disons que le PC a 32 Go de RAM. Si nous ajoutons mem=20GB à la liste des paramètres de démarrage du noyau, nous pouvons utiliser 12 Go comme énorme tampon dma contigu. Pour mapper cette mémoire sur l’espace utilisateur, implémentez simplement mmap () en tant que

remap_pfn_range(vma,
    vma->vm_start,
    (0x500000000 >> PAGE_SHIFT) + vma->vm_pgoff, 
    vma->vm_end - vma->vm_start,
    vma->vm_page_prot)

Bien entendu, ce système d'exploitation de 12 Go est totalement omis et ne peut être utilisé que par les processus qui l'ont mappé dans son espace d'adressage. Nous pouvons essayer de l'éviter en utilisant le CMA (Contiguous Memory Allocator).

Encore une fois, les astuces ci-dessus ne remplaceront pas le pilote complet Scatter-Gather, zero copy DMA, mais sont utiles pendant le temps de développement ou sur certaines plates-formes moins performantes.

0
SlawekS