Soluzione

Segue un esempio di soluzione del cracker multicore dove il limite massimo di processi da crackare è specificato dall’utente come primo parametro da riga di comando.

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#define BLOCK_SIZE 16

/* Leggi il contenuto del file prodotto dal figlio con process
 * identifier 'pid'. */
char* read_file(pid_t pid) {
    char c, *buf, *fname;
    int fd, idx, stop, fname_size, buf_size;

    /* Calcola la dimensione del nome del file. Successivamente alloca un
     * buffer della dimensione necessaria e scrivi al suo interno il nome
     * del file. */
    fname_size = snprintf(NULL, 0, "output_%d.txt", pid) + 1;
    fname = malloc(sizeof(char) * (fname_size+1));
    snprintf(fname, fname_size, "output_%d.txt", pid);

    /* Apri il file. Ritorna NULL in caso di errore (questa situazione non
     * dovrebbe mai accadere). */
    fd = open(fname, O_RDONLY);
    if (fd < 0) {
        free(fname);
        return NULL;
    }
    
    /* Leggi dal file un byte alla volta. Quando il buffer in cui inserire
     * il contenuto del file è pieno, alloca ulteriori BLOCK_SIZE byte. Il
     * ciclo termina quando la funzione 'read' ritorna 0, ovvero il file è
     * stato completamente letto. */
    buf = NULL;
    buf_size = idx = stop = 0;
    while (!stop) {
        stop = read(fd, &c, sizeof(char)) == 0;
        if (stop) {
            c = '\0';
        }
        if (idx >= buf_size) {
            buf_size += BLOCK_SIZE;
            buf = realloc(buf, buf_size * sizeof(char));
        }
        buf[idx++] = c;
    }

    /* Chiudi il file e rimuovilo dal filesystem. */
    close(fd);
    unlink(fname);
    free(fname);
    
    return buf;
}

/* Attendi la terminazione di un processo. Stampa l'hash che il processo
 * terminato doveva crackare e, se terminato con successo, stampa a video
 * il testo in chiaro. Ritorna 1 se il cracker ha terminato con successo,
 * 0 se il cracker ha fallito o non ci sono altri processi da attendere. */
int wait_child(pid_t crackers[], char *hashes[]) {
    pid_t pid;
    char *plain;
    int i, status;

    /* Attendi la terminazione di un processo. Ritorna 0 se il processo
     * non è terminato normalmente, ad esempio a causa di un segnale. */
    pid = wait(&status);
    if (pid < 0 || !WIFEXITED(status)) {
        return 0;
    }

    /* Cerca l'hash che doveva essere crackato dal processo che è appena
     * terminato. Sebbene non ci sia alcun controllo per evitare di uscire
     * dal vettore, siamo sicuri che non si verificherà alcun overflow! */ 
    for (i = 0; crackers[i] != pid; i++) {}
    /* Leggi da file e stampa il testo corrispondente all'hash se il processo
     * è terminato con successo, altrimenti stampa un messaggio d'errore. */
    if (WEXITSTATUS(status) == 0) {
        plain = read_file(pid);
        printf("[OK] %s -> %s\n", hashes[i+2], plain);
        free(plain);
    } else {
        printf("[FAIL] %s -> boh :(\n", hashes[i+2]);
    }

    return 1 - WEXITSTATUS(status);
}

int main(int argc, char *argv[]) {
    int i, success, total, running, max_procs;
    pid_t *crackers, pid;

    if (argc < 3) {
        fprintf(stderr, "Usage: ./multicore MAX_PROCS HASH [HASH...]\n");
        exit(EXIT_FAILURE);
    }

    /* Leggi il numero di processi passato da riga di comando. Termina il
     * programma se il valore fornito è invalido. */
    max_procs = atoi(argv[1]);
    if (max_procs <= 0) {
        fprintf(stderr, "Invalid number of processes.\n");
        exit(EXIT_FAILURE);
    }
    /* Questo vettore serve per tenere traccia di quale processo sta
     * crackando un certo hash. In particolare, la posizione 'i' contiene
     * il PID del processo che sta crackando l'hash 'argv[i+2]'. */
    crackers = malloc(sizeof(pid_t) * (argc-2));
    
    running = success = 0;
    for (int i = 2; i < argc; i++) {
        /* Attendi la terminazione di un cracker se il numero di processi
         * in esecuzione è pari al massimo specificato dall'utente. */
        if (running >= max_procs) {
            success += wait_child(crackers, argv);
            running--;
        }
        /* Crea un processo cracker per ogni hash passato da riga di comando.
         * Qualora una fork dovesse fallire, l'istruzione 'kill' invia un segnale
         * SIGKILL a tutti i processi figli ed al processo stesso. */
        pid = fork();
        if (pid < 0) {
            perror("Fork fallita.");
            kill(0, SIGKILL);
        } else if (pid == 0) {
            execl("./cracker", "cracker", argv[i], NULL);
            perror("Exec fallita.");
            exit(EXIT_FAILURE);
        }
        crackers[i-2] = pid;
        running++;
    }

    /* Attendi la terminazione dei rimanenti cracker. */ 
    while (running > 0) {
        success += wait_child(crackers, argv);
        running--;
    }
    printf("Crackati %d hash su %d!\n", success, argc-2);
    free(crackers);

    return EXIT_SUCCESS;
}

Aumentare il numero di processi non è necessariamente vantaggioso: se il numero di processi creati è superiore al numero di core della CPU presente nel sistema, i vari cracker dovranno condividere l’utilizzo della CPU e quindi non ottenete alcun miglioramento nei tempi di esecuzione rispetto all’uso di un numero minore di processi. Potete osservare questo comportamento nel grafico sottostante dove viene mostrato il tempo necessario per crackare i 64 hash presenti nel file hashes_medium.txt su un laptop con una CPU quad-core in relazione al numero di processi cracker utilizzati.