¿Cuál es la diferencia entre el espacio del usuario y el espacio del kernel?


73

¿Se utiliza el espacio Kernel cuando Kernel se ejecuta en nombre del programa de usuario, es decir, System Call? ¿O es el espacio de direcciones para todos los hilos del núcleo (por ejemplo, el planificador)?

Si es el primero, ¿significa que el programa de usuario normal no puede tener más de 3 GB de memoria (si la división es 3 GB + 1 GB)? Además, en ese caso, ¿cómo puede el kernel usar High Memory, porque a qué dirección de memoria virtual se asignarán las páginas de la memoria alta, ya que 1GB de espacio en el kernel se asignará lógicamente?

Respuestas:


93

¿Se utiliza el espacio Kernel cuando Kernel se ejecuta en nombre del programa de usuario, es decir, System Call? ¿O es el espacio de direcciones para todos los hilos del núcleo (por ejemplo, el planificador)?

Si y si.

Antes de continuar, debemos decir esto sobre la memoria.

La memoria se divide en dos áreas distintas:

  • El espacio del usuario , que es un conjunto de ubicaciones donde se ejecutan los procesos normales del usuario (es decir, todo lo que no sea el núcleo). El papel del kernel es administrar las aplicaciones que se ejecutan en este espacio para que no se mezclen entre sí y con la máquina.
  • El espacio del núcleo , que es la ubicación donde se almacena el código del núcleo, y se ejecuta bajo.

Los procesos que se ejecutan en el espacio del usuario solo tienen acceso a una parte limitada de la memoria, mientras que el núcleo tiene acceso a toda la memoria. Los procesos que se ejecutan en el espacio del usuario tampoco tienen acceso al espacio del kernel. Los procesos de espacio de usuario solo pueden acceder a una pequeña parte del núcleo a través de una interfaz expuesta por el núcleo: el sistema llama . Si un proceso realiza una llamada al sistema, se envía una interrupción de software al núcleo, que luego envía el controlador de interrupciones apropiado y continúa su trabajo una vez que el controlador ha finalizado.

El código de espacio del kernel tiene la propiedad de ejecutarse en "modo kernel", que (en su computadora de escritorio típica -x86-) es lo que usted llama código que se ejecuta bajo el anillo 0 . Típicamente en la arquitectura x86, hay 4 anillos de protección . Anillo 0 (modo kernel), Anillo 1 (puede ser utilizado por hipervisores o controladores de máquinas virtuales), Anillo 2 (puede ser utilizado por controladores, aunque no estoy tan seguro de eso). Ring 3 es lo que ejecutan las aplicaciones típicas. Es el anillo menos privilegiado, y las aplicaciones que se ejecutan en él tienen acceso a un subconjunto de las instrucciones del procesador. El anillo 0 (espacio del kernel) es el anillo más privilegiado y tiene acceso a todas las instrucciones de la máquina. Por ejemplo, una aplicación "simple" (como un navegador) no puede usar las instrucciones de ensamblaje x86lgdtcargar la tabla de descriptores globales o hltdetener un procesador.

Si es el primero, ¿significa que el programa de usuario normal no puede tener más de 3 GB de memoria (si la división es 3 GB + 1 GB)? Además, en ese caso, ¿cómo puede el kernel usar High Memory, porque a qué dirección de memoria virtual se asignarán las páginas de la memoria alta, ya que 1GB de espacio en el kernel se asignará lógicamente?

Para obtener una respuesta a esto, consulte la excelente respuesta de wag aquí


44
No dudes en decirme si me he equivocado en alguna parte. Soy nuevo en la programación del kernel, y arrojé aquí lo que he aprendido hasta ahora, junto con alguna otra información que encontré en la web. Lo que significa que puede haber deficiencias en mi comprensión de los conceptos que pueden demostrarse en el texto.
NlightNFotis

¡Gracias! Creo que ahora lo entiendo mejor. Solo para asegurarme de que lo entiendo correctamente, tengo una pregunta más. Una vez más, considerando que los primeros 3 GB se usan para el espacio de usuario y que se usan 128 MB de espacio en el núcleo para la memoria alta, ¿los 896 MB restantes (memoria baja) se asignan estáticamente en el momento del arranque?
Poojan

1
@NlightNFotis Digo que casi 15 personas creen que lo que sea que hayas dicho, es correcto (o eso nos haces pensar;))
Braiam

¿Pensé que el anillo x86 -1era para hipervisores? en.wikipedia.org/wiki/Protection_ring
Dori

1
Tenga en cuenta la diferencia entre la memoria virtual y la memoria física. La mayor parte de lo que pregunta es sobre memoria virtual. Esto se asigna a la memoria física, esto se complica a medida que la memoria física se acerca a los 3GB y se usa PAE. Luego se vuelve simple de nuevo cuando se usa un núcleo de 64 bits, en este caso las direcciones negativas están reservadas para el núcleo y las positivas para el espacio del usuario. Los procesos de 32 bits ahora pueden usar 4 GB de espacio virtual. Los procesos de 64 bits pueden usar mucho más, generalmente un valor de 48 bits (en este momento en x86-64).
ctrl-alt-delor

16

Los anillos de CPU son la distinción más clara

En modo protegido x86, la CPU está siempre en uno de los 4 anillos. El kernel de Linux solo usa 0 y 3:

  • 0 para el núcleo
  • 3 para usuarios

Esta es la definición más dura y rápida de kernel vs userland.

Por qué Linux no usa los anillos 1 y 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

¿Cómo se determina el anillo actual?

El anillo actual se selecciona mediante una combinación de:

  • tabla de descriptores globales: una tabla en memoria de entradas GDT, y cada entrada tiene un campo Privlque codifica el anillo.

    La instrucción LGDT establece la dirección en la tabla de descriptores actual.

    Ver también: http://wiki.osdev.org/Global_Descriptor_Table

  • el segmento registra CS, DS, etc., que apuntan al índice de una entrada en el GDT.

    Por ejemplo, CS = 0significa que la primera entrada del GDT está actualmente activa para el código de ejecución.

¿Qué puede hacer cada anillo?

El chip de la CPU está construido físicamente para que:

  • el anillo 0 puede hacer cualquier cosa

  • ring 3 no puede ejecutar varias instrucciones y escribir en varios registros, especialmente:

    • no puede cambiar su propio anillo! De lo contrario, podría configurarse para sonar 0 y los anillos serían inútiles.

      En otras palabras, no se puede modificar el descriptor de segmento actual , que determina el anillo actual.

    • no puede modificar las tablas de página: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      En otras palabras, no puede modificar el registro CR3, y la paginación misma evita la modificación de las tablas de página.

      Esto evita que un proceso vea la memoria de otros procesos por razones de seguridad / facilidad de programación.

    • no puede registrar manejadores de interrupciones. Estos se configuran escribiendo en ubicaciones de memoria, lo que también se evita mediante paginación.

      Los controladores se ejecutan en el anillo 0 y romperían el modelo de seguridad.

      En otras palabras, no puede usar las instrucciones LGDT y LIDT.

    • no puede hacer instrucciones IO como iny out, y por lo tanto tener accesos arbitrarios de hardware.

      De lo contrario, por ejemplo, los permisos de archivo serían inútiles si algún programa pudiera leer directamente desde el disco.

      Más precisamente, gracias a Michael Petch : en realidad es posible que el sistema operativo permita instrucciones de E / S en el anillo 3, esto en realidad está controlado por el segmento de estado de la tarea .

      Lo que no es posible es que el anillo 3 se dé permiso para hacerlo si no lo tenía en primer lugar.

      Linux siempre lo rechaza. Ver también: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

¿Cómo hacen la transición los programas y sistemas operativos entre anillos?

  • cuando se enciende la CPU, comienza a ejecutar el programa inicial en el anillo 0 (bueno, pero es una buena aproximación). Puede pensar que este programa inicial es el núcleo (pero normalmente es un gestor de arranque que luego llama al núcleo aún en el anillo 0).

  • cuando un proceso de userland quiere que el kernel haga algo por él, como escribir en un archivo, utiliza una instrucción que genera una interrupción como int 0x80osyscall para señalar el kernel. x86-64 Ejemplo de Linux syscall hello world:

    .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
    

    compilar y ejecutar:

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

    GitHub aguas arriba .

    Cuando esto sucede, la CPU llama a un controlador de devolución de llamada de interrupción que el núcleo registró en el momento del arranque. Aquí hay un ejemplo concreto de metal desnudo que registra un controlador y lo usa .

    Este controlador se ejecuta en el anillo 0, que decide si el kernel permitirá esta acción, realiza la acción y reinicia el programa de usuario en el anillo 3. x86_64

  • cuando execse usa la llamada al sistema (o cuando se inicia/init el núcleo ), el núcleo prepara los registros y la memoria del nuevo proceso de usuario, luego salta al punto de entrada y cambia la CPU para que suene 3

  • Si el programa intenta hacer algo malo como escribir en un registro prohibido o en una dirección de memoria (debido a la paginación), la CPU también llama a algún controlador de devolución de llamada del núcleo en el anillo 0.

    Pero dado que el país de usuarios era travieso, el núcleo podría matar el proceso esta vez, o darle una advertencia con una señal.

  • Cuando se inicia el kernel, configura un reloj de hardware con cierta frecuencia fija, que genera interrupciones periódicamente.

    Este reloj de hardware genera interrupciones que ejecutan el anillo 0, y le permite programar qué procesos de usuario y tierra se activarán.

    De esta manera, la programación puede ocurrir incluso si los procesos no realizan ninguna llamada al sistema.

¿Cuál es el punto de tener múltiples anillos?

Hay dos ventajas principales de separar el kernel y el userland:

  • es más fácil hacer programas, ya que está más seguro de que uno no interferirá con el otro. Por ejemplo, un proceso de usuario no tiene que preocuparse por sobrescribir la memoria de otro programa debido a la paginación, ni por poner el hardware en un estado no válido para otro proceso.
  • Es más seguro. Por ejemplo, los permisos de archivos y la separación de la memoria podrían evitar que una aplicación de piratería lea sus datos bancarios. Esto supone, por supuesto, que confías en el núcleo.

¿Cómo jugar con eso?

He creado una configuración de metal desnudo que debería ser una buena forma de manipular los anillos directamente: https://github.com/cirosantilli/x86-bare-metal-examples

Desafortunadamente, no tuve la paciencia para hacer un ejemplo de userland, pero fui tan lejos como la configuración de paginación, por lo que userland debería ser factible. Me encantaría ver una solicitud de extracción.

Alternativamente, los módulos del kernel de Linux se ejecutan en el anillo 0, por lo que puede usarlos para probar operaciones privilegiadas, por ejemplo, lea los registros de control: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-from-a-program-getting-segmenta / 7419306 # 7419306

Aquí hay una configuración conveniente de QEMU + Buildroot para probarlo sin matar a su host.

La desventaja de los módulos del kernel es que otros kthreads se están ejecutando y podrían interferir con sus experimentos. Pero, en teoría, puede hacerse cargo de todos los controladores de interrupciones con su módulo de kernel y poseer el sistema, ese sería un proyecto interesante en realidad.

Anillos negativos

Si bien los anillos negativos no se mencionan realmente en el manual de Intel, en realidad hay modos de CPU que tienen capacidades adicionales que el anillo 0 en sí mismo, por lo que son una buena opción para el nombre de "anillo negativo".

Un ejemplo es el modo de hipervisor utilizado en la virtualización.

Para más detalles, consulte: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1

BRAZO

En ARM, los anillos se denominan niveles de excepción, pero las ideas principales siguen siendo las mismas.

Existen 4 niveles de excepción en ARMv8, comúnmente utilizados como:

  • EL0: tierra de usuario

  • EL1: kernel ("supervisor" en terminología ARM).

    Se ingresó con la svcinstrucción (SuperVisor Call), anteriormente conocida como swi ensamblaje unificado anterior , que es la instrucción utilizada para realizar llamadas al sistema Linux. Hola ejemplo de ARMv8 mundial:

    .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 aguas arriba .

    Pruébelo con QEMU en 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
    

    Aquí hay un ejemplo concreto de metal desnudo que registra un controlador SVC y realiza una llamada SVC .

  • EL2: hipervisores , por ejemplo Xen .

    Entró con la hvcinstrucción (llamada HyperVisor).

    Un hipervisor es para un sistema operativo, lo que un sistema operativo es para el usuario.

    Por ejemplo, Xen le permite ejecutar múltiples sistemas operativos, como Linux o Windows, en el mismo sistema al mismo tiempo, y aísla los sistemas operativos entre sí por seguridad y facilidad de depuración, al igual que Linux lo hace para los programas de usuario.

    Los hipervisores son una parte clave de la infraestructura de la nube actual: permiten que varios servidores se ejecuten en un solo hardware, manteniendo el uso del hardware siempre cerca del 100% y ahorrando mucho dinero.

    AWS, por ejemplo, usó Xen hasta 2017, cuando su traslado a KVM fue noticia .

  • EL3: otro nivel más. TODO ejemplo.

    Entró con la smcinstrucción (llamada en modo seguro)

El modelo de referencia de arquitectura ARMv8 DDI 0487C.a - Capítulo D1 - El modelo del programador de nivel de sistema AArch64 - La figura D1-1 ilustra esto maravillosamente:

ingrese la descripción de la imagen aquí

Observe cómo ARM, tal vez debido al beneficio de la retrospectiva, tiene una mejor convención de nomenclatura para los niveles de privilegio que x86, sin la necesidad de niveles negativos: 0 es el más bajo y 3 el más alto. Los niveles más altos tienden a crearse con más frecuencia que los más bajos.

El EL actual se puede consultar con las MRSinstrucciones: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

ARM no requiere que todos los niveles de excepción estén presentes para permitir implementaciones que no necesitan la función para guardar el área del chip. ARMv8 "Niveles de excepción" dice:

Una implementación podría no incluir todos los niveles de excepción. Todas las implementaciones deben incluir EL0 y EL1. EL2 y EL3 son opcionales.

QEMU, por ejemplo, está predeterminado en EL1, pero EL2 y EL3 se pueden habilitar con opciones de línea de comando: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up

Fragmentos de código probados en Ubuntu 18.10.


3

Si es el primero, ¿significa que el programa de usuario normal no puede tener más de 3 GB de memoria (si la división es 3 GB + 1 GB)?

Sí, este es el caso en un sistema Linux normal. Había un conjunto de parches "4G / 4G" flotando en un punto que hacía que el usuario y los espacios de direcciones del kernel fueran completamente independientes (a un costo de rendimiento porque dificultaba el acceso del kernel a la memoria del usuario), pero no creo alguna vez se fusionaron río arriba y el interés disminuyó con el auge de x86-64

Además, en ese caso, ¿cómo puede el kernel usar High Memory, porque a qué dirección de memoria virtual se asignarán las páginas de la memoria alta, ya que 1GB de espacio en el kernel se asignará lógicamente?

La forma en que Linux solía funcionar (y todavía lo hace en sistemas donde la memoria es pequeña en comparación con el espacio de direcciones) fue que toda la memoria física se asignó permanentemente en la parte del núcleo del espacio de direcciones. Esto permitió que el kernel tuviera acceso a toda la memoria física sin reasignación, pero claramente no escala a máquinas de 32 bits con mucha memoria física.

Así nació el concepto de baja y alta memoria. La memoria "baja" se asigna permanentemente en el espacio de direcciones del núcleo. memoria "alta" no lo es.

Cuando el procesador ejecuta una llamada al sistema, se ejecuta en modo kernel pero aún en el contexto del proceso actual. Por lo tanto, puede acceder directamente tanto al espacio de direcciones del núcleo como al espacio de direcciones del usuario del proceso actual (suponiendo que no esté utilizando los parches 4G / 4G mencionados anteriormente). Esto significa que no es problema asignar memoria "alta" a un proceso de usuario.

Usar memoria "alta" para propósitos de kernel es más un problema. Para acceder a una memoria alta que no está asignada al proceso actual, debe asignarse temporalmente al espacio de direcciones del núcleo. Eso significa código extra y una penalización de rendimiento.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.