Linux ​programozás 9789639863293 [PDF]


143 67 4MB

Hungarian Pages 610 Year 2012

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Page 1......Page 1
Page 2......Page 2
Page 3......Page 3
Page 4......Page 4
Page 5......Page 5
Page 18......Page 0
toc......Page 6
foreword......Page 16
Page 19......Page 19
Page 20......Page 20
Page 21......Page 21
Page 22......Page 22
Page 23......Page 23
Page 24......Page 24
Page 25......Page 25
Page 26......Page 26
Page 27......Page 27
Page 28......Page 28
Page 29......Page 29
Page 30......Page 30
Page 31......Page 31
Page 32......Page 32
Page 33......Page 33
Page 34......Page 34
Page 35......Page 35
Page 36......Page 36
Page 37......Page 37
Page 38......Page 38
Page 39......Page 39
Page 40......Page 40
Page 41......Page 41
Page 42......Page 42
Page 43......Page 43
Page 44......Page 44
Page 45......Page 45
Page 46......Page 46
Page 47......Page 47
Page 48......Page 48
Page 49......Page 49
Page 50......Page 50
Page 51......Page 51
Page 52......Page 52
Page 53......Page 53
Page 54......Page 54
Page 55......Page 55
Page 56......Page 56
Page 57......Page 57
Page 58......Page 58
Page 59......Page 59
Page 60......Page 60
Page 61......Page 61
Page 62......Page 62
Page 63......Page 63
Page 64......Page 64
Page 65......Page 65
Page 66......Page 66
Page 67......Page 67
Page 68......Page 68
Page 69......Page 69
Page 70......Page 70
Page 71......Page 71
Page 72......Page 72
Page 73......Page 73
Page 74......Page 74
Page 75......Page 75
Page 76......Page 76
Page 77......Page 77
Page 78......Page 78
Page 79......Page 79
Page 80......Page 80
Page 81......Page 81
Page 82......Page 82
Page 83......Page 83
Page 84......Page 84
Page 85......Page 85
Page 86......Page 86
Page 87......Page 87
Page 88......Page 88
Page 89......Page 89
Page 90......Page 90
Page 91......Page 91
Page 92......Page 92
Page 93......Page 93
Page 94......Page 94
Page 95......Page 95
Page 96......Page 96
Page 97......Page 97
Page 98......Page 98
Page 99......Page 99
Page 100......Page 100
Page 101......Page 101
Page 102......Page 102
Page 103......Page 103
Page 104......Page 104
Page 105......Page 105
Page 106......Page 106
Page 107......Page 107
Page 108......Page 108
Page 109......Page 109
Page 110......Page 110
Page 111......Page 111
Page 112......Page 112
Page 113......Page 113
Page 114......Page 114
Page 115......Page 115
Page 116......Page 116
Page 117......Page 117
Page 118......Page 118
Page 119......Page 119
Page 120......Page 120
Page 121......Page 121
Page 122......Page 122
Page 123......Page 123
Page 124......Page 124
Page 125......Page 125
Page 126......Page 126
Page 127......Page 127
Page 128......Page 128
Page 129......Page 129
Page 130......Page 130
Page 131......Page 131
Page 132......Page 132
Page 133......Page 133
Page 134......Page 134
Page 135......Page 135
Page 136......Page 136
Page 137......Page 137
Page 138......Page 138
Page 139......Page 139
Page 140......Page 140
Page 141......Page 141
Page 142......Page 142
Page 143......Page 143
Page 144......Page 144
Page 145......Page 145
Page 146......Page 146
Page 147......Page 147
Page 148......Page 148
Page 149......Page 149
Page 150......Page 150
Page 151......Page 151
Page 152......Page 152
Page 153......Page 153
Page 154......Page 154
Page 155......Page 155
Page 156......Page 156
Page 157......Page 157
Page 158......Page 158
Page 159......Page 159
Page 160......Page 160
Page 161......Page 161
Page 162......Page 162
Page 163......Page 163
Page 164......Page 164
Page 165......Page 165
Page 166......Page 166
Page 167......Page 167
Page 168......Page 168
Page 169......Page 169
Page 170......Page 170
Page 171......Page 171
Page 172......Page 172
Page 173......Page 173
Page 174......Page 174
Page 175......Page 175
Page 176......Page 176
Page 177......Page 177
Page 178......Page 178
Page 179......Page 179
Page 180......Page 180
Page 181......Page 181
Page 182......Page 182
Page 183......Page 183
Page 184......Page 184
Page 185......Page 185
Page 186......Page 186
Page 187......Page 187
Page 188......Page 188
Page 189......Page 189
Page 190......Page 190
Page 191......Page 191
Page 192......Page 192
Page 193......Page 193
Page 194......Page 194
Page 195......Page 195
Page 196......Page 196
Page 197......Page 197
Page 198......Page 198
Page 199......Page 199
Page 200......Page 200
Page 201......Page 201
Page 202......Page 202
Page 203......Page 203
Page 204......Page 204
Page 205......Page 205
Page 206......Page 206
Page 207......Page 207
Page 208......Page 208
Page 209......Page 209
Page 210......Page 210
Page 211......Page 211
Page 212......Page 212
Page 213......Page 213
Page 214......Page 214
Page 215......Page 215
Page 216......Page 216
Page 217......Page 217
Page 218......Page 218
Page 219......Page 219
Page 220......Page 220
Page 221......Page 221
Page 222......Page 222
Page 223......Page 223
Page 224......Page 224
Page 225......Page 225
Page 226......Page 226
Page 227......Page 227
Page 228......Page 228
Page 229......Page 229
Page 230......Page 230
Page 231......Page 231
Page 232......Page 232
Page 233......Page 233
Page 234......Page 234
Page 235......Page 235
Page 236......Page 236
Page 237......Page 237
Page 238......Page 238
Page 239......Page 239
Page 240......Page 240
Page 241......Page 241
Page 242......Page 242
Page 243......Page 243
Page 244......Page 244
Page 245......Page 245
Page 246......Page 246
Page 247......Page 247
Page 248......Page 248
Page 249......Page 249
Page 250......Page 250
Page 251......Page 251
Page 252......Page 252
Page 253......Page 253
Page 254......Page 254
Page 255......Page 255
Page 256......Page 256
Page 257......Page 257
Page 258......Page 258
Page 259......Page 259
Page 260......Page 260
Page 261......Page 261
Page 262......Page 262
Page 263......Page 263
Page 264......Page 264
Page 265......Page 265
Page 266......Page 266
Page 267......Page 267
Page 268......Page 268
Page 269......Page 269
Page 270......Page 270
Page 271......Page 271
Page 272......Page 272
Page 273......Page 273
Page 274......Page 274
Page 275......Page 275
Page 276......Page 276
Page 277......Page 277
Page 278......Page 278
Page 279......Page 279
Page 280......Page 280
Page 281......Page 281
Page 282......Page 282
Page 283......Page 283
Page 284......Page 284
Page 285......Page 285
Page 286......Page 286
Page 287......Page 287
Page 288......Page 288
Page 289......Page 289
Page 290......Page 290
Page 291......Page 291
Page 292......Page 292
Page 293......Page 293
Page 294......Page 294
Page 295......Page 295
Page 296......Page 296
Page 297......Page 297
Page 298......Page 298
Page 299......Page 299
Page 300......Page 300
Page 301......Page 301
Page 302......Page 302
Page 303......Page 303
Page 304......Page 304
Page 305......Page 305
Page 306......Page 306
Page 307......Page 307
Page 308......Page 308
Page 309......Page 309
Page 310......Page 310
Page 311......Page 311
Page 312......Page 312
Page 313......Page 313
Page 314......Page 314
Page 315......Page 315
Page 316......Page 316
Page 317......Page 317
Page 318......Page 318
Page 319......Page 319
Page 320......Page 320
Page 321......Page 321
Page 322......Page 322
Page 323......Page 323
Page 324......Page 324
Page 325......Page 325
Page 326......Page 326
Page 327......Page 327
Page 328......Page 328
Page 329......Page 329
Page 330......Page 330
Page 331......Page 331
Page 332......Page 332
Page 333......Page 333
Page 334......Page 334
Page 335......Page 335
Page 336......Page 336
Page 337......Page 337
Page 338......Page 338
Page 339......Page 339
Page 340......Page 340
Page 341......Page 341
Page 342......Page 342
Page 343......Page 343
Page 344......Page 344
Page 345......Page 345
Page 346......Page 346
Page 347......Page 347
Page 348......Page 348
Page 349......Page 349
Page 350......Page 350
Page 351......Page 351
Page 352......Page 352
Page 353......Page 353
Page 354......Page 354
Page 355......Page 355
Page 356......Page 356
Page 357......Page 357
Page 358......Page 358
Page 359......Page 359
Page 360......Page 360
Page 361......Page 361
Page 362......Page 362
Page 363......Page 363
Page 364......Page 364
Page 365......Page 365
Page 366......Page 366
Page 367......Page 367
Page 368......Page 368
Page 369......Page 369
Page 370......Page 370
Page 371......Page 371
Page 372......Page 372
Page 373......Page 373
Page 374......Page 374
Page 375......Page 375
Page 376......Page 376
Page 377......Page 377
Page 378......Page 378
Page 379......Page 379
Page 380......Page 380
Page 381......Page 381
Page 382......Page 382
Page 383......Page 383
Page 384......Page 384
Page 385......Page 385
Page 386......Page 386
Page 387......Page 387
Page 388......Page 388
Page 389......Page 389
Page 390......Page 390
Page 391......Page 391
Page 392......Page 392
Page 393......Page 393
Page 394......Page 394
Page 395......Page 395
Page 396......Page 396
Page 397......Page 397
Page 398......Page 398
Page 399......Page 399
Page 400......Page 400
Page 401......Page 401
Page 402......Page 402
Page 403......Page 403
Page 404......Page 404
Page 405......Page 405
Page 406......Page 406
Page 407......Page 407
Page 408......Page 408
Page 409......Page 409
Page 410......Page 410
Page 411......Page 411
Page 412......Page 412
Page 413......Page 413
Page 414......Page 414
Page 415......Page 415
Page 416......Page 416
Page 417......Page 417
Page 418......Page 418
Page 419......Page 419
Page 420......Page 420
Page 421......Page 421
Page 422......Page 422
Page 423......Page 423
Page 424......Page 424
Page 425......Page 425
Page 426......Page 426
Page 427......Page 427
Page 428......Page 428
Page 429......Page 429
Page 430......Page 430
Page 431......Page 431
Page 432......Page 432
Page 433......Page 433
Page 434......Page 434
Page 435......Page 435
Page 436......Page 436
Page 437......Page 437
Page 438......Page 438
Page 439......Page 439
Page 440......Page 440
Page 441......Page 441
Page 442......Page 442
Page 443......Page 443
Page 444......Page 444
Page 445......Page 445
Page 446......Page 446
Page 447......Page 447
Page 448......Page 448
Page 449......Page 449
Page 450......Page 450
Page 451......Page 451
Page 452......Page 452
Page 453......Page 453
Page 454......Page 454
Page 455......Page 455
Page 456......Page 456
Page 457......Page 457
Page 458......Page 458
Page 459......Page 459
Page 460......Page 460
Page 461......Page 461
Page 462......Page 462
Page 463......Page 463
Page 464......Page 464
Page 465......Page 465
Page 466......Page 466
Page 467......Page 467
Page 468......Page 468
Page 469......Page 469
Page 470......Page 470
Page 471......Page 471
Page 472......Page 472
Page 473......Page 473
Page 474......Page 474
Page 475......Page 475
Page 476......Page 476
Page 477......Page 477
Page 478......Page 478
Page 479......Page 479
Page 480......Page 480
Page 481......Page 481
Page 482......Page 482
Page 483......Page 483
Page 484......Page 484
Page 485......Page 485
Page 486......Page 486
Page 487......Page 487
Page 488......Page 488
Page 489......Page 489
Page 490......Page 490
Page 491......Page 491
Page 492......Page 492
Page 493......Page 493
Page 494......Page 494
Page 495......Page 495
Page 496......Page 496
Page 497......Page 497
Page 498......Page 498
Page 499......Page 499
Page 500......Page 500
Page 501......Page 501
Page 502......Page 502
Page 503......Page 503
Page 504......Page 504
Page 505......Page 505
Page 506......Page 506
Page 507......Page 507
Page 508......Page 508
Page 509......Page 509
Page 510......Page 510
Page 511......Page 511
Page 512......Page 512
Page 513......Page 513
Page 514......Page 514
Page 515......Page 515
Page 516......Page 516
Page 517......Page 517
Page 518......Page 518
Page 519......Page 519
Page 520......Page 520
Page 521......Page 521
Page 522......Page 522
Page 523......Page 523
Page 524......Page 524
Page 525......Page 525
Page 526......Page 526
Page 527......Page 527
Page 528......Page 528
Page 529......Page 529
Page 530......Page 530
Page 531......Page 531
Page 532......Page 532
Page 533......Page 533
Page 534......Page 534
Page 535......Page 535
Page 536......Page 536
Page 537......Page 537
Page 538......Page 538
Page 539......Page 539
Page 540......Page 540
Page 541......Page 541
Page 542......Page 542
Page 543......Page 543
Page 544......Page 544
Page 545......Page 545
Page 546......Page 546
Page 547......Page 547
Page 548......Page 548
Page 549......Page 549
Page 550......Page 550
Page 551......Page 551
Page 552......Page 552
Page 553......Page 553
Page 554......Page 554
Page 555......Page 555
Page 556......Page 556
Page 557......Page 557
Page 558......Page 558
Page 559......Page 559
Page 560......Page 560
Page 561......Page 561
Page 562......Page 562
Page 563......Page 563
Page 564......Page 564
Page 565......Page 565
Page 566......Page 566
Page 567......Page 567
Page 568......Page 568
Page 569......Page 569
Page 570......Page 570
Page 571......Page 571
Page 572......Page 572
Page 573......Page 573
Page 574......Page 574
Page 575......Page 575
Page 576......Page 576
Page 577......Page 577
Page 578......Page 578
Page 579......Page 579
Page 580......Page 580
Page 581......Page 581
Page 582......Page 582
Page 583......Page 583
Page 584......Page 584
Page 585......Page 585
Page 586......Page 586
Page 587......Page 587
Page 588......Page 588
Page 589......Page 589
Page 590......Page 590
Page 591......Page 591
Page 592......Page 592
Page 593......Page 593
Page 594......Page 594
Page 595......Page 595
Page 596......Page 596
Page 597......Page 597
Page 598......Page 598
Page 599......Page 599
Page 600......Page 600
Page 601......Page 601
Page 602......Page 602
Page 603......Page 603
Page 604......Page 604
Page 605......Page 605
Page 606......Page 606
Page 607......Page 607
Page 608......Page 608
Page 609......Page 609
Page 610......Page 610
Papiere empfehlen

Linux ​programozás
 9789639863293 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Alkalmazott informatika sorozat

Asztalos Márk Bányász Gábor Levendovszky Tihamér

Második, átdolgozott kiadás

Asztalos Márk Bányász Gábor Levendovszky Tihamér

programozás

Asztalos Márk — Bányász Gábor — Levendovszky Tihamér

Linux programozás Második, átdolgozott kiadás

Asztalos Márk Bányász Gábor Levendovszky Tihamér

Lin ux programozás Második, átdolgozott kiadás

2012

Linux programozás

Második, átdolgozott kiadás Asztalos Márk, Bányász Gábor, Levendovszky Tihamér Alkalmazott informatika sorozat Budapesti Műszaki és Gazdaságtudományi Egyetem Villamosmérnöki és Informatikai Kar Automatizálási és Alkalmazott Informatikai Tanszék Alkalmazott Informatika Csoport C Asztalos Márk, Bányász Gábor, Levendovszky Tihamér, 2012. Sorozatszerkesztő: Charaf Hassan Lektor: Völgyesi Péter

ISBN 978-963-9863-29-3 ISSN 1785-363X

Minden jog fenntartva. Jelen könyvet, illetve annak részeit a kiadó engedélye nélkül tilos reprodukálni, adatrögzítő rendszerben tárolni, bármilyen formában vagy eszközzel elektronikus úton vagy más módon közölni.

■ Az 1795-ben alapított Magyar Könyvkiadók és Egyesülésének a tagja ■ 2060 Bicske, Diófa u. 3. ■ Tel.: 36-22-565-310 ■ Fax: 36-22-565-311 ■ www.szak.hu ■ e-mail: [email protected] ■ http://www.facebook.com/szakkiado ■ Kiadóvezető: Kis Ádám, e-mail• [email protected] ■ Főszerkesztő: Kis Balázs, e-mail: [email protected] SZAK Kiadó Kft. Könyvterjszők

Tartalomjegyzék  Előszó a második kiadáshoz ....................................... xv 1. Bevezetés .......................................................... 1 1.1. A Linux .................................................................................................1 1.2. A szabad szoftver és a Linux története ........................................ 2 1.2.1. FSF ................................................................................................ 2 1.2.2. GPL ................................................................................................ 3 1.2.3. GNU ............................................................................................... 4 1.2.4. Linux-kernel.................................................................................. 4 1.2.5. A Linux rendszer .......................................................................... 6 1.2.6. Linux-disztribúciók ....................................................................... 6 1.3. Információforrások ...........................................................................8

2. Betekintés a Linux-kernelbe ................................. 11 2.1. A Linux-kernel felépítése ..............................................................11 2.2. A Linux elindulása ..........................................................................13 2.3. Processzek .........................................................................................14 2.3.1. A Linux-processzekhez kapcsolódó információk ....................... 15 2.3.2. A processz állapotai .................................................................... 17 2.3.3. Azonosítók ................................................................................... 18 2.3.4. Processzek létrehozása és terminálása ..................................... 19 2.3.5. A programok futtatása ............................................................... 20 2.3.6. Ütemezés ..................................................................................... 21 2.3.6.1. Klasszikus ütemezés .......................................................... 21 2.3.6.2. Az O(1) ütemezés ................................................................ 23 2.3.6.3. Teljesen igazságos ütemező ............................................... 24 2.3.6.4. Multiprocesszoros ütemezés .............................................. 25 2.3.7. Megszakításkezelés .................................................................... 25 2.3.8. Valósidejűség .............................................................................. 26 2.3.9. Idő és időzítők ............................................................................. 26 2.4. Memóriakezelés ...............................................................................27 2.4.1. A virtuálismemória-kezelés........................................................ 27 2.4.2. Lapozás ........................................................................................ 28

Tartalomjegyzék 

2.4.3. A lapozás implementációja a Linuxon ....................................... 29 2.4.4. Igény szerinti lapozás ................................................................. 31 2.4.5. Lapcsere ...................................................................................... 32 2.4.6. Megosztott virtuális memória .................................................... 34 2.4.7. Másolás íráskor (COW technika) ............................................... 34 2.4.8. A hozzáférés vezérlése ................................................................ 35 2.4.9. A lapkezelés gyorsítása .............................................................. 35 2.5. A virtuális állományrendszer .......................................................36 2.5.1. Az állományabsztrakció.............................................................. 36 2.5.2. Speciális állományok .................................................................. 38 2.5.2.1. Eszközállományok .............................................................. 38 2.5.2.2. Könyvtár ............................................................................. 40 2.5.2.3. Szimbolikus hivatkozás...................................................... 40 2.5.2.4. Csővezeték .......................................................................... 40 2.5.2.5. Socket .................................................................................. 41 2.5.3. Az inode ....................................................................................... 41 2.5.4. Az állományleírók ....................................................................... 44 2.6. A Linux programozási felülete .....................................................44

3. Programkönyvtárak készítése ............................... 47 3.1 . Statikus programkönyvtárak ......................................................47 3.2. Megosztott programkönyvtárak ..................................................55 3.2.1. Megosztott programkönyvtár készítése ..................................... 56 3.2.2. Megosztott programkönyvtárak használata ............................. 60 3.2.3. Megosztott programkönyvtárak dinamikus betöltése .............. 63 3.3. Megosztott könyvtárak C++ nyelven ..........................................69 3.3.1. Programkönyvtárbeli C++-osztályok használata ...................... 69 3.3.2. C++-objektumok dinamikus betöltése programkönyvtárból ................................................................... 72 3.4. A megosztott könyvtárak működési mechanizmusai .............77 3.4.1. A betöltött program .................................................................... 78 3.4.2. Statikus könyvtárat tartalmazó program linkelése és betöltése ...................................................................................... 80 3.4.3. Megosztott könyvtár linkelése és betöltése ............................... 83 3.4.3.1. A címtartomány kezelése ................................................... 83 3.4.3.2. A megosztott könyvtárak megvalósításának alapkoncepciói..................................................................... 85 3.4.3.3. A megosztott könyvtárakkal kapcsolatos linkelés és betöltés.............................................................. 87 3.4.3.4. Dinamikusan linkelt megosztott könyvtár linkelése és betöltése .......................................................... 89 3.4.4. A programkönyvtárak használatának optimalizálása ............. 89

vi 

Tartalomjegyzék 

4. Állomány- és I/O kezelés ..................................... 95 4.1. Egyszerű állománykezelés ............................................................95 4.1.1. Az állományleíró ......................................................................... 96 4.1.2. Állományok megnyitása ............................................................. 97 4.1.3. Állományok bezárása.................................................................. 98 4.1.4. Írás, olvasás és pozicionálás az állományban ........................... 99 4.1.5. Részleges és teljes olvasás ........................................................ 101 4.1.6. Az írásművelet finomhangolása ............................................... 103 4.1.7. Állományok rövidítése .............................................................. 106 4.1.8. Állományok átirányítása .......................................................... 106 4.2. Inode-információk ........................................................................ 108 4.2.1. Inode-információk lekérdezése ................................................. 109 4.2.2. Jogok lekérdezése ..................................................................... 110 4.2.3. Jogok állítása ............................................................................ 111 4.2.4. Tulajdonos és csoport beállítása .............................................. 112 4.2.5. Az időbélyeg beállítása ............................................................. 112 4.3. További állományműveletek...................................................... 113 4.3.1. Eszközállományok és pipe bejegyzések létrehozása ............... 113 4.3.2. Merev hivatkozás létrehozása .................................................. 114 4.3.3. Szimbolikus hivatkozás létrehozása ........................................ 115 4.3.4. Állományok törlése ................................................................... 116 4.3.5. Állományok átnevezése ............................................................ 116 4.4. Könyvtárműveletek ..................................................................... 117 4.5. Csővezetékek ................................................................................. 120 4.5.1. Névtelen csővezetékek .............................................................. 121 4.5.2. Megnevezett csővezetékek........................................................ 123 4.6. Blokkolt és nem blokkolt I/O ..................................................... 126 4.7. A multiplexelt I/O módszerei ..................................................... 129 4.7.1. Multiplexelés a select() függvénnyel ........................................ 129 4.7.2. Multiplexelés a poll() függvénnyel ........................................... 134 4.7.3. A multiplexelési módszerek összehasonlítása......................... 139 4.8. Állományok leképezése a memóriába...................................... 139 4.9. Állományzárolás ........................................................................... 143 4.9.1. Zárolóállományok ..................................................................... 144 4.9.2. Rekordzárolás ........................................................................... 145 4.9.3. Kötelező zárolás ........................................................................ 148 4.10. Kapcsolat a magas szintű állománykezeléssel .................... 149 4.11. Soros kommunikáció ................................................................. 150 4.11.1. Kanonikus feldolgozás ............................................................ 151 4.11.2. Nem kanonikus feldolgozás .................................................... 154

vii 

Tartalomjegyzék 

5. Párhuzamos programozás .................................. 157 5.1. Processzek ...................................................................................... 157 5.2. Processzek közötti kommunikáció (IPC)................................ 165 5.2.1. Szemaforok ................................................................................ 166 5.2.2. Üzenetsorok .............................................................................. 178 5.2.3. Megosztott memória ................................................................. 185 5.3. Processzek a Linux rendszerben .............................................. 189 5.3.1. Feladatvezérlés ......................................................................... 191 5.3.2. Démonok.................................................................................... 193 5.3.3. Programok indítása shellből..................................................... 197 5.3.4. Jogosultságok ............................................................................ 198 5.3.5. Felhasználói nevek és csoportnevek ........................................ 200 5.4. Szálak .............................................................................................. 202 5.4.1. Szálak létrehozása .................................................................... 203 5.4.2. Szálak létrehozása C++ nyelven .............................................. 207 5.4.3. Szálak attribútumai ................................................................. 210 5.4.4. Szálbiztos függvények .............................................................. 212 5.4.5. Szál leállítása ............................................................................ 217 5.4.6. Szálak és a fork/exec hívások .................................................. 220 5.5. POSIX-szinkronizáció.................................................................. 221 5.5.1. Kölcsönös kizárás (mutex) ........................................................ 221 5.5.2. Feltételes változók .................................................................... 227 5.5.3. Szemaforok ................................................................................ 233 5.5.4. Spinlock ..................................................................................... 236 5.5.5. További lehetőségek: POSIX megosztott memória és üzenetsorok ............................................................................... 238 5.6. Jelzések ........................................................................................... 238 5.6.1. A jelzésküldés és -fogadás folyamata....................................... 239 5.6.2. Jelzések megvalósítása............................................................. 245 5.6.3. A jelzéskezelő és a főprogram egymásra hatása ..................... 247 5.6.4. Jelzések és a többszálú processz .............................................. 250 5.6.5. Jelzések programozása ............................................................. 250 5.6.5.1. Jelzések küldése ............................................................... 251 5.6.5.2. Jelzések letiltása és engedélyezése. Függőben lévő jelzések ..................................................... 253 5.6.5.3. A jelzések kezelése ........................................................... 254 5.6.5.4. Szinkron jelzéskezelés ...................................................... 258 5.6.6. A SIGCHLD jelzés .................................................................... 262

viii 

Tartalomjegyzék 

6. Hálózati kommunikáció ..................................... 265 6.1. A socket ........................................................................................... 265 6.2. Az összeköttetés-alapú kommunikáció ................................... 267 6.2.1. A kapcsolat felépítése ............................................................... 268 6.2.2. A socket címhez kötése ............................................................. 268 6.2.3. Várakozás a kapcsolódásra ...................................................... 269 6.2.4. Kapcsolódás a szerverhez ......................................................... 269 6.2.5. A kommunikáció ....................................................................... 270 6.2.6. A kapcsolat bontása .................................................................. 270 6.2.7. További kapcsolatok kezelése a szerverben ............................ 271 6.3. Az összeköttetés nélküli kommunikáció ................................. 272 6.3.1. A kommunikáció ....................................................................... 273 6.3.2. A connect() használata .............................................................. 274 6.3.3. A socket lezárása ...................................................................... 275 6.4. Unix domain socket ..................................................................... 275 6.4.1. Unix domain socket címek ....................................................... 275 6.4.2. Unix domain socket adatfolyam szerveralkalmazás .............. 276 6.4.3. Unix domain socket adatfolyam kliensalkalmazás................. 278 6.4.4. Unix domain socket datagram kommunikáció........................ 280 6.4.5. Névtelen Unix domain socket .................................................. 280 6.4.6. A Linux absztrakt névtere ....................................................... 280 6.5. IP ...................................................................................................... 282 6.5.1. Röviden az IP-hálózatokról ...................................................... 282 6.5.2. Az IP protokoll rétegződése ...................................................... 284 6.5.3. IPv4-es címzés ........................................................................... 285 6.5.4. IPv4-es címosztályok ................................................................ 286 6.5.5. IPv4-es speciális címek ............................................................. 287 6.5.6. IPv6-os címzés ........................................................................... 288 6.5.7. Portok ........................................................................................ 289 6.5.8. A hardverfüggő különbségek feloldása .................................... 290 6.5.9. A socketcím megadása .............................................................. 290 6.5.10. Lokális cím megadása ............................................................ 293 6.5.11. Név- és címfeloldás ................................................................. 294 6.5.11.1. A getaddrinfo() függvény................................................ 294 6.5.11.2. A getnameinfo() függvény............................................... 298 6.5.12. Összeköttetés-alapú kommunikáció ...................................... 302 6.5.12.1. TCP kliens-szerver példa ............................................... 304 6.5.12.2. TCP szerver alkalmazás ................................................ 308 6.5.12.3. TCP-kliensalkalmazás ................................................... 315 6.5.13. Összeköttetés nélküli kommunikáció .................................... 317 6.5.13.1. UDP-kommunikáció-példa ............................................. 317 6.5.13.2. Többes küldés ................................................................. 320

ix 

Tartalomjegyzék 

6.6. Socketbeállítások ......................................................................... 326 6.7. Segédprogramok........................................................................... 330 6.8. Távoli eljáráshívás ....................................................................... 331 6.8.1. Az RPC-modell .......................................................................... 332 6.8.2. Verziók és számok..................................................................... 332 6.8.3. Portmap ..................................................................................... 333 6.8.4. Szállítás ..................................................................................... 333 6.8.5. XDR ........................................................................................... 333 6.8.6. rpcinfo ........................................................................................ 334 6.8.7. rpcgen ........................................................................................ 334 6.8.8. Helyi eljárás átalakítása távoli eljárássá ................................ 335

7. Fejlesztés a Linux-kernelben .............................. 339 7.1. Verziófüggőség .............................................................................. 340 7.2. A kernel- és az alkalmazásfejlesztés eltérései ....................... 341 7.2.1. Felhasználói üzemmód — kernelüzemmód ............................. 342 7.3. Kernelmodulok ............................................................................. 343 7.3.1. Hello modul világ ...................................................................... 344 7.3.2. Fordítás ..................................................................................... 346 7.3.3. A modulok betöltése és eltávolítása ......................................... 347 7.3.3.1. insmod/rmmod................................................................. 347 7.3.3.2. modprobe ........................................................................... 347 7.3.4. Egymásra épülő modulok ......................................................... 348 7.4. Paraméterátadás a modulok számára ..................................... 351 7.5. Karakteres eszközvezérlő ........................................................... 353 7.5.1. Fő- és mellékazonosító (major és minor number) ................... 354 7.5.2. Az eszközállományok dinamikus létrehozása ......................... 355 7.5.3. Állományműveletek .................................................................. 356 7.5.4. Használatszámláló.................................................................... 357 7.5.5. „Hello világ” driver ................................................................... 358 7.5.6. Az open és a release függvények............................................... 362 7.5.7. A mellékazonosító (minor number) használata ...................... 363 7.5.8. Az ioctl() implementációja ........................................................ 366 7.6. A /proc állományrendszer ........................................................... 370 7.7. A hibakeresés módszerei ............................................................ 375 7.7.1. A printk() használata ................................................................ 375 7.7.2. A /proc használata ................................................................... 376 7.7.3. Kernelopciók ............................................................................. 377 7.7.4. Az Oops üzenet.......................................................................... 378 7.7.4.1. Az „Oops” üzenet értelmezése kernel esetében............... 379 7.7.4.2. Az „Oops” üzenet értelmezése kernelmodul esetében............................................................................. 380 x 

Tartalomjegyzék 

7.7.5. Magic SysRq .............................................................................. 381 7.7.6. A gdb program használata ....................................................... 382 7.7.7. A kgdb használata .................................................................... 382 7.7.8. További hibakeresési módszerek ............................................. 383 7.8. Memóriakezelés a kernelben ..................................................... 384 7.8.1. Címtípusok ................................................................................ 384 7.8.2. Memóriaallokáció ...................................................................... 384 7.9. A párhuzamosság kezelése ......................................................... 386 7.9.1. Atomi műveletek ....................................................................... 387 7.9.2. Ciklikus zárolás (spinlock) ....................................................... 389 7.9.3. Szemafor (semaphore) .............................................................. 391 7.9.4. Mutex ......................................................................................... 392 7.9.5. Olvasó/író ciklikus zárolás (spinlock) és szemafor (semaphore) ............................................................................... 393 7.9.6. A nagy kernelzárolás ................................................................ 394 7.10. I/O műveletek blokkolása ......................................................... 395 7.10.1. Elaltatás .................................................................................. 396 7.10.2. Felébresztés............................................................................. 397 7.10.3. Példa ........................................................................................ 398 7.11. A select() és a poll() támogatása .............................................. 403 7.12. Az mmap támogatása ................................................................. 404 7.13. I/O portok kezelése..................................................................... 407 7.14. I/O memória kezelése................................................................. 408 7.15. Megszakításkezelés .................................................................... 410 7.15.1. Megszakítások megosztása .................................................... 412 7.15.2. A megszakításkezelő függvények megkötései ....................... 412 7.15.3. A megszakítás tiltása és engedélyezése ................................ 412 7.15.4. A szoftvermegszakítás ............................................................ 413 7.15.5. A BH-mechanizmus ................................................................ 414 7.15.5.1. A kisfeladat (tasklet) ...................................................... 414 7.15.5.2. Munkasor ........................................................................ 417 7.16. A kernelszálak ............................................................................. 421 7.17. Várakozás ..................................................................................... 423 7.17.1. Rövid várakozások .................................................................. 423 7.17.2. Hosszú várakozás ................................................................... 424 7.18. Időzítők ......................................................................................... 425 7.19. Eszközvezérlő modell ................................................................ 426 7.19.1. A busz ...................................................................................... 426 7.19.2. Eszköz- és eszközvezérlő lista ................................................ 428 7.19.3. sysfs.......................................................................................... 429 7.19.4. Buszattribútumok exportálása .............................................. 429

xi 

Tartalomjegyzék 

7.19.5. Az eszközvezérlő ..................................................................... 430 7.19.6. Eszközvezérlő attribútumok exportálása .............................. 433 7.19.7. Az eszköz ................................................................................. 433 7.19.8. Az eszköz attribútumainak exportálása ................................ 435 7.19.9. Példa ........................................................................................ 436 7.20. További információk ................................................................. 438

8. A Qt keretrendszer programozása ........................ 439 8.1. Az X Window rendszer................................................................. 439 8.1.1. Az X Window rendszer felépítése ............................................. 439 8.1.2. X Windows kliensalkalmazások ............................................... 440 8.1.3. Asztali környezet ...................................................................... 441 8.2. Fejlesztés Qt alatt ......................................................................... 442 8.2.1. Hello Világ!................................................................................ 443 8.2.2. Projektállományok .................................................................... 447 8.2.3. A QObject szolgáltatásai........................................................... 448 8.2.4. A QtCore modul ......................................................................... 449 8.3. A Qt eseménykezelés-modellje .................................................. 450 8.3.1. Szignálok létrehozása ............................................................... 452 8.3.2. Szlotfüggvények létrehozása .................................................... 454 8.3.3. Szignálok és szlotok összekapcsolása ...................................... 455 8.3.4. Szlot az átmeneti objektumokban ............................................ 457 8.3.5. A Meta Object Compiler ........................................................... 458 8.4. Ablakok és vezérlők ..................................................................... 459 8.4.1. Dialógusablakok készítése ....................................................... 460 8.4.2. A Qt vezérlőkészlete ................................................................. 466 8.4.3. Saját alkalmazásablakok ......................................................... 467 8.4.4. A főablak programozása ........................................................... 469 8.4.5. Lokalizáció ................................................................................ 479 8.4.6. Saját vezérlők készítése............................................................ 483 8.5. A dokumentum/nézet architektúra .......................................... 489 8.5.1. Az alkalmazás szerepe.............................................................. 491 8.5.2. A dokumentumosztály .............................................................. 493 8.5.3. A nézetosztályok ....................................................................... 497 8.5.4. További osztályok ..................................................................... 503 8.6. További technológiák .................................................................. 509 8.6.1. Többszálú alkalmazásfejlesztés ............................................... 509 8.6.2. Adatbáziskezelés ....................................................................... 516 8.6.3. Hálózati kommunikáció............................................................ 521 8.7. Összefoglalás ................................................................................. 530

xii 

Tartalomjegyzék 

A függelék: Fejlesztőeszközök ................................. 533 A.1. Szövegszerkesztők ....................................................................... 533 A.1.1. Emacs ........................................................................................ 533 A.1.2. vi (vim) ...................................................................................... 534 A.1.3. nano (pico) ................................................................................ 534 A.1.4. joe .............................................................................................. 534 A.1.5. mc .............................................................................................. 534 A.1.6. Grafikus szövegszerkesztők ..................................................... 535 A.2. Fordítók ......................................................................................... 535 A.2.1. GNU Compiler Collection ........................................................ 536 A.2.1. gcc .............................................................................................. 536 A.2.3. LLVM ........................................................................................ 540 A.3. Make ................................................................................................ 541 A.3.1. Megjegyzések ............................................................................ 542 A.3.2. Explicit szabályok .................................................................... 542 A.3.3. Hamis tárgy .............................................................................. 544 A.3.4. Változódefiníciók ...................................................................... 545 A.3.5. A változó értékadásának speciális esetei ................................ 546 A.3.6. Többsoros változók definiálása ................................................ 547 A.3.7. A változó hivatkozásának speciális esetei .............................. 547 A.3.8. Automatikus változók .............................................................. 548 A.3.9. Többszörös cél ........................................................................... 549 A.3.10. Mintaszabályok ...................................................................... 550 A.3.11. Klasszikus ragozási szabályok .............................................. 551 A.3.12. Implicit szabályok .................................................................. 552 A.3.13. Speciális tárgyak .................................................................... 553 A.3.14. Direktívák ............................................................................... 554 A.4. Make alternatívák........................................................................ 554 A.4.1. Autotools ................................................................................... 555 A.4.2. CMake ....................................................................................... 555 A.4.3. qmake ........................................................................................ 555 A.4.4. SCons ........................................................................................ 556 A.5. IDE................................................................................................... 556

B függelék: Hibakeresés ......................................... 557 B.1. gdb ................................................................................................... 557 B.1.1. Példa a gdb használatára ........................................................ 558 B.1.2. A gdb leggyakrabban használt parancsai ............................... 561 B.1.3. A gdb indítása .......................................................................... 561 B.1.4. Töréspontok: breakpoint, watchpoint, catchpoint................... 562 B.1.5. Data Display Debugger (DDD) ................................................ 566 B.1.6. Az IDE-k beépített hibakeresője ............................................. 567 xiii 

Tartalomjegyzék 

B.2. Memóriakezelési hibák .............................................................. 568 B.2.1. Malloc hibakeresők .................................................................. 569 B.2.1.1. Memóriaterület túlírása .................................................. 570 B.2.1.2. Eléírás .............................................................................. 571 B.2.1.3. Felszabadított terület használata ................................... 571 B.2.1.4. Memóriaszivárgás ............................................................ 571 B.2.1.5. A malloc hibakeresők korlátai ........................................ 571 B.2.2. Electric Fence ............................................................................ 572 B.2.2.1. Az Electric Fence használata ........................................... 572 B.2.2.2. A Memory Alignment kapcsoló ........................................ 574 B.2.2.3. Az eléírás .......................................................................... 574 B.2.2.4. Az Electric Fence további lehetőségei ............................. 575 B.2.2.5. Erőforrásigények.............................................................. 575 B.2.3. DUMA ....................................................................................... 576 B.3. Valgrind ......................................................................................... 576 B.3.1. Memcheck.................................................................................. 577 B.3.1.1. A memcheck modul működése ......................................... 578 B.3.2. Helgrind .................................................................................... 580 B.4. Rendszerhívások monitorozása: strace .................................. 582 B.5. Könyvtárfüggvényhívások monitorozása: ltrace ................. 582 B.6. További hasznos segédeszközök .............................................. 582

Tárgymutató ........................................................ 585 Irodalomjegyzék ................................................... 593

xiv 

Előszó a második kiadáshoz  Könyvünk első kiadása óta a Linux fejlődése töretlen. A beágyazott szoftverrendszerek egyre nagyobb térhódításának köszönhetően a Linux népszerűsége is egyre nő, ezen belül számos mobileszköz – köztük okostelefonok – operációs rendszerévé vált. Ezzel együtt az operációs rendszer programozási felülete is sokat fejlődött. Ezért döntöttünk úgy, hogy a könyv első kiadása alapos átdolgozásra, illetve kiegészítésre szorul. Munkánk során csaknem minden fejezetet átírtunk, aktuálissá tettünk, a kernel programozásával kapcsolatos részt teljesen újraírtuk. A Linux a közelmúltban volt húszéves, az interneten nagyon sok cikk, példaprogram és közösségi oldalak állnak rendelkezésre. A Linux nagyon sokban követi a POSIX-szabványt, amely számos további dokumentációforrást jelent. Így véleményünk szerint egy Linuxról szóló könyv akkor a leghasznosabb, ha rendszerezi a programozáshoz szükséges ismereteket, a rendszer működését mutatja be, és a programozás logikája vezeti a tárgyalást. Könyvünkben így próbálunk hathatós segítséget nyújtani: az olvasót bevezetjük a rendszer működésébe, nemcsak a mit? és hogyan? kérdésekre adunk választ programrészletekkel illusztrálva, hanem a miért? kérdésre fektetjük a hangsúlyt, és arra építve mutatjuk be a többit. A legtöbb esetben egy példa motiválja a bemutatandó megoldást, amelyet külön kiemelünk. Feladat Készítsünk megosztott könyvtárat a kerekítést végző függvényünkkel.

A különösen fontos következtetéseket, jó tanácsokat „útmutatókban” összegezzük. Útmutató Ha a programozás számunkra több mint kész példakódok összefésülése, majd azok próbálkozással történő kijavítása, és időt szánunk a működés megértésére, sokkal bonyolultabb hibákat sokkal előbb észreveszünk, — ez képessé tesz minket Linux alatti szoftver tervezésére is.

Az első hat fejezet a Linux rendszer C nyelven hívható alapfunkcióit tárgyalja, egy fejezet a kernelmodulok készítéséről szól, míg az utolsó fejezet az XWindow rendszer programozását mutatja be C++ nyelven Qt-környezetben. A könyv alapját a Budapesti Műszaki és Gazdaságtudományi Egyetem Villamosmérnöki és Informatikai Karán választható „Linux-programozás” tantárgy elő-

Előszó a második kiadáshoz 

adásai és laborfoglalkozásai képezik. A könyv első fejezetei azt feltételezik, hogy az olvasó tisztában van a C és C++ nyelvek alapjaival, az alapvető adatstruktúrákkal, és rendelkezik az elemi programozástechnikai ismeretekkel, amelyek például az [1,2,3] irodalmi hivatkozásnak megfelelő könyv feldolgozásának eredményeként szerezhetők meg. Olyan olvasókra is számítunk, akik Linux alatt próbálnak először komolyabban programozni, ezért a fontosabb fejlesztőeszközöket és magát az operációs rendszert teljesen az alapoktól tárgyaljuk, és a függvényhívásokat olyan részletességgel mutatjuk be, hogy azokat közvetlenül fel lehessen használni. Ahol csak tehettük, az egyes funkciókat egyszerű példákkal illusztráltuk. Reményeink szerint azok, akik feldolgozzák a könyv első hat fejezetét, képesek lesznek arra, hogy önállóan megtalálják és értelmezzék azokat a további Linuxszal kapcsolatos információkat, amelyekre szükségük van a fejlesztés során. Ebben a kiadásban különösen a szoftvertervezőket próbáljuk segíteni: bemutatjuk, hogy a Linux által biztosított megoldások milyen logikát követnek, melyek a járható utak, és mik ezek előnyei/hátrányai, valamint melyik módszer mikor hatékony. Azok számára, akik ipari projektet indítanának, bemutatunk néhány olyan eszközt (grafikus fejlesztői környezet – „A” függelék, a memóriaszivárgást felderítő programok, hibakeresők – „B” függelék), amelyek nélkül hoszszabb programok írása nehézkes és hosszan tartó volna. A grafikus fejlesztés bemutatásakor döntenünk kellett, hiszen a számos eszközkészlet mindegyikét nem mutathatjuk be. Választásunk a Qt-re esett mivel elterjedt mind asztali, mind beágyazott környezetben, valamint jól strukturált fejlesztői eszközkészlet. Úgy gondoljuk, hogy a grafikus felhasználói felület programozása és tervezése ma már kiforrottnak mondható, így ha valaki egy környezetet megismer, gyakorlatát minimális változtatásokkal más rendszerekben is alkalmazni tudja. A Qt alatti programozással foglalkozó részek haladó C++-programozói szintet feltételeznek. A C++ nyelv szükséges elemeit a [2] irodalmi hivatkozás első 12 fejezete, illetve a [3] hivatkozás ismerteti. A könyvet tankönyvként használók számára a következő fejezetek feldolgozását javasoljuk: ●

Linuxot használó C kurzus: „A” függelék, (bevezetés a nyelvi elemekbe, szabványos könyvtári függvények, például [1] alapján): 4, 5, 6.



Operációs rendszerek alapjai kurzus gyakorlati illusztrálása: 1–5.



Linux-programozási kurzus: „A” függelék , „B” függelék, 1–8.

Továbbra is igyekeztünk, hogy az egyes fejezetek a lehetőségekhez mérten és a téma jellegétől függően önálló egészet alkossanak, esetleges rövidebb ismétlések árán is. Az itt leírt tananyag számonkérése a téma jellege miatt különösen nehézkes. Ezt megkönnyítendő a számon kérhető fogalmakra, valamint a fontosabb mondanivalóra vastag betűkkel hívtuk fel a figyelmet. A folyó szövegben

xvi 

Előszó a második kiadáshoz 

gyakran hivatkozunk a programkódban szereplő változókra, konstansokra, makrókra stb. Ezeket dőlt betűkkel szedtük az elkülöníthetőség miatt. A programrészleteket, a parancssorba beviendő parancsokat és szkripteket szürke háttér jelzi. A szerzők törekedtek arra, hogy a könyvben szereplő kódrészek elektronikusan is hozzáférhetők legyenek, ezek a példák az alábbi oldalon érhetők el: http://szak.hu/linux. Jelen munkában felhasználtuk a Linux saját dokumentációit, így az info és a man oldalakat, a POSIX-szabványokat. Az egyes forrásokat a könyv jellege és a tárgyalás folyamatossága miatt nem jelöltük külön, az irodalomjegyzékben összegeztük őket. Az egyes új fogalmaknak magyar megfelelői mellett kerek zárójelben közöljük azok angol megfelelőit a további tájékozódás megkönnyítéséhez. Elsőként szeretnénk megköszönni Völgyesi Péternek különösen gondos lektori munkáját és értékes tanácsait. Köszönjük Lattman Zsolt, Szilvási Sándor, Horváth Péter és Babják Benjamin visszajelzéseit és a kézirat egyes részeinek átolvasását. Köszönjük továbbá Laczkó Krisztina olvasószerkesztői munkáját, amely jelentősen növelte a kézirat szövegének igényességét és érthetőségét, valamint Mamira Györgynek a kézirat tördelését. Köszönetünket szeretnénk kifejezni Szilvási Sándornak a fedélborító külső megjelenésének az elkészítéséért, az Institute for Software Integrated Systems kutatóintézetének (Vanderbilt Egyetem, Nashville, Tennessee, USA), valamint az Automatizálási és Alkalmazott Informatikai Tanszék (Budapesti Műszaki és Gazdaságtudományi Egyetem) Alkalmazott Informatika Csoportjának, hallgatóinknak és a SZAK Kiadó munkatársainak. Végül pedig továbbra is bízunk abban, hogy ez a könyv sokak számára lesz megbízható segítség tanulmányaik és munkáik során, és reményeink szerint akad néhány olyan olvasó, aki szabadidejét nem kímélő, lelkes tagja lesz a szabad szoftverek önkéntes fejlesztőgárdájának.

Budapest, 2012. október A szerzők

xvii 

ELSŐ FEJEZET

Bevezetés

1.1. A Linux A Linux szónak több jelentése is van. Műszaki értelemben pontos definíciója a következő: A Linux szabadon terjeszthető, Unix1 szerű operációsrendszer kernel. -

-

A legtöbb ember a Linux szó hallatán azonban a Linux-kernelen alapuló teljes operációs rendszerre gondol. Így általában az alábbiakat értjük rajta: A Linux szabadon terjeszthető, Unix-szerű operációs rendszer, amely tartalmazza a kernelt, a rendszereszközöket, a programokat és a teljes fejlesztői környezetet. A továbbiakban mi is a második jelentését vesszük alapul, vagyis a Linuxot mint operációs rendszert mutatjuk be. A Linux kiváló, ingyenes platformot ad a programok fejlesztéséhez. Az alapvető fejlesztőeszközök a rendszer részét képezik. Unix-szerűségéből adódóan programjainkat könnyen átvihetjük majdnem minden Unix- és Unix-szerű rendszerre. További előnyei a következők: •

A teljes operációs rendszer forráskódja szabadon hozzáférhető, használható, vizsgálható és szükség esetén módosítható.



Ebbe a körbe beletartozik a kernel is, így komolyabb, a kernel módosítását igénylő problémák megoldására is lehetőségünk nyílik.

A Unix egy 1960-as években keletkezett többfelhasználós operációs rendszer, amelynek nagy részét C nyelven írták az AT&T cégnél. Évtizedekig a legnépszerűbb operációs rendszerek egyike volt. A System V az AT&T által fejlesztett Unix alapverziójának a neve. A másik jelentős változat a kaliforniai Berkeley Egyetemhez kötődik, ez a Berkeley Software Distribution, röviden BSD.

1. fejezet: Bevezetés



A Linux fejlesztése nem profitorientált fejlesztők kezében van, így fejlődésekor csak műszaki szempontok döntenek, marketinghatások nem befolyásolják.



A Linux-felhasználók és -fejlesztők tábora széles és lelkes. Ennek következtében az interneten nagy mennyiségű segítség és dokumentáció található.

Az előnyei mellett természetesen meg kell említenünk a hátrányait is. A decentralizált fejlesztés és a marketinghatások hiányából adódóan a Linux nem rendelkezik olyan egységes, felhasználóbarát kezelői felülettel, mint a versenytársai, beleértve a fejlesztői eszközök felületét is. Ennek ellensúlyozására az egyes disztribúciók készítői többnyire törekednek arra, hogy a kezükből kiadott rendszer egységes, jól használható felületet nyújtson. Ám a disztribúciók mennyisége ugyanakkor megnehezítheti a fejlesztők dolgát, ha minden rendszeren támogatni szeretnék a programcsomagjukat. 2 Mindezek figyelembevételével azonban a Linux így is kiváló lehetőségeket nyújt a fejlesztésekhez, elsősorban a grafikus felhasználói felülettel nem rendelkező programok területén, de hathatós támogatást biztosít grafikus kliensalkalmazások számára is. A Linux rendszerek használata a beágyazott eszközök területén a legjelentősebb. Számos olyan eszközt találhatunk manapság az otthonokban, amelyekről sokszor nem is tudjuk, hogy rajtuk egy Linux rendszer teszi a háttérben a dolgát (például otthoni router, DVD felvevő/lejátszó, fényképezőgépek stb.). A beágyazott alkalmazások egy külön csoportját alkotják a mobiltelefonok. A Linux rendszer számos mobiltelefon operációs rendszerének is az alapja. Ezek egy része lényegében csak a kernelt és a főbb fejlesztői könyvtárakat használja (például az Android), de találhatunk olyat is, amelyik szinte egy teljes linuxos számítógépnek felel meg.

1.2. A szabad szoftver és a Linux története 1.2.1. FSF A számítástechnika hajnalán a cégek kis jelentőséget tulajdonítottak a szoftvereknek. Elsősorban a hardvert akarták eladni, a szoftvereket csak járulékosan adták hozzá, üzleti jelentőséget nem tulajdonítottak neki. Ez azt eredményezte, hogy a forráskódok, az algoritmusok szabadon terjedhettek.

2

2

A különböző disztribúciók okozta problémában segítséget nyújt a később említett LSBprojekt.

1.2. A szabad szoftver és a Linux története

Ám ez az időszak nem tartott sokáig, a gyártók hamar rájöttek a szoftverben rejlő üzleti lehetőségekre, és ezzel beköszöntött a zárt forráskódú programok korszaka. Ez lényegében azt jelenti, hogy a szoftverek forráskódját, mint szellemi termékeket, a cégek levédik, és üzleti titokként szigorúan őrzik. Ezek a változások nem nyerték el az akkoriban a Massachusettsi Műszaki Egyetemen (Massachusetts Institute of Technology, MIT) dolgozó Richard Stallman tetszését. Így megalapította a Free Software Foundation (FSF) elnevezésű szervezetet a massachusettsi Cambridge-ben. Az FSF célja a szabadon terjeszthető szoftverek fejlesztése lett.

1.2.2. GPL Az FSF nevében a „free" szabadságot jelent, nem ingyenességet. Stallman hite szerint a szoftvernek és a hozzátartozó dokumentációnak, forráskódnak szabadon hozzáférhetőnek és terjeszthetőnek kell lennie. Ennek elősegítésére megalkotta (némi segítséggel) a General Public License-t (GPL, magyarul: általános felhasználói licenc), amelyet 1989-től használtak. A GPL három fő irányelve a következő: 1. Mindenkinek, aki GPL-es szoftvert kap, megvan a joga arra, hogy ingyenesen továbbadja a forráskódját. (Leszámítva a terjesztési költségeket.) 2. Minden szoftver, amely GPL-es szoftverből származik, szintén GPL-es kell, hogy legyen. 3. A GPL-es szoftver birtokosának megvan a joga ahhoz, hogy a szoftvereit olyan feltételekkel terjessze, amelyek nem állnak konfliktusban a GPL-lel. A GPL egyik jellemzője, hogy nem nyilatkozik az árról. Vagyis a GPL-es szoftvertermékeinket szabadon értékesíthetjük. Egyetlen kikötés az, hogy a forráskód ingyenesen jár a szoftverhez. A vevő ezután azonban szabadon terjesztheti a programot és a forráskódját. Az internet elterjedésével ez azt eredményezte, hogy a GPL-es szoftvertermékek ára alacsony lett (sok esetben ingyenesek), de lehetőség nyílik ugyanakkor arra is, hogy a termékhez kapcsolódó szolgáltatásokat, támogatást térítés ellenében nyújtsák. A GPL licenc 2-es verziója 1991 nyarán született meg, és a szoftverek jelentős része ezt használja. Mivel a megkötések nagyon szigorúnak bizonyultak a fejlesztői könyvtárakkal kapcsolatban, ezért megszületett az LGPL licenc (eredetileg Library General Public License, később GNU Lesser General Public License) körülbelül a GPLv2-vel egy időben. Az LPGL első változata a 2-es verziószámot kapta.

3

1. fejezet: Bevezetés

Időközben azonban megjelentek olyan gyártók, akik „kreatívan" értelmezték a GPLv2 licencet. A szöveget betartották, ám az alapelvet megsértették. 3 A problémák orvoslására 2007 nyarán jelent meg a GPLv3. A GPLv3 fogadtatása azonban vegyes volt, ezért sokan maradtak a GPLv2-nél. A GPLv3-mal párhuzamosan egy új licenc is született reagálva a kor kihívásaira, nevezetesen a hálózaton futó alkalmazásokra, ilyenek például a webes alkalmazások. Ez az új licenc az AGPLv3 (Affero General Public License).

1.2.3. GNU Az FSF által támogatott legfőbb mozgalom a GNU's Not Unix (röviden GNU) projekt, amelynek az a célja, hogy szabadon terjeszthető Unix-szerű operációs rendszert hozzon létre. Ez a projekt nagyon sokat adott hozzá a Linux rendszerhez. Csak a legfontosabbakat említve: a C fejlesztői könyvtár, a GNU Compiler Collection, amely a legelterjedtebb fordító eszközcsomag, a GDB, amely a fő hibakereső program, továbbá számos, a rendszer alapjaként szolgáló segédprogram.

1.2.4. Linux-kernel A Linux-kernel 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 a gépén. A Minix az operációs rendszerek működését, felépítését volt hivatott bemutatni, ezért egyszerűnek, könnyen értelmezhetőnek kellett maradnia Emiatt nem tudta kielégíteni Linus Torvalds igényeit, aki ezért belevágott egy saját, Unix-szerű operációs rendszer fejlesztésébe. Eredetileg a Linux-kernelt gyenge licenccel látta el, amely csak annyi korlátozást tartalmazott, hogy a Linux-kernel nem használható fel üzleti célokra. Ám ezt rövidesen GPL-re cserélte. A GPL feltételei lehetővé tették más fejlesztőknek is, hogy csatlakozzanak a projekthez. 4 A MINIX közösség jelentős mértékben támogatta a munkát. Abban az időben nagy szükség volt egy szabad kernelre, mivel a GNU-projekt kernelrésze még nem készült el, a BSD rendszerrel kapcsolatban pedig jogi problémák merültek fel. A pereskedés közel két évig gátolta a szabad BSD-változatok fejlesztését. Ez a környezet nagyban hozzájárult a Linux megszületéséhez.

4

4

Egyes cégek, bár betartották a GPLv2 licencet, és kiadták az ez alá tartozó forráskódokat, a hardverben azonban olyan trükköket alkalmaztak, mint a digitális aláírást, amelylyel megakadályozták, hogy rajtuk kívül bárki más új szoftververziót fordíthasson és telepíthessen az eszközre. A szakzsargonban ez a módszer „tivoization" néven terjedt el, mivel a TiVo cég alkalmazta először. A Linux-kernel jelenleg a GPLv2 licencet használja.

1.2. A szabad szoftver és a Linux története

A Linux-kernel fejlődésének állomásai a következők: •

1991. szeptember: a Linux 0.01-es verziója megjelent az ftp.funet.fi szerveren, amely a finn egyetemi hálózat állományszervere.



1991. október: Linux 0.02.



1991. december: Linux 0.11 — az első önhordó Linux. Vagyis a Linuxkernel ettől kezdve fordítható a Linux rendszeren. A korábbi fejlesztések Minix alatt történtek.



1992. február: Linux 0.12 — az első GPL licences kernel.



1992. március: Linux 0.95 — az X Windows rendszert átültették Linuxra, így már grafikus felületet is kapott.



1994. március: Linux 1.0.0 — a gyors fejlődést látva hamarabb várta mindenki, de kellett még néhány fejlesztői verzió, mire elég éretté vált a rendszer. Ez a kernel is még csak egy processzort és csak i386os architektúrát támogatott.



1995. március: Linux 1.2.0 — a Linux megérkezett más architektúrákra is. Az Alpha, az SPARC és a MIPS rendszerek is bekerültek a támogatott platformok közé.



1996. június 9: Linux 2.0.0 — megjelent a többprocesszoros rendszerek támogatása (SMP).



1999. január 25: Linux 2.2.0 — javult az SMP támogatása, megjelent a támogatott rendszerek között az m68k és a PowerPC.



2001. január 4: Linux 2.4.0 — megjelent az ISA PnP-, az USB- és a PCkártya- (PC card) támogatás. Továbbá a támogatott architektúrák közé bekerült a PA-RISC. Ez a verzió abban is más a korábbiaknál, hogy a felgyorsult fejlődés hatására már a stabil változatnak is egyre több újdonságot kellett átvennie, ilyen például a Bluetooth, az LVM, a RAID, az ext3 állományrendszer.



2003. december 17: Linux 2.6.0 — megváltozott, felgyorsult a fejlesztési modell. Nincs külön stabil és fejlesztői vonal, hanem sokkal rövidebb ciklusokban ebbe a vonulatba kerül bele minden újítás. Emellett számos újítást és új architektúrák támogatását hozta ez a verzió, ezért ezek felsorolására jelen keretek közt nem vállalkozunk.



2011. július 22: Linux 3.0 — a Linux 20 éves évfordulója alkalmából jelent meg. A verzióváltás jelképes, mivel a fejlesztések folyamatosan belekerültek a 2.6.x kernelekbe. Így az új verzió technikai újítást nem hozott a 2.6.39-es verzióhoz képest. Megváltozott a verziószám felépítése, mivel a fejlesztési modell szükségtelenné tette a három szintet.

5

1. fejezet: Bevezetés

1.2.5. A Linux rendszer A Linux-projekt már a kezdetektől 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ük felépítéséhez. A rendszer további jelentős részletei a kaliforniai Berkley Egyetem nyílt Unix-forráskódjaiból, illetve az X konzorciumtól származnak. A különböző Unix-fajták egységesített programozói felületének létrehozására született meg a C nyelven definiált POSIX- (Portable Operating System Interface) szabvány — a szó végén az X a Unix világára utal —, amelyet a Linux implementálásakor is messzemenőkig figyelembe vettek. Szintén a Unix-fajták egységesítésére jött létre a SUS (Single UNIX Specification), amely egy szabványgyűjtemény. A SUS aktuális verzióinak a magját a POSIX-szabványok alkotják. A SUS definiálja a headerállományokat, a rendszerhívásokat, a könyvtári függvényeket és az alapvető segédprogramokat, amelyeket egy Unix rendszernek nyújtania kell. Emellett információkat tartalmaz a szabványok mögött álló megfontolásokról is. 6 A Linux hasonlóan más nyílt operációs rendszerekhez nem rendelkezik a SUS Unix tanúsítványával. Ennek oka részben a tanúsítvány megszerzésének költsége, másrészt a Linux gyors fejlődése, amely további extraköltséget jelentene a tanúsítvány megtartásához. Ugyanakkor a rendszer fejlesztői törekednek a SUS-szabványok teljesítésére. Ezen okokból a Linuxra a Unix-szerű (Unix-like) jelzőt használjuk.

1.2.6. Linux-disztribúciók Az első Linux-disztribúció (terjesztési csomag) megjelenése előtt, ha valaki Linux-használó akart lenni, akkor jól kellett ismernie a Unix rendszereket, a Linux felépítését, konfigurálását, elindulásának a folyamatát. Ennek az az oka, hogy a használónak kellett a komponensekből felépítenie a rendszert. Beláthatjuk, hogy ez jelentős akadályt jelentett a rendszer elterjedésében, hiszen a kezdőknek nem sok esélyük volt megismerni a rendszert. Ezért, amint a Linux rendszert a Linux-fejlesztőin kívül mások is elkezdték használni, me gj elentek az első disztribúciók. A korai disztribúciók többnyire lelkes egyének csomagjai voltak. Néhány a korai disztribúciókból: Boot-root, MCC Interim Linux, TAMU, SLS, Yggdrasil Linux/GNU/X. 5

6

6

Jelenleg a POSIX-szabvány az IEEE Std 1003.1-2008 szabványt jelenti, amelyet POSIX.12008-ként rövidítenek. A POSIX-implementációk eltérhetnek, ugyanis a szabvány csak felületet határoz meg, nem megvalósítást. Legegyszerűbben a man oldalak végén találjuk meg egy adott függvény POSIX-kompatibilitását. Jelenleg a SUS 3-as verziója a legelterjedtebb, amely a POSIX:2001-es szabványon alapul. Az a rendszer amelyik ezt teljesíti az jogosult a UNIX 03 címke használatára. A POSIX:2008 a SUSv4 alapját szolgáltatja.

1.2. A szabad szoftver és a Linux története

Az első jelentős disztribúció az 1993-ban megjelent Slackware volt, amelyet az SLS-disztribúcióból alakított ki Patrick Volkerding. A Slackware tekinthető az első olyan csomagnak, amelyet már komolyabb háttértudás nélkül is lehetett installálni és használni. Megszületése nagyban elősegítette a Linux terjedését, népszerűségének a növekedését.? A Slackware egyben az alapját szolgáltatta több későbbi rendszernek is, például a Red Hatnek és a SuSE-nek. A Slackware mellett azonban az SLS miatti elégedetlenség egy másik disztribúció megszületéséhez is hozzájárult. Ian Murdock elindította a Debian projektet (a disztribúció neve a felesége, Debra és saját keresztnevének összeolvasztása). A Debian szintén 1993-ban született, ám az első 1.x verzió csak 1996 nyarán jelent meg. A Debian számos további disztribúció alapjaként szolgált és szolgál mai is. A Red Hat Linux-disztribúció 1994 novemberében jelent meg az 1.0-s verzióval. Ez volt az első olyan disztribúció, amely az RPM csomagkezelőjét használta. Ez nagy előrelépést jelentett, ugyanis sokkal könnyebbé tette a szoftvercsomagok adminisztrálását, telepítését, frissítését. Ez is hozzájárult a Linux további elterjedéséhez, mert felhasználóbarát, teljes rendszert biztosított. Emellett a Red Hat cég olyan szintű támogatás nyújtott a termékéhez, amely lehetővé tette, hogy a cégek is komolyan fontolóra vegyék a rendszer használatát. 2003-ban a Red Hat Linux ebben a formájában megszűnt. Helyette a Red Hat Enterprise Linux (RHEL) rendszert ajánlja a cég vállalati környezetbe, illetve életre hívta és támogatja a Fedora Projectet, amely a Fedora disztribúciót tartja karban. Az üzleti világban az RHEL az egyik legnépszerűbb disztribúció. Napjaink egyik leggyakrabban használt Linux-disztribúciója a Debianalapú Ubuntu. A disztribúció egy dél-afrikai humanista filozófiáról kapta a nevét, amelynek lényege a mások iránti emberségesség: „azért vagyok az, aki vagyok, amiért mi mindannyian azok vagyunk, akik vagyunk". A rendszert a dél-afrikai Mark Shuttleworth alapította Canonical Ltd. fejleszti, amely a terméktámogatást pénzért árulja, ugyanakkor az operációs rendszer ingyenes. Az első verzió 2004 októberében jelent meg. Az eddig említett disztribúciókat számos újabb is követte, amelyek különböző célok mentén fejlődtek. Így lehetőségünk van kicsi és gyors, nagy és látványos, stabil és kevésbé aktuális vagy nem annyira stabil, de minden újdonságot tartalmazó rendszert is választani. A disztribúciók listája rendkívül hosszú, így nem is vállalkozunk a felsorolásukra. Az interneten megtalálhatjuk a nekünk legszimpatikusabbat. Ám ha magyar nyelvű és magyarok által gondozott disztribúciót szeretnénk, akkor erre is van lehetőségünk az UHU Linux- (http: / / uhulinux.hu) disztribúció révén. Az oktatási intézmények számára kifejlesztett SuliX (http: / / www.sulix.hu) szintén ebbe a kategóriába tartozik.

7

Jelen írás szerzője is a Slackware disztribúciója révén találkozott először a Linux rendszerrel 1993 végén. 7

1. fejezet: Bevezetés

A Linux-disztribúciók általában a fejlesztői könyvtárakat, a fordítókat, az értelmezőket, a parancsértelmezőket, az alkalmazásokat, a segédprogramokat, a konfigurációs és csomagkezelő eszközöket és még sok más komponenst is tartalmaznak a Linux-kernel mellett. Az alapelemek többnyire megegyeznek a disztribúciókban, csak eltérő verziókkal, illetve kisebb-nagyobb módosításokkal találkozhatunk. Az opcionális fejlesztői könyvtárak már nem minden diszt8 ribúcióban lelhetők fel, de forrásból lefordíthatjuk őket hozzájuk. Mivel a különböző Linux-disztribúciók nagymértékben eltérhetnek egymástól, ezért felhasználók és a fejlesztők számára jelentős problémát jelenthet a különbözőségek kezelése. Felismerve a problémát a Linux-disztribúciók készítői létrehozták az LSB (Linux Standard Base) projektet, amelynek célja a rendszerstruktúra egységesítése és szabványosítása. Ennek eredménye az is, hogy a különböző Linux rendszereken nagyjából egységes könyvtárstruktúrákkal találkozunk. Az LSB a korábban említett POSIX- és SUS-specifikációkat veszi alapul, illetve ezek mellett számos nyílt szabványra is épít. Fejlesztőként az LSB-projekt hatalmas segítséget nyújt nekünk, mivel így egységesen kezelhetjük a Linux rendszert, és a szoftvereink számos disztribúción lesznek működőképesek. A könyv tematikájának kialakításakor törekedtünk arra, hogy lehetőség szerint disztribúciófüggetlenek legyünk. Így a tárgyalt témakörök minden disztribúció esetén alkalmazhatók, illetve a fejlesztői eszközöket is úgy választottuk ki, hogy lehetőleg általánosan elérhetők legyenek. Ám a példaprogramok fordításakor időnként találkozhatunk azzal a problémával, hogy egyes disztribúciók esetében a könyvtárstruktúra eltérhet. Viszont az LSB-t követő disztribúcióknál ennek minimális az esélye.

1.3. Információforrások A Linux története során mindig is kötődött az internethez. Internetes közösség készítette, fejlesztette, terjesztette, így a dokumentációk nagy része is az interneten található. Ezért a legfőbb információforrásnak is az internetet tekinthetjük. A Linux-világ egyik klasszikus információforrása a Linux Documentation Project (Linux dokumentációs projekt, LDP). Az LDP elsődleges feladata magas szintű, ingyenes dokumentációk fejlesztése a GNU/Linux operációs rendszer számára. Céljuk minden Linuxszal kapcsolatos témakör lefedése a megfelelő dokumentumokkal. Bár sok dokumentum nem teljesen aktuális, ennek elle-

8

8

Gyakran elhangzik a kérdés: melyik a legjobb disztribúció? Szerintünk ilyen nincs, csak olyan, amely elég közel áll a felhasználó egyéni igényeihez, és már nem kell olyan sokat dolgoznia rajta, hogy teljesen a saját képére alakítsa. Ha rosszul választ kiindulási alapot, akkor több munkája lesz vele.

1.3. Információforrások

nére is az egyik legjelentősebb információforrásnak tekinthető. Az egyes Linux-disztribútorok sokszor nagyon komoly és jól használható dokumentációt készítenek. Ám ezek a dokumentációk többnyire az adott disztribúció használatáról, adminisztrálásáról szólnak. Szoftverfejlesztésről ritkán találunk dokumentációkat. Az LDP által elkészített dokumentumok a következő címen találhatók meg: http:/ / www.tldp.org / . A kézikönyvoldalak (manual page, rövidítve man page) a Unix klasszikus elektronikus dokumentációs formája, amelyet a Linux is örökölt. Ezek az oldalak eredetileg a man segédprogrammal jeleníthetők meg, de manapság HTML és egyéb formátumban is elérhető. Emiatt magyarul számos elnevezésük van, legtöbben az angol elnevezést használják, írott formában sokszor csak „kézikönyv" használatos. Az első kézikönyveket a Unix szerzői írták a hetvenes évek legelején. A tagolása egységes, a POSIX-szabvány is ezzel a felépítéssel írja le az egyes interfészeket, ezek a leírások kézikönyvek formájában is rendelkezésre állnak — sokszor kiegészítve az adott platformra jellemző további információval, esetleg eltérésekkel. Ez a mai napig az egyik legfontosabb dokumentáció. Az info formátumot a GNU vezette be, ez képes linkeket és egyéb formázásokat is kezelni. A HOWTO-k egy-egy konkrét probléma megoldását nyújtják. Számos formátumban (HTML, PostScript, PDF, egyszerű szöveg) elérhetők. Az útmutató (guide) kifejezés az LDP által készített könyveket jelöli. Ezek egy-egy témakör bővebb kifejtését tartalmazzák. Fejlesztőként a fejlesztői könyvtárak dokumentációinak vehetjük a legnagyobb hasznát. Minden fejlesztői könyvtár honlapján találunk minimálisan egy API-dokumentációt. Gyakran találkozhatunk azonban komoly leírásokkal, gyakorlópéldákkal is. Az LWN (http:/ / lwn.net) a Linux-világ hírlapja. Igyekszik összegyűjteni minden újdonságot a Linux világából. Az általános hírek mellett a Development rovatból tájékozódhatunk a legújabb fejlesztői hírekről, míg a Kernel rovat a kernelfejlesztés iránt érdeklődőknek szolgál friss információkkal. inter Ha a feladatunkkal elakadnánk, számos fórumot találhatunk az , ahol feltehetjük a kérdéseinket. Ráadásul sokszor az is előfordul, hogy mások már feltették a kérdést, és már meg is válaszolták. Így ha elakadunk, akkor gyakran a legnagyobb segítséget a webes keresők jelentik, amelyek megtalálják a kívánt fórumbejegyzéseket, sőt a kézikönyvek és a howtok között is képesek keresni.

9

MÁSODIK FEJEZET

Betekintés a Linuxkernelbe Ebben a fejezetben a Linux-kernel egyes részeivel ismerkedünk meg. A következő ismeretek nem elengedhetetlenek ahhoz, hogy a könyv további fejezeteiben található példaprogramokat közvetlenül felhasználjuk, a fejezetek megértéséhez azonban igen. Ezek a tudnivalók megvilágítják, hogy mi zajlik le a rendszer belső magjában a programunk futása közben, így hasznos háttérinformációt szolgáltathatnak a fejlesztők számára. Ez a fejezet tehát egyfajta áttekintést kíván nyújtani. A további, specializált részekben sokszor részletesebben tárgyaljuk az itt bemutatottakat.

2.1. A Linux-kernel felépítése Egy operációs rendszer magjának strukturális felépítésénél két alapvető szélsőséges választásunk van. Választhatunk a mikrokernel- és a monolitikuskernel-struktúra között. A lényegi különbség a kettő között az, hogy milyen szolgáltatásokat valósítanak meg a felhasználói címtérben, illetve a kernel címterében. A mikrokernel csak a legszükségesebb operációsrendszer-szolgáltatásokat futtatja a kernelben, míg a többit a rugalmasság érdekében a felhasználói címtartományban implementálja. Ezzel szemben a monolitikus kernelben a legtöbb szolgáltatás a kernel részeként fut a nagyobb teljesítmény érdekében. A kettő között félúton a hibrid kernel található. Vegytiszta megoldások egyre kevésbé vannak: a mikrokernel-architektúrákon a jobb teljesítmény miatt bizonyos alacsony szintű szolgáltatásokat áthelyeznek a kernelbe (pl. a grafikusvezérlők). A monolitikus kernel esetében részben a rugalmasság, részben a teljesítménymaximalizálás érdekében tesznek át funkciókat a felhasználói tartományba. A Linux a monolitikuskernel-megközelítéshez áll közelebb, a gyökerei oda nyúlnak vissza.

2. fejezet: Betekintés a Linux-kernelbe

A mikrokernel és monolitikus kernel kategóriától függetlenül a modern operációs rendszerek kernele dinamikusan betölthető modulokból épül fel, így használat közben az igényeknek megfelelően bővíthető vagy csökkenthető. A struktúrák alapjainak megismerése után nézzük meg, milyen részekre oszthatjuk fel a Linux-kernelt. (Ez a felosztás vázlatos, a könnyebb érthetőség érdekében nem tér ki a rendszer minden részletére.)



Felhasználói processzek



Fejlesztői könyvtárak (pl glibc)



Kernel Rendszerhívások

Processzkezelő Állományrendszerek

Ütemező Hálózati réteg

Memóriakezelő

Egyéb alrendszerek

)

IPC

Perifériák kezelése

Hardver 2.1. ábra. A kernel vázlatos felépítése

A felhasználói programok a rendszerhívásokon keresztül kérhetik a kerneltől a kívánt szolgáltatásokat. Ezeket a rendszerhívásokat a programok általában a rendszerkönyvtárak segítségével érik el. A fájlrendszerek magasabb szintű absztrakciót nyújtanak a perifériák kezelésére. Ennek az alrendszernek a segítségével kezelhetünk állományokat, könyvtárakat az egyes eszközökön (ilyen állományrendszerek az ext4, a proc, az MSDOS, az NTFS, az iso-9660, az NFS stb.), de a fájlkezeés műveleteivel férhetünk hozzá például a soros portokhoz is. A hálózati réteg a különböző hálózati protokollok implementációját tartalmazza (IPv4, IPv6 IPX, Ethernet stb.).

12

2.2. A Linux elindulása

A perifériakezelő alrendszer az eszközök alacsony szintű kezelését valósítja meg. Hozzátartozik a háttértárolók, az I/O eszközök, a soros/párhuzamos és az egyéb portok kezelése. A folyamatkezelő alrendszer több, a processzek kezelésével kapcsolatos funkciót valósít meg: •

A Linux többfeladatos (multitask) rendszer, ez azt jelenti, hogy több processzt futtat párhuzamosan (többprocesszoros rendszer) vagy kvázi párhuzamosan, váltogatva (egyprocesszoros rendszer). Azt, hogy az egyes processzek mikor jussanak a processzorhoz, az ütemező dönti el.



A processzeknek általában szükségük van arra, hogy egymással kommunikáljanak. Ezt az IPC- (Interprocess Communication, processzek közötti kommunikáció) alrendszer teszi lehetővé.



A fizikai memória kiosztását a processzek között a memóriakezelő alrendszer végzi el.

A következő fejezetekben főként a processzkezelő alrendszer egyes részeivel, az ütemezés működésével, illetve a memóriakezelés elméletével ismerkedünk meg. Az IPC-alrendszerre nem térünk ki; lehetőségeit, használatát később tárgyaljuk.

2.2. A Linux elindulása Egy operációs rendszer betöltődése, elindulása első pillantásra mindig kicsit rejtélyes dolognak tűnik. Általában ha egy, a háttértárolón lévő programot szeretnénk betölteni, lefuttatni, akkor beírjuk a nevét a parancsértelmezőbe, vagy rákattintunk az ikonjára, és az operációs rendszer elindítja. Ám hogyan történik mindez, amikor a gép indulásakor magát az operációs rendszert szeretnénk betölteni, elindítani? A Linux-kernel betöltését és elindítását az úgynevezett kernelbetöltő végzi el. Ilyen program a LILO (The Linux Loader), a LOADLIN, a Grub és még sorolhatnánk. A program betöltéséhez szükség van egy kis hardveres segítségre is. Általában a gép csak olvasható memóriájában van egy kis program (az x86-os architektúra esetében ennek a neve BIOS vagy (U)EFI), amely megtalálja, betölti és futtatja ezt a kernelbetöltőt. Vagyis összegezve: egy rövid, beégetett program lefut, és elindít egy valamivel nagyobb betöltőprogramot. Ez a betöltőprogram pedig elindít egy még nagyobb programot, nevezetesen az operációs rendszer kernelprogramját. 9

9

Lehetőség van arra, hogy a betöltőprogramokat még tovább láncoljuk. Ezáltal lehetővé válik a több operációs rendszert tartalmazó gépeken, hogy induláskor kiválaszthassuk a számunkra szükségeset. 13

2. fejezet: Betekintés a Linux-kernelbe

A kernel manapság általában tömörített formában van a lemezeken, és képes önmagát kitömöríteni. Így az első lépés a kernel kitömörítése, majd a kód feldolgozása a kitömörített kernel kezdőcímétől folytatódik. Ezt követi a hardver inicializálása (memóriakezelő, megszakítástáblák stb.), majd az első C-függvény (start_kernel()) meghívása. Ez a függvény, amely egyben a 0-s azonosítójú processz belépési pontja, inicializálja a kernel egyes részeit. Az inicializáció végén a 0-s processz elindít egy kernelszálat (a neve ínit), majd egy üresjárati ciklusba (idle loop) kezd, így a továbbiakban a 0-s processz szerepe már elhanyagolható. Az ínit kernelszálnak vagy processznek a processzazonosítója az 1. Ez a rendszer első igazi processze. Elvégez még néhány beállítást (elindítja a fájlrendszert szinkronizáló és lapcserekezelő folyamatokat, feléleszti a rendszerkonzolt, felcsatolja [mount] a gyökér-állományrendszert [root file system]), majd lefuttatja a rendszerinicializáló programot (nem keverendő a korábban említett processzel). Ez a program az adott disztribúciótól függően a követkevalamelyike: /etc/init, /bin/init, /sbin/init.10 Az ínit program az /etc/ izők ntab1 konfigurációs állomány segítségével új, immár felhasználói 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 mind az ínit kernelszál leszármazottai.

2.3. Processzek A processz egy működés közbeni program. Ebből adódóan a programkódot és a hozzátartozó erőforrásokat tartalmazza. A processzek meghatározott feladatokat hajtanak végre az operációs rendszeren belül. A feladat leírása az a program, amely gépi kódú utasítások és adatok együtteséből áll. Ezeket a lemezeken tároljuk, így a program önmagában passzív entitás. Ezzel szemben a processz már dinamikus entitás. Folyamatosan változik, ahogy a processzor egymás után futtatja az egyes utasításokat. A program kódja és adatai mellett a processz tartalmazza a programszámlálót, a CPUregisztereket, továbbá a processz vermét, amely az átmeneti adatokat (függvényparaméterek, visszatérési címek, elmentett változók) tárolja.

10

11

Ha egyik helyen sem találja meg a rendszer az ínit programot, akkor megpróbálja feldolgozni az /etc/rc állományt, és elindít egy shellt, hogy a rendszergazda megjavíthassa a rendszert. A Linux rendszereken elterjedőben van az eseményalapú, upstart nevű ínit implementáció. Ez a megoldás a hagyományos System V ínit programhoz képest eltérő konfigurációs megoldásokat alkalmaz.

14

2.3. Processzek A Linux többfeladatos (multitask) operációs rendszer, a processzek saját jogokkal rendelkező elkülönített feladatok (task), amelyeket a Linux párhuzamosan futtat. Egy processz összeomlása nem okozza a rendszer más processzeinek az összeomlá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 ez alól a biztonságos, kernel által kezelt mechanizmusok, amelyekkel a processzek magvalósíthatják az egymás közötti kommunikációt. Életciklusa során egy processz számos rendszererőforrást használhat (CPU, memória, állományok, fizikai eszközök stb.). A Linux feladata az, hogy ezeket a hozzáféréseket könyvelje, kezelje, és igazságosan elossza a konkuráló processzek között.

2.3.1. A Linux-processzekhez kapcsolódó információk A Linux minden processzhez hozzárendel egy leíró adatstruktúrát, 12 az ebben szereplő adatok jellemzik az adott processzt, és befolyásolják a működését. Ez a feladatokat leíró adatstruktúra nagy és komplex, ám felosztható néhány funkcionális területre:

1,



Állapot A processz a futása során a körülményektől függően különböző állapotokba kerülhet, ezeket az állapotokat a 2.3.2. alfejezetben tárgyaljuk.



Azonosítók A rendszerben minden processznek van egyedi azonosítója. Ezen túl minden processz rendelkezik felhasználói és csoportazonosítókkal. Ezek szabályozzák az állományokhoz és az eszközökhöz való hozzáférést a rendszerben. (Bővebben lásd a 2.3.3. alfejezetben.)



Kapcsolatok A Linuxban egyetlen processz sem független a többitől. Az ínit processzt leszámítva minden processznek van szülője. Az új processzek nem létrejönnek, hanem a korábbiakból másolódnak, klónozódnak. Minden processzleíró adatstruktúra tartalmaz hivatkozásokat a szülőprocesszre és a leszármazottakra. Ezt a kapcsolódási fát a pstree paranccsal nézhetjük meg.



Ütemezési információ Az ütemezőnek szüksége van bizonyos információkra: prioritás, statisztikai információk stb., hogy igazságosan dönthessen, hogy melyik processz kerüljön sorra. Az ütemezést a 2.3.6. alfejezet tárgyalja.

A leíró adatstruktúra típusa megtalálható a kernelforrásban. A neve task_struct, és az include/linux/sched.h állományban található.

15

2. fejezet: Betekintés a Linux-kernelbe

16



Memóriainformációk A processz memóriafoglalásával kapcsolatos információk (kód-, adat-, veremszegmensek) tartoznak ide.



Fájlrendszer A processzek megnyithatnak és bezárhatnak állományokat. A processzleíró adatstruktúra tartalmazza az összes megnyitott állomány leíróját, továbbá az aktuális, úgynevezett munkakönyvtárának (working directory) a mutatóját.



Processzek közötti kommunikáció A Linux támogatja a klasszikus Unix IPC-mechanizmusokat (jelzések, csővezetékek, szemaforok) és a System V IPC-mechanizmusokat is (megosztott memória, szemafor, üzenetsor). Egy kifejezetten a Unix operációs rendszerekre jellemző processzek közti aszinkron kommunikációs forma a jelzés (signal). A jelzést küldő processz nem várakozik a kézbesítésre, küldés után folytatja futását. Jelzés érkezésekor a fogadó processz normál futása megszakad, és vagy a processz által megadott, vagy az alapértelmezett jelzéskezelő függvény fut le. Ezek után a processz ott folytatja a futását, ahol abbahagyta. A futás megszakítását, a jelzéskezelő futtatását, majd a futás folytatását a kernel teljes mértékben kezeli. A jelzéshez tartozik egy egész szám, amely a jelzés kiváltásának okát adja meg. Ezt az egész számot szimbolikus konstanssal adjuk meg, értéke a signal.h állományban található. A szimbolikus konstansok SIG előtaggal kezdődnek. Például nullával való osztás esetén a kernel SIGFPE jelzést küld a műveletet futtató processznek, illetve akkor, ha egy processz futását azonnal meg szeretnénk szakítani, SIGKILL jelzést küldünk neki.



Idő és időzítők A kernel naplózza a processzek létrehozási és CPU-felhasználási idejét. A Linux támogatja továbbá az intervallumidőzítők használatát a processzekben, amelyeket beállítva jelzéseket kaphatunk bizonyos idő elteltével. Ezek lehetnek egyszeri vagy periodikusan ismétlődő értesítések. (Bővebben lásd a 2.3.8. alfejezetben.)



Processzorspecifikus adatok (Context) A folyamat a futása során használja a processzor regisztereit, a vermet stb. Ez a processz környezete, és amikor feladatváltásra kerül sor, ezeket az adatokat le kell menteni a processzt leíró adatstruktúrába. Amikor a processz újraindul, innen állítódnak vissza az adatok.

2.3. Processzek

2.3.2. A processz állapotai A futás során a processzek különböző állapotokba juthatnak. Az állapot függhet a processz aktuális teendőjétől és a külső hatásoktól. A processz aktuális állapotát a processzhez rendelt leíró struktúra állapotleíró változója tárolja. Linux alatt egy processz lehetséges állapotai a következők: •

A processz éppen fut az egyik processzoron. (Az állapotváltozó értéke:

RUNNING.) •

A processz futásra kész, de másik foglalja a processzort, ezért várakozik a listában. (Az állapotváltozó értéke ilyenkor is: RUNNING.)



A processz egy erőforrásra vagy eseményre várakozik. Ilyenkor attól függően kap értéket az állapotváltozó, hogy a várakozást megszakíthatja egy jelzés (INTERRUPTABLE), vagy sem (UNINTERUPTABLE). A jelzéssel nem megszakítható állapot egyik alesete az, amikor a kritikus, a folyamat leállását eredményező jelzések még megszakíthatják a várakozást (KILLABLE).



A processz felfüggesztve án, általában a SIGSTOP jelzés következtében. Ez az állapot hibajavításkor jellemző, de a terminálban futtatott folyamatnál a CTRL + Z billentyű kombinációval szintén elérhetjük. (Az állapotváltozó értéke: STOPPED.)



A processz kész az elmúlásra (már nem élő, de még nem halott: zombi), de valami oknál fogva még mindig foglalja a leíró adat struktúráját. (Az állapotváltozó értéke: ZOMBIE.)

Az állapotok kapcsolatait a 2.2. ábra szemlélteti:

Esemény bekövetkezik

Eseményre vár Futás

Létrehozás

vége

Jelzés

Jelzés

Megszűnik 2.2.

ábra.

A processz állapotatmenet - diagramja

17

2. fejezet: Betekintés a Linux-kernelbe

A processzeket a fenti állapotdiagram szerint az ütemező (scheduler) kezeli. Az ütemezési algoritmusokat és a processzekhez tartozó adatterületeket a 2.3.6. alfejezetben tárgyaljuk bővebben.

2.3.3. Azonosítók A Linux-kernel minden processzhez egy egyedi azonosítót rendel: pid (process ID). Később ezzel az azonosítószámmal hivatkozhatunk a processzre. Minden folyamat egy processzcsoport tagja, amelynek azonosítóját a pgid (process group ID) mező tartalmazza. Amikor a folyamat létrejön, a szülőfolyamat pgid azonosítóját örökli, ám ez később módosítható. A processzcsoport arra használható, hogy tagjainak egyszerre küldjünk jelzéseket, vagy valamelyik tagjának befejeződésére várakozzunk. A konvenció szerint a pgid azonosító számértéke a csoport első tagjának pid értékével egyezik meg. Új csoportot is úgy tudunk létrehozni, ha a pgid értékét a folyamat pid értékére állítjuk. Ekkor a folyamatunk a csoport vezetője lesz. A csoport vezetőjének szerepe annyiban speciális, hogy ha véget ér, akkor a csoport többi tagja egy SIGHUP jelzést kap. (Bővebben lásd az 5.6. Jelzések című alfejezetben.) A jelzés hatására a folyamatok dönthetnek arról, hogy leállnak-e (alapértelmezett), vagy folytatják a futásukat. Több processzcsoport összekombinálható egy munkamenetté (session). Minden folyamatnak a munkameneten belül azonos a sessionid értéke. Linux alatt a szálak speciális folyamatoknak számítanak. Ezért fontos a rendszer számára annak könyvelése, hogy mely szálak tartoznak egybe, egy folyamathoz. Az összetartozó szálakat a szálcsoportok tartalmazzák. A folyamat szálcsoport-azonosítója a tgid. 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 az ellenőrzésére. A Linux rendszerben minden állománynak van tulajdonosa és jogosultsági beállításai. A legegyszerűbb jogok a read, a write és az execute. Ezeket rendeljük hozzá a felhasználók három osztályához: ezek a tulajdonos, a 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. Ebből következően a processzek az alábbi azonosítókkal rendelkeznek:

18



uid, gid A felhasználó felhasználói és csoportazonosítói, amelyekkel a processz fut.



Effektív uid és gid Ezeket az azonosítókat használja a rendszer annak vizsgálatára, hogy a processz hozzáférhet-e az állományhoz. Különbözhetnek a valódi felhasználótól és csoporttól. Ezeket a programokat nevezzük setuidos,

2.3. Processzek

illetve csoportállítás esetén setgides programoknak. Segítségükkel korlátozott hozzáférést nyújthatunk a rendszer egyes védett, csak a rendszergazda számára hozzáférhető részeihez. •

Fájlrendszer-uid és gid Ezek normál esetben megegyeznek a valódi azonosítókkal, és a fájlrendszerhez való hozzáférési jogosultságokat szabályozzák. Elsősorban az NFS-fájlrendszereknél 13 van rájuk szükség, amikor a felhasználói üzemmódú NFS-szervernek különböző állományokhoz kell hozzáférnie az adott felhasználó nevében. Ebben az esetben csak a fájlrendszer-azonosítók változnak.



Mentett uid és gid 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ésőbb visszaállíthatók legyenek.

-

2.3.4. Processzek létrehozása és terminálása Új processzt egy korábbi processz lemásolásával hozhatunk létre. Ez a fork rendszerhívással valósítható meg. A másolat teljesen megegyezik az eredetivel, csak a processzazonosítóban különböznek. Az eredeti processzt szülőprocessznek, a másolatot, a leszármazottat gyerekprocessznek nevezzük. Természetesen, ha később az egyes processzek módosítanak a belső változóikon, akkor ez a másik processznél már nem érvényesül. A valóságban a gyerekfolyamat létrehozásakor nem történik tényleges másolás. Helyette a Linux a COW metódust használja (lásd később a 2.4.7. alfejezetben). Ha egy program egy másik programot akar futtatni, akkor sincs más út: a processznek le kell másolnia magát, és a gyerekprocessz tölti be a futtatandó másik programot. Ezért gyakori az a forgatókönyv, hogy az új folyamat létrehozása után egy rendszerhívással azonnal betöltünk egy új programot, és azt futtatjuk. Ilyenkor az eredeti folyamat memóriájának a lemásolása felesleges. Erre az esetre szolgál a vfork függvény, amelyben a memória másolása helyett a rendszer megosztja a memóriát a szülő és a gyerek között addig, amíg a gyerek betölt egy új programkódot. Erre az átmeneti időre a szülőfolyamat blokkolódik. Amióta a Linux a COW metódust használja a fork rendszerhívásnál, azóta a vfork előnye valójában már elhanyagolható, így nem jellemző a használata.

13

Az NFS (Network File System) elsősorban Unix rendszereknél használt hálózati fájlrendszer. 19

2. fejezet: Betekintés a Linux-kernelbe

A Linux mind a fork, mind a vfork rendszerhívást valójában a clone rendszerhívással valósítja meg. A clone létrehoz egy új folyamatot a szülőfolyamatból, és lehetővé teszi, hogy megadjunk, mely memóriaterületeken osztozzon a két folyamat. Mint látható, a fork és a vfork függvények valójában a clone specializált esetei. A clone teszi lehetővé a kernelszintű szálkezelés implementációját is Linux alatt. 14 A gyerekfolyamat a szülő teljes mása, csak a pid és a ppid értékekben tér el tőle. Ezen kívül nem öröklődnek még az állományzárolások és az aktuális jelzések. Ha a gyerekfolyamat a programkódjának a végére ért, vagy termináló jellegű jelzést kap, akkor ezt a tényt egy SIGCHLD jelzéssel jelzi a szülő számára, befejezi a futását, és zombi állapotba kerül. Addig zombi állapotban marad, amíg a szülőjének átadja az eredményét. Ameddig a szülő ezt nem veszi át tőle, addig a gyerekfolyamat zombi állapotban várakozik. Felvetődik a kérdés, mi történik akkor, ha a szülő fejezi be hamarabb a futását, és nem a gyerek. A gyerek ilyenkor árva lesz, és az ínit veszi át a szülő szerepét. Amikor a gyerekfolyamat végzett, akkor az ínit átveszi tőle a viszszatérési értékét, így teljesen megszűnhet.

2.3.5. A programok futtatása A Linuxban, mint a többi Unix rendszerben, a programokat és a parancsokat általában a parancsértelmező futtatja. A parancsértelmező egy felhasználói processz, amelynek a neve shell. Linuxos rendszereken több parancsértelmező közül választhatunk (sh, bash, tcsh stb.). A parancsértelmezők tartalmaznak néhány beépített parancsot. A többi begépelt utasítást mint programnevet értelmezik. A parancsként megadott állománynévvel keresnek egy futtatható állományt a PATH környezeti változó által megadott könyvtárakban. Ha megtalálják, akkor betöltik és lefuttatják. A parancsértelmező a fent említett fork metódussal lemásolja magát, és a megtalált állomány az új, leszármazott processzben fut. Normál esetben a parancsértelmező megvárja a gyerekprocessz futásának a végét, és csak ezután adja vissza a vezérlést a felhasználónak, de lehetőség van a processzt a háttérben is futtatni. A futtatható fájl többféle bináris vagy szöveges parancsállomány (script) lehet. A parancsállományt az első sorában megadott program (amely általában egy parancsértelmező), ennek hiányában az éppen használt parancsértelmező értelmezi.

14

Mint látható, Linux alatt a szálak nincsenek szigorúan megkülönböztetve a processzektől . A szálak valójában közös memórián osztozó processzek saját processzazonosítóval.

20

2.3. Processzek

A bináris állományok információkat tartalmaznak az operációs rendszer számára, hogy értelmezni és futtatni tudja őket, továbbá tartalmazzák a programkódot és az adatokat. A Linux alatt a leggyakrabban használt bináris formátum az ELF. 15 A támogatott bináris formátumokat a kernel fordításakor választhatjuk ki, vagy utólag modulként illeszthetjük be az értelmezésüket. Az általánosan használt formátumok az a.out, az ELF, de például a Java class fájlok felismerését is bekapcsolhatjuk.

2.3.6. Ütemezés A Linux rendszerek párhuzamosan több folyamatot futtathatnak. Ritka az az eset, amikor a processzorok számát nem haladja meg a folyamatok száma. Ezért, hogy az egyes processzek többé-kevésbé párhuzamosan futhassanak, az operációs rendszernek folyamatosan váltogatnia kell, hogy melyik processz kapja meg a processzort. Az ütemező feladata az, hogy szabályozza a proceszszoridő kiosztását az egyes folyamatok számára. Ahhoz, hogy az ütemezés felhasználói szempontból kényelmes legyen, szükség van arra, hogy a felhasználó az egyes folyamatok prioritását szabályozhassa, és az ő szemszögéből nézve a processzek párhuzamosan fussanak. A rossz ütemezési algoritmus az operációs rendszert döcögőssé, lassúvá teheti, az egyes processzek túlzott hatással lehetnek egymásra a processzoridő elosztása során. Ezért a Linux fejlesztői nagy figyelmet fordítottak az ütemező kialakítására. A processzek váltogatásával kapcsolatban két kérdés merül fel. •

Mikor cseréljünk le egy processzt?



Ha már eldöntöttük, hogy lecserélünk egy processzt, melyik legyen az a processz, amelyik a következő lépésben megkaphatja a CPU-t?

Ezekre a kérdésekre adunk választ a továbbiakban.

2.3.6.1. Klasszikus ütemezés A klasszikus Linux-ütemezés időosztásos rendszert használ. Ez azt jelenti, hogy az ütemező az időt kis szeletekre osztja (time slice), és ezekben az időszeletekben valamilyen kiválasztó algoritmus alapján adja meg az egyes processzeknek a futás lehetőségét. Ám az egyes processzeknek nem kell kihasználniuk az egész -

15

Az ELF (Executable and Linkable Format, futtatható és linkelhető formátum) bináris formátumot eredetileg a System V Release 4 UNIX-verzióban vezették be. Később a Linux fejlesztői is átvették, mert a belső felépítése sokkal flexibilisebb, mint a régebbi a.out formátum. Ezen kívül egy hatékony debug formátuma is létezik, amelynek neve DWARF (Debugging With Attribute Record Format), amely dinamikusan egy láncolt listában tárolja a hibakereséshez szükséges információt. 21

2. fejezet: Betekintés a Linux-kernelbe

időszeletet, akár le is mondhatnak a processzorról, ha éppen valamilyen rendszereseményre kell várakozniuk. Ezek a várakozások a rendszerhívásokon belül vannak, amikor a processz éppen kernelüzemmódban fut. 16 A Linuxban a nem futó processzeknek nincs előjoga az éppen futó processzekkel szemben, nem szakíthatják meg a futó folyamatokat annak érdekében, hogy átvegyék a processzort. Vagyis az időszeleten belül csak a futó folyamatok mondhatnak le a processzorról önkéntes alapon, és adhatják át egymásnak a futás lehetőségét. A fenti két alapelv gondoskodik arról, hogy megfelelő időpontban következzen be a váltás. A másik feladat a következő processz kiválasztása a futásra készek közül. Ezt a választási algoritmust a kernelen belül a schedule() függvény implementálja. A klasszikus ütemezés három stratégiát (policy) támogat: a szokásos Unix ütemezési módszert és két, valós idejű (real-time) processzek ütemezésére szolgáló algoritmust. A valós idejű processz a szokásos értelmezés szerint azt jelentené, hogy az operációs rendszer garantálja, hogy az adott processz egy megadott, rövid időn belül megkapja a processzort, amikor szüksége van rá, így reagálhat a külső eseményekre. Ezt hívjuk „hard real-time"-nak. Ám a Linux csak egy úgynevezett „soft real-time" megközelítést támogat, amely a valós idejű processzeket a kiválasztás során előre veszi, így a lehetőség szerinti legkisebb késleltetéssel juttatja processzorhoz. Míg a klasszikus ütemezésnél jelentős az eltérés a Linux soft real-time megoldása és egy hard real-time rendszer között, a manapság használatos megoldásoknál már nem olyan éles a határvonal. (Ezt a témakört részletesen lásd a 2.3.8. alfejezetben.) A Linux a következő futtatandó processz kiválasztásához prioritásos ütemezőalgoritmust használ. A normál processzek két prioritásértékkel rendelkeznek: statikus és dinamikus prioritással. A valós idejű processzekhez a Linux tárol még egy prioritásértéket is, a valós idejű prioritást (real time priority). Ezek a prioritásértékek egyszerű egész számok, amelyeknek a segítségével a kiválasztóalgoritmus súlyozza az egyes processzeket. • A statikus prioritás A névben szereplő statikus szó jelentése az, hogy értéke nem változik az idő függvényében, csak a felhasználó módosíthatja. A processzinformációk között a neve nice, amely arra utal, hogy az ütemező milyen „kedves" lesz a processzel, és mennyire kínálja processzoridővel.

16

Minden processz részben felhasználói üzemmódban, részben kernelüzemmódban fut. A felhasználói üzemmódban jóval kevesebb lehetősége van a processznek, mint kernelüzemmódban, ezért bizonyos erőforrások, rendszerszolgáltatások igénybevételéhez át kell kapcsolnia. Ilyenkor az átkapcsoláshoz a processz egy rendszerhívást hajt végre (a Linux felhasználói kézikönyv [manual] 2. szekciója tartalmazza ezeket, például: read()). Ezen a ponton a kernel futtatja a processz további részét.

22

2.3. Processzek



A dinamikus prioritás A dinamikus prioritás lényegében egy számláló, értéke a processz futásának függvényében változik. Segítségével az ütemező nyilvántartja, hogy a processz mennyi futási időt használt el a neki kiutaltból. Ha például egy adott processz sokáig nem jutott hozzá a CPU-hoz, akkor a dinamikus prioritási értéke magas lesz. A processzinformációk között a neve counter, vagyis számláló.



A valós idejű prioritás Jelzi, hogy a processz valós idejű, így minden normál processzt háttérbe szorít a választáskor. További funkciója, hogy a valós idejű processzek közötti prioritásviszonyt megmutassa. A processzinformációk között a neve rt_priority.

Normál processzek esetében a Linux ütemezési algoritmusa az időt korszakokra (epoch) bontja. A korszak elején minden processz kap egy meghatározott mennyiségű időegységet, vagyis a kernel a számlálóját egy, a statikus prioritásából meghatározott értékre állítja Amikor egy processz fut, ezeket az időegységeket használja el, vagyis a számlálója csökken. Ha elfogyott az időegysége, akkor már csak a következő ciklusban futhat legközelebb. Egy korszaknak akkor van vége, amikor minden RUNNING állapotú processznek elfogyott az időegysége (tehát a várakozó processzek nem számítanak). Ilyenkor új korszak kezdődik a processzek életében. Ezzel a módszerrel elérhető, hogy ésszerű időn belül minden processz kapjon több-kevesebb futásidőt, vagyis egyiket se éheztessük ki. A FIFO valós idejű ütemezés esetén csak a valós idejű prioritásnak van szerepe az ütemezésben. A legnagyobb prioritású processzek közül a sorban a legelső futhat egészen addig, amíg át nem adja másnak a futás lehetőségét (várakoznia kell, vagy véget ért), vagy nagyobb prioritású processz nem igényli a processzort. A körbeforgó (round-robin) valós idejű ütemezési algoritmus a FIFO továbbfejlesztett változata. Működése hasonlít a FIFO ütemezéshez, ám egy processz csak addig futhat, amíg a neki kiutalt időegysége el nem fogy, vagyis a számlálójának az értéke el nem éri a 0-t. Ilyenkor a sor végére kerül, és a rendszer megint kiutal számára időegységeket. Ezáltal lehetővé teszi a CPU igazságos elosztását az azonos valós idejű prioritással rendelkező processzek között.

2.3.6.2. Az 0(1) ütemezés Az 0(1) ütemezés a 2.6-os kernellel együtt jelent meg. A Java virtuális gépek miatt volt rá szükség a sok párhuzamosan futó szál következtében. Mivel a korábbi ütemezőalgoritmus futási ideje egyenes arányban állt a processzek/ szálak számával, ezért nagy mennyiségű szál esetén a hatékonysága jelentősen csökkent. Az 0(1) ütemező erre a problémára jelentett megoldást, mivel az ütemezési algoritmus futásideje nem növekszik a processzek számával. Ezt a Linux úgy oldja meg, hogy prioritási szintekbe rendezi a processzeket, amely 23

2. fejezet: Betekintés a Linux-kernelbe

egy kétszeresen láncolt lista. Egy bittérkép alapján az ütemező nagyon gyorsan meg tudja találni azt a legmagasabb prioritási szintet, ahol processz várakozik. Ezután az ezen a prioritási szinten elhelyezkedő első processzt kell futtatnia. A bittérkép mérete csak a prioritási szintek számától függ, a processzek számától nem.

2.3.6.3. Teljesen igazságos ütemező A teljesen igazságos ütemező (Completely Fair Scheduler, CFS) a 2.6.23-as kerneltől kezdődően az általános kernel alapértelmezett ütemezőalgoritmusa. A klasszikus ütemezési algoritmushoz képest az egyik legjelentősebb eltérése, hogy a futási idő arányára koncentrál. A CFS olyan körkörös prioritású ütemező, amely a processzek időszeletének arányát próbálja igazságossá tenni. Ideális esetben, ha van N darab processzünk, mindegyik a rendelkezésre álló idő 1 /N-ed részében fut. Ezt a számítást egy adott időtartamra kalkuláljuk ki, a neve céllappangási idő (target latency). Ha ez az idő 30 ms, és három processzünk van, N = 3, mindegyik processzre 1/3 arányban jut idő, vagyis mindegyik 10 ms-ig fut. A processzek közötti váltás időigénye elhanyagolható. Ha azonban túl sok processz fut a rendszerben, tételezzük fel, hogy 100, akkor 0,3 ms-onként kellene váltani, ez pedig már nem kifizetődő. Ezért ennek az algoritmusnak van egy minimális felbontása (minimum granularity), amely alá nem megy az ütemezés. Ha ez az érték 1 ms, akkor mind a 100 processz 1 ms-ig fut. Természetesen ebben az esetben az algoritmus már nem is nevezhető igazságosnak még az időarány tekintetében sem. A nice értékek abszolút értéke nem számít, a relatív értékek alapján az algoritmus egy arányszámot számol. Két processz esetén nulla, és egy 5 nice értékű ugyanolyan arányban fut, mint egy 10 és 15 értékű. Az algoritmus implementációja azt az időt tárolja, amennyit a processz legutoljára futott, pontosabban ezt még elosztja a processzek számával. Ezt az értéket virtuális futásidőnek (virtual runtime) nevezzük, amely nanoszekundum felbontású. Ezt az értéket az ütemező folyamatosan karbantartja. Ideális esetben ez az érték ugyanannyi lenne minden azonos prioritású processz esetében. Egy valós processzornál ezt úgy lehet ellensúlyozni, hogy mindig a legkisebb virtuális futásidejű processzt futtatjuk. A CFS is pontosan így tesz. Ahhoz, hogy megtalálja a legkisebb értéket, a várakozási listát egy teljesen kiegyensúlyozott piros-fekete bináris fában tárolja. Ennek a leggyakrabban használt bal oldali elemét gyorsítótárazza is a hatékonyság érdekében. Az ütemezés ennél az algoritmusnál O(log n) komplexitású, ám tényleges értéke kisebb, mint a korábbi ütemezőnél volt. A taszkok váltása konstans idő, viszont a leváltott taszk beillesztése a bináris fába O(log n) időt igényel. Az ütemező további újítása a korábbakkal szemben, hogy az ütemezési stratégiák implementációja moduláris felépítésű lett. Így szabadon bővíthető további algoritmusokkal. Ugyanakkor tartalmaz egy új stratégiát is, amelynek neve „batch". Ez lehetővé teszi olyan alkalmazások futtatását, amelyeket

24

2.3. Processzek

hosszabban érdemes futtatni a processzoron, ugyanakkor nem igénylik a gyakori meghívást, mert nem kell felhasználói eseményekre reagálniuk. Ezek többnyire szerveralkalmazások. A CFS-ütemezőben is az történik, hogy a valós idejű folyamatok megelőzik a normál ütemezésűeket. Ugyanakkor, mivel nincsenek időszeletek, ezért a rendszer szinte azonnal válthat, ha egy valós idejű folyamat futásra késszé válik. A gyorsítótárazás miatt a taszkváltás is rövid idő alatt végrehajtódik.

2.3.6.4. Multiprocesszoros ütemezés Szimmetrikus multiprocesszoros (SMP) környezetben az ütemezési metódust kicsit módosítani kell. Ilyenkor minden processzor külön, saját magának futtatja az ütemező funkciót, ám a processzoroknak célszerű információkat cserélniük a rendszer teljesítményének a növelése érdekében. Amikor az ütemező kiszámolja az adott processz súlyozását, azt is figyelembe kell vennie, hogy korábban a processz ugyanazon a processzoron futott-e, vagy egy másikon. Azok a processzek, amelyek az adott CPU-n futottak, mindig előnyt élveznek, mivel a CPU-hardver gyorsítótára még mindig tartalmazhat rájuk vonatkozó információkat. Ezzel a módszerrel az operációs rendszer növelni tudja a találatok számát a gyorsítótárakban. Ezzel kapcsolatban azonban felvetődik egy probléma. Tegyük fel, hogy az ütemező találkozik azzal az esettel, hogy egy processz prioritása nagyobb, mint a többié, ám korábban egy másik processzoron futott. Ilyenkor vagy elveszítjük gyorsítótárakban visszamaradó információk lehetőségét, vagy nem használjuk ki az SMP-architektúrából adódó lehetőséget, hogy a ráérő processzoron futtassuk a processzt. A Linux-SMP a dilemma feloldására egy adaptív empirikus szabályt alkalmaz. Ez egy olyan kompromisszum, amely függ a processzorok gyorsítótárának a méretétől. Ha a gyorsítótár nagyobb, akkor a rendszer jobban ragaszkodik ahhoz, hogy egy processz mindig ugyanazon a processzoron fusson.

2.3.7. Megszakításkezelés A Linuxban két megszakításkezelő-típust különböztetünk meg. A „gyors" megszakításkezelő letiltja a megszakításokat, ezért gyorsra kell elkészítenünk. A „lassú" megszakításkezelő nem tiltja le a megszakításokat, mert az általa igényelt hosszabb futásidőre ez nem lenne jó ötlet. Azért, hogy a megszakításkezelő rutinok gyorsan lefuthassanak, a Linuxfejlesztők egy olyan megoldást alkalmaznak, amely a feladatot két részre választja szét: • Felső rész (Top half) Ez a tényleges megszakításkezelő rutin. A feladata az, hogy az adatokat gyorsan letárolja az utólagos feldolgozáshoz, majd bejegyezze a másik fél futtatására vonatkozó kérelmét. 25

2. fejezet: Betekintés a Linux-kernelbe



Alsó rész (Bottom hal}) Ez a rész ténylegesen már nem a megszakításkezelőben fut le, hanem utána kicsivel. A komolyabb, időigényesebb számításokat itt végezzük el. Technikailag két eszköz közül választhatunk az alsó rész implementációja során: kisfeladat (Tasklet), munkasor (Work queue).

2.3.8. Valósidejűség Jelenleg már aránylag kevés eltérés van a normál és a valós idejű kernel között. Így nem kritikus helyeken a normál kernelt is nyugodtan választhatjuk a beágyazott rendszereinkhez Nézzük meg, melyek azok az elemek, amelyek mindezt lehetővé teszik. •

Mint láthattuk, a CFS-ütemező nanoszekundum felbontású, így nagyon gyors reakciót tesz lehetővé, és a taszkváltás is konstans időt igényel.



A kernelszálak a 2.6-os kernelben megszakíthatók, így egy hosszabb művelet sem tudja lefogni a processzort.



A szinkronizálásokat optimalizálták a kernelben, hogy a lehető legkevésbé akadályozzák egymást a futó processzek.

A valós idejű (RT) kernelt a normál kernelből egy javítófolt- (patch) halmaz segítségével állíthatjuk elő. A valós idejű kernel az alábbi további funkciókkal rendelkezik: •

Tartalmaz egy direkt hozzáférési lehetőséget a fizikai memóriához.



Tartalmaz néhány memóriakezelésbeli módosítást.



A gyenge pontok felderítésére pedig tartalmaz egy késleltetésmonitorozó eszközt (Latency tracer).

2.3.9. Idő és időzítők A kernel könyveli a processzek létrehozási időpontját, és az életük során felhasznált CPU-időt. Ezeknek az időknek a mértékegysége történelmileg a jiffy (pillanat), amelynek normál mértékegységben értelmezett értéke a kernel beállításától függ. A rendszer könyveli a processznek a kernel-, illetve felhasználói üzemmódban töltött idejét. Ezek mellett a könyvelések mellett a Linux támogatja az intervallumidőzítőket is. A processz ezeket felhasználhatja, hogy különböző jelzéseket küldessen magának, amikor lejárnak. Háromféle intervallumidőzítőt különböztetünk meg:

26

2.4. Memóriakezelés



Real Az időzítő valós időben dolgozik, és lejártakor egy SIGALRM jelzést küld.



Virtual Az időzítő csak akkor működik, amikor a processz fut, és lejártakor egy SIGVTALRM jelzést küld.



Profile Az időzítő egyrészt a processz futási idejében működik, másrészt akkor, amikor a rendszer a processzhez tartozó műveleteket hajt végre. Lejártakor egy SIGPROF jelzést küld. Elsősorban arra használják, hogy lemérjék, a processz mennyi időt tölt a felhasználói, illetve a kernelüzemmódban.

Egy vagy akár több időzítőt is használhatunk egyszerre. Beállíthatjuk őket egy-egy jelzésre vagy ismétlődőre is. A Linux kezeli az összes szükséges információt a processz adatstruktúrájában. Rendszerhívásokkal konfigurálhatjuk, indíthatjuk, leállíthatjuk és olvashatjuk őket.

2.4. Memóriakezelés A memória a CPU után a másik legfontosabb erőforrás, így a memóriakezelő alrendszer az operációs rendszerek egyik legfontosabb eleme.

2.4.1. A virtuálismemória-kezelés A kezdetek óta gyakran felmerül a probléma, hogy több memóriára van szükségünk, mint amennyit a gépünk fizikailag tartalmaz. Ugyanakkor a rendelkezésünkre áll a merevlemez, amely ugyan lassabban kezelhető, de nagy kapacitással rendelkezik. Ezért jó lenne, hogy amikor kifutunk a szabad memóriaterületből, akkor a memória egyes, nem használt részeit átmenetileg a merevlemezre helyezhetnénk, így felszabadulhatna a szükséges hely a programok számára. A virtuálismemória-kezelés az a módszer, amely ezt biztosítja számunkra• a memória és a merevlemez felhasználásával nagyobb memóriaterület elérését teszi lehetővé a processzeknek, mint amennyi szabad fizikai memória valójában rendelkezésünkre áll. Mindezt ráadásul úgy oldjuk meg, hogy a processz számára az egész memóriaterület egységesen kezelhető és átlátszó legyen.

27

2. fejezet: Betekintés a Linux-kernelbe

2.4.2. Lapozás A virtuálismemória-kezelés megvalósításának egyik, jelenleg legelterjedtebben használt módszere a memórialapozás technikája. A rendszer memóriáját tartományokra, úgynevezett lapokra (page) osztjuk. Ezeket a lapokat a kernel egymástól függetlenül mozgathatja a memória és a merevlemez között. Természetesen a lapok kezelése többletadminisztrációval jár. A rendszernek nyilván kell tartania, hogy az egyes lapok éppen hol helyezkednek el. Szükség esetén a lapokat ki kell írnia a háttértárolóra, vagy betöltenie, és az egészet le kell képeznie a processz számára használható formára. De a lapozás a memóriakorlátok leküzdésénél többet is nyújt. A lapszervezésű memóriát védelmi célokra is felhasználhatjuk. A rendszerben minden processz saját virtuálismemória-területtel rendelkezik. Ezek a címtartományok teljesen elkülönülnek egymástól, így az egyik futó folyamatnak nem lehet hatása a másikra. Továbbá a hardver virtuálismemória-kezelő mechanizmusa lehetővé teszi, hogy a lapokhoz védelmi attribútumokat rendeljünk, így egyes memóriaterületeket teljesen írásvédetté tehessünk. Nézzük meg, hogyan is működik a lapozás (paging) módszere a gyakorlatban. A 2.3 ábra egy egyszerűsített példát mutat be, amelyben két processz virtuálismemória-területét képezzük le a fizikai memóriára. (A Linux ennél bonyolultabb, többlépcsős leképezést használ, erről később lesz szó.) A processz a működése során folyamatosan használja a memóriát. Ott található a kódja, amelyeket a processzor kiolvas és végrehajt, de ott tárolja az adatait is. Ezek az adatok mind a memória egy-egy tárolóegységében helyezkednek el, amelyekre címekkel hivatkozhatunk. A virtuális memória használatakor ezek a címek mind virtuális címek a virtuális memóriában. (Vagyis minden processznek van egy saját „virtuális birodalma".) Ezeket a virtuális címeket a memóriakezelő egység (Memory Management Unit, MMU) alakítja fizikai címekké. Az MMU a korszerű rendszereknél a processzor része. Az átalakítást az operációs rendszer által karbantartott információs táblák alapján végzi el, amelyeknek neve laptábla. Ennek a folyamatnak a megkönnyítésére a virtuális és a fizikai memória egyenlő méretű (Intel x86-os architektúra esetén 4 kB-os) lapokra tagolódik. Minden lap rendelkezik egy saját egyedi lapazonosító számmal (Page Frame Number). Példánkban a virtuális cím két részből tevődik össze. Egy eltolásból (offset) és egy lapazonosító számból (Page Frame Number, PFN). Ha a lapok mérete 4 kB, akkor az alsó 12 bit adja az eltolást, a felette lévő 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 lapazonosítót megkeresi a laptáblában, és átkonvertálja a lap fizikai kezdőcímére. Ezek után az eltolás segítségével a laptábla alapján már megtalálja a memóriában a kérdéses fizikai címet.

28

2.4. Memóriakezelés Processz Y

Processz X VPFN 7 VPFN 6

VPFN 7

Processz Y Lap tábla

Processz X Lap tábla

-411—

VPFN 5

VPFN 5 VPFN 4

PFN 4

VPFN 4

VPFN 3

PFN 3

VPFN 3

VPFN 2

PFN 2

VPFN 2

VPFN 1

PFN 1

VPFN 1

VPFN 0

PFN 0

VPFN 0

VIRTUÁLIS MEMÓRIA

FIZIKAI MEMÓRIA

VIRTUÁLIS MEMÓRIA

2.3. ábra. A virtuális memória leképezése fizikai memóriára VPFN laptáblaptábA példánkban (2.3. ábra) két processz van, és mindkét processz saját lával rendelkezik. Ezek a laptáblák az adott processz virtuális lapjait a melával mória fizikai lapjaira képezik le. ttribútumokat tartalmazza: Minden laptáblabejegyzés az a



Az adott táblabejegyzés érvényes-e.



A fizikai lapazonosító (PFN).



Hozzáférési információ: az adott lap kezelésével kapcsolatos információk, írható-e, kód vagy adat.

2.4.3. A lapozás implementációja a Linuxon A virtuális cím leképezését fizikai címmé az MMU végzi. Az MMU a proceszeltészor része, ebből következően a cím leképezése az egyes architektúrákon eltérő lehet. Az x86-os architektúra esetén a virtuális címből a fizikai cím kétszintű leképezéssel kapható meg (lásd Mint látható, a virtuális cím ebben az esetben 3 részre bontható: lapkönyvtárindex, laptáblaindex, eltolás. A lapkönyvtár (page directory) a laptáblákra mutató hivatkozások tömbje. A lapkönyvtárindex ebből a tömbből választ ki egy laptáblát. A laptábla tartalmazza a hivatkozásokat a fizikai memória me egyes lapjaira. A laptáblaindex ebből meghatároz egy elemet, vagyis egy memórialapot. A fizikai cím ennek a lapnak a címéből és az eltolás összegéből mórialapot. számítható. A virtuális cím ilyen módon képezhető le fizikai címmé.

29

2. fejezet: Betekintés a Linux-kernelbe

Fizikai memória

Virtuális cím Lapkönyvtár index

Eltolás (offset)

Laptáblaindex

Lapkönyvtár

Laptábla

2.4. ábra. Kétszintű leképezés (x86 32bit)

Egyes 64 bites architektúrákon, mint például az Alpha, egy ilyen felosztásban a lapkönyvtár és laptáblatömbök nagyon nagy méretűek lennének, ezért a rendszertervezők bevezették a háromszintű leképezést. Ezt a 2.5. ábrán láthatjuk. Fizikai memória

Virtuális cím Lapkönyvtárindex

Középlapkönyvtárindex

Lapkönyvtár

Laptáblaindex

Középső lapkönyvtár

2.5. ábra. Háromszintű leképezés (Alpha)

30

offset

Laptábla

2.4. Memóriakezelés

Ahogy az ábrából is látható, a leképezés alapelve megegyezik az előzőekben tárgyalt kétszintű leképezéssel, csak kiegészül egy további szinttel. A lapkönyvtár és a laptábla közé beiktattunk egy középső lapkönyvtárat (page middle directory), továbbá a virtuális cím is négy részre tagolódik. A lapkönyvtárbejegyzések így a középső lapkönyvtárakra hivatkoznak, amelynek a bejegyzései laptáblákra mutatnak. A fizikai cím, hasonlóan az előzőhöz, a laptábla által tartalmazott lapcímből és az eltolás összegéből adódik. A Linux-kernel fejlesztői természetesen egységesre szerették volna elkészíteni az MMU-k kezelését, függetlenül az architektúrák különbségeitől. Ezért a Linux-kernel a háromszintű leképezést alkalmazta. Ahhoz azonban, hogy ez az x86-os architektúrákon is működjön, egy kis trükkhöz kellett folyamodni: az x86-os rendszerek esetén a középső lapkönyvtár csak egy elemet tartalmaz, így a háromszintű leképezés kétszintűvé egyszerűsödik. Ezt követték az ia64-es rendszerek, ahol már a négyszintű leképezést támogatja a hardver. Eleinte a kernelfejlesztők trükkökkel a háromszintű leképezést alkalmazták ezeken a rendszereken is, ám ezzel mesterségesen korlátozták a folyamatok maximális virtuális címterét 512 GB-ra. A kernel miatt korlátozni a hardver képességét nem tűnt célszerűnek, ezért később átalakították a leképezőalgoritmust négyszintűre. Így a virtuális cím felépítése az ia64-es rendszerek esetében a 2.6. ábra szerint alakul. PGD PGD PUD PMD PTE

PUD

PMD

PTE

Eltolás

Lapkönyvtár (Page Global Directory) Felső lapkönyvtár (Page Upper Directory) Középső lapkönyvtá Directory) Laptábla (Page Table)

2.6. ábra. A virtuális cím felépítése ia64-es rendszerek esetén

a jelenlegi kernel a platformfüggetlen virtuálismemória-kezelő algoritalgoritmusban musban a négyszintű leképezést használja, amelyet az egyszerűbb architektúrák esetében illeszt az adott rendszerhez. Így

2.4.4. Igény szerinti lapozás A valós élettel ellentétben a lustaság a számítógépek világában sokszor igen előnyös tulajdonság. Ugyanis ha csak akkor végez el egyes műveleteket a gép, amikor tényleg szükséges, akkor ezzel rendszeridőt takarítunk meg a többi feladat számára, így a rendszerünk teljesítménye növekszik.

31

2. fejezet: Betekintés a Linux-kernelbe

Ez az elmélet, átültetve a memóriakezelés világába, jelentheti azt, hogy csak akkor tölti be a rendszer a lapot a háttértárolóról, amikor egy processz hivatkozik rá, igényli az információt. Például egy adatbázis-kezelő programnál elég, ha csak azokat az adatokat tartja a memóriában, amelyekre éppen szükség van. Ezt a technikát igény szerinti lapozásnak (demand paging) nevezzük. Amikor egy processz olyan laphoz próbál hozzáférni, amelyik éppen nem található meg a memóriában, az MMU egy laphibát (page fault) generál, amellyel értesíti az operációs rendszert a problémáról 17 Ha a hivatkozott virtuális cím nem érvényes, az azt jelenti, hogy a processz olyan címre hivatkozott, amelyre nem lett volna szabad. Ilyenkor az operációs rendszer — a többi védelmében — megszünteti a processzt. Ha a hivatkozott virtuális cím érvényes, de a lap nem található éppen a memóriában, az operációs rendszernek be kell hoznia a hivatkozott lapot a háttértárolóról. Természetesen ez a folyamat eltart egy ideig, ezért a processzor addig egy másik processzt futtat tovább. A beolvasott lap közben beíródik a merevlemezről a fizikai memóriába, és bekerül a megfelelő bejegyzés a laptáblába. Ezek után a processz a megállás helyétől 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 processzek kódjának a betöltésénél is. Amikor a programot lefuttatjuk, akkor a rendszer nem tölti be a teljes kódot a fizikai memóriába, csak az elejét, míg a maradékot csak leképezi a folyamat virtuálismemória-területére. Ahogy a kód fut, és laphibákat okoz, a Linux úgy hozza be a kód többi részét is. Ez általában öszszességében is gyorsabb, mint betölteni a teljes programot, mert a nagyobb programok esetében gyakran csak egy része fut le a kódnak.

2.4.5. Lapcsere Amikor a processznek újabb virtuális lapok behozására van szüksége, és nincs szabad hely a fizikai memóriában, az operációs rendszernek helyet kell teremtenie. Ezt úgy teszi meg, hogy egyes lapokat eltávolít a fizikai memóriából. Ha az eltávolítandó lap kódot vagy olyan adatrészeket tartalmaz, amelyek nem módosultak, akkor nem szükséges a lapot lementeni. Ilyenkor ez egyszerűen eldobható, és legközelebb, amikor szükség lesz rá, megtalálható a háttértárolón. Ha azonban a lap módosult, akkor az operációs rendszernek el kell tárolnia a tartalmát, hogy később előhozhassa. Ezeket a lapokat nevezzük piszkos lapnak (dirty page), és az állományt, ahova az MMU az eltávolításkor 17

Az illegális memória-hozzáférések (pl. csak olvasható lapra írás) szintén laphibát eredményeznek (ezt lásd később).

32

2.4. Memóriakezelés

elmenti őket, lapcsereállománynak (swap file). 18 A hozzáférés a lapcsereeszközhöz 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 lapcsere-algoritmus nem elég hatékony, előfordulhat, hogy egyes lapok folyamatosan cserélődnek, és ezzel pazarolják a rendszer idejét. Hogy ezt elkerüljük, az algoritmusnak lehetőleg a fizikai memóriában kell tartania azokat a lapokat, amelyeken a processzek éppen dolgoznak, vagy később dolgozni fognak. Ezeket a lapokat hívjuk munkahalmaznak (working set). Ugyanakkor az algoritmusnak elég egyszerűnek kell lennie, hogy ne igényeljen túl sok rendszeridőt a választás. A kernel döntéshozó munkáját támogató algoritmusok sokszor bele vannak építve a processzorokba. A kernel ezek közül ugyanakkor csak azokat az algoritmusokat használhatja fel, amelyeket minden processzor ismer, a többit szoftverben kell implementálnia. A Linux az úgynevezett Least Recently Used (LRU, „legrégebben használt") lapozási technikát alkalmazza a lapok kiválasztására. Ebben a sémában a rendszer nyilvántart egy lapok közötti sorrendet az alapján, hogy mikor fértek hozzá utoljára az adott laphoz. Minél régebben fértek hozzá egy laphoz, annál inkább sor kerül rá a következő lapcsereműveletnél. A laphozzáférési sorrendet egy láncolt listában lehetne kezelni. Ha hozzáférünk egy laphoz, akkor a hivatkozását a láncolt lista elejére tesszük. Ezáltal a hivatkozásnak a listában elfoglalt helye megmutatja, hogy milyen sorrendben fértünk hozzá a lapokhoz. Ha azonban minden laphozzáférés esetén frissítenénk a láncolt listát, akkor ez használhatatlan mértékben lassítaná a memória-hozzáférés sebességét. 19 Ezért az előbb említett egyszerű metódussal szemben a Linux egy módosított durva léptékű LRU-technikát használ. Ez a megoldás alapvetően két műveleten alapul. Amikor egy folyamat hozzáfér egy laphoz, akkor ezt egy jelzőbit beállításával jelzi a rendszer. Ezt a műveletet minden jelenlegi architektúra hardveresen támogatja, így gyorsan működik. Másrészt elég egyszerű ahhoz, hogy szükség esetén szoftveresen is megvalósítható legyen. A másik művelet két láncolt lista nyilvántartása. Az egyik lista az aktív lapokat, a másik az inaktív lapokat tartja nyilván. A lapok mindkét irányban vándorolnak a két lista között. A váltás alapja a hozzáférési bit állapota. A kernel bizonyos időközönként megvizsgálja az elóbb említett hozzáférést jelző bit állapotát. Ha a lap idáig az inaktív listában szerepelt, akkor átkerül az aktív lista elejére. Ha idáig aktív volt, és a legutóbbi vizsgálat óta nem fértek hozzá, akkor az inaktív lista elejére kerül. Ha ezek után a legrégebben 18

19

A lapcsereállomány nem feltétlen fájl, lehet partíció vagy akár külön merevlemez is. Ezért célszerűbb egységesen eszköznek nevezni. A Linux több lapcsereeszközt is tud használni egyszerre. Ezeket prioritás szerint rendezi. Amíg meg nem telik, a legnagyobb prioritású eszközt használja, majd a következővel folytatja. A megoldás a gyakorlatban azért sem implementálható, mert a kernelnek értesítést kellene kapnia minden memórialap-hozzáférésről. Ez a hardver támogatása nélkül nem oldható meg. 33

2. fejezet: Betekintés a Linux-kernelbe

használt lapra vagyunk kíváncsiak, akkor az inaktív lista végén találjuk meg. Nem biztos, hogy az inaktív lista végén a sorrend teljesen korrekt az utolsó hozzáférés időpontja szerint, de nincs is szükségünk abszolút pontos eredményre. Mindegyik ott található laphoz régen fértek hozzá. Ez a kis pontatlanság nem zavaró, hiszen ennek következtében az algoritmusunk nagyságrendekkel gyorsabb, mint a korábban tárgyalt pontos algoritmus.

2.4.6. Megosztott virtuális memória A virtuálismemória-kezelés lehetővé teszi több processznek, hogy egy közös memóriaterületen osztozzanak. Ehhez csak arra van szükség, hogy bejegyezzünk a processzek laptábláiba egy közös fizikai lapra való hivatkozást, így képezve le ezt a lapot a virtuális címterületre. Természetesen ugyanazt a lapot a két virtuális címtartományban két különböző helyre is leképezhetjük. Ez a megosztott memóriakezelés lehetőséget ad a folyamatoknak, hogy a közös memóriaterületen adatokat cseréljenek. Ugyanakkor a megfelelő működés érdekében szinkronizálási módszereket is használni kell. A Linux a szálkezelést is a megosztott virtuális memória segítségével implementálja. (Erről már volt szó a 2.3.4. alfejezetben.) Ilyenkor a processzek közös memórián osztoznak, és mint szálakat használhatjuk őket.

2.4.7. Másolás íráskor (COW technika) A Linux a megosztott memóriát nemcsak adatátvitelre, illetve a szálak implementációjánál használja, hanem a folyamatok másolásának gyorsítására is (fork). Elméletileg egy új folyamat létrehozásakor a szülőfolyamat címtere lemásolódik, és ez alkotja a gyerek címterét. A valóságban nem ez történik, mert ez nagyon lassú és nagy memóriaigényű folyamat lenne. Helyette a Linux a másolás íráskor (copy on write, COW) technikáját használja. Amikor a gyerekfolyamathoz le kellene másolni a szülőfolyamat memóriatartalmát, valójában a rendszer csak a virtuálismemória-leképezéshez használt laptáblákat másolja le. Így a gyerekfolyamat címterébe ugyanazok a fizikai lapok képződnek le, mint amelyeket a szülő használ. Ám a folyamatok elkülönítése érdekében a lapok csak olvashatók lesznek mindkét folyamat számára. Ha valamelyik folyamat írni próbálja a lapot, ez egy hozzáférési hibát okoz. A hiba lekezelőrutinja tudja a hiba okát, ezért a lapot lemásolja, és lecseréli a virtuális memóriában. Ettől kezdve mindkét folyamat ismét tudja írni a lapot, illetve a másolatát.

34

2.4. Memóriakezelés

2.4.8. A hozzáférés vezérlése A laptáblabejegyzések az eddig tárgyaltak mellett hozzáférési információkat is tartalmaznak Amikor az MMU egy bejegyzés alapján a virtuális címeket fizikai címekké alakítja át, párhuzamosan ellenőrzi azokat a hozzáférési információkat is, hogy az adott processz számára a művelet engedélyezett-e, vagy sem. Több oka is lehet, amiért korlátozzuk a hozzáférést egyes memóriaterületekhez. Egyes területek (például a programkód tárolására szolgáló memóriarész) csak olvasható lehet, ezért 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. Meg kell akadályoznunk továbbá, hogy a processzek hozzáférhessenek a kernel adataihoz, a biztonság érdekében ezeket csak rendszerhívásokon keresztül érhetik el. A Linux az alábbi jogokat tartja nyilván egy lappal kapcsolatban (ezeket képezi le az adott architektúrán érvényes jogokra): •

a lap bent van-e a fizikai memóriában;



olvasható-e;



írható-e;



futtatható-e;



a lap a kernel címteréhez vagy a felhasználó címteréhez tartozik-e;



a lapot módosították-e (dirty), így ki kell-e majd írni a lapcsereállományba;



a laphoz hozzáfértek-e;



további, a gyorsítótárakkal kapcsolatos beállítások.

2.4.9. A lapkezelés gyorsítása Ha egy virtuális címet megpróbálunk leképezni fizikai címmé, akkor látható, hogy ez több memória-hozzáférést is igényel, mire végigjutunk az összes szinten. Jóllehet idáig azt mondtuk, hogy a memória olvasása aránylag gyors művelet, valójában elmarad a CPU sebessége mögött. Így a lapleképezés műveletei visszafoghatják a rendszer teljesítményét. Hogy ezt megakadályozzák, a rendszermérnökök gyorsítótárakat integráltak az MMU-ba. Ezek neve: Translation Look-aside Buffer (TLB, „félretekintő fordításbuffer": a „félretekintés" az alternatív keresési módra utal).

35

2. fejezet: Betekintés a Linux-kernelbe

A TLB tárolja a legutóbbi lapleképezések eredményét. Amikor egy virtuális címet kell lefordítania az MMU-nak, akkor először egyező TLB-bejegyzést keres. Ha talál, akkor a segítségével azonnal lefordíthatja a fizikai címre, és ezzel jelentős gyorsulást érhet el. A Linuxnak a TLB kezelésével kapcsolatosan nincs sok teendője. Egyetlen feladata, hogy értesítse az MMU-t, ha valamelyik tárolt leképezés már nem érvényes.

2.5. A virtuális állományrendszer Ebben a fejezetben bemutatjuk az Unix-világ egyik legfontosabb absztrakcióját, az állományabsztrakciós felületet. Ennek a felületnek a programozását a 4. Állomány- és I/O kezelés fejezetben, az állománykezelésnél mutatjuk be, a felület megvalósítását pedig a 7. Fejlesztés a Linux-kernelben fejezetben részletezzük.

2.5.1. Az állományabsztrakció Az első Unix rendszer egyik újítását, amely még ma is áthatja az összes operációs rendszert, a hagyomány a következőképpen foglalja össze minden állomány („everything is a file"). Ez a talán kissé leegyszerűsített megfogalmazás azt takarja, hogy a különböző 1/O perifériák sok tekintetben úgy használhatók, mint az állományok. Ha egy folyamat használni szeretné őket, jeleznie kell ezt a szándékát a kernelnek, amely egy állományleírót ad vissza. Ezek után mind az állományokat, mind a perifériákat írjuk, olvassuk, majd bezárjuk. Az implementáció szintjén ez úgy jelenik meg, hogy az állományok megnyitásakor egy állományleírót kapunk vissza, és ugyanazokkal a függvényekkel írhatjuk, illetve olvashatjuk az állományokat. Ez az ötlet nagyon kényelmessé tette a perifériák és az állományok közötti átjárhatóságot. A billentyűzet is egy állomány, amelyről csak olvashatunk, a terminál kimenete egy olyan állomány, amelyet csak írhatunk. Ezért egy program kimenete lehet egy állomány vagy egy terminál, ez mindössze a megnyitott állományleírótól függ. Sőt két program közötti kommunikációt megvalósító csővezeték használata is egy állományleíróval végzett írás és olvasás. Természetesen rögtön felmerül az a probléma, hogy az íráson és az olvasáson kívül számos művelet van (például pozicionálás), amelyre számos eszköz (például a billentyűzet) nem alkalmas. Sőt már az előző bekezdésben felfigyelhettünk arra, hogy a billentyűzetet reprezentáló leírót csak olvashatjuk, míg a terminál leíróját csak írhatjuk.

36

2.5. A virtuális állományrendszer

Ez egyáltalán nem zavaró, hiszen bár vannak különbségek, mi a hasonlóságokat szeretnénk kiaknázni. Egy bemeneti állomány, amelyből csak olvasunk, lecserélhető a billentyűzetre anélkül, hogy egyetlen olvasást végző függvényt lecserélnénk a programunkban. A továbbiakban az állomány és a fájl szavakat a Linux filozófiájával összhangban absztrakt értelemben használjuk• mind állományrendszerbeli állományok, mind 1/0 eszközök lehetnek a leíró mögött. Az állományabsztrakciós felület az összes állomány- és I/O művelet uniója, összessége. Ez az alábbi műveleteket jelenti: •

Olvasás (read): byte-ok olvasása egy adott méretű bufferba.



Írás (write): egy adott méretű buffer kiírása.



Pozicionálás (llseek): az aktuális pozíció módosítása.



Aszinkron olvasás és írás (aio_read, aio_write): POSIX aszinkron I/O műveletek.



Könyvtár tartalmának olvasása (readdir): ha az állomány könyvtár, akkor a tartalmát adja vissza.



Várakozás állományműveletre (poll): ha az olvasási vagy írási művelet éppen nem hajtható végre, akkor tudunk várakozni a feltétel teljesülésére.



I/O vezérlés (ioctl): speciális műveletek, beállítások az állományra.



Leképezés a memóriába (mmap): az állományt vagy annak egy részét leképezhetjük a virtuális memóriába, így memóriaműveletekkel írhatjuk és olvashatjuk. Ha az állomány eszköz, akkor a segítségével lehetőség van megosztott memória kialakítására az alkalmazás és az eszközmeghajtó között.



Megnyitás (open): az állomány megnyitása. A kernel a megnyitás alapján tudja, hogy az állomány használatban van.



Lezárás (close): az állomány lezárása.



Szinkronizálás (sync, fsync, fflush, aio_fsync ): ha bufferelt írást alkalmazunk, akkor a buffer tartalmát azonnal kiírja.



Állományzárolás (flock): ha több folyamat használná az állományt, akkor a zárolásokkal szinkronizálhatjuk a hozzáférést.

Ismét hangsúlyozzuk, hogy ritka az az eszköz vagy állomány-rendszerbeli állomány, amely az összes műveletet képes lenne megvalósítani. Ha egy kernelobjektum megvalósítja az állományabsztrakciós interfészt, akkor legalább egy műveletet támogat a fentiek közül. Egy leíróra meghívhatjuk a fenti függvények bármelyikét. Ha a művelet nem lenne támogatva, akkor hibaüzenettel térne vissza, amelyet a programunkban lekezelhetünk.

37

2. fejezet: Betekintés a Linux-kernelbe

Összefoglalva az eddigieket: az állományleírók akkor cserélhetők fel, ha csak a mindegyikük által támogatott műveleteket használjuk. Vagyis az állományabsztrakciós felület az műveletek uniója, a műveletek metszete mentén pedig ugyanazokkal a függvényekkel használhatunk több különböző állománytípust. Ezek után vegyük sorra az állománytípusokat. Ezek az alábbiak: •

egyszerű állományok (regular files),



speciális állományok (special files).

Az egyszerű állományok a hagyományos, állomány-rendszerbeli állományokat jelentik. Egy állomány-rendszerbeli állomány műveletei függenek az állomány típusától. Az Ext3-as állományrendszer egyszerű állományai az alábbi műveleteket támogatják: •

megnyitás, lezárás;



olvasás, írás;



aszinkron olvasás, írás;



pozicionálás;



I/O kontroll;



memóriába való leképezés;



szinkronizálás.

A speciális állományok olyan nem hagyományos állományok, amelyek megvalósítják az állományabsztrakciós felület legalább egy műveletét.

2.5.2. Speciális állományok A Linux számos olyan állománytípust is használ, amely a hagyományos értelemben véve nem állomány, ám implementálja az állományabsztrakciós interfészt.

2.5.2.1. Eszközállományok Az eszközökhöz való hozzáférés eszközállományokon (device file) keresztül történik. Az eszközállományoknak két típusa van: blokkos eszközállomány (block device file) és karakteres eszközállomány (character device file). A karakteres eszközállományok az általánosabban használt eszközinterfészek. Az állományabsztrakciós interfésznek akár minden függvényét támogathatják attól függően, hogy az eszközre értelmezhetők-e. A blokkos eszközállományok speciálisabbak, és csak az alábbi műveleteket támogatják:

38

2.5. A virtuális állományrendszer



megnyitás, bezárás;



olvasás, írás;



aszinkron olvasás, írás;



pozicionálás;



I/O vezérlés;



memóriába való leképezés;



szinkronizálás.

Jóllehet a karakteres és a blokkos eszközöket megadhatjuk a fenti módon a rajtuk értelmezett műveletekkel, a működésük alapján érthetjük meg őket igazán. Az állományabsztrakciós felület tulajdonképpen függvénypointerek halmaza: összerendeli a műveleteket azok megvalósításával. A műveleteket úgynevezett eszközvezérlők (device drivers) valósítják meg. Ha például meghívjuk az open rendszerhívást, akkor ez a kernelben úgy van implementálva, hogy megkeresi az adott állományleíróhoz tartozó eszközvezérlő függvénymutató listáját, és kiválasztja az openhez tartozó bejegyzést. Ha az open műveletet nem valósítja meg az adott eszközvezérlő, akkor ez a mutató nulla. Ebben az esetben a kernel az open rendszerhívás visszatérési értékében jelzi a hibát. Ha az eszközvezérlő megvalósítja a megnyitási műveletet, akkor a vizsgált mutató egy kezelőfüggvényre mutat, amelyet a rendszerhívás implementációja meghív. Az eszközvezérlők a kernel részét alkotják. Gyakran úgynevezett kernelmodulban implementáljuk őket (lásd a 7.3. Kernelmodulok című alfejezetet). A karakteres eszközvezérlők, amint nevük is mutatja, byte-onkénti írástolvasást tesznek lehetővé. A karakteres eszközök egyszerűen megadják a támogatott műveletekre mutató függvénymutatókat, a kernel pedig közvetlenül meghívja őket. A blokkos eszközvezérlők felépítése speciálisabb, mivel olyan eszközökhöz készültek, amelyeknél nem férhetünk hozzá egy-egy byte-hoz közvetlenül, hanem csak byte-ok csoportját, blokkokat tudunk kezelni. Erre jó példa a merevlemez, amelyhez ha byte-onként férnénk hozzá, akkor ez drasztikusan lelassítaná a rendszert. Az eszközvezérlő felépítése sokkal bonyolultabb, mert köztes gyorsítótárakat kell alkalmaznia a blokkok tárolására, illetve aszinkron mechanizmusokat a blokkok mozgatására. Az állományabsztrakciós interfészt sem közvetlenül valósítják meg: ezt a kernel valósítja meg helyettük, és egy műveletsort hoz létre számukra, amelyben felsorakoztatja a kéréseket. Eközben a kernel számos optimalizációt is elvégez. A blokkos eszközvezérlők tetszőleges sorrendben szolgálhatják ki a műveletsorban található műveleteket. Jóllehet vannak eszközök, amelyek se nem blokkosak, se nem karakteresek (például a hálózati kártya), az eszközvezérlők nagy többsége jól megvalósítható valamelyik eszközvezérlő-típussal.

39

2. fejezet: Betekintés a Linux-kernelbe

2.5.2.2. Könyvtár A könyvtár olyan speciális állomány, amely a benne lévő állományok listáját tartalmazza. A régi Unix rendszerekben az implementációk megengedték, hogy a programok az egyszerű állományok kezelésére szolgáló függvényekkel hozzá is férjenek a könyvtárállományokhoz. A könnyebb kezelhetőségért azonban egy speciális rendszerhíváskészlet került az újabb rendszerekbe. (Ezeket lásd a 4.4. Könyvtárműveletek alfejezetben.)

2.5.2.3. Szimbolikus hivatkozás A szimbolikus hivatkozás (symbolic link, symlink, soft link) olyan 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 hivatkozás, kiolvassa az értékét, és megnyitja a hivatkozott állományt. Ezt a műveletet a szimbolikus hivatkozás követésének hívjuk. A rendszerhívások alapértelmezett esetben követik a szimbolikus hivatkozásokat.

2.5.2.4. Csővezeték A csővezeték (pipe) a Unix-világ legegyszerűbb IPC-mechanizmusa. Mint a neve is elárulja, egy virtuális csővezetéket képez memóriában, amelynek végeire egy-egy állományleíróval hivatkozhatunk. Általában az egyik processz információkat ír bele az egyik oldalán, míg egy másik processz a másik végén a beírási sorrendben kiolvassa az adatokat. Mivel a két processz párhuzamosan kezeli, ezért kis memóriaterületre van szükség köztes tárolóként. A parancsértelmező a csővezetékeket a processzek közötti I/O átirányításra, míg sok más program az alprocesszeikkel való kommunikációra használja. Két típusát különböztetjük meg: névtelen (unnamed) és megnevezett (named) csővezetékeket. A névtelen csővezetékek akkor jönnek létre, amikor szükség van rájuk, és amikor mindkét oldal lezárja, akkor eltűnnek. Azért névtelenek, mert nem látszódnak a fájlrendszerben, nincsen nevük. A megnevezett csővezetékek ezzel szemben fájlnévvel jelennek meg a fájlrendszerben, és a processzek ezzel a névvel férhetnek hozzájuk. A csővezetékeket FIFOnak is nevezik, mivel az adatok FIFO (first in, first out, elsőként berakott elem vehető ki először) rendszerben közlekednek rajta. Hangsúlyozandó, hogy az állományabsztrakciós felület használata nem feltétlenül jelenti azt, hogy az állomány megjelenik az állományrendszerben. Erre jó példát nyújtanak a névtelen csővezetékek és a socketek. -

40

2.5. A virtuális állományrendszer

2.5.2.5. Socket A socketek hasonlítanak a csővezetékekre. IPC-csatornaként használhatók a folyamatok között, ám flexibilisebbek, mint a csővezetékek. Kétirányúak, és lehetővé teszik a kommunikációt két, különböző gépen futó processz között is. Vagyis ezekkel valósíthatjuk meg a hálózati kommunikációt (lásd a későbbi fejezetekben). A socketek állományként kezelhetők, mint a többi speciális állomány is, ám a rendszer tartalmaz olyan függvényeket is, amelyek speciálisan a socketekhez készültek. Az állományrendszerben találkozhatunk úgynevezett socketállományokkal. Ez a Unix Domain Socket protokoll (lásd a 6.4. Unix domain socket alfejezetben) címzési mechanizmusának a része, és két folyamat közötti socketkapcsolat felépítésében lát el feladatot. Közvetlenül az állományt nem használjuk, csak meghatározott rendszerhívások paramétereként. A socketállomány csak a Unix Domain Socket protokoll esetén tölt be szerepet. Más protokolloknál a socketmechanizmus nem használja az állományrendszert.

2.5.3. Az inode Az állomány egyetlen egyedi azonosítója az inode (information node, információs csomópont). Kulcsszerepet tölt be a kernel állománykezelésében. Az állomány inode-ja tartalmaz szinte minden információt az állományról, beleértve a jogokat, a méretét, a hivatkozások számát. Ez alól két információ képez kivételt: •

az állomány neve,



az állomány adattartalma, mivel az inode csak a mutatót tartalmazza az állományhoz tartozó adatblokkokra, magát az adatot nem. 20

Az állományok neve a könyvtárakban van eltárolva. A könyvtárállomány név és inode-szám összerendeléseket tartalmaz. Vagyis amikor egy állomány tartalmát meg akarjuk nézni, akkor megadjuk a nevét. A kernel a könyvtárállományban megkeresi a névhez rendelt inode-ot. Az inode-ban talált mutató alapján pedig elérkezünk a tényleges adathoz. Ezt a leképezést a 2.7. ábra jeleníti meg.

20

A Linux-kernel 3.2-es verziójától az ext4-es állományrendszer tartalmazza az úgynevezett inline data funkciót, amely lehetővé teszi, hogy nagyon kis adatmennyiség esetén az inode szabad területén tárolódjon az állomány tartalma. Ez sok kis állomány esetében számottevő tárhely-megtakarítást eredményez.

41

2. fejezet: Betekintés a Linux-kernelbe Könyvtár

Könyvtári

fájli inode1 fáj l2 inode2

inode1 inode

Jogosultság Tulajdonos Csoport Adatindex

inode inode2

Jogosultság Tulajdonos Csoport Adatindex

2.7. ábra. Állománynévtől az adatig

Lehetőségünk van arra is, hogy ugyanarra az inode-ra más névvel is hivatkozzunk, akár másik könyvtárból. Ezt nevezzük merev hivatkozásnak (kard link) (lásd a 2.8. ábrát). Könyvtár

Könyvtári

fájlt inode1 fájl2 inode2

inode

inode1

Jogosultság Tulajdonos Csoport Adatindex

Könyvtár Könyvtárt

á113 inode1

2.8. ábra. Merev hivatkozás

Az inode-számnak azonban csak egy állományrendszeren belül van értelme. Ezért merev hivatkozást csak egy partíción belül hozhatunk létre. Partíciók között csak a korában említett szimbolikus hivatkozás használható (lásd a 2.9. ábrát).

42

22.52.

Könyvtár Könyvtárt

fáj 11 fájl2 inode2

A virtuális állományrendszer

inode inodei

Jogosultság Tulajdonos Csoport Adatindex

inode1 Könyvt yvtárt

fáil3 inode3

inode inode2

Jogosultság Tulajdonos Csoport Link név

2.9. ábra. Szimbolikus hivatkozás

Az inode tartalmazza a rá hivatkozó állománynevek, vagyis a merev hivatkozások számát. Ezt hívjuk link countnak (a kapcsolatok száma) Amikor egy állományt törlünk, akkor valójában egy merev hivatkozást törlünk, és ez a szám csökken eggyel. Ha eléri a 0 értéket, és egyetlen folyamat sem tartja éppen nyitva az állományt, akkor ténylegesen törlődik, és a hely felszabadul. Viszont ha legalább egy folyamat nyitva tartja, csak akkor történik meg a tárolóhely felszabadítása, miután mindenki bezárta. 21 Az inode tárolásának módja és tartalma a használt állományrendszer függvénye. Jelentősen eltérhet a különböző állományrendszer-típusoknál. Ám ennek lekezelését nem háríthatjuk az alkalmazásokra. Ezért a kernel a memóriában csak egyféle reprezentációt használ, amelyet in-core inode-nak almazások minden ee a reprezentációval találtalálkoznak. A merevlemezen tárolt inode-okat on-disk inode-nak nevezzük. koznak. Amikor egy processz megnyitja az állományt, az on-disk inode betöltődik, és a kernelben található állományrendszer-kezelő rutinok automatikusan in-core inode-dá alakítják. A leképezést visszafelé is elvégzik, amikor az in-core inode módosul. A megfeleltethető értékeket visszakonvertálják, és lementik az ondisk inode-ba. Az on-disk és az in-core inode nem teljesen ugyanazt az információt tartalmazza. Például csak az in-core inode könyveli az adott állományhoz kapcsolódó folyamatok számát. Néhány állománytípus, például a névtelen csővezeték, nem rendelkezik on-disk inode-dal, csak a memóriában létezik.

21

Ilyenkor nem törlődik ténylegesen az állomány tartalma, csak speciális esetekben. Valójában csak újra felhasználhatónak lesznek nyilvánítva afájl3 kok. 43

22. fejezet: Betekintés a Linux-kernelbe

2.5.4. Az állományleírók Az alkalmazások számára az állományt többnyire az állományleíró reprezentálja Amikor egy állományt megnyitunk, akkor a kernel visszaad egy kis egész számot (int), amely a továbbiakban az állománnyal kapcsolatos műveletek során hivatkozásként szolgál az állományra. Az állományleíró csak a folyamaton belül van értelmezve. Ha ugyanazt az állományt egy másik folyamatban is megnyitjuk, akkor lehet, hogy másik állományleírót kapunk. Az állományleírók konkrét számértéke valójában az állományok megnyitásának sorrendjétől függ. A kernel 0-tól kezdődően kezdi kiosztani a folyamat számára. Ám az első három leíró foglalt. •

0: bemenet



1• kimenet



2: hibakimenet

Ezt követően az újabb állománymegnyitások során egyre nagyobb számokat kapunk vissza. Lehetőségünk van arra is, hogy állományleírókat kicseréljünk egymással. Így az is megoldható, hogy a 0, 1, 2 leírók mögött az állományt kicseréljük, és így a folyamat bemenetét vagy kimenetét például egy csővezetékre cseréljük le. Ezt a kimenet/bemenet átirányításának nevezzük (lásd részletesen a 4.1.8. Állományok átirányítása című alfejezetben). A bemenetet, a kimenetet és a hibakimenetet nemcsak a programban, hanem akár már indításnál a shell parancsban is átirányítatjuk:

ere A fenti példában az első program kimenetét egy csővezetékre kötjük, amelynek a másik végét a sort program bemenetére csatlakoztatjuk. A sort program ábécésorrendbe rendezi a sorokat, majd a kimenetét egy állományba irányítjuk át.

2.6. A Linux programozási felülete A Linux programozási felületének két jól elkülöníthető szintje van: az egyik a kernel felülete, a másik az erre épülő, felhasználói üzemmódban futó könyvtárak. A kernel felhasználói üzemmódban hívható funkcióit rendszerhívásoknak (system call, röviden syscall) nevezzük, ezek összessége adja a kernel programozási felületét. A rendszerhívások teszik lehetővé az átjárást a felhasználói és a kernelüzemmód között: ezeket ugyanis felhasználói üzemmódban hívjuk, de kernelüzemmódban futnak. Mivel az üzemmódok közötti váltás architektúrafüggő, ezért az implementáció részletei is különböznek az egyes architektúrákon. 44

22.62. A Linux programozási felülete

A rendszerhívást egyedi szám, a rendszerhívásszám (syscall number) azonosítja. Erre azért van szükség, mert az üzemmódok közötti átkapcsoláskor nem hagyományos függvényhívást végzünk: a kernelnek egy belépési pontja van, amely a memórián és a regisztereken keresztül veszi át az adatokat, köztük a rendszerhívásszámot. Ez utóbbi segítségével egy táblázatból kiválasztja, majd meghívja a megfelelő rendszerhívás-kezelőt. A Linux örökölte a rendszerhívásait a Unix rendszerektől, amely meglehetősen stabil és kiforrott felületet ad. Ezért nagyon ritkán vezetnek be új rendszerhívásokat. Ha mégis szükségünk lenne erre, a Linux makrókkal teszi egyszerűvé új rendszerhívás készítését. Ezt a 2.10. ábra szemlélteti. A rendszerhívások meghívásához tisztáznunk kell az alkalmazásprogramozói felület (Application Programming Interface, API) és a bináris alkalmazásfelület (Application Binary Interface, ABI) fogalmát. Ezekre a fogalmakra magyarul is az angol rövidítést használjuk. Az API forráskódszintű felületet definiál, a Linux estében C-függvények prototípusait, illetve azok viselkedését, a Linux esetén kézikönyvoldalakat. Jóllehet a rendszerhívásokat közvetlenül is hívhatnánk, hiszen azok is Cfüggvények, a Linuxot a C programkönyvtár (C library, libc) API-ján keresztül programozzuk. Ennek az API-nak túlnyomó része POSIX-szabvány, de a Unix/Linux operációs rendszerek estében elég közel van a rendszerhívások biztosította API-hoz. Sok esetben a libc függvény maga a rendszerhívás. Ugyanakkor nem minden libc függvény felel meg közvetlenül egy rendszerhívásnak. A libc fölé számtalan API áll rendelkezésre, sokuk interpreter és virtuális gép formájában, ezáltal teszi lehetővé a C-nél magasabb szintű nyelvek használatát. A szabványos C++-könyvtár is a libc-re épít. -

Felhasználói üzemmód - -- write() libc hívás

teml_handler() rendszersysteml_handler sys_write() rendszerhívás Kernel üzemmód 2.10. ábra. A rendszerhívás folyamata

Mint a neve is sugallja, az ABI a bináris kompatibilitás feltételeit írja le, ide tartoznak a hívási konvenciók, a regiszterek használata, byte-sorrend. Az ABI lényegében a fordítót és a linkert érinti. Míg a Linux API nagy részét a POSIX-szabvány definiálja, az ABI k specifikációja architektúrafüggő. Ez a specifikáció fellelhető az interneten, gyakorlatban arra építünk, hogy egy adott platformon a gcc csomag ismeri és támogatja a Linux ABI-ját. -

45

HARMADIK FEJEZET

Programkönyvtárak készítése

A programkönyvtárak alapjainak ismerete elengedhetetlen egy Linux rendszer testreszabásához, elkészítésük és felhasználásuk nélkül pedig nem képzelhető el nagyobb program. Ha beépülő modulokat (plugin) szeretnénk megvalósítani, vagy a programunk architektúráját lecserélhető bináris komponensekre építjük fel, ugyancsak a programkönyvtárak nyújtják az implementáció hátterét. Ebben a fejezetben áttekintjük a programkönyvtárak alapjait, majd programozási kérdéseiket tárgyaljuk meg C nyelven, végül kitérünk a C++-osztályokat tartalmazó programkönyvtárakkal kapcsolatos megoldásokra. A fejezetet egy haladó témakör zárja: megvizsgáljuk a programbetöltés folyamatát, valamint ennek keretében a sebességoptimalizáló eszközök működését is.

3.1 . Statikus programkönyvtárak Bevezetésként nézzünk meg egy egyszerű példát: ceiling.c */ /* include double ceil(double); int main() { double x; printf("Kerem az x-et: "); scanf("%lf", &x); printf("ceil(x) = %6.4f\n", ceil(x)); return 0; }

32. fejezet: Programkönyvtárak készítése double cei 1 (doubl e x) {

double i x=(int)x; return ix ./libround.so.1 (0x006c7000) libc.so.6 => /lib/t1s/libc.so.6 (0x009dd000) /lib/ld-linux.so.2 => /libild-linux.so.2 (0x009c4000) ldd ./libround.so libc.so.6 => /lib/t1s/libc.so.6 (0x00815000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x009c4000)

A programkönyvtárban található szimbólumokat a statikus programkönyvtáraknál megismert nm segédprogrammal írathatjuk ki.

3.2.3. Megosztott programkönyvtárak dinamikus betöltése Feladat Módosítsuk úgy a kerekítő főprogramunkat, hogy ne csak a saját kerekítőalgoritmusunkat tudja használni, hanem más programozók is írhassanak kerekítőalgoritmusokat: ezeket beépülő modulnak (plug-in) nevezzük.

35

List Dynamic Dependecies: kb. listázd ki a dinamikus függőségeket. 63

3. fejezet: Programkönyvtárak készítése

A feladathoz meg kell oldanunk azt, hogy a lefordított főprogram képes legyen olyan programkönyvtárakat betölteni, amelyek még nem állnak rendelkezésre linkelési időben. A statikus könyvtárakkal ez természetesen kivitelezhetetlen. Az előző fejezetben a megosztott programkönyvtárat a linker név szerint hozzáfűzte a programunkhoz, az adott nevű programkönyvtárhoz pedig automatikusan betöltőkódot generált, amely a program indulásakor megkereste és betöltötte a könyvtárat. Természetesen ügyeskedhetnénk az LD_LIBRARY PATH változóval, hogy különböző helyről töltse be az ugyanolyan nevű, de különböző programkönyvtárunkat, vagy felülírhatnánk az adott könyvtárat, játszhatnánk az so-névvel, ám ezeket a megoldásokat egyértelműen nevezhetjük igénytelennek. Ez akkor válik nyilvánvalóvá, ha egy nagyobb program különböző részeiben különböző beépülő modulokat szeretnénk használni. Szerencsére lehetőségünk van arra, hogy futás közben igény szerint betöltsünk egy megadott nevű programkönyvtárat, és annak függvényeire és kívülről elérhető változóira név szerint hivatkozhassunk. A programkönyvtárat használat után el is távolíthatjuk a memóriából. Nézzük meg, milyen függvényekkel lehetséges ez: const char* pluginPath = "./libround.so.1"; handle = dlopen(pluginPath, if(!handle)

RTLD_LAZY);

{

fputs(dlerror(), stderr); exit(1);

Példánkban sztringkonstansként definiáljuk a programkönyvtár elérési útvonalát, a gyakorlatban legtöbbször ezt konfigurációs állományból vagy felhasználói adatbevitel alapján kapjuk meg. Ezután megnyitjuk a programkönyvtárat a dlopen segítségével. Ez a függvény végzi el a dinamikus linkelést, amelyet az előző fejezet példájában a linker végzett el, valamint a betöltést. Ha a programkönyvtár további programkönyvtárakat használ, azokat is automatikusan betölti a hivatkozásaikkal együtt tetszőleges mélységig. A bemutatott dinamikus betöltés során a program indulásakor nem töltődik be a programkönyvtár, csak akkor, amikor a vezérlés eléri a dlopen függvényt. Vagyis ha a megosztott programkönyvtár nem található, a program elindul, mindössze a dlopen függvény visszatérési értéke lesz NULL. Ellenkező esetben egy leírót (handle) kapunk, amelyet a programkönyvtáron végzett műveletek során át kell adnunk paraméterként. Ha valami hiba történik, bővebb információval a dlerror függvény szolgál, amely a hiba leírását adja vissza C-típusú sztringként. Ez a hibakezelési módszer érvényes a programkönyvtár továbbiakban ismertetett függvényeire is. Nézzük meg, hogy a program betöltése után hogyan férhetünk hozzá a program függvényeihez: 64

3.2. Megosztott programkönyvtárak

double (*round)(double); round = dlsym(handle, "round"); if((error = dlerror()) ! = NULL) {

fputs(error, stderr); exit(1); printf("%lf\n", round(3.14));

A fenti programrészletben a round függvényhez férünk hozzá. Elsőként definiálunk egy függvénypointert, amely egy double argumentumlistájú double visszatérési értékű függvényre mutat. A fordítónak szüksége van a függvény prototípusára (lásd korábban is). Ennek megadása elkerülhetetlen, hiszen csak a linkelést/betöltést végezzük dinamikusan, a fordítást nem. Egy programkönyvtárbeli függvényre mutató mutatót a dlsym függvénnyel szerezhetünk. A függvény meghívása ezek után úgy történik, mint bármelyik mutatójával az adott függvény meghívása. A dlsym tulajdonképpen egy adott szimbólum kezdőcímét adja vissza, legyen az függvény vagy változó. Így a programkönyvtár roundingMethod nevű globális változójához nagyon hasonlóan férhetünk hozzá: int* roundingMethod; roundingMethod = dlsym(handle, "roundingMethod"); if((error = dlerror()) != NULL) {

fputs(error, stderr); exit(1); *roundingMethod =1;

A fordítónak ezúttal is szüksége van a változó típusára, amelyet ezúttal is pointerként definiálunk; ugyanazzal az indirekcióval férhetünk hozzá a pointer tartalmához, mint általában. Ha itt rossz típust adunk meg, akkor a függvényhívást előkészítő kódrészlet által a veremben elhelyezett adatstruktúra különböző lesz a függvény által várttól, és ez szinte bármilyen furcsa működést eredményezhet. Mindig gondosan figyeljünk a főprogramban megadott pointer és a programkönyvtárbeli függvény egyezésére. A dlsym nemcsak az általunk betöltött programkönyvtárban található szimbólumokat adja vissza, hanem a programkönyvtár által használt programkönyvtárak szimbólumait is tetszőleges mélységig. Ha nem használjuk a programkönyvtárat, akkor fel kell szabadítanunk, ennek hatására — ha a program közvetlenül vagy más betöltött programkönyvtárakon keresztül nem hivatkozik a programkönyvtárra — az operációs rendszer eltávolítja a programkönyvtárat a memóriából. A felszabadítást a dlclose függvény végzi el: dlclose(handle);

65

3. fejezet: Programkönyvtárak készítése

Térjünk vissza a dlopen függvényhez. A függvény második paramétereként szabályozhatjuk azt, hogy a programkönyvtárban található szimbólumokat (lásd a 3.4.3.4. Dinamikusan linkelt megosztott könyvtár linkelése és betöltése alfejezetet) mikor oldja fel a dinamikus linker Ha azt szeretnénk, hogy a dlopen meghívásakor történjen a szimbólumfeloldás, akkor az RTLD_NOW értéket használjuk. Ilyenkor, ha a valamelyik szimbólumfeloldás nem sikerül, a dlopen hibával tér vissza. Ha a nem definiált szimbólumhoz való hozzáféréskor szeretnénk a szimbólum feloldását, akkor az RTLD_LAZY jelzőbitet adjuk át a függvénynek. Útmutató Ha nem szeretnénk a betöltést a szimbólum-hozzáférés sebességére optimalizálni, valamint ráérünk az egyes szimbólum-hozzáférésnél lekezelni a hibákat, használjunk az RTLD_LAZY-t.

A teljes program forráskódja a következő: // dynamic_roundmai n c #include #include #include "round. h" int errorcode; const char* pluginPath = "./libround.so.1"; -

int mai n(i nt argc, char **argv)

{

void *handle; double ("round)(double); int* roundingMethod; char *error; /" Megnyitjuk a konyvtarat. * / handle = dlopen(pluginPath, RTLD_LAZY); i f(! handle) {

fputs(dlerror(), stderr) ; exit(1); }

/* Hozzaferunk a round szimbol umhoz "/ round = dl sym(handl e , "round"); if((error = dlerror()) ! = NULL) {

fputs(error, stderr); exit(1);

66

3.2. Megosztott programkönyvtárak /* Hozzaferunk a roundi ngmethod szimbol umhoz . */ roundi ngmethod = dl sym(handl e , " roundi ngmethod") ; i f((error = dl error()) != NULL) { fputs (error , stder r) ; exi t (1) ;

double x= 4.2; *roundi ngmethod =0; pri ntf ("%1 f \ n" , round(x)) ; *roundi ngmethod =1; pri ntf ("%1 f\ n" round(x)); *roundingmethod =2; printf("%lf\n", round(x)); *roundingmethod =3; round(x); printf("%d\n",errorCode); /* felszabaditjuk a konyvtarat dlclose(handle); }

Fordításkor hozzá kell linkelnünk a programunkhoz a dinamikus linker könyvtárát, ez a fentiekben ismertetett függvényeket tartalmazó programkönyvtár (libdl.so): gcc dynamic_roundmain.c -o dynamic_roundmain -1d1

Amikor azonban futtatjuk a programot, hibaüzenetet kapunk: ./libround.so.1: undefined symbol: errorCode

A függőség ugyanis esetünkben kétirányú: nemcsak a programkönyvtárban lévő szimbólumokat kell feloldani, hanem a programkönyvtárban externként definiált errorCode szimbólumot is. Úgy tudjuk rávenni a linkert, hogy a főprogram publikus szimbólumait tegye eléretővé a dinamikusan betöltött programkönyvtárak számára, hogy a gcc-nek megadjuk az rdynamic kapcsolót: -

gcc dynamic_roundmain.c -o dynamic_roundmain -1 dl -rdynami c

67

3. fejezet: Programkönyvtárak készítése

Ha azt szeretnénk, hogy a programkönyvtárunk hasonlóképpen megosztaná a külsőleg hozzáférhető szimbólumait az általa használt programkönyvtárakkal, használjuk az RTLD_GLOBAL jelzőbitet a dlopen paramétereként VAGY kapcsolatban a többi jelzőbittel. Útmutató

Valójában ez egy eléggé ritkán használt megoldás, mindezzel inkább a dinamikusan

betöltött programkönyvtárak lehetőségeit szeretnénk illusztrálni. Ha a programkönyvtárnak szüksége van főprogrambeli változókra vagy függvényekre, azt lehetőleg pointerekkel adjuk át a programkönyvtár függvényeinek.

Ismét hangsúlyozzuk, hogy a betöltés teljesen dinamikus, a programnak nincsen függősége a megosztott könyvtárra. Programunk csak a libc tót (printf és társai), valamint a dinamikus programbetöltőtől (dynamic loader) függ (dlopen és hasonlókat tartalmazó programkönyvtár). -

ldd /dynamic_roundmain libdl.so.2 => /lib/libdl.so.2 (0x00afb000) 1ibc.so.6 => /lib/t1s/libc.so.6 (0x009dd000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x009c4000)

Mindebből következik, hogy ha megadtunk konstruktort, illetve destruktort a programkönyvtárnak, akkor az a programkönyvtárat elsőként betöltő dlopen, illetve az utolsóként felszabadító dlclose hatására hívódik meg. Az így használt programkönyvtárat dinamikusan linkelt megosztott programkönyvtárnak (dynamically linked shared library) nevezzük, de használatos a dinamikusan betöltött programkönyvtár (dynamically loaded shared library) elnevezés is. Mind a statikusan, mind a dinamikusan linkelt jelző a megosztott programkönyvtár felhasználásának a módját jelenti, a megosztott könyvtár számára ez nem jelent különbséget, csupán a főprogram számára. Dinamikus esetben a főprogram függvényhívásokkal tölti be és távolítja el a programkönyvtárat, a szimbólumfeloldást függvénypointereken keresztül végzi: a szimbólumhivatkozás a függvénypointer, a dlsym által visszaadott érték a szimbólumdefiníció helye. Statikus esetben a linker gondoskodik róla, hogy a programkönyvtárakat betöltse az induló program, és a szimbólumfeloldást is elvégzi. Ebben a fejezetben ismertettük a C nyelvű programozás egyik legrugalmasabb architektúráját: hogyan lehet olyan beépülő modulokat írni, amelyekről elég csak a benne lévő függvények nevét és argumentumlistáját, illetve a változók nevét és típusát ismernünk, a könyvtár kiválasztását és így a konkrét implementációt megadhatjuk futási időben. Felmerül a kérdés, hogy létezik-e olyan megoldás, ahol még ennél is kevesebb információ is elég lenne, például megkérdezhetnénk a könyvtárat, milyen függvényei vannak, ezeknek melyek az argumentumai, és milyen típusúak.

68

3.3. Megosztott könyvtárak C++ nyelven

A válasz sajnos nemleges, ezzel ugyanis elértük a C nyelv határait: a struktúrák adattagjainak felépítése és a függvények argumentumai elvesznek a fordítás során: pointerműveletekké vagy veremkezelő utasításokká alakulnak, amelyekből lehetetlen visszanyerni a fenti kérdésekre kapható válaszokat. 36

3.3. Megosztott könyvtárak C++ nyelven Az előző fejezetekben láttuk, hogy a programkönyvtárak használatát lehetővé tévő mechanizmusok a fordítás után, a linkelés és a betöltés folyamatának részeként aktiválódnak. A C++-szimbólumok linkerszintű reprezentációja némileg eltér a C nyelvűekétől. Ebben a fejezetben bemutatjuk, hogy miként kezelhetjük ezeket a különbségeket. Mivel a C++ objektumorientált nyelv, így a programok alapvető adatstruktúrái az osztályok, nem pedig a függvények, mint a C-programokban. Ezért az elvárásaink is mások: míg az előző fejezetben az adott prototípusú C-függvények implementációját kicserélhettük egy újabb verziójú programkönyvtárban, a C++ esetében ugyanolyan interfészű (ugyanolyan publikus tagváltozókkal és tagfüggvényekkel rendelkező) osztályokkal szeretnénk ugyanezt megtenni. Jóllehet erre a problémára nem lehet igazán elegáns C/C++ megoldást javasolni, az alábbiakban bemutatjuk a lehetőségeket.

3.3.1. Programkönyvtárbeli C++-osztályok használata A C++ nyelv lehetővé teszi a függvénynevek túlterhelését (function overloading), ez azt jelenti, hogy ugyanazt a függvénynevet különböző paraméterlistával többször is felhasználhatjuk. Mivel a sok ugyanolyan nevű szimbólum linkerhibához vezetne, a C++ ezt a névelferdítés (name mangling) használatával oldja meg. Ez azt jelenti, hogy a fordító az argumentumlista típusait beleépíti a függvénynévbe. Erre szabványos eljárás nincs, minden fordító saját megoldást választhat, és választ is a tapasztalatok szerint. Ennek következtében a C++-függvények szimbólumneveit nemcsak hogy nem egyszerű kitalálni a függvénynév alapján, de még a sokat tapasztalt programozói szem számára sem túl esztétikus a látvány. A névelferdítés a statikus könyvtárak esetén akkor okozhat problémát, ha az egyik állományt C-fordítóval, a másikat C++-fordítóval fordítjuk. Sőt ugyanez a probléma a különböző C++-fordítók esetében is. Ekkor a linker szintjén ugyanazok a prototípusok másként látszódhatnak, ezért a linker nem tudja öszszepárosítani a függvény hívását a függvény implementációjával, és nem defi36

Ez a probléma vezetett el a Java- és .NET-környezetek önleíró adatstruktúráihoz. 69

3. fejezet: Programkönyvtárak készítése

niált szimbólumot jelez. Ez a probléma programkönyvtárak használata nélkül is ugyanúgy előkerülhet különböző fordítóval fordított tárgykódú állományok linkelésekor. A nem dinamikusan betöltött, megosztott programkönyvtár esetén is hasonló a helyzet: azonos fordítónál a linker elvégzi az összepárosítást. Nézzünk egy példát. Feladat Írjunk C++-osztályt, amelynek egyetlen függvénye felfelé kerekíti a megadott bemenetet. Az osztályt megosztott programkönyvtárban helyezzük el.

Elsőként készítsük el az osztályt. Az osztálydeklarációt külön .h állományban helyezzük el: // rounder.h class Rounder {

publ i c : int Round(doubl e) ; } ;

Az osztály egyetlen függvényének a definícióját tartalmazza a rounder.cpp állomány: #include #include #include "rounder.h" int Rounder::Round(double x) {

doubl e ix; ix = (int)x; return ix < x ? ix+1 : ix; }

Ezekből az állományokból megosztott könyvtárat készítünk a már jól ismert módon, de ezúttal C++-fordítóval: g++ -c rounder.cpp g++ -shared -w1,-soname,librounder.so.1 -o librounder.so.1.0.1 \ rounder.o -lc /sbin/ldconfig -n .

70

3.3. Megosztott könyvtárak C++ nyelven

Ezután elkészítjük a főprogramot a roundermain.cpp állományban: #i ncl ude "rounder . h" #i ncl ude using namespace std; int mai n() {

double x= 4.2; Rounder r; cout « r.Round(x)«endl; }

A főprogramot a szokásos módon fordítjuk: -orounder

de rmai n

cp

-

lrounder

Ha visszatekintünk a forráskódra, látható, hogy a főprogram csak az osztálydeklarációt ismeri, a függvények implementációját nem. A fordító tudja, hogyan kell a C++-függvényeket kezelni, a linker pedig összepárosítja a megfelelően fordított szimbólumhivatkozásokat az implementációval. A dinamikus linkelésű programkönyvtáraknál éppen az okoz problémát, hogy C-függvénypointereket használunk (ez a dlsym visszatérési értéke), így a fordító nem ismeri fel, hogy C++-függvényekről van szó, és nem képes helyettünk kezelni. A linkerszintű nevek elferdítési konvencióján túl rögtön egy másik problémába is ütközünk. A C++-osztályok nem statikus tagfüggvényeinek az adott objektumpéldányra mutató pointert („this pointer") is át kell adnunk. Mind a névelferdítést, mind a this pointer átadását érdemes a fordítóra bíznunk, nincs értelme „kézzel" hamisítanunk, mert a fordítókat nem kötik szabványok, az implementáció egyik verzióról a másikra változhat, sőt elviekben akár egy függvény hozzáadása is módosíthatja a névelferdítést. A névelferdítésre, illetve a C++-tagfüggvények hívására a dlsym nem nyújt támogatást. Következésképpen C++-tagfüggvényeket közvetlenül nem érhetünk el a dlsym függvény használatával. Névelferdítés esetén a megoldást az jelenti, ha megadjuk a fordítónak, hogy egyes függvényeknél ne használjon névelferdítést. Ezt az extern „C" kulcsszóval érhetjük el. extern

"C" double round (doubl e x)

{

}

71

3. fejezet: Programkönyvtárak készítése

Természetesen, ha egy függvényt extern „C"-vel deklaráltunk, akkor nem terhelhetjük túl. Ezt a megoldást azonban tagfüggvényekre nem alkalmazhatjuk. Útmutató Ha programkönyvtárban található C++-osztályokat szeretnénk felhasználni, írjunk extern „C" függvényeket, amelyek példányosítják az osztályokat, és felszabadítják a létrehozott objektumokat.

Az eddigiekre építve a következő fejezetben bemutatjuk, hogyan tudunk dinamikusan betöltött programkönyvtárak C++-objektumaihoz hozzáférni.

3.3.2. C++-objektumok dinamikus betöltése programkönyvtárból Feladat Az előző feladat kerekítést végző megosztott programkönyvtárát módosítsuk úgy, hogy dinamikusan is betölthető legyen. Készítsük el a dinamikus betöltést végző főprogramot is.

Mint ahogy a függvények esetében, itt is egy indirekció segít: míg a függvényeknél függvénypointert használtunk, itt az osztályra mutató pointerrel dolgozunk. Természetesen az osztály deklarációjára továbbra is szükség van a főprogramban is, ez alapján az információ alapján veszi észre a fordító, hogy egy C++-osztály tagfüggvényét kell hívnia, és így kezelni tudja helyettünk a C++-sajátosságokat. A függvény implementációját viszont elrejtjük a főprogram elől, ez adja a megosztott könyvtár erejét: bármikor lecserélhetjük egy másik implementációval. Ennek azonban az az ára, hogy nem használhatjuk a new operátort, ugyanis ennek a fordítása azt feltételezi, hogy a konstruktor címe rendelkezésre áll linkelési időben, ez pedig a dinamikus linkelésű könyvtárak esetében már nem teljesül. A példányosítást tehát a programkönyvtár egy függvényében kell elvégeznünk. Mivel ezt a függvényt meg szeretnénk hívni a főprogramból, extern „C"-nek deklaráljuk. Ennek jegyében így egészíthetjük ki a rounder.h állományt: extern "c" Rounder* create(); extern "c" void destroy(Rounder*); typedef Rounder*(*create_t)(); typedef void(*destroy_t)(Rounder*);

Előrelátóan a pointertípusokat is deklaráljuk, hiszen azokkal a dlsym függvénymutatókkal tér vissza, amelyeket konvertálnunk kell a függvény típusára. Az eddigiek alapján a főprogram egyszerű:

72

3.3. Megosztott könyvtárak C++ nyelven

#include #include #include #include

"rounder.h"



using namespace std; const char* pluginPath = "./librounder.so.1"; int main(int argc, char **argv) { void *handle; create_t createFuncPtr; destroy_t destroyFuncPtr; char *error; // Megnyitjuk a konyvtarat. handle = dlopen(pluginPath, RTLD_LAZY); if(!handle) { fputs(dlerror(), stderr); exit(1); } // Hozzaferunk a create szimbolumhoz. createFuncPtr = (create_t) dlsym(handle, "create"); if((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } // Hozzaferunk a destroy szimbolumhoz. destroyFuncPtr = (destroy_t)dlsym(handle, "destroy"); if((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } double x= 4.2; // Letrehozunk egy Rounder tipusu objektumot Rounder* r = (*createFuncPtr)(); cout « r->Round(x)«endl; // Felszabaditjuk a Rounder objektumot (*destroyFuncPtr)(r);

73

3. fejezet: Programkönyvtárak készítése

// Felszabaditjuk a konyvtarat dlclose(handle); }

A dlsym visszatérési értékét explicit típuskonverzióval kell az adott típusra alakítani, mert a C++ — a C nyelvvel ellentétben — szigorúan típusos nyelv. Fordításkor azonban linkertől jövő hibaüzenetet kapunk: (.text+0x107): function main': undefined reference to 'Rounder::Round(double) collect2: ld returned 1 exit status

1

A linker nem képes elérni a tagfüggvényt, mert az a programkönyvtárban van definiálva. Nem dinamikusan betöltött könyvtár esetén a linker megtalálta a teljes osztálydefiníciót a programkönyvtárban, itt azonban linkelési időben nem tudunk semmit a függvény implementációjáról, így annak címéről sem. A linker tehát joggal jelzi, hogy nem találja a függvényt. Eddigi ismereteink alapján máshogyan is eljuthattunk volna erre a következtetésre: már egyszerű függvények esetében is a függvénypointer alapján tudtunk hozzáférni a függvényekhez, itt azonban csak az osztályhoz férünk hozzá mutatóval, a függvényhez nem. Látszólag ekkor el is akadunk, hiszen visszaértünk a kiindulási problémához: C++-tagfüggvényekhez nem tudunk pointeren keresztül hozzáférni, másképp pedig a linker problémát jelez. Ha az implementációt beleírjuk a függvényekbe, akkor már az egész osztályt átemeltük a főprogramba. Egy olyan megoldásra van szükségünk, amely esetén •

nem kell megadnunk a függvény törzsét, csak a deklarációját,



mégis tudunk pointert definiálni az osztályra, és meg tudjuk hívni a függvényeit.

A C++-ban a tisztán virtuális függvény pontosan ilyen nyelvi konstrukció. Vagyis létrehozunk egy kizárólag virtuális függvényeket tartalmazó absztrakt osztályt, amelyet mind a főprogramban, mind a programkönyvtárban definiálunk. A létrehozást végző függvény ugyanakkor egy leszármazott osztályt példányosít (az absztrakt osztályt nem is tudná), és ezt adja vissza az absztraktősosztály-típusú mutatón keresztül. Mindezek fényében a programkönyvtárat az alábbiak szerint valósíthatjuk meg. A közös rounder.h tartalmazza az absztrakt osztályt és a névelferdítés nélküli függvényeket: cl ass Rounder { publ i c: vi rtual int Round(doubl e) = 0; vi rtual ~Rounder(){} ;

74

3.3. Megosztott könyvtárak C++ nyelven

extern "c" Rounder* create(); extern "C" void destroy(Rounder*); typedef Rounder*(*create_t)(); typedef void(*destroy_t)(Rounder*);

Mivel az osztály leszármazottjait Rounder típusú pointeren keresztül szabadítjuk fel, virtuális destruktort is definiálunk. Elviekben ez is lehetne tisztán virtuális, de a C++-ban a tisztán virtuális destruktor szintaxisa meglehetősen átláthatatlan. A programkönyvtárat így implementáltuk: #include #include #include " rounder h" class upRounder: public Rounder {

int Round(doubl e) ; } ;

int upRounder::Round(double x) doubl e ix; ix = (int)x; return ix 0, akkor van kiirando adat. */ while((len = read(STDIN_FILENO, buf, sizeof(buf))) != 0) {

i f (1 en == -1) if(errno == EINTR) {

continue; // ujra megprobaljuk }

perror (" read") ; return -1;

if(write(STDOUT_FILENO, buf, len) == -1) {

perror("write"); return -1;

return 0;

102

4.1. Egyszerű állománykezelés

A megoldásban a hibakezelés némi magyarázatra szorul. Elképzelhető, hogy mielőtt az olvasásművelet bármilyen adatot beolvasott volna, jelzés érkezik. Ilyenkor az olvasásműveletet ismét megpróbálhatjuk. Ezt az esetet úgy tudjuk azonosítani, hogy az errno az EINTR értéket veszi fel. A fenti programban az állomány végéig olvasunk, ha a read függvény állományvége karaktert olvas, nullával tér vissza; ekkor a ciklus már nem fut le. A terminálon Ctrl + D billentyűkombinációval írhatjuk be az állomány vége karaktert. Tudjuk, hogy ha a visszatérési érték nagyobb, mint nulla, ez a beolvasott adat méretét jelenti. Útmutató Mindig fordítsunk különös figyelmet arra, hogy a read visszatérési értéke, ne pedig a megadott bufferméret alapján elemezzük a buffert. Ha sztringet olvasunk, ne felejtsük el a lezárókaraktert a buffer végén elhelyezni.

Az olvasáshoz hasonlóan az írás is lehet részleges, de ez egyszerű állományok esetében sohasem fordul elő. Speciális állományoknál (például csővezetékek, lásd a 4.5. Csővezetékek alfejezetet) megeshet, hogy a write az adatnak csak egy részét írta ki, és egy ciklusban újra ki kell írnunk a maradékot. Ezt a módszert a megnevezett csővezetékeknél ismertetjük. Ha előre tudjuk, hogy miként szeretnénk használni az állományt, jelentős teljesítményjavulást érhetünk el a posixjadvise függvénnyel. Ennek segítségével az állomány egyes régióira megadhatjuk a használat módját, például azt, hogy az állomány egy régióját használjuk vagy nem használjuk, a közeljövőben használjuk egyszer vagy többször, esetleg szekvenciálisan férünk hozzá. Megjegyzendő még, hogy ha adatszerkezetünk tömbökből áll (például mátrixokat szeretnénk kiírni/beolvasni), akkor ezek kiírása, illetve beolvasása hatékonyabb a bufferek tömbjét kezelő writev és readv fügvényekkel, amelyek egyben atomi végrehajtást is biztosítanak.

4.1.6. Az írásművelet finomhangolása Az írásművelet mélyebb megértéséhez meg kell ismernünk, hogy a Linux hogyan, milyen lépésekben optimalizálja ezt a műveletet. Mivel a merevlemezek sebessége több nagyságrenddel kisebb a processzor sebességénél, érdemes az éppen módosított állományokat a memóriában tartani. Erre a Linux egy lemezgyorsítótárat (disk cache) hoz létre, amelyet laptárnak (page cache) nevezünk. A gyorsítótár virtuálismemória-lapokból áll, ezek tartalma az egy lemezen található blokk adatai. Az összerendelést a lapok és a blokkok között az úgynevezett I/O bufferek tárolják." 55

A lapkezelésű virtuális tárolókkal való integráció eredményeképpen a bufferek tartalmát memórialapok jelentik, a buffereknek csak az összerendelést kell kezelniük, a kernelnek az adatot nem kell külön tárolnia. 103

4. fejezet: Állomány- és I/O kezelés

Olvasáskor a kernel megnézi, hogy az adott blokkhoz tartozó lap bent van-e a laptárban. Ha igen, akkor ezzel tér vissza. Ha nem, akkor a kernel betölti az állomány kért inode-ját a laptárba — mivel egy állományt általában szekvenciálisan olvasunk — az állomány folytatását kijelölő inode-dal együtt. Így az olvasás először a memóriabeli inode-ot keresi, ha nincs bent a memóriában, akkor behozza, ahogy a következő inode-ot is (előretekintő lapozási stratégia). Mivel a virtuális memóriában mindig a legfrissebb lap van, ez mindig a legfrissebb adatot találja meg, és nem jelent problémát, hogy az írás által okozott változás nem jelenik meg a lemezen. A write függvény csak néhány ellenőrzést végez, lemásolja a kiírandó adatot a laptár megfelelő lapjára, ezután visszatér. A memórialapok kiírását a háttértárolóra tisztítószálak (flusher threads) végzik akkor, ha elfogy a memória, a kiíratlan adatok egy adott időintervallumnál hosszabb ideig vannak a memóriában, 56 valamint az sync, illetve az fsync rendszerhívás hatására. Az alábbiakban megnézzük, hogy a kernel hogyan írja ki az adatokat a lemezre. A kernel úgynevezett blokkos eszközvezérlőkön (lásd a 2.5.2.1. Eszközállományok alfejezetet) keresztül írja ki a lapokat. Ez azt jelenti, hogy az adatokat a kernel nem adatfolyamban juttatja el a lemezre írást elvégző eszközvezérlőhöz, hanem egy feladatsorban feladatokat rendel neki. Az optimalizálás miatt mind a kernel, mind az eszközvezérlő felcserélheti a feladatok sorrendjét. A kernel I/O ütemező (I/O scheduler) alrendszere megpróbálja úgy elrendezni az adatokat, hogy az egymáshoz közeli blokkokra vonatkozó írásműveleteket összegyűjti, a lemezmeghajtó hardver működésének megfelelő sorrendbe rakja, így az eszközvezérlő együtt írja őket ki a lemezre. Ez a megoldás kétségkívül gyors, de van néhány hátránya is. Az első probléma a késleltetett írás következménye. Ha az írás rögtön visszatért, nem tudunk hibaüzenetet adni, hiszen a write függvény már rég visszatért, sőt előfordulhat, hogy a program futása is befejeződött, mire a kernel háttérszálai elvégzik a kiírást. A másik probléma a sorrend. Ha minden adat kiíródott, a sorrend sohasem jelent problémát vagy inkonzisztens viselkedést, de ha a rendszer — például áramszünet miatt — összeomlik, akkor kellemetlen következmények lehetnek. A fejezet további részében megmutatjuk, hogy miként kényszeríthetjük a kernelt arra, hogy minden változtatást azonnal írjon ki a lemezre. Lényeges, hogy ezeket a módszereket csak indokolt esetben használjuk, mert jelentősen lelassítják a programunkat, illetve a rendszer működését. A fentiekben részletesen ismertetett bufferelt írás és olvasás ugyanis nagyon hatékony, és nélkülük jelentős teljesítménycsökkenésre számíthatunk.

Ezt a /proc/sys/vm/dirty_expire_centisecs állományban állíthatjuk be századmásodpercekben. 104

4.1. Egyszerű állománykezelés

Mivel ebben az esetben nem használunk buffereket, a lemez tartalma teljesen összhangban, más szóval szinkronban van a kiírt tartalommal. Ezért ezt az állománykezelést szinkronizált I/O-nak (synchronized I/O) nevezzük. Elsőként két függvény mutatunk be:

int Mindkét függvény kiírja a megadott leíró által kijelölt állományt a merevlemezre. Az írásművelet végét meg is várja, csak ezután tér vissza. Ha a merevlemeznek hardveres gyorsítótára van, ezt a függvény nem tudja kezelni, ezért előfordulhat, hogy az adat csak a gyorsítótárig jut, így a lemezre nem íródik ki. Amíg az fsync az állományhoz tartozó metaadatot is kiírja (időbélyek és egyéb inode-adatok), az fsyncdata csak az adatot szinkronizálja. Ugyanakkor egyik sem szinkronizálja az állományhoz tartozó könyvtárbejegyzést, így előfordulhat, hogy egy állomány átnevezése után az állomány a merevlemezen teljesen friss, de egy rendszerösszeomlás után nem érhető el, mert a könyvtárbejegyzés csak a memóriában létezett. Az egész rendszer összes bufferét az alábbi függvénnyel írathatjuk ki a merevlemezre: vold syne(vói ./pipex

A program létrehozta az alábbi állományt az aktuális könyvtárban: S ls pipex -1 prw 1 tihamer staff 0 sep 8 00:23 pipex

Az első p azt jelzi, hogy az állomány típusa csővezeték („pipe"). Ezután elkészíthetjük a csővezeték másik oldalán az írásműveletet végző programot. Ez nem okoz különösebb meglepetést: int main(int argc, char*argv[]) {

ínt fd; int len, res; char* buffer = "Hello!"; len = strlen(buffer)-1; // Nem kuldjuk at a lezarokaraktert fd = open (argv[1], O_WRONLY); i f(fd == -1) {

perror("open"); return -1; }

if( (res = write(fd, buffer, len)) == -1) perror("write");

close(fd); return 0; ]

.11•1 1~--—-

-

-

Érdemes megfigyelni, hogy ha elindítjuk az olvasást végző programot, amíg nem érkezik adat, a read függvény nem tér vissza: várakozik. Ez problémát jelenthet, ha egyszerre több csővezetéket szeretnénk figyelni. Ez a probléma nagyon gyakran előfordul: kezelését a következő fejezetben mutatjuk be.

125

4. fejezet: Állomány- és I/O kezelés

4.6. Blokkolt és nem blokkolt I/O Nézzük meg az alábbi példát. Feladat Írjunk programot, amely két csővezetékről (pipel és pipe2) olvas adatot folyamatosan,

és ezeket kiírja a szabványos kimenetre.

Elsőként deklaráljuk a szükséges változókat, közöttük leírókat, és a buffert, majd megnyitjuk az állományt: int fds[2]; char buf[2048]; int fdix; ínt res;

0111

/* Olvasasra megnyitjuk a pipel es pipe2 allornanyokat, ha leteznek. */ i f((fds[0]=open("pipel", O_RDONLY)) < 0) perror("open pipel"); return 1; } if((fds[1]=open("pipe2", O_RDONLY)) < 0) { perror("open pipe2"); return 1; }

Ezek után felváltva olvassuk a csővezetékeket: fdix=0; while(1) /* Ha az adat rendelkezesre all, beolvassuk, es megjelenitjuk */ res=read(fds[fdix], buf, sizeof(buf) - 1); if(res == 0) printf("A pipe%d lezarult\n", fdix+1); return 0; }

else if(res < 0) perror("read"); return 1;

126

4.6. Blokkolt és nem blokkolt I/O buf [res] = ' \O' ; printf("Pipe%d: %s", fdix+1, buf); /* A masik leí ro kerul sorra. */ fdi x=(fdix+1)%2;

A csővezetékekkel foglalkozó fejezetben láttuk, hogy ha akkor olvasunk a csővezetékből, amikor nincsen rajta adat (a másik oldal nem küldött semmit), az olvasásművelet blokkolódik: adatra várakozik, és csak akkor tér vissza, ha a csővezeték másik oldalán írásművelet történik. A kommunikációnak ezt a módját blokkolt 1/0-nak (blocking I/O) nevezzük. Ez nemcsak azt jelenti, hogy a programunk ki van szolgáltatva egy másik program kénye-kedvének, hanem azt is, hogy két I/O kommunikációban nem vehetünk részt egyszerre. Ugyanis ha adatra várakozunk az egyik csővezeték végén, nem tudunk figyelni arra, hogy milyen adatok jönnek egy másik csővezetéken, vagy hogy a felhasználó milyen billentyűzetparancsot ad ki a billentyűzeten, esetleg a hálózaton. A megoldás kézenfekvő: kérjük meg a kernelt, hogy ne blokkolja az olvasásműveleteinket. Ezt nern blokkolódó (nonblocking) 1/0-nak nevezzük. Ha az állományokat az O_NONBLOCK opcióval nyitjuk meg, akkor az írási-olvasási műveletek nem blokkolják az olvasás- és az írásműveleteket. Ilyenkor a readO rendszerhívás azonnal visszatér. Ha nem állt rendelkezésre olvasandó adat, akkor az olvasási és írási műveletek —1-et adnak vissza, és az errno változót EAGAIN ra állítják. Írás esetén ez azt jelenti, hogy még egyszer meg kell ismételnünk a műveletet. 6° -

Feladat Módosítsuk az előző programot úgy, hogy nem blokkolódó 1/0

-

t használunk.

Ehhez az állomány megnyitását az O_NONBLOCK jelzőbittel végezzük: /* Olvasasra megnyitjuk a pipel es pipe2 allomanyokat, ha leteznek. */ if((fds[0]=open("pípe1", O_RDONLY j O_NONBLOCK)) < 0) {

perror("open pipel"); return 1; }

if((fds[1]--open("pipe2", O_RDONLY { perror("open pípet"); return 1;

O_NONBLOCK)) < 0)

Olvasás esetén kezelnünk kell azt az esetet is, amikor nincs beolvasott adat (az errno értéke EAGAIN):

60

Ez egyszerű állományok esetében sosem fordul elő. 127

4. fejezet: Állomány- és I/O kezelés fdix=0; while(1)

I

/* Ha az adat rendelkezesre all, beolvassuk, es megjelenitjuk *I res=read(fds[fdix], buf, sizeof(buf) - 1); if(res > 0)

I

buf[res] = • \O'; príntf("Pipe%d: %s", fdix+1, buf);

else if(res == 0) printf("A pipe%d lezarult\n", fdix+1); return 0; else if((res < 0) && (errno 1= EAGAIN))

I

I

perror("read"); return 1;

/* A masik leiro kerul sorra. */

I

fdix=(fdix+1)%2;

A nem blokkolt read() abban az esetben, amikor a csővezetéken nem áll rendelkezésre olvasandó adat, de a csővezeték másik oldalát nyitva tartjuk, EAGAIN hibaüzenetet ad visszatérési értékként. A csővezeték másik oldalának lezárását az előző példához hasonlóan a 0 visszatérési érték jelzi. A nem blokkolt I/O kezelés megadja ugyan a lehetőségét, hogy az egyes állományleírók között gyorsan váltogassunk, de ennek az az ára, hogy a program folyamatosan olvasgatja mindkét leírót, és ezzel terheli a rendszert, még akkor is, amikor nincs kommunikáció. Ezért ezt a módszert egyáltalán nem érdemes alkalmaznunk. Az ideális megoldás az, hogy az operációs rendszernek megadjuk, hogy milyen állományleírókat szeretnénk olvasni, és a rendszer értesít az adat rendelkezésre állásáról, továbbá a program erre az értesítésre reagálva olvas. Fontos megjegyezni: annak érdekében, hogy az értesítésre való várakozás közben elkerüljük a blokkolt olvasásnál tapasztalt „elhanyagolt" állományleírók problémáját, a programban a minket érdeklő összes állományleíróra egyszerre és egy helyen kell várakoznunk. Itt különösen jól megfigyelhető az állományabsztrakció ereje: minden olyan objektum elérhető állományként, amelyre érdemes várakozni. A „biztonság kedvéért" ilyenkor nem blokkolódó állományműveleteket használunk. Ezt a megoldást I/O multiplexelésnek (I/O multiplexing) nevezzük, amelyet a Linux többféleképpen is támogat. Az alábbiakban ezeket vesszük sorra.

128

4.7. A multiplexelt I/O módszerei

4.7. A multiplexelt I/O módszerei Az előző fejezetben láttuk, hogy ha egyszerre több adatforrással szeretnénk kommunikálni, akkor multiplexelt 1/0-ra van szükségünk. Ez minden esetben a következő forgatókönyvet jelenti: A) Megkérjük a kernelt, hogy értesítsen, ha az állományleíró készen áll valamilyen I/O műveletre. B) Várakozunk az értesítésre. Ilyenkor a processz „várakozik" állapotba kerül, nem veszi el a processzoridőt más taszkoktól. C) Az értesítés felébreszti a processzt, ekkor megvizsgáljuk, hogy melyik állományleíróról és -műveletról szól az értesítés. D) Végrehajtjuk az I/O műveletet nem blokkolódó üzemmódban. E) Újrakezdjük a B) lépéstől. Ez a megoldás egy más jellegű programvezérlést jelent, mint az eddigi programok. Ahelyett, hogy a program elejétől végig lefutna némi várakozással, a programnak lesz olyan része, amely eseményekre várakozik, és azokra reagál. 61

4.7.1. Multiplexelés a select() függvénnyel A POSIX rendszerekben a multiplexelés legrégebbi megvalósítása a selectO rendszerhívás. Ebben a megoldásban egyetlen rendszerhívás különböző paramétereivel adjuk meg, hogy milyen állományleírókon milyen művelet végrehajhatóságára vagyunk kíváncsiak, maximum mennyi időt szeretnénk várakozni, továbbá ugyanez a rendszerhívás várakozik az eredményekre, és adja vissza őket. A függvény deklarációja a következő: 4include

hmiL

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tímeval *timeout);

A középső három paraméter (readfds, writefds és exceptfds) egy-egy halmaz, ezekkel adhatjuk meg, hogy mely állományleírókra vagyunk kíváncsiak, illetve visszatérés után ezek a halmazok tartalmazzák azokat az állományleírókat, amelyek készek a rajtuk való műveletvégzésre. Az első állományleíró-lista, a readfds, azokat az állományleírókat tartalmazza, amelyeken olvasást végez6,

Ha az egész program ilyen vezérlésre épül, eseményvezérelt programnak hívjuk. A 8.3. A Qt eseménykezelés-modellje alfejezetben részletesen bemutatjuk az ilyen programok sajártosságait.

129

4. fejezet: Állomány- és I/O kezelés

hetünk (olvasható adat áll rendelkezésre). A writefds az írásra kész állományleírókra várakozik. Az exceptfds nagyon ritkán használatos. Egy gyakori esetet leszámítva 62 használata meglehetősen esetleges, így a továbbiakban nem foglalkozunk vele. Ha valamilyen hiba történik az állománnyal, akkor az belekerül a readfds be is, és az olvasást végző függvény adja vissza a hibát, valamint egyúttal tipikusan az errno t is beállítja. Ezekből a listákból bármelyik lehet NULL. 63 Mindegyik paraméter egy-egy mutató egy fd_set adatstruktúrára, amely az állományleírók egy halmaza. Ezeket a következő makrókkal kezelhetjük: -

-

Fo_zERo(fd_set *set);

Ez a makró kitörli az állományleíró-listát. Az alábbi a makró használatos a lista inicializálására: FO_SET(int

fd, fd_set *set);

Az alábbi makró az fd leírót hozzáadja a listához: FD_CLR(int

fd, fd_set *set);

A következő makró az fd leírót kitörli a listából: FD_ISSET(int

fd, fd_set *set);

Ez a makró igazzal tér vissza, ha az fd benne van a listában. A select függvény első paramétereként a listákban szereplő legnagyobb állomány leírójánál eggyel nagyobb számot kell megadnunk, így nekünk kell kíkeresni a legnagyobb leírót. A timeout paraméter tartalmazza azt a maximális időt, ameddig a selectO várakozhat. Ha ez letelik, akkor a selectO mindenképpen visszatér. A select() visszatérésekor módosítva adja vissza az értéket, jelezve, hogy mennyi idő van még hátra a megadott maximális várakozási időből, ez azonban nem mindegyik rendszeren van így, ezért tekintsük ezt az értéket definiálatlannak. A fentiek tükrében a select függvényt az alábbi forgatókönyv szerint használhatjuk: A) Megkérjük a kernelt, hogy értesítsen akkor, ha az állományleíró készen áll valamilyen I/O műveletre. 1. Összerakjuk a megfelelő halmazokat (FD_SET). Mivel a select majd módosítja az átadott leíróhalmazt, ezeket célszerű lemásolni, azaz eltárolni egy másik változóban. Mivel a leíróhalmazon nem lehet végigiterálni, külön eltároljuk az állományleírókat, esetünkben egy fds tömbben. 62

63

Socketek esetén soron kívüli adat érkezett (lásd a 6.2. Az összeköttetés alapú kommunikáció alfejezetet). A select függvényt sokszor használják portolható várakozásra, ilyenkor az utolsó kivéte-

lével az összes argumentumot lenullázzuk. 130

4.7. A multiplexelt I/O módszerei

2. Kiszámoljuk a legnagyobb állományleírót. 3. Inicializáljuk a timeval struktúrát. B) Várakozunk az értesítésre. Ilyenkor a processz „várakozik" állapotba kerül, nem veszi el a processzoridőt más taszkoktól. 1. Meghívjuk a select függvényt. C) Az értesítés felébreszti a processzt, ekkor megvizsgáljuk, hogy melyik állományleíróról és -műveletről szól az értesítés. 1. Megvizsgáljuk a select visszatérési értékét. a) Ha kisebb, mint nulla, akkor hiba történt. Kiírjuk a hibát, majd kilépünk. b) Ha nulla, akkor lejárt a maximum-várakozásiidő, de semmi változás nem történt a leírókkal. Itt elvégezzük azokat a műveleteket, amelyekért a maximumidőt beállítottuk, majd E-től folytatjuk. c)

Ha nagyobb, mint nulla, akkor ez a három halmazban található összes leíró száma.

2. Felhasználva az Al -ben készített fds tömböt, megnézzük, hogy annak elemei benne vannak-e a halmazban (FD_ISSET). D) Végrehajtjuk az I/O műveletet nem blokkolódó üzemmódban. 1. Ha az állomány benne van a readfds-ben, addig olvassuk, amíg van olvasandó adat. A nulla érték továbbra is az állomány végét jelenti, csővezetékeknél (és socketeknél) azt, hogy a kapcsolatot a másik oldalon lezárták. Ha lezárták, és folytatni akarjuk a kommunikációt, kivesszük az állományleírót a halmazokból, és folytatjuk E-től. 2. Ha az állomány benne van a writefds-ben, akkor írhatjuk. E) Újrakezdjük a B) lépéstől. 1. Az Al-ben készült másolat alapján visszaállítjuk a leíróhalmazokat, és beállítjuk a timeval struktúrát. 2. Folytatjuk B-től. Ennek tükrében vizsgáljuk meg a következő egyszerű példát. Feladat Módosítsuk az előző programot úgy, hogy a multiplexelést select függvénnyel oldjuk meg.

131

4. fejezet: Állomány- és I/O kezelés /* multipipe3.c - A pipel-t es a pipe2-t olvassa parhuzamosan, a select() metodussal.*/ #include #include #include #include



int main(voíd) { int fds[2]; char buf[2048]; ínt í, res, maxfd; fd_set watchset; /* A figyelendo leirok. */ fd_set inset; /* A select() metodus altal frissitett lista. */ /* Olvasasra megnyitjuk a pipel es pípet allomanyokat, ha l eteznek. */ if((fds[0]=open("pipel", O_RDONLY I O_NONBLOCK)) < 0) {

perror("open pipel"); return 1; if((fds[1]=open("pipe2", O_RDONLY I O_NONBLOCK)) < 0) {

perror("open pipe2"); return 1;

/* A ket leírot elhelyezzek a listaban. */ FD_ZER0(&watchset); FD_SET(fds[0], &watchset); FD_SET(fds[1], &watchset); /* Kiszamoljuk a legnagyobb leiro erteket. */ maxfd = fds[0] > fds[1] ? fds[0] : fds[1]; /* A ciklus addig tart, amíg legalabb egy leirot figyelunk. while(FD_IssET(fds[0], &watchset) II FD_ISSET(fds[1], &watchset)) {

/* Lemasoljuk a leirolistat, mert a select() metodus modositja. */ inset=watchset; if(select(maxfd 4- 1, &ínset, NULL, NULL, NULL) < 0) perror("select"); return 1;

132

4.7. A multiplexelt I/O módszerei

/" Ellenorizzuk, mely leiro tartalmaz olvashato adatot."/ for(i=0; i < 2; i++) {

if(FD_ISSET(fds[i], &inset)) /* Az fds[i] olvashato, ezert olvassuk is. res = read(fds[i], buf, sizeof(buf) - 1); if(res > 0)

./

{

buf[res] = '\0'; printf("Pipe%d: %s", 1+1, buf); else if(res == 0) {

/* A pipe-ot lezartak. "/

close(fds[1]); Fo_CLR(fds[1], &watchset); el se perror("read"); return 1;

return 0;

Ez a program hasonló eredményt ad, mint a nem blokkolt módszert használó, ám jóval takarékosabban bánik az erőforrásokkal. Összehasonlítva a programot a korábban kifejtett forgatóköny D.1. pontjával, látható, hogy nem olvastuk addig az állományt, ameddig csak lehetett (az EAGAIN bekövetkeztéig). Elviekben kétféle eseményről kaphatunk értesítést: •

az adott állományra adat érkezett, így olvashatóvá vált, illetve



olvasható adat áll rendelkezésre az adott állományban.

Az első esetben csak a változásról kapunk értesítést, a másik esetben mindaddig, amíg az állományleíró olvasható állapotban van. Az első módszert élvezérelt (edge triggered), a másodikat szintvezérelt (level triggered) értesítésnek nevezzük. A későbbiekben tárgyalt epoll-t kivéve mindegyik módszer szintvezérelt. Ezért, ha az állományt addig olvassuk, amíg tudjuk, megkíméljük magunkat néhány felesleges select hívástól, illetve az argumentumok felépítésétől, de adatot akkor sem veszítünk, ha egyszerre csak egybuffernyi adatot olvasunk be. A select függvényt a BSD Unix vezette be, a hívásnak van egy POSIXverziója is:

133

4. fejezet: Állomány- és I/O kezelés #include ‹sys/select.h> int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *ntimeout, const sigset_t *sigmask);

A legfontosabb különbség a kettő között az, hogy a pselect a várakozás idejére képes letiltani jelzéseket a jelzésmaszkot tartalmazó utolsó paraméter alapján. Visszatéréskor a kernel visszaállítja az eredeti beállításokat. Akad két apróbb különbség is: a maximum-várakozásiidőt nagyobb felbontással adhatjuk meg a timespec struktúrával, és a függvény ezt az értéket nem írja felül. Bár ennek a struktúrának nanoszekundumot is megadhatunk, a gyakorlatban nincsen nagy jelentősége, mert ezek a függvények a mikroszekundum felbontást sem tudják garantálni. A select és a pselect legfontosabb előnye a hordozhatóság. Legnagyobb hátránya az, hogy első paraméterként a legnagyobb leírót kell megadnunk, és a függvények a színfalak mögött az összes kisebb leírót figyelemmel kísérik, függetlenül attól, hogy benne van-e valamelyik halmazban. Természetesen a várakozási időt sem kényelmes minden hívás után beállítani, de ezt a problémát a pselect kiküszöböli.

4.7.2. Multiplexelés a poll() függvénnyel A selectO mellett a Linux rendelkezik egy másik hasonló eszközzel is: a poll0lal. Ez a függvény a System V Unix válasza a multiplexelésre, amely erőforrás-takarékosabb, mint a selectO. Ellentétben a select függvénnyel, a poll számára minden leíróhoz jelzőbitekkel megadjuk azokat az eseményeket, amelyekről értesítést szeretnénk kapni. A függvény ahelyett, hogy ezeket felülírná, egy másik jelzőbitsorozattal adja vissza az adott leíróhoz tartozó értesítéseket. Ezt az adatstruktúrát a

pollfd definiálja: struct pollfd { int fd; short events; short revents; 1;

/* állományleíró */ /* figyelt események */ /* vissszaadott (bekövetkezett) események */

Az fd mezőnek a megnyitott állományleírót adjuk értékül. Az events mező adja meg, hogy a pollQ az adott állomány mely eseményeit figyelje. Ez egy olyan bitmaszk az adott eseményekre, amely a 4.7. táblázatban bemutatott jelzó'bitek VAGY kapcsolatából áll össze:

134

4.7. A multiplexelt I/O módszerei

4,7. táblázat. A poll jelzőbitjei

kdt•

Jelentés

POLLIN

Adat érkezett, amelyet beolvashatunk.

POLLPRI

Soron kívüli adat érkezett, amelyet beolvashatunk.

POLLOUT

Írhatunk.

POLLWRBAND

Soron kívüli adatot írhatunk.

A Linux ezeken kívül további jelzőbiteket is ismer, jó néhány közülük ekvivalens

a fentiek valamelyikével. A select readfds halmazának a POLLIN I POLLPRI bitkombináció felel meg, míg a writefds nek a POLLOUT POLLWRBAND. Az revents mező tartalmazza az értesítéseket, vagyis az events mezőben megadott események közül azokat, amelyek bekövetkeztek. Van három esemény, amelyet mindenképpen visszakapunk, ha megtörténtek, ezért ezeket nem kell megadnunk figyelendő eseményekként. Ezeket a 4.8. táblázat foglalja össze. -

4.8. táblázat. A poll által figyelhető események Jelzőbit

Jelentés

POLLERR

Hiba történt az állományleíróval.

POLLHUP

A kapcsolatot lezárták. 64

POLLNVAL

Rossz állományleírót adtunk meg.

A poll rendszerhívás ezekből az eseményekből álló tömböt, annak méretét és egy várakozási időt vár: 4include int sem_init(sem_t *sem, int pshared, unsigned int value);

Ha a pshared nem 0, akkor a szemaforhoz más processzek is hozzáférhetnek, egyébként csak az adott processz szálai. A szemafor értéke a value paraméterben megadott szám lesz. Megnevezett szemafort az alábbi függvénnyel hozhatunk létre: finclude sem_t *sem_open(const char *name, int oflag, ...);

Az első paraméter a szemafor neve, a második a megnyitás jelzőbitjei az állomány megnyitásánál megismert O_CREAT és O_EXCL, amelyekhez az fcntl.h állományt is be kell építenünk. Ha az O_CREAT jezló'bitet beállítjuk, akkor egy harmadik és egy negyedik paramétert is meg kell adnunk. Ekkor a függvény prototípusa így néz ki:

233

5. fejezet: Párhuzamos programozás sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); _ _

A további paraméterek a jogosultságok és a szemafor kezdeti értéke. Vegyük sorra a műveleteket. Egy sem szemafor értékét a

#include

"aele•~1~•11~el

int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem);

függvényekkel csökkenthetjük eggyel. Ha a sem szemafor értéke pozitív, mindkét függvény eggyel csökkenti az értékét, és visszatér 0-val. Ha a szemafor értéke 0, a sem_trywait azonnal visszatér EAGAIN értékkel, míg a sem_wait várakozik a szemafor pozitív értékére. 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 értékét használat után a #include int sem_post(sem_t *sem);

függvénnyel növelhetjük eggyel. A szemafor aktuális értékét a #include 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, amelynek abszolút értéke megadja a szemaforra várakozó processzek számát. Ha a sval pozitív, ez a szemafor aktuális értékét jelenti. Névtelen szemafort a #include int sem_destroy(sem_t *sem);

11111111L~211=1

függvénnyel szüntethetünk meg. Megnevezett szemafort a #include int sem_close(sem_t *sem);

függvénnyel zárhatunk le.

234

--

.__

5.5. POSIX-szinkronizáció

A megnevezett szemaforok a processz kilépésekor vagy az exec függvénycsalád hívásakor automatikusan lezáródnak. Mivel a megnevezett szemaforok perzisztensek, a close függvény hatására nem szabadulnak fel, és értéküket is megőrzik. Ha a close után újra meghívjuk a sem_open függvényt a nevével, ugyanazt a szemafort kapjuk vissza, ugyanolyan állapotban, ahogy a processzek hagyták. A megnevezett szemafort a sem_unlink függvénnyel törölhetjük. A szemafor az állományokhoz hasnonlóan nem törlődik rögtön, a kernel megvárja míg az összes processz lezárja a leírót, és utána szabadítja csak fel. Ugyanakkor az a processz, amely a sem_unlinket hívta, már nem nyithatja meg újra. Feladat Készítsünk olyan programot, amely egy repülőtéri bejelentkezést szimulál. Hozzunk létre egy POSIX-szemafort, ahol a szemafor a légitársaság pultjait jelenti. Ezúttal egy nagy közös sor van, és a szemafor kezdeti értéke jelzi a pultok maximális számát. Egy szál hozza létre a szemafortömböt, egy másik szál pedig a következőt: „menjen oda" az első üres pulthoz, ha minden pult foglalt, álljon be abba a sorba, ahol legkevesebben várakoznak. Az utasok nem feltétlenül érkezési sorrendben kerülnek sorra, mert a túl későn érkező utasokat a később indulók előreengedhetik.

Elsőként az utasszálat mutatjuk be: /* Globalis valtozok *7 sem_t szemafor; int utas_szamlalo=0;

`_

void* utas_szal (void"arg) {

í nt sorsz am=" (i nt" - )arg ; Free(arg): printf("A %d. utas sorban all...\n",sorszam); /* varakozunk a szemaforra: innen csak akkor jutunk */ /* tovabb, ha egy pult szabad lesz */ sem_wait(&szemafor); printf("A %d. utas sorra kerult...\n",sorszam); sleep(KISZOLGALASI_IDO) ; printf("A %d. utas tavozott...\n",sorszam); /* El engedjuk a szemafort sem_post(&szemafor); return NULL;

tr i

}

235

5. fejezet: Párhuzamos programozás

A szál megpróbálja lefoglalni a szemafort. Ha nem sikerül, várakozik, vagyis sorba áll. Ha sikerül, akkor a szemafort, vagyis a szabad pultok számát csökkenti eggyel, várakozik a kiszolgálási ideig, majd megnöveli eggyel a szemafort, és kilép („távozik"). A főprogram egy lehetséges implementációja az alábbi: int main() {

int ch; int* p_sorszam; pthread_t szal_leiro; /* Beallitjuk a szemafor erteket */ sem_init(&szemafor,O,PULTOK_SZAMA); printf("Nyomjon u-t uj utas kuldesehez, k-t a kilepeshez!\n"); do do ch=getchar(); }while(ch!='u'&&ch!='k'); utas_szamlalo++; p_sorszam=malloc(sizeof(int)); *p_sorszam=utas_szamlalo; pthread_create(&szal_leiro,NULL,utas_szal,p_sorszam); while(ch!='k'); printf("Pultokat bezartak, sorban allok hazamennek...\n"); return 0; }

A főprogram a felhasználói bemenet alapján hozza létre a szálakat, vagyis az utasokat. Kilépéskor az összes szál megszűnik, vagyis az összes utas eltűnik, ez feladatunkban nem okoz problémát, ezért nem várakozunk a szálakra, így leíróikat sem mentjük el.

5.5.4. Spinlock Amikor egy szál nem tudja lefoglalni a mutexet, rögtön várakozó állapotba kerül. Amikor a mutex állapota megváltozik, akkor a szálat fel kell ébreszteni, amely újra megpróbálja lefoglalni a mutexet. Ha a mutexet csak nagyon rövid ídeig fogják a szálak, akkor a várakozás helyett érdemes rögtön újra

236

5.5. POSIX-szinkronizáció

próbálkozni. Ha ez sorozatosan nem sikerül, akkor az állandó próbálkozással pazaroljuk a processzoridőt. Ha viszont ez rendre sikerül, akkor a várakozásfelébresztés idejét megtakarítottuk. Pontosan ezt a forgatókönyvet valósítja meg a spinlock. Feladat Készítsünk szálbiztos FIFO-tárolót spinlockkal.

Elsőként deklarálnunk kell a spinlock objektumot, majd gondoskodnunk kell az inicializálásáról és a felszabadításáról. templatecclass Type> cl ass Fi fo

pthread_spinlock_t spin; } ;

/* Al apertel mezett konstruktor '/ template Fi fo : Fi fo() /* Inicializaljuk a spi nlockot es a tarolo strukturakat. pthread_spi n_i ni t(&spi n , 0) ;

*1

}

/* Destruktor */ templ a te Fi fo: :-Fi fo ()

pthread_spi n_destroy(&spi n) ;

A Lock

osztály végzi a lefoglalást és felszabadítást:

cl ass Lock {

pthread_spi nlock_t* spin; publ ic: Lock (pthread_spi n1 ock_t*spi n) : spin(spin){pthread_spin_lock(spin);} -Lock {pthread_spin_unlock(spin);} ; 1;

237

5. fejezet: Párhuzamos programozás

Pusztán az osztály alapján nem tudjuk megmondani, hogy érdemes-e a mutexet spinlockra cserélni, hiszen az elemek kivétele és elhelyezése során a használattól függően gyakori vagy nagyon ritka is lehet. Útmutató Általában használjunk mutexet, hacsak nem vagyunk meggyőződve az ütközés ritkaságáról.

5.5.5. További lehetőségek: POSIX megosztott memória és üzenetsorok A POSIX megosztott memóriát az shm_open függvénnyel hozhatjuk létre, és az shm_unlink függvénnyel szüntethetjük meg. A megosztott memóriát a processzek a megnevezett szemaforhoz hasonlóan / karakterrel kezdődő névvel tudják azonosítani. A POSIX-üzenetsorok szintén név alapján azonosíthatók független processzekből, szülő-gyermek viszony esetén a leíró öröklésével oldható meg a közös üzenetsor létrehozása. Megnyitáskor egy leíró jön létre (mq_open). A küldő- (mq_send) és az olvasó- (mq_receive, mq_timedreceive) függvények a read/write-hoz hasonló buffereket várnak. Az üzenetekhez prioritásokat is rendelhetünk. Az olvasófüggvények alapértelmezésben blokkolódnak, a nem blokkolódó üzemmódot nekünk kell beállítani. A szemaforhoz hasonlóan létezik lezárás (mq_close) és megszüntetés (mq_unlink).

5.6. Jelzések A jelzések a Linux programozásának szinte minden területén előfordulnak, így az előző fejezetekben is számtalanszor utaltunk rájuk. A jelzések lényegében egyidősek a Unix rendszerekkel. A jelzés (signal) a processzek közötti aszinkron kommunikáció egy formája, amelynek segítségével egy egész számot lehet küldeni, és amelyet a processz soron kívül kezel. Az aszinkron tulajdonság azt jelenti, hogy általános esetben a jelzést küldő processz nem várakozik a küldés után, hanem rögtön folytatja a futását, nem kap értesítést arról, hogy a jelzés célba ért-e. Minden jelzéshez egy egyedi egész számot rendelünk, amely egytől számozódik. Az ezekhez a számokhoz rendelt szimbolikus konstansokhoz a signal.h állományon keresztül férhetünk hozzá (a signum.h állomány tartalmazza őket). A konstansok SIG előtaggal kezdődnek, például a program futásának megszakítására szolgáló SIGINT a kettes sorszámú szignált jelöli (#define SIGINT 2). A soron kívüli kezelés azt jelenti, hogy a processz normál végrehajtása megszakad, lefut a jelzést kezelő kód, majd utána folytatódik az ere-

238

5.6. Jelzések

deti végrehajtás. E miatt a tulajdonságuk miatt a jelzéseket gyakran a megszakításokhoz hasonlítják. Jóllehet a jelzéseket elsősorban processzek közötti kommunikációra használjuk, egy folyamat küldhet magának is jelzéseket. Sőt ez a kommunikációs forma szálak között is használható. Bár jelzéseket bármelyik processz küldhet, a jelzések legfőbb forrása maga a kernel. Az alábbi esetekben a kernel nagyon gyakran jelzésekkel küld értesítést. •

Valamilyen hardveresemény történt, tipikusan hardveres megszakítás érkezett, és a kernel erről értesíteni szeretné a processzeket (például időzítés, nullával való osztás).



A felhasználó a terminálfelületen valamilyen speciális billentyűkombinációt nyomott le, például Ctrl + C SIGINT jelzést generál.



Valamilyen szoftveresemény történt: egy állományleíró olvashatóvá vált, soron kívüli adat érkezett a hálózaton, gyermekprocessz befejezte a futását.

A jelzések alapkoncepciója nagyon egyszerű: egy folyamat vagy a kernel egy egész számot küld a programunknak, ennek hatására a program normális futása megszakad, és a programon belül automatikusan meghívódik egy kezelőfüggvény, amely reagál a jelzésre, vagy ha a processz nem regisztrált kezelőfüggvényt, a kernel vagy figyelmen kívül hagyja, vagy egy alapértelmezett kezelőfüggvénnyel kezeli a jelzést. Az eddigi fejezetekben pontosan így tekintettünk a jelzésekre. Ugyanakkor ebben a témakörben különösen igaz az, hogy az ördög a részletekben rejlik. A Unix rendszereknek meglehetősen sok kísérletezésre volt szüksége ahhoz, hogy a jelzések megbízható kommunikációs módszerré váljanak, azaz ne vesszenek el a processzek tudta nélkül. Ahhoz, hogy biztonsággal használjuk a jelzéseket, részletesen meg kell értenünk a működésüket és a helyes használat részleteit. Ebben segítenek a következő fejezetek.

5.6.1. A jelzésküldés és -fogadás folyamata A küldő processz szempontjából meglehetősen egyszerű a helyzet. A jelzés küldésére felhasználói üzemmódban a kill, tgkill, valamint a sigqueue rendszerhívások valamelyikével lehet utasítani a kernelt. Jelzést egy adott processznek, processzcsoportnak vagy szálnak lehet küldeni. Ha a processznek nem volt joga a címzettnek jelzést küldeni, a rendszerhívások hibával térnek vissza. Ezt a folyamatot jelzésküldésnek (sending a signal) nevezzük. A címzett processz reakcióját egy jelzésre a jelzés elrendezésének (disposition) nevezzük. A címzett processz háromféleképpen rendezheti el a küldött jelzést:

239

5. fejezet: Párhuzamos programozás



Figyelmen kívül hagyja: ilyenkor a processz állapota és futása ugyanaz marad, mintha a jelzés meg sem érkezett volna.



Lefuttat egy függvényt, az úgynevezett jelzéskezelőt (signal handler). Ezt a függvényt a processznek előre be kell regisztrálnia.



Hagyja, hogy a kernel egy alapértelmezett funkciót hajtson végre.

A jelzéstől függően a kernel alapértelmezett funkciója háromféle lehet: •

Figyelmen kívül hagyja a jelzést (F).



Terminálja a processzt Labnormal process termination", T).



A processz memóriabeli és regisztereinek aktuális állapotát elmenti egy állományba (core dump), majd terminálja (CD).



A processz felfüggesztett (STOPPED) állapotba kerül (STOP).



A processz felfüggesztett állapotból a futásra kész (RUNNING) állapotba kerül (FUT).

Az egyes jelzéseket és a kernel alapértelmezett funkcióját az 5.9. táblázat táblázat tartalmazza. 5.9. táblázat. A jelzések azonosítója, leírása és alapértelmezett elrendezése

Jelzés

Leírás

Funkció

SIGABRT

Az abort0 függvény generálja.

CD

SIGALRM

Egy alarmQ által felhúzott időzítő lejárt.

T

SIGBUS

Memória-hozzáférési problémák.

CD

SIGCHLD

A gyermekprocesszt terminálták vagy felfüggesz- F tették.

SIGCONT

A processz a leállítás után folytatja a futását.

FUT

SIGHUP

A processz terminálját becsukták.

T

SIGFPE

Lebegőpontos számítási hiba.

CD

SIGILL

A processzor illegális utasítást hajtott végre.

CD

SIGINT

A felhasználó megszakításkaraktert (^C) küldött T a terminálon keresztül.

SIGIO

Aszinkron 1/0: olvasható adat érkezett.

T

SIGKILL

Mindenképpen terminálja a processzt.

T

SIGQUIT

A felhasználó kilépési karaktert (^\) küldött.

CD

SIGPIPE

A processz olyan csővezetékbe írt, amelynek nin- T csen olvasója.

SIGPROF

A profiler által használt időzítő lejárt.

T

SIGPWR

Tápfeszültséghiba.

T

240

5.6. Jelzések

IJelzés

Leírás

Funkció

SIGSEGV

Illegális memóriacímhez való hozzáférés.

CD

SIGSTOP

Mindenképpen felfüggeszti a folyamat futását.

STOP

SIGTERM

Kezelhető (felülbírálható) processzmegszüntetés. T

SIGTRAP

Töréspont nyomkövetéshez.

SIGTSTP

A felhasználó felfüggesztéskaraktert (^Z) küldött. STOP

SIGTTIN

A háttérben futó alkalmazás megpróbálta olvasni STOP a terminált.

SIGTTOU

A háttérben futó alkalmazás írni próbált a termi- STOP nálra.

SIGWINCH

A terminál mérete megváltozott.

SIGURG

Sürgős I/0 esemény.

F

CD

SIGUSRI

Programozó által definiált jelzés.

T

SIGUSR2

Programozó által definiált jelzés.

T

SIGXCPU

CPU időszeletének túllépése.

CD

SIGXFSZ

Fájlméret határának átlépése.

CD

SIGVTALRM

A setitimer() által felhúzott virtuáls időzítő lejárt. T

A fenti táblázatot áttanulmányozva láthatjuk, hogy a Linux elég szigorú: ha a tipikus forgatókönyv szerínt általában kezelnünk kellene egy jelzést, akkor a kerne] alapértelmezésben terminálja a programot, nehogy rejtve maradjon a hiba. Ha komoly hardverhiba történik, amely után könnyen előfordulhat, hogy a hiba felderítése érdekében nyomon szeretnénk követni a processzt, ott a kernel core dump állományt is készít. A processz felfüggesztésével és újraindításával kapcsolatos jelzések kezelését a terminál feledatvezérlése szerint alakították ki. A rendszer elvárt működése érdekében a kernel fenntartja a lehetőséget, hogy bizonyos jelzések elrendezését ne lehessen megváltoztatni. Ezeknek a jelzéseknek a nevét vastagon szedtük a táblázatban. Ugyanígy a 0 processzazonosítójú processznek nem lehet jelzést küldeni, míg az 1-es processznek (ínit) csak olyanokat továbbít a kernel, amelyre jelzéskezelőt definiál. Ha egy processz jelzést kap, megszakítja a futását, és végrehajtja az adott jelzéshez tartozó elrendezést. Mivel a pontos feltételek implementációfüggők, érdemes azzal a feltételezéssel dolgoznunk, hogy bármelyik két utasítás között érkezhet jelzés, és végrehajtódhat az elrendezése. Ha a jelzéskezelőból használjuk a program változóit, a fenti feltételezés tükrében oda kell figyelnünk a versenyhelyzetekre. Vegyük példaként az alábbi pszeudokódot:

241

5. fejezet: Párhuzamos programozás

int idozito; static void idozito_jelzeskezelo

I

idozito = 1;

i nt main() { // idozito_jelzeskezelo beregisztrálása a SIGALRM jelzésre idozito = 0; alarm(150); // 150 nanoszekundum múlva SIGALRM jelzés while(!idozito) { pause(); // A pause függvény a következő jelzésig várakozik }

I A fenti programrészlet egy klasszikus hibát tartalmaz. A megoldás alapgondolata az, hogy a főprogram úgy várakozik egy meghatározott ideig, hogy felhúz egy órát. Az óra a beálított idő leteltével egy SIGALRM jelzést küld. A főprogram és a jelzéskezelő egy globális változón keresztül kommunikál: az idozito értéke az induláskor nulla, a jelzéskezelő állítja be egyre. A főprogram addig várakozik, amíg a változó értéke egy nem lesz. A pause függvény egy jelzésre vár. A while ciklusra azért van szükség, hogy ha más jelzés ébresztené fel a pause-t, akkor tovább várakozzunk A fenti program az esetek nagy részében jól működik. Ám ha az ütemezés éppen úgy alakul, elképzelhető, hogy a jelzés az időzítő vizsgálata után, de még a pause függvény meghívása előtt következik be. Addig, ameddig a program más jelzést nem kap, várakozik — ez pedig tetszőleges ideig eltarthat. A megoldás az lenne, ha beállíthatnánk egy kritikus szekciót, amelynek a futása alatt a processz nem fogadhatna jelzéseket. Viszont ha eközben jelzés érkezík, annak nem szabad elvesznie, ezért egy várakozási sorban kell tartani. Természetesen ennek az alapgondolatnak többféle variációja létezhet, például a várakozási sor feldolgozásának sorrendje sokféle lehet, vagy egy adott típusú jelzés többszöri előfordulásakor csak egyet tárolhatunk. Első megoldásként tételezzük fel az alábbi lehetséges megoldást. Minden egyes jelzéshez hozzárendelünk egy engedélyezőbitet. Ha ez a bit igaz, a processz kezeli a jelzést, ha nem igaz, akkor a kernel nem kézbesíti a jelzést. Mivel ez utóbbi esetben a jelzés nem veszhet el, minden jelzéshez felállítunk egy várakozási sort is. Amikor egy nem engedélyezett jelzés érkezik, a kernel beleteszi ebbe a sorba Amikor a processz újra engedélyezi a jelzéseket, akkor a kernel az éppen érvényes elrendezés szerint kézbesíti a várakozási sorokban található jelzéseket: a legkisebb értékűhöz tartozó sort üríti ki először. Természetesen — a várakozási sorok FIFO-jellege miatt — érkezési sorrendben dolgozza fel egy adott sor elemeit. Az ezen az elven működő jelzéseket valós

242

5.6. Jelzések

idejű jelzéseknek (real-time signal) nevezzük. A valós idejű jelzéseket a sigqueue függvény segítségével adhatjuk hozzá a várakozási sorokhoz, a SIGRTMIN és a SIGRTMAX közötti jelzésekkel a kezdő és a végértéket is beleértve. Mivel a jelzés értéke ilyenkor csak a prioritásról árulkodik, a sigqueue további paraméterek elküldését is lehetővé teszi. Sokszor a valós idejű jelzések által nyújtott lehetőségeknél jóval kevesebbre van szükségünk. Nézzük meg példaként azt az esetet, amikor egy időzítőeseményre szeretnénk valamilyen feladatot periodikusan elvégezni. Előfordulhat, hogy a processzor leterheltsége miatt az események egymásra futnak: egy eseményre még nem tudtunk reagálni, de az újabb már megérkezett. Ilyenkor nem probléma, ha arra az eseményre, amelyról amúgy is lemaradtunk, nem reagálunk. A torlódás leegyszerűsítésével szemben viszont nem szeretnénk, ha egy jelzés elveszne. Ezalatt pontosan azt értjük, hogy ha a jelzés feldolgozásának megkezdése után még egy ugyanolyan jelzés érkezik, arról külön értesítést szeretnénk kapni, nem veszhet el. Ezt érdemes úgy tekintenünk, hogy az egyes jelzésekhez tartozó várakozási sor egyelemű, vagyis egyszerre egy adott típusból csak egy feldolgozatlan jelzést tudunk tárolni. Az így működő jelzéseket hagyományos jelzéseknek (traditional signals) nevezzük. A hagyományos jelzéseket a kill rendszerhívással küldhetünk.

Útmutató Mivel a hagyományos jelzések esetében a torlódó jelzésekről a processz nem kap külön értesítést, a jelzéseket nem érdemes számolni, mert egy adott jelzéssorozatra a processzor terhelésétöl és egyéb rendszerjellemzőktől függő eredményt kapunk. Mind a hagyományos mind a valós idejü jelzések esetén egy elküldött jelzésre a címzett maximum egyszer reagálhat („consumable resource"). Jelzés egyik modellben sem veszhet el, pusztán az egymásra torlódott jelzésekről csak egy értesítést kap a címzett. A valós idejű jelzéseknél a sorrend adott, a hagyományos jelzéseknél nincs előírva, de az implementációk a processz adott állapotát érintő jelzéseket általában előbb kézbesítík. A kerneltől kapott jelzések hagyományos jelzések, mínt ahogy a Linux rendszer és a programok többsége is ilyen jelzéseket küld. A fentiekben tehát a kritikus szekciónak csak az egyik felét vizsgáltuk: a kritikus szekciót úgy alakítottuk ki, hogy a jelzéseket letiltottuk, valamint egy szigorú és egy megengedőbb megoldást ismertettünk arra, hogy a jelzések a letiltás alatt ne vesszenek el. Viszatérve a példánkhoz, a kritikus szekciót az óra felhúzása előtt beállítjuk. A SIGALRM hagyományos jelzés: ha a kritikus szekcióban már érkezik jelzés, az nem veszik el, ha több is érkezik, csak egyre hívódik meg a jelzéskezelő: int idozito; static void idozito_jelzeskezelo {

idozito = 1;

243

5. fejezet: Párhuzamos programozás

int main() {

// idozito_jelzeskezelo beregisztrálása a SIGALRM jelzésre idozito = 0; // Az SIGALRM letiltása sigprocmask(...); alarm(150); // 150 nanoszekundum múlva SIGALRM jelzés whi le( ! i dozi to) // Kritikus szekció vége sigprocmask(...)

SIGALRM engedélyezése

pause(); // A pause függvény a következő jelzésig várakozík

A fenti programrészlettel sajnos nem sokkal vagyunk előrébb: ha a jelzés a kritikus szekció vége (sigprocmask) és a pause függvény között következik be, a főprogram ugyanúgy nem ébred fel, mint azelőtt. A pause helyett olyan atomi műveletre van szükségünk, amely osztatlanul engedélyezi a SIGALRM jelzést, és várakozik a legközelebbi jelzésig. Ez a sigsuspend függvény. Argumentumában megadhatjuk azokat a jelzéseket, amelyre várakoznia kell, ezeket engedélyezi. Ha a megadott jelzések valamelyike érkezik, először megvárja a jelzéskezelő lefutását, és csak utána tér vissza. Viszont ha sigsuspend a jelzéskezelő lefutása után tér vissza, akkor a jelzéskezelő futása után rögtön le kell tiltania a megadott jelzéseket, vagyis vissza kell állítania az egyes jelzések engedélyezettségét a hívás előtti állapotokba. Ugyanis ha nekünk kellene ezt megtennünk egy soron következő sigprocmask hívással, akkor jelzéseket veszthetnénk: int idozito; static void idozito_jelzeskezelo() {

idozito = 1;

int main() {

// idozito_jelzeskezelo beregi sztrálása a SIGALRM jel zésre idozito = 0; /7 Az SIGALRM letiltása

sigprocmask(...); alarm(150); // 150 nanoszekundum múlva SIGALRM jelzés

244

while(!idozito) // Kritikus szekció vége - SIGALRM engedélyezése, várakozás sigsuspend(...); // Az átadott jelzés a SIGALRM // visszatérés után a hívás elötti állapotot állítja vissza

Ezzel megoldottuk a jelzésre várakozás problémáját anélkül, hogy jelzéseket vesztettünk volna. Útmutató A pause függvény helyett használjunk sigsuspendet.

Speciális jelzés a nulla értékű jelzés: ha ezt küldjük, a kernel semmit nem kézbesít, viszont ellenőrizhetjük, hogy az adott azonosítójú processznek jogunk van-e üzenetet küldeni, vagy azt, hogy a processz megtalálható-e a rendszerben. Ez utóbbira jobb megoldás a wait függvények használata (ha a szülő szeretné tudni, hogy a gyermeke fut-e) vagy közös szinkronizációs objektumok, zárolások, illyetve IPC-mechanizmusok használata, amelyeket a kilépő processz átállít. Ezek a technikák ugyanis nem okoznak problémát akkor sem, ha a nem használt azonosítókat a kernel új processzekhez rendeli.

5.6.2. Jelzések megvalósítása A kernel szempontjából ez valamivel összetettebb folyamat. Tegyük fel, hogy processz vagy maga a kernel egy jelzésküldést kezdeményez. Ha a küldő folyamatnak volt ehhez joga, a kernel értesíti a címzett processzt a jelzés érkezéséről, vagyis beállítja a processz megfelelő adatstruktúráit. A kernel szempontjából ez a jelzésküldés folyamatának az első része, amelyet a jelzés generálásának (signal generation) nevezünk, míg a folyamat második része a jelzés kézbesítése (signal delivery). A kézbesítés folyamán a kernel feldolgoztatja a címzett processzel a jelzést. Azokat a generált jelzéseket, amelyek még nem lettek kézbesítve, függőben lévő (pending) jelzéseknek nevezzük. Az egyes jelzések engedélyezett/letiltott állapotát a jelzésmaszk (signal mask) tárolja. Minden folyamathoz tartozik egy jelzésmaszk, amelyet a már említett sigprocmask függvénnyel állíthatunk. A SIGSTOP és a SIGKILL jelzéseket nem lehet letiltani. A kernel megpróbálja kiszűrni az egyszerűbben kezelhető eseteket. Elsőként megvizsgálja, hogy a generálandó szignált nem hagyja-e figyelmen kívül a processz. Ha ez így van, akkor a jelzés nem is generálódik. Ezt csak akkor lehet megtenni, ha a jelzés engedélyezett, mert letiltott jelzés nem veszhet el, hiszen a processz engedélyezés előtt megváltoztathatja a jelzés elrendezését.

245

5. fejezet: Párhuzamos programozás

A második eset az, amikor a kernel jelzést generál, és megpróbálja azonnal kézbesíteni. Ez akkor lehetséges, ha a címzett processz már fut. Ez úgy fordulhat elő, hogy a kernel hardvermegszakítás miatt generálta a jelzést (például nullával való osztás), amelyet az éppen futó processznek kell kézbesítenie. A másik lehetőség az, ha a processz saját magának küldte a jelzést. Ezekben az esetekben a generálás után azonnal megtörténik a kézbesítés is. A nem azonnal kézbesített jelzések esetében a generáláskor a kernel várakozási sorba helyezi el a jelzést, és beállít egy jelzőbitet a processzleíróban. A kernel két várakozási sort tart fenn a függőben lévő jelzések számára: a privát várakozási sort szálanként hozza létre, míg a megosztott várakozási sor a processznek szóló jelzéseket tartalmazza. A várakozási sor maximális méretét az operációs rendszer konfigurációs paraméterei között tartjuk nyilván (RLIMIT SIGPENDING). Ha a generált jelzés nem valós idejű, és van már ilyen jelzés a várakozási sorban, akkor nincs további lépésre szükség, a generálás véget ér. Ellenkező esetben a kernel hozzáadja a jelzést a megfelelő sorhoz. Ha a jelzés engedélyezve van, akkor a kernel beállítja a processzben a jelzés érkezését jelölő jelzőbitet, és megpróbálja felébreszteni a processzt, vagyis várakozási állapotból átviszi a futásra kész állapotba, és hozzáadja a futásra kész processzek listájához. A kernel minden egyes alkalommal, amikor kernelüzemmódból felhasználói üzemmódba kapcsol azért, hogy egy adott processzt futtasson, ellenőrzi, hogy ennek a processznek van-e függőben lévő jelzése. Tipikusan ilyen átkapcsolás történik a taszkváltás, illetve rendszerhívásokból való visszatérés esetén. Ha vannak ilyen jelzések, akkor a kernel kézbesíti őket. Így, amikor processz futtatása legközelebb sorra kerül, a kezdetét veszi a kézbesítés folyamata. Mivel a kernel nem hoz létre minden jelzésnek külön várakozási sort, csak kettőt tart fenn, kézbesítéskor nem sorrendben dolgozza fel az üzeneteket, hanem „válogat" a bennük lévő jelzések között. A kernel elsőként elkezdi kiüríteni először a privát, majd utána a megosztott várakozási sort. Először a legalacsonyabb számú jelzéseket dolgozza fel, az azonos értékűeket pedig érkezési sorrendben. Ha a jelzés elrendezése a kernel alapértelmezett mechanizmusa, akkor meghívódik az alapértelmezett mecahnizmus. Jól ismert kivételt jelent az ínit processz, ez esetben ugyanis a kernel figyelmen kívül hagyja a jelzést. Ha a jelzés elrendezése egy jelzéskezelő meghívását írja elő, akkor ezt a kernelnek kell meghívnia. Mivel a függvény a felhasználói címtérben van, a kernelnek át kell kapcsolnia. Ez több problémát is felvet. A függvénynek nem a veremben található címre kell visszatérnie, hanem a kernelbe. Ezért a kernel a jelzéskezelő hívása előtt beállítja a sigreturn függvény címét, amely egy rendszerhíváson keresztül visszatér a kernelbe. Ráadásul, mivel a jelzéskezelők rendszerhívásokat is használhatnak, fontos, hogy a kernel helyett ne a megszakított főprogramba térjen vissza a függvény. Ezért a jelzéskezelő futtatásához a kernel nem használhatja a megszakított program veremtartalmát.

246

5.6. Jelzések

A Linux erre azt a megoldást használja, hogy létrehoz egy vermet a felhasználói címtartományban (ennek helyét mi is megadhatjuk a signaltstack függvénnyel), arra átmásolja a kernel kontextusát, köztük a jelzés esetleges paramétereit, ezután beállítja a visszatérési címet a sigreturn címére, majd elugrik a jelzéskezelő kezdőcímére. A jelzéskezelő végeztével a sigreturn rendszerhívás átkapcsol kernelüzemmódba, visszaállítja az eredetileg megszakított program kontextusát, majd átadja neki a vezérlést. Sokszor nem akarunk visszatérni a jelzéskezelőből, kényelmesebb elugrani egy program adott pontjára. Ehhez a setjmp longjmp párost választhatjuk. Az előbbi elmenti a kontextust a program egy adott pontján egy bufferba, a longjmp pedig visszaállítja. Ez azért fog működni, mert a longjmp a kernel által beállított vermet felülírja a program setjmp által elmentett kontextusával, beleértve az utasításszámlálót is. Így a program a megfelelő veremtartalommal és kontextussal folytatja a futását a setjmp által kijelölt helytől. Könnyen lehet azonban, hogy a jelzéskezelőben más jelzések vannak letiltva és engedélyezve, mint a setjmp által meghatározott ponton. Mivel a setjmp nem menti el a jelzésmaszkot, a jelzéskezelőben érvényes maszkkal fut tovább a program. Ha ezt el szeretnénk kerülni, akkor a használjuk a sigsetjmp és a siglongjmp függvényeket, amelyek mindössze annyiban különböznek a setjmpllongjmp párostól, hogy ezek elmentik, illetve visszaállítják a jelzésmaszkot is. A processz létrehozásával kapcsolatos szabályok az alábbiak. Ha a fork hívással hozunk létre új processzt, az örökli a szülő által beállított elrendezéseket és a jelzésmaszkot, viszont a függőben lévő jelzéseket nem. Az exec függvénycsalád a jelzéskezelőkre beállított elrendezést alapértelmezettre állítja, ugyanis az új program betöltésével a jelzéskezelők címe érvénytelenné válik. Minden egyéb jelzésekkel kapcsolatos beállítás megmarad. -

5.6.3. A jelzéskezelő és a főprogram egymásra hatása Elsőként vizsgáljuk meg annak hatását, hogy egy jelzés milyen állapotban éri a processzt. Azt az esetet már korábban tárgyaltuk, amikor a processz éppen fut: ilyenkor a kernel a jelzést azonnal kézbesíti. A rendszer várakozó állapota valójában kétféle állapotot takar: megszakítható (INTERRUPTIBLE) és megszakíthatatlan (UNINTERRUPTIBLE). Ez a megkülönböztetés pontosan a jelzések miatt létezik: megszakíthatatlan állapotban a processz nem fogadhat jelzést. Ilyenkor a generálás megtörténik, de a kézbesítés majd csak akkor, ha a processz elhagyja ezt az állapotot. A processz általában meglehetősen kevés időt tölt ebben az állapotban, főként merevlemezzel kapcsolatos műveletek esetében, ezért ez legtöbbször nem okoz problémát. Nagyon ritkán, de előfordulhat, például valamilyen lemezhiba folytán, hogy a processz beragad ebbe az állapotba. Természetesen ilyenkor még SIGKILL t sem képes fogadni, csak a rendszer újraindítása segít a prob-

247

5. fejezet: Párhuzamos programozás

lémán. Ezt elkerülendő, a Linux egy újabb állapotot is bevezet a megszakíthatatlan állapoton belül, ez a KILLABLE. Ebben az állapotban csak a SIGKILL érkezésekor ébreszti fel (futó állapotba viszi át) a processzt, amelynek a futása a jelzés fontosságának megfelelően rögtön megszakad. A megszakítható állapotban érkezett jelzések különösen fontosak a programozó számára, ugyanis ebben az állapotban szoktak várakozni a blokkolódó rendszerhívások, például a read függvény. Ilyenkor a jelzés megszakítja a rendszerhívást, majd a kernel lefuttatja a jelzés elrendezését. Ezután két választási lehetőségünk van: a rendszerhívásból visszatérünk EINTR értékkel, vagy újraindítjuk a rendszerhívást. A jól megírt program fel van készülve az EINTR visszatérési értékre, és szükség szerint újraindítja a rendszerhívást. Ezt már bemutattuk a 4.1.5. Részleges és teljes olvasás alfejezetben: while((len = read(STDIN_FRENo, buf, sizeof(buf))) != 0) { if(len == -1) { i f(errno == EINTR) {

continue; // ujra megprobaljuk

perror("read"); return -1;

if(write(STDOUT_FILENO, buf, len) == -1) perror("write"); return -1; }

Erre a megoldásra akár makrót is definiálhatunk: ha a visszatérési érték hiba, és az errno értéke EINTR, akkor egy while ciklusban még egyszer meghívjuk a függvényt: #define SIGNAL_RESTART(FUNC) while((FuNC) == -1 && errno == EINTR); • • •

SIGNAL_RESTART(len = read(STDIN_FILEN0, buf, sizeof(buf))) i f(len = -1) // További hibák kezelése

248

5.6. Jelzések

Minden pontenciálisan blokkolódó hívás még a fenti makróba csomagolva sem a legkényelmesebb megoldás. A sigaction függvény SA_RESTART paraméterével megadhatjuk, hogy az egyes jelzéseknél a rendszer automatikusan újraindítsa (újra meghívja) a megszakított rendszerhívást. Ennek használata elég körülményes, mert jelzésenként kell beállítani az újraindítást, valamint nem minden rendszerhívás indítható újra. Ezt az információt a kézikönyv 7. fejezetében található signal oldala írja le. A másik problémát a függvények nem globális adatai okozhatják (globális változók, statikus lokális változók). Például a printf függvény globális buffereket módosít. Ha a printf egy futása éppen módosítja a globális buffert, de még nem végzett a művelettel, ebben az inkonzisztens állapotban meghívódik egy jelzéskezelő, amely szintén hív egy printf-et, amely a globális buffert inkonzisztens állapotban találja. Ilyenkor nagyon nehéz megjósolni, mi lesz a program működése, azt viszont nem nehéz, hogy ez nagy valószínűséggel nem az elvárt viselkedés lesz. Hasonló a probléma a memóriafoglalásnál és -felszabadításnál (malloclfree), amelynek számos implementációja globális láncolt listában tartja az adatokat. Ezeket a függvényeket nem reentráns függvényeknek nevezzük. Ha a jelzéskezelőben is és a főprogramban is használunk nem reentráns függvényeket, a program működése definiálatlan. Ezt általában az alábbi szabály betartásával szokták elkerülni. Útmutató Ne használjunk a jelzéskezelőben nem reentráns függvényeket.

A reentráns függvények listáját szintén a signal (7) kézikönyoldala tartalmazza (jelzésbiztos függvényeknek nevezve őket). Tipikusan azok a függvények nincsenek ezek között, amelyek valamilyen I/O műveletet végeznek, vagy a malloc/ free függvények valamelyikét hívják. A következő egymásra hatás abból ered, hogy egy műveleti hardveresemény által kiváltott jelzés (SIGBUS, SIGILL, SIGSEGV, SIGFPE) feldolgozása után a megszakított program ugyanonnan folytatja a futását, ahol abbahagyta. Ha például nullával való osztás történt, a jelzés feldolgozása és a jelzéskezelő normál visszatérése után folytadódik a program, amely újra előidézi a nullával való osztást és vele együtt a jelzést is. Útmutató A hardveresemény által kiváltott jelzések esetében törekedjünk a legegyszerűbb megoldásra: hagyjuk érvényesülni az alapértelmezett kezelés mechanizmusát. Ha ez mégsem oldható meg, azok jelzéskezelőit vagy ugróutasítással (siglongjmp, longjmp), vagy az exit függvénnyel hagyjuk el.

Ha egy hardveresemény által kiváltott jelzést figyelmen kívül hagyunk, a kernel ennek ellenére kézbesíti. Ha letiltjuk őket, akkor terminálja a programot.

249

5. fejezet: Párhuzamos programozás

5.6.4. Jelzések és a többszálú processz Többszálú programok esetén a szabványok és az implementációk arra törekedtek, hogy megtartsák a jelzések eredeti viselkedését abban az esetben, amikor a processznek küldünk jelzéseket, ugyanakkor megpróbálták ezt kiegészíteni egy intuitív és használható szálak közötti jelzésküldéssel. Egy többszálú programban a szálak nem osztoznak az alábbiakon: •

jelzésmaszk,



a már említett privát várakozási sor,



a kernel általt a jelzéskezeló'k számára létrehozott ideiglenes verem.

Ez azt jelenti, hogy a jelzéselrendezések viszont közösek a szálakra nézve: ha egy szál megváltoztat egy elrendezést, az az egész processzre, így a többi szálra is érvényes. Az alapértelmezett elrendezések közül a STOP és a terminálás processzszintű: a kernel az összes szálat megállítja, illetve terminálja. A legfontosabb kérdés az, hogy ki kapja a többszálú processznek küldött jelzést. Az egyes szálaknak külön küldött jelzést (pthread_kill, pthread_sigqueue) az adott szál kapja. Az azonnal kézbesített jelzéseket (harverjelzések és a processz önmagának küldött jelzései), valamint a SIGPIPE jelzést szintén. A többit a kernel mind a processz várakozási sorában helyezi el. Ha a kernel a processz várakozási sorában lévő jelzést dolgoz fel, amelynek elrendezése egy jelzéskezelő, akkor kiválaszt egy tetszőleges szálat, amely nem tiltja le az adott jelzést, és ezt megszakítva futtatja le a jelzést. Ha mindegyik blokkolja a jelzést, az természetesen a várakozási sorban marad. Sajnos a szálkezelő függvényeket, beleértve a szinkronizációt is, nem használhatjuk a jelzéskezelőben. Útmutató Szálakban ne használjunk aszinkron jelzéskezelést. Lehetőleg egyetlen szálban engedélyezzük a jelzéseket, és ott szinkron módon kezeljük őket.

5.6.5. Jelzések programozása A jelzések működése után vegyük sorra a programozás részleteit. A programkódban kétféleképpen valósíthatunk meg jelzéskezelést. Az egyik a már ismertetett megoldás, amikor jelzéskezelőt adunk meg. A másik lehetőség — amely nagyon hasonlít a select/poll függvényekhez — az, hogy letiltjuk az összes letiltható jelzést, és egy függvénnyel várakozunk arra, hogy a processz/szál várakozási sorába jelzés érkezzen Amikor a függvény visszatér, kezeljük a jelzést. Ez utóbbi sokszor nagyon praktikus lehet — kifejezetten szálak esetén —, sem a reentráns függvényekkel, sem pedig a jelzések elveszítésével nem kell foglalkoznunk. Az első módszert aszinkron jelzéskezelésnek (asynchronous signal handling), míg a másodikat szinkron jelzéskezelés-

250

5.6. Jelzések

nek (synchronous signal handling) nevezzük. Hangsúlyozni kell, hogy csak a jelzéskezelés szinkron, a kommunikáció formája továbbra is aszinkron, hiszen a jelzést küldő processz/szál nem várakozik. A jelzéskezélésnél viszont nem tetszőleges helyen szakítja meg a programot, hanem szinkronizáltan, a várakozófüggvény belsejében. Nem kezelhetjük szinkron módon sem a SIGKILL, sem a SIGSTOP jelzéseket, hiszen ezek elrendezését nem változtathatjuk meg, nem tilthatjuk le őket, valamint nem is várakozhatunk rájuk. A továbbiakban sorra vesszük azokat a függvényeket, amelyekkel mindezt megvalósíthatjuk.

5.6.5.1. Jelzések küldése Jelzéseket a ki//0 rendszerhívással küldhetünk: #include #include int kill(pid_t pici, int sig);

Apid argumentum a processz azonosítója, a sig pedig a jelzés azonosítója. A pid speciális értékeivel lehetőségünk van arra, hogy az ínit processzen és saját magán kívül az összes processznek (-1) küldjünk jelzést. Ha a sig argumentum nulla, akkor a ki//0 függvény nem küld jelzést, de a hibaellenőrzést elvégzi. Így megnézhetjük, hogy van-e jogunk és lehetőségünk jelzést küldeni egy processznek. Ha a pid kisebb, mint nulla, akkor a kernel a változó abszolút értékének megfelelő processzcsoportnak küldi a jelzést. Ezt a konverzíót elvégzi helyettünk az alábbi függvény, ahol közvetlenül megadhatjuk a processzcsoport azonosítóját: #include #include ,nt main() sigset_t alarmMask, fullmask; int fd; struct signalfd_siginfo info; int ret; sigemptyset(&alarmmask); sigaddset(&alarniMask, SIGALRM); // Az összes jelzés letiltása si gfi 1 1 set(8/ful 1 mask) ; sigprocmask(5IG_BLOCK, &filllmask, NULL); /7 Leíró létrehozása fd = signalfd(-1, &alarmmask, 5Fp_N0NBL0Ck); alarm(10); // 10 masodperc múlva SIGALRM jelzés // A select az stdinnre es a jelzés leírójára figyel fd_set readfds; FD_SET(STDIN_FILENO, &readfds); FD_SET(fd, &readfds); select(fd+1, &readfds, NULL, NULL, 0); if(FD_ISSET(fd, &readfds)) // Na benne van az eredményhalmazban {

ret = read(fd, &info, sizeof(struct signalfd_siginfo)); printf("%d processz jelzest kuldott.\n", info.ssi_pid); }

26]

5. fejezet: Párhuzamos programozás

close(fd); // Le kell zárnunk az állományleírót printf("megérkezett vagy megnyomták.\n"); return(alarm(0)); // Kikapcsoljuk az időzitőt }

5.6.6. A SIGCHLD jelzés A processzek tárgyalásánál (lásd az 5.1. Processzek alfejezetben) már említettük, hogy a processzek a kilépés után nem szűnnek meg teljesen: néhány lekérdezhető statisztikát és a kilépési értéket tartalmazó adatstruktúrákat megőrzik, hogy a szülőprocessz le tudja kérdezni ezeket. Ezt a processzállapotot neveztük zombinak. A zombiprocesszeket úgy szabadíthatjuk fel, hogy meghívjuk valamelyik wait függvényt vagy blokkok, vagy nem blokkolt módon. A gyermekprocessz kilépéséról, azaz zombiállapotba kerüléséról a szülőprocessz SIGCHLD jelzést kap. Mivel a wait függvények jelzésbiztosak, a jelzésből rögtön meg is hívhatjuk ó'ket az alábbiak szerint: i nt oldErrno = errno; while (waitpid( - 1, NULL, WNOHANG) > 0) continue;

errno = oldErrno;

A fenti kódrészlet végigiterál az összes kilépett (zombi) gyermekfolyamaton. Ha nincs több ilyen gyermekfolyamat, a függvény nullával tér vissza, és a ciklus véget ér. Akkor is kilépünk, ha hiba történt. Mivel az errno változót a főprogram is használhatja, ezért elmentjük, és visszaáWtjuk, ha a waitpid esetleg megváltoztatta volna. A zombik begyűjtésére ez a leghordozhatóbb megoldás. Útmutató A jelzéskezelőkben mindig gondoskodjunk az errno változó elmentéséről és visszaállításáról, ha olyan függvényt hívunk, amely átállíthatja az errno változót.

Ha azt szeretnénk, hogy a szülő értesítést kapjon arról, hogy a gyermekprocessz jelzést kapott, akkor a sigaction függvénynek meg kell adnunk a SA_NOCLDSTOP jelzőbitet, amikor a SIGCHLD jelzéskezelőjét beállítjuk. Ha le szeretnénk tiltani azt, hogy egy már létező processz zombivá alakuljon, több módszer is létezik. A SIGCHLD jelzés alapértelmezett beállítása a figyelmen kívül hagyás. Ennél az egy jelzésnél mást jelent, ha meghagyjuk az alapértelmezettet, vagy expliciten beállítjuk a jelzés figyelmen kívül hagyását. Az utóbbi esetben a processz gyermekprocesszeiből nem képződik zombi,

262

5.6. Jelzések

kilépéskor megszűnnek A beállítás előtt már zombivá alakult folyamatok viszont nem szűnnek meg. Nagyon hasonló működést érhetünk el, ha a sigaction hívás SA_NOCLDWAIT kapcsolójával állítjuk be e jelzéskezelőt. Ilyenkor szintén nem alakulnak zombivá a gyermekprocesszek, viszont megszűnésükről jelzést kapunk. Ekkor természetesen nem férünk hozzá a statisztikákhoz és a visszatérési értékekhez. Figyeljünk arra, hogy a jelzéskezelőt vagy a figyelmen kívül hagyást még az első gyermekprocessz létrehozása előtt beállítsuk, különben lemaradhatunk az értesítésről.

263

HATODIK FEJEZET

Hálózati kommunikáció Mai, számítógép-hálózatokkal átszőtt világunkban a Linux rendszerek egyik fő alkalmazási területét a hálózati alkalmazások jelentik. Ebben a fejezetben ezeknek a kommunikációknak a megvalósítási alapjait ismerjük meg. Nem foglalkozunk az összes protokollal, jóllehet a Linux többet is támogat (TCP/IP, AppleTalk, IPX stb.). Ezek részletes ismertetése meghaladná a könyv kereteit. Vizsgálódásunk egyik területe a Berkeley socket-API, amely tulajdonképpen egy általános kommunikációs interfész. Mi két implementációját tárgyaljuk. A legfontosabb a TCP/IP protokoll használata, amely lényegében működteti az internetet. A másik, egyszerűbb protokoll a Unix domain socket, amely tulajdonképpen nem hálózati kommunikáció, hanem egy olyan IPC-mechanizmus, amely csak egy gépen belül használható, ám a programozás hasonlósága miatt itt tárgyaljuk. Továbbá megismerhetjük a TCP/IP-re épülő távoli eljáráshívást (Remote Procedure Calling, RPC). Ez egy magasabb szintű, általában egyszerűbben használható kommunikációs metódus.

6.1. A socket Mint a Linux más erőforrásait, a socketeket is a fájlabsztrakciós interfészen keresztül implementálták a rendszerbe. A hálózati kapcsolatnak azokat a végpontjait, amelyeket a programozó használhat, socketeknek nevezzük, ezek egyben egy állománytípust is jelentenek. Létrehozásuk a socket0 rendszerhívással történik, amely egy állományleíróval tér vissza. Miután a socketet inicializáltuk, a read0 és a write() függvényekkel kezelhető, mint minden más állományleíró, használat után pedig a close0 függvénnyel le kell zárnunk. Új socketeket a socket0 rendszerhívással hozhatunk létre, amely az inicializált socket állományleírójával tér vissza. Létrehozásakor a sockethez egy meghatározott protokollt rendelünk, ám ezek után még nem kapcsolódik sehová. Ebben az állapotában még nem olvasható vagy írható:

6. fejezet: Hálózati kommunikáció

#include

int

.ocket (i nt domain, í nt type, int protocol);

Mint az openO, a socket() is 0-nál kisebb értékkel tér vissza hiba esetén, és ha sikeres, az állományleíróval, amely 0 vagy annál nagyobb. Három paraméter definiálja a használandó protokollt. A domain a protokollcsaládot adja meg, és értéke a 6.1. táblázatban található lista valamelyike: 6.1. táblázat. A protokolcsctládokhoz tartozó értékek Protokoll

Jelentés

PF_UN1X, PF_LOCAL

Unix domain socket (gépen belüli kommunikáció)

PF INET

IPv4 protokoll

PF INET6

IPv6 protokoll

PF IPX

Novell IPX

PF NETLINK

A kernel felhasználói interfésze

PF X25

X.25 protokoll

PF AX25

AX.25 protokoll, a rádióamatőrök használják

PF ATMPVC

Nyers ATM-csomagok

PF APPLETALK

AppleTalk

PF PACKET

Alacsony szintű csomaginterfész

A következő paraméter, a type, a protokollcsaládon belül a kommunikáció módját definiálja a 6.2. táblázatban található értékek egyikével. 6.2. táblázat. A kommunikáció lehetséges módjai Típus

Jelentés

SOCK STREA11,1

Sorrendtartó, megbízható, kétirányú, kapcsolatalapú byte-folyam-kommunikációt valósít meg.

SOCK DGRA111

Datagramalapú (kapcsolatmentes, nem megbízható) kommunikáció.

SOCK SEQPACKET

Sorrendtartó, megbízható, kétirányú, kapcsolatalapú kommunikációs vonal, fix méretű datagramok számára.

SOCK RAW

Nyers hálózatiprotokoll-hozzáférést tesz lehetővé.

SOCK RDM

Megbízható datagramalapú kommunikációs réteg. (Nem sorrendtartó.)

SOCK PACKET

Elavult opció.

266

6.2. Az összeköttetés-alapú kommunikáció

Az utolsó paraméter, a protocol, a protokollcsaládon belül konkretizálja a protokollt az adott kommunikációs módhoz. A családokon belül általában csak egy protokoll létezik egy típusú kommunikációra, amely egyben az alapértelmezett. Így ennek a paraméternek az értéke leggyakrabban O. Például a PF_INET családon belül a TCP (Transmission Control Protocol) az alapértelmezett folyamprotokoll, és az UDP a datagramalapú. Ám egy kivételt is mutathatunk. Ez a SOCK RAW kommunikációs mód. A SOCK RAW mód például a PF INET család választásával lehetővé teszi olyan socketek létrehozását, ahol az IPv4 kommunikációs protokoll implementálását a felhasználói tartományban végezzük el. Ám ennél többre is képes. Az utolsó, protocol paraméterként megadhatunk a 0 mellett olyan értékeket is, mint az IPPROTO_RAW, az IPPROTO_ICMP, az IPPOROTO_IGMP, az IPPROTO TCP, az IPPROTO_UDP. Ezzel olyan protokollokat is elérhetünk a családból, amelyet a többi paraméterrel nem (ICMP, IGMP). 78 A kommunikációs típusok listáját végignézve láthatjuk, hogy alapvetően két részre oszlanak: a kapcsolat- vagy összeköttetés-alapú, illetve a kapcsolat vagy összeköttetés nélküli kommunikáció. Az összeköttetés-alapú kommunikáció egy kapcsolódási folyamattal kezdődik, amelynek eredményeképpen egy folyamatos kapcsolatot hozunk létre két fél között. Ennek során lényegében egy kétirányú csatorna jön létre, amelyen a programok oda-vissza kommunikálhatnak. A kommunikáció végén pedig le kell bontani a csatornát. Az összeköttetés nélküli kommunikáció esetében viszont elmarad a kapcsolódási folyamat. A kiküldött csomagjainkat külön-külön címezzük és küldjük a többi kommunikációs résztvevőnek. Továbbá bárkitől kaphatunk is csomagot. A kommunikáció végén a kapcsolatot sem kell lebontanunk, csak abbahagyjuk a kommunikációt. Analógiaként nézhetjük a telefonálást és az SMS-küldést. A telefonálás a kapcsolatalapú kommunikációra hasonlít. Tárcsáznunk kell a kommunikációs partnerünket, neki fogadnia kell a hívást, beszélgetünk, és a végén bontunk. Az SMS esetén azonban nem kell végigvinnünk a hívási folyamatot, csak megcímezzük az üzenetet, és elküldjük. Ugyanígy bármikor kaphatunk másoktól is üzeneteket.

6.2. Az összeköttetés-alapú kommunikáció Első lépésként az összeköttetés-alapú kommunikációt tekintjük át. Ennek a kommunikációs módnak lényeges része a kapcsolat felépítése Amikor ez létrejön, a kommunikáció hasonlóan történik, mint korábban a csővezetékeknél láttuk. Majd a párbeszéd végén valamelyik fél bontja a kapcsolatot. Nézzük meg sorban ezeket a műveleteket. 78

A SOCK RAW módot csak rendszergazdai jogosultságokkal futó folyamatokban használhatjuk. 267

6. fejezet: Hálózati kommunikáció

6.2.1. A kapcsolat felépítése Ha egy összeköttetés-alapú kapcsolatotadatfolyam-socketet (stream socket) hoztunk létre, akkor hozzá kell kapcsolódnunk egy másik géphez. A socket kapcsolódása aszimmetrikus művelet, vagyis a létrejövő kapcsolat két oldalán a feladatok eltérnek egymástól. A kapcsolat felépítését követően a kommunikáció viszont már szimetrikus. A kliensoldalon a socket létrehozása után kapcsolódunk (connect) a szerverhez. Ha a kapcsolódás sikeres (a szerver elfogadja a kapcsolódási kérelmet), akkor a socketen mint állományleírón keresztül tudunk adatot küldeni és fogadni. Ezt a típusú socketet klienssocketnek nevezzük. A szerver és a kliens közti lényeges különbség a kapcsolat felépítésében és a kezelendő socketek számában rejlik. Ennek megfelelően a szerveroldalon kétfajta socket található. Az egyiket a továbbiakban szerversocketnek hívjuk. Funkciója szerint várakozik (listen) egy címen, amelyet előre megadtunk (bind), és arra vár, hogy a kliensoldali socketek kapcsolódjanak (connect) hozzá. Ha egy kapcsolódási kérelem érkezik, a szerver elfogadja (accept), ennek eredményeképpen létrejön egy klienssocket. Ezen keresztül történik az adatcsere. A kapcsolódási kérelem elfogadása után a kommunikáció a szerversockettől függetlenül zajlik, a szerversocket csak további kapcsolódási kérelmekre vár. A szerveroldalon ezért gyakran több klienssockettel kell törődnünk.

6.2.2. A socket címhez kötése Ahhoz, hogy a kliens meg tudja címezni a szervert a kapcsolódáskor, a szervernek az adott címhez kell kötnie a socketjét. Ez egy olyan helyi cím, ahol a szerver a bejövő kapcsolatokat várja. A kliensprogramnál is lehetőség van lokális cím megadására, ez azonban nem kötelező, mert nem hivatkozunk a címre. Ilyenkor a rendszer automatikusan egy címet generál a kapcsolódáskor. A címhozzárendelés műveletét kötésnek (binding) nevezzük, és a bind() rendszerhívással tehetjük meg: #include int bí nd(i nt sock , struct sockaddr *my_addr, socklen_t addrlen);

Az első paraméter a socket leírója, a második a címet leíró struktúra, az utolsó a címet leíró struktúra hossza. A címstruktúra alakja itt nincs specifikálva, mivel protokollonként eltérő a címábrázolás. Mindig az adott protokoll címalakját használjuk. Mivel az egyes címtípusoknak a méretigénye eltérő, ezért kell megadnunk a cím hosszát.

268

6.2. Az összeköttetés-alapú kommunikáció

6.2.3. Várakozás a kapcsolódásra A címhez kötött socketünk önmagában még nem alkalmas a kapcsolatok fogadására, még nem lesz belőle szerversocket. A processz a listen() rendszerhívással állítja be a socketre ahhoz, hogy fogadja a kapcsolódásokat, majd a kernel kezeli a kapcsolódási igényeket. Egy kapcsolódási igény beérkezése után azonban még nem épül fel azonnal a kapcsolat. A várakozó processznek az acceptO rendszerhívással kell elfogadni a kapcsolódást. Azokat a kapcsolódási igényeket, amelyeket az accept0-tel még nem fogadott, függőben lévő kapcsolatnak (pending connection) nevezzük. Normál esetben az accept() függvény blokkolódik, amíg egy kliens kapcsolódni nem próbál hozzá. Természetesen átállíthatjuk az fenti() rendszerhívás segítségével nem blokkolódó módba is. Ilyenkor az accept0 azonnal visszatér, amikor egyetlen kliens sem próbál kapcsolódni. Mivel a szerversocket is állomány, a selectQ vagy a poll() rendszerhívást is alkalmazhatjuk a kapcsolódási igények észlelésére. Ezekben az esetekben a szerversocketre érkezett kapcsolódási igény mint olvasási esemény érzékelhető. A listen() és az accept() függvények formája a következő: #include int listen(int sock, int backlog); int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

Első 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 ez a függőben lévő kapcsolatok várakozási lista mérete. Az accept0 fogadja a kapcsolódásokat. Visszatérési értéke az új kapcsolat leírója, egy klienssocket. Az addr és az addrlen paraméterekben a másik oldal címét kapjuk meg. Az addr által mutatott struktúrába a cím, az addrlen által mutatott változóba pedig a cím mérete kerül.

6.2.4. Kapcsolódás a szerverhez A kliens a létrehozott socketet a szerverhez hasonlóan a bind0 rendszerhívással hozzárendelheti egy helyi címhez. Ezzel azonban a kliensprogram általában nem törődik, a kernelre bízza, hogy ezt automatikusan megtegye. Ezek után a kliens a connectO rendszerhívással kapcsolódhat a szerverhez: #include int connect(int sock, struct sockaddr *addr, socklen_t addrlen);

269

6. fejezet: Hálózati kommunikáció

Az első paraméter a socket leírója, a további paraméterek a szerversocket címét adják meg. A kapcsolat felépülését a 6.1. ábra mutatja. Kliens

Szerver

socket() bind() (isten() socket()

accept()

connect()



write()

Kommunikáció ►



read()

read() 4

write()

close()

close()

6.1. ábra. Az összeköttetés-alapú kommunikáció

6.2.5. A kommunikáció A kapcsolat felépítését követően a szerveroldalon az accept0 által visszaadott klienssocketet, kliensoldalon pedig a kapcsolathoz létrehozott socketet két összekapcsolt állományleíróként használhatjuk, mint korábban a csővezetékeknél. Amit az egyik oldalon a write() függvénnyel beleírunk, azt a másik oldalon a read0 függvénnyel olvashatjuk. Ám eltérően a csővezetékektől a kommunikáció kétirányú, vagyis mindkét oldalon írhatunk és olvashatunk is

6.2.6. A kapcsolat bontása A socketkapcsolat lebontása tipikusan az állományoknál megismert close0 rendszerhívással történik Ha a kapcsolat egyik végét lezárjuk, akkor azt a másik oldal is érzékeli, amikor legközelebb olvassa a kapcsolat klienssocketjét. Ekkor a socket már nem használható további kommunikációra, ezért a leírót a túloldalon is lezárjuk. 270

6.2. Az összeköttetés-alapú kommunikáció

A shutdown0 rendszerhívás lehetőséget ad arra is, hogy a kétirányú kommunikációból csak az egyik irányt zárjuk le: #include ‹sys/socket.h> int shutdown(int sockfd, int how);

Az első paraméter a socket leírója. A második a lezárás módját adja meg a 6.3. táblázatban található értékek közül. 6.3, táblázat. A shutdown módjai

Típus

Jelentés

SHUT RD

Csak az olvasási ágat zárjuk le. Az olvasás „állomány vége" jelzést (0 visszatérési érték) ad vissza. Írni továbbra is tudunk a socketbe.

SHUT WR

Csak az írási ágat zárjuk le. Amikor a túloldal teljesen kiolvasta a korábban elküldött tartalmat, egy „állomány vége" jelzést kap. A további írási próbálkozásra hibát kapunk, olvasni viszont továbbra is tudjuk.

SHUT RDWR

Mind az olvasási, mind az írási ágat lezárjuk. Mintha az előző két lezárást egyaránt elvégeznénk.

Ha a shutdown0 rendszerhívást a SHUT RDWR opcióval hívjuk meg, akkor a működés hasonlít a close() rendszerhíváséra. Néhány jelentős eltérés azonban van a két megoldás között. A close0 az állományleírót zárja le, míg a shutdown0 a kommunikációs csatornát. Vagyis ha a dup0 rendszerhívással a socketleíróról másolatot készítünk, akkor a close0 csak egy leírót zár le. A kapcsolat csak akkor záródik le, ha minden leírót lezártunk. Ezzel szemben, ha a shutdown0-t bármelyik másolatra meghívjuk, akkor lezárja a kapcsolatot, és a többi másolaton keresztül sem folytathatjuk a kommunikációt. Ha a másolat a fark() rendszerhívás hatására születik, akkor is hasonló a helyzet. Ugyanakkor a shutdown0 nem zárja le az állományleírót. Ezért használata után a close0 meghívására van szükség.

6.2.7. További kapcsolatok kezelése a szerverben A 6.1. ábrán látható kommunikációs folyamat során a szerver csak egy kapcsolatot képes kiszolgálni, hiszen a kapcsolat felépülése után nem hívja meg ismét az accept() függvényt. A szerverek többségénél azonban nem ez a kívánt működési mód. A tipikus elvárás az, hogy egy szerver több kapcsolatot is képes fogadni, és párhuzamosan kiszolgálja a klienseket. Ennek megvalósításához a szerverimplementációnkban párhuzamosan kell kommunikálnunk a 271

6. fejezet: Hálózati kommunikáció

kliensekkel a klienssocketeken keresztül, közben ismét meg kell hívnunk az accept() függvényt a szerversocketre. (A 6.5.12.2. TCP szerver alkalmazás al-

fejezetben láthatunk majd példákat a szerverfeladatok párhuzamosítására.)

6.3. Az összeköttetés nélküli kommunikáció Összeköttetés-alapú kommunikáció esetén a kommunikációt megbízható adatfolyamként foghatjuk fel. Ez egyrészt kényelmes megoldás, mert nem kell foglalkoznunk azzal, hogy az adat, amelyet elküldtünk, valóban célba ért-e: a kommunikációt végrehajtó függvények jelzik a hibát vagy a kapcsolat bontását. Másrészt azonban az adatok szinkronizációja több csomagforgalmat és kernelerőforrás-használatot (CPU, memóriabufferek, időzítők) is jelent. Ezért olyan alkalmazások esetén, ahol „nem túl nagy probléma", ha elveszik „egy-egy" csomag, ott alkalmazhatunk összeköttetés nélküli megoldást, amely egyben gyorsabb kommunikációt eredményez. Ilyenek tipikusan a multimédia-alkalmazások, hiszen ha egy zene hangmintáit küldjük el, nem jelent számottevő minőségromlást egy-egy keret kimaradása, valamint, ha későn érkezik, nem is tudunk mit kezdeni vele, hiszen már előrébb tartunk a lejátszásban. Az összeköttetés nélküli kommunikáció esetén létrehozunk egy socketet (socket()), és egy címhez rendeljük (bind()). Ezen keresztül fogadjuk a hozzánk érkező csomagokat (datagram). Ugyanakkor nemcsak fogadjuk a csomagokat, de küldünk is a többi alkalmazás számára. Ám mivel létrejött kapcsolat híján a rendszer nem tudhatja, kinek szánjuk a csomagokat, ezért egyesével meg is kell címeznünk őket. Így a korábban látott read/write függvények itt nem működnek. 1. fél

2. fél

socket()

socket()

bind()

bind0

sendto() recyfrom()•

Kommunikáció



recvfrom() sendto()

4,

....... ♦ ................................ close() close() 6.2. ábra. Összeköttetés nélküli kommunikáció

272

6.3. Az összeköttetés nélküli kommunikáció

A 6.2. ábra mutatja az összeköttetés nélküli kommunikáció létrehozásának a menetét. A két oldal ebben az esetben szimmetrikus, szemben a korábban látott összeköttetés-alapú kommunikációval. Mindkét oldal működik kezdeményezőként és fogadóként is. Sőt valójában nem is csak két oldal van, hanem több kommunikációs fél is elképzelhető. Ahhoz, hogy a hozzánk érkező csomagokat fogadni tudjuk, a socketünket hozzá kell kötnünk egy címhez. Lehetőségünk van azonban arra, hogy a két kommunikációs fél esetében csak az egyiknél végezzük el a címhez kötést. Ebben a struktúrában a bind() függvényt használó oldal lesz a szerver, a dinamikuscím-hozzárendelést használó oldal a kliens. Felvetődik a kérdés, hogyan deríti ki a szerver a kliens címét, és küld neki csomagot. Ehhez az szükséges, hogy a kliens kezdeményezze a kommunikációt az első csomag elküldésével. A szerver fogadva a csomagot megtalálja benne a kliens címét. Ezt követően a szerver a kapott címre válaszcsomagot tud küldeni.

6.3.1. A kommunikáció Láthatóan az összeköttetés nélküli kommunikáció esetében a korábban megismert readO és write() függvények nem használhatók. Ennek az az oka, hogy nem adatfolyam-jellegű kommunikációt végzünk, hanem csomagokat kezelünk. Ez még megoldható is lehetne az absztrakciós interfészen keresztül, másik nagy hiányosságuk azonban az, hogy nem támogatják a címkezelést. Így helyettük a recufromQ és a sendto() függvényeket kell használnunk. Összeköttetés nélküli kapcsolat esetén tehát az adatfogadás az #include #include ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

függvénnyel történik. Az első három paraméter a readO függvényból már ismerős lehet. A sockfd a socket leírója, a buf a buffer mutatója, amelybe az adatokat várjuk. A len a buffer mérete. Ha a megadott buffer rövidebb, mint amekkora hosszúságú adat érkezett, akkor a recyfrom() a csomag végét automatikusan levágja. A flags paraméter socketspecifikus I/O beállításokra szolgál. Ezeket bővebben a mm() függvénynél tárgyaljuk (lásd a 6.5.12. Összeköttetés-alapú kommunikáció alfejezetben). A src_addr paraméterben egy címstruktúra mutatóját várja a függvény, ha szeretnénk visszakapni tőle a csomag forráscímét. Ezt a struktúrát előre le kell foglalnunk, és az addrlen paraméterben adjuk át a méretét. A visszakapott cím méretét szintén az addrlen paraméterben kapjuk meg. Ha nem vagyunk kíváncsiak a csomag forrására, akkor NULL mutatók megadásával ezt a szolgáltatást kikapcsolhatjuk. 273

6. fejezet: Hálózati kommunikáció

Ameddig nem érkezik adat, a recyfrom() függvény várakozik, majd ezt követően az adatok mennyiségével vagy hibát jelző mínusz értékkel tér vissza, ahogy a read0 függvénynél történik. Az összeköttetés nélküli kapcsolatok esetén küldésre az #include #include ssi ze_t sendto(i nt sockfd, const voí d *buf, si ze_t len, i nt flags, const st ruc t sockaddr *dest_addr, sockl en_t addrlen);

függvényt használjuk. Mint az előző esetben, az első három paraméter hasonlít a writeQ függvény paramétereire. A sockfd a socket leírója, a buf a buffer mutatója, amelyben az adatok találhatók. A len a bufferben lévő adatok mérete. A flags paraméter socketspecifikus I/O beállításokra szolgál. Ezeket bővebben a send0 függvénynél tárgyaljuk (lásd a 6.5.12. Összeköttetés alapú kommunikáció alfejezetben). A dest_addr paraméterben egy címstruktúra mutatóját várja a függvény, amely a célcímet tartalmazza. A struktúra méretét az addrlen foglalja magában. A függvény addig blokkolódik, amíg az adatokat a protokollkezelő rendszer a kimenő bufferbe bele nem tudja írni. Ezt követően visszatér az elküldött byte-ok számával. -

6.3.2. A connect() használata Bár az összeköttetés nélküli kommunikáció esetén nem építünk fel kapcsolatot a két fél között, a connectO függvény mégis használható Ekkor valójában nem kapcsolatot hoz létre, hanem csak bejegyzi a megadott célcímet. Így a csomagok küldésénél nem kell megadnunk mindig a célcímet, mert a connectO nél megadottakat használja. Ezáltal lehetővé válik a readQ és a writeQ függvények használata is csomagok küldésére és fogadására. Az alábbi lista összefoglalja a connectQ függvény hatásait arra a socketre, amelyre meghívtuk: -



A writeQ függvénnyel (vagy a később tárgyalt send() függvénnyel) kiküldött csomagok mind a connectO függvényben megadott címre érkeznek.



A socketen keresztül csak olyan csomagokat kapunk meg, amelyek a connect0 ben megadott címről érkeznek. -



274

A túloldal nem érzékeli, hogy a connect() függvényt használtuk, mivel nem épül fel kapcsolat.

6.4. Unix domain socket

6.3.3. A socket lezárása A socketet mindkét oldalon a closeQ függvénnyel zárhatjuk le. Kapcsolat hiányában azonban ez nem jelent kapcsolatzárást, és a túloldal nem érzékeli, hogy lezártuk a socketet. Csupán annyi történik, hogy a továbbiakban nem fogadjuk a csomagokat.

6.4. Unix domain socket A Unix domain socket a legegyszerűbb protokollcsalád, amely a socket-API-n keresztül elérhető. Valójában nem hálózati protokoll. Csak egy gépen belül képes kapcsolatokat felépíteni. Habár ez komoly korlátozó tényező, mégis sok alkalmazás használja, mivel flexibilis IPC-mechanizmust nyújt. A címei állománynevek, amelyek az állományrendszerben jönnek létre. Azok a socketállományok, amelyeket létrehoz, a stat() rendszerhívással vizsgálhatók, ám nem lehet megnyitni az open() függvénnyel őket. Helyette a socket API-t kell használni. A Unix domain socket támogatja mind az adatfolyam- (stream), mind a csomag- (datagram) kommunikációs interfészt, a datagram interfész azonban ritkán használatos. A stream interfész a megnevezett csővezetékekhez hasonlít, ám nem teljesen azonos velük. Ha több processz megnyit egy megnevezett csővezetéket, akkor egyikük kiolvashatja belőle, amit egy másik beleírt. Lényegében olyan, mint egy hirdetőtábla. M egyik processz elküld egy üzenetet rá, egy másik pedig elveszi onnan. Ezzel szemben a Unix domain socket kapcsolatorientált. Minden kapcsolat egy-egy új kommunikációs csatorna. A szerver egyszerre több klienskapcsolatot kezelhet, és mindegyikhez külön leíró tartozik. E tulajdonságok révén alkalmasabb az IPC-feladatokra, mint a megnevezett csővezeték, ezért sok Linux-szolgáltatás alkalmazza, többek közt az X Window System és a naplórendszer ís.

6.4.1. Unix domain socket címek A Unix domain socket címek állománynevek a fájlrendszerben. Ha az állomány nem létezik, akkor a rendszer, amikor meghívjuk a bind() függvényt, socket típusú állományként létrehozza. Ha az állománynév már használt, akkor a bindQ hibával tér vissza (EADDRINUSE). A bind() a 0666-t állítja be jogosultságnak (módosítva az umask értékével). Ahhoz, hogy a connect() rendszerhívással kapcsolódhassunk a sockethez, olvasási és írási joggal kell rendelkeznünk a socketállományra. 275

6. fejezet: Hálózati kommunikáció

A Unix domain socket címeinek megadásához a struct sockaddr_un struktúrát használjuk: #include #include struct sockaddr_un unsigned short sun_famíly; /* AF_UNIX * / char sun_path[uNIX_PATH_MAx]; /* eleresi u t

*/

;

A sunjamily mezőnek, AF UNIX-nak kell lennie. Ez jelzi, hogy Unix domain socket címet tartalmaz. A sun_path tartalmazza az állománynevet C-stringként, vagyis egy '\0' karakterrel lezárva. A rendszerhívásokban használt címstruktúraméret az állománynév hossza, plusz a sunjamily elem mérete. Bár használhatjuk a teljes struktúraméretet is, hiszen az állománynevet lezártuk.

6.4.2. Unix domain socket adatfolyam szerveralkalmazás Az összeköttetés-alapú szerveralkalmazás felépítése megegyezik a korábban a 6.2. Az összeköttetés-alapú kommunikáció alfejezetben tárgyaltakkal, kiegészítve a Unix domain socket kommunikációban használt címzéssel. Feladat Készítsünk egy Unix domain socket szerveralkalmazást, amely fogadja a kliensek kapcsolatait (egyszerre egyet), és a tőlük kapott adatokat kiírja az alapértelmezett kimenetre.

A megoldás a következő: /* uszerver.c - Egyszeru pelda szerver a Unix domain socket hasznalatara. */ #include #include #include #include



int mai n (voi d) struct sockaddr_un address; int sock, conn; socklen_t addrlen; char buf[1024]; int amount;

276

6.4. Unix domain socket

/* Letrehozzuk a socketet. */ 1f((sock = socket(FF_UNIX, SOCK_STREAM, 0)) < 0) perror("socket"); return -1;

/* Letoroljuk a korabbí socketallomanyt. */ unlink("./sample-socket"); memset(&address, 0, sizeof(address)); address.sun_family = AF_UNIX; strncpy(address.sun_path, "./sample-socket", sizeof(address.sun_path) - 1); /* A teljes Cirrl hossz tartalmazza a sun_family elemet es az eleresi ut hosszat. */ addrlen = sizeof(address.sun_family) + strnlen(address.sun_path, sizeof(address.sun_path));



/* A socketet hozzakotjuk a címhez. */ if(bind(sock, (struct sockaddr *) &address, addrlen)) perror("bind"); return -1;

/* Bekapcsoljuk a kapcsolodasra valo varakozast. */ if(listen(sock, 5)) perror("listen"); return -1; } /* Fogadjuk a kapcsolodasokat. */ while((conn = accept(sock, (struct sockaddr* ) &address, &addrlen)) >= 0) { /* Fogadjuk az adatokat. */ printf("Adatok erkeznek...\n"); while ((amount = read(conn, buf, sizeof(buf))) > 0) if (write(STDOUT_FILENO, buf, amount) != amount) perror("write"); return -1; } }

if(amount < 0) perror("read"); return -1;

277

6. fejezet: Hálózati kommunikáció

príntf("...vege\n"); /* Bontjuk a kapcsolatot. close(conn);

*/

if(conn < 0) perror("accept"); return -1;

/* Lezarjuk a szerver socketet. */ close(sock); return 0;

Ez az elég egyszerű példaprogram 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 kezelésére is alkalmas megoldásokat.) Az általános socketkezeléshez képest láthatunk a programban egy unlink0 hívást. Erre azért van szükség, mert ha már az állomány létezik, a bind0 hibával térne vissza, akkor is, ha az állomány socketállomány.

6.4.3. Unix domain socket adatfolyam kliensalkalmazás A kliensalkalmazás természetesen ugyancsak követi a socketeknél már ismertetett módszereket, a Unix domain socket kommunikációnál tárgyalt címzéssel. Feladat

Készítsük el az előző fejezetben látott szerver klienspárját. A kliens az alapértelme-

zett bemeneten fogadja az általunk beírt szöveget, és ezt a kapcsolaton keresztül küldje el a szervernek.

A következő program egy példa a megoldásra: /* ukliens.c - Egyszeru peldakliens a Unix domain socket hasznalatara. */ #include #include 0)

I O.

struct sockaddr_un address; int sock; size_t addrlen; char buf[1024]; int amount;

if (write(sock, buf, amount) != amount) . perror("wTite"); i re t urn

ilfil

} } if(amount < 0) { perror("read"); return -1; } /* Bontjuk a kapcsolatot. */ close(sock); return 0;

279

6. fejezet: Hálózati kommunikáció

6.4.4. Unix domain socket datagram kommunikáció Az előző fejezetekben láthattuk az általános összeköttetés-alapú kommunikációs megoldások alkalmazását a Unix domain socket protokollra. Ezekkel analóg módon az összeköttetés nélküli datagramkommunikáció is az általános függvényeket alkalmazza kiegészítve a Unix domain socket címzéssel. Ezért datagramkommunikációra példát majd csak az IP-kommunikáció tárgyalásánál mutatunk Útmutató Bár a datagramkommunikáció nem megbízható és nem sorrendtartó a specifikációk értelmében, ez a Unix domain socket protokoll esetében nem igaz. Mivel a kommunikáció csak lokálisan, a kernel mechanizmusain keresztül történik, így garantált, hogy nem vesznek el csomagok, és nem cserélődik fel a sorrendjük.

6.4.5. Névtelen Unix domain socket Mivel a Unix domain socket egy sor előnnyel rendelkezik a csővezetékekkel szemben (ilyen például a kétirányú kommunikáció), ezért gyakran alkalmazzák IPC-kommunikációhoz. A használatukat megkönnyítendő létezik egy socketpair0 rendszerhívás, amely egy pár összekapcsolt, név nélküli socketet hoz létre: include #include i nt socketpai r(int domain, int type,

int prot, int sockfdsl2]);

Az első három paraméter megegyezik a socket0 rendszerhívásnál tárgyaltakkal. Az utolsó paraméter, a sockfds, tartalmazza a visszaadott socketleírókat. Szemben a névtelen csővezetékekkel a két leíró egyenértékű.

6.4.6. A Linux absztrakt névtere A Unix domain socket kommunikáció során címként socketállományokat használva szembesülhetünk néhány problémával:

280



A socketállományokat rendszeresen törölnünk kell, mert ha az állomány létezik, akkor a bindO hibával tér vissza.



Ha megszakad a program, akkor ott marad az állományrendszerben a socketállomány.

6.4. Unix domain socket



Előfordulnak olyan helyzetek, amikor nincs jogosultságunk állományt létrehozni.



Használhatunk olyan állományrendszert, amely nem támogatja a socketállományokat.

Ezek a problémák mínd azt mutatják, hogy az állományok használata címként sokszor gondot jelent. Ugyanakkor a Linux más rendszerektől eltérően tartalmaz egy plusz szolgáltatást, amellyel elkerülhetjük az állománynevek használatát. Ez a szolgáltatás az absztrakt névtér. Az absztrakt névtér használata egyszerű. Alapesetben a címstruktúra sun_path eleme egy C-sztringet tartalmaz, vagyis nem null karakterek sorozatát a végén lezárva egy '\0' karakterrel. Ha absztrakt nevet szeretnénk megadni, akkor ettől eltérően egy '\0' karakterrel kell kezdenünk a mezőt, majd az ezt követő karakterek tartalmazzák az azonosítót. Ám ebben az esetben nem jelezhetjük 0' karakterrel az azonosító végét. Így a megadott szöveget teljesen a címstruktúra végéig veszi figyelembe a rendszer. Ezt azonban befolyásolhatjuk, amikor megadjuk a címstruktúra hosszát. Nézzünk erre egy példát: struct sockaddr_un address; size_t addrlen; memset(&address, 0, sizeof(address)); address.sun_family = AF_UNIX; address.sun_path[0] = 0; strncpy(address.sun_path + 1, "myaddr", sizeof(address.sun_path) - 2); addrlen = sizeof(address.sun_family) + strnlen(address.sun_path + 1, sizeof(address.sun_path) - 1) + 1;

A sun_path mező első elemének beállítjuk a 0-t. Ezt követően a második bytetól bemásoljuk az általunk megadott szöveget. A méret kiszámolásánál figyelnünk kell arra, hogy a szöveg hosszát csak a második karaktertől mérjük, illetve a kezdő '\0' karaktert is hozzáadjuk. Hasonlóan összeállítva a címet mind a szervernél, mind a kliensnél a korábbi példák továbbra is működnek, de nem igénylik állományok létrehozását, illetve törlését. Útmutató

Bár ebben az esetben nem láthatjuk az állományrendszerben a címeket, a netstat

programmal lehetőségünk van kilistázni a Unix domain socket szervereket, illetve kapcsolatokat. Így a címként használt absztrakt neveket is láthatjuk.

281

6. fejezet: Hálózati kommunikáció

6.5. IP Az előző fejezetben a Unix domain socket protokoll használatával csak egy gépen belül kommunikálhattak a folyamataink. Ebben a fejezetben bemutatjuk az Internet protokoll (IP) használatát, amellyel már lehetőségünk van számítógép-hálózaton keresztül kommunikációt felépíteni különböző számítógépeken futó folyamatok között. Az IP-kommunikáció során is az eddig megismert socketkezelő függvényeket alkalmazzuk. Eltérést nagyrészt csak a címzésnél látunk. A fejezetben párhuzamosan ismertetjük az IP protokoll korábbi 4-es verziójának (IPv4) és az új 6-os verziójának (IPv6) használatát. Jelenleg az IPv4 protokoll használatos széles körben, ám a programjainkban célszerű felkészülnünk az IPv6 támogatására is, hogy a jövőben használhatók legyenek.

6.5.1. Röviden az IP-hálózatokról Mielőtt ismertetnénk az IP protokollok használatát, illetve az internetes kommunikációt, röviden bemutatjuk az IP-hálózatok működését, hogy a később tárgyalt fogalmak értelmet nyerjenek. Kommunikáció a hálózaton Az internet lokális hálózatokból épül fel, sok kisebb-nagyobb hálózatból, amelyeket útvonalválasztók (routerek) kapcsolnak össze. Ez azt is jelenti, hogy a hálózati kommunikáció egy lokális hálózaton belül lévő számítógépek között másképpen zajlik, mint az egymástól távoli, különböző lokális hálózatokba tartozó számítógépek között. Lokális hálózat Lokális hálózatnak tekintendő az a hálózat, amelyen belül két számítógép között router közbeiktatása nélkül, közvetlenül lehet kommunikálni. Ez tipikusan egy switchre, 79 vagy több switchből álló struktúrára UTP-kábellel kapcsolódó számítógépeket jelent. Szokták ezt szegmensnek vagy alhálózatnak is neve zni. 90

79



A switch olyan hálózati eszköz, amely a portjaira (csatlakozóira) kapcsolt eszközök közötti kommunikációt biztosítja. Elődje a HUB, amely az egyik portjára érkező jeleket a többi portjára továbbítja. A switch ehhez képest annyi többletintelligenciával rendelkezik, hogy a csomagokat megvizsgálja, és csak arra a portjára továbbítja, ahol a címzett eszköz található. Másfajta hálózattípusok is léteznek (Token bus, Token ring), ám ezekkel a hétköznapok során ritkán találkozunk

282

6.5. IP

Klasszikus esetben egy lokális hálózaton belül, ha az egyik számítógép elküld egy csomagot, akkor azt az összes többi számítógép megkapja, de csak az használja fel, amelyiknek szól. Hogy kinek szól, azt a címzett gép hálózati kártyájának fizikai címe (MAC-cím, hardvercím stb.) határozza meg. Ez a cím minden hálózati kártyára egyedi, és csak ennek ismeretében lehetséges a két számítógép között kommunikációt megvalósítani. Manapság a működés csak annyiban tér el, hogy a switcheszközök is képesek értelmezni a fizikai címet és megjegyezni, hogy melyik portjukon található az ezt használó gép. Így az optimalizáció érdekében csak arra küldik tovább a csomagot. Vagyis a kommunikáció két gép között a fizikai címmel történik. Miért van szükség akkor az IP-címre, miért nem használja a protokoll a fizikai címeket? Először is, mert kényelmetlen, nehezen megjegyezhető. De ami sokkal fontosabb: elvileg még megváltoztathatatlan 81 a hálózati kártya legyártása során adott egyedi cím. Így garantálják, hogy egy lokális hálózaton véletlenül se legyen két egyforma fizikai címmel rendelkező gép. Mivel a fizikai címeket a gyártáskor határozzák meg, ezért semmilyen információt nem hordoz az alhálózattal kapcsolatban. Önmagában a címből nem tudjuk eldönteni, hogy a lokális hálózatunk tagja-e, vagy ha nem, akkor hol található a világban. Elvileg összeállíthatnánk egy nyilvántartási táblázatot, de ennek karbantartása lehetetlen feladat lenne. Ezért van szükség egy másik cím használatára is, és ez az IP-cím. Hogyan dönthető el, hogy egy adott IP-címmel rendelkező gépnek (címzettnek) mi a fizikai címe? Erre szolgál az ARP (Address Resolution Protocol). Ha egy gép egy másiknak akar csomagot küldeni, akkor első körben a címzett IP-címe és a hálózati maszk alapján eldönti, hogy a célgép vele egyező alhálózatnak a tagja-e, vagy sem (a módszert lásd a 6.5.3. IPv4-es címzés alfejezetben). Ha a célgép ugyanannak az alhálózatnak a tagja, akkor elküld egy broadcast- (mindenkinek szóló) üzenetet, amelyben megkérdezi, hogy melyik is az adott IP-címmel rendelkező számítógép, és mi a fizikai címe. Az üzenetet mindenki veszi, de csak az válaszol rá, aki az adott IP-cím tulajdonosa, és elküldi a kezdeményezőnek a saját fizikai címét (a kezdeményező a sajátját természetesen feltüntette az üzenetben). Ezt követően a kezdeményező — hogy ne kelljen folyton ARP-üzeneteket küldözgetni — elhelyezi a címzettre vonatkozó információkat egy gyorsítótárba (ARP Cache), és legközelebb, ha ugyanazzal a címzettel akar kommunikálni, akkor már ebből veszi az adatokat. A fizikai cím kiderítése után már csak annyi a feladat, hogy ezzel a fizikai célcímmel kell a gépnek csomagokat küldenie, így a célgép megkapja őket.

81

Gyakorlatban a gyártási folyamat egyszerűsítése miatt megváltoztatható, illetve szoftveresen felül is írható. 283

6. fejezet: Hálózati kommunikáció

Globális hálózat Mi történik akkor, ha olyan címzettel akar egy számítógép kommunikálni, amelyik nincs vele egy szegmensen? Ekkor jut szerephez a router. A router (útválasztó) egy kitüntetett számítógép a szegmensen, amely egyszerre több lokális hálózathoz is kapcsolódik, és amelyik éppen ezért több szegmensbe is tud adatot küldeni, így lehetővé teszi a szegmensek közötti kommunikációt. Ha egy számítógép egy másik szegmensben (ezt az alhálózati maszk segítségével állapítja meg) lévő géppel akar kommunikálni, akkor nem közvetlenül a címzettel kezdeményez kapcsolatot, hanem az alapértelmezett routerrel (ez minden gép esetében be van állítva). Ehhez persze először ARP-vel kideríti a router fizikai címét, majd elküldi az adatcsomagot, azzal az utasítással, hogy a megadott IP-címre kell eljuttatni. Ezt követően, ha a célcím valamelyik, a routerhez kapcsolódó alhálózathoz tartozik, akkor a router ARP-vel kideríti ennek a fizikai címét, és elküldi a csomagot. Ha a címzett semelyik, a routerhez kapcsolódó szegmenshez sem tartozik, akkor a router is egy másik routerrel veszi fel a kapcsolatot, amely több másik alhálózatot kezelő routerrel is kapcsolatban áll, és annak küldi tovább a csomagot. Ezt a megoldást addig ismétli a rendszer, amíg eljut egy olyan szintre, ahol a router tudja, hogy a célgép alhálózata merre található. Ezt követően a csomag eljut a célalhálózat routeréhez, amely továbbítja a célgépnek. Fontos megjegyezni, hogy két számítógép között ebben az esetben is csak egy szegmensen belül és a fizikai címek alapján zajlik a közvetlen kommunikáció. Szegmenseken kívülre közvetetten (routerek közbeiktatásával) kerülnek a csomagok.

6.5.2. Az IP protokoll rétegződése Az IP protokoll családot több protokollra és több rétegre bontjuk. A rétegeket és a protokollokat a 6.3. ábra szemlélteti: Alkalmazások

TCP

UDP IP Ethernet

Szállítási réteg Hálózati réteg Adatkapcsolati réteg

6.3. ábra. Az IP protokoll család gyakrabban használt elemei

284

6.5. IP

Az adatkapcsolati réteg biztosítja a kommunikációt a hálózat elemei között. A hálózati réteg teszi lehetővé a csomagok eljutását a küldőtől a címzettig. Az IP protokoll is ebben a rétegben található, vagyis a legfőbb feladata az, hogy az interneten található két gép között megoldja a címzést és az adatok továbbítását. A szállítási réteg biztosítja, hogy az alkalmazások között az adatátvitel transzparensen megvalósulhasson. Az UDP protokoll lényegében az alkalmazások megcímezhetőségével (portok kezelése) egészíti ki az IP-t. A TCP a portok kezelése mellett még a kapcsolat megbízhatóságát is garantálja.

6.5.3. IPv4-es címzés Az IP-címek egyedíek mínden hoszt esetében, és 32 bitből (4 byte-ból) állnak. Szokásos megjelenésük a 4 byte pontokkal elválasztva. Próbáljuk meg lekérdezni saját gépünk IP-címét: 8 ip addr 1: lo: mtu 16436 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 00:22:3f:dl:d3:a7 brd ff:ff:ff:ff:ff:ff i net 192.168.1.10/24 brd 192.168.1.255 scope global eth0 i net6 fe80::211:2fff:fedl:d3a7/64 scope link valid_lft forever preferred_lft forever

~a

Amikor egy szervezet hálózati adminisztrátora igényel egy címtartományt, akkor kap egy 32 bites hálózati címet és egy 32 bites hálózati maszkot. Ez a két szám meghatároz egy címtartományt. Ezt a 6.4. ábra szemlélteti. Hálózati cím

il000000liolol000 ocw0000ll00000000

Hálózati maszk 11111111111111111 4- Hálózati

Hoszt -›

Helyi cím

110000 00 10101000 0000 0001 0 0 0 0 1010

Idegen cím

10 0 110 0 0 01 0 0 0 0 1 0 101 111 01 0 0 0 0 1 0 10

6.4. ábra. IPv4-es hálózati cím és maszk

285

6. fejezet: Hálózati kommunikáció

Egy IP-címből azok a bitek, ahol a hálózati maszk 1-est tartalmaz, a hálózati címet határozzák meg. Azok a bitek, ahol a maszkban 0 szerepel, az adott gép egyedi hosztcímét adják meg. Vagyis egy hálózaton belül a számítógépek egyedi IP-címei csak azokban a bitekben térhetnek el a hálózati címtől, ahol a maszkban 0 szerepel. Ha az IP-cím a maszk 1-es értékeinél is eltér, akkor az már egy másik alhálózatnak az eleme. Egy számítógépen csak a gép egyedi IP-címét és a hálózati maszkot szoktuk megadni, mivel az előző összefüggések alapján ebből a két értékből a hálózati cím előállítható. A megadás során még egy egyszerűsítéssel szoktunk élni. A hálózati maszkban nem keveredhetnek az 1-es és a 0-s értékek. A maszk elején x darab 1-es, majd ezt követően (32—x) darab 0 bit következik. Így a maszk teljesen leírható x-szel, amely egy szám 0 és 32 között. Ezt a számot az IP-cím után egy „1" jellel elválasztva írjuk. Például:

X.01Atitt ..10,

-

A „/24" azt jelzi, hogy a maszk 24 darab 1-est majd 8 darab 0-t tartalmaz. Vagyis az első 24 bit a hálózati címhez, a maradék 8 bit a hosztcímhez tartozik. Ugyanez a maszk pontozott formátumban így néz ki:

.Z.5.295.a~: • _ Egy tartomány első és utolsó címének speciális jelentése van. Ezeket számítógépeknek nem adjuk ki. Az első cím, amikor a hosztcím bitjei végig 0-k, a hálózati cím. Az utolsó cím, amikor a hosztcím végig 1-es a broadcastcím. A broadcastcímre küldött csomagokat az alhálózat minden gépe megkapja.

6.5.4. IPv4-es címosztályok Manapság a hálózati címtartományok meghatározást hálózati cím és hálózati maszk alapján végezzük. Korábban azonban a tartományok meghatározása az ún. címosztályok alapján történt. Ezt jelenleg már nem használják, ám időnként még mindig találkozunk a címosztályok fogalmával. Az IPv4 32 bites címe ebben az esetben is kétfelé oszlik. Az első M bit egy azonosító, amely megmutatja a hálózat típusát, a következő N bit a hálózat címe, a maradék 32-M-N bit pedig az adott hálózaton belül egy számítógép címe. Attól függően, hogy N értékét mekkorára választjuk, több hálózatot címezhetünk meg (N-et növelve), illetve több gépből álló hálózatot címezhetünk meg (N-et csökkentve). A címosztályokat a 6.4. táblázat foglalja össze:

286

6.5. IP 6.4. táblázat. Címosztályok

Cím

Első cím

Utolsó cím

Azonosító

A osztály

1.0.0.0

127.255.255.255

0

7

B osztály

128.0.0.0

191.255.255.255

10

14

C osztály

192.0.0.0

223.255.255.255

110

21

D osztály

224.0.0.0

239.255.255.255

1110

Többes küldés

E osztály

240.0.0.0

247.255.255.255

11110

Fenntartva

Feladat Állapítsuk meg, hogy a www.aut.bme.hu szerver IP-címe milyen osztályba tartozik. $ ping www.aut.bme.hu PING www.aut.bme.hu (152.66.188.11) 56(84) bytes of data.

Amint a táblázat alapján látható, ez egy B osztályú IP-cím, és ez nem meglepő, 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).

6.5.5. IPv4-es speciális címek A teljes IPv4-es címtartományban vannak olyan tartományok, amelyeknek speciális a jelentése. A 127.0.0.1 cím tipikusan a loopbackcím, amely arra használatos, hogy a folyamatok a saját gépüket megcímezzék, vagyis egy gépen belül folytassanak IP-kommunikációt. Valójában a 127.0.0.0/8 tartomány bármelyik címét lehet erre a feladatra használni, de az első címet szokták. A 10.0.0.0/8, a 172.16.0.0/12 és a 192.168.0.0/16 privát IP-címtartományok. Ez azt jelenti, hogy az interneten nem találkozunk ilyen címeket használó gépekkel, mivel az útvonalválasztók (router) nem továbbítják ezeket a csomagokat. A saját hálózatunkban szabadon használhatjuk a privát címtartomány címeit, közvetlenül ezek a gépek azonban nem kommunikálhatnak az interneten. 82 A 224.0.0.0/4 D osztályú címtartomány címeit a többes küldéshez használhatjuk. A többes küldés során egy-egy csomagot nemcsak egy gépnek, hanem hosztok csoportjának küldjük el. (Használatát bővebben lásd a 6.5.13.2. Többes küldés alfejezetben.)

82

Ha privát címeket használó számítógépekkel el akarjuk érni az internetet, akkor címfordítást (network address translation, NAT) kell használnunk. 287

6. fejezet: Hálózati kommunikáció

6.5.6. IPv6-os címzés A 6-os verziószámú IP-címek létrejöttének elsődleges oka az, hogy az eredeti (4-es verziójú) IP-címekből kifogytunk. A probléma megoldására dolgozták ki az IPv6-ot, amely már 128 bites címeket használ. Az IPv6 a 16 byte-os címeken kívül számos többletszolgáltatást nyújt, így például: •

automatikuscím-konfiguráció,



fejlettebb többes küldés,



az útvonalválasztók (router) feladata egyszerűsödött,



több útvonalválasztási opció,



mobilitás,



hitelesítés,



adatbiztonság.

A 16 byte-os IP-címek leírásához értelemszerűen más formátumot használunk: a számokat hexadecimálisan 4-es csoportokra osztjuk kettősponttal elválasztva, ez összesen 8 csoportot jelent. Például:

0000:4200010000:0000}203Z£: ~1~0CM.'"' Várható, hogy az IPv6-os címek kiosztásánál (és valószínűleg még elég sokáig) a cím kezdetben sok, nullákkal teli blokkot tartalmaz. Egyszerűsítésképpen bevezették azt, hogy a kezdő nullák minden blokkon belül elhagyhatók, egy vagy több nullából álló blokk két kettősponttal helyettesíthető:

•0000t, t n 546A4: FEEI)4 DEAL Az áttérés megkönnyítésére a régi IP-címek két kettősponttal kezdődően a hagyományos, pontokkal elválasztott módon írhatók le:

A régi IP-címek a következő minta alapján illeszkednek az új címbe:

000'400.~0004:s00~~,;gxxX_-_ -_ _ Itt az XXXX:XXXX az IPv4-es cím hexadecimális formátumban. A lehetséges rövidítéseket alkalmazva az átírási séma a következő: _

288

6.5. IP

Igya

152.66.188.11 IPv4-es cím IPv6 os formátumban az alábbi: -

::FFFF:9842:BCOB

6.5.7. Portok Eddig az IP-réteg címzését vizsgáltuk, amely a számítógép-hálózaton lévő gépek azonosítására szolgál. Egy gépen azonban általában több szolgáltatás is fut, amelyeket meg kell különböztetnünk egymástól. Vagyis az egyes alkalmazásokat is meg kell címeznünk valahogyan. A TCP- és az UDP-réteg lehetővé teszi több virtuális „csatorna" létrehozását két gép között. A szállítási réteg ezt a kommunikációt a portokkal azonosítja. Egy TCP/UDP kommunikációban mindkét fél külön portszámmal rendelkezik. Az 1024-nél kisebb számú portokat jól ismert portoknak (well-known ports) nevezzük, amelyek meghatározott szolgáltatásoknak vannak fenntartva. A felhasználói programok az 1024-tól 49 151-ig lévő tartományt használhatják (regisztrált portok registered ports). A dinamikus és a magánportok (Dynamic and Private Ports) a 49 152 — 65 535 intervallumban helyezkednek el. Az Internet Assigned Numbers Authority (TANA, az internet számhozzárendelő hatósága) szervezet által definiált portok listája a http:/ /www.iana.org/ assignments/port-numbers oldalon található. A legismertebb szolgáltatások portjait a 6.5. táblázat foglalja össze. —

6.5. táblázat.

Szolgáltatások portjai

Szolgáltatás neve

Port

ftp-daca

20

ftp

21

ssh

22

telnet

23

smtp

25

http

80

port map

111

https

443

Linux alatt a szolgáltatások az /etc/services fájlban vannak felsorolva.

289

6. fejezet: Hálózati kommunikáció

6.5.8. A hardverfüggő különbségek feloldása A hálózati kommunikáció byte-ok sorozatán alapszik. Egyes processzorok azonban különböző módon tárolják a különböző adattípusokat. Mivel a különböző processzorra! szerelt gépeknek is szót kell érteniük egymással, ezért ennek a nehézségnek az áthidalására definiáltak egy hálózati byte-sorrendet (network byte order). A hálózati byte-sorrendben az alacsonyabb helyértékű byte jön elóbb („a nagyobb van hátul" — big endian). Azoknál az architektúráknál, ahol az ún. hoszt byte-sorrendje ellenkező („a kisebb van hátul" — little endian) — ilyenek például az Intel 8086-os alapú processzorok konverziós függvények állnak rendelkezésünkre, amelyeket a 6.6 táblázat foglal össze. 6.6. táblázat. Byte-sorrend-konverziós függvények Függvény

Leírás

ntohs

Egy 16 bites számot a hálózati byte-sorrendből a hoszt byte-sorrendbe (big-endian—little-endian) vált át.

ntohl

Egy 32-bites számot a hálózati byte-sorrendból a hoszt byte-sorrendjébe (big-endian—little-endian) vált át.

htons

Egy 16-bites számot a hoszt byte-sorrendjéből hálózati byte-sorrendbe (little-endian—big-endian) vált át.

htonl

Egy 32-bites számot a gép byte-sorrendjéből hálózati byte-sorrendbe (líttle-endian—big-endian) vált át.

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. A byte-sorrend problémájával elsősorban a címzésnél találkozunk. Az átvitt adatok értelmezését már mi definiáljuk az alkalmazásszintű protokoll specifikálásánál, ám az implementáció során érdemes észben tartani ezt a problémát.

6.5.9. A socketcím megadása A 6.2. Az összeköttetés-alapú kommunikáció alfejezetben bevezettük a connectO függvényt, amellyel a szerverhez tudunk kapcsolódni, illetve a bind0 hívást, amellyel címet rendelhetünk a socketekhez. Mindkét függvény egy-egy címet vár paraméterül. Nézzük meg közelebbről a cím megadásának a módját. IPv4

Az IPv4-es címeket a sockaddr_in struktúra definiálja, amelyet a netinet/ in.h állomány tartalmaz: 290

6.5. IP

struct sockadd•_in sa_family_t in_port_t struct in_addr unsigned char

};

sin_family: /* Címcsalád = AF_INET */ sin_port; /" A port száma */ sin_addr; IPv4 cim */ sin_zero[8]; / * struct sockaddr vége */

Az in_addr struktúra felépítése a következő: struct in_addr in_addr_t s_addr; /* előjel nélküli 32 bites szám */

IPv6 Az IPv6-os struktúra hasonlít az IPv4-hez, ám ebben az esetben a cím 32 bit helyett 128 bites. A címet a sockaddr_in6 struktúra tárolja, amely szintén a netinetlin.h állományban található: struct sockaddr_in6 sa_family_t in_port_t uint32_t struct in6_addr uint32_t }: shememmew

sin6_family; /* Címcsalád = AF_INET6 sin6_port; /* A port száma */ sín6_flowinfo; sin6_addr; /* IPv6 cím */ sin6_scope_id;

A sinű_flowinfo és a sin6 scope_id mezők értelmezésére jelen keretek közt nem térünk ki. 0 értéket használunk az esetükben. Az in6 addr struktúra felépítése az alábbi: struct in6_addr a

uint8_t s6_addr[16];

N _

41"- ►

MMM

;

Mind az IPv4, mind az IPv6 esetében a rendszer a címet bináris formában, hálózati byte-sorrendben várja. Ezek megadása azonban a felhasználó számára nehézkes lenne. A 6.5.3. IPv4-es címzés és a 6.5.6. IPv6-os címzés alfejezetekben láthattuk az elterjedt címmegadási formátumokat. Ezek szöveges formátumok, amelyekból a bináris cím előállítása függvények segítségével lehetséges. Az IPv4-es protokoll esetén az inet_aton() és az inet_ntoa() függvényeket használhatjuk arra, hogy a pontozott szöveges formátumból a bináris formátumot előállítsuk, illetve fordítva. Ezek a függvények azonban csak IPv4 esetében használhatók, ezért manapság már elavultnak számítanak.

291

6. fejezet: Hálózati kommunikáció

Az inet_pton0 és az inet_ntop0 függvények hasonlítanak az inet_aton0 és az inet_ntocco függvényekre, ám mind az IPv4 pontozott szöveges formátumát, mind az IPv6 hexadecimális szöveges formátumát támogatják. Ezért ezeket a függvényeket érdemes megvizsgálni. Az inet_pton0 függvény nevében a p prezentációt (presentation), az n hálózatot (network) jelent. Vagyis a függvény az ember által kezelhető szöveges formátumú címből hálózati byte-sorrendes bináris formátumot állít elő. i nclude

int inet_pitoneint af, const char *src, void *4st) 4Az af a címcsalád, vagyis esetünkben AF INET vagy AF INET6. Az src paraméter az IP-cím szöveges reprezentációját tartalmazza. A dst paraméternek egy nem tipizált mutatót kell beállítanunk. Ennek valójában egy in_addr vagy egy in_addr6 típusú struktúrára mutató értéknek kell lennie, attól függően, hogy IPv4-es vagy IPv6-os címet alakítunk át. A függvény visszatérési értéke sikeres konverzió esetén 1. Ha 0 értéket kapunk vissza, akkor az azt jelenti, hogy a szöveges reprezentáció nem megfelelő. Hibás af érték esetén pedig —1 értéket ad vissza a függvény. Az inet_ntop0 függvény az ellenkező irányú konverziót végzi el. A hálózati bináris címből egy olvasható szöveges címet állít elő: #include const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

Az af értéke ebben az esetben is AF INET és AF INET6 lehet. Az src paraméternek egy in_addr vagy egy in_addr6 struktúrára kell mutatnia, amely a bináris címet tartalmazza. A dst paraméterként meg kell adnunk egy karaktertömb-mutatót, amelybe a függvény a szöveges reprezentációt elhelyezi nullával lezárva. A tömb méretét a size paraméterrel kell specifikálnunk. Ha túl kicsi tömböt adunk meg, akkor a konverzió sikertelen lesz. Sikeres konverzió esetén a függvény a szöveges reprezentáció mutatójával tér vissza (a dst paraméter). Ellenkező esetben NULL értékkel jelzi a hibát. A megfelelő szövegbufferméret megválasztásához a netinet/in.h állomány tartalmaz két segéddefiníciót az IPv4-es és az IPv6-os címekhez:

#dgfine 44W' «afilite 1~A011~

292

10,

6.5. IP

6.5.10. Lokális cím megadása A 6.2.2. A socket címhez kötése alfejezetben megmutattuk a socketek címhez kötését. A címhez kötés művelete egy lokális cím megadását igényli. Az eddig látott függvények segítségével össze tudunk állítani egy olyan struktúrát, ahol a programunkat futtató számítógép IP-címét adjuk meg, továbbá egy portot. Ezt követően az adott címhez köthetjük a socketet. Ezzel a módszerrel azonban több gond is van. Egyrészt ki kellene derítenünk a programot futtató gép címét. Másrészt egy internetre kötött gépnek legalább két IP-címe van: egy hálózati és egy loopbackcíme A megoldás egy speciális címbeállítás használata, amely a futtató gép minden IP-címéhez hozzáköti a socketet. Ez a cím IPv4 esetén az INADDR ANY, amely négybyte-os, 0-t tartalmazó érték. Megfog INAOD,LANY

ttin.A4dr_t) No0000000)

Az IPv6 is rendelkezik egy hasonló definícióval, ennek neve IN6ADDR ANY INIT, és értéke 16 darab 0 byte-ot tartalmazó tömb: #define IN6ADOR—ANY—XNZ1-

Ot0,0~9404nAMilb04

Bár a két konstans hasonló, a használatukban van egy különbség. Az INADDR_ ANY skalártípus, ezért bárhol használhatjuk értékadásra. Viszont az IN6ADDR_ANY _INIT tömbérték, amelyet a C nyelv szintaktikája szerint egy egyszerű egyenlőségjellel nem másolhatunk le, kivéve a tömb létrehozását: torsi strUOt InLiddr*100,0

0 .XN6APORANYLXNZ ,

A rendszer tartalmazza az ebben a példában szereplő értékadást, így az in6addr_any globális változó fel is használható értékadásra. Bizonyos szolgáltatások esetén előfordul, hogy azokat csak lokálisan elérhetővé akarjuk tenni, és nem publikáljuk az internet felé. Ilyenkor csak a loopbackinterfészhez kötjük hozzá a socketet. Ennek meghivatkozására is tartalmaz konstansokat a rendszer. IPv4 esetén a loopbackinterfész címe

INADDRLOOBACK: Iftlefine IkAnDILL001%,«

.ax7fOoGLIII 1 127 .4.tra.

IPv6 esetén egy tömbkonstansunk van IN6ADDR_LOOPBACK_INIT néven:

4ht!ifil* DítOtikE0OPWIUSTY . ,11kt,t 144 _ Att 4:0.0A41.4.44';04 gti;04tUl *-

293

6. fejezet: Hálózati kommunikáció

A definíció mellett in6addr_loopback néven globális változó is rendelkezésünkre áll:

const struct in6_addr in6addr_loopback Útmutató Figyeljünk arra, hogy az IPv4-es konstansok hosztbyte-sorrendben vannak, ezért még konvertálni kell öket hálózati byte-sorrendre a htonI0 függvénnyel. Ez a 0-t tartalmazó INADDR_ANY esetén még nem fontos, de az INADDR_LOOPBACK értéke már nem O. Az IPv6-os konstansok viszont hálózati byte-sorrendben vannak.

6.5.11. Név- és címfeloldás Az IP-címek használata a felhasználók számára nehézkes, mivel sok számot kellene fejben tartaniuk. Emellett, ha a szerver IP-címét átírjuk, akkor a felhasználók nem találják meg többé. Ezért a gyakorlatban nem is IP-címeket, hanem hosztneveket használunk. Ha a hoszt nevéből az IP-címét szeretnénk megállapítani, akkor névfeloldásról beszélünk. A névfeloldás történhet számos forrás alapján: •

lokális állomány (/etc/hosts),



központi szerverek (DNS, mDNS, Yellow pages stb.),

Szerencsére programozóként nem kell minden lehetséges forrást leimplementálnunk. Helyette a Linux egységes névfeloldást végző függvényeket nyújt.

6.5.11.1. A getaddrinfo() függvény A getaddrinfo0 függvény a hosztnevet és a szolgáltatásnevet (lásd 6.5.7. Portok alfejezetben) képes átkonvertálni IP-címmé és portszámmá. 83 A függvény egyaránt kezeli az IPv4-es és az IPv6-os címeket. Használata során olyan kritériumokat adunk meg, amelyekkel meghatározzuk a megfelelő címeket. Ennek hatására a függvény egy címekből álló listát ad vissza. A függvény alakja a következő: #include #include int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); void freeaddrinfo(struct addrinfo *res); const char *gai trerror(int errcode);

83

A getaddrinfo() függvény elődeinek tekinthető a gethostbynaine(), amely hosztnév feloldásra képes, illetve a getservbyname(), amely a szolgáltatások portját adja vissza. Ezeket a függvényeket azonban számos korlátjuk miatt manapság nem használjuk.

294

6.5. IP

A lista memóriafoglalásának felszabadításához és a hibakezeléshez további függvények állnak rendelkezésre. A node paraméterben kell megadnunk a hosztnevet, a service paraméterben pedig azt a szolgáltatást, amelyeknek a címére kíváncsiak vagyunk Mind a két esetben szöveges formátumot használunk. A hosztnévnél megadhatunk IP-címet a szokásos formátumokban — ekkor az inet_pton0 függvényt helyettesítheti —, illetve a hoszt nevét. A szolgáltatásnál is használhatjuk a megnevezés mellett a port számát szöveges, decimális formátumban. Ha a service paraméternek NULL értéket adunk meg, a függvény a válaszban a portszámot nem állítja be. A hints paraméterben adhatjuk meg azokat a kritéríumokat, amelyek meg kell, hogy feleljenek a találatoknak. A res paraméterben kapjuk meg a választ láncoltlista-formában. Később a láncolt lista felszabadításában a freeaddrinfo() függvény segít, amellyel egy lépésben megoldható a feladat. A getciddrinfo0 függvény visszatérési értéke 0, ha sikeresen végrehajtódott. Hiba esetén a visszatérési érték egy olyan hibakód, amelynek szöveges információvá alakítását a gai_strerrorO függvénnyel végezhetjük el. Mind a kritériumok, mind a válasz típusa struct addrinfo, ennek felépítése a következő: struct addrinfo int ai_flags ; ai _fami 1 y ; int int ai_socktype; int ai_protocol ; si ze_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *al_next; } ;

Az ai_family mező a címcsaládot adja meg. Értéke lehet AF INET és AF_INET6

is. Kritériumként AF UNSPEC értéket is beállíthatunk. Ekkor a válaszlistában IPv4-es és IPv6-os címek is szerepelhetnek. Az ai_socktype mezőben a SOCK STREAM és a SOCK DGRAM értékeket vagy 0-t adhatunk meg. 0 érték esetén mind TCP-, mind UDP-bejegyzéseket tartalmazhat a válaszlista, ha az adott szolgáltatás mindkét protokollt támogatja. Az ai_protocol a protokollindexet adja meg. Kritériumként 0-t megadva azt jelenti, hogy a protokollértéket nem kötjük meg. Az eddig felsorolt három mező rendre a socketO függvény paraméterei lehetnek, amikor felhasználunk egy válaszbejegyzést. Az ai_addrlen mező a cím méretét adja meg byte-okban. A címet az ai_addr mező tartalmazza byte-osan sockaddr_in (IPv4) vagy sockaddr_in6 (IPv6) formátumban. Ezeket az értékeket használhatjuk a cím megadásánál.

295

6, fejezet: Hálózati kommunikáció

Az ai_canonname mező csak a lista első eleménél van kitöltve, és a hoszt nevét tartalmazza abban az esetben, ha kritériumként az ai_flags mezőben az AI CANONNAME értéket megadtuk. Az ai_next mező a láncolt lista következő elemére mutat, illetve a lista utolsó eleménél az értéke NULL. A lista végére hagytuk az ai_flags mezőt, amely egy kicsit hosszabb magyarázatot igényel. Ez a mező a kritériumok összeállításánál kap szerepet. Egy bitmaszkról van szó, amely különböző opciókat kapcsolhat be. A használható értékeket a 6.7. táblázat foglalja össze: 6.7. táblázat.

A címmegadás és szú-rés opciói -

Jelzőbit

Leírás

AI ADDRCONFIG

A válaszlista csak akkor tartalmaz IPv4-es címet, ha a lokális gépnek legalább egy IPv4-es címe van, amely nem a loopbackcím. Illetve IPv6-os címek esetén hasonlóképpen.

AI ALL

Csak az AI V4MAPPED értékkel van jelentése. Értelmezését lásd ott.

AI CANONNAME

Ha a node paraméter értéke nem NULL, akkor a válaszlista első elemének ai_canonname mezője tartalmazza a hoszt nevét.

AI NUMERICHOST

Kikapcsolja a névfeloldást, és a node paraméter értéke csak numerikus címreprezentáció lehet. Ezzel megtakaríthatjuk a névfeloldás idejét.

AI NUMERICSERV

Kikapcsolja a szolgáltatás névfeloldását. Szolgáltatásnak csak számot adhatunk meg (szövegesen).

AI PASSIVE

A címstruktúra egy szerversocket címhez kötéséhez használható lokális címet tartalmaz, ha ez az opció be van állítva, és a node paraméter NULL. Vagyis a cím INADDR ANY vagy IN6ADDR ANY INIT lesz.

Al V4MAPPED

Ha ez az opció be van kapcsolva, és mellette a kritérium aijamily mező értéke AF INET6, és a függvény ennek ellenére csak IPv4-es címet talál, akkor az IPv4-es címet IPv6-os formátumban adja vissza. Ha az AI ALL opcióval együtt alkalmazzuk, akkor IPv6-os és IPv4-es címeket is visszaad, de utóbbiakat IPv6-os formátumba alakítva.

Járjuk körbe az AI PASSIVE opciót egy kicsit jobban. Ha ezt az opciót beállítjuk, és nem adunk meg hosztnevet, akkor a kapott cím tipikusan a bind0 függvénnyel használható (INADDR ANY vagy IN6ADD_ANY INI7). Ha nem állítjuk be ezt az opciót, akkor a kapott cím a connect() és a sendto0 függvények számára használható. Ez esetben, ha megadunk hosztnevet, akkor annak a címét vagy címeit kapjuk vissza. Arra is lehetőségünk van azonban, hogy nem adunk meg hosztnevet, és akkor a loopbackcímet kapjuk vissza (INADDR_LOOPBACK vagy IN6ADDR_LOOPBACK INIT).

296

6.5. IP

Bár a kritériummegadásra és a válaszlista előállítására ugyanazt a struktúrát használjuk, valójában a mezők használatában van némi eltérés. Kritérium megadásakor csak az aijlags, az aiJamily, az ai_socktype és az aiprotocol mezőknek van szerepe. A többi mező értékét nem használjuk, értéküknek 0-nak vagy NULL-nak kell lennie. A válaszban pedig az aijlags mezőnek nincsen szerepe. Ha semmilyen kritériumot nem kívánunk beállítani, csak a hoszt nevére és a szolgáltatásra szűrni, akkor kritériumként NULL értéket is beállíthatunk. Ez azzal egyenértékű, mintha az aijlags mezőnek (AI V4MAPPED I Al ADDRCONFIG) értéket, az aijamily mezőnek AF UNSPEC értéket, míg a többi mezőnek 0-t állítottunk volna be. Feladat Alkossunk egy egyszerű programot, amely a paraméterként kapott hosztnevet IPcímmé alakítja és kiírja. /* getípaddr.c - internetes névfeloldás. */ #include #include #include #include int main(int argc, char* argv[]) struct addrinfo hints; struct addrinfo* res; struct addrinfo* p; int err; char ips[INET6_ADORSTRLEN]; if(argc != 2) printf("Használat: %s \n", argv[0]); return -1;

} memset(&hints, 0, sizeof(hínts)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; err = getaddrinfo(argv[11, NULL, &hints, &res); if(err != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1;

} for(p = res; p != NULL; p = p >ai_next) -

if(p->ai_family == AF_INET)

{

297

6. fejezet: Hálózati kommunikáció i f(inet_ntop(AF_INET, &((struct sockaddr_in*)(p->ai_addr))-> sin_addr, íps, sizeof(ips)) != NULL) {

printf("IP: %s\n", íps); }

else if(p->ai_family == AF_INET6) if(inet_ntop(AF_INET6, &((struct sockaddr_in6*)(p->ai_addr))-> sin6_addr, ips, sizeof(ips)) != NULL) printf("IP: Yos\n", ips);

}

}

freeaddrinfo(res); return 0;

Teszteljük is le: ./getipaddr www.aut.bme.hu IP: 152.66.188.11

S . /getipaddr www.google.com IP: 173.194.35.176 IP: 173.194.35.177 IP: 173.194.35.178 IP: 173.194.35.179 IP: 173.194.35.180 IP: 2a00:1450:4016:801::1011 S . /getipaddr that.s.not.funny.com getaddrinfo: Name or service not known

6.5.11.2. A getnanné*;fi3O függvény A getnameinfo0 függvény nagyjából a getaddrinfoo függvény inverze. Az a feladata, hogy a socketcímet hoszt- és szolgáltatásnévvé konvertálja." A függvény alakja az alábbi:

84

Ez a függvény egyesíti a korábban, az IPv4 esetén használatos gethostbyaddr0 és geiservbyport0 függvények funkcionalitását, ám azoknál flexibilisebb.

298

#include #include int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);

Az sa paraméter a socketcímstruktúra (sockaddr_in vagy sockaddr_in6) muta-

tója. A struktúra méretét a salen paraméterben kell megadnunk. A host és a serv paraméterként egy-egy általunk lefoglalt karaktertömböt kell átadnunk, amelyeknek méretét a hostlen és a servlen paraméterekkel tudjuk közölni. Ebben a két tömbben kapjuk vissza a hoszt és a szolgáltatás nevét NULL terminált sztringként. Ha valamelyik értékre nem vagyunk kíváncsiak, akkor NULL értéket és 0 hosszúságot megadva ezt jelezhetjük. Természetesen legalább az egyiket kérnünk kell, mert különben nincs értelme meghívni a függvényt. A flags paraméter módosíthatja a függvény működését a 6.8. táblázat szerint. 6.8. táblázat. A getnameinfo függvény jelzőbitjei

Jelzőbit

Leírás

NI DGRAM

Egyes portoknál TCP és UDP protokollok esetében más-más szolgáltatás fut. Alapértelmezetten a TCP-szolgáltatás nevét kapjuk vissza. Ezzel a paraméterrel az UDP-szolgáltatást kapjuk meg.

NI NAMEREQD

Alapértelmezetten, ha a hosztnevet nem találja a függvény, akkor szöveges IP-címet ad vissza. Ha megadjuk ezt az opciót, akkor ebben az esetben hibával tér vissza a függvény.

NI NOFQDN

Alapértelmezetten teljes hosztnevet (Fully Qualified Domain Name) kapunk vissza, ezzel az opcióval csak a rövid hosztnevet.

NI NUMERICHOST

Ha megadjuk, akkor nem történik névfeloldás, hanem csak szöveges IP-címet kapunk vissza. Ez történik akkor is, ha nem sikerül a hosztnevet kideríteni.

NI NUMERICSERV

Numerikusan, egy decimális számot tartalmazó szövegként kapjuk vissza a portot.

Sikeres végrehajtás esetén a függvény 0-val tér vissza. Sikertelenség esetén hibakódot kapunk visszatérési értékként, amelyet ekkor is a gai_strerror() függvénnyel tudunk szöveggé alakítani. A getnameinfo() függvényt elsősorban az acceptQ vagy a reevfrom() függvények által visszaadott címekre alkalmazzuk. Tipikusan a naplózáshoz alakítjuk vissza a socketcímeket a felhasználó számára is értelmezhető formátumba. Feladat Az

előző fejezet példáját alakítsuk úgy át, hogy a socketcímet szöveges IP-címmé a

getnameinfo0 függvénnyel alakítjuk vissza.

299

6. fejezet: Hálózati kommunikáció

/* getipaddr.c - Internetes névfeloldás. */ #include #include #include int main(int argc, char* argv[]) {

struct addrinfo hints; struct addrinfo* res; struct addrinfo* p; int err; char ip5[INET6_ADDRSTRLEN]; if(argc != 2) printf("Használat: %s \n", argv[0]); return -1; }

memset(&hints, 0, sizeof(hints)); hints.ai_family = Ar_uNSPEC; hints.ai_socktype = SOCK_STREAM; err = getaddrinfo(argv[1], NULL, &hints, &res); i f(err != 0) {

fpríntf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1;

for(p = res; p != NULL; p = p->ai_next) {

if(getnameinfo(p->ai_addr, p->ai_addrlen, ips, sizeof(íps), NULL, 0, NI_NUMERICHOST) = 0) printf("IP: %s\n", ips); } }

freeaddrinfo(res); return 0;

Ám nemcsak a címet hozhatjuk szöveges formába ezzel a függvénnyel, hanem inverz névfeloldást is végezhetünk vele. Feladat Készítsünk egy programot, amely a paraméterként megkapott IPv4-es címet hosztnévvé alakítja.

300

6.5. IP /* gethost.c - IP címhez tartozó név feloldása. */ #ínclude #include #include #include



int main(int argc, char* argv[]) struct sockaddr_ín addr; char name[Ni_mAxH0ST]; int err; if(argc != 2) printf("Használat: %s \n", argv[0]); return -1; }

memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; i fOnet_ptOn(AF_INET, argv[1], &addr.sin_addr) == 0)

‚11

perror("inet_pton"); return -1;

err = getnameinfo((struct sockaddr*)&addr, sizeof(addr), name, sizeof(name), NULL, 0, NI_NAMEREQD); if(err != 0) fprintf(stderr, "getnameinfo: %s\n", gai_strerror(err)); return -1;

printf("A gép neve: %s\n", name); return 0; }

A programot letesztelve a következő kimenetet kapjuk: $ ./gethost 152.66.188.11 A gep neve: www.aut.bme.hu $ ./gethost 152.66.188.111 qetnameinfo: Name or service not known

emmemer-

301

6. fejezet: Hálózati kommunikáció

A példaprogramban szembesültünk azzal a problémával, hogy foglalnunk kell egy megfelelő méretű buffert a gép nevének. Szeretnénk akkorát foglalni, hogy beleférjen a teljes név, de nem tudhatjuk, mi elég. Hasonló problémával a szolgáltatásnevek feloldásánál is szembesülhetünk. A programozó dolgát az alábbi két definíció megkönnyíti:

Ez a két érték akkora, hogy a várható nevek beleférnek ekkora bufferekbe.

6.5.12. Összeköttetés-alapú kommunikáció A 6.2. Az összeköttetés-alapú kommunikáció alfejezetben már bemutattuk az összeköttetés-alapú kommunikáció egyes lépéseit. IP-kommunikáció esetén is ezeket a lépéseket követjük, csak annyi kiegészítéssel, hogy IP-socketet hozunk létre, és a címet az előző fejezetekben megismert módon állítjuk össze. Így jelenlegi ismereteink alapján már képesek vagyunk egy TCP-kommunikáció felépítésére és használatára. A kommunikáció során használhatjuk a reado és a write() függvényeket, a rendszer tartalmaz azonban két olyan másik függvényt, amely több lehetőséget nyújt. Az adatok küldéséhez használhatjuk a send0 függvényt: #include ssize_t send(int sockfd, const void *buf,, si ze_t len, int flags) ;

Az első argumentum a socket leírója, a második az elküldendő adat bufferének a mutatója, a harmadik az elküldendő adat mérete. Eddig a paraméterezés megegyezik a write() függvényével. A különbséget az utolsó, flags argumentum jelenti, amelyben további opciókat állíthatunk be. A flags argumentumban megadott jelzőbitek jelentését a 6.9. táblázat mutatja. 6.9. táblázat. A send jelzőbitjei

4040bit _

Lelt4

MSG_DONTROUTE A csomag nem mehet keresztül az útválasztókon, csak közvetlenül ugyanazon a hálózaton lévő gép kaphatja meg.

MSG_DONTWAIT

Engedélyezi a nem blokkoló I/O-t. EAGAIN hibával tér vissza, ha várakozni kellett volna a kiírásra, mert tele van a buffer.

MSG_MORE

További adatot szeretnénk még kiküldeni. Ezért nem küldi el a csomagot addig, amíg nem kap egy sendO hívást MSG_MORE opció nélkül.

302

6.5. iP

48126bit

Leírás

MSG_NOSIGNAL

Adatfolyam-alapú kapcsolat esetén a program nem kap SIGPIPE jelzést, amikor a kapcsolat megszakad. Ez azonban az EPIPE hibajelzést nem érinti.

MSG_OOB

Soron kívüli sürgős adatcsomagot (out-of-band data) küld. Általában jelzések hatására használják.

'

A sertd() visszatérési értéke hasonlít a write() függvényéhez. Sikeres küldés esetén az elküldött byte-ok számát kapjuk vissza, hiba esetén pedig negatív értéket. Utóbbinál az errno globális változó tartalmazza a hibát. A send0 párja a recv() függvény, amely a readO függvényre hasonlít: 4include ssize_t recv(int sockfd, void *buf, size_t len, int flags); A függvény első három paramétere a read() függvénynél is megszokott alakú.

Sorban a socketleíró, a fogadó buffer és a buffer mérete. Ezt követi a flags paraméter, amellyel további opciókat állíthatunk be. A flags paraméter értéke a 6.1(). táblázatban összefoglalt jelzó'bitekből állhat össze:

r

6.10. táblázat. A recv jelzőbitjei

Jelzőbit

d

Leírás

MSG_DONTWAIT

Engedélyezi a nem blokkoló 1/0-t. EAGAIN hibával tér vissza, ha várakozni kellett volna az olvasásra, mert nem érkezett adat.

MSG_OOB

Soron kívüli adat fogadása.

MSG_PEEK

Az adat beolvasása történik meg anélkül, hogy a beolvasott adatot eltávolítaná a bufferből. A következő recv0 hívás ugyanazt az adatot még egyszer kiolvassa.

MSG_WAITALL

Addig nem tér vissza, amíg a megadott buffer meg nem telik, vagy egyéb rendhagyó dolog nem történik, például jelzés érkezik.

A recv0 függvény visszatérési értéke megegyezik a read() függvényével. Sikeres olvasás esetén a bufferbe beírt byte-ok száma, hiba esetén mínusz érték, valamint 0, ha a túloldal lezárta a kapcsolatot. Gyakori feladat, hogy a TCP-kapcsolaton keresztül egy állományt kell elküldenünk. Ezt leimplementálhatjuk úgy, hogy egy ciklusban a readO függvénnyel beolvassuk az adatokat egy bufferba, majd a send0 függvénnyel elküldjük a TCP-kapcsolaton. Ez a megoldás jól működik, ám nagyobb méretű állomány esetében sokszor meghívódna a ciklus, és egyre inkább előjönne a hátránya, miszerint az adatokat az egyik rendszerhívással a kernelból egy tömbbe másoljuk, majd egy másik rendszerhívással vissza a kernelbe. Egyszerűbb és gyorsabb egy lépésben a kernelben elvégezni az egész műveletet. Ezt valósítja meg a sendfileQ rendszerhívás: 303

6. fejezet: Hálózati kommunikáció #include ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

A függvény az in_fd állományleíróval megadott állományból az out_fd állományleíróval leírt socketkapcsolatba küldi az adatokat. Ha az offset paraméter nem NULL, akkor egy olyan változó mutatóját kell, hogy tartalmazza, amely megadja az eltolást az in_fd állomány elejéhez képest. Az olvasást és a küldést csak ettól a pozíciótól kezdi el a függvény. Amikor a függvény visszatér, akkor módosítja az eltolás értékét. Az új érték az utolsó elküldött byte + 1 lesz. Ha az offset nem NULL, akkor a függvény nem módosítja a bemeneti állomány aktuális pozícióját. Ha az offset értéke NULL, akkor az olvasás a bemeneti állomány aktuális pozíciójától indul, és a függvény végén frissül is. A count paraméter a másolandó byte-ok számát adja meg. Ha több lenne, mint amennyi byte-ot az állomány tartalmaz, akkor csak az állomány végéig továbbítja a byte-okat. Sikeres visszatéréskor a visszatérési érték az elküldött byte-ok száma. Hiba esetén mínusz érték, és az errno globális változó tartalmazza a hibakódot. A sendfile0 rendszerhívás mellett a Linux támogat még további nem szabványos rendszerhívásokat is hasonló célokra: spliceO, vmspliceO, teeO. Ezekre jelen keretek közt nem térünk ki.

6.5.12.1. TCP kliens-szerver példa Eddigi ismereteink alapján könnyen meg tudunk valósítani egy egyszerű TCP-szervert és a hozzákapcsolódó klienst. Feladat Alkossunk egy egyszerű TCP-szervert, amely képes egy kapcsolat fogadására, majd a klienstől kapott adatokat kiírja az alapértelmezett kimenetre.

A feladatot az alábbi, IPv6-ot támogató szerverprogram megoldja: #include #include #include #include #include #include #include #define PORT "1122"

304

6.5. IP int main() struct addrinfo hints; struct addrinfo* res; ínt err; struct sockaddr_in6 addr; socklen_t addrlen; char ips[Ni_mAXHOST]; char servs[NI_MAXSERV]; int ssock, csock; char buf[256]; int len; int reuse;

I

memset(&hints, 0, sizeof(hínts)); hínts.aí_flags = AI_PASSIVE; hints.ai_family = AF_INE76; hints.ai_socktype = SOCK_STREAM; err = getaddrinfo(NULL, PORT, &hints, &res); i f(err I-- 0) fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; if(res == NULL) {

return -1; }

ssock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if(ssock < 0) perror("socket"); return 1;

reuse = 1; setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); if(bind(ssock, res->ai_addr, res->ai_addrlen) < 0) {

perror("bind"); return 1; } if(listen(ssock, 5) < 0) perror("listen"); return 1;

305

6. fejezet: Hálózati kommunikáció

freeaddrinfo(res); addrlen = sizeof(addr); while((csock = accept(ssock, (struct sockaddr*)&addr, &addrlen)) >= 0) {

if(getnameinfo((struct sockaddr*)&addr, addrlen, i ps, sizeof(ips), servs, sizeof(servs), 0) == 0) {

printf("Kacsolódás: %s:%s\n", ips, servs); }

while((len = recv(csock, buf, sizeof(buf), 0)) > 0) write(STDOUT_FILENO,

buf, len);

printf("Kapcsolat zárása.\n"); close(ssock);

close(ssock); return 0;

A címhez kötéshez össze kell állítanunk egy lokális címet. Ezt megtehetjük közvetlenül a sockaddr_in6 struktúra kitöltésével, vagy rábízhatjuk a getaddrinfo() függvényre. Példánkban az utóbbit választottuk, hogy bemutassuk a használatát. A szervercímhez a függvény kritériumában meg kell adnunk az Al PASSIVE opciót. Ugyanakkor a paraméterek közt hosztnévnek NULL értéket adtunk meg. A két beállítás hatására a cím értéke 1N6ADDR ANY INIT lesz. A cím előállítása után létrehozunk egy socketet, majd a setsockoptQ függvény segítségével beállítjuk, hogy a programunk leállása után a szolgáltatás portja azonnal ísmét használható legyen. (A setsockopt() függvényt bővebben a 6.6. Socketbeállítá sok alfejezetben ismertetjük.) Ezt követően hozzákötjük a socketet a címhez, és bekapcsoljuk a szervermódot. Majd egy cikluson belül fogadjuk a kapcsolódásokat, kiírjuk a kliens címét, portját és a kapott adatokat. Amikor a kliens lezárja a kapcsolatot, akkor a recv() függvény nullával tér vissza, és megszakad a ciklus. Ezt követően a szerver is zárja a socketet, és várja a következő kapcsolatot. Feladat Alkossuk meg az előző szerver klienspárját. A kliens kapcsolódjon a paraméterként megadott szervergéphez, és a socketkapcsolaton küldje el az alapértelmezett bemenetén kapott szöveget.

Az alábbi példaprogram valósítja meg a feladatot:

306

6.5. IP #include #include #include #include #include #include #include





#define PORT "1122" int main(int argc, char* argv[]) {

struct addrinfo hints; struct addrinfo* res; int err; int csock; char buf[1024]; i nt len; if(argc != 2) {

printf("Használat: %s \n", argv[0]); return 1; }

memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; err = getaddrinfo(argv[1], if(err != 0)

PORT, &hints, &res);

fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; } if(res == NULL) {

return -1; } csock = socket(res->ai_family, res->ai_socktype,

res >ai_protocol); -

if(csock < 0) { perror("socket"); return -1;

if(connect(csock, res->ai_addr, res->ai_addrlen) < 0) perror("connect"); return -1;

307

6. fejezet: Hálózati kommunikáció

while((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {

send(csock, buf, len, 0); }

close(csock); freeaddrinfo(res); return 0;



}

A példaprogramban a getaddrinfo() függvény segítségével előállítjuk a célgép megadott portjához tartozó címstruktúrát. Ha több címet is visszakapunk, akkor a lista első elemét választjuk a kapcsolódáshoz. A kapcsolat létrejötte után a program folyamatosan olvassa az alapértelmezett bemenetet, és a szöveget elküldi a socket, kapcsolaton keresztül Amikor a bemenetet a CTRL +D gombokkal lezárjuk, akkor a program zárja a socketkapcsolatot, és véget ér.

6.5.12.2. TCP szerver alkalmazás Az előző fejezetbeli szerverpélda alkalmazása tartalmaz egy súlyos hiányosságot. Az alkalmazás egyszerre csak egy kapcsolatot képes fogadni. Addig, amíg ezt kiszolgálja, nem hívódik meg az aceept() függvény, és nem fogad újabb kapcsolatot. Az ilyen jellegű szervereket iteratív szervernek nevezzük, és legfeljebb olyan esetekben alkalmazzuk, amikor a kliensek kiszolgálása nagyon gyors. Általában a kliens-szerver kommunikáció hosszabb, és a szerverektől azt várjuk el, hogy konkurensen egyszerre több klienst is kiszolgáljanak. Ilyenkor párhuzamosan kell futnia a kapcsolatok fogadásának és a kliensek kiszolgálásának. Erre a problémára több szerveroldali megközelítést alkalmazhatunk. Kapcsolatonként egy szál (folyamat) Minden bejövő kapcsolatnak új szálat (vagy folyamatot) indítunk, amelynek átadjuk a kapcsolódott klienssocketet. A klienssocketet ezt követően már a szál (folyamat) kezeli. Ekkor minden kapcsolatot a kód szempontjából szimmetrikusan kezelünk, de a szálak (folyamatok) közötti váltás lassító tényező. Illetve a saját stack miatt nem skálázódik jól nagy szervereken. Processzek esetében a Linux copy-on-write technikája (forkQ használatakor csak akkor másolja le a memóriaterületet, ha arra a gyermekprocessz ír) jelentősen csökkenti az új processz létrehozásának az idejét, jóllehet egy új processz létrehozása külön erőforrást emészt fel a rendszerből, és az indítható processzeknek felső korlátja van.

308

6.5. IP

Ugyanakkor processzek esetében az 5.1. Processzek alfejezetben tárgyalt exec() függvénycsalád segítségével könnyen indíthatunk más programokat." Robusztusság szempontjából viszont, ha új processzt indítunk, annak esetleges összeomlása nem rántja magával az összes többi processzt. Előre elindított szálak (folyamatok) A fenti módszert gyorsíthatjuk azzal, hogy a szerverprogram elindítása után rögtön egy meghatározott számú szálat (folyamatot) indítunk el („process pool", „thread pool"), és ha valamelyik szabad, az kezeli az újonnan bejövő kapcsolatot. A korábbi megoldáshoz képest ekkor egy feladatkiosztó algoritmust is kell készítenünk. A megoldás előnye az, hogy a szálak (folyamatok) számának korlátozásával optimálisan kihasználhatjuk a gép erőforrásait. Emellett egy DoS-támadás 86 csak a szolgáltatást érinti, és nem „fojtja" meg az egész gépet. A módszer hátránya az, hogy esetleg feleslegesen sok szálat (processzt) futtat foglalva az erőforrásokat. Ennek kezelésére a komolyabb programok menedzsmentalgoritmust alkalmaznak, amely a terhelés figyelembevételével megszüntet vagy létrehoz szálakat (folyamatokat). Ám egy ilyen algoritmus implementálása bonyolítja a programot. Nézzünk egy egyszerű példát az előre allokált szálak használatára. A példaprogramban egy véletlen számokat visszaadó szervert mutatunk be: #include #include #include #include #ínclude #include #include #define PORT 2233 #define THREAD_NUM 3

int ssock; pthread_mutex_t smutex = PTHREAD_MUTEX_INITIALIZER ;

Ha paraméterként vesszük át a futtatandó program nevét, mindig gondoljuk át a szerver támadhatóságát. Ha egy nem ellenőrzött sztringet átadunk az execQ függvénynek, a távoli felhasználó akármit lefuttathat a gépünkön a futó szerverprogram jogosultságával. A leleményes betörő ilyenkor például lefuttatja a sendmail segédprogramot, hogy az küldje el neki a jelszavakat tartalmazó fájlt, amellyel aztán otthon „eljátszadozhat". Alapszabály tehát: futtatás előtt mindig ellenőrizzük a kívülről kapott paramétereket. K6 Az elárasztásos támadás (DoS, denial-of-service attack) vagy elosztott elárasztásos támadás (DDoS, distributed denial-of-service attack) során olyan sok kapcsolódási kérelem érkezik a szerverszolgáltatáshoz, amelyet az nem tud kezelni. A támadó így teszi elérhetetlenné a szolgáltatást, de akár az egész szervergépet is megbéníthatja. 85

309

6. fejezet: Hálózati kommunikáció

void* comth(void* arg)

f

int ix; int csock; char buf[64]; ix = *((int*)arg); free(arg); while(1) pthread_mutex_lock(&smutex); csock = accept(ssock, NULL, NULL); pthread_mutex_unlock(&smutex); sleep(3); spríntf(buf, "%d. szál: %d\n", ix, rand() % 10); send(csock, buf, strnlen(buf, sizeof(buf)), 0); close(csock); return NULL;

} int main()

f struct sockaddr_in6 addr; pthread_t th[THRE4o_Num]; int i; int reuse; int* pix; if((ssock = socket(PF_INET6, SOCK_STREAM, 0)) < 0) {

perror("socket"); return 1; } reuse = 1; setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); memset(&addr, 0, addr.sin6_family addr.sin6_addr = addr.sin6_port =

310

sízeof(addr)); = AF_INET6; in6addr_any; htons(PORT);

S.S. IP

if(bind(ssock, (struct sockaddr*)&addr, sizeof(addr)) < 0) perror("bind"); return 1;

if(listen(ssock,

5) < 0)

perror("listen"); return 1;

srand(time(NULL)); for(i = 0; i < THREAD_NUM; i++) pix = malloc(sizeof(int)); *pix = i; pthread_create(&th[i], NULL, comth, pix); }

// végtelen várakozás while(1) sleep(1); return 0;

A program induláskor elvégzi a szerversocket beállításait, majd létrehoz három szálat. A kapcsolatok szétosztására a szálak között egy aránylag egyszerű trükköt alkalmazunk. Egy mutex segítségével elérjük, hogy egyszerre egy szál hívhassa meg az acceptQ függvényt, és fogadhasson kapcsolatot. Így nem kell átadnunk a klienskapcsolatokat a szálaknak. A szerver kipróbálásához a telnet program segítségével kapcsolódjunk a tcp 2233-as porthoz. Kis várakozás után visszakapunk egy véletlen számot, és a szerver bontja a kapcsolatot. Több kliens kezelése egy szálon Az állományok párhuzamos kezelésénél megismert seleet(), pollQ és epoi/0 függvényeket használhatjuk a socketek esetén is. Így egy szálon, eseményvezérelten kezeljük az összes klienssocketet. Emellett a szerversocketet is kezelhetjük ezzel a megoldással, mert a kapcsolódás egy olvasási eseményt generál ebben az esetben. Az egyszálú megvalósítás lehetővé teszi, hogy az egyes klienskapcsolatok között adatokat mozgassunk szinkronizálás nélkül. Így leginkább ilyen jellegű alkalmazásokban látszik az előnye. Ugyanakkor a módszer hátránya az, hogy a kód bonyolultabb, nem annyira szimmetrikus, mint a korábbiakban látottak.

311

6. fejezet: Hálózati kommunikáció

Az alábbi program egy csevegőszervert valósít meg. Egy ilyen szolgáltatásnál a felhasználók egymásnak küldik az üzeneteiket, vagyis az egyik klienskapcsolaton érkező szöveget a többi kapcsolatra kell kiküldenünk. A kapcsolatok közötti adatátvitel miatt a szolgáltatást egy szálon érdemes leimplementálni: #include #include #include #ínclude #include #include #include





#define PORT 3344 #define MAXCONNS 5 #define FULLMSG "megtelt!"

,iI•111■



int ssock; int connect_list[mAxCONNs]; struct pollfd poll_list[mAXcoNNS + 1]; int build_poll_list() { int i, count; poll_list[0].fd = ssock; poll_list[0].events = POLLIN; count = 1; for(i = 0; í < MAXCONNS; i++) {

if(connect_list[1] >= 0) poll_list[count].fd = connect_list[i]; poll_list[count].events = POLLIN; count++;

return count;

void handle_new_connection() int i, csock; csock = accept(ssock, NULL, NULL); if(csock < 0) return; for(i = 0; i < MAXCONNS; i++) {

if(connect_list[i] < 0)

312

6.5.1p

{ connect_list[i] = csock; csock = -1; break;

if(csock >= 0) send(csock, FULLMSG, strlen(FULLMSG), 0); close(csock);

void process_read(int csock) { char buf[256]; int len; int i; len = recv(csock, buf, sizeof(buf), 0); if(len > 0) { for(i = 0; i < MAXCONNS; i++) { i f((connect_list[i] >= 0) && (connect_list[i] != csock)) send(connect_list[i], buf, len, 0); }

int main() struct sockaddr_in6 addr; i nt reuse; int i; int íi; if((ssock = socket(PF_INET6, SOCK_STREAM, 0)) c 0) { perror("socket"); return 1; } reuse = I; setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); memset(&addr, 0, addr.sinfl_family addr.sin6_addr = addr.sin6_port =

sizeof(addr)); = AF_INET6; in6addr_any; htons(PORT);

313

6. fejezet: Hálózati kommunikáció i f(bind(ssock, (struct sockaddr*)&addr, sizeof(addr)) < 0) perror("bind"); return 1; } if(listen(ssock, 5) < 0) { perror("listen"); return 1; } for(i = 0; i< MAXCONNS; i++) connect_list[i] = -1; while(1) i nt count = build_poll_list(); if(poll(poll_list, count, -1) > 0) { if(poll_list[0].revents & POLLIN) {

handle_new_connection(); for(i = 1; í < count; i++) if(poll_list[i].revents & (POLLERR I POLLHUP)) for(ii = 0 ; ii < MAXCONNS; ii++) i f(connect_list[ii] = pol 1_1 í st[i { connect_list[ii] = -1;

I

. fd)

cl ose(pol 1_11 st[i].fd) ; else íf(poll_list[i].revents & POLLIN)

_..

11111

1

process_read(poll_líst[i].fd);

return 0;

A program beállít egy szerversocketet, majd összeállít egy kapcsolatlistát. Ezt követően a poll0 rendszerhívással várja, hogy kapcsolódási kérelem érkezzen. Amikor egy olvasási esemény érkezik a szerversocketre, akkor fogadja a kapcsolatot, és elhelyezi a listába (vagy visszautasítja, ha a szerver megtelt). Ezt követően ismét meghívódik a pollQ. Ha valamelyik klienskapcsolatra érkezík adat, akkor a program beolvassa, és kiküldi a többi kapcsolatra. 314

6.5. IP

A szerverre a telnet program segítségével tudunk csatlakozni (tcp 3344-es port). Érdemes egyszerre több kapcsolatot felépíteni. Ezt követően, amit az egyik kapcsolaton elküldünk, az a szöveg a többi kapcsolaton megjelenik.

6.5.12.3. TCP-kliensalkalmazás A kliensalkalmazás az esetek többségében egyetlen klienssocketet tartalmaz, amely kapcsolatot kezdeményez valamilyen szerver felé. Majd a kapcsolat felvétele után megindul a kommunikáció. Itt az adatfogadás jelenthet némi problémát, ugyanis alapértelmezésben a recv0 hívás blokkolódik, és mindaddig nem tér vissza, amíg nem érkezik valamilyen csomag. Ráadásul kliensoldalon gyakran grafikus felhasználói felület található, amely látszólag „lefagy", mert az alkalmazás blokkolt állapotban nem tudja frissíteni a képernyőt és reagálni a felhasználói interakciókra. Ugyanez a probléma léphet fel szöveges terminál esetében is, hiszen blokkolt állapotban nem tudunk olvasni a szabványos bemenetról. Lehetséges megoldások a következők. Szöveges módban továbbra is használhatjuk a selectO, poll(), epoll() függvényt, amellyel nemcsak a socketre, hanem a szabványos bemenetre (stdin) is várakozunk. Egy másik megoldás a külön szál indítása. Ilyenkor azonban sokszor meg kell oldanunk a szálak kommunikációját és a közös adathozzáférés szinkronizációját. Külön folyamatot is indíthatunk a socketkommunikációra, de a nehézségek hasonlóak a szálakéihoz. Grafikus felhasználói felület esetén aszinkron socket kezelésére van szükségünk. A grafikus fejlesztői könyvtárak tartalmaznak ennek megvalósításához mechanizmusokat. Használjuk ezeket. A socketkommunikációban lehetőségünk van a magas szintű fájlkezelés használatára is. Erre mutat példát az alábbi kliensprogram, amely a HTTP 0.9-es protokoll segítségével lement egy internetoldalt. A kapcsolódás után a szerveralkalmazásnál már látottaknak megfelelően használhatnánk a recv() és a send() hívásokat a kommunikációra (amelyek, mint kiderült, sokkal több lehetőséget adnak a kommunikáció szabályozására), de a példa egyszerűségénél fogva kényelmesebb a magas szintű megoldást alkalmazni: #include #include #include #include #include #include #include #include int main(int argc, char* argv[]) struct addrinfo hints; struct addrinfo* res;

315

6. fejezet: Hálózati kommunikáció strucc addrinfo* p; ínt err; char ips[INET6_ADDRsTRLEN]; int csock; char buf[1024]; FILE* fpsock; FILE* fpfile; int len; if(argc != 4) { printf("Használat: %s \n", argv[0]); return 1;

memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; 1105 err = getaddrinfo(argv[1], "http", &hints, &res); if(err != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; } fpfile = fopen(argv[3], "w"); if(fpfile == NULL) { perror("fopen"); return -1; } for(p = res; p != NULL; p = p->ai_next) { lf(getnameinfo(p->ai_addr, p->ai_addrlen, ips, sizeof(ips), NULL, 0, NI_NUMERICHOST) == 0) { printf("Kacsolódás: %s\n", ips); } csock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); i f(csock < 0) continue;

I

316

if(connect(csock, p->ai_addr, p->ai_addrlen) == 0) { printf("Sikeres kapcsolódás.\n"); fpsock = fdopen(csock, "r+"); // HTTP 0.9 oldal lekérés fpríntf(fpsock, "GET %s\r\n", argv[2]); fflush(fpsock); printf("Adatok mentése.\n");

6.5. IP

whíle((len = fread(buf, 1, sizeof(buf), fpsock)) > 0) fwrite(buf, 1, len, fpfile); printf("Kapcsolat bontása.\n"); fclose(fpsock); break; }

el se perror("connect");

close(csock);

fclose(fpfile); freeaddrinfo(res);

return 0; Próbáljuk ki a programot.: ./httpment www.aut.bme.hu / test.html

6.5.13. Összeköttetés nélküli kommunikáció Összeköttetés nélküli kapcsolathoz az IP-család esetében a szállítási rétegben UDP-t (User Datagram Protocol, felhasználói datagramprotokoll) használunk Az UDP protokoll működése egyszerűbb, a fejléce kisebb, így gyors kommunikációt tesz lehetővé. Ám nem garantált, hogy •

a csomag célba ér;



az elküldött adatok ugyanabban a sorrendben érkeznek meg, amelyben küldtük őket.

Az UDP-csomagok mérete legfeljebb 64 kB lehet.

6.5.13.1. UDP-kommunikáció-példa A 6.3. Az összeköttetés nélküli kommunikáció alfejezetben bemutattuk a kapcsolat nélküli kommunikáció használatát, az előző fejezetekben pedig az IP-címek kezelését. Így egy UDP-kommunikációt megvalósító programot is összeállíthatunk.

317

6. fejezet: Hálózati kommunikáció

A datagram-alapú kommunikációknál általában nem különböztetünk meg szerver- és kliensszerepet, mivel mindkét oldal fogad és küld is adatokat. A következő példában azonban a könnyebb érthetőség kedvéért a két funkciót szétválasztottuk. Így lesz egy fogadó- és egy küldőprogramunk. A fogadóprogram UDP-csomagokat fogad. Kiírja a küldőt és a csomag tartalmát: #include #include #include #include #include #include #include





#define PORT "1122" int main() { struct addrinfo hints; struct addrinfo* res; int err; struct sockaddr_in6 addr; sockl en_t addrlen; char ips[NI_MAXHOST]; char se rvs [NI_MAXSERV] ; int sock; char buf [256]; í nt len; memset(&hints. 0, sizeof(hints)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_INET6; hínts.ai_socktype = SOCK_DGRAM; err = getaddrinfo(NULL, PORT, &hints, &res); i f(err != 0) fprintf(stderr, "getaddrinfo: %s \n", gai_strerror(err)); return -1; } if(res == NULL) {

return -1;

sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

318

b 5. IP

if(sock < 0) perror("socket"); return 1;

i f(bind(sock, res->ai_addr, res->ai_addrlen) < 0) perror("bind"); return 1;

} freeaddrínfo(res): addrlen = sízeof(addr); while((len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&addr, &addrlen)) > 0)

{ if(getnameinfoUstruct sockaddr*)&addr, addrlen, ips, sizeof(ips), servs, sizeof(servs), 0) == 0) fprintf(stdout, "%s:%s: ", ips, servs); fflush(stdout);

} write(STDOUT_FILENo, buf, len);

close(sock);

return 0;

A küldőprogram a szabványos bemeneten fogad szöveget, és soronként egyegy csomagban elküldi a szervernek. Egészen addig csinálja ezt, amíg a CTRL + D billentyűkkel leállítjuk a bevitelt: #include #include #include #include #include #include #include #include







#define PORT "1122" int main(int argc, Ihar* argv[]) struct addrinfo hints; struct addrinfo* res;

319

6. fejezet: Hálózati kommunikáció i nt err; int sock; char buf[1024]; int len; if(argc != 2)

f

printf("Használat: %s \n", argv[0]); return 1;

memset(&hints, 0, sizeof(hints)); hints.aí_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; err = getaddrínfo(argv[1], if(err != 0)

PORT, &hints, &res);

{

fprintf(stderr, "getaddrinfo: %s\n", gaí_strerror(err)); return -1; }

if(res == NULL) {

return -1; }

sock = socket(res->ai_family, res->ai_socktype, res->aí_protocol); if(sock < 0) perror("socket"); return -1;

while((len = read(ST0IN_FILENO, buf, sizeof(buf))) > 0) {

sendto(sock, buf, len, 0, res->ai_addr, res->ai_addrlen);

freeaddrínfo(res); close(sock); return 0;

6.5.13.2.

Többes küldés

Amikor nemcsak egy végpontnak, hanem egyszerre többnek szeretnénk elküldeni valamit, elég egyszerű dolgunk van: speciális IP-címre kell küldenünk a csomagot. A többes küldést (multicast) az IP-hálózaton az útválasztók bonyolítják le. 320

6.5. IP

IPv4 esetén valamelyik D osztályú (224.0.0.0-239.255.255.255) címet kell használnunk. Minden D osztályú cím egy hosztcsoportot jelent Vannak ideiglenes hosztcsoportok, amelyhez csatlakozhatunk, vagy amelyból kiválhatunk. Van azonban néhány állandó cím is. 87 6.11. táblázat. Előredefiniált IPv4-es multicastcsoportok

Az egy LAN-on lévő útválasztók

Az IPv6 esetén a többes küldés címeinek az ff00::/ 8 tartomány van fenntartva. Ebben az esetben is számos előre definált csoport van. 88 6.12. táblázat. Előredefiniált IPv6-os multicastcsoportok

Az egy LAN-on lévő útválasztók

A többes küldés fogadása előre definiált csoportoknál nem igényel plusz műveletet. Ha azonban dinamikusan létrehozott csoportot szeretnénk használni, akkor a socketünket hozzá kell rendelnünk a kiválasztott csoporthoz. Ezt a setsockopt0 függvénnyel (bővebben lásd a 6.6. Socketbeállítások alfejezetben) végezhetjük el: #include ai_family, res->ai_socktype, res->ai_protocol); if(sock < 0) {

perror("socket"); return 1;

if(bind(sock, res->ai_addr, res->ai_addrlen) < 0) {

perror("bind"); return 1;

freeaddrinfo(res); memset(&mreq, 0, sizeof(mreq)); inet_pton(AF_INET, "224.1.1.1", &mreq.imr_multiaddr);

324

6.5. IP if(setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) != 0 ) perror("IP_ADO_MEMBERSHIP"); return -1;

memset(&mreq6, 0, sizeof(mreq6)); net_pton(AF_INET6, "ff02 : :13D" , &mreq6, pv6mr_mul ti add r) ; i f(setsockopt(sock, IPPROTO_IPv6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6)) != 0 ) perror("IPV6_JOIN_GROUP"); return -1; }

addrlen = sizeof(addr); while((len = recvfrom(sock, buf, sizeof(buf), 0, (strucc sockaddr*)&addr, &addrlen)) > 0)

MI

i f(getnameinfo((struct sockaddr*)&addr, addrlen, ips, sizeof(ips), servs, sizeof(servs), 0) == 0) { fprintf(stdout, "%s:%s: ", ips, servs); fflush(stdout);

1

write(STDOUT_FILENO, buf, len);

close(sock); return 0;

A szolgáltatásunkat hozzárendeljük a 224.1.1.1 IPv4-es és az f'f()2::13D IPv6-os csoportcímekhez. Ha lefuttatjuk a módosított fogadóprogramot, akkor a korábbi küldőprogrammal tesztelhetjük. Feladat Teszteljük a módosított UDP-fogadó programot úgy, hogy a küldőprogramnál a következő címeket használjuk: 224.1.1.1, ff02::13D, ff02::1.

325

6. fejezet: Hálózati kommunikáció

6.6. Socketbeállítások A socketbeállításokkal a socket működésének számos jellemzőjét tudjuk módosítani. Egy-egy socketművelet során több protokollszintet is használunk egyszerre. Egy TCP-kommunikáció során például használjuk a TCP-, az IPés a socketszintet. Az egyes szinteken különböző opciókat tudunk beállítani és ezzel hangolni a kommunikációt. A beállításokat a getsockoptQ rendszerhívással kérdezhetjük le, és a setsockoptO rendszerhívással módosíthatjuk: #include int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

Az első paraméter a socket leírója. Ezzel hivatkozunk arra a socketre, amelynek a beállításait módosítani szeretnénk. A level paraméterrel adjuk meg, hogy melyik protokollszinthez tartozik az a beállítás, amelyet kezelni szeretnénk. A 6.16. táblázatban összefoglaljuk a tipikusan használt értékeket. 6.16. táblázat. IP protokoll szintek

Érték

Szint

„man" oldal

SOL_SOCKNT

socket és egyben Unix

socket(7), unix(7)

1PPROTO_IP

IPv4

ip(7)

rPPROTO IP6

IPv6

ipv6(7)

IPPROTO_TCP

TCP

tcp(7)

1PPROTO_UDP

UDP

udp(7)

Az optname paraméterben adjuk meg a kiválasztott opciót. A lehetséges beállítások a protokollszinttől függenek. Az előző táblázat utolsó oszlopában megadtuk az adott szintet leíró „man" oldal nevét. Ezek a leírások tartalmazzák a teljes beállításlistát. Az optval paraméter tartalmazza a beállítás értékét, míg az optlen a változó méretét. Az érték típusa függ a beállítástól, és az előbb említett leírások tartalmazzák. Az alábbiakban kiemelünk néhány gyakrabban használt beállítást. A többes küldés beállításaira nem térünk ki (ezeket lásd a 6.5.13.2. Többes küldés alfejezetben).

326

6.6. Socketbeállítások

SO_KEEPALIVE A socketszint tartalmazza. Kapcsolatalapú socket esetén ú. n. életben tartó (keep-alive) üzeneteket küld. Ennek segítségével abban az esetben is érzékelhetjük a kommunikációs hibát, ha egyébként hosszan nem forgalmazunk adatot. Az érték típusa integer. 0 és 1 értékkel kapcsolhatjuk ki, illetve be.

SO_REUSEADDR A socketszint tartalmazza. A bindo rendszerhívás számára jelzi, hogy a lokális portot újra felhasználhatja, ha éppen nincs aktív socket az adott porton. Az érték típusa integer. 0 és 1 értékkel kapcsolhatjuk ki, illetve be. Útmutató Ahogy a korábbi példáinkban is látható volt, ennek a beállításnak a használata igen gyakori. Ha nem használnánk, akkor a folyamat leállítását követően nem lehetne az alkalmazást azonnal újraindítani, mert ekkor a bind() művelet hibával térne vissza. Azt jelezné, hogy a portot már másik folyamat használja. Ekkor ki kell várnunk egy bizonyos időt, amíg a port újra használhatóvá válik. A SO_REUSEADDR beállítás használatával viszont a port azonnal újrahasznosítható, ha lezártuk a szerversocketet.

TCP_CORK, UDP_CORK A TCP-, illetve az LTDP-réteg tartalmazza őket. Ha beállítjuk ezt az opciót, akkor a sendO, illetve a sendto0 függvények meghívásakor nem küldi el azonnal a csomagokat, hanem egy várakozási sorban gyűjtögeti őket. Ezt követően az opció kikapcsolásakor az adatok kiküldése egy csomagban történik. Az érték típusa integer, és 0/1 értékekkel kapcsolhatjuk ki és be az opciót. Ezek a beállítások nem hordozhatók, csak Linux rendszereken működnek. Az alábbi példában a korábbi UDP-küldő program módosításával demonstráljuk a beállítás működését: #include #include #include #include #include #include #ínclude #include #include #define PORT "1122" #define SAYS "Simon mondja: " int main(int argc, char* argv[]) struct addrinfo hints; struct addrinfo* res; ínt err;

BEI 327

6. fejezet: Hálózati kommunikáció

int sock; char buf[1024]; int len; int state; i f(argc != 2) printf("Használat: %s \n", argv[0]); return 1;

memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hínts.ai_socktype = SOCK_DGRAM; err = getaddrinfo(argv[1], PORT, &hínts, &res); if(err != 0) {

fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1;



}

if(res == NULL) {

return -1;

sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); i f(sock < 0) {

perror("socket"); return -1; }

state = 1; setsockopt(sock, IPPROTO_UDP, UDP_CORK, &state, sizeof(state)); while((len = read(STDIN_FTLENO, buf, sizeof(buf))) > 0) {

sendto(sock, buf, len, 0, res->ai_addr, res->ai_addrlen); }

freeaddrinfo(res); state = 0; setsockopt(sock, IPPROTO_UDP, UDP_CORK, &state, sizeof(state)); close(sock): return 0;

328

6.6. Socketbeállítások

Fordítsuk le és próbáljuk ki a programot. Azt tapasztaljuk, hogy a beírt szöveget nem soronként küldi el a fogadónak, hanem egyben, egy csomagban. A gyakorlatban finomabban tudjuk szabályozni a csomagok összevonását a sendO, illetve a sendto() rendszerhívás MSG_MORE opciójával. Ilyenkor az MSG MORE opcióval küldött adatok belekerülnek a várakozási sorba, majd a soron következő MSG_MORE opció nélküli hívás során az adatok kiküldése egyben történik meg. Ennek működését a következő példaprogram mutatja be: #include #include #include #include #include #include #í nclude #include #define PORT "1122" #define SAYS "Simon mondja: " int main(int argc, char* argv[]) struct addrinfo hints; struct addrinfo* res; int err; int sock; char buf[1024]; int len; if(argc != 2) {

printf("Használat: %s \n", argv[0]); return 1; }

memset(&hints, 0, sizeof(hints)); hints.ai_famíly = AF_UNSPEC; hints,ai_socktype = SOCK_DGRAM; err = getaddrinfo(argv[1], if(err != 0)

PORT, &hints, &res);

{

fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; if(res == NULL)

t

return -1;

sock = socket(res->ai_family, res->aí_socktype, res->ai_protocol);

329

6. fejezet: Hálózati kommunikáció

i f(sock < 0) perror("socket"); return -1;

while((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {

sendto(sock, SAYS, strlen(SAYS), MSG_MORE, res->ai_addr, res->ai_addrlen); sendto(sock, buf, len, 0, res->ai_addr, res->ai_addrlen);

freeaddrinfo(res); cl ose(sock) ; return 0;

Ha a programot kipróbáljuk, akkor azt tapasztalhatjuk, hogy a két sendto0 hívás adatai egyben, egy csomagban küldődnek ki.

6.7. Segédprogramok A hálózati kommunikáció implementálásakor belefuthatunk hibákba, amikor a kommunikáció valamiért nem tud felépülni a programjaink között. Ilyenkor segédprogramok segíthetnek kideríteni, hogy meddig jutottunk el a kapcsolat felépítésében. Az egyik legfontosabb program a netstat, amely számos információt kiír a hálózati alrendszerről. Paraméterek nélkül kiírja az éppen élő hálózati kapcsolatokat. A -a paraméterrel a lista kiegészül a szerver- és az UDP-socketekkel. A -t és a -u opcióval kiegészítve a listát leszűkíthetjük a TCP-, illetve UDP-socketekre. Emellett a -n opcióval kikapcsolhatjuk a névfeloldást, és mindent numerikusan láthatunk A netstat a forgalmazott csomagokról nem ad információt, ám a tcpdump program igen. Segítségével monitorozhatjuk a hálózati csomagforgalmat, és alacsony szinten megnézhetjük, hogy a programunk tényleg küld-e vagy kape csomagokat. A tcpdump program szöveges interfésze miatt nagyobb forgalom esetén nehezen kezelhető. Ám a feladatra létezik egy grafikus felhasználói felülettel rendelkező program, a wireshark. 89 A grafikus felület nagyban segíti a program használatát és az adatok értelmezését. 89

A Wireshark program weboldala: http://www.wireshark.org/.

330

6.8. Távoli eljáráshívás

, '

a

Q.7 ,Caoture

émkae

C4r8.uril8Frer8 wON0(.3 , p ,rdr,,,t 147 66 188 111 rWcres'neri: 1671 Tele.$.71lt Jpols Irccm,lls he(:.

a a •

ZT ITU ‚

0>sbnac e 1 0.000000

2 0.000514

J üi ts

•"•

848•8388:0 ,

Hetimtett-_f3:de:ta tlicrosof_fb. ld: f

,

Lenath

Broadcast Hewlett

-

ARP t3 de la

ARP

42 who has 152.66.188.11? is a1 00:15

65 I52.66 188.11

-- .11 • II

Frame 1, 42 bytes un wtre (336 birs), 42 bytes captured ( 336 blts) Ethernet II. Src: Hewlett. fS:de:la (68:b5:99:f3:de: la), 0st: Broadcast (ff:ff:ff:ff:ff:ff)

6.5. ábra. A Wireshark főablaka

A Wireshark program felületét a 6.5. ábra mutatja. A program főablaka három részre tagolódik. A felső részben egy listát találhatunk, amely az elkapott csomagokat tartalmazza. Ha ebből a listából kiválasztunk egy csomagot, akkor az alsó részben láthatjuk a bináris tartalmát, míg a középső rész ezt értelmezett formában mutatja. A vizsgálódásaink során elsősorban a középső részt használjuk. A középső mező a kiválasztott csomag által tartalmazott keretek listáját mutatja. Ahogy a csomag az egyes hálózati rétegeken keresztül haladva felépül, minden réteg egy új keretet alkot belőle. Minden keret rendelkezik néhány fejlécmezővel és egy adatrésszel. A lista elemeit lenyitva a fejlécmezők tartalmát tekinthetjük meg.

6.8. Távoli eljáráshívás A távoli eljáráshívás (Remote Procedure Call, RPC) egy magas szintű kommunikációs paradigma. Lehetővé teszi távoli gépeken lévő eljárások meghívását, miközben elrejti a felhasználó elől az alsóbb hálózati rétegeket. Az RPC logikai kliens-szerver modellt valósít meg. A kliens szolgáltatási igényeket küld a kiszolgálónak. 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.

331

6. fejezet: Hálózati kommunikáció

Az RPC megkíméli a felhasználót az alsóbb hálózati rétegek ismeretétől és programozásától. A hívások transzparensek. A hívónak explicite nincs tudomása az RPC-ről, a távoli eljárásokat ugyanúgy hívja, mint egy helyi eljárást.

6.8.1. Az RPC-modell A távoli eljáráshívás modellje hasonlít a helyi eljáráshívás modelljére. A helyi eljárás hívója a hívás argumentumait egy jól meghatározott helyre teszi, majd átadja a vezérlést az eljárásnak. A hívás eredményét a hívó egy jól meghatározott helyről elveszi, és tovább fut. 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ó üzenetet küld a szervernek, majd válaszra vár. 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üzenetből az eredményt, és tovább fut.

A gép

B gép

Kliensprogram

Szerverdémon RPC-hívás Szolgáltatás meghívása

°17 Válasz

Szolgáltatás végrehajtása

Szolgaltatás kész

Program folytatódik

6.6. ábra. RPC kommunikáció -

A szerveroldalon egy várakozó — alvó — processz várja a hívók üzeneteit. Az érkező üzenetből kiveszi az eljárás paramétereit, elvégzi a feladatot, majd visszaküldi a válaszüzenetet. A két processz közül egy időben csak az egyik aktív, a másik várakozó állapotban van.

6.8.2. Verziók és számok Minden RPC-eljárást egyértelműen 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 verziószáma is. Így a szolgáltatások bővítése vagy változtatása esetén 332

6.8. Távoli eljáráshívás

kell új programszámot adni, csak a verziószámot növelni. A programszámok egy része előredefiniált, más részük fenntartott. A fejlesztők számára a 0x20000000-0x3fffffff tartomány áll rendelkezésre. nem

6.8.3. Portmap Minden hálózati szolgáltatáshoz dinamikusan vagy statikusan hozzá lehet rendelni 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 egy RPC-kérő üzenetet küld a távoli gépen futó portmap démonnak. A portmap RPC-válaszüzenetben megadja a kért portszámot. Ezután a kliens már közvetlenül küldhet üzenetet a megadott portra. A fentiekből 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.

6.8.4. Szállítás Az RPC független a szállítási protokolltól. Nem foglalkozik azzal, hogy miként adódik át az üzenet az egyik processztő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 - például az összeköttetés-alapú TCP - felett kevésbé okoz gondot. Egy kevésbé megbízható szállítási réteg - például UDP - felett futó RPC-alkalmazásnak magának kell gondoskodnia azonban 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 egyszerűség kedvéért a TCP-t ajánlatos használni.

6.8.5. XDR Az RPC feltételezi az ún. külső adatábrázolás (eXternal Data Representation, XDR) meglétét. Az XDR gépfüggetlen adatleíró és kódoló nyelv, amely jól használható különböző számítógép-architektúrák közötti adatátvitelnél. Az RPC tetszőleges adatstruktúrák kezelésére képes, függetlenül az egyes gépek belső adatábrázolásától. Az adatokat elküldés előtt XDR-formára alakítja (serializing), a vett adatokat pedig visszaalakítja (deserializing).

333

6. fejezet: Hálózati kommunikáció

6.8.6.

rpcinfo

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övetkező:

6.8.7. rpcgen A távoli hívást használó alkalmazások programozása nehézkes és bonyolult lehet. Az egyik nehézséget éppen az adatok konverziója jelenti. Szerencsére létezik egy rpcgen nevű 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-re. A fordító az interfészdefiníció alapján néhány C nyelvű forráslistát állít elő: a közösen használt definíciókat (headerfájlt), a kliens- és a szerveroldali RPC-programok vázát (skeleton). A vázak feladata az, hogy elrejtsék az alsóbb hálózati rétegeket a program többi részétől. A programfejlesztőnek mindössze az a feladata, hogy megírja a szerveroldali eljárásokat. Az eljárások tetszőleges nyelven megírhatók, figyelembe véve természetesen az interfészdefiníciót és a rendszerhívási konvenciókat. Az eljárásokat összeszerkesztve a szerveroldali vázzal, megkapjuk a futtatható szerveroldali programot. A kliensoldalon a főprogramot kell megírni, ebből hívjuk a távoli eljárásokat. Ezt linkelve a kliensoldali vázzal, megkapjuk a futtatható kliensprogramot. Az rpcgen nek létezik egy —a kapcsolója is, amely tovább egyszerűsíti a feladatunkat Használatával az interfészállományból még a megírandó programrészekre is kapunk egy C nyelvű vázat, amelyet már csak ki kell egészítenünk, hogy használhassuk. (Ügyeljünk arra, hogy egy ismételt generálással a C-vázak felülíródnak, ezért célszerű átneveznünk őket.) Így általában a következő parancsot használjuk: -

-rpoOr-41, - I rltferfa~. ahol az interface.x az interfészállomány.

334

1

6.8. Távoli eljáráshívás

6.8.8. Helyi eljárás átalakítása távoli eljárássá Tegyük fel, hogy van egy eljárásunk, amely lokálisan fut. Ezt az eljárást szeretnénk távolivá tenni. Az átalakítást egy egyszerű példán vizsgáljuk meg. A lokális program a híváskor megadott paramétert, mint üzenetet, kiírja a képernyőre. A cél az, hogy távoli gép képernyőjére is tudjunk üzenetet kiírni. A program lokális változata a következő: /* printmsg.c - uzenet kiirasa a kepernyore. */ #include int printmessage(char* msg) í printf("%s\n", msg); return 1;

int main(int argc, char* argv[]) { char* message; i f(argc != 2) t fprintf(stderr, "Hasznalat: %s \n", argv[0]); return -1; message = argv[1]; tf(!printmessage(message)) fprintf(stderr, "%s: nem megjelenitheto az uzenet.\n", argv[0]); return -1; }

printf("Az uzenet elkuldve!\n"); return 0;

A protokoll kialakításához szükség vannak annak ismeretére, hogy az eljárásnak milyen típusú paraméterei és visszatérési értékei vannak. Jelen esetben a printmessage0 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ésű állományban írjuk meg: /* msg.)( - Tavolí uzenet nyomtatas protokollja.''/ program MESSAGEPROG version MESSAGEVERS { int PRINTMESSAGE(string) = 1; } = 1; } = 0x20000099;

/* /* /* /* /*

programnev verzionev 1. fuggveny verzioszam programszam

*/ */ */ */ */

335

6. fejezet: Hálózati kommunikáció

A távoli program egyetlen eljárást tartalmaz, amelynek száma 1. Az RPC automatikusan generál egy O. eljárást is. Ez a szerver megszólítására (pinging) használható A nagybetűk használata nem szükséges, csak hasznos konvenció. Észrevehető, hogy az argumentum típusa nem char*, hanem string. Ez azért van, mert a különböző nyelvek másként értelmezik a szöveges típust, sőt a char* még a C nyelvben sem egyértelmű. A használható változótípusokat az XDR dokumentációja tartalmazza. Nézzük a távoli eljárás implementációját: /* msg_proc.c - Tavoli printmessage eljaras. */ #include #include #include "msg.h" int* printmessage_1(char** msg)

f

static int result; printf("%s\n", *msg); result = 1; return (&result);

A printmessage_10 távoli eljárás három dologban különbözik a printmessage() helyi eljárástól. Argumentumként stringre mutató pointert vesz át string helyett. Ez minden távoli eljárásra érvényes. Ha nincs argumentum, akkor void*-ot kell megadni. Vissza egész számra mutató pointert ad, egész helyett. Ez szintén jellemző a távoli eljárásokra. A pointer-visszaadás miatt kell a result változót staticként megadni. Ha nincs visszatérési érték, akkor void*-ot kell használni. 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 protokolldefinícióban szereplő név kisbetűkkel, egy aláhúzási karakter, végül pedig a verziószám. Végül következzen a kliensprogram, amely a távoli eljárást meghívja: /* rprintmsg.c - A printmsg.c tavoli verzioja. */ #include #include #include "msg.h" ínt maín(int argc, char* argv[]) { CLIENT* cl; int* result; char* server; char* message;

336

6.8. Távoli eljáráshívás i f(argc != 3)

{ fprintf(stderr, "Hasznalat: %s .szerver> \n", argv[0]); return -1; server = argv[1]; message = argv[2]; /* A kliensleiro letrehozasa, kapcsolat felvetele a

szerverrel. -V cl = clnt_create(server,

MESSAGEPROG, MESSAGEVERS ,

"tcp");

if(c1 == NULL) {

clnt_pereateerror(server); return -1; }

7* A tavoli eljaras meghivasa. */

result = printmessage_1(&message, cl); if(*result == NULL) {

clnt_perror(cl, "call failed"); return -1; }

if(result == 0)

f

fprintf(stderr, "%s: %s nem megjelenitheto az uzenet\n", argv[0], server); return -1; printf("Az uzenet elkuldve a %s szervernek!\n", server); return 0;

Első lépésként a kapcsolatot hozzuk létre a clnecreate0 meghívásával. A visszakapott leírót a távoli eljárás argumentumaként használjuk. A cInt_create0 utolsó paramétere a szállítási protokollt adja meg. Jelen esetben ez TCP, de lehet UDP is. A printmessage_1O 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 módon 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.

337

6. fejezet: Hálózati kommunikáció

A kiegészítendő kódvázat (skeleton) tartalmazó állományokat az

r>00:111~ • x paranccsal generálhatjuk. Ez a következő á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 kliens- és szerveroldali formában is.



A kliensprogram vázát az msg_clnt.c állományban. Ebben szerepel a printmessage_1() eljárás, amelyet a kliensprogram hív.



A kiszolgálóprogram vázát az msg_suc.c állományban. Ez a program hívja az msgproc.c állományban található printmessage_10 eljárást.

Az rpcgen a —a kapcsoló használatakor ezek mellett a kliens- és a szerverprogram egyszerű 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 generált kliens- és szerverprogram forrását is letörli.)

338

HETEDIK FEJEZET

Fejlesztés a Linuxkernelben Az operációs rendszer egyik feladata az, hogy egy egységes, magas szintű programozási felületet nyújtva elrejtse a felhasználó elől a rendszer hardverelemeinek a kezelését. Az állománykezelés során például nem kell foglalkoznunk a konkrét tárolóeszköz közvetlen vezérlésével, mivel az operációs rendszer megteszi ezt helyettünk. Ha a fejlesztők belemerülnek a Linux-kernelbe, annak az az egyik leggyakoribb oka, hogy speciális eszközeikhez meghajtóprogramot szeretnének készíteni. Ilyenkor az a feladat, hogy az eszköz alacsony szintű interfészére építve megvalósítsák az operációs rendszer magas szintű interfészét. A Unix világában az eszközöket állományként, az úgynevezett fájlabsztrakciós interfészen kezeljük (lásd a 2.1. A Linux-kernel felépítése alfejezetben). Ez a Linux-meghajtóprogram esetében azt jelenti, hogy az eszköz kezelését állományműveletekre kell leképeznünk. 9° A Linuxnál ez a feladat sok esetben nem is olyan nehéz, ám komoly odafigyelést igényel, mivel a kernel a rendszer érzékeny része: egy elrontott kód ezen a helyen a rendszer összeomlását is eredményezheti. Természetesen az eszközök skálája, így az eszközvezérlőké is, elég széles. Ráadásul a kernel kapcsolódó függvényeit gyakran váltják fel új verziók. Ezért ebben a fejezetben átfogó bevezetést nyújtunk a leggyakrabban használt technikákba, főként a kevéssé változó alapelvek ismertetésére és azok illusztrálására szorítkozunk, mintegy kiindulási alapként azok számára, akik ebben a témában szeretnének elmélyedni. Linux alatt az eszközvezérlőket kernelmodulként valósítjuk meg. A kernelmodul a betöltés után hozzákapcsolódik a kernelhez, és annak szerves részét alkotja. Így lényegében a kernelmodul-fejlesztés Linux-kernel-fejlesztést jelent.

9

° A Linuxban nem minden meghajtó rendelkezik állományinterfésszel, ám a többség igen.

7. fejezet: Fejlesztés a Linux-kernelben

7.1. Verziófüggőség A kernel a Linux egyik legdinamikusabban fejlődő része. Ebből következően a verzióváltások során gyakran kerülnek bele strukturális és szintaktikai változások. A 2.4-es stabil kernel verzióról a 2.6-os stabil verzióra történő váltás a modulok jelentős átírását tette szükségessé, mert a két kernelverzió közötti különbségek jelentősek voltak. A 2.6-os verziótól kezdődően a kernel fejlesztési ciklusa megváltozott. Megszűnt a stabil mellett párhuzamosan megjelenő fejlesztői kernelváltozat, így a változások közvetlenül a kernel egyetlen fejlesztési vonalába kerültek. Ennek egyik hatása az, hogy szinte folyamatosan kisebb átalakításokat kell végeznünk a moduljainkon. Természetesen, ha két egymástól számozásban távolabb eső 2.6.X-es kernelverzió között váltunk, akkor a kis módosítások száma is megnövekszik. Ugyanakkor nem jellemző olyan drasztikus nagy átalakítás, mint a korábban említett 2.4 ---> 2.6 váltás esetében. Ebből is következik, hogy a kernel fő verziószámai régóta nem is változtak. 9 ' A nemrég megjelent 3.0-s kernel verziószáma is valójában csak szimbolikus, és nem tartalmaz jelentős eltéréseket a 2.6.39-es verzióhoz képest. A folyamatos fejlődés tükrében fontos felhívni a figyelmet arra, hogy az ebben a fejezetben található forráskódok a könyv íráskor aktuális 2.6.39-es kernelverzióhoz készültek. Jóllehet későbbi verziók használatakor előfordulhat, hogy a szintaktika egyes pontokon eltér, ám a kód mögött álló alapelvek lassabban változnak, így reményeink szerint a fejezet a későbbi kernelverzióknál is aktuális marad. A kisebb szintaktikai módosításokat általában a fordításkor kapott hibajelzések alapján rögtön észrevesszük. Ilyenkor a helyes szintaktika könnyen felderíthető az aktuális kernelforrás tanulmányozásával. Ha keresünk például egy olyan eszközvezérlőt, amely a problémás részletében hasonlít a mi eszközvezérlőnkre, akkor innen kideríthetjük, hogy az aktuális kernelverzió esetében mi a helyes szintaktika. Időnként a kernelforrás mellett található dokumentációk is segíthetnek. Sajnos a kernel nem tartozik a Linux jól dokumentált részei közé, ugyanis a forráskód fejlesztése általában gyorsabb, minthogy a dokumentáció követni tudná. Szerencsére az utóbbi években jelentős fejlődés tapasztalható ezen a téren is: a kernelfejlesztó'k egyre inkább dokumentálják a munkájukat.

91

A 2.6.0-s kernelverzió 2003. december 17-én jelent meg. A 2.6.X-es kernelek korszaka egészen 2011. július 21-ig tartott, amikor Linus Torvalds bejelentette a 3.0-s verziót a Linux-kernel 20. születésnapja alkalmából.

340

7.2. A kernel- és az alkalmazásfejlesztés eltérései

7.2. A kernel- és az alkalmazásfejlesztés eltérései Először részleteznünk kell a kernel és az alkalmazások fejlesztése közti különbségeket. A későbbi fejezetekben elsősorban kernelmodulok fejlesztését tárgyaljuk, ám nincs különbség a modulként elkészített és a kernelbe közvetlenül belefordított kódok között a megkötések tekintetében. Először is a kernelrészek készítésénél lehetőleg felejtsünk el más nyelveket, és dolgozzunk C-ben, esetleg assemblyben. Általában az alkalmazások egy feladaton dolgoznak az indítástól a folyamat végéig. Ezért ezeknek egyetlen belépési pontjuk van, C nyelvű programok esetén tipikusan a main függvény. A kernelmodulok életciklusa sokkal jobban hasonlít a nem statikus programkönyvtárakéhoz: a programkönyvtár rendelkezésre állását regisztrálással adjuk a rendszer tudtára, ettől kezdve arra vár, hogy egy program betöltse és meghívja a szolgáltatásait. Hasonlóképpen a kernelmodulok először csak regisztrálódnak, hogy később feladatokat teljesíthessenek, és ezzel az inicializáló függvényük már véget is ér. A modul másik belépési pontja a tisztogatófüggvény, amely a modul eltávolításakor aktivizálódik, pontosabban közvetlenül előtte. A modul szolgáltatásait kötött prototípusú függvények valósítják meg, amelyeket a programok a kernel közvetítésével például az állományabsztrakciós interfészen keresztül használhatnak. Szekvenciálisan lefutó kódot csak úgy implementálhatunk, ha kernelszálat indítunk (lásd a 7.17. Várakozás alfejezetet). A programok fejlesztésekor gyakran használunk olyan függvényeket, amelyek fejlesztői könyvtárakban vannak. Ilyenkor maga a program nem tartalmazza a függvényt, hanem a fordítási fázis után a linker vagy a betöltőprogram a linkelési fázisban oldja fel ezeket a külső hivatkozásokat. Ilyen például a printf() függvény, amely a libc könyvtárban található. Ezzel szemben a modulok csak a kernellel linkelődnek, ezért csak olyan függvényeket használhatnak, amelyek a kernelben (beleértve más modulokat) már léteznek. Természetesen a modulok fejlesztésében nem használhatunk semmilyen libc függvényt, hiszen az egy jóval magasabb szintű, felhasználói üzemmódban használatos függvénykönyvtár. A kernel által rendelkezésre bocsátott függvényeket a / proc /kallsyms virtuálisszöveg-állományban tudjuk megnézni. Ez csak az éppen aktuális függvénylistát jeleníti meg, amely a modulok betöltésével és eltávolításával bővülhet, illetve csökkenhet. Mivel nem használhatunk fejlesztői könyvtárakat, így a hagyományos headerállományok sem szerepelhetnek a kódunkban. Helyettük a kernelforrás include alkönyvtárában található állományok kerülnek bele, és erről a kernelforrás Makefile-ja gondoskodik azzal, hogy a megfelelő elérési utakat adja meg a fordítónak. Ebben a könyvtárban találunk egy linux és egy asm könyvtárat, amelyek az általunk használható headerállományokat tartalmazzák.

341

7. fejezet: Fejlesztés a Linux-kernelben

A kernelrészek kódjait és adatait általában a fizikai memóriában tároljuk, vagyis nem kerülhetnek ki egy lapcsere folytán a merevlemezre. Hogy megértsük ennek az okát, nézzünk meg egy példát. Tegyük fel, hogy a kernel adatait is olyan virtuálismemória-területen tartjuk, mint az alkalmazásokéit. Ebben az esetben előfordulhat, hogy egyes lapok kikerülnek a lapcserepartícióra, ha kevés a hely a fizikai memóriában. Ha éppen a lapcsere algoritmuskódja vagy adatai kerülnének ki: többet nem lehetne visszatölteni sem ezt a lapot, sem más lapokat a rendszerbe. Természetesen a fizikai memória véges, ezért ne szabad elpazarolni. Nagy mennyiségű adathoz kernelfejlesztéskor is lehetőségünk van arra, hogy virtuális memóriát is allokáljunk. Ezekre a területekre csak adatokat helyezhetünk el, és számolnunk kell az esetleges lapcsere okozta lassabb hozzáféréssel. Lebegőpontos számításokat ne használjunk. Ha mégis szükséges lenne, akkor nekünk kell gondoskodnunk az FPU állapotának lementéséről és viszszaállításáról. Az utolsó nagy eltérés a hibák lekezelésénél tapasztalható. Amíg a programok fejlesztésekor egy hibás memória, illetve I/O művelet (segmentation fault) a rendszer szempontjából nem veszélyes, és könnyen kiszűrhető, a kernelmodulban elkövetett hasonló hiba komoly következményekkel, akár még a rendszer összeomlásával is járhat. Ugyanakkor a kernel fejlődésével a hibakezelő metódusok is fejlődtek, így manapság az esetek többségében a rendszer még működőképes marad, és olyan hibajelzést (a jól ismert Oops üzenet) képes produkálni, amely nagyban segíti a fejlesztést. Ugyanakkor egy hiba után a rendszer instabil állapotba kerülhet, ezért célszerű újraindítani. Ellenkező esetben furcsa, nem determinisztikus hibajelenségeket tapasztalhatunk, amelyek lehetetlenné tehetik a hiba felderítését. A hibakeresés is nehézkesebb: az alkalmazásoknál használt módszerek nem eredményesek, vagy csak komolyabb előkészületek után, és akkor is csak megkötésekkel. Ám más módszerek is rendelkezésünkre állnak, bár ezek sokszor nem nyújtanak olyan segítséget, mint amit az alkalmazásoknál megszokhattunk. A továbbiakban bemutatunk majd néhány jól használható módszert.

7.2.1. Felhasználói üzemmód — kernelüzemmód A felhasználói alkalmazások saját virtuális címteret kapnak indításkor: ebben a címtérben tudnak dolgozni a futásuk során. Ugyanakkor az egymás mellett futó párhuzamos folyamatok el vannak választva egymástól. Egymás területeihez nem férhetnek hozzá, csak szabályozott csatornákon érintkezhetnek (lásd a korábbi fejezetekben). A folyamat virtuális címtartománya 32 bites rendszerben 2 32 , vagyis 4 GB. Ezt a Linux rendszer két részre osztja. Az alső 3 GB a felhasználói címtartomány, míg a felső 1 GB a kernel számára fenntartott tartomány. A méretek függetlenek attól, hogy a számítógép valójában mennyi fizikai memóriát tartalmaz, mert itt csak virtuális címterületekről van szó. 342

7.3. Kernelmodulok

A mai processzorok több privilégiumszint használatát teszik lehetővé. Ezek azt szabályozzák, hogy a folyamatok mihez férhetnek hozzá. Közülük a Linux két szintet használ: a felhasználói üzemmódot és a kernelüzemmódot. A fő különbség a két üzemmód között az, hogy felhasználói üzemmódban a folyamatok a felső kernelcímtérhez nem férhetnek hozzá, nem olvashatják, írhatják vagy futtathatják a rajta lévő kódot. Az átjárást a felhasználói üzemmód és a kernelüzemmód között a rendszerhívások nyújtják. A rendszerhívások meghívásával tudnak a folyamatok olyan műveleteket végrehajtani, amelyek a saját virtuális birodalmukon kívül található részekre hatással vannak. Ilyenkor a rendszer ellenőrzi a jogosultságaikat, és végrehajtja a műveletet. Azok a műveletek, amelyek kernelmódban futnak, például a rendszerhívások implementációja vagy a késó'bb tárgyalandó kernelszálak, hozzáférhetnek a kernel címtartományának az egészéhez. Ez azt jelenti, hogy semmilyen korlátozás nem vonatkozik rájuk, bármilyen műveletet végrehajthatnak. Vagyis elmondhatjuk, hogy a hibás vagy a rosszindulatú kódot tartalmazó modulokkal szemben a kernel védtelen, ezért a rendszer adminisztrátorára, illetve a rendszerfejlesztőkre hárul az a feladat, hogy ezektől a veszélyektől a rendszert megvédjék.

7.3. Kernelmodulok A Linux-kernel eredetileg monolitikus, vagyis egyetlen nagy program, amely tartalmaz minden olyan funkciót és belső adatstruktúrát, amely az operációs rendszerhez tartozik (lásd a 2.1. A Linux kernel felépítése alfejezetben). Az alternatíva a mikrokernel lenne, amely minden funkcionális elemet külön egységekben tartalmaz, és az egyes részek között jól meghatározott kommunikáció zajlik. Így minden új komponens vagy funkció hozzáadása a kernelhez nagyon időigényes feladat lenne. A korai kernelverzióknál, ha egy új eszközt tettünk a gépbe, a kernelt ténylegesen újra kellett konfigurálni és fordítani. A Linux úgy orvosolta a monolitikus kernel rugalmatlanságát, hogy az 1.2-es verzió óta már teljes támogatást nyújt a futás közben betölthető kernelmodulok használatához. A kernelmodulok olyan tárgykódú binárisok, amelyeket a rendszer indítása után bármikor betölthetünk, és dinamikusan hozzálinkelhetünk a kernelhez. Majd amikor már nincs rá szükségünk, lekapcsoljuk (unlink), és eltávolíthatjuk a modult memóriából. Ez lehetővé teszi, hogy az operációs rendszer egyes komponenseit dinamikusan betöltsük és eltávolítsuk, attól függően, hogy szükségünk van-e rájuk, vagy sem. A legtöbb eszközvezérlőt modulként valósították meg, és ez a megoldás javasolható a saját eszközvezérlőink elkészítésénél is. Ez a megközelítés magát a fejlesztést is nagyban könnyíti és gyorsítja. -

343

7. fejezet: Fejlesztés a Linux-kernelben

Feladat A példa-eszközvezérlö feladata az lesz, hogy olvasáskor kiírja a „Hello!" szöveget, íráskor pedig a konzolra kiírja a speciális állományba foglaltakat.

7.3.1. Hello modul világ Kezdésként nézzük meg, hogyan alkothatjuk meg a legegyszerűbb kernelmodult. Ezt tekinthetjük majd vázként is a későbbi munkánkhoz. Ez a kód a kernel 2.6.x-es verziója alatt fordul és működik: /* hellomodule.c

-

Egyszeru kernel modul. */

#include #include int ni t_module(void) {

printk("Hello modul vilag! \n"); return 0;

void cl eanup_module(void) printk("viszlat modul vilag! \n");

MODULE_LICENSE("GPL");

A modul a lehető legegyszerűbb. Minden modul minimum két függvénnyel rendelkezik. Az egyik a modul betöltésekor (init_module()), a másik az eltávolításkor (cleanup_module()) hajtódik végre. Ezek formája a 2.6-os kernel esetén a példában látható. A gyakorlatban azonban nem a függvények beépített neveit használjuk, hanem a saját nevekkel létrehozott függvényeket regisztráljuk be, ez ugyanis flexibilisebb megoldás. Ezzel a kibővítéssel a következő kódig jutunk: /* hellomodule.c - Egyszeru kernel modul. */ #include #include #include

static int

ini t hello_init(void)

{

printk("Hello modul vi lag! \n"); return 0; }

344

7.3. Kernelmodulok

static void

exit hello_exit(void)

{

printk("viszlat modul vilag!\n");

module_init(hello_init); module_exit(hello_exit); mODuLE_DEsCRIPTIoN("Hello module"); MODULE_LICENSE("GPL");

A modul betöltésekor és eltávolításakor meghívandó függvényeket a module_init() és a module_exit() makrókkal jelöljük ki. Megfigyelhetünk még egy kis eltérést a függvények alakjában. Nevezetesen az _init és az _exit makrókat. Ezeket a makrókat arra használjuk, hogy megjelöljük a kernel számára az inicializációs és az eltávolító függvényeket. A kernel a makrók által nyújtott információt felhasználhatja a memóriafelhasználás optimalizálására. Jelenleg ezek az optimalizációs algoritmusok csak akkor lépnek működésbe, ha a kernelmodult belefordítjuk a kernelbe. Ha tehát a kernelbe építettük a modult, az init makró hatására az inicializációs folyamat után az init függvény megszűnik, és a hozzátartozó memória felszabadul. Az _exit makró hatására a tisztogatófüggvényt figyelmen kívül hagyja a rendszer a fordítás során, hiszen a kernel kitöltődésekor nincs szükség felszabadításra. Ha a modult nem építjük a kernelbe, ezek a függvények normál inicializáló és tisztogatófüggvényekké válnak, és a makróknak init és az _exit makrók használata nincs különösebb hatásuk. Vagyis az azért célszerű, hogy egy kódban mindkét esetet kezelhessük. A példaprogramban a regisztrált függvényeink kiírásokat végeznek. Az üzenetek kiírására a printk() függvényt használjuk, amely hasonlít a printf() függvényhez, de a Linux-kernelben van definiálva. A modul betöltésekor a Linux-kernelhez linkelődik, így a printk() függvény is használhatóvá válik. (A printf() például nem használható, hiszen a felhasználói könyvtárban van definiálva.) A modul végén makrókkal megadhatjuk a modul készítőjét, leírását és a licencinformációit. Jóllehet ezeket az adatokat nem kötelező megadni, legalább a licencet célszerű beállítanunk Ha nem GPL licencet állítunk be, akkor a modul betöltésekor figyelmeztetést kapunk, illetve egyes, a kernelben lévő függvények elérhetetlenek lesznek a modulunk számára.

_

_

345

7. fejezet: Fejlesztés a Linux-kernelben

7.3.2. Fordítás Miután elkészültünk a kernelmodulunk forráskódjával, a következő lépés a fordítás. A fordításhoz mindenképpen szükség van a használt kernel forrására. Ez nemcsak azt jelenti, hogy le kell töltenünk egy kernelforrást, amely ugyanolyan verziójú, mint amit éppen használunk, hanem hogy ha vannak módosítások (patch) a kernelünkben, akkor azoknak a forrásban is meg kell lenniük, továbbá a konfigurációnak is egyeznie kell. Szerencsére ez többnyire nem jelent komoly problémát, hiszen a disztribúciók többsége minden kernelverzióhoz tartalmazza a forráscsomagot is (tipikusan: linux-source, linux-kernel-source) vagy pedig egy kernelfejlesztői csomagot (tipikusan: linux-devel, linux-kernel-devel, linux-headers). Utóbbi általában nem teljes forráscsomag, hanem csak azok a részei vannak benne, amelyek a modulok fordításához szükségesek. Ez természetesen lényegesen kisebb, ám számunkra teljes értékű. A modulunk fordításához felhasználjuk a kernelforrás Makefile-jait. Ezért a telepítés után pontosan tudnunk kell, hogy melyik könyvtárban helyezkedik el a fő Makefile. Ezt a korábban említett és feltelepített forráscsomagok állománylistájának a megtekintésével lehet kideríteni. A forrás gyökérkönyvtárára van szükségünk (tipikusan: /usr/src/ alatt egy könyvtár, amely a nevében a kernelverziót is tartalmazza). A fordítási metódus a következő: 1. A modul könyvtárában létre kell hoznunk egy Makefile nevű állományt. Ebben az állományban a következő sor beírásával jelezhetjük, hogy a hellomodulel.c állományt szeretnénk felvenni a fordítási listába (a tárgykódú állomány nevét kell megadni, vagyis a „.0" kiterjesztést: bel -1~91 e .9

olaj -rt1

2. Ezt követően meg kell hívnunk a make parancsot úgy, hogy a kernelforrás fó'könyvtárában lévő Makefile állományt használja. Ugyanakkor át kell adnunk a SUBDIRS paraméterrel a modulforrásunk könyvtárát. Ezt követően a „modules" célt is meg kell adni a fordításhoz, amellyel jelezzük, hogy kernelmodulokat szeretnénk fordítani. Ezzel a parancssor a következő lesz: make

-

C = MSG_LEN) return 0; // Maximum annyi bajtot szolgalunk ki, amilyen hosszu a szoveg. if((*ppos+count) > MSG_LEN) count = MSG_LEN *ppos; -

/* Atmasoljuk az adatot a kernel cimteruletrol, az olvaso fuggveny altal kapott bufferbe. */ if(copy_to_user(buf, msg+*ppos, count)) {

return

-

EFAULT;

*ppos+=count; /* Vísszaterunk a visszaadott adatmennyiseg hosszaval. */ return count;

/* A megnyitas muveletet lekezelo fuggveny. */ static int hello_open(struct inode *inode, struct file *pfile) t /* Noveljuk a hasznalati szamlalot. */ try_module_get(THIs_mODULE); printk(DEVICE_NAME " open.\n"); return 0;

359

7. fejezet: Fejlesztés a Linux-kernelben

/* A lezaras muveletet lekezelo fuggveny. */ statíc int hello_close(struct inode *inode, struct file *pfile)

{ printk(DEvICE_NAME " close.\n"); /* Csokkentjuk a hasznalati szamlalot. */ module_put(THIS_M0DuLE): return 0;

static struct file_operations hello_fops = owner: THIS_MODULE, read: hello_read, write: hello_write, open: hello_open, release: hello_close }: /* A kernel modul inicíalízalasat vegzo fuggveny. static int init hello_init(void) int res; struct devicec err; /* Regisztraljuk a karakter tipusu eszkozt. '/ res = register_chrdev(major_num, DEVICE_NAME, &hello_fops); i f(res GBUFFERSIZE)

count = GBUFFERSIZE; }

if(down_ínterruptible(&lock)) return ERESTARTSYS; if(copy_from_user(gbuffer, buffer, count)) -

{

gbufferlen = 0; up(&lock); return EFAULT; -

gbufferlen = count; up(&lock); return count; } /* A kernel modul inicializalasat vegzo fuggveny. */ static int init hello_init(void)

_

struct proc_dir_entry *hello_proc_ent; /* Dinamikusan hozzuk letre a proc allomany bejegyzest. */ hello_proc_ent = create_proc_entry("hello",

S_IFREG I S_IRUGO I

S_IWUGO, 0);

/* Beallitjuk a link szamlalot es a lekezelo fuggvenyt. if(!hello_proc_ent) { remove_proc_entry("hello",0); printk("Error: A /proc/hello nem letrehozhato.\n"); return -ENOMEM; }

hello_proc_ent->nlink = 1; hello_proc_ent->read_proc = procfile_read; hello_proc_ent->write_proc = procfile_write; hello_proc_ent - >mode = S_IFREG I S_IRUGO I S_IWUGO; hello_proc_ent - >uid = 0;

373

7. fejezet: Fejlesztés a Linux-kernelben

hello_proc_ent->gid = 0; hello_proc_ent->size = 256; printk("A /proc/hello letrehozva.\n"); return 0;

/* A kernel modul eltavolítasa elott a felszabaditasokat vegzi. exit hello_exit(void) static void

A

/

/* megsemmisitjuk a proc allomany bejegyzest. */ remove_proc_entry("hello",0); printk("A /proc/hello eltavolitva.\n");

module_init(hello_init); module_exit(hello_exít); MODULE_DESCRIPTION("Hell0 proc"); MODULE_LICENSE("GPL");

A példában egy olyan proc állományt hozunk létre, amely hasonlít a valós állományokra. Vagyis amit beleírunk, azt utána kiolvashatjuk belőle. Ugyanakkor tartalmaz egyszerűsítéseket is, így nem teljes értékű implementáció. A virtuális állomány párhuzamos olvasása és írása versenyhelyzetet eredményezhet, ezért a példában a globális tárolótömbhöz való hozzáférést szinkronizációs eszközzel kell szabályoznunk. A korábbi példákban nem volt szükség erre, mert a globális változónkat csak olvastuk, ezzel szemben ebben a példában olvassuk és írjuk is párhuzamosan. A példában ezt egy szemaforral tettük. (A szinkronizációs eszközöket és használati területüket egy későbbi fejezetben tárgyaljuk, ám a helyes implementációhoz szükség volt a használatára.) Ahhoz, hogy a folyamatok ténylegesen tudják írni az állományt, az írást kezelő függvény regisztrációja mellett arra is szükség van, hogy az írásjogot megadjuk a felhasználóknak Az állomány létrehozásánál láthatjuk, hogy a példában mindenkinek joga van írni a virtuális állományunkat. Felmerülhet a kérdés: hogyan lehetséges az, hogy írható az állományunk, pedig nem is a /proc/sys könyvtár alatt található. Az igaz, hogy általában az írható proc állományok az említett könyvtár alatt találhatók, de ez csak konvenció. Valójában a /proc könyvtár alatt tetszőleges állományt tehetünk írhatóvá.

374

7.7. A hibakeresés módszerei

7.7. A hiba keresés módszerei A hibakeresésre kernelfejlesztés esetében korlátozottabbak a lehetőségeink, mint egy alkalmazásnál. Mivel a kernelmodul a betöltés után a kernel szerves részét alkotja, ezért ilyenkor lényegében abban a programban kell hibát keresnünk, amely a működő rendszer központi eleme, és ugyanezen program alatt futnak a hibakeresésre használt eszközeink is. Ezért a feladat jóval bonyolultabb, mint felhasználói címtartományban. A legegyszerűbb és ugyanakkor leggyakrabban használt módszereket már megmutattuk. Mind a printk() függvény, mind a /proc állományrendszer alkalmas arra, hogy információt közöljünk a kernelmodul állapotáról, így használhatók a hibakereséséhez. Ezeken kívül léteznek kifinomultabb módszerek is, amelyekre ugyancsak mutatunk példát ebben a fejezetben.

7.7.1. A príntk() használata A legegyszerűbb és ezért az egyik leggyakrabban használt módszer a hibakeresésre, ha a lényeges pontokon kiírjuk az állapotokat. A kernel esetén ezt a printkQ függvénnyel tehetjük meg, majd a kernelnaplóból nézhetjük vissza.'" A kernelnaplóba írt üzenetek különböző fontosságúak lehetnek. A hibakereséshez általában minden szóba jöhető információt kiíratunk, ez pedig nagy mennyiségű naplóbejegyzést eredményez. Viszont valószínűleg kisebb a száma a fontos, konkrét hibát jelző üzeneteinknek. Azért, hogy ezeket a naplózásokat kézben tarthassuk, a Linux szintekbe szervezi a különböző fontosságú üzeneteket, ezt célszerű nekünk is alkalmaznunk a kiírásaink során. Az alábbi szinteket különböztetjük meg (7.3. táblázat): 7.3, táblázat. Kernel üzenetek szintjei

Címke

Érték

Leírás

KERN_DEBUG

A legkevésbé fontos üzenetek, amelyeket csak tesztelésnél használunk.

KERN_INFO

Információk a driver működéséről.

KERN_NOTICE

Nincsen gond, de azért jelzünk valamilyen említésre méltó szituációt.

KERN_WARNING

Problémás helyzet jelzése.

KERN_ERR

Hiba jelzése.

101

A kernelnaplót a dmesg paranccsal vagy a log állományokból nézhetjük vissza. Tipikus helye a /var/logimessages vagy /var/logisyslog állomány a syslog rendszer beállításától függően. 375

7. fejezet: Fejlesztés a Linux-kernelben

Címke

Érték

Leírás

KERN_CRIT

Kritikus hiba jelzése.

KERN_ALERT

Vészhelyzet, amely esetleg sürgős beavatkozást igényel.

KERN_EMERG

A legfontosabb üzenetek. Többnyire a rendszer meghalása előtt kapjuk.

naplózórendszer lehetővé teszi, hogy a későbbiekben a fontossági szint alapján szűrjünk. Vagyis nem szükséges a szoftveren módosítani, ha például az egyszerűbb hibakeresési üzeneteket ki akarjuk kapcsolni. A fontossági szintek használata a következő:

A

printk(KERN_DEBUG "uzenet \ n") ;

A szűrés beállítása az alábbi: echo 8 > /proc/sys/kernel/printk

A példában a „8"-as érték adja meg, hogy a KERN_DEBUG üzeneteket is látni szeretnénk. Ahogy ezt a számot csökkentjük, úgy csak az egyre fontosabb üzeneteket láthatjuk. A „0" érték azt jelenti, hogy csak a KERN_EMERG üzeneteket kapjuk meg. Előfordul, hogy szeretnénk egy üzenetben kiírni, hogy éppen merre járunk a kódban. Természetesen ezt megtehetjük úgy, hogy a szövegbe beleírjuk a pozíciót, ám létezik ennek automatizált módja is. Például a forráskódállomány nevének és sorszámának kiírása a következő: printk(KERN_DERuG "Kod: %s:% -i \n",

FILE ,

LINE )

Ha az éppen aktuális programkód pointerére vagyunk kíváncsiak, vagyis arra, hogy a kiírást végző gépi instrukció hol található a virtuális memóriában, a következőt kell tennünk: printk(KERN_DEBuG "Cim: Yop\n", ptr);

7.7.2. A /proc használata Az előző fejezetben láthattuk, hogyan valósíthatunk meg olyan állapotellenőrzést, amikor is a kernelmodul adja a futása során a jelzéseket. Néha azonban egy ilyen jellegű megoldás olyan mennyiségű állapotinformációval áraszt el bennünket, amelynek a kezelése kényelmetlen. Emellett a rendszer futását is erősen lelassíthatja egy ilyen jellegű kiírásáradat.

376

7.7. A hibakeresés módszerei

Ilyenkor a megoldás az lenne, ha bizonyos időpontokban, amikor éppen szükség van rájuk, megvizsgálhatnánk az állapotváltozók aktuális értékét, vagyis igény szerinti lekérdezést alkalmaznánk. Ezt teszi lehetővé a /proc állományrendszer a virtuális állományain keresztül. Ennek implementációját korábban láthattuk. Az újdonságot csak az jelenti, hogy a korábban megismert mechanizmust hibakeresésre is használhatjuk. A /proc állományrendszer mellett hasonló hatást érhetünk el az ioca0 rendszerhívás implementációjával is, amelyre most nem térünk ki. Az ioca0 rendszerhívást azonban csak eszközvezérló'knél alkalmazhatjuk, így általános modulok esetében nem járható út.

7.7.3. Kernelopciók A kernel fordítása előtt, a konfiguráláskor számos hibaérzékelési mechanizmust bekapcsolhatunk. Ezeknek az opcióknak a hatására a fordítás során plusz hibaérzékelési és kezelési algoritmusok fordulnak bele a kernelbe. Így az általunk írt kód hibáira könnyebben fény derülhet. A kernel hibaérzékelési algoritmusainak egy jó része azonban a plusz ellenőrzések miatt lassítja a kernel működését, így normál rendszerben nem használhatók, ezért alapesetben ki vannak kapcsolva. A hibaérzékelési/kezelési opciókat a kernelkonfiguráció során a „Kernel Hacking" ágban találhatjuk meg. Minden opcióhoz kapunk segítséget, amely leírja az adott beállítás által nyújtott szolgáltatást. A lehetséges hibakeresési opciók listája hosszú, és kernelverziónként változik, bővül, így nem vállalkozunk a lista részletes leírására és elemzésére. Az alábbiakban összegyűjtöttünk néhány fontosabb opciót. •

CONFIG_DEBUG_KERNEL: Bekapcsolja a hibakeresési opciókat.



CONFIG_MAGIC_SYSRQ: Bekapcsolja a „magic SysRq" mechanizmust. Ez segítséget nyújthat kritikus esetben a státusz megvizsgálására. (A „magic SysRq" mechanizmusról a 7.7.5. Magic SysRq alfejezetben lesz szó részletesebben.)



CONFIG_DEBUG_SLAB: További ellenőrzések bekapcsolása, amelyekkel memóriahasználati hibák deríthetó'k fel. Speciális byte-értékek beállításával érzékelhetővé teszi a memóriainicializálási és -túlírási hibákat.



CONFIG_DEBUG_SPINLOCK: Ciklikus zárolás használati hibák detektálása a feladata, például az inicializálás elmulasztása. (A ciklikus zárolási eszközt a szinkronizálás témakörében tárgyaljuk.)

377

7. fejezet: Fejlesztés a Linux-kernelben



CONFIG_DEBUG_SPINLOCK_SLEEP: A ciklikus zárolási eszköz leírásánál szó lesz arról, hogy a ciklikus zárolást nem szabad olyankor használni, amikor a kritikus szakaszban sleepet használó függvény van. Ezzel az opcióval kideríthetjük, ha mégis elkövettük ezt a hibát.



CONFIG_DEBUG_INFO: A hibakeresési információk belefordítása a kernelbe. Ezt később a gdb-vel használhatjuk.



CONFIG_DEBUG_STACKOVERFLOW: A stacktúlcsordulás érzékelése.



CONFIG_DEBUG_STACK_USAGE: A veremhasználat monitorozása, statisztikák.



CONFIG_DEBUG_PAGEALLOC: A felszabadított lapokat eltávolítja a kernel memóriaterületéből.



CONFIG_DEBUG_DRIVER: Az eszközvezérlők kódjaiban bekapcsolja a hibaüzenetek kiírását.

Bár nem a kernel hibakeresési szekciójában szerepelnek, ám a hibakeresés szempontjából hasznos opciók lehetnek az alábbiak is: •

CONFIG_KALLSYSMS: Ezen opció hatására a szimbólumok elérhetőek lesznek a /proc/kallsyms állományban.



CONFIG_IKCONFIG, CONFIG_IKCONFIG_PROC: Az aktuális kernel konfigurációja elérhetővé válik a proc állományrendszeren keresztül. Így ellenőrizhetjük a beállításait, illetve az adott beállításokhoz fordíthatjuk a modulokat.



CONFIG_PROFILING: Általában a rendszer finomhangolására szolgál, de segíthet a hibák felderítésében is.

7.7.4. Az Oops üzenet Ha a kernelben, kernelmodulokban súlyos hiba történik, akkor a kernel ezt egy „Oops" üzenettel jelzi. Természetesen a súlyos hiba eredményezheti a rendszer teljes lefagyását, ez pedig odáig fajulhat, hogy még „Oops" üzenetet sem kapunk, ez azonban manapság ritka. Ugyanakkor attól, hogy a rendszer működőképes marad, még sérülhet olyan mértékben, hogy a további működése kiszámíthatatlan lesz. Ezt az „Oops" üzenet jelzi is. Ilyenkor a hibaüzenet feldolgozása után célszerű a rendszert újraindítani. Ha ezt elmulasztanánk, akkor „furcsa" hibákat kaphatunk, amelyek lehetetlenné teszik a további hibakeresést. Ahhoz, hogy az „Oops" üzenet könnyen értelmezhető legyen, a kernelt a CONFIG_KALLSYMS és a CONFIG_DEBUG_INFO opciók bekapcsolásával kell fordítanunk. Szerencsére ezek az opciók többnyire a normál használatban lévő kernelekben is be vannak kapcsolva, így nem kell új kernelt fordítanunk.

378

7.7. A hibakeresés módszerei

Az alábbiakban egy „Oops" üzenetet láthatunk (néhány sor elhagyásával): BUG:

unable to handle kernel NULL pointer dereference at 00000000

EIP: 0060:[]

EFLAGS: 00210246 CPU: 1 EIP is at iret_exc+0x6aa/0x97e EAX: 00000000 EBX: 00000005 ECX: 00000005 EDX: 00000003 ESI: b8015000 EDI: 00000000 EBP: f33c5f68 ESP: f33c5f54 DS: 007b ES: 007b FS: 00d8 GS: 0033 SS: 0068 Process bash (pid: 8148, ti=f33c5000 task=f31e19a0 task.ti=f33c5000) Stack: 00000005 00000005 00000005 f24b5600 f90d3047 f33c5f74 f90d3054 00000005 f33c5f90 c0492f18 f33c5f9c b8015000 f24b5600 fffffff7 b8015000 f33c5fb0 c049300c f33c5f9c 00000000 00000000 00000000 00000001 00000005 f33c5000 Call Trace: [] ? hello_write+Ox0/0x29 [buggy_driver] [] ? hello_write+Oxd/0x29 [buggy_driver] [] ? vfs_write+0x84/0xdf [] ? sys_write+Ox3b/0x60 [] ? syscall_call+0x7/Oxb

A legelső sor megmutatja, hogy pontosan milyen hiba is történt. Az EIP értéke elárulja, hogy éppen melyik gépi utasításnál járt a program. Az értelmezés egyszerűsítése érdekében nem memóriacímet ad meg, hanem a C függvény nevét és az eltolást a függvény belépési pontjához képest. Esetünkben bár a hiba az adott ponton következett be, a ténylegesen hibás kód valójában nem ott van. Ebből is látható, hogy az EIP értéke nem feltétlenül a hiba helyére mutat. A „Call Trace" mező tartalmazza az egymásba ágyazódó függvényhívásokat. Esetünkben itt találjuk meg azt a sort, amely a kernel egy másik részén a hibát okozta. Ez a sor a hello_write függvényben található, és a függvény elejéhez képest a Oxd eltolás környékén van a problémás utasítás. Ezt onnan tudjuk, hogy nem a megszakításkezelő visszatérésénél találjuk a hibát, és nem is a függvény belépési pontjánál. Így a hivatkozási listában ez a következő elem, amely jogosan gyanús lehet számunkra. Mivel az eltolás gépi utasításban értendő, ezért még meg kell fejtenünk, hogy ez konkrétan hol található a C forrásban.

7.7.4.11. Az Aops" üzenet értelmezése kernel esetében Ha a hibás kódrészlet a kernelben található, és nem külön kernelmodulban, akkor az elemzéshez szükségünk lesz a vmlinux állományra, amely a kernel fordításakor keletkezik. Ez az állomány a kernel tömörítetlen, helyfüggő programkódja. Így az „Oops" üzenet által megadott címeken található utasítások visszakereshetők belőle. A hibakeresés a gdb program segítségével történik:

379

7. fejezet: Fejlesztés a Linux-kernelben

Ezt követően megkereshetjük a memóriacímhez tartozó sort: WftelidbY De megkereshetjük a függvény neve alapján is: (gdb) info line fuggveny

7.7.4.2. Az „Oops" üzenet értelmezése kernelmodul esetében Ha a probléma egy kernelmodulban található, akkor a feladatunk nehezebb. Az előző esetben használt umlinux állomány ekkor természetesen nem tartalmazza a kérdéses részeket. A problémás részek a modul állományában találhatók. Ugyanakkor az „Oops" üzenetben lévő memóriacímek pedig attól függnek, hogy a kernelmodult az adott helyzetben hova töltötte be a kernel, amely természetesen változhat. Vagyis van egy címünk, de nem tudhatjuk, hogy a binárisban ez melyik helyet jelenti. Így hivatalos módszer nincs is jelenleg erre a helyzetre. Szerencsére azért némi ügyeskedéssel kideríthetjük a hiányzó információt. Nyissuk meg a kernelmodult:

Mivel az „Oops" üzenetben már a kernel visszafejtette az abszolút címeket C függvénynevekre és eltolásokra, ezért kérdezzük le, hogy a kernelmodulban most hol található az a függvény, amelyhez képest az eltolást nézzük: , 440.. 10:1 Uniz htlons" .

A kapott pozícióhoz adjuk hozzá az eltolást, és kérdezzük le az így kapott címet, amely a hibás sor:

Ez a parancs megad egy sorszámot a C-forrásban, amely a példában a 17. Nézzük meg, mi található ott:

Ezzel eljutottunk a hibás sorig, és már csak rá kell jönniink, miért adódott a hiba. Ha esetleg szeretnénk az egész függvényt assemblyben látni és ellenőrizni, hogy mi található az adott eltolási ponton, akkor az alábbi paranccsal fejthetjük vissza a kódot:

380

7.7. A hibakeresés módszerei

7.7.5. Magic SysRq Az előző fejezetben láttuk, hogy időnként előfordulhat, hogy a rendszer „Oops" üzenet nélkül teljesen lefagy. Ez értelemszerűen nagyban hátráltatja a munkánkat, mert nincs semmi információnk a hibáról, legfeljebb annyi, amit addig a kernel a konzolra kiírt. Szerencsére azért ilyenkor sem vagyunk teljesen elveszve. A Magic SysRq mechanizmus révén speciális gombkombinációk lenyomásával egyszerű parancsokat hajthatunk végre. Ezekkel megvizsgálhatjuk a rendszer aktuális állapotát. Természetesen ez nagyon alacsony szintű vizsgálatot jelent, így mélyrehatóbb ismereteket igényel az így rendelkezésre álló információ felhasználása. PC esetén a speciális gombkombináció: ALT + SysRq és a parancs. Néhány parancsot példaként bemutatunk, a teljes listát a kernel forrásában a Documentation/sysrq.txt állományban találhatjuk meg. 7.4. táblázat. Magic SysRq parancsok

Parancs

Leírás

b

Azonnal újraindítja a rendszert az állományrendszerek szinkronizálása és lecsatolása nélkül.

c

Rendszerösszeomlást okoz a napló (crashdump) generálása érdekében.

e

SIGTERM szignált küld minden folyamatnak az ínit kivételével.

g

Elindítja a kgdb t, ezáltal lehetővé téve a távoli hibakeresést.

h

Megjeleníti a segítséget.

-

SIGKILL szignált küld minden folyamatnak az ínit kivételével. 1

Megmutatja az aktív CPU-stack backtrace információit.

m

Az aktuális memóriainformációkat kiírja a konzolra. Az aktuális regiszterértékeket kiírja a konzolra.

s

Az állományrendszereket szinkronizálja, kiírja a buffer tartalmát.

t

Az aktuális taszkok listáját és információit kiírja a konzolra.

u

Csak olvasható módban újracsatolja az állományrendszereket.

0— 9

Beállítja a konzol naplózási szintjét. (Lásd a 7.7.1. A printkQ használata alfejezetben.)

A mechanizmust a kernelben engedélyezni vagy tiltani a /proc/sys/kernel/ sysrq virtuális állomány írásával lehet. Ha gondjaink támadnának a SysRq gombkombináció használatával, akkor a parancskaraktert a /proc/sysrq trigger virtuális állományba írva ugyanazt a hatást érhetjük el: -

e cho

t > /proc/s y s rq-t ri gg e r

381

7. fejezet: Fejlesztés a Linux-kernelben

7.7.6. A gdb program használata A gdb program használata a kernel esetében jóval korlátozottabb, mint ahogy az alkalmazásoknál megismertük. Figyelembe kell vennünk, hogy ebben az esetben egy olyan „alkalmazásban" keressük a hibát, amelyben éppen fut a hibakereső alkalmazás is. Ezért nincs lehetőségünk a folyamat megállítására, léptetésére, mert az hatással lenne a hibakereső alkalmazásra is. Ezért lényegében csak egy pillanatnyi helyzetet értékelhetünk ki A mechanizmus a gdb program core állománykezelését használja. Ehhez a kernel generál egy virtuális core állományt /proc/kcore néven. Igy a parancssor a következőképpen néz ki: gdb vmlinux /proc/kcore

Ezt követően a gdb print utasításának segítségével megnézhetjük a változók aktuális tartalmát. Ám vegyük figyelembe, hogy az első megtekintés után a gdb tárolja az értéket a gyorsítás érdekében. Vagyis ugyanannak a változónak egy későbbi lekérdezésében már nem bízhatunk meg maradéktalanul. A modulok debugolása nem megoldott ebben a mechanizmusban.

7.7.7. A kgdb használata Lehetőség van arra is, hogy egy másik gépről soros porton keresztül debugoljuk a kernelt. Az előző megoldáshoz képest így nagyobb lehet a mozgásterünk, mivel interaktívan debugolhatunk. A módszer használatához szükség van egy kernelkiegészítésre (patch), amelyet a régebbi kernelverziók esetén rá kell húznunk a kernelforrásra, az újabb verziók esetén azonban már tartalmazza a forrás. A kgdb elérhetősége a következő: http://kgdb.sourceforge.net . A távoli debugoláshoz, meg kell adnunk a kommunikációra használt soros vonalat és annak beállításait. Ha belefordítottuk a kernelbe a kgdb-t, akkor a kernel betöltésekor, ha modulként fordítottuk, a modul betöltésekor meg kell adnunk az alábbi paramétert: kgdboc=,[baud]

Például: kgdboc=/dev/tty50,115200

Ha ezt elmulasztottuk volna, akkor utólagos beállításra is van lehetőség a sysfs állományrendszeren keresztül: echo tty5O > /sys/module/kgdboc/parameters/kgdboc

382

7.7. A hibakeresés módszerei

Ezt követően a debugüzemmódot többféleképpen is indíthatjuk: •

A kernel fordításakor beállíthatjuk, hogy automatikusan induljon el.



A kernel indításakor egy paraméter segítségével is elindíthatjuk.



Menet közben az SysRq +g billentyűkombinációval indíthatjuk.

Az elindítást követően egy másik gépről soros porton keresztül a gdb programmal debugolhatunk. Ehhez természetesen szükségünk van a módosított kernel vmlinux állományára is a távoli gépen. A gdb elindítása a hagyományos módon történik:

4db :2ortInioc Ezt követően meg kell adnunk a soros kommunikáció sebességét, illetve hogy melyik porton keresztül csatlakozzon a másik gépre: set remotebaud 115200 target remote /devitty50

_ 411111111ffla

Amikor a gdb csatlakozik a másik géphez, akkor hasonlóan az alkalmazások hibakereséséhez megvizsgálhatjuk a tesztelt rendszer belső állapotát, vagy elhelyezhetünk töréspontokat a kernelben, és folytathatjuk a futtatást a töréspontig. A kernelben történő hibakereséshez jól használható módszer, a modulokat azonban ezzel a módszerrel is nehézkes nyomkövetni, bár nem lehetetlen. Ha a soros port nyújtotta sebességet zavaróan lassúnak találnánk, akkor vannak hálózaton működő megvalósítások is (kgdb over ethernet), ám ezek alapértelmezésben nem részei a kernelnek.

7.7.8. További hibakeresési módszerek Az eddigi felsorolással nem zárult le teljesen az eszköztárunk. A User Mode Linux (UML, felhasználói módban futó Linux) virtuális Linux rendszereket képes futtatni felhasználói folyamatként egy valódi Linux rendszerben. Ezáltal az UML lehetőséget nyújt arra, hogy a virtuális rendszerben teszteljünk anélkül, hogy a valós rendszert veszélyeztetnénk. Ám ennél többre is képes: segítségével a Linux-kernelt mint egy felhasználói folyamatot debugolhatjuk. Így a hibakeresés olyan lesz, mint a felhasználói alkalmazások esetében. Az UML hátránya az, hogy beágyazott rendszerek fejlesztésekor csak akkor használható, ha a célgép architektúrája megegyezik a fejlesztői géppel, illetve a perifériák is ugyanúgy elérhetők. Általában ez nehezen teljesíthető követelmény, de ilyenkor jól használható helyette a kgdb. Az UML másik hátránya az aránylag munkaigényes rendszer-összeállítás.

383

7. fejezet: Fejlesztés a Linux-kernelben

További kernel-üzemmódbeli hibakeresési eszköz lehet még az OHCI1394 Firewire port használata, amelyen keresztül direkt hozzáférés nyerhető a memóriához, így nagyon alacsony szintű vizsgálatokat végezhetünk egy másik gép segítségével.

7.8. Memóriakezelés a kernelben 7.8.1. Címtípusok A kernelprogramozás során több címtípussal is találkozhatunk. Ezért célszerű megvizsgálnunk, milyen címtípusokat különböztetünk meg. Ezek közül nem mindegyiket használjuk a kernelben, mivel a fizikai cím közvetlen használata összeférhetetlen a virtuális címzés elveivel, de a többivel találkozhatunk munkánk során. •

Fizikai cím: A processzor a memóriához való hozzáféréshez használja.



Kernel-logikaicím: A kernel által használt címtartomány a normál allokálásoknál Fizikai címként kezeljük, viszont a legtöbb architektúrán valójában a fizikai címtől egy eltolással tér el.



Kernel-virtuáliscím: Valójában a kernel-logikaicím is egy virtuális cím: lineáris leképezés fizikai címre. A vmalloc kal allokált memóriánál viszont a címtartomány folytonossága fontosabb. Ezért itt valódi virtuális címeket használunk, amelyeknél nem garantált a lineáris leképezés. -



Felhasználói virtuális cím: A felhasználói processzek saját virtuális címtartománnyal rendelkeznek. A felhasználói virtuális cím ebben a címtartományban egy hivatkozás.



Buszcím: Az architektúrák általában a perifériák fizikai címeit használják az adatátvitelre. Ezért a buszcím fizikai cím.

7.8.2. Memóriaallokáció A kernelmodulokban a memóriaallokációt leggyakrabban a kmalloc0 függvénnyel végezzük. Ennek általános alakja a következő: void* ktalloc(size-t sizo,

384

flAgs);

7.8. Memóriakezelés a kernelben

A flags mező értéke a leggyakrabban GFP KERNEL, ám használhatunk további értékeket is:



GFP_ATOMIC: Normál esetben a kmalloe0 meghívhatja a sleep függvényt. Ezzel a paraméterrel ez nem történhet meg, a memóriafoglalás atomi műveletté válhat, ezért a megszakításkezelő függvényekben is használható.



GFP_KERNEL: A szokásos allokációs metódus. Az allokáció során a kernel használhat sleep függvényt.



GFP_USER: A felhasználói címtartománybeli lapok allokálásánál használatos alapértelmezett beállítás. Ettől a foglalás még nem a felhasználói címtartományban történik, mindössze ugyanazokkal a beállításokkal megy végbe az allokáció.



GFP_NOIO: Ugyanaz, mint a GFP_KERNEL, csak közben nem hajtódnak végre az 1/0 műveletek.



GFP_NOFS: Mint a GFP_KERNEL, csak közben nem hajtódnak végre az állományrendszer-műveletek.

A felszabadítást minden esetben a kfree0 függvény végzi:

Igfree(voli *afidit'l;



ffl~á

A kmalloc0 által allokált terület a kernel logikai címtartományába esik (kernelbeli logikai cím), vagyis lényegében fizikai címeket használunk egy eltolással. A fizikai cím használata megnehezíti a lapokon átnyúló allokációt, mert megfelelő méretű összefüggő területet kell találni hozzá a memóriában. Ezért az allokáció egyszerűsítésére a kernel induláskor a memóriát több különböző méretű részre osztja. Ezek egy részét konkrét, a kernelben gyakran használt leíróstruktúrák tárolására tartja fent, így ezek allokációja gyors lesz. A másik részét általános allokációk részére, különböző méretű blokkokra osztva allokálja. Ezt az allokációs stratégiát hívjuk slab allokációnak. A Linux-kernel több konkrét algoritmust is tartalmaz erre a feladatra, amelyeknek a működése eltérő." Mivel a slab allokáció algoritmusa sokféle lehet, a konkrét jellemzők is eltérnek, használatuk azonban egységes. A kmalloc0 az általános használatú blokkokat alkalmazza, azokból azt a legkisebb szabad blokkot osztja ki az allokációs kérésre, amelybe még belefér az igényelt terület. Vagyis a krnalloe0 lényegében nem is allokál, csak a szabad blokkok közül osztogat. Ebből is láthatjuk, hogy az általa kiosztott terület véges, ezért óvatosan kell bánnunk vele, és kerülnünk kell a memóriaszivárgást.

102

Az eredeti SLAB algoritmus helyett manapság a SLUB használata az elterjedt. Emellett még a SLOB (Simple List Of Blocks) algoritmus is választható, amely a kicsi, beágyazott rendszerek számára előnyös. 385

7. fejezet: Fejlesztés a Linux-kernelben

A kmalloc0 használata során az alábbi korlátokra figyeljünk: •

A blokkok mérete előre rögzített: tipikusan a 2 hatványai; az egyre nagyobb blokkokból pedig egyre kevesebb van. A minimum és a maximum méret általában 8 byte és 8 kilobyte.



Ha nagyobb memóriaterületre van szükség, akkor a vmallocQ-kal allokálhatunk a virtuális memóriából.



A másik lehetőség nagyméretű allokációk esetén az, ha teljes lapot vagy több lapból álló összefüggő részt kérünk.

Ez utóbbi két lehetőségre mutatunk néhány függvényt. Mint az előző felsorolásban is szerepelt, ha nagy egybefüggő területre van szükségünk, akkor a vmalloc() függvénnyel allokálhatunk a virtuális címtartományból: void* vmal 1 oc(unsi gned long si ze);

Ilyenkor a felszabadítás a ufree() függvénnyel történik: vold vfrea(void* addr);

A vmalloc0-nál nagyobb teljesítményt érhetünk el, ha egy lapot vagy több lapból álló folytonos területet allokálunk az alábbi függvényekkel: unsigned long get_zeroed_page(gfp_t gfp_mask); unsigned long __get_free_page(gfp_t gfp_mask); unsigned long _get_free_pages(gfp_t gfp_mask, unsigned int order);

Az első függvény egy nullákkal teleírt lapot ad vissza. A második hasonló az elsőhöz, de nem inicializálja a területet. A harmadik függvénnyel kaphatunk vissza több lapból álló területet. Mivel azonban a lapok számának 2 egész számú hatványának kell lennie, ezért a visszaadott lapok száma a következőképpen adódik: lapszám = 2^order. A lap(ok) felszabadítását az alábbi függvényekkel végezzük: void free_page(unsigned long addr); void free_pages(unsigned long addr, unsigned int order);

7.9. A párhuzamosság kezelése A kernelfejlesztés során is találkozhatunk olyan helyzetekkel, amikor a párhuzamos végrehajtás miatt szinkronizációra van szükség. Ilyen esetek a következők:

386

7.9. A párhuzamosság kezelése



Ha megszakításkezelőben használunk olyan változókat, amelyek máshol is hozzáférhetők a rendszerben.



Ha a kódunk sleep() rendszerhívást tartalmaz. Ez tulajdonképpen azt jelenti, hogy a függvényünk futását megállítjuk az adott ponton, és más folyamatokra vált át a kernel. (Van több olyan más rendszerhívás is, amely használja a sleep() függvényt. Láttuk, hogy a kmalloc() is ilyen, ha nem a GFP ATOMIC kapcsolót használjuk, de a copy_from_user() is idetartozik.)



Ha kernelszálakat használunk.



Ha többprocesszoros rendszert használunk. (Ide tartoznak a többmagos processzorok és a többszálú processzorok is.)

A párhuzamosság problémáinak kezeléséhez olyan szinkronizációs eszközökre van szükségünk, amelyek a kölcsönös kizárást valósítanak meg. A Linux-kernelben több ilyen eszközt is találunk, ezeket tipikus alkalmazási területükkel együtt a következő fejezetekben mutatjuk be.

7.9.1. Atomi műveletek A legegyszerűbb szinkronizáció, ha nincs szükségünk szinkronizációs eszközre. Ezt úgy érhetjük el, ha a műveletünk atomi, vagyis gépikód-szinten egy utasításban valósul meg. Így nincs olyan veszély, hogy a művelet végrehajtása közben egy másik processzoron futó másik kód hatással lesz a műveletünkre, mivel az egy lépésben valósul meg. Annak eldöntéséhez, hogy a művelet atomi-e, gépikód-szinten kell megvizsgálnunk. Ebből adódóan platformfüggő és C-fordító-függő, hogy egy művelet tényleg atomi lesz-e. A kernel platformfüggő, assembly részeiben gondoskodhatunk arról, hogy teljesüljön a műveleteinkre ez a feltétel. Ám a platformfüggetlen kernelrészek számára is lehetővé kellett tenni, hogy garantáltan atomi műveleteket használhassanak. Ezt a kernel fejlesztői egy atomi típus és a rajta értelmezett függvények definiálásával érték el. Amikor a kernelt egy új platformra ültetik át a fejlesztők, akkor ezeket a függvényeket az adott architektúrának megfelelően kell megvalósítaniuk úgy, hogy a végeredmény valóban atomi legyen, vagy megfelelően szinkronizált. Előfordulhat, hogy egy adott platformon az implementáció a hagyományos műveleteknél lassabb. Ezért az atomi műveleteket csak ott használjuk, ahol tényleg szükség van rájuk. Az atomi műveletek egyszerűek, különben nem lehetne egy gépi utasításban elvégezni őket. 103 Ezért az atomi függvények halmaza csak egész számokon értelmezett érték növelő/csökkentő/tesztelő és bitműveleteket tartalmaz. 103

Ez nem jelenti azt, hogy minden atomi művelet minden processzortípuson egy gépi utasításra fordul le. Ám a lista összeállítása során egy-egy processzor utasításkészletéból indultak ki a fejlesztők. 387

7. fejezet: Fejlesztés a Linux-kernelben

Az egész számot kezelő atomi függvények csak az atomic_t adattípuson értelmezettek, amely egy előjeles egész számot tartalmaz. Más, hagyományos típusokkal nem használhatók. Továbbá az atomi adattípuson nem használhatók a hagyományos operátorok sem. Az atomic_t típusú változót csak az ATOMIC_INITO makróval inicializálhatjuk. Például: ":*tiitte

4"4,atitbilö-';«~t~ilia~

Az alábbi atomi egészszám-műveleteket használhatjuk (7.5. táblázat): 7.5. táblázat. Atomi egészszám-műveletek Függvény

Leírás

atomic_read(atomic_t *u)

Az atomi érték kiolvasása.

atomic_set(atomic_t *v, int i)

Az atomi érték beállítása i-re.

void atomic_add(int i, atomic_t *v)

Hozzáadja i t v hez.

void atomic_sub(int i, atomic_t *u)

Kivonja i t

void atomic_inc(atomic_t *u)

Megnöveli 1 gyel v értékét.

-

-

-

-

void atomic_dec(atomic_t *v)

Csökkenti 1 gyel v értékét.

int atomic_add_return(int i, atomic_t *v)

Hozzáadja i t v hez és visszaadja az eredményt.

int atomic_sub_return(int i, atomic_t *v)

Kivonja i t v ből, és visszaadja az eredményt.

int atomic_inc_retu.rn(atomic_t *v)

Növeli a v értékét 1-gyel, és visszaadja.

int atomic_dec_return(atomic_t *v)

Csökkenti a v értékét 1-gyel, és viszszaadja.

int atomic_add_negative(int i, atomic_t *v);

Hozzáadja i-t v-hez. Ha az eredmény negatív, akkor igaz, egyébként hamis értékkel tér vissza.

int atomic_sub_and_test(int i, atomic_t *v);

Kivonja i-t v-ből. Ha az eredmény 0, akkor igaz, egyébként hamis értékkel tér vissza.

int atomic_inc_and_test(atomic_t *0)

Megnöveli 1-gyel v értékét. Ha az eredmény 0, akkor igaz, egyébként hamis értékkel tér vissza.

int atomic_dec_and_test(atomic_t *u)

Csökkenti 1-gyel v értékét. Ha az eredmény 0, akkor igaz, egyébként hamis értékkel tér vissza.

int atomic xchg(atomic_t *v, int i);

Beállítja v-t i-re, és v korábbi értékével tér vissza.

int atomic_cmpxchg(atomict *v, int u, int i);

Ha v értéke megegyezik u-val, akkor beállítja v-nek az i értékét. A visszatérési érték a régi értéke a v-nek.

388

-

-

-

-

-

7.9. A párhuzamosság kezelése

Függvény

Leírás

int atmnic_add_unless(atontic_t *u, int i, int u);

Ha v értéke nem egyenlő u-val, akkor i-t hozzáadja v-hez, és igaz értékkel tér vissza. Egyébként a visszatérési értéke hamis.

Az atomi műveletek másik csoportját a bitműveletek alkotják. A bitműveletek unsigned long típusú értékeket kezelnek, és egy memóriacímmel megadott memóriaterületen végzik el a műveletet annyi byte-ra értelmezve, amennyi az unsigned long típus mérete az adott architektúrán. A byte-sorrend (littleendian, big-endian) értelmezése szintén az architektúrától függ• annak a byte-sorrendjével egyezik meg. Ez sajnos azt jelenti, hogy a bitműveletek platformfüggők. A bitértékeket az alábbi függvényekkel állíthatjuk be: void set_bit(unsigned long nr, volatile unsigned long *addr) void clear_bit(unsigned long nr, volatile unsigned long *addr); void change_bit(unsigned long nr, volatile unsigned long *addr);

A függvények értelmezése sorrendben az nr bit beállítása, törlése, átállítása. Az értékbeállítást kombinálhatjuk ellenőrzéssel is: int test_and_set_bit(unsigned long nr, volatile unsigned long *addr); int test_and_clear_bit(unsigned long nr, volatile unsigned long *addr); int test_and_change_bit(unsigned long nr, _ _ _ volatile unsigned long *addr); _

A függvénynevek is utalnak arra, hogy először az adott bit értékének vizsgálata történik meg, és ezt követi az érték állítása. Ha a régi bitérték 1, akkor igaz a visszatérési érték, egyébként hamis.

7.9.2. Ciklikus zárolás (spinlock) A ciklikus zárolás (spinlock) egy olyan szinkronizációs eszköz, amely folyamatosan, egy CPU-t terhelő ciklusban kísérletezik a zárolás megszerzésével mindaddig, amíg meg nem szerezte. Ezért csak rövid kritikus szakaszok esetén használjuk, egyébként a várakozó szálak/folyamatok számottevő mértékében pazarolnánk a CPU-t. Ugyanakkor a rövid szakaszok esetében hatékonyabb, mint az összetettebb szemafor. További előnye az, hogy egyprocesszoros rendszernél üres szakasszal helyettesíti a rendszer, ugyanis ott nincs szükség ilyen jellegű szinkronizálásra.

389

7. fejezet: Fejlesztés a Linux-kernelben

Ugyanakkor figyelnünk kell arra, hogy a ciklikus zárolással védett kritikus szakasz ne tartalmazzon sleep0 et hívó függvényt. Ha ugyanis egy száll folyamat lefoglalja a zárolást, és sleep0 miatt egy olyan szál/folyamat kapja meg a vezérlést, amelyik szintén megpróbálja megszerezni a zárolást, akkor CPU-pazarlóan vár rá a végtelenségig. Szélsőséges esetben ez holtponthoz és így a rendszer lefagyásához vezet. Az előző példával analóg eset állhat elő, ha megszakításkezelő függvényben használunk ciklikus zárolást. Ezért megszakításkezelőben erősen ellenjavallt a ciklikus zárolás alkalmazása. Egy ciklikus zárolás létrehozása az alábbiak szerint történhet: -

_ _ Oxi Ezt követően inicializálnunk kell: s pi rt.:11;x1L3 ni t U1 pc ; Ám a létrehozást és az inicializálást egy lépésben egy makróval is elvégezhetjük, sőt érdemesebb ezt a megoldást választani: ~4É ~INF-3~1-0(.1W Ock) ;Lefoglalása a következő: ..01111.0tV411«ic.) ;

Felszabadítása pedig az alábbi: stvirt_mrtydkal:04c): Előfordulhat, hogy a kritikus szakasznál arról is gondoskodnunk kell, hogy párhuzamosan ne hívódhasson meg egy megszakításkezelő. A megszakításkezelés átmeneti letiltását és engedélyezését az alábbi függvényekkel tehetjük meg: unsi gned 1 ong fl ags ;

spi n_1 ock_i rqsave (8,1ock , flags); spi n_unl ock_i rgrestore(&lock,

flags);

1~1~ .

11111

-

Tapasztaltabb programozóknak gyanús lehet, hogy a spin_lockirqsave0 második paramétereként nem a flags változó mutatója szerepel. Ez nem nyomdahiba. A bemutatott függvények valójában makrók, ezért valójában nem érték szerinti átadást láthatunk, hanem a változót adjuk át.

390

7.9. A párhuzamosság kezelése

7.9.3. Szemafor (semaphore) A szemafor (senzaphore) a ciklikus zárolásnál összetettebb mechanizmus, ezért több erőforrást is igényel. Így a nagyon rövid szakaszok esetén inkább a ciklikus zárolást részesítjük előnyben. Ugyanakkor a komolyabb esetekben, illetve ha a ciklikus zárolás a megkötései miatt nem használható, akkor ez a helyes választás. Figyeljünk arra, hogy a semaphore a várakozáshoz sleepQ függvényt használ, így nem alkalmazhatjuk olyan helyen, ahol a sleep()-es függvények tiltottak.'" A semaphore létrehozása a következő:

Struct semaphore mm; Inicializálása eszerint történik:

rfrtid siminiltCs:tmct

va3.).;

A létrehozást és az inicializálást kombináló makró az alábbi:

static DerINLSOIMORk(sem); A val paraméter a szemafor kezdőértékét adja meg. A szemafor lefoglalásakor lehet, hogy várakozásra kényszerül a függvény. Ám attól függően, hogy ezt a várakozást mi szakíthatja meg, több függvény is választható a lefoglalásra. Egyszerű lefoglalás az, amikor csak a szemafor felszabadulása vet véget a várakozásnak:

00« .(5tr41:kt..se~tore Ha egy jelzés megszakíthatja a várakozást (ilyenkor -EINTR a visszatérési értéke): itt*.4S,WIUMW.1%ii‚tible(St -rilCt. $fflOritgire: ' 11'4004 Ha kritikus szignál szakíthatja csak meg:

ticiWn.*17141~.~tiotk

10

' Nem használhatunk sleep0-es függvényeket például a megszakításkezelő függvényekben (hardver- és szoftver-megszakításkezelők). Emellett ciklikus zárolás által védett szakaszokban sem. 391

7. fejezet: Fejlesztés a Linux-kernelben

Ha nem akarunk várakozni, hanem csak egy hibajelzést szeretnénk, amenynyiben nem lehet lefoglalni: 500110e *$.210; Ha időkorlátos várakozást szeretnénk:

int downiMeout(wuct semaphöre *sem,long jiffies); Az időkorlát mértékegysége jiffy. (Lásd a 2.3.9. Idő és időzítők alfejezetben.) A szemafor elengedése a következő: uraid up(stNct semaphöre *sem);

7.9.4. M utex A kernelfejlesztők a szemafort többnyire 1 kezdőértékkel mutexként kölcsönös kizárás megvalósítására használják olyan esetekben, ahol a ciklikus zárolás nem alkalmazható. Ha azonban nem használjuk ki teljesen a funkcióit, akkor lehetséges egy egyszerűbb megvalósítás is, amellyel valamelyest jobb lesz a rendszer teljesítménye. Ilyen megfontolás vezette a fejlesztőket, amikor bevezették a kernel mutexeszközét. A kernelfejlesztések során a mutex azokban az esetekben használható, ahol egyébként szemafort használnánk a kölcsönös kizárásra. A megkötések nagyrészt a szemafor megkötéseivel azonosak, ugyanakkor vannak új elemek is: •

Csak a lefoglaló szabadíthatja fel.



Rekurzív foglalás vagy többszörös felszabadítás nem engedélyezett.



Nem használható megszakítás kontextusában. (Vagyis sem hardver-, sem szoftvermegszakítást kezelő függvényben.)

A mutex típusa az alábbi: ,„%t

Inutglci

Inicializálása a következő:

inutex~ttmutW).; A létrehozás és az inicializálás a következő makróval történhet: WINP.iltilliTEXONt«) ;

392

7.9. A párhuzamosság kezelése

A mutex lefoglalása így néz ki: void mutex_lock(struct mutex *lock);

A mutex lefoglalása, ha a várakozást jelzéssel megszakíthatóvá szeretnénk tenni, a következő: 1

1~14

4.012

1,4

41jítitii;

A mutex lefoglalása, ha sikertelenség esetén nem várakozni akarunk, hanem hibavisszajelzést szeretnénk kapni: int mutex_trylock(struct mutex *lock);

11115~11—

A mutex felszabadítása az alábbi:

A mutex foglaltságának ellenőrzése pedig eszerint történik: -

int mutex_is_locked(struct mutex *lock);

7.9.5. Olvasó/író ciklikus zárolás (spinlock) és szemafor (semaphore) A kritikus szakasznál célszerű megkülönböztetnünk, hogy a szakaszon belül a védett változót írjuk vagy olvassuk. Mivel ha csak olvassuk az értéket, akkor azt párhuzamosan több szál is probléma nélkül megteheti (megosztott zárolás). Míg ha írjuk, akkor másik szálnak sem írni, sem olvasni nem szabad (kizáró zárolás). A két eltérő zárolás használatával növelhetjük a rendszer teljesítményét. Az olvasó/író ciklikus zárolás létrehozása és inicializálása a következő: rwlock_t rwlock; rwlock_i ni t (&rwlock)

; 1111~1~1~1~"

A létrehozást és az inicializálást kombináló makró: stati C DEF/NE_RWLOCK( rwl ock)

~~~ -

•=.

Olvasási (megosztott) zárolás: read_lock(&rwlock); read_unlock(&rwlock);

393

7. fejezet: Fejlesztés a Linux-kernelben

Írási (kizáró) zárolás: wri te_l ock (8irwl ock) ; wri te_unl ock(&rwl ock) ;

Az olvasó/író szemafor létrehozása: struct rw_semaphore rwsem;

Inicializálás: void ínit_rwsem(struct rw_semaphore *sem);

Lefoglalás olvasásra: void down_read(struct rw_semaphore *sem); ínt down_read_trylock(struct rw_semaphore *sem);

Felszabadítás olvasás esetén: void up_read(struct rw_semaphore *sem);

Lefoglalás írásra: void down_write(struct rw_semaphore *sem); int down_write_trylock(struct rw_semaphore *sem);

Felszabadítás írás esetén: void up_wr te(struct rw_semaphore *sem) ;

Előfordulhat, hogy egy olvasási zárolást felszabadítás nélkül írási zárolásra szeretnénk átalakítani, ám erre a Linux-kernel implementációjában nincs lehetőség: ilyen jellegű próbálkozás holtpontot okoz. Ugyanakkor az írási zárolást bármikor „visszaléptethetjük" olvasási zárolásra: void downgrade_wri te(struct rw_semaphore *sem) ;

7.9.6. A nagy kernelzárolás A nagy kernelzárolás (Bíg Kernel Lock) egy globális rekurzív ciklikus zárolás. Használata nem javasolt, mivel értelemszerűen jelentősen korlátozza a kernel működését, és rontja a rendszer valósidejűségét, ugyanis akadályozza a kernel párhuzamos működését.

394

7.10.1/0 műveletek blokkolása

Ez a zárolásfajta a 2.0-s verziótól volt jelent a kernelben, amikor a többprocesszoros rendszerek támogatása belekerült (symmetric multiprocessing, SMP). A lehetséges konkurenciaproblémákat úgy előzték meg a fejlesztők, hogy kernelüzemmódban az egész kernelt zárolták. Ezzel kivédték, hogy egy másik processzor is futtasson párhuzamosan kernelmódú kódot. A konkurenciaproblémák kezelésére széles körben alkalmazták, mert segítségével úgymond „biztosra lehetett menni", és nem kellett végiggondolni az adott konkurenciahelyzetek hatásait. Ugyanakkor ez egy átmenetinek szánt megoldás volt, amelynek a helyét később átgondoltabb, kifinomultabb megoldások vették át. Az idők folyamán egyre több helyről sikerült kiszorítani, és végül a 2.6.37-es verzióban sikerült végleg eltávolítani. Bár lehetőség van rá, hogy a kernel konfigurációja során visszakapcsoljuk, ez értelemszerűen nem javasolt. A teljesség kedvéért azonban a nagy kernelzárolás lefoglaló és felszabadító függvényeit is megemlítjük:

1 ock_ke rnel 0 ; unl ock_kernel () ;

_

7.10. I/O műveletek blokkolása Az eszközvezérlőknél gyakran felmerülő probléma, hogy várakoznunk kell a hardverre, hogy végre tudjunk hajtani egy műveletet. Ennek leggyakoribb formája, hogy a felhasználói alkalmazás már olvasná az adatokat, de még nem kaptunk meg mindent az eszköztől. Nem célszerű hibajelzéssel visszatérnünk, mert akkor ismételten meg kell hívnia az olvasást/írást végző függvényünket Azt az utasítást sem adhatjuk, hogy itt az állomány vége, mert nem igaz. Helyette blokkolnunk kellene a függvényt, és csak akkor kellene visszatérnie, amikor már van adatunk. Ez a megoldás már ismerős lehet az alkalmazásfejlesztésből, hiszen számos függvénynél tapasztalhattuk, hogy csak akkor térnek vissza, ha megérkezett az adat. Most megvizsgáljuk, hogyan tudjuk ezt a működést kerneloldalon megvalósítani. Természetesen nem célszerű a kódunkba egy várakozó ciklust tenni, mert ez a várakozás idejére teljesen lefoglalna egy CPU-t. Ha csak egyetlen CPU van a gépünkben, akkor ez egyenlő a rendszer lefagyásával. Helyette egy sleep jellegű várakozást kell választanunk, vagyis a várakozás időtartamára engedjük, hogy más folyamatok fussanak, míg a minket hívó folyamat alszik. Majd amikor az esemény bekövetkezik, akkor felébresztjük, és folytathatja a működését. Erre a kernel a következő hatékony megoldást biztosítja. A 2.3.2. A processz állapotai alfejezetben említettük a processz állapotait. Ha egy processz egy adott eseményre várakozik, a kernel várakozási állapotba állítja a

395

7. fejezet: Fejlesztés a Linux-kennelben

processzt, és elhelyezi egy kétszeresen láncolt listába. Ha egy jelzés „felébreszti" ezeket a folyamatokat, ezek ellenőrzik, hogy az esemény, amelyre várakoznak, bekövetkezett-e. Ha nem, akkor maradnak a várakozási sorban, ha igen, akkor eltávolítják magukat a várakozási sorból. A várakozási sor programozása egy ugyanilyen nevű, úgynevezett várakozásisor- (wait queue) változót igényel a Linuxban. Ennek létrehozása és inicializálása a következő: wait_queue_Wead_t wait_queue; init_waitqueue_head(&wait_queue);

~~~-

Ha statikusan hozzuk létre, vagyis nem egy függvény lokális változója, illetve dinamikusan allokált struktúra része, akkor a következő makróval fordítási időben is elvégezhetjük a létrehozást és inicializálást: DECLARE_WAIT_QUEUE_HEAD(wait_queUe);

7.10.1. Elaltatás Miután létrehoztuk a várakozásisor-változót, a segítségével elaltathatjuk a processzeket a következő függvények valamelyikével: sleep_on(wait_queue_head_t *queue);

1111111~~1111111~11-:

A processzt a sleep függvény segítségével eseményre várakozó állapotba küldi, és behelyezi a várakozási sorba. Hátránya ennek a függvénynek, hogy jelzésekkel nem szakítható meg, ezért a processz beleragadhat az adott műveletbe. interruptible_sleep_on(wait_queue_head_t *queue);

Működése megegyezik a sleep_on() függvényével, de jelzések megszakíthatják. Az eszközvezérlőkben az egyik leggyakrabban használt függvény a blokkolásra: sleep_on_timeout(wait_queue_head_t *queue, long timeout);

Ez a sleep_on() timeoutos változata. Vagyis a megadott időnél tovább nem várakozik. Az idő jiffyben van megadva. (Lásd a 2.3.9. Idő és időzítők alfejezetben.) interruptible_sleep_on_timeout(wait_queue_head_t *queue, _ _ _ long timeout); _

Ez az interruptible_sleep_on() timeoutos változata (lásd az előzőt):

396

7.10. I/O műveletek blokkolása

wait_event(wait_queue_head_t queue, int condition); wait_event_timeout(wait_queue_head_t queue, int condition, long timeout);

Ez a makró kombinálja a várakozást és a feltétel tesztelését. Mindezt úgy, hogy a kritikus versenyhelyzeteket kizárja. Használatakor a folyamat csak akkor ébred fel ténylegesen, ha a feltétel (condition) igaz értékű: wait_event_interruptible(wait_queue_head_t queue, int condition); wait_event_interruptíble_timeout(wait_queue_head_t queue, int condition, long timeout);

Ez a wait_euent() megszakítható változata. Ez a makró a javasolt módszer az eseményekre való várakozásnál, ugyanis tartalmazza a biztonságos feltétel kiértékelését és a megszakíthatóságot is: waiLeveot_,Ici 1101 e(wai t_queue_h ead_t queue,

int conditi on);

Ez a csak fontos szignálokkal megszakítható változat. A sleep_on használata manapság nem javasolható. Helyette a wait_eventet használják. Ennek az a fő oka, hogy a sleep_ont a fejlesztők többnyire egy while ciklusban alkalmazták. Ennél a megoldásnál azonban a wait_event jobb.

7.10.2. Felébresztés Míután elaltattuk a folyamatot, fel is kell ébresztenünk, amikor már megérkezett a várt adat. Ezt az eszközvezérlő egy másik részén szoktuk elvégezni, tipikusan egy megszakításkezelőben. Erre a következő függvények használhatók: wake_up(wait_queue_head_t *queue)

Ez a függvény felébreszti az összes olyan folyamatot, amely a várakozási listában szerepel. wake_up_interruptible(wait_queue_head_t *queue)

Ez a függvény azokat a folyamatokat ébreszti fel, amelyek interruptible sleep állapotban vannak. A többiek tovább alszanak. wake_up_sync(waít_queue_head_t *queue) wake_up_interruptible_sync(wait_queue_head_t *queue)

Az előző függvények a folyamatok felébresztése mellett azonnal egy ütemező hívást is eredményeztek, hogy a várakozó processzek ténylegesen tovább futhassanak. Ez a két függvény csak futásra kész állapotba teszi ó'ket, de újraütemezést nem vált ki. 397

7. fejezet: Fejlesztés a Linux-kernelben

Ha interruptible sleep állapotban vagyunk, akkor lényegében mindegy, hogy a wake_up0 vagy a wake_up_interruptible0 függvényt használjuk, a szokás azonban az utóbbi alkalmazása.

7.10.3. Példa Feladat Írjunk egy karakteres eszközvezérlőt, amelyben az olvasás addig blokkolódik, amíg nem irunk az eszközállományba. Mivel nincs feltétel, amelyre várakoznánk, ezért a sleep_on függvényt használhatjuk.

/* Blokkolas pelda 1 */ #include #include #include #ínclude #include #include





#define DEVICE_NAME "hello" static ínt major_num=120; #define CLASS_NAME "helloclass" static struct class* hello_class; wait_queue_head_t wait_queue; /* Az iras muveletet lekezelo rutin. */ static ssize_t hello_write(struct file *pfile, const char *buf, size_t count, loff_t *ppos) { printk("Ebresztes...\n"); wake_up_interruptible(&wait_queue); return count;

/* Az olvasas muveletet kezelo fuggveny. */ static ssize_t hello_read(struct file *pfile, char *buf, size_t count, loff_t *ppos) { if(pfile->f_flags & O_NONBLOCK) { return -EAGAIN;

printk("Alvas...\n"); interruptible_sleep_on(&wait_queue); printk("Alvas vege\n"); return 0;

398

7.10. I/O műveletek blokkolása

/* A megnyitas muveletet lekezelo fuggveny. */ static int hello_open(struct ínode *inode. struct file *pfile) try_module_get(THIS_mODuLE); printk(DEvICE_NAmE " megnyitas.\n"); return 0;

/* A lezaras muveletet kezelo fuggveny. */ static int hello_close(struct inode *inode, struct file *pfile) {

printk(DEVICE_NAME " lezaras.\n"); module_put(THIS_mODuLE); return 0; }

static struct file_operations hello_fops = {

owner: THIS_MODULE, read: hello_read, write: hello_write, open: hello_open, release: hello_close 1; /* A kernel modul inicializalasat vegzo fuggveny. */ static int init hello_init(void) int res; struct device* err; init_waitqueue_head(&wait_gueue); /* Regisztraljuk a karakter tipusu eszkozt. */ res = register_chrdev(major_num, DEVICE_NAME, &hello_tops); if(resvm_end - vma->vm_start; if(vsize != PAGE_SIZE) printk(DEVICE_NAME " hibas meret: %lu\n", vsize); EINVAL; return -

}

if (vma->vm_flags & VM_WRITE) return -EPERM; if (PAGE_SIZE > (1 « 16)) return -ENOSYS; if(remap_pfn_range(vma, vma->vm_start, virt_to_phys((void*)data) > PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -EAGAIN; } vma >vm_ops = &hello_vm_ops; hello_vm_open(vma); return 0; -

406

7.13.1/0 portok kezelése

A kezelőfüggvényünkben ellenőrizzük a paramétereket, a méretet és a jogosultságbeállításokat. Ezt követően elvégezzük a cím átalakítást és a leképezést egy sorban. Majd a megnyitó és a lezáró függvény regisztrációja következik, és egyben meg is hívjuk a megnyitó függvényt. A kész kezelőfüggvényünket be kell állítanunk az eszközvezérlő regisztrálásánál, hogy az mmap metódust implementálja, és máris használhatjuk ezt a gyors kommunikációs módot az alkalmazásainkban: struct file_operations hello_fops = {

owner:

THIS_MODULE,

mmap:

hello_mmap

1;

7.13. I/O portok kezelése Az eddigi fejezetekben bemutattuk, hogy hogyan tudunk az alkalmazások és a kernelmodulok között kommunikálni. Ám egy eszközvezérlőnél ez még csak a feladat egyik fele, hiszen az eszközvezérlőnek nem elég az alkalmazásokkal kommunikálnia, az általa kezelt hardverrel is tartania kell a kapcsolatot. Ezért a következő néhány fejezetben azt mutatjuk be, hogyan tudunk kernelből különféle eszközöket vezérelni. Az egyszerűbb hardvereszközökkel a kommunikációt leggyakrabban az I/O portok segítségével folytatja a CPU, mégpedig az in és az out parancsok segítségével. A Linux-kernelmodulokban is elérhetjük ezeket a parancsokat, illetve portokat. Ám a portok használata előtt illik a tartományt lefoglalni. Az I/O porttartomány lefoglalásának az a célja, hogy a kernel könyvelje, hogy melyik tartományt melyik eszközvezérlő használja. Így egy lefoglalási kérelem során jelezheti, ha az adott tartományt egy másik eszközvezérlő netán már használja. Természetesen az eszközvezérlő eltávolításakor a tartományt is fel kell szabadítanunk, hogy más eszközvezérlő is használhassa. Porttartomány lefoglalása az alábbi függyénnyel történik: struct resource* request_region(resource_size_t start, resource_size_t n, const char *name);

A start a tartomány eleje, az n a tartomány mérete. A name paraméterre azért van szükség, hogy ha megnézzük a lefoglalt tartományok listáját, akkor láthassuk, hogy melyik eszközvezérlő használja. Ezért ez a paraméter az eszközvezérlő szöveges elnevezését tartalmazza. A lefoglalhatóságot külön is ellenőrizhetjük: int check_region(resource_size_t start, resource_size_t n);

407

7. fejezet: Fejlesztés a Linux-kernelben

A használat után a terület felszabadítása a következő:

rvi7fase.re9icr«resource..~ 5tarti rpstkume..,size_t

ki);

A lefoglalt I/O portok listáját felhasználóként is elérhetjük a /proc/ioports virtuális állomány megtekintésével. A tartomány lefoglalása után a portokat különböző I/O műveletekkel érhetjük el. Az adat méretétől függően több függvény közül is választhatunk: •

8 bites

unsigned char inb(int port);

vojg1 outb(unsigned char value, int port); •

1 6 bites

unsigned short inw(int port) ; voí d outw(unsí gned short value, int port) ;



32 bites

unsigned long inb(int port); void outl (unsigned long value, int port) ;

Ugyanakkor mindegyik függvénynek létezik egy várakozó („pause") változata, amely egy kis várakozást is tartalmaz. Erre a lassabb buszok/kártyák használatakor lehet szükségünk. Ezeknek a függvényeknek az alakja megegyezik az előzőekben felsoroltakkal, ám a függvények nevének a végére egy _p utótagot kell illeszteni.

7.14. 1/0 memória kezelése Bár az egyszerű eszközöknél az 1/0 portok használata népszerű, a kicsit komolyabb eszközökkel az I/O memórián keresztül tartjuk a kapcsolatot, mivel sokkal gyorsabb és sokkal nagyobb mennyiségű adat átvitelére alkalmas. Az I/O memória olyan, mint egy szokásos RAM-terület. Ám nem egy memóriamodult kell keresnünk az alaplapon, hanem ez valójában az eszközökben, a bővító'kártyákon foglal helyet, és a processzor többnyire egy buszon (ISA, PCI) keresztül éri el. A teljes képhez hozzátartozik, hogy a processzorgyártók manapság gyakran integrálnak eszközöket az alaplapi vezérlő chipkészletbe vagy éppen a processzorba. Természetesen ekkor az I/O memória is integráltan található meg.

408

7.14. I/O memória kezelése

Bár elméletben egyszerű memóriamutatókkal is tudnánk kezelni ó'ket, mint egy memóriatömböt, ám ez a módszer nem javasolható. Helyette speciális függvényeink vannak erre a feladatra. Mielőtt hozzáférhetnénk az I/O memóriaterülethez le kell foglalnunk: struct resource* request_mem_region(resource_size_t start, resource_size_t n, const char *name);

A start paraméterrel adhatjuk meg a régió elejének fizikai címét, az n-nel a méretét, míg a name az eszközkezelőnk szöveges elnevezése a késó'bbi adminisztrációhoz. Mielőtt lefoglalnánk a területet, ellenőrizhetjük, hogy más már használja-e: int Ch«k_metn_region(resource_size_t start, resource_size_t n);

A lefoglalt területet nem szabad elfelejtenünk felszabadítani, amikor már nincs rá szükségünk:

Ilk~~~fopin~ ~

~+._

A foglalásokat felhasználóként is ellenőrizhetjük a /proc/iomem állomány megnézésével. Ebben tételesen szerepelnek a használt memóriaterületek és az eszközvezérlők megnevezései. Sok rendszeren az I/O memória lefoglalása önmagában nem elég a memóriaterület eléréséhez, ugyanis nem lehet őket közvetlenül elérni. Ezeken a rendszereken a buszmemóriából a kernel által elérhető virtuális memóriaterületre kell leképezni a régiót. A leképezés az alábbi függvényekkel történhet: void __iomem *ioremap(resource_size_t offset, unsigned long size);

void _iomem *ioremap_nocache(resource_size_t offset,

t

unsigned long size);

A művelet végén a leképezés megszüntetéséről se feledkezzünk meg:

01111~~1~~1~1ffik

Ezen a ponton már hozzáférhetünk az eszköz memóriaterületéhez, és adatokat mozgathatunk. Némelyik rendszer esetében közvetlenül memóriakezelő függvényekkel is megtehetjük ezt, ám általában csak az alábbi függvények működnek: unsigned ínt íoread8(void __iomem *); unsigned int ioreadl6(void _iomem *); unsigned int ioread32(void _iomem *); void iowrite8(u8, voíd __iomem *); void iowritel6(u16, void iomem *); void iowrite32(u32, void _iomem *);

1111111W 409

7. fejezet: Fejlesztés a Linux- kernelben

A függvények 1, 2, vagy 4 byte-ot olvasnak be vagy írnak ki a megadott címre. Léteznek azonban ismétlődő („repetitive") megoldások is a tömbök kezelésére: void ioread8_rep(void _iomem *port, void *buf, unsigned long count); void ioreadl6_rep(void iomem *port, void *buf, unsigned long count); voíd ioread32_rep(void _iomem *port, void *buf, unsigned long count); void iowrite8_rep(void iomem *port, const void *buf, unsigned long count); void iowritel6_rep(voíd iomem *port, const void *buf, unsigned long count); void iowrite32_rep(void iomem *port, const void *buf, unsigned long count);

Továbbá a megszokott C-függvényeknek is megtalálhatjuk az analógiáit az I/O memória kezelésére:

_

void memset_io(volatile void i omem *addr, unsigned char val, int count); void memcpy_fromio(void *dst, const volatile void i omem *src, int count); void memcpy_toio(volatile void iomem *dst, const void *src, int count);

7.15. Megszakításkezelés Ha az eszközvezérlőnkben megszakítást szeretnénk kezelni, akkor az alábbi műveleteket kell elvégeznünk: 1. Létre kell hoznunk egy megszakításkezelő függvényt. 2. Regisztrálnunk kell a megszakításkezelő függvényt az adott megszakításhoz. 3. A meghajtóprogram eltávolításakor a megszakításkezelő regisztrációját is el kell távolítanunk. A megszakításkezelő függvénymutató típusa a következő: typedef rq retu rn_t (*i rq_handl er_t) (int irq,

voi d *devid);

A függvény paraméterei a megszakítás száma (irq), amelynek hatására a függvény meghívódik, illetve egy azonosító (devid), amelyet a kezelőfüggvény regisztrációjánál adunk meg.

410

7.15. Megszakításkezelés

A megszakításkezelőnket az alábbi függvénnyel regisztrálhatjuk: int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void *devid); 4■11111~.

=Mb

Paraméterei a megszakítás száma (irq), a megszakításkezelő függvény (handler), az opciók (flags), az eszközvezérlő neve (devname), megosztott megszakítás esetén az egyedi azonosító (devid). A flags mező értékei az alábbiak lehetnek:



IRQF_DISABLED (SA_INTERRUPT): A megszakításkezelő futása alatt a megszakítások letiltódnak. Ezért lehetőleg rövidre kell megírnunk a megszakításkezelőt.



IRQF_SHARED (SA_SHIRQ): Annak jelzése, hogy a megszakítást megosztjuk más megszakításkezelőkkel. Ilyenkor általában több hardver használja ugyanazt a megszakításvonalat.



IRQF_SAMPLE_RANDOM (SA_SAMPLE_RANDOM): A megszakítás felhasználható a véletlenszám-generálásához. Ezt olyan megszakításoknál alkalmazhatjuk, amely véletlenszerűen generálódik. Ilyen például a billentyűzet megszakítása.



IRQF_TIMER: A megszakítás az időzítőtól (timer) érkezik.

A megszakításkezelő függvény implementációjánál az alábbi értékekkel térhetünk vissza: •

IRQ_HANDLED: A megszakítást kezelte a függvény.



IRQ_NONE: A megszakítást nem kezelte a függvény.

A sikeres regisztrációt a /proc/interrupts virtuális fájlban ellenőrizhetjük, amely tartalmazza a megszakításokat és a megszakításkezelőket. A megszakításkezelő regisztrációjának törlése a következő függvénnyel történik: void free_irq(unsigned int irq, void *devid); Ha a megszakításvonalat megosztottan használtuk más megszakításkezelőkkel, akkor ügyeljünk arra, hogy a felszabadításnál ugyanazt a devid értéket adjuk meg, amelyet a regisztrációnál. A rendszer nem akadályozza meg, hogy más eszközvezérlő regisztrációját töröljük, ezért különösen oda kell figyelnünk rá.

411

7. fejezet: Fejlesztés a Linux-kernelben

7.15.1. Megszakítások megosztása Mivel a megszakításvonalak száma többnyire véges, ezért a hardverkészítők gyakran kényszerülnek arra, hogy több eszköz között osszanak meg egy megszakításvonalat. Ebben az esetben a megszakításkezelő függvény regisztrációjánál jeleznünk kell az IRQF SHARED flaggel a megosztott használatot. Emellett a devid paraméternek meg kell adnunk egy egyedi értéket, amellyel később a regisztrációnkat azonosíthatjuk. A megszakításkezelő függvény implementációjánál az IRQ_NONE visszatérési értékkel jelezhetjük, ha a megszakítás nem az általunk kezelt eszköznek szólt. Természetesen ilyenkor vigyáznunk kell a megszakítás esetleges letiltásával és engedélyezésével, mivel ebben az esetben más eszközök kezelésére is hatással lehet.

7.15.2. A megszakításkezelő függvények megkötései A megszakításkezelő függvény implementációjakor több szabályt is be kell tartanunk. •

Nem használhatunk sleep0-es függvényt, mivel beláthatatlan következményekkel járna egy taszkváltás a megszakításkezelő kontextusban.



A kmalloc0-os allokációt is csak a GFP ATOMIC flaggel végezhetjük, mivel egyéb esetben sleep0 függvényt használhat az allokáció során.



A kernel és a user space között nem mozgathatunk adatokat, mivel az erre szolgáló függvények sleep0-es függvények.



Törekedjünk a gyorsaságra. A nagy számításokat lehetőleg máshol végezzük.



Mivel nem processz hívja meg a függvényt, ezért a processzspecifikus adatok a megszakításkezelőből nem érhetó'k eb

7.15.3. A megszakítás tiltása és engedélyezése Ha egy megszakítást le szeretnénk tiltani, az alábbi két függvénnyel tehetjük meg: void disablo_irq(unsigned int irq);

vola dhable,_1 ro—nosync(unsigned int i rO); -

412

7.15. Megszakításkezelés

A nosync függvény abban különbözik a társától, hogy míg a normál változat az esetleg futó megszakításkezelő függvény végét megvárja a visszatérés előtt, a nosync változat azonnal visszatér a függvény meghívása után. A megszakítás ismételt engedélyezése az alábbi függvénnyel történik: void enable_irq(unsigned int rq);

Ha szükségünk van az összes megszakítás letiltására, illetve engedélyezésére, akkor ezt az alábbi függvényekkel tehetjük meg: void local_i rg_enable(); void local_i rq_disable();

Többnyire szeretnénk a korábbi beállításokat lementeni, illetve az engedélyezéskor visszaállítani. Az alábbi függvények erre nyújtanak lehetőséget: 1-06 void local_i rg_save(unsigned leng flags); void local_i rg_restore(unsigned long flags);

7.15.4. A szoftvermegszakítás A szoftvermegszakítások (software interrupt, softirq) lehetővé teszik a kernelnek egy feladat végrehajtásának a késleltetését. Jellegre és megkötésekben egyeznek a korábban tárgyalt hardvermegszakításokkal, csak nem hardveresemény generálja őket, hanem szoftver váltja ki a mechanizmust.'w Amikor egy függvényhívással szoftvermegszakítást váltunk ki, akkor erre a kernel nem azonnal reagál, hanem kicsivel később hívja meg a kezelőfüggvényt. Egész pontosan, amikor egy hardvermegszakítás kezelőfüggvénye véget ér, akkor a kernel lefuttatja a várakozó szoftvermegszakításokhoz tartozó kezelőfüggvényeket. A szoftvermegszakítás-kezelés központi eleme egy 32 bejegyzést tartalmazó tábla, amely az egyes kezelőfüggvények mutatóit, adatait tartalmazza. A megszakítás generálásakor a regisztrált függvények hívódnak meg. A 32-es limit aránylag szűknek tűnhet, ám közvetlenül nem szokás használni ezt a mechanizmust. Ehelyett az erre épülő szolgáltatásokat vesszük igénybe, ilyenek például a kisfeladatok (lásd a 7.15.5.1. A kisfeladat (tasklet) alfejezetben).

1°6 1°7

A megadott függvények valójában makrók, ezért a paraméter nem érték szerint adódik át. A Linux-kernel esetén tárgyalt szoftvermegszakítás terminológia nem egyezik meg az x86-os világban használt hasonló nevű fogalommal, azaz nem az Intel x86-os processzor INT x instrukciójának használatáról van szó.

413

7. fejezet: Fejlesztés a Linux-kernelben

7.15.5. A BH-mechanizmus A hardvermegszakítás-kezelőben sokszor komolyabb adatfeldolgozást is el kell végeznünk, ekkor azonban nem lesz gyors az implementáció, amely a megszakításkezelő egyik legfontosabb alapkövetelménye. Erre a problémára nyújt megoldást az alsórész- (Bottom half, BH) mechanizmus, amely a megszakításkezelőt egy felső részre (Top half) és egy alsó részre (Bottom half) választja szét. A felső rész a tényleges megszakításkezelő rutin. Ennek feladata csak az adatok gyors letárolása a késó'bbi feldolgozáshoz, illetve a feldolgozó rutin futtatásának kérvényezése. Az alsórész-rutin már nem megszakításidőben fut, ezért a futás idejére nem érvényesek a szigorú megkötések. Az alsó rész implementációjára két mechanizmust használhatunk: •

Kisfeladat (Tasklet)



Munkasor (Workqueue)

A két megoldásnak eltérő tulajdonságai, előnyei és hátrányai vannak, amelyeket a következőkben részletesen megvizsgálunk. Létezik még egy harmadik megoldás is, amely közeli rokonságban áll a munkasorral, nevezetesen a kernelszálak használata a feladatok elvégzésére, ennek később külön fejezetet szentelünk.

7.15.5.1. A kisfeladat (tasklet) Ha a hardvermegszakításban elvégeztük a gyors kritikus dolgokat, kézenfekvő megoldás, hogy a feladat maradék részére meghívunk egy szoftvermegszakítást (lásd a 7.15.4. A szoftvermegszakítás alfejezetben). A Linux-kernel erre külön támogatást nyújt, ezeket kisfeladatoknak (tasklet) nevezzük. A kisfeladatok szoftvermegszakítás által meghívott kódrészletek, amelyeket a programozó regisztrálhat, illetve eltávolíthat, és amelyeket a kernel ütemez. Így a megoldásunk a következő. A megszakítás időigényesebb részét egy megadott paraméterlistájú függvényben implementáljuk, és beregisztráljuk mint kisfeladatot. A hardvermegszakítás végén kérjük a kisfeladat ütemezését. Ha nem használjuk tovább a kisfeladatot, akkor fel kell szabadítanunk. Kisfeladatot akkor alkalmazunk, ha a feladat túl kevés ahhoz, hogy külön szálat rendeljünk hozzá. A kisfeladat implementációja során figyelembe kell vennünk néhány megkötést:

414



A kisfeladat futtatását többször is kérhetjük, mielőtt lefutna. Ilyenkor azonban csak egyszer fut le.



Ha a kisfeladat fut már egy CPU-n a kérvényezéskor, akkor később ismét lefut.



A kisfeladat azon a CPU-n fut le, ahol először kérvényezték.

7.15. Megszakításkezelés



Egyszerre a kisfeladat csak egy példányban futhat még SMP-rendszerben is.



Különböző kisfeladatok viszont futhatnak párhuzamosan SMP-rendszerben.



A kisfeladat nem indul el, amíg a megszakításkezelő be nem fejeződik.



A kisfeladat futása során meghívódhat a megszakításkezelő.



Mivel a kisfeladat szoftvermegszakítás-kontextusban fut, ezért a megszakítás-kezelők implementációs megkötései rá is érvényesek: sleep0-et tartalmazó függvényt nem használhatunk kisfeladatban sem.

A kisfeladat-mechanizmus használatához először létre kell hoznunk egy kezelőfüggvényt Ennek alakja a következő: vala k^seÍadat^i(urísignd léging '

j

Feladatot a következő makróval hozhatunk létre: DÉCLARLtA$ 10,....ET(Ov

,

A név a kisfeladat neve, amellyel később hivatkozhatunk rá. A függvény a kezelőfüggvényünk neve. Az adat mezőben található számot kapja meg a kezelőfüggvényünk adatként egyetlen paraméterén keresztül. Ezt követően a kisfeladat lefuttatását a tasklet_scheduleQ függvénnyel kérhetjük:

if:Utásklet'

.~1.1

le

Nézzük meg, hogyan is használhatjuk ezeket a függvényeket. Feladat Írjunk kernelmodult, amely számolja a beállított megszakítást. A számláló értékét egy kisfeladat másolja le, hogy aztán a másolatot elérjük egy proc állományon keresztül. /* helloi rq.c - Egyszeru megszaki tas kezel es kisfeladat haszna] ataval . */ #include #include #include #include #include unsigned long counter1=0; unsigned long counter2=0; unsigned int cirq=1; static unsigned int devid=33;

415

7. fejezet: Fejlesztés a Linux-kernelben

static DEFINE_SRINLOCK(hello_lock); void hello_tasklet_proc(unsigned long); DECLARE_TASKLET(hello_tasklet, hello_tasklet_proc, 0); /* megszakitas kezelo fuggveny. */ irgreturn_t counter_irq(int irq, void *dev_id) { counterl++; tasklet_schedule(&hello_tasklet); return IRQ_NONE; } /* Tasklet kezelo fuggveny. */ void hello_tasklet_proc(unsigned long nothing) { unsigned long flags; spin_lock_irqsave(&hello_lock, flags); counter2=counterl; spin_unlock_irgrestore(&hello_lock, flags); } /* Az allomany olvasasat lekezelo fuggveny. */ int procfile_read(char* buffer, char** buffer_location, off_t offset, int buffer_length, int *eof, void *procdata) { int len; spin_lock(&hello_lock); len=sprintf(buffer, "Szamlalo: %lu\n", counter2); spin_unlock(&hello_lock); return len;

/* A kernel modul inicializalasat vegzo fuggveny. */ static int _init hello_init(void) { struct proc_dir_entry *hello_proc_ent; /* Regisztraljuk a megszakitas kezelot. */ if(request_irq(cirq, counter_irq, IRQF_SHARED, "Hello szamlalo", &devid)) { printk(KERN_WARNING "A %d megszakitas foglalt.\n", cirq); cirq=-1; } /* Dinamikusan hozzuk letre a proc allomany bejegyzest. */ hello_proc_ent = create_proc_entry("hello", S_IFREG I S_IRUGO, 0);

416

7.15. Megszakításkezelés

/* Beallítjuk a link szamlalot es a lekezelo fuggvenyt if(hello_proc_ent) { hello_proc_ent->nlink = 1; hello_proc_ent->read_proc = procfile_read;

*

return 0; } /* A kernel modul eltavolitasa elott a felszabaditasokat vegzi.

static void _exit hello_exit(void) { /* Megszuntetjuk a megszakitas kezelo regisztraciojat. if(cirg>=0) free_irg(cirg, &devid);

*/

/* Megsemmisitjuk a proc allomany bejegyzest. */ remove_proc_entry("hello",0); } module_init(hello_init); module_exit(hello_exit); m000LE_DEscRIPTI0N("Bello proc"); MODULE_LICENSE( "GPL ");

A példában az 1-es megszakításra regisztrálunk, és megosztottan használjuk az eredeti kezelőfüggvénnyel. Ez a megszakításvonal általában az időzítő, amelyre rendszeresen érkeznek megszakítások. Mivel a mi megszakításkezelő függvényünk csak egy „potya"-függvény, és nem akarjuk megzavarni a rendszer működését, ezért visszatérési értékként IRQ_NONE értéket használunk. Az időzítő megszakítása helyett választhatunk azonban más megszakításvonalat is, amelyre vagy nincs megszakításkezelő regisztrálva, vagy megosztottan használatos.

7.15.5.2. Munkasor A munkasor (work queue) olyan kódrészlet, amelyet kernelszálak futtatnak a háttérben. A munkasor aktiválása a 7.10. I/O műveletek blokkolása alfejezetben tárgyalt várakozási sorokon keresztül történik: a munkasor aktiválását ugyanúgy eseményként kezeli a futtató szál, amelyre várakozik. Az esemény ekkor a futtatás kérelmezése. A munkasor tágabb lehetőségeket nyújt, mint a kisfeladat: mivel a függvény ebben az esetben egy külön kernelszálban hívódik meg, ezért egyrészt használhatunk sleep() es függvényeket is, másrészt az implementáció hosszára nincs megkötés, mivel közben lehetséges a taszkváltás. Ugyanakkor továbbra sem érhetjük el más processzek címterét, ám ez nem szokott komoly problémát jelenteni. -

417

7. fejezet: Fejlesztés a Linux-kernelben

Használatuk alapkoncepciói nagyon hasonlítanak a kisfeladatokéhoz: regisztrálunk egy végrehajtó rutint, kérjük annak lefuttatását, ez majd valamikor egy későbbi időpontban lefut, és amikor nincs már szükségünk rá, fel kell szabadítanunk. Az adatstruktúrák és a működés természetesen más, mint a kisfeladatok esetében. A munkasor használatához létre kell hoznunk egy kezelőfüggvényt, amelyet a mechanizmus meghív. Ennek alakja a következő: void (*work_func_t)(struct work_struct *);

Továbbá létre kell hoznunk magát a munkasort: struct workqueue_struct * create_workqueue(const char *name);

Ugyanakkor a kernelmodul eltávolításakor nem szabad elfelejtenünk a munkasor megsemmisítését: void destroy_workqueue(struct workqueue_struct *wq);

A létrehozást követően a megszakításkezelő rutinban a feldolgozófüggvényünkből egy work_struct típusú elemet kell létrehoznunk. Az első alkalommal ezt az alábbi makróval tehetjük meg: INIT_WORK(struct work_struct *work, void (*function)(void *));

A későbbi meghívások során a módosításhoz elegendő a következő makrót használnunk: PREPARE_WORK(struct work_struct *work, void (*function)(void *));

Még szintén a megszakításkezelő rutinban el kell helyeznünk a work_struct elemmel reprezentált feladatot a munkasorba: int queue_work(struct workqueue_struct *wq, struct work_struct *work);

Feladat Valósítsuk meg az előző fejezetben látott példát kisfeladat helyett munkasorral. /* hel 1 oi rq2 . c - Egyszeru pel da a munkasor haszna] atara. */ #include #include #include #include #include #include

418

7.15. Megszakításkezelés

unsigned long counter1=0; unsigned long counter2=0; unsigned int cirq=1; static unsigned int devid=33; static bEFINE_SPINL0cK(hello_lock); static struct workqueue_struct* hello_wq; void hello_wq_proc(struct work_struct* work); /* Megszakitas kezelo fuggveny. */ ircireturn_t counter_irq(int irq, void *dev_id) { static int inited = 0; static struct work_struct task; counterl++; /* A workqueue taszk elokeszitese. if(inited == 0) { INIT_woRk(&task, hello_wq_proc); inited = 1; //else //{ // PREPARE_WORK(&task, hello_wq_proc); //} /* A taszk behelyezese a workqueue-ba. */ queue_work(hello_wq, &task); return IRQ_NONE; }

/* A feladatot vegrehajto fuggveny. */ void hello_wq_proc(struct work_struct* work) {

unsigned long flags; spin_lock_irqsave(&hello_lock, flags); counter2=counterl; spin_unlock_irgrestore(&hello_lock, flags);

/* Az allomany olvasasat lekezelo fuggveny. */

int procfile_read(char* buffer, char** buffer_location, off_t offset, int buffer_length, int *eof, void *procdata) { int len; spin_lock(&hello_lock); len=sprintf(buffer, "counter: %lu\n", counter2); spin_unlock(&hello_lock); return len; }

419

7. fejezet: Fejlesztés a Linux-kernelben /* A kernel modul inicializalasat vegzo fuggveny. */ static int init hello_init(void) {

struct proc_dir_entry *hello_proc_ent; /* Letrehozzuk a workqueue-t. */ hello_wq = create_workqueue("helloworkqueue"); /* Regisztraljuk a megszakitas kezelot. */ if(request_irq(cirq, counter_irq, IRQF_SHARED, "Hello counter", &devid)) {

printk(KERN_WARNING "A %d megszakitas foglalt.\n", cirq); cirq=-1;

/* Dinamikusan hozzuk letre a proc allomany bejegyzest. */ S_IRUGO, hello_proc_ent = create_proc_entry("hello", S_IFREG 0); /* Beallitjuk a link szamlalot es a lekezelo fuggvenyt. */ if(hello_proc_ent) hello_proc_ent->nlink = 1; hello_proc_ent->read_proc = procfile_read; return 0;

/* A kernel modul eltavolitasa elott a felszabaditasokat vegzi. */ exit hello_exit(void) static void /* Megszuntetjuk a megszakitas kezelo regisztraciojat. */ if(cirq>=0) free_irq(cirq, &devid); /* Megsemmisitjuk a workqueue-t. */ destroy_workqueue(hello_wq); /* Megsemmisitjuk a proc allomany bejegyzest. */ remove_proc_entry("hello",0);

module_init(hello_init); module_exit(hello_exit); MODULE_DESCRIPTION("Hello proc"); MODULE_LICENSE("GPL");

A példa működésében megegyezik a kisfeladat témakörénél látottakkal, azzal az eltéréssel, hogy itt munkasort használtunk a feladat végrehajtásakor.

420

7.16. A kernelszálak

7.16. A kernelszálak A megszakításkezelés alsó részeként kernelszálakat is használhatunk. Ez hasonlít a munkasor esetére, ám több lehetőségünk nyílik, mert mi magunk végezzük a szálkezelést. Például a megszakítás hatására akár egy felhasználói módban futó programot is elindíthatunk, vagy kommunikálhatunk vele. (Lásd a call_usermodehelper04.) Természetesen a megszakításokon kívül más esetekben is alkalmazhatjuk a kernelszálakat, tipikusan akkor, amikor egy hosszú adatfeldolgozás megvalósítására van szükségünk. A kernelszál (kernek thread) lényegében egy olyan folyamat, amelynek nincs virtuális címterülete a felhasználói címtartományban. Ehelyette a szálak a kernel címtartományán osztoznak. Erre a memóriakezelésnél oda kell figyelnünk. A kernelszál létrehozásához először is meg kell írnunk a szálfüggvényt, amely a szál belépési pontjaként szolgál, hasonlóan az alkalmazásszintű szálkezeléshez. A szálfüggvény alakja az alábbi: int thr

dfh(void *data);

Ezt követően egy alvó állapotú szálat az alábbi függvénnyel hozhatunk létre:

A threadfn a szál fő függvénye. A data a függvénynek átadott paraméter. A namefmt, ... rész a szál neve printf-es alakban argumentumokkal — vagyis egy formázó szöveg, majd egy nem meghatározott számú paraméter. Visszatérési értékként megkapjuk a szálat leíró struktúrára mutató pointert. Ezzel hivatkozhatunk később a szálra. Mivel jelenleg még alvó állapotban van a szálunk, fel kell ébresztenünk: int wake_up prcrcess (strúct - t .

k

t

'sk);

A létrehozás és az ébresztés műveletét egyben is elvégezhetjük: struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char namefmt[], ...);

A függvény paraméterezése megegyezik a szál létrehozásánál látottakkal. Ezeknek a függvényeknek a meghívása után létrejött a kernelszálunk, és egymás után hajtja végre a szálkezelő függvényben foglalt utasításokat. Amikor a függvénynek vége, akkor a szál is leáll. Fontos, hogy a szálat kívülről nem állíthatjuk le. Ennek az az oka, hogy egy ilyen véletlenszerű időpontban 421

7. fejezet: Fejlesztés a Linux-kernelben

elkövetett megszakítás nem definiált állapotba hozhatná a rendszert. Helyette a kezelőfüggvényünkben kell rendszeresen ellenőrizni, hogy szálunkat más szálak le akarják-e állítani. Ezt a következő függvénnyel tehetjük meg: int kthread_should_stop(void);

A 0-tól eltérő érték jelenti, hogy le kellene állítanunk a szálunkat, ezért a függvény közvetlenül használható az if paramétereként. Ha ezt implementáltuk, akkor a szál leállítását egy másik szálból az alábbi függvénnyel kérhetjük: int kthread_stop(struct task_struct *k);

Figyeljünk arra, hogy ezt a függvényt már leállt, nem létező szálakra ne hívjuk meg, mert hibát eredményez.'" Feladat Írjunk egy kernelmodult, amely elindít egy szálat. A szál másodpercenként növeljen egy számlálót, amelynek értékét ki is írja. Emellett figyelnünk kell arra, hogy a modul eltávolítása előtt le kell állítanunk a szálat. /* hellothread.c - Egyszeru szalkezeles.

*7

#include

  • #include #include #include struct task_struct* pth; /* A szal fo fuggvenye. */

    int mythread(void* arg) { int i = 0; /* A leallitasi kerelem ellenorzese. */ while(lkthread_should_stop()) {

    ssleep(1); printk("thread: %d\n", 1); 1++; }

    return 0;

    108

    A kódban elég nehéz annak ellenőrzése, hogy egy szál él-e még. Ezért ha a kthread_stop0 függvényt akarjuk használni, akkor a szálat úgy implementáljuk, hogy csak ennek a függvénynek a hatására álljon le. Ezzel teljesítjük az említett követelményt.

    422

    7.17. Várakozás

    static int init hello_init(void) { /* A szal letrehozasa es elinditasa. */ pth = kthread_run(mythread, NULL, "mythread"); return 0;

    static void _exit hello_exit(void) { /* A szal leallitasanak kerelmezese. kthread_stop(pth);

    module_init(hello_init); module_exit(hello_exit); m00uLE_DEscRIPTION("Hello module"); mOpuLE_LICENSE("GPL");

    A példában szereplő ssleep0 függvényt a következő fejezetben tárgyaljuk.

    7.17. Várakozás Az eszközökkel való kommunikáció során gyakran van szükségünk kisebb nagyobb várakozások implementálására a kódunkban, hogy az adott protokoll időzítéseit tartani tudjuk. A várakozás ideje alapján két esetet különböztethetünk meg. A rövid és a hosszú várakozást attól függően, hogy a rendszer órajelénél (jiffy) rövidebb vagy több órajelnyi várakozásról van szó. A várakozás módja alapján van olyan függvény, amely egy ciklus futtatásával éri el a megfelelő idejű várakozást, és van olyan, amelyik más folyamatra vált át, amíg eltelik az idő.

    7.17.1. Rövid várakozások Amikor a várakozás rövidebb lehet a jiffy idejénél, és néhány nano-, micro-, vagy millisecundum, akkor az alábbi függvények használhatók a várakozásra: void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs);

    423

    7. fejezet: Fejlesztés a Linux - kernelben

    Ezek a függvények a processzor sebessége alapján számított várakozó ciklusokat futtatnak, amelyek lefoglalják az adott processzort. Van egy másik megoldás, amely a sleep függvényre épít. Ehhez a következő függvényeket használhatjuk: void msleep(unsigned int msecs); unsigned long msleep_interruptible(unsigned int msecs); void ssleep(unsigned int seconds);

    Az első két függvény a meghatározott millisecundum időre alvó állapotba helyezi a hívó processzt. Az utóbbi megszakítható, és a visszatérési értéke jelzi, hogy mennyi millisecundum van még hátra a meghatározott idő elteltéig. Az ssleepQ nem szakítható meg, és a megadott másodpercig helyezi alvó állapotba a processzt. Ezeknek a függvényeknek a hátránya, hogy a tényleges várakozás több lehet a megadott időnél.

    7.17.2. Hosszú várakozás A több jiffy idejű várakozás esetén az egyik lehetőségünk egy ciklus használata, amely az adott jiffy ideig tart: while(time_after(jtime,

    ji ffi es))

    {

    //mag

    A példában a jtime az általunk megadott későbbi jiffyben mért azon időpont, ameddig a ciklus tart. A ciklus magjának több függvényt is használhatunk. Ha pontos várakozást szeretnénk, akkor használható a epurelax() függvény, amelynek az implementációja platformonként eltérő lehet. A másik lehetőség, hogy lemondunk a CPU-ról a schedule() függvény meghívásával. A ciklusos megoldásnál kényelmesebb egy timeouttal dolgozó függvény alkalmazása. Lehetnek ilyenek a korábbiakban, a blokkolódó függvényeknél ismertetett wait_event függvények: wait_event_timeout(wait_queue_head_t queue, int condition, long timeout); wait_event_interruptible_timeout(wait_queue_head_t queue, ínt condition, long timeout);

    Ezek a függvények egy várakozási sor létrehozását igénylik. Létezik ugyanakkor egy konkrétan erre a célra készült megoldás is, amelyet a következő függvényekkel érhetünk el•

    424

    7.18. Időzítők signed long schedule_timeout(signed long timeout); signed long schedule_timeout_interruptible(signed long timeout); signed long schedule_timeout_uninterruptible(signed long timeout);

    Ezek a függvények a meghívásuk után beállítanak egy időzítőt, majd várakozó állapotba váltják a folyamatot, hogy addig más folyamatok futhassanak. Az időzítő lekezelő függvénye ismét felébreszti a folyamatot. Bár a paraméter típusából arra következtethetünk, hogy a timeout argumentumnak negatív értéket is megadhatunk, valójában ekkor a függvény azonnal visszatér, és hibajelzést ír ki a konzolra.

    7.18. Időzítők Előfordulhat, hogy késleltetve, aszinkron módon kell végrehajtanunk műveleteket. Ilyenkor használhatjuk a kernelben lévő időzítőket. Ehhez létrehozunk egy függvényt, amelyet megadott időközönként a rendszer meghív. Erre a függvényre ugyanazok a megszorítások érvényesek, mint a megszakításkezelő függvényekre, mert egy megszakításkezelő futtatja. A kezelőfüggvény alakja a következő:

    unc(wrtsi gne d A leíró adatstruktúra a ti mer függvény regisztrálásához az alábbi: struct timer_list { unsigned long expíres; void ("function)(unsigned long); unsigned long data;

    1; A struktúra bővebb, és a maradék részek inicializálásához szükség van az alábbi függvények valamelyikének a meghívására: void init_timer(struct timer_list * timer); TINIER_INITIALIzER(_function, _expires, _data)

    A két lépést egyben is elvégezhetjük a következő függvény használatával: void setup_timer(struct timer_list * timer, void ("function)(unsigned long), unsigned long data);

    425

    7. fejezet: Fejlesztés a Linux-kernelben

    A következő függvényekkel ebben a sorrendben engedélyezhetjük, tilthatjuk, vagy módosíthatjuk az időzítőt: void add_timer(struct timer_list *timer); int del_timer(struct timer_list * timer); int mod_timer(struct timer_list *timer, unsigned long expi res); SMP-rendszereken az időzítő törlésekor előfordulhat, hogy közben a kezelőfüggvény egy másik processzoron még fut. Ezért létezik a függvénynek egy olyan variánsa, amely csak akkor tér vissza, ha a kezelőfüggvény már befejeződött. Ennek alakja így néz ki: int del_timer_sync(st uct timer_l t *timer);

    Lekérdezhetjük, hogy a timer függvény elő van-e jegyezve futtatásra, vagyis a megadott idő lejárt-e: int timer_pending(const struct timer_list * timer);

    7.19. Eszközvezérlő modell A számítógépben található eszközök többnyire különböző buszokon keresztül csatlakoznak a rendszerhez. Ezek a buszok eltérően működnek, a rajtuk folyó kommunikáció más és más. A 2.4-es és a korábbi kernelekben a különböző buszokra csatlakozó eszközök vezérlőkódjaiban eltérő struktúrák tartották nyilván a rendszerbe csatlakoztatott eszközöket, általános könyvelés nem létezett. Így egy eszköz jelenlétéről csak a kernelüzenetek böngészésével szerezhettünk tudomást Ez a kaotikus helyzet a 2.5-ös kernelben oldódott meg, majd ez a megoldás a 2.6-os kernelnek is részévé vált. Egy egységes eszközvezérlő modell alakult ki a buszok és a rajtuk helyet foglaló eszközök leírására, továbbá a globális könyvelés is megoldottá vált. Az adatstruktúra hozzáférhető és kezelhető, így már pontos képünk van az rendszerben található eszközökről.

    7.19.1. A busz Bár a buszok eltérnek egymástól, rendelkeznek olyan általános jellemzó'kkel, amelyek egységesen megtalálhatók bennük. Ezek adják az általános buszmodell alapját. A buszleíró struktúra az általános attribútumokat és az általános műveletekhez tartozó visszahívandó függvények (callback function) mutatóit tartalmazza. Ilyen művelet például az eszközök felderítése (detection), a busz leállítása, a tápellátás kezelése. 426

    7.19. Eszközvezérlő modell

    Egy buszt a kernelben a bus_type adatstruktúra ír le. Ez a következőképpen néz ki: struct bus_type { const char struct bus_attri bute struct devi ce_att ri bute struct dri ver_attri bute

    *name; *bus_attrs; *clev_attrs ; *dry_attrs ;

    int (*match)(struct device *dev, struct device_driver *drv); int (*uevent)(struct device *dev, struct kobj_uevent_env *env); int (*probe)(struct device *dev); int (*remove)(struct device *dev); void (*shutdown)(struct device *dev); int (*suspend)(struct device *dev, pm_message_t state); int (*resume)(struct device *dev); const struct dev_pm_ops *pm; struct subsys_private *p; };

    A mezők értelmezése a következő (7. 7. táblázat): 7 .7 . táblázat. A busz regisztrációsstruktúra mezői -

    Név

    Értelmezés

    name

    A busz neve.

    bus_attrs

    A busz alapértelmezett attribútumai.

    dev_attrs

    A buszra csatlakoztatott eszközök alapértelmezett attribútumai.

    drv_attrs

    A busz eszközvezérlőinek alapértelmezett attribútumai.

    match

    Ez a függvény hívódik meg, amikor új eszközt vagy eszközvezérlőt adunk a buszhoz. Nullától eltérő értékkel kell visszatérnie, ha az eszközvezérlő kezelni tudja az eszközt.

    uevent

    Ez a függvény hívódik meg, ha egy eszközt hozzáadunk vagy eltávolítunk a rendszerből.

    probe

    Meghívódik, amikor új eszközt vagy eszközvezérlőt adunk a buszhoz, és meghívja az eszközvezérlő probe függvényét.

    remove

    Meghívódik, amikor egy eszközt eltávolítunk a buszról.

    shutdown

    A rendszer leállításakor hívódik meg, hogy az eszközöket leállítsa.

    suspend

    Akkor hívódik meg, amikor a buszon található egyik eszköz alvó módba akar kapcsolni.

    resume

    Akkor hívódik meg, amikor a busz egyik eszközét fel kell ébresztenie.

    pm

    Tápmenedzsment-műveletek.

    p

    A busz eszközvezérlőjének privát adatait tároló struktúra.

    427

    7. fejezet: Fejlesztés a Linux-kernelben

    Egy új busz regisztrációjakor az eszközvezérlőben minimum a nevet és a match függvényt meg kell adnunk. Például: struct bus_type pci_bus_type = { .name = "pci", . match = pci_bus_match, ;

    Ezen kívül a struktúrát a headerállományban ki kell exportálnunk:

    struct b4s.,t0e A regisztrációt a következő függvény végzi el: itt bus-.l giste

    uc't'bus-Ay0

    A match() függvény implementációja buszfüggő. Ennek az a feladata, hogy megállapítsa, hogy egy bizonyos eszközt egy bizonyos eszközmeghajtó képes-e kezelni Amikor egy új eszközmeghajtót töltünk be, akkor a match0 függvény meghívódik minden olyan eszközre, amelyet nem kezelnek még a korábbi eszközmeghajtók, hogy a rendszer kiderítse, vajon az új kezeli-e.

    7.19.2. Eszköz- és eszközvezérlő lista Korábban a buszhoz kapcsolódó eszközök és eszközvezérlők listáját a busz eszközvezérlője tartotta nyilván a saját egyéni módján. Az általános modellben azonban ez a könyvelés is helyet kapott. A bus_type struktúra p mezője tartalmazza a busz privát adatait. Ebben kapott helyet az eszközök és az eszközvezérlők listája. Az eszközvezérlők listáján az alábbi függvénnyel tudunk végigiterálni: int bus_for_each_drv(struct bus_type *bus, struct device_driver *start, void *data, int (*fn)(struct device_driver *, void *));

    Hasonlóan az eszközök listáján iteráló függvény az alábbi: int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *dev, void *data));

    Kereshetünk egy eszközt név szerint: struct device *bus_find_device_by_name(struct bus_type *bus, struct device *start, const char *name);

    428

    7.19. Eszközvezérlő modell

    Esetleg úgy is kereshetünk, hogy egy függvénnyel teszteljük, hogy megtaláltuk-e: struct devi ce "bus_fi nd_devi ce(struct bus_type *bus, struct devi ce "start, void *data, int ("match) (struct devi ce "dev, void *data));

    7.19.3. sysfs A sysfs állományrendszerben található egy könyvtár, ahol elérhetó'k az eddig tárgyalt információk:

    Ebben a könyvtárban minden regisztrált busz kap egy, a nevével megegyező könyvtárat. A busz könyvtárában megtalálhatók a busz attribútumait reprezentáló virtuális állományok, továbbá egy drivers és egy devices alkönyvtár. Ezek tartalmazzák az előző fejezetben említett eszközvezérlő- és eszközkönyvelést. Minden eszköz, illetve eszközvezérlő további alkönyvtárakat kap, amelyekben elérhetó'k az attribútumaik.

    7.19.4. Buszattribútumok exportálása A korábbiakban láttuk, hogy a busz eszközvezérlője rendelkezhet bizonyos attribútumokkal. Ezeket az attribútumokat kiexportálhatjuk, és elérhetővé tehetjük a sysfs állományrendszeren keresztül. Ehhez első körben implementálnunk kell egy olyan függvényt, amely a virtuális állomány olvasásakor hívódik meg, és egy másik opcionálist, amely íráskor. Ezek alakja az attribútum leíróstruktúrájában található meg: struct bus_attri bute { struct attribute attr; ssize_t (*show)(struct bus_type *bus, char *buf); ssize_t (*store)(struct bus_type *bus, const char "buf, size_t count); } ;

    Az attribute struktúra tartalmazza a nevet és a jogosultságbeállításokat: struct attribute { const char *name; mode_t mode; };

    429

    7. fejezet: Fejlesztés a Linux-kernelben

    A show() függvény az attribútum olvasásakor hívódik meg. Az implementációjában a buf paraméterben kapott bufferbe kell elhelyeznünk a visszaadandó szöveget, és a visszatérési értékben kell megadnunk a szöveg hosszát. Hasonlít a korábban a proc állomány olvasásánál látható művelethez. A store0 függvény az állomány írásakor hívódik meg. A buf paraméterben kapjuk a szöveget tartalmazó buffert és a count paraméterben a hosszát. Visszatérési értékként a kezelt byte-ok számát adjuk vissza. A szöveg értelmezése és esetleg más adattípusra alakítása a függvény feladata. A függvények implementációja után a kiexportálást az alábbi makróval végezzük el:

    41,441

    d414

    Sorban a név, a jogosultság, a show() függvény, a store() függvény. Ha csak olvasható attribútumot szeretnénk készíteni, akkor a store0 függvény mutatója helyett NULL értéket kell megadnunk és megfelelően beállítanunk a jogosultságokat. A makró meghívása megegyezik azzal, mintha létrehoztunk volna egy bus_attr_pelda változót az alábbiak szerint, és beállítottuk volna a mezők értékeit: stati c bus_attribute bus_attr_pel da

    A következő lépés az, hogy a virtuális állományt hozzáadjuk a sysfs könyvtárstruktúrához:

    i nt bus_.c reaté_fi le (sttu bus_type

    bus-a

    bute *

    Itt meg kell adnunk a buszstruktúra és az attribútumstruktúra mutatóját, és a függvény hatására létrejön a virtuális állomány a busz könyvtárában. A busz eszközvezérlő moduljának eltávolításakor természetesen meg kell szüntetnünk az állományt. Ezt a következő függvénnyel tehetjük meg: void bus_remove_file(struct bus_type *, struct bus_attribute

    ;

    7.19.5. Az eszközvezérlő Az eddigiek során elkészítettük és beregisztráltuk a buszunkat. Ezt követően be kell regisztrálnunk azokat az eszközvezérlőket, amelyek a buszra csatlakozó eszközöket kezelik. Természetesen nem minden eszközhöz kell ezt elvégezni, csak eszköztípusokhoz, hiszen az egyforma eszközök egyformán kezelhetők.

    430

    7.19. Eszközvezérlő modell

    Az eszközvezérlőt az alábbi adatstruktúra írja le: struct device_driver { const char *name; struct bus_type *bus; *owner; struct module int (*probe) (struct device *dev); int (*remove) (struct device *dev); void (*shutdown) (struct device *dev); int (*suspend) (struct device *dev, pm_message_t state); int (*resume) (struct device *dev); const struct attribute_group **groups; const struct dev_pm_ops *pm; struct driver_private *p; 1;

    A mezó'k értelmezése a következő (7.8. táblázat): 7.8. táblázat. Az eszközvezérlő regisztrációs struktúra mezői Név

    Értelmezés

    name

    Az eszközvezérlő neve.

    bus

    A busz, amelyhez az eszközvezérlő tartozik.

    owner

    A kernelmodul, amelyhez tartozik (THIS_MODULE).

    probe

    Akkor hívódik meg, ha egy új eszközt csatlakoztatunk a buszhoz. Teszteljük benne, hogy az eszközt kezeli-e az eszközvezérlő, illetve elvégezzük az összerendelést.

    remove

    Az eszköz eltávolításakor hívódik meg.

    shutdown

    A rendszer leállításakor hívódik meg.

    suspend

    Az eszköz elaltatásakor hívódik meg.

    resume

    Az eszköz felébresztését implementálja.

    groups

    Alapértelmezett attribútumok.

    pm

    Tápmenedzsment-műveletek.

    p

    Az eszközvezérlő privát adatai.

    Az eszközvezérlőben minimum a name és a bus mezőket ki kell töltenünk a regisztrációhoz. Ezt követően a regisztrációt az alábbi függvénnyel végezhetjük el: 141

    ; driv



    regi

    er trgct

    setWi ndowTi tl e ("Hel ló Világ");

    QLabel *label = new QLabel("Szöveg:"); QTextEdit *txtEdit = new QTextEdit(); QPushButton *btn = new QPushButton("Kilép"); QVBoxLayout *layout = new QVBoxLayout(); wi ndow->setLayout (1 ayout) ; 1 ayout->addWi dget (1 abel ) ; 1 ayout->addWi dget (txtEdi t) ; layout->addwidget(btn); QObject::connect(btn, SIGNAL(clicked()), &app, window->show(); return app.exec();

    sLoT(guit()));

    }

    A programunk egyetlen forrásállományból áll (main.cpp), ezen belül pedig a main, függvényben hozzuk létre a felületet. Az első négy sorban az include utasítások segítségével beépítjük a Qt keretrendszerből használt osztályok definícióit. Minden Qt-osztályhoz egy külön headerfájl tartozik, amelynek neve megegyezik az osztály nevével. A keretrendszer összes osztályának neve hagyományosan „Q" betűvel kezdődik. A main függvényben először egy QApplication objektumot hozunk létre. Ez az osztály felelős az alkalmazás eseménykezelő ciklusának az elindításáért. Az eseménykezelő ciklus fogadja többek között az ablakozó rendszertől érkező eseményeket, így a felhasználói interakciókat is, amelyeket a megfelelő vezérló'khöz továbbít. Az eseménykezelő ciklus addig fut, amíg az alkalmazás utolsó ablakát be nem zárjuk. (Az eseménykezelésre a későbbiekben még viszszatérünk.) A Qt keretrendszerben — az X Window rendszerhez hasonlóan — a grafikus vezérlőket widgeteknek nevezik. Minden grafikus elem a QWidget ősosztályból származik. Egy alkalmazásban használt vezérlőket a keretrendszer automatikusan fastruktúrába rendezi, vagyis minden elemnek pontosan egy szülője és tetszőleges számú gyereke lehet. Ezt a fastruktúrát futás közben programozottan is be lehet járni, benne a vezérlőket típus és név alapján meg tudjuk keresni. A gyermekvezérlők mindig a szülő területén belül jelennek meg. Az a vezérlő, amelynek nincsen szülője, mindig egy önálló alkalmazásablakként (top level window) jelenik meg, általában ez azt jelenti, hogy saját fejléce (címsora) és kerete van. A fenti kódban létrehozott QWidget példány lesz az alkalmazás főablaka, mivel ennek nem adtunk meg szülőt. A vezérlő setWindowTitle függvényével adjuk meg az ablak címét. -

    444

    8.2. Fejlesztés Qt alatt

    A felületen a következő típusú vezérlőket helyezzük el: •

    QLabel (egyszerű szöveg kiírására),



    QTextEdit (többsoros szövegbeviteli doboz) és



    QPushButton (nyomógomb).

    A vezérlők kialakítása után egy elrendezéskezelő (layout manager) objektumot hozunk létre, amelynek segítségével automatikusan elrendezzük a felületre elhelyezett elemeket. Az elrendezéskezelő felelős a benne elhelyezett vezérlők méretének és pozíciójának automatikus állításáért. Ez biztosítja, hogy például az ablak átméretezése után is megfelelően legyenek elhelyezve az elemek. A példában használt QVBoxLayout osztály függőlegesen egymás alá helyezi el azokat a vezérlőket, amelyeknek az elrendezéséért felelős. Egy elrendezéskezelő objektumhoz az addWidget metódussal adhatjuk hozzá az elrendezendő elemeket. A hozzáadás sorrendje természetesen számít, például a QVBoxLayout ilyen sorrendben helyezi el a widgeteket egymás alá. A keretrendszerben az objektumok közötti kommunikációt az úgynevezett szignál-szlot mechanizmuson alapuló eseménykezelés teszi lehetővé. Ennek lényege, hogy tetszőleges, a QObject ősosztályból származó osztály speciális eseménytípusokat publikálhat, amelyeket szignáloknak (signal) nevezünk. Egy adott eseménytípusbeli esemény bekövetkezését a szignál kibocsátásával (emit) jelezzük. Az eseményekre speciális deklarációjú függvényekkel, úgynevezett szlotokkal (s/ot) reagálhatunk. A szlotokat szintén a QObject ősosztályból leszármazó osztályokban definiálhatjuk. Ha egy szignált összekötünk egy szlottal, akkor valahányszor kibocsátják a szignált (jelzik a szignál által reprezentált eseménytípus egy konkrét előfordulását), a szlotfüggvény meghívódik. A fejlesztés során a szignálfüggvényekhez nem készítünk függvénytörzset, mert azt a Qt generálja helyettünk. A generált függvény biztosítja, hogy a szignállal összekötött szlotfüggvények meghívódjanak. A szlotfüggvény törzsét természetesen nekünk kell definiálni. Egy szignállal akár több szlotfüggvény is összeköthető, és egy szlotfüggvényt akár több szignállal is összeköthetünk, az egyetlen megkötés, hogy a szignálfüggvény és a szlot paraméterlistája megegyezzen. Amikor egy szignált kibocsátunk, a keretrendszer biztosítja, hogy az összes olyan szlotfüggvény meghívódjon, amellyel a szignált összekötöttük, és paraméterként megkapja a szignál kibocsátásakor megadott értékeket. (A szignálok és a szlotok működését, valamint a keretrendszer eseménykezelési modelljét lásd később.) Egy szignál és egy szlot összekötése a QObject osztály connect metódusával történik. A connect meghívásakor a SIGNALO és a SLOT() makrókat kell használni, hogy megadjuk a szignál- és a szlotfüggvényeket. A fenti példában a btn objektum clicked szignálját kötjük össze a QApplication objektum quit szlotjával az alábbi függvényhívás segítségével: QObject::connect(btn, SIGNAL(clicked()), &app, SLOT(quit()));

    445

    8. fejezet: A Qt keretrendszer programozása

    A QPushButton osztály által publikált clicked szignál akkor következik be, amikor a nyomógombot a felhasználó megnyomja. A Qt keretrendszer biztosítja, hogy ha a felhasználó az egérrel egy nyomógomb határain belül kattint, akkor a nyomógomb megkapja ezt az üzenetet. Ha a nyomógomb engedélyezve van, kibocsát egy clicked szignált. Ezzel a szignállal kell összekötnünk azokat a függvényeket, amelyek meghívását ki szeretnénk váltani a kattintást jelző eseménnyel. Vagyis a kattintásesemény kezeléséhez a programozónak csak egy szlotfüggvényt kell létrehoznia és azt összekötnie a szignállal. Bármilyen felhasználói beavatkozás esetén a Qt keretrendszer feldolgozza az operációs rendszertől jövő értesítést, ezek bekövetkezését pedig a vezérlők szignálokkal jelzik. A quit függvény kilép az alkalmazásból. Ezt a függvényt a QApplication osztály szlotként publikálja A szignál és a szlot összekötése biztosítja, hogy amint rákattintunk a gombra, az alkalmazás azonnal bezáruljon. Minden vezérlő láthatatlan a létrehozás pillanatában, amíg meg nem hívjuk rajta a show metódust. A példában a főablakot jelenítjük meg, amely gondoskodik arról, hogy az összes tartalmazott vezérlő ugyancsak megjelenjen. A main függvény utolsó sorában elindítjuk az alkalmazásobjektum eseménykezelő ciklusát az exec függvény segítségével. Ez akkor ér véget, amikor meghívódik az alkalmazás quit függvénye, a ciklus mindaddig fogadja és továbbítja a felhasználói eseményeket Az egyetlen main.cpp forrásállomány megírása után a következő lépés a program lefordítása. A Qt keretrendszerben minden programhoz egy platformfüggetlen projektállomány tartozik, amelynek kiterjesztése .pro. Ez az állomány tartalmazza többek között a programhoz kapcsolódó forrásfájlok listáját. A projektállományt a qmake programmal hozhatjuk létre:

    Az állomány neve a forráskódot tartalmazó könyvtár neve lesz. A projektállományokból szintén a qmake segítésével tudunk platformspecifikus make állományokat generálni:

    A generált make állományok segítségével az adott platformon elérhető fordítóval már a hagyományos módon generálhatjuk a végrehajtható programot: make

    446

    8.2. Fejlesztés Qt alatt

    8.2.2. Projektállományok Az előző példában elkészítettünk egy egyszerű Qt-alapú grafikus alkalmazást, az alábbiakban a program fordítását vizsgáljuk meg részletesebben. A leírt fordítási folyamatnak két Qt keretrendszer specifikus pontja van: •

    a .pro kiterjesztésű projektállomány megírása és



    a platformspecifikus make állományok generálása a qmake programmal.

    Ezeket a feladatokat az integrált fejlesztői környezetek automatikusan el tudják végezni, de azért érdemes megismerkedni a projektállományok szerkezetével. A projektállományt megírhatjuk mi magunk is, de a qmake segítségével generálhatjuk is a —project kapcsoló használatával (lásd az előző példában). A korábbi, egyetlen main.cpp állományt tartalmazó programhoz a qmake a következő projektfájlt generálja: ################################################################## # Automatically generated by qmake (2.01a) P júl. 1 13:07:41 2011 ################################################################## TEMPLATE = app TARGET = DEPENDPATH += INCLUDEPATH += . # Input SOURCES += main.cpp

    A projektállományok legfontosabb részei a következők: •

    A # jellel kezdődő sorok megjegyzések, ezeket a feldolgozásnál a qmake nem veszi figyelembe.



    A megjegyzések utáni első sorban a TEMPLATE változónak adunk értéket, ez általában app vagy lib lehet. Az app jelzi, hogy egy önálló futtatható alkalmazást akarunk generálni a projektből, a lib pedig, hogy egy programkönyvtárat.



    A TARGET változóban megadhatjuk a generálandó file nevét (kiterjesztés és verziószám nélkül), üres érték esetén az alapértelmezett név a projektállományt tartalmazó könyvtár neve.



    A DEPENDPATH és az INCLUDEPATH változók értékei tárolják azoknak a könyvtáraknak a neveit, ahol a nem a projekthez tartozó, de hivatkozott headerfájlokat és programkönyvtárakat kell keresnie a fordítónak.

    447

    8. fejezet: A Qt keretrendszer programozása



    A SOURCES változónál soroljuk fel a fordítandó forrásállományokat.



    Amennyiben különálló headerfájljaink is lennének, azokat a HEADERS után kellene felsorolni.

    A fenti változók esetében gyakran nemcsak egy, hanem több nevet is fel kell sorolni, ilyenkor ezeket szóközzel elválasztva írjuk le. Ha egy sor túl hosszú, akkor a \ jellel törhetjük, és új sorban folytathatjuk. Egy további lehetőség az, hogy a += operátor segítségével lehet egy változóhoz hozzáfűzni újabb értéket, ezt többször is meg lehet tenni egymás után. Az alábbi példában mindhárom lehetőséget megmutatjuk: SOURCES = sourcel.cpp source2.cpp \

    source3.cpp SOURCES += source4.cpp SOURCES += source5.cpp

    Egy projektállomány tehát egy Qt-projektet ír le, ahol a projekt tartalmazza a generálandó alkalmazás vagy programkönyvtár nevét, az összes fordítandó forrásállományt és azokat a könyvtárakat, ahol a fordításhoz szükséges hivatkozott programkönyvtárak megtalálhatók. A projektállomány mindezeket az információkat platformfüggetlen formában tárolja. A projektállomány megírása után, a qmake parancs kiadásával (a projektállományt tartalmazó könyvtárban) a projektállományból generálunk egy platformspecifikus make állományt, amelyet már hagyományosan tudunk fordítani.

    8.2.3. A QObject szolgáltatásai A Qt keretrendszer olyan speciális konstrukciókat biztosít, amelyek lehetővé teszik az objektumok közötti hatékony kommunikációt (szignál-szlot mechanizmus) és a metaadatokhoz való hozzáférést. A keretrendszer alaposztálya a QObject. Ennek példányait a keretrendszer fastruktúrába rendezve tárolja, amely programozottan bejárható futás közben, így az objektumok kereshetővé válnak. A Qt lehetővé teszi olyan tulajdonságok (property) definiálását, amelyek futási időben lekérdezhetők. A fenti szolgáltatások egy részét hagyományos C++-technikákkal valósították meg, de bizonyos kiterjesztések (például a szignál-szlot mechanizmus) szükségessé teszik egy speciális előfordító, a moc (Meta Object Compiler) használatát. (A moc működésére a Qt eseménykezelés-modelljének tárgyalásánál visszatérünk.) -

    448

    8.2. Fejlesztés Qt alatt

    8.2.4. A QtCore modul A Qt keretrendszer nem csak grafikus felületek fejlesztéséhez szükséges vezérlőket tartalmazó osztálykönyvtárat kínál. Sok olyan osztály található benne, amely a felhasználói felülettől független programozói interfészt biztosít, többek között például adatbázisok elérésére, hálózati kommunikáció írására vagy többszálú programozásra. Amikor egy forrásfájlban a keretrendszer egyik osztályára hivatkozunk, akkor ehhez az include utasítással be kell építeni a megfelelő osztályt. A beépítendő állomány neve megegyezik az osztály nevével. A következő sor szükséges ahhoz például, hogy a QApplication osztályt tudjuk használni a kódunkban:

    data());

    A setText metódusban kell frissíteni a tárolt szöveget és a textChanged jelzést leadni, vagyis a textChanged szignált kibocsátani. Ennek a szintaktikája a következő: emit (‹argumentumok>);

    vagyis az emit kulcsszó után a szignálfüggvényt hívjuk meg a megfelelő paraméterértékek átadásával.

    8.3.2. Szlotfüggvények létrehozása A programunkban szükség lesz egy másik osztályra is, amelynek a szlotját öszszeköthetjük a textChanged szignállal. A ContentWatcher osztály segítségével egy TextContent típusú objektumot figyelhetünk meg, úgy, hogy feliratkozunk arra a szignálra, amely az adott objektum szövegének a megváltozását jelzi: /* contentwatcher.h */ #ifndef FILEWATCHER_H #define FILEWATCHER_H #include #include class ContentWatcher : public QObject { Q_OBJECT public slots: void contentChanged(const char * newText);

    1;

    #endif // FILEWATCHER_H

    454

    8.3. A Ot eseménykezelés modellje -

    /* contentwatcher.cpp */ #include "contentwatcher.h" #include voi d Contentwatcher : : contentChanged(const char " newText){ std : : cout « "Content has changed: " « newText ; }

    A ContentWatcher osztály definiál egy contentChanged szlotfüggvényt. A szlotokat mindig a „slots:" után soroljuk fel. A szlotok mindig publikusak, akárcsak a szignálok, ennek ellenére mégis adhatunk meg láthatóságot, ennek azonban csak akkor van jelentősége, ha a szlotot mint hagyományos függvényt használjuk. A contentChanged paraméterlistája megegyezik a Text Content osztályban definiált textChanged szignál paraméterezésével. A szlotfüggvény implementációjában egyszerűen kiírjuk a konzolra a megváltozott szöveget.

    8.3.3. Szignálok és szlotok összekapcsolása Egy szignált és egy szlotot a már korábban látott connect függvénnyel kapcsolhatunk össze: #incl ude bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection )

    Paraméterei a következők: •

    sender: Arra az objektumra mutató pointer, amelytől a szignál származik.



    signal: A kezelendő szignál. A sender objektumnak kell küldenie.



    receiver: Arra az objektumra mutató pointer, amelynek fogadnia kell a szignált.



    method: A receiver szlotfüggvénye, amely lekezeli a szignált.



    type: Az utolsó paraméterrel a kapcsolódás típusát lehet beállítani. Lehetséges értékeit a 8.1. táblázatban soroljuk fel.

    455

    8. fejezet: A Qt keretrendszer programozása

    8.1. táblázat. A szignálok és a szlotok összekapcsolásának típusai (a Qt::ConnectionType értékei)

    Típus

    Leírás

    Qt:.AutoConnection

    Ez az alapértelmezett típus. Ilyenkor a rendszer automatikusan választ aszerint, hogy a szignált publikáló és a szlotot definiáló objektumok egy szálban vannak-e Ha igen, akkor a DirectConnection típus szerint a szignál kibocsátásakor a szlot közvetlenül hívódik meg, ha nem, akkor a QueuedConnection típus szerint a szignál bekerül a másik szál eseménykezelő ciklusának várakozási sorába.

    Qt::DirectConnection

    A szlotot azonnal meghívjuk, amikor a szignált kibocsátjuk.

    Qt::QueuedConnection

    A szignált akkor hívja meg a keretrendszer, amikor a szlotfüggvényt tartalmazó objektum szálának az eseménykezelő ciklusa feldolgozza a szignál hatására küldött üzenetet.

    Qt::BlockingQueuedConnection

    Ugyanúgy működik, mint a QueuedConnection, csak a szignál kibocsátásakor a küldő szál blokkolódik, amíg a szlot függvény le nem fut. (Csak különböző szálak esetén használható.)

    Qt:: UniqueConnection

    Ugyanúgy működik, mint az AutoConnection, de csak akkor történik meg az összekapcsolás, ha az korábban még nem létezett. Tehát a rendszer biztosítja, hogy egy objektum egy szignálját egy másik objektum egy szlotjával csak egyszer lehessen öszszekapcsolni.

    Amikor egy szignál hatására meghívódik egy szlotfüggvény, ezen belül fontos lehet tudni, hogy melyik objektum küldte a jelzést. A megoldás a QObject osztály sender függvénye:

    ;cón*,-, Ez a függvény egy szignál hatására meghívott szlotfüggvényben a szignált küldő objektummal tér vissza, egyébként pedig 0-val. Ahhoz, hogy az előbbi két osztályban definiált szignált és szlotot használni tudjuk, a QObject osztály connect függvényének segítségével a két osztály egy-egy példányán össze kell kötni a függvényeket, ezt a példánkban az alkalmazás main függvényében tesszük meg. Az alkalmazásunk harmadik forrásállománya a main függvényt tartalmazó main.cpp. Ebben felhasználjuk a TextContent és a ContentWatcher osztályainkat, mindkettőt példányosítjuk, majd összekötjük a megfelelő szignált és szlotot:

    456

    8.3. A Qt eseménykezelés-modellje /* main.cpp */ #include #include #include int main(int argc, char *argv[]) {

    TextContent * content = new TextContent(); ContentWatcher * watcher = new ContentWatcher(); QObject::connect(content, SIGNAL(textChanged(const char *)), watcher, SLOT(contentChanged(const char *))); content->setText("Helló szignál-szlot!"); return 0; }

    A program lefordításához szükség van egy .pro kiterjesztésű projektállományra, amelyet ismét automatikusan generálhatunk a qmake-kel, vagy megírhatjuk manuálisan is: # watchDog.pro TEMPLATE = app SOURCES += main.cpp \ textcontent.cpp \ contentwatcher.cpp HEADERS += \ textcontent.h contentwatcher.h

    Ezután már csak a make állomány előállítása és a make program meghívása hiányzik a projekt lefordításához: qmake WatchDog.pro make

    8.3.4. Szlot az átmeneti objektumokban Ha egy szlotfüggvény olyan objektumban helyezkedik el, amelyet csak átmenetileg használunk, akkor szükség lehet a létrehozott szignál-szlot kapcsolatok megsemmisítésére. A szignál-szlot összeköttetéseket a QObject osztály disconnect függvényével kapcsolhatjuk szét: bool QObject: :di sconnect const QObject * sender, const char * signal, const QObject receiver, const char * method )

    457

    8. fejezet: A Qt keretrendszer programozása

    A sender paraméter által tartalmazott egyik szignálról (signal) lecsatlakoztatjuk a receiver egyik szlotját (s/ot). Ha a signal vagy a slot paraméternek 0 értéket adunk meg, akkor ez az összes szignál vagy sz/ot lecsatlakoztatását jelenti. A szignált kibocsátó objektum nem tud arról, hogy a szignálhoz milyen szlotfüggvények csatlakoztak, illetve csatlakoztak-e egyáltalán, ezzel tehát nem kell foglalkozni a szignál kibocsátásakor. Előfordulhat, hogy egy korábban öszszekapcsolt szlotfüggvényt tartalmazó objektumot időközben már töröltünk a memóriából. A Qt keretrendszer gondoskodik róla, hogy a törölt objektum destruktorában szétkapcsolja az objektum szlotjaihoz tartozó kapcsolatokat, így ez nem okoz hibát a futás során.

    8.3.5. A Meta Object Compiler A szignál-szlot mechanizmus tárgyalásakor számos olyan új nyelvi elemmel találkoztunk a forrásállományokban (például slots, signals, emit), amelyek nem felelnek meg a C++ nyelv szintaxisának. Továbbá a szignálokat a függvényekhez hasonló szintaktikával deklaráltuk, implementációt mégsem írtunk hozzájuk. A Qt keretrendszer egyrészt makrókat biztosít a fenti kulcsszavak helyettesítésére, így a C++-fordító számára értelmezhető lesz kód, másrészt egy előfordítót használ, amellyel az osztályhoz kiegészítő kódot generál. Ez az előfordító a korábban már bemutatott moc, amely feldolgoz minden olyan headerállományt, amelyben a Q_OBJECT makró szerepel. A feldolgozás során ezekhez a headerállományokhoz egy moc_.cpp nevű állományt készít. Ez tartalmazza az osztályokhoz tartozó metainformációkat és a szignálok implementációját. A linkeléskor az általunk írt forráskódok mellett ezeket az állományokat is fel kell használni. A moc használata a qmake programmal teljesen áttekinthetően történik, így a fenti fordítási lépésekkel közvetlenül nem kell foglalkoznunk A fentiek illusztrálására nézzük meg, hogy milyen lépésekből áll az előző WatchDog elnevezésű programunk fordítása. (Az itt bemutatott kimenet nem teljes, a könnyebb olvashatóság kedvéért a fordító hívásakor megadott kapcsolók közül csak az állományok nevére vonatkozókat hagytuk benne, így hiányoznak például a belinkelt programkönyvtárak.) g++ -c -o main.o main.cpp g++ -c -o textcontent.o textcontent.cpp g++ -c -o contentwatcher.o contentwatcher.cpp /usr/bin/moc-qt4 textcontent.h -o moc_textcontent.cpp g++ -c -o moc_textcontent.o moc_textcontent.cpp /usr/bin/moc-qt4 contentwatcher.h -o moc_contentwatcher.cpp g++ -c -o moc_contentwatcher.o moc_contentwatcher.cpp g++ -o watchDog main.o textcontent.o contentwatcher.o moc_textcontent.o moc_contentwatcher.o

    458

    8.4. Ablakok és vezérlők

    A cpp állományokat a fordító hagyományosan fordítja le. A moc segítségével minden headerállományból előállítunk egy másik cpp forrásállományt, ezeket ugyancsak lefordítjuk, majd a linkeléskor felhasználjuk, és beépítjük a végleges programba.

    8.4. Ablakok és vezérlők Az alábbiakban a grafikus felhasználói felület programozását vizsgáljuk meg részletesebben. A Qt keretrendszerben a felhasználói felületen megjelenő vezérlők mindegyike a QWidget ősosztályból származik. Ez az osztály kezeli az ablakozó rendszertó) érkező felhasználói eseményeket, továbbá az általános ablakjellemzőket, és nyilvántartja a szülő-gyermek viszonyokat. Az ablak eseményei lehetnek méretváltozással vagy áthelyezéssel kapcsolatosak, valamint a felhasználó beavatkozásának az eredményei. Az eseménykezelés a szignál-szlot mechanizmuson alapul. A vezérlőket az első példában ismertetett elrendezéskezelő objektumok segítéségével általában automatikusan rendezzük el egy szülőn belül, így a felületek könnyen tudnak alkalmazkodni az ablak átméretezéséhez. A vezérlőket szigorú fastruktúrába rendezzük, az a vezérlő, amelynek nincsen szülője, önálló ablakként jelenik meg. A gyermek mindig a szülő felületén helyezkedik el, a szülőwidget által behatárolva. Az ablakoknak alapvetően kétféle típusa van: a dialógusablakok és a hagyományos alkalmazásablakok. Egy grafikus felülettel rendelkező alkalmazásnak tipikusan van egy főablaka, ahol eszközsorokat és menüket helyezünk el — itt tudjuk szerkeszteni a megnyitott dokumentumokat —, és több dialógusablaka, amelyek kiegészítik a főablak funkcióit. A dialógusablakok a QDialog osztályból származnak. A dialógusablakokat az egyszerűbb felületek megjelenítésére használjuk, van úgynevezett visszatérési értékük. A visszatérési értéket a dialógusablak bezárásakor állíthatjuk be, ez utólag lekérdezhető, illetve van olyan blokkoló függvény (exec), amellyel megjeleníthetjük a dialógusablakot, és ennek valóban ez lesz a visszatérési értéke. A főablakot reprezentáló osztályok a QMainWindow ból származnak. Ez az osztály saját alapértelmezett elrendezéssel rendelkezik, menüket, státuszsort, eszközsorokat lehet benne elhelyezni. Rendelkezik továbbá olyan területekkel, ahová lebegő ablakokat lehet dokkolni, és egy olyan központi területtel, amelybe tetszőleges vezérlőt lehet elhelyezni. Ha nincsen szükségünk a Qt által biztosított ablakfunkciókra (például eszközsávok, menüsáv), akkor bármelyik vezérlőt használhatjuk főablaknak (lásd az első példában is, ahol a QWidget egy példányát jelenítettünk meg). -

    459

    8. fejezet: A Qt keretrendszer programozása

    8.4.1. Dialógusablakok készítése A dialógusablakokat kétféleképpen használhatjuk. Az egyik, amíg látható, blokkol minden hozzáférést a szülőablakhoz, a másik esetben nem korlátozza semmiben a felhasználót, a szülőablakot is használhatjuk. Az első változat a modális dialógusablak (modal dialog), a második a nem modális dialógusablak (modeless dialog). A választás, hogy modális vagy nem modális dialógusablakot használjunk-e, főként a dialógusablak funkciójától függ. Ha a programban semmi hasznosat nem tehetünk, amíg valamilyen adatot meg nem kapunk, vagy a felhasználó nem nyugtáz valamilyen információt, akkor célszerű modális dialógusablakot használni. Például egy állománynév kiválasztására is modális dialógusablakot szoktunk megjeleníteni. Ezzel szemben a keresés funkcióját általában nem modális dialógusablakként valósítjuk meg, hiszen közben szerkeszthetjük a dokumentumot. Egy modális dialógusablakot a QDialog osztály exec függvényével jelenítünk meg. Ilyenkor az alkalmazás eredeti eseménykezelő ciklusa blokkolódik, és a dialógusablak új ciklust indít. Ez a ciklus blokkolja a más ablakokhoz érkező felhasználói eseményeket, de továbbítja például az újrarajzolást kérő üzenetet. Amikor a felhasználó bezárja a dialógusablakot, akkor leáll az újonnan indított eseménykezelő ciklus, visszatér az exec függvény, és így az eredeti ciklus folytatja a működését. A modális dialógusablakokat tipikusan lokális változóként hozzuk létre, általában valamilyen esemény kezelésére írt szlotfüggvényben, így amikor a vezérlés elhagyja az adott függvényt, a dialógusablak felszabadul. A nem modális dialógusablakok osztoznak az eseménykezelő cikluson a többi ablakkal, így a létrehozás után nem blokkolódhat a létrehozó szál egy olyan eseménykezelő szlotban, amelyben létrehozták, és nem is szabad törölni az objektumot, mert a függvényből való visszatérés után is elérhető kell, hogy maradjon. Ezért a nem modális dialógusablakokat tipikusan az alkalmazás főablakának tagváltozójaként definiáljuk, a konstruktorban hozzuk létre, és valamilyen esemény hatására szlotból jelenítjük meg vagy rejtjük el. A megjelenítéshez az exec helyett a show függvényt használjuk. Ez az exec-kel szemben a dialógusablak megjelenítése után azonnal visszatér, és nem vár az ablak bezárására. A show továbbá nem indít külön eseménykezelő ciklust az új ablak számára, az ide érkező üzeneteket az alkalmazás főablakának az eseménykezelője kapja, és továbbítja a dialógusablakhoz. Ezért, ha nem modális dialógusablakot jelenítünk meg a show függvénnyel, akkor mindenképpen kell már futnia eseménykezelő ciklusnak, ezt a QApplication osztály exec függvényével indíthatjuk el. Feladat Írjunk egyetlen dialógusablakból álló alkalmazást, amelyben egy nevet és egy e-mail címet tudunk megadni. A dialógusablakból a „Rendben" és a „Mégsem" nyomógombokkal tehessen kilépni, a bezárásüzenetben mutassuk meg a dialógusablakba begépelt adatokat, és írjuk ki, hogy melyik gombbal léptünk ki.

    460

    8.4. Ablakok és vezérlők

    A következő példa dialógusablakában (8.3. példa) tehát egy ismerősünk nevét és e-mail címét adhatjuk meg. Ezen a példán bemutatjuk, hogyan kell saját dialógusablak-osztályt írni, és hogyan kell az elrendezéskezelő objektumok segítségével elhelyezni a vezérlőket. Uj bejegyzes

    8.3. ábra. QDialog-alapú felület név és e-mail cím beírására

    Saját dialógusablak készítésekor leszármaztatunk a QDialog ősosztályból, azoknak a további létrehozott vezérlőknek, amelyeket meg akarunk jeleníteni, mindig a this mutatóval elért aktuális példányt adjuk meg szüló'ként, így ezek a dialógusablakon belül jelennek meg. /* contactdialog.h */ #ifndef CONTACTDIALOG_H #define CONTACTDIALOG_H #include class ContactDialog : public QDialog {

    Q_OBJECT public: ContactDialog(QWidget *parent = 0); QString getName(); QString getEmail(); private: QGroupBox *group; QLabel *lblName; QLabel *lblEmail; QLineEdit *txtName; QLineEdit *txtEmail; QPushButton *btn0k; QPushButton *btnCancel; QGridLayout *grid; QVBoxLayout *mainLayout; QHBoxLayout *buttonLayout; } ;

    #endif // CONTACTDIALOG_H

    461

    8. fejezet: A Qt keretrendszer programozása

    A ContactDialog osztály deklarációjában tagváltozóként tároljuk a dialóguson megjelenő összes vezérlőt. A group nevű tagváltozó egy QGroupBox típusú mutató. Ez megjelenít címmel egy keretet, a kereten belülre pedig tetszőleges további tartalmazott vezérló'ket helyezhetünk. A QLabel osztály segítségével egy szöveget vagy egy képet tudunk megjeleníteni, a fenti példában a „Név" és „E-mail" címkék megjelenítésére használjuk. A QLineEdit osztállyal egy egysoros szövegdobozt jeleníthetünk meg, amelybe a felhasználó tetszőleges szöveget gépelhet. Az egyszerű nyomógombok létrehozására a Qt keretrendszerben a QPushButton osztály használható: két ilyen típusú tagváltozót definiálunk a két nyomógomb számára. A további három tagváltozó a vezérlők elrendezését segítő objektumokat reprezentálják. A QGridLayout négyzetrács alakban helyezi el az elemeket, ezek használatakor minden vezérlőhöz meg kell adni, hogy a négyzetrács melyik cellájába tartozik. A QVBoxLayout a hozzátartozó vezérló'ket függőlegesen egymás alá helyezi, míg a QHBoxLayout vízszintesen egymás mellé. A fenti objektumok segítségével a következőképpen helyezzük el az elemeket: •

    a QGridBoxLayout négyzetrácsba rendezi a QGroupBoxon belüli négy darab vezérlőt, nevezetesen a két feliratot és a két szövegdobozt;



    a QHBoxLayout csak a két gombot rendezi el vízszintesen egymás mellé;



    végül a QVBoxLayout objektum segítségével a QGroupBoxot, illetve a gombokat tartalmazó vízszintes sávot helyezzük el egymás alatt. Az automatikus elrendezések használatakor definiálhatunk üres területeket is, amelyek átméreteződnek, amikor az ablak mérete megváltozik. Az elrendezéskezelő objektumok szerepét és működését a 8.4. ábra mutatja.

    QGridLayout átméretezhető üres — területek QHBoxLayout

    8.4. ábra. A dialógusablak vezérlőinek elrendezése

    462

    8.4. Ablakok és vezérlők

    A dialógusablaknak van két további függvénye is: lekérdezhető a beírt név és az e-mail cím. Erre szolgálnak a publikus getName és getEmail függvények: contactdialog.cpp */ #include "contactdialog.h" ContactDialog::ContactDialog(Qwidget *parent) : QDialog(parent) {

    this->setWindowTitle("új bejegyzés"); group = new OGroupBox("Adatok"); lblName = new QLabel("Név:"); lblEmail = new QLabel("E-mait:"); txtName = new QLineEdit(); txtEmail = new QLineEdit(); grid = new QGridLayout; grid->addWidget(lblName, grid->addwidget(txtName, grid->addWidget(lblEmail, grid->addwidget(txtEmail, group->setLayout(grid);

    0, 0, 1, 1,

    0); 1); 0); 1);

    btnok = new OPushButton("Rendben"); btnCancel = new QPushButton("Mégsem"); buttonLayout = new QHBoxLayout; buttonLayout->addstretch(); buttonLayout->addwidget(btn0k); buttonLayout->addWidget(btnCancel); mainLayout = new QHBoxLayout; this->setLayout(mainLayout); mainLayout->addWidget(group); mainLayout->addStretch(); mainLayout->addLayout(buttonLayout); connect(btnOk, SIGNAL(clicked()), this, SLOT(accept())); connect(btnCancel, SIGNAL(clicked()), this, SLOT(reject()));

    QString ContactDialog::getName(){ return txtName->text();

    QString ContactDialog::getEmail(){ return txtEmail->text();

    463

    8. fejezet: A Qt keretrendszer programozása

    Az osztály konstruktorában először a QGroupBoxot, illetve az abban található négy vezérlőt hozzuk létre. Az elrendezéskezelő objektumokhoz az addWidget függvénnyel adhatjuk hozzá azokat a vezérlőket, amelyeknek az elrendezéséért felelősek. Ha nem egy vezérlőt akarunk hozzáadni az elrendezéshez, hanem egy másik elrendezéskezelő objektumot, akkor az addLayout függvényt kell használni, mivel ezek az objektumok nem a QWidget osztályból származnak. Az elemek természetesen a hozzáadás sorrendjében helyezkednek el. A QGridLayout használatakor az addWidget függvény meghívásakor meg kell adni, hogy melyik sorba, illetve oszlopba kerüljön a vezérlő (a sorok és oszlopok indexelése 0-val kezdődik). Természetesen arra is van lehetőség, hogy egy vezérlő több egymás melletti cellát foglaljon el, így a cellákat összevonjuk, ezeket a további opcionális paraméterekben lehet specifikálni. A QGroupBox gyermekvezérlőjéből lesz a négyzetrácsban elhelyezett négy másik vezérlő. Ezt a négy vezérlőt pedig a QGridLayout objektum segítségével rendezzük el, ezért a QGroupBox objektumnak meg kell hívnia a setLayout függvényét, paraméterként pedig az QGridLayout példányt kell átadni. Elrendezéskezelő objektumok használatakor a gyermekvezérlőket nem közvetlenül a szülőhöz adjuk, hanem a szülőnek a setLayout függvényével megadjuk először az elrendezéskezelőt, majd ahhoz tesszük hozzá a gyermekvezérlőket a fentiek szerint. A fenti kódban nem adtuk meg expliciten azt, hogy a két címke és szövegdoboz a group gyermekvezérlője legyen. Ezt megtehettük volna úgy, hogy a vezérlők konstruktorainak átadjuk paraméterként a group szülőt. Erre azonban nincsen szükség, mert az elrendezéskezelő objektumok használatakor a setLayout függvény meghívásával a keretrendszer automatikusan inicializálja a szülő-gyermek viszonyokat — ez nagyban egyszerűsíti a kódírást. A következő lépésben a két nyomógombot és az azokat vízszintesen elrendező objektumot hozzuk létre. A Qt keretrendszerben a vezérlők elrendezésének egyik fontos eleme az üres terek kezelése. A fenti példában azt szeretnénk, hogy ha az ablakot vízszintesen átméretezzük, akkor a két nyomógomb mérete ne változzon, hanem mindig az adott sor jobb oldalát foglalják el, a bal oldalon pedig az üres terület szélessége változzon. Az addStretch függvény éppen egy ilyen, automatikusan változó méretű üres területet (spacer) helyez el, mégpedig a két nyomógomb elé, ugyanis ezek hozzáadása előtt hívtuk meg ezt a függvényt. A következő sorokban az előzőkhöz hasonlóan inicializáljuk a függőleges elrendezést biztosító objektumot, a fenti négyzetrács és az alsó gombsor közé pedig ismét egy üres területet helyezünk el A QDialog osztálynak van két szlotja: accept és reject. Ha bármelyik függvényt meghívjuk, az ablak automatikusan bezáródik, és a dialógusablak „eredménye" vagy „visszatérési értéke" a megfelelő konstans QDialog::Accepted vagy QDialog::Rejected lesz. Ezt az értéket a result függvénnyel tudjuk lekérdezni. A két létrehozott gombnak a clicked szignálját elég összekötni a megfelelő szlottal (accept és reject), így ezek automatikusan meghívódnak a gomb megnyomásakor. Természetesen saját eseménykezelő szlotfüggvényt is írhat464

    8.4. Ablakok és vezérlők

    nánk a gombnyomás lekezelésére, és ezen belül mi is bezárhatnánk az abla, kot, illetve beállíthatnák a visszatérési értéket. Előbbit a close, utóbbit a setResult függvénnyel tehetjük meg. A Qt keretrendszer nagyban egyszerűsíti a memóriakezeléssel kapcsolatos feladatokat. A szigorú fastruktúrába rendezett felhasználóifelület-elemek felszabadításáról nem kell külön gondoskodni. Ha egy ablakot bezárunk, akkor automatikusan felszabadítja az összes benne lévő vezérlő számára lefoglalt memóriaterületet. Ezért, habár a ContactDialog konstruktorában a new kulcsszóval területet foglaltunk a tartalmazott vezérlőknek és az elrendezéskezelő objektumoknak, ezeket a hivatkozásokat a destruktorban nem kell külön felszabadítani: /* main.cpp */ #include #include "contactdialog.h" int main(int argc, char * argv[]) QApplication app(argc, argv); ContactDialog * dlg = new contactpialog(); dlg->exec(); QMessageBox msg; msg.setwindowTitle("Eredmény"); QString message = "Név: " % dlg->getName() % email: " % dlg->getEmail() % ( dlg->result() == QDialog::Accepted ? " (jóváhagyva)" " (nincs jóváhagyva)"); msg.setText(message); msg. show(); return app.exec(); }

    Ahhoz, hogy a dialógusablakot ki tudjuk próbálni, szükség van egy main függvényre. A fenti forrásállomány első sorában a QApplication példányt inicializáljuk, majd létrehozunk egy példányt a korábban definiált dialógusablakból. A dialógusablakot az exec függvénnyel jelenítjük meg modálisan. Az exec nem tér vissza, amíg a dialógusablak eseménykezelő ciklusa fut, vagyis amíg az ablakot be nem zárjuk. Ha az ablakot nem modálisan szeretnénk megjeleníteni, akkor a show függvényt kellene használni. Erre a későbbiekben mutatunk még példát. A dialógusablak bezárása után létrehozunk egy szövegdobozt (QMessageBox osztály), amelybe a dialógusablak visszatérési értékétől függő üzenetet jelenítünk meg. A QMessageBox szintén egy beépített dialógusablak-típus.

    465

    8. fejezet: A Qt keretrendszer programozása

    A main függvény utolsó sorában az app.exec() hívásra azért van mindenképpen szükség, mert nem modális dialógusablakot is használunk (msg), amely nem rendelkezik saját eseménykezelő ciklussal. Ebben a példában tehát bemutattuk, hogyan tudunk olyan alkalmazást írni, amelynek a főablaka egy dialógusablak. Sokszor a célunknak ennyi is megfelel. A saját alkalmazásablakokat bemutató fejezetekben részletesen megvizsgáljuk azt, hogyan érdemes egy főablakkal rendelkező alkalmazásban a modális és a nem modális dialógusablakokat használni.

    8.4.2. A Qt vezérlőkészlete A Qt keretrendszer egyik erőssége a nagyszámú testre szabható vezérlőt tartalmazó osztálykönyvtár. A szokásos egyszerűbb vezérlők mellett találhatók összetettebb vezérlők, és rendelkezésre állnak a szokásos dialógusablakok is. A 8.2. táblázatban csak néhányat sorolunk fel a legfontosabb vezérlők közül. 8.2. táblázat. A Qt keretrendszer legfontosabb vezérlői

    Vezérlők neve

    Leírás

    Egyszerűbb vezérlők QLineEdit, QTextEdit, QDateEdit, QDateTimeEdit, QTimeEdit

    Beviteli mezó'k szövegekhez, dátumokhoz és időpontokhoz

    QCheckBox, QRadioButton

    Jelölőnégyzet- és választónégyzet-osztályok

    QComboBox

    Legördülő lista

    QFontComboBox

    Betűtípus választó lista

    QLabel

    Egyszerű szöveg vagy kép megjelenítése

    QMenu

    Menük megjelenítése

    -

    Összetettebb vezérlők QCalendarWidget

    Naptárat megjelenítő vezérlő

    QListWidget

    Elemeket listanézetben megjelenítő vezérlő

    QTableWidget

    Elemeket táblázatnézetben megjelenítő vezérlő

    QTreeWidget

    Elemeket fastruktúrában megjelenítő vezérlő

    QWebView

    Webes dokumentumok megjelenítésére és szerkesztésére használható vezérlő

    466

    8.4. Ablakok és vezérlők

    Vezérlők neve

    Leírás

    Dialógusablakok

    QColorDialog

    Színválasztó dialógusablak

    QFileDialog

    Állományok és könyvtárak kiválasztására használható dialógusablak

    QFontDialog

    Dialógusablak betűtípus beállításainak szerkesztésére

    QlnputDialog

    Egy darab egyszerű típusú adat beolvasása a felhasználótól (a beolvasandó típus lehet szöveg, szám vagy egy előre megadott lista egyik eleme)

    QMessageBox

    Üzenet megjelenítése a felhasználó számára

    A fenti táblázatban felsorolt vezérlők mindegyike hasonlóan használható. Mindegyik a QWidgetból származik, így példányosítás után bármilyen szülőwidgeten elhelyezhetők, vagy önálló ablakként is megjeleníthetők. Mindegyik vezérlő szignálokat publikál, amelyekkel jelzi, hogy valamilyen változás történt az állapotában. A szignálok természetesen az adott vezérlő típusának megfelelő eseményeket jelzik, a QCheckBox például a kijelölési állapot megváltoztatásának jelzésére tartalmaz szignált, a QListWidet pedig arra, hogy megváltozott az éppen kijelölt elem a listában. Természetesen minden vezérlő a típusának megfelelő tulajdonságokkal rendelkezik, amelyek a megjelenítését befolyásolják. Ezeknek a tulajdonságoknak a lekérdezésére és módosítására a megfelelő függvények rendelkezésre állnak. Ha egy konkrét vezérlőt használunk a programozás során, érdemes használat előtt átnézni a dokumentációban azt, hogy milyen függvényekkel szabhatjuk testre a kinézetét, és milyen eseményekről küld szignálokat a vezérlő.

    8.4.3. Saját alkalmazásablakok A QMainWindow osztály segítésével definiálhatjuk egy alkalmazás szokásos felhasználói felületét, amelyen menük, eszköztárak és státuszinformáció jelennek meg, találhatók továbbá lebegő és dokkolható ablakok. A QMainWindow osztály felülete előre meghatározott területekre van felosztva, mindegyik területre meghatározott típusú vezérlőket tudunk elhelyezni. A felosztás a 8.5. ábrán látható: •

    menüsor (menu bar) és státuszsor (status bar) az ablak tetején és alján,



    eszköztárak (tool bar) helye körben az ablak szélein,



    lebegőablakok (floating window) dokkolására használható terület az eszköztárak mellett, illetve



    a központi vezérlő területe, amely az ablak közepét, a legnagyobb területet tölti ki. 467

    8. fejezet: A Qt keretrendszer programozása

    menüsor eszköztárak lebegő ablakok dokkolásának helye központi vezérlő

    státuszsor

    8.5. ábra. A QMainWindow területének felosztása

    A lebegőablakok általában az alkalmazás főablaka fölött különálló ablakokként helyezkednek el, de lehetőség van ezeket statikusan is elhelyezni, „dokkolni" az ablak valamelyik oldalán. Mind a menüsorból, mind a státuszsorból csak eggyel rendelkezik az ablak. A QMainWindow használatának előnye, hogy beépítetten támogatja a fenti vezérlők elhelyezését. Természetesen ezek közül egyiket sem kell ténylegesen fel is használnunk az alkalmazásunkban. Ám ha egyikre sincsen szükségünk, akkor nem érdemes a QMainWindow-ból leszármaztatni a főablakot. A QMainWindow használatakor választhatunk az SDI (single document interface) és az MDI (multiple document interface) felületek közül, a keretrendszer mindegyik támogatására különböző vezérló'ket biztosít. Az SDI felületen a központi vezérlő területén egyszerre egy dokumentumablak jelenik meg. Ha egy másik dokumentummal akarunk dolgozni, akkor az elsőt be kell zárni, és ezután tudunk csak újabb ablakot megnyitni. Az MDI esetben viszont egyszerre több dokumentumablakon is dolgozhatunk, vagyis a központi területen egyszerre több — saját fejléccel, kerettel rendelkező, átméretezhető — ablak is lehet megnyitva. Ezeknek az átméretezhetőségét, mozgatását a QMainWindow biztosítja. Az SDI esetében a központi területre bármilyen vezérlőt elhelyezhetünk, ez tölti ki a rendelkezésre álló teljes területet. Az MDI esetében a központi vezérlő helyére egy QMdiArea típust kell elhelyezni, amelyhez az addSubWindow függvénnyel már tetszőleges vezérlőt hozzáadhatunk. Minden egyes ilyen vezérlő külön ablakként jelenik meg. Látható, hogy az SDI felület programozásához semmilyen speciális vezérlőt nem kell használni, egy MDI felület megvalósítása azonban bonyolultabb, erre a dokumentum-nézet architektúra tárgyalásakor mutatunk példát. 468

    8.4. Ablakok és vezérlők

    8.4.4. A főablak programozása A továbbiakban megvizsgáljuk, hogy milyen függvényekkel tudunk hozzáférni a főablak menü-, eszköz- és státuszsoraihoz, és hogyan tudjuk őket az akciókkal összekapcsolni. Egy alkalmazásban általában vannak olyan funkciók, amelyek a felület több pontjáról is elérhetők, például a menüsor vagy az eszköztár egyik eleméról, illetve valamilyen billentyűkombináció segítségével. Általában ilyen például az „Új dokumentum" funkció. A felhasználó szemszögéből csak az adott funkciónak van jelentősége, és érdektelen, hogy éppen melyik módon hívta meg. Egy ilyen funkciót az úgynevezett akcióobjektummal írunk le, ez a QAction osztály egyik példánya. Az akció (action) fogja össze az ugyanannak a funkciónak a meghívásához szükséges különböző eseményeket és a funkciót kezelő szlotfüggvényt, valamint a funkció leírását szövegekkel és képekkel. Egy már létrehozott akciót hozzáadhatunk egy menüsorhoz vagy egy eszköztárhoz, avagy egyszerűen magához az ablakhoz, ha csak billentyűkombinációval szeretnénk elérni. Mindegyik esetben az adott vezérlő (menüsor, eszköztár, QMainWindow) addAction függvényét használjuk az akció megjelenítéséhez. Egy QAction létrehozásakor megadható ikon, menüszöveg, gyorsbillentyű, státuszszöveg, súgószöveg (tooltip). A QAction publikál egy triggered szignált, ezzel kapunk értesítést, ha a felhasználó az adott funkciót meghívta függetlenül attól, hogy a felhasználói felület melyik elemén keresztül aktiválódott. Amikor szeretnénk egy menüt létrehozni, a menuBar függvénnyel lekérdezzük az ablaktól a menüsorát. A menüsor valójában a legelső lekérdezéskor inicializálódik, későbbi hívásokkor ugyanazt az objektumot adja vissza, így ha nem akarunk menüt létrehozni, és nem próbálunk meg hozzáférni, akkor nem is jön létre. Hasonlóan működnek az eszköztárak és a státuszsor is. A QMainWindow osztály menuBar metódusának visszatérési értéke egy QMenuBar objektum, amely az ablak menüsorát írja le. A menüsorhoz az addMenu függvénnyel adhatunk hozzá újabb menüelemeket. Ezek a menüpontok a QMenu osztály példányai. Az új példányt magával az addMenu függvénnyel is létrehozhatjuk, ekkor csak egy szöveget kell átadni a menüpont létrehozásához: #include QMenu * QMenu::addMenu ( const QString & title ) QMenu * QMenu::addMenu ( const Qlcon & icon, const QString & title)

    A QMenu addAction függvényével egy akciónak megfelelő QAction objektumot adhatunk hozzá a menühöz. Ennek a szövege pedig egy további menüpontként jelenik meg az eredeti menün belül. Lehetőség van arra, hogy a QAction példányt az addAction függvényhívással együtt hozzuk létre, ekkor csak a megjelenítendő menüpont címét kell átadni.

    469

    8. fejezet: A Qt keretrendszer programozása QAction * QMenu::addAction ( const QString & text ) QAction * QMenu::addAction ( const Qicon & icon, const QString & text )

    Az alábbi kódrészlet először létrehoz egy „Fájl" menüt a menüsoron, majd inicializál egy QAction példányt, ennek szövege a „Kilép" lesz. Ezután a menüben megjeleníti az akciót: leMenu = thi s->menuBar()->addmenu ("Faji ") ; exitAction = new QAction("Ki lép", thi s); filemenu->addAction(exi tActi on);

    QMenu * fi

    A menüsorhoz hasonlóan működnek az eszköztárak is, de azokból — a menüsorral ellentétben — több is lehet. Az ablak addToolBar függvényével hozhatunk létre egy QToolBar típusú eszköztárat, amelyre szintén az addAction függvénnyel tudjuk az akcióknak megfelelő elemeket elhelyezni. Státuszsorból is csak egy van az alkalmazáson belül, ezt a statusBar függvénnyel érjük el. A státuszsoron háromféle információt tudunk megjeleníteni: ideiglenes szöveget (temporary), általános (normal) és állandó (permanent) információt. Az általános és az állandó információ nem csak szöveg lehet, ezek me gj elenítéséhez saját QLabel objektumokat kell létrehozni, és ezeket az addWidget függvénnyel kell a státuszsorhoz adni. Lehetőség van több QLabel hozzáadására is. Az állandó szöveg egészen addig látható, amíg a removeWidget függvénnyel a megfelelő QLabel vezérlőt el nem távolítjuk. Az ideiglenes szöveget tipikusan valamilyen üzenet rövid idejű megjelenítésére alkalmazzuk, erre a showMessage függvényt használjuk, amelynek a megjelenítendő szöveg mellett megadhatjuk, hogy mennyi ideig legyen látható. Az ideiglenes szöveg, eltakarhatja a normál információt, de az állandó információ mindig látható marad, amíg programozottan el nem tüntetjük. Feladat Írjunk egy olyan alkalmazást, amelyben ismerőseink címlistáját (név és e-mail cím) tárolhatjuk. A főablakban jelenítsük meg a lista adatait táblázatos formában. Készítsünk dialógusablakokat, amelyekkel új bejegyzést vehetünk fel, illetve kereshetünk név szerint a listában. Az elsőt modálisan, a másodikat nem modálisan jelenítsük meg.

    Új bejegyzés létrehozására az előző példában bemutatott ContactDialog dialógusablakot használjuk. Írunk továbbá egy SearchDialog osztályt is, amely ugyancsak egy dialógusablak, és a keresést teszi lehetővé. Az ablakban megjelenítünk egy „Fájl" menüt, amelyben három menüpont lesz: „keresés", „új bejegyzés" és „Kilép". Ezen kívül használjuk a státuszsort is a felhasználónak szánt üzenetek megjelenítésére. Az alkalmazás felülete a 8.6. ábrán látható.

    470

    8.4. Ablakok és vezérlők

    8.6. ábra. Címlista-alkalmazás képernyőképe /* contactswindow.h */ #ifndef CONTACTSWINDOW_H #define CONTACTSWINDOW_H #include ‹QtGui> #include "contactdialog.h" #include "searchdialog.h" class Contactswindow : public Qmainwindow { Q_OBJECT public: explicit ContactsWindow(QWidget *parent = 0)

    ;

    signals: public slots: void addNewContact(); void updateStatusText(); void search(Qstring expression); void showSearch(); protected: void closeEvent(QCloseEvent *event); private: QAction *exitAction; QAction *newContactAction; QAction *showSearchAction;

    471

    8. fejezet: A Qt keretrendszer programozása QMenu *fi 1 emenu ; QTableWi dget * table;

    SearchDialog * searchDi al og ; // Nemodál i s , ezért tagváltozó } ,

    #endif // CONTACTSWINDOW_H

    A ContactsWindow osztály a QMainWindow-ból származik, és a Q_OBJECT makróval kezdődik, hiszen saját szlotokat definiálunk. A closeEvent az ősosztályban definiált virtuális függvény, amely azelőtt hívódik meg, mielőtt az ablakot bezárjuk. Ezt a függvényt felüldefiniáljuk, így kapunk értesítést az ablak bezárásáról. A függvényen belül rá tudunk kérdezni arra, hogy a felhasználó valóban ki akar-e lépni. A felüldefiniálást biztosító függvényen belül lehetőségünk van a bezárás megszakítására is. Az osztály tagváltozói közül az első három akciókat ír le. Az exitAction segítségével az ablakot lehet bezárni. Ezt a funkciót a menüből is el lehet érni, illetve egy billentyűkombinációt is rendelünk hozzá. A második akció a newContactAction. Ezzel jelzi a felhasználó, hogy egy új bejegyzést szeretne létrehozni, ilyenkor modálisan megjelenítünk egy ContactDialog típusú dialógusablakot. Ez az akció szintén a menüsorból, illetve egy billentyűkombinációval érhető el. A harmadik a showSearchAction, amellyel a kereső dialógusablakot jelenítjük meg. Ezt nem modálisan tesszük, mivel folyamatosan elérhető lesz, miközben az alkalmazás főablakában is dolgozunk. Több szlotfüggvényt is definiálunk. Az addNewContacttal a newContactAction akció bekövetkezésétől kezdve kapunk értesítést, az updateStatusTexttel pedig arról, ha a táblázatban új elemet jelölünk ki. A showSearch szlottal a kereső ablakot jelenítjük majd meg (ezt a showSearchAction akción keresztül érjük el). Végezetül a search szlotfüggvénnyel kapunk értesítést arról, hogy egy bizonyos szövegre kell rákeresni a megjelenített adatok között. A fileMenu változóval érjük el a „Fájl" menüt leíró objektumot. Arra a menüsorra, amelyben a saját menünk is megjelenik, nem tárolunk külön referenciát, mert azt mindig le tudjuk kérdezni a menuBar függvénnyel. A table változó egy QTableWidget típusra mutat, amellyel egyszerű elemekből álló táblázatot tudunk megjeleníteni. A táblázat celláiban QTableWidgetltem típusú elemek vannak, egy-egy elemben többféle információt is tárolhatunk, de ebben a példában mi egyszerű szöveget jelenítünk meg. Végezetül a searchDialog tagváltozó egy SearchDialog típusú dialógusablakra mutat. Ezt a dialógusablakot nem modálisan jelenítjük meg, így fontos, hogy az alkalmazásablakban mindig elérhető legyen. ContactDialog objektumra viszont nem tárolunk referenciát• lokális változóként hozzuk majd létre, amikor meg akarjuk jeleníteni. A modális dialógusablakot mindig a megjelenítés előtt hozzuk létre, majd bezárása és az bekért adatok feldolgozása után rögtön töröljük, mert később már nem lesz rá szükség.

    472

    8.4. Ablakok és vezérlők

    /* contactswindow.cpp */ #include #include #include #include

    "contactswindow.h" "contactdialog.h"



    contactswindow::contactswindow(Qwidget *parent) : Qmainwindow(parent) { this->setwindowTitle(tr("Címlista")); fileMenu = this->menuBar()->addmenu(tr("&Fájl")); exitAction = new QAction(tr("&Kilép"), this); exitAction->setShortcut(tr("Ctrl+Q")); exitAction->setStatusTip(tr("Alkalmazás bezárása")); connect(exitAction, SIGNAL(triggered()), this, SLOT(close())); fileMenu->addAction(exitAction); newContactAction = new QAction(tr("&új bejegyzés"), this);

    newContactAction->setShortcut(OKeySequence::New); newContactAction->setStatusTip(tr("Új bejegyzés felvétele")); connect(newContactAction, siGNAL(triggered()), this, SLOT(addNewContact())); filemenu->addAction(newContactAction); showsearchAction = new QAction(tr("Keresé&s"), this); showSearchAction->setShortcut(OKeysequence::Find); showSearchAction->setStatusTip(tr("Keresés név alapján")); filemenu->addAction(showsearchAction); connect(showsearchAction, SIGNAL(triggered()), this, SLOT(showsearch())); this->table = new QTablewidget; table->setSelectionmode(QAbstractItemview::SingleSelection); table->setColumnCount(2); this->setcentralwidget(table); QStringList headerLabels; headerLabels«"Név"«"E-mail"; table->setHorizontalHeaderLabels(headerLabels); connect(table, SIGNAL(itemSelectionChanged()), this, SLOT(updateStatusText())); searchDialog = new Searchoialog; connect(searchDialog, SIGNAL(searchinvoked(QString)), this, SLOT(search(Qstring))); } void ContactsWindow::showSearch(){ if (!searchoialog->isVisible()) this->searchDialog->show();

    473

    8. fejezet: A Qt keretrendszer programozása

    void ContactsWindow::search(QString expression){ int currentlySelectedRowNbr = -1; QList selecteditems = table->selecteditems(); i f (selecteditems.length() > 0) currentlySelectedRowNbr = selectedItems.first()->row(); int i; bool found = false; for (i = currentlySelectedRowNbr + 1; i < table->rowCount(); i++) { if (table->item(i, 0)->text().contains(expression)) { table->clearSelection(); table->item(i, 0)->setSelected(true); found = true; break; } }

    if (!found){ table->clearSelection(); } }

    void Contactswindow::closeEvent(QCloseEvent *event) {

    QMessageBox msg(this); msg.setwindowTitle(tr("Kilépés")); msg.setText(tr("Tényleg kilép?")); msg.setStandardButtons(QMessageBox::Yes 1 QMessageBox::Cancel); i f (msg.exec() == QmessageBox::Yes) event->accept(); el se event->ignore(); }

    void contactswindow::addNewContact(){ ContactDialog * newContactDialog = new ContactDialog(this); i f (newContactDialog->exec() == Wialog::Accepted) {

    int currentRowsNbr = table->rowCount(); table->insertRow(currentRowsNbr); QTablewidgetItem * newNameitem = new QTablewidgetItem(newContactDialog->getName()); table->setitem(currentRowsNbr, 0, newNameItem); QTablewidgetItem * newEmailitem = new QTableWidgetItem(newContactDialog->getEmail()); table->setItem(currentRowsNbr, 1, newEmailitem); }

    delete newContactDialog; }

    474

    8.4. Ablakok és vezérlők

    void contactswindow::updatestatusText() {

    Qtist selecteditems = table->selecteditems(); if (selecteditems.length() > 0) this->statusBar()->showmessage( "Name: " % table->item(selecteditems.first()->row(), 0)->text() ) ; }

    Az osztály implementációs állományában először a konstruktort írtuk meg. A setWindowTitle függvénnyel állítjuk be az ablak fejlécében megjelenő címet. Ezután következik a „Fájl" menü inicializálása A menuBar függvénnyel kérjük el a referenciát az alkalmazásablak menüsorára, és mivel ez az első hívása a függvénynek, itt jön létre maga a menüsorobjektum. Nem kell külön létrehozni QMenu objektumot és azt hozzáadni a menüsorhoz, ez megtehető egy lépésben az addMenu függvénnyel, ezért csak a menüpont címét adjuk át. A példában a „&Fájl" szövegkonstans egy tr függvényhíváson belül található. A tr függvény a QObject osztály egy metódusa, és a felület lokalizálhatóvá tételéhez használjuk. Ha egy külön állományban lefordítottuk az adott szöveget egy másik nyelvre, akkor a tr függvény — a felület nyelve alapján — ezt a fordítást adja vissza az eredeti szövegkonstans helyett. Arra, hogy miként lehet egy adott nyelvű fordítást megadni, a következő fejezetben térünk vissza, itt csak az a fontos, hogy a tr függvénybe ágyazva jelezni kell, hogy az adott szöveget lokalizálhatóvá akarjuk tenni. Természetesen, ha biztosan tudjuk, hogy később nem akarjuk lokalizálhatóvá tenni az alkalmazásunkat, akkor a tr függvényhívás elhagyható, és a szöveg önmagában is állhat Az addMenu függvény visszatérési értéke az új menüpontra mutató referencia. A menüpont címében az „F' betű előtti „&" jel az adott menüponthoz tartozó gyorsbillentyűt jelzi, ebben az esetben tehát a „Fájl" menünek az „F' betű lesz a gyorsbillentyűje. Az első akció, amelyet létrehozunk, az exitAction. A konstruktorának átadott szöveg lesz a menüpont címe, amikor hozzáadjuk egy menühöz. Ezenkívül egy gyorsbillentyűt is megadunk a setShortcut függvénnyel. A setStatusTip függvénynek átadunk egy szöveget, amely ideiglenes szövegként megjelenik a státuszsoron, amikor az adott menüpont fölé visszük az egeret. Az akcióhoz lehetne még ikonképet is megadni (seticon), amelyet az eszköztáron helyezhetnénk el. Az akció használatához még két lépés szükséges. Először egy megfelelő szlotot össze kell kötni az akció triggered szignáljával, így értesítést kapunk arról, ha az adott funkciót a felhasználó el akarta érni. Itt az alkalmazásablak close szlotját használjuk, amelynek a meghívásakor bezárul az ablak. Mivel a close szlotot az ősosztály tartalmazza, itt ennek a megírásával nem kell foglalkozni. Másodszor, az akciót el kell helyezni a menüsoron, mert amíg nincsen egy vezérlőhöz sem hozzárendelve, nem lehet elérni. A QMenu osztály addAction függvényével a paraméterként átadott akcióobjektum menüpontként jelenik meg az eredeti menüpontot belül. Ilyenkor a billentyűkombináció is automatikusan működik. 475

    8. fejezet: A Qt keretrendszer programozása

    A newContactAction funkcióval jelenítünk meg egy ContactDialog dialógusablakot. Az exitAction inicializálásának lépéseihez képest ennek a létrehozásánál két különbséget láthatunk. Az első az, hogy a gyorsbillentyűt nem szövegként adjuk át, hanem a QKeySequence osztály StandardKey felsorolástípusának egy értékeként: QKeySequence::New. A QKeySequence osztályban vannak definiálva azok az általános billentyűkombinációk, amelyeket gyakran használunk az alkalmazásokban, ilyen például az „Új dokumentum", a „Nyomtatás", a „Megnyitás" vagy a „Bezárás". Az adott funkcióhoz tartozó alapértelmezett billentyűkombinációk az operációs rendszertől függően változhatnak. Ha nem szövegként definiáljuk az akcióhoz tartozó gyorsbillentyűt, mint az exitAction esetében, hanem a fenti konstansok használatával, akkor a keretrendszer az aktuális platformnak megfelelő alapértelmezett billentyűparancsot használja. Ezzel nagyban növeljük az alkalmazásunk hordozhatóságát. A második különbség az exitActionhöz képest, hogy itt egy saját szlotot használunk az akció eseményének a kezelésére, nevezetesen az addNewContact függvényt, hiszen itt saját alkalmazásspecifikus logikát szeretnénk írni: megjeleníteni egy dialógusablakot. A konstruktor végén létrehozzuk az alkalmazás főablakának központi vezérlőjét (central widget), ez tölti ki az ablak középső részét, amelyet éppen nem foglalnak el az eszközsorok, a menüsor, a státuszsor, illetve az esetlegesen dokkolt lebegőablakok. A központi vezérlő tetszőleges QWidget lehet, itt a QTableWidget típust használjuk. A létrehozás után az ablak setCentralWidget függvényével meg kell adni, hogy ez legyen a központi vezérlő. A QTableWidget konstruktorában most sem kellett megadni a szülővezérlőt, mert a setCentralWidget automatikusan beállítja. A táblázatnak néhány tulajdonságát is testre szabjuk: megadjuk, hogy 2 oszlopból álljon (setColumnCount), hogy egyszerre csak egy cellát lehessen kijelölni (setSelectionMode), és specifikáljuk az oszlopok fejléceinek a címét (setHorizontalHeaderLabels). Ez utóbbi beállításhoz szükség van egy QStringList típusú objektumra, ebben szövegkonstansok listáját lehet hatékonyan tárolni. A QStringList osztály felüldefiniálja a « operátort is, ennek a segítségével kényelmesen tudunk új elemeket hozzáadni a listához (lásd a fenti kódrészletben). Az alkalmazás úgy működik, hogy ha kiválasztunk egy cellát a táblázatban, akkor egy rövid ideig a státuszsoron is kiírjuk az adott sorhoz tartozó nevet. Ehhez egy értesítésre van szükségünk arról, ha a kijelölt cella megváltozik, ezért összekapcsoljuk a tábla itemSelectionChanged szignálját az updateStatusText szlottal, amelyet mi készítünk el A konstruktor végén létrehozzuk a SearchDialog osztály egy példányát, de nem hívunk rajta show metódust, ezért ez nem jelenik meg. Természetesen lehetőség lenne ezt az objektumot magát a dialógusablak első megjelenítése előtt is létrehozni, nem pedig itt a konstruktorban. A SearchDialog definícióját késó'bb mutatjuk be, ám előzetesen megállapítható, hogy publikál egy searchInvoked nevű szignált, amellyel jelezni tudja, hogy valamilyen szövegre akar keresni a felhasználó. A searchInvoked szignál paraméterlistája egy

    476

    8.4. Ablakok és vezérlők

    QStringból án, ez lesz az a szöveg, amelyre keresünk. A search szlotfüggvényt kötjük össze a szignállal. A connect függvényben a SIGNAL és a SLOT makrók használatakor a paraméterlistát is jelölni kell. A konstruktor után jönnek a tagfüggvények implementációi. A showSearch ellenőrzi, hogy látható-e a keresőablak, ha nem, akkor a show hívással megjeleníti. A search szlotfüggvény felelős a keresés elvégzéséért. Példánkban csak a nevekben keresünk, az e-mail címekben nem. Szeretnénk, ha a keresés úgy működne, hogy a dialógusablak által kibocsátott szignálnál az éppen kijelölt cella sora utáni sorokban történjen csak a keresés. Ha a keresés során találunk egy olyan nevet, amelyben szerepel a kifejezés, akkor jelöljük ki az adott cellát. Ha az utolsó sorban sem találunk találatot, akkor egyszerűen megszüntetjük a kijelölést a táblázatban. Így tehát folytonosan tudunk keresni a táblázatban, és minden előfordulást megtalálunk. A fenti logika implementációjához a QTableWidget néhány metódusát is meg kell ismernünk. A selectedltems visszaadja azoknak a cellaelemeknek a listáját, amelyeket ki lettek jelölve. A táblázat inicializálásakor megadtuk, hogy egyszerre maximum egy cella lehet kijelölve, így feltételezhetjük, hogy a visszaadott listának 0 vagy 1 eleme lehet. A QList egy olyan sablonosztály, amellyel adott típusú elemek listáját tudjuk kezelni, ebben az esetben QTableWidgetltem példányok mutatóit. A listán a szokásos műveletek hajthatók végre, például beszúrás, törlés, keresés. A továbbiakban csak a lista méretét visszaadó size függvényre és az első elemmel visszatérő first függvényre lesz szükségünk. A QTableWidget rowCount függvénnyel a sorok száma, az itemmel pedig egy adott sor- és oszlopindexű cellaelem kérdezhető le. A clearSelection függvény, ahogyan a neve is mutatja, megszünteti a kijelölést a táblázatban. Végezetül a QTableWidgetltem setSelection függvényét említjük meg, amellyel egy adott cellát tudunk kijelölni. A closeEvent függvény virtuális, azelőtt hívódik meg, mielőtt az ablak bezáródik. A függvény egyetlen paraméterének típusa QCloseEvent típusú. Ebben a függvényben egy felugró ablakban megkérdezzük a felhasználót, hogy valóban ki akar-e lépni az alkalmazásból. Az ablaknak címet adunk (setWindowTitle), és megadjuk a kiírandó szöveget (setText), majd beállítjuk, hogy milyen gombokat jelenítsen meg Ehhez a QMessageBox osztályban definiált StandardButton felsorolástípus elemeit használjuk fel, jelen esetben a QMessageBox::Yes és a QMessageBox::Cancel konstansokra van szükségünk, hogy a felhasználó az „Igen" vagy „Mégsem" gombok közül tudjon választani. Az ablakot modálisan jelenítjük meg, ezért az exec függvényt használjuk, amelynek visszatérési értéke annak a gombnak a kódja lesz, amellyel bezártuk a szövegdobozt. Ha a felhasználó válasza QMessageBox::Cancel, vagyis meg 'akarja szakítani a kilépést az alkalmazásból, akkor a paraméterként megkapott event változón meghívjuk az ignore függvényt, ezzel megszakítjuk az ablak bezárását. Ha ezt nem hívjuk meg, akkor a closeEvent visszatérése után valóban megtörténik az ablak bezárása.

    477

    8. fejezet: A Qt keretrendszer programozása

    Az addNewContact szlotfüggvény implementációjában először létrehozunk egy ContactDialog példányt, ezután megjelenítjük a dialógusablakot. A dialógusablaknak a visszatérési eredményét a korábbi példában a result függvénnyel kérdeztük le, de ugyanez lesz az exec függvény visszatérési értéke is. Ha a dialógusablakot jóváhagytuk, akkor lekérdezzük a beírt nevet és e-mail címet, majd ezeknek megfelelően létrehozunk két új cellát. A táblázatba az insertRow függvénnyel beszúrunk egy új sort, ennek a sornak a celláit már tudjuk változtatni a setltem függvény segítségével. A másik szlotfüggvény az updateStatusText, ezzel kapunk értesítést arról, ha a táblázatban új elemet választottunk ki. Ismét a táblázat selectedltems függvényét használjuk annak lekérdezésére, hogy van-e kijelölt cella, és ismét kihasználjuk, hogy maximum egy darab cella lehet egyszerre kijelölve. A kijelölt cellához tartozó sor első oszlopából kiolvassuk az aktuális nevet, majd ezt 2 másodpercig megjelenítjük a státuszsoron. Ehhez a QStatusBar osztály showMessage függvényét használjuk, amelynek első paramétere a megjelenítendő szöveg, második pedig a megjelenítés időtartama milliszekundumban. Ezzel az alkalmazásablak elkészült, már csak a dialógusablakok definíciója hiányzik. A ContactDialog kódját a korábbiakban már tárgyaltuk, az alábbiakban a SearchDialogot mutatjuk be: /* searchdialog.h */ #ifndef SEARCHDIALOG_H #define SEARCHDIALOG_H #include class searchDialog : public QDialog { Q_OBJECT public: explicit SearchQialog(Qwidget *parent = 0); signals: void searchinvoked(Qstring expresssion); public slots: void search(); protected: QH8oxLayout * layout; Qi_ineEdit *txtExpression; QPushButton * btnsearch; }

    ;

    #endif // SEARCHDIALOG_H

    478

    8.4. Ablakok és vezérlők

    /* searchdialog.cpp */ #include "searchdialog.h" SearchDialog::Searchpialog(QWidget *parent) : Wialog(parent) { layout = new QHBoxLayout; this->setLayout(layout); txtExpression = new QLineEdit; layout->addwidget(txtExpression); btnSearch = new QPushButton(tr("Keress")); layout->addwidget(btnSearch); connect(btnSearch, SIGNAL(clicked()), this, SLOT(search())); } void Searchoialog::search(){ emit searchInvoked(txtExpression->text());

    A dialógusablak kódja a Qt keretrendszer eddig tárgyalt elemeinek ismeretében nem szorul részletes magyarázatra. A dialógusablakon egy egysoros szövegbeviteli mezőt (QLineEdit) és egy nyomógombot (QPushButton) helyezünk el vízszintesen egymás mellett. A keresésgomb megnyomásakor bocsátja ki a dialógusablak a searchlnvoked szignált. Ezután már csak a main függvényre van szükség, amelyben inicializáljuk az alkalmazást, és megjelenítjük a főablakot: /* main.cpp */ #include "contactswindow.h" #include int main(int argc, char * argv[]) { QApplication app(argc, argv); ContactsWindow * w = new ContactsWindow; w->show(); return app.exec();

    8.4.5. Lokalizáció Azokat az alkalmazásokat, amelyekben lehetővé akarjuk tenni, hogy a felület több különböző nyelven is elérhető legyen, érdemes úgy megtervezni, hogy a különböző nyelvű fordításokat lehetőleg minél kevesebb további programozás nélkül, minél egyszerűbben tudjuk megadni. Az ilyen jellegű tervezést nemzetköziesítésnek (internationalization) nevezzük. Lokalizációnak (localization) hívjuk azt, amikor az alkalmazásunkat egy adott nyelvre lefordítjuk. 479

    8. fejezet: A Qt keretrendszer programozása

    A Qt keretrendszerben ez a folyamat úgy működik, hogy a megjelenített szövegkonstansokat különálló erőforrás-állományokban tároljuk, amelyeket egyenként lefordíthatunk különálló nyelvekre, ezeket a fordításokat pedig mellékeljük a programunkhoz. A lokalizáció tehát abból áll, hogy az erőforrás-állományoknak elkészítjük a különböző nyelvű fordítását (ehhez semmilyen programozói tudás nem kell), illetve gondoskodunk a betöltésükről a programon belül. Az előző példában említettük, hogy azokat a szövegkonstansokat, melyeket lokalizálni szeretnénk, külön meg kell jelölni a forráskódban. Erre a QObject osztályban definiált tr függvényt használjuk: QSt ri ng QObject: : t r ( const char sourceText, const char * disambiguation = 0, int n = -1 )

    A tr függvény első paramétere az alapértelmezett szöveg, a további paraméterek opcionálisak. Előfordulhat, hogy ugyanazt a szöveget az alkalmazás két különböző helyén máshogyan akarjuk lefordítani, ilyenkor a két kontextust meg akarjuk különböztetni, erre szolgál a második paraméter. Ha a szöveg és a kontextus is megegyezik, akkor ugyanaz lesz a fordítás is. A harmadik, int típusú paraméternek a többes szám képzésében van szerepe. Ha az eredeti szövegkonstans tartalmazza a „%n" szöveget, akkor ezt a végleges szövegben a harmadik paraméter (n) értékével helyettesítjük. A fordításnál megadható az adott kifejezés többes számú és egyes számú változata is, az n értékétől függően a megfelelőt használja fel a keretrendszer. A lokalizálandó szövegek megjelölése után a következő lépés a lehetséges fordítások elkészítése. A különböző nyelvű fordításokat különálló fájlokból olvassa be a rendszer. Minden nyelvhez létre kell hozni egy .ts kiterjesztésű xml állományt. Az állomány minden egyes szöveg-kontextus pároshoz tartalmaz egy fordítást. Az xml állomány vázát automatikusan generálhatjuk a Qt fordítóprogramjával, ennek neve lupdate. Ha létre szeretnénk hozni egy ilyen állományt, a projektállományban ezt jeleznünk kell: TRANSLATIONS += ContactsApplication_en_US.ts

    A fenti sor jelzi, hogy egy ContactsApplication_en_US.ts nevű állományban szeretnénk fordítást készíteni az alkalmazásunkhoz. A fájl nevének a végződése konvenció szerint a fordítás nyelvét jelzi, de ez nem kötelező, tetszőleges fájlnevet is használhatunk. Természetesen több különböző fordítást is készíthetünk, ekkor több nevet kell megadni. Ezután kell a projekt állománynevével meghívni az lupdate programot, például: lupdate ContactsApplication.pro

    480

    8.4. Ablakok és vezérlők

    A program legenerálja a megfelelő xml állomány vázát. Az lupdate bejárja a teljes forráskódot, és megkeresi a tr függvénnyel megjelölt hívásokat, majd üresen hagyja az adott szöveg fordítását. Ezeket a részeket kell nekünk a fordítással kiegészítenünk. Fontos tulajdonsága az lupdate programnak, hogy amennyiben a generálandó állomány már létezik, akkor nem törli annak a tartalmát, csak az időközben a forráskódba beírt újabb fordítandó szövegek alapján kiegészíti. Így inkrementálisan használható az alkalmazás írásának különböző fázisaiban a fordítások elkészítésére. Ennek az xml állománynak a vázát tehát nem kell manuálisan összeállítani, ez jelentős munkától és az xml séma ismeretétől kíméli meg a programozót. Az alábbi kód a ContactsApplication alkalmazás angol nyelvű fordításának egy részét mutatja be. Látható benne az eredeti szöveg, a fordítás, illetve az eredeti szöveg helye:

    show(); return app.exec(); }

    A fenti kódrészlet alapján látható, hogyan kell módosítani az alkalmazásunk main függvényét, hogy a ContactsApplication _en_US állományban megadott angol fordítást tudja használni A .qm állománynak ebben az esetben a futtatott állománnyal egy könyvtárban kell lennie, vagy a load függvényben az elérési útvonalat is meg kell adni az állomány neve előtt. Az előző példában a lefordított kódrészleten kívül egy qt_en nevű fordítást is betöltünk. Erre azért van szükség, mert az alkalmazásunk nemcsak általunk írt vezérlőket tartalmaz, hanem a Qt keretrendszer részét képező beépített felhasználói felületi elemeket is, ilyenek például a felugró ablakokon megjelenített „Jóváhagyás" és „Mégsem" nyomógombok. Az ezeken megjelenő szövegek fordításáról is nekünk kell gondoskodnunk. A keretrendszer telepítésekor megadott mappában megtalálhatók ezeknek a beépített szövegeknek a fordításai, a qt_en — ahogyan a neve is mutatja — éppen az angol nyelvű fordításokat tartalmazza, de a magyarral együtt sok más nyelvhez is megtalálható itt a fordítás. A fenti kódrészletben azt feltételezzük, hogy ezt az állományt az alkalmazásunkat tartalmazó könyvtárba másoltuk, ezért nem adunk meg elérési útvonalat Természetesen az így felhasznált állományt is tetszés szerint módosíthatjuk, ha jobban testre akarnánk szabni a lefordított szövegeket.

    482

    8.4. Ablakok és vezérlők

    8.4.6. Saját vezérlők készítése Saját vezérlőnek (custom widget) nevezzük azokat az újrafelhasználható vezérlőket, amelyeket magunk definiáltunk. Saját vezérlők készítésére több lehetőségünk is van a Qt keretrendszerben. Az első lehetőség, hogy egy létező vezérlőosztályból leszármaztatunk, ekkor ennek csak azokat a tulajdonságait változtatjuk meg, amelyek számunkra fontosak. A másik módszer az, hogy a QWidget ősosztályból leszármaztatva mi magunk írjuk meg a teljes vezérlőt. Mivel minden QWidgetnek lehetnek tartalmazott vezérlői, előfordulhat, hogy a saját vezérlőnket egyszerűen más létező vezérló'k komponálásával öszszeállíthatjuk. Ilyenkor az új vezérlőnek csak a tartalmazott komponensek elhelyezéséért kell felelnie. A legbonyolultabb eset az, amikor saját megjelenítést szeretnénk definiálni, a következő részben ezt az esetet vizsgáljuk meg részletesen. Saját vezérlő készítéséhez az alábbi feladatokat kell megoldanunk: •

    Hogyan kapja a vezérlő az eseményeket a felhasználótól?



    Hogyan kommunikál a vezérlő a szülőjével?



    Hogyan érheti el a saját területét a vezérlő, és hogyan rajzolhat rá?

    A rendszer által továbbított események mondják meg azt a vezérlőnek, hogy újra kell rajzolnia a felületét, mert a felhasználó átméretezte, elmozgatta, az egérrel a felületére kattintott, vagy billentyűket ütött le. Természetesen ennek kezelése Qt alatt a szignál-szlot mechanizmussal történik. Ugyanakkor saját vezérlők készítésekor mindig ugyanazokat a szlotokat kell megadnunk, hiszen például a rajzolási eseményt mindig kezelnünk kell. Sokszor jobb lenne, ha valamilyen alapértelmezett működést feltételezhetnénk, és csak akkor kellene kezelni az adott szignált, ha ettől a működéstől el szeretnénk térni. A QWidget támogatást nyújt ebben: a szlotok olyan virtuális függvények, amelyneknek a működését tetszés szerint felülírhatjuk vagy megtarthatjuk. Így az első kérdésre a válasz az, hogy a QWidget virtuális függvények meghívásával ad lehetőséget az események kezelésére. Mindegyik metódus rendelkezik egy, az esemény jellemzőit leíró paraméterrel. A származtatott osztályok ezeket a függvényeket tetszés szerint felüldefiniálják. Az egyes függvények implementációjától függ, hogy az adott elem milyen szerepet valósít meg, hogyan reagál egy adott eseményre. A QWidget osztályban több tíz ilyen virtuális függvényt találunk, ezek neve általában az „Event" szöveggel végződik. A leggyakrabban használtak a paintEvent és a resizeEvent. Az elsővel a vezérlő újrarajzolását jelzi a rendszer, a másodikkal azt, hogy a vezérlőt átméretezték. Külön függvényeket találunk a billentyűzet (keyPressEuent, keyReleaseEvent) és az egér (mousePressEvent, mouseDoubleClickEvent, mouseMoveEvent, mouseReleaseEvent) felől érkező események kezelésére. A további függvények megtalálhatóak a QWidget osztály dokumentációjában.

    483

    8. fejezet: A Qt keretrendszer programozása

    A vezérlő a szülőablakkal közvetlenül szignál-szlot alapon kommunikál. A QWidget a QObject osztályból származik, ezért implementálja a szignál-szlot metódust. A vezérlő szignálokat használ, hogy jelezze a felhasználó beavatkozásait vagy az állapotának a megváltozását. A rajzolást a QPainter osztállyal végezzük. A QPainter osztály alacsony szintű rajzfunkciókat biztosít, amelyekkel az adott vezérlőnk területére rajzolhatunk. Így nagyon sokféle rajzfunkció érhető el az egyszerű vonalhúzástól a bonyolultabb alakzatok megjelenítéséig. Lehetőség van továbbá szöveg és képek kezelésére is. Feladat Készítsünk egy vezérlőt, amely kezdetben megjelenít egy téglalapot, amelynek a kerete és a két átlója 4 pixel széles kék színű vonal, a belseje pedig piros színű. Amikor a felhasználó rákattint a vezérlőre, akkor a kitöltés megváltozik, ekkor egy ellipszist rajzolunk ki, amelynek stílusa megegyezik a téglalapéval.

    Saját vezérlő

    8.7. ábra.

    Saját vezérlő állapotai

    Ebben a példában megmutatjuk, hogyan kapunk értesítést arról, hogy a kirajzolásnak meg kell történnie, és hogyan lehet tetszőleges tartalmat rajzolni a vezérlő felületére. Az elkészített alkalmazás felhasználói felülete a 8.7. ábrán látható. /* customwidget.h */ #ifndef CUSTOMWIDGET_H #define CUSTOMWIDGET_H #1 ncl ude class customwidget : public Qwidget {

    Q_OB J ECT

    public: explicit customwidget(Qwidget *parent = 0); enum Shape { Rectangle, Ellipse }; QSize sizeHint0 const;

    484

    8.4. Ablakok és vezérlők

    signals: void shapechanged(int newshape); protected: void mousePressEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event) ; private: int currentShape; }; #endif // CUSTOMWIDGET_H

    A CustomWidget osztály minden vezérlő ősosztályából, a QWidgetból származik. Mivel szeretnénk saját szignált is definiálni, ezért a deklarációja a Q_OBJECT makróval kezdődik. Szükségünk lesz két konstansra (Rectangle és Ellipse), ezek segítségével jelezzük, hogy éppen melyik állapotban van a vezérlő. Az aktuális állapotot a currentShape tagváltozóban tároljuk. Minden vezérlőnek lekérdezhető az alapértelmezett mérete, ezt adja viszsza a sizeHint függvény, amely a QWidget osztály egy virtuális függvénye. A CustomWidget osztály definiál egy szignált is (shapeChanged), ezzel jelezzük, amikor az aktuális állapot megváltozik. A függvény int típusú paramétere az új állapotot tartalmazza. Az osztályban felüldefiniálunk két további virtuális függvényt is, ezek a mousePressEvent és a paintEvent. Az első függvénynyel kapunk jelzést arról, hogy egérkattintás-esemény történt a vezérlőnkön, a második függvény pedig azt az eseményt jelzi, hogy a vezérlőnk egészét vagy egy részét újra ki kell rajzolni. Erre például akkor van szükség, ha a vezérlő egy részét eddig egy másik ablak takarta, ám most újra láthatóvá válik. Egy másik oka lehet az újrarajzolási eseménynek, ha a programunkból meghívtuk a vezérlőn az update vagy a repaint tagfüggvényeket. Ezekkel a függvényekkel jelezhetjük, hogy újra akarjuk rajzolni a vezérlőt, mert például a megjelenített adatok megváltoztak, és így frissíteni kell a megjelenítését. Magát a paintEvent függvényt közvetlenül sosem szabad meghívni, csak az előző függvények egyikével közvetetten. Az update és a repaint között az a különbség, hogy az utóbbinál az újrarajzolás azonnal megtörténik, míg az első hívásakor az újrarajzolási kérés üzenetként bekerül az ablak üzenetsorába, és csak akkor történik meg a kiszolgálása, amikor az előtte lévő üzeneteket feldolgoztuk. Általában csak akkor haszáljunk repaintet, ha a szlotban már szükségük van a kirajzolás megjelenítésére.

    485

    8. fejezet: A Qt keretrendszer programozása

    /* customwidget.cpp */ #include "customwidget.h" CustomWidget::Customwidget(Qwidget *parent) : Qwidget(parent) {

    currentshape = Rectangle; setSizePolicy(QSizePolicy::minimum, QSizePolicy::Minimum); }

    void customwidget::mousePressEvent(QMouseEvent *event){ currentShape = currentShape == Rectangle ? Ellipse : Rectangle; update(); emit shapeChanged(currentShape); }

    void Customwidget::paintEvent(QPaintEvent *event){ QPainter painter(this); QPen pen; pen.setColor(Qt::blue); pen.setWidth(4); painter.setPen(pen); QBrush brush; brush.setColor(Qt::red); brush.setStyle(Qt::SolidPattern); painter.setBrush(brush); switch (currentshape){ case Rectangle: painter.drawRect(0, 0, this->size().width(), this->size().height()); painter.drawLine(0, 0, this->size().width(), this->size().height() ); painter.drawLine(0, this->size().height(), this->size().width(), 0); break; case Ellipse: painter.drawEllipse(0, 0, this->size().width(), this->size().height()); break;

    QSize CustomWidget::sizeHint() const {

    return QSize(200, 200); }

    486

    8.4. Ablakok és vezérlők

    A konstruktor implementációjában először beállítjuk a kezdőállapotot, ez a Rectangle konstans lesz. A következő lépés a setSizePolicy függvény meghívása. Minden vezérlő esetében meg lehet határozni, hogy milyen szabályok szerint lehet átméretezni Amikor az adott típusú vezérlő elrendezéséért egy elrendezéskezelő objektum a felelős, akkor ennek figyelembe kell venni ezeket a szabályokat: void setSizePolicy ( QSi zePol cy : :Policy horizontal, QSizePolicy: :Policy vertica7 )

    A setSizePolicy függvény első paramétere a vízszintes átméretezés szabályát, a második a függőlegesét definiálja. A lehetséges beállításokat a 8.3. táblázat tartalmazza. 8.3. táblázat. A

    vezérlők átméretezési szabályainak lehetséges típusai

    A szabály kódja

    Leírás

    QSizePolicy::Fixed

    A vezérlő méretét nem lehet megváltoztatni, az mindig az alapértelmezett érték lesz.

    QSizePolicy::Minimum

    A vezérlő alapértelmezett mérete a minimumérték, tehát annál kisebbre nem lehet átméretezni.

    QSizePolicy::Maximum

    A vezérlő alapértelmezett mérete a maximumérték, tehát annál nagyobbra nem lehet átméretezni.

    QSizePolicy::Preferred

    A vezérlő alapértelmezett mérete az optimális méret, de ha szükséges, kisebbre és nagyobbra is lehet állítani.

    QSizePolicy::Expanding

    Ha van elég hely, a vezérlőt érdemes minél nagyobb méretűre átméretezni, de szükség esetén a méretet csökkenteni is lehet.

    QSizePolicy::MinimumExpanding

    A vezérlőt kisebbre nem lehet venni, mint az alapértelmezett érték, de ha lehetséges, érdemes minél nagyobbra venni.

    QSizePolicy::Ignored

    A vezérlő alapértelmezett értékét figyelmen kívül kell hagyni, akkora helyet töltsön ki, amenynyit csak lehet.

    A konstruktorban használt argumentumok mellett tehát megmondjuk, hogy a vezérlőnek a sizeHint függvény által visszaadott szélessége és magassága a minimumérték, ezeknél kisebbre nem lehet átméretezni egyik irányban sem. Az alapértelmezettnél nagyobbra azonban át lehet méretezni. Az egérkattintási esemény bekövetkezésekor a mousePressEvent függvény hívódik meg. Ekkor megváltoztatjuk a vezérlő eddigi állapotát, majd a megváltozott állapot miatt újrarajzoltatjuk az update függvényhívással. Végül kibocsátjuk a shapeChanged szignált, jelezve, hogy a vezérlő aktuális állapota megváltozott. 487

    8. fejezet: A Qt keretrendszer programozása

    Utolsóként a paintEvent függvényt implementáljuk. Ez akkor hívódik meg, amikor a vezérlő tartalmát frissíteni kell, a rajzolást a függvényen belül végezhetjük el. A rajzolás megkezdéséhez szükség van egy olyan QPainter példányra, amellyel az aktuális vezérlő területére tudunk rajzolni, ezt pedig úgy kaphatjuk meg, hogy a konstruktorában átadjuk az aktuális vezérlőt. Minden alakzat rajzolásakor meg kell határozni, hogy az alakzat körvonala (toll — pen) és a kitöltése (ecset — brush) milyen stílusú legyen. Az elsőt egy QPen típusú, a másodikat egy QBrush típusú objektummal írjuk le. A toll esetén többek között meghatározhatjuk a színt, a vonalvastagságot, a kitöltést, az ecset esetén ugyancsak a színt és a kitöltést. A példában egy kék színű, 4 pixel széles tollat és egy egyenletes piros kitöltést inicializálunk. A QPen, illetve a QBrush objektumok létrehozása után a QPainter objektum setPen, illetve setBrush függvényeivel be is kell ezeket állítani a rajzoláshoz. A QPainter osztály drawRect és drawEllipse függvényeinek a felhasználásával rajzolunk egy téglalapot vagy egy ellipszist a vezérlő aktuális állapotától függően. Mindkét függvény esetében az adott alakzatot befoglaló téglalap bal felső sarkának koordinátáit, illetve szélességét és magasságát kell átadni. A rajzolás során kétdimenziós koordináta-rendszerrel dolgozunk, amelynek bal felső sarkának koordinátái a (0,0), szélessége és magassága pedig a vezérlő aktuális mérete, amelyet a size függvénnyel kérdezhetünk le A QPainter segítségével tehát könnyen megvalósíthatunk alacsony szintű rajzfunkciókat, ám a képességei nem csak erre korlátozódnak. Bonyolultabb alakzatokat, összetett színátmeneteket, koordinátatranszformációt és sok egyéb szolgáltatást biztosít ez az osztály. A QPainter segítségével nemcsak egy QWidgetre tudunk rajzolni, hanem bármilyen QPaintDevice típusú objektumra, ez az objektum az alaposztálya azoknak az objektumoknak, amelyek valamilyen rajzolható felületet tartalmaznak Valójában a QWidget is ebből származik. További két gyakran előforduló leszármazott osztály a QPrinter és a QPixmap. Az első segítségével kinyomtatható felületet érünk el, a másodikkal pedig a memóriában tárolt bittérképet. A QPainterben a felhasználástól, tehát a konkrét QpaintDevice tól függetlenül történik a rajzolás ugyanazokkal a függvényekkel, vagyis az a kódrészlet, amely egy memóriában tárolt képet rajzol ki, egy az egyben felhasználható arra, hogy egy vezérlő felületére vagy egy nyomtatható területre rajzoljunk. Végezetül megmutatjuk a main.cpp állományt, amelyben az elkészített vezérlő használatához létrehozunk egy alkalmazásablakot, amelyben a központi vezérlőjének beállítjuk a CustomWidget egy példányát: -

    /* main.cpp */ #include #include "customwidget.h" int mai n(i nt argc, char * argv[]) {

    QAppl cati on app (argc , a rgv) ; Qmai nwi ndow " w = new Qmai nwi ndow;

    488

    8.5. A dokumentum/nézet architektúra

    w->setwindowTitle("saját vezérlő"); Customwidget * c = new CustomWidget; w >setCentralWidget(c); w->show(); return app.exec(); -

    8.5. 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 áttekinthető, bonyolultsága pedig kezelhető legyen. A felhasználói felülettel rendelkező (GUI) alkalmazások fejlesztésekor nagyon sok esetben adott valamilyen adathalmazunk, amelyet többféleképpen, több nézetből 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öző — nézetben. A felhasználó természetes elvárása az, hogy ha egy adott nézetben megváltoztatott valamit, mind az adathalmaz, mind a többi nézet a változtatásnak megfelelően frissüljön. Erre kínál egyfajta általános megoldást a dokumentum/nézet (document / view) architektúra. Az adatok egyes me gj elenítési formáját nézetnek (view) nevezzük. Az adatokat a dokumentum (document) tárolja. Ha egy nézeten keresztül megváltoztatunk valamit, a nézet frissíti a megfelelő adatokat a dokumentumban is. A dokumentum feladata az, hogy ha az állapotában változás történt, akkor az összes hozzákapcsolódó nézetet azonnal értesítse, és ezzel biztosítsa, hogy a nézetek mindig a legfrissebb adatokat mutatják. Ha tehát egy nézeten keresztül módosítottuk a dokumentum adatait, akkor az összes többi nézet is automatikusan értesítést kap erről. Az értesített nézetek lekérdezik a dokumentumtól az új adatokat, és gondoskodnak saját újrarajzolásukról. Egy dokumentum állapotát természetesen nem csak a nézeteken keresztül lehet módosítani, ám bármi okozza is a változást, a nézeteket mindig értesíteni kell róla. Általában létezik egy alkalmazás (application) szereplő is, amely inicializálja magát a programot, feldolgozza a parancssori argumentumokat, felépíti a dokumentumot, létrehozza a nézeteket, és hozzákapcsolja őket a dokumentumhoz. A fentiekből következik, hogy a dokumentumnak tudnia kell azokról a nézetekről, 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 olyan listát, amelyben az értesítendő nézetekre mutató pointereket tárol. Í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ézettípusú listaelemek egy megadott tagfüggvényének a meghívásával.

    489

    8. fejezet: A Qt keretrendszer programozása

    Természetesen a nézeteknek is tudnia kell az általuk megjelenített dokumentumról A dokumentumot egy, a dokumentumnézet-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 megfelelő megoldás, ilyenkor a legcélszerűbb az, ha a nézet külön eltárol egy referenciát a hozzátartozó dokumentumra. Az alkalmazásablakok tárgyalásakor már szó volt arról, hogy a Qt keretrendszer támogatja az SDI- és az MDI-alkalmazások készítését, az SDI rövidítés az egy dokumentumablakos, az MDI a több dokumentumablakos felületre utal. Fontos hangsúlyozni, hogy a Qt terminológiájában a dokumentumablak a főablakon belül megjelenített olyan további ablakot jelent, amelyben az aktuálisan szerkesztett adatokhoz tartozó funkciókat elérhetjük. Az SDI és az MDI kifejezések tehát csak arra utalnak, hogy ilyen ablakokból hány darab lehet egyszerre megnyitva, vagyis pusztán a felhasználói felület szempontjából osztályozzák az alkalmazásokat. A dokumentumablak és a dokumentum/nézet architektúra dokumentumkomponense két különböző tényező, csak a nevük hasonló. Könnyen elképzelhető például, hogy a dokumentum/nézet architektúra szerint egyszerre csak egy dokumentum található egy alkalmazásban, de ehhez több nézetet jeleníthetünk meg különálló ablakokban. Így a Qt terminológiája szerint MDI-alkalmazásfelületről beszélünk. Ebben a fejezetben egy MDI-alkalmazásfelületen keresztül bemutatjuk, hogyan tudunk több dokumentumablakkal egyszerre dolgozni a Qt keretrendszerben. A több dokumentum kezelésére alkalmas példaalkalmazás komponenseit a dokumentum/nézet architektúrára építve kezeljük, így mutatjuk be, hogy ezzel a módszerrel hogyan lehet kézben tartani nagyobb méretű programok bonyolultságát. Feladat Készítsünk programot mérési eredmények (0 és 10 közötti számok) listájának a kezeléséhez. Ezeket az eredményeket egy listában szeretnénk kiírni a felhasználónak (ez az egyik nézet), illetve egy hisztogram formájában is meg akarjuk jeleníteni őket (ez a második nézet). A mérési adatokat egy-egy állományból olvassuk be. Nemcsak megjelenítjük az adatokat kétféle nézetben, hanem azt is feltételezzük, hogy a mérési eredményeket szolgáltató eszköztől újabb adatok érkezhetnek folyamatosan, így ezekkel is frissíteni kell a dokumentumot.

    A feladat megoldását az alábbiakban részletesen bemutatjuk, az alkalmazás felületének egy képernyőképe a 8.8. ábrán látható. A példában az adatforrás megvalósítására a felület biztosít majd egy gombot, ezt megnyomva az aktuális dokumentum fogadni tudja az adatokat. Egy dokumentumhoz tartozó két nézetet mindig egyazon dokumentumablakon belül jelenítjük meg, és minden dokumentumhoz külön ablak tartozik. Természetesen ugyanolyan könnyen megvalósítható lenne az is, hogy a két nézet két különböző ablakban jelenjen meg, ám kiderül, hogy a dokumentum/nézet architektúra működése szempontjából ennek nincs jelentősége. Azért választottuk az első megoldást, mert így minden nézetről egyértelmű, hogy éppen melyik dokumentumhoz tartozik. 490

    8.5. A dokumentum/nézet architektúra KonyvMOI Elle

    )(4 /home/mark/Documents/1.data -

    Diume

    ❑ x

    8.2

    s63.clata'

    8.2 9.6 23 1.9

    9.6 2.3 1/ 0 6.3 9.1

    9.1 92 6.6 0.7 6.9

    12 6.6 0.7

    8.8. ábra. MDI-alkalmazás képernyőképe

    8.5.1. Az alkalmazás szerepe Az alkalmazáskomponens általában az alkalmazás főablakát zárja egységbe. A főablak feladata, hogy a felhasználói felület egyes elemeit (menü, állapotsor, eszközsáv, ablakok) összefogja: #ifndef MDIMAINWINDOW_H #define MDIMAINWINDOW_H #include #include "document.h" cl ass moimai nwi ndow :

    public Qmai nwi ndow

    {

    Q_OBJECT public: explicit moimai nwi ndow(Qwi dget *parent = 0) ; si gnal s : public sl ots : void open () ; void save () ; void attach5ou rce () ;

    491

    8. fejezet: A Qt keretrendszer programozása

    private: QMdiArea * mdiArea; QToolBar * fileToolbar; QMenu *fileMenu; QAction QAction QAction QAction

    * * * *

    exitAction; openDocumentAction; saveDocumentAction; attachDataSourceAction;

    QList * documents; ;

    #endif // mDIMAINWINDOW_H

    Az MDIMainWindow osztály a QMainWindowból származik. QAction típusú tagváltozókat deklaráltunk a kilépésre, új dokumentum megnyitására, az aktív dokumentum mentésére, illetve arra, hogy az aktív dokumentumhoz hozzácsatoljunk egy adatforrást. Az alkalmazás főablakában látható dokumentumablakok közül az éppen a fókuszt birtoklót nevezzük aktív dokumentumablaknak, és az ehhez tartozó dokumentumot nevezzük aktív dokumentumnak. A dokumentumok típusát a document.h állományban deklaráljuk (lásd később) Az alkalmazásunkban lévő dokumentumokat egy listában tároljuk, ehhez a QList osztályt használjuk. A főablaknak három fontos vezérlője van, amelyekhez külön tagváltozót is rendelünk. A menüsávon egy „Fájl" feliratú menüt helyezünk el, ezt a QMenu típussal írjuk le. Megjelenítünk továbbá egy eszközsávot is (fileToolBar), amelyben ugyanazok a funkciók lesznek elérhetők, mint a menüben. Végezetül az alkalmazás központi vezérlője egy QMdiArea példány lesz, amely biztosítja a több dokumentumablakos környezet használatát: moimainwindow::mDIMainWindow(OWidget *parent) : QMainWindow(parent) {

    mdiArea = new QMdiArea; this->setCentralWidget(mdiArea); documents = new OList;

    = this->menusar0->addmenu(tr("&Fájl")); fileToolbar = thi s ->addTool Bar (t r ("Fi 1 e")) ; fileMenu

    openDocumentAction = new QAction(tr("&Megnyitás"), this); openDocumentAction->setIcon(QIcon::fromTheme("document-open")); openDocumentAction->setShortcut(OKeySeguence::Open); fileMenu->addAction(openDocumentAction); fileToolbar->addAction(openDocumentAction); connect(openDocumentAction, SIGNAL(triggered()), this, SLOT(open()));

    492

    8.5. A dokumentum/nézet architektúra saveDocumentAction = new QAction(tr("Menté&s"), this); saveDocumentAction->setIcon(QIcon::fromTheme("document-save")); saveDocumentAction->setShortcut(QKeySeguence::Close); fileMenu->addAction(saveDocumentAction); fileToolbar->addAction(saveDocumentAction); connect(saveDocumentAction, SIGNAL(triggered()), this, SLOT(save())); attachDataSourceAction = new QAction(tr("Forrás cs&atolása"), this); attachDataSourceAction->setIcon(QIcon::fromTheme( "system-run")); fileToolbar->addAction(attachDataSourceAction); connect(attachDataSourceAction, SIGNAL(triggered()), this, SLOT(attachSource())); exitAction = new QAction(tr("&kilépés"), this); exitAction->setIcon(QIcon;:fromTheme("application-exit")); exitAction->setShortcut(QKeySequence::Quit); fileMenu->addAction(exitAction); connect(exitAction, SIGNAL(triggered()), this, SLOT(close())); }

    A konstruktorban látható, hogy az összes tagváltozót inicializáljuk, és elhelyezzük a menüben és az eszközsávon a megfelelő funkciókat. A kilépéshez tartozó funkció csak a menüben, az adatforrás csatlakoztatásához tartozó funkció csak az eszközsávon jelenik meg. A további függvények implementációját a dokumentum- és a nézetosztályok tárgyalása után mutatjuk be.

    8.5.2. A dokumentumosztály A dokumentumnak (a példában Document osztály) a következő feladatai vannak: •

    tárolnia kell az adatokat;



    interfészt kell biztosítania az adatok módosítására;



    tárolnia kell a hozzá tartozó nézeteket, és lehetővé kell tennie újabb nézetek hozzáadását, egy már tárolt nézet eltávolítását;



    gondoskodnia kell arról, hogy ha a tárolt adatok változnak, akkor az összes nézet frissüljön;



    interfészt kell biztosítania az adatok lekérdezésére, ugyanis a nézetek a frissítés során lekérdezik a dokumentumtól, hogy milyen adatokat is kell pontosan megjeleníteni:

    493

    8. fejezet: A Qt keretrendszer programozása

    #ifndef DOCUMENT_H #define DOCUMENT_H #i ncl ude #i ncl ude #i nclude "vi ew. h" cl ass view; cl ass Document : public QObject {

    Q_OBJECT public: explicit Document(QObject *parent = 0); void addvi ew(Vi ew view); void removevi ew(vi ew " view); QList * getState() ; si gnal s : public slots: void appendoata(doubl e val ue) ; bool openFi 1 e (QStri ng fi 1 eName) ; bool saveFi 1 e() ; protected: void updateAllvi ews() ; pri vate: QLi st " state ; QLi stdocument->getstate(); int barHeight = 10; int maxitems = rect().height() / barHeight; int index; int offset = 0; for (index = data->size() - maxitems < 0 ? 0 : data->size() - maxitems; indexsize(); index++) double value = data->at(index); painter.drawRect(0, offset*barHeight, (value/10)*rect().width(), barHeight); offset ++;

    501

    8. fejezet: A Qt keretrendszer programozása

    void Diagramview;:resizeEvent(QResizeEvent * resizeEvent){ updateBuffer(); update();

    A paintEvent átmásolja a buffer tartalmát a vezérlőre, ehhez a QPainter osztály drawPixmap függvényét használjuk. Fontos ellenőrizni előtte, hogy a buffer valóban megfelelően lett-e inicializálva, mert amíg még nem rendeltünk dokumentumot a nézethez, addig nem történik meg a bufferbe rajzolás sem. Az updateView függvénnyel értesül a nézet a dokumentum változásáról, ezért ebben frissíteni kell a buffert, majd — hogy a változás meg is jelenjen — meg kell hívni az update függvényt, amely egy újrarajzolási jelzést küld az adott vezérlőnek. Lehetőség van az update helyett a repaint függvény használatára is. A resizeEvent függvényben, a vezérlő méretének megváltozásakor ugyanezt a logikát kell megírni, frissíteni a buffert, majd az update hívással megjeleníteni a friss tartalmat. A tartalom előállítását az UpdateBuffer függvény végzi. Ebben a rajzolás ugyanúgy, egy QPainter objektummal történik, mintha magára a vezérlőre rajzolnánk közvetlenül, csak itt a memóriabeli kép mutatóját kell átadni a konstruktorban. A QPixmap példányokat az inicializálás után nem lehet átméretezni, ezért a függvény minden egyes futtatásakor új QPixmap példányt hozunk létre. A további kódrészlet a korábban leírt megjelenítést végzi el, vagyis kiszámítja, hogy hány téglalap fér el a vezérlő területén, és ettől függően kirajzolja az utolsó néhány adatnak megfelelő téglalapot. A barHeight lokális változó rögzíti, hogy a megjelenített téglalapok magassága 10 pixel, a maxItems lokális változóban tároljuk azt, hogy ekkora téglalapokból legfeljebb hány darab fér el a vezérlő aktuális területén. A for ciklusban a data gyűjteményben tárolt utolsó maxltems darab elem indexén iterálunk végig. Természetesen, ha a data elemeinek a száma kisebb, mint a maximálisan megjeleníthető elemek száma, akkor az összeset kirajzolja a program. A QList generikus gyűjtemény elemeinek a számát a size függvénnyel, míg egy adott indexhez tartozó elemet az at függvénnyel kérdezhetünk le. Az offset lokális változót használjuk arra, hogy megadjuk, hányadik téglalapot razoljuk ki, ez azért fontos, mert a kirajzolt téglalapok számától függ, hogy a téglalap bal felső sarkának mi legyen a függőleges koordinátája. Azért nem a for ciklus index nevű futó változóját használjuk erre, mert nem biztos, hogy a gyűjtemény minden elemét megjelenítjük. Kiemelendő még a QPainter osztály drawRect függvénye, amely az aktuális tollal és ecsettel kirajzol egy téglalapot. A drawRect függvénynek többféle paraméterezése is létezik, a fenti példában először a bal felső sarok vízszintes és függőleges koordinátáit, majd a téglalap szélességét és magasságát kell átadni.

    502

    8.5. A dokumentum/nézet architektúra

    8.5.4. További osztályok A teljesség kedvéért bemutatjuk a DataSource osztály definícióját is, amelynek segítségével egy adatforrást tudunk a dokumentumokhoz kapcsolni. Egy valós alkalmazásban ennek a komponensnek a segítségével fogadnánk a külső eszközről érkező adatokat. Ebben a példában egyszerűen egy véletlenszám-generátor küld másodpercenként újabb „mérési eredményeket" a dokumentumosztálynak.m A dokumentum appendData függvénye egy szlot, így a másik szálban futó adatforrás a szignál-szlot mechanizmusra építve küldhet értesítést: #ifndef DATASOURCE_H #define DATASOURCE_H #include #include "document.h" cl ass Document ; cl ass DataSource : public QObj ect {

    Q_0137 ECT public: explicit DataSource(Qobject *parent = 0); void setDocument(Document * doc); si gnal s : void dataRecei ved (doubl e data) ; public sl ots void ti me rTi medOut () ; void stopTimer() ; pri vate Document *document ; QTi mer * ti mer ; ;

    #endif // DATASOURCE_H

    A DataSource osztály deklarációja a következő fontosabb tagokat tartalmazza: időzítő (timer), timerTimedOut és stopTimer szlotfüggvények, illetve a dataReceived szignál.

    11

    Jelen tárgyalásban a hangsúly az adatforrás és a dokumentumosztály közötti kommunikáción van, a mérési adatok érkezését csak szimuláljuk. A gyakorlatban az adat érkezhet egy eszközvezérlőtól, hálózatról, másik száltól és számos egyéb módon.

    503

    8. fejezet: A Qt keretrendszer programozása

    Az időzítéshez a Qt beépített időzítőosztályát, a QTimert használjuk. A QTimer objektumok a timeout szignállal jelzik, hogy eltelt az előre beállított idő, erre a timerTimedOut szlottal iratkozunk fel. A dataReceived szignállal jelez az adatforrás a dokumentumnak, hogy új adat érkezett. A stopTimer függvényben állítjuk le az időzítőt, amikor már nincsen rá szükség, mert amikor egy dokumentumot bezárunk, akkor a hozzákapcsolódó adatforrásban működő időzítőt is felszabadíthatjuk: DataSource::DataSource(Q0bject *parent) : QObject(parent) { } void QataSource::setDocument(Document *doc){ this->document = doc; timer = new QTimer(this); connect (ti me r , SIGNAL (ti meout ()) thi s , SLOT (ti merTimedOut 0)) connect(this, SIGNAL(dataReceived(double)), doc, SLOT(appendData(double)), Qt::QueuedConnection); timer->start(1000);

    ;

    void DataSource : : me rTi medOut(){ emi t dataReceived((doubl e) (q rand () % 100) / 10.0) ; } void DataSource::stopTimer(){ this->timer->stop(); }

    A DataSource osztály setDocument függvényével tudjuk az adatforrást egy dokumentumhoz csatolni, ilyenkor indítjuk el az időzítőt, amelynek a jelzéséről a timerTimedOut szlottal kapunk értesítést. Az időzítőt a QTimer osztály start metódusával indítjuk el. Szintén a setDocument függvényben kötjük öszsze a dataReceived szignált és a dokumentum appendData szlotfüggvényét. A timerTimedOut szlotban a qrand függvénnyel generálunk egy véletlen számot, majd ezt normalizálva egy 0 és 10 közötti véletlen értékké generáljuk, kibocsátjuk a dataReceived szignált, jelezve, hogy új adat érkezett. Végezetül pedig a stopTimer szlot az időzítő leállításáért felelős. A DocumentWindow osztály összefogja és megjeleníti az egy dokumentumhoz tartozó nézeteket. Ismét hangsúlyozni kell, hogy ez az osztály nem képezi a dokumentum/nézet architektúra részét. A DocumentWindow egy olyan QWidgetból származó vezérlő, amely egy vízszintes elrendezéskezelő objektum segítségével kirajzolja az ablakhoz az addView függvénnyel hozzáadott nézeteket. A DocumentWindow egy referenciát tárol a dokumentumra, és ha létezik, akkor a dokumentumhoz kapcsolt adatforrásra is:

    504

    8.5. A dokumentum/nézet architektúra #ifndef DOCUMENTWINDOW_H #define DOCUMENTWINDOW_H #include #include "document.h" #include "datasource.h" class Document; class view; class DataSource; class Documentwindow : public Qwidget { Q_OBJECT public: explicit Documentwindow(Qwidget *parent = 0); void setDocument(Document * doc); void setDataSource(DataSource * source); DataSource * getDataSource(); void addview(view* view); Document * getDocument(); signals: public slots: private: Document * document; private: QHBoxLayout * layout; DataSource * source; } ;

    #endif /7

    DOCUMENTWINDOW_H

    Az implementációban csak a setDataSource függvény érdemel külön említést. Ezzel állítjuk be az adatforrás-referenciát egy új adatforrásra. A beállítás után az adott vezérlő destroyed szignálját összekötjük az adatforrás stopTimer szlotjával, így az adott ablak bezárásakor az adatforrás is befejezi az adatok küldését: Documentwindow::Documentwindow(Qwidget *parent): Qwidget(parent) this->layout = new QHBoxLayout; this->setLayout(layout); this->source = NULL; }

    505

    8. fejezet: A Qt keretrendszer programozása void Documentwindow::setDOCUment(DoCUment *doc){ this->document = doc; }

    void DocumentWindow::addView(view *view){ this->layout->addwidget(view); this->update(); }

    Document * Documentwindow::getDocument(){ return this->document;

    void DocumentWindow::setDataSource(DataSource *source){ this->source = source; connect(this, SIGNAL(destroyed()), source, SLOT(stopTimer())); }

    DataSource * Documentwindow::getDataSource(){ return this->source; }

    Végezetül bemutatjuk az alkalmazásosztály implementációját: mDimainWindow::MDIMainwindow(Qwidget *parent) : QMainwindow(parent) {

    mdiArea = new QMdiArea; this->setCentralwidget(mdiArea); documents = new QList; fileMenu = this->menuBar()->addmenu(tr("&File")); fileToolbar = this->addToolBar(tr("File")); openDocumentAction = new QAction(tr("&Open"), this); openDocumentAction->setIcon(QIcon::fromTheme("document-open")); openDocumentAction->setShortcut(QKeySeguence::Open); fileMenu->addAction(openDocumentAction); fileToolbar->addAction(openDocumentAction); connect(openDocumentAction, SIGNAL(triggered()), this, SLOT(open())); saveDocumentAction = new QAction(tr("&Save"), this); saveDocumentAction->seticon(QIcon::fromTheme("document-save")); saveDocumentAction->setShortcut(QKeySeguence::Close); fileMenu->addAction(saveDocumentAction); fileToolbar->addAction(saveDocumentAction); connect(saveDocumentAction, SIGNAL(triggered()), this, SLOT(save()));

    506

    8.5. A dokumentum/nézet architektúra attachDataSourceAction = new QAction(tr("&Attach"), this); attachDataSourceAction-> setIcon(QIcon::fromTheme("system-run")); fileToolbar->addAction(attachDataSourceAction); connect(attachDataSourceAction, SIGNAL(triggered()), this, SLOT(attachSource())); exitAction = new QAction(tr("E&xit"), this); exitAction->setIcon(Qicon::fromTheme("application-exit")); exitAction->setShortcut(QxeySequence::Quit); filemenu->addAction(exitAction); connect(exitAction, SIGNAL(triggered()), this, sLOT(close())); }

    void mDimainwindow::open() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open document"), "—/Documents", tr("Document files (*.data)")); if (fileName == "") return; Document * doc = new Document; this->documents->append(doc); doc->openFile(fileName); Documentwindow * w = new Documentwindow; w->setObjectName("Documentwindow"); QMdiSubwindow * wContainer = this->mdiArea->addSubwindow(w); w->setDocument(doc); wContainer->setwindowTitle(fileName); Tableview * table = new Tableview; table->setDocument(doc); w->addview(table); doc->addview(table); Diagramview * diagram = new Diagramview; diagram->setDocument(doc); w->addview(diagram); doc->addview(diagram); w->show();

    void mDIMainwindow::save(){ QMdisubwindow * subwindow = this->mdiArea->activesubwindow(); if (subwindow == NULL) return; Documentwindow * w = subwindow->findChild("Documentwindow"); w->getDocument()->saveFile(); }

    507

    8. fejezet: A Qt keretrendszer programozása

    void mDimainwindow::attachSource(){ Qmdisubwindow * subwindow = this->mdiArea->activeSubwindow(); if (subwindow == NULL) return; Documentwindow * w = subwindow-> findchild("Documentwindow"); if (w == NULL) return; if (w->getDatasource() != NULL) return; DataSource * source = new DataSource; source->setDocument(w->getDocument()); w->setDataSource(source);

    A konstruktorban felépítjük a felhasználói felületet, létrehozzuk a QAction objektumokat, és elhelyezzük őket a menüben és az eszköztárban. Az MDI esetében az alkalmazás főablakának központi vezérlője egy QMdiArea példány kell, hogy legyen. Dokumentumot megnyitni az openDocumentAction segítségével tudunk, ennek meghívásakor az open szlot fut le. A QFileDialog osztály statikus getOpenFileName függvényével megjelenítünk egy dialógusablakot, amellyel a .data kiterjesztésű állományokat tudjuk kiválasztani. Ezután létrehozzuk a dokumentumot és a hozzátartozó két nézetet. A két nézetet nem külön ablakokban, hanem egy DocumentWindow példányban helyezzük el. A példánynak a setObjectName függvénnyel adunk egy nevet, ez segít majd azonosítani, ha az adott elemet a vezérlők fastruktúrájában akarjuk programozottan keresni (lásd később). A létrehozott DocumentWindow példányt a QMdiArea osztály addSubWindow függvényével jelenítjük meg egy újabb ablakként a főablakon belül. Ilyenkor a QMdiAreán belül létrejön egy QMdiSubWindow típusú vezérlő — ez az addSubWindow visszatérési értéke —, amely felelős a kirajzolt ablak fejlécének és keretének a megjelenítéséért. Ezen belül a központi vezérlő lesz az addSubWindow függvénynek átadott DocumentWindow példány. Az ablakot végül a show függvénnyel meg kell jeleníteni. Egy dokumentum aktuális állapotát a save szlotfüggvény menti el Amikor a mentési funkciót kiválasztjuk, az aktív ablak dokumentumát szeretnénk menteni. Az aktív ablakot a QMdiArea osztály activeSubWindow függvényével kérdezzük le, amely a korábbiakban elmondottak értelmében egy QMdiSubWindow példánnyal tér vissza. Ennek a példánynak a gyermekvezérlője az adott dokumentumhoz tartozó DocumentWindow, amelyet legegyszerűbben a findChild sablonfüggvénnyel kérdezhetünk le. Ennek meg kell adni az adott vezérlő típusát és az objektum nevét, amelyet korábban beállítottunk a setObjectName függvénnyel. Így megszereztük a referenciát az aktív DocumentWindow példányra, ettől már lekérdezhető a dokumentum. Ezt a dokumentumot kell elmenteni a saveFile függvénnyel.

    508

    8.6. További technológiák

    Az attachSource függvény hasonlóképpen működik. Miután elértük az aktív ablakhoz tartozó DocumentWindow példányt és azon keresztül a dokumentumot, létrehozunk egy DataSource objektumot, és a dokumentumhoz kapcsoljuk. Összefoglalva elmondhatjuk, hogy az alkalmazás az, amely inicializálja és „mozgásba hozza" a dokumentum/nézet architektúrát. Az MDIMainWindow és a DocumentWindow közösen látják el az alkalmazáskomponens feladatait, vagyis a dokumentumok és nézetek létrehozását és kezelését, de ez nem érinti a dokumentum- és a nézetosztályok használatát.

    8.6. További technológiák A Qt keretrendszer nem csak egy grafikus vezérlőket tartalmazó osztálykönyvtár. Több modulja van, amely a felhasználói felület mögötti alkalmazáslogika platformfüggetlen programozását segíti. A Qt-t gyakran használják csak konzolos alkalmazások fejlesztésekor is. A következőkben áttekintjük a Qt keretrendszer néhány fontos technológiáját és az azokhoz kapcsolódó osztályokat. Az első a QtCore alapmodul része, és a többszálú programozást teszi lehetővé. A második az adatbáziskezelés elérését támogató könyvtár, amely egy külön modulban, a QtSqlben található. Végül bemutatjuk a hálózati kommunikáció programozásához használható osztálykönyvtárat, a QtNetwork modult.

    8.6.1. Többszálú alkalmazásfejlesztés A Qt keretrendszer QtCore moduljának része a többszálú programozást lehetővé tévő osztálykönyvtár. Ez támogatja új szálak indítását és a szálak közötti szinkronizációt. Többszálú alkalmazás írásakor figyelni kell arra, hogy az objektumokat melyik szálban hoztuk létre. Egy QObject típusú objektumot csak abban a szálban szabad létrehozni, amelyben a szülőobjektumot hoztuk létre, és gondoskodni kell a létrehozott objektumok törléséről, mielőtt magát a szálat törölnénk. A szálak közötti szinkronizációt és kommunikációt többféle segédosztály teszi lehetővé. Korábban már bemutattuk, hogy a Qt keretrendszer fontos tulajdonsága az, hogy a szignál-szlot mechanizmus alkalmas arra, hogy többszálú környezetben használjuk, azaz lehetőség van bizonyos szignálokat más szálakban futó szlotokkal összekötni. A keretrendszer lehetővé teszi, hogy minden szálnak legyen külön eseménykezelő ciklusa, és ez teszi lehetővé a várakozásisor-alapú összeköttetések használatát.

    509

    8. fejezet: A Qt keretrendszer programozása

    Az új szálak programozásának bemutatása előtt tekintsük át röviden, hogyan indul el egy alkalmazás főszála a Qt keretrendszerben. Minden alkalmazás a main függvény futtatásával kezdődik, ebben a korábbi példáknál általában létrehoztunk egy QApplication típusú objektumot, itt a main függvény végén meghívtuk az exec metódusát. Ez az objektum felelős az alkalmazás főszálának, a grafikus szálnak a futtatásáért. Az exec hívásakor valójában a főszálhoz tartozó eseménykezelő ciklus indul el. Erre az eseménykezelő ciklusra azért van szükség, hogy az operációs rendszertől érkező üzeneteket, illetve a QObject példányok közötti üzeneteket kezelni tudjuk. Ezért egy ilyen alkalmazásobjektumból csak egy lehet a programunkban. Ha nem grafikus felülettel rendelkező, hanem más Qt-funkciókat használó konzolos alkalmazást írunk, akkor a QApplication helyett a QCoreApplicationt kell példányosítani. Ez az osztály a QApplication őse, és ugyancsak elindít egy üzenetkezelő ciklust, de nem a grafikus felület számára. A felhasználói felületet alkotó vezérló'k mindig az alkalmazás főszálában futnak, ezért ezekhez az objektumokhoz csak a főszálban szabad hozzáférni. Tegyük fel, hogy egy grafikus felülettel rendelkező alkalmazásban egy gomb megnyomásának hatására időigényes számítási műveletet végzünk el. Ezzel az a probléma, hogy ha a műveletnek nem indítunk új szálat, akkor az az alkalmazás főszálában fut, amely egyben a megjelenítésért is felelős. A művelet hosszabb ideig blokkolja a szál működését, így a grafikus felület a felhasználó számára elérhetetlen lesz, mert a felhasználói eseményeket a művelet befejezéséig nem tudja feldolgozni. Érdemes tehát ekkor új szálat indítani Ilyenkor meg kell oldani a két szál közötti kommunikációt. Ha például a számítás eredményét egy szövegdobozban akarjuk megjeleníteni, akkor a másik szálból egy üzenetet kell küldenünk a főszálnak a művelet végén, ugyanis a másik szálból nem módosíthatjuk a főszálban lévő objektumok, például a vezérló'k állapotát. Erre a problémára a legkényelmesebb megoldás a szignál-szlot mechanizmus használata, ez ugyanis a programozó számára átlátszóan kezeli azt, hogy a szignált publikáló és a szlotot definiáló objektumok ugyanabban vagy másik szálban futnak-e. Egy szál indításához szükség van egy olyan objektumra, amely a QThread osztályból származik A szálak a main függvény helyett a QThread osztály run metódusának meghívásával indulnak el. Saját szál definiálásához a QThreadból kell leszármaztatni és felülírni a virtuális run metódust. A szál végrehajtása akkor fejeződik be, amikor a run függvény visszatér, pontosan úgy, ahogyan az alkalmazás futása is véget ér, amikor a main függvény befejeződik: void QT;ktr Ödmr14.'0..4.

    A run láthatósága protected, ezért a szál elindításához nem közvetlenül ezt a függvényt hívjuk meg, hanem a QThread start metódusát. Egy adott szál állapotát az isRunning, illetve az isFinished metódusokkal lehet lekérdezni a QThread objektumtól, továbbá három szignál is rendelkezésünkre áll, amelyek jelzik, amikor a szál elindult (started), befejezte a futását (finished), illetve a szálat leállították még annak befejezése előtt (terminated): 510

    8.6. További technológiák

    bool QThread::isFinished O const bool QThread::isRunning O const void QThread::started void QThread::finished void QThread: :terminated

    // signal // signal // signal

    Új szál indításakor szükség lehet saját eseménykezelő ciklusra, ha például a szignál-szlot mechanizmust szeretnénk használni. Ennek elindítására az exec függvényt használjuk, ezt a run függvényen belül kell meghívni: -

    QTbre4d - ' Ilyenkor a szál folyamatosan fut, amíg az eseménykezelő ciklust le nem állítjuk. A leállításhoz a szál quit vagy exit függvényét kell meghívni. A quit megegyezik az exit(0) hívásával. Az exit paramétere a szál visszatérési értéke lesz, és a szál elindításához használt exec függvény ezzel tér vissza. Mindkét függvényt szlotként definiálták, így szignálok segítségével is könnyen leállíthatunk egy szálat: voi Qrh read : qui t () void QTh reád : : exi t ( i nt returncode = 0 ) Feladat Készítsünk egy grafikus felülettel rendelkező alkalmazást, amely a felületen található nyomógomb megnyomására indít egy új szálat, amelyben elvégez egy számítási műveletet, majd annak az eredményét megjeleníti.

    Ezen a példán megmutatjuk, hogyan tudunk új szálat indítani, hogyan definiáljuk azt, hogy az indított szál milyen műveletet hajtson végre, és hogyan tudunk a szignál-szlot mechanizmus segítségével kommunikációt megvalósítani különböző szálak között. A 8.9. ábrán látható az alkalmazás képernyőképe. A művelet, amelyet elvégzünk a 12 és a 13 számok összegének kiszámítása, ezt a „12 + 13 = ?" szöveg kiírásával jelezzük, amelyre egy QLabelt használunk. Alatta helyezkedik el egy QPushButton típusú nyomógomb, amellyel elindíthatjuk a számítást végző szálat. Legalul egy másik QLabelt helyezünk el, ez kezdetben nem tartalmaz semmilyen szöveget, az eredményt jeleníti meg, amikor megtörtént a számítás.

    8.9. ábra. Többszálú alkalmazás főablaka

    511

    8. fejezet: A Qt keretrendszer programozása

    Kezdjük a számítást a végző szál megírásával: /* calculatorthread.h */ #i fndef CALCULATORTHREAD_H #define CALCULATORTHREAD_H #include cl ass CalculatorThread : public QThread {

    Q_OBJECT public: explicit cal cul atorThread(Q0bject *parent = 0) ; void run() ; si gnal s : void resultcomputed(i nt resul t) ; public slots: void startcomputi ng(int a, int b) ;

    }

    ;

    #endif

    /7 CALCULATORTHREAD_H

    A CalculatorThread osztály a QThreadból származik. Felüldefiniáljuk a run metódust, amelyben a szál elindításakor lefutó logikát írjuk meg. A szál elindítása után várakozunk, amíg a startComputing szlot meg nem hívódik. Tehát a run függvényben egyszerűen csak elindítjuk az eseménykezelő ciklust az exec meghívásával. A startComputing függvény két int típusú argumentuma adja meg, hogy milyen számokon kell az összeadást elvégezni. A számítás végeztével a szál egy szignált bocsát ki, amely tartalmazza a művelet eredményét. Ez a resultComputed szignál. A startComputing maga is egy szlotfüggvény, így egy szignál segítségével meg tudjuk hívni. A connect függvény bemutatásakor már szó volt a várakozásisor-alapú öszszeköttetésekről. Ha a szignált publikáló és a szlotot definiáló objektumokat külön szálban hoztuk létre, akkor ez az alapértelmezett összeköttetés-típus. Ilyenkor a szignál kibocsátása után a vezérlés nem blokkolódik, aszinkron módon elküldjük a szignálról az üzenetet a másik szálban futó objektumnak. Összefoglalva tehát a szálosztály működését: elindítása után várakozik az eseménykezelő ciklus, amíg az osztály startComputing szlotjának meghívásával nem indítunk el egy számítást. A művelet elvégzését a resultComputed esemény kibocsátásával jelezzük:

    512

    8.6. További technológiák

    /* calculatorthread.cpp */ #include "calculatorthread.h" CalculatorThread::CalculatorThread(Q0bject *parent) : QThread(parent) { } void CalculatorThread::run() { exec(); }

    void CalculatorThread::startComputing(int a, int b) int result = a + b; sleep(3); // Így már tényleg hosszú művelet emit resultComputed(result); }

    Maga a számítás — amelyet a startComputing függvény valósít meg — a két paraméterként kapott szám összeadását jelenti, ezután az eredményt a resultComputed szignálon keresztül küldjük el. Azért, hogy a számítás valóban időigényes legyen, a QThread sleep függvényével elaltatjuk 3 másodpercre a szálat, mielőtt az eredményt visszaküldenénk. Erre a sleep függvényt használjuk:

    Az szálosztályunk használatához ezt példányosítani kell, bekötni a megfelelő szignálokat és szlotokat, majd elindítani a start függvénnyel, ezeket a felhasználói felület kódjának tárgyalása után vesszük sorra: /* resultwindow.h */ #ifndef RESULTWINDOw_H #define RESULTWINDOW_H #include class ResultWindow : public QWidget {

    Q_OBJECT public: explicit ResultWindow(QWidget *parent = 0); signals: void startComputing(int a, int b); public slots: void resultComputed(int result); private slots: void buttonClicked();

    513

    8. fejezet: A üt keretrendszer programozása pri vate : QLabel * 1 blTask ; QPushwitton * btnCompute ; QLabel * 1 bl Resul t ; QVBoxLayout * layout; }; #endif // RESULTWINDOW_H

    A felhasználói felületet a ResultWindow osztály tartalmazza. Az osztály tagváltozói az a három vezérlő, amelyet elhelyezünk a felületen, és egy QVBoxLayout típusú elrendezéskezelő, amely függőlegesen egymás alá igazítja őket. A startComputing szignállal jelezzük majd, hogy szeretnénk, ha két int típusú számon elvégeznénk a számítási műveletet. Ezt kötjük össze a szálobjektum azonos nevű szlotjával. Deklarálunk továbbá egy buttonClicked szlotot is, ezzel kapunk értesítést majd a gomb megnyomásáról: /* resultwindow.cpp */ #i ncl ude " resul twi ndow . h" Resul tWi ndow : : Resul twi ndow(Qwi dget *pa rent) : QWi dget ( pa rent ) {

    layout = new Qv8oxLayout ; 1 bl Tas k = new QLabel ("12 + 13 = ?") ; layout->addwi dget (1b1Task) ; btnCompute= new QPushButton("Számítás"); layout->addwidget(btnCompute); 1 bl Resul t = new ; layout->addwi dget(lbl Resul t) ; this->setLayout(layout); connect(btnCompute, siGNAL(clicked()), this, SLOT(buttonClicked()));

    void Resul twi ndow: : buttoncl i cked() emit startcomputing(12, 13);

    void Resul twi ndow: : resul tComputed(i nt resul t) { lbl Resul t->setText (Qstri ng : : numbe r ( resul t)) ;

    514

    8.6. További technológiák

    A konstruktorban létrehozzuk a vezérlőket és az elrendezéskezelő objektumot, majd a connect függvénnyel összekapcsoljuk a nyomógomb clicked szignálját az osztályban definiált buttonClicked szlottal. A felület megjelenítése után tehát addig nem történik semmi, amíg a gombot meg nem nyomjuk, ekkor meghívódik a buttonClicked szlot, amelyben kiváltjuk a startComputing szignált. Az argumentumai a felületen is kiírt 12 és 13 számok lesznek. Ezután várakozunk, amíg a számítás elvégzése után meghívódik a resultComputed szlot, amelynek a paramétere tartalmazza az eredményt. Ezt a QLabel osztály setText függvényével írjuk ki a felületre. Mivel a számítást külön szálban végeztük, így a felhasználói felület továbbra is működőképes marad, és reagál a felhasználói eseményekre: /* main.cpp */ #include #include #include #include

    "calculatorthread.h" "resultwindow.h"

    int main(int argc, char * argv[]) { QApplication app(argc, argv); Resultwindow * window = new ResultWindow; CalculatorThread * thread = new CalculatorThread; QObject::connect(window, SIGNAL(startComputing(int,int)), thread, SLOT(startComputing(int,int))); QObject::connect(thread, sIGNAL(resultComputed(int)), window, SLOT(resultComputed(int))): thread->start(); window->show(); return app.exec(); }

    A programunk main függvényében inicializálunk egy ResultWindow és egy CalculatorThread példányt. A felhasználói felület miatt létrehozzuk a QApplication objektumot is, amely a főszál elindításáért felelős. Ezután történik a szignálok és a szlotok összekapcsolása. Végül a szálat a start függvénnyel indítjuk el, az ablakot pedig a show metódussal jelenítjük meg. A Qt keretrendszer a szálak indításán és a szignál-szlot mechanizmuson túl is biztosít szolgáltatásokat többszálú programok támogatására. A 8.4. táblázatban látható néhány fontosabb osztály, amelyek a szálak közötti szinkronizációt segítik.

    515

    8. fejezet: A Qt keretrendszer programozása

    8.4. táblázat. Szálak közötti szinkronizációt támogató osztályok

    QMutex

    Kölcsönös kizárást (mutex) megvalósító zár.

    QReadWriteLock

    Hasonló a mutexhez, de az objektumokhoz való hozzáférés során megkülönbözteti az írási és az olvasási szándékot.

    QSemaphore

    Szemafort megvalósító osztály.

    QWaitCondition

    Lehetővé teszi, egy feltétel létrehozását, ennek a bekövetkeztéig egy szál várakozik. A feltételt teljesülését más szálakból tudjuk engedélyezni.

    Ezek a szinkronizációs osztályok a QT-szintű reprezentációi az 5. Párhuzamos programozás fejezetben tárgyalt megoldásoknak.

    8.6.2. Adatbáziskezelés Összetettebb információhalmazzal dolgozó alkalmazásokban az adatokat általában nem egyszerű állományokba mentjük, hanem valamilyen adatbáziskezelő rendszert használunk a tárolásukra. Az adatbázisok SQL nyelvű programozásához szükséges felületet a QtSql modul biztosítja. A modul lehetőséget teremt a különböző adatbáziskezelő rendszerek egységes és platformfüggetlen elérésére. A modulban definiált osztályok két csoportba oszthatók: az adatbáziskezelő-függő vezérlő- (driver) réteg osztályai biztosítják a kommunikációt a különböző platformokhoz, míg az SQL programozói interfész (SQL API) osztályai biztosítják az platformfüggetlen SQL-alapú alkalmazásfejlesztést. A modul eléréséhez a forrásfájljainkban be kell építeni a megfelelő osztályokat, vagy a következő sor segítségével egyszerre tudjuk beépíteni a teljes modult: #í ncl
    >

    A modul használatát továbbá a projektállományban is jelezni kell. Az alábbi sorral adhatjuk meg azt, hogy a fordítás során a linker a QtSql könyvtárat is vegye figyelembe: QT += 541 Feladat Írjunk alkalmazást, amely csatlakozik egy adatbázishoz, amelyben könyvek címeit és szerzőit tároljuk egy táblában. A csatlakozás után írjuk ki az adatbázis tartalmát a konzolra.

    516

    8.6. További technológiák

    Ennek a példának az elkészítésében bemutatjuk, hogyan kell kapcsolódni egy adatbázishoz, hogyan tudunk SQL nyelvű parancsokat küldeni a programunkon keresztül, és hogyan tudjuk egy lekérdezés eredményét programozottan feldolgozni. A továbbiakban egy MySQl-típusú adatbáziskezelőt használunk, amelyben azt feltételezzük, hogy egy qtDatabase nevű adatbázisban tároljuk az adatokat. Az adatbázisban egyetlen táblát definiáltunk, ebben könyveknek az adatait (egyedi azonosító, cím és szerző) tároljuk. A tábla neve Book, az oszlopai pedig a 8.5. táblázatban láthatók. 8.5. táblázat. qtDatabase adatbázis Book táblájának oszlopai

    Oszlop neve

    Típusa

    Leírása

    Id

    Int(11)

    A könyv azonosítója (elsődleges kulcs)

    Title

    varchar(255)

    A könyv címe

    Author

    varchar(255)

    A könyv szerzője

    A programunk egyetlen main függvényből áll, itt építjük fel a kapcsolatot, küldjük el a lekérdezést, és írjuk ki az eredményt a hibakereső konzolra: /* main.cpp */ #include #include #include #include



    int main() {

    QSq1Database db = QSq1Database::addDatabase("QMYSQL"); db.setHostName("localhost"); db.setpatabaseName("qtQatabase"); db.setuserName("dbUser"); db.setPassword("passw"); if (!db.open()) {

    quebug() « "Cannot connect to database..."; quebug() « db.lastError(); return 1;

    QsqlQuery selectQuery; if (!selectQuery.exec("select

    from Book"))

    {

    cpebug() « "Error with executing query";

    517

    8. fejezet: A Qt keretrendszer programozása whi le (sel ectQuery. next()) {

    gátring title = selectQuery.val ue(1) toString(); QString author = selectQuery.val ue(2) .toString(); cpebug() « author « ": " « title; } db. cl ose() ; return 0; }

    Az adatbázis-eléréséhez használt osztályok bemutatása előtt érdemes felhívni a figyelmet arra, hogy a fenti main függvényben nem hoztunk létre QApplication objektumot, mivel itt nincsen szükségünk eseménykezelő ciklusra: nem használjuk a szignál-szlot mechanizmust, és nem használunk olyan osztályokat, amelyek ilyen ciklus meglétét követelik. Az adatbázishoz való hozzáférés lépései minden alkalmazás esetében a következők: 1. Felépítjük a kapcsolatot: ehhez szükség van az adatbázis címére és a belépési adatokra. 2. Elküldünk az adatbázisnak egy SQL nyelvű utasítást. 3. Feldolgozzuk az utasítás eredményét. Ha az utasítás egy lekérdezés, akkor annak visszatérési eredményét soronként tudjuk bejárni és feldolgozni. Egyéb esetben a visszatérési érték egy skalár. 4. Lezárjuk az adatbáziskapcsolatot. Az adatbázishoz való hozzáférés mindig a kapcsolat felépítésével kezdődik, ezt egy QSq1Database példánnyal írjuk le. Egy program fejlesztése során több különböző adatbáziskapcsolatra is szükség lehet, így több példányt is létrehozhatunk, mindegyiket egyedi névvel azonosítjuk. Lehetőség van egy alapértelmezett kapcsolatot is létrehozni az alkalmazáson belül, ennek nem adunk meg nevet, ez történik a példaalkalmazásban is. A továbbiakban használt függvények nagy részére igaz, hogy opcionális paraméterként várják az adatbáziskapcsolat nevét, ha ezt nem adjuk meg, akkor az alapértelmezett kapcsolatot használják. Minden kapcsolat létrehozásánál meg kell adni az adatbázis típusát, ez alapján a Qt keretrendszer a továbbiakban a megfelelő vezérlőt használja a kommunikációra. A fenti példában a db változó lesz az alapértelmezett kapcsolat, amelyet a QSq1Database osztály statikus addDatabase függvényével hozhatunk létre. Ez a függvény egyben el is tárolja a létrehozott adatbáziskapcsolatokat egy belső listában:

    518

    8.6. További technológiák

    QSg1Database QSg1Database::addDatabase ( const QString & type, const QString & connectionName =

    QLatin1String( defaultConnection ))

    A függvény második, opcionális paramétere az adatbázis-kapcsolat neve. A „QMYSQL" szöveg azt jelzi, hogy egy MySQL-típusú adatbázishoz akarunk csatlakozni. Az 8.6. táblázatban olvasható néhány olyan vezérlőnév, amellyel további adatbáziskezelő rendszerekhez férhetünk hozzá. 8.6. táblázat. A Qt keretrendszer adatbáziskezelő vezérlőinek azonosítói

    Vezérlőnév

    Adatbáziskezelő rendszer

    QDB2

    IBM DB2

    QSQLITE

    SQLite (3-as verzió)

    QSQLITE2

    SQLite (2-es verzió)

    QOCI

    Oracle Call Interface Driver

    QPSQL

    PostgreSQL

    QODBC

    ODBC- (Open Database Connectivity) kompatibilis adatbáziskezelők, például Microsoft SQL Server

    Az adatbázis-kapcsolat példányának inicializálása után meg kell adnunk a kapcsolódás paramétereit, ezek az adatbázisszerver címe, az adatbázis neve, illetve a felhasználó neve és jelszava. A fenti példában az adatbázisszerver az alkalmazással egy gépen fut, ezért a címe localhost. Ahhoz, hogy a kapcsolat valóban fel is épüljön, meg kell hívni a kapcsolat open függvényét, amelynek bool típusú visszatérési értéke jelzi, hogy sikeres volt-e a kapcsolódás. Ha hiba történt, akkor arról további információt a QSqlDatabase statikus lastError függvényével kérhetünk le: QSglError QSg1Database::lastError () const

    A QtSql modul többi osztályánál is általában ilyen módszerrel lehet megtudni az előforduló hibák pontos adatait. A függvények többnyire csak azt jelzik, hogy hiba történt, amelyet a lastError függvénnyel lekérdezhetünk. Ha a program során egy adatbázis-kapcsolatra már nincsen szükségünk, akkor eltávolíthatjuk a listából a QSqlDatabase statikus removeDatabase függvényével. Előtte azonban be kell zárni a kapcsolatot a close függvénnyel. A fenti példában a felhasználó számára megjelenítendő adatokat a hibakereső konzolra írjuk, amelyet a QDebug osztállyal érünk el. Ez az osztály egy kimeneti adatfolyamot (output stream) biztosít, amelyre a C++-ban megszokott « operátor segítségével tudunk adatokat küldeni. Általában ezt az osztályt nem kell explicit módon példányosítani, elég, ha meghívjuk a qDebug() függvényt, amely egy alapértelmezett példánnyal tér vissza.

    519

    8. fejezet: A Qt keretrendszer programozása

    A kapcsolat sikeres felépítése után lehetőségünk van SQL nyelvű utasításokat küldeni az adatbázisnak. Egy SQL-parancsot a QSqlQuery osztállyal írhatunk le, amelynek meg kell adni a parancs szövegét. A korábbiaknak megfelelően, opcionálisan megadható az adatbázis-kapcsolat is. Az utasítás típusától függetlenül az exec függvénnyel tudjuk ezt lefuttatni: bool QSql Query : : exec ( const QString & query ) bool QSql Query : :exec ()

    Mind a konstruktornak, mind az exec függvénynek többféle paraméterezése létezik, az SQL-parancsot megadhatjuk a konstruktorban vagy az exec függvény argumentumán keresztül is. Az open függvényhez hasonlóan ennek is a bool típusú visszatérési értéke jelzi, hogy történt-e hiba, ha igen, akkor annak részleteit a korábban ismertetett módszerrel tudhatjuk meg. Ha olyan utasítást adunk ki, amelynek az eredménye nem lekérdezés (például insert utasítás), akkor ezután több feladatunk nincsen. A példánkban szereplő parancs azonban egy lekérdezés, eredménye a Book táblában tárolt összes sor. A QSqlQuery osztály segítéségével a lekérdezés eredményét soronként tudjuk feldolgozni. Az eredményhez úgy férünk hozzá, mintha egy virtuális kurzorunk (iterátorunk) lenne, amellyel szekvenciálisan haladhatunk az eredmény sorain. A kurzort továbbléptethetjük a next függvénnyel, illetve a kurzor által mutatott sornak lekérdezhetjük az adatait. A next függvény visszatérési értéke bool típusú, minden léptetés után jelzi, hogy volt-e további sor az eredményben. A kurzor kezdetben nem az első sorra mutat, ahhoz, hogy a lekérdezés első sorához hozzáférjünk, előtte ugyanúgy meg kell hívni a next függvényt, mint a későbbiekben egy újabb sorhoz való ugrásnál. Ha a lekérdezés eredménye egyetlen sort sem tartalmaz, akkor már az első hívásnál false lesz a visszatérési érték. Annak a sornak az adatait, amelyre éppen a kurzor mutat, a value függvénnyel kérdezhetjük le: QVariant QSqlQuery: :val ue ( int

    index )

    const

    A függvény egyetlen paramétere a sor egy oszlopának indexe, visszatérési értéke pedig egy QVariant típusú objektum. A QVariant segítségével egységesen tudjuk tárolni egy cella tartalmát annak típusától függetlenül. A QVariant tagfüggvényeivel a tárolt adatot a megfelelő Qt típusra lehet konvertálni. Az aktuális adat típusát a type függvénnyel le is kérdezhetjük: Type QVari ant: :type () const

    A visszatérési érték QVariant::Type felsorolás típusú, ebben a lehetséges adatbázistábla-típusok vannak felsorolva. A konvertáláshoz toT formájú függvényeket kell használni, ahol T helyére a megfelelő típus azonosítóját kell behelyettesíteni. Erre példa a fenti kódban látható toString függvény, amely az adott értéket QString típusúvá alakítva adja vissza. Ha olyan típus-

    520

    8.6. További technológiák

    ra próbálnánk átalakítani a cella tartalmát, amilyenre nem lehet, akkor az adott típustól függ, hogy mi lesz a függvényhívás eredménye. Természetesen lehetőség van ellenőrizni is még az átalakítás előtt, hogy átalakítható-e az adat a megfelelő típusra, ehhez használhatjuk a canConvert függvényt: bool QVariant::canConvert ( Type t ) const

    A 8.7. táblázatban néhányat sorolunk fel a típusok lehetséges értékei közül, és jelezzük, hogy milyen Qt-osztálytípusra lehet az adott értéket átalakítani. 8.7. táblázat. A QVariant osztályban tárolható legfontosabb adattípusok Adattípus

    Leírás

    QVariant::Inoalid

    hibás adat — nincs típus

    QVariant::Bitmap

    QBitmap

    kép

    QVariant::Bool

    bool

    logikai igaz/hamis érték

    QVariant::Date

    QDate

    dátum

    QVariant::Daterime

    QDateTime

    dátum és időpont

    QVariant::Time

    QTime

    időpont

    QVariant::String

    QString

    szöveg

    QVariant::Int

    int

    egész szám

    Konstans

    A példában az 1-es és a 2-es indexű, azaz a második és a harmadik oszlop tartalmát alakítjuk át QString típusúra minden egyes sorban, és ezeket írjuk ki a felhasználónak. Az első oszlop az azonosítót tartalmazza, erre itt nincsen szükség. A program végén lezárjuk az adatbázis-kapcsolatot a close metódussal. Természetesen a kapcsolat létrehozása után több SQL-utasítást is végrehajthattunk volna, nem kell minden egyes parancshoz újabb kapcsolatot inicializálni.

    8.6.3. Hálózati kommunikáció Hálózati kommunikáció programozására is saját osztálykönyvtárat biztosít a Qt keretrendszer, ezeket a QtNetwork modulban találhatjuk összegyűjtve. Segítségükkel TCP/IP alapú kliens-, illetve szerveralkalmazásokat készíthetünk. A modul biztosít néhány alacsony szintű kommunikációt megvalósító osztályt (például QTcpSocket, QUdpSocket, QTcpServer), illetve néhány ezekre épülő alkalmazásrétegbeli protokollt egységbe záró osztályt (például http és ftp kommunikáció használatára) is. A magasabb szintű kommunikáció is természetesen az alacsonyabb szintű programozói interfészre épül, ezért az alábbiakban ez utóbbinak a használatát tárgyaljuk részletesen.

    521

    8. fejezet: A Qt keretrendszer programozása

    A Qt keretrendszerben a hálózati programozás koncepciója alapvetően megegyezik a korábban bemutatott módszerekével. Az alaposztályok a QTcpSocket, illetve a QUdpSocket, amelyek — ahogyan a nevük is mutatja — socketeket írnak le. Mind a szerver-, mind a kliensoldalon egy-egy socketosztályon keresztül történik az adatok írása és olvasása. A könnyebbség a Qt keretrendszer használatában a korábbi megoldással szemben először is a TCP-kapcsolat szerveroldali programozásában érezhető: a QTcpServer osztály nagyban egyszerűsíti a szerveroldali kapcsolat kiépítését. Ez írja a le azt a szerverszolgáltatást, amely egy adott címen és porton várja a beérkező kapcsolatokat. Amikor egy új kapcsolódási kérelem érkezik, akkor felépít egy QTcpSocketet. A második könnyebbség az, hogy a már megszokott módon, a szignál-szlot mechanizmus segítségével kapunk értesítést minden olyan eseményről, amely a socketeken történik. Így a hálózati programozás során valójában létre kell hozni a megfelelő objektumokat, ezekban beállítani a megfelelő hálózati címeket, majd azokra az eseményekre, amelyeket az alkalmazásunkban kezelni szeretnénk, szlotfüggvényekkel kell feliratkoznunk. A hálózatkezelő modul használatához a projektállományt ki kell egészítenünk az alábbi sorral. Ezzel jelezzük, hogy a linker a hálózati modul osztálykönyvtárát is használja: QT

    +=

    network

    Ebben a fejezetben a TCP-csomagokkal történő kommunikációt tárgyaljuk részletesen, az UDP-alapú kommunikáció ehhez hasonló módon történik. Feladat Írjunk egy egyszerű csevegőalkalmazást. A szerveroldali program tudjon klienseket fogadni, és amennyiben egy kliens üzenetet küld, azt továbbítsa az összes többi csatlakozott kliensnek. A kliensalkalmazásnak legyen grafikus felhasználói felülete, és jelenítse meg az öszszes beérkezett és elküldött üzenetet.

    Először a szerveralkalmazást írjuk meg. Mivel a QTcpSocketnek a szignáljaira kell feliratkozni, ehhez szükség lesz egy szlotokat definiáló osztályra. Erre szolgál a ChatServer osztály, amely a teljes alkalmazáslogikát tárolja, és a szignál-szlot mechanizmus miatt a QObjectból származik: /* chatserver.h */ #ifndef CHATSERVER_H #define CHATSERVER_H #include #include class chatserver : public Q0bject {

    Q_OBJECT

    522

    8.6. További technológiák public: explicit ChatServer(Q0bject *parent

    0);

    public slots: void newConnection(); void textReceived(); private: QTcpServer * tcpServer; QList * clientsockets; }; #endif // CHATSERVER_H

    Az osztálynak két privát tagváltozója van, egy QTcpServer és egy lista, amely a QTcpSocketek mutatóját tárolja. A QTcpServer felelős a TCP-szerver létrehozásáért, a listában pedig a csatlakozott kliensekkel kiépített socketeket tároljuk. Minden, a szerverre érkezett üzenet esetében kiírjuk a kliens sorszámát és magát az üzenetet. A szerveralkalmazás nem grafikus, a konzolra írja ki az üzeneteket. Az osztály két szlotfüggvényt is tartalmaz. A newConnection segítségével kapunk értesítést arról, hogy egy kliens megpróbált csatlakozni, a textReceived szlottal pedig arról, hogy egy klienstől adat érkezett: /* chatserver.cpp */ #include #include #include #include #include

    "chatserver.h"



    ChatServer::ChatServer(Q0bject *parent) : QObject(parent) {

    tcpServer = new QTcpServer(); this->clientSockets = new QList(); connect(tcpserver, SIGNAL(newConnection()), this, SLOT(newConnection())); if (tcpServer->listen(QHostAddress::Any, 9935)){ dDebug() « "A szerver alkalmazás elindult..."; } }

    void ChatServer::newConnection(){ QTcpSocket * socket = tcpServer->nextPendingConnection(); clientSockets->append(socket); connect(socket, SIGNAL(readyRead()), this, SLOT(textReceived())); cffiebug() «"kliens ["« this->clientSockets->size()-1 « "] csatlakozott"; }

    523

    8. fejezet: A Qt keretrendszer programozása void ChatServer::textReceived(){ QTcpSocket * socket = (QTcpSocket*)sender(); int index = this->clientsockets->index0f(socket); QString message = "[" % QString::number(index) % "]:" % socket->readAll(); qDebug() « message; QListIterator iterator(*clientsockets); while (iterator.hasNext()) {

    QTcpSocket * client = iterator.next(); if (client != socket) {

    client->write(message.toLatinl());

    A szerverosztály implementációja a konstruktort és a két szlotfüggvényt tartalmazza. A konstruktorban inicializáljuk a szervert, és feliratkozunk ennek newConnection szignáljára, ez jelzi, ha csatlakozási kérés érkezett. A listen függvénnyel megadjuk, hogy a szerver egy adott címen és portszámon figyelje a beérkező kéréseket. A függvény nem blokkolódik, rögtön viszszatér. Ha valamilyen esemény történik, arról a megfelelő szignállal kapunk értesítést: bool QTcpServer : :1 i sten ( const QHostAddress & address quintl6 port = 0 )

    =

    QHostAddress::Any,

    A címet a QHostAddress osztállyal írhatjuk le. A QHostAddress::Any egy olyan konstans, amely jelzi, hogy a szerver az összes hálózati interfészen figyel. A fenti példában a portszámot is megadjuk, de átadhatunk 0-t is a második paraméternek, ilyenkor a szerverhez egy véletlen portszámot rendel a rendszer. A listen függvény visszatérési értéke jelzi, hogy sikeres volt-e a szerver indítása. A ChatServer osztály példányosítása után tehát elindul a szerver, és várja a beérkező csatlakozási kérelmeket Amikor a kliens megpróbál csatlakozni, akkor a QTcpServer példány kibocsátja a newConnection szignált, amelyre az azonos nevű szlotfüggvénnyel iratkoztunk fel. Ebben a függvényben először lekérdezzük az új kapcsolathoz tartozó QTcpSocket példányt a nextPendingConnection függvény segítségével, ezt a példányt pedig eltároljuk a clientSockets listában. Mivel a QTcpSocket most jött létre, ezért itt kell feliratkozni a readyRead szignáljára, hogy értesítést kapjunk, amikor új olvasható adat érkezik. Erre a textReceived szlotfüggvényt használjuk. Tehát, amikor a kliens adatot küld a szervernek, akkor a textReceived függvény hívódik meg, ebben egyszerűen kiírjuk a konzolra az érkezett adatot.

    524

    8.6. További technológiák

    Az első nehézség, amely eló'kerül a textReceived implementálásakor, az az, hogy hogyan tudjuk meg, hogy melyik klienstől érkezett az adat, ugyanis az összes klienshez tartozó QTcpSocketnek a readyRead szignáljára ugyanazzal a függvénnyel iratkozunk fel. A megoldás a már korábban bemutatott sender függvény. Ennek a segítségével kapunk pointert az adott klienshez tartozó QTcpSocketre. A példánkban a kliens sorszáma a hozzátartozó socketnek a clientSockets listában eltárolt sorszáma lesz. A QTcpSocket osztálytól a következő függvénnyel tudjuk lekérdezni a klienstől érkezett adatot: QByteArray QI0Device::readAll ()

    Az aktuális üzenet kiolvasása után az adott szöveget elküldjük az összes csatlakozott kliensnek, kivéve természetesen az üzenet küldőjét. Erre szolgál a függvény végén található ciklus. A QTcpSocket readAll metódusával olvastuk ki az adatokat, a write metódussal pedig el tudunk küldeni egy szöveget. Ennek a függvénynek többféle paraméterezése létezik, a példában a const char * paraméterűt használjuk, amely a paraméterként átadott szövegkonstansot küldi el a hálózaton keresztül: qint64 QI0Device::write ( const char * data )

    Végezetül megmutatjuk a main.cpp forrásállományt, amelyben a Qt keretrendszer inicializálásán kívül mindössze a ChatServer osztályt példányosítjuk, ugyanis ennek a konstruktorában a szerver is létrejön. Mivel az alkalmazás nem rendelkezik grafikus felülettel, viszont eseménykezelésre szükségünk van, a QApplication helyett a QCoreApplication osztályt példányosítjuk: /* main.cpp */ #include #include #include int main(int argc, char *argv[]) {

    QCoreApplication a(argc, argv); ChatServer * server = new ChatServer(); return a.exec(); }

    A következő feladat a kliensalkalmazás elkészítése. Ebben nem kell QTcpServer objektumot inicializálni, mindössze egy QTcpSocket példányt kell létrehozni, amelyet a megfelelő cím megadásával a csatlakoztatunk szerverhez. A kliensalkalmazás már grafikus felhasználói felülettel rendelkezik, ez a 8.10. ábrán látható. A felső — csak olvasható — szövegdoboz tartalmazza az eddig elküldött szövegeket, illetve a legelső sorban van egy üzenetet, amely azt 525

    8. fejezet: A Qt keretrendszer programozása

    jelzi, hogy sikerült-e felépíteni a kapcsolatot a szerverrel, továbbá kiírja az eddig elküldött üzeneteket is. Alul látható egy másik szövegdoboz, amelybe az elküldendő üzenetet lehet begépelni, mellette pedig egy gomb, amellyel el is lehet küldeni. Amíg a kapcsolódás nem járt sikerrel, a szövegdobozba nem lehet írni.

    Uzenet:

    Küldés

    8.10. ábra. Csevegő kliensalkalmazásának képernyőképe /* chatclientwindow.h */ #ifndef CHATCLIENTWINDOW_H #define CHATCLIENTWINDOW_H #include #include #include cl ass ChatC1 i entWi ndow : public Qwi dget {

    Q_OBJECT public: explicit ChatClientwindow(Qwidget *parent = 0); public slots: void sendText(); void hostConnected(); void socketReadyRead(); private : QvBoxLayout * vLayout ; QHBoxLayout hLayout ; QTextEdit * txtChatHistory; QLi neEdi t * txtInput ; QLabel * 1b1Text; QPushButton * btnSend;

    wrcpsocket * socket ;

    #endif // CHATCLIENTWINDOW_H

    526

    8.6. További technológiák

    A chatclientwindow.h állomány tartalmazza a ChatClientWindow osztály deklarációját. A QWidgetből származó osztálynak a korábban bemutatott vezérlőknek megfelelő tagváltozói vannak. Látható továbbá, hogy két elrendezéssegítő osztálypéldány is szerepel: a QHBoxLayout segítségével a „Szöveg" szöveget, a szövegbeviteli dobozt és a nyomógombot helyezzük el egymás mellé, a QVBoxLayout pedig a korábbi üzeneteket tartalmazó dobozt igazítja az előbbi vízszintes tartalom fölé. Szintén tagváltozóként definiálunk egy QTcpSocketet (ennek a funkcióját lásd korábban). A konstruktoron kívül három szlotfüggvényt definiálunk. A hostConnected függvénnyel kapunk majd értesítést arról, hogy sikerült-e a csatlakozás a szerverhez. A sendText szlotot a nyomógomb megnyomásakor kibocsátott szignál aktiválja, jelezve ezzel, hogy a begépelt adatot küldjük el a szervernek. Végül a socketReadyRead szlottal iratkozunk fel arra a szignálra, amely új üzenet érkezésekor jelez: /* chatclientwindow.cpp */ #include "chatclientwindow.h" ChatClientwindow::ChatclientWindow(QWidget *parent) : Qwidget(parent) {

    vLayout = new QVBoxLayout; this->setLayout(vLayout); txtChatHistory = new QTextEdit; txtChatHistory->setReadOnly(true); vLayout->addwidget(txtChatHistory); hLayout = new QHBoxLayout; lblText = new QLabel; lblText->setText("üzenet:"); hLayout->addWidget(lblText); txtlnput = new QLineEdit; hLayout->addwidget(txtlnput); btnSend = new QPushButton; btnSend->setText("Rüldés"); connect(btnSend, SIGNAL(clicked()), this, SLOT(sendText())); hLayout->addWidget(btnSend); vLayout->addLayout(hLayout); txtChatHistory->append("Szerverkapcsolat még nem épült fel."); txtInput->setoisabled(true);

    527

    8. fejezet: A Qt keretrendszer programozása

    socket = new QTcpSocket(); connect(socket, SIGNAL(connected()), this, SLOT(hostConnected())) connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead())); socket->connectToHost("localhost", 9935);

    void ChatCli entWi ndow: sendText () QString text = txtInput->text(); txtinput->cl ear () ; txtChatHi story->append( QTime cu r rentTi me() toSt ri ng socket->wri te (text . toLati nl()) ; socket ->fl ush() ;

    % " én :

    11

    % text) ;

    voíd ChatClientwindow::hostConnected(){ txtChatHistory->clear(); txtChatHistory->append("Szerverkapcsolat felépítve"); txtInput->setEnabl ed (t rue) ;

    void ChatClientWindow::socketReadyRead(){ QString message(socket->readAll()); txtchatHistory->append( QTime::currentTime().toString() % " " % message);

    Az implementációs állomány a konstruktorral kezdődik. Először a felületen elhelyezett vezérlőket és az elrendezéskezelő objektumokat inicializáljuk a korábban már ismertetett módon. Mivel a fenti szövegdoboz többsoros, ezért azt a QTextEdit vezérlővel hozzuk létre, míg a lentebbi, egysoros szövegdoboz a QLineEdit osztály példánya. A létrehozott vezérlőknek néhány tulajdonságát módosítjuk. A QTextEdit osztály setReadOnly metódusával csak olvashatóvá tesszük a felső szövegdobozt, illetve az append függvénnyel egy újabb szöveget adunk hozzá a doboz eddigi tartalmához. A QLineEdit setDisabled függvényével nem engedélyezett állapotba helyezzük a lenti szövegdobozt, ezzel jelezve, hogy nem lehet bele írni, amíg a kapcsolat létre nem jött. A QPushButton típusú gomb szövegét a setText függvénnyel állítjuk be, ugyanitt bekötjük a sendText szlotot a gomb clicked szignáljához. A konstruktor második fele a hálózati kapcsolat felépítését végzi el. A QTcpSocket létrehozása után összekötjük ennek a connected szignálját a saját osztályunk hostConnected szlotfüggvényével, majd a connectToHost függvénnyel megpróbálunk csatlakozni a szerverhez. Az utóbbi függvény első paramétere a szerver címe, a második pedig a portszáma. Még a csatlakozás előtt feliratkozunk a szerversocket egy másik szignáljára, a readyReadre is. Ez a szignál jelzi, hogy új adat érkezett (lásd a szerveralkalmazásnál). 528

    8.6. További technológiák

    Amikor tehát a fenti ablakot inicializáljuk, és lefut a konstruktor, akkor elhelyezzük a vezérlőket, és a fenti szövegdobozban megjelenik egy szöveg, amely szerint a hálózati kapcsolatot még nem sikerült felépíteni, majd megpróbálunk csatlakozni a szerverhez. Ha sikerült, akkor automatikusan meghívódik a hostConnected szlot. Ebben a függvényben módosítjuk a felhasználói felületet, a felső szövegdobozba kiírjuk, hogy a kapcsolat most már létrejött, illetve a lenti szövegdobozt is engedélyezzük a setEnabled függvénnyel. A további teendők a felhasználóra várnak. Miután beírt egy szöveget az alsó szövegdobozba, meg kell nyomnia a küldésgombot, ennek hatására a sendText szlot hívódik meg. Ebben először kiolvassuk a szövegdoboz tartalmát, majd ezt az aktuális időponttal együtt kiírjuk a felső szövegdobozba, továbbá a már korábban említett write függvénnyel elküldjük a hálózaton keresztül a szöveget. A flush függvény meghívásával biztosítjuk, hogy a QTcp Socket bufferének a tartalmát azonnal elküldjük. Amikor új üzenet érkezik a szervertől, a socketReadyRead szlotfüggvény hívódik meg, ebben kiolvassuk az új üzenetet, majd kiírjuk a szövegdobozba. A teljesség kedvéért a kliensalkalmazás main függvényét is bemutatjuk: /* main.cpp */ #include #include "chatcl i entwi ndow. h" int main(int argc, char * argv[]) { QApplication app(argc, argv); ChatClientWindow * client = new ChatClientWindow; client->show(); return app.exec(); }

    A main.cpp állományban inicializáljuk az alkalmazásobjektumot és a ChatClientWindow osztályunkat, majd megjelenítjük az utóbbit. Innen maga a widget felelős a hálózati kommunikációért.

    529

    8. fejezet: A Qt keretrendszer programozása

    8.7. Összefogla lá s Ebben a fejezetben bemutattuk a Qt keretrendszer legfontosabb technológiáit, és ismertettük néhány fontos osztályát. A Qt keretrendszer grafikus felületek programozása mellett nagyban támogatja a legkülönbözőbb konzolos alkalmazások fejlesztését, továbbá könnyen megvalósítható az alacsony és magasabb szintű alkalmazások közötti kommunikáció. Bemutattuk a Qt szignál-szlot mechanizmusát, dialógusablakok, összetett alkalmazásablakok és saját vezérló'k készítésének a módszereit. A dokumentum/nézet architektúrában láthattuk, hogyan lehet összetett alkalmazásoknál is kézben tartani a program működésének átláthatóságát a különböző komponensek szerepeinek szétválasztásával. Végül a Qt keretrendszer részét képező néhány fontos technológiát, a többszálú programozást, az adatbáziselérést és a hálózatkezelést tárgyaltuk. A Qt keretrendszer azonban nemcsak egy alkalmazásprogramozói könyvtárból áll, hanem egy teljes szoftvercsomagból, amely nagyban egyszerűsíti nemcsak a grafikus felhasználó felülettel rendelkező, de a konzolos vagy beágyazott alkalmazások fejlesztését egyaránt. A könyv írásának időpontjában a Qt keretrendszerhez ajánlott első számú fejlesztőeszköz a QtCreator, amely egy ingyenes, platformfüggetlen integrált fejlesztői környezet és a Qt keretrendszer része. A QtCreator többek között a következő eszközöket biztosítja a fejlesztéshez:

    530



    Forráskódszerkesztő funkciók többek között C++ és Javascript nyelvekhez automatikus kódkiegészítéssel.



    Teljes projektmenedzsment rendszer, amely automatikusan kezeli a .pro projektfájlokat, így ezeket nem szükséges kézzel szerkeszteni. A projekteket automatikusan fordítja és futtatja.



    Szoros integráció verziókezelő rendszerekkel.



    A környezet része a Qt Designer, amely egy grafikus felhasználói felülettervező alkalmazás. A felületen keresztül megtervezhetjük az alkalmazásunk kinézetét, elhelyezhetjük a vezérlőket, elrendezésobjektumokat hozhatunk létre. Továbbá lehetőség van szintén a felületen keresztül az egyes vezérló'k által definiált szignálok és szlotok összekapacsolására. Így egyetlen programsor írása nélkül is megalkothatjuk a felhasználói felület és az eseménykezelés vázát. A kialakított felület leírását egy .ui kiterjesztésű xml állományban tárolja a rendszer, amelyből automatikusan generálja egy annak megfelelő osztály kódját, így ezt utána felhasználhatjuk az alkalmazásunkban



    A QtLinguist program segítségével egy grafikus felületen tudjuk szerkeszteni a .ts kiterjesztésű xml állományainkat, amelyek az alkalmazásaink lokalizált szövegeit tárolják.

    8.7. Összefoglalás

    • A QtCreator része egy QML szerkesztő is. A QML (Qt Markup Language) egy jauctscipt alapú nyelv, amelynek a segítségével deklaratívan lehet leírni a felhasználói felületeket. A QML része a QtQuick keretrendszernek, amely képes az így leírt felületet futtatni. A QML nyelv bemutatása meghaladja ennek a könyvnek a kereteit, de fontos megemlíteni, mert egyre elterjedtebb módszer a Qt-alapú fejlesztésekben. A fent felsorolt funkciók, a platformfüggetlen fejlesztó'környezet és a keretrendszer azon képessége, hogy a mobileszközöktől a beágyazott rendszereken át az asztali számítógépekig nagyon sok operációs rendszeren működik, a Qt-t az egyik legsokoldalúbb alkalmazásplatformmá teszik.

    531

    A

    FÜGGELÉK

    Fejlesztőeszközök Linux alatt a fejlesztőeszközök széles választéka áll rendelkezésünkre. Ezekből kiválaszthatjuk a nekünk megfelelőt, ám néhány fontos eszközt mindenkinek ismernie kell. A Linux-disztribúciók számtalan megbízható fejlesztőeszközt tartalmaznak, amelyek főként a Unix rendszerekben korábban elterjedt eszközöknek felelnek meg. Ezek az eszközök nem rendelkeznek barátságos felülettel, a legtöbbjük parancssoros, felhasználói felület nélkül. Ám sok éven keresztül bizonyították megbízhatóságukat, használhatóságukat. Számos új felhasználóbarát program a háttérben továbbra is ezeket a parancssoros eszközöket használja.

    A.1. Szövegszerkesztők A Linuxhoz is találhatunk több integrált fejlesztői környezetet (Integrated Development Environment, IDE), ám továbbra is gyakran használunk egyszerű szövegszerkesztőket. Sok Unix-fejlesztő továbbra is ragaszkodik ezekhez a kevésbé barátságos, de a feladathoz sokszor tökéletesen elegendő eszközökhöz. A Linux története során az alábbi eszközök terjedtek el.

    A.1.1.

    Emacs

    Az eredeti Emacs programot Richard Stallman (az FSF alapítója) készítette. Évekig a GNU Emacs volt a legnépszerűbb változat. Később elterjedt a grafikus környezethez készített XEmacs. A felhasználói felülete nem olyan feltűnő, mint sok más rendszernél, ám számos, a fejlesztők számára jól használható funkcióval rendelkezik. Ilyen például a szintaxis-ellenőrzés. Ha a fordítót hozzáillesztjük, képes annak hibaüzeneteit értelmezni és a hibákat megmutatni. Lehetővé teszi továbbá a hibajavító program környezetbe való integrálását is.

    A függelék: Fejlesztőeszközök

    A.1.2. vi (vim) A vi egy egyszerű szövegszerkesztő. 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 lehető legkevesebb kézmozgással lehessen használni. Tapasztalatlan felhasználó számára azonban a kezelése meglehetősen bonyolult lehet, ezért népszerűsége erősen megcsappant. Elszánt Linux/Unix felhasználók szokták alkalmazni.

    A.1.3. nano (pico) A nano és az elődje a pico egyszerű képernyő-orientált szövegszerkesztő. Általában minden Linux rendszeren megtalálható. Elterjedten használják a kisebb szövegek gyors módosítására a terminálból. Komolyabb feladatokra nem ajánlható. Ezekben az esetekben alkalmasabb a következő részben tárgyalt joe vagy egy, a feladatnak megfelelően specializált szerkesztő. A program parancsai folyamatosan láthatók a képernyő alján. A ^ jel azt jelenti, hogy a billentyűt a Ctrl gomb nyomva tartása mellett kell használni. Ha ez esetleg a terminál típusa miatt nem működne, akkor az ESC gomb kétszeri megnyomását is alkalmazhatjuk helyette.

    A.1.4. joe A joe a nanónál fejlettebb és nagyobb, szintén szöveges terminálban használható szövegszerkesztő. Komolyabb feladatokra is alkalmas a magasabb szintű szolgáltatásai révén. Egyszerre több állományt is megnyithatunk vele különböző opciókkal. Az állományokat a kiterjesztésük alapján felismeri, és automatikusan szintaxiskiemeléssel segíti a munkát. Komolyságát a parancsok számából is láthatjuk, amelyet a -k-h gombokkal hozhatunk elő és tüntethetünk el A jelölésekben a „^" jel azt jelenti, hogy a billentyűket a Ctrl gomb nyomva tartása mellett kell használni

    A.1.5. mc A Midnight Commander, röviden mc elterjedt alternatívája a Norton Commander nevű MS DOS/Windows alkalmazásnak. Szöveges, kvázigrafikus felülettel rendelkezik. Gyakran használják olyan felhasználók, akik nem szeretnek folyton parancsokat beírni, a grafikus felülethez képest azonban alacsonyabb szintű megoldásra vágynak.

    534

    A.2. Fordítók

    A Midnight Commander rendelkezik egy beépített szövegszerkesztővel, amelyet mceditnek hívnak. Elérhető az F4 gombbal vagy önálló programként indítva. Sokan használják rövidebb forráskódok szerkesztésére. Ezt elősegíti, hogy ugyancsak rendelkezik szintaxiskiemelő szolgáltatással.

    A.1.6. Grafikus szövegszerkesztők A grafikus környezetek (Desktop Environment) rendelkeznek saját fejlett szövegszerkesztőkkel, amelyek támogatják forráskódok szerkesztését is. A KDEkörnyezetben a kate jó választás, de a kwrite is jól használható. A GNOME-környezet pedig a gedit nevű programot nyújtja. Egyszerű forráskódok megírására ezek jól használható eszközök.

    A.2. Fordítók Linux alatt, a programozási nyelvektől függően, az alábbi kiterjesztésekkel találkozhatunk:

    .a

    Statikus fejlesztői könyvtár

    .c

    C nyelvű forrás

    .0 .cc .cpp .cxx .c++

    C++ nyelvű forrás

    .class

    Java-byte-kód

    .F .f .for

    Fortran nyelvű forrás

    .h

    C/C++ nyelvű headerfájl

    .hxx

    C++ nyelvű headerfájl

    java

    Java nyelvű forrás

    .m

    Objective-C forrás

    .o

    Tárgykódú (object) fájl

    .p

    Pascal-forrás

    .patch

    Különbségállomány

    .perl

    Perl-forrás

    .ph

    Perl-headerfájl

    .pl

    Perl-szkript

    .pm

    Perl-modulszkript

    .py

    Python-forrás

    535

    A függelék: Fejlesztőeszközök

    Állományutótag

    Jelentés

    .pyc

    Lefordított Python-kód

    .S .s

    Assembly kód

    .S0

    Megosztott könyvtár

    .tel .tk

    Tcl-szkript

    Természetesen a lista nem teljes, és nem is lehet az, hiszen mindig születnek újabb jelölések. Az utótagokhoz tartozó nyelvekhez kapcsolódóan a továbbiakban röviden áttekintjük a GNU Compiler Collection fordítócsomagot.

    A.2.1. GNU Compiler Collection (gcc), C++ (g++), Objective-C, Fortran, Java (GCJ), Ada (GNAT) és a Go nyelvek fordítóit foglalja egy csomagba. Ezáltal az előbb felsorolt nyelveken írt programokat mind lefordíthatjuk a GCC-vel. További nyelvekhez (Mercury, Pascal) is léteznek előtétfelületek (frontend), ám ezeknek egy részét még nem integrálták a GCC hivatalos csomagjába. Mi a továbbiakban a C/C++ nyelvekre hagyatkozunk. A GNU Compiler Collection (http: //gcc.gnu.org/) a C

    A.11. gcc A gcc-nek nincs felhasználói felülete. Parancssorból kell a megfelelő paraméterekkel meghívnunk. Használata számos paramétere ellenére egyszerű, ugyanis általános esetben ezeknek a paramétereknek csak egy kis részére van szükségünk. Használat előtt érdemes ellenőrizni, hogy az adott gépen a gcc melyik verzióját telepítették. Ezt az alábbi paranccsal tehetjük meg: gcc -v

    Ebből megtudhatjuk a gcc verzióját, továbbá azt a platformot, amelyre lefordították. A program gyakran használt paramétereit az A.1. táblázat tartalmazza. A.1. táblázat. A gcc leggyakrabban használt paraméterei

    Paraméter

    Jelentés

    -o fájlnév

    A kimeneti állománynév megadása. Ha nem adjuk meg, akkor az alapértelmezett fájlnév a.out lesz.

    536

    A.2. Fordítók

    Paraméter

    Jelentés

    -c

    Fordítás, linkelés nélkül. A paraméterként megadott forrásállományból tárgykódú (object) fájlt készít.

    -Ddefiníció=x

    Definiálja a definíciómakrót x értékkel.

    -Ikönyvtárnév

    Hozzáadja a könyvtárnév paraméterben meghatározott könyvtárat ahhoz a listához, amelyben a .h kiterjesztésű (header) állományokat keresi.

    -Lkönyvtárnéu

    Hozzáadja a könyvtárnév paraméterben meghatározott könyvtárat ahhoz a listához, amelyben a fejlesztői könyvtár- (library) állományokat keresi.

    -llibrary

    A programhoz hozzákapcsolja a library nevű programkönyvtár metódusait.

    -static

    Az alapértelmezett dinamikus linkelés helyett a fordító a statikus programkönyvtárakat linkeli a programba, ha azok rendelkezésre állnak.

    -g, -gN, -ggdb, -ggdbN

    A lefordított állományt ellátja a hibakereséshez (debug) szükséges információkkal A -g opció megadásával a fordító a szabványos hibainformá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 hibakereső értelmez. A paraméter végén megadhatunk egy szintet (N) 0 és 3 között. 0: nincs hibakeresési információ, 3: extrainformációk is belekerülnek. Az alapértelmezett a 2-es szint.

    - 0, -ON

    Optimalizálja a programot az N optimalizációs szintnek megfelelően. Az optimalizáció során kisebb/gyorsabb kód előállítására törekszik a fordító. A szint 0-tól 3-ig választható meg. 0 esetén nincs optimalizáció. Az alapértelmezett 1-es szint esetén csak néhány optimalizációt végez a gcc. A leggyakrabban használt optimalizációs szint a 2-es.

    - Wall

    Az összes gyakran használt figyelmeztetést (warning) bekapcsolja. 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. Egy tetszőleges szövegszerkesztőben elkészítettük a már jól megszokott kódot: /* hello.c */ #include int main() {

    printf("Hello vilag!\n"); return 0; }

    537

    A függelék: Fejlesztőeszközök

    Ezután kipróbáljuk a programunkat Ehhez az alábbi paranccsal fordíthatjuk le: -o héllO h 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. Hibátlan kód esetén a fordító nem ír ki semmilyen üzenetet. Innen tudhatjuk, hogy sikeresen dolgoztunk. Ezután már csak futtatnunk kell a programot: "Vh;04 , A programunk fut, és a konzolon megjelenik a következő felirat:

    Hell o vilag! Vagyis meghívtuk a gcc programot, amely lefordította (compile) a kódot, majd meghívta az ld nevű linkerprogramot, amely a lefordított kódunkhoz hozzáfűzte a fejlesztői könyvtárakból a felhasznált függvényeket, és létrehozta a futtatható bináris állományt Ha csak tárgykódú állományt (object file) akarunk létrehozni (amelynek 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 nevű tárgykódú fájl jött létre. Ez az állomány gépi kódban tartalmazza az általunk írt programot. Ezt természetesen linkelnünk kell még, hogy egy kész futtatható binárist kapjunk: gcc -o hello

    A —o kapcsoló segítségével tudjuk megadni a linkelés eredményeként létrejövő futtatható fájl nevét. Ha ezt elhagyjuk, alapértelmezésben egy a.out nevű állomány jön létre. A következőkben megvizsgáljuk azt az esetet, amikor a programunk több (esetünkben két) forrásállományból áll. Az egyik tartalmazza a főprogramot: /* si ncprg .c */ #i ncl ude double si nc (doubl e) ; int mai n() {

    double x;

    538

    A.2. Fordítók

    printf("Kerem az x-et: "); scanf("%lf", &x); printf("sinc(x) = %6.4f\n", sinc(x)); return 0; }

    A másik implementálja a sinx/x függvényt: /* sinc.c *I #i ncl ude double sinc(double x) {

    return sin(x)/x; }

    A teljes program lefordítása a következő: gcc -o sincprg sincprg.c sinc.c -1m Így a sincprg futtatható fájlhoz jutunk. Ugyanezt megoldhattuk volna több lépésben is: gcc -c sincprg.c gcc -c sinc.c gcc -o sincprg sincprg.o sinc.o -1m Az —/m kapcsolóra azért van szükségünk, hogy a sin() függvényt tartalmazó matematikai programkönyvtárat hozzáfűzzük a programunkhoz. Ha ezt elmulasztjuk, akkor egy hibaüzenetet kapunk a fordítótól arról, hogy nem találja a sin() függvény referenciáját. Az —1 kapcsoló általában 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árakban találhatók. Ha a felhasznált programkönyvtár máshol található, az elérési útvonalat meg kell adnunk az —L kapcsolóval: gcc prog.c -L/home/myname/mylibs mylib.a

    Hasonló problémánk lehet a .h-kiterjesztésű állományokkal (header file). A rendszerhez tartozó headerállományok alapértelmezetten az /usr/include könyvtárban (illetve az innen kiinduló könyvtárstruktúrában) találhatók. Így ha ettől eltérünk, az —I kapcsolót kell használnunk saját headerútvonalak megadásához: gcc prog.c -I/home/myname/myheaders

    539

    A függelék: Fejlesztőeszközök

    Ez azonban ritkán fordul elő, ugyanis a C-programokból általában az aktuális könyvtárhoz viszonyítva adjuk meg a saját .h-állományainkat, vagyis relatív útvonalmegadást használunk:

    gcc prog.c

    . . inlyheeder

    A C-előfeldolgozó (preprocessor) másik gyakran használt funkciója a #define direktíva. Ezt is megadhatjuk közvetlenül parancssorból. A

    gcc —DMAX_ARRAY_S ZÉ=80 priig.c —o Prdg hatása teljes mértékben megegyezik a prog.c programban elhelyezett M~R,

    ZE

    preprocesszordirektívával. Ennek a funkciónak gyakori felhasználása a

    paraméterezés. Ilyenkor a programban feltételes fordítási direktívákat helyezhetünk el a hibakeresés megkönnyítésére:

    #ifdef DEBUG ri ntf CA szül el I riddl t " A fenti esetben látjuk az új szálak indulását debug üzemmódban, amikor a DEBUG makrót definiáljuk. Viszont ez az információ szükségtelen a felhasználó számára egy már letesztelt program esetében, így ekkor újrafordítjuk a programot a DEFINE nélkül.

    A.2.3. LLVM A GNU Compiler Collection mellett egyre nagyobb népszerűségre tesz szert az LLVM (Low Level Virtual Machine, http://llvm.org/), amely a GCC-hez hasonlóan számos nyelvet támogat. Az LLVM egy olyan hatékony fordítóprogram-fejlesztő környezet, amely kész megoldásokat ad számos fordításnál használatos funkcióra, köztük (nyelvfüggetlen) optimalizálásra, köztes belső nyelvre, valamint kódgenerálásra. A clang („C nyelv") projekt az LLVM natív C-fordítója, amely gcc kompatibilis, de figyelemreméltóan gyorsabb annál, továbbá léteznek olyan eszközök, amelyek integrálják a gcc felületét a c/anggal. A C, C++, Objective-C nyelvekhez használhatjuk az LLVM GCC és a Clang felületét is. Könnyen lehet, hogy az eszköz valamilyen formában átveszi a gcc szerepét.

    540

    A.3. Make

    A.3. Make A unixos fejlesztések egyik oszlopa a make program. A Linux rendszereken a GNU make terjedt el (http:/ /www.gnu.org/software/make/) . Ez az eszköz lehetővé teszi, hogy a programunk fordítási menetét könnyen leírjuk és automatizáljuk. Nemcsak nagy programok esetében, hanem akár egy forrásállományból álló programnál is egyszerűbb a fordítás, ha csak egy make parancsot kell kiadnunk az aktuális könyvtárban a fordító paraméterezése helyett. További szolgáltatása a make programnak, hogy egy sok forrásállományból álló program fordításakor csak a módosított állományokat fordítja újra, illetve azokat, amelyekre ezek hatással lehetnek. Ahhoz, hogy mindezeket a funkciókat megvalósíthassa a make egy úgynevezett Makefile 112 -ban le kell írnunk a programunk fordításának menetét a forrásállományok megadásával. Nézzünk erre egy példát: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:

    # Makefile objs = sincprg.o sinc.o libs = -1m sincprg: $(objs) gcc -o sincprg $(objs) $(libs) install: sincprg install -m 644 sincprg /usr/local/bin . PHONY : i nstal 1

    Az 1. sorban egy megjegyzést (comment) láthatunk. A unixos tradíciók alapján a megj egyzést # karakterrel jelöljük. A 3. sorban definiáljuk az objs változót, amelynek értéke: sincprg.o sinc.o.

    A 4. sorban ugyanezt tesszük a libs változóval. A későbbiekben ezt használjuk fel a linkelési paraméterek beállítására. A 6. sorban egy szabály (rule) megadását láthatjuk. Ebben a sincprg állomány az objs változó értékeitől függ. A sincprg állományt hívjuk a szabály céljának (target), és az $(objs) adja a függőségi listát (dependency list). Megfigyelhetjük a változó használatának módját is. A 7. sorban egy parancssor található, ám ez többsoros is lehet. Azt mutatja meg, hogy hogyan készíthetjük el a célobjektumot a függőségi lista elemeiből. Itt állhat több parancssor is szükség szerint, ám minden sort TAB karakterrel kell kezdeni.

    112

    Ha alkalmazzuk a make -f fájlnév szintaxist, akkor a make a megadott fájlt dolgozza fel az alapértelmezett Makefile állománynév helyett. 541

    A függelék: Fejlesztőeszközök

    A 9. sor speciális. Ebben a szabályban valójában nem állomány létrehozása a célunk, hanem az installációs művelet megadása. A 10. sorban végezzük el a programunk telepítését: az install program meghívásával bemásoljuk az /usr/ local/bin könyvtárba. A 12. sor egy probléma megoldását rejti. A 9. sorban a cél nem állomány volt, hanem parancs. Ám, ha mégis létezik egy install nevű állomány, és az frissebb, mint a függőségi listában szereplő sincprg állomány, akkor nem fut le a szabályunk. Ezt küszöböljük ki a . PHONY kulcsszóval, amely módosítja a make működését. Ebben az esetben megadhatjuk vele, hogy az install cél esetén ne figyelje az állomány létét, hanem minden esetben hajtsa végre a szabályt, ha kérjük. Általánosan megfogalmazva a Makefile-ok ötféle dolgot tartalmazhatnak. Ezek a következők: •

    megjegyzések,



    explicit szabályok,



    implicit szabályok,



    változódefiníciók,



    direktívák.

    A.3.1. Megjegyzések A megjegyzések magyarázatul szolgálnak, a make program gyakorlatilag figyelmen kívül hagyja őket. A # karakterrel kell kezdődniük.

    A.3.2. Explicit szabályok A szabály (rule) azt határozza meg, hogy mikor és hogyan kell újrafordítani egy vagy több állományt. Az így keletkező állományokat a szabály céljának vagy tárgyának (target) nevezzük." Hogy egy állomány létrejöjjön, általában más állományokra is szükség van. Ezek listáját nevezzük függőségi listának, feltételeknek vagy előkövetelménynek. Például: fcm

    gc

    H3

    -

    slefsh

    -c g fkíó'.›c

    Ám mint láthattuk, a cél nem minden esetben állomány létrehozása.

    542

    A.3. make A fenti példában a szabály tárgya a foo.o fájl, az eló'követelmény a foo.c, illetve a foo.h állomány. Mindez azt jelenti, hogy szükség van a foo.c és a defs.h állományokra, továbbá a foo.o fájlt akkor kell újrafordítani, ha •

    a foo.o fájl nem létezik,



    a foo.c idó'bélyege későbbi, mint a foo.o időbélyege,



    a defs.h időbélyege későbbi, mint a foo.o időbélyege.

    Azt, hogy a foo.o fájlt, hogyan kell létrehozni, a második sor adja meg. A defs.h nincs a gcc paraméterei között: a függőséget egy - a foo.c fájlban található - #include „defs.h"C nyelvű preprocesszordirektíva jelenti. A szabály általános formája a következő:

    A TARGY egy állománynév. Az ELOKOVETELMENYEK fájlnevek szóközzel vannak elválasztva. A fájlnevek tartalmazhatnak speciális jelentésű, úgynevezett helyettesítő karaktereket (wildcard characters), mint például a „." (aktuális könyvtár), „*" vagy „%" (tetszőleges karaktersorozat), „-" (home könyvtár). A RECEPT szerepelhet vagy az ELOKOVETELMENYEK-kel egy sorban pontosvesszővel elválasztva, vagy a következő sorokban, amelyek mindegyikét egy TAB karakterrel kell kezdeni. A parancssorozat végét egy olyan sor jelzi a make-nek, amelynek az elején nincs TAB karakter. Útmutató Ügyeljünk arra, hogy sok szövegszerkesztő hajlamos a TAB gomb lenyomására space-karaktereket elhelyezni a szövegállományba, és ez később hibát eredményez. Újabban azonban sok szövegszerkesztő az állomány nevéből detektálja, hogy Makefile-t készítünk. Ebben az esetben viszont az állománynév megadásával célszerű kezdenünk.

    Mivel a $ jel már foglalt a változókhoz, ezért ha a parancsban $ jel szerepelne, akkor helyette $$-t kell írnunk. Ha egy sor végére „ \" jelet teszünk, a következő sor az előző folytatása lesz, teljesen úgy, mintha a második sort folytatólagosan írtuk volna. Erre azért van szükség, mert a make minden parancssort külön shellben futtat le. Ezáltal például a cd parancs hatása is csak abban a sorban érvényesül, ahova írjuk. Például:

    cd konyvtar; \ gcc -c -g foo.c Ha a make programnak paraméterként megadjuk egy tárgy nevét, akkor az a szabály hajtódik végre, amely a tárgy előállításához kell. A make felépít egy függőségi fát, amelyet bejárva ellenőrzi, hogy melyik részeket kell újrafordítani. Majd elvégzi a szükséges lépéseket, hogy előállítsa az aktuális célt. 543

    A függelék: Fejlesztőeszközök

    Ha a make programot paraméterek nélkül futtatjuk, akkor automatikusan az első szabály hajtódik végre, valamint azok, amelyektől valamilyen módon függ. Útmutató A make program használatánál ügyeljünk arra, hogy mivel a lépések szükségességét a tárgy- és a függőségállományok időbélyege alapján vizsgálja, ezért ha az időbeállítások megkavarodnak, akkor nem lesz jó a működés. Ilyen helyzet leggyakrabban akkor fordul elő, ha a fejlesztés során gépet váltunk, és a két számítógép órája nem jár szinkronban. Természetesen az idő visszaállítása is okozhat ilyen problémát. Ilyenkor célszerű az időbélyeget aktualizálni a touch paranccsal, és a köztes állományok letörlésével újrafordítani az egész projektet.

    A.3.3. Hamis tárgy Gyakran előfordul, hogy valamilyen műveletre egy szabályt készítünk, amelynek a receptrésze tartalmazza a végrehajtandó parancsokat. A szabály célja ilyenkor nem egy előállítandó állomány, hanem pusztán névként funkcionál az, amellyel a szabályra hivatkozunk. Az ilyen tárgyakat hívjuk hamis tárgynak. Ha azonban valamelyik művelet tényleg létrehozná a tárgyként megadott állományt, és az frissebb lenne a feltételeknél, akkor a szabályhoz tartozó recept parancsai egyáltalán nem hajtódnak végre. Gyakori, hogy nincs megadva feltétel a hamis szabályhoz. Ilyenkor egyáltalán nem hajtódik végre a szabály, ha a tárgyállomány létezik. Azért, hogy elkerüljük a korábban vázolt problémákat, a .PHONY kulcsszóval egyértelműen deklarálnunk kell a hamis célokat. Például: .PHONY: clean

    cl ean: rm *.o prg Ha egy könyvtárban több program forrása is szerepel, akkor gyakran készítünk egy hamis szabályt „all" néven, amely mindent lefordít és előállít. És hogy ez az alapértelmezett szabály is legyen egyben, ezért első szabályként írjuk meg. Például: alt:

    prgl prg2

    .PHONY: all prgl: prgl.o gcc -o prgl prgl.o prg2: prg2.o gcc -o prg2 prg2.o

    544

    A.3. Make

    A.3.4. Változódefiníciók Mint az első példában is láthattuk, gyakran előfordul, hogy egyes állományneveknek több helyen is szerepelniük kell. Ilyenkor, hogy megkönnyítsük a dolgunkat, változókat használunk. Ekkor elegendő 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, megkönnyítve ezzel a későbbi módosításokat. A Makefile-ban a változók típusa mindig szöveg. A változók megadásának szintaxisa a következő:

    Erre a változóra később a

    '.414 vagy a

    szintaxissal hivatkozhatunk. A változó neve nem tartalmazhat „:", „#", „=" és semmilyen üresmezőkaraktert. A gyakorlatban még szűkebb halmazt használunk: betűket, számokat és a „: karaktert. A változók nevénél a rendszer figyeli a kis- és nagybetűket. A tradíció az, hogy a változó nevét csupa nagybetűkkel írjuk. Jelenleg azonban gyakrabban kisbetűs neveket használunk a Makefile-ban, és a nagybetűs nevek inkább rendszerfunkciókra vannak fenntartva. Egyes nagybetűs változók felüldefiniálásával módosíthatjuk a rendszer működését. Emellett néhány karakterkombinációnak speciális jelentése van. (Ezeket lásd az automatikus változókról szóló fejezetben.) Lehetőségünk van arra, hogy a változók értékének megadásakor más változók értékeit is felhasználjuk. Például:

    Prgs' = $(Prgl) ..g.PrOW Van azonban egy olyan tulajdonsága a rendszernek, amelyre figyelnünk kell. A változók kiértékelése a felhasználás helyén történik. Azokat az értékeket veszi figyelembe a rendszer, amelyeket addig definiáltunk, és szükség szerint rekurzívan kifejti. Például: objs = $(objl) $ (obj2) objl = el so.o obj 2 = masodi k.o prg : $ (obj s)

    545

    A függelék: Fejlesztőeszközök

    Amikor a „prg" szabályt meghívjuk, akkor a feltételeknél kiértékeli a program az „objs" változót. Ennek értéke „$(objl) $(obj2)". A program kiértékeli mindkét változót, így megkapjuk az „elso.o masodik.o" szöveget, amelyet behelyettesít. Ennek a megoldásnak van azonban egy másik oldala is. A változók használatakor kerülnünk kell a következő megoldást: objs = elso.o objs = S(objs) masodik.o Azonnal arra számítanánk, hogy szekvenciálisan hajtódnak végre a műveletek, és a végeredményként az „elso.o masodik.o" szöveg helyettesítődik majd be a „$(objs)" helyére. Ezzel szemben egy végtelen ciklust idézünk elő, és a behelyettesítés helyén végtelen „masodik.o" szöveg lesz, mivel ez az utoljára beállított érték.

    A.3.5. A változó értékadásának speciális esetei Láthatóan a felhasználás helyén elvégzett rekurzív kiértékelés csapdákat rejt magában. Erre megoldás az egyszerű kiértékelés, amelyet a definiálás helyén végez el a rendszer. Ennek jele a „:=" karakterkombináció. A működést a következő példa szemlélteti:

    a := egy b := $(a) ketto a := harom Ez egyenlő azzal, mintha a következőket írtuk volna: b := egy ketto a := harom

    Az előzőekben említett hibás példára is van helyes megoldás. Vagyis megoldható, hogy a változó értékéhez hozzáadjunk további szövegeket. Ezt a „+=" jellel tehetjük meg. A korábban látott példa helyesen a következő: objs = elso.o objs += masodik.o Így az „objs" változó használatakor az eredmény nem végtelen ciklus lesz, hanem az „elso.o masodik.o" szöveg. Az eddig látottak mellett van lehetőségünk feltételes értékadásra is. Ebben az esetben a változónak csak akkor adunk értéket, ha még nincs definiálva. Ennek jele a „?=". Az alábbi példán nézzük meg a működését:

    546

    A.3. Make

    = egy . ketto

    7=

    Ez egyenlő a következővel: egy

    Bár ebben a példában nem látszik közvetlenül az eszköz haszna, ám egy öszszetett, elágazásokat vagy további állományokat tartalmazó Makefile esetén nagyon hasznos tud lenni.

    A.3.6. Többsoros változók definiálása A define direktíva használatával is adhatunk a változóknak értéket. Szintaxisa lehetővé teszi, hogy az érték újsor-karaktereket is tartalmazzon: define ket-sor = echo $(a) echo ketto endef

    A define szót a változó neve követi. Ezt követi a művelet. Alapértelmezett a „=" jel, amelyet el is hagyhatunk. Ám használható a „+=" jel is, ezáltal bővíthetjük a változó korábbi értékét. A műveleti jel után mást már nem írhatunk az első sorba. Az értékek a következő sorokban következnek, és a blokkot az endef szó zárja. Az endef előtt szereplő újsor-karakter már nem számít bele az értékadásba. Láthatóan az értékadásnál hivatkozhatunk más változókra is, ahogy a korábbi esetekben is. Szükség esetén meg is szüntethetjük a definiált változót az undefine kulcsszóval: undefine ket-sor `

    A.3.7. A változó hivatkozásának speciális esetei A helyettesítő hivatkozás révén lehetőségünk van arra is, hogy a változóra való hivatkozás során az értékét módosítva helyettesítsük be. Ilyenkor egy konverziós szabályt definiálhatunk. A konverzió során az értéket szavanként kezeljük, és a szavak végén található szövegrészleteket cseréljük le más karaktersorozatokra. Nézzünk egy példát: s

    547

    A függelék: Fejlesztőeszközök

    Ebben a példában az „objs" változó értéke „elso.o masodik.o" lesz. Vagyis egy szólistából könnyen előállíthatunk módosított listákat is. A helyettesítő hivatkozás hivatalos formátuma a következő: $ (val tozo : a=b)

    vagy ${val tozo : a=b}

    Itt a „valtozo" egy változó, amely szavakból álló listát tartalmaz. A művelet minden szó végén lecseréli az „a" szövegrészletet „b" szövegre. Egy másik speciális változó-hivatkozásfajta a számított változónevek használata. Ebben az esetben a hivatkozott változó nevét egy másik változó tartalmazza. Egy példán demonstrálva: a=b b=c eredm := $($(a)) A „$(a)" értéke „b". Ezt behelyettesítve a „$($(a))" kifejezésbe „$(b)"-t kapunk. Ennek értéke „c". Vagyis az „eredm" változó értéke „c" lesz. A számított változóneveket a helyettesítő hivatkozásokkal is kombinálhatjuk: src_1 := elso.c src_2 := masodik.c objs := $(src_S(a): .c=.o)

    Az „a" változó értékétől függően vagy az „src_1" vagy az „src_2" értékéből állítjuk elő az „objs" értékét, és ennek során a „.c" kiterjesztést „.o"-ra cseréljük.

    A.3.8. Automatikus változók A receptek készítésénél gyakran sokat segít, ha a forrás- és a célállományok nevét nem kell minden esetben beírnunk, hanem a szabály céljából és feltételeiből generálhatjuk. Ez az eddig tárgyalt esetekben könnyebbséget jelent, ám több később tárgyalt esetben nélkülözhetetlen eszköz. Általános szabályokat nem tudunk úgy definiálni, hogy az állományneveket ne generálnánk, hiszen ezekben az esetekben nem tudunk konkrét állományokat megadni. Az állománynevek előállítását az automatikus változókkal végezzük. Az A.2. táblázat foglalja össze az automatikus változókat a jelentésükkel.

    548

    A.3. Make A.2. táblázat. Automatikus változók

    Automatikus változó

    Jelentés

    $@>

    A célállomány neve.

    $