Fin dalla sua nascita, sul finire degli anni sessanta, UNIX è stato accompagnato dal concetto di processo, che ha subito nel corso degli anni delle evoluzioni, ma sostanzialmente è rimasto simile, così tanto che il kernel del tempo conteneva già tutte le chiamate che vedremo in questo articolo. Queste chiamate possono essere considerate standard per ogni sistema UNIX, Linux naturalmente compreso. Un processo UNIX è un programma in esecuzione che ha i seguenti attributi:
Queste parti sono scomposte per un semplice motivo, più processi che eseguono lo stesso programma non hanno bisogno di duplicare la memoria necessaria, il text ad esempio si può condividere.
Ogni processo è identificato in modo univoco da un PID ( Process ID ), per visualizzarne la lista sul nostro sistema basta eseguire il comando ps oppure dare un’occhiata nella directory /proc. Un processo può ottenere in ogni momento il suo PID chiamando la funzione getpid().
// File proc.c #include <stdio.h> void main() { printf("Il numero del mio processo è %d\n", getpid()); }
Per compilare questo codice utilizzare il comando ( vedere articolo sul compilatore gcc ):
gcc -o proc proc.c
ed eseguirlo con il comando
./proc
Quando si esegue un programma compilato, il sistema operativo carica in memoria il text e le eventuali librerie dinamiche necessarie. Ovviamente se qualche oggetto risulta già in memoria non viene caricato di nuovo, risparmiando così tempo e risorse. Successivamente la CPU inizia l’esecuzione del programma a partire dalla funzione main(). Questa funzione dovrebbe sempre restituire un intero ed avere due argomenti, ecco il prototipo:
int main(int argc, char** argv);
argc indica il numero di argomenti, mentre argv è un array di stringhe che contiene gli argomenti, il primo è sempre il nome dell’eseguibile.
// File proc.c #include <stdio.h> int main(int argc, char** argv) { int i ; printf("Il numero del mio processo è %d\n", getpid()); printf("Mentre il mio nome è %s\n", argv[0]); for (i = 1; i < argc; i++) printf("L'argomento n.%d è %s\n", i, argv[i]); return 0; }
Eseguite il programma inserendo due o tre argomenti e vedrete nell’output i valori immessi.
Alcuni programmi sfruttano questa caratteristica per dare la possibilità di essere richiamati con nomi differenti, ma in realtà sono lo stesso eseguibile con un link simbolico, ad esempio gzip e gunzip. Vediamo come fare per implementare questa caratteristica.
// File somma.c #include <stdio.h> int main(int argc, char** argv) { int a, b; if (argc != 3) return -1; a = atoi(argv[1]); b = atoi(argv[2]); if (strcmp(argv[0], "somma") == 0 || strcmp(argv[0], "./somma") == 0) { printf("La somma di %d e %d è %d\n", a, b, a+b); return a+b; } else if (strcmp(argv[0], "sottrazione") == 0 || strcmp(argv[0], "./sottrazione") == 0) { printf("La differenza di %d e %d è %d\n", a, b, a-b); return a-b; } else if (strcmp(argv[0], "moltiplicazione") == 0 || strcmp(argv[0], "./moltiplicazione") == 0) { printf("Il prodotto di %d e %d è %d\n", a, b, a*b); return a*b; } else if (strcmp(argv[0], "divisione") == 0 || strcmp(argv[0], "./divisione") == 0) { printf("La rapporto di %d e %d è %d\n", a, b, a/b); return a/b; } return 0; }
Dopo aver compilato occorre creare i tre link simbolici:
ln -sf somma sottrazione
ln -sf somma moltiplicazione
ln -sf somma divisione
a questo punto provate a dare il comando:
./moltiplicazione 13 6
ed osservate l’output, il programma è lo stesso, ma richiamato con un nome differente compie azioni diverse.
Per creare un nuovo processo si usa la chiamata di sistema fork(), che genera una copia del processo che lo invoca con alcune importanti considerazioni:
Linux fa uso della memoria copy-on-write e solo le porzioni di memoria modificate dal figlio sono duplicate, tutto il resto punta a strutture del processo padre. La funzione fork() restituisce zero al figlio, mentre il PID del figlio al padre, per questo motivo padre e figlio intraprendono due strade diverse e possono compiere anche istruzioni diverse con l’uso di un semplice if.
// File fork.c #include <stdio.h> int main() { int pid; char c; printf("Prima della fork, hello world!\n"); pid = fork(); if (pid != 0) { printf("Il padre ha ottenuto %d\n", pid); c = 'a'; } else { printf("Il figlio ha ottenuto %d\n", pid); c = 'b'; } srand(getpid() ^ time(NULL)); while (1) { write(1, &c, 1); usleep(rand() % 100000); } return 0; }
In questo semplicissimo esempio il padre stampa “a” ed il figlio stampa “b”, le write sono intervallate da pause per meglio vedere l’alternarsi irregolare delle lettere sullo schermo. Per interrompere l’esecuzione dei processi e quindi del programma ( dato il loop infinito ) basta premere CTRL+C.
La scelta di restituire zero al figlio non è arbitraria, per esso ottenere il PID è facile con getpid() e quello del padre con getppid(), ma per il padre non vi è modo di ottenere quello del figlio se non alla chiamata di fork() appunto. Un processo termina per diverse ragioni:
Ci sarebbe molto da dire ancora sui processi affiancati ormai anche dai thread, una sorta di processi più snelli. Un processo può diventare un contenitore di thread che condividono lo stesso codice eseguibile e la stessa memoria di variabili globali e dati allocati dinamicamente. Ogni thread ha però uno stack a se stante, quindi le variabili locali e le funzioni eseguite sono diverse, un pò come più pezzi dello stesso programma in esecuzione contemporaneamente.
Sono arrivato alla convinzione che un abbonamento per tutti i miei software gestionali sia il…
MerciGest è un software per la gestione del magazzino completamente gratuito. Continua a leggere→
In ufficio può capitare di doversi allontanare dal proprio posto di lavoro, ecco che allora…
In questo articolo vedremo quando è più o meno utile togliere la corrente ad un…
Dopo la pausa invernale dovuta al lavoro che devo fare per sostentarmi, eccomi di nuovo…
Vediamo come eliminare i files direttamente da Windows senza utilizzare il cestino. Continua a leggere→