
Continuamos a explorar
Elbrus portando a
Embox .
Este artigo é parte dois de um artigo técnico sobre arquitetura Elbrus. A
primeira parte tratou de pilhas, registros e assim por diante. Antes de ler esta parte, recomendamos que você estude o primeiro, pois ele aborda as coisas básicas da arquitetura Elbrus. Esta seção se concentrará em cronômetros, interrupções e exceções. Novamente, isso não é documentação oficial. Para isso, você deve entrar em contato com os desenvolvedores da Elbrus no
ICST .
Chegando ao estudo da Elbrus, queríamos iniciar rapidamente o cronômetro, porque, como você sabe, a multitarefa preemptiva não funciona sem ele. Para fazer isso, parecia suficiente implementar o controlador de interrupção e o próprio cronômetro, mas encontramos dificuldades esperadas
inesperadas , para onde iríamos sem elas. Eles começaram a procurar recursos de depuração e descobriram que os desenvolvedores cuidavam disso introduzindo vários comandos que permitem criar várias situações excepcionais. Por exemplo, você pode gerar uma exceção de um tipo especial através dos registros PSR (Processor Status Register) e UPSR (User processador status register). Para PSR, o bit exc_last_wish é o sinalizador de exceção exc_last_wish ao retornar do procedimento e, para o UPSR, o exc_d_interrupt é o sinalizador de interrupção atrasada gerado pela operação VFDI (sinalizador de verificação de interrupção atrasada).
O código é o seguinte:
#define UPSR_DI (1 << 3) rrs %upsr, %r1 ors %r1, UPSR_DI, %r1 rws %r1, %upsr vfdi
Lançado. Mas nada aconteceu, o sistema travou em algum lugar, nada foi enviado para o console. Na verdade, vimos isso quando tentamos iniciar a interrupção a partir do timer, mas havia muitos componentes, e aqui ficou claro que algo interrompeu o progresso seqüencial do nosso programa e o controle foi transferido para a tabela de exceção (em termos de arquitetura Elbrus, é mais correto não falar sobre a tabela interrupções e sobre a tabela de exceções). Assumimos que o processador, no entanto, lançou uma exceção, mas havia algum "lixo" onde transferia o controle. Como se viu, ele transfere o controle para o mesmo local em que colocamos a imagem da Embox, o que significa que havia um ponto de entrada - a função de entrada.
Para verificação, fizemos o seguinte. Iniciou um contador de entradas na entrada (). Inicialmente, todas as CPUs começam com as interrupções desativadas, entram na entrada (), após as quais deixamos apenas um núcleo ativo, e o restante entra em um loop sem fim. Depois que o contador for igual ao número de CPUs, consideramos que todos os hits subsequentes na entrada são exceções. Lembro que antes era como descrito em
nosso primeiro artigo sobre Elbrus cpuid = __e2k_atomic32_add(1, &last_cpuid); if (cpuid > 1) { while(1); } memcpy((void*)0, &_t_entry, 0x1800); kernel_start();
Fez
if (entries_count >= CPU_COUNT) { e2k_trap_handler(regs); ... } e2k_wait_all(); entries_count = __e2k_atomic32_add(1, &entries_count); if (entries_count > 1) { cpu_idle(); } e2k_kernel_start(); }
E, finalmente, vimos a reação de entrar na interrupção (apenas com a ajuda de printf, imprimimos uma linha).
Aqui, vale a pena explicar que, inicialmente, na primeira versão, esperávamos copiar a tabela de exceção, mas, em primeiro lugar, verificou-se que estava em nosso endereço e, em segundo lugar, não conseguimos fazer a cópia correta. Eu tive que reescrever os scripts do vinculador, o ponto de entrada no sistema e o manipulador de interrupções, ou seja, eu precisava da parte do montador, um pouco mais tarde.
É assim que a parte da parte modificada do vinculador de script agora se parece:
.text : { _start = .; _t_entry = .; *(.ttable_entry0) . = _t_entry + 0x800; *(.ttable_entry1) . = _t_entry + 0x1000; *(.ttable_entry2) . = _t_entry + 0x1800; _t_entry_end = .; *(.e2k_entry) *(.cpu_idle) }
isto é, removemos a seção de entrada da tabela de exceções. A seção cpu_idle também está localizada lá para as CPUs que não são usadas.
É assim que a função de entrada se parece com nosso kernel ativo, no qual a Embox será executada:
static void e2k_kernel_start(void) { extern void kernel_start(void); int psr; while (idled_cpus_count < CPU_COUNT - 1) ; ... e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE); kernel_start(); }
Bem, de acordo com a instrução VFDI, uma exceção foi lançada. Agora você precisa obter o número dele para garantir que esta seja a exceção correta. Para isso, a Elbrus possui registros de informações de interrupção TIR (registros de informações de interceptação). Eles contêm informações sobre os últimos comandos, ou seja, a parte final do rastreamento. O rastreamento é coletado durante a execução do programa e "congela" ao inserir uma interrupção. TIR inclui as partes baixa (64 bits) e alta (64 bits). A palavra baixa contém os sinalizadores de exceção e a palavra alta contém um ponteiro para a instrução que levou à exceção e o número TIR atual. Assim, no nosso caso, exc_d_interrupt é o quarto bit.
Nota Ainda temos algum mal-entendido em relação à profundidade (número) de TIRs. A documentação fornece:
“A profundidade da memória TIR, ou seja, o número de registros de informações de interceptação, é determinada
Macro TIR_NUM igual ao número de estágios do pipeline do processador necessários para
emitir todas as situações especiais possíveis. TIR_NUM = 19; "
Na prática, vemos a profundidade = 1 e, portanto, usamos apenas o registro TIR0.
Os especialistas do MCST nos explicaram que tudo está correto, e só haverá TIR0 para interrupções "precisas", mas para outras situações, pode haver algo mais. Mas, enquanto estamos falando apenas de interrupções no temporizador, isso não nos incomoda.
Ok, agora vamos ver o que é necessário para entrar / sair corretamente do manipulador de exceções. De fato, é necessário salvar na entrada e restaurar os 5 registros a seguir na saída. Três registros de preparação de transferência de controle são ctpr [1,2,3] e dois registros de controle de ciclo são ILCR (Registro de valores iniciais do contador de ciclo) e LSR (Registro de status do ciclo).
.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 getsp -(5 * 8), %dr0 std %dr1, [%dr0 + PT_CTRP1] std %dr2, [%dr0 + PT_CTRP2] std %dr3, [%dr0 + PT_CTRP3] std %dr4, [%dr0 + PT_ILCR] std %dr5, [%dr0 + PT_LSR] disp %ctpr1, e2k_entry ct %ctpr1
Na verdade, é tudo, depois de sair do manipulador de exceções, você precisa restaurar esses 5 registros.
Fazemos isso com uma macro:
#define RESTORE_COMMON_REGS(regs) \ ({ \ uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \ ctpr3 = regs->ctpr3, lsr = regs->lsr, \ ilcr = regs->ilcr; \ \ 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); \ })
Também é importante não esquecer, após a restauração dos registros, para invocar a operação DONE (Retorno do manipulador de interrupção de hardware). Esta operação é necessária, em particular, para processar corretamente as operações de transferência de controle interrompidas. Fazemos isso com uma macro:
#define E2K_DONE \ do { \ asm volatile ("{nop 3} {done}" ::: "ctpr3"); \ } while (0)
Na verdade, fazemos o retorno da interrupção diretamente no código C usando essas duas macros.
e2k_trap_handler(regs); RESTORE_COMMON_REGS(regs); E2K_DONE;
Interrupções externas
Vamos começar com como habilitar interrupções externas. Em Elbrus, o APIC (ou melhor, seu análogo) é usado como um controlador de interrupção; a Embox já tinha esse driver. Portanto, foi possível escolher um cronômetro para o sistema. Existem dois temporizadores, um muito parecido com o
PIT , o outro
LAPIC Timer , também é bastante padrão, por isso não faz sentido falar sobre eles. Isso e aquilo pareciam simples, e aquilo e aquilo já existiam na Embox, mas o driver do temporizador LAPIC parecia mais perspectiva, além da implementação do temporizador PIT nos parecia mais fora do padrão. Portanto, parecia mais fácil concluir. Além disso, a documentação oficial descreveu os registros APIC e LAPIC, que eram ligeiramente diferentes dos originais. Trazê-los não faz sentido, como você pode ver no original.
Além de permitir interrupções no APIC, você deve habilitar o tratamento de interrupções através dos registros PSR / UPSR. Ambos os registradores possuem sinalizadores para permitir interrupções externas e interrupções não mascaráveis.
Mas aqui é muito importante observar que o registro PSR é
local para a função (isso foi discutido na
primeira parte técnica ). E isso significa que, se você a definir dentro de uma função, quando você chamar todas as funções subseqüentes, ela será herdada, mas quando você retornar da função, ela retornará ao seu estado original. Daí a pergunta, mas como gerenciar interrupções?
Usamos a seguinte solução. O registro PSR permite ativar o gerenciamento através do UPSR, que já é global (o que precisamos). Portanto, habilitamos o controle via UPSR diretamente (importante!) Antes da função de login do núcleo da Embox:
asm volatile ("rrs %%psr, %0" : "=r"(psr) :); psr |= (PSR_IE | PSR_NMIE | PSR_UIE); asm volatile ("rws %0, %%psr" : : "ri"(psr)); kernel_start();
De alguma forma, por acaso, depois da refatoração, peguei e coloquei essas linhas em uma função separada ... E o registro é local para a função. É claro que tudo quebrou :)
Então, tudo parece estar ativado no processador, vá para o controlador de interrupção.
Como vimos acima, as informações sobre o número da exceção estão no registro TIR. Além disso, o 32º bit nesse registro relata que ocorreu uma interrupção externa.
Depois de ligar o cronômetro, alguns dias de tormento se seguiram, já que nenhuma interrupção pôde ser obtida. O motivo foi divertido o suficiente. Existem indicadores de 64 bits no Elbrus, e o endereço do registro no APIC entrou no uint32_t, por isso os usamos. Porém, se você precisar, por exemplo, converter 0xF0000000 em um ponteiro, não receberá 0xF0000000, mas 0xFFFFFFFFF0000000. Ou seja, o compilador expandirá seu sinal int não assinado.
Aqui, é claro, era necessário usar o uintptr_t, porque, como se viu, no padrão C99, esse tipo de conversão foi definido como a implementação definida.
Depois que finalmente vimos o 32º bit aumentado em TIR, começamos a procurar como obter o número de interrupção. Acabou sendo bastante simples, embora nem um pouco parecido com o x86, essa é uma das diferenças entre as implementações do LAPIC. Para Elbrus, para obter o número de interrupção, você precisa entrar no registro LAPIC especial:
#define APIC_VECT (0xFEE00000 + 0xFF0)
onde 0xFEE00000 é o endereço base dos registros LAPIC.
Acabou por pegar o temporizador do sistema e o temporizador LAPIC.
Conclusão
As informações fornecidas nas duas primeiras partes técnicas do artigo sobre a arquitetura Elbrus são suficientes para implementar interrupções de hardware e multitarefa preemptiva em qualquer sistema operacional. Na verdade, as capturas de tela fornecidas atestam isso.

Esta não é a última parte técnica sobre a arquitetura Elbrus. Agora estamos dominando o gerenciamento de memória (MMU) em Elbrus, esperamos falar sobre isso em breve. Precisamos disso não apenas para a implementação de espaços de endereço virtual, mas também para o trabalho normal com periféricos, porque, por meio desse mecanismo, você pode desativar ou ativar o cache de uma área específica do espaço de endereço.
Tudo o que está escrito no artigo pode ser encontrado no repositório da
Embox . Você também pode criar e executar, se é claro que existe uma plataforma de hardware. É verdade que um compilador é necessário para isso e só pode ser obtido no
MCST . A documentação oficial pode ser solicitada lá.