Jaka jest różnica między przestrzenią użytkownika a przestrzenią jądra?

72

Czy przestrzeń jądra jest używana, gdy jądro wykonuje się w imieniu programu użytkownika, tj. Wywołania systemowego? Czy jest to przestrzeń adresowa dla wszystkich wątków jądra (na przykład program planujący)?

Jeśli jest to pierwszy, czy to nie oznacza, że ​​normalny program użytkownika nie może mieć więcej niż 3 GB pamięci (jeśli podział wynosi 3 GB + 1 GB)? Również w takim przypadku, w jaki sposób jądro może korzystać z wysokiej pamięci, ponieważ na jaki adres pamięci wirtualnej zostaną zamapowane strony z dużej pamięci, ponieważ 1 GB miejsca w jądrze zostanie zmapowane logicznie?

Poojan
źródło

Odpowiedzi:

93

Czy przestrzeń jądra jest używana, gdy jądro wykonuje się w imieniu programu użytkownika, tj. Wywołania systemowego? Czy jest to przestrzeń adresowa dla wszystkich wątków jądra (na przykład program planujący)?

Tak i tak.

Zanim przejdziemy dalej, powinniśmy powiedzieć o pamięci.

Get pamięci jest podzielony na dwa odrębne obszary:

  • Przestrzeń użytkownika , która jest zbiorem lokalizacji, w których działają normalne procesy użytkownika (tj. Wszystko inne niż jądro). Rola jądra polega na zarządzaniu aplikacjami działającymi w tym obszarze przed niechlujnością między sobą i maszyną.
  • Przestrzeń jądra , czyli miejsce , w którym przechowywany jest kod jądra, i wykonuje się pod nim.

Procesy działające w przestrzeni użytkownika mają dostęp tylko do ograniczonej części pamięci, podczas gdy jądro ma dostęp do całej pamięci. Procesy działające w przestrzeni użytkownika również nie mają dostępu do przestrzeni jądra. Procesy przestrzeni użytkownika mogą uzyskać dostęp tylko do niewielkiej części jądra za pośrednictwem interfejsu odsłoniętego przez jądro - wywołania systemowe . Jeśli proces wykonuje wywołanie systemowe, do jądra wysyłane jest przerwanie programowe, które następnie wysyła odpowiednią procedurę obsługi przerwań i kontynuuje pracę po zakończeniu procedury obsługi.

Kod przestrzeni jądra ma właściwość do uruchomienia w „trybie jądra”, który (na typowym komputerze stacjonarnym -x86-) jest tym, co nazywamy kodem, który wykonuje się pod pierścieniem 0 . Zazwyczaj w architekturze x86 istnieją 4 pierścienie ochronne . Pierścień 0 (tryb jądra), Pierścień 1 (może być używany przez hiperwizory lub sterowniki maszyny wirtualnej), Pierścień 2 (może być używany przez sterowniki, ale nie jestem tego pewien). Pierścień 3 to typowe aplikacje. Jest to najmniej uprzywilejowany pierścień, a działające na nim aplikacje mają dostęp do podzbioru instrukcji procesora. Pierścień 0 (przestrzeń jądra) jest najbardziej uprzywilejowanym pierścieniem i ma dostęp do wszystkich instrukcji maszyny. Na przykład „prosta” aplikacja (np. Przeglądarka) nie może korzystać z instrukcji montażu x86lgdtaby załadować globalną tabelę deskryptorów lub hltzatrzymać procesor.

Jeśli jest to pierwszy, czy to nie oznacza, że ​​normalny program użytkownika nie może mieć więcej niż 3 GB pamięci (jeśli podział wynosi 3 GB + 1 GB)? Również w takim przypadku, w jaki sposób jądro może korzystać z wysokiej pamięci, ponieważ na jaki adres pamięci wirtualnej zostaną zamapowane strony z dużej pamięci, ponieważ 1 GB miejsca w jądrze zostanie zmapowane logicznie?

Dla odpowiedzi na ten temat znajduje się w doskonałej odpowiedzi przez machać tutaj

NlightNFotis
źródło
4
Nie wahaj się i powiedz mi, czy gdzieś popełniłem błąd. Jestem nowy w programowaniu jądra i rzuciłem tutaj to, czego się nauczyłem do tej pory, wraz z kilkoma innymi informacjami, które znalazłem w Internecie. Co oznacza, że ​​mogą występować braki w moim rozumieniu pojęć, które mogą zostać wykazane w tekście.
NlightNFotis
Dzięki! Myślę, że teraz rozumiem to lepiej. Aby upewnić się, że poprawnie go otrzymam, mam jeszcze jedno pytanie. Ponownie, biorąc pod uwagę, że pierwsze 3 GB jest wykorzystywane na przestrzeń użytkownika, a 128 MB miejsca na jądro jest wykorzystywane na wysoką pamięć, czy pozostałe 896 MB (mała pamięć) jest mapowane statycznie podczas uruchamiania?
Poojan,
1
@NlightNFotis Mówię, że prawie 15 osób uważa, że ​​cokolwiek powiedziałeś, jest poprawne (lub tak
każesz
Myślałem, że pierścień x86 -1jest przeznaczony dla hiperwizorów? en.wikipedia.org/wiki/Protection_ring
Dori
1
Zwróć uwagę na różnicę między pamięcią wirtualną a pamięcią fizyczną. Większość pytań dotyczy pamięci wirtualnej. Jest to mapowane na pamięć fizyczną, co komplikuje się, gdy pamięć fizyczna zbliża się do 3 GB, i używana jest funkcja PAE. Potem znów staje się to proste, gdy używane jest jądro 64-bitowe, w tym przypadku adresy ujemne są zarezerwowane dla jądra, a adresy dodatnie dla przestrzeni użytkownika. Procesy 32-bitowe mogą teraz wykorzystywać 4 GB przestrzeni wirtualnej. Procesy 64-bitowe mogą zużywać znacznie więcej - zazwyczaj 48 bitów (obecnie na x86-64).
ctrl-alt-delor
16

Pierścienie procesora są najbardziej wyraźnym rozróżnieniem

W trybie chronionym x86 procesor jest zawsze w jednym z 4 pierścieni. Jądro Linux używa tylko 0 i 3:

  • 0 dla jądra
  • 3 dla użytkowników

Jest to najtrudniejsza i najszybsza definicja jądra w porównaniu do przestrzeni użytkownika.

Dlaczego Linux nie używa pierścieni 1 i 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

Jak określa się bieżący pierścień?

Bieżący pierścień jest wybierany przez kombinację:

  • globalna tabela deskryptorów: tablica wpisów GDT w pamięci, a każda pozycja ma pole, Privlktóre koduje pierścień.

    Instrukcja LGDT ustawia adres do bieżącej tabeli deskryptorów.

    Zobacz także: http://wiki.osdev.org/Global_Descriptor_Table

  • segment rejestruje CS, DS itp., które wskazują na indeks wpisu w GDT.

    Na przykład CS = 0oznacza, że ​​pierwszy wpis GDT jest obecnie aktywny dla kodu wykonawczego.

Co może zrobić każdy pierścień?

Układ procesora jest fizycznie zbudowany, dzięki czemu:

  • pierścień 0 może zrobić wszystko

  • pierścień 3 nie może uruchomić kilku instrukcji i zapisać w kilku rejestrach, w szczególności:

    • nie może zmienić własnego pierścienia! W przeciwnym razie mógłby ustawić się na pierścień 0 i pierścienie byłyby bezużyteczne.

      Innymi słowy, nie można zmodyfikować bieżącego deskryptora segmentu , który określa bieżący pierścień.

    • nie może modyfikować tabel stron: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      Innymi słowy, nie można zmodyfikować rejestru CR3, a samo stronicowanie zapobiega modyfikacji tabel stron.

      Uniemożliwia to procesowi odczytanie pamięci innych procesów ze względów bezpieczeństwa / łatwości programowania.

    • nie można zarejestrować programów obsługi przerwań. Są one konfigurowane przez zapis w lokalizacjach pamięci, co również zapobiega stronicowaniu.

      Programy obsługi działają w pierścieniu 0 i psują model bezpieczeństwa.

      Innymi słowy, nie można użyć instrukcji LGDT i LIDT.

    • nie może wykonywać instrukcji IO takich jak ini out, a zatem ma dowolny dostęp do sprzętu.

      W przeciwnym razie, na przykład, uprawnienia do plików byłyby bezużyteczne, gdyby jakikolwiek program mógł bezpośrednio czytać z dysku.

      Dokładniej dzięki Michaelowi Petchowi : w rzeczywistości system operacyjny może zezwolić na instrukcje IO na pierścieniu 3, jest to faktycznie kontrolowane przez segment stanu zadania .

      Niemożliwe jest, aby pierścień 3 wyraził na to zgodę, jeśli go nie miał.

      Linux zawsze go nie zezwala. Zobacz także: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

W jaki sposób programy i systemy operacyjne przechodzą między pierścieniami?

  • gdy procesor jest włączony, zaczyna uruchamiać program początkowy w pierścieniu 0 (no cóż, ale jest to dobre przybliżenie). Możesz myśleć, że ten program początkowy jest jądrem (ale zwykle jest to program ładujący, który następnie wywołuje jądro wciąż w pierścieniu 0).

  • gdy proces użytkownika chce, aby jądro zrobiło coś takiego, jak zapis do pliku, używa instrukcji, która generuje przerwanie, takie jak int 0x80lubsyscall sygnalizowanie jądra. x86-64 Linux syscall przykład 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
    

    skompiluj i uruchom:

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

    GitHub w górę .

    Kiedy tak się dzieje, CPU wywołuje procedurę obsługi wywołania zwrotnego przerwania, którą jądro zarejestrowało w czasie uruchamiania. Oto konkretny przykład z nagiego metalu, który rejestruje moduł obsługi i używa go .

    Ten moduł obsługi działa w pierścieniu 0, który decyduje, czy jądro zezwoli na to działanie, wykonaj działanie i zrestartuj program użytkownika w pierścieniu 3. x86_64

  • kiedy execużywane jest wywołanie systemowe (lub gdy jądro się uruchomi/init ), jądro przygotowuje rejestry i pamięć nowego procesu użytkownika, następnie przeskakuje do punktu wejścia i przełącza CPU na pierścień 3

  • Jeśli program próbuje zrobić coś niegrzecznego, np. Zapis do zabronionego rejestru lub adresu pamięci (z powodu stronicowania), CPU wywołuje również funkcję obsługi wywołania zwrotnego jądra w pierścieniu 0.

    Ale ponieważ środowisko użytkownika było niegrzeczne, jądro może tym razem zabić proces lub ostrzec go sygnałem.

  • Kiedy jądro uruchamia się, ustawia zegar sprzętowy z pewną stałą częstotliwością, która okresowo generuje przerwania.

    Ten zegar sprzętowy generuje przerwania, które uruchamiają pierścień 0, i pozwala mu zaplanować, które procesy użytkownika będą się budzić.

    W ten sposób planowanie może się zdarzyć, nawet jeśli procesy nie wykonują żadnych wywołań systemowych.

Jaki jest sens posiadania wielu pierścieni?

Istnieją dwie główne zalety oddzielenia jądra i przestrzeni użytkownika:

  • łatwiej jest tworzyć programy, ponieważ masz większą pewność, że jeden nie będzie kolidował z drugim. Np. Jeden proces użytkownika nie musi się martwić o nadpisanie pamięci innego programu z powodu stronicowania, ani o umieszczenie sprzętu w niewłaściwym stanie dla innego procesu.
  • jest bardziej bezpieczny. Np. Uprawnienia do plików i separacja pamięci mogą uniemożliwić aplikacji hakerskiej odczyt danych bankowych. To oczywiście oznacza, że ​​ufasz jądru.

Jak się z tym bawić?

Stworzyłem gołe metalowe ustawienie, które powinno być dobrym sposobem na bezpośrednie manipulowanie pierścieniami: https://github.com/cirosantilli/x86-bare-metal-examples

Niestety nie miałem cierpliwości, by podać przykład przestrzeni użytkownika, ale posunąłem się do konfiguracji stronicowania, więc droga użytkownika powinna być możliwa. Chciałbym zobaczyć prośbę o pociągnięcie.

Alternatywnie, moduły jądra Linux działają w pierścieniu 0, więc można ich użyć do wypróbowania operacji uprzywilejowanych, np. Odczytać rejestry kontrolne: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-from-a-program-getting-segmenta / 7419306 # 7419306

Oto wygodna konfiguracja QEMU + Buildroot, aby wypróbować ją bez zabijania hosta.

Minusem modułów jądra jest to, że działają inne kthreads i mogą zakłócać twoje eksperymenty. Ale teoretycznie możesz przejąć wszystkie moduły obsługi przerwań za pomocą modułu jądra i posiadać system, co w rzeczywistości byłoby ciekawym projektem.

Pierścienie ujemne

Chociaż w podręczniku Intela nie ma tak naprawdę odniesień do pierścieni ujemnych, w rzeczywistości istnieją tryby procesora, które mają więcej możliwości niż sam pierścień 0, a zatem dobrze pasują do nazwy „pierścień ujemny”.

Jednym z przykładów jest tryb hiperwizora używany w wirtualizacji.

Aby uzyskać więcej informacji, zobacz: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1

RAMIĘ

W ARM pierścienie nazywane są zamiast tego Poziomami Wyjątków, ale główne idee pozostają takie same.

Istnieją 4 poziomy wyjątków w ARMv8, powszechnie używane jako:

  • EL0: obszar użytkownika

  • EL1: jądro („superwizor” w terminologii ARM).

    Wprowadzony z svcinstrukcją (SuperVisor Call), wcześniej znaną jako swi przed zunifikowanym asemblerem , która jest instrukcją używaną do wykonywania wywołań systemowych Linuksa. Przykład Witaj świecie 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 w górę .

    Przetestuj to za pomocą QEMU na 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
    

    Oto konkretny przykład typu baremetal, który rejestruje moduł obsługi SVC i wykonuje wywołanie SVC .

  • EL2: hiperwizory , na przykład Xen .

    Wprowadzony z hvcinstrukcją (połączenie HyperVisor).

    Hiperwizor dotyczy systemu operacyjnego, czym jest system operacyjny dla użytkownika.

    Na przykład Xen pozwala na jednoczesne uruchamianie wielu systemów operacyjnych, takich jak Linux lub Windows, i izoluje systemy operacyjne dla bezpieczeństwa i łatwości debugowania, podobnie jak Linux dla programów użytkownika.

    Hiperwizory są kluczową częścią dzisiejszej infrastruktury chmurowej: pozwalają wielu serwerom działać na jednym sprzęcie, dzięki czemu zużycie sprzętu jest zawsze bliskie 100% i pozwala zaoszczędzić dużo pieniędzy.

    Na przykład AWS używał Xen do 2017 roku, kiedy wiadomość o jego przejściu na KVM była aktualna .

  • EL3: kolejny poziom. Przykład DO ZROBIENIA.

    Wprowadzono z smcinstrukcją (połączenie w trybie bezpiecznym)

ARMv8 Architektura Model referencyjny DDI 0487C.a - Rozdział D1 - na poziomie systemu AArch64 programisty Model - Rysunek D1-1 ilustruje to pięknie:

wprowadź opis zdjęcia tutaj

Zwróć uwagę, że ARM, być może ze względu na korzyści z perspektywy czasu, ma lepszą konwencję nazewnictwa dla poziomów uprawnień niż x86, bez potrzeby stosowania poziomów ujemnych: 0 oznacza najniższą, a 3 najwyższą. Wyższe poziomy są zwykle tworzone częściej niż niższe.

Aktualny EL można uzyskać za pomocą MRSinstrukcji: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

ARM nie wymaga obecności wszystkich poziomów wyjątków, aby umożliwić implementacje, które nie potrzebują tej funkcji do oszczędzania obszaru chipa. ARMv8 „Poziomy wyjątków” mówi:

Implementacja może nie obejmować wszystkich poziomów wyjątków. Wszystkie implementacje muszą zawierać EL0 i EL1. EL2 i EL3 są opcjonalne.

Na przykład QEMU domyślnie ma wartość EL1, ale EL2 i EL3 można włączyć za pomocą opcji wiersza poleceń: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulations-a53-power-up

Fragmenty kodu przetestowane na systemie Ubuntu 18.10.

Ciro Santilli
źródło
3

Jeśli jest to pierwszy, czy to nie oznacza, że ​​normalny program użytkownika nie może mieć więcej niż 3 GB pamięci (jeśli podział wynosi 3 GB + 1 GB)?

Tak jest w przypadku normalnego systemu Linux. W jednym punkcie unosił się zestaw łatek „4G / 4G”, dzięki czemu przestrzenie adresowe użytkownika i jądra były całkowicie niezależne (kosztem wydajności, ponieważ utrudniało jądru dostęp do pamięci użytkownika), ale nie sądzę były one zawsze łączone w górę i zainteresowanie spadło wraz ze wzrostem x86-64

Również w takim przypadku, w jaki sposób jądro może korzystać z wysokiej pamięci, ponieważ na jaki adres pamięci wirtualnej zostaną zamapowane strony z dużej pamięci, ponieważ 1 GB miejsca w jądrze zostanie zmapowane logicznie?

Sposób, w jaki Linux działał (i nadal działa w systemach, w których pamięć jest niewielka w porównaniu do przestrzeni adresowej), polegał na tym, że cała pamięć fizyczna była na stałe odwzorowana w części przestrzeni adresowej jądra. Dzięki temu jądro miało dostęp do całej pamięci fizycznej bez ponownego mapowania, ale wyraźnie nie skaluje się do 32-bitowych maszyn z dużą ilością pamięci fizycznej.

Tak narodziła się koncepcja niskiej i wysokiej pamięci. „niska” pamięć jest trwale mapowana do przestrzeni adresowej jądra. „wysoka” pamięć nie jest.

Gdy procesor wykonuje wywołanie systemowe, działa w trybie jądra, ale nadal w kontekście bieżącego procesu. Dzięki temu może bezpośrednio uzyskiwać dostęp zarówno do przestrzeni adresowej jądra, jak i przestrzeni adresowej użytkownika bieżącego procesu (zakładając, że nie używasz wspomnianych łat 4G / 4G). Oznacza to, że przydzielenie „dużej” pamięci procesowi przestrzeni użytkownika nie stanowi problemu.

Używanie „wysokiej” pamięci do celów jądra jest większym problemem. Aby uzyskać dostęp do dużej pamięci, która nie jest odwzorowana na bieżący proces, musi zostać tymczasowo odwzorowana w przestrzeni adresowej jądra. Oznacza to dodatkowy kod i obniżenie wydajności.

płyn do płukania
źródło