La Gestione dei Processi in Linux

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:

  • le istruzioni del programma stesso o codice eseguibile ( text nel gergo di UNIX )
  • la memoria destinata a contenere le variabili globali e tutto ciò che il programma alloca dinamicamente
  • una tabella che tiene traccia dei file aperti
  • una tabella per gestire i segnali del processo
  • lo stack per le variabili locali e le informazioni necessarie alla chiamata ed al ritorno delle funzioni
  • lo stato della CPU ( valore dei registri ed altro )

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:

  • il text non ha bisogno di essere duplicato, perché i due processi lo usano solo in lettura
  • il PID del processo figlio è diverso da quello del padre

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:

  • la funzione main() finisce con un valore di ritorno
  • il programma chiama la funzione exit() passandole un parametro intero
  • il processo riceve il segnale che ne forza la terminazione

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.

Pubblicato
Categorie: Linux Taggato

Di Giampaolo Rossi

Sviluppatore software da oltre 16 anni.