Programming | UNIX-Linux » Bányász Gábor - Linux programozás

Datasheet

Year, pagecount:2002, 186 page(s)

Language:Hungarian

Downloads:1373

Uploaded:February 27, 2006

Size:599 KB

Institution:
-

Comments:

Attachment:-

Download in PDF:Please log in!



Comments

No comments yet. You can be the first!


Content extract

LINUX PROG RAM OZÁS v0.3pre 2002.1202 22:54 Bányász Gábor ( banyasz.gabor@autbmehu ) Levendovszky Tihamér ( levendovszky.tihamer@autbmehu ) 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.31 Linux Documentation Project (LDP).8 1.32 Linux Software Map (LSM) .9 1.33 További információforrások.9 2. A LINUX KERNEL RÉSZLETEI I. 10 2.1 M EMÓRIA MENEDZSMENT.10 2.11 A Virtuális Memória modell. 11 2.111 2.112 2.113 2.114 2.115 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.12 Cache-ek . 14 2.2 PROCESSZEK.15 2.21 A Linux processzek. 16 2.22 Azonosítók . 17 2.23 Ütemezés . 18 2.24 Processzek létrehozása. 19 2.25 Ido és idozítok . 20 2.26 A programok futtatása. 20 3. FEJLESZTOI ESZKÖZÖK. 22 3.1 3.11

3.12 3.13 3.14 3.2 3.21 3.22 3.3 3.31 3.32 3.33 3.34 3.35 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.11 Példa. 35 4.12 A gdb indítása. 36 4.13 Breakpoint, watchpoint, catchpoint . 36 4.14 xxgdb . 38 4.15 DDD . 39 4.16 KDevelop internal debugger. 39 4.2 M EMÓRIAKEZELÉSI HIBÁK.39 4.21 Electric Fence. 41 4.211 4.212 4.213 Az Electric Fence használata.41 Memory Alignment kapcsoló .43 Az eléírás .43 TARTALOMJEGYZÉK 3 4.214 4.215 4.22 További lehetoségek .44 Eroforrás igények.44 NJAMD (Not Just Another Malloc Debugger). 45 4.221 4.222 4.223 4.224 Használata.45 Memory leak detektálás .47 Az NJAMD kapcsolói.47 Összegzés.48 4.23 mpr. 48 4.24 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.11 5.12 5.13 5.14 5.15 5.16 5.17 5.2 5.21 5.22 5.23 5.24 5.25 5.3 5.31 5.32 5.33 5.34 5.35 5.4 5.5 5.51 5.52 5.53 5.54 5.55 5.56 5.6 5.61 5.62 5.7 5.71 5.72 5.8 5.81 5.82 5.83 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.11 Jogosultságok, azonosítók és jellemzok. 81 6.12 A folyamat állapotai. 82 6.13 Folyamatok létrehozása és megszüntetése. 82 6.14 Folyamatok közötti kommunikáció (IPC). 86 6.141 6.15 6.16 Folyamatok szinkronizációja .87 Üzenetsorok (Message Queues). 92 Megosztott memória (Shared Memory). 98 TARTALOMJEGYZÉK 4 6.17 Jelzések (Signals) .102 6.171 6.172 6.173 6.174 6.175 6.176 6.177 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.21 Szálak létrehozása.107 6.22 Kölcsönös kizárás (Mutex) .112 6.23 Feltételes változók (Condition Variable) .113 6.24 Szemaforok .115 6.25 Core-dump mechanizmus adaptálása.116 7. KÖNYVTÁRAK FEJLESZTÉS E.118 7.1 7.2 7.3 7.31 7.32 7.33 7.4 7.5 7.51 7.52 7.53 7.54 7.55 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.11 8.12 8.13 8.14 8.15 8.2 8.21 8.22 8.23 8.24 8.3 8.31 8.32 8.33 8.34 8.35 8.36 8.37 8.38 8.4 8.41 8.42 8.43 8.44 8.45 8.46 8.47 8.48 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.11 X Window architektúra.148 9.12 Window manager.148 9.13 Kliens applikációk.149 9.14 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.21 Egyszeru applikáció.153 10.3 A

SIGNAL-SLOT MODELL .157 10.31 Slot létrehozása.158 10.32 Signal küldése.158 10.33 Signal és slot összekapcsolása.158 10.34 Signal-slot metódus paraméterekkel.160 10.35 Slot átmeneti objektumokban.160 10.4 M ETA OBJECT COMPILER (MOC) .161 10.5 EGYSZERU SZÁRMAZTATOTT WIDGET.161 10.51 A QWidget.162 10.52 Signal-slot kezelés.162 10.53 A widget megrajzolása.162 10.531 10.532 A rajzolás kiváltása.163 A rajzolás .163 10.54 A felhasználói esemény kezelése .163 10.6 DIALÓGUS ABLAKOK .164 10.61 Standard dialógus ablakok.164 10.611 10.612 10.613 10.614 10.615 10.616 10.62 10.621 10.622 10.63 10.631 10.632 10.633 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.64 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.91 A Dokumentum/Nézet Architektúra alapkoncepciói.174 10.92 A Dokumentum/Nézet architektúra a KDevelop által generált SDI kódban .175 10.921 10.922 10.923 10.93 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.31 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.tldporg/ 1.32 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.execpccom/ 1.33 További információforrások ?? ?? ?? ?? ?? ?? Linux system labs: http://www.lslcom/ RedHat dokumentumok: http://www.redhatcom/docs/ Linoleum (a fejlesztésekhez): http://leapster.org/linoleum/ Linux.hu: http://wwwlinuxhu/ 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.11 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.111 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.112 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.113 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.114 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.115 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.12 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.21 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.22 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.23 Ü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.24 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.25 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.26 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 aout 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.11 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.12 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.13 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ó <ctrl>- g <ctrl>- x <ctrl>-o <ctrl>-j <ctrl>-r <ctrl>-w <ctrl>- y <ctrl>- v <ctrl>-k <ctrl>- u <ctrl>-c <ctrl>-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.14 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 <ctrl>-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 .soxyz .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.21 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.22 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 73 296-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 "); 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 aout 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 ", 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 sincc Í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 sinco 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 myliba 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 sinco 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: secondo sinco ?? 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.31 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.32 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: fooc defsh 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 fooc illetve a fooh állományok. Mindez azt jelenti, hogy a fooo file-t akkor kell újrafordítani, ha ?? a foo.o file nem létezik, ?? a foo.c idobélyege korábbi, mint a fooo idobélyege, ?? a defs.h idobélyege korábbi, mint a fooo 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 defsh 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.33 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 secondo thirdo lesz, azonban ez nem így van. Valójában az értéke $(OBJS) thirdo 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.34 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.35 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: .co: $(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.11 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.12 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.13 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.14 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.15 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.16 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 ", dyn); free(dyn); // 2. felszabaditott terulet hasznalata strcpy(dyn, "12345"); printf("2: %s ", dyn); // 3. tuliras (nagyon) dyn=malloc(5); strcpy(dyn, "12345678"); printf("3: %s ", dyn); // 4. ele iras *(dyn-1)=; printf("4: %s ", dyn); // memory leak !!! // 5. lokalis valtozo tuliras strcpy(local, "12345"); printf("5: %s ", local); // 6. lokalis valtozo eleiras local[-1]=; printf("6: %s ", local); // 7. globalis valtozo tuliras strcpy(global, "12345"); printf("7: %s ", global);

// 8. globalis valtozo eleiras global[-1]=; printf("8: %s ", 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.ibiblioorg/pub/Linux/devel/lang/c/ 4.21 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.211 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 libefencea vagy libefenceso 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.so00 vagy LD PRELOAD=libefence.so00 /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.20 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.so00 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.20 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.so6 DEBUG 43 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so6 #1 0x080484fc in main () at buggy.c:23 #2 0x42017589 in libc start main () from /lib/i686/libc.so6 (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.212 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.20 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.so6 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so6 #1 0x080485f8 in main () at buggy.c:18 #2 0x42017589 in libc start main () from /lib/i686/libc.so6 (gdb) Így már megtalálta a 18. sorban található kis túlírásunkat is 4.213 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.20 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)=; (gdb) 4.214 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.215 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.22 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.221 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.so6 (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.so6 (gdb) where #0 0x420807a6 in strcpy () from /lib/i686/libc.so6 #1 0x080484c8 in main () at buggy.c:18 #2 0x42017589 in libc start main () from /lib/i686/libc.so6 (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)=; (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.so6 (gdb) where #0 0x42029331 in kill () from /lib/i686/libc.so6 #1 0x4202911a in raise () from /lib/i686/libc.so6 #2 0x40029b12 in nj free init () from /usr/lib/libnjamd.so0 #3 0x40029ff1 in nj sunderflow free () from /usr/lib/libnjamd.so0 #4 0x4002ce2c in free () from /usr/lib/libnjamd.so0 #5 0x08048609 in main () at buggy.c:20 #6 0x42017589 in libc start main () from /lib/i686/libc.so6 (gdb) DEBUG 47 4.222 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.223 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.224 Ö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.23 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.<pid>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 log27391gz 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.27391gz 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.27391gz | 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.24 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.11 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.12

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.13 Á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 <fcntl.h> 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.14 Á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 <unistd.h> Á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.15 Í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 <unistd.h> 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> <fcntl.h> <stdio.h> <stdlib.h> <unistd.h> int main(void) { int fd; /* open the file, creating it if its 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! ", 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 <unistd.h> 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.16 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 <unistd.h> /*

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.17 Á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 <unistd.h> 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.21 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 <unistd.h> 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.22 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 <unistd.h> 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.23 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.24 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 <unistd.h> 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.25 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 <utime.h> 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.31 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 <fcntl.h> #include <unistd.h> 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.32 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 <unistd.h> 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.33 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 <unistd.h> 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 <unistd.h> 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 ‘’ 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.34 Á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 <unistd.h> int unlink(const char *pathname); ÁLLOMÁNY ÉS I/O KEZELÉS 64 5.35 Á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 <unistd.h> 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.51 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 <unistd.h> 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 ‘’ 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.52 Könyvtárváltás Két rendszerhívás áll rendelkezésünkre a könyvtárváltáshoz: #include <unistd.h> 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.53 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 <unistd.h> 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.54 Könyvtár létrehozása Új könyvtárat az alábbi rendszerhívással hozhatunk létre: #include <unistd.h> 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.55 Könyvtár törlése Könyvtárat letörölni a következo egyszeru rendszerhívással tudunk : #include <unistd.h> 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.56 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 <dirent.h> 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 <dirent.h> 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 <dirent.h> #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 <fcntl.h> #include <stdio.h> #include <unistd.h> 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 "); return 0; } buf[i] = ; 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.61 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> <fcntl.h> <stdio.h> <unistd.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] = ; 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.62 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 <unistd.h> 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 <fcntl.h> <stdio.h> <sys/time.h> <sys/types.h> <unistd.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 were 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, dont try to read

from it again */ close(fds[i]); FD CLR(fds[i], &watchset); } else { buf[rc] = ; 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.71 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.72 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 <fcntl.h> 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> <fcntl.h> <stdio.h> <unistd.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]; /* well 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); /* theres a chance the lock was freed between the F SETLK and F GETLK; make sure theres still a conflict before complaining about it */ if (lockinfo.l type != F UNLCK) { sprintf(message, "conflict with process %d. press " "<return> 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 "); getlock(fd, F RDLCK); printf("got read lock "); waitforuser(" press <return> to continue:"); printf("releasing lock "); getlock(fd, F UNLCK); printf("getting write lock "); getlock(fd, F WRLCK); printf("got write lock "); waitforuser(" press <return> 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.81 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> <fcntl.h> <termios.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 dont 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 dont 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; /* / 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; /* / 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; /* / /* 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 ", buf, res); if (buf[0]==z) STOP=TRUE; } /* restore the old port settings / tcsetattr(fd,TCSANOW,&oldtio); } 5.82 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> <fcntl.h> <termios.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 ", buf, res); if (buf[0]==z) STOP=TRUE; ÁLLOMÁNY ÉS I/O KEZELÉS 80 } tcsetattr(fd,TCSANOW,&oldtio); } 5.83 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.11 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.12 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.13 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 <unistd.h> 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 <unistd.h> 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 <unistd.h> 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<unistd.h> int main() { int pid; printf("Starting program. "); pid=fork(); if(pid<0) { printf("Forking error. "); exit(-1); } if(pid==0) { /* A gyermek folyamat - pid 0 / printf("My name is Child. James Child My PID: %d

Fork returned: %d ",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 ",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 <unistd.h> 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<unistd.h> int child function(); int parent function(int child id); int child function() { printf("My name is Child. James Child My PID: %d ",getpid()); printf("My Parents PID: %d ",getppid()); exit(0); } int parent function(int child id) { printf("My name is Parent. James Parent My PID: %d ",getpid()); printf("My childs ID is: %d ",child id); exit(0); } int main() { int pid; printf("Starting program. "); pid=fork(); KONKURENS PROGRAMOZÁS 86 if(pid<0) { printf("Forking error. "); 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 Parents PID: 3690 My childs 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.14 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> <IPC ID> 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.141 Folyamatok szinkronizációja Nézzük eloször a szinkronizáció lehetoségeit! 6.1411 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.1412 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! "); exit(1); }

printf("Semaphore with id #%d has been created succesfully ",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.15 Ü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. ",key); return 1; } printf("Course number #%d is available for registration. ",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: "); printf("Name: %s ",pmsg->data.name); printf("Address: %s ",pmsg->data.address); printf("Social Security#: %s ",pmsg->data.ssnumber); printf("Remarks: %s ",pmsg->data.remark); } /* Uzenetsor torlese / msgctl(mqid, IPC RMID, 0); printf("Registration is closed. "); 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 <course key> "); 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. ",key); return 1; } printf("Name: "); scanf("%s",msg.dataname); printf("Address: "); scanf("%s",msg.dataaddress); printf("Social Security#: "); scanf("%s",msg.datassnumber); printf("Remarks: "); scanf("%s",msg.dataremark); /* Uzenetkuldes / printf("Registering student . "); 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. "); return -1; } printf("Student has been registered successfully. "); return 0; } 6.16 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 "); exit(1); } /* A megosztott memoria felcsatolasa / if((shmptr=shmat(shmid,0,0))==(char*)-1) { fprintf(stderr,"Error attaching shared memory resource "); exit(1); } printf("Writing data to memory. "); for(c=0;c<9;c++) { shmptr[c]=1+c; /*123456789/ } shmptr[9]=; printf("Press <Enter> to continue "); 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 "); exit(1); } /* A megosztott memoria felcsatolasa / shmptr=shmat(shmid,0,0); if(shmptr==(char*)-1) { fprintf(stderr,"Error attaching shared memory resource "); exit(1); } printf("Shared memory data: %s ",shmptr); /* A megosztott memoria lecsatolasa / shmdt(shmptr); } 6.17 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.171 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.172 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 signalh á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.173 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.174 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.175 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.176 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 <unistd.h> 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.177 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.356 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.21 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 <pthread.h> 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 <pthread.h> #include <stdlib.h> #include <stdio.h> void *thread function(void arg) { int i; printf("Thread started. "); for ( i=1; i<=20; i++ ) { printf("%d. Hello thread world! ",i); sleep(1); } printf("Thread exiting. "); return NULL; } int main(void) { pthread t mythread; if ( pthread create( &mythread, NULL, thread function, NULL) ) { fprintf(stderr,"Error creating thread. "); exit(1); } printf("Main thread exiting. "); 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. "); 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. "); 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 <pthread.h> 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. "); exit(1); } if ( pthread join ( mythread, NULL ) ) { fprintf(stderr,"Error joining thread. "); exit(1); }

KONKURENS PROGRAMOZÁS 110 printf("Main thread exiting. "); 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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.22 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.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> int pthread mutex unlock(pthread mutex t *mutex); függvénnyel engedhetjük el. 6.23 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.h> pthread cond t cond = PTHREAD COND INITIALIZER; Egyébként pedig a #include <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 <pthread.h> 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 = nowtv sec + 10; timeout.tv nsec = nowtv usec * 1000; 6.24 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.25 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 24-es kernel verzióknál azonban már foglalkoztak a problémával. A 246-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. "); } 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 .soxyz .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:slmatho: 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 file1o file2o példájára történhet, ahol a fenti sor létrehozza a file1.o és a file2o tárgykódú fileokból a my librarya 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.31 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.32 Megosztott programkönyvtárak létrehozása Az alábbi példa az a.c és a bc könyvtárakból létrehoz egy libmystuffso1 sonevu, libmystuff.so101 fizikai nevu könyvtárat gcc -fPIC -g -c -Wall a.c gcc -fPIC -g -c -Wall b.c gcc -shared -Wl,-soname,libmystuff.so1 -o libmystuff.so101 ao bo -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.33 Megosztott könyvtárak

betöltése A programkönyvtárak betöltését a /lib/ld–linux.soX (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.soconf 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.socache nevu file-ban tárolódnak Az elozo fejezetben említett ldconfig segédprogram beolvassa a /etc/ld.soconf tartalmát, majd felállítja a szimbolikus linkeket, majd azokat az /etc/ld.socache 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.sopreload 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/ldsocache-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 <dlfcn.h> 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 ", (*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.51 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. "); } /*

libhello.h */ void hello(void); 7.52 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 libhelloc # Létrehozzuk a libhello-static.a nevu statikus könyvtárat: ar rcs libhello-static.a libhello-statico # 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 useo # 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.53 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ú libhelloo 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.so0 -o libhello.so00 libhelloo -lc # Most akár bemásolhatnánk a libhello.so00 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.so0 libhelloso # Lefordítjuk a demo use programot: gcc -Wall -g -c demo use.c -o demo useo # 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.54 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 <dlfcn.h> #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, "Couldnt open libhello.so: %s ", 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, "Couldnt find hello: %s ", 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.55 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.253 # 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.11 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.12 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.13 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.14 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.15 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.21 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 ‘’ karakternek zárnia, bár általában így használják. 8.22 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> <unistd.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 "); 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 "); 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.23 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 <unistd.h> 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.24 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.31 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.32 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.6670136 Bcast:1526670255 . 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.000 128.000 192.000 224.000 240.000 Utolsó cím 127.255255255 191.255255255 223.255255255 239.255255255 247.255255255 Azonosító 0 10 110 1110 11110 N 7 14 21 Multicast Fenntartva Állapítsuk meg, hogy a www.autbmehu szerver IP címe milyen osztályba tartozik! $ ping www.autbmehu PING avalon.autbmehu (152667016) from 1526670136 : 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.33 A socket cím megadása A 8.15

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 <netinet/in.h> #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 addrs addr = inet addr("1526670136"); Hasonló funkciót lát el a #include <sys/socket.h> #include <netinet/in.h> #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 <netinet/in.h> #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 <netinet/in.h> #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 <netinet/in.h> #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 <netinet/in.h> #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 <netinet/in.h> #include <arpa/inet.h> in addr t inet netof(struct in addr in); hívás segítségével kaphatjuk meg. 8.34 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 <netdb.h> extern int h errno; struct hostent *gethostbyname(const char name); függvénnyel érhetjük el. Ennek visszatérési értéke: #include <netdb.h> 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 <netdb.h> #include <stdio.h> int main(int argc, char *argv[]) { struct hostent* p host; struct in addr* in; if(argc!=2) { printf("Usage: getipaddr <hostname> "); return -1; } p host=gethostbyname(argv[1]); if(p host!=NULL) { in=(struct in addr *)(p host->h addr); printf("IP address: %s ",inet ntoa(*in)); } else { printf("Host %s not found. ",argv[1]); return -1; } return 0; } Teszteljük is le: $ ./getipaddr wwwautbmehu IP address: 152.667016 $ ./getipaddr wwwthatsnotfunnycom Host www.thatsnotfunnycom 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 <netdb.h> #include <stdio.h> int main(int argc, char *argv[]) { struct hostent* p host; struct in addr addr; if(argc!=2) { printf("Usage: gethost <IP address> "); return -1; } if ((int)(addr.s addr = inet addr(argv[1])) == -1) { printf("IP address %s is not valid ",argv[1]); return -1; } p host=gethostbyaddr((char*)&addr,sizeof(addr),AF INET); if(p host!=NULL) { printf("Server name: %s ",p host->h name); } else { printf("Host %s not found. ",argv[1]); return -1; } return 0; } A programot letesztelve a következo eredmények adódtak: $ ./gethost 152667016 Server name: avalon.autbmehu $ ./gethost 1526670456 IP address 152.6670456 is not valid $ ./gethost 152777034 Host 152.777034 not found

HÁLÓZATI KOMMUNIKÁCIÓ 139 8.35 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.ianaorg/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 <netdb.h> 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 <netdb.h> 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.36 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.37 Szerver applikáció 8.38 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.41 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.42 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.43 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.44 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.45 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.46 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.47 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.48 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 ", msg); return 1; } int main(int argc, char* argv[]) { char* message; if(argc != 2) { fprintf(stderr, "usage: %s <message> ", argv[0]); return -1; } message = argv[1];

if(!printmessage(message)) { fprintf(stderr, "%s: couldnt print your message ", argv[0]); return -1; } HÁLÓZATI KOMMUNIKÁCIÓ 145 printf("Message delivered! "); 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 ", *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 printmsgc 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 <host> <message> ", 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 couldnt print your message ", argv[0], server); return -1; } printf("Message delivered to %s! ", 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.11 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.12 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.13 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.14 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 10 verzió

megjelenése ?? 2000. október 23-án adták ki a 20 stabil verziót ?? 2002. április 3 A 30 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 <qstring.h> #include <kapp.h> #include <klineedit.h> 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 : khellocpp 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.21 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 <kapp.h> #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 <kmainwindow.h> class QLabel; /* * This is a simple KDE application. * * @author David Sweet <dsweet@kde.org> */ 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 <qlabel.h> #include #include #include #include #include #include #include <kstdaccel.h> <kiconloader.h> <kmenubar.h> <kapp.h> <kaction.h> <kstdaction.h> <klocale.h> #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.31 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.32 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.33 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 <qwidget.h> 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 <iostream.h> #include <qpushbutton.h> 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.34 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.35 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 mywindowmoc 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 mywindowo -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.51 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.52 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.53 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.531 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.532 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.54 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.61 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.611 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.612 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.613 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.614 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.615 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 <kapp.h> <kfiledialog.h>

<kfontdialog.h> <kcolordlg.h> <kmessagebox.h> <kurl.h> 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(), qcolorgreen(), qcolorblue()); KMessageBox::error (0, message, "Error: Color selected"); } return 0; } 10.616 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.62 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.621 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.622 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.63 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.631 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 <kcolordlg.h> 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 <kcolordlg.h> 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.632 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.633 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.64 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 <kapp.h> #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.91 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.92 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.921 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 wasnt 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.922 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 <qlist.h> 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 dataend();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.923 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.h> 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(), ry(), &m

ScreenBuffer, rx(), ry(), r.width(), rheight() ); } 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!=dataend();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!=dataend();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.93 A Dokumentum/Nézet architektúra a KDevelop által generált MDI kódban <coming soon>