Αυτό το άρθρο περιγράφει την υλοποίηση αγωγών στον πυρήνα του Unix. Ήμουν κάπως απογοητευμένος που ένα πρόσφατο άρθρο με τίτλο "
Για τι πράγμα μιλάμε?
Οι αγωγοί, «πιθανώς η πιο σημαντική εφεύρεση στο Unix», είναι ένα καθοριστικό χαρακτηριστικό της υποκείμενης φιλοσοφίας του Unix της σύνδεσης μικρών προγραμμάτων μεταξύ τους, καθώς και ένα οικείο σημάδι στη γραμμή εντολών:
$ echo hello | wc -c
6
Αυτή η λειτουργία εξαρτάται από την κλήση συστήματος που παρέχεται από τον πυρήνα pipe
, το οποίο περιγράφεται στις σελίδες τεκμηρίωσης
Οι αγωγοί παρέχουν ένα μονοκατευθυντικό κανάλι για επικοινωνία μεταξύ διεργασιών. Ο αγωγός έχει μια είσοδο (λήξη εγγραφής) και μια έξοδο (άκρο ανάγνωσης). Τα δεδομένα που εγγράφονται στην είσοδο του αγωγού μπορούν να διαβαστούν στην έξοδο.
Ο αγωγός δημιουργείται χρησιμοποιώντας την κλήση
pipe(2)
, το οποίο επιστρέφει δύο περιγραφείς αρχείων: ο ένας αναφέρεται στην είσοδο του αγωγού και ο δεύτερος στην έξοδο.
Η έξοδος ίχνους από την παραπάνω εντολή δείχνει τη δημιουργία του αγωγού και τη ροή δεδομένων μέσω αυτού από τη μια διεργασία στην άλλη:
$ strace -qf -e execve,pipe,dup2,read,write
sh -c 'echo hello | wc -c'
execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4]) = 0
[pid 2604795] dup2(4, 1) = 1
[pid 2604795] write(1, "hellon", 6) = 6
[pid 2604796] dup2(3, 0) = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2) = 2
Η γονική διαδικασία καλεί pipe()
για να λάβετε προσαρτημένες περιγραφές αρχείων. Μια θυγατρική διεργασία γράφει σε μια λαβή και μια άλλη διεργασία διαβάζει τα ίδια δεδομένα από μια άλλη λαβή. Το κέλυφος χρησιμοποιεί dup2 για να "μετονομάσει" τους περιγραφείς 3 και 4 για να ταιριάζει με το stdin και το stdout.
Χωρίς σωλήνες, το κέλυφος θα έπρεπε να γράψει το αποτέλεσμα μιας διαδικασίας σε ένα αρχείο και να το περάσει σε μια άλλη διεργασία για να διαβάσει τα δεδομένα από το αρχείο. Ως αποτέλεσμα, θα σπαταλούσαμε περισσότερους πόρους και χώρο στο δίσκο. Ωστόσο, οι αγωγοί είναι καλοί όχι μόνο επειδή σας επιτρέπουν να αποφύγετε τη χρήση προσωρινών αρχείων:
Εάν μια διεργασία προσπαθεί να διαβάσει από έναν κενό αγωγό τότε
read(2)
θα μπλοκάρει μέχρι να γίνουν διαθέσιμα τα δεδομένα. Εάν μια διεργασία προσπαθήσει να γράψει σε μια πλήρη διοχέτευση, τότεwrite(2)
θα μπλοκάρει μέχρι να διαβαστούν αρκετά δεδομένα από τη διοχέτευση για την εκτέλεση της εγγραφής.
Όπως η απαίτηση POSIX, αυτή είναι μια σημαντική ιδιότητα: εγγραφή στο pipeline μέχρι PIPE_BUF
byte (τουλάχιστον 512) πρέπει να είναι ατομικά, έτσι ώστε οι διεργασίες να μπορούν να επικοινωνούν μεταξύ τους μέσω του αγωγού με τρόπο που τα κανονικά αρχεία (τα οποία δεν παρέχουν τέτοιες εγγυήσεις) δεν μπορούν.
Όταν χρησιμοποιείτε ένα κανονικό αρχείο, μια διεργασία μπορεί να γράψει όλη την έξοδο της σε αυτό και να τη μεταβιβάσει σε άλλη διεργασία. Ή οι διεργασίες μπορούν να λειτουργούν σε μια εξαιρετικά παράλληλη λειτουργία, χρησιμοποιώντας έναν εξωτερικό μηχανισμό σηματοδότησης (όπως ένας σηματοφόρος) για να ειδοποιούν η μία την άλλη όταν έχει ολοκληρωθεί μια εγγραφή ή ανάγνωση. Οι μεταφορείς μας σώζουν από όλη αυτή την ταλαιπωρία.
Τι ψάχνουμε;
Θα το εξηγήσω με απλά λόγια, ώστε να είναι πιο εύκολο για εσάς να φανταστείτε πώς μπορεί να λειτουργήσει ένας μεταφορέας. Θα χρειαστεί να εκχωρήσετε ένα buffer και κάποια κατάσταση στη μνήμη. Θα χρειαστείτε συναρτήσεις για να προσθέσετε και να αφαιρέσετε δεδομένα από το buffer. Θα χρειαστείτε κάποιο μέσο για να καλέσετε συναρτήσεις κατά τις λειτουργίες ανάγνωσης και εγγραφής σε περιγραφείς αρχείων. Και θα χρειαστείτε κλειδαριές για να εφαρμόσετε την ειδική συμπεριφορά που περιγράφεται παραπάνω.
Τώρα είμαστε έτοιμοι να ανακρίνουμε τον πηγαίο κώδικα του πυρήνα κάτω από έντονο φως λαμπτήρα για να επιβεβαιώσουμε ή να διαψεύσουμε το ασαφές νοητικό μας μοντέλο. Αλλά να είστε πάντα προετοιμασμένοι για το απροσδόκητο.
Πού κοιτάμε;
Δεν ξέρω πού είναι το αντίγραφό μου από το διάσημο βιβλίο "
Η περιπλάνηση στα αρχεία του TUHS είναι σαν να επισκέπτεσαι ένα μουσείο. Μπορούμε να δούμε την κοινή μας ιστορία και σέβομαι την πολυετή προσπάθεια να ανακτήσω όλο αυτό το υλικό σπιθαμή προς σπιθαμή από παλιές κασέτες και εκτυπώσεις. Και έχω πλήρη επίγνωση αυτών των θραυσμάτων που εξακολουθούν να λείπουν.
Έχοντας ικανοποιήσει την περιέργειά μας για την αρχαία ιστορία των μεταφορέων, μπορούμε να δούμε τους σύγχρονους πυρήνες για σύγκριση.
παρεμπιπτόντως, pipe
είναι ο αριθμός κλήσης συστήματος 42 στον πίνακα sysent[]
. Σύμπτωση?
Παραδοσιακοί πυρήνες Unix (1970–1974)
Δεν βρήκα κανένα ίχνος pipe(2)
ούτε μέσα
Το TUHS αναφέρει ότι
Το Unix 1973rd Edition ήταν η τελευταία έκδοση με πυρήνα γραμμένο σε γλώσσα assembly, αλλά και η πρώτη έκδοση με pipelines. Κατά τη διάρκεια του XNUMX, έγιναν εργασίες για τη βελτίωση της τρίτης έκδοσης, ο πυρήνας ξαναγράφηκε σε C και έτσι εμφανίστηκε η τέταρτη έκδοση του Unix.
Ένας αναγνώστης βρήκε μια σάρωση ενός εγγράφου στο οποίο ο Doug McIlroy πρότεινε την ιδέα της «σύνδεσης προγραμμάτων σαν σωλήνας κήπου».
Στο βιβλίο του Brian Kernighan
Όταν κυκλοφόρησε το Unix, η γοητεία μου με τις κορουτίνες με οδήγησε να ζητήσω από τον συγγραφέα του λειτουργικού συστήματος, Ken Thompson, να επιτρέψει στα δεδομένα που είναι γραμμένα σε μια διεργασία να πηγαίνουν όχι μόνο στη συσκευή, αλλά και να εξάγουν σε άλλη διεργασία. Ο Κεν αποφάσισε ότι ήταν δυνατό. Ωστόσο, ως μινιμαλιστής, ήθελε κάθε λειτουργία του συστήματος να παίζει σημαντικό ρόλο. Είναι πραγματικά μεγάλο πλεονέκτημα η απευθείας εγγραφή μεταξύ διεργασιών έναντι της εγγραφής σε ένα ενδιάμεσο αρχείο; Μόνο όταν έκανα μια συγκεκριμένη πρόταση με το πιασάρικο όνομα "pipeline" και μια περιγραφή της σύνταξης για την αλληλεπίδραση μεταξύ των διαδικασιών που ο Ken τελικά αναφώνησε: "Θα το κάνω!"
Και έκανε. Ένα μοιραίο απόγευμα, ο Ken άλλαξε τον πυρήνα και το κέλυφος, διόρθωσε πολλά τυπικά προγράμματα για να τυποποιήσει τον τρόπο με τον οποίο αποδέχονταν την είσοδο (που θα μπορούσε να προέρχεται από μια διοχέτευση) και άλλαξε επίσης ονόματα αρχείων. Την επόμενη μέρα, οι αγωγοί άρχισαν να χρησιμοποιούνται ευρέως σε εφαρμογές. Μέχρι το τέλος της εβδομάδας, οι γραμματείς τα χρησιμοποιούσαν για να στείλουν έγγραφα από επεξεργαστές κειμένου στον εκτυπωτή. Λίγο αργότερα, ο Ken αντικατέστησε το αρχικό API και τη σύνταξη για την περιτύλιξη της χρήσης των αγωγών με πιο καθαρές συμβάσεις, οι οποίες χρησιμοποιούνται από τότε.
Δυστυχώς, ο πηγαίος κώδικας για την τρίτη έκδοση του πυρήνα Unix έχει χαθεί. Και παρόλο που έχουμε τον πηγαίο κώδικα του πυρήνα γραμμένο σε C
Έχουμε τεκμηρίωση κειμένου για pipe(2)
και από τις δύο εκδόσεις, ώστε να μπορείτε να ξεκινήσετε αναζητώντας την τεκμηρίωση pipe(2)
είναι γραμμένο σε γλώσσα assembly και επιστρέφει μόνο έναν περιγραφέα αρχείου, αλλά παρέχει ήδη την αναμενόμενη βασική λειτουργικότητα:
Κλήση συστήματος σωλήνας δημιουργεί έναν μηχανισμό εισόδου/εξόδου που ονομάζεται αγωγός. Ο επιστρεφόμενος περιγραφέας αρχείου μπορεί να χρησιμοποιηθεί για λειτουργίες ανάγνωσης και εγγραφής. Όταν κάτι εγγράφεται στη διοχέτευση, αποθηκεύονται έως και 504 byte δεδομένων, μετά από την οποία η διαδικασία εγγραφής αναστέλλεται. Κατά την ανάγνωση από τη διοχέτευση, τα δεδομένα προσωρινής αποθήκευσης αφαιρούνται.
Μέχρι το επόμενο έτος ο πυρήνας είχε ξαναγραφτεί σε C, και pipe(fildes)
»
Κλήση συστήματος σωλήνας δημιουργεί έναν μηχανισμό εισόδου/εξόδου που ονομάζεται αγωγός. Οι επιστρεφόμενοι περιγραφείς αρχείων μπορούν να χρησιμοποιηθούν σε λειτουργίες ανάγνωσης και εγγραφής. Όταν γράφεται κάτι στη διοχέτευση, χρησιμοποιείται η λαβή που επιστρέφεται στο r1 (αντίστοιχα fildes[1]), αποθηκεύεται στην προσωρινή μνήμη σε 4096 byte δεδομένων, μετά την οποία η διαδικασία εγγραφής αναστέλλεται. Κατά την ανάγνωση από τη διοχέτευση, η λαβή που επέστρεψε στο r0 (αντίστοιχα αρχεία[0]) λαμβάνει τα δεδομένα.
Υποτίθεται ότι μόλις οριστεί ένας αγωγός, δύο (ή περισσότερες) διεργασίες επικοινωνίας (δημιουργήθηκαν από επόμενες κλήσεις προς πιρούνι) θα μεταφέρει δεδομένα από τον αγωγό χρησιμοποιώντας κλήσεις ανάγνωση и γράφω.
Το κέλυφος έχει μια σύνταξη για τον καθορισμό μιας γραμμικής σειράς διεργασιών που συνδέονται με έναν αγωγό.
Οι κλήσεις για ανάγνωση από μια κενή διοχέτευση (που δεν περιέχει δεδομένα προσωρινής αποθήκευσης) που έχει μόνο ένα άκρο (όλες οι περιγραφές αρχείων εγγραφής είναι κλειστές) επιστρέφουν "τέλος αρχείου". Οι κλήσεις για γράψιμο σε παρόμοια κατάσταση αγνοούνται.
Πιο νωρίς
Έκτη έκδοση του Unix (1975)
Ας αρχίσουμε να διαβάζουμε τον πηγαίο κώδικα του Unix
Για πολλά χρόνια το βιβλίο Λιοντάρια ήταν το μόνο έγγραφο στον πυρήνα του Unix διαθέσιμο εκτός των εργαστηρίων Bell. Αν και η άδεια της έκτης έκδοσης επέτρεπε στους δασκάλους να χρησιμοποιήσουν τον πηγαίο κώδικα, η άδεια έβδομης έκδοσης απέκλεισε αυτή τη δυνατότητα, έτσι το βιβλίο διανεμήθηκε με τη μορφή παράνομων δακτυλογραφημένων αντιγράφων.
Σήμερα μπορείτε να αγοράσετε μια επανέκδοση του βιβλίου, το εξώφυλλο του οποίου δείχνει μαθητές σε μια μηχανή αντιγραφής. Και χάρη στον Warren Toomey (που ξεκίνησε το έργο TUHS) μπορείτε να κάνετε λήψη
Πριν από περισσότερα από 15 χρόνια, πληκτρολόγησα ένα αντίγραφο του πηγαίου κώδικα που δίνεται Λιοντάρια, γιατί δεν μου άρεσε η ποιότητα του αντιγράφου μου από άγνωστο αριθμό άλλων αντιγράφων. Το TUHS δεν υπήρχε ακόμα και δεν είχα πρόσβαση στις παλιές πηγές. Αλλά το 1988, βρήκα μια παλιά κασέτα 9 κομματιών που περιείχε ένα αντίγραφο ασφαλείας από έναν υπολογιστή PDP11. Ήταν δύσκολο να καταλάβουμε αν λειτουργούσε, αλλά υπήρχε ένα άθικτο δέντρο /usr/src/ στο οποίο τα περισσότερα αρχεία έφεραν την ετικέτα του έτους 1979, το οποίο ακόμη και τότε έμοιαζε αρχαίο. Ήταν η έβδομη έκδοση ή το παράγωγό της PWB, όπως πίστευα.
Πήρα το εύρημα ως βάση και επεξεργάστηκα με μη αυτόματο τρόπο τις πηγές στην έκτη έκδοση. Ορισμένοι από τον κώδικα παρέμειναν ο ίδιος, αλλά κάποιοι έπρεπε να τροποποιηθούν ελαφρώς, αλλάζοντας το σύγχρονο διακριτικό += στο ξεπερασμένο =+. Κάποια πράγματα απλά διαγράφηκαν και κάποια έπρεπε να ξαναγραφτούν εντελώς, αλλά όχι πάρα πολλά.
Και σήμερα μπορούμε να διαβάσουμε διαδικτυακά στο TUHS τον πηγαίο κώδικα της έκτης έκδοσης από
Παρεμπιπτόντως, με την πρώτη ματιά, το κύριο χαρακτηριστικό του C-code πριν από την περίοδο των Kernighan και Ritchie είναι συντομία. Δεν είναι συχνά που μπορώ να εισαγάγω κομμάτια κώδικα χωρίς εκτεταμένη επεξεργασία για να χωρέσω σε μια σχετικά στενή περιοχή προβολής στον ιστότοπό μου.
Νωρίς
/*
* Max allowable buffering per pipe.
* This is also the max size of the
* file created to implement the pipe.
* If this size is bigger than 4096,
* pipes will be implemented in LARG
* files, which is probably not good.
*/
#define PIPSIZ 4096
Το μέγεθος του buffer δεν έχει αλλάξει από την τέταρτη έκδοση. Αλλά εδώ βλέπουμε, χωρίς καμία δημόσια τεκμηρίωση, ότι οι αγωγοί κάποτε χρησιμοποιούσαν αρχεία ως εφεδρική αποθήκευση!
Όσον αφορά τα αρχεία LARG, αντιστοιχούν σε
Εδώ είναι η πραγματική κλήση συστήματος pipe
:
/*
* The sys-pipe entry.
* Allocate an inode on the root device.
* Allocate 2 file structures.
* Put it all together with flags.
*/
pipe()
{
register *ip, *rf, *wf;
int r;
ip = ialloc(rootdev);
if(ip == NULL)
return;
rf = falloc();
if(rf == NULL) {
iput(ip);
return;
}
r = u.u_ar0[R0];
wf = falloc();
if(wf == NULL) {
rf->f_count = 0;
u.u_ofile[r] = NULL;
iput(ip);
return;
}
u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
u.u_ar0[R0] = r; /* rf's fd */
wf->f_flag = FWRITE|FPIPE;
wf->f_inode = ip;
rf->f_flag = FREAD|FPIPE;
rf->f_inode = ip;
ip->i_count = 2;
ip->i_flag = IACC|IUPD;
ip->i_mode = IALLOC;
}
Το σχόλιο περιγράφει ξεκάθαρα τι συμβαίνει εδώ. Αλλά η κατανόηση του κώδικα δεν είναι τόσο εύκολη, εν μέρει λόγω του τρόπου "R0
и R1
μεταβιβάζονται οι παράμετροι κλήσης συστήματος και οι τιμές επιστροφής.
Ας προσπαθήσουμε με
pipe()
πρέπει να περάσει R0
и R1
επιστροφή αριθμών περιγραφής αρχείων για ανάγνωση και γραφή. falloc()
επιστρέφει δείκτη στη δομή του αρχείου, αλλά και "επιστρέφει" μέσω u.u_ar0[R0]
και ένα περιγραφικό αρχείου. Δηλαδή, ο κώδικας αποθηκεύεται r
περιγραφέας αρχείου για ανάγνωση και εκχωρεί έναν περιγραφέα αρχείου για εγγραφή απευθείας από u.u_ar0[R0]
μετά τη δεύτερη κλήση falloc()
.
Σημαία FPIPE
, που ορίσαμε κατά τη δημιουργία του pipeline, ελέγχει τη συμπεριφορά της συνάρτησης
/*
* common code for read and write calls:
* check permissions, set base, count, and offset,
* and switch out to readi, writei, or pipe code.
*/
rdwr(mode)
{
register *fp, m;
m = mode;
fp = getf(u.u_ar0[R0]);
/* … */
if(fp->f_flag&FPIPE) {
if(m==FREAD)
readp(fp); else
writep(fp);
}
/* … */
}
Στη συνέχεια η συνάρτηση readp()
в pipe.c
διαβάζει δεδομένα από τον αγωγό. Αλλά είναι καλύτερο να παρακολουθείτε την εφαρμογή ξεκινώντας από writep()
. Και πάλι, ο κώδικας έχει γίνει πιο περίπλοκος λόγω των συμβάσεων της μεταβίβασης ορισμάτων, αλλά ορισμένες λεπτομέρειες μπορούν να παραλειφθούν.
writep(fp)
{
register *rp, *ip, c;
rp = fp;
ip = rp->f_inode;
c = u.u_count;
loop:
/* If all done, return. */
plock(ip);
if(c == 0) {
prele(ip);
u.u_count = 0;
return;
}
/*
* If there are not both read and write sides of the
* pipe active, return error and signal too.
*/
if(ip->i_count < 2) {
prele(ip);
u.u_error = EPIPE;
psignal(u.u_procp, SIGPIPE);
return;
}
/*
* If the pipe is full, wait for reads to deplete
* and truncate it.
*/
if(ip->i_size1 == PIPSIZ) {
ip->i_mode =| IWRITE;
prele(ip);
sleep(ip+1, PPIPE);
goto loop;
}
/* Write what is possible and loop back. */
u.u_offset[0] = 0;
u.u_offset[1] = ip->i_size1;
u.u_count = min(c, PIPSIZ-u.u_offset[1]);
c =- u.u_count;
writei(ip);
prele(ip);
if(ip->i_mode&IREAD) {
ip->i_mode =& ~IREAD;
wakeup(ip+2);
}
goto loop;
}
Θέλουμε να γράψουμε bytes στην είσοδο του αγωγού u.u_count
. Πρώτα πρέπει να κλειδώσουμε το inode (δείτε παρακάτω plock
/prele
).
Στη συνέχεια ελέγχουμε τον μετρητή αναφοράς inode. Όσο και τα δύο άκρα του αγωγού παραμένουν ανοιχτά, ο μετρητής πρέπει να είναι ίσος με 2. Κρατάμε έναν σύνδεσμο (από rp->f_inode
), οπότε αν ο μετρητής είναι μικρότερος από 2, πρέπει να σημαίνει ότι η διαδικασία ανάγνωσης έχει κλείσει το άκρο του αγωγού. Με άλλα λόγια, προσπαθούμε να γράψουμε σε έναν κλειστό αγωγό, και αυτό είναι ένα σφάλμα. Κωδικός σφάλματος για πρώτη φορά EPIPE
και σήμα SIGPIPE
εμφανίστηκε στην έκτη έκδοση του Unix.
Αλλά ακόμα κι αν ο μεταφορέας είναι ανοιχτός, μπορεί να είναι γεμάτος. Σε αυτήν την περίπτωση, απελευθερώνουμε την κλειδαριά και πηγαίνουμε για ύπνο με την ελπίδα ότι μια άλλη διαδικασία θα διαβάσει από τον αγωγό και θα ελευθερώσει αρκετό χώρο σε αυτήν. Αφού ξυπνήσαμε, επιστρέφουμε στην αρχή, ξανακλείνουμε την κλειδαριά και ξεκινάμε έναν νέο κύκλο ηχογράφησης.
Εάν υπάρχει αρκετός ελεύθερος χώρος στον αγωγό, τότε γράφουμε δεδομένα σε αυτόν χρησιμοποιώντας i_size1
inode (αν ο αγωγός είναι κενός, μπορεί να είναι ίσος με 0) υποδεικνύει το τέλος των δεδομένων που περιέχει ήδη. Εάν υπάρχει αρκετός χώρος εγγραφής, μπορούμε να γεμίσουμε τον αγωγό από i_size1
να PIPESIZ
. Στη συνέχεια, απελευθερώνουμε την κλειδαριά και προσπαθούμε να ξυπνήσουμε οποιαδήποτε διαδικασία περιμένει να διαβάσει από το pipeline. Επιστρέφουμε στην αρχή για να δούμε αν μπορέσαμε να γράψουμε όσα byte χρειαζόμασταν. Εάν αποτύχει, τότε ξεκινάμε έναν νέο κύκλο εγγραφής.
Συνήθως η παράμετρος i_mode
Το inode χρησιμοποιείται για την αποθήκευση δικαιωμάτων r
, w
и x
. Αλλά στην περίπτωση των αγωγών, σηματοδοτούμε ότι κάποια διαδικασία περιμένει για εγγραφή ή ανάγνωση χρησιμοποιώντας bit IREAD
и IWRITE
αντίστοιχα. Η διαδικασία ορίζει τη σημαία και καλεί sleep()
, και αναμένεται ότι κάποια άλλη διαδικασία στο μέλλον θα προκαλέσει wakeup()
.
Η πραγματική μαγεία συμβαίνει μέσα sleep()
и wakeup()
. Εφαρμόζονται σε
/*
* Give up the processor till a wakeup occurs
* on chan, at which time the process
* enters the scheduling queue at priority pri.
* The most important effect of pri is that when
* pri<0 a signal cannot disturb the sleep;
* if pri>=0 signals will be processed.
* Callers of this routine must be prepared for
* premature return, and check that the reason for
* sleeping has gone away.
*/
sleep(chan, pri) /* … */
/*
* Wake up all processes sleeping on chan.
*/
wakeup(chan) /* … */
Η διαδικασία που προκαλεί sleep()
για ένα συγκεκριμένο κανάλι, μπορεί αργότερα να ξυπνήσει από μια άλλη διαδικασία, η οποία θα προκαλέσει wakeup()
για το ίδιο κανάλι. writep()
и readp()
συντονίζουν τις ενέργειές τους μέσω τέτοιων ζευγαρωμένων κλήσεων. σημειώστε ότι pipe.c
δίνει πάντα προτεραιότητα PPIPE
κατά την κλήση sleep()
, Αρα αυτο ειναι sleep()
μπορεί να διακοπεί από ένα σήμα.
Τώρα έχουμε τα πάντα για να κατανοήσουμε τη λειτουργία readp()
:
readp(fp)
int *fp;
{
register *rp, *ip;
rp = fp;
ip = rp->f_inode;
loop:
/* Very conservative locking. */
plock(ip);
/*
* If the head (read) has caught up with
* the tail (write), reset both to 0.
*/
if(rp->f_offset[1] == ip->i_size1) {
if(rp->f_offset[1] != 0) {
rp->f_offset[1] = 0;
ip->i_size1 = 0;
if(ip->i_mode&IWRITE) {
ip->i_mode =& ~IWRITE;
wakeup(ip+1);
}
}
/*
* If there are not both reader and
* writer active, return without
* satisfying read.
*/
prele(ip);
if(ip->i_count < 2)
return;
ip->i_mode =| IREAD;
sleep(ip+2, PPIPE);
goto loop;
}
/* Read and return */
u.u_offset[0] = 0;
u.u_offset[1] = rp->f_offset[1];
readi(ip);
rp->f_offset[1] = u.u_offset[1];
prele(ip);
}
Ίσως σας είναι πιο εύκολο να διαβάσετε αυτή τη λειτουργία από κάτω προς τα πάνω. Ο κλάδος "ανάγνωση και επιστροφή" χρησιμοποιείται συνήθως όταν υπάρχουν κάποια δεδομένα στη διοχέτευση. Σε αυτή την περίπτωση χρησιμοποιούμε f_offset
ανάγνωση και, στη συνέχεια, ενημερώστε την τιμή της αντίστοιχης μετατόπισης.
Σε επόμενες αναγνώσεις, ο αγωγός θα είναι άδειος εάν έχει φτάσει η μετατόπιση ανάγνωσης i_size1
στο inode. Επαναφέρουμε τη θέση στο 0 και προσπαθούμε να ξυπνήσουμε οποιαδήποτε διαδικασία θέλει να γράψει στο pipeline. Γνωρίζουμε ότι όταν ο μεταφορέας είναι γεμάτος, writep()
θα αποκοιμηθεί ip+1
. Και τώρα που ο αγωγός είναι άδειος, μπορούμε να τον αφυπνίσουμε για να συνεχίσει τον κύκλο εγγραφής του.
Αν δεν έχετε τίποτα να διαβάσετε, τότε readp()
μπορεί να ορίσει μια σημαία IREAD
και αποκοιμηθείτε ip+2
. Ξέρουμε τι θα τον ξυπνήσει writep()
, όταν γράφει κάποια δεδομένα στο pipeline.
Σχόλια για u
«Μπορούμε να τις αντιμετωπίσουμε σαν κανονικές συναρτήσεις I/O που παίρνουν ένα αρχείο, μια θέση, ένα buffer στη μνήμη και μετρούν τον αριθμό των byte για ανάγνωση ή εγγραφή.
/*
* Read the file corresponding to
* the inode pointed at by the argument.
* The actual read arguments are found
* in the variables:
* u_base core address for destination
* u_offset byte offset in file
* u_count number of bytes to read
* u_segflg read to kernel/user
*/
readi(aip)
struct inode *aip;
/* … */
/*
* Write the file corresponding to
* the inode pointed at by the argument.
* The actual write arguments are found
* in the variables:
* u_base core address for source
* u_offset byte offset in file
* u_count number of bytes to write
* u_segflg write to kernel/user
*/
writei(aip)
struct inode *aip;
/* … */
Όσο για το «συντηρητικό» μπλοκάρισμα, λοιπόν readp()
и writep()
μπλοκάρουν το inode μέχρι να ολοκληρώσουν την εργασία τους ή να λάβουν ένα αποτέλεσμα (δηλαδή να καλέσουν wakeup
). plock()
и prele()
εργαστείτε απλά: χρησιμοποιώντας ένα διαφορετικό σύνολο κλήσεων sleep
и wakeup
επιτρέψτε μας να ξυπνήσουμε οποιαδήποτε διαδικασία χρειάζεται το κλείδωμα που μόλις απελευθερώσαμε:
/*
* Lock a pipe.
* If its already locked, set the WANT bit and sleep.
*/
plock(ip)
int *ip;
{
register *rp;
rp = ip;
while(rp->i_flag&ILOCK) {
rp->i_flag =| IWANT;
sleep(rp, PPIPE);
}
rp->i_flag =| ILOCK;
}
/*
* Unlock a pipe.
* If WANT bit is on, wakeup.
* This routine is also used to unlock inodes in general.
*/
prele(ip)
int *ip;
{
register *rp;
rp = ip;
rp->i_flag =& ~ILOCK;
if(rp->i_flag&IWANT) {
rp->i_flag =& ~IWANT;
wakeup(rp);
}
}
Στην αρχή δεν μπορούσα να καταλάβω γιατί readp()
δεν προκαλεί prele(ip)
πριν την κλήση wakeup(ip+1)
. Το πρώτο πράγμα είναι writep()
προκαλεί στον κύκλο του, αυτό plock(ip)
, που οδηγεί σε αδιέξοδο αν readp()
δεν έχω αφαιρέσει ακόμα το μπλοκ μου, οπότε με κάποιο τρόπο ο κώδικας πρέπει να λειτουργεί σωστά. Αν κοιτάξεις wakeup()
, τότε γίνεται σαφές ότι επισημαίνει μόνο τη διαδικασία ύπνου ως έτοιμη για εκτέλεση, έτσι ώστε στο μέλλον sched()
πραγματικά το ξεκίνησε. Έτσι readp()
αιτίες wakeup()
, αφαιρεί την κλειδαριά, θέτει IREAD
και κλήσεις sleep(ip+2)
- όλα αυτά πριν writep()
ξαναρχίζει τον κύκλο.
Αυτό ολοκληρώνει την περιγραφή των μεταφορέων στην έκτη έκδοση. Απλός κώδικας, εκτεταμένες συνέπειες.
Xv6, ένας απλός πυρήνας που μοιάζει με Unix
Για να δημιουργήσετε τον πυρήνα
Ο κώδικας περιέχει μια σαφή και προσεκτική υλοποίηση pipealloc()
:
#define PIPESIZE 512
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
int
pipealloc(struct file **f0, struct file **f1)
{
struct pipe *p;
p = 0;
*f0 = *f1 = 0;
if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
goto bad;
if((p = (struct pipe*)kalloc()) == 0)
goto bad;
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
p->nread = 0;
initlock(&p->lock, "pipe");
(*f0)->type = FD_PIPE;
(*f0)->readable = 1;
(*f0)->writable = 0;
(*f0)->pipe = p;
(*f1)->type = FD_PIPE;
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
return 0;
bad:
if(p)
kfree((char*)p);
if(*f0)
fileclose(*f0);
if(*f1)
fileclose(*f1);
return -1;
}
pipealloc()
ορίζει την κατάσταση της υπόλοιπης υλοποίησης, η οποία περιλαμβάνει τις συναρτήσεις piperead()
, pipewrite()
и pipeclose()
. Πραγματική κλήση συστήματος sys_pipe
είναι ένα περιτύλιγμα που εφαρμόζεται σε
Linux 0.01
Μπορείτε να βρείτε τον πηγαίο κώδικα Linux 0.01. Θα είναι διδακτική η μελέτη της υλοποίησης αγωγών στο δικό του fs
/pipe.c
. Αυτό χρησιμοποιεί ένα inode για να αναπαραστήσει τη διοχέτευση, αλλά η ίδια η διοχέτευση είναι γραμμένη σε μοντέρνο C. Εάν έχετε επεξεργαστεί τον κώδικα της XNUMXης έκδοσης, δεν θα έχετε πρόβλημα εδώ. Αυτή είναι η εμφάνιση της συνάρτησης write_pipe()
:
int write_pipe(struct m_inode * inode, char * buf, int count)
{
char * b=buf;
wake_up(&inode->i_wait);
if (inode->i_count != 2) { /* no readers */
current->signal |= (1<<(SIGPIPE-1));
return -1;
}
while (count-->0) {
while (PIPE_FULL(*inode)) {
wake_up(&inode->i_wait);
if (inode->i_count != 2) {
current->signal |= (1<<(SIGPIPE-1));
return b-buf;
}
sleep_on(&inode->i_wait);
}
((char *)inode->i_size)[PIPE_HEAD(*inode)] =
get_fs_byte(b++);
INC_PIPE( PIPE_HEAD(*inode) );
wake_up(&inode->i_wait);
}
wake_up(&inode->i_wait);
return b-buf;
}
Χωρίς καν να κοιτάξετε τους ορισμούς της δομής, μπορείτε να καταλάβετε πώς χρησιμοποιείται ο αριθμός αναφοράς inode για να ελέγξετε εάν μια λειτουργία εγγραφής έχει ως αποτέλεσμα SIGPIPE
. Εκτός από την εργασία byte-by-byte, αυτή η λειτουργία είναι εύκολο να συγκριθεί με τις ιδέες που περιγράφονται παραπάνω. Ακόμα και η λογική sleep_on
/wake_up
δεν φαίνεται τόσο εξωγήινο.
Σύγχρονοι πυρήνες Linux, FreeBSD, NetBSD, OpenBSD
Έτρεξα γρήγορα μέσα από μερικούς σύγχρονους πυρήνες. Κανένα από αυτά δεν έχει πλέον εφαρμογή δίσκου (δεν αποτελεί έκπληξη). Το Linux έχει τη δική του υλοποίηση. Αν και οι τρεις σύγχρονοι πυρήνες BSD περιέχουν υλοποιήσεις που βασίζονται σε κώδικα που γράφτηκε από τον John Dyson, με τα χρόνια έχουν γίνει πολύ διαφορετικοί μεταξύ τους.
Να διαβασω fs
/pipe.c
(σε Linux) ή sys
/kern
/sys_pipe.c
(σε *BSD), χρειάζεται πραγματική αφοσίωση. Ο σημερινός κώδικας αφορά την απόδοση και την υποστήριξη λειτουργιών όπως διανυσματικά και ασύγχρονα I/O. Και οι λεπτομέρειες της εκχώρησης μνήμης, των κλειδαριών και της διαμόρφωσης πυρήνα ποικίλλουν πολύ. Δεν είναι αυτό που χρειάζονται τα κολέγια για ένα εισαγωγικό μάθημα λειτουργικών συστημάτων.
Τέλος πάντων, με ενδιέφερε να ανακαλύψω κάποια παλιά μοτίβα (όπως η δημιουργία SIGPIPE
και επιστροφή EPIPE
κατά την εγγραφή σε μια κλειστή διοχέτευση) σε όλους αυτούς τους διαφορετικούς σύγχρονους πυρήνες. Πιθανότατα δεν θα δω ποτέ υπολογιστή PDP-11 στην πραγματική ζωή, αλλά υπάρχουν ακόμα πολλά να μάθω από τον κώδικα που γράφτηκε χρόνια πριν γεννηθώ.
Ένα άρθρο που γράφτηκε από τον Divi Kapoor το 2011:
Πηγή: www.habr.com