Acesso à memória do espaço do usuário a partir do kernel Linux

Uma introdução à memória e APIs de espaço do usuário do Linux

Como o kernel e o espaço do usuário existem em espaços de endereço virtuais diferentes, existem considerações especiais para movimentar dados entre eles. Explore as ideias por trás dos espaços de endereço virtuais e as APIs do kernel para movimentar dados do e para o espaço do usuário, e aprenda algumas das outras técnicas de mapeamento usadas para mapear memória.

M. Tim Jones, Consultant Engineer, Emulex Corp.

M. Tim JonesM. Tim Jones é arquiteto de firmware integrado e autor das obras Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming (agora, na segunda edição), AI Application Programming (na segunda edição) e BSD Sockets Programming from a Multilanguage Perspective. Sua experiência em engenharia vai desde o desenvolvimento de kernels para espaçonaves geossíncronas até a arquitetura de sistemas integrados e o desenvolvimento de protocolos de rede. Tim é engenheiro consultor da Emulex Corp. em Longmont, Colorado.



21/Set/2010

Conecte-se ao Tim

Tim é um de nossos autores mais prolíficos e populares. Navegue por todos os artigos de Tim no developerWorks. Veja o perfil de Tim e conecte com ele, outros autores e demais leitores no My developerWorks.

Embora o byte seja a menor unidade de memória que pode ser endereçada no Linux®, é a página que serve como a abstração gerenciada da memória. Este artigo começa com uma discussão sobre o gerenciamento da memória no Linux e, em seguida, explora os métodos para manipular o espaço de endereço a partir do kernel.

Memória no Linux

No Linux, a memória do usuário e a memória do kernel são independentes e implementadas em espaços de endereço separados. Os espaços de endereço são virtualizados, ou seja, os endereços são abstraídos da memória física (por meio de um processo que será detalhado mais adiante). Como os espaços de endereço são virtualizados, podem existir vários deles. Na verdade, o kernel reside em um espaço de endereço, e cada processo reside em seu próprio espaço. Esses espaços consistem em endereços de memória virtual, permitindo que vários processos com espaços de endereço independentes se refiram a um espaço de endereço físico consideravelmente menor (a memória física da máquina). Isso não é apenas conveniente, como também é seguro, pois cada espaço de endereço está isolado e, consequentemente, protegido.

Mas essa segurança tem um preço. Como cada processo (e o kernel) pode ter endereços idênticos que se referem a regiões diferentes da memória física, não é imediatamente possível compartilhar memória. Felizmente, existem algumas soluções. Processos do usuário podem compartilhar memória por meio do mecanismo de memória compartilhada da Portable Operating System Interface for UNIX® (POSIX) (shmem), com a advertência de que cada processo pode ter um endereço virtual diferente que se refere à mesma região da memória física.

O mapeamento da memória virtual para a memória física ocorre por meio de tabelas de página, implementadas no hardware subjacente (veja a Figura 1). O próprio hardware fornece o mapeamento, mas é o kernel que gerencia as tabelas e sua configuração. Observe que, como mostrado aqui, um processo pode ter um grande espaço de endereço, mas é esparso, ou seja, pequenas regiões (páginas) do espaço se referem à memória física através das tabelas de página. Isso permite que um processo tenha um enorme espaço de endereço que é definido apenas para as páginas necessárias em um dado momento.

Figura 1. Tabelas de página fornecem o mapeamento de endereços virtuais para endereços físicos
Page tables provide the mapping from virtual addresses to physical addresses

A capacidade de definir memória para processos de maneira esparsa significa que a memória física subjacente pode ser confirmada em excesso. Por meio de um processo chamado paginação (embora, no Linux, seja geralmente chamado de troca), páginas pouco usadas são movidas dinamicamente para um dispositivo de armazenamento mais lento (por exemplo, um disco) para acomodar outras páginas que precisam ser acessadas (veja a Figura 2). Isso permite que a memória física do computador forneça páginas que um aplicativo precisa imediatamente, e migre páginas menos necessárias para o disco para melhor uso da memória física. Observe que algumas páginas podem se referir a arquivos, caso no qual os dados podem ser descarregados se a página estiver suja (por meio do cache de página) ou, se a página estiver limpa, simplesmente descartados.

Figura 2. A troca permite melhor uso do espaço da memória física ao migrar páginas pouco usadas para armazenamento mais lento e menos caro
Swap permits better use of the physical memory space by migrating less-used pages to slower and less expensive storage

Arquiteturas sem MMU

Nem todos os processadores têm MMUs. Portanto, a distribuição uClinux (microcontroller Linux) suporta um único espaço de endereço de operação. Essa arquitetura não tem a proteção oferecida por uma MMU, mas permite que o Linux seja executado em outra classe de processadores. Consulte a seção Recursos para informações sobre uClinux.

O processo pelo qual uma página é selecionada para ser descarregada para o armazenamento é chamado de algoritmo de substituição de página e pode ser implementado usando alguns algoritmos diferentes (como, por exemplo, usado menos recentemente). Esse processo pode ocorrer quando é solicitado um local de memória cuja página não está na memória (não há mapeamento presente na memory management unit [MMU]). Esse evento é chamado de falha de página e é detectado pelo hardware (a MMU) e gerenciado pelo firmware após uma interrupção de falha de página ocorrer. A Figura 3 contém uma ilustração dessa pilha.

O Linux apresenta uma implementação interessante da troca, que oferece algumas características úteis. O sistema de troca do Linux permite a criação e uso de várias partições e prioridades de troca, o que permite uma hierarquia de troca para dispositivos de armazenamento que fornecem diferentes características de desempenho (por exemplo, uma troca de primeiro nível em uma unidade de estado sólido [SSD] e um espaço de troca maior, de segundo nível, em um dispositivo de armazenamento mais lento). Designar uma prioridade maior à troca em SSD permite que ele seja usado até estar cheio; só então as páginas seriam gravadas na partição de troca de baixa prioridade (mais lenta).

Figura 3. Espaços de endereço e elementos de mapeamento de endereço virtual para físico
Virtual-to-physical address mapping

Nem todas as páginas podem ser descarregadas para a área de troca. Por exemplo, código do kernel que responde a interrupções, ou código que gerencia as tabelas de página e a lógica de troca. Essas são páginas que, obviamente, não devem ser descarregadas para a área de troca, e portanto são fixadas, ou residentes permanentemente na memória. Embora páginas do kernel não possam ser descarregadas, páginas do espaço do usuário podem, mas é possível fixá-las através da função mlock (ou mlockall) para bloquear a página. Esse é o propósito das funções de acesso à memória do espaço do usuário. Se o kernel achasse que um endereço passado por um usuário é válido e acessível, ocorreria por fim um pânico do kernel (por exemplo, porque a página do usuário foi descarregada para a área de troca, resultando em uma falha de página no kernel). Essa interface de programação de aplicativo (API) assegura que tais casos sejam tratados corretamente.


APIs do Kernel

Agora, vamos explorar as APIs do kernel para manipular a memória do usuário. Observe que esta seção cobre a interface do kernel e do espaço do usuário, mas a próxima explora mais algumas APIs de memória. As funções de acesso à memória do espaço do usuário que iremos explorar estão listadas na Tabela 1.

Tabela 1. A API de acesso à memória do espaço do usuário
FunçãoDescrição
access_okVerifica a validade do ponteiro de memória do espaço do usuário
get_userObtém uma variável simples do espaço do usuário
put_userEnvia uma variável simples para o espaço do usuário
clear_userLimpa ou zera um bloco no espaço do usuário
copy_to_userCopia um bloco de dados do kernel para o espaço do usuário
copy_from_userCopia um bloco de dados do espaço do usuário para o kernel
strnlen_userObtém o tamanho de um buffer de cadeia de caractere no espaço do usuário
strncpy_from_userCopia uma cadeia de caractere do espaço de usuário para o kernel

Como seria de se esperar, a implementação dessas funções depende da arquitetura. Em arquiteturas x86, é possível achar essas funções e símbolos definidos em ./linux/arch/x86/include/asm/uaccess.h, com origem em ./linux/arch/x86/lib/usercopy_32.c e usercopy_64.c.

O papel das funções de movimentação de dados é mostrado na Figura 4 em relação aos tipos envolvidos na cópia (simples vs. agregado).

Figura 4. Movimentação de dados usando a API de acesso à memória do espaço do usuário
Data movement using the User Space Memory Access API

A função access_ok

A função access_ok é usada para verificar a validade do ponteiro no espaço do usuário que deverá ser acessado. O responsável pela chamada fornece o ponteiro (que se refere ao início do bloco de dados), o tamanho do bloco e o tipo de acesso (se a área deve ser lida ou gravada). O protótipo de função é definido como:

access_ok( type, addr, size );

O argumento type pode ser especificado como VERIFY_READ ou VERIFY_WRITE. O simbólico VERIFY_WRITE também identifica se a região da memória é legível, além de gravável. A função retorna diferente de zero se a região for provavelmente acessível (mas o acesso ainda pode resultar em -EFAULT). Essa função simplesmente verifica se o endereço é provável no espaço do usuário, não no kernel.

A função get_user

Para ler uma variável simples do espaço do usuário, usa-se a função get_user. Ela é usada para tipos simples, tais como char e int, mas tipos de dados maiores, como estruturas, devem usar a função copy_from_user. O protótipo aceita uma variável (para armazenar os dados) e um endereço no espaço do usuário para a operação de leitura:

get_user( x, ptr );

A função get_user mapeia para uma de duas funções internas. Internamente, ela determina o tamanho da variável sendo acessada (baseado na variável fornecida para armazenar o resultado) e forma uma chamada interna por meio de __get_user_x. Essa função retorna zero em caso de sucesso. Em geral, as funções get_user e put_user são mais rápidas que suas equivalentes de cópia de bloco, e devem ser usadas quando pequenos tipos forem movimentados.

A função put_user

A função put_user é usada para gravar uma variável simples do kernel para o espaço do usuário. Assim como get_user, ela aceita uma variável (que contém o valor a ser escrito) e um endereço no espaço do usuário como destino da gravação:

put_user( x, ptr );

Assim como get_user, a função put_user é mapeada internamente para a função put_user_x, e retorna 0 em caso de sucesso ou -EFAULT em caso de erro.

A função clear_user

A função clear_user é usada para zerar um bloco de memória no espaço do usuário. Ela toma um ponteiro no espaço do usuário e um tamanho para zerar, definido em bytes:

clear_user( ptr, n );

Internamente, a função clear_user verifica primeiramente se o ponteiro do espaço do usuário é gravável (via access_ok), e em seguida chama uma função interna (escrita em assembly sequencial) para realizar a operação de Limpeza. Essa função está otimizada como um loop muito rígido usando instruções de cadeia de caractere com o prefixo de repetição. Ela retorna o número de bytes que não puderam ser limpos, ou zero caso a operação tenha tido sucesso.

A função copy_to_user

A função copy_to_user copia um bloco de dados do kernel para o espaço do usuário. Ela aceita um ponteiro para um buffer no espaço do usuário, um ponteiro para um buffer no kernel e um comprimento definido em bytes. A função retorna zero em caso de sucesso, ou diferente de zero para indicar o número de bytes que não foram transferidos.

copy_to_user( to, from, n );

Após verificar a possibilidade de gravar no buffer do usuário (através de access_ok), é chamada a função interna __copy_to_user, que, por sua vez, chama __copy_from_user_inatomic (em ./linux/arch/x86/include/asm/uaccess_XX.h, no qual XX é 32 ou 64, dependendo da arquitetura). A função (depois de determinar deve-se realizar cópias de 1, 2 ou 4 bytes) chama por fim __copy_to_user_ll, que é responsável pelo trabalho de verdade. Em hardware quebrado (antes do i486, quando o bit WP não era honrado em modo de supervisor), as tabelas de página podiam mudar a qualquer momento, exigindo que as páginas desejadas fossem fixadas na memória, de modo que não fossem descarregadas para a área de troca enquanto estavam sendo endereçadas. Depois do i486, o processo nada mais é que uma cópia otimizada.

A função copy_from_user

A função copy_from_user copia um bloco de dados do espaço do usuário para um buffer do kernel. Ela aceita um buffer de destino (no espaço do kernel), um buffer de origem (no espaço do usuário) e um comprimento definido em bytes. Assim como copy_to_user, a função retorna zero em caso de sucesso, e diferente de zero para indicar o fracasso em copiar certo número de bytes.

copy_from_user( to, from, n );

A função começa por verificar a possibilidade de ler o buffer de origem no espaço do usuário (via access_ok), e em seguida chama __copy_from_user e, por fim, __copy_from_user_ll. A partir daí, dependendo da arquitetura, uma chamada é feita para copiar do buffer do usuário para um buffer do kernel zerando (os bytes indisponíveis). As funções de assembly otimizadas incluem a capacidade de gerenciar.

A função strnlen_user

A função strnlen_user é usada da mesma maneira que strnlen, mas ela considera que o buffer está disponível no espaço do usuário. A função strnlen_user aceita dois parâmetros: o endereço do buffer no espaço do usuário e o comprimento máximo a ser verificado.

strnlen_user( src, n );

A função strnlen_user verifica primeiramente se o buffer do usuário é legível, por meio de uma chamada para access_ok. Se for acessível, a função strlen é chamada, e o argumento max length é ignorado.

A função strncpy_from_user

A função strncpy_from_user copia uma cadeia de caractere do espaço do usuário para um buffer do kernel, dados um endereço de origem no espaço do usuário e um comprimento máximo.

strncpy_from_user( dest, src, n );

Como se trata de uma cópia do espaço do usuário, a função primeiramente verifica se o buffer é gravável por meio de access_ok. De forma semelhante a copy_from_user, essa função é implementada como uma função assembly otimizada (em ./linux/arch/x86/lib/usercopy_XX.c).


Outros esquemas para mapeamento de memória

A seção anterior explorou métodos para movimentar dados entre o kernel e o espaço do usuário (com o kernel iniciando a operação). O Linux oferece outros métodos que podem ser usados para movimentação de dados, tanto no kernel como no espaço do usuário. Embora esses métodos possam não apresentar funcionalidade igual à das funções de acesso à memória do espaço do usuário, eles são semelhantes em sua capacidade de mapear memória entre os espaços de endereço.

Observe que, no espaço do usuário, como os processos do usuário aparecem em espaços de endereço separados, a movimentação de dados entre eles deve ocorrer por meio de algum tipo de mecanismo de comunicação interprocessos. O Linux oferece vários esquemas (tais como filas de mensagens), mas o mais notável é a memória compartilhada POSIX (shmem). Esse mecanismo permite que um processo crie uma área de memória e compartilhe essa região com um ou mais processos. Observe que cada processo pode mapear a região de memória compartilhada para diferentes endereços em seus respectivos espaços de endereços. Portanto é necessário um certo deslocamento de endereços.

A função mmap permite que um aplicativo do espaço do usuário crie um mapeamento no espaço de endereço virtual. Essa funcionalidade é comum em algumas classes de drivers de dispositivo (para desempenho), permitindo que a memória física do dispositivo seja mapeada para o espaço de endereço virtual do processo. Em um driver, a função mmap é implementada através da função do kernel remap_pfn_range, que fornece um mapeamento linear da memória do dispositivo para o espaço de endereço do usuário.


Indo além

Este artigo explorou o tópico de gerenciamento de memória no Linux (para entender os motivos da paginação), e explorou as funções de acesso à memória do espaço do usuário que usam esses conceitos. Movimentar dados entre o espaço de usuário e o kernel não é tão simples quanto parece, mas o Linux inclui um conjunto simples de APIs que gerencia os detalhes da tarefa em diferentes plataformas.

Recursos

Aprender

Obter produtos e tecnologias

  • Avalie produtos IBM da maneira mais conveniente para você: faça download da versão de teste de um produto, teste um produto on-line, use-o em um ambiente de nuvem ou passe algumas horas na Sandbox SOA aprendendo a implementar a Arquitetura Orientada a Serviços de forma eficiente.

Discutir

  • Envolva-se com a comunidade do My developerWorks. Conecte-se com outros usuários do developerWorks enquanto explora os blogs, fóruns, grupos e wikis conduzidos por desenvolvedores.

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=525202
ArticleTitle=Acesso à memória do espaço do usuário a partir do kernel Linux
publish-date=09212010