В чем разница между пространством пользователя и пространством ядра?


73

Используется ли пространство ядра, когда ядро ​​выполняется от имени пользовательской программы, т.е. системного вызова? Или это адресное пространство для всех потоков ядра (например, планировщика)?

Если это первый, чем это означает, что обычная пользовательская программа не может иметь более 3 ГБ памяти (если деление составляет 3 ГБ + 1 ГБ)? Кроме того, в таком случае, как ядро ​​может использовать High Memory, потому что на какой адрес виртуальной памяти будут отображаться страницы из верхней памяти, поскольку логически будет отображаться 1 ГБ пространства ядра?

Ответы:


93

Используется ли пространство ядра, когда ядро ​​выполняется от имени пользовательской программы, т.е. системного вызова? Или это адресное пространство для всех потоков ядра (например, планировщика)?

Да и да.

Прежде чем мы пойдем дальше, мы должны заявить о памяти.

Получение памяти разделено на две отдельные области:

  • Пространство пользователя , которое представляет собой набор мест, в которых выполняются обычные пользовательские процессы (т. Е. Все, кроме ядра). Роль ядра состоит в том, чтобы управлять приложениями, работающими в этом пространстве, не связываясь друг с другом и с машиной.
  • Пространство ядра , в котором хранится код ядра и выполняется в нем.

Процессы, выполняющиеся в пространстве пользователя, имеют доступ только к ограниченной части памяти, тогда как ядро ​​имеет доступ ко всей памяти. Процессы, выполняющиеся в пространстве пользователя, также не имеют доступа к пространству ядра. Процессы пользовательского пространства могут получить доступ только к небольшой части ядра через интерфейс, предоставляемый ядром - системные вызовы . Если процесс выполняет системный вызов, программному прерыванию отправляется ядро, которое затем отправляет соответствующий обработчик прерываний и продолжает свою работу после его завершения.

Код пространства ядра обладает свойством запускаться в «режиме ядра», который (на вашем обычном настольном компьютере) является тем, что вы называете кодом, который выполняется под кольцом 0 . Как правило, в архитектуре x86 существует 4 кольца защиты . Звонок 0 (режим ядра), Звонок 1 (может использоваться гипервизорами или драйверами виртуальных машин), Звонок 2 (может использоваться драйверами, хотя я в этом не уверен). Кольцо 3 - это то, что типичные приложения работают под. Это наименее привилегированное кольцо, и приложения, работающие на нем, имеют доступ к подмножеству инструкций процессора. Кольцо 0 (пространство ядра) является наиболее привилегированным кольцом и имеет доступ ко всем инструкциям машины. Например, «простое» приложение (например, браузер) не может использовать инструкции по сборке x86lgdtзагрузить таблицу глобальных дескрипторов или hltостановить процессор.

Если это первый, чем это означает, что обычная пользовательская программа не может иметь более 3 ГБ памяти (если деление составляет 3 ГБ + 1 ГБ)? Кроме того, в таком случае, как ядро ​​может использовать High Memory, потому что на какой адрес виртуальной памяти будут отображаться страницы из верхней памяти, поскольку логически будет отображаться 1 ГБ пространства ядра?

Для ответа на этот вопрос, пожалуйста, обратитесь к отличному ответу от wag здесь


4
Не стесняйтесь сказать мне, если я где-то допустил ошибку. Я новичок в программировании ядра, и я привел здесь то, что я узнал до сих пор, а также некоторую другую информацию, которую я нашел в Интернете. Это означает, что могут быть недостатки в моем понимании концепций, которые могут быть продемонстрированы в тексте.
NlightNFotis

Спасибо! Я думаю, теперь я понимаю это лучше. Просто чтобы убедиться, что я правильно понял, у меня есть еще один вопрос. Опять же, учитывая, что первые 3 ГБ используются для пространства пользователя, а 128 МБ пространства ядра используется для высокой памяти, оставшиеся 896 МБ (низкая память) статически отображаются во время загрузки?
Пуджан

1
@NlightNFotis Я говорю, что почти 15 человек считают, что все, что вы сказали, правильно (или так, вы заставляете нас думать;))
Брайам

Я думал, что x86 кольцо -1было для гипервизоров? ru.wikipedia.org/wiki/Protection_ring
Дори,

1
Обратите внимание на разницу между виртуальной памятью и физической памятью. Большая часть того, о чем вы спрашиваете, касается виртуальной памяти. Это сопоставляется с физической памятью, это усложняется, когда физическая память приближается к 3 ГБ, и используется PAE. Затем, когда используется 64-битное ядро, все становится просто, в этом случае отрицательные адреса зарезервированы для ядра, а положительные - для пространства пользователя. 32-битные процессы теперь могут использовать 4 ГБ виртуального пространства. 64-битные процессы могут использовать намного больше - как правило, на 48 бит (в настоящее время на x86-64).
ctrl-alt-delor

16

Кольца процессора - самое четкое различие

В защищенном режиме x86 процессор всегда находится в одном из 4 звонков. Ядро Linux использует только 0 и 3:

  • 0 для ядра
  • 3 для пользователей

Это наиболее сложное и быстрое определение ядра и пользовательского пространства.

Почему Linux не использует кольца 1 и 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

Как определяется текущее кольцо?

Текущее кольцо выбирается комбинацией:

  • таблица глобальных дескрипторов: таблица в памяти записей GDT, и каждая запись имеет поле, Privlкоторое кодирует кольцо.

    Инструкция LGDT устанавливает адрес для текущей таблицы дескрипторов.

    Смотрите также: http://wiki.osdev.org/Global_Descriptor_Table

  • Сегмент регистрирует CS, DS и т. д., которые указывают на индекс записи в GDT.

    Например, CS = 0означает, что первая запись GDT в данный момент активна для исполняемого кода.

Что может сделать каждое кольцо?

Чип процессора физически построен так, что:

  • кольцо 0 может сделать что угодно

  • кольцо 3 не может выполнить несколько инструкций и записать в несколько регистров, в частности:

    • не может изменить свое собственное кольцо! В противном случае он мог бы установить себе кольцо 0, и кольца были бы бесполезны.

      Другими словами, нельзя изменить текущий дескриптор сегмента , который определяет текущее кольцо.

    • невозможно изменить таблицы страниц: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      Другими словами, нельзя изменить регистр CR3, а само разбиение на страницы предотвращает изменение таблиц страниц.

      Это препятствует тому, чтобы один процесс видел память других процессов из соображений безопасности / простоты программирования.

    • не может зарегистрировать обработчики прерываний. Они настраиваются путем записи в ячейки памяти, что также предотвращается подкачкой.

      Обработчики работают в кольце 0 и нарушают модель безопасности.

      Другими словами, нельзя использовать инструкции LGDT и LIDT.

    • не может выполнять инструкции ввода-вывода, такие как inи out, и, следовательно, иметь произвольный доступ к оборудованию.

      В противном случае, например, права доступа к файлам будут бесполезны, если какая-либо программа сможет напрямую читать с диска.

      Точнее, благодаря Майклу Петчу : операционная система может разрешить инструкции ввода-вывода на 3-м кольце, это фактически контролируется сегментом состояния задачи .

      То, что невозможно, это для кольца 3, чтобы дать себе разрешение сделать это, если у него не было его в первую очередь.

      Linux всегда запрещает это. Смотрите также: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

Как программы и операционные системы переходят между кольцами?

  • когда процессор включен, он запускает исходную программу в кольце 0 (что-то вроде, но это хорошее приближение). Вы можете считать эту исходную программу ядром (но обычно это загрузчик, который затем вызывает ядро ​​все еще в кольце 0).

  • когда пользовательский процесс хочет, чтобы ядро ​​сделало что-то для него, например, запись в файл, он использует инструкцию, которая генерирует прерывание, например, int 0x80илиsyscall для сигнализации ядру. x86-64 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
    

    скомпилируйте и запустите:

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

    GitHub вверх по течению .

    Когда это происходит, ЦП вызывает обработчик обратного вызова прерывания, который ядро ​​зарегистрировало во время загрузки. Вот конкретный пример с использованием неизолированного металла, который регистрирует обработчик и использует его .

    Этот обработчик работает в кольце 0, который решает, разрешит ли ядро ​​это действие, выполняет действие и перезапускает программу userland в кольце 3. x86_64

  • когда используется execсистемный вызов (или когда запускается/init ядро ), ядро подготавливает регистры и память нового пользовательского процесса, затем переходит к точке входа и переключает ЦП на вызов 3

  • Если программа пытается сделать что-то непослушное, например запись в запрещенный регистр или адрес памяти (из-за подкачки), ЦП также вызывает некоторый обработчик обратного вызова ядра в кольце 0

    Но поскольку пользовательская область была непослушной, ядро ​​на этот раз может убить процесс или выдать ему предупреждение с сигналом.

  • Когда ядро ​​загружается, оно устанавливает аппаратные часы с некоторой фиксированной частотой, которая периодически генерирует прерывания.

    Эти аппаратные часы генерируют прерывания, которые запускают кольцо 0, и позволяют ему планировать, какие процессы пользователя активизируются.

    Таким образом, планирование может происходить, даже если процессы не выполняют никаких системных вызовов.

Какой смысл иметь несколько колец?

Существует два основных преимущества разделения ядра и пользовательского пространства:

  • проще создавать программы, так как вы уверены, что одно не будет мешать другому. Например, один пользовательский процесс не должен беспокоиться о перезаписи памяти другой программы из-за подкачки страниц или о переводе оборудования в недопустимое состояние для другого процесса.
  • это более безопасно. Например, права доступа к файлам и разделение памяти могут помешать хакерскому приложению читать ваши банковские данные. Это предполагает, конечно, что вы доверяете ядру.

Как поиграться с этим?

Я создал «голую железную» установку, которая должна быть хорошим способом для непосредственного управления кольцами: https://github.com/cirosantilli/x86-bare-metal-examples

К сожалению, у меня не хватило терпения сделать пример пользовательского пространства, но я дошел до настройки пейджинга, поэтому пользовательское пространство должно быть выполнимым. Я хотел бы увидеть запрос на получение.

Кроме того, модули ядра Linux работают в кольце 0, поэтому вы можете использовать их для проверки привилегированных операций, например, прочитайте регистры управления: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-CR2-Cr3-с-а-программа пробивной-сегмент А / 7419306 # 7419306

Вот удобная настройка QEMU + Buildroot, чтобы попробовать ее, не убивая своего хоста.

Недостатком модулей ядра является то, что другие kthreads работают и могут мешать вашим экспериментам. Но в теории вы можете взять на себя все обработчики прерываний с вашим модулем ядра и владеть системой, это будет действительно интересный проект.

Отрицательные кольца

Хотя отрицательные кольца фактически не упоминаются в руководстве Intel, на самом деле существуют режимы ЦП, которые имеют более широкие возможности, чем само кольцо 0, и, таким образом, хорошо подходят для названия «отрицательного кольца».

Одним из примеров является режим гипервизора, используемый в виртуализации.

Для получения дополнительной информации см .: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1.

РУКА

В ARM кольца называются уровнями исключения, но основные идеи остаются прежними.

В ARMv8 существует 4 уровня исключений, которые обычно используются как:

  • EL0: пользовательская область

  • EL1: ядро ​​(«супервизор» в терминологии ARM).

    Введено с svcинструкцией (SuperVisor Call), ранее известной как swi ранее унифицированная сборка , которая является инструкцией, используемой для выполнения системных вызовов Linux. Привет мир 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 вверх по течению .

    Проверьте это с QEMU на 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
    

    Ниже приведен конкретный пример с использованием неизолированного металла, который регистрирует обработчик SVC и выполняет вызов SVC .

  • EL2: гипервизоры , например Xen .

    hvcВведено с инструкцией (HyperVisor Call).

    Гипервизор для ОС, то же самое, что ОС для пользователя.

    Например, Xen позволяет вам запускать несколько ОС, таких как Linux или Windows, в одной и той же системе одновременно, и он изолирует ОС друг от друга для обеспечения безопасности и простоты отладки, как это делает Linux для пользовательских программ.

    Гипервизоры являются ключевой частью современной облачной инфраструктуры: они позволяют нескольким серверам работать на одном оборудовании, поддерживая аппаратное использование всегда близким к 100% и экономя много денег.

    Например, AWS использовала Xen до 2017 года, когда новость о его переходе на KVM .

  • EL3: еще один уровень. Пример TODO.

    Введено с smcинструкцией (вызов в безопасном режиме)

ARMv8 Architecture Reference Model DDI 0487C.a - Глава D1 - Модель The AArch64 System Level Программиста - Рисунок D1-1 иллюстрирует это красиво:

введите описание изображения здесь

Обратите внимание, что ARM, возможно, благодаря ретроспективе, имеет лучшее соглашение об именах для уровней привилегий, чем x86, без необходимости использования отрицательных уровней: 0 является самым низким, а 3 самым высоким. Более высокие уровни, как правило, создаются чаще, чем более низкие.

Текущий EL может быть запрошен с помощью MRSинструкции: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

ARM не требует наличия всех уровней исключений, чтобы обеспечить реализации, которым не требуется эта функция для сохранения площади микросхемы. ARMv8 «Уровни исключений» говорит:

Реализация может не включать все уровни исключений. Все реализации должны включать EL0 и EL1. EL2 и EL3 являются необязательными.

QEMU, например, по умолчанию EL1, но EL2 и EL3 можно включить с помощью параметров командной строки: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulated-a53-power-up

Фрагменты кода, протестированные на Ubuntu 18.10.


3

Если это первый, чем это означает, что обычная пользовательская программа не может иметь более 3 ГБ памяти (если деление составляет 3 ГБ + 1 ГБ)?

Да, это так в обычной системе Linux. Существовал набор патчей "4G / 4G", плавающих в одной точке, которые делали адресное пространство пользователя и ядра полностью независимым (с затратами на производительность, потому что это затрудняло доступ ядра к пользовательской памяти), но я не думаю, что они когда-либо были объединены вверх по течению, и интерес уменьшился с повышением x86-64

Кроме того, в таком случае, как ядро ​​может использовать High Memory, потому что на какой адрес виртуальной памяти будут отображаться страницы из верхней памяти, поскольку логически будет отображаться 1 ГБ пространства ядра?

Линукс обычно работал (и до сих пор работает в системах, где память мала по сравнению с адресным пространством), состоял в том, что вся физическая память постоянно отображалась в части ядра адресного пространства. Это позволило ядру получить доступ ко всей физической памяти без переназначения, но, очевидно, оно не масштабируется до 32-битных машин с большим количеством физической памяти.

Так родилась концепция низкой и высокой памяти. «Низкая» память постоянно отображается в адресное пространство ядра. «высокой» памяти нет.

Когда процессор выполняет системный вызов, он работает в режиме ядра, но все еще в контексте текущего процесса. Таким образом, он может напрямую обращаться как к адресному пространству ядра, так и к адресному пространству пользователя текущего процесса (при условии, что вы не используете вышеупомянутые патчи 4G / 4G). Это означает, что не проблема для того, чтобы «высокая» память была выделена процессу пользователя.

Использование «высокой» памяти для целей ядра - большая проблема. Чтобы получить доступ к высокому объему памяти, который не сопоставлен с текущим процессом, он должен быть временно сопоставлен с адресным пространством ядра. Это означает дополнительный код и снижение производительности.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.