Esercitazione sulla gestione dei processi

L’esercitazione di oggi consiste nell’implementazione di un programma C che parallelizza su più core il cracking di una lista di hash SHA-256. L’archivio (link) contiene tre versioni di un cracker che implementa un attacco bruteforce basato su dizionario:

  • cracker-x86: Linux, 32 bit;
  • cracker-x86-64: Linux, 64 bit;
  • cracker-macOS: macOS.

Il cracker riceve l’hash da crackare come argomento da riga di comando. Se l’operazione ha avuto successo, il cracker termina con exit(0) e inserisce la parola corrispondente all’hash nel file output_PID.txt dove PID è il process identifier del cracker. Se l’operazione fallisce, il cracker termina con exit(1).

Ecco un esempio di crack riuscito:

Gli hash da crackare sono specificati come parametri da riga di comando. Il vostro programma dovrà eseguire un’istanza del cracker per ciascun hash, attendere la terminazione dei vari processi e stampare, per ciascun hash, la corrispondente stringa se il cracking ha avuto successo. Infine dovrete stampare il numero di hash crackati rispetto al numero totale.

Per le operazioni sui file utilizzate le funzioni open, read e close al posto di quelle presenti nella libreria standard di C che già conoscete (fopen, fread, fscanf, etc). In questo modo potete iniziare a familiarizzare con il loro utilizzo, dal momento che le useremo anche per le operazioni sulle pipe che vedremo nelle prossime lezioni. Eccone una breve descrizione (semplificata):

  • int open(const char *path, int oflag): apre il file indicato dalla stringa path. Il valore del parametro oflag permette di controllare vari aspetti dell’apertura del file, inclusa la scelta di aprirlo in lettura e/o scrittura: per aprire il file in lettura usate la costante O_RDONLY come primo parametro. In caso di successo la funzione restituisce un numero non negativo che identifica il file aperto, noto come file descriptor. Se l’operazione di apertura fallisce, la funzione restituisce -1.
  • ssize_t read(int fildes, void *buf, size_t nbyte): legge al più nbyte bytes dal file identificato dal descrittore fildes e li inserisce nel buffer buf. Il valore di ritorno è il numero di byte letti da file; quando non sono presenti ulteriori byte da leggere, la funzione restituisce 0.
  • int close(int fildes): chiude il file identificato dal descrittore fildes.

Per ulteriori informazioni su una funzione fate riferimento alla corrispondente pagina del manuale che potete visualizzare con il comando man 2 NOME_FUNZIONE.

Ecco un esempio di esecuzione del cracker multicore e dell’output che deve produrre:

Verificate che il vostro programma sia effettivamente più veloce rispetto ad un’implementazione che non distribuisce il cracking degli hash su più core. Per misurare il tempo di esecuzione di un programma potete usare il comando time come segue:

Estensione: Limitare il numero di processi

La soluzione che avete implementato soffre di un piccolo problema: quando il numero di hash da crackare è elevato, il vostro programma “inonda” il sistema di processi cracker, rendendolo potenzialmente inutilizzabile. Implementate una variante del programma che lancia in contemporanea al massimo N cracker, dove N è un parametro fornito dall’utente (ad esempio come primo parametro da riga di comando). Se il numero di hash da crackare è superiore a N, la vostra implementazione dovrà attendere la terminazione di uno dei cracker in esecuzione prima di lanciare un nuovo processo.

Verificate come varia il tempo di esecuzione in base al numero di cracker in esecuzione. È sempre vantaggioso aumentare il numero di cracker? Perché?

Soluzione

La soluzione sarà disponibile dopo la lezione qui.

4 thoughts on “Esercitazione sulla gestione dei processi”

  1. Buongiorno professore,

    con questo semplice programma volevo capire se era possibile scambiare messaggi tra processi forkati tramite un int pointer (questo per risolvere il problema di conteggiare i processi forkati con esito positivo facendo che ogni child incrementi un contatore ed evitare così l’apertura dei file di output):

    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/wait.h>
    
    int main(){
    	int pid;
    	int * ptr; 
    
    	ptr = (int*) malloc(sizeof(int));
    	*ptr = 0;
    	printf("ptr: %d @ %p\n", *ptr, ptr);
    	
    	if ((pid = fork()) < 0){
    		perror("fork error");
    		exit(SYSTEM_FAILURE);
    	}
    	if (pid == 0){
    		*ptr = *ptr + 1;
    		printf("ptr: %d @ %p incremented by child  pid %d ppid %d\n", *ptr, ptr, getpid(), getppid());
    	}
    	
    	if (pid > 0){
    		sleep(1);
    		printf("ptr: %d @ %p\n", *ptr, ptr);
    		*ptr = *ptr + 1;
    		printf("ptr: %d @ %p incremented by parent pid %d ppid %d\n", *ptr, ptr, getpid(), getppid());
    	}
    	printf("\nlast instruction - ptr: %d @ %p pid %d ppid %d\n", *ptr, ptr, getpid(), getppid());
    }
    
    

    Il valore finale di *ptr non è 2, come mi aspettavo, ma 1. Ho provato anche a mettere una sleep(1) nel processo parent per dargli tempo a leggere il valore aggiornato dal child ma niente. Non capisco in fondo perché succede questo, è possibile che la condivisione della memoria virtuale tra processi e il meccanismo di read-on-copy centri qualcosa?

    Grazie in anticipo

    1. Il motivo è proprio legato al fatto che la fork esegue una copia della memoria: dopo la fork ogni processo avrà una copia distinta del puntatore (e di conseguenza del valore puntato). Non c’è condivisione di memoria tra processi dopo una fork.

      In particolare vedi la spiegazione in qui.

  2. Ci sarà una copia distinta del puntatore e del valore puntato, ma cosa succede con gli indirizzi di memoria contenuti nei puntatori? Dalla printf di sopra mi risulta che i valori degli indirizzi siano uguali. Ma allora dovrei poter accedere allo stesso indirizzo di memoria da 2 puntatori diversi, ma questo non è possibile. Non capisco che parte di questo ragionamento non sia corretta.
    Grazie mille

  3. La memoria è virtualizzata. I puntatori che vedi sono puntatori in memoria virtuale e quindi sono mappati in memoria fisica in due indirizzi differenti (a parte quando si usa la copy-on-write che permette di puntare, temporaneamente, allo stesso indirizzo fisico).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.