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

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

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

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

$ echo hello | wc -c
6

تعتمد هذه الوظيفة على استدعاء النظام المقدم من kernel 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 على الأقل) ذرية بحيث يمكن للعمليات أن تتواصل مع بعضها البعض عبر خط الأنابيب بطريقة لا تستطيع الملفات العادية (التي لا توفر مثل هذه الضمانات).

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

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

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

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

اين نبحث؟

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

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

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

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

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

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

تدعي TUHS ذلك الطبعة الثالثة يونكس (فبراير 1973) كانت النسخة الأولى مع خطوط الأنابيب:

كانت النسخة الثالثة من Unix هي الإصدار الأخير مع نواة مكتوبة في المُجمِّع ، ولكن أيضًا الإصدار الأول مع خطوط الأنابيب. خلال عام 1973 ، كان العمل جاريًا لتحسين الإصدار الثالث ، وتمت إعادة كتابة النواة بلغة C ، وبالتالي ولدت النسخة الرابعة من Unix.

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

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

عندما ظهر Unix ، جعلني شغفي بالكوروتينات أطلب من مؤلف نظام التشغيل ، Ken Thompson ، السماح للبيانات المكتوبة لبعض العمليات بالانتقال ليس فقط إلى الجهاز ، ولكن أيضًا للخروج إلى عملية أخرى. اعتقد كين أن ذلك ممكن. ومع ذلك ، باعتباره الحد الأدنى ، أراد أن تلعب كل ميزة في النظام دورًا مهمًا. هل تعتبر الكتابة المباشرة بين العمليات ميزة كبيرة حقًا على الكتابة في ملف وسيط؟ وفقط عندما قدمت اقتراحًا محددًا باسم "خط الأنابيب" الجذاب ووصفًا لتركيب تفاعل العمليات ، صرخ كين أخيرًا: "سأفعل ذلك!".

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

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

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

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

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

استدعاء النظام أنبوب ينشئ آلية إدخال / إخراج تسمى خط أنابيب. يمكن استخدام واصفات الملفات المرتجعة في عمليات القراءة والكتابة. عند كتابة شيء ما في خط الأنابيب ، يتم استخدام الواصف الذي يتم إرجاعه في r1 (على التوالي fildes [1]) ، ويتم تخزينه مؤقتًا حتى 4096 بايت من البيانات ، وبعد ذلك يتم تعليق عملية الكتابة. عند القراءة من خط الأنابيب ، يعود الواصف إلى r0 (على التوالي. fildes [0]) يأخذ البيانات.

من المفترض أنه بمجرد تحديد خط الأنابيب ، هناك عمليتان (أو أكثر) متفاعلتان (تم إنشاؤهما بواسطة الاستدعاءات اللاحقة شوكة) سيمرر البيانات من خط الأنابيب باستخدام المكالمات اقرأ и اكتب.

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

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

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

إصدار يونكس السادس (1975)

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

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

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

منذ أكثر من 15 عامًا ، قمت بكتابة نسخة من شفرة المصدر المتوفرة في أسودلأنني لم تعجبني جودة نسختي من عدد غير معروف من النسخ الأخرى. لم تكن TUHS موجودة بعد ، ولم يكن لدي وصول إلى المصادر القديمة. لكن في عام 1988 عثرت على شريط قديم به 9 مسارات بها نسخة احتياطية من جهاز كمبيوتر PDP11. كان من الصعب معرفة ما إذا كانت تعمل ، ولكن كانت هناك شجرة / usr / src / سليمة تم فيها تمييز معظم الملفات عام 1979 ، والتي بدت قديمة حتى ذلك الحين. لقد كانت الطبعة السابعة ، أو مشتق من PWB ، على ما أعتقد.

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

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

بالمناسبة ، للوهلة الأولى ، فإن السمة الرئيسية للرمز 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 ، فهي تتوافق مع ملفات inode-flag 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 يتم تمرير معلمات استدعاء النظام وقيم الإرجاع.

دعونا نحاول مع ialloc () ضع على القرص inode (inode)وبمساعدة فالوك () - مخزن اثنين ملف. إذا سارت الأمور على ما يرام ، فسنقوم بتعيين علامات لتحديد هذه الملفات على أنها طرفي خط الأنابيب ، ونوجهها إلى نفس inode (الذي يصبح عدد مراجعها 2) ، ونضع علامة على inode على أنه معدّل وقيد الاستخدام. انتبه لطلبات وضعت() في مسارات الخطأ لتقليل عدد المرجع في 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 ظهر في الإصدار السادس من يونكس.

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

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

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

Xv6 ، نواة بسيطة تشبه يونكس

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

يحتوي الكود على تنفيذ واضح ومدروس الأنابيب ج، مدعومًا بمخزن مؤقت في الذاكرة بدلاً من 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. أوصي بقراءة كل الكود الخاص به. التعقيد على مستوى الكود المصدري للإصدار السادس ، ولكنه أسهل بكثير وأكثر متعة في القراءة.

لينكس شنومكس

يمكنك العثور على الكود المصدري لنظام 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 على الهواء مباشرة ، ولكن لا يزال هناك الكثير لنتعلمه من الكود الذي تمت كتابته قبل سنوات قليلة من ولادتي.

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

المصدر: www.habr.com

إضافة تعليق