Pergunta

Estou trabalhando no design do kernel e tenho algumas perguntas sobre a paginação.

A idéia básica que eu tenho até agora é a seguinte: cada programa recebe seu próprio (ou assim pensa) 4G de memória, menos uma seção em algum lugar que me reservo para o kernel funções que o programa pode ligar. Portanto, o sistema operacional precisa descobrir alguma maneira de carregar as páginas na memória que o programa precisa usar durante sua operação.

Agora, supondo que tivéssemos infinitas quantidades de memória e tempo do processador, eu poderia carregar/alocar qualquer página em que o programa tenha escrito ou leu o que aconteceu usando falhas de página para páginas que não existiam (ou foram trocadas) para que o sistema tenha sido Porém, poderia alocá -los ou trocá -los rapidamente. No mundo real, preciso otimizar esse processo, para que não tenhamos um programa constantemente consumindo toda a memória que ele tocou.

Então, acho que minha pergunta é: como um sistema operacional geralmente faz isso? Meu pensamento inicial é criar uma função que o programa chama para definir/gratuitamente páginas, que ele pode gerenciar a memória por conta própria, mas um programa geralmente faz isso ou o compilador assume que ele tem reinado livre? Além disso, como o compilador lida com situações em que ele precisa alocar um segmento de memória bastante grande? Preciso fornecer uma função que tenta dar x páginas em ordem?

Obviamente, essa não é uma pergunta específica do idioma, mas sou parcial para o padrão C e bom com C ++, então eu gostaria que qualquer exemplo de código esteja nesse ou na montagem. (A montagem não deve ser necessária, pretendo fazer com que ela funcione com o máximo de código C e otimize como uma última etapa.)

Outra coisa que deve ser mais fácil de responder também: como geralmente lida com as funções do kernel que um programa precisa ligar? Tudo bem apenas ter uma área definida de memória (eu estava pensando no final do espaço virtual) que contém a maioria das funções básicas/processo de processo específico que o programa pode chamar? Meu pensamento a partir daí seria que as funções do kernel façam algo muito chique e trocar as páginas (para que os programas não pudessem ver o kernel sensível funções em seu próprio espaço) quando os programas precisavam fazer algo importante, mas não sou realmente focando na segurança neste momento.

Então, acho que estou mais preocupado com as idéias gerais de design do que com os detalhes. Eu gostaria de tornar o kernel completamente compatível com o GCC (de alguma forma) e preciso garantir que ele forneça tudo o que um programa normal precisaria.

Obrigado por qualquer conselho.

Foi útil?

Solução

Um bom ponto de partida para todas essas perguntas é ver como o Unix faz isso. Como diz uma citação famosa: "Aqueles que não entendem o Unix estão fadados a reinventá -la mal".

Primeiro, sobre chamar funções do kernel. Não basta simplesmente ter as funções em algum lugar que um programa pode ligar, já que o programa provavelmente está sendo executado no "Modo do usuário" (anel 3 no IA-32) e o kernel precisa ser executado no "kernel Mode" (geralmente anel 0 no IA-32) para fazer suas operações privilegiadas. De alguma forma, você deve fazer a transição entre os dois modos, e isso é muito específico da arquitetura.

No IA-32, a maneira tradicional é usar um portão no IDT juntamente com uma interrupção de software (o Linux usa o INT 0x80). Os processadores mais recentes têm outras maneiras (mais rápidas) de fazê -lo e quais estão disponíveis depende se a CPU é da AMD ou Intel e do modelo específico da CPU. Para acomodar essa variação, os kernels Linux recentes usam uma página de código mapeada pelo kernel na parte superior do espaço de endereço para cada processo. Portanto, no Linux recente, para fazer uma chamada de sistema, você chama uma função nesta página, que, por sua vez na inicialização, dependendo dos recursos do seu processador).

Agora, o gerenciamento da memória. Isto é um enorme sujeito; Você pode escrever um grande livro sobre isso e não exaustar o assunto.

Lembre -se de que há pelo menos dois Visualizações da memória: a visualização física (a ordem real das páginas, visível ao subsistema de memória de hardware e, muitas vezes, aos periféricos externos) e à visualização lógica (a ordem das páginas vistas pelos programas em execução na CPU). É muito fácil confundir os dois. Você estará alocando fisica páginas e atribuí -las a lógico endereços no espaço de endereços do programa ou kernel. Uma única página física pode ter vários endereços lógicos e pode ser mapeada para diferentes endereços lógicos em diferentes processos.

A memória do kernel (reservada para o kernel) geralmente é mapeada na parte superior do espaço de endereço de todos os processos. No entanto, ele é configurado para que só possa ser atribuído ao modo de kernel. Não há necessidade de truques sofisticados para esconder essa parte da memória; O hardware faz todo o trabalho de bloquear o acesso (no IA-32, é feito por meio de sinalizadores de página ou limites de segmento).

Os programas alocam memória no restante do espaço de endereço de várias maneiras:

  • Parte da memória é alocada pelo carregador de programas do kernel. Isso inclui o código do programa (ou "Texto"), o programa inicializado Data ("Dados"), o programa que não é inicializado dados ("BSS", preenchido por zero), a pilha e várias chances e fins. Quanto para alocar, onde, quais devem ser o conteúdo inicial, quais sinalizadores de proteção usar e várias outras coisas, são lidos dos cabeçalhos do arquivo executável a serem carregados.
  • Tradicionalmente no Unix, há uma área de memória que pode crescer e encolher (seu limite superior pode ser alterado através do brk() chamada do sistema). Isso é tradicionalmente usado pelo heap (o alocador de memória na biblioteca C, da qual malloc() é uma das interfaces, é responsável pela pilha).
  • Muitas vezes, você pode pedir ao kernel para mapear um arquivo para uma área de espaço de endereço. Leia e gravações para essa área são (via Paging Magic) direcionadas ao arquivo de apoio. Isso geralmente é chamado mmap(). Com um anônimo mmap, você pode alocar novas áreas do espaço de endereço que não são apoiadas por nenhum arquivo, mas, de outra forma, agem da mesma maneira. O carregador do programa do kernel costuma usar mmap Para alocar partes do código do programa (por exemplo, o código do programa pode ser apoiado pelo próprio executável).

As áreas de acesso ao espaço de endereço que não são alocadas de forma alguma (ou são reservadas para o kernel) são consideradas um erro e no UNIX fará com que um sinal seja enviado ao programa.

O compilador aloca a memória estaticamente (especificando -a nos cabeçalhos de arquivo executável; o carregador do programa do kernel alocará a memória ao carregar o programa) ou dinamicamente (chamando uma função na biblioteca padrão do idioma, que geralmente chama uma função na função na função na função da função na função da função na função do C Biblioteca padrão de idioma, que então chama o kernel para alocar memória e subdividir, se necessário).

A melhor maneira de aprender o básico de tudo isso é ler um dos vários livros sobre sistemas operacionais, em particular os que usam uma variante UNIX como exemplo. Ele será mais detalhado do que eu poderia em uma resposta no StackOverflow.

Outras dicas

A resposta a esta pergunta depende muito da arquitetura. Vou assumir que você está falando sobre x86. Com x86, um kernel geralmente fornece um conjunto de chamadas do sistema, que são pontos de entrada predeterminados no kernel. O código do usuário pode inserir apenas o kernel nesses pontos específicos, para que o kernel tenha um controle cuidadoso sobre como ele interage com o código do usuário.

No x86, existem duas maneiras de implementar chamadas do sistema: com interrupções e com as instruções Sysenter/Sysexit. Com interrupções, o kernel configura um Tabela de descritores de interrupção (IDT), que define os possíveis pontos de entrada no kernel. O código do usuário pode então usar o int Instrução para gerar uma interrupção suave para ligar para o kernel. As interrupções também podem ser geradas por hardware (as chamadas interrupções duras); Essas interrupções geralmente devem ser distintas das interrupções suaves, mas não precisam ser.

As instruções Sysenter e Sysexit são uma maneira mais rápida de executar chamadas do sistema, pois o manuseio de interrupções é lento; Não estou tão familiarizado em usá -los, por isso não posso comentar se eles são ou não uma escolha melhor para a sua situação.

Qualquer que seja o que você usar, você terá que definir a interface de chamada do sistema. Você provavelmente desejará passar os argumentos de chamada do sistema nos registros e não na pilha, pois gerar uma interrupção fará com que você mude as pilhas para a pilha do kernel. Isso significa que você quase certamente precisará escrever alguns stubs de linguagem de montagem na extremidade do modo de usuário para fazer a chamada do sistema e novamente na extremidade do kernel para reunir os argumentos de chamada do sistema e salvar os registros.

Depois de ter tudo isso no lugar, você pode começar a pensar em lidar com falhas de página. As falhas da página são efetivamente apenas mais um tipo de interrupção - quando o código do usuário tenta acessar um endereço virtual para o qual não há entrada na tabela de páginas, ele gerará a interrupção 14 e você também obterá o endereço de falha como código de erro. O kernel pode pegar essas informações e, em seguida, decidir ler na página que faltava no disco, adicione o mapeamento da tabela de páginas e voltar ao código do usuário.

Eu recomendo que você dê uma olhada em alguns dos materiais do Sistemas operacionais do MIT classe. Confira a seção de referências, ele tem muitas coisas boas.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top