LINUX PROG RAM OZÁS v0.3pre 2002.12.02. 22:54
Bányász Gábor (
[email protected] ) Levendovszky Tihamér (
[email protected] ) Automatizálási és Alkalmazott Inf. tsz.
TARTALOMJEGYZÉK 2
Tartalomjegyzék LINUX PROGRAMOZÁS...................................................................................................................................1 1.
BEVEZETÉS .................................................................................................................................................6 1.1 A LINUX ................................................................................................................................................6 1.2 A SZABAD SZOFTVER ÉS A LINUX TÖRTÉNETE ................................................................................6 1.3 INFORMÁCIÓFORRÁSOK.......................................................................................................................8 1.3.1 Linux Documentation Project (LDP)..........................................................................................8 1.3.2 Linux Software Map (LSM) ..........................................................................................................9 1.3.3 További információforrások.........................................................................................................9
2.
A LINUX KERNEL RÉSZLETEI I. ................................................................................................... 10 2.1 M EMÓRIA MENEDZSMENT.................................................................................................................10 2.1.1 A Virtuális Memória modell...................................................................................................... 11 2.1.1.1 2.1.1.2 2.1.1.3 2.1.1.4 2.1.1.5
Igény szerinti lapozás ...........................................................................................................12 Swapping..............................................................................................................................13 Megosztott virtuális memória ...............................................................................................13 Fizikai és virtuális címzési módok .......................................................................................13 Hozzáférés vezérlés ..............................................................................................................13
2.1.2 Cache-ek ....................................................................................................................................... 14 2.2 PROCESSZEK........................................................................................................................................15 2.2.1 A Linux processzek...................................................................................................................... 16 2.2.2 Azonosítók .................................................................................................................................... 17 2.2.3 Ütemezés ....................................................................................................................................... 18 2.2.4 Processzek létrehozása............................................................................................................... 19 2.2.5 Ido és idozítok .............................................................................................................................. 20 2.2.6 A programok futtatása................................................................................................................ 20 3.
FEJLESZTOI ESZKÖZÖK.................................................................................................................. 22 3.1 3.1.1 3.1.2 3.1.3 3.1.4 3.2 3.2.1 3.2.2 3.3 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.4
4.
SZÖVEGSZERKESZTOK .......................................................................................................................22 Emacs ............................................................................................................................................ 22 vi..................................................................................................................................................... 22 pico ................................................................................................................................................ 22 joe................................................................................................................................................... 23 FORDÍTÓK ............................................................................................................................................24 GNU Compiler Collection......................................................................................................... 24 gcc.................................................................................................................................................. 25 M AKE...................................................................................................................................................28 Kommentek................................................................................................................................... 29 Explicit szabályok........................................................................................................................ 29 Változódefiníciók ......................................................................................................................... 30 Direktívák ..................................................................................................................................... 31 Implicit szabályok........................................................................................................................ 31 KDEVELOP ..........................................................................................................................................33
DEBUG ........................................................................................................................................................ 35 4.1 GDB .......................................................................................................................................................35 4.1.1 Példa.............................................................................................................................................. 35 4.1.2 A gdb indítása.............................................................................................................................. 36 4.1.3 Breakpoint, watchpoint, catchpoint ......................................................................................... 36 4.1.4 xxgdb ............................................................................................................................................. 38 4.1.5 DDD .............................................................................................................................................. 39 4.1.6 KDevelop internal debugger..................................................................................................... 39 4.2 M EMÓRIAKEZELÉSI HIBÁK................................................................................................................39 4.2.1 Electric Fence.............................................................................................................................. 41 4.2.1.1 4.2.1.2 4.2.1.3
Az Electric Fence használata................................................................................................41 Memory Alignment kapcsoló ...............................................................................................43 Az eléírás ..............................................................................................................................43
TARTALOMJEGYZÉK 3 4.2.1.4 4.2.1.5
4.2.2
További lehetoségek .............................................................................................................44 Eroforrás igények..................................................................................................................44
NJAMD (Not Just Another Malloc Debugger)....................................................................... 45
4.2.2.1 4.2.2.2 4.2.2.3 4.2.2.4
Használata.............................................................................................................................45 Memory leak detektálás ........................................................................................................47 Az NJAMD kapcsolói...........................................................................................................47 Összegzés..............................................................................................................................48
4.2.3 mpr................................................................................................................................................. 48 4.2.4 MemProf ....................................................................................................................................... 49 4.3 RENDSZERHÍVÁSOK MONITOROZÁSA : STRACE ..............................................................................50 4.4 TOVÁBBI HASZNOS SEGÉDESZKÖZÖK..............................................................................................50 5.
ÁLLOMÁNY ÉS I/O KEZELÉS .......................................................................................................... 52 5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.4 5.5 5.5.1 5.5.2 5.5.3 5.5.4 5.5.5 5.5.6 5.6 5.6.1 5.6.2 5.7 5.7.1 5.7.2 5.8 5.8.1 5.8.2 5.8.3
6.
EGYSZERU ÁLLOMÁNYKEZELÉS.......................................................................................................54 Az állományleíró ......................................................................................................................... 54 Hozzáférés állományleíró nélkül .............................................................................................. 54 Állományok megnyitása ............................................................................................................. 54 Állományok bezárása.................................................................................................................. 55 Írás, olvasás, állományban mozgás......................................................................................... 56 Részleges írás, olvasás............................................................................................................... 58 Állományok rövidítése................................................................................................................ 58 INODE INFORMÁCIÓK.........................................................................................................................59 Inode információk kiolvasása ................................................................................................... 59 Jogok lekérdezése........................................................................................................................ 60 Jogok állítása............................................................................................................................... 60 Tulajdonos és csoport állítás.................................................................................................... 60 Idobélyeg állítás.......................................................................................................................... 61 KÖNYVTÁR BEJEGYZÉSEK MÓDOSÍTÁSA ........................................................................................62 Eszköz állományok és Pipe bejegyzések .................................................................................. 62 Hard link létrehozása ................................................................................................................. 62 Szimbolikus link létrehozása ..................................................................................................... 63 Állományok törlése...................................................................................................................... 63 Állomány átnevezése................................................................................................................... 64 NÉV NÉLKÜLI PIPE-OK.......................................................................................................................64 KÖNYVTÁRMUVELETEK....................................................................................................................64 Munkakönyvtár............................................................................................................................ 64 Könyvtárváltás............................................................................................................................. 65 Root könyvtár módosítása.......................................................................................................... 65 Könyvtár létrehozása .................................................................................................................. 66 Könyvtár törlése.......................................................................................................................... 66 Könyvtártartalom olvasása ....................................................................................................... 66 I/O M ULTIPLEXING............................................................................................................................67 Nem blokkolt I/O......................................................................................................................... 68 Multiplexálás a select() függvénnyel....................................................................................... 70 LOCKOLÁS...........................................................................................................................................72 Lock állományok ......................................................................................................................... 72 Record lock................................................................................................................................... 73 SOROS PORT KEZELÉS........................................................................................................................76 Kanonikus feldolgozás................................................................................................................ 76 Nem kanonikus feldolgozás....................................................................................................... 78 Aszinkron kezelés......................................................................................................................... 80
KONKURENS PROGRAMOZÁS....................................................................................................... 81 6.1 FOLYAMATOK (PROCESSES) .............................................................................................................81 6.1.1 Jogosultságok, azonosítók és jellemzok................................................................................... 81 6.1.2 A folyamat állapotai................................................................................................................... 82 6.1.3 Folyamatok létrehozása és megszüntetése.............................................................................. 82 6.1.4 Folyamatok közötti kommunikáció (IPC)................................................................................ 86 6.1.4.1
6.1.5 6.1.6
Folyamatok szinkronizációja ................................................................................................87
Üzenetsorok (Message Queues)................................................................................................ 92 Megosztott memória (Shared Memory)................................................................................... 98
TARTALOMJEGYZÉK 4 6.1.7
Jelzések (Signals) ......................................................................................................................102
6.1.7.1 6.1.7.2 6.1.7.3 6.1.7.4 6.1.7.5 6.1.7.6 6.1.7.7
Jelzések küldése..................................................................................................................102 A sigset_t használata...........................................................................................................103 Jelzések lekezelése..............................................................................................................103 A jelzések maszkolása........................................................................................................104 Az aktív jelzések.................................................................................................................105 Jelzésre várakozás...............................................................................................................105 Jelzések listája ....................................................................................................................105
6.2 SZÁLAK ÉS SZINKRONIZÁCIÓJUK ...................................................................................................106 6.2.1 Szálak létrehozása.....................................................................................................................107 6.2.2 Kölcsönös kizárás (Mutex) ......................................................................................................112 6.2.3 Feltételes változók (Condition Variable) ..............................................................................113 6.2.4 Szemaforok .................................................................................................................................115 6.2.5 Core-dump mechanizmus adaptálása....................................................................................116 7.
KÖNYVTÁRAK FEJLESZTÉS E......................................................................................................118 7.1 7.2 7.3 7.3.1 7.3.2 7.3.3 7.4 7.5 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5
8.
BEVEZETÉS........................................................................................................................................118 STATIKUS PROGRAMKÖNYVTÁRAK...............................................................................................119 M EGOSZTOTT PROGRAMKÖNYVTÁRAK ........................................................................................119 Elnevezési szintek ......................................................................................................................120 Megosztott programkönyvtárak létrehozása ........................................................................120 Megosztott könyvtárak betöltése.............................................................................................121 DINAMIKUSAN BETÖLTÖTT PROGRAMKÖNYVTÁRAK .................................................................121 PÉLDÁK PROGRAMKÖNYVTÁRAKRA .............................................................................................122 Egy egyszeru programkönyvtár..............................................................................................122 Statikus felhasználás.................................................................................................................123 Megosztott programkönyvtár fordítása.................................................................................123 Dinamikus könyvtárhasználat.................................................................................................124 Dinamikus script.......................................................................................................................125
HÁLÓZATI KOMMUNIKÁCIÓ ......................................................................................................126 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.1.5 8.2 8.2.1 8.2.2 8.2.3 8.2.4 8.3 8.3.1 8.3.2 8.3.3 8.3.4 8.3.5 8.3.6 8.3.7 8.3.8 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.4.6 8.4.7 8.4.8
EGYSZERU SOCKET KEZELÉS..........................................................................................................126 Socketek létrehozása .................................................................................................................126 A kapcsolat felépítése...............................................................................................................127 A socket címhez kötése.............................................................................................................128 Várakozás a kapcsolódásra.....................................................................................................128 A szerverhez kapcsolódás........................................................................................................128 UNIX DOMAIN SOCKET.....................................................................................................................129 Unix domain címek ...................................................................................................................130 Szerver applikáció.....................................................................................................................130 Kliens applikáció.......................................................................................................................132 Névtelen Unix domain socket..................................................................................................133 TCP/IP ...............................................................................................................................................133 A hardverfüggo különbségek feloldása .................................................................................133 Címzés .........................................................................................................................................134 A socket cím megadása ............................................................................................................134 Név- és címfeloldás...................................................................................................................136 Portok ..........................................................................................................................................139 Kommunikáció...........................................................................................................................140 Szerver applikáció.....................................................................................................................141 Kliens applikáció.......................................................................................................................141 TÁVOLI ELJÁRÁSHÍVÁS (RPC) .......................................................................................................141 Az RPC modell...........................................................................................................................141 Verziók és számok .....................................................................................................................142 Portmap ......................................................................................................................................142 Transzport ..................................................................................................................................143 XDR .............................................................................................................................................143 rcpinfo .........................................................................................................................................143 rpcgen..........................................................................................................................................143 Helyi eljárás átalakítása távoli eljárássá .............................................................................144
TARTALOMJEGYZÉK 5 9.
X WINDOW, KDE .................................................................................................................................148 9.1 X W INDOW ........................................................................................................................................148 9.1.1 X Window architektúra.............................................................................................................148 9.1.2 Window manager.......................................................................................................................148 9.1.3 Kliens applikációk.....................................................................................................................149 9.1.4 Desktop Environment...............................................................................................................149 9.2 KDE ...................................................................................................................................................150
10.
KDE FEJLESZTÉS ................................................................................................................................151
10.1 HELLO W ORLD .................................................................................................................................151 10.2 KDE PROGRAM STRUKTÚRA ..........................................................................................................152 10.2.1 Egyszeru applikáció............................................................................................................153 10.3 A SIGNAL-SLOT MODELL .................................................................................................................157 10.3.1 Slot létrehozása....................................................................................................................158 10.3.2 Signal küldése.......................................................................................................................158 10.3.3 Signal és slot összekapcsolása...........................................................................................158 10.3.4 Signal-slot metódus paraméterekkel.................................................................................160 10.3.5 Slot átmeneti objektumokban.............................................................................................160 10.4 M ETA OBJECT COMPILER (MOC) ...................................................................................................161 10.5 EGYSZERU SZÁRMAZTATOTT WIDGET...........................................................................................161 10.5.1 A QWidget.............................................................................................................................162 10.5.2 Signal-slot kezelés................................................................................................................162 10.5.3 A widget megrajzolása........................................................................................................162 10.5.3.1 10.5.3.2
A rajzolás kiváltása.............................................................................................................163 A rajzolás ............................................................................................................................163
10.5.4 A felhasználói esemény kezelése .......................................................................................163 10.6 DIALÓGUS ABLAKOK .......................................................................................................................164 10.6.1 Standard dialógus ablakok.................................................................................................164 10.6.1.1 10.6.1.2 10.6.1.3 10.6.1.4 10.6.1.5 10.6.1.6
10.6.2 10.6.2.1 10.6.2.2
10.6.3 10.6.3.1 10.6.3.2 10.6.3.3
KFileDialog........................................................................................................................164 KFontDialog .......................................................................................................................165 KColorDialog.....................................................................................................................165 KMessageBox.....................................................................................................................166 Példa program .....................................................................................................................166 További dialógus ablakok...................................................................................................167
Egyéni dialógus ablakok.....................................................................................................168 Fix elhelyezés .....................................................................................................................168 Dinamikus elhelyezés .........................................................................................................168
Modalitás – modális és nem modális dialógus ablakok ................................................169 Modális dialógus ablak .......................................................................................................169 Nem modális dialógus ablak...............................................................................................170 Nem modális dialógus ablak megsemmisítése...................................................................171
10.6.4 Dialógus ablak tervezési szabályok ..................................................................................173 10.7 DIALÓGUS ALAPÚ APPLIKÁCIÓ.......................................................................................................173 10.8 KONFIGURÁCIÓS ÁLLOMÁNYOK ....................................................................................................174 10.9 A DOKUMENTUM /NÉZET A RCHITEKTÚRA...................................................................................174 10.9.1 A Dokumentum/Nézet Architektúra alapkoncepciói......................................................174 10.9.2 A Dokumentum/Nézet architektúra a KDevelop által generált SDI kódban .............175 10.9.2.1 10.9.2.2 10.9.2.3
10.9.3
Az Alkalmazás szerepe.......................................................................................................176 A Dokumentum feladatai....................................................................................................178 A Nézet funkciói.................................................................................................................182
A Dokumentum/Nézet architektúra a KDevelop által generált MDI kódban............186
BEVEZETÉS 6
1. Bevez etés 1.1 A Linux A Linux szónak több jelentése is van. A technikailag pontos definíciója: A Linux egy szabadon terjesztheto, Unix-szeru operációs rendszer kernel. Azonban a legtöbb ember a Linux szó hallatán az egész operációs rendszerre gondol, amely a Linux kernelen alapul. Így általában az alábbi értelemben használjuk: A Linux egy szabadon terjesztheto, Unix-szeru operációs rendszer, amely tartalmazza a kernelt, a rendszer eszközöket, programokat, és a teljes fejlesztoi környezetet. Mi is ezt a második értelmét használjuk. A Linux egy kiváló, ingyenes platformot ad a programok fejlesztéséhez. Az alapveto fejleszto eszközök a rendszer részét képezik. A Unix-szeruségébol adódóan, programjainkat könnyen portolhatjuk majdnem minden Unix és Unix-szeru rendszerre. További elonyei: ?? A teljes operációs rendszer forráskódja szabadon hozzáférheto, használható, vizsgálható és szükség esetén módosítható. ?? Ebbe a körbe bele tartozik a kernel is, így extrémebb problémák, amelyek a kernel módosítását igénylik, megoldására is lehetoségünk nyílik. ?? Mivel a stabil és a fejlesztoi kernel vonala jól elkülönül, ezért célrendszerek készítésénél szabadon választhatunk, hogy egy stabil, vagy a legújabb fejlesztéseket tartalmazó rendszerre van szükségünk. ?? A Linux fejlesztése nem profit orientált fejlesztok kezében van, így fejlodésekor csak technikai szempontok döntenek, marketinghatások nem befolyásolják. ?? A Linux felhasználók és fejlesztok tábora széles és lelkes. Ennek következtében az Interneten nagy mennyiségu segítség és dokumentáció lelheto fel. Az elonyei mellett meg kell természetesen említenünk hátrányait is. A decentralizált fejlesztés és a marketing hatások hiányából adódóan a Linux nem rendelkezik olyan egységes, felhasználó barát kezeloi felülettel, mint konkurensei. (Bele értendo ebbe a fejlesztoi eszközöket is.) Azonban ennek figyelembe vételével is a Linux kiváló lehetoségeket nyújt a fejlesztésekhez, elso sorban a nem desktop applikációk területén.
1.2 A szabad szoftver és a Linux története A számítástechnika hajnalán a cégek kis jelentoséget tulajdonítottak a szoftvereknek. Elso sorban a hardwaret szándékozták eladni, a szoftvereket csak járulékosan adták
BEVEZETÉS 7 hozzá, marketing jelentoséget nem tulajdonítottak neki. Ez azt eredményezte, hogy a forráskódok, algoritmusok szabadon terjedtek az akadémiai szférában. Azonban ez az idoszak nem tartott sokáig, a cégek hamar rájöttek a szoftverekben rejlo üzleti lehetoségekre, és ezzel beköszöntött a copyright korszaka. Az intellektuális eredményeket a cégek levédik, és szigorúan orzik. Ezek a változások nem nyerték el Richard Stallman (Massachusetts Institute of Technology, MIT) tetszését. Ennek hatására megalapította a Free Software Foundation (FSF) szervezetet Cambridge-ben. Az FSF célja szabadon fejlesztheto szoftverek fejlesztése. A kifejezésben a free nem ingyenességet, hanem szabadságot jelent. Hite szerint a szoftvereknek a hozzájuk tartozó dokumentációval, forrás kóddal együtt szabadon hozzáférhetonek és terjeszthetonek kell lenniük. Ennek elosegítésére megalkotta (némi segítséggel) a General Public License-t (GPL). A GPL három fo irányelve: 1. Mindenkinek, aki GPL-es szoftvert kap, megvan a joga, hogy ingyenesen tovább terjessze a forráskódját. (Leszámítva a terjesztési költségeket.) 2. Minden szoftver, amely GPL-es szoftverbol származik, szintén GPL-es kell, hogy legyen. 3. A GPL-es szoftver birtokosának megvan a joga, hogy szoftvereit olyan feltételekkel terjessze, amelyek nem állnak konfliktusban a GPL-el. A GPL egyik jellemzoje, hogy nem nyilatkozik az árról. Vagyis a GPL-es szoftverek tetszoleges áron eladhatóak a vevonek. Egyetlen kikötés, hogy a forrás kód ingyenesen jár a szoftverhez. Azonban a vevo szabadon terjesztheti a programot, és a forráskódját. Az Internet elterjedésével ez azt eredményezte, hogy a GPL-es szoftverek ára alacsony (sok esetben ingyenesek), de lehetoség nyílik ugyanakkor arra, hogy a termékhez kapcsolódó szolgáltatásokat, támogatást adjanak el. Az FSF által támogatott legfobb mozgalom a GNU’s Not Unix (röviden GNU) projekt, amelynek célja, hogy egy szabadon terjesztheto Unix-szeru operációs rendszert hozzon létre. A Linux története 1991-re nyúlik vissza. Linus Torvalds a Helsinki Egyetem diákja ekkor kezdett bele a projektbe. Eredetileg az Andrew S. Tanenbaum által tanulmányi célokra készített Minix operációs rendszerét használta gépén. A Minix az operációs rendszerek muködését, felépítését volt hivatott bemutatni, ezért egyszerunek, könnyen értelmezhetonek kellett maradnia. Ebbol következoen nem tudta kielégíteni Linus igényeit, ezért egy saját Unix-szeru operációs rendszer fejlesztésébe vágott bele. Eredetileg a Linux kernelt egy gyenge licensszel látta el, azonban ezt rövidesen GPLre cserélte. A GPL feltételei lehetové tették más fejlesztoknek is, hogy csatlakozzanak a projekthez, és segítsék munkáját.
BEVEZETÉS 8 A GNU C- library projektje lehetové tette applikációk fejlesztését is a rendszerre. A gcc, bash, Emacs programok portolt változatai gyorsan követték. Így az 1992-es év elején már aránylag könnyen installálható volt a Linux 0.95 a legtöbb Intel gépen. A Linux projekt már a kezdetektol szorosan összefonódott a GNU projekttel. A GNU projekt forráskódjai fontosak voltak a Linux közösség számára a rendszer felépítéséhez. A rendszer további jelentos részletei származnak a Kaliforniai Berkley egyetem nyílt Unix forráskódjaiból, illetve az X konzorciumtól. A Linux fejlodésével egyre többen foglalkoztak az installációt és a használatot megkönnyíto disztribúciók készítésével. A Slackware volt az elso olyan csomag, amelyet már komolyabb háttértudás nélkül is lehetett installálni és használni. Megszületése nagyban elosegítette a Linux terjedését, népszeruségének növekedését. A RedHat disztribúció viszonylag késon született a társaihoz képest, azonban napjaink egyik legnépszerubb változata. Céljuk egy stabil, biztonságos, könnyen installálható és használható csomag készítése. Továbbá termékeikhez támogatást, tanfolyamokat, könyveket is nyújtanak. Ennek következtében az üzleti Linux felhasználás egyik vezetojévé notték ki magukat. A Linux disztribúciók általában a Linux kernel mellett tartalmazzák a fejlesztoi könyvtárakat, fordítókat, értelmezoket, shell-eket, applikációkat, segéd programokat, konfigurációs eszközöket és még sok más komponenst. Napjainkban már sok Linux disztribúcióval találkozhatunk. Mindegyiknek megvannak az elonyei, hátrányai, hívoi. Azonban mindegyik ugyanazt a Linux kernelt és ugyanazokat a fejlesztoi alapkönyvtárakat tartalmazza.
1.3 Információforrások A Linux története során mindig is kötodött az Internethez. Az Internet közösség készítette, fejlesztette, terjesztette, így a dokumentációk is az Interneten találhatóak meg nagyrészt. Ezért a legfobb információforrásnak a hálózatot tekinthetjük.
1.3.1 Linux Documentation Project (LDP) A Linux világ egyik legjelentosebb információ forrása a Linux Documentation Project (LDP). Az LDP elsodleges feladata ingyenes, magas szintu dokumentációk fejlesztése a GNU/Linux operációs rendszer számára. Céljuk minden Linux-al kapcsolatos témakör letakarása a megfelelo dokumentumokkal. Ez magában foglalja a HOWTO-k és guide-ok készítését, a man oldalak, info, és egyéb dokumentumok összefogását. ?? A HOWTO-k egy-egy probléma megoldását nyújtják. Elérhetoek számos formátumban: HTML, PostScript, PDF, egyszeru text. ?? A guide kifejezés az LDP által készített könyveket takarja. Egy-egy témakör bovebb kifejtését tartalmazzák. ?? A man oldal az általános Unix formátum az elektronikus manuálok tárolására.
BEVEZETÉS 9 Az LDP által elkészített dokumentumok megtalálhatóak a következo címen: http://www.tldp.org/
1.3.2 Linux Software Map (LSM) Amennyiben problémánk akad egyes Linux-os programok megtalálásával, akkor segíthet a Linux Software Map (LSM). Ez egy nagy adatbázis, amely a Linux-os programok jellemzoit tartalmazza úgy, mint nevét, rövid leírását, verzióját, íróját, fellelhetoségét és licenszét. Címe: http://lsm.execpc.com/
1.3.3 További információforrások ?? ?? ?? ?? ?? ??
Linux system labs: http://www.lsl.com/ RedHat dokumentumok: http://www.redhat.com/docs/ Linoleum (a fejlesztésekhez): http://leapster.org/linoleum/ Linux.hu: http://www.linux.hu/ Newsgroup-ok, levelezési listák. A GPL-es szoftverek mindegyikének a forráskódja elérheto, így kérdéseinkre ott is megtalálhatjuk a választ. ?? És még számos hely található az Interneten, ahol a hiányzó információra rálelhetünk.
A LINUX K ERNEL RÉSZLETEI I. 10
2. A Linux Kernel részletei I. Ebben a fejezetben a Linux kernel egyes, általánosan a fejle sztok számára fontos részeivel ismerkedünk meg. Elsosorban a memória menedzsmenttel és a processzekkel kapcsolatos kérdéseket tárgyaljuk. A kernel további részeinek ismertetésére az egyes témakörök kapcsán kerül sor.
2.1 Memória menedzsment A memória menedzsment alrendszer az operációs rendszerek egyik legfontosabb eleme. A kezdetek óta gyakran felmerül a probléma, hogy több memóriára lenne szükségünk, mint amennyi a gépünkben fizikailag adott. Több stratégia is kialakult az idok során ennek megoldására. Az egyik legsikeresebb a virtuális memóriakezelés. A virtuális memória modellel úgy tunik, mintha több memória állna rendelkezésünkre az aktuálisnál azáltal, hogy szükség szerint osztja meg a versenyzo processzek között. De a virtuális memóriakezelés ennél többet is nyújt. A memória menedzsment alrendszer feladatai: Nagy címtartomány Az operációs rendszer a valódinál nagyobb memória területet mutat a rendszer felé. A virtuális memória a valódi többszöröse is lehet. Védelem Minden processz a rendszerben saját virtuális memória területtel rendelkezik. Ezek a címtartományok teljesen elkülönülnek egymástól, így az egyik applikációnak nem lehet hatása a másikra. Továbbá a hardware virtuális memória kezelo mechanizmusa lehetové teszi, hogy egyes memória területeket teljesen írásvédetté tehessünk. Ez megvédi a programok kód és adat területeit attól, hogy hibás programok beleírhassanak. Memória leképezés A memória leképezés lehetové teszi kép- és adatállományok leképezését a processz memória területére. A memória leképezés során az állományok tartalma közvetlenül bekapcsolódik a processz virtuális memória területére. Igazságos fizikai memória allokáció A memória menedzsment alrendszer lehetové teszi minden futó processz számára, hogy igazságosan részesedjen a fizikai memóriából. Megosztott virtuális memória Habár a virtuális memóriakezelés lehetové teszi a programok számára, hogy egymástól elválasztott címtartományokat kapjanak, idonként szükség van arra, hogy a processzek osztozzanak egy megosztott memória területen. Említhetjük az esetet, amikor több processz ugyanazt a kódot használja (például többen futtatják a bash shellt), ilyenkor ahelyett, hogy bemásolnánk a kódot minden virtuális memória területre sokkal célszerubb, ha csak egy
A LINUX K ERNEL RÉSZLETEI I. 11 példányban tartjuk a fizikai memóriában és a processzek osztoznak rajta. Hasonló az eset a dinamikus könyvtárakkal, ahol szintén a kódot megosztjuk a folyamatok között. A megosztott memória egy másik alkalmazási területe az Inter Process Communication (IPC). A processzek közötti kommunikáció egyik metódusa, amikor két vagy több folyamat a megosztott memórián keresztül cserél információkat. A Linux támogatja a Unix System V shared memory IPC metódusát.
2.1.1 A Virtuális Memória modell
Ábra 2-1 A virtuális memória leképezése fizikai memóriára
A Linux által használt virtuális memóriakezelés tárgyalásaként egy egyszerusített absztrakt modellt tekintünk át. (A Linux memóriakezelése ennél összetettebb, azonban az ismertetett elméleteken alapszik.) Amikor a processzor futtat egy programot, kiolvassa a hozzá tartozó parancsokat a memóriából és dekódolja azt. A dekódolás - futtatás során szükség szerint olvashatja és írhatja egyes memória területek tartalmát. Majd tovább lép a következo kód elemre. Ezen a módon a processzor folyamatosan parancsokat olvas és adatokat ír vagy olvas a memóriából. A virtuális memória használata esetén ezek a címek mind virtuális címek. Ezeket a virtuális címeket a processzor átkonvertálja fizikai címekre az operációs rendszer által karban tartott információs táblák alapján. Ennek a folyamatnak a megkönnyítésére a virtuális és a fizikai memória egyenlo, 4Kbyte-os (Intel x86) lapokra tagolódik. Minden lap rendelkezik egy saját egyedi azonosító számmal, page frame number (PFN).
A LINUX K ERNEL RÉSZLETEI I. 12 Ebben a modellben a virtuális cím két részbol tevodik össze. Egy ofszetbol és egy lapszámból. Ha a lapok mérete 4Kbyte, akkor az elso 12 bit adja az ofszetet, a felette levo bitek a lap számát. Minden alkalommal, amikor a processzor egy virtuális címet kap, szétválasztja ezeket a részeket, majd a virtuális lapszámot átkonvertálja fizikaira. Ezek után az ofszet segítségével már megtalálja a memóriában a kérdéses fizikai címet. A leképezéshez a processzor a lap táblákat (page tables) használja. A 2.1-es ábra a virtuális címterület használatát szemlélteti két processzes esetre. Mindkét folyamat saját lap táblával rendelkezik. Ezek a lap táblák az adott processz virtuális lapjait a memória fizikai lapjaira képezik le. Minden lap tábla bejegyzés az alábbi bejegyzéseket tartalmazza: ?? Az adott tábla bejegyzés érvényes-e. ?? A fizikai lapszám (PFN). ?? Hozzáférési információ. Az adott lap kezelésével kapcsolatos információk, írható-e, kód vagy adat. 2.1.1.1 Igény szerinti lapozás Ha a fizikai memória jóval kevesebb, mint a virtuális memória, akkor az operációs rendszer csak óvatosan adhat ki területeket, kerülve a fizikai memória ineffektív felhasználását. Az egyik metódus a takarékoskodásra, hogy csak azokat a virtuális lapokat töltjük be a memóriába, amelyeket a futó programok éppen használnak. Például egy adatbázis kezelo applikációnál elég, ha csak azokat az adatokat tartjuk a memóriában, amelyeket éppen kezelünk. Ezt a technikát, amikor csak a használt virtuális lapokat töltjük be a memóriába, igény szerinti lapozásnak hívjuk. Amikor a processz olyan virtuális címhez próbál hozzáférni, amely éppen nem található meg a memóriában, a processzor nem találja meg a lap tábla bejegyzését. Ilyenkor értesíti az operációs rendszert a problémáról. Ha a hivatkozott virtuális cím nem érvényes, az azt jelenti, hogy a processz olyan címre hivatkozott, amire nem lett volna szabad. Ilyenkor az operációs rendszer terminálja a folyamatot a többi védelmében. Ha a hivatkozott virtuális cím érvényes, de a lap nem található éppen a memóriában, akkor az operációs rendszernek be kell hoznia a háttértárolóról. Természetesen ez a folyamat eltart egy ideig, ezért a processzor addig egy másik folyamatot futtat tovább. A beolvasott lap közben beíródik a merevlemezrol a fizikai memóriába, és bekerül a megfelelo bejegyzés a lap táblába. Ezek után a folyamat a megállás helyétol fut tovább. Ilyenkor természetesen a processzor már el tudja végezni a leképezést, így folytatódhat a feldolgozás. A Linux az igény szerinti lapozás módszerét használja a folyamatok kódjának betöltésénél. Amikor a parancsokat lefuttatjuk, a kódot tartalmazó állományt megnyitja rendszer, és a tartalmát leképezi a folyamatok virtuális memória területére. Ezt hívjuk memória leképezésnek (memory mapping). Ilyenkor csak a kód eleje kerül be a fizikai memóriába, a többi marad a lemezen. Ahogy a kód fut és generálja a lap hibákat, úgy a Linux behozza a kód többi részét is.
A LINUX K ERNEL RÉSZLETEI I. 13 2.1.1.2 Swapping Amikor a folyamatnak újabb virtuális lapok behozására van szüksége, és nincs szabad hely a fizikai memóriában, olyankor az operációs rendszernek helyet kell csinálnia úgy, hogy egyes lapokat eltávolít. Ha az eltávolítandó lap kódot, vagy olyan adatrészeket tartalmaz, amelyek nem módosultak, olyankor nem szükséges a lapot lementeni. Ilyenkor egyszeruen kidobható, és legközelebb megtalálható az adott kód vagy adat állományban, amikor szükség lesz rá. Azonban ha a lap módosult, akkor az operációs rendszernek el kell tárolnia a tartalmát, hogy késobb elohozhassa. Ezeket a lapokat nevezzük dirty page-nek, és az állományt, ahova eltávolításkor mentodnek swap file-nak. A hozzáférés a swap állományhoz nagyon hosszú ideig tart a rendszer sebességéhez képest, ezért az operációs rendszernek az optimalizáció érdekében mérlegelnie kell. Ha a swap algoritmus nem elég effektív elofordulhat az, hogy egyes lapok folyamatosan swappelodnek, ezzel elpazarolva a rendszer idejét. Hogy ezt elkerüljük az algoritmusnak azokat a lapokat, amelyeken a folyamatok dolgoznak éppen, lehetoleg a fizikai memóriában kell tartania. Ezeket a lapokat hívjuk working set-nek. A Linux a Least Recently Used (LRU) lapozási technikát használja a lapok kiválasztására. Ebben a sémában a rendszer nyilvántartja minden laphoz, hogy hányszor fértek hozzá. Minél régebben fértek hozzá egy lapho z, annál inkább kerül rá a sor a következo swap muveletnél. 2.1.1.3 Megosztott virtuális memória A virtuális memóriakezelés lehetové teszi több folyamatnak, hogy egy memória területet megosszanak. Ehhez csak arra van szükség, hogy egy közös fizikai lapra való hivatkozást mindkét processz lap táblájába bejegyezzünk, ezáltal azt a lapot leképezve a virtuális címterületükre. Természetesen ugyanazt a lapot a két virtuális címtartományban két különbözo helyre is leképezhetjük. 2.1.1.4 Fizikai és virtuális címzési módok Nem sok értelme lenne, hogy maga az operációs rendszer a virtuális memóriában fusson. Ha belegondolunk, meglehetosen bonyolult szituáció volna, ha az operációs rendszernek a saját memória lapjait kellene kezelnie. A legtöbb multifunkciós processzor támogatja a fizikai címzést is a virtuális címzés mellett. A fizikai címzési mód nem igényel lap táblákat, a processzor nem végez semmilyen cím leképezést ebben a módban. A Linux kernel is természetesen ebben a fizikai címtartományban fut. (Az Alpha AXP processzor nem támogatja a fizikai címzési módot, ezért ezen a platformon más megoldásokat alkalmaznak a Linux kernelnél.) 2.1.1.5 Hozzáférés vezérlés A lap tábla bejegyzések az eddig tárgyaltak mellett hozzáférési információkat is tartalmaznak. Amikor a processzor egy bejegyzés alapján átalakítja a virtuális címeket
A LINUX K ERNEL RÉSZLETEI I. 14 fizikai címekké, párhuzamosan ellenorzi ezeket a hozzáférési információkat is, hogy az adott process számára a muvelet engedélyezett-e vagy sem. Több oka is lehet, amiért korlátozzuk a hozzáférést egyes memória területekhez. Egyes területek, mint például a program kód tárolására szolgáló memória rész, csak olvasható lehet, az operációs rendszernek meg kell akadályoznia, hogy a processz adatokat írhasson a kódjába. Ezzel szemben azoknak a lapoknak, amelyek adatokat tartalmaznak, írhatónak kell lenniük, de futtatni nem szabad a memória tartalmát. A futtatásra is a legtöbb processzor két módot támogat: kernel és user módot. Ennek oka, hogy nem akarjuk, hogy kernel kódot futtasson egy felhasználói program, vagy hogy a kernel adatstruktúrájához hozzáférhessen. Összességében jellemzoen az alábbi jogok szerepelnek egy lap tábla bejegyzésben: ?? Érvényesség ?? Futtathatóság ?? Írhatóság ?? Olvashatóság ?? Kernel módú program olvashatja-e a lapot. ?? User módú program olvashatja-e a lapot. ?? Kernel módú program írhatja-e a lapot. ?? User módú program írhatja-e a lapot. Továbbá a Linux által támogatott további jogok: ?? Page dirty: A lapot ki kell-e írni a swap állományba. ?? Page accessed: A laphoz hozzáfértek-e.
2.1.2 Cache-ek Az eddigi modell önmagában muködik, azonban nem elég effektív. A teljesítmény növelés egyik módszere cache-ek használata. A Linux több különbözo, memória menedzsmenthez kapcsolódó gyorsító tárat használ. Buffer Cache A buffer cache a blokkos eszközök adatait tartalmazza. Blokkos eszköz az összes merevlemez, így ez a cache lényegében lemezgyorsító tár. Page Cache Feladata hogy gyorsítsa a lemezen tárolt kódokhoz és adatokhoz való hozzáférést a lapok beolvasása során. Amikor a lapokat beolvassuk a memóriába, a tartalma a page cache-ben is eltárolódik. Swap Cache Csak a módosított (dirty) lapok kerülnek a swap állományba. Amikor egy lap nem módosult a legutolsó kiírása óta, olyankor nincs szükség arra, hogy újra kiírjuk a swap állományba. Ilyenkor egyszeruen kidobható. Ezzel a swap kezelése során idot takaríthatunk meg. Hardware Cache -ek Az egyik leggyakoribb hardware cache a processzorban van, és a lap tábla bejegyzések tárolására szolgál. Ennek segítségével a processzornak nem kell
A LINUX K ERNEL RÉSZLETEI I. 15 mindig közvetlenül a lap táblákat olvasnia, hozzáférhet a gyorsító tárból is az információhoz. Ezek a Translation Look-aside Buffer-ek egy vagy több processz lap tábla bejegyzéseinek másolatát tartalmazzák. Amikor egy virtuális címet kell lefordítania a processzornak, akkor eloször egy egyezo TLB bejegyzést keres. Ha talál, akkor segítségével azonnal lefordíthatja a fizikai címre. Ha nem talált megfelelot, akkor az operációs rendszerhez fordul segítségért. Ilyenkor az operációs rendszer létrehoz egy új TLB bejegyzést, amely megoldja a problémát, és a processzor folytathatja a munkát. Hátránya a cache-eknek hogy kezelésük plusz erofeszítéseket igényel (ido, memória), továbbá megsérülésük a rendszer összeomlásához vezet.
2.2 Processzek Ebben a fejezetben megtudjuk, hogy mi az a processz, és a Linux kernel hogyan hozza létre, menedzseli és törli a processzeket a rendszerbol. A processzek egyes feladatokat hajtanak végre az operációs rendszeren belül. A program gépi kódú utasítások, és adatok összessége, amelyeket a lemezeken tárolunk. Így önmagában egy passzív entitás. A processz ez a program muködés közben, amikor éppen fut. Ebbol látszik, hogy dinamikus entitás, folyamatosan változik, ahogy egymás után futtatja az egyes utasításokat a processzor. A program kódja és adatai mellett a processz tartalmazza a programszámlálót, a CPU regisztereket, továbbá a processz stack-jét, amely az átmeneti adatokat tartalmazza (függvény paraméterek, visszatérési címek, elmentett változók). A Linux egy multitaszkos operációs rendszer, a processzek szeparált taszkok saját jogokkal és feladatokkal, amelyeket a Linux párhuzamosan futtatni képes. Egy processz meghibásodása nem okozza a rendszer más processzeinek meghibásodását. Minden különálló processz a saját virtuális címtartományában fut és nem képes más processzekre hatni. Kivételt képeznek a biztonságos, kernel által menedzselt mechanizmusok. Élete során egy processz számos rendszer eroforrást használhat (CPU, memória, állományok, fizikai eszközök, stb.). A Linux feladata, hogy ezeket a hozzáféréseket könyvelje, menedzselje, és igazságosan elossza a konkuráló processzek között. A legértékesebb eroforrás a CPU. Általában egy áll rendelkezésre belole, de több CPU kezelésére is alkalmas a rendszer. A Linux egy multitaszkos rendszer, így egyik lényeges célja, hogy lehetoleg mindig mindegyik CPU-n fusson egy processz a leheto legjobb kihasználás érdekében. Amennyiben több a processzünk, mint a processzorunk (általában ez a helyzet), olyankor a processzeknek meg kell osztozniuk. A megoldás egyszeru: általában egy processz addig fut, amíg várakozásra kényszerül (általában egy rendszer eroforrásra), majd amikor azt megkapta folytathatja a futását. Amikor a processz várakozik, olyankor az operációs rendszer elveszi tole a CPU-t és átadja egy másik rászoruló processznek. Az ütemezo dönti el, hogy melyik processz legyen a következo. A Linux számos stratégiát használ az igazságos döntéshez.
A LINUX K ERNEL RÉSZLETEI I. 16
2.2.1 A Linux processzek A Linux menedzseli a processzeket a rendszerben. Minden processzhez hozzárendel egy leíró adat struktúrát, és bejegyez egy hivatkozást rá a taszk listájába. Ebbol következik, hogy a processzek maximális száma függ ennek a listának a méretétol, amely alapértelmezett esetben 512 bejegyzést tartalmazhat. A normál processzek mellett a Linux támogat real-time processzeket is. Ezeknek a processzeknek nagyon gyorsan kell reagálnia külso eseményekre, ezért a normál folyamatoktól külön kezeli oket az ütemezo. A taszkokat leíró adatstruktúra nagy és komplex, azonban felosztható néhány funkcionális területre: Állapo t A processz a futása során különbözo állapotokba kerülhet a körülmények függvényében: Running A processz fut, vagy futásra kész. Waiting A processz egy eseményre vagy egy eroforrásra vár. A Linux megkülönböztet megszakítható és nem megszakítható várakozásokat. A megszakítható várakozás esetén egy szignál megszakíthatja, míg nem megszakítható esetén valamilyen hardware eseményre vár és semmilyen körülmények között nem megszakítható a várakozás. Stopped A processz megállt, általában valamilyen szignál következtében. A debug-olás alatt lévo processzek lehetnek ilyen állapotban. Zombie Ez egy leállított processz, ami valami oknál fogva még mindig foglalja a leíró adat struktúráját. Ahogy a neve is mondja egy halott folyamat. Ütemezési információ Az ütemezonek szüksége van erre az információra, hogy igazságosan dönthessen, hogy melyik processz kerüljön sorra. Azonosítók Minden processznek a rendszerben van egy processz azonosítója. A processz azonosító nem egy index a processz táblában, hanem egy egyszeru szám. Továbbá minden processz rendelkezik egy felhasználó és egy csoportazonosítóval. Ezek szabályozzák az állományokhoz és az eszközökhöz való hozzáférést a rendszerben. Inter-Processz kommunikáció A Linux támogatja a klasszikus Unix IPC mechanizmusokat (szignál, pipe, szemafor) és a System V IPC mechanizmusokat is (megosztott memória, szemafor, üzenet lista). Ezekre késobb térünk ki. Kapcsolatok
A LINUX K ERNEL RÉSZLETEI I. 17 A Linuxban egyetlen processz sem független a többitol. Minden processznek, leszámítva az init processzt, van egy szüloje. Az új processzek nem létrejönnek, hanem a korábbiakból másolódnak, klóónozódnak. Minden processz leíró adatstruktúra tartalmaz hivatkozásokat a szülo processzre és a leszármazottakra. A pstree paranccsal megnézhetjük ezt a kapcsolódási fát. Ido és idozítok A kernel naplózza a processzek létrehozási idejét, a CPU felhasználásuk idejét. További a Linux támogatja intervallum idozítok használatát a processzekben, amelyeket beállítva szignálokat kaphatunk bizonyos ido elteltével. Ezek lehetnek egyszeri vagy periodikusan ismétlodo értesítések. File rendszer A processzek megnyithatnak és bezárhatnak állományokat. A processz leíró adatstruktúra tartalmazza az összes megnyitott állomány leíróját, továbbá a hivatkozást két VFS inode-ra. A VFS inode-ok egy állományt vagy egy könyvtárat írha tnak le egy file rendszeren. A két inode-ból az elso a processz home könyvtárát mutatja, a második az aktuális working könyvtárat. A VFS inode-oknak van egy számláló mezoje, amely számolja, hogy hány processz hivatkozik rá. Ez az oka, amiért nem törölhetünk olyan könyvtárakat, amelyeket egy processz használ. Virtuális memória A legtöbb processznek van valamennyi virtuális memóriája. Ez alól csak a kernel szálak és daemon-ok kivételek. A korábbiakban láthattuk, ahogy a Linux kernel ezt kezeli. Processzor specifikus adatok Amikor a processz fut, olyankor használja a processzor regisztereit, a stack-et, stb. Ez a processz környezete, és amikor taszkváltásra kerül sor, ezeket az adatokat le kell menteni a processz leíró adatstruktúrába. Amikor a processz újraindul innen állítódnak vissza.
2.2.2 Azonosítók A Linux, mint minden más Unix rendszer, felhasználói és csoportazonosítókat használ az állományok hozzáférési jogosultságának ellenorzésére. A Linux rendszerben minden állománynak van tulajdonosa és jogosultság beállításai. A lege gyszerubb jogok a read, write, és az execute. Ezeket rendeljük hozzá a felhasználók három csoportjához úgy, mint tulajdonos, csoport, és a rendszer többi felhasználója. A felhasználók mindhárom osztályára külön beállítható mindhárom jog. Természetesen ezek a jogok valójában nem a felhasználóra, hanem a felhasználó azonosítójával futó processzekre érvényesek. Ebbol következoen a processzek az alábbi azonosítókkal rendelkeznek: uid, gid A felhasználó user és a group azonosítói, amelyekkel a processz fut. effektív uid és gid
A LINUX K ERNEL RÉSZLETEI I. 18 Egyes programoknak meg kell változtatniuk az azonosítóikat a sajátjukra (amelyet a VFS inode-ok tárolnak), így nem a futtató processz jogait öröklik tovább. Ezeket a programokat nevezzük setuid-os programoknak. Segítségükkel korlátozott hozzáférést nyújthatunk a rendszer egyes védett részeihez. Az effektív uid és gid a program azonosítói, az uid és a gid marad az eredeti, a kernel pedig a jogosultság ellenorzésnél az effektív azonosítókat használja. file rendszer uid és gid Ezek normál esetben megegyeznek az effektív azonosítókkal, és a file rendszer hozzáférési jogosultságokat szabályozzák. Elsosorban az NFS filerendszereknél van rá szükség, amikor a user módú NFS szervernek különbözo állományokhoz kell hozzáférnie, mintha az adott felhasználó nevében. Ebben az esetben csak a file-rendszer azonosítók változnak. saved uid és gid Ezek az azonosítók a POSIX szabvány teljesítése érdekében lettek implementálva. Olyan programok használják, amelyek a processz azonosítóit rendszerhívások által megváltoztatják. A valódi uid és gid elmentésére szolgálnak, hogy késobb visszaállíthatóak legyenek.
2.2.3 Ütemezés Minden processz részben user módban, részben system módban fut. Az, hogy ezeket a módokat hogyan támogatja a hardware, eltéro lehet, azonban mindig van valami biztonságos mechanizmus arra, hogy hogyan kerülhet a processz user módból system módba és vissza. A user módban jóval kevesebb lehetosége van a processznek, mint system módban. Ezért minden alkalommal, amikor a processz egy rendszerhívást hajt végre, a user módból átkapcsol system módba és folytatja a futását. Ezen a ponton a kernel futtatja a processznek azt a részét. A Linuxban a processzeknek nincs elojoga az éppen futó processzekkel szemben, nem akadályozhatják meg a futását, hogy átvegyék a processzort. Azonban minden processz lemondhat a CPU-ról, amelyiken fut, ha éppen valami rendszer eseményre vár. Például ha egy processznek várnia kell egy karakterre. Ezek a várakoztatások a rendszerhívásokon belül vannak, amikor a processz éppen system módban fut. Ebben az esetben a várakozó processz felfüggesztodik, és egy másik futhat. A processzek rendszeresen használnak rendszerhívásokat, és gyakran kell várakozniuk. Azonban ha a processzt addig engedjük futni, amíg a következo várakozásra kényszerül, akkor az idonként két rendszerhívás között akár komoly idot is jelenthet. Ezért szükség van még egy megkötésre: egy processzt csak rövid ideig engedhetünk futni, és amikor ez letelik, akkor várakozó állapotba kerül, majd késobb folytathatja. Ezt a rövid idot nevezzük idoszeletnek (time-slice). Az ütemezo (scheduler) az, aminek a következo processzt ki kell választania a futásra készek közül. Futásra kész az a processz, amely már csak CPU-ra vár, hogy futhasson. A Linux egy egyszeru prioritás alapú ütemezo algoritmus használ a választáshoz.
A LINUX K ERNEL RÉSZLETEI I. 19 Az ütemezo számára a CPU ido elosztásához az alábbi információkat tárolja a rendszer minden processzhez: policy Az ütemezési metódus, amelyet a processzhez rendelünk. Két típusa van a Linux processzeknek: normál és valós ideju. A valós ideju processzeknek magasabb prioritásuk van, mint bármely más processznek. Ha egy valós ideju processz futásra kész, akkor mindig ot választja elsonek a rendszer. A normál processzeknél csak egy általános, idoosztásos metódust választhatunk. A real-time processzeknek azonban kétféle metódusuk lehet: round robin vagy first in, first out. A FIFO algoritmus egy egyszeru nem idoosztásos algoritmus és a statikusan beállított prioritási szint alapján választ a rendszer. A round robin a FIFO továbbfejlesztett változata, ahol a processz csak egy bizonyos ideig futhat és a prioritás módosításával minden futásra kész valós ideju processz lehetoséget. priority Ez a prioritás, amelyet az ütemezo a processznek ad. Módosítható rendszerhívásokkal és a renice paranccsal. rt_priority A Linuxban használatos real-time processzek számára egy relatív prioritást adhatunk meg. counter Az az ido, ameddig a processz futhat. Indulásképpen a prioritás értékre állítódik be, majd az óra ütésekre csökken. Az ütemezot több helyen is meghívja a kernel. Lefut, amikor az aktuális processz várakozó állapotba kerül, meghívódhat rendszerhívások végén, vagy amikor a processz visszatér user módba a system módból. Továbbá amikor a processz counter értéke eléri a nullát.
2.2.4 Processzek létrehozása A rendszer induláskor kernel módban fut, és csak egyetlen inicializáló processz létezik. Ez rendelkezik mindazokkal az adatokkal, amelyrol a többi processznél beszéltünk, és ugyanúgy egy leíró adatstruktúrában letároljuk, amikor a többi processz létrejön és fut. A rendszer inicializáció végén a processz elindít egy kernel szálat (neve init) és ezek után egy idle loop-ban kezd, és a továbbiakban már nincs szerepe. Amikor a rendszernek nincs semmi feladata az ütemezo ezt az idle processzt futtatja. Az init kernel szálnak, vagy processznek a processz azonosítója 1. Ez a rendszer elso igazi processze. Elvégez néhány beállítást (feléleszti a rendszer konzolt, mount-olja a root file rendszert), majd lefuttatja a rendszerinicializáló programot (nem keverendo a korábban említett processzel). Ez a program valamelyik a következok közül (az adott disztribúciótól függ): /etc/init, /bin/init, /sbin/init. Az init program az /etc/inittab konfigurációs állomány segítségével új processzeket hoz létre, és ezek további új processzeket. Például a getty processz létrehozza a login processzt, amikor a felhasználó bejelentkezik. Ezek a processzek mint az init kernel szál leszármazottai.
A LINUX K ERNEL RÉSZLETEI I. 20
Újabb processzeket létrehozhatunk egy régebbi processz klóónozásával, vagy ritkábban az aktuális processz klóónozásával. Az újabb folyamat a fork (processz) vagy a clone (thread) rendszerhívással hozható létre, és maga a klóónozás kernel módban történik. A rendszerhívás végén, pedig egy új processz kerül be a várakozási listába.
2.2.5 Ido és idozítok A kernel könyveli a processzek létrehozási idopontját és az életük során felhasznált CPU idot. Mértékegysége a jiffies. Minden óra ütésre frissíti a jiffies-ben mért idot, amit a processz a system és a user módban eltöltött. Továbbá ezek mellett a könyvelések mellett a Linux támogatja a processzek számára az intervallumidozítoket. A processz felhasználhatja ezeket az idozítoket, hogy különbözo szignálokat küldessen magának, amikor lejárnak. Három féle intervallumidozítot különböztetünk meg: Real A timer valós idoben dolgozik, és lejártakor egy SIGALRM szignált küld. Virtual A timer csak akkor muködik, amikor a processz fut, és lejártakor egy SIGVTALRM szignált küld. Profile A timer egyrészt a processz futási idejében muködik, másrészt, amikor a rendszer a processzhez tartozó muveleteket hajt végre. Lejártakor egy SIGPROF szignált küld. Elso sorban arra használják, hogy lemérjék az applikáció mennyi idot tölt a user és a kernel módban. Egy vagy akár több idozítot is használhatunk egyszerre. A Linux kezeli az összes szükséges információt hozzá a processz adatstruktúrájában. Rendszerhívásokkal konfigurálhatjuk, indíthatjuk, leállíthatjuk, és olvashatjuk oket.
2.2.6 A programok futtatása A Linuxban, mint a többi Unix rendszerben, a programokat és a parancsokat általában a parancsértelmezo futtatja. A parancsértelmezo egy felhasználói processz és a neve shell. A Linuxokban sok shell közül választhatunk (sh, bash, tcsh, stb.). A parancsok, néhány beépített parancs kivételével, általában bináris állományok. Ha egy parancs nevét beadjuk, akkor a shell végigjárja a keresési utakat, és egy futtatható állományt keres a megadott névvel. Ha me gtalálja, akkor betölti és lefuttatja. A shell klóónolja magát a fent említett fork metódussal és az új leszármazott processzben fut a megtalált állomány. Normál esetben a shell megvárja a gyerek processz futásának végét, és csak
A LINUX K ERNEL RÉSZLETEI I. 21 utána adja vissza a promptot. Azonban lehetoség van a processzt a háttérben is futtatni. A futtatható file többféle bináris vagy script állomány lehet. A script-et a megadott shell értelmezi. A bináris állományok kódot és adatot tartalmaznak, továbbá információkat a operációs rend szer számára, hogy futtatni tudja oket. A Linux alatt a leggyakrabban használt bináris formátum az ELF. A támogatott bináris formátumokat a kernel fordítása során választhatjuk ki, vagy utólag modulként illeszthetjük be. Az általánosan használt formátumok az a.out az ELF és néha a Java.
FEJLESZTOI ESZKÖZÖK 22
3. Fejlesztoi eszközök Linux alatt a fejleszto eszközöknek széles választéka áll rendelkezésünkre. Ezekbol kiválaszthatjuk a nekünk megfelelot, azonban néhány fontos eszközt mindenkinek ismernie kell. A Linux disztribúciók számtalan megbízható fejleszto eszközt tartalmaznak, amelyek foként a Unix rendszerekben korábban elterjedt eszközöknek felelnek meg. Ezek az eszközök nem nyújtanak barátságos felületet, a legtöbbjük parancssoros, felhasználói felület nélkül. Azonban sok éven keresztül bizonyították megbízhatóságukat, használhatóságukat. Kezelésük megtanulása meg csak egy kis ido kérdése.
3.1 Szövegszerkesztok Linuxhoz is találhatunk több integrált fejlesztoi környezetet (integrated development environment, IDE), azonban egys zerubb esetekben még továbbra is gyakran nyúlunk az egyszeru szövegszerkesztokhöz. Sok Unix fejleszto továbbra is ragaszkodik ezekhez a kevésbé barátságos, de a feladathoz sokszor tökéletesen elegendo, eszközökhöz. A Linux története során az alábbi eszközök terjedtek el:
3.1.1 Emacs Az eredeti Emacs szövegszerkesztot Richard Stallman, az FSF alapítója, készítette. Évekig a GNU Emacs volt a legnépszerubb változat. Késobb a grafikus környezethez készített XEmacs terjedt el. A felhasználói felülete nem olyan csillogó, mint sok más rendszernél, azonban számos, a fejlesztok számára jól használható funkcióval rendelkezik. Ilyen például a szintaxis ellenorzés. Vagy ha a fordítót beleintegráljuk képes annak hibaüzeneteit értelmezni, és a hibákat megmutatni. Továbbá le hetové teszi a debugger integrálását is a környezetbe.
3.1.2 vi A vi egy egyszeru szövegszerkeszto. Kezelését leginkább a gépelni tudók igényeihez alakították. A parancskészletét úgy állították össze, hogy a leheto legkevesebb kézmozgással használható legyen. Azonban egy tapasztalatlan felhasználó számára kezelése meglehetosen bonyolult lehet, ezért népszerusége erosen megcsappant.
3.1.3 pico A pico egy egyszeru képernyo orientált szövegszerkeszto. Általában minden Linux rendszeren megtalálható. Alapvetoen a pine levelezo csomag része. Elterjedten
FEJLESZTOI ESZKÖZÖK 23 használják a kisebb szövegek gyors módosítására. Komolyabb feladatokra nem ajánlott. Ezekben az esetekben alkalmasabb a következo részben tárgyalt joe vagy egy a feladatnak megfeleloen specializálódott editor. A program parancsai folyamatosan láthatóak a képernyo alján. A "^" jel azt jelenti, hogy a billentyut a ctrl gomb nyomva tartása mellett kell használni. Ha ez esetleg a terminál típusa miatt nem muködne, akkor a dupla ESC gombnyomást is alkalmazhatjuk helyette. Parancsai: Billentyukombináció
- g - x -o -j -r -w - y - v -k - u
-c -t
Parancs Segítség Kilépés (módosítás esetén rákérdez a mentésre) Az állomány kiírása Rendezés Állomány beolvasása és beillesztése a szerkesztett állományba Szó keresés Elozo oldal Következo oldal Szövegrész kivágása Az utoljára kivágott rész beillesztése az adott kurzor pozícióra. (Többször is vissza lehet illeszteni, vagyis másolni lehet vele.) Aktuális kurzor pozíció Helyesírás ellenorzés
3.1.4 joe A joe egy elterjedten használt szövegszerkeszto. Komolyabb feladatokra is alkalmas, mint a pico. Egyszerre több állományt is megnyithatunk vele különbözo opciókkal. Komolyságát a parancsok számából is láthatjuk, amelyet a -k-h gombokkal hozhatunk elo és tüntethetünk el. Itt is "^" jel azt jelenti, hogy a billentyuket a ctrl gomb nyomva tartása mellett kell használni. Gyakrabban használt parancsok: Billentyukombináció ^KF ^L ^KB ^KK ^KM ^KC ^KY ^Y
Parancs Szöveg keresése Ismételt keresés Blokk elejének kijelölése Blokk végének kijelölése Blokk mozgatása Blokk másolása Blokk törlése Sor törlése
FEJLESZTOI ESZKÖZÖK 24 Billentyukombináció ^_ ^KD ^KX ^C
Parancs Változtatás visszaléptetése Mentés Kilépés mentéssel Kilépés mentés nélkül
3.2 Fordítók Linux alatt a programo zási nyelvektol függoen az alábbi kiterjesztésekkel találkozhatunk: File utótag .a .c .C .cc .cpp .cxx .c++ .f .for .h .hxx .java .o .pl .pm .s .sa .so.x.y.z .tcl .tk .x
Jelentés Könyvtár C nyelvu forrás C++ nyelvu forrás Fortran nyelvu forrás C/C++ nyelvu header file C++ nyelvu header file Java nyelvu forrás Tárgykódú (object) file Perl Script Perl modul script Assembly kód Statikus programkönyvtár Megosztott könyvtár Tcl script RPC interfész file
3.2.1 GNU Compiler Collection A GNU Compiler Collection a C, C++, Objective-C, Ada, Fortran, és a Java nyelvek fordítóit foglalja össze egy csomagba. Ezáltal az ezen nyelveken írt programokat mind lefordíthatjuk a GCC-vel. A “GCC”-t a GNU Compiler Collection rövidítéseként értjük általában, azonban ez egyben a csomag C fordítójának is a leggyakrabban használt neve (GNU C Compiler). A C++ forrásokat lefordítására a G++ fordító szolgál. Azonban valójában a fordítók integrációja miatt továbbra is a gcc programot használjuk. Hasonló a helyzet az Ada fordítóval, amelyet GNAT-nak neveznek. További nyelvekhez (Mercury, Pascal) is léteznek front end-ek, azonban ezeknek egy részét még nem integrálták be a rendszerbe. Mi a továbbiakban a vizsgálódásainkat a C/C++ nyelvekre korlátozzuk.
FEJLESZTOI ESZKÖZÖK 25
3.2.2 gcc A gcc nem rendelkezik felhasználói felülettel. Parancssorból kell meghívnunk a megfelelo paraméterekkel. Számos paramétere ellenére szerencsére használata egyszeru, mivel általános esetben ezen paramétereknek csak egy kis részére van szükségünk. Használat elott érdemes ellenoriznünk, hogy az adott gépen a gcc mely verzióját telepítették. Ezt az alábbi paranccsal tehetjük meg: gcc –v
Erre válaszként ilyen sorokat kapunk: Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/2.96/specs gcc version 2.96 20000731 (Red Hat Linux 7.3 2.96-112)
Ebbol megtudhatjuk a gcc verzióját, továbbá a platformot, amelyre lefordították. A program további, gyakran használt paraméterei: Paraméter
Jelentés A kimeneti állománynév megadása. Ha nem adjuk meg, akkor az -o filename alapértelmezett file név az “a.out” lesz. Fordítás, linkelés nélkül. A paraméterként megadott forrás -c állományból tárgykódú (object) file-t készít. -Ddefiníció=x A definiálja a definició makro-t x értékre. Hozzáadja a könyvtárnév paraméterben meghatározott könyvtárt -Ikönyvtárnév ahhoz a listához, amelyben a header állományokat keresi. Hozzáadja a könyvtárnév paraméterben meghatározott könyvtárt -Lkönyvtárnév ahhoz a listához, amelyben a library állományokat keresi. -llibrary A programhoz hozzálinkeli a library nevu programkönyvtárat. Az alapértelmezett dinamikus linkelés helyett a fordító a statikus -static programkönyvtárakat linkeli a programba. A lefordított állományt ellátja a debuggoláshoz szükséges információkkal. A -g opció megadásával a fordító a standard debug -g, -ggdb információkat helyezi el. A -ggdb opció arra utasítja a fordítót, hogy olyan további információkat is elhelyezzen a programban, amelyeket csak a gdb debugger értelmez. Optimalizálja a programot az n optimalizációs szintre. -O, -On Alapértelmezett esetben a gcc csak néhány optimalizációt végez. A legáltalánosabban használt optimalizációs szint a 2-es. Az összes, általában használt figyelmeztetést (warning) bekapcsolja. -Wall A csak speciális esetben hasznos figyelmeztetéseket külön kell bekapcsolni. Példaként nézzünk végig néhány esetet a gcc program használatára.
FEJLESZTOI ESZKÖZÖK 26 Egy tetszoleges szövegszerkesztoben elkészítettük a már jól megszokott kódot: /* * Hello.c */ #include <stdio.h> int main() { printf("Hello, world\n"); return 0; }
Szeretnénk a jól sikerült programunkat ki is próbálni. Ehhez az alábbi paranccsal fordíthatjuk le: gcc -o Hello Hello.c
Ha nem rontottuk el a kódot, akkor a fordító hiba üzenet nélkül lefordítja a forrást, és a programra a futtatási jogot is beállítja. Ezután már csak futtatnunk kell: ./Hello
A programunk és a konzolon megjelenik a következo felirat: Hello, world
Vagyis meghívtuk a gcc programot, amely lefordította (compile) a kódot, majd meghívta az ld nevu linker programot, amely létrehozta a futtatható bináris állományt. Ha csak tárgykódú állományt (object file) akarunk létrehozni (melynek kiterjesztése .o), vagyis ki szeretnénk hagyni a linkelés folyamatát, akkor a –c kapcsolót használjuk a gcc program paraméterezésekor: gcc -c Hello.c
Ennek eredményeképpen egy Hello.o nevu tárgykódú file jött létre. Ezt természetesen össze kell még linkelnünk: gcc -o Hello Hello.o
A –o kapcsoló segítségével tudjuk megadni a linkelés eredményeként létrejövo futtatható file nevét. Ha ezt elhagyjuk, alapértelmezésben egy a.out nevu állomány jön létre. A következokben megvizsgáljuk azt az esetet, amikor a programunk több (esetünkben ketto) forrás állományból áll. Az egyik tartalmazza a foprogramot: /* second.c */ #include <stdio.h> double sinc(double);
FEJLESZTOI ESZKÖZÖK 27 int main() { double x; printf("Please input x: "); scanf("%lf", &x); printf("sinc(x) = %6.4f\n", sinc(x)); return 0; }
A másik implementálja a
sinx függvényt: x
/* sinc.c */ #include <math.h> double sinc(double x) { return sin(x)/x; }
Ennek fordítása: gcc -o second -lm second.c sinc.c
Így a second futtatható file-hoz jutunk. Ugyanezt megoldhattuk volna több lépésben is: gcc -c second.c gcc -c sinc.c gcc -o second -lm second.o sinc.o
Az -lm kapcsolóra azért van szükségünk, hogy hozzálinkeljük a sin(x) függvényt tartalmazó matematikai programkönyvtárat a programunkhoz. Általában a –l kapcsoló arra szolgál, hogy egy programkönyvtárat hozzáfordítsunk a programunkhoz. A Linux rendszerhez számos szabványosított programkönyvtár tartozik, amelyek a /lib illetve a /usr/lib könyvtárban találhatóak. Amennyiben a felhasznált programkönyvtár máshol található, az elérési útvonalat meg kell adnunk a –L kapcsolóval: gcc prog.c -L/home/myname/mylibs mylib.a
Hasonló problémánk lehet a header állományokkal. A rendszerhez tartozó header állományok alapértelmezetten a /usr/include könyvtárban (illetve az innen kiinduló könyvtárstruktúrában) találhatóak, így amennyiben ettol eltérünk, a –I kapcsolót kell használnunk a saját header file útvonalak megadásához: gcc prog.c -I/home/myname/myheaders
Ez azonban ritkán fordul elo, ugyanis a C programokból általában az aktuális könyvtárhoz viszonyítva adjuk meg a saját header állományainkat. A C elofeldolgozó (preprocessor) másik gyakran használt funkciója a #define direktíva. Ezt is megadhatjuk közvetlenül parancssorból. A
FEJLESZTOI ESZKÖZÖK 28 gcc –DMAX_ARRAY_SIZE=80 prog.c -o prog
hatása teljes mértékben megegyezik a prog.c programban elhelyezett #define MAX_ARRAY_SIZE=80
preprocesszor direktívával. Ennek a funkciónak gyakori felhasználása a gcc -DDEBUG prog.c -o prog
paraméterezés. Ilyenkor a programban elhelyezhetünk feltételes fordítási direktívákat a debuggolás megkönnyítésére: #ifdef DEBUG printf("Thread started successfully."); #endif
A fenti esetben látjuk az új szálak indulását debug módban, viszont ez az információ szükségtelen a felhasználó számára egy már letesztelt program esetén.
3.3 Make A Unix-os fejlesztések egyik oszlopa a make program. Ez az eszköz lehetové teszi, hogy könnyen leírjuk és automatizáljuk a programunk fordításának menetét. De nem csak nagy programok esetén, hanem akár egy forrásállományból álló programnál is egyszerusíti a fordítást azáltal, hogy csak egy make parancsot kell kiadnunk az aktuális könyvtárban a fordító paraméterezése helyett. Továbbá ha egy komolyabb program sok forrás állományból áll, de csak néhá nyat módosítunk, akkor az összes állomány újrafordítása helyett csak a szükségeseket frissíti. Ahhoz, hogy mindezeket a funkciókat megvalósíthassa egy ún. Makefile-ban le kell írnunk a programunk összes forrás állományát. Erre láthatunk egy példát: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
# Makefile OBJS = second.o sinc.o LIBS = -lm second: $(OBJS) gcc –o second $(OBJS) $(LIBS) install: second install –m 644 second /usr/local/bin .PHONY: install
?? Az 1. sorban egy kommentet láthatunk. A Unix-os tradíciók alapján a kommentet egy # karakterrel jelöljük. ?? A 3. sorban definiáljuk az OBJS változót, melynek értéke: second.o sinc.o ?? a 4. sorban ugyanezt tesszük a LIBS változóval. A késobbiekben ezt fogjuk majd felhasználni linkelési paraméterek beállítására. ?? A 6. sorban egy szabály (rule) megadását láthatjuk. Ebben az second állomány az OBJS változó értékeitol függ. A second állományt hívjuk itt a szabály
FEJLESZTOI ESZKÖZÖK 29
?? ?? ?? ??
céljának (target) és a $(OBJS) adja a függoségi listát. Megfigyelhetjük a változó használatának módját is. A 7. sor egy parancssor. Azt mondja el, hogy hogyan készíthetjük el a cél objektumot a függoségi lista elemeibol. Itt állhat több parancssor is szükség szerint, azonban minden sort TAB karakterrel kell kezdeni. A 9. sor egy érdekes célt tartalmaz. Ebben a szabályban valójában nem egy állomány létrehozása a célunk, hanem az installációs muvelet megadása. A 10. sorban végezzük el a programunk installációját az install programmal az /usr/local/bin könyvtárba. A 11. sor egy problémának a megoldását rejti. A 9. sorban a cél nem egy állomány volt. Azonban ha mégis létezik egy install nevu állomány, és az frissebb, mint a függoségi listában szereplo second állomány, akkor nem fog lefutni a szabályunk. Ezzel természetesen összezavarja és elrontja a Makefileunk muködését. Ezt küszöböljük ki ezzel a sorral. A .PHONY egy direktíva, amely módosítja a make muködését. Ebben az esetben megadhatjuk vele, hogy az install cél nem egy file neve.
A Makefile szerkezete Általánosan megfogalmazva a Makefile-ok ötféle dolgot tartalmazhatnak. Ezek: ?? Kommentek ?? Explicit szabályok ?? Változódefiníciók ?? Direktívák ?? Implicit szabályok
3.3.1 Kommentek A kommentek magyarázatul szolgálnak, a make segédprogram gyakorlatilag figyelmen kívül hagyja oket. A kommenteknek # karakterrel kell kezdodniük.
3.3.2 Explicit szabályok Egy szabály (rule) azt határozza meg, hogy mikor és hogyan kell újrafordítani egy vagy több állományt. Az így keletkezo állományokat a szabály céljának vagy tárgyának (target) nevezzük. Azonban mint láthattuk a cél nem minden esetben állomány. Hogy ezek az állományok létre jöjjenek, általában más állományokra van szükség. Ezek listáját nevezzük függoségi listának, feltételeknek vagy elokövetelmény nek. Például: foo.o: foo.c defs.h gcc -c -g foo.c
# példa szabályra
A fenti példában a szabály tárgya a foo.o file, az elokövetelmény a foo.c illetve a foo.h állományok. Mindez azt jelenti, hogy a foo.o file-t akkor kell újrafordítani, ha ?? a foo.o file nem létezik, ?? a foo.c idobélyege korábbi, mint a foo.o idobélyege, ?? a defs.h idobélyege korábbi, mint a foo.o idobélyege.
FEJLESZTOI ESZKÖZÖK 30 Azt, hogy a foo.o file-t hogyan kell létrehozni, a második sor adja meg. A defs.h nincs a gcc paraméterei között: a függoséget egy – a foo.c file-ban található - #include “defs.h” C nyelvu preprocesszor direktíva jelenti. A szabály általános formája: TÁRGYAK: ELOKÖVETELMÉNYEK; PARANCS PARANCS ...
A TÁRGYAK szóközzel elválasztott file nevek, akárcsak az ELOKÖVETELMÉNYEK. A file nevek tartalmazhatnak speciális jelentésu karaktereket (wildcard characters), mint például a „. ” (aktuális könyvtár), „*” vagy „%” (tetszoleges mennyiségu karaktersorozat), „~” (home könyvtár). A PARANCS vagy az elokövetelményekkel egy sorban van pontosvesszovel elválasztva, vagy a következo sorokban, amelyeket TAB karakterrel kell kezdeni. Mivel a $ jel már foglalt, a valódi $ jelek helyett $$-t kell írnunk. Ha egy sor végére „\” jelet teszünk, a következo sor az elozo sor folytatása lesz, teljesen úgy, mintha a második sort folytatólagosan írtuk volna. Erre azért van szükség, mert minden parancssort külön subshell-ben futtat le a make. Ezáltal a cd parancsnak a hatása is csak abban a sorban érvényesül, ahova írjuk. Például: cd konyvtar; \ gcc –c –g foo.c
Amikor a make segédprogramot paraméterek nélkül futtatjuk, automatikusan az elso szabály hajtódik végre, valamint azok a szabályok, amelyektol az a szabály valamilyen módon függ (vagyis tárgya feltétele valamely feldolgozandó szabálynak). A másik lehetoség, hogy a make segédprogramot egy szabálynak a nevével paramétereztük. Ilyenkor azt a szabályt hajtódik végre, illetve amelyektol az a szabály függ.
3.3.3 Változódefiníciók Mint az elso példában is láthattuk, gyakran elofordul, hogy ugyanazoknak az állományneveknek több helyen is szerepelniük kell. Ilyenkor, hogy könnyítsük a dolgunkat, változókat használunk. Ezáltal elég egyszer megadnunk a listát, a többi helyre már csak a változót helyezzük. A változók használatának másik célja, hogy a Makefile-unkat rendezettebbé tegyük, ezáltal megkönnyítve a dolgunkat a késobbi módosításoknál. Mint láthattuk a változók megadásának szintaxisa: VÁLTOZÓ = ÉRTÉK
Erre a változóra késobb a $(VÁLTOZÓ)
szintaxissal hivatkozhatunk.
FEJLESZTOI ESZKÖZÖK 31
A változók használata során kerüljük az alábbi megoldásokat: OBJS = first.o OBJS = $(OBJS) second.o OBJS = $(OBJS) third.o
Azt várnánk, hogy ezek után az OBJS értéke firs.o second.o third.o lesz, azonban ez nem így van. Valójában az értéke $(OBJS) third.o mert a make csak akkor értékeli ki a változót, amikor azt használjuk, és nem szekvenciálisan, ahogy vártuk. Ezért a fenti példa egy végtelen ciklust eredményez. Erre a problémára a GNU make tartalmaz megoldást, azonban ezt egyedisége miatt kerüljük, hogy ne merüljenek fel portolási problémák. A változók használata még további lehetoségeket is rejt, azonban ezek ritka használata miatt itt nem térünk ki rá. További információkat az info make paranccsal érhetünk el.
3.3.4 Direktívák A direktívák nagyon hasonlóak a C nyelvben használt preprocesszor direktívákhoz. Más file-ok futtatása: include FILE_NEVEK...
Ahol a FILE_NEVEK egy kifejezés, amely tartalmazhat szóközzel elválasztott file neveket speciális karakterekkel és változókat. A leggyakrabban használt elemek a feltételes fordítási direktívákhoz kötodnek: ifeq ($(CC),gcc) libs=$(libs_for_gcc) else libs=$(normal_libs) endif
A fenti példában, ha a CC változó értéke gcc, akkor a második sor hajtódik végre, egyébként a negyedik. Direktívaként változót is megadhatunk, amely tartalmazhat új sor karaktert is: define two-lines echo foo echo $(bar) endef
További leírás a make parancs info oldalán található.
3.3.5 Implicit szabályok Megvizsgálva a C nyelvu programok fordítását, a folyamat két lépésre oszlik. 1. A .c végu forrás állományok fordítása .o végu tárgykódú állományokká. 2. Az .o végu tárgykódú állományok fordítása.
FEJLESZTOI ESZKÖZÖK 32 Jó lenne, ha egy xxx.o állományra hivatkozva a make automatikusan utánanézne, hogy van-e egy xxx.c nevu file, majd azt úgy kezelné, mint egy lefuttatandó szabály tárgyát. Ennek a szabálynak a keretében a gcc-t felparaméterezve lefordítaná az xxx.c állományt xxx.o tárgykódú állománnyá. Többek között ezt a funkciót teszik lehetové az implicit szabályok. Ha a make program egy állományt talál, amely egy feldolgozandó szabály feltételei között szerepel, ugyanakkor nem szerepel egyetlen szabály tárgyaként sem, akkor megpróbál egy alapértelmezett szabályt találni rá, és ha ez sikerül, akkor végre is hajtja. Ezeket az alapértelmezett szabályokat implicit szabályoknak nevezzük. Az implicit szabályok nagy része elore adott, de mi is írhatunk elo. Az implicit szabályok közül a legfontosabbak az ún. ragozási (suffix) szabályok. A make programnak megadható egy suffix lista, amelyet megpróbál eltávolítani az egyes cél nevekbol. Az így kapott suffix alapján keres a suffix szabályban. Ilyen suffix lehet például egy file kiterjesztése. Példaként nézzük egy ilyen szabály megadást: .c.o: $(CC) –c $(CFLAGS) $(CPPFLAGS) –o $@ $< .SUFFIXES: .c .o
Ez a példa a .c kiterjesztésu forrás állományokból a hozzá tartozó .o kiterjesztésu állományok eloállítására fogalmaz meg egy általános szabályt. (Ez, a C forrásokból a tárgykódú állományok eloállításának szabálya, a make programban alapértelmezésben adott, így általános esetekben nincs szükség a megadására.) A suffix szabályok általános alakja: FsCs: PARANCS
Az Fs a forrás suffix, a Cs a cél suffix. A forrásból a cél eloállításához a make a PARANCS sorban megadott utasításokat hajtja végre. A korábbi példában meg a make programnak egy eddig nem látott szolgá ltatásával is találkoztunk. A $@ és a $< egy-egy automatikus változó, más néven dinamikus makró. Az egyértelmu, hogy egy ilyen általánosan megfogalmazott szabálynál szükségünk van egy mechanizmusra, amivel a feltétel és a cél állományok nevét elérhetjük a parancssorban. Erre szolgálnak az automatikus változók. Az automatikus változók jelentése a következo: Aut. változó $@ $< $? $^
Jelentés A cél file neve. A függoségi lista elso elemének neve. Az összes olyan feltétel file neve (sorban, space-el elválasztva), amely frissebb a cél állománynál. Az összes feltétel file neve space-el elválasztva.
FEJLESZTOI ESZKÖZÖK 33 Aut. változó $+ $*
Jelentés Jelentése nagyrész egyezik az elozovel, azonban duplikált feltétel file neveket többször is tartalmazza úgy, ahogy az elokövetelmények közt szerepel. A cél file nevének suffix nélküli része.
3.4 KDevelop
Ábra 3-1 KDevelop screenshoot
Manapság a fejlesztok a munkájukhoz kényelmes környezetet szeretnének, amely átveszi tolük a gyakran ismétlodo egyszeru muveletek egy részét. A Unix világ hagyományos, “fapados” fejlesztoi eszközei nem elégítik ki ezt az igényt, ezért született meg 1998-ban a KDevelop projekt. Célja egy könnyen használható C/C++ IDE (Integrated Development Enviroment) létrehozása volt a Unix rendszerekhez. Magába integrálja a GNU általános fejlesztoi eszközeit, mint például a g++ fordítót és a gdb debuggert. Ezek hagyományos megbízhatóságát ötvözi a kényelmes, grafikus kezeloi felülettel, és néhány, a fejlesztot segíto automatizmussal. Ezáltal a fejleszto jobban koncentrálhatja a figyelmét a kódolásra a parancssoros programok kezelése helyett. A KDevelop elsodleges célja, hogy felgyorsítsa a KDE programok fejlesztését, azonban a C/C++ fejlesztés bármely területén jól használható, ingyenes eszköz.
FEJLESZTOI ESZKÖZÖK 34 A KDevelop az alábbi szolgáltatásokkal rendelkezik: ?? Minden fejlesztoi eszközt integrál, amely a C++ fejlesztéshez szükséges: fordító, linker, automake és autoconf. ?? KAppWizard, amely kész applikációkat generál. ?? Osztály generátor, amely az új osztályok generálását és a projektbe való integrálását végzi. ?? File menedzsment, amely a forrás, header és dokumentációs állományokat kezeli. ?? SGML és HTML felhasználói dokumentum generátor. ?? Automatikus HTML alapú API dokumentáció generátor, amely kereszthivatkozásokat hoz létre a felhasznált programkönyvtárak dokumentációjához. ?? A többnyelvu kezeloi felület támogatása a fejlesztett applikációkban. ?? WYSIWYG (What you see is what you get) szerkesztoi felület a dialógus ablakokhoz. ?? CVS front-end a projektek verziómenedzsmentjéhez. ?? Az applikációk debugolása az integrált debuggerrel. ?? Ikon editor. ?? Egyéb, a fejlesztésekhez szükséges programok hozzáadása a “Tools” menühöz.
DEBUG 35
4. Debug A C az egyik legelterjedtebb nyelv, és egyértelmuen a Linux rendszerek általános programozási nyelvének tekintheto, azonban van jó pár olyan jellemzoje, amely használata során könnyen vezethet nehezen felderítheto bug-okhoz. Ilyen gyakori és nehezen felderítheto hibafajta a memory leak, amikor a lefoglalt memória felszabadítása elmarad, vagy a buffer overrun, amikor a program túlírja a lefoglalt területet. Ebben a fejezetben ezeknek a problémáknak a megoldására is látunk példákat.
4.1 gdb A célja egy debugger-nek, mint például a gdb-nek, az hogy belelássunk egy program muködésébe, hogy követhessük a futás során lezajló folyamatokat. Továbbá, hogy megtudjuk mi történt a program összeomlásakor, mi vezetett a hibához. A gdb funkcióit, amelyekkel ezt lehetové teszi, alapvetoen négy csoportba oszthatjuk: ?? A program elindítása. ?? A program megállítása meghatározott feltételek esetén. ?? A program megálláskori állapotának vizsgálata. ?? A program egyes részeinek megváltoztatása és hatásának vizsgálata a hibára. A gdb a C-ben és C++-ban írt programok vizsgálatára használható. Más nyelvek támogatása csak részleges.
4.1.1 Példa A debuggoláshoz az adott programnak tartalmaznia kell a debug információkat. (A fordító –g kapcsolója.) Eloször indítsuk el a programot a gdb myprogram
utasítással, majd állítsuk be a program kimenetének a szélességét a set width=70
segítségével. Helyezzünk el egy töréspontot (breakpoint) a myfunction nevu függvénynél: break myfunction
Ezután a program futtatásához adjuk ki a run
parancsot!
DEBUG 36 Amikor elértük a töréspontot a program megáll. Ilyenkor a leggyakrabban használt parancsok: Parancs n s p variable bt c Ctrl+D
Magyarázat A következo (next line) sorra ugrás Belépés (step into) egy függvénybe Kiírja (print) a variable változó aktuális értékét A stack frame megjelenítése (backtrace) A program folytatása (continue) A program leállítása (az EOF jel)
Sikeres debuggolás után a quit paranccsal léphetünk ki.
4.1.2 A gdb indítása A gdb-t többféle argumentummal és opcióval indíthatjuk, amellyel befolyásolhatjuk a debuggolási környezetet. A leggyakrabban egy argumentummal használjuk, amely a vizsgálandó program neve: gdb PROGRAM
Azonban a program mellett megadhatjuk második paraméterként a core állományt: gdb PROGRAM CORE
A core file helyett azonban megadhatunk egy processz ID-t is, ha egy éppen futó processzt szeretnénk megvizsgálni: gdb PROGRAM 1234
Ilyenkor a gdb az “1234” számú processzhez kapcsolódik. Ezeket a funkciókat a gdb elindítása után parancsokkal is elérhetjük. További lehetoségként a gdb-t használhatjuk más gépeken futó alkalmazások távoli debuggolására is.
4.1.3 Breakpoint, watchpoint, catchpoint A breakpoint megállítja a program futását, amikor az elér a program egy meghatározott pontjára. Minden breakpoint-hoz megadhatunk plusz feltételeket is. Beállításuk a break paranccsal és paramétereivel történik. Megadhatjuk a program egy sorát, függvény nevet, vagy az egzakt címet a programon belül: break FUNCTION
A forrásállomány FUNCTION függvényének meghívásánál helyezi el a töréspontot. (Lekezeli a C++ függvény felüldefiniálását is.) break +OFFSET break -OFFSET
DEBUG 37 Az aktuális pozíciótól megadott számú sorral vissza, vagy elorébb helyezi el a töréspontot. break LINENUM
Az aktuális forrásállomány LINENUM sorában helyezi el a töréspontot. break FILENAME:LINENUM
A FILENAME forrás állomány LINENUM sorában helyezi el a töréspontot. break *ADDRESS
Az ADDRESS címen helyezi el a töréspontot. break
Argumentum nélkül az aktuális stack frame következo utasítására helyezi el a töréspontot. break ... if COND
A törésponthoz feltételeket is megadhatunk. A COND kifejezés minden alkalommal kiértékelodik, amikor a töréspontot eléri a processz, és csak akkor áll meg, ha az értéke nem nulla, vagyis igaz. A watchpoint olyan speciális breakpoint, amely akkor állítja meg a programot, ha a megadott kifejezés értéke változik. Nem kell megadni azt a helyet, ahol ez történhet. A watchpoint-okat különbözo parancsokkal állíthatjuk be: watch EXPR
A gdb megállítja a programot, amikor az EXPR kifejezés értékét a program módosítja, írja. rwatch EXPR
A watchpoint akkor állítja meg a futást, amikor az EXPR kifejezést a program olvassa. awatch EXPR
A watchpoint mind az olvasáskor, mind az íráskor megállítja a program futását. A beállítások után a watchpoint-okat ugyanúgy kezelhetjük, mint a breakpoint-okat. Ugyanazokkal a parancsokkal engedélyezhetjük, tilthatjuk, törölhetjük. A catchpoint egy másik speciális breakpoint, amely akkor állítja meg a program futását, ha egy meghatározott esemény következik be. Például egy C++ programban bekövetkezo exception, vagy egy könyvtár betöltése ilyen esemény lehet. Ugyanúgy, mint a watchpoint-oknál, itt is több lehetoségünk van a töréspont beállítására. A catchpoint-ok általános beállítási formája: catch EVENT
Ahol az EVENT kifejezés az alábbiak közül valamelyik: EVENT
Jelentés
DEBUG 38 EVENT throw catch exec fork vfork load [LIBNAME] unload [LIBNAME]
Jelentés Egy C++ exception keletkezésekor. Egy C++ exception lekezelésekor. Az “exec” függvény meghívásakor. Az “fork” függvény meghívásakor. Az “vfork” függvény meghívásakor. A LIBNAME könyvtár betöltésekor. A LIBNAME könyvtár eltávolításakor.
A catchpoint-okat szintén ugyanúgy kezelhetjük a beállítás után, mint a korábbi töréspontokat.
4.1.4 xxgdb A gdb nagyon hasznos segédprogram, azonban parancssoros felületét nem mindenki kedveli. Ezért kezelésének megkönnyítéséhez készült több grafikus front-end is. Ezek egyike, talán a legegyszerubb, az xxgdb. Az alábbi ábrán láthatunk egy képet a felületérol.
Ábra 4-1 xxgdb
Mint látható a felületén lényegében a gdb parancsokat gombokkal érhetjük el. A másik plusz, amit a gdb- vel szemben nyújt, hogy a forrás állományt párhuzamosan figyelemmel kísérhetjük.
DEBUG 39
4.1.5 DDD A DDD egy GNU projekt, melynek célja, hogy grafikus fron-endként szolgáljon a parancssoros debuggerek számára, mint például a GDB, DBX, WDB, Ladebug, JDB, XDB, a Perl debugger, vagy a Python debugger. A szokásos front-end funkciók mellett, mint például a forrás állományok megtekintése, több jelentos szolgáltatással is rendelkezik. Ilyenek az interaktív grafikus adatábrázolások, ahol az adatstruktúrák gráfként vannak megjelenítve. A DDD felületét mutatja a következo kép:
Ábra 4-2 A DDD felülete
A Data Window az aktuális megfigyelt program adatait mutatja. A Source Window a program forráskódját mutatja. A Debugger Console fogadja a debugger parancsokat és mutatja az üzeneteit. A Command Tool a leggyakrabban használt parancsok gombjait tartalmazza. A Machine Code Window az aktuális gépi kódú programot mutatja. Az Execution Window a vizsgált program bemeneteit és kimeneteit tartalmazza.
4.1.6 KDevelop internal debugger A KDevelop is rendelkezik egy beépített gdb grafikus front-enddel. Ez funkcióiban valamelyest elmarad a DDD mögött, azonban egy jól használható barátságos felületet nyújt. Ezáltal lehetové teszi a fejlesztett projektünk debuggolását a fejlesztoi környezeten belül. Használata során lehetoségünk nyílik a processz adatainak vizsgálatára, breakpointok használatára, a framestack megfigyelésére.
4.2 Memóriakezelési hibák A fejezet elején már volt arról szó, hogy a C és C++ nyelv használata során számos memóriakezelési hibát ejthetünk, amelyeknek felderítése nehéz feladat. Ennek megoldására számos eszköz született. Most ezekbol ismerhetünk meg néhányat.
DEBUG 40
A vizsgálataink elott állítsunk elo egy hibás programot, amely az “állatorvosi ló” szerepét fogja betölteni. Íme a memóriakezelési hibáktól hemzsego kódunk: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
/* * Buggy code */ #include <stdlib.h> #include <stdio.h> #include <string.h> char global[5]; int main(void) { char* dyn; char local[5]; // 1. tuliras (kicsit) dyn=malloc(5); strcpy(dyn, "12345"); printf("1: %s\n", dyn); free(dyn); // 2. felszabaditott terulet hasznalata strcpy(dyn, "12345"); printf("2: %s\n", dyn); // 3. tuliras (nagyon) dyn=malloc(5); strcpy(dyn, "12345678"); printf("3: %s\n", dyn); // 4. ele iras *(dyn-1)='\0'; printf("4: %s\n", dyn); // memory leak !!! // 5. lokalis valtozo tuliras strcpy(local, "12345"); printf("5: %s\n", local); // 6. lokalis valtozo eleiras local[-1]='\0'; printf("6: %s\n", local); // 7. globalis valtozo tuliras strcpy(global, "12345"); printf("7: %s\n", global); // 8. globalis valtozo eleiras global[-1]='\0'; printf("8: %s\n", global); return 0; }
DEBUG 41 Ez a kód háromféle memóriát allokál, és ezekkel követ el hibákat. Az elso típus a dinamikusan, malloc-al allokált memória, a második lokális változó, amely a program stackjében helyezkedik el, a harmadik pedig globális változó, amely egy külön részen helyezkedik el. Mindhárom esetben túlírjuk a lefo glalt tartományt, illetve egy-egy byte-ot elé írunk. Ezek mellett a kód tartalmaz egy hozzáférést egy már felszabadított memória részhez és egy memory leak-et is. Habár ezt a programot boven elláttuk hibákkal, mégis általában probléma nélkül lefut. De ez nem jelenti azt, hogy ezek a hibák lényegtelenek. A túlírások idovel a program váratlan, és elsore megmagyarázhatatlannak tuno összeomlásához vezethetnek. A memory leak-ek pedig idovel feleszik a számítógép memóriáját. Elsore a kódot lefordítva, és lefuttatva az alábbit látjuk: $ gcc -ggdb -Wall -o buggy buggy.c $ ./buggy 1: 12345 2: 12345 3: 12345678 4: 12345678 5: 12345 6: 12345 7: 12345 8: 12345
A következokben megismerhetünk néhány eszközt, amely ennél többet mutat számunkra. Ezek egy része megtalálható az alábbi címen sok más eszköz társaságában: http://www.ibiblio.org/pub/Linux/devel/lang/c/
4.2.1 Electric Fence Az elso eszközt, amit megismerünk, Electric Fence-nek hívják. Bár a memory leak-ek vizsgálatára ez az eszköz nem használható, ugyanakkor a programozókat segítheti a buffer túlírások és a már felszabadított memória használatának felderítésében. Más malloc debuggerrel ellentétben az Electric Fence a hibás olvasásokat is detektálja. Az Electric Fence a számítógép virtuális memória kezelo hardware-t használja a hibák felderítésére azáltal, hogy letiltott memória lapokat helyez közvetlenül a lefoglalt memória területek után (illetve elé a felhasználó beállításától függoen). Amikor a program ezeket a tiltott területeket írja, vagy olvassa, a hardware egy segmentation fault hibát vált ki, amely a program leállásához vezet. Ilyenkor a debugerrel megvizsgálhatjuk a hiba okát. Hasonló képen a felszabadított memória területeket is letiltja, így azok utólagos használata szintén hibához és leálláshoz vezet. 4.2.1.1 Az Electric Fence használata Az Electric Fence a C library normál malloc függvényét cseréli le a saját speciális allokációs rutinjára. Így egyetlen feladatunk, hogy a libefence.a vagy libefence.so könyvtár állomány hozzálinkeljük a programunkhoz. Ezt kétféle képen tehetjük.
DEBUG 42 Az –lefence argumentummal fordításkor hozzálinkelhetjük a programunkhoz a könyvtár állományt. Ez a példa programunknál: gcc -ggdb -Wall -o buggy buggy.c -lefence
A másik lehetoség dinamikus linkelés esetén az LD_PRELOAD környezeti változó használata. Ezzel megadhatjuk, hogy a program futása elott a könyvtár állomány betöltodjön. export LD_PRELOAD=libefence.so.0.0
vagy LD_PRELOAD=libefence.so.0.0 ./buggy
(Az utóbbi a javasolt.) Ha ezek után lefuttatjuk a programunkat, akkor már nem garázdálkodhat szabadon. Az Electric Fence védelmi algoritmusai detektálják a hibát. $ ./buggy Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens 1: 12345 Segmentation fault
Mint látható az Electric Fence nem mondja meg hol a hiba, viszont a hibát sokkal érzékelhetobbé teszi, és ez által egy debuggerrel, mint például a gdb, megvizsgálhatjuk. (Ehhez természetesen a programnak rendelkeznie kell a debug információkkal is.) Esetünkben ez az alábbiak szerint alakul: $ gdb ./buggy GNU gdb Red Hat Linux (5.2-2) Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. This GDB was configured as "i386-redhat-linux"... (gdb)
Ha dinamikusan szeretnénk hozzáadni a könyvtár állományt, akkor az alábbiakra is szükségünk van: (gdb) set env LD_PRELOAD=libefence.so.0.0
Ezek után megkezdhetjük a program vizsgálatát. (gdb) r Starting program: /home/tomcat/buggy [New Thread 1024 (LWP 26727)] Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens 1: 12345 Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 1024 (LWP 26727)] 0x420807a6 in strcpy () from /lib/i686/libc.so.6
DEBUG 43 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so.6 #1 0x080484fc in main () at buggy.c:23 #2 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb)
Mint látható a 23. sorban máris elkapott a rendszer egy hibát. 23
strcpy(dyn, "12345");
Ez az a hely, ahol a felszabadított memória területre próbáltunk írni. Ha ezt a sort kikommentezzük, akkor a 24. sorban, a felszabadított terület olvasásánál érzékeli a rendszer a hibát. Így tovább haladva a következo hiba a 28. sorban található, ahol jelentosen túlírjuk a lefoglalt memóriát. Ezt is eltávolítva további hibát nem érzékel, pedig mint látható maradt még boven. 4.2.1.2 Memory Alignment kapcsoló Mint észrevehettük az Electric Fence átszaladt az elso túlíráson, ahol a tartományt csak egy byte-al írtuk túl. A probléma forrása a memóriaillesztés. A modern processzorok a memóriát több byte-os részekre osztják. Ez a 32 bites processzorok esetén 4 byte. A malloc általános implementációja is így kerekítve foglalja le a memóriát. Alapértelmezett beállítások mellett az Electric Fence is ezt a módszert alkalmazza, ezért nem vette észre az 1 byte-os differenciát. Ahhoz, hogy ezt a hibát megtaláljuk, az Electric Fence-nek meg kell adnunk, hogy 1 byte-os illesztést használjon. Ezt az EF_ALIGNMENT környezeti változó 1-re állításával tehetjük meg: (gdb) set env EF_ALIGNMENT=1 (gdb) r Starting program: /home/tomcat/buggy [New Thread 1024 (LWP 26810)] Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 1024 (LWP 26810)] 0x420807a6 in strcpy () from /lib/i686/libc.so.6 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so.6 #1 0x080485f8 in main () at buggy.c:18 #2 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb)
Így már megtalálta a 18. sorban található kis túlírásunkat is. 4.2.1.3 Az eléírás Mint látható idáig nem sikerült detektálnunk a buffer underrun esetét, vagyis amikor a lefoglalt terület elé írtunk egy byte-ot. Az EF_PROTECT_BELOW változó 1-re
DEBUG 44 állításával azt kérhetjük az Electric Fence-tol, hogy a letiltott memória lapokat a lefoglalt terület elé helyezze, így ezt a hiba típust szurje ki. Természetesen ilyenkor a buffer overrun-t vagyis a túlírást nem tudja érzékelni. (gdb) set env EF_PROTECT_BELOW=1 (gdb) r Starting program: /home/tomcat/buggy [New Thread 1024 (LWP 26868)] Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens 1: 12345 3: 12345678 Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 1024 (LWP 26868)] 0x08048658 in main () at buggy.c:32 32 *(dyn-1)='\0'; (gdb)
4.2.1.4 További lehetoségek Még az alábbi kapcsolókkal találkozhatunk az eszköz használata során: Változó EF_PROTECT_FREE
Jelentés Ha értéke 1, akkor a felszabadított memória területek nem kerülnek újra kiosztásra, hanem letiltja oket a rendszer. EF_ALLOW_MALLOC_0 Ha értéke 1, akkor engedélyezi a 0 méretu területek allokálását. EF_FILL Amikor 0 és 255 között van az értéke, akkor a lefoglalt terület minden byte-ját feltölti a rendszer ezzel az értékkel. Ezáltal segíti az inicializációs problémák kiszurését.
4.2.1.5 Eroforrás igények Habár az Electric Fence hasznos eszköz, könnyen kezelheto és gyors (mivel minden hozzáférés ellenorzést a hardware végez), ennek megvan az ára. A legtöbb processzor a hozzáférés vezérlést csak egy-egy lapra engedi állítani. Egy lap mérete például egy Intel x86-os processzornál 4KB. Mivel az Electric Fence allokáló rutinja két különbözo területet foglal le minden alkalommal (egyet amihez hozzáférhet a program, és egyet, aminél letiltja), ezért minden egyes allokálás lefoglal egy 4KB-os lapot. Ha a tesztelt kódban sok kisméretu memória allokáció van, akkor az Electric Fence használatával több nagyságrenddel megnohet a memória felhasználás. Természetesen a helyzet tovább romlik, ha a felszabadított területeket védjük, mert ilyenkor valójában nem szabadul fel memória. Ezért ha azt tapasztaljuk, hogy használatakor a rendszernek nagyon megfogyatkozik a szabad memória kapacitása, akkor ajánlatos a swap területet megnövelni.
DEBUG 45
4.2.2 NJAMD (Not Just Another Malloc Debugger) Az NJAMD egy teljes funkcionalitású malloc debugger. Véd a buffer overflow, underflow és a felszabadított memória írásától-olvasásától, mint az Electric Fence, azonban alkalmas a memory leak felfedezésére is. A könyvtár állomány mellett rendelkezik egy, egyenlore még fejlesztésre szoruló, front-endel is, de használható az Electric Fence- hez hasonlóan a gdb-bol, vagy más debuggerbol. 4.2.2.1 Használata Hasonlóan használhatjuk, mint az Electric Fence-t. Hozzálinkelhetjük az –lnjamd kapcsolóval a megfelelo könyvtárat a programunkhoz: gcc -ggdb -Wall -o buggy buggy.c -lnjamd
Vagy használhatjuk az LD_PRELOAD környezeti változót: LD_PRELOAD=libnjamd.so
Ezek mellet egy további lehetoség az njamd segédprogram - front-end használata. Ez tulajdonképpen a gdb-t hívja meg, és az LD_PRELOAD környezeti változóval használja a könyvtár állományt: $ njamd -e ./buggy NJAMD - Not Just Another Malloc Debugger -----------------------------------------------------------------njamd> start njamd>
-----------------------------------------------------------------welcome to change it and/or distribute copies of it under certain conditions. This GDB was configured as "i386-redhat-linux"... (gdb) set env LD_PRELOAD=libnjamd.so (gdb) set env NJAMD_PROT=over (gdb) set env NJAMD_ALIGN=1 (gdb) set env NJAMD_DUMP_LEAKS_ON_EXIT=1 (gdb) set env NJAMD_ALLOW_READ=1 (gdb) set env NJAMD_FE_PORT=33452 (gdb) run Starting program: /home/tomcat/buggy Program received signal SIGSEGV, Segmentation fault. 0x420807a6 in strcpy () from /lib/i686/libc.so.6 (gdb)
A programunkkal tesztelve az elso futtatásra az alábbi eredményt kapjuk: (gdb) set env LD_PRELOAD=libnjamd.so (gdb) r Starting program: /home/tomcat/buggy
DEBUG 46 Program received signal SIGSEGV, Segmentation fault. 0x420807a6 in strcpy () from /lib/i686/libc.so.6 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so.6 #1 0x080484c8 in main () at buggy.c:18 #2 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb)
Ebbol látható, hogy a kis túlírást elsore megtalálta a 18. sorban. Tehát az alapértelmezett memória illesztési beállítása olyan, hogy az 1 byte-os hibákat is érzékeli. Tovább tesztelve megtalálja a 23. sorban a felszabadított terület írását, a 24-ben az olvasását, és a 28. sorban a nagy túlírást. Az alulírást is detektálhatjuk a 32. sorban, ha az NJAMD_PROT környezeti változót strict értékre állítjuk. (gdb) set env NJAMD_PROT=strict (gdb) r Starting program: /home/tomcat/buggy 1: 3: Program received signal SIGSEGV, Segmentation fault. 0x08048622 in main () at buggy.c:32 32 *(dyn-1)='\0'; (gdb)
Azonban az Electric Fence-el ellentétben az NJAMD ilyenkor is bizonyos mértékig érzékeli a túlírást, amikor felszabadítjuk a lefoglalt területet. (gdb) set env NJAMD_PROT=strict (gdb) r Starting program: /home/tomcat/buggy 1: 12345 NJAMD/free: heap corruption. Try using the overflow option to pinpoint source of error ... Program received signal SIGSEGV, Segmentation fault. 0x42029331 in kill () from /lib/i686/libc.so.6 (gdb) where #0 0x42029331 in kill () from /lib/i686/libc.so.6 #1 0x4202911a in raise () from /lib/i686/libc.so.6 #2 0x40029b12 in nj_free_init () from /usr/lib/libnjamd.so.0 #3 0x40029ff1 in __nj_sunderflow_free () from /usr/lib/libnjamd.so.0 #4 0x4002ce2c in free () from /usr/lib/libnjamd.so.0 #5 0x08048609 in main () at buggy.c:20 #6 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb)
DEBUG 47 4.2.2.2 Memory leak detektálás Az NJAMD képes a memory leak problémák detektálására is. Ehhez az NJAMD_DUMP_LEAKS_ON_EXIT környezeti változó használatára van szükség. Ebben a változóban megadhatjuk a visszakeresési szintet is. Az alapértelmezett maximum 3. Egy példa a használatára: $ LD_PRELOAD=libnjamd.so NJAMD_PROT=none NJAMD_DUMP_LEAKS_ON_EXIT=3 ./buggy 1: 3: 4: 5: 12345 6: 12345 7: 12345 8: 12345 0x4213b000-0x4213d000: Aligned len 5 Allocation callstack: called from ./buggy[0x8048603] called from ./buggy(__libc_start_main+0x95)[0x42017589] called from ./buggy(free+0x41)[0x80484e1] Not Freed NJAMD totals: Allocation totals: Leaked User Memory: Peak User Memory: NJAMD Overhead at peak: Peak NJAMD Overhead: Average NJAMD Overhead: Address space used: NJAMD Overhead at exit:
2 total, 1 leaked 5 bytes 5 bytes 3.995 kB 3.995 kB 3.995 kB per alloc 16.000 kB 3.995 kB
Mint látható megtalálta az 1 darab 5 byte-os memóriaszivárgásunkat. Emellett további statisztikai információkat is kapunk. 4.2.2.3 Az NJAMD kapcsolói Az NJAMD az alábbi környezeti változókat használja, mint a muködését befolyásoló kapcsolókat: Változó NJAMD_PROT
Érték overflow
Jelentés Alapértelmezett érték. A túlírások ellen véd. strict Az összes alulírás ellen véd. underflow A nagyobb méretu alulírások ellen véd. none Csak memory leak ellenorzés van.
DEBUG 48 Változó NJAMD_CHK_FREE
Érték segv
Jelentés Az alapértelmezett metódus a felszabadított memória védelmére. Segmentation fault hibajelzés nélkül. error Hibajelzést is ad. none Kikapcsolja a felszabadított memória védelmét. nofree Kikapcsolja a memória felszabadítást. NJAMD_NO_FREE_INFO Ha 1, akkor kikapcsolja a felszabadított területekrol az információtárolást. NJAMD_ALIGN Memória illesztés beállítása. NJAMD_DUMP_LEAKS_ON_EXIT Memory leak információk megjelenítése. Értéke a visszakeresés mértéke. Alapértelmezett maximum: 3 NJAMD_DUMP_STATS_ON_EXIT Ha 1, akkor a program futásának végén statisztikai információkat közöl. NJAMD_DUMP_CORE hard Ha korrekt és teljes core dump állományt szeretnénk. soft Ha a core file mellett statisztikai információkat is szeretnénk. NJAMD_PERSISTENT_HEAP Ha 1, akkor lementi a heap tartalmát egy állományba. NJAMD_ALLOW_MALLOC_0 Ha 1, akkor engedélyezi a 0 méretu allokálást és felszabadítást. NJAMD_ALLOW_FREE_0
4.2.2.4 Összegzés Mint láthattuk az NJAMD rendelkezik mindazokkal a funkciókkal, mint az Electric Fence, és azon felül még további szolgáltatásokkal is. Ezért használata célszerubb. Azonban a memory leak információk nehezen kezelhetoek, és ez sem jelent megoldást a lokális és globális változók hibáinak detektálására. A vizsgált jellemzok állításával hangolhatjuk a rendszer sebességét, teljesítményét.
4.2.3 mpr Az mpr egy másik malloc eszköz a memory leak-ek megtalálására. Funkcionalitását tekintve egy memória allokáció profiler a C/C++ programok számára. Egyszeru, brute force megoldást alkalmaz a memória szivárgások felderítésére. A futás közben logolja az összes malloc és free hívást, hogy megtalálja a nem felszabadított részeket. Alapja egy könyvtár állomány, amelyet hasonlóan a korábbiakhoz hozzálinkelhetünk a programunkhoz az –lmpr kapcsolóval gcc -ggdb -Wall -o buggy buggy.c -lmpr
DEBUG 49 vagy az LD_PRELOAD változóval: LD_PRELOAD=libmpr.so
Azonban ezzel szemben a használata az mpr segédprogrammal javasolt: mpr ./buggy
Ilyenkor a futás közbeni eseményekrol egy log állományt készít. Ennek neve log..gz. Azonban az MPRFI környezeti változóval megváltoztathatjuk a log tárolásának módját. Az mprmap program segítségével megjeleníthetjük az állomány tartalmát olvasható formában. Ilyenkor megtekinthetjük az össze memóriafoglalást és felszabadítást. $ mprmap -l ./buggy log.27391.gz m:__register_frame():__libc_global_ctors():init():_dl_init_interna l():24:134518768 m:main(buggy.c,17):__libc_start_main():5:134518800 f:main(buggy.c,20):__libc_start_main():134518800 m:main(buggy.c,27):__libc_start_main():5:134518800 f:__deregister_frame():_fini():_dl_fini():exit():__libc_start_main ():134518768
Az mprsize statisztikát jelenít meg a memóriafoglalásról: $ mprsize log.27391.gz 5 2 24 1
10 24
29.41% 70.59%
Az mprleak kiszuri a logból a memóriaszivárgásokat. Ezt az mprmap programmal olvasható formába alakíthatjuk: $ mprleak log.27391.gz | mprmap -l ./buggy m:main(buggy.c,27):__libc_start_main():5:134518800
Mint láthatjuk meg is találta a programunk hibáját.
4.2.4 MemProf A MemProf szintén egy memória használatot monitorozó eszköz, azonban az mpr-el szemben több elonyt is tartalmaz. Egyrészt a grafikus kezeloi felületén a program futása közben követhetové teszi az egyes függvények memória használatát. Másrészt folyamatosan ellenorzi a memóriát, hogy talál-e olyan blokkokat, amelyekre már nincs hivatkozás. Ez az memory leak-ek egy tipikus esete, amikor elfelejtjük felszabadítani a memóriát, és a hivatkozást is töröljük, módisítjuk. Ezzel a programmal ezek a problémák menet közben is megfigyelhetoek, így egy elég komoly segítséget jelent a hibakeresésben. Kezeloi felülete a következo ábrán látható:
DEBUG 50
Ábra 4-3 MemProf
4.3 Rendszerhívások monitorozása: strace Az strace egy hasznos diagnosztikai, debuggolási eszköz. Általános esetben az strace lefuttatja a paraméterként megadott programot, és monitorozza a processz rendszerhívásait és a szignálokat, amelyeket kap. Az összes rendszerhívást a paramétereivel együtt a standard hibakimenetre, vagy a megadott kimeneti állományba írja. Használható a rendszer muködésének vizsgálatához, megismeréséhez is.
4.4 További hasznos segédeszközök A lint segédprogram a hibakeresésben lehet segítségünkre. Használata: lint myprogram.c
A lint szolgáltatásai között megemlítjük a típusellenorzést, paraméterátadást, lehetséges memóriahibák felderítését.
DEBUG 51 Az indent segédprogram olvashatóvá teszi a C forráskódot szóköz, tabulátor és hasonló jellegu karakterek beiktatásával. Használata: indent myfile.c
Bináris file-ok analízisénél jól jöhet, ha hexadecimálisan is meg tudunk jeleníteni egy file-t. Ezt a hexdump filename
segédprogrammal tehetjük meg.
ÁLLOMÁNY ÉS I/O KEZELÉS 52
5. Állomány és I/O kezelés Az állomány a Unix világ egyik legáltalánosabb eroforrás absztrakciója. Az olyan eroforrások, mint a memória, lemez terület, eszközök, IPC csatornák, mind állományként reprezentálódnak a Unix rendszereken. Az interfészek általánosításával egyszerusödik a programozók feladata, illetve egyes eroforrások kezelése kompatibilis egymással. A következo állománytípusokat különböztetjük meg: Egyszeru állomány Az egyszeru állományok azok, amelyekre eloször gondolnánk az állomány szó hallatán. Vagyis byte-ok sorozata, adatok, kód, stb. Pipe A csovezeték a Unix legegyszerubb interprocess communication (IPC) mechanizmusa. Általában egy processz információkat ír bele, míg egy másik kiolvassa. A shell a pipe-okat használja az I/O átirányításokra, míg sok más program az alprocesszeikkel való kommunikációra. Két típusát különböztetjük meg: unnamed és named. Az unnamed pipe-ok akkor kreálódnak, amikor szükség van rájuk, és amikor mindkét oldal lezárja, akkor eltunnek. Azért hívják névtelennek, mert nem látszódnak a file rendszerben, nincs nevük. A named pipe-ok ezzel szemben egy file névvel látszódnak a file rendszerben, és a processzek ezzel a névvel férhetnek hozzá. A pipe-okat FIFO-nak is nevezik, mivel az adatok FIFO rendszerben közlekednek rajta. Könyvtár A könyvtár speciális állomány, amely a benne lévo állományok listáját tartalmazza. A régi Unix rendszerekben az implementációk megengedték, hogy a programok az egyszeru állományok kezelésére szolgáló függvényekkel hozzá is férjenek. Azonban a könnyebb kezelhetoségért egy speciális rendszerhívás készlet került az újabb rendszerekbe. Ezeket késobb tárgyaljuk. Eszközök A legtöbb fizikai eszköz a Unix rendszereken mint állomány jelenik meg. Két eszköztípust különböztetünk meg: blokk és karakter típusú eszközök. A blokkos eszköz állomány egy olyan hardware eszközt reprezentál, amelyet nem olvashatunk byte-osan, csak byte-ok blokkját. A Linux a blokkos eszközöket speciálisan kezeli, és általában file rendszert tartalmaznak. A karakteres eszköz byte-onként olvasható. A modemek, terminálok, printerek, hangkártyák, és az egér mind- mind karakteres eszköz. Tradicionálisan az eszköz leíró speciális állományok a /dev könyvtárban találhatóak. Szimbolikus link A szimbolikus link (röviden simlink) egy speciális állomány, amely egy másik állomány elérési információit tartalmazza. Amikor megnyitjuk, a rendszer érzékeli, hogy szimbolikus link, kiolvassa az értékét, és megnyitja a
ÁLLOMÁNY ÉS I/O KEZELÉS 53 hivatkozott állományt. Ezt a muveletet a szimbolikus link követésének hívjuk. A rendszerhívások alapértelmezett esetben követik a szimbolikus linkeket. Socket A socket-ek mint a pipe-ok IPC csatornaként használhatóak. Flexibilisebbek, mint a pipe-ok, mert lehetové teszik két különbözo gépen futó processz között is a kommunikációt. Sok operációs rendszerben egy az egyes összerendelés van az állományok és az állománynevek között. (Minden állománynak egy neve van, és minden állománynév egy állományt jelöl.) A Unix szakított ezzel a koncepcióval a nagyobb felxibilitás érdekében. Az állomány egyetlen egyedi azonosítója az inode-ja (information node). Az állomány inode-ja tartalmaz minden információt az állományról, beleértve a jogokat, méretét, a hivatkozások számát. Két inode típust különböztetünk meg. Az in-core inode az, amelyikkel nekünk dolgunk lesz. Minden megnyitott állományhoz tartozik egy ilyen, a kernel a memóriában tartja, és minden állománytípushoz egyforma. A másik típus az on-disk inode, amely a lemezen tárolódik. Minden állomány rendelkezik eggyel. A tárolása, struktúrája a file rendszer függvénye. Amikor egy processz megnyitja az állományt az on-disk inode betöltodik a memóriába és átkonvertálódik in-core inode-ra. Amikor az in-core inode módosul, az visszakonvertálódik on-disk inode-ra, és a file rendszeren tárolódik. A két inode típus szinkronizálását a kernel végzi, a legtöbb rendszerhívás azzal végzodik, hogy mindkettot frissíti. Az on-disk és az in-core inode nem teljesen ugyanazokat az információkat tartalmazza. Például csak az in-core inode könyveli az adott állományhoz kapcsolódó processzek számát. Néhány állománytípus, mint például az unnamed pipe, nem rendelkezik on-disk inode-al. Az állományok neve egy adott könyvtárban csak hivatkozások az on-disk inode-okra. A file név lényegében egy mutató. Az on-disk inode tartalmazza a rá hivatkozó file nevek számát. Ezt hívjuk link count-nak. Amikor egy állományt törölni próbálunk, akkor ez a szám csökken eggyel. Ha eléri a 0 értéket, és egyetlen processz sem tartja éppen nyitva, akkor ténylegesen törlodik, és a hely felszabadul. Ha egy processz nyitva tartja, akkor csak akkor szabadul fel a tárolóhely, amikor használatát befejezi, és bezárja. Az eddig megismert jellemzok az alapján néhány érdekes példa: ?? Több processz is nyitva tarthat egy állományt, ami valójában meg csak nem is létezett soha (mint pl. a pipe) ?? Létrehozunk egy állományt, letöröljük a hivatkozást rá, és meg ugyanúgy írhatunk bele, vagy olvashatunk belole. ?? Ha az egyik állományt módosítjuk egy másikban rögtön látható a változás, ha mindkét név ugyanarra az inode-ra hivatkozik. Ez így elso hallásra érdekes és zavarba ejto lenne, azonban ha ismerjük az állománykezelés mögötti mechanizmusokat, akkor mind értheto.
ÁLLOMÁNY ÉS I/O KEZELÉS 54
5.1 Egyszeru állománykezelés A Linux számos file kezelo rendszerhívással legegyszerubb, legáltalánosabbakat nézzük.
rendelkezik.
Kezdetként
a
5.1.1 Az állományleíró Amikor egy processz hozzáférést szerez egy állományhoz, vagyis megnyitja, a kernel egy állományleíró t ad vissza, amelynek a segítségével a processz a késobbi muveletek során hivatkozhat az állományra. Az állományleírók kicsi, pozitív egész számok, amelyek a processz által megnyitott állományok tömbjének indexeként szolgál. Az elso három leíró (0, 1 és 2) speciális célokat szolgál, minden processz lefuttatásakor létrejön. Az elso (0) a standard bemenet leírója, ahonnan a program az interaktív bemenetet kapja. A második (1) a processz standard kimenete, a program futása során keletkezett kimenet jó része ide irányítódik. A hibajelentések kimeneteként szolgál a harmadik (2) standard hiba kimenet leírója. A standard C könyvtár követi ezt a szerkezetet, így a kimenet, bemenet függvényei automatikusan használják ezeket a leírókat. Az unistd.h header file tartalmazza az STDIN_FILENO, STDOUT_FILENO, és STDERR_FILENO makrókat, amelyekkel ezeket a leírókat elérhetjük a programunkból.
5.1.2 Hozzáférés állományleíró nélkül Az állománykezelo rendszerhívások egyik csoportját alkotják azok a függvények, amelyek állományleírókat használnak. A rendszerhívások másik alakja, amikor paraméterként az állomány nevét adjuk meg, és így kérünk az állomány inode-jára vonatkozó muveleteket.
5.1.3 Állományok megnyitása Bár a Linux több állományfajtát is támogat, a legáltalánosabban használtak az egyszeru állományok. A programok, a konfigurációs file-ok, és az adat file-ok mind az állományok ezen csoportjába tartoznak. Ezen állományok megnyitására két függvény áll rendelkezésünkre: #include <sys/types.h> #include <sys/stat.h> #include int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int creat(const char *pathname, mode_t mode);
ÁLLOMÁNY ÉS I/O KEZELÉS 55 Az open() függvény a megadott állomány leírójával tér vissza. Ha értéke kisebb, mint 0, akkor az állomány megnyitása meghiúsult, ilyenkor a szokásos módon az errno változó tartalmazza a hibakódot. A flags paraméter tartalmazza a hozzáférési igényünket, illetve korlátozza a késobb használható függvények körét. Értéke az alábbi választási lehetoségek közül az egyik: O_RDONLY (csak olvasható), O_RDWR (írható és olvasható), O_WRONLY (csak írható). Továbbá ezt kiegészíti egy, vagy több az alábbi opciók közül (bites VAGY kapcsolattal): Opció O_CREAT O_EXCL O_NOCTTY
O_TRUNC O_APPEND O_NONBLOCK
O_SYNC
Jelentés Ha az állomány nem létezik, akkor létrehozza egyszeru fileként. Az O_CREAT opcióval együtt használatos. Az open() hibával tér vissza, ha az állomány már létezik. A megnyitott file nem lesz a processz kontrol terminálja. Csak akkor érvényesül, ha a processz nem rendelkezik kontrol terminállal, és egy terminál eszközt nyit meg. Ha az állomány létezik, akkor tartalma törlodik és az állomány méret 0 lesz. Az állomány végéhez fuzi íráskor az adatokat. (A véletlen hozzáférés ilyenkor is engedélyezett.) Az állománymuveletek nem blokkolják a processzt. Az egyszeru állományok esetén a muveletek mindig várakoznak, mert a lemezmuveletek gyorsak. Azonban egyes állománytípusok esetén a válaszido nem meghatározott. Például egy pipe esetén ha nincs bejövo adat, az normál esetben blokkolja a processzt az olvasás muveletnél. Azonban ezzel az opcióval ilyenkor azonnal visszatér, és 0 byte beolvasását jelzi. Normál esetben a kernel write cache-t alkalmaz, amely nagyban javítja a rendszer teljesítményét. Azonban ez adatvesztést eredményezhet. Ezzel az opcióval elérheto, hogy az írási muvelet végére az adat valóban tárolva legyen a lemezen. Ez például adatbázisok esetén nagyon fontos.
A mode paraméter tartalmazza a hozzáférési jogosultságokat új állományok létrehozása esetén. Az open() függvénynél csak az O_CREAT opció esetén van értelme. A creat() függvény egyenértéku az alábbi függvénnyel: open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode)
5.1.4 Állományok bezárása Azon kevés függvények közé tartozik az állományok bezárása, amely az összes állománytípusra megegyezik: #include
ÁLLOMÁNY ÉS I/O KEZELÉS 56
int close(int fd);
Elég egyszeru metódus. Azonban egy fontos dolgot érdemes vele kapcsolatban megjegyezni: lehet sikertelen. Néhány file rendszer nem tárolja le az állományok utolsó darabját, amíg az állományt be nem zárjuk. (pl. az NFS) Ha ez a végso írási muvelet sikertelen, akkor a close() függvény hibával tér vissza. Vagyis amennyiben nem használjuk a szinkron írási módot (O_SYNC), akkor mindig ellenoriznünk kell a visszatérési értékét, még ha nagyon ritkán is következik be hiba.
5.1.5 Írás, olvasás, állományban mozgás Van néhány módja az állományból olvasásnak, illetve írásnak. A legegyszerubbet vesszük itt. A két muvelet lényegében egyforma: #include ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
Mindketto tartalmaz egy állományleírót fd, egy mutatót az adatbufferre buf, és a buffer hosszát count. A read() függvény beolvassa az adatokat az állományból és a bufferbe írja. A write() a count mennyiségu byte-ot a bufferbol az állományba írja. Mindkét függvény az átvitt byte-ok számát adja vissza, vagy –1-et hiba esetén. /* hwwrite.c -- writes "Hello World!" to file hw in the current directory */ #include #include #include #include #include
<errno.h> <stdio.h> <stdlib.h>
int main(void) { int fd; /* open the file, creating it if it's not there, and removing its contents if it is there */ if ((fd = open("hw", O_TRUNC | O_CREAT | O_WRONLY, 0644)) < 0) { perror("open"), exit(1); } /* the magic number of 13 is the number of characters which will be written */ if (write(fd, "Hello World!\n", 13) != 13) { perror("write"); exit(1); } close(fd); return 0; }
ÁLLOMÁNY ÉS I/O KEZELÉS 57 A Unix állományokat két részre oszthatjuk. A seek-elheto és a nem seek-elheto. A nem seek-elheto állományok FIFO csatornák, amelyek nem támogatják a véletlen hozzáférést, és az adatok nem újraolvashatóak, illetve nem felülírhatóak. A seekelheto állományok lehetové teszik az írási vagy olvasási muveleteket az állomány teljes területén. A pipe-ok, karakteres eszközök ne m seek-elheto állományok, a blokkos eszközök, és az egyszeru állományok seek-elhetoek. Amennyiben egy program ki akarja használni a véletlen hozzáférés lehetoségét, az írási és olvasási muveletek elott pozícionálnia kell az állományon belüli aktuális pozíciót. Erre szolgál az lseek() függvény: #include off_t lseek(int fd, off_t offset, int whence);
Az fd leírójú állomány aktuális pozícióját elmozgatja offset byte-nyit a whence pozícióhoz képest, ahol a whence értéke az alábbi lehet: Opció SEEK_SET SEEK_CUR SEEK_END
Jelentés Az állomány eleje. Az aktuális pozíció. Az állomány vége.
Az utóbbi két érték (SEEK_CUR és SEEK_END) esetén az offset értéke negatív is lehet. Ilyenkor természetesen a pozíció az ellenkezo irányba módosul. Például ha az állomány végétol vissza 5 byte- nyira szeretnénk állítani a mutatót: lseek(fd, -5, SEEK_END);
Az lseek() visszatérési értéke az aktuális pozíció az állomány elejétol, illetve –1 hiba esetén. Ezáltal például az size=lseek(fd, 0, SEEK_END);
egy egyszeru megoldás az állomány méretének kitalálására. Habár a különbözo processzek, amelyek egyszerre használják az állományt, nem módosítják egymás aktuális pozíció értékét, ez nem jelenti azt, hogy biztonságosan tudják párhuzamosan írni a file-t, könnyen felülírhatják egymás adatait. Amennyiben szükséges, hogy több processz is írjon az állományban, ezt párhuzamosan csak hozzáfuzéssel tehetik meg. Ilyenkor az O_APPEND opció gondoskodik arról, hogy az írásmuveletek atomiak legyenek. A POSIX szabvány lehetové teszi, hogy az aktuális pozíciót nagyobb értékre állítsuk, mint az állomány vége. Ilyenkor az állomány a megadott értékure növekszik, és oda kerül az állomány vége. Az egyetlen bökkeno a dologban, hogy a legtöbb rendszer nem allokál a kimaradt területnek lemez helyet és nem is írja ki. Csak az állomány logikai mérete módosul. Az állomány ezen területeit lyukaknak nevezzük. A lyuk területérol olvasás egy adag 0-t ad vissza, míg az írási muvelet hibát eredményez. Ebbol következik, hogy az lseek() függvényt nem használhatjuk lemez terület
ÁLLOMÁNY ÉS I/O KEZELÉS 58 allokálásra. Az ilyen lyukas állományok leginkább csak akkor használatosak, amikor az adat pozíciója is információt hordoz és takarékoskodunk a lemez területtel. Például a hash táblák.
5.1.6 Részleges írás, olvasás Habár mind a read(), mind a write() függvény paraméterei közt megadjuk a byte-ok számát, semmi nem garantálja, hogy tényleg az adott mennyiségu byte feldolgozásra kerül. Illetve az olvasás esetén, amikor a megadott mennyiségu adatot sikerül beolvasni, elofordulhat, hogy még továbbiak várnak. A read() függvény muködése attól is függ, hogy megadtuk-e az O_NONBLOCK opciót. Sok file rendszeren az O_NONBLOCK opció nem változtat semmit a végrehajtás muveletében. Ezeken a file rendszereken a muvelet nem igényel számottevo idot. Ezeket hívjuk fast file-oknak. A nem blokkolt olvasásra még visszatérünk a párhuzamos állománykezelés témakörében. Most nézzünk egy példát a részleges olvasásra: /* cat.c -- simple version of cat */ #include <stdio.h> #include /* While there is data on standard in (fd 0), copy it to standard out (fd 1). Exit once no more data is available. */ int main(void) { char buf[1024]; int len; /* len will be >= 0 while data is available, and read() is successful */ while ((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0) { if (write(1, buf, len) != len) { perror("write"); return 1; } } /* len was <= 0; If len = 0, no more data is available. Otherwise, an error occurred. */ if (len < 0) { perror("read"); return 1; } return 0; }
5.1.7 Állományok rövidítése Az állományok végére, ha írunk, akkor a rendszer automatikusan növeli a hosszát. Felül is írhatjuk az adatokat. Azonban mit tegyünk, ha az állomány végén található információkra egyáltalán nincs szükségünk. Kell lennie egy metódusnak, amellyel az állományokat összenyomhatjuk. Erre szolgálnak a következo függvények:
ÁLLOMÁNY ÉS I/O KEZELÉS 59
#include int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);
Használatukkal az állomány méretét a length paraméterben megadottra módosíthatjuk. Az állomány levágott része elveszik. Azonban ha a length értéke nagyobb, mint az állomány aktuális mérete, akkor megnöveli a virtuális méretet úgy, hogy az adott ponton lyuk keletkezik az állományban.
5.2 Inode információk 5.2.1 Inode információk kiolvasása A fejezet elején már tárgyaltuk az inode-ok fogalmát. Lényegében egy leíró adatstruktúra, amely az adott állomány paramétereit tartalmazza. A Linux az alábbi 3 függvényt támogatja ezen információk eléréséhez: #include int stat(const char *file_name, struct stat *buf); int lstat(const char *file_name, struct stat *buf); int fstat(int fd, struct stat *buf);
Az elso függvény, a stat(), visszaadja a file_name paraméter által megadott állomány inode információit. Amennyiben szükséges követi a szimbolikus linkeket. Ha ez utóbbi funkciót el szeretnénk kerülni, akkor az lstat() függvényt kell használnunk. A legutolsó változat, a fstat() függvény, megnyitott állományok inode információinak elérését teszi lehetové. A struct stat az alábbi elemeket tartalmazza : Típus dev_t ino_t mode_t nlink_t uid_t gid_t dev_t
Mezo st_dev st_ino st_mode st_nlink st_uid st_gid st_rdev
off_t unsigned long unsigned long time_t time_t time_t
st_size st_blksize
Leírás Az állományt tartalmazó eszköz azonosítója. Az állomány on-disk inode száma. Az állomány jogai és típusa. A referenciák száma erre az inode-ra. Az állomány user ID-ja. Az állomány group ID-ja. Ha az állomány speciális eszköz leíró, akkor ez a mezo tartalmazza a major és minor azonosítót. Az állomány mérete byte-okban. A file rendszer blokk mérete.
st_blocks
Az állomány által allokált blokkok száma.
st_atime st_mtime st_ctime
A legutolsó állományhoz való hozzáférés idopontja. A legutolsó állomány módosítás idopontja. A legutolsó változtatás idopontja az állományon, vagy az inode információn.
ÁLLOMÁNY ÉS I/O KEZELÉS 60
5.2.2 Jogok lekérdezése Habár az inode leíró struktúra st_mode eleme tartalmazza az állomány jogait, amellyel meghatározhatjuk, mihez van jogunk és mihez nincs, azonban ezen információk kinyerése nem olyan egyszeru, mint szeretnénk. Ugyanakkor a kernel már rendelkezik a kód részlettel, amely meghatározza a hozzáférési jogainkat. Egy egyszeru rendszerhívással le is kérdezhetjük ezeket: #include int access(const char *pathname, int mode);
A mode paraméter a következo értékekbol egyet vagy többet is tartalmazhat: Érték F_OK R_OK W_OK X_OK
Jelentés Az állomány létezik-e, elérheto-e. A processz olvashatja-e az állományt. A processz írhatja-e az állományt. A processz futtathatja-e az állományt. (Könyvtár esetén keresési jog.)
5.2.3 Jogok állítása Az állományok hozzáférési jogai a chmod() rendszerhívással módosíthatóak. #include <sys/stat.h> int chmod(const char *path, mode_t mode); int fchmod(int fd, mode_t mode);
Habár a chmod() paramétere az állomány elérési útvonala és neve, ne felejtsük el, hogy valójában inode információkat állítunk. Vagyis ha több hivatkozás van az adott állományra, akkor a többi jogai is változnak. A mode paraméter a hozzáférés vezérlo bitek kombinációja. Tipikus, hogy a programozók oktálisan adják meg. Csak a root felhasználó és az állomány tulajdonosa jogosult a jogok állítására. Mások ha ezzel a függvényhívással próbálkoznak, akkor EPERM hibaüzenetet kapnak.
5.2.4 Tulajdonos és csoport állítás Mint a jogok az állomány tulajdonosa és csoportja is az inode struktúrában tárolódik. Egy rendszerhívás szolgál mindketto állítására. #include int chown(const char *path, uid_t owner, gid_t group); int lchown(const char *path, uid_t owner, gid_t group); int fchown(int fd, uid_t owner, gid_t group);
ÁLLOMÁNY ÉS I/O KEZELÉS 61 Az owner és group paraméterek adják meg az új tulajdonost és csoportot. Ha bármelyikük is -1, akkor nem változik az érték. Csak a root jogosult a tulajdonos állítására. A tulajdonos állítása esetén biztonsági okokból a setuid bit mindig törlodik. Mind a tulajdonos, mind a root felhasználó módosíthatja az állomány csoportját, azonban a tulajdonosnak tagnak kell lennie az új csoportban.
5.2.5 Idobélyeg állítás Az állomány tulajdonosa állíthatja az mtime és atime információkat. Ez lehetové teszi az olyan archiváló programoknak, mint amilyen a tar, hogy visszaállíthassák az állomány idobélyegét arra az értékre, amely az archiválásnál volt. A ctime értéke ilyenkor frissítodik természetesen, és nem módosítható. Két lehetoségünk van az idobélyeg információk állítására. Az utime() és az utimes(). Az utime() a System V rendszerbol származik, onnan adoptálta a POSIX. Az utimes() a BSD rendszerekbol ered. A két függvény egyenértéku, csak az idobélyeg megadása különbözik. #include int utime(const char *filename, struct utimbuf *buf); #include <sys/time.h> int utimes(char *filename, struct timeval *tvp);
A POSIX utime() által használt struct utimbuf (amely az utime.h állományban van definiálva) struktúra a következo : struct utimbuf { time_t actime; time_t modtime; }
A BSD utimes() függvénye az idobélyeget a struct timeval struktúrával írja le. (Ez a sys/time.h állományban van definiálva.) struct timeval { long tv_sec; long tv_usec; }
A tv_sec elem az új atime értéket tartalmazza, a tv_usec pedig az új mtime-ot. Ha bármelyik függvénynek második paraméterként NULL értéket adunk meg, akkor mindkét idobélyeg az aktuális idopontra állítódik.
ÁLLOMÁNY ÉS I/O KEZELÉS 62
5.3 Könyvtár bejegyzések módosítása Ne felejtsük el, hogy a könyvtárbejegyzések csak egyszeru mutatók az on-disk inodeokra. Minden lényeges információ az inode-okban tárolódik. Az open() függvény lehetové teszi a processznek, hogy új, egyszeru állományokat hozzon létre. Azonban további függvényekre van szükség egyéb típusú állományok létrehozásához, vagy a könyvtárbejegyzések módosításához. Ebben a fejezetben a szimbolikus linkek, az eszközkezelo állományok, és a FIFO bejegyzések kezeléséhez szükséges rendszerhívásokkal foglalkozunk.
5.3.1 Eszköz állományok és Pipe bejegyzések A processzek az mknod() rendszerhívás segítségével hozhatnak létre named pipe-okat és eszköz állományokat a file rendszeren. #include #include int mknod(const char *pathname, mode_t mode, dev_t dev);
A pathname a létrehozandó bejegyzés neve. A mode a hozzáférési jogokat (amelyet az umask értéke módosít) és az állomány típusát (S_IFIFO, S_IFBLK, és S_IFCHR) határozza meg. Az utolsó dev paraméter tartalmazza a major és minor azonosítóit a létrehozandó eszköz állománynak. Az eszköz típusa és major száma megadja a kernelnek, hogy melyik eszközkezelot hívja meg, a minor szám az eszközmeghajtón belüli szelektálásra szolgál, ha az adott eszközkezelo több eszközt szolgál ki. Csak a root hozhat létre eszköz állományokat. A sys/sysmacros.h állomány tartalmaz három makrót a dev paraméter beállítására. A makedev () függvény elso paramétere a major szám, a második a minor szám, és ezekbol létrehozza a dev_t értéket. A major() és minor() makrók a dev_t értékébol kiszámolják az eszköz major, illetve minor számát.
5.3.2 Hard link létrehozása Amikor a file rendszerben több név hivatkozik ugyanarra az inode-ra, az állományokat hard link-nek nevezzük. Minden hivatkozásnak ugyanazon az állományrendszeren kell lennie. Ezek a hivatkozások teljesen egyenértékuek, illetve egyik törlése nem vezet az állomány törléséhez. A link() rendszerhívás szolgál a hard link-ek létrehozására: #include int link(const char *oldpath, const char *newpath);
Az oldpath a már létezo állománynevet tartalmazza, a newpath az új hard link neve. Minden felhaszná ló létrehozhat új linket egy állományra, ha van hozzá olvasási joga, továbbá írás joga a könyvtárra, ahova a linket létrehozza, és futtatási joga a könyvtárra, ahol az eredeti állomány van. Könyvtárakra linket csak a root hozhat létre, azonban ez erosen ellenjavallt.
ÁLLOMÁNY ÉS I/O KEZELÉS 63
5.3.3 Szimbolikus link létrehozása A szimbolikus link a linkek egy flexibilisebb típusa, mint a hard link-ek, azonban nem egyenrangúak az állomány többi hivatkozásával. Vagyis ha az adott állományra az összes hard link-et megszüntetjük, akkor a szimbolikus link a semmibe fog mutatni. A szimbolikus linkek könyvtárak közötti használata általános, továbbá létrehozható partíciók között is. A rendszerhívások többsége automatikusan követi a szimbolikus linkeket, hogy megtalálja az inode-ot. Kivéve ha olya n változatukat használjuk, amelyek ezt eleve kizárják. A következo függvények nem követik Linux alatt a szimbolikus linkeket: ?? ?? ?? ?? ??
chown() lstat() readlink() rename() unlink()
Szimbolikus linkeket a symlink() rendszerhívással hozhatunk létre: #include int symlink(const char *oldpath, const char *newpath);
Paraméterezése megegyezik a hard link-nél tárgyalttal. A szimbolikus linkek értékének megtalálása: #include int readlink(const char *path, char *buf, size_t bufsize);
A buf paraméterbe kerül a szimbolikus link neve, amennyiben belefér. A bufsize tartalmazza a buf hosszát byte-okban. Általában PATH_MAX méretu a buffert használunk, hogy elférjen a tartalom. A függvény egyik furcsasága, hogy nem tesz a string végére ‘\0’ karaktert, vagyis a buf tartalma nem korrekt C string. Visszatérési értéke a visszaadott byte-ok száma, vagy -1 hiba esetén.
5.3.4 Állományok törlése Az állományok törlése tulajdonképpen az inode-ra mutató hivatkozások törlése, és csak akkor jelenti az állomány tényleges törlését, ha az adott hard link az utolsó, amely az állományra hivatkozik. Mivel nincs arra metódus, hogy az állományt egzaktul töröljük, ezért ezt a metódust unlink()-nek nevezik. #include int unlink(const char *pathname);
ÁLLOMÁNY ÉS I/O KEZELÉS 64
5.3.5 Állomány átnevezése Az állomány nevét megváltoztathatjuk egészen addig, amíg az új név is ugyanarra a fizikai partícióra hivatkozik. Ha már egy létezo nevet adtunk meg új névként, akkor eloször azt a hivatkozást megszünteti a rendszer, és utána hajtja végre az átnevezést. A rename()rendszerhívás garantáltan atomi, vagyis minden processz egyszerre csak az egyik nevén láthatja az állományt. (Nincs olyan eset, hogy egyszerre mindkét néven, vayg egyik néven sem látható.) Mivel az állomány megnyitások lényegében az inode-hoz kötodnek, ezért ha más processzek éppen nyitva tartják az állományt, akkor nincs rájuk hatással az átnevezése. Továbbra is muködne, mintha nem is lenne más az állomány neve. A rendszerhívás alakja a következo : #include <stdio.h> int rename(const char *oldpath, const char *newpath);
Meghívás után az oldpath nevu hivatkozás neve newpath lesz.
5.4 Név nélküli Pipe-ok A név nélküli pipe-ok hasonlítanak a névvel rendelkezo pipe-okra, azonban a file rendszeren nem hozunk létre hivatkozást rájuk. Nincsen nevük, és automatikusan megsemmisülnek, ha minden leírójukat bezárjuk. Majdnem kizárólag a szülo és gyerek processzek közötti kommunikációra használjuk. Egy névtelen pipe-ot a következo rendszerhívással hozhatunk létre: #include int pipe(int fds[2]);
Két állományleíróval tér vissza, az egyik csak olvasható (fds[0]), a másik csak írható (fds[1]).
5.5 Könyvtármuveletek A Linux, mint sok más operációs rendszer, a könyvtárakat az állományok rendszerezésére használja. A könyvtárak állományokat és további könyvtárakat tartalmaznak. Minden Linux rendszernek van egy, úgynevezett gyökér könyvtára, más néven /, amely az egész könyvtár struktúra eredete, gyökere.
5.5.1 Munkakönyvtár A getcwd() függvény lehetové teszi a processzek számára, hogy megállapítsák az aktuális munka könyvtárukat. #include char *getcwd(char *buf, size_t size);
ÁLLOMÁNY ÉS I/O KEZELÉS 65 Az elso paraméter, buf, egy bufferre mutat, amely a könyvtár elérési útvonalát tartalmazza. Ha a könyvtár leírása hosszabb, mint size-1 byte (a -1 azért szükséges, hogy egy ‘\0’ karakterrel le tudja zárni), akkor a függvény ERANGE hibával tér vissza. Ha a hívás sikeres, akkor a buf tartalmazza az információt, hiba esetén értéke NULL. Ha nem tudjuk elore a szükséges buffer méretet, akkor célszeru többször, egyre nagyobb bufferrel próbálkozni, amíg nem lesz sikeres a függvény. Erre a Linux támogat egy megoldást. Ha a paraméter aktuális értéke NULL, akkor a szükséges méretu helyet allokálja neki és visszaadja az értéket. Azonban ebben az esetben ne felejtsük el free()- vel felszabadítani.
5.5.2 Könyvtárváltás Két rendszerhívás áll rendelkezésünkre a könyvtárváltáshoz: #include int chdir(const char *path); int fchdir(int fd);
Az elso az állománynevet használja argumentumként, a második egy megnyitott könyvtár leíróját. Mindkét esetben a megadott könyvtár lesz az új munka könyvtár. Ezek a függvények akkor térnek vissza hibával, ha a megadott könyvtár valójában nem könyvtár, vagy pedig a processznek nincsenek meg a jogai a használatához.
5.5.3 Root könyvtár módosítása Habár a rendszernek csak egyetlen igazi root könyvtára van, a “/” könyvtár jelentése változhat az egyes processzeknél. Ezt a lehetoséget általában védelmi okokból használják a programfejlesztok. Így a program a jogai ellenére sem tud hozzáférni a teljes file rendszerhez, csak az új gyökér könyvtár alkönyvtáraihoz. Például egy ftp daemon esetén, ha a root könyvtárát a /home/ftp könyvtárra állítjuk, akkor a “/” könyvtár ezt a könyvtárat jelenti, és a “/..” könyvtárváltás sem vezet ki ebbol az alrészbol. A processz egyszeruen állíthatja a root könyvtárát a következo rendsze rhívással, ha rendelkezik rendszergazdai jogosultságokkal: #include int chroot(const char *path);
A paraméternek megadott könyvtár lesz az új root könyvtár. Ez a rendszerhívás ugyanakkor nem módosítja az aktuális munka könyvtárat. Így a processz továbbra is hozzáférhet annak tartalmához, illetve onnan relatíve minden más könyvtárhoz. Ezért a chroot() függvényhívást követoen általában a munka könyvtárat is az adott könyvtárra állítjuk a chdir(“/”) rendszerhívással. Ha ezt nem tennénk, az valószínuleg biztonsági problémákhoz vezetne.
ÁLLOMÁNY ÉS I/O KEZELÉS 66
5.5.4 Könyvtár létrehozása Új könyvtárat az alábbi rendszerhívással hozhatunk létre: #include int mkdir(const char *pathname, mode_t mode);
A paraméterként megadott pathname könyvtárat hozza létre a mode változóban megadott jogokkal (amelyet az umask értéke módosít). Ha a pathname egy már létezo állomány, vagy a megadott elérési út valamely eleme nem könyvtár vagy szimbólikus link egy könyvtárra, akkor a rendszerhívás hibával tér vissza.
5.5.5 Könyvtár törlése Könyvtárat letörölni a következo egyszeru rendszerhívással tudunk : #include int rmdir(const char *pathname);
Ahhoz, hogy muködjön a könyvtárnak üresnek kell lennie. Ellenkezo esetben ENOTEMPTY hibaüzenetet kapunk vissza.
5.5.6 Könyvtártartalom olvasása Általános, hogy egy programnak szüksége van egy adott könyvtár állományainak listájára. A Linux erre a problémára is ad egy függvénygyujteményt, amely lehetové teszi a könyvtárbejegyzések kezelését. A könyvtárakat megnyitni és bezárni a következo rendszerhívásokkal tudjuk : #include DIR *opendir(const char *name); int closedir(DIR *dir);
Az opendir() egy mutatóval tér vissza, amely a könyvtár leírójaként használatos a muveletek során. Amikor a könyvtárat megnyitottuk, az egyes bejegyzéseket szekvenciálisan elérhetjük. Erre a readdir() függvény szolgál: #include struct dirent *readdir(DIR *dir);
A visszatérési értékként kapott dirent struktúra a sorban következo állomány leírását tartalmazza. (Ebben a listában nem szerepelnek a könyvtárak, illetve rendezetlen. Ha rendezett állománylistára van szükségünk, akkor magunknak kell rendeznünk.) A dirent struktúra több mezot is tartalmaz, azonban az egyetlen portolható eleme a d_name mezo, amely az állomány nevét tartalmazza. A többi elem rendszer specifikus. Ezekbol egy lényegest említünk meg, a d_ino mezot, amely az állomány inode számát tartalmazza.
ÁLLOMÁNY ÉS I/O KEZELÉS 67 A readdir() függvény mind hiba esetén, mind a könyvtár lista végén NULL-al tér vissza. Ahhoz hogy differenciálni tudjunk a ketto között, le kell ellenoriznünk az errno értékét. /* dircontents.c - display all of the files in the current directory */ #include <errno.h> #include #include <stdio.h> int main(void) { DIR * dir; struct dirent * ent; /* "." is the current directory */ if (!(dir = opendir("."))) { perror("opendir"); return 1; } /* set errno to 0, so we can tell when readdir() fails */ errno = 0; while ((ent = readdir(dir))) { puts(ent->d_name); /* reset errno, as puts() could modify it */ errno = 0; } if (errno) { perror("readdir"); return 1; } closedir(dir); return 0; }
5.6 I/O Multiplexing Sok kliens/szerver applikációnak van szüksége arra, hogy párhuzamosan több speciális állományt olvasson, vagy írjon. Például egy Web browser-nek szüksége van arra, hogy szimultán hálózati kapcsolatokon keresztül egyszerre az oldal több komponensét töltse le, hogy gyorsítsa a hozzáférést. A legegyszerubb megoldás egy ilyen esetre, ha a browser minden kapcsolaton keresztül beolvassa az érkezett adatokat, majd tovább lép a következore. Vagyis a read() muveleteket egy ciklusba ágyazzuk, és felváltva olvassuk a csatornákat. Ez a megoldás jól muködik egészen addig, amíg minden csatornán folyamatosan érkezik adat. Ha valamelyik lemaradna, akkor elkezdodnek a problémák. Ilyenkor annak a kapcsolatnak a következo olvasásakor a browser blokkolódik a read() rendszerhívásnál és egészen addig áll és várakozik, amíg érkezik adat. Természetesen általában ez nem a kívánt muködési mód.
ÁLLOMÁNY ÉS I/O KEZELÉS 68 Ennek a problémának az illusztrálására nézzük a következo példát. Ez a rövid program két pipe-ot (p1 és p2) olvas folyamatosan, és tartalmukat a képernyore írja. /* mpx-blocks.c -- reads input from pipes p1, p2 alternately */ #include #include <stdio.h> #include int main(void) { int fds[2]; char buf[4096]; int i; int fd; if ((fds[0] = open("p1", O_RDONLY)) < 0) { perror("open p1"); return 1; } if ((fds[1] = open("p2", O_RDONLY)) < 0) { perror("open p2"); return 1; } fd = 0; while (1) { /* if data is available read it and display it */ i = read(fds[fd], buf, sizeof(buf) - 1); if (i < 0) { perror("read"); return 1; } else if (!i) { printf("pipe closed\n"); return 0; } buf[i] = '\0'; printf("read: %s", buf); /* read from the other file descriptor */ fd = (fd + 1) % 2; } }
Habár az mpx-blocks mindkét pipe-ot olvassa, nem végzi jól a dolgát. Egyszerre csak az egyik pipe-ot olvassa. Elindításkor csak az elsot, amíg valami adat érkezik. Ez ido alatt a másodikról teljesen megfeledkezik. Majd ha az elso pipe read() függvénye visszatér, akkor csak a másodikkal foglalkozik. Vagyis nem valósítja meg a kívánt multiplexálást.
5.6.1 Nem blokkolt I/O Emlékezzünk vissza a korábban tárgyalt nem blokkolt állomány kezelésre. Ha az állományokat a O_NONBLOCK opcióval nyitjuk meg, akkor az írás, olvasás muveletek nem blokkolják a processz futását. Ilyenkor a read() rendszerhívás azonnal visszatér. Ha nem tudott beolvasni adatot, akkor csak szimplán 0-t ad vissza.
ÁLLOMÁNY ÉS I/O KEZELÉS 69
A nem blokkolt I/O kezelés egy egyszeru megoldást ad a multiplexálásra, mivel az állomány operációk sosem blokkolják le a processz futását. Az elozo program ezzel a módosítással az alábbi lesz: /* mpx-nonblock.c -- reads input from pipes p1, p2 using nonblocking i/o */ #include #include #include #include
<errno.h> <stdio.h>
int main(void) { int fds[2]; char buf[4096]; int i; int fd; /* open both pipes in nonblocking mode */ if ((fds[0] = open("p1", O_RDONLY | O_NONBLOCK)) < 0) { perror("open p1"); return 1; } if ((fds[1] = open("p2", O_RDONLY | O_NONBLOCK)) < 0) { perror("open p2"); return 1; } fd = 0; while (1) { /* if data is available read it and display it */ i = read(fds[fd], buf, sizeof(buf) - 1); if ((i < 0) && (errno != EAGAIN)) { perror("read"); return 1; } else if (i > 0) { buf[i] = '\0'; printf("read: %s", buf); } /* read from the other file descriptor */ fd = (fd + 1) % 2; } }
Az elso lényeges különbség az elozo programhoz képest, hogy ez nem fejezi be a futását, amikor valamelyik olvasott pipe lezárul. A nem blokkolt read() abban az esetben, amikor a pipe-ba nem írunk 0-val tér vissza. Ha a pipe írási oldalát nyitva tartjuk, de nem írunk bele adatot, akkor a read() az EAGAIN hibaüzenetet adja visszatérési értékként. Habár a nem blokkolt I/O kezelés megadja a lehetoségét, hogy az egyes file leírók között gyorsan váltogassunk, ennek nagy az ára. A program folyamatosan olvasgatja mindkét leírót, és sosem függeszti fel futását. Ezzel fölöslegesen terheli a rendszert.
ÁLLOMÁNY ÉS I/O KEZELÉS 70
5.6.2 Multiplexálás a select() függvénnyel Az effektív multiplexálást a Unix a select() rendszer hívással támogatja, amely lehetové teszi, hogy a processz blokkolódjon és több állományra várakozzon párhuzamosan. A több állomány folytonos vizsgálatával szemben itt a processz egy rendszerhívással specifikálja, hogy mely állományok olvasásár, vagy írására várakozik. Amennyiben a felsorolt állományok valamelyike tartalmaz rendelkezésre álló adatot, vagy képes fogadni az új adatokat, a select() visszatér és az applikáció olvashatja, vagy írhatja azokat a file-okat a blokkolódás veszélye nélkül. Ezek után egy újabb select() hívással várakozhatunk az újabb adatokra. A select() formátuma: #include <sys/time.h> #include <sys/types.h> #include int select(int n, fd_set *readfds, *exceptfds, struct timeval *timeout);
fd_set
*writefds, fd_set
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
A középso három paraméter (readfds, writefds, és exceptfds) adja meg, hogy mely állományleírókra vagyunk kíváncsiak, melyeket kell vizsgálnia. Mindegyik paraméter egy-egy mutató egy fd_set adatstruktúrára. Ezeket a következo makrókkal kezelhetjük : FD_ZERO(fd_set *set);
Kitörli az állományleíró listát. Ez a makró használatos a lista inicializálására. FD_SET(int fd, fd_set *set);
Az fd leírót hozzáadja a listához. FD_CLR(int fd, fd_set *set);
Az fd leírót kitörli a listából. FD_ISSET(int fd, fd_set *set);
Igaz visszatérési értéket ad, ha az fd benne van a listában. Az elso állományleíró lista, readfds, azokat a file leírókat tartalmazza, amelyek akkor oldják fel a select() várakozását, amikor olvasható állapotba kerülnek. A writefds hasonlóan az írásra kész állományleírókra várakozik. Az exceptfds azokat a leírókat tartalmazza, amelyeknek valamely különleges állapotát várjuk. Ez Linux alatt csak akkor következik be, amikor out-of-band data jelzést kapunk egy hálózati kapcsolaton keresztül. Ezen listákból bármelyik lehet NULL. Az n változó tartalma a legnagyobb állományleíró száma a listákból + 1. A timeout paraméter tartalmazza azt a maximális idot, ameddig a select() várakozhat. Ha ez letelik, akkor a select() mindenképpen visszatér. A select() és a pselect() két
ÁLLOMÁNY ÉS I/O KEZELÉS 71 különbözo timeout értékmegadást tesz lehetové. A select() visszatéréskor módosítva adja vissza az értéket, jelezve, hogy mennyi ido telt el. A pselect() semmit nem változtat a timeout paraméterén. További különbsége a pselect()- nek, hogy tartalmaz egy sigmask paramétert. A pselect() ezzel a maszkkal helyettesíti az aktuális szignál maszkot a select rendszerhívás idejére. (A NULL érték kikapcsolja a funkciót.) Ezek után nézzük a programunk módosított változatát: /* mpx-select.c -- reads input from pipes p1, p2 using select() for multiplexing */ #include #include #include #include #include
<stdio.h> <sys/time.h> <sys/types.h>
int main(void) { int fds[2]; char buf[4096]; int i, rc, maxfd; fd_set watchset; fd_set inset;
/* fds to read from */ /* updated by select() */
/* open both pipes */ if ((fds[0] = open("p1", O_RDONLY | O_NONBLOCK)) < 0) { perror("open p1"); return 1; } if ((fds[1] = open("p2", O_RDONLY | O_NONBLOCK)) < 0) { perror("open p2"); return 1; } /* start off reading from both file descriptors */ FD_ZERO(&watchset); FD_SET(fds[0], &watchset); FD_SET(fds[1], &watchset); /* find the maximum file descriptor */ maxfd = fds[0] > fds[1] ? fds[0] : fds[1]; /* while we're watching one of fds[0] or fds[1] */ while (FD_ISSET(fds[0], &watchset) || FD_ISSET(fds[1], &watchset)) { /* we copy watchset here because select() updates it */ inset = watchset; if (select(maxfd + 1, &inset, NULL, NULL, NULL) < 0) { perror("select"); return 1; } /* check to see which file descriptors are ready to be read from */ for (i = 0; i < 2; i++) { if (FD_ISSET(fds[i], &inset)) {
ÁLLOMÁNY ÉS I/O KEZELÉS 72 /* fds[i] is ready for reading, go ahead... */ rc = read(fds[i], buf, sizeof(buf) - 1); if (rc < 0) { perror("read"); return 1; } else if (!rc) { /* this pipe has been closed, don't try to read from it again */ close(fds[i]); FD_CLR(fds[i], &watchset); } else { buf[rc] = '\0'; printf("read: %s", buf); } } } } return 0; }
Ez a program hasonló eredményt ad, mint a nem blokkolt módszert használó, azonban takarékosabban bánik az eroforrásokkal.
5.7 Lockolás Habár általános eset, hogy több processz egy állományt használjon, ehhez megfelelo óvatosságra van szükség. Sok állomány tartalmaz komplex adatstruktúrákat, amelyek egy versenyhelyzetben könnyen megsérülhetnek. Ezen problémák megoldását szolgálja a file lock. Két fajtáját különböztetjük meg. Az általánosabb tájékoztató lock csak információt szolgáltat, a kernel nem felügyeli a hozzáférést. Csak egy konvenció, amelyet az állományhoz hozzáféro processzek követnek. A másik fajta a kötelezo lock, amelynek használatát a kernel kezeli. Ha egy processz lockolja az állományt, hogy írhasson bele, akkor a többi processz, amely írni vagy olvasni próbál, felfüggesztodik a lock feloldásáig. Bár ez utóbbi megoldás tunik a hasznosabbnak, ugyanakkor csökkenti a rendszer teljesítményét a sok vizsgálat minden írás és olvasás muveletnél. A Linux két metódust támogat az állomány lockolásra: lock állományok, és record lock.
5.7.1 Lock állományok A lock állományok szolgáltatják a legegyszerubb lehetoséget a hozzáférés vezérlésre. Minden védendo állományhoz hozzárendelünk egy lock file-t. Amikor a lock állomány létezik, akkor az adott file lockolva van, tehát más processzeknek nem szabad hozzáférniük. Ha a lock file nem létezik, akkor a processz létrehozhatja és hozzáférhet az állományhoz. Amíg a lock file létrehozása atomi muvelet, addig ez a módszer garantálja a kölcsönös kizárást. Ebbol következik, hogy a lock file vizsgálatát és létrehozását egy rendszerhíváson belül kell megejtenünk, különben egyes helyzetekben nem muködik megfeleloen a
ÁLLOMÁNY ÉS I/O KEZELÉS 73 metódusunk. Erre használható az open() függvény O_EXCL flag-je. Ha ezt megadjuk, akkor az állomány létrehozása meghiúsul, ha már létezik. Ilyenkor hibajelzéssel tér vissza. Így a metódus az alábbi lehet: fd = open(“somefile.lck”, O_WRONLY | O_CREAT | O_EXCL, 0644); if((fd < 0) && (errno == EEXIST) { printf(“the file is already locked”); return 1; } else if(fd < 0) { perror(“lock”); return 1; } /* writing pid into the file */ close(fd);
A lock file megszüntetése az unlink(“somefile.lck”) függvényhívással lehetséges. A Linux rendszereknél általános a lock file használata, például a soros portok, vagy a jelszó állomány kezelésénél. Annak ellenére, hogy sokszor jól használhatóak, rendelkeznek néhány hátránnyal: ?? Egyszerre csak egy processz férhet hozzá az állományhoz, így kizárja több processz párhuzamos olvasásának lehetoségét. ?? Az O_EXCL flag csak a lokális file rendszereknél használható. ?? Ez a lock csak tájékoztató, a processzek ha akarnak, hozzáférhetnek az állományokhoz a lock file ellenére is. ?? Ha a lock állományt létrehozó processz meghibásodik, elszáll, a lock file megmarad. Ha a lock állomány tartalmazza a processz azonosítóját, akkor a többi processz megbizonyosodhat a futásáról, és szükség esetén törölheti a lock állományt. Viszont ez egy komplex megoldás és nem is muködik minden esetben.
5.7.2 Record lock A lock állományok problémájának megoldására a System V rendszerekben megjelent a record lock, amely elérheto a lockf() és a flock() rendszerhívásokkal. A POSIX szabvány definiál egy harmadik metódust is az fcntl() rendszerhívás használatával. A Linux mindhárom interfészt használja, azonban mi a POSIX megoldást tárgyaljuk. A record lock elonyei a lock állományokkal szemben: ?? Az álományok egyes területeit külön-külön lock-olhatjuk. ?? A lock-ok nem tárolódnak a file rendszeren, a kernel kezeli oket. ?? Amikor a processz terminálódik, a lock felszabadul.
ÁLLOMÁNY ÉS I/O KEZELÉS 74 A record lock két lock típusból áll: read lock és write lock. A read lock-ot shared lock-nak is hívják, mivel több processz számára is lehetové teszi egy állományterület párhuzamos olvasását. Amikor a processznek írnia kell egy állományt, akkor a write lock-al (exclusive lock) kizárólagos jogot kell szereznie az adott területre. Egyszerre csak egy processz írhatja a területet és mások még read lock-ot sem hozhatnak létre. A POSIX szabvány szerint a record lock az fcntl() rendszerhívásokon keresztül érheto el. Ennek definíciója: #include int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock *lock);
Esetünkben a lock használatára az utolsó eset használatos. Itt az utolsó paraméter mutató egy flock struktúrára. struct flock { short l_type; short l_whence; off_t l_start; off_t l_len; pid_t l_pid; };
Az l_type paraméter a lock típusa. A következo értékek valamelyike: Opció F_RDLCK F_WRLCK F_UNLCK
Jelentés read lock write lock lock eltávo lítása
A következo két paraméter (l_whence és l_start) a régió kezdetét tartalmazzák hasonlóan, mint ahogy a seek() fügvénynél tárgyaltuk. Az l_len paraméter a lefoglalt terület nagysága byte-okban. Ha értéke 0, akkor az állomány végéig tart a lock. Az utolsó l_pid paraméter csak a lekérdezésnél használatos és a lekérdezett processz azonosítóját tartalmazza. Az fcntl() parancsok (cmd argumentum) közül az alábbi 3 használatos az állományok lock-olására: Opció F_SETLK
F_SETLKW F_GETLK
Jelentés Beállítja a harmadik argumentumnak megadott lock-ot. Amennyiben ez konfliktusban van más processz lock-jával, akkor EAGAIN hiba értékkel tér vissza. Hasonló, mint az elozo, azonban blokkolja a processzt, amíg a lock-olás nem végrehajtható. Ellenorzi, hogy a lock-olás végrehajtható-e.
ÁLLOMÁNY ÉS I/O KEZELÉS 75
A record lock használatára nézzünk egy példát: /* lock.c -- simple example of record locking */ #include #include #include #include
<errno.h> <stdio.h>
/* displays the message, and waits for the user to press return */ void waitforuser(char * message) { char buf[10]; printf("%s", message); fflush(stdout); fgets(buf, 9, stdin); } /* Gets a lock of the indicated type on the fd which is passed. The type should be either F_UNLCK, F_RDLCK, or F_WRLCK */ void getlock(int fd, int type) { struct flock lockinfo; char message[80]; /* we'll lock the entire file */ lockinfo.l_whence = SEEK_SET; lockinfo.l_start = 0; lockinfo.l_len = 0; /* keep trying until we succeed */ while (1) { lockinfo.l_type = type; /* if we get the lock, return immediately */ if (!fcntl(fd, F_SETLK, &lockinfo)) return; /* find out who holds the conflicting lock */ fcntl(fd, F_GETLK, &lockinfo); /* there's a chance the lock was freed between the F_SETLK and F_GETLK; make sure there's still a conflict before complaining about it */ if (lockinfo.l_type != F_UNLCK) { sprintf(message, "conflict with process %d... press " " to retry:", lockinfo.l_pid); waitforuser(message); } } } int main(void) { int fd; /* set up a file to lock */ fd = open("testlockfile", O_RDWR | O_CREAT, 0666); if (fd < 0) { perror("open"); return 1;
ÁLLOMÁNY ÉS I/O KEZELÉS 76 } printf("getting read lock\n"); getlock(fd, F_RDLCK); printf("got read lock\n"); waitforuser("\npress to continue:"); printf("releasing lock\n"); getlock(fd, F_UNLCK); printf("getting write lock\n"); getlock(fd, F_WRLCK); printf("got write lock\n"); waitforuser("\npress to exit:"); /* locks are released when the file is closed */ return 0; }
5.8 Soros port kezelés A korábbiakban már megismerhettük az általános állománykezelési metódusokat. Ezek általánosan használhatóak minden file típusra, azonban egyes eszközöknél ennél többre van szükség. Ebben a fejezetben egy ilyen esetet, a soros portok kezelését vizsgáljuk meg példaként. A /dev/ttyS* eszköz interfészek a Linux géphez kapcsolható soros terminálok használatára lettek elso sorban kifejlesztve. Ezért használatukhoz a POSIX termios interfészen keresztül be kell állítanunk a paramétereit. A termios struktúra az alábbi: struct termios { tcflag_t c_iflag; tcflag_t c_oflag; tcflag_t c_cflag; tcflag_t c_lflag; cc_t c_line; cc_t c_cc[NCCS]; };
/* /* /* /* /* /*
input mode flags */ output mode flags */ control mode flags */ local mode flags */ line discipline */ control characters */
A soros portok kezelésére több konvenció létezik. Az alkalmazáshoz kell kiválasztani a megfelelot.
5.8.1 Kanonikus feldolgozás Ez a terminálok normál kezelési metódusa, de hasznos lehet más sor alapú kommunikációra is, amely azt jelenti, hogy a read() függvény egész sorokat olvas be. A sorok véget az ASCII LF karakter, az EOF, vagy az EOL karakter jelenti. A CR karakter nem terminálja a sort.
ÁLLOMÁNY ÉS I/O KEZELÉS 77 A kanonikus metódus kezel más terminálkezelo speciális karaktereket is : erase, delete, reprint, stb. Példa a kanonikus soros port kezelésre: #include #include #include #include #include
<sys/types.h> <sys/stat.h> <stdio.h>
#define BAUDRATE B38400 /* change this definition for the correct port */ #define MODEMDEVICE "/dev/ttyS1" #define FALSE 0 #define TRUE 1 volatile int STOP=FALSE; main() { int fd,c, res; struct termios oldtio,newtio; char buf[255]; /* Open modem device for reading and writing and not as controlling tty because we don't want to get killed if linenoise sends CTRL-C. */ fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY ); if (fd <0) {perror(MODEMDEVICE); exit(-1); } /* save current serial port settings */ tcgetattr(fd,&oldtio); /* clear struct for new port settings */ bzero(&newtio, sizeof(newtio)); /* BAUDRATE: Set bps rate. You could also use cfsetispeed and cfsetospeed. CRTSCTS : output hardware flow control CS8 : 8n1 (8bit,no parity,1 stopbit) CLOCAL : local connection, no modem contol CREAD : enable receiving characters */ newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD; /* IGNPAR : ignore bytes with parity errors ICRNL : map CR to NL */ newtio.c_iflag = IGNPAR | ICRNL; /* Raw output. */ newtio.c_oflag = 0; /* ICANON
: enable canonical input disable all echo functionality, and don't send signals to calling program
ÁLLOMÁNY ÉS I/O KEZELÉS 78 */ newtio.c_lflag = ICANON; /* initialize all control characters */ newtio.c_cc[VINTR] = 0; /* Ctrl-c */ newtio.c_cc[VQUIT] = 0; /* Ctrl-\ */ newtio.c_cc[VERASE] = 0; /* del */ newtio.c_cc[VKILL] = 0; /* @ */ newtio.c_cc[VEOF] = 4; /* Ctrl-d */ newtio.c_cc[VTIME] = 0; /* inter-character timer */ newtio.c_cc[VMIN] = 1; /* blocking read until 1 character arrives */ newtio.c_cc[VSWTC] = 0; /* '\0' */ newtio.c_cc[VSTART] = 0; /* Ctrl-q */ newtio.c_cc[VSTOP] = 0; /* Ctrl-s */ newtio.c_cc[VSUSP] = 0; /* Ctrl-z */ newtio.c_cc[VEOL] = 0; /* '\0' */ newtio.c_cc[VREPRINT] = 0; /* Ctrl-r */ newtio.c_cc[VDISCARD] = 0; /* Ctrl-u */ newtio.c_cc[VWERASE] = 0; /* Ctrl-w */ newtio.c_cc[VLNEXT] = 0; /* Ctrl-v */ newtio.c_cc[VEOL2] = 0; /* '\0' */ /* now clean the modem line and activate the settings */ tcflush(fd, TCIFLUSH); tcsetattr(fd,TCSANOW,&newtio); /* terminal settings done, now handle input */ while (STOP==FALSE) /* loop until terminating */ { /* read blocks program execution until a line terminating character is input, even if more than 255 chars are input. If the number of characters read is smaller than the number of chars available, subsequent reads will return the remaining chars. res will be set to the actual number of characters actually read */ res = read(fd,buf,255); buf[res]=0; /* set end of string, so we can printf */ printf(":%s:%d\n", buf, res); if (buf[0]=='z') STOP=TRUE; } /* restore the old port settings */ tcsetattr(fd,TCSANOW,&oldtio); }
5.8.2 Nem kanonikus feldolgozás A nem kanonikus feldolgozás esetén nem sorokat, hanem egy meghatározott mennyiségu karaktert olvasunk be. Ilyenkor természetesen a speciális karakterek sem kerülnek feldolgozásra. Használata akkor javasolt, ha mindíg egy fix számú karaktert várunk a soros vonalon. Két paraméter szabályozza a rendszer viselkedését ebben a módban: a c_cc[VTIME] a karakter timert állítja be, a c_cc[VMIN] a karakterek minimális számát. Ha a MIN > 0 és TIME = 0, akkor a csak a MIN paramétert használjuk, amellyel a beolvasandó karakterek számát definiáljuk.
ÁLLOMÁNY ÉS I/O KEZELÉS 79 Ha MIN = 0 és TIME > 0, akkor a TIME meghatározza a timeout értéket (tizedmásodpercben). Ilyenkor vagy beolvas egy karaktert, vagy a megadott ido múlva a read() visszatér karakter nélkül. Ha MIN > 0 és TIME > 0, akkor a read() visszatér, ha a MIN mennyiségu karaktert beolvasta, vagy a két karakter olvasás között az ido túllépte a megadott értéket. Ha MIN = 0 és TIME = 0, akkor a read() azonnal visszatér. Az elozo példa módosítva : #include #include #include #include #include
<sys/types.h> <sys/stat.h> <stdio.h>
#define BAUDRATE B38400 #define MODEMDEVICE "/dev/ttyS1" #define FALSE 0 #define TRUE 1 volatile int STOP=FALSE; main() { int fd,c, res; struct termios oldtio,newtio; char buf[255]; fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY ); if (fd <0) {perror(MODEMDEVICE); exit(-1); } tcgetattr(fd,&oldtio); /* save current port settings */ bzero(&newtio, newtio.c_cflag newtio.c_iflag newtio.c_oflag
sizeof(newtio)); = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD; = IGNPAR; = 0;
/* set input mode (non-canonical, no echo,...) */ newtio.c_lflag = 0; /* inter-character timer unused */ newtio.c_cc[VTIME] = 0; /* blocking read until 5 chars received */ newtio.c_cc[VMIN] = 5; tcflush(fd, TCIFLUSH); tcsetattr(fd,TCSANOW,&newtio); while (STOP==FALSE) { /* returns after 5 chars have been input */ res = read(fd,buf,255); buf[res]=0; /* so we can printf... */ printf(":%s:%d\n", buf, res); if (buf[0]=='z') STOP=TRUE;
ÁLLOMÁNY ÉS I/O KEZELÉS 80 } tcsetattr(fd,TCSANOW,&oldtio); }
5.8.3 Aszinkron kezelés Az elobb említett két módszer a szinkron metódus mellett aszinkron módon is használható. Az alapértelmezett az eddig látható szinkron kezelés, ahol a read() függvény blokkolódik, amíg információ érkezik, vagy a feltétel teljesül. Az aszinkron kezelési módnál a read() azonnal visszatér és a processz egy szignált kap majd az adatok megérkezésérol. Ezt a szignált a lekezelo függvény kapja meg. (A szignálokat majd egy késöbbi fejezetben tárgyaljuk.)
KONKURENS PROGRAMOZÁS 81
6. Konkurens programozás 6.1 Folyamatok (Processes) Egy operációs rendszerrel szemben támasztott alapveto követelmények egyike a többfeladatos (multitasking) muködés. Ez azt jelenti, hogy a felhasználók számára virtuálisan vagy a valóságban is - több program fut párhuzamosan. Folyamatoknak nevezzük a végrehajtás alatt álló programot, amelybe a programszámláló, regiszterek és a változók aktuális értékét is beleértjük. A fent mondottak értelmét nem csorbítjuk jelentosen, ha a folyamatra egyszeruen „futó program”-ként gondolunk.
6.1.1 Jogosultságok, azonosítók és jellemzok Minden folyamathoz az operációs rendszer egy egyedi folyamat azonosítót (Process ID) és egy folyamat csoport azonosítót (Process Group ID) rendel. Az egy csoprtba tartozó folyamatokhoz egy terminál tartozik, amelynek azonosítóját (terminál csoport azonosító, Terminal Group ID) a folyamat számon tartja. Egy folyamatot (kivéve a legelso folyamatot, az úgynevezett init-et) mindig egy másik folyamat hoz létre.A létrehozó folyamatot szülo folyamatnak (Parent Process), a létrehozott folyamatot gyermek folyamatnak (Child Process) nevezzük. Az init folyamatot kivéve minden folyamatnak van szüloje. Amennyiben egy szülo folyamat elobb szunik meg, mint annak gyermek folyamatai, a gyermek folyamatok árvákká (orphans) válnak, és szülojük automatikusan az init folyamat lesz. A folyamatok tudnak még a szülo folyamat azonosítójáról (Parent Process ID). A folyamat tárolja még az ot futtató felhasználó login nevét, és a felhasználóra jellemzo egyedi azonosítót, a felhasználói azonosítót (User ID), és a felhasználói csoportra jellemzo felhasználói csoportazonosítót (Group ID). Sokszor szükség van arra, hogy az operációs rendszer „belso” szolgáltatásait az egységsugarú felhasználó is igénybe vegye, amelyekhez biztonsági okok miatt nem adhatunk hozzáférést. Erre kínál megoldást a felhasználó „megszemélyesítése”, ami annyit jelent, hogy egy program a file tulajdonosának (ez gyakran a tejhatalmú root) felhasználói azonosítójával futhat. Például ha egy felhasználó meg szeretné változtatni a jelszavát, értelemszeruen hozzá kell férni a rendszer jelszóadatbázisához, amihez szükségképpen root jogosultságra van szükség. Ezért a passwd segédprogram root jogosultságokkal kell fusson, ugyanakkor bármely felhasználónak lehetové kell tenni jelszava megváltoztatását. Erre kínál megoldást a setuid illetve a setguid bit beállítása (részletesen a jogosultságoknál tárgyaltuk). Ekkor a folyamat a file tulajdonosának (setuid) és a tulajdonos csoportazonosítójának (setguid) megfelelo jogosultságokkal fut 1 . A folyamat számára erre az esetre létrehozták az effektív felhasználói azonosítót (Effective User ID) és az effektív felhasználói csoportazonosítót (Effective Group ID), amelyek értéke a file tulajdonosa - a fenti példánkban a root - felhasználói azonosítója illetve csoportazonosítója. Ha a setuid illetve a setguid bit nincs beállítva, 1
Mivel sok esetben a folyamat root jogosultságokkal fut, támadási felületet nyújt a rendszert feltörni kívánóknak. Ugyanis ha egy setuid-os program összeomlik, az operációs rendszer nem vált vissza, a felhasználó a file tulajdonos (root) jogosultságaival garázdálkodhat a gépen. Ez foként régebbi ssh programok esetén volt jól használható módszer hackerek számára. Nem csoda, hogy manapság megpróbálják minimalizálni a setuid alatt futó programok számát.
KONKURENS PROGRAMOZÁS 82 akkor az effektív azonosítók értelemszeruen megegyeznek a valódi felhasználói azonosítókkal. A folyamat tisztában van még az aktuális munkakönyvtárral és az umask változóval.
6.1.2 A folyamat állapotai Egy folyamat lehetséges állapotait és azok kapcsolatait a következo ábra szemlélteti:
Ábra 6-1 A folyamat állapotainak UML álapotdiagramja
Az állapotok közül a zombi állapot szorul magyarázatra: ilyenkor már csak a kilépési státuszt és a folyamatra vonatkozó statisztikákat tárolja az operációs rendszer, a többi adatot már felszabadította. A folyamatokat a fenti állapotdiagram szerint a folyamat ütemezo (scheduler) kezeli. Az ütemezési algoritmusokról, a folyamatokhoz tartozó adatterületekrol a [6][7] irodalmakban találunk akár részletekbe meno leírást. Lássuk, hogyan muködik mindez Linux alatt!
6.1.3 Folyamatok létrehozása és megszüntetése A legegyszerubben esetben az operációs rendszeren keresztül indítunk egy új folyamatot. Itt gyakorlatilag arról a lehetoségrol van szó, hogy miként érhetjük el a parancssorban adott lehetoségeket programból. Erre a #include <stdlib.h>
KONKURENS PROGRAMOZÁS 83 int system (const char * cmd);
függvény ad lehetoséget. Például, ha ki szeretnénk listázni egy könyvtár tartalmát, akkor a #include<stdlib.h> int main() { system("ls -a"); return 0; }
programrészlettel tehetjük meg. A system() függvény funkciója megvalósításához az execve() függvényt hívja, amely egy népes függvénycsalád egyik képviseloje. Ismerkedjünk most meg ezekkel a függvényekkel! Az #include int execve (const *const envp[]);
char
*filename, char *const argv [], char
függvény egy új folyamatot indít, amelyet az elso paraméterben megadott elérési útvonalon található futtatható file indításából2 keletkezik. Az argv egy NULL terminált stringeket tartalmazó tömb, melyet egy NULL pointernek kell zárnia. Ezeket fogja az adott program parancssori paraméterként megkapni. Hagyományosan ez a program teljes elérési útvonalát tartalmazza. Hasonlóan az envp a környezeti változókat tárolja változónév=érték formátumban. Itt is ügyeljünk arra, hogy az utolsó pointer NULL legyen. Az execve sikeres végrehajtás esetén nem tér vissza, az új program felülírja a hívót, örökli a hívó folyamat azonosítóját (Process ID), az összes le nem zárt file leírója törlodik. Vegyük szemügyre a hasonló funkciójú család többi tagját, amelyeknek deklarációi a következok: #include extern char **environ; int execl( const char *path, const char *arg, ...); int execlp( const char *file, const char *arg, ...); int execle( const char *path, const char *arg , ..., char * const envp[]); int execv( const char *path, char *const argv[]); int execvp( const char *file, char *const argv[]);
Az elso két függvény esetében a parancssori paramétereket nem egy tömbként, hanem külön argumentumként adjuk át, természetesen a sort egy NULL argumentummal zárva, az elso argumentumra vonatkozó elérési útvonal konevenciót megtartva. A két utolsó függvény mindezt az execve függvényhez hasonlóan tömbként adja át az argumentumokat. Ezek a függvények, amelyeknek nem adtunk meg környezeti 2
Linux alatt a #! kezdet egy scriptet jelent, melynek interpreterét a következo paraméter adja meg (például: #!/bin/bash). Ezt a funkciót azonban a POSIX nem definiálja.
KONKURENS PROGRAMOZÁS 84 változók átadására szolgáló paramétereket, a fent deklarált environ változóból veszik az átadandó környezeti változókat. Ha a megadott file paraméter nem tartalmaz „/” (perjel, slash) karaktert, akkor az execvp és az execlp a környezeti változók között megadott PATH változóban keresi az adott file-t. Ha a PATH nincs megadva, akkor ezek a függvények a PATH változó értékét a „:/bin:/usr/bin” stringnek feltételezi. Az elozoekben láttuk, hogy az exec...() függvények meghívásával a hívó folyamat futása megszakadt. Lássuk most a párhuzamos létrehozás lehetoségeit! A #include pid_t fork(void);
egy folyamatot két egyforma folyamattá változtat.Ekkor a fork-ot hívó szülo folyamat mellett egy új PID-del rendelkezo gyermek folyamat keletkezik. A fork a szülo folyamathoz a gyermek folyamat azonosítójával (PID), míg a gyermekhez 0 értékkel tér vissza. Hiba esetén a fork() visszatérési értéke -1 (természetesen a szülo felé, hiszen a gyermek folyamat létre sem jött), és az errno változó tartalmazza a hiba okát. Folyamat megszüntetése a #include <sys/types.h> #include <signal.h> int kill(pid_t pid, SIGINT);
függvénnyel lehetséges, amelyet a jelzéskezelésnél részletezünk. Elöljáróban annyit, hogy ezt az üzenetet csak az egyazon felhasználóhoz tartozó folyamatok küldhetik egymásnak. Az alábbi program szemlélteti a fork() használatát: #include<stdio.h> #include int main() { int pid; printf("Starting program...\n\n"); pid=fork(); if(pid<0) { printf("Forking error.\n\n"); exit(-1); } if(pid==0) { /* A gyermek folyamat - pid 0 */ printf("My name is Child. James Child. My PID: %d Fork returned: %d\n",getpid(),pid); } else { /* A szülo folyamat - a pid változó a gyermek folyamat azonosítója.*/
KONKURENS PROGRAMOZÁS 85 printf("My name is Parent. James Parent. My PID: %d Fork returned: %d\n",getpid(),pid); } }
Ennek a programnak a kimenete a következo: Starting program... My name is Parent. James Parent. My PID: 3843 Fork returned: 3844 My name is Child. James Child. My PID: 3844 Fork returned: 0
Látjuk, hogy a szülo folyamat azonosítója 3843, a gyermeké 3844. A szülo folyamat a fork() visszatérési értékébol értesül gyermeke azonosítójáról. Egy folyamat saját folyamat azonosítóját az file-ban deklarált pid_t getpid(void);
függvénnyel, a szülo azonosítóját a pid_t getppid(void);
függvénnyel kérdezhetjük le. Ezeket a függvényeket a fenti progrmrészletben is felhasználtuk. A következo programrészletben általános sémát adunk arra, hogyan hozzunk létre elágazást. #include<stdio.h> #include int child_function(); int parent_function(int child_id); int child_function() { printf("My name is Child. James Child. My PID: %d\n",getpid()); printf("My Parent's PID: %d\n\n",getppid()); exit(0); } int parent_function(int child_id) { printf("My name is Parent. James Parent. My PID: %d\n",getpid()); printf("My child's ID is: %d\n\n",child_id); exit(0); } int main() { int pid; printf("Starting program...\n\n"); pid=fork();
KONKURENS PROGRAMOZÁS 86 if(pid<0) { printf("Forking error.\n\n"); exit(-1); } if(pid==0) { /* A gyermek folyamat - pid 0 */ child_function(); } else { /* A szülo folyamat - a pid változó a gyermek folyamat azonosítója.*/ parent_function(pid); } }
A fenti porgram egyes függvényei (parent_function, child_function) tudnak identitásukról (a szülo tudja magáról, hogy szülo, a gyermek tudja magáról, hogy gyermek), illetve egymás folyamat azonosítóját is számon tartják. Felhasználjuk az void exit(int status);
függvényt melynek status paramétere a folyamat kilépési kódja (exit code). Konvencionálisan a 0 a hibátlan muködést jelenti, az ettol eltéro hibát jelent. A program kimenetét tekintve azonban meglepetés ér minket: Starting program... My name is Parent. James Parent. My PID: 3690 My name is Child. James Child. My PID: 3691 My Parent's PID: 3690 My child's ID is: 3691
Az elágazás utáni folyamatok ugyanis konkurrensen hajtódnak végre, ennek következtében „egyszerre”, egymás futását megszakítva futnak. Ez a megállapítás egy újabb kérdést vet fel: ?? Hogyan lehet két vagy több folyamat muködését egymáshoz szinkronizálni? ?? Hogyan tudnak az egymással párhuzamosan futó folyamatok kommunikálni? Ezekre a kérdésekre válaszolnak a következo fejezetek.
6.1.4 Folyamatok közötti kommunikáció (IPC) A UNIX operációs rendszer család folyamatok közötti kommunikációjának (Interprocess Communication) egyik fo alappillére a System V IPC. A System V IPC-t AT&T fejlesztett saját UNIX verziójához, melyet Linux alá is implementáltak. A System V IPC-rol részletes útmutatást ad a ipc(5) man oldal. A másik alternatíva a POSIX szabvány által definiált lehetoségek használata. Mivel Linux alatt mindkettot implementálták, szabadon használhatjuk bármelyiket.
KONKURENS PROGRAMOZÁS 87 A System V IPC programozása közben segítségünkre lehet néhány hasznos segédprogram. Az ipcs program kiírja a memóriában lévo olyan IPC objektumokat, amelyekhez a hívó folyamatnak olvasási joga van. Az ipcs paramétereit a következo táblázat foglalja össze. Parancs ipcs -q ipcs -s ipcs - m ipcs -h
Magyarázat Csak az üzenetsorokat mutatja Csak a szemaforokat mutatja Csak az osztott memóriát mutatja További segítség
Nézzünk erre egy példát: $ipcs -s ------ Semaphore Arrays -------key semid owner status 0x5309dbd2 81985536 schspy 644
perms
nsems 1
Szintén fejlesztés közben lehet hasznos, ha el tudjuk távolítani az egyes IPC objektumokat a Kernelbol. Erre szolgál a ipcrm <msg | sem | shm>
segédprogram. Például a fent megjelenített szemafort a ipcrm sem 81985536 paranccsal szüntethetjük meg.
A System V IPC-t Linux alatt alapvetoen egy kernelhívás hajtja végre: int ipc(unsigned int void *ptr, long fifth)
call,
int
first, int second, int third,
Az elso argumentum határozza meg a hívás típusát (milyen IPC funkciót kell végrehajtani), a többi paraméter a funkciótól függ. Ez a függvény értelemszeruen Linux specifikus, lehetoleg csak a kernel programozása közben használjuk. 6.1.4.1 Folyamatok szinkronizációja Nézzük eloször a szinkronizáció lehetoségeit!
6.1.4.1.1 Várakozás gyermekfolyamat végére Gyermekfolyamatok végére várakozni, állapotukat ellenorizni az alábbi két függvénnyel tudunk: #include <sys/types.h> #include <sys/wait.h>
KONKURENS PROGRAMOZÁS 88 pid_t wait(int *status) pid_t waitpid(pid_t pid, int *status, int options);
A wait() függvény akkor tér vissza, ha a hívó folyamat gyermekfolyamatai közül bármelyik befejezte muködését. Ha a hívás idopontjában egy gyermekfolyamat már zombi állapotban van, akkor a függvény azonnal visszatér. A status paraméter -1, ha hiba lépett föl, egyébként a gyermekfolyamat kilépési kódja (exit code). A waitpid függvény sokkal testreszabhatóbb: Itt a pid paraméterrel egy meghatározott folyamatazonosítójú gyermek kilépésére várakozhatunk, az utolsó paraméterrel megadható opcióktól függoen, amelyek közül a lényegesebbeket a következo táblázat foglalja össze. Érték < -1
Leírás Vár bármely gyermekfolyamat végére, melynek csoportazonosítója megegyezik a pid paraméterben adottal. -1 Ekkor a függvény hatása megegyezik a wait függvényével 0 Vár bármely gyermekfolyamat végére, melynek csoportazonosítója megegyezik a hívó folyamatéval. >0 Vár bármely gyermekfolyamat végére, melynek folyamatazonosítója megegyezik a pid paraméterben adottal. WNOHANG Felfüggesztés nélkül (no hang) visszatér, ha még egy gyermekfolyamat sem ért véget. Ezt a konstansot OR kapcsolatba kell hozni a fenti három lehetoség alapján válaszott értékkel.
6.1.4.1.2 Szemaforok Egy másik szinkronizációs lehetoség a szemafor (semaphore) használata. A szemafor egy (unsigned int) számlálóként képzelheto el, amelynek megváltoztatása oszthatatlan muvelet kell legyen, vagyis más szálak és folyamatok nem tudják megszakítani a szemafort állító folyamatot. Vizsgáljuk most meg, a szemafor miként használható szinkronizációs célokra! A szemafort egy meghatározott kezdeti értékre állítjuk. Valahányszor egy folyamat lefoglalja a szemafort (lock), a szemafor értéke eggyel csökken. Ha eléri a nullát, több folyamat már nem foglalhatja le a szemafort. Amennyiben egy folyamatnak már nincs több szemafor által védendo dolga, növeli eggyel a szemafor értékét, vagyis elengedi (release) a szemafort. A fentiekbol adódik, hogy a szemafor kezdeti értéke határozza meg azt, hogy egyszerre hány folyamat foglalhatja le a szemafort. Amennyiben ez a kezdeti érték 1, egyszerre csak egy folyamat foglalhatja le a szemafort, ami a kölcsönös kizárást (mutual exclusion) jelenti. Többféle szemaforkezelés lehetséges, az egyik a System V IPC által definiált, a másik a POSIX által támogatott. A System V IPC szemaforjai lényegében szemafortömbök. Létrehozásuk a #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
KONKURENS PROGRAMOZÁS 89
függvényhívással történhet, ahol az elso paraméter egy egyedi azonosító, az nsems a létrehozandó szemaforok száma, a semflg a hozzáférési jogosultság beállítására szolgál. Amennyiben a key paraméter egy már létezo szemaforhoz lett hozzárendelve, akkor a semget függvény a létezo szemafor azonosítóját adja vissza, egyébként az új szemaforét. Tehát már létezo szemaforhoz egy másik folyamatból a key paraméter segítségével kapcsolódhatunk hozzá. Ha új szemafort szeretnénk létrehozni, a kulcsgenerálásban segítségünkre lehet az # include <sys/types.h> # include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
függvény, amely a kötelezoen létezo file-ra mutató pathname és egy nem nulla proj_id paraméterekbol létrehoz egy egyedi kulcsot. Nézzünk most egy példát IPC szemafortömb létrehozására! #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h>
int main() { int semid;
/* Generating unique key number for semaphore */ key_t key = ftok(".", 's'); if((semid = semget(key, 5, IPC_CREAT|IPC_EXCL|0666))== -1) { fprintf(stderr, "Semaphore already exists!\n"); exit(1); } printf("Semaphore with id #%d has been created succesfully\n",semid); return 0; }
A fenti programrészletben a IPC_CREAT|IPC_EXCL jelzobitek szolgálnak magyarázatra. Az IPC_CREAT paraméter létrehozza a szemafort, ha még nem létezik. Az IPC_EXCL bitnek csak az IPC_CREAT együtt van értelme, ha a szemafor már létezik, akkor a semget függvény hibával tér vissza. Fordítsuk, futtassuk a programot, majd ellenorizzük a helyes muködést, valamint távolítsok el a létrehozott szemafort a kernelbol: $ gcc sem.c -o sem $ ./sem Semaphore with id #524288 has been created succesfully $ ipcs .
KONKURENS PROGRAMOZÁS 90 . . ------ Semaphore Arrays -------key semid owner status 0x7302ffc7 524288 tihamer 666 . . .
perms
nsems
5
$ ipcrm sem 524288 resource(s) deleted
Szemafor törlését természetesen programból is végrehajthatjuk, ha programunkba beszúrjuk a következo sort: semctl(semid, 0, IPC_RMID, 0);
A semctl függvény több szemaforvezérlo fukciót lát el. #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...);
Az elso argumentum a szemafor azonosítója, amivel a semopen függvény tért vissza. A semnum argmentum a szemafortömbbol választ ki egy elemet, ez egy nulla alapú index. A semnum és az összes utána következo argumentum a cmd értékétol függ. A lehetséges muveletekrt a következo táblázat foglalja össze. Muvelet IPC_STAT IPC_SET IPC_RMID GETALL GETNCNT GETPID GETVAL GETZCNT SETALL SETVAL
Leírás Szemaforinformáció lekérdezése. Olvasás jogosultság szükséges. Jogosultság, felhasználó- és csoportazonosítók megváltoztatása. Szemafortömb megszuntetése felébresztve a várakozó folyamatokat. A szemafortömb elemeinek értékét adja vissza. Egy szemaforra várakozó processzek száma. A szemafortömb folyamatazonosítóját kérdezi le. Egy szemafor értékét adja vissza. Egy szemafor nulla (foglalt) értékére várakozó processzek száma. A szemafortömb összes elemének értékét állítja be. Egy szemafor értékét állítja be.
A szemafor létrehozása és tulajdonságainak beállítása után vizsgáljuk meg, hogyan várakozhatunk egy szemaforra. Ezt a #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops);
függvénnyel tehetjük meg. Az elso paraméter a megszokott módon a szemafortömb azonosítására szolgál. A második paraméter sembuf típusú struktúrák egy tömbje,
KONKURENS PROGRAMOZÁS 91 amelyek a végrehajtandó muveleteket írják elo. A semop függvény garantálja, hogy ezek közül a muve letek közül vagy mind, vagy egyik sem lesz végrehajtva. A sembuf struktúra felépítése a következo: struct sembuf { ushort sem_num; short sem_op; short sem_flg; };
/* A szemafor indexe a tömbben */ /* Szemafor értékének változása */ /* A muvelet jelzobitjei */
A sem_op tagváltozó értéke elojelesen hozzáadódik a szemafor értékéhez. Amennyiben ez az érték negatív, úgy az eroforrás foglalásnak felel meg, ha pozitív, akkor az eroforrás elengedését jelenti. Nulla sem_op esetén a folyamat azt ellenorzi, hogy a szemafor értéke nulla-e. A jelzobitek értéke IPC_NOWAIT, SEM_UNDO, illetve a ketto bitenkénti vagy kapcsolata lehet. Az IPC_NOWAIT esetén a muveletet megkísérli végrehajtani, ha azonban ez nem sikerül, a semop azonnal hibával tér vissza, ha ez a jelzobit nincs beállítva, akkor a semop várakozik a szemafor kedvezo állapotára. Ha a sem_op értéke nulla volt és az IPC_NOWAIT nem volt beállítva, akkor a folyamat várakozik addig, amíg a szemafor értéke nulla nem lesz (összes eroforrás lefoglalva). A SEM_UNDO jelzobit bekapcsolása esetén a muvelet visszavonódik amikor a hívó folyamatnak vége lesz. A semop függvény utolsó argumentuma a sops tömbben lévo sembuf típusú struktúrák számát adjuk meg. 1. Példa. Készítsünk olyan programot, amely egy repülotéri bejelentkezést szimulál! Hozzunk létre egy szemafortömböt, ahol a szemafortömb egy légitársaságot, az egyes szemaforok pedig a légitársaság pultjait jelentik. Egy program hozza létre a szemafortömböt, egy másik program pedig „menjen oda” az elso üres pulthoz, amennyiben minden pult foglalt, álljon be abba a sorba, ahol legkevesebben várakoznak! A POSIX szemaforok egyetlen szinkronizációs objektumot jelentenek (szemben a System V IPC szemafortömbjével). Névtelen szemafort a következo függvénnyel hozhatunk létre: #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
Ha a pshared nem 0, akkor a szemaforhoz más folyamatok is hozzáférhetnek, egyébként csak az adott folyamat szálai. A szemafor értéke a value paraméterben megadott szám lesz. A Linux nem támogatja a folyamatok közötti szemaforokat, ezért nem nulla pshared argumentum esetin a függvény mindig hibával tér vissza. Egy sem szemafort a #include <semaphore.h> int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem);
KONKURENS PROGRAMOZÁS 92 függvényekkel foglalhatunk le. Ha a sem szemafor értéke pozitív, mindkét függvény lefoglalja, és visszatér 0- val. Ha a szemafort nem lehet lefoglalni (vagyis a szemafor értéke 0), a sem_trywait azonnal visszatér EAGAIN értékkel, míg a sem_wait várakozik a szemaforra. Ez utóbbi várakozást vagy a szemafor állapota (az értéke nagyobb lesz, mint nulla), vagy egy jelzés szakíthatja meg. A szemafort használat után a #include <semaphore.h> int sem_post(sem_t *sem);
függvénnyel engedhetjük el. A szemafor aktuális értékét a #include <semaphore.h> int sem_getvalue(sem_t *sem, int *sval);
függvénnyel kérdezhetjük le. Ha a szemafort lefoglalták, akkor a visszatérési érték nulla vagy egy negatív szám, melynek abszolút értéke megadja a szemaforra várkozó folyamatok számát. Ha a sval pozitív, a szemafor értékét jelenti. Névtelen szemafort a #include <semaphore.h> int sem_destroy(sem_t *sem);
függvénnyel szüntethetünk meg. 2. Példa. Hogyan kellene módosítanunk az 1. Példa programját, hogyha egy közös sor van, és a soron következo utas mindig a leghamarabb megüresedett pulthoz menne? Programunk legyen POSIX kompatibilis!
6.1.5 Üzenetsorok (Message Queues) Az üzenetsor egy olyan FIFO jellegu kommunikációs csatornát jelent, amelybe a programozó által meghatározott formátumú adatcsomagokat lehet belerakni. Az üzenetnek megadhatunk egy pozitív szám típust, amely alapján virtuálisan egy üzenetsoron több üzenetcsatornát használhatunk. Fizikailag ez egy láncolt listaként jelenik meg a Kernel címterében, amelyet a következo adatstruktúra ír le: struct msg { struct msg *msg_next; long msg_type; char *msg_spot; short msg_ts;
/* a közvetkezo üzenet a sorban */ /* az üzenet típusa */ /* maga az üzenet (a Kernel nem tud semmit a formátumról) */ /* az üzenet mérete */
};
A fentiekbol jól látható, hogy a Kernel csak az üzenet típusát kezeli (msg_type), a többi adat számára egy memóriaterületre mutató pointer (msg_spot), aminek tudja a méretét (msg_ts) és amit nem kell értlmezni.
KONKURENS PROGRAMOZÁS 93 Nézzük most meg, hogyan adhatjuk meg saját üzenetformátumunkat! Elsoként vizsgáljuk meg azt az alapveto struktúrát, amit ki kell bovítenünk, és ami sys/msg.h állományban van definiálva: /* üzenetformátum az üzenetküldo ésüzenet fogadó függvényeknek (ld. késobb) */ struct msgbuf { long mtype; /* üzenettípus */ char mtext[1]; /* adat */ };
Vagyis azok a függvények, amelyek üzenetet küldenek illetve foganak, a fenti struktúrára mutató pointert várnak, és a tényleges, a programozó által definiált struktúra méretét.
Ábra 6-2 Az msgbuf és a programozó által definiált üzenetstruktúra összehasonlítása.
A 6-2 ábra szemlélteti mindezt. Tegyük fel, hogy a programozó a következo struktúrát definiálta: struct studentinfo { char name[40]; char address[80]; char ssnumber[20]; char remark[80]; };
/* /* /* /*
Nev */ Cim */ Szemelyi szam */ Megjegyzes */
struct studentinfo_msg { long mtype; struct studentinfo data; };
Ha a studentinfo_msg struktúrát explicite átkonvertáljuk msgbuf típusú struktúrára, abból az IPC függvények csak a szaggatott vonalig látják, szükségünk van még arra az információra, ami megadja az ábrán jobboldalon szereplo, programozó által meghatározott adatstruktúra méretét, méghozzá az mtype nélkül. A fenti példa egyben be is mutatta, hogyan kell saját üzenetformátumot létrehozni. A System V IPC üzenetsorainak a kezelése koncepcióiban nagyon hasonlít a szemaforokéra. Üzenetsort a #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg);
KONKURENS PROGRAMOZÁS 94
függvénnyel hozhatunk létre, melynek az elso paramétere egy egyedi kulcs, amit ezúttal is létrehozhatunk az ftok függvénnyel. A jelzobitek a jogosultságokat állítják be, illetve megadhatjuk az IPC_CREAT és az IPC_EXCL jelzobiteket, amelyeket a System V szemaforok leírásánál mutattunk be. Amennyiben a megadott kulcs létezik, akkor a függvény a már létezo szemafor azonosítójával tér vissza, egyébként az azonosító az újonan létrehozotté. Hiba esetén a visszatérési érték -1. Példa szemafor létrehozására: /* Egyedi kulcs létrehozása ftok() segítségével*/ key=ftok("./",'m'); /* Az üzenetsor megnyitása */ if((mqid = msgget(key, IPC_CREAT|0660)) == -1) { /*Hiba */ }
Most egy másik folyamatból érjük el ugyanezt a szemafort: /* Az üzenetsor megnyitása */ if((mqid = msgget(key, 0660)) == -1) { /* Nincs ilyen üzenetsor. */ }
Üzenetet küldeni illetve fogadni az #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msgflg);
msqid, struct msgbuf *msgp, size_t msgsz, int
ssize_t msgrcv(int msqid, struct msgbuf long msgtype, int msgflg);
*msgp,
size_t msgsz,
függvények segítségével tudunk. A küldés során megadjuk az üzenetsor azonosítóját, a saját üzenetünk címét msgbuf struktúrúra konvertálva, az adatméretet (a struktúra méretébol le kell vonnunk az mtype méretét). Ha az üzenetsor tele van, akkor az msgsnd függvény hibával tér vissza, vagy várakozik a küldésre. Elso esetben jelzobitként beállíthatjuk IPC_NOWAIT bitet, egyébként nullát adunk meg. Üzenet kiolvasásánál megadjuk az üzenetsor azonosítóját, annak a memóriaterületnek a pointerét msgbuf típusúra konvertálva, ahova szeretnénk, hogy az msgrcv függvény lemásolja az üzenetet. Kiolvasásnál a méret a megadott memória terület méretét jelenti. Ha ennél kisebb az üzenet nem történik hiba, ha nagyobb akkor igen. Ha azonban beállítjuk az MSG_NOERROR jelzobitet, akkor az üzenet vége le lesz vágva, csak az elso msgsz számú byte lesz az msgp által mutatott memóriaterületre másolva. Az msgtype argumentumot szurésre használhatjuk az üzenet típusa alapján. ?? Ha az msgtype nulla, akkor az msgrcv a soron következo üzenetet olvassa ki az üzenetsorból ?? Ha pozitív,
KONKURENS PROGRAMOZÁS 95 o ha az MSG_EXCEPT jelzobit nincs bekapcsolva, akkor azt a legelso üzenetet, melynek típusa msgtype o ha az MSG_EXCEPT jelzobit be van kapcsolva, akkor azt az elso üzenetet, melynek típusa nem msgtype. ?? Ha az msgtype negatív, akkor a legalacsonyab típusú üzenet kerül kiolvasásra, melynek típusa kisebb vagy egyenlo, mint az msgtype abszolút értéke. Az üzenetsor vezérlését a #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);
függvénnyel végezhetjük. Az elso paraméter az üzenetsor azonosítója, a második paramétert az következo táblázat foglalja össze: Parancs IPC_STAT IPC_SET
IPC_RMID
Leírás Információt másol a buf argumentum által mutatott struktúrába. A buf által mutatott struktúra némely tagjai alapján átállítj az üzenetsor tuajdonságait. A figyelembe vett tagok a következok: ?? msg_perm.uid ?? msg_perm.gid ?? msg_perm.mode (az alsó 9 bit) ?? msg_qbytes Az üzenetsor megszüntetése.
Az utolsó paraméter típusa az üzenetsor tulajdonságait rögzíto struktúra: struct msqid_ds { struct ipc_perm msg_perm;/* Hozzáférési jogosultságok */ struct msg *msg_first; /* Az elso üzenet a üzenetsor láncolt listájában */ struct msg *msg_last; /* Az utolsó üzenet az üzenetsor láncolt listájában */ time_t msg_stime; /* A legutolsó küldés ideje */ time_t msg_rtime; /* A legutolsó olvasás ideje */ time_t msg_ctime; /* A legutolsó változtatás ideje */ struct wait_queue *wwait; struct wait_queue *rwait; ushort msg_cbytes; /* Az üzenetsorban lévo byte-ok száma (össszes üzenet) */ ushort msg_qnum; /* Az éppen az üzenetsorban lévo üzenetek száma */ ushort msg_qbytes; /* Az üzenetsorban levo byte-ok maximális száma */ ushort msg_lspid; /* A legutolsó küldo folyamat azonosítója */ ushort msg_lrpid; /* A legutolsó olvasó folyamat azonosítója */ };
KONKURENS PROGRAMOZÁS 96
Például üzenetsor eltávolítását a msgctl(mqid, IPC_RMID, 0);
programsorral végezhetjük. Végül rakjuk össze az eddigieket egy példában! Példa. Egy egyetemen egy kurzust annak azonosítójával jellemeznek. Egyszeru példánkban egy adott kurzusra maxium 5-en jelentkezhetnek. Írjunk egy kurzusszerver programot, ami „meghirdeti” a kurzust, és létrehoz egy üzenetsort, amin keresztül a hallgató regisztráló kliens program elküldi a hallgatók adatait. A szerver program egyszeruen csak kiírja a regisztrált hallgatók adatait. /* common.h: kozos szerver es kliens deklaraciok. */ #ifndef COMMON_H #define COMMON_H
#define MSG_TYPE_REGISTER 1 /* Az uzenet tipusa */ struct studentinfo { char name[40]; char address[80]; char ssnumber[20]; char remark[80]; };
/* Nev */ /* Cim */ /* Szemelyi szam */ /* Megjegyzes */
struct studentinfo_msg /* Sajat formatumu uzenetbuffer */ { long mtype; struct studentinfo data; }; #endif /* COMMON_H */ /* course_server.c: a kurzusszerver */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include"common.h"
#define MAX_REGISTRATION 5 /* Max ennyi hallgato jelentkezhet a kurzusra */ int main() { key_t key; int mqid,i; struct studentinfo_msg msg; struct studentinfo_msg *pmsg=&msg;
KONKURENS PROGRAMOZÁS 97
/* Egyedi kulcs letrehozasa ftok() segitsegevel*/ key=ftok("./",'m'); /* Az uzenetsor megnyitasa */ if((mqid = msgget(key, IPC_CREAT|0660)) == -1) { fprintf(stderr,"Course number #%d does not exist.\n",key); return 1; } printf("Course number #%d is available for registration.\n",key); for(i=0;i<MAX_REGISTRATION;i++) { pmsg->mtype = MSG_TYPE_REGISTER; msgrcv(mqid,(struct msgbuf *)pmsg, sizeof(msg)sizeof(long), \ MSG_TYPE_REGISTER, 0); printf("A student has signed up for the course:\n"); printf("Name: %s\n",pmsg->data.name); printf("Address: %s\n",pmsg->data.address); printf("Social Security#: %s\n",pmsg->data.ssnumber); printf("Remarks: %s\n\n",pmsg->data.remark); }
/* Uzenetsor torlese */ msgctl(mqid, IPC_RMID, 0); printf("Registration is closed.\n"); return 0; } /* student_register.c: a hallgatokat regisztralo kliens. */ /* Parancssori argumentum a kurzus szama. */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include"common.h"
int main (int argc, char*argv[]) { key_t key; int mqid; struct studentinfo_msg msg; int msgsize; if(argc!=2) { fprintf(stderr,"Usage: student_register \n"); return -1;
KONKURENS PROGRAMOZÁS 98 }
key=atoi(argv[1]); /* Az uzenetsor megnyitasa */ if((mqid = msgget(key, 0660)) == -1) { fprintf(stderr,"Course number #%d does not exist.\n",key); return 1; } printf("Name: "); scanf("%s",msg.data.name); printf("Address: "); scanf("%s",msg.data.address); printf("Social Security#: "); scanf("%s",msg.data.ssnumber); printf("Remarks: "); scanf("%s",msg.data.remark); /* Uzenetkuldes */ printf("Registering student ...\n"); msg.mtype = MSG_TYPE_REGISTER; /* A hossz az mtype tagvaltozo nelkul ertendo */ msgsize=sizeof(struct studentinfo_msg)-sizeof(long); if((msgsnd(mqid, (struct msgbuf *)&msg,msgsize, 0)) ==-1) { fprintf(stderr,"Cannot send registration information.\n"); return -1; } printf("Student has been registered successfully.\n"); return 0; }
6.1.6 Megosztott memória (Shared Memory) A megosztott memória egy közös memóriatartomány, amelyhez több folyama t is hozzáférhet. A megosztott memória a leghatékonyabb módja a folyamatok közti kommunikációnak, mert a közös memóriatartomány a hívó folyamat címterébe lapolódik, és a közvetlen memóriahozzáférés gyorsabb, mint bármely más eddig tárgyalt System V IPC mechanizmus. Az exit és exec rendszerhívások esetén a rendszer az összes megosztott memória eroforrást lecsatolja, fork esetén a gyermek folyamat örökli a szülohöz csatolt összes megosztott memóriatartományt.
KONKURENS PROGRAMOZÁS 99 Az eddigiekkel összhangban módon megosztott memóriát a #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int shmflg);
függvénnyel hozhatunk létre. A key létrehozásnál egyedi azonosító, amit legtöbbször egy ftok hívással generálunk. Ha a key argumentum nem egyedi a jelzobitek (shmflag) értékétol függoen hiba történik, vagy egy már létezo megosztott memóriatartomány azonosítóját kapjuk vissza. A size argumentum a kívánt memória mérete byte-okban, a lefoglalt memória mérete a size érték felkerekítve a PAGE_SIZE legkisebb többszörösére. Az shmflg értéke a már ismert hozzáférési jogosultság beállításaiból és az opcionális IPC_CREAT és IPC_EXCL jelzobitekbol. Amennyiben szeretnénk használni egy már létrehozott memóriatartományt az azonosító segtségével, akkor elobb hozzá kell csatolnunk (attach) a folyamat címtartományához. Ezt a #include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
hívással tehetjük meg, amely visszaad egy pointert a lefoglalt és a folyamat címtartományába leképezett megosztott memóriatartományra. Ha az shmaddr paraméterben megadunk egy memóriacímet és beállítjuk az SHM_RND jelzobitet, akkor a visszaadott cím az shmaddr értéke lesz lekerekítve a legközelebbi laphatárra. Ha az SHM_RND nincs beállítva, akkor shmaddr argumentumnak címhatárra igazított értéket kell tartalmaznia. A gyakorlati esetek többségében azonban ez a paraméter nulla, ami a rendszerre bízza az megfelelo címtartomány kiválasztását. Az shmflg argumentum a már említett SHM_RND értéken kívül SHM_RDONLY lehet, ami csak olvasásra csatolja a mehgosztott memóriát a folyamat címtartományához. Mivel most már van egy mutatónk, annak segítségével tudjuk írni és olvasni a megosztott memóriatartományt. Ha már nincs szükségünk többet a megosztott memória eroforrásra, azt le kell csatolni (detach), amit a #include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
függvény meghívásával kell végrehajtanunk, melynek egyetlen paramétere az shmat függvény által visszaadott mutató. A vezérlést ezúttal a #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
KONKURENS PROGRAMOZÁS 100 Az elso paraméter a megosztott memória azonosítója, a második paramétert a következo táblázat foglalja össze. Parancs IPC_STAT IPC_SET IPC_RMID SHM_LOCK SHM_UNLOCK
Leírás Információ a megosztott memóriáról. Hozzáférési jogosultságok és azonosítók megváltoztatása. A megosztott memória megszuntetése. A megosztott memória mindvégig a fizikai memóriában marad. Linux specifikus. Engedélyezi a swapping-et.
Az utolsó paraméter felépítése: struct shmid_ds { struct ipc_perm shm_perm; /*Hozzáférés és azonosítók beállítása */ int shm_segsz; /* A memóriatartomány mérete (byte-okban) */ time_t shm_atime; /* A legutolsó felcsatolás ideje */ time_t shm_dtime; /* A legutolsó lecsatolás szerepet*/ time_t shm_ctime; /* Az utolsó változás ideje */ unsigned short shm_cpid; /* A létrehozó folyamat azonosítója */ unsigned short shm_lpid; /* Az utolsó muvelet végrehajtójának azonosítója */ short shm_nattch; /* Az aktuális felcsatolások száma */ /*------------ A többi tagot a rendszer használja -----------*/ unsigned short shm_npages; /* A tartomány mérete (lapok száma) */ unsigned long *shm_pages; struct vm_area_struct *attaches; /* A felcsatolások leírója };
*/
Például a megosztott memóri megszüntetése a shmctl(shmid, IPC_RMID, 0);
hívással történhet. Ha nincs olya n folyamat, amelyik csatolva tartaná az eroforrást, azonnal megszunik, ha nem, akkor csak mintegy megjelöli megszüntetésre a megosztott memória eroforrást, a tényleges eltávolítás csak az utolsó lecsatolás után történik meg. A fentiek alapján nézzünk meg egy egyszeru példát! Példa. Írjunk programot, mely létrehoz egy megosztott memóriát, ír rá valamilyen adatot, majd készítsünk egy másik programot, amely kiolvassa mindezt. Ügyeljünk arra, hogy nem maradjon felszabadítatlan eroforrás a Kernelben (használjuk az ipcs segédprogramot)! /* common.h */ #ifndef COMMON_H #define COMMON_H
KONKURENS PROGRAMOZÁS 101
#define MEMORY_SIZE 10 #define SHM_ID 1234 #endif /* COMMON_H */ /* shmem.c: megosztott memoria letrehozasa */ #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include "common.h" int main() { int shmid; char c; char* shmptr; /* A megosztott memoria letrehozasa */ if ((shmid = shmget(SHM_ID, MEMORY_SIZE, IPC_CREAT | 0666)) < 0) { fprintf(stderr,"Error creating shared memory resource\n"); exit(1); } /* A megosztott memoria felcsatolasa */ if((shmptr=shmat(shmid,0,0))==(char*)-1) { fprintf(stderr,"Error attaching shared memory resource\n"); exit(1); } printf("Writing data to memory...\n"); for(c=0;c<9;c++) { shmptr[c]='1'+c; /*123456789*/ } shmptr[9]='\0'; printf("Press <Enter> to continue\n"); getchar(); /* A megosztott memoria lecsatolasa */ shmdt(shmptr); /* A megosztott memoria megszuntetese */ shmctl(shmid, IPC_RMID, 0); } /* shmrd.c: megosztott memoria kiolvasasa */ #include #include #include #include
<sys/ipc.h> <sys/shm.h> <stdio.h> "common.h"
int main()
KONKURENS PROGRAMOZÁS 102 { int shmid; char c; char* shmptr; /* A mar letezo megosztott memoria leirojanak lekerdezese */ shmid = shmget(SHM_ID, MEMORY_SIZE, 0666); if (shmid < 0) { fprintf(stderr,"Error accessing shared memory resource\n"); exit(1); } /* A megosztott memoria felcsatolasa */ shmptr=shmat(shmid,0,0); if(shmptr==(char*)-1) { fprintf(stderr,"Error attaching shared memory resource\n"); exit(1); } printf("Shared memory data: %s\n",shmptr);
/* A megosztott memoria lecsatolasa */ shmdt(shmptr); }
6.1.7 Jelzések (Signals) A jelzések (signals) az egyik legegyszerubb folyamatok közötti kommunikációfajta a POSIX világban. Arra szolgálnak, hogy valamilyen eseményrol soron kívül értesítsék az adott programot. A program reakciója ilyenkor hasonlít a megszakításkezelésre, amellyel gyakran párhuzamba állítják. A szignál érkezésekor a programon belül meghívódik egy lekezelo függvény, majd ennek végén a processz ott folytatja a futását, ahol abbahagyta. A szignálokat használjuk egy processz leállítására, vagy pl. arra hogy jelezzük a folyamatnak, hogy olvassa újra a konfigurációs állományát. Számos különbözo eseményrol hordozhatnak információt, azonban egy dologban egyeznek: mindegyikük aszinkron. Amikor egy processz jelzést kap, az alábbi 3 választási lehetosége van: ?? Figyelmen kívül hagyja a szignált. ?? A kernel lefuttatja a processz egy meghatározott részét mielott engedi tovább futni. ?? A kernel az alapértelmezett lekezelo metódusát hajtja végre. 6.1.7.1 Jelzések küldése A jelzéseket az egyik folyamat a másiknak a már korábban tárgyalt kill() rendszerhívással küldheti el.
KONKURENS PROGRAMOZÁS 103 6.1.7.2 A sigset_t használata Minden POSIX jelzés kezelo függvény egy szignál listát kap paraméterként többek között. A sigset_t adat típus egy szignál listát reprezentál és a signal.h állományban van definiálva. A POSIX szabvány 5 függvényt definiál ennek a listának a kezelésére: int sigemptyset(sigset_t *set);
Törli a szignál listát. int sigfillset(sigset_t *set);
Minden lehetséges szignált hozzáad a listához. int sigaddset(sigset_t *set, int signum);
A signum által meghatározott szignált hozzáadja a listához. int sigdelset(sigset_t *set, int signum);
A signum által meghatározott szignált törli a listából. int sigismember(const sigset_t *set, int signum);
Nem 0-val tér vissza, ha a szignál szerepel a listában, és 0- val ha nem. Ezen függvények csak akkor térnek vissza hibával, ha a megadott szignál inkorrekt. 6.1.7.3 Jelzések lekezelése A POSIX programok a szignálok lekezelo függvényét a sigaction() függvényhívással regisztrálják. #include <signal.h> int sigaction(int signum, struct sigaction * act, struct sigaction * oact);
Ez a rendszerhívás beállítja a signum által meghatározott szignálra az act lekezelot. Ha az oact nem NULL, akkor a korábbi lekezelo értékét veszi fel. Ha az act értéke NULL, akkor a függvény nem változtatja a lekezelot, azonban ezzel a megoldással megkaphatjuk a címét. A lekezelo függvényt a sigaction struktúra ír ja le: #include <signal.h> struct sigaction { sighandler_t sa_handler; sigset_t sa_mask; unsigned long sa_flags; void (*sa_restorer)(void); };
Az sa_handler egy függvény pointer, amelynek prototípusa a következo:
KONKURENS PROGRAMOZÁS 104 void handler(int signum);
Ahol a signum értéke a szignál száma, amely a függvényt meghívta. A struktúra továbbá tartalmaz egy szignál listát (sa_mask). Ezeket a szignálokat a rendszer blokkolja a lekezelo függvény futásának idejére. Azonban az a jelzés, amely a lekezelo hívását kiváltotta, mindenképpen blokkolásra kerül függetlenül attól, hogy ebben a listában szerepel-e vagy sem. (Ha mégsem akarjuk, hogy blokkolódjon, akkor azt az sa_flags paraméterrel befolyásolhatjuk.) Az sa_flags változó értéke az alábbiakból tevodik össze (bites VAGY kapcsolattal) : Parancs Leírás SA_NOCLDSTOP Normál esetben ha egy folyamat gyerek processze terminálódik, vagy megáll, akkor egy SIGCHLD szignált küld. Ha ezt az opciót beállítjuk a SIGCHLD szignálra, akkor csak a terminálódás esetén küldi. SA_NOMASK Amikor a szignál lekezelo függvénye meghívódik, akkor a szignál nem blokkolódik automatikusan. (Használata nem javasolt, csak speciális esetekben.) SA_ONESHOOT Miután a lekezelo meghívódott a beállítás visszaáll az eredetire. SA_RESTART Ha a szignál beköve tkezésekor éppen egy lassú rendszer hívás van folyamatban, akkor visszatéréskor a rendszerhívást újra kezdi. (Csak az SA_NOCLDSTOP értéket definiálja a POSIX szabvány, így a többit célszeru mellozni a portolási lehetoségek érdekében.) 6.1.7.4 A jelzések maszkolása Általános hogy a szignál lekezelok olyan adat struktúrákat módosítanak, amelyeket a program más részeinél használunk. Azonban a szignálok aszinkron volta komoly szinkronizációs problémákat vet fel, mert szerencsétlen esetben versenyhelyzetek alakulhatnak ki. Ahhoz, hogy ezt elkerüljük, szükségessé válhat, hogy egyes jelzéseket bizonyos idoszakra blokkoljunk. Ezt a listát, amelyben a blokkolt szignálokat megadjuk, a processz jelzés maszkjának nevezzük. A jelzés maszk egy sigset_t adat struktúra, amely a blokkolt jelzéseket tartalmazza. A sigprocmask() függvény teszi lehetové, hogy a processz szabályozhassa az aktuális jelzés maszkját. #include <signal.h> int sigprocmask(int what, const sigset_t * set, sigset_t * oset);
Az elso paraméter, a what, azt határozza meg, hogy a jelzés maszkot hogyan módosítjuk. Értékei: Parancs SIG_BLOCK
Leírás A set-ben levo szignálok hozzáadódnak a maszkhoz.
KONKURENS PROGRAMOZÁS 105 Parancs SIG_UNBLOCK SIG_SETMASK
Leírás A set-ben lévo szignálok törlodnek a maszkból. A set tartalma állítódik be mint új maszk.
Ha a set paraméter értéke NULL, akkor az elso paramétert nem veszi figyelembe a rendszer. Ilyenkor csak a maszk aktuális állapotát kérjük le. Mindhárom esetben az oset változó értéke az eredeti maszk. (Kivéve, ha NULL.) 6.1.7.5 Az aktív jelzések Azon jelzések listáját, amelyek éppen aktívak (érkezett szignál, de jelenleg éppen blokkolt), könnyen lekérdezhetjük. #include <signal.h> int sigpending(sigset_t * set);
A set változó tartalmazza az aktív jelzések listáját. 6.1.7.6 Jelzésre várakozás A jelzéseket használó programok esetén gyakori feladat, hogy egy szignálra kell várakoznunk. Ezt a pause() függvénnyel könnyen megtehetjük. #include int pause(void);
A pause() függvény addig várakozik, amíg a processz egy szignált kap. Ha az adott szignálnak lekeze lo függvénye is van, akkor az meg a pause() visszatérése elott lefut. A pause() függvény mindig –1 értékkel tér vissza és az errno értéke EINTR. A sigsuspend() függvény egy másik lehetoséget jelent a processz felfüggesztésére. #include <signal.h> int sigsuspend(const sigset_t * mask);
A sigsuspend() muködése megegyezik a pause() függvényével egy kivétellel. Meghívásakor a processz jelzés maszkját a paraméterben megadottra állítja. A szignál megérkezésekor pedig visszaállítja az eredetire. Ez lehetové teszi, hogy csak meghatározott jelzésekre várakozzon a program. 6.1.7.7 Jelzések listája Jelzés Leírás SIGABRT Az abort() függvény generálja. SIGALRM Egy alarm() lejárt. SIGBUS Hardware függo hiba. SIGCHLD Gyerek processz terminálódott.
KONKURENS PROGRAMOZÁS 106 Jelzés SIGCONT SIGHUP SIGFPE SIGILL SIGINT SIGIO SIGKILL SIGQUIT SIGPIPE SIGPROF SIGPWR SIGSEGV SIGSTOP SIGTERM SIGTRAP SIGTSTP SIGTTIN SIGTTOU SIGWINCH SIGURG SIGUSR1 SIGUSR2 SIGXCPU SIGXFSZ SIGVTALRM
Leírás A processz a leállítás után folytatja a futását. A processz terminálját becsukták. Lebegopontos számítási hiba. Illegális parancsot hajtott végre a rendszer. A felhasználó interrupt karaktert (^C) küldött. Aszinkrion I/O érkezett. Nem lekezelheto processz terminálás. A felhasználó kilépés karaktert (^\) küldött. A processz olyan pipe-ba írt, amelynek nincsen olvasója. A profil idozíto lejárt. Tápfeszültség hiba. Illegális memória kezelés. Leállítja a processzt terminálás nélkül. Lekezelheto processz terminálás. Töréspont. A felhasználó felfüggesztés karaktert (^Z) küldött. A háttérben futó alkalmazás megpróbálta olvasni a terminált. A háttérben futó alkalmazás írni próbált a terminálra. A terminál mérete megváltozott. Sürgos I/O esemény. Processz függo szignál. Processz függo szignál. CPU limit túllépés. File méret limit túllépés. A setitimer() idozíto lejárt.
6.2 Szálak és szinkronizációjuk A szálak könnyusúlyú folyamatok (Lightweight Processes), ellentétben a folyamatokkal. A folyamatoknak öt alapveto része van: ?? kód ?? adat ?? verem ?? file leírók ?? jelzéstáblák Amikor a folyamatok közt váltani kell (context switching), ezeket az adatstruktúrákat fel kell tölten az új folyamatnak megfeleloen, ami idot vesz igénybe. A szálak esetén ezek az adatstruktúrák közösek, tehát gyorsabban lehet váltani közöttük. A folyamatok között csak a kód rész közös, míg a szálak ugyanabban a címtartományban futnak. Ez sokkal könnyebben használható kommunikációs lehetoséget biztosít a szálak között. Linux alatt alapvetoen kétféle száltípus van: ?? Felhasználói módban ?? Kernel módban futó szálak.
KONKURENS PROGRAMOZÁS 107 A felhasználói módban futó szálak nem használják a Kernelt az ütemezéshez, maguk bonyolítják le az ütemezést. Ezt a megoldást kooperatív multitaszking nek nevezik, ahol egy folyamat definiál olyan függvényeket, amire átkapcsolhat a rendszer. Valamilyen módon a szál expliciten jelzi, hogy átadja a futás lehetoségét egy másik szálnak, vagy valamilyen idozítés alapján történik a váltás. Általában a felhasználói módban futó szálak esetén a váltás gyorsabb, mint a Kernel módban futónál. Ennek a megoldásnak további hátrányai, hogy egy szál nem adja át a CPU idoszeletet más szálaknak, így éhezés (starving) alakulhat ki a várakozó szálak között. Továbbá nem tudja kihasználni az operációs rendszer szimmetrikus multiprocesszor (SMP) támogatását. Végül amikor egy szinkron I/O hívás blokkol egy szálat, akkor a többi szál nem tud futni. Mindezekre a problémára léteznek megoldások (külso monitorozás az éhezés ellen, a szálak különbözo processzoron futtatása, szinkron I/O muveletre csomagoló (wrapper) függvények írása, amely nem blokkol), de ezek a megoldások sorá operációs rendszertol elvárt funkciókat implementálnak az operációs rendszer fölé, ami nehézkessé teszi ezeket a megoldásokat. A Kernel módban futó szálkezelés esetén a szálak automatikusan ki tudják használni az SMP elonyeit, az I/O blokkolás nem probléma, és Linux alatt a váltás nem sokkal lassabb a felhasználói módban implementált szálvezérlésnél tapasztaltnál. Ilyenkor a Kernel tartja számon a szálakhoz tartozó adatstruktúrákat és ütemezi a szálakat. Linux alatt találhatunk felhasználói módban futó szálkezeléshez programkönyvtárakat, és a Kernel a 1.3.56 verzió óta támogatja a Kernel módban futó szálkezelést. A továbbiakban a Kernel módú szálkezelést vizsgáljuk bemutatva a POSIX kompatibilis pthread programkönyvtárat.
6.2.1 Szálak létrehozása A Linux kernel módú szálkezlésének a kulcsa a #include <sched.h> int clone(int(*fn)(void*),void *child_stack,int flags,void *arg)
függvény. Ez a már részletezett fork függvény egy kiterjesztésének tekintheto. Az elso paraméter az indítandó folyamat vagy szál belépési pontja, a második a veremmutató, a harmadik argumentum pedig a megadott függvénynek átadandó paraméter. A flags argumentum finomítja a folyamat vagy szál létrehozását. A clone függvényhívásra épül a Linux pthread könyvtárának inicializáló függvénye: #include int pthread_create(pthread_t * thread, pthread_attr_t *attr, void * (*start_routine)(void *), void * arg);
Ezzel a függvénnyel egy szálat indíthatunk el, melynek belépési pontja a harmadik argumentumban megadott start_routine függvény, melynek prototípusa void* start_routine(void* param);
KONKURENS PROGRAMOZÁS 108 formában deklarálható. Ez a függvény vár egy paramétert, amit a pthread_create függvény utolsó paramétereként adhatunk meg. A thread argumentumban a szál leíróját kapjuk vissza. Az attr paraméter NULL esetén az alapértelmezett beállításokat jellemzi. Nézzünk most egy egyszeru példát szál indítására! /* theard1.c: Szál indítása */ #include #include <stdlib.h> #include <stdio.h>
void *thread_function(void *arg) { int i; printf("Thread started... \n"); for ( i=1; i<=20; i++ ) { printf("%d. Hello thread world!\n",i); sleep(1); } printf("Thread exiting... \n"); return NULL; } int main(void) { pthread_t mythread; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { fprintf(stderr,"Error creating thread.\n"); exit(1); } printf("Main thread exiting...\n"); return 0; }
Fordítsuk le a fenti programot: gcc thread1.c -o thread1 –lpthread
Amikor a pthread könyvtárat használjuk, azt hozzá is kell linkelnünk a programunkhoz, amit a –lpthread kapcsolóval tehetünk meg. Fordítás után indítsuk is el: $ ./thread1 Main thread exiting...
Ami feltehetoleg nem az az eredmény, amire számítottunk, ugyanis nem jelent meg a terminálon a szálban található printf hívások eredménye. Viszont hibát sem kaptunk, a foprogram sikeresen befejezte muködését. A magyarázat abban rejlik, hogy a foprogram külön szálként tovább fut a következo, a
KONKURENS PROGRAMOZÁS 109
printf("Main thread exiting...\n");
sorra, majd a main függvénybol kilép a vezérlés, ami terminálja a folyamatot, és kilépéskor leállítja a szálakat, és felszabadítja a folyamathoz tartozó összes eroforrást. Próbaképpen helyezzünk el egy késleltetést a programban a mielott a main függvénybol kilépünk: ... sleep(5); printf("Main thread exiting...\n"); return 0; ...
Ezután a futási eredmény már hasonlít az eredeti elképzeléshez: $ ./thread1 Thread started... 1. Hello thread world! 2. Hello thread world! 3. Hello thread world! 4. Hello thread world! 5. Hello thread world! Main thread exiting...
A késleltetés nem volt elég ahhoz, hogy a szál befejezze muködését, de már látjuk, hogy elindult és futott egy darabig. Ezután persze növelhetnénk az idozítést, de mivel minden hardveren más az ütemezés idobeli lefolyása, nem építhetünk rá. Szükségünk van egy olyan függvényre, amellyel a szálat indító függvénybol (példánk esetében a main függvénybol) meg tudnánk várni a szál lefutását. Erre kínál megoldást a #include int pthread_join(pthread_t thread, void **thread_return);
függvény, amely felfüggeszti a hívó szál muködését mindaddig, amíg a thread argumentumban megadott szál be nem fejezi futását. Ha a thread_return nem NULL, akkor az a szál visszatérési értékére mutat a sikeres visszatérés után. Módosítsuk a main függvényt mindezeknek megfeleloen: int main(void) { pthread_t mythread; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { fprintf(stderr,"Error creating thread.\n"); exit(1); } if ( pthread_join ( mythread, NULL ) ) { fprintf(stderr,"Error joining thread.\n"); exit(1); }
KONKURENS PROGRAMOZÁS 110
printf("Main thread exiting...\n"); return 0; }
Ezek után térjünk vissza a szál attribútumainak beállítására és lekérdezésére! A szál attribútumait a typedef struct __pthread_attr_s { int __detachstate; int __schedpolicy; struct __sched_param __schedparam; int __inheritsched; int __scope; /**** Nem állítható tulajdonságok ****/ size_t __guardsize; int __stackaddr_set; void *__stackaddr; size_t __stacksize; } pthread_attr_t;
struktúrában adhatjuk meg kizárólag a létrehozáskor a pthread_create függvény argumentumaként. A pthread_attr struktúra leírását a @. Táblázatban találjuk. A pthread_attr struktúrát a #include int pthread_attr_init(pthread_attr_t *attr);
függvény segítségével tölthetjük fel alapértelmezett értékekkel. A #include int pthread_attr_destroy(pthread_attr_t *attr);
fügvénnyel megszüntethetjük az attribútumot, melyet csak egy új pthread_attr_init függvényhívás után használhatunk. Ennek a függvénynek a Linux alatti implementációja semmit nem csinál, de ennek ellenére a POSIX kompatibilitás miatt érdemes lehet használni. Attribútum Attribútum leírása detachstate Két értéke lehet, PTHREAD_CREATE_JOINABLE (alapértelmezés), illetve PTHREAD_CREATE_DETACHED. Az elso esetben a szál kilépésére várakozhat egy másik szál (pthread_join fügvénnyel), és a szál kilépése után is fenntart eroforrásokat, amire csak a másik szál csatlakozása esetén van szükség. Ezek az eroforrások a PTHREAD_CREATE_DETACHED esetén rögtön a szál kilépésekor felszabadulnak, azonban a szál kilépésére nem tudunk várakozni.
KONKURENS PROGRAMOZÁS 111 Attribútum Attribútum leírása schedpolicy Lehet SCHED_OTHER, ami az alapértelmezett nem valósideju ütemezés, valamint két valósideju ütemezés, SCHED_RR egy körbenforgó (round-robin), a SCHED_FIFO egy FIFO prioritást jelent. A két utóbbihoz a folyamatnak super user jogokkal kell rendelkeznie. Schedparam Az ütemezési prioritás a két valósideju ütemezésre. Inheritsched Az alapértelmezés PTHREAD_EXPLICIT_SCHED, ha az új szál beállításai (shedpolicy, shedparam) a mérvadóak, PTHREAD_INHERIT_SCHED, ha a létrehozó szál beállításait veszi át az új szál. scope Az egyetlen lehetséges érték Linux alatt a PTHREAD_SCOPE_SYSTEM. A fenti paramétereket a következo függvényekkel értelemszeruen állíthatjuk: #include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); int pthread_attr_getschedpolicy(const *attr, int *policy);
pthread_attr_t
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit); int pthread_attr_getinheritsched(const *attr, int *inherit); int pthread_attr_setscope(pthread_attr_t scope);
pthread_attr_t
*attr,
int pthread_attr_getscope(const pthread_attr_t *attr, *scope);
int
int
A szál ütemezési paramétereit létrehozás után is változtathatjuk illetve lekérdezhetjük a #include int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param);
KONKURENS PROGRAMOZÁS 112 int pthread_getschedparam(pthread_t struct sched_param *param);
target_thread, int *policy,
függvények segítségével, melynek argumentumai megegyeznek a fent tárgyalt pthread_attr_t struktúra megfelelo tagjaival.
6.2.2 Kölcsönös kizárás (Mutex) A kölcsönös kizárás (Mutual Exclusion) hasznos eszköz szálak szinkronizációjára. A mutexnek két lehetséges állapota van: foglalt (locked), amikor egy szál birtokolja a mutexet, illetve szabad (unlocked) állapot, amikor egy szál sem birtokolja a mutexet. Egyszerre csak egy szál birtokolhatja a mutexet. Linux alatt a mutexeknek három fajtája van: ?? Gyors ?? Rekurzív ?? Hibaellenorzo A mutex fajtája azt mondja meg, hogy mi történik akkor, ha egy olyan szál próbál lefoglalni egy mutexet, ami már a birtokában van. Gyors szál esetén az adott szál arra várakozik, hogy a saját mag által már lefoglalt mutex felszabaduljon, amit csak ez a szál tudna felszabadítani, vagyis gyors mutex esetén végtelen ciklusba kerülünk. A hibaellenorzo mutex esetén a lefoglalást végzo függvény rögtön hibával tér vissza. Rekurzív mutex esetén a szál újra lefoglalja a mutexet, és annyiszor kell elengednie, ahányszor lefoglalta ahhoz, hogy a mutex más szálak által újra lefoglalhatóvá váljon. A mutex fajtáját a változó inicializálásakor is megadhatjuk: #include pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
Az NP utótag a nem hordozható (non-portable) makrókra utal. Mutexet a fenti statikus inícializáció helyett függvényhívással is létrehozhatunk: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
függvénnyel hozhatunk létre. Az utolsó paraméterrel jelenleg Linux alatt csak az attribútum fajtáját állíthatjuk. A mutex argumentumban kapjuk vissza a mutex objektumra mutató pointert. Mutex felszabadítását a #include int pthread_mutex_destroy(pthread_mutex_t *mutex);
KONKURENS PROGRAMOZÁS 113
fügvénnyel kell elvégeznünk, melynek feltétele, hogy a hívás pillanatában a mutex ne legyen foglalt. A jelenlegi Linux implementáció mindössze ellenorzi a szabad állapotot. Mutex lefoglalását a #include int pthread_mutex_lock(pthread_mutex_t *mutex);
fügvénnyel végezhetjük. Ha a mutex éppen szabad, akkor a pthread_mutex_lock lefoglalja, egyébként a hívó szál addig fel lesz függesztve, amíg a mutex szabaddá nem válik, és akkor kerül a hívó szál birtokába. Ennek a függvénynek a hátránya az, hogyha nem szabad a mutex, akkor a szál várakozik. Ha nem szeretnénk, hogy a függvény várakozzon, hanem csak annyit, hogy ha szabad a mutex, akkor lefoglalja, egyébként rögtön visszatér jelezve, hogy a mutex foglalt, akkor a #include int pthread_mutex_trylock(pthread_mutex_t *mutex);
függvényt használjuk, amely EBUSY értékkel tér vissza, ha a mutex foglalt, és nem blokkolja a szálat. Ha megszereztük a mutexet, akkor azt a #include int pthread_mutex_unlock(pthread_mutex_t *mutex);
függvénnyel engedhetjük el.
6.2.3 Feltételes változók (Condition Variable) A feltételes változók olyan szink ronizációs objektumok, amelyek lehetové teszik, hogy a szálak felfügesszék futásokat mindaddig, amíg egy eroforrásra igaz nem lesz valamilyen állítás. A feltételes változókon két muvelet végezheto, az egyik a várakozás a jelzésre, a másik muvelet a maga jelzés. Ilyenkor a feltételes változóra várakozó szálak közül egy elindul. A jelzés egy pillanatnyi esemény, a jelzés idopontjában éppen a jelzésre várakozó szálakat érinti, azok a szálak, amelyek a jelzés után kezdik meg várakozásukat, csak a következo jelzés veszi figyelembe. Mivel a jelzés csak egy pillanatnyi esemény, elofordulhat, hogy az egyik szál már készül a várakozásra, már meghívta a megfelelo várakozó függvényt, de a jelzést már lekési, ha a várakozó szál nem kap idoszeletet a futásra, illetve ne m kési le, ha a váarakozó szál elobb kap idoszeletet a várakozásra. Ez tehát kritikus versenyhelyzetet eredményez. Erre a problémára megoldást jelent, ha egy mutexet használunk, amelyet lefoglalunk, mielott meghívjuk a várakozó függvényt, és amikor az ténylegesen megkezdi a várakozást, automatikusan elengedi a mutexet, majd
KONKURENS PROGRAMOZÁS 114 amikor visszatér, újra megszerzi. Mindez természtesen nem elég, a versenyhelyzet elkerüléséhez az is szükséges, hogy mielott kiadunk egy jelzést, foglaljuk le a mutexet, majd a jelzés kiadása után engedjük is el. Így ha a jelzés kiadása elott valamelyik szál már elkezdett készülodni a várakozásra, az már megszerezte a mutexet, így a jelzés kiadása elott megvárjuk (a mutex szabad állapotára várakozva), amíg az megkezdi várakozását, és a vá rakozó függvény automatikusan elereszti a mutexet, és csak akkor adjuk ki a jelzést. Amikor a jelzés kiadása elott lefoglaljuk a mutexet, akkor egy „aki bújt, aki nem” jelleggel kiadjuk a jelzést a feltételes változóra már várakozó szálaknak, akik ez után szeretnének várakozni a feltételes változóra, meg kell szerezniük a mutexet, ami már csak a jelzés után lehetséges. Vagyis egy feltételes változóhoz mindig hozzá van rendelve egy mutex. A feltételes változókhoz kapcsolódó fügvények nem jelzésbiztosak (signal safe), ami azt jelenti, hogy jelzéskezelo fügvényekbol (signal handler) nem hívhatók. Feltételes változókat statikusan az alábbi kódrészlettel hozhatjuk létre: #include pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Egyébként pedig a #include int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
fügvénnyel inícializáljuk a feltételes változót, ahol ha a cond_attr NULL, akkor a feltételes változó az alapértelmezett paraméterekkel jön létre, egyébként, mivel a jelenlegi Linux implementáció nem definiál attribútumokat a feltételes változók számára, a cond_attr paramétert a pthread_cond_init függvény figyelmen kívül hagyja. Feltételes változót a #include int pthread_cond_destroy(pthread_cond_t *cond);
függvénnyel szütethetünk meg, ekkor egy szál sem várakozhat a feltételes változóra, ez utóbbi feltétel ellenorzésében ki is merül a függvény jelenlegi Linux implementációjának funkcionalitása. Jelzést a #include int pthread_cond_signal(pthread_cond_t *cond);
függvénnyel küldhetünk.
KONKURENS PROGRAMOZÁS 115 Ha azt szertenénk, hogy az összes szál, amely egy feltételes változóra várkozik, megkezdje futását, akkor a #include int pthread_cond_broadcast(pthread_cond_t *cond);
függvényt használjuk. Ne felejtsük el a fenti két függvény használata elott lefoglalni a mutexet, majd utána felszabadítani! Ha a várakozni szeretnénk egy feltételes változóra, akkor a #include int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
függvényt használjuk. A függvény mghívása elott le kell foglalnunk a mutexet, majd a meghívás után el kell engednünk. Amennyiben csak meghatározott ideig szeretnénk várakozni, akkor a #include int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
függvényt használjuk, a mutex szempontjából ugyanúgy, mint a pthread_cond_wait hívást. Az egyetelen különbség, hogy a függvény az abstime argumentumban megadott idonél többet nem vár, hanem újra lefoglalja a mutexet, és ETIMEDOUT értékkel tér vissza. Az ido beállítását 10 másodpercre a következo példa szemlélteti: struct timeval now; struct timespec timeout; gettimeofday(&now); timeout.tv_sec = now.tv_sec + 10; timeout.tv_nsec = now.tv_usec * 1000;
6.2.4 Szemaforok A POSIX szemaforok egyetlen szinkronizációs objektumot jelentenek (szemben a System V IPC szemafortömbjével). Névtelen szemafort a következo függvénnyel hozhatunk létre: #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
Ha a pshared nem 0, akkor a szemaforhoz más folyamatok is hozzáférhetnek, egyébként csak az adott folyamat szálai. A szemafor értéke a value paraméterben megadott szám lesz. A Linux nem támogatja a folyamatok közötti szemaforokat, ezért nem nulla pshared argumentum esetin a függvény mindig hibával tér vissza.
KONKURENS PROGRAMOZÁS 116 Egy sem szemafort a #include <semaphore.h> int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem);
függvényekkel foglalhatunk le. Ha a sem szemafor értéke pozitív, mindkét függvény lefoglalja, és visszatér 0- val. Ha a szemafort nem lehet lefoglalni (vagyis a szemafor értéke 0), a sem_trywait azonnal visszatér EAGAIN értékkel, míg a sem_wait várakozik a szemaforra. Ez utóbbi várakozást vagy a szemafor állapota (az értéke nagyobb lesz, mint nulla), vagy egy jelzés szakíthatja meg. A szemafort használat után a #include <semaphore.h> int sem_post(sem_t *sem);
függvénnyel engedhetjük el. A szemafor aktuális értékét a #include <semaphore.h> int sem_getvalue(sem_t *sem, int *sval);
függvénnyel kérdezhetjük le. Ha a szemafort lefoglalták, akkor a visszatérési érték nulla vagy egy negatív szám, melynek abszolút értéke megadja a szemaforra várkozó folyamatok számát. Ha a sval pozitív, a szemafor aktuális értékét jelenti. Névtelen szemafort a #include <semaphore.h> int sem_destroy(sem_t *sem);
függvénnyel szüntethetünk meg. Példa. Hogyan kellene módosítanunk az @@ fejezet példa programját, hogyha egy közös sor van, és a soron következo utas mindig a leghamarabb megüresedett pulthoz menne? Programunk legyen POSIX kompatibilis!
6.2.5 Core-dump mechanizmus adaptálása A core-dump mechanizmus a korábbi Linux kernel verziók esetén több szálú környezetben nem muködött megfeleloen. A 2.4-es kernel verzióknál azonban már foglalkoztak a problémával. A 2.4.6-os verzióban lekezelték, hogy az egyes szálak által generált core állományok ne írják felül egymást, amellyel a legkomolyabb probléma megoldódott. Azonban a következokben mutatunk egy megoldást, amely segítséget jelent a korábbi kernel verziók esetén is. void CoreDumpRestore() { sigaction(SIGABRT, &SigABRT, NULL); sigaction(SIGFPE, &SigFPE, NULL);
KONKURENS PROGRAMOZÁS 117 sigaction(SIGSEGV, &SigSEGV, NULL); } void CoreDumpHandler(int signum) { //restore signal handlers CoreDumpRestore(); if(fork()==0) abort(); struct rlimit rl; rl.rlim_cur=0; rl.rlim_max=0; setrlimit(RLIMIT_CORE, &rl); chdir("/proc"); abort(); } void CoreDumpInit() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler=CoreDumpHandler; if((sigaction(SIGABRT, &sa, &SigABRT)==-1) ||(sigaction(SIGFPE, &sa, &SigFPE)==-1) ||(sigaction(SIGSEGV, &sa, &SigSEGV)==-1) ) { //ERROR printf("Core dump handler initialization error.\n"); } sigset_t sset; sigemptyset(&sset); sigaddset(&sset,SIGABRT); sigaddset(&sset,SIGFPE); sigaddset(&sset,SIGSEGV); sigprocmask(SIG_UNBLOCK,&sset,NULL); }
A program elején a CoreDumpInit()-el átállítjuk a signal lekezeléseket a saját handler-ünkre. (A fent látható három szignál generál core dump-ot.) Ezek után ha valamelyik szál hibát követ el, annak meghívódik a szignál lekezeloje. Ez visszaállítja az eredeti lekezeloket és leállítja az összes szálat úgy, hogy ne generálódjon core dump általuk (ezért alkalmazzuk a fent látható megoldásokat, hogy ne keletkezzen core file, mert felülírna a nekünk érdekeset). Vagyis az egyenes ágon az abort leállítja a programot. Ugyanakkor a fork-olt példány pedig az abort() hatására le fogja generálni a core-t, így abban az egy core file-ban a korrekt információk lesznek megtalálhatóak a rendszer aktuális állapotáról.
KÖNYVTÁRAK FEJLESZTÉSE 118
7. Könyvtárak fejlesztése Az elozoekben feltételeztük, hogy lefordított forráskódunk egyetlen futtatható állomány lesz. Viszont felvetodhet a kérdés: szinte minden program használja például a printf( ) függvényt. Nincs rá valami lehetoség, hogy ne tároljuk el a kódját minden egyes programban a merevlemezen, és ne töltsük be annyiszor a memóriába, ahány program használja? Ezekre a kérdésekre ad választ ez a fejezet.
7.1 Bevezetés A programkönyvtár (program library) olyan lefordított (compiled) forráskód, amelyet nem futtatásra, hanem további felhasználás céljából készítettek. Linux alatt a programkönyvtárak három fajtája létezik: ?? statikus könyvtárak (static libraries) ?? megosztott könyvtárak (shared libraries) ?? dinamikusan betöltött könyvtárak (Dynamically Loaded Libraries) – a továbbiakban DL. Linux alatt is használatos a dinamikusan linkelt programkönyvtár (Dynamically Linked Libraries – DLL), amelynek jelentése nem egyértelmu: van aki az utolsó két típus valamelyikére, illetve mindkettore alkalmazza a DLL kifejezést. A programkönyvtár fajtájának megkülönböztetésére a Linux a következo táblázatban látható konvenciót alakította ki. Utótag .a .so.x.y.z .sa
Típus statikus könyvtár megosztott könyvtár x fo y mellék verziószámmal és z kibocsátás (release) számmal a.out formátumú megosztott könyvtár
Gyakran szükségünk van arra, hogy egy adott program milyen könyvtárakat használ. Nézzük meg például a lynx programot: ldd /usr/bin/lynx
Az nm program egy könyvtár szimbólumainak kiíratására való. Annak lekérdezésére, hogy egy adott függvény – jelen esetben a cos – melyik library-ben van, például a nm -o /lib/* /usr/lib/* /usr/lib/*/* \ /usr/local/lib/* 2> /dev/null | grep 'cos$'
lefuttatásával tehetünk lépéseket. Erre válaszul többek között a /usr/lib/libslang.a:slmath.o:
U cosh
sor jelenik meg. Itt az U azt jelenti, hogy a cosh függvény nincs ebben a könyvtárban, de ez a könyvtár hivatkozik rá. Az U helyett állhat W (Weak) illetve T. A W jelentése, hogy az adott szimbólum definiálva van, de olyan módon, hogy más library-
KÖNYVTÁRAK FEJLESZTÉSE 119 k felülírhatják. A T esetében a library tartalmazza a szimbólumot. A fenti szimbólumokon kívül állhat D az inicializált vagy B az inicializálatlan adatszekciót jelezve. Ezek mind globális szimbólumokat jelentenek, míg kisbetus megfeleloik lokálisakat. A könyvtárak elhelyezkedését illetoen a következo konvenciók alakultak ki: A rendszer által használt programkönyvtárak a /usr/lib könyvtárban találhatók, azok, amelyek szükségesek a rendszer indulásához a /lib könyvtárban vannak, végül azok, amelyek nem részei a rendszernek, a /usr/local/lib könyvtárban helyezkednek el. Ha programkönyvtárunk olyan programokat hív meg, amelyeket csak programkönyvtárakon keresztül lehet hívni, azt a /usr/local/libexec könyvtárba helyezzük el. Az XWindow rendszer által használt programkönyvtárak a /usr/X11R6/lib könyvtárban vannak. A továbbiakban áttekintjük a programkönyvtárak típusait, majd a 7.5 fejezetben példákon keresztül részletezzük létrehozásukat és használatukat.
7.2 Statikus programkönyvtárak Történetileg a könyvtáraknak ez a típusa jelent meg legeloször. Gyakorlatilag lefordított (compiled) tárgykódú (object) file-ok gyujteménye. Eredetileg az volt a céljuk, hogy gyorsabbá tegyék a fordítást, de ez ma már nem jelent különösebb elonyt a gyors compilerek miatt. Amennyiben valaki nem szeretné kiadni a forráskódot, ugyanakkor szeretné rendelkezésre bocsátani kódját, szintén használhat statikus könyvtárakat. Létrehozásuk a ar rcs my_library.a file1.o file2.o
példájára történhet, ahol a fenti sor létrehozza a file1.o és a file2.o tárgykódú fileokból a my_library.a statikus könyvtárat az ar segédprogrammal. Ezekbol látható a jelölési konvenció is: a statikus programkönyvtárakat .a utótaggal látjuk el. Felhasználásuk gcc fordító –l paraméterével lehetséges. Célszeru minden lib file-hoz egy vagy több header file-t készíteni a compiler számára. Így használatkor az #include direktíva segítségével megadjuk a prototípust, majd a linker számára elérhetové tesszük a .a utótagú könyvtár file-t, majd a paraméterezésrol sem feledkezünk meg. A gcc fordító alapértelmezése az, hogy ha talál .so kiterjesztésu megosztott könyvtárat, akkor azt linkeli a programba, ellenkezo esetben a statikusat. A szabványos könyvtárak általában mindkét formátumban rendelkezésre állnak.
7.3 Megosztott programkönyvtárak Ezek a könyvtárak a program indulásakor töltodnek be, és a futó programok osztoznak rajta, amivel memóriát és lemezterületet takarítanak meg. Számos lehetoséget nyújtanak: ?? Újabb verziók installálása, ugyanakkor a programok a régi verziót is használhatják ?? Már felinstallált programkönyvtárak egyes részeinek, függvényeinek átdefiniálása ?? Ez megteheto az alatt, amíg futó programok már létezo könyvtárakat használnak
KÖNYVTÁRAK FEJLESZTÉSE 120 A megosztott könyvtárak könnyu használatának egyik leglényegesebb eleme a konvenciók betartása, amire a tárgyaláskor mi is nagy hangsúlyt helyezünk.
7.3.1 Elnevezési szintek Sokszor elofordul, hogy csak a programkönyvtár belso muködésén változtattunk, amelynek nem kellene érintenie a programkönyvtárat használó programokat sem fordítási, sem pedig felhasználói szinten. Ha az elosztott könyvtárakra az oket használó programokból fizikai filenév szerint hivatkoznánk, lehetetlenné tenné a verziószámok változtatását 3 . Ugyanez a probléma fennáll a fejlesztok oldalán is: folyton újra kellene paraméterezni a gcc-t, ha egyújabb verziót állítunk elo. Ezért egy adott megosztott programkönyvtárnak három – a konvenció szerint különbözo – neve lehet. ?? Fizikai név – a könyvtárt tartalmazó file filerendszerbeli neve ?? Az úgynevezett sonév4 (soname) – amelyre a programkönyvtár betöltésekor hivatkozunk ?? Linkernév – amit a fordításkor használunk a gcc paramétereként Az sonév a konvenció szerint „lib” elotaggal kezdodik, amit „.so” követ, végül pont után a fo verziószám, ami csak az interfésszel (a benne lévo függvények prototípusaival vagy az osztályok deklarációival) változik. A fizikai névet az sonévbol úgy képezzük, hogy hozzáadunk egy pontot, a mellék verziószámot, egy újabb pontot és végül a kibocsátás (release) számát. A linkernév az sonévbol keletkezik, ha elhagyjuk a fo verziószámot. Ez a hierarchia teszi lehetové, hogy a fizikai file névtol függetlenül hivatkozzunk egy könyvtárra, vala mint a fizikai névtol függetlenül használjuk a fordításhoz. Így a fizikai névre nincs közvetlen programbeli hivatkozás: a programkönyvtár bármikor frissítheto. A programok belül az sonevekkel hivatkoznak a könyvtárakra. Ezért alakult ki az a konvenció, hogy az azonos interfészt használó programkönyvtárak azonos sonevekkel rendelkeznek.
7.3.2 Megosztott programkönyvtárak létrehozása Az alábbi példa az a.c és a b.c könyvtárakból létrehoz egy libmystuff.so.1 sonevu, libmystuff.so.1.0.1 fizikai nevu könyvtárat. gcc -fPIC -g -c -Wall a.c gcc -fPIC -g -c -Wall b.c gcc -shared -Wl,-soname,libmystuff.so.1 \ -o libmystuff.so.1.0.1 a.o b.o -lc
Ezek után fel kell álltanunk a szimbolikus linkeket: ldconfig -n a_programkönyvtárakat_tartalmazó_könyvtár
Ezek után hozzáadjuk a LD_LIBRARY_PATH környezeti változóhoz a könyvtárunk helyét, és ettol fogva hivatkozhatunk a programkönyvtárunkra. 3
A Microsoft Windows operációs rendszerek DLL verzióinak nyomonkövethetetlen kuszasága kituno példa arra, hogy megéri behatóbban foglalkozni ezzel a kérdéssel. 4 ejtsd „es-o-név”
KÖNYVTÁRAK FEJLESZTÉSE 121
7.3.3 Megosztott könyvtárak betöltése A programkönyvtárak betöltését a /lib/ld–linux.so.X (ahol X a verziószám) programkönyvtár végzi, amely betölti az összes megosztott programkönyvtárat, amelyet az adott program használ. Azt, hogy a betöltés folyamán mely könyvtárakban kell keresni a programkönyvtárakat, az /etc/ld.so.conf file tartalmazza. Az ennek megfelelo környezeti változó a LD_LIBRARY_PATH. Minden egyes programindításkor végigkeresni az összes könyvtárat nem lenne hatékony, ezért a programkönyvtárak nevei az /etc/ld.so.cache nevu file-ban tárolódnak. Az elozo fejezetben említett ldconfig segédprogram beolvassa a /etc/ld.so.conf tartalmát, majd felállítja a szimbolikus linkeket, majd azokat az /etc/ld.so.cache file-ba írja. Ha át akarunk definiálni néhány függvényt egy könyvtárból, a függvényekbol létrehozunk egy átdefiniáló könyvtárat (overriding libraries) .o utótaggal, majd annak nevét beírjuk a /etc/ld.so.preload file-ba.
7.4 Dinamikusan betöltött programkönyvtárak A DL-ek nem alkotnak új, különálló típust: lehet statikus tárgykód formátum, vagy megosztott programkönyvtár. A különbség mindössze abban rejlik, hogy míg az elozo két programkönyvtár típus esetében rögtön a program indításakor betöltodött a könyvtár, a DL-ek esetén ez nem történik meg, hanem a DL-t használó programnak kell betöltenie, illetve felszabadítani azt. Mindezt a void * dlopen(const char *filename, int flag);
illetve a dlclose(void * handle);
függvényekkel tehetjük meg. A dlopen visszatérése a könyvtár leírója, amely NULL hiba esetén. Ha a filenév nem abszolút útvonal, akkor a keresési sorrend a következo: 1. Az LD_LIBRARY környezeti változó kettosponttal elhatárolt listájában található könyvtárak 2. A /etc/ld.so.cache-ban található könyvtárak 3. A /lib, majd a /usr/lib könyvtárak A flag értéke RTLD_LAZY, RTLD_NOW, illetve RTLD_GLOBAL lehet. Az elso ketto flag kizárja egymást: az elso azt jelenti, hogy csak akkor kerüljön sor a szimbólumfelold ásra, ha hivatkozás történik, a második esetben a rendszer rögtön betöltéskor megkeresi a hivatkozott szimbólumokat. Az elso megoldás gyorsabb betöltést és lassabb hivatkozást eredményez, az utóbbi fordítva. Ha az RTD_GLOBAL-t OR kapcsolatba hozzuk a fenti két flag valamelyikével, a külsoleg hozzáférheto szimbólumok az ezután betöltött könyvtárak rendelkezésére állnak. A dlclose paramétere a dlopen által visszaadott leíró. A legfontosabb függvény a void * dlsym(void *handle, char *symbol);
KÖNYVTÁRAK FEJLESZTÉSE 122 amellyel hozzáférhetünk az adott szimbólumokhoz. Ha a fenti muveletek bármelyikénél hiba fordul elo, azt a char* dlerror( )
függvénnyel kérdezhetjük le, amely egyben törli is a hibát, nehogy a következo dlerror hívás mégegyszer ugyanazt adja vissza. Így a double cosine(double)
függvényt implementáló libm.so megosztott matematikai programkönyvtárt használó kódrészlet: #include <stdio.h> #include int main(int argc, char **argv) { void *handle; double (*cosine)(double); char *error; handle = dlopen ("/lib/libm.so", RTLD_LAZY); if (!handle) { fputs (dlerror(), stderr); exit(1); } cosine = dlsym(handle, "cos"); if ((error = dlerror()) != NULL) fputs(error, stderr); exit(1); }
{
printf ("%f\n", (*cosine)(2.0)); dlclose(handle); }
Ha ez a program a foo.c nevet viseli, akkor a gcc -Wl,export-dynamic -o foo foo.c –ldl
utasítással fordíthatjuk.
7.5 Példák programkönyvtárakra Ebben a fejezetben egy teljes példát mutatunk mindegyik könyvtártípus létrehozására és használatára részletes magyarázatokkal.
7.5.1 Egy egyszeru programkönyvtár /* libhello.c – bemutatja a programkönyvtárak használatát */ #include <stdio.h> void hello(void)
KÖNYVTÁRAK FEJLESZTÉSE 123 { printf("Hello, library world.\n"); }
/* libhello.h */ void hello(void);
7.5.2 Statikus felhasználás A program: /* demo_use.c – bemutatja a hello rutin közvetlen felhasználását */ #include "libhello.h" int main(void) { hello(); return 0; }
A programot lefordító script: #!/bin/sh # Statikus programkönyvtár példa # Lefordítjuk a statikus könyvtárat a libhello.c forráskódból # libhello-static.o tárgykóddá gcc -Wall -g -c -o libhello-static.o libhello.c # Létrehozzuk a libhello-static.a nevu statikus könyvtárat: ar rcs libhello-static.a libhello-static.o # Ezek után már felhasználható a könyvtár, általában a használat # helyére másoljuk. # Most hagyjuk az aktuális könyvtárban, és onnan fogjuk használni. # Lefordítjuk a demo_use programot: gcc -Wall -g -c demo_use.c -o demo_use.o # Létrehozzuk a demo_use programot; a -L kapcsolóval adjuk meg # azokat a könyvtárakat, ahol a gcc keresni fogja a # programkönyvtárakat. Jelen esetben ez az aktuális könyvtár, amit # „.” jelöl. Így a –L. paraméter miatt a gcc az aktuális # könyvtárban fogja keresni a programkönyvtárat. # # A libhello-static.a programkönyvtárat a –lhello-static # kapcsolóval hozzálinkeljük a programhoz: gcc -g -o demo_use_static demo_use.o -L. -lhello-static # Futtatjuk a programot: ./demo_use_static
7.5.3 Megosztott programkönyvtár fordítása
KÖNYVTÁRAK FEJLESZTÉSE 124 #!/bin/sh # Megosztott programkönyvtár példa # Létrehozzuk a libhello.c forrásból a tárgykódú libhello.o file# t: gcc -fPIC -Wall -g -c libhello.c # A megosztott programkönyvtár létrehozása. # Az -lc kapcsolót használjuk a libc programkönyvtár # hozzálinkelésére, mert a libhello.c használja a C könyvtárat: gcc -g -shared -Wl,-soname,libhello.so.0 \ -o libhello.so.0.0 libhello.o -lc # Most akár bemásolhatnánk a libhello.so.0.0 file-t # valahová, mondjuk a /usr/local/lib könyvtárba. # Most meghívjuk az ldconfig segédprogramot a szimbolikus linkek # felállítására (ne felejtsük le a végérol a pontot!!!): /sbin/ldconfig -n . # Létrehozzuk a linkernevet: ln -sf libhello.so.0 libhello.so # Lefordítjuk a demo_use programot: gcc -Wall -g -c demo_use.c -o demo_use.o # Létrehozzuk a demo_use programot: # Mivel a –L kapcsoló a fordítónak szól, csak a fordításkor keresi # az aktuális könyvtárban a programkönyvtárat, nem betöltéskor. # Itt használjuk a linker nevet. gcc -g -o demo_use demo_use.o -L. -lhello # Hozzáadjuk az aktuális könyvtárat a keresési útvonalat # tartalmazó LD_LIBRARY_PATH környezeti változóhoz, majd futtatjuk # a programot: LD_LIBRARY_PATH="." ./demo_use
7.5.4 Dinamikus könyvtárhasználat /* demo_dynamic.c – bemutatja a "hello" függvény dinamikus használatát */ /* Szükségünk van a dlfcn.h header file-ra: ebben vannak a dinamikus betöltés függvényei */ #include #include <stdio.h> /* Nem szükséges a hello függvény prototípusa, de szükségünk van az arra mutató pointerre a dlsym( ) számára: */ typedef void (*simple_demo_function)(void);
int main(void) { const char *error; void *module; simple_demo_function demo_function; /* A DL betöltése */ module = dlopen("libhello.so", RTLD_LAZY);
KÖNYVTÁRAK FEJLESZTÉSE 125 if (!module) { fprintf(stderr, "Couldn't open libhello.so: %s\n", dlerror()); exit(1); } /* Betöltjük a szimbólumot (jelen esetben a hello függvényt) */ dlerror(); demo_function = dlsym(module, "hello"); if ((error = dlerror())) { fprintf(stderr, "Couldn't find hello: %s\n", error); exit(1); } /* Most meghívjuk a DL-ben levo függvényt: */ (*demo_function)(); /* Minden kész, lezárjuk a DL-t: */ dlclose(module); return 0; }
7.5.5 Dinamikus script #!/bin/sh # Dinamikusan betöltött könyvtár példa # Tételezzük fel, hogy létrehoztuk a libhello.so megosztott # könyvtárat a hozzá tartozó file-okkal együtt a 2.2.5.3 # fejezetben. # Lefordítjuk a demo_dynamic programot tárgykódú programmá gcc -Wall -g -c demo_dynamic.c # Mivel nem a program indulásakor töltodik be a programkönyvtár, a # linkernek nem is kell tudni róla. Ugyanakkor szükségünk van az # -ldl kapcsolóra, hogy a betöltést elvégzo függvények hozzá # legyenek linkelve a programhoz. # Létrehozzuk a demo_use programot: gcc -g -o demo_dynamic demo_dynamic.o -ldl # Hozzáadjuk az aktuális könyvtárat a keresési útvonalat # tartalmazó LD_LIBRARY_PATH környezeti változóhoz, majd futtatjuk # a programot: LD_LIBRARY_PATH="." ./demo_dynamic
HÁLÓZATI KOMMUNIKÁCIÓ 126
8. Hálózati kommunikáció Mai, számítógép hálózatokkal átszott világunkban a Linux rendszerek egyik fo alkalmazási területét a hálózatos applikációk adják. Ebben a fejezetben ezen kommunikációk megvalósításának alapjait ismerjük meg. Nem foglalkozunk az összes protokollal, hiszen a Linux többet is támogat (TCP/IP, AppleTalk, IPX, stb.). Vizsgálódásunk egyik területe a Berkley socket API, amely tulajdonképpen egy általános kommunikációs interfész. Mi két implementációját tárgyaljuk. A legfontosabbat, a TCP/IP protokoll használatát, amely lényegében muködteti az Internetet. A másik, egyszerubb protokoll a Unix domain socket, amely tulajdonképpen nem hálózati kommunikáció, hanem egy IPC mechanizmus, ami csak egy gépen belül használható. Továbbá megismerhetjük a TCP/IP-re épülo távoli eljáráshívást (Remote Procedure Calling). Ez egy magasabb szintu, általában egyszerubben használható kommunikációs metódus.
8.1 Egyszeru socket kezelés Mint a Linux más eroforrásai a socketek is a file absztrakciós interfészen keresztul vannak implementálva a rendszerbe. Létrehozásuk a socket() rendszerhívással történik, amely egy állományleíróval tér vissza. Miután a socketet inicializáltuk a read() és write() függvényekkel kezelheto, mint minden más állományleíró. (Azonban nem célszeru.) Használat után pedig a close() függvénnyel be kell zárnunk.
8.1.1 Socketek létrehozása Új socketeket a socket() rendszerhívással hozhatunk létre, amely a nem inicializált socket állományleírójával tér vissza. Létrehozásakor a sockethez egy meghatározott protokollt rendelünk, azonban ezek után még nem kapcsolódik sehova. Ebben az állapotában meg nem olvasható vagy írható. #include <sys/socket.h> int socket(int domain, int type, int protocol);
Mint az open() a socket() is 0-nál kisebb értékkel tér vissza hiba esetén, és az állományleíróval, amely 0 vagy nagyobb, ha sikeres. A három paraméter a használandó protokollt definiálja. Az elso a protokoll családot adja meg és értéke a következo lista valamelyike: Protokoll PF_UNIX, PF_LOCAL PF_INET
Jelentés Unix domain (gépen belüli kommunikáció) IPv4 protokoll
HÁLÓZATI KOMMUNIKÁCIÓ 127 Protokoll PF_INET6 PF_IPX PF_NETLINK PF_X25 PF_AX25 PF_ATMPVC PF_APPLETALK PF_PACKET
Jelentés IPv6 protokoll Novell IPX Kernel user interfész X.25 protokoll AX.25 protokoll, az amator rádiósok használják Nyers ATM csomagok AppleTalk Alacsony szintu packet interfész
A következo type paraméter az alábbi értékek egyikével definiálja a protokoll családon belül a kommunikáció módját: Típus
Jelentés Egy sorrend tartó, megbízható, kétirányú, kapcsolat SOCK_STREAM alapú byte stream kommunikációt valósít meg. Datagram alapú (kapcsolatmentes, nem megbízható) SOCK_DGRAM kommunikáció. Sorrend tartó, megbízható, kétirányú, kapcsolat alapú SOCK_SEQPACKET kommunikációs vonal fix méretu datagramok számára. SOCK_RAW Nyers hálózati protokoll hozzáférést tesz lehetové. Megbízható datagram alapú kommunikációs réteg. (Nem SOCK_RDM sorrendtartó.) SOCK_PACKET Elavult opció. Az utolsó, protocol, paraméter a protokoll családon belüli protokollt definiálja. Általában csak egy protokoll létezik a családokon belül, amely egyben az alapértelmezett. Így ennek a paraméternek az értéke leggyakrabban 0. A PF_INET családon belül a TCP az alapértelmezett stream protokoll, és az UDP a datagram alapú.
8.1.2 A kapcsolat felépítése Ha egy stream socket-et hoztunk létre, akkor hozzá kell kapcsolódnunk egy másik géphez, ha használni is szeretnénk. A socket kapcsolódása egy aszimmetrikus muvelet, vagyis a létrejövendo kapcsolat két oldalán a feladatok eltérnek egymástól. Az egyik oldalnak készen kell állnia arra, hogy valaki kapcsolódhasson hozzá. Rendszerint ez egy szerver applikáció, amely folyamatosan fut és várja, hogy más folyamatok kapcsolódjanak hozzá. A kliens applikációk ezzel szemben létrehoznak egy socket-et, majd a megadott címhez próbálnak kapcsolódni, és a kommunikációt felépíteni. Amikor a szerver elfogadja a kapcsolódási kérelmet, akkor létrejön a kapcsolat a két socket között. Amikor ez megtörtént onnantól a socket alkalmas a kétirányú kommunikációra.
HÁLÓZATI KOMMUNIKÁCIÓ 128
8.1.3 A socket címhez kötése Mind a szervernek, mind a kliensnek meg kell adnia, hogy a rendszer melyik címet használja a socket-éhez. A helyi oldalon a cím hozzárendelés muveletét kötésnek (binding) nevezzük. Ezt a bind() rendszerhívással tehetjük meg. #include <sys/socket.h> int bind(int sock, struct sockaddr *my_addr, socklen_t addrlen);
Az elso paraméter a socket, a továbbiak a címet írják le.
8.1.4 Várakozás a kapcsolódásra A socket létrehozása után a szerver folyamat a bind() rendszerhívással hozzárendeli egy címhez, ahol várhatja a kapcsolódókat. A folyamat a listen() rendszerhívással állítja be a socket-re, hogy fogadja ezeket a kapcsolódásokat. Ezek után a kernel kezeli a kapcsolódási igényeket. Ilyenkor azonban még nem épül fel azonnal a kapcsolat. A várakozó folyamatnak az accept() rendszerhívással kell elfogadni a kapcsolódást. A még az accept()-el el nem fogadott kapcsolódási igényeket pending connection-nek nevezzük. Normál esetben az accept() függvény blokkolódik, amíg egy kliens kapcsolódni próbál hozzá. Természetesen állíthatjuk az fcntl() rendszerhívás segítségével nem blokkolódó módban is. Ilyenkor az accept() azonnal visszatér, amikor egyetlen kliens sem próbál kapcsolódni. A select() rendszerhívást is alkalmazhatjuk a kapcsolódási igények észlelésére. A listen() és az accept() függvények formája: #include <sys/socket.h> int listen(int sock, int backlog); int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
Elso paraméterként mindkét függvény a socket leíróját várja. A listen() második paramétere, a backlog, amely megadja, hogy hány kapcsolódni kívánó socket kérelme után utasítsa vissza az újakat. Vagyis hogy mekkora legyen a pending connection várakozási lista. Az accept() fogadja a kapcsolódásokat. Visszatérési értéke az új kapcsolat leírója. Az addr és addrlen paraméterekben a másik oldal címét kapjuk meg. Az addrlen egy integer szám, amely megadja az addr változó méretét.
8.1.5 A szerverhez kapcsolódás A kliens a szerverhez hasonlóan a létrehozott socket-et hozzárendelheti a bind() rendszerhívással egy helyi címhez. Azo nban ezzel a kliens program általában nem törodik, és a kernelre bízza, hogy ezt automatikusan megtegye. Ezek után a connect() rendszerhívással kapcsolódhat a kliens a szerverhez.
HÁLÓZATI KOMMUNIKÁCIÓ 129 #include <sys/socket.h> int connect(int sock, struct sockaddr *addr, socklen_t addrlen);
Az elso paraméter a socket leírója, a további paraméterek a szerver socket címét adják meg. A kapcsolat felépülését a következo ábrán láthatjuk :
Kliens
Szerver
socket()
socket()
bind()
listen() connect() accept()
Kapcsolat létrejött
Ábra 8-1 A socket kapcsolat felépülése
8.2 Unix domain socket A Unix domain socket a legegyszerubb protokoll család, amely a socket API-n keresztül elérheto. Valójában nem egy hálózati protokollt reprezentál, csak egy gépen belül képes kapcsolatokat felépíteni. Habár ez egy komoly korlátozó tényezo, mégis sok applikáció használja, lévén egy flexibilis IPC mechanizmust nyújt. A címei állománynevek, amelyek az állományrendszerben jönnek létre. A socket állományok, amelyeket létrehoz, a stat() rendszerhívással vizsgálhatóak, azonban nem lehet megnyitni az open() függvénnyel. Helyette a socket API-t kell használni. A Unix domain támogatja mind a stream, mind a datagram kommunikációs interfészt. Azonban a datagram interfész nem használatos, ezért mi sem tárgyaljuk. A stream interfész a named pipe-okhoz hasonlít, azonban nem teljesen azonos. Ha több processz megnyit egy named pipe-ot, akkor egyikük kiolvashatja belole, amit egy másik beleírt. Lényegében olyan, mint egy hirdetotábla. Az egyik processz elküld egy üzenetet rá, és egy másik elveszi onnan.
HÁLÓZATI KOMMUNIKÁCIÓ 130 Ezzel szemben a Unix domain socket kapcsolat orientált. Minden kapcsolat egy-egy új kommunikációs csatorna. A szerver egyszerre több kliens kapcsolatot kezelhet, és mindegyikhez külön leíró tartozik. Ezek a tulajdonságok alkalmasabbá teszik az IPC feladatokra, mint a named pipe, ezért sok Linux szolgáltatás alkalmazza. Többek közt az X Window System és a log rendszer is.
8.2.1 Unix domain címek A Unix domain címek állománynevek a file rendszeren. Ha az állomány nem létezik, akkor a rendszer létrehozza, mint socket típusú állomány, amikor meghívjuk a bind() függvényt. Ha az állománynév már használt, akkor a bind() hibával tér vissza (EADDRINUSE). A bind() jognak a 0666-t állítja be (módosítva az umask értékével). Ahhoz, hogy a connect() rendszerhívással kapcsolódhassunk a socket-hez, olvasás és írás jogunknak kell lenni, a socket állományra. A Unix domain socket címek megadásához a struct sockaddr_un struktúrát használjuk. #include <sys/socket.h> #include <sys/un.h> struct sockaddr_un { unsigned short sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
Az elso elemnek, sun_family, AF_UNIX-nak kell lennie. Ez jelzi, hogy Unix domain címet tartalmaz. A sun_path tartalmazza az állománynevet. A rendszerhívásokban használt cím struktúra méret az állománynév hossza, plusz a sun_family elem mérete. Az állománynevet nem kell ‘\0’ karakternek zárnia, bár általában így használják.
8.2.2 Szerver applikáció A szerver applikációnál használatos rendszerhívások ebben az esetben is megegyeznek a socket-eknél már tárgyaltakkal. Nézzünk egy példát a használatára: /* userver.c - simple server for Unix domain sockets */ /* Waits for a connection on socket. Once a connection from the socket to stdout connection, and then wait */ #include #include #include #include
<stdio.h> <sys/socket.h> <sys/un.h>
int main(void) {
the ./sample-socket Unix domain has been established, copy data until the other end closes the for another connection to the socket.
HÁLÓZATI KOMMUNIKÁCIÓ 131 struct sockaddr_un address; int sock, conn; size_t addrLength; char buf[1024]; int amount; if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } /* Remove any preexisting socket (or other file) */ unlink("./sample-socket"); address.sun_family = AF_UNIX; /* Unix domain socket */ strcpy(address.sun_path, "./sample-socket"); /* The total length of the address includes the sun_family element */ addrLength = sizeof(address.sun_family) + strlen(address.sun_path); if (bind(sock, (struct sockaddr *) &address, addrLength)) { perror("bind"); exit(1); } if (listen(sock, 5)) { perror("listen"); exit(1); } while ((conn = accept(sock, (struct sockaddr *) &address, &addrLength)) >= 0) { printf("---- getting data\n"); while ((amount = read(conn, buf, sizeof(buf))) > 0) { if (write(1, buf, amount) != amount) { perror("write"); exit(1); } } if (amount < 0) { perror("read"); exit(1); } printf("---- done\n"); close(conn); } if (conn < 0) { perror("accept"); exit(1); }
HÁLÓZATI KOMMUNIKÁCIÓ 132 close(sock); return 0; }
Ez egy elég egyszeru példa applikáció, amely bemutatja a szükséges rendszerhívásokat, viszont egyszerre egy kapcsolat lekezelésére alkalmas. (Természetesen készíthetünk ennél bonyolultabb, több kapcsolat párhuzamos lekezelésére is alkalmas megoldásokat.) Az általános socket kezeléshez képest láthatunk a programban egy unlink() hívást. Ez azért szükséges, mert a bind() hibával térne vissza, ha már az állomány létezik. (Akkor is, ha socket állomány.)
8.2.3 Kliens applikáció A kliens applikáció természetesen szintén követi a socket-eknél már ismertetett módszereket. A következo program az elobb ismertetett szerverhez kapcsolódik, és a standard bemenetén kapott szöveget küldi el: /* uclient.c - simple client for Unix domain sockets */ /* Connect to the ./sample-socket Unix domain socket, copy stdin into the socket, and then exit. */ #include <sys/socket.h> #include <sys/un.h> #include int main(void) { struct sockaddr_un address; int sock; size_t addrLength; char buf[1024]; int amount; if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } address.sun_family = AF_UNIX; /* Unix domain socket */ strcpy(address.sun_path, "./sample-socket"); /* The total length of the address includes the sun_family element */ addrLength = sizeof(address.sun_family) + strlen(address.sun_path); if (connect(sock, (struct sockaddr *) &address, addrLength)) { perror("connect"); exit(1); }
HÁLÓZATI KOMMUNIKÁCIÓ 133 while ((amount = read(0, buf, sizeof(buf))) > 0) { if (write(sock, buf, amount) != amount) { perror("write"); exit(1); } } if (amount < 0) { perror("read"); exit(1); } close(sock); return 0; }
8.2.4 Névtelen Unix domain socket Mivel a Unix domain socket a pipe-okhoz képest több elonnyel rendelkezik (mint például a full duplex kommunikáció), ezért gyakran alkalmazzák IPC kommunikációhoz. A használatukat könnyítendo létezik egy socketpair() rendszerhívás. Ez egy pár összekapcsolt, név nélküli socket-et hoz létre. #include <sys/types.h> #include <sys/socket.h> int socketpair(int domain, int type, int prot, int sockfds[2]);
Az elso három paraméter megegyezik a socket() rendszerhívásnál tárgyaltakkal. Az utolsó paraméter, sockfds, tartalmazza a visszaadott socket leírókat. A két leíró egyenértéku.
8.3 TCP/IP Miután áttekintettük a socketek létrehozását támogató függvényeket, nézzük meg, hogyan zajlik a hálózati kommunikáció!
8.3.1 A hardverfüggo különbségek feloldása A hálózati kommunikáció byte-ok sorozatán alapszik. Azonban egyes processzorok különbözo módon tárolják a különbözo adattípusokat. Ennek a nehézségnek az áthidalására definiáltak egy hálózati byte sorrendet (network byte order), ami a magasabb helyiértéku byte-ot küldi elobb („a nagyobb van hátul” - big endian). Azoknál az architektúráknál, ahol az ún. hoszt byte sorrend ellenkezo („a kisebb van hátul” – little endian) - ilyenek például az Intel 8086 alapú processzorok -, konverziós függvények állnak rendelkezésünkre, amelyeket a @táblázat foglal össze. Azokon az architektúrákon, ahol nem szükséges ez a konverzió, ezek a függvények a megadott argumentum értékekkel térnek vissza, vagyis hordozható kód esetén mindenképpen alkalmazzuk ezeket a függvényeket. Függvény
Leírás
HÁLÓZATI KOMMUNIKÁCIÓ 134 Függvény Leírás Ntohs 16 bites mennyiséget hálózati byte sorrendbol átvált a hoszt byte sorrendjébe (Big- Endian ? Little-Endian). Ntohl 32 bites mennyiséget hálózati byte sorrendbol átvált a hoszt byte sorrendjébe (Big- Endian ? Little-Endian). htons 16 bites mennyiséget a hoszt byte sorrendjébol átvált hálózati byte sorrendbe (Little-Endian ? Big-Endian). htonl 32 bites mennyiséget a gép byte sorrendjébol átvált hálózati hoszt sorrendbe (Little-Endian ? Big- Endian).
8.3.2 Címzés Az IP címek egyediek minden hosztra, és 32 bitbol (4 byte-ból) állnak. Szokásos megjelentése a 4 byte pontokkal elválasztva. Próbáljuk lekérdezni saját gépünk IP címét: $ /sbin/ifconfig eth0 Link encap:Ethernet HWaddr 00:04:76:95:91:9C inet addr:152.66.70.136 Bcast:152.66.70.255 ...
Ezek után vizsgáljuk meg, hogyan valósul meg a címkiosztás! A 32 bit kétfelé oszlik. Az elso M bit egy azonosító, amely megmutatja a hálózat típusát, a következo N bit egy egész hálózat címe, a maradék 32-N-M bit pedig az adott hálózaton belül egy számítógép címe. Attól függoen, hogy N értékét mekkorra választjuk, több hálózatot címezhetünk meg (N-et növelve), illetve több gépbol álló hálózatot címezhetünk meg (N-et csökkentve). Cím A osztály B osztály C osztály D osztály E osztály
Elso cím 1.0.0.0 128.0.0.0 192.0.0.0 224.0.0.0 240.0.0.0
Utolsó cím 127.255.255.255 191.255.255.255 223.255.255.255 239.255.255.255 247.255.255.255
Azonosító 0 10 110 1110 11110
N 7 14 21 Multicast Fenntartva
Állapítsuk meg, hogy a www.aut.bme.hu szerver IP címe milyen osztályba tartozik! $ ping www.aut.bme.hu PING avalon.aut.bme.hu (152.66.70.16) from 152.66.70.136 : 56(84) bytes of data. ...
Amint a @ táblázat alapján látható, ez egy B osztályú IP cím, ami nem meglepo, hiszen a BME-hez elég sok gép tartozik, így a hosztok megcímzéséhez 16 bitre van szükség (az A osztály esetén adott 24 sok lenne, a C osztályban használt 8 kevés).
8.3.3 A socket cím megadása A 8.1.5 fejezetben bevezettük a connect függvényt, amellyel a szreverhez tudunk kapcsolódni. Nézzük meg közelebbrol a cím megadásának módját! A legalapvetobb a sockaddr struktúra: struct sockaddr
HÁLÓZATI KOMMUNIKÁCIÓ 135 { unsigned short sa_family; char sa_data[14];
// címcsalád, AF_xxx // 14 byte-os protokoll cím
};
Hátránya a fenti adatszerkezetnek, hogy a sa_data adattagot kézzel kell kitöltenünk a port számával és az IP címmel, amely legalábbis elég nehézkes feladat. Ennek megkönnyítésére rendelkezésre áll a struct sockaddr_in { short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; };
// // // //
Címcsalád = AF_INET A port száma IP cím struct sockaddr vége
struktúra, amely nek nevében az in az internet rövidítése. Fontos, hogy a sin_port paramétert hálózati byte sorrendben adjuk meg! Az in_addr adattag típusa: struct in_addr { unsigned long int s_addr; }
Ez utóbbi struktúra és a megszokott IP cím reprezentáció közötti konverziót több függvény is segíti. Az #include <sys/socket.h> #include #include <arpa/inet.h> in_addr_t inet_addr(const char *cp);
egy pontozott formátumú IP cím sztring reprezentációjából in_addr típusú struktúrát készít hálózati byte sorrendben. Például: struct sockaddr_in internet_address; internet_address.sin_addr.s_addr = inet_addr("152.66.70.136");
Hasonló funkciót lát el a #include <sys/socket.h> #include #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp);
függvény, amelynek visszatérési értéke jelzi a hibát. Ha hibakezelés szükséges mindig ezt a függvényt alkalmazzuk a fenti inet_addr hívással szemben. A hoszt byte sorrend támogatására a #include <sys/socket.h>
HÁLÓZATI KOMMUNIKÁCIÓ 136 #include #include <arpa/inet.h> in_addr_t inet_network(const char *cp);
függvény a cp sztringben megadott pontozott formátumú IP címet hoszt byte sorrendu 32 bites címmé alakítja. Az #include <sys/socket.h> #include #include <arpa/inet.h> char *inet_ntoa(struct in_addr in);
hívás a hálózati byte sorrendben megadott 32 bites IP címet alakítja pontozott formátummá. A visszatérési érték egy statikusan foglalt bufferben tárolódik, amit a soron követezo hívások felülírnak. Ha külön meg szeretnénk adni a hálózat címét és külön a host címét, akkor a #include <sys/socket.h> #include #include <arpa/inet.h> struct in_addr inet_makeaddr(int net, int host);
függvényhívást használjuk. A bemeno paraméterek hoszt byte sorrendben értendok, a vissza adott címet hálózati byte sorrendben kapjuk. Ha az IP címbol szeretnénk kinyerni hoszt számát hoszt byte sorrendben, akkor a #include <sys/socket.h> #include #include <arpa/inet.h> in_addr_t inet_lnaof(struct in_addr in);
függvényt használhatjuk, míg a hálózat számát szintén hoszt byte sorrendben a #include <sys/socket.h> #include #include <arpa/inet.h> in_addr_t inet_netof(struct in_addr in);
hívás segítségével kaphatjuk meg.
8.3.4 Név- és címfeloldás Ha tudjuk egy hosztnak a nevét, szükségünk lehet az adott hoszt IP címére is. Ha egy hoszt nevébol a hoszt IP címét szertenénk megállapítani, akkor névfeloldásról beszélünk. Ezt a funkciót Linux alól a
HÁLÓZATI KOMMUNIKÁCIÓ 137
#include extern int h_errno; struct hostent *gethostbyname(const char *name);
függvénnyel érhetjük el. Ennek visszatérési értéke: #include struct hostent { char* h_name; // char** h_aliases; // int h_addrtype; // int h_length; // char** h_addr_list;// }; #define h_addr
A hoszt hivatalos neve NULL terminált tömb a hoszt más neveire A cím típusa, jelenleg mindig AF_INET A cím hossza byte-okban NULL terminált lista – az IP címek
h_addr_list[0] // Kompatibilitás miatt
Nézzünk a névfeloldásra egy példát! #include #include <stdio.h> int main(int argc, char *argv[]) { struct hostent* p_host; struct in_addr* in; if(argc!=2) { printf("Usage: getipaddr \n"); return -1; } p_host=gethostbyname(argv[1]); if(p_host!=NULL) { in=(struct in_addr *)(p_host->h_addr); printf("IP address: %s\n",inet_ntoa(*in)); } else { printf("Host %s not found.\n",argv[1]); return -1; } return 0; }
Teszteljük is le: $ ./getipaddr www.aut.bme.hu IP address: 152.66.70.16 $ ./getipaddr www.that.s.not.funny.com Host www.that.s.not.funny.com not found.
HÁLÓZATI KOMMUNIKÁCIÓ 138
Az inverz név feloldást AF_INET típusú socket-ek esetén a #include <sys/socket.h> struct hostent *gethostbyaddr(const char *addr, int len, int type);
függvényen keresztül érhetjük el. Az elso paraméter egy in_addr típusú struktúrára mutató pointer, a második argumentum az elso argumentumban megadott struktúra mérete, a harmadik paraméter általában AF_INET. Nézzünk erre a függvényre is egy példát: #include #include <stdio.h> int main(int argc, char *argv[]) { struct hostent* p_host; struct in_addr addr; if(argc!=2) { printf("Usage: gethost \n"); return -1; } if ((int)(addr.s_addr = inet_addr(argv[1])) == -1) { printf("IP address %s is not valid\n",argv[1]); return -1; } p_host=gethostbyaddr((char*)&addr,sizeof(addr),AF_INET); if(p_host!=NULL) { printf("Server name: %s\n",p_host->h_name); } else { printf("Host %s not found.\n",argv[1]); return -1; } return 0; }
A programot letesztelve a következo eredmények adódtak: $ ./gethost 152.66.70.16 Server name: avalon.aut.bme.hu $ ./gethost 152.66.70.456 IP address 152.66.70.456 is not valid $ ./gethost 152.77.70.34 Host 152.77.70.34 not found.
HÁLÓZATI KOMMUNIKÁCIÓ 139
8.3.5 Portok A TCP/UDP réteg lehetové tesz több virtuális „csatornát” két gép között, ezen kommunikációt a szállítási réteg portokkal azonosítja. Egy TCP/UDP kommunikációban mindkét fél külön port számmal rendelkezik. Az 1024-nél kisebb számú portokat jól ismert portoknak (well-known ports) nevezzük, amelyek meghatározott szolgálatokra vannak fenntartva. A felhasználói programok az 1024-tol 49151- ig lévo tartományt használhatják (regisztrált portok – registered ports). A dinamikus és magánportok (Dynamic and Private Ports) a 49152- 65535 intervallumban helyezkednek el. Az Interned Assigned Numbers Authority szervezet által definiált portok listája megtalálható a http://www.iana.org/assignments/port-numbers oldalon. Szolgáltatás ne ve ftp-data ftp ssh telnet smtp http https
Port 20 21 22 23 25 80 443
Linux alatt a szolgáltatások az /etc/services file-ban vannak felsorolva. Az itt levo adatokat kérdezhetjük le a #include struct servent *getservbyname(const char *name, const char *protocol);
függvénnyel, amely egy name nevu szolgáltatásról ad információt. A második argumentumban megadhatjuk a protokollt is, de ha az NULL, akkor minden protokoll megfelelo. Az információt a struct servent { char *s_name; char **s_aliases; int s_port; char *s_proto; }
/* /* /* /*
a szolgáltatás hivatalos neve */ alias lista, NULL terminált */ port száma hálózati byte sorrendben */ a protokoll neve */
struktúrában kapjuk vissza. Hasonló funkciót lát el a #include struct servent *getservbyport(int port, const char *protocol);
HÁLÓZATI KOMMUNIKÁCIÓ 140
függvény, amelynek a szolgáltatás neve helyett egy adott port számát adhatjuk meg. Megjegyezzük még, hogy a getservent(), setservent(), endservent() függvényekkel még közvetlenebb hozzáférésre van lehetoségünk az /etc/services file-ban tárolt információhoz, azonban itt ezt nem részletezzük.
8.3.6 Kommunikáció A kapcsolat létrejötte után megkezdodhet az adatok átvitele, a tényleges kommunikáció. Összeköttetés alapú kapcsolat esetén a #include <sys/types.h> #include <sys/socket.h> int send(int s, const void *msg, size_t len, int flags);
függvényt használjuk adatok küldésére. Az eslo argumentum a socket leírója, a második az elküldendo adat adat bufferére mutató pointer, a harmadik az elküldendo adat mérete. A flags argumentumban megadott jelzobitek jelentése: Jelzobit MSG_OOB
MSG_DONTROUTE
MSG_DONTWAIT
MSG_NOSIGNAL
Leírás Egy soron kívüli sürgos adatcsomagot (out-of-band data) küld. Általában jelzések hatására használják. A csomag nem mehet keresztül a routereken, csak közvetlen hálózatra kapcsolódó gép kapja meg. Engedélyezi a nem blokkoló I/O-t, EAGAIN-nal tér vissza, majd a már tárgyalt módon a select() utasítással kaphatunk értesítést a muvelet kimenetelérol. Adatfolyam alapú kapcsolat esetén a program nem kap SIGPIPE jelzést.
Összeköttetés nélküli kapcsolatok esetén használjuk a #include <sys/types.h> #include <sys/socket.h> int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
függvényt, amelynek a címzett socket címét illetve annak méretét is meg kell adnunk a függvény utolsó két paramétereként. A send párja a #include <sys/types.h> #include <sys/socket.h>
HÁLÓZATI KOMMUNIKÁCIÓ 141
int recv(int s, void *buf, size_t len, int flags);
függvény, amely egy buffert, annak méretét valamint egy jelzobitet vár. A jelzobitek értékét a @ táblázat foglalja össze. Jelzobit MSG_OOB MSG_PEEK
Leírás Soron kívüli adat fogadása. Az adat beolvasása anélkül, hogy a beolvasott adatot eltávolítaná a bufferbol. A következo recv hívás ugyanazt az adatot még egyszer kiolvassa. MSG_WAITALL Addig nem tér vissza, míg a megadott buffer megtelik, vagy egyéb rendhagyó dolog történik, pl. jelzés érkezik. MSG_NOSIGNAL Ugyanaz, mint a send függvény esetén Összeköttetés nélküli kapcsolat esetén a #include <sys/types.h> #include <sys/socket.h> int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
függvényt használjuk.
8.3.7 Szerver applikáció
8.3.8 Kliens applikáció
8.4 Távoli eljáráshívás (RPC) A távoli eljáráshívás (Remote Procedure Calling, RPC) egy magas szintu kommunikációs paradigma. Lehetové teszi távoli gépeken lévo eljárások meghívását, miközben elrejti a felhasználó elol az alsóbb hálózati rétegeket. Az RPC egy logikai kliens-szerver modellt valósít meg. A kliens szolgáltatási igényeket küld a szervernek. A szerver fogadja az igényeket, végrehajtja a kért funkciót, választ küld, majd visszaadja a vezérlést a kliensnek. Az RPC megkíméli a felhasználót az alsóbb hálózati rétegek ismeretétol és programozásától. A hívások transzparensek. A hívónak explicite nincs tudomása az RPC-rol, a távoli eljárásokat ugyanúgy hívja, mint egy helyi eljárást.
8.4.1 Az RPC modell A távoli eljáráshívás modellje hasonlít a helyi eljáráshívás modelljére. A helyi eljárás hívója a hívás argumentumait egy jól meghatározott helyre teszi, majd átadja a
HÁLÓZATI KOMMUNIKÁCIÓ 142 vezérlést az eljárásnak. A hívás eredményét a hívó egy jól meghatározott helyrol elveszi, és tovább fut. A távoli eljáráshívás esetén a végrehajtási szál két folyamaton (kliens és szerver) halad keresztül. A hívó egy üzenetet küld a szervernek, majd válaszra vár (block), A hívó üzenete tartalmazza a hívás paramétereit, a válaszüzenet pedig az eljárás eredményét. A kliens kiveszi a válaszüzenetbol az eredményt, és tovább fut. A szerver oldalon egy várakozó – alvó – folyamat várja a hívók üzeneteit. Az érkezo üzenetbol kiveszi az eljárás paramétereit, elvégzi a feladatot, majd visszaküldi a válaszüzenetet. A két folyamat közül egy idoben csak az egyik aktív, a másik várakozó állapotban van.
A gép
B gép
Kliens program
Szerver démon RPC hívás Szolgáltatás meghívás Szolgáltatás végrehajtás Válasz
Szolgáltatás kész
Program folytatódik
Ábra 8-2 RPC kommunikáció
8.4.2 Verziók és számok Minden RPC eljárást egyértelmuen meghatároz egy programszám és egy eljárásszám. A programszám az eljárások egy csoportját jelöli. A csoporton belül minden eljárásnak egyedi eljárásszáma van. Ezen kívül minden programnak van egy verziószáma is. Így a szolgáltatások bovítése, vagy változtatása esetén nem kell új programszámot adni, csak a verziószámot növelni. A programszámok egy része a Sun által definiált, más részük a fe nntartott. A fejlesztok számára a 0x20000000-0x3fffffff tartomány áll rendelkezésre. A kialakított RPC protokollt regisztráltatni is lehet. A Sun cég kérésre küld egy egyedi programszámot.
8.4.3 Portmap Minden hálózati szolgáltatáshoz dinamikusan vagy statikusan hozzá lehet rendelni egy portszámot. Ezeket a számokat regisztráltatni kell a gépen futó portmap démonnal. Ha egy hálózati szolgáltatás portszámára van szükség, akkor a kliens a távoli gépen futó portmap démonnak egy RPC kéroüzenetet küld. A portmap egy RPC válaszüzenetben megadja a kért portszámot. Ezután a kliens már közvetlenül
HÁLÓZATI KOMMUNIKÁCIÓ 143 küldhet üzenetet a megadott portra. A fentiekbol következik, hogy a portmap az egyetlen olyan hálózati szolgáltatás, amelynek mindenki által ismert, dedikált portszámmal kell rendelkeznie. Jelen esetben ez a portszám a 111.
8.4.4 Transzport Az RPC független a szállítási protokolltól. Nem foglalkozik azzal, hogy miként adódik át az üzenet az egyik folyamattól a másiknak. Csak az üzenetek specifikációjával és értelmezésével foglalkozik. Ugyancsak nem foglalkozik a megbízhatósági követelmények kielégítésével. Ez egy megbízható szállítási réteg – pl. az összeköttetés alapú TCP – felett kevésbé okoz gondot. De egy kevésbé megbízható szállítási réteg – pl. UDP – felett futó RPC alkalmazásnak magának kell gondoskodnia az üzenetek megbízható továbbításáról. Linux alatt az RPC támogatja mind az UDP, mind a TCP szállítási réteget. Az egyszeruség kedvéért a TCP-t ajánlatos használni.
8.4.5 XDR Az RPC feltételezi az ún. külso adatábrázolás (eXternal Data Representation, XDR) meglétét. Az XDR egy gépfüggetlen adatleíró és kódoló nyelv, amely jól használható különbözo számítógép architektúrák közötti adatátvitelnél. Az RPC tetszoleges adatstruktúrák kezelésére képes, függetlenül az egyes gépek belso adatábrázolásától. Az adatokat elküldés elott XDR formára alakítja (serializing), a vett adatokat pedig visszaalakítja (deserializing).
8.4.6 rcpinfo Az rpcinfo parancs használatával információkat kérhetünk a portmap démonnál bejegyzett programokról: program neve, száma, verziója, portszáma, a használt szállítási réteg. Ezen kívül meg lehet vele kérdezni, hogy egy program adott verziója létezik-e, illetve válaszol-e a kérésekre. A lokális gépre az információk lekérdezése a következo. rpcinfo –p localhost
8.4.7 rpcgen A távoli hívást használó alkalmazások programozása nehézkes és bonyolult lehet. Az egyik nehézséget éppen az adatok konverziója jelenti. Szerencsére létezik egy rpcgen nevu program, amely segíti a programozót az RPC alkalmazás elkészítésében. Az rpcgen egy fordító. Az RPC program interfészének definícióját – az eljárások nevét, az átadott és visszaadott paraméterek típusát – az úgynevezett RPC nyelven kell leírni. A nyelv hasonlít a C nyelvre. A fordító az interfész definíció alapján néhány C nyelvu forráslistát állít elo: a közösen használt definíciókat (header állományt), a kliens és a szerver oldali RPC programok vázát (skeleton). A vázak feladat, hogy elrejtsék az alsóbb hálózati rétegeket a program többi részétol.
HÁLÓZATI KOMMUNIKÁCIÓ 144 A programfejlesztonek mindössze az a feladata, hogy megírja a szerver oldali eljárásokat. Az eljárások tetszoleges nyelven megírhatóak, figyelembe véve természetesen az interfész definíciót és a rendszerhívási konvenciókat. Az eljárásokat összelinkelve a szerver oldali vázzal, megkapjuk a futtatható szerver oldali programot. A kliens oldalon a foprogramot kell megírni, amelybol a távoli eljárásokat hívjuk. Ezt összelinkelve a kliens oldali vázzal megkapjuk a futtatható kliens programot. Az rpcgen-nek létezik egy –a kapcsolója is, amely tovább egyszerusíti a feladatunkat. Használatával az interfész állományból még a megírandó program részekre is kapunk egy C nyelvu vázat, amelyet már csak ki kell egészítenünk, hogy használhassuk. Így általában a következo parancsot használjuk rpcgen –a interface.x
ahol az interface.x az interfész állomány.
8.4.8 Helyi eljárás átalakítása távoli eljárássá Tegyük fel, hogy van egy eljárásunk, ami lokálisan fut. Ezt az eljárást szeretnénk távolivá tenni. Az átalakítást egy egyszeru példán vizsgáljuk meg. A lokális program a híváskor megadott paramétert, mint üzenetet kiírja a képernyore. A cél az, hogy távoli gép képernyojére is tudjunk üzenetet kiírni. A program lokális változata: /* * printmsg.c: uzenet kiirasa a kepernyore */ #include <stdio.h> int printmessage(char* msg) { printf("%s\n", msg); return 1; } int main(int argc, char* argv[]) { char* message; if(argc != 2) { fprintf(stderr, "usage: %s <message>\n", argv[0]); return -1; } message = argv[1]; if(!printmessage(message)) { fprintf(stderr, "%s: couldn't print your message\n", argv[0]); return -1; }
HÁLÓZATI KOMMUNIKÁCIÓ 145 printf("Message delivered!\n"); return 0; }
A protokoll kialakításához szükséges annak ismerete, hogy az eljárásnak milyen típusú paraméterei és visszatérési értékei vannak. Jelen esetben a printmessage() függvény szöveget vesz át paraméterként és egy egész számot ad vissza. A protokollt hagyományosan egy .x kiterjesztésu állományban írjuk meg. /* * msg.x: tavoli uzenet nyomtatas protokollja */ program MESSAGEPROG { /* program nev version MESSAGEVERS { /* verzio nev int PRINTMESSAGE(string) = 1; /* 1. fuggveny } = 1; /* verzio szam } = 0x20000099; /* program szam
*/ */ */ */ */
A távoli program egyetlen eljárást tartalmaz, melynek száma 1. Az RPC automatikusan generál egy ö. eljárást is. Ez a szerver megszólítására (pinging) használható. A nagybetuk használata nem szükséges, csak hasznos konvenció. Észreveheto, hogy az argumentum típusa nem “char*”, hanem “string”. Ez azért van, mert a különbözo nyelvek másként értelmezik a szöveg típust, sot a “char*” még a C nyelvben sem egyértelmu. A használható változó típusokat az XDR dokumentációja tartalmazza. Most nézzük a távoli eljárás implementációját: /* * msg_proc.c: tavoli printmessage eljaras */ #include <stdio.h> #include <rpc/rpc.h> #include "msg.h" int* printmessage_1(char* msg) { static int result; printf("%s\n", *msg); result = 1; return (&result); }
A printmessage_1() távoli eljárás három dologban különbözik a printmessage() helyi eljárástól: ?? Argumentumként string-re mutató pointert vesz át string helyett. Ez minden távoli eljárásra érvényes. Ha nincs argumentum, akkor “void*” kell megadni. ?? Egész számra mutató pointert ad vissza egész helyett. Ez szintén jellemzo a távoli eljárásokra. A pointer visszaadás miatt kell a result változót static-ként megadni. Ha nincs visszatérési érték, akkor “void*” kell használni.
HÁLÓZATI KOMMUNIKÁCIÓ 146 ?? A távoli eljárás nevének a végére “_1” került. Általában az rpcgen által generált eljárások neve a protokoll definícióban szereplo név kisbetukkel, egy aláhúzás karakter, végül pedig a verziószám. Végül következzék a kliens program, amely a távoli eljárást meghívja: /* * rprintmsg.c: a printmsg.c tavoli verzioja */ #include <stdio.h> #include <rpc/rpc.h> #include "msg.h" int main(int argc, char* argv[]) { CLIENT* cl; int* result; char* server; char* message; if(argc != 3) { fprintf(stderr, "usage: %s <message>\n", argv[0]); return -1; } server = argv[1]; message = argv[2]; /* kliens handler letrehozasa, kapcsolat felvetele a szerverrel */ cl = clnt_create(server, MESSAGEPROG, MESSAGEVERS, "tcp"); if(cl == NULL) { clnt_pcreateerror(server); return -1; } /* tavoli eljaras meghivasa */ result = printmessage_1(&message, cl); if(result == NULL) { clnt_perror(cl, "call failed"); return -1; } if(*result == 0) { fprintf(stderr, "%s: %s couldn't print your message\n", argv[0], server); return -1; } printf("Message delivered to %s!\n", server); return 0; }
Elso lépésként a kapcsolatot hozzuk létre a clnt_create() meghívásával. A visszakapott leírót a távoli eljárás argumentumaként használjuk. A clnt_create()
HÁLÓZATI KOMMUNIKÁCIÓ 147 utolsó paramétere a szállítási protokollt adja meg. Jelen esetben ez TCP, de lehet UDP is. A printmessage_1() eljárás meghívása pontosan ugyanúgy történik, ahogy azt az msg_proc.c állományban deklaráltuk, de itt utolsó paraméterként meg kell adni a kliens kapcsolatazonosítót is. A távoli eljáráshívás kétféle képen térhet vissza hibával. Ha maga az RPC hívás nem sikerült, akkor a visszatérési érték NULL. Ha a távoli eljárás végrehajtása közben következik be hiba, akkor az alkalmazástól függ a hibajelzés. Esetünkben a 0 visszatérési érték mutatja a hibát. A skeleton állományokat az rpcgen msg.x
paranccsal generálhatjuk le. Ez a következo állományokat hozza létre. ?? Egy msg.h header állományt, amelyben definiálja a MESSAGEPROG, MESSAGEVERS és PRINTMESSAGE konstansokat, továbbá a függvényeket a kliens és a szerver oldali formában is. ?? A kliens program vázát az msg_clnt.c állományban. Ebben szerepel a printmessage_1() eljárás, amit a kliens program hív. ?? A szerver program vázát az msg_svc.c állományban. Ez a program hívja az msg_proc.c állományban található printmessage_1() eljárást. Az rpcgen a “-a” kapcsoló használata esetén ezek mellett a kliens és a szerver program egyszeru implementációját és a makefile-t is létrehozza. (Vigyázzunk a make clean használatával, mert itt a szokásos megvalósítással szemben a kliens és szerver program forrását is letörli.)
X WINDOW , KDE 148
9. X Window, KDE 9.1 X Window Manapság ha egy operációs rendszer versenyképes szeretne lenni a piacon, akkor kétség kívül szüksége van egy grafikus kezeloi felületre (GUI). A GUI felület egyértelmuen barátságosabb és könnyebben kezelheto, mint egy szöveges terminál. A Unix rendszerek és a Linux grafikus felülete az X Window (vagy egyszeruen X), amely hasonlóan sok más Unix-os fejlesztéshez egy akadémiai projekt eredménye. Az MIT- n az Athena projekt keretében fejlesztették ki. A Unix kezdetek óta rendelkezik a multiuser, multitaszk képességekkel, illetve a hálózatok elterjedése óta támogatja a távoli bejelentkezést. Az X architektúráját is úgy alakították ki, hogy illeszkedjen ebbe az elképzelésbe. Az X az Athena projekt keretében, 1984-ben jött létre. 1988-tól az X konzorcium felügyeli a fejlesztést és a terjesztést. Specifikációja szabadon hozzáférheto ez által utat nyitottak az ingyenes implementációk létrejöttének, amilyen az XFree86. Linux alatt is ezt a felületet használjuk. Azonban elterjedtsége nem korlátozódik a Linuxra, használják más operációs rendszereken is (BSD, OS/2), de nevével ellentétben más CPU architektúrákon is.
9.1.1 X Window architektúra Az X-et kliens-szerver architektúrájúra alakították ki. Az applikációk a kliensek, amelyek kommunikálnak a szerverrel. Kéréseket küldenek, és információkat kapnak. Az X szerver kezeli a képernyot és a bemeneti eszközöket (billentyuzet, egér), a programok ezekhez nem férnek hozzá, csak az X szervert kérik meg az egyes muveletek végrehajtására. Ennek a megoldásnak az elonye egyértelmu. A kliensnek nem kell az adott eszköz jellemzo ivel foglalkoznia, csak a szerver kommunikációt kell implementálnunk. Ez pedig nem különbözik attól, mintha csak egy grafikus könyvtárat használnánk. Azonban az X architektúra ennél tovább is megy. Nem szükséges, hogy a kliens és a szerver applikáció egy gépen fusson. A kliens-szerver protokoll hálózaton keresztül is megvalósítható bármilyen IPC mechanizmussal, amely megbízható stream jellegu kommunikációt biztosít. Az X csomag tartalmaz egy fejlesztoi könyvtárat, neve Xlib, amely az alacsony szintu kliens-szerver kommunikációt implementálja. A programjainkban csak ezeket a függvényeket kell használnunk, hogy alkalmasak legyenek X környezetben futni.
9.1.2 Window manager Amikor ablakos, grafikus felületet használunk, akkor igényünk szokott lenni arra, hogy a kliens ablakokat kezelhessük : mozgathassuk, átméretezhessük, minimalizálhassuk, maximalizálhassuk, stb. Az X nem kezeli ezeket a muveleteket, hanem a window manager-re vár a feladat.
X WINDOW , KDE 149 A window manager is csak egy kliens program, nem része az X-nek, így kívánság szerint cserélgethetjük. Széles választékból választhatunk az ízlésünknek megfelelot. Fo feladata a kliensek ablakainak menedzselése, azonban emellett különbözo egyéb funkciókkal is rendelkezhet. Az egyik ilyen általános funkció a kliens programok indítása.
9.1.3 Kliens applikációk Ha az eddigi információink alapján nekivágunk X-es applikációnk fejlesztésének az Xlib segítségével, hamar rájövünk, hogy ez nem egyszeru feladat. Az Xlib csak a szükséges alapokkal szolgál, és egy gomb kirakása, szöveg, vagy egyéb kontroll elem elhelyezése elég komplikált muvelet lehet. Az MS Windows alatti programozásban járatosakat leginkább a Windows API-ra emlékeztetheti. Szerencsére az elottünk járók a kontroll elemek nehézkes kezelését látva létrehoztak olyan programozói könyvtárakat, amelyek egyszerubbé teszik a feladatunkat. Ezeket a könyvtárakat Widget Library-nek nevezzük. Természetesen ezek után nem ajánlatos az Xlib könyvtár függvényeit közvetlenül hívni, mert konfliktusba keveredhetnek egymással. Mivel innentol a Widget könyvtár felelos minden elem megrajzolásáért és kezeléséért, ezért programunk kezeloi felületének jellemzoi, viselkedése nagyban függ a könyvtártól, amelyet választottunk. Az elso Widget könyvtár az Athena projekt keretében kifejlesztett Athena könyvtár volt. Csak a legalapvetobb elemeket tartalmazza, és a kontroll elemek kezelése eltér a manapság használatosaktól. Ezt követo legismertebb könyvtár a az Open Software Foundation (OSF) Motif csomagja volt. 1980-tól a korai 1990-es évekig volt elterjedt. A legkomolyabb hibája, hogy súlyos összegekbe kerül a developer license. Manapság már vannak jobb alternatívák árban, sebességben, szolgáltatásokban. Ilyen a Gtk, amely a GIMP projekthez készült. Aránylag kicsi, sok szolgáltatással, bovítheto, és teljesen ingyenes. Vagy egy másik népszeru toolkit, a Qt, amely a KDE projekt óta ismert igazán, mivel a KDE alapját szolgáltatja. A forráskódja nem, de a használata ingyenes. További alternatíva a LessTif, amely egy ingyenes API kompatibilis helyettesítoje a Motif- nak.
9.1.4 Desktop Environment A korábbiakból látható, hogy felhasználóként választhatunk window manager-t kedvünk szerint, és a programok írói is választhatnak widget könyvtárakat. Azonban ennek a nagy szabadságnak megvannak a hátrányai: ?? A kliens program írója határozza meg, hogy milyen widget könyvtárat használ. Azonban mivel e könyvtárak kontroll elemei nagyban különbözhetnek, elofordulhat, hogy ahány programot használunk, annyiféle módon kell kezelni. ?? Ezek a widget könyvtárak általában dinamikusan linkelodnek a programokhoz, azonban ha a kliens programjaink különbözoket használnak, akkor az plusz memóriát igényel. ?? A window manager-ek is sokfélék, különbözo kezelési koncepciókkal.
X WINDOW , KDE 150 ?? Az egész felület ezeknek következtében nem áll össze egy egységes egésszé. Nem lehet egyben konfigurálni a kinézetét, kezelését. Azért alakult ez így, mert az X kialakítása során a flexibilitásra, és a használók szabadságára helyezték a hangsúlyt, ellentétben a Windows és a MacOS megkötöttségeivel, zártságával. Azonban ez a fent említett helyzethez vezetett, ezért elotérbe került a komplex felület létrehozása a szabadsággal szemben. Ez vezetett a Desktop Environment-ek kialakulásához. A desktop environment tartalmaz szolgáltatásokat és módszertanokat, amellyel a felület egységesítheto és a problémák leküzdhetoek. Ugyanakkor több fajtája is létezik, és a választás szabadsága ez által megmarad. Az ismertebbek: CDE (Common Desktop Environment), KDE (K Desktop Environment), GNOME. Egyik ilyen környezet a KDE. A csomag az alábbi elemekbol épül fel: ?? Egy window manager (kwm). ?? Grafikus eszközkészletként a Qt, amelyet kibovít környezet specifikus funkciókkal (kdelibs). Ezzel a programozók számára egy eszközkészletet ad, hogy egyszeruen fejleszthessenek azonos kinézetu programokat. ?? Továbbá a környezetet kiegészítik olyan elemek, mint a launcher panel (klauncher), általános file manager (Konqueror), konfigurációs program (control panel), amellyel a felület általánosan konfigurálható, stb.
9.2 KDE A KDE egy korszeru, network transzparens, ingyenes desktop environment a Unix munkaállomások számára. Célja, hogy egy könnyen kezelheto felületet nyújtson a Unix munkaállomások számára, hasonlót mint a MacOS, vagy a Microsoft Windows. Ennek elosegítésére sok hasznos, egységes felülettel rendelkezo applikációt tartalmaz, lehetové teszi az általános konfigurációt, tartalmaz egy office környezetet, és egy fejlesztoi framework-öt a programozók számára. A KDE történelmének néhány momentuma : ?? 1996. októberében indította útjára a projektet Matthias Ettrich. ?? 1997. augusztus15: Az elso KDE találkozó. ?? 1998. július12. 1.0 verzió megjelenése. ?? 2000. október 23-án adták ki a 2.0 stabil verziót. ?? 2002. április 3. A 3.0 verzió. És természetesen a fejlodés nem állt meg, egyre újabb verziókat kaphatunk kézhez.
KDE FEJLESZTÉS 151
10. KDE fejlesztés Miért érdemes KDE alá fejleszteni? A KDE egy népszeru, ingyenes grafikus felület, ezért a legtöbb disztribúció tartalmazza. Emellett a fejlesztok támogatására számos jól használható elemet tartalmaz: hálózati állománykezelés, drag and drop funkció, IPC megoldások, többnyelvuség támogatása, stb. Az elemeket a KDE és a Qt könyvtárak C++-ban implementálják, amely lehetové teszi a nyelv elonyeinek kihasználását. Származtathatunk belole további osztályokat, amelyekben módosíthatjuk a viselkedést. Továbbá a Qt signal/slot mechanizmusa jól használható alternatívája a C típusú callback metódusnak. A KDE belso struktúrája a következo ábrán látható: KDE Alkalmazások
KOffice KParts
KDE és Qt Programkönyvtárak XWindow Rendszer Linux/UNIX Operációs Rendszer Ábra 10-1 A KDE belso struktúrája
(A KParts a KDE komponens rendszere, a KOffice pedig az office környezet.)
10.1 Hello World Kezdésként nézzük meg az egyszeru “Hello World” programon a KDE programok felépítését. #include #include #include int main(int argc,char* argv[]) { KApplication khello(argc, argv, "khello"); KLineEdit* helloedit=new KLineEdit(); QString hellostring("Hello world!"); helloedit->setText(hellostring); helloedit->setReadOnly(true); helloedit->setAlignment(Qt::AlignCenter); helloedit->show(); khello.setMainWidget(helloedit); return khello.exec(); }
KDE FEJLESZTÉS 152
Ahhoz, hogy lefordíthassuk a kódot, be kell állítanunk a KDEDIR és a QTDIR környezeti változókat. A KDEDIR az a könyvtár, ahova a KDE-t installáltuk. RedHat esetén a KDEDIR értéke /usr szokott lenni. A fenti kódot a következo Makefile állománnyal tudjuk lefordítani: QTINC = -I$(QTDIR)/include KDEINC = -I/usr/include/kde QTLIB = -L$(QTDIR)/lib KDELIB = -L/usr/lib/kde khello : khello.o g++ $(QTLIB) $(KDELIB) -lkdeui -lkdecore -lqt -ldl \ khello.o -o khello khello.o : khello.cpp g++ -c $(QTINC) $(KDEINC) khello.cpp
Ezek után lefuttatva a programot a következo ábrán látható eredményt kapjuk:
10.2 KDE program struktúra Egy tipikus KDE program struktúrája a következo ábrán látható:
KMyContent
KMyMainWindow KStatusBar KToolBar
KMainWindow KMenuBar
KApplication Ábra 10-2 KDE program struktúra
A KApplication az alacsony szintu KDE szolgáltatásokat nyújtja. A KMainWindow osztályból származik az applikáció fo ablakaként szolgáló KMyMainWindow osztály. A KMenuBar, KToolBar, KStatusBar osztályok a nevükben is szereplo ablakelemeket valósítják meg. A KMainWindow hozza létre, pozícionálja és méretezi oket, amely rutinokat a KMyMainWindow osztályban módosíthatjuk. A KMyContent valamely widget lehet, így megvalósítására számos osztály áll rendelkezésünkre. Kezelését szintén a foablak végzi. Mindezen widget-ek a KApplication osztályon keresztül
KDE FEJLESZTÉS 153 kommunikálnak a felhasználóval. A KApplication végzi az események feldolgozását és továbbítja a jelzéseket az egyes kontroll elemeknek. A KApplication osztály kapja meg az X üzeneteit az ablakozó rendszertol, és küldi tovább a szükséges widget számára. Biztosítja a hozzáférést a fontokhoz, a felület beállításaihoz, és feldolgozza a KDE programok szokásos parancssori paramétereit.
10.2.1 Egyszeru applikáció A tipikus main() függvény KDE applikációk esetén elég rövid, mivel a valódi munkát a KMainWindow-ból származtatott osztály végzi. #include #include "ksimpleapp.h" int main (int argc, char *argv[]) { KApplication kapplication (argc, argv, "ksimpleapp"); if (kapplication.isRestored()) RESTORE(KSimpleApp) else { KSimpleApp *ksimpleapp = new KSimpleApp; ksimpleapp->show(); } return kapplication.exec(); }
Létrehoz egy példányt a KApplication osztályból, illetve a KSimpleApp osztályunkból, amely a KMainWindow-ból származik (lentebb látható). A KApplication objektum konstruktorában átadjuk az argumentum paramétereket. Az utolsó paraméter a program neve, de egyben az ablak felirataként és az ikonok megadására is szolgál. Ha a session manager indítja a programunkat, vagyis visszaállítja, azt a KApplication::isRestored() függvény adja meg. Ilyenkor a feladatot a RESTORE() makró elvégzi. Ilyenkor az ablak felülete olyan állapotban állítódik vissza, ahogy az kilépéskor volt. Ha normál módon a felhasználó indítja a programot, akkor létrehozunk egy példányt a KSimpleApp osztályból, és láthatóvá tesszük. Bár valójában a show() függvény még nem teszi láthatóvá, csak miután már belefutott az eseményvezérlo ciklusba: kapplication.exec();
Ez a ciklus addig fut, amíg a legutolsó ablakot is be nem zárjuk. Fogadja az eseményeket az X-tol és továbbítja a KDE/Qt widget osztályoknak. Kilépéskor véget ér és ez által a programunk is befejezi futását. A ksimpleapp.h tartalmazza a fo widget deklarációját.
KDE FEJLESZTÉS 154
#include class QLabel; /** * This is a simple KDE application. * * @author David Sweet **/ class KSimpleApp : public KMainWindow { Q_OBJECT public: /** * Create the widget. **/ KSimpleApp(QWidget* parent = 0, const char *name=0); public slots: /** * Reposition the text in the context area. The user will * cycle through: left, center, and right. **/ void slotRepositionText(); private: QLabel *text; int alignment[3], indexalignment; };
A KMainWindow osztályból származtatjuk, melynek segítségével egy dokumentum alapú applikációt hozunk létre. A KMainWindow létrehozza a szokásos kezeloi elemeket (menubar, toolbar, statusbar), továbbá kezeli a session management alap funkcióit is, vagyis kilépéskor menti a felület állapotát és vissza is állítja. A KSimpleApp implementációja a következo : #include #include #include #include #include #include #include #include
#include "ksimpleapp.moc" KSimpleApp::KSimpleApp(QWidget* parent, const char *name) : KMainWindow (parent, name) { QPopupMenu *filemenu = new QPopupMenu; KAction *reposition = new KAction(i18n("&Reposition Text"), QIconSet(BarIcon("idea")),CTRL+Key_R,
KDE FEJLESZTÉS 155 this, SLOT(slotRepositionText()), actionCollection()); reposition->plug (filemenu); filemenu->insertSeparator(); KStdAction::quit(kapp, SLOT(closeAllWindows()), actionCollection())->plug (filemenu); menuBar()->insertItem(i18n("&File"), filemenu); reposition->plug(toolBar()); statusBar()->message(i18n("Ready!")); text = new QLabel(i18n("Hello!"), this); text->setBackgroundColor (Qt::white); alignment [0] = QLabel::AlignLeft | QLabel::AlignVCenter; alignment [1] = QLabel::AlignHCenter | QLabel::AlignVCenter; alignment [2] = QLabel::AlignRight | QLabel::AlignVCenter; indexalignment = 0; text->setAlignment(alignment[indexalignment]); setCentralWidget(text); } void KSimpleApp::slotRepositionText () { indexalignment = (indexalignment+1)%3; text->setAlignment(alignment[indexalignment]); statusBar()->message(i18n("Repositioned text in content area"), 1000); }
A QLabel widget egy statikus szöveg megjelenítésére szolgál. Jelen esetben a KMyContent funkcióját tölti be. A menüsáv egy File menü elemet tartalmaz. Ennek két eleme a szöveg átrendezése, és a kilépés. A toolbar egy elemet tartalmaz, amely szintén az átrendezés. A következo képen láthatjuk, hogy hogyan is fog kinézni:
KDE FEJLESZTÉS 156
Ábra 10-3 A KSimpleApp program felülete
A KSimpleApp konstruktorában láthatjuk ezen elemek létrehozását. Mielott eloállítanánk a menü sort, az egyes menüket létre kell hoznunk a QPopupMenu osztályból. Ez esetünkben a “File” menüt jelenti. Ezek után létrehozunk két action-t. Ez tartalmaz minden információt és függvényt, amely az egyes menü va gy toolbar funkciók lekezelésére szükséges. Az elso egy egyedi akció. Ezzel lehet majd a szöveg rendezését állítani. KAction *reposition = new KAction (i18n("&Reposition Text"), QIconSet(BarIcon("idea")),CTRL+Key_R, this, SLOT (slotRepositionText()), actionCollection());
Az “&” jel a gyorsító karakter jelzésére szolgál. Hatására az “R” karakter aláhúzással jelenik meg. A gyorsító billentyut késobb a CTRL+Key_R makróval állíthatjuk be. Ezek után a CTRL+R billentyukombinációval elohívhatjuk az akciót. A második paraméter az ikont állítja be, amely mind a menüben, mind a toolbar-on látható. A this és a SLOT(slotRepositionText()) paraméterek az események lekezelésének beállítására szolgálnak. Megadják, hogy melyik objektum melyik függvénye hívódjon meg, ha az akciót aktiváljuk (kiválasztjuk a menüelemet, rá klikkelünk az eszközsoron, vagy a gyorsító billentyuket használjuk). Ez példa a signal/slot mechanizmus használatára, amelyet majd késobb tárgyalunk. Végül a KAction::plug() függvénnyel helyezhetjük el a menüben, vagy az eszközsorban. A másik action a quit, amely egy általános menü elem. Ezeket az általánosan használt, szabványos akciókat a KStdAction osztály implementálja. A insertSeparator() függvény egy vízszintes vonalat he lyez el a két menü elem közé. Végül a “File” menüt elhelyezzük a menü sávban: menuBar()->insertItem(i18n("&File"), filemenu);
KDE FEJLESZTÉS 157
A menuBar() függvény elso hívása létrehozza a KMenuBar widget-et. Megszüntetni majd a KMainWindow fogja, amikor már nincs szükség rá. Ezek után az insertItem() függvény elhelyezi a menüt. Hasonlóan a menü sávhoz az eszközsáv is az elso toolBar() függvényhívásra jön létre. A plug() függvénnyel pedig elhelyezhetjük rajta az akció elemünket: reposition->plug(toolBar());
Ha rámutatunk az eszköz gombra, akkor egy kis segítség szöveg szokott megjelenni, amelyet tooltip-nek nevezünk. Ez az akció elso paramétereként megadott szöveg lesz. Következo lépésként a státusz sávot hozzuk létre a statusBar() függvény meghívásával. A beállított szöveg a “Ready!” lesz. Ezek után már csak a fent említett QLabel hozzuk létre, amelyet az applikáció fo megjelenítojeként használunk (setView()). Az átrendezés akció lekezeléseként a slotRepositionText() függvényt hoztuk létre. Ez átállítja a QLabel widget rendezési tulajdonságát, illetve módosítja a státusz sor szövegét.
10.3 A signal-slot modell A signal-slot modell a Qt egyszeru eseménykeze lési módszere és egyben az egyik legfontosabb része. A signal azt mondja meg nekünk, hogy valami történt. Signal-ok generálódnak, ha használjuk a felületet, vagy a számítógépen belül történik valami. Például kapunk egy jelzést az idorol. A slot-ok azok a függvények, amelyek a signal-okra válaszolnak. Fontos, hogy válaszoljon, ellenkezo esetben úgy tunhet, mintha a programunk lefagyott volna. A metódus objektum független. A slot függvény, amely lekezel egy bizonyos signal-t, a program bármelyik objektumában elhelyezkedhet. A signal-t küldo objektumnak semmit nem kell tudnia a slot függvényrol, vagy arról melyik objektumban van. Bár a signal-slot modell elsodleges funkciója az esemény lekezelés, használható kommunikációra is az objektumok között. Például amikor két ablaknak kell kommunikálnia egymással ez a módszer egyszerubb, mint pointerekkel elérni a másik objektumot. Sok más toolkit-nél az eseménykezelés callback függvényekkel történik. Azonban ezeknél nincs típus ellenorzés és paraméterezésük is korlátozott, nem úgy, mint a signal-slot modellnél.
KDE FEJLESZTÉS 158
10.3.1 Slot létrehozása A Qt könyvtár bázis osztálya a QObject. Minden olyan osztály, amely a signal-slot modellt implementálja közvetlenül, vagy követetve, de ebbol az osztályból kell, hogy származzon. Ezek után az elso lépés, hogy az osztály definíciós állományban elhelyezzük a Q_OBJECT kulcsszót. (Ezt a Qt moc fordítója fogja értelmezni.) A slot is csak egy szokásos tagfüggvény, azonban a slots szekcióban kell deklarálnunk. Ezek a függvények is lehetnek public, protected, és private típusúak. class MyObject : public QWidget { Q_OBJECT public: MyObject(); public slots: void mySlot(); };
A slot függvényünket mySlot()-nak hívják. A slots kulcsszó elott áll a hozzáférés beállítása, amely esetünkben public. A függvény implementációja semmiben sem különbözik egy szokásos függvénytol.
10.3.2 Signal küldése Amikor a Qt-t egy eseményrol akarjuk értesíteni, akkor egy signal-t küldünk. Amikor ez megtörténik, akkor a Qt lefuttat minden slot függvényt, amelyet a signal-hoz rendeltünk. Mielott a signal-t küldhetnénk, eloször definiálnunk kell. Az osztálynak, amely küldi, egy signal definícióval kell rendelkeznie. A signal-okat hasonlóan a slot-okhoz a signals szekcióban kell megadnunk. class MyObject : public QWidget { Q_OBJECT public: MyObject(); signals: void mySignal(); };
Elküldeni az emit() függvénnyel tudjuk. emit mySignal();
10.3.3 Signal és slot összekapcsolása Ahhoz, hogy egy slot reagáljon egy signal-ra, hozzá kell kapcsolnunk. Egy signal-hoz akár több slot függvényt is kapcsolhatunk. A parancs szintaxisa egyszeru:
KDE FEJLESZTÉS 159
connect(srcobj, SIGNAL(signal()), dstobj, SLOT(slot()))
Paraméterei: ?? srcobj: Azon objektumra mutató pointer, amelytol a signal származik. ?? signal: A kezelendo signal. Az srcobj objektumnak kell küldenie. ?? dstobj: Azon objektumra mutató pointer, amelynek fogadnia kell a signal-t. ?? slot: A dstobj slot tagfüggvénye, amely lekezeli a signal-t. A metódus használatát megnézhetjük a következo példán: Az osztály definíciója: #include class MyWindow : public QWidget { Q_OBJECT // Enable signals and slots public: MyWindow(); private slots: void slotButton1(); void slotButton2(); void slotButtons(); private: QPushButton *button1; QPushButton *button2; };
És az implementálása: #include "signal-slot.moc" #include #include MyWindow::MyWindow() : QWidget() { // Create button1 and connect it to this->slotButton1() button1 = new QPushButton("Button1", this); button1->setGeometry(10,10,100,40); button1->show(); connect(button1, SIGNAL(clicked()), this, SLOT(slotButton1())); // Create button2 and connect it to this->slotButton2() button2 = new QPushButton("Button2", this); button2->setGeometry(110,10,100,40); button2->show(); connect(button2, SIGNAL(clicked()), this, SLOT(slotButton2())); // When any button is clicked, call this->slotButtons() connect(button1, SIGNAL(clicked()), this, SLOT(slotButtons())); connect(button2, SIGNAL(clicked()), this, SLOT(slotButtons())); }
// This slot is called when button1 is clicked. void MyWindow::slotButton1() { cout << "Button1 was clicked" << endl;
KDE FEJLESZTÉS 160 }
// This slot is called when button2 is clicked void MyWindow::slotButton2() { cout << "Button2 was clicked" << endl; }
// This slot is called when any of the buttons were clicked void MyWindow::slotButtons() { cout << "A button was clicked" << endl; }
10.3.4 Signal-slot metódus paraméterekkel A kommunikáció során sokszor hasznos, ha többet is tudunk közölni a program másik részé vel, mint egy egyszeru jelzés. Ha erre van szükségünk, akkor a legegyszerubb módszer a signal-slot metódus paraméterekkel való használata. Ebben az esetben a signal, és a slot függvény paramétereinek meg kell egyezniük. Nézzünk erre is egy példát: class MyWindow : public QWidget { Q_OBJECT public: MyWindow(); private slots: void slotChanged(int i); signals: void changed(int i); };
A konstruktorban az alábbi módon összekapcsolhatjuk oket: MyWindow::MyWindow() : QWidget() { connect(this, SIGNAL(changed(int)), this, SLOT(slotChanged(int))); }
A signal küldése is egyszeru: void MyWindow::emitter(int i) { emit changed(i); }
10.3.5 Slot átmeneti objektumokban Amennyiben egy slot függvény olyan objektumban helyezkedik el, amelyet csak átmenetileg használunk, akkor figyelnünk kell arra, hogy megsemmisítése elott
KDE FEJLESZTÉS 161 megszüntessük a kapcsolatot. Ellenkezo esetben, amikor a signal legenerálódik, nem lesz meg a függvény, amelyet meg kellene hívnia. Így ilyenkor hibajelzést kapunk a rendszertol.
10.4 Meta Object Compiler (moc) A Meta Object Compiler (moc) a Qt része. Neve ellenére valójában nem igazi fordító. Valójában kulcsszavakat keres a forrás állományokban és C++ kódra cseréli oket. Ezzel a program fejlesztés során megtakarít valamennyi munkát, mert az általa eloállított kód, amelyet a fejlesztonek kellene megírnia, jóval komplexebb lehet, mint az eredeti. A moc a következo kulcsszavakat értelmezi: Q_OBJECT, public slots:, protected slots:, private slots:, signals: (A szignálok mindig publikusak.) A Q_OBJECT kulcsszó mondja meg a moc-nak, hogy Qt osztály definícióról van szó, amelyet át kell konvertálnia. Minden slot függvénynek a slots kulcsszók alatt, a signal-oknak a signals kulcsszó alatt kell szerepelnie. A moc ezeket értelmezi, és a Qt osztály definíciót átkonvertálja C++ kódra. Azokat az állományokat, amelyekben nem szerepelnek moc kulcsszavak, a hagyományos módon fordítjuk : g++ -I$QTDIR/include -c main.cpp
Ellenben ha használjuk oket egy forrásban, akkor eloször a moc programmal át kell konvertálnunk a header állományokat: moc mywindow.h -o mywindow.moc
Ezek után az eredményként kapott mywindow.moc file-t kell include-olnunk a forrás állományban. Maga a fordítás a hagyományos módon történik : g++ -I/$QTDIR/include -c mywindow.cpp
A tárgykódú állományokat pedig az alábbi módon linkelhetjük össze : g++ -o myprog main.o mywindow.o -L/$QTDIR/lib -lqt
Ezek után kész a programunk.
10.5 Egyszeru származtatott widget A widget a grafikus felhasználói felület részei. Az egyszerubbek lehetnek különbözo kontroll elemek, vagy csak jelzések, például nyomógomb, vagy felirat. A komplexebbek bonyolultabb muveleteket is elvégezhetnek, vagy komplikáltabb felhasználói bemenet kezelést is megvalósíthatnak, mint például egy helyesírás ellenorzo, vagy HTML oldal megjeleníto elem.
KDE FEJLESZTÉS 162
A KDE-ben a widget-et C++ osztályok reprezentálják. Általában minden egyes widget-hez egy osztály tartozik. Például a nyomógombot a QPushButton osztály reprezentálja. Azonban minden ilyen osztály egy közös osbol származik, amelynek a neve QWidget.
10.5.1 A QWidget A QWidget számos, az ablakokra vonatkozó eseményt kezel, menedzseli az általános ablakjellemzoket, nyilvántartja a szülot és a gyerekeket. Az ablak eseményei lehetnek méretváltozással, áthelyezéssel kapcsolatosak, vagy a felhasználó beavatkozásának eredményei. A szülo-gyerek kapcsolat meghatározza a widget megjelenítését. A gyerek a szülo felületén helyezkedik el, a szülo widget által határolva. A legfelso szinten lévo widget a desktop-on egy ablakkerettel ellátva jelenik meg. A rendszer által generált események mondják meg a widget-nek, hogy újra kell rajzolnia a felületét, a felhasználó átméretezte, elmozgatta, az egérrel a felületére klikkel, vagy billentyuket ütött le. A QWidget virtuális függvények meghívásával kezeli le ezeket az eseményeket. Mindegyik metódus rendelkezik egy paraméterrel, amely leírja az esemény jellemzoit. A származtatott osztályok ezeket a függvényeket felül definiálják. Az implementációtól függ, hogy az adott elem milyen funkcionalitást valósít meg. A legfontosabb rendszeresemény az ablak kirajzolására vonatkozik. Minden esetben megkapja a widget, amikor szükséges az ablak újra rajzolása. Ennek lekezeloje a paintEvent(). Megvalósítása meghatározza a kontroll elem megjelenését. A további függvények megtalálhatóak a QWidget osztály dokumentációjában.
10.5.2 Signal-slot kezelés A QWidget a QObject osztályból származik, ezért implementálja a signal-slot metódust. A widget szignálokat használ, hogy jelezze a felhasználó beavatkozásait, vagy állapotának megváltozását. Például a nyomógomb a clicked() szignállal jelzi, ha a felhasználó ráklikkelt a gombra. Az elem állapotának megváltoztatására implementáltak slot függvényeket. Ezáltal a widget egyszeruen tud reagálni olyan eseményekre is, amelyek nem közvetlenül rá irányulnak. Összekapcsolhatjuk más kontroll elem szignáljaival. Például ha megnyomunk egy “default” gombot, és ennek hatására az ablakon az alapértelmezett értékek állítódnak be.
10.5.3 A widget megrajzolása Habár a widget felülete a muködése során változhat az értékeknek megfeleloen, azonban egy dolog nem változik, mindig a paintEvent() függvény felel a rajzolásért. Magát a rajzolást a QPainter osztály segítségével végezhetjük el. Rajzolási primitíveket, szöveg megjelenítést, és egyéb komolyabb muveleteket tartalmaz.
KDE FEJLESZTÉS 163 10.5.3.1 A rajzolás kiváltása A paintEvent() lekezelo függvény meghívását a QWidget::update() függvénnyel a programból is kiválthatjuk. Ebben az esetben generálódik egy szignál, aminek hatására meghívódik a függvény, letörlodik a widget felülete, és az egész újra rajzolódik. Argumentumokat is átadhatunk neki, amellyel megadhatjuk, hogy csak a terület egy részét rajzolja újra. A QWidget::update(int x, int y, int width, int height) vagy a QWidget::update(QRect rectangle) függvények támogatják ezt a kezelési módot. Ahhoz, hogy ennek tényleg hasznát lássuk a paintEvent() függvényben implementálnunk kell a feldolgozását. Az update() függvény egy újrarajzolási eseményt helyez el a várakozási sorban. Ez csak akkor hajtódik végre valójában, amikor rá kerül a sor. Ennek a megoldásnak a haszna, hogy a program más fontos eseményekre is reagálhat közben, ezáltal a muködésében nem tapasztalhatunk fennakadásokat, lefagyáshoz hasonló viselkedést. A QWidget::repaint(int x, int y, int width, int height, bool erase) függvénnyel is kiválthatjuk az új rarajzolást. (Több formája is létezik hasonlóan az update() függvényhez.) Azonban ilyenkor a paintEvent() függvény közvetlenül hívódik meg. Tehát a frissítési kérelem nem kerül be az események várakozó listájába a többi esemény közé, mint az update() esetén. Ezért vigyáznunk kell, hogy ha egy ciklusból a repaint() függvényt hívjuk, akkor addig a rendszer más eseményekre nem fog reagálni. A rajzolás lehetséges a paintEvent() függvényen kívül is, azonban ilyenkor gondoskodnunk kell róla, hogy a paintEvent() a következo meghíváskor ugyanazt rajzolja ki. Ellenkezo esetben eltunik a következo frissítésnél, amit a felületre rajzoltunk. 10.5.3.2 A rajzolás A Qt-ben a rajzolásért a QPainter osztály a felelos. A widget felületére rajzolás, a bufferbe rajzolás (pixmap), a nyomtatáshoz a Postscript kimenet eloállítása mind ennek az osztálynak a feladata. A QPainter mindig egy a QPaintDevice osztályból származó objektumra rajzol. Ilyenek a QWidget, a QPixmap, a QPrinter, és a QPicture. A QPicture alkalmas arra, hogy felvegye a rajzolás menetét. Ezek után visszajátszható más eszköz felületére. Az egyszeru paintEvent() implementációhoz felhasználhatjuk. Ilyenkor minden rajzolási részletet felvehetünk, majd az egészet az újrarajzolásnál visszajátszhatjuk. És ezt megtehetjük akár a nyomtató eszközre is. Azonban bonyolultabb muveletek esetén és a felvétel és visszajátszás hosszadalmas folyamat lehet, és más megoldást kell használnunk.
10.5.4 A felhasználói esemény kezelése A programok leggyakrabban egér vagy billentyuzet eseményeket kapnak a felhasználóktól. Ez a KApplication osztály segítségével a megfelelo elemhez kerül. Ezeket az alábbi virtuális függvények felül definiálásával kezelhetjük le :
KDE FEJLESZTÉS 164
?? ?? ?? ?? ?? ??
mousePressEvent() mouseMoveEvent() mouseReleaseEvent() mouseDoubleClickEvent() keyPressEvent() keyReleaseEvent()
10.6 Dialógus ablakok A dialógus ablak az applikáció fontos része. Ha a dialógusok nem megfeleloen lettek megtervezve a feladathoz, akkor az nagyban csökkentheti a program hatékonyságát. A KDE felhasználói felület könyvtára (kdeui) számos osztályt és widget-et tartalmaz, amelybol az ablak felületét összeállíthatjuk. Ezeket kombinálhatjuk az ún. layout manager-ekkel, amelyek gondoskodnak az elemek elhelyezésérol. A dialógus ablakok felületének a megtervezéséhez több tervezo program is rendelkezésre áll. Ezekbol a legelterjedtebben használt a Qt csomaghoz tartozó Qt Designer. Segítségével egyszeruen megtervezhetjük a felületet, majd az általa készített leíró állományból, a KDevelop képes legenerálni a dialógus ablakot megvalósító osztályt.
10.6.1 Standard dialógus ablakok Mielott új dialógus ablakok tervezésébe vágnánk, elobb érdemes megnézni nem tettee meg már valaki más helyettünk. A KDE rendelkezik számos elore elkészített dialógus ablakkal az általános feladatok lekezelésére. Ezek használata ne m csak egyszerubb, hanem célszeru is az egységes kinézet érdekében. Vegyük a legfontosabbakat sorra. 10.6.1.1 KFileDialog Egy felhasználóbarát lehetoséget nyújt az állományok és könyvtárak kiválasztására. A QFileDialog tovább fejlesztett változata. Miután a felhasználó kiválasztotta az állományt, a tagfüggvényeivel elérhetjük ezt az értéket. Felülete a következo ábrán látható.
KDE FEJLESZTÉS 165
Ábra 10-4 KFileDialog
10.6.1.2 KFontDialog A KFontDialog egy dialógus ablakot szolgáltat a betutípus egyszeru kiválasztására. A választás után a KFontDialog::getFont() függvénnyel elérhetjük a kiválasztott értéket.
Ábra 10-5 KFontDialog
10.6.1.3 KColorDialog A KColorDialog osztály a szín kiválasztására nyújt egy egyszeru megoldást. Ezt megtehetjük grafikusan, vagy az értékek megadásával HSV vagy RGB rendszerben. A végeredményt a KColorDialog::getColor() függvénnyel kapjuk meg.
KDE FEJLESZTÉS 166
Ábra 10-6 KColorDialog
10.6.1.4 KMessageBox A KMessageBox egy egyszeru lehetoség információk megjelenítésére, vagy eldöntendo kérdések megjelenítésére, kezelésére. Számos statikus metódussal rendelkezik, amelyek nagyban befolyásolják a megjelenését, muködését. A feladatnak megfelelo kiválasztásával könnyen lekezelhetjük ezeket a gyakori feladatokat. A következo ábrán egy figyelmeztetés megjelenését láthatjuk.
Ábra 10-7 KMessageBox
10.6.1.5 Példa program A következo programmal elohozhatjuk a korábbi ábrákon látható dialógus ablakokat. A program feladata csak a dialógus ablakok egymás után való megjelenítése. Az utolsó dialógus ablak bezárásával a program futása is véget ér. /* main.cpp */ #include #include #include #include #include #include
int main (int argc, char *argv[]) { KApplication kapplication (argc, argv, "kstandarddialogs");
KDE FEJLESZTÉS 167 if (KMessageBox:: warningContinueCancel (0, "Are you sure you want to see this demo?", "Demo program", "See demo") == KMessageBox::Cancel) exit (0); KURL kurl = KFileDialog::getOpenURL (); if (!kurl.isMalformed()) { QString message; message.sprintf ("The file you selected was \"%s\".", (const char *)kurl.url()); KMessageBox::information (0, message, "File selected"); } QFont qfont; if (KFontDialog::getFont (qfont)) { QString message; message.sprintf ("Sorry, but you selected \"%d point %s\"", qfont.pointSize(), (const char *) qfont.family());
KMessageBox::sorry (0, message, "Font selected"); } QColor qcolor; if (KColorDialog::getColor (qcolor)) { QString message; message.sprintf ("Oh no! The color you selected was (R,G,B)=(%d,%d,%d).", qcolor.red(), qcolor.green(), qcolor.blue()); KMessageBox::error (0, message, "Error:
Color selected");
} return 0; }
10.6.1.6 További dialógus ablakok A KDE könyvtár további dialógus osztályokat is tartalmaz, amelyekre most bovebben nem térünk ki: KAboutDialog About dialógus ablak létrehozására KKeyDialog A gyorsító billentyuk konfigurálására. KLineEditDlg Egysoros szöveg bevitel kezelésére.
KDE FEJLESZTÉS 168 KPasswordDialog A jelszó megkérdezésére.
10.6.2 Egyéni dialógus ablakok Az egyéni dialógus ablakainkat a KDialogBase osztályból származtatjuk. Ez szolgál minden dialógus osztály szüloosztályaként. 10.6.2.1 Fix elhelyezés Ha tudjuk milyen elemeket szeretnénk elhelyezni a dialógus ablakunk felületén, és létrehoztuk oket, akkor a legegyszerubb, ha a bal felso sarkot 0, 0 koordinátájú pontnak tekintve egyszeruen az x és y koordináta megadásával megadjuk a pozícióját. Ezt minden widget-re meg kell tennünk, amely a dialógus ablak felületén megjelenik. Másik lehetoség egy terve zo program használata, ahogy arról már szó volt. A Qt csomag tartalmaz egy Qt Designer nevu eszközt, amely ezt a célt szolgálja. Segítségével egyszeruen megtervezheto a felület, és a tervezés közben folyamatosan láthatjuk, hogy hogyan fog kinézni. Az egyes elemek paramétereit is beállíthatjuk. Kimenetként a program egy leíró állományt állít elo. Ha ezt az állományt beillesztjük egy KDevelop projektbe, akkor automatikusan legenerálódik belole a dialógus ablak implementációja. Azonban ezt az állományt ne módosítsuk, mert módosítás esetén automatikusan felülíródik. Használatához származtassunk belole egy új osztályt, amelyben megírhatjuk a dialógus ablak függvényeit. Ha netán nem találunk a widget-et a Qt Designer-ben a célunk megvalósításához, akkor még ne adjuk fel. A Qt Designer csak a Qt könyvtár widget-jeit tartalmazza. A KDE könyvtárak ezt kibovítik, ezért lehet hogy ott megtaláljuk a kérdéses grafikai elemet. 10.6.2.2 Dinamikus elhelyezés A fix elhelyezésnek azonban van egy komoly hátránya. Nem képes alkalmazkodni a dialógus ablak méretváltozásaihoz. Ezzel kapcsolatosan a következo problémák merülnek fel: ?? Ha átméretezzük az ablakot, akkor a rajta lévo elemeknek valamilyen logikával alkalmazkodniuk kell ehhez. ?? Ha egy elemet leveszünk, vagy hozzáteszünk, akkor át kell rendezgetni a felületet. ?? Alkalmazkodni kell a font mérethez és annak változásához. ?? Ha a feliratok többnyelvuek (márpedig a KDE- nél így van), akkor alkalmazkodni kell a felirat változásaihoz is. Ennek megoldására rendelkezik a Qt könyvtár az ún. layout manager-ekkel. Ezek a QLayout osztályból származnak, és feladatuk a widget-ek menedzselése. A widget mérete, nyújtása, pozíciója könnyen kezelheto így. A QLayout különbözo viselkedésu megvalósításai a QHBoxLayout, amely vízszintes elrendezést biztosít, a QVBoxLayout, amely függolegeset, és a QGridLayout, amely
KDE FEJLESZTÉS 169 négyzetrács szerinti elrendezésrol gondoskodik. Ezek természetesen egymással kombinálhatóak, az egyik elrendezésbe beágyazhatunk egy másikat. Például a grafikai elemeket három csoportja bontjuk, ezeket egyesével függolegesen elrendezzük a QVBoxLayout segítségével, majd a csoportokat a QHBoxLayout menedzserrel egymás mellé helyezzük. Ezek mellett implementálhatunk saját layout manager-eket is.
10.6.3 Modalitás – modális és nem modális dialógus ablakok A dialógus ablakokat kétféle képen használhatjuk. Az egyik blokkol minden hozzáférést az applikáció többi részéhez, amíg látható, a másik esetben nem korlátozza semmiben a felhasználót. Az elso változat a modális dialógus ablak, a második a nem modális. A választás, hogy modális vagy nem modális dialógus ablakot használjunk, foként a dialógus ablak funkciójától függ. Például ha a programban semmi hasznosat nem tehetünk, amíg egy kérdést meg nem válaszolunk, akkor azt célszeru egy modális dialógus ablakon feltenni. Az állománynév kiválasztására is egy modális dialógust szoktunk használni. Ezzel szemben a keresés funkcióját általában nem modális dialógus ablakként valósítjuk meg. A nem modális dialógus ablak a felhasználó számára nagyobb flexibilitást tesz lehetové, nem korlátozza a lehetoségeket a fejleszto által elképzeltre. Ez ugyanakkor azzal jár, hogy a korrekt implementációja komplikáltabb lehet. Mindkét dialógus fajta a KDialogBase osztályból származik. A különbség a használat során jelentkezik. Ez a programozó szempontjából a megjelenítés és az adatok kiolvasása közötti különbséget jelenti. 10.6.3.1 Modális dialógus ablak Minden modális dialógus a QDialog::exec() függvényt használja az ablak megjelenítésére, és a program egyéb részeinek blokkolására. Amikor vissza tér, akkor a felhasználó választ adott a kérdésre. Ilyenkor az ablak láthatatlanná válik. #include int getColor( QColor &theColor, QWidget *parent ) { KColorDialog dialog( parent, "colorselector", true ); if( theColor.isValid() ) { dialog.setColor( theColor ); } int result = dialog.exec(); if( result == Accepted ) { theColor = dialog.color(); } return result; }
KDE FEJLESZTÉS 170 A dialógus objektum létrehozása után beállítjuk az értékét. Az exec() függvény meghívásával megjelenítjük az ablakot. Visszatérési értéke jelzi, hogy a felhasználó beállította az értéket, vagy úgy döntött, hogy mégse teszi. Az elobbi esetben kiolvassuk a beállított értéket. A dialógus objektum a stack-en jön létre, és automatikusan megsemmisül, amikor a függvény véget ér. Azonban ez nem feltételül szükséges. Vannak, akik a következo megoldást szokták preferálni és a dialógust a heap-en hozzák létre. #include int getColor( QColor &theColor, QWidget *parent ) { KColorDialog *dialog = new KColorDialog( parent, "colorselector", true ); if( dialog == 0 ) { return Rejected; // Rejected is a constant defined in QDialog } if( theColor.isValid() ) { dialog->setColor( theColor ); } int result = dialog->exec(); if( result == Accepted ) { theColor = dialog->color(); } delete dialog; // Important to avoid memory leaks return result; }
Amennyiben a dialógus nagy tárigénnyel rendelkezik, akkor az utóbbi megoldás sokkal célravezetobb lehet. Azonban vigyáznunk kell arra, hogy az objektumot megsemmisítsük. Ellenkezo esetben memory leak lesz a programunkban. 10.6.3.2 Nem modális dialógus ablak Amikor nem modális dialógus ablakot akarunk használni, akkor két dolgot kell megtennünk. Eloször is a dialógus objektumot a heap-en kell létrehoznunk (a new metódussal). A második, hogy a megjelenítést a show() függvénnyel végezzük el. A show() függvény az exec()-el szemben a dialógus ablak megjelenítése után azonnal visszatér, és nem vár az ablak bezárására. Ezért az objektum mutatóját el kell tárolnunk, mivel a megsemmisítést valahol a kód másik részén kell megejtenünk, amikor már nincs szükségünk az ablakra. A következo példa bemutatja a nem modális dialógus ablak használatát. KHexEditorWidget::KHexEditorWidget() { m_pDialog = NULL; }
KDE FEJLESZTÉS 171
KHexEditorWidget::~KHexEditorWidget() { if(m_pDialog != NULL) delete m_pDialog; } void KHexEditorWidget::gotoOffset() { if(m_pDialog == NULL) { m_pDialog = new KGotoDialog(topLevelWidget(), "goto", false); if(m_pDialog == NULL) { return; } connect(m_pDialog, SIGNAL(gotoOffset(uint,uint,bool,bool)), mHexView, SLOT(gotoOffset(uint,uint,bool,bool))); } m_pDialog->show(); }
Az m_pDialog mutató a KHexEditorWidget osztály tagváltozója. A konstruktorban NULL értékkel inicializáljuk, és a destruktorban megsemmisítjük. Elso megjelenítésekor létrehozzuk, a további esetekben pedig a show() fügvénnyel csak megjelenítjük. Amikor nem modális dialógus ablakot használunk, akkor a program többi részét értesítenünk kell a dialógus elemeinek változásairól. Erre a signal-slot metódus kiváló lehetoséget nyújt. 10.6.3.3 Nem modális dialógus ablak megsemmisítése A nem modális dialógus ablakok használata esetén felmerül a kérdés, hogy mikor semmisítsük meg. Alapvetoen két lehetoség közül választhatunk. Az egyik esetben az objektumot csak az applikáció bezárásakor, vagy a szülo osztály megsemmisítésekor szüntetjük meg. Ennek a megoldásnak kétségtelen elonye, hogy egyszeru. További haszna, hogy ha az ablakot újra használni szeretnénk, akkor csak elég újra megjelenítenünk. A legnagyobb hátránya viszont, hogy amikor nem látható, akkor is foglalja a memóriát. A másik lehetoség, hogy amikor a dialógust bezárjuk, azonnal meg is semmisítjük. Ez a jobb választás abban az esetben, ha egy dialógus ablak kicsi, gyorsan létrehozható, vagy ritkán használt. Habár így kicsit tovább tart az elohozása, azonban a memóriahasználatban kifizetodo lehet. Azonban egy dologra vigyáznunk kell. Nem szabad a dialógus ablak objektumát önmagából megsemmisíteni. Vagyis a következo megoldást mellozzük. void KGotoDialog::slotCancel() { hide(); // Ok, will hide the dialog delete this; // Bad! }
KDE FEJLESZTÉS 172
Itt a Cancel gomb lenyomásakor az objektumot önmagából próbáljuk felszabadítani, ami komoly hiba. A probléma ezzel, hogy a slot függvény a Cancel gomb szignáljához van kapcsolva. Azonban amikor egy szignált elküldünk, az csak akkor tér vissza, amikor már minden slot függvény visszatért. Azonban ha eközben valamelyik függvény megsemmisíti az ablakot és ezzel magát a gombot is a visszatéréskor nem tudni, hogy mi fog történni. A megsemmisítés megkönnyítésére a KDialogBase osztály egy hidden() szignált küld, amelyet felhasználhatunk. Azonban erre szintén érvényes az elobbi megkötés. Ezért az egyik gyakran használt megoldás az, hogy szignálra egy egyszer aktivizálódó, nulla késleltetésu idozítot húzunk fel. Ez azért biztonságos, mert bár a késleltetés zéró, az idozítohöz rendelt funkció csak akkor hajtódik végre, amikor a vezérlés visszakerül a fo ciklushoz. Ekkorra már minden slot befejezte a muködését, és ezáltal a szignál is visszatért. A következo példán láthatjuk ezt a metódust. void KJotsMain::configure() { if(m_pDialog == NULL) { m_pDialog=new ConfigureDialog(topLevelWidget(), 0, false); if(m_pDialog == NULL) { return; } connect(m_pDialog, SIGNAL(hidden()), this,SLOT(hide())); connect(m_pDialog, SIGNAL(valueChanged()), this, SLOT(updateConfiguration())); } m_pDialog->show(); } void KJotsMain::hide() { QTimer::singleShot(0, this, SLOT(destroy())); } void KJotsMain::destroy() { if((m_pDialog != NULL) && (!m_pDialog->isVisible())) { delete m_pDialog; m_pDialog = NULL; } }
Ez így kicsit bonyolultnak tunhet, ezért egyszerusítésére található megoldás a KDialogBase osztályban. Az egyik megoldás, a delayedDestruct() függvény, amely biztonságosan végrehajtja a megsemmisítést. Ez a funkció slot-ként is elérheto a slotDelayedDetsruct() függvénnyel. Hozzárendelhetjük szignálokhoz, amellyel a dialógus ablakot meg szeretnénk semmisíteni.
KDE FEJLESZTÉS 173
10.6.4 Dialógus ablak tervezési szabályok A dialógus ablak tervezésénél célszeru figyelembe venni a következo szempontokat: ?? Ne tervezzük a dialógus ablak tartalmát akkorára, hogy scrollbar-ra legyen szükségünk a használatához. Ilyenkor bontsuk lapokra és használjunk többlapos felületet. ?? Ne helyezzünk el menüt vagy toolbar-t a dialógus ablakon. Az a fo ablak hatásköre. A dialógus csak egyszeru akciókat tartalmazzon. ?? Lehetoleg a könyvtárak által kínált widget-ekbol építkezzünk. Ezáltal a felhasználó könnyebben megbarátkozik a kezeléssel. ?? Készítsük a felületet a leheto legegyszerubbre. ?? Az egyes állapotokat lehetoleg ne színekkel jelezzük, hanem szöveggel, vagy ábrákkal.
10.7 Dialógus alapú applikáció A rövid, egy egyszeru feladatra készült applikációkat gyakran, mint dialógus ablakokat implementálják. Számos ilyen programmal találkozhatunk a KDE környezetben. A dialógus alapú applikáció felépítése eltér az eddig megszokott dokumentum alapútól, azonban egyszeru. Nincs “KMainWindow”-hoz hasonló osztály a felület elemeinek kezeléséhez, azonban általában erre nincs is szükség, mert a felület egyszerubb. Nézzünk meg egy példa applikáció main.cpp kódját. #include #include "kdialogapp.h" int main (int argc, char *argv[]) { KApplication *kapplication = new KApplication(argc, argv, "kdialogapp"); KDialogApp * kdialogapp = new KDialogApp; kdialogapp->exec(); }
A példában a KDialogApp osztály egy a KDialogBase osztályból származtatott egyéni dialógus ablak. Látható, hogy szokás szerint a KApplication osztályból létrehozunk egy példányt, amellyel a bemeneti paramétereket is kezeljük, azonban ezek után nem használjuk a függvényeit. Ezzel szemben az esemény feldolgozó ciklus, a dialógus ablakban implementált. Vagyis a KApplication helyett a KDialogBase exec() függvényét hívjuk meg. Amikor a dialógust bezárjuk, és a függvény visszatér, az egyben az applikáció végét is jelenti.
KDE FEJLESZTÉS 174
10.8 Konfigurációs állományok A konfigurációs állományok tárolása KDE-ben emberek által értelmezheto formában, szöveg állományokban történik. Ezáltal a felhasználó, vagy az adminisztrátor egy egyszeru szöveg szerkeszto segítségével is módosítani tudja. További elonye, hogy script-ekkel ez a folyamat automatizálható is. Általában a felhasználó beállításai tárolódnak ezekben az állományokban. Az információkat kulcs – érték párokban tárolja a rendszer. Ezeket az értékeket csoportosíthatjuk is. #KDE Config File DefaultHeight=300 [Options ] BackgroundColor=255,255,255 ForegroundColor=100,100,255
Az állomány tartalmazhat megjegyzéseket is. Ezek a sorok #-al kezdodnek. Ezek az állományok a felhasználó home könyvtárában a .kde/share/config alkönyvtárban tárolódnak. Minden applikáció generál egy alapértelmezett állományt kappnamerc néven, amibol a kappname az applikáció neve. A konfigurációs állományok kezelésére a KConfig osztály szolgál. Az alapértelmezett config file objektuma a KApplication::config() függvénnyel érheto el. Erre nézzünk egy példát. kapp->config()->setGroup(“LineEditor”); str=kapp->config()->readEntry(“Text”,“Hello”));
Ha nem állítjuk be a group-ot, akkor az alapértelmezett “unnamed” csoportban keresi a rendszer a kulcs-értek párost. A readEntry() függvény visszaadja a “Text” kulcshoz tartozó értéket. Ha ezt nem találja, akkor az alapértelmezett érték a “Hello” lesz. Nézzük az írást. kapp->config()->setGroup(“LineEditor”); kapp->config()->writeEntry(“Text”,“Value”); kapp->config()->sync();
A writeEntry() beállítja a “Text” kulcshoz a “Value” értéket. Az írás muvelet cacheelve van, ezért kell meghívni a sync() függvényt.
10.9 A Dokumentum/Nézet Architektúra Ebben a fejezetben megvizsgáljuk, hogyan lehet összetettebb alkalmazásokat fejleszteni úgy, hogy a program struktúrája mindvégig áttekintheto bonyolultsága pedig kezelheto legyen.
10.9.1 A Dokumentum/Nézet Architektúra alapkoncepciói A felhasználói felülettel rendelkezo (GUI) alkalmazások fejlesztésekor nagyon sok esetben adott valamilyen adathalmazunk, amelyet többféleképpen, több nézetbol
KDE FEJLESZTÉS 175 szeretnénk megjeleníteni. Miután megjelenítettük az adatokat, a felhasználó módosítani szeretne rajtuk valamelyik - esetleg egymás után több különbözo nézetben. A felhasználó természetes elvárása, hogy ha egy adott nézetben megváltoztatott valamit, mind az adatha lmaz, mind a többi nézet a változtatásnak megfeleloen frissüljön. Példa. Egy file-ban mérési adatok állnak rendelkezésünkre, feladatunk, hogy megjelenítsük azokat táblázatos, hisztogram, és kördiagram formátumban. Ha a táblázatban egy mérési adatot módosítunk, szeretnénk, hogy a változás a hisztogram és a kördiagram nézetben is látszódjon. Erre kínál egy általános megoldást a Dokumentum/Nézet (Document/View) architektúra. Az adatok egyes megjelenítési formáját Nézetnek (View) nevezzük. Az adatokat a Dokumentum (Document) tárolja. Amennyiben egy Nézetben megváltoztatunk valamit, a Nézet megváltoztatja a Dokumentumban a megfelelo adatokat. Ezek után a Dokumentum értesíti az összes Nézetet, hogy valami változás történt. Erre az értesített Nézetek lekérdezik a Dokumentumot a megváltozott adatról. Általában jelen van egy Alkalmazás (Application) szereplo is, amely inicializálja az alkalmazást, feldolgozza a parancssori argumentumokat, felépíti a Dokumentumot, létrehozza a Nézeteket és hozzákapcsolja azokat a Dokumentumhoz. A fentiekbol következik, hogy a Dokumentumnak tudnia kell azokról a Nézetekrol, amelyeket értesítenie kell egy esetleges változás esetén. Ez implementációs szinten C++ nyelven legtöbbször úgy jelenik meg, hogy a Dokumentum tartalmaz egy listát, amelyben az értesítendo Nézetekre tárol pointereket. Így egy Nézet „feliratkozhat” erre a listára vagy lekapcsolódhat róla, a Dokumentum pedig bejárhatja ezt a listát, és értesítést küldhet a Nézet típusú listaelemek egy megadott tagfüggvényeinek meghívásával. Természetesen a Nézeteknek is tudnia kell az általuk megjelenített Dokumentumról. Ezt egy architektúrán kívüli osztályban, például az Alkalmazásban tárolhatjuk, és a Nézet közvetlenül az Alkalmazástól kérdezi le a Dokumentumot. Ha azonban több Dokumentum is létezik egy programon belül, akkor ez nem megfelelo megoldás, ilyenkor a legcélszerubb, ha a Nézet külön eltárol egy pointert a hozzá tartozó Dokumentumra. A KDevelop Wizardja kétféle Dokumentum/Nézet alapú kódot generál Single Document Interface illetve Multiple Document Interface alkalmazást. Az SDI alkalmazás egy Dokumentumot tartalmazhat (egy file-t jeleníthet meg), az MDI alkalmazás többet. Vizsgáljuk meg eloször az SDI alkalmazáshoz generált kódot!
10.9.2 A Dokumentum/Nézet architektúra a KDevelop által generált SDI kódban Tételezzük fel, hogy az alkalmazásunk neve SDI. Akkor a következo osztályok keletkeztek: ?? SDIApp (Alkalmazás) ?? SDIDoc (Dokumentum) ?? SDIView (Nézet)
KDE FEJLESZTÉS 176 10.9.2.1 Az Alkalmazás szerepe Az Alkalmazás osztály alapvetoen az alkalmazás foablakát zárja egységbe. A foablak feladata, hogy az egyes felhasználói felület elemeket (menü, státuszsor, eszközsor, ablakok) összefogja. Ezért az Alkalmazás egyes tagfüggvényei azok a szlotok, amelyeket a menüpontok által küldött szignálok értesítenek. Például egy a file megnyitását végzo generált kód: void SDIApp::slotFileOpen() { slotStatusMsg(i18n("Opening file...")); if(!doc->saveModified()) { // here saving wasn't successful } else { KURL url=KFileDialog::getOpenURL(QString::null, i18n("*|All files"), this, i18n("Open File...")); if(!url.isEmpty()) { doc->openDocument(url); setCaption(url.fileName(), false); fileOpenRecent->addURL( url ); } } slotStatusMsg(i18n("Ready.")); }
Az Alkalmazás osztály különbözo a programra jellemzo beállítások elmentésére illetve betöltésére is képes a config objektumon keresztül. Az elmentést a void SDIApp::saveOptions() { config->setGroup("General Options"); config->writeEntry("Geometry", size()); config->writeEntry("Show Toolbar", viewToolBar->isChecked()); config->writeEntry("Show Statusbar",viewStatusBar->isChecked()); config->writeEntry("ToolBarPos", (int) toolBar("mainToolBar")>barPos()); fileOpenRecent->saveEntries(config,"Recent Files"); }
függvény végzi, a betöltést void SDIApp::readOptions() { config->setGroup("General Options"); // bar status settings bool bViewToolbar = config->readBoolEntry("Show Toolbar", true); viewToolBar->setChecked(bViewToolbar); slotViewToolBar(); bool bViewStatusbar = config->readBoolEntry("Show Statusbar", true);
KDE FEJLESZTÉS 177 viewStatusBar->setChecked(bViewStatusbar); slotViewStatusBar();
// bar position settings KToolBar::BarPosition toolBarPos; toolBarPos=(KToolBar::BarPosition)config->readNumEntry( "ToolBarPos", KToolBar::Top); toolBar("mainToolBar")->setBarPos(toolBarPos); // initialize the recent file list fileOpenRecent->loadEntries(config,"Recent Files"); QSize size=config->readSizeEntry("Geometry"); if(!size.isEmpty()) { resize(size); } }
függvényben oldották meg. Saját beállításainkat is elmenthetjük, illetve betölthetjük. Az elmentés: config->setGroup("My Options"); config->writeEntry("NumberToRemember",42);
A betöltés: int x; config->setGroup("My Options"); x=config->readNumEntry("NumberToRemember");
Figyeljünk arra, hogy a setGroup() függvénnyel nehogy átállítsuk az aktuális szekciót, úgy, hogy a soron következo függvények még más aktuális szekcióra számítanak. Az alkalmazás osztály konstruktorában létrehozza a felhasználói felület elemeit és felépíti a Dokumentum/Nézet architektúrát. A Dokumentum a void SDIApp::initDocument() { doc = new SDIDoc(this); doc->newDocument(); }
függvénnyel jön létre, az Alkalmazás objektum doc változója tárolja el az egyetlen dokumentumot. A fenti pointer felszabadítása érdekében az Alkalmazás destruktorában helyezzük el a delete doc;
sort. A Nézetet a void SDIApp::initView() {
KDE FEJLESZTÉS 178
view = new SDIView(this); doc->addView(view); setCentralWidget(view); setCaption(doc->URL().fileName(),false); }
függvény hozza létre. Láthatóan a generált kód csak egy Nézetet támogat5 , azt hozzácsatolja a Dokumentumhoz. A Dokumentumban ilyenkor a Nézetre mutató pointer egy listában tárolódik, amely automatikusan törli, ha a lista megszunik, vagy ha az elemet a listából töröljük, amit a Dokumentum void SDIDoc::removeView(SDIView *view);
függvényével érhetünk el. Ezek után meghívódik az Alkalmazás objektum void SDIApp::openDocumentFile(const KURL& url) { slotStatusMsg(i18n("Opening file...")); doc->openDocument( url); fileOpenRecent->addURL( url ); slotStatusMsg(i18n("Ready.")); }
függvénye. Láthatólag a megnyitandó file átadódik a Dokumentumnak, illetve neve hozzáadódik a legutóbb használt file-ok listájához. Innen beindult a Dokumentum/Nézet architektúra muködése. Ez után az Alkalmazás objektum funkciója lényegileg annyi, hogy értesítse a Dokumentumot a menüpontoktól és az eszközsor gombjaitól érkezo üzenetekrol. 10.9.2.2 A Dokumentum feladatai Elsoként vizsgáljuk meg az értesítés mechanizmusát! A Dokumentum konstruktorában létrejön egy, a Nézeteket tároló lista, és beállítjuk a Nézetek automatikus felszabadítását: if(!pViewList) { pViewList = new QList<SDIView>(); } pViewList->setAutoDelete(true);
Ennek törlésére manuálisan adjuk hozzá a törlést a destruktorhoz: delete pViewList;
5
Ha több nézetet is szeretnénk támogatni, azt sajnos csak a generált kód komolyabb módosításával tudjuk megtenni.
KDE FEJLESZTÉS 179
Az összes Nézet értesítését a void SDIDoc::slotUpdateAllViews(SDIView *sender) { SDIView *w; if(pViewList) { for(w=pViewList->first(); w!=0; w=pViewList->next()) { if(w!=sender) w->updateView(); } } }
függvény végzi. A generált kóddal ellentétben nem a repaint() tagfüggvényt hívjuk meg, mert az a Nézettol csak az ablak újrafestését követeli meg. Ugyanakkor ha minden újrafestéskor lekérdezzük a Dokumentum adatait, az fölösleges pazarlás, mert az újrafestés az egyik leggyakoribb muvelet. Ennek következtében a Nézetben manuálisan létre kell majd hoznunk az updateView() függvényt, ami tényleg csak akkor hívódik meg, amikor le kell kérdeznünk a Dokumentumot.. Ha módosítjuk a Dokumentumot, akkor a fenti függvény meghívásán kívül hívjuk meg a setModified(bool _m=true)
függvényt! Ha mi is kíváncsiak vagyunk arra, hogy történt-e módosítás, akkor azt a bool isModified()
függvénnyel kérdezhetjük le. Ezután nézzük meg, hogyan „kelthetjük életre” a Dokumentumot, vagyis milyen függvényeket kell implementálnunk ahhoz, hogy a Dokumentum teljes legyen! A továbbiak illusztrálásához felhasználjuk a fenti példát. Úgy döntünk, hogy mérési eredményeinket egy double értékeket tartalmazó listában tároljuk. Adjuk tehát hozzá a Dokumentumhoz a következo tagváltozót: #include QValueList<double> m_data;
A fenti Qt programkönyvtárból származó QValueList egy láncolt lista implementáció, nagyon hasonlít a C++ szabványos programkönyvtárában (Standard Template Library) található listára (list). A value szó arra utal, hogy a lista érték szerint tárolja el a benne elhelyezett típusokat, vagyis lemásolja azokat, és nem referenciákat vagy pointereket tartalmaz. Mivel a Dokumentumban a megjelenítendo adatokat tároljuk, ezért azokhoz kapcsolódóan a következo muveletekre van szükségünk: ?? Inicializálás
KDE FEJLESZTÉS 180 ?? Felszabadítás ?? Betöltés ?? Kimentés Az inicializálást a generált bool SDIDoc::newDocument() { ///////////////////////////////////////////////// // TODO: Add your document initialization code here ///////////////////////////////////////////////// modified=false; doc_url.setFileName(i18n("Untitled")); return true; }
függvény implementálásával végezhetjük, inicializálásra, mert az alapértelmezett lista üres.
példánkban
nincs
szükségünk
Az adatokat a void SDIDoc::deleteContents() { m_data.clear(); }
függvényben szabadíthatjuk fel. Példánkban egyszeruen kiürítjük a listát. Az adatok file-ból való betöltését a KDE környezet szellemében a felhasználó számára a hálózat szempontjából átlátszó kell, hogy legyen, vagyis a felhasználó ugyanúgy megnyithasson távoli file-okat, mint a helyi merevlemezen találhatókat. Ezt úgy oldjuk meg, hogy amennyiben távoli file-ról van szó, úgy letöltjük azt egy ideiglenes file-ba, megnyitjuk, majd letöröljük az ideiglenes file-t. Utána értesítjük a Nézeteket, hogy megváltozott a Dokumentum tartalma. Mindezt a Dokumentum generált openDocument függvényében tesszük meg: bool SDIDoc::openDocument(const KURL& url, const char *format /*=0*/) { QString tmpfilename; if(!url.isLocalFile()) { if(!KIO::NetAccess::download( url, tmpfilename )) { KNotifyClient::event("cannotopenfile"); return false; } } else { tmpfilename=url.path(); }
KDE FEJLESZTÉS 181 QFile f(tmpfilename); if ( !f.open( IO_ReadOnly ) ) { SDIApp *pParent=(SDIApp *) parent(); KMessageBox::error(pParent,"Cannot open input file.","Error"); return false; } // This could be QTextStream in case of text data QDataStream stream(&f); double d;
while(!stream.atEnd()) { stream>>d; m_data.append(d); }
f.close();
if(!url.isLocalFile()) { KIO::NetAccess::removeTempFile( tmpfilename ); } modified=false; slotUpdateAllViews(NULL); return true; }
Az elmentést hasonlóan végezzük, csak a saveDocument generált függvényt implementáljuk: #include "knotifyclient.h" #include "ktempfile.h" bool SDIDoc::saveDocument(const KURL& url, const char *format /*=0*/) { QString tmpfilename; if(!url.isLocalFile()) { KTempFile tempFile; tmpfilename=tempFile.name(); } else { tmpfilename=url.path(); } QFile f(tmpfilename);
KDE FEJLESZTÉS 182 if(!f.open(IO_WriteOnly|IO_Truncate)) { KNotifyClient::event("cannotopenfile"); return false; } QDataStream stream(&f);
QValueList<double>::iterator it; for(it=m_data.begin();it!=m_data.end();it++) { stream<<*it; }
f.close(); if(!url.isLocalFile()) { if(!KIO::NetAccess::upload(tmpfilename,url)) { KNotifyClient::event("cannotopenfile"); return false; } KIO::NetAccess::removeTempFile( tmpfilename ); } modified=false; return true; }
10.9.2.3 A Nézet funkciói A Nézet implementálásához tekintsük ismét a mérési adatokat megjeleníto példát! Ebben a fejezetben egy hisztogramot kirajzoló Nézetet használunk példaként. Ha sok mérési adatot jelenítünk meg, akkor sok téglalapot kell kirajzolnunk, és ezért a megjelenítés közben az ablak többször is felvillanhat. Erre a problémára egy széles körben alkalmazott és támogatott megoldás a virtuális ablakok módszere . A technika lényege, hogy létrehozunk egy képernyovel kompatibilis felépítésu memóriaterületet, arra rajzolunk, majd a megjelenítéskor a memóriaterületet egy muvelettel a képernyore másoljuk. Ha nem alkalmazzuk a virtuális ablakok módszerét, akkor a rajzolás a Nézet paintEvent() függvényében történne: void SDIView::paintEvent( QPaintEvent * ) { QPainter painter; painter.begin(this); // Rajzolás a painter tagfüggvényeivel... painter.end(); }
KDE FEJLESZTÉS 183
A paintEvent függvény akkor hívódik meg, mikor az ablakot újra kell festeni. Közvetlenül sose hívjuk ezt a függvény, helyette az update() vagy sürgosebb esetekben a repaint() tagfüggvényt használjuk, ami végül a paintEvent meghívását eredményezi. Ehelyett azonban mi egy virtuális képernyore szeretnénk írni, ami lényegében egy memóriaterület. Ez a memóriabeli ablak a Qt programkönyvtárban a QPixmap osztály. Hozzunk is létre egy virtuális buffert a Nézet osztályban: #include QPixmap m_ScreenBuffer;
Erre az eredeti (képernyore) rajzoláshoz nagyon hasonlóan rajzolunk: QPainter painter; painter.begin(&m_ScreenBuffer); // Rajzolás a painter tagfüggvényeivel... painter.end();
Majd a QPixmap-ot a képernyore másoljuk, valahányszor az ablakot újra kell rajzolni. Erre szolgál a bitBlt függvény: void SDIView::paintEvent(QPaintEvent *e) { QWidget::paintEvent( e ); QRect r = e->rect(); // Copying the screen buffer to the screen bitBlt( this, r.x(), r.y(), &m_ScreenBuffer, r.x(), r.y(), r.width(), r.height() ); }
A példánkat illetoen már csak egy kérdés marad: Mikor rajzoljunk a virtuális ablakra? Ezt két helyen kell megtennünk: ?? Mikor a Dokumentum tartalma megváltozik ?? Mikor az ablak újraméretezodik A Dokumentum megváltozását az updateView() függvény meghívása jelzi, vagyis itt kell kirajzolnunk a hisztogramot: void SDIView::updateView() { // Erasing screen buffer and resetting its size m_ScreenBuffer.resize(width(),height()); m_ScreenBuffer.fill(Qt::white );
QValueList<double> data=getDocument()->getData(); QValueList<double>::iterator it;
KDE FEJLESZTÉS 184
double min, max; min=max=*data.begin(); for(it=data.begin();it!=data.end();it++) { min=(min<*it)?min:*it; max=(max>*it)?max:*it; } double range; if(min*max>0) { range=(min<0)?min:max; } else { range=fabs(min)+max; }
#define VALUE_WIDTH 20 int xsize=data.size()*VALUE_WIDTH; int xoffset=(m_ScreenBuffer.width()-xsize)/2; #define YMARGIN 4 int ysize=m_ScreenBuffer.height()-2*YMARGIN; double dy= range/ysize;
QPainter painter; QPen pen(black,1); painter.begin(&m_ScreenBuffer); painter.setPen(pen); int yzeroline=(max>0)?max/dy:YMARGIN; // Histogram color QColor c(0,255,255); int i=0;
for(it=data.begin();it!=data.end();it++,i++) { painter.fillRect(xoffset+i*VALUE_WIDTH,yzeroline,VALUE_WIDTH,1*(int)(*it/dy),c); painter.drawRect(xoffset+i*VALUE_WIDTH,yzeroline,VALUE_WIDTH,1*(int)(*it/dy)); } #define LINE_OFFSET 2 painter.drawLine(xoffset-LINE_OFFSET, yzeroline, m_ScreenBuffer.width()-xoffset+LINE_OFFSET,yzeroline);
KDE FEJLESZTÉS 185 painter.end(); //Update screen repaint(); }
Ezek után kezeljük az ablak átméretezését! Elso megoldásunk egy új, ideiglenes ablakot hoz létre az új ablak méretével, annak bal felso sarkába másolja a régit, majd a régit felülírja az ideiglenessel. Átméretezés esetén a resizeEvent függvény hívódik meg: void SDIView::resizeEvent(QResizeEvent*e) { QWidget::resizeEvent( e ); //
This is a resize which aligns topleft. int w = width() > m_ScreenBuffer.width() ? width() : m_ScreenBuffer.width(); int h = height() > m_ScreenBuffer.height() ? height() : m_ScreenBuffer.height(); QPixmap tmp( m_ScreenBuffer ); m_ScreenBuffer.resize( w, h );
m_ScreenBuffer.fill( Qt::white ); bitBlt(&m_ScreenBuffer, 0, 0, &tmp, 0, 0, tmp.width(), tmp.height() ); }
Példánkban ez nem túl elegáns, ugyanis a hisztogramot középre rendeztük, és a balra fel igazítás elrontja ezt a képet. Ezért – mivel az újraméretezés nem gyakori muvelet – példánkban az egyszeruség kedvéért meghívjuk az updateView() függvényt. Amennyiben tovább szeretnénk optimalizálni, a fenti kód módosításával elkerülhetjük a Dokumentum lekérdezését, azonban a csak nagyobb adatok lekérdezése esetén lesz jövedelmezobb, mint egy ideiglenes QPixmap bevezetése és másolása. Így a példa resizeEvent függvénye: void SDIView::resizeEvent(QResizeEvent*e) { QWidget::resizeEvent( e ); // Now we call updateView updateView(); }
A fentiekbol jól látszik, hogy ha nem lenne támogatás a Qt részérol a virtuális ablakok módszeréhez, miszerint ugyanazokkal a függvényekkel írhatunk a virtuális ablakra, mint a képernyore, nagyon ne héz volna az implementáció. A QPainter osztály egyparaméteru konstruktorára bízhatjuk a begin() függvény meghívását, illetve a destruktorra az end() hívását. Így a legelso paintEvent() implementációval azonos a következo:
KDE FEJLESZTÉS 186 void SDIView::paintEvent( QPaintEvent * ) { // Az egyparaméteru konstruktor meghívja a begin()-t. QPainter painter(this); // Rajzolás a painter tagfüggvényeivel... // A függvény elhagyásakor a destruktor meghívja az end()-et }
A fentebb bemutatott módszer elonye, hogy a konstruktor nem tud hibaüzenettel visszatérni, míg a begin() függvény igen. Ezért külso eszközök (például nyomtató) esetén ne használjuk az egyparaméteru konstruktor lehetoségeit.
10.9.3 A Dokumentum/Nézet architektúra a KDevelop által generált MDI kódban