operating systems and multicore programming (1dt089) · operating systems and multicore programming...

56
Operating Systems P s and Multicore Prog Problem Set 1 - Tutoria ramming (1DT089) al January 2013 Uppsala University [email protected]

Upload: letram

Post on 21-Nov-2018

237 views

Category:

Documents


0 download

TRANSCRIPT

Operating Systems and Multicore Programming (1DT089)

Problem Set 1 - Tutorial

Operating Systems and Multicore Programming (1DT089)

Problem Set 1 - Tutorial

Operating Systems and Multicore Programming (1DT089)

Problem Set 1 - Tutorial

January 2013 Uppsala University [email protected]

The init() functions is similar to many system calls in that it takes a pointer to memory allocated by the caller as an argument.

Using this pointer, the init() function can write data (integer values) to the memory allocated by the caller.

This method is often used by system calls and allows for the system calls to write data back to user space.

This technique is referred to as call by reference.

The caller provides a reference (a pointer) as argument and the callee uses this reference (pointer) to manipulate the pointee (the memory pointed to by the pointer).

pointers.c Programming with pointers

pointers.c main()

pointers.c main() and testrun

The main function can be declared to take two arguments:

★ argc - the number of arguments when calling main, for example from the command line when testing the program.

★ argv[] - an array of strings (pointers to chars). Each arguments to main will be stored here as a string.

Loop through the argv array and print all arguments (strings).

Note that the name of the program, ”a.out”, is provided as the first argument to main.

main.c Arguments to main()

Hmm, this doesn’t handle the string ”0” (zero) as the integer zero...

main_strtol.c Convert string to integer

We can use strtol() to check if any of the argument strings can be interpreted as an integer.

The conditional assignment operator ? can be used to check if valid argument is provided or if to use a default value.

Now we handle the string ”0” (zero) correctly.

main_defaults.c Setting default values

The switch statement can be used instead of a series of if statements.

The default can be used to catch anything that doesn’t match any of the other cases.

switch.c The switch statement

Each case branch must be separated by the break keyword. Without the break, execution falls through to the statements

Use

r Spa

ceK

erne

l Spa

ceCreate a new process using fork()

Parent Process

Parent calls fork()

OS creates a new process - a child process

TEXT (instructions)DATA

Rescources (open files, etc)

Child Process

TEXT (instructions)DATA

Rescources (open files, etc)

The child process is a copy of the parent (copy of TEXT, DATA and Resources)

pid_t is he data type for PIDs.

simple_fork_2.c Process ID (PID)

The return value of fork() is assigned to the pid variable. By executing the fork() inside the switch statement, we can easily check the three possible return values.

We use getpid() to get the callers PID.

We use getppid() to get the callers parent PID.

On success, fork() returns the PID of the new child process back to the parent.

simple_fork_2.c main()

The parent uses wait() to suspend execution until one of its child processes terminates.

When the child terminates, wait returns the PID of the child process.

simple_fork_3.c wait()

Random mystery

Writing such a program should be pretty easy - don’t you think?

random_mystery.c #defines and help() message

random_mystery.c Valid arguments or use defaults?

random_mystery.c main()

Use srand() to seed the PRNG.

Parent calls rand().

Child calls rand().

Test run

Obviously your ”random” program is not random - what is wrong?

random_mystery.c

Use

r Spa

ceK

erne

l Spa

ce

Parent Process

srand(time(NULL));

n = rand();

fork();

OS creates a new process - a child processThe child process is a copy of the parent (copy of TEXT, DATA and Resources)

Random mystery unfolds...

Child Process

n = rand();

Every child gets a copy of the parents- including the state of the PRNG!

Every child will get the same initial PRNG state and hence generate the same PRN (sequence):

The state of the PRNG is

kept in either the PCB or in some other

memory area belonging to the process.

Where is the state of the PRNG stored?

How does a PRNG work?

Parent seeds the PRNG.

Take 2 - children seeds the PRNG

Parent seeds the PRNG with the current time.

Child seeds the PRNG with the current time.

random_mystery.c

Now both parent and all

children got the same ”random”

number!

The OS creates the children so fast that the time returned by time(NULL) don’t change.

Hence all processes uses the same seed and therefore generates the same PRN.

random_mystery.c Take 2 - test run

Parent seeds the PRNG with:

(current time) XOR (parent pid)

Child seeds the PRNG with:

(current time) XOR (child pid)

random_mystery.c Take 3 - unique seeds

By doing our best to make sure each process uses a unique seed the program works as desired.

random_mystery.c Take 3 - test run

Writing such a program should be pretty easy - don’t you think?

simpsons.c exit() and wait() - talking to the dead

Excellent!When a process calls exit(N), the low-order bits of N can later be retrieved by a parent process. This can be useful...

simpsons.c exit()

Excellent!

By using int* as argument to wait() the OS can use this pointer to write the exit status set by the child on exit() back to the parents user space.

We can then use the WEXITSTATUS(stat_lock) macro that evaluates to the low-order 8 bits of the argument passed to exit() by the child.

simpsons.c wait()

The process is terminated but the PCB cannot be ”erased” yet.

A process in this state is in the zombie state.

When a child uses exit() and terminates, the exit status is saved in the PCB of the child.

This makes it possible for the parent to retrieve the exit status later using the wait() system call.

Therefore, the exit value is stored in the PCB until ”someone” reads this value using wait.

After ”someone” retrieves the exit value from the PCB, the PCB can be ”erased”.

simpsons.c Zombies

An array of names for the

children.

The childred set their exit

status to their index (id) in the

name array. The parent get the name of the terminated child in a

”clever way”. The index to the name in the kids array is

obtained from the status set by the child on exit.

simpsons.c

simpsons.c Test run

The universe (parent) process killed the Kenny

process (a child).

southpark.c kill() and signal()

SignalsA signal is a limited form of inter-process communication (IPC).

Essentially it is an asynchronous notification sent to a process in order to notify it of an event that occurred.

When a signal is sent to a process, the operating system interrupts the process's normal flow of execution.

If the process has previously registered a signal handler, that routine is executed. Otherwise the default signal handler is executed.

SignalsTyping certain key combinations at the controlling terminal of a running process causes the system to send it certain signals, for example:

➡ Ctrl-C sends an INT signal (SIGINT); by default, this causes the process to terminate.

➡ Ctrl-Z sends a TSTP signal (SIGTSTP); by default, this causes the process to suspend execution.

The kill(2) system call will send the specified signal to the process, if permissions allow. Similarly, the kill(1) command allows a user to send signals to processes.

The raise(3) library function sends the specified signal to the current process.

Exceptions such as division by zero or a segmentation violation will generate signals (here, SIGFPE and SIGSEGV respectively, which both by default cause a core dump and a program exit).

The kernel can generate a signal to notify the process of an event. For example, SIGPIPE will be generated when a process writes to a pipe which has been closed by the reader; by default, this causes the process to terminate.

NOTE

#include <unistd.h> /* standard unix functions, like getpid() */#include <sys/types.h> /* various type definitions, like pid_t */#include <signal.h> /* signal name macros, and the kill() prototype */

/* first, find my own process ID */pid_t my_pid = getpid();

/* now that i got my PID, send myself the STOP signal. */kill(my_pid, SIGSTOP);

An example of code that causes a process to suspend its own execution by sending itself the STOP signal:

Signals

Signals

#include <stdio.h> /* standard I/O functions */#include <unistd.h> /* standard unix functions, like getpid(), pause() */#include <sys/types.h> /* various type definitions, like pid_t */#include <signal.h> /* signal name macros, and the signal() prototype */

/* first, here is the signal handler */void catch_int(int sig_num){ /* re-set the signal handler again to catch_int, for next time */ signal(SIGINT, catch_int); /* and print the message */ printf("Don't do that"); fflush(stdout);}

.

.

./* and somewhere later in the code.... */

/* set the INT (Ctrl-C) signal handler to 'catch_int' */signal(SIGINT, catch_int);

/* now, lets get into an infinite loop of doing nothing. */for ( ;; ) pause(); // Suspend until any signal is received.

The signal() system call is used to set a signal handler for a single signal type.

A code snippest that causes the program to print the string "Don't do that" when a user presses Ctrl-C:

Catching signals

The signal() system call is used to set a signal handler for a single signal type.

Make sure the program will terminate if CTRL-C is pressed several times.

Why do we use a static variable? Where are static variables stored?

Catching signals

southpark.c main() - register a signal handler

We cut some more corners and make all process (including the parent) register the kill_handler as the SIGUSR1 signal handler.

We do this in order to be sure the Kenny child has this signal handler installed when the Universe (parent) sends the SIGUSR1 signal.

A signal handler only takes one argument. When the signal is received, the signal

number will be used as parameter in the call to the signal handler.

We cut some corners and ”hard code” the string ”Kenny” here...

southpark.c kill_handler()

Lets look at this section of the code in

more detail.

southpark.c Create the boys

To make the code in main simple, move everything for the child

processes to a separate function.

We cannot be sure the value of kenny_pid is know by the 1st child!

Must use kenny_id ... or make sure kenny_pid is known by forking() kenny first...

southpark.c Create the boys

Kenny goes into an infinite loop.

Kyle waits for Kenny to terminate.

southpark.c boy() - code for the child processes

southpark.c Kill Kenny

3.3.1) Process Creation - one more time

An array of strings: The first string is the name of a system program (any program in your PATH), the second string is the first parameter to the program. The third string is the set to NULL (no string) to indicate ”no more parameters”.

If execvp() successfully manages to replace the image of the process, this statement will never be reached.

execvp_test.c The execvp() standard C library function

execvp_test.c Test run

The shell forks a new child for each command read from the user.

The child process uses execvp() to replace the current process image with a new process image. In this case the process image of the system program (the command) to run.

The parent waits to the child to finnish executing the command.

simple_shell.c A first attempt at writing a shell

This is a very simple shell:

★ we can only run one system program at the time.

★ we cannot give any options to the system programs.

simple_shell.c Test run

Piping commands together

How can we support command lines with multiple commands piped together?

Use

r Spa

ceK

erne

l Spa

ce

Parent Process

int pfd[2];pipe(pfd);

DescriptorsDescriptors

0 stdin

1 stdout

2 stderr

3 pipe read

4 pipe write

fork();

// pfd[0] = 3// pfd[1] = 4

The parent can close the read descriptor to the pipe.

close(pfd[0]);

DescriptorsDescriptors

0 stdin

1 stdout

2 stderr

3 pipe read

4 pipe write

The child can close the write descriptor to the pipe.

close(pfd[1);

Now, the parent can act as a single producer and the child as a single consumer of data through the pipe using the read() and write() system calls - just as if it was a file.

0 read 0

1 write 1

FIFO

Using pipe(), fork() and close()+Child Process

int pfd[N][2]; pid_t pid;int i, status;

for (i = 0; i < N; i++) { pipe(pfd[i]); } for (i = 0; i < N; i++) { pid = fork(); if (pid < 0) { perror("Child was not created"); exit(EXIT_FAILURE); } if (pid == 0) { // CHILD GOES here... exit(EXIT_SUCCESS); } else { // PARENT GOES here... } }

pipe 00 read1 write

read 0 write 1

Parent Child 0pipe 10 read1 write

read 0 write 1

Child 1

Example: N = 2

If we need several pipes, it’s convenient to use an array where each element is a pair of descriptors.

Since the pipes are created by the parent before the children are created, all children will have open descriptors to all pipes.

pipe nr read descriptor

write descriptor

0 pfd[0][0] pfd[0][1]

1 pfd[1][0] pfd[1][1]

... ... ...

N-1 pfd[N-1][0] pfd[N-1][1]

pfd is a two dimensional array, an array where each element is a two element array.

Attempt Conditions ResultRead Empty pipe, writer attached Read blockedWrite Full pipe, reader attached Write blocked

Read Empty pipe, no writer attached EOF returned

Write No reader SIGPIPE

+

Don’t forget to close unused pipe file descriptors using the close() system call.

By default, a SIGPIPE causes the process to terminate.

It’s a god habit to ALWAYS close any descriptors you not intend to use.

pipe 00 read1 write

read 0 write 1

Parent Child 0pipe 10 read1 write

read 0 write 1

Child 1

Example:

Child 0 will only WRITE data to the parent using pipe 0.

Child 1 will only READ data from the parent using pipe 1

The parent will only read from pipe 0 and write to pipe1.

pipe 00 read1 write

read 0 write 1

Parent Child 0pipe 10 read1 write

read 0 write 1

Child 1

Example:

Child 0 will only WRITE data to the parent using pipe 0.

Child 1 will only READ data from the parent using pipe 1

The parent will only read from pipe 0 and write to pipe1.

It’s a god habit to ALWAYS close any descriptors you not intend to use.

pipe 1Parent Child 1Child 2

Child 0

Example: NUM_OF_CHILDS = 3

pipe 0

pipe 2

Read

Write

As the number of children increases, the number of open descriptors connecting all processes through the pipes quickly become quite large.