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).