دسته بندی:

محدود کردن پروسه‌های یک کاربر در لینوکس با ulimit و limits.conf

محدود کردن پروسه‌های یک کاربر در لینوکس با ulimit و limits.conf

حدود ۱۰ دقیقه مطالعه

یک کاربر بی‌احتیاط یا یک اسکریپت معیوب می‌تواند در چند ثانیه صدها پروسه بسازد، حافظه و CPU سرور را اشغال کند و کار را به جایی برساند که حتی SSH هم بالا نیاید. یکی از زیبایی‌های لینوکس این است که پشتیبان سرور را قادر می‌سازد تا در راستای استفاده بهتر از منابع سیستم، با ایجاد محدودیت‌های مورد نیاز برای پروسه‌های در حال اجرای هر کاربر، کنترل بهتری روی سرور داشته باشد. ابزار اصلی محدود کردن پروسه‌های یک کاربر در لینوکس، دستور ulimit و فایل /etc/security/limits.conf است.

در ادامه یاد می‌گیرید چگونه:

  • تعداد پروسه‌های شروع‌شده توسط یک user را محدود کنید
  • لیمیت‌های موجود را مشاهده نمایید
  • لیمیت‌های موجود را ویرایش نمایید
  • مطمئن شوید محدودیت واقعاً اعمال شده است
  • بفهمید چرا سرویس‌های systemd این محدودیت‌ها را نادیده می‌گیرند

پیش‌نیازها

  • دسترسی root به سرور
  • دقت بسیار بالا؛ یک عدد اشتباه در این فایل می‌تواند ورود کاربران یا اجرای سرویس‌ها را به کلی مسدود کند
  • یک سشن SSH باز با دسترسی root که تا پایان و تست کامل کار آن را نبندید؛ اگر تنظیمات اشتباه از آب درآمد، از همان سشن قبلی که هنوز محدودیت جدید روی آن اعمال نشده اصلاحش می‌کنید
  • یک نسخه پشتیبان از فایل پیکربندی:
cp -a /etc/security/limits.conf /root/limits.conf.bak

تفاوت soft limit و hard limit

در بسیاری از منابع فارسی (و در نسخه قدیمی‌تر همین مقاله) soft و hard به اشتباه «نرم‌افزاری» و «سخت‌افزاری» ترجمه شده‌اند. این ترجمه غلط است و هیچ ربطی به سخت‌افزار ندارد:

  • soft limit مقدار مؤثر و جاری است؛ همان چیزی که پروسه هنگام عبور از آن با خطا مواجه می‌شود. کاربر عادی می‌تواند این مقدار را تا سقف hard limit خودش بالا ببرد.
  • hard limit سقف مطلق است. فقط root می‌تواند آن را افزایش دهد. کاربر عادی فقط می‌تواند آن را کاهش دهد و این کاهش تا پایان همان سشن برگشت‌پذیر نیست.
  • اگر به جای soft یا hard از علامت - استفاده کنید، هر دو مقدار همزمان تنظیم می‌شوند.

ساختار فایل limits.conf

محدودیت‌های هر کاربر در فایل زیر تعریف می‌شود (نصبی در کار نیست؛ این فایل از قبل روی سیستم وجود دارد و فقط ویرایش می‌شود):

/etc/security/limits.conf

این فایل برای اعمال ulimit هایی استفاده می‌شود که ماژول pam_limits.so هنگام ساخته شدن سشن کاربر آن‌ها را روی پروسه اعمال می‌کند.

syntax هر خط این فایل به صورت زیر است:

<domain> <type> <item> <value>

Domain: محدودیت برای چه کسی؟

  • username — یک کاربر مشخص
  • @groupname — همه اعضای یک گروه
  • * — مقدار پیش‌فرض برای همه کاربران. دقت کنید که این wildcard شامل کاربر root نمی‌شود؛ برای محدود کردن root باید صریحاً نام root را بنویسید.
  • بازه‌های عددی هم پشتیبانی می‌شوند: 1000:2000 یک بازه uid و @1000:2000 یک بازه gid است. علامت % نیز فقط برای آیتم maxlogins کاربرد دارد.

Type: نوع محدودیت

یکی از سه مقدار soft، hard یا - که در بخش قبل توضیح داده شد.

Item: چه چیزی محدود شود؟

  • nproc — حداکثر تعداد پروسه‌های کاربر. مهم‌ترین آیتم برای جلوگیری از fork bomb.
  • nofile — حداکثر تعداد فایل‌های باز (file descriptor). عامل خطای معروف Too many open files.
  • fsize — حداکثر حجم فایلی که کاربر می‌تواند بسازد، بر حسب کیلوبایت.
  • as — حداکثر فضای آدرس (address space) بر حسب کیلوبایت.
  • core — حجم فایل core dump بر حسب کیلوبایت.
  • cpu — حداکثر زمان CPU بر حسب دقیقه.
  • memlock — حافظه‌ای که می‌توان قفل کرد، بر حسب کیلوبایت.
  • maxlogins — حداکثر تعداد ورود همزمان همان کاربر.

Value: مقدار محدودیت

یک عدد یا کلمه unlimited. واحد آیتم‌های حجمی کیلوبایت است، نه بایت و نه مگابایت. این اشتباه رایجی است: اگر عدد را به گمان مگابایت وارد کنید محدودیت هزار برابر تنگ‌تر از چیزی می‌شود که قصد داشتید، و اگر به گمان بایت وارد کنید هزار برابر بازتر و عملاً بی‌اثر.

یک مثال عملی

یک مثال خوب برای Limit:

@student          hard           nproc                20

خط فوق یک محدودیت hard برای گروه «student» با حداکثر ۲۰ پروسه ایجاد می‌کند.

در عمل معمولاً می‌خواهید هم یک soft limit بگذارید که کاربر با خطا روبه‌رو شود و هم یک hard limit به عنوان سقف نهایی. برای مثال، برای کاربری به نام appuser که یک اپلیکیشن روی آن اجرا می‌شود:

appuser    soft    nproc     256
appuser    hard    nproc     512
appuser    soft    nofile    8192
appuser    hard    nofile    16384

به جای دست‌کاری مستقیم limits.conf، بهتر است تغییرات خودتان را در یک فایل جداگانه داخل پوشه /etc/security/limits.d/ بنویسید تا از پیش‌فرض‌های توزیع جدا بماند و در به‌روزرسانی بسته‌ها دچار تداخل نشود:

vi /etc/security/limits.d/90-appuser.conf

نکته مهم درباره اولویت: فایل‌های .conf داخل limits.d به ترتیب حروف الفبا خوانده می‌شوند، پس نام فایل خود را با عدد بزرگ‌تری مثل 90- شروع کنید تا بعد از پیش‌فرض‌های توزیع خوانده شود. اما مهم‌تر از ترتیب فایل‌ها، «اختصاصی بودن» ورودی است: pam_limits ورودی مخصوص یک کاربر را بر ورودی گروه، و ورودی گروه را بر wildcard * ترجیح می‌دهد، فارغ از اینکه کدام زودتر خوانده شده باشد. بین دو ورودی هم‌سطح، آخرین مقدار خوانده‌شده برنده است. در توزیع‌های خانواده RHEL (CentOS، AlmaLinux، Rocky) معمولاً فایلی مانند 20-nproc.conf از قبل وجود دارد که یک مقدار پیش‌فرض nproc برای همه کاربران تعریف کرده است. قبل از هر تغییری محتوای این پوشه را ببینید:

ls -l /etc/security/limits.d/
cat /etc/security/limits.d/*.conf

بدون pam_limits هیچ چیز اعمال نمی‌شود

این فایل به تنهایی کاری نمی‌کند؛ ماژول pam_limits.so باید در پیکربندی PAM فعال باشد تا هنگام ساخته شدن سشن، محدودیت‌ها اعمال شوند. برای بررسی:

grep -R pam_limits /etc/pam.d/

در خانواده RHEL این خط معمولاً در /etc/pam.d/system-auth و /etc/pam.d/password-auth و در دبیان و اوبونتو در /etc/pam.d/common-session قرار دارد و شکل آن چنین است:

session    required     pam_limits.so

نتیجه مستقیم این معماری: محدودیت‌ها فقط روی سشن‌هایی اعمال می‌شوند که بعد از ویرایش فایل ساخته شده‌اند. سشن SSH فعلی، پنجره‌های باز screen یا tmux و پروسه‌های در حال اجرا همچنان با محدودیت‌های قدیمی کار می‌کنند. کاربر باید خارج و دوباره وارد شود.

چگونه مطمئن شویم اعمال شده است

ساده‌ترین راه، دیدن محدودیت‌های سشن جاری است:

ulimit -a          # مقادیر soft
ulimit -H -a       # مقادیر hard
ulimit -u          # تعداد مجاز پروسه
ulimit -n          # تعداد مجاز فایل باز

برای دیدن محدودیت‌های یک کاربر دیگر، بهتر است یک سشن login واقعی برای او ساخته شود؛ خط تیره در دستور زیر شل را به حالت login می‌برد و محیط کامل کاربر را بارگذاری می‌کند تا نتیجه همان چیزی باشد که کاربر واقعاً هنگام ورود می‌بیند:

su - appuser -c 'ulimit -a'

اگر می‌خواهید محدودیت‌های موجود برای یک فرایند (پروسه) خاص را مشاهده نمایید، با استفاده از دستور cat محدودیت‌های آن پروسه را ببینید:

# cat /proc/PID/limits

برای پیدا کردن PID پروسه می‌توانید از دستور ps یا pgrep استفاده نمایید:

ps -u appuser
pgrep -u appuser -l

مثال برای مشاهده محدودیت‌های موجود برای یک پروسه:

# cat /proc/2497/limits

خروجی به شکل زیر خواهد بود:

Limit                     Soft Limit           Hard Limit           Units
Max cpu time              unlimited            unlimited            seconds
Max file size             unlimited            unlimited            bytes
Max data size             unlimited            unlimited            bytes
Max stack size            8388608              unlimited            bytes
Max core file size        0                    unlimited            bytes
Max resident set          unlimited            unlimited            bytes
Max processes             32042                32042                processes
Max open files            1024                 4096                 files
Max locked memory         65536                65536                bytes
Max address space         unlimited            unlimited            bytes
Max file locks            unlimited            unlimited            locks
Max pending signals       32042                32042                signals
Max msgqueue size         819200               819200               bytes
Max nice priority         0                    0
Max realtime priority     0                    0
Max realtime timeout      unlimited            unlimited            us

خطوط فوق توضیح مختصری از محدودیت‌های موجود را نمایش می‌دهند. ستون Units واحد هر سطر را مشخص می‌کند؛ توجه کنید که آیتم‌های حجمی در این خروجی بر حسب بایت گزارش می‌شوند، در حالی که در limits.conf همان مقادیر بر حسب کیلوبایت وارد می‌شوند.

روی توزیع‌های امروزی ابزار prlimit (از بسته util-linux) هم همین اطلاعات را خواناتر نشان می‌دهد و حتی می‌تواند محدودیت یک پروسه در حال اجرا را بدون ری‌استارت تغییر دهد؛ در دستور زیر ۴۰۹۶ مقدار soft و ۸۱۹۲ مقدار hard است:

prlimit --pid 2497
prlimit --pid 2497 --nofile=4096:8192

برای شمارش پروسه‌های واقعی هر کاربر هم این دستور مفید است. توجه داشته باشید که در لینوکس هر thread یک task حساب می‌شود و در سقف nproc شمرده می‌شود؛ به همین دلیل سرویس‌های چندنخی مانند MySQL یا اپلیکیشن‌های Java خیلی زودتر از انتظار به سقف می‌خورند:

ps -eLo user= | sort | uniq -c | sort -rn | head

سرویس‌های systemd از limits.conf پیروی نمی‌کنند

این مهم‌ترین نکته‌ای است که در راهنماهای قدیمی غایب است. چون سرویس‌ها توسط systemd و بدون عبور از PAM اجرا می‌شوند، هرچه در limits.conf بنویسید روی Nginx، MySQL، PHP-FPM و بقیه دیمن‌ها بی‌اثر است. مقدار فعلی یک سرویس را این‌طور ببینید:

systemctl show nginx -p LimitNOFILE -p LimitNPROC -p TasksMax

برای تغییر، به جای دست زدن به یونیت‌فایل اصلی، یک drop-in بسازید:

systemctl edit nginx

و این محتوا را وارد کنید:

[Service]
LimitNOFILE=65535
LimitNPROC=512
TasksMax=1024

دستور systemctl edit خودش پس از ذخیره، پیکربندی را بازخوانی می‌کند و فقط ری‌استارت سرویس می‌ماند. اگر فایل drop-in را دستی در /etc/systemd/system/nginx.service.d/ ساخته‌اید، اول daemon-reload لازم است:

systemctl daemon-reload
systemctl restart nginx

دو نکته: اول اینکه RLIMIT_NPROC در سطح کاربر (UID) شمرده می‌شود، پس برای سرویس‌هایی که با کاربر root یا با کاربری مشترک بین چند سرویس اجرا می‌شوند نتیجه مورد انتظار را نمی‌دهد؛ در این حالت TasksMax که بر پایه cgroup کار می‌کند انتخاب درست‌تری است. دوم اینکه مقدار پیش‌فرض همه سرویس‌ها در /etc/systemd/system.conf با کلیدهایی مانند DefaultLimitNOFILE تعیین می‌شود و پس از تغییر آن به systemctl daemon-reexec نیاز دارید.

اگر با سرورهای قدیمی CentOS 6 یا اسکریپت‌های SysV init سر و کار دارید، آنجا محدودیت‌ها از پروسه init به ارث می‌رسید و گاهی مستقیماً با ulimit داخل اسکریپت راه‌اندازی یا فایل‌های /etc/sysconfig/ تنظیم می‌شد. این روش منسوخ است؛ روی CentOS 7 به بعد و AlmaLinux، Rocky، دبیان و اوبونتو باید از drop-in های systemd و systemctl restart به جای service ... restart و /etc/init.d/... استفاده کنید.

دو سناریوی واقعی

fork bomb: اسکریپتی که در یک حلقه بی‌پایان خودش را fork می‌کند تا جدول پروسه‌های کرنل پر شود. وقتی جدول پر شد، حتی root هم نمی‌تواند شل جدیدی باز کند و راه‌حل معمولاً ری‌استارت سخت سرور است. یک hard nproc معقول روی کاربران غیرسیستمی، این حمله را به یک خطای ساده برای همان کاربر تبدیل می‌کند و بقیه سرور سرپا می‌ماند.

پروسه فراری: یک کرون‌جاب یا اسکریپت PHP که به دلیل باگ، مدام پروسه یا اتصال جدید باز می‌کند. اینجا معمولاً اول خطای Too many open files در لاگ ظاهر می‌شود که با nofile کنترل می‌شود؛ اما محدودیت درمان باگ نیست، فقط جلوی سرایت آسیب به کل سرور را می‌گیرد.

چرا محدودیت من اعمال نشد؟

  • کاربر خارج و دوباره وارد نشده است؛ محدودیت فقط روی سشن‌های جدید اعمال می‌شود.
  • یک ورودی اختصاصی‌تر (مثلاً یک خط به نام همان کاربر در فایلی داخل /etc/security/limits.d/) بر خط گروهی یا wildcard شما غالب شده است.
  • ماژول pam_limits.so در سرویس PAM مربوطه (مثلاً sshd یا crond) فعال نیست.
  • پروسه‌ای که بررسی می‌کنید یک سرویس systemd است و اصلاً از PAM عبور نکرده.
  • خواستید soft را از hard بالاتر ببرید؛ کرنل چنین اجازه‌ای نمی‌دهد.
  • دامنه را * گذاشته‌اید و انتظار داشتید روی root هم اعمال شود.

چه زمانی این کار را نکنید

  • روی هاست اشتراکی که با CloudLinux مدیریت می‌شود، محدودسازی منابع کاربران معمولاً با LVE انجام می‌شود و دست بردن در limits.conf بیشتر باعث تداخل می‌شود تا نتیجه.
  • داخل کانتینر (Docker یا LXC)، محدودیت‌های اصلی از سمت هاست و cgroup اعمال می‌شوند.
  • برای محدود کردن مصرف حافظه سراغ as نروید؛ این آیتم فضای آدرس مجازی را می‌شمارد و نرم‌افزارهایی مانند JVM و MySQL که حجم زیادی حافظه مجازی رزرو می‌کنند با آن از کار می‌افتند. برای سقف حافظه واقعی از MemoryMax در یونیت systemd استفاده کنید.
  • هرگز یک hard nproc بسیار کوچک را روی دامنه * اعمال نکنید؛ کاربران سرویس مانند mysql یا nginx هم مشمول آن می‌شوند و سرویس‌ها بالا نمی‌آیند.

اگر به تنظیمات بیشتری نیاز دارید، فهرست کامل آیتم‌های قابل تنظیم و جزئیات هر کدام در مستندات رسمی این فایل آمده است؛ می‌توانید روی خود سرور دستور man 5 limits.conf را اجرا کنید یا این مقاله را ببینید.

آموزش‌های مرتبط