تو پارت قبلی system call interface آشنا شدیم و فهمیدیم که به عنوان یه واسط عمل می کنه و حالت اجرا رو از یوزر به کرنل تغییر میده . تو این قسمت با نحوه مدیریت پروسس ها و مکانیسم هایی که لینوکس برای مدیریت پروسس های بهره می بره آشنا میشم . نوشتن این قسمت چالش بر انگیز بود ، چون کل چیز جدید بود که هیچ ایده ای نداشتم چی هستن و باعث شد چیزای جدید در کنارش یاد بگیرم . احساس می کنم داستان کرونا و قرنطینه بیشتر از ۴ سال دانشگاه رفتن واسه من منفعت داشته ! بگذریم ….

پروسس ها در لینوکس

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

تو فضای کاربر با اجرا کردن برنامه یه پروسس به وجود می یاد ، یا در درون برنامه از سیستم کال های fork , exec استفاده میشه برای ایجاد پروسس . که فورک باعث ایجاد یه پروسس فرزند میشه و exec پروسس جدید رو با پروسس فعلی جایگزین می کنه .

task_struct

task_list

لینوکس از api های استاندارد مدیریت پروسس یونیکس مثل fork() , exec() , wait() و ترد های استاندارد POSIX استفاده می کنه . در عین حال تردها و پروسس های لینوکس کرنل متفاوت تر از کرنل های دیگه پیاده سازی شده . هیچ ساختار داخلی برای پروسس ها وجود نداره. کرنل لیستی از پروسس هارو (task list) توی یه لیست لینک شده دو جهته ذخیره می کنه که هر عضو این لیست ، توصیف کننده پروسس از نوع stuct task_struct هست . این توصیف کننده همه اطلاعاتی که کرنل در مورد پروسس داره یا نیاز داره رو تو خودش نگه میداره ، اطلاعاتی مثل اشاره گر هایی به منابع اعم از فضای آدرس ، توصیف کننده فایل ، IPC و همچنین اطلاعاتی راجب ارتباط بین پروسس ها ( پروسس پدر - فرزند ) و غیره که یه قسمتشو اینجا می بینیم.

struct task_struct {

    volatile long state;
    void stack;
    unsigned int flags;
    
    int prio, static_prio;

    struct list_head tasks;

    struct mm_struct mm, active_mm;

    pid_t pid;
    pid_t tgid;

    struct task_struct real_parent;

    char comm[TASK_COMM_LEN];

    struct thread_struct thread;

    struct files_struct files;

    ...

};

تو کد بالا ، وضعیت اجرا (long state) ، پشته (void ∗stack) ، یه سری فلگ (unsigned int flags;) ، پروسس پدر (struct task_struct ∗real_parent) ، و فایلای که داره استفاده میشه (struct files_struct ∗files) رو می بینیم . وضعیت اجرا که ازش اسمش معلومه نشون دهنده وضعیت تسک یا همون پروسس هست از معروف ترین حالت های اجرا میشه به:

  • running (TASK_RUNNING)
  • sleeping (TASK_INTERRUPTIBLE)
  • sleeping but unable to be woken up (TASK_UNINTERRUPTIBLE)
  • stopped (TASK_STOPPED)

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

مدیریت پروسس ها

خب حالا که کمی با پروسس ها آشنا شدیم و فهمیدیم که پروسس ها به صورت داینامیک به وجود میان و براشون یه task_struct تولید میشه . ولی پروسسی تو لینوکس وجود داره به اسم init که همیشه در حال اجراs ، برای همین به صورت استاتیک به تسک براش نوشته شده . این لینکو دنبال کنید .

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

پروسس جدید

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

process creation

do_fork() کارشو با alloc_pidmp که یه pid جدید اختصاص میده شروع میکنه . بعد چک می کنه که آیا دیباگری پروسس رو زیر نظر داره یا نه که در اینصورت فلگ CLONE_PTRACE ست میشه ،‌بعد copy_process فراخوانی میشه فلاگ ها ، رجیسترها ،‌پشته ، پروسس پدر و PID جدید رو بهش پاس می ده . دقت کنید که هنوز پروسس اجرا نشده . copy_process همه کارای لازم قبل اجرای پروسس رو انجام میده ، فلگای CLONE رو چک می کنه ولید باشن و در غیر اینصورت اررور EINVAL برمیگردونه .

بعد تابع dup_task_struct فراخوانی میشه و یه task_struct جدید با مشخصات پروسس مورد نظر ساخته میشه و مقداری دهی های اولیه برای وضعیت پروسس انجام میشه و کنترل برمیگرده به copy_process ، یه سری سکیوریتی چک انجام میشه و تعدادی تابع که چیزای مختلفی کپی می کنن مثل copy_files , copy_sighand and copy_signal , copy_mm , copy_thread

پروسس هنوز اجرا نشده ! تسک جدید به یک پروسسور انتصاب داده می شه بر اساس اینکه کدوم پروسسور اجازه اجرا داره (cpus_allowd) حالا پروسس اولویت پروسس پدر خودشو به ارث می بره و کنترل بر میگرده به do_fork() تو این مرحله پروسس به وجود اومده ولی هنوز به مرحله اجرا نرسیده که در این مرحله تابع wake_up_new_task اجرا میشه و پروسس جدید به یه صف اجرا اضافه میشه و بیدارش می کنن! بعد از اجرا و در حین برگشت PID به فراخواننده برگشت داده میشه .

تخریب پروسس

پایان کار پروسس ها یا همون تخریب پروسس های می تونه توسط چنتا رویداد اتفاق بیافته از طریق سیگنال ها یا فراخوانی تابع exit() . هر طور که میخواد این اتفاق بیافته تابع do_exit() فراخوانی میشه هدف اینکه تمامی ارجاعات به پروسس فعلی از سیستم عامل حذف بشه . اینکار اول با ست کردن فلگ PF_EXITING انجام میشه و وقتی این فلگ فعاله دستکاری روی پروسس اتفاق نمی افته . رها کردن منابعی که پروسس در طول حیاتش به دست آورده توسط یکی سری توابع انجام میشه مثلا حذف کردن مموری پیج exit_mm . اعلانی هایی هم از طریق تابع exit_notify فرستاده میشه مثلا به پروسس پدر سیگنالی فرستاده میشه که پروسس فرزند در حال ترمینیت شدنه . و در آخر وضعیت پروسس به PF_DEAD تغییر پیدا می کنه و تابع schedule فراخوانی میشه تا پروسس جدیدی برای اجرا انتخاب بشه . البته باید در نظر داشت اگه سیگنالینگ ، علامت دهی به پروسس پدر نیاز باشه تسک کاملا از بین نمی ره در غیر اینصورت حافظه استفاده شده توسط پروسس تصاحب میشه با تابع release_task

process creation

وضعیت های پروسس

خب گفتیم که تو task_struct وضعیت فعلی تسک تنظیم میشه در حالت کلی ۵ تا وضعیت داریم :

  • TASK_RUNNING : از اسمش معموله پروسس فعلی یا در حال اجراس یا تو صف اجرا
  • TASK_INTERRUPTIBLE : پروسس در حالت اسلیپینگ هست منتظر یه وضعیت که محیا بشه و بعدش به وضعیت اجرا تغییر داده بشه
  • TASK_UNINTERRUPTABLE : مثل بالایی هست با این تفاوت که با استفاده از سیگنال ها نمی شه به حالت اجرا برش گردوند ! تو موقعیت هایی استفاده می شه که لازمه پروسس در حالت انتظار باشه بدون اینکه وقفه ای اونو از این حالت خارج کنه یا در مواقعی که رویداد مورد نظر به سرعت اتفاق خواهد افتاد .
  • TASK_ZOMBIE : حالتیه که پروسس ترمینیت شده ولی پروسس پدر سیگنالی صادر نکرده پس توصیف کننده پروسس باقی می مونه که اگه پدر درخواست کرد دوباره allocate بشه .
  • TASK_STOPPED : اجرا پروسس متوقف شده . یعنی تسک در حال اجرا نیس یا اجازه اجرا نداره وقتی اتفاق می افته که مثلا SIGSTOP دریافت بشه .

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

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

طبیعتا اگه سیستمی تک پردازنده داشته باشیم استفاده از روش های زمان بندی انحصاری منطقی نیست . که البته این مسائل تو حوصله مطلب نیست . در کنار سیستم های تک پردازنده سیستم های چند پردازنده رو داریم و پردازنده های multi-core یه جورایی همون چند پردازنده هست ولی تو یه بسته فیزیکی البته که یک سری تفاوت هایی دارن . الآن می خواییم با دوتا از سیستم های چند پردازنده ای آشنا بشیم SMP و ASMP

Asymmetric Multiprocessing

در این سیستم از چند پردازنده ها ، با همه پردازنده ها به طور مساوی رفتار نمی شه و احتمالا یکی از سی پی یو ها فقط اجازه ی اجرای سیستم عامل اونیکی اجازه عملیات مربوط به I/O رو داشته باشه . به این سیستم master/slave هم گفته میشه یعنی ارباب و برده ! (اسمش یه جوریه :-) ) در این سیستم هر پردازنده می تونه از لحاظ کلاک و حتی معماری متفاوت باشه . هر کدوم از پردازنده ها می تونن حافظه رم مجزا برا خودشون داشته باشن . برای مثال TRS-8 Model 16 که یه دسکتاپ چند پردازنده بود و با سیستم عامل Xenix کار می کرد . ۳ تا پردازنده داشت یکیش zilog z80 ۸ بیت بود دومی Motorola 68000 و سومی intel8021 که توی کیبورد بود !!سیستم باحالی هم داشت موقع بوت پردازنده اولی به عنوان مستر شناخته میشد که ضعیفتر از پردازنده دومی هست ولی بعد Xenix اینرو به پردازنده دوم انتقال میداد و نقشاشون عوض میشده . اولی فقط کارای ورودی و خروجی انجام میداده . نکته مثبت این سیستم طراحی راحت تر و هزینه ساخت کمتر بود ولی از اونجایی که کار هر پردازنده توسط پردازنده مستر مشخص میشه و پردازنده های دیگه تو کار پردازش دیگری دخالتی ندارن زیاد کارایی خوبی نداشتن .

asymmetric multiprocessors

Symmetric Multiprocessing

تو این سیستم همه سی پی یو ها مساوی هستن و از یه حافظه اصلی استفاده می کنن و به همه ورودی و خروجی ها به طور مساوری دسترسی دارن . بیشتر سیستم های امروزی از این معماری استفاده می کنن . SMP به طور سنتی پردازنده ها رو بدون حافظه کش طراحی کرده . ولی بعد ها برای افزایش بهره وری حافظه کش بهش اضافه شد . پیاده سازی این معماری پیچیده و هزینه ساخت بالاتری نسبت به مدل قبلی داره . و همچنین باعث به وجود اومدن وضعیت رقابت میشه . برای حل این مشکل لینوکس کرنل یه هماهنگ سازی اولیه پیاده سازی کرده مثلا spin locks که اطمینان حاصل می کنه که قسمت های حساس ( مثل توابعی کرنل که به یه آدرس حافظه اشاره دارن ) فقط و فقط توسط یک پردازنده انجام بشه .

asymmetric multiprocessors

یه حالت کلاسیک race condition همون وضعیت رقابت ( معادل فارسی خوب به ذهنم نرسید ) یه پیاده سازی نادست از آزاد سازی منابع هست . کد زیر رو در نظر بگیرد . یه منبع مشترک داریم و تا وقتی که همه آزادش نکردن در دسترس نگش میداره و در اکثر مواقع درست کار می کنه و در مواقعی باعث میشه که این آزادی سازی منبع دوبار انجام بگیره .

void release_resource()
{
     counter--;

     if (!counter)
         free_resource();
}

اگه ترد A درست بعد از کم کردن مقدار counter توسط ترد B قبضه بشه و ، ترد B هم release_resource رو فراخوانی کنه از مقدار کانتر یه بار دیگه کم میشه و آزاد سازی منبع انجام میشه و وقتی A دوباره به کارش ادامه میده و شرط رو برقرار می بینه یکبار دیگه اقدام به آزاد سازی منبع میکنه .

race_condition

در این حالت بعد از شناسایی قسمت حساس از سه تا راه کار میشه استفاده کرد :

  1. قسمت حساس رو atomic کنیم
  2. قبضه کردن ( گرفتن پردازنده از پروسس )رو در قسمت حساس غیر فعال کنیم ( هر نوع interupt و غیره)
  3. دسترسی به قسمت حساس رو سریالایز کنیم ( استفاده از spin locks فقط به یه ترد اجازه دسترسی به قسمت حساس داده بشه )

عملیات اتمیک

تو بعضی وضعیت ها میشه از عملیات اتمیک برای رفع race استفاده کرد . اتمیک توسط سخت افزار فراهم میشه و لینوکس API برای دسترسی به این عملیات داره . برای مثال :

void release_resource()
{
    if (atomic_dec_and_test(&counter))
         free_resource();
}

البته ظاهرا یه عوارضی داره استفاده از این روش که از حوصله این متن خارجه .

یه سری توضیحات اضافی

POSIX : یه استاندارد از خانواده IEEE هست برای مدیریت قابلیت ها بین سیستم عاملا و یه سری API به همراه شل های کامند لاین و ابزار برای سازگاری نر افزار ها در سیستم عامل های مبنتی بر یونکیس و غیره اس .

Cgroup : مکانیسمی برای مدیریت سلسه مراتب پروسس ها و توزیع منابع به صورت کنترل شده . که توسط دو تا از مهندسین گوگل طراحی شده و سال ۲۰۰۸ به لینوکس اضافه شده

interupts : رویدادی هست که جریان اجرای معمولی رو تغییر میده منبع این رویداد می تونه توسط یه سخت افزار دیگه خارج از پردازنده باشه یا در داخل خود پردازنده تولید بشه . کسایی که با میکروکنترلرها کار کردن حتما با interupt ها سرو کار داشتن .

این پارت می تونست طولانی تر باشه که در اینصورت تخصصی تر میشد که من در اون حد تخصص ندارم :) پارت بعدی در مورد Virutal File system (VFS) خواهد بود .

نظرتون رو بنویسد
کپی بخش یا کل هر کدام از مطالب لینوکس ۹۸ تنها با کسب مجوز مکتوب امکان پذیر است.
وبلاگ لینوکس ۹۸ یک پروژه متن باز بوده و سورس آن در گیت‌هاب موجود است.