บทความนี้อธิบายถึงการใช้งานไปป์ไลน์ในเคอร์เนล 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()
เพื่อรับตัวอธิบายไฟล์ที่แนบมา กระบวนการลูกหนึ่งเขียนถึงตัวอธิบายหนึ่งตัวและอีกกระบวนการหนึ่งอ่านข้อมูลเดียวกันจากตัวอธิบายอื่น เชลล์ "เปลี่ยนชื่อ" อธิบาย 2 และ 3 ด้วย dup4 เพื่อให้ตรงกับ stdin และ stdout
หากไม่มีไพพ์ไลน์ เชลล์จะต้องเขียนเอาต์พุตของกระบวนการหนึ่งไปยังไฟล์และไพพ์ไปยังกระบวนการอื่นเพื่ออ่านข้อมูลจากไฟล์ เป็นผลให้เราเสียทรัพยากรและพื้นที่ดิสก์มากขึ้น อย่างไรก็ตาม ไปป์ไลน์มีประโยชน์มากกว่าการหลีกเลี่ยงไฟล์ชั่วคราว:
หากกระบวนการพยายามอ่านจากไปป์ไลน์ที่ว่างเปล่า
read(2)
จะบล็อกจนกว่าข้อมูลจะพร้อมใช้งาน หากกระบวนการพยายามเขียนไปยังไปป์ไลน์แบบเต็มwrite(2)
จะบล็อกจนกว่าจะอ่านข้อมูลเพียงพอจากไปป์ไลน์เพื่อเขียนให้เสร็จ
เช่นเดียวกับข้อกำหนด POSIX นี่เป็นคุณสมบัติที่สำคัญ: การเขียนไปยังไปป์ไลน์จนถึง PIPE_BUF
ไบต์ (อย่างน้อย 512) ต้องเป็น atomic เพื่อให้กระบวนการสามารถสื่อสารระหว่างกันผ่านไปป์ไลน์ในแบบที่ไฟล์ปกติ (ซึ่งไม่มีการรับประกันดังกล่าว) ไม่สามารถทำได้
ด้วยไฟล์ปกติ กระบวนการสามารถเขียนเอาต์พุตทั้งหมดไปยังกระบวนการนั้นและส่งต่อไปยังกระบวนการอื่น หรือกระบวนการต่างๆ สามารถทำงานในโหมดฮาร์ดขนาน โดยใช้กลไกการส่งสัญญาณภายนอก (เช่น สัญญาณ) เพื่อแจ้งให้กันและกันทราบเกี่ยวกับการเขียนหรือการอ่านที่เสร็จสมบูรณ์ ระบบลำเลียงช่วยเราจากความยุ่งยากทั้งหมดนี้
เรากำลังมองหาอะไร?
ฉันจะอธิบายด้วยนิ้วของฉันเพื่อให้คุณจินตนาการได้ง่ายขึ้นว่าสายพานทำงานอย่างไร คุณจะต้องจัดสรรบัฟเฟอร์และสถานะบางอย่างในหน่วยความจำ คุณจะต้องใช้ฟังก์ชันเพื่อเพิ่มและลบข้อมูลออกจากบัฟเฟอร์ คุณจะต้องมีสิ่งอำนวยความสะดวกบางอย่างในการเรียกใช้ฟังก์ชันระหว่างการดำเนินการอ่านและเขียนบนตัวอธิบายไฟล์ และจำเป็นต้องมีการล็อคเพื่อใช้พฤติกรรมพิเศษที่อธิบายไว้ข้างต้น
ตอนนี้เราพร้อมที่จะตรวจสอบซอร์สโค้ดของเคอร์เนลภายใต้แสงไฟสว่างจ้าเพื่อยืนยันหรือหักล้างโมเดลทางจิตที่คลุมเครือของเรา แต่เตรียมพร้อมสำหรับสิ่งที่ไม่คาดฝันเสมอ
เรากำลังมองหาที่ไหน?
ฉันไม่รู้ว่าสำเนาหนังสือที่มีชื่อเสียงของฉันอยู่ที่ไหน
การท่องไปตามหอจดหมายเหตุของ TUHS ก็เหมือนกับการเยี่ยมชมพิพิธภัณฑ์ เราสามารถดูประวัติที่ใช้ร่วมกันของเราได้และฉันก็เคารพในความพยายามหลายปีในการกู้คืนเนื้อหาทั้งหมดนี้ทีละนิดจากเทปและสิ่งพิมพ์เก่า ๆ และฉันก็ตระหนักดีถึงชิ้นส่วนที่ยังขาดหายไป
เมื่อพอใจกับความอยากรู้อยากเห็นของเราเกี่ยวกับประวัติศาสตร์อันเก่าแก่ของท่อแล้ว เราสามารถดูแกนสมัยใหม่เพื่อเปรียบเทียบได้
อนึ่ง pipe
คือ system call หมายเลข 42 ในตาราง sysent[]
. เหตุบังเอิญ?
เมล็ด Unix แบบดั้งเดิม (1970–1974)
ฉันไม่พบร่องรอยใดๆ pipe(2)
ไม่ว่าใน
TUHS อ้างว่า
Unix รุ่นที่ 1973 เป็นเวอร์ชันสุดท้ายที่มีเคอร์เนลที่เขียนด้วยแอสเซมเบลอร์ แต่ก็เป็นเวอร์ชันแรกที่มีไปป์ไลน์ด้วย ระหว่างปี XNUMX งานกำลังดำเนินการปรับปรุงรุ่นที่สาม เคอร์เนลถูกเขียนใหม่ด้วยภาษา C และด้วยเหตุนี้ Unix รุ่นที่สี่จึงถือกำเนิดขึ้น
ผู้อ่านคนหนึ่งพบการสแกนเอกสารที่ Doug McIlroy เสนอแนวคิดเรื่อง "การเชื่อมต่อโปรแกรมเหมือนสายสวน"
ในหนังสือของ Brian Kernighan
เมื่อ Unix ปรากฏขึ้น ความหลงใหลใน coroutines ของฉันทำให้ฉันขอให้ผู้เขียนระบบปฏิบัติการ Ken Thompson อนุญาตให้ข้อมูลที่เขียนไปยังกระบวนการบางอย่างไม่เพียงไปที่อุปกรณ์เท่านั้น แต่ยังรวมถึงทางออกจากกระบวนการอื่นด้วย เคนคิดว่ามันเป็นไปได้ อย่างไรก็ตาม ในฐานะผู้ที่มีแนวคิดแบบมินิมอล เขาต้องการให้ระบบทุกระบบมีบทบาทสำคัญ การเขียนโดยตรงระหว่างกระบวนการมีประโยชน์มากกว่าการเขียนไปยังไฟล์ระดับกลางหรือไม่? และเมื่อฉันทำข้อเสนอเฉพาะด้วยชื่อ "pipeline" ที่จับใจและคำอธิบายของไวยากรณ์ของการโต้ตอบของกระบวนการ ในที่สุด Ken ก็อุทาน: "ฉันจะทำ!"
และทำ ค่ำคืนแห่งโชคชะตาวันหนึ่ง เคนเปลี่ยนเคอร์เนลและเชลล์ แก้ไขโปรแกรมมาตรฐานหลายโปรแกรมเพื่อสร้างมาตรฐานวิธีรับอินพุต (ซึ่งอาจมาจากไปป์ไลน์) และเปลี่ยนชื่อไฟล์ ในวันถัดไป ท่อส่งถูกนำมาใช้อย่างแพร่หลายในแอปพลิเคชันต่างๆ ในตอนท้ายของสัปดาห์ เลขานุการใช้พวกเขาเพื่อส่งเอกสารจากโปรแกรมประมวลผลคำไปยังเครื่องพิมพ์ หลังจากนั้นไม่นาน Ken ได้แทนที่ API และไวยากรณ์ดั้งเดิมสำหรับการรวมการใช้ไปป์ไลน์ด้วยระเบียบแบบแผนที่ชัดเจนซึ่งใช้มาตั้งแต่นั้นเป็นต้นมา
ขออภัย ซอร์สโค้ดสำหรับเคอร์เนล Unix รุ่นที่สามได้สูญหายไป และแม้ว่าเราจะมีซอร์สโค้ดของเคอร์เนลที่เขียนด้วยภาษาซี
เรามีข้อความเอกสารสำหรับ pipe(2)
จากทั้งสองรุ่น ดังนั้นคุณจึงสามารถเริ่มต้นด้วยการค้นหาเอกสารประกอบ pipe(2)
ถูกเขียนขึ้นในแอสเซมเบลอร์และส่งคืนตัวอธิบายไฟล์เพียงตัวเดียว แต่มีฟังก์ชันการทำงานหลักที่คาดไว้อยู่แล้ว:
ระบบเรียก ท่อ สร้างกลไก I/O ที่เรียกว่าไปป์ไลน์ ตัวอธิบายไฟล์ที่ส่งคืนสามารถใช้สำหรับการดำเนินการอ่านและเขียน เมื่อบางสิ่งถูกเขียนไปยังไปป์ไลน์ มันจะบัฟเฟอร์ข้อมูลมากถึง 504 ไบต์ หลังจากนั้นกระบวนการเขียนจะถูกระงับ เมื่ออ่านจากไปป์ไลน์ ข้อมูลที่บัฟเฟอร์จะถูกนำมาใช้
ในปีต่อมา เคอร์เนลถูกเขียนใหม่ใน C และ pipe(fildes)
":
ระบบเรียก ท่อ สร้างกลไก I/O ที่เรียกว่าไปป์ไลน์ ตัวอธิบายไฟล์ที่ส่งคืนสามารถใช้ในการดำเนินการอ่านและเขียน เมื่อบางสิ่งถูกเขียนไปยังไปป์ไลน์ จะใช้ตัวอธิบายที่ส่งคืนใน r1 (resp. fildes[1]) โดยบัฟเฟอร์ข้อมูลมากถึง 4096 ไบต์ หลังจากนั้นกระบวนการเขียนจะถูกระงับ เมื่ออ่านจากไพพ์ไลน์ ตัวอธิบายจะกลับไปที่ r0 (resp. fildes[0]) รับข้อมูล
สันนิษฐานว่าเมื่อกำหนดไปป์ไลน์แล้ว กระบวนการโต้ตอบสองกระบวนการ (หรือมากกว่า) (สร้างโดยการเรียกใช้ที่ตามมา ส้อม) จะส่งผ่านข้อมูลจากไปป์ไลน์โดยใช้การโทร อ่าน и เขียน.
เชลล์มีไวยากรณ์สำหรับกำหนดอาร์เรย์เชิงเส้นของกระบวนการที่เชื่อมต่อผ่านไปป์ไลน์
การเรียกให้อ่านจากไพพ์ไลน์เปล่า (ไม่มีข้อมูลที่บัฟเฟอร์) ที่มีเพียงปลายด้านเดียว (ปิดตัวอธิบายไฟล์การเขียนทั้งหมด) ส่งคืน "end of file" การเขียนการโทรในสถานการณ์ที่คล้ายกันจะถูกละเว้น
เร็วที่สุด
Unix รุ่นที่หก (1975)
กำลังเริ่มอ่านซอร์สโค้ด Unix
หนังสือมาหลายปีแล้ว ไลออนส์ เป็นเอกสารเดียวบนเคอร์เนล Unix ที่มีอยู่ภายนอก Bell Labs แม้ว่าใบอนุญาตรุ่นที่หกจะอนุญาตให้ครูใช้ซอร์สโค้ดได้ แต่ใบอนุญาตรุ่นที่เจ็ดไม่มีความเป็นไปได้นี้ ดังนั้นหนังสือจึงถูกแจกจ่ายในรูปแบบการพิมพ์ที่ผิดกฎหมาย
วันนี้คุณสามารถซื้อสำเนาหนังสือที่พิมพ์ซ้ำซึ่งหน้าปกเป็นภาพนักเรียนที่เครื่องถ่ายเอกสาร และต้องขอบคุณ Warren Toomey (ผู้เริ่มโครงการ TUHS) คุณสามารถดาวน์โหลดได้
กว่า 15 ปีที่แล้ว ฉันพิมพ์สำเนาของซอร์สโค้ดที่ให้ไว้ใน ไลออนส์เพราะฉันไม่ชอบคุณภาพของสำเนาของฉันจากสำเนาอื่น ๆ ที่ไม่ทราบจำนวน ยังไม่มี TUHS และฉันไม่สามารถเข้าถึงแหล่งข้อมูลเก่าได้ แต่ในปี 1988 ฉันพบเทปเก่าที่มี 9 แทร็กที่มีการสำรองข้อมูลจากคอมพิวเตอร์ PDP11 มันยากที่จะรู้ว่ามันใช้งานได้หรือไม่ แต่มีต้นไม้ /usr/src/ ที่ไม่บุบสลายซึ่งไฟล์ส่วนใหญ่ถูกระบุว่าเป็นปี 1979 ซึ่งแม้จะดูโบราณ ฉันคิดว่ามันเป็นฉบับที่เจ็ดหรืออนุพันธ์ของ PWB
ฉันใช้การค้นหาเป็นพื้นฐานและแก้ไขแหล่งที่มาด้วยตนเองให้เป็นสถานะของการพิมพ์ครั้งที่หก ส่วนหนึ่งของโค้ดยังคงเหมือนเดิม บางส่วนต้องแก้ไขเล็กน้อย เปลี่ยนโทเค็นสมัยใหม่ += เป็น =+ ที่ล้าสมัย บางอย่างถูกลบออกไป และบางอย่างต้องเขียนใหม่ทั้งหมด แต่ไม่มากเกินไป
และวันนี้เราสามารถอ่านออนไลน์ได้ที่ TUHS ซอร์สโค้ดของรุ่นที่หกของ
อย่างไรก็ตาม ในแวบแรก คุณสมบัติหลักของรหัส C ก่อนยุคของ 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
ขนาดบัฟเฟอร์ไม่มีการเปลี่ยนแปลงตั้งแต่รุ่นที่สี่ แต่ที่นี่เราจะเห็นว่าไปป์ไลน์เคยใช้ไฟล์เป็นที่เก็บข้อมูลสำรองโดยไม่มีเอกสารสาธารณะ
สำหรับไฟล์ 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
ซึ่งเราตั้งค่าไว้เมื่อสร้างไปป์ไลน์ควบคุมลักษณะการทำงานของฟังก์ชัน
/*
* 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()
รีสตาร์ทวงจร
คำอธิบายของไปป์ไลน์เสร็จสมบูรณ์ในรุ่นที่หก รหัสง่าย ๆ ความหมายกว้างไกล
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
เป็น 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 บทความ "
ที่มา: will.com