首先大致问题如上,有以下两个疑问
#define __wait_event(wq, condition)
do {
DEFINE_WAIT(__wait);
for (;;) {
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);
if (condition)
break;
schedule();
}
finish_wait(&wq, &__wait);
} while (0)
然后就没有其它资料了,可以看到里面有一个死循环,所以底层是会用循环来判断是否满足条件?(感觉并没有到底层)那么这也是变相消耗系统资源了啊,有点像回调
2. 如果底层是使用循环来判断,那么例如定时器,操作系统是如何管理时间的?如果也是有循环,那么时间粒度是多少,1 毫秒?如果不是,如何判断时间到达?
感觉最底层一定不是我所猜测的这样,应该是有特殊的数据结构和方法。最后,不要说什么操作系统内核完成,我就是想学习一下原理,就算是汇编也想研究下,有懂的大佬能解下惑吗?或者给几个相关技术名词。
1
robot1 2019-12-26 10:36:10 +08:00
操作系统调度没有这么蠢 阻塞操作会导致任务调度器挂起进程,接下来当有中断或者信号时,再恢复进程。
简化版的任务调度可以研究一下各语言的协程实现 想要知道的更细可以看操作系统原理啊,大而全,专而深 |
2
x1596357 2019-12-26 10:37:51 +08:00
可以看看 Linux 内核调度器是怎么实现的和 yield 系统调用,执行 yield 后调度器选择另一个程序进行运行,并将当前程序加入调度等待队列。阻塞有阻塞队列,失去阻塞条件的时候被入调度等待队列。 至于调度时间粒度,看调度器设置,1ms 也有 100ns 也有,甚至还有 NoHz 调度器。固定时间的调度器由时钟中断驱动。
|
3
xiadong1994 2019-12-26 10:40:58 +08:00 via iPhone
schedule() 就是 call 调度器切换到其他进程,本进程就进到某个 event 的等待队列里去不会再被调度。event 发生以后 kernel 可能会唤醒一个或多个在等待的进程,唤醒其实就是把进程移到可执行的进程队列里面等待调度。
|
4
squancher OP @robot1 主要疑惑点是如何恢复的,就算是通过信号来通知,那么如何判断何时条件满足,判断条件满足这个过程是什么,像 socket 一样,流来了就唤醒,要做到实时,他是怎么判断的,是不断循环吗?
|
6
xiadong1994 2019-12-26 10:48:08 +08:00 via iPhone 1
@squancher 对于网络流这种 IO 操作是靠硬件中断来的
|
7
Michaelssss 2019-12-26 10:48:41 +08:00 1
。。。CPU 不会停下来,只会不停的运行,只不过 CPU 的时间分配在各个进程 /线程之间,你可以认为,只要电脑开着,必然 CPU 是不断 FOR 循环
|
8
augustheart 2019-12-26 10:50:56 +08:00 1
粗糙地来说,就是相当于(相当于)用 sleep 一直等待,就是我们自己经常会写的那种。
并不是真正的不消耗资源,而是消耗的资源可以忽略不计。这也是编程中 no magic 的一个例证。 至于时间粒度,基本不需要考虑,在 windows 下我用 GetTickcount 计时,经常做了一大堆的读取操作(而且还大量执行内核操作)后得到的时间间隔为 0,对现代 cpu 来说,1 毫秒是相当之久的,现在 cpu 比我们想象的快很多。 |
9
squancher OP |
10
hehheh 2019-12-26 10:55:43 +08:00
interrupt 啊,原来上微机原理课讲过。
|
11
codehz 2019-12-26 10:57:10 +08:00 via Android 1
其实了解一些基本硬件提供的机制就可以理解这个问题了(当然要具体实现那是另一个问题)
首先我们知道硬件有中断,其中包括时钟中断和 IO 中断(和别的) 于是我们可以做什么呢,时钟中断可以用来切换运行中的线程(抢占式调度) 对应的 IO 中断就可以用来唤醒由于特定 IO 请求而睡眠的线程了! (所以为什么要阻塞队列?因为有可能同时有好多线程有 IO 请求,总得设计一个结构去保存吧,然后请求来了就扫描一下看看谁可以被唤醒做事) 当然具体实现要复杂的多,比如 IO 中断怎么解析就是一个非常复杂的事情,解析好了也不是立即就唤醒的,除非是无阻塞 poll 模式,不然内核还要帮你把收到的数据填充到缓冲区(以 read 为例) |
12
zivyou 2019-12-26 10:59:34 +08:00 1
阻塞这个词是相对进程的上下文来说的,内核只是将进程挂起,不让它继续参与调度。
|
13
lihongjie0209 2019-12-26 11:11:55 +08:00
不给你调度不就行了? 至于说什么时候唤醒可以是硬件中断, 时钟, 或者是软件中断
|
14
gamexg 2019-12-26 11:16:31 +08:00
google 搜索关键字 操作系统原理 sleep
sleep 大体还是和进程调度在一起的。 需要可以继续搜索 操作系统原理 进程调度。 |
15
nevin47 2019-12-26 11:20:40 +08:00 via Android 1
LZ 有兴趣可以看看 CPU 流水线的概念
你可以比较粗糙的理解,大部分硬件都是电信号的收发器和处理器,电信号的激励是由时钟来整体控制的 对于阻塞任务,在 CPU 上只需要将这部分任务上下文切到挂起的状态,然后任务数据换到内存的某个地方。接下来时钟继续不断刷新电路,可以理解 CPU 上面执行的是其他任务。 当挂起的条件达成的时候,下一个 cycle 刷新了挂起的寄存器,触发了中断,这个时候调度模块会重新切入这段任务,继续执行下去 |
17
nevin47 2019-12-26 11:39:11 +08:00 via Android 7
@squancher 举个简单的例子
在某个神奇的单片机上,CPU 和系统共享一个硬时钟,时钟频率是 1Hz,也就是说一秒钟会从时钟刷新一次电信号 然后我们自己实现了一个 sleep(x)函数,这个函数会把 x 写到某个寄存器 reg_sleep 上,这个寄存器的特性是,每一次电信号刷新,寄存器会减 1(某些定时器的实现和这个类似) 然后我们还存在一个调度器,当每次刷新的时候,调度器如果发现 reg_sleep 的值从 0 变成非 0 了(某些中断的硬实现和这个类似),调度器就会把当前的任务挂起,然后引入新的任务 然后过了 x 秒之后,reg_sleep 的值从非零归位成 0 了,这个时候调度器就把之前挂起的任务,重新切入,继续执行 上面就是一个非常粗糙但是比较简略的 sleep 实现 |
18
zunceng 2019-12-26 11:44:36 +08:00
看内核太费劲 建议把几种 IO 模型了解下 毕竟现在好多被吹成神的软件( nodejs,nginx 等等) 很大的亮点就是应用了 IO 模型中的最优解
|
19
lxk11153 2019-12-26 12:09:50 +08:00
我觉得就是( @Michaelssss #7 )说的循环,其它人说的什么 sleep,调度,中断,信号等等我也整懵了,当然他们是可能出于更加全面理解操作系统这样的目的
@codehz #11 “扫描一下看看谁可以被唤醒做事” 我觉得楼主在问“怎么扫描” 是不是就是循环遍历一下“好多线程”所在的那个结构 @nevin47 #15-“继续不断刷新电路” #17-“调度器如果发现 reg_sleep” 调度器怎么发现?是不是就是循环一遍所有? 或者可以以“暴力密码破解”来理解,就是一个个试,对于系统就是 “循环接着从头循环接着从头循环接着从头循环”。至于循环什么,可能线程就对应线程 list/table(其它类似) 或者可以这样理解: Linux 下“一切皆文件” ,对于操作系统就是循环 ps. 以上我猜的,因为我不了解操作系统 |
20
dongyx 2019-12-26 12:21:59 +08:00 via iPhone
我自己的理解,就是循环。其他同学说的等待标记位变为某个值再唤醒,已经是 CPU+OS 封装之后应用层看到的现象了。操作系统会让进程休眠,但是内核会不断循环读取这个标志位,直到满足条件再把数据填入缓冲区并唤醒进程。
|
21
Michaelssss 2019-12-26 12:29:26 +08:00
@lxk11153
实质上阻塞机制就是 CPU 循环+中断+特定的数据结构 |
23
nevin47 2019-12-26 12:32:40 +08:00
|
24
Michaelssss 2019-12-26 12:32:51 +08:00
@Michaelssss
然后特定的数据结构以 Linux 为例,请看 https://github.com/torvalds/linux/blob/master/include/linux/sched.h 因为 Linux 进程和线程我记得是相同的数据结构 |
25
nevin47 2019-12-26 12:37:49 +08:00
@squancher #22 你要先认识到……CPU 的循环是 Cycle,不是上层语言的 for。
每一次 Cycle 干的事儿可以是一样的,开销是固定的。操作系统是配合中断+轮训来实现的 要是全靠软件循环,计算机早炸了…… |
26
caowentao 2019-12-26 12:44:12 +08:00 via iPhone
条件满足是一个时刻,需要通过采样获得,采样频率越高,获得的时刻越精准,响应实时性越高。循环查询展开是一个时域离散采样过程。
|
30
x1596357 2019-12-26 12:57:45 +08:00 via iPhone 1
@squancher 还是看看操作系统原理处理器调度一章比较好,这样你能系统的知道究竟是怎么工作的。论坛里一人几句也只是碎片的知识。轮询是一种忙等待策略,非常浪费资源。实际使用的是中断来的时候检测条件,比如你等待网络流量,网卡电信号接受到帧,网卡中断,内核解包,发现这个包就是你在等的,唤醒你的进程,然后就处理完了一个包,继续等待其他包。所谓的等待,在用户空间,进程看来,是失去处理器时间,实际上处理器会去执行其他程序。至于中断切换恢复上下文,这也是调度器的工作,保存恢复寄存器状态等。
|
31
GeruzoniAnsasu 2019-12-26 13:04:50 +08:00 via Android
@squancher
> 就是这个问题,上面问过了,怎样判断条件,能做到实时,快速反应 不是“实时”的。 协程你理解吧,协程状态变化不是“实时的”对吧,而是被等待 routine 状态改变的时候调度器把等待进程重新设为可调度而已 进程一样的,操作系统判断被等事件状态可改变时把状态修改(为完成),同时把在等待这个事件的所有进程重新设为可调度,下次调度时就能恢复它们的执行流程 顺带一提,正因为 linux 调度器算法机制不能做到“实时”,所以 linux 并不是一个实时操作系统。RTOS 需要有硬件机制来直接通知内核立即切换进程状态并调度目的进程 |
32
codehz 2019-12-26 13:05:17 +08:00
@squancher 中断是硬件实现,等于强行跳转到另一个执行逻辑(所谓中断处理函数,实际并不是手动调用过去的,而是硬件暴力跳转过去),软件层面无法改变也没法预测
|
33
codehz 2019-12-26 13:09:19 +08:00
判断的过程有非常多的选项,对于同步的系统调用来说,最简单了,就是遍历一个候选列表,然后一个一个判断过去(没错,就是链表)
你以为的实时只是假象,看起来很快,但起码有个几千个时钟周期的“浪费”,根本不是实时的 |
34
squancher OP @GeruzoniAnsasu 你说的我大概理解,我关心的是对于“操作系统判断被等事件状态”这个过程。
|
35
GeruzoniAnsasu 2019-12-26 13:13:26 +08:00 via Android 1
@squancher 假设我们实现一个慢悠悠的,没有优先级的,分片很长的调度方法
那,阻塞这件事就会变成( A 等待 B 进程,系统有 ABCD4 个进程) 1. A 进程进入等待状态,调度器把 A 关联到 B,A 变为不可调度 2. 调度 B 进程,B 进程获得时间片,跑 1 秒,任务没结束。 3. 调度 C 进程,C 进程获得时间片,跑 1 秒 4. 调度 D 进程,D 进程获得时间片,跑 1 秒 5. 调度 A 进程,A 进程不能调度,继续选下一个 6. 调度 B 进程,B 进程获得时间片,跑 1 秒任务结束 7. 调度器发现 B 进程结束了,遍历正在等 B 的进程,发现 A,A 设为可调度 8. 调度 C 进程 9. 调度 D 进程 10. 调度 A 进程,A 进程获得时间片,取得 B 状态,跑 1 秒 11. 调度 B 进程,B 进程所有关联的等待者已不再等待,可以卸载 B 进程 12. 调度 C 进程 …… |
36
GeruzoniAnsasu 2019-12-26 13:14:22 +08:00 via Android
@squancher 内核中有专门结构,进程内会通过 api 来通知内核修改内核对象的值。
|
37
zwhfly 2019-12-26 13:15:35 +08:00 via Android
@squancher 什么是中断?中断是指 CPU 停止执行一段程序,转而执行另一段程序。注意后一句同样重要,中断不仅可以是一个结束,还可以是一个开始。
比如读文件,一个典型的阻塞操作,从用户层到文件系统到驱动,一路会注册等待链,到最后给到 比如 AHCI 控制器,AHCI 控制器是硬件外设,在背后和硬盘间一通骚操作交换数据,这个过程和 CPU 无关,从用户线程到文件系统到驱动啥都不用干(死循环都不用),AHCI 控制器把数据准备好后给 CPU 发中断信号,CPU 收到中断信号就能执行“中断处理程序”,这时候这个中断处理程序就能异步恢复一些东西了,刚才注册的等待链就是响应链,一层一层最终让用户态代码继续执行。 |
38
GeruzoniAnsasu 2019-12-26 13:16:26 +08:00 via Android
|
39
FrankHB 2019-12-26 13:18:57 +08:00
操作系统不需要实现如何实时反应。典型的分时系统反而有延迟,还就不是实时的。
真正需要实时响应的场合由硬件实现保证。 处理器的中断是可以异步触发的,中断服务例程也是硬件原生支持的异步调用。发生中断时,控制如何转移到中断服务例程根本就不用操作系统关心(操作系统看到的就是硬件自己把内部状态改了,接下来执行到了不同的位置)。操作系统需要做的,是在初始化时指定处理中断的逻辑,以及在此之上实现调度器。 |
40
caowentao 2019-12-26 13:19:49 +08:00 via iPhone
1.实时速度有上限,不会超过工作主频。
2.楼主给出的__wait_event 方法已经是操作系统底层实现逻辑了。 3.软件信号事件,查询开销是必须的。 |
41
squancher OP 有点晕了,就能简单说一下定时器吗,Java 定时器 timer,我设定一个时间,操作系统怎么判断时间到了没有,是每毫秒检查一次还是?
|
42
zjsxwc 2019-12-26 13:23:22 +08:00
通过 中断、事件、tick 等方式让操作系统调度呗
|
45
codehz 2019-12-26 13:26:54 +08:00 via Android
@squanche 内核也有线程,定时器会专门开一个线程管理,然后他就可以做一个操作:依次排列各个定时器事件,循环检查第一个定时器有没有触发,然后循环内每次检查后都主动放弃执行绪切到另一个内核线程处理别的事务(
|
46
squancher OP 再拿人举个例子,我现在让另一个人一个小时后通知我一声,然后我要睡觉了,一直等待(中断),但是另一个人开始工作,要么一分钟看下时间,要么五分钟看下时间,时间差不多了就唤醒。
|
47
wamson 2019-12-26 13:29:24 +08:00 via iPhone
@squancher。。。想这么复杂干什么,其实就是 cpu 在每个时钟周期都会去检测某个引脚的电平是高电平还是低电平(这个电平是别的硬件电路传递过来的,比如 IO 事件一般是 DMA 控制器),如果是高电平则表示有中断信号到达,就会保存现场,然后跳去执行中断处理函数,完了恢复现场。
详细请参考<操作系统>,或者看一下<ucos ii 源码> |
49
zwhfly 2019-12-26 13:31:36 +08:00 via Android
中断是 CPU 的一个动作,而不是一个状态。中断是“状态转换”这个动作。
|
50
squancher OP 好吧,综上所述,有人能做个总结吗,这种“判断、检查”是否是快速循环检测,比如按照机器周期的频率,然后这个循环时间粒度可控吗?
|
51
codehz 2019-12-26 13:36:56 +08:00 1
其实不去实现操作系统也不是不能了解这个阻塞调用机制
你可以自己实现一个协程 然后看看不使用系统调用的情况下如何实现 timer (是不是只能一个死循环,最多优化优化检查策略) 然后我这里就做了一个模仿操作系统的协程模型 https://github.com/codehz/ctxco 其中 poller 函数可以理解为系统调用的处理函数(然后自己实现如何将请求异步提交给系统,并返回调度具体协程) |
52
zwhfly 2019-12-26 13:37:24 +08:00 via Android
对于 CPU 来说,中断是跳到另一段程序执行,而不是等待。但对中断前那段代码来说,由于被跳走了,所以逻辑上这段程序在等待,等待操作系统什么时候操纵 CPU 再跳回来。但 CPU 一直是有事干的,从来不闲着,最闲的时候也是循环执行 pause 指令(x86)。
|
53
caowentao 2019-12-26 13:42:36 +08:00 via iPhone
楼主一开始的理解就没有问题,只是对结论持怀疑态度
|
54
justRua 2019-12-26 13:47:43 +08:00
https://www.zhihu.com/question/20785028/answer/378615520
之前在知乎上看到过类似问题,底层也是轮询 |
55
nevin47 2019-12-26 13:48:41 +08:00 via Android
@squancher 47 楼讲的很清楚了
另外如果真的想系统理解,最好配合计算机组成原理和现代操作系统两本书看看。只言片语得理解很容易搞成民科了,你这个问题是软硬件领域的交界,以纯软件的思维去理解是偏的 |
56
squancher OP 这个问题困扰我很久了,就算硬件时钟给 CPU 发中断信号,硬件时钟同样得做时间判断,所以一句话就是能不能不通过循环,机器周期这些方法,有特殊的算法实现
|
57
smdbh 2019-12-26 13:50:49 +08:00
盲狙 wfe()
|
58
ccpp132 2019-12-26 13:52:28 +08:00
主要是硬件有中断机制,从这点出发其实没太大问题。光从软件上来理解是很难的。
|
59
GeruzoniAnsasu 2019-12-26 13:54:20 +08:00 via Android
|
61
zwhfly 2019-12-26 13:57:19 +08:00 via Android
“但是另一个人开始工作,要么一分钟看下时间,要么五分钟看下时间,时间差不多了就唤醒。”这个事不需要 CPU 上跑代码去做。有外部硬件时钟在做,比如最早的 PC 机用的是 MC146818 芯片,可编程,在规定时间向 CPU 发硬件中断信号。
|
63
pmispig 2019-12-26 13:59:55 +08:00
操作系统底层就是中断、轮询和回调咯
|
65
qiangmin 2019-12-26 14:04:26 +08:00 via Android
信号亮,自旋锁,互斥锁等。
|
66
qiangmin 2019-12-26 14:04:58 +08:00 via Android
信号量,自旋锁,互斥锁等。
|
67
zwhfly 2019-12-26 14:11:23 +08:00 via Android
@squancher “就算硬件时钟给 CPU 发中断信号,硬件时钟同样得做时间判断,所以一句话就是能不能不通过循环,机器周期这些方法,有特殊的算法实现”
时序逻辑电路是计算机的基石,可以说一切都是晶振驱动的。 |
68
zwhfly 2019-12-26 14:16:31 +08:00 via Android
对于这些外设硬件芯片来说,受高频时钟信号驱动不停工作是它的常态,是很正常的,功耗寿命等都是设计时保证的。不需要担心。
|
70
zivyou 2019-12-26 14:17:24 +08:00 6
1. linux 内核的 0 号进程,主体就是 C 语言的 for(;;){ schedule()}; 可以参见 linux-0.12 的源码。 同样,内核的调度核心 schedule()函数,是去遍历就绪进程队列(链表的遍历,但是若 list head 就满足,直接就可以返回)。
2. CPU 有一个叫时钟中断的东西。有一个硬件每隔一定时间(非常短,纳秒级)触发上升沿高电平,内核用 jiffies 计数。当然不是每次上升沿都引起时钟中断,通常是 1000 次触发一次时钟中断。也就是说,大约 1ms 一次时钟中断。 3. 在时钟中断的处理逻辑中,内核会调用 schedule()函数,这个函数会取就绪队列的一个进程,交出 CPU。 4. LZ 给出的代码,核心是内核的 workqueue 机制,workqueue 可以接受调度的、线程级: prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); 这个函数的意思是,将自己的 task struct 放入一个等待队列中(就是数据结构放入链表头的操作),这个函数有个参数自己在放入等待队列的时候,将自己的进程状态设置成 TASK_UNINTERRUPTIBLE。进入该状态后,这个进程就不会被 schedule() 函数唤醒了。 后续想要解除这个进程(称为 a) 的阻塞,必须在另一个进程(称为 b)中,b 在一顿操作之后,发现满足了该 workqueue 的 condition, b 就会取挂在该 workqueue 等待队列上的一个进程 task_struct,修改其进程状态,然后调用 schedule()。 5. 比较绕是因为 schedule()函数、workqueue、中断这些概念缠在一起。这几个东西的设计上是非常棒的 大概就是这样,凭我已有的理解写的,细节上可能有偏颇 |
72
zwhfly 2019-12-26 14:31:32 +08:00 via Android
|
75
jsq2627 2019-12-26 15:24:19 +08:00
@zivyou #70 补充,时钟中断只是其中一种方式,此外还有其他中断,例如 IO 中断、断电中断,也都会调起内核 handler
|
78
lewis89 2019-12-26 22:15:50 +08:00
你听这些 v 友讲了这么多 , 还不如读一下 深入理解计算机系统 跟 现代操作系统 的几个特定章节,说实话 这种问题别上论坛问,好好回去读书 才是正道。
|
79
raiz 2019-12-27 10:00:40 +08:00
CPU 中断机制
|
80
imagecap 2019-12-27 14:06:52 +08:00
看看进程调度管理相关资料。个人理解:不管是 sleep 还是 wait,他们的操作都是给当前线程或者进程设置睡眠或者挂起状态,进程调度会根据状态决定哪些该唤醒
|