Na odezvy běžného jádra nelze vůbec spoléhat, na Raspberry Pi po chvíli ukáže cyclictest při i nepromyšleném zatížení maxima okolo 15 milisekund.
https://rt.wiki.kernel.org/index.php/Cyclictest
Přitom maximální doby nejsou vůbec ničím zaručené/omezené. Průměrné latence jsou sice velmi pěkné i pro ne RT jádro, ale maximum při připojování USB zařízení, přepínání grafického režimu atd. může jít třeba i do sekund.
Veškeré příklady se vztahují na modifikované, plně preemptivní jádro. Viz rozbor a úvod v předchozím článku
GNU/Linux pro řízení a rychlost jeho odezvy
a průběžné testování prováděné v rámci projektu OSADL Realtime QA Farm
https://www.osadl.org/Hardware-overview.qa-farm-hardware.0.html
Patche lze nalézt na stránkách projektu fully-preemptive kernel
https://www.kernel.org/pub/linux/kernel/projects/rt/
https://rt.wiki.kernel.org/index.php/CONFIG_PREEMPT_RT_Patch
na stránkách OSADL jsou kompletní patch serie pro testované konfigurace.
Pro Raspberry Pi mám patche také naaplikované v rámci repository
https://github.com/ppisa/linux-rpi
Teď je tam 3.18, ale připravujeme pro testování jádro 4.1 nebo 4.4, podle toho, které bude vykazovat lepší výsledky. Verze jsou vybrané tak, aby byly dlouhou dobu podporované.
Archiv s nakompilovaným jádrem, s kterým jsem teď zrovna prováděl testy na RPi2 je zde
http://cmp.felk.cvut.cz/~pisa/linux/rpi/linux-3.18.16-rt13-rpi2.tar.gz
RTAI dokáže bezchybne chodiť. Testoval som ho tak, že som počas ovládania CNC mašiny, spúšťal videa, browsoval, proste som robil zle a neurobil žiadnu chybu.
RTAI zabezpečí pre aplikáciu presne vymedzený pravideľný čas a ostatné aplikácie čakajú vo fronte, takže aj také zložité jadro dokáže ústáť RT na jednotku.
Xenomai, RTAI má smysl tam, kde důvěrou a rychlostí odezvy fully-preemptive Linux nestačí. Ale jedná se o řešení, které vyžaduje v aplikacích používat v RT částech API odlišné od hlavního systému. RT doména s nejvyšší prioritou je v podstatě alternativní jádro, plánovat, které pouští RT aplikace ve vlastním prostředí a běžné Linuxové tasky a jádro se pouští ve zbývajícím čase.
Rozumím tomu správně, že přerušení od inkrementálního čidla může být zpracováno různými thready a tím pádem i na různých CPU jádrech? Pokud ano, tak ten ovladač není napsaný správně, protože není thread-safe.
Např. tohle: https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L66
Není thread safe konstrukce, volatile nestačí, protože nezajišťuje cache coherency, může se stát, že různé thready bodou mít v cache různé hodnoty position a tím pádem se může incementovat/decrementovat stará hodnota position.
Navíc ani operace ++ a -- nejsou obecně thread safe, může se to rozgenerovat na více instrukcí a nemusí to být atomické.
Máte pravdu, že by měl být použitý atomický typ. Ale potom by i stavová mašina pro dekódování pořadí musela být chráněná spinlockem. Původně byl kód psaný pro RPi1, kde to není problém. Pro RPi2 by to také neměl být problém, protože o obsluhu se nakonec postarají vlákna na jednom procesoru. Ale raději to ještě zkontroluji. Hardwarově je přerušení z celé brány vedené jen na jedno CPU (RPi2 neumí rozkládat zátěž jednoho zdroje na víc CPU, jen lze volit směřování, viz https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2836/QA7_rev3.4.pdf 3.2.2 Core un-related interrupts).
Obecně, pokud je k dispozici společné přerušení z libovolné změny libovolného signálu, tak lze softwarové dekódování IRC napsat velmi efektivně bez jediného skoku, použil jsem to již mnohokrát, ale zatím to nechám k procvičení čtenářům a našim studentům. V takovém případě není potřeba obecně potřeba ani volatile, protože jak syscall tak IRQ povedou k synchronizaci stavu CPU/ memory bariera a RPi2 cache jsou SMP koherentní. Přitom na úrovni jádra je zaručené, že jedno přerušení nemůže být spuštěné vícekrát v souběhu i pokud je zátěž rozkládaná na více CPU.
Pro více zdrojů, samostatná přerušení z různých pinů to ale obecně může být problém a pro obecnost bych to měl opravit.
Já si myslím, že by tam měl být spinlock. Pokud není potřeba spinlock, tak většinou není potřeba ani volatile. Se spinlockem zase volatile ztrácí smysl. Jen pro doplnění: https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt
Souhlasím, ale musí být i kolem celých přístupů k gpio_irc_state a je to již určitý overhead, Na fully preemptive jádře se spinlock irq save převede na kompletní RT mutex s děděním priorit atd. Takže je otázkou, jestli to nezkusit napsat lockless/blockless. Asi to tak zkusím v rámci cvičení časem vyřešit. Na RPi1/2 si ale myslím, že to není v daném použití kritické.
Spinlock má overhead, s tím souhlasím, ale myslím si, že i na RPi2 má ten kód nedefinované chování. Neexistence sychronizace se na multicore systému dřív nebo později někde projeví a i když se přerušení zpracovává na stejném CPU core, tak uživatelská aplikace poběží na jiném.
Tohle se předpokládám volá z uživatelského threadu?
https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L224
Což může zpracovat úplně jiné CPU core, které má nesychronizovanou cache, takže hodnota position se může číst z L1 CPU core cache a může být potencionálně úplně jiná (stará), než jakou zapsalo CPU core zpracovávající interrupt. Takže se může nedefinovaně dlouho dobu číst v uživatelské aplikaci neaktuální hodnota position.
Prakticky to asi moc nastávat nebude, protože data v L1 cache se mění často, ale není to zaručeno.
Výjimek, kdy datové cache mezi jádry nejsou synchronizované je minimum. Ve světě klasického SMP v podstatě neexistují. Jinak by nestačilo z vláken jen přistupovat na data do paměti a pak provést instrukce řešící synchronizaci CPU paměť (paměťové bariéry), musel by se provádět explicitní flush daných datových oblastí. Přitom flush bez příznaků pro každý byte (nebo alespoň slovo) nelze provést při nekoordinovaných cache správně, přepíší se data, která v dané řádce změnil dříve někdo jiný. Takže MOSI/MOESI protokol musí být na HW úrovni mezi datovými vyrovnávacími pamětmi implementovaný a aplikace, které na různých CPU zapisují do společných řádek jsou tvrdě penalizované.
Kde se naopak rozvolňuje model pro zvýšení rychlosti zpracování instrukcí, je asynchronizmus mezi jádrem a lokálním vstupem do paměťového subsystému, to je instrukce a přístupy se provádějí v pořadí podle dostupnosti dat (závislostí) a zde hrozí velké riziko, že jiné jádro uvidí (bude mít zviditelněné) změny v jiném pořadí než jádro, které je provádí. Paměťové bariéry, které zajistí zviditelnění následujících změn až po tom, co jsou předchozí změny viditelné (buď všechny nebo v určitém paměťovém rozsahu) jsou pak nutností. Měly by i v tomto kódu být. Ale čtení int32 je atomické, bariéry při vstupu do jádra a výstupu obvykle jsou (jen přístup na struct file kolem sebe má zámky) takže maximálně se může stát, že se hodnota změny čítače způsobená přerušením během daného syscallu nepromítne do vrácené hodnoty. To ale ničemu nevadí, příště bude přečtená novější hodnota. Obecně nebude přečtená hodnota starší, než byla hodnota v okamžiku volání jádra. Zde tedy problém není a často se právě s využitím RCU a podobných mechanizmů tam, kde to není na závadu, tento postup kdy je čtená hodnota, která může být starší než ta aktuální, al ene starší než nějaká garance na straně příjemce, používá. Proto jsem synchronizaci příliš neřešil, nechal kód tak jak byl pro RPi1. Na straně čtení je chyba jen pedagogická a ve skutečnosti to je optimalizace.
Problém je v tomto případě na straně update stavu, a ten jsem pro obecné použití řešit měl.
Co se pak týče obecné synchronizace pamětí, tak bariéry jsou jasné, často pak není řešená hardwarová synchronizace mezi periferiemi a seskupením (clusterem) CPU jader, někdy/výjimečně jsou clustery nesynchronizované nebo lze volit, jestli se mezi nimi datová cache synchronizuje. Pokud nejsou jádra používaná v režimu big.LITTLE s přesunem celého systému (všech tasků) z jedné skupiny na druhou, tak i u ARMu je zvykem datovou cache mezi clustery synchronizovat hardwarově.
Overhead s explicitní synchronizací datové cache mezi procesorovými skupinami a koprocesory a periferiemi nakonec i do světa RISC procesorů zavedl koherenci i mimo CPU, ARM CoreLink cache coherent interconnect (AMBA ACE). FreeScale (NXP) také pro PowerPC zavedlo CoreNet fabric která dovoluje volit mezi různými CPU jádry a periferiemi mezi koherentní a ne-koheretní variantou komunikace. Opět, ty části, které jsou single system image (jedno OS jádro a SMP) jsou určitě nastavené pro koherentní komunikaci. Jádro Linuxu pro plně nekoherentní datovou paměť cache mezi jádry není vůbec připravené.
Naopak ARM a i jiné RISC procesory často i přes zajištění koherentní datové cesty neřeší v HW koherenci lokálních instrukčních pamětí. I když i zde je snaha alespoň pro plnění řádek instrukční vyrovnávací paměti provádět snoop do datových cache vlastního i ostatních jader. Ono zcela bez snoopu a alespoň broadcastu invalidace instrukční paměti (nebo flushe datové) by každá změna kódu třeba v JIT enginu znamenala přes IPI přerušit běh kódu na všech ostatních jádrech a donutit je provést flush na paměťovém daném rozsahu. Tím by pro JAVA, Javascript a mnoho dalších technologií byla na RISCu reálně nepoužitelných.
Už do toho nechci moc vrtat, protože podrobně neznám hardware RPi2 a funkčnost toho kódu je zjevně závislá na konkrétním hardware, nicméně v tom kódu je ještě jeden problém.
Pokud se tohle spouští na různých jádrech (a pochopil jsem, že ano):
https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L91
https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L124
https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L157
https://github.com/ppisa/rpi-rt-control/blob/master/kernel/modules/rpi_gpio_irc_module.c#L190
Tak není zaručeno, že druhé jádro uvidí updatovanou hodnotu position, prev_phase..., i když ty funkce proběhnou sekvenčně a ne paralelně. Ty data zapsaná prvním jádrem totiž můžou viset ve store bufferu CPU. Musí tam být memory bariéra, která store buffer vyprázdní. Prakticky to asi nenastane, ale formálně správně to podle mě není.
Všechno tohle je proto, že se nepoužívají žádná sychronizační primitiva. Chápu, že se snažíte o maximální výkon, nicméně multithreadové věci je velmi složité domyslet a cokoliv nestandardního (volatile) vede na těžko odhalitelné chyby, které se můžou projevit jen ve velmi obskurních případech.
Má to být školní případ, proč ukazovat studentům kód, který je velmi subtilní (a formálně je zřejmě špatně, i když se to prakticky neprojevuje). Nebylo by lepší napsat to tak, jak se to normálně dělá, tozn. se spinlockem?
K problému s paralelním přístupem ke stavu jsem se již přiznal v předchozí diskuzi.
Konkrétní kód původně vznikal sice pod mým dohledem ale v rámci bakalářské práce. Bylo to před dvěma lety a jen pro RPi1. Pak jsem kód hodně pročišťoval, ale ještě jsem neuvažoval o RPi2.
Jinak příklad je to spíš motivační, ve smyslu zkuste si s RPi a nějakým HW pohrát.
V příkladu je další zásadní hrůza, přístup na GPIO z userspace a nastavování módů pinů. Tato část je zcela nechráněná. Na druhou stranu je přístup z userspace přímo na HW velmi přínosný při seznamování se s HW a experimentování. Tak jsem to zadával a i v práci nechal, navedl studenta na toto řešení.
Správné by bylo i pro PWM vytvořit ovladač a jak IRC tak PWM konfigurovat přes device-tree. Device tree jsem již pro různé platformy dával dohromady.
Takže jako pokročilejší level bych mohl příklady přepsat tak, jak by měly být. Ono v době vzniku kódu ani Raspberry Pi pořádně přes device-tree nebootovalo. Podpora chyběla. Stejně jako U-boot nebyl k dispozici.
Je to opravdu spíš hrátka, že to takto lze. Obecně sám každého, kdo chce použít RPi v seriózních aplikacích odrazuji, ale právě pro to, abych nalezl společnou řeč se studenty, kteří si často přímo žádali, že chtějí v rámci závěrečných prací pracovat na platformě, kterou si sami koupili a mají k ní vztah, tak jsem se vůbec začal RPi zabývat.
Zajímavé je, že teď v rámci projektu RTEMS komunikuji s člověkem z NASA, který investoval čas do portace z drahých GR Sparců na RPi, právě pro to, aby mohli do projetu v rámci GSoC zapojit studenty.
Takže RPi je v mnoha ohledech špatně. Sám jsem se k němu prvně dostal, když ho začala jedna firma používat v průmyslu a odcházely jim SD karty. Tak jsem jim problém řešil.
Bohužel cena je dnes to hlavní, takže i jiné firmy bez toho, že bych jim to doporučoval, nasazují RPi, tam, kam podle mě nepatří. také v každém článku varuji, toto je hraní, před seriózním použitím se podívejte po alternativách.
Ale souhlasím s tím, že připravit i tuto hračku nejlépe, jak by to bylo možné, má smysl. problém je především čas. Ale někdy se na to na chvíli zase podívám. A za to, že jste mě postrčil k tomu, že bych to měl udělat pořádně(ji) a předvést třeba tu integraci do device-tree děkuji.
2 gamer: Pokud mi pošlete e-mail tak až/jestli se k úpravám dostanu, tak Vám mohu emailem poslat připomínku a můžete mi to pak zkritizovat.
Určitě by mělo smysl kód přepsat na
request_irq(ircst->irc_irq_num[0],
irc_irq_handlerARF,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"irc1_irqARF", ircst)
případně i na RT jádře přidat IRQF_NO_THREAD, protože pro těch pár instrukcí končících "inc" a "dec" je požadování přeplánování vysloveně nesmysl. V případě RPi, které takto zvládá obě hrany pak budou stačit pouze dva piny.
Když by se ještě přidalo načtení obou vstupů, tak lze kód sloučit do jednoho handleru (ano teoreticky stále může běžet na rozdílných jádrech, ale pouze mimo RPi) a dekódování lze řešit tabulkami bez skoků. Ale o bych si napsal semestrální práce našich studentů pro VxWorks stejně jako několik nedotažených GSoC z letošního, minulého a předminulého roku.
Jen chci vysvětlit těm, komu to již z principu určování polohy z IRC není jasné, že celou tuto diskuzi je nutné brát jen jako zajímavé mozkové cvičení z oblasti paralelizmu. Sám jsem se do něho zapojil se zápalem, ale úpravy žádné reálné zlepšení nepřinesou.
I přes přidání bariér a třeba i zamykání doje v okamžiku změny pořadí vykonání obslužných rutin zpracovávající změny stavu signálů ke ztrátě polohy, protože právě pořadí změnu polohy a směr změny kóduje.
Úloha je právě zajímavá tím, že je možné při větším rozlišení IRC systém zatížit na maximum. Přitom alespoň na RPi1 právě řazení čtyř rozdílných úloh do "runqueue" ve režimu FIFO logicky, ale pro nás překvapivě přispělo k zvládnutí zátěže o nějaké to procento vyšší tím, že ojedinělé delší latence byly překonané ukládáním do fronty o hloubce 4. Popis tohoto zajímavého pozorování je uvedený v odkazovaném článku z Real-Time Linux Workshopu. Na RPi2 to možná na 100% platit nemusí, ale RPi2 má zase větší rezervu výkonu.
V některých případech se na úrovni HW zpracování IRC používá řešení, které by šlo použít i zde. Pokud je vyhodnocené, že se již motor otáčí velmi rychle, tak se přestanou sledovat (v SW zakáží přerušení) od jednoho kanálu a při změnách druhého se přičítá po dvou. Pokud se otáčky sníží, přejde se opět na režim sledování obou signálů a vyhodnocení směru. Z fyzikálních vlastností motoru/systému (setrvačnost, dynamika) totiž není možné, aby se směr ve vysokých otáčkách naráz měnil. Vždy bude vyhodnocený přechod na nízké otáčky (alespoň na několik pulzů) a v této době bude změna směru vyhodnocená.
Dekuji za clanek. Mne se libi ten minimalistickej modul, mozna ho nekdy pouziju jako uvodni priklad pro vlastni IRQ tvorbu. Zatim totiz jedu pres kombinaci mmapu, pollingu a usleepu a v jadru nemam ani radku (a jelikoz mam vyjimecne dost predvidatelny deje v HW, tak to kupodivu i celkem staci).