วิธีการใช้ไปป์ไลน์ใน 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()เพื่อรับตัวอธิบายไฟล์ที่แนบมา กระบวนการลูกหนึ่งเขียนถึงตัวอธิบายหนึ่งตัวและอีกกระบวนการหนึ่งอ่านข้อมูลเดียวกันจากตัวอธิบายอื่น เชลล์ "เปลี่ยนชื่อ" อธิบาย 2 และ 3 ด้วย dup4 เพื่อให้ตรงกับ stdin และ stdout

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

หากกระบวนการพยายามอ่านจากไปป์ไลน์ที่ว่างเปล่า read(2) จะบล็อกจนกว่าข้อมูลจะพร้อมใช้งาน หากกระบวนการพยายามเขียนไปยังไปป์ไลน์แบบเต็ม write(2) จะบล็อกจนกว่าจะอ่านข้อมูลเพียงพอจากไปป์ไลน์เพื่อเขียนให้เสร็จ

เช่นเดียวกับข้อกำหนด POSIX นี่เป็นคุณสมบัติที่สำคัญ: การเขียนไปยังไปป์ไลน์จนถึง PIPE_BUF ไบต์ (อย่างน้อย 512) ต้องเป็น atomic เพื่อให้กระบวนการสามารถสื่อสารระหว่างกันผ่านไปป์ไลน์ในแบบที่ไฟล์ปกติ (ซึ่งไม่มีการรับประกันดังกล่าว) ไม่สามารถทำได้

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

เรากำลังมองหาอะไร?

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

ตอนนี้เราพร้อมที่จะตรวจสอบซอร์สโค้ดของเคอร์เนลภายใต้แสงไฟสว่างจ้าเพื่อยืนยันหรือหักล้างโมเดลทางจิตที่คลุมเครือของเรา แต่เตรียมพร้อมสำหรับสิ่งที่ไม่คาดฝันเสมอ

เรากำลังมองหาที่ไหน?

ฉันไม่รู้ว่าสำเนาหนังสือที่มีชื่อเสียงของฉันอยู่ที่ไหนหนังสือสิงโต« ด้วยซอร์สโค้ด Unix 6 แต่ต้องขอบคุณ สมาคมมรดกยูนิกซ์ สามารถค้นหาได้ทางออนไลน์ รหัสแหล่งที่มา แม้แต่ระบบปฏิบัติการยูนิกซ์รุ่นเก่า

การท่องไปตามหอจดหมายเหตุของ TUHS ก็เหมือนกับการเยี่ยมชมพิพิธภัณฑ์ เราสามารถดูประวัติที่ใช้ร่วมกันของเราได้และฉันก็เคารพในความพยายามหลายปีในการกู้คืนเนื้อหาทั้งหมดนี้ทีละนิดจากเทปและสิ่งพิมพ์เก่า ๆ และฉันก็ตระหนักดีถึงชิ้นส่วนที่ยังขาดหายไป

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

อนึ่ง pipe คือ system call หมายเลข 42 ในตาราง sysent[]. เหตุบังเอิญ?

เมล็ด Unix แบบดั้งเดิม (1970–1974)

ฉันไม่พบร่องรอยใดๆ pipe(2) ไม่ว่าใน PDP-7 ยูนิกซ์ (มกราคม 1970) หรือใน ยูนิกซ์รุ่นแรก (พฤศจิกายน 1971) หรือในซอร์สโค้ดที่ไม่สมบูรณ์ พิมพ์ครั้งที่สอง (มิถุนายน 1972).

TUHS อ้างว่า ยูนิกซ์รุ่นที่สาม (กุมภาพันธ์ 1973) เป็นรุ่นแรกที่มีไปป์ไลน์:

Unix รุ่นที่ 1973 เป็นเวอร์ชันสุดท้ายที่มีเคอร์เนลที่เขียนด้วยแอสเซมเบลอร์ แต่ก็เป็นเวอร์ชันแรกที่มีไปป์ไลน์ด้วย ระหว่างปี XNUMX งานกำลังดำเนินการปรับปรุงรุ่นที่สาม เคอร์เนลถูกเขียนใหม่ด้วยภาษา C และด้วยเหตุนี้ Unix รุ่นที่สี่จึงถือกำเนิดขึ้น

ผู้อ่านคนหนึ่งพบการสแกนเอกสารที่ Doug McIlroy เสนอแนวคิดเรื่อง "การเชื่อมต่อโปรแกรมเหมือนสายสวน"

วิธีการใช้ไปป์ไลน์ใน Unix
ในหนังสือของ Brian KernighanUnix: ประวัติศาสตร์และความทรงจำ", ประวัติของการปรากฏตัวของสายพานยังกล่าวถึงเอกสารนี้: "... มันแขวนอยู่บนผนังในสำนักงานของฉันที่ Bell Labs เป็นเวลา 30 ปี" ที่นี่ บทสัมภาษณ์ของแม็คอิลรอยและอีกเรื่องจาก ผลงานของ McIlroy เขียนในปี 2014:

เมื่อ Unix ปรากฏขึ้น ความหลงใหลใน coroutines ของฉันทำให้ฉันขอให้ผู้เขียนระบบปฏิบัติการ Ken Thompson อนุญาตให้ข้อมูลที่เขียนไปยังกระบวนการบางอย่างไม่เพียงไปที่อุปกรณ์เท่านั้น แต่ยังรวมถึงทางออกจากกระบวนการอื่นด้วย เคนคิดว่ามันเป็นไปได้ อย่างไรก็ตาม ในฐานะผู้ที่มีแนวคิดแบบมินิมอล เขาต้องการให้ระบบทุกระบบมีบทบาทสำคัญ การเขียนโดยตรงระหว่างกระบวนการมีประโยชน์มากกว่าการเขียนไปยังไฟล์ระดับกลางหรือไม่? และเมื่อฉันทำข้อเสนอเฉพาะด้วยชื่อ "pipeline" ที่จับใจและคำอธิบายของไวยากรณ์ของการโต้ตอบของกระบวนการ ในที่สุด Ken ก็อุทาน: "ฉันจะทำ!"

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

ขออภัย ซอร์สโค้ดสำหรับเคอร์เนล Unix รุ่นที่สามได้สูญหายไป และแม้ว่าเราจะมีซอร์สโค้ดของเคอร์เนลที่เขียนด้วยภาษาซี ฉบับที่สี่ซึ่งเผยแพร่ในเดือนพฤศจิกายน พ.ศ. 1973 แต่เผยแพร่เมื่อไม่กี่เดือนก่อนการเปิดตัวอย่างเป็นทางการและไม่มีการดำเนินการไปป์ไลน์ น่าเสียดายที่ซอร์สโค้ดสำหรับคุณลักษณะ Unix ในตำนานนี้สูญหายไปหรืออาจจะตลอดไป

เรามีข้อความเอกสารสำหรับ pipe(2) จากทั้งสองรุ่น ดังนั้นคุณจึงสามารถเริ่มต้นด้วยการค้นหาเอกสารประกอบ พิมพ์ครั้งที่สาม (สำหรับบางคำ ให้ขีดเส้นใต้ "ด้วยตนเอง" สตริงของ ^H ตามตัวอักษรตามด้วยเครื่องหมายขีดล่าง!) โปรโต-pipe(2) ถูกเขียนขึ้นในแอสเซมเบลอร์และส่งคืนตัวอธิบายไฟล์เพียงตัวเดียว แต่มีฟังก์ชันการทำงานหลักที่คาดไว้อยู่แล้ว:

ระบบเรียก ท่อ สร้างกลไก I/O ที่เรียกว่าไปป์ไลน์ ตัวอธิบายไฟล์ที่ส่งคืนสามารถใช้สำหรับการดำเนินการอ่านและเขียน เมื่อบางสิ่งถูกเขียนไปยังไปป์ไลน์ มันจะบัฟเฟอร์ข้อมูลมากถึง 504 ไบต์ หลังจากนั้นกระบวนการเขียนจะถูกระงับ เมื่ออ่านจากไปป์ไลน์ ข้อมูลที่บัฟเฟอร์จะถูกนำมาใช้

ในปีต่อมา เคอร์เนลถูกเขียนใหม่ใน C และ ท่อ (2) ฉบับที่สี่ ได้รับรูปลักษณ์ที่ทันสมัยด้วยต้นแบบ "pipe(fildes)":

ระบบเรียก ท่อ สร้างกลไก I/O ที่เรียกว่าไปป์ไลน์ ตัวอธิบายไฟล์ที่ส่งคืนสามารถใช้ในการดำเนินการอ่านและเขียน เมื่อบางสิ่งถูกเขียนไปยังไปป์ไลน์ จะใช้ตัวอธิบายที่ส่งคืนใน r1 (resp. fildes[1]) โดยบัฟเฟอร์ข้อมูลมากถึง 4096 ไบต์ หลังจากนั้นกระบวนการเขียนจะถูกระงับ เมื่ออ่านจากไพพ์ไลน์ ตัวอธิบายจะกลับไปที่ r0 (resp. fildes[0]) รับข้อมูล

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

เชลล์มีไวยากรณ์สำหรับกำหนดอาร์เรย์เชิงเส้นของกระบวนการที่เชื่อมต่อผ่านไปป์ไลน์

การเรียกให้อ่านจากไพพ์ไลน์เปล่า (ไม่มีข้อมูลที่บัฟเฟอร์) ที่มีเพียงปลายด้านเดียว (ปิดตัวอธิบายไฟล์การเขียนทั้งหมด) ส่งคืน "end of file" การเขียนการโทรในสถานการณ์ที่คล้ายกันจะถูกละเว้น

เร็วที่สุด การดำเนินการไปป์ไลน์ที่อนุรักษ์ไว้ มีผลบังคับใช้ ถึง Unix รุ่นที่ห้า (มิถุนายน 1974) แต่เกือบจะเหมือนกับที่ปรากฏในรุ่นถัดไป เพิ่มเฉพาะความคิดเห็น ดังนั้นฉบับที่ห้าสามารถข้ามได้

Unix รุ่นที่หก (1975)

กำลังเริ่มอ่านซอร์สโค้ด Unix พิมพ์ครั้งที่หก (พฤษภาคม 1975). ขอบคุณมากที่ ไลออนส์ หาได้ง่ายกว่าแหล่งที่มาของเวอร์ชันก่อนหน้ามาก:

หนังสือมาหลายปีแล้ว ไลออนส์ เป็นเอกสารเดียวบนเคอร์เนล Unix ที่มีอยู่ภายนอก Bell Labs แม้ว่าใบอนุญาตรุ่นที่หกจะอนุญาตให้ครูใช้ซอร์สโค้ดได้ แต่ใบอนุญาตรุ่นที่เจ็ดไม่มีความเป็นไปได้นี้ ดังนั้นหนังสือจึงถูกแจกจ่ายในรูปแบบการพิมพ์ที่ผิดกฎหมาย

วันนี้คุณสามารถซื้อสำเนาหนังสือที่พิมพ์ซ้ำซึ่งหน้าปกเป็นภาพนักเรียนที่เครื่องถ่ายเอกสาร และต้องขอบคุณ Warren Toomey (ผู้เริ่มโครงการ TUHS) คุณสามารถดาวน์โหลดได้ ฉบับที่หก ที่มา PDF. ฉันต้องการให้คุณทราบว่าต้องใช้ความพยายามมากเพียงใดในการสร้างไฟล์:

กว่า 15 ปีที่แล้ว ฉันพิมพ์สำเนาของซอร์สโค้ดที่ให้ไว้ใน ไลออนส์เพราะฉันไม่ชอบคุณภาพของสำเนาของฉันจากสำเนาอื่น ๆ ที่ไม่ทราบจำนวน ยังไม่มี TUHS และฉันไม่สามารถเข้าถึงแหล่งข้อมูลเก่าได้ แต่ในปี 1988 ฉันพบเทปเก่าที่มี 9 แทร็กที่มีการสำรองข้อมูลจากคอมพิวเตอร์ PDP11 มันยากที่จะรู้ว่ามันใช้งานได้หรือไม่ แต่มีต้นไม้ /usr/src/ ที่ไม่บุบสลายซึ่งไฟล์ส่วนใหญ่ถูกระบุว่าเป็นปี 1979 ซึ่งแม้จะดูโบราณ ฉันคิดว่ามันเป็นฉบับที่เจ็ดหรืออนุพันธ์ของ PWB

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

และวันนี้เราสามารถอ่านออนไลน์ได้ที่ TUHS ซอร์สโค้ดของรุ่นที่หกของ เก็บถาวรซึ่ง Dennis Ritchie มีมือ.

อย่างไรก็ตาม ในแวบแรก คุณสมบัติหลักของรหัส C ก่อนยุคของ 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

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

สำหรับไฟล์ LARG นั้นสอดคล้องกับ ไอโหนดแฟล็ก 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;
}

ความคิดเห็นอธิบายอย่างชัดเจนว่าเกิดอะไรขึ้นที่นี่ แต่มันไม่ง่ายเลยที่จะเข้าใจโค้ด ส่วนหนึ่งเป็นเพราะวิธีการ "struct ผู้ใช้ u» และลงทะเบียน R0 и R1 พารามิเตอร์การเรียกระบบและค่าส่งคืนจะถูกส่งผ่าน

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

pipe() เนื่องจากผ่าน R0 и R1 ส่งคืนหมายเลขตัวอธิบายไฟล์สำหรับการอ่านและเขียน falloc() ส่งคืนตัวชี้ไปยังโครงสร้างไฟล์ แต่ยัง "ส่งคืน" ผ่าน u.u_ar0[R0] และตัวอธิบายไฟล์ นั่นคือรหัสถูกเก็บไว้ใน r ตัวอธิบายไฟล์สำหรับการอ่านและกำหนดตัวอธิบายสำหรับการเขียนโดยตรงจาก u.u_ar0[R0] หลังจากการโทรครั้งที่สอง falloc().

ธง FPIPEซึ่งเราตั้งค่าไว้เมื่อสร้างไปป์ไลน์ควบคุมลักษณะการทำงานของฟังก์ชัน rdwr() ใน sys2.cซึ่งเรียกรูทีน I/O เฉพาะ:

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

เราต้องการเขียนไบต์ไปยังอินพุตไปป์ไลน์ u.u_count. ก่อนอื่นเราต้องล็อคไอโหนด (ดูด้านล่าง plock/prele).

จากนั้นเราจะตรวจสอบจำนวนการอ้างอิงไอโหนด ตราบใดที่ปลายทั้งสองของไปป์ไลน์ยังเปิดอยู่ ตัวนับควรเป็น 2 เรายึดลิงค์เดียว (จาก rp->f_inode) ดังนั้นหากตัวนับมีค่าน้อยกว่า 2 แสดงว่ากระบวนการอ่านได้ปิดจุดสิ้นสุดของไปป์ไลน์แล้ว กล่าวอีกนัยหนึ่งเรากำลังพยายามเขียนถึงไปป์ไลน์ที่ปิด ซึ่งเป็นข้อผิดพลาด รหัสข้อผิดพลาดแรก EPIPE และสัญญาณ SIGPIPE ปรากฏใน Unix รุ่นที่หก

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

หากมีพื้นที่ว่างเพียงพอในไปป์ไลน์ เราจะเขียนข้อมูลลงไปโดยใช้ เขียน (). พารามิเตอร์ i_size1 inode'a (โดยไปป์ไลน์ว่างมีค่าเท่ากับ 0) ชี้ไปที่ส่วนท้ายของข้อมูลที่มีอยู่แล้ว หากมีพื้นที่เพียงพอในการเขียนเราสามารถเติมไปป์ไลน์ได้ i_size1 ไปยัง PIPESIZ. จากนั้นเราจะปลดล็อคและพยายามปลุกกระบวนการใด ๆ ที่กำลังรอการอ่านจากไปป์ไลน์ เรากลับไปที่จุดเริ่มต้นเพื่อดูว่าเราสามารถเขียนไบต์ได้มากเท่าที่เราต้องการหรือไม่ หากล้มเหลว เราจะเริ่มรอบการบันทึกใหม่

มักจะเป็นพารามิเตอร์ i_mode inode ใช้เพื่อจัดเก็บสิทธิ์ r, w и x. แต่ในกรณีของไปป์ไลน์ เราส่งสัญญาณว่ากระบวนการบางอย่างกำลังรอการเขียนหรืออ่านโดยใช้บิต 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 ที่ไอโหนด เรารีเซ็ตตำแหน่งเป็น 0 และพยายามปลุกกระบวนการที่ต้องการเขียนไปยังไปป์ไลน์ เรารู้ว่าเมื่อท่อลำเลียงเต็ม writep() หลับไป ip+1. และตอนนี้ไปป์ไลน์ว่างแล้ว เราก็สามารถปลุกมันขึ้นมาเพื่อกลับมาเขียนต่อได้

ถ้าไม่มีอะไรจะอ่านแล้ว readp() สามารถตั้งธงได้ IREAD และหลับไป ip+2. เรารู้ว่าอะไรจะปลุกเขา writep()เมื่อมันเขียนข้อมูลบางอย่างไปยังไปป์ไลน์

ความคิดเห็นเกี่ยวกับ อ่าน () และเขียน () จะช่วยให้คุณเข้าใจว่าแทนที่จะส่งพารามิเตอร์ผ่าน "u» เราสามารถปฏิบัติต่อมันเหมือนกับฟังก์ชัน I/O ทั่วไปที่รับไฟล์ ตำแหน่ง บัฟเฟอร์ในหน่วยความจำ และนับจำนวนไบต์ที่จะอ่านหรือเขียน

/*
 * 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() ล็อกไอโหนดจนกว่าจะเสร็จหรือได้ผลลัพธ์ (เช่น โทร 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() รีสตาร์ทวงจร

คำอธิบายของไปป์ไลน์เสร็จสมบูรณ์ในรุ่นที่หก รหัสง่าย ๆ ความหมายกว้างไกล

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

Xv6 เคอร์เนลแบบ Unix ที่เรียบง่าย

เพื่อสร้างนิวเคลียส เอ็กซ์วี6 ได้รับอิทธิพลจาก Unix รุ่นที่หก แต่เขียนด้วย C สมัยใหม่เพื่อให้ทำงานบนโปรเซสเซอร์ x86 รหัสนั้นอ่านและเข้าใจได้ง่าย นอกจากนี้ ไม่เหมือนกับแหล่ง Unix ที่มี TUHS คุณสามารถคอมไพล์ แก้ไข และรันบนสิ่งอื่นที่ไม่ใช่ PDP 11/70 ดังนั้นแกนนี้จึงถูกนำมาใช้กันอย่างแพร่หลายในมหาวิทยาลัยเพื่อเป็นสื่อการสอนเกี่ยวกับระบบปฏิบัติการ แหล่งที่มา อยู่บน Github.

รหัสประกอบด้วยการใช้งานที่ชัดเจนและรอบคอบ ไปป์.คสำรองโดยบัฟเฟอร์ในหน่วยความจำแทนไอโหนดบนดิสก์ ที่นี่ฉันให้คำจำกัดความของ "โครงสร้างไปป์ไลน์" และฟังก์ชันเท่านั้น 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 เป็น wrapper ที่ใช้ใน ซิสไฟล์.ซี. ฉันแนะนำให้อ่านรหัสทั้งหมดของเขา ความซับซ้อนอยู่ที่ระดับซอร์สโค้ดของรุ่นที่หก แต่อ่านง่ายกว่าและน่าอ่านกว่ามาก

Linux 0.01

คุณสามารถค้นหาซอร์สโค้ดสำหรับ Linux 0.01 มันจะให้คำแนะนำในการศึกษาการดำเนินการของท่อในเขา fs/pipe.c. ที่นี่ ไอโหนดถูกใช้เพื่อเป็นตัวแทนของไปป์ไลน์ แต่ตัวไปป์ไลน์นั้นเขียนด้วยภาษา C สมัยใหม่ หากคุณแฮ็กผ่านรหัสรุ่นที่หก คุณจะไม่มีปัญหาใดๆ นี่คือลักษณะของฟังก์ชัน 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;
}

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

เคอร์เนล Linux สมัยใหม่, FreeBSD, NetBSD, OpenBSD

ฉันรีบไปดูเมล็ดข้าวสมัยใหม่อย่างรวดเร็ว ไม่มีการใช้งานบนดิสก์อยู่แล้ว (ไม่น่าแปลกใจ) Linux มีการใช้งานของตัวเอง และแม้ว่าเคอร์เนล BSD ที่ทันสมัยทั้งสามจะมีการใช้งานตามโค้ดที่เขียนโดย John Dyson แต่ในช่วงหลายปีที่ผ่านมา เคอร์เนลทั้งสองก็มีความแตกต่างกันมากเกินไป

อ่าน fs/pipe.c (บนลินุกซ์) หรือ sys/kern/sys_pipe.c (บน *BSD) ต้องใช้ความทุ่มเทอย่างแท้จริง ประสิทธิภาพและการสนับสนุนคุณลักษณะต่างๆ เช่น vector และ asynchronous I/O มีความสำคัญในโค้ดในปัจจุบัน และรายละเอียดของการจัดสรรหน่วยความจำ การล็อก และการกำหนดค่าเคอร์เนลล้วนแตกต่างกันไปอย่างมาก นี่ไม่ใช่สิ่งที่มหาวิทยาลัยต้องการสำหรับหลักสูตรเบื้องต้นเกี่ยวกับระบบปฏิบัติการ

ไม่ว่าในกรณีใด มันเป็นเรื่องที่น่าสนใจสำหรับฉันที่จะค้นพบรูปแบบเก่าๆ สองสามรูปแบบ (เช่น การสร้าง SIGPIPE และการกลับมา EPIPE เมื่อเขียนไปป์ไลน์แบบปิด) ในเคอร์เนลสมัยใหม่ที่แตกต่างกันทั้งหมดเหล่านี้ ฉันอาจจะไม่เคยเห็นคอมพิวเตอร์ PDP-11 ใช้งานจริง แต่ยังมีอีกมากที่ต้องเรียนรู้จากโค้ดที่เขียนขึ้นเมื่อไม่กี่ปีก่อนที่ฉันจะเกิด

เขียนโดย Divi Kapoor ในปี 2011 บทความ "การติดตั้ง Linux Kernel ของ Pipes และ FIFOเป็นภาพรวมของวิธีการทำงานของ Linux ไปป์ไลน์ (จนถึงปัจจุบัน) ก การกระทำล่าสุดบน linux แสดงให้เห็นถึงโมเดลไปป์ไลน์ของการโต้ตอบซึ่งมีความสามารถเกินกว่าไฟล์ชั่วคราว และยังแสดงให้เห็นว่าไปป์ไลน์ไปไกลแค่ไหนจาก "การล็อคแบบอนุรักษ์นิยม" ในเคอร์เนล Unix รุ่นที่หก

ที่มา: will.com

เพิ่มความคิดเห็น