Notas da Aula 15 - Fundamentos de Sistemas Operacionais 1

Propaganda
Notas da Aula 15 - Fundamentos de Sistemas Operacionais
1. Software de Entrada e Saída: Visão Geral
Uma das tarefas do Sistema Operacional é simplificar o acesso aos dispositivos de hardware
pelos processos dos usuários. É desejável que o SO forneça mecanismos de acesso simples
para os processos, “escondendo” o máximo possível da complexidade da manipulação dos
dispositivos de hardware.
Na aula anterior, foram discutidos os aspectos de hardware das operações de entrada e
saída. Foram vistos tipos de dispositivos de E/S, modos de endereçamento destes dispositivos
e modos de transferência dos dados entre o processador e os dispositivos. No entanto,
nos exemplos de código apresentados até então, o acesso aos dispositivos passava pela
manipulação de registradores de controladores ou dos próprios dispositivos. Este tipo de
manipulação requer que o programador tenha um conhecimento perfeito da arquitetura do
dispositivo de E/S, incluindo detalhes de implementação específica de certos fabricantes.
Este método de acesso aos dispositivos de E/S já foi uma realidade. Em sistemas
computacionais antigos, o programador era obrigado a incluir em seu código todas as rotinas
de manipulação dos dispositivos de E/S, no nível de manipulação de registradores. Os
programadores precisavam ler manuais extensos que detalhavam sequências de passos para
a execução de cada tarefa desejada do dispositivo de E/S. Isso tornava os programas grandes,
difíceis de desenvolver e mais propícios a erros.
Ainda hoje existem sistemas que necessitam deste tipo de manipulação de baixo nível,
na manipulação de dispositivos de E/S. Alguns sistemas embarcados, baseados em
microcontroladores simples, requerem que o programador inclua em seu código todo o
tratamento do acesso aos dispositivos.
No entanto, em sistemas mais comuns, o Sistema Operacional passou a prover mecanismos
simplificados de acesso os dispositivos de Entrada e Saída. O SO fornece uma ou mais
camadas de abstração de software, cada uma provendo um conjunto de serviços que facilitam
ou melhoram o desempenho das operações de E/S.
Existem vários requisitos desejáveis para estas camadas de abstração do Sistema
Operacional. Em primeiro lugar, assim como em todos os outros aspectos de um SO, é
desejável que estas camadas permitam um uso eficiente dos dispositivos de E/S. Em
outras palavras, a facilidade de uso dos dispositivos não pode ser obtida em detrimento do
desempenho no acesso aos mesmos. No caso dos dispositivos de E/S, isso é fundamental
porque operações de entrada e saída são intrinsecamente lentas. Qualquer ineficiência no uso
dos dispositivos de E/S acaba se refletindo de maneira muito clara no desempenho do sistema.
Outro requisito importante é a simplificação do acesso aos dispositivos. Detalhes como
registradores e modos de transferência de dados devem ser escondidos do usuário. Se não
há simplificação no acesso aos dispositivos, não faz sentido utilizar camadas de abstração do
Sistema Operacional.
É desejável também que o acesso aos diferentes dispositivos seja o mais padronizado
possível. Ou seja, deseja-se que, do ponto de vista do programador, a maneira de manipular
ou acessar os vários dispositivos do sistema seja similar. Por exemplo, na maior parte dos
sistemas operacionais, para ler dados de um arquivo, utiliza-se uma função do tipo read. No
entanto, esta mesma função (em geral) pode ser utilizada para receber dados através de uma
conexão de rede. Isso é possível justamente porque os sistemas operacionais implementam
uma camada de abstração que torna o acesso a estes dois dispositivos (o disco rígido e a
placa de rede, no caso) homogêneo.
A este conjunto de camadas de abstração provido pelo Sistema Operacional para acesso
aos dispositivos de E/S, dá-se o nome de Subsistema de Entrada e Saída. Geralmente, o
subsistema de entrada e saída é composto por 4 camadas. Na camada mais baixa está
o hardware em si. Ou seja, os próprios dispositivos de E/S, com seus registradores e
especificidades. Detalhes como o modo de transferência de dados e o tipo de mapeamento de
endereços utilizados estão presentes nesta camada.
Uma camada acima encontram-se os chamados Device Drivers, ou Drivers de Dispositivo.
Esta camada é responsável por implementar as operações básicas de manipulação de cada
dispositivo do sistema. Na prática, os drivers são os programas responsáveis pela interação
com os dispositivos de E/S.
A próxima camada é chamada de E/S Independente do Dispositivo. Esta camada prôve
serviços que são comuns a todos os dispositivos de E/S, independentemente do seu tipo. Por
exemplo, esta camada é responsável pela nomeação dos dispositivos. Cada dispositivo precisa
de um identificador dentro do sistema, para que este possa ser requisitado pelos processos dos
usuários. Esta camada se encarrega de atribuir tais identificadores.
Por fim, a última camada do subsistema é chamada de E/S no Nível do Usuário. A função
desta camada é fornecer ao usuário uma visão dos dispositivos de E/S. Ou seja, esta camada
fornece ao programador funções e estruturas da dados que manipulam e representam os
dispositivos de E/S dentro do código de um programa.
É importante notar que, das quatro camadas citadas, apenas duas (os device drivers e o
E/S independente do dispositivo) são de fato parte do Sistema Operacional. A camada de
hardware obviamente é independente do SO, já que ela não é composta por software. Já
a camada de E/S no nível do usuário é geralmente fornecida por bibliotecas ou linguagens
de programação. Tais bibliotecas realizam o intermédio entre o programa do usuário e as
chamadas de sistema fornecidas pelo SO que dão acesso a camada de E/S independente do
dispositivo. Por exemplo, a função printf() é fornecida pela biblioteca padrão da linguagem C.
Quando esta função é chamada em um programa, a biblioteca de encarrega de formatar os
dados do usuário para impressão na tela e de realizar a chamada de sistema do SO que realiza
a operação de E/S necessária para que aqueles dados sejam efetivamente mostrados.
Vale ressaltar ainda que este modelo em camadas discutido aqui é apenas uma visão genérica.
Há diversos dispositivos de E/S que apresentam mais camadas de abstração implementadas
pelo SO. Um exemplo importante é a placa de rede. Embora seja possível utilizar a placa de
rede “diretamente”, passando apenas pelas 4 camadas descritas anteriormente, grande parte
dos sistemas operacionais implementam o chamado modelo de Sockets. Neste modelo, o
SO apresenta algumas outras camadas de software responsáveis pela implementação dos
protocolos de rede básicos, como os protocolos de rede (e.g., protocolo IP) e protocolos
de transporte (e.g., TCP, UDP e ICMP). Quando em um programa utilizamos a abstração
de Sockets, as chamadas de sistema fazem a ligação entre os processos dos usuários
e os módulos do SO que implementam tais protocolos. Eventualmente, um pacote será
construído para transmissão através da interface de rede. Somente então, as camadas de E/S
independente do dispositivo e dos drivers de dispositivo serão utilizadas.
2. Drivers de Dispositivo
Os drivers de dispositivo são pedaços de software que implementam as funcionalidades de
acesso aos dispositivos de E/S em um sistema operacional. Estes drivers, em geral, são
desenvolvidos pelos próprios fabricantes de cada dispositivo, já que eles precisam conter
detalhes de muito baixo nível sobre o acesso aos dispositivos.
São estes drivers os responsáveis por manipular os registradores de controle dos dispositivos
de E/S, de forma que este dispositivo possa ser manipulado. Os drivers também são
responsáveis por fornecer tratadores de interrupção específicos para seu dispositivo. Quando
ocorre uma interrupção no sistema, o SO identifica qual dispositivo a disparou e passa então o
controle da execução para o tratador de interrupção do driver.
Drivers de dispositivo fazem parte do SO e por isso são executados em Modo Supervisor.
Eles necessitam de privilégios de execução, pois as instruções do processador que realizam
a comunicação com os dispositovos de E/S são privilegiadas (instruções IN e OUT, ou mesmo
instruções MOV, quando o endereço especificado é de um dispositivo de E/S mapeado em
espaço de memória).
Geralmente (nos sistemas operacionais modernos, pelo menos), drivers podem ser carregados
dinamicamente. Isso é um característica interessante, pois cada dispositivo necessita de um
driver específico. De fato, mesmo dispositivos do mesmo tipo (como duas placas de vídeo) de
modelos ou fabricantes diferentes requerem drivers distintos. Isso ocorre porque, mesmo sendo
de um mesmo tipo, cada placa pode ter conjuntos de registradores de controle diferentes,
com semânticas de manipulação distintas. Dada a enorme variedade de marcas, modelos e
dispositivos existentes hoje no mercado, a possibilidade de carregamento dinâmico de drivers
pelo SO evita problemas graves como o crescimento excessivo do código do SO.
Embora os drivers de dispositivo implementem funções de muito baixo nível, se comunicando
diretamente com os dispositivos de E/S, a portabilidade destes drivers para sistemas
operacionais diferentes (migração do código de um SO para outro) tende a não ser trivial.
Isso ocorre porque cada SO estabelece uma interface padrão para os drivers. Esta interface
determina quais funções e informações o driver deve disponibilizar para o Sistema Operacional.
Por exemplo, é razoável que todos os drivers de dispositivos que permitam saída de dados
forneçam uma função de escrita de dados no dispositivo. Quando um processo do usuário
deseja escrever dados em um dispositivo, eventualmente o SO chamará esta função do driver
do dispositivo desejado. A interface padrão define exatamente quais funções precisam ser
disponibilizadas pelo driver, bem como quais argumentos devem ser recebidos e quais os
valores de retorno esperados.
3. E/S Independente do Dispositivo
Esta camada implementa os serviços do SO que são comuns a qualquer tipo de dispositivo de
E/S. Alguns exemplos de serviços providos por esta camada são:
● escalonamento de E/S;
● denominação;
● buferização;
● cache;
● alocação;
● permissões; e
● tratamento de erros.
Assim como o processador, o uso dos dispositivos de E/S geralmente precisa ser escalonado.
Boa parte dos dispositivos de E/S podem receber múltiplas requisições simultâneas e é tarefa
desta camada implementar mecanismos de escalonamento que decidam a ordem de acesso
dos vários processos. Assim como ocorre com o processador, a política de escalonamento
dos dispositivos de E/S pode ter impacto fundamental no desempenho. No caso de alguns
dispositivos, como o disco rígido, o escalonamento das requisições tem um impacto ainda
maior no desempenho que o escalonamento do processador.
A denominação dos dispositivos é outro serviço importante desta camada. Dispositivos
precisam ser acessados no sistema através de algum identificador. Esta camada se encarrega
de atribuir identificadores únicos a cada dispositivo. No Unix, por exemplo, cada dispositivo é
identificado por dois números, chamados de minor e major. O major é um número que identifica
o tipo do dispositivo (e.g., se é um dispositivo de rede, de armazenamento em massa),
enquanto o minor identifica o dispositivo entre os vários pertencentes àquele tipo.
A buferização é outro serviço com grande impacto no desempenho. Em geral, dispositivos
de E/S tem um tamanho ideal de bloco para leitura ou escrita. Por exemplo, um disco rígido
em geral é acessado para leitura ou escrita em blocos de alguns kbytes. Quando escrevemos
um programa, no entanto, é comum que queiramos ler apenas um byte por vez. Embora
seja possível fazer a leitura de um único byte por vez do disco rígido, isso é muito ineficiente
(quando estudarmos o escalonamento deste dispositivo de E/S, isso ficará evidente). Logo, a
cada requisição de leitura do disco rígido, o SO lê ao menos um bloco completo. Os bytes a
mais lidos do dispositivo são armazenados em um buffer interno do SO para posterior uso (se
estes bytes forem eventualmente requisitados pelo usuário).
O serviço de cache tem por objetivo armazenar em memória principal informações lidas
dos dispositivos de entrada e saída que são frequentemente requisitadas. Por exemplo,
suponha que haja um arquivo no sistema que seja requisitado para leitura por várias
aplicações constantemente. Um exemplo disso, é um sistema servidor de banco de dados.
Várias processos podem constantemente fazer consultas à base de dados que, por sua vez,
armazena seus dados em um pequeno conjunto de arquivos. Neste caso, estes arquivos serão
comumente requisitados, forçando o SO a fazer sucessivas requisições ao disco rígido para
leitura dos mesmos dados. Como a operação de leitura do disco rígido é muito mais lenta que
a leitura de dados da memória principal, o SO pode implementar uma política que reserve
uma parte desta para cache de arquivos. Assim, após a primeira requisição de leitura de um
determinado arquivo, este pode ser colocado em cache. Nas próximas requisições, o disco
rígido não precisa ser acessado, pois os bytes já estarão disponíveis em memória.
O serviço de cache é um dos grandes motivos pelos quais a quantidade de memória RAM
tem uma influência tão grande no desempenho dos computadores modernos. Quanto mais
memória RAM em um computador, mais informações o SO é capaz de armazenar em cache,
reduzindo assim o tempo de acesso em operações de E/S. É claro que a manipulação deste
serviço de cache tem uma série de complicações. Em primeiro lugar, a quantidade de memória
principal disponível para esta cache é limitada. Logo, é preciso existir uma política para a
entrada e saída de arquivos da cache. Além disso, o SO precisa ter cuidado com escritas
no arquivo. Se um processo requisita uma escrita em um arquivo que está em cache, o
SO precisa remover a cache ou atualizá-la com o novo estado do arquivo. O estudo destes
mecanismos tem grande impacto no desempenho do SO, porém eles estão muito além do
escopo desta disciplina.
Existem dispositivos que não permitem múltiplos acessos ao mesmo tempo. Por exemplo,
uma impressora pode atender a apenas uma requisição por vez. Neste caso, o SO deve
gerenciar o acesso a estes recursos. Uma das técnicas para a realização deste controle é a
spooling. Esta técnica consiste na utilização de uma fila (chamada de spool) que armazena os
pedidos de operações de E/S. Há um processo especial no SO que acessa este spool e realiza
a requisição de E/S. Como um único processo remove as requisições do spool e realiza as
operações de E/S, a utilização do dispositivo se torna sequencial. Um exemplo clássico deste
método são os spools de impressão.
Esta camada do subsistema de entrada e saída associa a cada dispositivo um conjunto de
permissões. Estas permissões determinam quais usuários do sistema podem realizar quais
tipos de acesso a cada dispositivo. Cada vez que um processo de um usuário tenta realizar
uma determinada operação sobre um dispositivo de E/S, as permissões são verificadas e o
acesso é permitido ou não.
Finalmente, esta camada provê também um serviço de aviso sobre condições de erro. Por
exemplo, se o dispositivo de E/S reporta um erro ao tentar atender a uma requisição de um
usuário, esta camada deve, de alguma forma, avisar ao processo que fez a requisição que esta
falhou. Idealmente, o processo deve ainda ser avisado dos motivos da falha. Existem falhas
que são irrecuperáveis, como um erro físico no dispositivo, que parou de responder. Outras
falhas são chhamadas de transientes, porque são apenas momentâneas. Por exemplo, o
dispositivo está ocupado naquele momento e não pode atender à requisição realizada. Neste
caso, o processo pode fazer novas tentativas, até que a requisição tenha sucesso.
4. E/S no Nível do Usuário
Para o usuário (ou para um programador), o SO deve fornecer um conjunto de rotinas
que permitam acesso aos dispositivos de E/S. Todas as operações suportadas por um
dispositivo devem estar disponíveis ao usuário através do SO. Do ponto de vista do SO,
esta disponibilização é feita através de chamadas do sistema. Quando um processo deseja
realizar uma operação de E/S, ele deve realizar a chamada de sistema correspondente. Isso
dispara uma interrupção de software que, por sua vez, aciona o SO. O SO lê os parâmetros
da chamada de sistema, que especificam o dispositivo de E/S desejado e as informações
relevantes à operação de entrada e saída.
No entanto, quando escrevemos um programa é comum não utilizarmos diretamente as
chamadas de sistema. Ao invés disso, utiliza-se um conjunto de funções e estruturas de dados
providas por uma biblioteca ou pela própria linguagem de programação. O compilador da
linguagem ou a biblioteca em questão se encarregam de fazer a “tradução” destas funções e
estruturas para as chamadas de sistema correspondentes. Isso simplifica bastante a execução
de requisições de E/S, além de auxiliar em muitos casos na portabilidade do código de um SO
para outro (linguagens como Java e Python, por exemplo, permitem que o mesmo código seja
utilizado em várias plataformas sem qualquer alteração).
Do ponto de vista do programador, existem alguns métodos diferentes para a realização de
uma operação de E/S. O método mais comum é a operação bloqueante. Em uma operação
de E/S bloqueante, o processo é bloqueado enquanto a requisição de entrada e saída não é
atendida. Por exemplo, se um processo requisita a leitura de dados a partir da placa de rede
utilizando o método bloqueante, este será bloqueado até que os dados sejam recebidos pela
interface de rede.
Por outro lado, é possível requisitar ao SO que as operações de E/S sejam não bloqueantes.
No caso de uma leitura não bloqueante, por exemplo, sempre que o processo requisita
uma operação de E/S, o SO verifica quais dados estão disponíveis naquele momento e os
retorna imediatamente para o processo. No exemplo anterior, da leitura de dados a partir
da placa de rede, se o dado requisitado pelo processo ainda não tiver sido recebido pela
placa, o SO avisará imediatamente ao processo que não há informações a serem recebidas
e este prosseguirá com a sua execução. Eventualmente, o processo pode realizar uma nova
requisição.
Há ainda um terceiro modo de operação chamado de assíncrono. O modo assíncrono é similar
ao modo não bloqueante, no sentido de que ambos fazem com que o processo continue em
execução, mesmo que a requisição de E/S não esteja imediatamente pronta. A diferença do
modo assíncrono é que o SO avisa ao processo quando sua requisição foi concluída (ou seja,
o processo não tem que repetir sua requisição, como no modo não bloqueante). Este aviso
pode ser implementado de várias formas, como através de interrupções (o SO interrompe a
execução normal do processo e passa a execução para uma função especial do programa
chamada de Tratador de Sinal) ou através de polling (o processo de tempos em tempos
pergunta ao SO se a sua requisição está completa).
Download