در این نوشتار میخواهیم چگونگی ایزوله کردن یک پردازه در سیستمعامل لینوکس را بررسی کنیم. اگر با Docker یک کانتینر ایجاد کرده باشید، در زمان کار با آن احساس کردهاید که داخل آن مشابه محیط یک ماشین مجازی است. مثلاً متوجه شدهاید که از دید یک پردازهی در حال اجرا در داخل کانتینر، فهرست پردازهها، شبکه و فایلسیستم با ماشین میزبان متفاوت است، همچنین میزان منابعی مانند RAM و CPU از دید پردازههای داخل کانتینر محدودتر از کل مقدار در اختیار میزبان میباشد.
این سطح از ایزولهکردن در سیستمعامل لینوکس توسط ویژگیهای namespace (فضانام)، cgroups, pivot_root و … پیادهسازی میشود. ایزوله کردن پردازهها موضوع کلیدی در مورد نحوهی عملکرد کانتینرها میباشد. فضانامها منابعی را به کانتینر اختصاص میدهند که کاملاً مستقل از میزبان (host) و بقیهی کانتینرهای مجاور میباشد.
این نوشتار در دو قسمت تنظیم شده است که در ادامه قسمت اول آن را میخوانید. قسمت دوم این مطلب را میتوانید از اینجا دنبال کنید.
فضانامها (namespaces) در لینوکس
فضانام یک ابزار برای محدود کردن دید پردازهها نسبت به ماشین میزبان است. در kernel لینوکس فضانامهای مختلفی وجود دارد که استفاده از هر کدام از آنها اجازه میدهد یک پردازه در همان فضانام (مثلا شبکه) دید مختص به خود را داشته باشد. مثلاً دو پردازهی در حال اجرا در میزبان میتوانند فایل سیستم مشترکی داشته باشند ولی دید آنها نسبت به کارت شبکه و تنظیمات آن متفاوت باشد و احساس کنند در دو شبکهی مجزا قرار دارند. به خاطر همین سطح از ایزوله کردن، فضانامها یکی از ابزارهای کلیدی برای کانتینری کردن برنامهها هستند.
فضانامهای موجود در لینوکس
- Unix Time Sharing (uts)
- Process ID (pid)
- Mount (mnt)
- Network (net)
- User ID / Group ID (user)
- Inter-Process Communications (ipc)
- Control Groups (cgroup)
- time
برای مشاهدهٔ فضانامهای موجود در سیستمعامل میتوانید از دستور lsns
استفاده کنید:
|
|
خروجی نمونه:
|
|
به صورت پیشفرض در ماشین میزبان از هر نوع فضانام یک مورد وجود دارد که توسط پردازهها مورد استفاده قرار میگیرد. پردازهها میتوانند فضانامهای بیشتری از نوعهای مختلفی ایجاد کنند و به آن متصل شوند. البته هر پردازه فقط میتواند در یک فضانام از یک نوع باشد. همچنین در صورتی که فضانام خالی باشد (مثلا هیچ پردازهای به آن متصل نشده باشد یا هیچ bind mount در آن وجود نداشته باشد و …) کرنل به صورت خودکار آن را حذف میکند. در ادامه به بررسی فضانامها میپردازیم.
Unshare چیست؟
در فضای سیستمعامل وقتی شما درخواست اجرای یک برنامه را صادر میکنید، سیستمعامل با اجرای یک سری روالها و سپس کپیکردن کد برنامه در حافظهی اصلی (رم) یک پردازهی جدید ایجاد میکند و به اصطلاح آن برنامه را اجرا میکند. در این فضا دو مفهوم پدر (parent) و فرزند (child) وجود دارد که به صورت پیشفرض با هم دادههای مشترکی دارند (برای اطلاعات بیشتر به fork و clone مراجعه کنید). برای ایزوله کردن پردازهها و حذف این نقاط مشترک میتوان از یک systemcall به نام unshare استفاده کرد. در واقع با استفاده از unshare میتوان یک برنامه را با فضانامهایی مختص به خودش که با پدرش یا دیگر پردازههای در حال اجرا مشترک نباشد، اجرا نمود.
اگر علاقهمند هستید جزئیات بیشتری بدانید راجع به set_ns ،clone ،nsenter و pivot_root مطالعه کنید.
فضانام UTS
هر ماشین دارای یک شناسه است که به آن hostname گفته میشود. درست مانند URL هر وبسایت از این شناسه میتوان برای اشاره به آن ماشین در داخل آن ماشین یا شبکه استفاده کرد. اگر یک ترمینال بر روی سیستمعامل لینوکس باز کنید با استفاده از دستور hostname میتوانید مقدار hostname فعلی را ببینید:
|
|
با قرار دادن یک پردازه در یک فضانام UTS اختصاصی، پردازه صاحب hostname و domainname اختصاصی خواهد شد و میتوان مقادیر دلخواهی برای hostname و domainname برای آن پردازه تنظیم کرد که این تغییر تاثیری در مقادیر مربوطه در ماشین میزبان نخواهد داشت.
تکنولوژیهای کانتینریکردن (مانند داکر) به هر کانتینر یک شناسهی تصادفی اختصاص میدهند که به صورت پیشفرض همین شناسه به عنوان hostname در آن کانتینر استفاده میشود. برای بررسی این موضوع با استفاده از دستور زیر میتوان یک کانتینر ایجاد کرد و مقدار hostname داخل کانتینر را مشاهده نمود.
|
|
با استفاده از unshare میتوانیم یک فضانام جدید UTS ایجاد کنیم و این مورد را آزمایش کنیم. برای اینکار باید unshare را با دسترسی کاربر root اجرا کنیم. در زمان فراخوانی unshare نوع فضانام و نام برنامهای که میخواهیم اجرا کنیم را باید وارد کنیم. در صورتی که نام برنامه وارد نشود به صورت پیشفرض از مقدار {SHELL}$ استفاده خواهد شد.
|
|
این کار باعث شده که bash داخل یک پردازهی جدید که فضانام UTS مختص به خودش را دارد اجرا شود. هر برنامهای که در این شل اجرا شود نیز این فضانام را به ارث خواهد برد. برای مثال همانطور که در بالا میبینید با اجرای hostname ابتدا همان مقدار تنظیم شده در ماشین میزبان نمایش داده میشود، سپس با تنظیم یک مقدار جدید، hostname در این فضانام تغییر میکند ولی در میزبان تغییر اعمال نشده است. این ایزوله کردن باعث میشود که میزبان و مهمان بدون تأثیر بر روی همدیگر مقادیر hostname را به مقداری دلخواه تغییر دهند.
فضانام شناسهٔ پردازهها (PID)
اگر شما دستور ps را داخل یک کانتینر اجرا کنید فقط پردازههای در حال اجرا در همان کانتینر را میبینید و به فهرست پردازههای در حال اجرا در ماشین میزبان دسترسی نخواهید داشت.
|
|
در واقع با بهرهبرداری از فضانام PID قابلیت محدود کردن امکان دسترسی به پردازههای در حال اجرا در میزبان برای کانتینرها با ایزوله کردن فهرست شناسهی پردازهها فراهم میشود. برای آزمایش فضانام PID مجدداً از unshare استفاده میکنیم:
|
|
اگه به خروجی دستورات بالا دقت کنید به نظر مشکلی وجود دارد. به غیر از دستور اول، بقیه دستورها با خطا مواجه شدهاند. اگه به متن خطا دقت کنید قالب آن از چپ به راست به صورت نام دستور، شناسهی پردازه و متن خطا میباشد. با اجرای هر دستور، شناسهی پردازهها در حال افزایش است و این نشان دهندهی اعمال شدن فضانام PID است، اما اگر نتوان بیش از یک پردازه در این فضانام اجرا کرد، کاملاً بلااستفاده باقی میماند. متن خطای دریافتی و همچنین توضیحات unshare نشان میدهند که:
|
|
پس با استفاده از fork– پردازهی جدید مستقیماً به صورت یک فرزند از unshare اجرا خواهد شد. حال مجدداً یک فضانام PID ایجاد میکنیم. با اجرای چند دستور مشاهده میکنیم خطای قبل رفع شده است:
|
|
سپس برای فهرست کردن پردازههای موجود در فضانام جدید از دستور ps استفاده میکنیم:
|
|
اگر در یک کانتینر ایجاد شده مثلاً با داکر دستور ps را وارد کنید فقط پردازههای موجود در همان کانتینر را مشاهده خواهید کرد. با توجه به خروجی دستور ps شاید اینطور به نظر برسد که فضانام PID جدید به درستی پردازهی جدید را ایزوله نکرده و پردازهی جدید به فهرست پردازههای در حال اجرا در میزبان دسترسی دارد اما در واقع اینطور نیست. فهرست پردازههای میزبان به این علت در دسترس است که دستور ps اطلاعات را از روی فایلهای موجود در مسیر proc/ میخواند و پردازش میکند. اگه از مسیر proc/ در میزبان ls بگیرید فهرستی از دایرکتوریها را مشاهده خواهید کرد که متناظر با پردازههای در حال اجرا در سیستمعامل میباشد که در داخل آنها اطلاعات کامل پردازهها به صورت فهرستی از فایلها وجود دارد.
|
|
برای اینکه در فضانام جدید دستور ps فقط اطلاعات پردازههای موجود در همین فضانام را برگردانند باید یک مسیر proc/ مجزا برای همین فضانام وجود داشته باشد که کرنل اطلاعات پردازههای موجود در آن را مدیریت کند که برای پیادهسازی آن میتوان از chroot استفاده کرد.
در زمان ایجاد یک کانتینر به طور پیشفرض به جای دسترسی کامل به فایلسیستم میزبان، پردازهها به بخش کوچکی از فایل سیستم دسترسی خواهند داشت، چون درست بعد از ایجاد کانتینر دایرکتوری root برای پردازهی اصلی داخل کانتینر تغییر میکند. در سیستمعامل لینوکس این عملیات با استفاده از chroot انجام میشود.
در توضیحات chroot اشاره شده است که chroot یک دستور یا shell را در دایرکتوری جدید اجرا میکند و همچنین اگر دستوری وارد نشود به صورت پیشفرض از مقدار متغیر SHELL$ استفاده خواهد کرد.
|
|
برای درک بهتر این فرآیند دستورات زیر را اجرا میکنیم:
|
|
با توجه به متن خطا به نظر میرسد که بعد از تغییر دایرکتوری root دسترسی به برنامههای موجود در دایرکتوری bin/ امکانپذیر نیست. بنابراین حتی امکان استفاده از دستوراتی مثل ls و id فراهم نیست. بنابراین تمامی فایلهای مورد نیاز، باید به دایرکتوری جدید انتقال داده شود؛ کاملاً مشابه زمانی که یک کانتینر واقعی ایجاد میشود. یک کانتینر از روی یک image ایجاد میشود که آن image حاوی تمامی فایلهایی است که پردازههای موجود در آن کانتینر به آن نیاز دارند. در واقع آن image دقیقاً حاوی فایلسیستمی است که پردازهها آن را میبینند.
بنابراین اگر ما فایلسیستم یک سیستمعامل کوچک مثل Alpine Linux را داشته باشیم میتوانیم به آن chroot کنیم و آن را در فضانام جدید اجرا کنیم. برای انجام این کار دستورات زیر را اجرا میکنیم که ابتدا یک دایرکتوری جدید ایجاد میشود، سپس آخرین نسخهی فعلی فایلسیستم را دانلود و استخراج میکنیم.
|
|
سپس با chroot در این دایرکتوری میتوانیم دستورات را در مسیر جدید اجرا کنیم.
|
|
با اتمام اجرای پردازه (یا دستور) فرزند، مجدداً کنترل به پردازهی پدر برمیگردد. برای اینکه بتوانیم دستورات بیشتری در دایرکتوری جدید اجرا کنیم میتوانیم یک شل را به عنوان پردازهی فرزند اجرا کنیم.
|
|
حالا با بهرهبرداری از قابلیت chroot و فضانامها میتوانیم یک فضانام PID ایجاد کرده و در زمان فراخوانی unshare دستور chroot را اجرا کنیم و سپس در داخل فضانام دایرکتوری proc/ فایلسیستم (image) را mount کنیم. با انجام این گامها یک قدم در ایزوله کردن پردازههای داخل کانتینر برداشته میشود و جزئیات پردازههای میزبان از دید پردازههای در حال اجرا در فضانام مخفی خواهد بود. برای آزمایش این موضوع دستورات زیر را اجرا میکنیم:
|
|
فضانام Mount
برای اینکه پردازههای داخل یک کانتینر به فایل سیستم میزبان دسترسی نداشته باشد، باید بین آنها مرزی ایجاد شود. با استفاده از فضانام mount میتوان این قابلیت را برای پردازهها فراهم کرد.
برای مثال در زیر ما در یک فضانام mount جدید یک bind mount ایجاد میکنیم. Bind mounts این قابلیت را فراهم میکند که بخشی از یک فایل سیستم که قبلاً mount شده را در یک دایرکتوری دیگه مجدداً mount کنید. با بهرهبرداری از این قابلیت میشود عملیات مختلفی مثل اشتراک گذاری فقط خواندنی، chroot jail یا کانتینری کردن را پیادهسازی کرد.
|
|
همانطور که میبینیم بعد از اعمال bind محتوای دایرکتوری src در dst نیز در دسترس است. با استفاده از findmnt میتوانیم جزئیات بیشتری را ببینیم که این mount در این فضانام ایجاد شده است و از دید میزبان مخفی میباشد.
|
|
برای مثال در یک ترمینال دیگر در میزبان و خارج از این فضانام همین دستور را اجرا میکنیم.
|
|
حالا اگر همین دستور findmnt را در فضانام ساخته شده مجدداً بدون پارامتر اجرا کنیم فهرست کاملی از mountهای میزبان را مشاهده خواهیم کرد. همانطور که در مورد فضانام PID اشاره کردیم، انتظار داریم این موارد از دید یک کانتینر مخفی شده باشد. مشابه شناسهی پردازهها کرنل از مسیر proc/ID/mounts/ اطلاعات مربوط به mountهای هر پردازه را میخواند. بنابراین وقتی که یک پردازه با یک فضانام اختصاصی ایجاد کنیم ولی همچنان از مسیر proc/ میزبان استفاده کند، پردازه میتواند به اطلاعات میزبان دسترسی داشته باشد.
برای ایزوله کردن یک پردازه نیاز است که ساخت یک فضانام جدید همراه با ایجاد یک فایلسیستم root و یک proc mount انجام شود.
|
|
به طور مشابه وقتی شما یک دایرکتوری در سیستم میزبان را در یک کانتینر mount میکنید (مثلاً در داکر: docker run -v path_in_host:path_in_container) ابتدا فایلسیستم root کانتینر در محل مناسب قرار میگیرد. سپس دایرکتوری مقصد در کانتینر ایجاد شده و در نهایت دایرکتوری مبدأ در مقصد bind mount میشود.
فضانام شبکه (Network)
یکی دیگر از سطوح ایزوله کردن، مربوط به فضای شبکه میباشد. فضانام شبکه به پردازهها اجازه میدهد که دید اختصاصی نسبت به کارت شبکه و جدول مسیریابی داشته باشند.
با استفاده از دستور زیر میتوان یک فضانام شبکه ایجاد کرد.
|
|
با استفاده از دستور lsns و تعیین نوع net میتوانیم جزئیات فضانام ایجاد شده را ببینیم. شناسهی پردازه را به خاطر بسپارید.
|
|
به صورت پیشفرض وقتی که یک پردازه را در فضانام شبکهی مختص به خودش قرار میدهیم، فقط یک کارت شبکه loopback دارد.
|
|
برای اینکه پردازهی در حال اجرا در کانتینر بتواند انتقال داده داشته باشد باید یک کارت شبکهی مجازی ایجاد کنیم که مثل دو سر یک کابل شبکه عمل میکند. در واقع فضانام شبکهی اختصاصی ایجاد شده برای پردازه را به فضانام شبکه میزبان (پیشفرض) متصل میکند. برای این کار در یک ترمینال دیگر دستور زیر را وارد میکنیم:
|
|
اگر بخواهیم دستور بالا را به زبان ساده بیان کنیم، ip link add اعلام میکند که میخواهیم یک اتصال ایجاد کنیم که در فضانام مربوط به پردازهی ۵۲۶۸ یک کارت شبکهی مجازی به اسم myeth1 ایجاد کند و آن را به فضانام پردازهی ۱ وصل کند و نام کارت شبکهی مجازی را myeth2 تنظیم کند.
حالا اگر در یک ترمینال در میزبان و همچنین در داخل فضانام جدید فهرست کارت شبکهها را بررسی کنیم این دو کارت شبکه را خواهیم دید.
در داخل فضانام شبکهی جدید:
|
|
در داخل میزبان:
|
|
در حالت فعلی هر دو کارت شبکهی جدید در وضعیت Down هستند و برای اینکه امکان انتقال داده فراهم شود باید وضعیت هر دو کارت به Up تغییر کند و به آنها آدرس IP تخصیص داده شود.
در ترمینال میزبان:
|
|
در فضانام جدید:
|
|
همانطور که اشاره کردیم با ایجاد یک فضانام شبکه، جدول مسیریابی و کارت شبکهها ایزوله خواهد شد و پردازهی موجود در کانتینر به اطلاعات میزبان دسترسی نخواهد داشت.
|
|
حالا هم در ترمینال ماشین میزبان و هم در فضانام جدید میتوانیم اتصال را تست کنیم. در ترمینال ماشین میزبان این دستور را اجرا کنید:
|
|
همینطور در فضانام:
|
|
مطابق خروجی بالا میبینیم که اتصال بین دو طرف برقرار شده است.
در قسمت اول با فضانامهای uts، pid، mount و network آشنا شدیم. در قسمت دوم این نوشتار فضانامهای user، ipc، cgroup و time را مورد بررسی قرار دادهایم.