Järjestelmäpalvelut ja poikkeukset¶
Osaamistavoitteet: Laiteohjelmiston tarjoamien järjestelmäpalveluiden käyttö ja poikkeustilanteet ohjelmien suorituksessa.
Laiteohjelmisto¶
Laiteohjelmisto, PC-maailmassa BIOS (engl. Basic Input Output System) kytkee siis tietokonejärjestelmän osat toimivaksi tietokoneeksi, alustaa ja käynnistää (ts. buuttaa) tietokoneessa komentotulkin / käyttöjärjestelmän ja tarjoaa käyttöjärjestelmälle ja sovelluksille matalan tason rajapinnan (mm. suojauksia) laiteresurssien käyttöön.
PC-maailmassa eri BIOS:t (tai nykyisin UEFI) sisältävät käyttäjille näkyvinä osina järjestelmän asennusohjelmiston ja käynnistyksen yhteydessä toteutettavat laitteistotestit. Yleisen laitteistoyhteensopivuuden saavuttamiseksi BIOS:n tarjoaa standardoitun integrointiympäristön eri valmistajien laitteistokomponenteille, jonka avulla PC-tietokonejärjestelmä voidaan rakentaa eeri valmistajien komponenteista. Tätä ideaa pidetään oleellisena tekijänä, alunperin IBM:n kehittämän, PC-teknologian voittokululle kotitietokoneiden alkuaikoina.
Yleisesti laiteohjelmiston/BIOSn palveluita kutsutaan kahden eri mekanismin kautta: ohjelmallisilla keskeytyksillä konekielestä (engl. trap instructions) tai järjestelmäkutsuilla (systeemikutsu, engl. trap / system call) ohjelmakoodista. Tällöin järjestelmäkutsua käytetään ohjelmassa ikäänkuin funktiokutsua. Usein järjestelmäkutsulle tarjotaan korkeamman tason käsittelijä/kääre (engl. wrapper), joka kutsuu varsinaista alemman tason BIOS:n järjestelmäkutsua. Esimerkiksi C-kielessä standardikirjasto (unistd.h) tarjoaa järjestelmäkutsut ohjelmien käyttöön, joita standardikirjaston toteutukset sitten hyödyntävät. Koska nämä ovat alemman tason funktiota, ne vaativat enemmän laitteisto/käyttöjärjestelmä-läheisiä yksityiskohtia ohjelmoijalta.
Tällainen kerrostus ohjelmasta käyttöjärjestelmän kautta laiteohjelmistoon tarvitaan siksi, että järjestelmäkutsulla on laajemmat oikeudet järjestelmän resursseihin (engl. kernel mode), kuin sitä kutsuvalla ohjelmalla (engl. user mode). Laiteohjelmiston kutsut ovat siis piilossa ohjelmoijalta. Esimerkiksi x86-suorittimien arkkitehtuurissa on erilaisia suojaustasoja. Lisäksi joissain prosessoreissa on suojattua käskyjä, joita vain järjestelmäkutsut voivat käyttää. Ennen kutsun suorittamista oikeudet tietenkin tarkistetaan laiteohjelmistossa.
Itse laiteohjelmisto on usein lukumuistissa (ROM-muisti, engl. read only memory) olevaa konekielistä koodia, jonne ohjelmakoodista hypätään hyppykäskyillä, ihan kuten aliohjelmiin yleensäkin. Varhaisissa tietokoneissa laiteohjelmisto olikin toteutettu prosessorin kanssa samalle piirikortille integroidulle erilliselle ROM-piirille, joka näkyi osana ohjelmamuistia. Päivitys tehtiin sitten vaihtamalla fyysinen piiri toiseen.
Esimerkki. IBM PC:n käynnistyssekvenssi BIOS:n avulla.
Poikkeukset¶
Yleisesti poikkeus on tapahtuma (engl. event), joka tulee suoritettavan ohjelman ulkopuolelta, keskeyttää ohjelman kontrollivuon (engl. control flow) ja johon prosessorin on reagoitava suorittamalla poikkeuksen käsittelijä (engl. exception handler). Käsittelijän suorittamisen jälkeen prosessori palaa suorittamaan ohjelmaa siitä mihin sen suoritus jäi.
Tapahtuma voi liittyä suoritettavana olevaan käskyyn, esimerkiksi jos yritetään jakaa nollalla. Myös ulkoiset tapahtumat voivat keskeyttää ohjelman suorituksen, esimerkiksi viesti I/O-laitteelta. Suorittimen sisäisesti tapahtuma muuttaa sen tilaa, esim. tilabittejä tai rekisterien arvoja, joita lukemalla käyttöjärjestelmä / ohjelma / laiteohjelmisto näkee mikä poikkeus tapahtui.
Poikkeuksen käsittely on siis laitteiston ja ohjelmiston yhteistyötä. Laitteisto asettaa tilamuutoksen ja ohjelmisto reagoi siihen. Yleensä käyttöjärjestelmissä / laiteohjelmistoissa on valmiit oletuskäsittelijät poikkeuksille, mutta ohjelmaan voidaan myös valikoidusti laatia omat käsittelijät. Suoritimmissa on myös ohjelmallisesti asetettavia omia poikkeuksia, jotka ovat ohjelmoijan vapaassa käytössä.
Näin poikkeukset tarjoavat keinon sovellusten ja käyttöjärjestelmän väliseen yhteistoimintaan. Sovellukset voivat pyytää alemman tason palveluita, kuten I/O:ta, käyttöjärjestelmältä poikkeuksien avulla. Käyttöjärjestelmä sitten hoitaa homman laiteohjelmiston kanssa yhteistyössä. Vastaavasti oheislaitteet viestivät sovelluksen kanssa poikkeusten avulla. Moniajoa tukevasti, ohjelman eri tehtävien yhtäaikainen suoritus voidaan toteuttaa ja kontrolloida poikkeusten avulla. Esimerkkinä SensorTagin RTOS:n kirjastot Task, Pin, UART, jne..
Poikkeusten käsittely¶
Poikkeukset identifioidaan niille annetun (0:sta alkavan positiivisen) järjestysnumeron avulla. Tämä järjestys määritellään suorittimen hardiksen suunnitteluvaiheessa tai BIOS:ssa käynnistysvaiheessa. Tietysti kun poikkeuksten numerokoodit on tunnettu ja standardoitu, mahdollistetaan erilaisten oheislaitteiden joustava liittäminen tietokonejärjestelmään sekä niiden yhdenmukainen käyttö ohjelmakoodista, kuten PC-maailmassa.
Järjestysnumero myös kuvaa poikkeuksien prioriteettijärjestyksen, jossa pienin numero vastaa korkeinta prioriteettia. Numeroiden perusteella saadaan poikkeustaulukko (engl. exception table), johon tallennetaan jokaiselle poikkeukselle sen käsittelijän muistiosoite. Näin ollen poikkeuksen käsittely on (ehdoton) hyppykäsky muistiosoitteeseen, josta käsittelijän koodi suoritetaan. Joissain arkkitehtuureissa (esim. moderni x86) käsittelijälle voidaan antaa vielä kutsuparametri ihan kuten aliohjelmakutsuissakin.
Esimerkkinä x86:sen keskeytystaulukko Sulautetuissa järjestelmissä puhutaan keskeytysvektorista (engl. interrupt vector table).
Usein poikkeustaulukko itse sijaitsee muistin aivan ensimmäisissä osoitteissa, eli alkaen muistipaikasta
0
. Kun tietokone käynnistetään, laiteohjelmisto ja/tai käyttöjärjestelmä alustaa poikkeustaulukon oletusarvoihin. Käyttöjärjestelmä voi yleensä myös luoda omia poikkeuksiaan poikkeustaulukon loppuun. Myös sovellukset, voivat muutella vektorin muistiosoitteita oikeuksiensa rajoissa. Sulautetuissa järjestelmissä esimerkiksi ohjelmoijalla on yleensä vapaus muutella keskeytysvektorin arvoja, käsittelijöitä toteuttaessa. Jokaisella poikkeuksella on oltava käsittelijä (engl. handler), joka on joko käyttöjärjestelmän oletuskäsittelijä tai ohjelmoijan laatima. Kuten keskeytyksen yhteydessä aiemmin esitettin, käsittelijä itsessään on kuten funktio, mutta konepellin alla eroja löytyy.
- Poikkeustilanteessa tallennetaan ohjelman tilasta yleisrekisterien lisäksi tilarekisteri. Jossain suorittimissa on omia rekistereitä poikkeustilanteiden tallennukseen, esimerkiksi poikkeuksen aiheuttaneen käskyn osoite.
- Jos käytetään pinoa, se on käyttöjärjestelmän oma pino, eikä ohjelman pino.
- Joissain prosessoreissa on oma konekielen paluukäsky käsittelijärutiineista (engl. exception return) ja oma paluukäsky aliohjelmasta, jotta keskeytetystä suorittimen tilasta saataisiin palautettua kaikki oleellinen. x86:sessa nämä käskyt ovat
rti
(return from interrupt) jarts
(return from subroutine). - Käsittelijä voi lopettaa ohjelman suorituksen, jolloin sieltä ei koskaan palata.
- Paluuosoite ei aina ole seuraava ohjelman käsky, vaan voidaan myös uudelleensuorittaa nykyinen käsky.
- Poikkeuskäsittelijöillä, ainakin käyttöjärjestelmän omilla, on laajemmat jos ei täydet oikeudet järjestelmän resursseihin kuin käyttäjien ohjelmilla.
Poikkeustyypit¶
Olemme jo aiemmassa materiaalissa tutustuneet yhteen poikkeustyyppiin, keskeytyksiin. Mutta itseasiassa poikkeustyyppejä on muitakin. Materiaalissa alla käytämme näille varsin kömpelöitä suomenkielisiä nimityksiä..
Keskeytys¶
Keskeytyksen siis aiheuttaa asynkroninen (tässä ajasta ja ohjelman suorituksesta riippumaton) signaali suorittimen sisältä tai ulkoisesti oheislaitteelta. Suoritin tarkistaa keskeytyslippujen tilat aina käskyn suorituksen jälkeen. Keskeytys ei siis keskeytä konekielisen käskyn suoritusta prosessorissa. Keskeytystä vastaavan tilalipun asettamisesta tilarekisteriin vastaa yleensä mikroarkkitehtuuri ja/tai laiteohjelmisto. Kun keskeytyslippu on ylhäällä tilarekisterissä, suoritetaan sitä vastaava käsittelijä. Käsittelijän suorituksen jälkeen palataan ohjelmaan ja suoritetaan sen seuraava käsky, jos se on keskeytyksen tyypistä riippuen mahdollista.
Laitteistokeskeytyksistä käytetään kahta nimitystä: IRQ (engl. Interrupt Request) ja NMI (engl. Non-maskable Interrupt). Näistä IRQ:t ovat oheislaitteille varattuja keskeytyslinjoja ohjauspiirissä joka taas on kiinni suorittimen keskeystyslinjoissa. NMI:t varataan taas keskusyksikön sisäisen toiminnin valvontaan, esimerkiksi nollalla jakaminen tai muistivirhe, ja näinollen niillä on korkeampi prioriteetti. Kun laitteistokeskeytys tulee, ihan vastaavasti hypätään sen käsittelijään laiteohjelmistossa.
Keskeytyslinjojen lukumäärä vaihtelee, esimerkiksi x86-perheessä niitä on 16 kpl jaettuna kahteen ohjainpiiriin. Näinollen PC-maailmassa, BIOS:ssa voidaan osaltaan määrittää mitkä keskeytyslinjat varataan millekin oheislaitteelle.
Kuva. Vanhan PC:n laitteistokeskeytyslinjat
Kuva. Vanhan PC:n laitteistokeskeytyslinjat
Ohjelmallisia keskeytyksiä varten, jotka itseasiassa ovat usein järjestelmäkutsuja (kts. alla), konekielen käskykannassa voi olla oma käsky. Kuten alla esimerkissä
int
. x86:sessa tämä keskeytyskutsu int 21
pyytää laiteohjelmiston kutsulla 0x2a
laiteohjelmiston palvelua antamaan päivämäärän, joka laskennan suorittaa integroitu reaaliaikakello-komponentti. mov ah,2ah # ah-rekisteriin kutsuttu palvelu
int 21h # keskeytyksestä suoritetaan haluttu palvelu
Järjestelmäkutsu¶
Laiteohjelmiston / (osin) käyttöjärjestelmän palveluita päästään siis ohjelmasta kutsumaan järjestelmäkutsujen kautta. Esimerkkejä järjestelmäkutsuista ovat eriliset I/O-toiminnot, kuten tiedoston lukeminen, tai moniajoon liittyvä toiminallisuus, kuten toisen prosessin / ohjelman käynnistäminen tai nykyinen ohjelman ajon lopettaminen. Järjestelmäkutsut ovat siis luonteeltaan synkronisia, koska ne aiheutetaan halutussa kohti ohjelmaa.
Järjestelmäkutsua varten voi olla suorittimessa valmiita konekielen käskyjä (engl. trap instructions) tai sitä käytetään kuten funktiokutsua, parametrit annetaan määrätyissä rtekistereissä. Lisäksi joissain prosessoreissa on suojattua käskyjä, joita vain järjestelmäkutsut voivat käyttää.
Esimerkki C-kielisestä ohjelmasta (linux/x86), joka käyttää printf:n sijaan järjestelmäkutsuja. write-funktion ensimmäinen parametri
1
tarkoittaa tässä tulostusvuota (engl. stdout), ts. yleensä ruutua. int main() {
write(1,"hello world\n",13); // printf("hello world\n");
_exit(0); // return 0;
}
Huomataan, että C-kielen standardikirjasto (unistd.h) tarjoaa järjestelmäkutsut ohjelman käyttöön. Koska ne ovat alemman tason funktiota, ne vaativat enemmän yksityiskohtia ohjelmoijalta.
Vastaava x86-assembly-koodi olisi seuraava:
string:
.ascii "hello world\n" # merkkijono ASCII-muodossa muistiin
main:
movq $1,%rax # write on kutsu nro 1
movq $1, %rdi # tulostusvuo näytölle on 1
movq $string, %rsi # merkkijono
movq $13, %rdx # merkkijonon pituus
syscall # suorita write
exit:
movq $60,%rax # exit on kutsu nro 60
movq $0, %rdi # palautusarvo 0
syscall
Assembly-kielessä kaikki järjestelmäkutsujen parametrit annetaan rekistereissä ja pinoa ei käytetä. Parametreilla on myös tietty järjestys, jossa
%rax
-rekisteriin menee kutsun numero, jne. Palautusarvo tulee myös %rax-rekisteriin.Fault¶
Vika (engl. fault) on sellainen virhetilanne, jonka mahdollisesti käsittelijä pystyy korjaamaan. Käsittelijän suorituksen jälkeen nykyinen ohjelmakäsky suoritetaan uudelleen. Jos käsittelijä ei pysty korjaamaan virhetilannetta, ohjelman suoritus lopetetaan virheeseen.
Esimerkki on vaikkapa viittaus muistiosoitteeseen, jota ei ole valmiina prosessorin välimuistissa (engl. page fault). Tällöin käsittelijä noutaa kyseisen muistiosoitteen sisällön keskusmuistista (tai ensin alemman tason välimuistista) ja tallentaa sen välimuistiin.
Abort¶
Tämä poikkeus (engl. abort) tapahtuu, kun järjestelmässä ilmenee virhetilanne jota ei voida ohjelmassa / laiteohjelmistossa / käyttöjärjestelmässä korjata. Esimerkkinä tästä käyttöjärjestelmän kaatuminen, viallinen data (pariteettivirhe) muistipiirissä tai jokin oheislaitteen/piirin vika. Tällöin ohjelman suoritus loppuu tähän kutsuun, eikä enää palata takaisin pääohjelmaan.
Esimerkki. Windowsin Blue screen of death on valitettavan tuttu ilmiö.. mutta tapahtuu tätä linuxissakin.
y86 poikkeukset¶
y86-arkkitehtuurissa on kolme sisäistä poikkeusta:
- Konekielen käsky
halt
, joka lopettaa ohjelman suorituksen - Väärä operaatiokoodi
INS
- Väärä muistiosoite
ADR
Mutta, poikkeuskäsittelijöitä ei ole y86-simulaattorissa toteutettu.
Sekventiaalisessa y86:sessa ohjelman suoritus keskeytyy ja vastaava tilabitti asetetaan. Tämän jälkeisillä käskyillä ei ole vaikutusta prosessorin tilaan.
Liukuhihna-suorittimessa poikkeusten käsittely onkin astetta monimutkaisempaa:
- Usea käsky eri vaiheissa aiheuttaa poikkeuksen yhtaikaa (samalla kellojaksolla). Tällöin pitää selvittää missä järjestyksessä poikkeukset käsitellään. y86:ssa idea on, että ensin käsitellään pisimmälle liukuhihnalla ehtinyt käsky.
- Ehdollisen hypyn tuloksena voi aiheutua poikkeus, jos muistista haettu käsky aiheuttaa poikkeuksen ja käy ilmi, että ennustus oli väärä. Tämä tilanne voi syntyä esimerkiksi jos muistiosoite (tai jopa operaatiokoodi) on väärä.
main: xorq %rax,%rax jne target irmovq $1,%rax target: movl $13,%rbx # väärä operaatiokoodi (tässä x86-käsky)
- Liukuhihnakäskyt muuttavat yhteistä resurssia virheelliseen tilaan (esim. pino-osoitin) ja tästä aiheutuu poikkeus. Tämä tilanne on mahdollinen jos käsky on Memory-tilassa ja toinen käsky on Execute-tilassa ja päivittää tilabitit. Eli myöhempi käsky ei tarkista tilabittejä ennen suoritustaan. Tämän vuoksi
STAT
-rekisterin pitäisi kulkea liukuhihnarekisterien mukana. - Nyt liukuhihnaprosessorissa Write back-vaiheessa tarkistetaan tilabitit, ja jos poikkeus on tapahtunut, W-liukuhihnarekisterin arvot voidaan tallettaa prosessorin tilana.
y86:sessa poikkeuksen tapahduttua ohjelman suoritus keskeytetään ja tätä seuraavat käskyt eivät enää muuta prosessorin tilaa.
Lopuksi¶
Poikkeuksien hallinta on nykyisissä prosessoreissa varsin monimutkainen asia, kuten huomasimme y86-liukuhihnaprosessorin tapauksesta..
Kuten olemme aiemmin oppineet, sulautetuissa järjestelmissä poikkeukset/tapahtumat/keskeytykset ovat hyvä tapa kommunikoida oheislaitteiden kanssa asynkronisuuden takia. SensorTagin RTOS:n toiminta on tästä syystä (suurelta osin) rakennettu poikkeuspohjaiseksi.
Anna palautetta
Kommentteja materiaalista?