آموزش ایجاد و مدیریت فرآیندها ( Process ) در لینوکس

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

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

1. پردازه ها

پردازه یا فرآیند موجودیتی است که از آن برای نشان دادن یک برنامه در حال اجرا استفاده می شود. به فرآیندها برنامه های در حال اجرا نیز می گویند. به هر پردازه در سیستم یک شناسه یکتا نسبت داده می شود که به آن شناسه پردازه یا PID می گوییم. PID یک پردازه ی موجود قابل تغییر دادن نیست.

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

همچنین سیستم عامل برای ارائه یک سرویس ممکن است یک پردازه ایجاد کند؛ در این حال ایجاد یک پردازه جدید برای ارائه سرویس باعث می شود که کاربر دیگر برای دریافت سرویس نیاز به صبر کردن نداشته باشد (مثلا یک پردازه برای کنترل عمل چاپ ایجاد می شود.). همچنین یک برنامه کاربر با ایجاد چندین پردازه می تواند از پیمانه ای شدن و فواید موازی سازی استفاده کند.

در لینوکس هر پردازه ی جدید به وسیله پردازه ی دیگری (که از قبل موجود می باشد) ایجاد می شود و همین سبب ایجاد یک رابطه والد-فرزند می شود. تنها استثنا در پردازه ای است که دارای شناسه 0 است. این پردازه هیچ والدی نداشته و به وسیله سیستم عامل در زمان boot سیستم ایجاد می شود. هر پردازه می تواند PID والد خود را با استفاده از فراخوانی سیستمی getppid بیابد.

یک پردازه نمی تواند والد خود را تغییر داده و یا رابطه والد-فرزندی را بشکند. با این حال تغییر والد یک فرزند ممکن است یک بار طی اجرای یک پردازه اتفاق بیفتد و این زمانی است که پردازه والد قبل از فرزندانش خاتمه بیابد. در این گونه مواقع سیستم عامل شناسه والد تمامی پردازه های فرندی که والدشان خاتمه یافته است را به 1 مقدار دهی می کند (1 شناسه پردازه init است). نحو دو فراخوانی سیستمی که در بالا به آن اشاره شد به صورت زیر است:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid();
pid_t getppid();

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

(ترمینال توسط ستون TTY در جدول نشان داده شده مشخص می شود.) اگر خواهان نشان دادن تمامی پردازه های سیستم هستیم می توانیم از گزینه a- استفاده کنیم. با استفاده از گزینه l- (این گزینه به معنی long format می باشد) می توانیم اطلاعات بیشتری راجع به پردازه ها کسب کنیم: به عنوان مثال PID والد پردازه ها، UID کاربران و غیره. در شکل زیر نحوه بکارگیری این دستور نشان داده شده است.

وب سایت توسینسو

2. برنامه ها و پردازه ها

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

data segment شامل داده های از پیش تعریف شده (به عنوان مثال ثوابت) و داده هایی که مقدار دهی اولیه شده اند، می باشد. این دو بخش به همراه stack segment -که شامل داده هایی است که به طور پویا به هنگام اجرای پردازه تخصیص داده می شوند قسمت های اساسی یک پردازه لینوکس هستند.

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

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

تمامی پردازه های سیستم اولاد مستقیم یا غیر مستقیم یک پردازه خاص هستند که به هنگام شروع به کار سیستم به وسیله دستور etcinit به وجود می آید. وقتی یک کاربر وارد سیستم می شود یک پردازه به طور اوماتیک ایجاد می شود. این پردازه shell یا همان مفسر فرمان مربوط به session کاربر است.

وظیفه این پردازه تفسیر و اجرای دستوراتی است که کاربر تایپ کرده است. برای ایجاد یک پردازه جدید می توان از فراخوانی سیستمی fork استفاده کرد. هر زمان که این فراخوانی اجرا شود یک پردازه جدید مستقل از پردازه ای که fork را فراخوانی کرده است ایجاد می شود و یک PID خودش را دارا خواهد بود.

این دو پردازه (پردازه ای که فراخوانی سیستمی fork را صدا زده است و پردازه ای که جدیدا ایجاد شده است) همروند هستند یا به عبارتی دیگر از لحاظ اجرا شدن مستقل از همدیگر اجرا می شوند. این دو پردازه وقتی صحبت از محتوا (کد، داده ها، پشته، فایل های باز و غیره) می شود، دو پردازه ی یکسان هستند.

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

اجرای یک برنامه در پردازه فرزند، به وسیله فراخوانی سیستمی exit (که یا به طور صریح توسط پردازه فرزند صدا زده می شود و یا به طور ضمنی توسط سیستم عامل در پایان تابع main فراخوانی می شود) خاتمه می پذیرد. تاثیر فراخوانی سیستمی exit به این صورت می باشد: پردازه جاری را خاتمه می دهد، یک کد خاتمه را در مدخل جدول پردازه مربوطه ذخیره می کند و در نهایت پردازه والد را (که منتظر خاتمه پردازه فرزند است) از خواب بیدار می کند.

3. فراخوانی سیستمی fork و exec

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

#include <sys/types.h>
#include <unistd.h>
pid_t fork();

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

برای اینکه بین برگشت از تابع fork در پردازه والد و برگشت از تابع fork در پردازه ی فرزند تمایز قائل شویم، تابع PID پردازه فرزند را در حالت اول (در پردازه پدر) و 0 را در حالت دوم (در پردازه فرزند) برمی گرداند. وقتی اجرای تابع fork موفقیت آمیز نباشد، 1- برگردانده می شود. کد زیر مطالب بالا را با مثال نشان می دهد:

pid=fork();
/* source code executed by both processes */
switch (pid)
case -1:
/* Error! Unsuccesful fork! */
case 0 :
/* source code executed only by the child*/
break;
default:
/* source code executed only by the parent*/
} 
/*source code executed by both processes*/

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

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

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

این است که با تست کردن مقدار برگشتی توسط fork، اجرا را بین دو پردازه (پردازه والد و پردازه فرزند) جدا کنیم.ایجاد یک پردازه فرزند یکسان (از لحاظ محتوا) با پدرش زمانی منطقی است که بتوانیم بخش کد و داده ی پردازه ی تازه ایجاد شده را تغییر دهیم، درست مانند این که یک برنامه جدید را بار و اجرا کنیم.

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

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

(fcnlt(fd, F__SETFT, 1. اگر فراخوانی exec با شکست مواجه شد، مقدار 1- برگردانده می شود. شکست در مواقعی رخ می دهد که مسیر فایل اجرایی را اشتباه داده باشیم یا فایل اجرایی درست اجرا نشود. یکی از راه های بکار گیری فراخوانی سیستمی exec در زیر ذکر شده است. در ادامه با یک مثال نحوه بکارگیری این دستور را نشان می دهیم.

#include <unistd.h>
int execl(const char * path, const char * arg0, ..., NULL);

4. فراخوانی سیستمی wait

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

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* pstatus);

در صورت موفقیت، این تابع PID فرزند خاتمه یافته و در صورت بروز خطا 1- را برمی گرداند. آرگومان pstatus آدرس جایی است که کد خاتمه فرزند در آنجا کپی شده است. (فرندی که PID آن برگردانده شده است) پردازه ای که wait را فراخوانی می کند می تواند:

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

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

  • (WIFEXITED(*pstatus: اگر پردازه فرزند به وسیله فراخوانی صریح یا ضمنی ( در پایان اجرا) exit و یا به وسیله فراخوانی دستور return در پایان تابع main خاتمه بیابد، مقدار true برگردانده می شود. در غیر این صورت false برگردانده می شود.
  • (WEXITSTATUS(*pstatus: کد خاتمه مشخص شده در پردازه ی فرزند را برمی گرداند.
  • (WIFSIGNALED(*pstatus: اگر پردازه فرزند در اثر دریافت یک سیگنال خاتمه بیابد true و در غیر این صورت false برگردانده می شود.
  • (WTERMSIG(*pstatus: کد سیگنالی که باعث خامته پردازه فرزند شده است را برمی گرداند. استفاده از این دستور زمانی عاقلانه است که ماکروی WIFSIGNALED مقدار true را برگردانده باشد.

در سه حالت یک پردازه خاتمه می یابد. 1- وقتی پردازه عمدا exit را فراخوانی کند 2- بعد از دریافت یک سیگنال خاتمه دهنده و یا دریافت سیگنالی که پردازه قادر به پردازش آن نیست. 3 -در اثر خرابی سیستم. کد حالتی که به وسیله متغیر pstatus برگردانده می شود، مشخص می کند که کدام یک از دو حالت اول اتفاق افتاده است.

5. فراخوانی سیستمی exit

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

void exit(int* status);

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

  • وقتی پدر قبل از فرزند خاتمه می یابد. در این حالت به همه فرزندان یک پدر جدید نسبت داده می شود. این پدر جدید پردازه init با شناسه 1 است. با این کار دیگر پروسه ی یتیمی در سیستم وجود نخواهد داشت.
  • وقتی فرزند قبل از پدر خود پایان می یابد. سیستم عامل بعضی از اطلاعات را راجع به پردازه خاتمه یافته ذخیره می کند. (PID ، دلیل خاتمه و غیره). پدر پردازه خاتمه یافته به این اطلاعات با استفاده از فراخوانی سیستمی wait دسترسی دارد. حالت zombie به پردازه ای گفته می شود که خاتمه یافته است و پدر آن نیز wait را فراخوانی نکرده است. به وسیله دستور ps می توان از پردازه های zombie اطلاع پیدا کرد. در این موقع در ستون حالت (‘S’) حرف Z چاپ می شود.
  • وقتی پردازه ای که از init ارث بری می کند، خاتمه بیابد. این پردازه ها وارد حالت zombie نمی شوند به این خاطر که پردازه ی init همیشه یکی از فراخوانی های wait یا waitpid را برای فرزندانش فراخوانی می کند. با این مکانیزم از سربار پیدا کردن سیستم با پردازه های zombie پرهیز می شود.

در شکل زیر خلاصه ای از استفاده از دستورات fork ، exit ، wait و exec به تصویر کشیده شده است.

وب سایت توسینسو

6. مثال

در برنامه زیر ابتدا یک پردازه فرزند ایجاد می شود، سپس تا خاتمه فرزند wait می شود و در نهایت PID فرزند به همراه حالت خاتمه اش (به صورت دهدهی و هگزادسیمال) لیست می شود.

/* parent.c */ // parent code
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
	int pid, state;
	printf("The parent: before the fork()\n");
	if ((pid=fork()) != 0)
		wait(&state);
	else {
		execl("./child","child", NULL);
		perror("Error exec");
	}
	printf("The parent: after fork()\n");
	state = WEXITSTATUS(state);
	printf("PID child=%d; terminated with the code %d=%x\n",pid, state, state);
}
/* child.c */ // child code
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
	int pid;
	printf("The child: begins the execution \n");
	pid=getpid();
	printf("The son: %d terminates\n", pid);
	exit(10);
}

نمونه اجرای برنامه

وب سایت توسینسو

7. برنامه شماره 1

برنامه ای بنویسید که از سه دستور fork متوالی استفاده کرده و هشت پردازه ایجاد کند. با استفاده از دستورات مربوط به مشخصات پردازه ها برای هر پردازه، شماره ID و شماره ID ایجاد کننده آن را به دست آورید. درختی براساس این IDها رسم کنید که رابطه Parent-child را نشان دهد.

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void main()
{
pid_t pid;
pid=fork();
printf("\nReturn value of fork call (0 in child proccess & pid of newly created proccess in parent proccess): %d",pid);
printf("\nProccess PID:%d",getpid());
printf("\nParent proccess PID: %d\n",getppid());

pid=fork();
printf("\nReturn value of fork call (0 in child proccess & pid of newly created proccess in parent proccess): %d",pid);
printf("\nProccess PID:%d",getpid());
printf("\nParent proccess PID: %d\n",getppid());

pid=fork();
printf("\nReturn value of fork call (0 in child proccess & pid of newly created proccess in parent proccess): %d",pid);
printf("\nProccess PID:%d",getpid());
printf("\nParent proccess PID: %d\n",getppid());
}

نمونه اجرای برنامه

همانطور که در شکل زیر نشان داده شده است، پردازه پدر قبل از اتمام اجرای فرزندان به پایان رسیده است. به همین دلیل، فرزندان یتیم شده و سیستم عامل شماره پردازه ی پدر آن ها را به 1 تنظیم می کند. برای جلوگیری از این اتفاق می توان از تابع wait استفاده کرد. در ادامه این امر نشان داده شده است.

وب سایت توسینسو

درخت پردازه ها

وب سایت توسینسو

برنامه پردازه پدر به شکلی دیگر

/* parent.c */ // parent code
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
	int pid, state;
	int i;
	for(i=0;i<3;i++)
	{
		pid=fork();
		wait(&state);
		//printf("\nfork call return value: %d",pid);
		printf("\nProccess PID:%d",getpid());
		printf("\tParent proccess PID: %d\n",getppid());
	}
}

نمونه اجرای برنامه و درخت پردازه ها

وب سایت توسینسو
وب سایت توسینسو

8. برنامه شماره 2

برنامه ای بنویسید که از یک دستور fork و همچنین تابع wait استفاده کند. برنامه باید به گونه ای باشد که فرآیند والد از نحوه اجرای فرآیند فرزند مطلع باشد.توضیح: این برنامه مانند مثال آورده شده در قسمت 6 می باشد. تنها با اضافه کردن ماکروی (WIFEXITED(*pstatus از اجرای موفقیت آمیز فرزند اطمینان حاصل می کنیم.

کد برنامه:

/* parent.c */ // parent code
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
	int pid, state;
	printf("The parent: before the fork()\n");
	if ((pid=fork()) != 0)
		wait(&state);
	else 
{
		execl("./child","child", NULL);
		perror("Error exec");
	}
	printf("The parent: after fork()\n");
	if(WIFEXITED(state)==1)
	{
		state = WEXITSTATUS(state);
		printf("PID child=%d; terminated successfuly with the code %d(in decimal)=%x(in hex)\n",pid, state, state);
}
}
/* child.c */ // child code
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
	int pid;
	printf("The child: begins the execution \n");
	pid=getpid();
	printf("The son: %d terminates\n", pid);
}

نمونه اجرای برنامه:

وب سایت توسینسو

نظرات