Estaciones de carga | Una guía para la resolución eficiente de problemas

Creo que los estudiantes que han estado expuestos al lenguaje C están familiarizados con la estructura de datos de la lista enlazada. Como una estructura de datos común en el lenguaje C, todos entienden bien la lista enlazada, pero me pregunto si todos saben cómo funciona la lista enlazada. en el núcleo está implementado. Los estudiantes que no tienen un contacto profundo con el kernel de Linux pueden no entender la lista de enlaces del kernel, ni lo ingeniosa que es su implementación. A continuación te daré uno de los beneficios.

En comparación con la implementación de la lista enlazada ordinaria, se puede decir que la implementación del kernel de Linux es única. La lista vinculada tradicional se puede conectar en serie en la lista vinculada agregando un puntero de nodo siguiente a los datos dentro de los datos. Por ejemplo, suponga una estructura de datos cat para describir a un miembro de la familia cat.

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
}
复制代码

La forma de almacenar esta estructura en una lista enlazada suele ser incrustar un puntero de lista enlazada en la estructura de datos, por ejemplo:

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
struct cat *next;/*指向下一只猫*/
struct cat *prev; /*指向上一只猫*/
}
复制代码

Pero, obviamente, si el kernel de Linux adopta este método de implementación, debido a que los tipos de datos de cada dispositivo en el kernel son diferentes, el kernel no podrá administrar varios dispositivos de manera uniforme, y este método de implementación también aumentará en gran medida la duplicación de código, lo que es obviamente contrario a la idea de polykernel. Por lo tanto, la implementación de la lista enlazada del kernel se introdujo oficialmente en la versión 2.1 del kernel, y se utilizó una lista enlazada simple y eficiente para unificar otras listas enlazadas.Su estructura de datos es la siguiente:

/**
 * 双向循环链表的节点结构
 */
struct list_head 
{
struct list_head *next, *prev;
};
复制代码

Se puede observar que no existe un campo de datos en cada nodo de la lista enlazada, es decir, por muy compleja que sea la estructura de datos, solo tiene los punteros delantero y trasero en la lista enlazada. Y cuando una estructura de datos (es decir, una estructura de dispositivo que describe un dispositivo) quiere ser administrada por una lista general enlazada, solo es necesario agregar un campo que contenga un nodo a la estructura.

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
struct list_head list;/*所以cat结构体形参链表*/
}
复制代码

Y la lista doblemente enlazada puede recorrer toda la lista enlazada desde el frente y el reverso de cualquier nodo, lo cual es muy conveniente de recorrer. El uso de una lista enlazada circular hace posible atravesar los nodos de gestión de forma continua, como la programación de procesos: el sistema operativo gestionará los procesos listos en una lista enlazada general que gestiona la cola de espera del proceso y les asigna segmentos de tiempo continuamente. para obtener La cpu realiza la programación de procesos una y otra vez. Ahora se puede usar la lista enlazada, pero obviamente no es lo suficientemente conveniente. Por lo tanto, el kernel proporciona un conjunto de rutinas de operación de listas enlazadas, como el método list_add() para agregar un nuevo nodo a la lista enlazada, y estos métodos tienen una característica unificada: solo aceptan la estructura list_head como parámetro.

static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}
复制代码

Al implementar esta lista vinculada de solo nodos, la estructura del dispositivo en el kernel se puede administrar de manera uniforme. Pero el problema que aún debe resolverse es cómo obtener la primera dirección de la estructura del dispositivo a través del nodo de la lista enlazada, y esta es la parte más ingeniosa de la implementación de la lista enlazada del núcleo.

#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
复制代码

内核链表获得设备结构体的首地址是通过container_of这个宏,我们来具体分析一下这个宏的实现,首先是它的三个参数ptr是成员变量的指针,type是结构体的类型,member是成员变量的名字。container _of宏的作用是通过结构体内某个成员变量的地址和该变量名,以及结构体类型。找到该结构体变量的地址。这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在结构中的偏移量,然后根据成员变量的地址反过来得出主结构变量的地址。下面具体分析各个部分: Typeof: 是用于返回一个变量的类型,这是GCC编译器的一个拓展功能,即typeof是编译器相关的,不是c语言中的某个标准。 (((type*)0)->member): ((type*)0)将0地址转换为type类型的结构体指针,也就是让编译器认为这个结构体是开始于程序段起始位置0,开始于0地址的话,我们得到的成员变量地址就直接相当于成员变量的偏移地址了。 (((type*)0)->member)引用结构体中member成员

const typeof( ((type *)0)->member ) *__mptr = (ptr);
复制代码

这里的意思是用typeof()获取结构体内member成员属性的类型,然后定义一个该类型的临时指针变量_mptr,将ptr所指向的member地址赋给_mptr,作用是防止对ptr和ptr指向的内容造成破坏。 其中用到的offsetof(type,member)

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
复制代码

这个宏的意思就是求出member相对于0地址的偏移量。

(type *)( (char *)__mptr - offsetof(type,member) );})
复制代码

这句话的意思是,把_mptr转换城char*类型,因为offsetof得到的偏移量是以字节为单位的。两者相减得到结构体的起始位置,再强转为type类型。自此我们便能得到设备结构体的首地址。 使用container_of宏,我们定义一个简单的函数便可返回包含list_head的父类型结构体。依靠list_entry方法,内核提供了创建,操作以及其他链表管理方法,你甚至不需要知道list_head所嵌入的对象数据结构。

可以看到Linux内核链表的实现不可谓不巧妙,通过对内存地址的通透理解,来实现通用的链表结构,才能更方便统一的管理内核中繁杂的设备。而内核中还有许多其它的巧妙实现思路,多多阅读并学习大牛的实现思路和巧妙方法,是能够显著提升我们日常开发效率的,同时也是对思维的一种提升。 阿木实验室致力于为机器人研发提供开源软硬件工具和课程服务,让研发更高效!

Supongo que te gusta

Origin juejin.im/post/7078500660425015332
Recomendado
Clasificación