Envio TCP de espaço do usuário com cópia zero da memória mapeada dma_mmap_coherent ()

14

Estou executando o Linux 5.1 em um Cyclone V SoC, que é um FPGA com dois núcleos ARMv7 em um chip. Meu objetivo é coletar muitos dados de uma interface externa e transmitir (parte) esses dados através de um soquete TCP. O desafio aqui é que a taxa de dados é muito alta e pode quase saturar a interface GbE. Eu tenho uma implementação funcional que apenas usa write()chamadas para o soquete, mas atinge o limite de 55 MB / s; aproximadamente metade do limite teórico de GbE. Agora estou tentando fazer com que a transmissão TCP de cópia zero funcione para aumentar a taxa de transferência, mas estou atingindo uma parede.

Para obter os dados do FPGA no espaço de usuário do Linux, escrevi um driver de kernel. Esse driver usa um bloco DMA no FPGA para copiar uma grande quantidade de dados de uma interface externa na memória DDR3 conectada aos núcleos do ARMv7. O driver aloca essa memória como um monte de buffers contíguos de 1 MB quando testados dma_alloc_coherent()com GFP_USER, e os expõe ao aplicativo userspace implementando mmap()em um arquivo /dev/e retornando um endereço para o aplicativo usando dma_mmap_coherent()os buffers pré-alocados.

Por enquanto, tudo bem; o aplicativo de espaço do usuário está vendo dados válidos e a taxa de transferência é mais que suficiente em> 360 MB / s, com espaço de sobra (a interface externa não é rápida o suficiente para realmente ver qual é o limite superior).

Para implementar redes TCP de cópia zero, minha primeira abordagem foi usar SO_ZEROCOPYno soquete:

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

No entanto, isso resulta em send: Bad address.

Depois de pesquisar um pouco, minha segunda abordagem foi usar um pipe e splice()seguido por vmsplice():

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}

No entanto, o resultado é o mesmo: vmsplice: Bad address.

Observe que, se eu substituir a chamada para vmsplice()ou send()para uma função que apenas imprima os dados apontados por buf(ou send() sem MSG_ZEROCOPY ), tudo estará funcionando bem; portanto, os dados estão acessíveis ao espaço do usuário, mas as chamadas vmsplice()/ send(..., MSG_ZEROCOPY)parecem incapazes de lidar com isso.

O que estou perdendo aqui? Existe alguma maneira de usar o TCP de cópia zero enviando com um endereço de espaço de usuário obtido de um driver de kernel dma_mmap_coherent()? Existe outra abordagem que eu poderia usar?

ATUALIZAR

Então, eu mergulhei um pouco mais no sendmsg() MSG_ZEROCOPYcaminho do kernel, e a chamada que eventualmente falha é get_user_pages_fast(). Essa chamada retorna -EFAULTporque check_vma_flags()localiza o VM_PFNMAPsinalizador definido no vma. Aparentemente, esse sinalizador é definido quando as páginas são mapeadas no espaço do usuário usando remap_pfn_range()ou dma_mmap_coherent(). Minha próxima abordagem é encontrar outro caminho para mmapessas páginas.

rem
fonte

Respostas:

8

Como postei em uma atualização na minha pergunta, o problema subjacente é que a rede zerocopy não funciona para a memória que foi mapeada usando remap_pfn_range()(que também dma_mmap_coherent()é usada sob o capô). O motivo é que esse tipo de memória (com o VM_PFNMAPconjunto de sinalizadores) não possui metadados na forma de struct page*associado a cada página, necessária.

A solução então é alocar a memória de uma forma que struct page*s está associada com a memória.

O fluxo de trabalho que agora funciona para eu alocar a memória é:

  1. Use struct page* page = alloc_pages(GFP_USER, page_order);para alocar um bloco de memória física contígua, onde é fornecido o número de páginas contíguas que serão alocadas 2**page_order.
  2. Divida a página de ordem superior / composta em páginas de ordem 0 chamando split_page(page, page_order);. Isso agora significa que struct page* pagese tornou uma matriz com 2**page_orderentradas.

Agora, para enviar uma região ao DMA (para recepção de dados):

  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. dmaengine_submit(dma_desc);

Quando obtemos um retorno de chamada do DMA que a transferência foi concluída, precisamos remover o mapeamento da região para transferir a propriedade desse bloco de memória de volta para a CPU, que cuida dos caches para garantir que não estamos lendo dados obsoletos:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

Agora, quando queremos implementar mmap(), basta chamar vm_insert_page()repetidamente todas as páginas de ordem 0 que pré-alocamos:

static int my_mmap(struct file *file, struct vm_area_struct *vma) {
    int res;
...
    for (i = 0; i < 2**page_order; ++i) {
        if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
            break;
        }
    }
    vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
    return res;
}

Quando o arquivo estiver fechado, não esqueça de liberar as páginas:

for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

A implementação mmap()dessa maneira agora permite que um soquete use esse buffer sendmsg()com o MSG_ZEROCOPYsinalizador.

Embora isso funcione, há duas coisas que não se encaixam bem comigo nessa abordagem:

  • Você só pode alocar buffers de tamanho 2 com esse método, embora possa implementar a lógica para chamar alloc_pagesquantas vezes forem necessárias com ordens decrescentes para obter um buffer de tamanho composto por sub-buffers de tamanhos variados. Isso exigirá alguma lógica para unir esses buffers nas mmap()DMA e com elas em sgchamadas scatter-gather ( ) single.
  • split_page() diz em sua documentação:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Esses problemas seriam facilmente resolvidos se houvesse alguma interface no kernel para alocar uma quantidade arbitrária de páginas físicas contíguas. Não sei por que não há, mas não considero os problemas acima tão importantes quanto a por que isso não está disponível / como implementá-lo :-)

rem
fonte
2

Talvez isso ajude você a entender por que o assign_pages requer um número de página com duas potências.

Para otimizar o processo de alocação de páginas (e diminuir as fragmentações externas), que são frequentemente envolvidas, o kernel Linux desenvolveu o cache de páginas por CPU e o alocador de amigos para alocar memória (existe outro alocador, laje, para servir alocações de memória menores que um página).

O cache de páginas por CPU atende à solicitação de alocação de uma página, enquanto o buddy-alocador mantém 11 listas, cada uma contendo 2 ^ {0-10} páginas físicas, respectivamente. Essas listas têm bom desempenho quando alocam e liberam páginas e, é claro, a premissa é que você está solicitando um buffer de tamanho 2.

medivh
fonte