نحوه پیاده سازی خطوط لوله در یونیکس

نحوه پیاده سازی خطوط لوله در یونیکس
این مقاله پیاده سازی خطوط لوله در هسته یونیکس را شرح می دهد. من تا حدودی ناامید شدم از یک مقاله اخیر با عنوان "خطوط لوله در یونیکس چگونه کار می کنند؟"مشخص شد هیچ در مورد ساختار داخلی کنجکاو شدم و منابع قدیمی را برای یافتن پاسخ جستجو کردم.

ما در مورد چه چیزی صحبت می کنیم؟

خطوط لوله، «احتمالاً مهم‌ترین اختراع در یونیکس»، یک ویژگی مشخص از فلسفه زیربنایی یونیکس در پیوند دادن برنامه‌های کوچک به یکدیگر، و همچنین یک علامت آشنا در خط فرمان است:

$ 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()برای دریافت توصیفگرهای فایل نصب شده یک پردازش فرزند در یک دسته می نویسد و فرآیند دیگر همان داده ها را از دسته دیگر می خواند. پوسته از dup2 برای "تغییر نام" توصیفگرهای 3 و 4 برای مطابقت با 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) اولین نسخه با نوار نقاله شد:

نسخه سوم یونیکس آخرین نسخه با هسته ای بود که به زبان اسمبلی نوشته شده بود، اما همچنین اولین نسخه با خط لوله بود. در طول سال 1973، کار برای بهبود نسخه سوم انجام شد، هسته به زبان C بازنویسی شد و بنابراین نسخه چهارم یونیکس ظاهر شد.

یکی از خوانندگان اسکن سندی را پیدا کرد که در آن داگ مک‌ایلروی ایده «اتصال برنامه‌ها مانند شلنگ باغ» را مطرح کرد.

نحوه پیاده سازی خطوط لوله در یونیکس
در کتاب برایان کرنیگانیونیکس: یک تاریخ و یک خاطرهدر تاریخ پیدایش نوار نقاله ها به این سند نیز اشاره شده است: «... به مدت 30 سال در دفتر من در آزمایشگاه بل به دیوار آویزان بود». اینجا مصاحبه با مک ایلروی، و داستان دیگری از اثر مک ایلروی که در سال 2014 نوشته شده است:

هنگامی که یونیکس منتشر شد، شیفتگی من به برنامه‌های معمول باعث شد از نویسنده سیستم‌عامل، کن تامپسون، بخواهم که اجازه دهد داده‌های نوشته‌شده در یک فرآیند نه تنها به دستگاه برود، بلکه به فرآیند دیگری نیز خروجی دهد. کن تصمیم گرفت که این امکان پذیر است. با این حال، به عنوان یک مینیمالیست، او می خواست هر عملکرد سیستم نقش مهمی ایفا کند. آیا نوشتن مستقیم بین فرآیندها واقعاً مزیت بزرگی نسبت به نوشتن در یک فایل میانی است؟ تنها زمانی که یک پیشنهاد خاص با نام جذاب «خط لوله» و توصیفی از نحو برای تعامل بین فرآیندها ارائه دادم که کن در نهایت فریاد زد: «این کار را انجام خواهم داد!»

و انجام داد. یک غروب سرنوشت‌ساز، کن هسته و پوسته را تغییر داد، چندین برنامه استاندارد را برای استاندارد کردن نحوه پذیرش ورودی (که می‌تواند از یک خط لوله باشد) اصلاح کرد و همچنین نام فایل‌ها را تغییر داد. روز بعد، خطوط لوله شروع به استفاده بسیار گسترده در برنامه های کاربردی کردند. در پایان هفته، منشی ها از آنها برای ارسال اسناد از پردازشگرهای کلمه به چاپگر استفاده می کردند. کمی بعد، کن API و نحو اصلی را برای بسته بندی استفاده از خطوط لوله با قراردادهای تمیزتر جایگزین کرد، که از آن زمان تاکنون مورد استفاده قرار گرفته است.

متأسفانه کد منبع نسخه سوم هسته یونیکس از بین رفته است. و اگرچه ما کد منبع هسته را داریم که به زبان C نوشته شده است ویرایش چهارم، در نوامبر 1973 منتشر شد، اما چندین ماه قبل از انتشار رسمی منتشر شد و شامل اجرای خط لوله نیست. شرم آور است که کد منبع این تابع افسانه ای یونیکس، شاید برای همیشه از بین رفته است.

ما اسناد متنی برای pipe(2) از هر دو نسخه، بنابراین می توانید با جستجوی اسناد شروع کنید ویرایش سوم (برای برخی از کلمات، زیر «دستی»، رشته‌ای از حروف تحت اللفظی ^H، به دنبال آن زیرخط!). این پروتوpipe(2) به زبان اسمبلی نوشته شده است و تنها یک توصیفگر فایل را برمی گرداند، اما از قبل عملکرد اصلی مورد انتظار را ارائه می دهد:

تماس سیستمی لوله یک مکانیزم ورودی/خروجی به نام خط لوله ایجاد می کند. توصیفگر فایل برگشتی می تواند برای عملیات خواندن و نوشتن استفاده شود. هنگامی که چیزی در خط لوله نوشته می شود، تا 504 بایت داده بافر می شود و پس از آن فرآیند نوشتن به حالت تعلیق در می آید. هنگام خواندن از خط لوله، داده های بافر حذف می شوند.

در سال بعد، هسته در C بازنویسی شد و لوله (2) در ویرایش چهارم ظاهر مدرن خود را با نمونه اولیه به دست آورد "pipe(fildes)»:

تماس سیستمی لوله یک مکانیزم ورودی/خروجی به نام خط لوله ایجاد می کند. توصیفگرهای فایل برگشتی را می توان در عملیات خواندن و نوشتن استفاده کرد. وقتی چیزی در خط لوله نوشته می‌شود، دسته‌ای که در r1 برگردانده می‌شود (مثلاً fildes[1]) استفاده می‌شود، در 4096 بایت داده بافر می‌شود و پس از آن فرآیند نوشتن به حالت تعلیق در می‌آید. هنگام خواندن از خط لوله، دسته ای که به r0 بازگشته است (مثلاً fildes[0]) داده ها را می گیرد.

فرض بر این است که پس از تعریف خط لوله، دو (یا بیشتر) فرآیندهای ارتباطی (ایجاد شده توسط تماس های بعدی به چنگال) داده ها را از خط لوله با استفاده از تماس ها منتقل می کند خواندن и نوشتن.

پوسته دارای نحوی برای تعریف یک آرایه خطی از فرآیندهای متصل شده توسط یک خط لوله است.

فراخوانی‌ها برای خواندن از یک خط لوله خالی (حاوی داده‌های بافری) که فقط یک انتها دارد (همه توصیف‌گرهای فایل نوشتاری بسته هستند) "انتهای فایل" را برمی‌گرداند. تماس‌ها برای نوشتن در شرایط مشابه نادیده گرفته می‌شوند.

اولین اجرای خط لوله حفظ شده اعمال می شود به ویرایش پنجم یونیکس (ژوئن 1974)، اما تقریباً مشابه نسخه ای است که در نسخه بعدی ظاهر شد. نظرات به تازگی اضافه شده است، بنابراین می توانید نسخه پنجم را رد کنید.

ویرایش ششم یونیکس (1975)

بیایید شروع به خواندن کد منبع یونیکس کنیم چاپ ششم (مه 1975). تا حد زیادی به لطف شیرها یافتن آن بسیار ساده تر از منابع نسخه های قبلی است:

برای سالهای زیادی کتاب شیرها تنها سند موجود در هسته یونیکس خارج از آزمایشگاه بل بود. اگرچه مجوز ویرایش ششم به معلمان اجازه می داد از کد منبع آن استفاده کنند، مجوز ویرایش هفتم این امکان را رد می کرد، بنابراین کتاب در قالب نسخه های تایپی غیرقانونی توزیع شد.

امروز می‌توانید نسخه‌ای از کتاب را بخرید که جلد آن دانش‌آموزان را در دستگاه کپی نشان می‌دهد. و به لطف وارن تومی (که پروژه TUHS را شروع کرد) می توانید دانلود کنید فایل پی دی اف با کد منبع برای ویرایش ششم. می‌خواهم به شما ایده بدهم که چقدر برای ایجاد فایل تلاش کرده‌اید:

بیش از 15 سال پیش، من یک کپی از کد منبع داده شده را تایپ کردم شیرها، زیرا کیفیت کپی خود را از تعداد نامعلومی از نسخه های دیگر دوست نداشتم. TUHS هنوز وجود نداشت و من به منابع قدیمی دسترسی نداشتم. اما در سال 1988، یک نوار قدیمی 9 آهنگی پیدا کردم که حاوی یک نسخه پشتیبان از یک کامپیوتر PDP11 بود. تشخیص اینکه آیا کار می کند یا نه سخت بود، اما یک درخت دست نخورده /usr/src/ وجود داشت که در آن بیشتر فایل ها با سال 1979 برچسب گذاری شده بودند، که حتی در آن زمان نیز قدیمی به نظر می رسید. همانطور که من معتقد بودم، این نسخه هفتم یا مشتق PWB بود.

من یافته را به عنوان مبنا قرار دادم و منابع را به صورت دستی به ویرایش ششم ویرایش کردم. برخی از کدها به همان صورت باقی ماندند، اما برخی از آنها باید کمی ویرایش می شدند و علامت += مدرن را به =+ منسوخ شده تغییر داد. برخی چیزها به سادگی حذف شدند و برخی باید به طور کامل بازنویسی می شدند، اما نه بیش از حد.

و امروز می‌توانیم به صورت آنلاین در TUHS کد منبع نسخه ششم را بخوانیم آرشیوی که دنیس ریچی به آن دست داشت.

به هر حال، در نگاه اول، ویژگی اصلی C-code قبل از دوره کرنیگان و ریچی، آن است. اختصار. خیلی اوقات پیش نمی آید که بتوانم تکه هایی از کد را بدون ویرایش گسترده وارد کنم تا فضای نمایش نسبتاً باریکی را در سایت خود داشته باشم.

در اوایل /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 user u» و ثبت نام می کند R0 и R1 پارامترهای فراخوانی سیستم و مقادیر بازگشتی ارسال می شوند.

بیایید سعی کنیم با ialloc() روی دیسک قرار دهید inode (دسته شاخص)، و با کمک falloc() - جای دو در حافظه فایل. اگر همه چیز به خوبی پیش برود، ما پرچم هایی را برای شناسایی این فایل ها به عنوان دو انتهای خط لوله تنظیم می کنیم، آنها را به همان inode (که تعداد مراجع روی 2 تنظیم می شود) نشان می دهیم و inode را به عنوان تغییر یافته و در حال استفاده علامت گذاری می کنیم. به درخواست ها توجه کنید قرار دادم() در مسیرهای خطا برای کاهش تعداد مراجع در inode جدید.

pipe() باید از طریق R0 и R1 اعداد توصیفگر فایل را برای خواندن و نوشتن بازگردانید. falloc() یک اشاره گر را به ساختار فایل برمی گرداند، اما از طریق "برمی گرداند". u.u_ar0[R0] و یک توصیفگر فایل یعنی کد در آن ذخیره می شود r توصیفگر فایل برای خواندن و اختصاص یک توصیفگر فایل برای نوشتن مستقیم از u.u_ar0[R0] بعد از تماس دوم falloc().

فلگ FPIPE، که هنگام ایجاد خط لوله تنظیم می کنیم، رفتار تابع را کنترل می کند rdwr() در sys2.cفراخوانی روتین های خاص I/O:

/*
 * common code for read and write calls:
 * check permissions, set base, count, and offset,
 * and switch out to readi, writei, or pipe code.
 */
rdwr(mode)
{
    register *fp, m;

    m = mode;
    fp = getf(u.u_ar0[R0]);
        /* … */

    if(fp->f_flag&FPIPE) {
        if(m==FREAD)
            readp(fp); else
            writep(fp);
    }
        /* … */
}

سپس تابع readp() в pipe.c داده ها را از خط لوله می خواند. اما بهتر است پیاده سازی را از ابتدا دنبال کنید writep(). باز هم، کد به دلیل قراردادهای گذراندن آرگومان ها پیچیده تر شده است، اما برخی از جزئیات را می توان حذف کرد.

writep(fp)
{
    register *rp, *ip, c;

    rp = fp;
    ip = rp->f_inode;
    c = u.u_count;

loop:
    /* If all done, return. */

    plock(ip);
    if(c == 0) {
        prele(ip);
        u.u_count = 0;
        return;
    }

    /*
     * If there are not both read and write sides of the
     * pipe active, return error and signal too.
     */

    if(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        return;
    }

    /*
     * If the pipe is full, wait for reads to deplete
     * and truncate it.
     */

    if(ip->i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        goto loop;
    }

    /* Write what is possible and loop back. */

    u.u_offset[0] = 0;
    u.u_offset[1] = ip->i_size1;
    u.u_count = min(c, PIPSIZ-u.u_offset[1]);
    c =- u.u_count;
    writei(ip);
    prele(ip);
    if(ip->i_mode&IREAD) {
        ip->i_mode =& ~IREAD;
        wakeup(ip+2);
    }
    goto loop;
}

می خواهیم بایت ها را در ورودی خط لوله بنویسیم u.u_count. ابتدا باید inode را قفل کنیم (به زیر مراجعه کنید plock/prele).

سپس شمارنده مرجع inode را بررسی می کنیم. تا زمانی که هر دو انتهای خط لوله باز بماند، شمارنده باید برابر با 2 باشد. ما یک پیوند را نگه می داریم (از rp->f_inode، بنابراین اگر شمارنده کمتر از 2 باشد، باید به این معنی باشد که فرآیند خواندن انتهای خط لوله را بسته است. به عبارت دیگر، ما سعی می کنیم در یک خط لوله بسته بنویسیم و این یک خطا است. کد خطای بار اول EPIPE و سیگنال SIGPIPE در نسخه ششم یونیکس ظاهر شد.

اما حتی اگر نوار نقاله باز باشد، ممکن است پر باشد. در این صورت قفل را رها کرده و به خواب می رویم به این امید که فرآیند دیگری از خط لوله خوانده شود و فضای کافی در آن آزاد شود. پس از بیدار شدن، به ابتدا باز می گردیم، قفل را دوباره آویزان می کنیم و یک چرخه ضبط جدید را شروع می کنیم.

اگر فضای خالی کافی در خط لوله وجود داشته باشد، با استفاده از آن داده ها را روی آن می نویسیم 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 در inode. ما موقعیت را به 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) نسخه بزرگ جديدي بود (چهار سال بعد) كه برنامه هاي جديد و ويژگي هاي هسته را معرفي كرد. همچنین در ارتباط با استفاده از نوع ریخته گری، اتحادیه ها و اشاره گرهای تایپ شده به سازه ها، دستخوش تغییرات قابل توجهی شد. با این حال کد نوار نقاله عملا بدون تغییر می توانیم از این نسخه بگذریم.

Xv6، یک هسته ساده یونیکس مانند

برای ایجاد هسته Xv6 تحت تأثیر نسخه ششم یونیکس، اما به زبان C مدرن برای اجرا بر روی پردازنده های x86 نوشته شده است. کد به راحتی قابل خواندن و قابل درک است. به علاوه، برخلاف منابع یونیکس با TUHS، می‌توانید آن را کامپایل کنید، آن را تغییر دهید، و آن را روی چیزی غیر از PDP 11/70 اجرا کنید. از این رو این هسته در دانشگاه ها به عنوان مطالب آموزشی در مورد سیستم عامل ها بسیار مورد استفاده قرار می گیرد. منابع در Github هستند.

کد حاوی یک پیاده سازی واضح و متفکرانه است لوله.ج، به جای 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

کد منبع لینوکس 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)، فداکاری واقعی می خواهد. کد امروز در مورد عملکرد و پشتیبانی از ویژگی هایی مانند بردار و I/O ناهمزمان است. و جزئیات تخصیص حافظه، قفل ها و پیکربندی هسته همگی بسیار متفاوت هستند. این چیزی نیست که کالج ها برای یک دوره مقدماتی سیستم عامل نیاز دارند.

به هر حال، من علاقه مند بودم برخی از الگوهای قدیمی (مانند تولید) را کشف کنم SIGPIPE و برگشت EPIPE هنگام نوشتن در یک خط لوله بسته) در تمام این هسته های مختلف مدرن. من احتمالا هرگز کامپیوتر PDP-11 را در زندگی واقعی نخواهم دید، اما هنوز چیزهای زیادی برای یادگیری از کدهایی وجود دارد که سال ها قبل از تولد من نوشته شده است.

مقاله ای که توسط دیوی کاپور در سال 2011 نوشته شده است:پیاده سازی هسته لینوکس لوله ها و FIFO" یک نمای کلی از نحوه عملکرد خطوط لوله (هنوز) در لینوکس ارائه می دهد. آ کامیت اخیر در لینوکس یک مدل خط لوله از تعامل را نشان می دهد که قابلیت های آن از فایل های موقت فراتر می رود. و همچنین نشان می دهد که خطوط لوله تا چه حد از "قفل کردن بسیار محافظه کارانه" هسته یونیکس ویرایش ششم فاصله داشته اند.

منبع: www.habr.com

اضافه کردن نظر