Prosesser og tråder. Martin Gilje Jaatun. 30. april 2007



Like dokumenter
Kort notat om parallellstyring IN147

GetMutex(lock) { while(testandset(lock)) {} } En context switch kan ikke ødelegge siden testen og endringen av lock skjer i samme instruksjon.

Eksempler på ikke-blokkerende systemkall:

Lars Vidar Magnusson. October 11, Lars Vidar Magnusson () Forelesning i Operativsystemer October 11, / 28

Definisjon av prosess

Oppsummering av IN147 siste del Operativsystemer Parallellisering Veien videre

Faglig kontakt under eksamen: Orestis Gkorgkas

Eksamen DAT 103. Oppgave 2. Kandidatnr.: 145 1) B 2) B 3) A 4) A 5) D 6) C 7) B 8) A 9) A 10) D

INF1010 Tråder J. Marit Nybakken Motivasjon. Å lage en tråd

En prosess kan sees på som et stykke arbeid som skal utføres på datamaskinen. Ofte vil det være flere prosesser/tråder på datamaskinen samtidig.

oppgavesett 4 INF1060 H15 Øystein Dale Hans Petter Taugbøl Kragset September 22, 2015 Institutt for informatikk, UiO

UNIVERSITETET I OSLO

D: Ingen trykte eller håndskrevne hjelpemiddel tillatt. Bestemt, enkel kalkulator tillatt.

Forelesning III Kap 8 & 7; Dagsplan. Gjenbruk. Condition synchronization. Gjennomgående eksempler. Kode: Design: Verktøy

Tråder og concurrency i Linux

Løsningsforslag til eksamen i IN 147 og IN 147A

INF1010 Tråder II 6. april 2016

HØGSKOLEN I SØR-TRØNDELAG

Oppgave 1 - Linux kommandolinje (%)

D: Ingen trykte eller håndskrevne hjelpemiddel tillatt. Bestemt, enkel kalkulator tillatt.

2 Om statiske variable/konstanter og statiske metoder.

Stein Gjessing. Institutt for informatikk. Universitetet i Oslo. Institutt for informatikk

Prosesstabeller (PCB - Prosess Control Block) Se på PD: kommandoene ps og top Bakgrunnsprosesser Opprettelse av prosesser:

public static <returtype> navn_til_prosedyre(<parameter liste>) { // implementasjon av prosedyren

public static <returtype> navn_til_prosedyre(<parameter liste>) { // implementasjon av prosedyren

Skisse til løsning for eksamensoppgave i TDT4186 Operativsystemer

Enkle generiske klasser i Java

Programmering i C++ Løsningsforslag Eksamen høsten 2005

OPPGAVE 1 OBLIGATORISKE OPPGAVER (OBLIG 1) (1) Uten å selv implementere og kjøre koden under, hva skriver koden ut til konsollen?

Innhold. Introduksjon til parallelle datamaskiner. Ulike typer parallelle arkitekturer. Prinsipper for synkronisering av felles hukommelse

Slope-Intercept Formula

EKSAMEN. Operativsystemer. Kontroller at oppgaven er komplett før du begynner å besvare spørsmålene.

TDT4100 Objektorientert programmering

IN1010 våren Repetisjon av tråder. 15. mai 2018

Tråder Repetisjon. 9. og 13. mai Tråder

EKSAMEN. Operativsystemer. 1. Læreboken "A Practical Guide to Red Hat Linux" av Mark Sobell 2. Maks. tre A-4 ark med selvskrevne notater.

Operativsystemer og grensesnitt

INF Logikk og analysemetoder Forslag til løsning på oppgave fra læreboken

Scheduling og prosesshåndtering

IN 147 Program og maskinvare. Vranglås De spisende filosofer Lettvektsprosesser Moderne synkroniseringsmetoder Meldinger Monitorer Linda

Argumenter fra kommandolinjen

Betinget eksekvering og logiske tester i shell

oppgavesett 4 INF1060 H16 Hans Petter Taugbøl Kragset Øystein Dale Christian Resell 27. september 2016 Institutt for informatikk, UiO

Samtidige prosesser. Prosessor modus. Hvordan kan OS effektivt kontrollere brukerprosesser? Hvordan kan OS. kontrollere brukerprosesser?

Tråder Repetisjon. 9. og 13. mai Tråder

HØGSKOLEN I SØR-TRØNDELAG

Debugging. Tore Berg Hansen, TISIP

Concurrency. Lars Vidar Magnusson. September 20, Lars Vidar Magnusson () Forelesning i Operativsystemer September 20, / 17

Forelesning Instruksjonstyper Kap 5.5

Python: Løkker. TDT4110 IT Grunnkurs Professor Guttorm Sindre

Oversikt. Historie Struktur Moderne UNIX systemer Moderne UNIX kernel struktur 1 UNIX. 2 Linux. 3 Process. 4 Process models

1,r H øgs kolen i Østfol d

Hangman. Level. Introduksjon

Trådløsnett med Windows XP. Wireless network with Windows XP

Array&ArrayList Lagring Liste Klasseparametre Arrayliste Testing Lenkelister

TDT4258 Eksamen vår 2013

Tilkobling og Triggere

Sprettball Erfaren ComputerCraft PDF

INF1000 Metoder. Marit Nybakken 16. februar 2004

Trådløsnett med Windows Vista. Wireless network with Windows Vista

Norsk (English below): Guide til anbefalt måte å printe gjennom plotter (Akropolis)

Del 4 Noen spesielle C-elementer

INF Notater. Veronika Heimsbakk 10. juni 2012

Array&ArrayList Lagring Liste Klasseparametre Arrayliste Testing Lenkelister Videre

Hjemmeeksamen 2 i INF3110/4110

JavaScriptbibliotek. Introduksjon MVVC. Informasjonsteknologi 2. Gløer Olav Langslet Sandvika VGS

I et Java-program må programmøren lage og starte hver tråd som programmet bruker. Er dette korrekt? Velg ett alternativ

INF2810: Funksjonell Programmering. Strømmer og utsatt evaluering

HØGSKOLEN I BERGEN Avdeling for ingeniørutdanning Data

Fra sekvensielt til parallelt

Prosesser og Tråder. Ketil Danielsen January 16, en tråd arbeider sekvensielt gjennom program

Tildeling av minne til prosesser

Plan for dagen. Kræsj-kurs i sanntidsprogrammering. Måter å tenke på. Programmering intro. Tråder & synkronisering

2 Om statiske variable/konstanter og statiske metoder.

The regulation requires that everyone at NTNU shall have fire drills and fire prevention courses.

Singletasking OS. Device minne Skjerm minne. Brukerprogram. Brukerdata/heap. Stack. Basis for flerprosess-systemer.

INF1010, 22. mai Prøveeksamen (Eksamen 12. juni 2012) Stein Gjessing Inst. for Informatikk Universitetet i Oslo

Bli Kjent med Datamaskinen Introduksjon ComputerCraft PDF

Hvordan en prosessor arbeider, del 1

EKSAMENSOPPGAVE I FAG TDT4186 OPERATIVSYSTEMER. Faglig kontakt under eksamen: Svein Erik Bratsberg og Arvid Staupe

UNIVERSITETET I OSLO

Løsningsforslag til eksamen i INF103

Hva er en kø? En lineær datastruktur der vi til enhver tid kun har tilgang til elementet som ble lagt inn først

Mer om C programmering og cuncurrency

HØGSKOLEN I SØR-TRØNDELAG Avdeling for informatikk og e-læring

Side 1 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006

DAT kandidatnummer: 142

La oss begynne med en repetisjon av hva som skjer når du kjører Javaprogrammet

Programmeringsspråket C

IN 147 Program og maskinvare

Dagens tema: 12 gode råd for en kompilatorskriver

Trådløst nett UiT Feilsøking. Wireless network UiT Problem solving

KROPPEN LEDER STRØM. Sett en finger på hvert av kontaktpunktene på modellen. Da får du et lydsignal.

Du må håndtere disse hendelsene ved å implementere funksjonene init(), changeh(), changev() og escape(), som beskrevet nedenfor.

Litt om Javas class-filer og byte-kode

EKSAMEN I FAG TDT MMI Lørdag 11. august 2012 Tid: kl

Hvordan føre reiseregninger i Unit4 Business World Forfatter:

Løsningsforslag for TDT4186 Operativsystemer

Oppgave 1a Definer følgende begreper: Nøkkel, supernøkkel og funksjonell avhengighet.

Introduksjon til DARK assembly

Transkript:

Prosesser og tråder Martin Gilje Jaatun 30. april 2007 Sammendrag Dette dokumentet er et forsøk på å formidle noen velmenende formuleringer om prosesser og tråder, dvs. kapittel 2 i [Tan01a]. Dokumentet er for det meste basert på [Tan01b] og [Tan01a]. Det er forøvrig tidvis skjelt til [SGG02]. Den observante leser vil ha registrert at dokumentet er skrevet på norsk, og norske ord og uttrykk er derfor forsøkt brukt gjennomgående. Imidlertid er etablerte engelske forkortelser beholdt. Innhold 1 Prosesser 3 1.1 Fødsel og død............................... 4 1.2 Prosesshierarki.............................. 5 1.3 Prosesstilstander............................. 5 1.4 Avbruddshåndtering........................... 6 2 Tråder 7 2.1 Trådmodellen............................... 7 2.2 Bruk av tråder.............................. 9 2.3 Tråder i brukermodus.......................... 11 2.4 Tråder i kjernen............................. 11 2.5 Hybride løsninger............................. 12 2.6 Ikke bare Snurre kan Sprett(e)..................... 13 2.7 Fra enkle tråder til flere......................... 13 2.8 Eksempel: Lettvektsprosesser i Linux.................. 15 2.9 Eksempel: Pthreads........................... 16 3 Interprosess kommunikasjon 16 3.1 Inkonsistens er ikke et aldredomstegn.................. 17 3.2 Et delt buffer............................... 17 3.3 Det store kappløpet........................... 19 1

3.4 Kritiske regioner............................. 20 3.5 Aktive løsninger............................. 21 3.5.1 Skru av avbrudd......................... 21 3.5.2 Låse-variable........................... 21 3.5.3 Hver sin tørn........................... 22 3.5.4 Petersons aktive løsning..................... 22 3.5.5 TSL - test og lås......................... 24 3.6 Ikke-aktive løsninger........................... 25 3.6.1 Sov du vesle spire ung...................... 25 3.6.2 Semaforer............................. 25 3.6.3 Monitor.............................. 28 3.6.4 Meldingssending......................... 28 3.7 Barriærer................................. 29 4 Klassiske IPC-problemer 29 4.1 Spisende filosofer............................. 29 4.2 Lesere og skrivere............................. 33 4.3 Den sovende barberer i Sevilla...................... 33 5 Planlegging 34 5.1 Mål for planleggingsalgoritmer..................... 34 5.1.1 Mål for satsvise systemer.................... 39 5.1.2 Mål for interaktive systemer................... 39 5.1.3 Planleggingsmål for sanntidssystemer............. 39 5.2 Satsvis planlegging............................ 40 5.3 Interaktiv planlegging.......................... 40 5.4 Planlegging i sanntidssystemer..................... 41 5.5 Politikk kontra mekanisme........................ 42 5.6 Planlegging av tråder........................... 42 A Ordliste 43 Figurer 1 Modell for fire prosesser......................... 4 2 De tre tilstandene en prosess kan være i................ 5 3 Planleggeren og prosesser........................ 6 4 Utvalg av informasjon i ett innslag i prosesstabellen......... 6 5 Tre prosesser kontra tre tråder..................... 7 6 Hver tråd har sin egen stakk...................... 8 7 En tekstbehandler med tre tråder.................... 9 8 En flertrådet vev-tjener......................... 10 9 Skisse av koden for tjeneren i fig. 8................... 10 2

10 Tråder i brukermodus kontra tråder i kjernen............. 12 11 Multipleksing av brukermodus tråder over kjernetråder........ 13 12 Opprettelse av en ny tråd når en melding ankommer......... 14 13 To tråder som kommer i konflikt over en global variabel....... 14 14 Eksempel på tråder med private globale variable........... 15 15 Kode for produsent og konsument.................... 18 16 Et kappløp i skriverkøen......................... 19 17 Gjensidig utelukkelse med kritiske regioner.............. 20 18 Gjensidig utelukkelse ved at to prosesser tar hver sin tørn...... 22 19 Petersons aktive løsning......................... 23 20 Gjensidig utelukkelse med TSL..................... 24 21 Kode for produsent/konsument med sleep()............. 26 22 Produsent-konsument med semaforer.................. 27 23 Mutex-operasjoner implementert vha. TSL............... 28 24 Pseudokode for monitor......................... 29 25 Monitor-løsning på produsent-konsument............... 30 26 Produsent-konsument i Java....................... 31 27 Meldingsbasert løsning på produsent-konsument........... 32 28 Bruk av en barriære for å synkronisere fire prosesser......... 32 29 Flyfoto av de spisende filosofer..................... 33 30 En ikke-løsning på de spisende filosofer................. 34 31 Semafor-basert løsning på spisende filosofer.............. 35 32 En løsning på lesere og skrivere..................... 36 33 Den sovende barberer.......................... 37 34 En løsning på den sovende barberer med semaforer.......... 38 35 CPU-intensive og I/O-intensive prosesser............... 39 36 SJF-planlegging.............................. 40 37 Eksempel på tre-nivå planlegging.................... 40 38 Round-Robin............................... 41 39 Planlegging med prioritetskøer..................... 41 40 Planlegging av tråder i brukermodus kontra kjernemodus...... 42 Tabeller 1 Hva som deles mellom tråder...................... 8 2 Tre måter å konstruere en tjener på.................. 11 1 Prosesser Poenget med multiprogrammering var å kunne ha flere prosesser i minnet på en gang. På et multiprogrammert system vil disse prosessene tilsynelatende kjøre i pa- 3

One program counter A B C Process switch A Four program counters B C D Process D C B A D Time (a) (b) (c) Figur 1: Modell for fire prosesser rallell, men som det fremgår av fig. 1 er det kun en prosess som kan legge beslag på CPU-en i et gitt øyeblikk. Det er opp til operativsystemet å bytte mellom prosessene på en slik måte at fiksjonen om parallellitet blir opprettholdt. Hver enkelt prosess oppfatter seg selv som en sekvensiell kjede av instruksjoner som utføres fra begynnelse til slutt, og i de fleste tilfeller vil prosessen leve i den villfarelse at den råder grunnen alene på systemet. 1.1 Fødsel og død Prosesser blir opprettet og avsluttet kontinuerlig i et normalt system. Viktige hendelser som fører til at prosesser blir opprettet inkluderer: Oppstart av systemet Utførelse av et systemkall for å opprette en prosess Bruker ber om å opprette en ny prosess Starting av en satsvis jobb (batch job) Hva så med når man kjører et nytt program? Det som vanligvis skjer da er at (f.eks.) skallet utfører et systemkall for å opprette en ny prosess, og deretter utføres en systemkall som bytter ut prosessinformasjonen (process image) til den nyopprettede prosessen med noe tilsvarende programmet som skal kjøres. Som nevnt termineres også prosesser, enten på eget initiativ (frivillig) eller ufrivillig. Frivillige termineringer skjer ved normal avslutning (exit) eller feil-avslutning, mens ufrivillig avslutning inntreffer enten ved at en fatal feil oppstår (f.eks. segmenteringsfeil) eller ved at prosessen blir drept av en annen prosess. 4

1 3 2 Blocked Running 4 Ready 1. Process blocks for input 2. Scheduler picks another process 3. Scheduler picks this process 4. Input becomes available 1.2 Prosesshierarki Figur 2: De tre tilstandene en prosess kan være i I Unix er det slik at prosesser kan opprette nye prosesser; de sistnevnte kaller vi gjerne barneprosesser (child processes). Barneprosessene kan på sin side opprette egne barneprosesser, osv. På denne måten får vi et hierarki av prosesser; i Unix kalt en prosess-gruppe. I slike tilfeller er ofte prosessene smertelig klar over at det finnes andre prosesser, og en del av de problemer som dette kan medføre er diskutert i avsnitt 3. I Windows-verdenen opererer man ikke med prosess-hierarkier; her er alle prosesser skapt like. Imidlertid er det slik at Windows 2000 (og XP) i mye større grad forholder seg til tråder bl.a. ved planlegging (scheduling), noe som gjør at en direkte sammenligning ikke blir helt rettferdig. 1.3 Prosesstilstander En prosess kan være i tre forskjellige tilstander: Kjørende, blokkert eller klar. Overganger mellom disse tilstandende er vist i fig. 2. Prosessen er i kjørende (running) tilstand når den faktisk bruker prosessoren, og i et en-prosessor system er det følgelig bare en prosess som kan være i kjørende tilstand om gangen. Hvis en prosess forsøker å utføre en I/O-operasjon som ikke umiddelbart kan tilfredsstilles, vil prosessen gå over i blokkert tilstand (i praksis vil dette stort sett gjelde alle I/O-anmodninger, siden I/O-enheter er så fryktelig mye tregere enn ting som CPU og minne). Når planleggeren finner ut at en kjørende prosess har kjørt lenge nok, vil den velge en annen prosess til å være kjørende, og den førstnevnte prosessen vil gå fra å være kjørende til å være klar. Mange prosesser kan være i klar tilstand samtidig, så planleggeren vedlikeholder en eller annen form for kø for å holde styr på dem. Når planleggeren på et senere tidspunkt bestemmer seg for at prosessen som er i klar tilstand skal få kjøre igjen, vil den gå over til kjørende tilstand. Til slutt, når en prosess som er blokkert fordi den venter på I/O får tilfredsstilt anmodningen sin (f.eks. dataene den har bedt om fra disken er tilgjengelige), vil den gå fra blokkert tilstand til klar tilstand, og dermed bli med i lotteriet om å få kjøre på prosessoren igjen. 5

Processes 0 1 n 2 n 1 Scheduler Figur 3: Planleggeren og prosesser Process management Memory management File management Registers Pointer to text segment Root directory Program counter Pointer to data segment Working directory Program status word Pointer to stack segment File descriptors Stack pointer User ID Process state Group ID Priority Scheduling parameters Process ID Parent process Process group Signals Time when process started CPU time used Children s CPU time Time of next alarm Figur 4: Utvalg av informasjon i ett innslag i prosesstabellen Som det fremgår av fig. 3, representerer planleggeren det laveste nivået av det prosess-strukturerte OS. På dette nivået håndteres avbrudd og planlegging av prosesser; over dette nivået har vi konseptuelt sekvensielle prosesser. En prosess er egentlig definert av informasjonen som OS-kjernen vedlikeholder om den. Kjernen har en prosess-tabell med ett innslag per prosess. Hvert innslag inneholder en mengde informasjon, som illustrert i fig. 4. 1.4 Avbruddshåndtering Når et avbrudd inntreffer, vil det laveste nivået av operativsystemet grovt sett gjøre følgende: 1. Maskinvare stapper programteller 1 etc. på stakken 1 Også kjent som instruksjonspekeren 6

Process 1 Process 1 Process 1 Process User space Thread Thread Kernel space Kernel (a) Kernel (b) Figur 5: Tre prosesser kontra tre tråder 2. Maskinvare laster ny programteller fra avbruddsvektor (for at CPU-en skal vite hvor den skal begynne å kjøre avbruddshåndtereren) 3. Assembly-rutine lagrer registre 4. Assembly-rutine setter opp ny stakk 5. C avbruddshåndterer-rutine (interrupt service routine) kjører (vil typisk lese og bufre inndata) 6. Planleggeren (scheduler) bestemmer hvilken prosess sin tur det er til å kjøre nå 7. C-funksjonen returnerer til assembly-koden 8. Assembly-rutinen starter opp prosessen som planleggeren valgte 2 Tråder Navnet tråd (thread) har utspring i samme begrep som å holde tråden i en historie. På mange måter kan vi se på en tråd som en lettvekts-prosess; flere tråder deler samme adresserom innen en prosess, men kjører likevel uavhengig av hverandre. 2.1 Trådmodellen Selv når man ikke forholder seg til tråder, dvs. når hver prosess bare gjør en ting, sier vi likevel at prosessen har en tråd den er enkelt-trådet (single-threaded). Dette illustreres i fig. 5 a). Når en prosess inneholder flere tråder, kaller vi den for fler-trådet (multithreaded), og det er først da vi begynner å snakke om trådprogrammering. 7

Delt for hele prosessen Adresserom Globale variable Åpne filer Barneprosesser Ventende alarmer Signaler og signalhåndterere Bokholderi-informasjon Lokal for hver tråd Programteller Registre Stakk Tilstand Tabell 1: Hva som deles mellom tråder Thread 2 Thread 1 Thread 3 Process Thread 1's stack Thread 3's stack Kernel Figur 6: Hver tråd har sin egen stakk For at det skal være noe poeng med tråder, må de ha noen fortrinn fremfor prosesser; dette fortrinnet er pris. Tråder er billigere enn prosesser, fordi de deler ressurser i mye større grad enn det prosesser gjør. I tabell 1 vises det en liten oversikt over hvilke datastrukturer som er felles (delt) for alle tråder i en prosess, og hvilke som er private for hver enkelt tråd. Naturlig nok må hver tråd ha sin egen programteller, siden det er denne som angir hvilken instruksjon som skal utføres neste gang. Videre må hver tråd også ha sin egen stakk (se fig. 6)- ellers ville bl.a. ikke tråder kunne kalle og returnere fra funksjoner. Tråder vil også ha sin egen tilstand, noe som er nødvendig dersom en tråd f.eks. skal kunne blokkere for I/O uten å samtidig blokkere alle de andre trådene i prosessen. 8

Four score and seven nation, or any nation lives that this nation who struggled here here to the unfinished they gave the last full years ago, our fathers so conceived and so might live. It is have consecrated it, far work which they who measure of devotion, brought forth upon this dedicated, can long altogether fitting and above our poor power fought here have thus that we here highly continent a new nation: endure. We are met on proper that we should to add or detract. The far so nobly advanced. resolve that these dead conceived in liberty, a great battlefield of do this. world will little note, It is rather for us to be shall not have died in and dedicated to the that war. But, in a larger sense, nor long remember, here dedicated to the vain that this nation, proposition that all We have come to we cannot dedicate, we what we say here, but great task remaining under God, shall have men are created equal. dedicate a portion of cannot consecrate we it can never forget before us, that from a new birth of freedom Now we are engaged that field as a final cannot hallow this what they did here. these honored dead we and that government of in a great civil war resting place for those ground. The brave It is for us the living, take increased devotion the people by the testing whether that who here gave their men, living and dead, rather, to be dedicated to that cause for which people, for the people Keyboard Kernel Disk Figur 7: En tekstbehandler med tre tråder 2.2 Bruk av tråder Vi bruker tråder i situasjoner hvor vi har bruk for parallellitet, men der man har behov for massiv tilgang til delte data 2, eller ikke ønsker å ta kostnaden med å opprette fullverdige prosesser. Et eksempel på bruk av tråder kan være i en tekstbehandler som vist i fig. 7. Her har man en tråd som tar imot inndata fra brukeren (dvs. tegn som brukeren taster på tastaturet), en (bakgrunns-)tråd som automatisk re-formatterer dokumentet men brukeren skriver, og n (bakgrunns-)tråd som periodevis lagrer dokumentet til disk (sikkerhetskopiering). Reformatteringen er nødvendig når brukeren gjør endringer midt inne i et dokument; når det f.eks. legges til en linje på side 52, må vanligvis også side 53, 54, 55,... osv. forskyves en linje. Dersom denne reformatteringen skulle utsettes til brukeren ber om å få se f.eks. side 145, ville det bli en uakseptabel forsinkelse. På tilsvarende måte vil en egen sikkerhetskopieringstråd gjøre det mulig for brukeren å fortsatt taste inn tekst selv om tekstbehandleren skriver til disk 3 En annen aktuell problemstilling er en vev-tjener som vist i fig. 8. Her har vi en tråd som vi kan kalle arbeidsleder (dispatcher) som tar imot anmodninger fra nettlesere via nettet, og et sett av arbeider-tråder som gjør selve jobben. Arbeidslederen velger 4 2 Husk imidlertid at det å bruke tråder ikke gjør at vi slipper unna koordinerings-problemene beskrevet senere snarere tvert imot. 3 Dette er forøvrig en ting som irriterer meg med StarOffice her blokkeres inndata fra tastaturet så lenge lagringen pågår - dette kan ta flere sekunder. 4 Vi kunne forsåvidt gjerne sett for oss at arbeidertrådene kunne opprettes ved behov, men hvis vi er fornøyd med et fast antall arbeidere, er det en liten tids-besparelse i å slippe å opprette tråden for hver anmodning 9

Web server process Dispatcher thread Worker thread User space Web page cache Kernel Kernel space Network connection Figur 8: En flertrådet vev-tjener while (TRUE) { while (TRUE) { get_ next_ request(&buf); wait_ for_ work(&buf) handoff_ work(&buf); look_ for_ page_ in_ cache(&buf, &page); if (page_not_in_cache(&page)) read_page_from_disk(&buf, &page); return_page(&page); (a) (b) Figur 9: Skisse av koden for tjeneren i fig. 8 en ledig arbeider-tråd (worker thread) til å håndtere anmodningen (typisk en HTTP GET), og går så umiddelbart tilbake til å vente på nye anmodninger. Vi antar at de ledige arbeiderne er blokkert, og at arbeidslederen vekker dem opp når den har en oppgave for dem. Når en arbeider-tråd våkner, vil den først sjekke om siden som er bedt om finnes i hurtiglageret (cache). I så fall kan anmodningen tilfredsstilles umiddelbart; i motsatt fall må arbeideren ut på disk for å hente siden. Den vil da blokkere for I/O, og en annen tråd (muligens arbeidslederen) vil få kjøre. En grov skisse av hvordan koden for en slik tjener kunne se ut er vist i fig. 9 a) er arbeidslederen, og b) er arbeideren(e). 10

Modell Tråder Enkelt-trædde prosesser Tilstandsmaskin (FSM) Karakteristsikker Parallellitet, blokkerende systemkall Ingen parallellitet, blokkerende systemkall Parallellitet, ikke-blokkerende systemkall, avbrudd Tabell 2: Tre måter å konstruere en tjener på 2.3 Tråder i brukermodus Vi kan på dette tidspunkt kanskje være enige om at tråder er kjekt å ha, men det gjenstår å finne ut hvor man skal implementere dem i kjernen eller ikke? Fordelen med å implementere tråder i brukermodus (user space) (fig. 10 a)) er at vi da kan tilby tråder også i operativsystemer som i utgangspunktet ikke støtter dem. Dette forutsetter at man i hver prosess må ha et kjøresystem (run-time system) som administrerer trådene og i praksis bli et slags grensesnitt mellom hver tråd og operativsystemet. Kjøresystemet må bl.a. administrere en tråd-tabell med oversikt over alle tråder i prosessen, med disses tilhørende private verdier. En annen fordel er at kontekst-skifter mellom tråder i en prosess kan utføres fullstendig lokalt til prosessen, uten at man trenger å gå over til kjerne-modus. Dette medfører at brukermodus-tråder generelt gir høyere ytelse enn kjerne-tråder. For at brukermodus-tråder skal fungere, kreves det bl.a. bruk av spesielle biblioteker (f.eks. Pthreads), og at hver tråd passer på å spille etter reglene i enkelte tilfeller vil det være mulig for uartige tråder å blokkere hele prosessen, og da faller jo poenget med multitråding bort. Dette betyr at enten må trådene bare bruke spesielle tråd-sikre (thread-safe) systemkall, eller så må eksisterende systemkall endres for å gjøre dem tråd-sikre. 2.4 Tråder i kjernen Når tråder implementeres som en del av kjernen som i fig. 10 b), trenger man ikke lenger noe kjøresystem i hver prosess; kjernen vedlikeholder selv trådtabeller etc. på samme måte som den håndterer prosesstabellen. Mens kjernen tidligere planla kun per prosess, kan den nå velge å kjøre tråder fra forskjellige prosesser i vilkårlig rekkefølge. I Windows 2000/XP bruker man kjerne-tråder, og faktisk er tråder det man primært forholder seg til når det gjelder ting som planlegging (primary execution unit). Også brukerprogrammer forholder seg til tråder; det finnes ingen fork()-kommando under Windows, men i stedet en myriade av forskjellige varianter av systemkallet CreateThread. 11

Process Thread Process Thread User space Kernel space Kernel Kernel Run-time system Thread table Process table Process table Thread table Figur 10: Tråder i brukermodus kontra tråder i kjernen 2.5 Hybride løsninger Som i mange andre situasjoner i livet, finnes det mange mellomløsninger på problemet med hvor man skal plassere trådene. Det er mulig å multiplekse et antall tråder i en prosess inn i en kjernetråd som vist i fig. 11. Her danner man seg et slags tråd-hierarki, men denne typen krumspring gjør nok ikke koden mer oversiktlig for programmererene. En annen hybrid tilnærming er det som i boken kalles planlegger-aktivering (scheduler activation). Poenget er å tilnærme seg funksjonaliteten til kjernetråder, men uten å miste ytelsen til brukermodus-tråder. Dette gjør man ved å unngå unødvendige overganger mellom bruker- og kjernemodus når en tråd utfører et blokkerende systemkall til tross for at man ikke forbyr bruk av ordinære systemkall. Når man bruker planlegger-aktivering, vil kjernen tilordne et et visst antall virtuelle prosessorer til hver prosess - en prosess kan be om å få tildelt nye prosessorer ved behov. Når en tråd utfører et blokkerende kall vil kjernen informere prosessens kjøresystem om hva som har skjedd, og dermed er det den virtuelle prosessoren som blokkeres, ikke hele prosessen. Når kjernen detekterer at tråden kan kjøre videre (dvs. data som den venter på er tilgjengelig e.l.), informerer den prosessen (eller altså kjøresystemet) om dette vha. et såkalt upcall. Problemet med planlegger-aktivering er at det krever relativt drastiske inngripen i operativsystemet, og operativsystemer som i utgangspunktet ikke støtter tråder vil ikke ha noe håp om å kunne brukes her. Videre representerer denne upcall - funksjonen et brudd på det fundamentale lagdelingsprinsippet om at funksjoner på høyere nivå kaller funksjoner på lavere nivå, men ikke omvendt. 12

Multiple user threads on a kernel thread User space Kernel Kernel thread Kernel space Figur 11: Multipleksing av brukermodus tråder over kjernetråder 2.6 Ikke bare Snurre kan Sprett(e) Tråder er ofte nyttige i distribuerte systemer, og fig. 12 viser hvordan en prosess kan opprette en ny tråd for å behandle en innkommende melding, ikke ulikt det tidligere eksempelet med en vev-tjener. I figuren er a) ment å være systemet før meldingen ankommer, mens b) viser systemet etter at meldingen er mottatt, og den nye tråden er opprettet. Mange vil se likhetstrekk mellom denne opp-sprettingen og tradisjonell gafling (forking) av prosesser i forbindelse med tjenere som startes av (x)inetd i Unix/Linux. Forskjellen er imidlertid at det som nevnt er betydelig billigere å opprette nye tråder enn fullstendige prosesser. Husk imidlertid at det er mye dårligere separasjon mellom tråder enn prosesser, slik at denne angrepsvinklingen ikke er anvendbar i situasjoner hvor sikkerhetsbetraktninger som konfidensialitet kan være aktuelle. 2.7 Fra enkle tråder til flere Mange (de fleste?) programmer har tradisjonelt vært skrevet for enkelt-trædde prosesser. Hvis man ønsker å ta et slikt program 5 og tvinge det (sparkende og hylende?) inn i den multi-trædde verden, er dette ingen triviell (og slettes ikke automatiserbar) oppgave. Et eksempel på et problem man kan få med multi-trædde programmer er relatert til bruken av globale variable. I fig. 13 ser vi set tilfelle hvor to tråder i samme 5 Dette blir ofte kalt legacy code, ettersom det valigvis er noe man har arvet eller dratt med seg fra før tidenes morgen. Et godt eksempel er de prehistoriske COBOL-programmene på store jern fra IBM som visstnok fremdeles kjører i de fleste av våre store banker 13

Process Existing thread Pop-up thread created to handle incoming message Incoming message (a) Network (b) Figur 12: Opprettelse av en ny tråd når en melding ankommer Thread 1 Thread 2 Time Access (errno set) Open (errno overwritten) Errno inspected Figur 13: To tråder som kommer i konflikt over en global variabel 14

Thread 1's code Thread 2's code Thread 1's stack Thread 2's stack Thread 1's globals Thread 2's globals Figur 14: Eksempel på tråder med private globale variable prosess begge utfører operasjoner som påvirker den globale variabelen errno. Den første tråden utfører operasjonen Access(), som medfører at errno blir satt til en gitt verdi (avhengig av operasjonen er vellykket eller ei). Rett etterpå blir tråden avbrutt, og den andre tråden får kjøre. Den andre tråden forsøker å åpne en fil, og dette resulterer tilsvarende at errno blir satt dvs. verdien som var der fra før blir overskrevet. Deretter blir det den første tråden sin tur igjen, men når den inspiserer verdien av errno for å finne ut om den tidligere operasjonen var vellykket, er det resultatet av den andre trådens systemkall den leser resultatet er kaos. For å forsøke å motvirke problemet i fig. 13 foreslår [Tan01a] at man kan la tråder få ha private globale variable slik som illustrert i fig. 14. Jeg tillater meg å stille meg litt skeptisk til denne løsningen det blir jo enda en spesialløsning, og bryter med tråd-paradigmet 6 slik vi har definert det. 2.8 Eksempel: Lettvektsprosesser i Linux I Linux (og sikkert også mange andre Unix-avarter) har man i utgangspunktet ikke hatt støtte for tråder 7, hverken i kjernen eller andre steder. Imidlertid har vi i Linux det vi kan kalle for lettvekstprosesser (lightweight processes) som på mange måter kan oppføre seg som tråder. Disse opprettes i Linux med systemkallet clone(). Lettvekstprosesser er akkurat som andre prosesser 8, men innslagene i prosesstabellen peker for det meste på data tilhørende en annen prosess. Dette betyr at 6 Dette blir det nærmeste vi kommer uttrykket paradigmeskifte i dette dokumentet. 7 Vi ser her vekk fra de forskjellige kule mer eller mindre eksperimentelle multithreaded kernels 8 i motsetning til Lotto-prosesser 15

lettvektsprosesser er billige å opprette, ettersom vi ikke trenger å kopiere inn prosessinformasjonen. Forøvrig er det slik at alle prosesser som startes opp med fork() begynner livet med en eksakt kopi av mor-prosessens adresserom. Herfra er veien kort til å angi at prosesser skal bruke copy on demand (eller copy on write) når de opprettes da må man ikke bruke mye energi akkurat når man skal opprette prosessen, men bare når man faktisk har behov for å gjøre noen endringer. Her er det heller ikke vanskelig å la seg overbevise om at slike lettvektsprosesser er et funn når man skal bruke execve() o.l. da er det jo bortkastet å bruke masse tid og penger 9 for å befolke prosesstabellen, når alt sammen likevel skal byttes ut med verdiene til programmet man skal starte! 2.9 Eksempel: Pthreads Pthreads er et eksempel på en programpakke for brukermodus-tråder. P-en i Pthreads står for POSIX, som vi allerede har stiftet bekjentskap med i forbindelse med systemkall. For å sitere [NBF96], er Pthreads en standardisert måte å dele et program inn i underoppgaver som kan flettes eller parallelliseres. For en programmerer er Pthreads et sett med typedefinisjoner (f.eks. pthread.h) og et sett med funksjoner (som f.eks. kan representeres av biblioteket libpthread.so). Som et banalt eksempel kan vi ta et tenkt Pthreads-program kalt hello.c, som kunne begynne omtrent slik: #include <pthread.h> pthread_t t; void foo(int bar) {... Dette kompileres i Linux med kommandoen cc -lpthread -o hello hello.c 3 Interprosess kommunikasjon Både prosesser og tråder kan være vanskelige nok å forholde seg til når de lever sine egne liv, men når disse i tillegg skal interagere på forskjellig vis med andre prosesser 9 prosessortid er penger... 16

og tråder kan det bli direkte utfordrende. Vi bruker samlebegrepet interprosess kommunikasjon om dette fenomenet, enten det er prosesser eller tråder det er snakk om. 3.1 Inkonsistens er ikke et aldredomstegn Samtidig tilgang til delte data fra paralle prosesser kan føre til inkonsistens, som vi allerede har sett i fig. 13. Dette kan man forhindre ved å benytte mekanismer som sikrer at samarbeidende (del-)prosesser utføres i tur og orden. 3.2 Et delt buffer Dette eksempelet er hentet fra [SGG02]. I fig. 15 vises skisse til kode for en såkalt produsent-konsument-situasjon som er ganske vanlig dataverdenen en prosess (produsenten) genererer data som en annen prosess (konsumenten) skal lese. Problemet med koden i fig. 15 er at uttrykkene counter++ og counter-- må utføres atomisk, dvs. at operasjonen må fulføres fullstendig uten avbrudd. Her kan man bli forledet til å tro at dette er trivielt, ettersom det er en enkelt instruksjon det er snakk om. Imidlertid kan uttrykket counter++ være implementert i maskinspråk 10 som følger: register1 = counter register1 = register1 +1 counter = register1 Hvis både produsenten og konsumenten prøver å endre bufferet samtidig, kan assemblyutsagnene bli flettet inn i hverandre. Flettingen avhenger av hvordan prosessene får kjøre av planleggeren. Hvis vi f.eks. antar at variabelen counter har verdien 5, kan vi risikere å oppleve følgende sekvens: producer: register1 = counter (register1 = 5) producer: register1 = register1+1 (register1=6) consumer: register2 = counter (register2 = 5) consumer: register2 = register2-1 (register2=4) producer: counter = register1 (counter= 6) consumer: counter = register2 (counter = 4) 10 Jeg kan knapt hevdes å være noen assembly-guru, så alle assembly/maskinspråk-eksempler må betraktes som mer eller mindre pseudokode - i dette tilfellet kanskje enda mer enn andre steder. Det finnes bl.a ikke noe = i assembly; register1 = counter skulle vel strengt tatt vært noe som minner mer om mov counter register1, men hovedpoenget er at det skjer i en instruksjonssyklus... vi får evt. skylde på [SGG02] dersom dette blir for forvirrende. 17

#define BUFFER_SIZE 10 typedef struct {... item; item buffer[buffer_size]; int in = 0; int out = 0; int counter = 0; producer() { item nextproduced; while (1) { while (counter == BUFFER_SIZE) ; /* do nothing */ buffer[in] = nextproduced; in = (in + 1) % BUFFER_SIZE; counter++; consumer() { item nextconsumed; while (1) { while (counter == 0) ; /* do nothing */ nextconsumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; counter--; Figur 15: Kode for produsent og konsument 18

Spooler directory Process A Process B 4 5 6 7 abc prog.c prog.n out = 4 in = 7 Figur 16: Et kappløp i skriverkøen Verdien av counter kan være enten 4 eller 6; rett svar skulle være 5. 3.3 Det store kappløpet Det vi nettopp har sett et eksempel på kaller vi kappløp-tilstander (race conditions), og er generelt noe som kan inntreffe når flere prosesser aksesserer og manipulerer delte data samtidig. Som vi har sett, avhenger den endelige verdien av dataene av hvem hvilken prosess som avslutter sist. For å unngå kappløp-tilstander må parallelle prosesser synkroniseres. Nok et eksempel på kappløp illustreres i fig. 16. Her er det to prosesser som begge ønsker å skrive ut et dokument, og som derfor må skrive til katalogen som inneholder skriverkøen (spool directory). Anta at katalogen er implementert som en tabell med plass til et fast antall filnavn, og at det finnes to variable in og out som peker på henholdsvis første ledige plass og fronten av køen. Når en prosess skal skrive ut en fil, vil den se på in, skrive navnet på filen inn i den korresponderende plassen i tabellen, og deretter øke in (f.eks. fra 7 til 8). Det som kan skje, er at de to prosessene A og B ønsker å skrive ut på samme tidspunkt. Anta at prosess A kjører først, og inspiserer in og ser at den har verdien 7. Før A får gjort noe mer, blir den imiflertid avbrutt, og planleggeren bestemmer at det er prosess B sin tur til å kjøre. Prosess B ser også på in, og ser at den fremdeles er 7. Prosess B skriver sitt filnavn inn i plass 7 i tabellen, og øker deretter in til 8. Etter hvert blir det A sin tur til å kjøre igjen, og den fortsetter der den slapp. A har allerede sjekket in, og vet at den er 7. A skriver sitt filnavn inn i plass 7, og setter in til 8 11. Resultatet er at begge prosessene tror de har skrevet ut filen sin, 11 Den observante leser vil ha merket seg at dette fort kan bli implementasjonsavhengig. Slik det er beskrevet i boka, vil prosessene skrive den verdien de oppfatter at in er pluss en til variabelen. Et annet 19

A enters critical region A leaves critical region Process A B attempts to enter critical region B enters critical region B leaves critical region Process B B blocked T 1 T 2 T 3 T 4 Time Figur 17: Gjensidig utelukkelse med kritiske regioner men bare filen til prosess A kommer faktisk til å bli skrevet ut. 3.4 Kritiske regioner Dette er et generelt problem som vi støter på i mange sammenhenger: Et antall (n) prosesser kappes alle om å bruke noen delte ressurser (data). Hver prosess har et kritisk kodesegment (critical section eller critical region) hvor de delte dataene aksesseres. For å unngå problemer, må følgende være oppfylt: Hvis en prosess kjører kode som inngår i dens kritiske segment (eller region), må ingen andre prosesser samtidig få lov til å kjøre kode i sine kritiske segmenter. Det ovenstående er en nødvendig, men ikke tilstrekkelig forutsetning for en god løsning. I tillegg må følgende forutsetninger være oppfylt: Man kan ikke gjøre noen antagelser om hastigheter til de forskjellige prosessene eller antall prosessorer Ingen prosess som ikke kjører i sin kritiske region kan blokkere noen annen prosess Ingen prosess må vente uendelig lenge på å gå inn i sin kritiske region Hvordan dette skal fungere, er illustrert i fig. 17. Våre venner prosessene A og B deler noen ressurser. Prosess A er først ute, og går inni sin kritiske region uten problemer alternativ kunne være at begge utfører en inkrementeringsoperasjon på variabelen, dvs. in++. I dette tilfellet ville man kunne risikere at begge prosessene skriver sine filnavn inn i samme posisjon, men siden begge inkrementerer in, vil vi få et tomt innslag i tabellen. 20

ved tidspunktt 1. Ved tidspunktt 2 ønsker så B å gå inn i sin kritiske region, men ettersom A fremdeles er inne i sin, blir B blokkert. Ved tidspunktt 3 er A ferdig med sin kritiske region, så B vekkes opp, og kan gå inn. Til slutt ved tidspunktt 4 er B ferdig med sin kritiske region, går ut, og alt er tilbake til utgangssituasjonen. 3.5 Aktive løsninger I [Tan01a] beskrives flere mer eller mindre vellykkede forsøk på løsning av kritiskregion-problemet. De første er de som involverer aktiv venting, dvs. at prosessene ikke gir fra seg prosessoren frivillig, men aktivt sjekker om forutsetningene for å gå inn i den kritiske regionen er til stede. 3.5.1 Skru av avbrudd Det første løsningsforslaget er å skru av avbrudd - hvis prosessene ikke kan bli avbrutt, kan de heller ikke risikere å bli ja, akkurat avbrutt midt inne i den kritiske regionen. Akkurat som i det politiske liv er ikke enkle løsninger alltid å foretrekke. Å skru av avbrudd vil absolutt forhindre at flere enn en prosess er inne i sin kritiske region om gangen, men dette er ikke en farbar vei på et modeerne multiprosessor-system. Ett problem er at når man skrur av avbrudd, vil man måtte stole på at prosessen som kjører faktisk skrur dem på igjen når den er ute av den kritiske regionen; hvis det skulle oppstå en feil, og avbruddene aldri blir skrudd på, vil aldri noen annen prosess bli startet, og systemet avgår ved døden. En annen ting er at ved å skru av avbrudd vil man påvirke alle andre prosesser i systemet, ikke bare de som har en kritisk region relatert til vår. Til slutt: Å skru av avbrudd fungerer bare på prosessoren vi gjør det på, så hvis det er flere prosessorer, vil prosesser som kjører på disse kunne fortsette å skape problemer med delte ressurser. 3.5.2 Låse-variable Neste forsøk involverer delte variable for å låse regionen. F.eks. kan vi definere variablen ledig slik at hvis den har verdien 1, kan en prosess gå inn i regionen; hvis den er 0 må den vente. En prosess som går inn i regionen må følgelig umiddelbart sette ledig til 0, og sette den tilbake til 1 når den er ferdig. Ulykkeligvis lider denne løsningen av akkurat det samme kappløp-problemet som vi har sett tidligere. Hvis prosess A ser at verdien er 1, men blir avbrutt av B før den får gjort noe mer, kan B også se at verdien er 1, og vips er begge prosessene inne i sin kritiske region samtidig. 21

while (TRUE) { while (TRUE) { while (turn!= 0) / * loop * / ; while (turn!= 1) critical_ region( ); critical_ region( ); / * loop * / ; turn = 1; turn = 0; noncritical_region( ); noncritical_region( ); (a) (b) Figur 18: Gjensidig utelukkelse ved at to prosesser tar hver sin tørn 3.5.3 Hver sin tørn Hvis vi har to prosesser som skal ha tilgang til delte ressurser, kan vi oppnå gjensidig utelukkelse hvis de oppfører seg pent og tar hver sin tørn (strict alternation). Koden i fig. 18 viser hvordan det kan gjøres. Variabelen turn er i utgangspunktet 0 eller 1, og når prosessene skal inn i sin region, sjekker de om den har deres verdi. I så fall går de inn; i motsatt fall venter de. Når en prosess som har gått inn i sin kritiske region er ferdig, setter den turn til den andre prosessen, som så evt. kan ga inn i den kritiske regionen. Dette kan i teorien fungere rimelig bra for to prosesser som lever ganske like, repetetive liv, f.eks. hvor en produsent lager en og en av et eller annet som en kosument fortærer (på ett eller annet vis) i samme tempo. Hvis det ikke finnes noen slik avhengighet, får vi imidlertid lett problemer, illustrert ved følgende eksempel: Prosess 1 er en treiging, men er først inne i den kritiske regionen. Den gjør seg ferdig, og går så videre for å gjøre noen grenseløst treige og langsomme oppgaver i sin ikke-kritiske region. Hvis prosess 0 er kjapp, og raser gjennom sin kritiske region, hiver seg rundt, og er klar igjen, blir det bråstopp når den kommer til den kritiske regionen dersom prosess 1 fremdeles er i sin ikke-kritiske region, ettersom turn er 1, og ikke blir satt til 0 igjen før etter at prosess 1 har vært innom sin kritiske region igjen. Dette bryter med en av forutsetningene fra avsnitt 3.4, nemlig den om at ingen prosesser som ikke er i sin kritiske region skal kunne blokkere andre prosesser. 3.5.4 Petersons aktive løsning Akkurat da leseren sikkert har infunnet seg med at dette problemet er uløselig, kommer Peterson til vår unnsetning. Løsningen i fig. 19 bruker en variabel for å angi hvis tur det er, samt en tabell for å holde styr på hvem som er interessert i å gå inn i den kritiske regionen. 22

#define FALSE 0 #define TRUE 1 #define N 2 / * number of processes * / int turn; / * whose turn is it? * / int interested[n]; / * all values initially 0 (FALSE) * / void enter_region(int process); { / * process is 0 or 1 * / int other; / * number of the other process * / other = 1 process; / * the opposite of process * / interested[process] = TRUE; / * show that you are interested * / turn = process; / * set flag * / while (turn == process && interested[other] == TRUE) / * null statement * / ; void leave_region(int process) { / * process: who is leaving * / interested[process] = FALSE; / * indicate departure from critical region * / Figur 19: Petersons aktive løsning 23

enter_ region: TSL REGISTER,LOCK copy lock to register and set lock to 1 CMP REGISTER,#0 was lock zero? JNE enter_region if it was non zero, lock was set, so loop RET return to caller; critical region entered leave_ region: MOVE LOCK,#0 RET return to caller store a 0 in lock Figur 20: Gjensidig utelukkelse med TSL Peterson definerer to funksjoner ved navn enter_region() og leave_region() som prosessene skal kalle når de henholdsvis ønsker å gå inn i og ut av sin kritiske region. I enter_region() vil (f.eks.) prosess 1 sette interested[1]=true, og deretter sette turn lik 1. Så går den i en evig løkke så lenge turn er lik 1 OG interested[0] er sann. Hvis det siste leddet slår til, er begge prosessene på vei inn i regionen samtidig, og siden vi er høflige tar vi det minste kakestykket når vi får velge først, dvs. hvis flaksen tilsier at det var vårt nummer som ble skrevet inn i turn, venter vi på den andre prosessen. 12 Når (igjen f.eks.) prosess 1 er ferdig i sin kritiske region, kaller den leave_region(), hvor den ganske enkelt setter interested[1]=false, noe som umiddlebart slipper frem prosess 0 dersom den står deer oppe og spinner. 3.5.5 TSL - test og lås Kappløptilstander inntreffer som vi har sett fordi mange av instruksjonene vi bruker ikke er atomiske. I denne sammenhengen er det mulig å få hjelp av velvillige prosessor-produsenter, hvis de implementerer den såkalte TSL-instruksjonen. Vi definerer TSL register lock slik at verdien av lock kopieres over i register, samtidig med at verdien 1 skrives til lock. Poenget er at dette skjer atomisk, dvs. i en instruksjonssyklus 13. Bruken av TSL illustreres i fig. 20, hvor denne brukes til å implementere funksjonene enter_region() og leave_region(). Dette sikrer gjensidig utelukkelse. 12 Dette blir mindre høflig men kanskje mer fornuftig hvis vi gjør som i [SGG02], og i stedet setter turn=other, og så kjører testen turn==other (merk antall likhetstegn) i løkken. Resultatet blir rimeligvis det samme. 13 Et alternativ til TSL er den litt mer generiske swap-instruksjonen, som bytter innholdet i to registre. Vi kan implementere TSL vha swap ved å sette innholdet til det ene registeret til 1 først 24

3.6 Ikke-aktive løsninger Det er lite ressursvennlig å bedrive for mye aktiv venting, ettersom man da kaster verdifull prosessortid ut av vinduet. Derfor skal vi se på noen løsninger som unngår dette. 3.6.1 Sov du vesle spire ung I stedet for å vente, kan vi la prosesser kalle en funksjon sleep() 14, som suspenderer prosessen til en annen prosess vekker den opp med wakeup(pid). Et forsøk på løsning av produsent-konsument-problemet er vist i fig. 21. Denne implementasjonen lider imidlertid av en fatal kappløpstilstand, som kan inntreffe som følger: Bufferet er tomt, og konsumenten har akkurat lest count og registrert at den er 0. Rett etterpå finner planleggeren ut at det er på tide å kjøre produsenten, og denne produserer et produkt. Produsenten inkrementerer count, og siden den nå er 1, regner den med at konsumenten må ligge og sove en plass mens den venter på produkter. Produsenten utfører derfor en wakeup, men siden konsumenten faktisk ikke har fått muligheten til å kalle sleep(), forsvinner oppvåkningen inn i et stort hull, og konsumenten kommer til å sove i all evighet. 3.6.2 Semaforer En semafor er i utgangspunktet en vanlig integer-variabel, men det spesielle med den er at den bare skal kunne endres vha. to spesielle, atomiske operasjoner, som [Tan01a] kaller up() og down() 15. En semafor kan heller ikke ha negative verdier, så hvis en prosess utfører en down() på en semafor som allerede er 0, vil prosessen blokkere seg selv, og vil ikke vekkes til live igjen før noen utfører en up() på den aktuelle semaforen. En løsning på produsent-konsument-problemet vha. semaforer er vist i fig. 22. En semafor kan f.eks. brukes for å kontrolle tilgang til en ressurstype det finnes et begrenset antall av, men der de forskjellige eksemplarene er likeverdige. Et spesialtilfelle er en semafor som bare kan ha verdien 0 eller 1; dette kalles en mutex. Operasjoner på en mutex kan f.eks. implementeres vha. TSL i pseudo-assembly som illustrert i fig. 23. 14 Må ikke forveksles med Unix/Linux systemkallet sleep() (SLEEP(3)), som suspenderer en prosess for et angitt antall sekunder 15 For sikkerhets skyld kalles disse operatorene andre ting andre steder i litteraturen. Dijkstra brukte opprinnelig bokstavene P og V, mens [SGG02] bruker signal() og wait() 25

#define N 100 / * number of slots in the buffer * / int count = 0; / * number of items in the buffer * / void producer(void) { int item; while (TRUE) { / * repeat forever * / item = produce_item( ); / * generate next item * / if (count == N) sleep( ); / * if buffer is full, go to sleep * / insert_item(item); / * put item in buffer * / count = count + 1; / * increment count of items in buffer * / if (count == 1) wakeup(consumer); / * was buffer empty? * / void consumer(void) { int item; while (TRUE) { / * repeat forever * / if (count == 0) sleep( ); / * if buffer is empty, got to sleep * / item = remove_item( ); / * take item out of buffer * / count = count 1; / * decrement count of items in buffer * / if (count == N 1) wakeup(producer); / * was buffer full? * / consume_item(item); / * print item * / Figur 21: Kode for produsent/konsument med sleep() 26

#define N 100 / * number of slots in the buffer * / typedef int semaphore; / * semaphores are a special kind of int * / semaphore mutex = 1; / * controls access to critical region * / semaphore empty = N; / * counts empty buffer slots * / semaphore full = 0; / * counts full buffer slots * / void producer(void) { int item; while (TRUE) { / * TRUE is the constant 1 * / item = produce_item( ); / * generate something to put in buffer * / down(&empty); / * decrement empty count * / down(&mutex); / * enter critical region * / insert_item(item); / * put new item in buffer * / up(&mutex); / * leave critical region * / up(&full); / * increment count of full slots * / void consumer(void) { int item; while (TRUE) { / * infinite loop * / down(&full); / * decrement full count * / down(&mutex); / * enter critical region * / item = remove_item( ); / * take item from buffer * / up(&mutex); / * leave critical region * / up(&empty); / * increment count of empty slots * / consume_item(item); / * do something with the item * / Figur 22: Produsent-konsument med semaforer 27

mutex_ lock: TSL REGISTER,MUTEX copy mutex to register and set mutex to 1 CMP REGISTER,#0 was mutex zero? JZE ok if it was zero, mutex was unlocked, so return CALL thread_yield mutex is busy; schedule another thread JMP mutex_ lock try again later ok: RET return to caller; critical region entered mutex_ unlock: MOVE MUTEX,#0 RET return to caller store a 0 in mutex Figur 23: Mutex-operasjoner implementert vha. TSL 3.6.3 Monitor En høynivå-løsning for å oppnå gjensidig utelukkelse er monitor-konstruksjonen. Dette må bygges inn som en del av programmeringspråket, og det er opp til kompilatoren å sørge for at kun en prosess er aktiv inne i en monitor til enhver tid. Hvis flere enn en prosess prøver å aksessere monitoren, vil de andre bli blokkert til den første forlater monitoren, hvorpå neste slippes inn, osv. Et pseudokode-skjelett for hvordan en monitor kan se ut er vist i fig. 24. En monitor kan brukes til å løse synkroniseringsproblemene vi har sett på hittil. Et eksempel på løsing av produsent-konsument-problemet er gitt i fig. 25. Bufferet har N innslag, og vi forutsetter som nevnt at bare en monitor-prosedyre kan være aktiv om gangen. Java er et av de få programmeringsspråkene som aktisk implementerer noe som minner om en monitor. Hvis man bruker nøkkelordet syncronized ved definisjonen av en klassemetode, vil JVM sørge for at kun en prosess får utført denne metoden om gangen. En Java-basert løsning på produsent-konsument-problemet vises i fig. 26. 3.6.4 Meldingssending I et distribuert system kan vi benytte meldinger til å styre tilgang til delte ressurser. I fig. 27 ser vi en meldingsbasert løsning på produsent-konsument. 28

monitor example integer i; condition c; procedure producer( );... end; procedure consumer( );... end; end monitor; Figur 24: Pseudokode for monitor 3.7 Barriærer I enkelte distribuerte applikasjoner vil det være slik at en rekke enheter bidrar med delresultater, og er nødt til å vente på det endelige, aggregerte resultatet før de kan gå videre med sine beregninger. I slike tilfeller kan vi bruke synkroniseringsmekanismen illustrert i fig. 28. 4 Klassiske IPC-problemer Det finnes en del klassiske IPC-(eller synkroniserings-)problemer som bl.a. brukes for å demonstrere hvor elegant nye primitiver man finner på løser nettopp disse problemene. I det følgende skal vi i filosofisk middagsselskap, vi skal lese og skrive litt, og til slutt går vi til barberen. 4.1 Spisende filosofer Anta at vi har fem filosofer som tilbringer livet sitt rundt et sirkulært bord som illustrert i fig. 29. Filosofene lever et enkelt liv; enten tenker de, eller så spiser de. Mellom hver filosof ligger det en gaffel, og hver filosof har en tallerken med spaghetti 29

monitor ProducerConsumer condition full, empty; integer count; procedure insert(item: integer); begin if count = N then wait(full); insert_item(item); count := count + 1; if count = 1 then signal(empty) end; function remove: integer; begin if count = 0 then wait(empty); remove = remove_item; count := count 1; if count = N 1 then signal(full) end; count := 0; end monitor; procedure producer; begin while true do begin item = produce_item; ProducerConsumer.insert(item) end end; procedure consumer; begin while true do begin item = ProducerConsumer.remove; consume_item(item) end end; Figur 25: Monitor-løsning på produsent-konsument 30

public class ProducerConsumer { static final int N = 100; // constant giving the buffer size static producer p = new producer( ); // instantiate a new producer thread static consumer c = new consumer( );// instantiate a new consumer thread static our_ monitor mon = new our_ monitor( ); // instantiate a new monitor public static void main(string args[ ]) { p.start( ); // start the producer thread c.start( ); // start the consumer thread static class producer extends Thread { public void run( ) { // run method contains the thread code int item; while (true) { // producer loop item = produce_item( ); mon.insert(item); private int produce_item( ) {... // actually produce static class consumer extends Thread { public void run( ) { run method contains the thread code int item; while (true) { // consumer loop item = mon.remove( ); consume_ item (item); private void consume_ item(int item) {... // actually consume static class our_monitor { // this is a monitor private int buffer[ ] = new int[n]; private int count = 0, lo = 0, hi = 0; // counters and indices public synchronized void insert(int val) { if (count == N) go_to_sleep( ); // if the buffer is full, go to sleep buffer [hi] = val; // insert an item into the buffer hi = (hi + 1) % N; // slot to place next item in count = count + 1; // one more item in the buffer now if (count == 1) notify( ); // if consumer was sleeping, wake it up public synchronized int remove( ) { int val; if (count == 0) go_to_sleep( ); // if the buffer is empty, go to sleep val = buffer [lo]; // fetch an item from the buffer lo = (lo + 1) % N; // slot to fetch next item from count = count 1; // one few items in the buffer if (count == N 1) notify( ); // if producer was sleeping, wake it up return val; private void go_ to_ sleep( ) { try{wait( ); catch(interruptedexception exc) {; 31 Figur 26: Produsent-konsument i Java