Baixe o app para aproveitar ainda mais
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.
Compartilhar