IT222 - Øving 2
Den raskeste lagringsstrukturen. Disse er det toppen noen få hundre av, og de er plassert i prosessoren. Hvert register kan for tiden maks inneholde 64 bits, det vanlige er 32 bits.
Det er kompilatorens/assemblerprogrammererens oppgave å opptimalisere bruken av disse.
Dette står mellom registre og minne i hastighet og lagringskapasitet, og brukes for å minske tiden prosessoren trenger på å hente ofte brukte data og instruksjoner, siden registre er særdeles mye kjappere enn minne.
Består av en rekke registre. Cache er svært dyrt å lage og derfor er det ikke så mye av det, mellom 256KB til 1MB.
Her mellomlagres instruksjoner og minne som utgjør et program som er under utførelse. Det er for lite til å romme alt en har behov for, og dessuten flyktig (RAM - Random Access Memory, ROM - Read Only Memory er ikke flyktig men enda dyrere å lage, og ikke så fleksibelt.) Ofte brukte deler kan kopieres til cacha for enda raskere tilgang.
Minnet deles opp av segmenter for spesialbruk, for eksempel lagrer MS-DOS BIOS i minnet, skjermbilde, i MS-DOS lagres et kopi av skjermbildet fra 640KB til 768KB for rask og smidig skjermoppdatering, dessuten var det ikke vanlig at skjermkortene hadde så fryktelig mye minne å snakke om, minneavbildet inn/ut-fil (memory-mapped IO) benytter minnet som ekstra kjapp harddisk-plass, ofte kallt RAM-disk, brukerprogram- og os-lager, det er dette det "alltid" blir for lite av, io-adresser, dette reduserer bruken av egne io-ordrer og avsatt for registrene til io-enhetene, slik at man kan bruke vanlige minneinstruksjoner for å jobbe mot io, og avbruddsvektorer for avbruddssignal, ibm-pc (MS-DOS) har disse fra 0-1KB.
Dette trenges fordi minnet er for lite til å oppbevare alt (dvs. det er for dyrt å ha nok minne til å kunne lagre alt), og fordi minnet er flyktig (upermanent, tømmes når strømmen går).
I masselageret oppbevares brukerprogram og -datafiler, os-et og eventuell virtuellt minne (hjelpelager/swap) når det er for lite RAM.
Typer masselager: magnetplatelager (winchester), adresseres via spor, sylindere og sektorer, informasjon om størrelse og oppdeling, og oppstartsprogrammet for os-et ligger i bootsektoren, innerst på platen. Har raskest aksesstid av masselagringstypene.Disketter holder maksimalt rundt 2.88MB, er treige og skjøre, og er ikke så mye brukt som de en gang var. Diskettens rolle er i ferd med å fullstendig bli tatt over av cd-rom, teknikken fungerer omtrent som med gamle lp-plater, men stiften er en laser (optisk, faseforandringer), eller eller kombinasjon av laser og magnetisk hode (magneto-optisk). Aksesstiden er dårlig, over hundre millisekunder.
Felles for alle typene er liten varighet, maks 30 år.
Det vanligste er fortsatt magnetbånd, disse kom på scenen alt i femti-årene. De er rimelige og har stor kapasitet, men har også verst aksesstid av alle lagringsmediene. Breddene 4mm (DAT) og 1/4" (QIC) er det mest brukte formatene for tiden. cd-rom tar innpå tape som backup-medium hvor mengdene ikke er så store.
Når arbeidsgangen til et program blir brutt grunnet en ytre begivenhet har man et avbrudd, avbruddsignalet informerer om hva som har skjedd. Når avbruddsignalet har blitt behandlet kan programmet fortsette der det var da det ble brutt. Man kan også filtrere vekk uønskede avbrudd. Avbruddene påvirker arbeidsgangen direkte, da evt. avbrudd må behandles før neste instruksjon kan hentes.
IO-avbrudd sendes når en io-enhet har igangsatt en operasjon, eller for å varsle om feil, maskinvareavbrudd (hardware interrupts) sier ifra når noe er galt med maskinvaren, programavbrudd skyldes program som har gjort noe ulovlig, for eksempel forsøkt å lese fra en beskytta del av minnet. Programmer kan forårsake slike avbrudd med vilje for å få operativ-systemet til å gjøre ting for seg. Tidsuret (timer) brukes bl.a av os-et for å holde styr på hvor lenge en prosess har kjørt.
Når avbruddet skal utføres, lagres først tilstanden (registre, flagg, tid brukt) før avbruddet kom, og så lastes avbruddsinstruksjonene inn. Når avbruddet er ferdigbehandlet, opprettes den opprinnelige tilstanden igjen. Avbrudd er prioriterte, dermed kan et avbrudd brytes av et annet avbrudd på vanlig måte.
Teknikker for lagring av tilstand: 1. Det avsettes plass for avbruddsvektorene, per type, i minnet, vanligvis de første adressene. En plass (avbruddsvektor) er for det nye registerinnholdet, en annen for det gamle. 2. vha. stakk. Ny tilstand lagres i minnet som for teknikk 1, men gammel tilstand legges på en stakk (eget minneområde). Kommer det et nytt avbrudd, legges forrige 'nye' tilstand på stakken og nåtidig nye tilstand i minnet. Dette gjør det enkelt å sjonglere et stort antall avbrudd.
En stakk er en LIFO (Last-In-First-Out) lagringsmetode. Man kan forestille seg en stakk som en stabel tallerkener, det er best å ta den tallerkenen som ligger øverst, ble lagt på sist, og ikke trekke ut en vilkårlig en midt i stabelen.
Adressen til siste innlagte data/tallerken lagres i en stakkpeker. Når ny data legges på, inkrementeres denne (forutsatt at stakken vokser oppover), når data hentes dekrementeres den. Stakkpekeren implementeres ofte vha. et eget stakkregister.
Rolle: se IV b.
En sikringsstruktur er et program eller en maskinvareenhet som overvåker og sikrer at ikke programmer gjør ting de ikke har lov til. Eksempler på dette er sikring mot feil ved bruk av I/O, f.eks. at to programmer ikke skriver til samme sted på disken samtidig, skriving til andre programmers minneområder, for stor bruk av ressurser m.v.
Denne sikringen sørger for at ikke et program spiser opp all CPU-tid i et fleroppgavekjørende miljø. Dette kan skje hvis programmer går i endless loops, eller om dårlig skrevne/ondsinnete programmer prøver å ta alt som finnes av ressurser. Et eksempel på sistnevnte er såkalte forkbomber, et enkelt program som kjører en endeløs rekke av forker (oppretter nye prosesser lik seg selv (UNIX)) og dermed sluker ressurser.
For å beskytte mot programmer som bruker mye CPU-tid kan har man timere, som sørger for å fordele tiden mellom prosessene som kjører. Dette hjelper ikke mot programmer som lager et uendelig antall nye prosesser, men dette kan man beskytte seg mot ved å begrense hvor mange prosesser en vanlig bruker kan starte.
De fleste datamaskiner har flere arbeidsmodus. Disse er i sin enkleste form brukermodus og priviligert modus, der noen instruksjoner kun er mulig å utføre i priviligert modus. For eksempel er alle I/O-instruksjoner priviligerte, slik at et brukerprogram må be operativsystemet om å få utføre I/O. Priviligerte funksjoner er generelt funksjoner som er viktige for systemet, så som instruksjoner som har med tilordning av minne, justering av klokke, m.v.
Noen systemer opererer også med flere modus, f.eks. 4 for VAX-arkitekturen og Intels x86-arkitektur.
Minnesikring går ut på å sikre for det første at brukerprogrammet ikke kan adressere og overskriver operativsystemets minneområder, og for det andre at programmer ikke adresserer andre programmers områder i multiprogrammeringsmiljøer. Brukerprogrammet vil da avsluttes med "protection fault" e.l. Det er også ønskelig å sikre minneområder innad i et programs minneområde. Programmer har gjerne et instruksjonsområde, et dataområde, og et område for arbeidsdata, og man ønsker da at instruksjoner kun skal kunne benyttes som instruksjoner, om data kun som data, og dessuten at dataområder som kun inneholder konstanter kun skal kunne benyttes ved lesing. Maskinvaren holder styr på hvor programmet kan aksessere minnet vha. grenseregistre, som forteller hvor minneområdene ligger. Prøver man å aksessere minne utenfor dette vil prosessoren gi styringen til operativsystemet, som vil terminere programmet.
Et eksempel på hvordan slik beskyttelse kan være nyttig er Sparc-prosessoren, som har mulighet for å beskytte mot eksekvering av stakken. Dette sikrer mot utnyttelse av sikkerhetshull, fordi programmer som utnytter sikkerhetshull gjerne gjør dette ved å kjøre data som ligger på stakken til et brukerprogram som kjører med root-access etter å ha overfylt en buffer. Ved å sperre for stack execution sikrer man mot slike programmer.
Et program er en samling programinstruksjoner. For hver utførelse av et enkelt program blir det opprettet en prosess. En prosess er en "utførelsesenhet". Enhver prosess har en oppføring i prosesstabellen, som er en samling av informasjon om alle prosessene som til enhver tid er under utføring. Når programmet er ferdig avsluttes prosessen og oppføringen fjernes fra tabellen.
I prosesstabellen er det informasjon som er unik for den enkelte prosess, hver prosess har et identifikasjonsfelt, pekere til minneområder, kopi av innholdet i registrene og et felt for den nåværende posisjon i programkoden.
Kilder: Tanenbaum, Staupe
Prosesser kan opprettes på forskjellig vis. Som regel har man slektskap mellom prosesser, ved at operativsystemet/kjernen starter den første prosessen, stamfaren. I UNIX heter denne prosessen init(8). Stamfaren kan så starte nye prosesser som blir dens barn. Disse prosessene kan igjen starte nye prosesser.
I UNIX blir prosesser skapt ved at den opprinnelige prosessen kaller fork();. fork oppretter en ny prosess som er en kopi av den gamle, med unntak av filhåndtak og signaler som ikke er plukket opp av morprosessen. Den nye prosessen får en ny prosess-ID og fortsetter fra akkurat samme utførelsespunkt som morprosessen. I morprosessen returnerer fork() barnets prosess-ID, mens barnet selv mottar 0.
Man ser altså at prosesser "arver" ved fødselen, omtrent som mennesker arver foreldrenes gener. Til forskjell til hva vi er vant med dør barna før foreldrene. Hvis en forelder dør vil også alle barnprosessene dens dø. For å unngå dette kan man i UNIX forke en egen prosess som forker enda en prosess (som skal overleve foreldrenes død), og deretter dør ved å bruke funksjonskallet _exit(n). Da vil alle barna til denne prosessen bli adoptert av stamfaren, dvs. INIT. Den opprinnelige prosessen fortsetter.
Andre operativsystemer kan ha andre måter å opprette nye prosesser på, i Windows NT f.eks. har ikke prosessene noe implisitt slektskap, og lever sine egne liv. I OS/2 og NT blir alle egenskaper arvet, også åpne filer og rør, men adresserommet blir initialisert med programkode som angitt i systemkallet.
En prosess kan avsluttes på forskjellig vis, enten ved at den er ferdig, at foreldreprosessen blir avsluttet, eller at operativsystemet eller brukeren avslutter prosessen, f.eks. ved å sende den et signal. En prosess kan også utføres ved feil, fx. hvis den forsøker å få tilgang til adresser utenfor sitt tildelte minneområde eller en rørforbindelse uventet forsvinner. Dersom prosessen bruker mer ressurser enn tillatt vil den også avsluttes, i UNIX er det fx. mulig å sette maksimalt minnebruk eller prosessortid.
Kilder: Tanenbaum, Staupe, fork(2), _exit(2)
Det er viktig å sikre gjensidig utelukkelse av prosesser for å unngå "race conditions" - det som skjer når to prosesser leser og skriver til delte minneområder samtidig. Race conditions kan få en hel del uønskede konsekvenser, som f.eks. at ingenting skjer når begge prosessene ønsker å skrive ut. (typisk eksempel)
Alle teknikker for å sikre gjensidig utelukkelse baserer seg på at programmene har en "kritisk region" som de går inn i når de skal bruke delte minneområder. Dette for gjøre det mulig for systemet å på en eller annen måte å kontrollere tilgangen til disse områdene.
En enkel måte å sikre gjensidig utelukkelse er å disable alle avbrudd når programmet entrer sin kritiske region. Dette har klare ulemper; ingen andre prosesser kan kjøre men en prosess er i sin kritiske region, og hvis prosessen ikke går ut av den kritiske regionen vil systemet rett og slett stoppe.
Dette introduserer en ny race condition; hva om prosessen går fra kjørende til ventende like etter at den har lest status på den delte låsevariabelen? Altså er dette en helt håpløs løsning.
Ideen her er at prosessene har hver sin "tur" i å være i kritisk region. Dette har også ulemper; hvis den ene prosessene er mye tregere enn den andre må en prosess stå og vente, selv om ingen annen prosess er inni sin kritiske region. Denne løsningen eliminerer muligheten for race conditions, men er ingen reell kandidat som løsning på grunn av ulempene.
Denne løsningen er en kombinasjon av ideen om at hver prosess har hver sin tur og låsevariabler. Ideen her er at en prosess som vil inn i kritisk region kaller en funksjon med sitt prosessnummer. Hvis ingen andre prosesser er i sin kritiske region returnerer denne funksjonen umiddelbart, og låser for andre prosesser. Når prosessen vil ut av kritisk region kaller den en annen funksjon, og andre funksjoner kan entre sin kritiske region. Ulempen med denne løsningen er at den har "busy waiting", en prosess som venter vil stå i loop og polle etter å få komme inn i kritisk region.
Denne løsningen baserer seg på at prosessoren har innebygget en funksjon Test and Set Lock, som gjør låsingen i en instruksjon, slik at den ikke kan bli avbrutt. Man utfører altså TSL-instruksjonen, deretter sammenligner man låse-registerer med det man prøvde å sette med TSL; er disse like var låsingen vellykket. Også her har man busy waiting som ulempe.
En prosess som er klar, kan bli hentet inn av OS'et når ingen andre prosesser er kjørende. Evt blir den hentet inn når det er nettopp denne prosessen sin tur å kjøre.
En prosess kan bli tvunget til å avgi CPU'en av OS'et. For eksempel kan en prosess ha brukt opp tidskvantumet det ble tildelt.
Hvis en kjørende prosess ønsker å bruke en ressurs, men må vente på en enhet, kan OS'et tvinge den til tilstand 'blokkert'.
Når en eventuell ressurs har blitt brukt av en prosess, og f.eks et tegn har blitt lest inn fra tastaturet, vil prosessen kunne gå fra blokkert til 'Klar'.
Hvis en prosess får behov for en ressurs, for eksempel input fra tastatur, vil vedkommede prosess gå fra tilstand 'kjørende' til 'blokkert' mens prosessen som henter inn data fra tastaturet går fra 'klar' til 'kjørende'.
2 medfører 1 - Hvis en prosess har brukt opp tiden sin og en ny prosess ligger og venter på CPU.
3 medfører 2 - Hm :)
4 medfører 1 - Den blokkerte prosessen har fått ressursen den ventet på og får fortsette fra klar til kjørende.
Ingenting vil skje hvis det ikke er andre prosesser. Evt, Hvis alle andre prosesser er blokkert eller klare.
Så lenge det ikke ligger andre klare prosesser som venter på CPU, vil ingen ting skje.
Så lenge ingen ting annet skal skje, må alle andre prosesser være blokkert.
Hvis ingen ting annet skal skje, må det være en annen kjørende prosess, som ikke kan blokkere.
Algoritmen skal være rettferdig slik at alle prosessene får sin del av CPU-tiden, uten at noen må vente unødvendig lenge. En effektiv algoritme sørger for at CPUen blir brukt oppimot 100% (ideelt tilfelle).
Interaktive brukere (brukere med skall) bør ha lav responstid, men også satsjobber bør bli gjennomført på kortest mulig tid. På toppen av det hele ønsker man å kunne kjøre flest mulig prosesser totalt sett.
Enklest mulig algoritme er en sirkulær kø. Hver prosess kjører til den har brukt opp sitt tildelte tidskvantum eller til den må vente på en ressurs, og kommer så bakerst i køen igjen.
En utvidelse av sirkulær kø får vi ved å innføre en ressurskø. Dermed kan flere prosesser vente på enheter samtidig, og CPUen kan brukes imens.
Ved å innføre prioriteter kan noen prosesser få fortrinnsrett fremfor andre. Dette gjør at interaktive prosesser kan "snike i køen" foran fx. en beregning, slik at responstiden for brukere senkes. En løsning er da å ha seperate køer for forskjellige nivåer, fx. for sanntid, interaktive og satsvise prosesser. Algoritmen må da itillegg fordele tiden mellom disse nivåene, som regel ved avbrudd (prosessene med høy prioritet venter på ressurser), men algoritmen må også passe på å heve prioriteten på de lavere prioriterte prosessene så de får kjørt.
En mer dynamisk køordning får man ved at prosessenes prioritet kan endres. Fx. kan en prosess få øket prioritet under inn/ut-operasjoner, mens prioriteten senkes ved beregninger. Prosessene vil da flyte opp og ned mellom køene.
For satsvise jobber kunne en køordning med den korteste jobben først være lurt, da må man totalt sett vente minst mulig på at jobbene skal bli ferdige, fordi en liten jobb kommer før en stor jobb slipper man å vente på en lang jobb for å komme igjennom med sin lille jobb. For den store jobben vil forskjellen i tid være liten i forhold til tiden den tar selv. Den samme teknikken kan man oppleve i handlesentra hvor kunden med fullastet vogn foran deg lar deg få gå til kassen før ham fordi du bare skal kjøpe et brød. For ham vil ventetiden endre seg lite, mens du slipper masse unødig venting. Et problem med denne løsningen er å anslå hvilken jobb som egentlig er kortest, slik at ordningen er best for satsvise jobber hvor man på forhånd vet størrelsen på jobbene, mens det blir verre for interaktive systemer.
En kan bruke garantert tildeling for CPU-kraften, ved at hver bruker av totalt n brukere garantert får 1/n deler av CPU-kraften. Brukeren som har fått minst andel av CPUen får CPUen inntil noen andre har fått minst.
I tillegg til den vanlige tildelingen kan en la prosessene selv få angi prioritet på sine barnprosesser, siden den gjerne vet best hva prosessene skal gjøre og hvor tidskritiske de forskjellige underdelene er. En slik prioritering blir også viktig når man innfører tråding. ("små underprosesser")
Tildelingsalgoritmen kan også ta hensyn til hvilke prosesser som er i primærminnet og hvilke som er delvis eller fullstendig swappet til sekundærlager, siden det tar mye tid å flytte kode og data imellom primær- og sekundærlager. Algoritmen kan fx. veksle mellom å ha prosessene A,B,C i minnet og D,E,F på disk, og la dem kjøre inntil det blir på tide å bytte ut med D,E,F i minnet og la dem kjøre istedet.
Tid (ms) | Kjørende | Klar høy prioritet | Klar lav prioritet | Ventende/blokkert magnetbånd inn/ut | Ventende/blokke |
---|---|---|---|---|---|
0 | 1 | 2,3 | - | - | - |
100 | 2 | 3,1 | - | - | - |
200 | 3 | 1,2 | - | - | - |
300 | 1 | 2,3 | - | - | - |
350 | 2 | 3 | - | 1 | - |
450 | 3 | 2 | - | 1 | - |
550 | 2 | 3 | - | 1 | - |
600 | 3 | - | - | 1 | 2 |
650 | - | - | - | 1,3 | 2 |
700 | 2 | - | - | 1,3 | - |
750 | - | - | - | 1,3 | 2 |
850 | 1 | - | - | 3 | - |
1000 | - | - | - | 3,1 | - |
1250 | 3 | - | - | 1 | - |
1500 | - | - | - | 1,3 | - |
1750 | 1 | - | - | 3 | - |
1900 | - | - | - | 3,1 | - |
2250 | - | - | - | 1 | - |
2750 | - | - | - | - | - |