منابع آموزشی و پژوهشی، برای دانشگاه و مدرسه
پشتیبانیانتشار محتوا
دورهٔ آموزشیرایگان

لینوکس ۳؛ آزمایشگاه خطاهای برنامه، فایل و شبکه

۸ درس در ۴ فصل۹ ساعت و ۵۵ دقیقهدربارهٔ دوره
ردگیری و تحویل رخداددرس ۸ از ۸

پروژهٔ پایانی؛ تشخیص خطا، اصلاح کوچک و گزارش قابل تحویل

۹۰ دقیقه آموزش متنی
در این درس ۸ بخش

اکنون از شش نوع آزمایش کوچک یک روش تشخیص می‌سازیم. هدف پروژه ساخت داشبورد یا نصب مجدد سرور نیست؛ باید بتوانیم پیام خطا را به عملیات مشخص وصل کنیم، فرضیه‌ای قابل ردشدن بنویسیم و اصلاحی کوچک را با شاهد تأیید کنیم. پروژهٔ این دوره واقعی و محلی است: فایل و سوکت موقت می‌سازد، فرزند خود را اجرا می‌کند و منابعش را جمع می‌کند. خروجی‌های آن را با رخداد سرویس تولیدی اشتباه نمی‌گیریم.

پیش از اجرا، دامنهٔ آزمایش را روشن کنید

پوشهٔ project باید lab.py، test_lab.py، README.md و RUNBOOK.md را داشته باشد. Python کتابخانهٔ استاندارد را به کار می‌گیرد و نصب بسته با pip لازم نیست. این نسخه برای Linux و حساب معمولی طراحی شده است؛ اجرای case با root به‌جای اثبات مجوز، با خروج ۷۷ متوقف می‌شود. ابتدا راهنمای پروژه را بخوانید و فرمان help را اجرا کنید؛ مسیر کاری درست از همان شروع اهمیت دارد.

bash
python3 --version
python3 lab.py --help
python3 -m py_compile lab.py test_lab.py
python3 -m unittest -v

py_compile خطاهای نگارشی را بررسی می‌کند، ولی سلامت رفتار را ثابت نمی‌کند. unittest سناریوهای تعریف‌شده را اجرا می‌کند؛ گزارش skipped با آزمون موفق یکسان نیست. نام آزمون، علت skip یا traceback را بخوانید. اگر پوشهٔ فعلی اشتباه باشد، نبود فایل یا ماژول، شکست محیط اجراست و هنوز دربارهٔ رفتار case چیزی نشان نمی‌دهد. موفقیت دستور نصب یا بازشدن یک ویرایشگر نیز جای اجرای آزمون نیست.

گزارش آزمون همراه بسته، دامنهٔ بررسی را صریح ثبت می‌کند: ۱۸ آزمون با موفقیت پایان یافته‌اند؛ دو آزمونِ گردش کامل CLI با حساب غیرریشه، اجرا‌نشده‌اند. شش هستهٔ سناریو زیر UID صفرِ بدون قابلیت مؤثر اجرا شده‌اند؛ این را حساب معمولی معرفی نمی‌کنیم. محافظ اجرای root هم واقعاً خروج ۷۷ داده است. ردگیری اختیاری strace نیز در همان محیط با EPERM رد شده؛ دستور و مستندات به معنی ادعای اجرای موفق آن نیست.

یک پروندهٔ شواهد کوچک و تازه بسازید

همهٔ caseها را نخست یک بار در حالت عادی اجرا کنید و خروجی JSON را نگه دارید. پوشهٔ شواهد را با mktemp و دسترسی محدود می‌سازیم تا فایل قبلی بازنویسی نشود. نام مسیر چاپ‌شده را ثبت کنید. این پوشه مستقل از پوشه‌های موقتِ داخلی caseهاست؛ آزمایشگاه منابع موقت خودش را پیش از اعلام موفقیت جمع می‌کند، ولی گزارش شما برای مطالعه باقی می‌ماند.

bash
umask 077
evidence_dir=$(mktemp -d) || exit 1
if python3 lab.py all > "$evidence_dir/results.json"; then
  lab_status=0
else
  lab_status=$?
fi
printf 'lab_status=%s\n' "$lab_status"
printf 'Evidence: %s\n' "$evidence_dir/results.json"
python3 -m json.tool "$evidence_dir/results.json"

خروج صفر، ok=true و شش نتیجهٔ داخلی موفق، معیار اجرای کامل همین مجموعه‌اند. نام caseها باید با شش انتخاب emfile، path، refused، inuse، timeout و signal سازگار باشد. مقادیر پورت، شمار فایل‌های بازشده و زمان سپری‌شده را از همان اجرا بخوانید. خروجی چاپ‌شده در یک مقاله یا دستگاه دیگر را جای مشاهدهٔ دستگاه خود ننویسید؛ به‌خصوص زمان‌های کوتاه به زمان‌بندی وابسته‌اند.

این مجموعه شش مسئلهٔ متفاوت را محدود می‌کند: EMFILE در فرزند با سقف پایین، خطاهای مسیر در پوشهٔ تازه، اتصال به آدرس رزروشدهٔ بدون listen، bind دوبارهٔ همان آدرس، انتظار خواندنِ دارای بودجه و پایان دو فرزند با رفتار متفاوت SIGTERM. خطای عمدیِ کنترل‌شده، درون case انتظار می‌رود و می‌تواند به موفقیت آموزشی منجر شود؛ شکست غیرمنتظرهٔ خود ابزار باید جدا گزارش شود.

یک تابع کامل از پروژه را بخوانید

تابع زیر عین case_timeout در lab.py بسته است. این قطعه به importها و تابع کمکی require در همان ماژول وابسته است؛ آن را به‌عنوان فایل مستقلِ بدون زمینه اجرا نکنید. اجرای کامل با python3 lab.py timeout انجام می‌شود. در مطالعه، ساخت سرور محلی، انتخاب مهلت، بستن سوکت‌ها و جمع‌کردن رشتهٔ سرور را دنبال کنید؛ فقط خطِ استثنا کل طراحی نیست.

python
def case_timeout():
    stop = threading.Event()
    errors = []
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(("127.0.0.1", 0))
    listener.listen(1)
    listener.settimeout(1)

    def serve():
        try:
            with listener:
                connection, _ = listener.accept()
                with connection:
                    connection.settimeout(0.5)
                    for _ in range(3):
                        connection.sendall(b"x")
                        if stop.wait(0.06):
                            return
                    stop.wait(1)
        except OSError as exc:
            if not stop.is_set():
                errors.append(repr(exc))

    worker = threading.Thread(target=serve, daemon=True)
    worker.start()
    start = time.monotonic()
    deadline = start + 0.20
    received = bytearray()
    connected = False
    expired = False
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
            client.settimeout(max(0.001, deadline - time.monotonic()))
            client.connect(listener.getsockname())
            connected = True
            while len(received) < 4:
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    expired = True
                    break
                client.settimeout(remaining)
                try:
                    data = client.recv(4 - len(received))
                except TimeoutError:
                    expired = True
                    break
                require(bool(data), "unexpected EOF before the controlled deadline")
                received.extend(data)
        elapsed = (time.monotonic() - start) * 1000
    finally:
        stop.set()
        listener.close()
        worker.join(2)
    require(not worker.is_alive(), "server thread did not stop")
    require(not errors, "local server failed: " + "; ".join(errors))
    require(connected and expired, "expected a read deadline after connection")
    return {"case": "timeout", "ok": True, "stage": "read", "connected": connected,
            "deadline_ms": 200, "elapsed_ms": round(elapsed, 3),
            "received_bytes": len(received), "server_joined": True}

بودجهٔ مشتری و محدودبودن عمر سرور، دو مسئولیت جدا هستند. تمام‌شدن انتظار مشتری نباید رشته یا فرزند بی‌انتها باقی بگذارد. همچنین یک JSON با ok=true باید پس از بررسی پاک‌سازی تولید شود، نه پیش از آن. وقتی الگو را به برنامهٔ خود می‌برید، این دو پرسش را نگه دارید: چه کسی مالک منبع است و در هر مسیر موفق یا ناموفق، چه کسی آن را جمع می‌کند؟

از errno به فرضیه برسید، نه به نسخهٔ آماده

اگر EMFILE دیدید، نخست هویت فرایند و الگوی باز و بسته‌شدن منابع را بررسی می‌کنید؛ خودِ نام خطا اجازه نمی‌دهد سقف تمام سیستم را بالا ببرید. اگر EACCES دیدید، هویت و دسترسی مسیر را می‌سنجید؛ chmod777 پاسخ عمومی نیست. اگر ECONNREFUSED دیدید، باید معلوم شود کدام آدرس و پورت و کدام مرحله مطرح‌اند. یک نام خطا دسته‌ای از مشاهده را محدود می‌کند، نه علت کامل را.

برای هر مسئله، فرضیه را به شکلی بنویسید که ردشدنش ممکن باشد. مثلاً «مسیر نسبی زیر cwd سرویس به فایل دیگری اشاره می‌کند» با اجرای کنترل‌شده از cwd مشخص آزموده می‌شود. «لینوکس مشکل دارد» آزمون روشن ندارد. پیش از اصلاح، شاهد اولیه را نگه دارید و فقط متغیر مرتبط را تغییر دهید؛ پس از اصلاح نیز همان آزمون قبلی و یک بررسی نبود پیامد تازه را انجام دهید.

قالب گزارش قابل تحویل

plaintext
دامنه: کدام case یا برنامهٔ مشخص؟
مشاهده: فرمان، خروجی، زمان و محیط
فرضیه: دلیل احتمالی با شاهد موافق
شاهد ردکننده: چه نتیجه‌ای فرضیه را رد می‌کند؟
آزمون کوچک: یک تغییر یا مشاهدهٔ محدود
اصلاح: دقیقاً کدام رفتار عوض شد؟
تأیید: آزمون دوباره و وضعیت منابع
محدودیت: چه چیزی هنوز آزمایش نشده است؟

گزارش برای خواننده‌ای نوشته می‌شود که کنار ترمینال شما نبوده است. نام فایل یا خط مرتبط را بنویسید و توده‌ای از خروجی نامرتبط نفرستید. در عین کوتاهی، دادهٔ لازم را حذف نکنید: case، هویت عادی کاربر، نسخهٔ Python و مرحلهٔ شکست برای بازتولید مهم‌اند. اطلاعات محرمانهٔ پروژهٔ واقعی را وارد گزارش عمومی نکنید؛ داده‌های این تمرین از ابتدا ساختگی و محلی انتخاب شده‌اند.

پایان رخداد زمانی است که شرط موفقیتِ همان مسئله تأیید شده باشد، نه وقتی آخرین فرمان خروج صفر داده است. ممکن است اتصال اکنون برقرار شود ولی خواندن همچنان معطل بماند؛ آن وقت یک مرحله اصلاح شده و بررسی باید با شواهد تازه ادامه یابد. پاک‌سازی و دریافت وضعیت فرزندان هم بخشی از تحویل است. یک آزمایش که خودش پردازهٔ رهاشده بر جا می‌گذارد، الگوی مناسبی برای نگهداری نیست.

ادامهٔ مسیر قبلی، با مرز روشن

در این درس، قرارداد اجرای واقعی برنامه را روی نمونه‌های کوچک بررسی کردیم. گزارش بازیابیِ پیکربندی در دورهٔ قبلی، موضوع دیگری بود و لازم نیست متنش را تکرار کنیم. برای انتقال روش به سرویس، همان ثبت هویت، مسیر کاری، خروجی و آزمون عملکرد لازم است؛ تغییر systemd یا Nginx تنها وقتی مطرح می‌شود که شاهد، همان لایه را نشان بدهد.

برای گزارش و بازیابیِ قبلی:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=5ba98f16-5eab-424e-8241-4281a60bd1fc
برای اعمال نسخهٔ تازه و آزمون بازگشتِ بک‌اند:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=6c928ef0-cec9-477f-b207-63b6d33d2d0c

مدخل‌های #linux-errno، #linux-working-directory، #linux-signal و #linux-timeout راهنمای مرورند. معیار پایان پروژه این است که شخص دیگری از روی گزارش شما بتواند آزمایش مجاز را تکرار کند، نتیجهٔ مورد انتظار را بشناسد و بداند کدام بخش‌ها هنوز دربارهٔ سرویس واقعی نامعلوم‌اند. اجرای مجموعهٔ محلی، مجوز تعمیم بی‌بررسی به شبکهٔ عمومی یا محیط تولید نیست.

تمرین و پاسخ تشریحی

پرسش 1: همکار می‌گوید «همهٔ caseها ok=true شدند، پس بک‌اند اصلی هم سالم است». گزارش شما چگونه این ادعا را اصلاح می‌کند؟

پاسخ تشریحی: ok=true یعنی آزمایشگاهِ محدود توانسته همان رفتار مورد انتظار و پاک‌سازی را مشاهده کند. caseها روی فایل موقت، فرزند خودمان و لوپ‌بک‌اند؛ دامنهٔ عمومی، Nginx، پایگاه داده و سرویس اصلی آزمایش نشده‌اند. در گزارش، نتیجهٔ آموزشی را ثبت می‌کنیم و برای سامانهٔ واقعی آزمون مستقلِ عملکرد، مسیر و شرایط اجرا می‌خواهیم. سلامت را از محیطی به محیط دیگر منتقل نمی‌کنیم.

پرسش 2: برنامه روی دسکتاپ فایل را پیدا می‌کند و در سرویس ENOENT می‌دهد. آیا نخست باید مجوز همهٔ فایل‌ها را بازتر کنیم؟

پاسخ تشریحی: خیر. ENOENT به نبودن جزء مسیر اشاره می‌کند و مسیر نسبی با cwd متفاوت می‌تواند علت باشد. نخست مسیر واقعی، WorkingDirectory و نسخهٔ انتشارِ مورد استفاده را بررسی می‌کنیم. یک آزمون با cwd صریح و همان هویت، فرضیه را روشن‌تر می‌کند. اگر بعد از اصلاح مسیر خطای مجوز پدید آمد، آن را جدا با شاهد مربوط بررسی می‌کنیم؛ یک اصلاح کلیِ بدون علت، دو مسئله را پنهان می‌کند.

پرسش 3: پس از اصلاح اتصال، پیام ECONNREFUSED از بین رفته اما خواندن به پایان نمی‌رسد. آیا همان خطا برگشته است؟

پاسخ تشریحی: شاهد تازه به مرحلهٔ دیگری مربوط است. رفع ردشدن اتصال ممکن است ما را به مرحلهٔ انتظار داده رسانده باشد؛ اکنون باید شرط کامل‌شدن پاسخ و بودجهٔ خواندن را بررسی کنیم. در خط زمانی، هر دو مشاهده را جدا ثبت می‌کنیم و فرضیهٔ قبلی را به‌زور نگه نمی‌داریم. اصلاح معتبر، مرحلهٔ مورد نظر را با آزمون متناسب تأیید می‌کند؛ محوشدن یک پیام به‌تنهایی پایان رخداد نیست.

منابع رسمی برای بررسی و مطالعهٔ بیشتر

Python 3.12 — unittest: https://docs.python.org/3.12/library/unittest.html
Python 3.12 — errno: https://docs.python.org/3.12/library/errno.html
Python 3.12 — subprocess: https://docs.python.org/3.12/library/subprocess.html
Linux man-pages — strace(1): https://man7.org/linux/man-pages/man1/strace.1.html

متن، سناریوها و پرسش‌های این درس تألیف مستقل تحریریه درس‌آوند است. منابع برای بررسی رفتار ابزار و ادامهٔ مطالعه معرفی شده‌اند؛ این آموزش ترجمهٔ یک منبع واحد نیست.

فایل آزمایشگاه، راهنمای اجرا، آزمون‌ها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دوره‌های مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثال‌های اختصاصی: تحریریه درس‌آوند.

ورود برای ثبت پیشرفت

متن درس‌های عمومی را بدون ورود بخوانید.

یادگیری‌ات را دنبال کن

برای نگهداری پیشرفت و ادامهٔ یادگیری از حساب خود استفاده کن.

ورود و ثبت‌نام
دربارهٔ این دوره

دورهٔ رایگان لینوکس ۳ در ۸ درس؛ آزمایش عملی خطاهای فایل، پورت، اتصال، Timeout و سیگنال، با Python، strace، تمرین و پاسخ. ادامهٔ لینوکس پیشرفته و استقرار بک‌اند؛ تألیف تحریریه درس‌آوند.

پرسشی دربارهٔ این درس داری؟گفت‌وگو با پشتیبانی در حساب کاربری
یکجا انتخاب کن، راحت‌تر دریافت کن

انتخاب‌های من ۰ منبع

مجموعهٔ مطالعه‌ات را بساز.

کنار مقاله‌ها «انتخاب» را بزنید تا چند فایل را با هم دریافت کنید.

انتخاب مقاله‌های رایگان