Продолжаем исследовать Эльбрус путем портирования на него Embox.
Данная статья является второй частью технической статьи об архитектуре Эльбрус. В первой части речь шла о стеках, регистрах и так далее. Перед прочтением этой части рекомендуем изучить первую, поскольку в ней рассказывается о базовых вещах архитектуры Эльбрус. В этой части речь пойдет о таймерах, прерываниях и исключениях. Это, опять же, не официальная документация. За ней следует обращаться к разработчикам Эльбруса в МЦСТ.
Приступая к изучению Эльбруса, нам хотелось побыстрее запустить таймер, ведь, как вы понимаете, вытесняющая многозадачность без него не работает. Для этого казалось достаточно реализовать контроллер прерываний и сам таймер, но мы столкнулись снеожиданными ожидаемыми трудностями, куда же без них. Стали искать возможности отладки и выяснили, что разработчики об этом позаботились, введя несколько команд, которые позволяют вызывать различные исключительные ситуации. Например, можно сгенерировать исключение специального вида через регистры PSR (Processor Status Register) и UPSR (User processor status register). Для PSR бит exc_last_wish — флаг генерации исключительной ситуации exc_last_wish при возврате из процедуры, а для UPSR — exc_d_interrupt, это флаг отложенного прерывания, которые вырабатываются операцией VFDI (Проверка флага отложенного прерывания).
Код следующий:
Запустили. Но ничего не произошло, система где-то висела, в консоль ничего не выводилось. Собственно это мы и видели, когда пытались запустить прерывание от таймера, но тогда было много составляющих, а тут было понятно, что произошло нечто прерывающее последовательный ход выполнения нашей программы, и управление передалось на таблицу исключений (в терминах архитектуры Эльбрус правильнее говорить не о таблице прерываний а о таблице исключений). Мы предположили, что процессор все-таки выработал исключение, но там, куда он передал управление, лежит какой-то “мусор”. Как оказалось, передает он управление в то самое место куда мы положили образ Embox, а значит там лежала точка входа — функция entry.
Для проверки мы сделали следующее. Завели счетчик входов в entry(). Изначально стартуют все CPU с выключенными прерываниями, заходят в entry(), после чего мы оставляем активным только одно ядро, все остальные уходят в бесконечный цикл. После того как счетчик сравнялся с количеством CPU, считаем что все последующие попадания в entry — это исключения. Напоминаю, что раньше было так, как описано в нашей самой первой статье про Эльбрус
Сделали так
И наконец увидели реакцию на вход в прерывание (просто с помощью printf вывели строчку).
Тут стоит объяснить, что изначально в первом варианте мы рассчитывали скопировать таблицу исключений, но во-первых, оказалось, что она находится по нашему адресу, а во-вторых, нам так и не удалось сделать корректное копирование. Пришлось переписывать линкер-скрипты, точку входа в систему, ну и обработчик прерывания, то есть понадобилась ассемблерная часть, о ней чуть позже.
Вот так теперь выглядит часть модифицированная часть линкер скрипта:
то есть секцию входа мы убрали за таблицу исключений. Там же находится и секция cpu_idle для тех CPU, которые не используются.
Вот так выглядит функция входа для нашего активного ядра, на котором будет выполняться Embox:
Отлично, по инструкции VFDI выработалось исключение. Теперь нужно получить его номер, чтобы убедиться, что это правильное исключение. Для этого в Эльбрусе есть регистры информации о прерываниях TIR (Trap Info registers). Они содержат информацию о нескольких последних командах, то есть заключительной части трассы выполнения (trace). Trace собирается во время выполнения программы и «замораживается» при входе в прерывание. TIR включает в себя младшую (64 бита) и старшую (64 бита) части. В младшем слове содержатся флаги исключений, а в старшем слове указатель на инструкцию приведшую к исключению и номер текущего TIR’a. Соответственно, в нашем случае exc_d_interrupt это 4-ый бит.
Примечание у нас еще осталось некоторое непонимание касательно глубины (кол-ва) TIR’ов. В документации приводится:
Специалисты в МЦСТ нам пояснили, что все правильно, и для «точных» прерываний будет только TIR0, а для других ситуаций может быть и другое. Но поскольку пока речь идет только о прерываниях от таймера, нам это не мешает.
Хорошо, теперь разберем, что нужно для правильного входа/выхода из обработчика исключения. На самом деле необходимо сохранять на входе и восстанавливать на выходе 5 следующих регистров. Три регистра подготовки передачи управления — ctpr[1,2,3], и два регистра управления циклами — ILCR(Регистр исходного значения счетчика циклов) и LSR (Регистр состояния цикла).
Собственно, на этом все, после выхода из обработчика исключения нужно восстановить эти 5 регистров.
Мы делаем это с помощью макроса:
Еще важно не забыть уже после восстановления регистров вызвать операцию DONE (Возврат из аппаратного обработчика прерываний). Эта операция нужна в частности для того, чтобы корректно обработать прерванные операции передачи управления. Это мы делаем с помощью макроса:
Собственно возврат из прерывания мы делаем прямо в Си коде с помощью этих двух макросов.
Начнем с того, как же разрешить внешние прерывания. В Эльбрусe в качестве контроллера прерываний используется APIC (точнее его аналог), в Embox уже был этот драйвер. Поэтому можно было подцепить к нему системный таймер. Есть два таймера, один какой-то очень похожий на PIT, другой LAPIC Timer, тоже достаточно стандартный, поэтому рассказывать о них не имеет смысла. И то, и то выглядело просто, и то и то в Embox уже было, но драйвер LAPIC-таймера выглядел более перспективно, к тому же реализация PIT таймера нам показалась более нестандартной. Следовательно, доделать казалось проще. К тому же в официальной документации были описаны регистры APIC и LAPIC, которые немного отличались от оригиналов. Приводить их нет смысла, поскольку можно посмотреть в оригинале.
Помимо разрешения прерывания в APIC необходимо разрешить обработку прерываний через регистры PSR/UPSR. В обоих регистрах есть флаги разрешения внешних прерываний и немаскируемых прерываний. НО тут очень важно отметить, что регистр PSR является локальным для функции (об этом говорилось в первой технической части). А это означает, что если вы его выставили внутри функции, то при вызове всех последующих функций он будет наследоваться, но при возврате из функции вернет свое первоначальное состояние. Отсюда вопрос, а как же управлять прерываниями?
Мы используем следующее решение. Регистр PSR позволяет включить управление через UPSR, который уже является глобальным (что нам и нужно). Поэтому мы разрешаем управление через UPSR непосредственно (важно!) перед функцией входа в ядро Embox:
Как-то случайно после рефакторинга я взял и вынес эти строчки в отдельную функцию… А регистр-то локальный для функции. Понятно, что все сломалось :)
Итак, в процессоре все необходимое, вроде бы, включили, переходим к контроллеру прерываний.
Как мы уже разобрались выше, информация о номере исключения находится в регистре TIR. Далее, 32-ой бит в этом регистре сообщает о том, что случилось внешнее прерывание.
После включения таймера последовало пару дней мучений, так как никакого прерывания получить не удалось. Причина была достаточной забавной. В Эльбрусе 64-битные указатели, а адрес регистра в APIC влазил в uint32_t, поэтому мы их и использовали. Но оказалось, что если вам нужно, например, привести 0xF0000000 к указателю, то вы получите не 0xF0000000, а 0xFFFFFFFFF0000000. То есть, компилятор расширит ваш unsigned int знаком.
Здесь конечно нужно было использовать uintptr_t, поскольку, как выяснилось, в стандарте C99 такого рода приведения типов implementation defined.
После того как мы наконец-то увидели поднятый 32-ой бит в TIR’e, стали искать как получить номер прерывания. Это оказалось довольно просто, хотя и совсем не так как на x86, это одно из отличий реализаций LAPIC. Для Эльбруса, чтобы достать номер прерывания, нужно залезть в специальный регистр LAPIC:
где 0xFEE00000 — это базовый адрес регистров LAPIC.
На этом все, получилось подцепить и системный таймер и LAPIC таймер.
Информации, приведенной в первых двух технических частях статьи про архитектуру Эльбрус, достаточно для того, чтобы реализовать в любой ОС аппаратные прерывания и вытесняющую многозадачность. Собственно, приведенные скриншоты об этом свидетельствуют.
Это не последняя техническая часть про архитектуру Эльбрус. Сейчас мы осваиваем управление памятью (MMU) в Эльбрусе, надеемся скоро расскажем и о нем. Нам это то и нужно не только для реализации виртуальных адресных пространств, но и для нормальной работы с периферией, ведь через этот механизм можно отключить или включить кеширование определенной области адресного пространства.
Все, что написано в статье можно найти в репозитории Embox. Также можно собрать и запустить, если конечно имеется аппаратная платформа. Правда, для этого нужен компилятор, а его можно получить только в МЦСТ. Официальную документацию можно запросить там же.
Данная статья является второй частью технической статьи об архитектуре Эльбрус. В первой части речь шла о стеках, регистрах и так далее. Перед прочтением этой части рекомендуем изучить первую, поскольку в ней рассказывается о базовых вещах архитектуры Эльбрус. В этой части речь пойдет о таймерах, прерываниях и исключениях. Это, опять же, не официальная документация. За ней следует обращаться к разработчикам Эльбруса в МЦСТ.
Приступая к изучению Эльбруса, нам хотелось побыстрее запустить таймер, ведь, как вы понимаете, вытесняющая многозадачность без него не работает. Для этого казалось достаточно реализовать контроллер прерываний и сам таймер, но мы столкнулись с
Код следующий:
#define UPSR_DI (1 << 3) /* Определен в .h файле */
rrs %upsr, %r1
ors %r1, UPSR_DI, %r1 /* upsr |= UPSR_DI; */
rws %r1, %upsr
vfdi /* Вот здесь должно выработаться исключение */
Запустили. Но ничего не произошло, система где-то висела, в консоль ничего не выводилось. Собственно это мы и видели, когда пытались запустить прерывание от таймера, но тогда было много составляющих, а тут было понятно, что произошло нечто прерывающее последовательный ход выполнения нашей программы, и управление передалось на таблицу исключений (в терминах архитектуры Эльбрус правильнее говорить не о таблице прерываний а о таблице исключений). Мы предположили, что процессор все-таки выработал исключение, но там, куда он передал управление, лежит какой-то “мусор”. Как оказалось, передает он управление в то самое место куда мы положили образ Embox, а значит там лежала точка входа — функция entry.
Для проверки мы сделали следующее. Завели счетчик входов в entry(). Изначально стартуют все CPU с выключенными прерываниями, заходят в entry(), после чего мы оставляем активным только одно ядро, все остальные уходят в бесконечный цикл. После того как счетчик сравнялся с количеством CPU, считаем что все последующие попадания в entry — это исключения. Напоминаю, что раньше было так, как описано в нашей самой первой статье про Эльбрус
cpuid = __e2k_atomic32_add(1, &last_cpuid);
if (cpuid > 1) {
/* XXX currently we support only single core */
while(1);
}
/* copy of trap table */
memcpy((void*)0, &_t_entry, 0x1800);
kernel_start();
Сделали так
/* Since we enable exceptions only when all CPUs except the main one
* reached the idle state (cpu_idle), we can rely that order and can
* guarantee exceptions happen strictly after all CPUS entries. */
if (entries_count >= CPU_COUNT) {
/* Entering here because of expection or interrupt */
e2k_trap_handler(regs);
...
}
/* It wasn't exception, so we decide this usual program execution,
* that is, Embox started on CPU0 or CPU1 */
e2k_wait_all();
entries_count = __e2k_atomic32_add(1, &entries_count);
if (entries_count > 1) {
/* XXX currently we support only single core */
cpu_idle();
}
e2k_kernel_start();
}
И наконец увидели реакцию на вход в прерывание (просто с помощью printf вывели строчку).
Тут стоит объяснить, что изначально в первом варианте мы рассчитывали скопировать таблицу исключений, но во-первых, оказалось, что она находится по нашему адресу, а во-вторых, нам так и не удалось сделать корректное копирование. Пришлось переписывать линкер-скрипты, точку входа в систему, ну и обработчик прерывания, то есть понадобилась ассемблерная часть, о ней чуть позже.
Вот так теперь выглядит часть модифицированная часть линкер скрипта:
.text : {
_start = .;
_t_entry = .;
/* Interrupt handler */
*(.ttable_entry0)
. = _t_entry + 0x800;
/* Syscall handler */
*(.ttable_entry1)
. = _t_entry + 0x1000;
/* longjmp handler */
*(.ttable_entry2)
. = _t_entry + 0x1800;
_t_entry_end = .;
*(.e2k_entry)
*(.cpu_idle)
/* text */
}
то есть секцию входа мы убрали за таблицу исключений. Там же находится и секция cpu_idle для тех CPU, которые не используются.
Вот так выглядит функция входа для нашего активного ядра, на котором будет выполняться Embox:
static void e2k_kernel_start(void) {
extern void kernel_start(void);
int psr;
/* Ждем пока остальные CPU “уснут” */
while (idled_cpus_count < CPU_COUNT - 1)
;
...
/* Отключаем операции с плавающей точкой, они разрешены по умолчанию */
e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE);
kernel_start(); /* Входим в Embox */
}
Отлично, по инструкции VFDI выработалось исключение. Теперь нужно получить его номер, чтобы убедиться, что это правильное исключение. Для этого в Эльбрусе есть регистры информации о прерываниях TIR (Trap Info registers). Они содержат информацию о нескольких последних командах, то есть заключительной части трассы выполнения (trace). Trace собирается во время выполнения программы и «замораживается» при входе в прерывание. TIR включает в себя младшую (64 бита) и старшую (64 бита) части. В младшем слове содержатся флаги исключений, а в старшем слове указатель на инструкцию приведшую к исключению и номер текущего TIR’a. Соответственно, в нашем случае exc_d_interrupt это 4-ый бит.
Примечание у нас еще осталось некоторое непонимание касательно глубины (кол-ва) TIR’ов. В документации приводится:
“Глубина памяти TIR, то есть количество регистров Trap Info, определяетсяНа практике же, мы видим глубину = 1, и поэтому используем только регистр TIR0.
TIR_NUM macro, равным количеству стадий конвейера процессора, требуемых для
выдачи всех возможных особых ситуаций. TIR_NUM = 19;”
Специалисты в МЦСТ нам пояснили, что все правильно, и для «точных» прерываний будет только TIR0, а для других ситуаций может быть и другое. Но поскольку пока речь идет только о прерываниях от таймера, нам это не мешает.
Хорошо, теперь разберем, что нужно для правильного входа/выхода из обработчика исключения. На самом деле необходимо сохранять на входе и восстанавливать на выходе 5 следующих регистров. Три регистра подготовки передачи управления — ctpr[1,2,3], и два регистра управления циклами — ILCR(Регистр исходного значения счетчика циклов) и LSR (Регистр состояния цикла).
.type ttable_entry0,@function
ttable_entry0:
setwd wsz = 0x10, nfx = 1;
rrd %ctpr1, %dr1
rrd %ctpr2, %dr2
rrd %ctpr3, %dr3
rrd %ilcr, %dr4
rrd %lsr, %dr5
/* sizeof pt_regs */
getsp -(5 * 8), %dr0
std %dr1, [%dr0 + PT_CTRP1] /* regs->ctpr1 = ctpr1 */
std %dr2, [%dr0 + PT_CTRP2] /* regs->ctpr2 = ctpr2 */
std %dr3, [%dr0 + PT_CTRP3] /* regs->ctpr3 = ctpr3 */
std %dr4, [%dr0 + PT_ILCR] /* regs->ilcr = ilcr */
std %dr5, [%dr0 + PT_LSR] /* regs->lsr = lsr */
disp %ctpr1, e2k_entry
ct %ctpr1
Собственно, на этом все, после выхода из обработчика исключения нужно восстановить эти 5 регистров.
Мы делаем это с помощью макроса:
#define RESTORE_COMMON_REGS(regs) \
({ \
uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \
ctpr3 = regs->ctpr3, lsr = regs->lsr, \
ilcr = regs->ilcr; \
/* ctpr2 is restored first because of tight time constraints \
* on restoring ctpr2 and aaldv. */ \
E2K_SET_DSREG(ctpr1, ctpr1); \
E2K_SET_DSREG(ctpr2, ctpr2); \
E2K_SET_DSREG(ctpr3, ctpr3); \
E2K_SET_DSREG(lsr, lsr); \
E2K_SET_DSREG(ilcr, ilcr); \
})
Еще важно не забыть уже после восстановления регистров вызвать операцию DONE (Возврат из аппаратного обработчика прерываний). Эта операция нужна в частности для того, чтобы корректно обработать прерванные операции передачи управления. Это мы делаем с помощью макроса:
#define E2K_DONE \
do { \
asm volatile ("{nop 3} {done}" ::: "ctpr3"); \
} while (0)
Собственно возврат из прерывания мы делаем прямо в Си коде с помощью этих двух макросов.
/* Entering here because of expection or interrupt */
e2k_trap_handler(regs);
RESTORE_COMMON_REGS(regs);
E2K_DONE;
Внешние прерывания
Начнем с того, как же разрешить внешние прерывания. В Эльбрусe в качестве контроллера прерываний используется APIC (точнее его аналог), в Embox уже был этот драйвер. Поэтому можно было подцепить к нему системный таймер. Есть два таймера, один какой-то очень похожий на PIT, другой LAPIC Timer, тоже достаточно стандартный, поэтому рассказывать о них не имеет смысла. И то, и то выглядело просто, и то и то в Embox уже было, но драйвер LAPIC-таймера выглядел более перспективно, к тому же реализация PIT таймера нам показалась более нестандартной. Следовательно, доделать казалось проще. К тому же в официальной документации были описаны регистры APIC и LAPIC, которые немного отличались от оригиналов. Приводить их нет смысла, поскольку можно посмотреть в оригинале.
Помимо разрешения прерывания в APIC необходимо разрешить обработку прерываний через регистры PSR/UPSR. В обоих регистрах есть флаги разрешения внешних прерываний и немаскируемых прерываний. НО тут очень важно отметить, что регистр PSR является локальным для функции (об этом говорилось в первой технической части). А это означает, что если вы его выставили внутри функции, то при вызове всех последующих функций он будет наследоваться, но при возврате из функции вернет свое первоначальное состояние. Отсюда вопрос, а как же управлять прерываниями?
Мы используем следующее решение. Регистр PSR позволяет включить управление через UPSR, который уже является глобальным (что нам и нужно). Поэтому мы разрешаем управление через UPSR непосредственно (важно!) перед функцией входа в ядро Embox:
/* PSR is local register and makes sense only within a function,
* so we set it here before kernel start. */
asm volatile ("rrs %%psr, %0" : "=r"(psr) :);
psr |= (PSR_IE | PSR_NMIE | PSR_UIE);
asm volatile ("rws %0, %%psr" : : "ri"(psr));
kernel_start();
Как-то случайно после рефакторинга я взял и вынес эти строчки в отдельную функцию… А регистр-то локальный для функции. Понятно, что все сломалось :)
Итак, в процессоре все необходимое, вроде бы, включили, переходим к контроллеру прерываний.
Как мы уже разобрались выше, информация о номере исключения находится в регистре TIR. Далее, 32-ой бит в этом регистре сообщает о том, что случилось внешнее прерывание.
После включения таймера последовало пару дней мучений, так как никакого прерывания получить не удалось. Причина была достаточной забавной. В Эльбрусе 64-битные указатели, а адрес регистра в APIC влазил в uint32_t, поэтому мы их и использовали. Но оказалось, что если вам нужно, например, привести 0xF0000000 к указателю, то вы получите не 0xF0000000, а 0xFFFFFFFFF0000000. То есть, компилятор расширит ваш unsigned int знаком.
Здесь конечно нужно было использовать uintptr_t, поскольку, как выяснилось, в стандарте C99 такого рода приведения типов implementation defined.
После того как мы наконец-то увидели поднятый 32-ой бит в TIR’e, стали искать как получить номер прерывания. Это оказалось довольно просто, хотя и совсем не так как на x86, это одно из отличий реализаций LAPIC. Для Эльбруса, чтобы достать номер прерывания, нужно залезть в специальный регистр LAPIC:
#define APIC_VECT (0xFEE00000 + 0xFF0)
где 0xFEE00000 — это базовый адрес регистров LAPIC.
На этом все, получилось подцепить и системный таймер и LAPIC таймер.
Заключение
Информации, приведенной в первых двух технических частях статьи про архитектуру Эльбрус, достаточно для того, чтобы реализовать в любой ОС аппаратные прерывания и вытесняющую многозадачность. Собственно, приведенные скриншоты об этом свидетельствуют.
Это не последняя техническая часть про архитектуру Эльбрус. Сейчас мы осваиваем управление памятью (MMU) в Эльбрусе, надеемся скоро расскажем и о нем. Нам это то и нужно не только для реализации виртуальных адресных пространств, но и для нормальной работы с периферией, ведь через этот механизм можно отключить или включить кеширование определенной области адресного пространства.
Все, что написано в статье можно найти в репозитории Embox. Также можно собрать и запустить, если конечно имеется аппаратная платформа. Правда, для этого нужен компилятор, а его можно получить только в МЦСТ. Официальную документацию можно запросить там же.