9.1 Compartilhamento de recursos: exclusão mútua
Quando duas tarefas atualizam uma mesma variável (recurso), precisamos empregar uma técnica de exclusão mútua. O objetivo é assegurar que, uma vez que uma tarefa acesse um recurso compartilhado, esta tenha acesso exclusivo até concluir sua operação. Desse modo, é garantida a consistência dos dados.
Para entender a necessidade deste processo, considere o seguinte exemplo:
int global_var = 0;
void vTask( void *pvParameters )
{
for( ;; )
{
global_var++;
}
}
int main( void )
{
/* --- APPLICATION TASKS CAN BE CREATED HERE --- */
xTaskCreate( vTask, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate( vTask, "Task 2", 1000, NULL, 1, NULL);
/* Start the created tasks running. */
vTaskStartScheduler();
/* Execution will only reach here if there was insufficient heap to
start the scheduler. */
for( ;; );
return 0;
}
Com uma análise superficial, não é possível detectar nenhuma inconsistência no programa. No entanto, a seguinte instrução em C:
global_var++;
equivale a múltiplas instruções na CPU.
Vamos considerar que a operação incremento realiza os seguintes três passos:
- lê o valor da memória e armazena em um registrador (READ);
- incrementa o valor no registrador (INC); e
- escreve o valor do registrador na memória (WRITE).
Agora, imagine a seguinte sequência de operações das tarefas, que corresponde a duas iterações da tarefa 1 e uma da tarefa 2:
Tarefa | operação | registrador | global_var |
---|---|---|---|
Task 1 | READ | 0 | 0 |
Task 1 | INC | 0 → 1 | 0 |
Task 1 | WRITE | 1 | 0 → 1 |
Task 2 | READ | 1 | 1 |
Task 1 | READ | 1 | 1 |
Task 1 | INC | 1 → 2 | 1 |
Task 1 | WRITE | 2 | 1 → 2 |
Task 2 | INC | 1 → 2 | 1 |
Task 2 | WRITE | 2 | 1 → 2 |
Como podemos observar, o valor final de global_var
foi 2, quando deveria ser 3 após três incrementos.
A solução passa por realizar o incremento como uma operação atômica, ou seja, que não pode ser interrompida.
Para solucionar este problema, podemos utilizar um mutex, que funciona como uma fechadura controlando o acesso ao recurso. Quando uma tarefa deseja acessar o recurso, ela "trava" o mutex, impossibilitando outras tarefas de acessá-lo.
No FreeRTOS, mutex podem ser criados com a função xSemaphoreCreateMutex
, "travados" com a função xSemaphoreTake()
e destravados com xSemaphoreGive()
.
Quando alguma outra tarefa deseja acessar um recurso travado por um mutex, ela entra no estado bloqueado até a liberação do mutex ou um timeout.
Este procedimento é ilustrado na Fig.:
Assim, o programa anterior pode ser reescrito com:
#include <stdio.h>
#include <stdlib.h>
/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
int global_var = 0;
SemaphoreHandle_t xMutex;
void vTask( void *pvParameters )
{
const TickType_t xMaxBlockTimeTicks = 0x20;
for( ;; )
{
xSemaphoreTake( xMutex, portMAX_DELAY ); // will block if mutex is already taken
global_var++;
printf("%d\n", global_var);
xSemaphoreGive( xMutex );
vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
}
}
int main( void )
{
xMutex = xSemaphoreCreateMutex();
/* Check the semaphore was created successfully before creating the tasks. */
if( xMutex != NULL ) {
/* --- APPLICATION TASKS CAN BE CREATED HERE --- */
xTaskCreate( vTask, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate( vTask, "Task 2", 1000, NULL, 1, NULL);
/* Start the created tasks running. */
vTaskStartScheduler();
}
/* Execution will only reach here if there was insufficient heap to
start the scheduler. */
for( ;; );
return 0;
}
onde foi adicionado um delay aleatório para simular acessos não regulares ao recurso.
9.2 Sincronização entre tarefas com semáforo binário
O semáforo binário é bastante similar ao mutex, pois possui dois estados e faz o bloqueio da tarefa caso não consiga obtê-lo.
No FreeRTOS, o semáforo binário é utilizado para sincronizar tarefas. Neste caso, a tarefa a ser executada fica em estado bloqueado esperando o semáforo. Outra tarefa é encarregada de "fornecer" o semáforo, sinalizando para aquela que deve iniciar o processamento. É mais fácil compreender esta situação a partir da Fig.:
O programa abaixo contém um exemplo de uso de um semáforo binário para sincronização:
#include <stdio.h>
/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#define BEEP_COUNT 5
SemaphoreHandle_t xBinarySemaphore;
void vTask1( void *pvParameters )
{
volatile char buf[10];
for( ;; )
{
buf[0] = '\0';
scanf("%s", buf);
if(buf[0]=='b')
xSemaphoreGive(xBinarySemaphore);
}
}
void vTask2( void *pvParameters )
{
volatile uint32_t ul;
const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
for( ;; )
{
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
for(ul = 0; ul < BEEP_COUNT; ul++) {
printf("Beep!\n");
vTaskDelay(xDelay1000ms);
}
}
}
int main( void )
{
xBinarySemaphore = xSemaphoreCreateBinary();
if(xBinarySemaphore != NULL) {
/* --- APPLICATION TASKS CAN BE CREATED HERE --- */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, NULL);
/* Start the created tasks running. */
vTaskStartScheduler();
}
/* Execution will only reach here if there was insufficient heap to
start the scheduler. */
for( ;; );
return 0;
}
A tarefa 1 espera por uma entrada de teclado, enquanto a tarefa 2 fica em estado bloqueado.
Caso seja digitado b
, o semáforo é sinalizado e, com isso, a tarefa 2 sai do bloqueio e entra em execução, pois possui prioridade mais alta.
Após a execução, a tarefa 2 se bloqueia novamente e a tarefa contínua 1 entra em execução.
9.3 Criando uma fila de eventos para a aplicação Alarme
A fila do FreeRTOS permite combinar a sincronização com o compartilhamento de recursos. O seu funcionamento é similar ao de uma fila FIFO (first in, first out) tradicional.
Em geral, uma (ou mais) tarefa(s) é(são) responsável(is) por inserir itens no final da fila e outra(s) por remover itens do início dela. Assim como no caso do semáforo binário, a tarefa pode esperar em estado bloqueado caso não exista item na fila.
Vamos adaptar nossa aplicação alarme para utilizar uma fila de eventos para a máquina de estados.
Para declarar a fila, primeiro precisamos incluir os headers apropriados e criar um handler do tipo QueueHandle_t
com:
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
QueueHandle_t xQueue;
Serão definidas duas tarefas: a primeira gera os eventos e põe na fila, e a segunda é a máquina de estados que consome e processa esses eventos da fila. A primeira tarefa pode ser escrita como:
void taskObterEvento(void *pvParameters) {
int codigoEvento;
BaseType_t xStatus;
for( ;; ) {
codigoEvento = NENHUM_EVENTO;
teclas = ihm.obterTeclas();
if (decodificarAcionar()) {
codigoEvento = ACIONAR;
xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
if( xStatus != pdPASS )
Serial.println("Erro ao enviar evento para fila");
continue;
}
if (decodificarDesacionar()) {
codigoEvento = DESACIONAR;
xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
if( xStatus != pdPASS )
Serial.println("Erro ao enviar evento para fila");
continue;
}
if (decodificarTimeout()) {
codigoEvento = TIMEOUT;
xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
if( xStatus != pdPASS )
Serial.println("Erro ao enviar evento para fila");
continue;
}
if (decodificarDisparar()) {
codigoEvento = DISPARAR;
xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
if( xStatus != pdPASS )
Serial.println("Erro ao enviar evento para fila");
continue;
}
}
}
onde a função xQueueSendToBack
é usada para enviar o evento para a fila.
Já a tarefa de máquina de estados é alterada para:
void taskMaqEstados(void *pvParameters) {
int codigoEvento;
for( ;; ) {
if( xQueueReceive( xQueue, &codigoEvento, portMAX_DELAY ) == pdPASS ) {
if (codigoEvento != NENHUM_EVENTO)
{
codigoAcao = obterAcao(estado, codigoEvento);
estado = obterProximoEstado(estado, codigoEvento);
executarAcao(codigoAcao);
printf("Estado: %d Evento: %d Acao:%d\n", estado, codigoEvento, codigoAcao);
}
}
else {
printf("Erro ao receber evento da fila\n");
}
}
}
Por fim, devemos inicializar a fila e as tarefas no main()
:
int main() {
iniciaSistema();
printf ("Alarme iniciado\n");
// configure tasks
xQueue = xQueueCreate(5, sizeof(int));
if(xQueue != NULL)
{
xTaskCreate(taskMaqEstados,"taskMaqEstados", 1500, NULL, 2, &xTaskMaqEstados);
xTaskCreate(taskObterEvento,"taskObterEvento", 1000, NULL, 1, &xTaskObterEvento);
vTaskStartScheduler();
}
else
{
/* The queue could not be created. */
}
} // main
Note que, como a prioridade da tarefa taskMaqEstados
é a maior, a fila nunca terá mais de um elemento.
Isto porque, uma vez inserido algum evento nela, a tarefa da máquina de estados é imediatamente desbloqueada e executada, consumindo o evento.
Depois de processar o evento, a tarefa prioritária entra em estado de bloqueio e a taskObterEvento
volta a executar.
A aplicação final do Alarme no FreeRTOS pode ser consultada em
https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_freertos
Bibliografia para este capítulo
- Código fonte do alarme no Arduino (com FreeRTOS): https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_freertos
- Tutorial e referência do FreeRTOS: https://www.freertos.org/Documentation/RTOS_book.html