Buscar

aula_lab1

Prévia do material em texto

Aula de Laboratório 1: Processos em Java 
 
Em Java, a maioria das aplicações usam multithread para atingir o 
paralelismo, pois como veremos mais adiante na disciplina, 
threads são mais leves e possuem um desempenho melhor. 
Entretanto, vamos agora aprender um pouco sobre processos. 
A ideia é que o aluno tente fazer cada um dos exercícios sem a 
ajuda do professor. Ele pode consultar os colegas, a documentação 
oficial do Java, o chatGPT, enfim, qualquer recurso que desejar. 
Nesta aula de laboratório, você terá uma introdução as classes 
ProcessBuilder e Process em Java. Elas são usadas para executar 
comandos e programas no sistema operacional Linux a partir do 
seu programa Java. Ao longo da aula, você enfrentará 7 exercícios 
progressivamente mais desafiadores para aprofundar seu 
conhecimento sobre processos usando Java. 
A classe Process é mais simples e limitada, geralmente usada para 
iniciar o processo, esperar que outro processo termine e assim por 
diante. A classe ProcessBuilder permite mais configurações, 
definindo qual comando será executado, qual diretório, 
redirecionamento de E/S e assim por diante. 
Em cada exercício, lembre-se de abrir o terminal, compilar os 
arquivos criados e depois executar o programa principal. 
 
Exercício 1: Executando um Comando Simples 
Crie um programa Java que utilize a classe ProcessBuilder para 
executar o comando "ls" e imprima o resultado no console. Neste 
exercício, a ideia é criar um objeto ProcessBuilder para configurar 
o processo, redirecionando a E/S para a mesma do processo pai e 
depois um objeto Process para iniciá-lo. Então, o processo pai 
aguarda o processo filho encerrar e após isso imprime o código de 
saída do processo. Esse é um exemplo de situação em que o 
processo pai cria um processo filho e fica esperando ele encerrar, 
para então continuar com suas ações. 
Alguns métodos que você pode usar: start() para iniciar o processo 
filho, inheritIO() para redirecionar a E/S do processo filho para a 
mesma do processo pai, waitFor() para o processo pai esperar o 
processo filho terminar. Lembre-se de usar try/catch para 
IOException e InterruptedException. 
 
Exercício 2: Executando um comando definido pelo usuário e 
com argumentos. 
Modifique o programa anterior, de maneira que o usuário digite 
pelo teclado qual comando deseja executar e possa incluir 
argumentos, por exemplo, dig ufes.br. O resto do programa deve 
permanecer inalterado. 
 
Exercício 3: Redirecionando a Saída para um Arquivo 
Modifique o programa anterior, de maneira que a saída do 
processo filho seja salva em um arquivo “output.txt” ao invés de 
ser exibida no console. Após o programa encerrar, abra o arquivo 
e veja se o conteúdo está correto. 
 
Exercício 4: Definindo um Diretório de Trabalho 
Crie um programa que utilize o ProcessBuilder para executar o 
comando "ls" em um diretório que o usuário definiu pelo teclado. 
Certifique-se de que o comando seja executado no diretório 
especificado e imprima o resultado no console. 
 
Exercício 5: Servidor e cliente de hora 
Crie um processo servidor que fica escutando em uma porta para 
novas conexões. Quando ele recebe uma requisição de outro 
processo, deve enviar a hora atual do sistema. 
O servidor deve ficar num laço infinito, sempre esperando novas 
conexões. O cliente faz uma requisição, recebe a resposta e 
imprime no console e encerra. 
Você pode se basear nos códigos usando sockets, mostrados nas 
aulas anteriores. 
Abra duas abas no terminal, primeiro execute o servidor e depois 
na outra aba execute o cliente. 
 
Exercício 6: Processos concorrentes 
Crie um arquivo ProcessoFilho.java que possui um laço que 
imprime a mensagem “Processo filho: X”, onde X é o número 
variando entre 0 e 10. Após imprimir cada número, deve dormir 
por 2 segundos. Isso é para simular que algum tipo de 
processamento está sendo realizado dentro desse processo. 
Crie um arquivo ProcessoPai.java que faz a mesma coisa, porém 
dormindo a cada um segundo. Ambos os processos devem 
executar de maneira concorrente, imprimindo suas mensagens no 
console. O processo pai é que deve criar e iniciar o processo filho. 
Isso serve para vermos o escalonamento entre os processos, 
lembrando que poderíamos criar vários processos, mas vamos 
usar apenas dois para facilitar o entendimento. Observação: não 
esqueça de fazer o processo filho herdar a E/S do processo pai. 
 
 
Exercício 7: Usando memória compartilhada 
Como vimos em aula, temos duas opções para comunicação em 
processos na mesma máquina: memória compartilhada e 
passagem de mensagens. No exercício 5 usamos passagem de 
mensagem, vamos agora usar memória compartilhada. 
Crie um arquivo SharedMemoryWriter.java que recebe um texto 
digitado pelo usuário e salva em um arquivo texto, que será usado 
como memória compartilhada. 
Após isso, crie um arquivo SharedMemoryReader.java que irá ler 
o arquivo texto e transformar o conteúdo em uma variável de 
memória compartilhada e imprimir seu conteúdo no console. 
Você vai precisar das seguintes classes: RandomAccessFile, 
FileChannel, MappedByteBuffer. 
Nesse exercício, por simplificação, um processo escreve e encerra 
e depois outro processo lê o que foi escrito e encerra. Entretanto, 
podemos ter dois ou mais processos lendo e/ou escrevendo na 
memória compartilhada enquanto estão em execução. 
Veja as 5 diferenças entre usar MappedByteBuffer e simplesmente 
ler o arquivo texto: 
 Mapeamento Direto para a Memória (Mapping to Memory): 
Com MappedByteBuffer, você está mapeando uma região do 
arquivo diretamente para a memória, o que significa que os dados 
no arquivo são tratados como se estivessem na memória principal. 
Isso permite o acesso direto aos dados no arquivo como se fossem 
matrizes na memória. 
 Acesso Eficiente: O mapeamento direto para a memória é 
altamente eficiente para leitura e gravação, pois evita a 
necessidade de copiar dados entre o disco e a memória. Em vez 
disso, os dados são acessados diretamente na memória mapeada. 
 Cache do Sistema de Arquivos: O sistema operacional lida com 
a gestão da memória mapeada para o arquivo e geralmente 
mantém partes do arquivo em cache na memória física, o que 
pode melhorar ainda mais o desempenho de leitura, 
especialmente para acessos subsequentes aos mesmos dados. 
 
 Tamanho Flexível: O tamanho da região mapeada pode ser 
menor ou igual ao tamanho do arquivo, o que permite acessar 
partes específicas do arquivo sem carregar o arquivo inteiro na 
memória. 
 Sincronização entre Processos: O mapeamento para a memória 
também pode ser compartilhado entre processos, permitindo a 
comunicação eficiente entre processos por meio da memória 
compartilhada. 
Por outro lado, a leitura direta de um arquivo envolve a leitura de 
dados do arquivo para a memória em um buffer tradicional e, em 
seguida, o processamento desses dados a partir desse buffer. Isso 
pode ser menos eficiente para grandes arquivos, pois requer a 
cópia explícita dos dados. 
A escolha entre MappedByteBuffer e a leitura direta de um 
arquivo depende das necessidades específicas do seu aplicativo. 
MappedByteBuffer é uma escolha eficiente quando você precisa 
acessar partes específicas de um arquivo, deseja tirar vantagem do 
cache do sistema de arquivos e/ou precisa de uma maneira de 
compartilhar dados entre processos. Por outro lado, a leitura 
direta de um arquivo pode ser mais simples em casos em que você 
só precisa ler o arquivo sequencialmente e não tem necessidade 
de compartilhamento de memória.

Continue navegando