Interrupt programs are generally included in the driver of a device. Therefore, interrupt handlers are essentially a kernel module. In the previous article, we also saw that a simple interrupt program is generally included in the driver of a device. Therefore, the interrupt handler is essentially a kernel module. In the previous article, we also saw that a simple interrupt processing process is very similar to the kernel template. However, that interrupt is the simplest interrupt. it does not use the lower half of the interrupt processing mechanism. in other articles, it also describes the lower half of the handling mechanism. here is a simple introduction!
In the above section, we use a simple example to analyze the basic structure of an interrupt program. It can be seen that the interrupt handler plays a key role in handling the interrupt and is also an essential part of the interrupt handler. However, today's interrupt handling process is divided into two parts: top half (top half) and bottom half (bottom half ). Why is an interruption divided into two parts? The following typical causes can be well explained.
1. interruption can interrupt the execution of other programs at any time. if the interrupted code is very important to the system, the shorter the execution time of the interrupt processing program, the better.
2. as we know from the above, when the interrupt processing program is being executed, it will block the interrupt requests of the same interrupt line. What's more serious is that if IRQF_DISABLED is set, the interrupt service program will block all other interrupt requests. The faster the interrupt handler is executed, the better.
In the above examples, the shorter the execution time of the interrupted service program, the better. Generally, the interrupt handler is executed in the upper half. In fact, in almost all cases, the upper half only executes the interrupt processing program. Therefore, we can think that a complete interrupt processing process is completed by the interrupt processing program and the lower half.
There is a reason for this division, because we must have a fast, asynchronous, and simple handler dedicated to responding quickly to hardware interrupt requests, at the same time, it is necessary to complete those operations that have strict time requirements. While those with relatively loose time requirements, other remaining work will be executed at any time later, that is, in the so-called lower half.
In short, the division of an interrupt processing process is mainly to reduce the workload of the interrupt processing program (of course, it is ideal to leave all the work to the lower half. However, the interrupt handler should at least complete the response to the interrupt request .), Because at least the same level of interrupt requests will be blocked during its operation, these are directly related to the response capability and performance of the entire system. During the execution of the second half, all interruptions are allowed to be responded.
Unlike the upper half, the interrupt handler can only be used. The lower half can be implemented through multiple mechanisms: tasklet, work queue, and soft interrupt. In subsequent articles on this blog, you will see that both mechanisms provide an execution mechanism in the lower half, which is more flexible than the upper half. The kernel is responsible for the execution time.
The above is a basic overview of the upper and lower parts.TaskletAndWork queueMechanism, you can have a deeper understanding of the next part of the execution.
Tasklet implementation
The tasklet (small task) mechanism is the most common method for interrupt processing in the lower half, and its usage is also very simple. As you know earlier, an interrupt program using tasklet will first execute the interrupt processing program to quickly complete the first half of the work, then, call tasklet to complete the work in the lower half. We can see that the lower half is called by the upper half, and when the lower half is executed is the work of the kernel. Corresponding to the tasklet we are talking about at the moment, in the interrupt processing program, in addition to finishing the response to the interrupt and other work, we also need to call tasklet, as shown in.
Tasklet is represented by the tasklet_struct struct. each such struct represents a tasklet. In You can see the following definition:
3 |
structtasklet_struct *next;
|
6 |
void(*func)(unsignedlong);
|
In this struct, the first member represents the next tasklet in the linked list. The second variable represents the tasklet status at the moment. generally, it is TASKLET_STATE_SCHED, indicating that the tasklet has been scheduled and is being run. this variable can also be TASKLET_STATE_RUN, indicating that it is running, however, it is only used in the case of multi-processor. The count member is a reference counter. the tasklet is activated only when its value is 0; otherwise, it is forbidden and cannot be executed. The following func variable is obviously a function pointer pointing to the tasklet processing function. the unique parameter of this processing function is data.
Use tasklet
Before using tasklet, you must first create a variable of the tasklet_struct type. There are usually two methods: static creation and dynamic creation. This official statement still makes us unable to understand what the two types of creation are. If the source code is not enough, you can understand it.
In Two macros in:
1 |
464#define DECLARE_TASKLET(name, func, data) \ |
2 |
465struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } |
4 |
467#define DECLARE_TASKLET_DISABLED(name, func, data) \ |
5 |
468struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } |
This is the two methods for creating tasklet statically. The tasklet created by the first macro is in the active state, and then suspended by the scheduling function and executed by the kernel. the tasklet created by the second macro is in the forbidden state. From the definition of two macros, we can see that the so-called static creation is to directly define a variable named tasklet_struct, assign values of parameters in the macro to each member of the name variable. Note: the difference between the two macros lies in the assignment of the name variable count member. the specific reason is described in the first part. Maybe you are confused about the initialization method such as ATOMIC_INIT. after reading the definition, you will be clear at a glance:
1 |
// In arch/x86/include/asm/atomic. h |
2 |
15#define ATOMIC_INIT(i) { (i) } |
In contrast to static creation, a tasklet is created dynamically by passing a pre-defined pointer to the tasklet_init function. The source code of this function is as follows.
1 |
470void tasklet_init(structtasklet_struct *t, |
2 |
471 void(*func)(unsignedlong), unsignedlongdata) |
6 |
475 atomic_set(&t->count, 0); |
I believe that you have read the above code, which is basically not difficult to understand. However, here we should particularly explain the atomic_set function:
1 |
// In arch/x86/include/asm/atomic. h |
2 |
35static inlinevoidatomic_set(atomic_t *v,inti) |
In tasklet_init, & t-> count is passed to this function. That is to say, the address of the count member of the atomic_t type is passed to the atomic_set function. In this function, the counter value is assigned to the member in the count variable. If we want to use I, it should be the following reference method: t-"count. I. Do you understand?
OK. You can create a tasklet using the above two methods. At the same time, you should note that both the above-mentioned creation methods have the func parameter. Through the source code of the above analysis, we can see that the func parameter is a function pointer that points to such a function:
1 |
void tasklet_handler(unsignedlongdata); |
Like the interrupt handler in the upper part, this function needs to be implemented by ourselves.
After the creation, we also need to schedule the tasklet using the following method:
1 |
tasklet_schedule(&my_tasklet) |
Through this function call, our tasklet will be suspended, waiting for the opportunity to be executed
Example
Here, we only analyze the call relationships between the two parts. the complete code can be viewed here.
01 |
//define a argument of tasklet struct |
02 |
static struct tasklet_struct mytasklet; |
04 |
static void mytasklet_handler(unsigned longdata) |
06 |
printk("This is tasklet handler..\n");
|
09 |
static irqreturn_t myirq_handler(int irq,void* dev) |
14 |
printk("-----------%d start--------------------------\n",count+1);
|
15 |
printk("The interrupt handeler is working..\n");
|
16 |
printk("The most of interrupt work will be done by following tasklet..\n");
|
17 |
tasklet_init(&mytasklet,mytasklet_handler,0);
|
18 |
tasklet_schedule(&mytasklet);
|
19 |
printk("The top half has been done and bottom half will be processed..\n");
|
From the code, we can see that by calling tasklet in the upper half, the interrupt program with loose time requirements is pushed back and executed.
Why do I still need a work queue?
The work queue is another way to push the interrupted part of the work back. it can implement jobs that cannot be implemented by some tasklets, such as the work queue mechanism that can sleep. The essence of this difference is that in the working queue mechanism, the pushed work is handed over to a worker thread) (a single core is usually handed over to the default thread events/0 ). Therefore, in this mechanism, the kernel is in the process context when it executes the rest of the interrupted work. That is to say, the interrupted code executed by the work queue shows some features of the process. The most typical is that it can be rescheduled or even sleep.
For the tasklet mechanism (the same is true for interrupt handlers), the kernel is in the interrupt context during execution. The interrupt context is irrelevant to the process, so you cannot sleep in the interrupt context.
Therefore, it is not difficult to select tasklet or work queue to complete the lower half. When the interrupted program needs to sleep, the work queue is undoubtedly your best choice; otherwise, use tasklet.
Interrupt context
To understand the interrupt context, let's look back at another familiar concept: Process context (this Chinese translation is really not very well understood, and it is much better to use the "environment ). A general process runs in the user state. if the process is called by the system, the program in the user space enters the kernel space, and the kernel indicates that the process runs in the kernel space. Because the user space and kernel space have different address ing, and the user space processes need to pass many variables and parameters to the kernel, the kernel also needs to save some registers and variables of the user process, this allows the system to return to the user space for further execution after the call is completed. In this way, the process context is generated.
The process context is the value in all the registers of the CPU, the status of the process, and the content in the stack when a process is executed. When the kernel needs to switch to another process (context switch), it needs to save all the statuses of the current process, that is, the context of the current process, so that the process can be executed again, resume the status when switching. All the work done in the above-mentioned work queue is handled by the worker thread, so it can demonstrate some features of the process, such as sleeping.
For interruptions, the hardware calls the interrupt handler through a trigger signal and enters the kernel space. In this process, some hardware variables and parameters must also be passed to the kernel. the kernel uses these parameters for interrupt processing, the interrupt context can be understood as the parameters passed by the hardware and the environment that the kernel needs to save, mainly the environment of the interrupted process. Therefore, tasklet in the interrupt context does not have the feature of sleep.
Use of Work queue
In the kernel, the following struct is used to indicate a specific job:
3 |
UnsignedLongPending;// Whether the job is waiting for processing
|
4 |
StructList_head entry;// Link the linked list of all jobs to form a work queue
|
5 |
Void(* Func )(Void*);// Processing functions
|
6 |
Void* Data;// Parameters passed to the processing function
|
7 |
Void* Wq_data;// Internal use data
|
8 |
StructTimer_list timer;// The timer used by the delayed working queue
|
The linked list of these jobs (struct) is the so-called work queue. When a worker thread is awakened, all jobs on the linked list are executed. when a job is completed, the corresponding work_struct struct is also deleted. When this work linked list does not work, the work thread will sleep.
You can use the following macro to create a task to be pushed back:
1 |
DECLARE_WORK(name,void(*func)(void*),void*data); |
You can also use the following macro to dynamically create a job:
1 |
INIT_WORK(structwork_struct *work,void(*func)(void*),void*data); |
Similar to tasklet, each job has a specific work queue processing function. the prototype is as follows:
1 |
void work_handler(void*data) |
The work queue mechanism is mapped to the specific interrupt program, that is, the pushed jobs will be executed in the work queue processing function pointed to by func.
After the work queue processing function is implemented, the schedule_work function is required to schedule the work, as shown in the following figure:
In this way, the work will be scheduled immediately. once the working thread is awakened, the work will be executed (because the task force column will be executed ).
(PS; in the previous articles, I analyzed the process of interrupt mechanism from a theoretical point of view and divided it into two parts to complete interrupt processing. In fact, it is only two steps to satisfy each condition. The actual example shows how to use the lower half of the http://oss.org.cn/kernel-book/ch03/3.3.3.htm)