Slides da Aula 06
Entrada/saída
Além de oferecer abstrações como processos, espaços de endereçamentos e arquivos, um sistema operacional também controla todos os dispositivos de E/S (entrada/saída) do computador. Ele deve emitir comandos para os dispositivos, interceptar interrupções e lidar com erros. Também deve fornecer uma interface entre os dispositivos e o resto do sistema que seja simples e fácil de usar. Na medida do possível, a interface deve ser a mesma para todos os dispositivos (independentemente do dispositivo). O código de E/S representa uma fração significativa do sistema operacional total. Como o sistema operacional gerencia a E/S é o assunto dessa aula.
Princípios do hardware de E/S
Diferentes pessoas veem o hardware de E/S de maneiras diferentes. Engenheiros elétricos o veem como chips, cabos, motores, suprimento de energia e todos os outros componentes físicos que compreendem o hardware. Programadores olham para a interface apresentada ao software — os comandos que o hardware aceita, as funções que ele realiza e os erros que podem ser reportados de volta. Neste livro, estamos interessados na programação de dispositivos de E/S, e não em seu projeto, construção ou manutenção; portanto, nosso interesse é saber como o hardware é programado, não como ele funciona por dentro. Não obstante, isso, a programação de muitos dispositivos de E/S está muitas vezes intimamente ligada à sua operação interna. Nos próximos parágrafos, apresentaremos uma pequena visão geral sobre hardwares de E/S à medida que estes se relacionam com a programação.
Dispositivos de E/S
Dispositivos de E/S podem ser divididos de modo geral em duas categorias: dispositivos de blocos e dispositivos de caractere. O primeiro armazena informações em blocos de tamanho fixo, cada um com seu próprio endereço. Tamanhos de blocos comuns variam de 512 a 65.536 bytes. Todas as transferências são em unidades de um ou mais blocos inteiros (consecutivos). A propriedade essencial de um dispositivo de bloco é que cada bloco pode ser lido ou escrito independentemente de todos os outros. Discos rígidos, discos Blu-ray e pendrives são dispositivos de bloco comuns.
Se você observar bem de perto, o limite entre dispositivos endereçáveis por blocos e aqueles que não são não é bem definido. Todo mundo concorda que um disco é um dispositivo endereçável por bloco, pois não importa a posição em que se encontra o braço no momento, é sempre possível buscar em outro cilindro e então esperar que o bloco solicitado gire sob a cabeça. Agora considere um velho dispositivo de fita magnética ainda usado, às vezes, para realizar backups de disco (porque fitas são baratas). Fitas contêm uma sequência de blocos. Se o dispositivo de fita receber um comando para ler o bloco N, ele sempre pode rebobiná-la e ir direto até chegar ao bloco N. Essa operação é análoga a um disco realizando uma busca, exceto por levar muito mais tempo. Também pode ou não ser possível reescrever um bloco no meio de uma fita. Mesmo que fosse possível usar as fitas como dispositivos de bloco com acesso aleatório, isso seria forçar o ponto de algum modo: em geral elas não são usadas dessa maneira.
O outro dispositivo de E/S é o de caractere. Um dispositivo de caractere envia ou aceita um fluxo de caracteres, desconsiderando qualquer estrutura de bloco. Ele não é endereçável e não tem qualquer operação de busca. Impressoras, interfaces de rede, mouses (para apontar), ratos (para experimentos de psicologia em laboratórios) e a maioria dos outros dispositivos que não são parecidos com discos podem ser vistos como dispositivos de caracteres.
Esse esquema de classificação não é perfeito. Alguns dispositivos não se enquadram nele. Relógios, por exemplo, não são endereçáveis por blocos. Tampouco geram ou aceitam fluxos de caracteres. Tudo o que fazem é causar interrupções em intervalos bem definidos. As telas mapeadas na memória não se enquadram bem no modelo também. Tampouco as telas de toque, quanto a isso. Ainda assim, o modelo de dispositivos de blocos e de caractere é suficientemente geral para ser usado como uma base para fazer alguns dos softwares do sistema operacional que tratam de E/S independentes dos dispositivos. O sistema de arquivos, por exemplo, lida apenas com dispositivos de blocos abstratos e deixa a parte dependente de dispositivos para softwares de nível mais baixo.
Dispositivos de E/S cobrem uma ampla gama de velocidades, o que coloca uma pressão considerável sobre o software para desempenhar bem através de muitas ordens de magnitude em taxas de transferência de dados. A figura “Algumas taxas de dados típicas de dispositivos, placas de redes e barramentos.” mostra as taxas de dados de alguns dispositivos comuns. A maioria desses dispositivos tende a ficar mais rápida com o passar do tempo.
O cartão controlador costuma ter um conector, no qual um cabo levando ao dispositivo em si pode ser conectado. Muitos controladores podem lidar com dois, quatro ou mesmo oito dispositivos idênticos. Se a interface entre o controlador e o dispositivo for padrão, seja um padrão oficial ANSI, IEEE ou ISO — ou um padrão de facto —, então as empresas podem produzir controladores ou dispositivos que se enquadrem àquela interface. Muitas empresas, por exemplo, produzem controladores de disco compatíveis com as interfaces SATA, SCSI, USB, Thunderbolt ou FireWire (IEEE 1394).
A interface entre o controlador e o dispositivo muitas vezes é de nível muito baixo. Um disco, por exemplo, pode ser formatado com 2 milhões de setores de 512 bytes por trilha. No entanto, o que realmente sai da unidade é um fluxo serial de bits, começando com um preâmbulo, então os 4096 bits em um setor, e por fim uma soma de verificação (checksum), ou código de correção de erro (ECC — Error Correcting Code). O preâmbulo é escrito quando o disco é formatado e contém o cilindro e número de setor, e dados similares, assim como informações de sincronização.
O trabalho do controlador é converter o fluxo serial de bits em um bloco de bytes, assim como realizar qualquer correção de erros necessária. O bloco de bytes é tipicamente montado primeiro, bit por bit, em um buffer dentro do controlador. Após a sua soma de verificação ter sido verificada e o bloco ter sido declarado livre de erros, ele pode então ser copiado para a memória principal.
O controlador para um monitor LCD também funciona um pouco como um dispositivo serial de bits em um nível igualmente baixo. Ele lê os bytes contendo os caracteres a serem exibidos da memória e gera os sinais para modificar a polarização da retro iluminação para os pixels correspondentes a fim de escrevê-los na tela. Se não fosse pelo controlador de tela, o programador do sistema operacional teria de programar explicitamente os campos elétricos de todos os pixels. Com o controlador, o sistema operacional o inicializa com alguns parâmetros, como o número de caracteres ou pixels por linha e o número de linhas por tela, e deixa o controlador cuidar realmente da orientação dos campos elétricos.
Em um tempo muito curto, telas de LCD substituíram completamente os velhos monitores CRT (Catho-de Ray Tube — Tubo de Raios Catódicos). Monitores CRT disparam um feixe de elétrons em uma tela fluorescente. Usando campos magnéticos, o sistema é capaz de curvar o feixe e atrair pixels sobre a tela. Comparado com as telas de LCD, os monitores CRT eram grandalhões, gastadores de energia e frágeis. Além disso, a resolução das telas de LCD (Retina) de hoje é tão boa que o olho humano é incapaz de distinguir pixels individuais. É difícil de imaginar que os laptops no passado vinham com uma pequena tela CRT que os deixava com mais de 20 cm de fundo e um peso de aproximadamente 12 quilos.
E/S mapeada na memória
Cada controlador tem alguns registradores que são usados para comunicar-se com a CPU. Ao escrever nesses registradores, o sistema operacional pode comandar o dispositivo a fornecer e aceitar dados, ligar-se e desligar-se, ou de outra maneira realizar alguma ação. Ao ler a partir desses registradores, o sistema operacional pode descobrir qual é o estado do dispositivo, se ele está preparado para aceitar um novo comando e assim por diante.
Além dos registradores de controle, muitos dispositivos têm um buffer de dados a partir do qual o sistema operacional pode ler e escrever. Por exemplo, uma maneira comum para os computadores exibirem pixels na tela é ter uma RAM de vídeo, que é basicamente apenas um buffer de dados, disponível para ser escrita pelos programas ou sistema operacional.
A questão que surge então é como a CPU se comunica com os registradores de controle e também com os buffers de dados do dispositivo. Existem duas alternativas. Na primeira abordagem, para cada registrador de controle é designado um número de porta de E/S, um inteiro de 8 ou 16 bits. O conjunto de todas as portas de E/S formam o espaço de E/S, que é protegido de maneira que programas de usuário comuns não consigam acessá-lo (apenas o sistema operacional). Usando uma instrução de E/S especial como
IN REG,PORT,
a CPU pode ler o registrador de controle PORT e armazenar o resultado no registrador de CPU REG. Similarmente, usando
OUT PORT,REG
a CPU pode escrever o conteúdo de REG para um controlador de registro. A maioria dos primeiros computadores, incluindo quase todos os de grande porte, como o IBM 360 e todos os seus sucessores, funcionava dessa maneira.
Nesse esquema, os espaços de endereçamento para memória e E/S são diferentes, como mostrado na figura “Espaço de memória (a)”. As instruções
IN R0,4
e
MOV R0,4
são completamente diferentes nesse projeto. A primeira lê o conteúdo da porta de E/S 4 e o coloca em R0, enquanto a segunda lê o conteúdo da palavra de memória 4 e o coloca em R0. Os 4 nesses exemplos referem-se a espaços de endereçamento diferentes e não relacionados.
A segunda abordagem, introduzida com o PDP-11, é mapear todos os registradores de controle no espaço da memória, como mostrado na figura “Espaço de memória (b)”. Para cada registrador de controle é designado um endereço de memória único para o qual nenhuma memória é designada. Esse sistema é chamado de E/S mapeada na memória. Na maioria dos sistemas, os endereços designados estão no — ou próximos do — topo do espaço de endereçamento. Um esquema híbrido, com buffers de dados de E/S mapeados na memória e portas de E/S separadas para os registradores de controle, é mostrado na figura “Espaço de memória (c)”. O x86 usa essa arquitetura, com endereços de 640K a 1M − 1 sendo reservados para buffers de dados de dispositivos em PCs compatíveis com a IBM, além de portas de E/S de 0 a 64K – 1.
Como esses esquemas realmente funcionam na prática? Em todos os casos, quando a CPU quer ler uma palavra, seja da memória ou de uma porta de E/S, ela coloca o endereço de que precisa nas linhas de endereçamento do barramento e então emite um sinal READ sobre uma linha de controle do barramento. Uma segunda linha de sinal é usada para dizer se o espaço de E/S ou o espaço de memória é necessário. Se for o espaço de memória, a memória responde ao pedido. Se for o espaço de E/S, o dispositivo de E/S responde ao pedido. Se houver apenas espaço de memória [como na figura “Espaço de memória (b)”], cada módulo de memória e cada dispositivo de E/S comparam as linhas de endereços com a faixa de endereços que elas servem. Se o endereço cair na sua faixa, ela responde ao pedido. Tendo em vista que nenhum endereço jamais é designado tanto à memória quanto a um dispositivo de E/S, não há ambiguidade ou conflito.
Esses dois esquemas de endereçamento dos controladores têm diferentes pontos fortes e fracos. Vamos começar com as vantagens da E/S mapeada na memória. Primeiro, se as instruções de E/S especiais são necessárias para ler e escrever os registradores de controle do dispositivo, acessá-los exige o uso de código de montagem, já que não há como executar uma instrução IN ou OUT em C ou C++. Uma chamada a esse procedimento acarreta um custo adicional ao controle de E/S. Por outro lado, com a E/S mapeada na memória, os registradores de controle do dispositivo são apenas variáveis na memória e podem ser endereçados em C da mesma maneira que quaisquer outras variáveis. Desse modo, com a E/S mapeada na memória, um driver do dispositivo de E/S pode ser escrito inteiramente em C. Sem a E/S mapeada na memória, é necessário algum código em linguagem de montagem.
Segundo, com a E/S mapeada na memória, nenhum mecanismo de proteção especial é necessário para evitar que processos do usuário realizem E/S. Tudo o que o sistema operacional precisa fazer é deixar de colocar aquela porção do espaço de endereçamento contendo os registros de controle no espaço de endereçamento virtual de qualquer usuário. Melhor ainda, se cada dispositivo tem os seus registradores de controle em uma página diferente do espaço de endereçamento, o sistema operacional pode dar a um usuário controle sobre dispositivos específicos, mas não dar a outros, ao simplesmente incluir as páginas desejadas em sua tabela de páginas. Esse esquema pode permitir que diferentes drivers de dispositivos sejam colocados em diferentes espaços de endereçamento, não apenas reduzindo o tamanho do núcleo, mas também impedindo que um driver interfira nos outros.
Terceiro, com a E/S mapeada na memória, cada instrução capaz de referenciar a memória também referência os registradores de controle. Por exemplo, se houver uma instrução, TEST, que testa se uma palavra de memória é 0, ela também poderá ser usada para testar se um registrador de controle é 0, o que pode ser o sinal de que o dispositivo está ocioso e pode aceitar um novo comando. O código em linguagem de montagem pode parecer da seguinte maneira:
LOOP:TEST PORT_4 // verifica se a porta 4 e 0
BEQ READY // se for 0, salta para READY
BRANCH LOOP // caso contrário, continua testando
READY:
Se a E/S mapeada na memória não estiver presente, o registrador de controle deve primeiro ser lido na CPU, então testado, exigindo duas instruções em vez de uma. No caso do laço mostrado, uma quarta instrução precisa ser adicionada, atrasando ligeiramente a detecção de ociosidade do dispositivo.
No projeto de computadores, praticamente tudo envolve uma análise de custo-benefício, e este é o caso aqui também. A E/S mapeada na memória também tem suas desvantagens. Primeiro, a maioria dos computadores hoje tem alguma forma de cache para as palavras de memória. O uso de cache para um registrador de controle do dispositivo seria desastroso. Considere o laço em código de montagem dado anteriormente na presença de cache. A primeira referência a PORT_4 o faria ser colocado em cache. Referências subsequentes simplesmente tomariam o valor da cache e nem perguntariam ao dispositivo. Então quando o dispositivo por fim estivesse pronto, o software não teria como descobrir. Em vez disso, o laço entraria em repetição para sempre.
Para evitar essa situação com a E/S mapeada na memória, o hardware tem de ser capaz de desabilitar seletivamente a cache, por exemplo, em um sistema por página. Essa característica acrescenta uma complexidade extra tanto para o hardware, quanto para o sistema operacional, o qual deve gerenciar a cache seletiva.
Segundo se houver apenas um espaço de endereçamento, então todos os módulos de memória e todos os dispositivos de E/S terão de examinar todas as referências de memória para ver quais devem ser respondidas por cada um. Se o computador tiver um único barramento, como na figura “Barramentos(a)”, cada componente poderá olhar para cada endereço diretamente. No entanto, a tendência nos computadores pessoais modernos é ter um barramento de memória de alta velocidade dedicado, como mostrado na figura “Barramentos(b)”. O barramento é feito sob medida para otimizar o desempenho da memória, sem concessões para o bem de dispositivos de E/S lentos. Os sistemas x86 podem ter múltiplos barramentos (memória, PCIe, SCSI e USB).
O problema de ter um barramento de memória separado em máquinas mapeadas na memória é que os dispositivos de E/S não têm como enxergar os endereços de memória quando estes são lançados no barramento da memória, de maneira que eles não têm como responder. Mais uma vez, medidas especiais precisam ser tomadas para fazer que a E/S mapeada na memória funcione em um sistema com múltiplos barramentos. Uma possibilidade pode ser enviar primeiro todas as referências de memória para a memória. Se esta falhar em responder, então a CPU tenta outros barramentos. Esse projeto pode se tornar exequível, mas ele exige uma complexidade adicional do hardware.
Um segundo projeto possível é colocar um dispositivo de escuta no barramento de memória para passar todos os endereços apresentados para os dispositivos de E/S potencialmente interessados. O problema aqui é que os dispositivos de E/S podem não ser capazes de processar pedidos na mesma velocidade da memória.
Um terceiro projeto possível, é filtrar endereços no controlador de memória. Nesse caso, o chip controlador de memória contém registradores de faixa que são pré-carregados no momento da inicialização. Por exemplo, de 640K a 1M − 1 poderia ser marcado como uma faixa de endereços reservada não utilizável como memória. Endereços que caem dentro dessas faixas marcadas são transferidos para dispositivos em vez da memória. A desvantagem desse esquema é a necessidade de descobrir no momento da inicialização quais endereços de memória são realmente endereços de memória. Desse modo, cada esquema tem argumentos favoráveis e contrários, de maneira que concessões e avaliações de custo-benefício são inevitáveis.
Acesso direto à memória (DMA)
Não importa se uma CPU tem ou não E/S mapeada na memória, ela precisa endereçar os controladores dos dispositivos para poder trocar dados com eles. A CPU pode requisitar dados de um controlador de E/S um byte de cada vez, mas fazê-lo desperdiça o tempo da CPU, de maneira que um esquema diferente, chamado de acesso direto à memória (Direct Memory Access — DMA) é usado muitas vezes. Para simplificar a explicação, presumimos que a CPU acessa todos os dispositivos e memória mediante um único barramento de sistema que conecta a CPU, a memória e os dispositivos de E/S, como mostrado na figura “Operação de transferência utilizando DMA”. Já sabemos que a organização real em sistemas modernos é mais complicada, mas todos os princípios são os mesmos. O sistema operacional pode usar somente DMA se o hardware tiver um controlador de DMA, o que a maioria dos sistemas tem. Às vezes esse controlador é integrado em controladores de disco e outros, mas um projeto desses exige um controlador de DMA separado para cada dispositivo. Com mais frequência, um único controlador de DMA está disponível (por exemplo, na placa-mãe) a fim de controlar as transferências para múltiplos dispositivos, muitas vezes simultaneamente.
Não importa onde esteja localizado fisicamente, o controlador de DMA tem acesso ao barramento do sistema independente da CPU, como mostrado na figura “Operação de transferência utilizando DMA. Ele contém vários registradores que podem ser escritos e lidos pela CPU. Esses incluem um registrador de endereço de memória, um registrador contador de bytes e um ou mais registradores de controle. Os registradores de controle especificam a porta de E/S a ser usada, a direção da transferência (leitura do dispositivo de E/S ou escrita para o dispositivo de E/S), a unidade de transferência (um byte por vez ou palavra por vez) e o número de bytes a ser transferido em um surto.
Para explicar como o DMA funciona, vamos examinar primeiro como ocorre uma leitura de disco quando o DMA não é usado. Primeiro o controlador de disco lê o bloco (um ou mais setores) do dispositivo serialmente, bit por bit, até que o bloco inteiro esteja no buffer interno do controlador. Em seguida, ele calcula a soma de verificação para verificar que nenhum erro de leitura tenha ocorrido. Então o controlador causa uma interrupção. Quando o sistema operacional começa a ser executado, ele pode ler o bloco de disco do buffer do controlador um byte ou uma palavra de cada vez executando um laço, com cada iteração lendo um byte ou palavra de um registrador do controlador e armazenando-a na memória principal.
Quando o DMA é usado, o procedimento é diferente. Primeiro a CPU programa o controlador de DMA configurando seus registradores para que ele saiba o que transferir para onde (passo 1 na figura “Operação de transferência utilizando DMA”). Ela também emite um comando para o controlador de disco dizendo para ele ler os dados do disco para o seu buffer interno e verificar a soma de verificação. Quando os dados que estão no buffer do controlador de disco são válidos, o DMA pode começar.
O controlador de DMA inicia a transferência emitindo uma solicitação de leitura via barramento para o controlador de disco (passo 2). Essa solicitação de leitura se parece com qualquer outra, e o controlador de disco não sabe (ou se importa) se ela veio da CPU ou de um controlador de DMA. Tipicamente, o endereço de memória para onde escrever está nas linhas de endereçamento do barramento, então quando o controlador de disco busca a palavra seguinte do seu buffer interno, ele sabe onde escrevê-la. A escrita na memória é outro ciclo de barramento-padrão (passo 3). Quando a escrita está completa, o controlador de disco envia um sinal de confirmação para o controlador de DMA, também via barramento (passo 4). O controlador de DMA então incrementa o endereço de memória e diminui o contador de bytes. Se o contador de bytes ainda for maior do que 0, os passos 2 até 4 são repetidos até que o contador chegue a 0. Nesse momento, o controlador de DMA interrompe a CPU para deixá-la ciente de que a transferência está completa agora. Quando o sistema operacional é inicializado, ele não precisa copiar o bloco de disco para a memória, pois ele já está lá. Controladores de DMA variam consideravelmente em sofisticação. Os mais simples lidam com uma transferência de cada vez, como acabamos de descrever. Os mais complexos podem ser programados para lidar com múltiplas transferências ao mesmo tempo. Esses controladores têm múltiplos conjuntos de registradores internamente, um para cada canal. A CPU inicializa carregando cada conjunto de registradores com os parâmetros relevantes para sua transferência. Cada transferência deve usar um controlador de dispositivos diferente. Após cada palavra ser transferida (passos 2 a 4) na figura “Operação de transferência utilizando DMA”, o controlador de DMA decide qual dispositivo servir em seguida. Ele pode ser configurado para usar um algoritmo de alternância circular (round-robin), ou ter um esquema de prioridade projetado para favorecer alguns dispositivos em detrimento de outros. Múltiplas solicitações para diferentes controladores de dispositivos podem estar pendentes ao mesmo tempo, desde que exista uma maneira clara de identificar separadamente os sinais de confirmação. Por esse motivo, muitas vezes uma linha diferente de confirmação no barramento é usada para cada canal de DMA.
Muitos barramentos podem operar em dois modos: modo uma palavra de cada vez (word-at-a-time mode) e modo bloco. Alguns controladores de DMA também podem operar em ambos os modos. No primeiro, a operação funciona como descrito: o controlador de DMA solicita a transferência de uma palavra e consegue. Se a CPU também quiser o barramento, ela tem de esperar. O mecanismo é chamado de roubo de ciclo, pois o controlador do dispositivo entra furtivamente e rouba um ciclo de barramento ocasional da CPU de vez em quando, atrasando-a ligeiramente. No modo bloco, o controlador de DMA diz para o dispositivo para adquirir o barramento, emitir uma série de transferências, então libera o barramento. Essa forma de operação é chamada de modo de surto (burst). Ela é mais eficiente do que o roubo de ciclo, pois adquirir o barramento leva tempo e múltiplas palavras podem ser transferidas pelo preço de uma aquisição de barramento. A desvantagem do modo de surto é que ele pode bloquear a CPU e outros dispositivos por um período substancial caso um surto longo esteja sendo transferido.
No modelo que estivemos discutindo, também chamado de modo direto (fly-by mode), o controlador do DMA diz para o controlador do dispositivo para transferir os dados diretamente para a memória principal. Um modo alternativo que alguns controladores de DMA usam estabelece que o controlador do dispositivo deve enviar a palavra para o controlador de DMA, que então emite uma segunda solicitação de barramento para escrever a palavra para qualquer que seja o seu destino. Esse esquema exige um ciclo de barramento extra por palavra transferida, mas é mais flexível no sentido de que ele pode também desempenhar cópias dispositivo–para-dispositivo e mesmo cópias memória-para-memória (ao emitir primeiro uma requisição de leitura à memória e então uma requisição de escrita à memória, em endereços diferentes).
A maioria dos controladores de DMA usa endereços físicos de memória para suas transferências. O uso de endereços físicos exige que o sistema operacional converta o endereço virtual do buffer de memória pretendido em um endereço físico e escreva esse endereço físico no registrador de endereço do controlador de DMA. Um esquema alternativo usado em alguns controladores de DMA é em vez disso escrever o próprio endereço virtual no controlador de DMA. Então o controlador de DMA deve usar a unidade de gerenciamento de memória (Memory Management Unit — MMU) para fazer a tradução de endereço virtual para físico. Apenas no caso em que a MMU faz parte da memória (possível, mas raro), em vez de parte da CPU, os endereços virtuais podem ser colocados no barramento.
Mencionamos anteriormente que o disco primeiro lê dados para seu buffer interno antes que o DMA possa ser inicializado. Você pode estar imaginando por que o controlador não armazena simplesmente os bytes na memória principal tão logo ele as recebe do disco. Em outras palavras, por que ele precisa de um buffer interno? Há duas razões. Primeiro, ao realizar armazenamento interno, o controlador de disco pode conferir a soma de verificação antes de começar uma transferência. Se a soma de verificação estiver incorreta, um erro é sinalizado e nenhuma transferência é feita.
A segunda razão é que uma vez inicializada uma transferência de disco os bits continuam chegando do disco a uma taxa constante, não importa se o controlador estiver pronto para eles ou não. Se o controlador tentasse escrever dados diretamente na memória, ele teria de acessar o barramento do sistema para cada palavra transferida. Se o barramento estivesse ocupado por algum outro dispositivo usando-o (por exemplo, no modo surto), o controlador teria de esperar. Se a próxima palavra de disco chegasse antes que a anterior tivesse sido armazenada, o controlador teria de armazená-la em outro lugar. Se o barramento estivesse muito ocupado, o controlador poderia terminar armazenando um número considerável de palavras e tendo bastante gerenciamento para fazer também. Quando o bloco é armazenado internamente, o barramento não se faz necessário até que o DMA comece; portanto, o projeto do controlador é muito mais simples, pois utilizando DMA o momento de transferência para a memória não é um fator crítico. (Alguns controladores mais antigos iam, na realidade, diretamente para a memória com apenas uma pequena quantidade de armazenamento interno, mas quando o barramento estava muito ocupado, uma transferência talvez tivesse de ser terminada com um erro de transbordo de pilha.)
Nem todos os computadores usam DMA. O argumento contra ele é que a CPU principal muitas vezes é muito mais rápida do que o controlador de DMA e pode fazer o trabalho muito mais rápido (quando o fator limitante não é a velocidade do dispositivo de E/S). Se não há outro trabalho para ela realizar, fazer a CPU (rápida) esperar pelo controlador de DMA (lento) terminar, não faz sentido. Também, livrar-se do controlador de DMA e ter a CPU realizando todo o trabalho via software economiza dinheiro, algo importante em computadores de baixo custo (embarcados).
Interrupções revisitadas
Introduzimos brevemente as interrupções anteriormente, mas há mais a ser dito. Em um sistema típico de computador pessoal, a estrutura de interrupção é como a mostrada na figura “Interrupções”. No nível do hardware, as interrupções funcionam como a seguir. Quando um dispositivo de E/S termina o trabalho dado a ele, gera uma interrupção (presumindo que as interrupções tenham sido habilitadas pelo sistema operacional). Ele faz isso enviando um sinal pela linha de barramento à qual está associado. Esse sinal é detectado pelo chip controlador de interrupções na placa-mãe, que então decide o que fazer.
Se nenhuma outra interrupção estiver pendente, o controlador de interrupção processa a interrupção imediatamente. No entanto, se outra interrupção estiver em andamento, ou outro dispositivo tiver feito uma solicitação simultânea em uma linha de requisição de interrupção de maior prioridade no barramento, o dispositivo é simplesmente ignorado naquele momento. Nesse caso, ele continua a gerar um sinal de interrupção no barramento até ser atendido pela CPU.
Para tratar a interrupção, o controlador coloca um número sobre as linhas de endereço especificando qual dispositivo requer atenção e repassa um sinal para interromper a CPU.
O sinal de interrupção faz a CPU parar aquilo que ela está fazendo e começar outra atividade. O número nas linhas de endereço é usado como um índice em uma tabela chamada de vetor de interrupções para buscar um novo contador de programa. Esse contador de programa aponta para o início da rotina de tratamento da interrupção correspondente. Em geral, interrupções de software (traps ou armadilhas) e de hardware usam o mesmo mecanismo desse ponto em diante, muitas vezes compartilhando o mesmo vetor de interrupções. A localização do vetor de interrupções pode ser estabelecida fisicamente na máquina ou estar em qualquer lugar na memória, com um registrador da CPU (carregado pelo sistema operacional) apontando para sua origem.
Logo após o início da execução, a rotina de tratamento da execução reconhece a interrupção escrevendo um determinado valor para uma das portas de E/S do controlador de interrupção. Esse reconhecimento diz ao controlador que ele está livre para gerar outra interrupção. Ao fazer a CPU atrasar esse reconhecimento até que ela esteja pronta para lidar com a próxima interrupção, podem ser evitadas condições de corrida envolvendo múltiplas (quase simultâneas) interrupções. Como nota, alguns computadores (mais velhos) não têm um controlador de interrupções centralizado, de maneira que cada controlador de dispositivo solicita as suas próprias interrupções.
O hardware sempre armazena determinadas informações antes de iniciar o procedimento de serviço. Quais informações e onde elas são armazenadas varia muito de CPU para CPU. No mínimo, o contador do programa deve ser salvo, de maneira que o processo interrompido possa ser reiniciado. No outro extremo, todos os registradores visíveis e muitos registradores internos podem ser salvos também.
Uma questão é onde salvar essas informações. Uma opção é colocá-las nos registradores internos que o sistema operacional pode ler conforme a necessidade. Um problema com essa abordagem é que então o controlador de interrupções não pode ser reconhecido até que todas as informações potencialmente relevantes tenham sido lidas, a fim de que uma segunda informação não sobreponha os registradores internos durante o salvamento. Essa estratégia leva a longos períodos desperdiçados quando as interrupções são desabilitadas e possivelmente a interrupções e dados perdidos.
Em consequência, a maioria das CPUs salva as informações em uma pilha. No entanto, essa abordagem também tem problemas. Para começo de conversa, de quem é a pilha? Se a pilha atual for usada, ela pode muito bem ser uma pilha do processo do usuário. O ponteiro da pilha pode não ter legitimidade, o que causaria um erro fatal quando o hardware tentasse escrever algumas palavras no endereço apontado. Também, ele poderia apontar para o fim de uma página. Após várias escritas na memória, o limite da página pode ser excedido e uma falta de página gerada. A ocorrência de uma falta de página durante o processamento de uma interrupção de hardware cria um problema maior: onde salvar o estado para tratar a falta de página?
Se for usada a pilha de núcleo, há uma chance muito maior de o ponteiro de pilha ser legítimo e estar apontando para uma página na memória. No entanto, o chaveamento para o modo núcleo pode requerer a troca de contextos da MMU e provavelmente invalidará a maior parte da cache — ou toda ela — e a tabela de tradução de endereços (translation look aside table — TLB). A recarga de toda essa informação, estática ou dinamicamente, aumenta o tempo para processar uma interrupção e, desse modo, desperdiça tempo de CPU.
Princípios do software de E/S
Vamos agora deixar de lado por enquanto o hardware de E/S e examinar o software de E/S. Primeiro analisaremos suas metas e então as diferentes maneiras que a E/S pode ser feita do ponto de vista do sistema operacional.
Objetivos do software de E/S
Um conceito fundamental no projeto de software de E/S é conhecido como independência de dispositivo. O que isso significa é que devemos ser capazes de escrever programas que podem acessar qualquer dispositivo de E/S sem ter de especificá-lo antecipadamente. Por exemplo, um programa que lê um arquivo como entrada deve ser capaz de ler um arquivo em um disco rígido, um DVD ou em um pen-drive sem ter de ser modificado para cada dispositivo diferente. Similarmente, deveria ser possível digitar um comando como
sort <input >output
que trabalhe com uma entrada vinda de qualquer tipo de disco ou teclado e a saída indo para qualquer tipo de disco ou tela. Fica a cargo do sistema operacional cuidar dos problemas causados pelo fato de que esses dispositivos são realmente diferentes e exigem sequências de comando muito diferentes para ler ou escrever.
Um objetivo muito relacionado com a independência do dispositivo é a nomeação uniforme. O nome de um arquivo ou um dispositivo deve simplesmente ser uma cadeia de caracteres ou um número inteiro e não depender do dispositivo de maneira alguma. No UNIX, todos os discos podem ser integrados na hierarquia do sistema de arquivos de maneiras arbitrárias, então o usuário não precisa estar ciente de qual nome corresponde a qual dispositivo. Por exemplo, um pen-drive pode ser montado em cima do diretório /usr/ast/backup de maneira que, ao copiar um arquivo para /usr/ast/backup/Monday, você copia o arquivo para o pen-drive. Assim, todos os arquivos e dispositivos são endereçados da mesma maneira: por um nome de caminho.
Outra questão importante para o software de E/S é o tratamento de erros. Em geral, erros devem ser tratados o mais próximo possível do hardware. Se o controlador descobre um erro de leitura, ele deve tentar corrigi-lo se puder. Se ele não puder, então o driver do dispositivo deverá lidar com ele, talvez simplesmente tentando ler o bloco novamente. Muitos erros são transitórios, como erros de leitura causados por grãos de poeira no cabeçote de leitura, e muitas vezes desaparecerão se a operação for repetida. Apenas se as camadas mais baixas não forem capazes de lidar com o problema as camadas superiores devem ser informadas a respeito. Em muitos casos, a recuperação de erros pode ser feita de modo transparente em um nível baixo sem que os níveis superiores sequer tomem conhecimento do erro.
Ainda outra questão importante é a das transferências síncronas (bloqueantes) versus assíncronas (orientadas à interrupção). A maioria das E/S físicas são assíncronas — a CPU inicializa a transferência e vai fazer outra coisa até a chegada da interrupção. Programas do usuário são muito mais fáceis de escrever se as operações de E/S forem bloqueantes — após uma chamada de sistema read , o programa é automaticamente suspenso até que os dados estejam disponíveis no buffer. Fica a cargo do sistema operacional fazer operações que são realmente orientadas à interrupção parecerem bloqueantes para os programas do usuário. No entanto, algumas aplicações de muito alto desempenho precisam controlar todos os detalhes da E/S, então alguns sistemas operacionais disponibilizam a E/S assíncrona para si.
Outra questão para o software de E/S é a utilização de buffer. Muitas vezes, dados provenientes de um dispositivo não podem ser armazenados diretamente em seu destino final. Por exemplo, quando um pacote chega da rede, o sistema operacional não sabe onde armazená-lo definitivamente até que o tenha colocado em algum lugar para examiná-lo. Também, alguns dispositivos têm severas restrições de tempo real (por exemplo, dispositivos de áudio digitais), portanto os dados devem ser colocados antecipadamente em um buffer de saída para separar a taxa na qual o buffer é preenchido da taxa na qual ele é esvaziado, a fim de evitar seu completo esvaziamento. A utilização do buffer envolve consideráveis operações de cópia e muitas vezes tem um impacto importante sobre o desempenho de E/S.
O conceito final que mencionaremos aqui é o de dispositivos compartilhados versus dedicados. Alguns dispositivos de E/S, como discos, podem ser usados por muitos usuários ao mesmo tempo. Nenhum problema é causado por múltiplos usuários terem arquivos abertos no mesmo disco ao mesmo tempo. Outros dispositivos, como impressoras, têm de ser dedicados a um único usuário até ele ter concluído sua operação. Então outro usuário pode ter a impressora. Ter dois ou mais usuários escrevendo caracteres de maneira aleatória e intercalada na mesma página definitivamente não funcionará. Introduzir dispositivos dedicados (não compartilhados) também introduz uma série de problemas, como os impasses. Novamente, o sistema operacional deve ser capaz de lidar com ambos os dispositivos — compartilhados e dedicados — de uma maneira que evite problemas.
E/S programada
Há três maneiras fundamentalmente diferentes de realizar E/S. Nesta seção examinaremos a primeira (E/S programada). Examinaremos as outras (E/S orientada à interrupções e E/S usando DMA). A forma mais simples de E/S é ter a CPU realizando todo o trabalho. Esse método é chamado de E/S programada.
É mais simples ilustrar como a E/S programada funciona mediante um exemplo. Considere um processo de usuário que quer imprimir a cadeia de oito caracteres “ABCDEFGH” na impressora por meio de uma interface serial. Telas em pequenos sistemas embutidos funcionam assim às vezes. O software primeiro monta a cadeia de caracteres em um buffer no espaço do usuário, como mostrado na figura “Impressão de caracteres(a)”.
O processo do usuário requisita então a impressora para escrita fazendo uma chamada de sistema para abri-la. Se a impressora estiver atualmente em uso por outro processo, a chamada fracassará e retornará um código de erro ou bloqueará até que a impressora esteja disponível, dependendo do sistema operacional e dos parâmetros da chamada. Uma vez que ele tenha a impressora, o processo do usuário faz uma chamada de sistema dizendo ao sistema operacional para imprimir a cadeia de caracteres na impressora.
O sistema operacional então (normalmente) copia o buffer com a cadeia de caracteres para um vetor — digamos, p — no espaço do núcleo, onde ele é mais facilmente acessado (pois o núcleo talvez tenha de mudar o mapa da memória para acessar o espaço do usuário). Ele então confere para ver se a impressora está disponível no momento. Se não estiver, ele espera até que ela esteja. Tão logo a impressora esteja disponível, o sistema operacional copia o primeiro caractere para o registrador de dados da impressora, nesse exemplo usando a E/S mapeada na memória. Essa ação ativa a impressora. O caractere pode não aparecer ainda porque algumas impressoras armazenam uma linha ou uma página antes de imprimir qualquer coisa. Na figura “Impressão de caracteres(b)”, no entanto, vemos que o primeiro caractere foi impresso e que o sistema marcou o “B” como o próximo caractere a ser impresso.
Tão logo copiado o primeiro caractere para a impressora, o sistema operacional verifica se ela está pronta para aceitar outro. Geralmente, a impressora tem um segundo registrador, que contém seu estado. O ato de escrever para o registrador de dados faz que o estado se torne “indisponível”. Quando o controlador da impressora tiver processado o caractere atual, ele indica a sua disponibilidade marcando algum bit em seu registrador de status ou colocando algum valor nele.
Nesse ponto, o sistema operacional espera que a impressora fique pronta de novo. Quando isso acontece, ele imprime o caractere seguinte, como mostrado na figura “Impressão de caractere(c)”. Esse laço continua até que a cadeia inteira tenha sido impressa. Então o controle retorna para o processo do usuário.
As ações seguidas pelo sistema operacional estão brevemente resumidas na figura “Código da impressão de caractere”. Primeiro os dados são copiados para o núcleo. Então o sistema operacional entra em um laço fechado, enviando um caractere de cada vez para a saída. O aspecto essencial da E/S programada, claramente ilustrado nessa figura, é que, após a saída de um caractere, a CPU continuamente verifica o dispositivo para ver se ele está pronto para aceitar outro. Esse comportamento é muitas vezes chamado de espera ocupada (busy waiting) ou polling.
A E/S programada é simples, mas tem a desvantagem de segurar a CPU o tempo todo até que toda a E/S tenha sido feita. Se o tempo para “imprimir” um caractere for muito curto (pois tudo o que a impressora está fazendo é copiar o novo caractere para um buffer interno), então a espera ocupada estará bem. No entanto, em sistemas mais complexos, em que a CPU tem outros trabalhos a fazer, a espera ocupada será ineficiente, e será necessário um método de E/S melhor.
E/S orientada a interrupções
Agora vamos considerar o caso da impressão em uma impressora que não armazena caracteres, mas imprime cada um à medida que ele chega. Se a impressora puder imprimir, digamos, 100 caracteres/segundo, cada caractere levará 10 ms para imprimir. Isso significa que após cada caractere ter sido escrito no registrador de dados da impressora, a CPU vai permanecer em um laço ocioso por 10 ms esperando a permissão para a saída do próximo caractere. Isso é mais tempo do que o necessário para realizar um chaveamento de contexto e executar algum outro processo durante os 10 ms que de outra maneira seriam desperdiçados.
A maneira de permitir que a CPU faça outra coisa enquanto espera que a impressora fique pronta é usar interrupções. Quando a chamada de sistema para imprimir a cadeia é feita, o buffer é copiado para o espaço do núcleo, como já mostramos, e o primeiro caractere é copiado para a impressora tão logo ela esteja disposta a aceitar um caractere. Nesse ponto, a CPU chama o escalonador e algum outro processo é executado. O processo que solicitou que a cadeia seja impressa é bloqueado até que a cadeia inteira seja impressa. O trabalho feito durante a chamada de sistema é mostrado na figura “Impressão orientada à interrupção(a)”.
Quando a impressora imprimiu o caractere e está preparada para aceitar o próximo, ela gera uma interrupção. Essa interrupção para o processo atual e salva seu estado. Então a rotina de tratamento de interrupção da impressora é executada. Uma versão simples desse código é mostrada na figura “Impressão orientada à interrupção(b)”. Se não houver mais caracteres a serem impressos, o tratador de interrupção excuta alguma ação para desbloquear o usuário. Caso contrário, ele sai com o caractere seguinte, reconhece a interrupção e retorna ao processo que estava sendo executado um momento antes da interrupção, o qual continua a partir do ponto em que ele parou.
E/S usando DMA
Uma desvantagem óbvia do mecanismo de E/S orientado a interrupções é que uma interrupção ocorre em cada caractere. Interrupções levam tempo; portanto, esse esquema desperdiça certa quantidade de tempo da CPU. Uma solução é usar o acesso direto à memória (DMA). Aqui a ideia é deixar que o controlador de DMA alimente os caracteres para a impressora um de cada vez, sem que a CPU seja incomodada. Na essência, o DMA executa E/S programada, apenas com o controlador do DMA realizando todo o trabalho, em vez da CPU principal. Essa estratégia exige um hardware especial (o controlador de DMA), mas libera a CPU durante a E/S para fazer outros trabalhos. Uma linha geral do código é dada na figura “Impressão usando DMA”.
A grande vantagem do DMA é reduzir o número de interrupções de uma por caractere para uma por buffer impresso. Se houver muitos caracteres e as interrupções forem lentas, esse sistema poderá representar uma melhoria importante. Por outro lado, o controlador de DMA normalmente é muito mais lento do que a CPU principal. Se o controlador de DMA não é capaz de dirigir o dispositivo em velocidade máxima, ou a CPU normalmente não tem nada para fazer de qualquer forma enquanto esperando pela interrupção do DMA, então a E/S orientada à interrupção ou mesmo a E/S programada podem ser melhores. Na maioria das vezes, no entanto, o DMA vale a pena.
Camadas do software de E/S
O software de E/S costuma ser organizado em quatro camadas, como mostrado na figura “E/S em camadas”. Cada camada tem uma função bem definida a desempenhar e uma interface bem definida para as camadas adjacentes. A funcionalidade e as interfaces diferem de sistema para sistema; portanto, a discussão que se segue, que examina todas as camadas começando de baixo, não é específica para uma máquina.
Discos
Agora começaremos a estudar alguns dispositivos de E/S reais. Começaremos com os discos, que são conceitualmente simples, mas muito importantes.
Hardware do disco
Existe uma série de tipos de discos. Os mais comuns são os discos rígidos magnéticos. Eles se caracterizam pelo fato de que leituras e escritas são igualmente rápidas, o que os torna adequados como memória secundária (paginação, sistemas de arquivos etc.). Arranjos desses discos são usados às vezes para fornecer um armazenamento altamente confiável. Para distribuição de programas, dados e filmes, discos ópticos (DVDs e Blu-ray) também são importantes. Por fim, discos de estado sólido são cada dia mais populares à medida que eles são rápidos e não contêm partes móveis.
Discos magnéticos
Discos magnéticos são organizados em cilindros, cada um contendo tantas trilhas quanto for o número de cabeçotes dispostos verticalmente. As trilhas são divididas em setores, com o número de setores em torno da circunferência sendo tipicamente 8 a 32 nos discos flexíveis e até várias centenas nos discos rígidos. O número de cabeçotes varia de 1 a cerca de 16.
Discos mais antigos têm pouca eletrônica e transmitem somente um fluxo de bits serial simples. Nesses discos, o controlador faz a maior parte do trabalho. Nos outros discos, em particular nos discos IDE (Integrated Drive Electronics — eletrônica integrada ao disco) e SATA (Serial ATA — ATA serial), a própria unidade contém um microcontrolador que realiza um trabalho considerável e permite que o controlador real emita um conjunto de comandos de nível mais elevado. O controlador muitas vezes controla a cache, faz o remapeamento de blocos defeituosos e muito mais.
Uma característica do dispositivo que tem implicações importantes para o driver do disco é a possibilidade de um controlador realizar buscas em duas ou mais unidades ao mesmo tempo. Elas são conhecidas como buscas sobrepostas (overlapped seeks). Enquanto o controlador e o software estão esperando que uma busca seja concluída em uma unidade, o controlador pode iniciar uma busca em outra. Muitos controladores também podem ler ou escrever em uma unidade enquanto realizam uma busca em uma ou mais unidades, mas um controlador de disco flexível não pode ler ou escrever em duas unidades ao mesmo tempo. (A leitura e a escrita exigem que o controlador mova bits em uma escala de tempo de microssegundos, então uma transferência usa quase todo o seu poder de computação.) A situação é diferente para discos rígidos com controladores integrados e, em um sistema com mais de um desses discos rígidos, eles podem operar simultaneamente, pelo menos até o ponto de transferência entre o disco e o buffer de memória do controlador. No entanto, apenas uma transferência entre o controlador e a memória principal é possível ao mesmo tempo. A capacidade de desempenhar duas ou mais operações ao mesmo tempo pode reduzir o tempo de acesso médio consideravelmente.
A figura “Comparação” compara parâmetros da mídia de armazenamento padrão para o PC IBM original com parâmetros de um disco feito três décadas mais tarde a fim de mostrar como os discos evoluíram de lá para cá. É interessante observar que nem todos os parâmetros tiveram a mesma evolução. O tempo médio de busca é quase 9 vezes melhor do que era, a taxa de transferência é 16 mil vezes melhor, enquanto a capacidade aumentou por um fator de 800 mil vezes. Esse padrão tem a ver com as melhorias relativamente graduais nas partes móveis, mas muito mais significativas nas densidades de bits das superfícies de gravação.
Um detalhe para o qual precisamos atentar ao examinarmos as especificações dos discos rígidos modernos é que a geometria especificada, e usada pelo driver, é quase sempre diferente do formato físico. Em discos antigos, o número de setores por trilha era o mesmo para todos os cilindros. Discos modernos são divididos em zonas com mais setores nas zonas externas do que nas internas. A figura “Geometria física(a)” ilustra um disco pequeno com duas zonas. A zona externa tem 32 setores por trilha; a interna tem 16 setores por trilha. Um disco real, como o WD 3000 HLFS, costuma ter 16 ou mais zonas, com o número de setores aumentando em aproximadamente 4% por zona à medida que se vai da zona mais interna para a mais externa.
Para esconder os detalhes de quantos setores tem cada trilha, a maioria dos discos modernos tem uma geometria virtual que é apresentada ao sistema operacional. O software é instruído a agir como se houvesse x cilindros, y cabeçotes e z setores por trilha. O controlador então realiza um remapeamento de uma solicitação para (x, y, z) no cilindro, cabeçote e setor real. Uma geometria virtual possível para o disco físico da figura “Geometria física(a)” é mostrada na figura “Geometria física(b)”. Em ambos os casos o disco tem 192 setores, apenas o arranjo publicado é diferente do real.
Para os PCs, os valores máximos para esses três parâmetros são muitas vezes (65535, 16 e 63), pela necessidade de eles continuarem compatíveis com as limitações do PC IBM original. Nessa máquina, campos de 16, 4 e 6 bits foram usados para especificar tais números, com cilindros e setores numerados começando em 1 e cabeçotes numerados começando em 0. Com esses parâmetros e 512 bytes por setor, o maior disco possível é 31,5 GB. Para contornar esse limite, todos os discos modernos aceitam um sistema chamado endereçamento lógico de bloco (logical block addressing), no qual os setores do disco são numerados consecutivamente começando em 0, sem levar em consideração a geometria do disco.
Formatação de disco
Um disco rígido consiste em uma pilha de pratos de alumínio, liga metálica ou vidro, em geral com 8,9 cm de diâmetro (ou 6,35 cm em notebooks). Em cada prato é depositada uma fina camada de um óxido de metal magnetizado. Após a fabricação, não há informação alguma no disco.
Antes que o disco possa ser usado, cada prato deve passar por uma formatação de baixo nível feita por software. A formatação consiste em uma série de trilhas concêntricas, cada uma contendo uma série de setores, com pequenos intervalos entre eles. O formato de um setor é mostrado na figura “Um setor de disco”.
O preâmbulo começa com um determinado padrão de bits que permite que o hardware reconheça o começo do setor. Ele também contém os números do cilindro e setor, assim como outras informações. O tamanho da porção de dados é determinado pelo programa de formatação de baixo nível. A maioria dos discos usa setores de 512 bytes. O campo ECC contém informações redundantes que podem ser usadas para a recuperação de erros de leitura. O tamanho e o conteúdo desse campo variam de fabricante para fabricante, dependendo de quanto espaço de disco o projetista está disposto a abrir mão em prol de uma maior confiabilidade, assim como o grau de complexidade do código de ECC que o controlador é capaz de manejar. Um campo de ECC de 16 bytes não é incomum. Além disso, todos os discos rígidos têm algum número de setores sobressalentes alocados para serem usados para substituir setores com defeito de fabricação.
A posição do setor 0 em cada trilha é deslocada com relação à trilha anterior quando a formatação de baixo nível é realizada. Esse deslocamento, chamado de deslocamento de cilindro (cylinder skew), é feito para melhorar o desempenho. A ideia é permitir que o disco leia múltiplas trilhas em uma operação contínua sem perder dados. A natureza do problema pode ser vista examinando-se a figura “Geometria física(a)”. Suponha que uma solicitação precise de 18 setores começando no setor 0 da trilha mais interna. A leitura dos primeiros 16 setores leva a uma rotação de disco, mas uma busca é necessária para mover o cabeçote de leitura/gravação para a trilha seguinte, mais externa, no setor 17. No momento em que o cabeçote se deslocou uma trilha, o setor 0 já passou
por ele, então uma rotação inteira é necessária até que ele volte novamente. Esse problema é eliminado deslocando-se os setores como mostrado na figura “Deslocamento de cilindro”.
A intensidade de deslocamento de cilindro depende da geometria do disco. Por exemplo, um disco de 10.000 RPM (rotações por minuto) leva 6 ms para realizar uma rotação completa. Se uma trilha contém 300 setores, um novo setor passa sob o cabeçote a cada 20 μs. Se o tempo de busca de uma trilha para outra for 800 μs, 40 setores passarão durante a busca, então o deslocamento de cilindro deve ter ao menos 40 setores, em vez dos três mostrados na figura “Deslocamento de cilindro”. Vale a pena observar que o chaveamento entre cabeçotes também leva um tempo finito; portanto, existe também um deslocamento de cabeçote assim como um de cilindro, mas o deslocamento de cabeçote não é muito grande, normalmente muito menor do que um tempo de setor.
Como resultado da formatação de baixo nível, a capacidade do disco é reduzida, dependendo dos tamanhos do preâmbulo, do intervalo entre setores e do ECC, assim como o número de setores sobressalentes reservado. Muitas vezes a capacidade formatada é 20% mais baixa do que a não formatada. Os setores sobressalentes não contam para a capacidade formatada; então, todos os discos de um determinado tipo têm exatamente a mesma capacidade quando enviados, independentemente de quantos setores defeituosos eles de fato têm (se o número de setores defeituosos exceder o de sobressalentes, o disco será rejeitado e não enviado).
Há uma confusão considerável a respeito da capacidade de disco porque alguns fabricantes anunciavam a capacidade não formatada para fazer seus discos parecerem maiores do que eles eram na realidade. Por exemplo, vamos considerar um disco cuja capacidade não formatada é de 200 × 109 bytes. Ele poderia ser vendido como um disco de 200 GB. No entanto, após a formatação, possivelmente apenas 170 × 109 bytes estavam disponíveis para dados. Para aumentar a confusão, o sistema operacional provavelmente relatará essa capacidade como sendo 158 GB, não 170 GB, pois o software considera uma memória de 1 GB como sendo 230 (1.073.741.824) bytes, não 109 (1.000.000.000) bytes. Seria melhor se isso fosse relatado como 158 GiB.
Para piorar ainda mais as coisas, no mundo das comunicações de dados, 1 Gbps significa 1 bilhão de bits/s porque o prefixo giga realmente significa 109 (um quilômetro tem 1.000 metros, não 1.024 metros, afinal de contas). Somente para os tamanhos de memória e de disco que as medidas quilo, mega, giga e tera significam 210, 220, 230 e 240, respectivamente.
Para evitar confusão, alguns autores usam os prefixos quilo, mega, giga e tera para significar 103, 106, 109 e 1012, respectivamente, enquanto usando kibi, mebi, gibi e tebi para significar 210, 220, 230 e 240, respectivamente. No entanto, o uso dos prefixos “b” é relativamente raro. Apenas caso você goste de números realmente grandes, os prefixos após tebi são pebi, exbi, zebi e yobi, então
um yobibyte representa uma quantidade considerável de bytes (280 para ser preciso).
A formatação também afeta o desempenho. Se um disco de 10.000 RPM tem 300 setores por trilha de 512 bytes cada, ele leva 6 ms para ler os 153.600 bytes em uma trilha para uma taxa de dados de 25.600.000 bytes/s ou 24,4 MB/s. Não é possível ir mais rápido do que isso, não importa o tipo de interface que esteja presente, mesmo que seja uma interface SCSI a 80 MB/s ou 160 MB/s.
Na realidade, ler continuamente com essa taxa exige um buffer grande no controlador. Considere, por exemplo, um controlador com um buffer de um setor que tenha recebido um comando para ler dois setores consecutivos. Após ler o primeiro setor do disco e realizar o cálculo ECC, os dados precisam ser transferidos para a memória principal. Enquanto essa transferência está sendo feita, o setor seguinte passará pelo cabeçote. Quando uma cópia para a memória for concluída, o controlador terá de esperar quase o tempo de uma rotação inteira para que o segundo setor dê a volta novamente.
Esse problema pode ser eliminado numerando os setores de maneira entrelaçada quando se formata o disco. Na figura “Formatação entrelaçada(a)”, vemos o padrão de numeração usual (ignorando o deslocamento de cilindro aqui). Na figura “Formatação entrelaçada(b)”, observa-se um entrelaçamento simples (single interleaving), que dá ao controlador algum descanso entre os setores consecutivos a fim de copiar o buffer para a memória principal.
Se o processo de cópia for muito lento, o entrelaçamento duplo da figura “Formatação entrelaçada(c)” poderá ser necessário. Se o controlador tem um buffer de apenas um setor, não importa se a cópia do buffer para a memória principal é feita pelo controlador, a CPU principal ou um chip DMA; ela ainda leva algum tempo. Para evitar a necessidade do entrelaçamento, o controlador deve ser capaz de armazenar uma trilha inteira. A maioria dos controladores modernos consegue armazenar trilhas inteiras.
Após a formatação de baixo nível ter sido concluída, o disco é dividido em partições. Logicamente, cada partição é como um disco separado. Partições são necessárias para permitir que múltiplos sistemas operacionais coexistam. Também, em alguns casos, uma partição pode ser usada como área de troca (swapping). No x86 e na maioria dos outros computadores, o setor 0 contém o registro mestre de inicialização (Master Boot Record — MBR), que contém um código de inicialização mais a tabela de partição no fim. O MBR, e desse modo o suporte para tabelas de partição, apareceu pela primeira vez nos PCs da IBM em 1983 para dar suporte ao então enorme disco rígido de 10 MB no PC XT. Os discos cresceram um pouco desde então. À medida que as entradas de partição MBR na maioria dos sistemas são limitadas a 32 bits, o tamanho de disco máximo que pode ser suportado pelos setores de 512 B é 2 TB. Por essa razão, a maioria dos sistemas operacionais desde então também suporta o novo GPT (GUID Partition Table), que suporta discos de até 9,4 ZB (9.444.732.965.739.290.426.880 bytes).
A tabela de partição dá o setor de inicialização e o tamanho de cada partição. No x86, a tabela de partição MBR tem espaço para quatro partições. Se todas forem para o Windows, elas serão chamadas C:, D:, E:, e F: e tratadas como discos separados. Se três delas forem para o Windows e uma para o UNIX, então o Windows chamará suas partições C:, D:, e E:. Se o drive USB for acrescentado, ele será o F:. Para ser capaz de inicializar do disco rígido, uma partição deve ser marcada como ativa na tabela de partição.
O passo final na preparação de um disco para ser usado é realizar uma formatação de alto nível de cada partição (separadamente). Essa operação insere um bloco de inicialização, a estrutura de gerenciamento de armazenamento livre (lista de blocos livres ou mapa de bits), diretório-raiz e um sistema de arquivo vazio. Ela também coloca um código na entrada da tabela de partições dizendo qual sistema de arquivos é usado na partição, pois muitos sistemas operacionais suportam múltiplos sistemas de arquivos incompatíveis (por razões históricas). Nesse ponto, o sistema pode ser inicializado.
Quando a energia é ligada, o BIOS entra em execução inicialmente e então carrega o registro mestre de inicialização e salta para ele. Esse programa então confere para ver qual partição está ativa. Então ele carrega o setor de inicialização específico daquela partição e o executa. O setor de inicialização contém um programa pequeno que geralmente carrega um carregador de inicialização maior que busca no sistema de arquivos para encontrar o núcleo do sistema operacional. Esse programa é carregado na memória e executado.
Relógios
Relógios (também chamados de temporizadores — timers) são essenciais para a operação de qualquer sistema multiprogramado por uma série de razões. Eles mantêm a hora do dia e evitam que um processo monopolize a CPU, entre outras coisas. O software do relógio pode assumir a forma de um driver de dispositivo, embora um relógio não seja nem um dispositivo de bloco, como um disco, tampouco um dispositivo de caractere, como um mouse.
Hardware de relógios
Dois tipos de relógios são usados em computadores, e ambos são bastante diferentes dos relógios de parede e de pulso usados pelas pessoas. Os relógios mais simples são ligados à rede elétrica de 110 ou 220 volts e causam uma interrupção a cada ciclo de voltagem, em 50 ou 60 Hz. Esses relógios costumavam dominar o mercado, mas são raros hoje.
O outro tipo de relógio é construído de três componentes: um oscilador de cristal, um contador e um registrador de apoio, como mostrado na figura “Relógio programável”. Quando um fragmento de cristal é cortado adequadamente e montado sob tensão, ele pode ser usado para gerar um sinal periódico de altíssima precisão, em geral na faixa de várias centenas de mega-hertz até alguns giga-hertz, dependendo do cristal escolhido. Usando a eletrônica, esse sinal básico pode ser multiplicado por um inteiro pequeno para conseguir frequências de até vários giga-hertz ou mesmo mais. Pelo menos um circuito desses normalmente é encontrado em qualquer computador, fornecendo um sinal de sincronização para os vários circuitos do computador. Esse sinal é colocado em um contador para fazê-lo contar regressivamente até zero. Quando o contador chega a zero, ele provoca uma interrupção na CPU.
Relógios programáveis tipicamente têm vários modos de operação. No modo disparo único (one-shot mode), quando o relógio é inicializado, ele copia o valor do registrador de apoio no contador e então decrementa o contador em cada pulso do cristal. Quando o contador chega a zero, ele provoca uma interrupção e para até que ele é explicitamente inicializado novamente pelo software. No modo onda quadrada, após atingir o zero e causar a interrupção, o registrador de apoio é automaticamente copiado para o contador, e todo o processo é repetido de novo indefinidamente. Essas interrupções periódicas são chamadas de tiques do relógio.
A vantagem do relógio programável é que a sua frequência de interrupção pode ser controlada pelo software. Se um cristal de 500 MHz for usado, então o contador é pulsado a cada 2 ns. Com registradores de 32 bits (sem sinal), as interrupções podem ser programadas para acontecer em intervalos de 2 ns a 8,6 s. Chips de relógios programáveis costumam conter dois ou três relógios programáveis independentemente e têm muitas outras opções também (por exemplo, contar com incremento em vez de decremento, desabilitar interrupções, e mais).
Para evitar que a hora atual seja perdida quando a energia do computador é desligada, a maioria dos computadores tem um relógio de back-up mantido por uma bateria, implementado com o tipo de circuito de baixo consumo usado em relógios digitais. O relógio de bateria pode ser lido na inicialização. Se o relógio de backup não estiver presente, o software pode pedir ao usuário a data e o horário atuais. Há também uma maneira padrão para um sistema de rede obter o horário atual de um servidor remoto. De qualquer modo, o horário é então traduzido para o número de tiques de relógio desde as 12 horas de 1 o de janeiro de 1970, de acordo com o Tempo Universal Coordenado (Universal Coordinated Time — UTC), antes conhecido como meio-dia de Greenwich, como o UNIX faz, ou de algum outro momento de referência. A origem do tempo para o Windows é o dia 1 o de janeiro de 1980. Em cada tique de relógio, o tempo real é incrementado por uma contagem. Normalmente programas utilitários são fornecidos para ajustar manualmente o relógio do sistema e o relógio de backup e para sincronizar os dois.
Software de relógio
Tudo o que o hardware de relógios faz é gerar interrupções a intervalos conhecidos. Todo o resto envolvendo tempo deve ser feito pelo software, o driver do relógio. As tarefas exatas do driver do relógio variam entre os sistemas operacionais, mas em geral incluem a maioria das ações seguintes:
- Manter o horário do dia.
- Evitar que processos sejam executados por mais tempo do que o permitido.
- Contabilizar o uso da CPU.
- ratar a chamada de sistema alarm feita pelos processos do usuário.
- Fornecer temporizadores watch dog para partes do próprio sistema.
- Gerar perfis de execução, realizar monitoramentos e coletar estatísticas.
A primeira função do relógio — a manutenção da hora do dia (também chamada de tempo real) não é difícil. Ela apenas exige incrementar um contador a cada tique do relógio, como mencionado anteriormente. A única coisa a ser observada é o número de bits no contador da hora do dia. Com uma frequência de relógio de 60 Hz, um contador de 32 bits ultrapassaria sua capacidade em apenas um pouco mais de dois anos.
Claramente, o sistema não consegue armazenar o tempo real como o número de tiques desde 1o de janeiro de 1970 em 32 bits.
Há três maneiras de resolver esse problema. A primeira maneira é usar um contador de 64 bits, embora fazê-lo torna a manutenção do contador mais cara, pois ela precisa ser feita muitas vezes por segundo. A segunda maneira é manter a hora do dia em segundos, em vez de em tiques, usando um contador subsidiário para contar tiques até que um segundo inteiro tenha sido acumulado. Como 232 segundos é mais do que 136 anos, esse método funcionará até o século XXII.
A terceira abordagem é contar os tiques, mas fazê-lo em relação ao momento em que o sistema foi inicializado, em vez de em relação a um momento externo fixo. Quando o relógio de backup é lido ou o usuário digita o tempo real, a hora de inicialização do sistema é calculada a partir do valor da hora do dia atual e armazenada na memória de qualquer maneira conveniente. Mais tarde, quando a hora do dia for pedida, a hora do dia armazenada é adicionada ao contador para se chegar à hora do dia atual. Todas as três abordagens são mostradas na figura “Três maneiras de manter a hora do dia”.
A segunda função do relógio é evitar que os processos sejam executados por um tempo longo demais. Sempre que um processo é iniciado, o escalonador inicializa um contador com o valor do quantum do processo em tiques do relógio. A cada interrupção do relógio, o driver decrementa o contador de quantum em 1. Quando chega a zero, o driver do relógio chama o escalonador para selecionar outro processo.
A terceira função do relógio é contabilizar o uso da CPU. A maneira mais precisa de fazer isso é inicializar um segundo temporizador, distinto do temporizador principal do sistema, sempre que um processo é iniciado. Quando um processo é parado, o temporizador pode ser lido para dizer quanto tempo ele esteve em execução. Para fazer as coisas direito, o segundo temporizador deve ser salvo quando ocorre uma interrupção e restaurado mais tarde.
Uma maneira menos precisa, porém mais simples, para contabilizar o uso da CPU é manter um ponteiro para a entrada da tabela de processos relativa ao processo em execução em uma variável global. A cada tique do relógio, um campo na entrada do processo atual é incrementado. Dessa maneira, cada tique do relógio é “cobrado” do processo em execução no momento do tique. Um problema menor com essa estratégia é que se muitas interrupções ocorrerem durante a execução de um processo, ele ainda será cobrado por um tique completo, mesmo que ele não tenha realizado muito trabalho. A contabilidade apropriada da CPU durante as interrupções é cara demais e feita raramente.
Em muitos sistemas, um processo pode solicitar que o sistema operacional lhe dê um aviso após um determinado intervalo. O aviso normalmente é um sinal, interrupção, mensagem, ou algo similar. Uma aplicação que requer o uso desses avisos é a comunicação em rede, na qual um pacote sem confirmação de recebimento dentro de um determinado intervalo de tempo deve ser retransmitido. Outra aplicação é o ensino auxiliado por computador, onde um estudante que não dê uma resposta dentro de um determinado tempo recebe a resposta do computador.
Se o driver do relógio tivesse relógios o bastante, ele poderia separar um relógio para cada solicitação. Não sendo o caso, ele deve simular múltiplos relógios virtuais com um único relógio físico. Uma maneira é manter uma tabela na qual o tempo do sinal para todos os temporizadores pendentes é mantido, assim como uma variável dando o tempo para o próximo. Sempre que a hora do dia for atualizada, o driver confere para ver se o sinal mais próximo ocorreu. Se ele tiver ocorrido, a tabela é pesquisada para encontrar o próximo sinal a ocorrer.
Se muitos sinais são esperados, é mais eficiente simular múltiplos relógios encadeando juntas todas as solicitações de relógio pendentes, ordenadas no tempo, em uma lista encadeada, como mostra a figura “Múltiplos relógios simulados”. Cada entrada na lista diz quantos tiques do relógio desde o sinal anterior deve-se esperar antes de gerar um novo sinal. Nesse exemplo, os sinais estão pendentes para 4203, 4207, 4213, 4215 e 4216.
Na figura “Múltiplos relógios simulados”, a interrupção seguinte ocorre em 3 tiques. Em cada tique, o “Próximo sinal” é decrementado. Quando ele chega a 0, o sinal correspondente para o primeiro item na lista é gerado, e o item é removido da lista. Então “Próximo sinal” é ajustado para o valor na entrada agora no início da lista, 4 nesse exemplo. Observe que durante uma interrupção de relógio, seu driver tem várias coisas para fazer — incrementar o tempo real, decrementar o quantum e verificar o 0, realizar a contabilidade da CPU e decrementar o contador do alarme. No entanto, cada uma dessas operações foi cuidadosamente arranjada para ser muito rápida, pois elas têm de ser feitas muitas vezes por segundo.
Partes do sistema operacional também precisam estabelecer temporizadores. Eles são chamados de temporizadores watchdog (cão de guarda) e são frequentemente usados (especialmente em dispositivos embarcados) para detectar problemas como travamentos do sistema. Por exemplo, um temporizador watchdog pode reinicializar um sistema que para de executar. Enquanto o sistema estiver executando, ele regularmente reinicia o temporizador, de maneira que ele nunca expira. Nesse caso, a expiração do temporizador prova que o sistema não executou por um longo tempo e leva a uma ação corretiva — como uma reinicialização completa do sistema.
O mecanismo usado pelo driver do relógio para lidar com temporizadores watchdog é o mesmo que para os sinais do usuário. A única diferença é que, quando um temporizador é desligado, em vez de causar um sinal, o driver do relógio chama uma rotina fornecida pelo chamador. A rotina faz parte do código do chamador. A rotina chamada pode fazer o que for necessário, mesmo causar uma interrupção, embora dentro do núcleo as interrupções sejam muitas vezes inconvenientes e sinais não existem. Essa é a razão pela qual o mecanismo do watchdog é fornecido. É importante observar que o mecanismo de watchdog funciona somente quando o driver do relógio e a rotina a ser chamada estão no mesmo espaço de endereçamento.
A última questão em nossa lista é o perfil de execução. Alguns sistemas operacionais fornecem um mecanismo pelo qual um programa do usuário pode obter do sistema um histograma do seu contador do programa, então ele pode ver onde está gastando seu tempo. Quando esse perfil de execução é uma possibilidade, a cada tique o driver confere para ver se o perfil de execução do processo atual está sendo obtido e, se afirmativo, calcula o intervalo (uma faixa de endereços) correspondente ao contador do programa atual. Ele então incrementa esse intervalo por um. Esse mecanismo também pode ser usado para extrair o perfil de execução do próprio sistema.
Resumo
A entrada/saída é um tópico importante, mas muitas vezes negligenciado. Uma fração substancial de qualquer sistema operacional diz respeito à E/S. A E/S pode ser conseguida de três maneiras. Primeiro, há a E/S programada, na qual a CPU principal envia ou recebe cada byte ou palavra e aguarda em um laço estreito esperando até que possa receber ou enviar o próximo byte ou palavra. Segundo, há a E/S orientada à interrupção, na qual a CPU inicia uma transferência de E/S para um caractere ou palavra e vai fazer outra coisa até a interrupção chegar sinalizando a conclusão da E/S. Terceiro, há o DMA, no qual um chip separado gerencia a transferência completa de um bloco de dados, gerando uma interrupção somente quando o bloco inteiro foi transferido.
A E/S pode ser estruturada em quatro níveis: as rotinas de tratamento de interrupção, os drivers de dispositivos, o software de E/S independente do dispositivo, e as bibliotecas de E/S e spoolers que executam no espaço do usuário. Os drivers do dispositivo lidam com os detalhes da execução dos dispositivos e com o fornecimento de interfaces uniformes para o resto do sistema operacional. O software de E/S independente do dispositivo realiza atividades como o armazenamento em buffers e relatórios de erros.
Os discos vêm em uma série de tipos, incluindo discos magnéticos, RAIDS, pen-drives e discos ópticos. Nos discos rotacionais, os algoritmos de escalonamento do braço do disco podem ser usados muitas vezes para melhorar o desempenho do disco, mas a presença de geometrias virtuais complica as coisas. Pareando dois discos, pode ser construído um meio de armazenamento estável com determinadas propriedades úteis.
Relógios são usados para manter um controle do tempo real — limitando o tempo que os processos podem ser executados —, lidar com temporizadores watchdog e contabilizar o uso da CPU.
Terminais orientados por caracteres têm uma série de questões relativas a caracteres especiais que podem ser entrada e sequências de escape especiais que podem ser saída. A entrada pode ser em modo cru ou modo cozido, dependendo de quanto controle o programa quer sobre ela. Sequências de escape na saída controlam o movimento do cursor e permitem a inserção e remoção de texto na tela.
A maioria dos sistemas UNIX usa o Sistema X Window como base de sua interface do usuário. Ele consiste em programas que são ligados a bibliotecas especiais que emitem comandos de desenho e um servidor X que escreve na tela.
Muitos computadores usam GUIs para sua saída. Esses são baseados no paradigma WIMP: janelas, ícones, menus e um dispositivo apontador (Windows, Icons, Menus, Pointing device). Programas baseados em GUIs são geralmente orientados a eventos, com eventos do teclado, mouse e outros sendo enviados para o programa para serem processados tão logo eles acontecem. Em sistemas UNIX, os GUIs quase sempre executam sobre o X.
Clientes magros têm algumas vantagens sobre os PCs padrão, notavelmente por sua simplicidade e menos manutenção para os usuários.
Por fim, o gerenciamento de energia é uma questão fundamental para telefones, tablets e notebooks, pois os tempos de vida das baterias são limitados, e para os computadores de mesa e de servidores devido às contas de luz da organização. Várias técnicas podem ser empregadas pelo sistema operacional para reduzir o consumo de energia. Programas também podem ajudar ao sacrificar alguma qualidade por mais tempo de vida das baterias.
Fim da aula