Laboratórios Xv6 e Utilidades Unix: Implementando Ferramentas Básicas

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.

  1. 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.c e user/grep.c para 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 atoi para convertê-los em inteiros.
  • Utilize a chamada de sistema sleep fornecida pelo kernel. A implementação do kernel pode ser encontrada em kernel/sysproc.c (sys_sleep), e sua definição para programas de usuário em user/user.h, com o stub de assembly em user/usys.S.
  • Assegure que a função main finalize com exit().
  • Adicione o programa sleep à lista UPROGS no Makefile para 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 o status para o sistema operacional. Um status de 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. Em main, um return 0; é equivalente a exit(0);, pois o valor de retorno da main é 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, enquanto return apenas termina a função atual.
  1. 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 pong e termina.
  • Utilize pipe() para criar os canais, fork() para criar o processo filho, read() e write() para I/O nos pipes, e getpid() 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.

  1. 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() e fork() 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.

  1. 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.c para 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
};

  1. 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() e exec() 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.h declara MAXARG, 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.

Tags: xv6 unix-utilities system-calls c-programming operating-systems

Publicado em 7-30 17:54