V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
LeeReamond
V2EX  ›  问与答

请问一些操作系统基础,线程能够进入等待/阻塞状态的底层原理是什么?

  •  
  •   LeeReamond · 2021-03-26 11:26:19 +08:00 · 2609 次点击
    这是一个创建于 1367 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,学习 Go 的过程中认识到管道这种进程间通信方式,感觉确实是比直接共享内存更合适的线程间同步模型。

    学习过程中产生一些问题,涉及到语言如何和操作系统相关联

    1 、管道通常有一个阻塞读取功能,比如消费者调用 recv()后进入阻塞,然后由生产者向管道内丢入一个数据,消费者就可以解除阻塞并获取数据,相当于被唤醒,这在语言层面上是如何实现的呢,我们在业务代码中似乎没有办法显式地让一个线程进入阻塞,然后再用另一个线程去 trigger 这个线程。

    2 、这种操作非常类似 socket 表现的性状,但我觉得怎么也不会用 socket 实现管道吧,我觉得对于统一进程内间的线程同步,socket 是一种非常昂贵的通信手段。

    3 、我们是否有办法显示地管理线程,比如子线程主动进入 block,而后主线程主动唤醒子线程,这种管理是否是可以实现的。类似地,还有主动地关闭一个线程的需求,不过据我所知线程本身似乎是并未设计在外部干预下关闭的这么一个功能。

    4 、类似地,java 语言中还有一个 wait 的状态,与 block 不同,wait 通常是指等待锁,而不是等待 IO,其他语言可以释放锁并唤醒一个 wait 线程,这与 block 又有什么区别呢? block 和 wait 的概念是 java 的概念还是操作系统的概念?

    问的比较乱,基础不扎实,有了解的朋友请帮忙回复一下,谢谢大家。

    第 1 条附言  ·  2021-03-26 17:18:53 +08:00
    感谢大家回复,重读了一遍原贴,发帖时未仔细校验有一些言语上的误导。这个问题是我在阅读 go 通道时想到的,但是我本意不是想讨论协程的问题,因为协程调度不出让控制权,不涉及这些。我想讨论的是例如 java 中多线程的情况,还有例如 rust 当中借鉴了 go 的管道实现线程通信,那么这种时候就涉及到在多线程模型下如何实现 A 线程通过管道唤醒 B 线程的问题了。另外第四点中提到的 wait 都指闲等待,不指忙等待,是我疏忽了。
    28 条回复    2021-03-28 00:44:57 +08:00
    dongcidaci
        1
    dongcidaci  
       2021-03-26 12:12:00 +08:00
    楼主可以思考的很深入
    learningman
        2
    learningman  
       2021-03-26 12:15:37 +08:00 via Android
    所有的等待本质都是轮询,如果你没有操作这个,说明语言或者系统已经帮你完成了。
    lcdtyph
        3
    lcdtyph  
       2021-03-26 12:30:02 +08:00 via iPhone
    操作系统会提供一些原语来支持条件唤醒,比如 linux 下的 wait_queue 或者用户台的条件变量

    wait 实际上可以分成 blocked wait 和 busy wait,后者不会使线程让出时间片。wait 只表示等待一个事件的出现,事件可以是 IO 就绪,锁被释放,信号 /中断到来等等,而等待的方式则可以是 blocked 或者 busy 的。
    lcdtyph
        4
    lcdtyph  
       2021-03-26 12:32:59 +08:00 via iPhone
    @learningman
    现代硬件支持非忙等待,比如中断形式,当内核没有要执行的指令的时候可以执行 mhalt 让 cpu 进入睡眠状态,从而等待下一个事件到来再唤醒内核
    sujin190
        5
    sujin190  
       2021-03-26 12:36:49 +08:00
    谁说不能让一个线程显式进入阻塞的,否则多线程中锁、信号量啥的是用来干嘛的,go 这种和线程其实不是一回事,想知道线程阻塞调度啥的,那就好好看多线程编程啥的比较靠谱
    unixeno
        6
    unixeno  
       2021-03-26 12:38:07 +08:00 via Android
    chan 的底层其实就是个锁+缓冲区(buffered chan),你可以去看下 golang 的 channel 底层实现
    你用互斥锁一样可以实现你说的这种 a 线程 trigger b 线程的效果
    zmxnv123
        7
    zmxnv123  
       2021-03-26 12:47:14 +08:00 via iPhone
    所有的自旋锁例如 java 的 wait 到都操作系统层级都是一个汇编指令,忘了叫什么了
    楼主可以学一下 6.828
    OysterQAQ
        8
    OysterQAQ  
       2021-03-26 13:10:19 +08:00 via iPhone
    看一下操作系统的线程部分,线程是操作系统中一个最基本的抽象,你说的调度是操作系统的功能,关于线程切换的问题就涉及到线程的实现了,有内核实现,也有用户空间实现的,由进程来管理,也有折中混合的
    baiyi
        9
    baiyi  
       2021-03-26 13:23:36 +08:00
    Go 由于有自己的调度模型,所以它的 channel 阻塞不是依赖于线程的。而是由 Go 自己的运行时来保存 goroutine 的上下文,然后等待唤醒。

    线程的并发可以看看 CSAPP 的第十二章 并发编程
    zhongrs232
        10
    zhongrs232  
       2021-03-26 13:55:32 +08:00
    Go 只有协程吧,Go 的协程和操作系统的线程是不一样的。不了解 Go 协程,但用 C 实现过一个协程库,我可以回答一下 Linux 下协程的切换原理,简单来说,可以将协程看成某个函数,每个协程(函数)在执行时都有一个上下文,用于保存协程执行时的状态,比如栈地址,PC 指针,返回地址等,这个上下文在 Linux 里面可以用 ucontext_t 结构体来表示。协程切换时,会先将自己的 ucontext_t 保存起来,然后调度器会根据调度策略找到另一个协程保存好的 ucontext_t,根据这个 ucontext_t 可以完全复原另一个正在运行的协程。这就是两个协程切换的原理。
    mrgeneral
        11
    mrgeneral  
       2021-03-26 14:35:37 +08:00
    Go 有自己的调度器,协程的执行在底层也是线程在运行,主是由调度器来决定线程运行哪个协程,协程自己也可以让出线程资源。

    你提到的关于阻塞调用如果是阻塞 IO,涉及到系统调用涉及到内核通信,都需要 Linux 底层 IO 模型( epoll ),为了感知这类调用,Go 把系统调用封装了一层,也就是协程只能通过一个系统调用中间层来实现系统调用,从而达到调度器 hook 的目的,最后的链路就是:协程->中间层->操作系统;操作系统->中间层->唤醒协程。
    mrgeneral
        12
    mrgeneral  
       2021-03-26 14:38:17 +08:00
    @mrgeneral 协程的沉睡和唤醒,就是调度器是否给他分配运行中的线程资源
    nevin47
        13
    nevin47  
       2021-03-26 14:42:49 +08:00
    @learningman #2 别误导人。。。。。
    Jirajine
        14
    Jirajine  
       2021-03-26 14:45:51 +08:00 via Android
    wanguorui123
        15
    wanguorui123  
       2021-03-26 15:03:00 +08:00
    可以了解下进程管理,等待 /阻塞本质上是进程调度的不同状态
    IsaacYoung
        16
    IsaacYoung  
       2021-03-26 15:03:01 +08:00
    test and set
    learningman
        17
    learningman  
       2021-03-26 16:26:50 +08:00   ❤️ 1
    @nevin47 #13 一般这种发言后面应该接上正确的纠正,否则和口嗨有什么区别呢?
    Jooooooooo
        18
    Jooooooooo  
       2021-03-26 16:44:40 +08:00
    @nevin47 定时器当然都是轮询, 2l 说的没问题.

    最后都是操作系统底层有个轮询帮你完成了定时.
    hfc
        19
    hfc  
       2021-03-26 17:03:15 +08:00
    4 、Java 里面的 wait 并不是单纯的指等待锁,而是因为某些原因(比如等待一个资源)而主动放弃 CPU 时间。这个时候线程如果是 wait 状态,那么需要第三方唤醒才能开始竞争 CPU 时间;如果是 time_wait 状态,那么过一段时间后线程就会主动醒来开始竞争 CPU 时间。而 block 状态指的是竞争锁的状态,此时因为锁已经被其它线程获取到了,当前线程只能进入 block 状态但并不会放弃 CPU 时间。
    LeeReamond
        20
    LeeReamond  
    OP
       2021-03-26 17:21:26 +08:00
    @sujin190 我表述有问题,我不是指 java 无法显式进入阻塞,而是我想知道如果翻译成 c/c++代码,这个阻塞是如何实现的,涉及那些操作系统或者硬件级别的操作,我不理解这个过程。
    ydongd
        21
    ydongd  
       2021-03-26 17:24:57 +08:00
    go 中的堵塞不是操作系统上的概念,只是表现结果相似。有自己的 GPM 调度模型,它的堵塞是将当前协程让出运行的系统线程,将自己加入到等待队列,不再执行调度,直到事件到来,条件满足再加入到运行队列等待调度。这里的等待队列示情况而定,比如 channel,就是加入到 channel 的等待队列,接受到数据后将其放入 go 运行的全局队列。实际上 go 中的所有涉及到系统调用的堵塞,都是封装过的,socket 在读数据没有数据时,将 go 程放到 go 实现的 netpoller 中等待,直到数据过来再加入到可运行队列。
    irytu
        22
    irytu  
       2021-03-26 20:14:58 +08:00 via iPhone
    应该是让出 CPU 给其他任务,等待的线程 /进程会被有需要的时候 wake up,比如 IO 中断
    sosilver
        23
    sosilver  
       2021-03-26 21:49:01 +08:00 via Android
    交出运行权(等待),反正要么主动要么被打断。针对 os 抽象的就是通过系统调用被塞入队列,比如你提的 rust channel 的 recv,内部调用 thread::park ( park 更下面通过 libc 等调某个系统调用实现)。轻量些的协程、async/await 等虽然不陷入系统,但是本质还是在某些调用点转移出去(比如.await )。只不过一类由 os 调度,一类由 runtime 调度。
    被打断的情况当然就是中断了。
    akira
        24
    akira  
       2021-03-26 22:06:09 +08:00
    管道 本质上就是文件来的吧,socket 也是一种文件吧
    busymilk
        25
    busymilk  
       2021-03-27 10:53:42 +08:00 via iPhone
    cpu 不调你的线程,不就行了……
    Claar
        26
    Claar  
       2021-03-27 15:04:11 +08:00 via iPhone
    1.要探讨真正的阻塞不应该看 go,因为 go 的阻塞本质应该是非阻塞,只是调度器挂起了任务;大概的过程是调度器发现有个任务想要阻塞等待一个 io,所以调度器直接把任务挂起了,并开始执行下一个任务,在开发者看来和等待没什么区别。
    2.看 linux 的源码可以看到真正的阻塞的代码
    nevin47
        27
    nevin47  
       2021-03-27 15:41:28 +08:00
    @learningman #17
    @Jooooooooo #18
    我想表达有问题的是:“所有的等待本质都是轮询”这个论述,不过昨天确实留了个言就跑了 sorry....

    我对 Go 不是特别熟悉,所以只能用 Linux 内核来举点例子。例如低精度的定时器,是通过 Jiffies 作为单位来的,背后确实就是一个 timer service 去轮询 n 级 timer 链表,然后定时触发回调。这种属于通过 调度器+轮询 来实现的等待(实际上例如低精度的 sleep 库函数这种的,都是类似)

    但是对于 hrtimer,或者某些要做硬实时的地方,我们往往喜欢使用 CPU 的 timer intterrupt,不同的体系结构有略微不同的实现,但是原理都差不多,要靠一个小的模块,例如 TSC/HPET,我没记错的 ARM/X86 都叫这个名字

    以上是我凭记忆写的,不一定十分。更多细节可以参考下《 ULK 》和 ARM/X86 芯片手册的 timer 子章节
    lcdtyph
        28
    lcdtyph  
       2021-03-28 00:44:57 +08:00 via iPhone
    @nevin47
    tsc 是 x86 平台的,是内置在 cpu 里东西,精度极高可以达到纳秒级,而且访问延迟低,在 intel 搞出来 invariant tsc 之后现代 linux 在 x86 平台会默认选择这个作为 clocksource

    hpet 是内置在主板芯片组中的,最开始是 intel 和微软弄出来的,但是精度基本都在最低要求的百纳秒级,而且因为不在 cpu 里面所以访问延迟很高

    arm 上类似 tsc 的东西是某个 pmc 寄存器的值,但是它没有 invariant 的特性所以不能被用作 clocksource,而是需要板载一个时钟模块,然后内核做 mmio 来操作这个外部定时器
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5945 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:39 · PVG 10:39 · LAX 18:39 · JFK 21:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.