As some of you might know, I’m currently studying for my B.Sc. in Computer Science. One of the courses I’ve taken this semester is Operating Systems, which talks about the principles OS’s are built on. Since this is a theoretical class and the material is actually kinda cool (and the fact that I have a test this Thursday), I thought I’d share (that is – brain-dump) and in the process study for the upcoming test. It would be cool to get questions, too, so let me know if you want me to expand on something I haven’t expanded on. Of course, it goes without saying that corrections will be more than welcome.
Quick note: The main OS’s we learn about are the Unix-based systems, but most notes are not specific only to those systems.
- The initial goal of Operating Systems was, since they were used on shared computers back in the day, the ability to run multiple processes ‘at once’. This means that when a process left to do some I/O, another process got some time on the CPU, since time on the CPU was expensive. This concept is called Multiprogramming. Today, you can’t really imagine an OS without multiple processes.
- A process can be in three main logical states: Ready (process ready for execution), Running (process is currently running) and Blocked (process has called an I/O operation and can not continue until that operation completes). There are four ways a process can move between these states:
- Ready to Running – The scheduler has decided that the process should begin running.
- Running to Blocked – During the execution of the process, it waits for an I/O operation to complete (no matter how long that operation is, since I/O operations are several orders of magnitude longer than simple execution on the CPU).
- Blocked to Ready – The I/O operation has completed and the process wants to continue running.
- Running to Ready – This vector only exists in systems where the principle of Pre-emption exists. This means that the scheduler has decided that the process has had time to run and it will now let another process run. We’ll get to preemption later.
- Process Control Information – This is information for the manager, such as priority, scheduling information, signals (next paragraph), memory granted, open handles, etc.
- Process State Information – This is information used for running the process, such as the contents of its registers and the location of its stacks.
Zombie processes relinquish their child processes to the OS which is a way to prevent any more zombies (the OS waits for those child processes).
Signals (Unix and derivatives only)
- Signals are notifications to a program about events that occurred (more or less like interrupts), such as Ctrl+C (SIGINT), Segmentation fault (e.g. access to unallocated memory – SIGSEGV), A child process termination (SIGCHLD), etc. All signals are called asynchronously into your process, but suspend current execution until they are handled.
- Some signals have default implementation (SIGINT exits, SIGSEGV does a core-dump) and some are simply ignored (SIGCHLD).
- To override the defaults, you can use the signal system call and either set a function of your own to handle the signal or ignore it. For instance, to ignore Ctrl+C, call signal(SIGINT, SIG_IGN);.
- If you register a new handler for a signal, you need to re-register it in the end of the function, since some old systems revert the signal to the default once the function returns.
- Calling signals is done with the system call kill(pid, signo).
- If you want a quick way to prevent zombies in your process, ignore the SIGCHILD signal. This means that the parent process doesn’t get notified about anything that happens to its child and the child does not wait for answers from the parent. This means, however, that if you wait for children, you will keep waiting until none are running and get an error that no children are running.
- Forking a new process means that all of the signals to which we’ve set a handler are reverted to the default behavior in the new process. Ignored signals keep getting ignored. To revert a signal manually, call signal with SIG_DFL).
- A thread is the basic unit of CPU utilization, which means that it’s not a process that’s doing the running – it’s its threads. Therefore, every process has at least one thread (the main thread).
- This means that now we have to divide the metadata between a process and a thread:
- A Process has its address space, global variables (that is, non-thread specific), handles (like open files), child processes (when applicable), signals and signal handlers and some accounting information, but who cares about that :)
- A Thread, on the other hand, is an executing unit, and as such has its stack, registers, variables and current execution state. This is a part of the Thread Control Block.
- Kernel Threads (Threads in Windows) are threads supplied by the kernel, which means that the kernel controls scheduling (heavier, but this is the only way a process could run on two different processors, for instance).
- Application Threads or User-Level Thread (Fibers in Windows) are threads that the application manages and lack lots of advantages, like the fact that once one of them blocks, they are all blocked. On the other hand, the kernel doesn’t manage them, so we’re free to do with them as we like. Application Threads usually use Coroutines to manage their time.
Next topic is Scheduling, but that’s for tomorrow. My Emacs-Pinky is bothering me again. :)