Kompletterande kompendium till kursen Realtidsprogrammering



Relevanta dokument
Realtidsprogrammering Ordinarie tentamen

Tentamen i Realtidsprogrammering för Au3, D3, E3

Omtentamen i Realtidsprogrammering för Au3, D3, E3

Tentamen omfattar 40 poäng fördelade på 7 uppgifter. 20 poäng krävs för betyget 3, 30 poäng för betyget 4 och 36 poäng för betyget 5.

Tentamen i Realtidsprogrammering

Tentamen i Realtidsprogrammering

Outline. Datorsystemtekni. Kravspecifikation. Kravspecifikation (forts.)

Tentamen i Realtidsprogrammering

Synkronisering. Föreläsning 8

Realtidssystem. - Dödläge - EDAF85 - Realtidssystem (Helsingborg) Elin A. Topp. Föreläsning 5

Realtidssystem. - Schemaläggning - EDAF85 - Realtidssystem (Helsingborg) Elin A. Topp. Föreläsning 6

Fö 5+6 TSEA81. Real-time kernel + Real-time OS

Operativsystem. Informationsteknologi sommarkurs 5p, Agenda. Slideset 7. Exempel på operativsystem. Operativsystem

AVR 3 - datorteknik. Avbrott. Digitala system 15 hp. Förberedelser

Föreläsning 3.1: Datastrukturer, en översikt

TDDIU81. Processer och trådar. Andreas Dahlberg, Jonathan Doherty, Tony Magnusson, Patrik Ottosson, Rasmus Siljedahl

Institutionen för elektro- och informationsteknologi, LTH

Datorteknik. Föreläsning 5. Realtidssystem och realtidsprogrammering. Institutionen för elektro- och informationsteknologi, LTH.

F5: Högnivåprogrammering

F5: Högnivåprogrammering

Operativsystem ID2200 Tentamen TEN1 3.8 hp :00-18:00

Exam Concurrent and Real-Time Programming

Föreläsning 5 Innehåll

Operativsystem ID2206 Tentamen TEN1 4.5 hp :00-18:00

Föreläsning 2. Operativsystem och programmering

Trådar. Aktiva objekt

Datorteknik ERIK LARSSON

Introduktion till algoritmer - Lektion 1 Matematikgymnasiet, Läsåret Lektion 1

Realtidssystem. - Schemaläggning - EDA698 - Realtidssystem (Helsingborg) Elin A. Topp. Föreläsning 6

Operativsystem. Hierarkin för hårdvara läses nerifrån

*Pekarvärden *Pekarvariabler & *

Systemkonstruktion LABORATION REALTIDSPROGRAMMERING

Att använda pekare i. C-kod

Bakgrund och motivation. Definition av algoritmer Beskrivningssätt Algoritmanalys. Algoritmer. Lars Larsson VT Lars Larsson Algoritmer 1

Tentamen, EDA501 Programmering M L TM W K V

732G Linköpings universitet 732G11. Johan Jernlås. Översikt. Repetition. Muddy. Funktioner / metoder. Punktnotation. Evalueringsordning

Operativsystem (IS1350) :00-12:00

Deadlocks. detektera och undvik

LABORATION. Datorteknik Y

Objektorienterad Programmering (TDDC77)

I Skapa Hej.java och skriv programmet. I Kompilera med javac Hej.java. I Rätta fel och repetera tills du lyckas kompilera ditt program

Föreläsning 6: Introduktion av listor

Föreläsning 3-4 Innehåll. Diskutera. Metod. Programexempel med metod

Föreläsning 11. Giriga algoritmer

Realtidssystem HT03. Vad är realtidssystem? Inbyggda system. Att programmera, Tasks (Uppgifter) Realtidssystem kräver analys

Programmering B med Visual C

Laboration 3 HI1024, Programmering, grundkurs, 8.0 hp

Mål. Datorteknik. Repetition av avbrott. Innehåll. Mätning och styrning. Datorer för mätning och styrning. timer. Datorsystem A/D. Analog insignal D/A

Föreläsning 3-4 Innehåll

Hjälpmedel: Inga hjälpmedel förutom penna, suddgummi och glatt humör.

Föreläsning 13 Innehåll

Tentamen den 18 mars svar Datorteknik, EIT070

Datorteknik 2 (AVR 2)

KPP053, HT2016 MATLAB, Föreläsning 1. Introduktion till MATLAB Skript Inläsning och utskrift av variabler Ekvationssystem Anonyma funktioner

Svar till tentamen den 16 december 2013 Datorarkitekturer med operativsystem, EDT621, 7,5 poäng

STOCKHOLMS UNIVERSITET MATEMATISKA INSTITUTIONEN Avd. Matematisk statistik Anders Björkström

KAP 18 SQL SERVER AGENT

Kompilering och exekvering. Föreläsning 1 Objektorienterad programmering DD1332. En kompilerbar och körbar java-kod. Kompilering och exekvering

Föreläsning 5 Innehåll. Val av algoritm och datastruktur. Analys av algoritmer. Tidsåtgång och problemets storlek

Multipel tilldelning. Introduktion till programmering D0009E. Föreläsning 6: Iteration. while-satsen. Kom ihåg. Snurror kontra rekursion

Fö 7: Operativsystem. Vad är ett operativsystem? Målsättning med operativsystem. Styr operativsystemet datorn?

Alla datorprogram har en sak gemensam; alla processerar indata för att producera något slags resultat, utdata.

Realtidsprogrammering. En introduktion Implementering (med exempel från PIC)

Användarhandledning Version 1.2

Algoritmer, datastrukturer och komplexitet

Laboration: Grunderna i MATLAB

Universitetet i Linköping Institutionen för datavetenskap Anders Haraldsson

Objektorienterad Programmering (TDDC77)

TDDI16 Datastrukturer och algoritmer. Algoritmanalys

Tentamen EDA698 Realtidssystem (Helsingborg)

Realtidsprogrammering

Operativsystem DVG A06. Definition. Varför operativsystem? - Vad är ett operativsystem?

Operativsystem. Innehåll. Operativsystemets funktion. Vad är ett OS? Vart hittar men ett OS? OS hanterar processorns resurser

Laboration 4: Knappstuds Drivrutiner för att eliminera störningar.

Lösningar till tentauppgifterna sätts ut på kurssidan på nätet idag kl 19. Omtentamen i Programmering C, 5p, fristående, kväll,

Öka prestanda i Shared-Cache multi-core processorer

SMD 134 Objektorienterad programmering

Lite om felhantering och Exceptions Mer om variabler och parametrar Fält (eng array) och klassen ArrayList.

KPP053, HT2015 MATLAB, Föreläsning 1. Introduktion till MATLAB Skript Inläsning och utskrift av variabler Ekvationssystem Anonyma funktioner

Realtidsstöd i Minix. En laborationrapport. Oktober 2012

Agenda. Arrayer deklaration, åtkomst Makron Flerdimensionella arrayer Initiering Strängar Funktioner och arrayer. Övningar nu och då

Lösningar till tentamen i EIT070 Datorteknik

Tentamen i TDP004 Objektorienterad Programmering Praktisk del

Programmering, grundkurs, 8.0 hp HI1024, HI1900 etc., Tentamen TEN1. Måndagen den 10 januari 2011,

Operativsystem ID1200/06 Tentamen :00-18:00

Introduktion till programmering SMD180. Föreläsning 4: Villkor och rekursion

Föreläsning 6 pekare och pekare tillsammans med arrayer

Vad är viktigast? Sammanfattning. Processer och trådar. Processer och trådar. Flerprocessorsystem. Schemaläggning. Interprocesskommunikation.

Föreläsning 11 Tisdag 6/6 2000

Övning från förra gången: readword

En Von Neumann-arkitektur ( Von Neumann-principen i föreläsning 1) innebär:

Datorsystemteknik Föreläsning 7DAVA14

TDIU01 - Programmering i C++, grundkurs

Programmera i C Varför programmera i C när det finns språk som Simula och Pascal??

Dynamiskt minne. Vad är dynamiskt minne Motivering Hur gör man i C Övningar

2 Pekare och dynamiska variabler.

Rekursion och induktion för algoritmkonstruktion

Föreläsning 2 Datastrukturer (DAT037)

Laboration 3 HI1024, Programmering, grundkurs, 8.0 hp

Introduktion till algoritmer - Lektion 4 Matematikgymnasiet, Läsåret Lektion 4

Transkript:

Kompletterande kompendium till kursen Realtidsprogrammering Mathias Broxvall, Lars Karlsson Örebro Universitet Januari 2010 v2010.1 1

2

Innehåll Innehåll...1 Processer i VxWorks...2 Multitasking...2 Ett tasks olika tillstånd...3 Schemaläggning och tidshantering i VxWorks...4 Delad kod...5 Hårdvaruavbrott i VxWorks...5 Andra sätt för tasks att kommunicera...6 Monitorer...7 Bankers algoritm...10 Bakgrund...10 Data...10 Algoritmen...10 Exempel...11 Övning...12 Upptäcka dödlåsning...13 Bakgrund...13 Data...13 Algoritmen...13 Exempel...14 Övning...15 Schemaläggning...16 Ingredienser...16 När går det att schemalägga en grupp processer?...17 Schemaläggningsalgoritmer...19 Sammanfattning...22 Övningar...22 Petri-nät...23 Inledning...23 Ett Petri-näts uppbyggnad...23 Ett Petri-näts beteende...24 Ömsesidig uteslutning med Petri-nät...26 Petri-nät med flera tecken per plats...31 Egenskaper hos Petri-nät...33 Mjukvara för analys av petrinät...35 Analys av mer avancerade petrinät...36 Utökningar av petrinät...37 Realtidsprogrammering instuderingsfrågor...43 Blandade övningar...44 Lösningar till övningar...49 1

Processer i VxWorks Multitasking I VxWorks är det realtidskärnan, wind, som hanterar multipla processer. Vilken process som kör när bestäms av realtidskärnans schemaläggare. Genom att låta schemaläggaren ofta byta mellan processerna kan det nästan se ut som att de olika processerna verkligen körs samtidigt. I VxWorks kallas varje oberoende process för ett task (ni få ursäkta den klumpiga försvenskningen av den engelska termen). Dessa olika task delar på samma minnesutrymme och kan därigenom använda samma variabler och kod. Däremot har varje task har sin egen exekveringskontext. Kontexten innehåller de CPU-registervärden och systemresurser som tasket använder när det är dess tur att exekvera. När ett task börjar exekvera hämtas dess exekveringskontext från dess "task control block" (TCB), och när ett task lämnar ifrån sig exekveringen sparas exekveringskontexten undan i TCB:t. Ni kan hitta definition av ett TCB som en struct windtcb i include filen tasklib.h. Ett TCB innehåller framför allt: Programräknaren (var denna task är i sin kod) CPU-register En stack för dynamiska variabler och funktionsanrop Vad som är standard input, output, och error En timer för fördröjningar En timer för tidsdelning Kontrollstrukturer för kärnan Signalhanterare Debugging-information. Förutom ett TCB så har ett task också: Ett namn Ett identitetsvärde (heltal) En prioritet Ett tillstånd 2

Ett tasks olika tillstånd Kärnan håller reda på de olika taskens tillstånd. Det finns fyra olika tillstånd (och ett antal kombinationer av dessa). READY PEND DELAY SUSPEND Tasket är körklart och väntar inte på någon resurs (förutom CPU:n) Tasket väntar på någon resurs (t ex semafor, meddelandekö) som inte är tillgänglig för ögonblicket. Tasket sover för en viss tid. Tasket har tillfälligt avstannat på grund av debugging. För att se vilket tillstånd en task är i kan ni använda funktionen statusstring definierad i vxwtasklib.h. Vad som får ett task att byta tillstånd illustreras i bilden nedan. 3

Schemaläggning och tidshantering i VxWorks Schemaläggarens uppgift är att fördela CPU-tid mellan de olika körklara tasken, dvs de som är i tillståndet READY. Task i andra tillstånd körs aldrig. Byten mellan olika processer som ska köras kallas för kontextbyten och detta görs när operativsystement periodiskt blir anropat via interrupts som genereras av hårdvaru timers. För att förstå när och hur dessa process byten sker så måste vi dels förstå hur VxWorks hanterar tid samt titta på de olika schemaläggnings algoritmerna som finns för att bestäma vilken process som ska köras när nästa kontext byte sker. Det finns två alternativa schemaläggningsmetoder; det förvalda alternativet är prioritetsbaserad föregripande (preemptive) schemaläggning, men tidsdelning (round robin) kan också väljas. Ticks i VxWorks När man programmeorar inbyggda system och använder flera processer som ska se ut att köra samtidigt så måste RTOS'et byta mellan de olika processerna kontinuerligt. För att kunna göra detta så behöver vissa operativsystemsfunktioner köras periodiskt för att kunna göra kontextbyten. Detta byte till operativsystems-funktionerna implementeras mha interupts som genereras av hårdvarutimers. I VxWorks så sker detta med en fast periodicitet. Den grundläggande tidsenheten som VxWorks använder för att mäta tid, en Tick, motsvarar den period med vilken dessa interrupt genereras. Period längden och det antal ticks som genereras varje sekund kan ställas in mha operativsystems-funktionen sysclkrateset(). Maximalt antal ticks per sekund som kan genereras beror i regel på hårdvaran, dvs. på upplösningen av de hårdvarutimers som används för att generera interrupts. Varje gång en tick genereras i VxWorks så kontrollerar operativsystemet om det finns någon annan task med högre prioritet än den som kör för närvarande som vill köra. Om det finns en sådan task så gör VxWorks ett kontextbyte, dvs. sparar undan exekveringskontexten (register innehåll, programräknare, stack,...) för den nuvarande processen och laddar in exekveringskontexten för den nya task'en. När en task gör ett operativsystems anrop som fördröjer den (t.ex. taskdelay) så försätts den i väntläge och operativsystemet gör ett kontextbyte till en annan task som är körklar och har högst prioritet. Eftersom kontextbyten endast kan ske i samband med ticks, så kan det hända att en task som sover blir tvungen att vänta lite längre än vad som var tänkt. Därför är det viktigt att ha en lämplig tick period som passar till applikationen. Naturligtvis tar dessa kontroller och byten mellan processer lite tid av processorn, så därför behövs en avvägning mellan processorkraft och hur ofta processbyten måste kunna ske. Prioritetsbaserad föregripande schemaläggning Principen för prioritetsbaserad föregripande schemaläggning är att det task körs som har högst prioritet av de som är körklara. Finns det flera körklara tasks med högsta prioritet så körs ett av dem. Ett task körs antingen till det hamnar i ett annat tillstånd (t ex i DELAY efter ett taskdelay()), eller tills ett task med högre prioritet än det nuvarande exekverande tasket blir körklart. I det senare fallet så tar det högre prioriterade tasket över. I bilden nedan så avbryts t1 av t2 (som har högre prioritet), och sedan t2 av t3 (som har ännu högre prioritet). När t3 är klar kan t2 fortsätta, och när t2 är klar kan t1 fortsätta. 4

Kärnan använder sig av 256 prioritetsnivåer, från 0 (högst) till 255 (lägst). Ett task tilldelas en prioritet när det skapas, men prioriteten kan också ändras dynamiskt. Tidsdelning Den prioritetsbaserad föregripande schemaläggningen kan kompletteras med tidsdelning (round robin). Detta innebär att CPU-tiden fördelas någorlunda jämnt mellan processer med samma prioritet. Utan tidsdelning kan ett enda task lägga beslag på processorn utan att andra tasks med samma prioritet får tid att köra. Med tidsdelning får varje körklar task (av de med högst prioritet) köra en begränsad tid, lämna över till nästa task och sedan vänta på sin tur igen. Detta åstadkommes genom att körklara tasks med samma prioritet ordnas i en först-in-först-ut-kö. Varje task har dessutom sin egen tidsdelnings-timer som håller reda på hur mycket tid tasket har kvar. Om ett task avbryts av ett annat task med högre prioritet, så får det första tasket fortsätta den tid det har kvar när det högre prioriterade tasket är klart. Detta illustreras i bilden nedan. Delad kod I VxWorks är det inte ovanligt att flera tasks använder sig av samma subrutin, t ex printf(), fast det bara finns ett exemplar av subrutinens kod i systemet. Detta kallas för delad kod. Det är viktigt att delad kod är "reentrant" (återinträdbar), d v s att flera tasks kan anropa den samtidigt utan att konflikter uppstår. Om detta ska hålla så får inte koden modifiera statiska (static) eller globala variabler. Det finns bara ett exemplar av varje statisk eller global variabel, d v s variabeln lagras på en enda plats i minnet. Om flera tasks modifierar den samtidigt kommer de att skriva i samma minnesutrymme, och det är detta som kan leda till konflikter. Lokala icke-statiska variabler, å andra sidan, är säkra eftersom ny plats för sådana variabler allokeras på stacken varje gång rutinen anropas. Det är också möjligt att starta flera tasks med samma huvud-rutin, eftersom varje task har sin egen stack och kontext, inklusive programräknare. Det är dessutom möjligt att skicka med parametrar till ett task. Även i detta fall måste koden vara återinträdbar. Bilden nedan visar hur flera tasks använder samma kod för att styra varsin led i en robotarm. Genom att parametersera tasken kan man tala om för dem vilken led de ska styra. 5

Hårdvaruavbrott i VxWorks Hårdvaruavbrott är naturligtvis mycket viktiga i VxWorks liksom i all realtidsprogramvara - det är ofta genom avbrott som ett realtidssystem kan detektera att någonting i systemets omgivning har hänt (t ex nya mätdata finns tillgängliga). Avbrott används ofta för att snabbt reagera på externa händelser utan att behöva polla indata portarna, vilket frigör viktig processorkraft till att utföra beräkningar istället för att kontinuerligt kontrollera om någon indata port har ändrats. För att kunna göra detta så krävs det att en eller flera ingångar i hårdvaran har kopplats till processorns avbrottsingångar, vilket beroror till stor del på hårdvaran som används. I VxWorks är hårdvaruavbrotten oberoende av vilket task som exekveras - varje interupt sparar undan den exekverande kontexten och skapar sin egen kontext. Den tidigare kontexten återställes sedan när avbrottsrutinen är klar. För att hantera avbrott behöver man skapa sina egna avbrottsservicerutiner (ISR) och lägga in dessa i en avbrottstabell. Det finns en funktion IntConnect(IR-nummer, funktion, arg)som gör detta. När ett hårvaruavbrott med det angivna numret kommer in, kommer funktionen att exekvera med det medföljande argumentet. Avbrott används ofta för att hämta in information från omvärlden, och naturligtvis vill man vidarebefordra denna information till olika tasks (kom ihåg att avbrottsrutiner exekverar utanför alla taskkontexter). Det finns ett flertal olika metoder för detta. En ISR kan: skriva i delade variabler och ringbuffrar, ge semaforer (utom av typen mutex), skicka meddelanden på en meddelandekö (om kön är full förloras meddelandet), skriva meddelanden på en pipe (se nedan), skicka signaler till ett task. Obs! När man skriver avbrottsrutiner är det viktigt att inte anropa någon funktion som kan leda till att rutinen får vänta, t ex semtake() eller scanf() eller taskdelay(). Avbrottsrutinen bör avslutas fort, och att ställa sig och vänta på en semafor eller annan resurs är ett grovt brott mot god avbrottsetikett. Andra sätt för tasks att kommunicera I VxWorks finns det fler sätt än delade variabler, semaforer och meddelandeköer för tasks att kommunicera med varandra. Signaler Signaler är ganska lika avbrott, men genereras inte från hårdvaran utan från ett task eller en ISR. Till skillnad från avbrott så skickas en signal till ett bestämt task. När tasket tar emot signalen avbryter det vad det håller på med och exekverar en signalhanteringsrutin. Till skillnad från ett avbrott så exekveras signalhanteringsrutinen i taskets kontext. Vilken rutin som exekveras bestäms av taskets signalhanteringstabell. Funktionen signal() används för att ställa upp signalhanteringstabellen och koppla ihop signaler med signalhanteringsrutiner. Funktionen kill() skickar en signal till ett bestämt task, och med funktionen raise() kan ett task signalera till sig självt. Det finns 32 olika möjliga signaler, numrerade från 0 till 31. Signaler används främst för att tala om att någonting har gått fel. Pipes En pipe är en virtuell I/O-enhet för task-till-task- kommunikation som man kan skriva på och läsa från med de vanliga I/O-funktionerna read() och write(). Man kan se en pipe som en virtuell fil som den ena processen skriver på och den andra läser från. En pipe skapas med funktionen pipedevcreate("/pipe/name", maxmsgs, maxlength). En pipe utför i princip samma funktioner som en meddelandekö, med några mindre skillnader. Sockets En socket (svenska: sockel) är en virtuell I/O-enhet för att kommunicera över ett nätverk, mellan processer som kan exekvera på olika maskiner. Det går också att använda för processer på samma maskin - oberoende av var de olika processerna befinner sig ser det precis likadant ut från programmerarens perspektiv. "Namnet" på en sockel består dels av en nätverksadress (t.ex. mic8.oru.se) som anger vilken maskin sockeln ligger på, och dels ett portnummer (t.ex. 8001). Det senare svarar inte mot någon fysiskt port på datorn - all sockelkommunikation går via datorns nätverksport utan är bara till för att skilja mellan olika sockets på samma maskin. Man kan skriva på och läsa från socklar med de vanliga I/O-funktionerna read() och write(). 6

Monitorer Denna sektion berör monitorer som kommunikationsform mellan processer, för en mer utförlig presentation av detta material läs kapitel 7 i kursboken. Som motiverande exempel tänker vi oss att vi behöver dela data mellan två eller fler processer. Vi kan t ex tänka oss att våra processer ska kunna skriva i och läsa från variabler innehållande textsträngar. Naturligtvis vill vi inte att t ex två processer försöker skriva i samma text-variabel samtidigt. Ett enkelt sätt att lösa detta är att sätta in semaforer vid de platser där läsande/skrivande sker: void task1() { semtake(shared_sem,wait_forever); strcpy(str1,shared_str); /* Write to shared string */ semgive(shared_sem); } void task2() { semtake(shared_sem,wait_forever); strcpy(shared_str,str2); /* Read from shared string */ semgive(shared_sem); } Tyvärr kan ett enda misstag i semaforhanteringen få katastrofala effekter. Om vi t ex glömmer att ge semaforen i task2 så innebär detta att ingen process därefter kan ta semaforen. I värsta fall kommer till slut samtliga processer att stå och vänta på semaforen, och hela systemet stannar upp. För att undvika detta kan vi i stället placera all semaforhantering på en enda plats: i en monitor. En monitor består av: - En datastruktur med plats för - delade data, och - semaforer för skydda kritiska regioner. - Ett antal procedurer för att manipulera (skriva, läsa etc) dessa data. Data i monitorn kan endast nås genom dessa procedurer. Exemplet ovan kan t ex skrivas om som i Tabel 1. Här finns en datastruktur för monitorn som innehåller dels den delade strängvariabeln och dels semaforen som behövs för att skydda strängen. Dessutom finns här procedurer för att initialisera monitorn och för att läsa och skriva från strängvariabeln. Notera att de senare inkluderar semaforhantering. 7

/* File: mon.c */ #include <semlib.h> struct string_monitor { SEM_ID mutex; char value[32]; }; struct string_monitor *initstringmonitor(void) { struct string_monitor *m; m = malloc(sizeof(struct string_monitor)); m->mutex = semmcreate(sem_q_fifo); m->value[0] = 0x00; return m; } char* read_string(struct string_monitor *m, char *dest) { semtake(m->mutex, WAIT_FOREVER); strcpy(m->value, dest); semgive(m->mutex); return dest; } char* write_string(struct string_monitor *m, char *src) { semtake(m->mutex, WAIT_FOREVER); strcpy(src, m->value); semgive(m->mutex); return src; } Table 1: Exempel på en monitor, implemenationsdelen. Vi skapar också en header-fil, som innehåller deklarationer av monitorn och dess procedurer. Lägg märke till att i den här header-filen syns inte vad som göms inne i monitorn. Både semaforen och strängen är väl skyddade från manipulation utifrån. /* File: mon.h */ struct extern extern extern string_monitor; struct string_monitor* initstringmonitor(void); char* read_string(struct string_monitor *m, char *dest); char* write_string(struct string_monitor *m, char *src); Table 2: Exempel på en monitor, deklarations delen. Nu kan vi sköta all manipulation av delade strängar med hjälp av vår monitor-datatyp och våra monitor-procedurer. De två processerna här nedan gör samma sak som de in det inledande exemplet. 8

/* File: tasks.c */ #include "mon.h" struct string_monitor *m1; int task1() { char str1[32]; write_string(m1, str1); } int task2() { char str2[32]; read_string(m1, str2); } void main() { } m1 = initstringmonitor(); /* Start processes etc */ Table 3: Hur monitorn kan användas. 9

Bankers algoritm Bakgrund Antag att ett datorsystem har m olika resurser och kör n olika processer. En av dessa processer - nummer k - begär plötsligt fler resurser. Med Bankers algoritm kan vi avgöra om det finns risk för dödlåsning om process k får de resurser den begär. Data Följande data behövs: Available[j] där 1 j m - hur många enheter av resurs j som för tillfället finns tillgängliga. Max[i,j], där 1 i n, 1 j m - hur många enheter av resurs j som process i maximalt kan behöva. Allocation[i,j] - hur många enheter av resurs j som för tillfället är tilldelade process i. Need[i,j] - hur många fler enheter av resurs j som process i maximalt kan behöva utöver de enheter den redan tilldelats. Max[i,j] = Allocation[i,j] + Need[i,j]. Lite förenklande notation: Tänk att A, B är vektorer av längd k. När vi skriver t.ex. A B, så menar vi att A[i] B[i] för varje i sådant att 1 i k. Med andra ord, man jämför tal på samma plats i de två vektorerna. T ex så har vi att (1 0 ) (1 2), eftersom 1 1 och 0 2. När vi skriver A + B = D, så menar vi att A[i] + B[i] = D[i] för varje i sådant att 1 i k. T ex har vi att (1 3) + (1 2) = ( 2 5), eftersom 1+ 1 = 2 och 3+2 = 5. Om C är en matris av storleken k l, så står Ci för vektorn (C[i,1], C[i,2],, C[i,l]), d v s den i:e raden i matrisen. T ex om 2 1 2 2 så är C1 = ( 2 1) och C2 = ( 2 2 ). C = Algoritmen Själva idén bakom algoritmen är att man tittar på läget som skulle uppstå om den aktuella processen tilldelades de resurser den begär. Sedan avgör man om det läget är säkert, d v s att man inte riskerar att hamna i en död låsning. Bankers algoritm Indata: Request k - hur många enheter process k begär av de olika resurserna. Obs! Den här vektorn innehåller bara information om den process som begär mer resurser; den har ingenting att göra med de andra processerna. Steg: 1. Om inte Request k Need k - fel! Processen har begärt fler enheter av någon resurs än vad den maximalt ska kunna behöva. 2. Om inte Request k Available - vänta! Det finns inte tillräckligt antal enheter av en av de begärda resurserna. 3. Pröva (rent hypotetiskt) vad som skulle hända om process k skulle tilldelas de begärda resurserna. Uppdatera de olika matriserna: Available := Available - Request k Allocation k := Allocation k + Request k Need k := Need k - Request k Om det nya läget är säkert (se nedan) så tilldela de begärda resurserna. Annars: återställ matriserna och låt processen vänta. Säkert läge? För att avgöra om ett läge är säkert, försöker man hitta ett sätt att ge varje process sitt maximala behov av resurser genom att upprepa de två stegen: (1) välj ut en process som kan tilldelas sitt maximala behov av resurser i det nuvarande läget och köra klart; och (2) notera att processen är klar och frigör de resurser som den har allokerat så att återstående processer kan använda dem. Om detta lyckas för samtliga processer är läget säkert; annars ej. Indata: Det nya läget enligt punkt 3 ovan. Temporära data: Work[j] - hur mycket av resurs j som finns tillgängligt vid varje steg. Finish[i] - process i är avslutad. 10

Steg: 1. Låt Work := Available Finish[i] := false, i=1,..,n. 2. Hitta ett l (d v s en process) sådant att både - Finish[l] = false - Needl Work d v s process l är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den dess maximala behov och sedan avsluta den. Finns inget sådant l: gå till 4. 3. Låt Work := Work + Allocationl Finish[l] := true d v s uppdatera till läget då process l är avslutad och har lämnat tillbaka alla sina allokerade resurser. Gå tillbaka till 2. 4. Om Finish[i] := true för alla i=1,..,n: alla processer har lyckats avsluta läget är säkert. Annars: läget är ej säkert. Exempel Vi antar att vi har två processer och två resurser. I matriserna lägger vi processerna radvis och resurserna kolumnvis, enligt: R1 R2 P1 2 2 P2 2 0 Av resurs #1 (i kolumn 1 i matriserna nedan) finns det totalt 4 enheter varav två är lediga (se Available), en används av process #1 (rad 1 kolumn 1 i matrisen Allocation) och en används av process #2 (rad 2 kolumn 1 i matrisen Allocation). Det finns också totalt 3 enheter av resurs #2 (i kolumn 2 i matriserna nedan) varav två är lediga (se Available) och en används av process #1 (rad 1 kolumn 2 i matrisen Allocation). Available = ( 2 2) 3 3 1 Allocation = 1 2 2 Max = Need = 1 0 2 1 2 2 Nu behöver process #1 plötsligt en mer av resurs 1. Vi får: Request1 = (1 0) Vi konstaterar att villkoren i steg 1 och steg 2 i Bankers är uppfyllda, och går vidare till steg 3. Vi testar vad det nya läget skulle vara: Available := Available - Request1 = ( 2 2 ) - (1 0 ) = (1 2 ) 2 1 1 0 Allocation1 := Allocation1 + Request1 = (1 1) + (1 0 ) = ( 2 1), vilket ger Allocation := 1 1 2 2 Need1 := Need1 - Request1 = ( 2 1) - (1 0 ) = (1 1), vilket ger Need := Vi vill nu testa om det nya läget är säkert. 11

1. Låt Work := Available = (1 2) false false Finish := 2. Process 1 är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den dess maximala behov och sedan avsluta den: - Finish[1] = false - Need1 Work, d v s (1 1) (1 2) 3. Uppdatera till läget då process 1 är avslutad och har lämnat tillbaka alla sina allokerade resurser: Work := Work + Allocation1= (1 2 ) + ( 2 1) = ( 3 3) true false Finish := 2. Process 2 är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den dess maximala behov och sedan avsluta den: - Finish[2] = false - Need2 Work d v s ( 2 2 ) ( 3 3) 3. Uppdatera till läget då process 2 är avslutad och har lämnat tillbaka alla sina allokerade resurser. Work := Work + Allocation2= ( 3 3) + (1 0 ) = ( 4 3) true true Finish := 2. 4. Inga processer kvar - gå till 4. Läget är säkert! Vi kan först ge P1 alla de resurser den behöver och låta den köra klart, och sedan göra samma sak med P2. Övning I exemplet ovan kunde process 1 tilldelas sina begärda resurser. Vi tänker oss att därefter (i det nya läget) begär process 2 följande resurser: Request2 = (1 2) Kan det leda till död låsning? Använd Bankers algoritm för att avgöra det. 12

Upptäcka dödlåsning Bakgrund Antag igen att datorsystemet har m olika resurser och kör n olika processer. Vi vill nu avgöra om systemet har hamnat i ett läge med död låsning. Observera att detta inte är självklart att avgöra eftersom systemet kan vara i dödlåsning även om det finns några processer som just nu kör. Tänk er exempelvis en dator som kör tre processer A,B och C. De två första processerna är pausade och väntar på att resurer ska frigöras medans process C kör. Det kan nu finnas två situationer: antingen så håller process C de resurser som behövs och kommer så småningom att frigöra dem, isåfall så kommer A och B att få köra i framtiden och vi har inte någon dödlåsning; alternativt så kan kan process A hålle de resurser som B behöver och virce versa, i så fall kommer de aldrig mer att kunna få de resurser de behöver och systemet sägs vara i dödlåsning. Naturligtvis så kan det vara betlydligt mer avancerat av avgöra om ett system är i dödlåsning eller inte och vi behöver en algoritm för det. Om man följer algoritmen nedan så fungerar detta alltid för att avgöra om ett system är i dödlåsning eller inte. Data Följande data behövs: Available[j] där 1 j m - hur många enheter av resurs j som för tillfället finns tillgängliga. Allocation[i,j] där 1 i n, 1 j m - hur många enheter av resurs j som för tillfället är tilldelade process i. Request[i,j] - antal enheter av resurs j som process i väntar på. Obs! Här är Request en matris som täcker alla processers nuvarande resursbegäran. Algoritmen För att avgöra om man har död låsning, försöker man hitta ett sätt att ge varje process sin begärda kvot av resurser genom att upprepa de två stegen: (1) välj ut en process som kan tilldelas sin begärda kvot av resurser i det nuvarande läget och sedan köra klart; och (2) notera att processen är klar och frigör de resurser som den har allokerat så att återstående processer kan använda dem. Om detta lyckas för samtliga processer är allting väl; annars har vi en död låsning med de återstående processerna inblandade. Lägg märket till att algoritmen för att avgöra om ett läge är säkert är väldigt lik den här algoritmen, för att upptäcka död låsning. Den enda skillnaden är att i den förra talar vi om processernas maximala behov av resurser, medan i den senare talar vi om processernas begärda kvot av resurser. Temporära data: Work[j] - hur mycket av resurs j som finns tillgängligt vid varje steg. Finish[i] - process i är avslutad. Steg: 1. Låt Work := Available false om Allocationi 0 true annars Finish[i] := 2. 3. 4. Hitta ett l (d v s en process) sådant att både - Finish[l] = false - Requestl Work d v s process l är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den vad den väntar på och sedan avsluta den. Finns inget sådant l: gå till 4. Låt Work := Work + Allocationl Finish[l] := true d v s uppdatera till läget då process l är avslutad och har lämnat tillbaka alla sina allokerade resurser. Gå tillbaka till 2. Om Finish[i] := false för något i=1,..,n så är systemet i ett läge med död låsning. 13

Exempel Vi tänker oss att datorsystemet befinner sig i följande läge: Available = ( 0 2) 2 1 2 0 0 1 Request = 1 0 Allocation = 1. Låt Work := Available = ( 0 2) false false Finish := 2. Process 1 är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den vad den väntar på och sedan avsluta den. - Finish[1] = false - Request1 Work, d v s ( 0 1) ( 0 2) 3. Uppdatera till läget då process 1 är avslutad och har lämnat tillbaka alla sina allokerade resurser. Work := Work + Allocation1= ( 2 3) true false Finish := 2. Process 2 är inte avslutad ännu, men det finns tillräckligt med resurser för att ge den vad den väntar på och sedan avsluta den. - Finish[2] = false - Request2 Work d v s (1 0) ( 2 3) 3. Uppdatera till läget då process 2 är avslutad och har lämnat tillbaka alla sina allokerade resurser. Work := Work + Allocation2= ( 4 3) true true Finish := 2. 4. 14 Inga processer kvar. Gå till 4. Ingen död låsning! Vi kan först ge P1 alla de resurser den begär och låta den köra klart, och sedan göra samma sak med P2.

Övning Vi tänker oss att datorsystemet befinner sig i följande läge: Available = ( 0 1) 2 1 3 1 1 0 Request = 0 2 Allocation = Har vi död låsning? 15

Schemaläggning När man utvecklar programvara för ett inbyggt system så räcker det vanligtvis inte med att se till att programmet är logiskt korrekt, d v s inte innehåller buggar med felaktiga pekare, semaforer som aldrig släppts, loopar som aldrig slutar, variabler som ges fel värden eller andra typer av programmerings- och designfel i koden. Man måste också kontrollera att programmet lyckas leva upp till de tidskrav man ställer när det exekveras på den hårdvara (processor etc.) som det är avsett för. Om man t ex har en process som ska utföra en viss beräkning var 50 ms samtidigt som beräkningen tar 100 ms att göra på den valda processorn, så får man se till att antingen göra beräkningen på ett snabbare sätt eller välja en snabbare processor. Det hela kompliceras dessutom ofta av att man har flera olika processer i systemet, var och en med sina egna tidskrav. Schemaläggning handlar om hur man fördelar processortid (och processorer, om man har flera sådana) mellan olika processer så att processernas olika tidskrav uppfylls. I ett realtidsoperativsystem sköter kärnans schemaläggare om själva växlandet mellan processer, men det är programmerarens uppgift att sätta prioriteter, lägga in fördröjningar o s v för att styra vilka processer som exekverar när. Ingredienser Olika sorters processer När man talar om schemaläggning brukar man skilja mellan två sorters processer. Periodiska processer utför en uppgift regelbundet, med en viss periodicitet. Det kan t ex röra sig om att läsa mätdata eller köra ett varv i en reglerslinga var 50 ms. Sådana processer har explicita tidsgränser; i synnerhet måste varje jobb vara klar inom en period. Med ett "jobb" menar vi t ex varje enskild läsning av mätdata eller varje varv i reglerslingan. Ibland kan jobben i en process ha tidigare tidsgränser än början på nästa period. Aperiodiska eller sporadiska processer reagerar på externa händelser som inte uppvisar någon regelbunden periodicitet, utan kanske ibland kan komma ofta (i en skur) och ibland inte komma alls under lång tid. Det kan t ex röra sig om en varningssignal, kommunikation med en annan process eller kommandon från en operatör. Även sådana processer är associerade med tidsgränser. I synnerhet kräver man ofta att systemet svarar på händelsen inom en viss tid. Generellt är periodiska processer lättare att schemalägga än aperiodiska, eftersom de senares beteende är svårare att förutsäga (kom ihåg: ickedeterminism!). Tidsgränser Det finns ett flertal olika sorters tidsgränser som kan vara relevanta för en process. Det vanligaste är "dödlinjer" (deadlines) som handlar om att processen måste vara klar med en viss beräkning innan en viss tidpunkt. Man kan också vara intresserad av den minimala respektive maximala tiden från det att en händelse sker till att processen börjar exekvera, eller den tid det tar för processen att exekvera, både exklusive och inklusive eventuella väntetider under exekveringen. Man skiljer på två sorters tidsgränser med avseende på hur avgörande de är: Hårda tidsgränser måste alltid hållas. En överskriden hård tidsgräns kan leda till att hela systemet fallerar eller orsaka allvarliga skador. Mjuka tidsgränser bör hållas, men man kan acceptera att de överskrids emellanåt, eller med liten marginal. En försening kan påverka effektiviteten av systemet, men ska normalt inte få katastrofala konsekvenser. Vilken typ av tidsgränser man har inverkar naturligtvis på hur man väljer att lösa schemaläggningen. Ofta har man en blandning av båda sorternas tidsgränser. Prioritet Att tilldela processer olika prioriteter är ett viktigt instrument inom schemaläggning. Det man kanske först tänker på är att använda prioriteter för att indikera hur viktig en viss process är relativt andra processer. De avgör vilka processer som får företräde när processorkraften inte räcker till för alla processer. Det är tanken bakom föregripande prioritetsbaserad schemaläggning: om en process med högre prioritet blir körklar, t ex på grund av någon händelse, så avbryter den processer med lägre prioritet. Att låta prioriteter stå för viktighet är relevant för mjuka tidsgränser, men fungerar inte lika bra för hårda tidsgränser. I det senare fallet kan man inte tillåta att någon process inte hinner med. Längre fram kommer vi att titta på hur prioriteter kan användas för att indikera hur ofta en viss process behöver exekvera, i så kallad "rate monotonic" schemaläggning. Det är vanligt att tilldela prioriteter en gång för alla innan processerna startar. Detta kallas för statiska prioriteter, och är t ex det typiska för processer i VxWorks. Det finns dock schemaläggningsalgoritmer som utnyttjar dynamiska prioriteter. Dessa schemaläggningsalgoritmer beräknar och tilldelar nya prioriteter fortlöpande. 16

Synkronisering De olika processerna i ett inbyggt system exekverar inte alltid oberoende av varandra. Ofta synkroniseras de med varandra med t ex semaforer och meddelandeköer. Detta komplicerar bilden ytterligare. Bland annat så kan det vara svårt att bedöma hur lång tid det kan ta för en process att utföra en viss beräkning om det ingår synkronisering med andra processer i beräkningen, och processen kan tvingas vänta. Detta gör processens exekveringstid svårbedömd. Ett annat knivigt problem är prioritetsinvertering. Det kan uppstå när en process P1 med hög prioritet väntar på t ex en semafor som hålls av en process P3 med låg prioritet. Om samtidigt en process P2 med mellanhög prioritet är körklar, så kommer den att ges företräde framför den lågt prioriterade P3. Eftersom P3 håller en semafor som P1 väntar på, fördröjs P1 indirekt av P2. Med andra ord, en process (P2) fördröjer exekveringen av en annan process med högre prioritet (P1) (se fig 1). Detta är naturligtvis inte en önskvärd situation. Figur 1: Inverterad prioritet. Gråa staplar visar när processen exekverar. Problemet med prioritets invertering kan i viss mån hanteras med prioritets ärvning. Detta innebär att om en process håller en resurs som en högre prioriterad process väntar på, så ärver den förra processen prioriteten av den senare. I exemplet ovan skulle detta innebära att P3 ärver P1s höga prioritet så länge som P1 väntar på semaforen som P3 håller. Därigenom kan P3 fortsätta exekvera tills den har gett semaforen till P1. I det läget återgår P3 till sin ursprungliga låga prioritet (se fig 2). Figur 2: Prioritets ärvning Prioritetsärvning är en metod som t ex används för mutex-semaforer i VxWorks. Tyvärr är metoden beroende av att man kan identifiera vilken process det är som kan ge den högt prioriterande processen (P1) det den väntar på. När det gäller en mutex-semafor så är det enkelt att förutse vilken process som kommer att ge den tillbaka: det alltid samma process som en gång tog den. Det är betydligt svårare att förutse vilken process som kan sätta igång P1 om P1 väntar vid t ex en meddelandekö, som i princip vilken annan process som helst kan skicka meddelanden på. När går det att schemalägga en grupp processer? En viktig fråga i schemaläggning är när det är möjligt att schemalägga en viss grupp processer, d v s om det går att fördela processortid på ett sådant sätt att processernas olika tidskrav uppfylls. Vi antar (när inget annat sägs) att de tidskrav vi har är att avsluta varje jobb innan nästa period. Exekveringstid För att svara på det börjar man med att uppskatta hur lång tid det tar att exekvera ett "jobb" (t ex en periodisk beräkning) av varje process. För att göra en sådan uppskattning möjligt bör man bl a: Undvika dynamisk minnesallokering. Undvika dynamiskt skapande av nya processer. 17

Se till att eventuella loopar har en känd övre gräns.1 Undvika rekursion (nästlade funktionsanrop, där en funktion kan anropa sig själv). Sätta begränsade väntetider på processkommunikation. För att sammanfatta: man bör undvika att göra saker som kan ta mycket olika lång tid vid olika tillfällen. Ibland kan t ex en process inte behöva vänta någon tid alls på en semafor, men vid andra tillfällen kan den behöva vänta väldigt länge. Därför blir processens exekveringstid svårbedömd. Framför allt kan den få en väldigt hög övre gräns. Nyttjandegrad Vet man hur långt tid det (maximalt) tar att exekvera ett jobb (e) av en periodisk process och med vilken periodicitet - d v s hur ofta - den exekverar (p) kan vi beräkna processens nyttjandegrad (N) enligt formeln: N= e p Det går också att beräkna nyttjandegraden för en grupp av processer: N totalt = e e1 e2 + +... + n p1 p 2 pn Nyttjandegraden för en grupp processer kan berätta en del om huruvida det är möjligt att schemalägga processerna så att alla tidskrav uppfylls. Vi antar att processerna är oberoende. För och främst, om N totalt > 1, d v s om nyttjandegraden är mer än 100%, så är det naturligtvis inte möjligt att schemalägga processerna på en enda processor. Hur är situationen om N totalt 1? Titta på följande processer P1, P2 och P3 (tabell 1). Process Period Exekveringstid Nyttjandegrad (ms) (ms) P1 80 40 50% P2 40 10 25% P3 20 5 25% Tabell 1: tre processer med en total nyttjandegrad på 100% Tillsammans har dessa processer en nyttjandegrad på 100%. De går dock att schemalägga så att alla tidskrav uppfylls, som bilden nedan visar (fig 3). Lägg märket till att P1 hinner exekvera sammanlagt 40 ms varje 80 ms-period, att P2 hinner exekvera 10 ms varje 40 ms-period, och slutligen att P3 hinner exekvera 5 ms varje 20 ms-period. Figur 3: Schemaläggning av processer med 100% nyttjandegrad Det är dock inte alltid som det är fallet att en grupp processer med nyttjandegrad mindre än 100% går att schemalägga om processernas prioriteter är statiska. Man kan teoretiskt visa att en grupp av n st periodiska och oberoende processer går att schemalägga på en processor om nyttjandegraden N totalt n ( 21 / n 1), vilket för stora n går mot 0.69. Med en större nyttjandegrad 0.69 < N totalt 1.0 beror det på förhållandena mellan processernas periodicitet och deras respektive exekveringstid om schemaläggning är möjligt. Ett särskilt gynnsamt fall är om processerna är enkelt periodiska. Detta innebär att för varje par av processer Pi och Pj så att perioderna pi < pj, så är pj är en heltalsmultipel Förutom eventuella loopar utanför den del av koden som utgör en "omgång". T ex vill man ofta ha en oändlig loop som går ett varv varje period. Detta är helt OK. 1 18