首页 文章 精选 留言 我的

精选列表

搜索[图像理解],共10000篇文章
优秀的个人博客,低调大师

理解Java线程池ThreadPoolExecutor

在Java的线程池的使用会有比较多的地方,有比较多的应用场景,介绍一下Java线程池ThreadPoolExecutor。 线程是一个操作系统概念。操作系统负责这个线程的创建、挂起、运行、阻塞和终结操作。而操作系统创建线程、切换线程状态、终结线程都要进行CPU调度----这是一个耗费时间和系统资源的事情。 大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。 1.ThreadPoolExecutor类 二话不多说,来看一下ThreadPoolExecutor类的具体实现源码。 继承抽象类AbstractExecutorService,它实现了ExecutorService接口 public class ThreadPoolExecutor extends AbstractExecutorService{····} public abstract class AbstractExecutorService implements ExecutorService {····} public interface ExecutorService extends Executor {····} public interface Executor {····} ThreadPoolExecutor类中提供了四个构造方法: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } 构造函数中需要传入的参数包括corePoolSize、maximumPoolSize、keepAliveTime、timeUnit、workQueue、threadFactory和handler。 corePoolSize:线程池主要用于执行任务的是核心线程数量。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 非核心线程:设置的大于corePoolSize参数小于maximumPoolSize参数的部分,就是线程池可以临时创建的“非核心线程”的最大数量。这种情况下如果某个线程没有运行任何任务,在等待keepAliveTime时间后,这个线程将会被销毁,直到线程池的线程数量重新达到corePoolSize。 maximumPoolSize:当前线程池允许创建的最大线程数量。那么如果设置的corePoolSize参数和设置的maximumPoolSize参数一致时,线程池在任何情况下都不会回收空闲线程。keepAliveTime和timeUnit也就失去了意义。 keepAliveTime:线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。 timeUnit:参数keepAliveTime的时间单位。 workQueue:一个阻塞队列,用来存储等待执行的任务。一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue。 threadFactory:线程工厂,主要用来创建线程。 handler:表示当拒绝处理任务时的策略。有以下四种取值: ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 2.线程池实现原理 (1)任务的执行 先来看一下ThreadPoolExecutor类中其他的一些比较重要成员变量: private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务 private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小,runState等)的改变都要使用这个锁 private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集 private volatile long keepAliveTime; //线程存活时间 private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间 private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列) private volatile int maximumPoolSize; //线程池最大能容忍的线程数 private volatile int poolSize; //线程池中当前的线程数 private volatile RejectedExecutionHandler handler; //任务拒绝策略 private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程 private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数 private long completedTaskCount; //用来记录已经执行完毕的任务个数 在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可: public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* * Proceed in 3 steps: * * 1. If fewer than corePoolSize threads are running, try to * start a new thread with the given command as its first * task. The call to addWorker atomically checks runState and * workerCount, and so prevents false alarms that would add * threads when it shouldn't, by returning false. * * 2. If a task can be successfully queued, then we still need * to double-check whether we should have added a thread * (because existing ones died since last checking) or that * the pool shut down since entry into this method. So we * recheck state and if necessary roll back the enqueuing if * stopped, or start a new thread if there are none. * * 3. If we cannot queue task, then we try to add a new * thread. If it fails, we know we are shut down or saturated * and so reject the task. */ int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); } ····························································································· <未完待续>

优秀的个人博客,低调大师

理解Docker(2):Docker 镜像

本系列文章将介绍Docker的有关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 (4)Docker 容器的隔离性 - 使用 cgroups 限制容器使用的资源 (5)Docker 网络 对于每个软件,除了它自身的代码以外,它的运行还需要有一个运行环境和依赖。不管这个软件是象往常一样运行在物理机或者虚机之中,还是运行在现在的容器之中,这些都是不变的。在传统环境中,软件在运行之前也需要经过 代码开发->运行环境准备 -> 安装软件 -> 运行软件 等环节,在容器环境中,中间的两个环节被镜像制作过程替代了。也就是说,镜像的制作也包括运行环境准备和安装软件等两个主要环节,以及一些其他环节。因此,Docker 容器镜像其实并没有什么新的理论,只是这过程有了新的方式而已。 镜像(image)是动态的容器的静态表示(specification),包括容器所要运行的应用代码以及运行时的配置。Docker 镜像包括一个或者多个只读层( read-only layers ),因此,镜像一旦被创建就再也不能被修改了。一个运行着的Docker 容器是一个镜像的实例( instantiation )。从同一个镜像中运行的容器包含有相同的应用代码和运行时依赖。但是不像镜像是静态的,每个运行着的容器都有一个可写层( writable layer ,也成为容器层 container layer),它位于底下的若干只读层之上。运行时的所有变化,包括对数据和文件的写和更新,都会保存在这个层中。因此,从同一个镜像运行的多个容器包含了不同的容器层。 Docker有两种方式来创建一个容器镜像: 创建一个容器,运行若干命令,再使用 docker commit 来生成一个新的镜像。不建议使用这种方案。 创建一个 Dockerfile 然后再使用 docker build 来创建一个镜像。大多人会使用 Dockerfile 来创建镜像。 1. docker build 生成镜像 1.1 生成过程实例 在使用 Dockerfile 创建容器之前,需要先准备一个 Dockerfile 文件,然后运行 docker build 命令来创建镜像。我们通过下面的例子来看看Docker 创建容器的过程。 FROM ubuntu:14.04 MAINTAINER sammy "sammy@sammy.com" RUN apt-get update RUN apt-get -y install ntp EXPOSE 5555 CMD ["/usr/sbin/ntpd"] 这是一个非常简单的Dockerfile,它的目的是基于 Ubuntu 14.04 基础镜像安装 ntp 从而生成一个新的镜像。看看其过程: root@devstack:/home/sammy/ntponubuntu# docker build -t sammy_ntp2 . Sending build context to Docker daemon 2.048 kB Step 1 : FROM ubuntu:14.04 ---> 4a725d3b3b1c Step 2 : MAINTAINER sammy "sammy@sammy.com" ---> Using cache ---> c4299e3f774c Step 3 : RUN apt-get update ---> Using cache ---> 694a19d54103 Step 4 : RUN apt-get -y install ntp ---> Running in 9bd153c65a76 Reading package lists... ... Fetched 561 kB in 10s (51.1 kB/s) Selecting previously unselected package libedit2:amd64. (Reading database ... 11558 files and directories currently installed.) ... Processing triggers for libc-bin (2.19-0ubuntu6.9) ... Processing triggers for ureadahead (0.100.0-16) ... ---> 9cc05cf6f48d Removing intermediate container 9bd153c65a76 Step 5 : EXPOSE 5555 ---> Running in eb4633151d98 ---> f5c96137bec9 Removing intermediate container eb4633151d98 Step 6 : CMD /usr/sbin/ntpd ---> Running in e81b1eae3678 ---> af678df648bc Removing intermediate container e81b1eae3678 Successfully built af678df648bc Dockerfile 中的每个步骤都会对应每一个 docker build 输出中的 step。 Step 1:FROM ubuntu:14.04 获取基础镜像 ubuntu:14.04. Docker 首先会在本地查找,如果找到了,则直接利用;否则从 Docker registry 中下载。在第一次使用这个基础镜像的时候,Docker 会从 Docker Hub 中下载这个镜像,并保存在本地: Step 1 : FROM ubuntu:14.04 14.04: Pulling from library/ubuntu 862a3e9af0ae: Pull complete 6498e51874bf: Pull complete 159ebdd1959b: Pull complete 0fdbedd3771a: Pull complete 7a1f7116d1e3: Pull complete Digest: sha256:5b5d48912298181c3c80086e7d3982029b288678fccabf2265899199c24d7f89 Status: Downloaded newer image for ubuntu:14.04 ---> 4a725d3b3b1c 以后再使用的时候就直接使用这个镜像而不再需要下载了。 Step 2:MAINTAINER sammy "sammy@sammy.com" 本例中依然是从 Cache 中环境新的镜像。在第一次的时候,Docker 会创建一个临时的容器1be8f33c1846,然后运行 MAINTAINER 命令,再使用 docker commit 生成新的镜像 Step 2 : MAINTAINER sammy "sammy@sammy.com" ---> Running in 1be8f33c1846 ---> c4299e3f774c 通过这个临时容器的过程(create -> commit -> destroy),生成了新的镜像c4299e3f774c: 2016-09-16T21:58:09.010886393+08:00 container create 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 2016-09-16T21:58:09.060071206+08:00 container commit 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (comment=, image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 2016-09-16T21:58:09.071988068+08:00 container destroy 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 这个镜像是基于 ubuntu 14.04 基础镜像生成的,layers 没有变化,只是元数据 CMD 发生了改变: "Cmd": [ "/bin/sh", "-c", "#(nop) ", "MAINTAINER sammy \"sammy@sammy.com\"" ] 因此可以认为只是镜像的元数据发生了改变。生成的新的镜像作为中间镜像会被保存在 cache 中。 Step 3:RUN apt-get update 本例中Docker 仍然从缓存中获取了镜像。在第一次的时候,Docker 仍然是通过创建临时容器在执行 docker commit 的方式来创建新的镜像: Step 3 : RUN apt-get update ---> Running in 8b3b97af3bd7 Ign http://archive.ubuntu.com trusty InRelease Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB] ... Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB] Fetched 22.2 MB in 16min 21s (22.6 kB/s) Reading package lists... ---> 694a19d54103 Removing intermediate container 8b3b97af3bd7 通过以上步骤,生成了新的中间镜像694a19d54103,它也会被保存在缓存中。你可以使用 docker inspect694a19d54103 命令查看该中间镜像,但是无法在docker images 列表中找到它,这是因为 docker images 默认隐藏了中间状态的镜像,因此你需要使用 docker images -a 来获取它: root@devstack:/home/sammy# docker images -a | grep 694a19d54103 <none> <none> 694a19d54103 11 hours ago 210.1 MB 该镜像和原始镜像相比,多了一个 layer,它保存的是 apt-get update 命令所带来的变化: "RootFS": { "Type": "layers", "Layers": [ "sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4", "sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc", "sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4", "sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6", "sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a", "sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93" #这一层是新加的 ] } Step 4:RUN apt-get -y install ntp 和上面 Step 3 过程一样,这个步骤也会通过创建临时容器,执行该命令,再使用 docker commit 命令生成一个中间镜像9cc05cf6f48d。和上面步骤生成的镜像相比,它又多了一层: root@devstack:/home/sammy# docker images -a | grep 9cc05cf6f48d <none> <none> 9cc05cf6f48d 10 hours ago 212.8 MB root@devstack:/home/sammy# docker inspect --format={{'.RootFS.Layers'}} 9cc05cf6f48d [sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4 sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4 sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6 sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93 sha256:a93086f33a2b7ee18eec2454b468141f95a403f5081284b6f177f83cdb3d54ba] Step 5:EXPOSE 5555 这一步和上面的 Step 2 一样,Docker 生成了一个临时容器,执行 EXPOSE 55 命令,再通过 docker commit 创建了中间镜像f5c96137bec9。该镜像的 layers 没有变化,但是元数据发生了一些变化,包括: "ExposedPorts": { "5555/tcp": {} } "Cmd": [ "/bin/sh", "-c", "#(nop) ", "EXPOSE 5555/tcp" ] Step 6:CMD ["/usr/sbin/ntpd"] 这一步和上面的步骤相同,最终它创建了镜像af678df648bc,该镜像只是修改了 CMD 元数据: "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"/usr/sbin/ntpd\"]" ] 该镜像也是Docker 根据本 Dockerfile 生成的最终镜像。它也出现在了 docker images 结果中: root@devstack:/home/sammy# docker images | grep af678df648bc sammy_ntp2 latest af678df648bc 11 hours ago 212.8 MB 我们可以使用 docker history 命令查看该镜像中每一层的信息: root@devstack:/home/sammy/ntponubuntu# docker history af678df648bc IMAGE CREATED CREATED BY SIZE COMMENT af678df648bc 16 hours ago /bin/sh -c #(nop) CMD ["/usr/sbin/ntpd"] 0 B f5c96137bec9 16 hours ago /bin/sh -c #(nop) EXPOSE 5555/tcp 0 B 9cc05cf6f48d 16 hours ago /bin/sh -c apt-get -y install ntp 2.679 MB 694a19d54103 16 hours ago /bin/sh -c apt-get update 22.17 MB c4299e3f774c 17 hours ago /bin/sh -c #(nop) MAINTAINER sammy "sammy@sa 0 B 4a725d3b3b1c 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 3 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'doc 7 B <missing> 3 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB <missing> 3 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B <missing> 3 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /u 194.6 kB <missing> 3 weeks ago /bin/sh -c #(nop) ADD file:ada91758a31d8de3c7 187.8 MB 以上过程说明: 容器镜像包括元数据和文件系统,其中文件系统是指对基础镜像的文件系统的修改,元数据不影响文件系统,只是会影响容器的配置 每个步骤都会生成一个新的镜像,新的镜像与上一次的镜像相比,要么元数据有了变化,要么文件系统有了变化而多加了一层 Docker 在需要执行指令时通过创建临时镜像,运行指定的命令,再通过 docker commit 来生成新的镜像 Docker 会将中间镜像都保存在缓存中,这样将来如果能直接使用的话就不需要再从头创建了。关于镜像缓存,请搜索相关文档。 1.2 Docker 镜像分层,COW 和 镜像大小(size) 1.2.1 镜像分层和容器层 从上面例子可以看出,一个 Docker 镜像是基于基础镜像的多层叠加,最终构成和容器的 rootfs (根文件系统)。当 Docker 创建一个容器时,它会在基础镜像的容器层之上添加一层新的薄薄的可写容器层。接下来,所有对容器的变化,比如写新的文件,修改已有文件和删除文件,都只会作用在这个容器层之中。因此,通过不拷贝完整的 rootfs,Docker 减少了容器所占用的空间,以及减少了容器启动所需时间。 1.2.2 COW 和镜像大小 COW,copy-on-write 技术,一方面带来了容器启动的快捷,另一方也造成了容器镜像大小的增加。每一次 RUN 命令都会在镜像上增加一层,每一层都会占用磁盘空间。举个例子,在 Ubuntu 14.04 基础镜像中运行 RUN apt-get upgrade 会在保留基础层的同时再创建一个新层来放所有新的文件,而不是修改老的文件,因此,新的镜像大小会超过直接在老的文件系统上做更新时的文件大小。因此,为了减少镜像大小起见,所有文件相关的操作,比如删除,释放和移动等,都需要尽可能地放在一个 RUN 指令中进行。 比如说,通过将上面的示例 Dockerfile 修改为: FROM ubuntu:14.04 MAINTAINER sammy "sammy@sammy.com" RUN apt-get update && apt-get -y install ntp EXPOSE 5555 CMD ["/usr/sbin/ntpd"] 结果产生的镜像,不仅层数少了一层(7 -> 6),而且大小减少了 0.001M :),因为这个例子比较特殊,文件都是添加,而没有更新,因此size 的下降非常小。 1.2.3 使用容器需要避免的一些做法 这篇文章10 things to avoid in docker containers列举了一些在使用容器时需要避免的做法,包括: 不要在容器中保存数据(Don’t store data in containers) 将应用打包到镜像再部署而不是更新到已有容器(Don’t ship your application in two pieces) 不要产生过大的镜像 (Don’t create large images) 不要使用单层镜像 (Don’t use a single layerimage) 不要从运行着的容器上产生镜像 (Don’t create imagesfrom running containers) 不要只是使用 “latest”标签 (Don’t use only the “latest” tag) 不要在容器内运行超过一个的进程 (Don’t run more than one process in a single container) 不要在容器内保存 credentials,而是要从外面通过环境变量传入 (Don’t store credentialsin the image. Use environment variables) 不要使用 root 用户跑容器进程(Don’t run processes as a root user) 不要依赖于IP地址,而是要从外面通过环境变量传入 (Don’t relyon IPaddresses) 2. Dockerfile 语法 上面的步骤说明了 Docker 可以通过读取 Dockerfile 的内容来生成容器镜像。Dockerfile 的每一行都是INSTRUCTION arguments 格式,即 “指令 参数”。关于 Dockerfile 的预防,请参考https://docs.docker.com/engine/reference/builder/。下面只是就一些主要的指令做一些说明。 2.1 几个主要指令 2.1.1 ADD 和 COPY Add:将 host 上的文件拷贝到或者将网络上的文件下载到容器中的指定目录 # Usage: ADD [source directory or URL] [destination directory] ADD /my_app_folder /my_app_folder 例子: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> ADD temp dockfile ENTRYPOINT top ADD 指令会将本地 temp 目录中的文件拷贝到容器的 dockfile 目录下面,从而在镜像中增加一个 layer。在未指定绝对路径的时候,会放到 WORKDIR 目录下面。 root@cc2a5605f905:/# ls dockfile/ dockerfile-add dockerfile-cmd dockerfile-env dockerfile-ports dockerfile-user dockerfile-user-h root@cc2a5605f905:/# pwd / 那两者有什么区别呢? ADD 多了2个功能, 下载URL和对支持的压缩格式的包进行解压. 其他都一样。比如ADD http://foo.com/bar.go /tmp/main.go 会将文件从因特网上方下载下来,ADD /foo.tar.gz /tmp/ 会将压缩文件解压再COPY过去 如果你不希望压缩文件拷贝到container后会被解压的话, 那么使用COPY。 如果需要自动下载URL并拷贝到container的话, 请使用ADD 2.1.2 CMD CMD:在容器被创建后执行的命令,和 RUN 不同,它是在构造容器时候所执行的命令 # Usage 1: CMD application "argument", "argument", .. CMD "echo" "Hello docker!" CMD 有三种格式: CMD ["executable","param1","param2"](like an exec, preferred form) CMD ["param1","param2"](作为 ENTRYPOINT 的参数) CMD command param1 param2(作为 shell 运行) 一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。 2.1.3ENTRYPOINT ENTRYPOINT :设置默认应用,会保证每次容器被创建后该应用都会被执行。CMD 和 ENTRYPOINT 的关系会在下面详细解释。 2.1.4ENV:设置环境变量,可以使用多次 # Usage: ENV key value ENV SERVER_WORKS 4 设置了后,后续的RUN命令都可以使用,并且会作为容器的环境变量。举个例子,下面是 dockfile: FROM ubuntu:14.04 ENV abc=1 ENV def=2 ENTRYPOINT top 生成镜像:docker build -t envimg4 -f dockerfile-env . 其元数据包括了这两个环境变量: "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "abc=1", "def=2" ], 启动容器:docker run -it --name envc41 envimg4。也能看到: "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "abc=1", "def=2" ] 进入容器:能看到定义的 abc 和 def 变量 root@devstack:/home/sammy/ntponubuntu# docker exec -it envc41 bash root@ba460e0e9dc4:/# echo $abc 1 root@ba460e0e9dc4:/# echo $def 2 2.1.5EXPOSE :向容器外暴露一个端口 # Usage: EXPOSE [port] EXPOSE 8080 2.1.6 FROM:指定进行的基础镜像,必须是第一条指令 # Usage: FROM [image name] FROM ubuntu 2.1.7MAINTAINER:可以在任意地方使用,设置镜像的作者 # Usage: MAINTAINER [name] MAINTAINER authors_name 2.1.8RUN:运行命令,结果会生成镜像中的一个新层 # Usage: RUN [command] RUN aptitude install -y ntp 2.1.9USER:设置该镜像的容器的主进程所使用的用户,以及后续 RUN, CMD 和 ENTRYPOINT 指令运行所使用的用户 语法: # Usage: USER [UID] USER 751 Dockerfile 中的默认用户是基础镜像中所使用的用户。比如,你的镜像是从一个使用非 root 用户 sammy 的镜像继承而来的,那么你的 Dockerfile 中 RUN 指定运行的命令的用户就会使用 sammy 用户。 举例: (1)创建 dockerfile 文件 root@devstack:/home/sammy/dockerfile# cat dockerfile-user FROM ubuntu:14.04 USER 1000 ENTRYPOINT top (2)创建镜像:docker build -t dockerfile-user-1000 -f dockerfile-user . (3)启动容器:docker run -it --name c-user-1000-3 dockerfile-user-1000 top 能看出来当前用户ID 为 1000: PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 1000 20 0 4440 648 548 S 0.0 0.0 0:00.00 sh 5 1000 20 0 19840 1296 984 R 0.0 0.1 0:00.00 top (4)基于该镜像再创造一个镜像,然后再启动一个容器,可以发现容器中进程所使用的用户ID 同样为 1000. 2.1.10VOLUME:允许容器访问host上某个目录 # Usage: VOLUME ["/dir_1", "/dir_2" ..] VOLUME ["/my_files"] 2.1.11WORKDIR:设置 CMD 所指定命令的执行目录 # Usage: WORKDIR /path WORKDIR ~/ 2.1.12 HEALTHCHECK: 容器健康检查 这是 Docker 1.12 版本中新引入的指令,其语法为 HEALTHCHECK [OPTIONS] CMD command。来看一个例子: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> RUN apt-get update RUN apt-get -y install curl EXPOSE 8888 CMD while true; do echo 'hello world' | nc -l -p 8888; done HEALTHCHECK --interval=10s --timeout=2s CMD curl -f http://localhost:8888/ || exit 1 在启动容器后,其health 状态首先是 starting,然后在过了10秒做了第一次健康检查成功后,变为 healthy 状态。 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 7 seconds ago Up 6 seconds (health: starting) 8888/tcp c-health2 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 9 seconds ago Up 8 seconds (health: starting) 8888/tcp c-health2 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 11 seconds ago Up 11 seconds (healthy) 8888/tcp c-health2 需要注意的是 CMD 是在容器之内运行的,因此,你需要确保其命令或者脚本存在于容器之内并且可以被运行。 2.2 几个比较绕的地方 2.2.1 EXPOSE 和 docker run -p -P 之间的关系 容器的端口必须被发出(publish)出来后才能被外界使用。Dockerfile 中的 EXPOSE 只是“标记”某个端口会被暴露出来,只有在使用了 docker run -p 或者 -P 后,端口才会被“发出”出来,此时端口才能被使用。 举例: (1)Dockerfile FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD while true; do echo 'hello world' | nc -l -p 8888; done (2)创建镜像:docker build -t no-exposed-ports -f dockerfile-ports . (3)启动容器1:docker run -d --name no-exposed-ports1 no-exposed-ports。此容器没有 exposed 和 published 任何端口。 (4)启动容器2:docker run -d --name no-exposed-ports2 -p 8888:8888 no-exposed-ports 此时容器的 8888 端口被发布为主机上的 8888 端口: "Ports": { "8888/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "8888" } ] } 该端口会正确返回: root@devstack:/home/sammy/dockerfile# telnet 0.0.0.0 8888 Trying 0.0.0.0... Connected to 0.0.0.0. Escape character is '^]'. hello world Connection closed by foreign host. (5)使用 -P 参数:docker run -d --name no-exposed-ports3 -P no-exposed-ports 此时没有任何端口被 published,说明 Docker 在使用了 “-P” 情形下只是自动将 exposed 的端口 published。 (6)使用 -p 加上一个不存在的端口:docker run -d --name no-exposed-ports4 -p 8889:8889 no-exposed-ports 此时,8889 端口会被暴露,但是没法使用。说明 -p 会将没有 exposed 的端口自动 exposed 出来。 (7)修改 dockerfile 为: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> EXPOSE 8888 CMD while true; do echo 'hello world' | nc -l -p 8888; done 创建镜像exposed-ports, 再运行docker run -d --name exposed-ports1 -P exposed-ports 创建一个容器,此时 8888 端口自动被 published 为主机上的 32776 端口: "Ports": { "8888/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "32776" } ] } 可见: EXPOSE或者--expose只是为其他命令提供所需信息的元数据,或者只是告诉容器操作人员有哪些已知选择。它只是作为记录机制,也就是告诉用户哪些端口会提供服务。它保存在容器的元数据中。 使用 -p 发布特定端口。如果该端口已经被 exposed,则发布它;如果它还没有被 exposed,则它会被 exposed 和 published。Docker 不会检查容器端口的正确性。 使用 -P 时 Docker 会自动将所有已经被 exposed 的端口发出出来。 2.2.2 CMD 和 ENTRYPOINT 这两个指令都指定了运行容器时所运行的命令。以下是它们共存的一些规则: Dockerfile 至少需要指定一个 CMD 或者 ENTRYPOINT 指令 CMD 可以用来指定 ENTRYPOINT 指令的参数 没有 ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”] 没有 CMD 错误,不允许 /bin/sh -c exec_entry p1_entry exec_entry p1_entry CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd exec_entry p1_entry exec_cmd p1_cmd CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd exec_entry p1_entry p1_cmd p2_cmd CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd 备注 只有 CMD 时,执行 CMD 定义的指令 CMD 和 ENTRYPOINT 都存在时,CMD 的指令作为 ENTRYPOINT 的参数 举例: (1)同时有 CMD 和 ENTRYPOINT FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD top ENTRYPOINT ps 此时会运行的指令为/bin/sh -c ps /bin/sh -c top 但是实际上只是运行了 ps: root@devstack:/home/sammy/dockerfile# /bin/sh -c ps /bin/sh -c top PID TTY TIME CMD 10789 pts/3 00:00:00 su 10790 pts/3 00:00:00 bash 18479 pts/3 00:00:00 sh 18480 pts/3 00:00:00 ps root@devstack:/home/sammy/dockerfile# /bin/sh -c ps PID TTY TIME CMD 10789 pts/3 00:00:00 su 10790 pts/3 00:00:00 bash 18481 pts/3 00:00:00 sh 18482 pts/3 00:00:00 ps (2)CMD 作为 ENTRYPOINT 的参数 FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD ["-n", "10"] ENTRYPOINT top 启动容器后运行的命令为/bin/sh -c top -n 10. 3. 在 Docker hub 上创建自己的镜像 当我们从docker镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。 从已经创建的容器中更新镜像,并且提交这个镜像 使用Dockerfile指令来创建一个新的镜像 通过以下步骤,采用第一种方法,在 docker hub 上创建自己的镜像: (1)创建 docker hub 帐号。https://hub.docker.com/ (2)基于一个镜像完成某些操作。比如基于 nginx 镜像,安装 ping ifconfig 等网络工具。首先运行docker run -it nginx /bin/bash基于 nginx:latest 创建一个容器,然后在容器中执行 apt-get 命令安装软件,然后运行 exit 退出容器。 (3)将容器中的内容保存为一个镜像 docker commit -m="install net tools" -a="sammyliu8" 3f8a4339aadd sammyliu8/nginx:v1 这里的3f8a4339aadd 为刚才容器的ID。此时,能在本地看到该镜像: (4)运行 docker login 登录 docker hub (5)运行docker push sammyliu8/nginx 将镜像上传到 docker hub。此时在 Docker hub 界面上能看到该镜像了。 (6)在其他节点上,可以运行 docker pull sammyliu8/nginx 拉该镜像了。 (7)不过,这样做出来的新nginx有个问题,那就是nginx 服务不会自动起来。这是因为,官方的 nginx 的CMD 为 nginx -g"daemon off;",但是新的镜像的CMD 为 /bin/bash。但是,运行前面命令启动的容器又无法安装软件。因此,只能先按照上面的步骤启动一个容器,制作镜像,然后基于该镜像再不带命令地再启一个容器,在另一个窗口中,使用docker commit 将其保存为新的镜像,并上传到docker hub中。问题解决。 参考链接 http://developers.redhat.com/blog/2016/03/09/more-about-docker-images-size/ http://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/ 本文转自SammyLiu博客园博客,原文链接:http://www.cnblogs.com/sammyliu/p/5877964.html ,如需转载请自行联系原作者

优秀的个人博客,低调大师

Linux 概念架构的理解

摘要 Linux kernel 成功的两个原因: 架构设计支持大量的志愿开发者加入到开发过程中; 每个子系统,尤其是那些需要改进的,都支持很好的扩展性。 正是这两个原因使得 Linux kernel 可以不断进化。 一、Linux内核在整个计算机系统中的位置 Fig 1 – 计算机系统分层结构 分层结构的原则: the dependencies between subsystems are from the top down: layers pictured near the top depend on lower layers, but subsystems nearer the bottom do not depend on higher layers. 这种子系统之间的依赖性只能是从上到下,也就是图中顶部的子系统依赖底部的子系统,反之则不行。 二、内核的作用 虚拟化(抽象),将计算机硬件抽象为一台虚拟机,供用户进程(process)使用;进程运行时完全不需要知道硬件是如何工作的,只要调用 Linux kernel 提供的虚拟接口(virtual interface)即可。 多任务处理,实际上是多个任务在并行使用计算机硬件资源,内核的任务是仲裁对资源的使用,制造每个进程都以为自己是独占系统的错觉。 PS:进程上下文切换就是要换掉程序状态字、换掉页表基地址寄存器的内容、换掉 current 指向的 task_struct 实例、换掉 PC ——>也就换掉了进程打开的文件(通过 task_struct 的 files 可以找到)、换掉了进程内存的执行空间(通过 task_struct 的 mem 可以找到); 三、Linux内核的整体架构 Linux内核的整体架构 中心系统是进程调度器(Process Scheduler,SCHED):所有其余的子系统都依赖于进程调度器,因为其余子系统都需要阻塞和恢复进程。当一个进程需要等待一个硬件动作完成 时,相应子系统会阻塞这个进程;当这个硬件动作完成时,子系统会将这个进程恢复:这个阻塞和恢复动作都要依赖于进程调度器完成。 上图中的每一个依赖箭头都有原因: 进程调度器依赖内存管理器(Memory manager):进程恢复执行时,需要依靠内存管理器分配供它运行的内存。 IPC 子系统依赖于内存管理器:共享内存机制是进程间通信的一种方法,运行两个进程利用同一块共享的内存空间进行信息传递。 VFS 依赖于网络接口(Network Interface):支持 NFS 网络文件系统; VFS 依赖于内存管理器:支持 ramdisk 设备 内存管理器依赖于 VFS,因为要支持交换(swapping),可以将暂时不运行的进程换出到磁盘上的交换分区(swap),进入挂起状态。 四、高度模块化设计的系统,利于分工合作。 只有极少数的程序员需要横跨多个模块开展工作,这种情况确实会发生,仅发生在当前系统需要依赖另一个子系统时; 硬件设备驱动(hardware device drivers)、文件系统模块(logical filesystem modules)、网络设备驱动(network device drivers)和网络协议模块(network protocol modules)这四个模块的可扩展性最高。 五、系统中的数据结构 任务列表(Task List) 进程调度器针对每个进程维护一个数据结构task_struct;所有的进程用链表管理,形成task list;进程调度器还维护一个current 指针指向当前正在占用 CPU 的进程。 内存映射(Memory Map) 内存管理器存储每个进程的虚拟地址到物理地址的映射;并且也提供了如何换出特定的页,或者是如何进行缺页处理。这些信息存放在数据结构mm_struct中。每个进程都有一个 mm_struct 结构,在进程的 task_struct 结构中有一个指针 mm 指向次进程的 mm_struct 结构。 在 mm_struct 中有一个指针 pgd,指向该进程的页目录表(即存放页目录首地址)——>当该进程被调度时,此指针被换成物理地址,写入控制寄存器 CR3(x86体系结构下的页基址寄存器) I-nodes VFS 通过 inodes 节点表示磁盘上的文件镜像,inodes 用于记录文件的物理属性。每个进程都有一个files_struct结构,用于表示该进程打开的文件,在 task_struct 中有个 files 指针。使用 inodes 节点可以实现文件共享。文件共享有两种方式:(1)通过同一个系统打开文件file指向同一个 inodes 节点,这种情况发生于父子进程间;(2)通过不同系统打开文件指向同一个 inode 节点,举例有硬链接;或者是两个不相关的指针打开同一个文件。 数据连接(Data Connection) 内核中所有的数据结构的根都在进程调度器维护的任务列表链表中。系统中每个进程的的数据结构 task_struct 中有一个指针 mm 指向它的内存映射信息;也有一个指针 files 指向它打开的文件(用户打开文件表);还有一个指针指向该进程打开的网络套接字。 六、子系统架构 1.进程调度器(Process Scheduler)架构 (1)目标 进程调度器是 Linux kernel 中最重要的子系统。系统通过它来控制对 CPU 的访问——不仅仅是用户进程对 CPU 的访问,也包括其余子系统对 CPU 的访问。 (2)模块 进程调度器 调度策略模块(scheduling policy module):决定哪个进程获得对 CPU 的访问权;调度策略应该让所有进程尽可能公平得共享 CPU。 体系结构相关模块(architecture-specific module)设计一组统一的抽象接口来屏蔽特定体系接口芯片的硬件细节。这个模块与 CPU 交互以阻塞和恢复进程。这些操作包括获取每个进程需要保存的寄存器和状态信息、执行汇编代码来完成阻塞或者恢复操作。 体系结构无关模块(architecture-independent module)与调度策略模块交互将决定下一个执行的进程,然后调用体系结构相关的代码去恢复那个进程的执行。不仅如此,这个模块还会调用内存管理器的 接口来确保被阻塞的进程的内存映射信息被正确得保存起来。 系统调用接口模块(system call interface)允许用户进程访问 Linux Kernel 明确暴露给用户进程的资源。通过一组定义合适的基本上不变的接口(POSIX 标准),将用户应用程序和 Linux 内核解耦,使得用户进程不会受到内核变化的影响。 (3)数据表示 调度器维护一个数据结构——task list,其中的元素时每个活动的进程 task_struct 实例;这个数据结构不仅仅包含用来阻塞和恢复进程的信息,也包含额外的计数和状态信息。这个数据结构在整个 kernel 层都可以公共访问。 (4)依赖关系、数据流、控制流 正如前面提到过的,调度器需要调用内存管理器提供的功能,去为需要恢复执行的进程选择合适的物理地址,正因为如此,所以 进程调度器子系统依赖于内存管理子系统。当其他内核子系统需要等待硬件请求完成时,它们都依赖于进程调度子系统进行进程的阻塞和恢复。这种依赖性通过函 数调用和访问共享的 task list 数据结构来体现。所有的内核子系统都要读或者写代表当前正在运行进程的数据结构,因此形成了贯穿整个系统的双向数据流。 除了内核层的数据流和控制流,OS 服务层还给用户进程提供注册定时器的接口。这形成了由调度器对用户进程的控制流。通常唤醒睡眠进程的用例不在正常的控制流范围,因为用户进程无法预知何时 被唤醒。最后,调度器与 CPU 交互来阻塞和恢复进程,这又形成它们之间的数据流和控制流——CPU 负责打断当前正在运行的进程,并允许内核调度其他的进程运行。 2.内存管理器(Memory Manager)架构 (1)目标 内存管理模块负责控制进程如何访问物理内存资源。通过硬件内存管理系统(MMU)管理进程虚拟内存和机器物理内存之间的映射。每一个进程都有自己独 立的虚拟内存空间,所以两个进程可能有相同的虚拟地址,但是它们实际上在不同的物理内存区域运行。MMU 提供内存保护,让两个进程的物理内存空间不互相干扰。内存管理模块还支持交换——将暂时不用的内存页换出到磁盘上的交换分区,这种技术让进程的虚拟地址空 间大于物理内存的大小。虚拟地址空间的大小由机器字长决定。 (2)模块 内存管理子系统 架构相关模块(architecture specific module)提供访问物理内存的虚拟接口; 架构无关模块(architecture independent module)负责每个进程的地址映射以及虚拟内存交换。当发生缺页错误时,由该模块负责决定哪个内存页应该被换出内存——因为这个内存页换出选择算法几 乎不需要改动,所以这里没有建立一个独立的策略模块。 系统调用接口(system call interface)为用户进程提供严格的访问接口(malloc 和 free;mmap 和 ummap)。这个模块允许用进程分配和释放内存、执行内存映射文件操作。 (3)数据表示 内存管理存放每个进程的虚拟内存到物理内存的映射信息。这种映射信息存放在 mm_struct 结构实例中,这个实例的指针又存放在每个进程的 task_struct 中。除了存放映射信息,数据块中还应该存放关于内存管理器如何获取和存储页的信息。例如:可执行代码能够将可执行镜像作为备份存储;但是动态申请的数据则 必须备份到系统页中。(这个没看懂,请高手解惑?) 最后,内存管理模块还应该存放访问和技术信息,以保证系统的安全。 (4)依赖关系、数据流和控制流 内存管理器控制物理内存,当页面失败(page fault)发生时,接受硬件的通知(缺页中断)—— 这意味着在内存管理模块和内存管理硬件之间存在双向的数据流和控制流。内存管理也依赖文件系统来支持交换和内存映射 I/O——这种需求意味着内存管理器需要调用对文件系统提供的函数接口(procedure calls),往磁盘中存放内存页和从磁盘中取内存页。因为文件系统请求非常慢,所以在等待内存页被换入之前,内存管理器要让进程需要进入休眠——这种需 求让内存管理器调用进程调度器的接口。由于每个进程的内存映射存放在进程调度器的数据结构中,所以在内存管理器和进程调度器之间也有双向的数据流和控制 流。用户进程可以建立新的进程地址空间,并且能够感知缺页错误——这里需要来自内存管理器的控制流。一般来说没有用户进程到内存管理器的数据流,但是用户 进程却可以通过 select 系统调用,从内存管理器获取一些信息。 3.虚拟文件系统(Virtual File System)架构 (1)目标 虚拟文件系统为存储在硬件设备上数据提供统一的访问接口。可以兼容不同的文件系统(ext2,ext4,ntf等等)。计算机中几乎所有的硬件设备 都被表示为一个通用的设备驱动接口。逻辑文件系统促进与其他操作系统标准的兼容性,并且允许开发者以不同的策略实现文件系统。虚拟文件系统更进一步,允许 系统管理员在任何设备上挂载任何逻辑文件系统。虚拟文件系统封装物理设备和逻辑文件系统的细节,并且允许用户进程使用统一的接口访问文件。 除了传统的文件系统目标,VFS 也负责装载新的可执行文件。这个任务由逻辑文件系统模块完成,使得 Linux 可以支持多种可执行文件。 (2)模块 虚拟文件系统模块 设备驱动模块(device driver module) 设备独立接口模块(Device Independent Interface):提供所有设备的同一视图 逻辑文件系统(logical file system):针对每种支持的文件系统 系统独立接口(system independent interface)提供硬件资源和逻辑文件系统都无关的接口,这个模块通过块设备节点或者字符设备节点提供所有的资源。 系统调用模块(system call interface)提供用户进程对文件系统的统一控制访问。虚拟文件系统为用户进程屏蔽了所有特殊的特性。 (3)数据表示 所有文件使用 inode 表示。每个 inode 都记录一个文件在硬件设备上的位置信息。不仅如此,inode 还存放着指向逻辑文件系统模块和设备驱动的的函数指针,这些指针能够执行具体的读写操作。通过按照这种形式(就是面向对象中的虚函数的思想)存放函数指 针,具体的逻辑文件系统和设备驱动可以向内核注册自己而不需要内核依赖具体的模块特性。 (4)依赖关系、数据流和控制流 一个特殊的设备驱动是 ramdisk,这个设备在主存中开辟一片区域,并把它当成持久性存储设备使用。这个设备驱动使用内存管理模块完成任务,所以在 VFS 与对内存管理模块存在依赖关系(图中的依赖关系反了,应该是 VFS 依赖于内存管理模块)、数据流和控制流。 逻辑文件系统支持网络文件系统。这个文件系统像访问本地文件一样,从另一台机器上访问文件。为了实现这个功能,一种逻辑文件系统通过网络子系统完成它的任务——这引入了 VFS 对网络子系统的一个依赖关系以及它们之间的控制流和数据流。 正如前面提到的,内存管理器使用 VFS 完成内存交换功能和内存映射 I/O。另外,当 VFS 等待硬件请求完成时,VFS 需要使用进程调度器阻塞进程;当请求完成时,VFS 需要通过进程调度器唤醒进程。最后,系统调用接口允许用户进程调用来存取数据。不像前面的子系统,VFS 没有提供给用户注册不明确调用的机制,所以没有从VFS到用户进程的控制流。 4.网络接口(Network Interface)架构 (1)目标 网络子系统让 Linux 系统能够通过网络与其他系统相连。这个子系统支持很多硬件设备,也支持很多网络协议。网络子系统将硬件和协议的实现细节都屏蔽掉,并抽象出简单易用的接口供用户进程和其他子系统使用——用户进程和其余子系统不需要知道硬件设备和协议的细节。 (2)模块 网络协议层模块图 网络设备驱动模块(network device drivers) 设备独立接口模块(device independent interface module)提供所有硬件设备的一致访问接口,使得高层子系统不需要知道硬件的细节信息。 网络协议模块(network protocol modules)负责实现每一个网络传输协议,例如:TCP,UDP,IP,HTTP,ARP等等~ 协议无关模块(protocol independent interface)提供独立于具体协议和具体硬件设备的一致性接口。这使得其余内核子系统无需依赖特定的协议或者设备就能访问网络。 系统调用接口模块(system calls interface)规定了用户进程可以访问的网络编程API (3)数据表示 每个网络对象都被表示为一个套接字(socket)。套接字与进程关联的方法和 inode 节点相同。通过两个 task_struct 指向同一个套接字,套接字可以被多个进程共享。 (4)数据流,控制流和依赖关系 当网络子系统需要等待硬件请求完成时,它需要通过进程调度系统将进程阻塞和唤醒——这形成了网络子系统和进程调度子系统之间的控制流和数据流。不仅如此,虚拟文件系统通过网络子系统实现网络文件系统(NFS)——这形成了 VFS 和网络子系统指甲的数据流和控制流。 七、结论 1、Linux 内核是整个 Linux 系统中的一层。内核从概念上由五个主要的子系统构成:进程调度器模块、内存管理模块、虚拟文件系统、网络接口模块和进程间通信模块。这些模块之间通过函数调用和共享数据结构进行数据交互。 2、Linux 内核架构促进了他的成功,这种架构使得大量的志愿开发人员可以合适得分工合作,并且使得各个特定的模块便于扩展。 可扩展性一:Linux 架构通过一项数据抽象技术使得这些子系统成为可扩展的——每个具体的硬件设备驱动都实现为单独的模块,该模块支持内核提供的统一的接口。通过这种方式,个人开发者只需要和其他内核开发者做最少的交互,就可以为 Linux 内核添加新的设备驱动。 可扩展性二:Linux 内核支持多种不同的体系结构。在每个子系统中,都将体系结构相关的代码分割出来,形成单独的模块。通过这种方法,一些厂家在推出他们自己的芯片时,他们的内核开发小组只需要重新实现内核中机器相关的代码,就可以讲内核移植到新的芯片上运行。 来源:51CTO

优秀的个人博客,低调大师

jPush 极光推送的理解

极光推送是给客户端ios或者android推送消息的工具。 只要在客户端嵌入sdk,同时配合服务器端来使用就可以了。 客户端可以给每个用户注册一个别名alias,和多个标签tag。 到时候,就可以给这些别名和标签发送推送了。 也可以给所有跟这个客户端有关联的用户发送消息。 客户端可以调用相应的接口来设置信息。 服务器端可以调用相应的接口来发送推送。 应用需要去极光上注册一个 $master_secret = 'a36fef7cbd5b47b74c20278c';$app_key='1d2bd69d09c3756fa20f9c96'; 这个值是确定这个应用的账号。 类似于用户名和密码。 它关联这个应用。 require_once 'vendor/autoload.php'; use JPush\Model as M; use JPush\JPushClient; use JPush\JPushLog; use Monolog\Logger; use Monolog\Handler\StreamHandler; use JPush\Exception\APIConnectionException; use JPush\Exception\APIRequestException; $br = '<br/>'; $spilt = ' - '; $master_secret = 'a36fef7cbd5b47b74c20278c'; $app_key='1d2bd69d09c3756fa20f9c96'; JPushLog::setLogHandlers(array(new StreamHandler('jpush.log', Logger::DEBUG))); $client = new JPushClient($app_key, $master_secret); //easy push try { $result = $client->push() ->setPlatform(M\all) //->setAudience(M\all) //->setAudience(M\audience(M\tag(array('tag1','tag2')))) //->setAudience(M\audience(M\alias(array('123')))) ->setAudience(M\audience(M\alias(array('1','123')),M\tag(array('tag1','tag2'))))//取交集,也就是别名为1,且其tag值为tag1或tag2的用户才能收到推送 ->setNotification(M\notification('Hi, JPush', M\android('Hi, android'), M\ios('你好,我是别名!', 'happy', 1, true))) ->printJSON() ->send(); echo 'Push Success.' . $br; echo 'sendno : ' . $result->sendno . $br; echo 'msg_id : ' .$result->msg_id . $br; echo 'Response JSON : ' . $result->json . $br; } catch (APIRequestException $e) { echo 'Push Fail.' . $br; echo 'Http Code : ' . $e->httpCode . $br; echo 'code : ' . $e->code . $br; echo 'Error Message : ' . $e->message . $br; echo 'Response JSON : ' . $e->json . $br; echo 'rateLimitLimit : ' . $e->rateLimitLimit . $br; echo 'rateLimitRemaining : ' . $e->rateLimitRemaining . $br; echo 'rateLimitReset : ' . $e->rateLimitReset . $br; } catch (APIConnectionException $e) { echo 'Push Fail: ' . $br; echo 'Error Message: ' . $e->getMessage() . $br; //response timeout means your request has probably be received by JPUsh Server,please check that whether need to be pushed again. echo 'IsResponseTimeout: ' . $e->isResponseTimeout . $br; } 这是调用接口。 setPlatform设置平台 setPlatform(M\all) //设置所有平台 setPlatform(M\platform('ios', 'android'))//设置android和ios setAudience设置受众 setAudience(M\all)设置所有受众 setAudience(M\audience(M\tag(array('tag1','tag2'))))//设置tag为tag1或tag2的受众,群发 setAudience(M\audience(M\alias(array('123')))) //设置别名alias为123的受众,单发 setAudience(M\audience(M\alias(array('1','123')),M\tag(array('tag1','tag2'))))//取交集,也就是别名为1,且其tag值为tag1或tag2的用户才能收到推送 setNotification设置通知 setNotification(M\notification('Hi, JPush')) //设置通用通知 setNotification(M\notification('Hi, JPush', M\android('Hi, android'))) //为安卓单独设置信息,ios则是显示第一个内容,也就是除了android之外的都是Hi,JPush setNotification(M\notification('Hi, JPush', M\ios('Hi, ios','happy',1,true)))//为ios单独设置信息 setNotification(M\notification('Hi, JPush', M\android('Hi, android'), M\ios('Hi, ios', 'happy', 1, true))) //两个都设置 printJSON打印json值 send发送 $sendno开发者指定的 API 调用标识 $msg_id 推送信息的唯一标示 如果想把一个推送关联一个新闻,可以通过参数extras setNotification(M\notification('Hi, JPush', M\android('Hi, android'), M\ios('快下班了', 'happy', 1, true,array('url'=>'www.123.com')))) function: JPush/Model/notification($alert /* args */) 构建notification对象 参数:$alert, ios(),android(),winphone()构建的对象 function: JPush/Model/ios($alert, $sound=null, $badge=null, $contentAvailable=null, $extras=null) 构建ios对象 setNotification(M\notification('Hi, JPush', M\android('Hi, android'), M\ios('快下班了', 'happy', 1, true,array('url'=>'www.123.com')))) 参数就是url,到时候就会通过url来进入新闻中去。 function: JPush/Model/android($alert, $title=null, $builder_id=null, $extras=null) 构建android对象 function: JPush/Model/winphone($alert, $title=null, $_open_page=null, $extras=null) 构建winphone对象 setMessage设置消息 设置message,本方法接受4个参数msg_content(string,必填),title(string),content_type(string),extras(Array) setMessage(M\message('这个是推送消息', '这是标题', '', array('url'=>'www.msg.com'))) //设置内容,标题,以及附加值 这里的数据,不是客户端来调用的,是系统调用的。可以不用设置。 tag,tag_and的区别,是前者是并集,后者是交集。 setAudience(M\audience(M\tag(array('tag1')),M\tag_and(array('tag2','tag3')), M\alias(array('123','1'))))//用户标签为tag1的,且用户别名为123的,且标签中必须同时有tag2,tag3的。 function sendAll($content = '', $extras = array(), $title = '壹指', $sound = 'default', $badge = '+1') { $response = $this->client->push()->setPlatform(M\all) ->setAudience(M\all) //->setNotification(M\notification($content)) ->setNotification(M\notification($content, M\android($content,$title,null,$extras), M\ios($content, $sound, $badge, true,$extras))) //->printJSON() ->setOptions(M\options(null, 86400, null, true))//第二个参数为0,表示不接受离线数据。86400表示离线数据保留一天。 ->send(); return $response->isOk; //isOk sendno msg_id } options 推送可选项。 当前包含如下几个可选项: sendnoint可选推送序号纯粹用来作为 API 调用标识,API 返回时被原样返回,以方便 API 调用方匹配请求与返回。 time_to_liveint可选离线消息保留时长推送当前用户不在线时,为该用户保留多长时间的离线消息,以便其上线时再次推送。默认 86400 (1 天),最长 10 天。设置为 0 表示不保留离线消息,只有推送当前在线的用户可以收到。 override_msg_idlong可选要覆盖的消息ID如果当前的推送要覆盖之前的一条推送,这里填写前一条推送的 msg_id 就会产生覆盖效果,即:1)该 msg_id 离线收到的消息是覆盖后的内容;2)即使该 msg_id Android 端用户已经收到,如果通知栏还未清除,则新的消息内容会覆盖之前这条通知;覆盖功能起作用的时限是:1 天。 如果在覆盖指定时限内该 msg_id 不存在,则返回 1003 错误,提示不是一次有效的消息覆盖操作,当前的消息不会被推送。 apns_productionboolean可选APNs是否生产环境 True 表示推送生产环境,False 表示要推送开发环境; 如果不指定则为推送生产环境。 (消息) JPush 官方 API LIbrary (SDK) 默认设置为推送 “开发环境”。 big_push_durationint可选定速推送时长(分钟)又名缓慢推送,把原本尽可能快的推送速度,降低下来,在给定的 n 分钟内,均匀地向这次推送的目标用户推送。最大值为 1440。未设置则不是定速推送。 本文转自TBHacker博客园博客,原文链接:http://www.cnblogs.com/jiqing9006/p/3945095.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

Macaca基础原理解析

导语 前面几篇文章介绍了在Macaca实践中的一些实用技巧与解决方案,今天简单分析一下Macaca的基础原理。这篇文章将以前面所分享的UI自动化Macaca-Java版实践心得中的demo为基础,进行一下实例讲解。 Macaca的基本组成 通过对源码各个模块的分析,可以帮助我们对Macaca的整体构成有一个基础的认识。Macaca已经开源,相关的源码在对应的github上都可以下载: https://github.com/macacajs。 大家会在alibaba集团的开源github上找到macaca的另一个仓库https://github.com/alibaba/macaca/,关于这两个仓库的关系这里简单讲一下,由于主仓库https://github.com/macacajs模块众多,所以在alibaba集团下的github上采用了a

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册