[Перевод] Xv6: учебная Unix-подобная ОС. Глава 5. Прерывания и драйверы устройств
Примечание. Авторы рекомендуют читать книгу вместе с исходным текстом xv6. Авторы подготовили и лабораторные работы по xv6.
Xv6 работает на RISC-V, поэтому для его сборки нужны RISC-V версии инструментов: QEMU 5.1+, GDB 8.3+, GCC, и Binutils. Инструкция поможет поставить инструменты.
Драйвер управляет устройством — настраивает, отправляет команды, обрабатывает прерывания и общается с процессами, которые ожидают завершения ввода-вывода. Код драйвера зависит от конкретного устройства, поэтому изучайте документацию на устройство, чтобы понять код драйвера.
Устройство прерывает процессор, когда требует внимания. Обработчик прерывания опознает устройство и вызовет процедуру драйвера. В xv6 это делает процедура devintr
.
Код драйвера состоит из двух частей — верхней и нижней. Программы выполняют верхнюю часть драйвера через системные вызовы, такие как read
и write
. Верхняя часть драйвера просит устройство начать операцию и ждет. Нижняя часть драйвера обработает прерывание, когда устройство завершит операцию, разбудит верхнюю часть драйвера и займет устройство следующей операцией.
Код: ввод с терминала
Файл kernel/console.c
содержит код драйвера терминала. Драйвер обрабатывает символы из последовательного порта UART на RISC-V, которые человек вводит на клавиатуре.
QEMU эмулирует микросхему 16550 UART и подключает клавиатуру и экран к UART. На реальном компьютере 16550
управляет портом RS232
, который работает с терминалом или другим компьютером.
Драйвер накапливает символы в кольцевом буфере cons.buf
. Индекс cons.r
указывает на первый символ, который прочтет функция consoleread
. Буфер хранит строки символов, а индекс cons.w
указывает на начало последней строки, которую еще вводит человек. Индекс cons.e
указывает позицию курсора в последней строке для ввода следующего символа.
Функция consoleintr обрабатывает ввод символа с клавиатуры и распознает комбинации клавиш:
или+
сотрет последний символ — в позицииcons.e
сотрет последнюю строку — символы буфера с+ cons.w
доcons.e
напечатает список процессов+
Функция consoleintr
разбудит процесс, который ожидает ввода, когда встретит символ переноса строки, конца файла или заполнит буфер.
Драйвер работает с UART, когда читает и пишет байты по адресам памяти управляющих регистров. Xv6 отображает регистры UART по адресам памяти с UART0 = 0x10000000
. Файл kernel/uart.c
определяет смещения регистров UART от адреса UART0
.
Регистр LSR
— Line Status Register — определяет, ожидает ли обработки символ, который UART получил от клавиатуры.
Регистр RHR
— Receive Holding Register — содержит символ, который UART получил от клавиатуры. UART помещает следующий символ из очереди в регистр RHR
, как только драйвер прочел регистр RHR
. UART сбросит флаг готовности в LSR
, когда очередь полученных символов опустеет.
UART отправляет байт, когда драйвер пишет байт в регистр THR
— Transmit Holding Register.
Функция main вызывает consoleinit
, чтобы настроить UART. Этот код просит контроллер UART прерывать процессор каждый раз, как получит символ с клавиатуры, и каждый раз, как отправит символ на экран.
Программа shell
работает с терминалом, как с файлом — открывает файл устройства console
и читает строки. Системный вызов read
выполнит функцию consoleread
драйвера, чтобы извлечь из буфера следующую строку. Функция consoleread
вызовет sleep
и заставит процесс ждать, пока драйвер не получит строку символов от UART, если буфер терминала пуст. Глава 7 расскажет о работе sleep.
UART прерывает процессор, когда получает символ с клавиатуры. Обработчик прерывания вызовет функцию devintr
, которая вызывает uartintr
. Процедура uartintr
получит символ от UART и передаст процедуре consoleintr
драйвера терминала. Процедура uartintr
не ждет ввода следующих символов — UART прервет процессор снова, когда получит следующий символ. Процедура consoleintr
заполняет буфер символами, пока не встретит конец строки — тогда разбудит процесс, который ожидает ввод.
Код: вывод на терминал
Системный вызов write
выполнит функцию consolewrite
драйвера, чтобы вывести символы на экран. Драйвер UART ведет буфер символов uart_tx_buf
для отправки, чтобы процесс не ждал, пока микросхема UART отправит каждый символ. Функция consolewrite
выполнит uartputc
для каждого символа, которая запишет символ в буфер. Процедура uartputc
вызовет sleep
и заставит процесс ждать, только если буфер заполнен.
UART прерывает процессор каждый раз, как отправит символ. Обработчик прерывания uartintr
вызовет uartstart
, чтобы передать UART следующий символ из буфера.
Буферы и прерывания помогают устройствам ввода-вывода и процессам работать асинхронно. Драйвер терминала обработает ввод, даже если ни один процесс не читает ввод с терминала. Следующий вызов read
получит этот ввод. Процесс отправит устройству массив байтов и сразу продолжит работу, даже если устройство работает медленно. Асинхронный ввод-вывод помогает процессам и устройствам работать параллельно.
Параллельность в драйверах
Функции consoleread
и consoleintr
вызывают acquire
, чтобы захватить блокировку и защитить буфер драйвера терминала от одновременного доступа из нескольких потоков.
Проблемы одновременного доступа:
Два процессора параллельно выполняют два процесса и оба одновременно выполняют
consoleread
.UART прерывает процессор, когда процессор выполняет
consoleintr
— еще не закончил обработку предыдущего прерывания.Два процессора параллельно обрабатывают прерывания UART и одновременно выполняют
consoleintr
.
Одновременная запись символов в буфер из нескольких потоков испортит содержимое буфера. Глава 6 расскажет, как блокировки защищают данные от одновременного доступа.
Обработчики прерываний не взаимодействуют с процессами. Обработчик прерывания не знает, какой процесс прервал, и не зависит от этого. Обработчик прерывания выполняет простейшие действия — копирует байты в буфер — и будит верхнюю часть драйвера.
Прерывания таймера
Xv6 обновляет время на часах и переключает потоки выполнения, когда таймер прерывает процессор. Обработчики прерываний usertrap
и kerneltrap
вызывают yield
, чтобы переключить потоки. Xv6 программирует микросхему таймера RISC-V так, чтобы таймер периодически прерывал каждый процессор.
Процессор RISC-V обрабатывает прерывания таймера в машинном режиме. Ядро xv6 обрабатывает прерывания таймера не так, как остальные прерывания, потому что машинный режим работает с другим набором управляющих регистров и не работает со страничной памятью.
Код kernel/start.c
настраивает микросхему таймера в машинном режиме до вызова main
:
Поручает микросхеме
CLINT
— code-local interruptor — периодически прерывать процессор.Резервирует память, где обработчик прерывания хранит адреса управляющих регистров
CLINT
и регистры процессора.Пишет адрес обработчика прерываний таймера
timervec
в регистрmtvec
процессора и включает прерывания таймера.
Непредсказуемые прерывания таймера способны нарушить работу ядра, потому что ядро не способно отключить прерывания таймера. Обработчик прерывания таймера timervec
провоцирует программное прерывание, которое ядро обработает, когда сочтет безопасным.
Обработчик прерывания timervec
работает в машинном режиме — сохраняет регистры процессора, назначает микросхеме CLINT
время следующего прерывания, провоцирует программное прерывание, восстанавливает регистры процессора и возвращает управление. Обработчик прерывания timervec
не выполняет кода на языке Си.
Реальность
Xv6 обрабатывает прерывания устройств и таймера как в режиме ядра, так и в режиме пользователя. Поток выполнения может долго работать в режиме ядра, поэтому важно, что xv6 прерывает поток в ядре по таймеру и выполняет другой поток. Код ядра знает, что будет прерван, поэтому сохраняет регистры процессора на стеке и защищает глобальные переменные блокировками.
Компьютер владеет множеством устройств — каждое со своими функциями и протоколом работы. Код драйверов устройств превосходит по объему код ядра современных ОС.
Драйвер UART получает один байт за другим из регистра RHR
— такой подход называют «программируемый ввод-вывод». Программируемый ввод-вывод работает медленно. Устройства с прямым доступом к памяти работают быстрее — читают и записывают массив байтов в область памяти, а через управляющий регистр отправляют команды.
Обработка прерываний загружает процессор. Разработчики оптимизируют обработку прерываний:
Высокоскоростные устройства прерывают процессор для обработки сразу нескольких запросов.
Драйвер отключает прерывания и периодически опрашивает устройство.
Опрос устройства тратит время процессора впустую, если устройство работает медленно или простаивает. Отдельные драйверы переключаются между опросами и прерываниями.
Драйвер UART копирует байты в буфер ядра, затем — в память процесса. Высокоскоростные устройства с прямым доступом к памяти копируют байты сразу в память процесса.
Глава 1 рассказывала, что программы работают с терминалом, как с файлом — вызовами read
и write
. Такие вызовы не умеют отправлять устройству команды, например, увеличить размер буфера терминала. Системный вызов ioctl
на Unix отправляет команды устройству через файловый дескриптор.
ОС реального времени ограничивают время отклика системы — задержки ведут к катастрофе. Система жесткого реального времени — библиотека, которую подключает приложение. Разработчик способен оценить худшее время отклика такой ОС. Xv6 не выполняет требования системы реального времени — ни жесткой, ни мягкой. Планировщик xv6 примитивный, а код ядра надолго отключает прерывания.
Упражнения
Измените
uart.c
, чтобы не использовать прерывания. Вам придется изменить иconsole.c
.Напишите драйвер Ethernet.