举个通俗易懂的例子吧
你去餐馆吃饭,你作为顾客相当于应用程序,餐馆厨房相当于系统内核,服务员相当于内核和应用层之间的接口。
你点好菜写下来,交给服务员,相当于调用内核接口,这时你就阻塞了,一直等待上菜。
服务员把你点的菜交给厨房,相当于给了内核一个请求,这个订单进入了内核的任务队列。服务员没有一直等待这个订单,他就去招待其他客人了。
如果有空闲的厨师,他就会接受一个订单开始做菜,相当于响应请求。
厨师做好了一个菜,就召唤服务员上菜,如果某个服务员有空就会去上菜,相当于请求完成通知。
顾客得到一个菜,一次系统调用完成。
你看,这个过程里,只有顾客是一只在等待的,服务员和厨师一直都在工作,没有死循环。当然聪明的顾客其实也没有死等,他会去刷手机,等服务员上菜,他才放下手机开始吃。
现实世界里都不会有真的死循环。
不搞长篇大论, 简要说一下:
首先:题主没弄清楚“中断”是个什么东西。中断(无论软硬),都是由外部主动发起,进而触发内部某些工作流程的。具体到你这个问题,基本流程就是由网卡接收到数据之后,通过DMA写数据到内存中,然后触发中断通知CPU,CPU再根据中断向量表执行对应的程序,接下来就是kernel代码接管并执行相关操作了(这就是问题中各个“感知”的基础过程)。
然后,一般来说,kernel里面是不存在进程/线程这种概念的。这两概念本身就是os封装/虚拟出来给上层应用的,kernel是跳脱于这个层次之外的。类似的,阻塞/非阻塞之类的概念也是由kernel/lib等提供的。
最后,本质上kernel就是一组庞大的围绕着刚才说的中断向量表工作的程序,和大多数常见的应用程序的工作模式有本质的区别。
充分说明了为什么完整的计算机基础知识是必要的,以及为什么有那么多人难以寸进
因为没有计算机基础知识,所以才有这种想法
以下是操作系统关于这方面会做的事情
网络编程通常要开启一个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调优常用手法之一
大家天天都在说阻塞,实际上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 内核线程才开始执行。来从网卡上取包,处理,放到接收队列,然后唤醒用户进程。
究其根源,是由网卡的硬中断来触发的。如果一段时间内没有网络包处理,那么没有死循环来消耗 CPU 的。
对网络底层还有啥不理解的,来看看我的公众号「开发内功修炼」 或许可以帮你解开一些困惑。
Github: GitHub - yanfeizhang/coder-kung-fu: 开发内功修炼
哦对了,想理解多路复用,来看看我的这一篇吧,也是从源码角度深入分析的。图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!
一句话就能说清楚的事…完美体现了“会者不难难者不会”这句俗话。
很简单:当你调用select或别的什么、暂时被阻塞时,实际上就是操作系统判断你“需要的资源无法满足”,于是就把你的进程放进了“挂起”队列,不给你分配CPU资源了。
那什么时候你能回到就绪队列、重新获得被调度资格呢?
很简单,当网卡或你等待的别的什么硬件发出一个中断,让操作系统知道你需要的资源来了,它自然就把你移出挂起队列,重新给你安排时间片。
从道理来说,早减晚增本身是没啥毛病的,毕竟只是个选项,丰俭由人。
大家怕的是某些人通过这些选项,再加点私货。而且这个说法和推迟退休一起出来,由不得大家多想。
按照目前的舆论情况,如果你敢允许早退减拿,估计只要不在体制内的人就统统早退了,反正也没啥规定领了社保就不能接着打工,对吧?甚至还可以把原来交给社保的那块放自己口袋。
所以,让你早退减拿是不可能的,忽悠大家晚退多拿的可能性比较大。