精华内容
下载资源
问答
  • docker 容器 defunct 僵尸进程

    千次阅读 2020-02-01 17:38:48
    在构建 Docker 容器时,我们需要注意子进程的“僵尸化”问题(PID 1 Zombie Reaping Problem)。这会导致不可预知的和看起来匪夷所思的问题。本文解释了这问题,也解释了如何去解决,并提供了种预构建方案:Base...

    在构建 Docker 容器时,我们需要注意子进程的“僵尸化”问题(PID 1 Zombie Reaping Problem)。这会导致不可预知的和看起来匪夷所思的问题。本文解释了这个问题,也解释了如何去解决,并提供了一种预构建方案:Baseimage-docker 。

    在通篇阅读之后,还有第二篇:

    《docker 僵尸进程解决方案 Baseimage-docker,胖容器和“容器也是虚拟机”》https://blog.csdn.net/whatday/article/details/104136271

    前言

    一年以前,当 Docker 还是 0.6 版本的时候,我们已经介绍过 Baseimage-docker 了——针对 Docker 优化的 Ubuntu 极小化基础镜像。我们可以从 Docker 仓库中将其拉取回来并作为基础镜像使用。

    笔者也算是 Docker 的早期使用者了,早在其 1.0 版本发布前,就已经将其用来做持续集成和构建开发环境了。因而,笔者才研发了 Baseimage-docker 以解决 Docker 工作模式中的一些问题。例如:Docker 并没有使用初始进程管理子进程地方式来运行任务,因此在容器结束时残留的僵尸进程可能会导致各种问题。Docker 也没有使用 syslog ,所以某些关键日志信息就无法被检索到,等等。

    而且,很多人并不理解 Baseimage-docker 究竟是要解决什么问题。因为这些都是鲜为人知的 Unix 操作系统底层工作机制。因此在本文中,我们将会详细讲解其中最重要的问题——子进程的“僵尸化”问题。

    我们认为:

    1. 这些 Baseimage-docker 所致力解决地问题适用于很多人;
    2. 大多数人并没有意识到这些问题,因此在创建自己的 Docker 镜像时会出现各种诡异地问题;
    3. 避免每个人去重复性地解决同样地问题是有价值的。

    因此笔者在闲暇时间将解决方案制作成了每个人都能使用的 Baseimage-docker 基础镜像。这份镜像中也包含了大量 Docker 镜像开发人员所需要的有用的工具。自此以后,笔者所有的 Docker 镜像就都是以此作为基础了。

    大家看起来也很认可这些工作:在 Docker 官方仓库中,Baseimage-docker 是紧随 Ubuntu 官方镜像和 CentOS 官方镜像之后的第三名最受欢迎镜像。

    初始进程的责任:“收割”“僵尸进程”

    Unix 的进程之间是树状结构的关系。每个进程都可以派生出子进程,而除了最顶端的进程之外,也都会有一个父进程。

    这个最顶端的进程就是初始进程,其在启动系统时被内核启动,并负责启动系统的其余功能部分。如:SSH 后台程序、Docker 后台程序、Apache/Nginx 和 GUI 桌面环境等等。这些程序又可能会派生出它们自己的子进程。

    这一部分并没有什么问题。但问题在于当一个进程终止时,会发生什么?假设上图中的 bash (5) 进程结束了,那么其会转变为「废弃进程」(defunct process),也被称作为“僵尸进程”(zombie process)。

    为什么会这样?因为 Unix 这样设计地目的,在于让父进程能够耐心“等待”子进程结束,从而获得其结束状态(exit status)。只有当父进程调用 waitpid() 之后,“僵尸进程”才会真正结束。手册里是这样描述地:

    一个已经终止但并未被“等待”的进程,就成为了一个“僵尸”。内核会记录这些“僵尸进程”的基本信息(PID、终止状态、资源占用信息),以确保其父进程在之后的时间里可以通过“等待”来获取这个子进程的信息。

    通常来说,人们会简单地认为“僵尸进程”就是那些会造成破坏的失控进程。但从 Unix 系统角度来分析,“僵尸进程”有着非常清晰地定义:进程已经终止,但尚未被其父进程“等待”。

    绝大多数情况下,这都不会产生什么问题。在一个子进程上调用 waitpid() 以消除其“僵尸”状态,被称为“收割”。多数应用程序都能够正确地“收割”其子进程。在上例中,操作系统会在 bash 进程终止时发送 SIGCHLD 信号以唤醒 sshd 进程,其在接收到信号后就“收割”掉了此子进程。

    但还有一种特殊情况——如果父进程终止了,无论是正常的(程序逻辑正常终止),还是用户操作导致的(比如用户杀死了该进程)——子进程会如何处理?它们不再拥有父进程,变成了「孤儿进程」(orphaned)(这是确切的技术术语)。

    此时初始进程(PID 1)就会因其被赋予地特殊任务而介入——「领养」(adopt)(同样的,这是确切的技术术语)「孤儿进程」。这就意味着初始进程会成为这些子进程的父进程,而无论其是否由初始进程创建。

    以 Nginx 为例,其默认就会作为后台程序运行。工作流程如下:Nginx 创建一个子进程后,自身进程结束,然后该子进程就被初始进程「领养」了。

    其中的要点是什么?操作系统内核自动处理了「领养」逻辑,因此内核其实是希望初始进程也自动完成对这些「孤儿进程」的“收割”逻辑

    这在 Unix 操作系统中是一个非常重要的机制,大量的软件都是因而设计和实现。几乎所有的服务(daemon)程序都预期初始进程会「领养」和“收割”其守护子进程

    尽管我们是以服务程序做例子,但系统并没有什么机制对此进行规约。任何一个进程在结束时,都会预期初始进程能够清理(「领养」和“收割”)其子进程。这一点,在《操作系统概述》《Unix 系统高级编程》两书中描述地非常详细。

     

    “僵尸进程”的危害

    “僵尸进程”都已经终止了,它们危害在哪里?它们原本占用的内存已经释放了吗?在 ps 中除了多了些条目,还有什么别的吗?

    是的,内存确实已经释放,但能够在 ps 中看到,说明它们还仍然占用着一些内核资源。对 Linux waitpid 的文档引用如下:

    在“僵尸进程”在被父进程“等待”以彻底消除之前,其仍然会被记录在内核进程表中。而当该表被写满后,新的进程将无法被创建。

    对 Docker 的影响

    这个问题会如何对 Docker 产生怎样的影响?我们可以看到很多人只在他们的容器中跑一个进程,而且也认为只需要跑这么一个进程就足够了。但显而易见地,这些进程无法承担初始进程在前文中所述的任务逻辑。因此,为了能够正确地“收割”被「领养」的进程,我们需要另外的初始进程来完成这些工作。

    举一个相对复杂地例子,我们的容器是一个 web 服务器,需要去跑一段基于 bash 的 CGI 脚本,而该脚本又会去调用 grep 程序。假定 web 服务器发现了 CGI 脚本执行超时,也中止了其继续执行。但此时 grep 程序并不会受到影响仍然继续执行,当其执行结束时,就变成了一个“僵尸进程”并由初始进程(即 web 服务器)「收养」。但 web 服务器无法正确地“收割”这个 grep 进程,所以该“僵尸进程”就在系统中常驻了。

    这个问题同样也存在于其它场景中。我们能看到人们尝尝为第三方程序创建 Docker 容器——又如 PostgreSQL ——并将其作为容器中的主进程运行。当我们运行别人的代码时,我们如何确保这些程序并不会派生出子进程并因而堆积大量的“僵尸进程”?唯独仅有我们运行着自己的代码,同时还对所有的依赖包和依赖包的依赖包做严格地审查,才能杜绝这种问题。因此,通常来说,我们很有必要来执行一个合适的初始化系统(init system)来避免这些问题地发生。

    完整的初始化系统让容器变得太重以至于像一台虚拟机?

    初始化系统与是否太重并无直接联系。我们可能会联想到 Upstart、Systemd、SysV init 之类的大型系统,也可能会联想到完整的系统启动流程。但一套“完整的初始化系统”对我们来说,既没必要,也不划算。

    我们所说的初始化系统,仅仅是一个能够启动应用程序、也能“收割”被「领养」地子进程的小程序。这样简单的初始化系统,才贴合 Docker 的哲学。

    一种简单的初始化系统

    哪些常用程序既能够运行其它的程序,又能够“收割”被「领养」地子进程呢?

    其实每个人都拥有一种几乎完美的解决方案——bash 。Bash 就可以很好地“收割”被「领养」地子进程,也能运行任何程序或代码。因此将我们的 Dockerfile 里的:

    CMD ["/path-to-your-app"]
    

    可以替换为:

    CMD ["/bin/bash", "-c", "set -e && /path-to-your-app"]
    

    -e 选项可以避免 bash 将脚本作为简单命令直接处理。)

    这样就能够产生我们所预期地进程层次:

    但这种方案仍然还有一个问题,那就是无法正确地处理信号。当我们通过 kill 来发送 SIGTERM 信号给 bash 时,进程终止了,但它不会发送 SIGTERM 给子进程。

    当 bash 终止时,其会发送 SIGKILL 信号来终止所有子进程(容器内的所有进程)。但因为 SIGKILL 是无法被捕获(trapped)地,所以没有办法干净地终止掉子进程。比如主程序在被终止时正在写入文件,那么该文件就会因此损坏。这就像直接拔掉了服务器的电源线一样残酷。

    我们为什么要关注初始进程是否被 SIGTERM 信号终止?因为 docker stop 就是发送 SIGTERM 信号给初始进程,而这个操作应当能够干净地停止容器以备之后的 docker start 。

    Bash 专家们会诱惑我们来写一段 EXIT 处理程序来简单地发送信号给子进程:

    #!/bin/bash
    function cleanup()
    {
        local pids=`jobs -p`
        if [[ "$pids" != "" ]]; then
            kill $pids >/dev/null 2>/dev/null
        fi
    }
    
    trap cleanup EXIT
    /path-to-your-app
    

    但这并不足以解决问题。仅仅发送信号给子进程是不够的,初始进程还需要“等待”子进程终止,然后才是自身终止。否则子进程地终止还是不干净的。

    因此,我们需要一套更合适的解决方案,但又不是像 Upstart、Systemd 和 SysV init 之类的重量级解决方案。再因此,笔者才在 Baseimage-docker 中专为 Docker 容器编写了一套轻量的初始化系统——my_init——一段 350 行的最小资源占用的 Python 程序。

    其主要功能在于:

    • “收割”被「领养」地子进程;
    • 启动子进程;
    • “等待”所有子进程终止后才自我终止,但也可以配置最大超时时间进行控制;
    • 将日志写入 docker logs

    Docker 会解决这个问题吗?

    相对来说,在 Docker 内部解决这一问题更为恰当。由其提供内置的初始化系统来“收割”被「领养」地子进程才最完美。但直至 2015 年一月,我们还看不到 Docker 团队在此问题上的作为。这不是批评——Docker 志向远大,而且笔者也确定 Docker 团队有更重要的事情要做,比如继续完善配置工具之类的。子进程的“僵尸化”问题在用户层面就很容易解决。因此在 Docker 官方解决这一问题之前,我们推荐大家使用一套合适的初始化系统自行搞定就好。

    这真是一个问题吗?

    直到此刻,这个问题仍然有点像是危险耸听。在我们从容器中看到“僵尸进程”之前,似乎一切都没有问题。但确保这一问题绝对不会发生的唯一方法,就是仔细审核程序代码、依赖包的代码和依赖包的依赖包的代码。在这些工作做完之前,就还是有可能会有代码派生出子进程,然后“僵尸化”。

    我们也许会想,我们从未见过问题发生,因此这应该只是小概率。但莫非定律说了,当事情可能变糟时,就一定会变糟。

    姑且不提“僵尸进程”会占用内核资源地问题,它们还会导致其它程序对进程的存在性检查出错。比如,笔者公司的 Phusion Passenger 应用服务程序会对进程进行管理,当它们崩溃时重启。而崩溃检查就是通过对 ps 命令的输出内容进行分析,而后向进程发送 0 信号来完成的。而“僵尸进程”既会出现在 ps 输出内容中,也能够正常地回应 0 信号,因而会被误认为是有效进程,导致实际服务挂起。

    再权衡一下,解决这个问题只需要 5 分钟即可,使用 Baseimage-docker 也好,在容器中使用笔者的 350 行 my_init 初始化系统也好。

    结语

    子进程的“僵尸化”问题是值得关注的。解决方案之一就是使用 Baseimage-docker 。

    Baseimage-docker 是唯一候选方案吗?显然不是。Baseimage-docker 致力于:

    1. 让人们意识到 Docker 容器的一些潜在问题和风险;
    2. 提供他人无需深入关注也能安全使用的预构建方案。

    这也意味着只要是解决了我们所描述地问题的方案,都是可行的。可以随意使用 C、Go、Ruby 或者什么别的语言来实现相应的方案。但我们既然已经有了现成的好方案,为什么还要再重新做轮子呢?

    或许我们使用地基础镜像并不是 Ubuntu,或者是 CentOS。但 Baseimage-docker 同样可以为我们所用。比如笔者公司的 passenger_rpm_automation 项目就基于 CentOS,直接将 Baseimage-docker 的 my_init 移植了过去。

    因此即便我们没有、也不想用 Baseimage-docker ,我们在前文中描述的问题仍然值得关注,也仍然值得仔细思考如何去解决。

     

     

    展开全文
  • fork函数详解与进程替换(exec)

    千次阅读 2018-08-10 21:43:06
    <1>fork定义 一个现有进程可以调用fork函数...我们经常说fork后的子进程相当于是子进程一个克隆,fork出来的父子进程并行fork之后的代码,但是子进程真的是完全复制了父进程吗?答案是不,那么到...

    <1>fork定义

    一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。

    我们经常说fork后的子进程相当于是子进程的一个克隆,fork出来的父子进程并行fork之后的代码,但是子进程真的是完全复制了父进程吗?答案是不,那么到底子进程复制了父进程的那些东西?那些东西又没有复制呢?

    <2>fork之后子进程到底复制了父进程什么?

    我们先来看看这段代码

    
    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<unistd.h>
    
    void main() 
    {
        char str[6]="hello";
        pid_t pid=fork();
    
        if(pid==0)
        {
    
            str[0]='f';
            printf("子进程中str=%s\n",str);
            printf("子进程中str指向的首地址:%x\n",(unsigned int)str);
        }
    
        else
        {
            sleep(1);
            printf("父进程中str=%s\n",str);
            printf("父进程中str指向的首地址:%x\n",(unsigned int)str);
        }
    }
    

    这个打印出来的结果是什么呢?我们一起来看看

    可以看出,父子进程之间打印的数据并不相同,说明子进程复制了父进程栈区的空间。但是为什么地址又是一样的呢?实际上这个是逻辑地址(虚拟地址),既然是逻辑地址(虚拟地址),那么又有什么所谓呢?映射到物理内存是不一样滴。地址映射-将程序地址空间中使用的逻辑地址变换成内存中的物理地址的过程。由内存管理单元(MMU)来完成。

    实际上fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用(下文会讲),出于效率考虑,linux中引入了“写时拷贝“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

    在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(因为两者的代码完全相同)。但如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。所以也就为什么不是直接在内存上给子进程也复制完完全全相同的区域呢,这就是原因。fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)相同没什么奇怪。

    具体过程是这样的:
    fork子进程完全复制父进程的虚拟地址空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。(简洁来说就是:内核只为新生成的子进程创建虚拟空间,它们来复制于父进程的虚拟空间,但是不为这些段分配物理空间,它们共享父进程的物理空间,当父子进程中有写内存的行为发生时,再为子进程相应的段分配物理空间(复制更改的变量所在的一页),这就是写时拷贝技术


    这就是所谓的“写时拷贝”技术。正因为fork采用了这种写时拷贝的机制,所以fork出来子进程之后,父子进程哪个先调度呢?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。。这些和父进程共享的空间,加载新的代码段。。。这就避免了“写时拷贝”共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时拷贝”的无用功。所以,一般是子进程先调度滴。

    假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。

    (注1:在理解时,你可以认为fork后,这两个相同的虚拟地址指向的是不同的物理地址,这样方便理解父子进程之间的独立性)
    (注2:但实际上,linux为了提高 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))

    <3>父子进程之间的资源共享

    Unix环境高级编程中8.3节中说,“子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段(当然这是进程不执行exec)。”

    对于父子进程之间的数据:.data段,.bss段 ,heap区,stack区的数据都是不共享的,但是.text段是共享的(因为代码一样鸭,没有进程替换的情况下。。。。)

    那么对于文件,父子进程共享吗?

    fork之前打开的文件描述符是共享的,fork之后的文件描述符是不共享的,为什么呢?

    实际上子进程也复制了父进程 的PCB,所以也将父进程中的文件描述符复制了,struct file是内核文件表,每个进程只要有它的地址,就可以找到,所以子进程便可以找到这个文件,对文件进行操作。子进程对文件操作也会影响父进程,实际上是操作的文件中的偏移量,共享了文件偏移量。但是在fork之后打开的文件,那就是各自进程打开各自的了,这当然是不共享的了。

    ps:你在子/父进程中close文件描述符,并不会影响另一个进程对这个文件操作,因为内核中实现的时候,要看struct file中的count引用计数,如果不为0,就不会删除文件,只是将你PCB中的指针赋为空而已,对其他操作该文件的进程没什么影响。

    实际上,fork后子进程和父进程共享的资源还包括:

    • 实际用户ID、实际组ID、有效用户ID、有效组ID
    • 添加组ID
    • 进程组ID
    • 会话期ID
    • 控制终端
    • 设置-用户-ID标志和设置-组-ID标志
    • 当前工作目录
    • 根目录
    • 文件方式创建屏蔽字
    • 信号屏蔽和排列
    • 对任一打开文件描述符的在执行时关闭标志
    • 环境
    • 连接的共享存储段(共享内存)
    • 资源限制

    父子进程之间的区别是:

    • fork的返回值
    • 进程ID
    • 不同的父进程ID
    • 子进程的tms_utime,tms_stime,tms-cutime以及tms_ustime设置为0
    • 父进程设置的锁,子进程不继承
    • 子进程的未决告警被清除
    • 子进程的未决信号集设置为空集

    <4>exec进程替换

    实际上,当进程调用一种exec函数时,该进程执行的程序完全替换为新的程序,而新程序则从其main函数开始执行。因为exec不创建新的进程,所以前后的进程ID(当然还有父进程号、进程组号、当前工作目录……)并未改变,exec只是用一个全新的程序替换了当前进程的正文,数据,堆和栈段。(注意区分程序和进程,可以将进程比作一个容器,程序就是里面装的物品)

    exec家族一共有六个函数,分别是:

    #include<unistd.h>
    
    (1)int execl(const char *path, const char *arg, ....../*(char *)0*/);
    
     
    
    (2)int execle(const char *path, const char *arg, .../*(char *)0*/ , char * const envp[]);
    
     
    
    (3)int execv(const char *path, char *const argv[]);
    
     
    
    (4)int execve(const char *path, char *const argv[], char *const envp[]);
    
     
    
    (5)int execvp(const char *file, char * const argv[]);
    
     
    
    (6)int execlp(const char *file, const char *arg, ....../*(char *)0*/);
    
          //6个函数返回值,出错返回-1,成功不返回值,因为调用方程序被替换,没有调用点接收返回的数
    

    其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

        exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

    与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

    exec 函数族的 6 个函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。

    l(list):参数地址列表,以空指针结尾。

    v(vector):存有各参数地址的指针数组(argv[]矢量)的地址。

    p(path):取filename作为参数,并按 PATH 环境变量指定的目录搜索可执行文件。

    e(environment):存有环境变量字符串地址的指针数组的地址,取envp[]数组,而不使用当前环境。

    exec 函数族装入并运行可执行程序 path/file,并将参数 arg0( arg1, arg2, argv[], envp[] ) 传递给此程序

     excel代码:

    
    #include <stdio.h>
    
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
    	printf("old program\n");
    
    
    	execl("/bin/ls", "ls", "-a", "-l", NULL);
          
            /* /bin/ls:外部程序,这里是/bin目录的 ls 可执行程序,必须带上路径(相对或绝对)
    
    	ls(第二个参数):没有意义,如果需要给这个外部程序传参,这里必须要写上字符串,至于字符串内容任意,只是为了对应所替换的程序的第一个参数agrv[0]
    
    	-a,-l:给外部程序 ls 传的参数(可以传多个参数)
    
    	NULL(或(char *)0):这个必须写上,代表给外部程序 ls 传参结束
    
    	*/
    
    
    	printf("execl failed\n");
           // 如果 execl() 执行成功,下面执行不到,因为当前进程已经被执行的 ls 替换了
    
    	
    
    	return 0;
    
    }
    

    运行结果:

    execv()示例代码:

    execv() 和 execl() 的用法基本是一样的,无非将列表传参,改为用指针数组。

    
    #include <stdio.h>
    
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    
    {
    
    	// execv() 和 execl() 的用法基本是一样的,无非将列表传参,改为用指针数组
    
    	// execl("/bin/ls", "ls", "-a", "-l", NULL);
    
    	
    
    	/* 指针数组
    
    	   ls(相当于execl中的第二个参数):没有意义,如果需要给这个外部程序传参,这里必须要写上字符串,至于字符串内容任意
    
    	   -a,-l:给外部程序 ls 传的参数
    
    	   NULL(或(char*)0):这个必须写上,代表给外部程序 ls 传参结束
    
    	*/
    
    	char *arg[]={"ls", "-a", "-l", NULL};
    
    
    
    	// /bin/ls:外部程序,这里是/bin目录的 ls 可执行程序,必须带上路径(相对或绝对)
    
    	// arg: 上面定义的指针数组地址
    
    	execv("/bin/ls", arg);
    	
    
    	printf("execv failed\n");
    	
    
    	return 0;
    
    }
    

    execlp() 或 execvp() 示例代码:

    execlp() 和 execl() 的区别在于,execlp() 指定的可执行程序可以不带路径名,如果不带路径名的话,会在环境变量 PATH指定的目录里寻找这个可执行程序,而 execl() 指定的可执行程序,必须带上路径名。

    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
           
            printf("old program\n");
    
    
    	execlp("ls", "ls", "-a", "-l", NULL);
            // 第一个参数 "ls",没有带路径名,在环境变量 PATH 里寻找这个可执行程序
    
    	// 其它参数用法和 execl() 一样
    
    	
    	/*
    
    	char *arg[]={"ls", "-a", "-l", NULL};
    
    	execvp("ls", arg);
    
    	*/
    
    	printf("execlp failed\n");
            //printf("execvp failed\n");
    
    	return 0;
    
    }
    

        

    execle() 或 execve() 示例代码:

    execle() 和 execve() 改变的是 exec 启动的程序的环境变量(只会改变进程的环境变量,不会影响系统的环境变量),其他四个函数启动的程序则使用默认系统环境变量。

    execle()示例代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h> // getenv()
    
    int main(int argc, char *argv[])
    {
    
    	
    
    	printf("before exec:USER=%s, HOME=%s\n", getenv("USER"), getenv("HOME"));
            // getenv() 获取指定环境变量的值
    
    
    	char *env[]={"USER=Hello", "HOME=/Example", NULL};    // 指针数据
    
    	
    
    	/* ./hello:外部程序,当前路径的 hello 程序,通过 gcc hello.c -o hello 编译
    
    		hello(第二个参数):这里没有意义,同上
    
    		NULL:给 hello 程序传参结束
    
    		env:改变 hello 程序的环境变量,正确来说,让 hello 程序只保留 env 的环境变量
    
    	 */
    
    	execle("./hello", "hello", NULL, env);
    
    	
    
    	/*
    
    	char *arg[]={"hello", NULL};		
    
    	execve("./hello", arg, env);	
    
    	*/
    	
    
    	printf("execle failed\n");
            //printf("execve failed\n");
    	
    
    	return 0;
    }
    

       外部程序,hello.c 示例代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
     
    int main(int argc, char *argv[])
    {
    
    	printf("\nin the hello fun, after exec:");
    
    	printf("USER=%s,HOME=%s\n", getenv("USER"),getenv("HOME"));
    
    	return 0;
    }
    

     

    <5>关于exec与打开文件的关系

    我们知道程序替换后,它的堆栈段,数据,代码都被替换了,你在你替换的程序中便不能使用原程序中的堆栈/数据/代码,那么对于在原程序中的文件描述符,新程序还能使用吗?我们来做一个测试吧。

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<string.h>
    #include<assert.h>
    #include<fcntl.h>
    //测试在原程序中打开的文件描述符,在替换后的程序中还能否使用
    int main(int argc,char *argv[])
    {
    	printf("old program\n");
    	int fd=open("a.txt",O_RDWR|O_CREAT,0664);
    	assert(fd!=-1);
    
    	char buff[128]="hello world\n";
    	write(fd,buff,strlen(buff));
    
    	char str[128]={0};
    	sprintf(str,"%d",fd);   //相当于atoi,将fd转化为字符串。
    	execl("./execl","execl",str,NULL);  //将字符串str当作参数传递给替换的程序execl
    	printf("execl failed\n");
    	return 0;
    }

    接着我们来看看所要替换的程序

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<string.h>
    #include<assert.h>
    
    int main(int argc,char *argv[])
    {
    	int fd;
    	sscanf(argv[1],"%d",&fd);   //再将字符串类型转化为整形,相当于itoa
    	lseek(fd,0,SEEK_SET);   //移动游标,否则就读不到数,也说明了其实替换的时候并没有清空进程的文件描述符还有偏移量。
    
    	while(1)
    	{
    		char buff[128]={0};
    		int n=read(fd,buff,127);
    		if(n<=0)
    			break;
    		printf("%s",buff);
    	}
    	close(fd);
    	return 0;
    }

    可以看出,在源文件中打开的文件描述符,如果源文件不关闭文件描述符在替换后的程序仍然是可以使用的。

    <6>浅谈exec与文件描述符关闭状态与close-on-exec

    我们知道fork之前打开的文件描述符,父子之间是共享的,但是fork之后的文件描述符并不是共享的。文件描述符是进程之间独立的,所以fork之后的文件描述符,即便数值相同,但是指向的文件表项是不同的,所以是不共享的。假设我们在子进程中打开一个文件,紧接着又调用exec(实际上与我上面写的单进程无两样),发现这个文件描述符依旧是可以用的。但是有的书上就写着子进程的文件描述符释放了。那么你就会疑惑了,哪为什么还可以使用呢?就我们上面所说的,他只是释放了文件描述符,但对于文件表项并没有释放(调用close才会释放),你使用这个整形文件描述符仍然可以访问到文件表项的。只回收了文件描述符,不回收文件表项就跟内存泄漏一个道理。

    一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了(当然我们可以像我上面写的测试代码一样,用参数传给替换的程序)。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。但是在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓的 close-on-exec。

    关于close-on-exec可以参照这个链接看看:https://blog.csdn.net/justmeloo/article/details/40184039

    展开全文
  • k8s通过yaml文件创建第一个pod容器 1.pod概述 Kubernetes 的基本调度单位,Pod 是一组紧密关联的容器集合,它们共享 PID、IPC、Network 和 UTS namespace。Pod 的设计理念是支持多个容器在一 Pod 中共享网络和...

    k8s通过yaml文件创建第一个pod容器-2.1

    1.pod概述

    Kubernetes 的基本调度单位,Pod 是一组紧密关联的容器集合,它们共享 PID、IPC、Network 和 UTS namespace。Pod 的设计理念是支持多个容器在一个 Pod 中共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。我们知道容器本质上就是进程,那么 Pod 实际上就是进程组了,只是这一组进程是作为一个整体来进行调度的。

    2.创建pod yaml文件

    2.1.创建pod yaml文件说明

    • 创建一个目录用来存放k8s的yaml文件
    mkdir -p k8s_yaml/pod
    cd k8s_yaml/pod/
    
    vim  nginx_pod.yaml
    
    apiVersion: v1  #定义k8s api的版本v1
    kind: Pod  #kind资源类型为 Pod
    metadata: #元数据声明
      name: nginx # 名称是nginx
      labels:  #标签申明
        app: web  #标签名称是 app:web
    spec:   #容器详细信息
      containers:  # 容器信息
        - name: nginx  #容器名称
          image: nginx:1.13  #容器镜像
          ports:  #容器访问端口
            - containerPort: 80
    

    2.2.创建pod

    • 在master节点上执行创建pod命令
    kubectl create -f  nginx_pod.yaml
    

    2.3.创建pod报错

    • 报错信息
      在这里插入图片描述
    • 解决方法
    # 删除apiserver配置文件中的ServiceAccount
    vim /etc/kubernetes/apiserver
    

    在这里插入图片描述

    • 重新启动apiserver
    systemctl restart kube-apiserver.service
    
    • 创建pod
    kubectl create -f  nginx_pod.yaml
    

    在这里插入图片描述

    2.2.查看已创建的pod

    # 查询所有pod
    kubectl get pod
    
    # 查询指定pod
    kubectl get pod podName
    

    2.3.pod没有运行起来解决方法

    在这里插入图片描述

    • 查看pod详细信息确定问题

    在k8s中排查错误最常用的方法就是查看容器的详细信息。

    kubectl describe pod nginx
    

    在这里插入图片描述
    在报错信息中了解到Scheduled调度服务选择了113Node节点上创建pod,在pull拉取镜像的时候失败了。

    • 在113Node节点上执行上面的地址拉取镜像,可以看到是失败的。
      在这里插入图片描述
    • 在docker官网仓库上获取
    docker search pod-infrastructure
    

    在这里插入图片描述

    • 修改node节点上kubelet下载地址

    在IP地址为113的node节点上编辑kubelet配置文件将默认的下载地址替换成上面我们查到的载地址。

    vim /etc/kubernetes/kubelet
    

    在这里插入图片描述

    • 重启kubelet服务
    systemctl restart kubelet.service
    
    • 在master上再次查看pod详细信息
    kubectl describe pod nginx
    
    • 下面的信息显示nginx拉取成功,pod已经启动。
      在这里插入图片描述

    3.搭建私有仓库

    上面pod启动问题已经解决了,可以在k8s上启动。但是还有个优化的地方没有去做。
    k8s通过scheduler调度服务选择一个node去创建pod,这次是在113Node节点上创建pod,那么当我们删除这pod后,下次可能就会在112Node上创建pod服务,那么还要修改112Node节点上kubelet配置文件下载地址。如果node节点很多,每个节点都要去docker官网下载镜像,如果网速不好这个时间会很长,还有可能下载失败。因此,我们需要配置一个私有仓库,在创建pod时候都去私有仓库去拉取镜像创建pod。

    3.1.运行私有仓库

    • 在master节点上下载registry仓库
    #查询registry仓库镜像地址
     docker search registry
    # 复制registry地址下载镜像
    docker pull docker.io/registry
    
    • 运行registry
     docker run -d -p 5000:5000 --restart=always --name registry -v /opt/myregistry:/var/lib/registry  registry
    

    3.2.上传镜像到私有仓库

    • 查看本地已下载的镜像
    docker images
    
    • 本地镜像上传到私有仓库
    # 将本地镜像打tag
    docker tag docker.io/tianyebj/pod-infrastructure:latest 172.16.8.111:5000/pod-infrastructure:latest
    
    #推送到私有仓库
    docker push 172.16.8.111:5000/pod-infrastructure:latest
    

    在这里插入图片描述

    3.3.修改kubelet和docker配置地址为私有仓库地址

    在所有的node节点上修改kubelet和docker配置文件的镜像地址为私有仓库地址。

    • 配置kubelet文件的私有仓库地址
    # 配置下载镜像地址为私有仓库
    vim /etc/kubernetes/kubelet
    
    # 重启kubelet
    systemctl restart kubelet.service
    

    在这里插入图片描述

    • 配置docker文件地址为私有仓库
    vim /etc/sysconfig/docker
    
    # Modify these options if you want to change the way the docker daemon runs
    
    # 下面registry的IP地址要换成你自己安装registry仓库的主机地址
    OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false --registry-mirror=https://registry.docker-cn.com --insecure-registry=172.16.8.111:5000'
    
    if [ -z "${DOCKER_CERT_PATH}" ]; then
        DOCKER_CERT_PATH=/etc/docker
    fi
    
    # Do not add registries in this file anymore. Use /etc/containers/registries.conf
    # instead. For more information reference the registries.conf(5) man page.
    
    • 重启docker服务
    systemctl restart docker
    

    3.4.pod使用本地私有库镜像创建容器

    当我们配置好自己的私有仓库后,就可以将下载下来的镜像全部打包上传到我们的私有仓库,以后在配置pod文件,容器拉取镜像指向我们私有仓库的镜像地址即可。不必在去docker官网仓库下载。可以节省很多多时间。

    • pod配置镜像地址为私有仓库镜像地址
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx2
      labels:
        app: web
    spec:
      containers:
        - name: nginx
    	# 创建容器镜像地址配置为我们的私有仓库镜像地址。前提是将该镜像上传到了我们的私有仓库才能使用,否则会报错找不到镜像。
          image: 172.16.8.111:5000/nginx:1.13
          ports:
            - containerPort: 80
    
    • 本地私有仓库镜像创建容器
    # 创建容器
    kubectl create -f nginx_pod2.yaml
    
    # 查看镜像下载地址
    kubectl describe pod nginx2
    

    在这里插入图片描述

    展开全文
  • 文章来源:极客时间「深入剖析Kubernetes」05节 白话容器基础 容器技术的火热, 必将为线上服务运维方式带来变革。中间件集群管理的目标是使中间件运维更轻、更快、更稳定、更简易, docker 容器技术便是我们实现...

    文章作者:张磊,Kubernetes项目维护者,极客时间「深入剖析Kubernetes」专栏作者

    文章来源:极客时间「深入剖析Kubernetes」第05节 白话容器基础

    容器技术的火热, 必将为线上服务运维方式带来变革。中间件集群管理的目标是使中间件运维更轻、更快、更稳定、更简易, docker 容器技术便是我们实现这一目标的有力工具。

     

    容器,到底是怎么一回事儿?

    容器其实是一种沙盒技术。顾名思义,沙盒就是能够像一个集装箱一样,把你的应用“装”起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰;而被装进集装箱的应用,也可以被方便地搬来搬去,这不就是PaaS最理想的状态嘛。

    不过,这两个能力说起来简单,但要用技术手段去实现它们,可能大多数人就无从下手了。

    所以,先来跟你说说这个“边界”的实现手段。

    假如,现在你要写一个计算加法的小程序,这个程序需要的输入来自于一个文件,计算完成后的结果则输出到另一个文件中。

    由于计算机只认识0和1,所以无论用哪种语言编写这段代码,最后都需要通过某种方式翻译成二进制文件,才能在计算机操作系统中运行起来。

    而为了能够让这些代码正常运行,我们往往还要给它提供数据,比如我们这个加法程序所需要的输入文件。这些数据加上代码本身的二进制文件,放在磁盘上,就是我们平常所说的一个“程序”,也叫代码的可执行镜像(executable image)。

    然后,我们就可以在计算机上运行这个“程序”了。

    首先,操作系统从“程序”中发现输入数据保存在一个文件中,所以这些数据就会被加载到内存中待命。同时,操作系统又读取到了计算加法的指令,这时,它就需要指示CPU完成加法操作。而CPU与内存协作进行加法计算,又会使用寄存器存放数值、内存堆栈保存执行的命令和变量。同时,计算机里还有被打开的文件,以及各种各样的I/O设备在不断地调用中修改自己的状态。

    就这样,一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和,就是我们今天的主角:进程。

    所以,对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现。

    容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。

     

    对于Docker等大多数Linux容器来说,Cgroups技术是用来制造约束的主要手段,而Namespace技术则是用来修改进程视图的主要方法。

    你可能会觉得Cgroups和Namespace这两个概念很抽象,别担心,接下来我们一起动手实践一下,你就很容易理解这两项技术了。

    假设你已经有了一个Linux操作系统上的Docker项目在运行,比如我的环境是Ubuntu 16.04和Docker CE 18.05。

    接下来,让我们首先创建一个容器来试试。

    $ docker run -it busybox /bin/sh
    / #

    这个命令是Docker项目最重要的一个操作,即大名鼎鼎的docker run。

    而-it参数告诉了Docker项目在启动容器后,需要给我们分配一个文本输入/输出环境,也就是TTY,跟容器的标准输入相关联,这样我们就可以和这个Docker容器进行交互了。而/bin/sh就是我们要在Docker容器里运行的程序。

    所以,上面这条指令翻译成人类的语言就是:请帮我启动一个容器,在容器里执行/bin/sh,并且给我分配一个命令行终端跟这个容器交互。

    这样,我的Ubuntu 16.04机器就变成了一个宿主机,而一个运行着/bin/sh的容器,就跑在了这个宿主机里面。

    上面的例子和原理,如果你已经玩过Docker,一定不会感到陌生。此时,如果我们在容器里执行一下ps指令,就会发现一些更有趣的事情:

    / # ps
    PID  USER   TIME COMMAND
      1 root   0:00 /bin/sh
      10 root   0:00 ps

    可以看到,我们在Docker里最开始执行的/bin/sh,就是这个容器内部的第1号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的/bin/sh,以及我们刚刚执行的ps,已经被Docker隔离在了一个跟宿主机完全不同的世界当中。

    这究竟是怎么做到的呢?

    本来,每当我们在宿主机上运行了一个/bin/sh程序,操作系统都会给它分配一个进程编号,比如PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。所以PID=100,可以粗略地理解为这个/bin/sh是我们公司里的第100号员工,而第1号员工就自然是比尔 · 盖茨这样统领全局的人物。

    而现在,我们要通过Docker把这个/bin/sh程序运行在一个容器当中。这时候,Docker就会在这个第100号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他99个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第1号员工。

    这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第100号进程。

    这种技术,就是Linux里面的Namespace机制。而Namespace的使用方式也非常有意思:它其实只是Linux创建新进程的一个可选参数。我们知道,在Linux系统中创建线程的系统调用是clone(),比如:

    int pid = clone(main_function, stack_size, SIGCHLD, NULL);

    这个系统调用就会为我们创建一个新的进程,并且返回它的进程号pid。

    而当我们用clone()系统调用创建一个新进程时,就可以在参数中指定CLONE_NEWPID参数,比如:

    int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

    这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的PID是1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的PID还是真实的数值,比如100。

    当然,我们还可以多次执行上面的clone()调用,这样就会创建多个PID Namespace,而每个Namespace里的应用进程,都会认为自己是当前容器里的第1号进程,它们既看不到宿主机里真正的进程空间,也看不到其他PID Namespace里的具体情况。

    除了我们刚刚用到的PID Namespace,Linux操作系统还提供了Mount、UTS、IPC、Network和User这些Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

    比如,Mount Namespace,用于让被隔离进程只看到当前Namespace里的挂载点信息;Network Namespace,用于让被隔离进程看到当前Namespace里的网络设备和配置。

    这,就是Linux容器最基本的实现原理了。

    所以,Docker容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组Namespace参数。这样,容器就只能“看”到当前Namespace所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

    所以说,容器其实是一种特殊的进程而已。

    谈到为“进程划分一个独立空间”的思想,相信你一定会联想到虚拟机。而且,你应该还看过一张虚拟机和容器的对比图。

      

        

    这幅图的左边,画出了虚拟机的工作原理。其中,名为Hypervisor的软件是虚拟机最主要的部分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如CPU、内存、I/O设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即Guest OS。

    这样,用户的应用进程就可以运行在这个虚拟的机器中,它能看到的自然也只有Guest OS的文件和目录,以及这个机器里的虚拟设备。这就是为什么虚拟机也能起到将不同的应用进程相互隔离的作用。

    而这幅图的右边,则用一个名为Docker Engine的软件替换了Hypervisor。这也是为什么,很多人会把Docker项目称为“轻量级”虚拟化技术的原因,实际上就是把虚拟机的概念套在了容器上。

     

    可是这样的说法,却并不严谨。

    在理解了Namespace的工作方式之后,你就会明白,跟真实存在的虚拟机不同,在使用Docker的时候,并没有一个真正的“Docker容器”运行在宿主机里面。Docker项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker为它们加上了各种各样的Namespace参数。

    这时,这些进程就会觉得自己是各自PID Namespace里的第1号进程,只能看到各自Mount Namespace里挂载的目录和文件,只能访问到各自Network Namespace里的网络设备,就仿佛运行在一个个“容器”里面,与世隔绝。

    不过,相信你此刻已经会心一笑:这些不过都是“障眼法”罢了。

    展开全文
  • 假如,现在你要写一个计算加法的小程序,这程序需要的输入来自于一个文件,计算完成后的结果则输出到另一个文件。 由于计算机只认识 0 和 1,所以无论用哪种语言编写这段代码,最后都需要通过某种方式翻译成二...
  • 容器技术第一讲:容器入门篇

    千次阅读 2019-03-19 18:00:00
    女主宣言容器作为Paas的种体现,越来越受到广大程序员的喜爱。本文作为容器的入门篇,对Docker的原理和主要技术进行了初步的介绍。PS:丰富的一线技术、多元化的表现形...
  • 白话容器基础之从进程说开去。 容器技术的兴起源于 PaaS 技术的普及; Docker 公司发布的 Docker 项目具有里程碑式的意义; Docker 项目通过“容器镜像”,解决了应用打包这根本性难题。 我希望你能理解这样...
  • 第一章:容器化技术 背景 2013~2014 年,以 Cloud Foundry 为代表的 PaaS 项目,逐渐完成了教育用户和开拓市场的艰巨任务,也正是在这将概念逐渐落地的过程,应用“打包”困难这问题,成了整个后端 技术圈子...
  • Linux容器是与系统其他部分隔离开的系列进程,从另一个系统镜像(文件系统)运行,并由该镜像提供支持进程所需的全部文件(环境文件)。 容器镜像包含了应用的所有依赖项,因而在从开发到测试再到生产的整个过程...
  • 因为Docker容器思想一个容器只做件事,所以JavaWeb整体会在一个容器中,数据库(Mysql,redis,Hbase)会单独存在另一个容器中,势必会造成容器之间的通信,由于Docker容器之间采用沙箱隔离运行机制,所以在不同...
  • 第一条:慎重地选择容器 C++提供了以下一些容器: n 标准STL序列容器:vector、string、deque和list。 n 标准STL关联容器:set、multiset、map和multimap。 n 非标准序列容器:slist和rope。slist是一单向链表...
  • 因为Docker容器思想一个容器只做件事,所以JavaWeb整体会在一个容器中,数据库(Mysql,redis,Hbase)会单独存在另一个容器中,势必会造成容器之间的通信,由于Docker容器之间采用沙箱隔离运行机制,所以在不同...
  • 这篇博客文章显示了如何调试运行在Docker容器中的简单Node.js应用程序。 本教程的布局方式允许您在构建自己的Node.js应用程序时将其用作参考,并且适合那些事先接触过JavaScript编程和Docker的读者。 先决条件 1...
  • Python中进程和线程的区别详解

    千次阅读 2019-03-04 09:30:46
     进程进程时计算机程序次执行的实例,由 程序段 数据段 PCB组成,是计算机资源分配和调度的基本单位,也是线程的容器  线程:线程也叫作轻量级进程,是程序执行的最小单位,他本身只拥有少部分执行必须的资源...
  • 第一:它是不可变的– 操作系统,库版本,配置,文件夹和应用都是一样的。您可以使用通过相同QA测试的镜像,使产品具有相同的表现。 第二:它是轻量级的– 容器的内存占用非常小。不需要几百几千MB,它只要对主进程...
  • 容器化的第一步就是制作镜像,不过,相较于之前介绍的制作 rootfs 的过程,Docker 为你提供了一种更便捷的方式,叫作 Dockerfile,如下所示。 # 使用官方提供的 Python 开发镜像作为基础镜像 FROM python:2.7-s...
  • Docker容器中常见的十种误区

    千次阅读 2016-08-03 12:08:44
    它是一个轻量级但十分强大的关于虚拟化技术的开源容器,在容器中还整合了构建并容器化应用的工作流程。目前大家已经开始认同并接受容器技术,并意识到它能够解决多种现实问题并具备系列无可比拟的优势。今天就和...
  • 而对于容器平台来说,容器是“第一公民”,他提供了容器注册与发现和负载均衡,同时容器技术将应用和外面的世界做了隔离,这样很多应用运行的假设就会失效。那当微服务应用运行在容器中的时候,我们会遇到哪些常见问...
  • Docker是一个开源项目,提供了一个打包、分发和运行任意程序的轻量级容器的开放平台。它没有语言支持、框架或者打包系统的限制,并可以运行在任何地方、任何时候,从小型的家用电脑到高端的服务器都可...
  • 本文旨在用三篇文章让读者能够清晰的认识与使用docker。本文结合理论与实战,先是从进程隔离、文件隔离、namespace、cgroups、libcontainer展开介绍容器的本质...容器本质上是进程隔离的技术。容器进程提供了...
  • 六章 Java容器

    千次阅读 2019-09-10 21:05:00
    文章目录谈谈你常用的集合 &...谈谈Java集合那些线程安全的集合 & 实现原理?请描述一下ConcurrentHashMap?请描述一下HashMap & 实现原理?比较一下ArrayMap & SparseArray & Ha...
  • 在每一个Kubernetes节点,运行着kubelet,负责为Pod创建销毁容器,kubelet预定义了API接口,通过GRPC从指定的位置调用特定的API进行相关操作。而这些CRI的实现者,如cri-o, containerd等,通过调用runc创建出容器...
  • 为什么容器里只能跑“一个进程”? 为什么我原先一直在用的某个 JVM 参数,在容器里就不好使了? 为什么 Kubernetes 就不能固定 IP 地址?容器网络连不通又该如何去 Debug? Kubernetes StatefulSet 和 ...
  • 项目1Docker容器简介任务1.1 了解Docker历史1.1.1 什么是 DockerDocker 最初是dotCloud 公司创始人 Solomon Hykes在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的次革新,并于2013 年 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 37,130
精华内容 14,852
关键字:

如何把容器中第一个进程替换