本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
引言
存在于想象中首届卷心菜之夜的talk题目,但是我更愿意称其为上一次思考[1]的续集。
这篇文章其实就是想搞清楚Linux服务端编程中几个基本问题,也提出了几个以前让我疑惑的问题,当然以后让我疑惑的问题以后也会平等对待,文章其实只是我笔记中的拷贝而已,我实在是不想花大精力做这样其实没什么意义的事情,遂随心所欲,随便敲打了。
我开始接触计算机时把功夫放在OS,网络上,年少轻狂,区区一年多的学习就开始沾沾自喜,自认为功力深厚,可能是虚荣心使然,也可能是韦伯-费希纳定律做祟,开始摆弄一些看似高大上的玩意,但现在看来不过是无知带来的快感,虚无而已。
思考最近做的事情,貌似我又回到了开始接触计算机时,每每这样思考,我都会感叹,老天啊,让我回到四年前吧!我一定重学数学,历史,哲学,心理学!当然计算机基础也不要落下。
别急着反驳,我想说dog250总是对的,这种按该死的高考学科把人划分为不同群体的做法,打碎了生活的张力,既然高考已经把人打成偏瘫了,步入社会后为什么还要继续把另一半身体也摧残?这也是我的timeline中除了blog,paper,代码以外总有那么一两本非计算机书籍的原因。
socket
简单聊聊我关心的事情,当socket执行完时:
- 首先通过
sock_alloc 从sock_fs 类型的文件系统的超级块分配了一个inode ,利用SOCKET_I 取到socket_alloc 其中的socket 结构。 - socket结构中的sock被
inet_create 填充为tcp_sock 。 - 其中
socket->sock->sk_state == TCP_CLOSE 。 - 返回给用户fd,此时
file 已经和socket 结构连接在一起了。 sock_init_data 还是在sock 这一级别做初始化,在inet_create 中inet_sock 也有部分数据被初始化,在tcp_v4_init_sock 中对inet_connection_sock 和tcp_sock 做一些初始化,实际的初始化函数是tcp_init_sock 。- 此时我们确定了协议(传输层
protocol ,控制层family )和套接字类型type 。[4]
bind
总结下port的分配遵循如下规则:
- 先把
local_port_range 划分为上下两部分 - 第一次遍历[half,high]部分中的奇数
- 然后[half,high]部分中的偶数
- 然后[low, half]部分中的奇数
- 然后[low, half]部分中的偶数
- 使用
inet_csk_bind_conflict(sk, tb, false, false) 判断是否出现冲突,这建立在net namespace 和port 确定时判读是否可以reuse 。
- 如果 bind 时不指定端口,那系统会怎么挑选端口?
a. inet_csk_find_open_port - 如果指定的端口被占用了,系统会不会强制使用?
a. 与reuse 和reuseport 设置有关 - 内核如何保存所有 socket 连接?怎样做到高效的冲突检测?
a. bhash
再聊聊我关心的事情,当bind 执行完时:
addr->sin_addr.s_addr 和 addr->sin_port 被赋值给inet_sock - 不处于CLOSED,或是此套接字己经绑定端口则不能被绑定
- 当然
sin_port 可能为0,此时需要内核负责找到一个合适的端口尝试绑定 a. 查看inet_csk_find_open_port 部分 - 当找到一个合适的端口后,把这个
socket 加入bhash 中 a. bhash 的index 是用net/port 做哈希的,一个port 要被加入bhash 的链表时需要下面两步判断 b. 地址不相同且允许端口重用时返回成功,逻辑在sk_reuseport_match 中 c. 地址相同时在此port上现有的套接字做冲突检测,其中会检查地址和port的冲突,需要检查reuseport ,逻辑在inet_csk_bind_conflict 中 - 触发这两个
BPF_CGROUP_INET4_POST_BIND /BPF_CGROUP_INET4_BIND cgroup attach点。
listen
其实看下来listen做的事情就比较少:
- listen到底是在干什么?
a. 把fd的对应的socket对象放入到listen_hash 和lhash2 ,在reuseport的情况下初始化或者加入到已有的sk_reuseport_cb 。 - 第二个参数
a. 目前看唯一的作用就是修改sk_max_ack_backlog ,而且这个参数是可以动态修改的。 - listening_hash / lhash2
a. 保存监听状态的套接字,前者使用port 做哈希,后者使用port+addr 做哈希,现在在tcp_v4_rcv 中只在lhash2 中查找套接字,而listening_hash 目前看源码应该只用在了proc 和inet_diag_dump_icsk
accept / accept4
- accept()函数的实现,陷入睡眠,等待被唤醒处理全连接队列中的数据
- accept()函数如何如何被唤醒
a. 信号,noblock,全连接队列本身就有值,三次握手的ACK到达时唤醒对应套接字等待队列上的第一个线程。当然每sk_sndtimeo 间隔后才会检查信号 - accept()函数如何解决惊群
a. 只唤醒等待队列上的一个entry,以此避免惊群。 - 多个进程accept(),优先唤醒哪个进程
a. 内核只会唤醒1个等待的进程,唤醒的逻辑是FIFO,这部分代码在sock_def_readable [13]。 - 在收到SYN和ACK的时候都会检查
sk_max_ack_backlog ,所以SOMAXCONN 参数其实影响了这两步
connect
client:
- 根据下一跳地址查找目的路由的缓存项
ip_route_connect - 在
inet_hash_connect 中做三件事情 a. 选择ephemeral port ,port 的选择偏向于偶数 b. 创建inet_bind_bucket 并加入bhash ,这里注意同一IP端口不能多次建立连接 c. 将tcp_sock 加入ehash - 调用
tcp_connect 发送SYN a. tcp_connect_init 做所有可以独立于 AF 的连接套接字设置 b. sk_stream_alloc_skb 分配skb,这里有一个sk_tx_skb_cache 的优化 c. 调用tcp_transmit_skb 传输这个SYN数据包,其中设置SYN包头的数据,包头数据在线性区 inet_wait_for_connect 中把当前线程加入sk的等待队列,调用tcp_rcv_synsent_state_process 等待SYN+ACK,如果没有延迟确认(defer_accept )机制的话就会调用tcp_send_ack 向对端发送ACK,这里第三次握手数据包中可以看到也没有payload。- 在
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()
- 收到 SYN segment,从
listening_hash 找到listen tcp_sock ,创建tcp_request_sock (NEW_SYN_RECV 状态),并将其加入ehash ,然后发送SYN+ACK - 收到 ACK segment,从
ehash 中找到tcp_request_sock ,从req->rsk_listener 找到listen tcp_sock ,创建新的 tcp_sock (SYN_RECV 状态) - 状态设置为
TCP_ESTABLISHED ,接下来把tcp_request_sock 加入listen socket 的accept queue - 在
accept 的时候也会隐式调用tcp_v4_rcv
总结
不要说话
参考:
- 对Linux服务端编程的一点浅薄理解
- 为什么Linux作为客户端的情况下不支持端口共用?
- 费希纳定律的推导过程图解
|