Syscalls (chamadas de sistema) causam alguma penalidade de desempenho devido ao isolamento entre o kernel e o espaço do usuário. Portanto, parece uma boa ideia reduzir as syscalls.
Então, o que eu pensei é que poderíamos agrupar syscalls em um único. Então, a ideia é colocar as syscalls e argumentos em uma estrutura de dados simples na memória. Então poderíamos introduzir uma nova syscall, que damos a essa estrutura de dados. O kernel poderia, então, disparar toda a funcionalidade em paralelo e retomar o thread se uma (ou todas) syscalls terminassem.
Acho que essa abordagem seria uma boa base para programação simultânea (E/S assíncrona) e melhoraria as soluções existentes de select/poll/epoll, permitindo a simultaneidade em qualquer syscall e reduzindo as trocas de contexto gerais.
Por que isso não é feito?
Isso já existe. No Linux é implementado por io_uring , disponível desde a versão 5.1 do kernel (maio de 2019): as operações são colocadas em uma fila (ou melhor, em anel) e processadas sem chamadas do sistema, com seus resultados indo para outra fila.
O conceito geral está feito e existe. O exemplo mais próximo é io_uring no Linux, como aponta a resposta de Stephen Kitt, mas está longe de ser o único exemplo desse tipo de interface. Windows, Solaris, AmigaOS e um pequeno punhado de outros sistemas operacionais, todos têm mecanismos de fila de conclusão orientados a IO semelhantes que funcionam de maneira semelhante a io_uring (o Linux está realmente um pouco atrasado aqui).
Além disso, existem muitas chamadas de sistema em sistemas semelhantes ao UNIX que, embora não funcionem como você está sugerindo, evitam muitas trocas de contexto em potencial, enviando algumas tarefas para o kernel que normalmente seriam feitas no espaço do usuário. A
sendfile()
chamada do sistema é provavelmente o melhor exemplo desse tipo de syscall, ela pega uma tarefa muito comum (copiar uma grande quantidade de dados de um descritor de arquivo para outro) e a envia totalmente para o modo kernel, evitando completamente o loop e várias trocas de contexto (e buffers extras) que seriam necessários para fazer isso no espaço do usuário.Uma coisa importante a entender aqui, porém, é que, para que isso faça sentido, o custo de realmente configurar tudo associado ao conjunto relevante de operações em massa como este deve ser menor do que o custo de fazê-lo apenas da maneira 'normal'. Usar io_uring só faz sentido se você estiver lidando com muitos IO, como ao emular um dispositivo de armazenamento em bloco para uma VM (QEMU suporta usá-lo para isso, e a diferença de desempenho mesmo em hardware de host rápido é insana ) ou lendo milhares de arquivos uma vez por segundo (a empresa para a qual trabalho recentemente começou a falar internamente sobre a possibilidade de usar io_uring para tais cargas de trabalho). De forma similar,
sendfile()
só faz sentido se você precisar de mais de uma iteração de leitura/gravação para copiar os dados por meio do espaço do usuário (embora isso geralmente seja uma função de não poder fornecer o espaço do buffer no espaço do usuário, não que seja mais rápido executar uma iteração de leitura/gravação) .Além disso, a chamada do sistema realmente precisa fazer sentido no contexto do processamento em lote. IO geralmente faz sentido aqui, desde que o processamento preserve a ordem das chamadas, mas muitas coisas simplesmente não. Seria bobagem tentar usar esse tipo de interface, por
exec()
exemplo (um fork e exec combinados talvez , mas não um exec simples). Da mesma forma, alguns tipos de chamada do sistema só são úteis se processados isoladamente. A manipulação da máscara de sinal do processo é um bom exemplo disso, além da configuração inicial, você quase sempre está fazendo isso para proteger uma seção crítica em seu código e geralmente precisa de manipulação imediata e previsível para essa finalidade.Esses recursos existem há muito tempo.
O Solaris 2.6 em 1997 adicionou uma chamada de sistema IO assíncrona do kernel que faz exatamente isso -
kaio()
.Uma maneira de acessá-lo é através da função `lio_listio() :
O código-fonte Illumos
libc
que foi de código aberto e descendente dessa implementação original do Solarislio_listio()
pode ser encontrado em https://github.com/illumos/illumos-gate/blob/470204d3561e07978b63600336e8d47cc75387fa/usr/src/lib/libc/port/aio /posix_aio.c#L121Uma razão pela qual recursos como esse não são mais comuns é que eles realmente não melhoram muito o desempenho, a menos que todo o sistema de software e hardware seja projetado para aproveitá-lo.
O armazenamento deve ser configurado para fornecer blocos alinhados adequadamente, os sistemas de arquivos devem ser construídos para que estejam alinhados adequadamente aos blocos fornecidos pelo sistema de armazenamento e toda a pilha de software precisa ser escrita para não estragar o IO - tudo isso tem para fazer IO devidamente alinhado.
E com discos giratórios, é fácil para um lote de operações de E/S para o(s) mesmo(s) disco(s) interferir umas nas outras e, na verdade, desacelerar tudo, pois o(s) cabeçote(s) gasta(m) mais tempo procurando.
E, na minha experiência, basta uma das camadas para fazer as coisas erradas para que a vantagem de desempenho das chamadas de sistema em lote desapareça na sobrecarga. Porque o IO é lento em comparação com a pior sobrecarga de chamada do sistema.
O custo de criar e manter um sistema combinado de hardware/software para aproveitar as vantagens da melhoria de desempenho que as chamadas de sistema IO em lote oferecem é imenso.
E os melhores números que já vi são que agrupar muitas chamadas IO em uma única chamada do sistema pode melhorar o desempenho em cerca de 25 a 30%.
Se você estiver processando centenas de GB de dados continuamente, o tempo todo, isso é importante.
Construir e manter todo um sistema como esse apenas para diminuir a latência de visualização de vídeos de gatos de 8 ms para 6 ms? Não muito.