从Linux源码看Socket(TCP)的accept

前言

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

一个最简单的Server端例子

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

今天,笔者就聚焦于accept。



代码如下:

void start_server(){
// server fd
int sockfd_server;
// accept fd
int sockfd;
int call_err;
struct sockaddr_in sock_addr;
......
call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr));
......
call_err=listen(sockfd_server,MAX_BACK_LOG);
......
while(1){
struct sockaddr_in* s_addr_client = mem_alloc(sizeof(struct sockaddr_in));
int client_length = sizeof(*s_addr_client);
// 这边就是我们今天的聚焦点accept
sockfd = accept(sockfd_server,(struct sockaddr_ *)(s_addr_client),(socklen_t *)&(client_length));
if(sockfd == -1){
printf("Accept error!\n");
continue;
}
process_connection(sockfd,(struct sockaddr_in*)(&s_addr_client));
}
}

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

accept系统调用

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

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

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

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

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

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



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

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

上述流程如下面所示:



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

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

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

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

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
struct inet_connection_sock *icsk = inet_csk(sk);
/* 获取当前监听sock的accept队列*/
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
......
/* 如果监听Socket状态非TCP_LISEN,返回错误 */
if (sk->sk_state != TCP_LISTEN)
goto out_err
/* 如果当前accept队列为空 */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* 如果是非阻塞模式,直接返回-EAGAIN */
error = -EAGAIN;
if (!timeo)
goto out_err;
/* 如果是阻塞模式,切超时时间不为0,则等待新连接进入队列 */
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
/* 到这里accept queue不为空,从queue中获取一个连接 */
req = reqsk_queue_remove(queue);
newsk = req->sk;
/* fastopen 判断逻辑 */
......
/* 返回新的sock,也就是accept派生出的和client端对等的那个sock */
return newsk
}

上面流程如下图所示:



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

static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
for (;;) {
/* 通过增加EXCLUSIVE标志使得在BIO中调用accept中不会产生惊群效应 */
prepare_to_wait_exclusive(sk_sleep(sk), &wait,
TASK_INTERRUPTIBLE);
if (reqsk_queue_empty(&icsk->icsk_accept_queue))
timeo = schedule_timeout(timeo);
.......
err = -EAGAIN;
/* 这边accept超时,返回的是-EAGAIN */
if (!timeo)
break;
}
finish_wait(sk_sleep(sk), &wait);
return err;
}

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

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

EPOLL(在accept时候)"惊群"

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

https://www.cnblogs.com/alchemystar/p/13161781.html

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



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

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

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

// 通过f_op->poll判定
epi->ffd.file->f_op->poll
|->tcp_poll
/* 如果sock是listen状态,则由下面函数负责 */
|->inet_csk_listen_poll /* 通过accept_queue队列是否为空判断监听sock是否有未处理事件*/
static inline unsigned int inet_csk_listen_poll(const struct sock *sk)
{
return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ?
(POLLIN | POLLRDNORM) : 0;
}

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

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

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

采用so_reuseport解决惊群

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



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

accept_queue全连接队列

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



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



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

https://www.cnblogs.com/alchemystar/p/13473999.html

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

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

https://www.cnblogs.com/alchemystar/p/13845081.html

总结

Linux内核源码博大精深,每次扎进去探索时候都会废寝忘食,其间可以看到各种优雅的设计,在此分享出来,希望对读者有所帮助。欢迎大家关注我公众号,里面有各种干货,还有大礼包相送哦!

从Linux源码看Socket(TCP)的accept的更多相关文章

  1. 从linux源码看socket(tcp)的timeout

    从linux源码看socket(tcp)的timeout 前言 网络编程中超时时间是一个重要但又容易被忽略的问题,对其的设置需要仔细斟酌.在经历了数次物理机宕机之后,笔者详细的考察了在网络编程(tcp ...

  2. 从Linux源码看Socket(TCP)Client端的Connect

    从Linux源码看Socket(TCP)Client端的Connect 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的 ...

  3. 从Linux源码看Socket(TCP)的bind

    从Linux源码看Socket(TCP)的bind 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看下Server ...

  4. 从Linux源码看Socket(TCP)的listen及连接队列

    从Linux源码看Socket(TCP)的listen及连接队列 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看 ...

  5. 从linux源码看socket的阻塞和非阻塞

    从linux源码看socket的阻塞和非阻塞 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 大部分高性能网络框架采用的是非阻塞模式.笔者这次就从linux ...

  6. 从linux源码看epoll

    从linux源码看epoll 前言 在linux的高性能网络编程中,绕不开的就是epoll.和select.poll等系统调用相比,epoll在需要监视大量文件描述符并且其中只有少数活跃的时候,表现出 ...

  7. 从Linux源码看TIME_WAIT状态的持续时间

    从Linux源码看TIME_WAIT状态的持续时间 前言 笔者一直以为在Linux下TIME_WAIT状态的Socket持续状态是60s左右.线上实际却存在TIME_WAIT超过100s的Socket ...

  8. mysql-5.5 for linux源码安装

    mysql-5.5 for linux源码安装 1.使用Yum安装依赖软件包 # yum install -y gcc gcc-c++ gcc-g77 autoconf automake bison  ...

  9. 如何从Linux源码获知版本信息

    /*************************************************************************** * 如何从Linux源码获知版本信息 * 声明 ...

随机推荐

  1. sqlserver with(NOLOCK) 或 with(READPAST)

    https://blog.csdn.net/shuicaohui5/article/details/6758868

  2. 3. Spark常见数据源

    *以下内容由<Spark快速大数据分析>整理所得. 读书笔记的第三部分是讲的是Spark有哪些常见数据源?怎么读取它们的数据并保存. Spark有三类常见的数据源: 文件格式与文件系统:它 ...

  3. CSS属性(字体与文本属性)

    1.字体属性 (1)font-family 把要对这个网站要设置的字体都写上,如果这个浏览器支持第一个字体,则会用,如果不支持则会尝试第二个,如果设置的字体系统都不支持则会使用系统默认的字体作为网站的 ...

  4. Javaweb项目页面实时显示后台处理结果

    http://www.cnblogs.com/dong-xu/p/6701271.html 此博文甚好,项目参照博主代码可实现. 前端页面: <%@ page language="ja ...

  5. redis的分布式锁工具LockUtil

    /** * 基于redis的分布式锁工具 * * @author yuyufeng * */ public class LockUtil { // 获取redis static JedisPool j ...

  6. Camtasia中对录制视频进行编辑——交互性

    随着新媒体的广泛发展,视频处理的需要也逐渐变得越来越大,很多人都不知道市场上的哪款软件是比较符合大众需要的.有的软件功能写的天花乱坠,但是实际操作确很难.并不符合大众的简单需求. 今天我便给大家推荐一 ...

  7. CorelDRAW中的合并和群组功能

    在设计过程中,对象就是我们进行操作的主体,这个操作主体可以是单个对象,也可以是多个对象,在有多个对象要进行处理时,就需要用到类似CorelDRAW里的合并和群组这两个功能了. 一.合并与拆分 合并功能 ...

  8. SFTP 服务器cd() 方法和 ls() 方法说明

    方法说明: cd():这个方法用于进入某个目录下. 默认情况,当连接SFTP服务器成功后直接进入用户目录,比如我连接自己本机SFTP服务器后进入/Users/mac目录.cd() 方法进入每一个目录都 ...

  9. Redis多线程原理详解

    本篇文章为你解答一下问题: 0:redis单线程的实现流程是怎样的? 1:redis哪些地方用到了多线程,哪些地方是单线程? 2:redis多线程是怎么实现的? 3:redis多线程是怎么做到无锁的? ...

  10. [LGOJ1273]有线电视网

    solution 用了一个很有意思的转移方法. $dp[i][j] $ 表达 \(i\) 作为根,\(j\)个终端时最大的收益,即钱数,当\(0\leq dp[1][i]\)时,即以1为根可以转移到\ ...