[Перевод] Xv6: учебная Unix-подобная ОС. Глава 4: Прерывания и системные вызовы

2bd99fb73299376e93e40ad37b5a0757.png

Примечание. Авторы рекомендуют читать книгу вместе с исходным текстом xv6. Авторы подготовили и лабораторные работы по xv6.

Xv6 работает на RISC-V, поэтому для его сборки нужны RISC-V версии инструментов: QEMU 5.1+, GDB 8.3+, GCC, и Binutils. Инструкция поможет поставить инструменты.

Процессор прерывает работу и передает управление ядру, когда:

  • Программа выполняет системный вызов.

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

  • Устройство требует внимания процессора, например, диск завершил чтение данных, которые требовала программа.

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

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

Xv6 содержит код для трех сценариев:

  • Прерывания из режима пользователя

  • Прерывания из режима ядра

  • Прерывания от таймера

Прерывания на RISC-V

Каждый RISC-V процессор владеет набором управляющих регистров, которые определяют, как процессор реагирует на прерывания. Документация RISC-V подробно рассказывает о них. XV6 использует следующие регистры:

  • stvec хранит адрес обработчика прерываний — процессор передаст управление по этому адресу.

  • sepc — процессор сохраняет счетчик инструкций pc программы в регистре sepc, прежде чем передать управление обработчику прерываний. Инструкция возврата из обработчика прерываний sret восстановит значение регистра pc из sepc. Ядро заставит обработчик передать управление другому коду, если изменит регистр sepc.

  • scause хранит номер прерывания — причину прерывания.

  • sscratch. Обработчик прерываний сохраняет регистры процессора в памяти. Инструкция записи в память store требует указать адрес памяти в регистре процессора, но регистры заняты. Обработчик сохранит регистр a0 в sscratch и использует a0 в инструкции store.

  • sstatus определяет состояние процессора при обработке прерывания:

    • Бит SIE регистра sstatus определяет, реагирует ли процессор на прерывания в режиме ядра.

    • Бит SPP регистра sstatus определяет, возникло ли прерывание в режиме пользователя или ядра. Инструкция sret вернет процессор в режим, который определяет бит SPP.

Процессор работает с этими регистрами только в режиме ядра — из режима пользователя регистры недоступны.

Ядро использует аналогичный набор регистров mtvec, mepc, mcause, mscratch, mstatus для обработки прерываний по таймеру в машинном режиме.

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

Процессор действует так, когда реагирует на прерывание, кроме прерываний от таймера:

  • Проверяет флаг SIE регистра sstatus, если прерывание пришло от устройства. Ничего не делает, если флаг сброшен, иначе — переходит к следующему шагу

  • Сбрасывает флаг SIE регистра sstatus

  • Присваивает sepc = pc

  • Устанавливает флаг SPP регистра sstatus в 1, если процессор работает в режиме ядра, иначе — сбрасывает флаг в 0

  • Пишет причину прерывания в scause

  • Переключается в режим ядра

  • Продолжает работу с адреса из регистра pc

Процессор не переключается на таблицу страниц ядра, стек ядра и не сохраняет регистры процессора, кроме pc. Процессор оставляет эти задачи обработчику прерывания. Другие ОС оптимизируют обработку прерываний, например, не переключаются на таблицу станиц ядра.

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

Прерывания режима пользователя

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

Процессор прерывает работу в режиме пользователя, если:

  • Программа выполняет системный вызов

  • Программа допускает ошибку — провоцирует исключение

  • Устройство требует внимания процессора

Процессор переключается на таблицу страниц ядра и стек ядра в ассемблерной процедуре uservec, прежде чем вызовет обработчик прерывания usertrap на языке Си. Затем процессор выполнит usertrapret и вернется на стек режима пользователя и таблицу страниц процесса в ассемблерной процедуре userret.

Процессор не переключается на таблицу страниц ядра, когда реагирует на прерывание, поэтому таблица страниц процесса содержит страницу trampoline с кодом uservec. Процедура uservec переключает процессор на таблицу страниц ядра, поэтому таблица страниц ядра отображает страницу trampoline на тот же виртуальный адрес, чтобы регистр pc указывал на корректный виртуальный адрес следующей инструкции и uservec продолжила выполнение после смены таблиц. Страница trampoline содержит и процедуру userret, которая возвращает процессор к таблице страниц процесса.

Флаг PTE_U у страницы trampoline сброшен, поэтому uservec и userret работают только в режиме ядра.

Процедура uservec сохраняет состояние программы — 32 регистра — на странице trapframe, переключается на таблицу страниц ядра и стек ядра и передает управление usertrap. Процесс хранит адрес стека ядра и таблицы страниц ядра на странице trapframe.

Процедура usertrap определяет причину прерывания, обрабатывает прерывание и передает управление usertrapret. Процедура usertrap назначает обработчиком прерываний процедуру kernelvec, затем сохраняет sepc в trapframe, так как прерывание по таймеру заставит переключить поток выполнения вызовом yield, а другой поток изменит sepc, когда вернется из режима ядра в режим пользователя. Процедура usertrap вызывает syscall, если прерывание — системный вызов, devintr, если прерывание — от устройства, иначе завершает процесс, реагируя на исключение. Процедура usertrap добавляет 4 к сохраненному pc, когда обрабатывает системный вызов, чтобы процесс продолжил работу со следующей за ecall инструкции.

Процедура usertrapret пишет в stvec адрес обработчика прерываний режима пользователя uservec, пишет в trapframe адреса таблицы страниц ядра, стека ядра, которые нужны uservec, и восстанавливает регистр sepc из trapframe. Затем usertrapret передает управление userret вместе с адресом таблицы страниц процесса.

Процедура userret переключается на таблицу страниц процесса и стек режима пользователя, восстанавливает регистры процессора из trapframe и выполняет инструкцию sret, чтобы вернуться в режим пользователя и продолжить выполнение программы.

Код: системные вызовы

Глава 2 рассказывала, как xv6 выполняет первый системный вызов exec. Этот раздел расскажет, как ядро выполняет код exec.

Программа initcode.S помещает номер системного вызова в регистр a7 процессора, а аргументы вызова — в регистры a0 и a1. Номер системного вызова — индекс элемента массива syscalls — массива указателей на функции. Инструкция ecall прерывает процессор — заставляет переключиться в режим ядра и выполнить обработчик прерываний uservec, затем функции usertrap и syscall.

Функция syscall получает номер системного вызова из сохраненного в trapframe регистра a7. Константа SYS_exec определяет номер системного вызова exec, а элемент массива syscalls[SYS_exec] указывает на функцию sys_exec.

Функция syscall пишет в p->trapframe->a0 значение, которое возвращает sys_exec, чтобы вызов exec в программе вернул это значение. Соглашение о вызовах языка Си на RISC-V говорит, что функции пишут возвращаемое значение в регистр a0. Системные вызовы возвращают 0, когда завершаются успешно, или отрицательное число, чтобы сообщить об ошибке. Функция syscall напечатает сообщение об ошибке и вернет -1, если программа передала неправильный номер системного вызова.

Код: аргументы системных вызовов

Код системных вызовов использует функции argint, argaddr и argfd, чтобы добраться до аргументов, сохраненных в trapframe. Программа передает аргументы через регистры процессора, а обработчик прерывания uservec сохраняет регистры в trapframe. Функции argint, argaddr и argfd вызывают argraw, чтобы извлечь n-й аргумент из trapframe и вернуть его как число, адрес и файловый дескриптор соответственно.

Ядро не способно обратиться по адресу памяти процесса, так как работает с таблицей страниц ядра, поэтому реализует функции копирования из памяти процесса в память ядра.

Функция fetchstr копирует строку из памяти процесса в память ядра. Функция fetchstr вызывает copyinstr, которая копирует до max байтов в буфер по адресу dst из буфера по виртуальному адресу srcva в таблице страниц процесса pagetable. Функция copyinstr использует walkaddr, чтобы найти физический адрес pa0 по виртуальному srcva в таблице страниц pagetable. Таблица страниц ядра отображает виртуальные адреса на те же физические, поэтому copyinstr копирует байты из pa0 в dst. Функция walkaddr проверяет, что виртуальный адрес принадлежит памяти процесса, поэтому программа не обманет ядро подменой адреса. Аналогичная функция copyout копирует байты из памяти ядра в память процесса.

Прерывания режима ядра

Xv6 назначает обработчиком прерываний процедуру kernelvec, когда входит в режим ядра. Процедура kernelvec знает, что работает с таблицей страниц ядра и стеком ядра. Прерывание от таймера переключит процессор на другой поток, поэтому kernelvec сохраняет регистры процессора на стеке ядра.

Процедура kernelvec вызывает процедуру kerneltrap, которая обрабатывает прерывания от устройств и исключения. Процедура kerneltrap вызывает devintr, чтобы опознать прерывание от устройства. Ядро вызовет panic и остановит работу, если произошло исключение в ядре.

Прерывание от таймера заставит kerneltrap вызвать yield, чтобы уступить процессор другому потоку. Каждый поток вызывает yield по таймеру, поэтому kerneltrap продолжит работу позже. Глава 7 расскажет о планировании процессов и работе yield.

Процедура kerneltrap сохраняет регистр sepc в локальной переменной на стеке ядра, чтобы защитить от переключения потоков.

Процедура kerneltrap возвращает управление kernelvec, которая восстанавливает регистры процессора и возвращает управление коду ядра, что работал до прерывания.

Процессор отключает прерывания — сбрасывает бит SIE — когда реагирует на прерывание. Обработчик прерываний режима пользователя назначает обработчиком kernelvec и включает прерывания, поэтому процессор не вызовет uservec дважды. Процедура kernelvec не включает прерывания, поэтому процессор не вызовет kernelvec дважды. Инструкция sret вернет флаг SIE в значение до прерывания SIE = SPIE, то есть включит прерывания снова.

Ошибки доступа к страницам

Xv6 завершает процесс, который провоцирует исключение, и останавливает работу, если ядро вызвало исключение.

Другие ОС используют ошибки доступа к страницам, чтобы реализовать приемы:

  • Копирование при записи

  • Ленивая выдача памяти

  • Выдача страниц по необходимости

  • Сброс страниц на диск

Процессор сообщит об ошибке доступа к странице, если:

  • Процессор не нашел виртуальный адрес в таблице страниц процесса — флаг PTE_V у записи сброшен

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

RISC-V различает три вида ошибок доступа к странице:

  • Инструкция load не может обратиться по виртуальному адресу

  • Инструкция store не может обратиться по виртуальному адресу

  • Регистр pc содержит недоступный виртуальный адрес

Регистр scause указывает на вид ошибки, а stval содержит недоступный виртуальный адрес.

Копирование при записи

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

ОС следит за вызовами fork, exec, exit и ошибками доступа к страницам, когда оптимизирует работу с помощью копирования при записи. Одна и та же физическая страница попадает во множество таблиц страниц после вызовов fork, а вызовы exec и exit освобождают виртуальные страницы, которые ссылаются на эту физическую страницу.

Копирование при записи ускоряет программы, которые после fork вызывают exec — fork не копирует ни байта, а exec заменяет память программой из файла.

Ленивая выдача памяти

Ядро не выдает страницы памяти, когда программа вызывает sbrk, а запоминает увеличение памяти и ожидает обращения к новой памяти. Ядро выдаст страницу памяти, когда обработает ошибку доступа к странице.

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

Программа не ждет ни секунды, если запрашивает большой объем памяти, благодаря ленивой выдаче. Вызов sbrk на гигабайт памяти заставил бы программу ждать, пока ядро выдаст 262144 страниц по 4096 байтов. Ленивая выдача равномерно распределит время ожидания. Ядро ускорит работу, если при ошибке доступа к странице выдаст не одну, а последовательность страниц.

Выдача страниц по необходимости

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

Сброс страниц на диск

Программы работают, даже если размер виртуальной памяти больше размера физической, благодаря сбросу страниц на диск. ОС хранит часть страниц в оперативной памяти, а остальные хранит на диске. Ядро сбрасывает флаг PTE_V для сброшенных на диск страниц и загружает страницу в память при ошибке доступа к странице.

Ядро замещает страницу в оперативной памяти той, что читает с диска, когда свободная память закончилась. Диск работает медленнее, чем оперативная память, поэтому чем реже ОС замещает страницы, тем быстрее программы работают. Замещать страницы не потребуется, если каждая программа работает с подмножеством страниц, которые умещаются в оперативную память.

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

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

ОС автоматически расширяет стек программы и отображает файлы в память при помощи ошибок доступа к страницам.

Реальность

Обработка прерываний кажется излишне сложной, потому что процессор RISC-V выполняет автоматически необходимый минимум действий, но другие ОС пользуются этим, чтобы ускорить обработку прерываний.

Другие ОС отображают страницы памяти ядра в таблицы страниц процессов, поэтому не используют страницу trampoline, не переключаются на таблицу страниц ядра, а код системных вызовов работает с адресами памяти процесса. Xv6 отказывается от такой оптимизации, чтобы избежать ошибок безопасности ядра из-за неверной работы с адресами памяти пользователя.

Современные ОС реализуют копирование при записи, ленивую выдачу памяти, выдачу страниц по необходимости, сброс страниц на диск, отображение файлов в память и т.д. Современные ОС в отличие от xv6 стремятся использовать как можно больше свободной памяти — размещают в памяти кеш диска, файловой системы и т.д. Xv6 не сбрасывает страницы на диск и завершит программу, если для нее не найдется свободной памяти.

Про кеш диска расскажет глава 8.

Упражнения

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

  • Реализуйте ленивую выдачу памяти

  • Реализуйте fork с копированием при записи

  • Можно ли избавиться от страницы trapframe в таблицах страниц процессов? Может ли uservec сохранять 32 регистра процессора на стеке ядра или в структуре proc?

  • Как избавить xv6 от страницы trampoline в таблицах страниц?

© Habrahabr.ru