-
Inleiding
In dit onderdeel gaan we een paar niet al te grote C-programma's bekijken
om daarmee het inzicht in de werking van Linux te vergroten. Enige kennis
van de taal C is daarbij wel noodzakelijk. We veronderstellen dat de lezer
een stuk C-code kan begrijpen. De voorbeelden zijn niet al te ingewikkeld.
De C programma's zijn:
fork3.c
fork3pid.c
fork.c
zombie.c
exec.c
signal.c
pipe.c
pipe2.c
files1.c
files2.c
files3.c
Programmacode van de voorbeelden zijn de bestanden:
fork3.c,
fork3pid,
fork.c,
zombie.c,
exec.c,
signal.c,
pipe.c,
pipe2.c,
files1.c,
files2.c en
files3.c.
-
Proces creatie
Om taken van het operating system aan te roepen gebruiken we zogenoemde
systeemaanroepen, ook wel system calls geheten. Vanuit C is zo'n system
call een function aanroep.
Een system call om een nieuw proces te maken is fork(). Deze system call
zorgt ervoor dat het huidige proces zich opsplitst in twee vrijwel gelijke
kopieen.
Een eenvoudig voorbeeld is het volgende programma (fork3.c)
#include <stdio.h>
int main(void){
fork();
fork();
fork();
printf("hallo\n");
exit(0);
}
Als we deze code compileren en uitvoeren krijgen we acht keer hallo op het
scherm. De eerste keer fork levert twee processen. Die elk weer fork
aanroepen met als resultaat vier processen die alle vier zich nog een keer
fork aanroepen. Met als resultaat acht processen die elk printf gaan
uitvoeren.
Opdracht 1: Controleer het resultaat.
We hebben al eerder gezien dat elk proces onder Linux een uniek PID ofwel
proces-nummer heeft. De fork system call levert voor het orginele proces
(het parent process) als return waarde de PID van het nieuwe gecreerde
proces (het child process). Het child proces zelf krijgt de waarde 0 terug.
Opdracht 2: Voer het volgende stukje code (fork3pid.c) uit en verklaar
het resultaat.
#include <stdio.h>
int main(void){
int pid;
fork();
fork();
pid = fork();
printf("hallo van PID %d\n", pid);
exit(0);
}
Een volgend voorbeeld (fork.c) gaat wat meer in detail.
#include <stdio.h>
main(){
int pid, status, died;
switch(pid = fork()){
case -1: fprintf(stderr,"Can't fork\n");
exit(-1);
case 0: /* child process */
printf("I'm the child\n");
exit(3);
default: /* parent process */
died = wait(&status);
}
printf("child was %d\n", pid);
printf("%d died\n", died);
printf("exit value %d\n", status >> 8);
printf("exit status %d\n", status & 0xFF);
}
In dit geval wacht de parent tot de child dood gaat (exit) en vangt de status
van de child op en geeft deze weer samen met het PID van de child.
Opdracht 3: Voer het programma uit en noteer de output. Doe het nog een keer en geef het verschil met de eerste uitvoer aan.
-
Zombies, init en orphans
Als er niet op een child gewcht wordt met een wait system call zal de exit
status bewaard blijven tot deze wordt opgevraagd. Het dode proces is dus nog
niet helemaal opgeruimd en zal een zombie worden.
Het volgende stukje C maakt een Zombie, die echter na een minuut zal
verdwijnen
#include <stdio.h>
main(){
int pid, status, died;
switch(pid = fork()){
case -1: fprintf(stderr,"Can't fork\n");
exit(-1);
case 0: /* child process */
printf("I'm the child\n");
exit(3);
default: /* parent process */
sleep(60);
died = wait(&status);
}
printf("child was %d\n", pid);
printf("%d died\n", died);
printf("exit value %d\n", status >> 8);
printf("exit status %d\n", status & 0xFF);
}
Opdracht 4: Ga dit na en gebruik ps en top om de processen te bekijken.
Hoe geeft ps een zombie aan?
Het kan ook zijn dat het parent proces helemaal niet toekomt aan een
wait en zelf voortijdig het loodje legt. In dat geval wordt de child een
orphan en zal door init geadopteerd worden.
Opdracht 5: Verander het testprogramma fork.c zo dat de child een orphan wordt en kijk
met ps wat het parent proces van de child is geworden. Geef aan wat je veranderd hebt.
-
Gedaanteverwisseling met exec
Een proces kan zijn executeerbare code in zijn geheel vervangen door een
andere executable. De basis system call heet execve. Om het leven wat
eenvoudiger te maken zijn er wat bibliotheekfuncties op basis van execve
gemaakt.
Bekijk het volgende stukje C:
#include <unistd.h>
#include <stdio.h>
int main(void){
printf("Daar gaat ie dan\n");
execl("/bin/ls", "ls", "-l", "/etc", (char *)0);
printf("terug van exec? Dat ging dus mis!\n");
}
Bij uitvoeren zal de executable van dit programma zichzelf veranderen
in de executable van het commando ls. Het oorspronkelijke programma
verdwijnt (als de execl lukt) en de printf statement na de execl wordt normaal
gesproken niet uitgevoerd.
Opdracht 6: Maakt een C programma dat fork uitvoert en
vervolgens het kindproces het commando ls laat uitvoeren met execl.
-
Het afvangen van signals
Met het commando kill of een daarvoor gedefinieerde toets is een signal naar een proces te sturen. Als we geen maatregelen nemen zal zo'n signale een proces om zeep helpen.
Signals zijn ook af te vangen zoals het volgend programma laat zien.
#include <signal.h>
#include <stdio.h>
int nsigs;
main()
{
int onintr();
signal(SIGINT, onintr); /* set interryupts handler for SIGINT */
for(;;) /* forever */
sleep(100);
}
onintr(sig)
{
signal(SIGINT, onintr); /* set interrupt handler again */
printf("signal %d received\n", sig);
if (++nsigs >= 5)
exit(0);
}
Opdracht 7: Test deze code met zowel het kill commando, door het proces als achtergrondproces te starten of door toetsaanslagen bij een voorgrond proces. Beschrijf het resultaat.
-
Interprocescommunicatie met pipe
Processen kunnen met elkaar communiceren via pipes.
Het volgende stukje C laat zien hoe dat in zijn werk gaat.
#include <unistd.h>
#include <stdio.h>
#define MESLENGTH 18
char *msg = "bericht over pipe";
int main(void){
char inpbuf[MESLENGTH];
int p[2]; /* prepare for pipe */
int pid;
if(pipe(p) == -1){
fprintf(stderr,"pipe creation failed\n");
exit(1);
}
switch(pid = fork()){
case -1:
fprintf(stderr,"cannot fork\n");
exit(2);
case 0:
write(p[1], msg, MESLENGTH);
break;
default:
read(p[0], inpbuf, MESLENGTH);
printf("Read from pipe: %s\n", inpbuf);
wait(NULL);
}
exit(0);
}
Opdracht 8: Voorzie de C code regel voor regel van commentaar waarin de werking wordt uitgelegd.
In de praktijk wordt een pipe vaak gebruikt om de standaard uitvoer van een proces te koppelen met standaard invoer van een tweede proces.
Het volgende stukje C doet dat. (Let op met het commando man dup2 krijg je informatie over de dup2 system call).
#include <unistd.h>
#include <stdio.h>
#define MESLENGTH 28
int main(void){
char inpbuf[MESLENGTH];
int p[2]; /* prepare for pipe */
int pid, n;
if(pipe(p) == -1){
fprintf(stderr,"pipe creation failed\n");
exit(1);
}
switch(pid = fork()){
case -1:
fprintf(stderr,"cannot fork\n");
exit(2);
case 0:
dup2(p[1],1); /* connect stdout to pipe */
close(p[0]);
close(p[1]);
printf("bericht over pipe via printf\n");
break;
default:
dup2(p[0],0); /* connect stdin to pipe */
close(p[0]);
close(p[1]);
n = read(0, inpbuf, MESLENGTH);
inpbuf[n]=(char)0;
printf("Read from pipe: %s\n", inpbuf);
wait(NULL);
}
exit(0);
}
Opdracht 9: Maak een C programma dat een commando
ls -l via een pipe koppelt aan wc -l. (Hint: gebruikt execl in combinatie met bovenstaand voorbeeld);
-
File I/O
De systems calls die we nu gaan bestuderen hebben te maken met file-I/O.
Bestudeer met het man commando de system calls: open, close, lseek, read en write. (Bij read en write moet je het commando man vertellen dat het om de system calls gaat. Deze staan in deel 2 van de man pagina's. het commando wordt dan:
man 2 write
man 2 read
De volgende C-programma's passen deze system calls toe. We beginnen eenvoudig. Met een variant van open die creat heet en een nieuwe file aanmaakt en opent.
#include <unistd.h>
#include <stdio.h>
char filename1[] = "fileX";
char filename2[] = "fileY";
int main(void){
int fd;
if((fd = creat(filename1, 0710)) == -1){
fprintf(stderr, "creating %s failed, returnvalue is %d\n", filename1, fd);
exit(1);
}
printf("created file descriptor %d\n", fd);
if((fd = creat(filename2, 0642)) == -1){
fprintf(stderr, "creating %s failed, returnvalue is %d\n", filename2, fd);
exit(1);
}
printf("created file descriptor %d\n", fd);
exit(0);
}
creat en open geven een getal terug die aangeduid wordt als filedescriptor. Met dit getal kunnen we bij read en write aan de geopende file refereren.
Standaard zijn er al 3 filedescriptors open. Te weten standaard input (fd=0),
standaard output (fd=1) en standaard error (fd=2). Daarom zien we de
filedescriptor 3 als waarde terugkomen voor de eerste creat en 4 voor de tweede.
Opdracht 10: Voer het programma uit. Wat zijn de filedesciptoren?
Wat zijn de permissies van de files, laat zien dat dit klopt met het programma.
Een file hoeft niet aaneengesloten te zijn. Het volgende programma maakt een file aan en schijft daar de letter A in, vervolgens gaat het programma met lseek naar een positie en schrijft daar de letter B.
#include <unistd.h>
#include <stdio.h>
char filename[] = "file20M";
int main(void){
int fd;
if((fd = creat(filename, 0600)) == -1){
fprintf(stderr, "creating failed: returnvalue is %d\n", fd);
exit(1);
}
if(write(fd, "A", 1) != 1)
fprintf(stderr, "write A failed\n");
if(lseek(fd, 20000000, SEEK_SET) != 20000000)
fprintf(stderr,"lseek failed\n");
if(write(fd, "B", 1) != 1)
fprintf(stderr, "write B failed\n");
printf("created file with a hole in it\n");
exit(0);
}
Opdracht 11: Voer het programma uit. Hoe groot is het bestand volgens ls, hoe groot volgens du. Verklaar het verschil.
Bij een fork erft het childproces de filedescriptoren van de parent. Eigenlijk hebben we dit bij pipe al toegepast. Een pipe was eigenlijk een array van twee filedescriptoren. Het volgende programma laat dit nog eens zien.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
main(){
int pid, status, died, fd;
fd = creat("sharedfile", 0644);
if(fd == -1){
fprintf(stderr,"cannot create sharedfile\n");
exit(2);
}
switch(pid = fork()){
case -1: fprintf(stderr,"Can't fork\n");
exit(1);
case 0: /* child process */
write(fd, "CCCC", 4);
close(fd);
exit(0);
default: /* parent process */
write(fd,"PPPP",4);
close(fd);
died = wait(&status);
}
exit(0);
}
Opdracht 12: Voer het programma uit en laat zien wat de inhoud van sharedfile is.
Opdracht 13: Pas het programma zo aan dat pas na de fork de file aangemaakt wordt (voor zowel de parent als de child). Wat is nu het resultaat?
-
Locking
Bij het aanpassen van gedeelde variabelen door meer processen kunnen er problemen ontstaan die bekend staan onder de naam racecondities.
Om dit probleem te bestuderen hebben we vier bestanden: initseat.c, badseat.c, seatlist.c en sellseat.c.
Met deze programma's is een simpel reserveringssysteem voor vliegtuigen na te bootsen.
Het systeem wordt aangemaakt met initseat, vervolgens is met sellseat een plek op een vlucht te reserveren. Badseat doet dat ook, maar zoals de naam al aangeeft gaat het daarmee niet (altijd) goed.
Opdracht 14: Voer de programma's uit en beschrijf hoe je kunt laten zien dat sellseat het wel goed doet en badseat niet. Wat is het essentiele verschil tussen badseat en sellseat? Wat is de critical section in de code?
-
Threads, racecondities, mutex
-
Client server over een netwerk
Opdracht 15: haal de programma's client.c en server.c op
en vertaal deze. Bestudeer de source zodat je weet hoe ze aangeroepen worden. Geef in je verslag aan heo dit in zijn werk gaat en laat zien wat er gebeurd.
Opdracht 16: Haal de bestanden hangserver.c en words op.
Vertaal hangserver.c en maak contact met deze server via telnet. Normaal gebruikt telnet een standaard poortnummer. Dit moet je aanpassen aan de poort waar je server naar luistert (zie: man telnet). Zoek dit poortnummer op in de sourcecode van hangserver.c en voer hangserver en telnet uit.
Deze server bedient maar een client tegelijk. Leg uit hoe je de server moet veranderen om meer clients tegelijk te kunnen bedienen (je hoeft dit niet uit te voeren, alleen uitleggen hoe).
|