AlexiaChen / AlexiaChen.github.io

My Blog https://github.com/AlexiaChen/AlexiaChen.github.io/issues
88 stars 11 forks source link

对Nginx队列源码ngx_queue_t的一次分析 #14

Open AlexiaChen opened 5 years ago

AlexiaChen commented 5 years ago

title: 对Nginx队列源码ngx_queue_t的一次分析 date: 2017-11-18 13:30:00 tags:

前言

前几天公司的群里讨论了Nginx内部实现的一个数据结构,ngx_queue_t。 实质上是一个双向链表,但是当时讨论没有讨论出结果来,原因我自己也被绕晕了,今天本着从问题本身出发,不被别人的想法思路所左右,不看网络上任何文章的分析,好好写测试用例来证明自己的想法,其实那天的问题是围绕着两个C语言的宏展开的,那么这篇文章也会重点讲解这两个宏的意义:offsetof 和 ngx_queue_data。所以这篇文章不会涉及到链表插入,删除等细节。

正文

首先,Nginx源码实现这个数据结构,用了大量的宏,我们先用我们的思路,把关键信息抽取出来:

#define u_char unsigned char  
typedef struct ngx_queue_s  ngx_queue_t;

struct ngx_queue_s {
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

以上代码就是这个链表的关键,ngx_queue_s 和 ngx_queue_t都是链表的节点类型,结构体中有链表的前驱(prev)和后驱(next),以实现正反向遍历。唉? 等等,如果大学的时候,上过数据结构这门课的人会有点奇怪,会这样想:链表的节点应该有相关的数据啊,不然链表有什么意义? 是的,链表的节点必须有数据,但是上面这个结构体却没有,这是怎么回事?如果在当时,我想大部分人会写成以下这样:

struct ngx_queue_s {
    int data;             // 数据
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

这样每访问一个节点的时候,就可以通过节点的指针指向数据了。但是,你有没有想过,万一这个链表的数据是人名呢?不是int型,又或是其他复杂点的类型怎么办? 是不是又要重新定一个新的链表节点类型? 这样就麻烦了不少,软件工程里面奉行了一个原则,这个原则就是DRY原则。所以为了让不同类型采用同一个数据结构实现,就需要类型与数据结构实现分离,让这个数据结构的实现适配不同的类型,当然C++中有STL已经帮你做好了,Java也是。但是C语言就不支持泛型,Nginx由于是C语言实现的,所以采用了一种叫寄宿链表的思想,这种思想就是分离数据类型与数据结构实现的。

看到这里,就有人会想,这种思想跟文章要分析的两个宏有什么关系呢? 现在我可以先放结论,它们之间有莫大的关系,正因为采用了寄宿链表的思想,才会间接产生了那两个宏,这种变形掩盖了一些实质。所以其实宏不是关键,是寄宿链表的思想才是关键!我们需要知道Why,而不是How,不然会深入无限的细节当中去。

话说回来,下面我们来看那两个宏的实现:

// 取struct_type类型的成员相对于struct_type基地址的偏移量(字节数) 一定要是POD 类型
#define offsetof(struct_type, member)   ((size_t) &((struct_type *) 0)->member)   

#define ngx_queue_data(node_addr,struct_type,member)  (struct_type *)((u_char*)node_addr - offsetof(struct_type, member))

我们发现ngx_queue_data这个宏复用了offsetof这个宏,那么首先分析offsetof这个宏:

这个宏根据名字可以很容易就看出来,就是字面意思,取某个结构体成员相对于结构体基址的偏移量(字节)。我们写一段代码测试来验证我们的想法:

typedef struct _SFamily{
    int id;
    char addr[256];
    int nums;
}Family;

//编译时断言,所谓的静态断言, 所以offsetof这个宏在编译时就计算出了结构体成员的偏移
static_assert(offsetof(Family, nums) == 260,"nums offset is 260");
static_assert(offsetof(Family, id) == 0, "id offset is 0");
static_assert(offsetof(Family, addr) == 4, "addr offset is 4");

根据代码结果,轻而易举的验证了我们的想法,而且这个求偏移是静态编译时就能得出的。不然以上代码直接不能通过编译。

好了,之后我们先别着急研究ngx_queue_data这个宏是怎么回事。先来看看怎么使用这个数据结构,怎样来用操作数据结构的一些方法,Nginx中已经定义好了,但是这些方法,是大量的宏定义的,这些宏不是关键,我把它们都改成了C/C++的函数形式:

#define u_char unsigned char  

// 取struct_type类型的成员相对于struct_type基地址的偏移量(字节数) 一定要是POD 类型
#define offsetof(struct_type, member)   ((size_t) &((struct_type *) 0)->member)   

typedef struct ngx_queue_s  ngx_queue_t;

struct ngx_queue_s {
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

void ngx_queue_init(ngx_queue_s* q){
    (q)->prev = q;                                                          
    (q)->next = q;
}

bool ngx_queue_empty(ngx_queue_s* head){
    return head == head->prev;
}

void ngx_queue_insert_head(ngx_queue_s* head, ngx_queue_s* node){

    node->next = head->next;
    head->next->prev = node;
    head->next = node;
    node->prev = head;

}

void ngx_queue_insert_after(ngx_queue_s* pos, ngx_queue_s* node){
    ngx_queue_insert_head(pos, node);
}

void ngx_queue_insert_tail(ngx_queue_s* tail, ngx_queue_s* node){

    node->prev = tail->prev;
    tail->prev->next = node;
    tail->prev = node;
    node->next = tail;
}

// 头节点和尾节点都是哑节点(dummy node),并不存储实际数据
ngx_queue_s* ngx_queue_head(ngx_queue_s* head){
    return head->next;
}

ngx_queue_s* ngx_queue_last(ngx_queue_s* tail){
    return tail->prev;
}

//直接取 head dummy node
ngx_queue_s* ngx_queue_sentinel(ngx_queue_s* head){
    return head;
}

//当前节点的下一个节点
ngx_queue_s* ngx_queue_next(ngx_queue_s* cur){
    return cur->next;
}

//当前节点的上一个节点
ngx_queue_s* ngx_queue_prev(ngx_queue_s* cur){
    return cur->prev;
}

//删除节点
void ngx_queue_remove(ngx_queue_s* node){
    node->prev->next = node->next;
    node->next->prev = node->prev;

}

#define ngx_queue_data(node_addr,struct_type,member)  (struct_type *)((u_char*)node_addr - offsetof(struct_type, member))

这些函数的用法官方都提供了,我们根据用法,自己实现些测试用例,我们打算实现两个链表,一个是存放书籍信息的,一个是存放人员信息的,写入数据,然后遍历打印它们,看看接口是否正确:

typedef struct _Book{
    ngx_queue_t link;
    int type;
    char name[256];
}Book;

typedef struct _Person{

    int sex;
    char name[256];
    ngx_queue_t link;
}Person;

 Book book1, book2, book3;
    Person person1, person2, person3;

    static_assert(offsetof(Book, link) == 0, "link offset is 0");
    static_assert(offsetof(Person, link) == 260, "link offset is 260");

    book1.type = 1;
    book2.type = 2;
    book3.type = 3;

    person1.sex = 1;
    person2.sex = 0;
    person3.sex = 1;

    strcpy(book1.name, "Harry Potter");
    strcpy(book2.name, "the lord of ring");
    strcpy(book3.name, "Jame");

    strcpy(person1.name, "Fan BingBing");
    strcpy(person2.name, "Wu YanZu");
    strcpy(person3.name, "ShuQi");

    //初始化队列
    ngx_queue_t bookQueue;
    ngx_queue_init(&bookQueue);
    ngx_queue_insert_head(&bookQueue, &book1.link);
    ngx_queue_insert_tail(&bookQueue, &book2.link);
    ngx_queue_insert_tail(&bookQueue, &book3.link);

    ngx_queue_t personQueue;
    ngx_queue_init(&personQueue);
    ngx_queue_insert_head(&personQueue, &person1.link);
    ngx_queue_insert_tail(&personQueue, &person2.link);
    ngx_queue_insert_tail(&personQueue, &person3.link);

    for (auto iter = ngx_queue_head(&bookQueue); iter != ngx_queue_sentinel(&bookQueue); iter = ngx_queue_next(iter))
    {
        Book* bookPtr = (Book*)ngx_queue_data(iter, Book, link);

        printf("Book name is: %s . type is %d \n", bookPtr->name, bookPtr->type);
    }

    for (auto iter = ngx_queue_head(&personQueue); iter != ngx_queue_sentinel(&personQueue); iter = ngx_queue_next(iter))
    {
        Person* personPtr = (Person*)ngx_queue_data(iter, Person, link);

        printf("Person name is: %s . sex is %s\n", personPtr->name, personPtr->sex == 1 ? "Female" : "Male");
    }

以上代码的打印结果如我们所想,是正确的。注意了,我们遍历访问链表单个节点的时候用了ngx_queue_data这个宏,正是我们需要了解的宏。看来跟C语言数组,C++ vector的遍历访问差不多嘛,访问一个节点元素需要的就是节点的下标(或者说地址)。

接下来我们再看链表节点的真正的数据是定义在哪?

typedef struct _Book{
    ngx_queue_t link;
    int type;
    char name[256];
}Book;

typedef struct _Person{

    int sex;
    char name[256];
    ngx_queue_t link;
}Person;

注意了,以上两个结构体里面都有链表节点类型ngx_queue_t,变量名link,想想,是不是数据类型与数据结构就分离了? 不同的数据类型复用了相同类型的数据结构? 这样访问节点就可以通过它们的link成员来正反向迭代了,所以之前的代码iter变量实际就是link成员的地址,我们可以访问每个结构体里面的link成员变量了,那么怎么知道结构体其他数据呢?没有地址,不知道在哪啊? 其实关键就是通过link成员的地址来计算出结构体的地址的,下面举个例子:

// iter为链表节点的地址,而链表节点又在Book和Person结构体中
Book* bookPtr = (Book*)ngx_queue_data(iter, Book, link);

Person* personPtr = (Person*)ngx_queue_data(iter, Person, link);

看到上面代码并且联系ngx_queue_data宏的实现,是不是就明白了?

链表节点成员的地址 ➖ 链表节点成员相对于结构体基址的偏移 = 结构体的基址

那么是不是就可以通过结构体基址的地址强制转换为结构体的指针,访问结构体其他数据成员了?还不明白? 那么再看下面:

typedef struct _Book{
    ngx_queue_t link; // 这个link变量的地址知道了,是不是就可以知道Book结构体基址了?  link的地址假如为123,link的偏移为0,那么123 - 0 = 123。123为数据的结构体地址
    int type;
    char name[256];
}Book;

typedef struct _Person{

    int sex;
    char name[256];
    ngx_queue_t link;// 这个link变量的地址知道了,是不是就可以知道Person结构体基址了?  link的地址假如为300,link的偏移为260,那么300 - 260 = 40。40为数据的结构体地址
}Person;

EOF