Transferência de Dados Eficiente através de Cópia Zero

Cópia Zero, Sobrecarga Zero

Este artigo explica como é possível aperfeiçoar o desempenho de aplicativos Java de E/S intensiva em execução nas plataformas Linux® e UNIX® por meio de uma técnica chamada cópia zero. A cópia zero permite que sejam evitadas cópias de dados redundantes entre buffers intermediários e reduz o número de comutadores de contexto entre o espaço de usuário e o espaço do kernel.

Sathish K. Palaniappan, System Software Engineer, Systems Documentation, Inc. (SDI)

Pramod B. Nagaraja é um engenheiro de software do sistema no Java Technology Centre, IBM India Labs.



Pramod B. Nagaraja, Associate System Software Engineer, Systems Documentation, Inc. (SDI)

Pramod B. Nagaraja é um engenheiro de software do sistema no Java Technology Centre, IBM India Labs.



02/Set/2008

Muitos aplicativos da Web servem uma quantidade significativa de conteúdo estático, que soma dados de leitura fora de um disco e grava novamente os mesmos dados para o soquete de resposta. Essa atividade pode parecer exigir relativamente pouca atividade da CPU, mas ela é, de certa forma, ineficaz: o kernel lê os dados fora do disco e os coloca no limite do usuário do kernel para o aplicativo e depois o aplicativo os coloca de volta ao limite do usuário do kernel para gravação no soquete. De fato, o aplicativo atua como um intermediário ineficaz que obtém os dados do arquivo de disco para o soquete.

Sempre que o dado passa do limite de kernel do usuário, ele deve ser copiado, o que consome ciclos de CPU e largura de banda da memória. Felizmente, é possível eliminar essas cópias através de uma técnica chamada, — adequadamente, de — cópia zero. Os aplicativos que usam a cópia zero solicitam que o kernel copie os dados diretamente do arquivo de disco para o soquete, sem passar pelo aplicativo. A cópia zero aumenta muito o desempenho do aplicativo e reduz o número de comutadores de contexto entre o modo do kernel e do usuário.

As bibliotecas de classe Java oferecem suporte à cópia zero nos sistemas Linux e UNIX através do método transferTo() em java.nio.channels.FileChannel. É possível utilizar o método transferTo() para transferir bytes diretamente do canal no qual ele foi chamado para outro canal de byte gravável, sem exigir que os dados passem pelo aplicativo. Este artigo primeiro demonstra a sobrecarga embutida na transferência de arquivo simples, feita através de semânticas de cópias tradicionais, e depois mostra como a técnica de cópia zero utiliza transferTo() para obter melhor desempenho.

Transferência de Dados: A Abordagem Tradicional

Considere o cenário de leitura de um arquivo e transferência de dados para outro programa através da rede. (Este cenário descreve o comportamento de muitos aplicativos do servidor, inclusive os aplicativos da Web que servem o conteúdo estático, os servidores FTP, os servidores de correio, etc). O núcleo da operação está nas duas chamadas da Lista 1 (consulte Faça Download de para obter um link para um código de amostra completo):

Lista 1. Copiando Bytes de um Arquivo para um Soquete
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

Embora a Lista 1 seja conceitualmente simples, internamente, a operação de cópia requer quatro comutadores de contexto entre o modo do usuário e do kernel e os dados são copiados quatro vezes antes da conclusão da operação. A Figura 1 mostra como os dados são movidos internamente do arquivo para o soquete:

Figura 1. Abordagem de Cópia de Dados Tradicional
Abordagem de Cópia de Dados Tradicional

A Figura 2 mostra a comutação de contexto:

Figura 2. Comutadores de Contexto Tradicionais
Comutadores de Contexto Tradicionais

As etapas envolvidas são:

  1. A chamada read() causa uma comutação de contexto (consulte a Figura 2) do modo do usuário para o modo do kernel. Internamente, sys_read() (ou equivalente) é emitido para a leitura dos dados a partir do arquivo. A primeira cópia (consulte a Figura 1) é executada pelo mecanismo de acesso direto à memória (DMA), que lê o conteúdo do arquivo do disco e o armazena em um buffer de espaço de endereço do kernel.
  2. A quantidade de dados solicitada é copiada do buffer de leitura no buffer de usuário e a chamada read() retorna. O retorno da chamada faz com que outro comutador de contexto do kernel volte para o modo do usuário. Agora os dados são armazenados no buffer de espaço de endereço do usuário.
  3. A chamada do soquete send() causa a comutação do contexto do modo do usuário para o modo do kernel. Uma terceira cópia é executada para colocar os dados novamente em um buffer de espaço de endereço do kernel. De qualquer forma, neste momento, os dados são colocados em um buffer diferente, um que seja associado ao soquete de destino.
  4. A chamada do sistema send() retorna, criando o quarto comutador de contexto. De forma independente e assíncrona, ocorre uma quarta cópia, à medida que o mecanismo DMA transmite os dados do buffer de kernel para o mecanismo de protocolo.

O uso do buffer de kernel intermediário (em vez de uma transferência direta dos dados no buffer do usuário) pode parecer ineficaz. Mas os buffers de kernel intermediários foram introduzidos no processo para melhorar o desempenho. O uso do buffer intermediário na leitura permite que o buffer de kernel atue como um "cache readahead", quando o aplicativo não solicita tantos dados quantos o buffer de kernel mantém. Isso melhora significativamente o desempenho, quando a quantidade de dados solicitada é menor que o tamanho do buffer de kernel. O buffer intermediário na gravação permite que a gravação seja concluída de forma assíncrona.

Infelizmente, essa abordagem pode tornar, por si só, um problema para o desempenho se o tamanho dos dados solicitados for consideravelmente maior que o tamanho de buffer do kernel. Os dados são copiados várias vezes entre o disco, o buffer de kernel e o buffer de usuário antes de finalmente serem entregues ao aplicativo.

A cópia zero melhora o desempenho, eliminando essas cópias de dados redundantes.


Transferência de Dados: A Abordagem de Cópia Zero

Se você examinar novamente o cenário tradicional, observará que a segunda e terceira cópias de dados não são realmente necessárias. O aplicativo não faz nada mais que armazenar os dados em cache e transferi-los de volta ao buffer de soquete. Em vez disso, os dados podem ser transferidos diretamente do buffer de leitura para o buffer de soquete. O método transferTo() permite que você faça exatamente isso. A Lista 2 mostra a assinatura do método de transferTo():

Lista 2. O Método transferTo()
public void transferTo(long position, long count, WritableByteChannel target);

O método transferTo() transfere dados do canal de arquivo para o canal de byte gravável fornecido. Internamente, isso depende do suporte do sistema operacional subjacente para a cópia zero; no UNIX e em vários recursos do Linux, essa chamada é roteada para a chamada do sistema sendfile(), mostrada na Lista 3, que transfere dados de um descritor de arquivos para outro:

Lista 3. A Chamada do Sistema sendfile()
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

A ação das chamadas file.read() e socket.send() na Lista 1 pode ser substituída por uma única chamada transferTo(), como mostrado na Lista 4:

Lista 4. Utilizando transferTo() para Copiar Dados de um Arquivo de Disco para um Soquete
transferTo(position, count, writableChannel);

A Figura 3 mostra o caminho dos dados quando o método transferTo() é utilizado:

Figura 3. Cópia de Dados com transferTo()
Cópia de Dados com transferTo()

A Figura 4 mostra os comutadores de contexto quando o método transferTo() é utilizado:

Figura 4. Comutação de Contexto com transferTo()
Comutação de Contexto usando o transferTo()

As etapas feitas quando se utilizatransferTo() como na Lista 4 são:

  1. O método transferTo() faz com que os conteúdos do arquivo sejam copiados em um buffer de leitura pelo mecanismo DMA. Depois os dados são copiados pelo kernel no buffer de kernel associado ao soquete de saída.
  2. A terceira cópia ocorre quando o mecanismo DMA transmite os dados dos buffers de soquete do kernel para o mecanismo de protocolo.

Este é um aperfeiçoamento: reduzimos o número de comutadores de contexto de quatro para dois e reduzimos o número de cópias de dados de quatro para três (apenas envolvendo a CPU). Mas isso ainda não nos leva a nossa meta de cópia zero. Podemos reduzir ainda mais a duplicação de dados feita pelo kernel se a placa da interface de rede subjacente oferecer suporte a operações de coleta. Nos kernels Linux 2.4 e posteriores, o descritor de buffer de soquete foi modificado para acomodar esse requisito. Essa abordagem não apenas reduz os diversos comutadores de contexto, mas também elimina as cópias de dados duplicadas que exigem envolvimento da CPU. O uso do usuário ainda permanece igual, mas a essência mudou:

  1. O método transferTo() faz com que o conteúdo do arquivo seja copiado em um buffer de kernel pelo mecanismo DMA.
  2. Nenhum dado é copiado no buffer do soquete. Ao contrário, somente descritores com informações sobre o local e o comprimento dos dados são anexados ao buffer de soquete. O mecanismo DMA transmite dados diretamente do buffer de kernel para o mecanismo de protocolo, eliminando assim a cópia final da CPU restante.

A Figura 5 mostra as cópias de dados usando transferTo() com a operação de coleta:

Figura 5. Cópias de Dados quando transferTo() e as Operações de Coleta São Utilizadas
Cópias de Dados quando transferTo() e as Operações de Coleta São Utilizadas

Criando um Servidor de Arquivo

Agora colocaremos a cópia zero em prática, utilizando o mesmo exemplo de transferência de um arquivo entre um cliente e um servidor (consulte Faça Download de para o código de amostra). TraditionalClient.java e TraditionalServer.java são baseados nas semânticas de cópias tradicionais, que utilizam File.read() e Socket.send(). TraditionalServer.java é um programa de servidor que atende em uma porta específica para a conexão do cliente e depois lê 4K bytes de dados de uma vez do soquete. TraditionalClient.java se conecta ao servidor, lê (utilizando File.read()) 4K bytes de dados de um arquivo e envia (utilizando socket.send()) o conteúdo para o servidor via soquete.

De modo semelhante, TransferToServer.java e TransferToClient.java executam a mesma função, mas, ao contrário, utilizam o método transferTo() (e, por sua vez, a chamada do sistema sendfile() ) para transferir o arquivo do servidor para o cliente.

Comparação de Desempenho

Executamos os programas de amostra em um sistema Linux executando o kernel 2.6 e medimos o tempo de execução em milissegundos tanto para a abordagem tradicional quanto para a abordagem transferTo() para vários tamanhos. A Tabela 1 mostra os resultados:

Tabela 1. Comparação de Desempenho: Abordagem Tradicional vs. Cópia Zero
Tamanho do ArquivoTransferência de Arquivo Normal (ms)transferTo (ms)
7MB15645
21MB337128
63MB843387
98MB1320617
200MB21241150
350MB36311762
700MB134984422
1GB183998537

Como é possível ver, a API transferTo() reduz o tempo em aproximadamente 65% quando comparada à abordagem tradicional. Isso pode aumentar significativamente o desempenho de aplicativos que fazem muitas cópias de dados de um canal de E/S para outro, como servidores da Web.


Resumo

Demonstramos as vantagens do desempenho utilizando transferTo(), comparado à leitura de um canal e à gravação dos mesmos dados em outro. Cópias de buffers intermediários, — mesmo aqueles ocultos no kernel, — podem ter um custo moderado. Em aplicativos que fazem muitas cópias de dados entre canais, a técnica de cópia zero pode oferecer uma melhoria significativa no desempenho.


Download

DescriçãoNomeTamanho
Sample programs for this articlej-zerocopy.zip3KB

Recursos

Aprender

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux
ArticleID=382607
ArticleTitle=Transferência de Dados Eficiente através de Cópia Zero
publish-date=09022008