Orthogonality is a property in computer systems design that increases the compactness of a system. The concept of orthogonality is that when you change one component of a system it neither creates nor propagates side effects to other components of the same system.
The goals of orthogonality is to create a simple yet complete system that is combinable. It should be simple in that making changes to one component should not force a change on another component. It should be a complete system in that it should have all the necessary components to have it run. Lastly, it should be combinable in that the user should be able to call any component and have it work in conjunction with another component, but have the components themselves independent.
To imagine OS orthogonality, visualize an xyz graph, where the x-axis represents the files API, the y-axis represents the processes API, and the z-axis represents the computer's memory. Changing any of these will not effect the other, because these axis' are perpendicular to one another.
Roughly, an Operating Systems API should have these independent feature sets, e.g. open can be called, then immediately waitpid:
Process | File |
fork | open |
waitpid | read |
exec | write |
exit | close |
The fork() system call creates a clone of the parent process, the clone is called a child.
pid_t fork(void);
// returns 0 if the fork succeeded and is now running in the child process
// returns -1 if the fork failed
// returns a value > 0 if the fork succeeded and is now running in the parent process
void printdate(void) {
pid_t p = fork();
//If for had an error
if(p < 0)
error();
//In child process
if(p == 0) {
char* args[2] = {"date", NULL};
execvp("/usr/bin/date", args);
error();
}
//Back in the parent process
int status;
//Wait for children to finish
if(waitpid(p, &status, 0) < 0)
error();
}
execlp(const char *file, const char *arg);
The exec family replaces the current process with a new process. Execlp is similar to fork() as it clones all the same things that fork() clones. However, the exec family destroys all the things that fork() keeps. This includes all the stack data and the instruction pointer. Unlike fork(), exec does not have a parent-child process – rather, the new process simply completely replaces the old process.
An example of the file and arg parameters, and execution of execlp:
void printDate (void) {
pid_t p = fork();
switch(p){
case -1: error(); //assuming we have a function error() handling errors
case 0:
execvp(“/usr/bin/date”, ((char *)[2]){“date”, NULL});
//if execvp fails, it will keep going in the process
error(); //so errors, since execvp failed
//default – this is unnecessary since we have to be parent
//process to be at this point and not hit error()
}
int status;
if (waitpid(p, &status, 0) < 0)
error(); //produces error if child returns -1
if (WIFEXITED(status) || WEXITSTATUS(status) != 0)
error;
}
The kernel contains a data structure called a file descriptor table, which contains the information for all open files. A file descriptor is one index of this data structure which contains the details of an open file. Each process has a process descriptor which then contains a file descriptor table. When an application wants to access a file, it does so through a system call. This will tell the kernel to access the file on behalf of the application. This protects the system because the application will not directly modify the file descriptor, instead it will have to go through the kernel.
In Operating Systems, there are frequently times that user input or device input is needed, such as the hard drive or the mouse/keyboard. In this case since devices tend to be slow as compared to CPU <-> RAM, system call overheads are usually recommended for the sake of robustness. However, there are many different types of devices out there.
Device Categories | Stream | Storage |
Examples | Network, mouse, keyboard | Hard drives, SSD, etc. |
Differences | Spontaneous data generation, infinite size | Request/response, random access, finite size |
In order to achieve this, we use system calls
open | close | read | write | lseek | |
Stream | ✔ | ✔ | ✔ | ✔ | ✘ |
Storage | ✔ | ✔ | ✔ | ✔ | ✔ |
void logdate(void) {
pid_t p = fork();
switch(p){
//if error in fork
case -1:
errors();
case 0:
//open file descriptor to the “log” file
int fd = open(“log”, O_WRONLY|O_APPEND);
if (fd < 0)
error();
if (fd != 1)
{
//set the stdoutput to fd
if (dup2(fd, 1) < 0)
error();
//close fd
close(fd);
}
//do the rest of log date function
default:
}
}
Notice the comment //Open the file here!
. In order to achieve this, we must utilize file descriptors and other system call functions, such as open/close. Below are both the syntax for open/close and example code:
int open(const char *path, int oflag, ... );
Where path is a string of the filename, and oflag is the flags associated with open.
int close (int fd);
Where fd is the file descriptor in which one is trying to close.
int fd = open(“log”, O_WRONLY|O_APPEND);
if (fd < 0)
error();
if (fd != 1)
{
//set the stdoutput to fd
if (dup2(fd, 1) < 0)
error();
//close fd
close(fd);
}
For each kernel, there is only a finite amount of file descriptors available to be used. Thus once all file descriptors are used, all the file descriptors are exhausted. After a file descriptor is no longer in use, it must be closed with the close(int fd) function.
Another potential problem is disconnection of file descriptor and the file itself. An example is shown below:
while (open (“/”, O_RDONLY) >= 0)
continue;
int fd = open(“file”, O_WRONLY|O_CREAT, 0666);
unlink(“file”);
write(fd, …); //doing I/O to a nameless file
read(fd, …); //doing I/O to a nameless file
A race condition is a situation in which two processes access the same “location”. This leads to problems as the two competes to see which one finishes the task earlier, which usually implies the other’s failure (but we want both to succeed!)