Referenciák
A gondok mindig a mutatókkal kezdődnek, de ez itt még álnevekkel
is kombinálódik...
Perl 4-ben kicsit nehézkes volt a bonyolult adatstruktúrák kezelése, de
most már vannak referenciák, így már mindazt a borzalmat el lehet követni,
amit egy C programban csak el tudunk képzelni. Sőt még többet is, mert
egy C programban nem lehetett a változóneveket menet közben manipulálni.
Referencák létrehozása
Az ötös verziótól kezdve Perlben három fajta referenciát különböztetünk
meg: "hard", "soft", és "szimbolikus" referenciákat. A soft referenciával
egy már meglévő változóra mutathatunk:
Legegyszerűbb a \ operátor használata. Ezzel egy újabb hivatkozást készíthetünk
egy változóra (egy már biztosan van a szimbólumtáblában):
$scalarref = \$scalar; # pointer egy skalárra
$arrayref = \@ARGV; # pointer egy skalár tömbre
$hashref = \%ENV; # pointer egy asszociatív tömbre
$coderef = \&handler; # pointer egy alprogramra
Hard referenciával egy új objektumot hozhatunk létre:
# Névtelen dolgokra is lehet hivatkozni:
$arrayref = [1, 2, ['a', 'b', 'c']];
$hashref = {
'Ádám' => 'Éva',
'Clyde' => 'Bonnie',
};
$coderef = sub { print "Nem nyert!\n"; };
Hard referenciáknál a Perl egy úgynevezett referenciaszámlálóval dolgozik,
amely azt tárolja, hogy hányan hivatkoznak az adott objektumra. Ha ez az
érték 0, akkor a garbage collector felszabadítja az adott objektumot. Ez
viszont azt jelenti, hogy pl. egy két irányba láncolt lista soha nem fog
"kézi segítség" nélkül felszabadulni.
Referenciák használata
Nagyon egyszerű egy referenciát használni: a programban egy változó neve
helyére bárhova beírhatunk egy megfelelő típusú referenciát tartalmazó
változót.
-
Az egyszerű esetek, amikor egy egyszerű struktúrát szeretnénk használni:
$ketto = $$scalarref;
print "A program neve:", $$arrayref[0], "\n";
print "HOME=", $$hashref{'HOME'};
&$coderef(1, 2, 3);
Persze lehet mutatni mutatóra is:
$refrefref = \\\"valami";
print $$$$refrefref;
-
Bármilyen hely, ahol egy referenciát kapnánk meg, helyettesíthető egy blokkal,
amely értéke a referencia lesz:
$ketto = ${$scalarref};
print "A program neve:", ${$arrayref}[0], "\n";
print "HOME=", ${$hashref}{'HOME'};
&{$coderef}(1, 2, 3);
No persze a blokk lehet bonyolultabb is:
&{ $inditas{$index} }(1, 2, 3); # megfelelő eljárás indul
$$hashref{"KEY"} = "VALUE"; # 1. eset
${$hashref}{"KEY"} = "VALUE"; # 2. eset
${$hashref{"KEY"}} = "VALUE"; # 3. eset
${$hashref->{"KEY"}} = "VALUE"; # 4. eset
Itt az 1. és 2., illetve a 3. és 4. eset egyenértékű.
-
A fenti esetek egyszerűsítésére szolgál a -> operátor:
print "A program neve:",$arrayref->[0],"\n";
print "HOME=",$hashref->{'HOME'};
Ennek balértéke bármilyen kifejezés lehet, amely egy referenciát ad vissza.
Ez az operátor el is hagyható {} vagy [] zárójelek között (de tényleg csak
közöttük!):
$array[$x]->{"valami"}->[0] = "január";
$array[$x]{"valami"}[0] = "január";
Ezzel el is jutottunk a többdimenziós C-beli tömbökhöz:
$tomb[42][4][2] += 42;
A kódrészre mutató hard referenciáknál fontos megjegyezni, hogy még az
ugyanazzal az utasítással generált blokkok is teljesen független eljárásoknak
számítanak, pl. a my kulcsszóval definiált változóik különbözőek:
sub newprint {
my $x = shift;
return sub { my $y = shift; print "$x $y\n"; };
}
$h = newprint("Hello");
$g = newprint("Papa");
.
.
.
&$h("world");
&$g("mama");
Ez a következőt fogja kiírni:
Hello world
Papa mama
Szimbolikus referenciák
Ha a fent említett hivatkozásokban egy blokk nem egy változó referenciájával,
hanem egy string-gel tér vissza, a nyelv akkor sem esik kétségbe. Szorgalmasan
elkezdi böngészni a szimbólumtáblát, hátha talál egy ilyen bejegyzést:
$nev = "ize";
$$nev = 1; # $ize
${$nev} = 2; # $ize
${$nev x 2} = 3; # $izeize
$nev->[0] = 4; # $ize[0]
&$nev(); # &ize()
$modulom = "MAS";
${"${modulom}::$nev"} = 5; # $MAS::ize
Ha ez a szabadság nem tetszik nekünk, akkor megköthetjük kezünket a
use
strict; használatával.
OOP
Lehet enélkül manapság nyelvet készíteni?
Amit a Perl objektumokról tudni kell:
-
Egy objektum csak egy egyszerű referencia, amely történetesen tudja, hogy
melyik osztályhoz tartozik.
-
Egy osztály egy package, amely tudja az objektum hivatkozásokat kezelni,
és van némi támogatása az öröklésre.
-
Egy metódus az egy egyszerű alprogram, amelynek az első paramétere egy
objektum referencia lesz.
Objektum
Az objektum bármilyen referencia lehet, ami meg van
áldva azzal
a tudással, hogy hol jött létre:
package ValamiUj;
sub new { bless {} } # 1. verzió
sub new { # kicsit bonyolultabban...
my $self = {};
bless $self; # így nem tudok leszármazni belőle
$self->initialize();
return $self;
}
Az így definialt osztályok esetén "osztály szintű" lesz a new. A következő
példa pedig azt mutatja, hogy miként lehet "objektum-szintű" new:
sub new {
my $this = shift;
my $class = ref($this);
my $self = {};
bless $self, $class; # így le tudok származni belőle
$self->initialize();
return $self;
}
A referencia általában egy asszociatív tömb szokott lenni, amely aztán
az objektum saját kis szimbólum táblájaként szolgál. A konstruktornak kell
magának allokálnia szükséges memóriát. Nem hívódnak meg az ősosztályok
konstruktorai.
Objektumok örököltetése
Egy objektum létrehozása után bármelyik alkalmazásunkban használható.Előfordulhat
azonban, hogy az objektum metódusai és adattagjai nem teljesen felelnek meg a kívánalmainknak.
A szükséges változtatásokat megtehetjük egyenként az osztályt használó alkalmazásokban, a megfelelő kód alkalmazásával,
de ez a megoldás - ha több különböző programban ugyanazokra a módosításokra van szükség,
nagy munkával jár. Az örököltetés egy egyszerűbb megoldást ajánl.
öröklődés segítségével egy alaposztály minden definícióját és metódusát átmentjük egy
származtatott osztályba, ahol kiegészíthetjük saját metódusainkkal, adatainkkal.
Fontos, hogy örököltethető osztály előállításakor a
bless függvény kétargumentumos
változatát használjuk, mivel a második argumentum nélkül létrehozott példányok egyargumentumos
bless esetén nem a megfelelő osztályhoz lennének hozzárendelve:
return bless $self, $class;
Egy osztály örököltetéséhez először az alaposztály inportálása szükséges az új osztály moduljába,
majd a @ISA használata. Ez utóbbival fejezhetjük ki hogy az új osztály a régi egy más formája.
Példa:
Csinálunk egy Photo osztályt, majd egy új objektumosztályt hozunk
létre, a PhotoNegative nevűt:
package Photo;
use strict; #nem biztonságos konstrukciók eliminálásáért felelős
sub new {
my $class = shift;
#Az osztály nevének meghatározása mindig az első paraméter
my $self = {}; # objektum referencia
# A fénykép tulajdonságainak definíciója:
$self->{ID} = undef;
$self->{TITLE} = undef;
$self->{PHOTOGRAPHER} = undef;
$self->{PHOTODATE} = undef;
#fénykép tulajdonságainak beállítása
my ($id, $title, $photographer, $photodate) =@_;
$self->{ID} = $id;
$self->{TITLE} = $title;
$self->{PHOTOGRAPHER} = $photographer;
$self->{PHOTODATE} = $photodate;
#objektum foganatosítása és visszatérés:
return bless $self, ref($class) || $class;
}
sub return_title (
my $self = shift;
print "A fénykép címe: $self->{TITLE}\n";
}
#A főprogram:
package main;
use Photo;
my $photo = new Photo(13, "A Mátra ősszel", "Makai László", "1997. okt. 1.");
$photo -> return_title();
#És a PhotoNegative:
package PhotoNegative;
use Photo;
@ISA = ("Photo");
Ha ugyanitt ezt az új osztályt szeretnénk használni:
package main;
use PhotoNegative;
myphoto = new PhotoNegative(13, "A Mátra ősszel", "Makai László", "1997. okt. 1.");
$photo -> return_title();
A további metódusokkal való bővítést az alábbi példában figyelhetjük meg:
package PhotoNegative;
use Photo;
@ISA = qw(Photo);
sub return_photographer { # új metódus, a fénykép készítőjének nevét adja vissza
my $self = shift;
print "A fényképet készítette: $self ->{PHOTOGRAPHER}\n";
}
sub return_all { # új metódus, minden adatot visszaad
my $self = shift;
print "A fénykép azonosítója: $self->{ID}\t";
print "A fénykép címe: $self->{TITLE}\n";
print "A fényképet készítette: $self->{PHOTOGRAPHER}\t";
print "A fénykép készült: $self->{PHOTODATE}\n;
}
# az ID, TITLE... a Photo-ban tárolt adatok, amiket a PhotoNegative örökölt.
package main;
use PhotoNegative;
myphoto = new PhotoNegative(13, "A Mátra ősszel", "Makai László", "1997. okt. 1.");
$photo -> return_title();
$photo -> return_photographer();
$photo -> return_all();
Amennyiben az alaposztály készítésénél nem adjuk meg a második paramétert a bless-ben,
hibajelzést kapunk futás közben, mivel a fordító így nem találja meg a return_photographer
ill. a return_all metódust.
Ha az alaposztály nem tartalmaz az utód számára fontos adattagokat, akkor ezeket is beépíthetjük
az új osztályba, ami a new metódus felüldefiniálását is magába foglalja. Ekkor először az
alaposztály konstruktorát hívjuk meg, majd elkészítjük a származtatott osztály új adatmezőit:
package PhotoNegative;
use Photo;
@ISA = qw(Photo);
sub new {
my ($class, $ID,$TITLE, $PHOTOGRAPHER, $PHOTODATE, $NEG,$PRINTS) = @_;
my $self = Photo->new($ID, $TITLE, $PHOTOGRAPHER, $PHOTODATE);
$self->{NEGNUMBER} = undef;
$self->{PRINTSAVAIL} = undef;
$self->{NEGNUMBER} = $NEG;
$self->{PRINTSAVAIL} = $PRINTS;
return bless $self, ref($class) || $class;
}
sub return_photographer.......
sub return_all {
my $self = shift;
print "A fénykép azonosítója: $self->{ID}\t";
print "A fénykép címe: $self->{TITLE}\n";
print "A fényképet készítette: $self->{PHOTOGRAPHER}\t";
print "A fénykép készült: $self->{PHOTODATE}\n;
print "A negatív sorszáma: $self->{NEGNUMBER}\n;
print "A meglévő másolatok száma: $self->{PRINTSAVAIL}\n;
}
Osztály
Az osztály egy package. Nincs semmi különös jelölés arra, hogy ez egy osztály,
sőt még az inicializáló eljárást sem kell
new-nak hívni.
Egy osztályban csak a metódusok öröklésére van támogatás az @ISA
tömbön keresztül. Ez egy modul neveket tartalmazó tömb. Ha egy metódust
nem található meg az aktuális package-ban, akkor az ebben a tömbben felsorolt
modulok lesznek bejárva a hiányzó alprogramért. Ez egy mélységi keresés
lesz. Ha itt sem talál semmit, és van egy AUTOLOAD nevű függvény, akkor
megpróbálja ezzel előszedetni a hiányzó eljárást. Ha ez a lehetőség sem
járt sikerrel, akkor egy UNIVERSAL-nak nevezett modulban fog keresgélni
a rendszer.
Az @ISA tömb szépsége az, hogy menet közben is lehet módosítani, azaz
menet közben megváltoztathatjuk egy osztály leszármazási fáját!
Nyilvánvalóan az adattagok öröklésére is szükség van egy objektum-orientált
nyelvben, ez a Perlben az @ISA tömb segítségével megvalósítható.
package A;
sub new {
my $type = shift;
my $self = {};
$self->{'a'} = 42;
bless $self, $type;
}
package B;
@ISA = qw( A ); # A a B ősosztálya
sub new {
my $type = shift;
my $self = A->new;
$self->{'b'} = 11;
bless $self, $type;
}
package main;
$c = B->new;
print "a = ", $c->{'a'}, "\n";
print "b = ", $c->{'b'}, "\n";
Metódus
Semmi különös jelölés nincsen rájuk, kivéve a destruktorokat.
Alapvetően kétfajta metódus van:
-
statikus metódus
-
Ez első paraméterében az osztály nevét kapja meg:
package ValamiUj;
sub holvagyok {
my ($neve) = @_;
print "holvagyok: $neve\n";
}
Ez a következőképpen hívható:
ValamiUj-holvagyok();
-
"rendes" metódus
-
Ez az első paraméterében egy referenciát vár.
package ValamiUj;
sub new {
my $self = {};
bless $self;
$self->{elso} = 1; # ez {"elso"}-vel egyenértékü
$self->{ize} = 42;
return $self;
}
sub kiir {
my $self = shift;
my @keys = @_ ? @_ : sort(keys(%$self));
foreach $key (@keys) {
print "\t$key = $self->{$key}\n";
}
}
És ennek hívása:
use ValamiUj;
$obj = ValamiUj::new();
$obj->kiir(); # C++ stílus
kiir obj "elso"; # "print" stílus
Destruktor is definiálható az osztályhoz, ha a modulban megadunk egy
DESTROY
nevű eljárást. Ez akkor lesz meghívva, ha a rendszer az objektumhoz tartozó
memóriaterületet fel akarja szabadítani.
Osztály metódusainak felüldefiniálása(overriding)
Az alaposztály metódusainak felüldefiniálása nagyon egyszerű, kétféleképpen valósítható meg. Az első teljes
mértékben felülírja az alapobjektum metódusát, a második úgy definiálja felül a metódust, hogy az általunk beépített
új funkciónak megfelelően viselkedjen, és utána hívja meg az alapobjektum metódusát.
A függvény teljes felüldefiniálását a függvény újraírásával valósíthatjuk meg. Maradva a Photo alaposztálynál, és a
PhotoNegative származtatott osztálynál a példánk legyen a return_title metódus teljes felüldefiniálása.
Az eredeti metódusunk a Photo-ban az alábbi:
sub return_title {
my $self = shift;
print "A kép címe: $self->{TITLE}\n";
}
A felüldefiniált pedig a PhotoNegative-ban az alábbi:
sub return_title {
my $self = shift;
print "A negatív sorszáma: $self->{NEGNUMBER}\t";
print "A kép címe: $self->{TITLE}\n";
}
Észrevehető azonban, hogy az új függvény részben megduplázza az eredeti osztály kódját.
Ez a második módszerrel megkerülhető, ha felüldefiniáljuk az eredeti metódust úgy, hogy az meghívja
az alaposztály eredeti return_title metódusát. Az alábbi kód ezt valósítja meg:
sub return_title {
my $self = shift;
print "A negatív sorszáma: $self->{NEGNUMBER}\t";
$self->SUPER::return_title();
}
A super referencia a szülöosztályra hivatkozik anélkül, hogy explicit módon meg kéne adni a szülőosztály nevét.
Annak eldöntésére, hogy melyik módszert használjuk, tisztáznunk kell, hogy bővítésre vagy cserére van szükségünk.
Amikor csak lehet, kerüljük a teljes lecserélést.
Itt következik egy példa, ami egy alapvető példa az öröklődésre, letölthető
zip
formátumban is.
=======
Base.pm
=======
# package deklaráció
package Base;
# strict modul mindig jó
use strict;
# ez egy konstruktor, amit csak osztályra lehet hívni
# írhatunk olyan konstruktort is, amit hívhatunk objektumra is, és akkor
# egy ugyanolyan típusú másik objektumot ad vissza (akár copy construktort is
# írhatunk ilyen módon, de az egyszerűség kedvéért ezt most kihagyjuk
sub new {
# minden egyes metódus első paraméterében megkapja az osztály nevét
# ezt átvesszük itt a $type változóba
my ($type) = @_;
# ez a módja annak, hogy egy olyan referenciát adjunk vissza, ami egy
# objektumra mutat. A bless függvény az első paraméterben megadott
# referencia által mutatott dolgot $type típusúra állítja, majd
# visszaadja az objektumot, majd pedig visszaadjuk a new hívójának az
# objektumra mutató referenciát
return bless {}, $type;
}
# ez a metódus fogja megmondani, hogy milyen típusa van annak az osztálynak
# illetve objektumnak, amire meghívtuk ezt a metódust
sub whoami {
# Perlben általában self-nek hívjuk a típust, ha nem tudjuk, hogy
# objektumot, illetve osztályt kapunk.
my ($self) = @_;
# ez csak egyszerű változódeklaráció, type-nak szoktuk hívni az osztály
# nevét, azaz ez egyszerűen egy string lesz
my $type;
# ha a metódust egy objektumra hívjuk, akkor az objektumot kapjuk első
# paraméterben. Ezt onnan dönthetjük el, hogy megvizsgáljuk a kapott
# paramétert, hogy referencia-e.
if ( ref($self) ) {
# ha referencia, akkor objektumok (bless-elt dolgok) esetében a
# ref függvény az objektum típusát adja vissza, ezért a type
# legyen ez az érték
$type = ref($self);
}
else {
# ha nem referencia, akkor a típust kaptuk első paraméterben
$type = $self;
}
# nincs más dolgunk, mint visszaadni a típust
return $type;
}
# destruktor, ami automatikusan meghívódik, mielőtt felszabadul az objektum
sub DESTROY {
# átvesszük a paramétert
my ($self) = shift;
# átadjuk a kapott paramétert a whoami-nak, aki majd jól eldönti, hogy
# ez egy osztály (type) vagy pedig egy objektum, az eredményt eltároljuk
# a whoami nevű változóban
my $whoami = $self->whoami();
# végül felszabadulás előtt kiírunk egy üzenetet
print "$whoami being destroyed...\n";
}
# a require kedvéért, amit a use hív meg
1;
==========
Derived.pm
==========
# package deklaráció
package Derived;
# strict modul mindig jó
use strict;
# öröklünk a Base osztálytól
use base qw(Base);
# a require kedvéért, amit a use hív meg
1;
=======
test.pl
=======
#!/usr/bin/perl -w
# strict modul a szép programért
use strict;
# használjuk a Base és a Derived osztályokat (csomagokat)
use Base;
use Derived;
# egy új Base objektum létrehozása
# itt látható az egyik metódushívási szintaxis:
# metódus osztálynév paraméterlista
# a konstruktornak most nem adtunk át paramétereket
my $base = new Base;
# új Derived objektum létrehozása
my $derived = new Derived;
# megkérjük az objektumot, hogy írja ki, hogy ő melyik osztálynak példánya
# bővebb magyarázatot lásd a metódus implementációjánál
# ez a másik metódushívási szintaxis:
# osztály->metódus(paraméterlista)
# illetve
# objektum->metódus(paraméterlista);
print $base->whoami(), "\n";
print $derived->whoami(), "\n";
# a következő úgy viselkedik, mintha dinamikus kötés történne
# valójában nem történik ilyesmi, egyszerűen csak egy referenciát állítunk át
# egy másik objektumra
$base = $derived;
print $base->whoami(), "\n"; # eredmény: 'Derived' !
A test.pl program kimenete pedig:
Base
Derived
Base being destroyed...
Derived
Derived being destroyed...
Itt következik egy másik példa, ami letölthető
zip
formátumban is. A példa igyekszik sokmindent bemutatni, mint például az
öröklést, a statikus és objektumszintű metódusokat, statikus és objektumszintű
attribútumokat, absztrakt osztályt, copy konstruktort, többszörös öröklődést.
A többi magyarázat a szükséges helyeken megjegyzésenként található.
=======
Alma.pm
=======
package Alma;
use strict;
use vars qw($miez);
# ez a modul exportálja a croak eljárást
use Carp;
# osztályszintű attribútum, osztályszintű volta a package és az osztály
# fogalmanak Perlbeli analógiájából következik
$miez = 'Absztrakt alma osztály';
# Perlben nincs new kulcsszó egy új objektum létrehozására, minden new
# metódust mi írunk, ezek lesznek a konstruktorok a programozó számára
# a Perl számára ebben a metódusban nincs semmi különleges
sub new {
my ($self) = @_;
# itt döntjük el azt, hogy ez most objektumszintű, vagy osztályszintű
# konstruktorként lett-e meghÍvva
my $type = ref $self ? ref $self : $self;
# szimulálhatjuk, hogy az osztály absztrakt
# ha egy gyerek osztályra hívódik meg a konstruktor, akkor
# a $type változóba annak az osztálynak neve a kerül, ellenkező
# esetben Alma lesz, ilyenkor kivételt dobunk
# a croak eljárás előnye a die-val szemben, hogy a hibát a hívásnál
# jelzi, ami sokkal hasznosabb ebben az esetben, mintha az osztályban
# jelezné. Érdemes még utánanézni ugyanezen modul confess metódusának,
# ami a teljes hívási vermet kiírja
# a croak eljárás előnye a die-val szemben, hogy a hibát a hívásnál
# jelzi, ami sokkal hasznosabb ebben az esetben, mintha az osztályban
# jelezné. Érdemes még utánanézni ugyanezen modul confess metódusának,
# ami a teljes hívási vermet kiírja.
croak "Az Alma osztály nem példányosítható" if $type eq "Alma";
if ( ref $self ) {
# objektumszintű konstruktorként meghívva a konstruktor
# copy konstruktorként fog viselkedni
# létrehozunk egy új "objektum-kezdeményt"
my $new = {};
# átmásolunk minden attribútumot
# vegyük észre, hogy ehhez nem kell tudnunk, hogy milyen
# attribútumokkal rendelkezik az adott objektum, így módosítás
# nélkül működni fog a gyerekosztályokra is
%$new = %$self;
# objektumot készítünk, megmondjuk a típusát is
bless $new, $type;
# visszaadjuk az új objektumot
return $new;
}
else {
# osztályszintű konstruktorként egy új objektumot
# inicializálunk
# az egészség egy objektumszintű attribútum
return bless { egeszseg => 100 }, $type;
}
}
# objektumszintű metódus, mégpedig azért, mert felhasználja az átadott
# objektumot. Osztályra hívva nem működik, ám ez csak futási időben derül ki!
sub egeszseg {
my ($self) = @_;
return $self->{egeszseg};
}
# A következő metódus néhány csúnya dolgot muvel az osztályszinu attribútumok
# virtuális örököltetése szimulálásának egy lehetséges megoldásáért.
# Bár ez más nyelvekben általában nem így működik, némi hackkel megoldható
# Perlben.
# A problémát az okozza, hogy a $miez változó osztály, pontosabban package
# szintű. Ha ezekután valaki meghívja a miez metódust egy gyermekosztályra,
# és abban ez a metódus nem lett felüldefiniálva, akkor a Perl megtalálja
# azt az Alma osztályban. Ám a miez ekkor az Alma osztály $miez változóját
# fogja elérni, ezért, ha a gyerekosztályban is definiálunk egy $miez változót,
# és mi most azt szeretnénk, hogy a miez annak az értékét adja vissza, csalódni
# fogunk.
# Ezt a problémát megoldja a következő metódus. Ezután ha egy gyerekosztályban
# felüldefiniáljuk a $miez változó értékét, az Alma osztálybeli miez metódus
# akkor is azt adja vissza.
sub miez {
# átvesszük az osztályt/objektumot
my ($self) = @_;
# meghatározzuk az osztály, azaz a package nevét
my $package = ref $self ? ref $self : $self;
# ki kell kapcsolni a strict referenciákat, mert a következő művelet
# ellenkező esetben fordítási hibát okozna
no strict qw(refs);
# a megadott csomagban lévő $miez változó elérése
my $miez = ${"$package\::miez"};
# egy apró ellenőrzés: ha az adott osztályban mégsincs ilyen változó,
# akkor az Alma osztály $miez változójának értékét adjuk vissza
defined $miez ? $miez : $Alma::miez;
}
1;
==========
Jonatan.pm
==========
package Jonatan;
use strict;
# öröklés az Alma osztályból
use base qw(Alma);
sub new {
my ($self) = @_;
# az ősosztály konstruktorának meghívása
# annak ellenére, hogy a bless az ősosztályban hajtódik végre,
# az objektum típusa Jonatan lesz, az átadott $self miatt
# az ősosztály konstruktorának meghívása
# annak ellenére, hogy a bless az ősosztályban hajtódik végre, az
# objektum típusa Jonatan lesz, az átadott $self miatt, ami vagy egy
# Jonatan típusú objektum, vagy a Jonatan osztály neve
my $obj = $self->SUPER::new;
# a friss Alma tulajdonságokkal rendelkező Jonatan objektum felruházása
# a Jonatanra jellemző tulajdonságokkal. Az átadott undef értékkel
# jelezzük, hogy a pirosság alapértelmezett értékét szeretnénk felvenni.
# a jonatanizál metódus visszadja az 'elkészült' objektumot, amit ezért
# mi is visszaadhatunk.
return $self->jonatanizal(undef, $obj);
}
# ez a metódus ruház fel egy objektumot Jonatan tulajdonságokkal, amiből
# pillanatnyilag egy van: a pirosság
sub jonatanizal {
# első paraméterben az objektumot kapjuk, ha osztályra hívnánk meg a
# metódust, akkor sem lenne gond, csak a $obj objektumot módosítja
my ($self, $pirossag, $obj) = @_;
# amennyiben az objektum már jonatanizált, akkor ezt nem tesszük meg
# újra, lévén ez egy inicializációs metódus, a jonatanizált objektum
# pedig már inicializálva van
return $obj if defined $obj->{jonatanizalva};
# a pirosság egy új objektumszintű attribútum
# ha a második paraméterben nem adtunk meg pirosságot
if ( defined $pirossag ) {
$obj->{pirossag} = $pirossag;
}
else {
$obj->{pirossag} = 6;
}
$obj->{jonatanizalva} = 1;
return $obj;
}
# a mi Jonatan almánk egészsége függ a pirosságától is :)
# minden metódus "virtuális", ezért könnyedén felüldefiniálhatóak
sub egeszseg {
my ($self) = @_;
return $self->{egeszseg} * 0.9 + $self->{pirossag} * 0.1;
}
1;
=========
Golden.pm
=========
# a Golden alma a a Jonatannal teljesen analóg módon készül, ezért ide nem
# írok megjegyzéseket
package Golden;
use strict;
use base qw(Alma);
use vars qw($miez);
$miez = 'Golden alma';
sub new {
my ($self) = @_;
my $obj = $self->SUPER::new;
$self->goldenizal(undef, $obj);
}
sub goldenizal {
my ($self, $sargasag, $obj) = @_;
return $obj if defined $obj->{goldenizalva};
if ( defined $sargasag ) {
$obj->{sargasag} = $sargasag;
}
else {
$obj->{sargasag} = 6;
}
$obj->{goldenizalva} = 1;
return $obj;
}
sub egeszseg {
my ($self) = @_;
return $self->{egeszseg} * 0.9 + $self->{sargasag} * 0.1;
}
sub ferges_e {
my ($self) = @_;
return $self->egeszseg() <= 0.2;
}
1;
===========
Goldatan.pm
===========
# Tudós biológusok kertészekkel együttműködve keresztezték a Goldent, és a
# Jonatant, igy született meg a Goldatan
package Goldatan;
use strict;
use base qw(Jonatan Golden);
1;
# A Goldatannal egyedül az volt a baj, hogy semmi olyan tulajdonságot nem
# mutatott, amit amúgy a Goldenek szoktak. Egyáltalán nem volt sárga. Egy
# icipici sárgasága sem volt. Furcsa módon viszont a férgességét meg tudták
# vizsgálni, ami viszont a Jonatanokra nem, csak a Goldenekre jellemző.
# Egyébként mindig csak a Jonatan viselkedése érvényesült.
# A biológusok és a kertészek értetlenül álltak a jelenség előtt.
#
# Ám akkor jött egy programozó, és megtalálta a probléma okát.
# "A gond az, hogy örököltünk a Jonatan osztályból is, és a Golden osztályból
# is. Perlben ez ugye abban nyilvánuk meg, hogy mindketten belekerülnek az
# @ISA tömbbe. Egy metódus keresésekor először az első osztály metódusai között
# keresünk, és ott meg is találjuk az egészségi állapotra vonatkozó metódust, és
# nem keresünk tovább. Ezért lesznek a Golden metódusai, a new és az egészség
# elérhetetlenek. Viszont ferges_e metódusa nincs a Jonatan osztálynak, ezért
# ebben az esetben meghivódik a Golden metódusa.
# A Perl erre nem figyelmeztet! De miért nem? Azért, mert nem történik semmi
# illegális. Valójában örökléskor néhány osztály neve belekerül egy tömbbe.
# Rendben, ez valóban nem illegális. Metódushivásnál pedig megtaláljuk a
# keresett metódust, ebben sincs semmi rossz.
# Éppen ezért erre nekünk kell odafigyelni."
===========
Jolden.pm
===========
# Ezek után a programozó bemutatta, hogy mire is gondolt, és megalkotta a
# Jolden almát.
package Jolden;
use strict;
use base qw(Jonatan Golden);
sub new {
# az új konstruktorban átvesszük az első paramétert, a $self-et
my ($self) = @_;
# meghívjuk az Alma osztálybeli konstruktort, ezért működni fog ez copy
# konstruktorként is!
my $obj = $self->Alma::new;
# felruházzuk az objektumot a speciális tulajdonságokkal
$self->goldenizal(undef, $obj);
$self->jonatanizal(undef, $obj);
# visszaadjuk az objektumot
return $obj;
}
# a programozó úgy alkotta meg a Joldent, hogy mindkét ősalmától függjön
# az egészsége
sub egeszseg {
my ($self) = @_;
return Jonatan::egeszseg($self) * 0.5 + Golden::egeszseg($self) * 0.5;
}
1;
==========
objtest.pl
==========
#!/usr/bin/perl -w
use strict;
use Alma;
use Jonatan;
use Golden;
use Goldatan;
use Jolden;
# ez egy olyan csomag, amit változók értékének kiírására használhatunk, könnyen
# olvasható formában ír ki bonyolult struktúrákat is
use Data::Dumper;
eval {
my $alma = new Alma;
};
if ( $@ ) {
print "Kivétel az Alma osztály példányosításakor: $@\n";
}
# a Golden osztály példányosítása
my $golden = new Golden;
# az osztály leírásának kiírása
print $golden->miez(), "\n";
# egy objektumszintű attribútum megváltoztatása
$golden->{sargasag} = 8;
# a new objektumra hívva copy konstruktorként működik
my $copy = $golden->new();
print Data::Dumper->Dump([$copy], ["copy"]);
# a gyerekbetegségektől szenvedő Goldatan alma egy példányának kiíratása
my $goldatan = new Goldatan;
print Data::Dumper->Dump([$goldatan], ["goldatan"]);
# a Jolden alma egy példánya
my $jolden = new Jolden;
print Data::Dumper->Dump([$jolden], ["jolden"]);
# az Alma osztályban működő copy konstruktor jól működik ebben az esetben is
$jolden->{sargasag} = 8;
my $cp = $jolden->new();
print Data::Dumper->Dump([$cp], ["cp"]);
Az objtest.pl program kimenete pedig:
Kivétel az Alma osztály példányosításakor:
Az Alma osztály nem példányosítható at ./objtest.pl line 14
Golden alma
$copy = bless( {
'egeszseg' => 100,
'goldenizalva' => 1,
'sargasag' => 8
}, 'Golden' );
$goldatan = bless( {
'jonatanizalva' => 1,
'egeszseg' => 100,
'pirossag' => 6
}, 'Goldatan' );
$jolden = bless( {
'jonatanizalva' => 1,
'egeszseg' => 100,
'goldenizalva' => 1,
'sargasag' => 6,
'pirossag' => 6
}, 'Jolden' );
$cp = bless( {
'jonatanizalva' => 1,
'egeszseg' => 100,
'goldenizalva' => 1,
'sargasag' => 8,
'pirossag' => 6
}, 'Jolden' );
Kivételkezelés
Perl-ben mindent lehet...
A
die és az
eval utasítások segítségével Perl-ben is megvalósítható
kivételkezelés. Nézzük hogyan:
eval {
...
# ha már saját hibaosztályunk van, adhatunk neki
# paramétereket is
die HibaModul::Exception->new(kod => 1);
}
if ($@) {
# az "isa" függvényt persze felül lehet definiálni, szóval
# jobb kerülni a meglepetéseket...
if (ref($@) && UNIVERSAL::isa($@, "HibaModul::Exception")) {
# itt kell lekezelni a kivételt
}
else {
# egyéb kivételek kezelése
...
# ha nem akarjuk kezelni a kivételt, adjuk tovább
die $@;
}
}
Az
eval BLOKK kifejezés értéke a definiálatlan (
undef)
érték lesz, ha a BLOKK futása egy
die utasítás hatására megszakad.
Ebben az esetben a
$@ változó tartalmazza a
die utasításnak
átadott paramétert. Ez a paraméter lehet egy referencia, így objektumok
is visszaadhatók, amik leírják a hibát.
A kivétel továbbadásánál a die utasításnak meg kell adni paraméterként
a $@ változót; ellenkező esetben a die megpróbálna hozzátenni
az értékéhez és mi valószínuleg ezt nem szeretnénk.
Amennyiben egy kivételt nem kezel le senki, akkor a Perl azt kiírja
a standart hibakimenetre és megszakítja a program futását. Amennyiben szép
hibaüzeneteket akarunk kapni az általunk definiált kivételosztályok esetében
is, célszeru ezen osztályok stringgé alakító operátorát (ez a ""
nevű speciális operátor) felüldefiniálni (lásd az operátorok
átlapolásáról szóló részt).
Többszálúság
Bevezetés
A Perl az 5.005-ös verziótól kezdődően támogatja a párhuzamos
végrehajtást. Ez egyenlőre még csak béta állapotban van, de már eléggé
letisztult ahoz, hogy próbálgatni és használni lehessen. A modellje leginkább a
POSIX thread-ekre hasonlit, azonban egy sor más modellból is vesz át használható
rutinokat (pl. szinkron blokkok). Mindenütt az adott platform threadrendszerét
követi (green threads, POSIX threads, Win32 threads, satöbbi), és ezek fölé ad
egy egységes felületet.
Thread-ek létrehozása
A "Thread" modulon keresztül történik,
objektum-orientáltan, azaz a modul egy Thread osztályt implementál. A
"
usethreads" konfigurációs változóval lehet megállapítani, hogy a
többszálas támogatás bele van-e fordítva a perl interpreterbe. Ime egy egyszerű
program, ami bemutatja a használatot:
use Config;
use Thread;
die "Nincsenek thread-ek!" unless $Config{usethreads};
$thr = new Thread \&kod;
sub kod {
print "En egy thread vagyok";
}
Mint látható, egy Thread objektum egy futó szálat jelent. Ha létrehozunk
egy ilyen objektumot, az rögtön el is indul. A paraméterei jelentik azt, hogy
milyen kód fusson benne, egy szubrutin formájában. A szubrutint paraméterezni is
lehet:
use Thread;
$param3 = "ze";
$thr = new Thread \&kod, "iksz", "ipszilon", $param3;
$thr = new Thread \&kod, @parameterek;
$thr = new Thread \&kod, qw(iksz ipszilon $param3);
sub kod {
my ($a,$b,$c) = @_;
print "En egy thread vagyok, a=$a, b=$b, c=$c\n";
}
Ez így három plusz szálat fog létrehozni, amik mind a "kod" szubrutint
futtatják, más-más paraméterekkel.
Új szálakat az Thread::async()
függvénnyel is létre lehet hozni, ami csak egy formai könnyités az előzőekhez
képest. Egy anonim szubrutint vár paraméterként, amit az aktuális környezetben
futtat le (mint az eval), új szálként.
use Thread qw(async);
async {
for(my $i=0; $i < 1000; $i++) {
print "Szamolok $i\n";
}
};
Hogy jobban érthető legyen, ugyanez részletesebben:
use Thread;
my $thr = Thread::async(
{ #anonim szubrutin
for(my $i=0; $i < 1000; $i++) {
print "Szamolok $i\n";
}
}
);
Ebből rögtön látható is, hogy miért kell pontosvessző mindenképpen az
async után - ez nem egy új vezérlőszerkezet, hanem egy függvény.
Thread-ek befejezése
Normális esetben egy thread úgy fejeződik be, hogy
végetér a szubrutin, amit futtat. A visszatérési értéket egy másik threadből
megtudhatjuk a join metódus segitségével. Ha a thread még fut, akkor a join
metódus a lekérdező thread-et felfüggeszti a futás végéig.
use Thread;
sub fibo($) {
my ($a,$b) = (0,1);
for(my ($i)=@_; $i > 0; $i++) {
($a,$b) = ($b,$a+$b);
}
return $b;
};
$thr = new Thread \&fibo, 10;
$result = $thr->join;
Ha a szubrutin futás közben meghal (die), akkor
erről join-nál értesülünk, mégpedig úgy, hogy a hiba a mi thread-unkban dobódik
tovább felfelé. Ha ki akarjuk védeni, használjunk eval-t.
use Thread;
sub hiba { 10/0; } # division by zero
$thr = new Thread \&hiba;
$result = eval{$thr->join};
if ($@) {
print "hiba van! $@";
} else {
print $result;
}
Egy thread után mindig fel kell takaritani. Normális esetben a join ezt
is megteszi. Ha azonban nem join-olunk, akkor a thread, miután végzett, ott
marad "halott" állapotban (várva, hogy valaki join-t hivjon), és erőforrásokat
foglal. Ha egy threaddel nem akarunk foglalkozni, akkor használjuk a detach()
metódust. Ez jelzi, hogy a szubrutin végeztével a perlnek kell utána mindent
feltakarítani. Ezután már nem lehet join-olni.
lock
Mivel minden thread hozzáférhet minden változóhoz, ezért sokféle
hiba léphet fel. A párhuzamos adathozzáférésekből adódó problémák kezelésére
szolgál a lock. Csak jelzésértékű, tehát bárki figyelmen kívül hagyhatja
(hasonló a UNIX-os flock()-hoz). Változókat zárhatunk le vele. Ha már valaki
lezárta, akkor addig vár, amig fel nem engedték. Nincsen hozzá unlock, egész
egyszerűen addig tartja fogva a változót, amíg az adott blokkban tartózkodunk.
Ebből következik, hogy nem lehet véletlenül lezárva hagyni egy változót.
use Thread qw(async);
$a = 1;
async {
# ....
{
lock $a;
my $b = $a;
$a = $b + 1;
} # itt szabadul fel a lock
# ....
}
async {
# ....
{
lock $a;
my $b = $a;
$a = $b - 1;
} # itt szabadul fel a lock
# ....
}
print $a;
Ha nem használunk lock-ot, akkor $a értéke 0,1,2 lehet a program
futása végén.
A lockokat egymásba is lehet ágyazni, tehát ugyanaz a thread
többször is lezárhatja a változót, az csak akkor lesz újra szabad mikor a
legkülső lock-os blokk is végetért.
Lezárni nem csak skalárt, de tömböt és
hash-t is lehet. De a lock csak a változóra vonatkozik, a belső tartalmára nem,
tehát ha egy thread lockol egy tömböt, egy másik thread még nyugodtan
lockolhatja az egyik elemét.
Megjegyzendő, hogy a lock nem a Thread modul
része, hanem a nyelv egy új eleme.
queue
A Thread modulban előre meg van írva egy thread-safe queue
osztály. Ennek segitségével biztonságosan adhatunk át adatokat egyik szálból a
másikba. Két metódusa van, a enqueue és a dequeue, amivel be- és kirakhatunk
adatokat. Ha a queue üres, akkor a dequeue addig várakozik, amíg bele nem kerül
valami.
use Thread qw(async);
use Thread::Queue;
my $q = new Thread::Queue;
async {
while(1) {
$q->enqueue(rand());
sleep(1);
}
}
async {
while(1) {
my $val = $q->dequeue;
print "$val\n";
}
}
Természetesen, a perl tradíciókhoz hiven az enqueue metódus több értéket
is kaphat, listában, ezeket az adott sorrendben egymás után beteszi a sorba.
Szemaforok
A szokásos szemafor implementáció is megtalálható a nyelvben.
Az up és down metódusokkal lehet növelni ill. csökkenteni a szemafor
számlálóját, ami garantáltan nem megy 0 alá. Mivel alapértelmezésben a számláló
1 értéken kezd, ezért ugyanúgy viselkedik, mint egy lock. A konstruktornak
azonban megadhatjuk a számláló kezdeti értékét. Ugyanígy az up és a down is
kaphat paramétert, és akkor annyival fogja növelni illetve csökkenteni az
értékét.
use Thread;
use Thread::Semaphore;
my $sem = new Thread::Semaphore 2;
sub kod($) {
$sem->down;
print "$i munkat kezd\n";
# munkat vegzek
print "$i munkat befejez\n";
$sem->up;
sleep(10);
}
foreach(1..10) { new Thread \&kod $_; }
Szubrutinok zárása
Az 5.005-ös perltől kezdve lehet egyes metódusokhoz
is attribútumokat rendelni. A számunkra fontos attribútum a locked. Ez
ekvivalens azzal, mintha a szubrutin elején lezárnánk.
sub egyszerre_egy {
use attrs qw(locked);
# ide egyszerre csak egy thread juthat el
# ...
}
Ekvivalens ezzel:
sub egyszerre_egy {
lock(\&egyszerre_egy);
# ide egyszerre csak egy thread juthat el
# ...
}
Egyebek
Maradt még néhány eljárás, ami a többszálú futtatáshoz nyújt
segitséget.
- Thread->self visszaadja az éppen futó threadet
- $thr->tid() egy számot ad vissza, ami egyértelmuen azonositja
a thread-et
- Thread->equal($thr,$thr) igaz, ha a két thread ugyanaz
- Thread->list az összes futó thread listája
- $thr->yield() felfüggeszti a futást, és átadja egy másiknak.
Ha az alatta lévő thread rendszer nem preemptív, akkor nagyon hasznos lehet
Folyamatok közti kommunikáció (IPC)
A Perl IPC lehetőségei alapvetően a UNIX szignáljaira, pipe-okra, socket rutinokra, valamint SysV IPC hívásokra épülnek.
SZIGNÁLOK:
A használt szignálkezelési modell egyszerű. A %SIG hash tartalmazza a programozó által installált szignálkezelők nevét (illetőleg referenciáit), ezek meghívásukkor argumentumként megkapják az őket elindító szignál nevét. Ezek generálódhatnak pl. valamely billentyukombináció hatására, más folyamat is küldheti, de bizonyos események kapcsán a kernel is automatikusan kiválthatja (pl. egy gyermekprocessz befejeződésekor).
Például egy interrupt-szignál kezelését az alábbi módon lehet megoldani:
sub catch_zap {
my $signame = shift;
$shucks++;
die "Somebody sent me a SIG$signame";
}
$SIG{INT} = 'catch_zap'; # could fail in modules
$SIG{INT} = \&catch_zap; # best strategy
A szignálok nevét a kill -l parancs listázza ki (a Config modulból is megtudhatók) Hozzunk létre egy @signame listát, mely a számmal van indexelve, valamint %signo hash-t, amely indexei a szignálok nevei (innen lehet majd a számot megtudni).
use Config;
defined $Config{sig_name} || die "No sigs?";
foreach $name (split(' ', $Config{sig_name})) {
$signo{$name} = $i;
$signame[$i] = $name;
$i++;
}
Így például azt, hogy a 17-es és a SIGALARM azonos-e, a következőképp tudhatjuk meg:
print "signal #17 = $signame[17]\n";
if ($signo{ALRM}) {
print "SIGALRM is $signo{ALRM}\n";
}
Kezelőként megadhatók az "IGNORE" és a "DEFAULT" sztringek is. Az első esetén a Perl próbálja figyelmen kívül hagyni a szignált, míg a másodikban egy alapértelmezett kódot végrehajtani.
A legtöbb Unix platformon "IGNORE" esetén CHLD szignál speciálisan viselkedik. Ilyenkor amikor a szülő folyamatban a wait() meghiúsul, akkor ?zombi folyamatok? nem jönnek létre.
Vannak olyan szignálok, mint például a KILL és a STOP, amelyeket sem elkapni, sem figyelmen kívül hagyni nem lehet. Az egyik lehetőség az ilyenek ideiglenes figyelmen kívül hagyására a local() használata. Ezen értékek az adott blokkban, illetve a blokkból hívott függvényeken belül érvényesek, a kilépés után automatikusan visszaállítódnak.
sub precious {
local $SIG{INT} = 'IGNORE';
&more_functions;
}
sub more_functions {
# interrupts still ignored, for now...
}
A negatív folyamat-azonosítóval küldött szignálokat a Unix-processz csoport minden tagja megkapja. A 0-s szignálnak is speciális jelentése van. Ez a gyermek-folyamatot nem befolyásolja, hanem ellenőrzi, hogy még működik-e, vagy az UID-je megváltozott-e.
Kezelőként névtelen függvények is megadhatók:
$SIG{INT} = sub { die "\nOutta here!\n" };
NAMED PIPE-OK:
A named pipe (vagy más néven FIFO) egy régi Unix IPC mechanizmus az azonos gépen futó folyamatok közti kommunikáció megvalósítására. működése megegyezik a hagyományos, névtelen pipe-okéival azzal a különbséggel, hogy a folyamatok a filenév alapján kapcsolódnak össze, ezért társításuk nem szükséges.
Egy ilyen named pipe létrehozására a Unix mknod parancsa (egyes rendszerekben mkfifo) használatos:
# system return val is backwards, so && not ||
#
$ENV{PATH} .= ":/etc:/usr/etc";
if ( system('mknod', $path, 'p')
&& system('mkfifo', $path) )
{
die "mk{nod,fifo} $path failed";
}
FIFO-t jól lehet alkalmazni abban az esetben, amikor a folyamatot egy vele össze nem függővel szeretnénk összekapcsolni. A pipe megnyitása esetén a program mindaddig blokkolódik, még a másik végén nincs másik processz.
Példa:
Legyen a .signature állomány egy named pipe, melynek a másik végén egy Perl program fut. Azt szeretnénk, hogy amennyiben valamely másik program az állományból próbál olvasni (pl. egy levelező program) blokkolódjon, és szignatúrát a Perl pogram adja majd számára.
chdir; # go home
$FIFO = '.signature';
$ENV{PATH} .= ":/etc:/usr/games";
while (1) {
unless (-p $FIFO) {
unlink $FIFO;
system('mknod', $FIFO, 'p')
&& die "can't mknod $FIFO: $!";
}
# next line blocks until there's a reader
open (FIFO, "> $FIFO") || die "can't write $FIFO: $!";
print FIFO "John Smith (smith\@host.org)\n", `fortune -s`;
close FIFO;
sleep 2; # to avoid dup signals
}
ÁTTÉTELES (BIZTONSÁGOS) SZIGNÁLOK:
A Perl 5.7.3-at megelőző verzióiban a saját szignálkezelő kódok használata két veszélyt rejtett magában:
- Néhány rendszer-könyvtárbeli függvény reentráns. Ha egy szignál megszakítja egy ilyen, Perl-programbeli végrehajtását (pl. malloc(), printf()), majd a kezelő újra meghívja, akkor a viselkedés kiszámíthatatlan.
-A Perl önmaga a legalacsonyabb szinteken nem reentráns. Ha a szignál megszakítja, miközben valamely belső adatstruktúrát változtat meg, szintén megjósolhatatlan eredményt kapunk.
Ezen tények ismeretében két dolgot tehetünk: lehetünk paranoiásak és pragmatikusak.
Az első azt a megközelítést takarja, miszerint tegyünk minél kevesebb kódot a szignálkezelőkbe. Pl. állítsuk át egy, már létező és értékkel rendelkező egész változó értékét, majd térjünk vissza. Ez egy lassú rendszerhívás végrehajtása közben nem segít, mert az csak újraindul. Ezért ilyenkor a die longjump() használata révén kell kijönni a handlerből.
A pragmatikus megközelítés ezzel szemben a "tudom, hogy kockázatos, de kényelmes" gondolaton alapul. Ekkor bármit lehet írni a kezelőbe, de fel kell készülni a fent említett eshetőségekre.
Az 5.7.3-as és frissebb Perl verziókban a probléma megoldására az áttételes (deferred) szignálokat alkalmazzák. Amikor a szignált a rendszer eljuttatja a folyamathoz (a Perl-t implementáló C kódhoz), egy flag-gel jelzi ezt, majd a kezelő azonnal visszatér. Ezek után, amikor a végrehajtás egy stratégiailag "biztonságos" ponthoz ér (új művelet végrehajtása előtt), és a flag be van állítva, akkor lefut a Perl szintű, %SIG-ben tárolt kezelő.
Ez a séma nagyobb rugalmasságot biztosít saját handlereink megírásánál, hiszen biztosak lehetünk benne, hogy lefutásakor nem valamely rendszer-könyvtárbeli rutin végrehajtása közben vagyunk. Néhány különbség azonban van az implemetációban:
- Hosszú végrehajtási ideju műveletek: az interpreter a szignál-flageket mindig egy új művelet végrehajtása előtt ellenőrzi. Ha valamely művelet hosszú ideig fut (pl. valamely reguláris kifejezés alkalmazása hosszú karakterláncon), akkor a közben bejövő szignálokról csak a befejeződés után szerez tudomást
- IO műveletek megszakítása: a szignál megérkezésekor az operációs rendszer megszakítja az IO műveletek végrehajtását (mint pl .a Perl <> operátorához használt read). A régebbi verziókban ilyen esetekben a kezelő azonnal lefutott. Közvetett mechanizmus esetén ez nem így történik, ezért ha a Perl a rendszer stdio könyvtárát használja, akkor a read újraindulhat anélkül, hogy a program visszakapná a vezérlést, így nincs lehetőség a %SIG-ben tárolt handler futtatására. Erre megoldás a :perlio réteg használta, amely az 5.7.3-as verziótól kezdve alapértelmezés.
- Újraindítható rendszerhívások: az ezt támogató rendszerekben korábban a Perl az SA_RESTART flag-et használta a %SIG kezelők beállításakor. Ennek hatására az ilyen újraindítható hívások a szignálok esetén futottak tovább, nem tértek vissza. Az 5.7.3-tól frissebb esetben ez a flag már nem használt, ezért ezek a rutinok is visszatérhetnek hibával ($!-et EINTR-re állítva) olyan helyeken is, ahol korábban lefutottak. (A :perlio réteg használatakor a read, write, close a fent leírtak szerint működik.)
- Szignálok, mint "hibák": bizonyos szignálok (pl. SEGV, ILL, BUS) virtuális memória vagy más típusú "hibák" keletkezésekor generálódnak. Ezek legtöbbször végzetes problémák, és a Perl-szintű kezelők nem sok mindent kezdhetnek velük (a régi mechanizmus kiváltképp nem volt biztonságos ezekben az esetekben). Azonban az új módszer esetén, amennyiben %SIG kezelő aktív, a flag beállítása mellett a fenti viselkedés érvényes. Ekkor viszont lehetséges, hogy a problémás utasítást az operációs rendszer változatlan formában újra lefuttatja, újra generálva ezzel a szignált. Ez egy elég furcsa ciklikussághoz vezet. A jövőbeni Perl változatokban szignálkezelést át fogják alakítani, hogy ezt a problémát lehessen kezelni- valószínuleg ilyen esetekben egyszerűen letiltják a %SIG kezelőt, vagy magát a szignált.
- Az operációs rendszer által kiváltott szignálok: néhány operációs rendszeren bizonyos szignálkezelők a visszatérés előtt "csinálnak valamit". Például a CHLD vagy CLD, amely a gyermek-folyamat befejeződését jelzi. Elképzelhető, hogy a handler vár (wait) a gyermek befejeződésére. Ebben a helyzetben az ilyen szignálok esetében a késleltetetés nem célravezető, az előző pontban leírt ciklikus kiváltódáshoz vezet, mivel a szignál újra és újra generálódik, hiszen vannak olyan befejeződött gyermek-folyamatok, amelyekre senki nem vár a wait-tel.
Amennyiben a régi szignál viselkedést szeretnénk (tekintet nélkül a lehetséges memóriával kapcsolatosa problémákra), akkor a PERL_SIGNALS környezeti változót kell az "unsafe" értékre beállítani (ez a Perl 5.8.1-ben bevezetett lehetőség).
AZ OPEN() HASZNÁLATA:
Az egyirányú folyamatok közti kommunikációra az alap open() is használható. Ehhez a második argumentum elejére vagy végére kell fűzni a pipe jelet. Például legyen egy gyermekprocessz, amelynek adatot akarunk küldeni a következő:
open(SPOOLER, "| cat -v | lpr -h 2>/dev/null")
|| die "can't fork: $!";
local $SIG{PIPE} = sub { die "spooler pipe broke" };
print SPOOLER "stuff\n";
close SPOOLER || die "bad spool: $! $?";
Az amelyik küldi az adatot pedig nézzen így ki:
open(STATUS, "netstat -an 2>&1 |")
|| die "can't fork: $!";
while (<STATUS>) {
next if /^(tcp|udp)/;
print;
}
close STATUS || die "bad netstat: $! $?";
Ügyeljünk rá, hogy mind az open(), mind a close() visszatérési értékét ellenőrizzük. Pipe-ba történő írás esetén a SIGPIPE szignált is el kell kapni. Ellenkező esetben problémát okoz, ha például olyan parancshoz kapcsolunk pipe-ot amely nem létezik: az open() valószínuleg sikeresen végrehajtódik, a kimenet viszont látványosan hibás lesz. A Perl nem tudhatja, hogy a parancs működik-e, hiszen másik processzben van, amelyben az exec() hibát okozhatott, aminek következtében a kérdéses parancstól adatokat fogadók egy gyors end of file jelzést kapnak, a neki adatot küldők pedig olyan szignált váltanak ki, melyet jobb lenne lekezelniük.
open(FH, "|bogus") or die "can't fork: $!";
print FH "bang\n" or die "can't write: $!";
close FH or die "can't close: $!";
Ez csak a close()-nál váltja ki a SIGPIPE-ot. Ezt például a következőképp lehet lekezelni:
$SIG{PIPE} = 'IGNORE';
open(FH, "|bogus") or die "can't fork: $!";
print FH "bang\n" or die "can't write: $!";
close FH or die "can't close: status=$?";
FILEHANDLE-k:
Mind a főprocessz, mind az általa indított gyermek processzek ugyanazt az STDION, STDOUT és STDERR filehandle-t használják, ezért gondot okoz, ha közülük egyszerre több szeretné valamelyiket elérni. Az is előfordulhat, hogy a gyermek számára a filhendle-t le akarjuk zárni, vagy újra nyitni. A probléma kiküszöbölhető, ha a pipe-ot open() segítségével nyitjuk meg, bár ez egyes rendszerek esetében azt is jelenti, hogy a gyermek folyamat nem élheti túl a szülőt.
FOLYAMATOK A HÁTTÉRBEN:
Parancsot a háttérben futtatni a következőképp lehet:
system("cmd &");
Ebben az esetben az STDOUT és STDERR (a használt shell-től függően az STDIN is) megegyezik a szülőével.
A GYERMEK FOLYAMAT TELJES ELVÁLASZTÁSA A SZÜLŐTŐL:
Néhány esetben (ha például szerver-folyamatokat szeretnénk elindítani), szükség lehet rá, hogy elválasszuk a gyermek processzeket a szülőtől. Ezt gyakran daemonizációnak nevezik. Egy daemon folyamatnál szerencsés, ha a gyökér-könyvtárat állítja be aktuálisnak a chdir() segítségével, lehetővé téve ezáltal, hogy azt a könyvtárat, amelyből elindították később lecsatolják a filerendszerről, valamint jó, ha a standard file leíróit átirányítja a /dev/null-ról, illetve a /dev/null-ra, így a véletlenszeru kimeneti üzenetek nem jelennek meg a felhasználó terminálján:
use POSIX 'setsid';
sub daemonize {
chdir '/' or die "Can't chdir to /: $!";
open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
open STDOUT, '>/dev/null'
or die "Can't write to /dev/null: $!";
defined(my $pid = fork) or die "Can't fork: $!";
exit if $pid;
setsid or die "Can't start a new session: $!";
open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
}
A fork()-nak meg kell előznie a setsidet(), biztosítva, hogy a folyamat nem egy folyamat csoport vezetője, ebben az esetben ugyanis a setsid() végrehajtása hibás lesz. Abban az esetben, ha rendszerünkben nincs meg ez utóbbi funkció, akkor nyissuk meg a /dev/tty-t, és használjuk helyette az IOCNOTTY ioctl()-t.
BIZTONSÁGOS PIPE MEGNYITÁS:
A folyamatok közti kommunikáció egy másik érdekes alkalmazása, amikor az egyszerű programot több processzeben indítjuk el, amelyek kommunikálnak egymás közt.. Az open() függvény a "-|" -ot vagy a "|-" -t kapja argumentumként, majd a következőt teszi: fork() segítségével létrehoz egy gyermek folyamatot, amelyet a megnyitni kívánt file handle-hez rendel, és amely ugyanazt a programot futtatja amelyet ő maga. Ezt lehet használni állományok biztonságos megnyitására abban az esetben, ha programunk átnevezett UID-vel vagy GID-vel fut. Ha a pipe-ot a ?-? jelre nyitjuk meg, akkor írhatunk a megnyitott filehandle-re, és ezt a gyermek processz a saját standard inputjáról jövő adatként kezeli, amennyiben pedig a ?-?-ról nyitjuk, akkor olvashatunk róla, mégpedig a gyermek folyamat standard outputjára írt adatait.
use English '-no_match_vars';
my $sleep_count = 0;
do {
$pid = open(KID_TO_WRITE, "|-");
unless (defined $pid) {
warn "cannot fork: $!";
die "bailing out" if $sleep_count++ > 6;
sleep 10;
}
} until defined $pid;
if ($pid) { # parent
print KID_TO_WRITE @some_data;
close(KID_TO_WRITE) || warn "kid exited $?";
} else { # child
($EUID, $EGID) = ($UID, $GID); # suid progs only
open (FILE, "> /safe/file")
|| die "can't open /safe/file: $!";
while (<STDIN>) {
print FILE; # child's STDIN is parent's KID
}
exit; # don't forget this
}
Ezt a konstrukciót gyakran alkalmazzák még abban az esetben, amikor a shell közbeavatkozása nélkül kell valamit elindítani. Ez megtehető ugyan a system() hívás alkalmazásával is, de ekkor nem lehet pipe-ot megnyitni, sem a visszahivatkozásokat (backtick) biztonságosan használni. Ennek az az oka, hogy sehogyan sem lehet a shell-t megakadályozni az argumentumok elkapásában. Ezért alacsony szintű kontrollt alkalmazva, az exec()-et kell direktben meghívni.
Példa pipe megnyitására, amelyről olvasunk:
# add error processing as above
$pid = open(KID_TO_READ, "-|");
if ($pid) { # parent
while (<KID_TO_READ>) {
# do something interesting
}
close(KID_TO_READ) || warn "kid exited $?";
} else { # child
($EUID, $EGID) = ($UID, $GID); # suid only
exec($program, @options, @args)
|| die "can't exec program: $!";
# NOTREACHED
}
Példa írásra:
# add error processing as above
$pid = open(KID_TO_WRITE, "|-");
$SIG{PIPE} = sub { die "whoops, $program pipe broke" };
if ($pid) { # parent
for (@data) {
print KID_TO_WRITE;
}
close(KID_TO_WRITE) || warn "kid exited $?";
} else { # child
($EUID, $EGID) = ($UID, $GID);
exec($program, @options, @args)
|| die "can't exec program: $!";
# NOTREACHED
}
Az 5.8.0-s Perl verzió óta az open listás formája is alkalmazható a pipe-okhoz:
open KID_PS, "-|", "ps", "aux" or die $!;
Ez az utasítás elindítja a ps() parancsot új shell nélkül (mivel az open()-nek háromnál több paramétere van), amelynek a standard outputjára kerülő információt a KID_PS filehandle-n keresztül olvassa. A parancs pipe-okról történő olvasás hasonlóan működik, ilyenkor ?-|? helyébe kell ?|-?-t írnunk.
Ezek a műveletek teljesen az Unix fork()-ra épülnek, ezért előfordulhat, hogy más rendszereken nem működnek teljesen tökéletesen.
KÉTIRÁNYÚ KOMMUNIKÁCIÓ MÁSIK FOLYAMATTAL:
Miközben ez a módszer meglehetősen jól működik egyirányú kommunikáció esetén, a kétirányúnál más a helyzet. A kézenfekvő megoldás sajnos nem működik:
open(PROG_FOR_READING_AND_WRITING, "| some program |")
Ez esetben az alábbit kapjuk:
Can't do bidirectional pipe at -e line 1.
A két végpont egyideju használatát az open2() könyvtári rutin teszi lehetővé. Rendelkezésre áll még egy open3() függvény is a három irányú kommunikáció lebonyolításához (így a gyermek folyamat STDERR-je is elkapható), de ehhez egy elég ügyetlen select() ciklust kell alkalmazni, amely meggátolja a normális Perl bemenet használatát. A forráskódját megtekintve látszik, hogy az open2() olyan alacsony szintű primitíveket használ, mint a Unix pipe() és exec(). Ezek segítségével építi fel az összes kapcsolatot. Ez hatékonyabb ugyan, mint a socketpair() használata, viszont kevésbé hordozható kódot eredményez. Éppen ezért az open2() és open3() csak a Unix, illetve POSIX szabványnak megfelelő rendszereken működnek.
Példa az open2() használatára:
use FileHandle;
use IPC::Open2;
$pid = open2(*Reader, *Writer, "cat -u -n" );
print Writer "stuff\n";
$got = <Reader>;
Itt az a probléma, hogy a Unix pufferelése túl sok időt vesz igénybe. Bár a Writer filehandle-t automatikusan kiolvassák, és a másik végén levő folyamat normál időn belül megkapja az adatot, azt már nem lehet garantálni, sem kikényszeríteni, hogy a válasz is hasonló gyors módon megérkezzen. Ebben az esetben a cat -u kapcsolója miatt (ami azt jelzi, hogy ne használjon puffert) ilyen gond nincs, de kevés Unix parancs lett a pipe-okon keresztüli kommunikációra tervezve, úgyhogy ez ritkán működik.
A megoldás a nem standard Comm.pl könyvtár használata, amely pszeudo-tty -ket használ.
require 'Comm.pl';
$ph = open_proc('cat -n');
for (1..10) {
print $ph "a line\n";
print "got back ", scalar <$ph>;
}
Ennél a módszernél a használt program forráskódjának ismerete nem szükséges. A Comm könyvtár tartalmazza az expect() és interact() függvényeket is.