سی‌گروپس

cgroups (مخفف control groups به معنی گروه‌های کنترلی) یک قابلیت هستهٔ لینوکس است که منابع مجموعه‌ای از فرایندها را مدیریت و مجزاسازی می‌کند.

cgroup v2

تاریخچه 
نسخه‌ی دوم سی‌گروپس در سال ۲۰۱۶ به همراه کرنل نسخه ۴.۵ منتشر شد. Tejun Heo مسئول طراحی و بازنویسی مجدد سی‌گروپس بوده است.

ساختار 
cgroup مکانیسمی برای سازمان‌دهی سلسله‌مراتبی پردازه‌ها و اختصاص منابع سیستم در طول این سلسله‌مراتب است. این ویژگی از دو قسمت اصلی تشکیل شده است: هسته و کنترل‌کننده‌ها . وظیفه‌ی اصلی هسته سازمان‌دهی پردازه‌ها در یک ساختار سلسله‌مراتبی است. کنترل‌کننده‌ها نیز وظیفه‌ی توزیع یک منبع خاص سیستم در طول این سلسله‌مراتب را دارند.

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

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

 cgroup.procs: لیستی از PID های پردازه‌هایی که درون این گروه قرار دارند.
 cgroup.controllers: لیست کنترل‌کننده‌های قابل استفاده برای این گروه.
 cgroup.subtree_control: لیست کنترل‌کننده‌های فعال یا غیرفعال شده برای این گروه. برای فعال یا غیرفعال کردن یک کنترل‌کننده باید در این فایل نوشته شود.
 cgroup.events: شامل دو رکورد populated و frozen است که برای هر کدام مقدار صفر یا یک مشخص شده است و وضعیت cgroup را نشان می‌دهند.
 cgroup.freeze: برای فریز کردن یا از فریز درآوردن یک cgroup باید در این فایل صفر یا یک نوشته شود.

کنترل‌کننده‌ها 
از بین کنترل‌کننده‌هایی که برای cgroup ها وجود دارند می‌توان به کنترل‌کننده‌های CPU ،Memory ،IO ، Writeback و PID اشاره کرد. با فعال‌کردن هر کدام از کنترل‌کننده‌ها فایل‌هایی با پیشوند کنترل‌کننده به پوشه‌ی فرزندان cgroup اضافه می‌شوند و در آن‌ها می‌توان تنظیمات مربوط به منابع را انجام داد.

برای توزیع منابع بین گروه‌ها چهار روش وجود دارد که به دوتا از آن‌ها اشاره می‌کنیم: یکی از آن‌ها با استفاده از وزن است؛ یعنی به هر گروه فرزند یک عدد نسبت داده می‌شود و منبع بر اساس نسبت این اعداد به مجموع توزیع می‌شود. این روش در توزیع CPU کاربرد دارد. روش دیگر محدود کردن منبع است؛ یعنی برای هر گروه یک مقدار بیشینه تعیین شود که مقدار استفاده‌ی آن از منبع از آن مقدار بیشتر نشود. به طور مثال برای توزیع IO می‌توان از این نوع تخصیص استفاده کرد.

قابلیت فریزر cgroup v2 
با استفاده از این ویژگی می‌توان تمام تسک‌های موجود در  یک سی‌گروپس را به طور موقت متوقف کرد. این ویژگی برای cgroup v2 در هسته‌ی لینوکس نسخه 5.2 اضافه شده است.

کاربردها 
قابلیت فریزر  cgroup در برنامه‌های مدیریت دسته‌ای کارهای سیستم که در آن‌ها مدیر یک سیستم می‌خواهد مجموعه‌ای از کارها رو هم‌زمان شروع یا متوقف کند کاربرد دارد. این گونه برنامه‌ها به خصوص در خوشه‌های HPC استفاده می‌شوند که در آن‌ها تعداد زیادی گره مختلف به صورت توزیع شده وجود دارند.

از سیگنال‌های کنترلی SIGSTOP و SIGCONT نیز می‌توان برای توقف یا ادامه دادن یک پردازه استفاده کرد. اما مشکل این سیگنال‌ها این است که می‌توانند توسط پردازه‌ی پدر مشاهده شوند و برنامه‌هایی مثل shell یا gdb در صورت استفاده از این سیگنال‌ها برای متوقف کردن و ادامه دادن برنامه دچار مشکل می‌شوند.

این قابلیت هم‌چنین می‌تواند برای حالت‌برداری از برنامه‌ها استفاده شود. با فریز کردن یک cgroup می‌توان پردازه‌های آن را به یک حالت غیرفعال برد و از آن‌ها یک تصویر ذخیره کرد و/یا با استفاده از رابط‌های کرنل اطلاعات مورد نظر درباره‌ی آن‌ها را ذخیره کرد و پس از آن با درآوردن cgroup از حالت فریز شده آن‌ها را دوباره فعال کرد.

پیاده‌سازی در کرنل 
برای پیاده‌سازی این قابلیت داده ساختار زیر به cgroups-defs.h اضافه شده است:struct cgroup_freezer_state {
	/* Should the cgroup and its descendants be frozen. */
	bool freeze;

	/* Should the cgroup actually be frozen? */
	int e_freeze;

	/* Fields below are protected by css_set_lock */

	/* Number of frozen descendant cgroups */
	int nr_frozen_descendants;

	/*
	 * Number of tasks, which are counted as frozen:
	 * frozen, SIGSTOPped, and PTRACEd.
	 */
	int nr_frozen_tasks;
};این داده ساختار به داده ساختار cgroup با عنوان freezer اضافه شده است. هم چنین در پرچم‌های cgroup دو بیت به CGRP_FREEZE و CGRP_FROZEN اختصاص داده شده است که به ترتیب نشان‌دهنده‌ی این هستند که cgroup باید فریز شود یا خیر و آیا cgroup فریز شده است یا خیر. به داده ساختار task_struct نیز یک پرچم frozen اضافه شده که مشخص می‌کند تسک فریز شده است یا خیر.

فرایند فریز شدن یک تسک 
در JOBCTL یک تله برای فریز کردن یک تسک در نظر گرفته شده است. تابع do_freezer_trap مسئول رسیدگی به این تله است. در این تابع پس از اطمینان از این که سیگنال یا تله‌ی پراهمیت دیگری که به آن‌ها رسیدگی نشده باشد وجود نداشته باشد تابع cgroup_enter_frozen را صدا می‌زند که در ادامه توضیح داده خواهد شد. پس از آن نیز تابع freezable_schedule صدا زده می‌شود که پرچمی را در تسک ست می‌کند که در زمان‌بندی شرکت داده نشود و سپس تسک بعدی را زمان‌بندی می‌کند. 

تابع freezable_schedule پیش از پیاده‌سازی فریزر cgroup نیز وجود داشته است و به طور مثال برای خوابیدن برای مدت زمان خاص در تابع do_nanosleep یا خوابیدن در صف در تابع futex_wait_queue_me استفاده شده است. در نتیجه برای این که تسک‌های فریز شده در زمان‌بند زمان‌بندی نشوند نیازی به تغییرات در زمان‌بند وجود ندارد.

در تابع cgroup_enter_frozen پرچم frozen تسک در صورتی که یک نباشد یک می‌شود و بعد از زیاد کردن تعداد تسک‌های فریز شده‌ی cgroup تسک، تابع cgroup_update_frozen برای cgroup صدا زده می‌شود:/*
 * Revisit the cgroup frozen state.
 * Checks if the cgroup is really frozen and perform all state transitions.
 */
void cgroup_update_frozen(struct cgroup *cgrp)
{
	bool frozen;

	lockdep_assert_held(&css_set_lock);

	/*
	 * If the cgroup has to be frozen (CGRP_FREEZE bit set),
	 * and all tasks are frozen and/or stopped, let's consider
	 * the cgroup frozen. Otherwise it's not frozen.
	 */
	frozen = test_bit(CGRP_FREEZE, &cgrp->flags) &&
		cgrp->freezer.nr_frozen_tasks == __cgroup_task_count(cgrp);

	if (frozen) {
		/* Already there? */
		if (test_bit(CGRP_FROZEN, &cgrp->flags))
			return;

		set_bit(CGRP_FROZEN, &cgrp->flags);
	} else {
		/* Already there? */
		if (!test_bit(CGRP_FROZEN, &cgrp->flags))
			return;

		clear_bit(CGRP_FROZEN, &cgrp->flags);
	}
	cgroup_file_notify(&cgrp->events_file);

	/* Update the state of ancestor cgroups. */
	cgroup_propagate_frozen(cgrp, frozen);
}در این تابع بررسی می‌شود که آیا یک cgroup باید به حالت frozen دربیاید یا خیر. یک cgroup باید فریز شود اگر پرچم CGRP_FREEZE آن یک باشد و تعداد تسک‌های فریز شده‌ی آن با تعداد کل تسک‌های زیردرختش برابر باشد. در صورتی که وضعیت یک cgroup از فریز شده به نشده یا بلعکس تغییر کرده باشد این تغییر در فایل مربوط به رخدادهای آن نوشته می‌شود و هم چنین یک تابع به نام cgroup_propagate_frozen صدا زده می‌شود که وظیفه‌ی آن انتشار وضعیت cgroup به اجداد آن است تا تعداد تسک‌های فریز شده‌ی آن‌ها نیز تغییر کند و در صورت لزوم وضعیت فریز بودن یا نبودن آن‌ها در فایل رخدادهایشان به‌روزرسانی شود.

فرایند فریز شدن یک cgroup 

هنگامی که تغییری در فایل cgroup.freeze که در قسمت اول ذکر شد صورت بگیرد تابع cgroup_freeze_write اجرا می‌شود. این تابع در صورتی که مقدار نوشته شده در فایل معتبر، یعنی صفر یا یک باشد تابع cgroup_freeze(cgrp, freeze)را صدا می‌زند. در این تابع به ازای هر کدام از گروه‌ها در زیردرخت cgrp مقدار e_freeze که در واقع تعداد فریزهای انجام شده روی یک گروه است بسته به یک یا صفر بودن freeze یکی زیاد یا کم می‌شود. در صورتی که این مقدار قبلا بزرگ‌تر از صفر بوده باشد و اکنون نیز بزرگ‌تر از بماند یعنی cgroup پیش از این نیز در حالت فریز قرار داشته است و لازم نیست کاری انجام شود. در غیر این صورت تابعcgroup_do_freeze(cgrp, freeze)صدا زده می‌شود که در این تابع پرچم CGRP_FREEZE مقداردهی می‌شود و پس از آن به ازای هر تسک داخل cgroup تابع cgroup_freeze_task اجرا می‌شود. 

لازم به ذکر است که پردازه‌های کرنل (kthreads) قابلیت فریز شدن ندارند و هر جایی که بخواهد تغییری در وضعیت تسک صورت بگیرد این نکته چک می‌شود و این پردازه‌ها کنار گذاشته می‌شود.

در تابع cgroup_freeze_task در صورتی که که تسک در حال فریز شدن باشد پرچم TRAP_FREEZE مربوط به jobctl را یک می‌کند و با استفاده از signal_wake_up به پردازه اعلام می‌کند که یک سیگنال برای رسیدگی دارد. هنگامی که پردازه شروع به اجرا کند به سیگنال گفته شده با استفاده از تابع do_freezer_trap که در بالا توضیح داده شد رسیدگی می‌کند و به حالت خواب می‌رود. در صورتی که تسک در حال بیرون آمدن از حالت فریز شدن باشد پرچم ذکر شده صفر می‌شود و با استفاده از تابع wake_up_process تسک بیدار می‌شود.

هنگامی که یک تسک بیدار می‌شود اگر پرچم frozen آن یک بوده باشد یعنی به تازگی از حالت فریز بیرون آمده است که در این صورت تابع cgroup_leave_frozen صدا زده می‌شود. در این تابع پس از کاهش تعداد بچه‌های فریزشده‌ی cgroup تابع cgroup_update_frozen صدا زده می‌شود که پیش‌تر توضیح داده شد و هم چنین پرچم frozen تسک را نیز صفر می‌کند.

مهاجرت یک تسک به cgroup دیگر 
نکته‌ی آخر فرایند مهاجرت یک تسک از یک cgroup به دیگری است. هنگامی که یک تسک به cgroup ای برود که فریز شده است متوقف می‌شود. هم چنین اگر یک تسک از یک cgroup فریز شده به گروه غیر فریز برود از حالت فریز بیرون می‌آید. برای پیاده‌سازی این قسمت در تابع cgroup_migrate_execute که برای مهاجرت تسک‌ها استفاده می‌شود تابع cgroup_freezer_migrate_task صدا زده می‌شود. در این تابع پس از کاهش و افزایش تعداد تسک‌های فریز شده‌ی cgroup مبدا و مقصد، تابع cgroup_update_frozen برای هر دوی مبدا و مقصد صدا زده می‌شود. در نهایت نیزcgroup_freeze_task(task, test_bit(CGRP_FREEZE, &dst->flags));صدا زده می‌شود که باعث می‌شود تسک با توجه به وضعیت cgroup مقصد (dst) به حالت مناسب برود.

منابع 

 
مستند کرنل لینوکس «Control groups v2»
 مستند کرنل لینوکس « Control groups v1  freezer subsystem »
 منبع کد لینوکس «kernel git»

امنیت سیستم‌عامل
نرم‌افزارهای مرتبط با مجازی‌سازی لینوکس
ویژگی‌های کرنل لینوکس