Διαδιεργασιακή επικοινωνία

Διαδιεργασιακή επικοινωνία (αγγλ.: InterProcess Communication, IPC) ονομάζεται στην πληροφορική ένα σύνολο μηχανισμών που παρέχουν τα λειτουργικά συστήματα των ηλεκτρονικών υπολογιστών, οι οποίοι διευκολύνουν την ανταλλαγή δεδομένων και τον συγχρονισμό μεταξύ ταυτοχρόνως εκτελούμενων διεργασιών μέσω δομών δεδομένων του πυρήνα. Τέτοιοι μηχανισμοί είναι απαραίτητοι στα μοντέρνα λειτουργικά συστήματα όπου, χάρη στον μηχανισμό της εικονικής μνήμης, κάθε διεργασία έχει τον δικό της ιδιωτικό χώρο εικονικών διευθύνσεων στον οποίον έχει πρόσβαση μόνο αυτή και ο πυρήνας. Προκειμένου να υπάρχει μία στοιχειώδης προστασία μνήμης μεταξύ διαφορετικών διεργασιών, καμία διεργασία δεν έχει δικαίωμα ανάγνωσης ή εγγραφής στον χώρο διευθύνσεων των υπολοίπων. Αν λοιπόν χρειάζεται δύο διαφορετικές διεργασίες να επικοινωνήσουν μεταξύ τους ή να ανταλλάξουν δεδομένα, αυτό μπορεί να γίνει μόνο μέσω του συστήματος αρχείων (π.χ. μία διεργασία να γράψει ένα αρχείο και μία άλλη να το διαβάσει) ή μέσω μίας μεθόδου διαδιεργασιακής επικοινωνίας. Με το προγραμματιστικό μοντέλο των υποδοχών (sockets) οι διεργασίες οι οποίες επικοινωνούν μπορούν να εκτελούνται σε διαφορετικούς υπολογιστές που διασυνδέονται μέσω ενός δικτύου.

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

POSIX Επεξεργασία

Η διαδιεργασιακή επικοινωνία στα λειτουργικά συστήματα τύπου UNIX καθορίζεται από το πρότυπο POSIX.

Σήματα Επεξεργασία

Κύριο λήμμα: Σήμα (υπολογιστές)

Μία περιορισμένη μορφή διαδιεργασιακής επικοινωνίας είναι τα σήματα (signals), διακοπές λογισμικού που δρουν ως σινιάλα και μπορούν να αποστέλλονται σε μία διεργασία από κάποια άλλη ή από τον πυρήνα, αναγκάζοντάς την να τα χειριστεί ασύγχρονα μόλις τα λάβει και ακολούθως να επιστρέφει στην κανονική ροή εκτέλεσης. Κάθε σήμα διακρίνεται από έναν ακέραιο με τον οποίο είναι συσχετισμένο κάποιο συμβολικό όνομα (SIGxxxx). Το μοντέλο είναι το εξής: ένα πρόγραμμα στον κώδικά του δηλώνει ότι μία συνάρτηση είναι χειριστής ενός συγκεκριμένου σήματος (εγκατάσταση χειριστή). Όταν ληφθεί το σήμα αυτό κατά την εκτέλεση της αντίστοιχης διεργασίας, αυτομάτως η τελευταία διακόπτει ό,τι έκανε (εντολή Α) και εκτελεί τον χειριστή. Μόλις ο χειριστής επιστρέψει ο έλεγχος δίνεται ξανά στην εντολή Α. Ένας χειριστής εγκαθίσταται με την κλήση συστήματος sigaction() (αν δίνεται η ειδική τιμή SIG_IGN ως όρισμα στη sigaction() αντί για όνομα συνάρτησης, τότε το αντίστοιχο σήμα αγνοείται από τη διεργασία), ενώ με τις κλήσεις kill() ή sigqueue() το τρέχον πρόγραμμα αποστέλλει ένα συγκεκριμένο σήμα σε μία συγκεκριμένη διεργασία (η διαφορά τους είναι ότι η sigqueue(), μαζί με το σήμα, αποστέλλει και έναν επιπλέον ακέραιο με δεδομένα του προγραμματιστή). Σχεδόν για κάθε σήμα υπάρχει κάποιος προεπιλεγμένος χειριστής ο οποίος παρέχεται από το πρότυπο POSIX και δεν χρειάζεται να γραφεί ή να εγκατασταθεί χειροκίνητα (συνήθως επιφέρει τον τερματισμό της διεργασίας ή αγνοεί τελείως το σήμα)· για κάποια συγκεκριμένα σήματα μάλιστα δεν μπορεί να υποσκελιστεί από χειριστή του προγραμματιστή.

Σωληνώσεις Επεξεργασία

Η παλαιότερη μέθοδος διαδιεργασιακής επικοινωνίας στο Unix είναι οι σωληνώσεις (pipes). Πρόκειται για δομές δεδομένων του πυρήνα που επιτρέπουν σε δύο συγγενείς διεργασίες να ανταλλάσσουν αμφίδρομα δεδομένα. Εμφανίζονται στον χώρο του χρήστη ως ζεύγη περιγραφέων αρχείων, χωρίς να αντιστοιχούν σε πραγματικά αρχεία, όπου ο ένας περιγραφέας είναι άκρο εγγραφής και ο άλλος άκρο ανάγνωσης. Οι συνήθεις κλήσεις συστήματος για Είσοδο / Έξοδο σε αρχεία (read(), write(), close()) βρίσκουν εφαρμογή και εδώ. Σε κάθε σωλήνωση μπορούν να αντιστοιχούν πολλαπλά ζεύγη περιγραφέων σε διαφορετικές διεργασίες, υλοποιώντας έτσι τη διαδιεργασιακή επικοινωνία. Αυτή η πολλαπλότητα γίνεται εφικτή μέσω της κατασκευής νέων διεργασιών (μοντέλο fork(), όπου η θυγατρική κληρονομεί όλους τους ανοικτούς περιγραφείς της γονικής διεργασίας). Είναι ευθύνη του προγραμματιστή να κλείσει τα άκρα ανάγνωσης ή εγγραφής στις αντίστοιχες διεργασίες ώστε να επιτύχει την επιθυμητή συμπεριφορά (π.χ. μία διεργασία να γράφει και η γονική της να διαβάζει τη σωλήνωση). Το μειονέκτημα είναι ότι με σωληνώσεις μόνο συγγενείς διεργασίες μπορούν να επικοινωνήσουν (γονική με θυγατρική ή αδελφές διεργασίες μεταξύ τους). Μία σωλήνωση δημιουργείται με την κλήση συστήματος pipe(). Η ανάγνωση από σωλήνωση με όλα τα άκρα εγγραφής κλειστά, είτε διαβάζει όλα τα δεδομένα που τοποθετήθηκαν στη σωλήνωση προτού κλείσει κάποιο άκρο εγγραφής και δεν αναγνώστηκαν ακόμη, είτε (αν δεν ισχύει αυτή η περίπτωση) η read() επιστρέφει αμέσως 0. Η εγγραφή σε σωληνώσεις με όλα τα άκρα ανάγνωσης κλειστά οδηγεί στην αποστολή του σήματος SIGPIPE από τον πυρήνα στην καλούσα διεργασία και στην αποτυχία της κλήσης write(). Σε σχέση με τις σωληνώσεις ενδιαφέρον παρουσιάζει η κλήση συστήματος dup(), η οποία δέχεται ως όρισμα έναν περιγραφέα αρχείου και τον κλωνοποιεί αναθέτοντας στο αντίγραφο τον πρώτο ελεύθερο αναγνωριστικό αριθμό (έτσι π.χ. μπορεί να αντικατασταθεί με κάποια σωλήνωση η τυπική είσοδος ή η τυπική έξοδος μίας διεργασίας).

Ακολουθεί ένα παράδειγμα χειρισμού σωληνώσεων σε γλώσσα προγραμματισμού C:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#define MAXLINE 512
int main(void)
{
	int n, fd[2];
	pid_t pid;
	char line[MAXLINE];
	if (pipe(fd) < 0)
		{perror("creating pipe"); exit(1); } /* Αποτυχία κατασκευής της σωλήνωσης */
	if ((pid = fork()) < 0)
		{perror("cannot fork"); exit(1); } /* Αποτυχία κατασκευής της θυγατρικής διεργασίας */
	else if (pid > 0) /* γονική διεργασία */
	{
		close(fd[0]); /* κλείσιμο του άκρου ανάγνωσης */
		write(fd[1], "message through pipe\n", 21);  /* κλήση εγγραφής σε αρχείο - εδώ χρησιμοποιείται για εγγραφή στο ανοιχτό άκρο της σωλήνωσης */
		close(fd[1]);
	}
	else
	{ /* θυγατρική διεργασία */
		close(fd[1]); /* κλείσιμο του άκρου εγγραφής */
		n = read(fd[0], line, MAXLINE); /* κλήση ανάγνωσης από αρχείο - εδώ χρησιμοποιείται για ανάγνωση από το ανοιχτό άκρο της σωλήνωσης */
		write(STDOUT_FILENO, line, n); close(fd[0]);
	}
	exit(0);
}

Εναλλακτικά μπορούν να χρησιμοποιηθούν κατονομασμένες σωληνώσεις (named pipes ή FIFOs). Αυτές κατά τη δημιουργία τους αντιστοιχίζονται σε οντότητες του συστήματος αρχείων, εικονικά αρχεία, ώστε να μπορούν να προσπελαστούν από μη συγγενείς διεργασίες που γνωρίζουν το μονοπάτι του εικονικού αρχείου. Ο χειρισμός τους γίνεται όπως των κοινών αρχείων, οπότε κάθε διεργασία για κάθε FIFO διατηρεί μόνο έναν περιγραφέα αρχείου ο οποίος επιστρέφεται από μία κοινή κλήση open(). Το μειονέκτημα είναι ότι ως αποτέλεσμα μία κατονομασμένη σωλήνωση λειτουργεί μονόδρομα, με κάθε διεργασία να μπορεί μόνο να γράφει σε αυτήν ή μόνο να διαβάζει από αυτήν, όπως συμβαίνει πάντα στο POSIX με τα αρχεία. Ένα FIFO δημιουργείται με την κλήση συστήματος mkfifo() και η διεργασία που το δημιούργησε πρέπει, αφού το κλείσει με την close(), να το διαγράψει από το σύστημα αρχείων με την κλήση συστήματος unlink(). Συνήθως πριν από την κλήση της close() η διεργασία αυτή διαβάζει δεδομένα από το FIFO, μέσω της κλήσης read(), τα οποία έχει γράψει εκεί κάποια άλλη διεργασία που γνώριζε το μονοπάτι του FIFO στο σύστημα αρχείων και το έχει ανοίξει για εγγραφή. Φυσιολογικά, κάθε κλήση open() για ανάγνωση μίας κατονομασμένης σωλήνωσης μπλοκάρει την καλούσα διεργασία μέχρι η σωλήνωση να ανοιχτεί από κάποια άλλη διεργασία για εγγραφή (και αντίστροφα). Αυτή η συμπεριφορά μπορεί να τροποποιηθεί με κατάλληλα ορίσματα (μη ανασταλτική open()) και σε αυτήν την περίπτωση το άνοιγμα για εγγραφή σε FIFO που δεν έχει ανοιχτεί για ανάγνωση αποτυγχάνει, ενώ το άνοιγμα για ανάγνωση σε FIFO που δεν έχει ανοιχτεί για εγγραφή επιστρέφει αμέσως. Οι αναγνώσεις από το FIFO διατηρούν το μέγεθος των αιτήσεων εγγραφής που προηγήθηκαν· αυτό σημαίνει πως αν μία διεργασία Α1 γράψει Ν byte, στη συνέχεια η ίδια γράψει Μ byte και τελικώς μία διεργασία Α2 διαβάσει το FIFO, η πρώτη αίτηση ανάγνωσης θα επιστρέψει τα αρχικά Ν byte και όχι τα ολικά Ν+Μ byte.

Πολλές διεργασίες μπορούν ταυτόχρονα να προσπελαύνουν το ίδιο αρχείο ή FIFO, πιθανώς για να ανταλλάξουν δεδομένα, με αποτέλεσμα να εμφανίζονται συνθήκες συναγωνισμού. Το πρόβλημα επιλύεται με τα κλειδώματα αρχείων, δηλαδή εξειδικευμένα mutex που παρέχει ο πυρήνας για αμοιβαίο αποκλεισμό. Τα κλειδώματα είναι είτε κοινόχρηστα είτε αποκλειστικά (με πολλαπλές διεργασίες να μπορούν να κατέχουν ταυτόχρονα ένα κοινόχρηστο κλείδωμα για το ίδιο αρχείο αλλά όχι ένα αποκλειστικό κλείδωμα, ή παράλληλα ένα κοινόχρηστο κι ένα αποκλειστικό κλείδωμα) και αφορούν ένα αρχείο και όχι έναν περιγραφέα αρχείου: αν ο τελευταίος κλωνοποιηθεί, με μία κλήση dup() ή fork(), δεν υπάρχουν πλέον δύο κλειδώματα αλλά δύο αναφορές στο ίδιο κλείδωμα. Τα κλειδώματα αρχείων είναι προσπελάσιμα μέσω της εξειδικευμένης κλήσης συστήματος flock() ή στο πλαίσιο της fcntl(), μίας γενικής χρήσης κλήσης χειρισμού αρχείων. Όταν μία διεργασία ανοίγει ένα αρχείο για ανάγνωση ζητά ένα κοινόχρηστο κλείδωμα, ενώ όταν το ανοίγει για εγγραφή ζητά ένα αποκλειστικό κλείδωμα. Στην τελευταία περίπτωση, αν το κλείδωμα του αρχείου το κατέχει κάποια άλλη διεργασία τότε η καλούσα μπλοκάρει μέχρι το κλείδωμα να ελευθερωθεί.

Διαδιεργασιακή επικοινωνία του System V Επεξεργασία

Μία άλλη οικογένεια μεθόδων διαδιεργασιακής επικοινωνίας στο Unix είναι οι μέθοδοι IPC του Unix System V, οι οποίες διαφέρουν από τις προηγούμενες (μη αντιστοιχίζοντας κάποια οντότητα σε περιγραφείς αρχείων) αλλά μεταξύ τους έχουν κοινά χαρακτηριστικά.