Portál AbcLinuxu, 8. května 2025 01:00
Stavy vydání jádra. Nebezpečí printk().
Současný vývojový kernel je 4.9-rc4 vydaný 5. listopadu. Linus k tomu řekl: „Nebudu vám lhát. Tohle není malé rc a byl bych raději, kdyby tomu bylo jinak. Ale ani to (pro tak velké) vydání není nepřiměřeně velké, takže bych si neměl začít dělat starosti. Zatím stále doufám, že skončíme s obvyklými sedmi kandidáty na vydání, tedy za předpokladu, že se věci zklidní. Uvidíme, jak to půjde, až se přiblížíme datu vydání.“
Seznam z 6. listopadu obsahuje 17 známých regresí v jádře 4.9.
Stabilní aktualizace: tento týden žádné nebyly vydány. Verze 4.8.7 a 4.4.31 byly v době psaní tohoto článku v procesu revidování, dostupné jsou od 10. listopadu.
Někdo by mohl být v pokušení si myslet, že se toho o jaderné funkci printk() mnoho říct nedá. Vše, co dělá, koneckonců je, že dá řádek textu na výstup do konzole. Jenže printk() má své problémy. Během své přednášky na Kernel Summitu Sergej Senožatský řekl, že printk() ve stávající podobě prostě nemůže používat. Dodal však dobrou zprávu, že ona funkce není neopravitelná, ba dokonce je řešení problémů v plánu.
Jedním z největších problémů spojených s printk() je deadlock, ke kterému může dojít vícero způsoby. Jedním z nich jsou reentrantní volání. Uvažujme vyvolání printk(), kterému zabrání nemaskovatelné přerušení (non-maskable interrupt, NMI). Obsluha tohoto NMI bude dost možná chtít něco vytisknout na výstup. NMI ostatně přísluší k mimořádným událostem. Jestliže první volání printk() drží nezbytný zámek, způsobí druhé volání při pokusu o získání stejného zámku deadlock. To je právě ten druh nepříjemnosti, kterému se vývojáři operačních systémů snaží zdaleka vyhnout.
Tento konkrétní problém se podařilo vyřešit. printk() nyní má speciální buffer pro každé CPU, který se používá pro volání v kontextu NMI. Výstup přechází do tohoto bufferu a po skončení NMI je vymazán, čímž odpadá nutnost zabrat zámky, které volání printk() obvykle potřebuje.
Tím ale výčet příležitostí k deadlocku printk() bohužel nekončí. Ukazuje se, že volání printk() mohou být rekurzivní, obvyklému zákazu rekurze v jádře navzdory. K rekurzivním voláním může dojít v důsledku varování vynořivších se z hlubin jádra. Ladění zámků bylo také zmíněno jako způsob, jak vytvořit volání printk() v nevhodnou dobu. Pokud dojde k volání printk() v nesprávnou dobu, výsledkem je rekurzivní volání, které může vyústit v deadlock způsobem analogickým tomu v případě zabraných volání.
Tento problém vypadá podobně jako NMI, takže by nemělo být překvapením, že i řešení je podobné. Sergej přišel s návrhem rozšířit myšlenku řešení NMI, a sice vytvořit pro výstup printk() větší počet bufferů pro každé CPU. Kdykoli se printk() dostane do té části kódu, kde by mohlo k rekurzi dojít, výstup ze všech rekurzivních volání půjde do těchto bufferů, aby je šlo vyprázdnit, až to bude bezpečné. Nebezpečné oblasti jsou označeny novými funkcemi printk_safe_enter() a print_safe_exit(). Možná trochu matoucí je, že printk_safe_enter() neoznačuje bezpečnou oblast, nýbrž místo, kde má být použit „bezpečný“ kód pro výstup.
Jelikož požadavek na buffery vyhrazené pro každé CPU vzniká ve stále více situacích, Peter Zijlstru zajímalo, zda by printk() neměl používat buffer pro určité CPU úplně vždycky. Sergej na to odpověděl, že se o tom uvažuje.
Hannes Reinecke řekl, že část problému vyplývá ze dvou odlišných případů použití printk(): „pokec“ a „systém každou chvíli spadne.“ První případ může přijít na řadu kdykoli, zatímco ten druhý je urgentní. Při nedostatku lepších informací musí printk() předpokládat, že naléhavé je všechno, ačkoliv spousta problémů by se dala vyřešit pouhým odložením výstupů, které nejsou urgentní, na bezpečnější chvíli. Linus Torvalds poukázal na to, že argument úrovně logování by měl indikovat, kdy se jedná o naléhavý výstup, ale Peter opáčil, že odkládání nekritických výstupů má ke konečnému řešení daleko. Skutečný problém je podle něj v ovladačích konzole. Na toto téma znovu přišlo později.
Jedním z problémů s odkládáním neurgentních výstupů je podle Sergeje skutečnost, že se může změnit pořadí zpráv, které později může být těžké znovu seřadit. Peter naznačil, že to není až tak velký problém, a Hannes poměrně důrazně prohlásil, že výstupy printk() na sobě mají časová razítka, takže jejich opětovné seřazení nebylo složité. Podle Linuse je problém v tom, že časová razítka se nemusí shodovat na různých CPU, tudíž pokud dojde k přesunu vlákna, pořadí zpráv může být chybné.
Petr Mládek, který se k Sergejovi při přednášení připojil, řekl, že je tu problém s buffery pro každé CPU: v podstatě nutně budou menší než jediný globální buffer, což může omezit objem hromaděných dat k výstupu. Takže je pravděpodobnější, že systém s buffery pro každé CPU bude ztrácet zprávy. Bylo poukázáno, že subsystém ftrace tento problém už dávno vyřešil, ale za cenu spousty komplikovaného kódu pro cyklické buffery. Linus řekl, že jedna záležitost, se kterou je třeba zacházet opatrně, jsou zprávy oops, které jsou výsledkem pádu jádra – ty musí okamžitě jít do konzole.
Sergej pokračoval tím, že deadlocků printk(), které je třeba vyřešit, je o poznání více. Dosud se debata týkala „vnitřních“ zámků, které jsou součástí printk(). Ovšem printk() musí často zabírat další zámky „venku“, v jiných částech jádra. Nejproblémovější oblastí se jeví posílání výstupu do konzole. V různých ovladačích sériových konzolí jsou zámky a související problémy, které mohou rovněž vést k deadlocku. Na rozdíl od vnitřních zámků ty externí printk() neovládá, takže tento problém je složitější na vyřešení.
Jádro již má funkci printk_deferred(), která dělá vše pro to, aby se vyhnula externím zámkům a znovu odkládala výstupy na bezpečnější chvíle. Sergej navrhuje, aby se printk() vždy chovalo jako printk_deferred(), čímž by se mezi nimi setřel rozdíl a časem by šlo printk_deferred() odstranit. Jedinou výjimkou by byly nouzové výstupy, které by šly vždy přímo do konzole. Linus navrhl jít ještě dál, a odkládat i nouzové případy, ale vyprázdnit vyrovnávací paměti bezprostředně poté.
Nicméně zámky nejsou jediným problémem s printk(). Toto volání musí při vytisknutí příslušných zpráv zavolat ovladače konzole a nakonec také zavolat console_unlock(), čímž se krom jiného na konzoli pošle všechen zbývající výstup. Háček je v několika nešťastných vlastnostech této funkce: může se zacyklit, nemusí být možné jí zabránit v běhu a případná prodleva závisí na rychlosti konzole, která nemusí být vůbec rychlá. V důsledku tedy nikdo neví, kolik času vlastně volání printk() zabere, takže v řadě situací – namátkou atomický kontext, kritické sekce RCU, kontext přerušení aj. – ani není zcela bezpečné ho provádět.
Jan Kára navrhl tento problém obejít tak, že printk() bude zcela asynchronní. Výstup by opět byl směřován do bufferu a do konzole odeslán později, ale v tomto případě by samotný zápis do konzole probíhal ve vyhrazeném jaderném vlákně. Volání printk() by prostě uložilo zprávu a pak použilo mechanismus irq_work k nastartování onoho vlákna. Tento návrh prošel prakticky bez námitek účastníků diskuze.
Dále je tu problém s pr_cont(), což je varianta printk(), která se používá k vytisknutí jednoho řádku pomocí vícero volání. Tato funkce na SMP systémech není bezpečná – výsledek výstupu může být rozsypaný a poškozený. Bylo by sice velmi žádoucí zbavit se typu výstupu s „pokračováním řádky“, leč počet volání pr_cont() v jádře prudce roste, jak upozornil Sergej. Linus poukázal na to, že problém spočívá v absenci jiného pohodlného způsobu, jak z jádra vytisknout řádky s proměnlivou délkou. Je možné upravit pr_cont() například tak, aby byly využívány buffery pro každé CPU, ale hodilo by se k tomu důkladně promyslet a navrhnout pomocnou funkci. Pak by snad mohla být použití pr_cont() snadno opravena skriptem Coccinelle.
Ted Ts'o se dotázal, jak velký problém proházený výstup skutečně tvoří – na produkčních systémech. Zdálo se, že panovala shoda na tom, že jde o problém vzácný. Linus řekl, že občas vídá škaredý výstup oops v důsledku pokračujících řádek. Andy Lutomirski s úšklebkem prohlásil, že jeho postup, jak se vypořádat se zpřeházenými řádkami, spočívá v tom, že počká, až mu je Linus srovná. Na tomto řešení se kolektiv jednohlasně shodl, a tak se nezdá, že by byla v nejbližší době v plánu nějaká práce v tomto směru.
Poslední téma, na které už mnoho času nezbylo, představoval semafor console_sem. Tento semafor řeší přístup ke všem konzolím v systému, takže je to globální úzké hrdlo, kde dochází k bránění v přístupu. Existují ovšem způsoby, jak zabrat console_sem, aniž by bylo potřeba upravovat seznam konzolí či zapisovat do konzole. Kupříkladu prosté čtení /proc/consoles z uživatelského prostoru zabere inkriminovaný semafor, což může vést k nepříjemným prodlevám týkajícím se i samotného printk(). Následné uvolnění tohoto semaforu opět vede k volání console_unlock(), se kterým jsou spojené stejné problémy.
Sergej navrhl, že console_sem by se mohl změnit v zámek čtenářů/písařů. Pak by způsoby použití, které nepřistupují k seznamu konzolí, mohly zámek získat v režimu čtenáře, čímž by se zvětšilo využití paralelizace. Nepomohlo by to však s přímými voláními console_unlock(), která by se stále zasekávala na vyprazdňování výstupu na zařízení. Pro tento případ přišla řeč na rozdělení console_unlock() na dvě varianty: synchronní a asynchronní. Ta druhá by mohla jednoduše probudit vlákno printk(), místo aby se sama zabývala zbývajícím výstupem do konzole. Nezdá se však, že by v tomto směru byla práce nějak urgentní.
Pak už přidělený čas vypršel a setkání skončilo. Pokud máte zájem, Sergejova prezentace je k dispozici na webu.
"zámek čtenářů/písařů" zní jak když si literární kroužek vyjede v létě na výlet na Karlštejn.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.