Portál AbcLinuxu, 26. dubna 2024 06:48

Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky

11. 3. 2014 | Luboš Doležel
Články - Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky  

Aktuální verze jádra: 3.14-rc3. Citáty týdne: Josh Triplett, Greg Kroah-Hartman, Torvald Riegel. POSIXové zámky specifické pro soubor: Problémy s POSIXovými zámky; POSIXové zámky specifické pro soubor; Používání POSIXových zámků specifických pro soubor; Používání zámků specifických pro soubor spolu s vlákny; Závěr.

Obsah

Aktuální verze jádra: 3.14-rc3

link

Aktuální vývojová verze jádra je 3.14-rc3 vydaná 16. února. Linus začíná být nevrlý: Když jsem psal oznámení rc2, tak jsem zmiňoval jak je malé. Také jsem zmiňoval, jak vám nevěřím a že mám podezření, že se někteří potají smějí a se svými žádostmi o přetažení vyčkávají, zlé malé potvory, jako jste právě vy. A nesnáším, když mám pravdu.

Stabilní aktualizace: verze 3.13.3, 3.12.11, 3.10.30 a 3.4.80 vyšly 13. února. Aktualizace 3.13.4, 3.12.12, 3.10.31 a 3.4.81 se právě revidují; jejich vydání můžeme očekávat 20. února nebo později.

Citáty týdne: Josh Triplett, Greg Kroah-Hartman, Torvald Riegel

link

Možná by stálo za to to probrat s lidmi od GCC, abychom přišli na to, jestli nám něco neuniká. (Při méně zjevných hodnotách toho „zjevného“.)

-- Josh Triplett

Proč můj strom? Cožpak já spravuji drivers/memory/?

/me hledá v gitu...

Aha, vypadá to, že možná jo...

Ok, jdu to zkouknout.

-- Greg Kroah-Hartman

Pokud nechcete, aby bylo překážkou, že váš zdravý rozum se liší od zdravého rozumu jiných (např. autorů kompilátorů), měli byste usilovat o to, aby specifikace byly přesné a říkaly to, co chcete. Jinými slovy, uvažujte o tom tak, že specifikace je program a lidé jsou počítače; v tomto případě je žádoucí, abyste měli dobře definovaný program.

-- Torvald Riegel

POSIXové zámky specifické pro soubor

link

Zámky souborů jsou intenzivně využívány aplikacemi, jako jsou databázové a souborové servery. Kdykoliv vícero programů přistupuje k souborům současně, je zde riziko poškození dat nebo jiných chyb, pokud přístup není důsledně synchronizován. Zámky souborů tento problém řeší, ale stávající implementace může být obtížné používat, hlavně u vícevlákenných programů. POSIXové zámky specifické pro soubor jsou snahou, jak zkombinovat POSIXové a BSD zámky do podoby API přátelštějšího k používání vláken.

Vícero zapisujících při snaze změnit stejný soubor si může vzájemně rušit své změny. Navíc může být nutné změnu v souboru provést na více místech. Pokud jiné vlákno vidí jen část změn, pak to může vést k vyvolání chyb v programu.

Zámky souborů jsou obecně dostupné ve dvou typech: zámky čtení (také nazývané sdílené) a zápisu (také známé jako výhradní). Pro danou čast souboru je možné vystavit vícero zámků pro čtení, ale zámek pro zápis může být jen jeden, a to jen pokud pro danou oblast není žádný zámek pro čtení nebo zápis. Zatímco na některých operačních systémech jsou zámky povinné, na unixových systémech jsou zpravidla jen jako doporučení (advisory). Takové zámky jsou jako semafory – fungují jen tehdy, když je všichni respektují.

Jeden z hlavních mechanismů pro zamykání souborů je určený standardem POSIX. POSIX nabízí zamykání libovolných rozsahů v souboru pro čtení nebo zápis. Bohužel má ale řadu problémů, kvůli kterým je toto API nevhodné k používání v moderních aplikacích.

Problémy s POSIXovými zámky

link

Kdykoliv se program pokusí získat zámek, tak je zámek buď přidělen, nebo zamítnut v závislosti na tom, jestli pro daný rozsah už existuje nějaký neslučitelný zámek. Pokud tam žádný neslučitelný zámek není, pak je zámek přidělen. Pokud tam je, pak je zamítnut.

Klasické POSIXové zámky od jednoho procesu nejsou nikdy v konfliktu s jiniými zámky od toho samého procesu. Pokud přijde požadavek na zámek, který by byl v konfliktu s jiným zámkem, jenž ten samý proces předtím vytvořil, pak jádro s požadavkem zachází jako se žádostí o úpravu stávajícího zámku. Proto jsou klasické POSIXové zámky nepoužitelné pro synchronizaci mezi vlákny v rámci stejného procesu. Vzhledem k převaze aplikací používajících vlákna v moderním softwaru jsou POSIXové zámky jako synchronizační mechanismus značně nepoužitelné.

Ještě horší je pak to, že podle standardu jsou všechny zámky zrušeny, pokud proces uzavře kterýkoliv popisovač odpovídající danému souboru, i když tyto zámky byly vytvořeny nad popisovačem, který je stále otevřený. Jde o drobnost, která většinu programátorů překvaví, jelikož si program musí hlídat, aby neuzavřel jakýkoliv popisovač, pokud jsou na příslušném souboru zámky, o něž by se přišlo.

Jestli k tomu může dojít, není lehké určit. Pokud program otevře dva různé pevné odkazy na stejný soubor, vytvoří zámek na jednom popisovači a zavře jiný, pak je tento zámek implicitně zrušen, i když je původní popisovač, kde byl zámek vytvořen, stále otevřený.

Toto je problém především pro aplikace používající složité knihovny pro přístup k souborům. Je běžné mít rutiny, které otevřou soubor, něco přečtou nebo zapíší a pak jej opět uzavřou, aniž by volající aplikace mohla tušit, že k tomu došlo. Pokud aplikace má náhodou na dotčeném souboru zámek, pak o něj může přijít, aniž by o tom věděla. Takové chování může vést k nenápadnému poškozování dat a ztrátě příčetnosti programátora. Jeremy Allison napsal výborný popis tohoto problému a toho, jak takto špatný standard vůbec vznikl (čtěte část nazvanou „First Implementation Past the Post“).

Máme tu ale konkurující (nebo doplňující) standard zamykání souborů, který má své původy v BSD Unixu. Tyto zámky (se kterými se pracuje pomocí volání flock()) mají rozumnější chování. Zatímco POSIXové zámky jsou vlastněné procesem, zámky BSD přísluší otevřenému souboru. Pokud proces otevře soubor dvakrát a pokusí se na obou popisovačích vytvořit výhradní zámek, pak u druhého z nich nebude zámek udělen. Proto se zámky BSD dají používat jako synchronizační mechanismus mezi vlákny, dokud vlákna mají své vlastní popisovače. Pozor na to, že vytvoření klonu popisovače přes dup() nestačí, jelikož je takto vytvořen odkaz na ten samý otevřený soubor.

Navíc jsou zámky BSD uvolněny jen tehdy, když je uzavřen poslední odkaz na otevřený soubor, nad kterým byly zámky vytvořeny. Proto pokud program otevře soubor, vytvoři zámek a následně použije dup() pro naklonování popisovače, tak k uvolnění zámku dojde automaticky až po uzavření obou popisovačů.

Jediným skutečným problémem u zámků BSD je to, že se týkají celého souboru. Oproti tomu POSIXové zámky mohou pracovat s libovolným rozsahem bajtů v souboru. Ačkoliv jsou zámky celých souborů užitečné (a řada aplikací zamyká celé soubory i s POSIXovými zámky), v mnoha případech nestačí. Aplikace jako databáze potřebují detailnější zamykání, aby bylo možné práci lépe paralelizovat.

POSIXové zámky specifické pro soubor

link

Dovolím si tvrdit, že to, co potřebujeme, je hybrid těchto dvou zámků – tedy zámek rozsahu bajtů, který má sémantiku zámků BSD při voláních fork() a close(). Navíc protože máme celou řadu programů, které používají „klasické“ POSIXové zámky, tyto nové zámky musejí brát ohled na klasické zámky, aby programy používající nové zámky s nimi dobře spolupracovaly.

Klasické POSIXové zámky se ovládají pomocí několika příkazů předávaných systémovému volání fcntl():

Tyto příkazy jsou doprovázeny ukazatelem na binární argument se struct flock, který vypadá následovně:

struct flock {
	short int l_type;   /* Typ zámku: F_RDLCK, F_WRLCK, or F_UNLCK.  */
	short int l_whence; /* Vůči čemu je `l_start' relativní (jako `lseek').  */
	off_t l_start;      /* Offset, na kterém zámek začíná.  */
	off_t l_len;        /* Velikost zamykané oblasit; nula vyjadřuje EOF.  */
	pid_t l_pid;        /* Proces držící zámek. (Jen u F_GETLK) */
};

Stejně tak i POSIXové zámky specifické pro soubor se používají pomocí podobných příkazů, jen mají příponu 'P':

Tyto nové příkazy by měly být těm, kteří používají klasické POSIXové zámky, velmi povědomé a berou ten samý argument struct flock. Jediným rozdílem mezi zámky specifickými pro soubor a klasickými POSIXovými zámky je jejich „vlastnictví“. Klasické POSIXové zámky jsou vlastněny procesem, kdežto POSIXové zámky specifické pro soubor jsou vlastněné otevřeným souborem.

Používání POSIXových zámků specifických pro soubor

link

Pro zpřístupnění definic zámků specifických pro soubor je nutné definovat makro _GNU_SOURCE, jelikož tyto zámky ještě nejsou součástí POSIXu. Používání nových zámků se od těch klasických moc neliší. V mnoha případech stačí jen vyměnit hodnotu povelu. Jsou tu ale drobné rozdíly.

Programátor je sváděn k tomu, aby si myslel, že nové zámky jsou „vlastněny“ popisovačem, ale technicky tomu tak není. Pokud je popisovač naklonován pomocí dup(), jádro jednoduše vytvoří dodatečný odkaz na otevřený soubor a přiřadí mu nový slot v tabulce popisovačů. Zámky vytvořené na naklonovaném popisovači nepovedou ke konfliktu k zámkům vytvořeným na původním popisovači. Jádro bude považovat takovou žádost jako požadavek na změnu stávajícího zámku. Navíc zámky specifické pro soubor budou automaticky uvolněny, jen pokud dojde k uzavření obou popisovačů, i když je možné zámek uvolnit ručně pomocí příkazu F_UNLCK.

Chování při fork() je velmi podobné. Při zavolání fork() vytvoří jádro dodatečný odkaz na každý otevřený soubor a přiřadí jej v novém procesu ke stejnému slotu v tabulce popisovačů. Zámky nastavené libovolným z procesů nepovedou ke vzájemným konfliktům a budou automaticky uvolněny, jen když oba procesy popisovač uzavřou.

Klasické zámky a zámky specifické pro soubor budou vzájemně vždy v konfliktu, i když jsou užívány stejným procesem nebo na stejném popisovači. Nepořepokládá se, že by je programy moc míchaly, ale vzhledem k problémům, jaké nedefinovaná chování způsobují, je vhodné to zmínit.

Co s F_GETLK?

F_GETLK se možná mělo jmenovat F_TESTLK. I když technicky dochází ke vrácení stavu uzamčení určitého rozsahu, jeho pravým účelem je zjistit, zda by daný zámek šlo nastavit, aniž by byl nastaven. Pokud je v daném rozsahu zámek, jenž by byl v konfliktu, pak jádro naplní struct flock informacemi o tomto zámku a nastaví l_pid na číslo procesu, kterému tento zámek patří.

Pole l_pid je pro zámky specifické pro soubor sporné, jelikož tyto zámky nejsou vlastněny procesem. Popisovač mohl být zděděn při fork(), takže hodnota l_pid je u konfliktních zámků poněkud nicněříkající. Přesto je u programů používajících klasické POSIXové zámky nutné do pole l_pid něco dát. Touto hodnotou je -1.

Tento precedens pochází z BSD. Na Linuxu fungují POSIXové a BSD zámky ve zcela odlišném jmenném prostoru. Na BSD ale pracují ve stejném, takže vzájemně vedou ke konfliktům. Pokud program drží zámek BSD na souboru a jiný proces vůči němu udělá F_GETLK, pak jádro BSD nastaví l_pid na -1. Protože přenositelné programy už takovéto chování musejí snášet, tak je používání stejného chování u specifických zámků rozumnou volbou.

Používání zámků specifických pro soubor spolu s vlákny

link

U moderních aplikací je běžné, že namísto forku používají vlákna. To je u klasických POSIXových zámků problém. Vztahují se na celý proces, proto nemohou být zámky vytvářené různými vlákny v rámci procesu v konfliktu.

Se zámky specifickými pro soubor je ale možné toto omezení obejít tak, že každému vláknu dáme vlastní otevřený soubor. Zde je příklad (v zájmu jednoduchosti je bez ošetřování chybových návratových hodnot):

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <pthread.h>

    #define FILENAME    "/tmp/foo"
    #define NUM_THREADS 3
    #define ITERATIONS  5

    void *
    thread_start(void *arg)
    {
        int i, fd, len;
        long tid = (long)arg;
        char buf[256];
        struct flock lck = {
            .l_whence = SEEK_SET,
            .l_start  = 0,
            .l_len    = 1,
        };

        fd = open(FILENAME, O_RDWR|O_CREAT, 0666);

        for (i = 0; i < ITERATIONS; i++) {
            lck.l_type = F_WRLCK;
            fcntl(fd, F_SETLKPW, &lck);

            len = sprintf(buf, "%d: tid=%ld fd=%d\n", i, tid, fd);

            lseek(fd, 0, SEEK_END);
            write(fd, buf, len);
            fsync(fd);

            lck.l_type = F_UNLCK;
            fcntl(fd, F_SETLKP, &lck);

            usleep(1);
        } 
        pthread_exit(NULL);
    }

    int
    main(int argc, char **argv)
    {
        long i;
        pthread_t threads[NUM_THREADS];

        truncate(FILENAME, 0);

        for (i = 0; i < NUM_THREADS; i++)
            pthread_create(&threads[i], NULL, thread_start, (void *)i);

        pthread_exit(NULL);
        return 0;
    }


Tato ukázka vytváří tři vlákna, každé z nich pětkrát připíše do souboru. Přístup k souboru je synchronizován pomocí zámků specifických pro soubor. Pokud předchozí program zkompilujeme a spustíme, pak v souboru /tmp/foo bude 15 řádků.

Pokud bychom ale nahradili příkazy F_SETLKP a F_SETLKPW jejich klasickými POSIXovými obdobami, pak by ztratily jakýkoliv účinek, jelikož by byly prováděny tím samým procesem. To vede k poškozování dat (chybějící řádky), jelikož některá vlákna se sejdou a přepíší si data.

Závěr

link

Nový typ zámků popisovaný v článku dokáže vyřešit problémy s klasickými POSIXovými zámky, ale programátoři, kteří je chtějí použít, by si měli dávat pozor na odlišnosti.

Vývojáři z několika projektů včetně Samba, NFS Ganesha, SQLite a OpenJDK vyjádřili svůj zájem o používání zámků specifických pro soubor, jelikož pomohou zjednodušit kód ve většině situací a pomohou odstranit problémy s poškozením dat, ke kterému může dojít po zavření souboru.

Podpora pro tento nový typ zámků je dostupná jako patch pro jádro s cílem patch zařadit do Linuxu 3.15. Hodit se může i patch do Glibc doplňující definice nutné pro používání tohoto typu zámků. Dobré je i to, že Austin Group (které dohlíží na POSIX) se myšlenka obecně líbí.

Odkazy a zdroje

Kernel coverage at LWN.net: February 20, 2014

Další články z této rubriky

Jaderné noviny – přehled za březen 2024
Jaderné noviny – přehled za únor 2024
Jaderné noviny – přehled za leden 2024
Jaderné noviny – přehled za prosinec 2023
Jaderné noviny – přehled za listopad 2023

Diskuse k tomuto článku

Ruža Becelin avatar 11.3.2014 08:01 Ruža Becelin | skóre: 40 | blog: RuzaBecelinBlog
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Odpovědět | Sbalit | Link | Blokovat | Admin
NFS Ganesga

NFS Ganesha
Nikola Ciprich avatar 11.3.2014 08:22 Nikola Ciprich | skóre: 23 | blog: NiX_blog | Palkovice
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Odpovědět | Sbalit | Link | Blokovat | Admin
nesmáším -> nesnáším ;-)

jinak díky za překlad.
Did you ever touch the starlight ? Dream for a thousand years? Have you ever seen the beauty Of a newborn century?
11.3.2014 11:36 Milan Roubal | skóre: 25
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
počkození -> poškození
11.3.2014 12:15 random
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
A v Joshově citátu: „… jestli nám něco zjevného neuniká.“
11.3.2014 22:31 mhepp | skóre: 22
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
ošetřování chových návratových hodnot

Ale jinak díky za prima počtení...
David Watzke avatar 12.3.2014 07:49 David Watzke | skóre: 74 | blog: Blog... | Praha
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Dík za opravy. Překlepy jsem opravil.
“Being honest may not get you a lot of friends but it’ll always get you the right ones” ―John Lennon
Ruža Becelin avatar 12.3.2014 09:11 Ruža Becelin | skóre: 40 | blog: RuzaBecelinBlog
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Tak jeste ta Ganesha ;-)
12.3.2014 09:13 Tomas
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Odpovědět | Sbalit | Link | Blokovat | Admin
Threads are evil. Avoid them.
12.3.2014 10:37 nikdo
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Computers are evil. Avoid them.
12.3.2014 22:30 biolog
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Ten problém s uvolněním obecně nastane, když už proces soubor otevřený má (např uživatelův rozeditovaný dokument) a zkusí jej z nějakého důvodu otevřít znova. To druhé otevření může klidně nastat z toho samého (i jediného) vlákna, např proces vyrábí náhledy dokumentů v adresáři. Vlákna s tím nesouvisí. (Jistě, s vlákny je všechno nebezpečnější.)
12.3.2014 22:40 ebik
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
DNS resolving (using standard libraries) blocks. Avoid it.
pavlix avatar 13.3.2014 07:58 pavlix | skóre: 54 | blog: pavlix
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Já už tu vlastně ani nejsem. Abclinuxu umřelo.
13.3.2014 10:59 nikdo
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Offtopic: to použití tagu <pre/> na odkazovaném webu (text Hi all, ...) dost znesnadňuje člověku čtení textu, když se nepostaráte ručně o zalámání řádků na nějakou rozumnou délku.
13.3.2014 11:01 nikdo
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Aha, až teď koukám, že je to z nějakého mailing listu, tam s tím asi nic nenaděláte :)
pavlix avatar 13.3.2014 12:05 pavlix | skóre: 54 | blog: pavlix
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
Tak. A popravdě ani nevím, jestli na tohle je v HTML něco rozumného.
Já už tu vlastně ani nejsem. Abclinuxu umřelo.
23.3.2014 02:36 retroslava | skóre: 9 | blog: TryCatch | Žižkoff
Rozbalit Rozbalit vše Re: Jaderné noviny – 20. 2. 2014: Problémy s POSIXovými zámky
CSS: white-space: pre-wrap;
Pozor! Jsem naprostý idiot. Co jsem napsal včera dnes už dávno neplatí. Zavazuji se, že budu diskutovat nezávazně.

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.