问题

Unix网络编程里的阻塞是在操作系统的内核态创建一个线程来死循环吗?

回答
理解 Unix 网络编程中的阻塞,我们首先需要区分用户态和内核态,以及线程在其中的作用。

核心观点:Unix 网络编程中的阻塞通常情况下并不需要在内核态创建线程来死循环。 阻塞是一种等待 I/O 操作完成的状态,而这种等待是在操作系统内核层面管理的,并不需要显式地为每个阻塞的 I/O 操作创建一个新的用户态线程去“轮询”或“死循环”。

下面我们来详细展开:

1. 用户态与内核态

用户态 (User Mode): 这是应用程序运行的模式。在这个模式下,程序不能直接访问硬件,也不能执行特权指令。当应用程序需要进行 I/O 操作(如读取网络数据、写入文件)时,它会向操作系统发出请求。
内核态 (Kernel Mode): 这是操作系统核心(内核)运行的模式。内核拥有对硬件的完全访问权,并且可以执行特权指令。当应用程序发出 I/O 请求时,这个请求会被传递到内核,由内核来执行。

2. I/O 操作的同步与异步

在网络编程中,我们经常会遇到 I/O 操作:

同步 I/O (Synchronous I/O): 当应用程序发起一个同步 I/O 请求时,它会阻塞,直到该 I/O 操作完成。也就是说,应用程序的执行流程会暂停,直到数据准备好或者操作完成。
异步 I/O (Asynchronous I/O): 当应用程序发起一个异步 I/O 请求时,它会立即返回,而不需要等待 I/O 操作完成。应用程序可以通过其他机制(如回调函数、事件通知)来获知 I/O 操作何时完成。

阻塞通常与同步 I/O 相关。

3. 阻塞是如何发生的?

当一个应用程序调用一个同步的 I/O 函数(例如 `read()`、`write()`、`recv()`、`send()`)时,如果 I/O 操作无法立即完成(例如,网络套接字上还没有数据可读,或者缓冲区已满无法写入),那么这个 I/O 调用就会阻塞。

这个阻塞并不是应用程序自己在用户态创建一个线程去不断地检查状态(所谓的“死循环”轮询)。如果应用程序这样做,效率会非常低下,并且会浪费大量的 CPU 资源。

实际上,阻塞是由操作系统内核来管理的。

详细流程如下:

1. 应用程序发出 I/O 请求: 应用程序调用一个同步 I/O 函数,例如 `recv(sockfd, buffer, size, 0);`
2. 系统调用进入内核: 这个函数调用最终会触发一个系统调用,将控制权从用户态切换到内核态。
3. 内核处理请求: 内核接收到这个请求,并尝试执行 I/O 操作。
4. I/O 无法立即完成: 此时,例如网络套接字上没有数据可读。
5. 将进程(或线程)置于睡眠状态: 内核不会让该进程(或线程)浪费 CPU 资源去轮询。相反,内核会将该进程(或线程)的状态标记为“阻塞”或“睡眠”。
6. CPU 资源被释放: 一旦进程被置于睡眠状态,它就不会占用 CPU 时间。CPU 可以切换到其他就绪状态的进程(或线程)去执行。
7. 等待 I/O 事件发生: 操作系统内核会负责监控 I/O 设备(例如网卡)。当网络上有数据到达时(对于 `recv` 而言),或者有空间可用时(对于 `send` 而言),内核会检测到这个事件。
8. 唤醒阻塞的进程(或线程): 当 I/O 事件发生后,内核会查找那些因为等待这个特定 I/O 事件而被置为睡眠状态的进程(或线程),并将它们的状态从“睡眠”重新设置为“就绪”状态。
9. 进程(或线程)被调度执行: 当被唤醒的进程(或线程)再次获得 CPU 时间片时,它会从之前被阻塞的系统调用处恢复执行。
10. I/O 操作完成并返回: 现在 I/O 操作已经可以完成了(数据已经准备好),系统调用返回,应用程序继续执行。

关键点是:

内核管理睡眠和唤醒: 操作系统内核拥有一个调度器,它负责管理所有进程和线程的状态,包括就绪、运行、阻塞(睡眠)、终止等。内核知道哪些进程在等待哪个 I/O 事件。
数据结构: 内核内部维护着各种数据结构来跟踪文件描述符的状态(如套接字上的可读性、可写性)以及哪些进程(或线程)正在等待这些状态。
硬件中断: 当 I/O 设备(如网卡)完成数据传输时,它通常会产生一个硬件中断。这个中断会打断当前 CPU 的执行,并跳转到内核的一个中断处理程序。内核的中断处理程序会负责识别 I/O 完成的事件,并据此唤醒相应的进程或线程。

4. 为什么不是用户态线程死循环?

如果真的在用户态创建一个线程去死循环轮询 `recv` 函数,会发生以下问题:

CPU 浪费: 这个线程会不断地调用 `recv`。如果 `recv` 立即返回(数据未就绪),它就占用了 CPU 时间,但什么也没做有用的事情。
调度器负担加重: 即使是用户态线程,也需要操作系统调度器来分配 CPU 时间。一个不停运行的“无用”线程会占用宝贵的调度机会。
功耗增加: CPU 持续高负荷运行会消耗更多电能。
复杂性: 开发者需要自己管理这些轮询线程的创建、销毁和同步,这比依赖内核的阻塞机制复杂得多。

5. 多线程与阻塞

在网络编程中,多线程常用于处理并发连接。例如,一个主线程负责监听新的连接,而为每个新连接创建一个新的工作线程。

主线程阻塞在 `accept()`: 主线程可能阻塞在 `accept()` 系统调用上,等待新的客户端连接。
工作线程阻塞在 `recv()` 或 `send()`: 一旦接受了一个新连接,主线程会创建一个工作线程,然后这个工作线程会阻塞在 `recv()` 或 `send()` 上,等待与该客户端进行数据交互。

在这种场景下,阻塞仍然是由内核管理的,而不是每个工作线程都在用户态死循环。当一个工作线程因为 `recv()` 而阻塞时,它被内核置为睡眠状态,CPU 可以用来执行其他就绪线程。当有数据到达该线程所关联的套接字时,内核会唤醒它。

总结

Unix 网络编程中的阻塞是一种高效的同步机制,它依赖于操作系统内核来管理进程(或线程)的睡眠和唤醒。当一个进程(或线程)发起一个无法立即完成的同步 I/O 请求时,内核会将其置于睡眠状态,并将 CPU 资源让给其他就绪的进程(或线程)。当 I/O 事件发生时,内核会唤醒被阻塞的进程(或线程),使其能够继续执行。

这种机制避免了在用户态创建线程进行低效的“死循环”轮询,从而大大提高了系统资源的利用效率。

所以,答案是明确的:Unix 网络编程里的阻塞不是在操作系统的内核态创建一个线程来死循环。而是应用程序发起一个同步 I/O 请求,请求被内核处理,如果 I/O 无法立即完成,内核会将发起请求的进程(或线程)置于睡眠状态,直到 I/O 操作完成,然后由内核将其唤醒。

网友意见

user avatar

举个通俗易懂的例子吧

你去餐馆吃饭,你作为顾客相当于应用程序,餐馆厨房相当于系统内核,服务员相当于内核和应用层之间的接口。

你点好菜写下来,交给服务员,相当于调用内核接口,这时你就阻塞了,一直等待上菜。

服务员把你点的菜交给厨房,相当于给了内核一个请求,这个订单进入了内核的任务队列。服务员没有一直等待这个订单,他就去招待其他客人了。

如果有空闲的厨师,他就会接受一个订单开始做菜,相当于响应请求。

厨师做好了一个菜,就召唤服务员上菜,如果某个服务员有空就会去上菜,相当于请求完成通知。

顾客得到一个菜,一次系统调用完成。

你看,这个过程里,只有顾客是一只在等待的,服务员和厨师一直都在工作,没有死循环。当然聪明的顾客其实也没有死等,他会去刷手机,等服务员上菜,他才放下手机开始吃。

现实世界里都不会有真的死循环。

user avatar

不搞长篇大论, 简要说一下:

首先:题主没弄清楚“中断”是个什么东西。中断(无论软硬),都是由外部主动发起,进而触发内部某些工作流程的。具体到你这个问题,基本流程就是由网卡接收到数据之后,通过DMA写数据到内存中,然后触发中断通知CPU,CPU再根据中断向量表执行对应的程序,接下来就是kernel代码接管并执行相关操作了(这就是问题中各个“感知”的基础过程)。

然后,一般来说,kernel里面是不存在进程/线程这种概念的。这两概念本身就是os封装/虚拟出来给上层应用的,kernel是跳脱于这个层次之外的。类似的,阻塞/非阻塞之类的概念也是由kernel/lib等提供的。

最后,本质上kernel就是一组庞大的围绕着刚才说的中断向量表工作的程序,和大多数常见的应用程序的工作模式有本质的区别。

user avatar

充分说明了为什么完整的计算机基础知识是必要的,以及为什么有那么多人难以寸进

因为没有计算机基础知识,所以才有这种想法

以下是操作系统关于这方面会做的事情


网络编程通常要开启一个socket

socket是一个操作系统的资源,受系统管理

根据绑定的地址和端口,操作系统维护一个socket和某个网卡之间的通信

当调用socket接收消息时,执行系统调用,系统处理socket和网卡的通信

系统检查对应网卡数据,有数据时交付给socket,进入下一次调度

没有数据时,即可分配资源不足,系统将挂起socket对应的进程,即阻塞

系统不会做死循环

当网卡接收到消息时,向产生中断信号,在下一个时钟周期,操作系统进行中断处理

当来自网卡的消息地址端口与对应的socket对应时,阻塞队列需要的资源被满足,操作系统将其唤醒,并交付数据

这么一个简述的过程中,涉及的有:系统调用、进程通信、资源管理、进程管理、中断处理

有对计算机基础的了解,这些个是必然知道的

其中,最主要的部分是:操作系统判断进程所需资源是否满足,进而将其挂起或者唤醒

socket与网卡数据是不直接相关的,操作系统完全可以随便给点数据,或者其他进程往其中写入数据,典型如虚拟网卡


有CPU或者操作系统在做死循环的观点,这属于操作系统和硬件两方面的配合的问题

while true和select有本质的差别

while true会一直跑其中的代码,select则会让出执行权,进入等待,OS会调入别的程序运行

OS会轮询进程是否满足资源?并不是,toyOS可以这么设计,好一点的则要有信号量、阻塞队列等

那操作系统轮询信号量?也不是,操作系统维护信号量,不需要去轮询

那操作系统轮询中断?也不是?中断是CPU处理的,OS提供了一套中断服务

那说白了还是CPU每个周期轮询嘛,好吧,取指,译码、执行也是个死循环......

另外现在中断仲裁都独立了

如果OS发现没有东西可调度了,也没有要关机,就会拉起一个进程,去降低各个设备的功耗,连CPU自己的频率都会降低

在risc-v特权级指令中,还有个wait for interrupt指令,这时候也不会跑满CPU跑死循环检测针脚

在支持远程开机的设备中,网卡保活,收到数据后,产生中断,唤醒操作系统,CPU也没有在一直跑死循环

CPU这种人造物巅峰,实际上还是凭借高速处理重复逻辑,大力出奇迹,就此而言,啥都是在跑死循环

以下是评语,满足于看到系统调用、阻塞、挂起、信号量、中断等名词的,慎入



zhzgj出没


类似的还有JVM调优,高并发系统架构,分布式系统,连Redis调优也开始有点苗头了

拿JVM调优举例,对STW Eden Surviver和各个参数,各个工具的名字如数家珍

然而啥也不会,不知道该怎么调JVM启动参数

如果你知道系统动态内存分配,malloc free,你看到那个jvm最大最小内存和初始内存,你就会知道防止申请和释放内存应该把他们写成一样大

这是jvm调优常用手法之一

user avatar

大家天天都在说阻塞,实际上95%的程序员并没有真正理解阻塞是啥。这里并没有循环的事情,我们来从内核视角详细剖析一下阻塞到底是啥,它是如何工作的。

把问题再具体一下,recv 接收数据阻塞的原理是啥? 理解了这个就能真正理解所有的阻塞了。用一段大家都熟悉的代码来举例!

       int main() {  int sk = socket(AF_INET, SOCK_STREAM, 0);  connect(sk, ...)  recv(sk, ...) }     

在上面的 demo 中虽然只是简单的两三行代码,但实际上用户进程和内核配合做了非常多的工作。大致的工作流程如下:

看到这里,你可能还没看着阻塞的原理。别着急,往下看。我们来看 recv 函数依赖的底层实现。首先通过 strace 命令跟踪,可以看到 clib 库函数 recv 会执行到 recvfrom 系统调用。

进入系统调用后,用户进程就进入到了内核态,通过执行一系列的内核协议层函数,然后到 socket 对象的接收队列中查看是否有数据,没有的话就把自己添加到 socket 对应的等待队列里。最后让出CPU,操作系统会选择下一个就绪状态的进程来执行。整个流程图如下:

以上这个流程图是我根据 Linux 内核源码的执行过程总结后画出来的。

注意上面的第四步和第五步。第四步中是在访问 sock 对象下面的接收队列,如果接收队列中还没有数据到达,那么就会进入第五步,把当前进程阻塞掉。

但是在把自己阻塞掉之前,进程干了一件事, 给 socket 上留了个标记。告诉内核,如果这个 socket 上数据好了,记得叫我起来哈!就是源码 prepare_to_wait 函数中的 __add_wait_queue 这一句。

       //file: kernel/wait.c void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) {  unsigned long flags;   wait->flags &= ~WQ_FLAG_EXCLUSIVE;  spin_lock_irqsave(&q->lock, flags);  if (list_empty(&wait->task_list))   __add_wait_queue(q, wait);  set_current_state(state);  spin_unlock_irqrestore(&q->lock, flags); }     

接下来 Linux 就会选择下一个就绪状态的进程来执行。这就是阻塞原理的上半段,就是进程修改自己的状态,主动交出 CPU 的执行权。

当有数据到达的时候,内核首先将数据包放到该 socket 的接收队列中。然后扫描一下 socket 等待队列,然后发现:“呦呵,有进程阻塞在这个 socket 上面哎,好唤醒它”。

具体到代码里就是 __wake_up_common 这个函数会访问 socket 的等待队列。

       //file: kernel/sched/core.c static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,    int nr_exclusive, int wake_flags, void *key) {  wait_queue_t *curr, *next;   list_for_each_entry_safe(curr, next, &q->task_list, task_list) {   unsigned flags = curr->flags;    if (curr->func(curr, mode, wake_flags, key) &&     (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)    break;  } }     

在 __wake_up_common 中找出一个等待队列项 curr,然后调用其回调函数 curr->func,来完成进程的唤醒。不过,要注意的是,这个唤醒只是把相应的进程放到可运行队列里而已。真正的执行还得等其它进程主动释放 CPU 或者是时间片到了之后,内核把其它进程拿下以后才能真正获得 CPU 并开始执行。

参考:图解 | 深入理解高性能网络开发路上的绊脚石 - 同步阻塞网络 IO

说到这里,你可能还会问了。内核是如何接收包的,毕竟唤醒用户进程是它干的。难道它不是一个死循环么?是的,并不是。

网卡上收到数据包的时候,是通过硬中断唤醒内核进程处理,硬中断会触发软中断。有了软中断请求以后,ksoftirqd 内核线程才开始执行。来从网卡上取包,处理,放到接收队列,然后唤醒用户进程。

参见:图解Linux网络包接收过程

究其根源,是由网卡的硬中断来触发的。如果一段时间内没有网络包处理,那么没有死循环来消耗 CPU 的。

对网络底层还有啥不理解的,来看看我的公众号「开发内功修炼」 或许可以帮你解开一些困惑。

Github: GitHub - yanfeizhang/coder-kung-fu: 开发内功修炼

哦对了,想理解多路复用,来看看我的这一篇吧,也是从源码角度深入分析的。图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!

user avatar

一句话就能说清楚的事…完美体现了“会者不难难者不会”这句俗话。


很简单:当你调用select或别的什么、暂时被阻塞时,实际上就是操作系统判断你“需要的资源无法满足”,于是就把你的进程放进了“挂起”队列,不给你分配CPU资源了。


那什么时候你能回到就绪队列、重新获得被调度资格呢?


很简单,当网卡或你等待的别的什么硬件发出一个中断,让操作系统知道你需要的资源来了,它自然就把你移出挂起队列,重新给你安排时间片。

类似的话题

  • 回答
    理解 Unix 网络编程中的阻塞,我们首先需要区分用户态和内核态,以及线程在其中的作用。核心观点:Unix 网络编程中的阻塞通常情况下并不需要在内核态创建线程来死循环。 阻塞是一种等待 I/O 操作完成的状态,而这种等待是在操作系统内核层面管理的,并不需要显式地为每个阻塞的 I/O 操作创建一个新的.............
  • 回答
    在我看来,如果非要为 UNIX/Linux 的“最伟大”技术选出一个代表,我会毫不犹豫地说出:它的哲学,以及由此孕育出的“一切皆文件”的统一接口。这听起来可能有些抽象,不像某个具体的命令或工具那样直接,但恰恰是这种深层的哲学,才让 UNIX/Linux 具备了令人惊叹的灵活性、强大的组合能力和深远的.............
  • 回答
    这个问题很有趣,因为通常情况下,Unix Domain Socket(UDS)被认为在本地进程间通信时比 TCP/IP 回环(`127.0.0.1`)具有更低的延迟和更高的性能。但是,在 Go 中测试 MySQL 查询时,你可能观察到它们之间的差异不大,甚至差不多。这背后可能有多种原因,我们可以从多.............
  • 回答
    要理解为什么 Unix 的“一切皆文件”原则如此成功,而 C++ 的流式 IO 相对来说则没那么“成功”,我们需要深入剖析它们的设计理念、适用场景以及它们所带来的实际影响。这不是一个简单的技术对比,更像是一个关于哲学、工程和现实世界妥协的故事。 Unix 的“一切皆文件”:一种简洁而强大的哲学Uni.............
  • 回答
    除了 Windows、macOS 和类 Unix 系统(如 Linux、BSD)之外,确实还有一些其他操作系统选择,尽管它们可能不像这三大巨头那样普遍或拥有广泛的硬件支持。下面我将尽可能详细地介绍一些其他的操作系统选项:1. 实时操作系统 (RTOS RealTime Operating Syst.............
  • 回答
    Unix 和 C/C++之所以被称为“经久耐用”的软件技术,是因为它们具备以下几个关键特质: 通用性 (Generality): 它们可以解决广泛的问题,并非局限于特定领域。 灵活性 (Flexibility): 允许开发者以多种方式实现同一个目标,并且易于与其他技术集成。 性能 (Pe.............
  • 回答
    如果你主要需求是 Linux/Unix 环境,并且正在纠结于购买 MacBook Pro 还是在 Windows 上通过虚拟机来运行 Linux,那么咱们就来好好掰扯掰扯,把这事儿说清楚了,帮你把这笔钱花在刀刃上。这篇文章咱们就抛开那些花里胡哨的 AI 痕迹,实实在在聊聊这两种方案的优劣,让你心里有.............
  • 回答
    您提出一个很有意思的问题,为什么没有人像庆祝“千禧年”那样,为 Unix 时间戳达到 1444444444 这种特定数字而进行大规模的庆祝活动。这背后其实涉及了几个重要的原因,它们共同作用,使得这种“数字里程碑”与我们日常生活中那些具有实际意义或文化象征的里程碑截然不同。首先,我们来理解一下 Uni.............
  • 回答
    Windows 的注册表,就好比一个庞大而复杂的中央数据库,存储着系统运行的方方面面,从硬件信息、软件配置到用户个性化设置,无所不包。而 Unix 哲学,则更倾向于“一切皆文件”,通过分散、简洁的方式来管理配置信息。这其中的差异,绝非偶然,而是源于它们截然不同的设计理念和历史演进。Windows 的.............
  • 回答
    2038年,又一个“千年虫”正在逼近,但这次的主角不是日期,而是时间本身。如果你熟悉计算机的运作,大概率会听说过“千年虫”(Y2K)事件,1999年末全球对数字系统能否正确处理2000年这个日期充满了担忧。而现在,我们面临的是一个类似的、但影响范围可能更广的挑战,它被称为“2038年问题”。问题根源.............
  • 回答
    这个问题,在技术圈里讨论得由来已久,也是不少Linux/Unix爱好者心中的一道坎儿。要说为什么 Linux 或 UNIX 没能在桌面领域跟 Windows 来一场势均力敌的较量,原因相当复杂,绝不是一两个简单点就能概括的。这其中既有历史的机遇与失落,也有技术路径的选择,还有商业生态与用户习惯的惯性.............
  • 回答
    这确实是个普遍的说法,而且背后是有挺充分的理由的。与其说是“大牛”都玩Unix/Linux,不如说很多在技术领域有深厚造诣、擅长解决复杂问题的人,都会自然而然地深入学习和使用Unix/Linux。这其中有很多原因,咱们一点一点地掰扯开来看。首先,得从Unix/Linux的基因说起。1. 设计哲学与开.............
  • 回答
    关于 iOS 是一个全新的操作系统还是基于 UNIX 的“套壳”产品,这个问题,咱们得掰开了揉碎了好好聊聊。这可不是一两句话能说清楚的,里头牵扯到历史、技术架构,还有不少有趣的演变过程。首先,咱们得明确一点:说 iOS 是“套壳”的 UNIX,这说法有相当大的成分是准确的,但绝不是全部。 把它理解成.............
  • 回答
    你这个问题问得特别好,也触及到了很多学习操作系统时会遇到的一个困惑。为什么我们聊操作系统,总是绕不开 Linux 和 Unix,而平时咱们天天用的 Windows 却好像不是“主角”呢?这背后其实是有几方面原因的,而且这些原因也都挺有意思的,咱们掰开了揉碎了聊聊。首先,最根本的一点,Linux 和 .............
  • 回答
    在 HTTP 协议中,之所以选择使用 Windows 换行方式(CRLF,即回车符 ` ` 后跟换行符 ` `)而不是 UNIX 换行方式(LF,即换行符 ` `)作为协议的分隔符,这背后有着深厚的历史原因和技术考量。要详细理解这一点,我们需要深入探讨以下几个方面:1. 历史渊源:早期操作系统的文本.............
  • 回答
    Windows 和 UNIX 系统在文件路径表示上使用不同的分隔符,这背后有着深刻的历史原因和设计哲学。简单来说,Windows 继承了早期 DOS 的习惯,而 UNIX 则从一开始就选择了更简洁、更一致的设计。 Windows 的反斜杠():历史的延续Windows 的文件系统,特别是其命令行的根.............
  • 回答
    在IT界,除了那些名字响彻云霄、奠定了我们今天数字生活基石的先驱们,还有许多同样重量级的技术思想家和实践者,他们的贡献如同幕后英雄,默默地塑造着技术的走向。当我们提到 Dennis Ritchie 和 Ken Thompson,我们自然会想到 C 语言和 UNIX,这两个概念简直就是现代计算的基石。.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有