C language skills commonly used in the Linux kernel (2)

Data structures and algorithms commonly used in the Linux kernel

Data structures and algorithms are widely used in the Linux kernel code , and the two most commonly used are linked lists and red-black trees .

1 linked list

The Linux kernel code makes extensive use of the linked list data structure.

The linked list is a data structure that solves the defect that the array cannot be dynamically expanded .

The elements contained in the linked list can be dynamically created and inserted and deleted .

Each element of the linked list is stored discretely, so it does not need to occupy continuous memory.

A linked list usually consists of several nodes, and each node has the same structure, consisting of two parts, the effective data area and the pointer area .

The valid data area is used to store valid data information , and the pointer area is used to point to the predecessor or successor node of the linked list. Therefore, a linked list is a storage structure that uses pointers to connect nodes in series.

(1) One-way linked list

The pointer area of ​​the singly linked list only contains a pointer to the next node, so a linked list with a single direction is formed, as shown in the following code.

struct list {
int data;  /*有效数据*/
struct list *next; /*指向下一个元素的指针*/
};

As shown in Figure 2.2, the one-way linked list has one-way mobility , that is, only the successor node of the current node can be accessed, but the predecessor node of the current node cannot be accessed, so it is rarely used in actual projects.

(2) Doubly linked list

As shown in Figure 2.3, the difference between the doubly linked list and the singly linked list is that the pointer area contains two pointers, one pointing to the previous node and the other pointing to the successor node, as shown in the following code.

struct list {
int data;  /*有效数据*/
struct list *next; /*指向下一个元素的指针*/
struct list *prev; /*指向上一个元素的指针*/
};

(3) Implementation of Linux kernel linked list

Singly linked lists and doubly linked lists have some limitations in actual use, such as the data area must be fixed data, but the actual requirements are varied.

This method cannot build a set of general linked lists, because each different data area needs a set of linked lists.

For this reason, the Linux kernel extracts the common parts of all linked list operation methods, and leaves the different parts to the code programmers to deal with .

The Linux kernel implements a set of encapsulation of a pure linked list. The data structure of the linked list node has only a pointer area and no data area . In addition, it also encapsulates various operation functions, such as creating node functions, inserting node functions, deleting node functions, and traversing node functions.

The Linux kernel linked list is described using the struct list_head data structure.

<include/linux/types.h>
struct list_head {
struct list_head *next, *prev;
};

The struct list_head data structure does not contain the data area of ​​the linked list node, and is usually embedded in other data structures. For example, a lru linked list node is embedded in the struct page data structure, and the page data structure is usually linked to the LRU linked list .

<include/linux/mm_types.h>
struct page {
struct list_head lru;
...
}

There are two ways to initialize the head of the linked list, one is static initialization and the other is dynamic initialization.

Initialize both the next and prev pointers and point to themselves, thus initializing an empty linked list with the head node.

<include/linux/list.h>
/*静态初始化*/
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
/*动态初始化*/
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}

To add a node to a linked list, the kernel provides several interface functions, such as list_add() is to add a node to the head of the list , and list_add_tail() is to insert the tail of the list.

<include/linux/list.h>
void list_add(struct list_head *new, struct list_head *head)
list_add_tail(struct list_head *new, struct list_head *head)

Interface function for traversing nodes.

#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)

This macro just traverses the current position of each node, so how to get the data structure of the node itself?
You also need to use the list_entry() macro here.

#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
container_of()宏的定义在kernel.h头文件中。
#define container_of(ptr, type, member) ({      \
const typeof( ((type *)0)->member ) *__mptr = (ptr);  \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

Among them, the offsetof() macro converts the address of 0 into a pointer of type type , and then obtains the pointer of the member member in the structure, that is, obtains the offset of the member in the type structure.

Finally, subtract the offset from the pointer ptr to get the real address of the type structure.

Below is an example of traversing a linked list.

<drivers/block/osdblk.c>
static ssize_t class_osdblk_list(struct class *c,
struct class_attribute *attr,
char *data)
{
int n = 0;
struct list_head *tmp;
list_for_each(tmp, &osdblkdev_list) {
struct osdblk_device *osdev;
osdev = list_entry(tmp, struct osdblk_device, node);
n += sprintf(data+n, "%d %d %llu %llu %s\n",
osdev->id,
osdev->major,
osdev->obj.partition,
osdev->obj.id,
osdev->osd_path);
}
return n;
}

2 red-black trees

Red Black Tree (Red Black Tree) is widely used in kernel memory management and process scheduling to organize sorted elements into trees.

Red-black trees are widely used in various fields of computer science, and they provide a good balance between speed and implementation complexity.

A red-black tree is a binary tree with the following characteristics.

Each node is either red or black.

Each leaf node is black.

If the node is both red, then both child nodes are black.

On a simple path from an internal node to a leaf node, the number of black nodes is the same for all leaf nodes.

One advantage of red-black trees is that all significant operations (such as insert, delete, search) can be done in O(log n) time, where n is the number of elements in the tree.

Classical algorithm textbooks will explain the implementation of red-black trees. Here is just an example of using red-black trees in the kernel for readers' reference in actual driver and kernel programming. This example can be found in the documentation/Rbtree.txt file of the kernel code.

#include <linux/init.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/rbtree.h>
MODULE_AUTHOR("figo.zhang");
MODULE_DESCRIPTION(" ");
MODULE_LICENSE("GPL");
struct mytype {
struct rb_node node;
int key;
};

/*红黑树根节点*/
struct rb_root mytree = RB_ROOT;
/*根据key来查找节点*/

struct mytype *my_search(struct rb_root *root, int new)
{
struct rb_node *node = root->rb_node;
while (node) {
struct mytype *data = container_of(node, struct mytype,node);
if (data->key > new)
node = node->rb_left;
else if (data->key < new)
node = node->rb_right;
else
return data;
}
return NULL;
}
/*插入一个元素到红黑树中*/
int my_insert(struct rb_root *root, struct mytype *data)
{
struct rb_node **new = &(root->rb_node), *parent=NULL;
/* 寻找可以添加新节点的地方 */
while (*new) {
struct mytype *this = container_of(*new, struct mytype,node);
parent = *new;
if (this->key > data->key)
new = &((*new)->rb_left);
else if (this->key < data->key) {
new = &((*new)->rb_right);
} 
else
return -1;
}

/* 添加一个新节点 */
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);
return 0;
}
static int __init my_init(void)
{
int i;
struct mytype *data;
struct rb_node *node;
/*插入元素*/
for (i =0; i < 20; i+=2) {
data = kmalloc(sizeof(struct mytype), GFP_KERNEL);
data->key = i;
my_insert(&mytree, data);
}

/*遍历红黑树,打印所有节点的key值*/
for (node = rb_first(&mytree); node; node =rb_next(node))
printk("key=%d\n", rb_entry(node, struct mytype, node)->key);
return 0;
}


static void __exit my_exit(void)
{
struct mytype *data;
struct rb_node *node;
for (node = rb_first(&mytree); node; node =rb_next(node)) {
data = rb_entry(node, struct mytype, node);
if (data) {
rb_erase(&data->node, &mytree);
kfree(data);
}
}
}
module_init(my_init);
module_exit(my_exit);

mytree is the root node of the red-black tree,
my_insert() implements inserting an element into the red-black tree, and
my_search() finds the node according to the key.

The kernel uses red-black trees extensively, such as the management of the virtual address space VMA.

3 lock-free ring buffer

The producer and consumer model is one of the most common in computer programming. The producer produces data, and the consumer consumes data, such as a network device, the hardware device receives network packets, and then the application program reads the network packets.

A ring buffer is a classic algorithm for implementing the producer and consumer model. A ring buffer usually has a read pointer and a write pointer.

The read pointer points to the readable data in the ring buffer, and the write pointer points to the writable data in the ring buffer. The buffer data is read and written by moving the read pointer and write pointer.

In the Linux kernel, KFIFO is implemented using a lock-free ring buffer . The full name of FIFO is "First In First Out", which is a first-in-first-out data structure. It is implemented by a ring buffer method and provides an unbounded byte stream service.

The advantage of using a ring buffer is that when a data element is consumed, the remaining data elements do not need to move their storage locations, thereby reducing duplication and improving efficiency.

(1) Create KFIFO

Before using KFIFO, it needs to be initialized. There are two ways of static initialization and dynamic initialization.

<include/linux/kfifo.h>
int kfifo_alloc(fifo, size, gfp_mask)

This function creates and allocates a KFIFO ring buffer of size size.

The first parameter fifo is the struct kfifo data structure pointing to the ring buffer;

The second parameter size is the number of specified buffer elements;

The third parameter gfp_mask indicates the allocation mask used to allocate KFIFO elements.

Static allocation can use the following macros.

#define DEFINE_KFIFO(fifo, type, size)
#define INIT_KFIFO(fifo)

(2) Listed

Writing data into the KFIFO ring buffer can use the kfifo_in() function interface.

int kfifo_in(fifo, buf, n)

This function copies the n data pointed by the buf pointer to the KFIFO ring buffer.

  • The first parameter fifo refers to the KFIFO ring buffer;
  • The second parameter buf points to the buffer of the data to be copied;
  • The third data is the number of data elements to be copied.

(3) Dequeue

To list or extract data from the KFIFO ring buffer, you can use the kfifo_out() function interface.

#define kfifo_out(fifo, buf, n)

This function copies n data elements from the ring buffer pointed to by fifo to the buffer pointed to by buf.

If the data elements of the KFIFO ring buffer are less than n, the copied data elements are less than n.

(4) Get the buffer size

KFIFO provides several interface functions to query the status of the ring buffer.

#define kfifo_size(fifo)
#define kfifo_len(fifo)
#define  kfifo_is_empty(fifo)
#define  kfifo_is_full(fifo)

kfifo_size() is used to obtain the size of the ring buffer, which is the maximum number of data elements that can be accommodated.
kfifo_len() is used to get how many valid data elements are in the current ring buffer.
kfifo_is_empty() determines whether the ring buffer is empty.
kfifo_is_full() determines whether the ring buffer is full.

(5) Interact with user space data

KFIFO also encapsulates two functions to interact with user space data.

#define  kfifo_from_user(fifo, from, len, copied)
#define  kfifo_to_user(fifo, to, len, copied)

kfifo_from_user() is to copy the len data elements of the user space pointed to by from to KFIFO, and the last parameter copied indicates that several data elements have been successfully copied.

kfifo_to_user() does the opposite, copying the data elements of the KFIFO to user space.

These two macros combine the mechanism of copy_to_user(), copy_from_user() and KFIFO to provide convenience for driver developers. The driver of the virtual FIFO device will implement these two interface functions.

References

"Run Linux Kernel"

Guess you like

Origin blog.csdn.net/weixin_45264425/article/details/132052700