ITKA203 Käyttöjärjestelmät, luento 7/14, Ke 8.6.2011 ==================================================== Info: - Demo 2 on julkaistu. Käydään läpi pikaohje == KYSY! - ensi viikolla Mattilanniemessä. Tämän päivän muistiinpanot: --------------------------- (Sanastosta: kernel == ydin == käyttöjärjestelmän ohjelmakoodi jota on suoritettava prosessorin ollessa käyttöjärjestelmätilassa) FLIH: - Ytimessä (kuten käyttäjän ohjelmissakin) tarvitaan atomisia toimenpiteitä eli sellaisia jotka eivät saa keskeytyä. Siksi keskeytyksen tullessa prosessori kieltää uudet (ainakin saman prioriteetin omaavat) keskeytykset, kunnes käyttöjärjestelmän keskeytyskäsittelijä eksplisiittisesti jälleen sallii ne. Käytännössä keskeytyskielto on lippubitti prosessorin lippurekisterissä (esim. 0=ei saa keskeyttää; 1=saa keskeyttää). - Aiemmin todettiin että FLIH:ssä prosessori vaihtaa toimintatilansa käyttöjärjestelmätilaksi (ellei se aiemmin jo ollut). - Olennaista prosessorin FLIH-toimenpiteessä on, että keskeytynyttä prosessia voidaan myöhemmin jatkaa samasta kohtaa. Siis kontekstista on tallennettava automaattisesti ainakin ohjelmalaskurin, pino-osoittimen ja lippurekisterin arvot. Pinoon, luonnollisestikin. - Olennaista on että keskeytyksen jälkeen ollaan suorittamassa seuraavaksi käyttöjärjestelmän koodia, jotakin ns. keskeytyskäsittelijää. + Eräs yksinkertainen tapa, jolla prosessori voisi löytää oikean keskeytyskäsittelijän, on laittaa IP-rekisteriin keskeytyksen aiheuttajaa vastaava muistipaikka **keskeytysvektorissa**, joka on muistialue, jossa on hyppykäskyjä ("jmp") eri tyyppisten keskeytysten käsittelijäohjelmiin. Käyttöjärjestelmä on varhaisessa käynnistymisensä vaiheessa valmistellut vektorin hyppykäskyt siten että ne hyppäävät oikeisiin käsittelijöihin. + Nykyään tämä saattaa olla hienostuneempaa.. FLIHin yksityiskohdat esimerkkiarkkitehtuurissamme x86-64:ssä on luettava jälkikäteen materiaalista; pyrin tiivistämään asian sinne tällä viikolla. Prosessin tilamuutosten tarkentaminen: - Nykyään on tyypillistä, että käyttöjärjestelmän koodia kartoitetaan mukaan jokaisen prosessin muistiavaruuteen. Välittömästi keskeytyksen jälkeen ei siis ole vielä vaihdettu suoritusta prosessista toiseen, vaan sanotaan että käynnissä ollut prosessi on "kernel running" -tilassa. IP:llä täytyy toki olla uusi arvo, joka osoittaa käyttöjärjestelmän koodiin. Myös SP voi osoittaa FLIH:in jälkeen eri pinoon, ns. kernel-pinoon. Muuta kontekstitietoa ei välttämättä tarvitse vaihtaa, jos keskeytys sattuisi vaikka olemaan nopeasti käsiteltävissä olevaa sorttia. Tarkennetaan käsitystä käyttöjärjestelmän tietorakenteista: - Prosessitaulu on yhtäjaksoinen muistialue, joka sisältää prosessien tiedot. Prosessi voidaan yksilöidä sen perusteella, monentenako sen elementti tässä prosessitaulussa on. Tämän indeksin nimi on PID ("process identifier" tjsp.). - Prosessielementin sisältöä tarkennettiin: + PID eli prosessin indeksi + vanhempiprosessin PID + UID ("user ID") eli tieto prosessin omistavasta käyttäjästä + kirjanpitotietoa kuten käynnistysajankohta, suoritusaika + tiedot muistialueista + tiedot muista ns. resursseista, mm. tiedostoista, viestikanavista Tutkittiin ps:n ja top:n tulosteita: PID:t, vanhempi- ja lapsiprosessit. Seuraavaksi katsottiin lisää esimerkkejä käyttöjärjestelmäkutsuista. Tutkittiin "ihan omaa shell-ohjelmaa" nimeltä minish: + Shellin perustoimintaperiaate: shell odottaa käyttäjältä komennon, jonka tulisi olla ohjelman nimi sekä argumenttilista. Sitten shell käynnistää ohjelman ja odottaa kunnes käynnistetty ohjelma loppuu. + Toki ohjelman käynnistys on käyttöjärjestelmän vastuulla ja sitä täytyy pyytää palveluna kutsurajapinnan kautta. + Unix-järjestelmissä ohjelmat käynnistetään ensin haarauttamalla fork nykyinen prosessi ja sitten vaihtamalla syntyneen lapsiprosessin ohjelma uudella ohjelmalla. + Kutsu fork() luo uuden prosessin (konkreettisesti siis prosessielementin ja virtuaalimuistiavaruuden). Uusi prosessi on identtinen kopio alkuperäisestä muuten, mutta fork() -kutsun paluuarvo on näissä kahdessa eri: * Lapsiprosessi voi halutessaan (eli yleensä) tunnistaa itsensä siitä, että fork() palauttaa luvun 0. * Vanhempiprosessi voi halutessaan (eli yleensä) tunnistaa itsensä siitä, että fork() palauttaa kokonaisluvun, joka on suurempi kuin 0. Tämä luku on syntyneen lapsiprosessin PID. * Jos fork() epäonnistuu (esim. prosessitaulu on jo täynnä eikä siis ole vapaata lokeroa millään PID:llä), se palauttaa luvun -1. + Kutsu exec() tai esimerkissämme execve() pyytää käyttöjärjestelmää korvaamaan prosessin tiedot vastaamaan uutta ohjelmaa, jonka se siis lataa muistiin tiedostosta, mahdollisesti linkittää kirjastoihin, ja alustaa (mm. kartoittaa prosessille muistia, asettaa kontekstissa RIP:n osoittamaan ohjelmakoodin alkua ja RSP:n "tyhjän" pinon pohjaa.) Kutsu exec() ei tietysti onnistuessaan palaa, eikä sen jälkeisiä käskyjä suoriteta (niiden koodihan ei enää edes näy prosessin muistiavaruudessa, jos uuden koodin lataaminen onnistui). Eli execin jälkeen toki kannattaa kirjoittaa koodia, jolla käsitellään execin epäonnistuminen syystä tai toisesta. Voi epäonnistua helposti esim. jos ei ole olemassa sen nimistä tiedostoa kuin kutsussa annettiin, tai jos pyytävällä prosessilla (UID-tieto) ei ole tiedostoon riittäviä oikeuksia. + esimerkkiohjelmassa on toteutettu tyypillinen fork() ja exec() pari, jossa myös tarkistetaan virhetilanteet. Koodiin kannattaa tutustua; forkkaus-osuuden toiminta täytyy ymmärtää. + Käyttöjärjestelmäkutsu wait() odottaa tässä tapauksessa lapsiprosessin suorituksen loppuun. Säie: - Säie (engl. "thread") vastaa prosessin tilaan ja vuoronnukseen liittyviä ominaisuuksia: + oma konteksti mutta isäntäprosessin resurssit kuten muistiavaruus ym.. + voidaan ajatella että jokaisella prosessilla on vähintään yksi säie eli "suorituskohta, jossa prosessi on menossa". - monia tarpeita varten (esim. rinnakkaislaskenta, käyttäjän klikkauksiin vastaaminen samalla kun ohjelma laskee jotakin tulosta "taustalla") saatetaan haluta suorittaa yhtä ohjelmaa eri kohdista. Säikeet mahdollistavat tämän. Käsitellään säikeitä nyt KLT-mielessä (kernel-level thread) eli käyttöjärjestelmän tarjoamana palveluna. - tullaan huomaamaan että säikeiden sujuva yhteispeli vaatii ohjelman tekijältä tiettyä huolellisuutta ja käyttöjärjestelmältä tiettyjä palveluita. Siirryttiin alustavasti seuraavaan aiheeseen. Prosessien kommunikointi (Inter-process communication, IPC): - Prosessien pitää voida kommunikoida toisilleen, esim. olisi kiva jos shellistä tai pääteyhteydestä käsin voisin pyytää jotakin toista prosessia suorittamaan lopputoimet ja sulkeutumaan nätisti. Esim. reaaliaikaisten chat-asiakasohjelmien täytyy pystyä kommunikoimaan toisilleen jopa verkkoyhteyksien yli. Jos tieteellinen laskentaohjelma ja sen käyttöliittymä toteutetaan eri prosesseina, toki käyttöliittymästä olisi kiva voida tarkistaa laskennan tilannetta ja ehkä pyytää myös välituloksia näytille... jne... eli ilmeisesti käyttöjärjestelmässä tarvitaan mekanismeja prosessien väliseen kommunikointiin. - Varmaankin yksinkertaisin prosessien kommunikointimuoto ovat signaalit. Katsottiin esimerkkejä signaalin lähettämisestä ja ohjelmasta, joka on valmistautunut reagoimaan niihin omalla tavallaan. + Käyttöjärjestelmä huolehtii että seuraavan kerran kun signaalin kohteena oleva prosessi pääsee suoritusvuoroon, ei sen suoritus jatkukaan aiemmasta kohdasta vaan signaalinkäsittelijästä. + sovellusohjelmoijan pitää tietysti itse kirjoittaa ohjelmansa signaalinkäsittelijät sekä hoitaa tapahtumaan sellaiset käyttöjärjestelmäkutsut, joilla signaalinkäsittelijät pyydetään rekisteröimään. + (ai niin, ja jos signaalikäsittelijöitä ei itse kirjoita, tapahtuu tiettyjen signaalien kohdalla oletuskäsittely...) - Katsottiin myös lyhyesti esimerkki, jossa prosessi lähetti toiselle prosessille merkkijonon, jonka saapumista kohteena oleva prosessi oli jäänyt odottamaan. Viestin lähetyksen jälkeen prosessi jäi taas odottamaan vastausta toisesta päästä. Sattumalta ohjelmassa oli "virhe" jonka vuoksi tietyssä tapauksessa viesti ei lähtenytkään menemään, mutta prosessi jäi silti odottamaan vastausta toisesta päästä. Juttelu loppui sitten siihen, lopullisesti, kun molemmat prosessit odottivat viestiä toiselta. Tämä on kärjistetty esimerkki lukkiutumistilanteesta, johon palaamme ensi viikolla, kuten muuhunkin prosessien väliseen kommunikointiin haasteineen ja ratkaisuineen.