ΛΕΙΤΟΥΡΓΙΚΑ ΣΥΣΤΗΜΑΤΑ IΙ Λύβας Χρήστος chrislibas@ssl-unipi.gr Αρχική επιµέλεια Πιτροπάκης Νικόλαος και Υφαντόπουλος Νικόλαος
>_ ΔΙΕΡΓΑΣΙΕΣ +- Με τον όρο διεργασία στο UNIX εννοούμε τη δυναμικη πράξη της εκτέλεσης ενός προγράμματος απο κάποιο χρήστη. Στα συστήματα UNIX κάθε διεργασία βλέπει τον δικο της εικονικο χώρο διευθύνσεων, ο οποίος στα μάτια κάθε διεργασίας ξεκινα απο το 0 και περιλαμβάνει διευθύνσεις των 32 bits ή 64 bits (ανάλογα την αρχιτεκτονική). +- Οι διεργασίες είναι ανεξάρτητες +- Το ΛΣ απομονώνει τις διεργασίες +- Οι διεργασίες μπορει να συνεργάζονται +- Μηχανισμοι επικοινωνίας και συγχρονισμου
>_ ΧΩΡΟΣ ΔΙΕΥΘΥΣΕΩΝ +- Το τμήμα κειμένου (text segment) περιέχει τον κώδικα του προγρά μματος που εκτελει εκείνη τη στιγμη η διεργασία και ξεκινα απο τη διε ύθυνση 0 του εικονικου χώρου διευθύνσεων. Τα περιεχόμενα του τμή ματος κειμένου δεν μπορούν να τροποποιηθούν απο τη διεργασία (read-only segment). +- Το τμήμα δεδομένων (data segment) όπου αποθηκεύονται τα στατικα δεδομένα της διεργασίας (παγκόσμιες μεταβλητές, πίνακες, κλπ.). Το αρχικο μέγεθος του τμήματος αυτου καθορίζεται στο χρόνο μετάφρασης και μπορει να μεταβληθει μέσα απο το πρόγραμμα, αν προκύψει ανάγκη κατα τη διάρκεια της εκτέλεσής του +- Το τμήμα στοίβας (stack segment) χρησιμοποιείται για τη στοίβα της διεργασίας. Εκει φυλάγονται διευθύνσεις επιστροφής, τοπικές μεταβλητές, κλπ. Το τμήμα στοίβας ξεκινα απο την ψηλότερη θέση του εικονικου χώρου διευθύνσεων και μετακινείται προς τα κάτω. Το μέ γεθος του αυξάνεται αυτόματα απο το σύστημα.
>_ ΔΙΕΡΓΑΣΙΑ ΓΟΝΕΑΣ ΔΙΕΡΓΑΣΙΑ ΠΑΙΔΙ +- Οι διεργασίες στο UNIX είναι οργανωμένες σε ένα δένδρο διεργασι ών. Όταν μια διεργασία δ1 δημιουργει μια άλλη διεργασία δ2, η δ2 γίνεται διεργασία-παιδι της δ1 στο δένδρο, και η δ1 είναι η διεργασία-γονέας (parent process) της δ2. Στη ρίζα του δέ νδρου αυτου βρίσκεται η διεργασία με όνομα init, η οποία είναι ουσιαστικα η αρχικη διεργασία του ΛΣ.
>_ ΑΡΙΘΜΟΣ ΤΑΥΤΟΤΗΤΑΣ ΔΙΕΡΓΑΣΙΑΣ +- Κάθε διεργασία έχει συσχετισμένο μαζι της έναν εγγυημένα μοναδικο αριθμο ταυτότητας διεργασίας (process-id, pid) που παρέ χεται δυναμικα απο το σύστημα. +- Ο αριθμός αυτός μπορει να περαστει σαν παράμετρος σε ορισμένες πρωτογενείς κλήσεις προκειμένου να αναφερθούμε σε κάποια διεργασ ία μέσα απο κάποια άλλη. +- Μια διεργασία μπορει να μάθει τον αριθμο ταυτότητάς της εκτελώ ντας την κλήση: #include <sys/types.h> #include <unistd.h> pid_t getpid(void);
>_ ΓΕΝΝΗΣΗ ΜΙΑΣ ΔΙΕΡΓΑΣΙΑΣ +- Η γέννηση μιας διεργασίας στο UNIX είναι διαδικασία δυο βημάτων: +- Πρώτα η διεργασία-πατέρας δημιουργει μια νέα διεργασία-παιδι, πιστο αντίγραφο του εαυτου της (κλήση fork()). +- Στη συνέχεια, η διεργασία-παιδι που προέκυψε αντικαθιστα το πρό γραμμα που εκτελει (αρχικα ίδιο με του πατέρα) με το νέο πρόγραμμα ( κλήση exec()). +- Προαιρετικα, ο πατέρας μπορει να περιμένει μέχρι τον τερματισμο κ άποιας διεργασίας-παιδιου του (κλήση wait()). +- Ο οικειοθελής τερματισμός μιας διεργασίας μπορει να γίνει με την κλήση exit()
>_ ΚΛΗΣΗ fork() +- Η κλήση fork() δημιουργει μια νέα διεργασία, πιστο αντίγραφο της διεργασίας-γονέα και έχει τη μορφη : #include <sys/types.h> #include <unistd.h> pid_t fork(void); +- Οι δυο διεργασίες μοιράζονται το ίδιο τμήμα κειμένου, ενω το τμήμα δεδομένων της νέας διεργασίας αποτελει πιστο αντίγραφο του τμή ματος δεδομένων της διεργασίας-γονέα. Επίσης, η νέα διεργασία μοιρά ζεται όλα τα ανοικτα αρχεία και σωληνώσεις του γονέα. +- Η μόνη διαφορα ανάμεσα στις 2 διεργασίες είναι η τιμη pid που επιστρέφει η κλήση fork()
>_ ΠΑΡΑΔΕΙΓΜΑ fork()
>_ ΚΛΗΣΗ exec() +- Αντικατάσταση κώδικα: κλήσεις exec +- Φόρτωση νέου κώδικα και δεδομένων +- Δεν αλλάζει το PID της διεργασίας +- Τα ανοιχτα αρχεία παραμένουν +- Τυπικός κύκλος ζωής διεργασίας
>_ ΠΑΡΑΛΛΑΓΕΣ ΤΗΣ exec() +- Παραλλαγές της exec με λίστα +- int execl(const char *path, const char *arg,...); +- int execlp(const char *file, const char *arg,...); +- int execle(const char *path, const char *arg,..., char * const envp[]); +- int execve(const char *filename, char *const argv[], char *const envp[]);
>_ ΚΛΗΣΗ execl() +- Η κλήση execl() προκαλει την εκτέλεση ενός νέου προγράμματος το οποίο αντικαθιστα τα τμήματα κειμένου, δεδομένων και στοίβας του καλούντος προγράμματος +- Το αρχικο πρόγραμμα χάνεται οριστικα απο τη μνήμη και η ίδια διεργασία συνεχίζεται με την εκτέλεση ενός νέου προγράμματος +- Τα αρχεία που είχε ανοίξει το αρχικο πρόγραμμα παραμένουν ανοικτα και κατα την εκτέλεση του νέου προγράμματος. +- Παράδειγμα:
>_ ΚΛΗΣΗ wait() +- Η κλήση wait() αναστέλλει την εκτέλεση του καλούντος προγράμματος μέ χρις ότου τερματισθει (ομαλα ή ανώμαλα) η εκτέλεση κάποιας απο τις διεργασίες-παιδια του. +- Αν κάποια διεργασία-παιδι έχει ήδη τερματιστει, τότε η κλήση επιστρέφει αμέσως -1. +- Η κλήση wait() επιστρέφει έναν αριθμο στην μεταβλητη status. Σε περί πτωση που η διεργασία- παιδι τερματίστηκε ομαλα με την κλήση exit(), τα 8 σημαντικότερα ψηφία του αριθμου αυτου παρέχουν την κατάσταση τερματισμου (exit status) της διεργασίας-παιδι Παράδειγμα: #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); Αποτέλεσμα: exit status = status >> 8; Αν τερματιστει βίαια η διεργασία τότε θα βγάλει 8 ψηφία με τον κωδικο του σ ήματος: signal_type = status & 0xFF;
>_ ΚΛΗΣΗ exit() +- Κάθε διεργασία κατα τον τερματισμο της επιστρέφει έναν κωδικο εξόδου (exit code) ώστε να μπορει να διαπιστωθει ο λόγος και ο τρόπος τερματισμου της. +- Η κλήση exit() κλείνει αυτόματα όλα τα ανοικτα αρχεία της διεργασίας και απελευθερώνει όλα τα τμήματα μνήμης που είχε δεσμέσει κατα τη διάρκεια της εκτέλεσής της. +- Όταν μια διεργασία τερματιστει, δεν καταστρέφεται ολοκληρωτικα αλλα παραμένει στην τερματισμένη κατάσταση (zombie state). +- Όταν ο πατέρας εκτελέσει την κλήση wait() και συλλέξει τον κωδικο εξόδου, η διεργασία καταστρέφεται ολοκληρωτικα, ελευθερώνοντας την εγγραφη του πίνακα διεργασιών που είχε Παράδειγμα: #include <unistd.h> void exit(int status); /*το status=0, όταν έχουµε κανονικο τερµατισµο της διεργασίας*/
>_ ΕΠΙΚΟΙΝΩΝΊΑ ΔΙΕΡΓΑΣΙΩΝ ΜEΣΩ ΣΩΛΗΝΩΣΕΩΝ (PIPES) (1/2) +- Ο μοναδικός μηχανισμός δια μέσω του οποίου δύο ή περισσότερες διεργασίες μπορούν να επικοινωνήσουν στο UNIX είναι οι σωληνώσεις (pipes). +- Ένα απλο παράδειγμα σωλήνωσης στο φλοιο ανάμεσα σε 2 διεργασί ες είναι: +- ls wc (παίρνει το αποτέλεσμα της ls και το εισάγει στο wc)
>_ ΕΠΙΚΟΙΝΩΝIΑ ΔΙΕΡΓΑΣΙΩΝ ΜEΣΩ ΣΩΛΗΝΩΣΕΩΝ (PIPES) (2/2) +- Για να δημιουργήσουμε μια σωλήνωση εκτελούμε την κλήση: #include <unistd.h> int pipe(int pd[2]); +- Η κλήση pipe() επιστρέφει δυο περιγραφητές αρχείων στα στοιχεία pd[0] και pd[1] του πίνακα pd. Ο περιγραφητής pd[1] χρησιμοποιείται για εγγραφη δεδομένων στο ένα άκρο της σωλήνωσης, ενω ο pd[0] χρησιμοποιεί ται για ανάγνωση δεδομένων απο το άλλο άκρο της. +- Oι διεργασίες-παιδια κληρονομούν όλους τους περιγραφητές αρχείων του πατέρα, μαζι με τις σωληνώσεις. +- Με τη κλήση write(pd[1],...) οι διεργασίες-παιδια μπορούν να γράφουν δεδομένα στη σωλήνωση, σαν να έγραφαν σε κάποιο κοινο αρχείο +- Με τη κλήση read(pd[0],...) μπορούν να διαβάσουν δεδομένα απο το άλλο άκρο της σωλήνωσης
>_ ΠΑΡΑΔΕΙΓΜΑ Να γραφει πρόγραμμα το οποίο να δημιουργει μια σωλήνωση pd και στη συνέχεια N διεργασίες-παιδια. Οι διεργασίες-παιδια να στέλνουν απο ένα μήνυμα στον γονέα και στη συνέχεια να τερματίζουν την εκτέ λεση τους. Ο γονέας να διαβάζει όλα τα μηνύματα απο το άλλο άκρο της σωλήνωσης, να τα τυπώνει στη standard έξοδο με τη σειρα που τα διαβάζει και να τερματίζει τον εαυτο του.
>_ ΛΥΣΗ
>_ ΕΠΕΞΗΓΗΣΗ 1. Η διεργασία-γονέας εκτελει την κλήση pipe(). 2. Η διεργασία-γονέας δημιουργει διεργασίες-παιδια μέσω κλήσεων fork(). 3. Οι διεργασίες-παιδια κλείνουν το άκρο ανάγνωσης της σωλήνωσης. 4. Η διεργασία-γονέας κλείνει το άκρο εγγραφής της σωλήνωσης. 5. Μεταφέρονται δεδομένα μέσω κλήσεων read() και write(). 6. Κάθε διεργασία κλείνει τα άκρα της σωλήνωσης που παρέμειναν ανοικτα.
UNIX is basically a simple operating system, but you have to be a genius to understand the simplicity. Dennis Ritchie