[Home] [Credit Search] [Category Browser] [Staff Roll Call] | The LINUX.COM Article Archive |
Originally Published: Wednesday, 15 August 2001 | Author: Subhasish Ghosh |
Published to: develop_articles/Development Articles | Page: 1/1 - [Std View] |
Understanding Re-entrant Kernels
Undertanding the details of the Linux kernel may seem beyond the reach of many people, but at Linux.com we don't believe in "beyond the reach". In this article Ghosh begins his exploration down into the depths of the machine with a look at how a multi-threaded program can help illustrate re-entrancy, and vica versa.
|
If you ever go to a book-shop, pick up a book on "Linux Kernel Architecture" or "Unix Kernel Architecture", or maybe even a book on "Linux Kernel programming" which has topics like 'POSIX Threads', 'Processes and Signals' etc. you are bound to come across words such as 'Re-entrancy', 'Re-entrant Kernels', 'Re-entrant threads' and so on. So, what does the word "Re-entrant' mean?
This article begins to explores the depths of the Linux, in other words, the Unix Kernel architecture, in this case specifically the term "Re-entrancy". So, these are the things that will be covered:
Please Note:
Though "re-entrancy" is an advanced topic in Linux Kernel architecture, I will try to explain all other things that need to be mentioned for proper understanding of the subject matter. The example shown at the end illustrating "re-entrant routines" uses "POSIX Threads" as its core.
I have used Red Hat Linux 7.1, Linux Kernel 2.4.2-2 for compiling all the code.
Let's look into the "Unix Process/Kernel Model" in more detail. A CPU either runs in User Mode or in Kernel Mode. (Please note however, Intel 80x86 microprocessors have four different execution states.) But, all standard Unix Kernels implements the User and the Kernel Mode.
When an application is running in User Mode, it cannot directly access the kernel programs, more precisely the Kernel data structures. But, when an application is executing in Kernel Mode, there are no restrictions on accessing the kernel data structures. An application mostly executes in the User mode, and switches to Kernel mode only when it needs to utilize a particular resource managed by the Kernel. Different CPU models govern this "switching" of an application from the User to the Kernel mode. Each CPU model provides special instructions to switch from User Mode to Kernel Mode and vice-versa when required.
Now let's talk about "processes" and "threads". A process is simply an application/program in execution. The path of execution defines a "thread". And an execution context is defined as an "apartment". In many articles and books, I have come across this line: "a "thread" is a Light-weight process". I personally feel that this definition of a "thread" is an injustice to threads! It's like defining Linux in terms of Microsoft Windows 98. 'Processes' and 'Threads' are two totally different entities and should never be defined in terms of one another. (For a very detailed coverage of "POSIX Threads", please refer to one of my forth coming articles at Linux.com.)
Now, the task of creating, eliminating and synchronizing processes is controlled by a group of routines in the Kernel. Same with POSIX threads. This group of routines are referred to as "re-entrant routines". An example illustrating "POSIX Threads using re-entrant routines" will been given later.
An important point to keep in mind while dealing with Unix/Linux Kernels is: The Kernel is not a process but only a process manager. The Process/Kernel model assumes that processes that require a kernel service utilize specific program constructs called "system calls". When a "system call" is invoked by a process (an executing program), the system call sets up a group of parameters, which then identifies the process request, then hands over the power to the CPU, which then performs the earlier mentioned "switching" of the process from User Mode to Kernel Mode.
On a uniprocessor system only one process is running at any given point in time and it may run either in User or in Kernel Mode. If it runs in Kernel Mode, the processor is executing some Kernel routine. Let's look at a transition between User and Kernel Mode more closely. Suppose we have a process named Process 1 initially running in User Mode. Process 1 issues a system call, after which Process 1 is switched to Kernel Mode and the system call is thus serviced. After the system call has been serviced, Process 1 then resumes execution in User Mode. All this time (while you were reading this section), another process named Process 2 has appeared and is waiting for its turn in User Mode. A timer interrupt occurs, and the "scheduler" in Kernel Mode is activated. Thus, a process switch takes place, Process 2 starts execution in User Mode until a particular hardware raises an interrupt. As a result of this interrupt, Process 2 switches to Kernel Mode and finally the interrupt is serviced. It then again switches back to User Mode. This is how transitions take place between the two modes.
After reading to this point, the most obvious question in the minds of the readers might be: This article was supposed to explain "Re-entrant Kernels". What has all this got to do with that? Well, a lot. Since understanding the basic operations of "Processes" and "Kernel Threads" is what re-entrancy is all about. So, let's move on to the next section.
Now the question that comes up is: Okay, now we know what re-entrancy means, but how is it implemented in reality? Well, one way to provide re-entrancy is to write functions so that they modify only local variables and do not alter global data structures. Such functions are referred to as "Re-entrant functions". A Kernel that implements "re-entrant routines" are referred to as "Re-entrant Kernels". Other than using "re-entrant functions", a Kernel can also use "Locking" methods to ensure that only one process can execute a non-re-entrant function at a time.
But one very significant point to keep in mind is: In Kernel Mode, every single process acts on its own set of memory locations and thus cannot interfere with the others.
On a "Re-entrant Kernel", when a hardware interrupt occurs, it is able to suspend the current running process, even if the process is in Kernel Mode. This improves the throughput of the device controllers that issue interrupts. What exactly happens is this: When a device issues an interrupt, it waits for the CPU to acknowledge. If the Kernel answers quickly, the device controller will be able to perform other tasks while the CPU handles the interrupt.
Theoretically speaking, this is all that we can say about re-entrancy issues in a Unix/Linux Kernel. But as mentioned earlier, the real fun of learning this stuff begins when we create a program, and see the "re-entrant routines" in action. So, let's move on to the next section and get our hands dirty with some raw Linux Kernel programming.
All the re-entrancy issues that we have discussed so far are implemented in multithreaded programs using the macro _REENTRANT. We define this macro before any #include lines in our program. Including the macro _REENTRANT in the command line when we are compiling the program does three significant things for us:
/* A Simple Threaded Program: By: Subhasish Ghosh Date: August 14th 2001 Place: Calcutta, West Bengal, India */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> void *thread_function(void *arg); char message[ ] = "Hello World"; int main( ) { int res; pthread_t a_thread; void *thread_result; res = pthread_create(&a_thread, NULL, thread_function, (void *)message); if (res != 0) { perror("Thread creation failed."); exit(EXIT_FAILURE); } printf("Waiting for thread to finish....\n"); res = pthread_join(a_thread, &thread_result); if (res != 0) { perror("Thread join failed"); exit(EXIT_FAILURE); } printf("Thread joined, it returned %s\n", (char *)thread_result); printf("Message is now %s\n", message); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { printf("thread_function is running. Argument was %s\n", (char *)arg); sleep(3); strcpy(message, "Bye!"); pthread_exit("Thank you for the CPU time"); }
cc -D_REENTRANT -o thread1 thread1.c -lpthread
Waiting for thread to finish...
thread_function is running. Argument was Hello World
Thread joined, it returned Thank you for the CPU time
Message is now Bye!
pthread_t a_thread;
void *thread_result;
res = pthread_create (&a_thread, NULL, thread_function, (void *)message);
Here we pass the address of the a_thread object that refers to the instance of the newly created thread and that we can use to refer to the thread afterwards. We pass the default thread attribute, as NULL, and the final two parameters are the function to call and a parameter to pass to it.
If the pthread_create call succeeds we now have two threads running. The original thread (which is main) continues and executes the code section lying after the pthread_create and a new thread starts executing in the function thread_function. The original thread checks that the new thread has started and then calls pthread_join. pthread_join is the thread equivalent of wait that processes use to collect child processes. The function is defined in pthread.h and is declared below:
int pthread_join (pthread_t th, void **thread_return);
Here we have:
res = pthread_join (a_thread, &thread_result);
Here we pass the identifier of the thread that we are waiting for to join and a pointer to a result. This function will wait until the thread terminates before it returns. It then prints the return value from the thread, the contents of the variable and then exits.
In thread_function something else happens. The new thread starts executing in thread_function, prints out its arguments, sleeps for a short period, updates the global variables and then exits returning a string to the main thread. The new thread writes to the same array, message, to which the original thread has access. Thus, this multithreaded program illustrates re-entrancy, though in a very controlled and mild fashion.
So, this brings us to the end of the world of "Re-entrant Kernels" at least for the time being. But as we have had seen earlier, creating re-entrant routines in Linux can be a whole lot of fun.
About the Author: My name is Subhasish Ghosh. I am 20 years old, currently a computer-systems engineering student in India. I am a Microsoft Certified Professional (MCP), MCSD, MCP certified on NT 4.0, recently completed Red Hat Linux Certified Engineer (RHCE) Training & cleared Brainbench.com "Linux General Administration" certification exam. I have been working with Linux for a long time now, have had programmed using C, C++, VC++, VB, COM, DCOM, MFC, ATL 3.0, PERL, Python and Linux Kernel programming.
Latest News: Currently busy playing with POSIX threads, riding my new bike at top speed and broke-off with my girlfriend. E-mail: subhasish_ghosh@linuxmail.org