Processes
Εισαγωγή Όταν εκκινεί το Λειτουργικό Σύστημα, ένα σύνολο λειτουργιών εκκινούν ως ξεχωριστές διεργασίες και καλεί ένα πρόγραμμα το οποίο ονομάζεται π.χ. Στο Linux init
Εισαγωγή Αρκετά προγράμματα εκτελούνται ως δαίμονες Εκκινούν και εκτελούνται στο background, συνήθως, χωρίς αλληλεπίδραση με τους χρήστες Μπορεί να δημιουργηθεί μια ιεραρχία διεργασιών με τη μορφή πατέρας – παιδί (parent – child) Ο πυρήνας κρατά συγκεκριμένες πληροφορίες για κάθε διεργασία όπως το ID κάθε διεργασίας (process ID – PID)
Εισαγωγή Η εντολή top παρουσιάζει πληροφορίες σχετικά με τις διεργασίες που εκτελούνται Οι πληροφορίες ανανεώνονται κάθε 3 δευτερόλεπτα Στην αρχή των αποτελεσμάτων παρουσιάζονται οι συνολικές πληροφορίες του συστήματος ενώ η λίστα των διεργασιών ξεκινά με αυτές τις διεργασίες που έχουν τη μεγαλύτερη δραστηριότητα
Εισαγωγή
Ιεραρχία Διεργασιών Όταν μια διεργασία δημιουργεί / καλεί μια νέα διεργασία τότε ονομάζεται πατρική διεργασία (parent) ενώ η καλούμενη ονομάζεται διεργασία παιδί (child) Ο πυρήνας του Linux χρησιμοποιεί ένα scheduler διεργασιών ώστε να αποφασίζει το ποια διεργασία θα είναι αυτή που θα χρησιμοποιήσει τον επεξεργαστή Κάθε διεργασία έχει και μια προτεραιότητα που υιοθετείται από τον scheduler ώστε να αποφασίσει τη σειρά εκτέλεσης των διεργασιών Διεργασίες με υψηλή προτεραιότητα εκτελούνται πιο συχνά ενώ διεργασίες με χαμηλή προτεραιότητα εκτελούνται πιο αραιά
Ιεραρχία Διεργασιών Η προκαθορισμένη προτεραιότητα είναι ίση με 0 Το εύρος τιμών των προτεραιοτήτων είναι από -20 μέχρι +20 Το λειτουργικό σύστημα καθορίζει την προτεραιότητα κάθε διεργασίας με βάση μια nice τιμή και την συμπεριφορά της κάθε διεργασίας Για παράδειγμα, διεργασίες που εκτελούνται για μεγάλες χρονικές περιόδους χωρίς διακοπές ‘παίρνουν’ χαμηλές προτεραιότητες
Δημιουργία και Εκτέλεση Διεργασιών Έχουμε τη δυνατότητα μέσα από ένα πρόγραμμα να δημιουργήσουμε μια διεργασία και να την εκτελέσουμε με τη βοήθεια της βιβλιοθήκης stdlib.h της C Για το σκοπό αυτό κάνουμε χρήση της συνάρτησης int system (const char *string);
Δημιουργία και Εκτέλεση Διεργασιών Στο ακόλουθο παράδειγμα, παρουσιάζουμε την εκτέλεση της εντολής ps ax του Linux μέσα από ένα πρόγραμμα C. #include <stdlib.h> #include <stdio.h> int main() { printf("Running ps with system\n"); system("ps ax"); printf("Done.\n"); exit(0); }
Δημιουργία και Εκτέλεση Διεργασιών Μπορούμε να θέσουμε την εκτέλεση της εντολής ps ax στο παρασκήνιο θέτοντας την εντολή system(“ps ax &”); Προγραμματιστικά το PID αναπαριστάται από τον τύπο pid_t που ορίζεται στη βιβλιοθήκη <sys/types.h> Η κλήση συστήματος pid_t getpid (void); μας επιστρέφει το PID της διεργασίας ενώ η pid_t getppid (void); μας δίνει το PID της πατρικής
Δημιουργία και Εκτέλεση Διεργασιών #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf ("My pid=%d\n", getpid ( )); printf ("Parent's pid=%d\n", getppid ( )); exit(0); }
Τερματισμός Διεργασιών Ο τερματισμός των διεργασιών γίνεται με την εντολή void exit (int status); και με χρήση της βιβλιοθήκης <stdlib.h> Μια κλήση στην exit() οδηγεί στην εκτέλεση κάποιων απαραίτητων βημάτων και στη συνέχεια στον τερματισμό της διεργασίας Το status & 0377 επιστρέφει στην πατρική διεργασία, η τιμή 0 δείχνει επιτυχία ενώ μια τιμή διαφορετική του 0 δείχνει ανεπιτυχή εκτέλεση Τα EXIT_SUCCESS και EXIT_FAILURE καθορίζονται για να δείξουν επιτυχία ή αποτυχία Συνεπώς, μια επιτυχής έξοδος μπορεί να επιδειχθεί με την ακόλουθη εντολή exit (EXIT_SUCCESS);
Τερματισμός Διεργασιών Τα βήματα που επιτελούνται πριν το τερματισμό μιας διεργασίας είναι τα ακόλουθα: Καλούνται όλες οι συναρτήσεις που έχουν καταχωρηθεί με τις atexit( ), on_exit( ) σε αντίστροφη σειρά καταχώρησης. Επιχειρείται το flushing όλων των ανοιχτών Ι/Ο ροών. Διαγράφονται όλα τα προσωρινά αρχεία που έχουν δημιουργηθεί με τη συνάρτηση tmpfile( ). Η exit() καλεί την _exit() ώστε ο πυρήνας να διαχειριστεί την υπόλοιπη διαδικασία τερματισμού της διεργασίας. #include <unistd.h> void _exit (int status);
Τερματισμός Διεργασιών Η κλήση συστήματος atexit() επιτρέπει τον καθορισμό συναρτήσεων που θα εκτελεστούν κατά την ομαλή διαδικασία τερματισμού μιας διεργασίας, π.χ. με κλήση της exit() ή της return. #include <stdlib.h> int atexit (void (*function)(void));
Τερματισμός Διεργασιών #include <stdio.h> #include <stdlib.h> void out (void) { printf ("atexit( ) succeeded!\n"); } int main (void) if (atexit (out)) fprintf(stderr, "atexit( ) failed!\n"); return 0;
Τερματισμός Διεργασιών Ενώ η atexit() έχει καθοριστεί σύμφωνα με το POSIX 1003.1-2001, το SunOS 4 ορίζει την ισοδύναμη της που είναι η on_exit() H on_exit() έχει την ίδια λειτουργία με την atexit(). #include <stdlib.h> int on_exit (void (*function)(int , void *), void *arg);
Οικογένεια exec Η οικογένεια εντολών exec μας επιτρέπει να εκκινήσουμε νέες διεργασίες Κάθε έκδοση της exec διαφοροποιείται στον τρόπο που εκκινεί τις διεργασίες και παρουσιάζει τις παραμέτρους των προγραμμάτων Η exec αντικαθιστά την τρέχουσα διεργασία με μια νέα διεργασία όπως καθορίζεται από τις παραμέτρους της εντολής Με την exec διευκολύνεται το πέρασμα από το ένα πρόγραμμα σε κάποιο άλλο
Οικογένεια exec Οι εντολές exec έχουν ως εξής: #include <unistd.h> char **environ; int execl(const char *path, const char *arg0, ..., (char *)0); int execlp(const char *file, const char *arg0, ..., (char *)0); int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
Οικογένεια exec #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { printf("Running ps with execlp\n"); execlp("ps", "ps", "ax", (char *)0); printf("Done.\n"); exit(0); }
Εντολή fork Όταν επιθυμούμε να χρησιμοποιήσουμε τις διεργασίες ώστε να εκτελούν περισσότερες από μια εργασίες κάθε φορά τότε θα πρέπει είτε να υιοθετήσουμε προγραμματισμό με νήματα (threads) είτε να δημιουργήσουμε μια ξεχωριστή διεργασία για το σκοπό αυτό Για τη δημιουργία ξεχωριστών διεργασιών χρησιμοποιούμε την εντολή fork() Η εντολή ‘διπλασιάζει’ την τρέχουσα διεργασία δημιουργώντας μια νέα εγγραφή στον πίνακα των διεργασιών που διατηρεί το Λειτουργικό Σύστημα Η νέα διεργασία είναι σχεδόν πανομοιότυπη με την αρχική και εκτελεί τον ίδιο κώδικα όμως με το δικό της χώρο δεδομένων, περιβάλλον και περιγραφείς αρχείων
Εντολή fork Η σύνταξη της fork() έχει ως ακολούθως: #include <sys/types.h> #include <unistd.h> pid_t fork(void);
Εντολή fork Το επόμενο σχήμα μας δείχνει τον τρόπο εκτέλεσης της fork().
Εντολή fork Η fork() μας επιτρέπει να αναγνωρίσουμε μέσα στον κώδικα το ποια διεργασία είναι μέσω του αποτελέσματος εκτέλεσης Αν η fork() επιστρέψει -1, τότε κάποιο σφάλμα έχει συμβεί Για παράδειγμα, το σφάλμα εκτέλεσης θα εμφανιστεί αν η πατρική διεργασία ξεπεράσει το μέγιστο αριθμό παιδιών που μπορεί να δημιουργήσει (CHILD_MAX) Αν η fork() επιστρέψει 0, τότε πρόκειται για τη διεργασία παιδί στην πατρική διεργασία επιστρέφει το PID της νέας διεργασίας – παιδί Η νέα διεργασία και η πατρική συνεχίζουν να εκτελούνται όπως η αρχική διεργασία
Εντολή fork Παράδειγμα 1 pid_t new_pid; new_pid = fork(); switch(new_pid) { case -1 : /* Error */ break; case 0 : /* We are child */ default : /* We are parent */ }
Εντολή fork Παράδειγμα 2 pid_t pid; pid = fork ( ); if (pid > 0) printf ("I am the parent of pid=%d!\n", pid); else if (!pid) printf ("I am the baby!\n"); else if (pid == -1) perror ("fork");
Εντολή fork case 0: message = "This is the child"; n = 5; break; #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pid; char *message; int n; printf("fork program starting\n"); pid = fork(); switch(pid) case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; n = 5; break; default: message = "This is the parent"; n = 3; } for(; n > 0; n--) { puts(message); sleep(1); exit(0);
Εντολή fork Αποτέλεσμα εκτέλεσης:
Εντολή fork Άλλο ένα παράδειγμα:
Εντολή fork Αποτελέσματα:
Αναμονή Τερματισμού Διεργασιών Η δημιουργία μιας νέας διεργασίας με την εντολή fork() αποδίδει ανεξαρτησία στην εκτέλεση της διεργασίας – παιδί Όμως, ορισμένες φορές, η πατρική διεργασία θα πρέπει να γνωρίζει το πέρας της εκτέλεσης της διεργασίας – παιδί Ιδιαίτερα σε περιπτώσεις όπου η πατρική – διεργασία δημιουργεί πολλαπλές διεργασίες Η εντολή wait επιτρέπει στην πατρική διεργασία να περιμένει το πέρας της εκτέλεσης των διεργασιών – παιδιών #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *stat_loc);
Αναμονή Τερματισμού Διεργασιών Με την εντολή wait η διεργασία – πατέρας σταματά μέχρι ένα από τις διεργασίες – παιδιά τερματίσει την εκτέλεσή του Η κλήση της εντολής επιστρέφει το PID διεργασίας – παιδί Η πληροφορία της κατάστασης της διεργασίας παιδιού (status information) είναι η τιμή που επιστρέφει η main ή η τιμή που περνάει με την εντολή exit Εφόσον ο δείκτης stat_loc δεν είναι null, στη θέση που δείχνει ο δείκτης θα γραφτεί το status information Στη βιβλιοθήκη sys/wait.h περιλαμβάνεται ένα σύνολο μακρο-εντολών με τις οποίες μπορούμε να μεταφράσουμε το status information
Αναμονή Τερματισμού Διεργασιών
Αναμονή Τερματισμού Διεργασιών Παράδειγμα:
Αναμονή Τερματισμού Διεργασιών Αποτέλεσμα:
Αναμονή Τερματισμού Διεργασιών Όταν μια διεργασία – παιδί τερματίσει τότε η σύνδεση πατέρα – παιδί παραμένει ‘ζωντανή’ μέχρι η διεργασία - πατέρας να τερματίσει ή να καλέσει ξανά την εντολή wait Η διεργασία παιδί παραμένει στον πίνακα των διεργασιών αφού ο κωδικός εξόδου της πρέπει να είναι αποθηκευμένος αφού υπάρχει η πιθανότητα η διεργασία – πατέρας να καλέσει την εντολή wait Σε αυτές τις περιπτώσεις η διεργασία – παιδί ονομάζεται διεργασία zombie
Αναμονή Τερματισμού Διεργασιών Στο ακόλουθο παράδειγμα, η διεργασία – παιδί τερματίζει την εκτύπωση των μηνυμάτων πριν την πατρική διεργασία
Αναμονή Τερματισμού Διεργασιών Αποτέλεσμα:
Αναμονή Τερματισμού Διεργασιών Ένας άλλος τρόπος για να περιμένουμε τον τερματισμό εκτέλεσης μιας διεργασίας – παιδί είναι η εντολή waitpid() #include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *stat_loc, int options);
Αναμονή Τερματισμού Διεργασιών Η παράμετρος pid καθορίζει επ’ ακριβώς το ποια διεργασία είναι αυτή της οποίας θα αναμείνουμε τον τερματισμό Οι τιμές που μπορεί να πάρει έχουν ως ακολούθως: < -1. Αναμένει για οποιαδήποτε διεργασία – παιδί για την οποία το ID της ομάδας διεργασιών είναι ίσο με την απόλυτη τιμή της τιμής που δίνουμε. Για παράδειγμα, αν βάλουμε -500 περιμένουμε να τερματιστεί η ομάδα διεργασιών με ID ίσο με 500. -1. Αναμένουμε για τον τερματισμό οποιασδήποτε διεργασίας – παιδί (ίδια με την εντολή wait()). 0. Αναμένουμε για τον τερματισμό οποιασδήποτε διεργασίας – παιδί που ανήκει στην ίδια ομάδα διεργασιών. > 0. Αναμένουμε τον τερματισμό οποιαδήποτε διεργασίας που ανήκει στην ομάδα διεργασιών που έχει ID ίσο με τον αριθμό που δίνουμε.
Ασκήσεις με fork() #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { int x = 1; pid_t pid = fork(); if (pid == 0) { printf("Child has x = %d\n", ++x); } else { printf("Parent has x = %d\n", --x); printf("Bye from process %d with x = %d\n", getpid(), x);
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() printf("L0\n"); fork(); printf("L1\n"); printf("Bye\n");
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() #include <stdio.h> #include <sys/types.h> int main() { fork(); printf("hello\n"); return 0; }
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() void fork3() { printf("L0\n"); fork(); printf("L1\n"); printf("L2\n"); printf("Bye\n"); }
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() void fork4() { printf("L0\n"); if (fork() != 0) { printf("L1\n"); printf("L2\n"); fork(); } printf("Bye\n");
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() void fork6() { if (fork() == 0) { /* Child */ printf("Terminating Child, PID = %d\n", getpid()); exit(0); } else { printf("Running Parent, PID = %d\n", getpid()); while (1) ; /* Infinite loop */ }
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() void fork7() { if (fork() == 0) { /* Child */ printf("Running Child, PID = %d\n", getpid()); while (1) ; /* Infinite loop */ } else { printf("Terminating Parent, PID = %d\n", getpid()); exit(0); }
Ασκήσεις με fork() Αποτέλεσμα
Ασκήσεις με fork() int a=10; if (fork() == 0) { a = a + 5; printf("%d, %d\n", a, &a); } else a = a –5; printf("%d, %d\n", a, &a);
Ασκήσεις με fork() Αποτέλεσμα (πρόκειται για virtual address και όχι για physical):