Режим ядра, прерывания и системные вызовы
Два режима процессора
Процессор умеет работать в разных режимах привилегий. На x86 их четыре («кольца» 0–3), но используются два:
| Режим ядра (кольцо 0) | Режим пользователя (кольцо 3) | |
|---|---|---|
| Кто работает | ядро ОС | процессы |
Привилегированные инструкции (mov cr3, hlt, in/out, cli) |
можно | исключение |
| Доступ к памяти | вся | только страницы с битом U/S = 1 |
| Ошибка | может «уронить» всю систему (kernel panic) | ядро убьёт только этот процесс |
Именно аппаратная защита делает ОС возможной: без неё любая программа могла бы переписать таблицы страниц и читать чужую память.
Три двери в ядро
- Аппаратные прерывания (interrupts) — асинхронные сигналы от устройств: таймер, клавиатура, сетевая карта, диск. Не связаны с текущей инструкцией.
- Исключения (exceptions) — синхронные, вызваны конкретной инструкцией: - отказы (faults) — инструкцию можно повторить после исправления: страничный отказ (page fault); - ловушки (traps) — после инструкции: точка останова отладчика; - аварии (aborts) — неисправимые: аппаратная ошибка.
- Системные вызовы — программа намеренно просит ядро об услуге специальной инструкцией.
Что ядро делает с исключением
| Исключение | Реакция ядра |
|---|---|
| Страничный отказ, страница допустима, но не в памяти | загрузить страницу, повторить инструкцию — процесс ничего не заметит |
| Страничный отказ на недопустимом адресе | послать SIGSEGV |
| Деление на ноль | послать SIGFPE |
| Недопустимая инструкция | послать SIGILL |
Ошибка шины / невыровненный доступ / доступ за конец файла в mmap |
послать SIGBUS |
Связь с задачей 1
Столбец «связан с аппаратным событием» в таблице сигналов — это сигналы, которые ядро посылает в ответ на исключения процессора: SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP.
Как выполняется системный вызов
На x86-64 Linux:
/* то, что делает обёртка write() в libc, упрощённо */
long ret;
asm volatile ("syscall"
: "=a"(ret)
: "a"(1), /* rax = номер вызова: 1 = write */
"D"(fd), /* rdi = 1-й аргумент */
"S"(buf), /* rsi = 2-й */
"d"(count) /* rdx = 3-й */
: "rcx", "r11", "memory");
if (ret < 0) { errno = -ret; ret = -1; }
- Обёртка кладёт номер вызова в
rax, аргументы — в регистры. - Инструкция
syscallпереключает процессор в кольцо 0 и прыгает на точку входа ядра. - Ядро сохраняет регистры пользователя (контекст) на стеке ядра этого процесса.
- По номеру выбирает обработчик из таблицы системных вызовов (
sys_call_table). - Обработчик проверяет аргументы (указатели должны вести в память пользователя!) и выполняет работу.
- Результат — в
rax;sysretвозвращает в режим пользователя.
На старых 32-битных системах для этого использовалась программная ловушка int 0x80.
Аналогия
Системный вызов — окошко в банке. Посетитель (процесс) не может зайти в хранилище сам; он заполняет бланк (регистры), отдаёт в окошко (syscall), сотрудник (ядро) проверяет бланк и выполняет операцию.
Переключение контекста
Когда ядро решает запустить другой процесс (например, по прерыванию таймера), оно выполняет переключение контекста:
- сохраняет регистры текущего процесса в его структуре (
task_structв Linux); - переключает адресное пространство — загружает в cr3 адрес таблиц страниц нового процесса (на большинстве процессоров это сбрасывает TLB);
- восстанавливает регистры нового процесса и возвращается в режим пользователя уже в нём.
Переключение контекста дорогое: кроме самих действий, после него TLB и кеши «холодные».
strace
$ strace -f ./prog
execve("./prog", ["./prog"], ...) = 0
mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0x7f...
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 4242
wait4(-1, NULL, 0, NULL) = 4242
write(1, "4241: 4242\n", 11) = 11
exit_group(0) = ?
Обратите внимание: fork() в glibc выполняется системным вызовом clone с флагом SIGCHLD — к этому вернёмся в уроке о потоках.
Итог
- Процессор различает режимы ядра и пользователя; привилегированные инструкции в режиме пользователя вызывают исключение.
- В ядро попадают через прерывания (асинхронно), исключения (синхронно) и системные вызовы.
- Аппаратные исключения превращаются в сигналы
SIGSEGV,SIGBUS,SIGFPE,SIGILL. - Системный вызов: номер в
rax, инструкцияsyscall, таблица вызовов, возврат черезsysret. - Переключение контекста меняет
cr3и сбрасывает TLB.
Практика решено 0 из 5
К какому типу перехода в ядро относится каждое событие?
| Аппаратное прерывание | Исключение (синхронное) | Системный вызов | |
|---|---|---|---|
| Нажата клавиша на клавиатуре | |||
| Деление целого числа на ноль | |||
| Обращение к странице, которой нет в памяти | |||
| Программа вызвала fork() | |||
| Сработал таймер — истёк квант времени | |||
| Диск закончил чтение блока (DMA) | |||
| Программа вызвала write() |
Расставьте шаги выполнения системного вызова write(1, buf, 5) на x86-64 Linux:
-
Выполняется инструкция
syscall: процессор переходит в режим ядра на адрес входа из регистра MSR -
Результат кладётся в
rax, регистры восстанавливаются,sysretвозвращает в режим пользователя -
Обёртка libc кладёт номер вызова (1 = write) в
rax, аргументы — вrdi,rsi,rdx - Ядро сохраняет регистры пользователя на стеке ядра
-
По номеру из
raxядро находит обработчик в таблице системных вызовов и вызываетsys_write -
Обёртка libc при отрицательном результате записывает код в
errnoи возвращает −1
Что произойдёт, если программа в режиме пользователя попытается выполнить привилегированную инструкцию (например, записать в cr3)?
Какие сигналы ядро посылает процессу в ответ на аппаратные исключения?
Какой утилитой можно посмотреть все системные вызовы, которые делает программа? (только название)
Войдите, чтобы отмечать пройденные уроки.