این مقاله پیاده سازی خطوط لوله در هسته یونیکس را شرح می دهد. من تا حدودی ناامید شدم از یک مقاله اخیر با عنوان "
ما در مورد چه چیزی صحبت می کنیم؟
خطوط لوله، «احتمالاً مهمترین اختراع در یونیکس»، یک ویژگی مشخص از فلسفه زیربنایی یونیکس در پیوند دادن برنامههای کوچک به یکدیگر، و همچنین یک علامت آشنا در خط فرمان است:
$ echo hello | wc -c
6
این عملکرد به فراخوانی سیستم ارائه شده توسط هسته بستگی دارد pipe
، که در صفحات مستندات توضیح داده شده است
خطوط لوله یک کانال یک طرفه برای ارتباطات بین فرآیندی فراهم می کند. خط لوله دارای یک ورودی (پایان نوشتن) و یک خروجی (پایان خواندن) است. داده های نوشته شده در ورودی خط لوله را می توان در خروجی خواند.
خط لوله با استفاده از تماس ایجاد می شود
pipe(2)
، که دو توصیفگر فایل را برمی گرداند: یکی به ورودی خط لوله و دومی به خروجی اشاره دارد.
خروجی ردیابی از دستور بالا ایجاد خط لوله و جریان داده از طریق آن از یک فرآیند به فرآیند دیگر را نشان می دهد:
$ strace -qf -e execve,pipe,dup2,read,write
sh -c 'echo hello | wc -c'
execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4]) = 0
[pid 2604795] dup2(4, 1) = 1
[pid 2604795] write(1, "hellon", 6) = 6
[pid 2604796] dup2(3, 0) = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2) = 2
فرآیند والد تماس می گیرد pipe()
برای دریافت توصیفگرهای فایل نصب شده یک پردازش فرزند در یک دسته می نویسد و فرآیند دیگر همان داده ها را از دسته دیگر می خواند. پوسته از dup2 برای "تغییر نام" توصیفگرهای 3 و 4 برای مطابقت با stdin و stdout استفاده می کند.
بدون لولهها، پوسته باید نتیجه یک فرآیند را در یک فایل بنویسد و آن را به فرآیند دیگری ارسال کند تا دادهها را از فایل بخواند. در نتیجه، منابع و فضای دیسک بیشتری را هدر خواهیم داد. با این حال، خطوط لوله خوب هستند نه تنها به این دلیل که به شما امکان می دهند از استفاده از فایل های موقت خودداری کنید:
اگر فرآیندی سعی در خواندن از یک خط لوله خالی دارد، پس
read(2)
تا زمانی که داده ها در دسترس قرار گیرند مسدود می شود. اگر فرآیندی سعی کند در یک خط لوله کامل بنویسد، پسwrite(2)
تا زمانی که داده های کافی از خط لوله برای انجام نوشتن خوانده نشود مسدود می شود.
مانند نیاز POSIX، این یک ویژگی مهم است: نوشتن به خط لوله تا PIPE_BUF
بایت ها (حداقل 512) باید اتمی باشند تا فرآیندها بتوانند از طریق خط لوله به گونه ای با یکدیگر ارتباط برقرار کنند که فایل های معمولی (که چنین تضمینی را ارائه نمی کنند) نمی توانند.
هنگام استفاده از یک فایل معمولی، یک فرآیند می تواند تمام خروجی خود را روی آن بنویسد و آن را به فرآیند دیگری منتقل کند. یا فرآیندها می توانند در حالت بسیار موازی، با استفاده از مکانیزم سیگنال دهی خارجی (مانند سمافور) برای اطلاع یکدیگر در هنگام اتمام نوشتن یا خواندن، عمل کنند. نوار نقاله ما را از این همه دردسر نجات می دهد.
ما دنبال چی میگردیم؟
من آن را به زبان ساده توضیح خواهم داد تا برای شما راحت تر تصور کنید که یک نوار نقاله چگونه می تواند کار کند. شما باید یک بافر و مقداری حالت در حافظه اختصاص دهید. برای افزودن و حذف داده ها از بافر به توابعی نیاز دارید. برای فراخوانی توابع در حین عملیات خواندن و نوشتن روی توصیفگرهای فایل به ابزارهایی نیاز دارید. و برای اجرای رفتار خاصی که در بالا توضیح داده شد به قفل نیاز خواهید داشت.
اکنون ما آماده ایم تا کد منبع هسته را زیر نور لامپ روشن بازجویی کنیم تا مدل ذهنی مبهم خود را تأیید یا رد کنیم. اما همیشه برای چیزهای غیرمنتظره آماده باشید.
به کجا نگاه می کنیم؟
من نمی دانم نسخه من از کتاب معروف کجاست
سرگردانی در آرشیو TUHS مانند بازدید از یک موزه است. ما میتوانیم به تاریخ مشترکمان نگاه کنیم، و من برای سالها تلاش برای بازیابی همه این مطالب ذره ذره از نوارها و چاپهای قدیمی احترام قائلم. و من به شدت از آن قطعاتی که هنوز مفقود هستند آگاه هستم.
با ارضای کنجکاوی خود در مورد تاریخ باستانی نوار نقاله ها، می توانیم برای مقایسه به هسته های مدرن نگاه کنیم.
به هر حال، pipe
شماره تماس سیستم 42 در جدول است sysent[]
. اتفاقی؟
هسته های سنتی یونیکس (1970-1974)
هیچ اثری پیدا نکردم pipe(2)
و نه در
TUHS بیان می کند که
نسخه سوم یونیکس آخرین نسخه با هسته ای بود که به زبان اسمبلی نوشته شده بود، اما همچنین اولین نسخه با خط لوله بود. در طول سال 1973، کار برای بهبود نسخه سوم انجام شد، هسته به زبان C بازنویسی شد و بنابراین نسخه چهارم یونیکس ظاهر شد.
یکی از خوانندگان اسکن سندی را پیدا کرد که در آن داگ مکایلروی ایده «اتصال برنامهها مانند شلنگ باغ» را مطرح کرد.
در کتاب برایان کرنیگان
هنگامی که یونیکس منتشر شد، شیفتگی من به برنامههای معمول باعث شد از نویسنده سیستمعامل، کن تامپسون، بخواهم که اجازه دهد دادههای نوشتهشده در یک فرآیند نه تنها به دستگاه برود، بلکه به فرآیند دیگری نیز خروجی دهد. کن تصمیم گرفت که این امکان پذیر است. با این حال، به عنوان یک مینیمالیست، او می خواست هر عملکرد سیستم نقش مهمی ایفا کند. آیا نوشتن مستقیم بین فرآیندها واقعاً مزیت بزرگی نسبت به نوشتن در یک فایل میانی است؟ تنها زمانی که یک پیشنهاد خاص با نام جذاب «خط لوله» و توصیفی از نحو برای تعامل بین فرآیندها ارائه دادم که کن در نهایت فریاد زد: «این کار را انجام خواهم داد!»
و انجام داد. یک غروب سرنوشتساز، کن هسته و پوسته را تغییر داد، چندین برنامه استاندارد را برای استاندارد کردن نحوه پذیرش ورودی (که میتواند از یک خط لوله باشد) اصلاح کرد و همچنین نام فایلها را تغییر داد. روز بعد، خطوط لوله شروع به استفاده بسیار گسترده در برنامه های کاربردی کردند. در پایان هفته، منشی ها از آنها برای ارسال اسناد از پردازشگرهای کلمه به چاپگر استفاده می کردند. کمی بعد، کن API و نحو اصلی را برای بسته بندی استفاده از خطوط لوله با قراردادهای تمیزتر جایگزین کرد، که از آن زمان تاکنون مورد استفاده قرار گرفته است.
متأسفانه کد منبع نسخه سوم هسته یونیکس از بین رفته است. و اگرچه ما کد منبع هسته را داریم که به زبان C نوشته شده است
ما اسناد متنی برای pipe(2)
از هر دو نسخه، بنابراین می توانید با جستجوی اسناد شروع کنید pipe(2)
به زبان اسمبلی نوشته شده است و تنها یک توصیفگر فایل را برمی گرداند، اما از قبل عملکرد اصلی مورد انتظار را ارائه می دهد:
تماس سیستمی لوله یک مکانیزم ورودی/خروجی به نام خط لوله ایجاد می کند. توصیفگر فایل برگشتی می تواند برای عملیات خواندن و نوشتن استفاده شود. هنگامی که چیزی در خط لوله نوشته می شود، تا 504 بایت داده بافر می شود و پس از آن فرآیند نوشتن به حالت تعلیق در می آید. هنگام خواندن از خط لوله، داده های بافر حذف می شوند.
در سال بعد، هسته در C بازنویسی شد و pipe(fildes)
»:
تماس سیستمی لوله یک مکانیزم ورودی/خروجی به نام خط لوله ایجاد می کند. توصیفگرهای فایل برگشتی را می توان در عملیات خواندن و نوشتن استفاده کرد. وقتی چیزی در خط لوله نوشته میشود، دستهای که در r1 برگردانده میشود (مثلاً fildes[1]) استفاده میشود، در 4096 بایت داده بافر میشود و پس از آن فرآیند نوشتن به حالت تعلیق در میآید. هنگام خواندن از خط لوله، دسته ای که به r0 بازگشته است (مثلاً fildes[0]) داده ها را می گیرد.
فرض بر این است که پس از تعریف خط لوله، دو (یا بیشتر) فرآیندهای ارتباطی (ایجاد شده توسط تماس های بعدی به چنگال) داده ها را از خط لوله با استفاده از تماس ها منتقل می کند خواندن и نوشتن.
پوسته دارای نحوی برای تعریف یک آرایه خطی از فرآیندهای متصل شده توسط یک خط لوله است.
فراخوانیها برای خواندن از یک خط لوله خالی (حاوی دادههای بافری) که فقط یک انتها دارد (همه توصیفگرهای فایل نوشتاری بسته هستند) "انتهای فایل" را برمیگرداند. تماسها برای نوشتن در شرایط مشابه نادیده گرفته میشوند.
اولین
ویرایش ششم یونیکس (1975)
بیایید شروع به خواندن کد منبع یونیکس کنیم
برای سالهای زیادی کتاب شیرها تنها سند موجود در هسته یونیکس خارج از آزمایشگاه بل بود. اگرچه مجوز ویرایش ششم به معلمان اجازه می داد از کد منبع آن استفاده کنند، مجوز ویرایش هفتم این امکان را رد می کرد، بنابراین کتاب در قالب نسخه های تایپی غیرقانونی توزیع شد.
امروز میتوانید نسخهای از کتاب را بخرید که جلد آن دانشآموزان را در دستگاه کپی نشان میدهد. و به لطف وارن تومی (که پروژه TUHS را شروع کرد) می توانید دانلود کنید
بیش از 15 سال پیش، من یک کپی از کد منبع داده شده را تایپ کردم شیرها، زیرا کیفیت کپی خود را از تعداد نامعلومی از نسخه های دیگر دوست نداشتم. TUHS هنوز وجود نداشت و من به منابع قدیمی دسترسی نداشتم. اما در سال 1988، یک نوار قدیمی 9 آهنگی پیدا کردم که حاوی یک نسخه پشتیبان از یک کامپیوتر PDP11 بود. تشخیص اینکه آیا کار می کند یا نه سخت بود، اما یک درخت دست نخورده /usr/src/ وجود داشت که در آن بیشتر فایل ها با سال 1979 برچسب گذاری شده بودند، که حتی در آن زمان نیز قدیمی به نظر می رسید. همانطور که من معتقد بودم، این نسخه هفتم یا مشتق PWB بود.
من یافته را به عنوان مبنا قرار دادم و منابع را به صورت دستی به ویرایش ششم ویرایش کردم. برخی از کدها به همان صورت باقی ماندند، اما برخی از آنها باید کمی ویرایش می شدند و علامت += مدرن را به =+ منسوخ شده تغییر داد. برخی چیزها به سادگی حذف شدند و برخی باید به طور کامل بازنویسی می شدند، اما نه بیش از حد.
و امروز میتوانیم به صورت آنلاین در TUHS کد منبع نسخه ششم را بخوانیم
به هر حال، در نگاه اول، ویژگی اصلی C-code قبل از دوره کرنیگان و ریچی، آن است. اختصار. خیلی اوقات پیش نمی آید که بتوانم تکه هایی از کد را بدون ویرایش گسترده وارد کنم تا فضای نمایش نسبتاً باریکی را در سایت خود داشته باشم.
در اوایل
/*
* Max allowable buffering per pipe.
* This is also the max size of the
* file created to implement the pipe.
* If this size is bigger than 4096,
* pipes will be implemented in LARG
* files, which is probably not good.
*/
#define PIPSIZ 4096
اندازه بافر از نسخه چهارم تغییر نکرده است. اما در اینجا می بینیم که بدون هیچ سند عمومی، خطوط لوله زمانی از فایل ها به عنوان ذخیره سازی پشتیبان استفاده می کردند!
در مورد فایل های LARG، آنها مطابقت دارند
در اینجا تماس واقعی سیستم است pipe
:
/*
* The sys-pipe entry.
* Allocate an inode on the root device.
* Allocate 2 file structures.
* Put it all together with flags.
*/
pipe()
{
register *ip, *rf, *wf;
int r;
ip = ialloc(rootdev);
if(ip == NULL)
return;
rf = falloc();
if(rf == NULL) {
iput(ip);
return;
}
r = u.u_ar0[R0];
wf = falloc();
if(wf == NULL) {
rf->f_count = 0;
u.u_ofile[r] = NULL;
iput(ip);
return;
}
u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
u.u_ar0[R0] = r; /* rf's fd */
wf->f_flag = FWRITE|FPIPE;
wf->f_inode = ip;
rf->f_flag = FREAD|FPIPE;
rf->f_inode = ip;
ip->i_count = 2;
ip->i_flag = IACC|IUPD;
ip->i_mode = IALLOC;
}
کامنت به وضوح آنچه را که در اینجا می گذرد توصیف می کند. اما درک کد چندان آسان نیست، تا حدی به دلیل روش "R0
и R1
پارامترهای فراخوانی سیستم و مقادیر بازگشتی ارسال می شوند.
بیایید سعی کنیم با
pipe()
باید از طریق R0
и R1
اعداد توصیفگر فایل را برای خواندن و نوشتن بازگردانید. falloc()
یک اشاره گر را به ساختار فایل برمی گرداند، اما از طریق "برمی گرداند". u.u_ar0[R0]
و یک توصیفگر فایل یعنی کد در آن ذخیره می شود r
توصیفگر فایل برای خواندن و اختصاص یک توصیفگر فایل برای نوشتن مستقیم از u.u_ar0[R0]
بعد از تماس دوم falloc()
.
فلگ FPIPE
، که هنگام ایجاد خط لوله تنظیم می کنیم، رفتار تابع را کنترل می کند
/*
* common code for read and write calls:
* check permissions, set base, count, and offset,
* and switch out to readi, writei, or pipe code.
*/
rdwr(mode)
{
register *fp, m;
m = mode;
fp = getf(u.u_ar0[R0]);
/* … */
if(fp->f_flag&FPIPE) {
if(m==FREAD)
readp(fp); else
writep(fp);
}
/* … */
}
سپس تابع readp()
в pipe.c
داده ها را از خط لوله می خواند. اما بهتر است پیاده سازی را از ابتدا دنبال کنید writep()
. باز هم، کد به دلیل قراردادهای گذراندن آرگومان ها پیچیده تر شده است، اما برخی از جزئیات را می توان حذف کرد.
writep(fp)
{
register *rp, *ip, c;
rp = fp;
ip = rp->f_inode;
c = u.u_count;
loop:
/* If all done, return. */
plock(ip);
if(c == 0) {
prele(ip);
u.u_count = 0;
return;
}
/*
* If there are not both read and write sides of the
* pipe active, return error and signal too.
*/
if(ip->i_count < 2) {
prele(ip);
u.u_error = EPIPE;
psignal(u.u_procp, SIGPIPE);
return;
}
/*
* If the pipe is full, wait for reads to deplete
* and truncate it.
*/
if(ip->i_size1 == PIPSIZ) {
ip->i_mode =| IWRITE;
prele(ip);
sleep(ip+1, PPIPE);
goto loop;
}
/* Write what is possible and loop back. */
u.u_offset[0] = 0;
u.u_offset[1] = ip->i_size1;
u.u_count = min(c, PIPSIZ-u.u_offset[1]);
c =- u.u_count;
writei(ip);
prele(ip);
if(ip->i_mode&IREAD) {
ip->i_mode =& ~IREAD;
wakeup(ip+2);
}
goto loop;
}
می خواهیم بایت ها را در ورودی خط لوله بنویسیم u.u_count
. ابتدا باید inode را قفل کنیم (به زیر مراجعه کنید plock
/prele
).
سپس شمارنده مرجع inode را بررسی می کنیم. تا زمانی که هر دو انتهای خط لوله باز بماند، شمارنده باید برابر با 2 باشد. ما یک پیوند را نگه می داریم (از rp->f_inode
، بنابراین اگر شمارنده کمتر از 2 باشد، باید به این معنی باشد که فرآیند خواندن انتهای خط لوله را بسته است. به عبارت دیگر، ما سعی می کنیم در یک خط لوله بسته بنویسیم و این یک خطا است. کد خطای بار اول EPIPE
و سیگنال SIGPIPE
در نسخه ششم یونیکس ظاهر شد.
اما حتی اگر نوار نقاله باز باشد، ممکن است پر باشد. در این صورت قفل را رها کرده و به خواب می رویم به این امید که فرآیند دیگری از خط لوله خوانده شود و فضای کافی در آن آزاد شود. پس از بیدار شدن، به ابتدا باز می گردیم، قفل را دوباره آویزان می کنیم و یک چرخه ضبط جدید را شروع می کنیم.
اگر فضای خالی کافی در خط لوله وجود داشته باشد، با استفاده از آن داده ها را روی آن می نویسیم i_size1
inode (اگر خط لوله خالی باشد، می تواند برابر با 0 باشد) نشان دهنده پایان داده هایی است که قبلاً حاوی آن است. اگر فضای ضبط کافی وجود داشته باشد، می توانیم خط لوله را از آنجا پر کنیم i_size1
به PIPESIZ
. سپس قفل را آزاد می کنیم و سعی می کنیم هر فرآیندی را که در انتظار خواندن از خط لوله است بیدار کنیم. ما به ابتدا برمی گردیم تا ببینیم آیا می توانیم به تعداد بایت هایی که نیاز داریم بنویسیم. اگر شکست خورد، چرخه ضبط جدیدی را شروع می کنیم.
معمولا پارامتر i_mode
inode برای ذخیره مجوزها استفاده می شود r
, w
и x
. اما در مورد خطوط لوله، ما سیگنال می دهیم که برخی از فرآیندها با استفاده از بیت ها منتظر نوشتن یا خواندن هستند IREAD
и IWRITE
به ترتیب. این فرآیند پرچم را تنظیم می کند و تماس می گیرد sleep()
، و انتظار می رود که برخی از فرآیندهای دیگر در آینده باعث شود wakeup()
.
جادوی واقعی در آن اتفاق می افتد sleep()
и wakeup()
. در اجرا می شوند
/*
* Give up the processor till a wakeup occurs
* on chan, at which time the process
* enters the scheduling queue at priority pri.
* The most important effect of pri is that when
* pri<0 a signal cannot disturb the sleep;
* if pri>=0 signals will be processed.
* Callers of this routine must be prepared for
* premature return, and check that the reason for
* sleeping has gone away.
*/
sleep(chan, pri) /* … */
/*
* Wake up all processes sleeping on chan.
*/
wakeup(chan) /* … */
فرآیندی که باعث می شود sleep()
برای یک کانال خاص، ممکن است بعداً توسط فرآیند دیگری بیدار شود که باعث می شود wakeup()
برای همین کانال writep()
и readp()
اقدامات خود را از طریق چنین تماس های زوجی هماهنگ کنند. توجه داشته باشید که pipe.c
همیشه اولویت می دهد PPIPE
هنگام تماس sleep()
، پس همین است sleep()
ممکن است با یک سیگنال قطع شود.
اکنون همه چیز برای درک عملکرد داریم readp()
:
readp(fp)
int *fp;
{
register *rp, *ip;
rp = fp;
ip = rp->f_inode;
loop:
/* Very conservative locking. */
plock(ip);
/*
* If the head (read) has caught up with
* the tail (write), reset both to 0.
*/
if(rp->f_offset[1] == ip->i_size1) {
if(rp->f_offset[1] != 0) {
rp->f_offset[1] = 0;
ip->i_size1 = 0;
if(ip->i_mode&IWRITE) {
ip->i_mode =& ~IWRITE;
wakeup(ip+1);
}
}
/*
* If there are not both reader and
* writer active, return without
* satisfying read.
*/
prele(ip);
if(ip->i_count < 2)
return;
ip->i_mode =| IREAD;
sleep(ip+2, PPIPE);
goto loop;
}
/* Read and return */
u.u_offset[0] = 0;
u.u_offset[1] = rp->f_offset[1];
readi(ip);
rp->f_offset[1] = u.u_offset[1];
prele(ip);
}
ممکن است خواندن این تابع از پایین به بالا برای شما آسان تر باشد. شاخه "خواندن و بازگشت" معمولاً زمانی استفاده می شود که برخی از داده ها در خط لوله وجود داشته باشد. در این مورد استفاده می کنیم f_offset
خواندن، و سپس مقدار افست مربوطه را به روز کنید.
در قرائت های بعدی، خط لوله خالی خواهد بود، در صورتی که افست خواندن به آن رسیده باشد i_size1
در 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()
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()
چرخه را از سر می گیرد.
این کامل کننده توضیحات نوار نقاله در ویرایش ششم است. کد ساده، پیامدهای گسترده.
Xv6، یک هسته ساده یونیکس مانند
برای ایجاد هسته
کد حاوی یک پیاده سازی واضح و متفکرانه است 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
یک لفاف است که در
لینوکس 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 نوشته شده است:
منبع: www.habr.com