A common mode in kernel programming is to initialize an activity outside the current thread and wait until the activity ends. This activity may be to create a new kernel thread or a new user space process, a request to an existing process, or some type of hardware action. In this case, we can use semaphores to synchronize the two
A common mode in kernel programming is to initialize an activity outside the current thread and wait until the activity ends. This activity may be to create a new kernel thread or a new user space process, a request to an existing process, or some type of hardware action. In this case, we can use semaphores to synchronize these two tasks. However, the kernel provides another mechanism-the completion interface. Completion is a lightweight mechanism that allows a thread to tell another thread that a job has been completed.
Structure and initialization
The implementation of completion in the kernel is based on the waiting queue (the theoretical knowledge about the waiting queue is described in the previous article). The completion structure is very simple:
Struct completion {
Unsigned int done;/* atomic weight used for synchronization */
Wait_queue_head_t wait;/* Wait for event queue */
};
Like semaphores, Initialization is classified into static initialization and dynamic initialization:
Static initialization:
# Define completion_initializer (work )\
{0, _ wait_queue_head_initializer (work ). Wait )}
# Define declare_completion (work )\
Struct completion work = completion_initializer (work)
Dynamic initialization:
Static inline void init_completion (struct completion * X)
{
X-> done = 0;
Init_waitqueue_head (& X-> wait );
}
It can be seen that the two initialization methods both set the done atomic quantity used for synchronization to 0. Later we will see that this variable is reduced by one in the wait-related functions and added to the complete series functions.
Implementation
Synchronization functions are usually paired, and completion is no exception. Let's look at the implementation of the two most basic complete and wait_for_completion functions.
Wait_for_completion is implemented by the following function:
Static inline long _ sched
Do_wait_for_common (struct completion * X, long timeout, int state)
{
If (! X-> done ){
Declare_waitqueue (wait, current );
Wait. Flags | = wq_flag_exclusive;
_ Add_wait_queue_tail (& X-> wait, & wait );
Do {
If (signal_pending_state (State, current )){
Timeout =-erestartsys;
Break;
}
_ Set_current_state (State );
Spin_unlock_irq (& X-> wait. Lock );
Timeout = schedule_timeout (timeout );
Spin_lock_irq (& X-> wait. Lock );
} While (! X-> done & timeout );
_ Remove_wait_queue (& X-> wait, & wait );
If (! X-> done)
Return timeout;
}
X-> done --;
Return timeout? : 1;
}
The complete implementation is as follows:
Void complete (struct completion * X)
{
Unsigned long flags;
Spin_lock_irqsave (& X-> wait. Lock, flags );
X-> done ++;
_ Wake_up_common (& X-> wait, task_normal, 1, 0, null );
Spin_unlock_irqrestore (& X-> wait. Lock, flags );
}
We can also think of its implementation without looking at the source code of the kernel implementation. It is nothing more than waiting for the done to become available (positive) in the wait function, while the complete function on the other side is the wake-up function, of course, it is to add one to the done to wake up the functions to be processed. Yes, as we can see from the code above, it is the same as what we think. The same is true for the kernel.
Application
Use the example in ldd3:
# Include <Linux/module. h>
# Include <Linux/init. h>
# Include <Linux/sched. h>
# Include <Linux/kernel. h>
# Include <Linux/fs. h>
# Include <Linux/types. h>
# Include <Linux/completion. h>
Module_license ("GPL ");
Static int complete_major= 250;
Declare_completion (COMP );
Ssize_t complete_read (struct file * filp, char _ User * Buf, size_t count, loff_t * POS)
{
Printk (kern_err "process % I (% s) going to sleep \ n", Current-> PID, current-> comm );
Wait_for_completion (& Comp );
Printk (kern_err "awoken % I (% s) \ n", Current-> PID, current-> comm );
Return 0;
}
Ssize_t complete_write (struct file * filp, const char _ User * Buf, size_t count, loff_t * POS)
{
Printk (kern_err "process % I (% s) awakening the readers... \ N ", Current-> PID, current-> comm );
Complete (& Comp );
Return count;
}
Struct file_operations complete_fops = {
. Owner = this_module,
. Read = complete_read,
. Write = complete_write,
};
Int complete_init (void)
{
Int result;
Result = register_chrdev (complete_major, "complete", & complete_fops );
If (result <0)
Return result;
If (complete_major = 0)
Complete_major = result;
Return 0;
}
Void complete_cleanup (void)
{
Unregister_chrdev (complete_major, "complete ");
}
Module_init (complete_init );
Module_exit (complete_cleanup );
Test procedure:
1. Create a complete node using mknod/dev/complete. manually create a file node for the driver on Linux.
2. insmod complete. ko inserts the driver module. Note that, because the device number manually assigned in our code is probably used by the system, if this happens, view the/proc/devices file. Find an unused device number.
3. CAT/dev/complete is used to read the device and call the READ function of the device.
4. Open another terminal and enter echo "Hello">/dev/complete. This command is used to write data to this device.