Διεργασία (υπολογιστές)

Διεργασία (process) είναι ένας όρος της πληροφορικής ο οποίος περιγράφει το στιγμιότυπο ενός προγράμματος που εκτελείται σε έναν υπολογιστή. Σε αντιδιαστολή με την έννοια του προγράμματος, το οποίο είναι ένα στατικό σύνολο εντολών, μια διεργασία συνιστά την εκτέλεση αυτών των εντολών. Επομένως ένα πρόγραμμα γενικώς συσχετίζεται με περισσότερες από μία διεργασίες, μία για κάθε φορά που εκτελείται. Μια διεργασία αποτελείται από το ίδιο το πρόγραμμα και από κάποιες τιμές που περιέχονται στη μνήμη και στους καταχωρητές του επεξεργαστή, δηλαδή την κατάσταση του συστήματος, κάθε στιγμή που το πρόγραμμα εκτελείται. Τα σύγχρονα λειτουργικά συστήματα επιτρέπουν την ταυτόχρονη συνύπαρξη πολλαπλών διεργασιών στη μνήμη του υπολογιστή καθώς υποστηρίζουν πολυδιεργασία, μία μέθοδο υλοποίησης ταυτοχρονισμού με την οποία, είτε με κατάλληλη κατανομή του χρόνου του μοναδικού επεξεργαστή (ψευδοπαράλληλη εκτέλεση) είτε λόγω της ύπαρξης περισσοτέρων του ενός επεξεργαστών (παράλληλη εκτέλεση), είναι εφικτή η ταυτόχρονη εκτέλεση πολλαπλών διεργασιών.

Unix

Στα συστήματα UNIX μία διεργασία μπορεί να δημιουργήσει μία θυγατρική της μέσω της κλήσης συστήματος fork(), η οποία κατασκευάζει ένα πλήρες αντίγραφο της καλούσας διεργασίας (συμπεριλαμβανομένου του κώδικα, των τρεχόντων δεδομένων της στη μνήμη και των πινάκων ανοιχτών αρχείων)· στη συνέχεια ο πυρήνας χρονοπρογραμματίζει τις δύο διεργασίες ξεχωριστά, αυτές ανταγωνίζονται μεταξύ τους και με όλες τις άλλες διεργασίες για την πρόσβαση στους πόρους του συστήματος και η εκτέλεσή τους συνεχίζει από την εντολή που ακολουθεί τη fork(). Ο κώδικάς τους μπορεί να διαφοροποιηθεί ρητά από τον προγραμματιστή μέσω κατάλληλου ελέγχου της τιμής επιστροφής της fork() αμέσως μετά την κλήση της: στη θυγατρική διεργασία η τιμή αυτή είναι 0 ενώ στη μητρική ισούται με το ακέραιο αναγνωριστικό της νέας διεργασίας, το οποίο της αποδίδεται αυτομάτως από τον πυρήνα. Αυτό το αναγνωριστικό η μητρική έχει την επιλογή να το αποθηκεύσει σε κάποια μεταβλητή, ώστε αργότερα να μπορεί να επικοινωνήσει ή να συγχρονιστεί με την μόλις κατασκευασθείσα θυγατρική της (π.χ. ώστε να είναι ικανή, μέσω της κλήσης συστήματος waitpid(pid, status), να ανασταλλεί αναμένοντας τον τερματισμό της διεργασίας με αναγνωριστικό pid, αν απαιτεί κάτι τέτοιο η λογική του προγράμματος).
Μία διεργασία μπορεί ακόμα να καλέσει την wait(status) σε κάποιο σημείο του κώδικά της, ώστε να ανασταλεί αναμένοντας τον τερματισμό οποιασδήποτε από τις θυγατρικές της διεργασίες (ξανά, αν απαιτεί έναν τέτοιο συγχρονισμό η λογική του προγράμματος)· η waitpid(pid) με όρισμα -1 αντί για το αναγνωριστικό κάποιας συγκεκριμένης διεργασίας λειτουργεί όπως η wait(). Κάθε θυγατρική διεργασία με τον τερματισμό της επιστρέφει μία ακέραια τιμή στον πυρήνα (π.χ. για έλεγχο σφαλμάτων) που παραμένει στη μνήμη μέχρι να διαβαστεί από τη μητρική μέσω της wait() ή της waitpid(), οι οποίες μετά το πέρας της αναστολής της καλούσας διεργασίας (που ίσως και να μην ανασταλεί, σε περίπτωση που η/οι θυγατρική/θυγατρικές έχει/έχουν ήδη τερματίσει πριν από την κλήση των waitpid()/wait()) αποθηκεύουν την εν λόγω τιμή στο όρισμά τους status. Οι διεργασίες των οποίων οι μητρικές τερματίστηκαν χωρίς να καλέσουν τη wait() ή την waitpid() ονομάζονται ζόμπι, καθώς συνεχίζουν να δεσμεύουν κάποιο χώρο στη μνήμη ακόμα και αν τερματίσουν την εκτέλεσή τους· το σύστημα ελέγχει περιοδικά για την ύπαρξη ζόμπι και απελευθερώνει τη μνήμη που δεσμεύουν. Η μητρική διεργασία έχει κι αυτή μία τιμή επιστροφής κατά τον τερματισμό της, αφού δεν αποτελεί παρά θυγατρική διεργασία του κελύφους διασύνδεσης με τον χρήστη.
Αντί ο προγραμματιστής να διαφοροποιήσει ρητά τον κώδικά του ανάλογα με την τιμή επιστροφής της fork(), μπορεί μέσω της κλήσης συστήματος execv(file) (ή διάφορες παραλλαγές της από σύστημα σε σύστημα, όπως οι execvp(), execl() κλπ) να αντικαταστήσει τον κώδικα και τα δεδομένα της θυγατρικής διεργασίας με αυτά που περιέχονται στο εκτελέσιμο αρχείο file (είναι βέβαια ξανά αναγκαίος ο έλεγχος για την τιμή επιστροφής της fork(), αφού η execv() πρέπει να αποτελεί την πρώτη εντολή που θα εκτελέσει η θυγατρική αλλά δεν πρέπει να εκτελεστεί από τη μητρική, αφού τότε θα αντικαταστήσει και τις δικές της εντολές και δεδομένα). Η εκτέλεση μίας διεργασίας μετά από μία κλήση execv(file) συνεχίζει από το σημείο εισόδου του περιεχόμενου στο εκτελέσιμο file κώδικα, το οποίο συνήθως είναι μία κεντρική διαδικασία όπου ο προγραμματιστής έχει οργανώσει σε υψηλό επίπεδο τη λειτουργικότητα και τη λογική του προγράμματος (π.χ. στη γλώσσα προγραμματισμού C τον ρόλο αυτό επιτελεί η συνάρτηση main())· οι άλλες διαδικασίες καλούνται ως υποπρογράμματα μέσα από την κεντρική.
Μία διεργασία μπορεί είτε να τερματιστεί αυτομάτως όταν η εκτέλεσή της φτάσει σε ένα σημείο εξόδου του κώδικά της (π.χ. αμέσως μετά το πέρας της τελευταίας εντολής που περιέχεται στη συνάρτηση main()), είτε να τερματιστεί με ρητή χρήση από τον προγραμματιστή της κλήσης συστήματος exit(returnvalue), η οποία γράφει τις όποιες εκκρεμείς προσωρινές ενδιάμεσες μνήμες Εισόδου / Εξόδου, απελευθερώνει ανοιχτούς πόρους (π.χ. αρχεία τα οποία έχει ανοίξει νωρίτερα η διεργασία), καλεί όποιους χειριστές εξόδου (διαδικασίες οι οποίες εκτελούνται αυτομάτως κατά τον τερματισμό του προγράμματος) έχει πιθανώς δηλώσει ο προγραμματιστής με τη συνάρτηση atexit() (παρεχόμενη από την πρότυπη βιβλιοθήκη της C) και, τελικώς, ενημερώνει κατάλληλα τις δομές δεδομένων του πυρήνα και τον χρονοπρογραμματιστή, αποθηκεύοντας παράλληλα την τιμή returnvalue (κωδικός εξόδου) ως τιμή επιστροφής της διεργασίας η οποία μόλις τερμάτισε. Με αυτόν τον τρόπο η μητρική διεργασία μπορεί να ειδοποιηθεί για τον τρόπο τερματισμού της θυγατρικής της ελέγχοντας τον κωδικό εξόδου· η πρότυπη βιβλιοθήκη της C παρέχει ένα σύνολο μακροεντολών με τις οποίες γίνεται αυτοματοποιημένος έλεγχος για τον τρόπο τερματισμού (π.χ. η μακροεντολή WIFEXITED(status) λαμβάνει θετική τιμή αν η διεργασία η οποία επέστρεψε τον κωδικό εξόδου status τερμάτισε φυσιολογικά).
Τα περισσότερα λειτουργικά συστήματα ελαφρύνουν τη χρονική επιβάρυνση που συνεπάγετα μία κλήση fork() αξιοποιώντας ποικίλες βελτιστοποιήσεις: π.χ. το τμήμα κώδικα δεν αντιγράφεται αφού ο κώδικας ενός προγράμματος στη μνήμη συνήθως είναι μη τροποποιήσιμος (μόνο για ανάγνωση), επομένως η νέα διεργασία που κατασκευάζεται μπορεί απλώς να απεικονίζει στον ιδιωτικό εικονικό της χώρο διευθύνσεων την περιοχή φυσικής μνήμης όπου είναι αποθηκευμένο το τμήμα κώδικα της γονικής της διεργασίας. Τα άλλα δεδομένα (π.χ. στοίβα, σωρός κλπ) μπορούν επίσης να μην αντιγράφονται αμέσως αλλά μόνο όταν, αργότερα, η μία από τις δύο διεργασίες τροποποιήσει το δικό της αντίγραφο κατά την εκτέλεσή της.
Ακολουθεί ένα παράδειγμα κώδικα χειρισμού διεργασιών στο Unix, σε γλώσσα προγραμματισμού C:
#include <stdio.h>   /* printf, stderr, fprintf */#include <unistd.h>  /* _exit, fork */#include <stdlib.h>  /* exit */#include <errno.h>   /* errno */int main(void){
   pid_t  pid;

   pid = fork();
   if (pid == 0)
   {
      /* Θυγατρική διεργασία:       * Όταν η fork() επιστρέφει 0, τότε ο εκτελούμενος κώδικας είναι της θυγατρικής διεργασίας       *       * Μέτρηση ως το δέκα, ανά δευτερόλεπτο       */
      int j;
      for (j = 0; j < 10; j++)
      {
         printf("child: %d\n", j);
         sleep(1);
      }
      _exit(0);  /* Δεν χρησιμοποιείται η exit() αλλά η _exit(0) η οποία δεν καλεί χειριστές εξόδου */
   }
   else if (pid > 0)
   { 
      /* Μητρική διεργασία:       * Όταν η fork() επιστρέφει θετικό ακέραιο, τότε ο εκτελούμενος κώδικας είναι της μητρικής διεργασίας και ο ακέραιος το αναγνωριστικό της μόλις κατασκευασθείσας διεργασίας       */
      int i;
      for (i = 0; i < 10; i++)
      {
         printf("parent: %d\n", i);
         sleep(1);
      }
      exit(0);
   }
   else
   {   
      /* Σφάλμα       * Όταν μία κλήση συστήματος επιστρέφει αρνητικό αριθμό, κάποιο σφάλμα έχει συμβεί       * (π.χ. το σύστημα έχει φτάσει στο επιτρεπτό πλήθος διεργασιών).       */
      fprintf(stderr, "can't fork, error %d\n", errno);
      exit(1);
   }}
Ακολουθεί ένα παράδειγμα χρήσης της execvp σε C:
int main(int argc, char *argv[]){
   int i;
   printf(“exec %s via fork”,argv[1]);
   printf(“with parameters:\n”);
   for (i=1; i<argc; i++)
   {
      printf(“argv[%d]=%s\n”,i-1,argv[i]);
   }
   if (!fork())
   {
      execvp(argv[1],&argv[1]);
      /* Ο κώδικας της εκτελούμενης (θυγατρικής διεργασίας) μόλις        * αντικαταστάθηκε από το εκτελέσιμο αρχείο που μεταβιβάστηκε στη μητρική       * διεργασία ως όρισμα γραμμής εντολών, επομένως οι επόμενες εντολές δεν        * μπορούν να εκτελεστούν ποτέ εκτός και αν έχει αποτύχει η κλήση της        * execv */
      perror(“execvp”);
      return;
   }}