目錄
回复内容:
首頁 後端開發 Python教學 为什么操作系统的事件监听不会占用100%的CPU?

为什么操作系统的事件监听不会占用100%的CPU?

Jun 06, 2016 pm 04:22 PM
cpu event pygame python while

我用python试图检测键盘事件,用的方法是在while循环中放置了pygame中的一个获取事件的函数event.get(),结果就是cpu始终占用100%。但是操作系统和其他语言(如C#)的事件监听函数基本不占cpu,它们是如何做到的?是牺牲了事件响应的实时性吗?

回复内容:

操作系统的事件监听是靠与CPU协作完成的,这一机制叫作硬件中断(Interrupt)。
正常情况下,CPU按照它内部程序计数器(Program counter)所指的指令(Instruction)顺序执行,或者如果所指的指令是跳转类指令,比如x86类的CALL或者JMP,跳转到指令所指的地方继续顺序执行。
只有四种情况,CPU不会顺序执行,包括上面所说的硬件中断,Trap(Trap (computing)),Fault,和Abort。

硬件中断时,CPU会转而执行硬件中断处理函数。Trap则是被x86的INT指令触发,它是内核系统调用(System call)的实现机制,它会使得CPU跳转到操作系统内核的系统函数(Exception Table)。Fault则是除0报错,缺页中断(Page fault),甚至内存保护错误实现所依靠的。

不会顺序执行的意思是,CPU会马上放下手头的指令,转而去处理这四种情况,在把这四种情况处理完之后再依靠CPU跳回到刚才的指令继续处理。这就是为什么即使单核CPU在100%占用处理另一个进程任务时,只要你的进程优先级够高,也能在键盘事件发生时让CPU停下而转而执行你的进程。

为什么监听键盘事件可以不占100%CPU,甚至可以0占用CPU呢?因为即使在CPU完全停止而不执行指令的状态(Idle (CPU)),硬件中断仍然会启动CPU开始执行中断处理函数(Interrupt handler)。

特别的,当你按下Ctrl+C时,键盘硬件会给CPU一个硬件中断,其中包含一个异常号(Exception Number),CPU拿到这个异常号马上调用之前由操作系统内核(Kernel (operating system))注册的中断处理函数,中断处理函数会调用内核中的键盘驱动,键盘驱动调用显示终端(Computer terminal)驱动在终端上显示"^C",同时通过调度器来唤醒相关的进程(Process (computing))。*nix还会生成信号(Unix signal)发送到当前的进程组(Process group),这些进程则会执行SIGINT的信号处理函数,这时我们的CPU就从内核模式(Kernel Mode)转到了用户模式(User mode)。

如果在这一事件发生之前你的进程使用了阻塞式(Blocking)的等待键盘响应,并且用户并没有什么程序执行。那么CPU大多数时候会被内核代码中的HLT指令转到空闲状态,只会被时钟的硬件中断周期性唤醒看看调度器有没有什么事情可以做。当键盘时间发生时,你的进程就会被唤醒,从等待函数中返回,继续执行之后的代码。你也将在电脑上屏幕上看到CPU仍然是0占用。

参考: Computer Systems: A Programmer's Perspective (3rd Edition)

这个问题涉及到系统处理事件的两种方法, 容我来组织一下语言。一种是interrupt,由CPU的中断机制来提醒操作系统发生了什么;另一种需要操作系统主动polling, 不停检查某应用程序是否有事件发生。操作系统一般是通过CPU中断机制来对应用程序提供服务的,原因很明显,使用第二种方法操作系统会不停地占有CPU资源来检查是否有事件发生。而且恰恰第二种方法实时性较差。想象用户程序要被timer interrupt打段之后才能进入kernel,然后kernel处理一些任务之后才会逐个poll所有进程的状态,这中间的latency肯定要比直接触发中断要大很多。


CPU中断机制有4种,trap, fault, interrupt, abort。他们之间有非常微妙的差别。这四种中断可以分为两大类,一类是同步发生的,包括trap, fault, abort,统称exception。另一类是异步发生的interrupt。trap是应用程序向系统主动申请服务的一种机制,syscall就是其中一种实现,所以我们通常说a user process traps into kernel。abort指的是程序的执行发生了一些意料之外的情况,通常是无法恢复执行的,比如硬件错误。fault也是指程序执行中的一些异常状况,但是通常是可以恢复执行的,比如page fault。最后interrupt通常是由外部输入硬件主动触发的中断,键盘、鼠标、触屏,timer等等就属于这种。这部分更详细的解释可以参考CSAPP的第二版的8.1节。


要描述清楚为什么系统监听键盘事件几乎不占用任何CPU资源,必须解释一下计算机系统如何处理external interrupt。假设我们在shell中跑了一个程序,然后按Ctrl + C来退出。首先,在我们按下Ctrl +C的时候,键盘控制器会发起一个interrupt并转给CPU。CPU通过查看自己的IDT (interrupt descriptor table),来找到与keyboard interrupt相对应的interrupt handler,CPU会自动把触发keyboard interrupt的进程(shell)的stack pointer, instruction pointer等信息push到kernel stack上,然后转到kernel stack和之前查到的interrupt handler开始执行。kernel会继续将剩余所有中断时的寄存器值全部push到kernel stack上形成一个trapframe,以便处理完中断后恢复执行shell。然后kernel根据CPU给的中断信息选择一个封装在kernel内部的interrupt handler继续执行。这个interrupt handler会拿到shell的context,然后给shell发送一个INT信号并恢复执行shell。shell接收的这个信号后会转到自己的INT signal handler继续执行。然后这个handler会调用一个system call kill来关闭自己跑在foreground的子进程(使用键盘的进程一定跑在foreground上),然后等待用户输入下一个命令。至此硬件+操作系统+shell相互合作处理一个键盘输入的过程就结束了。此过程省略数千字=,=。看起来很繁琐,但是系统就是这样,保证共享硬件资源的同时还要最大化效率,而且还要有足够的保护机制 =,=


EDIT: 修改了第二段关于四种中断机制概念的解释,更精确一些。不过这些术语本来就没有完全统一的说法。我觉得对于某种中断情况,例如page fault, external interrupt, syscall, divide by zero, 了解kernel如何进行处理就可以了,不必在意用语上的细节。

因为你调用的函数不是阻塞的,又没有加等待,所以会占满cpu。

如果轮询,则在循环中插入等待(sleep是其中一种),换句话说让cpu休息。

假如一次轮询需要0.1毫秒,增加10毫秒的等待,会使CPU每工作0.1毫秒休息10毫秒,如此,cpu占用下降到1%。

以上是一种很直观的解释。轮询或中断可以被包装成阻塞式调用,也就是说,你无需处理如何让cpu休息的问题,无消息时一直阻塞在该函数里面休息,当函数返回时必定能返回结果。如果你使用的函数本身是阻塞的,则你无需考虑cpu占用。

阻塞式调用解决了cpu满负荷的问题,因为内部已经包含了让cpu休息相关的操作。

然而阻塞式调用诞生了一个新问题:如何同时查询多个不同的消息呢?如何同时轮询多个不同的东西呢?

于是你无意中发现了异步通讯的核心。简单的说,一般就是在一个函数里面同时收听多个消息源,任何一种消息来了都反馈给你。如果该函数是阻塞的,则它必定返回一个消息。

但有时你希望除了消息之外还添加一点私货,所以会让函数不阻塞,这种情况下你仍然需要在循环中增加等待,避免cpu满载。

异步事件处理机制成为了现代服务器编程的主流,因为只有异步处理机制能够在短时间内处理庞大的请求数量而且不过分占用资源,多线程/多进程机制是无法做到的。

一言以蔽之:非阻塞式轮询,请加等待。不想加等待,请用阻塞式调用。同时轮询多个事件,用异步事件处理机制。 你要监视一个人的行踪,有两种方法:
1.不让他知道,你一天到晚盯着他,这会占满你的时间,但对他却没有任何影响,这叫做轮询
2.告诉他你关注着他,在他做了你关心的动作时叫他通知你,这几乎不占用你的时间,但对他来说会多一件不太麻烦的事,这叫做中断 我上一家就职的公司业务很忙,我的组长不但要处理本组的事物,还要对外响应其它组的需求。这是背景。
最开始大家有事都发邮件沟通。组长的工作方式是:处理一轮本组事物,查邮件看看来自外部的需求,有邮件就处理完再处理本组的事,没有就直接处理本组的事。这叫轮询。
突然有一天大领导有急事找他,给他发了封邮件。等了半个小时才得到回应,耽误了事。大领导很生气训我组长一顿。他总结教训,发现有些事是要及时响应的,于是给可能有急事的人说以后有事打电话。接到电话后他会放下手中的事立即处理电话来的需求,处理完了接着之前的工作。这叫中断。
后来有一天,HR给他打了个电话,要他去HR那领份材料处理完成再交给HR。他屁颠屁颠跑去领材料处理完,再交给HR的时候被告知其实三天内搞定都行。他仔细想了想,有些事开始的部分很急,比如领表,后面没那么急而且蛮费事。在接到某些电话时,他立即把前半部分处理完,然后扔下,择期处理。这叫硬中断和软中断分离。
再后来…公司的快递太多,大领导让他代领所有人的快递。于是他一天内接到了无数个快递小哥打的电话。累得要死。这叫中断风暴。
于是第二天他给门房大爷打了个招呼,让大爷暂存快递,第一个快递来的时候给他打个电话,他在中断上半部把这个事儿记下来。要是一天都没快递就不要打扰他。他在快下班时从门房把一天的快递领走发给大家。这叫在软中断轮询处理。
所以题主,如上面写的,在忙的时候中断是要比轮询更及时得到响应的。而你做的是让我如此聪明的组长闲的时候,对着邮箱不停按刷新…… 是的,牺牲了事件处理的实时性。另两个答案说的对,事件检测就是靠轮询或者中断。我来解释下为什么你的代码会占用100%CPU:
以Win任务管理器中的CPU使用率显示为例,我们知道,Windows是靠给每个线程分配时间片轮流执行来实现多线程/进程的,每个时间片大约是几毫秒到十几毫秒。Win8任务管理器默认以1秒1格的速度绘制CPU使用率曲线,也就是统计过去这1000ms里,有多少百分比时间被除了system idle进程使用了。你的Python使用了while死循环,会造成线程一直在使用分配到的时间片,所以CPU使用率会是100%。实际上你只要在while里加个time.sleep(0.001)就能使CPU降下来了。
sleep函数,是告诉操作系统,本线程要放弃当前未用完的时间片,并在接下来的指定时间内不要给我分配时间片。实际上整个操作系统里的进程在绝大多数时间里,都在sleep等待着,隔一会检查一下用户输入、消息投递之类的事件发生,所以CPU使用率并不高。sleep越多,单位时间内CPU使用率就越低。 监听一个事件是否发生有两种方法,一种是轮询,另一种是中断,第二种在Windows环境里,也可以称为钩子(不要太纠结叫什么,理解它就可以了)。

轮询就是你用python写的方法,一个while不停的检查,不停的询问:发生了没、发生了没……

钩子是什么意思呢?在Windows环境里,任何事件都以事件广播的形式发送到所有的窗口,窗口收到了事件然后去处理,对于一个按键(键盘)事件,大概的流程是这样的(XP-WIN7时代流程):

1)硬件中断/硬件端口数据
//WinIO能模拟,或者修改IDT是在这一层
2)键盘Port驱动(USB or PS/2)
//Filter驱动在此
//KeyboardClassServiceCallback也在这一层被调用
3)kbdclass驱动
//处理键盘布局和键盘语言,部分高端的病毒也工作在这里
4)Windows内核边界(zwCreate/zwReadFile)
----------------------(系统调用)----------------------
5)Windows内核边界(zwCreate/zwReadFile)
6)csrss.exe的win32k!RawInputThread读取,完成scancode和vk的转换
//SetWindowHook工作在这里(全局)
//kbd_event工作在这里
7)csrss.exe调用DispatchMessage等函数分发消息(此处开始广播键盘消息
//SetWindowHook工作在这里(进程)
//PostMessage和SendMessage在这里
8)各个进程(窗口线程)处理消息

在第6步那,如果挂上一个钩子,那么理论上所有常规的按键消息就都能收到了,当没有按键消息的是时候,这个钩子函数是不会被调用和执行的,所以必然也不会占用CPU。

同样的道理也适用于其它钩子:事件未发生,钩子未被执行,所以不占CPU。 Don't call me, I will call you......... 补充一下,事实上中断是必要条件,但是不充分。试想如果除了时钟没有任何中断源发生事件,CPU还是在运行的。区别是这个时候可以把它设置为低功耗状态。 想起来以前单片机里面的计时器延时和硬代码延时
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1422
52
Laravel 教程
1316
25
PHP教程
1267
29
C# 教程
1239
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

vscode在哪寫代碼 vscode在哪寫代碼 Apr 15, 2025 pm 09:54 PM

在 Visual Studio Code(VSCode)中編寫代碼簡單易行,只需安裝 VSCode、創建項目、選擇語言、創建文件、編寫代碼、保存並運行即可。 VSCode 的優點包括跨平台、免費開源、強大功能、擴展豐富,以及輕量快速。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

See all articles