Portál AbcLinuxu, 5. května 2025 15:19
Aktuální vývojová verze: 3.14-rc2. Citáty týdne: Steven Rostedt, Ingo Molnar, H. Peter Anvin, Heiko Carstens. Příznaky jako návrhový vzor API systémových volání: Opakování chyb; Další případy chybějících příznaků.
Aktuální vývojová verze jádra je 3.14-rc2 vydaná 9. února. Linus poznamenal, že patchů bylo pomálu, ale má obavy, že si to na něj vývojáři jen někde chystají. Já ale jaderné vývojáře znám, jsou rafinovaní. Podezírám Davema (vybírám někoho ne úplně náhodně), že se kdesi potají směje, zatímco čeká na toto oznámení, aby na mě zítra mohl navalit něco gigantického.
Stabilní aktualizace: verze 3.13.2, 3.12.10, 3.10.29 a 3.4.79 vyšly 9. února.
Verze 3.13.3, 3.12.11, 3.10.30 a 3.4.80 se právě revidují. Greg varuje, že tyto aktualizace mohou být poněkud problematičtější:
Něterá stabilní vydání létají z mého sestavovacího systému jako nic. V případě těchto to tak nebylo. Možná za to může špatné počasí, které při vytváření těchto jader panovalo, nebo snad něco jiného, tak či tak ale tyto aktualizace přicházely na svět za hrozivého křiku a odporu, sestřelovaly sestavovací servery všude kolem sebe a s každým patchem kompilace selhávala. [...]
Dobře je proto otestujte, na mých systémech stěží přežily a ani trochu jim nevěřím, že nesní vaše disky, nepostřílí vaše procesy a s ďábelským smíchem nebudou utíkat, zatímco z vašeho CPU zbude akorát topítko.
Pokud to s tím masakrem nebude až tak horké, tak se těchto aktualizací dočkáme 13. února nebo později.
Dále se reviduje verze 3.2.55, vydání očekáváme 14. února nebo později.
Tohle by byla dobrá chlastací hra. Kdykoliv někomu opravíte chybu ze sparse, tak se bude muset napít.
...kterážto by vedla ke zlepšené náladě mezi vývojáři a ještě více chybám ve Sparse, což by vedlo k ještě více opravám od PeterZ a ještě více vypitým kouskům, což by vedlo k pěknému cyklu. To vypadá na fajn zábavu pro všechny, až na Petra.
-- Ingo Molnar
Nemáme žádný dobrý způsob, jak řešit označení něčeho za zastaralé, to je ten problém. Obvykle na to nedojde, dokud nezjistím, že se nám tam vpletla chyba a „X už po dobu N vydání nefunguje a nikdo si toho nevšimnul“.
-- H. Peter Anvin o zastarávání podarchitektur
Máme v plánu odstranit podporu 31bitového jádra pro architekturu s390.
Důvod je docela jednoduchý: aktuální 31bitové jádro bylo rozbité skoro rok, než si toho někdo všimnul.
-- Heiko Carstens to předvádí v praxi
Systémové volání renameat2(), které nedávno navrhl Miklos Szeredi, je čerstvou připomínkou jednoho typu chyb při návrhu API mezi jádrem a uživatelským prostorem, které se na Linuxu často objevují (a předtím i na Unixu). Bližší pohled na minulost nám dává ponaučení, které bychom měli mít při přidávání všech budoucích systémových volání na srdci.
Systémové volání renameat2() je rozšířením renameat(), které je zase rozšířením starého volání rename(). Všechna tato volání plní podobný úkol: upravit položky v adresáři tak, aby existující soubor v rámci systému souborů dostal nové jméno. Původní volání rename() bralo jen dva argumenty: starou cestu a novou cestu. renameat() přidalo dva argumenty, každý souvisí s jednou z cest. Oba tyto nové argumenty mohou být popisovačem, který odpovídá adresáři: pokud je příslušná cesta relativní, pak je zpracována relativně k danému popisovači adresáře namísto relativně k aktuálnímu adresáři (jako to má rename()).
renameat() bylo jedno z balíku třinácti nových systémových volání, která přibyla v Linuxu 2.6.16 za účelem provádění různých operací se soubory. Dva účely argumentů s popisovačem adresáře jsou popsány v manuálové stránce openat(2):
Dalším krokem je pak renameat2(), které rozšiřuje funkčnost renameat() za účelem podpory nové situace: atomického prohození dvou stávajících cest. I když se tento případ vztahuje k dřívějším systémovým voláním, důvod, proč je potřeba definovat nové volání, je jednoduchý: renameat2() postrádá způsob, jak by jádro mohlo podporovat (a uživatel mohl požadovat) změny v jeho chování. Jinými slovy, schází mu argument typu flags, který by obsahovat bitovou masku, jako to mají volání jako clone(), fcntl(), mremap() nebo open(), která všechna mohou mít variabilní počet argumentů v zásvislosti na příznacích v argumentu flags.
renameat2() implementuje funkci „prohození“ a přidává nový argument flags, jehož bity mohou být použity k vybrání variací v chování tohoto systémového volání. Prvním z těchto bitů je RENAME_EXCHANGE, které volí funkci „prohazování“; bez tohoto příznaku se renameat2() chová jako renameat(). Přidání argumentu flags doufejme předejde tomu, abychom jednoho dne museli přidat volání renameat3 ve snaze rozšířit funkčnost. Andy Lutomirski se brzy ozval, že by se dal přidat další příznak: RENAME_NOREPLACE, který by při operaci přejmenování zabránil přepsání existujícího souboru. Původně bylo jediným způsobem, jak zabránit přepsání existujícího souboru bez rizika race conditions, použít link() (který selže, pokud cílová cesta existuje) pro vytvoření nového názvu a pak zavolat unlink() pro odstranění starého.
Při pohledu na příběh o renameat2 můžeme ale mít pocit dejà vu, jelikož důvodem pro vznik renameat() byla právě nemožnost rozšíření rename(), kterou by argument flags dodal. Uvažování nás vede k otázce: „Kolikrát jsme tuto chybu udělali?“ Odpovědí je „mnohokrát“.
Abychom našli další příklady, nemusíme hledat dlouho. Pokud se vrátíme ke třinácti systémovým voláním s „popisovačem adresáře“, které byly přidány v Linuxu 2.6.16, zjistíme, že bez nějakého zjevného důvodu dostaly čtyři z nich (fchownat(), fstatat(), linkat() a unlinkat()) navíc argument flags, který u tradičního volání nebyl, zatímco osm dalších (faccessat(), fchmodat(), futimesat(), mkdirat(), mknodat(), readlinkat(), renameat() a symlinkat()) jej nedostalo. (Zbývající volání openat() má argument flags, který pochází už z open().)
Jedno z volání bez argumentu flags – futimesat() – bylo brzy nahrazeno novým voláním, které jej už má (utimensat() přidaný v Linuxu 2.6.22) a vypadá to, že renameat() čeká stejný osud. To vede k myšlence: býval by se u ostatních volání také hodil argument flags? Při hlubším prozkoumání těchto funkcí je brzy jasné, že odpověď zní „ano“, alespoň u tří z nich.
Prvním případem je systémové volání faccessat(). Toto volání argument flags nemá, ale obalující funkce v knihovně GNU C (glibc) jej přidává. Pokud jsou v tomto argumentu nastaveny určité bity, pak obalení místo toho použije systémové volání fstatat(), aby zjistilo přístupová práva k souboru. Vypadá to, že se na chybějící argument flags přišlo až příliš pozdě, tak se návrhový problém obešel v glibc. (Autor systémových volání s „popisovačem adresáře“ byl tehdy správcem glibc.)
Druhým případem je systémové volání fchmodat() Podobně jako faccessat() postrádá argument flags, v obalení v glibc ale je. Obalující funkce nabízí příznak AT_SYMLINK_NOFOLLOW. Tento příznak ale není aktuálně podporován, jelikož jádro nenabízí potřebnou podporu. Je jasné, že obalení v glibc bylo napsáno tak, aby umožnilo vznik systémového volání fchmodat2() v budoucnosti.
Třetím případem je systémové volání readlinkat(). Abychom pochopili, jaký užitek by toto volání mělo z argumentu flags, tak se musíme podívat na tři volání přidaná v Linuxu 2.6.13, která jej mají: fchownat(), fstatat() a linkat(). Tato volání v Linuxu 2.6.39 přidala příznak AT_EMPTY_PATH. Pokud je ve volání nastaven tento příznak a argument s cestou je prázdným řetězcem, pak volání pracuje nad otevřeným souborem předaným v argumentu pro „popisovač adresáře“ (a v tomto případě může tento argument odkazovat nejen na adresáře). Toto umožňuje těmto voláním nabízet funkčnost podobnou té, co nabízejí fchmod() a fstat() ve tradičním unixovém API. (V tradičním API žádné flink() není.)
Striktně řečeno by funkčnost AT_EMPTY_PATH bylo možné podporovat i bez příznaku: pokud by cesta byla prázdným řetězcem, pak by volání mohla předpokládat, že mají pracovat nad argumentem s popisovačem. To, že je příznak vyžadován, má ale hned dva účely: dokumentuje záměr programátora a zabraňuje nehodám, ke kterým by mohlo docházet, kdyby argument s cestou byl prázdným řetězcem nechtěně.
Funkčnost „práce nad popisovačem“ se ukázala být užitečnou i u readlinkat(), které tuto možnost přidalo v Linuxu 2.6.39. readlinkat() ale nemá argument flags; toto volání jednoduše pracuje nad popisovačem, pokud je cesta prázdným řetězcem, a nemá tedy výhody, které s sebou příznak AT_EMPTY_PATH nese u jiných systémových volání. Proto je readlinkat() dalším systémovým voláním, kde by byl argument flags žádoucí.
Abychom to tedy shrnuli, ze osmi systémových volání s „popisovačem adresáře“, kde schází argument flags, se ukázalo, že je to chyba alespoň u pěti z nich.
Vývojáři Linuxu ale nebyli první, kdo takovou chybu při návrhu udělal. Dlouho předtím, než se Linux objevil, bylo wait() bez flags a pak wait3() s flags. A Linux opravil některé další výskyty tohoto omylu při návrhu poděděné od Unixu, takže máme například dup3() jako nástupce dup2() a pipe2 jako nástupce pipe() (obě volání byla přidána v Linuxu 2.6.27).
Navzdory ponaučení z minulosti se nám ale podařilo zopakovat tuto chybu i u volání specifických pro Linux. Kromě příkladů s popisovačem adresáře máme i další situace:
Původní volání | Nástupce |
---|---|
epoll_create() (2.6.0) | epoll_create1() (2.6.27) |
eventfd() (2.6.22) | eventfd2() (2.6.27) |
inotify_init() (2.6.13) | inotify_init1() (2.6.27) |
signalfd() (2.6.22) | signalfd4() (2.6.27) |
Uvědomění, že nějaké volání potřebuje argument flags, někdy přichází ve vlnách, kdykoliv vývojáři přijdou na to, že několik souvisejících API takový argument potřebuje; jedna taková vlna nastala v Linuxu 2.6.13, kdy čtyři z volání s „popisovačem adresáře“ obdržela argument flags.
Z tabulky výše je vidět, že další taková vlna nastala ve verzi 2.6.27, kdy bylo přidáno celkem šest nových volání. Všechna tato volání, stejně jako accept4() ze stejných důvodů přidané v Linuxu 2.6.28, vrací nové popisovače. Hlavním důvodem pro přidání těchto volání je umožnit volajícímu nastavit příznak uzavření při exec (close-on-exec) hned při vytvoření namísto pomocí odděleného volání fcntl(F_SETFD). Toto umožňuje aplikacím v uživatelském prostoru předejít jistým race conditions při používání tradičních volání v aplikaci s více vlákny. K těmto problémům by mohlo dojít, pokud by se jedno vlákno pokoušelo vytvořit popisovač a pak použít fcntl(F_SETFD) pro nastavení uzavření při exec, zatímco jiné vlákno by zrovna dělalo fork() a execve(). (Systémová volání socket() a socketpair() tuto novou funkčnost nabízejí od 2.6.27. Poněkud zvláštně ale bylo dosaženo toho samého pomocí vměstnání příznaků do vrchních bitů argumentu s typem soketu namísto vytvoření nových systémových volání s argumentem flags.)
Když se teď podíváme na nedávný vývoj v Linuxu, pak v Linuxu 2.6.28 byla přidána řada nových volání s argumentem flags, a to fanotify_init(), fanotify_mark(), open_by_handle_at() a name_to_handle_at(), ale v jejich případě byly příznaky zapotřebí už od začátku, takže nešlo o pojistku do budoucna.
Na druhou stranu tu máme chyby nebo málem chyby u jiných volání. syncfs() přidané v Linuxu 2.6.39 argument flags nemá, i když není jisté, zda by nějaký vývojář systému souborů příznaků využil, například aby volajícímu umožnil upravit způsob, jak má být systém souborů synchonizován na disk. Volání finit_module() přidané v Linuxu 3.8 dostalo argument flags až po žádosti na poslední chvíli; po jeho přidání se hned ukázal být užitečným.
Závěrem z této často opakované situace je, že vhodnou otázkou při návrhu každého nového systémového volání je: „Máme nějaký důvod nepřidat argument flags?“ Kladení si této otázky vývojáře vedlo ḱ moudrému rozhodnutí u volání process_vm_readv() a process_vm_writev() přidaných v Linuxu 3.2. Vývojáři těchto volání přidali (aktuálně nepoužívaný) argument flags, jelikož tušili, že se může jednou hodit. Jednoho dne se asi ukáže, že měli pravdu.
ioctl(int d, int request, ...)
žádné flags nejsou, ale pochopil jsem smysl námitky.
struct coffee_t
další položku, změní se velikost struktury. A programy zkompilované před změnou velikosti náhle budou vařit kafe s mlékem a citronem, protože se kvůli citronu podívají na pseudonáhodné hodnoty přesahující původní velikost pole. Anebo, pokud budou zkompilovány s runtime kontrolou velikosti struktur a polí, tak rovnou spadnou.
Lepší návrh by bylo pole značka+hodnota předem neznámé velikosti, ukončené nulovým záznamem.
uvař_kafe(NULL)
by uvařilo nějaké obyčejné standardní kafe, a přidáním položek do pole by se daly upravovat hodnoty.
No a časem by se ukázalo, že je problém implementovat přípravu kakaa. A tak by buď vznikl tag KAFE_BEZ_KAFE
, nebo nové volání uvař_kakao()
Zrovna nedávno jsem na podobný problém v kernelu narazil: V poli pro hardwarové hodiny chybí položka pro časovou zónu, kterou mají nové BIOSy. A bude asi nutné vytvořit novou strukturu, a nová volání, protože tomu starému to není jako předat, nepočítáme-li odporné obezličky.
uvař_kafe(int with_milk, int with_sugar, int more_water);
nebo zavést funkci uvař_kafe2
nebo uvař_kafe3
či dělat různé obskurnosti jako nacpat argument množství vody do horních bitů with_milk;
Proto se pídím po tom, proč právě pří vývoji jádra, kde jde o maximální udržitelnost po dlouhý čas bez rozbití stávajícího API se nějaký návrhový vzor při přidávání nové funkcionality neustálil jako standard. Ze čtení jaderných novin mám dojem, že problém "jak něco opravit / upravit / změnit a nerozbít stávající API" je poměrně častý.
na strukturu, ktera bude verzovanaTo už mi přijde daleko jednodušší současný systém, který verzuje celé volání. Případně se to dá do budoucna schovat za nějakou abstrakci v libc, kde nejsou takové extrémní nároky na kompatibilitu, nebo se to zabalí do abstrakce o krok dál.
ioctl()
bylo příliš šílené a ne dost flexibilní.
Abychom to nepřeháněli, začínalo se na 80386, sice se později objevily i pokusy portovat Linux i na starší verze, ale to byly spíš takové experimenty a IIRC to k ničemu kloudnému nevedlo.
C sice podporuje funkce s proměnným počtem parametrů, ale kdo s tím někdy pracoval, dospěl nejspíš k závěru, že lze-li se tomu vyhnout, je rozhodně lepší tak učinit. Aspoň já ano.
A předávat všechno přes pointer na strukturu? Jde-li o komplikovanější data, jako třeba u sigaction()
, pak jistě. Ale představa, že bych místo
L = read(fd, buf, len);
měl pokaždé psát
struct read_params par; ... par.fd = fd; par.buf = buf; par.count = len; L = read(&par);
(s tím, že nemám-li C99, musí být první řádek na začátku bloku) mi rozhodně nepřipadá jako krok správným směrem. A i pro debugování je lepší, když u jednodušších syscallů (a těch je většina) najdu parametry v registrech a nemusím je dohledávat na zásobníku nebo dokonce v paměti, kterou nemusím ani mít k dispozici.
Jinak řečeno, je vždycky nepříjemné, když se ukáže, že člověk nebyl dostatečně prozíravý. Ale být prozíravý až příliš také není ideální.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.