====Recap:==== * Processes - user code, instances of program. * Can be created with ''fork()'' * Process descriptors: pid, statistics, registers, address space * Important system calls pertaining to processes: ''fork()'', ''execvp()'', ''exit()'', ''waitpid()'' * ''fork()'' --yields--> original process + duplicated process * ''void exit(int)'' and ''pid_t waitpid(pid_t, int *, int)'' Q: What is the difference between ''exit(0)'' and ''return 0;''? \\ A: ''int main() { return 0; } == int main() { exit(0); }'' The only difference is that ''exit()'' can be called in any function to exit the program. Q: What is the advantage of ''exit(int)'' over using a file? \\ A: Example code and comments below. int main() { *(int*) NULL; // The OS terminates this program here. exit(1); // If we use files, this will not run and the file will not be written. } * ''exit()'' also lets you know why a program exited. The status is stored in the ''int''. * ''exit()'', ''return'', ''_exit()'' * value * kill, ... === === __Putting it all together with some example code__ int main() { pid_t child; int status; child = fork(); if(child == 0) { return 1; } else if(child > 0) { waitpid(child, &status, 0); } ... if(WIFEXITED(status)) { if(WEXITSTATUS(status)) ... .... } ==== ==== Q: What about zombie processes? \\ A: - child process becomes a child of the parent process ''init'' once former parent exits - ''init(1)'' gets rid of zombies init basically looks like this: while(1) wait(); ----The following editted by Akhilesh Singhania---- ==Multiple Processes:== How does the process of switching from the current process(P1) to a new process(P2) work? ==Context Switching:== This is defined as the method of switching from one process to another. The following steps must occur in order to successfully switch from P1 to P2. 1. Change the processor context to kernel. The context here consists of different process descriptors such as EIP, permissions, etc. 2. Save P1 registers into its processor descriptors. This is a very important step or else the information in the registers will be lost and P1 will fail to function properly when it is designated to run again. 3. Select and pick P2. The selection process of picking P2 has not been covered yet. More on this when processor scheduling is discussed. 4. Load different process descriptors from P2 to the processor such as the registers, eip, etc. 5. Context switch to P2. ==Important notes:== Only the kernel has the permissions to execute the commands to be executed for context switching. This is important to maintain the abstraction of the entire machine belonging to a given process. Therefore, the control of the processor is transferred from P1 to the kernel in the beggining of the context switching which carries out the above commands and then transfers the control to the new process(P2). Context switching is a very costly process. It requires a complete flushing of the instruction pipeline and a restart of the pipeline with the new instructions from P2. It also requires a change of address space, which is another costly process as the entire current address space of P1 is saved into it's processor descriptor and the entire address space of P2 is loaded onto the memory. It should be noted that while this is a costly process, it is also not performing any useful task from the perspective of the user. The only thing this process accomplishes is a switching from one process to another. From the point of view of the user of the computer, no useful tasks have been performed. The overhead of this process can be mitigated by coming up with wise designs for programming. For example, suppose we are writing a program to read the entire contents of a DVD in chunks. One way of accomplishing this is to read in a character at a time and process it. A typical DVD consists of about 4GBs of storage. Using this method, there will be around 4 billion context switching in the program everytime the program uses a system call to read the DVD. A smarter design would be to read larger chunks of buffer from the DVD at a time. Context Switching is a very expensive process and at times can result in a major performance bottleneck. It will not be a recommended technique for many time sensitive applications such as MP3 players, video players, etc. For such applications very smart design techniques like the one mentioned above or other structures such as threads should be employed. ==Outside resources and more information on this can be found at following:== http://www.cs.ucla.edu/~kohler/class/05s-osp/notes/notes3.html http://en.wikipedia.org/wiki/Context_switch http://osr5doc.sco.com:1997/PERFORM/context_switching_cpu.html ----End of edit by Akhilesh Singhania---- ===== ===== //(Start of section: Kunal Kundaje)// ===== File Descriptors: ===== The //file descriptor table// is found within the process descriptor and points to structures that contain details about all the files that the process has opened for reading and/or writing. By default, the first element of the file descriptor table (index 0) points to standard input ''stdin'', the second element (index 1) points to standard output ''stdout'', and the third element (index 2) points to standard error ''stderr''. When a parent process forks off a child process, the file descriptor table is //copied// from the parent's process descriptor to the child's process descriptor. This is illustrated in the diagram below. {{.:file_descriptors.png|.:file_descriptors.png}} As we can see, the standard input of both the parent and the child process points to the //same// open file structure, which maintains certain attributes like the position within the file, a reference count, and so on. ===== Pipes: ===== **Motivation:** Quite often, we need to use the output of one process and feed it in as input to another process. For example, let's say we want a list of all the file sizes on our disk, displayed in sorted order. UNIX-based systems have a built-in utility called "du" which lists file sizes, and a utility called "sort" that sorts some given input. We can combine the functionality of both these utilities to achieve our goal by first running "du" and storing the output to a file on the disk, and then running "sort" with input from that same file. However, this approach uses persistent storage, which is expensive. This is where //pipes// come in. We can string together the two utilities, du and sort, separated by a pipe, so that the output from du is piped directly into the sort's input. This is analogous to water flowing from one end of a pipe to another, which is where the name for this technique comes from. Here's the syntax for the du and sort example mentioned above: ''du * | sort'' Now, let's look at some code to see how pipes work. int fds[2]; pipe(fds); pid_t p = fork(); if(p) // child { close(3); dup2(4,0); close(4); } else // parent { close(4); dup2(3,1); close(3); } What we're essentially doing here is creating a pipe, with element 3 of the file descriptor table pointing to one end, and element 4 pointing to the other end. When the parent process forks off a child, the file descriptor table is copied, as mentioned earlier. We then use the system call ''dup2()'' to //duplicate// a file descriptor. In the parent process, we call dup2(3,1) so that ''stdout'' (i.e. element 1) points to the same end of the pipe as element 3 of the file descriptor table. This is the end of the pipe that the parent process will be //sending output to,// with input coming in from the keyboard via ''stdin'', as usual. In the child process, we call dup2(4,0) so that ''stdin'' (i.e. element 0) points to the other end of the pipe, which is what element 4 of the file descriptor table points to. This is the end of the pipe that the child process will be //getting input from,// and output will be directed to the screen via ''stdout'', as usual. Once this is done, the flow through the pipe is complete, and we can call ''close()'' on elements 3 and 4 since they're no longer necessary. The diagram below illustrates the basic setup after the code has executed. As we can see, P1 can receive input from the keyboard and send its output to the pipe, while P2 can receive P1's output from the other end of the pipe and send its output to the screen. This is exactly what we wanted. {{.:pipes.png|.:pipes.png}} ===== Threads: ===== A thread is the basic unit of CPU utilization. The main difference between processes and threads is that //threads share the same memory space,// whereas processes are isolated from each other for safety. This makes communication between threads easier that communication between processes. **Advantages of threads:** * Context-switching is more efficient because the address space is shared, and hence, there is no need to switch address spaces. * Communication between threads is easier because there is less isolation, as was pointed out above. **Disadvantages of threads:** * Since the address space is shared between multiple threads, each thread is more constrained by the amount of space that it gets. * Because threads are less isolated from each other compared to processes, one thread may stomp on another thread's data while they're both running. * Synchronization and co-ordination between multiple threads is difficult and leads to a number of potential problems and gotchas, as we'll see later in the course. \\ **Thread descriptors:** \\ Just like processes have process descriptors, threads have their own unique thread descriptors as well. The thread descriptor contains all the attributes that are copied (i.e. not shared) when the new thread is created, including thread statistics, register contents, thread state, kernel state, and wait queues. This was a basic introduction to threads. More information on threads, thread descriptors, and thread interfaces will follow in the next lecture. //(End of section: Kunal Kundaje)//