文章目录

  • 一、历史
  • 二、相关组件解析
    • 1、平台基石
      • ①、Jenkins
    • 2、任务执行
      • ①、Item&ItemGroup
      • ②、Job
      • ③、Run
      • ④、Executor&Executable
      • ⑤、WorkUnit
      • ⑥、AbstractProject和AbstractBuild
    • ⑦、Action
    • 3、资源设备
      • ①、Node
      • ②、Label
      • ③、Computer
    • 4、调度策略
      • ①、Queue
      • ②、NodeProvisioner
  • 三、流程调试
    • 1、创建一个FreeStyleProject
    • 2、构建调度调试
    • 3、构建开始调试

转载请注明

一、历史

Jenkins是由Java语言编写的一款开源的持续集成的工具,前身为Sun公司的Hudson,众所周知,sun2009年被Oracle收购,2010年末其主要贡献者与Oracle之间发生了内部争执,导致Hudson2011年年初,通过社区投票的方式,将项目名称由Hudson变更为Jenkins,但与此同时Oracle表示也会继续开发原来的Hudson,导致往后的一段时间,双方均认为对方是自己的复刻版本,到了第二年也就是2012的一月,Eclipse基金会将Hudson纳入,代表OracleHudson项目已经失去了兴趣,截止201311月,Jenkins已经拥有了远超Hudson的项目成员以及公共库

二、相关组件解析

Jenkins作为一款老牌CI/CD开源产品,历史悠久,也带来的代码量巨大的问题,这里尝试分析Jenkins源码中的几个核心组件

  • 平台基石:jenkins.model.Jenkins,它是Jenkins 的主要执行对象,所有的调度,运算,任务都是绑定在它上面。

  • 任务执行:在Jenkins中,所有的动作都包装为一个一个可执行的任务作业Job,底层采用线程来实现,其间分的层级也比较多,有:Item->Job->Run->Executor->Thread

  • 资源设备:提供CPU,内存等资源的设备,相关组件有LabelNodeComputer
  • 调度策略:如何分配执行任务与资源设备的关系,相关的组件有QueueNodeProvisionerMultiStageTimeSeriesTimeSeries

1、平台基石

①、Jenkins

系统的根对象,传承于Hudson,维持了某一个JobProjectExecutorUserBuildable Item等等的数据和状态,可以对Jenkins整个生命周期进行管理

2、任务执行

①、Item&ItemGroup

它是Jenkins中基本的配置单元,每一个Item都会被托管在一个ItemGroup中,而每一个Item本身也可以成为一个ItemGroup,这便形成了一个树状结构,而这个树状结构的根节点便是Jenkins

  • Item提供获取父ItemItemGroup的方法
public interface Item extends PersistenceRoot, SearchableModelObject, AccessControlled, OnMaster {/*** Gets the parent that contains this item.*/ItemGroup<? extends Item> getParent();(...)
  • 一个ItemGroup也提供获取所有子Item的方法
public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject {/*** Gets all the items in this collection in a read-only view* that matches supplied Predicate* @since 2.221*/default Collection<T> getItems(Predicate<T> pred) {return getItemsStream(pred).collect(Collectors.toList());}(...)

有点像文件系统中的文件夹的感觉,但是不同的是,在文件系统中,文件可以从一个目录移动到另一个目录,Item本身只能属于单独的ItemGroup,这个关系不能改变。

  • Windows设备管理器中,一个硬盘总是显示在磁盘驱动器下方,它永远不可能移动到处理器,显示器等等其他位置,同样的,ItemGroup也不是一个通用的容器,ItemGroup的每一个子类通常只能承载某种有限类型的Item,比如老熟人FreeStyleProject便是其子类

每一个Item都有一个唯一的名称用来区分其他Item,名称可以用“/”组合起来,从而形成一个完整的项目名称,并在Jenkins中来唯一标识。

②、Job

Job便是Jenkins下面可追踪的可运行实体,它是静态的概念,包含一次构建的所有配置信息,比如构建的脚本,被构建的源代码,构建所需时间,源代码仓库等等等信息

如果需要自定义一个Job类型,需要继承TopLevelItemDescriptor顶级Item描述器的对象,并为其加上Extension注解

③、Run

因为Job是一个可运行的实体,是一个静态的概念,那么在其运行起来后,对于这个动态的过程便是由Run来管理,比如在pre build stage任务执行构建前常做的拉取源代码,post build stage执行构建完成后执行归档产出物等,这些熟悉的操作都是由Run来管理,特别地,Run应该便是对应于Jenkins API中的Build

同样它也支持自定义,常与自定义Job来配合使用,毕竟自定义的Job理所应当需要特定的Run来执行才行

④、Executor&Executable

Executor是执行构建的线程,它可以按需执行,继承自Thread类,Executable为真正代替Executor执行计算的对象

  • Thread类使用run开启线程的时候,实际上调用的也是Runnable接口的run方法,所以既然Executor继承自Thread类,那么就应该还有一个类继承Runnable,这个类就是Executable
@Override
public void run() {//agent是否在线if (!owner.isOnline()) {(...)}//node节点是否为空if (owner.getNode() == null) {(...)}final WorkUnit workUnit;//将当前Executor写锁锁住lock.writeLock().lock();try {//记录执行开始时间startTime = System.currentTimeMillis();workUnit = this.workUnit;} finally {//释放当前Executor写锁lock.writeLock().unlock();}try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {//这个类是一个任务的一部分,代表了一个Executor的一次计算,一个Task包含多个SubTaskSubTask task;task = Queue.withLock(new Callable<SubTask>() {@Overridepublic SubTask call() throws Exception {if (!owner.isOnline()) {(...)}if (owner.getNode() == null) {(...)}workUnit.setExecutor(Executor.this);queue.onStartExecuting(Executor.this);(...)lock.writeLock().lock();try {Executor.this.executable = executable;} finally {lock.writeLock().unlock();}workUnit.setExecutable(executable);return task;}});Executable executable;//将当前Executor读锁锁住lock.readLock().lock();try {if (this.workUnit == null) {return;}executable = this.executable;} finally {//读锁解除lock.readLock().unlock();}if (LOGGER.isLoggable(FINE))LOGGER.log(FINE, getName()+" is going to execute "+executable);Throwable problems = null;try {//同步开始workUnit.context.synchronizeStart();if (executable == null) {return;}//估计时间executableEstimatedDuration = executable.getEstimatedDuration();(...)try (ACLContext context = ACL.as2(auth)) {//真正开始执行queue.execute(executable, task);}} catch (AsynchronousExecution x) {(...)} catch (Throwable e) {problems = e;} finally {(...)}} catch (InterruptedException e) {LOGGER.log(FINE, getName()+" interrupted",e);// die peacefully} catch(Exception | Error e) {LOGGER.log(SEVERE, getName()+": Unexpected executor death", e);} finally {if (asynchronousExecution == null) {finish2();}}
}
  • Thread类的start方法,自然也被Executor所继承下来,但是前者调用的是start是没有参数的,后者却并不支持直接调用,必须传入一个WorkUnit对象,否则会抛出UnsupportedOperationException
/*** Can't start executor like you normally start a thread.** @see #start(WorkUnit)*/
@Override
public void start() {throw new UnsupportedOperationException();
}/*protected*/ void start(WorkUnit task) {lock.writeLock().lock();try {this.workUnit = task;super.start();started = true;} finally {lock.writeLock().unlock();}
}

⑤、WorkUnit

表示从队列Queue中拿到Task任务交给给Executor的基本单位,类结构较为简单,有用于执行的任务SubTask,有执行线程Executor和其对应的Executable,最后还有一个工作单元的共享上下文

⑥、AbstractProject和AbstractBuild

前者表示软件构建的基础抽象类,其主要实现类有熟悉的ProjectMavenModule,后者代表软件运行的基础抽象类,其主要的实现类有熟悉的BuildMavenBuild

⑦、Action

盲猜应该是构建步骤,可以在任务配置中定义

3、资源设备

①、Node

Jenkins agent的基本类型,在实际操作中,也可以继承它来扩展定义新的agent类型,主要负责的是最基础的配置,比如ExecutorNumNodeNameDescription

②、Label

负责一组Node,因为在某些场景中,一个Job可能由多种语言实现,那么可以用Label来管理一组Node,如果遇到算力不足的情况,可以利用其内部属性NodeProvisioner来初始化新的Node

③、Computer

它和Node多少有点关系,但是也有一些显著的差异,Computer作为一系列Executor的拥有者,如果一个Node中没有配置Executor(可能是暂时的),那么也将不会拥有Computer对象,如果一个Node本身是存在的,且其已经执行过若干构建,当把它移除后,相应的Computer也需要一定时间被删除,在更改节点配置的时候,没有被影响的Computer依然会保存完整直到所有的Node都被删除

4、调度策略

①、Queue

这个对象实现了核心调度逻辑,其中的内部接口Task表示放置在此队列中的可执行任务,在队列中,Task可以被包装进另一个内部类Item中,所以我们可以跟踪额外的用于决定何时执行的其他数据,队列中的任务将经过如下几个阶段

  (进入) --> 等待任务队列 --+--> 阻塞任务队列|        ^|        ||        v+--> 可执行的任务 ---> 挂起 ---> 离开^            ||            |+--极少数情况--+

通常,一个任务的执行只能向出队方向移动,但是出现下面这种情况,任务的执行流程也可以回退

  • 被分配执行任务的Jenkins节点在执行器线程Executable启动前(或者实例化前)消失了,也就是这一个Jenkins节点被移除了,那么这个任务就可以回退到一个可执行的状态

换句话说,当执行器线程已经实例化的情况下,唯一能让让执行器线程Executable停止或销毁的情况是在让其执行的Jenkins节点不存在

另外,在任何阶段,队列中的节点都可以被移除出去,比如在构建的时候点击取消

②、NodeProvisioner

用于负载统计和决定何时需要增加新的NodeJenkins需要合理分配Executor和运算资源,保证运算充足的情况下,不启动冗余的Node,即使运算需求陡然升高,也会有条不紊的去开启Node,而NodeProvisioner便是通过调用Queue来管理下面的Node资源,而如何管理资源调度则是交给其内部类StandardStrategyImpl

三、流程调试

Jenkins所使用的Web框架,名为Stapler,这款框架国内很少见,网上的文档也极其稀少,所以这里仅仅做一个大致介绍

  • Stapler可以将程序对象Model绑定到一个URL上面,它能够自动帮我们的对象Model分配一个专属URL,创建一种非常直观的URL层级结构,这里的Model我们可以简单理解为对应于一个SpringMVC中的Controller

1、创建一个FreeStyleProject

Jenkins的创建Web页面为http://localhost:8080/jenkins/view/all/newJob,这个方法绑定到了View这个Model上面,但是View是一个抽象类,具体执行交由其实现类,对应的newJob.jelly页面查询到这个新建请求会发送给View.createItem方法

@RequirePOST
@Override
public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {ItemGroup<? extends TopLevelItem> ig = getOwner().getItemGroup();if (ig instanceof ModifiableItemGroup)return ((ModifiableItemGroup<? extends TopLevelItem>)ig).doCreateItem(req, rsp);return null;
}

方法会继续走到Jenkins对象的doCreateItem方法,然后继续走到ItemGroupMixIncreateTopLevelItem方法,这个方法会对这个工程进行若干合法性校验,校验通过会走到createProject方法

public synchronized TopLevelItem createTopLevelItem( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {(...)if(name==null)throw new Failure("Query parameter 'name' is required");{// check if the name looks good(...)}if(mode!=null && mode.equals("copy")) {(...)} else {if(isXmlSubmission) {(...)} else {(...)// 这一步真正开始创建result = createProject(descriptor, name, true);}}rsp.sendRedirect2(redirectAfterCreateItem(req, result));return result;
}

createProject方法会对创建的工程进一步做校验,如果合法,则调用newInstance进行创建

public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name, boolean notify )throws IOException {(...)TopLevelItem item = type.newInstance(parent, name);item.onCreatedFromScratch();item.save();add(item);Jenkins.get().rebuildDependencyGraphAsync();if (notify)ItemListener.fireOnCreated(item);return item;
}

创建工程完毕后,会进入http://localhost:8080/jenkins/job/Test/configure对工程进行若干配置,这一步对应的页面为configure.jelly页面,可以看到,当配置完成点击保存后,会把请求发送到configSubmit方法

根据Stapler的特性,这个请求肯定会发送到Job这个对象中,实际上,断点进入的方法为AbstractProject类,重写自JobdoConfigSubmit方法

@Override
@POST
public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {super.doConfigSubmit(req,rsp);updateTransientActions();// notify the queue as the project might be now tied to different nodeJenkins.get().getQueue().scheduleMaintenance();// this is to reflect the upstream build adjustments done aboveJenkins.get().rebuildDependencyGraphAsync();
}

给这个任务配置一个构建脚本

echo helloworld >> hello.txt

2、构建调度调试

对这个任务执行构建,断点走到ParameterizedJobMixIn对象的doBuild方法

default void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {getParameterizedJobMixIn().doBuild(req, rsp, delay);
}

这个方法会将当前构建任务从Queue中拿出来然后使用schedule2方法将这个构建任务放入一个WaitingItem的等待队列中,随时可以交给Jenkins来执行

public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {(...)Queue.Item item = Jenkins.get().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem();if (item != null) {rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());} else {rsp.sendRedirect(".");}
}

schedule2方法嵌套了好几层,不过始终都是在当前Queue对象下,真正提交构建任务的核心代码在scheduleInternal方法上,然后内部调用scheduleMaintenance方法提交当前构建任务

private @NonNull ScheduleResult scheduleInternal(Task p, int quietPeriod, List<Action> actions) {(...)scheduleMaintenance();   // let an executor know that a new item is in the queue.(...)
}
public Future<?> scheduleMaintenance() {return maintainerThread.submit();
}

submit方法也还没有结束

public synchronized Future<V> submit() {if (pending==null) {pending = new CompletableFuture<>();maybeRun();}return pending;
}

还要执行一个maybeRun方法,这里的submit方法会将当前任务正式提交进入等待队列,其中的参数是一个带回调的异步的方法,在这个方法中会继续直接调用task.call()创建一个新的FutureTask来将刚才提交的任务进行构建

private synchronized void maybeRun() {if (inprogress==null && pending!=null) {base.submit(new Callable<Void>() {@Overridepublic Void call() throws Exception {synchronized (AtmostOneTaskExecutor.this) {// everyone who submits after this should form a next batchinprogress = pending;pending = null;}try {(断点)              inprogress.complete(task.call());} catch (Throwable t) {LOGGER.log(Level.WARNING, null, t);inprogress.completeExceptionally(t);} finally {synchronized (AtmostOneTaskExecutor.this) {// if next one is pending, get that scheduledinprogress = null;maybeRun();}}return null;}});}
}

正常情况下,当前agentexecutor还有空闲的时候,这里是可以直接创建出我们想要的FutureTask来执行这个任务,执行构建的过程下面会聊,这里讲一下调度的特殊情况,当系统资源不足的情况下,即executor没有空闲,这段代码显然并不会立马执行,那么我们将断点打到下面代码的低13行,让程序停在这里,模拟了一下系统延迟执行的情况

  • 在聊特殊情况之前,首先了解一下Jenkins架构中的另一个抽象类SafeTimerTask,这个类是一个全局共享的定时器,他会每隔若干时间执行一个方法,在Queue类中有一个内部类为MaintainTask,它继承了这个抽象类,实现了doRun方法,并且在periodic方法中定义好了循环周期为五秒,也就是说MaintainTask对象每隔五秒就会执行一下doRun方法
private static class MaintainTask extends SafeTimerTask {private final WeakReference<Queue> queue;MaintainTask(Queue queue) {this.queue = new WeakReference<>(queue);}private void periodic() {long interval = 5000;Timer.get().scheduleWithFixedDelay(this, interval, interval, TimeUnit.MILLISECONDS);}@Overrideprotected void doRun() {Queue q = queue.get();if (q != null)q.maintain();elsecancel();}
}

当程序停下来后,我们就会发现,程序就会执行到这个doRun方法,那么这个方法的核心便是maintain方法,这个方法非常长,在整个Queue类中都应该算是一个很核心的方法,你长任你长,将这个方法简化,我们关注这一段代码:

public void maintain() {for (BuildableItem p : new ArrayList<>(buildables)) {(...)WorkUnitContext wuc = new WorkUnitContext(p);MappingWorksheet ws = new MappingWorksheet(p, candidates);Mapping m = loadBalancer.map(p.task, ws);m.execute(wuc);(...)}
}

Jenkins会将当前等待队列中一个一个拿出可构建的任务出来,然后调用execute方法执行

private void execute(WorkChunk wc, WorkUnitContext wuc) {assert capacity() >= wc.size();int e = 0;for (SubTask s : wc) {while (!get(e).isAvailable())e++;get(e++).set(wuc.createWorkUnit(s));}
}

这段代码需要关注这里的set方法,这里就可以看到调用了excutor.start()方法开始构建,还记得介绍Executor的时候有提到,要使用它开启一个线程,需要调用的是start的单参方法,无参的start会报UnsupportedOperationException,所以这里便传入了一个WorkUnit对象来开启线程执行任务

@Override
protected void set(WorkUnit p) {assert this.workUnit == null;this.workUnit = p;assert executor.isParking();executor.start(workUnit);// LOGGER.info("Starting "+executor.getName());
}

3、构建开始调试

前面讲到的两种方式最终都会调用Executor来执行构建任务,Executor开始执行的时候执行的是run方法,这段代码也非常长,所以还是将其简化,关注这段代码即可

@Override
public void run() {(...)startTime = System.currentTimeMillis();workUnit = this.workUnit;SubTask task;task = Queue.withLock(new Callable<SubTask>() {@Overridepublic SubTask call() throws Exception {workUnit.setExecutor(Executor.this);queue.onStartExecuting(Executor.this);SubTask task = workUnit.work;Executable executable = task.createExecutable();lock.writeLock().lock();try {Executor.this.executable = executable;} finally {lock.writeLock().unlock();}workUnit.setExecutable(executable);return task;}});Executable executable;executable = this.executable;Throwable problems = null;executableEstimatedDuration = executable.getEstimatedDuration();try (ACLContext context = ACL.as2(auth)) {queue.execute(executable, task);}(...)
}

将当前Executor中的Executable对象拿到,然后就进入execute方法执行构建

public void execute(@NonNull Runnable task, final ResourceActivity activity ) throws InterruptedException {(...)try {task.run();} finally {(...)}
}

task是传入的一个executable对象,这个对象实现了Runnable接口,然后调用其run方法,run方法还会执行一个execute方法,如下

protected final void execute(@NonNull RunExecution job) {if(result!=null)return;     // already built.OutputStream logger = null;StreamBuildListener listener=null;runner = job;onStartBuilding();try {long start = System.currentTimeMillis();try {try {Computer computer = Computer.currentComputer();Charset charset = null;if (computer != null) {charset = computer.getDefaultCharset();this.charset = charset.name();}logger = createLogger();listener = createBuildListener(job, logger, charset);listener.started(getCauses());Authentication auth = Jenkins.getAuthentication2();if (auth.equals(ACL.SYSTEM2)) {listener.getLogger().println(Messages.Run_running_as_SYSTEM());} else {String id = auth.getName();if (!auth.equals(Jenkins.ANONYMOUS2)) {final User usr = User.getById(id, false);if (usr != null) { // Encode user hyperlink for existing usersid = ModelHyperlinkNote.encodeTo(usr);}}listener.getLogger().println(Messages.Run_running_as_(id));}RunListener.fireStarted(this,listener);//执行入口setResult(job.run(listener));(...)} catch (ThreadDeath t) {(...)}} catch (ThreadDeath t) {throw t;} catch( Throwable e ) {(...)} finally {long end = System.currentTimeMillis();(...)}try {getParent().logRotate();} catch (Exception e) {LOGGER.log(Level.SEVERE, "Failed to rotate log",e);}} finally {(...)}
}

在这个execute方法中,会将当前执行的Computer对象拿到,然后还会创建若干的Listenner,如StreamBuildListenerRunListener等,用于构建期间发生的一些事件的监听,完成这些前置工作后,调用job对象run方法

@Override
public Result run(@NonNull BuildListener listener) throws Exception {final Node node = getCurrentNode();assert builtOn==null;builtOn = node.getNodeName();hudsonVersion = Jenkins.VERSION;this.listener = listener;Result result = null;buildEnvironments = new ArrayList<>();TearDownCheckEnvironment tearDownMarker = new TearDownCheckEnvironment();buildEnvironments.add(tearDownMarker);try {launcher = createLauncher(listener);if (!Jenkins.get().getNodes().isEmpty()) {if (node instanceof Jenkins) {listener.getLogger().print(Messages.AbstractBuild_BuildingOnMaster());} else {listener.getLogger().print(Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, node.getDisplayName())));Set<LabelAtom> assignedLabels = new HashSet<>(node.getAssignedLabels());assignedLabels.remove(node.getSelfLabel());if (!assignedLabels.isEmpty()) {boolean first = true;for (LabelAtom label : assignedLabels) {if (first) {listener.getLogger().print(" (");first = false;} else {listener.getLogger().print(' ');}listener.getLogger().print(label.getName());}listener.getLogger().print(')');}}} else {listener.getLogger().print(Messages.AbstractBuild_Building());}lease = decideWorkspace(node, Computer.currentComputer().getWorkspaceList());workspace = lease.path.getRemote();listener.getLogger().println(Messages.AbstractBuild_BuildingInWorkspace(workspace));for (WorkspaceListener wl : WorkspaceListener.all()) {wl.beforeUse(AbstractBuild.this, lease.path, listener);}getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener);getProject().getScmCheckoutStrategy().checkout(this);if (!preBuild(listener,project.getProperties()))return Result.FAILURE;result = doRun(listener);} finally {if (!tearDownMarker.tornDown) {result = Result.combine(result, tearDownBuildEnvironments(listener));}}if (node.getChannel() != null) {launcher.kill(getCharacteristicEnvVars());}if (result==null)    result = getResult();if (result==null)    result = Result.SUCCESS;return result;
}

这段代码会获取到构建的时候所需要的一些环境资源Environment,还会拿到当前工作空间workspace目录,如果配置了相关的构建前检出步骤,还会在这里执行,最后再调用doRun方法,返回值为当前任务是否执行成功,接下来的调用嵌套便很繁琐了,关系是:doRun()->build()->perform()->…->perform()

public boolean perform(AbstractBuild<?,?> build, Launcher launcher, TaskListener listener) throws InterruptedException {FilePath ws = build.getWorkspace();if (ws == null) {Node node = build.getBuiltOn();if (node == null) {throw new NullPointerException("no such build node: " + build.getBuiltOnStr());}throw new NullPointerException("no workspace from node " + node + " which is computer " + node.toComputer() + " and has channel " + node.getChannel());}FilePath script=null;int r = -1;try {try {script = createScriptFile(ws);} catch (IOException e) {Util.displayIOException(e,listener);Functions.printStackTrace(e, listener.fatalError(Messages.CommandInterpreter_UnableToProduceScript()));return false;}try {EnvVars envVars = build.getEnvironment(listener);for(Map.Entry<String,String> e : build.getBuildVariables().entrySet())envVars.put(e.getKey(),e.getValue());launcher.prepareFilterRules(build, this);Launcher.ProcStarter procStarter = launcher.launch();procStarter.cmds(buildCommandLine(script)).envs(envVars).stdout(listener).pwd(ws);try {Proc proc = procStarter.start();r = join(proc);} catch (EnvVarsFilterException se) {LOGGER.log(Level.FINE, "Environment variable filtering failed", se);return false;}if(isErrorlevelForUnstableBuild(r)) {build.setResult(Result.UNSTABLE);r = 0;}} catch (IOException e) {(...)}return r==0;} finally {(...)}
}

到这一步就已经开始执行我们配置的shell命令了,还可以继续再深入,感兴趣就继续跟下去吧

Jenkins任务调度源码简要分析相关推荐

  1. [Java] HashMap 源码简要分析

    特性 * 允许null作为key/value. * 不保证按照插入的顺序输出.使用hash构造的映射一般来讲是无序的. * 非线程安全. * 内部原理与Hashtable类似. 源码简要分析 publ ...

  2. RxJava Agera 从源码简要分析基本调用流程(2)

    2019独角兽企业重金招聘Python工程师标准>>> 版权声明:本文由晋中望原创文章,转载请注明出处:  文章原文链接:https://www.qcloud.com/communi ...

  3. Android 5.1 Settings源码简要分析

    概述: 先声明:本人工作快两年了,仍是菜鸟级别的,惭愧啊!以前遇到好多知识点都没有记录下来,感觉挺可惜的,现在有机会接触Android 源码.我们一个Android组的搞Setting,我觉得是得写得 ...

  4. Redis源码简要分析

    在文章的开头我们把所有服务端文件列出来,并且标示出其作用: adlist.c //双向链表 ae.c //事件驱动 ae_epoll.c //epoll接口, linux用 ae_kqueue.c / ...

  5. android广播注册源码,android 广播源码简要分析-注册

    android broadcast 1,广播注册 静态注册: 在系统服务启动时会添加PackageManagerService,在该类的构造方法中就会对各个应用安装目录的apk文件进行扫描解析 详细步 ...

  6. 资源调度源码分析和任务调度源码分析

    1.资源调度源码分析 资源请求简单图 资源调度Master路径: 路径:spark-1.6.0/core/src/main/scala/org.apache.spark/deploy/Master/M ...

  7. wifidog 源码初分析

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://quietmadman.blog.51cto.com/3269500/138629 ...

  8. vboot源码详细分析-1

    最近一直在研究bootloader之vboot,vboot短小精悍,如果只是用来进行系统的引导,而不要提供其他复杂的功能时候,我认为这是绝佳的上选.这里以MINI2440开发板配套的源码进行分析.这个 ...

  9. Mybatis底层原理学习(二):从源码角度分析一次查询操作过程

    在阅读这篇文章之前,建议先阅读一下我之前写的两篇文章,对理解这篇文章很有帮助,特别是Mybatis新手: 写给mybatis小白的入门指南 mybatis底层原理学习(一):SqlSessionFac ...

最新文章

  1. NSArray基础-数组排序
  2. 无线宝服务器连接不上,无线网络连接不上怎么办 为什么无线网络连接不上
  3. 两点之间的连线java_java计算图两点之间的路径实例代码
  4. Vue.js-Day05【安装路由(vue-router)、如何使用vue-router、404配置、激活class、动态路由、编程式导航、路由嵌套、路由元信息、导航拦截】
  5. Android_(菜单)选项菜单
  6. 信息学奥赛一本通(C++)在线评测系统——基础(三)数据结构 —— 1354:括弧匹配检验
  7. python查漏补缺--抽象类和接口以及Overrides、函数重载
  8. IDEA打开父类的接口方法快捷键
  9. dsPIC33EP 高速PWM模块初始化设置及应用
  10. 项目管理基本目录结构
  11. 杨洋python_杨洋老师 - 主页
  12. 为什么有些人喜欢用fiddler来抓包?
  13. ArcToolBox 提示ActiveX控件问题解决办法
  14. Android TextView设置多样式文本,跑马灯以及霓虹灯效果
  15. springmvc上传图片后显示损毁或不能显示_猿蜕变系列7——也说说springMVC上传姿势...
  16. mysql 窗口函数_MySQL-窗函数
  17. linux整人指令,六个愚人节Linux恶作剧
  18. php mysql_query 返回值
  19. 免费图片识别文字软件-办公利器
  20. MaprRduce v2 在 java 代码中远程提交作业到 Yarn 的配置项

热门文章

  1. 我比较笨,我得一步一步来
  2. Emscripten中的虚拟文件系统
  3. Gulp折腾记 - (3)常用任务构建的demo[改进版]
  4. Java实现文件下载
  5. NodeJS 发送 POST 请求 curl -d JS 类的静态属性使用
  6. 【学习随记】自由空间阻抗匹配
  7. 月下夜想曲200.6(攻略1)
  8. java+jsp基于ssm的智慧医疗系统医院挂号就诊系统-计算机毕业设计
  9. 超低排放行业标准发布!
  10. php搞笑证件,怎么制作搞笑证件 网络搞笑证件制作的软件怎么用的