كيف يتم تنفيذ خطوط أنابيب يونكس


توضح هذه المقالة تنفيذ خطوط الأنابيب في نواة يونكس. شعرت بخيبة أمل إلى حد ما مع مقال حديث بعنوان " كيف تعمل خطوط الأنابيب على يونكس؟" " لم يكن عن الجهاز الداخلي. أصبحت مهتمة ، ودفنت نفسي في المصادر القديمة لأجد الإجابة.

عن ماذا نتحدث؟


خطوط الأنابيب - "ربما يكون أهم اختراع في يونكس" - هي السمة المميزة لفلسفة يونكس الكامنة في الجمع بين البرامج الصغيرة معًا ، وكذلك سطر الأوامر المألوف:

$ 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, "hello\n", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hello\n", 16384) = 6
[pid 2604796] write(1, "6\n", 2)        = 2

تستدعي العملية الرئيسية pipe()للحصول على واصفات الملفات المرفقة. يكتب أحد العمليات التابعة إلى واصف واحد وقراءة عملية أخرى نفس البيانات من واصف آخر. المجمع باستخدام واصفات "إعادة تسمية" Dup2 3 و 4 لمطابقة stdin و stdout.

بدون خطوط الأنابيب ، سيتعين على shell كتابة نتيجة عملية واحدة إلى ملف ونقلها إلى عملية أخرى بحيث تقرأ البيانات من الملف. ونتيجة لذلك ، سننفق المزيد من الموارد ومساحة القرص. ومع ذلك ، فإن خطوط الأنابيب جيدة ليس فقط لأنها تتجنب استخدام الملفات المؤقتة:

, read(2) , . , write(2) , .

مثل متطلبات POSIX ، هذه خاصية مهمة: الكتابة إلى خط الأنابيب حتى PIPE_BUFبايت (على الأقل 512) يجب أن تكون ذرية بحيث يمكن للعمليات التواصل مع بعضها البعض عبر خط الأنابيب بنفس الطريقة التي يمكن للملفات العادية (التي لا توفر مثل هذه الضمانات).

عند استخدام ملف عادي ، يمكن للعملية كتابة جميع بيانات الإخراج الخاصة بها ونقلها إلى عملية أخرى. أو يمكن أن تعمل العمليات في وضع موازٍ صعب ، باستخدام آلية إشارات خارجية (مثل إشارة الإشارة) لإبلاغ بعضها البعض بشأن إكمال الكتابة أو القراءة. ينقذنا الناقلون كل هذه المشاكل.

ما الذي تبحث عنه؟


سأشرح ذلك على أصابعي لتسهيل عليك أن تتخيل كيف يمكن أن يعمل الناقل. ستحتاج إلى تخصيص مخزن مؤقت وبعض الحالات في الذاكرة. ستحتاج إلى وظائف لإضافة البيانات وإزالتها من المخزن المؤقت. سوف يستغرق الأمر بعض الوسائل لاستدعاء الوظائف أثناء عمليات القراءة والكتابة إلى واصفات الملفات. والأقفال مطلوبة لتنفيذ السلوك الخاص الموصوف أعلاه.

الآن نحن على استعداد للاستجواب في الضوء الساطع للمصابيح الكود المصدر للنواة لتأكيد أو دحض نموذجنا العقلي الغامض. لكن كن مستعدًا دائمًا لما هو غير متوقع.

أين ننظر؟


لا أعرف أين توجد نسختي من كتاب " Lions book " الشهير مع شفرة مصدر Unix 6 ، ولكن بفضل جمعية Unix Heritage يمكنك البحث عبر الإنترنت عن إصدارات أقدم من Unix في شفرة المصدر .

إن التجول في أرشيفات TUHS يشبه زيارة متحف. يمكننا إلقاء نظرة على تاريخنا المشترك ، وأنا أحترم سنوات عديدة من الجهود لاستعادة كل هذه المواد شيئًا فشيئًا من الأشرطة والمطبوعات القديمة. وأنا مدرك تمامًا لتلك الأجزاء التي لا تزال مفقودة.

بعد إرضاء فضولنا فيما يتعلق بالتاريخ القديم للناقلات ، يمكننا أن ننظر إلى النوى الحديثة للمقارنة.

بالمناسبة ، pipeهو رقم استدعاء النظام 42 في الجدول sysent[]. صدفة؟

نواة يونكس التقليدية (1970-1974)


لم أجد أي أثر pipe(2)سواء في PDP-7 Unix (يناير 1970) ، أو في الإصدار الأول من Unix (نوفمبر 1971) ، أو في شفرة المصدر غير المكتملة للطبعة الثانية (يونيو 1972).

تدعي TUHS أن الإصدار الثالث من Unix (فبراير 1973) كان الإصدار الأول مع خطوط الأنابيب:

كانت النسخة الثالثة من Unix أحدث إصدار مع نواة مكتوبة بلغة التجميع ، ولكن الإصدار الأول مع خطوط الأنابيب. خلال عام 1973 ، تم العمل على تحسين الإصدار الثالث ، وتم إعادة كتابة الأساسية في C ، وهكذا ظهرت الطبعة الرابعة من Unix.

وجد أحد القراء مسحًا لوثيقة اقترح فيها دوغ ماكلروي فكرة "ربط البرامج بمبدأ خرطوم الحديقة".


في كتاب Brian Kernighan " Unix: A History and Memoir " ، في تاريخ ظهور الناقلات ، تم ذكر هذه الوثيقة أيضًا: "... علقت على الحائط في مكتبي في Bell Labs لمدة 30 عامًا." إليك مقابلة مع McIlroy ، وقصة أخرى من عمل McIlroy مكتوبة في عام 2014 :

Unix, , , , - , , . , . , , , . ? «» , , -, : « !».

. , , ( ), . . . API , .

لسوء الحظ ، يتم فقدان شفرة مصدر kernel للطبعة الثالثة من Unix. وعلى الرغم من أن لدينا شفرة المصدر للنسخة الرابعة من النواة المكتوبة في C ، والتي تم إصدارها في نوفمبر 1973 ، فقد تم إصدارها قبل بضعة أشهر من الإصدار الرسمي ولا تحتوي على تطبيقات خط الأنابيب. إنه لأمر مؤسف أن يتم فقدان التعليمات البرمجية المصدر لوظيفة Unix الأسطورية ، ربما إلى الأبد.

لدينا نص الوثائق pipe(2)من كلا الإصدارين ، لذا يمكنك البدء بالبحث في الإصدار الثالث من الوثائق (بالنسبة لبعض الكلمات ، التي تحتها خط "يدويًا" ، سلسلة من الحرف الحرفية ^ H ، متبوعة بشرطة سفلية!). تمت كتابة هذا الإصدار الأولي في أداة pipe(2)التجميع وإرجاع واصف ملف واحد فقط ، ولكنه يوفر بالفعل الوظائف الأساسية المتوقعة:

يقوم استدعاء نظام الأنابيب بإنشاء آلية إدخال الإخراج تسمى خط الأنابيب. يمكن استخدام واصف الملف الذي تم إرجاعه لعمليات القراءة والكتابة. عندما يتم كتابة شيء ما على خط الأنابيب ، يتم تخزين ما يصل إلى 504 بايت من البيانات ، وبعد ذلك يتم إيقاف عملية الكتابة مؤقتًا. عند القراءة من خط الأنابيب ، يتم أخذ البيانات المخزنة مؤقتًا.

بحلول العام التالي ، تم إعادة كتابة النواة بالحرف C ، ووجد الأنبوب (2) في الطبعة الرابعة مظهره الحديث مع النموذج الأولي " pipe(fildes)":

pipe , . . - , , r1 (. fildes[1]), 4096 , . , r0 (. fildes[0]), .

, ( ) ( fork) read write.

يحتوي الغلاف على بناء جملة لتحديد مجموعة خطية من العمليات المتصلة عبر خط أنابيب.

مكالمات القراءة من خط أنابيب فارغ (لا يحتوي على بيانات مخزنة) لها نهاية واحدة فقط (جميع واصفات ملف الكتابة مغلقة) تُرجع "نهاية الملف". يتم تجاهل تسجيل المكالمات في موقف مماثل.

أقرب الباقين على قيد الحياة تنفيذ خط أنابيب يعود إلى الطبعة الخامسة من يونكس (يونيو 1974)، وإنما هو مطابق تقريبا لتلك التي ظهرت في الإصدار التالي. تمت إضافة التعليقات فقط ، لذلك يمكن تخطي الإصدار الخامس.

الطبعة السادسة من يونيكس (1975)


نبدأ في قراءة الكود المصدري لإصدار يونيكس السادس (مايو 1975) بفضل Lions إلى حد كبير ، فإن العثور عليه أسهل بكثير من التعليمات البرمجية المصدر للإصدارات السابقة:

لسنوات عديدة ، كانت الأسود وثيقة يونكس الأساسية الوحيدة المتاحة خارج أسوار Bell Labs. على الرغم من أن رخصة الإصدار السادس سمحت للمعلمين باستخدام شفرة المصدر الخاصة بها ، إلا أن رخصة الإصدار السابع استبعدت هذه الإمكانية ، لذلك تم توزيع الكتاب في شكل نسخ مطبوعة غير قانونية.

يمكنك اليوم شراء نسخة معاد طباعتها من الكتاب تظهر على غلافها آلة نسخ. وبفضل وارين تومي (الذي أطلق مشروع TUHS) ، يمكنك تنزيل ملف PDF برمز المصدر للطبعة السادسة . أريد أن أعطيك فكرة عن مقدار الجهد المبذول لإنشاء الملف:

15 , Lions, . TUHS , . 1988- 9 , PDP11. , , /usr/src/, 1979- , . PWB, .

. , , += =+. - , - , .

واليوم يمكننا أن نقرأ على الإنترنت على TUHS شفرة المصدر للطبعة السادسة من الأرشيف ، والتي كان لدينيس ريتشي يد لها .

بالمناسبة ، للوهلة الأولى ، فإن السمة الرئيسية لرمز C قبل فترة Kernigan و Richie هي إيجازها . في كثير من الأحيان ، أتمكن من تضمين مقتطفات الشفرة دون تحرير شامل لتناسب منطقة عرض ضيقة نسبيًا على موقعي.

في بداية /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 inode ، والتي تستخدمها "الخوارزمية عالية المعالجة" لمعالجة الكتل غير المباشرة لدعم أنظمة الملفات الأكبر حجمًا. بما أن كين قال أنه من الأفضل عدم استخدامها ، فسأقوم بكلماته بكل سرور.

هنا استدعاء النظام الحقيقي 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المعلمات نقل المكالمات النظام وعودة القيم.

دعونا نحاول استخدام ialloc () لوضع inode (inode ) على القرص ، واستخدام falloc () لوضع ملفين في الذاكرة . إذا كان كل شيء يسير على ما يرام ، فسوف نقوم بتعيين إشارات لتعريف هذه الملفات على أنها طرفي خط الأنابيب ، وتوجيهها إلى نفس inode (سيكون عدد مرجعها 2) ، ووضع علامة على inode على أنها تغيرت واستخدمت. انتبه للمكالمات إلى iput ()في مسارات الخطأ لتقليل العدد المرجعي في inode الجديد.

pipe()يجب من خلال R0وإعادة R1أرقام واصف الملف للقراءة والكتابة. falloc()إرجاع مؤشر إلى بنية الملف ، ولكن أيضًا "إرجاع" من خلال u.u_ar0[R0]واصف الملف. أي أن الشفرة تحفظ في rواصف ملف للقراءة وتعين واصفًا للكتابة مباشرة من u.u_ar0[R0]بعد المكالمة الثانية falloc(). تتحكم

العلامة FPIPEالتي قمنا بتعيينها عند إنشاء خط الأنابيب في سلوك وظيفة rdwr () في sys2.c ، والتي تستدعي إجراءات إدخال / إخراج محددة:

/*
 * 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. نحتاج أولاً إلى قفل Inode (انظر أدناه plock/ prele).

ثم تحقق من عدد مرجع inode. في حين أن طرفي خط الأنابيب لا يزالان مفتوحين ، يجب أن يكون العداد هو 2. نحتفظ بوصلة واحدة (خارج rp->f_inode) ، لذلك إذا كان العداد أقل من 2 ، فهذا يعني أن عملية القراءة قد أغلقت نهاية خط الأنابيب. بعبارة أخرى ، نحن نحاول الكتابة بخط أنابيب مغلق ، وهذا خطأ. ظهر رمز الخطأ EPIPEوالإشارة لأول SIGPIPEمرة في الإصدار السادس من Unix.

ولكن حتى لو كان الناقل مفتوحًا ، فقد يكون ممتلئًا. في هذه الحالة ، نزيل القفل ونذهب إلى النوم على أمل أن تتم قراءة عملية أخرى من خط الأنابيب وتحرير مساحة كافية فيه. بعد الاستيقاظ ، نعود إلى البداية ، ومرة ​​أخرى نحجب القفل ونبدأ دورة تسجيل جديدة.

إذا كانت هناك مساحة خالية كافية في خط الأنابيب ، فإننا نكتب البيانات إليها باستخدام writei () . تشير المعلمة i_size1في inode (مع خط أنابيب فارغ إلى 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);
}

قد تجد أنه من الأسهل قراءة هذه الوظيفة من الأسفل إلى الأعلى. عادة ما يستخدم فرع "القراءة والعودة" عندما يكون هناك بعض البيانات في خط الأنابيب. في هذه الحالة ، نستخدم readi () لقراءة أكبر قدر ممكن من البيانات بدءًا من f_offsetالقراءة الحالية ، ثم نقوم بتحديث قيمة الإزاحة المقابلة.

في عمليات القراءة اللاحقة ، سيكون خط الأنابيب فارغًا إذا وصل إزاحة القراءة إلى قيمة i_size1الإينود. نعيد تعيين الموضع إلى 0 ونحاول إيقاظ أي عملية تريد كتابتها إلى خط الأنابيب. نحن نعلم أنه عندما يكون الناقل ممتلئًا ، فإنه writep()سوف ينام ip+1. والآن بعد أن خط الأنابيب فارغ ، يمكننا إيقاظه حتى يستأنف دورة التسجيل.

إذا لم يكن هناك شيء للقراءة ، readp()فيمكنه وضع علم IREADوالنوم عليهip+2. نحن نعلم ما الذي سيوقظه writep()عندما يكتب بعض البيانات لخط الأنابيب. ستساعد

التعليقات على readi () و writei () على فهم أنه بدلاً من تمرير المعلمات من خلال " u" ، يمكننا معاملتها مثل وظائف الإدخال / الإخراج العادية ، والتي تأخذ ملفًا وموضعًا ومخزنًا مؤقتًا في الذاكرة وتحسب عدد وحدات البايت للقراءة أو الكتابة .

/*
 * 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()كتلة inode لطالما كانوا الانتهاء من العمل أو لا تحصل على النتيجة (أي قضية 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) كان الإصدار الرئيسي الجديد (بعد أربع سنوات) ، حيث ظهرت العديد من التطبيقات الجديدة وخصائص النواة. أيضا ، كانت هناك تغييرات كبيرة فيما يتعلق باستخدام نوع الصب ، union'ov والمؤشرات المكتوبة على الهياكل. ومع ذلك ، لم يتغير رمز خط الأنابيب كثيرًا. يمكننا تخطي هذه الطبعة.

Xv6 ، نواة بسيطة على شكل يونكس


أثرت النسخة السادسة من Unix على إنشاء Xv6 core ، ولكنها مكتوبة بلغة C الحديثة لتعمل على معالجات x86. الكود سهل القراءة ، إنه واضح. بالإضافة إلى ذلك ، على عكس مصادر Unix مع TUHS ، يمكنك تجميعها وتعديلها وتشغيلها على شيء آخر غير PDP 11/70. لذلك ، يستخدم هذا النواة على نطاق واسع في الجامعات كمواد تعليمية على أنظمة التشغيل. المصادر في جيثب .

يحتوي الكود على تطبيق واضح ومدروس جيدًا للأنبوب . 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لا يبدو غريبًا جدًا.

نواة لينكس الحديثة ، فري بي إس دي ، نت بي إس دي ، نواة أوبن بي إس دي


سرعان ما ذهبت إلى بعض النوى الحديثة. لا يوجد لدى أي منهم بالفعل تطبيق القرص (وليس من المستغرب). لينكس لديه تطبيقه الخاص. على الرغم من أن نواة BSD الثلاثة الحديثة تحتوي على تطبيقات تستند إلى التعليمات البرمجية التي كتبها جون دايسون ، إلا أنها على مر السنين أصبحت مختلفة تمامًا عن بعضها البعض.

لقراءة fs/ pipe.c(على Linux) أو sys/ kern/ sys_pipe.c(على * BSD) ، يلزم التفاني الحقيقي. إن الأداء والدعم لميزات مثل المتجه والإدخال / الإخراج غير المتزامن مهمان في التعليمات البرمجية اليوم. وتفاصيل تخصيص الذاكرة والأقفال وتكوين النواة - كل هذا مختلف تمامًا. ليس هذا ما تحتاجه الجامعات لدورة تمهيدية حول أنظمة التشغيل.

على أي حال ، كان من المثير للاهتمام بالنسبة لي اكتشاف العديد من الأنماط القديمة (على سبيل المثال ، توليد SIGPIPEوالعودة EPIPEعند الكتابة إلى خط أنابيب مغلق) في كل هذه النوى الحديثة المختلفة جدًا. ربما لن أرى أبدًا جهاز كمبيوتر حي PDP-11 ، ولكن لا يزال هناك الكثير لنتعلمه من الشفرة التي تمت كتابتها قبل بضع سنوات من ولادتي.

مقال كتبه Divi Kapoor في عام 2011 ، يقدم مقال " تنفيذ نواة Linux للأنابيب و FIFOs " نظرة عامة على كيفية عمل خطوط الأنابيب (حتى الآن) على Linux. و مؤخرا لينكس ارتكاب يوضح نموذج التفاعل عبر خط انابيب الذين تتجاوز قدرات الملفات المؤقتة القدرات. ويظهر أيضًا المدى الذي وصلت إليه خطوط الأنابيب من "القفل المحافظ للغاية" في الإصدار السادس من Unix kernel.

All Articles