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()хавсаргасан файлын тодорхойлогчдыг авахын тулд. Нэг хүүхэд процесс нь нэг тодорхойлогч руу бичдэг бол өөр процесс нь өөр тодорхойлогчоос ижил өгөгдлийг уншдаг. Бүрхүүл нь stdin болон stdout-тай тааруулахын тулд 2 ба 3-р тодорхойлогчдын нэрийг dup4-оор "өөрчилдөг".

Дамжуулах хоолойгүй бол бүрхүүл нь нэг процессын гаралтыг файл руу бичиж, файлаас өгөгдлийг уншихын тулд өөр процесс руу дамжуулах шаардлагатай болно. Үүний үр дүнд бид илүү их нөөц, дискний зайг үрэх болно. Гэсэн хэдий ч дамжуулах хоолой нь түр зуурын файлуудаас зайлсхийхэд илүү тохиромжтой:

Хэрэв процесс нь хоосон дамжуулах хоолойноос уншихыг оролдвол read(2) өгөгдөл бэлэн болтол хаах болно. Хэрэв процесс бүрэн шугам руу бичихийг оролдвол write(2) бичих ажлыг дуусгахын тулд дамжуулах хоолойноос хангалттай өгөгдлийг унших хүртэл блоклох болно.

POSIX шаардлагын нэгэн адил энэ нь чухал шинж чанар юм: дамжуулах хоолойд хүртэл бичих PIPE_BUF байт (хамгийн багадаа 512) атомын шинж чанартай байх ёстой бөгөөд ингэснээр процессууд хоорондоо дамжуулах хоолойгоор дамжуулан энгийн файлууд (ийм баталгаа өгдөггүй) холбогдож чаддаггүй.

Энгийн файлын хувьд процесс нь бүх гаралтыг түүнд бичиж, өөр процесс руу дамжуулах боломжтой. Эсвэл процессууд нь гадаад дохиоллын механизмыг (семафор гэх мэт) ашиглан бичих эсвэл уншиж дууссаны талаар бие биедээ мэдээлэх хатуу зэрэгцээ горимд ажиллах боломжтой. Туузан дамжуулагч биднийг энэ бүх бэрхшээлээс авардаг.

Бид юу хайж байна вэ?

Туузан дамжуулагч хэрхэн ажиллахыг төсөөлөхөд хялбар болгохын тулд би хуруугаараа тайлбарлах болно. Та санах ойд буфер болон зарим төлөвийг хуваарилах хэрэгтэй болно. Буферээс өгөгдөл нэмэх, устгах функцүүд хэрэгтэй болно. Файлын тодорхойлогч дээр унших, бичих үйлдлүүдийн үед функцуудыг дуудах зарим хэрэгсэл танд хэрэгтэй болно. Дээр дурдсан тусгай зан үйлийг хэрэгжүүлэхийн тулд цоож хэрэгтэй.

Одоо бид тодорхой бус сэтгэцийн загварыг батлах эсвэл үгүйсгэхийн тулд цөмийн эх кодыг хурц гэрлийн дор байцаахад бэлэн байна. Гэхдээ гэнэтийн зүйлд үргэлж бэлэн байгаарай.

Бид хаашаа хайж байна вэ?

Миний алдарт номын хуулбар хаана байгааг би мэдэхгүй.Арслангууд ном« Unix 6 эх кодтой, гэхдээ баярлалаа Юниксийн өвийн нийгэмлэг онлайнаар хайх боломжтой эх код Unix-ийн хуучин хувилбарууд ч гэсэн.

TUHS архиваар тэнүүчлэх нь музей үзэхтэй адил юм. Бид өөрсдийн хамтын түүхийг харж болох бөгөөд энэ бүх материалыг хуучин хуурцаг, хэвлэмэл материалаас бага багаар сэргээхийн тулд олон жилийн хүчин чармайлтыг би хүндэтгэдэг. Одоо ч байхгүй байгаа тэдгээр хэлтэрхийнүүдийг би маш сайн мэдэж байна.

Дамжуулах хоолойн эртний түүхийн талаарх бидний сониуч байдлыг хангасны дараа бид орчин үеийн цөмүүдийг харьцуулж үзэх боломжтой.

Дашрамд хэлэхэд, pipe Хүснэгтэнд байгаа системийн дуудлагын дугаар 42 sysent[]. Тохиолдол уу?

Уламжлалт Unix цөмүүд (1970–1974)

Би ямар ч ул мөр олсонгүй pipe(2) аль нь ч үгүй PDP-7 Unix (1970 оны XNUMX-р сар), мөн онд анхны хэвлэл Unix (1971 оны XNUMX-р сар), мөн бүрэн бус эх кодоор хоёр дахь хэвлэл (1972 оны XNUMX-р сар).

Үүнийг TUHS мэдэгдэв Unix гурав дахь хэвлэл (1973 оны XNUMX-р сар) нь дамжуулах хоолойтой анхны хувилбар байв.

Unix-ийн гурав дахь хэвлэл нь ассемблер дээр бичигдсэн цөмтэй сүүлчийн хувилбар байсан ч дамжуулах шугамтай анхны хувилбар юм. 1973 онд гурав дахь хэвлэлийг боловсронгуй болгох ажил хийгдэж, цөм нь Си хэл дээр дахин бичигдсэн тул Unix-ийн дөрөв дэх хэвлэл мэндэлжээ.

Нэг уншигч Даг Макилрой "цэцэрлэгийн хоолой шиг програмуудыг холбох" санааг дэвшүүлсэн баримт бичгийн сканнерыг олжээ.

Unix дээр дамжуулах хоолой хэрхэн хэрэгждэг
Брайан Керниганы номондЮникс: Түүх ба дурсамж”, конвейерүүдийн гадаад төрх байдлын түүхэнд мөн энэ баримт бичгийг дурьдсан байдаг: "... энэ нь Bell Labs дахь миний ажлын өрөөний хананд 30 жилийн турш өлгөгдсөн." Энд Макилройтой хийсэн ярилцлагаболон өөр нэг түүх McIlroy-ийн 2014 онд бичсэн бүтээл:

Юникс гарч ирэхэд миний корутинд хүсэл тэмүүлэл намайг үйлдлийн системийн зохиогч Кен Томпсоноос зарим процесст бичигдсэн өгөгдөл нь зөвхөн төхөөрөмжид төдийгүй өөр процесс руу гарах боломжийг олгохыг хүссэн. Кен үүнийг боломжтой гэж бодсон. Гэсэн хэдий ч минималист хүний ​​хувьд тэрээр системийн бүх функцийг чухал үүрэг гүйцэтгэхийг хүссэн. Процессуудын хооронд шууд бичих нь завсрын файл руу бичихээс үнэхээр том давуу тал мөн үү? Зөвхөн би "дамжуулах хоолой" гэсэн сэтгэл татам нэртэй тодорхой санал гаргаж, үйл явцын харилцан үйлчлэлийн синтаксийг тайлбарлахад л Кен эцэст нь "Би үүнийг хийх болно!" Гэж хэлэв.

Тэгээд хийсэн. Нэгэн хувь тавилантай орой Кен цөм болон бүрхүүлээ сольж, оролтыг хэрхэн хүлээн авахыг стандарт болгохын тулд хэд хэдэн стандарт программуудыг засч (энэ нь дамжуулах хоолойноос ирж магадгүй), мөн файлын нэрийг өөрчилсөн. Дараагийн өдөр нь дамжуулах хоолой нь хэрэглээнд маш өргөн хэрэглэгддэг. Долоо хоногийн эцэс гэхэд нарийн бичгийн дарга нар текст боловсруулагчаас хэвлэгч рүү бичиг баримт илгээхэд ашигладаг байв. Хэсэг хугацааны дараа Кен анхдагч API болон синтаксийг дамжуулах хоолойн хэрэглээг тэр цагаас хойш ашиглагдаж ирсэн цэвэр конвенцоор сольсон.

Харамсалтай нь Unix цөмийн гурав дахь хувилбарын эх код алдагдсан байна. Хэдийгээр бидэнд цөмийн эх код C хэл дээр бичигдсэн байдаг дөрөв дэх хэвлэл, 1973 оны XNUMX-р сард гарсан боловч албан ёсоор гарахаас хэдхэн сарын өмнө гарсан бөгөөд дамжуулах хоолойн хэрэгжилтийг агуулаагүй болно. Энэхүү домогт Unix функцийн эх код магадгүй үүрд алга болсон нь харамсалтай.

Бидэнд баримт бичгийн текст байна pipe(2) хоёр хувилбараас авах тул та баримтаас хайж эхлэх боломжтой гурав дахь хэвлэл (зарим үгсийн хувьд "гараар" доогуур зураастай, ^H үсгийн дарааллаар доогуур зураас!). Энэ прото-pipe(2) Ассемблер дээр бичигдсэн бөгөөд зөвхөн нэг файлын тодорхойлогчийг буцаадаг боловч хүлээгдэж буй үндсэн функцийг аль хэдийн хангасан байна:

Системийн дуудлага хоолой дамжуулах хоолой гэж нэрлэгддэг оролт гаралтын механизмыг бий болгодог. Буцсан файлын тодорхойлогчийг унших, бичих үйлдэлд ашиглаж болно. Дамжуулах хоолойд ямар нэг зүйл бичихэд 504 байт хүртэлх өгөгдлийг буферд оруулсны дараа бичих үйл явцыг түр зогсооно. Дамжуулах хоолойноос унших үед буферт хадгалагдсан өгөгдлийг авдаг.

Дараа жил гэхэд цөм нь C хэл дээр дахин бичигдсэн байсан ба хоолой (2) дөрөв дэх хэвлэл прототипээр орчин үеийн дүр төрхийг олж авсан "pipe(fildes)"

Системийн дуудлага хоолой дамжуулах хоолой гэж нэрлэгддэг оролт гаралтын механизмыг бий болгодог. Буцаагдсан файлын тодорхойлогчдыг унших, бичих үйлдэлд ашиглаж болно. Дамжуулах хоолойд ямар нэг зүйл бичих үед r1-д буцаасан тодорхойлогч (хариулт файлууд[1]) ашиглагдаж, 4096 байт хүртэл өгөгдөлд буферлэгдэж, дараа нь бичих үйл явц түр зогсдог. Дамжуулах хоолойноос унших үед r0 руу буцсан тодорхойлогч (хариулт fildes[0]) өгөгдлийг авдаг.

Дамжуулах хоолойг тодорхойлсны дараа хоёр (эсвэл түүнээс дээш) харилцан үйлчлэлийн процессууд (дараагийн дуудлагуудаар үүсгэгддэг) гэж үздэг. Салаа) дуудлага ашиглан дамжуулах хоолойноос өгөгдөл дамжуулах болно унш и бичих.

Бүрхүүл нь дамжуулах хоолойгоор холбогдсон процессуудын шугаман массивыг тодорхойлох синтакстай.

Зөвхөн нэг төгсгөлтэй (бүх бичих файлын тодорхойлогч хаалттай) хоосон дамжуулах хоолойноос унших дуудлага нь "файлын төгсгөл"-ийг буцаана. Үүнтэй төстэй нөхцөл байдалд дуудлага бичихийг үл тоомсорлодог.

Хамгийн эрт хадгалсан дамжуулах хоолойн хэрэгжилт хамаарна Unix-ийн тав дахь хэвлэлд (1974 оны XNUMX-р сар), гэхдээ энэ нь дараагийн хувилбарт гарсантай бараг ижил юм. Зөвхөн сэтгэгдэл нэмсэн тул тав дахь хэвлэлийг алгасаж болно.

Unix зургаа дахь хэвлэл (1975)

Unix эх кодыг уншиж эхэлж байна зургаа дахь хэвлэл (1975 оны XNUMX-р сар). -д их баярлалаа арслан Энэ нь өмнөх хувилбаруудын эх сурвалжаас хамаагүй хялбар байдаг:

Олон жилийн турш ном арслан нь Bell Labs-аас гадуур байдаг Unix цөм дээрх цорын ганц баримт бичиг байсан. Зургаа дахь хэвлэлтийн лиценз нь багш нарт эх кодыг ашиглах боломжийг олгосон ч долоо дахь хэвлэлтийн лиценз нь энэ боломжийг хассан тул номыг хууль бусаар бичгийн машинаар хэвлэсэн байна.

Өнөөдөр та хувилагч машин дээр оюутнуудыг дүрсэлсэн номын хуулбарыг худалдаж авах боломжтой. Мөн Уоррен Тоомей (TUHS төслийг эхлүүлсэн) ачаар та татаж авах боломжтой Зургаа дахь хэвлэл эх сурвалж PDF. Би танд файл үүсгэхэд хичнээн их хүчин чармайлт гаргасан тухай санаа өгөхийг хүсч байна.

15 гаруй жилийн өмнө би энд өгсөн эх кодын хуулбарыг шивсэн арсланУчир нь үл мэдэгдэх тооны бусад хуулбараас миний хуулбарын чанар надад таалагдаагүй. TUHS хараахан байхгүй байсан бөгөөд би хуучин эх сурвалжид хандах боломжгүй байсан. Гэхдээ 1988 онд би PDP9 компьютерээс нөөц хуулбартай байсан 11 замтай хуучин соронзон хальс олсон. Энэ нь ажиллаж байгаа эсэхийг мэдэхэд хэцүү байсан ч ихэнх файлууд нь 1979 гэж тэмдэглэгдсэн бүрэн бүтэн /usr/src/ мод байсан бөгөөд тэр үед ч эртний мэт харагддаг. Энэ бол долоо дахь хэвлэл буюу PWB-ийн дериватив байсан гэж би бодсон.

Би олдворыг үндэс болгон эх сурвалжийг зургаа дахь хэвлэлд гар аргаар засварлав. Кодын нэг хэсэг нь хэвээр үлдэж, хэсэг нь бага зэрэг засварлах шаардлагатай болж, орчин үеийн токен += хуучирсан =+ болж өөрчлөгдсөн. Ямар нэг зүйлийг зүгээр л устгасан бөгөөд ямар нэг зүйлийг бүрэн дахин бичих шаардлагатай байсан, гэхдээ тийм ч их биш.

Мөн өнөөдөр бид TUHS-ээс зургаа дахь хэвлэлийн эх кодыг онлайнаар уншиж болно Деннис Ричигийн гарт байсан архив.

Дашрамд хэлэхэд, Керниган, Ричи нарын үеэс өмнөх С кодын гол онцлог нь түүний товчлол. Би сайтынхаа харьцангуй нарийхан дэлгэцийн хэсэгт багтаахын тулд өргөн хүрээтэй засвар хийлгүйгээр кодын хэсгүүдийг оруулах боломжгүй байдаг.

Эхэндээ /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 файлуудын хувьд тэдгээр нь таарч байна inode-туг 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 системийн дуудлагын параметрүүд болон буцах утгуудыг дамжуулсан.

Хамт оролдъё ialloc() диск дээр байрлуулах inode (inode), мөн тусламжтайгаар falloc() - хоёрыг хадгалах файл. Хэрэв бүх зүйл зүгээр болвол бид эдгээр файлыг дамжуулах хоолойн хоёр төгсгөл гэж тодорхойлох тугуудыг байрлуулж, тэдгээрийг ижил иноод руу (түүний лавлагааны тоо 2 болно) зааж, инодыг өөрчлөгдсөн, ашиглаж байгаа гэж тэмдэглэнэ. гэсэн хүсэлтэд анхаарлаа хандуулаарай оруулах() шинэ inode дахь лавлагааны тоог багасгахын тулд алдааны замд.

pipe() дамжуулан төлөх ёстой R0 и R1 Унших, бичихэд зориулсан файлын тодорхойлогч дугаарыг буцаана. falloc() файлын бүтцэд заагчийг буцаана, гэхдээ мөн дамжуулан "буцах" u.u_ar0[R0] болон файлын тодорхойлогч. Энэ нь кодыг хадгалдаг гэсэн үг юм r унших файлын тодорхойлогч бөгөөд шууд бичих тодорхойлогчийг оноож өгдөг u.u_ar0[R0] хоёр дахь дуудлагын дараа falloc().

Flag FPIPEДамжуулах хоолой үүсгэх үед бидний тохируулсан , функцийн үйл ажиллагааг хянадаг sys2.c дахь rdwr(), энэ нь тодорхой 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).

Дараа нь бид inode лавлагааны тоог шалгана. Дамжуулах хоолойн хоёр төгсгөл нээлттэй хэвээр байгаа бол тоолуур нь 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(). Тэдгээр нь хэрэгжиж байна 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);
}

Энэ функцийг доороос дээш уншихад танд илүү хялбар санагдаж магадгүй. Дамжуулах хоолойд зарим өгөгдөл байгаа үед "унших ба буцах" салбарыг ихэвчлэн ашигладаг. Энэ тохиолдолд бид ашигладаг унших() одоогийнхоос аль болох их өгөгдлийг уншина уу f_offset уншиж, дараа нь харгалзах офсетийн утгыг шинэчилнэ үү.

Дараагийн уншилтууд дээр уншсан офсетэд хүрсэн бол дамжуулах хоолой хоосон байх болно i_size1 inode дээр. Бид байрлалыг 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() мөчлөгийг дахин эхлүүлнэ.

Энэ нь зургаа дахь хэвлэлд дамжуулах хоолойн тодорхойлолтыг дуусгаж байна. Энгийн код, өргөн хүрээний үр дагавар.

Unix-ийн долоо дахь хэвлэл (1979 оны XNUMX-р сар) нь олон шинэ програмууд болон цөмийн функцуудыг нэвтрүүлсэн шинэ томоохон хувилбар (дөрвөн жилийн дараа) байсан. Энэ нь мөн төрлийн цутгамал, нэгдлүүд болон бүтцэд бичигдсэн заагчийг ашиглахтай холбоотойгоор ихээхэн өөрчлөлт орсон. Гэсэн хэдий ч дамжуулах хоолойн код бараг өөрчлөгдөөгүй. Бид энэ хэвлэлийг алгасаж болно.

Xv6, Unix-тэй төстэй энгийн цөм

Цөм үүсгэхийн тулд Xv6 Unix-ийн зургаа дахь хэвлэлд нөлөөлсөн боловч x86 процессор дээр ажиллахаар орчин үеийн Си хэл дээр бичигдсэн. Код нь уншихад хялбар бөгөөд ойлгомжтой. Мөн TUHS-тэй Unix эх сурвалжаас ялгаатай нь та үүнийг эмхэтгэж, өөрчилж, PDP 11/70-ээс өөр зүйл дээр ажиллуулж болно. Иймээс энэхүү цөмийг их дээд сургуулиудад үйлдлийн системийн сургалтын хэрэглэгдэхүүн болгон өргөн ашигладаг. Эх сурвалжууд Github дээр байна.

Код нь тодорхой бөгөөд сайтар бодож хэрэгжүүлэлтийг агуулдаг хоолой.c, диск дээрх 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. Би түүний бүх кодыг уншихыг зөвлөж байна. Нарийн төвөгтэй байдал нь зургаа дахь хэвлэлийн эх кодын түвшинд байгаа боловч уншихад илүү хялбар бөгөөд илүү тааламжтай байдаг.

Линуксийн 0.01

Та Linux 0.01-ийн эх кодыг олох боломжтой. Түүний дотор дамжуулах хоолойн хэрэгжилтийг судлах нь сургамжтай байх болно fs/pipe.c. Энд дамжуулах хоолойг илэрхийлэхийн тулд inode ашигладаг, гэхдээ дамжуулах хоолой нь өөрөө орчин үеийн 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;
}

Бүтцийн тодорхойлолтыг харалгүйгээр та бичих үйлдлээр үр дүнд хүрсэн эсэхийг шалгахын тулд inode лавлагааны тоо хэрхэн ашиглагддагийг олж мэдэх боломжтой. SIGPIPE. Байтаар ажиллахаас гадна энэ функцийг дээрх санаануудтай харьцуулахад хялбар байдаг. Бүр логик sleep_on/wake_up тийм ч харь харагдахгүй байна.

Орчин үеийн Линуксийн цөм, FreeBSD, NetBSD, OpenBSD

Би зарим нэг орчин үеийн цөмтэй танилцсан. Тэдгээрийн аль нь ч аль хэдийн диск дээр суурилсан хэрэгжилтгүй (гайхах зүйл биш). Линукс нь өөрийн гэсэн хэрэгжилттэй байдаг. Хэдийгээр орчин үеийн гурван BSD цөм нь Жон Дайсоны бичсэн код дээр суурилсан хэрэгжилтийг агуулж байгаа ч олон жилийн туршид тэд бие биенээсээ хэт ялгаатай болсон.

Унших fs/pipe.c (Линукс дээр) эсвэл sys/kern/sys_pipe.c (*BSD дээр), энэ нь жинхэнэ зориулалт шаарддаг. Вектор болон асинхрон оролт/гаралт зэрэг функцүүдийн гүйцэтгэл, дэмжлэг нь өнөөдөр кодын хувьд чухал юм. Мөн санах ойн хуваарилалт, түгжээ, цөмийн тохиргооны нарийвчилсан мэдээлэл бүгд өөр өөр байдаг. Энэ нь их дээд сургуулиудад үйлдлийн системийн анхан шатны сургалтанд хэрэггүй.

Ямар ч байсан, хэд хэдэн хуучин хэв маягийг (жишээ нь, үүсгэх SIGPIPE мөн буцах EPIPE хаалттай дамжуулах хоолой руу бичих үед) эдгээр бүх, маш өөр, орчин үеийн цөмд. Би хэзээ ч PDP-11 компьютерийг амьдаар нь харахгүй байх, гэхдээ намайг төрөхөөс хэдэн жилийн өмнө бичсэн кодоос сурах зүйл их байна.

Диви Капурын 2011 онд бичсэн " нийтлэл.Линуксийн цөмийн хоолой ба FIFO-ийн хэрэгжилтЛинукс дамжуулах хоолой (одоо хүртэл) хэрхэн ажилладаг тухай тойм юм. А Линукс дээр саяхан хийсэн чадвар нь түр зуурын файлуудынхаас давсан харилцан үйлчлэлийн дамжуулах хоолойн загварыг харуулсан; Мөн түүнчлэн зургадугаар хэвлэлт Unix цөмд дамжуулах хоолой нь "маш консерватив түгжээ"-ээс хэр хол явсныг харуулж байна.

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх