ความแตกต่างระหว่าง "การเปลี่ยนเส้นทาง" และ "ท่อ" คืออะไร?


204

คำถามนี้อาจฟังดูโง่เล็กน้อย แต่ฉันไม่เห็นความแตกต่างระหว่างการเปลี่ยนเส้นทางและไพพ์

การเปลี่ยนเส้นทางที่ใช้ในการเปลี่ยนเส้นทาง stdout / stdin / stderr ls > log.txtเช่น

ท่อที่ใช้ในการให้การส่งออกของคำสั่งเป็น input ls | grep file.txtเพื่อคำสั่งอื่นเช่น

แต่ทำไมมีผู้ให้บริการสองรายสำหรับสิ่งเดียวกัน

ทำไมไม่เพียงแค่เขียนls > grepเพื่อส่งผ่านผลลัพธ์การเปลี่ยนเส้นทางแบบนี้ไม่ใช่หรือ ฉันกำลังคิดถึงอะไร

คำตอบ:


223

ไปป์นั้นใช้เพื่อส่งผ่านเอาต์พุตไปยังโปรแกรมหรือยูทิลิตี้อื่น

เปลี่ยนเส้นทางถูกใช้ในการส่งออกไปทั้งไฟล์หรือสตรีม

ตัวอย่าง: thing1 > thing2vsthing1 | thing2

thing1 > thing2

  1. เชลล์ของคุณจะรันโปรแกรมชื่อ thing1
  2. ทุกอย่างที่เอาท์พุทจะถูกวางไว้ในไฟล์ที่เรียกว่าthing1 thing2(หมายเหตุ - ถ้าthing2มีอยู่มันจะถูกเขียนทับ)

หากคุณต้องการส่งผ่านผลลัพธ์จากโปรแกรมthing1ไปยังโปรแกรมที่เรียกว่าthing2คุณสามารถทำสิ่งต่อไปนี้:

thing1 > temp_file && thing2 < temp_file

ซึ่งจะ

  1. เรียกใช้โปรแกรมชื่อ thing1
  2. บันทึกผลลัพธ์ลงในไฟล์ชื่อ temp_file
  3. เรียกใช้โปรแกรมชื่อthing2ทำท่าว่าบุคคลที่แป้นพิมพ์พิมพ์เนื้อหาของtemp_fileเป็นอินพุต

อย่างไรก็ตามนั่นเป็น clunky ดังนั้นพวกเขาจึงทำให้ท่อเป็นวิธีที่ง่ายกว่าในการทำเช่นนั้น thing1 | thing2ทำสิ่งเดียวกันthing1 > temp_file && thing2 < temp_file

แก้ไขเพื่อให้รายละเอียดเพิ่มเติมเกี่ยวกับคำถามในความคิดเห็น:

หาก>พยายามเป็นทั้ง "ส่งผ่านไปยังโปรแกรม" และ "เขียนไปยังไฟล์" อาจทำให้เกิดปัญหาทั้งสองทิศทาง

ตัวอย่างแรก:คุณกำลังพยายามเขียนลงไฟล์ มีไฟล์ชื่อที่คุณต้องการเขียนทับอยู่แล้ว อย่างไรก็ตามไฟล์นั้นสามารถเรียกใช้งานได้ สันนิษฐานว่ามันจะพยายามรันไฟล์นี้ผ่านอินพุต คุณต้องทำอะไรเช่นเขียนผลลัพธ์ไปยังชื่อไฟล์ใหม่แล้วเปลี่ยนชื่อไฟล์

ตัวอย่างที่สอง: ดังที่ Florian Diesch ชี้ให้เห็นถ้ามีคำสั่งอื่นในระบบที่ใช้ชื่อเดียวกัน (นั่นคือในเส้นทางดำเนินการ) หากคุณต้องการสร้างไฟล์ด้วยชื่อนั้นในโฟลเดอร์ปัจจุบันของคุณคุณจะต้องติดขัด

ประการที่สาม:หากคุณพิมพ์คำสั่งผิดจะไม่เตือนคุณว่าคำสั่งนั้นไม่มีอยู่จริง ตอนนี้ถ้าคุณพิมพ์มันจะบอกคุณls | gerp log.txt bash: gerp: command not foundหาก>หมายถึงทั้งคู่มันจะสร้างไฟล์ใหม่ให้คุณ (เตือนว่าไม่รู้ว่าต้องทำอะไรlog.txt)


ขอขอบคุณ. คุณพูดถึงthing1 > temp_file && thing2 < temp_fileการทำท่อได้ง่ายขึ้น แต่ทำไมไม่ใช้ตัว>ดำเนินการอีกครั้งเพื่อทำสิ่งนี้เช่นthing1 > thing2สำหรับคำสั่งthing1และthing2? ทำไมผู้ประกอบการพิเศษ|?
John Threepwood

1
"เอาท์พุทและเขียนลงในไฟล์" เป็นการกระทำที่แตกต่างจาก "นำเอาท์พุทแล้วส่งไปยังโปรแกรมอื่น" ฉันจะแก้ไขความคิดเพิ่มเติมให้เป็นคำตอบของฉัน ...
David Oneill

1
@JohnThreepwood พวกเขามีความหมายต่างกัน ถ้าฉันต้องการเปลี่ยนเส้นทางบางสิ่งไปยังไฟล์ชื่อlessเช่นนั้น thing | lessและ thing > lessแตกต่างอย่างสมบูรณ์แบบเพราะพวกเขาทำสิ่งต่าง ๆ สิ่งที่คุณเสนอจะสร้างความกำกวม
Darkhogg

มันถูกต้องหรือไม่ที่จะบอกว่า "thing1> temp_file" เป็นเพียงน้ำตาลในประโยคสำหรับ "thing1 | tee temp_file" ตั้งแต่หาข้อมูลเกี่ยวกับทีฉันแทบไม่เคยใช้การเปลี่ยนเส้นทาง
Sridhar Sarnobat

2
@ Sridhar-Sarnobat ไม่teeคำสั่งทำสิ่งที่แตกต่าง teeเขียนเอาต์พุตไปยังทั้งหน้าจอ ( stdout) และไฟล์ การเปลี่ยนเส้นทางทำเฉพาะไฟล์
David Oneill

22

หากความหมายของfoo > barมันขึ้นอยู่กับว่ามีคำสั่งชื่อbarที่จะทำให้การใช้การเปลี่ยนเส้นทางนั้นยากและผิดพลาดมากขึ้นหรือไม่: ทุกครั้งที่ฉันต้องการเปลี่ยนเส้นทางไปยังไฟล์ฉันต้องตรวจสอบก่อนว่ามีคำสั่งชื่อเหมือนไฟล์ปลายทางของฉัน


นี่จะเป็นปัญหาเฉพาะเมื่อคุณเขียนถึงbarในไดเรกทอรีที่เป็นส่วนหนึ่งของ$PATHตัวแปร env ของคุณ หากคุณอยู่ในสิ่งที่ต้องการ / bin ดังนั้นอาจเป็นปัญหาได้ แต่ถึงอย่างนั้นก็barจะต้องมีชุดสิทธิ์อนุญาตการใช้งานเพื่อให้เชลล์ตรวจสอบไม่เพียง แต่สำหรับการค้นหาไฟล์ปฏิบัติการbarแต่จริง ๆ แล้วสามารถเรียกใช้งานได้ และหากมีข้อกังวลเกี่ยวกับการเขียนทับไฟล์ที่มีอยู่nocloberตัวเลือกเชลล์ควรป้องกันการเขียนทับไฟล์ที่มีอยู่ในการเปลี่ยนเส้นทาง
Sergiy Kolodyazhnyy

13

จากคู่มือการบริหารระบบ Unix และ Linux:

การเปลี่ยนเส้นทาง

เปลือกตีความสัญลักษณ์ <,> และ >> เป็นคำแนะนำในการคำนวณหาเส้นทางใหม่ของคำสั่งที่ป้อนเข้าหรือส่งออกไปยังหรือจากไฟล์

ท่อ

ในการเชื่อมต่อ STDOUT ของคำสั่งเดียวกับ STDIN ของอีกคำสั่งให้ใช้ | สัญลักษณ์ที่รู้จักกันทั่วไปว่าเป็นท่อ

ดังนั้นการตีความของฉันคือ: ถ้ามันเป็นคำสั่งให้ใช้ท่อ หากคุณกำลังส่งออกไปยังหรือจากไฟล์ใช้การเปลี่ยนเส้นทาง


12

มีความแตกต่างที่สำคัญระหว่างผู้ให้บริการทั้งสอง:

  1. ls > log.txt -> คำสั่งนี้ส่งเอาต์พุตไปยังไฟล์ log.txt

  2. ls | grep file.txt-> คำสั่งนี้ส่งเอาต์พุตของคำสั่ง ls ไปยัง grep ผ่านการใช้ไปป์ ( |) และคำสั่ง grep ค้นหา file.txt ในอินพุตที่ให้ไว้โดยคำสั่งก่อนหน้า

หากคุณต้องทำงานเดียวกันโดยใช้สถานการณ์แรกมันจะเป็น:

ls > log.txt; grep 'file.txt' log.txt

ดังนั้นไพพ์ (พร้อม|) จะถูกใช้เพื่อส่งเอาต์พุตไปยังคำสั่งอื่นในขณะที่การรีไดเร็กต์ (พร้อม>) ถูกใช้เพื่อเปลี่ยนทิศทางเอาต์พุตไปยังไฟล์บางไฟล์


3

มีความแตกต่างระหว่างประโยคใหญ่ syntax:

  1. การเปลี่ยนเส้นทางคืออาร์กิวเมนต์ของโปรแกรม
  2. ไพพ์แยกสองคำสั่ง

cat [<infile] [>outfile]คุณสามารถคิดว่าการเปลี่ยนเส้นทางเช่นนี้ นี้มีความหมายเพื่อไม่สำคัญ: เป็นเช่นเดียวกับcat <infile >outfile cat >outfile <infileคุณสามารถผสมการเปลี่ยนเส้นทางกับข้อโต้แย้งอื่น ๆ : cat >outfile <infile -bและcat <infile -b >outfileทั้งคู่ก็ใช้ได้ดี นอกจากนี้คุณสามารถสตริงรวมกันมากกว่าหนึ่ง input หรือ output cat >outfile1 >outfile2 <infile1 <infile2(ปัจจัยการผลิตจะอ่านตามลำดับและผลผลิตทั้งหมดจะถูกเขียนไปยังไฟล์ที่ส่งออกในแต่ละ): เป้าหมายหรือแหล่งที่มาของการเปลี่ยนเส้นทางอาจเป็นชื่อไฟล์หรือชื่อของสตรีม (เช่น & 1 อย่างน้อยก็เป็นทุบตี)

แต่ท่อแยกคำสั่งเดียวจากคำสั่งอื่นโดยสิ้นเชิงคุณไม่สามารถผสมคำสั่งเหล่านั้นเข้ากับอาร์กิวเมนต์:

[command1] | [command2]

ไพพ์ใช้ทุกอย่างที่เขียนไปยังเอาต์พุตมาตรฐานจาก command1 และส่งไปยังอินพุตมาตรฐานของ command2

คุณยังสามารถรวมการวางท่อและการเปลี่ยนเส้นทาง ตัวอย่างเช่น:

cat <infile >outfile | cat <infile2 >outfile2

เป็นครั้งแรกที่catจะอ่านบรรทัดจาก INFILE แล้วพร้อมกันเขียนแต่ละบรรทัดจะ outfile catและส่งไปยังที่สอง

ในครั้งที่สองcatอินพุตมาตรฐานจะอ่านจากไพพ์ (เนื้อหาของ infile) ก่อนจากนั้นอ่านจาก infile2 เขียนแต่ละบรรทัดไปยัง outfile2 หลังจากรันสิ่งนี้ outfile จะเป็นสำเนาของ infile และ outfile2 จะมี infile ตามด้วย infile2

สุดท้ายคุณทำสิ่งที่คล้ายกับตัวอย่างของคุณโดยใช้การเปลี่ยนเส้นทาง "here string" (เฉพาะตระกูลทุบตีเท่านั้น) และ backticks:

grep blah <<<`ls`

จะให้ผลลัพธ์เช่นเดียวกับ

ls | grep blah

แต่ฉันคิดว่าเวอร์ชันการเปลี่ยนเส้นทางจะอ่านผลลัพธ์ทั้งหมดของ ls ลงในบัฟเฟอร์ (ในหน่วยความจำ) แล้วจึงป้อนบัฟเฟอร์นั้นให้ grep ทีละบรรทัดในขณะที่รุ่น piped จะใช้แต่ละบรรทัดจาก ls ตามที่ปรากฏ และส่งผ่านบรรทัดนั้นไปยัง grep


1
Nitpick: ลำดับมีความสำคัญในการเปลี่ยนเส้นทางหากคุณเปลี่ยนเส้นทางไปยังอีก fd: echo yes 1>&2 2>/tmp/blah; wc -l /tmp/blah; echo yes 2>/tmp/blah 1>&2; wc -l /tmp/blahนอกจากนี้การเปลี่ยนเส้นทางไปยังไฟล์จะใช้การเปลี่ยนเส้นทางล่าสุดเท่านั้น จะเขียนถึงecho yes >/tmp/blah >/tmp/blah2 /tmp/blah2
muru

2
การเปลี่ยนเส้นทางไม่ได้โต้แย้งโปรแกรมจริง โปรแกรมจะไม่ทราบหรือไม่สนใจว่าเอาต์พุตไปที่ใด (หรืออินพุตมาจาก) เป็นเพียงวิธีการบอกวิธีการจัดเรียงสิ่งต่าง ๆ ก่อนที่จะรันโปรแกรม
Alois Mahdal

3

หมายเหตุ: คำตอบสะท้อนถึงความเข้าใจของฉันเกี่ยวกับกลไกเหล่านี้เป็นปัจจุบันสะสมมากกว่าการวิจัยและการอ่านคำตอบของเพื่อนในเว็บไซต์นี้และunix.stackexchange.comและจะได้รับการปรับปรุงเมื่อเวลาผ่านไป อย่าลังเลที่จะถามคำถามหรือเสนอแนะการปรับปรุงในความคิดเห็น ฉันยังแนะนำให้คุณลองดูว่า syscalls ทำงานอย่างไรในเชลล์ด้วยstraceคำสั่ง นอกจากนี้โปรดอย่าถูกข่มขู่ด้วยแนวคิดเรื่องภายในหรือตึกระฟ้า - คุณไม่จำเป็นต้องรู้หรือสามารถใช้มันเพื่อทำความเข้าใจว่าเชลล์ทำสิ่งต่าง ๆ อย่างไร แต่พวกเขาช่วยทำความเข้าใจได้อย่างแน่นอน

TL; DR

  • |ไพพ์ไม่เกี่ยวข้องกับรายการบนดิสก์ดังนั้นจึงไม่มีหมายเลขinodeของระบบไฟล์ดิสก์ (แต่มี inode ในpipefsระบบไฟล์เสมือนใน kernel-space) แต่การเปลี่ยนเส้นทางมักเกี่ยวข้องกับไฟล์ซึ่งมีรายการดิสก์และดังนั้นจึงมีความสอดคล้องกัน inode
  • ไปป์ไม่lseek()สามารถทำได้ดังนั้นคำสั่งไม่สามารถอ่านข้อมูลบางส่วนแล้วย้อนกลับ แต่เมื่อคุณเปลี่ยนเส้นทางด้วย>หรือ<โดยปกติจะเป็นไฟล์ที่lseek()สามารถวัตถุได้ดังนั้นคำสั่งจึงสามารถนำทางได้ตามต้องการ
  • การเปลี่ยนเส้นทางเป็นการจัดการกับ file descriptors ซึ่งมีได้หลายอย่าง pipes มีคำอธิบายไฟล์เพียงสองคำเท่านั้นคำสั่งสำหรับคำสั่ง left และคำสั่ง right สำหรับคำสั่ง
  • การเปลี่ยนเส้นทางบนสตรีมและไพพ์มาตรฐานนั้นทั้งคู่ถูกบัฟเฟอร์
  • ท่อมักจะเกี่ยวข้องกับการฟอร์กดังนั้นจึงมีคู่ของกระบวนการที่เกี่ยวข้อง การเปลี่ยนเส้นทาง - ไม่เสมอไปแม้ว่าในทั้งสองกรณีผลลัพธ์ของตัวอธิบายไฟล์จะได้รับมรดกโดยกระบวนการย่อย
  • ไพพ์เชื่อมต่อตัวอธิบายไฟล์ (คู่) เสมอการเปลี่ยนเส้นทาง - ใช้ชื่อพา ธ หรือตัวอธิบายไฟล์
  • ไปป์เป็นวิธีการสื่อสารระหว่างกระบวนการในขณะที่การเปลี่ยนเส้นทางเป็นเพียงการจัดการกับไฟล์ที่เปิดหรือวัตถุที่มีลักษณะคล้ายไฟล์
  • ทั้งสองใช้dup2()syscalls ใต้ฝากระโปรงเพื่อจัดทำสำเนาของตัวให้คำอธิบายไฟล์ซึ่งการไหลของข้อมูลที่เกิดขึ้นจริง
  • การเปลี่ยนเส้นทางสามารถนำไปใช้ "ทั่วโลก" ด้วยexecคำสั่งในตัว (ดูนี้และสิ่งนี้ ) ดังนั้นหากคุณทำexec > output.txtทุกคำสั่งจะเขียนถึงoutput.txtจากนั้นเป็นต้นไป |ไพพ์จะใช้กับคำสั่งปัจจุบันเท่านั้น (ซึ่งหมายถึงคำสั่งแบบง่ายหรือคำสั่งย่อยเช่นseq 5 | (head -n1; head -n2)คำสั่งผสมหรือคำสั่งผสม
  • เมื่อเปลี่ยนเส้นทางจะทำในไฟล์สิ่งที่ต้องการecho "TEST" > fileและecho "TEST" >> fileการใช้งานทั้งopen()syscall ที่ไฟล์นั้น ( ดู ) dup2()และได้รับการอธิบายไฟล์จากมันจะผ่านมันไป ท่อ|ใช้pipe()และdup2()syscall เท่านั้น

  • เท่าที่คำสั่งถูกดำเนินการไปป์และการเปลี่ยนเส้นทางไม่เกิน file descriptors - วัตถุเหมือนไฟล์ที่พวกเขาอาจเขียนสุ่มสี่สุ่มห้าหรือจัดการกับพวกเขาภายใน (ซึ่งอาจก่อให้เกิดพฤติกรรมที่ไม่คาดคิดaptเช่นมีแนวโน้มที่จะไม่เขียนแม้แต่ถ้ามันรู้ว่ามีการเปลี่ยนเส้นทาง)

บทนำ

เพื่อที่จะเข้าใจว่ากลไกทั้งสองนี้แตกต่างกันอย่างไรจึงจำเป็นต้องเข้าใจคุณสมบัติที่สำคัญประวัติหลังทั้งสองและรากของพวกเขาในภาษาซี ในความเป็นจริงรู้ว่าสิ่งที่อธิบายไฟล์และวิธีdup2()และสายระบบการทำงานเป็นสิ่งจำเป็นเช่นเดียวกับpipe() lseek()เชลล์มีจุดประสงค์เพื่อทำให้กลไกเหล่านี้เป็นนามธรรมแก่ผู้ใช้ แต่การขุดลึกกว่าสิ่งที่เป็นนามธรรมจะช่วยให้เข้าใจถึงลักษณะที่แท้จริงของพฤติกรรมของเชลล์

ต้นกำเนิดของการเปลี่ยนเส้นทางและท่อ

อ้างอิงจากบทความเดนนิส Ritche ของคำทำนาย Petroglyphsท่อมาจากบันทึกภายใน 1964โดยมัลคอล์ดักลาส McIlroy , ในขณะที่พวกเขากำลังทำงานบนระบบปฏิบัติการ Multics อ้างถึง:

เพื่อนำข้อกังวลที่แข็งแกร่งที่สุดมาสรุป:

  1. เราควรมีวิธีการเชื่อมต่อโปรแกรมเช่นสายสวน - สกรูในส่วนอื่นเมื่อมันกลายเป็นเมื่อมีความจำเป็นในการนวดข้อมูลในอีกทางหนึ่ง นี่เป็นวิธีการของ IO ด้วย

สิ่งที่เห็นได้ชัดคือในเวลานั้นโปรแกรมสามารถเขียนลงดิสก์ได้ แต่ก็ไม่มีประสิทธิภาพหากเอาต์พุตมีขนาดใหญ่ หากต้องการอ้างอิงคำอธิบายของ Brian Kernighan ในวิดีโอUnix Pipeline :

ก่อนอื่นคุณไม่จำเป็นต้องเขียนโปรแกรมขนาดใหญ่อันยิ่งใหญ่ - คุณมีโปรแกรมขนาดเล็กที่มีอยู่แล้วซึ่งอาจทำหน้าที่บางส่วนของงานได้ ... อีกอย่างก็คือเป็นไปได้ว่าปริมาณข้อมูลที่คุณกำลังประมวลผลจะไม่เหมาะสมหาก คุณเก็บไว้ในไฟล์ ... เพราะจำไว้ว่าเราย้อนกลับไปในวันที่ดิสก์ในสิ่งเหล่านี้มีถ้าคุณโชคดีมีเมกะไบต์หรือสองข้อมูล ... ดังนั้นไปป์ไลน์ไม่ต้องยกตัวอย่างผลลัพธ์ทั้งหมด .

ดังนั้นความแตกต่างทางแนวคิดจึงชัดเจน: ไพพ์เป็นกลไกในการทำให้โปรแกรมพูดคุยกัน การเปลี่ยนเส้นทาง - เป็นวิธีการเขียนไฟล์ในระดับพื้นฐาน ในทั้งสองกรณีเปลือกทำให้ทั้งสองสิ่งนี้ง่าย แต่ใต้ฝากระโปรงมีอะไรเกิดขึ้นมากมาย

จะลึกลงไป: syscalls และการทำงานภายในของเชลล์

เราเริ่มต้นด้วยความคิดของไฟล์อธิบาย อธิบายไฟล์โดยทั่วไปไฟล์ที่เปิด (ไม่ว่าจะเป็นไฟล์บนดิสก์หรือในหน่วยความจำหรือไฟล์ที่ไม่ระบุชื่อ) ซึ่งจะถูกแสดงด้วยจำนวนเต็ม สตรีมข้อมูลมาตรฐานสองรายการ (stdin, stdout, stderr) คือตัวอธิบายไฟล์ 0,1 และ 2 ตามลำดับ พวกเขามาจากที่ไหน ? ในคำสั่งเชลล์ตัวอธิบายไฟล์นั้นสืบทอดมาจาก parent - shell และโดยทั่วไปแล้วสำหรับกระบวนการทั้งหมด - กระบวนการลูกสืบทอดตัวบ่งชี้ไฟล์ของผู้ปกครอง สำหรับdaemonsเป็นเรื่องปกติที่จะปิด descriptor ไฟล์ที่สืบทอดทั้งหมดและ / หรือเปลี่ยนเส้นทางไปยังที่อื่น

กลับสู่การเปลี่ยนเส้นทาง มันคืออะไรจริงๆ? มันเป็นกลไกที่บอกให้เชลล์เตรียมไฟล์ descriptors สำหรับคำสั่ง (เนื่องจากการเปลี่ยนเส้นทางทำโดยเชลล์ก่อนที่คำสั่งจะรัน) และชี้ไปที่ตำแหน่งที่ผู้ใช้แนะนำ ความคมชัดมาตรฐานของการเปลี่ยนเส้นทางการส่งออกเป็น

[n]>word

นั่น[n]คือหมายเลขตัวอธิบายไฟล์ เมื่อคุณทำเช่นecho "Something" > /dev/nullหมายเลข 1 echo 2> /dev/nullเป็นนัยที่นั่นและ

ภายใต้ประทุนทำโดยการทำซ้ำ descriptor ไฟล์ผ่านการdup2()เรียกของระบบ df > /dev/nullลองมา เชลล์จะสร้างกระบวนการลูกที่dfทำงาน แต่ก่อนหน้านั้นจะเปิด/dev/nullเป็น file descriptor # 3 และdup2(3,1)จะออกซึ่งจะทำสำเนาของ file descriptor 3 และสำเนาจะเป็น 1 คุณรู้ว่าคุณมีสองไฟล์file1.txtและอย่างไรfile2.txtและเมื่อคุณทำcp file1.txt file2.txtคุณจะมีไฟล์สองไฟล์เหมือนกัน แต่คุณสามารถจัดการได้อย่างอิสระ นั่นเป็นสิ่งเดียวกันที่เกิดขึ้นที่นี่ บ่อยครั้งที่คุณจะเห็นว่าก่อนที่จะใช้งานสิ่งที่bashต้องทำdup(1,10)เพื่อทำสำเนาไฟล์ descriptor # 1 ซึ่งคือstdout(และสำเนานั้นจะเป็น fd # 10) เพื่อเรียกคืนในภายหลัง สิ่งสำคัญคือให้สังเกตว่าเมื่อคุณพิจารณาคำสั่งในตัว(ซึ่งเป็นส่วนหนึ่งของเชลล์เองและไม่มีไฟล์ใน/binหรือที่อื่น ๆ ) หรือคำสั่งง่าย ๆ ในเชลล์ที่ไม่มีการโต้ตอบเชลล์จะไม่สร้างกระบวนการลูก

และจากนั้นเรามีสิ่งที่ต้องการและ[n]>&[m] [n]&<[m]นี่คือการทำสำเนาไฟล์ descriptors ซึ่งกลไกเดียวกับdup2()ตอนนี้มันอยู่ในรูปแบบเชลล์ซึ่งพร้อมใช้งานสำหรับผู้ใช้

หนึ่งในสิ่งสำคัญที่ควรทราบเกี่ยวกับการเปลี่ยนเส้นทางคือลำดับของพวกเขาไม่ได้รับการแก้ไข แต่มีความสำคัญต่อวิธีที่เชลล์ตีความสิ่งที่ผู้ใช้ต้องการ เปรียบเทียบสิ่งต่อไปนี้:

# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/  3>&2  2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd

# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/    2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd

การใช้ประโยชน์เหล่านี้ในการเขียนสคริปต์เชลล์สามารถใช้งานได้หลากหลาย:

และอื่น ๆ อีกมากมาย

ท่อประปาด้วยpipe()และdup2()

ดังนั้นท่อจะถูกสร้างขึ้นได้อย่างไร? ผ่านpipe()syscallซึ่งจะใช้เป็นอินพุตอาร์เรย์ (รายการ aka) ที่เรียกว่าประเภทpipefdสองรายการint(จำนวนเต็ม) จำนวนเต็มสองตัวนั้นเป็นตัวอธิบายไฟล์ pipefd[0]จะสิ้นสุดการอ่านของท่อและpipefd[1]จะเป็นจุดสิ้นสุดการเขียน ดังนั้นในdf | grep 'foo', grepจะได้รับสำเนาของpipefd[0]และ จะได้รับสำเนาของdf pipefd[1]แต่อย่างไร แน่นอนว่าด้วยความมหัศจรรย์ของdup2()syscall สำหรับdfในตัวอย่างสมมติว่าpipefd[1]มี # 4 เพื่อให้เปลือกจะทำให้เด็กทำdup2(4,1)(จำของฉัน cpเช่น?) แล้วทำเพื่อให้ทำงานได้จริงexecve() dfธรรมชาติdfจะสืบทอดไฟล์ descriptor # 1 แต่จะไม่รู้ว่ามันไม่ได้ชี้ไปที่เทอร์มินัลอีกต่อไป แต่จริงๆแล้ว fd # 4 ซึ่งจริงๆแล้วเป็นจุดสิ้นสุดการเขียนของไพพ์ โดยธรรมชาติแล้วสิ่งเดียวกันจะเกิดขึ้นพร้อมgrep 'foo'ยกเว้นจำนวนตัวอธิบายไฟล์ที่แตกต่างกัน

ตอนนี้คำถามที่น่าสนใจ: เราสามารถสร้างท่อที่เปลี่ยนเส้นทาง fd # 2 ได้หรือไม่ไม่ใช่แค่ fd # 1? ใช่ในความเป็นจริงนั่นคือสิ่งที่|&เกิดขึ้นในการทุบตี มาตรฐาน POSIX ต้องการภาษาคำสั่งเชลล์เพื่อสนับสนุนdf 2>&1 | grep 'foo'ไวยากรณ์สำหรับวัตถุประสงค์นั้น แต่bashทำได้|&เช่นกัน

สิ่งสำคัญที่ควรทราบคือไพพ์จะจัดการกับ file descriptors เสมอ มีไพพ์ที่มีอยู่FIFOหรือมีชื่อซึ่งมีชื่อไฟล์บนดิสก์และให้คุณใช้มันเป็นไฟล์ แต่ทำงานเหมือนไพพ์ แต่|ประเภทของท่อเป็นสิ่งที่รู้จักกันในชื่อท่อไม่ระบุชื่อ - พวกเขาไม่มีชื่อไฟล์เพราะพวกเขาเพียงสองวัตถุเชื่อมต่อกัน ความจริงที่ว่าเราไม่ได้จัดการกับไฟล์ก็มีความหมายที่สำคัญเช่นกัน: ท่อไม่lseek()สามารถทำได้ ไฟล์ทั้งในหน่วยความจำหรือบนดิสก์เป็นแบบสแตติก - โปรแกรมสามารถใช้lseek()syscall เพื่อข้ามไปยังไบต์ 120 จากนั้นกลับสู่ไบต์ 10 จากนั้นส่งต่อไปจนสุด ท่อไม่คงที่ - เป็นแบบต่อเนื่องดังนั้นคุณจึงไม่สามารถย้อนกลับข้อมูลที่ได้จากพวกเขาด้วยlseek(). นี่คือสิ่งที่ทำให้บางโปรแกรมทราบว่าพวกเขากำลังอ่านจากไฟล์หรือจากไปป์และทำให้พวกเขาสามารถทำการปรับเปลี่ยนที่จำเป็นสำหรับประสิทธิภาพที่มีประสิทธิภาพ; ในคำอื่น ๆprogสามารถตรวจสอบได้หากฉันทำหรือcat file.txt | prog prog < input.txtตัวอย่างเช่นการทำงานจริงที่เป็นหาง

คุณสมบัติที่น่าสนใจอีกสองอย่างของไพพ์คือพวกเขามีบัฟเฟอร์ซึ่งบน Linux คือ 4096 ไบต์และพวกเขามีระบบไฟล์ตามที่กำหนดไว้ในซอร์สโค้ด Linux ! พวกเขาไม่ได้เป็นเพียงวัตถุสำหรับส่งข้อมูลไปรอบ ๆ พวกมันเป็นโครงสร้างข้อมูลเอง! ในความเป็นจริงเนื่องจากมีระบบไฟล์ pipefs ซึ่งจัดการทั้งไพพ์และ FIFOs ไพพ์ จึงมีหมายเลขไอโหนดบนระบบไฟล์ที่เกี่ยวข้อง:

# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/  | cat  
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'

บนท่อ Linux เป็นแบบทิศทางเดียวเช่นเดียวกับการเปลี่ยนเส้นทาง ในการใช้งานที่คล้าย Unix บางอย่าง - มีท่อสองทิศทาง ถึงแม้ว่าจะมีความมหัศจรรย์ของการเขียนสคริปต์เชลล์คุณสามารถสร้างท่อสองทิศทางบน Linux ได้เช่นกัน

ดูสิ่งนี้ด้วย:


2

หากต้องการเพิ่มคำตอบอื่น ๆ ก็มีความแตกต่างทางความหมายที่ลึกซึ้งเช่นกันเช่นท่อปิดได้ง่ายกว่าการเปลี่ยนเส้นทาง:

seq 5 | (head -n1; head -n1)                # just 1
seq 5 > tmp5; (head -n1; head -n1) < tmp5   # 1 and 2
seq 5 | (read LINE; echo $LINE; head -n1)   # 1 and 2

ในตัวอย่างแรกเมื่อสายแรกที่headเสร็จสิ้นจะปิดท่อและยุติเพื่อให้มีการป้อนข้อมูลที่สามารถใช้ได้สำหรับสองseqhead

ในตัวอย่างที่สองหัวจะใช้บรรทัดแรก แต่เมื่อปิดลงจะเป็นstdin ไปป์ของตัวเองไฟล์จะยังคงเปิดอยู่สำหรับการโทรครั้งต่อไปที่จะใช้

ตัวอย่างที่สามแสดงให้เห็นว่าถ้าเราใช้readเพื่อหลีกเลี่ยงการปิดไปป์มันยังคงมีอยู่ในกระบวนการย่อย

ดังนั้น "กระแส" คือสิ่งที่เราแบ่งข้อมูลผ่าน (stdin ฯลฯ ) และเหมือนกันในทั้งสองกรณี แต่ท่อเชื่อมต่อสตรีมจากสองกระบวนการซึ่งการเปลี่ยนเส้นทางเชื่อมต่อสตรีมระหว่างกระบวนการและไฟล์ดังนั้นคุณจึง สามารถดูแหล่งที่มาของทั้งความคล้ายคลึงและความแตกต่าง

ป.ล. ถ้าคุณอยากรู้เกี่ยวกับและ / หรือประหลาดใจกับตัวอย่างเหล่านั้นเหมือนฉันคุณสามารถขุดเพิ่มเติมtrapเพื่อใช้เพื่อดูว่ากระบวนการแก้ไขเช่น:

(trap 'echo seq EXITed >&2' EXIT; seq 5) | (trap 'echo all done' EXIT; (trap 'echo first head exited' EXIT; head -n1)
echo '.'
(trap 'echo second head exited' EXIT; head -n1))

บางครั้งกระบวนการแรกปิดก่อนที่จะ1ถูกพิมพ์บางครั้งหลังจากนั้น

ฉันยังพบว่ามันน่าสนใจที่จะใช้exec <&-เพื่อปิดกระแสจากการเปลี่ยนเส้นทางเพื่อประมาณพฤติกรรมของไปป์ (แม้ว่าจะมีข้อผิดพลาด):

seq 5 > tmp5
(trap 'echo all done' EXIT
(trap 'echo first head exited' EXIT; head -n1)
echo '.'
exec <&-
(trap 'echo second head exited' EXIT; head -n1)) < tmp5`

"เมื่อการเรียกครั้งแรกจบลงจะเป็นการปิดท่อ" นี่เป็นความจริงที่ไม่ถูกต้องด้วยเหตุผลสองประการ หนึ่ง, (head -n1; head -n1) เป็น subshell ที่มีสองคำสั่งซึ่งแต่ละอันจะสืบทอดจุดสิ้นสุดการอ่านไปป์เป็น descriptor 0 ดังนั้น subshell และแต่ละคำสั่งจะเปิดไฟล์ descriptor นั้น เหตุผลที่สองคุณจะเห็นว่าด้วย strace -f bash -c 'seq 5 | (head -n1; head -n1) ' ดังนั้นหัวแรกจึงปิดเฉพาะสำเนาของตัวอธิบายไฟล์
Sergiy Kolodyazhnyy

ตัวอย่างที่สามนั้นไม่ถูกต้องเช่นกันเพราะreadใช้เพียงบรรทัดแรก (นั่นคือหนึ่งไบต์สำหรับ1และขึ้นบรรทัดใหม่) seqส่งทั้งหมด 10 ไบต์ (5 ตัวเลขและ 5 บรรทัดใหม่) ดังนั้นจึงมีเหลือ 8 ไบต์ในไพพ์บัฟเฟอร์และนั่นเป็นสาเหตุที่สองheadทำงาน - มีข้อมูลยังคงอยู่ในไพพ์บัฟเฟอร์ Btw หัวจะออกก็ต่อเมื่อมีการอ่าน 0 ไบต์เท่านั้นเช่นเดียวกับในhead /dev/null
Sergiy Kolodyazhnyy

ขอขอบคุณสำหรับการชี้แจง. ฉันกำลังทำความเข้าใจอย่างถูกต้องว่าในseq 5 | (head -n1; head -n1)สายแรกเปล่าท่อดังนั้นจึงยังคงมีอยู่ในสถานะที่เปิด แต่มีข้อมูลสำหรับสายที่สองไปที่ใดhead? ดังนั้นความแตกต่างในพฤติกรรมระหว่างไปป์และการเปลี่ยนเส้นทางเป็นเพราะหัวดึงข้อมูลทั้งหมดออกจากไปป์ แต่เพียง 2 บรรทัดจากไฟล์จัดการ?
Julian de Bhal

ถูกต้อง. และมันเป็นสิ่งที่สามารถเห็นได้ด้วยstraceคำสั่งที่ฉันให้ไว้ในความคิดเห็นแรก ด้วยการเปลี่ยนเส้นทางไฟล์ tmp อยู่บนดิสก์ซึ่งทำให้สามารถค้นหาได้ (เพราะพวกเขาใช้lseek()syscall - คำสั่งสามารถกระโดดไปรอบ ๆ ไฟล์ได้ตั้งแต่ไบต์แรกไปจนถึงสุดท้ายอย่างไรก็ตามพวกเขาต้องการ แต่ท่อนั้นเป็นแบบเรียงลำดับและไม่สามารถหาได้ งานคือการอ่านทุกอย่างก่อนหรือถ้าไฟล์มีขนาดใหญ่ - แม็พกับ RAM ผ่านการmmap()โทรฉันเคยทำtailPython ของตัวเองแล้วเจอปัญหาเดียวกัน
Sergiy Kolodyazhnyy

นอกจากนี้ยังเป็นสิ่งสำคัญที่ต้องจำไว้ว่าปลายอ่านของท่อ (ไฟล์อธิบาย) จะได้รับการ subshell แรก(...)และ subshell จะทำสำเนาของ stdin (...)ของตัวเองเพื่อแต่ละคำสั่งภายใน ดังนั้นพวกเขากำลังอ่านทางเทคนิคจากวัตถุเดียวกัน ก่อนอื่นhead คิดว่ามันอ่านจาก stdin ของตัวเอง ประการที่สองheadคิดว่ามันมี stdin ของตัวเอง แต่ในความเป็นจริงแล้ว fd # 1 (stdin) ของพวกเขาเป็นเพียงสำเนาของ fd เดียวกันซึ่งอ่านส่วนท้ายของไพพ์ นอกจากนี้ฉันได้โพสต์คำตอบดังนั้นอาจช่วยอธิบายสิ่งต่าง ๆ ได้
Sergiy Kolodyazhnyy

1

ฉันมีปัญหากับเรื่องนี้ใน C วันนี้ stdinหลักท่อมีความหมายแตกต่างกันเพื่อเปลี่ยนเส้นทางเป็นอย่างดีแม้ในขณะที่ส่งไปยัง จริง ๆ แล้วฉันคิดว่าได้รับความแตกต่างไปป์ควรไปที่อื่นstdinเพื่อให้stdinและเรียกมันว่าstdpipe(เพื่อสร้างความแตกต่างโดยพลการ) สามารถจัดการได้หลายวิธี

พิจารณาสิ่งนี้. เมื่อไพพ์หนึ่งเอาต์พุตของโปรแกรมหนึ่งไปยังอีกอันหนึ่งfstatดูเหมือนว่าจะคืนค่าศูนย์st_sizeทั้งๆที่ls -lha /proc/{PID}/fdแสดงว่ามีไฟล์ เมื่อเปลี่ยนเส้นทางไฟล์นี้เป็นกรณีที่ไม่ (อย่างน้อยในเดเบียนwheezy, stretchและjessieวานิลลาและอูบุนตู14.04, 16.04วานิลลา

หากคุณcat /proc/{PID}/fd/0เปลี่ยนเส้นทางคุณจะสามารถอ่านซ้ำหลาย ๆ ครั้งตามที่คุณต้องการ หากคุณทำเช่นนี้ด้วยไพพ์คุณจะสังเกตเห็นว่าครั้งที่สองที่คุณรันภารกิจอย่างต่อเนื่องคุณจะไม่ได้ผลลัพธ์ที่เหมือนกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.