توصیفگر پردازه (pidfd) چیست؟
همانگونه که از نامش پیداست pidfd یک توصیفگر پرونده بر روی یک پردازه میباشد که توانایی فرستادن سیگنال ها را به صورت race-free و امن را برای ما فراهم میکند.
برای درک بهتر تعریف بالا بهتر است هر قسمت را جداگانه بررسی کنیم
شماره پردازه (pid):
در هسته سیستم عامل لینوکس به هر پردازه یک عدد در بازه 0 تا 32768 به نام pid نسبت داده میشود که از طریق آن میتوان با استفاده از فراخوانی های سیستمی به این پردازه دست پیدا کرد و یا به آن سیگنال فرستاد.
اما این پیاده سازی یک مشکل دارد و آن این است که در هسته سیستمعامل اگر یک پردازه به هر دلیلی متوقف شود (کارش تمام شود و یا کشته شود) ممکن است پردازه جدیدی pid پردازهی مذکور را بگیرد و باعث ایجاد مشکلات مهم امنیتی شود. از جمله آنها میتوان به برنامههایی اشاره کرد که وظیفه مدیریت پردازههایی را دارند که لزوما از خود آن پردازه منشعب نشده اند (مانند systemd و یا LMKD اندروید)
توصیفگر پرونده (file descriptor):
توصیفگر پرونده یک نشانگر یکتا برای مدیریت منابع I/O (مانند file,socket,streams,...) میباشد که از طریق آن میتوان فراخوانی های سیستمی را روی منابع I/O انجام داد.
حال pidfd را دوباره تعریف میکنیم:
توصیفگر پردازه یا pidfd یک توصیفگر روی پردازه میباشد که به ما امکان مدیریت پردازه را (ارسال سیگنال، انجام فراخوانی سیستمی wait ، ...) به صورت امن میدهد. زیرا از وقتی که این توصیفگر ساخته میشود، همیشه به یک پردازه اشاره میکند و با متوقف شدن پردازه مورد نظر نیز این توصیفگر بسته میشود. پس دیگر مشکلات مطرح شده در بالا را نخواهیم داشت
دلایل بالا و همچنین درخواست زیاد کاربران برای فراهم آوردن روشی برای ایجاد پردازه های ناشناس باعث شد تا ایده طراحی pidfd برای اولین بار شکل بگیرد.
هنگامی که یک pidfd ساخته میشود، در تمام طول عمر خود همیشه به یک پردازه خاص اشاره میکند و به صورت پیشفرض با پرچم های O_CLOEXEC ساخته میشود که به این معناست که با بسته شدن پردازه مقصد، این توصیفگر پرونده نیز بسته شده و یک رابط کاربری امن برای مدیریت پردازه ها فراهم میکند
روند اضافه شدن PIDFD به هسته سیستم عامل روندی آهسته و پیوسته بود به طوری که شروع آن با معرفی ()pidfd_send_signal در هسته ورژن 5.1 و پس از آن اضافه شدن پرچم CLONE_PIDFD به فراخوانی سیستمی clone در هسته ورژن 5.2 و در ورژن 5.3 نیز شاهد اضافه شدن فراخانی سیستمی pidfd_open میباشیم که امکان ایجاد توصیفکننده پردازه روی پردازههایی که از روشی غیر از فراخوانی سیستمی clone با پرچم CLONE_PIDFD ایجاد شده اند (فراخوانی سیستمی fork و یا دیگر روش های فراخوانی clone ) را به ما میدهد.
در ادامه به توضیح روش نحوه پیاده سازی pidfd_open میپردازیم:
static int pidfd_create(struct pid *pid) { int fd; fd = anon_inode_getfd("[pidfd]", &pidfd_fops, get_pid(pid),O_RDWR | O_CLOEXEC); if (fd < 0)put_pid(pid); return fd; }
تابع ()pidfd_create یک anon_inode ساخته و اطلاعات مربوط به پردازه را در آن نوشته و آن را برمیگرداند.
درکل برای اختصاص یک توصیفگر پردازه چندین راه متفاوت وجود دارد
۱) استفاده از پروندههای موجود در /proc/
۲) ساختن یک توصیفگر ناشناس
که در هسته سیستمعامل برای حفظ یکپارچگی و کاهش پیچیدگی راه حل دوم مورد استفاده قرارگرفت.
همچنین جالب است بدانید که بحث های زیادی حول محور ذخیره کردن پردازه ها در این توصیفگر ها شکل گرفته. در هسته سیستم عامل هر پردازه به صورت یک task_struct درون لیست tasks ذخیره میشود که حاوی تمامی اطلاعاتی است که هسته سیستم عامل برای مدیریت پردازه ها به آن نیاز دارد و از همین رو حجم هر task_struct به حدود ۱.۷ کیلوبایت میرسد که بسیار حجم بالایی میباشد و نمیتوان از آن برای ذخیره سازی پردازه ها در توصیفگر پردازه استفاده کرد. به همین دلیل طراحان pidfd به استفاده از struct pid که حجم به نسبت کمتری دارد برای پیاده سازی pidfd روی آوردند
دقت کنید که تابع بالا تنها زمانی میتواند فراخوانی شود که جدول توصیفگر ها دیگر مشترک نیست تا از درز کردن توصیفگر جلوگیری شود.
SYSCALL_DEFINE2(pidfd_open, pid_t, pid, unsigned int, flags) { int fd, ret; struct pid *p; if (flags) return -EINVAL; if (pid <= 0) return -EINVAL; p = find_get_pid(pid); if (!p) return -ESRCH; ret = 0; rcu_read_lock(); if (!pid_task(p, PIDTYPE_TGID)) ret = -EINVAL; rcu_read_unlock(); fd = ret ?: pidfd_create(p); put_pid(p); return fd; }
فراخوانی سیستمی pidfd_open به این صورت عمل میکند که ابتدا اطمینان حاصل میکند که پرچم های داده شده درست هستند و سپس پردازه مورد نظر را از روی pid آن یافته و اطمینان حاصل میکند که این پردازنده در لیست تسک های جاری هسته باشد و سپس یک pidfd ساخته و آن را بازمیگرداند.
دقت کنید که برای بررسی وجود پردازه در لیست تسک های جاری از rcu_lock ها استفاده میکنیم زیرا سرعت تغییرات این لیست بسیار بالاست و rcu_lock نیز دقیقا برای همین شرایط طراحی شده است.
منابع:
Pidfd:
https://lwn.net/Articles/794707/
Kernel commits:
pidfd_open:
https://lwn.net/ml/linux-kernel/CAG48ez2gb94SqS30Ai4+VBHhnzBp5Po9_u00nMrvUW6Wqq6hPA@mail.gmail.com/
https://man7.org/linux/man-pages/man2/pidfd_open.2.html
task_struct: