2.2 Folyamat, memória és hibakimenet

2.2.1 Cél és konkrét helyzet

A diagnosztika tárgya a Northstar data/raw/orders_raw.csv fájl: 2405 rendeléssorból áll, és az 2.1 szerint elkülönített nyers mappa tudatosan hibás példányát adja. Egy sor tizenöt mezőt tartalmaz; a szerződés szerint öt extra duplikált sor, két hiányzó distance_km és egy negatív basket_minor van benne. A megfigyelési egység egy CSV-sor, a folyamat futása szempontjából pedig egyetlen Python-processz.

A tanuló dolga, hogy egy kis parancssori ellenőrzőt futtasson, a kimenetet szétválassza, rögzítse a visszatérési kódot, majd memóriabecslést készítsen. Előzetes becslés: ha egy sor átlagosan 350 byte, akkor a teljes szöveges fájl

2405⋅350B≈842kB≈0,80MiB.2405 \cdot 350\ \mathrm{B} \approx 842\ \mathrm{kB} \approx 0{,}80\ \mathrm{MiB}.

Egy listában tárolt verzió a Python string- és mutatóoverheadje miatt 1–1,5 MiB körül lehet. Ellenőrzésre a futás végén a hibák számának és az exit code-nak kell egyeznie: 8 észlelt sorhiba, 4-es saját hibakód. A pontos byte-méret gépfüggő, ezért csak nagyságrend és invariáns a cél.

2.2.2 A három alapfogalom: stdout, stderr és exit code

A futó program két alapértelmezett kimeneti csatornát kap: a standard kimenetet (stdout, standard output) és a standard hibakimenetet (stderr, standard error). A Python print() alapesetben a stdout-ra ír, a sys.stderr-re irányított írás pedig a stderr-re. A szétválasztás nem kozmetika: a tudományos futásnaplóban az adatszerű eredményt és a diagnosztikai zajt külön akarjuk tárolni. A shellben a > a stdout-ot irányítja fájlba, a 2> a stderr-t. PowerShellben ugyanez a két operátor működik az újabb verziókban, de az exit code ott a $LASTEXITCODE változóban jelenik meg, nem a $? boolean értékben.

Minden folyamat egy számmal tér vissza: ez az exit code, más néven visszatérési kód (exit status). A 0 hagyományosan sikert jelent, a nullától eltérő hibaosztályt. A konkrét szám a program saját szerződése. Bashban a $? az utolsó előtérben futott parancs kódját adja; ezért az értéket azonnal el kell menteni, mielőtt bármely másik parancs felülírná. Kézi kontroll Linux/macOS Bashban vagy WSL-ben, a projektgyökérből:

python -c 'import sys; print("adat"); print("hiba", file=sys.stderr)' \
  >reports/s006/out.txt 2>reports/s006/err.txt
status=$?
printf 'exit_code=%s\n' "$status"

Itt az out.txt egyetlen adat sort tartalmaz, az err.txt a hiba sort, az exit_code=0 pedig a sikerjelzést. A Python-kód semmilyen hibát nem váltott ki; a stderr-re írás önmagában nem rontja el a folyamat státuszát.

2.2.3 Python-kivétel, OOM és külső hibakód

Három különböző jelenséget könnyű összekeverni, mert mind „hibának” látszik:

Jelenség Ki állítja le Látható jel Kilépőkód
Python-kivétel a Python interpreter saját kivételkezelője Traceback ... a stderr-en általában 1
OOM (out of memory) az operációs rendszer memóriakezelője gyakran nincs traceback, Linuxon Killed Linuxon gyakran 137
Külső folyamat hibakódja a folyamat saját return/exit utasítása a shell $? változója programspecifikus

A kezeletlen Python-kivétel (exception) nyelvi hibacsatorna: a 1/0 kifejezés tracebacket ír, a processz kontrolláltan leáll, és a shell $? értéke 1 lesz. A MemoryError is kivétel: a Python megpróbált egy számára túl nagy objektumot lefoglalni, és erről a saját nyelvi mechanizmusán keresztül számolt be. Ez még nem jelenti, hogy az egész gép memóriája elfogyott.

Az OOM ezzel szemben operációs rendszer szintű esemény: ha a processz a rendelkezésre álló RAM-nál többet kér, a kernel megölheti anélkül, hogy a Python tracebacket írna. Linuxon a 137-es kilépőkód a 128 + 9 összegből adódik, ahol a 9 a SIGKILL jel sorszáma; ez gyakori OOM-gyanú, de önmagában nem elégséges bizonyíték. macOS-en más memórianyomás-kezelés működik, Windowson pedig a jelenség más üzenettel jelentkezhet.

A külső folyamat hibakódja sem Python-kivétel. Ha egy adateszköz, például egy parancssori program 4-gyel tér vissza, az az adott program szerződése szerinti hiba. Pythonból ezt így lehet lekérdezni, szemléltető jelleggel:

import subprocess
result = subprocess.run(["python", "-c", "import sys; sys.exit(7)"])
print(result.returncode)  # elméletileg várt: 7

A subprocess.run alapból nem dob Python-kivételt nemnulla visszatéréskor; a check=True kapcsolóval kérhetnénk kivételt. A kettő szétválasztása azért fontos, mert a hibanaplóban más javító lépés következik: kivételnél a kódot, OOM-nál a memóriaterhelést, külső hibakódnál az adott program dokumentációját kell vizsgálni.

2.2.4 Process, thread, RAM és megszakítás

A processz (folyamat) saját memóriaterületet és erőforrásokat kap; a thread (szál) ugyanazon processz memóriáján osztozó, könnyebb ütemezési egység. Egy Jupyter-kernel egy hosszú életű Python-processz; ha két notebook fut, legalább két külön kernelprocessz dolgozik. Linuxon a ps -o pid,ppid,rss,etime,comm, macOS-en a ps -axo ..., Windowson a Get-Process python segít azonosítani, melyik folyamat mennyi memóriát foglal. A ps eszköz rss mezőjének szokásos mértékegysége KiB (1024 bájtos egység), nem bájt; a pontos formátumot az adott platform dokumentációja szerint kell értelmezni. Az RSS a rezidens memórialapok nagyságát közelíti, és megosztott lapokat is tartalmazhat: nem azonos a folyamat kizárólagos fizikai RAM-terhelésével, és nem a lemezfoglalást méri.

A RAM és a lemez közötti különbség a hibadiagnózis alapja. A fájl mérete path.stat().st_size értékkel kérdezhető le; ez a lemezen elfoglalt tartós méret. A processz RAM-használata futás közben ennél lehet kisebb, ha streamelve dolgozunk, vagy nagyobb, ha a sorokat Python-listában, string-objectumokként tároljuk. A durva becslés:

Blista≈Bfájl+Nsor⋅Csor,B_{\text{lista}} \approx B_{\text{fájl}} + N_{\text{sor}} \cdot C_{\text{sor}},

ahol $B_{\text{fájl}}$ a CSV byte-mérete, $N_{\text{sor}}$ a sorok száma, $C_{\text{sor}}$ pedig egy sor listaelemének becsült overheadje. 64 bites CPython 3.12 környékén $C_{\text{sor}}$ nagyságrendileg 40–60 byte, mert egy listaelem 8 byte-os mutatót és egy legalább 49 byte-os string-objektumot jelent; ez becslés, nem mérés. A bináris átváltás:

1MiB=220B=1048576B.1\ \mathrm{MiB} = 2^{20}\ \mathrm{B} = 1\,048\,576\ \mathrm{B}.

A megszakítás (interrupt) a harmadik hibacsatorna. Az előtérben futó folyamatot Ctrl+C szakítja meg; Linux/macOS-en ez SIGINT jelet küld, a Python pedig KeyboardInterrupt kivétellé alakítja. A SIGKILL viszont nem kivétel: a processz azonnal megszűnik, finally blokk sem fut. A Jupyterben a kernel Interrupt gombja hasonló szerepet tölt be, de a kernel folyamatának tényleges állapotát hosszú feladat után külön ellenőrizni kell. A háttérben indított folyamat PID-jét a shell $! változója adja; Linuxon a kill PID, Windowson a Stop-Process -Id PID állítja le.

2.2.5 Teljes minimális munkapélda

Munkakönyvtár a projektgyökér; platform Linux/macOS Bash vagy WSL. Első lépésként mentsük el az alábbi ellenőrzőt scripts/s006_diagnose.py néven:

from __future__ import annotations

import csv
import sys
from pathlib import Path

EXPECTED_HEADER = [
    "order_id", "customer_id", "order_ts", "promised_days", "quantity_sum",
    "item_rows", "basket_minor", "shipping_fee_minor", "currency", "distance_km",
    "priority_flag", "delivery_days", "actual_delivery_at", "label_available_at",
    "target_late",
]

MIB = 1024 * 1024


def main(argv: list[str]) -> int:
    if len(argv) != 2:
        print(f"Használat: {Path(sys.argv[0]).name} BEMENET.csv", file=sys.stderr)
        return 2

    path = Path(argv[1])
    if not path.is_file():
        print(f"Nincs ilyen fájl: {path}", file=sys.stderr)
        return 1

    total_bytes = path.stat().st_size
    rows = 0
    issues: list[str] = []
    seen: set[str] = set()

    with path.open("r", encoding="utf-8", newline="") as handle:
        reader = csv.reader(handle)
        header = next(reader, None)
        if header != EXPECTED_HEADER:
            print(f"Fejléc eltér: {header}", file=sys.stderr)
            return 3

        for row in reader:
            rows += 1
            if len(row) != len(EXPECTED_HEADER):
                issues.append(f"sor {reader.line_num}: rossz mezőszám: {len(row)}")
                continue

            order_id = row[0]
            if order_id in seen:
                issues.append(f"sor {reader.line_num}: duplikált order_id {order_id}")
            seen.add(order_id)

            if row[9] == "":
                issues.append(f"sor {reader.line_num}: hiányzó distance_km")

            try:
                if int(row[6]) < 0:
                    issues.append(f"sor {reader.line_num}: negatív basket_minor {row[6]}")
            except ValueError:
                issues.append(f"sor {reader.line_num}: nem numerikus basket_minor {row[6]}")

    print(f"fajl={path}")
    print(f"rows_read={rows}")
    print(f"issue_count={len(issues)}")
    print(f"total_bytes={total_bytes}")
    print(f"average_row_bytes={total_bytes / rows:.1f}")
    estimated_full_load = total_bytes + rows * 60
    print(f"estimated_full_load_mib={estimated_full_load / MIB:.2f}")

    for issue in issues:
        print(issue, file=sys.stderr)

    return 4 if issues else 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))

A script a hibás bemeneten nemcsak leáll, hanem a hibasorokat a stderr-re írja, az összegzést a stdout-ra, a nullától eltérő kóddal pedig a shellben automatizálhatóan jelez. A következő futtatás a reports/s006 mappába gyűjti a futásnaplót:

mkdir -p reports/s006

python scripts/s006_diagnose.py data/raw/orders_raw.csv \
  >reports/s006/bad.out 2>reports/s006/bad.err
status=$?
printf 'exit_code=%s\n' "$status" >reports/s006/bad.log

cat reports/s006/bad.log
cat reports/s006/bad.out
cat reports/s006/bad.err

Szemléltető várható kimenet — a byte-számok a saját gépen futó generate_data.py eredményétől függenek:

exit_code=4

fajl=data/raw/orders_raw.csv
rows_read=2405
issue_count=8
total_bytes=882419
average_row_bytes=366.8
estimated_full_load_mib=0.98

sor 118: duplikált order_id ORD-...
sor 487: hiányzó distance_km
sor 902: hiányzó distance_km
sor 1203: negatív basket_minor -1350
sor 1501: duplikált order_id ORD-...
sor 1704: duplikált order_id ORD-...
sor 1988: duplikált order_id ORD-...
sor 2210: duplikált order_id ORD-...

A tiszta data/raw/orders.csv kontrollként futtatva:

python scripts/s006_diagnose.py data/raw/orders.csv \
  >reports/s006/good.out 2>reports/s006/good.err
status=$?
printf 'exit_code=%s\n' "$status"

Várható: exit_code=0, rows_read=2400, issue_count=0, üres good.err. A PowerShell-munkamenetben ugyanez a szerkezet 2> átirányítással és $LASTEXITCODE rögzítésével zajlik.

2.2.6 Eredmény értelmezése

A kézi kontroll a nyers fájl szerződéséből következik: 2405 sor = 2400 alapsor + 5 duplikált sor; a hiányzó distance_km két sora és a negatív basket_minor egy sora külön sorokon van, így összesen 8 észlelt sorhiba. A rows_read=2405 és issue_count=8 páros egzakt invariáns; ha bármelyik eltér, a fájl vagy a script változott. Az exit_code=4 a script saját szerződése: 0 tiszta, 1 hiányzó fájl, 2 rossz argumentum, 3 fejléc-eltérés, 4 sorhibák.

A memóriasor becslés, nem mérés. Az estimated_full_load_mib azt mondja meg, mennyit foglalna a sorok szövegének és a hozzá tartozó Python stringeknek a listában tartása; a futó script streamelve olvas, ezért valós RSS-használata ennél kisebb. Pontos processzmemóriát mérni csak saját gépen, megadott Python-verzióval, ps-sel vagy platformes eszközzel lehet; az itt közölt kimenet szemléltető.

Folyamat és mérés. A stdout a strukturált eredmény vagy emberi kimenet csatornája, a stderr a diagnosztikai üzeneteké; egyes eszközök eltérő konvenciót követnek, ezért a naplózási szerződés fontosabb, mint a puszta csatorna. A kilépési kódot a vizsgált parancs után közvetlenül rögzítsük, különben a következő cat vagy echo már felülírja a shell $? értékét.

CPU-kihasználtságból önmagában nem következik, hogy a Python-kód lassú vagy gyors: a méréshez a folyamat futásideje, I/O-tevékenysége, memóriája és a rendelkezésre álló magok száma is kell. A nagyobb CSV vizsgálatakor az adatméret és az olvasási mód legyen a mérési napló része.

2.2.7 Feladatok

  1. Értelmezés. A hibás futtatás után a bad.log tartalma exit_code=4. Melyik állítás igaz?

    1. A Python 4 kivételt dobott, ezért a tracebacket kell keresni a bad.err-ben.
    2. A script 8 sorhibát észlelt, ezeket a stderr-re írta, és a saját hibakódja 4.
    3. A 4-es kilépőkód OOM-ot jelent, ezért a folyamatot a kernel ölte meg. Válaszodat a megfigyelhető kimenetekre hivatkozva indokold.
  2. Implementálás. Bővítsd a s006_diagnose.py scriptet egy harmadik, opcionális paranccsal: ha a felhasználó megadja a data/raw/customers.csv fájlt, a script töltse be az ismert customer_id értékek halmazát, és minden olyan rendeléssort jelentsen hibaként, amelynek customer_id-je nem szerepel benne. A customers fájl első oszlopa a customer_id.

  3. Hibadiagnózis. Adott a következő Bash-részlet:

    python scripts/s006_diagnose.py data/raw/orders_raw.csv
    cat reports/s006/bad.out
    echo "status=$?"

    Miért lehet félrevezető az így kiírt status akkor is, ha a Python-futtatás 4-gyel tért vissza? Add meg a javított változatot.

  4. Transzfer megváltozott feltételre. Készíts 130 000 soros szintetikus fájlt data/derived/orders_130k.csv néven úgy, hogy a data/raw/orders.csv fejlécét és egyetlen érvényes adatsorát ismétled. Futtasd rajta a scriptet, és értelmezd a memóriabecslést. Melyik értéknek kell egzaktnak lennie, és melyiknél fogadható el ±20%-os eltérés?

2.2.8 Megoldások és indoklás

  1. A helyes válasz b. A 4-es kód a script alkalmazásszintű hibaszerződése, nem Python-kivétel és nem OOM. A bad.err soralapú diagnózisokat tartalmaz, nem tracebacket; a bad.out issue_count=8 értéke pedig megerősíti, hogy a 4-es a nyolc sorhibához kötött saját konvenció.

  2. A módosítás lényege:

    if len(argv) == 3:
        customer_path = Path(argv[2])
        raw_lines = customer_path.read_text(encoding="utf-8").splitlines()[1:]
        known_customers = {line.split(",")[0].strip() for line in raw_lines}
    else:
        known_customers = None

A sorciklusban, a mezőszám-ellenőrzés után:

            if known_customers is not None and row[1] not in known_customers:
                issues.append(
                    f"sor {reader.line_num}: ismeretlen customer_id {row[1]}"
                )

Futtatás:

python scripts/s006_diagnose.py data/raw/orders_raw.csv data/raw/customers.csv \
  >reports/s006/extended.out 2>reports/s006/extended.err
status=$?
printf 'exit_code=%s\n' "$status"

Az ellenőrző invariáns: rows_read=2405, issue_count=9, exit_code=4. A nyers fájlban egyetlen ismeretlen customer_id van, ezért pontosan eggyel több hiba keletkezik.

  1. A részlet azért félrevezető, mert $? mindig az utoljára végrehajtott parancs kódját adja; az echo előtt a cat reports/s006/bad.out fut le, amely sikeres fájlmegjelenítéskor 0-t ad. Így a kiírt status=0 a cat eredményét mutatja, nem a Python folyamatét. A javított változat a Python-kódot közvetlenül követően menti az értéket:
python scripts/s006_diagnose.py data/raw/orders_raw.csv
status=$?
cat reports/s006/bad.out
echo "status=$status"
  1. A szintetikus fájl elkészítése Bashban:
mkdir -p data/derived

python - <<'PY'
from pathlib import Path

raw = Path("data/raw/orders.csv")
lines = raw.read_text(encoding="utf-8").splitlines()
rows = [lines[0]] + [lines[1]] * 129_999
Path("data/derived/orders_130k.csv").write_text(
    "\n".join(rows) + "\n", encoding="utf-8"
)
PY

wc -l data/derived/orders_130k.csv
python scripts/s006_diagnose.py data/derived/orders_130k.csv \
  >reports/s006/big.out 2>reports/s006/big.err
status=$?
printf 'exit_code=%s\n' "$status"
cat reports/s006/big.out

A wc -l eredményének 130 000-nek, a script rows_read értékének 129 999-nek kell lennie, és issue_count=0, exit_code=0 várható, mert a minta sor érvényes. A becsült teljes tárolás nagyságrendileg a 2405 soros fájl ötvenszerese: ha az eredeti becslés 1 MiB körül volt, itt 50–55 MiB várható. Ez a becslés ±20%-os toleranciával értelmezhető, mert a fájl mérete és a string-overhead közelítése nem platformfüggetlen. Az exit_code=0 és az issue_count=0 viszont egzakt invariáns.

2.2.9 Gyakori tévedés ellenpéldával

Egy futtatás közben a felhasználó azt látja, hogy a Python-processz CPU-kihasználtsága 3%. Ebből gyakran „a CPU lassú, GPU-ra van szükség” következtetést vonnak le. Ez hibás: az alacsony CPU-használat ugyanúgy lehet lemezolvasási várakozás, hálózati késés vagy adatbázis-tranzakciós blokk. A fenti script 3%-os processzornál is végigfuthat, mert az ideje nagy részét a fájlrendszer olvasására várja; egy GPU semmivel sem olvasná gyorsabban a spinning diskről érkező bájtokat. A GPU akkor jön szóba, ha a szűk keresztmetszet numerikus, párhuzamosítható művelet, és ezt mérés bizonyítja. Ez a döntés a későbbi profilozási fejezet témája, nem a pillanatnyi CPU-százalék. A 1.2 adatéletciklus-fegyelme itt is érvényes: a distance_km hiánya a rendelés időpontjában ismert mező adatminőségi hibája, nem későn érkező label, ezért javítása nem sérti a predikciós sorrendet. Az 2.1 projektkonvenciót követtük: a nyers adatot nem módosítottuk, a futásnaplók a reports/s006 származtatott mappába kerültek. Önellenőrzés: igaz-e, hogy a stderr-re írás mindig nemnulla exit code-ot okoz? Nem: a print(file=sys.stderr) önmagában nem állítja a hibakódot. Igaz-e, hogy a 137 minden platformon OOM-ot jelent? Nem: Linuxon SIGKILL-gyanú, más rendszeren más konvenció él.

2.2.10 Rövid összefoglaló

A futó adatfolyamatnak három hibacsatornája van: a stdout az adatszerű kimeneté, a stderr a diagnosztikáé, az exit code pedig a processz automatikusan ellenőrizhető összegzéséé. A Python-kivétel nyelvi traceback, az OOM operációs rendszer szintű megállítás, a külső hibakód a program saját szerződése. A memóriabecslés a fájlméretből és a soronkénti overheadből nagyságrendet ad; a pontos RAM-mérést platformfüggő eszközzel kell elvégezni. A napló rögzítése után a következő lépésben a Git-verziókezelést kapcsoljuk a projekthez, hogy a scripts/s006_diagnose.py, a reports/s006 kimenetek és a nyers adat változásai nyomon követhetők legyenek.