Name _____________________________ CIS 46 11/17/2009 Exam 2 Answer four (4) of the first six (6) questions. Everyone must answer question 7. Extra credit (5 points) is given for answering an extra question. 1. In the questions below, explain the differences and similarities between unnamed pipes and FIFOs (named pipes). a) How is each type of pipe created? (Give the function or system call used). A pipe or unnamed pipe is created by the pipe() system call. A FIFO or named pipe is created by the mkfifo() function. b) Where are unnamed pipes stored? Where are FIFOs stored? Pipes are stored in the kernel memory and are removed when all processes using them terminate. FIFOs are stored as files in the file system in the directory that was specified when the FIFO was created. The FIFO continues to exist until removed by unlink(). c) What is the major limitation of unnamed pipes? Limitations: The two processes using the pipes must have a common ancestor or a parent/child relationship. Thus, the use of pipes is limited to processes with the same UID and the pipes are removed upon process termination. d) Taking your answer to c) in account, why would you choose to use a FIFO? A FIFO can be opened by unrelated processes and continues to exist in the file system until unlinked. 2. Consider the following code: #include #include #include #include #include char handmsg[] = "Caught a ^c\n"; void catch_ctrl_c(int signo) { write(STDERR_FILENO, handmsg, strlen(handmsg)); } int main() { ... struct sigaction act; act.sa_handler = catch_ctrl_c; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (sigaction(SIGINT, &act, NULL) < 0) perror("Appropriate message"); ... } a) Explain what is happening in this code fragment. The signal handling function, catch_ctrl_c() is specified as the parameter to the sigaction variable act. The process signal mask that is loaded when the signal handler is called is empty. No flags are specified. The system call sigaction() is used to load catch_ctrl_c() as the function to handle delivery of the SIGINT signal. b) The function catch_ctrl_c() uses write() because write() is "async_signal_safe". What does "async_signal_safe" mean and why is it important? The term "async_signal_safe" means that the function write() will function properly when there are concurrent calls to the signal handling function. That is, signal handling function may be called repeatedly due to the signal being delivered more than once. For example, the user may type ^c multiple times. c) How could the code above be changed to ignore SIGINT (^c) (you can write in words or write code)? Instead of loading a signal handling function, the parameter to the sigaction structure variable should be changed to SIG_IGN, act.sa_handler = SIG_IGN; 3. a) Explain what the Process Signal Mask (PSM) is and its affect when the kernel tries to deliver a signal. The process signal mask is the set of signals that are blocked. A signal sent to the process is not delivered until the process unblocks the signal. b) If a process "ignores" a signal, what happens when the kernel tries to deliver that signal? Can a process ignore all signals? A signal that is ignored is thrown away and never affects the process. Almost all signals can be ignored; however there are several signals such as SIGKILL, SIGSTOP and SIGLWP that can not be ignored or blocked. c) Why would a process choose to block a signal rather than just ignore it? An ignored signal is lost to the process. A blocked signal is available at a later time if the process chooses to unblock it. 4. a) The following code fragment is from a code that was discussed in class: typedef struct { char word[BUF_SIZE]; int my_state; } Info; void *speaker( Info * ); int main( int argc, char *argv[] ){ pthread_t t_ID[MAX]; Info words[MAX]; . . . pthread_create( &t_ID[i],NULL,(void *(*)(void *))speaker,(void *) &words[i]) Discuss each of the parameters to pthread_create() and why the complicated cast is necessary. The first parameter to pthread_create() will hold the returned value of the thread ID when the new thread is created. The second parameter is the thread's attributes which are set to NULL here or default attributes. The third parameter is the function that the thread should execute. The system call pthread_create() expects a function whose prototype is void *name(void *). Since the function speaker has a different prototype, it needs to be cast to the correct type. The last parameter is the parameter to be sent to speaker(). It also needs a cast to type void *. b) In every code that we discussed in class, the main() thread had code such as the following: for (i=0; i < thread_max; i++) pthread_join( t_ID[i], (void **) NULL); Is it necessary for the main() thread to join other threads? Why or why not? If a thread is joinable, all its resources are not released until another thread waits for it using pthread_join(). Any thread can wait for any other thread as long as it knows the thread ID. There is no requirement that the main thread use pthread_join(). c) Is there any advantage to running a multi-threaded code on a single processor computer? Yes. Each process receives a quantum of time. If the process has only one thread and that thread requests a slow service from the OS, the process is suspended. If there are multiple threads, another ready to execute thread can be executing while the suspended thread is waiting on the slow system call. Also, one thread can be designated to receive signals. that thread can load the signal handling functions and use sigwait(). The other threads would have the signals blocked. 5. a) Give an example of how deadlock could occur when using mutex locks? (You may explain in words or show code.) There are many ways that deadlock can be implemented. The easiest to explain is when two threads are competing for shared resources which need two mutex locks to access. The first thread A acquires mutex lock X and tries to acquire mutex lock Y. However, thread B has already acquired mutex lock Y and is trying to acquire mutex lock X. Both threads are suspended and deadlock has occurred. b) Explain why using a condition variable to coordinate multiple threads can be more flexible than using just a mutex lock. When using condition variables, a thread can not access a shared resource or a critical section of code until a condition has been satisfied. While a mutex lock is used to block access, it is released automatically if the condition is not satisfied. Another thread is then able to change the state of the condition and signal the suspended thread of the change. Using a condition variable eliminates busy waiting. c) What is starvation in a multithreaded code? Starvation occurs when one or more than one thread is denied access to a shared resource or critical section of code indefinitely. 6. a) Define the following terms: Mutual exclusion - Access to a shared resource or code fragment is allowed to only one thread at a time. critical section of code - A section of code that can be executed by a single thread at at time. atomic operation - An operation that is carried out with out any other interleaved operations. b) Explain what happens when the following conditions occur: i. A thread tries to decrement a semaphore whose value is already 0. The thread is suspended until another thread does a sem_post() operation. ii. A thread tries to increment a semaphore whose value is already 0. The thread successfully increments the semaphore value and returns immendiately. Everyone must answer the following question. 7. ( 28 points) The stand I/O library in C contains the functions: FILE *popen(const char *command, const char *mode); int pclose(FILE *stream); where "command" is the name of an executable program, "mode" refers to whether the calling process will read or write to the pipe and "stream" refers to a file pointer. The purpose of popen() is to allow a process to create another process running the program "command" which it will communicate with using an unnamed pipe. To achieve this, the function popen() needs to execute several system calls. The diagram below gives the relationships of the processes and pipe. _____________________ _________________ | original process |>---pipe---->| new process | |that executes popen()| |running "command"| a) How does the original process create the pipe? The original process does a pipe() system call. b) Once the pipe exists, what system call must be executed to start the new process? A fork() needs to be executed. c) How do we know which process is reading and which process is writing to the pipe? How do we guarantee that this happens correctly? The parameter mode indicates what action, reading or writing that the parent (original) process must perform. Depending on mode, the parent closes the unneeded pipe descriptor. The child process also has access to mode and closes the pipe descriptor that it will not be using. d) What steps and system calls are needed to ensure that the new process communicates with the original process using the pipe? The parent will be reading or writing to the pipe. It can use the function fdopen() to convert the pipe descriptor to a FILE * pointer to be returned to the calling function. The child mean while has to redirect either its standard input or standard output to the pipe descriptor. This can be done using either dup() or dup2(). e) How does the new process "run" the program "command"? The child process uses one of the exec*() functions to execute the new program, "command". f) What is the purpose of the pclose() function? The pclose() function allows the parent process to wait() for its child. Once the child process is done, the parent can close() access to the pipe without worry about a SIGPIPE being sent. g). Write a rough draft (list the functions used in the correct order) of the two function, popen() and pclose() using your answers in parts a) - f). You may add any function calls or system calls you believe are necessary. popen(): pipe(); // parent creates the pipe fork() // the child process is created if (parent) close() unneeded pipe descriptor depending on mode fdopen() the remaining pipe descriptor to obtain a FILE* return() the FILE * to the calling function if (child) close() unneeded pipe descriptor depending on mode dup() or dup2() standard input or standard output to the pipe close() the remaining pipe descriptor exec*() to run "command", some processing may be needed to extract "command" and create the argv[] array. pclose(): wait() for child close() the remaining access to the pipe