shared object vs. static vs. forrasmodulok

 ( apal | 2010. október 18., hétfő - 21:34 )

ha csinalunk egy *.so-t, tobb forrasmodulbol (*.o), es egy program tobb ilyen *.so-t tolt be, akkor hogyan erdemes a shared lib modulok kozos api konyvtarait csinalni? egy olyan felallas, hogy:

modul1.so: modul1.o modul_kozos.o
modul2.so: modul2.o modul_kozos.o

program.c:
...
dlopen("modul1.so",...)
dlopen("modul2.so",...)
...

ez mennyire okoz(hat) utkozeseket? a mod_common.o-ban levo" fuggvenyek/szimbolumok olyanertelemben "statikusak", hogy csak a modul[12].o hivja be oket (es kesobb dlsym()-mel sem akarjuk, meg veletlenul sem ezeket), de static-kent nyilvan nem deklaralhatoak. egy RTLD_NOW|RTLD_LOCAL kombinacio elegendo"-e a dlopen()-nek?

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

erdekes kerdes, visibility attribute-tel nem lehet valamit jatszani?

--
NetBSD - Simplicity is prerequisite for reliability

En fognam es csinalnek egy shared libet a kozosbol, es hozzalinkelnem a modulokat. Problem solved. (Az mas kerdes, hogy ez meg mas problemakat vet fel, de azokat szerintem egyszerubb orvosolni :)

Ez az oldal leírja, hogy hogyan történik a feloldás.

Ha jól értem, arról van szó, hogy minden .so, illetve a main process image, gyökerét alkotja egy-egy függőségi fának. Például legyen ez a helyzet:

          img                    f.so               k.so
         /   \                  /    \                | 
     a.so.1   b.so.1          g.so.1  h.so.1        l.so.1
    /      \        \                 /    \       /     \
   c.so.1  d.so.1   e.so.1         i.so   j.so  m.so.1    n.so.1

(Az elnevezésekkel eleve csaltam egy kicsit, hogy tükrözzék mind a dependency order-t, mind a load order-t; lásd lejjebb.)

Az "img" a bináris. Az alatta lévő fa azok a shared lib-ek, amelyektől a bináris függ. Az "f.so" és a "k.so" dlopen()-nel van megnyitva, az alattuk lévő so-k pedig azok, amelyektől az említett plugin-ok a normális link folyamat miatt függenek.

Namármost, a dlopen() speckója szerint (ha jól értem), kétféle rendezés van, load ordering és dependency ordering. A dependency ordering az "egyszerűbb", olyan értelemben, hogy az általa felállított sorrend beépül a load ordering-be.

A dependency ordering csak egyetlen (teljes) fára értelmes a fenti erdőből, míg a load ordering a teljes erdőre. A dependency ordering az adott fa szélességi bejárását jelenti (lásd az elnevezéseket a fenti példában). Így bármely adott fából előáll egy szekvencia (a "szélességi front".) A load ordering pedig ezeknek a szélességi front-oknak az egymás után fűzését jelenti balról jobbra, ahogy a fákat egymás után raktuk balról jobbra (vagyis a dlopen()-ek meghívási sorrendjében). Így a load ordering egy teljes rendezés (lásd a fenti példában a globális ábécé sorrendet).

Na, akkor mit mire használunk? A dependency ordering-et két dologra:

  1. Amikor dlsym()-mel egy konkrét ojjektumban (és az általa behúzott függőségekben) keresünk szimbólumot, akkor a dependency ordering játszik. Például az f.so és a k.so megnyitásakor visszakaptunk 1-1 handle-t; amikor a dlsym()-mel ezek valamelyikére hivatkozunk, akkor a keresés az adott fára korlátozódik, és ott szélességi bejárással történik.
  2. A dependency ordering-et akkor használjuk, amikor megállapítjuk, hogy egy adott .so által behúzott fa milyen sorrendben épül be a load ordering-be (lásd fentebb).

A load ordering-et három dologra használjuk:

  1. Amikor bármilyen szimbólumot megpróbálunk feloldatni a rendszerrel (relokáció), akkor a load ordering játszik. Tehát például amikor lazy módon betöltött so (-fa) egy függvényét akarjuk meghívni, akkor annak a függőségei load order szerint oldódnak fel. Illetve az RTLD_NOW módon betöltött so (-fa) hivatkozásainak feloldása is load order szerint történik (azonnal). Fontos, hogy a betöltéskor a dependency order szerint a fa front-ja először beépül a load ordering legvégére (teljesen), majd a relokáció ezután történik meg a friss load order szerint. Tehát simán lehet, hogy a kézzel betöltött .so relokációja nem is a saját szimbólumaira fog támaszkodni.
  2. Amikor dlsym()-mel a "globális ojjektumban" keresünk. A globális objektum-ra handle-t a dlopen(0, ...) ad vissza; vagyis ez pont arra jó, hogy a load order szerinti legelső definícióját megtaláljuk a szimbólumnak.
  3. Amikor a dlsym()-mel az RTLD_NEXT handle alatt keresünk -- ekkor a keresés a load order-nek (a teljes szekvenciának) abban a részében történik, amely azon .so után esik, amely az éppen végrehajtott dlsym(RTLD_NEXT, ...) függvényhívást tartalmazza.

Az RTLD_LOCAL arra jó, hogy az adott, egy db dlopen() során berántott .so fa ne kerüljön bele a load order-be (legalábbis ennek a konkrét dlopen() hívásnak a hatására).

Na akkor. A közös modult beleteheted statikusan a binárisba (file scope-pal és external linkage-dzsel). Ekkor minden később betöltött modul relokációjában közre fog működni. (gcc alatt ehhez még az -rdynamic kapcsolót is meg kell adni a bináris linkelésekor.)

A közös modulból csinálhatsz egy .so-t, ahogy algernon javasolta, és a modul1 / modul2 .so-k ettől függenének (normálisan, ldd-vel lekérdezhető módon). Ebben az esetben a közös cucc csak egyszer fog diszket és address space-t foglalni. Innentől kezdve mindegy, hogy RTLD_LOCAL-t vagy RTLD_GLOBAL-t használsz bármelyik modul dlopen()-jénél. Ha mindkettő dlopen()-nél RTLD_LOCAL-t adsz meg, akkor mindegyik modul a "saját" közös .so-jában fog relokálni -- azonban az ugyanaz a címtér lesz (elvileg...).

Ha a közös modult a fenti módon mindkét modul .so-ba statikusan belelinkeled, akkor dupla diszket és dupla címteret fogsz foglalni. (Mivel ezek text lapok, azért ezeket a kernel el tudja felejteni és szükség esetén újra be tudja tölteni, de a címteret akkor is duplán foglalja.) Itt már van különbség az RTLD_LOCAL és az RTLD_GLOBAL között. Ha mindkét dlopen() RTLD_LOCAL, akkor mindegyik modul a saját statikus common példányában fog keresni -- ezek azonban azonosak lesznek (mivel értelmesen build-elted a modulokat), bár külön címtartományban fognak élni. Ha azonban az első dlopen()-nél RTLD_GLOBAL-t adtál meg, akkor a második modul a common-ra való hivatkozásokat az elsőből fogja feloldani! Ugyanis a load order-ben az első modul előbb fog szerepelni, és a relokáció load order szerint történik. Ez persze megint nem fog problémát okozni, mivel értelmesen build-elted a modulokat.

A gyakorlati szempontból legtisztább ügy szerintem az, amit algernon javasolt -- tedd ki a közöst egy külön .so-ba. Ezzel diszket és címteret spórolsz. Emellett használd az RTLD_LOCAL-t, ami gyorsítani fogja a feloldást. Az RTLD_NOW nem szerintem nem szükséges (és ezért teljesítmény szempontjából kerülendő), ugyanis a modul[12].so függőségeit már a linker ellenőrizte (és rögzítette is), amikor megadtad neki a common.so-t. Vagyis common.so és simán RTLD_LOCAL. (A címtérrel spórolást azért jó lenne RTLD_LOCAL esetén ellenőrizni, lásd /proc/pid/maps.)

Az -rdynamic is működhetne, ettől azonban (1) ötvenszeresére nőne a binárisod mérete, (2) minden relokáció lelassulna jó eséllyel, (3) a link time-od megsokszorozódna, (4) ha nem töltesz be semmilyen modult, akkor feleslegesen foglalnak címteret a binárisban a common függvények/változók.

Az LD_PRELOAD környezeti változónak az a trükkje, hogy az így betöltött so (-fák) az img (a bináris) elé fognak bekerülni. Így lehet pl. a malloc()-ot felüldefiniálni, ugyanis a relokációnál az LD_PRELOAD-olt saját malloc()-unk fog a load order legelején állni. Nevezett malloc()-unk kódjában pedig (amely a preload-olt .so-ban tanyázik) az RTLD_NEXT-et használjuk az "igazi" malloc() megkeresésére, így a dlsym() a load order-nek a hívó .so utáni szakaszában fog keresni.

... Erre a litániára legalább egy ack-ot kérek, mert semmi sem idegesít jobban, mint amikor egy órát olvasok/gépelek, aztán annyit sem érkezik, hogy bikkmakk. (Többször előfordult velem, bár sosem apal részéről.) Legalább azt tudjam, hogy elolvasta a kérdező. (Nem kell vele egyetérteni, sőt, ha korrekció érkezik, annál jobb, mert úgy én is tanulok.)

A linkelt speckót persze mindenki rágja át maga, a fenti csak az én értelmezésem.

Ez szép lett nagyon, köszönjük.

Fuszenecker Róbert

+ack. koszonom, tok jo. ezt szerintem ki is fogom nyomtatni ;)

igen, ld_preload-dal is jatszok/jatszottam ma's projektek kapcsan is. az sem haszontalan, hogy az ember tudja hogy igazan pontosan hogy mukodik ez az egesz.

a.

A "csináljunk-e külön .so-t a legnagyobb közös osztóból" c. kérdésnél, azaz hogy ha a két .so ugyanazt a .o-t tartalmazza, akkor azzal mit kezdjünk, lényeges végiggondolni, hogy mit csinál a .o kódja, ha két példányban lesz benne ugyanaz a végleges programban.
Ha "csak" függvények vannak benne, amik nem tartalmaznak hivatkozást globális (pl. singleton) változókra, akkor ez pusztán méret és (a cache miatt) futási sebesség kérdése.
Ha azonban vannak ott R/W globális változók, akkor alapból azok is megduplázódnak, és az egyik kódpéldányban levő hivatkozások a hozzájuk tartozó globális változókat fogják használni, míg a másik példány meg a sajátjait. Ebből lehetnek zűrök: amit az egyik kódpéldány a globális változójába beír, azt a másik kódpéldány nem fogja látni, hiszen ugyanazon a néven a másik kódnál a másik változót fogja nézni. Ez utóbbi esetre az a megoldás, ha külön .so készül a közös kódrészletből, hogy az csak egy példányban kerüljön be a programba.

Ha "csak" függvények vannak benne, amik nem tartalmaznak hivatkozást globális (pl. singleton) változókra, akkor ez pusztán méret és (a cache miatt) futási sebesség kérdése.
igen, jogos a felteves, de ezek csak seged-fuggvenyek, raadasul elegge vegyesfelvagott (ui/cli-khez kiegeszites, par szamitas, ami epp kell). de mindez teljesen reentrans modon, ugyhogy effele problema nem nagyon merul fel.

igen, koszi megegyszer, azota jol atragtam magam ezen (csak tegnap napkozben nem volt erre epp ido). tenyleg nagyon hasznos. wiki-re ki kellene tenni ;]