)
但即使进程2、3、4没有请求I/O或阻塞它也可能不会运行到完成。如果我们简单地让任何作业获取CPU并一直保持直到完成那么它将阻塞所有其他作业。因此调度器在一段时间后会抢占该作业并将其放回运行队列给下一个作业运行的机会。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_5.png现在进程8723在CPU上运行。进程1、2、3、4可能最终完成了它之前阻塞的I/O操作因此它重新进入运行队列。但现在它排在队列末尾必须等到它前面的所有其他作业都获得它们的CPU时间片后才能再次获得工作机会。这种简单的轮询方法还假设所有作业具有相同的重要性并且应该始终获得相同数量的CPU时间。但在现实中我们有时希望某些作业比其他作业更受青睐。某些对操作系统平稳运行至关重要的系统作业或内核任务可能不应该被普通用户启动的不重要的长时间运行进程所抢占。优先级调度让我们再试一次。这次除了运行队列我们还让每个进程拥有一个优先级。如果所有作业具有相同的优先级那么调度器可以像之前一样从队列中选取第一个作业。但假设不同的作业有不同的优先级。在这种情况下调度器将从运行队列中选取优先级最高的作业并将其放到CPU上。例如进程8、7、2、3的优先级为15进程8、8、3、3是下一个。请注意当我们将进程8723放到CPU上时我们正在增加所有等待进程的优先级。这确保了任何等待很长时间的进程最终会获得足够高的优先级被选中。现在如果进程8、7、2、3被阻塞例如等待I/O那么它被放入等待队列同时下一个作业进程8833获得CPU并且运行队列中的其他作业的优先级再次增加。一段时间后进程8833被抢占但请注意当它被放回运行队列时它的优先级会略微降低因为它刚刚使用了CPU。然后进程1、2、3、4获得CPU。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_7.png如果进程8、7、2、3现在完成了它的I/O操作它将被放回运行队列但由于它从未在CPU上获得完整的运行周期它的优先级一直保持不变。这样被I/O阻塞的作业现在拥有最高的优先级。当进程1、2、3、4被抢占时它就获得了CPU。这是一个非常简化的动态优先级调度视图但我想你已经理解了通过允许优先级我们可以确保作业根据其需求进行调度并且通过基于CPU使用周期动态调整优先级我们可以避免即使是低优先级作业的“饥饿”现象。当然还有许多其他调度算法但对我们来说这个近似模型已经足够好现在可以看看如何调整进程优先级。调整进程优先级为此我们有getpriority和setpriority系统调用。默认情况下每个进程的优先级为0中性。which参数指定你感兴趣的是进程优先级、进程组优先级还是所有用户进程的优先级。对于多个进程getpriority将返回这些进程中最高的优先级。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_9.png默认优先级0是中性值。数值上更低的优先级值会导致更有利的调度更高的值意味着更不利的调度。这有点令人困惑因为通常我们认为更高的优先级会导致更有利的调度。这就是为什么我们要区分实际的内核优先级和进程的友好度。正如我们稍后将看到的我们可以使用nice工具调整进程的优先级这里的逻辑映射就更有意义了一个进程越“友好”它就越愿意让其他进程使用CPU。 所以更高的数字意味着你更友好因此获得更低的内核调度优先级。你可以设置的值范围从-20到20尽管在某些UNIX版本上你最多只能“友好”到19级。值为19或20的进程只有在没有优先级小于或等于0的进程运行时才会被调度。类似于我们处理ulimit的方式我们的进程只能提高其友好度即降低优先级除非你是超级用户否则永远不能降低它即提高优先级。我们稍后会看到这方面的例子。最后请注意由于我们的友好度值范围是-20到20而-1是getpriority的有效返回值这意味着我们不能仅仅依赖返回值来识别错误。因此我们需要在调用getpriority之前显式设置errno然后在调用之后检查errno。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_11.png让我们看看所有这些在实际中是如何运作的。观察系统负载当我们运行uptime命令时不仅会看到主机运行了多长时间还会看到负载平均值即三个数字。这三个数字分别显示过去1分钟、5分钟和15分钟的平均负载。平均负载定义为在该时间间隔内运行队列中进程数量的平均值。以下是一个简单的程序仅打印平均负载#includestdio.h#includestdlib.hintmain(void){doubleavgs[3];if(getloadavg(avgs,3)0){perror(getloadavg);exit(1);}printf(%.2f %.2f %.2f\n,avgs[0],avgs[1],avgs[2]);return0;}我们的程序打印的三个数字大致与uptime报告的数字匹配因为平均值是实时计算的。现在让我们尝试生成一些繁忙的工作。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_13.png创建繁忙作业这是一个会让我们的CPU忙碌一会儿的简单脚本。它只是不断地循环进行减法运算。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_15.png#!/bin/shi0while[$i-lt100000];doi$((i1))j$((100000-i))k$((j-i))donehttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_17.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_19.png如果我们运行这个脚本然后在另一个窗口查看top命令的输出我们会看到我们的繁忙作业例如进程ID 1361显示在那里内核优先级为28友好度为0。当我们的可运行作业减少时我们会发现这个脚本完成了。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_21.png现在让我们并行运行几个脚本实例以观察我们的CPU优先级是如何分配的。我连续运行了四个实例中间稍作等待。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_23.png我们期望这四个作业按照它们启动的顺序完成A、B、C、D。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_25.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_27.png在另一个窗口中我们应该看到这些作业一个接一个地出现每个都有相同的友好度级别并且大致相同的内核优先级随着它们获得CPU和被抢占每次更新时都会调整。随着它们完成我们将看到预期的顺序首先是A然后是B接着是C最后是D。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_29.png现在让我们尝试在启动作业后调整它们的优先级。使用nice和renice调整优先级我们可以使用nice命令指定初始优先级或者使用renice命令调整已运行进程的优先级。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_31.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_32.png让我们使用renice。但为此我们需要作业的进程ID。所以让我们在启动它们时打印出来。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_34.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_36.png现在我们有了作业A进程ID 2197、B1869等等。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_38.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_39.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_40.png让我们调整优先级给进程A最高的友好度级别即最低优先级给进程D友好度级别15给进程B友好度级别5让进程C保持默认。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_42.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_44.png现在我们可以再次在这里观察我们的作业。我们看到友好度级别在这里反映出来当前优先级也相应调整。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_46.png随着作业完成我们应该发现另一个终止顺序。现在C首先完成然后是B接着是DA最后完成。这反映了我们分配给每个进程的友好度级别表明优先级更高的作业确实获得了更多的CPU时间因此能够更快完成。现在让我们快速看一下nice如何设置优先级。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_48.png编程接口示例这里有一个程序它打印其当前优先级然后尝试将其优先级设置为命令行提供的值接着打印出新优先级然后尝试将优先级设置回程序启动时的值最后再次报告其当前优先级并退出。#includestdio.h#includestdlib.h#includesys/resource.h#includeerrno.hintmain(intargc,char*argv[]){intnew_nice,old_prio,new_prio;id_tpid;if(argc!2){fprintf(stderr,Usage: %s nice_value\n,argv[0]);exit(1);}new_niceatoi(argv[1]);pidgetpid();// 获取当前优先级errno0;old_priogetpriority(PRIO_PROCESS,pid);if(errno!0){perror(getpriority);exit(1);}printf(Initial priority: %d\n,old_prio);// 尝试设置新优先级if(setpriority(PRIO_PROCESS,pid,new_nice)0){perror(setpriority to new value);}// 获取新优先级errno0;new_priogetpriority(PRIO_PROCESS,pid);if(errno!0){perror(getpriority after set);exit(1);}printf(Priority after first set: %d\n,new_prio);// 尝试设置回旧优先级if(setpriority(PRIO_PROCESS,pid,old_prio)0){perror(setpriority back to old value);}// 获取最终优先级errno0;new_priogetpriority(PRIO_PROCESS,pid);if(errno!0){perror(getpriority final);exit(1);}printf(Final priority: %d\n,new_prio);return0;}让我们运行它并请求友好度级别5。我们以默认友好度级别0开始。我们能够设置新的友好度级别因为我们在提高友好度即降低优先级。之后我们尝试将优先级设置回0但这失败了。因此我们的优先级在整个过程中保持为5。如果我们以超级用户权限运行相同的命令那么我们就能够再次降低我们的友好度即提高优先级从5回到初始的0。如果我们一开始就非常“友好”会怎样让我们试试。我们的初始级别现在是10因此尝试将其设置为5会失败因为那是在降低我们的友好度即提高优先级。但我们当然仍然可以变得更友好进一步降低我们的优先级。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_50.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_52.png我们能一开始就“不友好”吗我们的默认值是0。让我们尝试-5。这失败了。我们不能将友好度级别降低到0以下。但请注意nice命令仍然让我们以默认优先级运行程序而超级用户当然可以以较低的友好度值启动然后随意调整。总结本节课中我们一起学习了进程优先级的管理。如前所述进程能够自愿自我限制资源使用。这既适用于ulimit资源限制也适用于CPU调度优先级。我们可以使用setpriority系统调用或使用命令行工具nice和renice来调整我们的CPU优先级即友好度。这可以针对单个进程或进程组进行。与人类世界不同一旦你变得“友好”你就不能再决定变得“淘气”。这遵循了一个原则我们可以降低自己的权限但一旦降低就不能再提高。目的是允许进程有意放弃某些权限以便它或其子进程在以后不能滥用这些权限。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d692eeebd2915e618af745931c87fafe_54.png最后虽然我们可以通过调整友好度在一定程度上控制获得CPU周期的频率但这对于我们在拥有多个CPU的系统上被调度到哪个CPU没有直接影响。可以理解在多CPU系统上你可能希望保留一个CPU用于系统进程而将所有其他作业分配给其他CPU。我们将在下一个视频中讨论如何实现这一点。070处理器亲和性与CPU集 概述在本节课中我们将学习如何将进程限制在特定的CPU上运行。上一节我们讨论了进程的CPU时间限制本节中我们来看看如何控制进程在哪个CPU核心上执行即处理器亲和性与CPU集。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/6bed22758f0c8eecde1a3bfad745d2c4_1.pngCPU调度与进程放置假设我们有一个包含四个CPU的系统上面运行着许多进程。这些进程包括从shell交互运行的命令、一些系统守护进程以及一些执行CPU密集型工作的通用作业。在通常的基于时间片和优先级的调度算法下这些进程可以被放置在任何可用的CPU上。随着工作的完成、作业被抢占和重新调度它们可能会从一个CPU移动到另一个CPU或者根据调度器的判断新作业被放置到CPU上。处理器亲和性CPU Pinning现在假设我们的工作作业都是CPU密集型的。如果它们被放置在任何CPU上可能会导致系统负载过高。根据它们的优先级一些系统守护进程可能无法像你希望的那样快速完成。因此我们尝试选择这些工作作业并确保它们不被放置在任何CPU上而只放置在CPU 1和CPU 2上。这种做法称为CPU固定或分配处理器亲和性。当我们这样做时工作进程被正确地放置到这些CPU上。但请注意我们仍然有其他作业在CPU 1和CPU 2上运行shell和find命令并没有从CPU上被驱逐。实际上新的进程仍然可以根据需要被放置到CPU 1和CPU 2上。只有那些被绑定到指定CPU的工作进程受到限制其他所有进程仍然可以按照调度器认为合适的方式放置。实践演示以下是一个让CPU保持忙碌的简单程序// 示例CPU密集型程序intmain(){while(1){// 空循环消耗CPU周期}return0;}在运行它之前我们在屏幕下半部分运行top命令。在这个系统上我们有四个可用的CPU可以看到一些系统进程。当我们启动工作进程时我们注意到它最初在CPU 0上启动然后被放置到CPU 2上并在那里持续运行和消耗周期。我们将其放入后台运行注意到它现在被移动到了CPU 3。然后我们启动第二个工作进程到后台它最终在CPU 2上运行。我们再启动一个又启动一个。现在我们有了四个工作进程调度器将它们分布到所有四个CPU上。如果我们再运行另一个作业例如dd它必须与其他进程共享CPU并最终在CPU 2上运行。我们的工作作业完全在用户空间执行但dd作业在执行I/O时也会在内核空间执行这可以在top的显示更新中看到。我们再运行另一个作业例如wc程序我们观察到wc程序在后续调用中被放置在不同的CPU上而我们的工作进程仍然占用着所有四个CPU。使用taskset命令分配处理器亲和性我们可以使用taskset控制命令来分配处理器亲和性。除了亲和性我们还可以调整调度算法和优先级。首先让我们查看当前shell的处理器亲和性taskset-p$$现在我们没有任何亲和性设置这意味着调度器可以自由地将这个shell放置在任何它喜欢的CPU上。现在运行一个工作作业它最终在CPU 2上。我们尝试将其移动到CPU 3sudotaskset-pc3PID更改CPU亲和性需要超级用户权限这是合理的。我们不希望允许普通用户随意移动他们的作业从而可能干扰其他进程。现在我们已经移动了进程我们的工作进程对CPU 3有了亲和性。我们可以在下面的top显示中看到它从CPU 2移动到了CPU 3。我们可以将其移回CPU 2、CPU 1或CPU 0并在top显示中看到更新。我们可以选择允许用户更改CPU亲和性。为此我们必须更改用户的set CPU affinity能力。现在我们可以将当前shell从没有亲和性移动到CPU 2。由于CPU亲和性是由子进程从其父进程继承的当我们在这里启动一个新的工作进程时它也会在CPU 2上运行。因此即使我们运行多个工作作业它们都将保持绑定到CPU 2而其他CPU保持空闲。请注意虽然我们将工作作业绑定到了CPU 2我们仍然可以将其他作业移动到该CPU上。例如移动top进程本身。同样即使子进程继承了其父进程的亲和性dd作为我们shell的子进程对CPU 2有亲和性也会被放置在该CPU上。我们仍然可以显式地将其移开同样也可以将工作作业移动到不同的CPU。CPU集CPU Sets我们已经看到了如何通过分配处理器亲和性将单个作业移动到特定的CPU上。但正如我们也看到的这仍然允许其他进程被放置到这些CPU上可能与我们的作业竞争。我们能否为一个或多个CPU保留特定的任务使得没有其他作业可以在它们上面运行假设我们想使用我们的四个CPU并保留其中两个给我们的工作作业一个给我们的shell。我们可以使用CPU集来实现这一点。当你创建CPU集时你总是会保留一个默认集用于任何剩余的进程。在我们的示例中所有的系统进程将最终在CPU 0上。我们可以显式地将我们的shell绑定到CPU 3这意味着随后由shell启动的任何进程也将被绑定到该CPU集。如果我们然后将我们的工作作业绑定到CPU 1和CPU 2那么我们将看到这样的分布。请注意我们可以从shell启动其他守护进程它们将继续仅被放置在CPU 3上。而任何未显式绑定到CPU集的作业的调度将最终在CPU 0上但除了我们的工作作业外没有其他作业会被分配到CPU 1和CPU 2。实践示例我们稍微扩展了我们的小忙碌程序以便更容易区分工作作业。现在我们的程序接受一个参数指定要启动多少个作业然后使用易于区分的argv[0]运行它们。像之前一样我们在屏幕下半部分运行top。我们启动六个工作作业它们按预期分布在所有四个CPU上。但正如我们所说我们想使用CPU集。cpuset命令显示当前的CPU集一个默认集包含所有四个CPU。现在让我们从我们的图示中复制一个设置。像之前一样创建CPU集不是普通用户允许的操作所以我们需要sudo权限。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/6bed22758f0c8eecde1a3bfad745d2c4_3.png现在我们有了三个CPU集默认CPU集仅包含CPU 0、CPU集1包含CPU 1和CPU 2以及CPU集2包含CPU 3。现在我们运行我们的六个工作作业它们都最终在默认CPU集的CPU 0上。这是因为任何未显式绑定到CPU集的作业只能被放置在默认CPU集中。这不是我们想要的所以我们将它们放置在CPU集1上。现在我们看到它们仅按计划分布在CPU 1和CPU 2上。但请注意尽管将工作进程绑定到给定的CPU集我们仍然可以显式地将其移动到默认CPU集中的CPU上。但是如果我们尝试将top进程从默认CPU集CPU 0移动到CPU 2我们会失败。CPU 2是非默认CPU集的一部分因此不允许任何未通过其他cpuset命令显式绑定到它的作业。对于CPU 3也是如此它是非默认CPU集2的一部分。我们甚至无法将我们从CPU集中移除的进程移回。为了使CPU和CPU集可用于任何其他作业我们必须再次删除CPU集此时所有作业可以再次被调度到任何CPU上。总结本节课中我们一起学习了如何将作业限制在特定的CPU上。我们注意到出于性能原因限制作业到特定的CPU是有益的在同一CPU上调度进程和线程可以减少CPU缓存未命中的次数或者确保资源被公平使用或者防止一组进程干扰其他作业。有两种方法可以实现这一点。第一种是CPU固定我们为进程或进程组分配CPU亲和性。虽然指定的进程将被绑定到指定的CPU但其他作业仍然可以被放置到同一个CPU上。正如我们所看到的子进程将从其父进程继承CPU亲和性但更改父进程的CPU亲和性不会同时更改其所有子进程的亲和性。与处理器亲和性相比CPU集允许你真正为一个或多个CPU保留特定的作业。调度器将无法将任何作业移动到这些CPU上除了那些显式绑定到它们的作业。像之前一样子进程从其父进程继承CPU放置你可以通过更改其亲和性显式地将进程从CPU集中移除。但要将其移回CPU集你需要显式调用cpuset命令。最后这些都不是标准化的。不同的操作系统以不同的方式实现CPU固定和CPU集并使用不同的命令行工具和库函数因此请务必检查你特定操作系统的参考手册。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/6bed22758f0c8eecde1a3bfad745d2c4_5.png回顾所有我们可以限制进程的不同方式我们现在应该能够将它们结合起来构建非常特定的环境。但在我们这样做之前我们还需要涵盖最后一个主题控制组、命名空间和能力。我们将在下一个视频中讨论这些内容。UNIX高级编程13.6能力、控制组与容器 在本节课中我们将完成对进程限制方法的讨论重点介绍POSIX能力、Linux控制组cgroups以及这些技术与我们之前讨论的其他方法如何共同构成了容器技术如Docker或LXC的基础。我们已经了解了限制CPU使用率、文件系统视图、内存和进程表访问的方法本节将进一步探讨如何通过更精细的控制来更好地隔离和管理进程组。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d0beaaa3c0e28bc3a633e72ccba2964b_1.png概述从能力到容器上一节我们探讨了资源视图的限制。本节中我们来看看如何通过定义进程的“能力”来实施更通用的权限控制并介绍用于资源隔离的Linux命名空间和控制组最终理解这些技术如何共同构建出轻量级的容器。POSIX能力模型一种定义通用需求的方法是使用POSIX能力模型。在这个模型中我们不是针对特定问题如受限shell或chroot提供具体解决方案而是识别进程所需的通用“能力”并授予对这些能力的细粒度访问控制。例如可以定义以下能力CAP_CHOWN: 更改文件所有者的能力。CAP_SETUID: 允许设置用户IDsetuid。CAP_LINUX_IMMUTABLE: 允许设置文件的不可变标志如chattr i。CAP_NET_BIND_SERVICE: 允许将网络套接字绑定到1024以下的端口。CAP_NET_ADMIN: 允许进行网络接口配置和路由表操作。CAP_NET_RAW: 允许使用原始数据包如ping。CAP_SYS_ADMIN: 一个更广泛的系统管理能力集合提供诸如挂载文件系统、设置主机名、管理交换空间等特权。不同操作系统对此标准的解释和实现方式各异。例如FreeBSD通过Capsicum框架实现能力模型而Linux系统的具体实现细节可以在capabilities(7)手册页中查阅。Linux命名空间另一种划分系统、限制进程和进程组对资源可见性的方法是Linux命名空间其灵感来源于贝尔实验室的Plan 9操作系统。使用命名空间一个进程组可以以不同于其他进程组的方式或者完全看不到某些系统组件。这些资源可以存在于多个命名空间中从而在划分系统、仅向特定进程组暴露必要资源方面提供了高度的灵活性。以下是可通过命名空间进行虚拟化的资源类型挂载点独立的文件系统视图。进程ID独立的进程ID空间仅能看到本命名空间内的进程。网络虚拟化的网络栈每个命名空间拥有自己的IP地址、路由表、防火墙规则等。System V IPC独立的信号量、共享内存和消息队列内核结构视图。UTS独立的系统主机名和域名。用户独立的用户ID映射例如可以将命名空间内的root用户映射到宿主系统的一个非特权用户。时间允许不同进程使用不同的系统时间。控制组控制组最初称为“进程容器”用于隔离我们之前讨论过的各种资源使用。以下是cgroups允许进行控制的主要方面内存限制。CPU使用率、优先级和限制。资源统计跟踪哪些进程以何种方式使用了哪些资源。进程控制允许暂停、中断、对进程进行检查点保存和恢复。cgroups经历了至少一次重新设计版本2目前支持以下控制器任务调度能力。CPU和内存使用率。cgroup自身的活动控制例如冻结组中的任务将不会被调度。大页支持和使用。块设备I/O。内存、内核内存和交换内存。线程监控。可用进程数量的限制。远程目录内存访问。cgroups通过虚拟文件系统实现通常挂载在/sys/fs/cgroup并允许通过挂载选项启用不同的控制器。以下是一个限制当前shell内存使用量的简单示例代码# 创建一个新的cgroup在cgroup v2中通常是在/sys/fs/cgroup下创建子目录sudomkdir/sys/fs/cgroup/memory/my_group# 将当前shell的进程ID写入cgroup.procs文件echo$$|sudotee/sys/fs/cgroup/memory/my_group/cgroup.procs# 设置内存限制例如限制为100MBecho“100M”|sudotee/sys/fs/cgroup/memory/my_group/memory.maxcgroups(7)手册页提供了非常详细和广泛的信息建议你花时间阅读。容器技术的集大成者https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d0beaaa3c0e28bc3a633e72ccba2964b_3.png最后容器顾名思义是一种“容纳”进程的方式。它们在同一操作系统内核上提供了一个隔离的执行环境这是与完全硬件虚拟化的重要区别因此是一种更轻量级的方法。为了“容纳”进程你可能会组合使用以下技术使用chroot或联合挂载来提供正确的文件系统视图。使用资源限制来避免进程干扰父系统或其他进程。使用命名空间来限制文件系统、进程、网络等的可见性。使用能力模型来限制进程可以执行的操作。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d0beaaa3c0e28bc3a633e72ccba2964b_5.png尽管我们在此上下文中讨论了许多应用此类限制的方法但cgroups和命名空间经常被一起讨论因为它们能很好地互补。事实上cgroups和命名空间的结合构成了许多操作系统级虚拟化和容器技术如CoreOS、LXC或Docker的基础。考虑我们从学期初就使用的操作系统基本分层模型底层是硬件。内核管理硬件。系统调用是进入内核的接口。一系列库函数允许应用程序在操作系统内执行。在传统模型中内核拥有对硬件的广泛访问权进程也大体上保留对硬件的相同视图。每个进程的文件系统、进程空间和网络能力是相同的尽管我们可以限制每个进程能直接操作的内容。在完全硬件虚拟化中情况有所不同。我们在硬件和内核之上增加了一个虚拟机监控程序它虚拟化硬件并将其提供给每个虚拟机。在虚拟机内部每个客户操作系统只能看到虚拟机监控程序提供的资源。而在轻量级操作系统级虚拟化中我们从一个熟悉的基本视图开始但可以应用本系列视频中的各种技术来创建受限环境。例如结合使用受限shell、特定挂载选项、固定的CPU优先级和一些文件属性来构建受限的文件系统视图并限制进程执行能力。结合使用chrootjail、CPU集和能力来限制特定进程的进程、文件系统和网络视图。创建由精心调优的命名空间、cgroups和资源限制组成的针对每个进程或进程组的受限环境。容器本质上就是运行在我们通用UNIX操作系统上的进程但我们对其进行了限制使得它们只能看到或访问我们允许的资源视图。然而请注意与完全硬件虚拟化不同容器仍然是进程或进程组。也就是说它们仍然运行在与宿主机或父进程相同的操作系统内核上。这既是优势虚拟环境的初始化比启动虚拟机快得多也是限制你只能运行与宿主机相同操作系统的容器而不能运行另一个操作系统。总结与思考本节课中我们一起学习了限制进程的各种方法以及这些技术如何最终催生了流行的容器技术。还有许多其他相关方法我们只是浅尝辄止但希望你已经看到这其中并无魔法。本学期到目前为止所涵盖的所有内容都应该能让你更好地理解Docker及其同类技术。或许从这里可以得出的最重要经验是大多数进程限制都可以通过某种方式被绕过。目标是自愿地限制自己使得即使系统被攻破攻击者也无法获得你之前可能拥有的攻击能力或提升的权限。理解Unix进程和基本语义对于设置和配置此类受限环境至关重要。从我们的第一堂课到现在我们已经走了很长一段路。或许现在可以带着对这些概念的理解回头重新审视一些主题。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/d0beaaa3c0e28bc3a633e72ccba2964b_7.png感谢观看下次再见072Union Mounts与Whiteout文件 https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_1.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_3.png在本节课中我们将要学习UNIX文件系统中的两个高级概念联合挂载Union Mounts和白化文件Whiteout Files。我们将从struct stat中未提及的一种特殊文件类型开始逐步解释其作用原理并通过实际操作演示其行为。概述在之前的课程中我们详细讨论了struct stat结构体它提供了文件的元信息包括文件权限和类型。我们提到了常规文件、目录、套接字和符号链接等类型但有一种特殊文件类型尚未涉及即白化文件。本节课将解释白化文件的作用及其产生的背景——联合挂载。联合挂载Union Mounts简介上一节我们介绍了文件类型的基本概念本节中我们来看看联合挂载。联合挂载允许你将两个目录的内容合并使其在单一位置可见。这通过将一个目录挂载在另一个目录之上来实现从而展示两个目录的联合内容。以下是联合挂载的基本操作步骤准备两个目录例如directory1和directory2。使用mount_union命令将directory2挂载到directory1之上。访问挂载点你将看到两个目录内容的合并视图。白化文件Whiteout Files的作用当在联合挂载的顶层目录中删除一个文件而该文件在底层目录中仍然存在时就会出现一个问题删除操作不应让底层文件意外显现。为了解决这个问题联合文件系统会动态生成一个白化文件。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_5.png白化文件的核心作用是在联合挂载的视图内隐藏底层文件中仍然存在的文件。它本身不是一个真实的文件而是一个占位符。当卸载联合文件系统后白化文件会自动消失。https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_7.png实践演示联合挂载与白化文件https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_9.pnghttps://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_11.png现在让我们通过一个实际例子来观察联合挂载和白化文件的行为。实验准备我们创建两个目录lower底层和upper顶层。它们的初始内容如下https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_13.pngupper目录内容file1 (内容: This is file1 in the upper directory) file2 subdir/file3 (内容: This is file3 in the upper directory) upper_file upper_dir/lower目录内容file1 (内容: This is file1 in the lower directory) file2 subdir/file3 (内容: This is file3 in the lower directory) lower_file lower_dir/创建联合挂载我们执行以下命令创建联合挂载mount_union upper lowercdlower# 此时‘lower’目录是联合视图的挂载点在联合视图即lower目录中执行ls -l你会看到upper和lower目录内容的合并。对于同名文件如file1、subdir/file3显示的是顶层upper版本。观察白化文件的产生在联合视图中删除file1rmfile1此时使用ls -l看不到file1。但使用ls -lW命令-W选项用于显示白化文件你会看到一个特殊的条目W--------- 1 user wheel 0 Jan 1 00:00 file1这个类型为W、链接数和大小均为0的文件就是白化文件。它掩盖了底层目录中仍然存在的file1。卸载与状态还原卸载联合文件系统umountlower检查原始目录lower/file1依然存在且内容未变。upper/file1已被删除。白化文件已随联合挂载的卸载而消失。其他行为影子目录Shadow Directories如果在联合视图中访问一个仅存在于底层目录的子目录例如lower_dir系统会在顶层目录自动创建一个对应的影子目录。这个目录在顶层是空的但确保了路径结构在联合视图中的一致性。核心概念总结https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_15.png本节课中我们一起学习了以下核心概念联合挂载将两个目录层叠合并的机制顶层目录内容覆盖底层同名内容。白化文件一种特殊的文件类型S_ISWHT(mode)用于在联合挂载中标记底层已被删除的文件防止其暴露。可通过ls -W查看。文件类型检测在代码中可以使用S_ISWHT(mode)宏来检测一个文件是否为白化文件就像使用S_ISREG()或S_ISDIR()一样。#includesys/stat.hif(S_ISWHT(st.st_mode)){// 这是一个白化文件}系统依赖性白化文件和联合挂载的支持程度依赖于操作系统和文件系统。本课示例基于NetBSD其他UNIX变体的实现可能有所不同。总结https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/apue/img/5c2ce515e80779527950110589915ee9_17.png在本节课中我们探讨了UNIX中的联合挂载机制及其产生的白化文件。联合挂载提供了合并目录内容的能力而白化文件则巧妙地解决了在联合视图中删除文件时可能出现的逻辑冲突。理解这些概念有助于你深入理解某些文件系统如OverlayFS、UnionFS的工作方式。请注意这些功能是系统相关的在实际编程和应用时应查阅相关操作系统的手册页man page以确认其支持与具体行为。