فایلدسکریپتور و EMFILE؛ چرا فایل موجود باز نمیشود؟
در این درس ۸ بخش
فرض کنید سرویس مدتی سالم کار میکند، سپس برای بازکردن فایل تنظیمات موجود پیام Too many open files میدهد. نصب دوبارهٔ برنامه و ساخت دوبارهٔ فایل، هنوز با این شاهد تناسبی ندارد. در این درس به منبع محدود دیگری نگاه میکنیم: جدول توصیفگرهای فرایند. با یک فرزند کوتاهعمر خطا را واقعاً ایجاد میکنیم و بعد نشان میدهیم چرا بستهشدن منابع و محدودهٔ محدودیت مهماند. هیچ سقف سراسری یا تنظیم سرویس واقعی را تغییر نمیدهیم؛ مسئلهٔ ظرفیت تولیدی پس از تشخیص و با دادهٔ همان سرویس مطرح میشود.
توصیفگر، نام فایل روی دیسک نیست
فایلدسکریپتور یا توصیفگر، عدد صحیح نامنفیای است که فرایند از آن برای اشاره به یک منبع باز استفاده میکند. فایل معمولی، سوکت و لوله میتوانند توصیفگر داشته باشند. صفر، یک و دو معمولاً برای ورودی و دو جریان خروجی استفاده میشوند، اما بازبودن و نقششان به محیط اجرای واقعی وابسته است. عددی مثل هفت، شناسهٔ جهانی همان فایل در همهٔ برنامهها نیست. مدخل #linux-file-descriptor را کنار #linux-process بخوانید؛ مالکیت و معنای عدد را در محدودهٔ فرایند بررسی میکنیم.
میان ورودی جدول توصیفگر و open file description تفاوت وجود دارد. نمونهٔ دوم، وضعیت بازشدن مانند مکان فعلی خواندن را نگه میدارد. دو توصیفگر حاصل از dup میتوانند به یک وضعیت باز مشترک اشاره کنند؛ دو open جدا روی همان مسیر الزاماً این اشتراک را ندارند. بستن یک توصیفگر، همهٔ ارجاعهای دیگر به آن وضعیت را خودکار نابود نمیکند. این تمایز برای لولهٔ والد و فرزند، فایلهای لاگ و تشخیص منبعِ باقیمانده مهم است؛ صرف دیدن نام مسیر تمام رابطه را نشان نمیدهد.
آزمایش کوچک؛ دو اشارهگر مشترک و یک بازشدن مستقل
فایل examples/02-descriptor-offset.py فقط چهار بایت ساختگی در پوشهٔ موقت میسازد. نخست فایل را باز، سپس توصیفگرش را تکثیر میکند و در پایان یک open مستقل میسازد. پیش از اجرا حدس بزنید سه خواندن یکبایتی چه میدهند. finally تمام توصیفگرهایی را که واقعاً ساخته شدهاند میبندد؛ اگر میانهٔ عملیات استثنا رخ دهد، بستن منابع فقط به رسیدن به آخرِ مسیر موفق وابسته نیست.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory(prefix="dars-fd-") as folder:
path = Path(folder) / "letters.txt"
path.write_bytes(b"ABCD")
first = os.open(path, os.O_RDONLY)
duplicate = separate = None
try:
duplicate = os.dup(first)
separate = os.open(path, os.O_RDONLY)
a = os.read(first, 1)
b = os.read(duplicate, 1)
c = os.read(separate, 1)
assert (a, b, c) == (b"A", b"B", b"A")
print(a.decode(), b.decode(), c.decode())
finally:
for descriptor in (separate, duplicate, first):
if descriptor is not None:
os.close(descriptor)
خروجی A B A است. خواندن از first مکان مشترک را یک بایت جلو میبرد؛ duplicate از همان مکان B را دریافت میکند. separate بازشدن مستقلی دارد و از A شروع میکند. عدد دقیق توصیفگرها در آزمون چاپ نمیشود، چون به محیط وابسته است و معیار آموزشی ما نیست. این مثال دربارهٔ مکان خواندن فایل معمولی است؛ از آن نتیجه نگیرید همهٔ انواع منابع، رفتار seek یا مفهوم offset یکسانی دارند.
حد نرم، سقف سخت و دامنهٔ واقعی محدودیت
RLIMIT_NOFILE حد مرتبط با شمارهٔ توصیفگرهای قابل تخصیص فرایند است. حد نرم در عمل اعمال میشود و حد سخت سقف تغییر آن است. فرایند عادی نمیتواند حد نرم را بالاتر از سقف سخت ببرد و افزایش حد سخت اختیار جدا میخواهد. در مشاهدهٔ عمومی، بهجای «سیستم اجازهٔ چند فایل میدهد؟» بپرسید «کدام فرایند، با کدام حد مؤثر؟». وجود شکاف در شمارهها، توصیفگرهای ارثرسیده و ابزار اندازهگیری هم باعث میشود یک شمارش ساده همیشه معادل ظرفیت باقیمانده نباشد.
EMFILE و ENFILE را یکی ندانید. اولی به حد توصیفگر فرایند مربوط است؛ دومی به محدودیت جدول فایلهای باز در سطح سیستم اشاره میکند. در هر دو، عملیات مشابهی ممکن است شکست بخورد، اما دامنهٔ بررسی فرق دارد. ایجاد عمدی ENFILE با مصرف ظرفیت کل میزبان تمرین این دوره نیست. همینطور بالابردن حد یک شل تعاملی، حد فرایندی را که قبلاً تحت مدیر سرویس آغاز شده خودکار عوض نمیکند. #linux-resource-limit این مرز را در مثال کوتاه خلاصه میکند.
اجرای EMFILE در فرزند محدود و کوتاهعمر
از پوشهٔ project فرمان بعدی را با کاربر معمولی اجرا کنید. آزمایش ابتدا محدودیت والد را ثبت میکند. فقط فرزند خودش حد نرم کوچکی، معمولاً ۳۲، میگیرد و حداکثر ۶۴ بار /dev/null را برای خواندن باز میکند. این وسیله فایل عظیم نمیسازد و دیسک را پر نمیکند. منابع در فرزند بسته میشوند و پس از پایان، والد دوباره بررسی میشود. حد سخت را بالا نمیبریم و هیچ تنظیمی برای نشستهای آینده ذخیره نمیکنیم.
python3 lab.py emfileJSON موفق باید case برابر emfile، errno برابر EMFILE و parent_limit_unchanged برابر true داشته باشد. opened تعداد بازشدنهای واقعاً موفق در همین اجراست؛ عدد ۲۹ را برای همهٔ دستگاهها حفظ نکنید. soft_limit معمولاً ۳۲ است، اما با سقف موجود سازگار میشود؛ اگر پیشنیاز حداقل لازم وجود نداشته باشد، skipped و خروج ۷۷ میگیرید. هدف، مشاهدهٔ شکست با مقدار کوچک و جمعکردن کامل منابع است، نه ساختن بار زیاد یا اثبات ظرفیت هزاران اتصالِ یک سرویس واقعی.
چگونه از این آزمایش به یک فرضیهٔ عملی برسیم؟
اگر سرویس واقعی با افزایش درخواستها، توصیفگرهای بیشتری نگه میدارد، سه فرضیهٔ متفاوت داریم: نشت در مسیر خطا، همزمانی بیش از طراحی، یا حدی که با بار مورد توافق سازگار نیست. یک عکس از تعداد منابع بهتنهایی میان آنها انتخاب نمیکند. روند را همراه نرخ درخواست و عمر اتصال بررسی کنید و یک بار کنترلشده در محیط مناسب بازتولید کنید. پروندهٔ تشخیص باید فرایند دقیق، زمان، نوع منابع و نمونهٔ مسیر کد را داشته باشد؛ نام سرویسِ مشابه روی میزبان دیگر کافی نیست.
در لینوکس /proc/PID/fd دیدی از توصیفگرهای همان PID میدهد، اما مجوز و پایداری لحظهای داده مهماند. خواندن فهرست در حال تغییر یک تراکنش اتمی نیست و برنامهٔ مشاهدهگر نیز برای خودش منابع باز میکند. در این تمرین، فقط خود فرایند فرمان زیر را میشناسیم و به مسیرهای فرایندهای دیگر سر نمیزنیم. خروجی خواندنیِ محدودیت، اختیار تغییر آن یا شواهد نشت را بهخودیخود نمیسازد.
python3 -c "import resource; print(resource.getrlimit(resource.RLIMIT_NOFILE))"زوج چاپشده ترتیب نرم و سخت دارد و عددهایش وابسته به محیطاند. مقایسهٔ این زوج با JSON آزمایش نشان میدهد محدودیت فرزند را نباید محدودیت والد معرفی کرد. اگر برنامهٔ پایش داخل یک کانتینر یا نشست دیگر اجرا شود، دامنهٔ مشاهده را نیز ثبت کنید. برای تبدیل مسئله به اقدام، ابتدا صاحب توصیفگر و مسیرِ نگهداری آن را پیدا کنید؛ تغییر همزمان حد، شمار worker و زمانبندی، رابطهٔ علت و نتیجه را مبهم میکند.
اصلاح کد را از پنهانکردن علامت جدا کنید
برای منابع کوتاهعمر، الگوی with یا finally کمک میکند مسیر موفق و خطا هر دو به close برسند. این ابزارها جای طراحیِ مالکیت منبع را نمیگیرند: معلوم کنید چه بخش برنامه آن را ساخته و چه بخش باید پایان عمرش را اعلام کند. اگر منبع قرار است مشترک بماند، زودبستن آن نیز خطاست و میتواند EBADF بسازد. اصلاح خوب با آزمونی همراه است که یک مسیر شکست را تکرار میکند و نشان میدهد منابع پس از هر دور جمع میشوند؛ فقط یک اجرای عادی کافی نیست.
تمرینها و پاسخ تشریحی
پرسش — فایلی روی دیسک وجود دارد و مجوز خواندنش هم درست است، اما برنامه EMFILE میدهد. چرا ساختن دوبارهٔ فایل راهحل مناسبی نیست؟
پاسخ تشریحی — EMFILE به ناتوانی این فرایند در گرفتن توصیفگر تازه، در محدودهٔ RLIMIT_NOFILE، اشاره میکند. وجود و محتوای فایل علت مستقیم این شکست نیست. شمار و چرخهٔ عمر توصیفگرهای همان فرایند، محدودیت مؤثر و مسیرهایی که close انجام نمیدهند بررسی میشوند. ساخت دوبارهٔ فایل ممکن است شاهد را عوض کند و مشکل را باقی بگذارد.
پرسش — در مثال A B A، چرا خواندن از duplicate حرف B میدهد ولی separate دوباره A میخواند؟
پاسخ تشریحی — duplicate با os.dup به همان open file description اول اشاره میکند؛ مکان خواندن پس از دریافت A جلو رفته است و خواندن دوم B را میگیرد. separate از یک open تازه آمده و مکان خواندن مستقل دارد. یکیبودن مسیر فایل با یکیبودن وضعیت بازِ آن یکسان نیست؛ هر سه توصیفگر هم باید سرانجام بسته شوند.
پرسش — همکار پیشنهاد میکند هنگام EMFILE محدودیت همهٔ سرویسها را نامحدود کنیم. چه شاهدهایی پیش از تغییر کم است؟
پاسخ تشریحی — باید محدودهٔ مشکل، حد نرم و سخت فرایند هدف، تعداد و نوع توصیفگرها و روند افزایش آنها مشخص شود. نشت، همزمانی زیاد یا طراحی اتصال ممکن است دخیل باشند و افزایش سقف، نشت را اصلاح نمیکند. سقف یک فرایند نیز با ظرفیت سراسری یکی نیست. آزمایش این درس فقط حد یک فرزند کوتاهعمر را پایین میآورد و توصیهٔ تغییر همهٔ سرویسها نمیدهد.
پیشنیازهای هدفمند و منابع
برای تفاوت فرایند و مصرف عمومی منابع، درس پردازهها را مرور کنید:
https://www.darsavand.com/courses/linux-server-from-zero?lesson=f17a796d-a6ba-4a67-bfde-3ad4cfa7f111
برای تفاوت فشار CPU و حافظه با مسئلهٔ توصیفگر، به درس فشار منابع برگردید:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=54d67687-5e1b-4ed5-8d0f-f062ec549579
کمبودن RAM یا load بالا، بهتنهایی EMFILE را توضیح نمیدهد. این درس بر شیوهٔ بازکردن و پایان عمر منابع تمرکز داشت.
منابع رسمی برای مطالعهٔ تکمیلی:
Linux man-pages — open(2): https://man7.org/linux/man-pages/man2/open.2.html
Linux man-pages — getrlimit(2): https://man7.org/linux/man-pages/man2/getrlimit.2.html
Python 3.12 — resource: https://docs.python.org/3.12/library/resource.html
Python 3.12 — os: https://docs.python.org/3.12/library/os.html
تألیف، مثالها و پرسشهای اختصاصی: تحریریه درسآوند.
فایل آزمایشگاه، راهنمای اجرا، آزمونها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دورههای مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.