So Apost R2 PDF
So Apost R2 PDF
So Apost R2 PDF
OPERACIONAIS
1. INTRODUÇÃO
1.1. Definição
Qualquer pessoa que teve algum contato com um sistema de computação sabe que o
mesmo consiste em dois componentes principais: o hardware (que engloba toda a parte
fisicamente montada, em geral em circuitos eletrônicos) e o software (que compreende toda
a programação para levar o hardware a executar o que foi determinado pelo usuário). É
óbvio que a falta de qualquer um dos componentes acima acarreta a inutilidade do outro,
bem como a ineficiência de um tem implicações sérias na utilização do outro.
O que talvez não seja de conhecimento difundido é o fato de que, nos sistemas atuais,
tanto o hardware como o software apresentam uma organização aproximada em camadas, de
forma a facilitar a sua utilização. É possível apresentar um esquema para essa organização
como na Figura1:
Como formador de uma máquina virtual, a função do S.O. é apresentar ao usuário uma
máquina com as seguintes características:
i) Facilidade de operação: isto é , o S.O. deve fornecer uma interface entre o usuário e o
hardware que apresenta maior facilidade de programação de a presente originalmente no
hardware. Um exemplo típico disto é a escrita em disco flexível. Para esta operação, um
controlador precisa das seguintes instruções:
recalibração: corresponde a um ajuste da cabeça de leitura na primeira trilha, pois cada
movimento posterior da cabeça é sempre relativo, de forma que se a mesma se apresenta
inicialmente mal posicionada, todos os futuros posicionamentos serão errados;
movimento da cabeça: isto é , o deslocamento da mesma para a trilha requerida;
espera do setor: que representa uma espera até que a cabeça de leitura se posiciona sobre o
setor;
escrita dos dados - verificação: para garantir que os dados foram verdadeiramente escritos e
sem nenhum erro.
Além destes passos podem existir outros, dependendo do controlador utilizado, do tipo
de acesso (leitura, escrita, formatação, etc) e de se ocorrem erros ou não. Atualmente
existem diversos controladores de disco que cuidam eles mesmo de tarefas como
posicionamento da cabeça, leitura ou escrita, verificação de erros. No entanto mesmo nestes
casos o processo de controle restante ao processador central é complicado, pois exige
tarefas como:
• controle das trilhas e setores físicos onde se encontra ou deve ser colocada a informação
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 3
• tentar novamente no caso de ocorrência de erros, visto que os mesmos são bastante
frequentes nos meios magnéticos atuais devido a condições transitórias
Como vemos, o programador médio não deve ser envolvido com os detalhes deste
processo todo. Para isto, os sistemas operacionais fornecem métodos muito mais simples e
estruturados de uma forma mais voltada aos problemas do usuário do que é a estrutura
interna do computador
ii) Extensão das capacidades da máquina: o S.O. pode fornecer também algumas
capacidades não presentes no computador original, como, por exemplo, múltiplos
usuários e sistemas de proteção de acesso.
1.2. Histórico
Para uma melhor idéia do curso de desenvolvimento dos sistemas operacionais atuais,
apresentamos a esquematização da evolução histórica dos mesmos, enfatizando a relação
entre a evolução dos S.O. e os avanços em hardware.
conceito de sistema operacional, sendo que cada usuário introduzia o seu programa por
painéis e aguardava os resultados. A probabilidade de falha do sistema durante a execução
de algum programa era altíssima, devido à baixa confiabilidade das válvulas a vácuo.
v) esta fita de saída é levada ao computador secundário (mais barato), lida e seu conteúdo
impresso em uma impressora comum;
vi) a saída da impressora é entregue aos usuários.
Este processo, denominado OFF-LINE, garantiu uma maior eficiência na utilização do
processador principal. Porém aumentou o tempo de resposta do sistema para cada usuário.
Este aumento do tempo de resposta do sistema se dá em função de se juntar uma
quantidade razoável de conjuntos de cartões para se gravar uma fita. Desta forma, cada
usuário, para obter a resposta a seu programa, deve aguardar a execução de diversos outros
programas armazenados na mesma fita. Isto fica ainda mais crítico quando um dos
programas de uma fita apresenta um tempo de execução muito elevado.
Com a introdução de circuitos integrados, houve uma grande redução no tamanho e custo
dos sistemas, bem com um aumento em sua complexidade e generalidade. Isto permitiu o
desenvolvimento de dispositivos de entrada e saída inteligentes, de forma que os próprios se
responsabilizam pelo controle da transferência de dados entre eles e a memória principal.
Outro desenvolvimento importante foi a introdução dos discos, que permitem um acesso
aleatório à informação contida nos mesmos, diferentemente das fitas magnéticas, que
somente permitem um acesso aos dados na ordem em que os mesmos estão gravados (note
que isto pode ficar
transparente através de uma programação cuidadosa, entretanto com alto custo em tempo de
execução). Estes foram fatores fundamentais para o sucesso do conceito de
multiprogramação, apresentado a seguir.
Simultaneamente com a utilização de circuitos integrados, surgiu o conceito de
multiprogramação. A idéia provém dos seguintes fatos:
i) durante a execução de programas que realizam alta utilização de cálculos (ex: programas
científicos) todos os dispositivos de entrada e saída permanecem inativos;
ii) durante a execução de programas com alta utilização de entrada e saída (ex: programas
comerciais com consultas à base de dados e impressão de relatórios) o processador
permanece durante grande porcentagem do tempo aguardando os dispositivos de
entrada/saída.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 6
Desta forma, surgiu a idéia de se colocar diversas tarefas (jobs) dentro de alguma
"partição" da memória principal e executando simultaneamente de forma que, se alguma
tarefa precisa aguardar a transferência de dados para um dispositivo, outra tarefa pode
utilizar o processador central neste período.
Outro conceito introduzido foi o de "SPOOL (de "Simultaneous Peripherical Operation
On Line") que corresponde à leitura imediata dos jobs para o disco no momento da sua
chegada, sendo que ao terminar um dos jobs ativos, um novo job é imediatamente
carregado do disco para a partição de memória vazia e executado (partição é um trecho de
memória alocado a um job). Este processo tem a vantagem de que, com a leitura simultânea
dos dados para um meio de armazenamento mais rápido e com a transferência de dados
entre os meios realizada simultaneamente com a operação da unidade de processamento
principal, desapareceu praticamente o tempo manual de montagem e desmontagem de fitas.
Além disso, dispondo de diversos jobs a serem executados no disco, o sistema operacional
podia escolher entre eles por prioridade, e não necessariamente por ordem de chegada.
Entretanto, até este ponto, o sistema continuava sendo um sistema de lotes, sendo o
tempo entre a apresentação de um conjunto de cartões e a retirada do resultado
extremamente alto, principalmente quando se está realizando a depuração de programas.
Para diminuir o tempo de resposta do sistema a um dado job foi introduzido o conceito de
compartilhamento de tempo ("time-sharing"), no qual cada usuário possui um terminal
ligado em linha com o computador, podendo ainda o computador rodar, no fundo, alguns
lotes com a utilização do tempo disponível devido à lentidão de entrada de dados dos
usuários.
Nesta época também surgiram os minicomputadores, com uma menor capacidade de
processamento numérico mas também com um custo muito menor.
2. PROCESSOS
2.1. Introdução
isto, quer-se dizer que não é possível assumir nada com relação à taxa com que cada
processo será executado, nem em relação a outros processos, nem em relação a diversas
execuções de um mesmo processo. Uma das implicações disto é que os programas não
podem ser feitos levando em consideração as temporizações de execução das instruções
(p.ex.: não se pode fazer um programa que execute certo número de repetições de um "loop"
e com isso espere conseguir uma demora fixa de tempo, como um segundo). Isto ocorre
porque não se sabe o momento em que a UCP será chaveada para outro processo, fazendo
com que o atual tenha sua continuação retardada.
Para fixar melhor a diferença entre um programa e um processo, observe a seguinte
comparação extravagante:
Suponha um padeiro não muito experiente. Para fazer um pão ele se utiliza de alguns
ingredientes (farinha, sal, água, bromato, etc.) e segue a receita de um livro de receitas.
Neste caso poder-se-ia estabelecer a seguinte comparação: O padeiro é a UCP, os
ingredientes são as entradas e a receita é o programa (desde que o padeiro entenda tudo o
que está escrito na receita, isto é , que a mesma esteja em uma linguagem apropriada). O
processo neste caso corresponde ao ato de o padeiro estar executando o pão da receita com
as entradas disponíveis.
Suponha agora que, enquanto o padeiro está amassando o pão (um dos passos
indicados pela receita) ele é picado por uma abelha. Como não é muito forte, ele começa a
chorar e julga extremamente importante cuidar da picada de abelha antes de acabar o pão.
Para isto ele dispõe de novas entradas (os medicamentos) e de um novo programa (o livro
de primeiros socorros). Terminado o curativo da picada o padeiro volta a amassar o pão no
mesmo ponto em que parou. Aqui tem-se uma interrupção (a picada de abelha) fazendo a
UCP (padeiro) chavear do processo de preparação do pão para o processo de curativo da
picada. Outro fato importante aqui é que o processo de preparação do pão deve ser
guardado com todo o seu estado corrente (isto é , em que ponto da receita ele está e qual a
disposição de todos os componentes da massa e seu estado atual) para que possa ser
reassumido no mesmo ponto em que foi interrompido.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 10
Outro estado a considerar é quando o processo tem todas as condições de executar, mas
não pode pois a UCP foi alocada para a execução de um outro processo. Neste caso o
processo está pronto.
Os três estados e suas interrelações podem ser apresentados como na Figura 3:
EXECUÇÃO
1
3
BLOQUEADO PRONTO
4
Para a implementação de processos, além dos programas à serem executados, devem ser
guardadas algumas informações, necessárias ao escalonador, para permitir que um processo
seja reassumido exatamente no ponto em que foi interrompido. Esses dados são
armazenados na chamada tabela de processos, que consiste em um vetor de estruturas com
uma entrada por processo. Esta estrutura contém dados como: contador de programa (PC),
ponteiro de pilha (SP), estado dos arquivos abertos, registradores, além de diversas outras
informações.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 12
Figura 4
Tem-se também duas variáveis, entra e sai que são utilizadas respectivamente para
indicar qual a próxima unidade de diretório vazia (e que portanto pode ser ocupada por um
novo nome de arquivo) e qual a próxima unidade a ser impressa.
Supondo que, no caso indicado na Figura 4, onde entra=7, um processo A resolve
requisitar a impressão de um arquivo chamado a.prg. Para isto, ele precisa ler o valor da
variável entra, colocar o nome do arquivo na unidade correspondente e incrementar o valor
de entra de um. Supondo então que o processo A leu entra=7, após o que foi interrompido
pelo escalonador, que determinou o início da execução do processo B. Este processo (B),
resolve então imprimir o arquivo b.prg. Para isto lê o valor de entra, onde encontra 7, coloca
b.prg na unidade 7 do diretório de spool, e incrementa entra para 8. Após isto o escalonador
determina a volta da execução do processo A, que retorna exatamente no ponto onde estava.
Então, para o processo A, o valor de entra continua sendo 7 e, portanto, ele colocar a.prg na
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 13
unidade 7 do diretório de spool (exatamente onde tinha sido colocado b.prg) e incrementa o
valor de entra que leu (isto é , 7) gerando como resultado 8, que será armazenado em entra.
O resultado de tudo isto é que o pedido de impressão de b.prg desapareceu, e portanto o
processo que o requisitou não será servido.
Isto é o que se chama de uma condição de disputa.
Esta é uma solução que obriga que a região crítica seja dada a um dos processos por vez,
em uma estrita alternância. O algoritmo apresentado abaixo representa um exemplo disto,
com a utilização de uma variável vez, que indica de qual processo é a vez de entrar na
região crítica:
procedure proc_0;
begin
while (TRUE)
do begin
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 15
procedure proc_1;
begin
while (TRUE)
do begin
while (vez ≠ 1) (*espera*) do;
secao_critica;
vez := 0;
secao_normal;
end
end;
prosseguir pois está aguardando alguma condição que somente pode ser atingida pela
execução do outro.
Para isto, define-se duas rotinas SLEEP e WAKEUP, que realizam a espera através do
bloqueamento do processo, ao invés do desperdício do tempo de UCP.
SLEEP: faz com que o processo que está executando seja transferido do estado de rodando
para o de bloqueado.
WAKEUP: pega um processo em estado bloqueado e o transfere para o estado pronto,
colocando-o disponível para execução quando o escalonador julgar adequado.
Para exemplificar a utilização de SLEEP e WAKEUP, observe o problema do produtor e
do consumidor. Neste problema clássico existem dois processos: um chamado produtor
que coloca dados em um buffer, e outro chamado consumidor que retira dados do buffer.
O buffer apresenta uma capacidade finita de reter dados, de forma que surgem problemas
em duas situações:
i) quando o produtor deseja colocar mais dados em um buffer cheio
ii) quando o consumidor deseja retirar dados de um buffer vazio.
Em ambos os casos, faz-se com que o processo que não pode acessar o buffer no
momento execute um SLEEP e seja bloqueado, somente sendo acordado quando o outro
processo alterar a situação do buffer e executar WAKEUP.
No programa abaixo tem-se uma tentativa de resolver o problema do produtor-
consumidor com a utilização de SLEEP e WAKEUP. A variável cont é compartilhada
entre os dois processos:
cont:=0; (* inicialmente buffer vazio *)
procedure produtor;
begin
while (TRUE)
do begin
produz_item(item); (* produz um item *)
if (cont=N) then SLEEP; (* se buffer cheio, dorme *)
entra_item(item); (* coloca item no buffer *)
cont:= cont+1; (* incremente n. de itens *)
if (cont=1)
then WAKEUP(consumidor); (*se buffer estava vazio,
end acorda consumidor *)
end;
procedure consumidor;
begin
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 17
while (TRUE)
do begin
if (cont=0) then SLEEP; (* se buffer vazio, dorme *)
remove_item(item); (* lê item do buffer *)
cont:= cont-1; (* decrementa n. de itens *)
if (cont=N-1)
then WAKEUP(produtor); (* se buffer estava cheio, acorda produtor *)
consome_item(item); (* utiliza o item lido *)
end
end;
Tem-se portanto uma condição em que tanto o produtor como o consumidor estão
dormindo, não podendo ser acordados pelos outros, o que caracteriza novamente um
deadlock.
Para este caso simples, o problema pode ser resolvido com a inclusão de um bit de espera
de WAKEUP. Assim, quando um WAKEUP for enviado para um processo já acordado, o
bit será marcado, sendo que a próxima vez que o processo realizar SLEEP, se o bit estiver
marcado então o processamento prossegue como se o WAKEUP tivesse sido automático.
Entretanto, esta solução não resolve o problema geral. Pode-se, por exemplo, construir
casos com três processos onde um bit de espera não é suficiente. O aumento para três ou
mais bits poderia resolver esses casos, porém o problema geral ainda permaneceria.
3.5. Semáforos
Para resolver este problema, Dijkstra propôs a utilização de uma variável inteira que
conta o número de WAKEUP realizados. A essa variável se deu o nome de semáforo.
Para trabalhar com os semáforos Dijkstra propôs duas primitivas:
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 18
Veja que, nas representações acima, são consideradas todas as ações de DOWN(v) e
UP(v) como indivisíveis, isto é , o processamento é realizado sem interrupções.
A solução do produtor-consumidor com semáforos é apresentada abaixo, onde
utilizamos três variáveis semáforo:
mutex : semáforo binário que garante a exclusão mútua dentro da região crítica
vazio : que indica o número de posições vazias no buffer
cheio : que indica o número de posições cheias no buffer
procedure produtor;
begin
while (TRUE)
do begin
produz_item(item);
DOWN(vazio); (* um vazio a menos *)
DOWN(mutex); (* testa exclusao mutua *)
entra_item(item); (* poe item no buffer *)
UP(mutex); (* sai da exclusao mutua *)
UP(cheio); (* um a mais cheio *)
end
end;
procedure consumidor;
begin
while (TRUE)
do begin
DOWN(cheio); (* um cheio a menos *)
DOWN(mutex); (* testa exclusao mutua *)
remove_item(item); (* le item do buffer *
UP(mutex); (* sai da exclusao mutua *)
UP(vazio); (* um vazio a mais *)
consome_item(item);
end
end;
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 19
procedure produtor;
begin
while (TRUE)
do begin
produz_item(item);
RECEIVE(consumidor,m); (* aguarda mensagem vazia *)
faz_mensagem(m,item); (* constroi mensagem*)
SEND(consumidor,m); (* envia item ao consumidor *)
end
end;
procedure consumidor;
begin
for i:=0 to N-1 do SEND(produtor,m); (* envia N vazios *)
while (TRUE)
do begin
RECEIVE(produtor,m); (* pega mensagem *)
extrai_item(m,item); (* pega item da mensagem *)
consome_item(item);
SEND(produtor,m); (* envia resposta vazia *)
end
end;
Nesta solução assumimos que todas as mensagens têm o mesmo tamanho e que
mensagens enviadas, mas ainda não recebidas, são automaticamente armazenadas pelo
sistema operacional.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 21
No início são enviados N vazios para permitir ao produtor encher todo o buffer antes de
bloquear. Caso o buffer esteja cheio, o produtor bloqueia no RECEIVE e aguarda a chegada
de uma nova mensagem vazia.
A passagem de mensagens pode ser feita de diversas formas, das quais citaremos três:
1. assinalando-se um endereço único para cada processo e fazendo as mensagens serem
endereçadas aos processos;
2. com a utilização de mailbox, uma nova estrutura de dados que representa um local onde
diversas mensagens podem ser bufferizadas. Neste caso, os endereços em SEND e
RECEIVE são as mailbox e não os processos. Se um processo tenta enviar para uma
mailbox cheia ou receber de uma mailbox vazia ele é automaticamente suspenso até que
a mailbox apresente as condições para a operação;
3. eliminando toda a bufferização, fazendo com que, se o processo que envia executa SEND
antes do de recepção, então, ele é bloqueado até que este último esteja pronto (e vice-
versa). Este mecanismo é conhecido como rendezvous;
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 22
4. ESCALONAMENTO DE PROCESSOS
Chamamos de escalonamento (scheduling) de processos o ato de realizar o chaveamento
dos processos ativos, de acordo com regras bem estabelecidas, de forma a que todos os
processos tenham a sua chance de utilizar a UCP.
O escalonador (scheduler) é , portanto, a parte do S.O. encarregada de decidir, entre
todos os processos prontos para executar, qual o que será rodado em cada instante.
Chamamos de algoritmo de escalonamento o método utilizado pelo escalonador para
decidir, a cada instante, qual o próximo processo a ser executado.
Podemos estabelecer os seguintes critérios para um algoritmo de escalonamento:
a. justiça: cada processo deve conseguir sua parte justa do tempo de UCP;
b. eficiência: garantir uma ocupação de 100% do tempo da UCP;
c. tempo de resposta: minimizar o tempo de resposta a comandos de usuários interativos;
d. minimização do tempo que os usuários de lotes (batch) devem aguardar até conseguir a
saída desses pedidos;
e. maximizar o número de serviços (jobs) processados por hora.
Como pode ser facilmente notado, vários critérios acima entram em contradição entre si,
de forma que devemos prejudicar alguns para conseguir melhoras em outros. O escalonador
deve, portanto, realizar algum compromisso entre os diversos critérios.
Para dificultar que os objetivos sejam alcançados, ainda temos o fato de que cada
processo é único e imprevisível.
Com o objetivo de prevenir que um processo rode durante um tempo excessivo, a
maioria dos computadores dispõe de um relógio, que é um dispositivo de temporização que
interrompe a UCP a cada intervalo dado de tempo. A cada interrupção, o S.O. interrompe a
execução do processo atual e roda o escalonador para decidir qual o próximo processo a ser
executado.
Esta estratégia de interromper um processo em execução é chamada de
escalonamento preemptivo (preemptive scheduling), e contrasta com a estratégia de
execução até o término.
Vejamos agora algumas formas de implementar o escalonamento.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 23
Figura 5
O valor do quantum deve ser cuidadosamente escolhido.Isso ocorre porque existe o
tempo de chaveamento (tempo para que o processo em execução seja interrompido; tenha
seu estado atual salvo; o escalonador decida qual o próximo processo e o estado deste novo
processo seja restaurado). Assim, se o valor do quantum for muito pequeno, teremos uma
grande proporção do tempo de execução da UCP gasta com o processo de chaveamento, que
não é de interesse para os usuários. Por exemplo, se todo o chaveamento de processos leva
5ms, e o quantum for de 20ms, teremos uma utilização de 20% do tempo total para o
chaveamento.
Por outro lado, se o valor do quantum for muito grande, isto pode acarretar um tempo de
resposta muito grande para usuários interativos. Veja por exemplo se, para um
chaveamento de 5ms escolhemos um quantum de 500ms. Neste caso o tempo gasto com o
chaveamento é menos que 1% do tempo total. Entretanto, se temos 10 processos
executando, o tempo de resposta para um processo pode ser de até 5s. Devemos pois
escolher um valor de compromisso, nem muito grande nem muito pequeno, de acordo com
o tempo de chaveamento.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 24
O esquema de round-robin trata igualmente todos os processos dos sistemas, sejam eles
altamente urgentes e importantes, sejam eles de pouca importância ou sem urgência. Para
poder oferecer um tratamento distinto à processos diversos, devemos introduzir o conceito
de prioridade.
No instante da criação de um processo, associamos ao mesmo uma prioridade. Quando o
escalonador tiver que escolher, entre os processos prontos, qual o que será executado em
seguida, ele escolhe o de mais alta prioridade. Como esse esquema pode fazer com que um
processo seja executado indefinidamente, não deixando espaço a outros processos, o
escalonador pode, a cada vez que o processo é escalado para execução, decrementar sua
prioridade. No momento em que sua prioridade fica abaixo de um outro processo pronto ele
é interrompido e o outro processo (agora com prioridade mais alta) é executado.
As prioridades podem ser associadas a processos de forma estática ou dinâmica.
Dizemos que uma prioridade é estática quando ela é associada no momento da criação do
processo (mesmo que depois ela seja decrementada para impedir o processo de monopolizar
a UCP). Uma prioridade é dinâmica quando o escalonador é que decide seu valor, baseado
em estatísticas sobre a execução deste processo em quanta anteriores.
Como exemplo de prioridades estáticas, podemos imaginar que em um sistema
implantado em um quartel, os processos iniciados por um general devem ter prioridade
sobre os processos iniciados por um coronel, e assim por diante, por toda a hierarquia.
Como exemplo de prioridade dinâmica, podemos citar um sistema que, quando percebe
que um dado processo efetua muitas operações de entrada/saída, aumenta a sua prioridade.
Isto é feito pois as operações de entrada/saída são realizadas independentemente da UCP,
de forma que, se um processo executa muitas dessas operações, é conveniente deixar que
ele requisite uma nova operação o mais rápido possível. Lembre-se que, ao requisitar uma
operação de entrada/saída o processo faz uma chamada para o S.O. e fica bloqueado,
aguardando o fim da operação. Assim, processos com grande utilização de E/S não
significam muita carga para a UCP, pois enquanto suas transferências estão se executando a
UCP pode estar alocada a outro processo que executa cálculos. Para implementar essa forma
de prioridade, o escalonador calcula a fração de tempo que o processo utilizou do seu
quantum na última chamada. Se o processo utilizou apenas uma pequena fração (e não
terminou) é provavelmente porque está esperando dados de um dispositivo de E/S. Desta
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 25
chaveamentos para este processo), sendo que no método de round-robin tradicional ele seria
selecionado 100 vezes até acabar (100 chaveamentos).
Por outro lado, este método, como definido até aqui, tem o grave inconveniente de que,
se um processo começa com uma grande quantidade de cálculos, mas em seguida se torna
interativo (isto é , exige comunicação com o usuário), teríamos um tempo de resposta muito
ruim. Para eliminar este problema, foi implementado o seguinte método: cada vez que um
<RET> é teclado no terminal de um processo, este processo é transferido para a classe de
prioridade mais alta, na esperança de que o mesmo irá se tornar interativo.
Esta solução funcionou bem até que um usuário descobriu que, durante a execução de
um processo grande, bastava sentar no terminal e teclar aleatoriamente <RET> para ter o
tempo de execução melhorado. Em seguida ele contou sua "descoberta" a todos. Vê-se então
que a implementação prática de uma estratégia eficiente é muito mais difícil na prática do
que em princípio.
Até aqui, consideramos que todos os processo executáveis estão na memória. Este
geralmente não é o caso, pois dificilmente a memória principal comportará todos os dados
necessários. Precisaremos, pois, manter parte dos processos em disco.
O problema que surge é que o tempo para ativar um processo que está em disco é
muito maior que o tempo necessário para ativar um processo que está na memória (uma a
duas ordens de grandeza maior).
A melhor solução para isto é a utilização de um escalonador em dois níveis. O
funcionamento será então da seguinte forma:
i. um subconjunto dos processos executáveis é mantido na memória;
ii. um outro subconjunto é mantido no disco;
iii. um escalonador (chamado de baixo nível) é utilizado para realizar o chaveamento (por
qualquer dos métodos já descritos, ou outros semelhantes) apenas entre os processos que
estão na memória;
iv. um outro escalonador (chamado de alto nível) é utilizado para trocar periodicamente o
conjunto de processo que estão na memória (substituindo-os por alguns que estavam no
disco).
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 27
Para realizar a escolha de qual conjunto de processos deve ser colocado na memória
principal o escalonador de alto nível deve responder perguntas tais como:
• Quanto tempo se passou desde que o processo foi posto ou retirado ?
• Quanto tempo de UCP o processo teve recentemente ?
• Qual o tamanho do processo ?
• Qual a prioridade do processo ?
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 28
5. GERENCIAMENTO DE MEMÓRIA
Neste caso, temos um único processo executando por vez, de forma que o mesmo pode
utilizar toda a memória disponível, com exceção da parte reservada ao sistema operacional,
que permanece constantemente em local pré-determinado da memória. O S.O. carrega um
programa do disco para a memória, executa-o e, em seguida, aguarda comandos do usuário
para carregar um novo programa, que se sobrepor ao anterior.
5.3. Multiprogramação
necessidade de chavear de um processo para outro (o que, com apenas um processo por
vez na memória representaria constantemente estar lendo e escrevendo no disco).
• É necessário que diversos processos estejam "simultaneamente" em execução, devido ao
fato de que muitos deles estão constantemente realizando operações de E/S, o que
implica em grandes esperas, nas quais, por questão de eficiência, a UCP deve ser
entregue a outro processo.
Uma desvantagem óbvia desse esquema é a de que pode ocorrer que uma partição grande
esteja sem utilização, enquanto que diversos processos estão aguardando para utilizar uma
partição menor (p.ex.: na figura acima temos três processos aguardando pela partição 1,
enquanto que a partição 3 está desocupada).
Podemos resolver esse problema da seguinte forma:
(i) estabelecemos apenas uma fila para todas as partições;
(ii)quando uma partição fica livre, um novo processo que caiba na partição livre é escolhido
e colocado na mesma, conforme indicado na Figura 7:
Suponha uma rotina em um programa que, ao término da ligação dos módulos deste, é
colocada na posição 100 em relação ao início do programa. É claro que se esse programa for
ser executado na partição 1, todas as chamadas dessa rotina devem ser enviadas para a
posição de memória 100k+100. Se o programa for executado na partição 2, as chamadas
para essa mesma rotina devem ser enviadas para a posição 200k+100.
Uma possível solução é modificar as instruções conforme o programa é carregado na
memória. Desta forma, quando o S.O. carrega o programa, adiciona a todas as instruções
que se referenciem a endereços, o valor do ponto inicial de carga do programa (i.e., o início
da partição escolhida para o mesmo). Esta solução exige que o ligador (linker) coloque no
início do código binário do programa uma tabela (ou uma lista) que apresente as indicações
das posições no programa que devem ser modificadas no carregamento.
Esta solução entretanto não resolve o problema da proteção, pois nada impede que um
programa errado ou malicioso leia ou altere posições de memória de outros usuários (dado
que as referências são sempre as posições absolutas de memória).
Uma solução para isto adotada no IBM 360 foi a de dividir a memória em unidades de 2k
bytes e associar um código de proteção de 4 bits a cada uma dessas regiões. Durante a
execução de um processo, o PSW contém um código de 4 bits, que é testado com todos os
acessos à memória realizados pelo processo, e gera uma interrupção se tentar acessar uma
região de código diferente.
Uma solução alternativa tanto para o problema da relocação como para o da proteção é a
utilização de registradores de base e limite. Sempre que um processo é carregado na
memória, o S.O. ajusta o valor do registrador de base de acordo com a disponibilidade de
memória. Este registrador é utilizado de forma que, sempre que um acesso é realizado na
memória pelo processo, o valor do registrador de base é automaticamente somado, o que faz
com que o código do programa em si não precise ser modificado durante o carregamento. O
registrador de limite é utilizado para determinar o espaço de memória que o processo pode
executar. Todo acesso realizado pelo processo à memória é testado com o valor do
registrador limite, para verificar se esse acesso está se realizando dentro do espaço reservado
ao processo (caso em que ele é válido) ou fora do seu espaço (acesso inválido). Uma
vantagem adicional do método dos registradores base e limite é o de permitir que um
programa seja movido na memória, mesmo após já estar em execução. No método anterior
isto não era possível sem realizar todo o processo de alteração dos endereços novamente.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 32
Num sistema em batch, desde que se mantenha a UCP ocupada o máximo de tempo
possível, não existe razão para se complicar o método de gerenciamento da memória.
Entretanto, num sistema de time-sharing, onde muitas vezes existe menos memória do que o
necessário para manter todos os processos de usuário, então é preciso que uma parte dos
processos sejam temporariamente mantidos em disco. Para executar processos que estão no
disco, eles devem ser enviados para a memória, o que significa retirar algum que lá estava.
Este processo é denominado troca (swapping), e é o que passaremos a estudar.
Figura 8
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 34
A desvantagem desse método, é que, quando um novo processo que ocupa k unidades de
memória deve ser carregado na memória, o gerenciador deve percorrer o mapa de bits para
encontrar k bits iguais a zero consecutivos, o que não é um processo simples.
Neste caso, mantemos uma lista encadeada de segmentos alocados e livres, sendo que
cada segmento é um processo ou um buraco entre dois processos. Na Figura 8,
anteriormente apresentada, temos também a lista associada ao trecho de memória indicado.
H indica um buraco e P um processo. A lista apresenta-se em ordem de endereços, de forma
que quando um processo termina ou é enviado para o disco, a atualização da lista é simples:
cada processo, desde que não seja nem o primeiro nem o último da lista, apresenta-se
cercado por dois segmentos, que podem ser buracos ou outros processos, o que nos dá as
quatro possibilidades mostradas na Figura 9:
Figura 9
Note que os buracos adjacentes devem ser combinados num único.
Vejamos agora alguns algoritmos que podem ser utilizados para escolher o ponto em que
deve ser carregado um processo recém criado ou que veio do disco por uma troca (foi
swapped in). Assumimos que o gerenciador de memória sabe quanto espaço alocar ao
processo:
1. First Fit (primeiro encaixe): neste caso, o algoritmo consiste em percorrer a fila até
encontrar o primeiro espaço em que caiba o processo. É um algoritmo rápido;
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 35
2. Next Fit (próximo encaixe): o mesmo que o algoritmo anterior, só que ao invés de
procurar sempre a partir do início da lista, procura a partir do último ponto em que
encontrou. Simulações mostraram que esse algoritmo apresenta um desempenho próximo
ao anterior;
3. Best Fit (melhor encaixe): consiste em verificar toda a lista e procurar o buraco que
tiver espaço mais próximo das necessidades do processo. É um algoritmo mais lento e,
além disso, simulações demonstram que tem um desempenho pior que o First Fit, devido
ao fato de tender a encher a memória com pequenos buracos sem nenhuma utilidade;
4. Worst Fit (pior ajuste): sugerido pelo fracasso do algoritmo anterior, consiste em pegar
sempre o maior buraco disponível. Simulações mostraram que também seu desempenho
é ruim.
Os quatro algoritmos podem ter sua velocidade aumentada pela manutenção de duas
listas separadas, uma para processos e outra para buracos. Ainda outro algoritmo possível,
quando temos duas listas separadas, é o Quick Fit (ajuste rápido), que consiste em manter
listas separadas para alguns dos tamanhos mais comuns especificados (por exemplo, uma
fila para 2k, outra para 4k, outra para 8k, etc.). Neste caso, a busca de um buraco com o
tamanho requerido é extremamente rápido, entretanto, quando um processo termina, a
liberação de seu espaço é complicada, devido à necessidade de reagrupar os buracos e
modificá-los de fila.
Chamamos de espaço de troca ao espaço ocupado no disco pelos processos que aí estão
guardados pelo fato de que foram retirados da memória devido a uma troca (swap).
Os algoritmos para gerenciar o espaço alocado em disco para swap são os mesmos
apresentados para o gerenciamento da memória, com a diferença de que em alguns sistemas,
cada processo tem no disco um espaço reservado para o mesmo, de forma que não está,
como na memória, sendo constantemente mudado de lugar.
Um fator adicional de diferença é o fato de que, pelos discos serem dispositivos de bloco,
a quantidade de espaço reservado para os processos no disco deverão ser múltiplas do
tamanho do bloco.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 36
5.6.1. Paginação
Paginação é uma técnica muito utilizada em sistemas com memória virtual. Antes,
estabeleçamos o conceito de espaço virtual.
Chamamos de espaço virtual ao espaço de memória que pode ser referenciado por um
programa qualquer em dado processador. Por exemplo, um processador com endereçamento
de 16 bits possui um espaço virtual de 64k bytes (se o endereçamento for em bytes). Quando
uma instrução como:
Quando o computador possui memória virtual, esse endereço virtual é enviado para uma
unidade de gerenciamento de memória, MMU (memory management unit), que corresponde
a um chip ou um conjunto de chips que translada esse endereço virtual em um endereço
físico, de acordo com uma tabela.
Figura 10
O espaço de endereços virtuais é dividido em unidades chamadas páginas e o espaço de
memória física é dividido em unidades chamadas quadros de página, de mesmo tamanho
das páginas. A MMU tem uma tabela que indica, para cada página, qual o quadro de página
que corresponde à mesma. No exemplo da Figura 10, temos um espaço virtual de 64k bytes,
uma memória física de 32k bytes, e páginas de 4k bytes. Se o processador tenta acessar o
endereço 0, a MMU verifica que isto corresponde ao primeiro endereço da primeira página,
verifica então que essa primeira página está alocada no terceiro quadro de página (i.e. o de
número 2). Converte então esse endereço para 8192 (decimal) e envia esse endereço
convertido para a memória. Note que nem o processador nem a memória precisaram ficar
sabendo da existência de paginação.
No entanto, como nem todas as páginas do espaço virtual podem estar residentes na
memória simultaneamente, ocorrer o caso de que um acesso seja realizado para uma página
que não está na memória. Para saber isto, a MMU mantém na tabela de translação um bit
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 38
para cada página que indica se a mesma está presente na memória ou não. Se um acesso for
realizado a uma página ausente, é gerada uma falta de página (page fault), o que chama uma
rotina de tratamento de interrupção específica para o S.O., que então se encarrega do
carregamento da página faltante e o ajuste correspondente na tabela de translação.
A forma mais comum de implementação da MMU, é escolher alguns dos bits mais
significativos do endereço virtual como indicadores do número de página e o restante dos
bits como um deslocamento dentro dessa página. Por exemplo, na Figura 10, apresentada
acima, de 16 bits do endereço virtual, 12 serão utilizados para o deslocamento (pois são
necessários 12 bits para endereçar os 4k bytes de uma página), sendo os 4 restantes
utilizados como um índice para qual das 16 páginas está sendo referenciada. A MMU
portanto, pega os 4 bits do índice de página, acessa a posição correspondente da tabela de
translação, verifica se a página está presente na memória, se não estiver, gera uma
interrupção para carregamento e, depois, verifica o valor colocado nessa entrada da tabela de
translação e os junta aos 12 bits de deslocamento dentro da página. A Figura 11 mostra a
operação da MMU.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 39
Figura 11
5.6.2. Segmentação
Figura 12
Note que o hardware suporta até 16 processos, cada um com 1024 páginas de 4k bytes cada
(isto é, cada processo com um endereço virtual de 4M bytes). A cada um dos 16 processos, a
MMU associa uma seção com 64 descritores de segmento, sendo que cada descritor contém
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 40
No momento em que é detectada a falta de uma página (i.e., ocorre um page fault), o
S.O. deve escolher uma das páginas atualmente residentes na memória para ser removida,
de forma a liberar um quadro de página para a colocação da página faltante. Deve-se tomar
o cuidado, caso a página a ser removida tenha sido alterada, de reescrevê-la no disco.
O problema que surge aqui é o seguinte: de que forma escolher a página a remover ?
É claro que devemos procurar remover uma página que não seja muito necessária aos
processos em execução, isto é, devemos remover uma página não muito utilizada. Se, ao
contrário, removêssemos uma página amplamente utilizada, em pouco tempo uma
referência à mesma seria realizada, o que ocasionaria um page-fault e a necessidade de
recarregar esta página, além de escolher outra para ser removida.
O que devemos considerar então é o seguinte: como determinar qual página não está
sendo muito utilizada ?
Os próximos ítens discutiram alguns métodos de se realizar essa escolha.
Este método seleciona para retirar uma página que não tenha sido recentemente utilizada.
Para determinar se uma página foi ou não utilizada recentemente, conta-se com o auxílio de
2 bits associados com cada uma das páginas na memória:
R: indica se a página já foi referenciada;
M: indica se a página foi modificada.
Estes bits, presentes em muitos hardwares de paginação, são alterados automaticamente
(por hardware) quando uma referência à página é realizada. Assim, quando uma instrução lê
um dado em uma certa posição de memória, o bit R da página correspondente é
automaticamente colocado em 1; quando uma instrução escreve em uma dada posição de
memória, os bits R e M da página correspondente são colocados em 1. Esses bits são
colocados inicialmente em 0 quando uma página é carregada na memória. Uma vez que
esses bits sejam colocados em 1, eles só podem voltar a 0 através de instruções em software.
O método de determinar se uma página foi recentemente utilizada é então o seguinte:
• a cada tiquetaque do relógio, o S.O. coloca todos os bits R das páginas em 0;
• quando ocorre um page fault, temos três categorias de páginas:
classe 0: R=0, M=0
classe 1: R=0, M=1
classe 2: R=1, M=0
classe 3: R=1, M=1
A classe 2 ocorre para uma página que foi referenciada pelo menos uma vez desde o
último tiquetaque do relógio, sem ter sido alterada. A classe 3 ocorre para uma página que já
foi alterada e referenciada desde o último tiquetaque. As classes 0 e 1 são as
correspondentes às classes 2 e 3 (respectivamente), que não tiveram nenhuma referência
desde o último tiquetaque do relógio.
- escolhe-se para ser retirada uma das páginas que pertencer à classe mais baixa não vazia,
no momento da ocorrência do page fault.
Esse algoritmo apresenta as vantagens de ser implementável de forma simples e
eficiente.
Quando o hardware não dispõe dos bits R e M, o S.O. pode simular esses bits através da
utilização dos mecanismos de proteção de páginas, da seguinte forma:
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 43
Neste caso, mantém-se uma fila de páginas referenciadas. Ao entrar uma nova página,
ela entra no fim da fila, substituindo a que estava colocada no início da fila.
O problema com este algoritmo é que pode retirar páginas muito valiosas (i.e., páginas
que, apesar de estarem a bastante tempo na memória estão sendo amplamente utilizadas).
Uma possível solução para esse problema consiste na utilização dos bits R e M:
• verifica os bits R e M da página mais antiga;
• se essa página for classe 0, escolhe-a para ser retirada;
• se não for, continua procurando na fila;
• se em toda a fila não achou nenhum classe 0, prossegue para as classes seguintes (1, 2 e
3).
Uma variação dessa solução é o algoritmo conhecido como segunda chance:
• verifica o bit R da página mais velha;
• se R for zero, utiliza essa página;
• se R for 1, põe R em 0 e coloca a página no fim da fila;
• prossegue analisando a fila, até encontrar uma página com R=0 (no pior caso, será a
primeira página, que teve seu R alterado para 0).
• a cada interrupção de relógio, o bit R correspondente a cada uma das páginas é somado a
esse contador (portanto, se a página foi referenciada dentro desse intervalo de relógio, o
valor do contador será incrementado, caso contrário, o valor do contador será mantido)
• quando ocorre um page fault, escolhe-se a página com valor menor nesse contador.
O grande problema com esse algoritmo é que, se uma página intensivamente utilizada
durante um certo tempo, ela tender a permanecer na memória, mesmo quando não for mais
necessária (pois adquiriu um valor alto no contador).
Felizmente, uma modificação simples pode ser feita no método evitando esse
inconveniente. A alteração consiste no seguinte:
• os contadores são deslocados 1 bit à direita, antes de somar R
• R é somado ao bit mais à esquerda (mais significativo) do contador, ao invés de ao bit
mais à direita (menos significativo).
Esse algoritmo é conhecido como algoritmo de aging. É obvio que uma página que não foi
referenciada a 4 tique-taques ter 4 zeros à esquerda e, portanto um valor baixo, colocando-a
como candidata à substituição.
Este algoritmo tem duas diferenças fundamentais em relação ao algoritmo de LRU:
1. não consegue decidir qual a referencia mais recente com intervalos menores que um
tiquetaque;
2. o número de bits do contador é finito e, portanto, quando este chega a zero, não
conseguimos mais distinguir as páginas mais antigas.
Por exemplo, com 8 bits no contador, uma página não referenciada a 9 tique-taques não
pode ser distinguida de uma página não referenciada a 1000 tique-taques. No entanto, em
geral 8 bits é suficiente para determinar que uma página já não é mais necessária.
Veremos alguns pontos que devem ser considerados, além do algoritmo de paginação
propriamente dito, para que um sistema de paginação tenha um bom desempenho.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 46
Num sistema puro de paginação, que também pode ser chamado de paginação por
demanda, o sistema começa sem nenhuma página na memória e elas vão sendo carregadas à
medida em que forem necessária, seguindo o algoritmo de substituição de página escolhido.
Esta estratégia pura pode ser melhorada, como veremos a seguir.
Um primeiro dado importante a considerar é a existência, na grande maioria dos
processos, de uma localidade de referências, o que significa que os processos mantém, em
cada uma das fases de sua execução, referências a frações pequenas do total do número de
páginas necessárias a ele. Um exemplo disso é um compilador, que durante um certo tempo
acessa as páginas correspondentes à análise sintática, depois essas páginas deixam de ser
necessárias, passando-se então a utilizar as de análise semântica e assim por diante.
Baseado neste fato, surge o conceito de conjunto ativo (working set), que corresponde ao
conjunto de páginas correntemente em uso de um dado processo. É fácil de notar que, se
todo o conjunto ativo de um processo estiver na memória principal, ele executará
praticamente sem gerar page faults, até que passe a uma nova fase do processamento. Por
outro lado, se não houver espaço para todo o conjunto ativo de um processo, este gerará
muitos page faults, o que ocasionará uma grande diminuição em seu tempo de execução,
devido à necessidade de constantes trocas de páginas entre memória e disco.
Aqui surge o conceito de thrashing, que ocorre quando um processo gera muitos page
fault em poucas instruções.
Surge então a seguinte consideração: o que fazer quando um processo novo é iniciado
ou, num sistema com swap, quando um processo que estava em disco deve ser colocado na
memória? Se deixarmos que esse processo gere tantos page fault quanto necessários para a
carga do seu working set, teremos uma execução muito lenta. Devemos então, de alguma
forma, determinar qual é o working set do processo e carregá-lo na memória antes de
permitir que o processo comece a executar. Este é o chamado modelo do conjunto ativo
(working set model). O ato de carregamento adiantado das páginas (antes da ocorrência do
page fault para a mesma) é chamado pré-paginação.
Vejamos agora algumas considerações com relação aos tamanhos dos working set. Se a
soma total dos working set de todos os processos residentes em memória é maior que a
quantidade de memória disponível, então ocorrer thrashing. (Obs: processos residentes na
memória são aqueles que o escalonador de baixo nível utiliza para a seleção do atualmente
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 47
em execução. O escalonador em alto nível é o responsável pela troca (swap) dos processos
residentes em memória, a certos intervalos de tempo.)
Portanto, devemos escolher os processos residentes em memória de forma que a soma de
seus working set não seja maior que a quantidade de memória disponível.
Após os fatos apresentados acima, surge a questão: como determinar quais as páginas de
um processo que fazem parte de seu working set? Uma possível solução para isto é utilizar
o algoritmo de aging, considerando como parte do working set apenas as páginas que
apresentem ao menos um bit em 1 em seus primeiros n bits (i.e., qualquer página que não
seja referenciada por n tique-taques consecutivos é retirada do working set do processo). O
valor de n deve ser definido experimentalmente.
Devemos agora determinar de que forma a memória será alocada entre os diversos
processos executáveis que competem por ela.
As duas estratégias básicas são chamadas de alocação local e alocação global, que são
descritas a seguir:
• alocação local: quando é gerado um page fault em um dado processo, retiramos uma das
páginas do próprio processo, para a colocação da página requerida;
• alocação global: quando um page fault é gerado, escolhe para retirar uma das páginas da
memória, não importa a qual processo ela esteja alocada.
Para exemplificar a diferença entre as duas técnicas, veja a Figura 13 onde, na situação
da figura (a), é gerado um page fault para a página A6 (os números ao lado das páginas
indicam o valor do contador do algoritmo de aging). Se seguimos uma estratégia local, a
página retirada será a A5 (conforme (b)), enquanto que para uma estratégia global, a página
retirada será a B3.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 48
Figura 13
É fácil de notar que a estratégia local mantém fixa a quantidade de memória destinada a
cada processo, enquanto que a estratégia global muda dinamicamente a alocação de
memória.
Em geral, os algoritmos globais apresentam maior eficiência, devido aos seguintes fatos:
• se o working set de um processo aumenta, a estratégia local tende a gerar thrashing;
• se o working set de um processo diminui, a estratégia local tende a desperdiçar memória.
Entretanto, se nos decidimos por utilização de uma estratégia global, devemos resolver a
seguinte questão: de que forma o sistema pode decidir dinamicamente (i.e., a todo instante)
quantos quadros de página alocar para cada processo (em outras palavras: qual o tamanho
do working set do processo em cada instante) ?
Uma possível solução é realizar a monitoração pelos bits de aging, conforme indicado
acima. Entretanto isto apresenta um problema, o working set de um processo pode mudar
em poucos microssegundos, enquanto que as indicações de aging só mudam a cada
tique-taque do relógio (digamos, 20ms).
Uma solução melhor para esse problema é a utilização de um algoritmo de alocação por
frequência de falta de página (PFF: page fault frequency). Esta técnica é baseada no fato de
que a taxa de falta de páginas para um dado processo decresce com o aumento do número de
quadros de página alocados para esse processo.Veja a Figura 14
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 49
Figura 14
Um outro problema que surge está relacionado com o fato de que diversos processo
podem estar, em um dado momento, utilizando o mesmo programa (p.ex.: diversos usuários
rodando um editor, ou um compilador). Para evitar duplicação de informações na memória,
é conveniente que essas informações comuns sejam armazenadas em páginas que são
compartilhadas pelos processos que as utilizam.
Um problema que surge logo de início é o seguinte: algumas partes não podem ser
compartilhadas como, por exemplo, os textos que estão sendo editados em um mesmo
editor. A solução para este problema é simples: basta saber quais as páginas que são apenas
de leitura (p.ex.: o código do programa), permitindo que estas sejam compartilhadas e
impedindo o compartilhamento das páginas que são de leitura e escrita (p.ex.: o texto sob
edição).
Um outro problema mais difícil de resolver é o seguinte:
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 51
A solução para isto é manter alguma forma de estrutura de dados que indique quais
das páginas residentes estão sendo compartilhadas. Este processo, entretanto, não é simples.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 52
7. SISTEMA DE ARQUIVOS
Para cada sistema operacional, as respostas à essas perguntas são dadas de forma diferente.
Figura 22
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 53
7.3. Diretórios
7.3.1. Conceito
Os diretórios são organizados como um conjunto de diversas entradas, uma para cada
arquivo. Algumas possibilidades de organização dos diversos arquivos em diretórios são as
seguintes:
• um único diretório para todos os arquivos (p.ex.: CP/M); um diretório para cada usuário;
• uma hierarquia de diretórios (p.ex.: MS-DOS e UNIX).
No último caso, surge a necessidade de se especificar a localização de um arquivo com
relação à hierarquia de diretórios. O modo geralmente utilizado é a especificação de um
caminho (path), que pode ser indicado de duas formas:
1. caminho absoluto: indica todo o caminho até o arquivo a partir da raiz da hierarquia;
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 54
Devemos estabelecer agora de que forma as informações sobre os arquivos devem ser
organizadas no diretório. Para isto veremos três exemplos de diretórios: o do CP/M
(diretório único), do MS-DOS e do UNIX (árvores de diretórios).
i) CP/M
O CP/M possui um único diretório, que consiste de diversas entradas como a mostrada
na Figura 23:
Figura 23
O código de usuário associa cada arquivo a um usuário identificado por um número. O
campo "extent" é utilizado para arquivos que ocupam mais de uma entrada no diretório
(quando são maiores que o tamanho possível em uma única entrada). Nesse campo temos o
número correspondente à aquela entrada do arquivo (0 para a primeira entrada, 1 para a
segunda, e assim sucessivamente). 16 espaços são reservados para os números dos blocos
em cada entrada no diretório. O contador de blocos (block count) indica quantos desses 16
espaços reservados estão efetivamente sendo utilizados (quando o arquivo ocupa mais de 16
blocos, uma nova entrada deve ser alocada para o mesmo no diretório, com o valor de
"extent" ajustado correspondentemente).
ii) MS-DOS
Figura 24
O campo de atributos é utilizado para indicar qual o tipo de arquivo (arquivo normal,
diretório, escondido, sistema, etc). Um dos campos indica o número do primeiro bloco
alocado para o arquivo. Para encontrar o bloco seguinte, busca-se a entrada na FAT
correspondente a esse primeiro bloco e encontra-se lá o número do bloco seguinte,
repetindo-se esse procedimento até encontrar o fim do arquivo. Note que a entrada no
diretório de uma arquivo apresenta também o tamanho do arquivo. Quando o atributo de um
arquivo indica que ele é do tipo diretório, então, o mesmo consistirá de entradas como as
apresentadas aqui, podendo inclusive apresentar entradas do tipo diretório, possibilitando a
formação de uma árvore de subdiretórios.
iii) UNIX
Figura 25
Os diretórios são arquivos como quaisquer outros, com tantos i-nodes quantos
necessários. A raiz tem seu i-node armazenado em uma posição fixa do disco. Além dos
arquivos normais, cada diretório possui também as entradas "." e "..", geradas no momento
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 56
Resta-nos uma consideração importante: de que forma cuidar de quais blocos do disco
estão livres e quais estão ocupados? Os métodos, como no caso de gerenciamento de
memória, são basicamente dois:
• lista encadeada;
• bit map (mapa de bits).
A segunda opção é geralmente a melhor, principalmente quando todo o bit map pode ser
mantido simultaneamente na memória. A primeira opção só é preferível quando o bit map
não pode ser mantido todo na memória e o disco está bastante ocupado (isto pois, neste
caso, com o método de bit map, se queremos encontrar espaço para um novo arquivo,
devemos ficar carregando as diversas partes do bit map do disco para a memória, pois como
o disco está cheio, é difícil de encontrar blocos vazios, o que torna o processo lento,
enquanto que na lista encadeada, basta carregar a lista de blocos vazios ao invés de ter que
carregar as diversas partes do bit map uma por vez até achar o espaço necessário).
Vejamos agora de que forma podemos fazer para saber quais os blocos que estão alocados a
um dado arquivo.
A primeira forma que poderia ser pensada seria a alocação de blocos de forma contígua
no disco. Ou seja, para armazenar um determinado arquivo seria percorrido o disco
buscando por um espaço de blocos contíguos que permitissem seu armazenamento.
Entretanto, muitas vezes não existem estes “buracos” contíguos no disco. Neste caso, seria
necessário ficar sempre reorganizando os espaços alocados no disco para criar os espaços
necessários, o que inviabiliza esta solução.
Um método possível é fazer uma lista encadeada utilizando os próprios blocos, de
forma que, num dados bloco (p.ex., de 1 kbytes), alguns de seus bytes são utilizados para
indicar qual o próximo bloco do arquivo. A grande vantagem desta estratégia é evitar a
fragmentação que ocorre no uso de alocação contígua.
Porém, esta solução apresenta o problema de que para se buscar o n-ésimo bloco de um
arquivo, devemos ler todos os n-1 blocos anteriores, além do próprio bloco, o que implica
numa grande sobrecarga de leituras em disco quando pretendemos realizar acesso aleatório
às informações do arquivo.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 58
Figura 26
Os problemas com esta solução são os seguintes:
Como a FAT ocupa locais fixos no disco, o tamanho desta é limitado pelo número de
setores alocados para a mesma;
Como cada entrada na FAT corresponde a um número fixo de bits, o número de setores no
disco é limitado, sendo que se quisermos utilizar discos maiores, devemos alterar
completamente a FAT.
Outra forma existente é a tabela de índices encontrada no sistema UNIX, que apresenta
grande facilidade de expansão de tamanho dos arquivos. Podemos resumir da seguinte
forma:
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 59
as listas (tabelas) de blocos de cada arquivo são mantidas em locais diferentes (i.e., não
existe um local fixo onde são guardadas as informações sobre os blocos de todos os
arquivos);
a cada arquivo, associa-se uma tabela, chamada i-node (nó-índice);
o i-node de um arquivo possui informações de proteção, contagem, blocos, entre outras;
no i-node existem 10 números de blocos diretos;
se o arquivo ocupa até 10 blocos, os números desses blocos são colocados nesse espaço
reservado no i-node;
se o arquivo ocupa mais do que 10 blocos, um novo bloco é alocado ao arquivo, que será
utilizado para armazenar tantos números de blocos desse arquivo quantos couberem no
bloco. O endereço desse bloco será armazenado em um número de bloco de endereçamento
indireto;
se ainda forem necessários mais blocos do que couberam nas estruturas anteriores, é
reservado um novo bloco que irá apontar para blocos que contêm endereços dos blocos dos
arquivos. O número desse novo bloco é armazenado num bloco de endereçamento
duplamente indireto;
se ainda não for suficiente isso, é alocado um bloco de endereçamento triplamente indireto.
Acompanhe o processo descrito acima pela Figura 27:
Figura 27
As vantagens desse método são as seguintes:
• os blocos indiretos são utilizados apenas quando necessário;
• mesmo para os maiores arquivos, no máximo são necessários três acessos a disco para se
conseguir o endereço de qualquer de seus blocos.
Ricardo Luís de Freitas Notas de Aula - Sistemas Operacionais - 60
A segunda solução (ligação simbólica) não tem esses problemas, pois, quando o dono
remove o arquivo o mesmo é eliminado e a partir desse ponto todos os outros usuários que
tentarem acessar o arquivo por ligação simbólica terão a mensagem de "arquivo
inexistente". No entanto ela apresenta sobrecarga no tempo de acesso, devido à necessidade
de percorrer "paths" extras até se chegar no arquivo e de ocupar um i-node a mais. Uma
outra vantagem desta solução é a de que permite realizar links com outras máquinas,
interligadas por uma rede (ao contrário do método do i-node, que só permite ligações
dentro do próprio disco).
As duas soluções apresentadas têm em comum uma desvantagem: o fato de que o
arquivo aparece com diferentes paths no sistema, fazendo com que, para sistemas
automáticos de cópia (em geral em backups), ele seja copiado diversas vezes.