Prévia do material em texto
UNIVERSIDADE VEIGA DE ALMEIDA
ALUNOS
LEONARDO ANDRADE FONTES DOS SANTOS
MATHEUS MOTA
RAFAEL MACHADO PEÇANHA LEAL
TALITTA GALVÃO
ARQUITETURA E FUNDAMENTOS DOS SISTEMAS OPERACIONAIS
TIJUCA – RIO DE JANEIRO
03/10/2023
1
ENGENHARIA E CIÊNCIA DA COMPUTAÇÃO
AVALIAÇÃO AV1
ARQUITETURA E FUNDAMENTOS DOS SISTEMAS OPERACIONAIS
Trabalho apresentado no curso de Engenharia e
Ciência da Computação da Universidade Veiga de
Almeida, na disciplina de Arquitetura e Fundamentos
dos Sistemas Operacionais, como Trabalho Avaliativo
para compor a nota de A1 de 2023.2
Professor: Miguel Angelo Zaccur de Figueiredo
TIJUCA – RIO DE JANEIRO
03/10/2023
2
SUMÁRIO:
1. PROPOSTA 1 (Grupo) - 1ª Atividade 3
2. PROPOSTA 2 (Individual)- 1ª Atividade 12
3. PROPOSTA 2 (Individual)- 2ª Atividade 17
4. REFERÊNCIAS 20
1ª Atividade: (valor 5,0 pontos; Competência: Aplicação; Ref.: Enade 2008)
Problema do Produtor-Consumidor
“O problema clássico da área de estudo dos Sistemas Operacionais chamado de produtor-
consumidor (também conhecido como problema do buffer limitado) é um exemplo clássico
de problema de sincronização multi-processo. O problema descreve dois processos, o
produtor e o consumidor, que compartilham um buffer de tamanho fixo. O trabalho do produtor
é gerar um dado, colocá-lo no buffer e repetir essa operação indefinidamente. Ao mesmo
tempo, a tarefa do consumidor é consumir tais dados (i.e. removendo do buffer), um de cada
vez. O problema consiste em assegurar que o produtor não irá tentar adicionar dados no
3
buffer quando este estiver cheio, e que o consumidor não tentará remover dados quando o
buffer estiver vazio..” (TANENBAUM, Sistemas Operacionais, 4ª Ed, Capítulo 2).
Desenvolva e implemente em qualquer linguagem de programação, um algoritmo que
reproduza o problema descrito acima.
DESENVOLVIMENTO
Linguagem utilizada: C
Compilador utilizado para testes de funcionalidades: Online C Compiler
(https://www.onlinegdb.com/online_c_compiler)
Iniciaremos o desenvolvimento da questão fornecendo um exemplo simples em
linguagem C, que demonstra o problema do produtor-consumidor, onde ocorre um conflito
de acesso ao buffer compartilhado entre threads produtoras e consumidoras sem a devida
4
sincronização. O exemplo representará uma situação onde as threads podem operar no
buffer sem garantir acesso exclusivo, ocasionando em resultados inesperados.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define NUM_PRODUCERS 2
#define NUM_CONSUMERS 2
int buffer[BUFFER_SIZE];
int count = 0; // Contador de itens no buffer
void *producer(void *arg) {
int item = 1;
while (1) {
if (count < BUFFER_SIZE) {
buffer[count++] = item;
printf("Produzido: %d\n", item);
item++;
}
sleep(1); // Simula produção
}
}
void *consumer(void *arg) {
while (1) {
if (count > 0) {
5
int item = buffer[--count];
printf("Consumido: %d\n", item);
}
sleep(1); // Simula consumo
}
}
int main() {
pthread_t producer_threads[NUM_PRODUCERS];
pthread_t consumer_threads[NUM_CONSUMERS];
for (int i = 0; i < NUM_PRODUCERS; i++) {
pthread_create(&producer_threads[i], NULL, producer, NULL);
}
for (int i = 0; i < NUM_CONSUMERS; i++) {
pthread_create(&consumer_threads[i], NULL, consumer, NULL);
}
for (int i = 0; i < NUM_PRODUCERS; i++) {
pthread_join(producer_threads[i], NULL);
}
for (int i = 0; i < NUM_CONSUMERS; i++) {
pthread_join(consumer_threads[i], NULL);
}
return 0;
6
}
Aqui está uma breve descrição do que está acontecendo no código:
1. Existem duas funções principais: producer e consumer, que são executadas pelas
threads produtoras e consumidoras, respectivamente.
2. As threads produtoras tentam adicionar itens ao buffer, enquanto as threads
consumidoras tentam remover itens do buffer.
3. O buffer é uma matriz simples de tamanho fixo (BUFFER_SIZE) e um contador count
é usado para rastrear o número de itens no buffer.
4. As operações de produção e consumo são realizadas sem sincronização adequada,
o que significa que várias threads podem acessar o buffer simultaneamente sem
exclusão mútua.
Agora, vamos representar em linguagem C um exemplo de um programa em C que
demonstra uso de threads, semáforos e mutexes para implementar a solução para o
problema do produtor-consumidor. Neste problema, há um buffer compartilhado entre as
threads produtoras e consumidoras, e o objetivo é garantir que as threads produtoras não
7
insiram elementos no buffer quando ele estiver cheio e que as threads consumidoras não
renovam elementos do buffer quando estiver vazio.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFF_SIZE 20
// Estrutura para representar o buffer compartilhado
typedef struct {
int values[BUFF_SIZE]; // Array que armazena os valores no buffer
int last; // Índice para o próximo local disponível no buffer
} buffer_t;
// Função para inicializar o buffer
void buffer_init(buffer_t *buffer);
// Função para inserir um valor no buffer
void buffer_insert(buffer_t *buffer, int value);
// Função para remover um valor do buffer
int buffer_remove(buffer_t *buffer);
void buffer_init(buffer_t *buffer) {
buffer->last = 0; // Inicializa o índice do último valor como zero
}
8
void buffer_insert(buffer_t *buffer, int value) {
buffer->values[buffer->last++] = value; // Insere um valor no buffer e atualiza o índice
}
int buffer_remove(buffer_t *buffer) {
int elem = buffer->values[0]; // Obtém o primeiro valor do buffer
buffer->last--; // Reduz o índice para indicar que um valor foi removido
for(int i = 0; i < buffer->last; i++) {
buffer->values[i] = buffer->values[i + 1]; // Desloca os valores restantes no buffer
}
return elem; // Retorna o valor removido
}
#define MAX 1000
sem_t empty; // Semáforo para controlar espaços vazios no buffer
sem_t full; // Semáforo para controlar espaços preenchidos no buffer
pthread_mutex_t mutex; // Mutex para garantir acesso exclusivo ao buffer
// Função que simula a produção de um item
int produce() {
sleep(1);
int i = rand() % 10; // Gera um número aleatório entre 0 e 9
printf("produzindo... %d!\n", i);
return i;
}
9
// Função que simula o consumo de um item
void consume(int value) {
sleep(1);
printf("consome:%d\n", value); // Imprime o valor consumido
fflush(stdout); // Força a impressão imediata
}
// Função executada pelas threads produtoras
void *producer(void * arg) {
buffer_t *buffer = (buffer_t *) arg; // Converte o argumento para o tipo buffer_t
for (int i = 0; i < MAX; i++) {
int value = produce(); // Produz um valor
sem_wait(&empty); // Aguarda espaço vazio no buffer
pthread_mutex_lock(&mutex);
buffer_insert(buffer, value); // Insere o valor no buffer
pthread_mutex_unlock(&mutex);
sem_post(&full); // Sinaliza que o buffer não está vazio
}
pthread_exit(NULL);
}
// Função executada pelas threads consumidoras
void *consumer(void * arg) {
buffer_t *buffer = (buffer_t *) arg; // Converte o argumento para o tipo buffer_t
for (int i = 0; i < MAX; i++) {
sem_wait(&full); // Aguarda o buffer não estar vazio
pthread_mutex_lock(&mutex);
int value = buffer_remove(buffer); // Remove um valor do buffer10
pthread_mutex_unlock(&mutex);
sem_post(&empty); // Sinaliza que o buffer não está cheio
consume(value); // Consome o valor
}
pthread_exit(NULL);
}
int main() {
pthread_t pro, con; // Variáveis para as threads produtoras e consumidoras
buffer_t buffer; // O buffer compartilhado
srand(time(NULL)); // Inicializa o gerador de números aleatórios com uma semente
baseada no tempo
buffer_init(&buffer); // Inicializa o buffer
pthread_mutex_init(&mutex, NULL); // Inicializa o mutex
sem_init(&empty, 0, BUFF_SIZE); // Inicializa o semáforo "empty" com o tamanho
máximo do buffer
sem_init(&full, 0, 0); // Inicializa o semáforo "full" com zero itens no buffer
pthread_create(&pro, NULL, &producer, (void *) &buffer); // Cria a thread produtora
pthread_create(&con, NULL, &consumer, (void *) &buffer); // Cria a thread consumidora
pthread_join(pro, NULL); // Aguarda o término da thread produtora
pthread_join(con, NULL); // Aguarda o término da thread consumidora
sem_destroy(&full); // Destroi o semáforo "full"
sem_destroy(&empty); // Destroi o semáforo "empty"
pthread_mutex_destroy(&mutex); // Destroi o mutex
return 0; // Encerra o programa
}
Aqui está uma breve explicação das principais partes do código:
11
1. buffer_t é uma estrutura que representa o buffer compartilhado, que contém um array
de valores (values) e um índice para o próximo local disponível no buffer (last).
2. As funções buffer_init, buffer_insert, e buffer_remove são usadas para inicializar o
buffer, inserir valores nele e remover valores dele, respectivamente.
3. As bibliotecas <stdio.h>, <stdlib.h>, <unistd.h>, <pthread.h>, e <semaphore.h> são
incluídas para fornecer funcionalidades de E/S, manipulação de threads, e
semáforos.
4. As constantes MAX e BUFF_SIZE definem o número máximo de itens a serem
produzidos e o tamanho máximo do buffer, respectivamente.
5. São definidos três objetos para controlar a sincronização entre as threads: sem_t
empty (semáforo para controlar espaços vazios no buffer), sem_t full (semáforo para
controlar espaços preenchidos no buffer) e pthread_mutex_t mutex (mutex para
garantir acesso exclusivo ao buffer).
6. As funções produce e consume simulam a produção e o consumo de itens,
respectivamente, com um pequeno atraso usando sleep.
7. As funções producer e consumer são as funções que as threads produtoras e
consumidoras executam, respectivamente. Elas implementam a lógica de produção
e consumo, usando semáforos para garantir que as threads esperem quando o
buffer estiver cheio (produtoras) ou vazio (consumidoras).
8. No main, as threads produtoras e consumidoras são criadas usando pthread_create.
Além disso, os semáforos e o mutex são inicializados e, no final do programa, eles
são destruídos para liberar os recursos alocados.
Em resumo o código ilustra como as threads podem compartilhar um recurso (o
buffer) de forma segura para evitar problemas de concorrência.
PROPOSTA2-Individual
1ª Atividade: (Valor 3,0 pontos; Competência: Síntese – 15 linhas; Ref.: Enade 2008)
A tabela abaixo relaciona e compara os tempos de latência de duas operações, em
um sistema operacional Unix sob uma arquitetura monoprocessada, implementadas de 3
formas diferentes: por threads a nível de usuário (implementadas pela aplicação), por threads
12
a nível de núcleo (escalonamento e chaveamento pelo núcleo), e por meio de processos
(TANENBAUM, Sistemas Operacionais, 4ª Ed, Capitulo 2).
Em função
dos dados apresentados na tabela acima:
a) Explique os prováveis motivos que embasam as diferenças de tempo de latências
observadas.
b) Elabore um programa em qualquer linguagem de programação que evidencie a
concorrência entre três instâncias de uma thread cujo objetivo é apenas imprimir seu
identificador (ID).
Resposta (a):
Um processo é uma instância individual de um programa em execução e é
representado no sistema operacional por estruturas de controle. Cada processo possui seu
próprio espaço de endereçamento, incluindo os valores atuais dos registradores da CPU. Os
processos competem pela atenção da CPU, geralmente permitindo apenas um processo em
execução por vez, seguindo uma única trilha de execução. Cada processo é criado por meio
de chamadas de sistema, como o "fork" no UNIX. Quando um processo pai cria um processo
filho, o processo filho tem seu próprio espaço de endereçamento, e essa criação pode ser
repetida, resultando em uma hierarquia de processos. No entanto, uma limitação é que, sem
o uso de threads, todos os processos criados competem pela CPU, potencialmente levando
a muitos processos aguardando acesso à CPU e gerando sobrecarga.
Threads, por outro lado, permitem paralelismo em um programa de aplicação,
permitindo que múltiplas atividades sejam executadas simultaneamente. Cada thread possui
sua própria pilha de execução, mas compartilha o mesmo espaço de endereçamento com
outras threads no mesmo processo. Isso torna as threads mais leves em termos de recursos
em comparação com os processos, uma vez que compartilham recursos comuns. As threads
podem ser criadas em dois modelos principais: threads de usuário e threads de núcleo.
Threads de usuário são criadas por meio de rotinas de biblioteca e são responsáveis
por implementar procedimentos de gerenciamento de threads. Elas incluem funcionalidades
para criar, destruir e bloquear threads, bem como algoritmos de escalonamento. O núcleo do
13
sistema operacional não reconhece diretamente essas threads, mas executa o
escalonamento no contexto das threads de usuário. Isso significa que o tempo de
processamento é distribuído pelo escalonador de threads da biblioteca, não envolvendo
diretamente o núcleo do sistema.
Threads de núcleo, por outro lado, são criadas diretamente pelo núcleo do sistema
operacional. Cada operação de manipulação de threads é mapeada para uma rotina de
biblioteca que empacota os parâmetros da chamada e executa uma troca de contexto. Isso
permite que o núcleo do sistema gerencie diretamente as operações das threads, mantendo
informações de contexto sobre threads e processos pesados. Assim, cada thread no
programa de aplicação corresponde a uma thread no núcleo.
Os valores fornecidos pela questão representam o tempo de latência para diferentes
operações em três cenários distintos: threads de nível de usuário, threads de nível de núcleo
e processos. Os números são apenas ilustrativos e podem variar de acordo com a
implementação e o ambiente do sistema. Vou tentar explicar essas operações em mais
detalhes:
1. FORK NULO:
• Threads de Nível de Usuário: 34 unidades de tempo.
• Threads de Nível de Núcleo: 948 unidades de tempo.
• Processos: 11.300 unidades de tempo.O "FORK NULO" se refere ao
tempo necessário para criar uma instância de thread ou processo, mas
sem alocar recursos adicionais. Isso é usado para medir o custo puro de
criação.
• Os números indicam que criar uma thread de nível de usuário é mais
eficiente em termos de tempo do que criar uma thread de nível de núcleo
14
ou um novo processo. Threads de nível de usuário tendem a ser mais
leves em termos de criação.
• A criação de um novo processo é a operação mais custosa em termos
de tempo, pois envolve a alocação de um espaço de endereçamento
separado e a duplicação de recursos.
2. SIGNAL-WAIT:
• Threads de Nível de Usuário: 37 unidades de tempo.
• Threads de Nível de Núcleo: 441 unidades de tempo.
• Processos: 1.840 unidades de tempo.
• O "SIGNAL-WAIT" refere-se ao tempo necessário para um thread ou
processo esperar por um sinal ou evento antes de continuar sua
execução.
• Os números indicam que a espera por eventos é mais eficiente em
threads de nível de usuário em comparação com threads de nível de
núcleo ou processos.
• Threads de nível de núcleotêm um desempenho intermediário nessa
operação.
• A operação é mais custosa em termos de tempo quando realizada em
processos devido à sobrecarga associada ao gerenciamento de
processos separados.
É importante notar que o chaveamento (troca) das threads dentro do mesmo processo
pesado envolve o núcleo do sistema, o que pode ter um impacto significativo no desempenho
do sistema como um todo, uma vez que as operações das threads são gerenciadas
diretamente pelo sistema operacional. Portanto, a escolha entre processos e threads
depende das necessidades específicas do programa e dos requisitos de desempenho. Esses
números destacam a importância de escolher a abordagem certa (threads ou processos) com
base nas necessidades específicas do aplicativo e nas características de desempenho
desejadas. Threads de nível de usuário geralmente são mais leves em termos de recursos e
têm menor latência em comparação com threads de nível de núcleo e processos, mas a
15
escolha depende das características de escalabilidade, compartilhamento de recursos e
sincronização do aplicativo.
Resposta (b):
Linguagem utilizada: C
Compilador utilizado: Online C compiler (https://www.onlinegdb.com/online_c_compiler)
#include <stdio.h>
#include <pthread.h>
#define NUM_THREADS 3
// Função que será executada por cada thread
void *print_id(void *thread_id) {
int *id = (int *)thread_id;
printf("Thread %d está sendo executada.\n", *id);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS] = {254,255 ,300};
// Criação de três threads
for (int i = 0; i < NUM_THREADS; i++) {
if (pthread_create(&threads[i], NULL, print_id, &thread_ids[i]) != 0) {
perror("Erro ao criar a thread.");
return 1;
16
}
}
// Aguarda até que todas as threads terminem
for (int i = 0; i < NUM_THREADS; i++) {
if (pthread_join(threads[i], NULL) != 0) {
perror("Erro ao aguardar a thread.");
return 1;
}
}
printf("Todas as threads terminaram.\n");
return 0;
}
Descrição do código:
• Foram criadas três threads, cada uma com um identificador único.
• A função print_id é executada pela thread e imprime seu identificador no
console.
• Usamos a biblioteca pthread para criar e gerenciar as threads.
• Esperamos até que todas as threads tenham terminado utilizando
pthread_join.
Ao executar este programa, você verá que as três threads concorrem pela CPU e
imprimem seus identificadores (IDs) em qualquer ordem, demonstrando a concorrência entre
17
elas. O resultado pode variar a cada execução, já que a ordem de execução das threads não
é terminante.
2ª Atividade: (Valor 2,0 pontos; Competência: Compreensão; Ref: Enade 2008)
A máquina acima
ilustra os estados em que um processo pode se encontrar durante o seu ciclo de vida.
Elabore um programa em qualquer linguagem de programação que simule o funcionamento
desta máquina, incluindo as circunstâncias em que ocorrem as transições (TANENBAUM,
Sistemas Operacionais, 4ª Ed, Capitulo 1).
Resposta:
Linguagem utilizada: C
Compilador utilizado: Online C compiler (https://www.onlinegdb.com/online_c_compiler)
#include <stdio.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdbool.h>
#define RUNNING 1
#define BLOCKED 2
#define READY 3
#define EXIT 0
18
void print_status(int status) {
if (status == RUNNING) {
printf("Estado: Running\n");
} else if (status == READY) {
printf("Estado: Ready\n");
} else if (status == BLOCKED) {
printf("Estado: Blocked\n");
} else if (status == EXIT) {
printf("Saindo do programa.\n");
}
}
int main() {
int curr_status = RUNNING;
while (true) {
print_status(curr_status);
int next_status;
printf("\nDigite a próxima mudança de estado (0 para sair): ");
scanf("%d", &next_status);
if (next_status == EXIT) {
curr_status = EXIT;
break; // Sai do loop
}
if (curr_status == RUNNING) {
19
if (next_status == BLOCKED || next_status == READY) {
curr_status = next_status;
} else {
printf("\nNão é possível\n");
}
} else if (curr_status == BLOCKED) {
if (next_status == READY) {
curr_status = next_status;
} else {
printf("\nNão é possível\n");
}
} else if (curr_status == READY) {
if (next_status == RUNNING) {
curr_status = next_status;
} else {
printf("\nNão é possível\n");
}
}
}
return 0;
}
O código tem como objetivo simular os estados de execução de um processo em um
sistema operacional e permite a transição entre esses estados com base nas entradas do
usuário. O programa continuará a solicitar ao usuário que insira um próximo estado até que
o usuário opte por sair digitando 0. Quando o usuário digita 0, o programa exibe “Saindo do
programa” e termina a execução. Caso contrário, o programa verifica se a transição entre os
estados é válida e atualiza o estado atual de acordo com a entrada do usuário.
20
REFERÊNCIAS:
UNICAMP. MC514- Sistemas Operacionais: Teoria e Prática. Disponível em:
https://www.ic.unicamp.br/~islene/mc514/prod-cons/prod-cons.pdf. Acesso em: 30 de
Setembro de 2023.
Prof. Rodrigo de Souza Couto. Sistema Operacionais: Modelos e uso de threads.
Disponível em: https://www.youtube.com/watch?v=tl8NpQOVewI. Acesso em: 30 de
Setembro de 2023.
TANENBAUM, Andrew S. Sistemas Operacionais Modernos. 4ª edição. São Paulo:
Pearson Education do Brasil, 2016.