Jak działają potoki w systemie Linux

25

Czytałem o tym, jak rury są implementowane w jądrze Linuksa i chciałem zweryfikować moje zrozumienie. Jeśli się mylę, zostanie wybrana odpowiedź z poprawnym wyjaśnieniem.

  • Linux ma VFS o nazwie pipefs, który jest zamontowany w jądrze (nie w przestrzeni użytkownika)
  • pipefs ma pojedynczy super blok i jest montowany we własnym rootie ( pipe:) obok/
  • pipefs nie może być oglądany bezpośrednio, w przeciwieństwie do większości systemów plików
  • Wejście do pipefs odbywa się poprzez pipe(2)syscall
  • pipe(2)Syscall wykorzystywane przez powłok do rur z |operatorem (ręcznie lub z dowolnego innego procesu) tworzy nowy plik pipefs która zachowuje się prawie jak normalny plik
  • Plik po lewej stronie operatora potoku został stdoutprzekierowany do pliku tymczasowego utworzonego w potokach
  • Plik po prawej stronie operatora potoku ma stdinustawiony plik na pipefs
  • pipefs jest przechowywany w pamięci i za pomocą jakiejś magii jądra nie powinien być stronicowany

Czy to wyjaśnienie, w jaki sposób rury (np. ls -la | less) Działają, jest właściwie poprawne?

Jednej rzeczy, której nie rozumiem, jest to, jak coś takiego jak bash ustawiłoby proces stdinlub stdoutzwracany przez deskryptor pliku pipe(2). Nie znalazłem jeszcze nic na ten temat.

Brandon Wamboldt
źródło
Zauważ, że mówisz o dwóch zasadniczo różnych warstwach rzeczy o tej samej nazwie. pipe()Wywołanie jądro wraz z maszyną, która je obsługuje ( pipefsitp) jest znacznie niższy poziom niż |operator zaoferował w swojej skorupie. Ten drugi jest zwykle implementowany przy użyciu tego pierwszego, ale nie musi tak być.
Greg Hewgill
Tak, szczególnie odnoszę się do operacji niższego poziomu, przy założeniu, że |operator po prostu wywołuje pipe(2)jak proces bash.
Brandon Wamboldt

Odpowiedzi:

19

Twoja dotychczasowa analiza jest ogólnie poprawna. Sposób, w jaki powłoka może ustawić standardowe wejście procesu na deskryptor potoku, może być (pseudokod):

pipe(p) // create a new pipe with two handles p[0] and p[1]
fork() // spawn a child process
    close(p[0]) // close the write end of the pipe in the child
    dup2(p[1], 0) // duplicate the pipe descriptor on top of fd 0 (stdin)
    close(p[1]) // close the other pipe descriptor
    exec() // run a new process with the new descriptors in place
Greg Hewgill
źródło
Dzięki! Ciekawe, dlaczego dup2potrzebne jest wywołanie, a nie można bezpośrednio przypisać deskryptora potoku do standardowego wejścia?
Brandon Wamboldt
3
Program wywołujący nie może wybrać wartości liczbowej deskryptora pliku, gdy jest on tworzony pipe(). dup2()Połączenie pozwala wywołujący skopiować deskryptor określonej wartości liczbowej (potrzebne, ponieważ 0, 1, 2 są standardowe wejście, wyjście, stderr). To jest odpowiednik jądra „przypisywania bezpośrednio do standardowego wejścia”. Zauważ, że globalna zmienna biblioteki wykonawczej C stdinjest a FILE *, która nie jest związana z jądrem (chociaż jest inicjowana w celu połączenia z deskryptorem 0).
Greg Hewgill
Świetna odpowiedź! Jestem trochę zagubiony w szczegółach. Zastanawiasz się tylko, dlaczego zamykasz (p [1]) przed uruchomieniem exec ()? Czy po powrocie dup2 p [1] nie wskazuje na fd 0? Następnie zamknij (p [1]) zamyka deskryptor pliku 0. W jaki sposób możemy odczytać ze standardowego procesu potomnego?
user1559897,
@ user1559897: dup2Połączenie się nie zmienia p[1]. Zamiast tego tworzy dwa uchwyty p[1]i 0wskazuje ten sam obiekt jądra (potok). Ponieważ proces potomny nie potrzebuje dwóch uchwytów stdin (i nie wiedziałby, co to jest numerowany uchwyt p[1]), p[1]został wcześniej zamknięty exec.
Greg Hewgill,
@GregHewgill Gotchu. Dzięki!
user1559897,