“People don't rise from nothing. We do owe something to parentage and patronage.”

Outiliers, Malcolm Gladwell

15 - Sistemas Operacionais

Sistemas operacionais (SO) são programas desenvolvidos para atuarem como intermediários entre o usuário e o hardware. É uma camada complexa de incorporando estruturas de baixo e alto níveis. A maioria dos SOs atuais tem interface gráfica e diversos aplicativos para facilitar tarefas recorrentes, além de tecnologias para facilitar a instalação e comunicação com novos dispositivos (plug and play).

Um SO tem dois objetivos básicos: abstrair e gerenciar. A abstração permite que atividades complexas sejam encapsuladas em interfaces disponíveis aos desenvolvedores para que não precisem começar sempre do zero a comunicação com o hardware. No caso de gestão de recursos, o SO administra os acessos ao processador, memória e discos, além da comunicação com outros dispositivos, seus controladores e drivers.

Podemos dividir os SOs em várias categorias: batch (em lote), rede, distribuído, multiusuário, servidor, desktop, móvel, embarcado e tempo real.

Os elementos básicos de um SO são:

  • núcleo (kernel): contém os programas responsáveis pela gestão de recursos do hardware e as principais abstrações e programas utilitários;
  • código de inicialização: configuração as tarefas iniciais para rodar o SO, principalmente ações de identificar dispositivos e inicializar a memória;
  • drivers: são códigos utilizados para acessar os dispositivos conectados ao computador;
  • programas utilitários: fornecem funcionalidades adicionais ao núcleo para facilitar operações recorrentes.

O kernel é a parte mais baixa do SO e tem um modo privilegiado de acesso ao processador.

SOs podem ser divididos em diferentes arquiteturas:

  • monolíticos: bloco único de código que se comporta como o núcleo e acessa o hardware sem restrições. Tem a vantagem de um desempenho mais rápido pela interação direta entre os componentes. Porém, erros podem ser propagados, gerando travamentos frequentes;
  • micronúcleo: o núcleo tem somente o código de baixo nível para interação com o hardware, além de algumas abstrações básicas. Tem outra camada, chamada serviço, para controlar os programas que gerenciam o espaço de usuário. É um sistema mais modular e flexível, além do melhor controle de falha entre as partes. Implementa o modelo de tarefa, com espaços de memória protegidos para cada aplicação. É menos performático que o modelo monolítico;
  • em camadas: o núcleo fica separado dos programas por uma cada intermediária, focadas em abstração e gestão de tarefas mais sofisticadas. O modelo de permissão diminui com a distância ao núcleo. Nesse modelo também há perda de desempenho, quando comparado ao monolítico. Também há uma questão de organizar a hierarquia de camadas;
  • híbridos: nessa abordagem o núcleo tem mais funcionalidades para permitir uma independência maior dos outros componentes.
  • avançadas: são utilizadas para aplicações específicas como computação em nuvem.

São exemplos: máquinas virtuais, contêineres, sistemas exonúcleo e uninúcleo.

15.1 - Conceito de Processo.

Dadas as operações básicas que o SO e os programas do usuários podem executar em um dado momento, o núcleo precisa definir como vai acionar o processador para cada pedido de execução. Uma visão ingênua do problema seria executar uma fila de programas de forma sequencial, onde o primeiro é executado por inteiro, depois o segundo e assim por diante. O problema desse tipo de abordagem é o tempo de espera para os outros itens da lista. Se o primeiro programa for muito grande ou lento (muitas operações) todos os outros ficarão aguardando. O mesmo aconteceria se o processador tivesse que aguardar por operações em outros dispositivos como disco, rede, etc… Uma solução prática utilizada com sucesso foi a divisão do tempo do processador em várias tarefas, o que dá a ilusão de multitarefas.

Um processador só pode executar uma instrução por vez. Para isso, definimos o conceito de tarefa, ou seja, um fluxo de instruções que vai resolver alguma finalidade. A tarefa é uma execução sequencial e dinâmica, diferente de um programa, que é estático e não interage com outras entidades. As tarefas podem ser implementadas como processos (threads). Uma tarefa tem várias propriedades como: comportamento, duração e prioridade. O sistema operacional deve decidir a ordem de execução e a organização de todas as tarefas pendentes.

Os sistemas mais antigos eram monotarefas, ou seja, só executavam uma de cada vez. Cada usuário deveria esperar a tarefa de outro ser executada completamente antes de poder colocar a sua para processamento. Como as tarefas foram ficando mais complexas, um operador humano que alternava entre diferentes atividades foi substituído por um programa monitor carregado em memória para gerenciar e executar os programas.

Os sistemas multitarefas aumentam a eficiência do processador, interrompendo uma tarefa mais lenta para executar alguma outra da fila de espera. Esse passo necessitou de um programa monitor mais sofisticado e também mais memória.

Nos sistemas de tempo compartilhado, cada tarefa tem uma fatia de tempo do processador, onde uma gestão de quem está fila organiza as atividades enviadas ao processador. Há um temporizador no hardware para controlar o recurso de preempção (remover uma tarefa à força).

Uma tarefa passa por vários estágios:

  1. Nova
  2. Pronta
  3. Executando
  4. Suspensa
  5. Terminada

O estado de uma tarefa em um instante específico do tempo é chamado de contexto. Ela é representada no núcleo por uma estrutura de dados chamada descritor, do tipo TCB (Task Control Block) ou PCB (Process Control Block). Os descritores são normalmente armazenados em listas ou vetores. Quando o SO muda de uma tarefa para outra, deve salvar o estado da tarefa atual no TCB e suspendê-la, atividade chamada troca de contexto. As rotinas relacionadas com a gestão de tarefas são feitas pelo dispatcher e a escolha da próxima tarefa pelo scheduler.

Uma medida de eficiência do uso do processo é dada pela equação $\epsilon =\frac{t_q}{t_q+t_{tc}}$, onde $t_q$ é o tempo de quantum (que a tarefa tem para usar o processador) e $t_{tc}$ o tempo de troca de contexto.

O conceito de processo vem historicamente da junção da tarefa e seus recursos em dado momento, seria uma casca isolada de execução. Porém, atualmente os SOs podem ter mais de uma tarefa por processo, que viraram uma unidade de contexto, um contêiner de utilização (área de memória, arquivos abertos, conexões, etc…). Processos contém tarefas que usam esses recursos de forma compartilhada e isolada pelo hardware. Por padrão um SO moderno associa uma tarefa por recurso.

15.2 - Gerência de Processos/Processador.

Para criar e destruir tarefas o SO precisa disponibilizar essas operações por chamadas do sistema. No Linux, por exemplo, o comando fork copia um processo para outro, onde os recursos do núcleo são os mesmos, mas executam em áreas da memória distintas.

É importante que o desenvolver destrua o processo para liberar os recursos associados. Tarefas dentro do mesmo processo podem trocar informações entre si. Tarefas entre processos diferentes devem se comunicar pelo IPC (Inter-Process Communication).

No sistemas UNIX os processos são organizados em árvore, o que facilita a comunicação quando o processo pai é encerrado. Isso não é igual no Windows, que não tem diferença entre processos pai e filho.

Uma thread (processo leve) é um fluxo de execução independente dentro do mesmo processo, ou seja, um processo contém uma ou mais threads, que compartilham recursos, mas executando códigos distintos. Seu contexto local é chamado de TLS (Thread Local Storage). Threads também são usadas no núcleo, para executar, por exemplo, rotinas de drivers. Modelos de threads:

  • N:1 = controle de threads está dentro dos processos pois núcleo executa só um processo de cada vez (N threads mapeadas para uma). Também chamado de fibers ou green threads. Apresenta problemas de coordenação de acesso à recursos compartilhados, uso da CPU e falta de paralelismo;
  • 1 : 1 = gestão das threads no núcleo do sistema. Cria no núcleo uma thread para cada uma existente em um processo. É o modelo mais frequente utilizado. É pouco escalável;
  • N : M = modelo híbrido, N threads de um processo são mapeadas para M threads no núcleo (M < N). O conjunto de threads associadas a um processo é denominada “thread pool”, com ajuste dinâmica de quantidade. É mais complexo e mais difícil de gerenciar que os outros.

O padrão de programação de threads vem do IEEE POSIX 1003.1.c ou POSIX Threads.

O escalador escolha qual tarefa será executada em um determinado momento, para isso deve priorizar esse processo de acordo com o tipo de tarefa (tempo real, interativas ou lote) e seu comportamento (orientadas a processamento e/ou orientadas para entrada e saída). Os critérios mais comuns utilizados são: (tempo de execução, tempo de espera, tempo de resposta, justiça e eficiência).

Os tipos de sistema operacional são: preemptivo e cooperativos. No primeiro, a tarefa é suspensa por algum evento e volta para a fila de processamento. No caso cooperativa, a tarefa só é removida se está aguardando algum recurso. Algoritmos de escalonamento:

  • FCFS (First-Come, First-Served) : Mais simples e elementar, atende as tarefas em sequência, na medida que mudam o estado para prontas.
  • RR (Round-Robin): Usa preempção para revezar as tarefas por um quantum de tempo t. Não utiliza a abordagem sequencial na seleção da próxima tarefa.
  • SJF (Shortest Job First): Seleciona a tarefa mais curta da fila primeiro. Reduz o tempo médio de espera. É difícil estimar a duração de cada tarefa. Pode utilizar a média das últimas execuções da tarefa para definir o quantum. Pode levar ao starvation de tarefas longas;
  • SRTF (Shortest Remaining Time First): Analisa as tarefas com tempo menor restante de execução para definir a prioridade;
  • PRIOc (Priority cooperative algorithm): Define a prioridade da tarefa, através de um número, e executa a fila.
  • PRIOp (Priority preemptive algorithm): Tarefas com maior prioridade são enviadas para processamento, suspendendo a que estiver em execução com prioridade menor;
  • PRIOd (Priority dynamic algorithm): Aplica um método de seleção misto entre prioridade e tempo de espera de execução.
15.3 - Comunicação, Concorrência e Sincronização de Processos.

A comunicação entre processos e tarefas é importante para que haja coordenação entre as atividades com um mesmo fim. Nem sempre um programa sequencial resolve o problema da melhor maneira, principalmente do ponto de vista da eficiência. As tarefas devem ter uma forma se comunicar para que a coordenação ocorra.

A comunicação pode ser direta ou indireta. No primeiro caso o emissor identifica de forma clara o receptor e vice-versa. No último, não há essa necessidade. Existe um canal de comunicação para interação entre as partes. Quanto ao sincronismo, a comunicação pode ser síncrona, ou bloqueante, assíncrona ou não bloqueante (utilizando um buffer para armazenar os dados) e semissíncrona, que bloqueia apenas por um prazo definido. A forma de envio é sequência de mensagens independentes (pacotes de dados) ou fluxo sequencial contínuo. A capacidade dos canais pode ser nula (não armazena dados), infinta (pode sempre enviar dados) e finita.

Problemas na comunicação:

  • Perda de dados;
  • Perda de integridade;
  • Perda de ordem;

Também podemos classificar a comunicação pelo número de participantes:

  • 1 : 1 : um emissor e um receptor;
  • M : N : muitos emissores e muitos receptores. São exemplos o message queues (mailbox), message bus, publish-subscribe, multicast, etc…

Sobre os mecanismos de comunicação:

  • Pipes: canal de comunicação de uma direção entre dois processos. No UNIX é comum utilizá-lo para passar a saída de um programa como entrada de outro. Tipo 1:1, síncrona.
  • Fila de mensagens: permite a implementação do envio e recepção de mensagens entre processos de forma ordenada. Tipo M para N, síncrona ou assíncrona.
  • Memória compartilhada: permite que tarefas em processos distintos possam trocar informações de forma mais rápida por um ambiente compartilhado.

Sempre que algum recurso é compartilhado há problemas de coordenação de acesso que precisam ser resolvidos. Perguntas do tipo: o que acontece se diferentes threads acessam a mesma variável no mesmo momento? quais problemas de lógica acontecem quando fluxos modificam os recursos em sequencias não previstas?

Para resolver estes e outros problemas foram criados algoritmos de sincronia e concorrência. Os casos mais comuns são resumidos abaixo:

Condições de disputa (race conditions): Ocorrem quando há interferência de dois fluxos distintos e concorrentes em recursos compartilhados. Isso pode causar diferenças em variáveis pois cada tarefa, em um determinado ponto, alterou um valor sem que outra tarefa fosse notificada. É um tipo de erro dinâmico, que acontece em tempo de execução.

Condições de Bernstein: Define formalmente quando tarefas paralelas não têm risco de condição de disputa. Verifica quando uma variável pode ser escrita simultaneamente por duas ou mais tarefas.

Antes de definir os mecanismos para as possíveis soluções deste problema, denominamos “seção crítica” o trecho de código que manipula variáveis que são compartilhadas entre tarefas e podem sofre de condições de corrida. Para evitar o acesso concorrente, utilizamos a “exclusão mútua”, que permite que somente uma tarefa por vez acesse determinado recurso. Alguns critérios devem ser respeitados:

  • Espera limitada: uma tarefa aguardando o acesso à seção crítica deve conseguí-lo eventualmente, após esperar um tempo finito;
  • Independência de outras tarefas: tarefas que não querem acessar a seção crítica não devem afetá-la;
  • Independência de fatores físicos: a solução deve ser lógica, sem depender de configurações de hardware.

Tentativas de solução do problema de acesso à seção crítica:

  • Inibir interrupções: bloqueia outras tarefas de enviar interrupção enquanto uma delas está na seção crítica. Pode causar problemas de bloqueio do sistema, perda de dados de E/S e travamentos;
  • Trivial: utiliza variável de controle (boolean) para validar quem pode ou não entrar na seção crítica em determinado instante de tempo. O maior problema dessa abordagem é que a atribuição e o teste são feitos em momentos distintos, o que também causa problemas de exclusão mútua. Outra questão é de performance, usar um loop infinito consome muito processador;
  • Alternância de uso: define uma variável “turno” que indica de quem é a vez de acessar a seção crítica. Implementa um algoritmo cíclico, que tem como problema o fato de que se uma tarefa não acessar a seção crítica, todas as subsequentes ficarão travadas;

Dois algoritmos foram propostos para resolver o problema: Dekker e Peterson.

Operações atômicas: permite isolar uma variável dos contextos, ou seja, somente uma tarefa tem acesso para atribuir e testar os valores em um instante de tempo t.

TSL (Test-and-Set Lock): instrução de máquina troca valor de uma variável e retornar o valor antigo. Atualmente são denominadas RMW (Read-Modify-Write), CAS (Compare-And-Swap), XCHG (Exchange);

Semáforo: São utilizados para coordenar de maneira eficiente e flexível o controle de exclusão mútua entre n tarefas. É utilizado para desenvolver mecanismos mais complexos como monitores. Possui uma fila de tarefas e um contador, porém, o acesso é somente via operações atômicas:

  • down: utilizado para decrementar o contador e testá-lo. Se resultado for negativo, a tarefa é suspensa e entra na fila, caso contrário pode continuar;
  • up: incrementa o contador. Se o retorno for negativo ou nulo, indica que há tarefas suspensas no semáforo;
  • init: inicializa o semáforo e define o valor inicial do contador.

Exemplo:

init(s,1); // inicializa o semáforo
void alterarConta(semaphore s, int *saldo, int valor)
{
    down(s); // solicitação de acesso
    (*saldo) += valor; // seção crítica
    up(s);  // libera o acesso 
}

Neste caso resolvemos os problemas de eficiência, justiça e independência.

Semáforos simplificados são chamados de mutexes (mutual exclusion), semáforos binários ou locks, pois só assumem dois valores possíveis: livre (1) e ocupado (0).

Variáveis de condição: são usadas para avaliar que uma determinada condição é verdade e assim acordar uma tarefa da fila para execução. Deve ser usada com um mutex para garantir exclusão mútua.

Semântica de Hoare: Acontece quando a variação de condição perde o mutex e a operação de sinalizar que o recurso está livre é entregue à primeira tarefa da fila;

Semântica Mesa: A operação de sinalizar que o recurso está livre acorda uma tarefa que espera pela condição, sem suspender a tarefa atual.

Monitores: em programas complexos o programador deve determinar as condições de sincronização das tarefas, o que pode causar problemas de exclusão mútua. Hansen e Hoare definiram o conceito de monitor, que sincroniza a requisição e liberação de uma seção crítica de forma transparente. Utiliza um mutex para controle de exclusão mútua. Ele encapsula o recurso compartilhado na forma de um objeto e provê modos de acesso para manipulá-lo.

15.4 - Gerenciamento de Memória: Memória Virtual, Paginação, Segmentação e “Swap”.

As memórias são divididas entre dois tipos principais: voláteis e não-voláteis. Essa característica nos diz se os dados armazenados são perdidos ou não caso o computador seja desligado. As memórias do processador (registradores e cache L1) são voláteis, assim como o cache da placa mãe (L2) e a memória RAM. Já as memórias flash, discos rígidos ópticos e fitas magnéticas, não são.

As memórias mais próximas ao processador são mais rápidas, ou seja, registrados, cache L1 e L2. Isso tanto em tempo de acesso como taxa de transferência de dados.

A memória principal ou RAM (Random Access Memory) organiza o uso de armazenamento para o sistema operacional e todos os processos sendo executados em dado momento. Há uma parte da RAM que fica inacessível, sendo utilizada para BIOS, VGA, Video e outras utilidades. O espaço da memória é dividido em bytes, cada um com seu endereço de acesso.

A CPU acessa a memória via o barramento de dados, de endereços e de controle. A quantidade de memória é limitada pelo barramento de dados, sendo que n vias conseguem gerar $2^n$ endereços diferentes. Processadores atuais podem ter até 48 vias para endereços, ou até 256 Tb de memória física.

Os endereços podem ser físicos ou lógicos (virtuais). Os endereços lógicos ficam disponíveis para os processos, que depois são traduzidos em endereços físicos na memória RAM, isso é feito pela MMU (Memory Management Unit), que pode estar integrada à própria CPU. Sua configuração é feita pelo sistema operacional. A MMU também pode controlar os acessos dos processos a diferentes área da memória, um tipo de permissionamento via hardware.

A memória virtual pode ser organizada em:

Partições: o espaço é dividido em partes e cada parte fica com um processo. As divisões podem ter tamanhos iguais ou não. A tabela de partições fica na memória RAM. É um modelo mais simples de organização, mas sofre de flexibilidade e fragmentação.

Segmentos: estruturas semelhantes de cada processo são mapeadas em áreas separadas na memória física. É subdividida em segmento e offset (posição dentro do segmento). É gerenciada pela MMU através da STBR (Segment Table Base Register) e STLR (Segment Table Limit Register). Não é um modelo utilizado atualmente e pode ser muito lento se não implementado com os devidos registradores de segmentos para acelerar o acesso aos dados do processo ativo.

Páginas: divide-se o endereço lógico em bloco de mesmo tamanho, chamados de páginas. Em arquiteturas modernas cada bloco tem 4096 bytes (4 KBytes). No caso da memória física, ela também é divida pelo mesmo tamanho, com cada bloco chamado de frame (quadro). As páginas são mapeadas para quadros através de tabelas de páginas. Há bits de status e controle (flags) relativos às páginas para validação de estado, permissão de leitura e escrita, se usuário pode acessar ou somente o núcleo do sistema, se dados estão na RAM ou em disco, se foi acessada recentemente e se foi modificada. Tabelas multiníveis são utilizadas para relacionar páginas em uma estrutura de dados de árvore, evitando desperdício de espaço linear quando muitas páginas não são utilizadas por um processo. Caches internos das páginas são gerenciados pela MMU e podem acelerar razoavelmente a busca por um objeto em memória.

Alguns processadores podem fazer uso de uma combinação de segmentos e páginas, oferecendo mais flexibilidade ao sistema operacional e aos desenvolvedores.

Um processo que concentra seus acessos em poucas áreas da memória é mais eficiente e essa propriedade é chamada de localidade de referências. É dividida em três formas: temporal, espacial e sequencial. Os fatores importantes na implementação dessa propriedade são: estruturas de dados e algoritmos do programa e qualidade do compilador.

A memória de cada processo pode ser subdividida em espaço do usuário e espaço do Kernel. As áreas devem ser isoladas dos demais processos e contém toda a informação necessária a sua execução. As principais seções são:

  • Text: armazena o código binário que será executado e a ligação com bibliotecas;
  • Data: contém variáveis estáticas inicializadas;
  • BSS (Block Started by Symbol): contém variáveis estáticas não-inicializadas;
  • Heap: armazena variáveis alocadas dinamicamente;
  • Stack: gerencia o fluxo de execução do processo;

As áreas Heap e Stack ficam opostas entre si na memória do processo e tem um espaço livre entre elas, para que possam crescer.

A alocação de variáveis em um programa pode ser de três tipos:

Estática: espaço definido na compilação do programa, reservado no momento da execução do processo até o seu encerramento. Ficam na seção DATA se iniciadas no código-fonte ou na BSS caso contrário.

Automática: variáveis definidas dentro do escopo de uma função são alocadas de forma automática dentro da seção STACK.

Dinâmica: A alocação dos blocos de memória é requisitada pela aplicação, que deve liberá-los depois. Usa a seção HEAP.

A atribuição de endereços de memória para as variáveis do programa pode ser feita na edição (por ex, programando Assembly), na compilação, na ligação (através do arquivo objeto e sua tabela de símbolos), na carga (DLL) ou na execução (com auxílio do hardware). A maioria dos SOs usa uma combinação dos itens anteriores para atribuição.

O mecanismo responsável pela alocação da memória é chamado de alocador e seu papel é reservar espaço na memória RAM de acordo com o fluxo (processos ou sistema operacional). Podem existir em diversos contextos:

  • Memória física
  • Espaço de núcleo
  • Espaço de usuário

O trabalho do alocador pode gerar fragmentação na memória, conforme libera ou aloca dados em estruturas sequenciais. Algumas estratégias de alocação são usadas para reduzir a fragmentação na memória, como first-fit, best-fit, worst-fit e next-fit, sendo as mais eficientes a best-fit e first-fit. A desfragmentação também uma opção, ou seja, reorganizar as áreas de memória e atualizar os endereços físicos das novas posições. A fragmentação interna lida com o problema de arredondar as alocações para evitar espaços livres pequenos entre áreas ocupadas na memória. Estratégias mais sofisticadas:

  • Alocador Buddy: também chamado de alocador por pares, sempre aloca blocos de memória de tamanho $2^n$, onde n é inteiro e ajustável. Isso pode gerar fragmentação interna.
  • Alocador Slab: especializado na alocação de objetos de núcleo, estruturas de dados que descrevem processos, arquivos abertos, sockets de rede, pipes, etc. São pequenos e nascem / morrem rapidamente. Um slab é uma divisão do cache com objetos do mesmo tipo.
  • Alocador no espaço do usuário: normalmente é gerenciada pelas bibliotecas do sistema ou da linguagem de programação utilizada.

Memory pool é um tipo de técnica utilizada para pré-alocação de memória onde a aplicação pode obter e liberar registro mais rapidamente.

Como a memória RAM é um recurso escasso e caro, algumas estratégias e técnicas são utilizadas para escrever e ler os dados em outros dispositivos, como por exemplo, discos, são extensões da memória e precisamos levar em conta a eficiência dessas rotinas. Primeiramente, partes ociosas da memória podem ser levadas para outro lugar, sendo que são trazidos de volta quando o processo é reativado. O sistema operacional regula esse processo de transferência. Técnicas usuais:

  • Overlays: módulos chamados overlays são separados pelo programador e podem ser carregados em momentos distintos (Turbo Pascal);
  • Swapping: move processos ociosos da RAM para o disco (swap-in, swap-out);
  • Paging: Move páginas individuais ou segmentos para o disco, a MMU dispara uma interrupção para o SO que carrega os elementos faltantes do disco.

Existem alguns algoritmos clássicos para escolha das páginas e a substituição na memória:

  • FIFO
  • Ótimo
  • LRU (Least Recently Used)
  • Random
  • Segunda chance
  • NRU (Not Recently Used)
  • Envelhecimento

Alguns algoritmos de página podem levar a mais faltas de páginas, mesmo com mais memória física. Esse problema foi estudo por Belady, um matemático húngaro, sendo chamado então de anomalia de Belady.

Thrashing ocorre quando a memória RAM não é suficiente para alocar todos os dados do conjunto ativo dos processos e fica alternando o estado de cada processo entre executando e suspenso para poder carregar as informações de outras fontes.

15.5 - Gerenciamento de Arquivos. 15.6 - Gerenciamento de Dispositivos de Entrada/Saída.

Computadores sem operações de entrada e saída de dados são basicamente inúteis. Por este motivo, existem enormes desafios em organizar, reconhecer, acessar, gravar e receber dados de diferentes dispositivos em um computador comum. Imagine que qualquer periférico com uma entrada padronizada, tipo USB, pode ser conectado em uma máquina, reconhecido e estar funcionando em questão de segundos. Qual a complexidade de fazer isso acontecer independente do hardware e do sistema operacional?

Do ponto de vista físico, os dispositivos conectados ao computador emitem um sinal elétrico analógico que deve ser convertido para sinal digital e armazenado em um buffer que poderá ser então acessado pela CPU através de um elemento chamado controlador de entrada. Do mesmo modo, para enviar dados ao dispositivo, utiliza-se um controlador de saída. As informações vêm e vão através de barramentos.

Periféricos complexos possuem microcontroladores ou processadores na placa que executa um código chamado de firmware e é independente do SO do computador.

Os barramentos modernos em computadores são divididos no chipset em north e south bridge. O primeiro lida com acesso à RAM e dispositivos de alta velocidade, além de estar ligado diretamente à CPU. O south bridge controla barramentos e portas mais lentas, como PCI, USB e SATA. A comunicação da CPU com a south bridge se dá pela north bridge. Essa arquitetura vem do padrão Von Neumann.

As portas, ou registradores, de entrada e saída dos dispositivos são divididas em: entrada, saída, status e controle. Elas formam a interface de acesso da CPU ao periférico. A forma de acesso pode ser mapeada em portas, mapeada em memória, híbrida ou por canais de entrada e saída.

O processador pode enviar um dado ao dispositivo pelo controlador, mas o contrário não é usual. Para o periférico acessar o processador é necessário o envio de uma interrupção (IRq - Interrupt Request), que são sinai elétricos pedindo a atenção da CPU. Quem recebe uma interrupção é o controlador de instruções programável (PIC - Programmable Interrupt Controller). Interrupções normalmente são números inteiros enviados pelo barramento que tem um caminho específico no chipset para que o processador suspenda uma atividade e executa a rotina de tratamento de exceção. As rotinas estão definidas em memória e podem ser encontradas através da tabela de interrupções (IVT - Interrupt Vector Table).

O software necessário para comunicar uma ação ao controlador do dispositivo externo é o driver. Cada periférico em determinado sistema operacional tem o seu driver. Os dispositivos são agrupados em classes para facilitar o tratamento de interfaces genéricas que sirva de comunicação com o SO. As classes comuns em UNIX são:

  • Orientados a caracteres: comunicação byte a byte;
  • Orientados a blocos: transferência em blocos de bytes de tamanho fixo;
  • Rede: envio e recepção em blocos de dados de tamanho variável;
  • Gráficos: requerem alto desempenho e área compartilhada com o processador (frame buffer).

Um driver pode servir para um único dispositivo ou para uma família. São constituídos de funções de entrada e saída, gerência (do driver e do dispositivo) e tratamento de eventos. Os drivers tem estrutura de armazenamento local e tem acesso privilegiado ao núcleo do Sistema Operacional. Um driver pode interagir com o dispositivo através de algumas estratégias:

  • Interação controlada por programa (varredura ou pooling): solicita operação ao controlador e monitora os bits na porta de status;
  • Interação controlada por eventos: utiliza interrupções para notificar o driver;
  • Acesso direito à memória: permite transferência direta de dados entre memória e controlador;

A notificação recebido por interrupção é tratada em dois níveis, primário e secundário. O primeiro registra e coloca o evento na fila, além de notificar o driver. O segundo lida com eventos pendentes registrados.

15.7 - Alocação de Recursos.

Os sistemas operacionais devem definir como disponibilizar os recursos do hardware para diferentes processos. Isso deve ser feito de maneira eficiente e transparente ao usuário, monitorando quais programas são mais custosos e executando diversas rotinas para manter a saúde do computador em todo instante.

Referências:

  1. Sistemas Operacionais - Carlos Maziero