GNU/Linux >> Tutoriels Linux >  >> Linux

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

Je m'embrouille avec la direction à mettre en œuvre. Je veux...

Tenez compte de l'application lors de la conception d'un pilote.
Quelle est la nature, la fréquence, la taille des mouvements de données et qu'est-ce qui peut se passer d'autre dans le système ?

L'API de lecture/écriture traditionnelle est-elle suffisante ? Le mappage direct de l'appareil dans l'espace utilisateur est-il correct ? Une mémoire partagée réfléchissante (semi-cohérente) est-elle souhaitable ?

La manipulation manuelle des données (lecture/écriture) est une assez bonne option si les données se prêtent à être bien comprises. L'utilisation d'une machine virtuelle à usage général et la lecture/écriture peuvent suffire avec une copie en ligne. Le mappage direct des accès non cachables au périphérique est pratique, mais peut être maladroit. Si l'accès est le mouvement relativement peu fréquent de gros blocs, il peut être judicieux d'utiliser une mémoire ordinaire, d'avoir la broche d'entraînement, de traduire les adresses, de DMA et de libérer les pages. En guise d'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 de 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 œuvres ci-dessus...

Trop souvent, les gens ne regardent pas le problème plus large, avant de creuser dans les détails. Souvent, les solutions les plus simples suffisent. Un petit effort pour construire un modèle comportemental peut aider à déterminer quelle API est préférable.


Je travaille exactement sur la même chose en ce moment et je passe le ioctl() itinéraire. 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 dispersion et de collecte avec l'API DMA en continu pour transférer des données directement vers et depuis l'appareil et la mémoire tampon de l'espace utilisateur.

La stratégie de mise en œuvre que j'utilise est que le ioctl() dans le pilote entre dans une boucle qui DMA est le tampon de l'espace utilisateur en morceaux de 256k (qui est la limite imposée par le matériel pour le nombre d'entrées scatter/gather qu'il peut gérer). Ceci est isolé à l'intérieur d'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, le ioctl() quitte et retourne à l'espace utilisateur

Pseudo-code pour le 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émentiel :

/*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'interruption 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 traitez pas ce pseudo-code comme un évangile et assurez-vous de doubler vérifier toute la logique et les paramètres;-).


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

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

Fondamentalement, cela se résume à une implémentation personnalisée de mmap() appeler (bien que file_operations ). Un pour la barre d'E/S 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 des tampons DMA en utilisant pci_alloc_consistent() c'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 ceux-ci en place, l'application de l'espace utilisateur peut à peu près tout faire - contrôler l'appareil en lisant/écrivant depuis/vers les registres d'E/S, allouer et libérer des tampons DMA de taille arbitraire et demander à l'appareil d'effectuer des transactions DMA. La seule partie manquante est la gestion des interruptions. J'effectuais des interrogations dans l'espace utilisateur, brûlais mon processeur et les interruptions étaient désactivées.

J'espère que cela aide. Bonne chance !


Vous avez fondamentalement la bonne idée :en 2.1, vous pouvez simplement faire en sorte que l'espace utilisateur alloue n'importe quelle ancienne mémoire. Vous voulez qu'il soit aligné sur la page, 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 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 de l'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 pages et parcourt la liste des pages en faisant sg_set_page() . Une fois la liste sg configurée, vous faites dma_map_sg() sur le tableau de scatterlist et ensuite vous pouvez obtenir le sg_dma_address et sg_dma_len pour chaque entrée dans la liste de dispersion (notez que vous devez utiliser la valeur de retour de dma_map_sg() car vous risquez de vous retrouver avec moins d'entrées mappées car les éléments peuvent ê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 l'attendre comme vous le souhaitez. Le schéma basé sur read() que vous avez est probablement correct.

Vous pouvez vous référer à drivers/infiniband/core/umem.c, en particulier ib_umem_get() , pour certains codes qui construisent ce mappage, bien que la généralité que ce code doit traiter puisse le rendre un peu déroutant.

Alternativement, si votre appareil ne gère pas trop bien les listes de dispersion/regroupement et que vous voulez une mémoire contiguë, vous pouvez utiliser get_free_pages() pour allouer un tampon physiquement contigu et utiliser dma_map_page() sur ça. Pour donner à l'espace utilisateur l'accès à cette mémoire, votre pilote n'a qu'à implémenter un mmap méthode au lieu de l'ioctl comme décrit ci-dessus.


Linux
  1. Pourquoi j'ai fait le passage de Mac à Linux

  2. Comment installer un pilote de périphérique sous Linux

  3. Linux - Comment le noyau Linux connaît-il les numéros majeurs et mineurs des périphériques ?

  4. Exécuter une fonction de l'espace utilisateur à partir de l'espace noyau

  5. Comment accéder (si possible) à l'espace noyau depuis l'espace utilisateur ?

Comment construire le noyau Linux à partir de zéro {Guide étape par étape}

Comment construire le noyau Linux à partir de zéro

Comment se connecter en SSH à votre serveur Linux à partir de Windows

Comment compiler le noyau Linux à partir de la source pour créer un noyau personnalisé

Comment se connecter en SSH à une machine Windows 10 depuis Linux OU Windows OU n'importe où

Segmentation de la mémoire Linux