寄存器在cpu工作流程中的位置

寄存器在cpu工作流程中的位置,第1张

寄存器通常分为高位和低位,如AH寄存器和AL寄存器,为了直观读出数据,多数使用十六进制表示,十六进制的数后加H表示,二进制用B,而十进制什么都不用加。如:4BC6H,4343,10001100B

16位结构CPU特点:

1运算器最多可以处理16位的数据

2寄存器的宽度为16

3寄存器与运算器之间的通路宽度为16

内存单元对应的地址叫物理地址

CPU给出物理地址的方法(地址总线20根的情况):

1由CPU提供一个16位段地址和另一个16位偏移地址,通过内部总线传送到地址加法器

2地址加法器将两个16位的地址同过运算合成一个20位的物理地址

物理地址=段地址16+偏移地址

3在通过内部总线将物理地址传送到输入输出控制器,控制器将其送到地址总线,再到相应的存储器

通用寄存器用于寄存一般的数据,如AX,BX,CX,DX

段寄存器有四个:CS,DS,SS,ES

段寄存器两个关键的寄存器:CS代码段寄存器和IP指令指针寄存器,也就是CS用于寄存段地址,而IP侧是偏移地址,物理地址表达式为CS:IP

CPU的工作过程:

1从CS:IP读取内存单元的指令,进入缓冲指令器(详细<汇编语言>P35-40)

2IP=IP+读取指令的长度,进入下一次的读取

21 通用寄存器

8086CPU的所有寄存器都是16位,可以存放两个字节。AX、BX、CX、DX四个寄存器通常用来存放一般性的数据,被称为通用寄存器。

一个16位寄存器可以存储一个16位的数据。

那么一个16位寄存器所能存储的数据的最大值为多少呢?

8086CPU的上一代CPU中的寄存器都是8位的,为了保证兼容,使原来基于上代CPU编写的程序稍加修改就可以运行在8086之上,8086CPU的AX、BX、CX、DX四个寄存器都可分为两个可独立使用的8位寄存器来使用:

AX可分为AH和AL;

BX可分为BH和BL;

CX可分为CH和CL;

DX可分为DH和DL;

23 汇编指令

汇编指令举例:

mov ax,18 将18送入寄存器AX AX=18

mov ah,78 将78送入积存器AH AH=78

add ax,8 将寄存器AX中的数值加上8 AX=AX+8

mov ax,bx 将寄存器BX中的数据送入寄存器AX AX=BX

add ax,bx 将AX和BX中的数值相加,结果存在AX中 AX=AX+BX

在写一条汇编指令或一个寄存器的名称时不区分大小写,如:mov ax,18和MOV AX,18的含义相同;bx和BX的含义相同。

注意:如果单独把AH和AL做为两个独立的8位寄存器来用,那么它们两个就是两个不相关的寄存器,不要错误的认为,诸如add al,93H的指令产生的进位会存储在ah中,add al,93H进行的是8位运算。

如果执行add ax,93H,低8位的进位会存储在ah中,CPU在执行这条指令时认为只有一个16位寄存器ax,进行的16位运算。指令add ax,93H执行后,ax中的值为:0158H。此时,使用的寄存器是16位寄存器ax,add ax,93H相当于将ax中的16位数据00c5H和另一个16位数据009CH相加,结果是16位的0158H。

在进行数据传送或运算时,要注意指令的两个 *** 作对象的位数应当是一致的,例如:

mov ax,bx

mov bx,cx

mov ax,18H

mov al,18H

add ax,bx

add ax,20000

等都是正确的指令,而

mov ax,bl (在8位寄存器和16位寄存器之间传送数据)

mov bh,ax (在16位寄存器和8位寄存器之间传送数据)

mov al,20000 (8位寄存器最大可存放值为255的数据)

add al,100H (将一个高于8位的数据加到一个8位寄存器中)

等都是错误的指令,错误的原因都是指令的两个 *** 作对象的位数不一致。

在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=F000H,IP=FFFFH,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFFOH单元中的指令是8086PC机开机后执行的第一条指令。

现在,我们更清楚了CS和IP的重要性,它们的内容提供了CPU要执行指令的地址。

mov指令不能用于设置CS、IP的值,原因很简单,因为8086CPU没有提供这样的功能。8086CPU为CS、IP提供了另外的指令来改变它们的值。能够改变CS、IP的内容的指令被统称为转移指令。

我们现在介绍一个最简单的可以修改CS、IP的指令:jmp指令。

若想同时修改CS、IP的内容,可用指令“jmp 段地址: 偏移地址”完成,如:

jmp 2AE3:3,执行后:CS=2AE3H,IP=0003H,CPU将从2AE33H处读取指令。

jmp 3:0B16,执行后:CS=0003H,IP=0B16H,CPU将从00B46H处读取指令。

若想仅修改IP的内容,可用指令“jmp 某一合法寄存器”完成,如:

jmp ax,指令执行前:ax=1000H,CS=2000H,IP=0003H

指令执行后:ax=1000H,CS=2000H,IP=1000H

指令“jmp 某一合法寄存器”的功能为:用寄存器中的值修改IP。

jmp ax,在含义上类似于mov IP,ax这样的指令。

CPU只认被CS:IP指向的内存单元中的内容为指令,所以,要让CPU执行我们放在代码段中的指令,必须要将CS:IP指向所定义的代码段中的第一条指令的首地址。

对于上面的例子,我们将一段代码存放在123BOH-123BAH内存单元中,将其定义为代码段,如果要让这段代码得到执行,可设CS=123BH,IP=0000H。

段地址在8086CPU的段寄存器中存放。当8086CPU要访问地址时,由段寄存器提供内存单元的段地址。8086CPU有4个段寄存器,其中CS用来存放指令的段地址。

CS存放指令的段地址,IP存放指令的偏移地址。

8086机中,任意时刻,CPU将CS:IP指向的内容当作指令执行。

8086CPU的工作过程:

1:从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;

2:IP指向下一条指令;

3:执行指令。(转到步骤1,重复这个过程。)

查看CPU和内存,用机器指令和汇编指令编程

1:预备知识:Debug的使用

(1)什么是Debug?

Debug是DOS,Windows都提供的实模式(8086方式)程序的调试工具,使用它,可以查看 CPU各种寄存器中的内容、内存的情况和在机器码级别跟踪程序的运行。

(2)我们用到的Debug功能

用Debug的R命令查看、改变CPU寄存器的内容;

用Debug的D命令查看内存中的内容;

用Debug的E命令改写内存中的内容;

用Debug的U命令将内容中的机器指令翻译成汇编指令;

用Debug的T命令执行一条机器指令;

用Debug的A命令以汇编指令的格式在内存中写入一条机器指令;

(3)进入Debug

Debug是在DOS方式下使用的程序。我们进入Debug前,应先进入到DOS方式,用以下方式可 以进入DOS:

1:重新启动计算机,进入DOS方式,此时进入的是实模式的DOS;

2:在Windows中进入DOS方式,此时进入的是虚拟8086模式的DOS;

下面说明在Windos2000中进入Debug的一种方法,在Windows98中进入的方法与此类似。

开始-运行-输入command

进入DOS方式后,如果显示为窗口方式,可以按下Alt+Enter键将窗口变成全屏方式,然后 运行Debug程序。

在 《追踪Redis Sentinel的CPU占有率长期接近100%的问题》 一文中,通过结合Redis Sentinel的源码,发现由于出现了"Too many open files"问题,致使Sentinel的acceptTcpHandler事件处理函数会被频繁并快速调用,最终导致了CPU长期接近100%的现象。但对于为什么会出现“Too many open files”这个问题,本文将在上一篇的基础上,继续探讨和分析。

“Too many open files”这个错误其实很常见,想必大家早已对其有一定的了解,这里打算再简单的介绍一下。

很明显,“Too many open files”即说明打开的文件(包括socket)数量过多,已经超出了系统设定给某个进程最大的文件描述符的个数,超过后即无法继续打开新的文件,并且报这个错误。

首先,我们需要了解有关open files的基本知识。详细的概念大家可以谷歌,网上也有各种各样的解决办法,这里只对open files做简单的介绍和总结。

我们在linux上运行ulimit -a 后,出现:

如图open files的个数为1024,很明显这个值太小了(linux默认即为1024),当进程耗尽1024个文件或socket后,就会出现“Too many open files”错误。现实生产环境中这个值很容易达到,所以一般都会进行相应修改。

最简单的修改方式是ulimit -n 65535,但这样重启系统后又会恢复。永久生效的方法是修改/etc/security/limitsconf 文件,在最后加入:

修改完成后,重启系统或者运行sysctl -p使之生效。

soft 和hard,表示对进程所能打开的文件描述符个数限制,其概念为:

他们的区别就是软限制可以在程序的进程中自行改变(突破限制),而硬限制则不行(除非程序进程有root权限)。

上面的规则大家最好自己尝试一下,以增加印象。

重启后,运行ulimit -a,可以看到open files 的值改变为:

简单介绍完open files,我们再来了解下file-max。这个参数相信有很多人经常与ulimit中的open files混淆,他们的区别我们必须了解。

file-max 从字面意思就可以看出是文件的最大个数,运行cat /proc/sys/fs/file-max,可以看到:

这表示当前系统所有进程一共可以打开的文件数量为387311。请务必注意”系统所有进程"这几个字。

运行 vim /etc/sysctlconf ,有时候你会看到类似:fsfile-max = 8192。出现这个则表示用户手动设置了这个值,没有则系统会有其默认值。手动设置的话,只需要在sysctlconf 中加上上述语句即可。

回到ulimit中的open files,它与file-max的区别就是:opem filese表示当前shell以及由它启动的进程的文件描述符的限制,也就是说ulimit中设置的open files,只是当前shell及其子进程的文件描述符的限定。是否清楚?可以简单的理解为:

好了,对于 file-max与open files的简单介绍到此为止。现在的问题就是,“Too many open files”到底是碰到哪个设置的雷区造成的。

结合上一篇,我们知道sentinel主要在执行accept函数时出现了“Too many open files”错误,熟悉accept这个系统调用的朋友很清楚,accept会接收客户端的请求,成功的话会建立连接,并返回新的socket描述符。所以,我们确定这里的“Too many open files”指的即是socket的数目过多。

我们猜测,是否是有大量的Jedis连接同时存在,耗尽服务器的socket资源,导致新的连接请求无法建立。所以,我们查看一下sentinel服务器的TCP连接,运行:netstat -anp | grep 26379,得到:

由上图可以发现,有非常多处于ESTABLISHED状态的TCP连接,运行 netstat -anp | grep 118:26379 | wc -l查看他们的个数:

可以看到,Sentinel同时维持了4071个TCP连接,而且过了很久之后,仍然是这么多,不会有大幅变化。

这时,也许你会想到,是否因为系统文件描述符的限制导致Sentinel无法建立更多的Socket,从而产生“Too many open files”的错误。所以,马上在Sentinel服务器上运行cat /proc/sys/fs/file-max,发现:

这个值很大,看似一切正常。继续运行sudo cat /proc/5515/limits,发现:

看到上图,我们似乎发现了端倪,这里的hard和soft都是4096,与前面的4072比较接近。为什么会这么低?我们继续查看一下ulimit,运行ulimit -a :

可以看到,ulimit中open files 的设置为64000,为什么会不一致?按理说sentinel应该最大有64000+的open files。

对于这个矛盾,一开始我怎么也想不明白。最后我推测:应该是在最早启动sentinel启动的时候,系统的设置为4096,sentinel启动之后,又在某个时间又改为64000,而sentinel进程确保持原有设置,从而导致很快达到限制。

我马上查看了进程的启动时间,运行ps -eo pid,lstart,etime | grep 5515:

发现进程启动于2015年12月2日,接着再次运行 ll /etc/security/limitsconf:

发现确实在2016年4月改动过, 然后我咨询了运维人员,他们告知我,确实改动过,但由多少改动到多少,他们也忘了。。。

为了了解Sentinel启动时的状况,紧接着查看了Sentinel的日志,下面是Sentinel启动时打印的画面:

上图说明了进程是2015年12月2日启动的,特别注意最开头的几行,非常关键:

这几句的意思是:

问题很清楚了,redis sentinel最大可以支持10000个客户端,也就是10032个文件描述符,但由于当前被人为限制到4096 了,所以,自动降低了标准。

因此,我猜测,最早open files的限制为4096时,Sentinel已经启动了,只要进程启动,改多少都没有用。很明显,在生产环境上,4096个连接请求很快就会达到。

接着,我继续查看了Sentinel的配置文件,如下图所示:

上图中, “Generated by CONFIG REWRITE”之前的都是是人工配置,其后为Sentinel自动重写的配置。

熟悉Redis的朋友都知道。Sentinel可能会对其配置文件进行更新重写:

我们很快注意到了maxclients 4064这个配置项,此时我很迷惑。我们知道,在Sentinel中是无法手动运行config set命令的,那这个4096必然不是来自于人工配置,Sentinel为什么要自动重写4064这个值。其实,仔细发现,这里Sentinel限制了最多4064个连接,加上32个预留,刚好为4096。

于是,综上,我猜测,Sentinel在启动的时候发现自己的10032个open files的预期与事实设置的4096不符,所以被迫遵守4096,减去预留的32,最终maxclients 只有4064,并且之后因为某些原因重写了配置,所以输出了这个值。

好吧,我的一贯作风,先猜测,再让源码说话。

我们可以通过异常信息定位异常所处源码,所以我搜索了前面提到的Sentinel在启动时打印的关于maxclients 的日志信息中的文本,如“You requested maxclients of ”。

这个异常出现在adjustOpenFilesLimit函数,通过函数名可以清楚它的作用,然后发现它的调用链只是:

``main()->initServer()->adjustOpenFilesLimit()```

所以,可以确定,在Sentinel服务器启动并进行初始化的时候,会调用adjustOpenFilesLimit函数对open files个数进行调整。调整策略是什么呢?我们查看源码:

在第1行中,REDIS_MIN_RESERVED_FDS即预留的32,是Sentinel保留的用于额外的的 *** 作,如listening sockets, log files 等。同时,这里读取了servermaxclients的值,看来servermaxclients具有初始化值,通过经过定位源码,发现调用链:

即Sentinel在启动时,调用initServerConfig()初始化配置,执行了servermaxclients = REDIS_MAX_CLIENTS(REDIS_MAX_CLIENTS为10000),所以servermaxclients就有了初始值10000。

回到adjustOpenFilesLimit()函数,adjustOpenFilesLimit最终目的就是得到适合的soft,并存在servermaxclients中,因为该函数比较重要,下面专门作出解释:

1 先得到maxfiles的初始值,即Sentinel的期望10032

2 然后获取进程当前的soft和hard,并存入limit ,即执行getrlimit(RLIMIT_NOFILE,&limit) :

调整过程为:

1 先用oldlimit变量保存进程当前的soft的值(如4096)

2 然后,判断oldlimit<maxfiles ,如果真,表示当前soft达不到你要求,需要调整。调整的时候,策略是从最大值往下尝试,以逐步获得Sentinel能申请到的最大soft。

尝试过程为:

1 首先f保存要尝试的soft值,初始值为maxfiles (10032),即从10032开始调整。

2 然后开始一个循环判断,只要f大于oldlimit,就执行一次setrlimit(RLIMIT_NOFILE,&limit) ,然后f减16:

这样,用这种一步一步尝试的方法,最终可用得到了Sentiel能获得的最大的soft值,最后减去32再保存在servermaxclients中。

另外,当得到Sentinel能获得的最合适的soft值f后,还要判断f与oldlimit(系统最初的soft限制,假设为4096),原因如下:

也许会直到f==4096才设置成功,但也会出现f<4096的情况,这是因为跨度为16,最后一不小心就减多了,但最后的soft值不应该比4096还小。所以,f=oldlimit就是这个意思。

最后,还有一个判断:

上面的过程我们用简单的表示为:

adjustOpenFilesLimit()的分析到此结束。但有一点一定要明确,adjustOpenFilesLimit()只会在Sentinel初始化的时候执行一次,目的就是将最合适的soft保存到了servermaxclients (第xx行),以后不会再调用。这样,一旦设置了servermaxclients ,只要Sentinel不重启,这个值就不会变化,这也就解释了为什么Sentinel启动之后再改变open files没有效果的原因了。

那什么时候发生了重写呢?即“Generated by CONFIG REWRITE”这句话什么时候会输出?接着上面,我又在源码里搜索了“Generated by CONFIG REWRITE”这句话,发现了常量REDIS_CONFIG_REWRITE_SIGNATURE,通过它继而发现如下调用链:

上面的调用链中,表示有很多地方会调用sentinelFlushConfig()。

什么时候调用sentinelFlushConfig()呢?经过查找,发现有很多条件都可以触发sentinelFlushConfig函数的调用,包括Leader选举、故障转移、使用Sentinel set 设置命令、Sentinel处理info信息等等。

而sentinelFlushConfig()则会利用rewriteConfig(),针对具体的配置项,分别进行重写,最终将Sentinel所有的状态持久化到了配置文件中。如下所示,在rewriteConfig()中,可以看到非常多的重写类型, 这些重写类型都是与redis的各个配置选项一一对应的:

当然,我们只需要找到其中关于max clients的重写即可,所以在该函数中,我们找到了调用:

可以看到,该函数传入了Sentinel当前的servermaxclients(已经在启动时调整过了,前面分析过),以及默认的REDIS_MAX_CLIENTS即10032。该函数作用就是将当前的servermaxclients的值重写到配置文件中去。什么时候重写呢,即当默认值与当前值不同的时候(也就是force==true的时候),具体可以查看其源码,篇幅限制我们不做详细介绍。

通过前面一大堆的分析,我们可以得出结论:

讲到这里,还有一个问题就是,为什么Sentinel服务器会长期持有4000多个Established状态的TCP连接而不释放。按目前生产环境的规模,正常情况下业务客户端使用的Jedis建立的TCP连接不应该有这么多。

经过查看,发现Sentinel上的很多连接在对应的客户端中并没有存在。如红框所示IP10XX74上:

总计有992个连接:

而实际上在10XX74上,只有5个与Sentinel的连接长期存在:

也就是说,在Sentinel中有大量的连接是无效的,客户端并没有持有,Sentinel一直没有释放。这个问题, 就涉及到了TCP保活的相关知识。

我们首先要了解, *** 作系统通常会自身提供TCP的keepalive机制,如在linux默认配置下,运行sysctl -a |grep keep,会看到如下信息:

上面表示如果连接的空闲时间超过 7200 秒(2 小时),Linux 就发送保持活动的探测包。每隔75秒发一次,总共发9次,如果9次都失败的话,表示连接失效。

TCP提供这种机制帮助我们判断对端是否存活,当TCP检测到对端不可用时,会出错并通知上层进行处理。keepalive机制默认是关闭的,应用程序需要使用SO_KEEPALIVE进行启用。

了解到这个知识之后,我们开始分析。在Redis的源码中,发现有如下调用链:

还记得acceptTcpHandler吗,acceptTcpHandler是TCP连接的事件处理器,当它为客户端成功创建了TCP连接后,会通过调用createClient函数为每个连接(fd)创建一个redisClient 实例,这个redisClient 与客户端是一一对应的。并且,还会设置一些TCP选项,如下所示。

如果用户在Redis中没有手动配置tcpkeepalive的话,servertcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE,默认为0。

由第x-x行我们可以明确,Redis服务器与客户端的连接默认是关闭保活机制的,因为只有当servertcpkeepalive不为0(修改配置文件或config set)时,才能调用anetKeepAlive方法设置TCP的keepalive选项。

我们知道,Sentinel是特殊模式的Redis,我们无法使用config set命令去修改其配置,包括tcpkeepalive 参数。所以,当Sentinel启动后,Sentinel也使用默认的tcpkeepalive ==0这个设置,不会启用tcpkeepalive ,与客户端的TCP连接都没有保活机制。也就是说,Sentinel不会主动去释放连接,哪怕是失效连接。

但是,TCP连接是双向的,Sentinel无法处理失效连接,那Jedis客户端呢?它是否可以主动断掉连接?我们定位到了Jedis建立连接的函数connect(),如下所示:

由第x行可以看到,Jedis启用了TCP的keepalive机制,并且没有设置其他keepalive相关选项。也就是说,Jedis客户端会采用linux默认的TCP keepalive机制,每隔7200秒去探测连接的情况。这样,即使与Sentinel的连接出问题,Jedis客户端也能主动释放掉,虽然时间有点久。

但是,实际上,如前面所示,Sentinel服务器上有很多失效连接持续保持,为什么会有这种现象?

对于上面的问题,能想到的原因就是,在Jedis去主动释放掉TCP连接前,该连接被强制断掉,没有进行完整的四次挥手的过程。而Sentinel却因为没有保活机制,没有感知到这个动作,导致其一直保持这个连接。

能干掉连接的元凶,马上想到了防火墙,于是我又询问了运维,结果,他们告知了我一个噩耗:

目前,生产环境上防火墙的设置是主动断掉超过10分钟没有数据交换的TCP连接。

好吧,绕了一大圈,至此,问题已经很清楚了。

终于,我们得出了结论:

有了前面的分析,其实解决办法很简单:

关于“追踪Redis Sentinel的CPU占有率长期接近100%的问题”到此就结束了,在写这两篇博文的时候,我收货了很多自己没有掌握的知识和技巧。现在觉得,写博文真的是一件值得坚持和认真对待的事情,早应该开始。不要问我为什么,当你尝试之后,也就和我一样明白了。

这两篇文章的分析过程肯定有疏漏和不足之处,个人能力有限,希望大家能够理解,并多多指教,非常感谢!我会继续进步!

前面提到过,在每个客户端上,都可以发现5个正常的TCP连接,他们是什么呢?让我们重新回到Jedis。

在《追踪Redis Sentinel的CPU占有率长期接近100%的问题 一》中,我们提到Jedis SentinelPool会为每一个Sentinel建立一个MasterListener线程,该线程用来监听主从切换,保证客户端的Jedis句柄始终对应在Master上。在这里,即会有5个MasterListener来对应5个Sentinel。

其实,MasterListener的监听功能根据Redis的pub sub功能实现的。MasterListener线程会去订阅+switch-master消息,该消息会在master节点地址改变时产生,一旦产生,MasterListener就重新初始化连接池,保证客户端使用的jedis句柄始终关联到Master上。

如下所示为MasterListener的线程函数,它会在一个无限循环中不断的创建Jedis句柄,利用该句柄去订阅+switch-master消息,只要发生了主从切换,就会触发onMessage。

如何实现订阅功能呢,我们需要查看subscribe函数的底层实现,它实际使用clientsetTimeoutInfinite()->connect建立了一个TCP连接,然后使用JedisPubSub的proceed方法去订阅频道,并且无限循环的读取订阅的信息。

在procee的方法中,实际先通过subscribe订阅频道,然后调用process方法读取订阅信息。

其实,subscribe函数就是简单的向服务器发送了一个SUBSCRIBE命令。

而process函数,篇幅较长,此处省略,其主要功能就是以无限循环的方式不断地读取订阅信息

综上,MasterListener线程会向Sentinel创建+switch-master频道的TCP订阅连接,并且会do while循环读取该频道信息。如果订阅或读取过程中出现Tcp连接异常,则释放Jedis句柄,然后等待5000ms 后重新创建Jedis句柄进行订阅。当然,这个过程会在一个循环之中。

至此,也就解释了为何每个业务客户端服务器和Sentinel服务器上,都有5个长期保持的、状态正常的TCP连接的原因了。

在 内存管理篇 ,我们研究了Kubernetes(K8s)requests和limits的资源含义,以及Docker容器运行时的内存含义。我们在这里将介绍K8中的CPU的含义,以及CPU的requests和limits,并研究如何为Pod设置合理的cpu资源。

那么,CPU在kubernetes中的确切含义是什么?一个CPU等价于node *** 作系统提供的一个CPU核心。

与内存相反,CPU在kubernetes中是可压缩的,这意味着它可以被throttled(限制)。当您指定容器的CPU limits时,实际上是在nodes上限制该容器的CPU时间,而不是设置容器对特定的一个CPU或一组CPU的亲和性。这意味着,即使您指定的limits少于节点上的CPU总数,您的容器仍会看到(并使用)该节点的所有CPU,只是你的容器可以使用的cpu的时间是有限的。

例如,在具有8个CPU核心的node上指定某个容器的CPU limits为4,这也就意味着该容器只能使用相当于4个CPU核心的全部时间,但是可以分布在所有CPU核心上。对于在专用node上运行的单个容器,这种情况下,该node所有CPU核心的最大允许使用率将为50%。

那么,kuberne上的这种限制转换成Docker中的控制会是怎样呢? K8s通过传递cpu-period和cpu-quota选项来实现kubernetes中的CPU limits 。 cpu-period始终设置为100000μs(100ms),表示跟踪容器CPU利用率的时间段。 cpu-quota是容器在每个cpu周期内可以使用的CPU时间总量。这两个设置都控制着内核的完全公平调度程序(CFS)。这是不同的K8s CPU限制转换为Docker配置的方式:

limits  1: cpu-quota=100000

limits  4: cpu-quota=400000

limits  05: cpu-quota=50000

limits  1 意味着每100ms,允许使用总CPU时间相当于一个CPU核心的100%CPU时间,  limits  4 意味着每100ms允许使用总CPU时间相当于一个CPU核心400%的CPU时间 (或者,相当于4个CPU核心100%的CPU时间) 。不要忘记,这些CPU时间都是分布在所有的CPU核心上的。 由于CFS配额的工作方式, 在给定时间段内超出其配额的任何容器在该时间段内将不允许再次运行,直到下一个时间段 -这意味着您的程序会感觉到莫名其妙的暂停,特别是如果您的程序设置了CPU limits,并且对延迟敏感的情况下这种感觉更加明显。

您可以通过指定Pod的request来指定Pod所需的CPU(或CPU的一部分),在指定request的时候,kubernetes不允许node的request的值超过节点的CPU核数(属于某个node上的所有pod的cpu的request的总和不能超过该node上显示的CPU数量)。您可以将request的总数设置为node的CPU的核数,但是当node的工作负载过高时,例如当CPU的使用率达到甚至超过100%时会怎样?这时, 容器的CPU调度优先级由Pod的``requests''的值来决定,该requests的值乘以1024后的乘积作为cpu-shares选项传递给Docker 。cpu-shares实际上就是一个权重;如果节点上运行的所有容器具有相同的权重,则在负载过大的情况下(没有空闲的CPU周期时),所有容器将具有相同的CPU调度优先级。如果一个容器的权重比其他容器大,则在负载过大的情况下,它将具有更高的CPU调度优先级,并能够比其他容器获得更多的CPU时间。

在 内存篇 , burstable的QoS配置意味着在其他容器不使用CPU的情况下,您的容器使用这些空闲的CPU时间,从而可能使用到比requests更多的CPU时间。这样可以潜在地更有效地利用基础资源,但具有更大的不可预测性,例如,对CPU比较依赖的应用程序的响应延迟可能会受到在同一nodes上的其他容器的暂时影响。在最坏的情况下,一个node上的burstable容器若过多,将会引起非常高的CPU负载,从而相互对彼此产生负面。

如果您是K8新手,那么最好将requests和limits的值设置为相同,从而确保你的pod是Guaranteed类型的容器。当您对CPU的资源利用特性了解的更深入时,并可能发现您在CPU方面的配置已过剩时,您可以考虑引入Burstable容器,以期获得更大的收益,甚至可以提高CPU的整体使用率。

您应该为容器分配多少个CPU?根本没有标准答案,这取决于你应用的组特性,可接受的性能,pod的调度策略,主机类型,成本等。

但是-如果您对你的应用了如指掌,则可以在性能测试环境中甚至在生产环境中尝试不同的配置。最终你将会找到性能和成本的平衡点,但是container-throttling是Linux上运行的CPU密集型应用的重要影响因素。通常可以使用Prometheus来查看CPU的相关信息,也可以直接进入容器并查看cgroup CPU统计信息:

cat /sys/fs/cgroup/cpu,cpuacct/cpustat

该文件中包含了总共执行了多少次CPU调度,容器被限制的总次数以及累积限制的时间(以纳秒为单位)。

参考: >

核心调度器管理活动进程的主要数据结构是就绪队列rq, 每个cpu都有自己的就绪队列,而每个活动进程某一时刻只会在一个就绪队列中。

对于调度器而言,由于支持多种调度算法,所以各种调度算法的就绪队列会嵌入到就绪队列的子队列中。

就绪队列具体定义见 rq定义

访问就绪队列所使用的锁 , 可以使用函数raw_spin_lock_irq()获取锁后关闭中断,或者使用函数 raw_spin_unlock_irq()释放锁并开启中断

当前就绪队列可投入运行的调度实体个数,如进程、线程等调度实体,但当前正在运行 的调度实体不计入 nr_running 中,因为它已经从就绪队列中脱离 函数nr_running()获取所有,同时文件文件 proc/loadavg也可了解相关信息

用于跟踪 CPU 负载|大小为CPU_LOAD_IDX_MAX,值为5,也可通过文件proc/sched_debug了解负载信息

更新负载时的时间|在 cpu_load 得到更新时此成员记录了当前工作时间(jiffies 值)。

用于在动态时钟模式开启时,记录当前时钟模式|不同模式切换会影响系统进 入不同的系统状态,通过先前系统状态和当前环境,系统决定进入不同的系统环境。

配置 CONFIG_NO_HZ_FULL 时,即动态时钟启用时,记录上一次调度 tick|动态时钟 管理中,调度并不周期性运行,因此需要成员记录调度时间。

用于跟踪就绪队列的负载,它是由 Local CPU 上所有可运行的调度实体共同作用的结果。成员 weight 用于记录具体权重,而 inv_weight 则用于优先级到权重的乘数因子 一般来说,在每次 tick 中断到来时,系统更新就绪队列负载

当 rq->cpu_load[]成员更新时,该成员记录更新的次数。在函数 cpu_load_update ()中 更新此值,此函数由 cpu_load_update_periodic()调用,或者在配置 CONFIG_NO_HZ_COMMON 下由函数 cpu_load_update_nohz ()调用

用于跟踪就绪队列中进程上下文切换的次数。

想了解某个具体进程的切换次数和调度信息则可以通过/proc/PID/schedstat 和/proc/PID/sched 两个文件了解详细信息|

分别对应:

CFS调度算法就绪队列

RT调度算法就绪队列

DL调度算法就绪队列

如果需要 CFS 支持组调度管理,那就 得把所有 CFS 加入到一个链表当中,leaf_cfs_rq_list 成员就是负责把本 CPU 下的就绪队列 中各个 CFS 子队列关联起来。并且在 cfs_rq 里面有成员 on_list,其表示当前的 CFS 队列 leaf_cfs_rq_list 成员是否加入到就绪队列 rq 的 leaf_cfs_rq_list 管理的链表当中。

用于跟踪当前系统中队列里面不可信号中断的调度实体个数

分别用于描述当前正在运行的任务,空闲任务,刚刚停止运行的任务;

用于指示就绪队列进行下一次负载平衡的时间,该成员在初始化时被设置为当前系 统 jiffies。

就绪队列中用于保存先前执行任务使用的内存描述符 mm_struct 结构。此成员在发生调度时由内核函数 context_switch()来更新。

描述的是队列的时间,一般在每个 tick 中 断到来时更新,其由上面提到的函数 update_rq_clock()对此成员更新。

clock_task 记录的是 队列中任务执行的时间,其由函数 update_rq_clock_task()负责更新。

描述当前就绪队列里面正在进行 IO 等待的进程个数,此成员一般和进程描述符 task_struct 中的 in_iowait 成员配合使用。

可以通过 proc/stat 下的 procs_blocked 值了解 nr_iowait 的大小

以上是rq基本成员,下面是SMP相关的成员:

描述了当前队列中需要唤醒的任务链表

这个成员在 SMP 下非常重要,其是负载均衡时任务迁移中间状态链表

对于那些需要迁移的任务来说,其会由本 CPU 下的就绪队列脱离出去,暂时的存 放到目标 CPU 下的就绪队列的 wake_list 链表当中。

指示了当前 rq 运行队列可以工作的 CPU,即描述了可以为此就绪队列服务的 CPU。

用于描述 rq 就绪队列所处的调度域。

两个成员都是用于 rq 就绪队列的 CPU 能力描述。在系统初始化时,两者都被设置为 SCHED_CAPACITY_SCALE 值,当前版本中值大小为 1024。cpu_capacity 是运行过程中使用 的能力,而 cpu_capacity_orig 则是保存最原始的能力。可以使用函数 capacity_of()或者 capacity_orig_of()获取 rq 就绪队列的 CPU 能力值。用户可以调用函数 update_cpu_capacity()更新此能力。

在调度(_schedule())函数末尾处的回调链表,每个回调函数只会执行一次。

目前仅在RT/DL调度中使用。

用于指示是否需要开启负载均衡 *** 作,在函数 load_balance()对其设置。在负载均衡 *** 作中,如果此值被设置,则内核把任务迁移到空闲 CPU 上运行。

用于指示可 push 任务所在的 CPU。在负载均衡 *** 作中,对可 push 的 CPU 选取 *** 作是一个非常繁琐的计算过程,一般会选择空闲的 CPU 处理迁移的任务。

多核环境中用于指示当前 rq 就绪队列所在的 CPU。

rq 就绪队列是否处于工作状态,函数 set_rq_online()对其设置为 1,即表示为工作状态, 而函数 set_rq_offline()对其设置为 0,表示处于非工作状态。

用于从就绪队列 rq 角度跟踪 CFS 就绪队列中的调度实体

用于记录负载平衡耗费的时间,这个时间是可以记录到空闲时间中去的。

用于记录 CPU 空闲时间,此成员会与上面的 idle_stamp 配合使用。

此值与上面 avg_idle 作用类似,其用于跟踪 CPU 空闲的最大时间。

以上就是关于寄存器在cpu工作流程中的位置全部的内容,包括:寄存器在cpu工作流程中的位置、追踪Redis Sentinel的CPU占有率长期接近100%的问题 二、Kubernetes CPU管理及调度等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zz/9752434.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-01
下一篇 2023-05-01

发表评论

登录后才能评论

评论列表(0条)

保存