023+limou+C语言的“可变参数列表”和“命令行参数”以及“递归调用”

news2024/11/25 13:23:16

0.前言

您好,这里是limou3434的一篇博文,感兴趣可以看看我的其他内容。本次我给您带来了C语言的“可变参数列表”,要明白这些内容,您可能需要重新复习下C语言视角的栈帧空间知识。最后我还给出两个小的C语言知识点:“命令行参数”和“递归调用”,这旨在补全您的C语言知识面。

1.函数栈帧(x86环境)

1.1.认识通用寄存器

  1. ebp:栈底寄存器
  2. esp:栈顶寄存器
  3. eip:指令寄存器,保存当前指令的下一条指令的地址
  4. eax:通用寄存器,保留临时数据,常用于返回值
  5. ebx:通用寄存器,保留临时数据
  6. ecx:通用寄存器,保留临时数据

1.2.认常见的汇编指令

  1. mov:数据转移指令
  2. push:数据入栈,同时esp栈顶寄存器也会发生改变/浮动
  3. pop:数据弹出至指定位置,同时esp栈顶寄存器也会发生改变/浮动
  4. sub:减法命令
  5. add:加法命令
  6. call:函数调用,作用有两个“1.压入返回地址2.转入目标函数”
  7. jump:通过修改eip,转入目标函数,进行调用
  8. ret:恢复返回地址,压入eip,类似“pop eip”命令

1.3.函数调用以及相对应的汇编代码

可以去看看我之前的博客

1.4.栈空间图解补充

关于我之前讲解的栈帧空间图解,还有一些细节的方面在这里需要强调,以便后续理解可变参数列表。

  1. 首先“main()”函数也是被其他函数调用的,这个函数就是“__mainCRTStartup()”,而调用这个函数的函数就是“mainCRTStartup()”,再深入探究就是操作系统的知识了
  2. 临时变量在函数被正式被调用之前就形成了
  3. 形参的实例化顺序是右向左的(即:对传过来的参数,从右向左依次进行push)
  4. 使用call指令调用完函数后,就需要返回call下一个命令的地址(所以可以看到在拷贝完变量后,栈空间又压入了call下一条指令的地址,然后进行跳转。这些都是call指令干的,即:call有两个作用)
  5. 减值来开辟空间是由编译器决定的,而减去的大小是编译器依靠类型来减去的(编译器有能力知道所有数据类型对应的定义变量的大小)
  6. 函数调用时,因拷贝形成的临时变量,在变量与变量之间有一定规律可循。例如:在push两个变量a、b时,可以发现a、b的地址空间是连续的(但这只限于较老的设备),因此是可以做到通过a的地址来修改b,如下代码(注意并不是所有情况下,下面的代码都有效。要看具体的编译器,在某些老的编译器里容易通过预测栈空间的存储顺序来反向推测代码的逻辑,进而达到篡改代码的效果,这是极其不安全的,易被他人入侵,因此很多编译器为了安全,都对此做了优化,其push后各临时变量的地址并不一定会连续。例如有种栈保护技术叫做“金丝雀技术(Canary机制)”,就是为了为此而诞生的,您可以去了解一下)
int Add(int a, int b)
{
  	printf("Before:%d\n", b);
	*(&a + 1) = 100;
	printf("After:%d\n", b);
  	int z;
  	z = a + b;
  	return z;
}
  1. 函数在被调用完后就会被销毁
  2. 在函数内定义的临时变量,其空间都是在该函数的栈帧内开辟的。另外,临时变量具有临时性的本质是因为函数栈帧会被销毁(由于临时变量空间在栈帧内开辟,栈帧销毁那么临时变量也就跟着销毁了)
  3. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
  4. 从栈帧空间可以看出,即便函数原型是不需要传参数的,而在调用函数的时候依旧进行了传参,也没有任何影响(只是进行了多次的push而不使用罢了。这点很重要,有助于理解后面的可变参数列表)

2.可变参数列表

2.1.可变参数列表的使用

在我们了解可变参数列表的原理之前,可以先来看看可变参数列表具体的使用是怎么样的。有了这一铺垫,哪怕您看不懂原理,使用起可变参数列表也是没有问题的。

#include <stdio.h>
#include <windows.h>
int GetMax(int number, ...)//注意:可变参数要被使用,则其前面至少有一个及以上个明确参数
{
	//使用四个宏来编写代码:va_list、va_start、va_arg、va_end
	//由于我们自己是不太可能在栈帧空间中一一找到所有临时变量对应的地址并且进行解引用,所以我们为了方便使用,C语言提供了“三个操作符”和“一个类型符”,来完成寻找临时变量的操作
	va_list arg;//1.定义一个可以访问可变参数部分的变量,其实就是一个char*类型的变量,这意味着该变量可以按照一个字节的方式访问数据 
	va_start(arg, number);//2.使arg指向可变参数部分
	int max = va_arg(arg, int);//3.根据类型大小可以获取可变参数列表中的参数数据(这里获取的是第一个int参数数据)
	for (int i = 0; i < number - 1; i++)
	{
		int x = va_arg(arg, int);//4.持续获取下一个参数
		if (max < x)
		{
			max = x;
		}
	}
	va_end(arg);//5.arg使用完毕,收尾工作,本质就是将arg指向NULL(类似free的使用,避免arg成为野指针)
	return max;
}
int main()
{
	int max = GetMax(5, 1, 2, 3, 4, 5);
	printf("%d", max);
	return 0;
}

2.2.可变参数列表的原理(x86环境)

本质就是利用:哪怕函数调用的时候使用了多余的实际参数,栈帧空间也会创建临时变量来保存这些实际参数,并且是根据参数列表从右向左创建对应的临时变量(从右往左一一进行push)。

那么具体的细节应该怎么理解呢?

  1. 首先在创建临时变量的时候,编译器依次push了5, 4, 3, 2, 1, 5这六个变量(注意顺序在函数调用中是GetMax(5, 1, 2, 3, 4, 5),即:从右到左push)
    在这里插入图片描述

  2. 然后根据va_list arg;这就定义一个可以访问可变参数部分的变量,其实就是一个char*类型的变量,这意味着该变量可以按照一个字节的方式访问数据

  3. va_start(arg, number);使arg指向可变参数部分,即指向临时变量number的地址

  4. int max = va_arg(arg, int);根据类型大小可以获取可变参数列表中的参数数据(这里获取的是第一个int参数数据)

  5. for (int i = 0; i < number - 1; i++) { int x = va_arg(arg, int)};这个语句则持续获取下一个参数(在x86比较好演示,甚至于我们可以根据栈帧规则手动创建一个不需要“…”可变参数的可变参数函数。然而在现代编译器中就比较难以实现了,比如在x64环境中连续push后的临时变量地址不容易手动找到,要查找只能依靠关键字va_arg(arg, int);而从这一点我们也可以明白为什么可变参数“…”必须放在最后,这是为了避免读取到参数列表中靠前面的临时参数)

如果感兴趣,可以试着将多个char类型的实参传给GetMax函数,求得多个字符中ACSII码值最大的字符,但是上述代码都不变,这个时候会有一个很神奇的现象,代码依旧能正常运行。有人会问“va_arg(arg, int)”语句不是会根据“int”类型来查找临时变量的地址吗?每一次arg的挪动都应该是int个字节才对。
是的没错,arg变量每一次的确是移动了4个字节(int的大小),但是由于char类型的实参在调用GetMax函数之前,会在栈帧中压入这几个cha类型变量的值,但是这里发生了整型提升(char->int),根本原因就是因为内存中存储的数据基本都是4个字节/8个字节起步的,char数据会隐式提升位int类型来压入栈。
这就导致一个比较违反直觉的事情:将代码改成va_arg(arg, char)这种行为是错误的!!!(当然也不只是char类型是特殊的,short和float类型也会发生类似的事情。)
因此根据类型提取数据的时候,我们更多是依靠int和double类型来进行提取的,不规范使用关键字va_arg的话,会造成不可预估的后果。

  1. va_end(arg);arg使用完毕,就需要进行收尾工作,其本质就是将arg指向NULL(类似动态内存管理中free的使用,避免arg成为野指针)
  2. 注意可变参数必须从头到尾逐个访问,如果使用宏va_arg访问到中途就停止,这是被允许的,但是一开始就像直接访问中间的参数,这是不行的(但是可以间接,或者在函数内创建变量来临时存储)
  3. 而在printf中,第一个参数计数“%”格式符号的个数,用来对应后面的可变参数(感兴趣可以自己实现一个printf函数,这个还是比较有难度的)
  4. 前面我有提到,如果没有规范使用关键字va_arg,在其中指定了错误的类型,其结果是未知的

思考一下,如果超出了可变参数列表的范围会怎么样呢?

2.3.四个宏的详细细节

注意:这几个宏在不同的编译器有可能有不一样的定义

2.3.1.va_list(VS2013的定义(x86环境))

typedef char* va_list;//本质就是一个char类型指针

2.3.2.va_start(VS2013的定义(x86环境))

注释可能有点长,其中需要对除法有更进一步的数学理解能力,还请您耐心看下去……

#define va_start __crt_va_start
#define _crt_va_start(ap, v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

//1.而其中宏“_INTSIZEOF”的定义是“#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )”
//2.即,“_INTSIZEOF”的作用是“4字节对齐(向上取整,整体为4的倍数)”
//3.而“_INTSIZEOF”能“4字节对其”的原理是:
	//3.1.对于“_INTSIZEOF”来说,n可能是char类型、double类型、int类型等,而“_INTSIZEOF”的作用就是把这些不同的类型转化为“4字节对其”的数字,让“(va_list)_ADDRESSOF(v)”能找到可变参数的第一个可变参数的地址,例如“char和short类型通过_INTSIZEOF计算出来的数值都是4,而double则是8”
	//3.2.从数学角度上来说,“_INTSIZEOF”的意思就是计算一个x,满足(x>=N)&&(x%4==0),其中N就是sizeof(n)的大小,例如:“sizeof(char) = 1,则x=4”。在C语言中N的取值只有1、2、4、8等
  	//3.3.因此实际上,再讲大白话一点“_INTSIZEOF”做的事情就是找到一个x,满足x为4的倍数,即“x=4*m”,并且“x>=N”(m!=0,N!=0)
  	//3.4.如果N能被4整除,则m==N/4①
  	//3.5.如果N不能被4整除,则m==N/4+1②
  	//3.6.其实按照上面的逻辑,就可以写出一个普通的“_INTSIZEOF”了。但是若要简介,则可以合并起来写来求得m,常见的写法就是“m=(N+4-1)/4”,这个式子可以整合两个公式
  		//3.6.1.“+4”是为了凑够数,让没能整除4的N变得能整除,并且得到的m==1
  		//3.6.2.“-1”是受到“+4”的影响,如果有“+4”并且N能被4整除,则m会计算多一个,因此“-1”后就不多了(或者理解为“N=能被4整除的部分+不能被4整除的部分r”,而“+3”就会导致“4<=r+3<7”,则“m==N/4+1”)
  	//3.7.这样就顺利得到m的值,就可以推导出4*m的大小啦,进而理解了宏“_INTSIZEOF”的工作原理,有了上述的公式,完全可以写一个和库里等价的“_INTSIZEOF”
  	//3.8.但是这样的方法还是不够简洁,对于表达式“4*m==4*(N+4-1)/4”,假设“w=N+4-1”,则得到“(w/4)*4”,欸?这不就是“(w/(2^2)*(2^2))”么?那么在比特位上不就相当于先右移两位,再左移两位么?这不就相当于给一个二进制序列的末两位二进制位给清0了?那不就可以直接写“w & ~3”了?直接一步到位,所以简洁版诞生了“w&~3”等价于“(N+4-1) & ~3”,这么一写,可不就是库里定义的“_INTSIZEOF”了嘛……这就是二进制的力量!!!将“/*”算术运算转化为“&~”位运算,效率得到了极大的提高
  	//3.9.最后再提一嘴,这其实是一种取整的方法嘛…
//4.因此“_crt_va_start”的整体实现就是:“ap = (char*)&v + 4*m”,其中m由v来决定,既:4*m通过“_INTSIZEOF”确认。这样就让qp参数存储的是指向可变参数的地址

2.3.3.va_arg(VS2013的定义(x86环境))

#define va_arg __crt_va_arg
#define _crt_va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t))- _INTSIZEOF(t)) )
//1.“_crt_va_arg”的作用有两个:一个是找到目标值,另一个是找到将ap自增
//2.这个代码写的非常具有特色和技术:自增"4*m"找到下一个可变参数2的起始地址,但是又减去“4*m”找回原来的可变参数1的起始地址。
//3.这个时候就厉害了,ap存储的是可变参数2的起始地址,但是使用宏“_crt_va_arg”时获取的是可变参数1的起始地址。使用宏“_crt_va_arg”就能达到既能取得可变参数1的起始地址,而下一个使用宏“_crt_va_arg”的时候自动指向下一个可变参数2的起始地址。
//4.获得可变参数1的起始地址后,就可以通过可变参数的类型来强制转化,并且进行解引用得到可变参数1内的数据

2.3.4.va_end(VS2013的定义(x86环境))

#define va_end __crt_va_end
#define _crt_va_end(ap) ( ap = (va_list)0 )
//这个就比较简单,就是将ap指针指向空而已

2.3.5.其他定义

在不同编译器中,这四个关键字还有可能存在不一样的定义,但是其基本逻辑是大差不差的,就比如VS2022的C库里定义的四个关键字和上面的就有很大区别,不过您只需了解即可……

3.命令行参数

main函数也是一个函数,也是可以携带参数的

int main(int argc, char* argv[], char* envp[])
{
	program-statements
}

而可以像下面一样使用,“argc”就是命令行中一串命令的子字符串个数,而每一个命令内包含的子字符串会被分别存储到“argv[]”这个数组中,这样就可以让程序在命令行中表现出不一样的行为

$ vim main.c
//--------
//vim中书写的代码
int main(int argc, char* argv[])
{
  	int i = 0;
	for(i = 0; i < argc; i++)
    {
     	printf("%s\n", argv[i]); 
    }
  	return 0;
}
//--------
$ gcc main.c
$ ./a.out abcdef ghij
./a.out
abcdef
ghij

而“envp[]”这个数组又是什么呢?存储的是环境变量,不存在就以NULL结尾

int main()
{
  	for(int i = 0; envp[i]; i++)
    {
    	printf("envp[i] = %d\n", i, envp[i]);
    }
	return 0;
}

4.递归的简单使用以及理解

4.1.递归的概念

函数在创建的时候,可以调用别的函数,包括自己调用自己,即:C语言支持递归。(比如:main调用自己,但是如果控制不好,容易崩溃)

4.2.递归的深入理解

只要是函数调用就会创建函数栈帧,而递归只是一种特殊的函数调用,而内存的大小是有限的,因此递归一定是有限次递归的,即:大部分情况下,递归必须有递归出口来结束递归。

递归会消耗时间和空间上的消耗

递归最适合在那种“问题和子问题是同一个解决方法”的问题里,例如某些有关二叉树的代码,您可以去了解一下,这是属于数据结构的知识。

#include <stdio.h>
int my_strlen(const char* str)
{
  	if(*str == '\0')
    {
      	return 0;
    }
  	return 1 + my_strlen(str + 1);
}
int main()
{
	int len = my_strlen("abcdef");
  	printf("%d\n", len)
  	return 0;
}

递归还有可能出现重复运算的问题,比如最经典的斐波那契的求解。其树形结构的递归就会出现大量冗余重复的计算

4.3.解决递归消耗过大问题

这里只针对斐波那契数列问题,介绍两种常见的方法供您使用。

4.3.1.方法一

int main()
{
    int n = 10;

    int* f = (int*)malloc(sizeof(int) * (n + 2));
    if (!f) exit(-1);

  	//实际上这就是一个简单的动态规划例子
  	//1.条件初始化
    f[1] = 1;
    f[2] = 1;
    int i = 3;
    while (i <= n)
    {
      	//2.递推过程
        f[i] = f[i - 1] + f[i - 2];
        i++;
    }
    printf("%d\n", f[n]);
    free(f);
    return 0;
}

4.3.2.方法二

int main()
{
	int n = 10;
	int first = 1;
	int second = 1;
	int third = 1;

	while (n >= 3)
	{
		third = second + first;
		first = second;
		second = third;
		n--;
	}
	printf("%d\n", third);
	return 0;
}

5.总结

本次我和您一起复习了C语言的栈帧空间,并且引出可变参数列表的使用和原理。还和您补充了一些有关“命令行参数”和“递归”的相关应用。到此我的C语言系列基础文章算是告一段落……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/650121.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Metasploit渗透测试框架

文章目录 Metasploit渗透测试框架基于TCP协议收集主机信息开放端口扫描db_nmap查询网段内在线存活的主机半连接的方式进行半连接扫描使用auxiliary/sniffer下的psunffle模块进行密码嗅探 基于SNMP协议收集主机信息基于SSH协议收集主机信息实战-制作Linux恶意病毒获取公司服务权…

JAVA开发(通过Apollo注入配置信息的几种方式)

前言 在springCloud中有一个重要的组件就是配置中心&#xff0c;config:server&#xff0c;用于配置springboot中需要注入的各种配置项。但是现在发现越来越多的企业使用Apollo进行集成。博主在开发中也是使用Apollo进行配置。本文总结Apollo的的使用&#xff0c;集成到spring…

大模型入门(四)—— 基于peft 微调 LLaMa模型

llama-7b模型大小大约27G&#xff0c;本文在单张/两张 16G V100上基于hugging face的peft库实现了llama-7b的微调。 1、模型和数据准备 使用的大模型&#xff1a;https://huggingface.co/decapoda-research/llama-7b-hf&#xff0c;已经是float16的模型。 微调数据集&#x…

ASEMI代理光宝光耦LTV-0314的应用与优势

编辑-Z 在电子设备的设计和制造过程中&#xff0c;光耦合器是一种至关重要的组件。它们在电路中起到隔离作用&#xff0c;保护电子设备免受电压冲击和电流过载的影响。今天&#xff0c;我们将深入探讨一种特殊的光耦合器——LTV-0314&#xff0c;它的特性、应用以及优势。 一、…

细说如何封装一个日历组件(多视图、可选择、国际化)

前言 最近好奇日历组件是怎么实现的。于是阅读了下react-calendar的源码&#xff0c;并实现了简化版的日历组件。本文把实现日历的设计思路分享给大家。只要理清了主要逻辑&#xff0c;就不难实现了。 技术栈&#xff1a;react、typescript 预览 在线预览demo&#xff1a;c…

亚马逊云科技中国峰会:探索强化学习的未来与Amazon DeepRacer赛车比赛

目录 一、如何构建自己的第一个强化学习模型第一步: 创建 AWS DeepRacer 资源第二步: 定义你的赛道第三步: 训练你的模型第四步: 优化你的模型第五步: 在仿真器中测试你的模型第六步: 在真实赛道上测试你的模型 二、Amazon DeepRacer 中国峰会总决赛三、Amazon DeepRacer 自动驾…

Redis基础+使用+八股文!万字详解一篇就够!

一、目标 学习Redis基础必须掌握的内容&#xff1a; 了解 Redis 以及缓存的作用&#xff1b;掌握 Redis 5 大基本数据类型的使用&#xff1b;掌握常见Redis 面试题&#xff1b;掌握 Redis 的持久化功能&#xff1b;了解 Redis 集群功能。 二、什么是缓存&#xff1f; 缓存定义…

Netty中PileLine类介绍

一、Netty基本介绍 Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具&#xff0c;用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty 在保证易于开发的同时还保证了其应用的性能&#xff0c;稳定性和伸缩性。 Netty 是一…

VTK Filter 总结

源对象 成像滤波器 可视化滤波器 可视化滤波器&#xff08;输入类型vtkDataSet&#xff09;。 可视化滤波器&#xff08;输入类型vtkPointSet) 可视化滤波器&#xff08;输入类型vtkPolyData) 可视化滤波器&#xff08;(输入类型vtkStructuredGrid)。 可视化滤波器&#xff08;…

浅析视频监控技术及AI发展趋势下的智能化视频技术应用

视频监控技术是指通过摄像机对指定区域进行实时视频直播、录制、传输、存储、管理和分析的技术系统。它可以用于监控各种场所&#xff0c;如校园、工厂、工地、工作场所、公共区域、交通工具等。视频监控技术主要涉及到以下几个部分&#xff1a; 1、摄像机 摄像机是视频监控技…

三年软件测试外包的我也没能转正

外包的群体庞大&#xff0c;很多企业为了节约高昂的人力成本&#xff0c;会把一些非核心业务承包给外包公司&#xff0c;这些工作往往是阶段性、辅助性&#xff0c;没有什么技术含量&#xff0c;而且由于外包人员不是与大厂签订劳动合同&#xff0c;因此&#xff0c;他们更像是…

图像点运算之灰度变换之非线性变换

目录 note code test note 图像点运算之灰度变换之非线性变换 例如&#xff1a;y 10 * x ^ 0.5 code void noline_convert_fun(uchar& in, uchar& out) {out 10 * (uchar)pow((float)in, 0.5); } void img_nonline_convert(Mat& src, Mat& res) {if (s…

html好看的登录界面2(十四种风格登录源码)

文章目录 1.登录风格效果说明1.1 凹显风登录界面1.2 大气简洁风登录界面1.3 弹出背景风登录界面1.4 动态左右切换风登陆界面1.5 简洁背景切换登录界面1.6 可关闭登录界面1.7 蒙蒙山雨风登录界面1.8 苹果弹框风登录界面1.9 上中下青春风登录界面1.10 夏日风登录界面1.11 星光熠熠…

【从零开始玩量化20】BigQuant平台策略代码本地化(与Github同步)

引言 最近发现了个不错的量化平台&#xff0c;BigQuant BigQuant的客服找到我&#xff0c;推荐他们平台给我使用&#xff0c;宣传的是人工智能&#xff0c;里面可以使用类似ChatGPT的聊天机器人&#xff0c;和可视化拖拉拽功能实现策略。 不过&#xff0c;这些都是锦上添花的…

单体 V/s 分布式架构

这是软件架构模式博客系列第 2 章,我们将讨论单体 V/s 分布式架构。 在软件领域,存在多种架构风格可供选择,我们需要关注不同架构风格带来的风险。选择符合业务需求的架构风格是一个长期迭代的过程。 架构风格可以分为两大主要类型:单体架构(将所有代码部署在一个单元中…

Rancher:外部服务连接K8S-MongoDB服务

Rancher&#xff1a;外部服务请求K8S-MongoDB服务 一、前置条件二、「Layer 4 」与「Layer 7」Load Balancing的区别三、部署容器化MongoDB四、Load Banlancer of Service五、mongoDB验证连接六、总结 #参考链接 [1] How access MongoDB in Kubernetes from outside the clust…

树莓派4B多串口配置

0. 实验准备以及原理 0.1 实验准备 安装树莓派官方系统的树莓派 4B&#xff0c;有 python 环境&#xff0c;安装了 serial 库 杜邦线若干 屏幕或者可以使用 VNC 进入到树莓派的图形界面 0.2 原理 树莓派 4B 有 UART0&#xff08;PL011&#xff09;、UART1&#xff08;mini …

腾讯安全周斌:用模型对抗,构建新一代业务风控免疫力

6月13日&#xff0c;腾讯安全联合IDC发布“数字安全免疫力”模型框架&#xff0c;主张将守护企业数据和数字业务两大资产作为企业安全建设的核心目标。腾讯安全副总裁周斌出席研讨论坛并发表主题演讲&#xff0c;他表示&#xff0c;在新技术的趋势影响下&#xff0c;黑灰产的攻…

TS系列之any与unknown详解,示例

文章目录 前言一、一个示例二、示例目的1、功能描述2、主要区别3、代码实现 总结 前言 本片文章主要是在写ts时遇到不知道类型&#xff0c;很容易就想到用any可以解决一切&#xff0c;但这样写并不好。所以今天就总结学习一下&#xff0c;比较好的处理任意类型的unknown。 一、…

patroni+etcd+antdb高可用

patronietcdantdb高可用架构图 Patroni组件功能 自动创建并管理主备流复制集群&#xff0c;并且通过api接口往dcs(Distributed Configuration Store&#xff0c;通常指etcd、zookeeper、consul等基于Raft协议的键值存储)读取以及更新键值来维护集群的状态。键值包括集群状态、…