再谈Linux服务端编程

Posted wx62bd5b9ca9fc1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再谈Linux服务端编程相关的知识,希望对你有一定的参考价值。


本作品采用​​知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议​​进行许可。

本作品 (李兆龙​ 博文, 由 李兆龙​ 创作),由 李兆龙 确认,转载请注明版权。

文章目录

引言

存在于想象中首届卷心菜之夜的talk题目,但是我更愿意称其为上一次思考[1]的续集。

这篇文章其实就是想搞清楚Linux服务端编程中几个基本问题,也提出了几个以前让我疑惑的问题,当然以后让我疑惑的问题以后也会平等对待,文章其实只是我笔记中的拷贝而已,我实在是不想花大精力做这样其实没什么意义的事情,遂随心所欲,随便敲打了。

我开始接触计算机时把功夫放在OS,网络上,年少轻狂,区区一年多的学习就开始沾沾自喜,自认为功力深厚,可能是虚荣心使然,也可能是韦伯-费希纳定律做祟,开始摆弄一些看似高大上的玩意,但现在看来不过是无知带来的快感,虚无而已。

思考最近做的事情,貌似我又回到了开始接触计算机时,每每这样思考,我都会感叹,老天啊,让我回到四年前吧!我一定重学数学,历史,哲学,心理学!当然计算机基础也不要落下。

别急着反驳,我想说dog250总是对的,这种按该死的高考学科把人划分为不同群体的做法,打碎了生活的张力,既然高考已经把人打成偏瘫了,步入社会后为什么还要继续把另一半身体也摧残?这也是我的timeline中除了blog,paper,代码以外总有那么一两本非计算机书籍的原因。

socket

简单聊聊我关心的事情,当socket执行完时:

  1. 首先通过​​sock_alloc​​​从​​sock_fs​​​类型的文件系统的超级块分配了一个​​inode​​​,利用​​SOCKET_I​​​取到​​socket_alloc​​​其中的​​socket​​结构。
  2. socket结构中的sock被​​inet_create​​​填充为​​tcp_sock​​。
  3. 其中​​socket->sock->sk_state == TCP_CLOSE​​。
  4. 返回给用户fd,此时​​file​​​已经和​​socket​​结构连接在一起了。
  5. ​sock_init_data​​​还是在​​sock​​​这一级别做初始化,在​​inet_create​​​中​​inet_sock​​​也有部分数据被初始化,在​​tcp_v4_init_sock​​​中对​​inet_connection_sock​​​和​​tcp_sock​​​做一些初始化,实际的初始化函数是​​tcp_init_sock​​。
  6. 此时我们确定了协议(传输层​​protocol​​​,控制层​​family​​​)和套接字类型​​type​​。[4]

bind

总结下port的分配遵循如下规则:

  1. 先把​​local_port_range​​划分为上下两部分
  2. 第一次遍历[half,high]部分中的奇数
  3. 然后[half,high]部分中的偶数
  4. 然后[low, half]部分中的奇数
  5. 然后[low, half]部分中的偶数
  6. 使用​​inet_csk_bind_conflict(sk, tb, false, false)​​​判断是否出现冲突,这建立在​​net namespace​​​和​​port​​​确定时判读是否可以​​reuse​​。

  1. 如果 bind 时不指定端口,那系统会怎么挑选端口?
    a.​​​inet_csk_find_open_port​
  2. 如果指定的端口被占用了,系统会不会强制使用?
    a. 与​​​reuse​​​和​​reuseport​​设置有关
  3. 内核如何保存所有 socket 连接?怎样做到高效的冲突检测?
    a.​​​bhash​

再聊聊我关心的事情,当​​bind​​执行完时:

  1. ​addr->sin_addr.s_addr​​​ 和​​addr->sin_port​​​ 被赋值给​​inet_sock​
  2. 不处于CLOSED,或是此套接字己经绑定端口则不能被绑定
  3. 当然​​sin_port​​​可能为0,此时需要内核负责找到一个合适的端口尝试绑定
    a. 查看​​​inet_csk_find_open_port​​部分
  4. 当找到一个合适的端口后,把这个​​socket​​​加入​​bhash​​​中
    a.​​​bhash​​​的​​index​​​是用​​net/port​​​做哈希的,一个​​port​​​要被加入​​bhash​​​的链表时需要下面两步判断
    b. 地址不相同且允许端口重用时返回成功,逻辑在​​​sk_reuseport_match​​​中
    c. 地址相同时在此port上现有的套接字做冲突检测,其中会检查地址和port的冲突,需要检查​​​reuseport​​​,逻辑在​​inet_csk_bind_conflict​​中
  5. 触发这两个​​BPF_CGROUP_INET4_POST_BIND​​​/​​BPF_CGROUP_INET4_BIND​​ cgroup attach点。

listen

其实看下来listen做的事情就比较少:

  1. listen到底是在干什么?
    a. 把fd的对应的socket对象放入到​​​listen_hash​​​和​​lhash2​​​,在reuseport的情况下初始化或者加入到已有的​​sk_reuseport_cb​​。
  2. 第二个参数
    a. 目前看唯一的作用就是修改​​​sk_max_ack_backlog​​,而且这个参数是可以动态修改的。
  3. listening_hash / lhash2
    a. 保存监听状态的套接字,前者使用​​​port​​​做哈希,后者使用​​port+addr​​​做哈希,现在在​​tcp_v4_rcv​​​中只在​​lhash2​​​中查找套接字,而​​listening_hash​​​目前看源码应该只用在了​​proc​​​和​​inet_diag_dump_icsk​

accept / accept4

  1. accept()函数的实现,陷入睡眠,等待被唤醒处理全连接队列中的数据
  2. accept()函数如何如何被唤醒
    a. 信号,noblock,全连接队列本身就有值,三次握手的ACK到达时唤醒对应套接字等待队列上的第一个线程。当然每​​​sk_sndtimeo​​间隔后才会检查信号
  3. accept()函数如何解决惊群
    a. 只唤醒等待队列上的一个entry,以此避免惊群。
  4. 多个进程accept(),优先唤醒哪个进程
    a. 内核只会唤醒1个等待的进程,唤醒的逻辑是FIFO,这部分代码在​​​sock_def_readable​​[13]。
  5. 在收到SYN和ACK的时候都会检查​​sk_max_ack_backlog​​​,所以​​SOMAXCONN​​参数其实影响了这两步

connect

client:

  1. 根据下一跳地址查找目的路由的缓存项​​ip_route_connect​
  2. 在​​inet_hash_connect​​​中做三件事情
    a. 选择​​​ephemeral port​​​,​​port​​​的选择偏向于偶数
    b. 创建​​​inet_bind_bucket​​​并加入​​bhash​​​,这里注意同一IP端口不能多次建立连接
    c. 将​​​tcp_sock​​​加入​​ehash​
  3. 调用​​tcp_connect​​​发送SYN
    a.​​​tcp_connect_init​​​做所有可以独立于 AF 的连接套接字设置
    b.​​​sk_stream_alloc_skb​​​分配skb,这里有一个​​sk_tx_skb_cache​​​的优化
    c. 调用​​​tcp_transmit_skb​​传输这个SYN数据包,其中设置SYN包头的数据,包头数据在线性区
  4. ​inet_wait_for_connect​​​中把当前线程加入sk的等待队列,调用​​tcp_rcv_synsent_state_process​​​等待SYN+ACK,如果没有延迟确认(​​defer_accept​​​)机制的话就会调用​​tcp_send_ack​​向对端发送ACK,这里第三次握手数据包中可以看到也没有payload。
  5. 在​​tcp_rcv_synsent_state_process​​​中处理SYN+ACK
    a. 收到RST报文的时候会关闭套接字
    b. 根据数据包中的数据设置窗口,发送队列seq,时间戳相关数据
    c. 如果启用了连接保活,则启用连接保活定时器
    d. 连接建立完成,如果没有设置​​​defer_accept​​​的话直接发送​​ACK​

server:
调用栈为​​​tcp_v4_rcv()​​​->​​tcp_v4_do_rcv()​​​->​​tcp_rcv_state_process()​​​->​​tcp_v4_conn_request()​​​->​​tcp_conn_request()​​​->​​tcp_v4_send_synack()​

  1. 收到 SYN segment,从​​listening_hash​​​找到​​listen tcp_sock​​​,创建​​tcp_request_sock​​​(​​NEW_SYN_RECV​​​状态),并将其加入​​ehash​​​,然后发送​​SYN+ACK​
  2. 收到 ACK segment,从​​ehash​​​中找到​​tcp_request_sock​​​,从​​req->rsk_listener​​​找到​​listen tcp_sock​​​,创建新的​​tcp_sock​​​(​​SYN_RECV​​ 状态)
  3. 状态设置为​​TCP_ESTABLISHED​​​,接下来把​​tcp_request_sock​​​加入​​listen socket​​​的​​accept queue​
  4. 在​​accept​​​的时候也会隐式调用​​tcp_v4_rcv​

总结

不要说话

参考:

  1. 对Linux服务端编程的一点浅薄理解
  2. ​为什么Linux作为客户端的情况下不支持端口共用?​
  3. 费希纳定律的推导过程图解


以上是关于再谈Linux服务端编程的主要内容,如果未能解决你的问题,请参考以下文章

再谈 JavaScript 异步编程

再谈前后端分离丨趣店前端团队基于 koajs 的前后端分离实践

linux多线程服务端编程 看啥书

如何看懂《Linux多线程服务端编程

Linux从青铜到王者第十七篇:Linux网络基础第二篇之UDP协议

在Linux系统如何让程序开机时自动启动