سی‌گروپس

**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 اضافه شده است.https://kernelnewbies.org/Linux_5.2#Add_freezer_controller_for_cgroups_v2

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

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

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

### پیاده‌سازی در کرنل
برای پیاده‌سازی این قابلیت داده ساختار زیر به cgroups-defs.h اضافه شده است:
```c
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 صدا زده می‌شود:
```c
/*
 * 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` اجرا می‌شود. این تابع در صورتی که مقدار نوشته شده در فایل معتبر، یعنی صفر یا یک باشد تابع 
```c
cgroup_freeze(cgrp, freeze)
```
را صدا می‌زند. در این تابع به ازای هر کدام از گروه‌ها در زیردرخت cgrp مقدار `e_freeze` که در واقع تعداد فریزهای انجام شده روی یک گروه است بسته به یک یا صفر بودن freeze یکی زیاد یا کم می‌شود. در صورتی که این مقدار قبلا بزرگ‌تر از صفر بوده باشد و اکنون نیز بزرگ‌تر از بماند یعنی cgroup پیش از این نیز در حالت فریز قرار داشته است و لازم نیست کاری انجام شود. در غیر این صورت تابع
```c
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` برای هر دوی مبدا و مقصد صدا زده می‌شود. در نهایت نیز
```c
cgroup_freeze_task(task, test_bit(CGRP_FREEZE, &dst->flags));
```
صدا زده می‌شود که باعث می‌شود تسک با توجه به وضعیت cgroup مقصد (dst) به حالت مناسب برود.

## منابع

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

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