Buscar

AV1 - Sistemas Operacionais

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.

Mais conteúdos dessa disciplina