Apostila SO1
Apostila SO1
Apostila SO1
2 HISTÓRICO .............................................................................................................. 20
4 PROCESSOS.............................................................................................................. 43
Mas afinal, se é importante para as pessoas a existência de bons softwares que aju-
dem nos seus trabalhos, como pode o sistema operacional influenciar na qualidade
e na disponibilidade de tais softwares?
É justamente neste segundo item que os sistemas operacionais podem ser bem su-
cedidos ou não, em despertar interesse para que a indústria de software e os pro-
gramadores independentes construam programas para determinados sistemas ope-
racionais. Isto justifica parte do sucesso do Microsoft Windows, pois, ao mesmo
tempo que ele provê uma interface bastante amigável com o usuário, para o progra-
mador, não é tão difícil criar um programa com janelas, botões, listas, etc, como se-
ria num sistema operacional como o MS-DOS. Além disso, os sistemas operacionais
da Microsoft rodam no hardware mais “popular” hoje em dia: os computadores base-
ados em IBM PC.
Microprogramação Hardware
Dispositivos Físicos
O que é muito importante observar quanto ao software básico é que, apesar de que
editores (ex: bloco de notas do Windows), compiladores (ex: compilador C no Unix),
e interpretadores de comando (ex: command.com ou explorer.exe no Windows)
normalmente serem instalados junto como sistema operacional em um computador,
eles não são o sistema operacional. Eles apenas utilizam o sistema operacional.
Portanto, o shell que normalmente usamos em um sistema operacional nada mais é
do que um programa que utiliza serviços do sistema operacional, mas com a finali-
dade de permitir que os usuários realizem suas tarefas mais freqüentes: executar
programas e trabalhar com arquivos.
São os mais antigos, pois apareceram por volta de 1956, na época dos cartões per-
furados. Pode-se dizer que seu surgimento se deveu ao uso de alguns cartões de
controle.
Sua principal característica é a falta de qualquer, tipo de interação com o usuário. As
tarefas, ou "jobs", são agrupadas conforme seu tipo, geralmente determinado pela
linguagem-fonte, e executadas sequencialmente uma-a-uma.
Sua finalidade é minimizar o tempo ocioso de CPU e de periféricos, devido ao seu
elevado custo, à custa de grandes tempos de resposta. Os tempos do processamen-
to (médios) são razoavelmente pequenos, já que o n° de jobs/unidade de tempo
Ao contrário dos sistemas batch, cujos longos tempos de resposta são um desincen-
tivo ao desenvolvimento e à criatividade, nos sistemas T.S. a interação com ò usuá-
rio é freqüente, à custa de tempos de processamento mais longos. Os pequenos
tempos de resposta necessários a essa interação constante exigem que periodica-
mente o sistema operacional seja executado, expulsando o processo corrente da
CPU, o que é chamado de time-slicing, ou concorrência.
Diversos algoritmos, que serão vistos posteriormente, visam melhorar a eficiência do
time-slicing,.dedicando, por exemplo, o tempo de processamento de um usuário ao
processamento de tarefas de outro. Isto permite grande realocação de recursos, in-
clusive memória e CPU, minimizando a ociosidade.
Em geral todos esses serviços são prestados com rotinas de nível muito baixo, tipi-
camente linguagem Assembly. Sua execução segue um ciclo do tipo espera, inter-
preta, executa, responde, espera, interpreta, executa, responde...
É evidente que alguns desses objetivos são bastante difíceis de se alcançar, princi-
palmente em conjunto. Na verdade o objeto principal de nosso estudo é compreen-
der exatamente os problemas que surgem quando tentamos conciliá-los.
b) Objetivos do Ponto de Vista do Usuário
Ao contrário dos objetivos do SO, que se referem ao que ele deve ser propor a reali-
A implementação dessas chamadas pode ser feita na forma de instruções SVC tipo
onde define o serviço solicitado (e pode ser operando imediato da instrução, ou um
registrador da CPU como operando ou como ponteiro).
Para copiar um arquivo para outro, por exemplo, são necessárias duas chamadas
para ler os nomes dos arquivos no teclado, duas chamadas para abri-los (supondo
que não haja nenhum erro na abertura), duas chamadas para o loop de leitu-
ra/escrita e duas chamadas para fechá-los.. No caso da ocorrência de erros (do tipo:
arquivo fonte inexistente, arquivo destino já existente, arquivos protegidos, erro geral
de leitura ou escrita, etc.) teremos mais uma chamada para cada condição e mais
uma para término anormal.
Programas do Sistema – são de nível mais alto que as chamadas. Os detalhes de
abrir, fechar, etc., típicos em chamadas ficam escondidos do usuário. Para copiar,
por exemplo, o próprio programa se encarregaria de fazer todo o tratamento dos pa-
râmetros necessários, e apenas solicitaria ao usuário os nomes dos arquivos, retor-
nando-lhe ao final algum tipo de mensagem. Esses programas são dos tipos:
Nestes sistemas definimos vários níveis de serviço, ou camadas, sendo que uma
camada só pode pedir serviço à inferior e prestar serviço à superior, o que facilita a
manutenção e evita o risco de loop de chamada.
Exemplo 1: Sistema THE, de Dijkstra.
- nível 5: Programas do usuário,
- nível 0: Hardware
- nível 0: Hardware
1.6.2 UNIX
As raízes do UNIX datam de meados dos anos 60, quando a AT&T, Honeywell,
GE e o MIT embarcaram em um massivo projeto para o desenvolvimento de um
utilitário de informação, chamado Multics (Multiplexed Information and Computing
Service).
Em 1973 o UNIX foi reescrito em C, talvez o fato mais importante da história deste
sistema operacional. Isto significava que o UNIX poderia ser portado para o novo
hardware em meses, e que mudanças eram fáceis. A linguagem C foi projetada
para o sistema operacional UNIX, e portanto há uma grande sinergia entre C e
UNIX.
Em 1975 foi lançada a V6, que foi a primeira versão de UNIX amplamente dispo-
nível fora dos domínios do Bell Labs, especialmente em universidades. Este foi o
início da diversidade e popularidade do UNIX. Nesta época a Universidade de
Berkley comprou as fontes do UNIX e alunos começaram a fazer modificações ao
sistema.
Surgiram outras versões com a inclusão de novas características. O 4.2 BSD foi
talvez umas das mais importantes versões do UNIX. O seu software de conexão
de redes tornava muito fácil a tarefa de conectar computadores UNIX a redes lo-
cais. Nessa versão é que foram integrados os softwares que implementam
TCP/IP e sockets.
O 4.4 BSD foi lançado em 1992 para várias plataformas: HP 9000/300, Sparc,
Apostila de Sistemas Operacionais - Prof. Bruno 17
386, DEC e outras, mas não em VAX. Entre as novas características estão:
• Novo sistema de memória virtual baseado em Mach 2.5
• Suporte ISO/OSI (baseado em ISODE)
A Sun Microsystem também lançou a sua versão do UNIX a partir do BSD. Isto
ocorreu até a versão SunOs 4.x. A nova versão, SunOs 5.x está baseada no
SVR4, embora tenha herdado algumas características do SunOs 4.x. O novo sis-
tema operacional da Sun, Solaris 2.x, engloba SunOs 5.x, Open Network Compu-
ting e Open Windows. É o solaris que provê o pacote de compatibilidade entre os
BSD/SunOs e o SVR4/SunOs 5.x.
A Microsoft também lançou uma versão do UNIX, chamada XENIX, que rodava
em PCs. Este sistema era inicialmente baseado na Versão 7, depois herdou ca-
racterísticas dos SIII e depois do SV.
1.6.3 WINDOWS NT
O Microsoft Windows NT começou a surgir em 18 de setembro de 1996, quando a
Intel Corporation e a Microsoft Corporation anunciaram que estavam trabalhando
no desenvolvimento de um novo sistema operacional para a futura família de pro-
cessadores de 64 bits da Intel. O Windows NT é o sistema operacional da próxi-
ma geração, visando operar PCs até boa parte do próximo século. Foi projetado
para ser um sistema operacional portável, capaz de se adequar facilmente a di-
versas plataformas de hardware, incluindo ambientes de um só processador e de
múltiplos processadores.
1.6.4 WINDOWS 95
Criado pela Microsoft Corporation o Windows 95 é um software básico classifica-
do na categoria de "Sistema Operacional". Ele cria uma interface gráfica para o
usuário (GUI - Graphical User Interface) para proporcionar a este uma comunica-
ção mais intuitiva e fácil com o computador. Este software usa a metáfora da me-
sa de trabalho (desktop) para dispor e arranjar informações gráficas e textuais na
tela. O usuário tem acesso a essas informações através do mouse, que é usado
para abrir janelas, selecionar opções, acionar vários objetos através de ícones,
mover, copiar, renomear ou excluir arquivos, executar programas, etc.
Apostila de Sistemas Operacionais - Prof. Bruno 18
O Windows 95 incorporou um conjunto de tecnologias que, somadas as inova-
ções de sua interface, significam uma autêntica revolução no uso de micros. Uma
das mudanças refere-se a própria interface gráfica, que evoluiu para facilitar ainda
mais a maneira como o indivíduo se relaciona com o equipamento. Essa melhoria
beneficia tanto usuários que conhecem pouco ou quase nada de microinformática
quanto profissionais.
Com o passar do tempo, o uso de teclados para entrada de dados se tornou comum,
e a saída passou a ser inicialmente impressa em papel. Posteriormente o console
assumiu a forma de um terminal com teclado e vídeo.
Além disso, este método não era eficiente na utilização de recursos. Supondo que
você tivesse reservado 1 hora de tempo de computador para executar um programa
em desenvolvimento. Se você tivesse alguns erros desagradáveis você provavel-
mente não terminaria dentro de 1 hora, e deveria juntar seus resultados e liberar a
máquina para a próxima pessoa da fila. Por outro lado, se o seu programa rodasse
sem problemas, você poderia terminar tudo em 35 minutos, e a máquina ficaria ocio-
sa até a próxima reserva de horário.
Mais tarde, compiladores para linguagens de alto nível, como FORTRAN e COBOL,
surgiram, facilitando muito a tarefa de programação, que antes era feita diretamente
na linguagem da máquina. Entretanto a operação do computador para executar um
programa em uma linguagem de alto nível era bem mais complexa.
Além disso, para reduzir ainda mais o tempo de preparação, jobs com necessidades
similares eram agrupados (batched) e executados em grupo pelo computador. Por
exemplo, supondo que o operador tivesse recebido um job FORTRAN, um COBOL,
e outro FORTRAN. Se ele os executasse nessa ordem, ele teria que preparar o job
FORTRAN (carregar fitas de compilador, etc.), então o COBOL, e novamente o
FORTRAN. Se ele executasse os dois jobs FORTRAN como um grupo ele prepara-
ria o ambiente FORTRAN apenas uma vez economizando tempo de preparação.
Esta abordagem marcou uma época, onde o processamento em batch (lotes) defi-
niu uma forma de utilização do computador: os usuários preparavam seus progra-
mas e dados, entregavam-nos ao operador do computador, que os agrupava segun-
do suas necessidades e os executava, produzindo as saídas para serem devolvidas
aos respectivos programadores.
Mesmo assim, quando um job parava, o operador teria que notar o fato observando
no console, determinar porque o programa parou (término normal ou anormal), listar
conteúdos de memória se necessário e então carregar a leitora de cartões ou de fita
de papel com o próximo job e inicializar o computador novamente. Durante a transi-
ção entre os jobs, novamente a UCP ficava ociosa.
Assim que o computador era ligado, o monitor residente era chamado, e transferia o
controle para um programa. Quando o programa terminava, ele retornava o controle
para o monitor residente, que ia para o próximo programa. Assim, o monitor residen-
te fornecia uma seqüência automática entre programas e jobs.
Para que o monitor residente pudesse saber qual programa deveria ser executado e
de que forma, cartões de controle foram introduzidos, de maneira muito semelhante
às instruções que os operadores recebiam dos programadores para execução de
seus programas. Assim, além do programas e dos dados para um job, cartões espe-
ciais de controle eram introduzidos entre os cartões de programa e dados do job a
executar, como por exemplo:
Para distinguir cartões de controle dos demais cartões era necessário identificá-los
com um caractere ou um padrão especial no cartão. Em nosso exemplo, o símbolo
do dólar ($) foi utilizado para este fim. A linguagem JCL (Job Control Language) da
IBM usava duas barras (//) nas primeiras duas colunas. A figura 1.1 ilustra este cená-
rio.
$END
dados do programa
$RUN
$LOAD
pgm. a ser compilado
$FTN
$JOB
Carregador
Monitor
Residente Seqüenciador automático de jobs
Interpretador dos cartões de controle
Área do programa do usuário
A principal vantagem da operação off-line foi de que o computador principal não es-
tava mais restrito pela velocidade das leitoras de cartão e impressoras de linha, e
sim pela velocidade das unidades de fita magnética mais rápidas. Essa técnica de
usar fitas magnéticas para todo o E/S podia ser aplicada com qualquer unidade de
equipamento de registro (leitoras e perfuradoras de cartão, leitoras e perfuradoras de
fitas de papel, impressoras).
Apostila de Sistemas Operacionais - Prof. Bruno 24
Além disso, nenhuma modificação precisa ser feita para adaptar programas de apli-
cação da operação direta para off-line. Considere um programa que executa em um
sistema com uma leitora de cartões acoplada. Quando ele quer um cartão, ele cha-
ma o driver de dispositivo da leitora de cartão no monitor residente. Se a operação
de cartão está em modo off-line, apenas o driver de dispositivo deve ser modificado.
Quando um programa precisa de um cartão de entrada, ele chama a mesma rotina
de sistema como antes. Entretanto, agora o código para aquela rotina não é o driver
da leitora de cartões, mas uma chamada para o driver da fita magnética. O programa
de aplicação recebe a mesma imagem do cartão em ambos os casos.
2.3 Buferização
Processamento off-line permite a sobreposição de operações de UCP e E/S pela
execução dessas duas ações em duas máquinas independentes. Se desejamos a-
tingir tal sobreposição em uma única máquina, comandos devem ser colocados en-
tre os dispositivos e a UCP para permitir uma separação similar de execução. Tam-
bém, uma arquitetura adequada deve ser desenvolvida para permitir buferização.
Por outro lado, se o dispositivo de entrada de dados termina primeiro, ele pode tanto
esperar como continuar com a leitura do próximo registro. Neste caso ele só deverá
parar quando os buffers estiverem cheios. Para que o dispositivo de entrada conti-
nue sempre trabalhando, normalmente os buffers costumam ter tamanho suficiente
para mantê-lo sempre ocupado.
A buferização é geralmente uma função do sistema operacional. O monitor residente
ou os drivers de dispositivo incluem buffers do sistema de E/S para cada dispositivo
de E/S. Assim, chamadas ao driver de dispositivo pelos programas de aplicação
normalmente causam apenas uma transferência do buffer para o sistema.
Apesar da buferização ser de alguma ajuda, ela raramente é suficiente para manter
a UCP sempre ocupada, já que os dispositivos de E/S costumam ser muito lentos
em relação à UCP.
2.4 Spooling
Com o passar do tempo, dispositivos baseados em discos tornaram-se comuns e
facilitaram muito a operação off-line dos sistemas. A vantagem é que em um disco
era possível escrever e ler a qualquer momento, enquanto que uma fita precisava
ser escrita até o fim para então ser rebobinada e lida.
2.5 Multiprogramação
O aspecto mais importante do escalonamento de jobs é a habilidade de multipro-
gramação. As técnicas de operação off-line, buferização e spooling têm suas limita-
ções na sobreposição de E/S. Em geral, um único usuário não pode manter tanto
UCP e dispositivos de E/S ocupados o tempo todo. A multiprogramação aumenta a
utilização de UCP, pois organiza os vários jobs de forma que a UCP sempre tenha
algo para processar.
Outra dificuldade em um sistema batch é que programas devem ser depurados esta-
ticamente, a partir de uma listagem. Um programador não pode modificar um pro-
grama quando ele está sendo executado para estudar o seu comportamento, como
hoje é possível na maioria dos ambientes de programação.
Sistemas batch são bastante apropriados para executar jobs grandes que precisam
de pouca interação. O usuário pode submeter jobs e retornar mais tarde para buscar
os resultados; não é necessário esperar seu processamento. Por outro lado, jobs
interativos costumam ser compostos por várias ações pequenas, onde os resultados
de cada ação podem ser imprevisíveis. O usuário submete o comando e espera pe-
los resultados. O tempo de resposta deve ser pequeno - da ordem de segundos no
máximo. Um sistema interativo é usado quando é necessário um tempo de resposta
pequeno.
A idéia de tempo compartilhado foi demonstrada no início de 1960, mas já que sis-
temas de tempo compartilhado são mais difíceis e custosos para construir (devido
aos numerosos dispositivos de E/S necessários), eles somente tornaram-se comuns
até o início dos anos 70. Conforme a popularidade destes sistemas cresceu, os pes-
quisadores tentaram combinar os recursos de sistemas batch e de tempo comparti-
lhado em um único sistema operacional. Muitos sistemas que foram inicialmente pro-
jetados como sistemas batch foram modificados para criar um subsistema de tempo
compartilhado. Por exemplo, o sistema batch OS/360 da IBM foi modificado para
suportar a opção de tempo compartilhado (Time Sharing Option - TSO). Ao mesmo
tempo, à maioria dos sistemas de tempo compartilhado foi adicionado um subsiste-
ma batch. Hoje em dia, a maioria dos sistemas fornecem ambos processamento bat-
ch e de tempo compartilhado, embora seu projeto básico e uso sejam de um ou de
outro tipo.
Sistemas operacionais de tempo compartilhado são sofisticados. Eles fornecem um
mecanismo para execução concorrente. Como na multiprogramação, vários jobs de-
ve ser mantidos simultaneamente na memória, o que requer alguma forma de ge-
renciamento de memória, proteção e escalonamento de UCP. Como a memória tem
tamanho limitado, e em dadas situações alguns jobs terão que ser retirados da me-
mória e gravados temporariamente em disco, para que outros programas possam
ser lidos do disco e postos em execução na memória. Quando aquele job novamen-
te precisar de continuação em sua execução, ele será trazido de volta para a memó-
ria.
DISP. Interface
CPU MEM
E/S 2
DISP. Interface
E/S 3
As unidades de E/S são os dispositivos a partir dos quais a CPU realiza a interface
com o mundo exterior. São considerados também unidades de E/S os dispositivos
de armazenamento de informação auxiliares, tais como discos rígidos e flexíveis,
unidades de fita magnética, etc.
É inquestionável a importância do sistema de E/S nos sistemas de computação.
Sem os quais seria impossível enviar dados para processamento ou obter resultados
de uma computação.
Observa-se que o acesso (leitura/escrita de dados) aos dispositivos de E/S será rea-
lizado por instruções de acesso à memória. O que distingüirá se o acesso é à memó-
ria ou à E/S será exclusivamente o endereço da posição de memória em questão.
Na figura 3.2 apresenta-se um esquema simplificado de um sistema com E/S mape-
ada em memória.
DISP. E/S
D 1
A2 E DISP. E/S
A1 C
2
O
CPU A0 D
DISP. E/S
3
DISP. E/S
LEIT
4
ESC H
MEM
Pode-se detectar uma CPU que apresenta E/S mapeada em espaço de E/S através
da observação do barramento de controle. Estas CPUs apresentam sinais (pinos)
especiais para indicar se o endereço gerado refere-se à memória ou às unidades de
E/S, bem como instruções de máquina específicas para acesso à memória e acesso
a posições de E/S. Quando a CPU executa uma instrução de acesso à memória ou à
E/S tais pinos são correspondentemente ativados.
DISP. E/S
D 1
E DISP. E/S
LEIT C
2
ESC O
D
DISP. E/S
3
DISP. E/S
CPU 4
A1
A0
mem/ES
MEM
Quando o valor lógico neste pino é igual a 1 isto indica que o endereço destina-se à
memória, quando é igual a 0 para a E/S. Quando a CPU executa uma instrução de
acesso à memória o circuito interno da CPU automaticamente escreve 1 no pino
MEM/~E.S. e este sinal vai habilitar o banco de memória, e desabilitar as portas de
E/S. Quando a instrução a ser executada é de E/S a situação oposta acontece.
Observações importantes:
Tempo
Disperdiçado
Prog. Principal
Operação de E/S
tempo
Desta forma evita-se aquele tempo que a CPU fica bloqueada aguardando que o
periférico termine alguma operação conforme descrito no ítem anterior (modo blo-
queado).
No entanto, deve-se incluir uma rotina de teste dos flags associados aos dispositivos
bem como os flags propriamente ditos. A rotina de teste, evidentemente, consumirá
certo tempo do processamento da CPU, no entanto este será certamente inferior às
somas dos tempos de espera do modo bloqueado.
A rotina testa então, cada um dos flags verificando o valor armazenado ali. Se o va-
lor for igual a 1, o periférico associado necessita a atenção da CPU que chama a
rotina de atendimento daquela dispositivo. Observe que a ordem de teste dos flags
deixa implícito, nesta política, o estabelecimento de prioridades.
Em alguns casos pode-se optar por seguir a rotina de teste de flags após o atendi-
mento de uma solicitação, em outros, a rotina é interrompida após atender-se a um
dispositivo, vindo a ser ativada posteriormente. Quando este esquema é adotado,
pode haver postergação infinita no atendimento de dispositivos de prioridade inferior,
caso os primeiros dispositivos a serem testados requisitem com muita freqüência a
atenção da CPU.
Caso, após a rotina de atendimento, testa-se os demais flags na seqüência, isto po-
de significar tempo gasto (não desprezível se o número de periféricos for muito
grande). O projetista do sistema deverá optar entre uma destas soluções levando em
conta:
a) número de periféricos
b) freqüência estimada de pedidos de atendimento por parte dos periféricos
c) número de pontos de teste de flags (chamada da rotina de polling) desejá-
veis dentro do programa
d) tempo de atendimento médio dos periféricos
Figura 3.6. Execução Típica de um Sistema que Executa Polling para E/S
3.2.3 Interjeição
Interjeição é um sistema aprimorado do Polling. Neste caso, antes de realizar o teste
em cada flag, a CPU testa um flag adicional que representa o OU-Lógico de todos os
flags associados aos periféricos.
Portanto, a CPU só testará os flags para descobrir qual o periférico que deseja reali-
zar a transferência, quando o flag adicional estiver em 1, significando que há pelo
menos um dispositivo de E/S com seu flag igual a 1.
Barr. de End. de
Escreve Flag
no Flag INTJ
DISP. E/S Flag
1
3.2.4 Interrupção
Os métodos anteriores para transferência de informação, apesar de resolverem o
problema e serem comparativamente mais baratos, apresentam sérios problemas
em sistemas onde exige-se maior desempenho, tanto da CPU quanto dos periféri-
cos. No modo bloqueado a CPU perde muito tempo esperando que o periférico con-
clua a operação para que o programa possa continuar sendo executado. Nos méto-
dos de polling e interjeição, apesar de aprimorado, ainda exigem teste periódico da
CPU para monitoração de seus estados.
De uma forma mais explícita, interrupção pode ser definida da seguinte maneira:
1. Trata-se de um sinal gerado pelo hardware externo à CPU e dirigido à esta, in-
dicando que um evento externo ocorreu e isto requer a atenção imediata da
CPU;
2. Este sinal ocorre assincronamente a execução da seqüência de programa. A
interrupção pode ocorrer a qualquer instante e isto não está sob controle do
programa executado;
3. O hardware de controle das interrupções (interno à CPU), se as interrupções
estiverem habilitadas, completa a execução da instrução corrente, e então for-
ça o controle a desviar para a rotina de atendimento do periférico que solicitou,
e;
4. Uma instrução especial de “retorno-de-interrupção” é empregada para direcio-
nar o controle de volta ao ponto do programa principal onde a interrupção ocor-
reu.
B arr. de Endereços de
B arr. de D ados
IN T D ispositivo
R .M . R .I.
de
E /S
ACK
interr.
C ontrole
de
Tem poriz.
C PU
Através do pino de INT o periférico que sinaliza a CPU a intenção de realizar uma
operação de E/S. A CPU, se habilitada, responderá (após a conclusão do ciclo de
instrução em andamento), som sinal de reconhecimento através da linha ACK.
Salvamento de Contexto:
Quando ocorre uma interrupção, o programa que está sendo executado pela CPU é
interrompido, o sistema executa a rotina de atendimento e direciona o controle de
volta ao programa principal.
Entretanto, para que a CPU seja capaz de retornar ao contexto anterior à interrup-
ção, certas atividades devem ser executadas. Como o pedido de interrupção é to-
talmente assíncrono ao programa sendo executado, o pedido pode ocorrer no meio
da execução de uma instrução. Neste caso, a CPU termina de executar a instrução
corrente antes de autorizar o envio do sinal ACK. Para que possa voltar ao ponto
exato onde parou no programa principal, a CPU deve guardar em alguma posição de
memória (pilha) ou mesmo em um registrador apropriado o valor corrente no conta-
dor de programa (PC), ou seja, o endereço da próxima instrução a ser executada
após a execução da rotina de atendimento da interrupção. Desta forma ao concluir a
rotina de atendimento, a CPU atribui ao PC aquele valor armazenado e passa a e-
xecutar novamente o programa principal no ponto correto.
− salvamento do PC
− desabilitação das int’s
A CPU pode dispor de instruções para habilitar e desabilitar interrupções. Seu uso
fica a critério do programador. Tais instruções podem ser úteis para desabilitar as
interrupções em trechos críticos de um programa.
Muitas vezes, mais informação deve ser salva para que quando o controle retorne da
rotina de atendimento para o programa principal consiga restabelecer-se todo o con-
texto. Um caso típico são os registradores de uso geral e registrador de status. Caso
o programador julgue que tal procedimento é necessário, este deve ser elaborado de
forma explícita, dentro da rotina de atendimento.
Barr. de Endereços de
Barr. de Dados
INT
R.M. R.I.
CPU
Figura 3.9. Sistema com um Nível de Prioridade e Vários Dispositivos
Uma outra forma (mais rápida) de identificar o dispositivo de mais alta prioridade que
solicitou interrupção é implementar polling por hardware através de uma cadeia
daisy-chain do sinal ACK, conforme inlustrado na figura abaixo.
INT
R.M. R.I.
interr.
Circuito de
Controle de
Interrupção ACK
CPU
Figura 3.10. Cadeia daisy-chain de Propagação de Reconhecimento (ACK)
Neste caso quando a CPU aceita um pedido de interrupção e o contexto já foi auto-
maticamente salvo, um sinal de reconhecimento é enviado através da linha ACK.
Este sinal entra dentro da lógica dos dispositivos de E/S e propaga-se para os dis-
positivos seguintes até que atinja o primeiro dispositivo que tenha solicitado interrup-
ção. A partir deste dispositivo o sinal propoagado será 0 (zero) indicando o não re-
conhecimento.
Para possibilitar esta flexibilidade tais sistemas contam com outras características
além daquelas existentes em sistemas com um único nível de prioridades.
CPU
Figura 3.11. Múltiplos níveis de prioridade.
Associado a cada nível de interrupção (linha de pedido de int./ INT REQ) existe um
flip-flop (R.I.) que armazena a requisição. Da mesma forma associado a cada regis-
trador de interrupção existe um registrador de máscara que possui as mesmas atri-
buições que no caso com um nível de prioridade.
É claro que, para que a CPU suporte múltiplas interrupções aninhadas (a CPU é in-
terrompida dentro da rotina de atendimento de uma outra interrupção), o hardware
deve fornecer uma forma de retorno ordenado às rotinas das interrupções de níveis
inferiores. Em outras palavras, o hardware da CPU deve guardar em ordem os PC’s
das múltiplas ocorrências de forma que, a medida que as rotinas de n;ivel mais alto
sejam terminadas, a execução se direcione para as rotinas de n;ivel mais baixo até o
programa principal, na ordem inversa em que iniciaram. Isto normalmente é realiza-
do em uma estrutura di tipo pilha que possui endereço inicial fixo.
Desta forma, quando retorna-se de uma rotina de interrupção, tanto o antigo PC bem
como a antiga máscara são restabelecidos. Quando o usuário pode definir as más-
caras é preciso tomar cuidado com a coerência (interrupções de nível mais baixo
não devem mascarar níveis mais altos e interrupções de nível mais alto não podem
ser interrompidas por níveis mais baixos).
1. Endereço Fixo
2. Vetorada
3. Auto-vetorada
Endereço Fixo:
Neste método, quando a CPU recebe um pedido de interrupção o PC e a máscara
são salvos e o PC é carregado sempre com um mesmo endereço. Isto faz com que
a CPU seja desviada sempre para a mesma posição de memória. Nesta posição
encontra-se a rotina de atendimento do dispositivo, ou, no caso de tratar-se de vá-
rios dispositivos, uma rotina de identificação do tipo polling, por exemplo.
Vetorada:
Neste método, quando o periférico recebe o sinal ACK, de reconhecimento do seu
pedido de interrupção ele escreve uma palavra no barramento de dados. A CPU lê
esta palavra e a utiliza como um índice para acessar uma tabela que contém os en-
dereços de todas as rotinas de atendimento. Esta área, onde encontra-se a tabela, é
normalmente pré-fixada e dedicada exclusivamente para esta finalidade.
Auto-vetorada:
O método de identificação auto-vetorsda, normalmente aparace associado ao méto-
do vetorado. Para ilustrar o funcionamento deste método, descreve-se o sistema de
identificação de interrupção do 68000 que emprega também este método.
INT
ACK vcc
CPU
1
2
IPL0 3
IPL1 4
IPL2 5
6
7
Figura 3.12. Sistema auto-vetorado do processador 68000
Além das instruções e dados, cada programa em execução possui uma área de
memória correspondente para armazenar os valores dos registradores da UCP,
quando o programa, por algum motivo, não estiver sendo executado. Essa área de
memória é conhecida como registro descritor (ou bloco descritor, bloco de contex-
to, registro de estado, vetor de estado) e, além dos valores dos registradores da
UCP, contém outras informações.
As interrupções são transparentes aos processos, pois o efeitos das mesmas é ape-
nas parar, temporariamente, a execução de um processo, o qual continuará sendo
executado, mais tarde, como se nada tivesse acontecido. Um trap, por outro lado, é
completamente diferente, pois bloqueia o processo até que o serviço requerido pelo
mesmo, ao sistema operacional, seja realizado.
Deve ser observado que um processo é uma entidade completamente definida por si
só, cujas operações (instruções executadas) se desenvolvem no tempo, em uma
ordem que é função exclusiva dos valores iniciais de suas variáveis e dos dados li-
dos durante a execução.
Tanto no paralelismo físico (real, com várias UCP) como no lógico (virtual, uma UCP
compartilhada), as velocidades relativas com que os processos acessarão dados
compartilhados não podem ser previstas. Isto implica em mecanismos de sincroniza-
ção entre processos, como vimos anteriormente com as instruções parbegin/parend.
Os processos durante suas execuções requerem operações de E/S que são execu-
tadas em dispositivos muito lentos que a UCP, pois os dispositivos periféricos pos-
suem componentes mecânicos, que funcionam a velocidades muito inferiores à dos
dispositivos eletrônicos que funcionam à velocidade da luz.
Quando um processo está realmente usando a UCP, diz-se que o mesmo está no
estado executando. Quando está esperando pelo término de um serviço que reque-
reu, diz-se que está no estado bloqueado. Quando o processo tem todas as condi-
ções para ser executado e só não está em execução porque a UCP está alocada
para outro processo, diz-se que o mesmo está no estado pronto. O sistema opera-
cional mantém uma lista (fila) dos processos que estão prontos, a chamada lista de
processos prontos. O diagrama da figura 4.1 mostra como os estados de um pro-
cesso podem mudar durante a execução.
Apostila de Sistemas Operacionais - Prof. Bruno 45
executando trap
escalonador
interrupção bloqueado
pronto interrupção
(conclusão do serviço)
Em geral, um trap faz com que o processo fique bloqueado. Entretanto, em algumas
ocasiões especiais, quando o sistema operacional pode atender imediatamente a
requisição de serviço, o processo pode ser novamente despachado, não ocorrendo
o bloqueio.
Quando é terminada a operação que fez com que o estado fique bloqueado, este
passa para o estado pronto. A transição que faz tal operação é definida como:
Manipulação de interrupções;
Criação e destruição de processos;
Troca de contexto de processos;
• Desacatamento de processos;
• Suspensão e reanimação de processos;
• Sincronização de processos;
• Intercomunicação entre processos;
• Manipulação de PCBs;
• Suporte a atividades de E/S;
• Suporte à alocação e desalocação de armazenamento;
• Suporte ao sistema de arquivos;
• Suporte a um mecanismo de chamada/retorno de procedimentos;
• Suporte a certas funções do sistema de contabilização.
Justiça: fazer com que cada processo ganhe seu tempo justo de CPU;
Eficiência: manter a CPU ocupada 100% do tempo (se houver demanda);
Tempo de Reposta: minimizar o tempo de resposta para os usuários interativos;
Tempo de Turnaround: minimizar o tempo que usuários batch devem esperar pelo
resultado;
Um pouco de análise mostrará que alguns desses objetivos são contraditórios. Para
minimizar o tempo de resposta para usuários interativos, o escalonador não deveria
Apostila de Sistemas Operacionais - Prof. Bruno 48
rodar nenhum job batch (exceto entre 3 e 6 da manhã, quando os usuários interati-
vos estão dormindo). Usuários batch não gostarão deste algoritmo, porque ele viola
a regra 4.
Processo Processo
corrente Próximo processo corrente Próximo processo
B F D G A F D G A B
(a) (b)
Figura 4.2 - Escalonamento Round Robin. (a) Lista de processos a executar, (b) Lis-
ta de processos a executar depois de terminado o quantum de ‘B’
Assim, o algoritmo round robin é semelhante ao FIFO, mas com a diferença de que é
preemptivo: os processos não executam até o seu final, mas sim durante um certo
tempo, um por vez. Executando sucessivamente em intervalos de tempo o job acaba
por terminar sua execução em algum momento.
Para evitar que processos com alta prioridade executem indefinidamente, o escalo-
nador pode decrementar a prioridade do processo atualmente executando a cada
tick de relógio (isto é, a cada interrupção de relógio). Se esta ação fizer com que a
prioridade do processo se torne menor do que a prioridade do processo que possuía
a segunda mais alta prioridade, então uma troca de processos ocorre.
Prioridades podem também ser atribuídas dinamicamente pelo sistema para atingir
certos objetivos do sistema. Por exemplo, alguns processos são altamente limitados
por E/S, e passam a maior parte do tempo esperando por operações de E/S. Sem-
pre que um desses processos quiser a CPU, ele deve obtê-la imediatamente, para
que possa iniciar sua próxima requisição de E/S, e deixá-la sendo feita em paralelo
com outro processo realmente processando. Fazer com que processos limitados por
E/S esperem um bom tempo pela CPU significa deixá-los um tempo demasiado ocu-
pando memória. Um algoritmo simples para prover um bom serviço a um processo
limitado por E/S é ajustar a sua prioridade para 1/f, onde f é a fração do último quan-
tum de processador que o processo utilizou. Um processo que utilizou somente 2 ms
do seu quantum de 100 ms ganharia uma prioridade 50, enquanto um processo que
executou durante 50 ms antes de bloquear ganharia prioridade 2, e um processo
que utilizou todo o quantum ganharia uma prioridade 1.
Multilevel feedback queues (filas multi-nível com retorno) fornecem uma estrutura
que atinge esses objetivos. O esquema é ilustrado na figura 4.3:
Usa a Término
Nível 1
CPU
(FIFO)
Preempção
Usa a Término
Nível 2
CPU
(FIFO)
Preempção
Usa a Término
Nível n
CPU
(round robin)
Preempção
Agora considere um processo limitado por CPU que necessita de um grande tempo
de CPU. Ele entra a rede de filas no nível mais alto, recebendo rapidamente seu
primeiro quantum de CPU, mas quando ele expira, o processo é movido para a fila
inferior. Agora o processo tem prioridade inferior aos da fila superior, mas eventual-
mente ele recebe a CPU, ganhando um quantum maior do que o anterior. Conforme
o processo ainda precise de CPU, ele vai caminhando pelas filas, até chegar à fila
de mais baixo nível, onde ele circula por uma fila round robin até que tenha termina-
do.
Filas Multi-nível com retorno são ideais para separar processos em categorias base-
adas na sua necessidade por CPU. Em um sistema de tempo compartilhado, cada
vez que o processo deixe a rede de filas, ele pode ser marcado com a identificação
do nível da fila onde ele esteve pela última vez. Quando o processo reentra no sis-
tema de filas, ele pode ser enviado diretamente para a fila onde ele anteriormente
completou sua execução, de forma que um processo retornando para as filas não
interfira no desempenho dos processos das filas de níveis mais altos.
Se processos são sempre colocados de volta na rede de filas no nível mais alto que
eles ocuparam da última vez que estiveram no sistema de filas, será impossível para
o sistema responder a mudanças no processo, como por exemplo, deixando de ser
limitado por CPU para ser limitado por E/S. Este problema pode ser resolvido mar-
Apostila de Sistemas Operacionais - Prof. Bruno 53
cando também o processo com o seu tempo de permanência na rede de filas na úl-
tima vez em que lá esteve. Assim, quando o processo reentra no sistema de filas,
ele pode ser colocado no lugar correto. Dessa forma, se o processo está entrando
em uma nova fase na qual ela deixa de ser limitado por CPU para ser limitado por
E/S, inicialmente ele vai sofrer uma penalidade pelo sistema, mas da próxima vez o
algoritmo perceberá a mudança de comportamento do processo. Uma outra maneira
de responder a mudanças no comportamento de um processo é colocá-lo em um
nível de filas cima do qual esteve se ele voluntariamente desistir da CPU antes do
término do seu quantum.
Uma variação comum deste algoritmo é ter os processos circulando em várias filas
round robin. O processo circula pela primeira fila um certo número de vezes, depois
desce um nível, circulando um número maior de vezes, e assim por diante.
SJF favorece jobs pequenos em prejuízo dos jobs maiores. Muitos projetistas acredi-
tam que quanto mais curto o job, melhor serviço ele deveria receber. Não há um
consenso universal quanto a isso, especialmente quando prioridades de jobs devem
ser consideradas.
SJF seleciona jobs para serviço de uma maneira que garante que o próximo job irá
completar e deixar o sistema o mais cedo possível. Isto tende a reduzir o número de
jobs esperando, e também reduz o número de jobs esperando atrás de grandes
jobs. Como resultado, SJF pode minimizar o tempo médio de espera conforme eles
passam pelo sistema.
O problema óbvio com SJF é que ele requer um conhecimento preciso de quanto
tempo um job demorará para executar, e esta informação não está usualmente dis-
ponível. O melhor que SJF pode fazer é se basear na estimativa do usuário de tem-
po de execução. Em ambientes de produção onde os mesmos jobs rodam regular-
mente, pode ser possível prover estimativas razoáveis. Mas em ambientes de de-
senvolvimento, os usuários raramente sabem durante quanto tempo seus programas
vão executar.
SJF, assim como FIFO, é não preemptivo e, portanto não é útil para sistemas de
tempo compartilhado nos quais tempos razoáveis de resposta devem ser garantidos.
É difícil e denota tempo determinar quais atividades podem e quais não podem ser
executadas em paralelo. Programas paralelos são muito mais difíceis de debugar do
que programas seqüenciais depois de supostamente consertar um bug, é pratica-
mente impossível reproduzir a seqüência exata de eventos que fez com que o erro
aparecesse, dessa forma então impossibilitando certificar, com certeza, de que o
erro foi corretamente removido.
Finalmente, é muito mais difícil provas que programas paralelos estão corretos, do
que programas seqüenciais. A prova da corretude de programas envolve geralmente
testes exaustivos, e as possibilidades de execução em programas paralelos podem
ser infinitas, sendo impossível a exaustão de todas as possibilidades.
Estes comandos ocorrem sempre aos pares e são comumente chamados parbe-
gin/parend (início e fim de execução paralela), ou cobegin/coend (início e fim de
execução concorrente). Utilizaremos parbegin/parend, conforme sugerido por Dijks-
tra (1965), na seguinte forma geral:
parbegin
comando-1;
comando-2;
.
.
.
comando-n
parend
1 b ** 2
2 4*a
3 (4 * a) * c
4 (b ** 2) - (4 * a * c)
5 (b ** 2 - 4 * a *c) ** .5
6 -b
7 (-b) + ((b ** 2 - 4 * a * c) ** .5)
8 2*a
9 (-b + (b ** 2 - 4 * a * c) ** .5) / (2 * a)
parbegin
temp1 := -b;
temp2 := b ** 2;
temp3 := 4 * a;
temp4 := 2 * a
parend;
Apostila de Sistemas Operacionais - Prof. Bruno 57
temp5 := temp3 * c;
temp5 := temp2 - temp5;
temp5 := temp5 ** .5;
temp5 := temp1 + temp5;
x := temp5 / temp4
program A; program B;
... ...
fork B; ...
... ...
join B; end.
...
end.
Suponhamos que LINHAS tenha o valor atual 21687. Agora suponhamos que o pri-
meiro processo execute as instruções LOAD e ADD, deixando então o valor 21688
no acumulador. Então o processo perde o processador (após o término de seu quan-
Apostila de Sistemas Operacionais - Prof. Bruno 58
tum) para o segundo processo. O segundo processo então executa as três instru-
ções, fazendo com que a variável linhas tenha o valor 21688. Ele então perde o pro-
cessador para o primeiro processo que continua executando de onde tinha parado,
e, portanto executando a instrução STORE, e armazenando 21688 na variável
LINHAS. Devido à falta de comunicação entre os processos, o sistema deixou de
contar uma linha o valor correto seria 21689.
O problema pode ser resolvido dando a cada processo acesso exclusivo à variável
LINHAS. Enquanto um processo incrementa a variável, todos os outros processos
desejando fazê-lo no mesmo instante deverão esperar; quando o primeiro processo
terminar o acesso à variável compartilhada, um dos processos passa a ter acesso à
variável. Assim, cada processo acessando a variável exclui todos outros de fazê-lo
simultaneamente. Isto é chamado de exclusão mútua. Como veremos, os proces-
sos em espera deverão ser gerenciados de forma a esperarem um tempo razoavel-
mente curto.
Fica claro, que para evitarmos problemas como o mostrando acima, é necessário
garantir que quando um processo está em sua região crítica, todos os outros pro-
cessos (pelo menos aqueles que acessam a mesma variável compartilhada modifi-
cável) sejam excluídos de suas respectivas regiões críticas.
Enquanto um processo está em sua região crítica, outros processos podem certa-
mente continuar sua execução fora de suas regiões críticas. Quando um processo
deixa sua região crítica, um dos processos esperando para entrar em sua própria
região crítica pode prosseguir. Garantir a exclusão mútua é um dos principais pro-
blemas em programação concorrente. Muitas soluções foram propostas: algumas
baseadas em software e outras baseadas em hardware; algumas em baixo nível,
outras em alto nível; algumas requerendo cooperação voluntária entre processos,
outras exigindo uma rígida aderência a protocolos estritos.
Um processo dentro de uma região crítica está em um estado muito especial. O pro-
cesso possui acesso exclusivo aos dados compartilhados modificáveis, e todos os
outros processos desejando acessá-los devem esperar. Assim, regiões críticas de-
vem executar o mais rápido possível, um processo não deve passar para o estado
Apostila de Sistemas Operacionais - Prof. Bruno 59
bloqueado dentro da região crítica, e regiões críticas devem ser cuidadosamente
codificadas (para evitar, por exemplo, loops infinitos).
program exclusão_mútua;
var linhas_digitadas: integer;
procedure processo_um;
while true do
begin
leia_próxima_linha_do_terminal;
entermutex;
linhas_digitadas := linhas_digitadas + 1;
exitmutex;
processe_a_linha;
end;
procedure processo_dois;
while true do
begin
leia_próxima_linha_do_terminal;
entermutex;
linhas_digitadas := linhas_digitadas + 1;
exitmutex;
processe_a_linha;
end;
begin
linhas_digitadas := 0;
parbegin
processo_um;
processo_dois;
parend;
end.
Nenhuma suposição deve ser feita sobre as velocidades relativas dos processos
concorrentes assíncronos.
Processos operando fora de suas regiões críticas não podem evitar que outros pro-
cessos entrem em suas próprias regiões críticas.
Processos não devem ter sua vez de entrar em sua região crítica indefinidamente
adiada.
Uma implementação elegante em software de exclusão mútua foi pela primeira vez
apresentada pelo matemático alemão Dekker. Esta solução implementa exclusão
mútua com “espera ocupada” (busy wait), pois os processos ficam em um loop infini-
to fazendo testes até conseguirem entrar nas suas regiões críticas. O algoritmo de
Dekker foi analisado e apresentado por diversos autores da área de sistemas opera-
cionais. Um deles, G. L. Peterson, apresentou mais tarde um algoritmo mais simplifi-
cado do algoritmo original de Dekker.
Entretanto, a solução de Dekker somente funciona para exclusão mútua entre dois
processos. Outras soluções foram inventadas, e contam com algum suporte do sis-
tema operacional, o que facilita a vida do programador. Vejamos a seguir estas solu-
ções.
Apostila de Sistemas Operacionais - Prof. Bruno 61
4.3.6 Exclusão Mútua para N Processos
Em 1965, Dijkstra foi o primeiro a apresentar uma solução em software para a im-
plementação de primitivas de exclusão mútua para n processos. Knuth respondeu
em 1966 com uma solução que eliminou a possibilidade de adiamento indefinido e-
xistente no algoritmo de Dijkstra, mas que ainda permitia que um processo pudesse
sofrer um longo atraso na hora de entrar em sua região crítica.
Brinch Hansen (1978) também discute controle de concorrência entre processos dis-
tribuídos. Burns, et alli (1982), oferece uma solução utilizando uma única variável
compartilhada. Carvalho e Roucairol (1983) discutem a garantia de exclusão mútua
em redes de computadores.
4.3.7 Semáforos
Dijkstra conseguiu abstrair as principais noções de exclusão mútua em seu conceito
de semáforos. Um semáforo é uma variável protegida cujo valor somente pode ser
acessado e alterado pelas operações P e V, e por uma operação de inicialização
que chamaremos inicializa_semáforo. Semáforos binários podem assumir so-
mente os valores 0 ou 1. Semáforos contadores (também chamados de semáfo-
ros genéricos) podem assumir somente valores inteiros não negativos.
if S > 0 then
S := S - 1
else
(espera no semáforo S)
Nós podemos assumir que existe uma política de enfileiramento FIFO (first-in-first-
out - o primeiro a entrar é o primeiro a sair) para os processos esperando para uma
operação P(S) completar.
program exemplo_semáforo;
var ativo: semaphore;
procedure processo_um;
begin
while true do
begin
algumas_funcoes_um;
P(ativo);
regiao_critica_um;
V(ativo);
outras_funcoes_um;
end
end;
procedure processo_dois;
begin
while true do
begin
algumas_funcoes_dois;
P(ativo);
regiao_critica_dois;
V(ativo);
outras_funcoes_dois;
end
end;
begin
inicializa_semaforo(ativo, 1);
parbegin
processo_um;
processo_dois
parend
end.
Mais genericamente, suponha que um processo queira ser notificado sobre a ocor-
rência de um evento particular. Suponha que algum outro processo seja capaz de
detectar que esse evento ocorreu. O exemplo seguinte mostra como operações de
semáforo podem ser usadas para implementar um mecanismo de sincronização
simples block/wakeup.
program block_wakeup;
var evento_de_interesse: semaphore;
procedure processo_um;
begin
algumas_funcoes_um;
P(evento_de_interesse);
outras_funcoes_um
end;
procedure processo_dois;
begin
algumas_funcoes_dois;
V(evento_de_interesse);
outras_funcoes_dois
end;
begin
inicializa_semaforo(evento_de_interesse, 0);
parbegin
processo_um;
processo_dois
parend
end.
Note que esse mecanismo funciona mesmo que o processo_dois detecte e sinalize o
evento com V(evento_de_interesse) antes que o processo_um execute
P(evento_de_interesse) - o semáforo será incrementado de 0 para 1, então
P(evento_de_interesse) irá simplesmente decrementar o semáforo de 1 para 0, e o
processo_um irá presseguir sem ter que esperar pelo evento.
program relacao_produtor_consumidor;
var numero: integer;
numero_depositado: semaphore;
numero_retirado: semaphore;
procedure processo_produtor;
var proximo_resultado: integer;
begin
while true do
begin
calcule_proximo_resultado;
P(numero_retirado);
numero := proximo_resultado;
V(numero_depositado)
procedure processo_consumidor;
var proximo_resultado: integer;
begin
while true do
begin
calcule_proximo_resultado;
P(numero_depositado);
proximo_resultado := numero;
V(numero_retirado);
write(proximo_resultado)
end
end;
begin
inicializa_semaforo(numero_depositado, 0);
inicializa_semaforo(numero_retirado, 1);
parbegin
processo_produtor;
processo_consumidor
parend
end.
Em algum momento, nosso processo na fila do semáforo vai chegar à primeira posi-
ção da fila. A próxima operação V vai remover o processo da fila do semáforo e co-
locá-lo na lista de processos prontos. É claro que processos tentando simultanea-
mente executar operações P e V em um semáforo ganharão acesso exclusivo ao
semáforo pelo núcleo do SO.
Vale notar o caso especial de que em sistemas com um único processador, a indivi-
sibilidade de P e V pode ser garantida simplesmente desabilitando interrupções en-
quanto operações P e V estão manipulando o semáforo. Isto previne que o proces-
sador seja “roubado” até que a manipulação do semáforo esteja completa. Neste
momento as interrupções poderão ser novamente habilitadas.
4.3.8 Monitores
A comunicação interprocessos utilizando semáforos e contadores de eventos parece
ser a solução definitiva. Entretanto, se analisarmos mais diretamente estas técnicas,
veremos que elas possuem alguns problemas:
São tão primitivos que é difícil expressar soluções para problemas de concorrência
mais complexos; seu uso em programas concorrentes aumenta a já difícil tarefa de
provar a corretude de programas;
o mau uso dessas primitivas, tanto de forma acidental como maliciosa, poderia cor-
romper a operação do sistema concorrente.
Para tornar mais fácil a escrita de programas corretos, Hoare (1974) e Brinch Han-
sen (1975) propuseram uma primitiva de sincronização de alto nível chamada de
monitor. Um monitor é uma coleção de procedimentos, variáveis, e estruturas de
dados que são todos agrupados em um tipo especial de módulo ou pacote. Proces-
sos podem chamar os procedimentos em um monitor sempre que o desejarem, mas
eles não podem diretamente acessar diretamente as estruturas de dados internas do
monitor através de procedimentos declarados fora do monitor. O exemplo abaixo
ilustra um monitor escrito em nossa linguagem imaginária, semelhante a Pascal:
monitor exemplo;
var i: integer;
c: condition; { variável de condição }
Monitores possuem uma importante propriedade que os torna útil para atingir exclu-
são mútua: somente um processo pode estar ativo em um monitor em qualquer mo-
mento. Monitores são uma construção da própria linguagem de programação utiliza-
da, de forma que o compilador sabe que eles são especiais, e pode manipular cha-
madas a procedimentos dos monitores de forma diferente da qual manipula outras
chamadas de procedimentos. Tipicamente, quando um processo chama um proce-
dimento de um monitor, as primeiras instruções do procedimento irão checar se al-
gum outro processo está ativo dentro do monitor. Se isto acontecer, o processo que
chamou o procedimento será suspenso até que outro processo tenha deixado o mo-
nitor. Se nenhum outro processo estiver usando o monitor, o processo que chamou o
procedimento poderá entrar.
program produtor_consumidor;
const N = 100;
var cont: integer;
procedure produtor;
begin
while true do begin
produz_item; { produz um item de dado }
if (cont = N) then
suspend; { se o buffer esta’ cheio, entra em suspensao }
entra_item; { coloca o item produzido no buffer }
Apostila de Sistemas Operacionais - Prof. Bruno 69
cont := cont + 1;
if (cont = 1) then
resume (consumidor); { se o buffer estava vazio, acorda o consumidor}
end;
end;
procedure consumidor;
begin
while true do begin
if (cont = 0) then
suspend; { se o buffer esta’ vazio, entra em suspensao }
remove_item; { remove o item do buffer }
cont := cont - 1;
if (cont = N - 1) then
resume (produtor); { se o buffer estava cheio, acoda o produtor }
consome_item; { imprime o item }
end;
end;
begin
cont := 0;
parbegin
produtor;
consumidor;
parend;
end.
Analisando o exemplo, podemos perceber que ali existe uma condição de corrida.
Ela pode ocorrer porque o acesso à variável cont é irrestrito. A seguinte situação
poderia ocorrer. O buffer está vazio e o consumidor acabou de ler o valor de cont
para checar se era 0. Neste exato momento, o escalonador decide parar a execução
do consumidor e começa a executar o produtor. O produtor entra com um item no
buffer, incrementa cont, e percebe que agora cont vale 1. Percebendo que cont era
anteriormente 0, e que portanto o consumidor deveria estar suspenso, o produtor
chama resume para acordar o consumidor.
Este outro processo, por exemplo, o consumidor, pode acordar seu parceiro suspen-
so executando um signal na variável de condição na qual seu parceiro está espe-
rando. Para evitar que dois processos estejam ativos no monitor ao mesmo tempo, é
preciso uma regra que diga o que acontece após um signal. Hoare propôs deixar o
processo recém-acordado prosseguir, suspendendo o outro. Brinch Hansen propôs
que o processo que chame signal deve deixar o monitor imediatamente. Em outras
palavras, uma chamada a signal deve aparecer somente como o último comando
em um procedimento de monitor. Utilizaremos a proposta de Brinch Hansen por ser
conceitualmente mais simples e também mais fácil de implementar. Se um comando
signal é executado em uma variável na qual vários processos estão esperando, so-
mente um deles, determinado pelo escalonador do sistema, é acordado.
monitor ProdutorConsumidor;
var cheio, vazio: condition; { variáveis de condição }
cont: integer;
procedure colocar;
begin
if cont = N then
wait(cheio);
entra_item;
cont := cont + 1;
if cont = 1 then
signal(vazio);
end;
procedure remover;
begin
if cont = 0 then
wait(vazio);
remove_item;
cont := cont - 1;
if cont = N - 1 then
signal(cheio);
end;
Apostila de Sistemas Operacionais - Prof. Bruno 71
count := 0;
end monitor;
procedure produtor;
begin
while true do begin
produz_item;
ProdutorConsumidor.colocar;
end
end;
procedure consumidor;
begin
while true do begin
ProdutorConsumidor.remover;
consome_item;
end
end;
Pode parecer que as operações wait e signal pareçam similares às operações sus-
pend e resume. De fato, elas são muito similares, mas com uma diferença crucial:
suspend e resume falharam porque enquanto um processo tentava ficar suspenso,
o outro tentava acordá-lo. Com monitores, isso não pode acontecer. A exclusão mú-
tua automática nos procedimentos do monitor garante isso pois, se por exemplo, o
produtor dentro de um procedimento do monitor descobre que o buffer está cheio,
ele será capaz de completar a operação wait sem se preocupar com a possibilidade
de que o escalonador venha a dar o processador ao consumidor antes que a opera-
ção wait complete. O consumidor não poderá nem sequer entrar no monitor antes
que a operação wait tenha terminado e o processo produtor tenha sido marcado
como suspenso pelo sistema.
Ao fazer a exclusão mútua de regiões críticas automática, monitores fazem com que
a programação paralela seja muito menos sujeita a erros do que com semáforos.
Entretanto, monitores possuem suas desvantagens. Como já foi dito, monitores são
um conceito da linguagem de programação. O compilador deve reconhecê-los e ar-
ranjar a exclusão mútua de acordo. C, Pascal e outras linguagens não possuem mo-
nitores, portanto não se pode esperar que os compiladores dessas linguagens dêem
algum suporte a exclusão mútua. De fato, como poderia o compilador saber quais
procedimentos estão em monitores e quais não estão?
Estas mesmas linguagens não possuem semáforos, mas adicioná-los a elas é fácil:
basta escrever algumas rotinas para as operações P e V e adicionar estas rotinas às
bibliotecas de funções. Os compiladores nem precisam “saber” do fato que os semá-
foros existem. É claro que o sistema operacional precisa suportar semáforos, para
que as operações P e V possam ser implementadas.
Para utilizar monitores, é necessária uma linguagem que os tenha por natureza. Mui-
to poucas linguagens, como Concurrent Euclid os possuem, e compiladores para
Apostila de Sistemas Operacionais - Prof. Bruno 72
elas são raros.
Outro problema com monitores, e também com semáforos, é que eles foram projeta-
dos para sistemas monoprocessados, ou para sistemas multiprocessados com me-
mória compartilhada. Em sistemas distribuídos, onde cada CPU possui sua própria
memória e está conectada às outras através de um rede de computadores, semáfo-
ros e monitores não podem ser aplicados.
Em alguns sistemas de spool, todo o job de impressão deve ser gerado antes do
início da impressão. Isto pode gerar uma situação de deadlock, uma vez que o es-
paço disponível em disco para a área de spooling é limitado. Se vários processos
começarem a gerar seus dados para o spool, é possível que o espaço disponível
para o spool fique cheio antes mesmo de um dos jobs de impressão tiver terminado
de ser gerado. Neste caso, todos os processos ficarão esperando pela liberação de
espaço em disco, o que jamais vai acontecer, e portanto gerando uma situação de
deadlock. A solução neste caso seria o operador do sistema cancelar um dos jobs
parcialmente gerados.
Nos sistemas operacionais mais populares, como o Windows 95, o sistema de spool
pode ser configurado para começar a imprimir assim que uma página estiver dispo-
nível em disco (isto porque algumas impressoras são orientadas a página, como as
impressoras laser). No Windows 3.x, o spool só iniciava a impressão após todo o job
de impressão ter sido gerado.
Certos recursos são não-preemptíveis, e não podem ser tomados de processos aos
quais foram alocados. Ex. unidades de fita. Contra-ex. disco.
Alguns recursos são compartilhados entre vários processos. Unidades de disco são
compartilhadas em geral. Memória principal e CPU são compartilhadas; apesar de
que em um instante a CPU pertence a um único processo, mas sua multiplexação
entre os vários processos transmite a idéia de que está sendo compartilhada.
Um sistema possui um número finito de recursos para serem distribuídos entre pro-
cessos concorrentes. Os recursos são classificados segundo vários tipos, sendo que
cada tipo pode consistir de uma quantidade de instâncias idênticas. Por exemplo, se
considerarmos o tipo de recurso CPU, em uma máquina com dois processadores,
temos duas instâncias do recurso CPU.
Uma cadeia circular de processos existe de forma que cada processo detém um ou
mais recursos que estão sendo requisitados pelo próximo processo na cadeia (con-
dição “circular wait”).
• Pode ser usado um protocolo para garantir que em um determinado sistema de-
adlocks jamais ocorrerão;
• Pode-se deixar o sistema entrar em um estado de deadlock e então tratar da sua
recuperação;
• Pode-se simplesmente ignorar o problema, e fingir que deadlocks nunca ocor-
rem. Esta solução é usada pela maioria dos sistemas operacionais, inclusive o
UNIX.
Para garantir que deadlocks nunca ocorrem, o sistema pode tanto usar um esquema
de prevenir deadlocks, ou evitar deadlocks. A prevenção de deadlocks é um con-
junto de regras de requisição de recursos que garantem que pelo menos uma das
condições necessárias para a ocorrência de deadlocks não esteja em efeito. Evitar
deadlocks, por outro lado, requer que seja fornecida ao sistema operacional informa-
ção adicional sobre quais recursos um processo irá requisitar e usar durante sua
execução. Com o conhecimento dessa informação, é possível decidir, a cada requi-
sição, se o processo pode prosseguir ou se deve esperar. Cada requisição requer
que o sistema operacional considere os recursos atualmente disponíveis, os recur-
sos alocados a cada processo, e as futuras requisições e liberações de cada pro-
cesso, para que possa decidir se a requisição corrente pode ser satisfeita ou deve
ser adiada.
Apesar do método de ignorar os deadlocks não parecer uma abordagem viável para
o problema da ocorrência de deadlocks, ele é utilizado em vários sistemas operacio-
nais. Em muitos sistemas, deadlocks ocorrem de forma não freqüente, como por e-
xemplo, uma vez por ano. Assim, é muito mais simples e “barato” usar este método
do que os dispendiosos métodos de prevenir, evitar, detectar e recuperar de situa-
ções de deadlock. Além disso, podem existir situações em que um sistema fica apa-
rentemente “congelado” sem estar, no entanto, em situação de deadlock. Imagine,
por exemplo, um sistema rodando um processo em tempo real com a máxima priori-
dade, ou ainda, um processo rodando em um sistema com escalonador não preemp-
tivo.
O problema é que nem todos os recursos podem ser alocados via spooling. Além
disso, o próprio sistema de spooling pode levar a situações de deadlock, conforme já
discutimos.
Esta solução parece ser boa mas pode levar a um sério desperdício de recursos. Por
exemplo, suponha um programa que lê dados de uma unidade de fita, processa-os
por uma hora, e em seguida imprime alguns gráficos em um plotter. Uma vez que
ambas a unidade de fita e o plotter estejam disponíveis, o programa pode prosse-
guir. O desperdício ocorre porque o plotter ficará alocado ao processo durante uma
hora antes de ser efetivamente utilizado.
Uma estratégia melhor seria utilizar a terceira estratégia de Havender, que determina
que todos os recursos devem ser numerados em ordem crescente. Assim, proces-
sos podem requisitar recursos sempre que quiserem, mas todas as requisições de-
vem ser em ordem crescente de numeração. Tomando a figura 10.3 como exemplo,
um processo poderia requisitar o recurso R1 e em seguida o recurso R3, mas não o
inverso.
A memória sempre foi vista como um recurso caro e por isso merece cuidados para
o seu gerenciamento correto. Apesar da queda vertiginosa do preço da memória re-
al, esta ainda é muitas vezes mais cara do que a memória secundária (discos, fitas,
etc.).
P ro g ra m a
fo n te
C o m p ila d o r o u T em po de
a s s e m b le r c o m p ila ç a o
M ó d u lo
o b je to
O u tro s
m ó d u lo s
o b je to
L in k e d ito r
(lig a d o r)
M ó d u lo T em po de
c a rre g á ve l c a rre g a m e n to
B ib lio te c a
d e s is te m a
C a rre g a d o r
(lo a d e r )
B ib lio te c a d e
s is te m a
c a rre g a d a
d in a m ic a m e n te Im a g e m e m
m e m ó ria d o T em po de
p ro g ra m a ex ecução
b in á rio
Quando este stub é executado, ele verifica se a rotina desejada já está em memória.
Se a rotina não está em memória, o programa a carrega. De qualquer forma, o stub
substitui a si mesmo pelo endereço da rotina, e em seguida a executa. Assim, da
próxima vez que o trecho de código que referencia a rotina é atingido, a rotina de
biblioteca é executada diretamente, sem custo novamente da ligação dinâmica. Sob
este esquema, todos os processos que usam uma mesma biblioteca de linguagem
executam somente sobre uma cópia do código da biblioteca.
Este recurso pode ser útil também para atualizações de versões de bibliotecas, co-
mo o conserto de bugs. Uma biblioteca pode ser substituída pela nova versão, e to-
dos os programas que a referenciam usarão a nova versão. Sem ligação dinâmica,
todos os programas que usassem tais bibliotecas precisariam ser religados para fun-
Apostila de Sistemas Operacionais - Prof. Bruno 83
cionarem com a nova biblioteca. Para que programas não executem acidentalmente
versões mais recentes e incompatíveis de bibliotecas, uma informação sobre versão
da biblioteca é incluída tanto no programa quanto na biblioteca. Mais do que uma
versão de biblioteca podem estar presentes em memória, e cada programa usa a
sua informação de versão para decidir qual biblioteca usar. Pequenas mudanças
normalmente mantém o número de versão, enquanto mudanças mais radicais in-
crementam o número de versão. Este esquema também é chamado de bibliotecas
compartilhadas.
5.1.4 Overlays
Em algumas situações pode ser que a memória física não seja suficiente para conter
todo o programa do usuário. Uma forma de resolver esse problema é o uso de over-
lays. As seções do programa não necessárias em um determinado momento podem
ser substituídas por uma outra porção de código trazida do disco para execução,
conforme ilustra a figura 5.2. O overlay manual requer planejamento cuidadoso e
demorado. Um programa com uma estrutura de overlays sofisticada pode ser difícil
de modificar. Alguns compiladores (Turbo Pascal) podem gerenciar overlays auto-
maticamente, mas sem eficiência muito grande. Como veremos adiante, os sistemas
de memória virtual eliminaram o trabalho do programador com a definição de over-
lays.
c
fase de fase de fase de
Área de iniciali- proces- geração
Overlay zação samento de
b b resultado
Porção mínima
do programa do b
usuário que deve b
permanecer em
memória até o
final da execução
a
Sistema
Operacional
0
R e g is tra d o r d e
re lo c aç ã o
14000
E n d e re ç o E n d e re ç o
ló gico fís ic o
+ M e m ó ria
CPU 346 14346
M M U
5.3 Swapping
Um processo precisa estar em memória para executar. Um processo, entretanto,
pode ser temporariamente ser retirado (swapped) da memória para uma área de ar-
mazenamento de trocas (área de swapping), de forma que mais tarde seja trazido de
volta para a memória para que continue executando. Por exemplo, suponha um am-
biente multiprogramado com um algoritmo de escalonamento de CPU round-robin.
Quanto o quantum de determinado processo expira, o gerenciador de memória do
SO começará a retirar (swap out) o processo recém interrompido, e recolocar (swap
in) outro processo no espaço de memória que foi liberado. Enquanto isso, o escalo-
nador de CPU irá alocar uma fatia de tempo para outro processo em memória. Con-
forme cada processo tem seu quantum expirado, ele será trocado (swapped) por
outro processo que estava na área de swapping. Em uma situação ideal, o gerenci-
ador de memória conseguirá trocar processos em uma velocidade tal que sempre
haja processos em memória, prontos para executar, sempre que o escalonador de
CPU decidir colocar outro processo em execução. O quantum dos processos devem
também ser suficientemente grande para que os processos consigam processar por
um tempo razoável antes de serem retirados (swapped out) da memória.
Uma variação desta política de swapping poderia ser usada para algortimos de esca-
lonamento baseados em prioridades. Se um processo com maior prioridade chega
no sistema e deseja CPU, então o gerenciador de memória poderia retirar um ou
mais processos com prioridades mais baixas de forma que ele possa carregar e e-
xecutar o processo de prioridade mais alta. Quando este processo de alta prioridade
terminasse, os processos de baixa prioridade poderiam ser trazidos de volta para a
memória e continuarem executando. Esta variante de swapping é às vezes chamada
de roll out, roll in.
Para o uso eficiente de CPU, é desejável que o tempo de execução para cada pro-
cesso seja longo em relação ao tempo de swap. Dessa forma, em um algoritmo de
escalonamento round-robin, por exemplo, o tempo do quantum deveria ser substan-
cialmente maior do que 0.216 segundos.
Portanto, seria útil saber exatamente quanta memória um processo de usuário está
usando, não simplesmente quanto ele poderia estar usando. Assim, precisaríamos
apenas retirar da memória a quantidade de memória realmente usada, reduzindo o
tempo de swap. Para que este esquema seja efetivo, o usuário deve manter o siste-
ma informado de quaisquer mudanças das necessidades de memória.
Como exemplo, assuma que a operação de I/O do processo P1 foi enfileirada por-
que o dispositivo estava ocupado. Se trocarmos o proccesso P1 pelo processo P2
que estava na área de swapping, assim que o dispositivo tornar-se livre, a operação
de I/O poderá tentar usar uma área de memória que agora pertence ao processo P2.
Duas soluções possíveis são: (1) nunca retirar para a área de swap um processo
com I/O pendente; ou (2) executar operações de I/O somente em buffers do próprio
sistema operacional. Neste último caso, as transferências entre o sistema operacio-
nal e a área de memória do processo somente ocorre quando o processo está pre-
sente na memória real.
O terceiro esquema da figura 3.4 apresenta algumas rotinas em ROM. Esta é a con-
figuração básica do IBM-PC. Alguns sistemas operacionais mais modernos podem
completamente ignorar as rotinas em ROM (BIOS – Basic Input Output System –
Sistema Básico de Entrada e Saída) do PC, que são usadas apenas durante o pro-
cesso de boot do sistema operacional.
Quando o sistema está organizado desta forma, somente um processo pode estar
rodando por vez. O usuário digita um comando no console, e o sistema operacional
carrega do disco e executa o programa desejado. Quando o programa termina, o
sistema operacional novamente apresenta um prompt para o usuário, esperando por
Apostila de Sistemas Operacionais - Prof. Bruno 89
um novo comando. A execução de um novo programa irá sobrepôr o conteúdo da
memória que foi usada pelo programa anterior.
Está claro que o SO deve ser protegido do usuário. Proteção pode ser implementada
simplesmente através de um recurso chamado boundary register (registrador de limi-
te), existente em algumas CPUs, conforme ilustrado na figura 5.5.
Sistema 0
Operacional
a CPU
Área do boundary
programa do register
usuário
a
Espaço livre
O boundary register contém o endereço da memória mais alta usada pelo SO. Cada
vez que um programa do usuário faz referência a um endereço de memória, o regis-
trador de limitação é verificado para certificar que o usuário não está prestes a es-
crever sobre a área de memória do SO. Se o usuário tenta entrar no código do SO, a
Apostila de Sistemas Operacionais - Prof. Bruno 90
instrução é interceptada e o job terminado com uma mensagem de erro apropriada.
Entretanto, é claro que o programa do usuário eventualmente precisa chamar certas
funções que estão no código do SO. Para isso o usuário usa uma instrução específi-
ca com a qual ele requisita serviços do SO (uma instrução de chamada em modo
supervisor - supervisor call). Por exemplo, o usuário desejando ler da unidade de fita
vai disparar uma instrução requisitando a operação ao SO, que executará a função
desejada e retornará o controle ao programa do usuário.
0
Sistema
Operacional
1432 CPU
Área do
programa do
usuário registrador
1432 de relocação
registrador
7000
de limite
8432
Espaço livre
Com este esquema, toda vez que um processo solicita acesso à memória, ele deve
passar pela verificação dos dois registradores, conforme a figura 5.7:
Endereço Endereço
lógico sim físico
CPU < +
Memória
não
Exceção: e rro de
endereçamento
Vale notar que o registrador de relocação fornece um esquema eficiente para permi-
tir que o tamanho do SO em memória cresça ou diminua dinamicamente. Esta flexi-
bilidade é desejável em muitas situações. Por exemplo, o SO contém código e buf-
fers para os device drivers. Se um device driver (ou um outor serviço do SO) não é
usado com freqüência, é interesante que suas áreas de código e dados sejam libe-
radas para uso pelos programas dos usuários. Este tipo de código é chamado de
código transiente (ao contrário de residente), pois ele é carregado e descarregado
da memória conforme sua necessidade. Dessa forma, ao carregar um driver deste
tipo, o sistema operacional muda de tamanho em memória, e o registrador de relo-
cação resolve o problema.
5.5.1 Paginação
É a técnica de gerência de memória onde o espaço de endereçamento virtual e o
espaço de endereçamento real são divididos em blocos do mesmo tamanho, cha-
mandos de páginas. As páginas no espaço virtual são denomindadas páginas virtu-
ais, enquanto as páginas no espaço real são chamadas de páginas reais ou frames.
O tamanho da página varia de sistema para sistema, mas normalmente está entre
512bytes a 4 kbytes. A maioria dos estudos em relação ao tamanho ideal da página
indica páginas de tamanho pequeno.
• Aleatória ⇒ a escolha aleatória como o nome já diz, não utiliza critéio algum de
seleção. Todas as páginas tem a mesma chance de serem selecionadas, inclusive
as páginas que são frequentemente referenciadas. Está estratégia consome poucos
recursos do sistema, mas é raramente utilizada.
• First-In-First-Out (FIFO) ⇒ nesse esquema, a página que primeiro foi utilizada será
a primeira a ser escolhida. Sua implementação é muito simples, sendo necessária
apenas uma fila, onde as páginas mais antigas estão no início da fila e as mais re-
centes no final. Este sistema tende a retornar a mesma página várias vezes.
5.5.3 Segmentação
É a técnica de gerência de memória, onde os programas são divididos logicamente
em sub-rotinas e estruturas de dados, e colocados em blocos de informações na
memória. Os blocos tem tamanhos diferentes e são chamados de segmentos, cada
um com seu próprio espaço de endereçamento.
O sistema operacional mantém uma tabela com as áreas livres e ocupadas da me-
mória. Quando um novo processo é carregado para a memória, o sistema localiza
um espaço livre que o acomode. Na segmentação somente os segmentos referenci-
ados são transferidos da memória secundária para a memória real.
Livro de Apoio:
Introdução à Arquitetura de Sistemas Operacionais
Francis B. Machado e Luiz Paulo Maia - Ed. Livros Técnicos e Científicos
Literatura Auxiliar:
Projeto de Sistemas Operacionais em Linguagem C.
Albuquerque, F. Ed. IBM Books EBRAS