Toda a verdade sobre o RTOS. Artigo 10. Agendador: recursos avançados e preservação de contexto



Em um artigo anterior, analisamos os vários tipos de planejamento suportados pelo RTOS e os recursos relacionados no Nucleus SE. Neste artigo, veremos as opções adicionais de planejamento no Nucleus SE e o processo de salvar e restaurar o contexto.

Artigos anteriores da série:
Artigo 9. Agendador: implementação
Artigo 8. Núcleo SE: Projeto Interno e Implantação
Artigo # 7 Núcleo SE: Introdução
Artigo 6. Outros serviços RTOS
Artigo 5. Interação e sincronização de tarefas
Artigo 4. Tarefas, alternância de contexto e interrupções
Artigo # 3 Tarefas e planejamento
Artigo 2. RTOS: estrutura e modo em tempo real
Artigo 1. RTOS: introdução.

Funcionalidades opcionais


Durante o desenvolvimento do Nucleus SE, tornei opcional o número máximo de funções, economizando memória e / ou tempo.

Suspender Tarefas


Conforme mencionado anteriormente no artigo Scheduler: Implementation , o Nucleus SE oferece suporte a várias opções para pausar tarefas, mas esse recurso é opcional e está incluído no símbolo NUSE_SUSPEND_ENABLE em nuse_config.h . Se definido como TRUE , a estrutura de dados é definida como NUSE_Task_Status [] . Este tipo de suspensão se aplica a todas as tarefas. A matriz é do tipo U8 , onde 2 petiscos são usados ​​separadamente. Os 4 bits inferiores contêm o status da tarefa:
NUSE_READY, NUSE_PURE_SUSPEND , NUSE_SLEEP_SUSPEND , NUSE_MAILBOX_SUSPEND etc. Se uma tarefa for suspensa por uma chamada de API (por exemplo, NUSE_MAILBOX_SUSPEND ), os 4 bits altos conterão o índice do objeto no qual a tarefa está suspensa. Essas informações são usadas quando o recurso fica disponível e para chamar a API, você precisa descobrir qual das tarefas suspensas precisa ser retomada.

Para executar a suspensão da tarefa, um par de funções do planejador é usado: NUSE_Suspend_Task () e NUSE_Wake_Task () .

O código NUSE_Suspend_Task () é o seguinte:



A função salva o novo estado da tarefa (todos os 8 bits), obtido como o parâmetro suspend_code. Quando você ativa o bloqueio (consulte "Bloquear chamadas de API" abaixo), o código de retorno NUSE_SUCCESS é salvo . Em seguida, NUSE_Reschedule () é chamado para transferir o controle para a próxima tarefa.

O código NUSE_Wake_Task () é bastante simples:



O status da tarefa é definido como NUSE_READY . Se o planejador de prioridade não for usado, a tarefa atual continuará ocupando o processador até chegar a hora de liberar o recurso. Se o agendador de prioridade for usado, NUSE_Reschedule () será chamado com o índice da tarefa como uma indicação de conclusão, pois a tarefa pode ter uma prioridade mais alta e deve ser executada imediatamente.

Bloquear chamadas de API


O Nucleus RTOS suporta várias chamadas de API com as quais um desenvolvedor pode pausar (bloquear) uma tarefa se os recursos não estiverem disponíveis. A tarefa será retomada quando os recursos estiverem disponíveis novamente. Esse mecanismo também é implementado no Nucleus SE e é aplicável a vários objetos do kernel: uma tarefa pode ser bloqueada em uma seção de memória, em um grupo de eventos, caixa de correio, fila, canal ou semáforo. Mas, como a maioria das ferramentas do Nucleus SE, é opcional e é definido pelo símbolo NUSE_BLOCKING_ENABLE em nuse_config.h . Se definido como TRUE , será definida a matriz NUSE_Task_Blocking_Return [] , que contém o código de retorno para cada tarefa; poderia ser NUSE_SUCCESS ou o código NUSE_MAILBOX_WAS_RESET , indicando que o objeto foi redefinido quando a tarefa foi bloqueada. Quando o bloqueio está ativado, o código correspondente é incluído nas funções da API usando a compilação condicional.

Contador do agendador


O núcleo RTOS calcula quantas vezes uma tarefa foi agendada desde que foi criada e redefinida pela última vez. Esse recurso também é implementado no Nucleus SE, mas é opcional e é definido pelo símbolo NUSE_SCHEDULE_COUNT_SUPPORT em nuse_config.h . Se definido como TRUE , é criada uma matriz de NUSE_Task_Schedule_Count [] do tipo U16 , que armazena o contador de cada tarefa no aplicativo.

Estado inicial da tarefa


Quando uma tarefa é criada no Nucleus RTOS, você pode selecionar seu estado: pronto ou pausado. No Nucleus SE, por padrão, todas as tarefas estão prontas na inicialização. A opção selecionada com o símbolo NUSE_INITIAL_TASK_STATE_SUPPORT em nuse_config.h permite selecionar o estado de inicialização. A matriz NUSE_Task_Initial_State [] é definida em nuse_config.c e requer a inicialização de NUSE_READY ou NUSE_PURE_SUSPEND para cada tarefa no aplicativo.

Salvando Contexto


A idéia de manter o contexto de uma tarefa com qualquer tipo de agendador, exceto o RTC (Executar até a conclusão), foi apresentada no artigo nº 3 “Tarefas e agendamento”. Como já mencionado, existem várias maneiras de manter o contexto. Como o Nucleus SE não foi projetado para processadores de 32 bits, optei por usar tabelas, não pilhas, para manter o contexto.

Uma matriz bidimensional do tipo ADDR NUSE_Task_Context [] [] é usada para salvar o contexto para todas as tarefas no aplicativo. As linhas são NUSE_TASK_NUMBER (o número de tarefas no aplicativo), as colunas são NUSE_REGISTERS (o número de registros que precisam ser salvos; depende do processador e está definido como nuse_types.h) .

Obviamente, manter o contexto e restaurar o código depende do processador. E este é o único código Nucleus SE vinculado a um dispositivo específico (e ambiente de desenvolvimento). Vou dar um exemplo do código de salvar / restaurar para o processador ColdFire. Embora essa opção possa parecer estranha devido a um processador desatualizado, seu assembler é mais fácil de ler do que os assemblies dos processadores mais modernos. O código é simples o suficiente para usar como base para criar uma opção de contexto para outros processadores:



Quando a alternância de contexto é necessária, esse código é chamado em NUSE_Context_Swap. Duas variáveis ​​são usadas: NUSE_Task_Active , o índice da tarefa atual, cujo contexto deve ser preservado; NUSE_Task_Next , o índice da tarefa cujo contexto você deseja carregar (consulte a seção Dados Globais).

O processo de preservação de contexto funciona da seguinte maneira:

  • Os registros A0 e D0 são armazenados temporariamente na pilha;
  • A0 está configurado para apontar para uma matriz de blocos de contexto NUSE_Task_Context [] [] ;
  • D0 é carregado usando NUSE_Task_Active e multiplicado por 72 (o ColdFire possui 18 registros, exigindo 72 bytes para armazenamento);
  • então D0 é adicionado a A0 , que agora aponta para um bloco de contexto para a tarefa atual;
  • então os registradores são armazenados no bloco de contexto; primeiro A0 e D0 (da pilha), depois D1-D7 e A1-A6 , depois SR e PC (da pilha, veremos a troca de contexto iniciada rapidamente) e, no final, o ponteiro da pilha é salvo.

O processo de carregamento de contexto é a mesma sequência de ações na ordem inversa:

  • A0 está configurado para apontar para uma matriz de blocos de contexto NUSE_Task_Context [] [] ;
  • D0 é carregado usando NUSE_Task_Active , incrementado e multiplicado por 72;
  • então D0 é adicionado a A0 , que agora aponta para o bloco de contexto da nova tarefa (como o carregamento do contexto deve ser feito no processo reverso de salvar a sequência, o ponteiro da pilha é necessário primeiro);
  • então os registradores são restaurados do bloco de contexto; primeiro, o ponteiro da pilha, PC e SR são empurrados para a pilha, D1-D7 e A1-A6 são carregados e no final de D0 e A0 .

A dificuldade na implementação da alternância de contexto é que o acesso ao registro de estado é difícil para muitos processadores (para ColdFire, esse é o SR ). Uma solução comum é a interrupção, ou seja, interrupção de programa ou interrupção condicional de ramificação, que faz com que o SR seja carregado na pilha junto com o PC . É assim que o Nucleus SE funciona no ColdFire. A macro NUSE_CONTEXT_SWAP () é definida em nuse_types.h , que se estende para:
asm ("armadilha nº 0");

A seguir está o código de inicialização ( NUSE_Init_Task () em nuse_init.c ) para blocos de contexto:



É assim que ocorre a inicialização do ponteiro da pilha, PC e SR . Os dois primeiros têm valores definidos pelo usuário em nuse_config.c . O valor de SR é definido como o caractere NUSE_STATUS_REGISTER em nuse_types.h . Para ColdFire, esse valor é 0x40002000 .

Dados globais


O agendador Nucleus SE requer muito pouca memória para armazenar dados, mas, é claro, usa estruturas de dados associadas a tarefas, que serão discutidas em detalhes nos artigos a seguir.

Dados RAM


O planejador não usa os dados localizados na ROM e de 2 a 5 variáveis ​​globais são colocadas na RAM (todas são definidas em nuse_globals.c ), dependendo do planejador usado:

  • NUSE_Task_Active - uma variável do tipo U8 que contém o índice da tarefa atual;
  • NUSE_Task_State - uma variável do tipo U8 que contém um valor que indica o status do código que está sendo executado no momento, que pode ser uma tarefa, um manipulador de interrupções ou um código de inicialização; os valores possíveis são: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT e NUSE_MISR_CONTEXT ;
  • NUSE_Task_Saved_State - uma variável do tipo U8 usada para proteger o valor de NUSE_Task_State em uma interrupção gerenciada;
  • NUSE_Task_Next - uma variável do tipo U8 que contém o índice da próxima tarefa, que deve ser agendada para todos os agendadores, exceto o RTC;
  • NUSE_Time_Slice_Ticks - uma variável do tipo U16 que contém um contador de fatias de tempo; usado apenas com o agendador TS.

Pegada de dados do planejador


O agendador do Nucleus SE não usa dados da ROM. A quantidade exata de dados de RAM varia de acordo com o planejador usado:

  • para RTC - 2 bytes ( NUSE_Task_Active e NUSE_Task_State );
  • para RR e prioridade - 4 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State e NUSE_Task_Next );
  • para TS - 6 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next e NUSE_Time_Slice_Ticks ).

Implementação de outros mecanismos de planejamento


Apesar do Nucleus SE oferecer uma escolha de 4 agendadores, cobrindo a maioria dos casos, a arquitetura aberta permite implementar oportunidades para outros casos.

Corte de tempo com a tarefa em segundo plano


Como já descrito no artigo nº 3, “Tarefas e agendamento”, o simples Agendador de fatia de tempo possui limitações, pois limita o tempo máximo que um processador pode executar uma tarefa. Uma opção mais sofisticada seria adicionar suporte para a tarefa em segundo plano. Essa tarefa pode ser agendada em qualquer slot alocado para tarefas em pausa e executada quando o slot for parcialmente liberado. Essa abordagem permite agendar tarefas em intervalos regulares e com uma porcentagem prevista do tempo de execução do processador.

Prioridade e Round Robin (RR)


Na maioria dos núcleos em tempo real, o agendador de prioridades suporta várias tarefas em cada nível de prioridade, ao contrário do Nucleus SE, em que cada tarefa tem um nível único. Dei preferência à última opção, pois ela simplifica bastante as estruturas de dados e, portanto, o código do agendador. Para suportar arquiteturas mais complexas, várias tabelas de ROM e RAM seriam necessárias.

Sobre o autor: Colin Walls trabalha na indústria eletrônica há mais de trinta anos, dedicando a maior parte de seu tempo ao firmware. Ele agora é engenheiro de firmware na Mentor Embedded (uma divisão da Mentor Graphics). Colin Walls frequentemente fala em conferências e seminários, autor de vários artigos técnicos e dois livros sobre firmware. Vive no Reino Unido. Blog profissional de Colin , e-mail: colin_walls@mentor.com.

Sobre tradução: essa série de artigos parecia interessante, pois, apesar das abordagens desatualizadas descritas em alguns lugares, o autor em um idioma muito compreensível apresenta ao leitor pouco treinado os recursos do sistema operacional em tempo real. Eu próprio pertenço à equipe de criadores do RTOS russo , que pretendemos liberar , e espero que o ciclo seja útil para desenvolvedores iniciantes.

Source: https://habr.com/ru/post/pt423967/


All Articles