例如我们想请求服务器上的一段数据,那么C语言的一段代码demo大概是下面这样:
intmain(){intsk=socket(AF_INET,SOCK_STREAM,0);connect(sk,...)recv(sk,...)}
但是在高并发的服务器开发中,这种网络IO的性能奇差。因为
1.进程在recv的时候大概率会被阻塞掉,导致一次进程切换2.当连接上数据就绪的时候进程又会被唤醒,又是一次进程切换3.一个进程同时只能等待一条连接,如果有很多并发,则需要很多进程如果用一句话来概括,那就是:同步阻塞网络IO是高性能网络开发路上的绊脚石!俗话说得好,知己知彼方能百战百胜。所以我们今天先不讲优化,只深入分析同步阻塞网络IO的内部实现。
在上面的demo中虽然只是简单的两三行代码,但实际上用户进程和内核配合做了非常多的工作。先是用户进程发起创建socket的指令,然后切换到内核态完成了内核对象的初始化。接下来Linux在数据包的接收上,是硬中断和ksoftirqd进程在进行处理。当ksoftirqd进程处理完以后,再通知到相关的用户进程。
从用户进程创建socket,到一个网络包抵达网卡到被用户进程接收到,总体上的流程图如下:
我们今天用图解加源码分析的方式来详细拆解一下上面的每一个步骤,来看一下在内核里是它们是怎么实现的。阅读完本文,你将深刻地理解在同步阻塞的网络IO性能低下的原因!
一、创建一个socket开篇源码中的socket函数调用执行完以后,内核在内部创建了一系列的socket相关的内核对象(是的,不是只有一个)。它们互相之间的关系如图。当然了,这个对象比图示的还要更复杂。我只在图中把和今天的主题相关的内容展现了出来。
我们来翻翻源码,看下上面的结构是如何被创造出来的。
//file:net/socket.cSYSCALL_DEFINE3(socket,int,family,int,type,int,protocol){......retval=sock_create(family,type,protocol,sock);}
sock_create是创建socket的主要位置。其中sock_create又调用了__sock_create。
//file:net/socket.cint__sock_create(structnet*net,intfamily,inttype,intprotocol,structsocket**res,intkern){structsocket*sock;conststructnet_proto_family*pf;......//分配socket对象sock=sock_alloc();//获得每个协议族的操作表pf=rcu_dereference(net_families[family]);//调用每个协议族的创建函数,对于AF_INET对应的是err=pf-create(net,sock,protocol,kern);}
在__sock_create里,首先调用sock_alloc来分配一个structsock对象。接着在获取协议族的操作函数表,并调用其create方法。对于AF_INET协议族来说,执行到的是inet_create方法。
//file:net/ipv4/af_inet.ctaticintinet_create(structnet*net,structsocket*sock,intprotocol,intkern){structsock*sk;//查找对应的协议,对于TCPSOCK_STREAM就是获取到了//staticstructinet_protoswinetsw_array[]=//{//{//.type=SOCK_STREAM,//.protocol=IPPROTO_TCP,//.prot=tcp_prot,//.ops=inet_stream_ops,//.no_check=0,//.flags=INET_PROTOSW_PERMANENT
//INET_PROTOSW_ICSK,//},//}list_for_each_entry_rcu(answer,inetsw[sock-type],list){//将inet_stream_ops赋到socket-ops上sock-ops=answer-ops;//获得tcp_protanswer_prot=answer-prot;//分配sock对象,并把tcp_prot赋到sock-sk_prot上sk=sk_alloc(net,PF_INET,GFP_KERNEL,answer_prot);//对sock对象进行初始化sock_init_data(sock,sk);}
在inet_create中,根据类型SOCK_STREAM查找到对于tcp定义的操作方法实现集合inet_stream_ops和tcp_prot。并把它们分别设置到socket-ops和sock-sk_prot上。
我们再往下看到了sock_init_data。在这个方法中将sock中的sk_data_ready函数指针进行了初始化,设置为默认sock_def_readable()。
//file:net/core/sock.cvoidsock_init_data(structsocket*sock,structsock*sk){sk-sk_data_ready=sock_def_readable;sk-sk_write_space=sock_def_write_space;sk-sk_error_report=sock_def_error_report;}
当软中断上收到数据包时会通过调用sk_data_ready函数指针(实际被设置成了sock_def_readable())来唤醒在sock上等待的进程。这个咱们后面介绍软中断的时候再说,这里记住这个就行了。
至此,一个tcp对象,确切地说是AF_INET协议族下SOCK_STREAM对象就算是创建完成了。这里花费了一次socket系统调用的开销
二、等待接收消息接着我们来看recv函数依赖的底层实现。首先通过strace命令跟踪,可以看到clib库函数recv会执行到recvfrom系统调用。
进入系统调用后,用户进程就进入到了内核态,通过执行一系列的内核协议层函数,然后到socket对象的接收队列中查看是否有数据,没有的话就把自己添加到socket对应的等待队列里。最后让出CPU,操作系统会选择下一个就绪状态的进程来执行。整个流程图如下:
看完了整个流程图,接下来让我们根据源码来看更详细的细节。其中我们今天要