概述
前两篇文章中,我们已经分析了 socket API 中用来创建 socket 的 socket
函数和用来
绑定地址的 bind
函数。正常到服务器程序在地址绑定之后就会在这个地址上开始监听等
待链接,这一工作可以调用 listen
函数来完成,代码如下:
1 | listen(server_fd, 10); |
这篇文章将要分析上面这条语句在内核中的具体实现流程。
图解
和前面的文章一样,我们首先看看函数调用关系的图解。带 *
的函数表示是通过函数指
针引用而不是直接调用
listen
函数到逻辑比 socket
和 bind
到函数逻辑要简单许多。
sockfd_lookup_light
函数在 bind
函数到讲解中提到过,这里不再重复。如果你不是
了解其中到机制,可以参考我的另外两篇文章 socket API 实现(一)—— socket 函数
和 socket API 实现(二)—— bind 函数。
这篇文章将会重点讲解 inet_listen
函数和 socket 中和监听相关的数据结构。
连接请求队列
连接请求队列分为半连接队列和全链接队列,这两个队列处于同一个结构体中,也就是
request_sock_quue
inet_connection_sock
结构的 icsk_accept_queue
字段就是这
个类型,用来存储一个 sock
的连接请求。request_sock_quue
的结构定义如下:
1 | struct request_sock_queue { |
其中前面两个字段用来全连接队列,而最后一个字段用来存储半连接队列。全连接队列是随
着连接请求不断到来并且被处理之后才逐步建立起来的。半连接队列则在 listen
函数中
创建,因为 listen
的第二个参数 backlog
指定了同时允许的最大半连接状态的连接
数,也就确定了半连接队列的长度,比如 listen(fd, 10)
中的 10。
在 inet_listen
中调用了 reqsk_queue_alloc
函数创建 sock 的半连接队列,也就是
分配和初始化 listen_sock
结构空间。代码如下:
1 | size_t lopt_size = sizeof(struct listen_sock); |
这段代码比较的晦涩,主要是因为 listen_sock
结构的定义非常的灵活:
1 | struct listen_sock { |
在结构体定义到末尾有一个零长数组,当我们分配的空间大于 sizeof(struct
listen_sock)
的时候,syn_table
字段会指向超出结构体大小空间的第一个字节。关于
零长数组到更多介绍,可以参考我写的另外一片文章 C 语言的趣味用法。
在上面到空间分配代码中,lopt_size
首先是 sizeof(struct listen_sock)
因为在
request_sock_queue
中表示半连接队列的 listen_opt
是一个指针,它到空间我们也
许要分配。之后 lopt_size += nr_table_entries * sizeof(struct request_sock *);
表示在 listen_sock
空间之后还有额外的 nr_table_entries * sizeof(struct
request_sock *)
空间,也就是我们前面提到的 syn_table
指向的空间。
从这段空间到分配我们可以看出,每一个连接请求都是一个 requset_sock
结构,半连接
状态的 request_sock
的指针存放在 syn_table
中,这种连接请求最都有
nr_table_entries
个。它的最大值由下面三个中的最小值决定:参数 backlog
,
sysctl_max_syn_backlog
变量, sock_net(sock->sk)->core.sysctl_somaxconn
。其
中 backlog
和 somaxconn
的比较在 sys_listen
中就已经完成并把较小的一个作为
参数传递给了 reqsk_queue_alloc
。剩下两个的比较则在前面的分配代码中完成。
分配空间之后 sock
的内存空间结构大致如下:
reqsk_queue_alloc
函数的主要工作就是分配上图中 listen_sock
的结构空间。
端口重用
在 inet_listen_start
中会调用 inet_csk_get_port
函数。这看起来不可思议,因为
端口绑定的工作是在 bind
函数中进行的,inet_csk_get_port
函数我们在 socket
API 实现(二)—— bind 函数 一文中已经提过,这里不再解释它的代码。我
看过的资料大部分都说这里调用 inet_csk_get_port
是为了检查是否绑定了端口,是否
端口占用等等。个人认为这些说法都有失偏颇。
其实这里调用 inet_listen_start
主要是有两个作用。第一如果没有绑定端口,那么先
进行端口绑定,如果绑定了端口则需要更改 inet_bind_bucket
中的 fastreuse
字段
,把他设置成否定值。这个字段的作用我们在socket API 实现(二)—— bind 函数
一文中已经解释过,在 inet_csk_start_listen
中设置了 sock_state
为 TCP_LISTEN
状态,这就使得端口可重用的第二个判断条件不成立了,也就是说
fastreuse
字段失效。调用 inet_csk_get_port
函数可以让可重用再次得到修正。以
下是简化的代码:
1 | if (!snum) { |
如果地址已经绑定,那么最开始的判断 !sum
不成立所以执行 else
(1)。因为地址已
经绑定了所以可以找到 inet_bind_bucket
结构,跳到 tb_found:
(2)。以为已经绑
定地址所以 tb->owners
不会为空(绑定的时候调用 inet_bind_hash
会把 sk
添加
到 tb->owners
中),所以接下来的 if
中会判断是否地址冲突,如果之后自己在
owners
队列中的话不会冲突(3)的判断为假不会执行。所以最后的流程到(4)设置
fastreuse
为假。
所以从上面的代码来看, 在 inet_csk_start_listen
中调用 inet_csk_get_port
主
要作用有三点:
如果没有绑定端口,先绑定端口
如果端口已经绑定检查是否有冲突
如果没有冲突把端口可重用的判断标志设置为否
加入监听哈希表
在 socket API 实现(二)—— bind 函数 一文中我们说过,为了快速的找
到 sock 结构,内核设置了三个哈希表,这三个哈希表在同一个结构 inet_hashinfo
之
中。和上一篇文章主题——绑定相关的哈希表是 bhash
字段,而和这篇文章的主题——监听
相关的哈希表是 listening_hash
函数调用关系图中的最后一个函数 inet_hash
就是
用来把 sock
加入到 listening_hash
队列中去的。
1 | if (sk->sk_state != TCP_LISTEN) { |
在《追踪 Linux TCP/IP 代码运行》一书的第七章 242 页中分析 __inet_hash_nolisten
的运行显然牛头不对马嘴。因为此时 sk->sk_state == TCP_LISTEN
。这个函数的作用其
实是找打该 sock 对应的哈希桶(ilb),然后把这个 sock 链接到这个哈希桶的哈希链中
。 struct sock
中对应的 sk_nulls_node
字段被用来链接这个哈希链,但是内核的代
码中说是提供给 udp/udp-lite
协议似乎有点误导的嫌疑。
1 | * @skc_nulls_node: main hash linkage for UDP/UDP-Lite protocol |
从上面的代码可以看出 sock
的 sk_nulls_node
来自 sock_common
,而
sock_common
对于 skc_nulls_node
的注释说它是提供给 udp/udp-lite
协议使用的
。