


















































Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Encontra documentos específicos para os exames da tua universidade
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Apostila SOII UERJ
Tipologia: Notas de estudo
1 / 58
Esta página não é visível na pré-visualização
Não perca as partes importantes!



















































1) Amarração de Endereços (Adress Binding):
Sistema Operacional pouco sofisticado executa um programa por vez e assim teremos um único programa na memória conforme figura 1.1. Na figura 1.2 temos uma amostra de um Sistema operacional um pouco mais sofisticado. Ele controla a execução de mais de um programa por vez. Assim, estão na memória dois ou mais processos sendo executado segundo controle do Sistema Operacional. Ainda na figura 1.2 podemos observar dois processos ocupando áreas diferentes na memória: o processo 1 começa no endereço 0 até o endereço 9999, ocupando um tamanho de 10000; o processo 2 começa no endereço 10000, ocupando um tamanho de 10000 também.
O problema é que não é tão simples para o Sistema Operacional controlar um programa na memória, veja o exemplo abaixo:
Exemplo em Assembly:
ADD AX, BX => Adicionar um registrador com outro (de BX para AX) MOV AX, CX => Mover o conteúdo de um registrador para outro (de CX para AX) MOV AX, [1000] => Mover o conteúdo do endereço 1000 para o registrador AX. JNZ 2000 => Desvia a execução para o endereço 2000 CALL 3000 => Chamada de Sub-rotina.
A partir do exemplo acima verificamos que o programa acima só vai servir para o processo 1, para o processo 2 não servirá, pois este último começa na posição 10000, e o endereço 1000 não faz parte do processo 2. Porém, o arquivo executável não sabe onde
será armazenado na memória depende da ordem em que os processos foram chamados pelo usuário. Então o código acima não serviria para o processo 2. Este problema faz parte um problema chamado de amarração de endereços(adress binding) , é a amarração entre um nome e um endereço. Geralmente, o programa tem nomes, veja exemplo abaixo:
SubRot:. . . Ret
CALL SubRot
A CPU não entende esta instrução “call SubRot”. Ela só entende endereços de memória, um array de byte. O montador de Assembly transforma o label “SubRot” em endereço, em um valor da posição de memória. Porém isto não é tudo existe o outro problema anterior que é o carregamento do programa na memória. O programa pode ser carregado na posição 0 ou 10000. Se o programa (1.1) for carregado na posição 10000 ele não vai funcionar. Isto pode ser resolvido no seu carregamento na memória. A seguir iremos ver as soluções para resolver a amarração de endereço permitindo que o programa seja carregado em qualquer lugar da memória.
1.1) Correção de endereços em tempo de carga: Exemplo de Sistema Operacional: MS-DOS para arquivos executáveis (.exe): O montador ou compilador gera código imaginando que o programa vai ser colocado em um certo lugar na memória, supondo no endereço 0, por exemplo. Com isso, é simplificado o trabalho do montador ou compilador. O sistema operacional antes de colocar o arquivo na memória ele corrige o endereço. O sistema operacional sabe onde será carregado o arquivo que sai do disco e será colocado na memória para execução. Ele vai a todos os endereços do programa e soma o endereço inicial daquele programa, segundo exemplo abaixo:
Endereço Inicial do programa = 10000 . . . MOV AX, [11000] => 10000 + 1000 = 11000 JNZ 12000 => 10000 + 2000 = 12000 CALL 3000 => 10000 + 3000 = 13000
Agora o programa (1.1) pode ser carregado na posição 10000 que irá funcionar. O programa executável é formado por código mais informações de controle que o sistema operacional usa para a correção de endereço. Pois, o sistema operacional precisa de uma referência de quais instruções ele precisa alterar, essas informações de controle dizem quais são as rotinas que precisam ser alteradas. Um exemplo de sistema operacional que usa este tipo de solução é o MS-DOS e Windows 16 bits para arquivos executáveis (.exe). As versões do Windows 32 bits e superiores não são mais assim. Existe uma instrução no MS-DOS e Window 16 bits chamada EXEHDR <nome do executável> que mostra todos os endereços que precisam ser corrigidos.
O sistema operacional não faz nada com relação à correção de endereço, mas a cada instrução executada a CPU soma o valor do endereço absoluto da instrução mais o endereço contido no registrador de base. Por exemplo, se o processo 2 foi bloqueado e o escalonador elege o processo 1 para execução, o valor do registrador de base passará de 10000 para 0. O responsável pela alteração do endereço do registrador de base é o sistema operacional. A vantagem desta solução para a solução anterior é que o executável não precisa ter nenhuma informação de controle de endereço, simplificando a carga. Quanto ao tempo de execução em teoria seria mais lento devido a cada instrução que faça referência a endereços absolutos ter uma soma, porém esta soma é feita pelo hardware, um hardware dedicado a fazer isto. Com isso o tempo que leva esta soma é irrelevante, não contribuí para que CPUs que usem registrador de base sejam mais lentas que CPUs que não usem registrador de base. Se fosse uma operação feita por software poderia ficar mais lento, mas não é o caso. Exemplos de sistemas operacionais que usam esta solução: MS-DOS para programas (.COM. e .EXE). O programa “.EXE” do MS-DOS é uma mistura da solução 1 e 2.1, ou seja, têm endereços que são resolvidos em tempo de carga e têm endereços relativos a base. A CPU Intel tinha um problema que a distância em relação à base não podia ser muito grande, a limitação era 64Kbytes, à distância do endereço da base. Se o programa “.EXE” tivesse menos que 64Kbytes então podia usar apenas a solução 2.1. Caso o programa tivesse mais que 64Kbytes então tinha que usar uma solução mista. A CPU Intel (até 80186) tem quatro registradores de base que para cada tipo de instrução é reservado um registrador de base diferente, veja abaixo, os registradores existentes: cs – code (instruções com endereços relativos a código) ds – data (instruções com endereços relativos a memória, variável global) ss – stack (instruções com endereços relativos a variáveis locais, pilha) es – extra Observação:
1.2.2) Relativos à instrução corrente: A CPU tem que ter instruções cujo endereço não é endereço que conta a partir do 0, a partir da base. São endereços que contam a partir da própria instrução. Por exemplo, se quiser fazer um loop o que conterá na instrução é a distância entre a instrução de loop e o endereço para onde se quer desviar, como é mostrado abaixo:
Isto é um endereçamento relativo a instrução corrente. Nesta solução não importa onde o programa foi carregado na memória, pois a distância de uma instrução desvio para o seu destino, como mostra o exemplo acima, não se altera. O código que só usa este tipo de endereço é chamado código relocável, porque ele pode ser mudado de posição na memória e continuar executando sem precisar fazer mais nada. A Intel tem tipo de instruções que fazem isto, por exemplo, JNZ relativo e o absoluto, o compilador escolhe um ou outro.
1.3) Segmentação: 1.3.1) Introdução de segmentação: Um processo contém: A) CÓDIGO: B) DADOS: B.1) variáveis GLOBAIS: quando o programa começa ela passa existir e quando o programa termina ela deixa de existir; B.2) variáveis LOCAIS: o problema maior que ocorre com essas variáveis está dentro de funções recursivas, como mostra o exemplo abaixo: Exemplo: function FAT(n: integer): integer; begin if n = 1 then FAT := 1 else FAT := n * FAT(n-1); end;
program principal; var FAT: integer; begin FAT := 1; for i := 1 to n do FAT := FAT * i; End.
A remoção é a complicação desta alocação de variável local em uma função recursiva, pois não existe uma única variável n na memória existem vários ns. Para resolver este problema é utilizada uma pilha de execução. Na pilha de execução temos o seguinte funcionamento, supondo n = 4:
Nota: Heap : local onde são armazenadas as variáveis dinâmicas. A mesma coisa acontece com Q e R, quando faz newQ e newR.
Se (ponteiro)P = 3,8, P está referenciando o ponteiro e está usando o valor 3,8 na variável dinâmica apontada por P. Quando acabar acontecerá o seguinte:
Diferença entre variáveis locais e dinâmicas : As Variáveis locais em um sequenciamento muito claro de alocação e desalocação quando a rotina começa a rodar, são alocadas todas as variáveis locais para essa rotina, quando a rotina acaba de rodar, são desalocados na mesma ordem em que foram alocadas. Nas variáveis dinâmicas o programador pode escolher a ordem de desalocação, não seguem a ordem de pilha como nas locais. Não dá para usar uma pilha para alocar variáveis dinâmicas. A grande diferença é o sequenciamento. Obs. : O ponteiro guarda o endereço na memória. A variável é aquilo para qual o ponteiro aponta. O ponteiro contém o endereço e esse endereço aponta para a variável dinâmica alocada na heap. A variável dinâmica é aquilo que foi alocado na heap. O grande ponto: a heap não é uma pilha. A heap é a 3ª área diferente de dados.
Quatro tipos de área diferentes de um processo :
No caso do UNIX juntou duas e agora são três áreas:
Obs. : Juntou globais e dinâmicas.
A partir do conceito acima podemos concluir que o processo fica todo contido na memória e podemos ter o mesmo processo em várias partes da memória.
Como essas áreas sofrem alteração?
Área de Código
Faz sentido ser modificado? Sim, durante a execução do programa. Esses programas geram códigos que são incorporados a eles mesmos. Mas isso não é muito comum. Os programas automodificáveis eles precisarão ter alteração na área de código, mas no caso mortal essa área é imutável, uma vez que o programa é carregado na memória e vira processo essa área vem a ser modificada e é por causa disso que desde que o hardware permita, essa área fica inibida, impedindo que o processo faça modificação nessa área e se o processo tentar fazer ele é abortado. Então, geralmente, a resposta à pergunta acima é Não.
Área de Pilha
Sofre modificação? Sim, pois as varáveis locais podem ser modificadas. A área de pilha é alterável. Não apenas isso. Dentro da área de pilha tem um ponteiro que aponta para um lugar dentro da área que esse ponteiro se chama topo da pilha e no caso da Intel que tudo que está abaixo dos outros está livre, só a parte de cima tem variáveis. Quando uma rotina se inicia (o que significa alocação? Significa que descer um pouco do topo), quando a rotina acaba, é solto o topo da pilha, quer dizer que a rotina acabou. A CPU de 30 anos pra cá, tem uma função específica para manipular o topo da pilha (mais básicas: paste e copy). Nessa área de pilha o valor muda, muda a área total, vai mudar o valor de dentro. Outra coisa é, pode ter um programa que tenha um tempo de execução muito grande, ou um programa que tenha variáveis locais muito grandes, pode acontecer
O que a segmentação propõe? O hardware propõe que ao invés da memória ser uma coisa contínua que a mesma seja bidimensional. A memória não tem mais de um endereço, a memória terá dois valores que dizem onde a memória está. Terá o número do segmento e o deslocamento onde essa memória está.
Nº do segmento Deslocamento dentro do segmento
Nº do segmento escolhe qual é a área e deslocamento do segmento diz o que dentro da área vai ser.
Na Intel : Ex: mov ds, 1 => qual segmento? mov ax, [2500] => qual deslocamento?
Vantagem: Com isso, o hardware conhecerá as áreas, e controlará melhor a memória, impede que o processo mexa em áreas que não são suas, ou seja, impede que não haja invasão de segmentos.
mov BX, [2500] => nesse caso o hardware identifica que o processo não tem acesso. Está fora do segmento, então aborta. Outra vantagem é que não importa para o programa onde está o segmento de memória, pois, o programa sabe que é um endereço bidimensional, a memória é contínua, o programa nem sabe que existe, ele só sabe o nº do segmento e o deslocamento.
Obs.: A segmentação possibilita a expansão da pilha. Quanto ao segmento de pilha o hardware ele é capaz de perceber que o topo está no seu limite inferior. O
hardware sabe o tamanho do segmento, o hardware levanta interrupção e o SO trata e testa o tamanho da pilha. A segmentação possibilita o reconhecimento do estouro da pilha.
Obs.:
Resumo: Controle de memória com várias soluções e ficaram pendentes duas soluções: A segmentação e a paginação.
1.3.2) Aprofundando dos conhecimentos sobre Segmentação:
Segmentação cria uma nova forma de ver a memória como uma coisa bidimensional. Quais os mecanismos de hardware para fazer com que a memória seja vista como bidimensional? Pois a memória física de um computador tem só uma dimensão, começa no endereço 0 da memória e vai até um endereço fffffff. A memória que existe em uma máquina ainda é essa, apesar da segmentação mostrar para o programador, áreas como uma coisa bidimensional.
Memória
Existe uma coisa que é a que faz a intermediação entre a bidimensionalidade e a memória física, se chama Tabela de Segmentos.
Tabela de Segmentos: Tem no mínimo dois lados , ou seja, duas informações: O início do segmento(memória física) e o tamanho do segmento. Para um programa que tenha quatro segmentos (0, 1, 2 , 3), a tabela vai estar dizendo onde esse segmento começa e qual o tamanho dele.
Início Tamanho 0 10000 5000 1 20000 2000 2 25000 6000 3 35000 5000
O 1º segmento começa em 10000 e acaba com 15000 então tem tamanho 5000. O 2º segmento começa em 20000 e acaba com 22000 então tem tamanho 2000. O 3º segmento começa em 25000 e acaba com 31000 então tem tamanho 6000. O 4º segmento começa em 35000 e acaba com 40000 então tem tamanho 5000.
Interrupção O SO tratará
3000 É maior que 2000
A tabela de segmentos é para cada processo. Se tiver outro processo com dois segmentos e um dos dois segmentos está ativo em um certo instante de execução, quando tiver executando o processo 2 estará sendo usado a tabela abaixo. Se eu fizer mov ds, 2 ,não consegue acessar, pois não há segmentos, no processo 2 então será gerado interrupção. Se usar deslocamento 6000 no segmento 0, vai abortar também. O processo só consegue mexer nos próprios segmentos. Isso evita que um bug no programa faça com que outro processo pare de executar.
Início Tam 0 5000 5060 1 15000 3000
Para compartilhar teria que ser feito com que dois processos apontem para o mesmo endereço inicial. O SO vê quantas áreas são e qual tamanho de cada área, então procura espaço na memória para cada segmento, pode ser junto ou separado. O benefício final é que um processo não veja a área de outro processo, não consiga alterar outro. Infelizmente a Microsoft quando criou o Windows 3.1 ela não fez assim, ela não dá uma tabela de segmento por processo, ela cria uma tabela de segmentos para todos os processos, isso é muito ruim, pois permite que um processo entre em um segmento de memória do outro, utiliza segmentação, mas não protege a memória. Além dos campos acima poderemos ter campos de proteção como permissão, alteração, leitura e executável. Vai dizer se aquele segmento permite alteração no dado. Por exemplo, em um segmento de código, não é alterável o conteúdo, mas poderá ser executado. Se o programador tentasse executar uma instrução de alteração em um segmento que não permite alteração também gera interrupção, então o processo é abortado. No caso da Intel que tem segmentação, quando altero o segmento que vamos utilizar, a CPU da Intel não só carrega o valor 2 de ds como carrega todos os dados de controle do segmento 2, isso faz com que não seja necessário ter a tabela de segmentos contida dentro da CPU, não precisará consultar memória, isso é otimização. Outra solução é carregar a tabela de segmentos na CPU, mas a tabela pode ser grande, tornando-se inviável. A tabela de segmentos é definida pelo SO, mas o programador define o tamanho do segmento, ás vezes é o compilador. O SO pode deslocar o segmento de lugar se fizer isso e alterar a tabela de segmentos, o processo não irá perceber que o segmento mudou de lugar. O segmento de pilha tem o tamanho, tem um início e um início real, quando é feito um push, esse push pode alcançar o início real, então ocorre interrupção que o SO trata, baixando um pouco do início real. Se for área de dados, vai abortar. Nos dois casos acontece interrupção, o que muda é o tratamento. O Windows permite a criação de outra área de dados. A tabela de segmentos fica onde? Os dados que ela contém apontam para PCB, a cada instrução que o PCB muda de endereço a CPU tem que validar se o
acesso é válido, e além disso tem que somar o início do segmento ao deslocamento, isso é feito a cada instrução que executa, isso é feito de forma rápida, mas para ser rápido, a tabela de segmentos tem que estar dentro do chip da CPU. O problema de fazer isso é que a tabela pode ser grande. A Intel coloca na memória, só que ficando na memória ficaria muito lento para fazer as validações e os cálculos a cada instrução que é executada pela CPU, então a Intel quando o programador indica que quer usar um certo segmento a CPU carrega o registrador dela (a parte escondida do registrador de segmento, o início, o tamanho) ficam contidos dentro da CPU ,então a validação é feita sem precisar ir a memória, portanto a execução de uma instrução vai ser rápida.
Problema da alocação de memória: Tenho uma memória disponível tem que alocar a memória dentro dessa memória disponível e o pedaço alocado tem que ficar indisponível. Com isso, cria situações que podem gerar o impedimento do surgimento de processos. Se tiver muitas alocações e desalocações o que pode acontecer é que terá muitas áreas disponíveis pequenas e separadas umas das outras, se eu precisar alocar uma nova área que seja maior que qualquer área dessa eu não vou conseguir, apesar da soma de cada área ser maior do que a área que eu quero alocar. Esse problema se chama Fragmentação de Memória.
Como se resolve esse problema?
Como resolver (1)? Deslocamento de todas as áreas para acabar com os buracos; Juntaria todas as áreas e formaria um espaço livre maior capaz de ser alocado para um novo processo. Isso é chamado de compactação.
Exemplo: A memória contém cinco processos(P1, P2, P3, P4 e P5) e entre eles tem uma área livre. A compactação vai juntar essas áreas. Na tabela de segmento o ínicio do processo P5, que antes era 40000 passa a ser 32000, o seu tamanho permanece em 5000, assim como se trata do último processo alocado na memória, o endereço inicial da área livre é 37000.
Memória Memória
A outra forma é tentar evitar (2) que a fragmentação ocorra e para isso existem algoritmos, e cada um tem seu objetivo, para a escolha de área livre, que é uma área que minimize a fragmentação.
Algoritmos para evitar a Fragmentação da Memória:
Memória
No esquema acima têm cinco áreas livres, cada uma com os seguintes tamanhos: 3000, 5000, 8000, 4000, 2000 respectivamente. Se o processo alocar uma área de 4000 na memória não haveria fragmentação, pois ele ocuparia a área livre de tamanho 4000. O problema é se tentar alocar um bloco de 3500, pois sobrará um espaço de 500, pequeno demais para poder ser alocado por um outro bloco, é o que o algoritmo Best Fit faz. O que pode ser feito é tentar alocar em um bloco maior e assim sobrar um pedaço maior para que possa ser alocado por outro bloco, por exemplo, alocar no bloco de 8000 sobrando 4500, que é o que o algoritmo Worst Fit faz. Mas, agora se quiser alocar um processo de 6000, por exemplo, não tem mais área com tamanho maior ou igual para este processo, o que vai ter que fazer é a compactação. Não existe um algoritmo prático para alocação. Pode ser uma coisa custosa procurar o espaço que melhor se adapta. Por isso, existe um outro algoritmo que coloca o processo no primeiro espaço livre encontrado que couber este processo. Este algoritmo é o First Fit. Dentro do sistema operacional existe uma lista encadeada com os espaços livres da memória para controlar esses espaços livres. Como mostra o esquema
abaixo: O endereço de início 0 e tamanho 3000 aponta para o endereço inicial 10000 de tamanho 5000 e este aponta o endereço inicial 25000 de tamanho 8000 e este aponta para o endereço inicial 40000 de tamanho 4000 e por último o endereço inicial 60000 de tamanho 2000.
Resumo: Os problemas e as soluções da alocação de memória são utilizados tanto para processos como para variáveis dinâmica, onde quem controla isto é o sistema operacional e a biblioteca padrão, e nesta última contém as informações sobre o que foi alocados na heap e etc.
1.4) Paginação:
Para micro o primeiro chip da Intel que tinha este mecanismo foi o 386 e o primeiro sistema operacional que começou a usar a paginação foi o Windows 3.0. O que é esse mecanismo? Resposta: Passa a existir duas formas diferentes de olhar a memória: espaço do endereçamento lógico e o espaço do endereçamento físico. Existe um mapeamento das páginas lógicas e das páginas físicas. A ordem do endereçamento lógico é diferente da ordem do endereçamento físico. E os espaços do endereçamento lógicos podem refletir no espaço do endereçamento físico. Cada processo tem o seu espaço de endereçamento lógico. Como mostra o exemplo abaixo que possui processos (1, 2 e 3) cada um com o seu espaço de endereçamento lógico. E cada processo acha que está sozinho na memória, e só vê o seu espaço de endereçamento lógico. Enquanto existir espaço disponível na memória poderá se alocado um espaço de endereçamento lógico para outros processos ou aumentar o tamanho da pilha. Tem endereços lógicos que correspondem fisicamente e têm outros que não.
Hoje em dia é mecanismo mais sofisticado, mas exige muito do hardware. Hardwares antigos não são capazes de fazer a paginação. O chip da Intel desde 1985 já tem este recurso. A Microsoft começou a usar a paginação em 1994, com Windows NT, no Windows 3.1 era bem primária. Em 1995, o Windows 95.
Instrução: Mov AX,[4100]
Div Resto da Divisão
Onde: Div = divisão por 4096 (nº da página) – Parte Alta dos bits acima; Resto da divisão = resto da divisão por 4096 (deslocamento da página) –Parte Baixa.
Logo:
Nº da Página Deslocamento 1 4 00001 000 000 000 100
Quem preenche a tabela de página é o sistema operacional quando aloca o processo. Cada processo tem uma tabela de página diferente.
O que o hardware faz é lê a tabela acima e pegar o mapeamento da posição real na memória a cada instrução executada. Em outras palavras, quem executa a conversão de virtual para real na tabela de página é o hardware. A tabela contém o mapeamento de virtual para real.
Endereço Lógico: ... 000 000 001 000 000 000 1000
Endereço Físico: ... 000 000 010 000 000 000 100
O grande problema da paginação é a ida duas vezes à memória: uma para fazer a conversão do endereço físico para o lógico e vice-versa e a outra para acessar o dado e jogar para o registrador. Por exemplo, se tenho 40000 Mb , então tenho 10 Kbytes para cada página. Isso deixa a CPU duas vezes mais lenta. A tabela de página fica no endereço físico, fica em memória e pode ocupar mais de uma página. Como esta tabela é muito grande para ficar dentro do chip da CPU então ela fica na memória no endereço física. Existe um mecanismo que torna o acesso a memória mais rápido que é TLB(Translation Look Aside Buffer). Translation Look Aside Buffer(TLB) resolve o problema descrito acima, que é uma tabela reduzida com as páginas mais acessadas. Esta tabela localiza-se internamente no chip da CPU.
Estrutura da TLB:
Nº da Página Lógica Nº da Página Física Válido 1 2 1 2 6 0 1
O hardware antes de fazer a conversão ele vai à TLB buscar o mapeamento caso exista da tabela de página.
**Exemplo de outras Instruções:
O sistema operacional ao escalonar outro processo zera a TLB. Tem uma instrução que faz isso.
1º problema : Perda de Velocidade Solução : TLB
Vamos fazer um cálculo para saber a perda de performance que a paginação pode ter utilizando a TLB: Exemplo : Tempo de acesso a memória = 100ns Tempo de acesso a TLB = 10 ns Duas situações podem ocorre para instruções que vai a memória: