Πώς υλοποιούνται οι αγωγοί στο Unix

Πώς υλοποιούνται οι αγωγοί στο Unix
Αυτό το άρθρο περιγράφει την υλοποίηση αγωγών στον πυρήνα του Unix. Ήμουν κάπως απογοητευμένος που ένα πρόσφατο άρθρο με τίτλο "Πώς λειτουργούν οι αγωγοί στο Unix;"Αποδείχθηκε όχι σχετικά με την εσωτερική δομή. Ήμουν περίεργος και έσκαψα σε παλιές πηγές για να βρω την απάντηση.

Για τι πράγμα μιλάμε?

Οι αγωγοί, «πιθανώς η πιο σημαντική εφεύρεση στο Unix», είναι ένα καθοριστικό χαρακτηριστικό της υποκείμενης φιλοσοφίας του Unix της σύνδεσης μικρών προγραμμάτων μεταξύ τους, καθώς και ένα οικείο σημάδι στη γραμμή εντολών:

$ echo hello | wc -c
6

Αυτή η λειτουργία εξαρτάται από την κλήση συστήματος που παρέχεται από τον πυρήνα pipe, το οποίο περιγράφεται στις σελίδες τεκμηρίωσης σωλήνας (7) и σωλήνας (2):

Οι αγωγοί παρέχουν ένα μονοκατευθυντικό κανάλι για επικοινωνία μεταξύ διεργασιών. Ο αγωγός έχει μια είσοδο (λήξη εγγραφής) και μια έξοδο (άκρο ανάγνωσης). Τα δεδομένα που εγγράφονται στην είσοδο του αγωγού μπορούν να διαβαστούν στην έξοδο.

Ο αγωγός δημιουργείται χρησιμοποιώντας την κλήση 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. Θα χρειαστείτε κάποιο μέσο για να καλέσετε συναρτήσεις κατά τις λειτουργίες ανάγνωσης και εγγραφής σε περιγραφείς αρχείων. Και θα χρειαστείτε κλειδαριές για να εφαρμόσετε την ειδική συμπεριφορά που περιγράφεται παραπάνω.

Τώρα είμαστε έτοιμοι να ανακρίνουμε τον πηγαίο κώδικα του πυρήνα κάτω από έντονο φως λαμπτήρα για να επιβεβαιώσουμε ή να διαψεύσουμε το ασαφές νοητικό μας μοντέλο. Αλλά να είστε πάντα προετοιμασμένοι για το απροσδόκητο.

Πού κοιτάμε;

Δεν ξέρω πού είναι το αντίγραφό μου από το διάσημο βιβλίο "Το βιβλίο των λιονταριών"με τον πηγαίο κώδικα του Unix 6, αλλά χάρη σε Η Unix Heritage Society μπορείτε να κάνετε αναζήτηση στο διαδίκτυο στο πηγαίος κώδικας ακόμα και παλαιότερες εκδόσεις του Unix.

Η περιπλάνηση στα αρχεία του TUHS είναι σαν να επισκέπτεσαι ένα μουσείο. Μπορούμε να δούμε την κοινή μας ιστορία και σέβομαι την πολυετή προσπάθεια να ανακτήσω όλο αυτό το υλικό σπιθαμή προς σπιθαμή από παλιές κασέτες και εκτυπώσεις. Και έχω πλήρη επίγνωση αυτών των θραυσμάτων που εξακολουθούν να λείπουν.

Έχοντας ικανοποιήσει την περιέργειά μας για την αρχαία ιστορία των μεταφορέων, μπορούμε να δούμε τους σύγχρονους πυρήνες για σύγκριση.

παρεμπιπτόντως, pipe είναι ο αριθμός κλήσης συστήματος 42 στον πίνακα sysent[]. Σύμπτωση?

Παραδοσιακοί πυρήνες Unix (1970–1974)

Δεν βρήκα κανένα ίχνος pipe(2) ούτε μέσα PDP-7 Unix (Ιανουάριος 1970), ούτε μέσα πρώτη έκδοση του Unix (Νοέμβριος 1971), ούτε στον ημιτελή πηγαίο κώδικα δεύτερη έκδοση (Ιούνιος 1972).

Το TUHS αναφέρει ότι τρίτη έκδοση του Unix (Φεβρουάριος 1973) έγινε η πρώτη έκδοση με μεταφορείς:

Το Unix 1973rd Edition ήταν η τελευταία έκδοση με πυρήνα γραμμένο σε γλώσσα assembly, αλλά και η πρώτη έκδοση με pipelines. Κατά τη διάρκεια του XNUMX, έγιναν εργασίες για τη βελτίωση της τρίτης έκδοσης, ο πυρήνας ξαναγράφηκε σε C και έτσι εμφανίστηκε η τέταρτη έκδοση του Unix.

Ένας αναγνώστης βρήκε μια σάρωση ενός εγγράφου στο οποίο ο Doug McIlroy πρότεινε την ιδέα της «σύνδεσης προγραμμάτων σαν σωλήνας κήπου».

Πώς υλοποιούνται οι αγωγοί στο Unix
Στο βιβλίο του Brian KernighanUnix: A History and a Memoir", στην ιστορία της εμφάνισης των μεταφορέων, αναφέρεται επίσης αυτό το έγγραφο: "... κρεμόταν στον τοίχο στο γραφείο μου στα Bell Labs για 30 χρόνια." Εδώ συνέντευξη με τον McIlroy, και μια άλλη ιστορία από Το έργο του McIlroy, που γράφτηκε το 2014:

Όταν κυκλοφόρησε το Unix, η γοητεία μου με τις κορουτίνες με οδήγησε να ζητήσω από τον συγγραφέα του λειτουργικού συστήματος, Ken Thompson, να επιτρέψει στα δεδομένα που είναι γραμμένα σε μια διεργασία να πηγαίνουν όχι μόνο στη συσκευή, αλλά και να εξάγουν σε άλλη διεργασία. Ο Κεν αποφάσισε ότι ήταν δυνατό. Ωστόσο, ως μινιμαλιστής, ήθελε κάθε λειτουργία του συστήματος να παίζει σημαντικό ρόλο. Είναι πραγματικά μεγάλο πλεονέκτημα η απευθείας εγγραφή μεταξύ διεργασιών έναντι της εγγραφής σε ένα ενδιάμεσο αρχείο; Μόνο όταν έκανα μια συγκεκριμένη πρόταση με το πιασάρικο όνομα "pipeline" και μια περιγραφή της σύνταξης για την αλληλεπίδραση μεταξύ των διαδικασιών που ο Ken τελικά αναφώνησε: "Θα το κάνω!"

Και έκανε. Ένα μοιραίο απόγευμα, ο Ken άλλαξε τον πυρήνα και το κέλυφος, διόρθωσε πολλά τυπικά προγράμματα για να τυποποιήσει τον τρόπο με τον οποίο αποδέχονταν την είσοδο (που θα μπορούσε να προέρχεται από μια διοχέτευση) και άλλαξε επίσης ονόματα αρχείων. Την επόμενη μέρα, οι αγωγοί άρχισαν να χρησιμοποιούνται ευρέως σε εφαρμογές. Μέχρι το τέλος της εβδομάδας, οι γραμματείς τα χρησιμοποιούσαν για να στείλουν έγγραφα από επεξεργαστές κειμένου στον εκτυπωτή. Λίγο αργότερα, ο Ken αντικατέστησε το αρχικό API και τη σύνταξη για την περιτύλιξη της χρήσης των αγωγών με πιο καθαρές συμβάσεις, οι οποίες χρησιμοποιούνται από τότε.

Δυστυχώς, ο πηγαίος κώδικας για την τρίτη έκδοση του πυρήνα Unix έχει χαθεί. Και παρόλο που έχουμε τον πηγαίο κώδικα του πυρήνα γραμμένο σε C τέταρτη έκδοση, κυκλοφόρησε τον Νοέμβριο του 1973, αλλά κυκλοφόρησε αρκετούς μήνες πριν από την επίσημη κυκλοφορία και δεν περιέχει υλοποιήσεις αγωγών. Είναι κρίμα που ο πηγαίος κώδικας αυτής της θρυλικής συνάρτησης Unix χάθηκε, ίσως για πάντα.

Έχουμε τεκμηρίωση κειμένου για pipe(2) και από τις δύο εκδόσεις, ώστε να μπορείτε να ξεκινήσετε αναζητώντας την τεκμηρίωση τρίτη έκδοση (για ορισμένες λέξεις, με υπογράμμιση "χειροκίνητα", μια σειρά κυριολεκτικών ^H, ακολουθούμενη από μια υπογράμμιση!). Αυτό το πρωτό-pipe(2) είναι γραμμένο σε γλώσσα assembly και επιστρέφει μόνο έναν περιγραφέα αρχείου, αλλά παρέχει ήδη την αναμενόμενη βασική λειτουργικότητα:

Κλήση συστήματος σωλήνας δημιουργεί έναν μηχανισμό εισόδου/εξόδου που ονομάζεται αγωγός. Ο επιστρεφόμενος περιγραφέας αρχείου μπορεί να χρησιμοποιηθεί για λειτουργίες ανάγνωσης και εγγραφής. Όταν κάτι εγγράφεται στη διοχέτευση, αποθηκεύονται έως και 504 byte δεδομένων, μετά από την οποία η διαδικασία εγγραφής αναστέλλεται. Κατά την ανάγνωση από τη διοχέτευση, τα δεδομένα προσωρινής αποθήκευσης αφαιρούνται.

Μέχρι το επόμενο έτος ο πυρήνας είχε ξαναγραφτεί σε C, και σωλήνας(2) στην τέταρτη έκδοση απέκτησε τη μοντέρνα του εμφάνιση με το πρωτότυπο "pipe(fildes)»

Κλήση συστήματος σωλήνας δημιουργεί έναν μηχανισμό εισόδου/εξόδου που ονομάζεται αγωγός. Οι επιστρεφόμενοι περιγραφείς αρχείων μπορούν να χρησιμοποιηθούν σε λειτουργίες ανάγνωσης και εγγραφής. Όταν γράφεται κάτι στη διοχέτευση, χρησιμοποιείται η λαβή που επιστρέφεται στο r1 (αντίστοιχα fildes[1]), αποθηκεύεται στην προσωρινή μνήμη σε 4096 byte δεδομένων, μετά την οποία η διαδικασία εγγραφής αναστέλλεται. Κατά την ανάγνωση από τη διοχέτευση, η λαβή που επέστρεψε στο r0 (αντίστοιχα αρχεία[0]) λαμβάνει τα δεδομένα.

Υποτίθεται ότι μόλις οριστεί ένας αγωγός, δύο (ή περισσότερες) διεργασίες επικοινωνίας (δημιουργήθηκαν από επόμενες κλήσεις προς πιρούνι) θα μεταφέρει δεδομένα από τον αγωγό χρησιμοποιώντας κλήσεις ανάγνωση и γράφω.

Το κέλυφος έχει μια σύνταξη για τον καθορισμό μιας γραμμικής σειράς διεργασιών που συνδέονται με έναν αγωγό.

Οι κλήσεις για ανάγνωση από μια κενή διοχέτευση (που δεν περιέχει δεδομένα προσωρινής αποθήκευσης) που έχει μόνο ένα άκρο (όλες οι περιγραφές αρχείων εγγραφής είναι κλειστές) επιστρέφουν "τέλος αρχείου". Οι κλήσεις για γράψιμο σε παρόμοια κατάσταση αγνοούνται.

Πιο νωρίς διατηρημένη υλοποίηση αγωγού σχετίζεται στην πέμπτη έκδοση του Unix (Ιούνιος 1974), αλλά είναι σχεδόν πανομοιότυπο με αυτό που εμφανίστηκε στην επόμενη κυκλοφορία. Τα σχόλια μόλις προστέθηκαν, ώστε να μπορείτε να παραλείψετε την πέμπτη έκδοση.

Έκτη έκδοση του Unix (1975)

Ας αρχίσουμε να διαβάζουμε τον πηγαίο κώδικα του Unix έκτη έκδοση (Μάιος 1975). Σε μεγάλο βαθμό χάρη σε Λιοντάρια είναι πολύ πιο εύκολο να βρεθεί από τις πηγές προηγούμενων εκδόσεων:

Για πολλά χρόνια το βιβλίο Λιοντάρια ήταν το μόνο έγγραφο στον πυρήνα του Unix διαθέσιμο εκτός των εργαστηρίων Bell. Αν και η άδεια της έκτης έκδοσης επέτρεπε στους δασκάλους να χρησιμοποιήσουν τον πηγαίο κώδικα, η άδεια έβδομης έκδοσης απέκλεισε αυτή τη δυνατότητα, έτσι το βιβλίο διανεμήθηκε με τη μορφή παράνομων δακτυλογραφημένων αντιγράφων.

Σήμερα μπορείτε να αγοράσετε μια επανέκδοση του βιβλίου, το εξώφυλλο του οποίου δείχνει μαθητές σε μια μηχανή αντιγραφής. Και χάρη στον Warren Toomey (που ξεκίνησε το έργο TUHS) μπορείτε να κάνετε λήψη Αρχείο PDF με πηγαίο κώδικα για την έκτη έκδοση. Θέλω να σας δώσω μια ιδέα για το πόση προσπάθεια καταβλήθηκε για τη δημιουργία του αρχείου:

Πριν από περισσότερα από 15 χρόνια, πληκτρολόγησα ένα αντίγραφο του πηγαίου κώδικα που δίνεται Λιοντάρια, γιατί δεν μου άρεσε η ποιότητα του αντιγράφου μου από άγνωστο αριθμό άλλων αντιγράφων. Το TUHS δεν υπήρχε ακόμα και δεν είχα πρόσβαση στις παλιές πηγές. Αλλά το 1988, βρήκα μια παλιά κασέτα 9 κομματιών που περιείχε ένα αντίγραφο ασφαλείας από έναν υπολογιστή PDP11. Ήταν δύσκολο να καταλάβουμε αν λειτουργούσε, αλλά υπήρχε ένα άθικτο δέντρο /usr/src/ στο οποίο τα περισσότερα αρχεία έφεραν την ετικέτα του έτους 1979, το οποίο ακόμη και τότε έμοιαζε αρχαίο. Ήταν η έβδομη έκδοση ή το παράγωγό της PWB, όπως πίστευα.

Πήρα το εύρημα ως βάση και επεξεργάστηκα με μη αυτόματο τρόπο τις πηγές στην έκτη έκδοση. Ορισμένοι από τον κώδικα παρέμειναν ο ίδιος, αλλά κάποιοι έπρεπε να τροποποιηθούν ελαφρώς, αλλάζοντας το σύγχρονο διακριτικό += στο ξεπερασμένο =+. Κάποια πράγματα απλά διαγράφηκαν και κάποια έπρεπε να ξαναγραφτούν εντελώς, αλλά όχι πάρα πολλά.

Και σήμερα μπορούμε να διαβάσουμε διαδικτυακά στο TUHS τον πηγαίο κώδικα της έκτης έκδοσης από αρχείο, στο οποίο είχε χέρι ο Ντένις Ρίτσι.

Παρεμπιπτόντως, με την πρώτη ματιά, το κύριο χαρακτηριστικό του C-code πριν από την περίοδο των Kernighan και Ritchie είναι συντομία. Δεν είναι συχνά που μπορώ να εισαγάγω κομμάτια κώδικα χωρίς εκτεταμένη επεξεργασία για να χωρέσω σε μια σχετικά στενή περιοχή προβολής στον ιστότοπό μου.

Νωρίς /usr/sys/ken/pipe.c υπάρχει ένα επεξηγηματικό σχόλιο (και ναι, υπάρχει και άλλο /usr/sys/dmr):

/*
 * 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, αντιστοιχούν σε σημαία inode LARG, το οποίο χρησιμοποιείται από τον "μεγάλο αλγόριθμο διευθύνσεων" για την επεξεργασία έμμεσα μπλοκ για την υποστήριξη μεγαλύτερων συστημάτων αρχείων. Εφόσον ο Ken είπε ότι είναι καλύτερα να μην τα χρησιμοποιήσω, θα δεχτώ ευχαρίστως τον λόγο του.

Εδώ είναι η πραγματική κλήση συστήματος 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;
}

Το σχόλιο περιγράφει ξεκάθαρα τι συμβαίνει εδώ. Αλλά η κατανόηση του κώδικα δεν είναι τόσο εύκολη, εν μέρει λόγω του τρόπου "struct χρήστη u» και εγγραφεί R0 и R1 μεταβιβάζονται οι παράμετροι κλήσης συστήματος και οι τιμές επιστροφής.

Ας προσπαθήσουμε με ialloc() βάλτε στο δίσκο inode (λαβή ευρετηρίου), και με τη βοήθεια falloc() - τοποθετήστε δύο στη μνήμη αρχείο. Εάν όλα πάνε καλά, θα ορίσουμε σημαίες για να προσδιορίσουμε αυτά τα αρχεία ως δύο άκρα της διοχέτευσης, θα τα δείξουμε στον ίδιο inode (του οποίου ο αριθμός αναφοράς θα οριστεί σε 2) και θα επισημάνουμε το inode ως τροποποιημένο και σε χρήση. Δώστε προσοχή στα αιτήματα προς βάζω() σε διαδρομές σφαλμάτων για μείωση του αριθμού αναφορών στο νέο inode.

pipe() πρέπει να περάσει R0 и R1 επιστροφή αριθμών περιγραφής αρχείων για ανάγνωση και γραφή. falloc() επιστρέφει δείκτη στη δομή του αρχείου, αλλά και "επιστρέφει" μέσω u.u_ar0[R0] και ένα περιγραφικό αρχείου. Δηλαδή, ο κώδικας αποθηκεύεται r περιγραφέας αρχείου για ανάγνωση και εκχωρεί έναν περιγραφέα αρχείου για εγγραφή απευθείας από u.u_ar0[R0] μετά τη δεύτερη κλήση falloc().

Σημαία FPIPE, που ορίσαμε κατά τη δημιουργία του pipeline, ελέγχει τη συμπεριφορά της συνάρτησης rdwr() στο sys2.cκλήση συγκεκριμένων ρουτινών εισόδου/εξόδου:

/*
 * 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.

Αλλά ακόμα κι αν ο μεταφορέας είναι ανοιχτός, μπορεί να είναι γεμάτος. Σε αυτήν την περίπτωση, απελευθερώνουμε την κλειδαριά και πηγαίνουμε για ύπνο με την ελπίδα ότι μια άλλη διαδικασία θα διαβάσει από τον αγωγό και θα ελευθερώσει αρκετό χώρο σε αυτήν. Αφού ξυπνήσαμε, επιστρέφουμε στην αρχή, ξανακλείνουμε την κλειδαριά και ξεκινάμε έναν νέο κύκλο ηχογράφησης.

Εάν υπάρχει αρκετός ελεύθερος χώρος στον αγωγό, τότε γράφουμε δεδομένα σε αυτόν χρησιμοποιώντας writei(). Παράμετρος i_size1 inode (αν ο αγωγός είναι κενός, μπορεί να είναι ίσος με 0) υποδεικνύει το τέλος των δεδομένων που περιέχει ήδη. Εάν υπάρχει αρκετός χώρος εγγραφής, μπορούμε να γεμίσουμε τον αγωγό από i_size1 να PIPESIZ. Στη συνέχεια, απελευθερώνουμε την κλειδαριά και προσπαθούμε να ξυπνήσουμε οποιαδήποτε διαδικασία περιμένει να διαβάσει από το pipeline. Επιστρέφουμε στην αρχή για να δούμε αν μπορέσαμε να γράψουμε όσα byte χρειαζόμασταν. Εάν αποτύχει, τότε ξεκινάμε έναν νέο κύκλο εγγραφής.

Συνήθως η παράμετρος i_mode Το inode χρησιμοποιείται για την αποθήκευση δικαιωμάτων r, w и x. Αλλά στην περίπτωση των αγωγών, σηματοδοτούμε ότι κάποια διαδικασία περιμένει για εγγραφή ή ανάγνωση χρησιμοποιώντας bit IREAD и IWRITE αντίστοιχα. Η διαδικασία ορίζει τη σημαία και καλεί sleep(), και αναμένεται ότι κάποια άλλη διαδικασία στο μέλλον θα προκαλέσει wakeup().

Η πραγματική μαγεία συμβαίνει μέσα sleep() и wakeup(). Εφαρμόζονται σε slp.c, η πηγή του περίφημου σχολίου «Δεν αναμένεται να το καταλάβεις». Ευτυχώς, δεν χρειάζεται να κατανοήσουμε τον κώδικα, απλά δείτε μερικά σχόλια:

/*
 * 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);
}

Ίσως σας είναι πιο εύκολο να διαβάσετε αυτή τη λειτουργία από κάτω προς τα πάνω. Ο κλάδος "ανάγνωση και επιστροφή" χρησιμοποιείται συνήθως όταν υπάρχουν κάποια δεδομένα στη διοχέτευση. Σε αυτή την περίπτωση χρησιμοποιούμε Readi () διαβάζουμε όσα δεδομένα είναι διαθέσιμα ξεκινώντας από το τρέχον f_offset ανάγνωση και, στη συνέχεια, ενημερώστε την τιμή της αντίστοιχης μετατόπισης.

Σε επόμενες αναγνώσεις, ο αγωγός θα είναι άδειος εάν έχει φτάσει η μετατόπιση ανάγνωσης i_size1 στο inode. Επαναφέρουμε τη θέση στο 0 και προσπαθούμε να ξυπνήσουμε οποιαδήποτε διαδικασία θέλει να γράψει στο pipeline. Γνωρίζουμε ότι όταν ο μεταφορέας είναι γεμάτος, writep() θα αποκοιμηθεί ip+1. Και τώρα που ο αγωγός είναι άδειος, μπορούμε να τον αφυπνίσουμε για να συνεχίσει τον κύκλο εγγραφής του.

Αν δεν έχετε τίποτα να διαβάσετε, τότε readp() μπορεί να ορίσει μια σημαία IREAD και αποκοιμηθείτε ip+2. Ξέρουμε τι θα τον ξυπνήσει writep(), όταν γράφει κάποια δεδομένα στο pipeline.

Σχόλια για readi() και writei() θα σας βοηθήσει να καταλάβετε ότι αντί να περάσετε τις παραμέτρους μέσω του "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() ξαναρχίζει τον κύκλο.

Αυτό ολοκληρώνει την περιγραφή των μεταφορέων στην έκτη έκδοση. Απλός κώδικας, εκτεταμένες συνέπειες.

Έβδομη έκδοση του Unix (Ιανουάριος 1979) ήταν μια νέα σημαντική έκδοση (τέσσερα χρόνια αργότερα) που εισήγαγε πολλές νέες εφαρμογές και χαρακτηριστικά πυρήνα. Υπέστη επίσης σημαντικές αλλαγές σε σχέση με τη χρήση χύτευσης τύπου, ενώσεων και δακτυλογραφημένων δεικτών σε κατασκευές. Ωστόσο κωδικός μεταφορέα πρακτικά αμετάβλητο. Μπορούμε να παραλείψουμε αυτή την έκδοση.

Xv6, ένας απλός πυρήνας που μοιάζει με Unix

Για να δημιουργήσετε τον πυρήνα Xv6 επηρεασμένο από την έκτη έκδοση του Unix, αλλά είναι γραμμένο σε σύγχρονη C για να τρέχει σε επεξεργαστές x86. Ο κώδικας είναι ευανάγνωστος και κατανοητός. Επιπλέον, σε αντίθεση με τις πηγές Unix με TUHS, μπορείτε να το μεταγλωττίσετε, να το τροποποιήσετε και να το εκτελέσετε σε κάτι διαφορετικό από ένα PDP 11/70. Επομένως, αυτός ο πυρήνας χρησιμοποιείται ευρέως στα πανεπιστήμια ως εκπαιδευτικό υλικό για λειτουργικά συστήματα. Πηγές βρίσκονται στο Github.

Ο κώδικας περιέχει μια σαφή και προσεκτική υλοποίηση σωλήνας.γ, που υποστηρίζεται από ένα buffer στη μνήμη αντί για ένα inode στο δίσκο. Εδώ δίνω μόνο τον ορισμό του "δομικού αγωγού" και τη λειτουργία 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 είναι ένα περιτύλιγμα που εφαρμόζεται σε sysfile.c. Συνιστώ να διαβάσετε ολόκληρο τον κώδικά του. Η πολυπλοκότητα είναι στο επίπεδο του πηγαίου κώδικα της έκτης έκδοσης, αλλά είναι πολύ πιο εύκολο και πιο ευχάριστο στην ανάγνωση.

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:Η υλοποίηση του πυρήνα Linux των σωλήνων και των FIFO" παρέχει μια επισκόπηση του τρόπου με τον οποίο λειτουργούν (ακόμα) οι αγωγοί στο Linux. ΕΝΑ πρόσφατη δέσμευση στο Linux απεικονίζει ένα μοντέλο αλληλεπίδρασης αγωγών, του οποίου οι δυνατότητες υπερβαίνουν αυτές των προσωρινών αρχείων. και δείχνει επίσης πόσο μακριά έχουν φτάσει οι αγωγοί από το "πολύ συντηρητικό κλείδωμα" του πυρήνα Unix της έκτης έκδοσης.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο