Εργαστήριο ΔΙΕΡΓΑΣΙΕΣ - ΔΙΑΧΕΙΡΙΣΗ Εισαγωγή Σκοπός τόσο αυτού του εργαστηρίου, όσο και των εργαστηρίων που ακολουθούν, είναι να γνωρίσουμε τους τρόπους δημιουργίας και διαχείρισης των διεργασιών (processes) από το λειτουργικό σύστημα και το χρήστη, επιλύνοντας ορισμένα πρακτικά προβλήματα. Διεργασίες Η διεργασία (process) αποτελεί ένα μηχανισμό εκτέλεσης ενός προγράμματος. Κάθε διεργασία εκτελεί ένα μοναδικό πρόγραμμα (όμως ένα πρόγραμμα από μόνο του δεν αποτελεί διεργασία). Το ίδιο πρόγραμμα μπορεί να εκτελείται από πολλές διεργασίες (με διαφορετικές ταχύτητες και ακολουθίες εκτέλεσης των εντολών). Η εκτέλεση μιας διεργασίας γίνεται σειριακά. Το λειτουργικό μπορεί όμως να εκτελεί πολλές διεργασίες «παράλληλα» μεταξύ τους. Οι καταστάσεις μιας διεργασίας έχουν ως εξής: new: η διεργασία δημιουργείται running: η διεργασία εκτελείται σε κάποιον επεξεργαστή waiting: η διεργασία αναμένει (απενεργοποιημένη) κάποιο συμβάν (event/signal) ready: η διεργασία αναμένει να τις δοθεί (από το ΛΣ) σε κάποιον επεξεργαστή για τη συνέχιση της εκτέλεσης της terminated: η διεργασία έχει ολοκληρώσει την εκτέλεσή της Τα τμήματα μνήμης μιας διεργασίας έχουν ως εξής: Τμήμα κώδικα (code section): περιέχει τις εντολές του προγράμματος που εκτελείται Τμήμα καθολικών δεδομένων (static/global data section): περιέχει τα μόνιμα δεδομένα του προγράμματος που εκτελείται Τμήμα δυναμικών δεδομένων (dynamic data section): περιέχει τα δυναμικά δεδομένα της εκτέλεσης Στοίβα/σωρός (stack): περιέχει τις προσωρινές τοπικές μεταβλητές της εκτέλεσης που δημιουργούνται λόγω κλήσεων συναρτήσεων. Δημιουργία διεργασίας 4 1 Το Λειτουργικό Σύστημα Unix 2 Διαχείριση Αρχείων & Καταλόγων στο Unix 3 Προγραμματισμός στο Φλοιό C 4 Διεργασίες - Διαχείριση 5 Διεργασίες - Εκτέλεση Εντολών 6 Διεργασίες Επικοινωνία με Σωληνώσεις/Διοχέτευση 7 Διαχείριση Νημάτων 8 Συγχρονισμός Διεργασιών & Νημάτων (mutexes) 9 Συγχρονισμός Διεργασιών & Νημάτων (conditions) 10 Συγχρονισμός Διεργασιών & Νημάτων (semaphores) 11 Χρονοπρογραμματισμός και Διαχείριση Μνήμης 12 Διαδιεργασιακή Επικοινωνία - IPC Οι πατρικές διεργασίες (parent processes) δημιουργούν διεργασίες παιδιά (child processes), όπου η κάθε διεργασία-παιδί αποτελεί αντίγραφο του πατέρα, και οι οποίες με τη σειρά τους μπορούν να δημιουργούν άλλες διεργασίες (αντίγραφά τους), δημιουργώντας έτσι ένα ιεραρχικό δένδρο διεργασιών. Με την κλήση συστήματος fork() δημιουργούνται νέες διεργασίες ως αντίγραφα, ενώ με την κλήση συστήματος exec(), η διεργασία-παιδί (ή και η διεργασία-πατέρας) που
την καλεί, μπορεί να φορτώσει και να εκτελέσει κάποιο πρόγραμμα, και έτσι να γίνεται αντικατάσταση του χώρου μνήμης της διεργασίας-παιδί με το νέο πρόγραμμα που εκτελείται. Παράδειγμα της σύνταξης της κλήσης fork(): pid = fork() όπου pid είναι ο κωδικός περιγραφής της διεργασίας (Process ID). Κατά την εκτέλεση, οι πατρικές και οι θυγατρικές διεργασίες εκτελούνται ταυτόχρονα, και οι πατρικές διεργασίες περιμένουν τον τερματισμό των παιδιών με χρήση της κλήσης συστήματος wait(). Με αυτόν τον τρόπο γίνεται και η μεταφορά δεδομένων προς τον πατέρα. Παράδειγμα της σύνταξης της κλήσης wait(): chpid = wait(&status); Η κλήση wait() επιστρέφει το PID της διεργασίας παιδί που τερματίζεται (chpid) και τοποθετεί ένα κωδικό στη μεταβλητή status σχετικά με την κατάσταση τερματισμού της διεργασίας παιδί. Στη διεργασία-πατέρας η κλήση fork() επιστρέφει το pid της διεργασίας παιδί (εάν δημιουργήθηκε επιτυχώς), και στη διεργασία-παιδί επιστρέφει το 0. Ο κωδικός της τρέχουσας διεργασίας μπορεί να δοθεί με την κλήση getpid(), ενώ ο κωδικός της πατρικής διεργασίας με την κλήση getppid(). Ο διαμοιρασμός των πόρων μπορεί να έχει ως εξής: οι διεργασίες-γονείς και οι διεργασίες-παιδιά μοιράζονται όλους τους πόρους, ή τα παιδιά παίρνουν υποσύνολο των πόρων, ή οι πατρικές και οι θυγατρικές δεν μοιράζονται κανέναν πόρο. Η διεργασία μετά την εκτέλεση της τελευταίας εντολής (exit() τερματισμός της διεργασίας) καλεί το ΛΣ, και οι πόροι της διεργασίας (η στοίβα, χώρος μνήμης) επανέρχονται στη διάθεση του ΛΣ. Η πατρική διεργασία μπορεί να διακόψει τη θυγατρική (abort), όταν: - Η θυγατρική έχει υπερβεί τους πόρους που της είχαν ανατεθεί - Η εργασία που ανατέθηκε στο παιδί δε χρειάζεται Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 2
Στον πίνακα που ακολουθεί δίνεται μια σύνοψη των κλήσεων του συστήματος (Unix/Linux) για τη διαχείριση των διεργασιών (process management): ΠΑΡΑΔΕΙΓΜΑ 1 Παρουσίαση προγραμμάτων δημιουργίας διεργασίας παιδί με χρήση της fork(). void main() int pid; pid = fork(); if (pid<0) /* τότε η fork() απέτυχε, πιθανώς η μνήμη γεμάτη */ handle_error(); /* ρουτίνα διαχείρισης σφάλματος */ else /* αλλιώς η fork() ήταν επιτυχής */ if (pid>0) /* pid >0, τότε εδώ τοποθετείται ο κώδικας της διεργασίας-πατέρα*/.. /* κώδικας της διεργασίας-πατέρα */ else /* pid =0, τότε εδώ τοποθετείται ο κώδικας της διεργασίας-παιδί */.. /* κώδικας της διεργασίας-παιδί */ ΠΑΡΑΔΕΙΓΜΑ 2 #include <sys/types.h> int main(void) pid_t child; Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 3
if (!(child = fork())) printf( Στο τμήμα του κώδικα της διεργασίας-παιδί\n ); exit(0); printf( Στο τμήμα του κώδικα της διεργασίας-πατέρα το παιδί είναι: %d\n, child); return 0; ΠΑΡΑΔΕΙΓΜΑ 3 Παρουσίαση των κλήσεων του συστήματος: fork(), getpid(), getppid(), στο παρακάτω πρόγραμμα το οποίο διπλασιάζεται και στη συνέχεια διακλαδώνεται ανάλογα με την τιμή επιστροφής της fork(). main() int pid; pid = fork(); if (pid!=0) /* κώδικας διεργασίας-πατέρα, parent code*/ printf( Διεργασία πατέρας με PID %d και PPID %d\n,getpid(),getppid()); printf( Το PID της διεργασίας-παιδί είναι: %d\n,pid); else /* κώδικας διεργασίας-παιδί, child code */ printf( Διεργασία παιδί με PID %d και PPID %d\n,getpid(), getppid()); printf( Η διεργασία με PID %d τερματίζει \n,getpid()); Το παραπάνω πρόγραμμα δίνει ενδεικτικά τις ακόλουθες γραμμές εξόδου: Διεργασία πατέρας με PID 1281 και PPID 1272 Διεργασία παιδί με PID 1282 και PPID 1281 Η διεργασία με PID 1282 τερματίζει Το PID της διεργασίας-παιδί είναι: 1282 Η διεργασία με PID 1281 τερματίζει Παρατηρούμε ότι όταν η αρχική διεργασία δημιουργεί ένα αντίγραφό της (διεργασία παιδί - child process) με την fork(), η διεργασία αυτή (διεργασία πατέρας - parent process) εκτελείται παράλληλα με τη διεργασία παιδί (όπως φαίνεται από τις δύο πρώτες γραμμές εξόδου), (συναγωνίζονται για κτήση χρόνου επεξεργασίας της CPU, ως επακόλουθο της ταυτόχρονης ύπαρξης στο σύστημα). ΠΑΡΑΔΕΙΓΜΑ 4 Παράδειγμα συνεργασίας διεργασιών (πατέρας και παιδί) στο οποίο η διεργασίαπατέρας περιμένει (με χρήση της wait()) τη διεργασία-παιδί να τερματίσει. Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 4
main() int pid, status, chpid; pid = fork(); if (pid!=0) /* ΚΩΔΙΚΑΣ ΔΙΕΡΓΑΣΙΑΣ ΠΑΤΕΡΑ */ printf( Διεργασία πατέρας με PID %d και PPID %d\n,getpid(),getppid()); chpid=wait(&status); /* Επιστρέφει το PID της διεργασίας παιδί που τερματίζεται και τοποθετεί ένα κωδικό στη status*/ printf( Η διεργασία-παιδί με PID %d τερμάτισε με κωδικό εξόδου\n,pid,status); else /* ΚΩΔΙΚΑΣ ΔΙΕΡΓΑΣΙΑΣ ΠΑΙΔΙ */ printf( Διεργασία παιδί με PID %d και PPID %d\n,getpid(), getppid()); exit(42); printf( Η διεργασία με PID %d τερματίζει \n,getpid()); ΠΑΡΑΔΕΙΓΜΑ 5 Παράδειγμα δημιουργίας διεργασίας-πατέρα και διεργασίας-παιδί. #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> int main(int argc,char **argv) pid_t pid; /* Το PID του child process */ pid = fork(); /* Δημιουργία μιας νέας διεργασίας child process */ if ( pid == -1 ) fprintf(stderr, "%s: απέτυχε η fork()\n", strerror(errno)); exit(13); else if ( pid == 0 ) /* κώδικας διεργασίας-παιδί */ printf("pid %ld: η διεργασία-παιδί ξεκίνησε, ο πατέρας είναι %ld.\n", (long)getpid(), /* το PID της διεργασίας*/ (long)getppid()); /* το PID της διεργασίας-πατέρα */ else /* κώδικας διεργασίας-πατέρα */ printf("pid %ld: η διεργασία-παιδί ξεκίνησε PID %ld.\n", (long)getpid(), /* το PID της διεργασίας-πατέρα */ (long)pid); /* το PID της διεργασίας-παιδί */ Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 5
sleep(1); /* χρονοκαθυστέρηση 1sec, απαιτείται διότι εδώ δεν έχουμε κάποιο συγχρονισμό μεταξύ του τερματισμού της διεργασίας παιδί και της διεργασίας πατέρα */ return 0; ΠΑΡΑΔΕΙΓΜΑ 6 Μια βελτίωση του παραπάνω παραδείγματος με χρήση της wait(). #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc,char **argv) pid_t pid; /* το PID της διεργασίας-παιδί child process */ pid_t wpid; /* το PID από την wait() */ int status; /* η κατάσταση εξόδου από την wait() */ pid = fork(); /* Δημιουργία νέας διεργασίας-παιδί child process */ if ( pid == -1 ) fprintf(stderr, "%s: απέτυχε η fork()\n", strerror(errno)); exit(13); else if ( pid == 0 ) printf("pid %ld: η διεργασία-παιδί ξεκίνησε, ο πατέρας είναι %ld.\n", (long)getpid(), /* το PID της διεργασίας-παιδί*/ (long)getppid()); /* το PID της διεργασίας-πατέρα*/ else /* τα PID της διεργασίας του πατέρα και του παιδιού */ printf("pid %ld: Started child PID %ld.\n", (long)getpid(), (long)pid); wpid = wait(&status); /* όπου status ο κωδικός εξόδου της διεργασίας-παιδί */ if ( wpid == -1 ) fprintf(stderr,"%s: wait()\n", strerror(errno)); return 1; else if ( wpid!= pid ) abort(); else printf("η διεργασία-παιδί PID %ld τερμάτισε με κατάσταση εξόδου 0x%04X\n", (long)pid, status); /*το PID της διεργασίας-παιδί και κατάσταση εξόδου*/ return 0; Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 6
Εργαστήριο 4: Ασκήσεις Οι εργαστηριακές ασκήσεις που ακολουθούν αφορούν προβλήματα διαχείρισης των διεργασιών (processes) στο λειτουργικό σύστημα Unix, τις οποίες ο σπουδαστής μπορεί να επιλύσει ακολουθώντας τα βήματα στα παραδείγματα που δόθηκαν σε αυτό το εργαστήριο. 4.1 Να γραφεί κώδικας ο οποίος δημιουργεί μια νέα διεργασία-παιδί η οποία εμφανίζει το ονοματεπώνυμό σας. 4.2 Να γραφεί κώδικας ο οποίος δημιουργεί μια νέα διεργασία-παιδί η οποία εμφανίζει την ειδικότητά σας, ενώ η διεργασία-πατέρας εμφανίζει την ονομασία του τμήματός σας. 4.3 Να γραφεί κώδικας ο οποίος δημιουργεί μια νέα διεργασία-παιδί η οποία στη συνέχεια δημιουργεί μια ακόμη διεργασία-παιδί. Να εμφανίζονται οι κωδικοί (pid) της κάθε διεργασίας, και η αρχική διεργασία-πατέρας να περιμένει τον τερματισμό της εκτέλεσης των διεργασιών παιδιών. Τι παρατηρείται κατά την εκτέλεση του προγράμματος; 4.4 Να γραφεί κώδικας ο οποίος δημιουργεί μια νέα διεργασία-παιδί η οποία στη συνέχεια δημιουργεί μια ακόμη διεργασία-παιδί. Η πρώτη διεργασία παιδί να εμφανίζει την τρέχουσα ημερομηνία, ενώ η δεύτερη διεργασία παιδί την τρέχουσα διαδρομή. 4.5 Να γραφεί κώδικας ο οποίος δημιουργεί μια νέα διεργασία-παιδί η οποία στη συνέχεια αφού υπολογίσει το άθροισμα δύο αριθμητικών δεδομένων τερματίζει, ενώ ή η διεργασία πατέρας που περίμενε (χρήση της wait) ελέγχει τον κώδικα τερματισμού της διεργασίας και αντίστοιχα βγάζει κάποιο μήνυμα. Σημειώσεις του μαθήματος Λειτουργικά Συστήματα 7