语言基础/单向链表的构建和使用(含Linux中SLIST的解析和使用)

文章目录

概述

本文讲述了数据结构单链表的基本概念,头指针、头结点、数据域、指针域等链表的描述术语,及单链表操作的简单实现。并在此基础上详细讲讲述 Linux 源码中 SLIST 单链表系列宏的原理和使用方法。

简单的链表

在讲述单链表前,不得不先回顾下线性表的概念。所谓线性表,是零个或多个数据元素的有限序列(序列是指有顺序的排列)。线性表首先是一个序列,也就是说,元素之间是有顺序的,若存在多个元素,则第一个元素没有前驱,最后的元素没有后继,其他的每个元素都是有些只有一个前驱和后继。以学校的小朋友为例,如果大家分散在操场各处,则不能算是线性表。如果一个小朋友去拉两个小朋友的衣服,那就不可以排成一队了;同样,如果一个小朋友后边的衣服,被两个小朋友拉扯,也不算是线性表。另外,线性表强调有限,事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。

描述链表的术语

typedef struct Node {ElemType data;struct Node *next;
} Node;

通常,我们把Node称作一个节点,每个节点包含两个部分。其中存储数据元素信息的域(字段)称作数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息(即下一个节点的内存地址)称作指针或链。链表总得有个头,我们把链表中的第一个节点的存储位置(即第一个节点对象的内存地址,如果有头结点,则是头结点对象的内存地址)叫做头指针。为了更加方便地对链表进行操作,会在单链表的第一个节点前附设一个节点,称作头节点
头指针是链表的必要元素或者说是固有的,其具有标识作用,所以常用头指针来代表链表本身。无论链表是否为空,头指针均不为空。头指针指向链表的第一个节点的内存,若有头节点,则是指向头结点对象的指针。而,头节点不一定是链表的必要元素。头节点的数据域是不能向其他节点那样存储业务数据的,一般无意义空置,但你也可以在其中存储些自定义的其他数据信息,如存储链表长度。有了头节点,对在链表第一节点前插入节点和删除第一节点这两种操作,就会变得简单,使得其操作与其他节点的操作过程相统一。

下文示例程序中,使用了头结点,
在这里插入图片描述
如上,在使用头节点的情况下,头指针、头节点、普通节点之间的关系如上图。头指针Ph等于头节点(对象)在内存中地址,而头节点数据域不实际存储数据元素,只是存储了第一节点的地址。参照下文示例代码main函数中定义的 LinkList 类型的 L 即链表头指针的,它是一个节点类型的指针,结合InitList源码,可得,头指针的赋值过程为,

 struct Node *L = (Node*)malloc(sizeof(Node));//如下初始化过程,本质上操作的是头节点的指针域L->next = NULL; 

对于LIST的客户端来说,头指针是 Node* 和 void* 并没有什么本质区别,它就是一个地址值,只要能在LIST内部使用头指针找到头结点或第一节点就行,只是为了代码上的优雅和易读写性,头指针被顺便定义成了节点类型的指针类型。

简单实现一个单链表

这是以前从某书中的源码里扒拉出来的,只是做了简单的调整,前几年在项目里,我甚至偶尔直接在其基础上私有化一个单链表用于项目。这里贴出来,并不是说它好或者不好,只是为了有个参考,以更好的理解后续要讲述的Linux中SLIST单链表

#include <iostream>
#include <stdio.h>#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0//Your Data /can be struct
typedef int ElemType;
//线性表链式存储-单链表结构
typedef struct Node {ElemType data;struct Node *next;
} Node;//定义LinkList
typedef struct Node *LinkList; /* 初始化链式线性表 */
int InitList(LinkList *L) {*L = (LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */if (!(*L))                           /* 存储分配失败 */return ERROR;(*L)->next = NULL;                   /* 指针域为空 */return OK;
}//若L为空表,则返回TRUE,否则返回FALSE
int ListEmpty(LinkList L) {if (L->next)return FALSE;elsereturn TRUE;
}//将L重置为空表 
int ClearList(LinkList *L) {LinkList p, q;p = (*L)->next;           /*  p指向第一个结点 */while (p) {               /*  没到表尾 */q = p->next;free(p);p = q;}(*L)->next = NULL;        /* 头结点指针域为空 */return OK;
}//返回L中数据元素个数
int ListLength(LinkList L) {int i = 0;LinkList p = L->next;   /* p指向第一个结点 */while (p) {i++;p = p->next;}return i;
}//用e返回L中第i个数据元素的值 //1≤i≤ListLength(L)
int GetElem(LinkList L, int i, ElemType *e) {int j;LinkList p;		//声明一结点p p = L->next;    //让p指向链表L的第一个结点 j = 1;		    //j为计数器 //p不为空或者计数器j还没有等于i时,循环继续while (p && j < i) {p = p->next;   /* 让p指向下一个结点 */++j;}if (!p || j > i)return ERROR;  /*  第i个元素不存在 */*e = p->data;      /*  取第i个元素的数据 */return OK;
}//返回L中第1个与e满足关系的数据元素的位序 /若这样的数据元素不存在则返回0
int LocateElem(LinkList L, ElemType e) {int i = 0;LinkList p = L->next;while (p) {i++;if (p->data == e) /* 找到这样的数据元素 */return i;p = p->next;}return 0;
}//在L中第i个位置之前插入新的数据元素e,L的长度加1 //1≤i≤ListLength(L)
int ListInsert(LinkList *L, int i, ElemType e) {int j;LinkList p, s;p = *L;j = 1;while (p && j < i) {             /* 寻找第i个结点 */p = p->next;++j;}if (!p || j > i) return ERROR;   /* 第i个元素不存在 *//*  生成新结点(C语言标准函数) */s = (LinkList)malloc(sizeof(Node));  s->data = e;s->next = p->next;    /* 将p的后继结点赋值给s的后继  */p->next = s;          /* 将s赋值给p的后继 */return OK;
}//删除L的第i个数据元素,并用e返回其值,L的长度减1 //1≤i≤ListLength(L)
int ListDelete(LinkList *L, int i, ElemType *e) {int j;LinkList p, q;p = *L;j = 1;while (p->next && j < i) {	/* 遍历寻找第i个元素 */p = p->next;++j;}if (!(p->next) || j > i)return ERROR;           /* 第i个元素不存在 */q = p->next;p->next = q->next;			/* 将q的后继赋值给p的后继 */*e = q->data;               /* 将q结点中的数据给e */free(q);                    /* 让系统回收此结点,释放内存 */return OK;
}//遍历链表 //依次对L的每个数据元素输出 
int ListTraverse(LinkList L) {LinkList p = L->next;while (p) {printf("%d ", p->data); //dosmoething..p = p->next;}printf("\n"); return OK;
}int main() {LinkList L;ElemType e;//初始化int i = InitList(&L);//插入新元素for (int j = 1; j <= 5; j++)i = ListInsert(&L, 1, j * 10);//遍历ListTraverse(L);//获取第4个数据//GetElem(L, 3, &e);//删除第3个数据//ListDelete(&L, 3, &e); //...不再赘述...system("pause");
}

上述代码可以直接在C和C++环境中编译和运行,具体测试代码比较简单,没有过多在此涉及。

SLIST_187">Linux之SLIST机理分析

在这里插入图片描述
进入Linux官网,以HTTTP方式进入 Index of /pub/linux/kernel/ 页面,图个吉利,这里选择下载 linux-6.8.6.tar.xz 版本。解压后可以在相应的目录下找到 linux-6.8.6\drivers\scsi\aic7xxx\queue.h 文件。在Everything中搜索时,可以找到好几个queue.h文件,只有目录 drivers/scsi/aic7xxx 包含的 queue.h 是我们想要的那个。该目录Adaptec AIC-7xxx系列(例如AIC-7870、AIC-7895等)的SCSI(Small Computer System Interface)控制器相关的驱动程序,主要负责与硬件交互,控制SCSI设备,以及提供对SCSI设备的访问和管理功能。该文件中主要包含了单向链表、单向有尾链表(Singly-linked Tail queue 可用作队列)、双向无尾链表、双向有尾链表(Tail queue 可用作队列)、循环链表(Circular queue)的数据结构和操作函数,用于在Linux内核中实现队列和链表的功能。本文仅讲解其中最简单的单链表结构。

/** @brief* A singly-linked list is headed by a single forward pointer. * The elements are singly linked for minimum space and pointer manipulation overhead at the expense of O(n) removal for arbitrary elements. * New elements can be added to the list after an existing element or at the head of the list.* Elements being removed from the head of the list should use the explicit macro for this purpose for optimum efficiency. * A singly-linked list may only be traversed in the forward direction.  * Singly-linked lists are ideal for applications with large datasets and few or no removals or for implementing a LIFO queue.
**/#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
#define _Q_INVALIDATE(a) (a) = ((void *)-1)
#else
#define _Q_INVALIDATE(a)
#endif/** Singly-linked List definitions.*/
#define SLIST_HEAD(name, type)						\
struct name {								\struct type *slh_first;	/* first element */			\
}#define	SLIST_HEAD_INITIALIZER(head)					\{ NULL }//条目/列表元素
#define SLIST_ENTRY(type)						\
struct {								\struct type *sle_next;	/* next element */			\
}/** Singly-linked List access methods.*/
#define	SLIST_FIRST(head)	((head)->slh_first)
#define	SLIST_END(head)		NULL
#define	SLIST_EMPTY(head)	(SLIST_FIRST(head) == SLIST_END(head))
#define	SLIST_NEXT(elm, field)	((elm)->field.sle_next)#define	SLIST_FOREACH(var, head, field)					\for((var) = SLIST_FIRST(head);					\(var) != SLIST_END(head);					\(var) = SLIST_NEXT(var, field))#define	SLIST_FOREACH_SAFE(var, head, field, tvar)			\for ((var) = SLIST_FIRST(head);				\(var) && ((tvar) = SLIST_NEXT(var, field), 1);		\(var) = (tvar))/** Singly-linked List functions.*/
#define	SLIST_INIT(head) {						\SLIST_FIRST(head) = SLIST_END(head);				\
}#define	SLIST_INSERT_AFTER(slistelm, elm, field) do {			\(elm)->field.sle_next = (slistelm)->field.sle_next;		\(slistelm)->field.sle_next = (elm);				\
} while (0)#define	SLIST_INSERT_HEAD(head, elm, field) do {			\(elm)->field.sle_next = (head)->slh_first;			\(head)->slh_first = (elm);					\
} while (0)#define	SLIST_REMOVE_AFTER(elm, field) do {				\(elm)->field.sle_next = (elm)->field.sle_next->field.sle_next;	\
} while (0)#define	SLIST_REMOVE_HEAD(head, field) do {				\(head)->slh_first = (head)->slh_first->field.sle_next;		\
} while (0)#define SLIST_REMOVE(head, elm, type, field) do {			\if ((head)->slh_first == (elm)) {				\SLIST_REMOVE_HEAD((head), field);			\} else {							\struct type *curelm = (head)->slh_first;		\\while (curelm->field.sle_next != (elm))			\curelm = curelm->field.sle_next;		\curelm->field.sle_next =				\curelm->field.sle_next->field.sle_next;		\_Q_INVALIDATE((elm)->field.sle_next);			\}								\
} while (0)

如上代码中的注释部分。
在这里插入图片描述

结构定义

SLIST_HEAD 宏定义了一个名称为 name 的结构体,包含一个 type 类型的字段,其含义是指向第一个元素的指针。SLIST_ENTRY宏定义的是单链表中每个元素的结构,其中包含一个指向下一个元素的指针。抛却字段名称不谈,这俩定义是一致的,看起来有点重复,但SLIST_HEAD和SLIST_ENTRY在单链表的表示和用途上是不同的,这样的设计有助于提高代码的清晰性和可维护性。

//你的私有数据结构
typedef struct tagYourData {int a;int b;
} TYourData; 
//typedef int TYourData; //also//借助SLIST_ENTRY定义链表结构
struct TLucyItem {TYourData data;SLIST_ENTRY(TLucyItem) linkNode;
};//定义链表头变量
SLIST_HEAD(TslistHead, TLucyItem) slistHead;

结合上文,SLIST_ENTRY宏的功能很明确,也很好理解。哈哈,但是钻个小牛角尖,单词 entry 本意是进入、加入、入口,同时也具有条目、账目、记录等含义。在计算机中,有 data entry: [计]数据输入,entry point: [计]入口点,等含义。那么这里的entry怎么翻译呢?

	struct TLucyItem {TYourData data;struct {struct TLucyItem* sle_next;} linkNode;}

结合 SLIST_ENTRY 的实际使用,将结构 TLucyItem 定义展开如上。我给 SLIST_ENTRY 对象取名字 linkNode,含义为链表的连接点,链表连接位置的记录。就这样吧!也许 Entry 这个名字是大神凭借个人喜好采用的。如果不考虑字面意思,这里的 linkNode 代表的是链表结构中的指针域。在链表中,我们通常提到的是数据域和指针域。指针域承担着连接节点的作用,通过指针域,我们可以在链表中进行节点的插入、删除、查找等操作,实现链表的灵活性和可操作性。

链表头变量的展开,如下,
在这里插入图片描述
要特别注意的2点是,
SLIST_ENTRY 宏函数、SLIST_HEAD宏函数中的 type 参数,其代表的类型是 TLucyItem,而不是 TYourData 类型,通过代码的展开,很容易理解这一点。即,type不是字节的数据类型,而是包含自己数据类型的链表结构类型。
宏函数SLIST_INSERT_HEAD、SLIST_FOREACH、SLIST_REMOVE_HEAD等函数参数中都传递了head参数,函数内部把head被认做事指针来使用,因此,如果我们使用SLIST_HEAD定义头对象,而不是指针时,相关位置要传递&slistHead才可以。同理,我们在定义链表头时,也可以直接定义头指针,如下,这可能会更利于编码过程,

SLIST_HEAD(TslistHead, TLucyItem) *pslistHead;

单链表初始化

    //链表初始化SLIST_INIT(&slistHead);

单链表插入元素

宏函数参数中的,field 不光有田地、场地,处理、应付等含义,它还具有字段的意思,在编程领域其可代表结构体字段。

    //第一个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 1;item->data.b = 10;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//展开 _INSERT_HEAD//item->linkNode.sle_next = (&slistHead)->slh_first;//(&slistHead)->slh_first = item;

这里要特别注意的是,SLIST_INSERT_HEAD(head, elm, field) 宏函数的 head 参数,要传递的是 &slistHead,即slistHead的地址,而不是直接传递slistHead本身。

宏函数 SLIST_INSERT_AFTER(slistelm, elm, field)
函数参数中 slistelm 是列表中的某已知的节点,elm 是新要插入的节点,本函数的功能是,将结点elm插入到结点slistelm后面。

单链表遍历元素

    //遍历单链表SLIST_FOREACH(item, &slistHead, linkNode) {printf("%d, %d \r\n ", item->data.a, item->data.b);}

在这里插入图片描述
SLIST实际使用中,可能要在其基础上进行一些功能扩展,如,保持单链表中元素的唯一性,此时也会使用到遍历操作。

单链表删除元素

SLIST 提供了3个删除元素的函数,具体参见上一节的原码。

//删除elm指定的后一个节点
SLIST_REMOVE_AFTER(elm, field)
//删除头节点指定的节点
SLIST_REMOVE_HEAD(head, field)
//删除elm指定的节点
SLIST_REMOVE(head, elm, type, field)

链表清空方案1,

    //链表清空操作while (!SLIST_EMPTY(&slistHead)) {item = SLIST_FIRST(&slistHead); //printf("remove %d, %d \n", item->data.a, item->data.b);SLIST_REMOVE(&slistHead, item, TLucyItem, linkNode);free(item); //同步释放item堆内存}

链表清空方案2,

    //链表清空操作 //需要在另外的过程中释放item堆内存while (!SLIST_EMPTY(&slistHead)) {SLIST_REMOVE_HEAD(&slistHead, linkNode);}

上述列表清空操作的代码可以展开为,
在这里插入图片描述
需要注意的是,清空方案P2过程中并没有释放链表元素对应的堆内存,不小心地化会造成内存泄漏哈。

SLIST_385">Linux之SLIST使用实践

https://www.cnblogs.com/imlgc/archive/2012/05/02/2479654.html

//你的私有数据结构
typedef struct tagYourData {int a;int b;
} TYourData; //借助SLIST_ENTRY定义链表结构/注意没有使用typedef定义结构别名
struct TLucyItem {TYourData data;SLIST_ENTRY(TLucyItem) linkNode;
};int main() {//定义链表头变量 //更建议直接定义成指针SLIST_HEAD(TslistHead, TLucyItem) slistHead;//链表初始化SLIST_INIT(&slistHead);//链表元素项//要动态创建struct TLucyItem* item = NULL;//第一个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 1;item->data.b = 10;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//第二个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 2;item->data.b = 20;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//遍历单链表SLIST_FOREACH(item, &slistHead, linkNode) {printf("iterator %d, %d \r\n", item->data.a, item->data.b);}//链表删除操作while (!SLIST_EMPTY(&slistHead)) {item = SLIST_FIRST(&slistHead);printf("remove %d, %d \n", item->data.a, item->data.b);SLIST_REMOVE(&slistHead, item, TLucyItem, linkNode);free(item);}system("pause");
}

上述代码运行结果如下,
在这里插入图片描述

纯C中typedef重命名带来的问题

在Keil5集成开发环境(STMF429+FreeRTOS)下,标准C99,使用SLIST时,遇到了一些问题。主要代码如下,

//
typedef struct tagLucyItem {TYourData data;SLIST_ENTRY(tagLucyItem) linkNode;
} TLucyItem;
//直接定义成指针会方便些
SLIST_HEAD(TSListHead4Luck, TLucyItem) *s_pListHead; //主要功能代码
int do_something(){//TESTSLIST_INIT(s_pListHead);//开辟堆内存TLucyItem *ptNode = pvPortMalloc(sizeof(TLucyItem));ptNode->data.a = 100; ptNode->data.b = 100;//执行插入操作SLIST_INSERT_HEAD(s_pListHead, ptNode, linkNode);...
}

在编译时,存在如下编译错误,
在这里插入图片描述
先谈谈C语言中,为什么喜欢将结构定义typedef为一个别名。
在纯C环境下,我们通常要定义结构体的别名,如上使用typedef定义的TLucyItem类型。如果不这么做,那么任何出现TLucyItem类型名称的地方,都要使用 struct TLucyItem 样式,如前一章节中SLIST的实践代码那样。在C++中,对于结构体类型的定义和使用,可以不用去typedef别名,而是直接使用结构类型名称即可。正是因为这样,出现了上述编译错误。我们宏展开报错的代码,

do {			(ptNode)->linkNode.sle_next = (s_pListHead)->slh_first;			(s_pListHead)->slh_first = (ptNode);					} while (0)

一共两行代码,每行对应一个错误告警。第一个错误显示,右边 slh_first 是 struct TLucyItem* 类型,左边 sle_next 是 struct tagLucyItem*类型,类型不兼容。好吧,这也能报错,在C++中tagLucyItem都可以做构造函数名称啦。一点点改呗,

typedef struct tagLucyItem {TYourData data;SLIST_ENTRY(/*tagLucyItem*/TLucyItem) linkNode;
} TLucyItem;

如上修改 TLucyItem 定义后,果然只剩下第2个错误告警了。我们继续来看看这个错误。右边 TLucyItem* 类型和左边 struct TLucyItem* 类型不兼容,好吧,这也太死板啦,就不能变通一点点。slh_first 是struct TLucyItem*类型,其中关键字struct是在通过SLIST_HEAD宏定义头结构时被宏定义函数体添加的。分析到这里,问题的原因算是确定了,struct Taa 和 Taa 在C编译过程中不兼容。有两种解决方案,
P1、这是不建议的方案。修改SLIST宏实现,将宏实现中 type 参数前的 struct 全部干掉。
P2、去掉上述TLucyItem的别名定义,直接定义它。当在程序内部使用到该结构类型时,统一的加上struct关键字使用它。好在在SLIST使用的过程中节点类型TLucyItem并不会多次使用,这种方案是可行的。不改动引用的源码,所以推荐。

预留

好了,该睡觉了。


http://www.ppmy.cn/news/1516627.html

相关文章

GitHub 与 AWS CodeCommit

代码库对决 欢迎来到雲闪世界。在软件开发领域&#xff0c;高效管理代码至关重要。Git 存储库等版本控制系统 (VCS) 是无名英雄&#xff0c;为代码更改、协作和历史跟踪提供了安全避风港。在选择合适的存储库平台时&#xff0c;出现了两个巨头&#xff1a;GitHub 和 AWS CodeC…

redis的aof日志配置项详解

Redis 的 AOF&#xff08;Append-Only File&#xff09;日志是一种持久化机制&#xff0c;用于记录数据库的所有写操作&#xff0c;以便在 Redis 重启时能够重建数据集。配置 AOF 日志时&#xff0c;有几个重要的配置项&#xff0c;下面是每个配置项的详细说明&#xff1a; app…

swagger,Knife4j和Yapi

目录 swagger swagger的作用 swagger的使用 一.导入依赖 二.创建swagger配置类&#xff0c;交给SpringIoC容器管理 三.使用swagger依赖的注解来给接口层(controller)的各种方法进行注释 Api ApiOperation ApiImplicitParam ApiModel ApiModelProperty 四&#xff1a;…

在建设网站需要注意哪些安全防护事项

企业如果正准备建设网站千万要注意网站安全防护&#xff0c;网站安全不可以忽视&#xff0c;今天来讲讲关于网站建设安全注意事项。 1、网站源代码要安全 你是不是发现平时浏览网站操作过程中&#xff0c;发觉文章标题和内容压根不对付&#xff0c;显示的是其它与公司毫不相干…

24. 重置dataframe的索引

哈喽,大家好,我是木头左! 在数据分析和处理过程中,经常需要对dataframe进行各种操作,其中之一就是重置索引。重置索引可以帮助更好地管理和组织数据,提高数据处理的效率。本文将详细介绍如何使用pandas库中的reset_index()函数来重置dataframe的索引。 1. reset_index()函…

浅析裸土检测算法的实际应用及裸土检测算法源码样本

在环境保护和农业管理的持续推进中&#xff0c;裸土检测算法作为一种先进的技术工具&#xff0c;发挥着越来越重要的作用。它不仅提升了裸土监测的效率和准确性&#xff0c;还在实际应用中展示了巨大的潜力。本文将探讨裸土检测算法在实际应用中的表现&#xff0c;揭示其带来的…

Ant Design Vue中Modal.confirm无法自动关闭

温馨tips:着急看解决方法可跳过碎碎念~ 前两天经理扔给我一个问题&#xff1a;“这个弹窗怎么关不上了&#xff1f;” 我怀着无所谓的心态&#xff1a;小意思啦&#xff0c;5分钟之内解决完~ …当然flag是不能随便乱立的 拉下来项目&#xff08;原神启动&#xff08;不是&…

利用漏洞实现 Outlook 的 RCE:第 2 部分

## 攻击面 Outlook 要播放的声音文件是波形音频文件格式( WAV)。它通过接收声音文件路径的PlaySound函数播放。PlaySound将加载文件、解析它,然后调用soundOpen,后者将调用不同的波形函数,例如waveOutOpen。 WAV 文件充当多个音频编解码器的容器(或包装器)。编解码器是一…

线下参会报名丨智源数据与行业应用 Workshop 第二期

目前&#xff0c;大模型在数据基建和行业落地仍存在不少挑战。北京智源人工智能研究院深耕数据工具研发与数据平台建设&#xff0c;并持续推动模型与垂直场景的应用&#xff0c;旨在通过举办“数据与行业应用系列Workshop”活动&#xff0c;广泛链接生态伙伴&#xff0c;共同探…

【RabbitMQ高级特性】消息可靠性原理

1. 消息确认机制 1.1 介绍 我们可以看到RabbitMQ的消息流转图&#xff1a; 当消息从Broker投递给消费者的时候会存在以下两种情况&#xff1a; consumer消费消息成功consumer消费消息异常 如果说RabbitMQ在每次将消息投递给消费者的时候就将消息从Broker中删除&#xff0c…

RAFT:Adapting Language Model to Domain Specific RAG

论文链接 简单来说&#xff0c;就是你SFT微调的时候得考虑RAG的场景。 RAG什么场景&#xff1f;你检索top-k回来&#xff0c;里面有相关doc有不相关doc&#xff0c;后者是影响性能的重要原因&#xff0c;LLM需要有强大的识别能力才能分得清哪块和你的query相关。微调就是为了这…

Apache CloudStack Official Document 翻译节选(九)

关于 Apache CloudStack 的 最佳实践 &#xff08;三&#xff09; 配置云外的 防火墙与交换机 对Cisco VNMC&#xff08;Cisco Virtual Network Management Center&#xff09;设备集成云外的客户机网路防火墙&#xff1a; 思科虚拟网络管理中心为思科网络虚拟服务提供了中心…

【Rust光年纪】深度解读:Rust语言中各类消息队列客户端库详细对比

选择最佳 Rust 消息队列客户端库&#xff1a;全面对比与分析 前言 随着现代应用程序的复杂性不断增加&#xff0c;消息队列成为构建可靠、高性能系统的重要组件。本文将介绍一些用于Rust语言的消息队列客户端库&#xff0c;包括AMQP、Apache Kafka、NSQ、Apache Pulsar和Rock…

GoWeb 设置别名和多环境配置

别名 vite.config.ts中添加代码如下即可 //设置别名resolve: {alias: {"": path.resolve(process.cwd(),"src"),//用替代src}}随后即可使用 配置多环境 vite.config.ts中添加代码如下 envDir: ./viteenv,//相对路径随后在项目根目录创建对应的viteenv…

什么是 SQL 注入,有哪些类型,如何预防?

如果说数据是系统的核心&#xff0c;那么SQL注入就是直插系统核心的漏洞。一直以来SQL注入漏洞就被列入OWASP最常见和影响最广泛的十大漏洞列表中。 SQL注入漏是系统漏洞中一种比较严重的漏洞&#xff0c;如果说数据是系统的核心&#xff0c;那么SQL注入就是直插系统核心的漏洞…

Web应用服务器Tomcat

一、Tomcat的功能介绍 Tomcat 服务器是一个免费的开放源代码的Web 应用服务器&#xff0c;属于轻量级应用服务器&#xff0c;在中小型系统和 并发访问用户不是很多的场合下被普遍使用&#xff0c;Tomcat 具有处理HTML页面的功能&#xff0c;它还是一个Servlet和 JSP容器。 官网…

PHP多门店民宿酒店预订系统小程序源码

&#x1f3e8;✨「多门店酒店民宿预订系统」——一键解锁全球住宿新体验&#xff01;&#x1f30d;&#x1f3e0; &#x1f31f; 开篇种草&#xff1a;旅行新伙伴&#xff0c;预订无忧&#xff01; 嘿小伙伴们&#xff0c;是不是每次计划旅行都被繁琐的酒店民宿预订搞得头大&…

uniapp封装请求

封装请求有两种&#xff1a; 一种是在服务端判断token是否失效&#xff0c;一种是在小程序端判断token是否过期&#xff0c;&#xff0c; 第二种在前端判断要简单些&#xff0c;&#xff0c;在拿到token的时候&#xff0c;并在前端设置一个token的过期时间的毫秒值&#xff0c…

分布式核心问题总结

一、幂等性 所谓幂等就是一次或多次操作同一个资源&#xff0c;所产生的影响均一致。产生问题的原因&#xff1a;网络阻塞和延迟、用户重复操作一锁 二判 三更新 三步严格控制顺序&#xff0c;确保加锁成功后进行数据查询和判断&#xff0c;幂等性判断通过后再更新&#xff0…

OpenCV绘图函数(3)判断点一条直线是否在一个矩形范围内的函数clipLine()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 裁剪线段与图像矩形相交的部分。 cv::clipLine 函数计算出完全位于指定矩形内的线段部分。如果线段完全位于矩形之外&#xff0c;则返回 false。…