3 hours ago
ecmx — HTTP/S server po nginx-ovim pravilima, na mikrokontroleru
Svaki embedded web server koji sam do sada koristio ima isti problem: rute su C funkcije zakucane u firmver. Hoćeš da dodaš redirect, promeniš timeout, zaštitiš /admin klijentskim sertifikatom? Novi build, novo flešovanje, i moli se da usput nisi slomio nešto drugo. A taj problem je na serverskoj strani rešen pre dvadeset godina — rešenje se zove nginx.conf.
ecmx je HTTP/S server za mikrokontrolere koji čita nginx-ovu konfiguraciju: isti format fajla, isti location blokovi (prefiks, =, ^~, ~, ~*), iste direktive — podskup, ne sve, o tome pošteno niže. I što je važnije od sintakse: pušta nginx-ove sopstvene testove, pa se njegovo ponašanje ne dokazuje mojom rečju nego test suite-om koji svako može da pokrene.
Uz njega, na drugom jezgru istog čipa, radi ecm-forth — real-time Forth interpreter u kome živi aplikaciona logika. Rezultat: ponašanje uređaja se menja izmenom tekst fajla i reload-om, ne flešovanjem.
Meta: NUCLEO-H755ZI-Q — Cortex-M7 na 480 MHz (server) plus Cortex-M4 na 240 MHz (Forth, sa tvrdim rokom od 1 ms).
Zašto ne neki od običnih embedded servera?
Legitimno pitanje — za prosto "serviraj mi index.html sa senzorskim očitavanjima" ecmx je overkill. Razlika se vidi kad uređaj treba da živi godinama kod klijenta:
Ne znam ni za jedan drugi server u ovoj klasi koji se meri nginx-ovim testovima — ako neko zna, neka javi, ozbiljno me zanima.
Kako je građen
Jezgro servera je sans-I/O: transport mu doda bajtove, ono vrati nameru (WANT_READ, WANT_WRITE, CLOSE, IDLE). Zato isto jezgro radi i preko POSIX soketa i na golom metalu bez sloja za kompatibilnost — i zato može da se testira punom brzinom na radnoj stanici.
Dve tvrde odluke, za ceo sistem: nula malloc-a (uključujući TLS i Forth VM) i nula statičkog RAM-a — 0 data i 0 bss u svih 19 modula servera.
Memorija
Svaki broj je marginalna cena iz dva prava linkovanja — sagradi sa modulom, sagradi bez njega, oduzmi. Zbir veličina objektnih fajlova se ne koristi, jer se tako deljene pomoćne funkcije i niske naplate dvaput.
Zauzeće čipa: fleš 317 376 od 1 048 576 B (30,3 %), RAM 482 016 od 524 288 B (91,9 % — server, stekovi, mrežni baferi, 16 TLS veza).
TLS pojede sve. Na 23,5 KB po vezi, 16 istovremenih TLS sesija traži oko 400 KB od 512 KB koliko ovaj čip ima. Pravi budžet nije broj funkcija nego broj veza — podizanje sa 16 na 64 košta 127 KB, više nego svi opcioni moduli zajedno.
Brzina
Četiri izmene, svaka merena posebno na hardveru. Merenja na radnoj stanici su svaki put pogrešno predvidela veličinu dobitka, pa ih ovde i ne navodim.
Korak 2: radna stanica je predvidela 1,58×, hardver dao 1,17×. Korak 3: snprintf zamenjen formaterom celih brojeva od 709 bajtova, proverenim protiv samog snprintf-a na 3,5 miliona slučajeva.
Korak 4 je zanimljiv. Na ruti koja stvarno prelazi na drugo jezgro, propusnost je otišla sa 631 na 3 853 zaht/s — 6,1×, a p99 kašnjenje sa 32 ms na 8,2 ms. Na svim ostalim rutama je oduzeo 3,6 %. Da sam naveo samo ono 6,1× ispalo bi lepše i bilo bi manje korisno.
Usko grlo nikad nije bilo sanduče između jezgara nego milisekundna granulacija tajmera: drugo jezgro odgovori za 51 µs, dvadeset puta brže nego što petlja ume da probudi vezu. Prekid je omogućio da se red tajmera potpuno preskoči. Sama granulacija je i dalje tu.
ecm-forth — logika na drugom jezgru, sa rokom
Server servira; logiku uređaja piše korisnik — u Forth-u. ecm-forth je threaded-code VM po Chuck Moore modelu: reči se jednom kompajliraju u tokene, runtime vidi samo brojeve, imena posle boot-a više ne postoje. Ćelija je mašinska reč (32-bit na STM32, 64-bit na serveru), dispatch je computed-goto, ceo VM je ~10 KB .text na Cortex-M7 (-Os).
Tri odluke koje ga čine upotrebljivim u produkciji, ne samo na demo tabli:
Na H755 Forth radi na Cortex-M4 sa tvrdim rokom od 1 ms po otkucaju. Sa ploče, u trenutku pisanja ovog posta:
Onaj međujezgarni prekid iz koraka 4 gore dodao je tačno 2 µs na najgori slučaj — zato je i smeo da uđe.
I VM je testiran kako treba: 303 provere × 5 konfiguracija (host 64-bit computed-goto, host 64-bit switch, host 32-bit, MCU profil, ASan+UBSan) plus prevod za M0/M4/M7 — na svakoj gradnji. U kompajler su ugrađene tri tehnike iz PicForth-a (tail-call na ";", constant folding, fuzija poređenja i grane), svaka izmerena istim bench harnessom: konstantna petlja 2,7×, flag petlja 1,7×, a repna rekurzija od 100k nivoa pretvorena iz "ruši stek" u "radi".
Naspram nginx-a — 25 imena direktiva prema 459
Referenca je nginx 1.24.0 sa podrazumevanim modulima plus http_ssl: 459 jedinstvenih direktiva u 53 modula. Pošteno sortirano, tri korpe:
Najoštrija ivica, otvoreno: nepoznata direktiva se trenutno ćutke ignoriše. nginx odbija da se digne na njoj. To znači da konfiguracija koja sadrži deny all ili auth_basic ovde uredno prođe i ne sprovodi ništa — bezbednosna zamka, ne neudobnost. To je sledeća stvar koja se popravlja.
Kako se proverava
Tvrdnje o ugrađenom serveru su jeftine. Ovih pet provera je teško lažirati i puštaju se na svakoj gradnji.
1. Nula alokacija, dokazano linkerom. Test binarni fajl se linkuje sa -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc. Svaka alokacija bilo gde — uključujući unutar mbedTLS-a — uveća brojač. Pun mTLS handshake i raskid prijavljuju nulu. Ovo nije tvrdnja iz pregleda koda; build pada ako ikad prestane da važi.
2. nginx-ovi sopstveni testovi, sa imeniocem koji se ne pumpa. 279 od 313. Dve ograde, obe čine broj gorim: 476 od 495 test fajlova se preskoči jer traže proxy, stream ili HTTP/2 — i konfiguracija koju server prijavljuje namerno ne tvrdi --without-http_rewrite_module, iako rewrite-a nema. Da tvrdi, rezultat bi bio 279/294, tako što bi padove sakrio umesto popravio.
3. Diferencijalno protiv pravog nginx-a. Isti zahtevi se puste na ecmx i na pravi nginx 1.24.0 nad istim fajlom, pa se odgovori porede polje po polje. Odvojeno, regex mašina se poredi sa PCRE2 10.42 na 367 584 parova obrazac/ulaz, uključujući nasumično generisanje obrazaca. Oba trenutno prijavljuju nula neslaganja.
4. Greška koju je ovo našlo prošle nedelje. POSIX klase znakova — [[:digit:]], [[:alnum:]] — kompajlirale su se bez greške i čitale kao običan skup znakova:
Znači location ~ ^/admin/[[:alnum:]]+$ ne bi hvatao ništa. Preživelo je 354 415 nasumičnih poređenja zato što generator obrazaca nikad ne emituje [[:. Uhvatio ga je harness sa fiksnim obrascima. To je argument za to da se puštaju oba, i razlog zašto se "nula neslaganja" uvek mora čitati zajedno sa "nad kojim ulazima". Sad je popravljeno: svih 14 POSIX klasa, negacija, i PCRE-ova tačna pravila za neispravne slučajeve.
5. Ploča opisuje samu sebe. Sve gore je i "datasheet" stranica koju servira sam H755 — sa živim brojačima (uptime, opsluženi zahtevi, najgori slučaj RT jezgra, propušteni rokovi) koje stranica vuče sa /api/sve svake tri sekunde. Server koji se opisuje je server koji ti odgovara.
Otvoreno
Sitno
Merenja: NUCLEO-H755ZI-Q, GCC -Os / -O2, mbedTLS bez hipa. Propusnost preko wrk -t2 -c8 -d20s na 100 Mbit Ethernetu, isti parametri u svakom prolazu. Referenca: nginx 1.24.0, PCRE2 10.42. Forth: 303 provere × 5 konfiguracija, bench harness identičan pre i posle optimizacija.
Pitanja i rupe u rezonovanju su dobrodošli — pola gornjeg spiska "otvoreno" je nastalo tako što je neko pitao "a jesi proverio...".
Svaki embedded web server koji sam do sada koristio ima isti problem: rute su C funkcije zakucane u firmver. Hoćeš da dodaš redirect, promeniš timeout, zaštitiš /admin klijentskim sertifikatom? Novi build, novo flešovanje, i moli se da usput nisi slomio nešto drugo. A taj problem je na serverskoj strani rešen pre dvadeset godina — rešenje se zove nginx.conf.
ecmx je HTTP/S server za mikrokontrolere koji čita nginx-ovu konfiguraciju: isti format fajla, isti location blokovi (prefiks, =, ^~, ~, ~*), iste direktive — podskup, ne sve, o tome pošteno niže. I što je važnije od sintakse: pušta nginx-ove sopstvene testove, pa se njegovo ponašanje ne dokazuje mojom rečju nego test suite-om koji svako može da pokrene.
Uz njega, na drugom jezgru istog čipa, radi ecm-forth — real-time Forth interpreter u kome živi aplikaciona logika. Rezultat: ponašanje uređaja se menja izmenom tekst fajla i reload-om, ne flešovanjem.
Meta: NUCLEO-H755ZI-Q — Cortex-M7 na 480 MHz (server) plus Cortex-M4 na 240 MHz (Forth, sa tvrdim rokom od 1 ms).
Zašto ne neki od običnih embedded servera?
Legitimno pitanje — za prosto "serviraj mi index.html sa senzorskim očitavanjima" ecmx je overkill. Razlika se vidi kad uređaj treba da živi godinama kod klijenta:
- Konfiguracija umesto rekompajliranja. Rute, TLS, keepalive, pristup — tekst fajl, ne #ifdef šuma. Ko je ikad pisao nginx.conf, već zna da podesi ovaj server.
- Ponašanje koje je neko već dokumentovao. Ne izmišljam semantiku pa je opisujem u README-u — preuzimam nginx-ovu, koja je opisana, poznata adminima i pokrivena tuđim testovima. Kad se ecmx i nginx ne slažu, to je po definiciji bug u ecmx-u.
- Deterministička memorija. Nula malloc-a — nigde, uključujući TLS. Nema fragmentacije hipa, nema "radio je tri nedelje pa stao". Sve što će ikad biti zauzeto zna se u trenutku linkovanja.
- Logika bez flešovanja. Forth skripta se kompajlira jednom pri boot-u, pa se VM zapečati. Izmena logike = nova skripta + reload. Firmver se ne dira.
Ne znam ni za jedan drugi server u ovoj klasi koji se meri nginx-ovim testovima — ako neko zna, neka javi, ozbiljno me zanima.
Kako je građen
Jezgro servera je sans-I/O: transport mu doda bajtove, ono vrati nameru (WANT_READ, WANT_WRITE, CLOSE, IDLE). Zato isto jezgro radi i preko POSIX soketa i na golom metalu bez sloja za kompatibilnost — i zato može da se testira punom brzinom na radnoj stanici.
Dve tvrde odluke, za ceo sistem: nula malloc-a (uključujući TLS i Forth VM) i nula statičkog RAM-a — 0 data i 0 bss u svih 19 modula servera.
Memorija
Svaki broj je marginalna cena iz dva prava linkovanja — sagradi sa modulom, sagradi bez njega, oduzmi. Zbir veličina objektnih fajlova se ne koristi, jer se tako deljene pomoćne funkcije i niske naplate dvaput.
Code:
komponenta fleš (B) RAM fiksni RAM po vezi
--------------------------------------------------------------------
jezgro (petlja, parser, 16 196 664 2 651
odgovor, baferi, tajmeri)
TLS (mbedTLS, bez hipa) 101 744 20 792 23 552
regex mašina 6 052 0 0
telnet konzola 5 384 0 0
promenljive 3 684 0 0
ciklus konfiguracije 2 620 0 0
datum / vreme 2 336 0 0
merni brojači 1 892 480 0
parser konfiguracije 1 724 0 0
statički fajlovi 1 116 0 0
auth kuka 316 0 0
--------------------------------------------------------------------
sve osim TLS-a 41 320 1 144 2 651Zauzeće čipa: fleš 317 376 od 1 048 576 B (30,3 %), RAM 482 016 od 524 288 B (91,9 % — server, stekovi, mrežni baferi, 16 TLS veza).
TLS pojede sve. Na 23,5 KB po vezi, 16 istovremenih TLS sesija traži oko 400 KB od 512 KB koliko ovaj čip ima. Pravi budžet nije broj funkcija nego broj veza — podizanje sa 16 na 64 košta 127 KB, više nego svi opcioni moduli zajedno.
Brzina
Četiri izmene, svaka merena posebno na hardveru. Merenja na radnoj stanici su svaki put pogrešno predvidela veličinu dobitka, pa ih ovde i ne navodim.
Code:
zaht/s promena
------------------------------------------------------------
1. polazno 3 567
2. -O2 4 155 +16,5 %
3. izbačen snprintf 4 305 +3,6 %
4. međujezgarni prekid (HSEM) 4 149 -3,6 %Korak 2: radna stanica je predvidela 1,58×, hardver dao 1,17×. Korak 3: snprintf zamenjen formaterom celih brojeva od 709 bajtova, proverenim protiv samog snprintf-a na 3,5 miliona slučajeva.
Korak 4 je zanimljiv. Na ruti koja stvarno prelazi na drugo jezgro, propusnost je otišla sa 631 na 3 853 zaht/s — 6,1×, a p99 kašnjenje sa 32 ms na 8,2 ms. Na svim ostalim rutama je oduzeo 3,6 %. Da sam naveo samo ono 6,1× ispalo bi lepše i bilo bi manje korisno.
Usko grlo nikad nije bilo sanduče između jezgara nego milisekundna granulacija tajmera: drugo jezgro odgovori za 51 µs, dvadeset puta brže nego što petlja ume da probudi vezu. Prekid je omogućio da se red tajmera potpuno preskoči. Sama granulacija je i dalje tu.
ecm-forth — logika na drugom jezgru, sa rokom
Server servira; logiku uređaja piše korisnik — u Forth-u. ecm-forth je threaded-code VM po Chuck Moore modelu: reči se jednom kompajliraju u tokene, runtime vidi samo brojeve, imena posle boot-a više ne postoje. Ćelija je mašinska reč (32-bit na STM32, 64-bit na serveru), dispatch je computed-goto, ceo VM je ~10 KB .text na Cortex-M7 (-Os).
Tri odluke koje ga čine upotrebljivim u produkciji, ne samo na demo tabli:
- Budžet koraka — nijedna reč ne može da vrti večno. "begin 0 until" vrati grešku "limit izvršavanja"; čip ne trepne, ništa se ne obesi.
- Pečat — posle boot-a VM odbija SVAKO novo definisanje. Izvršava se samo ono što je već kompajlirano. Nema "eval user inputa" rupe, po dizajnu.
- Nula alokacija — VM se digne nad statičkim baferom, ista priča kao server.
Na H755 Forth radi na Cortex-M4 sa tvrdim rokom od 1 ms po otkucaju. Sa ploče, u trenutku pisanja ovog posta:
Code:
najgori slučaj ikada 21 µs (od 1 000 µs budžeta)
propušteni rokovi od reseta 0
greške Forth koda 0Onaj međujezgarni prekid iz koraka 4 gore dodao je tačno 2 µs na najgori slučaj — zato je i smeo da uđe.
I VM je testiran kako treba: 303 provere × 5 konfiguracija (host 64-bit computed-goto, host 64-bit switch, host 32-bit, MCU profil, ASan+UBSan) plus prevod za M0/M4/M7 — na svakoj gradnji. U kompajler su ugrađene tri tehnike iz PicForth-a (tail-call na ";", constant folding, fuzija poređenja i grane), svaka izmerena istim bench harnessom: konstantna petlja 2,7×, flag petlja 1,7×, a repna rekurzija od 100k nivoa pretvorena iz "ruši stek" u "radi".
Naspram nginx-a — 25 imena direktiva prema 459
Referenca je nginx 1.24.0 sa podrazumevanim modulima plus http_ssl: 459 jedinstvenih direktiva u 53 modula. Pošteno sortirano, tri korpe:
Code:
radi statički fajlovi uslovni zahtevi, opsezi bajtova, ETag,
gzip_static; 77 od 78 diferencijalnih
provera se poklapa sa pravim nginx-om
location prefiks, =, ^~, ~, ~*
TLS mbedTLS bez alokacije, klijentski certi,
ECDSA P-256/P-384
delimično keepalive 4 direktive; granulacija tajminga 1 ms
nema error_page svaki 404/403/301 ide sa Content-Length: 0
(nginx šalje 153-169 B HTML-a)
rewrite/if/set 19 od 34 testa pada; regex mašina POSTOJI,
modul ne
allow/deny mali moduli, ali vidi upozorenje dole
auth_basic
neće ni biti gzip u radu zlib deflate traži ~262 KB po vezi na
nginx-ovim podrazumevanim vrednostima;
gzip_static pokriva isti posao
proxy/FastCGI 221 direktiva; traže klijentsku stranu,
upstream upstream skup, keš na disku i deljenu
memoriju između procesa — ovde je jedan
proces i jedna nit
HTTP/2, HTTP/3 van opsega za ovu klasu uređaja
stream, mailNajoštrija ivica, otvoreno: nepoznata direktiva se trenutno ćutke ignoriše. nginx odbija da se digne na njoj. To znači da konfiguracija koja sadrži deny all ili auth_basic ovde uredno prođe i ne sprovodi ništa — bezbednosna zamka, ne neudobnost. To je sledeća stvar koja se popravlja.
Kako se proverava
Tvrdnje o ugrađenom serveru su jeftine. Ovih pet provera je teško lažirati i puštaju se na svakoj gradnji.
1. Nula alokacija, dokazano linkerom. Test binarni fajl se linkuje sa -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc. Svaka alokacija bilo gde — uključujući unutar mbedTLS-a — uveća brojač. Pun mTLS handshake i raskid prijavljuju nulu. Ovo nije tvrdnja iz pregleda koda; build pada ako ikad prestane da važi.
2. nginx-ovi sopstveni testovi, sa imeniocem koji se ne pumpa. 279 od 313. Dve ograde, obe čine broj gorim: 476 od 495 test fajlova se preskoči jer traže proxy, stream ili HTTP/2 — i konfiguracija koju server prijavljuje namerno ne tvrdi --without-http_rewrite_module, iako rewrite-a nema. Da tvrdi, rezultat bi bio 279/294, tako što bi padove sakrio umesto popravio.
3. Diferencijalno protiv pravog nginx-a. Isti zahtevi se puste na ecmx i na pravi nginx 1.24.0 nad istim fajlom, pa se odgovori porede polje po polje. Odvojeno, regex mašina se poredi sa PCRE2 10.42 na 367 584 parova obrazac/ulaz, uključujući nasumično generisanje obrazaca. Oba trenutno prijavljuju nula neslaganja.
4. Greška koju je ovo našlo prošle nedelje. POSIX klase znakova — [[:digit:]], [[:alnum:]] — kompajlirale su se bez greške i čitale kao običan skup znakova:
Code:
[[:digit:]]+ nad "123" -> ne hvata <- trebalo bi da hvata
[[:digit:]]+ nad ":]" -> HVATA <- ne bi smelo
[0-9]+ nad "123" -> HVATA (kontrola, ispravno)Znači location ~ ^/admin/[[:alnum:]]+$ ne bi hvatao ništa. Preživelo je 354 415 nasumičnih poređenja zato što generator obrazaca nikad ne emituje [[:. Uhvatio ga je harness sa fiksnim obrascima. To je argument za to da se puštaju oba, i razlog zašto se "nula neslaganja" uvek mora čitati zajedno sa "nad kojim ulazima". Sad je popravljeno: svih 14 POSIX klasa, negacija, i PCRE-ova tačna pravila za neispravne slučajeve.
5. Ploča opisuje samu sebe. Sve gore je i "datasheet" stranica koju servira sam H755 — sa živim brojačima (uptime, opsluženi zahtevi, najgori slučaj RT jezgra, propušteni rokovi) koje stranica vuče sa /api/sve svake tri sekunde. Server koji se opisuje je server koji ti odgovara.
Otvoreno
Code:
nepoznate direktive se ćutke ignorišu bezbednost konfiguracija koja izgleda
zaštitno to nije
istek sertifikata se ne proverava bezbednost istekli klijentski certi
se prihvataju
listen [::]:port ubija server blok ispravnost proces radi bez slušača
granulacija tajmera je 1 ms brzina spori rukovaoci plaćaju
>=1 ms po krugu
nema rewrite / if / set funkcije 19 padova; mašina postoji
prazna tela odgovora na greške kozmetika odstupa od nginx-a na
svakom statusu greškeSitno
Merenja: NUCLEO-H755ZI-Q, GCC -Os / -O2, mbedTLS bez hipa. Propusnost preko wrk -t2 -c8 -d20s na 100 Mbit Ethernetu, isti parametri u svakom prolazu. Referenca: nginx 1.24.0, PCRE2 10.42. Forth: 303 provere × 5 konfiguracija, bench harness identičan pre i posle optimizacija.
Pitanja i rupe u rezonovanju su dobrodošli — pola gornjeg spiska "otvoreno" je nastalo tako što je neko pitao "a jesi proverio...".
