【Linux】进程概念、fork() 函数 (干货满满)
文章目录
- 📕 前言
- 📕 进程概念
- 📕 Linux下查看进程的两种方法
- 方法一
- 方法二
- 📕 pid() 、ppid() 函数
- 📕 fork() 函数、父子进程
- 初识
- 再理解
- 📕 fork做了什么
- 📕 如何理解 fork 有两个返回值
📕 前言
我们已经知道,操作系统进行“管理”的本质是——先描述、再组织。描述是一个面向对象的过程,组织是使用数据结构的过程。而进程也是被操作系统管理的,操作系统也要对其先描述,后组织。
描述无疑就是将进程的各种信息存储在一个结构体里面,描述好之后,将描述进程的结构体用链表(也可能是其他)维护,所以查找、删除进程等等的操作,就变成了对链表的增删查改。所以,管理者实际上不需要和被管理者进行直接交流,只需要对被管理对象的数据做管理!!
操作系统对进程的管理也和上面一样,要“先描述,后组织”。
📕 进程概念
当我们打开QQ、微信 ,是为了和朋友聊天,刷朋友圈等;打开vscode,是为了进行编程或者其他操作……任何启动并运行程序的行为,操作系统都会将程序转换为进程,从而完成特定的任务!!!
如下,磁盘中有某个可执行文件(由于是在磁盘中,所以该文件关机再开机还是保存着的)。可执行文件本质上还是一个文件,也有文件的内容和属性。当我们在Linux 环境下使用 ./a.out 的时候(假设该文件名为 a.out),该文件对应的代码和数据(文件的内容)就会被加载进内存,所以CPU才能访问该文件的代码和数据。
但是,这样就算创建一个进程了吗?
答案是否定的。以学校为例子,如何才能成为一个学校的学生,只要进入学校,在学校自由行走就算是学生了吗?当然不是啦,不然学校的保安、保洁阿姨、参观人员岂不都是学生。是不是学生,最重要的就是学校是否对你进行“管理”——即学校是否拥有你的学籍档案,你的学生信息是否在学校的教务系统里面。
所以,创建一个进程,不仅要将代码和数据加载到内存里面,还要将进程的一些信息存储在结构体里面,并用某种数据结构进行维护,让操作系统进行管理。
如下图,我们绝大多数情况下,都不会只运行一个程序,所以也不会只创建一个进程。用来描述进程的结构体,在Linux环境下被称作 task_struct (早期的进程被称作任务),当然,也可以称它为pcb。每创建一个进程,就会创建一个对应的pcb存储进程独有的一些信息。然后将这些pcb以链表的形式进行维护。
比如现在有一个进程A,要对里面的数据进行修改,那么就会遍历链表,找到进程A对应的pcb,然后通过pcb对其数据进行修改。又或者,现在要运行优先级最高的进程,那么操作系统就会遍历pcb链表,找到优先级最高的进程(pcb里面包含进程的所有属性,当然也包含优先级),确定该进程要被调度,通过pcb找到该进程的代码和数据,将它的入口代码放到CPU里面,这样该进程就被运行了。
所以,对进程的管理,并不是直接对代码和数据进行管理,而是将代码和数据提取出一些特定信息,存入task_struct 结构体里面,成为一个节点,再将节点存入链表,从而管理链表。这样,对进程的管理实际上就是对pcb形成的结构体的增删查改,这样就完成了对进程管理的建模的过程。
综上所述,进程 = 当前进程的代码和数据 + pcb/task_struct
当然,上述都是对软件进行管理的解释,但是,操作系统对硬件进行管理也是类似的。
pcb提取了进程的属性,这个进程的属性和文件的属性有一点点关系,并不大。文件的属性存储的是:这个文件是谁创建的,什么时候创建的,有什么权限等等。但是为进程创建的pcb结构体,是一种内核数据结构,是由操作系统动态创建和维护的,和磁盘文件的属性没有关系。
所以,pcb里面的属性相当于是另起炉灶,里面的属性基本上和文件属性无关。
task_struct 存储的一些信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
📕 Linux下查看进程的两种方法
方法一
ps axj | head -1 && ps ajx | grep myprocess | grep -v grep
上述指令是查看自己创建的进程。
如下是系统中所有的进程,ps axj | head -1 是将第一行拿出来,即 PPID 、PID 等等。 PID (process id)表示进程的唯一标识,PPID表示该进程的父进程的唯一标识……这样方便查看各列信息代表上面意思。
&& 是逻辑与,相当于前面的指令执行成功,然后执行后面的。
ps ajx | grep myprocess 代表的是,选出自己创建的进程(可执行文件名要为myprocess,也可以根据自己的文件名修改)。
grep -v grep ,由于进程信息是 ps 打印出来,然后通过管道传给 grep ,由 grep 进行过滤,但是 grep 本身也是一个进程,所以 grep -v grep 是将 grep 的进程信息去掉。
如下图,是上述指令执行结果。我们可以看到,同一个可执行程序被执行了两次,但是其PID是不一样的,说明创建了两个进程。也就是说,如果一个可执行程序可以多次被加载到内存,那么该可执行程序内部就可以有多个进程。
方法二
ls /proc
根目录下的 proc 目录,这个目录和普通的目录并不一样。proc 是一个内存级的文件系统,只有当操作系统启动的时候,该目录才会存在,在磁盘上并不会存在这个目录。
ls -l /proc ,使用该指令查看到的信息如下,proc 目录下面也是一个个目录。目录名字就是特定进程的PID。但是用这个指令查看进程并不直观。所以一般用 ls /proc 。
如下,使用 ls /proc 查看进程就非常直观了。
当然,由于 /proc 目录以及它目录里的内容,是操作系统动态创建的,所以,当一个进程终止之后,它在 /proc 里面对应的文件夹也会被删除。
如下图,当PID为 28201 的进程还在运行的时候,是可以进入 /proc/28201 的,该目录下面存储的是 28201 这个进程的相关信息。但是使用 ctrl+c 终止进程之后,就看不到该目录下任何信息,甚至无法返回上级目录。这是因为这个目录已经被删除,/proc 下没有 28201 为目录名的文件了。
📕 pid() 、ppid() 函数
pid() 函数是获取当前进程的 pid,ppid() 函数是获取当前进程的父进程的 pid 。
如下,linux环境下执行下列的C语言代码,其运行结果如下图。
1 #include<stdio.h> 2 #include<unistd.h>3 int main()4 {5 while(1)6 {7 printf("我是一个进程,我的pid是:%d,我的ppid是:%d \n",getpid(),getppid());8 sleep(1);9 }10 return 0;11 }
./myprocess 这个进程的父进程 28071 ,实际上代表的是命令行解释器,也叫 bash,它本质上也是一个进程。我们在 linux 下敲的命令 ,比如 ls ,ll,grep 等等,都是由 bash 创建子进程去实现的,当然这些命令大部分也是由 C 语言实现,我们自己也可以写类似的命令,只是需要配置PATH(后面的文章会解释)。
这样设置是有很多好处的,比如,上述的这些指令,如果不是由 bash 创建子进程去完成,而是由 bash 本身完成,那么如果运行失败,bash 也就挂了。但是如果是子进程挂了,那么也不影响 bash 的运行。
📕 fork() 函数、父子进程
初识
如果不想用上述的两种方法创建子进程(./myporocess 、 指令),可以使用 fork() 在代码层面上直接创建新的子进程。可以使用 man 手册查看具体细节。
1 #include<stdio.h>2 #include<unistd.h>3 int main()4 {5 printf("AAAAAAAAAAA:\n");6 fork();7 printf("BBBBBBBBBBB:pid:%d,ppid:%d \n",getpid(),getppid());8 sleep(1); 9 return 0;10 }
运行上述代码,查看结果如下,说明 fork(); 那一行之后,确实是有两个进程。并且还是父子进程,进程 29333 的父进程 是 29332 。
但是,打印 A 是由哪一个进程执行的呢? 修改代码并编译运行之后,如下图,可以看出打印 A 是由父进程执行的。
我们可以查看一下上面进程 29758 的父进程 28071 ,它实际上就是 bash 。调用链就是:当运行 ./myprocess ,该程序就成了 bash 的子进程, 然后 ./myprocess 这个进程又创建一个子进程。其实就类似于一个树状结构。当然,进程之间一般只谈父子,不谈爷孙。
此外,父子进程谁先运行,这是由操作系统的调度选择算法来决定的,我们并不能决定。
再理解
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>int main()
{pid_t ret=fork();if(ret == 0){while(1){printf("我是子进程,我的pid是:%d,我的父进程是:%d \n",getpid(),getppid());sleep(1);}}else if(ret > 0) {while(1){printf("我是父进程,我的pid是:%d,我的父进程是:%d \n",getpid(),getppid());sleep(1);}}return 0;
}
执行上述代码,结果如下。但是这里惊奇地发现,if 和 else if 居然同时成立了!!!两个while循环也同时执行了!!!
但是不用惊讶,我们先从上面创建子进程的样例中总结一点东西。
- fork 之后,执行流会变成两个执行流。
- fork 之后,谁先运行由调度器决定。
- fork 之后,在 fork 函数后面的代码,父子进程是共享的,一般通过 if 和 else if 来把执行流分流。(从 A只打印一份,B打印两份 ;两个循环都执行 可以看出)
📕 fork做了什么
前面说到,进程 = 内核数据结构 + 代码和数据 ,所以,fork 创建一个子进程,也必然要 创建对应的pcb 和 加载代码和数据到内存。但是,实际上只需要创建 pcb 即可,子进程的代码和数据是使用父进程的。
如下图,蓝色方框代表父进程的pcb,其代码和数据放在右边的黄色方框(两层来区分代码和数据)。创建子进程之后,只创建了子进程的 pcb ,这个 pcb 绝大部分信息和 父进程的pcb 是一样的,少数不同(pid,ppid 等等),然后用链表连起来。子进程的pcb 里面的指针,指向的代码和数据和父进程的是同一块。
同时,进程的运行是具有独立性的!就好像我们同时打开 浏览器、微信、qq、迅雷,随便关掉一个,并不影响其他的使用。父子进程也是两个进程,所以也具有独立性!从下图可以看出,杀掉子进程,父进程依然在执行。
可是,既然进程是互相独立的,父子进程都有各自的 pcb ,这当然没话说,但是却共用同一份代码和数据?这如何体现独立性呢?
对于代码而言,代码是只读的,无论父子进程都无法修改,所以可以说是独立的;但是对于数据而言,怎么证明它是独立的呢?
我们可以通过下面的代码来证明。先设置一个变量 x ,在一个执行流中,不改变 x 的值,输出其值和地址;在另一个执行流中,改变其值,输出其值和地址。
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>int main()
{int x=100; pid_t ret=fork();if(ret == 0){while(1){printf("我是子进程,我的pid是:%d,我的父进程是:%d,x的值是 %d ,地址是: %p \n",getpid(),getppid(),x,&x);sleep(1);}}else if(ret > 0) {while(1){printf("我是父进程,我的pid是:%d,我的父进程是:%d,x的值是 %d ,地址是: %p \n",getpid(),getppid(),x,&x);x=666;sleep(1);}}return 0;
}
编译运行之后,结果如下,可以看出,当一个进程修改 x 的值之后,并不会对另一个进程产生干扰。
对于数据而言:当有一个执行流想要修改数据的时候,操作系统会自动给该进程触发写时拷贝
。写时拷贝就是,当想要写入数据的时候,将这份数据拷贝到另一个地方,将写入的数据放到新的空间,不影响原来的空间。(当然这只是简单概念,具体信息要了解进程地址空间)
但是,变量x 打印出来的地址却是一样的,即使按照写时拷贝的说法,也说不通呀。那么只有一个理由,这里的地址不是物理地址(实际上是进程地址空间 / 虚拟地址)。
📕 如何理解 fork 有两个返回值
当函数内部准备执行 return 的时候,函数的主体功能是已经完成了的。
fork 它本质上就是操作系统提供的一个函数,提供接口给外部使用,创建子进程。当 fork 函数执行到return 语句的时候,主体功能已经完成,子进程已经被创建,共享下面的代码。而return 也是语句,所以 return 既要被父进程使用,又要被子进程使用。相当于return了两次。
至于一个变量存储了两个值,实际上是写时拷贝。
相关文章:
【Linux】进程概念、fork() 函数 (干货满满)
文章目录📕 前言📕 进程概念📕 Linux下查看进程的两种方法方法一方法二📕 pid() 、ppid() 函数📕 fork() 函数、父子进程初识再理解📕 fork做了什么📕 如何理解 fork 有两个返回值📕…...
【动态规划】最长上升子序列、最大子数组和题解及代码实现
Halo,这里是Ppeua。平时主要更新C语言,C,数据结构算法......感兴趣就关注我吧!你定不会失望。 🌈个人主页:主页链接 🌈算法专栏:专栏链接 我会一直往里填充内容哒! &…...
Ajax进阶篇02---跨域与JSONP
前言❤️ 不管前方的路多么崎岖不平,只要走的方向正确,都比站在原地更接近幸福 ❤️Ajax进阶篇02---跨域与JSONP一、Ajax进阶篇02---跨域与JSONP(1)同源策略1.1 什么是同源1.2 什么是同源策略(2)跨域2.1 什…...
C 语言编程 — 线程池设计与实现
目录 文章目录目录线程池(Thread Pool)tiny-threadpool数据结构设计Task / JobTask / Job QueueWorker / ThreadThread Pool ManagerPublic APIsPrivate Functions运行示例线程池(Thread Pool) 线程池(Thread Pool&am…...
并发编程要点
Java并发编程中的三大特性分别是原子性、可见性和有序性,它们分别靠以下机制实现: 原子性:原子性指的是对于一个操作,要么全部执行,要么全部不执行。Java提供了一些原子性操作,例如AtomicInteger等…...
HDFS黑名单退役服务器
黑名单:表示在黑名单的主机IP地址不可以,用来存储数据。 企业中:配置黑名单,用来退役服务器。 黑名单配置步骤如下: 1)编辑/opt/module/hadoop-3.1.3/etc/hadoop目录下的blacklist文件 添加如下主机名称&…...
基于stm32智能语音电梯消毒系统
这次来分享个最近做的项目,stm32智能语音电梯消毒系统功能说明:在电梯,房间,客道区域内,检测到人,则执行相关动作!例如继电器开关灯,喷洒酒精等行为。手机app/微信小程序可以控制需要…...
FreeRTOS系列第1篇---为什么选择FreeRTOS?
1.为什么学习RTOS? 作为基于ARM7、Cortex-M3硬件开发的嵌入式工程师,我一直反对使用RTOS。不仅因为不恰当的使用RTOS会给项目带来额外的稳定性风险,更重要的是我认为绝大多数基于ARM7、Cortex-M3硬件的项目,还没复杂到使用RTOS的地…...
基于.NET Core内置浏览器窗体应用程序界面框架
更多开源项目请查看:一个专注推荐.Net开源项目的榜单 平常我们在做项目过程中,桌面软件具备操作高效、利用本地计算机做一些复杂运算、或者设定快捷操作等优势,但是桌面软件也有很多缺点,比如升级问题、系统兼容问题、系统bug排查…...
【数据结构初阶】一文带你学会归并排序(递归非递归)
目录 前言 递归实现 代码实现 非递归实现 代码实现 总结 前言 归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 作为一种典型的分而治之思想…...
Simulink壁咚(一)——What and How
目录 一、前言 二、Simulink 知多少 三、滤波算法 四、Model Verification 五、Model Coverage 六、Simulink测试实例 七、Simulink Test 八、Test Manager 九、Test Harness 十、 学习 一、前言 Simulink从2017b以后更加工程化和实用化,基于MBD的功能日趋…...
【PyTorch】Pytorch基础第0章
本文参加新星计划人工智能(Pytorch)赛道:https://bbs.csdn.net/topics/613989052 这是目录PyTorch的简介PyTorch 构建深度学习模型的步骤搭建pytorch使用环境PyTorch的简介 PyTorch 是一个开源的机器学习框架,由 Facebook 的人工智能研究院(…...
Android学习总结
积累熟练掌握 Java 语言,面向对象分析设计能力,反射原理,自定义注解及泛型,多次采用设计模式重构公司项目;熟练掌握 IVM 原理,反射,动态代理以及对 ClassLoader 热修复有比较深的理解࿱…...
虚拟机ubuntu安装samba服务
安装samba apt-get install samba 新建一个共享目录 mkdir /home/l/work chmod 777 /home/l/work 配置服务 配置 /etc/samba/smb.confsudo smbpasswd -a l(添加用户名名称) 防火墙关闭 Ubuntu中 我们使用命令查看当前防火墙状态; sudo ufw status inactive状态是防火墙关闭…...
开发板中的内存压力测试,你了解多少?
1. 测试目的内存压力测试的目的是评估开发板中的内存子系统性能和稳定性,以确保它能够满足特定的应用需求。开发板通常用于嵌入式系统、物联网设备、嵌入式智能家居等场景,这些场景对内存的要求通常比较高。其内存压力测试的主要目的有:1.对确…...
MATLAB | 这些花里胡哨的热图怎么画
好早之前写过一个绘制相关系数矩阵的代码,但是会自动求相关系数,而且画出来的热图只能是方形,这里写一款允许nan值出现,任意形状的热图绘制代码,绘制效果如下: 如遇到bug请后台提出,并去gitee下…...
Java开发的一些编码建议
1、无论是类、方法、字段、变量,尽可能的限制他们的作用范围,可以避免出现不必要的错误;同时虚拟机也能有更大的优化空间。 2、错误越早发现越好,编译时发生错误比在运行时发生错误好。而且编译时错误能更好的定位问题所在。 这…...
【YOLOv8/YOLOv7/YOLOv5/YOLOv4/Faster-rcnn系列算法改进NO.59】引入ASPP模块
前言作为当前先进的深度学习目标检测算法YOLOv8,已经集合了大量的trick,但是还是有提高和改进的空间,针对具体应用场景下的检测难点,可以不同的改进方法。此后的系列文章,将重点对YOLOv8的如何改进进行详细的介绍&…...
C++STL set/multiset容器 构造和赋值 大小和交换 插入和删除 查找和统计
文章目录set/multiset容器1 set容器 基本概念2 set容器 构造和赋值3 set容器 大小和交换4 set容器 插入和删除5 set容器 查找和统计set/multiset容器 1 set容器 基本概念 简介: 所有元素都会在插入时会被自动排序,例如,在set容器放入元素1、…...
产品研发项目进度管理软件工具有哪些推荐?整理10款最佳进度管理软件
项目进度管理是确保项目按时完成的关键过程,使用合适的项目进度管理工具能确保帮助项目管理者实时了解和控制项目的进展情况,及时发现和解决问题,减少项目风险,提高项目效率和管理水平。这里将整理出国内外最受欢迎的10款项目进度…...
「ML 实践篇」分类系统:图片数字识别
目的:使用 MNIST 数据集,建立数字图像识别模型,识别任意图像中的数字; 文章目录1. 数据准备(MNIST)2. 二元分类器(SGD)3. 性能测试1. 交叉验证2. 混淆矩阵3. 查准率与查全率4. P-R 曲…...
从大专到测开,上海某字母站大厂的面试题,岗位是测开(25K*16)
简单介绍一句,大专出身,三年经验。跳了四次槽,面试了无数次,现在把自己的面试经验整理出来分享给大家,堪称必杀技! 1,一切从实际出发,对实际工作进行适当修饰 2,不会的简…...
【面试题】Python软件工程师能力评估试题(一)
文章目录前言应试者需知(一)Python 语言基础能力评估1、理解问题并完成代码:2、阅读理解代码,并在空白处补充完整代码:3、编写一个装饰器:exposer4、阅读代码并在空白处补充完整代码:5、自行用P…...
Java八股文(Java多线程面试题)
并行和并发的区别?(1)并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;(2)并行是在不同实体上的多个事件,并发是在同一实体上的多个事件&#…...
小程序当前页面如何分享别的页面内容呢?
需求分析 因为功能的需要分为两点 他需要调转转发,并且有首页转发点击button按钮进行转发邀请好友帮忙助力,如何做到一个页面多种转发 如何区分,是button转发还剩右上角三个点转发呢? 通过onShareAppMessage()这个函数的事件…...
编写Java哪个编译器好
现在能够编写Java代码的工具简直不要太多,各种各样五花八门,但目前效率最高的还是Intellij Idea。但这个工具对于完全零基础的小白来说,第一次用起来是比较复杂的,因为它的功能太多了。这就好比你要学开车,如果上来就给…...
第十六章 Java为什么使用序列化
为何要指定serialVersionUID的值如果不指定显示serialVersionUID的值,jvm在序列化时会自动生成一个serialVersionUID,跟属性一起序列化,再进行持久化或者网络传输,在反序列化时,jvm会根据属性自动生成一个新版的serial…...
28岁小公司程序员,无车无房不敢结婚,要不要转行?
大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成…...
出道即封神的ChatGPT,现在怎么样了?
从互联网的普及到智能手机,都让广袤的世界触手而及,如今身在浪潮中的我们,已深知其力。前阵子爆火的ChatGPT,不少人保持观望态度。现如今,国内关于ChatGPT的各大社群讨论,似乎沉寂了不少,现在怎…...
【计算机视觉】CNN 可视化算法
文章目录一、CAM算法1.1 概述1.2 CAM算法介绍二、Grad-CAM算法2.1 概述2.2 Guided Backpropagation2.3 Occlusion Sensitivity2.4 Grad-CAM 整体结构和效果2.5 Grad-CAM 实现细节一、CAM算法 1.1 概述 本文介绍 2016 年提出的 CAM (Class Activation Mapping) 算法࿰…...
网站优化月总结/就业seo好还是sem
一 PON基础知识 1.1 PON技术概念 PON(Passive Optical Network)即无源光网络,一种基于点到多点(P2MP)拓朴的技术。“无源”指ODN(光分配网络)不含有任何电子器件及电子电源,ODN全部由光分路器Splitter等无源器件组成,不需要贵重的有源电子设…...
开通网站必须做域名空间/seo培训中心
1.FastDFS介绍 FastDFS是用c语言编写的一款开源的分布式文件系统。FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载…...
wordpress 备案链接/sem竞价托管费用
正则表达式的预编译功能 学习目标正则表达式的预编译功能Pattern用法matcher用法在阿里巴巴开发手册中规定如下:【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。说明: 不要在方法体内 定义:…...
禅城网站设计/拉新人拿奖励的app
特征脸方法是90年代初期由Turk和Pentland提出的目前最流行的算法之一,具有简单有效的特点,也称为基于主成分分析(principal component analysis,简称PCA)的人脸识别方法。特征子脸技术的基本思想是:从统计的观点,寻找人脸图像分布的基本元素&…...
app界面设计欣赏网站/推广普通话绘画
The information in this article applies to:- Microsoft SQL Server 7.0,2000数据库日志文件丢失时的恢复步骤Revision History:VersionDateCreatorDescription1.0.0.12003-3-25郑昀草稿 Implementation Scope:本文是用于向Microsoft SQL Server维护人员描述我误删…...
本校网站建设/业务多平台怎么样
B - 最少硬币问题 Description 设有n种不同面值的硬币,各硬币的面值存于数组T[1:n]中。现要用这些面值的硬币来找钱。可以使用的各种面值的硬币个数存于数组Coins[1:n]中。 对任意钱数0≤m≤20001,设计一个用最少硬币找钱m的方法。 对于给定的1≤n≤10…...