محدود کردن پروسههای یک کاربر در لینوکس با 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 را اجرا کنید یا این مقاله را ببینید.