Luku 1

Ohjelma

Tietokone suorittaa ohjelmia sen omalla suorittimella. Mutta mikä on ohjelma? Puhekielessä ohjelma voi itse olla melkein mikä tahansa määrittely tapahtumien kululle, mutta tietokoneen suorittamalle ohjelmalle annetaan paljon tarkemmat vaatimukset. Tutustumme niihin tässä. Tarkastelemme myös (tietokone)ohjelmien erilaisia esitysmuotoja.

Ohjelma

Ohjelma on jonkin ongelman algoritmin avulla kuvattu ratkaisu, jonka voi suorittaa tietokoneella. Algoritmissa on joukko askelia, jotka järjestyksessä suoritettuina antavat ongelman ratkaisun. Ruokareseptit ovat hyviä esimerkkejä algoritmeista, vaikka tietokoneet niitä yleensä eivät voi suoraan suorittaa. Robotit kyllä osaavat, mutta robotteja ohjaavat algoritmit ne vasta monimutkaisia ovatkin.

Algoritmi juustokakun tekemiseksi

1. Jauha keksit yleiskoneessa keksimuruiksi.
2. Lisää voisula ja sokeri, sekoita tasaiseksi.
3. Kaada seos voidellun vuoan pohjalle ja taputtele tasaiseksi.
4. Sekoita täytteeksi kulhossa rahka, juusto, munat, sokeri ja suuruste.
5. Kaada täyte vuokaan ja paista kunnes täyte on hyytynyt.
6. Nosta kakku pöydälle jäähtymään ja irrota se heti vuoasta veitsellä.
7. Anna jäähtyä ennen tarjoilua.

Meitä kiinnostaa vain tietokoneella ratkaistavissa olevat ongelmat ja niitä ratkaisevat ohjelmat. Ongelma voi vaikkapa olla, että paljonko on 87*555+32 tai mikä on nopein reitti ajaa polkupyörällä paikasta A paikkaan B? Ensimmäisen esimerkin ratkaisu on hyvin yksinkertainen, mutta jälkimmäinen vaatii huomattavan monimutkaisen algoritmin ja paljon dataa.

Algoritmi lausekkeen laskemiseksi laskimella

1. Nollaa laskimen muisti.
2. Laske 87 * 555.
3. Lisää tuloon 32.
Algoritmi nopeimman pyöräilyreitin valitsemiseksi

1. Hae kaikki pyöräilyreitit paikasta A paikkaan B käyttäen
   tunnettuja katuja/teitä/pyöräteitä. Ota mukaan vain reitit,
   joissa mennään koko ajan lähemmäs B:tä.
2. Jaa kukin reitti pätkiin kadun/tien/risteyksen tyypistä riippuen.
3. Laske kullekin reittipätkälle sen vaatima aika pyöräilyyn.
4. Laske kunkin reitin kokonaisaika.
5. Näytä nopeimman reitin kokonaisaika ja reitti kartalla.

Kontrollin siirto

Ohjelmille on tyypillistä, että niissä on joukko peräkkäin suoritettavia enemmän tai vähemmän monimutkaisia toimintoja. Yleensä ohjelmassa suoritetaan toimintoja siinä järjestyksessä kuin ne ohjelmassa esitetään. Joissakin tapauksissa on kuitenkin tärkeää, että tästä oletusarvoisesta toimintojen järjestyksestä poiketaan. Tällaista tapahtumaa kutsutaan kontrollin siirroksi. Tyypillinen tapaus on haarautuminen, jossa tietyn ehdon vallitessa valitaan kahdesta tai useammasta vaihtoehdosta se oikea toimintojen joukko tällä suorituskerralla. Toinen tyypillinen tapaus on toistorakenteiden (silmukoiden) käyttö, missä annettu joukko toimintoja suoritetaan monta kertaan kunnes jokin ehto tulee todeksi.

Kolmas hyödyllinen kontrollin siirtoon liittyvä käsite on aliohjelma (funktio, metodi). Aliohjelmassa annettua joukkoa toimintoja voidaan suorittaa useammasta paikkaa ohjelmaa kutsumalla kyseistä aliohjelmaa, mahdollisesti joidenkin parametrien kanssa. Aliohjelman kutsuminen on täsmällisesti määritelty toiminto muiden toimintojen joukossa. Parametrien avulla aliohjelman toimintaa voidaan säätää joka kutsukerralla siihen tilanteeseen sopivaksi. Aliohjelmat voivat myös palauttaa jonkin arvon kutsukohtaan, jolloin niitä sanotaan funktioiksi. Joissakin ohjelmointikielissä aliohjelmia tai funktioita sanotaan metodeiksi. Aliohjelmille on tyypillistä, että niillä on oma laskentaympäristö, jossa voi esimerkiksi olla aliohjelman omia tietorakenteita. Niitä voi käyttää vain tämän aliohjelman suorituksen aikana. Aliohjelmille on ominaista, että sen toimintojen loppuun suorittamisen jälkeen palataan takaisin aliohjelman kutsukohtaan. Aliohjelmaa kutsuttaessa kontrolli siirtyy sen alkuun ja aliohjelmasta palatessa kontrolli palaa aina aliohjelman kutsukohdan jälkeiseen toimintaan.

Esimerkki: Funktio Sum(Table, n)

Funktio Sum(Table, n) palauttaa arvonaan n-alkioisen taulukon Table
kaikkien n:n alkion summan.

Kutsu Sum(Noppa, 6) palauttaa taulukon Noppa kaikkien kuuden alkion
arvojen summan 21.

Kutsu Sum(Palkka, 2345678) palauttaa taulukossa Palkka olevien pienen
valtion kaikkien työssäkäyvien 2345678 henkilön palkkojen
summan 10 135 676 310,23.

Samaa funktiota voi käyttää hyvin moneen tarkoitukseen
sopivilla parametrien valinnoilla.

Käyttöjärjestelmän palvelut aliohjelmina

Käyttöjärjestelmissä on useita aliohjelmia tai funktioita, joita mikä tahansa suorituksessa oleva ohjelma voi käyttää (kutsua). Ne eroavat ohjelman omista aliohjelmista siinä, että ne on suunniteltu yleiskäyttöisiksi ja helpottamaan ohjelmien toteutusta. Tällaisia palveluja ovat esimerkiksi tekstin lukeminen näppäimistöltä tai tiedoston kirjoittaminen muistitikulle. On tosi käytännöllistä, että jokaisen ohjelmoijan ei tarvitse erikseen pohtia tällaisten toimintojen yksityiskohtia. Ohjelmien (ja ohjelmoijien!) ei edes tarvitse tietää minkälainen näppäimistö tai muistitikku laitteistossa on. Ohjelma toimii myös sellaisenaan, vaikka muistitikku vaihdettaisiin kovalevyyn.

Käyttöjärjestelmän palvelupyyntöjä kutsutaan joskus järjestelmäkutsuiksi, koska halutaan tehdä selkeä ero ohjelman omien aliohjelmien kutsuilla ja käyttöjärjestelmän palvelujen kutsuilla. Järjestelmäkutsuilla ohjelma siirtyy suorittamaan käyttöjärjestelmän palveluita ja sen vuoksi näiden (järjestelmä)palveluiden toteutuksen kanssa pitää olla erityisen varovainen. Järjestelmäpalvelun kutsun yhteydessä tarkistetaan, onko palvelua kutsuneella ohjelmalla oikeus käyttää juuri tuota palvelua. Esimerkiksi, mikä tahansa ohjelma voi kirjoittaa omia tietojaan kovalevyn tiedostojärjestelmään, mutta mikä tahansa ohjelma ei saa fyysisesti tutkia kovalevyn sisältöä. Sen lisäksi käyttöjärjestelmä valvoo, että ohjelma saa lukea ja kirjoittaa vain sellaisia tietoja, mihin sillä on oikeus.

Symbolinen konekieli, numeerinen konekieli

Tietokone osaa suorittaa ainoastaan konekielisiä ohjelmia, jotka on suoritusaikana talletettu sen muistiin. Puhdas konekieli on pelkkiä numeroita, mikä sopii hyvin koneille mutta huonosti ihmiselle. Kustakin konekielestä on ihmisiä varten oma symbolinen konekieli, joka on muistuttaa hyvin paljon kyseistä konekieltä mutta on paljon helpompi ihmisen lukea ja kirjoittaa. Lausekkeen 87*555+32 arvon laskeva ratkaisu symbolisella konekielellä voisi olla vaikkapa:

Esimerkki: Lauseke 87*555+32 symbolisella konekielellä

LOAD R1, =87   -- laita rekisterin R1 arvoksi 87. R1:n vanha arvo tuhoutuu.
MUL  R1, =555  -- kerro R1:n arvo luvulla 555
ADD  R1, =32   -- lisää R1:n arvoon luku 32. Vastaus on nyt rekisterissä R1.

Tämän on silti aika vaikeaselkoista, varsinkin jos algoritmi on vähänkin monimutkaisempi. On vaikea pitää kirjaa, mikä merkitys kullakin rekisterillä eri hetkillä on ja kaikki laskenta tapahtuu kuitenkin rekistereitä käyttäen. Konekielen numeerinen muoto on vielä vaikeampi, koska siinä on pelkkiä numeroita:

Esimerkki: Lauseke 87*555+32 numeerisella konekielellä

35651571   -- laita rekisterin R1 arvoksi 87. R1:n vanha arvo tuhoutuu.
320864811  -- kerro R1:n arvo luvulla 555
287309856  -- lisää R1:n arvoon luku 32. Vastaus on nyt rekisterissä R1.

Symbolinen konekieli on huomattavasti mukavampaa lukea, eikö olekin! Yleensä konekieliset ratkaisut toteutetaan käyttäen symbolista konekieltä, joka käännetään (muunnetaan) numeeriseen muotoon ennen suoritusta. Symbolisen konekielen kääntäminen konekieleksi on hyvin suoraviivaista, vaikka se ei ihan triviaalia olekaan.

Korkean tason kieli

Olisi mukavaa, jos tietokoneelle voisi antaa algoritmin missä tahansa muodossa, vaikka sanelemalla se ääneen suomen kielellä tai kirjoittamalla auki halutut toimenpiteet proosana sanskriitin kielellä. Käytännössä näin ei kuitenkaan tehdä, koska noilla tavoin annettujen algoritmien täsmällinen ymmärtäminen on (ainakin vielä) liian vaikeata. Algoritmien kuvauskielen on hyvä olla yleinen ja riittävän säännönmukainen, jotta sitä voidaan käsitellä koneellisesti. Symbolinen konekieli sopisi tähän, mutta se on turhan yksinkertaisella tasolla, jotta sen avulla olisi järkevää toteuttaa suuria ohjelmistoja. Symbolisessa konekielessä on lisäksi huono ominaisuus, että sen avulla esitetyt ohjelmat ovat suoritettavissa ainoastaan juuri sitä konekieltä käyttävissä tietokoneissa.

Ohjelmat toteutetaan (ohjelmoidaan) yleensä tätä varten suunnitelluilla korkean tason ohjelmointikielillä. Ne ovat riittävän säännönmukaisia, jotta niillä kirjoitetut ohjelmat ovat helppoja tai ainakin mahdollisia muuttaa automaattisesti tietokoneella suoritettaviin muotoihin. Tällaisia kieliä ovat esimerkiksi Java, C, C++, Scheme, Prolog, Python ja SQL. Vanhimpia käyttökelpoisia korkean tason ohjelmointikieliä olivat 1950-luvulla julkaistut Fortran ja ALGOL. Fortran on edelleenkin käytössä, vaikka sitä on vuosien saatossa kehitetty huomattavasti.

Osaat ehkä jo itsekin jotain korkean tason kieltä. Esimerkiksi, yllämainitun lausekkeen ratkaisu on Javalla, C'llä, Fortranilla, ALGOLilla ja Schemellä vähän erilainen:

Esimerkki: Lauseke 87*555+32 Javalla tai C'llä

X = 87*555+32;   -- vastaus on nyt muuttujan X arvona
Esimerkki: Lauseke 87*555+32 Fortranilla tai ALGOLilla

X := 87*555+32;  -- vastaus on nyt muuttujan X arvona
Esimerkki: Lauseke 87*555+32 Schemellä

(+ (* 87 555) 32)-- vastaus on nyt ulomman lausekkeen arvona

Ratkaisujen ohjelmointi korkean tason kielellä on paljon lähempänä ihmisen omaa ajattelumallia ja sen vuoksi ainakin aluksi helpompia toteuttaa kuin (symbolisella) konekielellä kirjoitetut ohjelmat. Ohjelmia voi kirjoittaa myös symbolisella konekielellä, mutta se vaatii niin konekielen kuin sitä suorittavien suorittimien täsmällistä tuntemista. Yleensä käyttöjärjestelmän alimmat suoritinsidonnaiset osat tehdään (symbolisella) konekielellä, kun muu käyttöjärjestelmä ja tavallinen ohjelmointi toteutetaan korkean tason kielillä.

Ohjelmoinnin ammattilaiset osaavat useita ohjelmointikieliä, koska ongelman ratkaisu voi olla helpompi kuvata tietyllä ohjelmointikielellä kuin jollain toisella. Joskus kannattaa käyttää Prologia, kun taas joku toinen ongelma tai ongelman osa voi olla kätevämpää ratkaista C'llä tai C++'lla. Tämä on vähän sama asia kuin se, että ranskan kieli on kuulemma erittäin hyvä rakkauden asioihin, kun suomen kieli taas on hyvä kiroilemiseen. Ilmankos meillä suomalaisilla on joskus ongelmia rakkauden suhteen. Olisiko parempi käyttää aika ranskan kielen opiskeluun Pythonin asemesta?

Ohjelman esitystavan muunnokset

Ohjelmointi tehdään yleensä korkean tason kielellä. Tuloksena syntynyt ohjelma talletetaan massamuistiin, jossa sitä voidaan säilyttää ilman sähkövirtaa. Suoritusaikana ohjelman pitää olla kuitenkin konekielisessä muodossa talletettuna haipuvaan mutta nopeaan keskusmuistiin.

Tarvitsemme esitystavan muunnoksen korkean tason kielestä konekieleen. Muunnos tehdään vaiheittain. Ensin ohjelma käännetään konekieliseen muotoon. Sitten siihen linkitetään (yhdistetään) muita itse tehtyjä ohjelman osia tai valmiina eri kirjastoissa olevia ohjelmien osia eli kirjastomoduuleita.

Kolme isoa laatikkoa, jotka kuvaavat kaikki massamuistin sisältöä. Vasemman puolimmainen laatikko esittää korkean tason kielistä esitystä ja siinä on kolme moduulia, Compute, Print ja Math. Kun ne käännetään, niistä saadaan keskimmäisen laatikon konekieliset esitykset moduuleille Compute, Print ja Math. Kirjastomoduuli Math voi olla siellä jo valmiinakin konekielisessä muodossa. Kun nämä kolme moduulia linkitetään yhteen, saadaan oikeanpuoliseen laatikkoon ohjelma P konekielisessä muodossa yhtenä latausmoduulina.

Lopuksi ohjelma ladataan konekielisessä muodossa tietokoneen muistiin ja sen suoritus voi alkaa. Näin tapahtuu esimerkiksi, kun klikkaat tietokoneen näytöllä jotain kuvaketta (ikonia). Tällöin kyseisen ohjelman valmiiksi linkitetty konekielinen esitysmuoto ladataan massamuistista keskusmuistiin suoritusta varten. Joissakin tapauksissa kuvake voi esittää tiedostoa (esim. cute-cat.jpg, jolloin kuvaketta klikkaamalla aukeaa kyseistä tiedostoa käsittelevä ohjelma (esim. Paint) ja kyseinen tiedosto on ohjelmassa valmiiksi avattuna.

Yksinkertainen esimerkkijärjestelmä, jossa Suoritin, muisti ja massamuisti ovat yhdistettynä väylään. Massamuisti on yhdistetty väylään laiteohjaimen kautta. Massamuistissa olevasta ohjelma P:stä menee nuoli muistissa olevaan prosessiin P1, joka sisältää P1:n koodin, datan ja muut tiedot.

Kääntäjät, linkittäjät ja lataajat ovat ihan tavallisia ohjelmia ja ne sisältyvät käyttöjärjestelmän peruspalikoihin. Ohjelman lataaminen massamuistista muistiin suoritusta varten on itse asiassa vähän monimutkaisempaa kuin miltä se ehkä kuulostaa. Se ei ole pelkkää konekielisen koodin kopiointia, vaikkakin sekin pitää tietenkin tehdä. Jokaista ohjelman suorituskertaa varten käyttöjärjestelmä luo ohjelmasta suorituskelpoisen prosessin, jolle varataan tarvittavat muistialueet ja muut järjestelmän resurssit. Prosessi on siis ohjelman suoritusaikainen esitysmuoto. Samasta ohjelmasta voi olla yhtä aikaa monta prosessia suoritettavana. Esimerkiksi jokainen näytöllä oleva selaimen ikkuna on oma prosessinsa samasta selain-ohjelmasta.

Tulkittavat ohjelmointikielet

Jotkut ohjelmointikielistä ovat ns. tulkittavia kieliä. Se tarkoittaa, että niillä kirjoitettuja ohjelmia ei käännetäkään suorituksen tekevän tietokoneen omalle konekielelle vaan ne annetaan jollekin toiselle ohjelmalle (tulkille) suoraan tai vähän muokattuna syötteenä. Esimerkkinä tällaisesta tulkittavasta korkean tason ohjelmointikielestä on Python. Python-tulkki on tässä tilanteessa se varsinainen tietokoneella suoritettava ohjelma ja sinun Pythonilla kirjoittama ohjelmasi vain tekstimuotoista dataa (syötettä) Python-tulkille. Python-tulkki taas on ihan tavallinen (esim.) C-kielellä kirjoitettu ohjelma. Tosin on olemassa myös Python-tulkki PyPy, joka on kirjoitettu Pythonilla itsellään! Sehän on vähän sama kuin että ranskan kielen oppikirja olisi kirjoitettu vain ranskan kielellä ilman mitään sanastoa. Ei ihan helppoa!

Kun Python-tulkki (prosessi PT) suorittaa (ks. seuraava kuva) suorittaa Python ohjelmaa (P), niin käyttäjästä tuntuu ihan siltä, että tietokone osaisi suorittaa Python-ohjelmia. Näinhän se tavallaan onkin, mutta tälläkin kertaa varsinainen älykkyys ja osaaminen ovat järjestelmässä suorituksessa olevassa Python-tulkissa (PT). Todellisen konekielisen ohjelman tapauksessa (ks. edellinen kuva) tietokoneen suoritin itsessään osaa suorittaa konekielisen ohjelman konekäskyjä ja siinä on suuri ero!

Yksinkertainen esimerkkijärjestelmä. Suoritin, muisti ja massamuisti ovat yhdistettynä väylään. Massamuisti on yhdistetty väylään laiteohjaimen kautta. Massamuistissa on suoritettava Python-ohjelma P sen tekstuaalisessa esitysmuodossa. Siellä on myös Python-tulkki konekielisessä esitysmuodossa. Lataamalla ohjelma Python-tulkki muistiin saadaan suorituksessa oleva prosessi PythonTulkki, jolle on annettu myös muistiin kopioitu ohjelma P datana.

Toinen esimerkki tulkin käytöstä on Java-ohjelmien kääntäminen ns. tavukoodiksi (byte-code), jota voi sitten suorittaa tulkitsemalla Java-virtuaalikoneessa (JVM). Javan tavukoodi on hypoteettisen (ei todellisen, suunnitellun?) tietokoneen (JVM) konekieltä, mikä on hyvin erilaista kuin järjestelmän oman suorittimen konekieli. JVM:lle ei voi antaa siinä suoritettavaa ohjelmaa suoraan Javalla kirjoitettuna, vaan suoritettava Java-ohjelma on aina ensin käännettävä tavukoodiksi.

Myös tässä tapauksessa käyttäjällä on mielikuva ja tuntuma, että tietokone osaa suorittaa Java-ohjelmia (ks. seuraava kuva). Nyt tämän harhakuvan saa aikaa kaksi erillistä ohjelmaa. Ensin yksi ohjelma kääntää etukäteen Java-kielisen ohjelman (P) Javan tavukoodiksi ja sitten toinen ohjelma (JVM) suoritusaikana emuloi (jäljittelee) hypoteettista Java-virtuaalikonetta, joka suorittaa ohjelman P tavukoodisia käskyjä yksi kerrallaan. Käyttäjälle voi helposti jäädä mielikuva, että tietokone osaa suorittaa Java-ohjelmia, vaikka suorituksen tekee oikeasti järjestelmässä ajettava ohjelma (JVM).

Yksinkertainen esimerkkijärjestelmä. Suoritin, muisti ja massamuisti ovat yhdistettynä väylään. Massamuisti on yhdistetty väylään laiteohjaimen kautta. Massamuistissa on tekstuaalisessa muodossa Java-ohjelma P, josta kääntämällä saadaan massamuistiin Java-ohjelma P tavukoodina. Massamuistissa on myös konekielinen JVM. JVM ladataan muistiin, jolloin siitä tulee prosessi JVM. Muistiin kopioidaan myös tavukoodinen Java-ohjelma P, joka syötetään datana JVM:lle.

On tärkeää erotella se, mitä tietokonejärjestelmä eri ohjelmien avustuksella osaa tehdä, siitä, mitä suoritin itse osaa tehdä. Järjestelmän älykkyys on siinä suoritettavissa ohjelmissa ja ohjelmia voi suorittaa joko suoraan suorittimella tai tulkitsemalla (emuloimalla) niitä muiden ohjelmien avustuksella.

On olemassa tavukoodikääntäjiä myös muille ohjelmointikielille (esimerkiksi Pythonille), jolloin niilläkin kirjoitetut ohjelmat voidaan suorittaa JVM:ssä. Yhden ohjelman eri osat voi täten olla helposti toteutettuna juuri niihin osiin parhaiten sopivilla ohjelmointikielillä. Tässäkin tilanteessa varsinainen suorituksessa oleva ohjelma on JVM, joka lukee tavukoodia syötteenään.

Java-virtuaalikoneen (JVM) voi toteuttaa myös muilla tavoin kuin tulkitsemalla. Yksi tapa on tavukoodin kääntäminen suoraan järjestelmän omalle konekielelle. Toinen tapa on sellaisen suorittimen toteuttaminen, joka tavallisten konekäskyjen lisäksi osaa suorittaa myös tavukoodin käskyjä konekäskyinä. Emme käsittele näitä tapoja tällä kurssilla sen enempää.

Tulkittavia ohjelmointikieliä moititaan usein hitaiksi, koska niillä kirjoitettuja ohjelmia pitää suorittaa yksi käsky kerrallaan tulkin kautta. Tämä syö aika paljon laskentatehoa, kun jokaista tulkittavaa käskyä varten pitää suorittaa monta todellista konekäskyä. Se ei kuitenkaan välttämättä haittaa, koska tulkittavat ohjelmat voivat hyödyntää valmiiksi ohjelmoituja nopeita kirjastomoduuleita. Kirjastomoduulien koodi on puhdasta konekieltä, eikä sitä tarvitse tulkita. Jos pääosa ohjelman tekemästä laskennasta tapahtuu näissä nopeissa kirjastomoduuleissa, niin muiden osien hitaampi suoritus tulkitsemalla ei vaikuta kokonaisaikaan paljoakaan.

Vaikka järjestelmässä suoritettaisiin missä tahansa muodossa olevaa ohjelmaa, niin todellisuudessa ollaan aina suorittamassa jotain tietokoneen omaa konekielistä koodia. Keskitymme jatkossa tällaisen konekielisen ohjelman suoritukseen, oli ohjelma sitten vaikkapa käyttäjän itse tekemä sovellus, Python-tulkki tai Javan tavukoodia tulkitseva JVM. Huomiomme kohteena on jatkossa vain sellaiset ohjelmat, joiden esitysmuoto suoritusaikana on tietokoneen oma konekieli.

Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!