Atividade-desafio 2

Micro-Kernel de Tempo Real

Editado em 7 Out 2003

Atenção: esta atividade é opcional, estritamente individual, e substitui a prova. Deve ser entregue até o dia da prova

O objetivo desta atividade é implementar um Micro-Kernel de Tempo Real para o 8086.

Sistemas embarcados usualmente possuem um micro-kernel que controla as tarefas concorrentes do sistema. O micro-kernel faz o papel de um sistema operacional muito simples: cria e escalona tarefas (processos), provê mecanismos de sinalização entre tarefas, etc. Quando o micro-kernel responde a eventos com rapidez ele se diz de tempo real.
Se o escalonamento das tarefas é feito através de um relógio de tempo real (hardware que interrompe o processador a intervalos de tempo regulares) o micro-kernel se diz preemptivo (pois pode interromper uma tarefa a qualquer instante e passar o controle da CPU para outra tarefa). Caso contrário ele é dito não-preemptivo e uma tarefa em execução libera o uso do processador através de uma função especial do sistema (t_yield).
O escalonamento de tarefas pode usar prioridades ou ser bastante simples como o escalonamento circular (em inglês round-robin), que ciclicamente dá o controle da CPU a cada tarefa em condições de executar. Será o caso desse projeto. Se as tarefas compartilham o mesmo espaço de endereçamento elas são também chamadas de threads ou processos leves. Nesse caso elas têm acesso aos mesmos dados e conflitos no seu acesso podem aparecer, requerendo técnicas de exclusão mútua entre tarefas para acesso a dados compartilhados (também ditas de implementação de Regiões Críticas). Este é o caso, por exemplo, das funções int 21h do DOS, que não podem ser usadas simultâneamente por duas tarefas.

Tarefas de sistemas embarcados usualmente nunca terminam; ficam num laço à espera de um evento sobre o qual devem atuar. No nosso caso vamos permitir o término de uma tarefa.
Uma thread no nosso caso é simplesmente uma rotina que possui sua própria pilha (e que pode chamar outras rotinas).

V. deve programar uma biblioteca de rotinas que permitam implementar threads num sistema com escalonamento não-preemptivo (infelizmente a máquina virtual que emula o DOS no Windows não permite o controle de interrupções, necessário à implementação de escalonamento preemptivo, que é muito mais poderoso do ponto de vista prático e mais interessante também).

No sistema que V.vai implementar haverá um número máximo prefixado de threads, NT, (é conveniente que seja uma potência de 2, pois facilita o cálculo de índices), e espaço em memória para controle das threads será pre-alocado para esse número.
Uma thread será identificada por um índice (tid) >=0 e < NT.
Apenas duas tabelas e algumas variáveis auxiliares serão necessárias para implementar a biblioteca:

A variável ctask conterá o identificador da thread (tid) correntemente em execução (pode ser conveniente manter num vetor os tids das threads na ordem em que foram criadas).
Para implementar exclusão mútua usando semáforos (locks) V. precisará de um vetor sem contendo NSEM entradas (um parâmetro do sistema). As operações sobre semáforos, t-lock e t_unlock serão detalhadas a seguir.

As seguintes funções básicas fazem parte da bibioteca:

Funções auxiliares: a função time, vista em aula, devolve em dx,ax (high, low) o valor do contador de ticks do relógio de tempo real (RTC) do PC. Este contador contém o número de ticks do relógio desde meia noite (um tick a cada 50,3 ms; 18 ticks correspondem a 1 segundo).

Devido ao fato de o sistema ser não preemptivo, V. poderá depurá-lo com o turbo-debugger. É relativamente simples tranformá-lo num micro-kernel preemptivo: a função t_yield passaria a ser a rotina de interrupção do relógio de tempo real!. As outras funções da biblioteca não teriam alteração alguma, (exceto t_create) mas elas devem executar com interrupções desablitadas por razões óbvias (este é o problema básico da máquina virtual DOS que o Windows 2000/NT fornece: não é possivel desabilitar interrupções). As instruções cli e sti são inóquas e V. não deve usá-las, pois cli por alguma razão misteriosa atrasa a saída de mensagens enviadas via int 21h. V. poderia fazer o micro-kernel numa máquina com o velho sistema DOS, ou talvez num Windows mais antigo.
Descobri, no entanto, um truque (sujo) para implemetar um micro-kernel preemptivo no ambiente Windows NT/2000. De qualquer forma é importante implementar primeiro o micro-kernel não preemptivo, pois V. poderá depurá-lo com o turbo debugger ao contrário do preemptivo, especialmente as funções t_create e t_yield que são as mais críticas.
Se V. chegar ao final desta atividade darei a dica de como transformar o seu micro-kernel num micro-kernel preemptivo com pouquíssimas mudanças!
A título de ilustração, abaixo transcrevo algumas funções do nosso micro-kernel preemptivo.

   Org 100h
   ;aqui definição das constantes básicas acima mencionadas
   
   call init       ; create idle task and start RTClock
   call debug      ; send a numbered message to video
   mov ax,[ct]     ; ct is incremented at each interrupt (my debug help)
here:              ; check if RTC interrupt is enabled
    call t_yield   ; let idle task run
    cmp ax, [ct]
    jz here
   call debug      ; interrupt is working,for  we came out of the loop
   mov dx,task1
   call t_create   ; creates 1st task
   mov [tid],di    ; save tid of task created
   mov dx, task2   ; creates 2nd task
   call t_create
   mov [tid+2], di ; save tid of new task
   mov si, [tid]
   call t_join     ; waits for task1 to end
   call debug
   mov si, [tid+2]
   call t_join     ; and now waits for task2
end0:
   call debug
end:
    call restint        ; restore original 1ch interrupt vector
    mov ah, 4ch         ; back to DOS!
    int 21h
;________________________________________________________________
debug:
    inc byte[msg]       ; trick to number a standard message
    mov dx, msg
    call prtmsg			; prints message proteced by semaphore
    ret
;_____________________________________________________
task1:			         ; my first task									  		
    mov ax,4
my0:
    push ax
    mov dx,task1msg
    call prtmsg
    mov cx,18
    call t_sleep
    pop ax
    dec ax
    jnz my0
    call t_exit	 ; finish task
    jmp $        ; should never come here!
;__________________________________________________________________

task2:				    ; second task											   
    mov cx,18
    call t_sleep        ; sleep for 1 second
    mov dx, task2msg
    call prtmsg         ;show message protected by semaphore
    call t_exit         ; finish this task
    jmp $               ; should never come here
;********************************************************************
prtmsg:                 ; show message via DOS int 21h, protected by semaphore 0
                        ; input: dx= message address
    mov ax,1
    lock xchg ax, [sem] ; check semaphore: 1 = locked
    or ax,ax
    jz free
    call t_yield        ; semaphore locked, try again later
    jmp prtmsg
free:                   ; now we hold the semaphore
    mov ah,09           ; send message to video
    int 21h
    mov word[sem],0     ; unlock semaphore
    ret
;***********************************************************************

t_idle:                 ; idle task
    call t_yield        ; do nothing but allow others to run!
    jmp t_idle

;***********************************************************************
t_lock:              ; locks semaphore 
                     ; input bx = semaphore to lock
                     ; output: none
lock0:
    mov  ax, 1       ; 1: locks semaphore, 0: semaphore is unlocked
    lock xchg ax, [sem+bx] ; individible: locks bus in a multiprocessor system
    or ax, ax        ; if ax is 0
    jz lock1         ; then semaphore was uncloked
    call t_yield     ; else was locked by other task, release processor
    jmp lock0        ; and try again
lock1:
    ret
;*********************************************************************
t_unlock:               ; unlock semaphore bx
    mov word[sem+bx],0
    ret

;*********************************************************************