“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 atuar como intermediários entre usuários, aplicações e hardware. Eles formam uma camada complexa que combina mecanismos de baixo nível, como controle de interrupções, memória e processador, com abstrações de alto nível, como arquivos, processos, interfaces de programação e serviços ao usuário.

Os dois objetivos centrais de um SO são abstrair e gerenciar. A abstração esconde detalhes de hardware e oferece interfaces padronizadas para que desenvolvedores e usuários trabalhem em um nível mais simples. A gerência organiza o uso de recursos escassos, como tempo de CPU, memória principal, dispositivos de entrada e saída e armazenamento secundário.

Esse papel é fundamental em computação porque quase todo software real depende do sistema operacional para criar processos, alocar memória, acessar arquivos, comunicar-se pela rede, usar periféricos e executar tarefas concorrentes com segurança e isolamento.

Podemos dividir os SOs em várias categorias, dependendo do contexto de uso:

  • batch (em lote): executa conjuntos de tarefas sem interação contínua do usuário;
  • rede: oferece serviços de comunicação e compartilhamento entre máquinas conectadas;
  • distribuído: coordena recursos espalhados em vários computadores, tentando apresentar uma visão integrada;
  • multiusuário: permite que diferentes usuários utilizem o sistema com isolamento e controle de permissões;
  • servidor: otimiza disponibilidade, escalabilidade e atendimento de múltiplas requisições;
  • desktop: prioriza interatividade e suporte a aplicações de uso geral;
  • móvel: precisa lidar com bateria, sensores, conectividade variável e interface tátil;
  • embarcado: opera em dispositivos dedicados, com recursos limitados e funções específicas;
  • tempo real: precisa respeitar prazos rígidos ou previsíveis de resposta.

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 as tarefas iniciais para colocar o sistema em funcionamento, como detecção de dispositivos e preparação de 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 sistema operacional e executa em modo privilegiado, com acesso controlado ao processador e ao hardware. Ele implementa chamadas de sistema, escalonamento, gerência de memória, sincronização, E/S e mecanismos de proteção.

SOs podem ser divididos em diferentes arquiteturas:

  • monolíticos: grande bloco de código executando em modo núcleo, com comunicação direta entre componentes. Em geral, favorece desempenho, mas reduz isolamento de falhas;
  • micronúcleo: deixa no núcleo apenas mecanismos mínimos, como escalonamento, memória e comunicação básica. Serviços adicionais são movidos para fora do kernel, o que melhora modularidade e isolamento, mas pode aumentar o custo de comunicação;
  • em camadas: organiza o sistema em níveis de abstração, cada um dependendo do nível inferior. Facilita organização conceitual, embora possa introduzir sobrecusto;
  • híbridos: combinam ideias de diferentes modelos, tentando equilibrar desempenho, modularidade e compatibilidade.

Também existem abordagens especializadas importantes para ambientes modernos, como máquinas virtuais, contêineres, exonúcleos e unikernels. Em computação em nuvem e infraestrutura de servidores, essas técnicas são usadas para isolamento, implantação rápida, portabilidade e redução de overhead em cenários específicos.

15.1 - Conceito de Processo.

Quando vários programas precisam usar o processador, o sistema operacional deve decidir quem executa, por quanto tempo e em que ordem. Uma abordagem ingênua seria executar um programa inteiro e só depois iniciar o próximo. Essa solução funciona mal porque desperdiça recursos sempre que o programa ativo precisa esperar disco, rede, teclado ou qualquer outra operação lenta de entrada e saída.

Historicamente, os primeiros sistemas eram bastante simples e executavam uma única tarefa por vez, muitas vezes em modo batch, isto é, por lote. O usuário submetia um conjunto de trabalhos, e o sistema os processava sequencialmente, sem grande interação durante a execução. Esse modelo era suficiente enquanto as máquinas eram caras, lentas e usadas por poucas pessoas especializadas.

Com a evolução do hardware e o aumento da demanda por melhor aproveitamento da CPU, surgiram técnicas de multiprogramação. A ideia passou a ser manter vários programas residentes em memória e alternar entre eles quando um ficasse bloqueado esperando entrada e saída. Isso aumentou bastante a utilização do processador.

O passo seguinte foi o tempo compartilhado, ou time-sharing, em que cada tarefa passou a receber pequenas fatias de tempo do processador. Esse modelo permitiu uso interativo e, posteriormente, sistemas multiusuário, nos quais diferentes pessoas podiam compartilhar a mesma máquina com a impressão de execução quase simultânea de seus programas.

Para resolver isso, os sistemas operacionais modernos usam multiprogramação e tempo compartilhado. O processador alterna entre diferentes fluxos de execução, dando a impressão de paralelismo mesmo quando há apenas um núcleo disponível. Em máquinas com múltiplos núcleos, parte dessas tarefas ainda pode executar realmente em paralelo.

É importante distinguir três conceitos:

  • programa: código estático armazenado em disco ou memória;
  • processo: instância de um programa em execução, com contexto, memória, arquivos abertos e demais recursos associados;
  • thread: fluxo de execução dentro de um processo, compartilhando parte dos recursos desse processo.

Um navegador web, por exemplo, pode ter múltiplos processos para isolamento entre abas e múltiplas threads dentro de cada processo para interface gráfica, rede, renderização e execução de scripts. Essa distinção é essencial para entender concorrência e gerenciamento de recursos.

Em sistemas multiusuário, essa organização se torna ainda mais importante porque tarefas de pessoas diferentes competem pelos mesmos recursos físicos. O sistema operacional precisa isolar processos, aplicar permissões, contabilizar uso de CPU e memória e manter tempos de resposta aceitáveis mesmo com múltiplas sessões ativas.

Nos sistemas de tempo compartilhado, cada tarefa recebe uma fatia de tempo do processador, chamada quantum. Um temporizador de hardware permite preempção, isto é, a remoção forçada da tarefa em execução para que outra tarefa da fila possa avançar.

Uma tarefa ou processo passa por vários estágios:

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

De forma informal, o fluxo mais comum é: o processo nasce, entra na fila de prontas, é colocado para executar, pode ser suspenso por espera de E/S ou preempção, volta à fila e eventualmente termina.

O estado de uma tarefa em um instante específico do tempo é chamado de contexto. Ele inclui registradores, contador de programa, pilha, informações de memória, prioridade e outros metadados. No núcleo, esse estado é representado por estruturas de dados como TCB (Task Control Block) ou PCB (Process Control Block). Quando o SO troca uma tarefa por outra, ele salva o contexto da tarefa atual e restaura o da próxima. Essa operação é chamada troca de contexto.

De forma informal, um TCB costuma guardar informações como: identificador da thread, registradores salvos, ponteiro de pilha, estado atual, prioridade, contador de quantum e referências ao processo ao qual a thread pertence. Já um PCB tende a guardar: identificador do processo, mapa de memória, arquivos abertos, permissões, credenciais do usuário, conexões, sinais pendentes e a lista de threads do processo.

Exemplo: em um navegador, cada aba pode ser tratada por um processo com seu próprio PCB, enquanto threads internas de renderização, rede e interface possuem TCBs separados. Se a thread de renderização precisar ser interrompida, o núcleo manipula principalmente o TCB correspondente; se a aba inteira for encerrada, o PCB e os recursos associados ao processo são liberados.

As rotinas relacionadas à gestão imediata da troca são executadas pelo dispatcher, enquanto a escolha de qual tarefa será executada cabe ao scheduler.

A troca de contexto é relativamente lenta porque não envolve apenas guardar alguns registradores. O sistema precisa salvar o estado corrente, atualizar estruturas internas do escalonador, trocar pilhas, eventualmente invalidar ou atualizar entradas de cache e TLB, restaurar o contexto da próxima tarefa e retomar a execução no ponto correto. Além disso, a troca costuma prejudicar localidade de referência, pois a nova tarefa pode usar dados e instruções diferentes, forçando recargas em cache e aumentando custo indireto.

Uma medida simples de eficiência associada ao quantum é

$$ \epsilon=\frac{t_q}{t_q+t_{tc}} $$

onde $t_q$ é o tempo de quantum e $t_{tc}$ é o tempo de troca de contexto. Se, por exemplo, $t_q=9$ ms e $t_{tc}=1$ ms, então

$$ \epsilon=\frac{9}{10}=0{,}9 $$

ou seja, cerca de 90% do tempo é gasto em trabalho útil e 10% em troca de contexto. Se o quantum for muito pequeno, a responsividade pode melhorar, mas o custo administrativo do sistema cresce.

Atualmente, processos são vistos como contêineres de recursos e proteção, enquanto threads são fluxos de execução que operam dentro deles. Essa separação é uma das bases da organização de aplicações modernas, servidores concorrentes e interfaces responsivas.

Também é útil distinguir concorrência e paralelismo. Concorrência significa organizar logicamente múltiplas tarefas que progridem de forma intercalada. Paralelismo significa executar de fato mais de uma tarefa ao mesmo tempo em múltiplos núcleos ou processadores.

Outra distinção central é entre modo usuário e modo núcleo. Aplicações comuns executam em modo usuário com permissões limitadas. Quando precisam criar processos, acessar dispositivos ou manipular recursos protegidos, recorrem a chamadas de sistema que transferem temporariamente a execução ao kernel.

Em sistemas interativos, também é comum separar processos de primeiro plano e segundo plano. Os primeiros estão associados diretamente à sessão ativa do usuário, como um terminal ou editor. Os segundos executam serviços auxiliares, como daemons, indexadores, sincronizadores e rotinas agendadas.

Nos sistemas UNIX, conceitos como processos órfãos e zumbis também ajudam a entender o ciclo de vida. Um processo órfão é aquele cujo pai terminou antes dele; o sistema o reatribui a outro processo supervisor. Um processo zumbi já encerrou sua execução, mas ainda mantém seu descritor até que o processo pai leia o status de término.

A afinidade de CPU é outro tema relevante. Em arquiteturas com múltiplos núcleos, o escalonador pode preferir manter uma tarefa no mesmo núcleo em que ela vinha executando, aproveitando melhor caches já aquecidos e reduzindo custo de migração.

Do ponto de vista de API, a criação de processos e threads varia entre famílias de sistemas operacionais. Em UNIX, o modelo tradicional envolve chamadas como fork, exec e wait. Em Windows, a criação tende a partir de primitivas específicas para processo e thread desde o início. Essas diferenças afetam bibliotecas, runtimes e portabilidade de aplicações.

15.2 - Gerência de Processos/Processador.

A gerência de processos e do processador envolve criar, organizar, suspender, retomar e encerrar unidades de execução, além de decidir como o tempo de CPU será distribuído entre elas. Esse trabalho está no centro do sistema operacional, porque quase toda aplicação real depende da existência de vários fluxos concorrentes competindo por memória, disco, rede e processador.

As operações de criação, sincronização e término são oferecidas por chamadas de sistema. Em sistemas do tipo UNIX, por exemplo, fork cria um processo filho a partir de um processo pai, herdando inicialmente vários elementos do contexto, como descritores de arquivos, variáveis de ambiente e diretório corrente. Depois disso, uma chamada exec pode substituir a imagem do programa em execução por outro executável. Essa dupla fork + exec é uma das bases clássicas do modelo de processos em sistemas multitarefa.

Encerrar corretamente um processo ou thread é tão importante quanto criá-lo. Nessa fase, o sistema precisa liberar memória, descritores de arquivos, sockets, locks, buffers, temporizadores e outros recursos associados. Se isso não for feito, aparecem vazamentos de recursos, processos zumbis, degradação de desempenho e, em casos mais graves, esgotamento de estruturas internas do sistema operacional.

Em sistemas UNIX, processos costumam ser organizados em árvores de criação. O pai pode criar filhos, esperar seu término, recolher o código de saída e enviar sinais de controle. Em servidores e shells, esse modelo aparece o tempo todo: um shell cria um processo para executar um comando; um servidor pode criar vários workers; um processo controlador pode reiniciar filhos que falharem.

Uma thread é um fluxo de execução independente dentro de um processo. Enquanto processos distintos tendem a ter espaços de endereçamento separados, threads do mesmo processo compartilham código, heap, arquivos abertos e outros recursos, mas mantêm pilha, registradores e contexto de execução próprios. Também é comum cada thread possuir TLS (Thread Local Storage), usado para dados privados. Em termos práticos, um navegador moderno pode ter processos separados por abas ou serviços, e dentro de cada processo manter múltiplas threads para interface, renderização, rede e execução de scripts.

Essa distinção entre processo e thread aparece com clareza em aplicações concorrentes. Uma interface gráfica pode manter uma thread para eventos da janela, outra para leitura de rede e outra para computação pesada, evitando congelamento visual. Um servidor web pode ter um processo principal aceitando conexões e um conjunto de threads ou processos trabalhadores executando tarefas específicas.

Modelos clássicos de implementação de threads:

  • N:1: o gerenciamento fica inteiramente no espaço do usuário; várias threads da aplicação são mapeadas para uma única entidade executável no núcleo. O modelo é simples e barato, mas uma chamada bloqueante pode parar todo o processo e o uso de múltiplos núcleos fica limitado;
  • 1:1: cada thread da aplicação corresponde a uma thread do núcleo. Esse modelo facilita paralelismo real em máquinas multicore e é o mais comum em sistemas modernos, embora possa introduzir maior overhead de criação e escalonamento;
  • N:M: várias threads de usuário são distribuídas sobre um conjunto menor de threads do núcleo. O objetivo é combinar flexibilidade e desempenho, mas a implementação é mais complexa.

O termo thread pool designa um conjunto reutilizável de threads prontas para executar tarefas. Em vez de criar e destruir threads continuamente, o sistema ou a aplicação mantém um pool ativo e entrega trabalhos a ele. Esse padrão aparece em servidores HTTP, filas de tarefas, runtimes de linguagens e bibliotecas de paralelismo, pois reduz custo de criação, melhora throughput e ajuda a limitar concorrência excessiva.

Em ambientes POSIX, o padrão mais conhecido para programação com threads é o POSIX Threads, ou pthreads. A ideia geral pode ser ilustrada pelo pseudocódigo abaixo:

criar_thread(trabalhador, conexao_1)
criar_thread(trabalhador, conexao_2)
criar_thread(trabalhador, conexao_3)

funcao trabalhador(conexao):
    requisicao = ler(conexao)
    resposta = processar(requisicao)
    enviar(conexao, resposta)

Nesse exemplo, várias threads executam a mesma função sobre conexões diferentes. O código do programa é compartilhado, mas cada thread mantém seu próprio fluxo de controle, sua pilha e seus dados locais. Em sistemas com múltiplos núcleos, essas threads podem inclusive executar em paralelo.

Depois de definir quais tarefas existem, o sistema operacional precisa decidir quem usa o processador em cada instante. O escalonador analisa o comportamento das tarefas, o tipo de sistema e os objetivos desejados. Em geral, distinguimos tarefas de tempo real, interativas ou em lote; e também tarefas orientadas a processamento (CPU-bound) ou orientadas a entrada e saída (I/O-bound).

Tarefas CPU-bound passam a maior parte do tempo usando a CPU, como simulações numéricas, compilação, renderização ou treinamento de modelos. Tarefas I/O-bound passam boa parte do tempo esperando disco, rede ou periféricos, como servidores de arquivos, navegadores e bancos de dados sob carga mista. Essa diferença importa porque um escalonador eficiente tende a favorecer respostas rápidas para tarefas interativas e uso intenso da CPU para tarefas computacionalmente pesadas.

Entre os critérios mais comuns de escalonamento estão:

  • tempo de execução: quanto tempo a tarefa realmente utiliza a CPU;
  • tempo de espera: quanto tempo permanece na fila antes de executar;
  • tempo de resposta: rapidez com que o sistema reage ao primeiro pedido da tarefa;
  • justiça ou fairness: evitar que certas tarefas sejam preteridas indefinidamente;
  • throughput: quantidade de trabalho concluído em certo intervalo;
  • previsibilidade: quão regular e estável é o comportamento temporal do sistema.

Quanto ao controle do processador, podemos distinguir sistemas preemptivos e cooperativos. No modelo preemptivo, o núcleo pode retirar a CPU de uma tarefa por interrupção de temporizador, prioridade ou política de escalonamento. No modelo cooperativo, a tarefa devolve o processador voluntariamente ou quando bloqueia por recurso. Sistemas cooperativos costumam ser mais simples, mas um programa mal comportado pode monopolizar a máquina por tempo excessivo.

Algoritmos clássicos de escalonamento incluem:

  • FCFS (First-Come, First-Served): atende as tarefas na ordem de chegada;
  • RR (Round-Robin): usa preempção para revezar tarefas por um quantum fixo;
  • SJF (Shortest Job First): seleciona primeiro a tarefa estimada como mais curta;
  • SRTF (Shortest Remaining Time First): versão preemptiva baseada no menor tempo restante;
  • prioridade preemptiva ou não preemptiva: ordena tarefas por importância relativa;
  • prioridade dinâmica: ajusta a prioridade com base em espera, interatividade ou histórico de uso.

Podemos ilustrar o efeito dessas políticas com três tarefas de duração 9, 5 e 2 unidades de tempo. Em FCFS, se a ordem de chegada for 9, depois 5, depois 2, a tarefa mais curta precisa esperar a conclusão das duas anteriores. Em SJF, a ordem preferida seria 2, depois 5, depois 9, reduzindo o tempo médio de espera. Em compensação, se tarefas curtas continuarem chegando, uma tarefa longa pode sofrer starvation.

Round-Robin enfatiza responsividade. Se três tarefas A, B e C precisam de 5, 4 e 3 unidades de tempo e o quantum é 2, a ordem de execução ocorre em rodadas:

$$ A_2,\;B_2,\;C_2,\;A_2,\;B_2,\;C_1,\;A_1 $$

Nenhuma tarefa fica muito tempo sem atendimento, o que melhora a sensação de fluidez em sistemas interativos. Em contrapartida, o número de trocas de contexto aumenta, e quantuns muito pequenos podem desperdiçar tempo de CPU apenas alternando tarefas.

Uma comparação simples entre cargas CPU-bound e I/O-bound ajuda a entender esse trade-off. Se uma tarefa usa a CPU por longos intervalos e outra bloqueia com frequência para ler da rede, um escalonador puramente orientado a throughput pode favorecer a primeira. Já um escalonador voltado à interatividade tende a atender rapidamente a tarefa que retorna de E/S, porque isso melhora o tempo de resposta percebido pelo usuário.

Na prática, escalonadores reais combinam prioridades, aging, heurísticas de interatividade, afinidade de cache e balanceamento entre núcleos. Ainda assim, FCFS, RR, SJF, SRTF e escalonamento por prioridade continuam sendo modelos fundamentais para compreender latência, throughput, fairness, starvation e previsibilidade.

15.3 - Comunicação, Concorrência e Sincronização de Processos.

A comunicação entre processos e tarefas é necessária sempre que atividades distintas cooperam em um mesmo objetivo. Em um sistema moderno, isso aparece em servidores, navegadores, bancos de dados, pipelines de processamento, sistemas de arquivos e drivers. Nem sempre um programa puramente sequencial resolve o problema da melhor maneira; muitas vezes é preciso dividir trabalho, compartilhar resultados e coordenar acesso a recursos.

Quando falamos em IPC (Interprocess Communication), estamos tratando dos mecanismos que permitem a troca de dados entre processos isolados. Essa comunicação pode ser direta, quando emissor e receptor se conhecem explicitamente; ou indireta, quando existe um canal intermediário, como uma fila ou mailbox. Ela também pode ser síncrona, quando o emissor ou receptor bloqueia esperando a operação terminar; assíncrona, quando o envio ou recepção pode prosseguir com apoio de buffers; ou temporizada, quando o bloqueio é limitado por prazo.

Também é útil classificar os canais pelo padrão de comunicação. Em alguns casos temos trocas 1:1, como um pipe simples entre dois processos. Em outros, há comunicação M:N, como filas de mensagens, publish-subscribe, barramentos de eventos e multicast. Em todos esses cenários, surgem questões práticas como ordem de entrega, buffer cheio, perda de dados, duplicação, corrupção e sincronização entre produtores e consumidores.

Entre os mecanismos mais comuns de IPC, podemos destacar:

  • pipes: canais normalmente unidirecionais, muito usados em sistemas UNIX para conectar a saída de um programa à entrada de outro;
  • filas de mensagens: permitem envio e recepção ordenada de mensagens discretas, com desacoplamento maior entre produtores e consumidores;
  • memória compartilhada: oferece alto desempenho, pois vários processos enxergam uma área comum de memória, mas exige sincronização explícita;
  • sockets locais ou de rede: usados quando processos precisam conversar na mesma máquina ou através da rede, como em arquiteturas cliente-servidor.

Um exemplo clássico de pipe em shell é:

cat acesso.log | grep ERRO | sort

Nesse caso, cada processo produz um fluxo que serve de entrada para o seguinte. Já um banco de dados ou um servidor de cache tende a preferir sockets. Um conjunto de processos analíticos pode preferir memória compartilhada para reduzir cópias de dados grandes. A escolha do mecanismo depende do volume de dados, do custo de cópia, do grau de isolamento e da necessidade de ordenação.

Mesmo quando a comunicação funciona, ainda resta o problema da concorrência. Concorrência significa que múltiplas atividades progridem de forma intercalada, possivelmente compartilhando estado. Paralelismo é o caso particular em que duas ou mais delas realmente executam ao mesmo tempo em núcleos diferentes. Em ambos os casos, a ordem exata dos eventos pode variar, e essa variabilidade é fonte frequente de erros.

Uma condição de corrida, ou race condition, ocorre quando o resultado depende da ordem imprevisível de acesso a um recurso compartilhado. Considere duas threads incrementando a mesma variável saldo:

Thread A: le saldo = 10
Thread B: le saldo = 10
Thread A: grava saldo = 11
Thread B: grava saldo = 11

O resultado final deveria ser 12, mas termina em 11. O problema surge porque a operação saldo = saldo + 1 não é atômica: ela envolve ler, calcular e escrever. Qualquer trecho desse tipo que manipule estado compartilhado é chamado de seção crítica.

Uma solução correta para o problema da seção crítica costuma exigir três propriedades clássicas: exclusão mútua, de modo que apenas uma tarefa entre na região crítica por vez; progresso, para que tarefas aptas a entrar não fiquem bloqueadas sem motivo; e espera limitada, para que uma tarefa não aguarde indefinidamente enquanto outras continuam avançando.

Historicamente, várias tentativas foram propostas antes das primitivas modernas. Inibir interrupções pode funcionar apenas em contextos muito específicos do núcleo e é inadequado para programas comuns. Uma variável booleana simples falha porque o teste e a atualização podem ser interrompidos entre si. A alternância estrita por turno resolve alguns conflitos, mas é pouco flexível e pode bloquear o sistema mesmo quando a outra tarefa não quer entrar na seção crítica. Algoritmos como os de Dekker e Peterson têm valor didático por mostrarem que exclusão mútua pode ser construída logicamente, mas sua aplicação prática hoje é limitada.

Nos sistemas modernos, a base de muitos mecanismos de sincronização está em operações atômicas fornecidas pelo hardware. Instruções como TSL (Test-and-Set Lock), CAS (Compare-And-Swap), XCHG (Exchange) e outras operações RMW (Read-Modify-Write) executam leitura e modificação como uma ação indivisível. Isso permite implementar mutexes, spinlocks e semáforos de forma segura mesmo em ambientes multiprocessados.

Um mutex é a primitiva mais simples de exclusão mútua: ele protege uma região crítica e permite que apenas uma thread por vez entre nela. Em pseudocódigo:

lock(mutex)
saldo = saldo + valor
unlock(mutex)

Se a região crítica for muito curta e o bloqueio for breve, esse mecanismo costuma ser suficiente. Entretanto, quando várias tarefas precisam coordenar produção, consumo ou disponibilidade de recursos, entram em cena semáforos e variáveis de condição.

Um semáforo mantém um contador e uma fila de espera. Em um semáforo binário, os valores práticos são 0 e 1, funcionando de forma semelhante a um lock. Em um semáforo contador, o valor representa quantas instâncias de um recurso estão disponíveis. As operações tradicionais são down ou P, que tenta consumir uma unidade e bloqueia se não houver recurso; e up ou V, que devolve uma unidade e pode acordar tarefas suspensas.

O problema produtor-consumidor é um exemplo clássico. Suponha um buffer circular com capacidade N. Um produtor só pode inserir item se houver espaço; um consumidor só pode remover item se houver dados. Isso pode ser modelado com um mutex e dois semáforos contadores:

semaforo itens = 0
semaforo espacos = N
mutex buffer_lock = livre

produtor(item):
    down(espacos)
    lock(buffer_lock)
    inserir(buffer, item)
    unlock(buffer_lock)
    up(itens)

consumidor():
    down(itens)
    lock(buffer_lock)
    item = remover(buffer)
    unlock(buffer_lock)
    up(espacos)

Nesse esquema, espacos impede overflow do buffer, itens impede consumo de buffer vazio, e buffer_lock protege a estrutura de dados interna. Esse tipo de padrão aparece em filas de impressão, pipelines multimídia, processamento de logs, servidores e drivers.

Variáveis de condição oferecem um mecanismo mais expressivo para coordenação baseada em estados. Elas são usadas junto com um mutex. A operação wait libera temporariamente o mutex e suspende a thread até que a condição desejada se torne verdadeira; signal acorda uma tarefa; broadcast acorda todas. Em termos conceituais, isso é útil quando a thread não quer apenas exclusão mútua, mas também aguardar uma propriedade do sistema.

lock(mutex)
while fila_vazia():
    wait(cond_nao_vazia, mutex)
item = remover(fila)
unlock(mutex)

Esse padrão é comum em monitores, filas concorrentes e bibliotecas de sincronização de alto nível. A diferença entre as semânticas de Hoare e Mesa está no momento em que a thread acordada assume o controle. Na semântica de Hoare, o sinal transfere imediatamente a execução para a thread acordada. Na semântica Mesa, mais comum em sistemas reais, o sinal apenas acorda a thread, que disputará novamente o mutex antes de prosseguir.

Monitores são uma abstração de nível mais alto que encapsula estado compartilhado, operações seguras e variáveis de condição em um único módulo. Em vez de espalhar chamadas de lock e unlock por todo o código, o programador centraliza a sincronização dentro da própria estrutura. Linguagens e bibliotecas modernas oferecem mecanismos próximos dessa ideia, como synchronized em Java, std::mutex e std::condition_variable em C++, ou primitivas de sincronização em bibliotecas POSIX.

Além de condições de corrida, projetos concorrentes precisam considerar deadlock, starvation e livelock. Em deadlock, um conjunto de tarefas fica preso esperando recursos umas das outras. Em starvation, uma tarefa continua apta a executar, mas nunca é escolhida ou nunca recebe o recurso necessário. Em livelock, as tarefas continuam mudando de estado, mas sem progresso efetivo. Esses problemas aparecem, por exemplo, em bancos de dados, sistemas de arquivos, servidores concorrentes e escalonadores internos do núcleo.

Sincronização correta tem custo. Locks demais reduzem paralelismo; locks de menos produzem corrupção de estado; espera ocupada pode desperdiçar CPU; e contenção elevada aumenta latência. Por isso, o projeto de sistemas concorrentes precisa equilibrar segurança, desempenho, granularidade de bloqueio e simplicidade de implementação.

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

O gerenciamento de memória existe para oferecer duas garantias ao mesmo tempo: cada processo precisa de um espaço de endereçamento que pareça grande, contínuo e privado; e o sistema operacional precisa usar uma memória física limitada de forma eficiente e segura. Para isso, entram em cena hierarquia de memória, tradução de endereços, alocadores, paginação, segmentação e políticas de substituição.

As memórias podem ser classificadas como voláteis e não voláteis. Registradores, caches e RAM perdem conteúdo quando a máquina é desligada; SSDs, discos e outras mídias persistentes não. Essa distinção importa porque o sistema operacional precisa mover dados entre níveis com custos muito diferentes de acesso.

A hierarquia de memória é organizada aproximadamente assim: registradores no topo, depois caches L1, L2 e L3, em seguida RAM, e por fim armazenamento secundário, como SSD ou HD. Quanto mais próximo da CPU, menor a latência e menor a capacidade. Isso explica por que algoritmos e estruturas de dados com boa localidade costumam ser muito mais eficientes do que outros com a mesma complexidade assintótica.

A memória principal, ou RAM (Random Access Memory), organiza o armazenamento temporário usado pelo sistema operacional e pelos processos em execução. O espaço é dividido em bytes, e cada byte possui um endereço. A CPU acessa esse espaço por meio de barramentos de dados, endereços e controle. Se o barramento de endereços possui $n$ linhas, então o número teórico máximo de posições endereçáveis é:

$$ 2^n $$

Por exemplo, um barramento de 32 bits pode endereçar até $2^{32}$ posições, o que corresponde a 4 GB em um modelo byte-addressable. Já um espaço virtual de 64 bits é muito maior, ainda que os sistemas reais usem apenas parte dele.

Os processos, porém, normalmente não enxergam endereços físicos diretamente. Eles operam sobre endereços lógicos ou virtuais. A tradução desses endereços para posições reais na RAM é feita pela MMU (Memory Management Unit). Além de traduzir, a MMU participa da proteção de memória, impedindo que um processo leia ou escreva regiões pertencentes a outro processo ou ao núcleo.

Essa separação é fundamental para estabilidade e segurança. Se um programa com erro pudesse escrever em qualquer região física, ele poderia corromper o kernel, destruir dados de outros processos ou comprometer o sistema inteiro. O espaço virtual isolado reduz esse risco e ainda facilita relocação, carregamento sob demanda e compartilhamento controlado de bibliotecas.

Historicamente, a memória foi organizada de diferentes maneiras. Em particionamento, a RAM é dividida em regiões fixas ou variáveis, e cada processo ocupa uma dessas regiões. O modelo é simples, mas sofre com baixa flexibilidade e com fragmentação. Em partições fixas, pode haver desperdício interno; em partições variáveis, aparece fragmentação externa.

Na segmentação, o espaço de endereçamento é dividido em regiões lógicas, como código, dados, pilha e tabelas. Um endereço segmentado possui algo como (segmento, deslocamento). A segmentação é conceitualmente elegante porque acompanha a estrutura lógica do programa, mas traz desafios de alocação e proteção. Em sistemas modernos, ela costuma aparecer combinada com outros mecanismos, enquanto a paginação é dominante em sistemas de uso geral.

Na paginação, o espaço virtual é dividido em blocos de tamanho fixo chamados páginas. A memória física é dividida no mesmo tamanho, em blocos chamados quadros ou frames. Uma tabela de páginas informa em que quadro está cada página, além de flags como presença, permissão de leitura/escrita, modificação e acesso recente.

Um tamanho típico de página é 4096 bytes, ou 4 KB. Como $4096 = 2^{12}$, os 12 bits menos significativos do endereço representam o deslocamento interno na página, e os bits restantes identificam o número da página virtual. Se o endereço virtual for 12345, então:

$$ 12345 = 3 \cdot 4096 + 57 $$

Isso significa que o endereço está na página virtual 3, deslocamento 57. Se a tabela de páginas indicar que a página 3 está carregada no quadro físico 10, então o endereço físico correspondente será:

$$ 10 \cdot 4096 + 57 = 41017 $$

Como essa tradução acontece com enorme frequência, os processadores usam TLBs (Translation Lookaside Buffers), que funcionam como caches de traduções recentes. Se a tradução estiver na TLB, a CPU evita consultar a tabela de páginas completa. Caso contrário, ocorre um TLB miss, e o hardware ou o sistema precisa buscar a informação na estrutura principal. Em programas com bom padrão de acesso, a TLB ajuda bastante o desempenho.

Em espaços virtuais grandes, tabelas de páginas lineares podem desperdiçar muita memória. Por isso, são usadas tabelas multinível, nas quais o endereço virtual é dividido em partes que selecionam níveis sucessivos da estrutura. A ideia intuitiva é simples: em vez de manter uma tabela enorme para todo o espaço possível, o sistema cria apenas os níveis realmente necessários para as regiões utilizadas pelo processo.

Quando a página referenciada não está em RAM, ocorre uma falta de página, ou page fault. O acesso gera uma exceção, o núcleo assume o controle, localiza a página no armazenamento secundário, escolhe um quadro livre ou uma página vítima, atualiza a tabela de páginas e retoma a execução. Esse mecanismo viabiliza memória virtual muito maior que a RAM física, mas a diferença de custo entre RAM e disco torna cada falta de página relativamente cara.

Como a RAM é limitada, o sistema precisa decidir o que manter residente e o que remover temporariamente. Algumas técnicas históricas incluem overlays, em que o próprio programador controla módulos carregados sob demanda; swapping, em que processos inteiros ou grandes partes deles são movidos entre RAM e disco; e paging, em que a unidade de troca é a página individual. Em sistemas atuais, paginação e swap são predominantes, embora a palavra swap não signifique RAM extra, e sim uso de armazenamento secundário como apoio à memória virtual.

Um conceito central para desempenho é a localidade de referência. Localidade temporal significa que dados usados recentemente tendem a ser usados de novo em breve. Localidade espacial significa que dados próximos tendem a ser acessados em sequência. Um laço sobre um vetor costuma explorar bem ambas. Por exemplo:

for (i = 0; i < n; i++) {
    soma += vetor[i];
}

Esse código percorre posições contíguas, o que favorece cache e paginação. Já em matrizes, a ordem de acesso também importa. Em linguagens com armazenamento por linha, percorrer linha por linha costuma ser mais amigável ao cache do que percorrer coluna por coluna:

for (i = 0; i < n; i++) {
    for (j = 0; j < m; j++) {
        soma += matriz[i][j];
    }
}

O espaço virtual de um processo normalmente é subdividido em áreas com funções distintas. Uma organização comum inclui:

  • text: código executável e, em muitos casos, partes somente leitura;
  • data: variáveis globais ou estáticas inicializadas;
  • BSS: variáveis globais ou estáticas não inicializadas;
  • heap: memória dinâmica solicitada pela aplicação;
  • stack: pilhas de execução, parâmetros, endereços de retorno e variáveis locais automáticas.

Um exemplo simples em C ajuda a visualizar:

int global_inicializada = 10;     // data
int global_nao_inicializada;      // BSS

int main() {
    int local = 5;                // stack
    int *p = malloc(sizeof(int)); // heap
    *p = 42;
}

Em geral, variáveis locais vivem na stack e são destruídas automaticamente ao fim da função. Blocos pedidos com malloc, new ou mecanismos equivalentes vivem na heap e exigem gerenciamento explícito ou automático, conforme a linguagem. Heap e stack costumam crescer em direções opostas, deixando espaço intermediário para expansão.

A alocação pode ser estática, automática ou dinâmica. A estática é definida antes da execução e dura por toda a vida do processo. A automática acompanha o fluxo de chamadas. A dinâmica oferece flexibilidade, mas pode introduzir custos maiores, vazamentos e fragmentação.

O mecanismo que reserva blocos de memória é chamado alocador. Ele pode existir no espaço do núcleo, no espaço do usuário ou em bibliotecas e runtimes de linguagem. Algumas estratégias clássicas de alocação são first-fit, best-fit, worst-fit e next-fit. A fragmentação interna ocorre quando um bloco alocado é maior do que o necessário; a fragmentação externa ocorre quando sobram vários espaços livres pequenos e dispersos, dificultando novas alocações grandes.

Entre alocadores conhecidos, o buddy allocator trabalha com blocos de tamanho $2^n$, o que simplifica fusões, mas pode aumentar fragmentação interna. O slab allocator é muito usado no kernel para objetos frequentes e de tamanho semelhante, como descritores de processos, arquivos e sockets. No espaço do usuário, bibliotecas e runtimes frequentemente combinam várias técnicas e podem até manter memory pools para reduzir custo e fragmentação em cargas repetitivas.

Quando a RAM fica pressionada, o sistema precisa escolher páginas vítimas para remoção. Alguns algoritmos clássicos de substituição são:

  • FIFO: remove a página mais antiga na RAM;
  • Ótimo: remove a página cujo próximo uso está mais distante no futuro; é teórico, pois exige conhecer a sequência futura;
  • LRU (Least Recently Used): remove a menos recentemente usada;
  • Segunda chance ou clock: aproxima FIFO, mas preserva páginas usadas recentemente;
  • NRU (Not Recently Used): usa bits de referência e modificação para escolher vítimas menos importantes;
  • Random: escolhe aleatoriamente, servindo às vezes como baseline simples.

Considere, por exemplo, a sequência de referências 1, 2, 3, 1, 4, 5 com apenas 3 quadros. Depois de carregar 1, 2, 3, uma política FIFO expulsaria 1 para trazer 4, mesmo que 1 tenha sido usada recentemente. Uma política LRU, por outro lado, analisaria o uso mais recente para tentar preservar páginas ainda quentes. A diferença entre essas escolhas afeta diretamente o número de faltas de página.

Alguns algoritmos apresentam comportamentos contraintuitivos. A anomalia de Belady mostra que, sob FIFO, aumentar o número de quadros pode em certos casos aumentar o número de faltas de página. Isso reforça que quantidade de memória, sozinha, não resolve todo problema de desempenho se a política de substituição for inadequada.

Outro conceito importante é o working set, ou conjunto de trabalho: o subconjunto de páginas que um processo vem usando com frequência em certo intervalo. Se o conjunto de trabalho de muitos processos não cabe na RAM, o sistema entra em thrashing, gastando mais tempo trocando páginas entre RAM e disco do que executando trabalho útil. Em aplicações reais, isso aparece como lentidão severa, disco saturado e baixa utilização efetiva da CPU.

Esses mecanismos têm aplicações diretas em navegadores com muitas abas, bancos de dados, compiladores, máquinas virtuais, containers, servidores de cache, processamento de imagens e treinamento de modelos. Em todos esses casos, entender localidade, paginação, TLB, swap e políticas de substituição ajuda a explicar por que certos programas escalam bem e outros degradam rapidamente quando a pressão de memória aumenta.

15.5 - Gerenciamento de Arquivos.

O gerenciamento de arquivos é uma das funções mais visíveis do sistema operacional. Arquivos permitem armazenar dados de forma persistente, isto é, manter informações mesmo após o fim da execução de um processo ou o desligamento da máquina. Programas, documentos, imagens, logs, bancos de dados e arquivos de configuração dependem dessa abstração.

Para aplicações e usuários, um arquivo pode ser visto como uma sequência nomeada de bytes associada a atributos e organizada dentro de diretórios. Essa interface simples esconde uma implementação complexa: o sistema operacional precisa localizar blocos físicos no dispositivo, administrar espaço livre, garantir integridade dos dados e aplicar regras de acesso concorrente e permissões.

Entre as operações básicas oferecidas por um sistema de arquivos estão:

  • criar e remover arquivos e diretórios;
  • abrir, fechar, ler, escrever e reposicionar o cursor de leitura;
  • renomear e mover objetos;
  • consultar metadados como tamanho, dono, permissões e datas;
  • controlar compartilhamento e bloqueio de acesso.

Além do conteúdo, cada arquivo possui metadados. Esses metadados podem incluir nome, tipo, tamanho, identificador, dono, grupo, permissões, timestamps e localização interna no dispositivo. Em muitos sistemas de arquivos, essas informações são mantidas em estruturas separadas do conteúdo bruto.

Diretórios são estruturas especiais que organizam arquivos e outros diretórios em forma hierárquica. Isso permite construir caminhos absolutos e relativos, facilitando localização e isolamento lógico de dados. Essa hierarquia também é importante para políticas de segurança, backup, montagem de partições e administração de usuários.

Do ponto de vista lógico, o sistema de arquivos oferece uma visão contínua e organizada. Do ponto de vista físico, porém, o armazenamento é dividido em blocos, páginas ou setores. O sistema operacional precisa mapear o arquivo lógico para essas unidades físicas.

Esse mapeamento pode seguir diferentes estratégias de alocação:

  • alocação contígua, em que os blocos do arquivo ficam próximos e em sequência;
  • alocação encadeada, em que cada bloco referencia o próximo;
  • alocação indexada, em que estruturas auxiliares apontam para os blocos do arquivo.

Cada estratégia envolve trade-offs. Alocação contígua favorece leitura sequencial, mas dificulta crescimento. Alocação encadeada simplifica expansão, mas pode prejudicar acesso aleatório. Alocação indexada dá mais flexibilidade, mas exige estruturas extras de controle.

Outra distinção importante é entre acesso sequencial e acesso aleatório. Em acesso sequencial, os dados são lidos ou escritos em ordem natural. Em acesso aleatório, o processo pode saltar diretamente para uma posição específica do arquivo. Bancos de dados, formatos multimídia, compiladores e sistemas de índices dependem fortemente dessa segunda forma de acesso.

Para melhorar desempenho, sistemas operacionais mantêm cache de arquivos em memória principal. Quando uma aplicação solicita dados, o sistema tenta atendê-la a partir desse cache antes de acessar o dispositivo físico. Isso reduz latência, mas também exige políticas de coerência para garantir que o conteúdo em memória e o conteúdo persistido permaneçam compatíveis.

Permissões são outro componente central. Em sistemas multiusuário, o SO precisa definir quem pode ler, escrever ou executar determinado arquivo. Em UNIX, isso normalmente é expresso por dono, grupo e outros; em outros sistemas, podem existir listas de controle de acesso mais detalhadas.

Também é necessário lidar com concorrência. Dois processos podem tentar gravar no mesmo arquivo simultaneamente, ou um pode tentar ler enquanto outro ainda escreve. O sistema operacional e as bibliotecas associadas oferecem mecanismos de travamento, serialização e coordenação para evitar corrupção de dados.

Em muitos sistemas modernos, técnicas como journaling ajudam a manter a consistência após falhas. Antes de efetivar certas mudanças estruturais, o sistema registra a operação em um log. Se houver queda de energia ou interrupção inesperada, esse log pode ser usado para recuperar um estado consistente com menos perda.

Exemplos de sistemas de arquivos incluem FAT, NTFS, ext4, XFS, APFS e ZFS. Eles diferem em suporte a permissões, journaling, snapshots, compressão, integridade, escalabilidade e tolerância a falhas.

Em computação moderna, o gerenciamento de arquivos também aparece em ambientes distribuídos e em nuvem. Nesses contextos, além de armazenar dados, o sistema precisa tratar replicação, sincronização entre múltiplos nós, consistência, latência de rede e disponibilidade sob falha.

Para aplicações, as operações costumam aparecer por interfaces como open, read, write, close e seek. Linguagens de programação encapsulam essas primitivas em bibliotecas, mas a semântica fundamental continua vindo do sistema operacional.

Do ponto de vista de desempenho, o gerenciamento de arquivos é decisivo em bancos de dados, compiladores, servidores web, logs, sistemas de backup, aplicações multimídia e pipelines de processamento de dados. Escolhas como tamanho de bloco, política de cache, frequência de sincronização em disco e forma de alocação podem alterar significativamente o comportamento da aplicação.

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

O gerenciamento de entrada e saída existe para permitir que programas conversem com o mundo externo de forma padronizada, apesar da enorme diversidade de dispositivos físicos. Teclados, SSDs, placas de rede, webcams, sensores USB, GPUs e impressoras possuem tempos de resposta, volumes de dados e protocolos muito diferentes. O sistema operacional precisa esconder boa parte dessa heterogeneidade e oferecer uma interface uniforme para as aplicações.

Uma forma útil de enxergar o problema é pensar em uma pilha de abstrações. Na base está o dispositivo físico. Acima dele aparece o controlador, que intermedeia a comunicação com a CPU e com a memória. Em seguida vêm barramentos e protocolos como USB, PCIe ou SATA/NVMe. O driver do sistema operacional conversa com esse hardware e expõe operações mais genéricas ao núcleo. Por fim, a aplicação acessa tudo isso por chamadas como open, read, write, ioctl e close.

Nem todo dispositivo trabalha com sinais analógicos, e muitos já possuem eletrônica digital complexa. Por isso, é mais seguro dizer que cada periférico segue um protocolo de comunicação próprio e troca dados com o computador por meio de registradores, buffers, filas e sinais de controle. A CPU normalmente não conversa diretamente com o mecanismo interno do dispositivo; ela se comunica com o controlador associado a ele.

O controlador é o elemento que intermedeia a operação prática de E/S. Ele costuma expor registradores de dados, status e controle. Um registrador de dados transporta bytes ou blocos; um registrador de status informa se o dispositivo está pronto, ocupado ou em erro; e um registrador de controle recebe comandos como iniciar leitura, gravar bloco, limpar interrupção ou redefinir estado.

Em muitos dispositivos modernos, parte da lógica operacional reside em firmware, isto é, código embarcado no próprio hardware. Isso aparece em SSDs, placas de rede, teclados USB, controladoras NVMe e GPUs. O firmware não substitui o driver: ele executa no dispositivo, enquanto o driver executa no sistema operacional e adapta o hardware à interface do núcleo.

Historicamente, muitas arquiteturas separavam controladores em blocos como north bridge e south bridge. Em máquinas atuais, vários desses componentes foram integrados ao processador ou ao chipset principal, mas essa referência histórica ainda ajuda a entender que diferentes caminhos de E/S podem ter velocidades e prioridades muito distintas.

O acesso da CPU aos registradores do dispositivo pode seguir modelos diferentes. Em E/S mapeada em portas, o processador usa instruções específicas para ler e escrever portas dedicadas. Em E/S mapeada em memória, esses registradores aparecem como endereços especiais no espaço de memória, permitindo acesso com instruções comuns de carga e armazenamento. Há ainda arquiteturas híbridas e controladores mais sofisticados com canais próprios de transferência.

Um fluxo conceitual simples de operação pode ser descrito assim: o driver escreve um comando no registrador de controle, verifica ou aguarda a mudança do registrador de status e lê ou grava dados em buffers associados ao dispositivo. Em uma leitura de disco, por exemplo, o sistema pode indicar o bloco desejado, disparar a operação e depois esperar a conclusão para transferir os dados ao processo solicitante.

O software que implementa essa comunicação é o driver. Cada dispositivo, ou cada família de dispositivos, precisa de um driver compatível com o sistema operacional. O driver traduz operações genéricas do núcleo em comandos específicos do hardware, configura o controlador, trata erros, gerencia buffers e participa da sincronização entre o dispositivo e o restante do sistema. Por executar funções privilegiadas, o driver costuma operar em modo núcleo ou em um ambiente muito controlado.

Em muitos sistemas, os dispositivos são agrupados em classes para simplificar a interface:

  • orientados a caracteres: comunicação byte a byte, como teclado, porta serial e alguns sensores;
  • orientados a blocos: transferência em blocos de tamanho fixo, como SSDs e discos;
  • de rede: envio e recepção de quadros ou pacotes de tamanho variável por placas de rede;
  • gráficos: dispositivos de alto volume de dados, como GPU e framebuffer, com forte preocupação de desempenho.

Uma questão central em E/S é como o sistema detecta que o dispositivo terminou uma operação ou precisa de atenção. Existem três estratégias principais. Na primeira, chamada polling, o driver verifica repetidamente o registrador de status até que o dispositivo fique pronto. Essa abordagem é simples, mas pode desperdiçar CPU em espera ativa. Na segunda, baseada em interrupções, o dispositivo ou controlador envia um sinal à CPU quando algo relevante acontece. Na terceira, usa-se DMA (Direct Memory Access), permitindo que grandes transferências ocorram diretamente entre dispositivo e memória principal com intervenção mínima da CPU.

O mecanismo de interrupções é especialmente importante. Quando o dispositivo termina uma leitura ou recebe novos dados, ele pode gerar uma IRQ (Interrupt Request). O controlador de interrupções encaminha o evento à CPU, que salva contexto suficiente, interrompe o fluxo atual e executa uma rotina tratadora registrada na tabela apropriada. Em muitos sistemas, o atendimento ocorre em dois estágios: um tratamento primário rápido, que reconhece o evento e coleta o mínimo necessário; e um tratamento posterior, fora do contexto crítico imediato, que realiza o trabalho mais pesado.

O contraste entre polling e interrupção fica claro em um teclado. Se o sistema consultasse o teclado milhões de vezes por segundo para saber se uma tecla foi pressionada, grande parte da CPU seria desperdiçada. Com interrupções, o dispositivo avisa apenas quando há um evento real. Em contrapartida, em situações de altíssima taxa de eventos ou em dispositivos muito rápidos, polling controlado pode ser útil para evitar excesso de interrupções.

DMA é especialmente vantajoso quando o volume de dados é grande, como em leitura de blocos de disco, recepção intensa de pacotes de rede ou captura de vídeo. Em vez de a CPU copiar byte por byte, o controlador de DMA transfere um bloco inteiro entre o dispositivo e a RAM. A CPU continua responsável por configurar a operação e tratar sua conclusão, mas fica liberada do trabalho repetitivo de movimentar cada dado individualmente.

Um fluxo simplificado de leitura de arquivo pode ser visto assim:

processo chama read(fd, buffer, n)
kernel verifica cache
se necessario, driver programa controladora
dispositivo transfere dados por interrupcao ou DMA
kernel marca operacao como concluida
processo acorda e recebe os dados

Esse fluxo mostra por que E/S pode bloquear processos. Se os dados ainda não estiverem disponíveis, o processo normalmente entra em espera até a conclusão da operação. Em sistemas e bibliotecas modernas, também existem E/S não bloqueante, multiplexação de eventos e APIs assíncronas, muito usadas em servidores de alta concorrência e interfaces reativas.

Buffers e filas são essenciais nesse contexto porque CPU e dispositivos trabalham em velocidades diferentes. Um buffer desacopla produtor e consumidor temporariamente: a placa de rede pode depositar pacotes em filas; o driver pode acumular dados antes de entregá-los à aplicação; a escrita em disco pode ser agrupada antes do envio ao hardware. Esse desacoplamento ajuda a melhorar throughput, mas também exige políticas de sincronização, flush e tratamento de estouro.

Em E/S, latência e throughput nem sempre apontam na mesma direção. Um teclado ou mouse exige baixa latência para manter sensação de resposta imediata. Já um SSD ou uma interface de rede sob carga pesada pode priorizar throughput agregando requisições e explorando paralelismo interno. Sistemas gráficos e multimídia também acrescentam a exigência de regularidade temporal, isto é, entregar dados no ritmo correto para evitar falhas perceptíveis.

Falhas e recuperação também fazem parte do problema. O dispositivo pode estar ausente, ocupado, desconectado, com timeout ou retornar erro de leitura e escrita. O driver e o sistema operacional precisam decidir quando repetir a operação, quando sinalizar falha à aplicação e como manter o restante do sistema estável, inclusive em cenários de remoção a quente.

Esses princípios aparecem em quase todo sistema real: leitura de uma tecla, gravação de um arquivo em SSD, recepção de um pacote pela rede, envio de um quadro à tela ou coleta de dados de um sensor USB. Em todos os casos, o sistema operacional precisa equilibrar padronização, desempenho, isolamento, uso de CPU e robustez diante de dispositivos muito diferentes entre si.

15.7 - Alocação de Recursos.

Os sistemas operacionais precisam definir como disponibilizar recursos finitos para múltiplos processos, usuários e serviços. Esses recursos incluem processador, memória, arquivos, descritores, largura de banda de rede, disco, dispositivos de entrada e saída e até mecanismos de sincronização, como locks e semáforos. Em sistemas móveis, consumo de energia também entra nessa conta. A alocação deve buscar eficiência, justiça, previsibilidade, isolamento e uso adequado do hardware.

Nem todos os recursos têm a mesma natureza. Alguns são preemptíveis, isto é, podem ser retirados de uma tarefa e reatribuídos com custo controlado, como fatias de CPU ou certas páginas de memória. Outros são não preemptíveis ou só podem ser retomados com muito cuidado, como uma impressora em uso, um arquivo sendo gravado ou um lock que protege uma estrutura crítica. Essa diferença ajuda a entender por que certas políticas de alocação são simples em alguns casos e difíceis em outros.

Também é útil distinguir política e mecanismo. Política responde perguntas como: qual processo recebe o recurso primeiro, por quanto tempo, com qual prioridade e sob quais limites? Mecanismo é a implementação concreta dessa decisão: filas de prontos, cotas de memória, controle de banda, filas de disco, contadores de referência, time slices, prioridades e limites de cgroups ou containers.

Nem sempre os objetivos da alocação são compatíveis. Um sistema pode priorizar throughput máximo, isto é, concluir o maior número possível de tarefas por unidade de tempo, ou pode priorizar tempo de resposta para manter a interface fluida. Em servidores, utilização média e equilíbrio de carga podem pesar mais. Em sistemas interativos, latência percebida e responsividade costumam ser decisivas. Em sistemas de tempo real, previsibilidade temporal pode ser mais importante do que throughput.

Quando vários processos disputam um mesmo recurso, o sistema pode enfileirar pedidos, priorizar certas tarefas, impor cotas, adiar requisições ou até rejeitá-las. Esse problema aparece em muitos níveis: tarefas competem por CPU, processos competem por memória física, threads disputam locks, aplicações disputam conexões de rede e máquinas virtuais concorrem por núcleos e armazenamento em um mesmo host.

Um exemplo simples ajuda a concretizar. Suponha uma máquina com 2 núcleos e 8 GB de RAM executando quatro serviços. Se dois deles precisarem de muita CPU e outros dois forem intensivos em memória, o sistema terá de balancear tempo de processador, pressionar ou não o swap, priorizar certas filas de E/S e talvez impor limites para impedir que um único serviço degrade todos os demais. O mesmo raciocínio vale para três máquinas virtuais disputando um host físico ou vários containers compartilhando um nó em um cluster.

Na prática, a alocação reaparece em quase todos os subsistemas do sistema operacional. No processador, aparece no escalonamento e nas prioridades. Na memória, surge em paginação, working set, quotas e substituição. Em disco e rede, aparece em filas, cache, controle de taxa e ordenação de requisições. Em E/S, envolve buffers, DMA, interrupções e divisão de acesso entre dispositivos concorrentes.

Problemas clássicos de alocação incluem starvation, inversão de prioridade e deadlock. Em starvation, uma tarefa permanece apta a executar, mas nunca recebe o recurso porque outras continuam sendo favorecidas. Na inversão de prioridade, uma tarefa importante pode ficar bloqueada por outra menos prioritária que segura um recurso compartilhado. Em deadlock, dois ou mais processos entram em espera circular e ninguém consegue prosseguir. Isso ocorre tipicamente quando há exclusão mútua, retenção e espera, ausência de preempção e dependência circular.

As estratégias clássicas para lidar com deadlock incluem prevenção, evasão, detecção e recuperação. Prevenção tenta impedir estruturalmente que as condições do impasse ocorram; evasão decide dinamicamente se um novo pedido é seguro; detecção procura ciclos de espera já formados; e recuperação tenta quebrar o impasse por aborto, rollback ou retirada forçada de algum recurso quando isso é possível.

Outro aspecto importante é o controle de admissão. Em vez de aceitar toda carga recebida, o sistema ou serviço pode limitar o número de conexões, processos, threads ou operações pendentes. Essa decisão é essencial para evitar colapso sob sobrecarga. Servidores web, bancos de dados, sistemas de filas e plataformas de nuvem frequentemente preferem recusar ou atrasar parte das requisições a permitir que toda a infraestrutura entre em degradação severa.

Em cloud computing e virtualização, a alocação de recursos se torna ainda mais explícita. O provedor precisa decidir quantos núcleos virtuais, memória, armazenamento e largura de banda conceder a cada cliente ou serviço. Técnicas como quotas, reservas, limites máximos, oversubscription e autoscaling permitem usar melhor a infraestrutura, mas também podem aumentar contenção quando muitos consumidores demandam recursos ao mesmo tempo.

Essa discussão se relaciona diretamente com qualidade de serviço e isolamento. Um sistema multiusuário precisa impedir que uma aplicação monopolize CPU, memória ou I/O e prejudique as demais. Em segurança, isso também importa: uma alocação mal controlada pode facilitar negação de serviço, exaustão de descritores, consumo abusivo de memória ou saturação de filas internas.

Do ponto de vista operacional, a alocação pode ser observada por métricas como uso de CPU, tempo de fila, latência de disco, taxa de falta de página, consumo de memória, largura de banda, número de processos bloqueados e nível de throttling. Essas medições ajudam a entender se a política atual está promovendo equilíbrio real ou apenas deslocando o gargalo de um subsistema para outro.

Em computação moderna, a alocação de recursos aparece em escalonadores de CPU, alocadores de memória, filas de disco, controle de banda, quotas de contêineres, isolamento entre máquinas virtuais, políticas de banco de dados, serviços de nuvem e plataformas distribuídas. Em todos esses cenários, o problema central é o mesmo: distribuir recursos finitos entre demandas concorrentes sem comprometer desempenho, justiça, previsibilidade e estabilidade do sistema.


Glossário

BIOS
Firmware básico de inicialização de hardware em arquiteturas tradicionais.
BSS
Região de memória que armazena variáveis estáticas não inicializadas.
CAS
Compare-And-Swap, instrução atômica usada em sincronização concorrente.
Deadlock
Impasse em que duas ou mais tarefas ficam esperando recursos umas das outras e nenhuma consegue prosseguir.
Dispatcher
Rotina do sistema operacional responsável por efetivar a troca para a próxima tarefa escolhida.
DMA
Direct Memory Access, mecanismo de transferência direta entre dispositivo e memória.
Driver
Software que controla a comunicação entre o sistema operacional e um dispositivo.
Fairness
Critério de justiça na distribuição de CPU ou outros recursos entre tarefas concorrentes.
FCFS
First-Come, First-Served, política que atende tarefas na ordem de chegada.
FIFO
First-In, First-Out, política que remove primeiro o item mais antigo.
Firmware
Código embarcado em hardware para controlar o próprio dispositivo.
Frame
Quadro de memória física usado para armazenar uma página quando há paginação.
Heap
Região de memória usada para alocação dinâmica.
IPC
Inter-Process Communication, mecanismos de comunicação entre processos.
IRQ
Interrupt Request, sinal de interrupção enviado por hardware.
Kernel
Núcleo do sistema operacional, responsável pelo controle privilegiado de recursos.
Livelock
Situação em que tarefas continuam mudando de estado, mas sem realizar progresso útil.
LRU
Least Recently Used, política que remove a página menos usada recentemente.
MMU
Memory Management Unit, unidade de hardware que traduz endereços e ajuda na proteção de memória.
Monitor
Estrutura de sincronização que encapsula recurso compartilhado e controla acesso concorrente.
Mutex
Mecanismo de exclusão mútua que permite apenas uma tarefa por vez em uma seção crítica.
Page fault
Falta de página que ocorre quando o processo acessa uma página virtual que não está carregada na RAM.
PCB
Process Control Block, estrutura com contexto e metadados de um processo.
POSIX
Padrão de interface para sistemas compatíveis com Unix.
Preempção
Interrupção forçada de uma tarefa para dar lugar a outra.
Polling
Técnica em que o software consulta repetidamente o estado de um dispositivo em vez de esperar interrupção.
pthreads
Implementação padronizada de threads em ambientes POSIX, conhecida como POSIX Threads.
Quantum
Fatia de tempo de CPU atribuída a uma tarefa.
Race condition
Erro causado por acessos concorrentes não coordenados a recursos compartilhados.
Round-Robin
Política de escalonamento preemptiva que alterna tarefas em rodadas usando um quantum fixo.
Scheduler
Componente que escolhe qual tarefa deve executar a seguir.
Semáforo
Estrutura de sincronização baseada em contador e operações atômicas.
Stack
Região de memória associada ao fluxo de chamadas e variáveis locais.
Starvation
Problema em que uma tarefa espera indefinidamente por recurso ou CPU.
Swap
Área ou mecanismo de transferência entre RAM e armazenamento secundário.
TCB
Task Control Block, estrutura de contexto de uma tarefa ou thread.
Thread
Fluxo de execução dentro de um processo.
Throughput
Quantidade de trabalho concluído por unidade de tempo em um sistema.
Thrashing
Situação em que o sistema passa mais tempo trocando páginas do que executando trabalho útil.
TLB
Translation Lookaside Buffer, cache de traduções de endereços virtuais para físicos.
TLS
Thread Local Storage, armazenamento local privado de cada thread.
Working set
Conjunto de páginas que um processo tem usado com frequência em certo intervalo recente.

Referências:

  1. Sistemas Operacionais - Carlos Maziero
  2. Operating System Concepts - Abraham Silberschatz, Peter B. Galvin, Greg Gagne
  3. Modern Operating Systems - Andrew S. Tanenbaum, Herbert Bos