先说下这个“题目”的背景,在开发一种云网关,这种网关的主要功能是集成硬件驱动,比如Modbus、DL/T645、S7PPI等工控现场驱动,采集工控现场数据,然后将数据传输到云平台上(阿里云、华为云等)。相当于在网关中嵌入了一个 小型的 工控组态软件,对于工控软件,常规的操作流程是:
1、创建设备表
由于每个设备要采集的变量有很多,而且这些变量很多时候不是连续的,比如变量1地址为1,变量2地址为3,变量3的地址为5,变量4的地址为7,变量5的地址为10,这个时候,我们当然可以每个变量读一次,但是这种方式效率过低,而且操作硬件接口(串口)也比较频繁,所以一般采取的方式时,一次读取地址1~地址10,供10个地址的值,然后从这10个地址中筛选我们需要的变量1~5。所以这种操作就是创建设备表。所以这里的“设备”不一定就是一个设备,更精确的说,应该是一个设备的一段连续地址。由于现场情况不同,所以创建的设备表个数是不同的,也就是要读取设备中,连续地址段 的个数是不同的,那么存储这些设备返回的数据,还是需要通过动态申请内存来存储的。所以这里采用 动态申请内存+链表存储,所以我们的设备表是 链表结构。
2、创建变量表
经过步骤1后,我们就可以创建我们 想要的 变量了,就是上面的变量1、2、3....,同样的道理,我们也是采用 动态申请内存+链表的方式, 所以 要采集 到的所有变量也 是 链表结构。
3、创建要 上传云服务器的变量表
这里如果是初次接触网关的人可能会疑惑,为什么还要再次创建一次要上传到云服务器的变量表呢,要传什么变量就在步骤2中上传就可以了啊? 这种想法当然是没问题的,但是 我们实际的应用情况可能是现场采集的变量有很多,因为 需要在本地显示,但是上传到云服务器上的变量 并不需求全部上传,而是只上传 重要的一些变量,这一方面是资费的考虑,还有 一方面是 云服务器的一些限制,比如阿里云的 产品只支持200个物模型,可以理解成阿里云对于单个设备,只支持200个变量。所以我们需要再次创建 要上传到云服务器的变量表,这里可以是动态内存+链表,也可以是直接按 固定方式申请,这个看自己的习惯了。
到了步骤3,就引出了我们这篇文章标题中的问题了,由于变量表 是链表结构,链表结构的缺点就是地址不连续, 这就导致我们不能像访问数据那样,直接去锁定链表的某个节点,进而访问链表节点中的值,所以 从链表中查找某个节点,只能是遍历这个链表,这么说可能比较抽象,来做个简易图:
关于链表操作的相关知识,可以参考之前写的文章《深入理解Linux内核之链表 list.h 功能实现原理、接口说明及示例代码》
我们的问题就是, 需要从这6个变量中,查找变量1、3、5。如果只是从这6个变量中查找3个变量,那这篇文章就没有任何意义了,我们来增加点难度,也是 这篇文章的终极目的:
(1)我们的变量有2000个,也就是链表节点有2000个。
(2)我们需要从这2000个节点中,找到随机的200个节点。
(3)步骤2要每隔3秒执行一次,将找到的随机 节点 上传到云服务器。
由于链表的查找目前仅支持遍历,也就是找1个点需要遍历整个链表,所以需求2中从2000个节点找200个随机 点的笨方法就是遍历200次链表,这个貌似也不难,大不了轮询200次嘛,但是需求3中就比较难了,每隔三秒就要来一轮 200次的 从2000节点查找200随机节点,这个如果不使用点技巧,将极大的影响程序运行效率。那么如何解决这个问题呢?这就是今天文章的重点。
尽管链表中的节点有 “关联”的特点,但是节点的本身的地址不是连续的,这也是为什么链表查找只支持遍历, 不支持定点查询,所以解决上述问题的难点 也就是如何定点 访问 链表节点, 这里的“ 定点”其实就是如何确定 链表 节点的 具体地址。如果我们能确定我们要筛选的 节点地址,然后将这个地址存储起来,以后我们 通过这个地址,直接锁定节点,然后读取链表节点的参数,具体操作步骤是:
(1)遍历整个变量链表,然后将需要筛选出的节点地址 存储。
(2)需要执行周期 筛选 操作时, 直接通过 存储到的 节点地址,获取节点内容。
所以 这里又引申出一个问题,就是步骤1中“链表节点地址的存储”,这里存储的是“地址”,能存储地址的只有“指针”,存储一个节点,需要1个指针,存储n个节点,就需要n个指针,所以我们需要 一个指针数组,用于存放这些节点地址。由于要筛选的变量个数不定,所以这个指针数组的大小最好也是可以 自由设定的,这个时候,二级指针就派上了用途,二级指针可以简单的理解为可以 动态分配大小的 指针数组,关于二级指针的知识点,可以参考之前的文章:
《如何理解C指针及二级指针》、《如何理解C指针及二级指针(1):二级指针的使用方式》。
接下来,我们就以示例代码来实现标题中的 方法。
程序伪操作:
/* 定义一个 链表节点类型的 二级指针 */
/* 对这个二级指针 进行动态内存申请,个数就是我们想要筛选的变量个数 */
/* 遍历整个链表,将 需要筛选出来的变量对应节点的 地址赋值给 二级指针,二级指针地址跟着递增 */
经过这三个步骤的操作,我们就能遍历一次,获取到 需要筛选的节点地址了,然后当我们执行周期筛选操作的时候,只需要直接按照顺序对二级指针进行引用,获取节点地址,进而获取链表节点中的内容。
具体代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "infra_list.h"
//示例链表结构
typedef struct dev_info_list{
int id;
int para;
char buf[10];
struct slist_s list;
} dev_info_list_t;
//定义 链表 头指针
static struct slist_s list_head;
//定义用于 存储筛选节点地址的 二级指针
static dev_info_list_t **ppvar_node = NULL;
int main(int argc, char *argv[]) {
int i,j;
int need_find[10] = {1,3,5,7,9,11,13,15,17,19};
dev_info_list_t *pdev_info_list_tmp = NULL;
struct slist_s *head = &list_head;
//初始化链表 头
slist_init(head);
// 二级指针的 动态内存申请,这里我们要筛选10个 节点,所以
ppvar_node = (dev_info_list_t **)malloc(10*sizeof(dev_info_list_t *));
// 链表节点 赋值,这里创建了 20个 链表节点
for(i = 0; i < 20; i++){
pdev_info_list_tmp = (dev_info_list_t *)malloc(sizeof(dev_info_list_t));
pdev_info_list_tmp->id = i + 1;
pdev_info_list_tmp->para = i + 1;
for(j = 0; j < 10; j++){
pdev_info_list_tmp->buf[j] = i*2 + j;
}
slist_add_tail(&(pdev_info_list_tmp->list), head);
}
/* 打印链表所有节点*/
slist_for_each_entry(head, pdev_info_list_tmp, dev_info_list_t, list){
printf("id:%d ", pdev_info_list_tmp->id);
printf("para:%d\n", pdev_info_list_tmp->para);
printf("buf data:");
for(i = 0; i < 9; i++){
printf("<%.3d>", pdev_info_list_tmp->buf[i]);
}
printf("<%.3d>\n", pdev_info_list_tmp->buf[i]);
}
/* 遍历10次链表,筛选出我们想要 的 节点id对应的节点地址,并存储*/
j = 0;
for(i = 0; i < 10; i++){
slist_for_each_entry(head, pdev_info_list_tmp, dev_info_list_t, list){
if(pdev_info_list_tmp->id == need_find[i]){
ppvar_node[j++] = pdev_info_list_tmp;
}
}
}
/* 打印 筛选出的 节点内容*/
printf("/*--------------------------------------------------------*/\n");
for(i = 0; i < 10; i++){
printf("id:%d ", ppvar_node[i]->id);
printf("para:%d\n", ppvar_node[i]->para);
printf("buf data:");
for(j = 0; j < 9; j++){
printf("<%.3d>", ppvar_node[i]->buf[j]);
}
printf("<%.3d>\n", ppvar_node[i]->buf[j]);
}
return 0;
}
总结:
这里的头文件<infra_list.h> 可以参考之前的文章《深入理解Linux内核之链表 list.h 功能实现原理、接口说明及示例代码》
这里的筛选查找,只需要执行一次,然后后面我们就可以直接通过地址直接访问节点,效率自然提高了。当然了,如果想要筛选的 变量变了, 需要重新执行一次筛选操作。
转载:https://blog.csdn.net/u012351051/article/details/101787737