Operating Systemss and Multicore Programming (1DT089) Problem Set 1 - Tutorial January 2013 Uppsala University karl.marklund@it.uu.se
pointers.c Programming with pointers 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 main()
pointers.c main() and testrun
main.c Arguments to main() 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_strtol.c Convert string to integer We can use strtol() to check if any of the argument strings can be interpreted as an integer. Hmm, this doesn t handle the string 0 (zero) as the integer zero...
main_defaults.c Setting default values Now we handle the string 0 (zero) correctly. The conditional assignment operator? can be used to check if valid argument is provided or if to use a default value.
switch.c The switch statement Each case branch must be separated by the break keyword. Without the break, execution falls through to the statements 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.
Create a new process using fork() Parent Process Child Process User Space TEXT (instructions) DATA Parent calls fork() TEXT (instructions) DATA Rescources (open files, etc) Rescources (open files, etc) Kernel Space OS creates a new process - a child process The child process is a copy of the parent (copy of TEXT, DATA and Resources)
simple_fork_1.c Always check for errors!!! On error, fork() return -1 On success, fork() return twice! fork() returns 0 if executing in the new child process. fork() returns a value not equal to -1 and not equal to zero if executing in the parent process.
simple_fork_2.c Process ID (PID) pid_t is he data type for PIDs.
simple_fork_2.c main() 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 getppid() to get the callers parent PID. We use getpid() to get the callers PID. On success, fork() returns the PID of the new child process back to the parent.
simple_fork_3.c wait() 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.
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().
random_mystery.c Test run Obviously your random program is not random - what is wrong?
Random mystery unfolds... How does a PRNG work? Parent Process Child Process User Space Parent seeds the PRNG. Where is the state of the PRNG stored? srand(time(null)); n = rand(); fork(); n = rand(); Every child gets a copy of the parents- including the state of the PRNG! Kernel Space The state of the PRNG is kept in either the PCB or in some other memory area belonging to the process. OS creates a new process - a child process Every child will get the same initial PRNG state and hence generate the same PRN (sequence): The child process is a copy of the parent (copy of TEXT, DATA and Resources)
random_mystery.c 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 Take 2 - test run 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 3 - unique seeds 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 - test run By doing our best to make sure each process uses a unique seed the program works as desired.
simpsons.c exit() and wait() - talking to the dead Writing such a program should be pretty easy - don t you think?
simpsons.c exit() 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 wait() 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 Zombies 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. The process is terminated but the PCB cannot be erased yet. A process in this state is in the zombie state. After someone retrieves the exit value from the PCB, the PCB can be erased.
simpsons.c 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 Test run
southpark.c kill() and signal() The universe (parent) process killed the Kenny process (a child).
Signals A 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.
Signals Typing 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). NOTE 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.
Signals An example of code that causes a process to suspend its own execution by sending itself the STOP signal: #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);
Signals The signal() system call is used to set a signal handler for a single signal type. #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. A code snippest that causes the program to print the string "Don't do that" when a user presses Ctrl-C:
Catching signals Why do we use a static variable? Where are static variables stored? Make sure the program will terminate if CTRL-C is pressed several times. The signal() system call is used to set a signal handler for a single signal type.
Catching signals
southpark.c main() - register a signal handler
southpark.c kill_handler() 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... 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.
southpark.c Create the boys 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 boy() - code for the child processes Kenny goes into an infinite loop. Kyle waits for Kenny to terminate.
southpark.c Kill Kenny
3.3.1) Process Creation - one more time
execvp_test.c The execvp() standard C library function If execvp() successfully manages to replace the image of the process, this statement will never be reached. 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.
execvp_test.c Test run
simple_shell.c A first attempt at writing a shell 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 Test run 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.
Piping commands together How can we support command lines with multiple commands piped together?
+ Using pipe(), fork() and close() Parent Process The parent can close the read descriptor to the Child Process User Space int pfd[2]; pipe(pfd); // pfd[0] = 3 // pfd[1] = 4 fork(); pipe. Now, the parent can act as a single producer and the child as a single The child can close the write descriptor to the pipe. close(pfd[0]); consumer of data through the pipe using the read() close(pfd[1); and write() system calls - just as if it was a file. Descriptors Descriptors 0 stdin 0 stdin 1 stdout 0 read 0 1 stdout Kernel Space 2 stderr 3 pipe read 4 pipe write FIFO 1 write 1 2 stderr 3 pipe read 4 pipe write
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 we need several pipes, it s convenient to use an array where each element is a pair of descriptors. pfd is a two dimensional array, an array where each element is a two element array. 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] } if (pid < 0) { perror("child was not created"); exit(exit_failure); } if (pid == 0) { // CHILD GOES here... exit(exit_success); } else { // PARENT GOES here... } Example: N = 2 Since the pipes are created by the parent before the children are created, all children will have open descriptors to all pipes. Child 1 0 read 1 write pipe 1 read 0 write 1 Parent Child 0 0 read 1 write pipe 0 read 0 write 1
+ Attempt Conditions Result Read Empty pipe, writer attached Read blocked Write 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. 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. Child 1 0 read 1 write pipe 1 read 0 write 1 Parent Child 0 0 read 1 write pipe 0 read 0 write 1
It s a god habit to ALWAYS close any descriptors you not intend to use. 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. Child 1 0 read 1 write pipe 1 read 0 write 1 Parent Child 0 0 read 1 write pipe 0 read 0 write 1
Example: NUM_OF_CHILDS = 3 Read Write Child 0 As the number of children increases, the number of open descriptors connecting all processes through the pipes quickly become quite large. pipe 0 Child 2 pipe 2 Parent pipe 1 Child 1