Este artigo detalha a implementação de diversas utilidades Unix no ambiente do sistema operacional Xv6, como parte dos exercícios práticos do curso MIT6.S081. Abordaremos a criação das ferramentas sleep, pingpong, primes, find e xargs, destacando os conceitos fundamentais de chamadas de sistema, comunicação entre processos e manipulação de arquivos.
- Implementando o Comando
sleep
O objetivo é criar uma utilidade sleep que pausa a execução do processo pelo número de segundos especificado como argumento.
Requisitos e Considerações
- Consulte arquivos como
user/echo.ceuser/grep.cpara entender a estrutura de programas de usuário no Xv6. - O programa deve imprimir uma mensagem de erro se nenhum argumento for fornecido.
- Argumentos da linha de comando são strings; utilize a função
atoipara convertê-los em inteiros. - Utilize a chamada de sistema
sleepfornecida pelo kernel. A implementação do kernel pode ser encontrada emkernel/sysproc.c(sys_sleep), e sua definição para programas de usuário emuser/user.h, com o stub de assembly emuser/usys.S. - Assegure que a função
mainfinalize comexit(). - Adicione o programa
sleepà listaUPROGSnoMakefilepara que seja compilado e executável no shell do Xv6.
Implementação
A implementação é direta. Após verificar a presença do argumento, o valor é convertido e passado para a chamada de sistema sleep.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h" // Inclui definições para chamadas de sistema como sleep e exit
int main(int argc, char *argv[]) {
if (argc < 2) { // Verifica se há pelo menos um argumento (o nome do programa + o tempo)
fprintf(2, "Uso: sleep <segundos>\n"); // Escreve mensagem de erro para o stderr
exit(1); // Sai com código de erro
}
// Converte o primeiro argumento (após o nome do programa) para inteiro e chama sleep
sleep(atoi(argv[1]));
exit(0); // Sai com sucesso
}
Reflexões: exit() vs. return
É importante entender a distinção entre exit() e return, especialmente em main:
exit(status): Esta é uma função de biblioteca que encerra o processo chamador. Ela libera todos os recursos alocados pelo processo e retorna ostatuspara o sistema operacional. Umstatusde 0 geralmente indica sucesso, enquanto um valor diferente de zero indica um erro.return value: Usado para sair de uma função e retornar um valor para a função que a chamou. Emmain, umreturn 0;é equivalente aexit(0);, pois o valor de retorno damainé o status de saída do processo. No entanto,exit()pode ser chamado de qualquer lugar no programa para terminar o processo imediatamente, sem desenrolar a pilha de chamadas, enquantoreturnapenas termina a função atual.
- Comunicação entre Processos com
pingpong
Este exercício consiste em implementar uma comunicação básica entre um processo pai e um processo filho usando pipes.
Requisitos e Considerações
- Crie um par de pipes (um para cada direção) para que o pai e o filho possam trocar um byte.
- O processo pai envia um byte ao filho.
- O filho recebe o byte, imprime
<pid>: received ping(onde<pid>é seu ID de processo), então envia um byte de volta ao pai e termina. - O pai lê o byte enviado pelo filho, imprime
<pid>: received ponge termina. - Utilize
pipe()para criar os canais,fork()para criar o processo filho,read()ewrite()para I/O nos pipes, egetpid()para obter o ID do processo. - Lembre-se de fechar as extremidades não utilizadas dos pipes em ambos os processos para evitar bloqueios e vazamento de descritores de arquivo.
Implementação
São usados dois pipes para comunicação bidirecional. O processo pai escreve em um pipe e lê do outro, e o filho faz o inverso.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
int pai_para_filho[2]; // Pipe para comunicação do pai para o filho
int filho_para_pai[2]; // Pipe para comunicação do filho para o pai
int id_processo;
char buffer_msg[10]; // Buffer para armazenar as mensagens "ping" ou "pong"
// Cria os pipes
if (pipe(pai_para_filho) < 0 || pipe(filho_para_pai) < 0) {
fprintf(2, "Erro ao criar pipes.\n");
exit(1);
}
id_processo = fork(); // Cria um novo processo
if (id_processo == 0) { // Processo Filho
close(pai_para_filho[1]); // Fecha a extremidade de escrita do pipe pai_para_filho
close(filho_para_pai[0]); // Fecha a extremidade de leitura do pipe filho_para_pai
// Lê a mensagem do pai (neste caso, "ping")
read(pai_para_filho[0], buffer_msg, sizeof("ping"));
printf("%d: received %s\n", getpid(), buffer_msg);
// Escreve a mensagem para o pai ("pong")
write(filho_para_pai[1], "pong", sizeof("pong"));
close(pai_para_filho[0]); // Fecha a extremidade de leitura restante
close(filho_para_pai[1]); // Fecha a extremidade de escrita restante
exit(0);
} else { // Processo Pai
close(pai_para_filho[0]); // Fecha a extremidade de leitura do pipe pai_para_filho
close(filho_para_pai[1]); // Fecha a extremidade de escrita do pipe filho_para_pai
// Escreve a mensagem para o filho ("ping")
write(pai_para_filho[1], "ping", sizeof("ping"));
close(pai_para_filho[1]); // Fecha a extremidade de escrita restante
// Espera o filho terminar e, em seguida, lê a mensagem
wait(0); // Espera por qualquer processo filho
read(filho_para_pai[0], buffer_msg, sizeof("pong"));
printf("%d: received %s\n", getpid(), buffer_msg);
close(filho_para_pai[0]); // Fecha a extremidade de leitura restante
exit(0);
}
}
Reflexões
A gestão dos descritores de arquivo (especialmente o fechamento das extremidades não utilizadas dos pipes) é crucial. Se uma extremidade de escrita não for fechada, um processo que tenta ler pode ficar bloqueado indefinidamente, esperando por dados que nunca virão, pois o sistema pensa que a extremidade de escrita ainda está aberta.
- Gerador de Números Primos com
primes
Este exercício implementa um "sieve de Eratóstenes" distribuído usando processos e pipes para encontrar números primos.
Requisitos e Considerações
- Utilize
pipe()efork()para configurar um pipeline de processos. - O processo inicial alimenta o primeiro pipe com números de 2 a 35 (ou até 35).
- Para cada número primo encontrado, um novo processo é criado. Este processo lê números de um pipe à sua esquerda, filtra os múltiplos do seu primo e escreve os números restantes em um pipe à sua direita.
- Tenha muito cuidado para fechar os descritores de arquivo que não são necessários em cada processo, para não esgotar os recursos do Xv6.
- Quando o lado de escrita de um pipe é fechado,
read()retorna zero. Isso é crucial para a condição de terminação da recursão. - O método mais simples é escrever inteiros de 32 bits (4 bytes) diretamente nos pipes, em vez de usar I/O formatado ASCII.
- O processo principal deve esperar que toda a cadeia de processos filho termine antes de sair.
Implementação
A solução utiliza uma função recursiva (filtra_primos) para criar a cadeia de processos. Cada processo filho é responsável por um primo específico.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#define EXTREMIDADE_ESCRITA 1
#define EXTREMIDADE_LEITURA 0
// Função recursiva para processar e filtrar números primos
void filtra_primos(int pipe_entrada[]) {
int numero_primo_atual;
close(pipe_entrada[EXTREMIDADE_ESCRITA]); // Este processo só lê do pipe de entrada
// Se não conseguir ler o primeiro número, significa que o pipe de entrada está vazio
if (read(pipe_entrada[EXTREMIDADE_LEITURA], &numero_primo_atual, sizeof(numero_primo_atual)) == 0) {
close(pipe_entrada[EXTREMIDADE_LEITURA]); // Fecha a extremidade de leitura e sai
exit(0);
}
printf("prime %d\n", numero_primo_atual); // O primeiro número lido é um primo
int pipe_saida[2]; // Cria um novo pipe para o próximo filtro
pipe(pipe_saida);
int pid_filho = fork(); // Cria um novo processo filho
if (pid_filho == 0) { // Código do Processo Filho
filtra_primos(pipe_saida); // Chama a função recursivamente para o filho
} else { // Código do Processo Pai
int numero_lido;
close(pipe_saida[EXTREMIDADE_LEITURA]); // O pai escreve para o pipe de saída do filho
// Lê números do pipe de entrada e os passa para o pipe de saída do filho se não forem múltiplos do primo atual
while (read(pipe_entrada[EXTREMIDADE_LEITURA], &numero_lido, sizeof(numero_lido)) != 0) {
if (numero_lido % numero_primo_atual != 0) {
write(pipe_saida[EXTREMIDADE_ESCRITA], &numero_lido, sizeof(numero_lido));
}
}
close(pipe_entrada[EXTREMIDADE_LEITURA]); // Fecha a extremidade de leitura do pipe de entrada
close(pipe_saida[EXTREMIDADE_ESCRITA]); // Fecha a extremidade de escrita do pipe de saída
wait(0); // Espera o processo filho terminar antes de sair
}
exit(0);
}
int main(int argc, char *argv[]) {
int pipe_inicial[2];
pipe(pipe_inicial); // Cria o pipe inicial
int pid_root = fork(); // Cria o primeiro processo filho
if (pid_root == 0) { // Código do Processo Filho (primeiro filtro)
filtra_primos(pipe_inicial);
} else { // Código do Processo Pai (alimenta o pipeline)
close(pipe_inicial[EXTREMIDADE_LEITURA]); // O pai só escreve para o pipe inicial
// Envia números de 2 a 35 para o pipe
for (int i = 2; i < 36; i++) {
write(pipe_inicial[EXTREMIDADE_ESCRITA], &i, sizeof(i));
}
close(pipe_inicial[EXTREMIDADE_ESCRITA]); // Fecha a extremidade de escrita, sinalizando EOF
wait(0); // Espera o primeiro processo filho (e toda a cadeia) terminar
}
exit(0);
}
Reflexões
A solução se benfeicia da recursão para modelar a cadeia de filtros. O aspecto mais crítico é o gerenciamento de descritores de arquivo: cada processo deve fechar as extremidades dos pipes que não utilizará. Falhar nisso pode levar a esgotamento de descritores de arquivo ou processos bloqueados indefinidamente. O uso de wait(0) no processo pai garante que todos os processos na cadeia de recursão sejam encerrados antes que o processo raiz saia.
- Implementando o Comando
find
O objetivo é criar uma versão simplificada do utilitário find que busca um arquivo pelo nome em um diretório e seus subdiretórios.
Requisitos e Considerações
- Consulte
user/ls.cpara entender como ler o conteúdo de um diretório. - Use recursão para explorar subdiretórios, mas ignore as entradas especiais
"."e"..". - A comparação de strings em C deve ser feita com
strcmp(), não com o operador==.
Implementação
A função procurar_arquivo é recursiva. Ela abre um diretório, itera sobre suas entradas e, para cada uma, verifica se é o arquivo procurado ou um subdiretório a ser explorado.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h" // Contém struct dirent e constantes de tipo de arquivo
#include "kernel/fcntl.h" // Contém flags para open
// Função auxiliar para extrair o nome base de um caminho (e.g., "dir/file" -> "file")
char* obter_nome_base(char *caminho) {
char *ponteiro;
// Itera de trás para frente para encontrar a última barra '/'
for(ponteiro = caminho + strlen(caminho); ponteiro >= caminho && *ponteiro != '/'; ponteiro--);
ponteiro++; // Avança para o caractere após a barra
return ponteiro;
}
// Função principal para procurar o arquivo recursivamente
void procurar_arquivo(char *caminho_atual, char *nome_alvo) {
char buffer_caminho[512], *p;
int fd;
struct dirent entrada_dir; // Estrutura para entrada de diretório
struct stat info_stat; // Estrutura para informações do arquivo/diretório
// Tenta abrir o caminho atual
if((fd = open(caminho_atual, O_RDONLY)) < 0){
fprintf(2, "find: não foi possível abrir %s\n", caminho_atual);
return;
}
// Obtém informações do arquivo/diretório
if(fstat(fd, &info_stat) < 0){
fprintf(2, "find: não foi possível obter stat de %s\n", caminho_atual);
close(fd);
return;
}
switch(info_stat.type){
case T_FILE: // Se for um arquivo
// Compara o nome base do arquivo com o nome alvo
if(strcmp(obter_nome_base(caminho_atual), nome_alvo) == 0)
printf("%s\n", caminho_atual); // Imprime o caminho completo se for uma correspondência
break;
case T_DIR: // Se for um diretório
// Verifica se o buffer é grande o suficiente para o novo caminho
if(strlen(caminho_atual) + 1 + DIRSIZ + 1 > sizeof(buffer_caminho)){
printf("find: caminho muito longo\n");
break;
}
strcpy(buffer_caminho, caminho_atual); // Copia o caminho atual para o buffer
p = buffer_caminho + strlen(buffer_caminho); // Posiciona o ponteiro no final do caminho
*p++ = '/'; // Adiciona uma barra para formar um novo caminho (e.g., "dir/" )
// Lê cada entrada do diretório
while(read(fd, &entrada_dir, sizeof(entrada_dir)) == sizeof(entrada_dir)){
// Ignora entradas vazias e os diretórios especiais "." e ".."
if(entrada_dir.inum == 0 || strcmp(entrada_dir.nome, ".") == 0 || strcmp(entrada_dir.nome, "..") == 0)
continue;
// Copia o nome da entrada para formar o novo caminho (e.g., "dir/nome_entrada")
memmove(p, entrada_dir.nome, DIRSIZ);
p[DIRSIZ] = 0; // Garante terminação nula da string
// Obtém informações da nova entrada
if(stat(buffer_caminho, &info_stat) < 0){
printf("find: não foi possível obter stat de %s\n", buffer_caminho);
continue;
}
procurar_arquivo(buffer_caminho, nome_alvo); // Chama recursivamente para a nova entrada
}
break;
}
close(fd); // Fecha o descritor de arquivo do diretório
}
int main(int argc, char *argv[]) {
if(argc != 3){
printf("Uso: find <diretorio_inicial> <nome_do_arquivo>\n");
exit(1);
}
procurar_arquivo(argv[1], argv[2]); // Inicia a busca
exit(0);
}
Reflexões
O coração do find é a travessia recursiva do sistema de arquivos. É fundamental lidar com os diretórios especiais "." e ".." para evitar loops infinitos. A estrutura struct dirent, definida em kernel/fs.h, é usada para armazenar o número do inode e o nome de cada entrada de diretório. A função stat() é vital para determinar se uma entrada é um arquivo ou um diretório.
struct dirent {
ushort inum; // Número do inode
char name[DIRSIZ]; // Nome do arquivo
};
- Implementando o Comando
xargs
O programa xargs lê itens de entrada padrão, separados por espaços ou quebras de linha, e executa um comando especificado, passando esses itens como argumentos.
Requisitos e Considerações
- Utilize
fork()eexec()para invocar o comando especificado. - No processo pai, use
wait()para aguardar a conclusão do processo filho. - Para ler a entrada, leia um caractere por vez até encontrar uma quebra de linha (
'\n') ou espaço (' '), delimitando os argumentos. kernel/param.hdeclaraMAXARG, que pode ser útil para dimensionar o array de argumentos.
Implementação
A complexidade reside em analisar a entrada padrão, construindo dinamicamente o array de argumentos para exec().
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h" // Para MAXARG
#define TAM_BUFFER_ENTRADA 1024 // Tamanho máximo do buffer para uma linha de entrada
int main(int argc, char *argv[]) {
char *argumentos_exec[MAXARG]; // Array de ponteiros para strings para exec()
char buffer_entrada[TAM_BUFFER_ENTRADA]; // Buffer para ler a entrada padrão
int i;
if (argc < 2) {
fprintf(2, "Uso: xargs <comando> [argumentos_iniciais...]\n");
exit(1);
}
// Copia os argumentos iniciais passados para xargs (e.g., "ls", "-l")
int arg_fixo_count = 0;
for (i = 1; i < argc; i++) {
argumentos_exec[arg_fixo_count++] = argv[i];
}
// Loop principal para ler do stdin linha por linha
while (1) {
int buffer_idx = 0; // Posição atual no buffer_entrada
int start_of_arg = 0; // Início do argumento atual dentro do buffer
int arg_stdin_count = 0; // Número de argumentos lidos do stdin para a linha atual
char caractere_lido;
int bytes_lidos;
// Loop para ler caracteres até encontrar uma quebra de linha ou EOF
while ((bytes_lidos = read(0, &caractere_lido, sizeof(caractere_lido))) > 0) {
if (buffer_idx >= TAM_BUFFER_ENTRADA - 1) { // Prevenção de overflow
fprintf(2, "xargs: linha de entrada muito longa\n");
exit(1);
}
if (caractere_lido == ' ' || caractere_lido == '\n') {
if (buffer_idx > start_of_arg) { // Se há um argumento não vazio
buffer_entrada[buffer_idx] = '\0'; // Termina a string do argumento
// Adiciona o ponteiro para o início do argumento ao array de argumentos
argumentos_exec[arg_fixo_count + arg_stdin_count] = &buffer_entrada[start_of_arg];
arg_stdin_count++;
}
start_of_arg = buffer_idx + 1; // Próximo argumento começará após este caractere
if (caractere_lido == '\n') break; // Fim da linha, executar o comando
} else {
buffer_entrada[buffer_idx] = caractere_lido;
}
buffer_idx++;
}
if (bytes_lidos == 0 && arg_stdin_count == 0 && buffer_idx == start_of_arg) {
// EOF e nenhuma entrada pendente para processar
break;
}
// Adiciona um terminador NULL ao array de argumentos para exec()
argumentos_exec[arg_fixo_count + arg_stdin_count] = (char *)0;
int pid_comando = fork();
if (pid_comando == 0) { // Processo Filho
exec(argumentos_exec[0], argumentos_exec);
fprintf(2, "xargs: falha na execução do comando %s\n", argumentos_exec[0]);
exit(1); // Sai com erro se exec falhar
} else { // Processo Pai
wait((int *)0); // Espera o comando filho terminar
}
}
exit(0);
}
Reflexões
O desafio principal do xargs é a análise da entrada (parsing) e a construção dinâmica do array de argumentos. O array argumentos_exec precisa conter o nome do comando a ser executado como seu primeiro elemento (argumentos_exec[0]), seguido pelos argumentos iniciais de xargs, e finalmente os argumentos lidos da entrada padrão. Um terminador NULL é essencial para o array de argumentos de exec(). O uso de fork() e exec() é padrão para executar programas externos, e wait() garante que o xargs pai não continue lendo novas linhas antes que o comando anterior seja concluído.