我正在尝试学习操作系统概念。这是两个简单的Python代码:
while True:
pass
和这个:
from time import sleep
while True:
sleep(0.00000001)
问题:为什么运行第一个代码时 CPU 使用率为 100%,但运行第二个代码时 CPU 使用率约为 1% 到 2%?我知道这可能听起来很愚蠢,但为什么我们不能sleep
在不使用睡眠系统调用的情况下实现用户空间模式之类的东西?
注意:我试图理解睡眠来自linux内核的系统调用但说实话,我不明白那里发生了什么。我还搜索了 NOP 汇编代码,结果发现它并没有真正起作用没有什么但做了一些无用的事情(比如 xchg eax、eax),也许这就是 CPU 使用率 100% 的原因。但我不确定。
究竟有哪些睡眠系统调用的汇编代码是我们无法在用户空间模式下执行的?是不是类似 HLT 的东西
我还尝试在代码中使用HLT
汇编,如下所示:
section .text
global _start
_start:
hlt
halter:
jmp _start
section .data
msg db 'Hello world',0xa
len equ $ - msg
但运行此代码后,我看到内核一般保护错误如下:
[15499.991751] traps: hello[22512] general protection fault ip:401000 sp:7ffda4e83980 error:0 in hello[401000+1000]
我不知道也许这与保护环或者我的代码是错误的?这里的另一个问题是操作系统是否在系统调用下使用HLT
或其他受保护的汇编命令sleep
?
答案1
为什么运行第一个代码时 CPU 使用率为 100%,但运行第二个代码时 CPU 使用率约为 1% 到 2%?
因为第一个是“繁忙循环”:您始终在执行代码。第二个告诉操作系统该特定进程想要暂停(睡眠),因此操作系统取消调度该进程,如果没有其他进程正在使用 CPU,则 CPU 会变得空闲。
我还搜索了 NOP 汇编代码,结果发现它并不是真的什么都不做
好吧,NOP = 无操作:它正在主动执行没有效果的代码。您可以使用它来填充代码,但不能将 CPU 置于低功耗状态。
究竟有哪些睡眠系统调用的汇编代码是我们无法在用户空间模式下执行的?
x86 CPU 上的现代操作系统使用mwait
.其他 CPU 架构使用其他命令。
但运行此代码后,我看到像这样的内核一般保护错误
这是因为操作系统应该在管理员模式下执行此操作。正如我上面所写,操作系统需要能够保持调度进程,因此进程本身不允许将 CPU 置于空闲模式。
这里的另一个问题是操作系统在睡眠系统调用下使用 HLT 或其他受保护的汇编命令
是的,它确实。虽然它不是在睡眠调用期间执行,而是在调度程序循环内执行,当调度程序检测到没有要运行的进程时。
第一部分的一个问题。如果我使用很少的时间段,即:
sleep(0.0000000000000001)
调度程序是否仍然进入下一个进程?
对于实际的操作系统系统调用,请参阅man 3 sleep
(以秒为单位的分辨率)、man usleep
(以微秒为单位的分辨率)和man nanosleep
(以纳秒为单位的分辨率)。
无论您在 python 代码中使用什么浮点数,都不会比 python 使用的系统调用(无论它是什么变体)获得更好的分辨率。
联机帮助页说“将调用线程的执行暂停(至少)usec 微秒”。等等,所以我假设即使延迟为零(然后立即重新安排)它也会被取消安排,但我没有测试这一点,也没有阅读内核代码。 。
答案2
操作系统的“CPU 使用率”测量基于用户空间进程/线程(任务)是否在 CPU 上运行,即使这些指令只是浪费时间。如果任务不使用 CPU 时间,则意味着操作系统可能正在运行其他任务。
当用户空间忙等待时,情况并非如此,即使它可以强制 CPU 进入低功耗状态。 CPU 电源状态无关紧要,正如 marcelm 评论的那样:重要的是您是否告诉操作系统该任务应该休眠。操作系统可以让CPU进入睡眠状态如果没有其他任务。
甚至 x86pause
指令不让操作系统在CPU上调度不同的任务。最近的tpause
或umonitor
/也不会umwait
,尽管它们可以将 CPU 置于 C0.1 或 C0.2 省电状态。
在 x86-64 中也是nop
真正的 NOP,没有后端执行单元。xchg eax,eax
在64位模式下不是NOP,它将EAX零扩展为RAX,因此它不能使用0x90
64位模式下的编码,只能在16位或32位模式下使用。这就是为什么nop
有自己的条目在英特尔手册中。当然,即使在 32 位模式下,现代 CPU 也会将0x90
其与其他长 nop 操作码一起识别为无操作和特殊情况。
但话又说回来,这不是相关的事情,CPU使用率是指CPU是否可以进入省电状态或运行其他东西,因为内核知道这个进程没有任何东西可以运行。
hlt
是一条特权指令(仅在环 0 中有效,又名 CPL=0,如您在Intel的手动录入例外情况:#GP(0) 如果当前权限级别不为0。
用户空间无法让 CPU 进入睡眠状态,直到下一个中断到来为止,只有内核可以做到这一点(如果调度程序决定在该用户空间任务执行期间没有其他事情可做sleep
)。
或者直到 WAITPKG CPU 功能(Tremont 和 Alder Lake)为止,尽管umwait
让内核为用户空间启动的睡眠设置持续时间限制,以便操作系统可以限制用户空间的睡眠时间不那么长。而且它们的睡眠深度受到限制,甚至比hlt
(C1) 还浅,这对于唤醒延迟很重要。也许实时操作系统知道某个关键中断即将到来,并希望 CPU 没有任何额外的唤醒延迟。或者不想让任何事情让 CPU 进入睡眠状态。 (umwait
控制知道内核要设置,hlt
但不知道。)
不过,用户空间可能会浪费 CPU 时间,直到下一次中断为止,这是抢占式多任务内核重新获得 CPU 控制权的唯一方法,除非用户空间发生系统调用或 CPU 异常。 (也许像这样的系统调用yield()
专门用于提示操作系统上下文切换到等待空闲 CPU 核心的其他任务。)
跨站重复:
以及相关问答:
https://stackoverflow.com/questions/58386042/multiple-nop-instructions-do-not-consistently-take-longer-than-a-single-nop-inst(NOP 不会消耗固定的时间。乱序超标量 CPU 不会为每条线性相加的指令带来单独的成本)
https://stackoverflow.com/questions/1719071/how-is-sleep-implemented-at-the-os-level
这个问题IMO属于Stack Overflow,带有标签[程序集][操作系统][x86][CPU架构]。至少那里更适合; unix.SE 主要是关于使用它,而不是操作系统理论/概念。
如果可能的话,不要使用大量的微小睡眠,而是使用poll
或select
等待文件描述符上的活动。然后您的进程(和 CPU)可以保持睡眠状态,而不是不断醒来进行另一个系统调用。
如果您要忙等待,在该循环中放入睡眠会减轻其糟糕程度,但与睡眠或阻塞相比,操作系统会在有事情要做时唤醒您,这仍然不是很好。
打个比方,想象一下人们在银行排队等候的过程。 银行出纳员是CPU,他们执行客户(进程)的请求。
sleep()
就像离开出纳员并坐在椅子上,在指定的时间后重新排队。 (如果大多数人都这样做,该行通常是空的:CPU 空闲,因此新任务可以立即运行)。- 循环运行 NOP 就像谈论天气一样,让出纳员有事可做,但不做任何银行业务。出纳员无法为其他顾客提供服务,因为你让他们无所事事。
(与真正的银行不同,先发制人的多任务处理涉及柜员强迫客户走到队伍后面,即使他们还没有完成他们想做的所有事情。)
答案3
仅添加到 dirkt 关于您可以做什么、应该做什么、不应该做什么的良好答案:
有两种主要的不同方式可以让你的代码在一段时间内不执行任何操作:
A/对计时器进行编程,并释放 CPU 进行其他活动,期望您的代码被唤醒并能够继续遵循计时器中断。
这当然是在共享时间多任务系统中运行编码程序时遵循的最公平的方法(特别是关于 CFS,设计的任务在 SCHED_OTHER 调度策略下运行)
乙/除了保留 CPU 之外什么也不做。因为期望被某个定时器中断唤醒是:一种期望。在任何情况下都不能保证。当中断触发时,系统可能正忙:
- 在一些实时调度策略下运行一些任务,
- 运行一些中断处理程序,屏蔽所有中断或不可中断内核代码的任何部分(甚至在所谓的可抢占式 Linux 内核下仍然存在一些中断处理程序)。
- 并不是说根据所选择的定时器系统,中断甚至可能永远不会被触发。
无论如何,代码预期再次运行的时刻与实际再次运行的时刻之间的延迟(按时间顺序排列的时间差)绝对是不可预测的。
这在某些情况下显然是不可接受的。
想象一下,与某些设备通信(写入设备内存地址),必须保证两个连续操作之间严格的最小时序,并且必须保证严格的最大时序,除非面临潜在的欠载或至少不可接受的延迟。
当然,由于遵循这种方式,您很容易破坏大多数内核代码的效率,您应该只在所谓的原子上下文运行专用的内核函数,因此从用户空间受到限制,不能直接使用某些标记为特权的 cpu 操作码。 (如HLT)
顺便说一句和现在回答标题中表达的真正问题,我记得有时当我需要等待非常精确的数量时,我会依赖 NOP中央处理器周期。 (这是您可以通过操作码确定的唯一事情)因为,在现代架构上,内存、设备位于总线上,以不同的(有时是不相关的)时钟频率(与CPU相比)工作,这种方法导致不可移植(并且很难)调试)代码。这显然不再是正确的道路。
绝对建议保留 NOP 以设置断点。
答案4
NOP,可能更好地被认为是空操作。它做了一些事情,至少在理论上是这样,但它不会改变任何东西——除了程序计数器。
它们有多种用途,特别是在较旧的系统中等待一个时钟周期以使某些硬件准备就绪,或者在像 SPArc 这样的先执行后分支系统中,如果您想分支,则必须执行下一条指令。如果你不分支的话它就会被执行!在更换电脑之前您没有什么有价值的事情可做吗?只需在其中添加一个 NOP 即可!