IPC之十二:使用libdbus在D-Bus上异步发送/接收信号的实例
IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,本 IPC 系列文章的前十篇介绍了几乎所有的常用的 IPC 方法,每种方法都给出了具体实例,前面的文章里介绍了 D-Bus 的基本概念以及调用远程方法的实例,本文介绍 D-Bus 的异步处理机制,以及信号处理的基本方法,本文给出了异步处理 D-Bus 的实例,附有完整的源代码;本文实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文不适合 Linux 编程的初学者阅读。
1 D-Bus 的信号(Signal)
-
在阅读本文之前,建议阅读关于 D-Bus 的另一篇文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》
-
在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中,介绍了服务端如何在 D-Bus 上提供方法调用服务以及客户端如何向服务端请求一个方法调用服务;
-
通过 D-Bus 向服务端请求方法调用服务,仅仅是 D-Bus 一半的功能,D-Bus 还支持异步的广播通信方法,这种机制称为信号(Signal),当服务端需要向大量接收者发送通知时,该机制非常有用;
-
举例来说,如果系统正在关闭、网络连接中断以及类似的系统范围内的情况,相关系统服务进程应该广播一个通知,使对这些服务有需求的进程能够及时做出反应,这样一种方式,使得接收信号的进程无需轮询服务状态;
-
D-Bus 的信号(Signal)与调用方法(Method Call)有许多类似的地方,这里简要回顾一下在上一篇文章中讨论的调用方法的概念:
- 服务程序连接 D-Bus(dbus-daemon),获得一个连接,D-Bus 随机给这个连接分配一个唯一名称;
- 为该连接绑定一个固定的名称(Bus Name),以方便客户端访问这个连接,总线名称通常以反向域名的形式命名;
- 在该连接下可以建立一个或多个对象(Object),对象名称以路径(类似文件系统路径)表示;
- 在每个对象上可以建立一个或者多个接口(Interface),接口的名称也是使用反向域名的命名方式;
- 每个接口下可以有一个或者多个方法(Method);
- 客户端需要请求服务端的某个方法时,需要知道总线名称、对象路径、接口名称以及方法名称,并将调用参数传送给服务端;
-
信号(Signal)也是建立在一个接口下,一个接口下不仅可以有一个或者多个方法,还可以有一个或者多个信号,
- 1 – 4 同上;
- 每个接口下可以有一个或者多个信号(Signal),信号的命名与调用方法一样,没有很多规则,比如:sig_demo
- 客户端想要收到某个信号时,需要向总线注册,告知总线感兴趣的信号(包括:对象路径、接口名称和信号名称),只能收到向总线注册过的信号;
-
所以其实一个接口下可以有若干个方法和信号,除此之外,接口下还可以有若干个属性(Properties),方法、信号、属性组合在一起构成一个接口;
-
本文仅讨论接口,有关属性的事情,以后的文章中再讨论;
-
发送信号通常是服务端的事情,信号通常是以广播的方式发出,而只有订阅了这个消息的客户端才能收到消息,实际上,信号也是可以点对点发送的(仅发给指定客户端),这个以后讨论,先讨论通常的广播信号;
-
服务端发送信号的步骤
- 使用
dbus_bus_get()
连接到 D-Bus,获得一个连接 DBusConnection; - 使用
dbus_message_new_signal()
为构建信号初始化一个 DBusMessge; - 使用
dbus_message_append_args()
将信号参数添加到信号的 DBusMessage 中; - 使用
dbus_connection_send()
将信号放入发送队列; - 使用
dbus_connection_flush()
将发送队列的消息全部发送出去; - 使用
dbus_message_unref()
释放信号的 DBusMessage;
- 使用
-
整个过程与在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中描述的客户端向服务端请求一个服务的过程高度相似,但要简单一些:
- 请求服务时是客户端向服务端发出请求,而发送信号时是服务端发送一个广播消息;
- 请求服务时,服务端通常要在连接上绑定一个公用的名称(总线名称),客户端在请求服务时必须要指定这个总线名称,否则 D-Bus 不知道你要向哪个应用程序请求服务,发送信号时,服务端不一定需要在连接上绑定名称,因为通常客户端只需要接收信号,不需要向服务端发送任何消息;
- 请求服务时,客户端在发出请求后通常需要服务端的回复,发送信号时没有任何回复消息;
-
客户端要接收到信号,需要订阅指定的信号,D-Bus 只会把你订阅的信号推送过来;
-
使用
dbus_bus_add_match()
订阅信号:void dbus_bus_add_match(DBusConnection *connection,const char *rule,DBusError *error);
- connection 是使用
dbus_bus_get()
获得的连接; - error 已经在很多函数调用中出现过,不多说了;
- 这个 rule 参数是这个函数的灵魂,这是一个字符串,这个字符串定义了一个规则,告诉 D-Bus 我要订阅符合这个规则的信号;
- 这个规则使用
key/value
的形式描述,可以有多个key/value
对用于描述多个条件,每个key/value
对用 “,” 分隔; - 举个 rule 的例子:
"type='signal',sender='cn.whowin.dbus', path='/cn/whowin/dbus', interface='cn.whowin.dbus_iface',member='notify'"
- 在这个例子中,
type='signal'
表示消息类型为信号,sender 是发送方的总线名称,path 是发送方的对象路径,interface 是发送方的接口名称,member 是信号名称,D-Bus 会把符合这些条件的信号推送到订阅的进程中; - 在描述规则时不用把条件写的这么全,比如:
"type='signal',sender='cn.whowin.dbus',path='/cn/whowin/dbus'"
,则从cn.whowin.dbus
的对象/cn/whowin/dbus
发出的消息都可以收到; - 在这个调用中,如果 error 参数为 NULL,则调用
dbus_bus_add_match()
后会立即返回,不会产生阻塞,但是订阅不会生效,需要执行dbus_connection_flush(conn)
后订阅才会生效,而且如果发生了错误程序也是无法知晓的,所以,不建议这样做; - 目前 rule 可以使用的
key/value
对中的 key 可以为:type, sender, interface, member, path, destination; - destination 指的是目的地址,比如:
:275.6
,在广播信号中通常用不上; - 下面这段代码订阅了一个信号:
DBusError dbus_error; DBusConnection *conn;dbus_error_init(&dbus_error); conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);dbus_bus_add_match(conn, "type='signal',path='/cn/whowin/dbus/signal',interface='cn.whowin.dbus_iface'", &dbus_error); ......
- connection 是使用
-
如果有必要,你可以使用
dbus_bus_add_match()
订阅多个信号。
2 libdbus 的异步处理机制
-
客户端并不知道什么时候会有信号发出来,所以为了能及时收到信号必须不断轮询,像这样:
DBusConnection *conn; DBusMessage *message; ...... while (dbus_connection_read_write_dispatch(conn, -1)) {// loopmessage = dbus_connection_pop_message(conn);if (message == NULL) {usleep(10000);continue;}if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_SIGNAL) {usleep(10000);continue;}...... }
-
函数
dbus_connection_read_write_dispatch()
在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中做过介绍; -
显然,这样的编程模式并不高效,尤其是当程序不仅仅是要接收信号,还有其他工作要做时,这种程序架构就显得更加不可接受;
-
实际上,libdbus 还提供了另外一种异步接收信息的方式,像下面这样的代码:
DBusHandlerResult signal_filter(DBusConnection *connection, DBusMessage *message, void *usr_data) {DBusError dbus_error;dbus_error_init(&dbus_error);if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_SIGNAL) {printf("Client: This is not a signal.\n");return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;}......return DBUS_HANDLER_RESULT_HANDLED; }int main() {DBusError dbus_error;DBusConnection *conn;dbus_error_init(&dbus_error);conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);dbus_connection_add_filter(conn, signal_filter, NULL, NULL);dbus_bus_add_match(conn, "type='signal',path='/cn/whowin/dbus/signal',interface='cn.whowin.dbus_iface'", &dbus_error);while (dbus_connection_read_write_dispatch(conn, -1)) {/* loop */......}......return; }
- 在主程序中,首先使用
dbus_connection_add_filter()
添加了一个过滤器(Filter),然后再用dbus_bus_add_match()
订阅感兴趣的信号; - 在下面的 while 循环中,并不需要去接收消息,当订阅的信号到来时,会直接调用过滤器,在过滤器里处理收到的信号即可;
- 所以,上面这段程序实际上是在函数
signal_filter()
中处理信号;
- 在主程序中,首先使用
-
函数
dbus_connection_add_filter()
原型dbus_bool_t dbus_connection_add_filter(DBusConnection *connection,DBusHandleMessageFunction function,void *user_data,DBusFreeFunction free_data_function )
- connection 为通过
dbus_bus_get()
获得的连接; - function 为过滤器调用的函数;
- user_data 为传递给 function 的参数;
- free_data_function 为释放 user_data 需要调用的函数;
- connection 为通过
-
DBusHandleMessageFunction
的定义typedef DBusHandlerResult(* DBusHandleMessageFunction)(DBusConnection *connection,DBusMessage *message,void *user_data);
- 所以在
dbus_connection_add_filter()
中,function 是一个函数指针,该函数将接收三个参数,第一个是从dbus_bus_get()
获得的连接,第二个参数是一个消息结构DBusMessage
,表示收到的消息,第三个参数是用户数据,在使用dbus_connection_add_filter()
添加过滤器时设置; - 当一个过滤器函数被调用时,收到的消息已经在 message 中了;
- 这个过滤器函数的返回值是 DBusHandlerResult,这是一个枚举类型,其值有三个:
DBUS_HANDLER_RESULT_HANDLED
、DBUS_HANDLER_RESULT_NOT_YET_HANDLED
和DBUS_HANDLER_RESULT_NEED_MEMORY
; DBUS_HANDLER_RESULT_HANDLED
表示该过滤器函数已经获得了一个有效消息并进行了处理,该消息无需再交给其他过滤器处理;DBUS_HANDLER_RESULT_NOT_YET_HANDLED
表示该过滤器没有处理该消息,如果有其他过滤器,可以把该消息交给其他过滤器处理;DBUS_HANDLER_RESULT_NEED_MEMORY
通常用不上;
- 所以在
-
调用过滤器函数是由 libdbus 实现的,应该是在调用
dbus_connection_read_write_dispatch()
时,当有可读消息时,调用过滤器函数; -
调用过滤器函数后的返回值并不会返回到应用程序中,但是对其它过滤器可能会产生影响,当系统内有多个过滤器时,当前过滤器返回
DBUS_HANDLER_RESULT_HANDLED
意味着已经处理好了这个消息,不必再使用其它过滤器处理该消息;DBUS_HANDLER_RESULT_NOT_YET_HANDLED
意味着这个消息没有在当前过滤器中被处理,如果有其它过滤器,应该尝试使用其它过滤器处理;
-
所以,过滤器函数的返回一定要正确,否则会有消息丢失;
-
如果有必要,你可以添加多个过滤器,去处理不同的消息;
-
过滤器的概念,其实也不仅仅可以用在接收信号上,也可以用在调用方法上;
-
尽管我们向 D-Bus 订阅了我们感兴趣的信号,但其实有时也会一些不符合订阅条件的信号到来,所以,在程序中还是要做一些判断,以确保收到的是我们期望的信号,如果不是,返回
DBUS_HANDLER_RESULT_NOT_YET_HANDLED
,让其它过滤器去处理。
3 使用 libdbus 异步接收信号的实例
-
源程序:dbus-signals.c(点击文件名下载源程序,建议使用UTF-8字符集)演示了使用 libdbus 对信号进行发送和接收,以及如何异步接收信号;
-
该程序是一个多进程程序,建立了一个服务端进程和三个客户端进程;
-
服务端进程在启动后发送出一个内容为 “start” 的信号,暂停 5 秒后,再发出一个内容为 “quit” 的信号,然后退出进程;
-
服务端在发送信号时,其对象路径、接口名称和信号名称均相同;
-
客户端进程订阅了服务端的信号,并添加了两个过滤器,一个用于处理内容为 “start” 的信号,另一个用于处理内容为 “quit” 的信号,这里仅是为了演示多个过滤器的工作方式;
-
客户端检查收到的信号,如果其内容为 “quit”,则退出进程;
-
编译:
gcc -Wall -g dbus-signals.c -o dbus-signals `pkg-config --libs --cflags dbus-1`
-
有关
pkg-config --libs --cflags dbus-1
可以参阅文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中的简要说明; -
运行:
./dbus-signals
-
运行截图:
-
程序运行后,客户端进程的两个过滤器都显示了 “Wrong object path” 的信息,这条信息是 D-Bus 为客户端连接分配了名称后发送过来的通知信号,虽然我们没有订阅,但 D-Bus 会强行推送过来;
-
这条通知信号在经过过滤器时,过滤器返回了 “DBUS_HANDLER_RESULT_NOT_YET_HANDLED”,因为这个返回值导致这个消息在经过第一个过滤器后还会再进入第二个过滤器进行处理,如果过滤器在遇到对象路径不对时返回 “DBUS_HANDLER_RESULT_HANDLED”,则这条消息不会再去第二个过滤器,读者可以尝试修改程序看看是不是这样;
-
如果你多次运行这个程序你会发现,信号总是首先到达
signal_quit()
过滤器,然后才到达signal_start()
过滤器,这是因为我们先添加的signal_quit()
过滤器,如果你改动一下程序,先添加signal_start()
过滤器,再添加signal_quit()
过滤器,你会看到信号到达的顺序也会发生变化。
欢迎订阅 『进程间通信专栏』
相关文章:

IPC之十二:使用libdbus在D-Bus上异步发送/接收信号的实例
IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,本 IPC 系列文章的前十篇介绍了几乎所有的常用的 IPC 方法,每种方法都给出了具体实例,前面的文章里介绍了 D-Bus 的基本概念以及调用远程方法的实例,本文介绍 D-Bus…...

ES6之生成器(Generator)
✨ 专栏介绍 在现代Web开发中,JavaScript已经成为了不可或缺的一部分。它不仅可以为网页增加交互性和动态性,还可以在后端开发中使用Node.js构建高效的服务器端应用程序。作为一种灵活且易学的脚本语言,JavaScript具有广泛的应用场景&#x…...

Matlab技巧[绘画逻辑分析仪产生的数据]
绘画逻辑分析仪产生的数据 逻分上抓到了ADC数字信号,一共是10Bit,12MHZ的波形: 这里用并口协议已经解析出数据: 导出csv表格数据(这个数据为补码,所以要做数据转换): 现在要把这个数据绘制成波形,用Python和表格直接绘制速度太慢了,转了一圈发现MATLAB很好用,操作方法如下:…...
Go面试题学习
1.并发安全性 Go语言中的并发安全性是什么?如何确保并发安全性? 并发安全性是指在并发编程中,多个goroutine对共享资源的访问不会导致数据竞争和不确定的结果。 使用互斥锁(Mutex):通过使用互斥锁来保护…...
SQL效率-查询条件需避免使用函数处理索引字段
一个sql效率的问题 问题 假设created_at 是date类型、是索引,那么以下2种方式有没效率差异: WHERE TO_CHAR(created_at, ‘YYYY-MM-DD’) ‘2020-02-01’WHERE created_at TO_DATE(‘2020-02-01’ , ‘YYYY-MM-DD’) DBA回复 有的,第一…...

【Spring 篇】Spring:轻松驾驭 Java 世界的利器
在 Java 开发领域,Spring 框架无疑是一颗璀璨的明星,它不仅提供了全面的企业级特性,还为开发者提供了简便而强大的开发方式。本文将深入探讨 Spring 框架的简介、配置和快速入门,带你轻松驾驭 Java 世界的利器。 Spring 简介 Sp…...

八个LOGO素材网站推荐分享
即时设计资源广场 在UI界面设计中,为了找到合适的图标icon,你有没有尝试过翻遍整个网络,找到自己想要的,却无法下载或收费使用?最后,只收集图标icon材料需要半天时间。专业设计师使用的图标icon设计材料“…...
React格式化规范
React并没有特定的格式要求,它允许开发者根据自己的喜好和项目需求来选择代码的格式化风格。然而,在React社区中有一些常见的约定和最佳实践,以下是一些常用的格式化规范和建议: 缩进:使用2个或4个空格来进行缩进&…...
如何利用Conda管理多种虚拟环境与Jupyter Notebook内核切换
写在开头 在数据科学与机器学习领域,项目之间可能存在不同的依赖关系和版本要求。为了有效管理这些差异,使用虚拟环境成为一种标准实践。本文将介绍如何利用Conda这一强大的环境管理工具,结合Jupyter Notebook,使得在不同项目之间灵活切换变得轻而易举。 2. Conda简介 2…...
博客摘录「 什么是QPS、TPS、吞吐量?- 高并发名词概念」2024年1月5日
1.什么是高并发? 高并发(High Concurrency)。通常是指系统在短时间内的大量操作。 高并发相关的常见指标有:QPS、TPS、吞吐量、并发数等。 2.QPS(Query Per Second) QPS每秒查询率,是指系统…...

PTA——逆序的三位数
程序每次读入一个正3位数,然后输出按位逆序的数字。注意:当输入的数字含有结尾的0时,输出不应带有前导的0。比如输入700,输出应该是7。 输入格式: 每个测试是一个3位的正整数。 输出格式: 输出按位逆序…...

ChatGPT怎么帮我上班的
1.解放生产力 1)标准格式,完美输出。GPT对于公文等具有一定标准格式的文件,可以进行完美仿写,随随便便以假乱真那都是小菜一碟,这对于经常要开展规范成文的人来说,简直就是个福音,只要前期调教…...

WPF 漂亮长方体、正文体简单实现方法 Path实现长方体 正方体方案 WPF快速实现长方体、正方体的方法源代码
这段XAML代码在WPF中实现了一个类似长方体视觉效果的图形 声明式绘制:通过Path、PathGeometry和PathFigure等元素组合,能够以声明方式精确描述长方体每个面的位置和形状,无需编写复杂的绘图逻辑,清晰直观。 层次结构与ZIndex控制…...

Nginx(十三) 配置文件详解 - 反向代理(超详细)
本篇文章主要讲ngx_http_proxy_module和ngx_stream_proxy_module模块下各指令的使用方法。 1. 代理请求 proxy_pass 1.1 proxy_pass 代理请求 Syntax: proxy_pass URL; Default: — Context: location, if in location, limit_except 设置代理服务器的协议和地址以…...

谷歌浏览器启用实时字幕功能
在 Chrome 中使用“实时字幕”功能 - Google Chrome帮助 在 Chrome 中使用“实时字幕”功能 从计算机上的 Chrome 浏览器中,您可以使用“实时字幕”功能自动为视频、播客、游戏、直播、视频通话或其他音频媒体生成字幕。音频和字幕均在本地处理,并会保…...
php接口优化 使用curl_multi_init批量请求
PHP使用CURL同时抓取多个URL地址 抓取多个URL地址是Web开发中常见的需求,使用PHP的curl库可以简化这个过程。本文将详细介绍如何使用PHP的curl库同时请求多个URL地址,并提供具体的代码案例和注释。 curl库介绍 curl是一个常用的开源网络传输工具&…...
联邦拜占庭共识算法的工作流程
1 前言 联邦拜占庭共识算法(Federated Byzantine Agreement,简称FBA)是一种解决分布式系统中拜占庭问题的共识算法,是拜占庭容错共识算法里的其中一种,主要应用于区块链技术中。这种算法允许系统内部的各种节点自由进…...
国家开放大学形成性考核 统一考试 学习资料参考
试卷代号:11119 机械CAD/CAM 参考试题 一、单项选择题(从所给的四个选项中,找出你认为是正确的答案,将其编号填入括号内。每小题3分,共45分) 1.下述CAD/CAM过程的概念中,属于CAM范畴的是&am…...
2401C++,实现文件服务器和聊天室
文件服务器 使用yalantinglibs,几行代码开发静态文件服务器 最近的workshop上的一个任务,就是实现一个文件服务器,只要设置下载目录之后,就可下载目录里面的文件. 看看用yalantinglibs怎么实现一个静态文件服务器的吧. coro_http::coro_http_server server(1, 9001); server.…...
【ESP-NOW 入门(ESP32 with Arduino IDE)】
ESP-NOW 入门(ESP32 with Arduino IDE) 1. 前言2. Arduino集成开发环境3. ESP-NOW 简介3.1 ESP-NOW 支持以下功能:3.2 ESP-NOW 技术还存在以下局限性:4. ESP-NOW 单向通信4.1 一个 ESP32 开发板向另一个 ESP32 开发板发送数据4.2 一个“主”ESP32 向多个 ESP32“slave”发送…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...