Capítulo DOZE Software Básico 1 2 . 1 Introdução Emprega-se comumente a palavra software para designar o conjunto de programas que são utilizados com um sistema de hardware para facilitar seu uso por programadores e operadores do sistema. Entretanto, esta utilização do termo exclui programas de aplicações, incluindo apenas programas que fornecem funções gerais independentes dos detalhes de uma aplicação (software do sistema ou software básico). Na prática, o que o usuário enxerga facilmente deste grupo (embora ele seja mais extenso) são as linguagens que ele encontra disponíveis para programação, como Basic, Fortran, Cobol, Pascal, Logo, C, Delphi, Visual C, C++, Java, etc, e programas escritos para resolver problemas específicos ou que realizam funções especiais como planilhas de cálculo, editores de texto, jogos eletrônicos, entre outros, que fazem parte do software de aplicação. Os programas que são escritos em linguagens de programação de alto nível (emprega-se esta denominação como distinção a linguagens mais próximas da máquina) precisam ser convertidos em programas de máquina. O processo desta conversão é executado por um elemento de software denominado de processador de linguagem. Os programas escritos em alto nível tendem a ser independentes da estrutura da máquina na qual serão executados, mas os programas de baixo nível não. Por isso, esta conversão é dependente da máquina sobre a qual vai ser executado o programa. Os processadores de linguagem são programas longos, que ocupam espaço significativo na memória do computador. Por isso, freqüentemente ficam armazenados (residem) em um dispositivo de entrada/saída, como um disco, por exemplo. Este programa será “chamado” e copiado para a memória quando o programador quiser fazer uso dele. O uso de um processador de linguagem significa que o programador vai executá-lo, tendo como entrada de dados o programa escrito em linguagem de alto nível. Como saída, será obtida uma representação do programa em forma diretamente executável pela máquina ou uma forma mais próxima desta do que a representação anterior (em alto nível). A escrita de um programa consiste na especificação, direta ou indireta, de uma seqüência de instruções de máquina para o computador. As instruções de máquina formam um padrão binário com o qual é difícil de trabalhar do ponto de vista humano e complexo para entender. É mais fácil de escrever programas empregando símbolos mais familiares, orientados ao usuário, tais como os caracteres alfa-numéricos e suas combinações. Como conseqüência desta facilidade, torna-se necessário traduzir esta representação simbólica de programas em elementos binários que possam ser reconhecidos pelo hardware. 1 2 . 2 Linguagens de programação Um programa é uma lista de instruções ou comandos que determina ao computador a execução de uma tarefa de processamento de dados. Há diversas linguagens de programação, através das quais pode-se escrever programas para o computador; mas o computador só pode executá-los quando eles estão representados internamente na forma binária, organizado e codificado de forma a resultar na geração de sinais de controle da UCP. 12-1 Portanto, os programas que não estão escritos na forma binária necessitam ser traduzidos antes da sua execução. Os programas podem se incluir em uma das seguintes categorias: • código binário: sequência de instruções e operandos em binário; lista a representação exata das instruções como elas aparecem na memória; • código octal ou hexadecimal: representação, equivalente ao código binário, em representação octal ou hexadecimal; • código simbólico: o usuário emprega símbolos tais como letras, números e caracteres especiais, para representar o código das instruções, composto por parte de operação, endereço e outras. Cada instrução simbólica pode ser traduzida em uma instrução binária equivalente, o que é feito por um programa montador (assembler). Por esta razão, este programa simbólico é referido como programa em linguagem de montagem ou assembler. • linguagens de programação em alto nível: são linguagens especiais desenvolvidas para facilitar a especificação de procedimentos usados na solução de problemas, não havendo preocupação com o comportamento do hardware que suporta estas operações. Portanto, elas empregam símbolos e formatos orientados a problemas, de acordo com o raciocínio comumente empregado na solução destes. Assim, cada comando precisa ser traduzido em uma seqüência de instruções binárias, antes que o programa possa ser executado em um computador. O programa que traduz um programa escrito em linguagem de alto-nível para binário é chamado de compilador. O termo linguagens de programação refere-se em geral ao estudo da estrutura das diversas linguagens de programação de alto nível, sendo realizado de forma independente dos dispositivos computacionais de do seu hardware. Este não é o enfoque para o pessoal de arquitetura, que o utiliza com outra conotação incluindo linguagens a nível de máquina. A seguir será exemplificada esta relação existente entre as diversas linguagens de programação. Esta relação não é estabelecida diretamente a nível de comandos, mas tenta deixar claro o nível crescente de dificuldades de programação e depuração de código, à medida que de vai do alto para o baixo nível de programação. 1 2 . 3 Exemplo com NEANDER Problema: Realizar a multiplicação de dois números inteiros positivos, a e b (a≥0, b≥0), utilizando o método das somas sucessivas, ou seja, somar a consigo mesmo b vezes. Descrição em português estruturado: Sejam a,b,r três números inteiros positivos. Então fazer: 1. Zerar r 2. Enquanto b for maior que zero, somar a em r e decrementar b de uma unidade 3. Fim. O resultado está em r Descrição em pseudo-linguagem de alto nível Begin Program Variable a,b,r: type positive-integer r:=0 while b>0 do r:=r+a b:=b-1 12-2 end while End Program Descrição em pseudo-linguagem, sem uso de while Begin Program Variable a,b,r: type positive-integer Label laço r:=0 laço: if b>0 then r:=r+a b:=b-1 goto laço end if End Program Descrição em pseudo-linguagem, somente com uso de if-goto Begin Program Variable a,b,r: type positive-integer Label laço, fim r:=0 laço: if b=0 goto fim r:=r+a b:=b-1 goto laço fim: End Program Descrição em pseudo-assembler fim: ORG 0 LDA Zero STA r LDA b JZ fim LDA r ADD a STA r LDA b ADD m1 STA b JMP laço HLT a b r zero m1 DEF BYTE DEF BYTE DEF BYTE DEF BYTE=0 DEF BYTE=-1 laço: % define endereço de início do programa % r:=0 % if b=0 goto fim % r:=r+a % b:=b-1 % goto laço (poderia ser JMP laço+1: otimização !) % define variável a % define variável b % define variável r % define variável zero e inicializa com 0 % define variável m1 e inicializa com menos um Descrição na linguagem do NEANDER (sem labels) Endereço 0 2 4 Instrução LDA 26 STA 25 LDA 24 % r:=0 % if b=0 goto fim 12-3 6 8 10 12 14 16 18 20 22 23 24 25 26 27 JN 22 LDA 25 ADD 23 STA 25 LDA 24 ADD 27 STA 24 JMP 4 HLT 0 0 0 0 255 % r:=r+a % b:=b-1 % goto laço % define variável a % define variável b % define variável r % define variável zero e inicializa com 0 % define variável m1 e inicializa com menos um Descrição na linguagem do NEANDER (em decimal) Endereço 0 2 4 6 8 10 12 14 16 18 20 22 23 24 25 26 27 Instrução 32 26 16 25 32 24 160 22 32 25 48 23 16 25 32 24 48 27 16 24 128 4 240 0 0 0 0 255 % r:=0 % if b=0 goto fim % r:=r+a % b:=b-1 % goto laço % define variável a % define variável b % define variável r % define variável zero e inicializa com 0 % define variável m1 e inicializa com menos um 1 2 . 4 Programas a nível de sistema A produção de programas do sistema é complexa, e requer extenso conhecimento e prática em computação. A existência destes programas representa vantagens e conveniência aos programadores de aplicações e aos usuários de computadores em geral. O software do sistema pode ser dividido em várias categorias (algumas citadas anteriormente): • processadores de linguagens, que convertem programas para a linguagem de máquina a partir de linguagens orientadas a usuários ou a aplicações; • biblioteca de programas, que fornecem rotinas padrão para o programador de aplicações; • programas carregadores (ou “loaders”), para facilitar a carga dos diversos programas na memória do computador; • programas utilitários, para facilitar a comunicação entre os componentes do computador e entre o computador e o usuário; • programas de diagnóstico, que facilitam a manutenção do computador; • um sistema operacional, que supervisiona os demais programas e controla sua execução. 12-4 Assemblers Nos primórdios da computação, o programador de computadores tinha a sua disposição uma máquina que, através do hardware, executava certas funções básicas. A programação do computador consistia em escrever uma série de uns e zeros (linguagem de máquina), colocar esta série na memória, pressionar um botão. Com isto o computador iniciava a execução destas instruções. É extremamente difícil, entretanto, escrever e ler programas em linguagem de máquina. Na procura por um método mais conveniente, desenvolveram-se os processadores de linguagem, ou seja, programas que traduzem um programa fonte escrito pelo usuário em um programa objeto que pode ser entendido pelo computador. Um montador ou assembler é um programa do sistema que traduz um programa escrito em linguagem assembler para um programa equivalente descrito em linguagem binária (da máquina). Tipicamente, em uma linguagem assembler utilizam-se mnemônicos (símbolos) para cada instrução de máquina, e a principal função do montador é traduzir cada um destes símbolos no código binário equivalente. Compiladores Com o aumento da complexidade dos programas, mesmo o uso de linguagem assembler não fornece o grau de abstração necessário para uma boa compreensão do programa. Assim, desenvolveram-se as linguagens de alto nível, onde um único comando substitui dezenas de instruções assembler. Um compilador é um programa do sistema que traduz um programa em linguagem de alto-nível para a linguagem de máquina. Uma linguagem de alto nível é suficientemente abstrata para ser independente do hardware (ou quase independente diversas características da arquitetura, como por exemplo a representação dos números, são refletidos nas linguagens). Para poder utilizar uma linguagem de alto nível, o programador deve conhecer sua sintaxe (forma) e sua semântica (significado). As linguagens de alto nível estão em constante evolução, refletindo as mudanças que ocorrem nas metodologias de programação. Dentre as primeiras linguagens destacam-se FORTRAN, COBOL, ALGOL e PL/I. Entre as linguagens mais utilizadas atualmente podem ser citadas C e PASCAL. Dois outros processadores de linguagens largamente usados são os montadores de macros (ou macro-assemblers) e os interpretadores. Macros Uma macro é uma pseudo-instrução que define um grupo de instruções de máquina. Uma pseudo-instrução é uma instrução que existe para definir condições especiais para o montador; não resulta em código propriamente dito, mas provoca transformações no código original. Um montador de macros traduz programas escritos em linguagem assembler com facilidades de macros, o que significa usar nomes simbólicos para representar seqüências de instruções. Cada vez que o programa encontra esta macro, substitui-a pela seqüências de instruções. Assim, por exemplo, poderia ser definida uma macro para o NEANDER que realizasse subtrações. Tal macro (convenientemente denominada SUB), conteria as instruções necessárias para realizar uma subtração (inversão do subtraendo, soma de uma unidade e soma com o minuendo), como ilustrado na definição da macro a seguir: MACRO SUB end STA aux LDA end NOT ADD um ADD aux END MACRO % realiza AC <– AC – MEM(end) % salva Acumulador em MEM(aux) % note-se que end é um parâmetro da macro % MEM(um) contém a constante UM 12-5 Todas as ocorrências posteriores do símbolo SUB seriam substituídas pela sequência acima, uma vez para cada símbolo. As macros não devem ser confundidas com as subrotinas. Subrotinas são conjuntos de instruções que podem ser utilizadas diversas vezes por um programa; assim, ações repetitivas não precisam ser escritas repetidamente e nem geram trechos repetidos de código em um programa. Cada vez que uma rotina é utilizada, é executado um desvio do programa principal (é o que chama a subrotina) para o endereço inicial onde ela se encontra carregada; após sua execução, é executado um novo desvio que causa o retorno para o programa principal. Uma subrotina para subtração seria por exemplo o seguinte trecho de programa, armazenado a partir do endereço xx : xx STA aux LDA end NOT ADD um ADD aux RET % salva Acumulador em MEM(aux) % note-se que end é um parâmetro da subrotina % MEM(um) contém a constante UM A chamada desta função seria feita através de uma instrução de chamada para subrotina (CALL xx) ou de desvio para subrotina (JSR xx). Note-se que na chamada deve ser fornecido o endereço de memória onde a subrotina foi armazenada. Tanto a instrução de chamada (CALL ou JSR) como a instrução de retorno (RET) devem existir na arquitetura do computador. O NEANDER, por exemplo, não possui estas facilidades. Interpretadores Um interpretador também é um processador de linguagem que traduz cada comando de um programa escrito em linguagem de alto nível, executando-o imediatamente. Assim, o interpretador trabalha alternando ações de tradução e execução. O compilador atua exclusivamente na tradução do programa fonte e não na execução, o que os diferencia. Assim o interpretador responde mais rapidamente a modificações no programa fonte, o que é bastante útil em um ambiente de desenvolvimento. Na execução rotineira de programas ele se torna mais lento, uma vez que refaz a tradução de cada comando a cada nova execução. O exemplo mais conhecido de interpretador é o utilizado na linguagem BASIC. Bibliotecas Bibliotecas de programas existem para simplificar tarefas repetitivas de programação. Assim, rotinas muito utilizadas são padronizadas, catalogadas e tornadas acessíveis aos usuários. Isto implica no estabelecimento de convenções. Nas aplicações científicas, encontra-se neste campo a programação de funções matemáticas tais como raiz quadrada, funções exponenciais, inversão de matrizes, etc Para processamento de dados comercial, encontram-se funções de organização de arquivos como ordenação e busca ou procura. Muitas outras estão disponíveis ou podem ser programadas de acordo com o interesse dos usuários. Carregadores Carregadores são programas do sistema que inserem ou posicionam outros programas na memória e preparam-nos para a execução. Podem ser do tipo absoluto, que carregam o programa a partir de um endereço especificado pelo usuário, ou do tipo relocável, que não permitem a intervenção do usuário, posicionando o programa de forma eficiente, de acordo com as disponibilidades de espaço na memória. Em um esquema simples, o montador assembler armazena sua saída em uma memória secundária (disco, por exemplo). A função do carregador seria simplesmente transferir o código desta memória secundária para a memória principal e transferir o controle para ele (colocar no PC para o endereço da primeira instrução do programa). 12-6 O uso de generalizado de bibliotecas, subrotinas e macros, entretanto, faz com que um carregador tenha mais tarefas a realizar. Basicamente, um carregador desempenha quatro tarefas: • Obter e reservar espaço de memória para o programa e suas subrotinas (Alocação). • Resolver os endereços simbólicos (labels) entre o programa e as bibliotecas utilizadas (Ligação ou linking ). • Ajustar todas as referências a endereços, de forma que elas correspondam aos endereços fisicamente utilizados, ou seja, aos endereços da porção da memória onde o programa foi carregado (Relocação). • Colocar fisicamente as instruções e os dados na memória (Carga). O período de execução de um programa é denominado de tempo de execução. O período de tradução de uma programa fonte é denominado de tempo de compilação ou tempo de montagem. O tempo que se refere ao período de carga e preparação de um programa para execução é chamado de tempo de carga. Um programa carregador especial é o bootstrap loader, cuja função é inicializar as atividades no computador, quando ele é ligado. O início da operação do computador não ocorre apenas com a ligação da energia, mas com a execução de um programa de inicialização. Este programa pode existir residente na memória ou pode ser carregado a cada início de operação. Utilitários Programas utilitários correspondem a uma coleção de rotinas freqüentemente empregadas que o programador pode usar para facilitar o seu trabalho de desenvolver tarefas específicas e consequentemente seu trabalho de programação. Assim como no grupo anterior, estão catalogados e disponíveis aos usuários do sistema. Como exemplos, pode-se citar: editores de texto, ferramentas de depuração de programas (“memory dump”, “trace”, “debuggers”, etc), rotinas de entrada e saída. Programas de diagnóstico têm por objetivo exercitar certas partes do hardware do sistema a fim de verificar situações de mau-funcionamento ou para testar a funcionalidade destas unidades. Assim, ele fornece sinais ao hardware e coleta as respostas correspondentes, comparando-as com as informações esperadas (resultados corretos obtidos a partir da especificação da máquina, por exemplo). Estes programas auxiliam o pessoal que trabalha em manutenção. Também podem ser rodados preventivamente para antecipar a ocorrência de problemas. Sistema Operacional Um sistema operacional é uma coleção de programas que controlam a operação do computador com o propósito de obter um desempenho eficiente. Consiste basicamente em programas de controle permanentemente residentes na memória, que supervisionam todos demais programas executados no computador. Em geral, sua eficiência é dependente dos recursos de hardware existentes para suportá-lo, uma vez que ele faz uso intensivo destes recursos. Sistemas operacionais rudimentares, instalados em geral em máquinas pequenas, são referidos pelos termos: programa monitor, supervisor ou executivo. Os sistemas operacionais serão estudados com maiores detalhes na próxima unidade. 1 2 . 5 Interface entre hardware e software Uma questão essencial a nível da arquitetura e organização de computadores é o estabelecimento da interface entre o hardware e o software. É possível conhecer-se vários 12-7 aspectos de software, sem que se tenha familiaridade com o detalhamento do hardware associado; da mesma forma, é possível projetar-se partes de hardware sem conhecer suas capacidades a nível do software. Entretanto, os projetistas da arquitetura de computadores precisam conhecer hardware e software, pois estas áreas influenciam-se mutuamente. Uma das definições estabelecidas a nível desta interface é a escolha das funções que serão implementadas no hardware e quais utilizarão o software. Reflexos destas definições serão sentidas na maior ou menor facilidade de programação e na velocidade de execução (de obtenção de respostas) destas funções. Adicionalmente, podem existir estruturas de arquitetura para apoiar funções do sistema operacional ou mecanismos que auxiliem na detecção de erros de programação. Existem três níveis básicos de implementação de funções de máquina no computador: • por hardware, fixas pelo projeto lógico dos circuitos; • por microcódigo, que consiste em um nível intermediário entre o hardware e o software, e que pode conceitualmente ser enxergado como um conjunto de programas escritos em baixíssimo nível (especificação direta de sinais de controle) que executam diretamente sobre o hardware; • por software, especificadas em uma linguagem de baixo nível. Funções complexas podem ser obtidas a partir da composição de outras dos níveis inferiores. O nível de problemas quando da definição da interface varia bastante em complexidade. Assim como é necessário definir detalhes de implementação de instruções, também é necessário determinar a arquitetura global da máquina. Há, por exemplo, arquiteturas dirigidas a determinadas linguagens, que utilizam combinações implementadas em hardware e firmware para estender a arquitetura de uma máquina na direção das formas utilizadas pelas linguagens de programação. Assim, grande parte das arquiteturas atuais são influenciadas por conceitos de estruturas de programação encontradas em linguagens de programação de alto nível e por funções comuns aos sistemas operacionais. Entretanto, existem divergências sobre quanto deve pesar esta influência e se ela deve ser mais motivada pelas características convenientes às linguagens ou aos sistemas operacionais. Esta é uma questão que resta a resolver. 1 2 . 6 Sistemas operacionais Na primeira geração de computadores, quando a programação era feita quase que exclusivamente que através de chaves e lâmpadas, os sistemas operacionais eram praticamente inexistentes. A complexidade de tal programação, entretanto, motivou o desenvolvimento de uma grande série de programas, analisados na seção anterior. Um quadro típico da década de 60, quando o sistema de entrada e saída era baseado em cartões perfurados, é descrito a seguir: • o programador perfurava o seu programa fonte (por exemplo, na linguagem FORTRAN) em um conjunto de cartões. • o programador pegava os cartões do compilador FORTRAN, marcados de verde para serem visualmente distinguidos dos demais, os colocava na leitora de cartões e carregava o compilador para a memória. • a seguir, com o compilador na memória, o programador colocava o programa fonte (em cartões incolores) na leitora. O compilador lia estes cartões e, caso não existissem erros, perfurava uma série de cartões (vermelhos) com o código objeto gerado. 12-8 • terminada a tarefa do compilador, o programador colocava o carregador (em cartões rosa) na leitora e carregava-o para a memória. • com o carregador na memória, o programador colocava agora seu programa objeto na leitora, seguido dos cartões correspondentes às bibliotecas utilizadas (em diversas cores específicas) • o carregador lia todos estes cartões, preparava o programa na memória e transferia o controle do computador para este programa. • o programador colocava na leitora os cartões contendo os dados a serem manipulados pelo programa, que lia estes cartões e gerava relatórios em uma impressora. • existindo erros na programação, todo o processo tinha que se refeito (e um dos pesadelos da época era a queda dos cartões no chão, embaralhando-os). Este sistema de cartões multicoloridos, embora facilitasse em muito o uso do computador, ainda era muito insatisfatório. O programador tinha que comandar manualmente todo o processo, e grande parte do tempo do computador era desperdiçado lendo e perfurando cartões. Conforme a demanda por tempo de processamento, memória, dispositivos de entrada e saída e quantidade de dados aumentava, a gerência eficiente destes recursos tornouse crítica. Todos os recursos de computação eram na época muito caros e valiosos, e tinham de ser eficientemente aproveitados para não gerar desperdícios. Foi nesta época que os sistemas operacionais foram desenvolvidos e refinados, sendo os precursores dos sistemas utilizados hoje em dia. Um dos mais utilizados na época era o sistema operacional baseado em lotes (batch operating system ), que permitia que um certo número de tarefas (jobs ) fossem colocadas juntas na leitora de cartões. Assim, por exemplo, o exemplo descrito acima poderia ser realizado pelo seguinte conjunto de cartões: //EXAMPLE //STEP1 EXEC 100 9100 /* //STEP2 /* //STEP3 JOB DONOVAN, FORTRAN, NOPUNCH READ 9100,N DO 100 I = 1,N I2 = I * I I3 = I * I * I PRINT 9100, I, I2, I3 FORMAT (3I10) END T168,1,100,0 EXEC LOAD EXEC OBJECT 10 /* O primeiro cartão define um processo que o usuário de nome Donovan deseja realizar; o segundo cartão indica que os cartões seguintes devem ser processados pelo compilador FORTRAN (até o cartão de fim de arquivo, marcado com /*). O passo seguinte é a chamada do carregador, sem nenhum outro cartão especial. O último passo comanda a execução do programa objeto recém gerado, e fornece os dados a serem lidos (no caso o número 10). Todos os cartões são interpretados pelo sistema operacional, que inclui todos os programas do sistema mencionados anteriormente, além de outros que supervisionam e controlam as operações de todos os programas no computador. O sistema operacional realiza as seguintes funções: • aloca espaço de memória e carrega programas para a execução; 12-9 • fornece serviços para a obtenção de dados de entrada e para a produção de dados de saída; • fornece recuperação automática para diversos tipos de erros, como erro na leitura de um dispositivo de entrada, ou erro de “overflow”. Nos sistemas operacionais simples, a memória totalmente alocada a um único programa. Assim, se este programa não utiliza toda a memória, parte deste recurso não é utilizado. Nos tempos atuais isto não é considerado um problema sério, mas até a década de 70 a memória era um recurso extremamente caro e que não poderia ficar ocioso. Para usar a memória na sua integridade, desenvolveram-se os sistemas operacionais multiprogramados. Em sistemas multiprogramados, dois ou mais programas são carregados na memória (em áreas diferentes, naturalmente) e o computador os executa simultaneamente. Havendo apenas uma UCP, o sistema só pode processar, em cada instante, tarefas relativas a um usuário. Mas o sistema operacional atende a todos com uma rotatividade intensa, de tal forma que ele parece estar executando todos em paralelo. Portanto, nestes sistemas, muitos programas podem residir simultaneamente no computador. O sistema operacional aloca os diversos recursos computacionais para os programas selecionados e mantém as atividades de chaveamento dos recursos durante o funcionamento do sistema. Exemplificando, enquanto um programa está sendo executado na UCP, um outro programa pode estar recebendo dados de um dispositivo periférico (fita magnética, por exemplo), e um terceiro pode estar imprimindo dados. Em tais sistemas um dos principais problemas é a gerência eficiente de memória. Com a constante alocação e liberação de memória, podem surgir áreas de memória muito pequenas para conterem um programa. O surgimento destes “buracos” na memória é conhecido como fragmentação. A fragmentação tem sido minimizada por diversas técnicas. Uma delas consiste em rearranjar os endereços do programas (realocação dinâmica), movendo-os de lugar e assim reunindo todas as porções não utilizadas em uma única região contínua de memória. Outra técnica é a de paginação: por este método, os programas são sub-divididos em porções iguais ou páginas, e a memória é dividida em porções denominadas blocos. Na hora da carga do programa, as páginas são carregadas nos blocos. Em um sistema de paginação simples todas as páginas do programa são carregadas na memória. Já em um sistema mais sofisticado de paginação por demanda, um programa pode ser executado sem que todas as suas páginas estejam na memória. As páginas são carregadas conforme elas são necessárias, isto é, quando elas são referenciadas (demandadas). Em sistemas onde há mais do que uma UCP, há a possibilidade real de que sejam executadas duas ou mais instruções ao mesmo tempo: neste caso, tem-se multiprocessamento, ou seja, cada processador pode executar um programa diferente em paralelo (ao mesmo tempo) aos demais. Existem sistemas nos quais o tempo do processador é partilhado entre diversos usuários: são os sistemas de time-sharing (ou “de divisão de tempo”). Nestes, muitos usuários comunicam-se com o sistema computacional através de terminais remotos. O sistema operacional aloca a cada um, ou seja, a cada “job”, um período de tempo (“time-slice”) com base em considerações de prioridade. O “job” é uma unidade de trabalho especificado aplicado na execução de uma tarefa de processamento de dados. Assim, durante estes períodos de tempo, o computador faz com que o computador processe um “job” até que ocorra uma das seguintes condições: • o “job” é completado; • um erro é detectado; • ocorre solicitação ou necessidade de uma entrada/saída; • o período de tempo termina. 12-10 Então o processador é designado para o “job” de mais alta prioridade. Nos dois primeiros casos, o “job” deve ser removido da memória. Nos dois últimos, o “job” é suspenso temporariamente. O sistema operacional contribui para o usos mais eficiente dos recursos de hardware pelo gerenciamento dos recursos de memória. Por exemplo, se um programa não pode ser acomodado inteiramente na memória devido ao seu tamanho, o sistema operacional pode dividi-lo em partes denominadas de páginas ou segmentos, transferindo-os gradualmente da memória secundária para a principal. O efeito do sistema operacional na gerenciamento do sistema computacional visa melhorar sua eficiência, a qual é avaliada pelo “throughput”. “Throughput” é a quantidade de processamento que o sistema realiza com sucesso durante um intervalo de tempo especificado. Pode servir como medida de avaliação tanto para hardware como para o software. A contribuição do sistema operacional neste sentido é resultante da eficiência das facilidades existentes no seu código. 1 2 . 7 Funções básicas dos sistemas operacionais Um sistema operacional é um conjunto de programas que permite ao computador controlar os recursos, executar programas e gerenciar dados. Inclui todos os programas do sistema mencionados anteriormente, além de outros que supervisionam e controlam as operações de todos os programas no computador. Do ponto de vista do usuário, a função do sistema operacional é auxiliá-lo na mecânica de resolução de problemas. O sistema operacional tem três encargos principais: gerenciar a execução de programas e ações realizadas na memória, gerenciar o armazenamento de arquivos e gerenciar as atividades de E/S. Para tanto, ele detém três funções básicas, que são: • controlar os recursos do sistema de computação; • executar os programas do computador; • gerenciar dados. Estas funções são executadas sem que o programador precise estar instruindo o sistema operacional a fazê-lo, a cada momento. Na verdade, o sistema operacional já existe como uma série de programas que fazem parte da máquina e que permitem uma operação razoavelmente confortável para o programador de aplicações. Os programas de controle minimizam a intervenção do operador de tal forma que as operações do computador fluem de forma suave e contínua. O programa mais importante no sistema é o supervisor, cuja maior parte reside na memória (está sempre lá). Ele controla o sistema operacional inteiro e chama outros programas do sistema operacional (do disco), quando necessário, para que eles permaneçam na memória enquanto forem executados. Após, eles retornam para o disco, para que se tenha uso eficiente do espaço de memória. O sistema operacional aumenta a eficiência de duas maneiras: • ele atua como promotor da cooperação entre os usuários do sistema, ajudando-os a fazerem o melhor uso possível dos recursos computacionais de forma que todos tirem proveito. • ele chama tradutores e outros programas para que se encarreguem de tarefas comuns (usuais). Isto libera os programadores de aplicações de tarefas rotineiras e repetitivas. A fim de cumprir as funções acima listadas, o sistema operacional executa atividades tais como: • operações de seqüenciamento e escalonamento de “jobs” (alocação de espaço de memória e carga de programas para a execução), e controle de tráfego: o sistema recebe 12-11 os diferentes “jobs”, e com base em suas características e necessidades decide sobre sua execução (prioridades, tempo de execução, recursos disponíveis, etc). Quando é possível executar-se entrada/saída simultaneamente à execução de um programa, todas estas funções são escalonadas pelo controlador de tráfego. • programação de entrada e saída: executa diretamente as ações relacionadas às operações de entrada e saída, quando o canal de entrada e saída tem seu conjunto próprio de instruções especializadas, necessitando apenas de instruções simples do usuário referentes a estas operações. • auto-proteção (contra o usuário) e proteção do usuário com relação aos demais: oferece proteção ao usuário, evitando que seus programas, bases de dados ou arquivos sejam modificados por ações maliciosas ou acidentais. Igualmente, o próprio sistema operacional deve assegurar sua auto-inviolabilidade. • gerenciamento de armazenamento secundário: é o controle do uso de discos, fitas e outros meios de armazenamento secundário para os programas e dados do usuário, sendo uma tarefa realizada pelo sistema operacional, transparente do ponto de vista do usuário. • manipulação de erros: o sistema operacional deve realizar ações específicas com relação aos diversos tipos de erros que podem ser causados durante a operação e uso da máquina, que podem variar desde o envio de aviso ao usuário até a correção ou modificação de parâmetros para poder prosseguir na operação. Estas funções precisam ser compatibilizadas com as estruturas de hardware, de modo que se obtenha o melhor compromisso em objetivos conflitantes tais como eficiência, ciclos rápidos de execução, e conveniência do usuário. Para tanto são usadas diversas técnicas de implementação; algumas são comentadas ao longo deste material. 1 2 . 8 Processos e escalonamento Um processo pode ser visto como uma seqüência ou conjunto de operações que realizam uma tarefa computacional, como por exemplo um processo de leitura, impressão, execução, etc. Um processo computacional pode ser seqüencial com um conjunto de operações ordenadas em tempo ou concorrente com operações paralelas. O conceito de processos é importante para sistemas operacionais porque a realização de cada processo pode representar a execução de uma tarefa isolada, complementar ou concorrente escalonados pelo sistema operacional. Processos seqüenciais são caracterizados por uma ordenação da execução de suas tarefas no tempo, portanto facilitando a administração de um conjunto de tarefas por um sistema operacional. Em processos concorrentes é possível ter duas ou mais operações em paralelo. Os processos concorrentes existem por causa da competição no uso dos recursos de um computador como, por exemplo, usos da memória, UCP, leitoras, etc, resultando em escalonamento de operações para minimizar qualquer conflito entre processos como também reduzir o tempo de execução. Isto é feito normalmente através de processos como, por exemplo, leitura pelos canais, enquanto a UCP executa outros processos. A operação spooling é baseada sobre o princípio de sobreposição de processos com o auxílio de canais ou dispositivos especiais. Em geral, o compartilhamento dos recursos de um computador em tempo e espaço necessita de um módulo de escalonamento associado aos sistemas operacionais multiprogramáveis. Existem duas formas principais de escalonamento: escalonamento sem e com preempção. O escalonamento sem preempção assume que um processo já em posse de um recurso (UCP, leitora, impressora, etc) não é interrompido até o final da execução do processo. Para implementar tal política, são usados modelos primitivos na decisão de “enfileiramento” dos 12-12 processos, tais como: primeiro a chegar, primeiro a ser servido, ou processos com mínimo tempo de execução. Com preempção, um processo pode ser interrompido em execução para transferir controle de um recurso para outro processo ou atender às necessidades do sistema. Este tipo de escalonamento é muito empregado em sistemas de multiprogramação onde existe compartilhamento de espaço e tempo por todos os processos. Em sistemas de multiprogramação, os modelos para determinar a política de escalonamento podem ser bastante complexos. A interrupção, bastante usada em sistemas multiprogramáveis, tem a função de preempção de um processo momentaneamente em posse de um recurso de um recurso, tal como UCP, dispositivos, etc. 1 2 . 9 Carga do sistema (inicialização da máquina) Denomina-se de carga do sistema, à operação que tem por objetivo colocar o computador em condições de funcionamento. Consiste, fundamentalmente, em carregá-lo com rotinas essenciais ao atendimento dos diversos programas de aplicação que lhe serão posteriormente submetidos. Esta operação é comumente denominada de bootstrap, referindo-se informalmente ao procedimento como “dar o boot” na máquina. Esta operação envolve: a colocação das primeiras instruções na memória a partir de um comando específico: nas máquinas modernas, este comando já vem embutido na inicialização da máquina; nas antigas, era disparado por uma tecla ou botão especial, ou estas instruções eram carregadas manualmente na memória da máquina. Este programa dispara a leitura (de um disquete, do disco, de uma memória de armazenamento permanente, por exemplo) de um programa carregador que então é posicionado na memória; a partir deste, novos programas ou rotinas são sucessivamente carregados ou posicionados na memória, para executar as ações subseqüentes. Esta carga completa é denominada de boot inicial do sistema. O computador não tem a capacidade de reter a informação que está na memória principal quando é cortada a energia elétrica: assim, esta operação é repetida a cada nova ligação da máquina. 1 2 .1 0 Multiprogramação Assim, como visto na unidade anterior, a multiprogramação se refere à existência de mais do que um programa em diferentes partes da memória principal ao mesmo tempo. Seu principal objetivo é a eficiência computacional. Uma técnica relacionada é a multi-tarefas, ou a existência de diversas tarefas que são parte do mesmo “job” e podem ser executadas simultaneamente. Em sistemas multiprogramados, dois ou mais programas são carregados em diferentes áreas da memória, na expectativa de que eles vão gerar uma quantidade significativa de trabalho para o computador. Se este trabalho gerado exceder a capacidade do computador, o resultado é que a máquina (em princípio) não deverá ficar parada, esperando novas tarefas. Assim, o programa supervisor executa o trabalho de alocação das diferentes unidades pela manutenção de listas de passos que estão prontos para execução em cada uma das unidades. Quando uma unidade completa um passo, o supervisor pode consultar a sua lista para um novo trabalho. No caso das unidades de entrada/saída, a conclusão de uma passo é indicado por uma interrupção, que sinaliza à UCP para parar temporariamente o programa atual e para alocar mais trabalho à unidade de E/S, se houver. No caso da CPU, a conclusão é indicada quando o programa na UCP ou chega ao final do job ou requisita buffers de E/S que ainda não estão disponíveis – isto é – se uma entrada foi requisitada, e o sistema ainda não preencheu o buffer, ou se uma saída foi requisitada, o sistema ainda não esvaziou os buffers anteriores de tal forma que haja lugar para os novos. Neste gerenciamento de execução de jobs, há problemas envolvidos tais como: 12-13 • quando ocorre a interrupção nas atividades de um job, visando transferir o processamento para outro, é necessário assegurar a guarda de valores contidos nos registradores e memória, para posteriormente poder retomar as atividades; • determinação de qual(is) job(s) deve(m) ser carregado(s) na memória quando há diversos aguardando na fila de execução, e qual tarefa deve ser alocada a uma unidade em particular, quando há diversas tarefas que aguardam por programas que já estão na memória. A solução emprega algoritmos de escalonamento e critérios de prioridades; • custo de memória, já que há necessidade de maior espaço de memória do que em sistemas onde somente um job pode ser rodado em cada momento; • ainda outros problemas dizem respeito a questões tais como: alocação de memória, relocação e proteção. A fim de racionalizar o compartilhamento pa UCP (processador) entre os diversos jobs (processos) utiliza-se um scheduler, ou seja, um elemento de software que realiza duas funções básicas: 1. selecionar um processo entre os prontos para executar (ready) para ser o próximo a ganhar o controle da UCP. 2. determinar a fatia máxima de tempo de processador (time-slice) que o processo pode utilizar antes de ir novamente para a fila dos processos prontos para executar. A Figura 12.1 ilustra as transições de estado que um processo pode sofrer. Os processos que reúnem todas as condições necessárias para serem executados estão no estado “pronto” (ready, em inglês). Quando um dos processos deste grupo ganha o controle do processador, ele transiciona para o estado “executando” (running). Ele sairá deste estado ou quando a sua fatia de tempo terminar (neste caso ele retorna para o grupo pronto) ou quando necessitar de um evento externo à UCP (neste caso vai para o grupo bloqueado). Um processo em estado “bloqueado” (blocked) permanece neste estado até que o evento esperado ocorra (como o término de uma operação de E/S, a execução de uma determinada operação por outro processo, etc). Quando o evento ocorre, o processo volta ao grupo “pronto”. Processo selecionado Pronto Executando Processo precisa esperar pelo término de um evento (por exemplo, E/S) Processo interrompido (fim da fatia de tempo) Bloqueado O evento esperado pelo processo ocorreu Figura 12.1 - Estados de um processo A figura não ilustra nem o “nascimento” nem a “morte” de processos. Quando um processo é inicializado, ele vai primeiramente para a fila dos processos “prontos”, concorrendo com os demais ao uso da UCP. Quando um processo encerra sua execução, ele simplesmente não retorna ao grupo “pronto”. 12-14 Diversos problemas podem ocorrer no escalonamento dos processos, principalmente relacionados à sincronização entre processos. Um deste problemas é o da “corrida” (race), quando a sincronização é tão crítica que diversas ordens de escalonamento podem produzir diferentes computações (resultados diferentes). Outro problema é o do “deadlock”, quando diversos processos se bloqueiam mutuamente, e de tal forma que cada processo fica esperando por eventos que deveriam ser gerados pelos outros processos bloqueados. Estes problemas podem ser resolvidos de duas maneiras básicas distintas: por cooperação ou por comunicação. Na comunicação, os processos trocam mensagens entre si, de forma a se sincronizarem e impedir o surgimento de problemas. Na cooperação, existem recursos críticos, que devem ser utilizados única e integralmente por um processo antes de passarem para outros processos. Estes recursos de “exclusão mútua” normalmente tem seu acesso controlado por variáveis do tipo “semáforo”. O detalhamento exato dos problemas acima, assim como as metodologias e soluções possíveis serão analisados em outras disciplinas. 1 2 .1 1 Multiprocessamento O multiprocessamento se refere aos sistemas onde há duas ou mais UCPs em um único sistema computacional; assim, há a possibilidade real de que sejam executadas duas ou mais instruções ao mesmo tempo. Estas UCPs estão conectadas à mesma memória, de tal forma que elas podem estar executando partes do mesmo ou de diferentes programas. O uso de múltiplas UCPs visa incrementar a capacidade de processamento do sistema, freqüentemente avaliado em mips (millions of instructions per second). Ainda há os sistemas nos quais várias UCPs, cada uma com a sua própria memória, podem ser interligadas através de canais, cada uma assemelhando-se a um dispositivo de E/S do ponto de vista dos demais computadores. Isto não é multiprocessamento: é uma sistema multicomputador, também denominado de rede de computadores. Nestes sistemas, há distribuição dos jobs aos computadores que estão com tempo de processamento disponível nas modalidades requisitadas, o que é caracterizado como distribuição de carga. 1 2 .1 2 Exemplos de sistemas operacionais Os sistemas operacionais vêm normalmente incorporados às máquinas (computadores), sendo cobrados à parte ou não. Atualmente, há tendência no uso de sistemas operacionais genéricos, isto é, sistemas operacionais cuja natureza permite que ele opere com sistemas de computadores de diferentes fabricantes. Estes sistemas tem sido freqüentemente criados por companhias de software e não por fabricantes de máquinas. Exemplos destes casos são o UNIX e o MS-DOS. O UNIX foi desenvolvido em 1971 por Ken Thompson e Denis Ritchie na AT&T Bell Laboratories para uso nos minicomputadores DEC da Bell. Os projetistas surpreenderam-se com a aceitação no mercado externo à Bell. Como razões para o fato, apontam: o uso disseminado do software por faculdades e universidades, cujos usuários repassaram o uso posteriormente às indústrias; outra razão foi a enorme redução no preço por cópia, em 1981, sendo estas comercializadas por US$ 40. Este é um sistema operacional multi-usuário, com divisão de tempo (time-sharing) que, embora inicialmente implementado para minicomputadores, atualmente roda em máquinas de grande porte e em alguns microcomputadores. Entre os problemas relacionados ao UNIX, apontam-se: sua dificuldade de utilização (do ponto de vista do usuário, não é “user-friendly”, embora esta característica tenha sido melhorada pelo uso de menus tradicionais); além disto, faltam ao UNIX algumas características de segurança sofisticadas. Para computadores pessoais, o sistema operacional mais popular é o MS-DOS (Microsoft Disk Operating System; Bill Gates, Microsoft). Este sistema foi escolhido pela IBM para incorporá-lo aos seus computadores pessoais, em detrimento do CP/M (Gary Kildall, Digital Research) que já se encontrava em uso, na época. Neste sistema, os programas são executados pela emissão de um comando, ou seja, um nome que chama o programa 12-15 desejado. Os comandos internos do DOS são colocados na memória do computador quando o usuário liga-o. Os demais, que residem em disco, são chamados de comandos externos do DOS. De forma geral, o software escrito para rodar em um sistema operacional, não rodam em outro. Assim, os projetistas de software tentam maximizar as vendas de seu produto escrevendo programas para os sistemas operacionais mais utilizados. Alguns sistemas fornecem figuras e/ou designações simplificadas aos invés de comandos ou símbolos simples como o PROMPT (>) do DOS. O efeito destas figuras é o de apresentar um aspecto mais “amigável” para o usuário, existindo tal como uma “roupagem” ou shell ao redor do sistema operacional. Assim, eles criam um ambiente confortável para o usuário, que não precisa estar memorizando comandos específicos. 1 2 .1 3 Redes de computadores Computadores podem ser interligados entre si, de forma a compartilhar recursos (memória, periféricos, UCP e informação). Se os computadores estão próximos entre si (na mesma sala ou no mesmo prédio), a rede é denominada de rede local ou LAN (Local Area Network). Se por outro lado os computadores estão geograficamente distantes, tem-se uma WAN (Wide Area Network). Ao nível de hardware, a comunicação é realizada através de circuitos especiais, que transformam os dados a serem transmitidos em sinais elétricos. Como meio físico utilizam-se desde pares telefônicos até cabos coaxiais, fibras óticas, enlaces de microondas e até satélites de comunicação. Ao nível de software desenvolveram-se diversos métodos de comunicação (protocolos), que definem como esta comunicação é realizada e comos os dados intercambiados devem ser interpretados. Frente a sua complexidade, redes modernas são projetadas de forma altamente modularizada. A maioria das redes se encontra organizada em uma série de camadas hierárquicas, onde cada camada utiliza os serviços definidos da camada inferior e fornece uma outra série de serviços, de mais alto nível, para a camada superior. A International Standards Organization (ISO) criou o modelo de referência OSI (Open Systems Interconection), que define sete destas camadas em cada máquina: 1. Físico: define as características dos equipamentos e os requisitos para a realização das ligações. 2. Enlace (ou ligação): define os meios e os procedimentos para a transmissão de blocos de informação e controle dos possíveis erros que possam ocorrer. 3. Rede: define o intercâmbio de informação dentro da rede. Trata do agrupamento da informação em pacotes, do endereçamento dentro da rede e da detecção e correção de erros. 4. Transporte: trata da transferência de mensagens, do agrupamento e decomposição dos pacotes de dados. Outra atribuição é a otimização do uso da rede, selecionando as conexões adequadas. 5. Sessão: controla as operações realizadas sobre os dados, a fim de assegurar sua integridade com respeito ao uso compartilhado dos mesmos. Neste nível agrupamse as mensagens relacionadas entre si (estabelecendo uma sessão). 6. Apresentação: trata da organização das entradas e saídas, definindo os formatos necessários aos terminais, aos arquivos e aos dados, a fim de que possam ser utilizados pela sessão e pela aplicação do usuário. 7. Aplicação: consiste no controle e supervisão dos processamentos dos usuários que se intercomunicam. 12-16 Do ponto de vista do sistema operacional, tem-se um recurso a mais a ser gerenciado: a rede. Serviços que não estão disponíveis no computador local podem estar acessíveis via rede e, para utilizá-los de forma adequada, o sistema operacional deve ter conhecimento deles. Temse assim os sistemas operacionais distribuídos, onde os recursos a serem gerenciados não estão concentrados em um único computador, mas sim espalhados ao longo da rede. O processador deixa de ser uma entidade única no sistema. Além do tradicional aspecto do escalonamento de processos (qual processo será executado pelo processador), surge a questão do escalonamento de processadores (qual dos processadores disponíveis irá executar determinado processo).Uma outra consequência da multiplicidade de processadores é a possibilidade de continuidade do processamento após a ocorrência de falhas em um determinado processador. Como provavelmente ainda haverá réplicas desse recurso na rede, os processos afetados pela falha podem continuar após migrarem para um processador não falho. Tais vantagens, entretanto, são pouco exploradas atualmente. Esse fato decorre da pouca utilização de sistemas operacionais distribuídos nesse tipo de ambiente. É mais frequente o emprego de sistemas operacionais centralizados tradicionais, que não foram projetados especificamente para ambientes distribuídos, com extensões para suportar algumas operações remotas básicas (como transferência de arquivos e execução remota de processos, por exemplo). Por um sistema distribuído se entende atualmente um sistema que consiste de múltiplos processadores que não compartilham memória primária e se comunicam por mensagens através de uma rede de comunicação. Programas distribuídos podem contar com quatro tipos de processos: clientes, servidores, filtros e pares. Clientes e servidores interagem usualmente de forma síncrona: um cliente envia uma mensagem de requisição de serviço a um servidor local ou remoto; o serviço é executado pelo servidor, que retorna os resultados ao cliente (que espera bloqueado). Um servidor pode ser projetado para atender múltiplas requisições concorrentemente. Processos filtro recebem, processam e enviam adiante os dados obtidos. Processos pares interagem com mensagens para cooperar no atendimento a requisições. Nos dias de hoje os ambientes computacionais da maioria das organizações apresentam um realidade heterogênea. Esta heterogeneidade, que pode ser definida com a diversidade existente em sistemas de computação, manifesta-se especialmente sob os seguintes aspectos: • arquitetura da máquina: processadores, memória e dispositivos de E/S. As arquiteturas atuais diferem em uma série de aspectos, dos quais o principal é a família do elemento processador utilizado no equipamento. Por família entenda-se um conjunto de elementos processadores com certo grau de compatibilidade entre si. Exemplos são os microprocessadores Intel 80x86 e os Motorola 680x0. Uma parcela das estações de trabalho existentes atualmente utiliza processadores RISC (Reduced Instruction Set Computer), embora as linhas de processadores de cada fabricante sejam incompatíveis entre si. Há ainda outros tipos de processadores, tal como os Transputers, utilizados em máquinas paralelas. • sistemas operacionais: chamadas do sistema, interface com o usuário (interpretador de comandos e ambientes de janelas) e mecanismos de comunicação entre processos; • redes de comunicação: protocolos de comunicação e interfaces de rede; • linguagens de programação e compiladores: diferenças entre implementações de uma mesma linguagem e uso de diferentes linguagens. Um sistema operacional de rede pode ser definido como a soma das camadas de software escritas em cada máquina (host, ou hospedeiro) para propiciar comunicação e compartilhamento de recursos entre a máquina local e diferentes máquinas remotas. Tanto o computador local como os equipamentos remotos que compõe a rede são entidades autônomas. 12-17