一、概述
网络包从网卡送到协议栈后,内核还有一项重要的工作,就是要能通知用户进程,让用户进程能够收到并处理这些数据。用户进程和内核的交互一般有两种典型的方案,一种是同步阻塞,另一种是多路IO复用。
二、内核socket对象
三、同步阻塞IO
同步阻塞IO在java中叫BIO,典型的用户进程代码:
//创建socket
//建立connect
//接收数据recv
虽然用户进程代码只有两三行代码,但实际上用户进程和内核配合做了很多的工作。首先是用户进程发起创建socket的指令,然后用户进程切换到内核态完成socket内核对象的初始化,接下来,Linux在数据包的接收上是通过cpu硬中断和内核ksoftirqd线程进行处理,ksoftirqd线程处理完后,再通知用户进程。
同步阻塞IO总体流程如下:
3.1 用户进程等待接收数据
用户进程进行recv系统调用后,用户进程就进入了内核态,到socket对象的接收队列中查看是否有数据,没有的话就把自己添加到socket对应的等待队列里,最后让出CPU,所以这里就能看到用户进程阻塞了。
看一下,socket对象上注册的tcp_recvmsg函数,阻塞了当前进程:
int tcp_recvmsg(struct kiocb *iocb,struct sock *sk,struct msghdr *msg,size_t len,int nonblock,int flags,int *addr_len){
int copied = 0;
......
do{
//遍历接收队列接收数据
skb_queue_walk(&sk->sk_receive_queue,skb){
......
}
......
}
if(copied >= target){
......
}else{
//没有收到足够的数据,调用sk_wait_data阻塞当前进程
sk_wait_data(sk,&timeo);
}
}
3.2 软中断
网卡接收到数据后,发出硬中断通知CPU,最后交由软中断处理,也就是内核ksoftirqd线程去处理。ksoftiqrd线程主要做两件事,一是从RingBuffer取出数据放到socket对象的接收队列,二是唤醒阻塞在socket等待队列上的用户进程。
内核函数sock_def_readable最终调用了default_wake_function,来唤醒用户进程:
// kernel/sched/core.c
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, void *key){
return try_to_wake_up(curr->private, mode, wake_flags);
}
curr->private,这个就是阻塞的用户进程描述符。在多路IO复用技术中也能看到这一步。
四、综述
同步阻塞IO接收网络包整体分为两个部分,每次用户进程为了等socket上的数据而主动陷入阻塞,让出CPU,操作系统调度另外的进程在CPU上运行,等到数据准备好后,阻塞的用户进程又会被唤醒,操作系统重新调度用户进程在CPU上运行,整个过程就产生了两次进程间的上下文切换的开销,并且用户进程在等待数据的时候是陷入阻塞的,所以同步阻塞IO效率很低。