Table of Contents
System Calls
Before getting into threads, we have to first understand what are system calls.
Let us first understand, how memory looks in the computer system and where Operating System code is present in the memory and how the processes stay in the memory too.
From the picture above it can be seen that the process P1 is loaded in the memory at a location and similarly there is another process P2 which is also loaded in the memory at some other location.
Now, the thing is that:
A process should never access any memory location other than what is actually allocated to it, until and unless proper mechanism and permissions are provided to it.
But if this is the case every user-process cannot perform that much task because most of the things are managed by Operating System itself.
Example: Code for accessing the keyboard is written as a part of Operating System (or kernel) itself. So, every time user-level process has to call that module from the OS code.
Note: The user-level process calling a module present as a part of OS code is known as System Call.
Note: A system call, is same as the normal function call but in a system call, a user-level process is trying to call the function present in the code of Operating System.
Some systems or different operating systems have different mechanisms of implementing this system call procedure. Some give direct access to H.L.P (high-level programs) to run that code (module = example = keyboard) from the code of OS.
Example: In Linux, if a C-program is written for performing some manipulations on a file present in the system, it can be done with the help of the following system calls present in the OS code (Linux kernel):
- write() system call
- read() system call
- open() system call
- close() system call
Similarly, some systems (OS’s) provide a different way to do that.
Question: What they do?
Answer: They have a system library or local library and the library system call is going to implement the system call for the user-level process.
Example: In C-language, printf() is the function which is loaded when the program is executed (during execution called as a process) and whenever during the execution of a program, a call to printf() is made then it internally calls write(). So, this is how a user-level program calls the function present in the system library (which is printf() in this case) and in turn, internally that printf() code raises a system call write().
Question: What is the problem with the process mentioned above?
Answer: Always a context-switch is involved which is a big overhead.
Question: What kind of context switching is involved here?
Answer: Whenever a user-level process wants to access the code which is a part of OS in order to perform some tasks then the CPU has to pause the current execution of the program and has to go to OS to request that piece of code, which is requested and after getting that it has to again come back to where it has paused the execution of the program which has requested that code. So, this is what context switching looks like in a system call.
Question: Do we have any better mechanism to avoid that context-switching?
Answer: Technically, NO, we don’t have a better mechanism to completely avoid that context-switching but later in this post, we are going to look at a more reliable and fast mechanism in order to somehow avoid this context-switching.
Question: Why we cannot give the direct access of OS code required to a user-level process?
Answer: Security is a big concern here. Therefore, this protected manner of calling a system call by context switching is used.
Now that we have gone through the system calls, let us give a go through to a system call named as fork() in order to understand the concept more deeply and then we will lead towards threads.
Let us understand “why threads?” by examining fork() system call.
fork() = system call
A typical program using a fork() system call is shown below:
{
int i = fork();
if (i == 0)
{
// child process
}
if ( i != 0)
{
// parent process
}
}
Line 2 = here whenever the program is executed, fork() will get called and after that, it will return some values:
- 0 (zero) to the child process
- pid of a child to the parent process
- otherwise, -1 as a value is returned to the parent process stating that no child process is created.
Line 4 to 7 = this section of code is going to be executed if the fork() returns a value = 0 and it a block to be executed for the child process
Note: If a smart move in programming is made, then this section can be used to execute another program/code which can be written to this section.
Example: A Bash shell (Bourne again shell) in Linux can be a good example for concept explained in the note above, which says that whenever we run a command (it can be any, like = ls, cat, ps et cetera) in the Bash then that command’s code is actually substituted at the place of lines from 4 to 7 mentioned in the above program and a written example is shown below:
{
int i = fork();
if (i == 0)
{
execve(/bin/ls);
}
if ( i != 0)
{
// parent process
}
}
From the code above, execve() system call is used to execute a system binary, which in this case is the binary file location for ls command.
Summary for fork() and execve() system call:
Whenever a process P1 wants to execute or access a code or utility/program which is a part of the operating system’s code, then a fork() system call is made and that in turn will provide an exact code of caller program and will place it in a new location in the memory and named it as process P2 (say) and after that it will substitute the code for the requested utility (like ls, cat) in the place of code from the lines 4 to 7 (it can be anything, but in our example the code for child is going be in the location from 4 to 7). So, as a result:
- P1 = will be the parent process
- P2 = will be the child process
Question: Why this behaviour as explained above?
Answer: There are certain things which are considered for stating such behaviour:
- Isolation between processes is provided
- Security is good, as no process is accessing any irrelevant location in the memory.
Note: Although context-switching is a big overhead, still this technique is used most of the times.
Question: Is there any better solution or method?
Answer: Yes, it is known as threads.
Question: Why use threads or why threads are used?
Answer: Let us assume that there is a web-sever which is continuously listening on a port number for requests from the clients accessing the website which is delivered by the web-server, then what used to happen before threads are introduced is – everytime a new client made a request, a copy of the server’s process is created to deliver the content requested by the client and this goes on and on.
Question: What kind of context switching is actually involved here?
Answer: Switching from user mode to kernel/os mode.
Problem? = Not copying, but the context switching required on each request is a big overhead and in order to avoid that threads are introduced.
One more example before getting into threads is of our web server and client architecture.
Explanation of the above picture:
Initially, there is only one process running as a web server, afterwards whenever a first client request came, then fork() is executed and a new copy of web server process is created and placed somewhere in the memory and then that copy 1 is going to handle that client’s request. Moreover, whenever another request came then again a new copy of initial web server is created and is used to handle the next request and this goes on and on.
Question: Problem with above example and process creation with the fork()?
Answer: From the picture above we can see that there is a DATA part and a CODE part and these will remain the same throughout the multiple copies of the original web server process. So, there are two basic problems with this approach:
- CPU heavy = Because more the number of copies, more context switching is required.
- Storage heavy = Because whenever a new copy is created, the same thing (data + code) is present in all the copies of the same process.
Note: Above explained is a concrete example of what multi-programming looks like in a single CPU system.
Note: To avoid problem occurring above, we use threads and more specifically multi-threading.
Question: Why multi-threading and not multi-programming?
Answer: They both have their advantages:
- Multi-programming = Whenever parallelism is required this is used. In this concept, we only have one CPU but we are able to perform many tasks at the same time and processes/tasks will context switched by CPU at different intervals of time. It is an expensive task. It is useful when want to perform independent tasks.
- Multi-threading = Whenever the same thing is request again and again like the code of web server which is only coded to deliver basic HTML, then multi-threading is used.
A conclusion from the above example:
We use multithreading in our example of web server because its basic task is to server normal/basic HTML and for each client. So, there is no benefit in creating a child process for each and every client request. But if we still use that, it will be a big overhead and performance decreasing. Therefore, going with multi-programming is a disadvantage because we don’t have different tasks for each request and for the same code. So, as a good solution, we can use multi-threading.
Threads
Whenever we are talking about threads, we use the term task instead of the term process.
A thread is basically a part of a process which wants to perform several specific things. It is generally defined as the smallest set of instructions used for a particular task.
There are two types of threads:
- User-Level threads
- Kernel-Level threads
User-Level Threads
It is created by a user process moreover, there is a unique thing about them, that kernel does not know anything about them and will treat them as a single threaded process or a single process in a system. They are managed by the user-level library (runtime environment).
Problems in user-level threads
- Blocking a system call will block the whole task (process): It means if a user-level thread tries to generate a system call (for example) for accessing the Hard Disk Drive (HDD), then maybe at that time the HDD is used by another process (basically if it was already busy) then as a result the system call generated will get blocked by the Operating System. As a result, the whole multi-threaded task also gets block for the same time because all enabling permissions for a system call is under OS (basically a kernel) and we know a kernel does not know anything about a user-level thread, so as a result, it will only see it like any other process in the system and bocks its system call request (which is actually made by a single thread enabled in it).
- Unfair Scheduling: It means if the time quantum = 2 milliseconds is used with the round-robin scheduling algorithm which is in turn used by the OS. Then if a task T1 contains threads = 100 and another task T2 contains threads = 2 so as a result, if it is a user-level thread the OS (kernel) will give both the tasks same time to execute, based on what is the time quantum used. So, a task with 100 threads in it will get only one chance to execute (any of its threads for a time quantum of 2 milliseconds only) after that there is a turn of the second task T2 with two threads only. This is happening because OS is treating it as it is a single process.
Kernel-Level Threads
In this, if the thread from a multi-threaded task tries to create a system call and if the system is busy with another task (process) then the call made will be blocked by the OS.
Question: Difference between user-level threads and a kernel-level thread?
Answer: In a kernel-level thread, the kernel of the system (OS) already has meta-data about the thread then by having that knowledge, it can identify which thread, from which task (process) has raised a system call and as a result, it will only block that particular thread (which raised a system call) and not the whole task (process).
Note: In kernel-level threads, there is no unfair scheduling i.e task with a larger number of threads might have a have a higher time quantum.
Problems in kernel-level threads
- Expensive compared to user-level threads because system calls can be made.
- Context switching a kernel-level thread requires system calls.
Summary
This is the summary for what we have learned in this particular post, about a system call, fork(), execve() and threads.
Comment here