Πανεπιστήμιο Θεσσαλίας Τμήμα Πληροφορικής ΕΥ311-Διαδικτυακός και Ταυτόχρονος Προγραμματισμός Εργαστήριο: Παραδείγματα δημιουργίας διεργασιών στο Linux Ένα πρόγραμμα (το στιγμιότυπο της εκτέλεσης του οποίου αποτελεί μια διεργασία) μπορεί να δημιουργήσει διεργασίες-παιδιά (ή θυγατρικές διεργασίες) χρησιμοποιώντας την κλήση συστήματος fork(). pid_t fork(void); Επιτυχής εκτέλεση της fork() επιστρέφει το process id της διεργασίας παιδί που δημιουργήθηκε στην πατρική διεργασία και 0 στη διεργασία παιδί. Παράδειγμα 1 Στον κώδικα που ακολουθεί βρίσκεται ένα απλό παράδειγμα δημιουργίας διεργασιών. Εκτελέστε το και παρατηρείστε ότι παρότι η printf() καλείται μια φορά, η έξοδος εμφανίζεται δύο φορές (μια για τον πατέρα και μια για το παιδί). pid_t p; /* fork returns type pid_t */ p = fork(); printf("fork returned %d\n", p); Η διεργασία παιδί είναι μια νέα διεργασία, με δικό της process id, της οποίας ο κώδικας και οι πόροι που διαθέτει αποτελούν αντίγραφο της διεργασίας που τη δημιούργησε. Η εκτέλεση στη διεργασία παιδί, θα ξεκινήσει από το σημείο του κώδικα της πατρικής διεργασίας που κλήθηκε η fork(). Παράδειγμα 2 Στη συνέχεια, σε ένα πιο προχωρημένο παράδειγμα, η τιμή που επιστρέφεται από τη fork() ελέγχεται και ανάλογα με τον αν εκτελείται ο κώδικας της πατρικής ή της θυγατρικής διεργασίας εμφανίζονται τα process ids της αρχικής και της θυγατρικής διεργασίας με τη χρήση των κατάλληλων, κατά περίπτωση, κλήσεων συστήματος: pid_t getpid(void); pid_t getppid(void); 1
Η κλήση συστήματος getpid() επιστρέφει το process id της διεργασίας που την καλεί και η κλήση συστήματος getppid() επιστρέφει, στη διεργασία που την καλεί, το process id της πατρικής της διεργασίας. pid_t p; printf("original program, pid=%d\n", getpid()); p = fork(); if (p == 0) { printf("in child process, pid=%d, ppid=%d\n", getpid(), getppid()); else { printf("in parent, pid=%d, fork returned=%d\n", getpid(), p); Παράδειγμα 3 Στο ακόλουθο παράδειγμα δείτε πως αντικαθιστούμε τον κώδικα που θα εκτελέσει η θυγατρική διεργασία με την κλήση συστήματος execv(). //Δηλώσεις της οικογένειας κλήσεων συστήματος exec extern char **environ; 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 execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); run /* Define a null terminated array of the command to followed by any parameters, in this case none */ char *arg[] = { "/bin/ls", 0 ; /* fork, and exec within child process */ if (fork() == 0) { printf("in child process:\n"); execv(arg[0], arg); printf("i will never be called\n"); printf("execution continues in parent process\n"); 2
Παράδειγμα 4 Στο επόμενο παράδειγμα ο πατέρας περιμένει την ολοκλήρωση της εκτέλεσης του παιδιού με την κλήση συστήματος waitpid(). Οι κλήσεις συστήματος wait() και waitpid() αναστέλλουν τη λειτουργία της διεργασίας που τις καλεί έως ότου α) μια από τις διεργασίες παιδιά που έχουν δημιουργηθεί επιστρέψει (κλήση συστήματος wait() ή β) η διεργασία που προσδιορίζεται από το όρισμα pid της waitpid() επιστρέψει. Οι κλήσεις συστήματος wait() και waitpid() λαμβάνουν στη μεταβλητή status την κατάσταση που επέστρεψε (μέσω της return ή της exit()) η διεργασία παιδί. Την κατάσταση αυτή μπορούν να εμφανίζουν ως ακέραιο με τη μακροεντολή WEXITSTATUS (για περισσότερες πληροφορίες ανατρέξτε στο εγχειρίδιο του συστήματος). #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); run /* Define a null terminated array of the command to followed by any parameters, in this case none */ char *arg[] = { "/bin/pwd", 0 ; pid_t pid; int status; /* fork, and exec within child process */ if ((pid=fork()) == 0) { printf("in child process:\n"); sleep(30); execv(arg[0], arg); printf("i will never be called\n"); else if (pid<0) { //fork failed perror("fork"); status = -1; else { printf("execution continues in parent process\n"); if (waitpid (pid, &status, 0)!= pid) status = -1; return status; 3
Παράδειγμα 5 Στο επόμενο παράδειγμα η γονική διεργασία τερματίζει πριν τη διεργασία παιδί. Η διεργασία παιδί που έχει δημιουργηθεί, κληρονομείται από την αρχική διεργασία (init) του λειτουργικού συστήματος Linux με process id 1. Στο παρακάτω παράδειγμα, η πατρική διεργασία καλεί τη fork(), περιμένει δύο δευτερόλεπτα (καλώντας τη sleep(2)) και στη συνέχεια τερματίζει. H διεργασία παιδί συνεχίζει εμφανίζοντας το process id της γονικής της διεργασίας για 5 δευτερόλεπτα. Παρατηρείστε ότι το ppid της διεργασίας αλλάζει σε 1 μετά τον τερματισμό της πατρικής διεργασίας (μετά την ολοκλήρωση της πατρικής διεργασίας ο έλεγχος επιστρέφει στο κέλυφος του λειτουργικού συστήματος αφού το παιδί εκτελείται στο παρασκήνιο). int main(void) { int i; if (fork()) { /* Parent */ sleep(2); _exit(0); for (i=0; i < 5; i++) { printf("my parent is %d\n", getppid()); sleep(1); Παράδειγμα 6 Το παράδειγμα 6 δείχνει το αντίθετο από το παράδειγμα 5, δηλαδή τι συμβαίνει όταν η διεργασία παιδί τερματίζει πριν τη γονική διεργασία. int main(void) { int i; if (!fork()) { /* Child exits immediately*/ _exit(0); /* Parent waits around for a minute */ sleep(60); Εκτελέστε το πρόγραμμα στο παρασκήνιο (χρησιμοποιώντας τον τελεστή &). Εμφανίστε μια λίστα των διεργασιών του συστήματος (με την εντολή ps ef grep <αριθμός διεργασίας που επιστράφηκε με την εκτέλεση του προγράμματος>). Η διεργασία παιδί, παρότι τερματίστηκε, εμφανίζεται στο σύστημα 4
ως defunct ή αλλιώς ως zombie. Όταν η γονική διεργασία ολοκληρώσει, 60 δευτερόλεπτα αργότερα, και οι δύο διεργασίες εξαφανίζονται. Είναι απαραίτητο η γονική διεργασία να φροντίζει για την ολοκλήρωση της εκτέλεσης των θυγατρικών της διεργασιών και να μην αφήνει διεργασίες παιδιά σε κατάσταση defunct. Το γεγονός αυτό μπορεί να είναι ιδιαίτερα επιβλαβές για τους πόρους του συστήματος σε περίπτωση εκτέλεσης προγραμμάτων εξυπηρετητών που προγραμματίζονται να μην τερματίζουν από μόνοι τους ποτέ (μπορεί φυσικά να τους τερματίσει ο διαχειριστής του συστήματος). Διεργασίες παιδιά εξυπηρετητών που γίνονται defunct κρατούν δεσμευμένους πόρους και δεν τερματίζονται ποτέ. Παράδειγμα 7 Δείτε τι συμβαίνει στα ανοικτά αρχεία μιας διεργασίας, εκτελώντας τον κώδικα που ακολουθεί. Θα πρέπει να δημιουργήσετε ένα αρχείο με όνομα infile στον κατάλογο τον οποίο βρίσκεται και ο πηγαίος κώδικας του προγράμματος. Το αρχείο αυτό θα περιέχει έναν αριθμό από το 1 έως το 10 σε κάθε γραμμή. #include <strings.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { int fd_in, fd_out; char buf[1024]; memset(buf, 0, 1024); /* clear buffer*/ fd_in = open("/tmp/infile", O_RDONLY); fd_out = open("/tmp/outfile", O_WRONLY O_CREAT); fork(); /* It doesn't matter about child vs parent */ while (read(fd_in, buf, 2) > 0) { /* Loop through the infile */ printf("%d: %s", getpid(), buf); /* Write a line */ sprintf(buf, "%d Hello, world!\n\r", getpid()); write(fd_out, buf, strlen(buf)); sleep(1); memset(buf, 0, 1024); /* clear buffer*/ sleep(10); Όταν δημιουργούνται διεργασίες παιδιά ο πυρήνας του λειτουργικού συστήματος δημιουργεί ένα αντίγραφο όλων των ανοιχτών περιγραφέων αρχείου (file descriptors). Τα ερωτήματα που δημιουργούνται εδώ είναι: αν έχει ανοιχτεί ένα αρχείο πριν την κλήση της fork() τι συμβαίνει όταν και οι δύο διεργασίες επιχειρήσουν ανάγνωση ή εγγραφή; Υπάρχει περίπτωση μια διεργασία να γράψει δεδομένα πάνω σε αυτά της άλλης; Υπάρχει περίπτωση να διαβαστούν δύο αντίγραφα του ίδιου μέρους των περιεχομένων του αρχείου; 5
Η εκτέλεση του παραπάνω προγράμματος δείχνει ότι όταν μια διεργασία διαβάζει από το αρχείο, ο δείκτης της τρέχουσας θέσης του αρχείου μετακινείται και για τις δύο διεργασίες. Ομοίως, όταν μια διεργασία γράφει σε ένα αρχείο, η επόμενη εγγραφή γίνεται στο τέλος του αρχείου. Αυτό συμβαίνει διότι ο πυρήνας του λειτουργικού συστήματος είναι αυτός που διατηρεί τις πληροφορίες για την κατάσταση του ανοικτού αρχείου και ο περιγραφέας αρχείου χρησιμοποιείται ως ένας απλός προσδιοριστής για τη διεργασία. Υποδείξεις: Ο πηγαίος κώδικας από όλα τα παραδείγματα που χρησιμοποιούνται σε αυτό το κείμενο βρίσκεται στο φάκελο Έγγραφα Εργαστήριο Processes Source Code, στο δικτυακό τόπο του μαθήματος στο eclass. Η μεταγλώττιση ενός προγράμματος στο Linux γίνεται με τη χρήση της εντολής: gcc o <όνομα εκτελέσιμου> <όνομα αρχείου που περιέχει τον πηγαίο κώδικα>. Η εντολή αυτή θα πρέπει να δοθεί από τερματικό ενώ ο τρέχων κατάλογος εργασίας θα πρέπει να είναι αυτός στον οποίο βρίσκεται αποθηκευμένος ο πηγαίος κώδικας του προγράμματος. (Η εντολή με την οποία μπορούμε να βρούμε τον τρέχοντα κατάλογο είναι η pwd ενώ μετακινούμαστε μεταξύ καταλόγων με την εντολή cd. Τα περιεχόμενα των καταλόγων βρίσκονται με την εντολή ls. Περισσότερες πληροφορίες για τη λειτουργικότητα και τις παραμέτρους των εντολών (αλλά και των κλήσεων συστήματος) μπορούν να βρεθούν μέσω της βοήθειας του Linux που δίνεται αν πληκτρολογήσουμε man <όνομα εντολής>). Η εκτέλεση ενός προγράμματος γίνεται πληκτρολογώντας:./<όνομα εκτελέσιμου> Η εκτέλεση ενός προγράμματος στο παρασκήνιο γίνεται πληκτρολογώντας:./<όνομα εκτελέσιμου> & Πέτρος Λάμψας 6