• Nenhum resultado encontrado

7 Sistemas operativos de tempo real

N/A
N/A
Protected

Academic year: 2021

Share "7 Sistemas operativos de tempo real"

Copied!
59
0
0

Texto

(1)

7 Sistemas operativos de tempo real

Os sistemas operativos de tempo real têm como objectivo garantir que o sistema tenha uma resposta a um acontecimento externo dentro de um intervalo de tempo limitado e previamente especificado.

Tal já não é garantido por um sistema operativo de tempo partilhado (por vezes também chamado de tempo virtual), onde o que se pretende é obter um bom tempo de resposta em termos médio, embora não previsível.

7.1 Modelo do núcleo

Qualquer núcleo de sistema operativo multi-tarefa deve disponibilizar 3 funções:

Escalonamento de processos ou tarefas (Scheduling) determina qual a tarefa seguinte que tomará conta do CPU (será executada)

Lançamento ou despacho de processos ou tarefas (Dispatching) Faz a comutação de contexto e outras operações necessárias para retirar uma tarefa de execução e substituir por outra.

Comunicação (e sincronização) entre processos ou tarefas

Quando o núcleo é referido, normalmente está-se a referir à menor parte do sistema operativo que disponibiliza estas funções. Mas um sistema operativo pode ser representado por camadas de abstracção desde o hardware até ao interface com as aplicações.

Dependendo das funções que são disponibilizadas, pode-se ter:

Um nano-kernel disponibiliza apenas o dispatching e a gestão de interrupções.

Um micro-kernel, adiciona o escalonamento das tarefas, de modo que se têm uma gestão de processos completa, onde a máquina física é multiplexada, de modo que cada processo pode ser visto como uma máquina virtual que executa um programa.

• Quando é disponibilizada a comunicação entre tarefas e a sua sincronização têm-se um kernel ou núcleo.

Um executivo já disponibiliza também um Sub-sistema de entradas/saídas (E/S) e um

Sub-sistema de ficheiros.

(2)

Finalmente um sistema operativo, disponibiliza também um interpretador de comandos ou shell, e um interface para que as aplicações possam aceder a funções de sistema (API). Na realidade esta API costuma existir sempre entre o núcleo, qualquer que este seja, e a shell ou outra aplicação. Pode no entanto em sistemas pequenos, fazer parte de um compilador (uma biblioteca) que gere aplicações para o sistema em causa.

Fig. 7-1: Camadas de um sistema operativo

Por vezes é distinguida mais uma camada no núcleo, responsável pela gestão de memória física e / ou virtual, entre a comunicação e sincronização de tarefas e a gestão de processos.

Se bem que hoje em dia sejam comuns os sistemas operativos multi-tarefa e multi-utilizador existem outros tipos de sistemas operativos, que podem ser utilizados para executar aplicações de tempo real. A próxima secção irá abordar estes vários tipos sendo os sistemas operativos multi-tarefa e multi -utilizador tratados numa secção posterior.

Controlo de processos (Interrupções, dispatching)

Nano-Kernel Escalonamento de tarefas

Micro-Kernel Comunicação e sincronização

entre tarefas

Kernel Sub-sistema de ficheiros

Sub-sistema de E/S

Executivo SHELL

Interface de sistema (API)

Sistema operativo

Hardware Aplicações

Gestão de

processos

(3)

7.2 Tipos de Sistemas operativos de tempo real

7.2.1 Sistemas Polled loop

Estes sistemas consistem no núcleo de tempo-real mais simples. Permitem rápidas respostas para periféricos isolados. Podem ser resumidos a um teste repetitivo a uma flag (que indica que um acontecimento qualquer sucedeu).

No exemplo seguinte quando flag é verdadeira indica que chegou um dado pacote de dados.

Este deve ser copiado para a memória do CPU, através de DMA, pelo procedimento process_data:

while TRUE do begin

if flag then

begin

process_data;

flag := FALSE;

end;

end;

Como só existe uma única tarefa não existe necessidade de escalonamento ou sincronização de tarefas.

Estes sistemas operam bem quando um único CPU é dedicado a tratar a entrada-saída de um periférico rápido, e não é comum sucederem eventos diferentes ao mesmo tempo.

7.2.2 Sistemas mono-tarefa

Neste caso têm-se um sistema operativo completo, incluindo um interpretador de comandos e possivelmente uma API, mas mais uma vez sem necessidade de escalonamento ou sincronização de tarefas, de modo que o núcleo se resume a gestão de memória e tratamento de interrupções. Possívelmente terá também um executivo que trate das entradas-saídas.

Exemplos de sistemas deste género são o CP/M e o MS-DOS, embora não sejam sistemas operativos de tempo-real. No caso do primeiro é composto principalmente por três secções:

• console command processor (CCP)

• basic input/output system (BIOS)

(4)

• basic disk operating system (BDOS)

No BIOS reside todo o código para efectuar E/S com os periféricos, incluindo os drivers e a gestão de interrupções. Pode-se considerar o BIOS como o núcleo e o BDOS como o executivo.

Embora existam um conjunto de chamadas de sistema, isto é chamadas ao BDOS, não existe uma API bem definida e nenhuma segurança em termos de acesso ao sistema, de modo que uma aplicação pode aceder directamente a todo o sistema, se bem que esse acesso tenha de ser programado num segmento de código em assembler.

Nestes sistemas, bem como é normal na maior parte, existem dois tipos de periféricos:

Sequenciais ou caracter onde os dados são transmitidos byte a byte.

(impressoras, teclados ...)

Bloco onde os dados são transferidos em blocos. No caso do CP/M 128 bytes / bloco.

(disk drives ...)

7.2.3 Sistemas foreground / background

Se bem que o tipo anterior de sistemas não permitam nativamente multi-tarefa, estes podem ser levados a executar outras aplicações em foreground ao mesmo tempo que executam uma

CCP

Aplicação

BDOS

BIOS

Hardware

(CPU, Disco, periféricos)

Utilizador

(5)

em background, se for desviado o vector de interrupção para que seja executado um dado segmento de código. Normalmente é desviada a rotina de tratamento do teclado, pois é um periférico lento, que é verificado periodicamente. Assim antes de processar o tratamento da interrupção poderá ser chamada uma ou um conjunto de rotinas.

Fig. 7-2: Tratamento de tarefas em background e foreground

A tarefa em backgroung segue o processamento normal, um ciclo infinito com aquisição de dados, actualização do display e algum tratamento da informação. Quando sucede uma interrupção de teclado é suspensa esta tarefa e a(s) tarefa(s) executada(s) em background.

Nos sistemas MS-Dos este tipo de aplicações são chamadas TSR (Terminate and stay resident). Alguns problemas podem suceder se se tentar utilizar o BIOS, sem desligar as interrupções. Além disso a mudança de contexto, deve ser efectuada pelo programador,

Background task loop forever

update display

user input

do something

Interrupt vector ...

Keyboard ...

keyboard handler handle keyboard RTI

Foreground task A do something

JMP keyboard handler

returns to Background task

(6)

guardando pelo menos os registos do processador antes de se chamar uma dada rotina que será executada em foreground.

Num sistema de tempo-real deste tipo, tarefas críticas em termos de tempo de execução, devem ser executadas em foreground.

7.2.4 Sistemas orientados por interrupções

Estes são sistemas multi-tarefa onde o programa principal é um ciclo infinito ou um jump to self:

SELF:

JMP SELF

As tarefas são escalonadas por interrupções de software ou de hardware, ao passo que o despacho é normalmente efectuado pelas rotinas de tratamento de interrupções (podendo no entanto também estar implementado em hardware). Estes sistemas são classificados quanto à ocorrência das interrupções como:

fixed-rate ou periódicos onde as interrupções são apenas periódicas

Esporádicos onde as interrupções são apenas aperiódicas

Híbridos onde as interrupções ocorrem tanto periodicamente como aperiodicamente

Sempre que uma dada tarefa é atribuída a um processador outra terá de ser retirada. Assim tem de ser guardada o estado da máquina, de modo que quando essa tarefa volte a ser atribuída ao CPU, possa continuar o processamento onde tinha parado. A este processo é chamada mudança de contexto.

O estado da máquina ou contexto da tarefa, é normalmente guardado numa pilha. Esta

operação é crítica para o desempenho do sistema, pois é efectuada constantemente. Em

sistema periódicos o time slice mínimo deve ser também ajustado de modo que a operação de

mudança de contexto não seja significativa quando comparada a este. A informação que deve

ser guardada e trocada deve ser mínima e normalmente refere-se aos registos do processador:

(7)

• Registos gerais do CPU

• Programa Counter

• Flags (registo de estado do CPU)

• Registos de Memoria (segmentos ou páginas)

• Registos do co-processador (FPU)

No entanto em alguns sistemas podem ser guardadas também ponteiros para as zonas de E/S mapeadas (memory mapped I/O) e outras variáveis especiais dependentes do sistema.

Esta operação é efectuada pelo despacho, de modo que está codificada nas rotinas de interrupção.

As interrupções devem ser desligadas durante esta operação, para garantir que todo o contexto é trocado.

7.2.4.1 Sistemas periódicos

Em sistemas round-robin os processos são executados sequencialmente até ao fim, de um modo cíclico. Se a cada tarefa for atribuído um quantum de tempo fixo ou time slice, estas detêm o CPU por esse tempo, após o qual são comutadas com a tarefa seguinte numa maneira circular.

Nestes casos é utilizado um relógio com um tempo fixo que inicia uma dada interrupção. As tarefas executáveis são colocadas numa fila circular:

Fig. 7-3: Fila de tarefas executáveis circular

de modo que o diagrama de activação é o seguinte:

T

0

T

1

T

2

... T

n

(8)

Fig. 7-4: Activação de tarefas segundo uma estratégia round-robin

7.2.4.2 Sistemas aperiódicos ou preemptivos

À possibilidade de uma tarefa de alta prioridade interromper outra de prioridade inferior chama-se preempção. Assim em sistemas orientados por interrupções, a prioridade de uma dada tarefa está associada à a prioridade de uma dada interrupção.

Fig. 7-5: Preempção de tarefas

Neste caso como a tarefa T

1

é mais prioritária que T

0

interrompe-a até que se complete. Após T

1

estar completa é que T

0

pode-se completar.

A prioridade das interrupções pode ser fixa ou dinâmica, tendo-se um escalonamento com a mesma designação.

T

0

T

1

T

0

t ...

pr io ri da de

T

0

T

1

T

2

T

n

T

0

T

1

t ...

o rd em d e atr ib u iç ão ao C P U

time slice

(9)

Exemplo:

Sistemas com prioridade dinâmica são úteis por exemplo em sistemas de gestão de ameaças, como os presentes num avião militar, onde são seguidos vários aviões inimigos, cada um por um processo. Periodicamente a ameaça relativa de cada avião inimigo é recalculada, baseada na proximidade e postura entre outros factores, de modo que as prioridades de cada processo indicam qual o avião inimigo mais perigoso.

7.2.4.2.1 Sistemas Rate-monotonic

Um tipo especial de sistema operativo de tempo real orientado por interrupções, preemptivo e com prioridade fixa é chamado Rate-monotonic. Nestes sistemas as prioridades são atribuídas de modo que quanto maior fôr a frequência de execução maior é a prioridade.

Exemplo:

Num sistema de navegação de uma aeronave, a tarefa que lê o acelerómetro cada 5 ms têm a maior prioridade e a tarefa que lê o giroscópio cada 40 ms tem a segunda prioridade mais alta. Já a tarefa que actualiza o display em cada segundo têm a prioridade mais baixa.

O escalonamento rate-monotonic representa o método óptimo para este tipo de sistemas com prioridade fixa (Liu e Layland, 1973) de modo que se não for possível obter uma estratégia de escalonamento bem sucedida, com nenhuma outra técnica de escalonamento com prioridade fixa se conseguirá.

Por outro lado, analises teóricas demonstram que se a utilização do CPU for menor que log 2, (este valor é obtido pela construção e análise de uma árvore de acontecimentos, daí o log 2) isto é aproximadamente 70%, todas as deadlines serão cumpridas.

No entanto esta análise não toma em conta casos práticos como o tempo necessário para a mudança de contexto, contenção (não disponibilidade temporária) de recursos, etc.

7.2.4.3 Sistemas Híbridos

Nestes sistemas as interrupções podem ser periódicas ou aperiódicas. As interrupções esporádicas são normalmente utilizadas para lançar uma tarefa que trate uma dada situação que requer atenção imediata, como um erro crítico.

Alguns sistemas híbridos comerciais são uma combinação de Round-robin e sistemas

preemptivos.

(10)

Estes sistemas são normalmente utilizados em sistemas integrados ou embebidos (embedded systems). Sistemas orientados por interrupções são fáceis de escrever pois o escalonamento pode ser todo efectuado por hardware, no entanto o jmt to self processado contínuamente pode consumir demasiado tempo e a implementação de serviços mais avançados é difícil.

São exemplo disso drivers mais complexos, interfaces com redes multi-camada, etc.

No entanto estes serviços estão disponíveis num sistema operativo de tempo-real multi-tarefa e multi-utilizador comercial.

7.3 Sistemas operativos de tempo real multi-tarefa e multi- utilizador

Nesta secção são tratadas principalmente as seguintes funções de um núcleo multi-tarefa e multi-utilizador de tempo-real:

• Gestão de tarefas (incluí gestão de interrupções)

• Gestão de memória

• Sincronização e comunicação entre tarefas (incluindo partilha de código e de periféricos)

Vão ser abordados sistemas simultaneamente multi-tarefa e multi-utilizador, no entanto não é obrigatório que um sistema operativo tenha ambas as características. A figura seguinte ilustra um sistema multi-utilizador:

Fig. 7-6: SO multi-utilizador SO multi-utilizador

Hardware Dados

Código

programa

Utilizador 0

Dados Código

programa

Utilizador n

(11)

Neste caso cada utilizador corre apenas um programa, como se detivesse todos os recursos do computador só para si. No entanto cada utilizador de alguma forma partilha o CPU, e o sistema operativo garante que cada utilizador têm um ambiente protegido de modo que um programa não interfere com o outro.

Já um sistema operativo também multi-tarefa, tem de garantir que cada utilizador pode lançar mais que um processo, que cada um destes tem um área de dados local, mas também pode comunicar com os outros processos, quer por um segmento de memória partilhada ou por mensagens.

Fig. 7-7: SO multi-tarefa

A figura seguinte mostra o diagrama de blocos de um sistema operativo de tempo-real multi- utilizador e multi-tarefa genérico, a partir de agora chamado apenas sistema operativo ou SOTR.

Dados 0 Código 0

Tarefa 0

Dados n Código n

Tarefa n

Utilizador n

Memória partilhada SO multi-tarefa

Hardware

mensagem

(12)

Fig. 7-8: SO de tempo real genérico

7.3.1 Gestão de tarefas

Normalmente o escalonamento é efectuado utilizando uma estratégia de round-robin e/ou preemptivo com prioridade, onde o sistema operativo consiste na tarefa com maior prioridade.

As tarefas são descritas um por TCB (task control block) ou descritor de processo. A informação que consta neste varia de sistema para sistema mas normalmente consiste em:

• ID (pid)

• Prioridade da tarefa (inteiro)

Estado da tarefa (Ready, active, suspended, existent)

• Contexto (PC, Registos do CPU, etc)

Shell Sub-sistema

de ficheiros

Tarefas de sistema Aplicação nível de

utilizador

Sub-sistema de E/S

Gestão de recursos (sincronização,

memória ...)

Gestão de processos (escalonamento e

despacho)

Rotinas de tratamento de

interrupções relógio de

tempo real nível de

sistema

nível de hardware

executivo

núcleo

(13)

• Localização (endereço de memória física quando carregada na RAM e endereço no disco ou página da memória virtual)

Portanto este descritor pode ser usado também para guardar o contexto, quando uma tarefa é retirada do CPU.

Normalmente o sistema tem filas (possivelmente circulares) que guardam os TCBs das tarefas activas, prontas, existentes e suspensas:

Fig. 7-9: Filas circulares para os estados das tarefas T

n

tail head Suspended

tail head Ready

tail head Active

tail

head

Existent

(14)

Deste modo ao passar uma tarefa de um estado para outro, como por exemplo de Ready para Suspended, é bastante rápido pois apenas é necessário actualizar os ponteiros de modo que seja passada de uma fila para o fim da outra

A forma como o estado das tarefas varia pode ser visto pelo seguinte diagrama, se bem que este não seja único, pois esta é uma parte crítica do SO sujeita a aprefeiçoamento:

Activa suspender ou

completada

Não existente Pronta

Existente Suspensa

suspender

desligar

desligar

ligar

destruir

criar desligar activar

despacho ou interrupção

Fig. 7-10: Diagrama de estados das tarefas para um SOTR típico

Assim uma tarefa pode estar em quatro estados:

Activa ou executável (active, running): A tarefa que tem o controlo do CPU.

Pronta (ready, runnable on): Numa fila pronta a ser executada. È movida para o

estado de activa pelo despacho, para ser executada durante um quantum de tempo, ou

por uma interrupção.

(15)

Suspensa ou bloqueada (suspended, blocked): Numa fila de processos suspensos, à espera de um determinado recurso (pode ter sido bloqueada por um mecanismo de sincronização, como por exemplo um semáforo).

Ou então por preempção (se um outra tarefa a interromper). Neste caso um par de primitivas (suspender, acordar) é utilizado para mover a tarefa entre a fila pronta e a fila de suspensa e para a activar. (estas primitivas só poderão ser chamadas por tarefas que têm algum privilégio sobre a tarefa a suspender, como tarefas de sistema.

No caso de tarefas de utilizador, estas só podem suspender outras tarefas do mesmo utilizador).

Existente ou dormente (existent, dorment, off): O sistema operativo sabe da existência da tarefa mas não lhe foi ainda atribuída uma prioridade e colocada na fila de executáveis ou pronta. Pode ser colocada ou retirada dessa fila com o par de primitivas (ligar, desligar).

Não Existente (terminated): O sistema operativo ainda não sabe da existência da tarefa, isto é, ainda não foi criado o TCB, no entanto esta pode estar residente na memória principal. Para criar um TCB é usada a primitiva (criar), passando a Existente. Para terminar uma tarefa e disponibilizar a memória (embora não necessariamente apagá-la) pode ser usada a primitiva (destruir).

Em alguns casos o núcleo de tempo real é preemptivo puro, isto é, não implementa a partilha do processador com base num quantum de tempo ou time slice. Assim uma tarefa só abandona o processador quando é se auto-suspende ou outra tarefa mais prioritária se tornou executável e a suspende.

7.3.1.1 Prioridades das tarefas

As prioridades das tarefas podem ser divididas em classes, que poderão também ser divididas em vários níveis de prioridade. Assim por ordem de prioridade mais elevada para a menor têm-se:

Interrupção Tarefas que requerem resposta muito rápida, na ordem do mili-

(16)

segundos. Como exemplo têm-se tarefas do SO como o dispatcher e o relógio de tempo real. Este último pode consistir apenas no incremento periódico de um dado contador na memória principal.

Como já se viu as interrupções forçam o re-escalonamento das tarefas, e o sistema não tem controlo sobre a altura destas. É assim necessário manter o processamento das rotinas de interrupção a um mínimo de modo que normalmente apenas preservam a informação necessária, e passam-na para uma rotina de interrupção de mais baixa prioridade, ao nível de relógio ou mesmo base.

As rotinas de interrupção devem preservar o contexto da tarefa interrompida. É pois comum que os registos do processador utilizados pela rotina de tratamento de interrupção sejam colocados na pilha quando se entra nela, e retirados desta imediatamente antes de se retornar da interrupção, isto é antes da instrução RTI.

Relógio Tarefas que são repetitivas ou com alturas de execução muito precisas, como por exemplo tarefas de amostragem e tarefas de controlo. Por vezes a prioridade do scheduler pertence a esta classe.

Podem-se dividir estas tarefas em:

Cíclicas: as que requerem sincronização precisa com o mundo exterior.

Para garantir esta sincronização quanto maior é a precisão requerida maior é a prioridade da tarefa.

Delay (atrasadas): As que desejam ter um atraso ou intervalo de tempo fixo entre sucessivas repetições, ou que pretendem atrasar o seu processamento por um dado período de tempo, para aguardar que um acontecimento externo se complete como por exemplo o fecho de um relé (tipicamente 20 ms)

Uma forma de implementar isto é colocar os TCBs destas

tarefas numa fila de espera por ordem da primeira a necessitar

(17)

de ser executada. Uma tarefa ao nível de prioridade de relógio verifica periodicamente se chegou a altura de executar a primeira tarefa da fila.

Fig. 7-11: Colocação de mais um TCB na fila delayed

Para isso consulta o relógio de tempo real, compara-o com o tempo previsto para a execução da primeira tarefa da fila delayed, que pode estar indicado no TCB, e se for altura de execução sinaliza-o ao despacho.

A medida de tempo utilizada é normalmente o tick, que corresponde a um impulso do relógio, e é o menor intervalo de tempo conhecido pelo sistema. No entanto numa linguagem de alto nível é comum indicar um atraso em segundos ou mili- segundos, que serão posteriormente convertidos para ticks.

Deste modo têm-se mais um estado de tarefas, normalmente chamado delayed. Estas tarefas circulam normalmente entre este estado e Ready.

Base Tarefas de baixa prioridade, que não têm deadline ou esta não é rígida.

São normalmente iniciadas por alguma pedido em vez de periódicas.

Este pedido pode ser devido a input por parte do utilizador, ou algum acontecimento externo.

t=2 t=5 t=8

tail head Delayed

t=6

verifica se é altura de

execução relógio de

tempo real sinaliza

despacho

(18)

7.3.2 Gestão de memória em tempo -real

A alocação dinâmica de memória pode ser efectuada quer explicitamente pelas necessidades de um programa, onde é responsabilidade do programador alocá-la e dealocá-la, ou implicitamente pelo SO, na criação de TCBs para tarefas novas e sua posterior eliminação, por exemplo.

No entanto, se bem que necessária, reduz o desempenho do sistema, bem como a análise de escalonamento. Neste último caso, qualquer alocação que pode por em causa o determinismo do sistema é chamada de perigosa. O determinismo em termos de eventos pode ser destruído, por exemplo, por um overflow da pilha. Já o determinismo em termos temporais pode ser destruído por uma situação de deadlock. Esta situação será tratada no ponto dedicado à sincronização de tarefas.

Assim é importante evitar alocação perigosa ao mesmo tempo que se deve reduzir a sobrecarga temporal introduzida pela gestão de memória dinâmica.

Neste ponto vão ser abordadas técnicas para gestão de memória utilizando uma pilha ou alocação dinâmica.

7.3.2.1 Mudança de contexto

Como já foi referido a mudança de contexto implica uma gestão de memória, embora transparente para o utilizador do SO. Esta operação pode ser efectuada guardando o contexto numa ou em várias pilhas. Esta aproximação é melhor para sistemas orientados apenas por interrupções e sistema foreground / background.

Já o modelo TCB é a melhor aproximação para sistemas multi-utilizador / multi-tarefa. Um desenvolvimento desta temática pode ser encontrado em (Baker, 1990).

No caso do modelo TCB as lista podem ser:

fixas Neste caso, na inicialização do sistema n TCBs são alocados dinamicamente,

e colocados na fila Existentes. À medida que são criadas tarefas são referenciadas

por estes TCBs que passam para a fila Pronta. Quando são eliminadas tarefas, os

TCBs voltam à fila Existente nunca existindo alocação dinâmica de memória durante

o funcionamento do SO.

(19)

dinâmicas Neste caso, cada vez que uma tarefa é criada é alocado dinamicamente, provavelmente a partir do heap, um TCB para a referenciar, sendo posteriormente colocado na fila Pronta. Quando a tarefa fôr eliminada, o TCB é retirado da fila onde se encontra quer seja Pronta, Activa, Suspensa ou Existente sendo depois esta memória dealocada.

7.3.2.2 Pilha

Como já foi visto, uma pilha pode ser usada para guardar o contexto ao se entrar numa rotina de interrupção. Esta mudança de contexto deve no entanto ser programada explicitamente na entrada da rotina de interrupção e imediatamente após se terem desligado as interrupções.

Este contexto deve ser recuperado antes de se terminar a rotina de interrupção, imediatamente antes de ligarem as interrupções:

Rotina de tratamento da interrupção n:

; Disable interrupts SAVE

...

RESTORE

; Enable interrupts

RTI

No segmento de código acima foram utilizadas duas rotinas para guardar e recuperar o contexto, SAVE e RESTORE, respectivamente.

Nestas duas rotinas o conteúdo de cada registo é colocado ou retirado da pilha. Alguns

processadores dispõe no entanto de instruções que permitem que um conjunto de n registos

possa ser guardado ou recuperado a partir de n posições contíguas na memória. No entanto

estas macro instruções podem ter sido desenhadas para serem interrompidas de modo que as

interrupções devem ser desligadas mesmo que seja possível mudar o contexto com apenas

uma desta macro instruções.

(20)

7.3.2.2.1 Escalonamento Round-robin

No caso de uma estratégia round-robin, onde o escalonamento é FIFO não é possível utilizar uma pilha (FILO) para guardar o contexto. Neste caso deve ser utilizada uma lista circular, também chamada de ring buffer.

7.3.2.2.2 Pilhas múltiplas

O tamanho máximo de uma pilha deve ser definido na inicialização do SO, de modo que deve ser conhecido à partida. Isto é possível se não fôr utilizada recursividade ou se não forem utilizadas estruturas de dados dinâmicas (no heap), onde os ponteiros para elas devem ser alocados na pilha.

Uma forma de tentar que a pilha satisfaça as necessidades é utilizar um esquema, onde cada tarefa têm uma pilha local. Embora o seu tamanho deva também ser definido à partida, pode ser efectuado de modo a satisfazer as necessidades de cada tarefa específica. Um ponteiro para estas pilhas deve ser guardado junto com o contexto da tarefa, na pilha principal do SO:

Fig. 7-12: Esquema de pilhas locais para 3 tarefas, onde a tarefa 1 detêm o CPU

Usando este esquema, mesmo que ocorra um overflow da pilha local à aplicação, ou tarefa, muito possivelmente esta terminará anormalmente indicando o sucedido com uma mensagem

Pilha principal (SO)

Pilha local tarefa 0 contexto tarefa 2

contexto tarefa 0

Pilha local tarefa 1

Pilha local tarefa 2

CPU

(21)

de erro. Mas se a pilha fosse comum ao sistema operativo, e nela fosse também necessário guardar contextos de tarefas, todo o SO poderia ficar instável sem que is so se torne aparente.

7.3.2.3 Alocação dinâmica

A alocação de memória dinâmica pode ser explicitamente efectuada por um programa, ou implícita, como é o caso da criação ou comutação de uma tarefa, onde é necessário arranjar espaço para o seu código, possivelmente para a sua área de dados (heap e/ou pilha), e em alguns casos também o TCB.

7.3.2.3.1 Alocação de memória explicita

Em linguagens de programação como o C, Pascal, Modula-2 e ADA, um processo pode em qualquer altura pedir uma alocação dinâmica de memória. Esta normalmente é efectuada por um pedido ao SO, é e alocada no Heap. Este pode consistir numa estrutura de dados baseada numa árvore binária, ou numa lista, onde existe informação para o início de cada segmento de memória disponível e o seu comprimento. Normalmente a ordenação é efectuada por endereços físicos ou por bloco maior.

Fig. 7-13: Fragmentação do heap

Quando é pedida um alocação de memória é procurado um bloco igual ou maior ao pretendido. Quando é libertada um área deve ser verificada se pode ser combinada com um

ocupado 3 K

ocupado 5 K

ocupado a)

ocupado

ocupado 5 K

ocupado ocupado

2 K

b)

1 k 5 K ocupado

ocupado ocupado

2 K

c)

8 K ocupado

ocupado ocupado

d)

(22)

bloco contíguo de modo a reduzir a fragmentação (daí a utilidade de uma ordenação por endereços fisicos).

No exemplo acima para um pedido de alocação de 1 KByte poder-se-ía escolher o bloco com 2 KBytes ou o com 10 KBytes. Se se escolher o bloco com 3K, vai ser ocupado 1 K, ficando dois livres como é mostrado em b). Supondo agora que o bloco de memória com 1 KByte ocupado era libertado ficar-se-ía na situação c). Como este é contíguo a dois outros, estes são combinados de modo a reduzir a fragmentação, como mostra d).

Os algoritmos utilizados não podem ser muito sofisticados, pois poderiam tornar-se demasiado lentos. Os mais comuns são:

Best-fit Procura um bloco com a dimensão mais próxima da pretendida por excesso. Se é encontrado um bloco exactamente idêntico, o que é difícil, este método é óptimo. No entanto se tal não suceder vão ficar disponíveis pequenas área de difícil aproveitamento.

Worst-fit Procura o maior bloco livre. A ideia é que este possa ser utilizado para futuros pedidos. No entanto assim esgotam-se primeiro os blocos maiores, podendo um pedido de um bloco grande não ser satisfeito.

Se existir uma ordenação por tamanho dos blocos o maior é sempre o primeiro facilitando a busca. Será no entanto necessária uma reordenação sempre que o primeiro deixe de ser o maior.

First-fit É escolhido o primeiro bloco com dimensão maior ou igual à pretendida encontrado. Pretende-se assim reduzir a busca, à custa de uma maior fragmentação no início da memória. Além disso não é necessário manter uma ordenação por dimensão dos blocos.

No entanto tende-se a ter uma fragmentação maior no início da memória, onde muitos blocos pequenos são sempre verificados, e provavelmente suficientemente grandes para satisfazer o pedido.

Next-Fit É uma modificação do anterior onde a busca de um bloco livre começa

onde terminou a anterior em vez de começar do início. Tenta evitar criar muitos

blocos pequenos no início da lista, mas espalha a fragmentação por toda a memória.

(23)

Existem alguns outros métodos de busca de segmentos no heap, mais elaborados, como por exemplo o buddy binário, sendo no entanto mais morosos.

De qualquer modo este esquema de alocação de memória dinâmica, se bem que em muitos casos necessário, leva a alguma imprevisibilidade.

Uma outra consequência de uma má gestão de memória é a formação de garbage ou lixo na memória. Este pode ser criado por uma tarefa que terminou sem libertar a memória alocada.

Uma forma de eliminar este lixo é incluir no heap informação sobre a tarefa que alocou determinado bloco, por exemplo o seu ID. Periodicamente uma tarefa de sistema verifica se existem blocos de memória alocados para tarefas já terminadas e se fôr esse o caso liberta -os.

Esta operação é chamada de garbage collection, e como é morosa deve ser efectuada em background ou atribuída uma prioridade baixa, de modo a não perturbar tarefas críticas (que obviamente devem ser executadas em foreground ou ter uma prioridade alta)

7.3.2.3.2 Alocação de memória implícita

Num sistema operativo multi-tarefa e multi-utilizador, é necessário alocar na memória disponível pelo menos o código da tarefa que está a ser executada pelo CPU. Se a memória não for suficiente para alocar todas as tarefas podem-se recorrer a vários esquemas, baseados no armazenamento temporário na memória secundária, usualmente o disco rígido. Assim este métodos são fortemente dependentes da velocidade de acesso do disco:

Transferência (swapping) O SO está sempre residente na memória principal.

Fig. 7-14: swapping Espaço do

utilizador

SO Tarefa 1 Memória principal

Tarefa 2 Memória secundária

contexto

Tarefa 2

(24)

Quando é necessário comutar a tarefa que usa o processador, todo o seu código e contexto é trocado com o da próxima tarefa a ser servida pelo CPU, e que está guardada na memória secundária.

Overlays Neste caso pretende-se que uma só aplicação, maior que a memória disponível seja executada. Neste esquema uma parte da aplicação, sempre residente na memória principal, chamada root contém código comum a todas as overlays. Estas residem na memória secundária e são trocadas com outra overlay na memória principal à medida que o seu código é necessário.

Fig. 7-15: Overlaing

Neste esquema é comum que tanto o segmento de código comum, root, bem como todas as Overlays estão sempre presentes na memória secundária como ficheiros independentes.

Memória paginada Segundo esta técnica um programa pode ser dividido em segmentos não contíguos, chamados páginas, com um tamanho constante de modo a

Espaço do utilizador

SO Overlay 1

Memória principal

Overlay 0 Memória secundária

Overlay 2 root

Overlay 1

root

(25)

evitar fragmentação. Estas páginas podem estar na memória principal ou na secundária, consistindo assim num esquema de suporte à memória virtual.

Fig. 7-16: Tradução de um endereço virtual num endereço físico

A memória virtual é assim composta pela principal e pela secundária, existindo um endereço virtual baseado numa página e num deslocamento, que deve ser convertido em físico. No entanto um programa só pode ser executado na memória principal, de modo que se o código necessário estiver numa página residente na memória secundária é gerada um excepção de falta de página ou page fault. Na tabela de páginas existe um campo que indica se está na memória principal, normalmente lógico e designado por presente, e o endereço de base onde se encontra o início da página se esta se encontrar na memória física.

Se existir espaço na memória principal a página é imediatamente chamada, se não é trocada com outra.

Página n Memória principal

Página m Página n+1 Página Deslocamento

Endereço virtual

Presente = 1 Tabela de

páginas

Base Presente = 0 Base

Página m-2 Memória secundária

Página m Página m-1

Falha de página

E n der eço f ísi co : Base + d esl oc am e nto

Memória virtual

(26)

Um dos melhores algoritmos utilizados para troca de páginas entre a memória principal e a secundária é o LRU (Least Recent Used) ou menos recentemente usado, que simplesmente troca a página menos recentemente usada, ou acedida pelo CPU. No entanto algum esforço deve ser efectuado para manter um registo de uso de páginas. Um algoritmo mais simples é o FIFO onde simplesmente a página que foi carregada para a memória principal há mais tempo é substituída, no entanto este esquema normalmente implica mais falta de páginas.

A técnica de paginação deve ser evitada em sistemas embebidos onde a sobrecarga pode ser demasiado elevada e não existir hardware que a suporte.

Pode-se no entanto arranjar uma forma de reduzir a imprevisibilidade e a sobrecarga decorrentes da paginação, obrigando que algumas páginas da tarefa, ou mesmo todas, permaneçam sempre na memória física. Esta facilidade é chamada memory locking.

Alguns SO comerciais, permitem que não só o código mas também os dados sejam mantidos na memória principal. Alguns permitem-no também para o segmento correspondente à pilha principal do SO. Deste modo à custa de alguma memória reduz-se os tempos de execução, para estas tarefas, pois a operação de paginação não é efectuada, mas mais importante ainda o seu tempo de execução pode ser previsto.

7.3.2.4 Partilha de código na memória principal

Em muitas aplicações de tempo real as mesmas acções devem ser efectuadas por tarefas diferentes. Uma solução é incluir estas operações numa biblioteca comum que depois é linkada com a aplicação. No entanto esta solução se bem que funcional implica um desperdício de memória principal, visto que código idêntico é replicado.

Outra solução consiste em ter a rotina partilhada guardada num só lugar, possivelmente uma biblioteca de linkagem dinâmica ou DLL, e ter um mecanismo que permita que mais que uma tarefa aceda a essa(s) rotina(s) p artilhada(s) sem interferências umas das outras.

Para ilustrar esta interferência suponha-se que uma tarefa A está a usar a sub-rotina

partilhada S, mas antes que esta sub-rotina termine, uma tarefa mais prioritária B, toma conta

do CPU e começa a utilizar esta mesma sub-rotina partilhada. Quando B termina volta a ser

executada a tarefa A, onde o PC aponta para o meio da sub-rotina S. No entanto qualquer

variável local de S tem valores relevantes para B ao passo que os de A forma perdidos.

(27)

Assim não existe garantia que os dados locais de S estejam correlacionados com a tarefa que a chama:

Fig. 7-17: Partilha de código não correlacionado

Existem pelo menos duas forma de resolver este problema:

Código série reutilizável

Fig. 7-18: Código série reutilizável Tarefa

A

Tarefa B

Sub-rotina S

Dados locais a S

i) Dados locais de S referentes a A

iii) Dados locais de S referentes a A reescritos com os referentes a B ii) A é suspensa por B

iv) A é resumida mas os dados locais de S referem-se a B

Tarefa A

Tarefa B Sub-rotina

S

Dados locais a S i) A acede a S

iii) B tem de aguardar que A liberte S antes de entrar

ii) A é suspensa por B

iv) A liberta S, B pode entrar em S

lock

unlock

(28)

A sub-rotina S é desenhada de modo que o valor de qualquer variável local, na entrada não tem influência nas acções da rotina. Pode-se por exemplo, se for o caso disso, eliminar a dependência de variáveis estáticas, isto é de chamadas anteriores à própria rotina S.

Depois deve ser usado um mecanismo de exclusão que garante que se S estiver a ser utilizado por uma tarefa só poderá ser utilizada por outra quando a primeira a libertar.

No exemplo acima, se se usar escalonamento preemptivo puro, se a tarefa B mais prioritária interromper a A, como não pode entrar na sub-rotina S fica bloqueada. No entanto como A só pode tomar uso do CPU quando B terminar, nunca o faz de modo que não pode terminar de usar e libertar a rotina S. Assim ambas as tarefas estão bloqueadas chegando-se a uma situação de deadlock. Esta situação será melhor tratada mais à frente neste capítulo.

Uma forma de evitar isto, mesmo num sistema preemptivo puro, uma tarefa prioritária que aguarda a disponibilização de um recurso deve ser retirada do CPU até que o recurso esteja disponível, e colocada possivelmente no início da fila Suspensa.

Se por outro lado se estiver a usar um esquema de partilha do processador por um quantum e ambas as tarefas têm a mesma prioridade, o deadlock não sucede, simplesmente B fica bloqueada, mesmo quando detém o CPU, enquanto A não terminar de usar S.

Código re-entrante A rotina S é codificada como código puro. Quaisquer resultados intermédios são guardados na memória local da tarefa que a chamou, ou na pilha local dessa tarefa. Para isso um ponteiro para os dados locais está guardado no TCB da tarefa.

Neste caso a rotina S é reentrante pois sejam quais forem as tarefa que lhe acedem, em qualquer momento, tem os seus dados preservados localmente e estão pois correlacionados.

È uma boa política programar os drivers utilizando código reentrante.

(29)

Fig. 7-19: Código re-entrante

7.3.2.5 Notas finais sobre a memória secundária

Num sistema de tempo real, um dos principais problemas nas E/S para o disco rígido (que normalmente constituí a memória secundária), consiste na fragmentação de ficheiro. È um problema análogo à fragmentação de memória, mas ainda pior, pois em adição à sobrecarga lógica para localizar a próxima unidade de alocação, existe também uma física mais significante e que tem a ver com o tempo de posicionamento da cabeça de leitura/escrita.

Uma forma de reduzir este problema, utilizada pelo Unix de tempo-real, é tentar forçar que todos os sectores de um ficheiro se sigam uns aos outros no espaço físico do disco. Esta técnica é chamada de alocação de ficheiros contígua.

No entanto outros problemas podem surgir. Se se tentar adicionar algo ao fim do ficheiro e não existir espaço contíguo, para manter todo este ficheiro contíguo terá de se encontrar uma zona no disco onde se possa colocá-lo contiguamente, o que envolve uma sobrecarga que não pode ser prevista à partida.

No entanto se se souber o tamanho máximo que o ficheiro pode ocupar, pode-se utilizar uma técnica de pré-alocação de sectores contíguos para o ficheiro, o que permite não só optimizar o acesso a este, mas também torná-lo previsível.

Como exemplo de ficheiros contíguos pré-alocados temos o swap file ou memória virtual do Windows. Já no caso do Linux esta é uma partição, que embora não seja um ficheiro, também é uma área de disco rígido pré-alocada e contígua.

Sub-rotina S (código puro)

O TCB indica os dados locais de cada tarefa

S usa memória local da tarefa que a chamou

Tarefa A (código) (dados) TCB - A

dados

TCB - B dados

Tarefa B

(código)

(dados)

(30)

7.3.3 Sincronização e comunicação entre tarefas

A programação em tempo real pode requerer multi-programação, para obter a velocidade de execução desejada. Se se dispuser de um sistema operativo multi-tarefa, pode-se ter aplicações que apenas são constituídas por uma tarefa, mas que concorrem com outras tarefas, mais que não sejam do próprio SO, ou então aplicações que são constituídas por várias tarefas, podendo essa ser criadas em tempo de execução.

De qualquer dos modos, uma tarefa ou processo é um programa sequencial em execução, podendo no entanto comunicar com outros processos.

Além disso a concorrência sobre recursos partilhados, como por exemplo impressoras, deve ser sincronizada entre as tarefas concorrentes.

7.3.3.1 Exclusão mútua e mecanismos de sincronização

Considere-se um sistema constituído por n processos concorrentes {P

1

P

2

...,P

n

}. Cada pro- cesso tem um segmento de código, denominado de região crítica, que deve ser executado em exclusão mútua, isto é, num dado instante, apenas um dos processos pode executar a sua região crítica.

Fig. 7-20: Escrita concorrente para a consola por duas tarefas num esquema round-robin Tarefa A

print "Eu sou a tarefa A"

Tarefa B

print "Eu sou a tarefa B"

Consola

Eu Eu sou sou a a tarefa tarefa A B

(31)

Considere-se como exemplo mais que uma tarefa escrever para um dado ficheiro, esteja ou não associado a um periférico. Estas tarfas devem ser sincronizadas de modo que cada uma escreva continuamente, mesmo que estas partilhem o processador num esquema de round- robin.

Se isto não for garantido cada tarefa escreve durante o quantum temporal que lhe está associado, sendo o resultado uma mistura de todos os trabalhos de escrita concorrentes.

Para assegurar a exclusão mútua da zona crítica esta pode estar envolvida em protocolos de entrada e saída:

CICLO

Protocolo de entrada;

Zona crítica;

Protocolo de saída

Zona não-crítica;

O problema da exclusão mútua consiste na implementação desses protocolos, satisfazendo as seguintes condições:

• não são feitas nenhumas restrições quanto às instruções e ao número de processadores que o hardware suporta. Assume-se no entanto que as instruções básicas em linguagem máquina são executadas atomicamente, isto é, se duas dessas instruções forem executadas simultaneamente, o seu resultado será sempre igual à sua execução sequencial, numa ordem qualquer;

• não são feitas suposições no que respeita à velocidade de execução dos n processos;

• quando um processo estiver numa secção não-crítica de código, não pode evitar que

qualquer outro processo entre na sua zona critica;

(32)

• quando houver vários processos a pretenderem entrar nas suas zonas críticas, a decisão sobre qual o processo a entrar não pode ser adiada indefinidamente.

Os mecanismos de sincronização não são apenas utilizados para resolver o problema da exclusão mútua; são utilizados em todas as situações que exigem cooperação entre processos. A exclusão mútua é, no entanto, um conceito que está normalmente presente em todos os problemas de sincronização.

7.3.3.1.1 Primitivas

Uma primeira solução correcta para este problema foi apresentada por um matemático holan- dês, T. Dekker, para um sistema com dois processos. Dijkstra estendeu a solução para n pro- cessos. Contudo, estes algoritmos são demasiado complexos para ter utilidade prática (Ben- Ben-Ari, 1982). Dado o seu carácter histórico e didático são encontrados em muitas publicações da especialidade, das quais se pode salientar (Dijkstra, 1965), (Peterson and Silberschlatz, 1983). Para resolver este problema, foram introduzidas primitivas (operações executadas atomicamente) de sincronização.

Para que estas primitivas sejam atómicas tem que se garantir que o sistema operativo não pode interromper uma tarefa antes da primitiva estar completa.

Supondo que se pretende verificar se uma dada tarefa pode entrar numa zona crítica, como por exemplo uma rotina de escrita para um periférico partilhado, tem de se verificar se este periférico está em uso. Se estiver terá de se aguardar:

CICLO

Enquanto TEST_AND_SET (S) = TRUE WAIT; "Protocolo de entrada"

Zona crítica;

Neste caso pode-se implementar o protocolo de entrada com uma primitiva TEST_AND_SET, onde uma dada variável, neste caso S, é testada. Assume-se que se S for verdadeira o periférico está a ser usado, mas se S for falsa, a tarefa terá que requerer o periférico para si de modo que terá de colocar S como verdadeira, atomicamente com o teste.

Normalmente isto envolve a verificação de um valor na memória e sua alteração. Em alguns

processadores têm instruções de código de máquina atómicas, por exemplo TANDS, que

(33)

fazem isto sem poderem ser interrompidas. A operação atómica de TANDS pode ser vista como:

• carrega uma palavra da memória

coloca numa flag, possivelmente a Zeros, o valor do bit de maior ordem

• Se o bit de maior ordem for 0

coloca-o a 1 e guarda a palavra na mesma posição de memória

Desta forma a implementação em código de máquina da primitiva TEST_AND_SET pode ser:

loop: TANDS S

JZ loop

Deste modo, enquanto a flag Zeros fôr 1, significa que a posição de memória S guarda um valor verdadeiro (tem o bit de maior ordem a 1) e o periférico está ocupado.

Quando Z = 0, pode-se entrar na região crítica, mas S contínua a ter o valor verdadeiro, agora colocado por TANDS.

Uma implementação desta primitiva, para máquinas que não tem instruções TANDS, como é o caso de processadores RISC, foi sugerida por (Dijkstra, 1968):

loop: ; disable interrupts MOV AX, S MOV S, TRUE

; enabel interrupts CMP AX, TRUE

JE loop

Assumindo que ao desligar as interrupções uma tarefa não pode ser retirada do CPU, e que

disable e enable interrupts devem ser instruções assembler atómicas.

(34)

Neste caso em S é sempre colocado o valor verdadeiro, para indicar que o periférico está ocupado (SET). Depois o valor de S, previamente movido para um registo é comparado para determinar se era verdadeiro ou falso (TEST). Se era verdadeiro volta a testar (WAIT)

Estas técnica de Test and Set é utilizada nas primitivas de outros mecanismos de comunicação, para garantir que são atómicas, como por exemplo as primitivas wait (s) e signal (s) utilizadas para semáforos que será abordado num ponto seguinte.

7.3.3.1.2 Event flags

Algumas linguagens fornecem estes mecanismos de comunicação. Na sua essência são interrupções simuladas criadas pelo programador. Levantar uma destas flags sinaliza ao SO que deve invocar o handler apropriado.

As tarefa que aguardam a ocorrência de um evento estão bloqueadas.

Em C / C++, por exemplo os sinais são um tipo de gestor de interrupções de software que é usado para reagir a uma excepção, indicada pela operação raise:

#include <signal.h>

void handler (int sig) {

/* trata o erro aqui. É definida com signal e chamada com raise*/

}

main() { ...

signal (SIGINT, handler) /* define handler para o sinal SIGINT */

...

if (error) raise (SIGINT); /*Erro detectado invoca handler e continua o processamento */

No exemplo acima se um erro for detectado a rotina handler deve tratar deste. O

processamento pode continuar ou a aplicação ser interrompida, dependente do código de

handler.

(35)

7.3.3.1.3 Semáforos

Com a introdução do semáforo por Dijkstra, o estudo da programação concorrente sofreu um avanço considerável. Sendo um mecanismo de sincronização fácil de implementar, é suficientemente poderoso para fornecer soluções elegantes a vários problemas de programação concorrente. Tornou-se um padrão, com o qual se comparam novos mecanismos e em função dos quais se constróiem outras.

Um semáforo S é uma variável inteira que pode tomar somente valores não-negativos, sendo as operações (de carácter atómico) de Wait(S) e Signal(S) as únicas operações permitidas em S, além da sua inicialização.

Quando um semáforo apenas pode tomar os valores 0 e 1, designa-se por semáforo binário ou simplesmente por semáforo; caso contrário é chamado de semáforo contador.

As operações de Wait(S) e Signal(S) aparecem frequentemente na literatura na forma de P(S) e V(S), respectivamente (primeiras letras das palavras em holandês), e os seus algoritmos mais utilizados são:

Wait ( S )

SE S > 0 ENTAO

S := S - 1

SENÃO

“suspende a execução do processo que evocou Wait(S), colocando-o numa fila de espera associada ao semáf oro S”

FIMSE

Esta primitiva deve ser utilizada na entrada da zona crítica, para testar se o recurso está disponível. Repare-se que se o contador do semáforo for maior que zero, S > 0, indica que um dos recursos pretendidos está disponível. Assim S normalmente toma o valor de recursos idênticos no sistema. Por exemplo se o sistema tivesse duas impressora Laser idênticas, inicialmente S tomaria o valor 2, indicando que ambas estão disponíveis.

Já no caso de semáforos binário, para cada impressora ter-se-ía um semáforo diferente que teria o valor 1 ou 0 indicando se a impressora correspondente está ou não ocupada.

Já ao sair-se da zona crítica deve-se libertar o recurso incrementando a variável do semáforo,

com:

(36)

Signal ( S ):

SE “algum processo P esta suspenso no semáforo S” ENTAO

“continua a execução de P”

SENÃO

S := S + 1

FIMSE

A definição de semáforo acima descrita não corresponde à definição clássica apresentada por (Dijkstra, 1968). Na operação de Wait(S) a “suspensão do processo” vem substituir um ciclo de “busy ...waiting" utilizado na definição clássica, bem como na operação de Signal(s) a versão de Dijkstra não garantia que algum dos processos saísse do ciclo de "busy ... waiting".

Nesta disciplina utiliza-se na definição de Signal(S) uma pseudo-instrução de "continua a execução de P". Entenda-se por "continua ... “ o levantamento da suspensão da execução de P, permitindo-se assim que, posteriormente, P entre na sua zona crítica.

Saliente-se que, se houver vários processos suspensões no semáforo S e se um outro processo executar Signal(s), nada se garante sobre qual dos processos dará continuidade à sua execução.

A solução para o problema da exclusão mútua é trivial usando semáforos. O código de cada processo passará a:

CICLO

Wait (S)

Zona critica

Signal (S);

Zona não-crítica

em que S é um semáforo binário, comum a todos os processos.

Embora os semáforos, constituam mecanismos eficazes de sincronização, passíveis de serem

usadas em vários esquemas arbitrários de sincronização, a sua utilização na programação de

aplicações complexas acarreta normalmente uma grande quantidade de erros. Isto porque as

operações de Wait e Signal têm de ser explicitamente programadas em todas as situações que

(37)

impliquem sincronização, em todo e qualquer processo. Uma simples troca das operações Wait e Signal para o mesmo semáforo, ou a aplicação duma destas operações para um semáforo s1 que devia ser aplicada a um semáforo s2 origina erros de difícil detecção.

7.3.3.1.4 Regiões críticas

Para evitar os erros de programação efectuados ao utilizar os semáforos nos problemas de exclusão mútua, (Hoare, 1972) propôs uma nova construção de linguagem, a região crí tica.

Se vários processos partilharem uma dada variável v, de tipo T, ela pode ser declarada:

VAR v : shared T

sendo a variável v acedida apenas dentro duma instrução de região com a seguinte forma:

region v do I

Esta construção faz com que, enquanto a instrução I estiver a ser executada, mais nenhum processo possa aceder à variável v. Desta maneira, se as duas i nstruções:

Tarefa A Tarefa B

region v do I1 region v do I2

forem executadas concorrentemente em dois processes distintos, o resultado será equivalente à execução sequencial de Sl seguida de S2, ou de S2 seguida de Sl.

Esta construção de linguagem pode ser implementada pel o compilador do seguinte modo:

i) para cada declaração: VAR v : shared T

O compilador gera um semáforo S

v

inicializado a 1;

ii) para cada instrução: region v do I

O compilador gera o seguinte código:

(38)

Wait (S

v

);

I;

Signal (S

v

);

As regiões criticas podem ser embricadas. Contudo, esta situação pode ocasionar deadlock.

7.3.3.1.5 Regiões condicionalmente críticas

A construção apresentada no parágrafo anterior resolve eficientemente o problema da zona crítica. Não pode, no entanto, ser usada para solucionar problemas mais gerais de sincronização. Por esta razão (Hoare, 1972) introduziu as regiões condicionalmente críti cas.

A principal diferença entre esta construção e a anteriormente expressa reside na instrução de região, que aqui tem a forma:

region v when B do I;

onde B é uma variável booleana. As regiões associadas com a mesma variável partilhada excluem-se mutuamente no tempo, tal como acontecia na construção anterior. Quando um processo entra na região crítica, a expressão booleana B é calculada. Se a expressão for verdadeira, a instrução I é executada; caso contrário a exclusão mútua é retirada (para que a região não fique inacessível a todos os outros processos), e o processo é suspenso até que B se torne verdadeiro e não exista nenhum outro processo na região crítica.

De salientar que existem outras definições de regiões condicionalmente críticas. Todas elas têm um problema comum, o de, caso existam vários processos suspensos à espera de que a sua expressão booleana B se torne verdadeira, sempre que um processo saia da região crítica, todas as expressões têm que ser recalculadas, o que se traduz numa sobrecarga para o sistema.

7.3.3.1.6 Monitores

A característica comum a todos os mecanismos de sincronização até agora referidos é a de

que cada processo tem que fornecer a sua sincronização explicitamente. A necessidade de

um mecanismo de sincronização que permitisse uma utilização mais estruturada levou

(Hansen, 1973) e (Hoare, 1974) a introduzir o conceito de monitor.

(39)

A ideia subjacente ao monitor advém das linguagens de programação e consiste em arranjar mecanismos de estruturação de dados e de acesso aos mesmos. Dados que são partilhados concorrentemente por vários processos são encapsulados num monitor, juntamente com os seus procedimentos de acesso. O monitor garante a suspensão dum processo que evoque qualquer um dos seus procedimentos, se outro processo estiver a executar dentro do monitor.

Deste modo, a exclusão mútua no acesso aos dados partilhados é conseguida automatica- mente, não havendo necessidade de o programador se preocupar com ela. Por outro lado, visto o acesso aos dados do monitor ser centralizado, desde que os algoritmos dos procedimentos de um monitor estejam correctos, todas as evocações dos seus procedimentos pelos vários processos estarão automaticamente correctas.

Já que o monitor centraliza o acesso a dados partilhados, consiste, além da declaração dos dados e do código relativo aos procedimentos de acesso, dum corpo, onde são efectuadas as inicializações necessárias.

Fig. 7-21: Monitor simples

No exemplo acima o acesso aos dados críticos só pode ser efectuado pelos procedimentos ReadData e WriteData, de modo que seja garantida automaticamente, pelo monitor, a exclusão mútua no acesso aos dados críticos.

Para poder ser usado em esquemas de sincronização arbitrários, o monitor fornece mecanismos adicionais de sincronização denominados condições. As únicas operações passíveis de serem executadas numa variável do tipo condição são:

dados críticos ReadData

WriteData

(40)

Wait ( c );

"Suspende a execução da tarefa que evocou esta operação (e que estava no interior do monitor) até que a condição c se verifique"

Signal ( c ) ;

SE "existem processes suspensos na condição c" ENTÃO

“suspende a execução do processo que evocou Signal ( c ) continua a execução de um dos processos suspensos em c";

FIMSE

Fig. 7-22: Monitor geral

No exemplo acima, existem três entradas no monitor que uma tarefa pode utilizar para aceder ao recurso, e duas condições onde tarefas que tenham ganho entrada podem ter de esperar.

A tarefa T15 está no monitor a aceder aos recursos. 3 tarefas estão à espera de entrada nos pontos de entrada. Duas tarefas T4 e T5 tinham entrado, mas foram suspensas à espera da condição A. (executando uma operação wait (A), esperando que a condição se verifique, possivelmente a libertação de um recurso)

As entradas e condições funcionam como filas de espera de modo que só uma tarefa possa entrar no monitor e aceder aos recursos.

recurso entrada 1

entrada 3 entrada 2

condição A

condição B T15

T2 T16

T5

T4 T6

(41)

De notar que a execução de Wait(c) deve implicar a libertação da exclusão mútua à entrada do monitor; não sendo assim, o sistema entraria em deadlock, uma vez que nenhum outro processo poderia executar Signal(c);

Há diferentes opiniões no que concerne ao algoritmo de Signal(c). Ilustre-se o problema com um exemplo: Suponha-se que, quando um processo P executa Signal(c), existe um processo Q suspenso na condição c. Se for permitido ao processo Q continuar a sua execução, então P deve ser suspenso; caso contrário estariam os dois activos dentro do monitor, o que contrariaria a sua definição. É um facto que um deles deve esperar, mas existem duas possibilidades:

a) P espera até que Q saia do monitor, ou que execute uma instrução de Wait noutra condição;

b) Q espera até que P saia do monitor, ou execute uma instrução de Wait noutra condição.

Existem argumentos a favor de uma ou da outra possibilidade. Todavia, deve ser salientado que se se optar pela solução b), a condição pela qual Q estava à espera pode não se manter válida na altura em que retoma a sua execução. Hansen adoptou uma situação de compro- misso: em cada procedimento existe no máximo uma operação de Signal, que deve ser forçosamente a última instrução do procedimento. Desta maneira, o processo que sinaliza sai do monitor imediatamente, resolvendo-se as ambiguidades. Esta solução torna, no entanto, o monitor menos maleável.

O conceito de monitor, quando usado com cuidado, pode conduzir a implementações muito eficientes (Lagally, 1979). Mesmo assim, apresenta alguns defeitos:

- Pode conduzir a deadlocks, se existirem monitores embricados (Peterson and Silberschlatz, 1983)

- Se se adoptar o algoritmo proposto neste texto para a operação de Signal, a

verificação da correcção de monitores torna-se difícil, dado que um processo

previamente suspenso por Wait, reactivado depois por uma operação de Signal, pode

novamente ser suspenso por uma operação de Wait ou de Signal numa outra condição

(Lagally, 1979).

(42)

7.3.3.1.7 Spooler

Fig. 7-23: Spooling de trabalhos de impressão ficheiro de

spool n

spooler tarefa 0

tarefa n

ficheiro de spool 0

: :

ficheiro de spool n-1 tarefa n-1

pedido n-1

pedido n ... ... pedido 0

tempo

FILA DE PEDIDOS

Impressora

ordem de impressão

c) impressão completa

d) retira pedido corrente da fila e passa a imprimir o

próximo a) imprimir para ficheiros temporários

independentes

b) adicionar pedido a

fila de impressão

Referências

Documentos relacionados

Visando a este cenário, o Ministério da Saúde criou o Programa de Educação pelo Trabalho para a Saúde (PET-Saúde), regulamentado pela Portaria Interministerial

A persuasão comportamental foi exercida através do ensaísmo e do tratadismo de seus adeptos; temas como a relação entre ciências naturais e sociais, o papel da Tradição

Engenharia de Cardápio. Segundo Rapp, o topo ou o canto superior direito da página 

´e aquele pelo qual a filosofia alem˜a traduziu, depois de Kant, o latim existentia, mas Heidegger deu-lhe um sentido muito particu- lar, j´a que designa na sua filosofia

Transformar los espacios es también transformar la dinámica de las relaciones dentro del ambiente de trabajo, la glo- balización alteró el mundo corporativo, en ese sentido es

Em seu trabalho, Marina Murta e Nathalia Gonçalves evocam o nome de um dos mais polêmicos artistas do século XX, que participou do despontar da Arte Moderna e de todas as

Possíveis danos para a saúde: Longo prazo - efeitos sistémicos Utilização final: Consumidores. Vias de

1º Fica renovado o reconhecimento dos cursos superiores de graduação constantes da tabela do Anexo desta Portaria, ministrados pelas Instituições de Educação Superior citadas,