Vad händer när man kör ett program? När man kompilerar ett program och sedan länkar ihop det så stoppar länkaren in lite extra kod i programmet. Det är denna kod som i sin tur anropar main-funktionen ungefär som ni anropar underfunktioner. Den extra kod som kommer in har bland annat två syften: Se till att ditt C program får rätt omgivning att köras i och att städa upp efter ditt program när det är klart. Funktionsanrop Vad händer vid ett funktionsanrop (Principiell beskrivning) Man lägger upp nuvarande adress och parametrarna på stacken, sedan hoppar man iväg till funktionen. När denna funktion är klar så plockar man fram den gamla stackpekaren och den gamla adressen och återgår till att exekvera den koden. På samma sätt med main Den extra koden gör alltså likadant för main och ser till att kommandoradsargumenten går att nå som argument till main. Kommandoradsargument Här gäller samma sak som vid ett vanligt funktionsanrop; extrakoden läser in argumenten och lagrar dem i en strängarray, sedan så skickar den upp antalet argument och pekaren på stacken följt av ett anrop till main Avsluta programmet Normal Ett program kan avslutas normalt, dvs genom att man "rasar ut ur" main, man gör från main, man anropar eller _. Denna rutin städar upp lite granna efter ditt program innan den låter kärnan ta över igen. _ Här sker ingen städning alls utan man hoppar direkt tillbaka till kärnan Onormal Detta sker genom ett anrop till abort eller att en signal gör att programmet avslutas. Vi kommer att gå igenom detta på en senare lektion. Exithandlers Genom funktionen at så kan man registrera funktioner som exekveras sedan man har anropat. Se exempel i boken 7.3 _ or _Exit _ or _Exit Figure 7.2 How a C program is started and how it terminates user functions main function C start-up routine exec function _ user process handler... handler standard I/O cleanup Minneshantering Som ni känner till så kan man få mera minne till variabler etc vid behov. I C gör man det genom att anropa någon funktion i alloc-familjen malloc Den kanske vanligaste är malloc som ger dig en pekare till ett minnesutrymme av en viss storlek. Innehållet i minnesutrymmet är odefinierat när du få tillgång till det. oc oc ger dig möjlighet att får utrymme för X antal objekt av storleken Y. Det utrymme då får har "nollats realloc Om redan har ett utrymme för data men behöver ändra storleken så kan du använda dig av realloc. Notera att det existerade datat kan flyttas om så använd dig av den nya pekaren. Innehållet i det eventuella nya utrymmet är odefinierat Minneshantering forts. alloca alloca är lite speciellt i och med att den plockar utrymmet från stacken i stället för heapen. Det innebär att man inte behöver göra en free när rutinen är klar men å andra sidan så kan man bara använda den i funktionen eller i rutiner som anropas av den. free När man är klar med ett minnessegment så kan man lämna tillbaka det genom att anropa free
Processer Processer i kärnan Varje program körs i sin egen process. Använd getpid för att få reda på identifikationsnummer. Barn En process kan yngla av sig ("spawn") sub-processer genom att använda funktionen fork. Processerna kommer direkt efter anropet att se identiska ut (utom vad gäller returvärdet från fork) De delar ej minne med varandra. Innehållet i minnet kopieras ofta först då vi försöker förändra det (för att undvika att kopiera saker i onödan) Race conditions Det finns en uppenbar risk när fler än en process kör och de kommunicerar med varandra. Det är att processerna hamnar i en "tävlan" om vilken som kommer först till en viss punkt i koden. Detta kan ställa till stora problem (vi kommer att titta mer på detta senare i kursen). #include <stdio.h> #include <unistd.h> #include <sys/types.h> int var = 1; fork, exempel 1 pid = fork(); pid = fork(); printf("pid = %d, var = %d\n", getpid(), var); (0); fork, exempel 2 int var = 1; if ((pid = fork()) < 0) fprintf(stderr, "Error creating \n"); else if (pid == 0) /* Child */ var++; else /* Parent */ var--; printf("pid = %d, var = %d\n", getpid(), var); (0); char buf[] = "a write to stdout\n"; int size = sizeof(buf)-1; if (write(stdout_fileno, buf, size)!= size) printf("\n"); /* don't flush stdout */ if ((pid = fork()) < 0) else if (pid == 0) /* */ printf("\n"); else sleep(2); /* */ printf("\n"); printf("\n"); (0); testkörning kill och ps boule[2126] % a.out a write to stdout boule[2127] % boule[2127] % a.out > tst.txt boule[2128] % cat tst.txt a write to stdout boule[2129] % ps listar processer som körs. Kolla man ps för lämpliga flaggor (varierar lite mellan systemen) kill Kan användas för att avsluta en process kill pid
exec Man kan starta ett program genom att först göra en fork och sedan göra ett exec-anrop som i sin tur startar programmet. Det intressanta är att detta nya program kör i den process som skapades av fork-anropet. Det finns flera olika exec-funktioner. Beroende på vad man vill göra, hur man vill skicka med parametrar och vilka omgivningsvariabler man vill att programmet ska använda sig av Exec forts. int execl(const char *path, const char... /* (char *)0); int execle(const char *path, const char... /* (char *)0, char * envp[] */ ); int execlp(const char *file, const char... /* (char *)0); exec forts. execlp execl execle build argv build argv build argv execvp execv execve try each PATH prefix use environ int main(int argc, char *argv[]) if ((pid = fork()) < 0) else if (pid == 0) /* Child */ execl("/home/staff/mr/bin/envir", "MYprog", "umu", (char *) 0); if (waitpid(pid, NULL, 0) < 0) 0; pointer environ: list NULL strings HOME=/Home/staff/mr\0 PATH=/usr/local/bin:/usr/bin\0 SHELL=/usr/local/bin/tcsh USER=mr PRINTER=astrid getenv(const char *name) funktion för att läsa av ett värde som finns i en variable putenv(const char *str) tar en sträng på formen name=value och lägger till den till omgivningen setenv(const char *name, const char *value, int rewrite) unsetenv(const char *name) tar bort en definition av name Figure 7.5
wait high address low address stack heap uninitialized data initialized data text command-line arguments and variables initialized to zero by exec read from program file by exec Figure 7.6 pid_t wait(int *status) pid_t waitpid(pid_t pid, int *status, int options) ett anrop till wait/waitpid kan: blockera om alla barn fortfarande körs era omedelbart med statusen för barnet era omedelbart med en felkod (om processen saknar barn) Om barnet avslutas innan föräldern lagras info om processen (bla status) tills dess föräldern avslutas eller anropar wait för att läsa av infon barnet blir en zombie process Om föräldraprocessen avslutas innan barnet kommer föräldrarollen att övertas av init (pid 1) Macron för att undersöka statusen WEXITSTATUS(status) Tar fram status WIFEXITED(status) Kollar om processen avslutades normalt WIFSIGNALED(status) Sant om status indikerade att processen avslutades felaktigt (genom en signal) WIFSTOPPED(status) Sant om barnet stoppat Pipe Ett sätt kommunicera mellan olika processer är att använda s. Det finns flera sätt att kommunicera på (se sid 496), men s är den äldsta metoden och finns implementerat på alla system. Ett sätt att se på en är just som ett enkelriktat rör, man skickar in på ena sidan och plockar ut från den andra. För att skapa en så använder man funktionen int ( int filedes[2]) som erar två stycken 'file descriptors'. Den file descriptor som finns på index 0 är "läsändan' och den på 1 är "skrivändan". Skapa en Säg att jag nu vill skapa att ett program ska skapa en barnprocess och sätta upp en till den så att föräldern kan skicka information till barnet. Då kan man göra på följande sätt: 1. Anropa för att skapa själva n 2. Gör en fork för att få barnprocessen. Kom ihåg att barnet får en kopia av förälderns variabler etc, dvs barnet kommer att ha tillgång till de file descriptorer som n har. 3. Nu bör barnet stänga sin "skrivända 4. Och föräldern bör stänga sin "läsända" Figure 15.2 Half-duplex user process
Figure 15.3 Half-duplex after a fork Figure 15.4 Pipe from to fork Föräldern stänger läsänden och barnet stänger skrivänden.