Você está na página 1de 87

SISTEMAS

OPERACIONAIS

JANEIRO 2013

Sumário

1 Introdução

5

1.1 O

Hardware

 

5

1.2 O

Software

5

1.3 Máquina de Níveis

5

1.4 O Sistema Operacional

6

1.4.1 O Sistema Operacional como uma Máquina Estendida

6

1.4.2 O Sistema Operacional como um Gerenciador de Recursos

6

1.5

Histórico dos Sistemas Operacionais

7

1.5.1 1ª Geração (1945-1955) - válvulas e painéis de conectores

7

1.5.2 2ª Geração (1955-1965) - transistores e sistemas de lote

7

1.5.3 3ª Geração (1965-1980) - circuitos integrados

8

1.5.4 4ª Geração (1980-hoje) - integração em larga escala

8

1.6 Conceitos

de

Hardware

8

1.7 Conceitos

de

Software

10

1.8 Conceitos de Sistema Operacional

10

1.8.1 Processos

 

10

1.8.2 Arquivos

11

1.8.3 Interpretador de Comandos (Shell)

11

1.8.4 Chamadas de Sistema

12

1.9

Tipos de Sistemas Operacionais

14

1.9.1

Multiprocessamento

15

1.10 Sistemas Multiprogramáveis

16

1.11 Estrutura do Sistema Operacional

17

1.11.1 Modos de Operação

17

1.11.2 Ativação e Desativação do Sistema

17

1.11.3 Sistemas

Monolíticos

18

1.11.4 Sistemas em Camadas

18

1.11.5 Máquinas Virtuais

19

1.11.6 Sistemas Microkernel

20

1.12

Exercícios

 

20

2

Processos

22

2.1 Mudança de Contexto

23

2.2 Subprocesso e Thread

24

2.3 Tipos de Processos

25

2.4 Processos Cooperativos

25

2.5 Exercícios

26

3 Comunicação Entre Processos

27

3.1 Condição de Corrida

27

3.2 Região Crítica

28

3.3 Sincronização Condicional

28

3.4 Exclusão Mútua por Hardware

29

3.4.1 Desabilitação de Interrupções

29

3.4.2 Instrução TSL (Test and Set Lock)

29

3.5

Exclusão Mútua por Software com Espera Ativa

30

3.5.1

Alternância Estrita

30

 

3.5.2

Solução de Peterson

31

3.6

Exclusão Mútua por Software sem Espera Ativa

31

3.6.1 Semáforos

31

3.6.2 Monitores

33

3.6.3 Troca de Mensagens

35

3.7

Deadlock

37

3.7.1 Prevenção de Deadlock

37

3.7.2 Detecção do Deadlock

38

3.7.3 Correção do Deadlock

38

3.8

Problemas Clássicos de Comunicação Interprocessos

38

3.8.1 Problema dos Filósofos Comendo

38

3.8.2 Problema dos Leitores e dos Escritores

40

3.8.3 Problema do Barbeiro Adormecido

40

3.9

Exercícios

41

4 Gerência do Processador

43

4.1

Algoritmos de Escalonamento

 

44

4.1.1 Escalonamento First In First Out (FIFO)

44

4.1.2 Escalonamento Shortest Job First (SJF)

44

4.1.3 Escalonamento

Cooperativo

 

44

4.1.4 Escalonamento Circular (Round Robin)

44

4.1.5 Escalonamento

por

Prioridade

45

4.1.6 Escalonamento

por

Múltiplas

Filas

46

4.1.7 Escalonamento por Múltiplas Filas com Realimentação

46

4.1.8 Escalonamento em Sistemas de Tempo Real

47

4.2 Escalonamento com Múltiplas UCPs

 

47

4.3 Exercícios

47

5 Gerência de Memória

49

5.1 Atribuição de Endereços

49

5.2 Carregamento Dinâmico

49

5.2.1

Ligação Dinâmica

50

5.3 Contígua Simples

Alocação

50

5.4 Particionada

Alocação

50

5.4.1 Alocação

Particionada

Estática

50

5.4.2 Alocação

Particionada

Dinâmica

51

5.4.3 Estratégias para Escolha da Partição

51

5.5 Swapping

53

5.6 Memória Virtual

53

5.6.1 Espaço de Endereçamento Virtual

54

5.6.2 Paginação

54

5.6.3 Segmentação

60

5.6.4 com

Segmentação

Paginação

61

5.6.5 Segmentação com Paginação: O Pentium Intel

62

5.6.6 Proteção

63

5.6.7 Compartilhamento de Memória

63

5.6.8 Trashing

63

5.7

Exercícios

64

6

Sistema de Arquivos

67

6.1

Arquivos

67

6.1.1 Organização de Arquivos

 

67

6.1.2 Métodos

de Acesso

68

6.1.3 Tipos de

Arquivos

68

6.1.4 Operações de Entrada/Saída

69

6.1.5 Atributos

 

69

6.2 Diretórios

69

6.3 Alocação de Espaço em Disco

 

70

Alocação

6.3.1 Contígua

71

6.3.2 Alocação

por

Lista

Encadeada

71

6.3.3 Alocação

por

Lista

Encadeada Utilizando Índice

72

6.3.4 Nós-I

(Alocação Indexada)

72

6.4

Exemplos de Implementação de Diretórios

73

6.4.1 Diretórios no CP/M

 

73

6.4.2 Diretórios no MS-DOS

73

6.4.3 Diretórios no UNIX

74

6.5

Proteção de Acesso

74

6.5.1 Senha de Acesso

74

6.5.2 Grupos de Usuários

74

6.5.3 Lista de Controle de Acesso

75

6.6 Implementação de Cache

 

75

6.7 Exercícios

 

75

7 Gerência de Dispositivos

77

7.1

O Hardware de Entrada/Saída

77

7.1.1 Espera em Ciclo

78

7.1.2 Interrupções

79

7.1.3 Acesso Direto à Memória

79

7.2 Operações de Entrada/Saída

79

7.3 Subsistema de Entrada/Saída

80

7.4 Drivers de Dispositivo

80

7.5 Controladores

81

7.6 Dispositivos de Entrada/Saída

82

7.6.1 Discos de RAM

82

7.6.2 Disco Rígido

82

7.7

Exercícios

85

8 Bibliografia

87

1 Introdução

1.1 O Hardware

Um sistema computacional moderno consiste em:

1 ou mais processadores;

memória RAM;

disco;

interface de rede;

impressora;

placa de vídeo;

etc.

É extremamente difícil escrever um programa para controlar todos os dispositivos. É preciso

isolar o programador do hardware.

Solução: Colocar uma camada de software por sobre o hardware básico para gerenciar o sistema e oferecer ao usuário uma máquina virtual.

1.2 O Software

O software permite ao computador recuperar, armazenar e processar informações.

De forma geral pode ser dividido em duas espécies: programas de sistema e programas aplicativos. Os programas de sistema gerenciam a operação do computador, enquanto os programas aplicativos executam o trabalho que o usuário deseja.

O sistema operacional é o principal programa de sistema. Ele fornece a base para que programas aplicativos possam ser escritos.

1.3 Máquina de Níveis

O hardware não tem muita utilidade isoladamente. São os programas (softwares) que realizam

as operações que os usuários desejam. O hardware é o responsável pela execução das instruções do software para que seja realizada alguma tarefa.

Tanto o hardware quanto o software são logicamente equivalentes, interagindo de forma única para o usuário.

Nos primeiros computadores, a programação era realizada em painéis, através de fios, o que era uma grande dificuldade para os programadores da época. A solução foi o surgimento do sistema

operacional, que tornou a interação entre usuário e computador mais simples, confiável e eficiente.

A partir desse acontecimento a parte física do computador tornou-se transparente para o usuário.

Partindo desse princípio, podemos considerar o computador como uma máquina de níveis, onde inicialmente existem dois níveis: o nível do hardware e o nível do sistema operacional. Assim o usuário pode enxergar a máquina como sendo apenas o sistema operacional (como se o hardware não existisse).

Um computador possui tantos níveis quantos forem necessários para adequar o usuário às suas diversas aplicações. Quando o usuário está trabalhando em um desses níveis, não necessita saber da existência das outras camadas.

Sistema Reserva de Navegador Bancário Passagem WEB Compila- Editores Shell dores Sistema Operacional Linguagem
Sistema
Reserva de
Navegador
Bancário
Passagem
WEB
Compila-
Editores
Shell
dores
Sistema Operacional
Linguagem de Máquina
Microprograma
Dispositivos Físicos

Programas aplicativos

Programas de sistema

Hardware

Os programas de sistema que executam acima do Sistema Operacional executam no modo usuário, enquanto o sistema operacional executa no modo kernel (ou modo supervisor), sendo protegido do usuário pelo hardware.

A linguagem de máquina não é um hardware propriamente dito, mas considera-se que ela faz

parte do hardware.

O microprograma só existe em máquinas CISC. Ele tem a função de interpretar a linguagem

de máquina.

1.4 O Sistema Operacional

O sistema operacional é um programa que atua como um intermediário entre o usuário e o

hardware. Seu propósito é fornecer um ambiente no qual os usuários possam executar seus programas e tem como principal objetivo tornar conveniente o uso de um sistema computacional usando o hardware de maneira eficiente.

O sistema operacional realiza basicamente duas funções sem muita correlação, funcionando

como uma máquina estendida e como um gerenciador de recursos.

1.4.1 O Sistema Operacional como uma Máquina Estendida

A arquitetura da maior parte dos computadores no nível de linguagem de máquina é difícil de

programar (principalmente E/S). Para escrever em um disco, por exemplo, é preciso escolher a trilha, a cabeça e o setor, formatar, inicializar a controladora e o dispositivo, etc., tudo isto escrevendo em registradores.

O sistema operacional é o programa que esconde do programador a complexidade do hardware, fornecendo-lhe uma interface bastante simples.

O sistema operacional também oculta detalhes relacionados a interrupções, temporizadores,

gerenciamento de memória e outros recursos de baixo nível.

Deste ponto de vista, a função do sistema operacional é apresentar ao usuário uma máquina estendida (ou máquina virtual), que apesar de equivalente ao verdadeiro hardware é mais fácil de programar. Ele facilita o acesso aos recursos do sistema.

1.4.2 O Sistema Operacional como um Gerenciador de Recursos

Computadores consistem em processadores, memórias, temporizadores, discos, etc. Sob um ponto de vista alternativo ao anterior, a função do sistema operacional é oferecer um esquema de alocação ordenada e controlada dos dispositivos que compõem o computador, entre os programas que competem por ele.

Exemplo: Acesso à impressora.

Quando um computador tem múltiplos usuários a necessidade de gerenciar e proteger a memória e outros recursos é ainda maior, para evitar que um usuário interfira no trabalho de outro.

Este ponto de vista sustenta que a tarefa principal do sistema operacional é monitorar quem está utilizando qual recurso, atender requisições de recursos, medir a utilização dos recursos e medir as requisições conflitantes de diferentes processos.

1.5 Histórico dos Sistemas Operacionais

A história dos sistemas operacionais está intimamente relacionada à história da arquitetura das

máquinas nas quais eles rodam.

Os primeiros sistemas operacionais foram desenvolvidos para facilitar o uso do hardware. A

medida que os sistemas operacionais foram desenvolvidos tornou-se evidente que mudanças no projeto de hardware poderia simplificá-los. Desta forma, os sistemas operacionais não só foram influenciados pela arquitetura dos computadores, como também influenciaram no desenvolvimentos destas arquiteturas.

1.5.1 1ª Geração (1945-1955) - válvulas e painéis de conectores

Um único grupo de pessoas projetava, construía, programava e operava cada máquina. Toda a programação era feita em linguagem de máquina ligando fios aos painéis de conectores (não existia sequer a linguagem Assembly).

Não existia o conceito de sistema operacional.

O ENIAC (Electronic Numerical Integrator and Computer) foi o primeiro computador

eletrônico digital de propósito geral. Ele possuía 18.000 válvulas, 10.000 capacitores, 70.000

resistores e pesava 30 toneladas. Quando em operação consumia 140 quilowatts e era capaz de realizar 5.000 adições por segundo.

O UNIVAC I (Universal Automatic Computer) foi a primeira máquina comercial a ser

construída, criada especialmente para o censo americano de 1950.

1.5.2 2ª Geração (1955-1965) - transistores e sistemas de lote

Com a invenção do transistor os computadores se tornaram menores, mais confiáveis, mais rápidos e passaram a consumir menos energia. As memórias de núcleo magnético foram substituídas por memórias mais rápidas e com maior capacidade de armazenamento.

e vendidos com a expectativa de uma

durabilidade maior.

Nesta geração surgiram as primeiras linguagens de programação. Para executar um “job” o programador escrevia seus programas em cartões perfurados (Fortran, Assembly ou Cobol). Os usuários não interagiam diretamente com o sistema o operador pegava o conjunto de cartões do programador e os carregava na máquina junto com o conjunto de cartões do compilador. O resultado era gerado em uma impressora.

O sistema operacional destas máquinas tinha a função de carregar os cartões em memória e

transferir o controle da máquina para o programa recém carregado. Os sistemas operacionais passaram a ter seu próprio conjunto de rotinas para operações de entrada e saída (E/S), facilitando o trabalho dos programadores.

Para agilizar o processamento, serviço com necessidades semelhantes eram agrupados formando um mesmo lote (batch), e executados como um grupo.

Neste ambiente a UCP ficava quase sempre ociosa devido à velocidade dos mecanismos de

E/S.

Os

computadores

passaram

a

ser

fabricados

As máquinas desta geração executavam cerca de 200.000 instruções por segundo.

1.5.3

3ª Geração (1965-1980) - circuitos integrados

No início da década de 60 a maioria dos fabricantes de computadores tinham duas linhas de produtos, uma linha científica e uma linha comercial.

A IBM desenvolveu o Sistema 360 (System 360), uma família de máquinas compatíveis a

nível de software, onde elas diferiam em preço e performance. Foi preciso desenvolver um sistema operacional que executasse em todos os modelos, sendo bom tanto em ambientes científicos quanto

em ambientes comerciais.

O OS/360 era um sistema operacional muito grande escrito em Assembly que continha muitos

problemas, necessitando de um fluxo contínuo de versões. Com ele foram introduzidos os conceitos de multiprogramação, spool (para entrada e saída de jobs) e tempo compartilhado (com terminais on-line).

Em 1969 surge o sistema operacional UNIX, construído com base no MULTICS.

As máquinas desta geração executavam cerca de 5 milhões de instruções por segundo.

1.5.4 4ª Geração (1980-hoje) - integração em larga escala

O desenvolvimento de circuitos LSI e VLSI permitiu o desenvolvimento de computadores

pessoais. Surgiram também as estações de trabalho (computadores pessoais mais poderosos,

geralmente ligados em rede).

Surgiram os sistemas operacionais DOS (Disk Operation System) e VMS (Virtual Memory System).

Surgiram os conceitos de multitarefa e multiprocessamento, assim como máquinas com mais de um processador, impulsionados por aplicações que exigiam uma grande quantidade de cálculos.

Com a difusão das WANs foram desenvolvidos vários protocolos de rede para a interconexão de computadores. Os softwares de rede passaram a estar intimamente relacionados ao sistema operacional e surgem os sistemas operacionais de rede.

As máquinas mais modernas desta geração executam bilhões de instruções de ponto flutuante por segundo.

1.6 Conceitos de Hardware

Memória Unidade Lógica Entrada Unidade de e Aritmética Controle Saída UCP
Memória
Unidade Lógica
Entrada
Unidade de
e Aritmética
Controle
Saída
UCP

A unidade central de processamento (UCP), ou simplesmente processador, controla as

funções realizadas por cada unidade funcional do restante do sistema. Ela executa as instruções dos programas através de duas unidades básicas: unidade de controle (UC) e unidade lógica e

aritmética (ULA). Sua velocidade é medida pelo número de instruções que ela executa por segundo (MIPS e FLOPS).

A unidade lógica e aritmética tem por função realizar operações sobre os dados que lhe são

entregues, enquanto cabe à unidade de controle enviar sinais de controle a todas as partes do sistema para que essas partes possam funcionar de forma coordenada a fim de executar as instruções que são entregues à UCP.

Barramento

de dados Acumulador RDM Memória Registradores Principal REM Barramento de endereços CI RI ULA Decodificador
de dados
Acumulador
RDM
Memória
Registradores
Principal
REM
Barramento
de endereços
CI
RI
ULA
Decodificador
de instruções
UC
Clock
Unidade Lógica
Unidade de
e Aritmética
Controle
Barramento
interno

O relógio (clock) gera pulsos periódicos para sincronizar o sistema. O sinal do relógio é usado

pela unidade de controle para a execução das instruções. Em conjunto com a UCP determina a velocidade do sistema.

Registradores são dispositivos de alta velocidade localizados na UCP para armazenamento temporário de dados. O número de registradores varia de acordo com a arquitetura da UCP. Existem registradores de uso específico e registradores de uso geral. Alguns exemplos de registradores de uso específico são: contador de instruções - CI (program counter - PC), apontador da pilha - AP (stack pointer SP) e registrador de estado - RE (program status word - PSW).

Na memória principal são armazenados instruções e dados durante a execução de um programa. É composta por unidades chamadas células, que contém bits. A célula é acessada através de seu endereço. Uma célula é endereçada através do registrador de endereço de memória - REM (memory address register - MAR) e do registrador de dados da memória - RDM (Memory Buffer Register - MBR), ambos localizados na UCP.

As memórias podem ser classificadas, de acordo com a sua volatilidade, em: R/W (RAM), ROM, PROM, EPROM e EEPROM. Podem ainda ser estáticas ou dinâmicas.

A memória cache é uma memória volátil de alta velocidade que tem seu uso limitado devido

ao alto custo. Ela possui algoritmo próprio de operação, podendo ser separada em cache de instruções e cache de dados.

A memória secundária é um meio não volátil de armazenamento de programas e dados. Tem

acesso lento quando comparado à memória principal. Disco magnético, CD-ROM e unidade de fita são exemplos de memória secundária.

Os dispositivos de entrada e saída (E/S) permitem a comunicação entre o computador e o mundo externo. Existem duas categorias de dispositivos de E/S: dispositivos utilizados como memória secundária e dispositivos utilizados para a comunicação homem-máquina.

O barramento é um conjunto de condutores onde trafegam dados, endereços e sinais de controle, interligando os vários componentes do sistema. Exemplos de barramento para microcomputadores incluem: ISA, EISA, PCI, PCI-Express, AGP, USB, SCSI e SATA.

Processadores RISC (Reduced Instruction Set Computer) se caracterizam por possuírem poucas instruções, que são executadas diretamente pelo hardware. Costumam possuir um grande número de registradores. Exemplos de processadores RISC são Sparc (Sun), RS-6000 (IBM), Alpha (DEC) e o MIPS.

Processadores CISC (Complex Instruction Set Computer) possuem instruções complexas que são interpretadas pelo microprograma da UCP e costumam possuir um pequeno número de registradores. Exemplos de processadores CISC são a linha x86 e Pentium da Intel e a linha 68xxx da Motorola.

1.7 Conceitos de Software

O hardware do computador tem pouca utilização prática se não houver um conjunto de

softwares que permitam ao usuário utilizar a máquina.

O tradutor faz o mapeamento de uma linguagem em outra. Quando traduz da linguagem

Assembly para um código objeto é conhecido como montador (assembler). Quando traduz de uma linguagem de alto nível para um código objeto é conhecido como compilador.

O interpretador é um software que no momento da execução do programa traduz cada

instrução e a executa. A maior desvantagem dos interpretadores é o tempo gasto com a tradução no

momento da execução. Sua vantagem é permitir tipos de dados dinâmicos.

O linker é o responsável por gerar, a partir de um ou mais módulos objeto, um único

programa executável. Ele deve procurar em bibliotecas as funções referenciadas nos objetos.

O

loader é o programa responsável por carregar um programa em memória para execução.

O

depurador (debugger) é um utilitário que permite ao usuário controlar a execução de um

programa a fim de detectar erros.

A linguagem de máquina é a linguagem que a UCP entende e pode ser processada

diretamente por seu hardware.

O microprograma é um programa interno às UCPs CISC que interpreta a linguagem de

máquina destas UCPs. Processadores microprogramáveis permitem a criação de novas instruções através da modificação de seu microprograma.

1.8 Conceitos de Sistema Operacional

A interface entre o sistema operacional e os programas de usuário é definida pelo conjunto de

instruções estendidas que o sistema operacional proporciona. Estas instruções estendidas são

conhecidas por chamadas de sistema (ou system calls).

As chamadas de sistema variam de um sistema operacional para outro.

1.8.1 Processos

Um processo é basicamente um programa em execução mais seu espaço de endereçamento (incluindo valores de variáveis) e conjunto de registradores.

Associado a cada processo está seu espaço de endereçamento (ou imagem de núcleo). O espaço de endereçamento é uma lista de endereços da memória onde o processo pode ler e escrever, contendo o programa executável, os dados e a pilha.

Também associado com cada processo está um conjunto de registradores (contador de instruções, ponteiro da pilha, etc.).

Periodicamente o sistema operacional pode parar de executar um processo e começar a executar outro. Mais tarde o processo deve ser reiniciado no mesmo estado em que estava quando parou. As informações sobre o processo são armazenadas em uma tabela conhecida como tabela de processos.

As principais chamadas do sistema de gerenciamento de processos são as que lidam com a criação e com o encerramento de processos. Se um processo pode criar um ou mais processos filho, que podem criar novos processos filho, obtém-se uma árvore de processos.

Processos que cooperam para executar determinada tarefa costumam se comunicar. Essa comunicação é chamada de comunicação interprocessos.

Ocasionalmente é necessário transmitir informações para um processo que não as está aguardando. Neste caso envia-se um sinal ao processo.

A cada usuário do sistema corresponde um uid (identificação de usuário). Cada processo tem

a uid do usuário que o iniciou.

1.8.2 Arquivos

Uma das funções do sistema operacional é esconder as peculiaridades dos discos e outros dispositivos de E/S. São necessárias chamadas de sistema para criar, remover, ler e escrever arquivos.

Para fornecer um local para manter os arquivos existe o conceito de diretório. São necessárias chamadas de sistema para criar e remover diretórios, assim como inserir e remover arquivos de diretórios. As entradas de diretórios podem ser arquivos ou diretórios.

Cada arquivo dentro da hierarquia de diretórios pode ser especificado por seu nome de caminho a partir do diretório raiz (por seu caminho absoluto) ou por seu caminho a partir do diretório de trabalho atual (caminho relativo). Processos podem mudar seu diretório de trabalho emitindo uma chamada de sistema.

Arquivos e diretórios costumam ser protegidos. As permissões são verificadas no momento em que é solicitada a abertura de um arquivo. Se o acesso for permitido é devolvido um número inteiro chamado descritor de arquivo.

Alguns sistemas operacionais permitem acesso a um sistema de arquivos somente se ele estiver montado em um sistema de arquivos raiz ou primário.

Existe um recurso denominado pipe que é um tipo de pseudo-arquivo que pode ser utilizado para conectar dois processos.

1.8.3 Interpretador de Comandos (Shell)

Um dos componentes mais importantes de um sistema operacional é o interpretador de comandos (shell), que constitui a interface entre o usuário e o sistema operacional. Alguns sistemas operacionais incluem o interpretador de comandos em seu núcleo, enquanto outros tratam o interpretador de comandos como um programa especial.

Uma forma usual de requerer que uma tarefa seja realizada pelo sistema operacional é por meio do uso de comandos de controle. Nestes sistemas, um programa que lê e interpreta comandos de controle é automaticamente executado. Tal programa é frequentemente conhecido como a shell (casca) do sistema. Sua tarefa é bastante simples: obter o próximo comando e providenciar para que ele seja executado.

Nesse aspecto, os sistemas operacionais costumam ser bastante diferentes uns dos outros. Um exemplo de interface amigável pode ser encontrado no Macintosh e no Windows, com janelas, menus e mouse. Outros usuários preferem interpretadores de comandos com interfaces mais poderosas, complexas e mais difíceis de aprender, onde são digitados comandos que são mostrados em uma tela ou terminal. As interfaces dos interpretadores de comandos do UNIX e do MS-DOS são desta forma.

O interpretador de comandos, faz uso pesado de muitos recursos do sistema operacional,

sendo a interface primária entre o usuário e o sistema operacional.

Exemplos de comandos:

date

date > arq

sort <arq1 >arq2

cat arq1 arq2 arq3 | sort > /dev/lp

cat arq1 arq2 arq3 | sort > /dev/lp &

1.8.4 Chamadas de Sistema

A interação entre os processos dos usuários e o sistema operacional é feita por meio de chamadas de sistema (system calls). Em geral as chamadas de sistema estão disponíveis em linguagem Assembly, mas alguns sistemas permitem que as chamadas de sistema sejam feitas diretamente em uma linguagem de alto nível.

Serão vistos alguns exemplos de chamadas de sistemas do padrão POSIX divididos em seis categorias:

gerenciamento de processos;

sinais;

gerenciamento de arquivos;

gerenciamento de diretório;

proteção;

gerenciamento de tempo.

Chamadas de sistema para gerenciamento de processos:

pid = fork()Cria um processo filho.

pid = waitpid(pid, &statloc, opts) Bloqueia até que o processo filho termine.

s = wait(&status) Bloqueia até que um processo filho termine. Obsoleta.

s = execve(name, arg, envp) Substitui a imagem de memória por um processo.

exit(status) Termina o processo.

size = brk(addr) Especifica o segmento de dados.

pid = getpid()Retorna o id do processo.

pid = getpgrp()Retorna o id do grupo do processo.

l = ptrace(req, pid, addr, data) Permite controlar a execução de outro processo.

Exemplo de um shell simples:

while (1) { ler_comando(comando, parametros); if (fork() != 0) waitpid(-1, &status, 0); /* código pai */ else execve(comando, parametros, 0); /* executa comando */

}

Chamadas de sistema para sinalização:

s = sigaction(sig, &act, &oldact) Especifica a ação a ser executada para um sinal.

s = sigreturn(&context) Retorna de uma rotina de tratamento de sinal.

s = sigprocmask(how, &set, &old) Altera a lista de sinais bloqueados.

s = sigpending(set) Retorna o conjunto de sinais pendentes por loqueio.

s = sigsuspend(mask) Suspende o processo até que chegue um dos sinais selecionados.

s = kill(pid, sig) Envia um sinal a um processo.

residual = alarm(seconds) Programa um sinal SIGALARM.

s = pause() Suspende o processo até a chegada de um sinal.

Exemplo:

void handler(int sig)

{

printf("Terminada a execucao\n");

exit(0);

}

main()

{

signal(SIGINT, handler); while (1);

}

Chamadas de sistema para gerenciamento de arquivos:

fd = create(name, mode) Cria um novo arquivo. É uma chamada obsoleta.

fd = mknod(name, mode, addr) Cria arquivos especiais.

fd = open(file, how,

s = close(fd) Fecha o arquivo.

n = read(fd, buffer, nbytes) Lê dados de um arquivo.

n = write(fd, buffer, nbytes) Grava dados em um arquivo.

pos = lseek(fd, offset, whence) Move o ponteiro do arquivo.

s = stat(name, &buf) Retorna informações sobre um arquivo.

s = fstat(fd, &buf) Retorna informações sobre um arquivo aberto.

fd = dup(fd) Cria cópia do descritor de arquivos.

s = pipe(&fd[0]) Cria um par de descritor de arquivos ligados por um pipe.

s = ioctl(fd, request, argp) Operações em arquivos especiais.

f = access(name, amode) Verifica se o processo tem permissão para acessar o arquivo.

s = rename(old, new) Muda o nome do arquivo.

s = fcntl(fd, cmd,

) Abre um arquivo, criando se necessário.

) Executa operações especiais em arquivos.

Exemplo de configuração de pipeline entre 2 processos:

pipeline (char *process1, char *process2)

{

int fd[2];

pipe(&fd[0]);

if (fork() != 0) { /* processo pai */

close(fd[0]);

close(STD_OUTPUT);

dup(fd[1]);

close(fd[1]);

execve(process1, NULL, 0);

}

else { /* processo filho */

close(fd[1]);

close(STD_INPUT);

dup(fd[0]);

close(fd[0]);

execve(process2, NULL, 0);

}

Chamadas de sistema para gerenciamento de diretórios:

s = mkdir(name, mode) Cria diretório.

s = rmdir(name) Remove diretório vazio.

s = link(name1, name2) Cria uma nova entrada para um arquivo em um diretório.

s = unlink(name) Remove uma entrada de diretório.

s = mount(special, name, flag) Monta um sistema de arquivos.

s = umount(special) Desmonta um sistema de arquivos.

s = sync()Força a escrita de blocos em cache no disco.

s = chdir(dirname) Muda o diretório de trabalho.

s = chroot(dirname) Muda o diretório raiz.

Chamadas de sistema para proteção:

s = chmod(name, mode) Muda as permissões de um arquivo.

uid = getuid()Retorna o id do usuário do processo.

gid = getgid()Retorna o id do grupo do usuário do processo.

s = setuid(uid) Muda o id do usuário do processo.

s = setgid(gid) Muda o id do grupo do processo.

s = chown(name, owner, group) Muda o dono do arquivo.

oldmask = umask(complmode) Muda a máscara de criação de arquivos.

Chamadas de sistema para gerenciamento de tempo:

seconds = time(&seconds) Obtém a hora.

s = stime(tp) Ajusta o relógio.

s = utime(file, timep) Altera a hora de último acesso do arquivo.

s = times(buffer) Obtém quanto tempo de UCP o processo utilizou.

1.9 Tipos de Sistemas Operacionais

Os sistemas operacionais podem ser classificados em sistema monoprogramável/monotarefa, sistema multiprogramável/multitarefa e sistema com múltiplos processadores.

O sistema monoprogramável/monotarefa é voltado para a execução de um único programa. Para executar outro programa, o programa corrente em execução deve terminar antes. Processador, memória, periféricos e outros recursos são dedicados exclusivamente ao programa em execução. Nesses sistemas enquanto um programa aguarda por um evento o processador permanece ocioso. Caso a memória não seja totalmente utilizada pelo programa ela é subutilizada. Não existe preocupação com a proteção do sistema.

No sistema multiprogramável/multitarefa vários programas podem estar em execução “simultaneamente”. O sistema operacional se preocupa em gerenciar, de forma ordenada e protegida, o acesso concorrente aos seus recursos. Dependendo do número de usuários que podem usar o sistema, ele pode ser classificado como monousuário ou multiusuário. Um sistema monousuário é aquele em que somente um usuário pode estar utilizando o sistema, mesmo que executando mais de um programa em um determinado instante. Já um sistema multiusuário permite que mais de um usuário utilize o sistema simultaneamente. As vantagens dos sistemas multiprogramáveis são o aumento da produtividade dos usuários e a redução de custos a partir do compartilhamento dos diversos recursos do sistema.

Os sistemas multiprogramáveis/multitarefa podem ser classificados pela forma como suas aplicações são gerenciadas em:

Sistemas batch Programas ficam armazenados em discos ou fitas, esperando para serem executados sequencialmente.

Sistemas batch multiprogramados Programas ficam armazenados na memória. Quando um programa que está sendo executado e precisa esperar por uma tarefa, em vez da UCP ficar ociosa ela passa a executar outro programa, e assim por diante.

Sistemas de tempo compartilhado (time-sharing) São uma extensão da multiprogramação. Permitem a interação de usuários com o sistema através de terminais. Cada usuário recebe uma fatia do tempo do processador.

Sistemas de tempo real Semelhantes aos sistemas de tempo compartilhado, atendendo ao tempo de resposta exigido para as aplicações. O escalonamento não se dá por fatia de tempo, mas sim por prioridade.

Os

sistemas

com

múltiplos

processadores

são

estudados

no

tópico

a

seguir

(multiprocessamento).

1.9.1 Multiprocessamento

Um sistema multiprocessado é um sistema que possui mais de uma UCP, localizadas próximas uma das outras, se comunicando e compartilhando um barramento, por onde são transmitidos os dados entre os componentes do sistema computacional.

A principal vantagem do multiprocessamento é o aumento da quantidade de trabalho realizado por unidade de tempo (throughput) do sistema como um todo.

Um sistema multiprocessado pode ser desenvolvido para apresentar maior confiabilidade, sendo construído de forma que a parada de um processador não provoque a parada do sistema como um todo.

Os sistemas com múltiplos processadores caracterizam-se por possuir duas ou mais UCPs interligadas, trabalhando em conjunto. Em função da forma como as UCPs se comunicam e do grau de compartilhamento de memória e outros recursos, os sistemas com múltiplos processadores podem ser classificados em fortemente acoplados ou fracamente acoplados.

Nos sistemas fortemente acoplados existem dois ou mais processadores compartilhando uma única memória e controlados por um único sistema operacional. São geralmente utilizados no processamento de aplicações que fazem uso intensivo da UCP. Sua grande vantagem é o aumento do throughput. Subdividem-se ainda em sistemas simétricos e sistemas assimétricos.

Nos sistemas fortemente acoplados assimétricos existe um processador principal, responsável pelo controle dos demais processadores e pela execução do sistema operacional. O processador principal também é responsável por realizar todas as operações de entrada e saída.

Nos sistemas fortemente acoplados simétricos todos os processadores realizam a mesma função, exceto pela inicialização do sistema (boot), que é realizada por um único processador. O sistema operacional deve ser construído de forma a evitar conflitos entre os processadores.

Nos sistemas fracamente acoplados existem dois ou mais sistemas de computação conectados através de linhas de comunicação. Cada sistema possui seu próprio sistema operacional e funciona de forma independente. Sua utilização é caracterizada pelo processamento distribuído entre os vários processadores. Subdividem-se ainda em sistemas operacionais de rede e sistemas operacionais distribuídos.

No sistema operacional de rede cada nó possui seu próprio sistema operacional, podendo inclusive ser diferente dos demais. Caso haja perda da conexão entre os nós, os sistemas podem operar normalmente, podendo apenas ter alguns recursos indisponíveis. Permite, entre outras, cópia remota de arquivos, emulação de terminal, impressão remota, gerência remota e correio eletrônico. Os usuários estão cientes da existência de múltiplos computadores e podem se conectar a máquinas remotas.

No sistema operacional distribuído cada nó da rede possui seu próprio sistema operacional, mas com um relacionamento mais forte entre os nós. Geralmente os sistemas operacionais dos nós é o mesmo. Aparece para os usuários como um sistema tradicional. O usuário não sabe onde seus programas estão sendo executados e onde seus arquivos são armazenados. Permite que um aplicativo execute em mais de um processador. Os sistemas distribuídos podem ser considerados como uma evolução dos sistemas fortemente acoplados, onde uma aplicação pode ser executada por qualquer processador. Sua grande vantagem é a possibilidade de redundância do sistema.

Mesmo que um sistema não tenha programas que tenham sido escritos para tirar vantagem da máquina ter mais de um processador, é vantajoso ter mais de um processador na máquina, uma vez que isto permitirá que mais de um processo seja executado simultaneamente, aumentando o throughput do sistema.

1.10 Sistemas Multiprogramáveis

A possibilidade de periféricos e dispositivos funcionarem simultaneamente, juntamente com a

UCP, permitiu a execução de tarefas concorrentes, que é o princípio para projeto de sistemas

multiprogramáveis.

Nos sistemas multiprogramáveis vários processos podem estar residentes em memória concorrendo pela utilização da UCP. Quando um processo solicita uma operação de E/S, outros processos poderão estar disponíveis para utilizar a UCP.

A utilização concorrente da UCP deve ser implementada de maneira que, quando um processo

perde seu uso e depois retorna para continuar o processamento, seu estado deve ser idêntico ao do momento em que foi interrompido.

Durante a execução de um processo, alguns eventos podem ocorrer, obrigando a intervenção do sistema operacional. Este tipo de intervenção é chamado interrupção ou exceção. O que diferencia uma interrupção de uma exceção é o tipo de evento que gera esta condição.

Uma interrupção é gerada pelo sistema operacional ou por algum dispositivo e independe do programa que está sendo executado.

No momento em que a unidade de controle detecta a ocorrência de algum tipo de interrupção, o programa em execução é interrompido, e o controle é desviado para uma rotina responsável pelo tratamento da interrupção. Muitas vezes, após a execução dessa rotina, o controle deve voltar ao programa que estava sendo processado. Para isso acontecer é necessário que, no momento da interrupção, um conjunto de informações sobre a execução do programa seja preservado. Essas informações consistem no conteúdo de alguns registradores que deverão ser restaurados posteriormente para a continuação do programa.

Interrupção

Programa

para a continuação do programa. Interrupção Programa Salva os registradores Identifica a origem da

Salva os registradores

Identifica a origem

da interrupção

Obtém o endereço da

rotina de tratamento

interrupção Obtém o endereço da rotina de tratamento Rotina de tratamento Restaura os registradores No

Rotina de

tratamento

o endereço da rotina de tratamento Rotina de tratamento Restaura os registradores No momento que uma
o endereço da rotina de tratamento Rotina de tratamento Restaura os registradores No momento que uma
o endereço da rotina de tratamento Rotina de tratamento Restaura os registradores No momento que uma
o endereço da rotina de tratamento Rotina de tratamento Restaura os registradores No momento que uma

Restaura os

registradores

No momento que uma interrupção acontece, a UCP deve saber para qual rotina de tratamento deverá ser desviado o fluxo de execução. Essa informação está em uma estrutura do sistema chamada vetor de interrupção.

A interrupção é o mecanismo que tornou possível a implementação da concorrência nos computadores, sendo o fundamento dos sistemas multiprogramáveis.

Uma exceção é resultado direto da execução de uma instrução do próprio programa. O mecanismo de tratamento de exceções é semelhante ao de interrupções e podem ser escritas pelo próprio programador. Dessa forma, é possível evitar que um programa seja encerrado no caso de ocorrer, por exemplo, uma divisão por zero.

1.11 Estrutura do Sistema Operacional

O sistema operacional é formado por um conjunto de rotinas que oferecem serviços às

aplicações dos usuários do sistema, bem como a outras rotinas do próprio sistema. Esse conjunto de rotinas é chamado de kernel ou núcleo do sistema.

As principais funções do kernel são:

tratamento de interrupções;

criação e eliminação de processos;

sincronização e comunicação entre processos;

escalonamento e controle dos processos;

gerência de memória;

gerência do sistema de arquivos;

operações de E/S;

contabilização e segurança do sistema.

Para proteger o kernel, os usuários solicitam serviços através de chamadas de sistemas.

Chamada Aplicação Núcleo Hardware de Sistema
Chamada
Aplicação
Núcleo
Hardware
de
Sistema

1.11.1 Modos de Operação

Para que um sistema operacional possa trabalhar de forma segura é preciso existir um esquema de proteção que proteja o sistema operacional e todos os outros programas e dados de qualquer programa incorreto. Assim é necessário que o hardware ofereça pelo menos dois modos de operação para diferenciar os modos de execução. Estes modos são conhecidos como modo usuário e modo kernel. O modo kernel também é conhecido como modo supervisor ou modo privilegiado.

Existem instruções que só devem ser executadas pelo sistema operacional para impedir problemas de segurança do sistema. Instruções que tem o poder de comprometer o sistema são conhecidas como instruções privilegiadas, enquanto instruções que não oferecem perigo ao sistema são conhecidas como instruções não privilegiadas.

Quando o sistema está no modo usuário as aplicações podem executar somente instruções não privilegiadas. Quando o sistema está no modo kernel as aplicações podem executar qualquer instrução do processador.

Durante a inicialização do sistema (boot) o sistema está no modo kernel. Quando o sistema operacional é carregado ele inicia a execução de processos do usuário no modo usuário. Sempre que ocorre uma interrupção o modo de operação muda para o modo kernel e, antes de transferir o controle da máquina para um processo do usuário, ele coloca a UCP novamente no modo usuário.

Sempre que uma aplicação necessita de um serviço privilegiado ela deve fazer uma chamada de sistema. A chamada de sistema muda o modo de operação para o modo kernel, executando o serviço e voltando ao modo usuário.

Caso uma aplicação tente executar uma instrução privilegiada sem estar no modo kernel uma exceção será gerada.

1.11.2 Ativação e Desativação do Sistema

Toda vez que um computador é ligado, é necessário que o sistema operacional seja carregado da memória secundária para a memória principal. Esse processo é denominado ativação do sistema (boot).

Além da carga do sistema operacional, a ativação do sistema também consiste na execução de arquivos de inicialização. Nestes arquivos são especificados procedimentos de inicialização e configuração de hardware e software específicos para cada ambiente.

Na maioria dos sistemas também existe o processo de desativação (shutdown). Este procedimento permite que as aplicações e componentes do sistema sejam desativados de forma ordenada, garantindo a integridade do sistema.

1.11.3 Sistemas Monolíticos

Não existe uma estrutura real. O sistema operacional é escrito como uma coleção de procedimentos, cada um podendo chamar qualquer um dos outros. Os procedimentos são compilados e “linkados” em um único programa executável. Cada procedimento é visível aos demais.

É possível ter um mínimo de estrutura requisitando-se chamadas de sistema ao colocar os

parâmetros em locais bem definidos e chamar interrupções. A máquina é comutada para o modo kernel e o controle é transferido ao sistema operacional. Quando a chamada termina o controle é

devolvido ao programa de usuário.

Programas de

usuário executados

no modo usuário

Sistema operacional

executado no

modo kernel

Programa de usuário 2 Programa de usuário 1 4 1 3 2
Programa de usuário 2
Programa de usuário 1
4
1
3
2

1. Interrupção gerada para o kernel

2. S.O. determina o serviço necessário

3. S.O. chama o procedimento de serviço

4. Controle retorna para o programa de usuário

1.11.4 Sistemas em Camadas

São definidas camadas sobrepostas onde cada camada oferece um conjunto de funções que podem ser utilizadas pelas camadas superiores.

A vantagem da estruturação em camadas é isolar as funções do sistema operacional, criando

uma hierarquia e protegendo as camadas mais internas.

A depuração de erros também é simplificada. Como cada nível usa apenas operações fornecidas por níveis inferiores, o sistema operacional pode ser depurado da camada mais interna para a mais externa sem grandes dificuldades.

Um problema com a implementação em camadas é a queda de desempenho provocada no sistema, uma vez que qualquer operação deve passar por todas as camadas inferiores até chegar ao hardware.

5

4

3

2

1

0

Operador Programas de usuário Entrada/Saída Comunicação Gerência de memória Multiprogramação MULTICS
Operador
Programas de usuário
Entrada/Saída
Comunicação
Gerência de memória
Multiprogramação
MULTICS
Usuário Supervisor Executivo Kernel VMS
Usuário
Supervisor
Executivo
Kernel
VMS

Abaixo segue uma breve descrição das camadas do MULTICS.

A camada 0 lida com a alocação do processador alternando entre processos quando ocorrem

interrupções ou quando temporizadores expiram. Acima da camada 0 os processos não precisam se preocupar se existe mais de um processo sendo executado.

A camada 1 faz o gerenciamento de memória, alocando espaço para os processos. Acima dela

os processos não precisam se preocupar se estão em memória ou em disco.

A camada 2 manipula a comunicação de cada processo com a console do operador. Acima

dela cada processo tem seu próprio console de operador.

A camada 3 cuida de gerenciar os dispositivos de E/S. Acima dela cada processo pode lidar

com dispositivos de E/S abstratos amigáveis.

Na camada 4 residem os programas de usuários, que não precisam se preocupar com gerenciamento de processos, memória, console e E/S.

O processo do operador do sistema se localiza na camada 5.

1.11.5 Máquinas Virtuais

No hardware básico é executado o monitor de máquina virtual, oferecendo várias máquinas virtuais à camada superior. Tais máquinas virtuais são cópias exatas do hardware básico e cada uma pode executar qualquer sistema operacional.

Quando um processo executa uma chamada de sistema:

A chamada é interceptada pelo sistema operacional da máquina virtual;

O sistema operacional emite as instruções normais de E/S de hardware;

As instruções são interceptadas pelo monitor de máquina virtual que as executa simulando o hardware real.

processos kernel hardware
processos
kernel
hardware

Máquina sem monitor

de máquina virtual

processos processos processos kernel kernel kernel Monitor de máquina virtual hardware
processos
processos
processos
kernel
kernel
kernel
Monitor de máquina virtual
hardware

Máquina com monitor

de máquina virtual

Colocando a multiprogramação como responsabilidade do monitor de máquina virtual pode- se oferecer uma máquina estendida mais simples, flexível e fácil de manter.

O modelo de máquina virtual não só oferece a possibilidade de executar diferentes sistemas

operacionais em um mesmo hardware como também garante o isolamento total entre as máquinas,

fornecendo um excelente nível de segurança entre elas.

Em uma outra abordagem a máquina virtual pode ser criada com um subconjunto dos recursos da máquina real. Em uma camada mais baixa é executado um programa chamado exokernel, que atribui recursos às máquinas virtuais e garante que nenhuma máquina virtual utilize recursos de outra. Cada máquina virtual pode ter seu próprio sistema operacional. Tem a vantagem de economizar uma camada de mapeamento necessária ao monitor de máquina virtual, já que o monitor de máquina virtual precisa manter uma tabela dos recursos em uso por cada máquina virtual.

1.11.6 Sistemas Microkernel

Uma tendência nos sistemas operacionais modernos é mover o máximo do código para as camadas superiores, implementando parte das funções do sistema operacional em processos de usuário. Isto faz com que o núcleo seja menor e mais simples.

Para requisitar um serviço um processo de usuário (processo cliente) envia uma requisição a um processo servidor. O kernel gerencia a comunicação entre os processos.

gerenciar. Para trocar alguma

característica do sistema operacional basta modificar o servidor específico.

Tal

divisão

torna as

partes

menores

e

mais

fáceis

de

Se

algum servidor falhar o sistema operacional não ficará completamente comprometido.

O

modelo se adapta facilmente a sistemas com múltiplos processadores e sistemas

distribuídos. Quando o cliente se comunica com um servidor ele não precisa saber em que

processador ou máquina o servidor se encontra.

Nem todos os processos servidores podem executar em modo usuário, já que alguns precisam utilizar instruções privilegiadas (drivers, por exemplo). O kernel deve incorporar estas funções ou devem existir alguns processos servidores críticos executando em modo kernel. Outra solução é implementar mecanismos no kernel deixando a política para os servidores no espaço do usuário.

Processo cliente Processo cliente memória Servidor Processo servidor terminal Servidor arquivos Servidor kernel
Processo cliente
Processo cliente
memória Servidor
Processo servidor
terminal Servidor
arquivos Servidor
kernel

1.12 Exercícios

usuário Modo

Modo kernel

1)

Qual a diferença entre programas aplicativos e programas de sistema? O sistema operacional é um programa de sistema ou um programa aplicativo?

2)

Qual o objetivo em se instalar um sistema operacional em um computador?

3)

Quais as dificuldades que um programador teria no desenvolvimento de uma aplicação para um ambiente sem sistema operacional?

4)

Explique a seguinte afirmação: “O sistema operacional pode ser visto como uma máquina estendida ou como um gerenciador de recursos.”

5)

Explique o conceito de máquina estendida. Qual sua vantagem?

6)

Qual a função da unidade central de processamento (UCP) de um computador?

7)

Quais são os componentes de uma UCP e quais são suas funções?

8)

Qual a diferença entre registrador, memória cache, memória principal e memória secundária?

9)

O que são memórias voláteis e não voláteis?

10) Quais as diferenças entre processadores CISC e processadores RISC?

11) O que se entende por “ativação do sistema (boot)” e “desativação do sistema (shutdown)”.

12) Qual a diferença entre tradutor e interpretador?

13) Qual a diferença entre um compilador e um linker?

14) O que é um microprograma?

15) Por que o código objeto gerado por um compilador ainda não pode ser executado?

16) Por que a execução de programas interpretados é mais lenta do que a de programas compilados?

17) Qual a função do linker?

18) Qual a função do loader?

19) Como um depurador pode facilitar o trabalho de um programador?

20) O que são as chamadas de sistema de um sistema operacional?

21) O que é um interpretador de comandos (shell)?

22) O que diferencia um sistema operacional monoprogramável/monotarefa de um sistema operacional multiprogramável/multitarefa? Que tipo de importância cada um deles costuma dar à proteção do sistema?

23) Por que diz-se que um sistema operacional monoprogramável subutiliza o sistema?

24) Por que a proteção do sistema é tão importante para um sistema operacional multitarefa?

25) O que vem a ser um sistema com múltiplos processadores? Qual a vantagem?

26) É vantajoso ter mais de uma UCP em uma máquina somente se esta máquina executar programas aplicativos desenvolvidos para explorar o paralelismo da máquina? Explique.

27) O que vem a ser um sistema multiprogramável?

28) O que caracteriza um sistema batch?

29) Como funcionam os sistemas de tempo compartilhado? Qual a vantagem na sua utilização?

30) Qual a diferença entre sistemas de tempo compartilhado e de tempo real?

31) Quando se deve utilizar um sistema operacional de tempo real?

32) Qual a diferença entre sistema multiprocessados simétricos e assimétricos?

33) Qual a importância do mecanismo de interrupção para um sistema operacional multitarefa?

34) Qual a diferença entre uma interrupção e uma exceção?

35) Explique o funcionamento do mecanismo de interrupções.

36) O que é o núcleo (kernel) do sistema operacional? Cite 2 funções importantes.

37) Qual a diferença entre um processo estar executando no modo usuário ou no modo kernel?

38) O que são instruções privilegiadas e não privilegiadas?

39) O que é uma chamada de sistema? Qual sua importância para a segurança do sistema?

40) Como um programador pode fazer para executar uma chamada de sistema em seu programa?

41) O que é um sistema operacional com estrutura monolítica?

42) Cite uma vantagem de se projetar um sistema operacional em camadas.

43) Qual a função do monitor de máquina virtual?

44) O que se pode falar sobre a interferência do funcionamento de uma máquina virtual em outra dentro de um mesmo computador?

45) O que caracteriza um sistema operacional com estrutura microkernel?

46) Como funciona o modelo cliente-servidor na arquitetura microkernel? Cite vantagens na sua utilização?

2 Processos

Os primeiros sistemas permitiam a execução de apenas um programa de cada vez. Esse programa tinha o controle completo do sistema e acesso a todos os seus recursos. Os sistema atuais permitem que diversos programas sejam carregados na memória e executados simultaneamente. Essa evolução tornou necessário um controle maior na divisão de tarefas dos vários programas, resultando na noção de processo.

Em um sistema multiprogramável a UCP alterna entre processos, dedicando um pouco de seu tempo a cada um, dando a ilusão de paralelismo. Este esquema costuma ser chamado de pseudoparalelismo.

Neste modelo, todo software executado no computador é organizado em processos sequenciais, ou, simplesmente, processos. Este modelo foi desenvolvido para tornar o paralelismo mais fácil de tratar, uma vez que monitorar atividades paralelas é muito difícil.

Um processo é um programa em execução, incluindo os valores atuais dos registradores e variáveis, assim como seu espaço de endereçamento. Um programa por si só não é um processo, mas uma entidade passiva. Um processo é uma entidade ativa, com um contador de instruções que especifica a próxima instrução a ser executada e um conjunto de registradores a ele associado.

Embora dois processos possam estar associados a um mesmo programa, são duas sequências de execução distintas.

Conceitualmente, cada processo tem sua própria UCP. Com a UCP alternando entre os processos, a velocidade com que um processo executa não será uniforme.

4 processos

concorrendo

Processo
Processo

D

C

B

A

será uniforme. 4 processos concorrendo Processo D C B A Tempo É preciso dispor de uma

Tempo

É preciso dispor de uma forma de criar e destruir processos quando for necessário. No UNIX os processos são criados pela chamada de sistema “fork” que cria uma cópia exata do processo em execução. Em outros sistemas existem chamadas para criar um processo, carregar sua memória, e começar a executar. O Windows pode criar processos das duas formas.

Eventualmente um processo que está em execução necessita parar momentaneamente sua execução por estar aguardando uma informação ainda não disponível ou porque já executou por muito tempo e precisa liberar a UCP para outro processo.

Um processo pode mudar de estado, podendo estar nos estados novo, executando, pronto, bloqueado ou terminado.

Novo Bloqueado Terminado 5 4 1 6 2 Pronto Executando 3
Novo
Bloqueado
Terminado
5
4
1
6
2
Pronto
Executando
3

1.

O processo é admitido no sistema

2.

O escalonador põe o processo selecionado em execução

3.

O processo é interrompido

4.

O processo bloqueia para E/S ou evento

5.

Fim de E/S ou ocorrência de evento esperado

6.

O processo termina sua execução

Para implementar o modelo de processos o sistema operacional mantém uma tabela de processos, com uma entrada por processo. Esta entrada é chamada de Bloco de Controle de Processo BCP (Process Control Block PCB), e contém todas as informações do processo. Algumas entradas do BCP são:

estado do processo

prioridade do processo

número do processo

registradores da UCP

informações relativas ao gerenciamento de memória

informações de contabilidade

informações sobre operações de E/S

etc.

Essa visão dá origem ao seguinte modelo:

0 1 2 3 n-2 n-1 Escalonador
0
1
2
3
n-2
n-1
Escalonador

processos sequenciais gerencia interrupções e agendamento

O nível mais baixo do sistema operacional é o escalonador (também conhecido como

agendador). Ele cuida do gerenciamento de interrupções e dos detalhes de como iniciar e parar

processos. Também costuma ser muito pequeno.

Um processo passa pelas várias filas de seleção durante sua execução. Cabe ao escalonador selecionar processos destas filas e decidir qual será o próximo a ser executado.

O escalonador é chamado com muita frequência. Um processo pode ser executado por apenas

alguns milissegundos e ter que esperar por ter feito uma requisição de E/S. O escalonador costuma

ser chamado pelo menos uma vez a cada 100 ms para realizar a troca de processos. Devido ao pequeno intervalo de tempo entre as chamadas ao escalonador sua execução deve ser bastante rápida para que não se gaste muito tempo de UCP com trabalho de gerência.

2.1 Mudança de Contexto

Para transferir o controle da UCP de um processo para outro é necessário guardar o estado do processo em execução e carregar o estado do processo a entrar em execução. Esta tarefa é conhecida como mudança de contexto (ou troca de contexto).

O tempo gasto na mudança de contexto varia e depende de fatores como velocidade da

memória, quantidade de registradores e existência de instruções especiais. Este tempo costuma

variar de 1 a 1000 s.

O contexto de um processo pode ser dividido em 3 elementos básicos: contexto de hardware,

contexto de software e espaço de endereçamento.

O contexto de hardware constitui-se basicamente do conteúdo dos registradores. No

momento em que o processo perde a UCP, o sistema salva suas informações. Ele é fundamental para a implementação dos sistemas multiprogramáveis.

O contexto de software especifica características do processo que vão influir na execução de

um programa. Ele define basicamente 3 grupos de informações sobre um processo: identificação, quotas e privilégios. A identificação identifica o processo para o sistema de forma única, através de

seu pid, uid e gid. Quotas são os limites de cada recurso que o sistema operacional pode alocar, como número de arquivos abertos, quantidade de memória, número subprocessos que podem ser criados, etc. Privilégio é o que o processo pode ou não fazer em relação ao sistema e outros processos.

O espaço de endereçamento é a área de memória do processo onde o programa será

executado e a área de memória onde os dados do processo serão armazenados. Cada processo

possui seu próprio espaço de endereçamento, que deve ser protegido dos demais.

2.2 Subprocesso e Thread

Quando um processo (processo pai) cria um outro processo, o processo criado é conhecido como subprocesso ou processo filho. O subprocesso, por sua vez, pode criar outros subprocessos. Caso um processo deixe de existir, os subprocessos subordinados são eliminados.

A utilização de subprocessos permite dividir uma aplicação em partes que podem trabalhar de

forma concorrente.

O uso de subprocessos demanda consumo de diversos recursos do sistema. Sempre que um

novo processo é criado o sistema deve alocar recursos (contexto de hardware, contexto de software e espaço de endereçamento) para ele, além de consumir tempo de UCP. Ainda, cada processo possui seu próprio BCP.

Na tentativa de diminuir o tempo gasto na criação/eliminação de processos, bem como economizar recursos do sistema, foi introduzido o conceito de thread. Em um ambiente com múltiplos threads não é necessário haver vários processos para se implementar aplicações concorrentes.

Threads compartilham o processador da mesma maneira que um processo. Cada thread possui seu próprio conjunto de registradores (contexto de hardware), porém compartilha o mesmo espaço de endereçamento com os demais threads do processo. No momento em que um thread perde a utilização do processador, o sistema salva suas informações. Threads passam pelos mesmos estados que um processo.

A grande diferença entre subprocessos e threads é em relação ao espaço de endereçamento.

Subprocessos possuem, cada um, espaços independentes e protegidos. Threads, por outro lado, compartilham o mesmo espaço de endereçamento do processo, sem nenhuma proteção, permitindo que um thread possa alterar dados de outro thread. Threads são desenvolvidos para trabalharem de forma cooperativa, voltados para desempenhar uma tarefa em conjunto. Threads também são conhecidas como processos leves.

A mudança de contexto entre threads em um mesmo processo exige uma alteração de um

conjunto de registradores, mas não é necessário nenhum outro trabalho, como gerenciamento de memória, por exemplo, tornando-a mais leve que a mudança de contexto entre processos.

thread contador de programa
thread
contador de programa

processo

3 processos, com 1 thread cada

3 processos, com 1 thread cada

1 processo com 3 threads

1 processo com 3 threads

Quando múltiplos threads estão presentes no mesmo processo, alguns campos da tabela de processos não são por processo, mas por thread.

Em alguns sistemas os threads são gerenciados no espaço do usuário, sem o conhecimento do sistema operacional. É o caso do pacote P-threads (POSIX). A comutação de threads é muito mais rápida quando é feita no espaço do usuário por não precisar fazer uma chamada ao kernel. Porém, quando os threads são executados no espaço do usuário e um thread bloqueia, todo o processo é bloqueado pelo kernel. Threads no nível do usuário também faz com que o tempo dedicado a threads de diferentes processos não seja distribuído de uma forma justa.

2.3

Tipos de Processos

Existem basicamente dois tipos de processos com relação ao tipo de processamento que executam: CPU-bound e I/O-bound.

Os processos do tipo CPU-bound passam a maior parte do tempo no estado executando, realizando poucas operações de E/S. Costumam ser encontrados em aplicações científicas.

Os processos do tipo I/O-bound passam a maior parte do tempo no estado bloqueado por realizar um elevado número de operações de E/S. Costumam ser encontrados em aplicações comerciais. Processos interativos também são exemplos deste tipo de processo.

2.4 Processos Cooperativos

Os processos concorrentes em um sistema operacional podem ser independentes ou cooperativos. Um processo é um processo independente se ele não afeta e não é afetado por nenhum outro processo do sistema. Por outro lado, qualquer processo que afetada ou é afetado por outro processo é dito um processo cooperativo. Algumas razões para a cooperação entre processos são:

Compartilhamento de informações Acesso concorrente a recursos.

Aumento da velocidade de processamento Aplicações que podem ser divididas em subtarefas a serem executadas paralelamente.

Modularidade Dividir as funções do sistema em processos separados.

Conveniência Permitir a troca de dados entre processos dos usuários.

Para que possa ser implementada a cooperação entre processos é preciso existir mecanismos que permitam aos processos comunicarem-se uns com os outros e sincronizarem suas ações.

Para ilustrar o conceito de processos cooperativos pode-se usar o exemplo do produtor/consumidor. Um processo produtor produz informações que são consumidas por um processo consumidor.

Para permitir a execução concorrente dos processos produtor e consumidor é preciso haver uma área de armazenamento de itens a qual os dois processos tenham acesso. Ainda, os dois processos devem estar sincronizados para que o consumidor não tente consumir quando não houver item produzido.

Outro problema é que a área de armazenamento costuma ser limitada. Assim o produtor só poderá produzir enquanto houver espaço para armazenamento de itens.

O controle de acesso a área de armazenamento pode ser feito por meio do sistema operacional

usando comunicação interprocessos ou explicitamente pelo programador com o uso de memória compartilhada.

Utilizando memória compartilhada teríamos as seguintes definições compartilhadas pelos processos:

const n = type item = var buffer: array [0

;

;

n-1] of item;

counter, in, out: integer;

A área de armazenamento compartilhada é implementada por meio de uma fila circular. A

variável in aponta para a próxima posição livre na área de armazenamento, enquanto out aponta

para a primeira posição ocupada na área de armazenamento.

O código do processo produtor fica:

repeat nextp := produz_item; while (counter = n) do (*faz nada*); buffer[in] := nextp; in := (in+1) mod n; counter := counter + 1; until false;

O código do processo consumidor fica:

repeat while (counter = 0) do (*faz nada*); nextc := buffer[out]; out := (out+1) mod n; counter := counter - 1; consome_item(nextc); until false;

2.5 Exercícios

1) O que é um processo?

2) Pode-se afirmar que um programa executado em uma mesma máquina com sistema operacional multitarefa levará sempre o mesmo tempo para terminar sua execução? Explique.

3) Quais os estados em que um processo pode estar? Que transições podem ocorrer entre estes estados?

4) Para cada possível mudança de estado de um processo, cite um exemplo de evento que pode levar a tal mudança.

5) Qual a função do bloco de controle de processos (PCB)?

6) Qual a função do escalonador de um sistema operacional?

7) O que significa mudança de contexto?

8) Que importância tem a tabela de processos para a mudança de contexto?

9) O que são threads?

10) Qual a diferença entre threads e subprocessos?

11) Como o uso de threads pode melhorar o desempenho de aplicações paralelas em ambientes com múltiplos processadores?

12) Cite uma vantagem de se utilizar threads no lugar de processos cooperativos.

13) O que diferencia um processo CPU-bound de um processo I/O-bound?

14) Dê exemplos de aplicações CPU-bound e I/O-bound.

15) O que são processos cooperativos?

16) Cite duas razões para a cooperação entre processos.

3 Comunicação Entre Processos

O surgimento dos sistemas multiprogramáveis tornou possível criar aplicações nas quais

diferentes partes de seu código pudessem executar de forma concorrente. Tais aplicações são conhecidas como aplicações concorrentes.

É comum que processos de aplicações concorrentes compartilhem recursos do sistema. Não importa quais recursos são compartilhados, os problemas decorrentes serão os mesmos. O compartilhamento de recursos entre processos pode gerar situações indesejáveis capazes de comprometer o sistema.

Se dois processos concorrentes trocam informações através de um buffer, um processo só poderá gravar dados caso o buffer não esteja cheio, enquanto um processo só poderá ler dados caso o buffer não esteja vazio. Em qualquer caso, os processos deverão aguardar até que o buffer esteja pronto para as operações de E/S.

As considerações anteriores levam a 3 questões:

Como um processo passa informações a outro?

Como certificar que 2 processos não se interfiram?

Como realizar o sequenciamento quando um processo depende do outro?

É bastante comum que aplicações concorrentes necessitem trocar informações. Tal

comunicação pode se dar por intermédio de variáveis compartilhadas (memória compartilhada) ou por troca de mensagens. Mas, independente do mecanismo de comunicação utilizado, é preciso que os processos tenham condições de se manterem sincronizados.

Os mecanismos que garantem a comunicação entre processos concorrentes e o acesso a recursos compartilhados são chamados mecanismos de sincronização. Os mesmos mecanismos se aplicam a threads.

3.1 Condição de Corrida

Em alguns sistemas, processos que estão trabalhando em conjunto muitas vezes utilizam uma memória comum, onde cada processo pode ler ou escrever. Este armazenamento compartilhado pode ser feito na memória principal ou em um arquivo em disco.

Seja, por exemplo, um programa que atualize o saldo de um cliente após o lançamento de um débito ou um crédito em um registro. O trecho do programa que faz a atualização poderia ser:

Read(Arquivo, Registro); Readln(Valor_cred_deb); Registro.Saldo := Registro.Saldo + Valor_cred_deb; Write(Arquivo, Registro);

Suponha que o processo seja executado de forma concorrente por dois caixas e que o primeiro processo leia o valor do registro. Caso o segundo processo venha a ler também o valor do mesmo registro antes que o primeiro tenha a oportunidade de gravar sua alteração, haverá uma inconsistência ao final das operações. Uma delas não será efetivada.

Problemas como esse são conhecidos como condição de corrida, que é quando dois ou mais processos estão acessando dados compartilhados e o resultado final do processamento depende de quem executa quando.

3.2

Região Crítica

Como evitar as condições de corrida? A forma mais simples é impedir que dois ou mais processos acessem um mesmo recurso no mesmo instante, impedindo que eles acessem o recurso compartilhado simultaneamente. Quando um processo estiver acessando o recurso os demais deverão esperar. A esta exclusividade de acesso dá-se o nome de exclusão mútua, uma questão importante para o desenvolvimento de um sistema operacional.

A parte do programa que acessa a memória compartilhada é denominada seção crítica ou região crítica (RC). Quando um processo está executando dentro de sua região crítica, nenhum outro processo pode entrar na região crítica.

Begin

Entra_Regiao_Critica;

Regiao_Critica;

Sai_Regiao_Critica;

End.

Não é suficiente evitar que o processo seja interrompido dentro da região crítica. São quatro as condições para uma boa solução:

1. Não pode haver mais de um processo simultaneamente dentro de suas regiões crítica.

2. Nenhuma suposição pode ser feita sobre a velocidade ou o número de UCPs.

3. Nenhum processo que execute fora de sua região crítica pode bloquear outro processo.

4. Nenhum processo deve ter que esperar eternamente para entrar em sua região crítica (starvation).

Como pode ser observado, para garantir a implementação da exclusão mútua os processos envolvidos devem fazer acesso aos recursos compartilhados de forma sincronizada.

3.3 Sincronização Condicional

Outra situação na qual é necessária a sincronização entre processos concorrentes é quando um recurso compartilhado não se encontra pronto para ser utilizado. Nesse caso, o processo que deseja acessar o recurso deverá ser colocado no estado bloqueado até o recurso ficar pronto para o processamento.

Um exemplo clássico é a comunicação entre dois processos através de operações de gravação e leitura em um buffer. Os processos envolvidos devem estar sincronizados de forma que um processo não tente gravar dados em um buffer cheio ou ler de um buffer vazio.

Abaixo

é

mostrado,

produtor/consumidor.

como

exemplo de sincronização condicional, o programa

PROGRAM Produtor_Consumidor;

CONST TamBuf

TYPE Tipo_Dado = (* Tipo qualquer *); (* Tipo do dado *)

= (* Tamanho qualquer *); (* Tamanho do Buffer *)

VAR Buffer

: ARRAY [1

TamBuf]

OF Tipo_Dado;

Dado

: Tipo_Dado;

 

Cont

:

0

TamBuf;

(* Contador que controla o Buffer *)

PROCEDURE Produtor; BEGIN REPEAT Produz_Dado (Dado); WHILE (Cont = TamBuf) DO (* Nao faz nada *); Grava_Buffer (Dado, Cont); UNTIL False; END;

PROCEDURE Consumidor; BEGIN REPEAT WHILE (Cont = 0) Do

(* Nao faz nada *);

Le_Buffer (Dado, Cont);

Consome_Dado (Dado); UNTIL False; END; BEGIN Cont := 0; PARBEGIN Produtor; Consumidor; PAREND; END.

Nessa solução, a tarefa de colocar e retirar os dados do buffer é realizada pelos procedimentos Grava_Buffer e Le_Buffer. Essas duas rotinas deverão ser executadas de forma mutuamente exclusiva.

3.4 Exclusão Mútua por Hardware

As soluções por hardware não podem ser utilizadas por qualquer programa, pois normalmente requerem o uso de instruções privilegiadas, mas são importantes de serem estudadas porque criam mecanismos que permitem a implementação das soluções por software.

3.4.1 Desabilitação de Interrupções

Consiste em permitir ao processo do usuário desabilitar interrupções antes de entrar na região crítica de forma a evitar que o processo seja interrompido dentro da região crítica. Como a mudança de contexto só pode ser realizada através de interrupções, o processo que as desabilitou terá acesso exclusivo garantido. O processo deve habilitar novamente as interrupções ao sair da região crítica.

Não é bom que processos de usuários possam desativar interrupções. Além disso, sistemas com mais de uma UCP só terão uma delas desativada. É um mecanismo útil apenas quando utilizado pelo kernel.

3.4.2 Instrução TSL (Test and Set Lock)

Consiste em uma instrução especial da UCP que permite ler uma variável, armazenar seu conteúdo em uma outra e atribuir um novo valor a essa variável. Tal instrução tem como característica ser sempre executada sem ser interrompida. Assim, não existe a possibilidade de dois processos estarem manipulando uma variável compartilhada ao mesmo tempo, possibilitando a implementação da exclusão mútua.

Test_and_Set (X,Y);

A instrução possui o formato acima e quando executada o valor lógico de Y é copiado para X, sendo atribuído à Y o valor lógico verdadeiro.

PROGRAM Programa_Test_and_Set; VAR Bloqueio : BOOLEAN; PROCEDURE Processo_A; VAR Pode_A : BOOLEAN; BEGIN REPEAT Pode_A := True; WHILE (Pode_A) Do Test_and_Set (Pode_A, Bloqueio); Regiao_Critica_A; Bloqueio := False; UNTIL False; END;

PROCEDURE Processo_B; VAR Pode_B : BOOLEAN; BEGIN REPEAT Pode_B := True; WHILE (Pode_B) Do Test_and_Set (Pode_B, Bloqueio); Regiao_Critica_B; Bloqueio := False; UNTIL False; END; BEGIN Bloqueio := False; PARBEGIN Processo_A; Processo_B; PAREND; END.

A solução apresentada acima, apesar de funcionar apresenta o problema de ser uma solução com espera ativa (espera ocupada), ou seja, apesar do processo não poder continuar sua execução, ele continua consumindo tempo de UCP desnecessariamente.

3.5 Exclusão Mútua por Software com Espera Ativa

As primeiras soluções por software que surgiram para implementar a exclusão mútua, apesar de mais simples, apresentam o problema da espera ativa. A seguir são mostradas duas destas soluções.

3.5.1 Alternância Estrita

Dois processos concorrem de forma alternada por uma região crítica e testam continuamente uma variável até que seu valor seja o desejado. Assim, eles alternam a ordem de entrada na região crítica.

Program Alternancia; var vez: char; procedure Processo_A; begin repeat while (vez = 'B') do (* nao faz nada *); Regiao_Critica_A; vez := 'B'; Processamento_Longo; until false; end; procedure Processo_B; begin repeat while (vez = 'A') do (* nao faz nada *); Regiao_Critica_B; vez := 'A'; until false; end; begin vez := 'A'; parbegin Processo_A; Processo_B; parend; end.

A solução apresentada tem o problema de desperdiçar tempo de UCP (espera ativa), além de

violar as 2 a e 3 a condições para uma boa solução.

3.5.2 Solução de Peterson

Peterson apresentou uma solução totalmente por software em 1981.

Program Peterson; var CA, CB: Boolean; Vez: Char; Procedure Processo_A; Begin Repeat CA := True; Vez := 'B'; While (CB and Vez='B') do (* faz nada *); Regiao_Critica_A; CA := False; Processamento_A; Until False; End; Procedure Processo_B; Begin Repeat CB := True; Vez := 'A'; While (CA and Vez='A') do (* faz nada *); Regiao_Critica_B; CA := False; Processamento_B; Until False; End; Begin CA := False; CB := False; ParBegin Processo_A; Processo_B; ParEnd; End.

3.6 Exclusão Mútua por Software sem Espera Ativa

A espera ativa gasta tempo de UCP e apresenta o problema da inversão de prioridade, que

ocorre quando um processo de maior prioridade deseja entrar em sua região crítica mas não pode porque outro processo de menor prioridade já está na região crítica. O processo de maior prioridade não pode entrar na região crítica e o de menor prioridade recebe pouco tempo de UCP para sair da região crítica.

Para resolver este problema é melhor utilizar mecanismos de sincronização para bloquear um processo que deseja entrar na região crítica e não pode, evitando que a UCP seja utilizada de forma desapropriada.

3.6.1 Semáforos

Um semáforo é uma variável inteira que conta sinais enviados a ela. Associadas aos semáforos existem duas operações especiais: up e down. A operação down decrementa o valor do semáforo se ele for maior que 0, senão o processo é bloqueado. A operação up incrementa o valor do semáforo caso não hajam processos que tenham sido bloqueados pela operação down, senão um processo é desbloqueado.

No caso da exclusão mútua, as instruções down e up funcionam como protocolos para que um processo possa entrar e sair de sua região crítica. O semáforo fica associado a um recurso compartilhado, indicando quando o recurso está sendo acessado por um dos processos concorrentes. Se seu valor for maior que 0, nenhum processo está utilizando o recurso, caso contrário, o processo fica impedido de acessar o recurso.

Sempre que deseja entrar na sua região crítica um processo executa uma instrução down. Se o semáforo for maior que 0 ele é decrementado de 1 e o processo que solicitou a operação pode executar sua região crítica. Se uma instrução down é executada em um semáforo cujo valor seja 0, o processo que solicitou a operação ficará no estado bloqueado em uma fila associada ao semáforo.

Quando o processo que está acessando o recurso sai de sua região crítica, ele executa uma instrução up, incrementando o semáforo de 1 e liberando o acesso ao recurso. Se um ou mais processos estiverem esperando, o sistema escolhe um processo na fila de espera e muda seu estado para pronto.

As operações up e down são realizadas pelo sistema operacional, que deve garantir que elas sejam executadas atomicamente.

Exemplo de utilização de semáforos para acesso à região crítica:

Program Semaforo; Var s: semaforo; Procedure Processo_A; Begin Repeat Down(s); Regiao_Critica_A; Up(s); Until False; End; Procedure Processo_B; Begin Repeat Down(s); Regiao_Critica_B; Up(s); Until False; End; Begin s := 1; ParBegin Processo_A; Processo_B; ParEnd; End.

Exemplo de sincronização condicional com semáforos (produtor/consumidor):

PROGRAM Produtor_Consumidor;

CONST TamBuf

TYPE Tipo_Dado = (* Tipo qualquer *); VAR Vazio : Semaforo; (* Quantas entradas do buffer estao vazias *) Cheio : Semaforo; (* Quantas entradas do buffer estao ocupadas *)

= (* Tamanho qualquer *); (* Tamanho do buffer *)

Mutex : Semaforo; (* Semaforo usado para garantir a exclusao mutua *)

Buffer : ARRAY [1

TamBuf] OF Tipo_Dado;

Dado1, Dado2 : Tipo_Dado; PROCEDURE Produtor; BEGIN

REPEAT Produz_Dado (Dado1); down (Vazio); down (Mutex);

Grava_Dado (Dado1, Buffer); up (Mutex); up (Cheio); UNTIL False; END; PROCEDURE Consumidor; BEGIN REPEAT down (Cheio); down (Mutex); Le_Dado (Dado2, Buffer); up (Mutex); up (Vazio); Consome_Dado (Dado2); UNTIL False; END; BEGIN Vazio := TamBuf; Cheio := 0; Mutex := 1; PARBEGIN Produtor; Consumidor; PAREND; END.

Deve-se ter cuidado com o uso de semáforos. No exemplo do produtor/consumidor, se o produtor fizer down em mutex antes de em vazio e o buffer estiver cheio, o consumidor faz down em mutex para acessar o buffer e os dois processos bloqueiam. Semáforo devem ser usados com cuidado.

Pode-se ocultar o mecanismo de interrupções utilizando um semáforo para cada dispositivo de

E/S.

3.6.2 Monitores

O uso de semáforos exige do programador muito cuidado, pois qualquer engano pode levar a problemas de sincronização imprevisíveis e difíceis de reproduzir. Monitores são mecanismos de sincronização de alto nível que tentam tornar mais fáceis o desenvolvimento e a correção de programas concorrentes.

Um monitor é uma coleção de variáveis, procedimentos e estruturas de dados que são agrupados em um pacote. Em um dado instante somente um processo pode estar ativo em um monitor. Toda vez que algum processo chama um procedimento do monitor, o monitor verifica se já existe outro processo executando qualquer procedimento do monitor. Caso exista, o processo ficará aguardando a sua vez até que tenha permissão para executar.

Variáveis Globais Procedimento Procedimento Procedimento Procedimento Código de Inicialização
Variáveis Globais
Procedimento
Procedimento
Procedimento
Procedimento
Código de
Inicialização

As variáveis globais do monitor são visíveis apenas a ele e a seus procedimentos. O bloco de comandos do monitor é responsável por inicializar essas variáveis, sendo executado apenas urna vez na ativação do programa onde está declarado o monitor.

Cabe ao compilador implementar as exclusões mútuas em entradas de monitor. A cláusula synchronized da linguagem Java é um exemplo de implementação de monitores. O programador transforma regiões críticas em procedimentos de monitor colocando todas as regiões críticas em forma de procedimentos no monitor. Desta forma o desenvolvimento de programas concorrentes fica mais fácil.

Para a implementação da sincronização condicional é necessário utilizar variáveis de condição e duas instruções que operam sobre elas: wait e signal. Por exemplo, quando o produtor não pode continuar faz wait na variável “full” e fica bloqueado. O consumidor pode então entrar na região crítica e acordar o produtor com signal. Para não haver mais de um processo na região crítica, signal tem que ser a última instrução executada na região crítica.

PROGRAM Produtor_Consumidor;

CONST TamBuf

TYPE Tipo_Dado = (* Tipo qualquer *);

VAR Buffer : ARRAY [1

= (* Tamanho qualquer *); (* Tamanho do buffer *)

TamBuf] OF Tipo_Dado;

MONITOR Condicional; VAR Vazio, Cheio : Condition; (* Variaveis de condicao *);

Cont

: INTEGER;

PROCEDURE Produz(Dado : Tipo_Dado); BEGIN IF (Cont = TamBuf) THEN WAIT (Cheio); Grava_Dado (Dado, Buffer); Cont := Cont + 1; IF (Cont = 1 THEN SIGNAL (Vazio); END; PROCEDURE Consome(var Dado : Tipo_Dado); BEGIN IF (Cont = 0) THEN WAIT (Vazio); Le_Dado (Dado, Buffer);

Cont := Cont - 1; IF (Cont = TamBuf - 1) THEN SIGNAL (Cheio); END; BEGIN

Cont

:= 0;

END; (* Termina monitor *) PROCEDURE Produtor; VAR Dado : Tipo_Dado; BEGIN REPEAT Produz_Dado (Dado); Condicional.Produz (Dado); UNTIL False; END; PROCEDURE Consumidor; VAR Dado : Tipo_Dado; BEGIN REPEAT Condicional.Consome (Dado); Consome_Dado (Dado); UNTIL False; END; BEGIN PARBEGIN Produtor; Consumidor;

PAREND;

END.

3.6.3 Troca de Mensagens

A troca de mensagens é um mecanismo de comunicação e sincronização entre processos sem

a necessidade de utilização de memória compartilhada. Ela é implementada pelo sistema

operacional através das rotinas send e receive. A rotina send é responsável por enviar uma mensagem para um processo receptor enquanto a rotina receive é responsável por receber uma mensagem de um processo.

send (Receptor, Mensagem); receive (Transmissor, Mensagem);

Os procedimentos send e receive, mesmo não tendo suas execuções mutuamente exclusivas, permitem tanto a comunicação entre processos quanto a sincronização entre eles.

Um problema com a troca de mensagens é que uma mensagem de um processo P para outro processo Q pode ser perdida em algum lugar. Existem três métodos básicos para tratamento dessa falha:

O sistema operacional é responsável por detectar a ocorrência dessa falha e por enviar novamente a mensagem.

O processo remetente é responsável por detectar a ocorrência dessa falha e por transmitir novamente a mensagem.

O sistema operacional é responsável por detectar a ocorrência dessa falha e notifica o processo remetente de que a mensagem foi perdida.

A detecção de mensagens perdidas não é sempre necessária. Alguns protocolos de rede especificam que o envio de mensagens não é confiável, enquanto outros garantem o envio.

O método mais comum de detecção de mensagem perdida é baseado em expiração de tempo.

Quando uma mensagem é enviada, uma mensagem de confirmação de recebimento da mensagem é enviada de volta. O sistema operacional ou um processo pode então especificar um intervalo de tempo durante o qual ele espera pela chegada da confirmação de recebimento da mensagem. Se a confirmação não chegar nesse período supõem-se que a mensagem se perdeu e ela é reenviada. No entanto é possível que ela não tenha se perdido, mas simplesmente demorado mais do que o esperado. Nesse caso, pode-se ter várias cópias de uma mesma mensagem. Deve existir um mecanismo para fazer a distinção entre esses vários tipos de mensagens. Esse problema pode ser resolvido numerando-se as mensagens.

Para que dois processos se comuniquem, eles devem ter uma maneira de se referir um ao

outro.

No mecanismo de comunicação direta, também conhecida como endereçamento direto, cada

processo que queira se comunicar com outro deve usar explicitamente o nome do processo receptor

ou remetente da mensagem. Nesse esquema, as primitivas send e receive são definidas:

send (P, mensagem) Envia uma mensagem ao processo P.

receive (Q, mensagem) Recebe uma mensagem do processo Q.

A comunicação direta só permite a comunicação entre dois processos. Seu maior problema é a

necessidade da especificação da identificação dos processos envolvidos na troca de mensagens.

O exemplo produtor/consumidor para este caso é:

Produtor

Consumidor

REPEAT Produz_Dado (Dado); SEND (Consumidor, Dado); UNTIL False;

REPEAT RECEIVE (Produtor, Dado); Consome_Dado (Dado); UNTIL False;

Esse exemplo exibe uma simetria no endereçamento, tanto o processo remetente quanto o destinatário precisam usar o nome do outro processo para se comunicar. Uma variante desse esquema usa uma assimetria no endereçamento, apenas o remetente necessita usar o nome do receptor. Assim, as primitivas send e receive são definidas:

send (P, mensagem) Envia uma mensagem ao processo P.

receive (id, mensagem) Recebe uma mensagem de qualquer processo e a identificação do processo com o qual a comunicação ocorreu é armazenado na variável id.

Na comunicação indireta, também conhecida como endereçamento indireto, as mensagens são enviadas e recebidas por intermédio de uma área compartilhada conhecida como caixa postal (mailbox). Uma caixa postal pode ser vista abstratamente como um lugar no qual processos podem colocar mensagens e do qual mensagens podem ser retiradas. Nesse esquema um processo pode se comunicar com outro por intermédio de diversas caixas postais diferentes. Dois processos só podem se comunicar se compartilharem o uso de alguma caixa postal. As primitivas send e receive são definidas:

send(A, mensagem) Envia uma mensagem para a caixa postal A.

receive(A, mensagem) Recebe uma mensagem da caixa postal A.

A troca de mensagens pode ou não bloquear os processos envolvidos. Basicamente, existem

duas formas de troca de mensagens entre processos: comunicação síncrona e comunicação assíncrona.

Na comunicação síncrona, quando um processo envia uma mensagem fica esperando até que o processo receptor leia a mensagem, ou quando um processo tenta receber uma mensagem permanece esperando até que o processo transmissor envie alguma mensagem. Esse tipo de comunicação dispensa a necessidade de buffers. Esse mecanismo também é conhecido como rendezvous.

Na comunicação assíncrona nem o receptor permanece aguardando o envio de uma mensagem nem o transmissor o seu recebimento. Além da necessidade de buffers para armazenar as mensagens, deve haver outros mecanismos de sincronização que permitam ao processo identificar se uma mensagem já foi enviada ou recebida. A grande vantagem da comunicação assíncrona é o maior paralelismo na execução dos processos.

O programa a seguir ilustra a utilização de troca de mensagens para o produtor/consumidor.

PROGRAM Produtor_Consumidor; PROCEDURE Produtor; VAR Msg : Tipo_Msg; BEGIN REPEAT Produz_Mensagem (Msg); SEND (Msg); UNTIL False; END; PROCEDURE Consumidor; VAR Msg : Tipo_Msg; BEGIN REPEAT RECEIVE (Msg); Consome_Mensagem (Msg); UNTIL Falte; END; BEGIN PARBEGIN Produtor; Consumidor; PAREND;

END.

3.7 Deadlock

Um processo está em deadlock quando está esperando por um evento que nunca ocorrerá. Essa situação costuma ser consequência do compartilhamento de recursos do sistema entre vários processos, sendo que cada processo deve ter acesso ao recurso de forma exclusiva.

Sendo A e B dois processos que necessitem dos recursos 1 e 2, suponha que A adquira o recurso 1 e que B adquira o recurso 2. Se no futuro A precisar do recurso 2 e B precisar do recurso 1, os dois processos entrarão em espera circular, levando ao deadlock.

Para que ocorram situações de deadlock em um sistema, são necessárias pelo menos quatro condições:

1. Cada recurso só pode estar alocado a um único processo em um determinado instante (exclusão mútua).

2. Um processo, além dos recursos já alocados, pode estar esperando por outros recursos.

3. Um recurso não pode ser liberado de um processo só porque outros processos desejam o mesmo recurso (não preempção).

4. Um processo pode ter de esperar por um recurso alocado a outro processo e vice-versa (espera circular).

3.7.1 Prevenção de Deadlock

Para prevenir a ocorrência de deadlocks basta garantir que uma das quatro condições necessárias para sua ocorrência nunca se satisfaça.

A ausência da primeira condição (exclusão mútua) certamente acaba com o problema do

deadlock. Entretanto, a falta da exclusão mútua gera inconsistências sérias no nível dos processos e

do sistema.

Na segunda condição, se for possível evitar que os processos que já possuam recursos garantidos requisitem novos recursos, estará resolvido o problema do deadlock. Uma maneira de implementar esse mecanismo é, sempre que um processo necessitar de recursos para executar, ele deve requisitá-los antes de começar sua execução. Se todos os recursos necessários ao processo estiverem disponíveis o processo poderá alocá-los e iniciar sua execução. Caso contrário, nenhum recurso será alocado e o processo ficará no estado bloqueado. Esse mecanismo pode produzir um grande desperdício na utilização dos recursos do sistema. Outro problema decorrente desse mecanismo é a dificuldade de se determinar o número de recursos que um processo deverá alocar antes da sua execução. No entanto, o mais grave nesse método é a possibilidade de um processo sofrer starvation, ou seja, todos os recursos necessários à sua execução nunca estarem disponíveis ao mesmo tempo.

A terceira condição pode ser evitada quando permitimos que um recurso seja retirado de um

processo no caso de outro processo necessitar do mesmo recurso. A liberação de recursos já garantidos por um processo pode ocasionar sérios problemas, podendo até fazer o processo perder todo o processamento até então realizado. Outro problema desse mecanismo é a possibilidade de

um processo sofrer starvation, quando o processo garante alguns recursos necessários à sua execução e o sistema os libera em seguida.

A última maneira de evitar um deadlock é excluir a possibilidade da espera circular (quarta

condição). Uma forma de implementar esse mecanismo é forçar o processo a ter apenas um recurso de cada vez. Se necessitar de outro recurso, deve liberar o primeiro. Outra forma de implementar tal mecanismo é numerar os recursos e forçar que os processos possam alocar os recursos somente em determinada ordem.

3.7.2

Detecção do Deadlock

Em sistemas que não possuam mecanismos que previnam a ocorrência de deadlocks, é necessário um esquema de detecção e correção do problema.

Para detectar deadlocks os sistemas operacionais devem manter estruturas de dados capazes de identificar cada recurso do sistema, o processo que o está alocando e os processos que estão à espera da liberação do recurso. Toda vez que um recurso é alocado ou liberado por um processo, a estrutura deve ser atualizada.

Dependendo do tipo de sistema, o ciclo de busca por um deadlock pode variar. Em sistemas de tempo compartilhado, o tempo de busca pode ser maior, sem comprometer o desempenho e a confiabilidade do sistema. Sistemas de tempo real, por sua vez, devem constantemente certificar-se da ocorrência de deadlocks, porém essa maior segurança gera mais overhead no sistema.

3.7.3 Correção do Deadlock

Uma solução bastante utilizada pela maioria dos sistemas operacionais é simplesmente eliminar um ou mais processos envolvidos no deadlock, eliminando a espera circular.

A eliminação dos processos envolvidos no deadlock pode não ser simples, dependendo do

tipo do recurso envolvido. Se um processo estiver atualizando um arquivo ou imprimindo uma listagem, o sistema deve garantir que esses recursos sejam liberados sem problemas. Os processos eliminados não têm como ser recuperados, porém outros processos que estavam em deadlock poderão prosseguir.

Uma solução menos drástica envolve a liberação de apenas alguns recursos alocados aos processos para outros processos, até que o ciclo de espera termine. Para que essa solução seja realmente eficiente, é necessário que o sistema possa suspender um processo, liberar seus recursos e, após a solução do problema, retornar à execução do processo sem perder o processamento já realizado. Esse mecanismo é conhecido como rollback e, além do overhead gerado, é muito difícil de ser implementado por ser bastante dependente da aplicação que está sendo processada.

3.8 Problemas Clássicos de Comunicação Interprocessos

3.8.1 Problema dos Filósofos Comendo

É um problema que visa demonstrar a sincronização entre processos. Consiste em filósofos

sentados em torno de uma mesa redonda, cada um com um prato de macarrão a sua frente. O macarrão é escorregadio e só pode ser comido se forem usados dois garfos. Os filósofos só pensam ou comem, e para comer precisam dos dois garfos. Eles só pegam os garfos na hora de comer.

Solução errada:

procedure filosofo(i: integer); begin while (true) begin pensa; pega_garfo(ESQ(i)); pega_garfo(DIR(i)); come; libera_garfo(ESQ(i)); libera_garfo(DIR(i)); end;

end;

Se todos pegarem o garfo esquerdo, todos os processos ficarão bloqueados.

Se colocar as 5 declarações após “pensa” protegidas por um semáforo binário funciona, mas somente um filósofo come por vez.

Uma solução correta é:

Program Filosofos;

const N=5; PENSANDO=0; FOME=1; COMENDO=2; var

estado: array [1 mutex: semaphore;

S: array [1

N] of integer;

N]

of semaphore;

i: integer; function ESQ(i:integer): integer; begin ESQ := ((i-1) mod N) + 1; end; function DIR(i:integer): integer; begin DIR := ((i+1) mod N) + 1; end; procedure teste(i: integer) begin if ((estado[i]=FOME) and (estado[ESQ(i)]<>COMENDO)

and (estado[DIR(i)]<>COMENDO)) then begin estado[i] := COMENDO; up(S[i]); end;

end; procedure pega_garfos(i: integer); begin down(mutex); estado[i] := FOME; teste(i); up(mutex); down(s[i]); end; procedure larga_garfos(i: integer); begin down(mutex); estado[i] := PENSANDO; teste(ESQ(i)); teste(DIR(i)); up(mutex); end; procedure filosofo(i: integer); begin while (true) begin pensa; pega_garfos(i); come; larga_garfos(i); end;

end; begin mutex := 1; for i:=1 to N do S[i] := 0; for i:=1 to N do estado[i] := PENSANDO; parbegin for i:=1 to N do filosofo(i); end; end.

3.8.2 Problema dos Leitores e dos Escritores

Visa resolver o problema de atualizar um banco de dados no qual vários processos estão realizando consultas paralelamente. Procura-se uma solução na qual as consultas possam ser realizadas em paralelo, ao passo que uma atualização só poderá ocorrer se o processo conseguir acesso exclusivo ao banco de dados.

Program LeitoresEscritores; var mutex, db: semaphore; rc: integer; procedure escritor; begin while (true) begin cria_dados; down(db); escreve_banco_dados; up(db); end;

end; procedure leitor; begin while (true) begin down(mutex); rc := rc + 1; if (rc=1) then down(db); up(mutex); le_banco_dados; down(mutex); rc := rc - 1; if (rc=0) then up(db); up(mutex); usa_dados; end;

end; begin mutex := 1; db := 1; rc := 0; (* Iniciar processos leitores e escritores *) end.

Problema: Escritor só acessa se nenhum leitor quiser acessar antes.

3.8.3 Problema do Barbeiro Adormecido

Simula uma barbearia onde vários barbeiros estão a disposição dos clientes que entram. Se não houver cliente, o barbeiro dorme. O primeiro cliente que chega acorda um barbeiro para ter seu cabelo cortado. O cliente que chega e encontra todos os barbeiros ocupados senta-se em uma das cadeiras para aguardar até que um barbeiro fique livre. Se não tiver cadeira para sentar o cliente vai embora.

Program BarbeiroAdormecido; const CADEIRAS=5; var clientes, barbeiros, mutex: semaphore; esperando: integer; procedure barbeiro; begin while(true) begin down(clientes);

down(mutex); esperando := esperando - 1; up(barbeiros); up(mutex); corta_cabelo; end;

end; procedure cliente; begin down(mutex); if (esperando<CADEIRAS) then begin esperando := esperando + 1; up(clientes); up(mutex); down(barbeiros); cabelo_cortado; end else up(mutex);

end; begin clientes := 0; barbeiros := 0; mutex := 0; esperando := 0; (* iniciar processos clientes e barbeiros *) end.

3.9 Exercícios

1) O que é uma aplicação concorrente?

2) O que é um mecanismo de sincronização?

3) O que é condição de corrida? Por que ela é problemática?

4) O que é região crítica? Qual é a sua finalidade?

5) Por que o mecanismo de exclusão mútua evita a condição de corrida?

6) O que é starvation?

7) Para que serve a sincronização condicional?

8) Como a desabilitação de interrupções pode evitar a condição de corrida?

9) Por que não adianta desabilitar interrupções em sistemas com mais de uma UCP?

10) Por que não é conveniente que o sistema operacional permita que um programa de usuário possa desabilitar interrupções?

11) O que é espera ativa? Por que ela deve ser evitada?

12) Explique de forma sucinta como é o funcionamento dos semáforos.

13) O mecanismo de semáforo pode ser utilizado para resolver o problema de starvation? Explique.

14) Como o mecanismo de semáforo pode ser utilizado para resolver o problema da condição de corrida?

sincronização

condicional?

15) Como

o

mecanismo

de

semáforo

pode

ser

utilizado

para

realizar

a

16) A utilização errada de semáforos pode levar ao travamento de uma aplicação? Explique.

17) Como é o funcionamento dos monitores como solução para a condição de corrida? Por que signal deve ser a última instrução executada na região crítica?

18) Por que monitores são de mais fácil utilização pelos programadores do que os semáforos?

19) Para a cooperação entre processos pode ser utilizada memória compartilhada ou troca de mensagens. Qual a diferença?

20) Explique de forma sucinta como funciona o mecanismo de troca de mensagens.

21) Na troca de mensagens, qual a diferença entre comunicação direta e comunicação indireta?

22) Na troca de mensagens, qual a diferença entre comunicação síncrona e comunicação assíncrona?

23) No mecanismo de troca de mensagens, o que ocorre quando um processo tenta enviar uma mensagem ao outro e o canal de comunicação não tem mais espaço para armazenar esta mensagem?

24) O que é deadlock?

25) Qual a diferença entre prevenção de deadlock e detecção do deadlock?

26) Cite uma forma de corrigir o deadlock.

4 Gerência do Processador

A multiprogramação tem como objetivo permitir que, a todo instante, haja algum processo

sendo executado para maximizar a utilização da UCP.

O conceito que possibilitou a implementação de sistemas multiprogramáveis foi a

possibilidade da UCP ser compartilhada entre diversos processos. Portanto, deve existir um critério para determinar qual a ordem na escolha dos processos para execução dentre os vários que concorrem pela UCP.

O procedimento de seleção é conhecido como escalonamento (scheduling). A parte do

sistema operacional responsável pelo escalonamento é o escalonador (scheduler), as vezes chamado de agendador. Sempre que a UCP se torna ociosa o escalonador seleciona um processo, dentre aqueles que estão na memória prontos para serem executados, e aloca a UCP para que ele possa ser executado.

Os principais objetivos do escalonamento são:

Manter a UCP ocupada a maior parte do tempo;

Balancear a utilização da UCP entre os processos;

Maximizar o throughput do sistema;

Oferecer tempos de resposta razoáveis para os usuários interativos.

Esses objetivos devem ser atendidos de forma que nenhum processo fique esperando indefinidamente pela utilização do processador (starvation).

A principal função de um algoritmo de escalonamento é decidir qual dos processos prontos

deve ser alocado à UCP. Os principais critérios de escalonamento são:

Imparcialidade Oferecer uma fatia justa da UCP a cada processo.

Utilização da UCP É desejável que a UCP passe a maior parte de seu tempo ocupada.

Throughput (produtividade) Representa o número de processos executados durante um intervalo de tempo.

Tempo de processamento (turnaround) Tempo que um processo leva desde a sua admissão no sistema até o seu término.

Tempo de resposta Em sistemas interativos, é o tempo desde o momento da submissão de um pedido até a primeira resposta produzida.

Tempo de espera Tempo que um processo fica esperando na fila de processos prontos.

Algumas destas metas são contraditórias. Sempre que se favorece alguma meta outra é prejudicada. Outra complicação é que o comportamento dos processos é imprevisível.

O algoritmo de escalonamento não é o único responsável pelo tempo de execução de um

processo. Ele afeta somente o tempo de espera na fila de processos prontos.

Os algoritmos de escalonamento podem ser classificados como preemptivos ou não-preemptivos. Quando o sistema pode interromper um processo durante sua execução para colocá-lo no estado pronto e colocar outro processo no estado executando, tem-se um sistema preemptivo. Senão tem-se um sistema não-preemptivo.

O escalonamento preemptivo permite que o sistema dê atenção imediata a processos mais

prioritários, além de proporcionar melhor tempo de resposta em sistemas de tempo compartilhado. Outro benefício decorrente é o compartilhamento do processador de uma maneira mais uniforme.

A troca de um processo por outro na UCP (mudança de contexto) causada pela preempção,

gera um overhead ao sistema. Para isto não se tornar crítico, o sistema deve estabelecer

corretamente os critérios de preempção.

Sistemas que usam escalonamento preemptivo têm o problema da condição de corrida, o que não ocorre com sistemas que usam escalonamento não-preemptivo. No escalonamento não- preemptivo, quando um processo ganha o direito de utilizar a UCP, nenhum outro processo pode lhe tirar esse recurso.

4.1 Algoritmos de Escalonamento

4.1.1 Escalonamento First In First Out (FIFO)

Nesse escalonamento, o processo que chegar primeiro é o primeiro a ser selecionado para execução. É necessária apenas uma fila, onde os processos que passam para o estado pronto entram no seu final. Quando um processo ganha a UCP, ele a utiliza sem ser interrompido, caracterizando-o como um algoritmo não-preemptivo.

O problema do escalonamento FIFO é a impossibilidade de se prever quando um processo

terá sua execução iniciada. Outro problema é a possibilidade de processos CPU-bound de menor importância prejudicarem processos I/O-bound mais prioritários.

Este algoritmo foi inicialmente implementado em sistemas batch.

4.1.2 Escalonamento Shortest Job First (SJF)

Esse algoritmo associa a cada processo seu tempo de execução. Quando a UCP está livre, o processo no estado pronto que precisar de menos tempo para terminar é selecionado para execução.

O escalonamento SJF beneficia processos que necessitam de pouco processamento e reduz o

tempo médio de espera em relação ao FIFO. O problema é determinar quanto tempo de UCP cada processo necessita para terminar seu processamento. Em ambientes de produção é possível estimar o tempo de execução, mas em ambientes de desenvolvimento é muito difícil.

É um algoritmo de escalonamento não-preemptivo e, assim como o FIFO, também foi

utilizado nos primeiros sistemas operacionais com processamento batch.

4.1.3 Escalonamento Cooperativo

Tanto o SJF quanto o FIFO não são algoritmos de escalonamento aplicáveis a sistemas de tempo compartilhado, onde um tempo de resposta razoável deve ser garantido a usuários interativos.

No escalonamento cooperativo, quando um processo já está em execução a um determinado tempo, ele voluntariamente libera a UCP retornando para a fila de processos prontos.

Sua principal característica está no fato da liberação da UCP ser uma tarefa realizada exclusivamente pelo processo em execução, que a libera para um outro processo. Não existe nenhuma intervenção do sistema operacional na execução do processo. Isto pode ocasionar sérios problemas na medida em que um programa pode não liberar a UCP ou um programa mal escrito pode entrar em loop, monopolizando a UCP.

É um algoritmo de escalonamento não-preemptivo.

4.1.4 Escalonamento Circular (Round Robin)

Esse algoritmo é bem semelhante ao FIFO. Entretanto, quando um processo passa para o estado executando, existe um tempo limite (conhecido como time-slice ou quantum) para utilização da UCP de forma contínua. Quando esse tempo expira o processo volta ao estado pronto, dando a vez para outro processo.

A fila de processos no estado pronto é tratada como uma fila circular. O escalonamento é

realizado alocando a UCP para cada processo da fila no intervalo de tempo determinado pelo

quantum.

Se o quantum for muito pequeno gasta-se muito tempo de UCP com trabalho administrativo.

Se o quantum for muito grande a interatividade fica prejudicada, já que um processo que sai de execução pode demorar muito a voltar. Em geral, o quantum varia de 10 a 100 ms.

poderá monopolizar a UCP,

caracterizando-o como um algoritmo de escalonamento preemptivo.

Através

do

escalonamento

circular,

nenhum

processo

Processo atual processo Próximo A B C D
Processo atual
processo Próximo
A
B
C
D

Processo atual

processo Próximo

Próximo A B C D Processo atual processo Próximo preempção B C D A Um problema
preempção B C D A
preempção
B
C
D
A

Um problema com o escalonamento circular é que ele não oferece qualquer tratamento diferenciado a processos I/O-bound. Assim processos CPU-bound terão a tendência de monopolizar a utilização da UCP enquanto processos I/O-bound permanecem a espera.

4.1.5 Escalonamento por Prioridade

O escalonamento circular consegue melhorar a distribuição do tempo de UCP em relação aos

escalonamentos não-preemptivos, porém ainda não consegue implementar um compartilhamento equitativo entre os diferentes tipos de processos.

Para solucionar esse problema, os processos I/O-bound devem levar alguma vantagem no escalonamento, a fim de compensar o tempo excessivo gasto no estado bloqueado. Como alguns processos devem ser tratados de maneira diferente dos outros, é preciso associar a cada um deles uma prioridade de execução. Assim, processos de maior prioridade são escalonados preferencialmente.

A preempção por prioridade é implementada mediante um relógio que interrompe o

processador periodicamente para que a rotina de escalonamento reavalie prioridades e, possivelmente, escalone outro processo, caracterizando-o como um algoritmo de escalonamento

preemptivo.

Todos os sistemas de tempo compartilhado implementam algum esquema de prioridade. A prioridade é uma característica do contexto de software de um processo, podendo ser estática ou dinâmica.

Tem-se a prioridade estática quando a prioridade não é modificada durante a existência do processo. Apesar da simplicidade de implementação, a prioridade estática pode ocasionar tempos de resposta elevados.

Na prioridade dinâmica, a prioridade do processo pode ser ajustada de acordo com o tipo de

processamento realizado pelo processo e/ou a carga do sistema. Quando o processo sai do estado bloqueado, recebe um acréscimo à sua prioridade. Dessa forma, os processos I/O-bound terão mais chance de ser escalonados e compensar o tempo que passam no estado bloqueado.

Para evitar que processos com maior prioridade executem indefinidamente, a prioridade é diminuída com o passar do tempo.

Uma outra forma de se obter prioridade dinâmica é fazer com que o quantum do processo seja inversamente proporcional à fração do último quantum utilizado.

Embora os sistemas de prioridade dinâmica sejam mais complexos e gerem um overhead maior, o tempo de resposta oferecido compensa.

4.1.6

Escalonamento por Múltiplas Filas

Como os processos de um sistema possuem diferentes características de processamento, é difícil que um único mecanismo de escalonamento seja adequado a todos. Uma boa política seria classificar os processos em função do tipo de processamento realizado e aplicar a cada grupo mecanismos de escalonamentos distintos.

O escalonamento por múltiplas filas implementa diversas filas de processo no estado pronto,

onde cada processo é associado exclusivamente a uma delas. Cada fila possui um mecanismo próprio de escalonamento, em função das características do processo. Nesse esquema, os processos devem ser classificados, previamente, em função do tipo de processamento, para poderem ser encaminhados a uma determinada fila.

Cada fila possui uma prioridade associada, que estabelece quais filas são prioritárias em relação às outras. O sistema só pode escalonar processos de uma fila se todas as outras de prioridade maior estiverem vazias.

Para exemplificar esse escalonamento, considere que os processos, em função de suas características, sejam divididos em três grupos: sistema, interativo e batch. Os processos do sistema devem ser colocados em uma fila de prioridade superior à dos outros processos, implementando um algoritmo de escalonamento baseado em prioridades. Os processos de usuários interativos devem estar em uma fila de prioridade intermediária, implementando, por exemplo, o escalonamento circular. O mesmo mecanismo de escalonamento pode ser utilizado na fila de processos batch, com a diferença de que esta fila deverá possuir uma prioridade mais baixa.

4.1.7 Escalonamento por Múltiplas Filas com Realimentação

No escalonamento por múltiplas filas os processos são previamente associados a uma determinada fila. No caso de um processo que altere o seu comportamento no correr do tempo, esse esquema é falho, pois o processo não poderá ser redirecionado para uma outra fila mais adequada.

O escalonamento por múltiplas filas com realimentação também implementa diversas filas,

onde cada qual tem associada uma prioridade de execução, porém os processos não permanecem obrigatoriamente em uma mesma fila até o término do processamento.

Esse esquema permite que os processos sejam redirecionados entre as filas do sistema, fazendo com que o sistema operacional implemente um mecanismo de ajuste dinâmico (mecanismo adaptativo) que tem como objetivo ajustar os processos em função de seu comportamento.

Um processo, ao ser criado, entra no final da fila de mais alta prioridade. Quando um processo em execução deixa a UCP, seja por preempção por prioridade ou por solicitação a um recurso do sistema, ele é reescalonado dentro da mesma fila. Caso o processo esgote seu quantum, ele é redirecionado para uma fila de menor prioridade.

O quantum em cada fila varia em função da sua prioridade. Quanto maior a prioridade da fila,

menor é o seu quantum. Assim, o quantum dos processos não é estático, variando em função da fila na qual ele se encontra.

Essa política de escalonamento atende as necessidades dos diversos tipos de processos. No caso de processos I/O-bound, ela oferece um bom tempo de resposta, já que esses processos têm prioridades altas por permanecerem a maior parte do tempo nas filas de mais alta ordem. No caso de processos CPU-bound, a tendência é de que, ao entrar na fila de mais alta prioridade, o processo ganhe a UCP, gaste seu quantum de tempo e seja direcionado para uma fila de menor prioridade.

O maior problema deste algoritmo é que por sua complexidade ele pode gerar um grande

overhead ao sistema.

4.1.8 Escalonamento em Sistemas de Tempo Real

Sistemas de tempo real devem ser utilizados quando existem aplicações que necessitem dar respostas imediatas a alguns eventos do sistema.

O escalonador deve levar em conta a a importância relativa da tarefa para o sistema e tratá-la

com a prioridade adequada. Assim, o algoritmo de escalonamento por prioridade é o mais adequado para sistemas deste tipo. Não deve haver o conceito de fatia de tempo e a prioridade de cada

processo deve ser estática.

4.2 Escalonamento com Múltiplas UCPs

O mecanismo de escalonamento para sistemas com múltiplas UCPs é bem mais complexo que

com uma única UCP. A abordagem é diferenciada para sistemas fracamente acoplados ou fortemente acoplados.

Em sistemas fracamente acoplados, cada UCP faz seu próprio escalonamento local. O sistema possui, além da UCP, sua memória principal, sistema operacional, algoritmo de escalonamento e sua própria fila de processos prontos para execução.

Se várias UCPs idênticas estiverem disponíveis em um sistema fortemente acoplado, seria possível usar uma fila de processos prontos separada para cada UCP. Entretanto uma UCP poderia ficar ociosa, enquanto outra estivesse muito ocupada. Assim, implementa-se uma única fila de processos prontos para todas as UCPs. Todos os processos estão presentes nesta única fila e são escalonados no primeiro processador disponível. Como a memória é única para todas as UCPs e todos os programas, não faz diferença em qual UCP a execução ocorrerá. Este caso em que não importa em qual das UCPs se dará a execução do processo é conhecido como multiprocessamento simétrico.

Nesta solução é importante que seja implementada a exclusão mútua. No caso de mais de uma UCP tornar-se disponível em um mesmo instante, não pode haver a possibilidade de um mesmo processo ser escalonado por duas UCPs diferentes.

Uma outra solução é designar uma UCP que executa o algoritmo de escalonamento, sendo ela a responsável por determinar a tarefa de cada UCP do sistema.

4.3 Exercícios

1) Qual a função do escalonador de um sistema operacional?

2) No que se refere à gerência do processador, o que diferencia um sistema operacional preemptivo de um sistema operacional não-preemptivo?

3) Por que um algoritmo de escalonamento preemptivo pode levar ao problema da condição de corrida?

4) Por que no algoritmo de escalonamento FIFO processos CPU-bound de menor importância podem prejudicar processos I/O-bound prioritários?

5) Como o algoritmo de escalonamento SJF reduz o tempo médio de espera em relação ao FIFO?

6) Como funciona o escalonamento cooperativo? Ele é preemptivo? Explique.

7) Como é possível que um processo consiga utilizar a UCP sem dar a chance de outro processo entrar em execução quando se usa escalonamento cooperativo?

8) O que é quantum para um algoritmo de escalonamento preemptivo?

9) O que diferencia o escalonamento circular (Round Robin) do escalonamento por prioridade?

10) Para o escalonamento circular, o que é quantum?

11) Por que um valor alto para quantum prejudica a interatividade?

seja

preemptivo?

13) Por que processos I/O-bound podem ser prejudicados no escalonamento circular? Como o escalonamento por prioridade resolve este problema?

12) Como

o

sistema

operacional

faz

para

garantir

que

o

escalonamento

circular

14) Explique sucintamente como funciona um algoritmo de escalonamento por prioridade.

15) Qual a diferença entre prioridade estática e prioridade dinâmica para um algoritmo de escalonamento por prioridade?

16) Como o escalonador determina o próximo processo a entrar em execução em um sistema de tempo real?

17) Como o escalonador de tempo real trata o quantum?

18) Em um sistema com múltiplas UCPs, é possível que um processo que saiu de execução em uma UCP, quando volte ao estado pronto, entre em execução em outra UCP? Explique.

19) O que é multiprocessamento simétrico?

5 Gerência de Memória

Na memória principal residem todos os programas e dados que serão executados ou referenciados pela UCP. Toda vez que for ser executado um programa residente na memória secundária deve-se carregá-lo na memória principal.

Enquanto nos sistemas monoprogramáveis a gerência da memória não é muito complexa, nos sistemas multiprogramáveis ela se torna crítica, uma vez que o sistema operacional deve gerenciar o acesso dos vários processos à memória de forma concorrente.

O gerenciador de memória é a parte do sistema operacional que gerencia a hierarquia de

memória. Seu trabalho consiste em:

Controlar que parte da memória está em uso.

Alocar memória para os processos.

Gerenciar a troca entre a memória principal e o disco.

5.1 Atribuição de Endereços

Um programa é armazenado em disco como um arquivo binário executável. Para ser executado ele deve ser colocado em memória como parte de um processo.

Os sistemas operacionais atuais permitem que os processos dos usuários sejam colocados em

qualquer parte da memória. Assim, embora o espaço de endereçamento do computador comece no endereço zero, este espaço não precisa ser o endereço inicial de um processo. Os endereços usados nos programas podem ser representados de maneiras diferentes. No programa fonte os endereços costumam ser simbólicos, representados por nomes de variáveis. O compilador normalmente associa este endereço simbólico a um endereço relativo, como uma posição a partir do início do programa. O loader por sua vez transforma endereços relativos em endereços absolutos.

A atribuição de endereços a instruções ou dados pode ser feita:

Em tempo de compilação A posição de memória em que o programa será armazenado é conhecida durante a compilação. Se precisar mudar o programa deve ser recompilado.

Em tempo de carga A atribuição final de endereços absolutos é feita durante a carga do programa em memória.

Em tempo de execução Quando um programa durante sua execução, pode ser transferido de um local para outro na memória. A atribuição de endereços deve ser feita somente durante a execução.

5.2 Carregamento Dinâmico

No carregamento dinâmico o programa é dividido em um programa principal e uma série de

rotinas que ficam armazenadas em disco. Para a execução o programa principal é carregado na

memória. Quando uma rotina é chamada, ela é carregada na memória.

A vantagem do carregamento dinâmico é que uma rotina não usada nunca é carregada na

memória.

O carregamento dinâmico não requer obrigatoriamente suporte especial do sistema

operacional, mas o sistema operacional pode fornecer chamadas de sistema para facilitar sua implementação.

5.2.1

Ligação Dinâmica

O conceito de ligação dinâmica é similar ao de carregamento dinâmico, só que na ligação

dinâmica é a ligação (linkedição) que é realizada em tempo de execução.

Este recurso é comumente utilizado para bibliotecas do sistema. Sua ausência obriga que todos os programas do sistema tenham uma cópia das rotinas do sistema que utiliza, gerando um grande desperdício tanto de espaço em disco quanto de memória. Com este mecanismo é incluído no código executável do programa um código para ligação dinâmica, que indica onde e como armazenar uma rotina de biblioteca.

Este recurso pode ser estendido para casos em que ocorrem mudanças no código da biblioteca. Uma biblioteca pode ser substituída por uma nova versão e todos os programas que a usam passarão automaticamente a usar a nova versão.

Diferentemente do carregamento dinâmico, a ligação dinâmica costuma requerer algum suporte do sistema operacional.

5.3 Alocação Contígua Simples

Nesse esquema a memória principal é dividida em duas partes: uma para o sistema operacional e outra para o programa do usuário. Assim o programador deve criar suas aplicações, preocupado apenas em não ultrapassar o espaço de memória disponível.

O usuário tem controle sobre toda a memória principal, podendo acessar qualquer posição de

memória. Para proteger o sistema operacional, alguns sistemas implementam proteção através de registradores, que delimitam as áreas do sistema operacional e do usuário. Assim, sempre que um programa de usuário faz referência a um endereço na memória, o sistema verifica se o endereço está nos seus limites.

A principio, os programas dos usuários estavam limitados ao tamanho da memória principal

disponível. A solução encontrada foi dividir o programa em partes, de forma que pudessem executar independentemente uma da outra, utilizando uma mesma área de memória. Essa técnica é chamada de overlay (sobreposição).

A definição das áreas de overlay é função do programador. O tamanho de uma área de overlay

será estabelecido a partir do tamanho do maior módulo. A técnica de overlay tem a vantagem de permitir ao programador expandir os limites da memória principal, mas pode trazer implicações quanto ao desempenho das aplicações pela excessiva transferência entre as memórias principal e secundária.

5.4 Alocação Particionada

Nos sistemas monoprogramáveis o processador permanece ocioso e a memória é subutilizada, enquanto o programa aguarda por algum evento.

é

possibilidade de estarem na memória principal ao mesmo tempo.

a

Para

a

multiprogramação

ser

eficiente

necessário

que

vários

programas

tenham

5.4.1 Alocação Particionada Estática

Nos primeiros sistemas multiprogramáveis, a memória foi dividida em pedaços de tamanho fixo, chamados partições. O tamanho das partições era estabelecido na fase de inicialização do sistema. Sempre que fosse necessária a alteração do tamanho de uma partição o sistema deveria ser reinicializado com uma nova configuração.

Os programas só podiam executar em uma das partições, mesmo se outras estivessem disponíveis, por causa dos compiladores e montadores que geravam apenas código absoluto. A esse tipo de alocação chamou-se alocação particionada estática absoluta.

Com a evolução dos compiladores, linkers e loaders, a geração de código relocável foi possível e os programas puderam ser carregados em qualquer partição. Assim foi criado um novo tipo de organização, denominado alocação particionada estática relocável. Neste esquema o endereço base é determinado durante o carregamento do programa.

Para manter o controle sobre quais partições estavam alocadas ou não, os sistemas possuíam uma tabela delimitando cada partição, seu tamanho e se estava em uso ou não.

Nesse esquema de alocação de memória, a proteção baseia-se em dois registradores, que indicam os limites inferior e superior da partição onde o programa está sendo executado.

Memória Principal

Sistema Operacional
Sistema Operacional
Partição onde o programa está sendo executado
Partição onde o
programa está sendo
executado

Endereço inicial

Endereço final

Tanto nos sistemas de alocação absoluta quanto nos de alocação relocável, os programas normalmente não preenchiam totalmente as partições onde eram carregados. Além disso, se um programa for maior que qualquer partição livre, ele ficará aguardando uma que o acomode, mesmo que existam partições adjacentes que somadas totalizem o tamanho do programa. Esse tipo de problema é conhecido como fragmentação interna.

5.4.2 Alocação Particionada Dinâmica

A alocação particionada estática leva ao problema da fragmentação, diminuindo a capacidade de compartilhamento de memória.

Na alocação particionada dinâmica não existe o conceito de partições de tamanho fixo. Nesse esquema, cada processo cria uma partição com o tamanho que necessita.

A B C D 2 MB 3 MB 4 MB 2 MB
A
B
C
D
2 MB
3 MB
4 MB
2 MB
Sistema Operacional
Sistema Operacional
A B C D 2 MB 3 MB 4 MB 2 MB Sistema Operacional 12 MB

12 MB

Sistema Operacional Processo A 2 MB Processo B 3 MB Processo C 4 MB Processo
Sistema Operacional
Processo A
2
MB
Processo B
3
MB
Processo C
4
MB
Processo D
2
MB
1
MB

A fragmentação começará a ocorrer quando os programas forem terminando e deixando espaços na memória, não permitindo o ingresso de novos programas. Este tipo de fragmentação é denominada fragmentação externa.

Depois de detectada a fragmentação, existem duas soluções para o problema. Na primeira, os espaços adjacentes são reunidos, produzindo um único espaço de tamanho maior. A segunda solução envolve a relocação de todas as partições ocupadas, eliminando todos os espaços entre elas.

Esse mecanismo de compactação, também conhecido como alocação dinâmica com relocação, reduz o problema da fragmentação, mas a complexidade do seu algoritmo e o consumo de recursos do sistema podem torná-lo inviável.

5.4.3 Estratégias para Escolha da Partição

Existem basicamente cinco estratégias para determinar em qual partição livre um programa será carregado: primeiro ajuste, próximo ajuste, melhor ajuste, pior ajuste e ajuste rápido.

A melhor estratégia a ser adotada depende do tamanho dos programas processados no

ambiente. Independentemente do algoritmo utilizado, o sistema deve possuir uma relação de áreas

livres com o endereço e o tamanho de cada uma.

Na representação com lista encadeada é mantida uma lista encadeada com os segmentos alocados e com os segmentos livres. A manutenção é mais rápida se a lista for duplamente encadeada.

A B C D E
A
B
C
D
E
Lista encadeada 0 5 L 5 3 8 6 P 14 4 P A P
Lista encadeada
0
5
L 5
3
8
6
P
14
4
P A
P B
C
L 18
2
20
6
26
3
L
29
3
P D
P E

Mantendo a lista classificada por endereço, quando um processo termina, a área que ele ocupava pode se juntar a uma ou duas lacunas adjacentes formando uma só lacuna.

5.4.3.1 Algoritmo do Primeiro Ajuste (First-fit)

Esse algoritmo escolhe a primeira partição livre de tamanho suficiente para carregar o processo. Ele costuma ser muito rápido por pesquisar o mínimo possível.

Nesse algoritmo a lista de áreas livres costuma estar ordenada por endereço.

5.4.3.2 Algoritmo do Próximo Ajuste

É semelhante ao algoritmo do primeiro ajuste, mas inicia sua procura a partir do ponto em que parou na procura anterior.

5.4.3.3 Algoritmo do Melhor Ajuste (Best-fit)

Esse algoritmo escolhe a partição em que o processo deixa o menor espaço sem utilização. Nesse algoritmo, se a lista de áreas livres não estiver ordenada por tamanho, o tempo de busca por uma área desocupada pode comprometer seu desempenho.

Uma desvantagem desse método é que é alocada a partição que deixa a menor área livre, existindo uma tendência de que a memória fique com pequenas áreas não contíguas, aumentando o problema da fragmentação.

5.4.3.4 Algoritmo do Pior Ajuste (Worst-fit)

Esse algoritmo escolhe a partição em que o processo deixa o maior espaço sem utilização. Ao utilizar as partições maiores, são deixados espaços livres maiores que permitem a um maior número de programas utilizar a memória, diminuindo o problema da fragmentação. Simulações mostraram que este algoritmo não traz bons resultados.

5.4.3.5 Algoritmo do Ajuste Rápido

Este algoritmo mantém listas separadas para alguns tamanhos mais comuns. Ele também gera lacunas pequenas, mas é extremamente rápido na alocação de memória.

Simulações mostraram que dentre os algoritmos acima o que obteve melhores resultados foi o algoritmo do primeiro ajuste, sendo o mais rápido e o que consome menos recursos do sistema.

A lista de lacunas pode ser mantida ordenada de formas diferentes a fim de beneficiar o

algoritmo que estiver em uso.

Um outro aspecto a ser levado em conta é que processos podem crescer. Portanto é bom deixar lacunas entre os processos para que seja permitido o seu crescimento.

5.5

Swapping

Mesmo com o aumento da eficiência da gerência de memória, muitas vezes um programa não podia ser executado por falta de uma partição livre disponível. A técnica de swapping veio tentar resolver o problema da insuficiência de memória.

Nos esquemas anteriores, um processo permanecia na memória principal até o final da sua execução. Eles funcionam bem para sistemas em lote, mas para sistemas de tempo compartilhado uma nova estratégia deve ser adotada, uma vez que pode não haver memória suficiente para armazenar todos os processos.

O swapping é uma técnica aplicada à gerência de memória para programas que esperam por

memória livre para serem processados. O sistema escolhe um processo que é levado da memória principal para o disco (swap out), retornando posteriormente para a memória principal (swap in).

Um dos problemas gerados pelo swapping é a relocação dos processos. No caso de um processo que sai e volta muitas vezes para a memória, é necessário que a relocação seja realizada pelo loader a cada carregamento. Essa situação torna o mecanismo ineficiente em função do tempo gasto para o carregamento.

A melhor solução é uma implementação em hardware para permitir que a relocação seja

realizada durante a execução do programa. Esse tipo de mecanismo é denominado relocação

dinâmica.

A relocação dinâmica é realizada através de um registrador especial denominado registrador

de relocação. No momento em que o processo é carregado na memória, o registrador recebe o endereço inicial da região de memória que o processo irá ocupar. Toda vez que ocorrer uma referência a algum endereço, o endereço contido na instrução será somado ao conteúdo do registrador, gerando o endereço físico. Assim um programa pode ser carregado em qualquer região da memória.

Quando o swap cria muitas lacunas pode-se ganhar espaço movendo os processos para realizar a compactação de memória. A compactação costuma ser evitada por tomar muito tempo da UCP.

Se o processo precisar alocar memória e existir uma lacuna entre ele e o processo adjacente, é alocado espaço desta lacuna. Se tal lacuna não existir o processo deve ser movido para uma lacuna com espaço suficiente ou então deve ser realizada uma compactação de memória. Se não existe espaço na memória nem em disco o processo deve esperar ou ser eliminado.

O conceito de swapping permitiu um maior compartilhamento da memória assim como um

maior throughput. A técnica se mostrou eficiente para sistemas com poucos usuários em ambientes

com aplicações pequenas. Seu maior problema é o elevado custo das operações de entrada/saída.

IBM são exemplos de sistemas operacionais que

implementam tal técnica.

O CTSS

do

MIT

e

o

OS/360

da