ΑΤΕΙ Θεσσαλονίκης

Τµήµα Πληροφορικής

Λειτουργικά Συστήµατα Ι

Ιούνιος 2007

Η συνάρτηση fork() (Λειτουργικά συστήµατα)
Η συνάρτηση συστήµατος fork() σε ένα λειτουργικό σύστηµα όπως το UNIX και το POSIX
δηµιουργεί ένα αντίγραφο µιας διεργασίας το οποίο δίδεται στο σύστηµα προς εκτέλεση. Το
αντίγραφο καλείται παιδί (child) και η αρχική διεργασία καλείται γονέας (parent). Οι δύο διεργασίες
εκτελούνται ταυτόχρονα (concurrently). Προφανώς αυτό έχει νόηµα µόνο όταν το σύστηµα
υποστηρίζει πολυπρογραµµατισµό (multitasking) και συνεξέλιξη (concurrency) πράγµα, βέβαια,
που συµβαίνει µε όλα τα µοντέρνα λειτουργικά συστήµατα.
Η fork µπορεί να κληθεί µέσα από το πρόγραµµά µας, δηλαδή µπορούµε να κάνουµε µια κλήση
συστήµατος (system call). Η fork επιστρέφει την τιµή 0 µέσα στη διεργασία «παιδί» ενώ
επιστρέφει µια θετική τιµή µέσα στην διεργασία «γονέας». Για παράδειγµα, το παρακάτω πρόγραµµα
σε γλώσσα C δηµιουργεί ένα παιδί:
main(int argc, char *argv[])
{
int i;
pid_t h;
/* δήλωση της µεταβλητής h τύπου pid_t */

}

h = fork();
/*δηµιούργησε αντίγραφο*/
if (h==0) {
/* αυτό εκτελείται από το παιδί */
for (i=0; i<3; i++) {
fprintf(“**Child %d\n”, i);
}
exit(0);
/* το παιδί τερµατίζει κανονικά */
}
else if (h>0) { /* αυτό εκτελείται από τον γονέα */
for (i=0; i<3; i++) {
fprintf(“*Parent %d\n”, i);
}
exit(0);
/* ο γονέας τερµατίζει κανονικά */
}
else {
/* δεν δηµιουργήθηκε παιδί */
fprintf(“fork error!\n”);
exit(1);
/* ο γονέας τερµατίζει µε σφάλµα */
}

Το παιδί τρέχει ταυτόχρονα και ανεξάρτητα από τον γονέα και δεν βρίσκεται σε συγχρονισµό µαζί
του. Προφανώς λοιπόν στην οθόνη µας θα δούµε µια απρόβλεπτη µίξη από το output του παιδιού και
από το output του γονέα, όπως για παράδειγµα:
*Parent 0
**Child 0
**Child 1
*Parent 1
*Parent 2
**Child 2

είτε

είτε

*Parent 0
*Parent 1
*Parent 2
**Child 0
**Child 1
**Child 2

**Child 0
**Child 1
**Child 2
*Parent 0
*Parent 1
*Parent 2

...κλπ

Ο γονέας µπορεί να εκτελέσει και άλλα fork οπότε µπορεί να αποκτήσει και άλλα παιδιά. Οµοίως,
ένα παιδί µπορεί να εκτελέσει ένα ή περισσότερα fork οπότε να έχει και αυτό ένα ή περισσότερα
παιδιά.

Γιατί να χρησιµοποιήσουµε την συνάρτηση fork?
Αφού η συνάρτηση fork δηµιουργεί ένα ακριβές αντίγραφο της αρχικής διεργασίας µε ίδιο κώδικα,
ίσως κανείς να αναρωτηθεί, και λογικά, γιατί να µπούµε στον κόπο να εκτελέσουµε την fork. Η
Καθηγητής Κ. ∆ιαµαντάρας

1

/* ο γονέας τερµατίζει µε σφάλµα */ } else if (h1 == 0) { /* Κώδικας παιδιού 1 */ exit(0). h = wait(&status). h1 = fork(). σε κάποια λειτουργικά συστήµατα (βλ. exit(0). exit(1). Έτσι ενώ ο κώδικας είναι συνολικά ο ίδιος. /* το παιδί 1 τερµατίζει κανονικά */ } h2 = fork(). Μια διεργασία που εκτελεί wait() βρίσκεται σε µια από τις παρακάτω δύο καταστάσεις: 1. κατ’ αρχήν. Επί πλέον. Αν υπάρχει τουλάχιστον ένα παιδί σε εκτέλεση την ώρα που καλείται η wait() τότε ο γονέας που την κάλεσε θα µπλοκάρει (θα µπει σε αναµονή) µέχρι ένα από τα παιδιά του να τερµατιστεί. ∆ιαµαντάρας 2 . Η wait() παίρνει σαν όρισµα έναν pointer σε µια ακέραια µεταβλητή και επιστρέφει σαν έξοδο το process ID της διεργασίας παιδί που τελείωσε. h. Η συνάρτηση wait() Αν επιθυµούµε κάποιο συγχρονισµό µεταξύ γονέα και παιδιών µπορούµε να χρησιµοποιήσουµε την συνάρτηση συστήµατος wait(). UNIX). char *argv[]) { pid_t h1. το παιδί θα εκτελέσει άλλο κοµµάτι (πχ. h2. main(int argc. /* ∆ηµιουργία δεύτερου παιδιού */ if (h2 < 0) { printf("Error forking process 2\n"). το if) ενώ ο γονέας άλλο (πχ.ΑΤΕΙ Θεσσαλονίκης Τµήµα Πληροφορικής Λειτουργικά Συστήµατα Ι Ιούνιος 2007 απάντηση είναι ότι. Αν δεν υπάρχει παιδί που να εκτελείται την ώρα που καλείται η wait() τότε δεν συµβαίνει απολύτως τίποτα. h). /* περίµενε ένα οποιοδήποτε παιδί */ printf("Parent message: Child %d has finished\n". i. η fork είναι ο κατ’ εξοχήν τρόπος για να δηµιουργούµε νέες διεργασίες και να τις θέτουµε στο σύστηµα προς εκτέλεση. exit(1). το else). Αµέσως µετά ο γονέας θα συνεχίσει την εκτέλεση ακόµα και αν υπάρχουν και άλλα παιδιά του που συνεχίζουν να εκτελούνται. Αν υπάρχουν πολλά παιδιά µπορούν να χρησιµοποιηθούν είτε πολλαπλά if-then-else είτε µια εντολή switch. int status. µε την χρήση του if-then-else το παιδί εκτελεί άλλο κοµµάτι του προγράµµατος απ’ ότι ο γονέας. Είναι σα να µην υπάρχει. Η συνάρτηση αυτή θέτει την διεργασία που την καλεί σε κατάσταση αναµονής µέχρι να τερµατιστεί ένα οποιοδήποτε παιδί της. h). /* το παιδί 2 τερµατίζει κανονικά */ } } /* Από εδώ και κάτω κώδικας γονέα: */ h = wait(&status). 2. /* περίµενε το δεύτερο παιδί */ printf("Parent message: Child %d has finished\n". /* ο γονέας τερµατίζει κανονικά */ Καθηγητής Κ. /* ∆ηµιουργία πρώτου παιδιού */ if (h1 < 0) { printf("Error forking process 1\n"). /* ο γονέας τερµατίζει µε σφάλµα */ } else if (h2 == 0) { /* Κώδικας παιδιού 2 */ exit(0).

Αρχείο: myprogram. execv() Pointer to list of arguments. Non-executable files given to shell. πχ. h2.οτιδήποτε */ } Αρχείο: test. Uses search path. Αν είναι µικρότερο. Με τον τρόπο αυτό: • ο γονέας Α µε την fork() δηµιουργεί ένα παιδί Β που στην αρχή είναι ακριβές αντίγραφό του • κατόπιν η διεργασία Β καλεί την execl(“myprogram”) η οποία σβήνει από τη µνήµη όλες τις περιοχές που καταλάµβανε προηγουµένως η Β (τµήµα κώδικα. ο χώρος µνήµης που αποδεσµεύεται επιστρέφει στο λειτουργικό σύστηµα. } h2 = fork(). κλπ). Η διαδικασία αυτή είναι γνωστή ως overlaying. Ο συνδυασµός fork-exec είναι ο βασικός τρόπος δηµιουργίας και εκτέλεσης νέων διεργασιών στο λειτουργικό σύστηµα UNIX.c → δηµιουργεί το εκτελέσιµο αρχείο test το οποίο εκτελεί δύο διεργασίες “myprogram 25” και “myprogram 38” main(int argc. execle() List of arguments. γίνεται αίτηση για να δοθεί έξτρα χώρος. NULL). char *argv[]) { /* Έστω ότι το πρόγραµµα παίρνει */ /* ένα όρισµα n στο command line */ /* και κάνει κάτι . Όταν µια διεργασία Χ καλεί. Αποτελείται από τις εξής συναρτήσεις: Συνάρτηση Ορίσµατα execl() List of arguments. execlp() List of arguments. h1 = fork(). την execl(“myprogram”) το λειτουργικό σύστηµα φορτώνει το πρόγραµµα myprogram στον ίδιο χώρο µνήµης όπου προηγουµένως βρισκόταν η διεργασία Χ σβήνοντάς την τελείως και αντικαθιστώντας την µε το νέο πρόγραµµα. Uses search path. Non-executable files given to shell. ∆ιαµαντάρας /* δηµιούργησε δεύτερο παιδί */ 3 . Αν δε µπορεί να δοθεί ο απαιτούµενος χώρος η κλήση της execl() αποτυγχάνει.c → δηµιουργεί το εκτελέσιµο αρχείο myprogram main(int argc.”25”. σωρός. No environment passed. Η κλήση της execl() συνδυάζεται µε κλήση προηγουµένως στην fork(). execve() Pointer to list of arguments. Pointer to list of environment values. Pointer to list of environment values. /* δηµιούργησε ένα παιδί */ if (h1<0) {exit(1). char *argv[]) { pid_t h1. Προφανώς το νέο πρόγραµµα µπορεί να είναι µεγαλύτερο ή µικρότερο σε µέγεθος µνήµης από το Β.} /* error forking child 1 */ if (h1==0) { /* αντικατέστησε το παιδί */ /* µε το πρόγραµµα “myprogram 25” */ execl(“myprogram”. Στην ίδια θέση µνήµης τοποθετούνται τα αντίστοιχα τµήµατα του νέου προγράµµατος myprogram. ενώ αν είναι µεγαλύτερος. τµήµα δεδοµένων. Καθηγητής Κ. execvp() Pointer to list of arguments. στίβα.ΑΤΕΙ Θεσσαλονίκης Τµήµα Πληροφορικής Λειτουργικά Συστήµατα Ι Ιούνιος 2007 Η οικογένεια συναρτήσεων exec Η οικογένεια συναρτήσεων συστήµατος exec υπάρχει στα λειτουργικά συστήµατα Unix και POSIX. No environment passed.

while (1) { /* σε ατέρµονο loop κάνε τα εξής: */ ∆ιάβασε µια γραµµή εντολών στο string “command” και τις παραµέτρους στη λίστα “params” h = fork(). } } Καθηγητής Κ. κάλεσε τον error_handler(). Αυτή η συνάρτηση διατίθεται και στα Windows 95.”38”. } if (h==0) { /* το παιδί εκτελεί το command */ execl(command. Εναλλακτικά µπορεί κανείς να χρησιµοποιήσει την συνάρτηση συστήµατος CREATEPROCESS η οποία φορτώνει ένα νέο πρόγραµµα και ξεκινάει την εκτέλεσή του. Το shell του UNIX Το shell είναι ένα πρόγραµµα το οποίο διαβάζει µια γραµµή εντολών (command line) την οποία δακτυλογραφεί ο χρήστης από το πληκτρολόγιο και εκτελεί το πρόγραµµα που δακτυλογραφήθηκε. } } Στο λειτουργικό σύστηµα Windows ΝΤ υποστηρίζονται συναρτήσεις POSIX οπότε µπορεί κανείς να χρησιµοποιήσει fork-exec. char *argv[]) { char command[]. pid_t h. NULL). ∆ιαµαντάρας 4 .c: main(int argc. } /* o γονέας shell περιµένει το παιδί να τελειώσει */ /* και ξαναπηγαίνει στην αρχή του loop */ /* για να διαβάζει το επόµενο command */ wait(&status). Πχ. (τύπωσε τα ονόµατα των αρχείων ή directories που βρίσκονται στο current directory) ls ls a* (τύπωσε τα ονόµατα των αρχείων ή directories που αρχίζουν από “a”) cd /usr/bin (άλλαξε το current directory σε /usr/bin) .params).. int status. command). *params[].} /* error forking child 2 */ if (h2==0) { /* αντικατέστησε το δεύτερο παιδί */ /* µε το πρόγραµµα “myprogram 38” */ execl(“myprogram”.ΑΤΕΙ Θεσσαλονίκης Τµήµα Πληροφορικής Λειτουργικά Συστήµατα Ι Ιούνιος 2007 if (h2<0) {exit(1).κλπ Αρχείο myshell.. Το όνοµα του προγράµµατος µπορεί να ακολoυθείται και από άλλα strings – παραµέτρους. /* δηµιούργησε παιδί */ if (h<0) { /* error στο fork */ printf(“Cannot execute %s\n”.

Παράδειγµα: Αρχικός κώδικας L1: x = read(). Το νήµα που εκτελεί την εντολή join(count) µειώνει τον µετρητή count κατά 1 και το νήµα καταστρέφεται. count2 = 2. Για παράδειγµα. αλλά θα συνεχίσει να εκτελείται Οι εντολές fork και join βρίσκουν εφαρµογή τόσο στα συστήµατα πολυνηµάτωσης (multithreading systems) όσο και στα παράλληλα συστήµατα (parallel systems). goto L5. Με τις εντολές αυτές µπορεί να υλοποιηθεί οποιοσδήποτε γράφος εξάρτησης (dependence graph) χωρίς κύκλους. ∆ιαµαντάρας 5 . L3: b = x-1. L4: c = x+1. e = c*d. L6: join(count2). έστω ότι αρχικά count=3. Η διεργασία ωστόσο είναι µια. L2: a = 2*x. goto L7. L5: join(count1). • το τρίτο νήµα που θα εκτελέσει join(count)θα θέσει count=0 και ∆ΕΝ θα καταστραφεί. goto L5. goto L4. Γράφος Προήγησης L1 L2 a=2*x x=read() L3 b=x-1 L5 d=a+b join(count1) join(count2) L6 e=c*d L7 x=x+1 L4 c=x+1 Κώδικας µε fork-join count1 = 2. Ένας γράφος εξάρτησης έχει κόµβους τις εντολές ή τις υπορουτίνες που πρέπει να εκτελεστούν σ’ ένα πρόγραµµα και έχει ακµές που ενώνουν µεταξύ τους τις εντολές ή τις ρουτίνες που η µια εξαρτάται από το αποτέλεσµα της άλλης. • το δεύτερο νήµα που θα εκτελέσει join(count)θα θέσει count=1 και θα καταστραφεί. goto L6. η εντολή fork(Label) δηµιουργεί ένα νέο νήµα που ξεκινάει την εκτέλεσή του από την εντολή µε πινακίδα Label. fork(L3). end.ΑΤΕΙ Θεσσαλονίκης Τµήµα Πληροφορικής Λειτουργικά Συστήµατα Ι Ιούνιος 2007 Οι εντολές fork-join για την δηµιουργία και διαχείριση νηµάτων Οι εντολή fork για την δηµιουργία νηµάτων δεν πρέπει να συγχέεται µε την συνάρτηση συστήµατος fork για την δηµιουργία διεργασιών που είδαµε στην προηγούµενη ενότητα. goto L6. L4: c = x+1. τότε: • το πρώτο νήµα που θα εκτελέσει την εντολή join(count)θα θέσει count=2 και θα καταστραφεί. L1: x = read(). L5: d = a+b. Γράφοντας κώδικα µε fork-join Σε γενικές γραµµές η εντολή fork δηµιουργεί διακλαδώσεις ενώ η εντολή join χρησιµοποιείται όταν περισσότερες από µια ακµές συγκλίνουν σε ένα κόµβο. L3: b = x-1. Οποιοσδήποτε γράφος µπορεί να µετατραπεί σε παράλληλο κώδικα χρησιµοποιώντας fork-join αν ακολουθήσουµε τα εξής απλά βήµατα: Καθηγητής Κ. εκτός αν count==0 οπότε συνεχίζει. Συγκεκριµένα. L6: e = c*d. Η λογική είναι παρόµοια αλλά η εντoλή fork έχει να κάνει µε νήµατα. L2: a = 2*x. fork(L2). L7: x = x+1. d = a+b. L7: x = x+1. Η εντολή fork συνδυάζεται µε την εντολή join η οποία παίρνει ως όρισµα µια ακέραια µεταβλητή count.

αν ο κόµβος έχει τρια παιδιά µε πινακίδες L4. fork(L5). • Βήµα 3: Για κάθε κόµβο στον οποίο συγκλίνουν n ακµές γράψε join(count) αµέσως µετά την πινακίδα αλλά πριν από την εντολή που αντιστοιχεί σε αυτήν.ΑΤΕΙ Θεσσαλονίκης Τµήµα Πληροφορικής Λειτουργικά Συστήµατα Ι Ιούνιος 2007 • Βήµα 1: Σε κάθε εντολή – κόµβο του γράφου δώσε µια πινακίδα (Label) • Βήµα 2: Για κάθε κόµβο που διακλαδίζεται σε n παιδιά γράψε n–1 fork και 1 goto µετά από την εντολή που εκτελεί ο κόµβος. goto L7. γράψε fork(L4). A Γ Β ∆ Ε Ζ Η Θ Καθηγητής Κ. Πχ. και L7. Το count αρχικοποιείται στο n. Άσκηση Υλοποίησε τον παρακάτω γράφο εξάρτησης χρησιµοποιώντας τις εντολές fork-join. ∆ιαµαντάρας 6 . L5. • Βήµα 4 (προαιρετικό): σβήσε τα µη απαραίτητα goto.

Sign up to vote on this title
UsefulNot useful