Tentamen Datorteknik och realtidssystem, TSEA81 Datum 2017-01-09 Lokal U1 Tid 08-12 Kurskod TSEA81 Provkod TEN1 Kursnamn Datorteknik och realtidssystem Institution ISY Antal frågor 5 Antal sidor (inklusive denna sida) 10 Kursansvarig Anders Nilsson Lärare som besöker skrivsalen Anders Nilsson Telefon under skrivtiden 013-282635 Besöker skrivsalen Cirka 9 och 11 Kursadministratör Gunnel Hässler Tillåtna hjälpmedel Inga Poäng Betyg 41-50 5 Betygsgränser 31-40 4 21-30 3 0-20 U Viktig information Alla svar ska ha en motivation om inget annat anges. Om du svarar med programkod räknas kommentarer i programkoden som motivation. Svar som ej är motiverade kan leda till poängavdrag Om inget annat anges ska du anta att schemaläggningsmetoden som används är priority based, preemptive, scheduling Om du är osäker på det exakta namnet för en viss funktion, skriv en kommentar om vad funktionen gör så kommer vi troligtvis att förstå vad du menar. (Detsamma gäller syntaxen för programspråket C.) Skriv läsbart! Oläsbar text kan inte bedömas och ger därmed inga poäng. Lycka till! 1
Fråga 1: Teori(9p) (a) (3p) Är följande påstående sant eller falskt? Den viktigaste egenskapen hos ett realtidssystem är att alla program som körs är väl optimerade och körs så snabbt som möjligt. Motivera noga! (b) (3p) Förklara vad priority inversion är och ge ett exempel på hur det kan uppkomma. (c) (3p) Namnge en vanlig mekanism och beskriv hur denna fungerar för att lösa problemet med priority inversion. Fråga 2: Schemaläggning(8p) Antag att du har följande två aktiviteter som måste köras enligt följande specifikationer på en dator som enbart har en processor: activity1(): Denna funktion måste köras var 7:e millisekund. Den tar 4 millisekunder att köra activity2(): Denna funktion måste köras var 3:e millisekund. Den tar 1 millisekund att köra Det här är utdrag ifrån källkoden till det program som är tänkt att implementera detta: void *thread1(void *unused) while(1) activity1(); // Tar 4 millisekunder att köra usleep(3000); // Sov i 3 millisekunder void *thread2(void *unused) while(1) activity2(); // Tar 1 millisekund att köra usleep(2000); // Sov i 2 millisekunder int main(int argc, char **argv) pthread_t t1; pthread_t t2; struct sched_param sp; pthread_create(&t1, NULL, thread1, NULL); pthread_create(&t2, NULL, thread2, NULL); sp.sched_priority = 1; // Högre siffra i detta fält -> processen har högre prioritet pthread_setschedparam(t1, SCHED_FIFO, &sp); // Sätt tråd 1 till prioritet 1 sp.sched_priority = 2; pthread_setschedparam(t2, SCHED_FIFO, &sp); // Sätt tråd 2 till prioritet 2 pthread_join(t1,null) pthread_join(t2,null) return 0; Visa vad som händer när detta program körs genom att rita ett tidsdiagram. Kommer specifikationerna ovan att uppfyllas? Om specifikationerna uppfylls: Beräkna hur mycket av datorns processorkapacitet som används Om specifikationerna inte uppfylls: Modifiera programlistningen ovan så att specifikationerna uppfylls (samt visa att specifikationerna uppfyllts), alternativt visa på att det är omöjligt att uppfylla specifikationerna. 2
Fråga 3: Avbrott(8p) (a) (4p) Diskutera varför det är en dålig ide att styra åtkomsten till gemensamma variabler via en semafor om ett av de ställen som behöver komma åt variablerna är en avbrotts-rutin. Nedan följer ett utdrag ur en hypotetisk applikation som visar ett icke-fungerande sätt att synkronisera åtkomst till gemensamma variabler: // Detta är en avbrotts-hanterare void gpio_interrupt(void) int val = get_gpio_port(); si_sem_wait(¶m.mutex); param.val = val; si_sem_signal(¶m.mutex); // Detta är en helt vanlig funktion som aldrig anropas ifrån ett // avbrott: int calculate_zaphod(void) si_sem_wait(¶m.mutex); int retval = param.val * 71423 / 42 + 79; si_sem_signal(¶m.mutex); return retval; (b) (4p) Visa hur programkoden ovan kan skrivas om så att ett lämpligare sätt används för att synkronisera åtkomsten till de delade variablerna i structen param. Fråga 4: Monitorer(15p) Du är ansvarig för att skriva delar av en programvara som styr lagerrobotar i ett automatiserat lager. Din uppgift är att implementera funktionen charge robot() som anropas av huvudprogrammet när aktuell lagerrobot har för lite laddning kvar i batteriet samt har åkt till det område i lagret där laddaren finns. Du ska följa specifikationerna nedan när du implementerar denna funktion: Varje robot styrs av en egen task Om fler än en robot behöver laddas så ska den robot som har minst laddning kvar i batteriet ha förtur till laddaren. Laddning kan bara ske för en robot i taget. Den synkroniseringsmekanism som ska användas i charge robot() är en så kallad monitor. Du kan välja om du vill använda SimpleOS eller pthreads för att lösa uppgiften. Det finns sammanlagt MAX ROBOTS antal robotar i systemet. När charge robot() returnerar kommer roboten automatiskt att åka ifrån laddningsområdet och återvända till sina vanliga sysslor. Till din hjälp har du följande funktioner: unsigned int get charge(unsigned int robot id) - Returnerar laddningen hos roboten med id:t robot id unsigned int my id(void) - Returnerar id för den robot som denna task styr. void activate charger(void) - Denna funktion gör så att roboten åker in i laddningsenheten och laddar roboten. Endast en robot får laddas i taget. Tips: Du vill eventuellt använda en array i stil med följande för att hålla koll på vilka robotar som behöver laddas: unsigned int want_to_charge[max_robots]; // Sätt en etta i denna array // om denna robot försöker // ladda sig. Annars sätter // du en nolla 3
Fråga 5: Operativsystem(10p) Nedanstående programlistning är ett utdrag av funktionerna sem wait samt sem post från ett hypotetiskt operativsystem: void sem_wait(struct sem *s) if( *** > 0) else void sem_post(struct sem *s) if (!empty( *** )) else Platser som markerats med *** har dock raderats. Varje förekomst av *** ska ersättas med någon av följande kodsnuttar: s->counter++ s->counter-- s->mutex s->counter s->event s->wait list s->ready list DISABLE INTERRUPTS ENABLE INTERRUPTS schedule() int task id = remove highest prio(s->wait list) ready list insert(task id) ready list remove(current task id) wait list insert(s->wait list,current task id) Din uppgift är att skriva ner de kompletta implementationerna av sem wait respektive sem post. För full poäng måste din källkod även vara kommenterad! 4
Lösningsförslag fråga 1: 1a Falskt. (1p) Motivation (2p): Den viktigaste egenskapen är att ett realtidssystem är deterministiskt/förutsägbart, t ex på så sätt att deadlines möts/hålls, och på så sätt att gemensamma resurser skyddas. För full poäng behöver man framhålla att just determinism är viktigt, samt ge exempel på vad som avses med det, enligt exempel ovan. 1b Se läroboken under avsnittet för prioritetsinversion. 1c Mekanismen som avses är prioritetsärvning (priority inheritance). Se läroboken under avsnittet för prioritetsärvning. Lösningsförslag fråga 2: Följande gäller: Tidskraven att aktiviteterna ska köras var x:te millisekund innebär INTE att de måste starta eller avsluta sitt arbete periodisk var x:te millisekund, utan att de ska starta och avsluta sitt arbete inom denna periodtid som tidskraven utgör. Något annat vore orimligt i ett realtidssystem. För full poäng behöver man: -visa att specifikationerna inte uppfylls genom att rita ett korrekt tidsdiagram (med tillräckligt lång analys) där det framgår att activity1 inte klarar sina deadlines, antingen genom att tydligt markera var dessa deadlines finns eller att i motiveringen berätta vid vilken tidpunkt activity1 missar sin deadline, samt dra en korrekt slutsats baserad på korrekta orsaker. -visa korrekt modifierad programkod med kommentarer, eller MYCKET klart och entydigt via text/pseudokod visa hur modifieringen ser ut (här har många tagit sig friheten att bara översiktligt berätta vad sleep until gör, det är ingen standardfunktion). -visa att tidsdiagrammet efter modifiering nu uppfylls, genom att göra en tillräckligt lång analys där det nu framgår att aktiviteterna håller sina deadlines, att mönstret upprepar sig efter en viss tidpunkt och att dra en korrekt slutsats baserad på korrekta orsaker. Under rättningen har det gjorts många välvilliga tolkningar i denna uppgift... Det här händer när programmet körs: thread1 thread 2 (högst prioritet) t=0 ms Körklar Kör activity2() t=1 ms Kör activity1() usleep() t=2 ms Kör activity1() usleep() t=3 ms Körklar Kör activity2() t=4 ms Kör activity1() usleep() t=5 ms Kör activity1() (klar) usleep() t=6 ms usleep() Kör activity2() t=7 ms usleep() usleep() t=8 ms usleep() usleep() t=9 ms Körklar Kör activity2() t=10 ms Kör activity1() usleep() t=11 ms Kör activity1() usleep() t=12 ms Körklar Kör activity2() 5
t=13 ms Kör activity1() usleep() t=14 ms Kör activity1() (klar) usleep() t=15 ms usleep() Kör activity2() t=16 ms usleep() usleep() t=17 ms usleep() usleep() t=18 ms Körklar Kör activity2() t=19 ms Kör activity1() usleep() t=20 ms Kör activity1() usleep() t=21 ms Körklar Kör activity2() t=22 ms Kör activity1() usleep() t=23 ms Kör activity1() (klar) usleep() osv osv Man kan i tidsdiagrammet konstatera att activity1() under den tredje körningen missar deadline vid tidpunkten 21 ms då den är klar först vid tidpunkten 23 ms. Man kan även resonera som så att tidmätningen för activity1() startar vid t=0 varpå den missar deadline readan vid t=13 ms. Korrekt analys som visar att specifikationen inte uppfylls för omodifierad kod i båda fallen ger 3p förutsatt att man korrekt motivierar sin slutsats. Problemet är att trådarna använder en sleep-funktion som sover till en relativt angiven tidpunkt. Problemet är att detta bygger på att de vet exakt hur lång tid funktionsanropen tar och att körtiden för anropet till activity1() i thread1 ej kommer att vara fem millisekunder längre eftersom körningen avbryts några gånger av thread2. En bättre lösning är att modifiera programkoden så att det ser ut ungefär som följer: // Uppdatera den angivna tidpunkten (ts) genom att lägga till angivet antal // millisekunder till ts och sov till den uträknade tidpunkten. void sleep_until(struct timespec *ts, unsigned int delay_in_us) ts->tv_nsec += delay_in_us * 1000; if(ts->tv_nsec > 1000000000) ts->tv_nsec -= 1000000000; ts->tv_sec++; // Notering: Den exakta syntaxen till detta anrop är inte viktig. // Så länge det är uppenbart vad som åsyftas här så // är det OK. clock_nanosleep(clock_monotonic, TIMER_ABSTIME, &ts, NULL); void *thread1(void *unused) struct timespec ts; clock_gettime(clock_monotonic, &ts); while(1) activity1(); sleep_until(&ts, 7); // Vänta till periodens slut // thread2 modifieras på motsvarande sätt (med fördröjningen 3 ms // istället för 7) Korrekt modifierad och kommenterad/förklarad programkod ger 2p. Då kommer körningen av programmet att se ut på följande sätt: thread1 thread 2 (högst prioritet) 6
t=0 ms Körklar Kör activity2() (hämtar originaltid) t=1 ms Kör activity1() (hämtar originaltid) sleep_until(3ms) t=2 ms Kör activity1() t=3 ms Körklar Kör activity2() t=4 ms Kör activity1() sleep_until(6ms) t=5 ms Kör activity1() (klar) t=6 ms sleep_until(8ms) Kör activity2() t=7 ms sleep_until(9ms) t=8 ms Kör activity1() t=9 ms Körklar Kör activity2() t=10 ms Kör activity1() sleep_until(12ms) t=11 ms Kör activity1() t=12 ms Körklar Kör activity2() t=13 ms Kör activity1() (klar) sleep_until(15ms) t=14 ms sleep_until(15ms) t=15 ms Körklar Kör activity2() t=16 ms Kör activity1() sleep_until(18ms) t=17 ms Kör activity1() t=18 ms Körklar Kör activity2() t=19 ms Kör activity1() sleep_until(21ms) t=20 ms Kör activity1() (klar) t=21 ms sleep_until(22ms) Kör activity2() t=22 ms Kör activity1() sleep_until(24ms) t=23 ms Kör activity1() osv osv Här kan noteras att vid tiden t=22ms så befinner vi oss i samma tillstånd som vid tiden t=1ms. Vi kan alltså konstatera att detta utseende kommer att fortsätta upprepa sig med periodtiden 21 ms. Korrekt demonstration och motivation av att specifikationerna kan uppfyllas enligt modifierad programkod ger 3p. Lösningsförslag fråga 3: 3a Då ett avbrott i princip kan komma när som helst så medför det att en semafor i ett avbrott kommer att lägga körande task på väntelistan, förutsatt att avbrottet inte får tillgång till resursen, t ex på grund av att avbrottet kommer mellan wait och signal i en task som använder samma semafor som avbrottet. Om då körande task är calculate zaphod (eller någon annan task som använder samma semafor) så blir ju effekten densamma som om man gör två på varandra följande wait (på samma semafor) i en och samma task. Dvs det uppstår i detta fall en låsning mellan avbrottet och tasken. En annan task blir ju dock körande (via schedule i wait). Alternativt, om körande task inte har nåt med semaforen att göra (det kan ju tänkas att en taskswitch har gjorts) så blir den tasken väntande (placerad i väntelistan för semaforen) av fel orsak. Överkurs: Vissa realtidssystem, t ex SimpleOS, kräver att det alltid finns en körande task, dvs om ingen annan task kör så kommer idle task att köra, men om idle task har lagts på en semafors väntelista och ingen annan task vill köra, så finns ingen task att växla till, varpå realtidssystemet får ett obestämt beteende. 3b void gpio_interrupt(void) 7
int val = get_gpio_port(); param.val = val; int calculate_zaphod(void) // De utkommenterade raderna visar var vi måste ha wait/signal om // vi behöver skydda param.val även mot åtkomst ifrån andra tasks // och inte bara ifrån avbrottsrutinen. // si_sem_wait(¶m.mutex); DISABLE_INTERRUPTS; int retval = param.val * 71423 / 42 + 79; ENABLE_INTERRUPTS; // si_sem_signal(¶m.mutex); return retval; Då vi inte vill ha wait/signal i avbrottet måste all användning av param.val istället skyddas genom att stänga av avbrott. Observera att DISABLE INTERRUPTS/ENABLE INTERRUPTS behöver placeras innanför wait/signal, eftersom implementationen av wait gör ENABLE INTERRUPTS innan retur. Lösningsförslag fråga 4: unsigned int want_to_charge[max_robots]; pthread_mutex_t charge_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t charge_cv = PTHREAD_COND_INITIALIZER; int charger_is_busy = 0; // Undersök om roboten med angett id har minst laddning kvar bland // de som faktiskt försöker ladda sig. int has_least_charge(int id) unsigned int mycharge = get_charge(id); int i; for(i=0; i < MAX_ROBOTS; i++) if(i == id) continue; if(want_to_charge[i] && (get_charge(i) < mycharge)) return 0; // Om vi gick igenom alla robotar som vill laddas utan att hitta // någon med lägre laddning så är det vi som har minst laddning! return 1; void charge_robot(void) pthread_mutex_lock(&charge_mutex); want_to_charge[my_id()] = 1; while(!has_least_charge(my_id()) charger_is_busy) 8
pthread_cond_wait(&charge_cv, &charge_mutex); charger_is_busy = 1; // Följande utkommenterade rad behövs inte för just de // funktioner som vi skriver, men kan behövas om annan typ av // funktionalitet ska implementeras. // pthread_cond_broadcast(&charge_cv); pthread_mutex_unlock(&charge_mutex); // Vi ser till att inte ha någon mutex låst medans roboten // laddas för att undvika att trådar "hänger" sig i väntan på // att roboten laddas. (Något som kan ta avsevärd tid.) Till // denna tentauppgift är det inte strikt nödvändigt, men det // är ofta en bra idé att designa sitt system så att lås inte // hålles onödigt länge. (Detta underlättar också // vidareutveckling av systemet om ny funktionalitet behöver // läggas till.) activate_charger(); pthread_mutex_lock(&charge_mutex); charger_is_busy = 0; want_to_charge[my_id()] = 0; pthread_cond_broadcast(&charge_cv); pthread_mutex_unlock(&charge_mutex); Lösningsförslag fråga 5 void sem_wait(struct sem *s) DISABLE_INTERRUPTS; // Skydda semaforens datastruktur genom att // se till så att processorn inte kan bli avbruten // när koden nedan körs. if(s->counter > 0) s->counter--; else // Om semaforen är tillgänglig behöver vi enbart räkna // ner den med ett // Semaforens räknare är noll. Se till så att processen hamnar i semaforens // väntelista och inte längre är markerad som körklar genom att ta bort // processen från listan med körklara processer. ready_list_remove(current_task_id); wait_list_insert(s->wait_list,current_task_id); schedule(); // Hitta den högst prioriterade körklara // processen och växla till denna. ENABLE_INTERRUPTS; // Tillåt avbrott igen void sem_post(struct sem *s) 9
DISABLE_INTERRUPTS; // Se ovan if (!empty(s->wait_list)) // Det är minst en process som ligger och väntar i sem_wait() // på denna semafor. Ta bort den högst prioriterade processen som // väntar ifrån semaforens väntelista och gör denna körklar. // (Vi räknar inte upp semaforen här eftersom sem_wait isåfall // direkt skulle behöva räkna ner den.) int task_id = remove_highest_prio(s->wait_list); ready_list_insert(task_id); schedule(); // Växla till den uppväckta processen om det är så // att denna har högre prioritet än aktuell process. else // Ingen väntar på semaforen just nu, räkna bara upp semaforens värde. s->counter++; ENABLE_INTERRUPTS; Notering: För full poäng räcker det inte att skriva kommentarer som enbart förklarar vad varje rad kod gör utan att sätta in raderna i ett större sammanhang. Dvs, kod som kommenteras i stil med nedan gör ingen särskild glad (oavsett om det är på tentan eller i verkliga livet). void sem_post(struct sem *s) DISABLE_INTERRUPTS; // Stäng av interrupts if(!empty(s->wait_list)) // Kolla om väntelistan är tom // osv... 10