Yarn源码分析之MapReduce作业中任务Task调度整体流程(一)
v2版本的MapReduce作业中,作业JOB_SETUP_COMPLETED事件的发生,即作业SETUP阶段完成事件,会触发作业由SETUP状态转换到RUNNING状态,而作业状态转换中涉及作业信息的处理,是由SetupCompletedTransition来完成的,它主要做了四件事:
1、通过设置作业Job的成员变量setupProgress为1,标记作业setup已完成;
2、调度作业Job的Map Task;
3、调度作业的JobReduce Task;
4、如果没有task了,则生成JOB_COMPLETED事件并交由作业的事件处理器eventHandler进行处理。
本文,我们就将研究作业Job中Task是如何被调度的。
首先看下SetupCompletedTransition中transition()方法关于作业Job中Task调度的代码,如下:
- // 调度作业Job的Map Task
- job.scheduleTasks(job.mapTasks, job.numReduceTasks == 0);
- // 调度作业Job的Reduce Task
- job.scheduleTasks(job.reduceTasks, true);
它实际上是通过Job,也就是JobImpl的scheduleTasks()完成的,这个方法需要两个参数,第一个是作业Job待调度任务的任务ID集合taskIDs,第二个参数是表示是否恢复任务输出的标志位recoverTaskOutput,对于Map-Only型作业中Map任务和所有类型作业的Reduce任务,都需要恢复,标志位recoverTaskOutput为true,具体代码如下:
- protected void scheduleTasks(Set<TaskId> taskIDs,
- boolean recoverTaskOutput) {
- // 遍历传入的任务集合taskIDs中的每个TaskId,对taskID做以下处理:
- for (TaskId taskID : taskIDs) {
- // 根据taskID从集合completedTasksFromPreviousRun中移除对应元素,并获取被移除的元素TaskInfo实例taskInfo
- TaskInfo taskInfo = completedTasksFromPreviousRun.remove(taskID);
- if (taskInfo != null) {// 若存在taskID对应任务信息TaskInfo实例taskInfo
- // 构造T_RECOVER类型任务恢复事件TaskRecoverEvent,交给eventHandler处理,标志位recoverTaskOutput表示是否恢复任务的输出,
- // 对于Map-Only型Map任务和所有的Reduce任务,都需要恢复,标志位recoverTaskOutput为true
- eventHandler.handle(new TaskRecoverEvent(taskID, taskInfo,
- committer, recoverTaskOutput));
- } else {
- // 否则,构造T_SCHEDULE类型任务调度事件TaskEvent,交给eventHandler处理
- eventHandler.handle(new TaskEvent(taskID, TaskEventType.T_SCHEDULE));
- }
- }
- }
scheduleTasks()方法遍历传入的任务集合taskIDs中的每个TaskId实例taskID,对taskID做以下处理:
1、根据taskID从集合completedTasksFromPreviousRun中移除对应元素,并获取被移除的元素TaskInfo实例taskInfo;
2、若存在taskID对应任务信息TaskInfo实例taskInfo,构造T_RECOVER类型任务恢复事件TaskRecoverEvent,交给eventHandler处理,标志位recoverTaskOutput表示是否恢复任务的输出,对于Map-Only型Map任务和所有的Reduce任务,都需要恢复,标志位recoverTaskOutput为true;
3、否则,构造T_SCHEDULE类型任务调度事件TaskEvent,交给eventHandler处理。
我们先看T_SCHEDULE类型任务调度事件TaskEvent的处理,它是交由Job的eventHandler来处理的,而这个eventHandler是在Job被创建时(即构造JobImpl实例时)由MRAppMaster的dispatcher来赋值的,而在MRAppMaster中,dispatcher被创建后就会注册任务事件的处理器TaskEventDispatcher实例,代码如下:
- dispatcher.register(TaskEventType.class, new TaskEventDispatcher());
而这个任务事件处理器TaskEventDispatcher中处理任务事件TaskEvent的handle()方法定义如下:
- private class TaskEventDispatcher implements EventHandler<TaskEvent> {
- @SuppressWarnings("unchecked")
- @Override
- public void handle(TaskEvent event) {
- Task task = context.getJob(event.getTaskID().getJobId()).getTask(
- event.getTaskID());
- ((EventHandler<TaskEvent>)task).handle(event);
- }
- }
它实际上是通过作业Job中相关任务Task的handle()方法来处理的,而这个任务Task的实现则是TaskImpl,其中对于各种任务事件的处理,也是类似作业Job,由一个任务Task的状态机进行处理,关于任务Task的状态机,我们会有专门的文章进行介绍,这里,您只需要知道在TaskImpl中,对于上述两种任务状态机中任务状态的转换、触发事件及事件处理者定义如下:
- private static final StateMachineFactory
- <TaskImpl, TaskStateInternal, TaskEventType, TaskEvent>
- stateMachineFactory
- = new StateMachineFactory<TaskImpl, TaskStateInternal, TaskEventType, TaskEvent>
- (TaskStateInternal.NEW)
- // 省略部分代码
- .addTransition(TaskStateInternal.NEW, TaskStateInternal.SCHEDULED,
- TaskEventType.T_SCHEDULE, new InitialScheduleTransition())
- // 省略部分代码
- .addTransition(TaskStateInternal.NEW,
- EnumSet.of(TaskStateInternal.FAILED,
- TaskStateInternal.KILLED,
- TaskStateInternal.RUNNING,
- TaskStateInternal.SUCCEEDED),
- TaskEventType.T_RECOVER, new RecoverTransition())
- // 省略部分代码
由此可见,对于T_RECOVER类型任务恢复事件TaskRecoverEvent,Task状态机指定由RecoverTransition处理,并且任务Task的状态会由NEW转换为RUNNING、FAILED、KILLED、SUCCEEDED等,而对于T_SCHEDULE类型任务调度事件TaskEvent,则由Task状态机指定为InitialScheduleTransition处理,并且任务Task的状态会由NEW转换为SCHEDULED。下面,我们挨个进行分析。
一、T_SCHEDULE类型任务调度事件TaskEvent
由InitialScheduleTransition进行处理,任务Task的状态会由NEW转换为SCHEDULED,InitialScheduleTransition代码如下:
- private static class InitialScheduleTransition
- implements SingleArcTransition<TaskImpl, TaskEvent> {
- @Override
- public void transition(TaskImpl task, TaskEvent event) {
- // 添加并调度任务运行尝试TaskAttempt,Avataar.VIRGIN表示它是第一个Attempt,
- // 而剩余的Avataar.SPECULATIVE表示它是为拖后腿任务开启的一个Attempt,即推测执行原理
- task.addAndScheduleAttempt(Avataar.VIRGIN);
- // 设置任务的调度时间scheduledTime为当前时间
- task.scheduledTime = task.clock.getTime();
- // 发送任务启动事件
- task.sendTaskStartedEvent();
- }
- }
InitialScheduleTransition的处理逻辑比较简单,大体如下:
1、调用addAndScheduleAttempt()方法,添加并调度任务运行尝试TaskAttempt,Avataar.VIRGIN表示它是第一个Attempt,而剩余的Avataar.SPECULATIVE表示它是为拖后腿任务开启的一个Attempt,即推测执行原理;
2、设置任务的调度时间scheduledTime为当前时间;
3、发送任务启动事件。
其中,1中的addAndScheduleAttempt()方法实现如下:
- // This is always called in the Write Lock
- private void addAndScheduleAttempt(Avataar avataar) {
- // 调用addAttempt()方法,创建一个任务运行尝试TaskAttempt实例attempt,
- // 并将其添加到attempt集合attempts中,还会设置attempt的Avataar属性
- TaskAttempt attempt = addAttempt(avataar);
- // 将attempt的id添加到正在执行的attempt集合inProgressAttempts中
- inProgressAttempts.add(attempt.getID());
- //schedule the nextAttemptNumber
- // 调度TaskAttempt
- // 如果集合failedAttempts大小大于0,说明该Task之前有TaskAttempt失败过,此次为重新调度,
- // TaskAttemp事件类型为TA_RESCHEDULE,
- if (failedAttempts.size() > 0) {
- eventHandler.handle(new TaskAttemptEvent(attempt.getID(),
- TaskAttemptEventType.TA_RESCHEDULE));
- } else {
- // 否则为TaskAttemp事件类型为TA_SCHEDULE
- eventHandler.handle(new TaskAttemptEvent(attempt.getID(),
- TaskAttemptEventType.TA_SCHEDULE));
- }
- }
addAndScheduleAttempt()方法处理逻辑如下:
1、调用addAttempt()方法,创建一个任务运行尝试TaskAttempt实例attempt,并将其添加到attempt集合attempts中,还会设置attempt的Avataar属性;
2、将attempt的id添加到正在执行的attempt集合inProgressAttempts中;
3、调度TaskAttempt:如果集合failedAttempts大小大于0,说明该Task之前有TaskAttempt失败过,此次为重新调度,TaskAttemp事件类型为TA_RESCHEDULE,否则为TaskAttemp事件类型为TA_SCHEDULE。
而addAttempt()方法实现如下:
- private TaskAttemptImpl addAttempt(Avataar avataar) {
- / 调用createAttempt()方法创建任务运行尝试TaskAttemptImpl实例attempt
- TaskAttemptImpl attempt = createAttempt();
- // 设置attempt的Avataar属性
- attempt.setAvataar(avataar);
- // 记录debug级别日志信息:Created attempt ... ...
- if (LOG.isDebugEnabled()) {
- LOG.debug("Created attempt " + attempt.getID());
- }
- // 将创建的任务运行尝试TaskAttemptImpl实例attempt与其ID的对应关系添加到TaskImpl的任务运行尝试集合attempts中,
- // attempts先被初始化为Collections.emptyMap()
- // this.attempts = Collections.emptyMap();
- switch (attempts.size()) {
- case 0:
- // 如果attempts大小为0,即为Collections.emptyMap(),则将其更换为Collections.singletonMap(),并加入该TaskAttemptImpl实例attempt
- attempts = Collections.singletonMap(attempt.getID(),
- (TaskAttempt) attempt);
- break;
- case 1:
- // 如果attempts大小为1,即为Collections.singletonMap(),则将其替换为LinkedHashMap,并加入之前和现在的TaskAttemptImpl实例attempt
- Map<TaskAttemptId, TaskAttempt> newAttempts
- = new LinkedHashMap<TaskAttemptId, TaskAttempt>(maxAttempts);
- newAttempts.putAll(attempts);
- attempts = newAttempts;
- attempts.put(attempt.getID(), attempt);
- break;
- default:
- // 如果attempts大小大于1,说明其实一个LinkedHashMap,直接put吧
- attempts.put(attempt.getID(), attempt);
- break;
- }
- // 累加TaskAttempt计数器nextAttemptNumber
- ++nextAttemptNumber;
- // 返回TaskAttemptImpl实例attempt
- return attempt;
- }
其处理逻辑如下:
1、调用createAttempt()方法创建任务运行尝试TaskAttemptImpl实例attempt;
2、设置attempt的Avataar属性;
3、记录debug级别日志信息:Created attempt ... ...;
4、将创建的任务运行尝试TaskAttemptImpl实例attempt与其ID的对应关系添加到TaskImpl的任务运行尝试集合attempts中,attempts先被初始化为Collections.emptyMap():
4.1、如果attempts大小为0,即为Collections.emptyMap(),则将其更换为Collections.singletonMap(),并加入该TaskAttemptImpl实例attempt;
4.2、如果attempts大小为1,即为Collections.singletonMap(),则将其替换为LinkedHashMap,并加入之前和现在的TaskAttemptImpl实例attempt;
4.3、如果attempts大小大于1,说明其实一个LinkedHashMap,直接put吧;
5、累加TaskAttempt计数器nextAttemptNumber;
6、返回TaskAttemptImpl实例attempt。
继续往下追踪createAttempt()方法,其在TaskImpl中代码如下:
- protected abstract TaskAttemptImpl createAttempt();
这是一个抽象方法,由其子类实现,而它的子类有两个,表示Map任务的MapTaskImpl和表示Reduce任务的ReduceTaskImpl,其createAttempt()方法分别实现如下:
1、MapTaskImpl.createAttempt()
- @Override
- protected TaskAttemptImpl createAttempt() {
- return new MapTaskAttemptImpl(getID(), nextAttemptNumber,
- eventHandler, jobFile,
- partition, taskSplitMetaInfo, conf, taskAttemptListener,
- jobToken, credentials, clock, appContext);
- }
生成一个MapTaskAttemptImpl实例,传入表示Attempt序号的nextAttemptNumber、事件处理器eventHandler、作业文件jobFile、分区信息partition、分片元数据信息taskSplitMetaInfo等关键变量。
2、ReduceTaskImpl.createAttempt()
- @Override
- protected TaskAttemptImpl createAttempt() {
- return new ReduceTaskAttemptImpl(getID(), nextAttemptNumber,
- eventHandler, jobFile,
- partition, numMapTasks, conf, taskAttemptListener,
- jobToken, credentials, clock, appContext);
- }
生成一个ReduceTaskAttemptImpl实例,除不需要分片元数据信息taskSplitMetaInfo,和需要一个Map任务数numMapTasks外,其他与MapTaskAttemptImpl基本相同。
TaskAttempt生成了,接下来就应该进行调度执行了。我们再折回去看看addAndScheduleAttempt()方法中,发送的TA_SCHEDULE或TA_RESCHEDULE类型的TaskAttemptEvent,其与JobImpl、TaskImpl一样,是由TaskAttempt状态机负责处理的,如下所示:
- // 在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,
- // TaskAttempt的状态由NEW转换成UNASSIGNED
- .addTransition(TaskAttemptStateInternal.NEW, TaskAttemptStateInternal.UNASSIGNED,
- TaskAttemptEventType.TA_SCHEDULE, new RequestContainerTransition(false))
- // 在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,
- // TaskAttempt的状态由NEW转换成UNASSIGNED
- .addTransition(TaskAttemptStateInternal.NEW, TaskAttemptStateInternal.UNASSIGNED,
- TaskAttemptEventType.TA_RESCHEDULE, new RequestContainerTransition(true))
- // 上述二者的区别是RequestContainerTransition传入的标志位rescheduled,前者为false,后者为true
在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,TaskAttempt的状态由NEW转换成UNASSIGNED;在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,TaskAttempt的状态由NEW转换成UNASSIGNED;上述二者的区别是RequestContainerTransition传入的标志位rescheduled,前者为false,后者为true。
我们再看下RequestContainerTransition的实现,代码如下:
- @SuppressWarnings("unchecked")
- @Override
- public void transition(TaskAttemptImpl taskAttempt,
- TaskAttemptEvent event) {
- // Tell any speculator that we're requesting a container
- // taskAttempt的事件处理器eventHandler处理SpeculatorEvent事件,告诉所有的speculator,此时正在申请一个容器
- taskAttempt.eventHandler.handle
- (new SpeculatorEvent(taskAttempt.getID().getTaskId(), +1));
- //request for container
- // 申请容器
- if (rescheduled) {// Task的Attempt重新调度
- // 构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,
- // 这个eventHandler实际上是MRAppMaster中的dispatcher,依次经过TaskImpl、TaskAttemptImpl的创建传递过来的,
- taskAttempt.eventHandler.handle(
- ContainerRequestEvent.createContainerRequestEventForFailedContainer(
- taskAttempt.attemptId,
- taskAttempt.resourceCapability));
- } else {// Task的Attempt第一次调度
- // 构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,
- taskAttempt.eventHandler.handle(new ContainerRequestEvent(
- taskAttempt.attemptId, taskAttempt.resourceCapability,
- taskAttempt.dataLocalHosts.toArray(
- new String[taskAttempt.dataLocalHosts.size()]),
- taskAttempt.dataLocalRacks.toArray(
- new String[taskAttempt.dataLocalRacks.size()])));
- }
- // 两者创建的ContainerRequestEvent事件的区别是,rescheduled时,不需要考虑Node和Lock位置属性,因为此时Attempt之前已经失败过,此时应当能够以完成Attempt为首要任务,
- // 同时,两者的事件类型都是ContainerAllocator.EventType.CONTAINER_REQ,
- // MRAppMaster中的dispatcher针对该事件ContainerAllocator.EventType注册的事件处理器是LocalContainerAllocator或RMContainerAllocator
- }
RequestContainerTransition的transition()方法处理逻辑如下:
1、TaskAttempt的事件处理器eventHandler处理SpeculatorEvent事件,告诉所有的speculator,此时正在申请一个容器;
2、申请容器:
2.1、如果是Task的Attempt重新调度,构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,这个eventHandler实际上是MRAppMaster中的dispatcher,依次经过TaskImpl、TaskAttemptImpl的创建传递过来的;
2.2、否则如果是Task的Attempt第一次调度,构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理。
两者创建的ContainerRequestEvent事件的区别是,rescheduled时,不需要考虑Node和Lock位置属性,因为此时Attempt之前已经失败过,此时应当能够以完成Attempt为首要任务,同时,两者的事件类型都是ContainerAllocator.EventType.CONTAINER_REQ,MRAppMaster中的dispatcher针对该事件ContainerAllocator.EventType注册的事件处理器是LocalContainerAllocator或RMContainerAllocator。
关于Yarn容器等资源申请与分配RMContainerAllocator的介绍,我会在以后的文章中为大家讲解,这里,你只需要了解其执行的大体流程即可:
1、RMContainerAllocator首先间接继承自AbstractService,它是Hadoop中的一种服务,有服务初始化serviceInit()及服务启动serviceStart()方法要执行;
2、RMContainerAllocator针对容器请求分配事件,是一个双重生产者-消费者模式,第一层生产者通过其handle()方法,将容器请求分配ContainerAllocatorEvent加入其内部eventQueue队列,第一层消费者通过其内部事件处理线程eventHandlingThread,不断的从事件队列eventQueue中take事件进行消费,而消费的方式是做为第二层生产者,将事件按照任务类型放入调度请求列表scheduledRequests、pendingReduces中,scheduledRequests是一个复杂的区分Map和Reduce任务的会立即被调度的请求列表,而pendingReduces只是存储等待被调度的Reduce任务请求的列表,其会根据Yarn中资源情况和Map任务完成情况确定是将事件移送至(即rampUp)scheduledRequests,还是从scheduledRequests移回Reduce任务调度请求至pendingReduces(即rampDown),而第二层的消费者则是RMContainerAllocator祖先父类RMCommunicator中的心跳线程allocatorThread,它周期性的调用heartbeat()方法,从Yarn的RM中获取可用资源,然后消费scheduledRequests列表中的请求,进行容器分配;
3、RMContainerAllocator中,对于Map任务来说,它经历的数据结构,或者生命周期为scheduled->assigned->completed,而Reduce任务则是pending->scheduled->assigned->completed;
4、经过一些的复杂逻辑后,包括综合判断资源情况、任务本地性、优先调度失败任务、Map任务完成比例、针对拖后退的任务进行推测执行等,无论是Map任务还是Reduce任务,最终在分配到容器Container后,都会发送一个TaskAttemptContainerAssignedEvent事件,交由TaskAttemptImpl的状态机中ContainerAssignedTransition进行处理,而其方法则最终会构造ContainerRemoteLaunchEvent事件,进行Container远程加载,在远程或本机或本进程Container中Launch任务尝试进行任务的执行。
关于RMContainerAllocator,因为其结构、处理逻辑比较复杂,我会专门写文章进行分析,敬请期待!
二、T_RECOVER类型任务恢复事件TaskRecoverEvent
未完待续!敬请关注后续文章!
Yarn源码分析之MapReduce作业中任务Task调度整体流程(一)的更多相关文章
- Yarn源码分析之如何确定作业运行方式Uber or Non-Uber?
在MRAppMaster中,当MapReduce作业初始化时,它会通过作业状态机JobImpl中InitTransition的transition()方法,进行MapReduce作业初始化相关操作,而 ...
- Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(二)
本文继<Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)>,接着讲述MapReduce作业在MRAppMaster上处理总流程,继上篇讲到作业初始化之后的作 ...
- Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)
我们知道,如果想要在Yarn上运行MapReduce作业,仅需实现一个ApplicationMaster组件即可,而MRAppMaster正是MapReduce在Yarn上ApplicationMas ...
- Android多线程之(一)View.post()源码分析——在子线程中更新UI
提起View.post(),相信不少童鞋一点都不陌生,它用得最多的有两个功能,使用简便而且实用: 1)在子线程中更新UI.从子线程中切换到主线程更新UI,不需要额外new一个Handler实例来实现. ...
- Hbase源码分析:Hbase UI中Requests Per Second的具体含义
Hbase源码分析:Hbase UI中Requests Per Second的具体含义 让运维加监控,被问到Requests Per Second(见下图)的具体含义是什么?我一时竟回答不上来,虽然大 ...
- [源码分析] 分布式任务队列 Celery 之 发送Task & AMQP
[源码分析] 分布式任务队列 Celery 之 发送Task & AMQP 目录 [源码分析] 分布式任务队列 Celery 之 发送Task & AMQP 0x00 摘要 0x01 ...
- Hadoop2源码分析-MapReduce篇
1.概述 前面我们已经对Hadoop有了一个初步认识,接下来我们开始学习Hadoop的一些核心的功能,其中包含mapreduce,fs,hdfs,ipc,io,yarn,今天为大家分享的是mapred ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- 【源码分析】- 在SpringBoot中你会使用REST风格处理请求吗?
目录 前言 1.什么是 REST 风格 1.1 资源(Resources) 1.2 表现层(Representation) 1.3 状态转化(State Transfer) 1.4 综述 ...
随机推荐
- iOS开发——MJExtension复杂数组用法
最近在看MJExtension的Demo,发现了一个plist文件直接转数组模型的方法.以前研究过但是浅尝辄止没有解决,这几天有时间,好好看了看,找到了解决办法,与大家分享. 如果大家的项目中有这种嵌 ...
- MySQL查询时区分大小写(转)
说明:在MySQL查询时要区分大小写会涉及到两个概念character set和collation,这两个概念在表设计时或者在查询时都可以指定的,详细参考:http://www.cnblogs.com ...
- nginx 隐藏index.php 并开启rewrite日志调试(apache也有)
开启rewrite 日志 error_log /data/log/nginx/error.log notice; 位于最外层,大约在文件的前几行 再在http{}括号里增加一行:rewri ...
- delphi设计浮动窗口
delphi设计浮动窗口 用过Photoshop的朋友一定对它的那些方便的浮动面板记忆犹新,其实这些面板就是一个个的小窗体,但这些小窗体都放在Photoshop的主窗体上 (不是存在主窗体中),有自己 ...
- u-boot中添加mtdparts支持以及Linux的分区设置
简介 作者:彭东林 邮箱:pengdonglin137@163.com u-boot版本:u-boot-2015.04 Linux版本:Linux-3.14 硬件平台:tq2440, 内存:64M ...
- mac 切换默认python版本
https://www.zhihu.com/question/30941329 首先终端的“python”命令会执行/usr/local/bin下的“python”链接,链接相当于win下的快捷方式, ...
- Mysql5.6.x版本半同步主从复制的开启方法
介绍 先了解一下mysql的主从复制是什么回事,我们都知道,mysql主从复制是基于binlog的复制方式,而mysql默认的主从复制方式,其实是异步复制. 主库实际上并不关心从库是否把数据拉完没有, ...
- Yii2系列教程六:集成编辑器
上一篇文章我们实现了简单的用户权限管理,至于更先进的RBAC,我后面会单独出一篇文章来说说.在这一篇文章当中,我主要想写的是在Yii2中集成一个编辑器,因为在我们的实际开发当中,一个简单的textar ...
- 关于Linux开源项目基础组件make编译流程
关于Linux开源项目基础组件make编译流程 非常多Linux开源项目都会用到编译出可运行文件的make.这个是有一套流程的. 首先,GNU构建系统:https://en.wikipedia. ...
- 编程算法 - 和为s的两个数字 代码(C)
和为s的两个数字 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 输入一个递增排序的数组和一个数字s, 在数组中查找两个数, 使得它们的和正好是 ...