Mi sto confondendo con la direzione da implementare. Voglio...
Considera l'applicazione durante la progettazione di un driver.
Qual è la natura del movimento dei dati, la frequenza, le dimensioni e cos'altro potrebbe accadere nel sistema?
La tradizionale API di lettura/scrittura è sufficiente? La mappatura diretta del dispositivo nello spazio utente va bene? È auspicabile una memoria condivisa riflessiva (semi-coerente)?
La manipolazione manuale dei dati (lettura/scrittura) è un'opzione abbastanza buona se i dati si prestano a essere ben compresi. L'utilizzo di VM per uso generico e lettura/scrittura può essere sufficiente con una copia in linea. La mappatura diretta degli accessi non memorizzabili nella cache alla periferica è conveniente, ma può essere scomoda. Se l'accesso è il movimento relativamente poco frequente di blocchi di grandi dimensioni, potrebbe avere senso utilizzare la memoria normale, disporre del pin dell'unità, tradurre indirizzi, DMA e rilasciare le pagine. Come ottimizzazione, le pagine (forse enormi) possono essere pre-appuntate e tradotte; l'unità può quindi riconoscere la memoria preparata ed evitare le complessità della traduzione dinamica. Se ci sono molte piccole operazioni di I/O, ha senso far funzionare l'unità in modo asincrono. Se l'eleganza è importante, il flag della pagina sporca della VM può essere utilizzato per identificare automaticamente ciò che deve essere spostato e una chiamata (meta_sync()) può essere utilizzata per svuotare le pagine. Forse un misto dei lavori di cui sopra...
Troppo spesso le persone non guardano al problema più grande, prima di scavare nei dettagli. Spesso sono sufficienti le soluzioni più semplici. Un piccolo sforzo nella costruzione di un modello comportamentale può aiutare a determinare quale API è preferibile.
In realtà sto lavorando esattamente alla stessa cosa in questo momento e sto andando al ioctl()
percorso. L'idea generale è che lo spazio utente allochi il buffer che verrà utilizzato per il trasferimento DMA e un ioctl()
verrà utilizzato per passare la dimensione e l'indirizzo di questo buffer al driver del dispositivo. Il driver utilizzerà quindi gli elenchi di raccolta a dispersione insieme all'API DMA di streaming per trasferire i dati direttamente da e verso il dispositivo e il buffer dello spazio utente.
La strategia di implementazione che sto usando è che il ioctl()
nel driver entra in un ciclo che DMA è il buffer dello spazio utente in blocchi di 256k (che è il limite imposto dall'hardware per il numero di voci scatter/gather che può gestire). Questo è isolato all'interno di una funzione che si blocca fino al completamento di ogni trasferimento (vedi sotto). Quando tutti i byte vengono trasferiti o la funzione di trasferimento incrementale restituisce un errore ioctl()
esce e ritorna allo spazio utente
Pseudo codice per il 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 codice per la funzione di trasferimento incrementale:
/*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(¤t->mm->mmap_sem);
ret = get_user_pages(current,
current->mm,
udata,
npages,
is_writing_to_userspace,
0,
&pages_array,
NULL);
up_read(¤t->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;
}
Il gestore di interrupt è eccezionalmente breve:
/* 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);
Tieni presente che questo è solo un approccio generale, ho lavorato su questo driver nelle ultime settimane e devo ancora testarlo effettivamente ... Quindi, per favore, non trattare questo pseudo codice come vangelo e assicurati di raddoppiare controlla tutta la logica e i parametri;-).
Ad un certo punto volevo consentire all'applicazione dello spazio utente di allocare i buffer DMA e farla mappare nello spazio utente e ottenere l'indirizzo fisico per poter controllare il mio dispositivo ed eseguire transazioni DMA (bus mastering) interamente dallo spazio utente, totalmente bypassando il kernel di Linux. Ho usato un approccio leggermente diverso però. Per prima cosa ho iniziato con un modulo kernel minimo che stava inizializzando/sondando il dispositivo PCIe e creando un dispositivo a caratteri. Quel driver ha quindi consentito a un'applicazione in spazio utente di fare due cose:
- Mappa la barra I/O del dispositivo PCIe nello spazio utente utilizzando
remap_pfn_range()
funzione. - Assegna e libera i buffer DMA, mappali allo spazio utente e passa un indirizzo di bus fisico all'applicazione dello spazio utente.
Fondamentalmente, si riduce a un'implementazione personalizzata di mmap()
call (sebbene file_operations
). Uno per la barra I/O è 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;
}
E un altro che alloca i buffer DMA usando pci_alloc_consistent()
è un po' più complicato:
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! */
}
Una volta che questi sono a posto, l'applicazione dello spazio utente può praticamente fare tutto:controllare il dispositivo leggendo/scrivendo da/a registri I/O, allocare e liberare buffer DMA di dimensioni arbitrarie e fare in modo che il dispositivo esegua transazioni DMA. L'unica parte mancante è la gestione degli interrupt. Stavo eseguendo il polling nello spazio utente, bruciando la mia CPU e avevo gli interrupt disabilitati.
Spero che sia d'aiuto. Buona fortuna!
Fondamentalmente hai l'idea giusta:in 2.1, puoi semplicemente fare in modo che lo spazio utente allochi qualsiasi vecchia memoria. Lo vuoi allineato alla pagina, quindi posix_memalign()
è una comoda API da usare.
Quindi fai in modo che lo spazio utente passi nell'indirizzo virtuale dello spazio utente e nella dimensione di questo buffer in qualche modo; ioctl() è un buon modo rapido e sporco per farlo. Nel kernel, alloca un array di buffer di dimensioni appropriate di struct page*
-- user_buf_size/PAGE_SIZE
voci -- e usa get_user_pages()
per ottenere un elenco di struct page* per il buffer dello spazio utente.
Una volta ottenuto questo, puoi allocare un array di struct scatterlist
che ha le stesse dimensioni dell'array di pagine e scorre l'elenco delle pagine facendo sg_set_page()
. Dopo che l'elenco sg è stato impostato, esegui dma_map_sg()
sull'array di scatterlist e quindi puoi ottenere sg_dma_address
e sg_dma_len
per ogni voce nell'elenco a dispersione (nota che devi utilizzare il valore restituito di dma_map_sg()
perché potresti finire con meno voci mappate perché le cose potrebbero essere unite dal codice di mappatura DMA).
Questo ti dà tutti gli indirizzi bus da passare al tuo dispositivo, quindi puoi attivare il DMA e aspettarlo come preferisci. Lo schema basato su read() che hai probabilmente va bene.
Puoi fare riferimento a drivers/infiniband/core/umem.c, in particolare ib_umem_get()
, per parte del codice che crea questa mappatura, anche se la generalità con cui quel codice deve occuparsi potrebbe creare un po' di confusione.
In alternativa, se il tuo dispositivo non gestisce troppo bene gli elenchi scatter/gather e desideri una memoria contigua, puoi utilizzare get_free_pages()
per allocare un buffer fisicamente contiguo e usare dma_map_page()
su quello. Per dare accesso allo spazio utente a quella memoria, il tuo driver deve solo implementare un mmap
metodo invece di ioctl come descritto sopra.