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.