Järjestelmäpalvelut ja poikkeukset¶
Osaamistavoitteet: Laiteohjelmisto 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 modernimmin UEFI) sisältävät käyttäjille näkyvinä osina järjestelmän asennusohjelmiston ja käynnistyksen yhteydessä toteutettavat laitteistotestit.
Yleisesti laitteistoyhteensopivuuden saavuttamiseksi laiteohjelmisto tarjoaa standardoitun integrointiympäristön eri valmistajien laitteistokomponenteille, jonka avulla tietokonejärjestelmä voidaan kasata eri valmistajien komponenteista. Tätä ideaa pidetään oleellisena tekijänä (alunperin IBM:n kehittämän) PC-teknologian voittokululle kotitietokoneiden alkuaikoina.
Laiteohjelmiston/BIOSn palveluita kutsutaan kahden eri mekanismin kautta:
- Ohjelmallisilla keskeytyksillä konekielestä (engl. trap instructions)
- Esimerkki. PC-kotitietokoneen BIOS-kutsulistaus
- järjestelmäkutsuilla (systeemikutsu, engl. trap / system call) ohjelmakoodista, joita käytetään ohjelmassa ikäänkuin valmista aliohjelmakirjastoa.
- Esimerkki käynnistyssekvenssi BIOS:n avulla.
Esimerkki. Kirjoitusmerkin tulostus ruudulle BIOS-kutsua ja ohjelmistokeskeytystä käyttäen
mov ah, 0x0e # rekisteri AH funktio = 0x0E (Display Character)
mov al, 0x56 # rekisteri AL kirjoitusmerkki
int 0x10 # keskeytys 0x10 (BIOS video service)
Itse laiteohjelmisto on usein lukumuistissa (ROM-muisti, engl. read only memory) olevaa konekielistä/mikro-ohjelmakoodia, jonne ohjelmakoodista hypätään hyppykäskyillä ihan kuten aliohjelmiin yleensäkin. Varhaisissa kotitietokoneissa laiteohjelmisto olikin toteutettu prosessorin kanssa samalle piirikortille integroidulle erilliselle ROM-piirille, joka näkyi osana ohjelmamuistia. Päivitys BIOS:n tehtiin sitten vaihtamalla fyysinen piiri toiseen.
Esimerkki. Commodore 64-kotimikron laiteohjelmiston (Kernal) valmiit funktiot on toteutettu suorittimen konekielellä.
BIOS-kutsujen kerrostus¶
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 mm. standardikirjaston toteutukset sitten hyödyntävät.
Koska nämä kutsut ovat vielä alemman/laiteläheisemmän kerroksen funktiota, ne vaativat enemmän laitteisto/käyttöjärjestelmä-läheisien yksityiskohtien huomiointia 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.
Poikkeukset ja niiden hallinta¶
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 yllämainitun 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.
Usein myös moniajoa tukevasti, ohjelmamme 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. 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 toiminnan valvontaan, esimerkiksi estetään nollalla jakaminen tai muistivirhe, ja näinollen niillä on korkeampi prioriteetti. Kun laitteistokeskeytys tulee ihan vastaavasti hypätään sen käsittelijään laiteohjelmistossa.
Esimerkki. x86-perheessä laitteistokeskeytyksiä on 16 kpl (jaettuna kahteen ohjainpiiriin).
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 palvelut jakaantuvat myös osin käyttöjärjestelmän hallinnoimiksi palveluiksi, joita päästään siis ohjelmassa 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 taas rekistereissä). Lisäksi joissain suorittimissa on suojattuja 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. Huomataan järjestelmäkutsun tekevä assembly-kielen käsky
syscall
. 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
Sekventiaalisessa y86:sessa ohjelman suoritus keskeytyy ja vastaava tilabitti asetetaan. Tämän jälkeisillä käskyillä ei ole vaikutusta prosessorin tilaan.
y86-liukuhihnasuorittimessa poikkeusten käsittely onkin astetta monimutkaisempaa, esimerkkinä poikkeusten käsittelyn ohjauslogiikan toteutuksesta.
- 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.
Mutta, poikkeuskäsittelijöitä ei ole y86-simulaattorissa toteutettu. y86:sessa poikkeuksen tapahduttua ohjelman suoritus keskeytetään ja tätä seuraavat käskyt eivät enää muuta prosessorin tilaa.
Lopuksi¶
Mielenkiintoista huomata, että laiteohjelmiston toimintaidea onkin pysynyt samana vuosikymmeniä. Mutta suoritinteknologioiden kehittyessä poikkeuksien hallinta on nykyisissä prosessoreissa sekin varsin monimutkainen asia.
Anna palautetta
Kommentteja materiaalista?