Timeline
Timeline
2025-11-13
init
This article introduces the core kernel tools and mechanisms in advanced Linux character device driver development, discussing in detail the principle of the container_of macro, the creation and operation methods of kernel circular doubly linked lists, as well as the process sleep mechanism and the initialization and implementation of wait queues.
Linux Driver Notes
| Table of Contents | Links |
|---|---|
| 1. Linux Driver Framework | |
| 2. Linux Driver Loading Logic | |
| 3. Character Device Basics | |
| 4. Concurrency and Race Conditions | |
| 5. Advanced Character Device Progression | |
| 6. Interrupts | |
| 7. Platform Bus | |
| 8. Device Tree | |
| 9. Device Model | |
| 10. Hot Plug | |
| 11. pinctrl Subsystem | |
| 12. GPIO Subsystem | |
| 13. Input Subsystem | |
| 14. Single Bus | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. Network Device | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
Kernel Tools and Helper Functions
container_of Macro
1 | // include/linux/kernel.h |
ptrPointer to a member within the structure.typeTarget structure type.memberName of the member in the structure.BUILD_BUG_ON_MSG(...)Compile-time type check to ensure ptr matches the member type, preventing type errors.__mptr - offsetof(type, member)Core formula: Subtract the member’s offset within the structure from the member pointer to get the structure’s starting address.offsetof(type, member)Standard macro to calculate the byte offset of a member within a structure.
container_ofConsider the offset of name from the start of the structure to obtain the correct pointer position. From the pointer__mptrsubtract the offset of the field name to get the correct position.
Linked list
A driver manages multiple devices. Assuming there are 5 devices, it may be necessary to track each device in the driver, which requires a linked list.
Kernel developers only implemented circular doubly linked lists because this structure can implement FIFO and LIFO, and kernel developers aim to keep code minimal. The header file to add in the code to support linked lists is<linux/list.h>. The core data structure for linked list implementation in the kernel isstruct list_head, defined as follows:
1 | struct list_head { |
struct list_headUsed in the list head and each node. In the kernel, before representing a data structure as a linked list, the structure must embed thestruct list_headfield.
Example
Let’s create a car linked list:
1 | struct car { |
Before creating the car linked list, its structure must be modified to embed thestruct list_headfield. The structure becomes as follows:
1 | struct car { |
Createstruct list_heada variable that always points to the head (first element) of the linked list. This instance of list_head is not associated with any car; it is a special instance:
1 | static LIST_HEAD(carlist); |
Now you can create cars and add them to the linked list carlist:
1 |
|
Creating and initializing a linked list
There are two methods to create and initialize a linked list.
- Dynamic method
The dynamic method consists ofstruct list_headinitialized with theINIT_LIST_HEADmacro: ·
1 | struct list_head mylist; |
INIT_LIST_HEAD is defined as follows
1 | /** |
- Static method
Static allocation is done via the LIST_HEAD macro:
1 | LIST_HEAD(mylist) |
LIST_HEAD is defined as follows
1 |
This assigns each pointer (prev and next) within the name field to point to name itself (just like INIT_LIST_HEAD does)
Creating a linked list node
To create a new node, simply create an instance of the data structure and initialize the embedded list_head field. Taking a car as an example, the code is as follows:
1 | struct car *blackcar = kzalloc(sizeof(struct car), GFP_KERNEL); |
Adding a linked list node
Provided by the kernellist_add()Used to add a new item to the linked list, it is an internal function__list_addwrapper of:
1 | /* |
The following example adds two cars to the linked list:
1 | list_add(&redcar->list, &carlist); |
This pattern can be used to implement a stack. Another function for adding a node to the linked list, the code is as follows:
1 | /** |
This will insert the specified new item at the end of the linked list. For the previous example, the following code can be used:
1 | list_add_tail(&redcar->list, &carlist); |
This pattern can be used to implement a queue
Deleting a linked list node
Linked list handling in kernel code is a simple task. Deleting a node is straightforward:
1 | /** |
Delete the red car:
1 | list_del(&redcar->list); |
list_del disconnects the prev and next pointers of the specified node, removing it. The memory allocated to this node must be manually freed using kfree.
Linked list traversal
Using the macrolist_for_each_entry(pos, head, member)for linked list traversal.
pos: Used for iteration.head: The head node of the linked list.member: The data structure (in our case, it is thestruct carlist defined in ) of the linked liststruct list_headname.
1 | struct car *acar; /* Loop counter*/ |
list_for_each_entrydefined as follows:
1 | /** |
Kernel sleep mechanism
A process releases the processor through the sleep mechanism, allowing it to handle other processes. The processor may sleep due to sensing data availability or waiting for resource release.
The kernel scheduler manages the list of tasks to be run, which is called the run queue. Sleeping processes are no longer scheduled because they have been removed from the run queue. A sleeping process will never be executed unless its state changes (wake-up). Once a process enters a waiting state, it can release the processor, but it must be ensured that there is a condition or another process to wake it up.
The Linux kernel simplifies the implementation of the sleep mechanism by providing a set of functions and data structures.
Wait queue
Wait queues are actually used to handle blocked I/O, waiting for a specific condition to be met and sensing data or resource availability.
The wait queue is a kernel mechanism for implementing blocking and wake-up,The wait queue is based on a doubly circular linked list structure, where the list head and list node represent the wait queue head and wait queue element respectively.

Definition
include/linux/wait.h
1 | typedef struct wait_queue_entry wait_queue_entry_t; |
Initialize wait queue
| API | Purpose | Description / Parameters | Notes |
|---|---|---|---|
DECLARE_WAIT_QUEUE_HEAD(name) | Static definition of wait queue head | Generate a wait queue head namednameglobal/static wait queue head | most common method |
init_waitqueue_head(wq) | dynamically initialize wait queue head | suitable for wait queue head in dynamically allocated structures | memory must be allocated first |
wait queue entry API
these APIs are mostly not needed; task is usually current
| API | purpose | description / parameters | notes |
|---|---|---|---|
DECLARE_WAITQUEUE(name, task) | statically create a wait queue entry | taskusually fill incurrent | global or static scenario |
init_waitqueue_entry(&entry, task) | dynamically initialize queue entry | task is usuallycurrent | Dynamically create item |
add_wait_queue(head, entry) | Add waiting item to wait queue | Non-exclusive wait | Mostly used for low-level calls (not recommended for direct use) |
add_wait_queue_exclusive(head, entry) | Exclusive wait | Only wake up 1 exclusive process when waking | Used for blocking queues such as write operations |
remove_wait_queue(head, entry) | Remove item from wait queue | and add_wait_queue pairing |
Sleep wait API
| API | Purpose | Description | Return value / Notes |
|---|---|---|---|
wait_event(wq, condition) | Condition not met → sleep (non-interruptible) | Return if condition is true | Cannot be interrupted by signals |
wait_event_timeout(wq, condition, timeout) | Uninterruptible sleep + timeout | Timeout is in jiffies | Returns remaining jiffies or 0 on timeout |
wait_event_interruptible(wq, condition) | Interruptible sleep | Can be interrupted by signals | Returns when interrupted-ERESTARTSYS |
wait_event_interruptible_timeout(wq, condition, timeout) | Interruptible + timeout | Returns: >0 remaining time, 0 timeout, <0 interrupted by signal | |
wait_event_killable(wq, condition) | Only interrupted by fatal signal | Safer than interruptible | Interrupted by kill signal |
wait_event_killable_timeout(wq, condition, timeout) | killable + timeout | Same as above |
wait_event_interruptibleDoes not continuously poll, but only evaluates the condition when called. If the condition is false, the process will enterTASK_INTERRUPTIBLEstatus and remove it from the run queue.
After that, each time when calling in the wait queuewake_up_interruptible, the condition is rechecked. Ifwake_up_interruptiblethe runtime finds the condition true, the process in the wait queue will be awakened and its status set toTASK_RUNNING. Processes are awakened in the order they went to sleep.
To wake up all processes waiting in the queue, you should usewake_up_interruptible_all
Ifwake_uporwake_up_interruptibleis called and the condition is still FALSE, nothing happens. Ifwake_up(orwake_up_interuptible) is not called, the process will never be awakened.
wait_event、wake_up and wake_up_all. They handle processes in the queue with exclusive (uninterruptible) waiting because they cannot be interrupted by signals. They should only be used for critical tasks.
Interruptible functions are optional (but recommended). Since they can be interrupted by signals, their return value should be checked. A non-zero value means the sleep was interrupted by some signal, and the driver should return
ERESTARTSYS。
Wake-up API
| API | Usage | Description / Parameters | Wake-up Rules |
|---|---|---|---|
wake_up(&wq) | Wake up all non-exclusive waiters in the queue | Do not modify task state | Suitable for many readers, no restriction on wake-up |
wake_up_all(&wq) | Wake up all waiters | Include exclusive | Force wake up all tasks |
wake_up_interruptible(&wq) | Wake upTASK_INTERRUPTIBLEState task | Adapt for interruptible wait | |
wake_up_interruptible_all(&wq) | Wake up all interruptible waiters | ||
wake_up_nr(&wq, nr) | Wake up nr exclusive waiters | Most commonly used:wake_up(&wq) | exclusive: wake up only one |
There are two types of waiters in the wait queue:
| Type | How it comes | Typical scenario |
|---|---|---|
| Non-exclusive | add_wait_queue() | Multiple readers (read) |
| exclusive | add_wait_queue_exclusive() | Writer, resource contention |
The design goal of exclusive is:
Avoid the ‘thundering herd’
Delays and timer management
Time is one of the most commonly used resources after memory. It is used to perform almost everything: delaying work, sleeping, scheduling, timeouts, and many other tasks.
There are two types of time. The kernel uses absolute time to know the specific time, that is, the date and time of the day, while relative time is used by the kernel scheduler.
- For absolute time, there is a hardware chip called the Real-Time Clock (RTC).
- To handle relative time, the kernel relies on a CPU feature (peripheral) called a timer, which from the kernel’s perspective is referred to as a kernel timer.
Kernel timers are divided into two distinct parts.
- Standard timers or system timers.
- High-resolution timers.
Linux kernel standard timers
Standard timers are kernel timers that operate with Jiffy granularity.
The hardware provides the kernel with a system timer to measure elapsed time (i.e., a timing method based on future time points, starting from the current moment and ending at a future moment).The kernel can only calculate and manage time with the help of the system timer, butthe precision of kernel timers is not high, so they cannot be used as high-resolution timers.
Moreover, kernel timers do not run periodically; they automatically shut down upon reaching the timing endpoint. To achieve periodic timing, the timer must be restarted in the timer handler function.
Definition
In the Linux kernel, the timer_list structure is used to represent kernel timers
include/linux/timer.h
1 | struct timer_list { |
DEFINE_TIMER
1 | // include/linux/timer.h |
The following code can be used to define a timer and its corresponding timer handler function
1 | DEFINE_TIMER(timer_test,function_test);//Define a timer |
timer_setup()
| Item | Description |
|---|---|
| Function Definition | void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags); |
| Header File | #include <linux/timer.h> |
| Parameter timer | The timer object to be initializedstruct timer_listTimer object |
| Parameter callback | Callback function executed after timer expiration, type isvoid (*)(struct timer_list *) |
| Parameter flags | Timer flags (e.g.,TIMER_DEFERRABLE、TIMER_PINNEDetc.) |
| Function | Initialize a timer object, prepare it for first use, set callback function and flags |
| Return Value | No return value (void) |
| Description | Only completes initialization, does not start the timer; must be used withadd_timer()ormod_timer()to start timing |
timer_setup_on_stack()
| Item | Description |
|---|---|
| Function Definition | void timer_setup_on_stack(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags); |
| Header File | #include <linux/timer.h> |
| Parameter timer | Timer object located on the stackstruct timer_listTimer object |
| Parameter callback | Callback function executed when the timer expires |
| Parameter flags | Timer Flag |
| Function | Initialize a timer object allocated on the stack |
| Return Value | No return value (void) |
| Notes | Must be used withdestroy_timer_on_stack()in pairs, otherwise it may cause kernel debug warnings or resource issues |
destroy_timer_on_stack()
| Item | Description |
|---|---|
| Function Definition | static inline void destroy_timer_on_stack(struct timer_list *timer); |
| Header File | #include <linux/timer.h> |
| Parameter timer | on the stackstruct timer_listtimer object |
| Function | Destroy a timer created viatimer_setup_on_stack()initialized on the stack |
| Return Value | No return value (void) |
| Usage Scenario | Only for timers allocated on the stack (not heap/global variables) |
| Notes | Must be used withtimer_setup_on_stack()in pairs |
add_timer()
| Item | Description |
|---|---|
| Function Definition | void add_timer(struct timer_list *timer); |
| Header File | #include <linux/timer.h> |
| Parameter timer | An initialized timer object, must be setexpires(expiry time) andfunction(callback function) |
| Function | Register the timer with the kernel to start timing. Whenexpiresexpires, the callback function is triggered |
| Return value | No return value (void) |
del_timer()
| Item | Description |
|---|---|
| Function definition | int del_timer(struct timer_list *timer); |
| Header file | #include <linux/timer.h> |
| Parameter timer | The timer object to be deleted |
| Function | Delete the timer from the kernel so it no longer fires. Does not wait for the callback to finish executing (if waiting is needed, usedel_timer_sync()) |
| Return value | Successfully deleted:1(timer was active) no longer in the active queue:0 |
Note: Delete the timer from the kernel so it no longer fires. To wait for the callback to finish executing, usedel_timer_sync(), wait for the handler (even if executing on another CPU) to complete. Do not hold locks that prevent the handler from finishing, as this will cause a deadlock.
The timer should be released in the module cleanup routine. You can calltimer_pending()function to independently check if the timer is running
timer_pending()
This function checks if there is a pending triggered timer callback.
| Item | Description |
|---|---|
| Function definition | static inline int timer_pending(const struct timer_list *timer); |
| Header file | #include <linux/timer.h> |
| Parameter timer | The timer whose status is to be checkedstruct timer_listTimer Object |
| Function | Determine whether the timer is currently in a pending state |
| Return Value | If the timer has been added to the kernel timer queue (is timing), return1; otherwise, return0 |
| Notes | The caller must ensure that access to the timer is concurrency-protected (e.g., by locking or within the same context) |
mod_timer()
| Item | Description |
|---|---|
| Function Definition | int mod_timer(struct timer_list *timer, unsigned long expires); |
| Header File | #include <linux/timer.h> |
| Parameter timer | The timer object to be modified |
| Parameter expires | New expiration time (in jiffies) |
| Function | Modify the expiration time of the timer. If the timer is not active, it willautomatically activate. If it is active, it willrestart the timer |
| Return value | Timer was previously active:1 Timer was previously inactive:0 |
Before using the add_timer() function to register a timer with the Linux kernel, you also need to set the timeout period, which is determined by the timer_The expires parameter in the list structure, in units of ticks. The system tick frequency can be set through the Linux kernel configuration graphical interface menuconfig during compilation, with the specific path as follows:
1 | -> Kernel Features |
The current kernel version supports optional system tick rates of 100Hz, 250Hz, 300Hz, and 1000Hz, with 300Hz selected by default.
Global variable jiffies
The global variable jiffies records the total number of ticks since system boot. jiffies_64 is used for 64-bit systems, while jiffies is used for 32-bit systems. At boot, the kernel initializes this variable to 0. Thereafter, each clock interrupt handler increments its value. The increase in jiffies per second equals the configured system tick rate. This variable is defined ininclude/linux/jiffies.hIn the file (already included in timer.h, no need to include again), the specific definition is as follows:
1 | extern u64 __cacheline_aligned_in_smp jiffies_64; |
jiffies and time unit conversion functions
| Function definition | Purpose |
|---|---|
| int jiffies_to_msecs(const unsigned long j) | Convertjiffiesparameter of typejto the corresponding milliseconds |
| int jiffies_to_usecs(const unsigned long j) | Convertjiffiesparameter of typejto the corresponding microseconds |
| u64 jiffies_to_nsecs(const unsigned long j) | Convertjiffiesparameter of typejto the corresponding nanoseconds |
| long msecs_to_jiffies(const unsigned int m) | Convert milliseconds tojiffiestype |
| long usecs_to_jiffies(const unsigned int u) | Convert microseconds tojiffiestype |
| unsigned long nsecs_to_jiffies(u64 n) | Convert nanoseconds tojiffiestype |
Example
1 |
|
You can use atomic variables and timer to create a timer, example as follows:
1 |
|
Linux High Resolution Timer HRT
hrtimer= High Resolution TimerEnabled by the kernel configuration optionCONFIG_HIGH_RES_TIMERS, features as follows:
- Nanosecond precision (based on ktime)
- Suitable for high-precision periodic tasks
- Runs in softirq context
- Can be triggered periodically or once
Compared with traditionaltimer_listdifferences:
| Item | timer_list | hrtimer |
|---|---|---|
| Precision | jiffies level | nanosecond level |
| Precision depends on | HZ | hardware high-precision clock |
| Suitable for | ordinary delays | high-precision periodic tasks |
Unit Symbol Equals how many seconds Application scenario Millisecond ms 10⁻³ s = 0.001 s Computer response, audio Microsecond μs 10⁻⁶ s Electronics, network latency Nanosecond ns 10⁻⁹ s CPU clock cycle, light propagation Picosecond ps 10⁻¹² s Laser, quantum physics Femtosecond fs 10⁻¹⁵ s Chemical reaction kinetics Attosecond as 10⁻¹⁸ s Electron motion inside atoms (related to 2023 Nobel Prize in Physics)
Header file:
1 |
Check if HRT is available on the system.
- Check the kernel configuration file, which should contain something like:
CONFIG_ HIGH_RES_TIMERS=y - Check
cat /proc/timer_listorcat /proc/timer_list | grep resolutionresults..resolutionitem must show1 nsecs, event
handler must showhrtimer_interrupts。 - Use
clock_getressystem call. - In kernel code, use
#ifdef CONFIG_HIGH_RES_TIMERS。
When the system enables HRT, the precision of sleep and timer system calls no longer depends on jiffies, but they become as precise as HRT. This is why some systems do not supportnanosleep()and similar reasons.
struct hrtimer
1 | /** |
hrtimer_init()
Initialize hrtimer. Before initializing hrtimer, you need to set ktime, which represents the duration.
| Item | Description |
|---|---|
| Prototype | void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode); |
| Function | Initialize hrtimer |
| Parameters | timer: timer which_clock: clock source mode: mode |
| Return value | None |
Common clock:
- CLOCK_MONOTONIC
- CLOCK_REALTIME
Common mode:
- HRTIMER_MODE_REL (relative time, relative to the current time value)
- HRTIMER_MODE_ABS (absolute time)
hrtimer_start()
| Item | Description |
|---|---|
| Prototype | int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode); |
| Function | Start timer |
| Parameters | timer: timer tim: time mode: mode |
| Return Value | 0 or 1 |
hrtimer_cancel()
| Item | Description |
|---|---|
| Prototype | int hrtimer_cancel(struct hrtimer *timer); |
| Function | Cancel Timer |
| Return Value | 1 = Successfully canceled; 0 = Not running |
hrtimer_try_to_cancel()
| Item | Description |
|---|---|
| Function Definition | extern int hrtimer_try_to_cancel(struct hrtimer *timer); |
| Header File | #include <linux/hrtimer.h> |
| Parameter timer | To be canceledstruct hrtimerHigh-resolution timer object |
| Function | Attempt to cancel a high-resolution timer (non-blocking mode) |
| Return value | >0: Successfully canceled (timer is active) 0: Timer is not active (expired or not started) -1: Timer callback function is executing, cannot cancel |
| Blocking or not | ❌ Does not wait for callback to complete (non-blocking), can be called in interrupt context |
hrtimer_try_to_cancel internally calls hrtimer_callback_running
hrtimer_callback_running()
Independently check if the hrtimer callback function is still running:
| Item | Description |
|---|---|
| Function definition | static inline int hrtimer_callback_running(struct hrtimer *timer); |
| Header file | #include <linux/hrtimer.h> |
| Parameter timer | The timer to check the status ofstruct hrtimerHigh-resolution timer object |
| Function | Determine whether the callback function of this hrtimer is currently executing |
| Return value | Returnsnon-zero (true); otherwise returns0 (false) |
| Blocking | ❌ Non-blocking, only performs status check |
| Use case | Used to detect callback execution status, often used withhrtimer_try_to_cancel()Use |
1 | /* |
hrtimer_forward_now()
| Project | Description |
|---|---|
| Prototype | u64 hrtimer_forward_now(struct hrtimer *timer, ktime_t interval); |
| Function | Used for periodic timing |
| Return value | Forwarding count |
Callback return value
1 | enum hrtimer_restart { |
To prevent the timer from automatically restarting, the hrtimer callback function must return HRTIMER_NORESTART
Example
1 |
|
Dynamic Tick/Tickless Kernel
With the previous HZ option, even when idle, the kernel would interrupt HZ times per second to reschedule tasks. If HZ is set to 1000, there are 1000 kernel interrupts per second, preventing the CPU from staying idle for long periods, thus affecting CPU power consumption.
Now let’s take a look atkernels without fixed or predefined ticks, in these kernels,the tick is disabled until some task needs to be executed. Such kernels are called Tickless kernels.
In fact, tick activation is scheduled based on the next operation, so its correct name should be dynamic tick kernel. The kernel is responsible for task scheduling in the system and maintains a list of runnable tasks (run queue). When there are no tasks to schedule, the scheduler switches to the idle thread, which enables dynamic tick bydisabling the periodic tick until the next timer expires (when a new task is queued for processing)。
The kernel also maintains a list of task timeouts internally (it knows when to sleep and for how long).
- In the idle state, if the next tick is farther than the smallest timeout in the task timeout list, the kernel programs the timer with that timeout value.
- When the timer expires, the kernel re-enables the periodic tick and calls the scheduler, which schedules the task associated with the timeout.
In this way, the Tickless kernel removes the periodic tick, saving power.
Delays and Sleep in the Kernel
There are two types of delays, depending on the context in which the code runs:Atomic or non-atomic. The header file required for handling kernel delays is#include <linux/delay.h>。
Atomic context
Tasks in atomic context (such as ISRs) cannot enter sleep state, and scheduling is not possible. This is whydelays in atomic context must use busy-wait loops. The kernel provides the Xdelay family of functions, which consume enough time in a busy loop (based on jiffies) to achieve the required delay.
ndelay(unsigned long nsecs)。udelay(unsigned long usecs)。mdelay(unsigned long msecs)。
You should always useudelay(), becausendelay()the precision of depends on the precision of the hardware timer (which may not be the case for embedded SoCs). It is not recommended to usemdelay()。
Timer handlers (callbacks) execute in atomic context, meaning sleep is strictly prohibited. This refers to all functions that could cause the calling program to sleep, such as allocating memory, locking mutexes, or explicitly callingsleep()functions, etc.
Non-atomic context
In non-atomic context, the kernel providessleep[_range]family of functions, and which function to use depends on how long the delay needs to be.
udelay(unsigned long usecs): Based on a busy-wait loop. This function should be used if you need to sleep for a few microseconds (approximately ≤10 us).usleep_range(unsigned long min, unsigned long max): Depends on hrtimer, sleeping for a few microseconds to a few milliseconds (10 us~20
ms) is recommended to use it, avoiding theudelay()busy-wait loop.msleep(unsigned long msecs): Supported by jiffies/traditional timers. Use this function for long sleeps of several milliseconds or more (10 ms+).
Kernel source documentation
Documentation/timers/timers-howto.rstexplains sleep and delay-related topics in detail.
Calling user-space programs from the kernel
Consider the following example:
1 |
|
In the previous example, the API used (call_usermodehelper) is part of the Usermode-helper API, and all functions are defined inkernel/kmod.cIts usage is very simple. It is used by the kernel, for example, for module loading/unloading and Cgroup management.
IO Models
IO Operation Process
A complete IO flow typically includes the following key steps:
- IO Call: The application initiates an IO request (e.g., reading a file) to the kernel through the system call interface.
- IO Execution: After receiving the request, the kernel completes the specific IO (e.g., reading data from disk) by operating the hardware through the driver, and returns the final result to user space.
A complete IO process needs to include the following three steps:
- The application in user space initiates an IO call request (system call) to the kernel.
- The kernel operating system prepares the data, loading the data from the IO device into the kernel buffer.
- The operating system copies the data, transferring the data from the kernel buffer to the user process buffer.
Classification of IO Models
In actual development, IO operations often become a key factor affecting program performance. Suppose there is a scenario: reading 100MB of data from disk and processing it, where reading the data takes 20 seconds and processing the data also takes 20 seconds. If the most traditional sequential flow is adopted—reading first and then processing—the entire process takes about 40 seconds, which is clearly inefficient. So, can we process the data while waiting for it? Of course! This is where the IO programming model comes into play.
Under the POSIX/Linux definition: IO models include blocking IO, non-blocking IO, signal-driven IO, IO multiplexing, and asynchronous IO. The first four are called synchronous IO.
- Synchronous IO
- Blocking IO
- Non-blocking IO
- IO Multiplexing
- Signal-driven IO
- Asynchronous IO
The difference between synchronous and asynchronous iswhether to wait for the IO execution result, or who completes the data copy to user space?。
**Synchronous IO:**The final action of copying data from kernel to user space is done by the user thread when calling
read()Asynchronous IO:
- User mode initiates IO request → returns immediately
- Kernel completes IO in the background
- Data has been copied to user space
- Kernel notifies the process: IO complete
Synchronous blocking IO
When a process performs an IO operation (such as a read operation), it first initiates a system call, thus switching to kernel space for processing,When the data in kernel space is not ready, the process will be blocked and will not continue execution, until the data in kernel space is ready, the data is copied from kernel space to user space, and finally returned to the application process for data processing in user space.

Blocking IO can obtain results in a timely manner and immediately process the obtained results. However, before obtaining the results, it cannot handle other tasks and must constantly monitor the results. For example,the scanf function in C language。
Synchronous Non-blocking IO
Unlike the blocking IO model, when non-blocking IO performs an IO operation,if the kernel data is not ready, the kernel immediately returns an error to the process, without blocking; if the kernel space data is ready, the kernel immediately returns the data to the user-space process.

The advantage of non-blocking IO is high efficiency, allowing other tasks to be done in the same time. However, the disadvantage is also obvious: frequent checking of data readiness can lead to high CPU usage. To solve this problem, non-blocking IO is often used in conjunction with IO multiplexing technology.
IO Multiplexing
The select(), poll(), and epoll() functions are mechanisms for implementing IO multiplexing.
IO multiplexing allowsa single process to monitor multiple descriptors. When a descriptor is found to be ready, it notifies the program to perform the corresponding read or write operation.。
Taking the select() function as an example, as shown in the figure. When using it, you need to pass the set of file descriptors to be monitored and a timeout value to select().
When select() is executed, the system triggers a system call,and the kernel will traverse and check whether these descriptors have triggered the target event.(e.g., readable or writable). If an event is detected, it returns immediately; ifno event is detected, the process enters a blocked state and sleeps,until any descriptor is ready or a timeout occurs。
After select() returns, user space must traverse all descriptors to confirm which one triggered the event, thereby achieving the effect of a single thread managing multiple IO operations simultaneously.

The advantage of IO multiplexing is that a single process/thread can monitor and handle multiple IO streams simultaneously, greatly improving efficiency. However, IO multiplexing is not a panacea,although IO multiplexing can monitor multiple IO streams, the actual processing of results can only be done sequentially, making it moresuitable for IO-intensive scenarios where each IO stream has a small amount of data and arrival times are scattered(e.g., online chat).
Additionally, select has an upper limit on the number of descriptors it can monitor (typically no more than 1024), andit needs to traverse to determine which IO generated data. Therefore, when there are many IO streams, efficiency is low (this issue is resolved by epoll).
Signal-driven IO
Signal-driven IO refers to**The process will inform the kernel in advance that when an event occurs on a certain descriptor, the kernel should send a SIGIO signal to notify the process.**The process can then handle the event within the signal handler function.
For example, in a Linux system, when a user presses Ctrl+C to terminate a running task, the system actually sends a SIGINT signal to the process, and the default handler for this signal is to exit the current program.
Specifically for the I/O model, the process needs to first register a corresponding signal handler for the SIGIO signal and enable signal-driven I/O for the relevant descriptor. When data is ready, the process receives a SIGIO signal and can call I/O operation functions within the signal handler to process the data.

Asynchronous I/O
aio_read()The function is often used for asynchronous I/O. When a process usesaio_read()to read data,it returns immediately if the data is not yet ready, without blocking。
If the data is ready, it copies the data from kernel space to the user-space buffer, and thenexecutes the defined callback function to process the received data。

However, for Linux AIO, Linux AIOonly supports direct I/Omodestorage file(storage file), and is mainly used inthe database subfield;
whileio_uringsupports storage files and network files (network sockets), and also supports more asynchronous system calls (accept/openat/stat/...), rather than being limited toread/writesystem calls.
Wait queue implements blocking I/O
In Linux drivers,blocking processes can be implemented using wait queues。
Step 1: Initialize the wait queue head and set the condition to false (condition=0).
Step 2: Callwait_event()at the point where blocking is needed, causing the process to enter a sleep state
Step 3: When the condition is met, the sleep needs to be lifted. First set the condition (condition=1), then callwake_up()function to wake up the sleeping processes in the wait queue.
Driver:
1 |
|
Test
1 | $ cat /dev/waitqueue_test0 & |
Non-blocking access
The application can use the following example code to implement blocking access:
1 | int fd; |
It can be seen that forthe default read mode of the device driver file is blocking, so previous experimental routine tests all used blocking I/O.
If the application wants to access the driver device file in non-blocking mode, it can use the following code:
1 | int fd; |
When using the open function to open “/dev/xxx_dev” device file, the parameter “O_NONBLOCK” is added, indicating that the device is opened in non-blocking mode, so reading data from the device is also non-blocking.
Driver
Through the f field in the file structure_in flags, it checks whether the application opened the device via O_NONBLOCK mode
1 | static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off) |
Implementation of IO multiplexing
IO multiplexing is a synchronous IO model. IO multiplexing canenable a process to monitor multiple file descriptors. Once a file descriptor is ready, it notifies the application to perform the corresponding read or write operation. When no file descriptor is ready, it blocks the application, thereby freeing CPU resources.
At the application layer, Linux provides three models for implementing IO multiplexing: select, poll, and epoll.
- poll and select are basically the same; both can monitor multiple file descriptors and obtain ready file descriptors by polling them.
- epoll changes active polling to passive notification, where it passively receives notifications when events occur.
Linux application layer poll
| Item | Content |
|---|---|
| Function | Monitor read/write events or exceptional events on multiple file descriptors |
| Prototype | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
| Parameters | -fds: Array of struct pollfd, describing the file descriptors and events to be monitored- nfds: Number of file descriptors to be monitored- timeout: Timeout (ms)>0: Wait for the specified time; = 0: Return immediately; -1: Block indefinitely until an event occurs |
| Return Value | >0: Returns the number ofrevents ≠ 0file descriptors =0: timeout -1: failure |
struct pollfd
1 | struct pollfd { |
events and revents of pollfd
| event type | constant value | as value for events | as value for revents | description |
|---|---|---|---|---|
| read event | POLLIN | ✔ | ✔ | normal data readable |
| read event | POLLRDNORM | ✔ | ✔ | readable (normal data) |
| Read event | POLLRDBAND | ✔ | ✔ | Readable (out-of-band data) |
| Read event | POLLPRI | ✔ | ✔ | Readable (high-priority data) |
| Write event | POLLOUT | ✔ | ✔ | Writable |
| Write event | POLLWRNORM | ✔ | ✔ | Writable (normal data) |
| Write event | POLLWRBAND | ✔ | ✔ | Writable (out-of-band data) |
| Error event | POLLERR | ✔ | Error occurred | |
| Error event | POLLHUP | ✔ | Hang-up occurred | |
| Error event | POLLNVAL | ✔ | Descriptor is not an open file |
Driver-level poll
| Item | Content |
|---|---|
| Prototype | unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait); |
| Function | Tells the kernel whether the current device can be accessed in a non-blocking manner (readable/writable) |
| Parameters | -filp: File structure pointer- wait: poll_table passed by the kernel, used to register the wait queue |
| Return value | Returns a status bitmask (same as POLLIN/POLLOUT events) |
If passive waiting is needed (to avoid wasting CPU cycles when sensing a character device), it must implementpoll()function, whenever a user-space program executes a system call on the file associated with the deviceselect()orpoll()will be called every timepoll()function.
The core kernel function of this method ispoll_wait(), which is defined in<linux/poll.h>. This header file should be included in the driver code.
Implementation in the driver
poll_wait()
| Item | Content |
|---|---|
| Purpose | Add the driver’s wait queue to the poll_table for select/poll/epoll |
| Prototype | void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait); |
| Header file | #include <linux/poll.h> |
| Parameters | -filp: file- queue: wait queue head (wait_queue_head_t) - wait: poll_table (from application layer) |
| Return value | None |
| Features | **Does not block!**Only registers the wait queue |
poll_wait()Based on the events registered in thestruct poll_tablestructure (passed as the third parameter), associate the device linked to thestruct filepstructure (as the first parameter) to the list of devices that can wake up the process (specified by the second parameterstruct wait_queue_head_tstructure, in which the process is sleeping).
The user process can runpoll()、select()orepoll()system call to add a set of files to wait on to the wait queue, to check if any associated devices are ready.
Afterwards, the kernel will call the poll entry of the driver associated with each device file. Each driver’s poll method then calls poll_wait to register events for processes that need to receive kernel notifications, putting the processes to sleep until these events occur, and registering the driver as one that can wake up the processes.
Steps
- Declare a wait queue for each event type (read, write, exception) that requires passive waiting. When there is no data to read or the device is not writable, put the task into that queue:
1 | static DECLARE_WAIT_QUEUE_HEAD(my_wq); |
- Implement the poll function like this
1 |
|
- When there is new data or the device becomes writable, notify the wait queue:
1 | wake_up_interruptible(&my_rq); /* Ready to read */ |
The following two methods can be used to notify a readable event:
- Notify in the driver’s write() method, meaning the written data can be read back;
- Notify in the IRQ handler, meaning data sent by the external device can be read back.
The following two methods can be used to notify a writable event:
- Notify in the driver’s read() method, meaning the buffer is empty and can be refilled;
- Notify in the IRQ handler, meaning the device has finished sending data and is ready to receive data again.
Example
app read.c
1 |
|
Driver
1 |
|
Test:
1 | $ insmod poll_test.ko |
Signal-driven I/O
Signal-driven I/O does not require the application to poll the device status. Once the device is ready, it triggers the SIGIO signal, which then calls the registered handler function.
To implement signal-driven I/O, the application and driver must cooperate. The application uses signal-driven I/O in three steps:
- Step 1: Register the signal handler. The application uses the signal function to register the handler for the SIGIO signal.
- Step 2: Set the process that can receive this signal (using the fcntl function)
- Step 3: Enable signal-driven I/O. Typically, use the F_SETFL command of the fcntl function to set the FASYNC flag.
In user space, either O_ASYNC or FASYNC can be used.
1
2
3
4
5
6
7
8
9
10 // /usr/aarch64-linux-gnu/include/bits/fcntl-linux.h
/* Define some more compatibility macros to be backward compatible with
BSD systems which did not managed to hide these kernel macros. */
Driver implementation
When the application enables signal-driven I/O, it triggers the fasync function in the driver. Therefore, first implement the fasync function in the file_operations structure, with the function prototype as follows:
1 | int (*fasync) (int fd,struct file *filp,int on) |
In the driver, the fasync function calls the fasync_helper function to manipulate the fasync_struct, the fasync_helper function prototype is as follows:
1 | int fasync_helper(int fd,struct file *filp,int on,struct fasync_struct **fapp) |
Wherestruct fasync_structDefined as follows:
1 | struct fasync_struct { |
struct fasync_structis a linked list node used by the kernel to manage which processes wish to receive SIGIO/SIGURG signals for a certain file.
Each
fasync_struct= one subscriberThe driver stores thelinked list head pointer
kill_fasync()and traverses this linked list to send signals
When the device is ready,the driver needs to call the kill_fasync function to notify the application, at which point the application’s SIGIO signal handler will be executed. kill_fasync is responsible for sending the specified signal, and its function prototype is as follows:
1 | void kill_fasync(struct fasync_struct **fp,int sig,int band) |
- Function parameters:
- fp: The
fasync_struct - sig: The signal to send
- band: Set to POLLIN when readable, and POLLOUT when writable
- fp: The
Example
app
1 |
|
Driver
1 |
|
Test:
1 | $ insmod signal_io.ko |
Asynchronous I/O
Asynchronous I/O relies on the implementation in the application-layer glibc and can be independent of the Linux kernel
Reference:
Forio_uringYou can refer to this article on our site
Linux Kernel Printing
dmesg
You can obtain kernel print information using the dmesg command in the terminal. The specific usage of this command is as follows:
dmesgCommand
- Full English name: display message
- Function: The kernel stores print information in the ring buffer. You can use the dmesg command to view kernel print information.
- Common parameters:
- -C, --clear Clear the kernel ring buffer
- -c, --read-clear Read and clear all messages
- -T, --show-timestamp Display timestamps
The dmesg command can also be used in combination with the grep command. For example, to find print information containing the keyword ‘usb’, you can use the following command:
1 | dmesg | grep usb |
Combined with the tail command to view the last 100 lines
1 | dmesg | tail -n 100 |
kmsg file
All kernel print information is output to the circular buffer ‘log_buf’. To facilitate reading kernel print information in user space, the Linux kernel driver maps this circular buffer to the file node kmsg under the /proc directory.
When reading the Log Buffer via cat or other applications, it can continuously wait for new logs, soaccessing /proc/kmsg is suitable for long-term log reading, and once there is a new log, it can be printed out.
First, use the following command to read the kmsg file, which will block when there is no new kernel print information
1 | cat /proc/kmsg |
Adjust kernel print level
Kernel log printing is controlled by the corresponding print level. When printk is called, the kernel compares the message log level with the current console log level; if the former is
higher (lower value) than the latter, the message is immediately printed to the console.
You can check the log level parameters like this: The output of print logs can be controlled by adjusting the kernel print level. Use the following command to view the current default print level
1 | cat /proc/sys/kernel/printk |
It can be seen that the kernel print level is determined by four numbers, ‘7 4 1 7’ corresponding to console_loglevel、default_message_loglevel、minimum_console_loglevel、default_console_loglevel, with specific type descriptions as follows:
- console_loglevel
- Description: Controls which levels of messages can be output tothe terminal(console)。
- Rule: Only when the log priority of the messageis higher than console_loglevelwill it be displayed on the terminal.
- Example: “7” means allowing messages of all levels (0~7) to be output to the terminal.
- default_message_loglevel
- Description:
printkThe default log level (priority) when the function prints messages. - Example: “4” means the default message level iswarning。
- Description:
- minimum_console_loglevel
- Description: The minimum value that console_loglevel can be set to.
- Example: “1” means console_loglevel can be set to a minimum of 1, which isemerg。
- default_console_loglevel
- Description: The default value of console_loglevel when the kernel starts.
- Example: “7” means by default,all levelsare allowed to output to the terminal.
include/linux/printk.c
1 | int console_printk[4] = { |
include/linux/kern_levels.h
1 | /* SPDX-License-Identifier: GPL-2.0 */ |
| Number | Level Name | Description |
|---|---|---|
| 0 | KERN_EMERG | Emergency, system unavailable |
| 1 | KERN_ALERT | Alert, needs immediate action |
| 2 | KERN_CRIT | Critical error |
| 3 | KERN_ERR | General error |
| 4 | KERN_WARNING | Warning |
| 5 | KERN_NOTICE | Normal but important information |
| 6 | KERN_INFO | Informational message |
| 7 | KERN_DEBUG | Debug message |
Modify kernel print level:
1 | echo 0 4 1 7 > /proc/sys/kernel/printk |
Before printing information, printk can include the corresponding print level macro definition, the specific format is as follows:
1 | printk(打印等级 "打印信息") |
For example:
1 | printk(KERN_ERR "This is an error\n"); |
If the debug level is omitted,
printk("This is anerror\n")the kernel will provide a debug level to this function based on theCONFIG_DEFAULT_MESSAGE_LOGLEVELconfiguration option (which is the default
kernel log level).
In practice, the following macros with more meaningful names can be used; they are wrappers for the previously defined pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info and pr_debug:
1 | pr_err("This is the same error\n"); |
llseek positioning device driver
Application layer lseek()
All open files have acurrent file offset, hereinafter referred to as cfo. cfo is usually anon-negative integer, indicating the number of bytes from the beginning of the file to the current position. Read and write operations typically start at cfo and increase cfo by the number of bytes read or written. When a file is opened, cfo is initialized to 0, unless O_APPEND is used. The cfo of a file can be changed using the lseek function.
| Item | Description |
|---|---|
| Function definition | off_t lseek(int fd, off_t offset, int whence); |
| Header file | #include <sys/types.h>#include <unistd.h> |
| Parametersfd | File descriptor |
| Parametersoff_t offset | Offset,in bytes,Positive and negative indicate forward and backward movement respectively |
| Parameterswhence | Position base, optionalSEEK_SET(beginning of file),SEEK_CUR(current pointer position),SEEK_END(end of file) |
| Function | Move file read/write pointer; get file length; extend file space |
| Return value | SuccessReturns the current offset (SEEK_CUR), returns -1 on failure |
Example:
Set the file position pointer to 100 (beginning + 100 bytes)
1
lseek(fd,100,SEEK_SET);
Set the file position to the end of the file
1
lseek(fd,0,SEEK_END);
Determine the current file position
1
lseek(fd,0,SEEK_CUR);
Driver layer llseek()
1 | loff_t (*llseek)(struct file *file, loff_t offset, int whence); |
- Purpose: File pointer offset operation (similar to lseek system call).
- Parameters:
file: File object.offset: Offset relative to the current file position, defining how much the current position will change.whence: Defines where to start seeking, possible values are as follows:SEEK_SET: relative to the beginning of the fileSEEK_CUR: relative to the current file pointerSEEK_END: relative to the end of the file
- Return value:
- New file pointer position (
loff_t) - Returns a negative value on error (e.g.,
-EINVAL)
- New file pointer position (
Usage steps
- Use a switch statement to check each whence case; since its values are limited, adjust newpos accordingly:
1 | switch( whence ) { |
- Check if newpos is valid:
1 | if ( newpos < 0 ) |
- Update f_pos with the new position
1 | filp->f_pos = newpos; |
- Return the new file pointer position
1 | return newpos; |
Example
1 |
|
For devices with fixed buffer sizes, directly return the kernel’s already implementedfixed_size_llseek(file, offset, whence, KMEM_SIZE);
Test case:
1 | #include <stdio.h> |
Test:
1 | $ ./test_llseek.o |
ioctl device operation
Application-layer ioctl()
| Item | Description |
|---|---|
| Function definition | int ioctl(int fd, unsigned long op, … /* arg */ ); |
| Header file | #include <sys/ioctl.h>(May require device-specific header files, such aslinux/ioctl.h) |
| Parameter fd | Opened device file descriptor (e.g.,/dev/...) |
| Parameter op | IO control commands (usually via_IO,_IOR,_IOW,_IOWRmacros, etc.) |
| parameter arg(optional) | Data associated with the request command, can beint*,void*,struct *etc. |
| Function | Execute control commands on the device driver (non-data read/write type), used for configuring hardware, obtaining status, sending control instructions, etc. |
| Return value | Success: usually0(may also return other positive values, depending on the request) Failure: returns**-1**, and sets errno |
Among the above three parameters, the most important is the second op parameter, which is of type unsigned int. To efficiently use the op parameter to convey more control information, an unsigned int op is split into 4 segments, each with its own meaning. The bit field breakdown of unsigned int cmd is as follows:
1 | | 31 30 | 29 ................ 16 | 15 ...... 8 | 7 .......... 0 | |
- op[31:30]dirData (args) transfer direction (read/write)
- op[29:16]sizeSize of data (args)
- op[15:8]typeCommand type, which can be understood as the command’s key, generally an ASCII code (a character from 0-255, some characters are already occupied, and the sequence range of each character may be partially occupied)
- op[7:0]nrCommand sequence number, an 8-bit number (sequence number, between 0-255)
The op parameter is defined byioctl synthesis macrosand is obtained through definition. The four synthesis macros are defined as follows:
- Define a command that requires no parameters:
1 |
|
- Define a command where the application reads parameters from the driver:
1 |
|
- Define a command where the application writes parameters to the driver:
1 |
|
- Define a command where parameters are passed bidirectionally (write first, then read):
1 |
The macro definition parameter descriptions are as follows:
- type:Command type, generally an ASCII code value called the magic number; a driver typically uses one type
- nr: The sequence number under this command. A driver has multiple commands, and generally their type and sequence numbers differ
- size: the type of args
For example, the following code can be used to define three macros: one that takes no parameters, one that writes parameters to the driver, and one that reads parameters from the driver:
1 |
In the Linux kernel’s ioctl interface, each ioctl command can only pass one user-space pointer (i.e., one parameter). But this does not mean only an integer can be passed; it can be done bypassing a structure pointer to achieve ‘multiple parameters’。
SIZE = arg 参数的数据结构大小(字节数)It is automatically encoded into the ioctl command code when the driver creates it.Example:
1 _IOR('M', 1, int)It will automatically encode:
1 sizeof(int) = 4into op. If it is:
1 _IOW('M', 2, struct task_info)then:
1 sizeof(struct task_info)User calls
1
2 struct task_info info;
ioctl(fd, MY_WRITE_TASK, &info);After the kernel receives it:
1 copy_from_user(&kernel_buffer, (void __user *)arg, sizeof(struct task_info));
The kernel source code’s Documentation/driver-api/ioctl.rstwill provide detailed explanation.
Exampleeep_ioctl.h::
1 |
|
driver layer ioctl()
1 | long unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg); |
unlocked_ioctlfor user programs with native bit width
compat_ioctlfor 32-bit user programs running on 64-bit kernelcalling undefined
ioctlreturns when command-ENOTTYerror
the kernel will useioctl decomposition macroto parse cmd:
_IOC_DIR(cmd)— data direction_IOC_TYPE(cmd)—— device magic_IOC_NR(cmd)—— command number_IOC_SIZE(cmd)—— transfer data size
example
1 |
|
Encapsulate Driver-Provided API Functions
As driver engineers, we can certainly understand the function of each line of code. However, in general, applications are written by professional application engineers. The above code writing style is not conducive to application engineers’ understanding and program portability, so encapsulating the application API is an inevitable task.
Compile into a library file,One .c file per function, each compiled into a library file
Optimize driver stability and efficiency
Detect ioctl commands
The cmd command of ioctl is obtained through composition macros, and there are corresponding decomposition macros to obtain each parameter. The four decomposition macros are as follows:
- Decompose the cmd command to obtain the command type:
1 | _IOC_TYPE(cmd) |
- Decompose the cmd command to obtain the data (args) transfer direction:
1 | _IOC_DIR(cmd) |
- Decompose the cmd command to obtain the command number:
1 | _IOC_NR(cmd) |
- Decompose the cmd command to obtain the data (args) size:
1 | _IOC_SIZE(cmd) |
In the driver, the above decomposition macros can be used to judge the parameters such as the type of the incoming ioctl command, thereby determining whether the incoming parameters are correct, thus optimizing the stability of the driver.
Check whether the passed address is reasonable
access_ok()
| Item | Description |
|---|---|
| Function prototype | int access_ok(const void __user *addr, unsigned long size); |
| Header file | #include <linux/uaccess.h> |
| Parameter addr | Pointer variable in user space, pointing to the starting address of the memory block to check |
| Parameter size | Size of the memory block to check (in bytes) |
| Function | Check whether the specified user-space memory block is accessible (read/write), used to protect kernel access to user-space data security |
| Return value | Success:1(accessible) Failure:0(inaccessible) |
Example:
1 | len = sizeof(struct args); |
Branch prediction optimization
Modern CPUs have ICache and pipeline mechanisms. That is, when executing the current instruction, the ICache pre-fetches subsequent instructions to improve efficiency. However, if the result of a conditional branch jumps to another instruction, pre-fetching the next instruction wastes time.
likely and unlikely macros
The ones we will use arelikely and unlikely macros, which will always make the compiler place the code with a high probability of execution in a forward position, thereby improving the efficiency of the driver.
The likely and unlikely macros are defined in the kernel source file include/linux/compiler.h, with the specific definitions as follows:
1 |
__builtin_The role of expect is to inform the compiler that the expression exp is more likely to equal c, allowing the compiler to better optimize the code based on this factor. Therefore, the role of likely and unlikely is to express that x is more likely (likely) or less likely (unlikely) to be true.
Example:
1 | if(unlikely(copy_from_user(&test,(int *)arg,sizeof(test)) != 0)){ |
Driver Debugging
debugfs
Example
Kernel Examplearch/arm/mm/ptdump_debugfs.c
1 | // SPDX-License-Identifier: GPL-2.0 |
Only need debugfs.h and seq_file.h these two header files, using seq_file makes printing simpler.
Debug Printing
dump_stack()
dump_stack()The role of is:
Print the call trace of the current CPU to the kernel log.
Calling it in kernel code is equivalent to executing in user spacebacktrace()or the C library’sprintf("%pS", __builtin_return_address())functions like that.
Example:
1 |
|
WARN_ON(condition)
Function of WARN_ON(condition):When the condition in parentheses is true, the kernel will throw a stack traceback and print the function call relationship.。
It is usually used for the kernel to throw a warning, implying that something unreasonable has occurred.
WARN_ON actually also calls dump_stack, but with an additional parameter condition to determine whether the condition is true., for example, WARN_ON(1) means the condition check succeeds, and the function will execute successfully.
Example:
1 |
|
BUG() and BUG_ON(condition)
There are many places in the kernel that call statements like BUG_ON(), which is very much like a runtime assertion in the kernel, meaning that execution should not have reached BUG_ON() statement,Once BUG_ON() executes, the kernel will immediately throw an oops(an oops is thrown only when a fatal error occurs in Linux), causing stack traceback and error message printing.
Most architectures define BUG() and BUG_ON() as some kind of illegal operation, which naturally produces the required oops. The application layer can see that a segmentation fault has occurred.
The parameter condition determines whether the condition holds. For example, BUG_ON(1) means the condition is true, and the function will execute successfully.
1 |
|
panic (fmt…)
panic(fmt…) function:The output print will cause the system to crash and will print out the function call chain and register values.
1 |
|

