Qual é a diferença entre o espaço do usuário e o espaço do kernel?

72

O espaço do Kernel é usado quando o Kernel está sendo executado em nome do programa do usuário, ou seja, Chamada do Sistema? Ou é o espaço de endereço para todos os threads do Kernel (por exemplo, agendador)?

Se for o primeiro, significa que o programa normal do usuário não pode ter mais de 3 GB de memória (se a divisão for 3 GB + 1 GB)? Além disso, nesse caso, como o kernel pode usar Alta Memória, porque para qual endereço de memória virtual as páginas da memória alta serão mapeadas, pois 1 GB de espaço no kernel será mapeado logicamente?

Poojan
fonte

Respostas:

93

O espaço do Kernel é usado quando o Kernel está sendo executado em nome do programa do usuário, ou seja, Chamada do Sistema? Ou é o espaço de endereço para todos os threads do Kernel (por exemplo, agendador)?

Sim e sim.

Antes de prosseguirmos, devemos declarar isso sobre memória.

O get de memória é dividido em duas áreas distintas:

  • O espaço do usuário , que é um conjunto de locais onde os processos normais do usuário são executados (ou seja, tudo que não seja o kernel). O papel do kernel é gerenciar aplicativos em execução neste espaço, mexendo um com o outro e com a máquina.
  • O espaço do kernel , que é o local em que o código do kernel está armazenado e é executado em.

Os processos em execução no espaço do usuário têm acesso apenas a uma parte limitada da memória, enquanto o kernel tem acesso a toda a memória. Os processos em execução no espaço do usuário também não têm acesso ao espaço do kernel. Os processos de espaço do usuário podem acessar apenas uma pequena parte do kernel por meio de uma interface exposta pelo kernel - o sistema chama . Se um processo executa uma chamada do sistema, uma interrupção de software é enviada ao kernel, que despacha o manipulador de interrupção apropriado e continua seu trabalho após o término do manipulador.

O código de espaço do kernel possui a propriedade para executar no "modo kernel", que (em seu computador desktop -x86- típico) é o que você chama de código que é executado no anel 0 . Normalmente na arquitetura x86, existem 4 anéis de proteção . Anel 0 (modo kernel), Anel 1 (pode ser usado por hipervisores ou drivers de máquinas virtuais), Anel 2 (pode ser usado por drivers, embora não tenha tanta certeza disso). O anel 3 é o que os aplicativos comuns executam. É o anel menos privilegiado e os aplicativos em execução nele têm acesso a um subconjunto das instruções do processador. O anel 0 (espaço do kernel) é o anel mais privilegiado e tem acesso a todas as instruções da máquina. Por exemplo, um aplicativo "simples" (como um navegador) não pode usar instruções de montagem x86lgdtpara carregar a tabela de descritores globais ou hltpara interromper um processador.

Se for o primeiro, significa que o programa normal do usuário não pode ter mais de 3 GB de memória (se a divisão for 3 GB + 1 GB)? Além disso, nesse caso, como o kernel pode usar Alta Memória, porque para qual endereço de memória virtual as páginas da memória alta serão mapeadas, pois 1 GB de espaço no kernel será mapeado logicamente?

Para obter uma resposta, consulte a excelente resposta por wag aqui

NlightNFotis
fonte
4
Não hesite em me dizer se cometi algum erro em algum lugar. Eu sou novo na programação do kernel e joguei aqui o que aprendi até agora, juntamente com outras informações que encontrei na web. O que significa que pode haver deficiências na minha compreensão dos conceitos que podem ser demonstrados no texto.
N
Obrigado! Eu acho que agora eu entendo melhor. Apenas para garantir que eu entendi corretamente, tenho mais uma pergunta. Novamente, considerando que os primeiros 3 GB são usados ​​para o espaço do usuário e 128 MB de espaço do kernel são usados ​​para Memória alta, os 896 MB restantes (Memória Baixa) estão mapeados estaticamente no momento da inicialização?
precisa saber é o seguinte
11
@NlightNFotis digo que quase 15 pessoas acreditam que tudo o que você disse, está correta (ou assim que você nos faz pensar;))
Braiam
Eu pensei que o anel x86 -1era para hipervisores? pt.wikipedia.org/wiki/Protection_ring
Dori
11
Observe a diferença entre memória virtual e memória física. A maior parte do que você pergunta é sobre memória virtual. Isso é mapeado para a memória física, fica complicado quando a memória física se aproxima de 3 GB e o PAE é usado. Torna-se simples novamente quando um kernel de 64 bits é usado; nesse caso, endereços negativos são reservados para o kernel e positivos para o espaço do usuário. Os processos de 32 bits agora podem usar 4 GB de espaço virtual. Os processos de 64 bits podem usar muito mais - normalmente 48 bits (no momento, no x86-64).
Ctrl-alt-delor
16

Os anéis da CPU são a distinção mais clara

No modo protegido x86, a CPU está sempre em um dos quatro toques. O kernel do Linux usa apenas 0 e 3:

  • 0 para o kernel
  • 3 para usuários

Esta é a definição mais rígida e rápida de kernel vs userland.

Por que o Linux não usa os anéis 1 e 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

Como é determinado o anel atual?

O anel atual é selecionado por uma combinação de:

  • tabela descritor global: uma tabela na memória de entradas GDT e cada entrada possui um campo Privlque codifica o anel.

    A instrução LGDT define o endereço para a tabela atual do descritor.

    Veja também: http://wiki.osdev.org/Global_Descriptor_Table

  • o segmento registra CS, DS, etc., que apontam para o índice de uma entrada no GDT.

    Por exemplo, CS = 0significa que a primeira entrada do GDT está ativa no momento para o código em execução.

O que cada anel pode fazer?

O chip da CPU é fisicamente construído para que:

  • anel 0 pode fazer qualquer coisa

  • o anel 3 não pode executar várias instruções e gravar em vários registros, principalmente:

    • não pode mudar seu próprio anel! Caso contrário, poderia definir-se para tocar 0 e os anéis seriam inúteis.

      Em outras palavras, não é possível modificar o descritor de segmento atual , que determina o anel atual.

    • não pode modificar as tabelas da página: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      Em outras palavras, não é possível modificar o registro CR3 e a paginação em si impede a modificação das tabelas de páginas.

      Isso impede que um processo veja a memória de outros processos por motivos de segurança / facilidade de programação.

    • não pode registrar manipuladores de interrupção. Esses são configurados gravando nos locais da memória, o que também é impedido pela paginação.

      Os manipuladores executam no anel 0 e quebram o modelo de segurança.

      Em outras palavras, não é possível usar as instruções LGDT e LIDT.

    • não pode executar instruções de E / S como ine out, portanto, possui acessos de hardware arbitrários.

      Caso contrário, por exemplo, as permissões de arquivo seriam inúteis se algum programa pudesse ler diretamente do disco.

      Mais precisamente graças a Michael Petch : é realmente possível que o sistema operacional permita instruções de E / S no anel 3; isso é realmente controlado pelo segmento de estado da tarefa .

      O que não é possível é que o anel 3 se dê permissão para fazê-lo se não o tiver em primeiro lugar.

      O Linux sempre o desaprova. Consulte também: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

Como os programas e sistemas operacionais fazem a transição entre os anéis?

  • quando a CPU está ligada, ele começa a executar o programa inicial no anel 0 (bem, mas é uma boa aproximação). Você pode pensar que esse programa inicial é o kernel (mas normalmente é um gerenciador de inicialização que chama o kernel ainda no anel 0).

  • quando um processo da terra do usuário deseja que o kernel faça algo como gravar em um arquivo, ele usa uma instrução que gera uma interrupção como int 0x80ousyscall para sinalizar o kernel. x86-64 Linux syscall hello world example:

    .data
    hello_world:
        .ascii "hello world\n"
        hello_world_len = . - hello_world
    .text
    .global _start
    _start:
        /* write */
        mov $1, %rax
        mov $1, %rdi
        mov $hello_world, %rsi
        mov $hello_world_len, %rdx
        syscall
    
        /* exit */
        mov $60, %rax
        mov $0, %rdi
        syscall
    

    compile e execute:

    as -o hello_world.o hello_world.S
    ld -o hello_world.out hello_world.o
    ./hello_world.out
    

    GitHub upstream .

    Quando isso acontece, a CPU chama um manipulador de retorno de chamada de interrupção que o kernel registrou no momento da inicialização. Aqui está um exemplo baremetal concreto que registra um manipulador e o utiliza .

    Esse manipulador é executado no anel 0, que decide se o kernel permitirá essa ação, faça a ação e reinicie o programa userland no anel 3. x86_64

  • quando a execchamada do sistema é usada (ou quando o kernel inicia/init ), o kernel prepara os registros e a memória do novo processo da terra do usuário, depois pula para o ponto de entrada e muda a CPU para tocar 3

  • Se o programa tentar fazer algo malicioso, como gravar em um registro ou endereço de memória proibido (por causa da paginação), a CPU também chamará algum manipulador de retorno de chamada do kernel no anel 0.

    Mas como a região do usuário foi malcriada, o kernel pode interromper o processo dessa vez ou avisar com um sinal.

  • Quando o kernel é inicializado, ele configura um relógio de hardware com alguma frequência fixa, o que gera interrupções periodicamente.

    Esse relógio de hardware gera interrupções que executam o anel 0 e permite agendar quais processos da terra do usuário serão ativados.

    Dessa forma, o agendamento pode ocorrer mesmo se os processos não estiverem fazendo nenhuma chamada do sistema.

Qual o sentido de ter vários anéis?

Existem duas vantagens principais na separação do kernel e da terra do usuário:

  • é mais fácil criar programas, pois você tem mais certeza de que um não interferirá no outro. Por exemplo, um processo da terra do usuário não precisa se preocupar em substituir a memória de outro programa por causa da paginação, nem em colocar o hardware em um estado inválido para outro processo.
  • é mais seguro. Por exemplo, permissões de arquivo e separação de memória podem impedir que um aplicativo de hackers leia seus dados bancários. Isso supõe, é claro, que você confie no kernel.

Como brincar com isso?

Eu criei uma configuração bare metal que deve ser uma boa maneira de manipular anéis diretamente: https://github.com/cirosantilli/x86-bare-metal-examples

Infelizmente, não tive a paciência de fazer um exemplo da terra do usuário, mas fui até a configuração de paginação, portanto a terra do usuário deve ser viável. Eu adoraria ver uma solicitação de recebimento.

Como alternativa, os módulos do kernel Linux são executados no anel 0, para que você possa usá-los para experimentar operações privilegiadas, por exemplo, leia os registros de controle: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-de-um-programa-obtendo-segmento / 7419306 # 7419306

Aqui está uma configuração conveniente do QEMU + Buildroot para testá-lo sem matar o seu host.

A desvantagem dos módulos do kernel é que outros kthreads estão em execução e podem interferir nos seus experimentos. Mas, em teoria, você pode assumir todos os manipuladores de interrupção com o seu módulo do kernel e possuir o sistema, que seria realmente um projeto interessante.

Anéis negativos

Embora os anéis negativos não sejam realmente mencionados no manual da Intel, na verdade existem modos de CPU que possuem recursos adicionais que o próprio anel 0 e, portanto, são adequados para o nome "anel negativo".

Um exemplo é o modo hypervisor usado na virtualização.

Para obter mais detalhes, consulte: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1

BRAÇO

No ARM, os anéis são chamados de Níveis de exceção, mas as idéias principais permanecem as mesmas.

Existem 4 níveis de exceção no ARMv8, comumente usados ​​como:

  • EL0: userland

  • EL1: kernel ("supervisor" na terminologia do ARM).

    Introduzida com a svcinstrução (SuperVisor Call), anteriormente conhecida como swi montagem unificada , que é a instrução usada para fazer chamadas do sistema Linux. Olá, mundo, exemplo do ARMv8:

    .text
    .global _start
    _start:
        /* write */
        mov x0, 1
        ldr x1, =msg
        ldr x2, =len
        mov x8, 64
        svc 0
    
        /* exit */
        mov x0, 0
        mov x8, 93
        svc 0
    msg:
        .ascii "hello syscall v8\n"
    len = . - msg
    

    GitHub upstream .

    Teste com o QEMU no Ubuntu 16.04:

    sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
    arm-linux-gnueabihf-as -o hello.o hello.S
    arm-linux-gnueabihf-ld -o hello hello.o
    qemu-arm hello
    

    Aqui está um exemplo baremetal concreto que registra um manipulador SVC e faz uma chamada SVC .

  • EL2: hipervisores , por exemplo, Xen .

    Introduzido com a hvcinstrução (Chamada HyperVisor).

    Um hypervisor é para um sistema operacional, o que é um sistema operacional para a terra do usuário.

    Por exemplo, o Xen permite executar vários SOs como Linux ou Windows no mesmo sistema ao mesmo tempo, e isola os SOs uns dos outros para segurança e facilidade de depuração, assim como o Linux faz para programas da área de usuário.

    Os hipervisores são uma parte essencial da infraestrutura de nuvem atual: eles permitem a execução de vários servidores em um único hardware, mantendo o uso de hardware sempre próximo de 100% e economizando muito dinheiro.

    A AWS, por exemplo, usou o Xen até 2017, quando foi transferida para a KVM .

  • EL3: mais um nível. Exemplo TODO.

    Introduzido com a smcinstrução (Secure Mode Call)

O Modelo de Referência de Arquitetura do ARMv8 DDI 0487C.a - Capítulo D1 - O Modelo do Programador de Nível de Sistema AArch64 - A Figura D1-1 ilustra isso de maneira bonita:

insira a descrição da imagem aqui

Observe como o ARM, talvez devido ao benefício da retrospectiva, tenha uma convenção de nomenclatura melhor para os níveis de privilégio do que x86, sem a necessidade de níveis negativos: 0 sendo o mais baixo e 3 o mais alto. Níveis mais altos tendem a ser criados com mais frequência do que os inferiores.

O EL atual pode ser consultado com a MRSinstrução: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

O ARM não exige que todos os níveis de exceção estejam presentes para permitir implementações que não precisam do recurso para economizar área de chip. O ARMv8 "Níveis de exceção" diz:

Uma implementação pode não incluir todos os níveis de exceção. Todas as implementações devem incluir EL0 e EL1. EL2 e EL3 são opcionais.

Por exemplo, o QEMU usa EL1, mas EL2 e EL3 podem ser ativados com opções de linha de comando: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up

Trechos de código testados no Ubuntu 18.10.

Ciro Santilli adicionou uma nova foto
fonte
3

Se for o primeiro, significa que o programa normal do usuário não pode ter mais de 3 GB de memória (se a divisão for 3 GB + 1 GB)?

Sim, este é o caso de um sistema linux normal. Havia um conjunto de patches "4G / 4G" flutuando em um ponto que tornava os espaços de endereço do usuário e do kernel completamente independentes (a um custo de desempenho porque dificultava o acesso do kernel à memória do usuário), mas acho que não eles já foram mesclados a montante e o interesse diminuiu com a ascensão de x86-64

Além disso, nesse caso, como o kernel pode usar Alta Memória, porque para qual endereço de memória virtual as páginas da memória alta serão mapeadas, pois 1 GB de espaço no kernel será mapeado logicamente?

A maneira como o linux costumava funcionar (e ainda funciona em sistemas em que a memória é pequena em comparação com o espaço de endereço) era que toda a memória física era permanentemente mapeada na parte do kernel do espaço de endereço. Isso permitiu ao kernel acessar toda a memória física sem remapear, mas claramente não é escalável para máquinas de 32 bits com muita memória física.

Assim nasceu o conceito de memória baixa e alta. a memória "baixa" é permanentemente mapeada no espaço de endereço dos kernels. memória "alta" não é.

Quando o processador está executando uma chamada de sistema, ele está sendo executado no modo kernel, mas ainda no contexto do processo atual. Portanto, ele pode acessar diretamente o espaço de endereço do kernel e o espaço de endereço do usuário do processo atual (supondo que você não esteja usando os patches 4G / 4G acima mencionados). Isso significa que não há problema em que a memória "alta" seja alocada para um processo da terra do usuário.

Usar memória "alta" para fins de kernel é mais um problema. Para acessar memória alta que não é mapeada para o processo atual, ela deve ser mapeada temporariamente no espaço de endereço do kernel. Isso significa código extra e uma penalidade de desempenho.

plugwash
fonte