# FC01 — FreeRTOS `heap_4`: od kursora do wolnych bloków ## Decyzja dydaktyczna To jest jedna 30-minutowa lekcja i bezpośrednia kontynuacja projektu K&R 5.4 z karty C07. Karta nie uczy pięciu implementacji heap jako pięciu równorzędnych tematów. `heap_1`, `heap_2`, `heap_3` i `heap_5` zajmują łącznie maksymalnie minutę kontekstu. Cała właściwa praca dotyczy `heap_4`. Task 1 i Task 2 są krótkimi modelami prowadzonymi przez nauczyciela. Uczeń najpierw zapisuje predykcję, ale nie buduje i nie debugguje tych programów osobno podczas lekcji. Task 3, linkujący upstream `heap_4.c`, jest jedynym centralnym przebiegiem ucznia. ## Stan wejściowy z K&R 5.4 Uczeń przychodzi z działającym kodem: ```text allocbuf[64] allocp allocator_reset() alloc_local(5) -> offset 0 alloc_local(7) -> offset 5 allocp -> offset 12 alloc_local(0) oraz OOM -> NULL bez przesunięcia kursora ``` Nie powtarzamy sposobu działania tego kodu. Zaczynamy od jego granicy: > Jak zwolnić obszar 5 B spod offsetu 0, zachować późniejszy obszar 7 B spod > offsetu 5 i ponownie wykorzystać powstałą dziurę? W projekcie nie istnieje operacja zwolnienia pojedynczego obszaru. Nawet proste cofnięcie kursora unieważniłoby wszystkie późniejsze przydziały. Brakuje tożsamości bloku, jego rozmiaru, zbioru dziur i mechanizmu ich łączenia. ## Cel w jednym zdaniu Po 30 minutach uczeń potrafi wyjaśnić i pokazać w debuggerze, jak `heap_4` dodaje do liniowej areny nagłówki, wybór first-fit, podział bloku, listę wolnych bloków oraz scalanie sąsiadów, a następnie odróżnić bieżącą ilość wolnej pamięci, największy wolny blok i minimum-ever. ## Most K&R 5.4 -> `heap_4` | K&R 5.4 — stan projektu | Ograniczenie | `heap_4` — nowy mechanizm | | --- | --- | --- | | `allocbuf[64]` i jeden `allocp` | istnieje tylko wolny ogon areny | `ucHeap[]` i bloki opisane nagłówkami | | wynik to surowy adres | allocator nie zna później rozmiaru obszaru | `BlockLink_t` przed payloadem przechowuje rozmiar | | kolejne przydziały tylko przesuwają kursor | nie można użyć dziury | lista wolnych bloków i wybór first-fit | | brak zwalniania pojedynczego obszaru | reset odzyskuje wszystko naraz | `vPortFree()` odzyskuje wskazany blok | | brak relacji między dziurami | sąsiednie dziury pozostają rozdzielone | porządek adresowy i coalescing w obie strony | | tylko sukces albo `NULL` | brak informacji o fragmentacji | free, largest, liczba bloków i minimum-ever | W realnym `heap_4.c` najwyższy bit `xBlockSize` oznacza blok przydzielony. Uproszczone modele Task 1–2 pokazują geometrię i listę, ale nie odtwarzają tego bitu. To jawna granica modelu. ## Przygotowanie przed lekcją Budowanie, flashowanie i wykrywanie debug probe nie wchodzą do budżetu 30 minut. Przed wejściem uczniów: 1. zbuduj Task 3 dla `pico2_w` / RP2350 RISC-V; 2. wgraj zweryfikowany obraz i otwórz sesję na tym samym ELF; 3. ustaw pierwszy breakpoint na `heap4_fragmented_checkpoint`; 4. przygotuj widok `ucHeap`, `g_fragmented_stats`, `g_final_stats`, `xFreeBytesRemaining` i `xMinimumEverFreeBytesRemaining`; 5. przygotuj Hazard3 jako awaryjny zamiennik — podczas lekcji użyj jednej platformy, nigdy obu. `heap_4` jest implementacją wybraną i linkowaną przez projekt. Nie jest sprzętowym heapem ani peryferium RP2350. ## Przebieg 30 minut | Czas | Tryb | Działanie i dowód | | --- | --- | --- | | 0–3 min | problem | Przypomnij wyłącznie offsety `0 -> 5 -> 12`. Uczeń próbuje zwolnić pierwszy obszar bez utraty drugiego i nazywa brakującą operację. | | 3–4 min | kontekst | Jedno zdanie: FreeRTOS dostarcza wymienne implementacje tego samego API; ta karta i oficjalne przykłady Pico 2 W linkują `heap_4`. | | 4–9 min | Task 1 — demo | Do żądania dodaj nagłówek, wyrównaj rozmiar, wybierz pierwszy pasujący blok i podziel go. Uczeń zapisuje `wanted` i `remainder` przed odsłonięciem wyniku. | | 9–16 min | Task 2 — demo | Dla `A:48 | B:48 | C:48` pokaż listę `A -> C`, wstaw `B` po adresie i sprawdź scalenie w prawo oraz w lewo: `2 bloki / 96 B / largest 48 B -> 1 blok / 144 B / largest 144 B`. | | 16–27 min | Task 3 — RUN | Na przygotowanym Pico 2 W przejdź przez prawdziwy `heap_4.c`: trzy alokacje, free A i C, checkpoint z dwoma blokami, free B, checkpoint z jednym blokiem, OOM=`NULL` i jeden malloc-failed hook. Uczeń wypełnia tabelę relacji. | | 27–30 min | wyjście | Uczeń wymienia trzy dodatki ponad K&R (`header`, `free list`, `coalescing`) i wyjaśnia: suma wolnych bajtów może być większa od największego bloku, a minimum-ever nie rośnie po `free`. | ## Task 1 — model nagłówka i splitu Task 1 nie jest osobnym laboratorium. Na RV32 nagłówek modelu ma 8 B, więc żądanie 13 B daje `wanted=24 B` i `remainder=232 B` z areny 256 B. Na AMD64 odpowiednie wartości to 16 B, 32 B i 224 B. Różnica ABI jest tylko pomocą w zrozumieniu `sizeof` i wyrównania; podczas głównego przebiegu obowiązuje RP2350. Predykcja ucznia: ```text free list: [16 B] -> [80 B] -> [160 B] request + header po wyrównaniu: 24 B first-fit wybiera: __________ po split: allocated ______ B, remainder ______ B ``` Wykonywalny Task 1 dowodzi obecnie geometrii splitu jednego bloku. Samo przeszukanie dwóch kandydatów jest elementem demonstracji i realnego `pvPortMalloc()`, dlatego nie opisujemy Task 1 jako pełnego testu first-fit. ## Task 2 — model listy adresowej i coalescingu ```text pamięć: [ A:48 FREE ][ B:48 USED ][ C:48 FREE ] free list: A --------------------------------> C vPortFree(B) 1. wstaw B pomiędzy A i C według adresu; 2. jeśli end(B) == addr(C), połącz B+C; 3. jeśli end(A) == addr(BC), połącz A+BC; wynik: [ ABC:144 FREE ] free list: A -> END ``` Uczeń przed odsłonięciem rysuje listę i wpisuje `2 -> 1`, `96 -> 144` oraz `48 -> 144` odpowiednio dla liczby bloków, sumy i largest. ## Task 3 — centralny przebieg upstream `heap_4.c` ```text RESET/INIT -> ALLOC A(24) -> ALLOC B(40) -> ALLOC C(16) -> FREE A -> FREE C -> heap4_fragmented_checkpoint # 2 free blocks -> FREE B -> OOM request -> task03_debug_checkpoint # 1 free block, pass=1 ``` Uczeń zapisuje tylko relacje, ponieważ dokładne bajty zależą od ABI i konfiguracji: | Obserwacja | Oczekiwany związek | | --- | --- | | po trzech alokacjach | `after_allocations < initial_free` | | stan pofragmentowany | `free_blocks == 2`, `largest < total_free` | | po `free(B)` | `final_free == initial_free`, `free_blocks == 1` | | low-water mark | `minimum_ever <= after_allocations` i nie rośnie po free | | OOM | wynik `NULL`, hook `1`, brak uszkodzenia listy | Minimalny dowód w GDB: ```gdb p g_fragmented_stats p g_final_stats p xFreeBytesRemaining p xMinimumEverFreeBytesRemaining p xStart p pxEnd x/96bx ucHeap ``` ## Kryterium zaliczenia Uczeń zalicza lekcję, jeżeli: - wskaże, dlaczego jeden kursor nie wystarcza do zwolnienia obszaru ze środka; - rozróżni nagłówek allocatora od payloadu i stosu `sp`; - przewidzi przejście wolnej listy `2 -> 1` po zwolnieniu B; - wyjaśni różnicę `total free` kontra `largest free block`; - wyjaśni, dlaczego minimum-ever nie wraca po `free`; - pokaże `aligned=1 oom=1 hooks=1 asserts=0 pass=1` w Task 3. ## Poza lekcją Jako rozszerzenie lub pracę własną pozostają: pełne czytanie `heap_4.c`, uruchomienie Task 1–2 na AMD64 i Hazard3, samodzielny deploy RP2350 oraz porównanie z `heap_1`, `heap_2`, `heap_3` i `heap_5`.