从Linux源码看Socket(TCP)的Accept

2025-05-27 0 98

从Linux源码看Socket(TCP)的Accept

前言

笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。今天笔者就从Linux源码的角度看下Server端的Socket在进行Accept的时候到底做了哪些事情(基于Linux 3.10内核)。

一个最简单的Server端例子

众所周知,一个Server端Socket的建立,需要socket、bind、listen、accept四个步骤。

今天,笔者就聚焦于accept

从Linux源码看Socket(TCP)的Accept

代码如下:

  1. voidstart_server(){
  2. //serverfd
  3. intsockfd_server;
  4. //acceptfd
  5. intsockfd;
  6. intcall_err;
  7. structsockaddr_insock_addr;
  8. ……
  9. call_err=bind(sockfd_server,(structsockaddr*)(&sock_addr),sizeof(sock_addr));
  10. ……
  11. call_err=listen(sockfd_server,MAX_BACK_LOG);
  12. ……
  13. while(1){
  14. structsockaddr_in*s_addr_client=mem_alloc(sizeof(structsockaddr_in));
  15. intclient_length=sizeof(*s_addr_client);
  16. //这边就是我们今天的聚焦点accept
  17. sockfd=accept(sockfd_server,(structsockaddr_*)(s_addr_client),(socklen_t*)&(client_length));
  18. if(sockfd==-1){
  19. printf("Accepterror!\\n");
  20. continue;
  21. }
  22. process_connection(sockfd,(structsockaddr_in*)(&s_addr_client));
  23. }
  24. }

首先我们通过socket系统调用创建了一个Socket,其中指定了SOCK_STREAM,而且最后一个参数为0,也就是建立了一个通常所有的TCP Socket。在这里,我们直接给出TCP Socket所对应的ops也就是操作函数。

从Linux源码看Socket(TCP)的Accept

accept系统调用

好了,我们直接进入accept系统调用吧。

  1. #include<sys/socket.h>
  2. //成功,返回代表新连接的描述符,错误返回-1,同时错误码设置在errno
  3. intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);
  4. //注意,实际上Linux还有个accept扩展accept4:
  5. //额外添加的flags参数可以为新连接描述符设置O_NONBLOCK|O_CLOEXEC(执行exec后关闭)这两个标记
  6. intaccept4(intsockfd,structsockaddr*addr,socklen_t*addrlen,intflags);

注意,这边的accept调用是被glibc用SYSCALL_CANCEL包了一层,其将返回值修正为只有0和-1这两个选择,同时将错误码的绝对值设置在errno内。由于glibc对于系统调用的封装过于复杂,就不在这里细讲了。如果要寻找具体的逻辑,用

  1. //注意accept和(之间要有空格,不然搜索不到
  2. accept(int

在整个glibc代码中搜索即可。

理解accept的关键点是,它会创建一个新的Socket,这个新的Socket来与对端运行connect()的对等Socket进行连接,如下图所示:

从Linux源码看Socket(TCP)的Accept

接下来,我们就进入Linux内核源码栈吧

  1. accept
  2. |->SYSCALL_CANCEL(accept……)
  3. ……
  4. |->SYSCALL_DEFINE3(accept
  5. //最终调用了sys_accept4
  6. |->sys_accept4
  7. /*检测监听描述符fd是否存在,不存在,返回-BADF
  8. |->sockfd_lookup_light
  9. |->sock_alloc/*新建Socket*/
  10. |->get_unused_fd_flags/*获取一个未用的fd*/
  11. |->sock->ops->accept(sock…)/*调用核心*/

上述流程如下面所示:

从Linux源码看Socket(TCP)的Accept

由此得知,核心函数在sock->ops->accept上,由于我们关注的是TCP,那么其实现即为

inet_stream_ops->accept也即inet_accept,再次跟踪下调用栈:

  1. sock->ops->accept
  2. |->inet_steam_ops->accept(inet_accept)
  3. /*由一开始的sock图可知sk_prot=tcp_prot
  4. |->sk1->sk_prot->accept
  5. |->inet_csk_accept

好了,穿过了层层包装,终于到具体逻辑部分了。上代码:

  1. structsock*inet_csk_accept(structsock*sk,intflags,int*err)
  2. {
  3. structinet_connection_sock*icsk=inet_csk(sk);
  4. /*获取当前监听sock的accept队列*/
  5. structrequest_sock_queue*queue=&icsk->icsk_accept_queue;
  6. ……
  7. /*如果监听Socket状态非TCP_LISEN,返回错误*/
  8. if(sk->sk_state!=TCP_LISTEN)
  9. gotoout_err
  10. /*如果当前accept队列为空*/
  11. if(reqsk_queue_empty(queue)){
  12. longtimeo=sock_rcvtimeo(sk,flags&O_NONBLOCK);
  13. /*如果是非阻塞模式,直接返回-EAGAIN*/
  14. error=-EAGAIN;
  15. if(!timeo)
  16. gotoout_err;
  17. /*如果是阻塞模式,切超时时间不为0,则等待新连接进入队列*/
  18. error=inet_csk_wait_for_connect(sk,timeo);
  19. if(error)
  20. gotoout_err;
  21. }
  22. /*到这里acceptqueue不为空,从queue中获取一个连接*/
  23. req=reqsk_queue_remove(queue);
  24. newsk=req->sk;
  25. /*fastopen判断逻辑*/
  26. ……
  27. /*返回新的sock,也就是accept派生出的和client端对等的那个sock*/
  28. returnnewsk
  29. }

上面流程如下图所示:

从Linux源码看Socket(TCP)的Accept

我们关注下inet_csk_wait_for_connect,即accept的超时逻辑:

  1. staticintinet_csk_wait_for_connect(structsock*sk,longtimeo)
  2. {
  3. for(;;){
  4. /*通过增加EXCLUSIVE标志使得在BIO中调用accept中不会产生惊群效应*/
  5. prepare_to_wait_exclusive(sk_sleep(sk),&wait,
  6. TASK_INTERRUPTIBLE);
  7. if(reqsk_queue_empty(&icsk->icsk_accept_queue))
  8. timeo=schedule_timeout(timeo);
  9. …….
  10. err=-EAGAIN;
  11. /*这边accept超时,返回的是-EAGAIN*/
  12. if(!timeo)
  13. break;
  14. }
  15. finish_wait(sk_sleep(sk),&wait);
  16. returnerr;
  17. }

通过exclusice标志使得我们在BIO中调用accept(不用epoll/select等)时,不会惊群。

由代码得知在accept超时时候返回(errno)的是EAGAIN而不是ETIMEOUT。

EPOLL(在accept时候)”惊群”

由于在EPOLL LT(水平触发模式下),一次accept事件,可能会唤醒多个等待在此listen fd上的(epoll_wait)线程,而最终可能只有一个能成功的获取到新连接(newfd),其它的都是-EGAIN,也即有一些不必要的线程被唤醒了,做了无用功。关于epoll的原理可以看下笔者之前的博客《从linux源码看epoll》:

  1. https://my.oschina.net/alchemystar/blog/3008840

在这里描述一下原因,核心就是epoll_wait在水平触发下会在这个fd仍有未处理事件的时候重新塞回ready_list并在此唤醒另一个等待在epoll上的进程!

从Linux源码看Socket(TCP)的Accept

所以我们看到,虽然epoll_wait的时候给自己加了exclusive不会在有中断事件触发的时候惊群,但是水平触发这个机制确也造成了类似”惊群”的现象!

由上面的讨论看出,fd1仍旧有事件是造成额外唤醒的原因,这个也很好理解,毕竟这个事件是另一个线程处理的,那个线程估摸着还没来得及运行,自然也来不及处理!

我们看下在accept事件中,怎么判定这个fd(listen sock的fd)还有未处理事件的。

  1. //通过f_op->poll判定
  2. epi->ffd.file->f_op->poll
  3. |->tcp_poll
  4. /*如果sock是listen状态,则由下面函数负责*/
  5. |->inet_csk_listen_poll
  6. /*通过accept_queue队列是否为空判断监听sock是否有未处理事件*/
  7. staticinlineunsignedintinet_csk_listen_poll(conststructsock*sk)
  8. {
  9. return!reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue)?
  10. (POLLIN|POLLRDNORM):0;
  11. }

那么我们就可以根据逻辑画出时序图了。

从Linux源码看Socket(TCP)的Accept

其实不仅仅是accept,要是多线程epoll_wait同一个fd的read/write也是同样的惊群,只不过应该不会有人这么做吧。

正是由于这种”惊群”效应的存在,所以我们经常采用单开一个线程去专门accept的形式,例如reactor模式即是如此。但是,如果一瞬间有大量连接涌进来,单线程处理还是有瓶颈的,无法充分利用多核的优势,在海量短连接场景下就显得稍显无力了。这也是有解决方式的!

采用so_reuseport解决惊群

前面讲过,由于我们是在同一个fd上多线程去运行epoll_wait才会有此问题,那么其实我们多开几个fd就解决了。首先想到的方案是,多开几个端口号,人为分开监听fd,但这个明显带来了额外的复杂性。为了解决这一问题,Linux提供了so_reuseport这个参数,其原理如下图所示:

从Linux源码看Socket(TCP)的Accept

多个fd监听同一个端口号,在内核中做负载均衡(Sharding),将accept的任务分散到不同的线程的不同Socket上(Sharding),毫无疑问可以利用多核能力,大幅提升连接成功后的Socket分发能力。那么我们的线程模型也可以改为用多线程accept了,如下图所示:

从Linux源码看Socket(TCP)的Accept

accept_queue全连接队列

在前面的讨论中,accept_queue是accept系统调用中的核心成员,那么这个accept_queue是怎么被填充(add)的呢?如下图所示:

从Linux源码看Socket(TCP)的Accept

图中展示了client和server在三次交互中,accept_queue(全连接队列)和syn_table半连接hash表的变迁情况。在accept_queue被填充后,由用户线程通过accept系统调用从队列中获取对应的fd

从Linux源码看Socket(TCP)的Accept

值得注意的是,当用户线程来不及处理的时候,内核会drop掉三次握手成功的连接,导致一些诡异的现象,具体可以看笔者另一篇博客《解Bug之路-dubbo流量上线时的非平滑问题》:

  1. https://my.oschina.net/alchemystar/blog/3098219

另外,对于accept_queue具体的填充机制以及源码,可以见笔者另一篇博客的详细分析

《从Linux源码看Socket(TCP)的listen及连接队列》:

  1. https://my.oschina.net/alchemystar/blog/4672630

总结

Linux内核源码博大精深,每次扎进去探索时候都会废寝忘食,其间可以看到各种优雅的设计,在此分享出来,希望对读者有所帮助。

原文链接:https://mp.weixin.qq.com/s/GeHCfU0bu3v-Gx7SaTwNzw

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 从Linux源码看Socket(TCP)的Accept https://www.kuaiidc.com/62847.html

相关文章

发表评论
暂无评论