计算机网络是个庞大且复杂的话题,这不是一本书就可以说清楚的。然而,在互联网极大普及的今天,很少有应用程序不与网络打交道,我还是抱着授人以渔的思想,尝试介绍 Linux 在网络层面的整体架构,这样在解决具体问题的时候,才可以有针对性地进行分析。
本章不打算从头到尾来解释 Linux 网络从物理层到应用层的所有实现。关于理论方面的知识,可以单独学习 Andrew 的《计算机网络》,Steven 的《TCP/IP 详解卷1》,在实现方面,可以参考《深入理解 Linux 网络技术内幕》。
本章主要介绍 Linux 网络层数据的整体流转架构,包括如下内容:
分析数据在网络层的流转过程。
socket 接口层的实现。
netfilter 和 lvs 的关系。
一些常用的网络相关的系统参数。
Nginx 服务器监听 socket 初始化过程。
8.1 数据在网络层的流转
在大多数情况下,我们接触到的网络都是基于 TCP/IP 协议栈的实现,TCP/IP 协议是一个分层的网络模型(见图8-1)。所以,Linux 内核对网络层的实现也是围绕这个分层模型展开的。
图8-1 TCP/IP 网络模型的基本结构
8.1.1 sk_buff 结构
sk_buff 结构是 Linux 实现的在网络各层中流转的数据结构,这个结构相对较为复杂,我们这里略去细节,仅为说明流程进行简单介绍(代码详见:/Linux-4.5.2/include/Linux/skbuff.h)。
struct sk_buff {
union {
struct {
// 内核维护了一个 sk_buff_head 链表,next 代表该 skb 的下一个元素,prev 代表上一个元素
struct sk_buff *next;
struct sk_buff *prev;
…
struct sock *sk; // L4 层对应的 socket 结构
struct net_device *dev; // sbk 相关联的网络设备
…
unsigned int len, // 整个缓冲区大小,包括 head
data_len; // 只包括数据的大小
__u16 mac_len, // mac 报头大小
hdr_len;
…
__be16 protocol; // 当前层的协议
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
…
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,*data;
unsigned int truesize; // 缓冲区总大小,包括 skb 自己
atomic_t users;
};
其中 tail、end、head、data 的关系如图8-2所示。
图8-2 tail、end、head、data 指针
数据往上一层传输之后,data 指针就指向当前层报头的尾端,也就是上一层报头的开始。
8.1.2 数据流转过程
在了解了 Linux 网络中传输的数据结构 skb 之后,我们来看各层的处理流程(如图8-3所示),我们以接收数据为例,网卡首先获取网络中的数据,然后经过网卡驱动解析数据,封装成链路层的数据帧给 L2 层,L2 层经过处理之后(比如 bridge 网桥),假如数据还在本机内,则传送给 L3 层,L3 层经过处理(route 路由),假如数据还在本机内,则交给 L4 层,最后交给用户态程序。同理,发送数据的过程反过来类推即可。
图8-3 Linux 对网络数据的收发流程
下面,我们自底向上对每一层内核的处理过程进行分析。
首先,我们先搞明白 Linux 如何响应网卡的请求,在系统启动的时候,会注册响应网络读写事件的软中断。
static int __init net_dev_init(void)
{
…
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
…
}
其中 net_tx_action 用于响应传输数据,net_rx_action 用于响应接收数据。
我们先来分析一下 net_rx_action:
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget;
LIST_HEAD(list);
LIST_HEAD(repoll);
local_irq_disable();
list_splice_init(&sd->poll_list, &list);
local_irq_enable();
for (;;) {
struct napi_struct *n;
…
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
…
}
local_irq_disable();
list_splice_tail_init(&sd->poll_list, &list);
list_splice_tail(&repoll, &list);
list_splice(&list, &sd->poll_list);
if (!list_empty(&sd->poll_list))
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
net_rps_action_and_irq_enable(sd);
}
net_rx_action 对于当前 CPU 对应 poll_list 队列中的所有 dev,调用 dev->poll 方法。该方法是由对应 dev 的驱动程序实现的,用于接收及处理报文。
net_rx_action 每次运行都有一定的限度,并不一定要将所有报文都处理完。在处理完一定数量的报文配额、或处理过程超过一定时间后,net_rx_action 便会返回。返回前触发一次 NET_RX_SOFTIRQ 软中断,等待下一次中断到来的时候继续被调度。
从 net_rx_action 的实现我们可以发现,Linux 对于网络数据的接收是中断+轮询的方式,网卡驱动通过实现 poll 方法进行轮询,结合软中断的过程。这是为了更好地提升性能,否则所有数据收发都依赖中断的方式,那么 CPU 将会在不停的中断响应当中忙死。
至于网卡驱动的实现,这里就不再进行分析了,有兴趣大家可以自己去研究一下网卡驱动,一般网卡驱动实现 poll 函数之后,会解析出 skb 结构,然后调用 netif_receive_skb 函数把数据交给网络协议栈。
在分析 netif_receive_skb 的实现之前,我们先来了解一下 Linux 对不同协议的抽象:
struct packet_type {
__be16 type; // 协议类型
struct net_device *dev;
// 协议处理函数
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *);
bool (*id_match)(struct packet_type *ptype,
struct sock *sk);
void *af_packet_priv;
struct list_head list;
};
每种协议都有一个 packet_type 来描述,其中,最主要的3个属性为 type、dev、func,分别代表协议的类型,注册的设备以及协议处理函数。
netif_receive_skb 最终会调用 __netif_receive_skb_core 函数,下面我们分析其执行过程:
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
rx_handler_func_t *rx_handler;
struct net_device *orig_dev;
bool deliver_exact = false;
int ret = NET_RX_DROP;
__be16 type;
…
orig_dev = skb->dev;
skb_reset_network_header(skb); // 重制 skb 头,现在 skb 指向 ip 头
…
// 遍历全局 ptype_all,做相应协议处理,例如 tcpdump 或 raw socket
list_for_each_entry_rcu(ptype, &ptype_all, list)
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
// 遍历设备上注册的 ptype_all 进行处理
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
…
type = skb->protocol; // 获取协议类型
if (likely(!deliver_exact)) { // 根据全局定义的协议进行报文处理
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&ptype_base[ntohs(type) &
PTYPE_HASH_MASK]);
}
// 根据设备特定的协议进行报文处理
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&orig_dev->ptype_specific);
// 如果设备发生变化,那么还需要针对新设备的注册协议进行处理
if (unlikely(skb->dev != orig_dev)) {
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&skb->dev->ptype_specific);
}
// 处理最后一个未处理的 packet_type
if (pt_prev) {
if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))
goto drop;
else
ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
} else {
drop:
atomic_long_inc(&skb->dev->rx_dropped);
kfree_skb(skb);
ret = NET_RX_DROP;
}
out:
return ret;
}
在数据包处理过程中,首先会看 ptype_all 中是否有注册的协议处理函数,如果有则调用相应的处理函数,然后再到 ptype_base 中寻找进行处理。
其中 ptype_all 为一个双向链表,ptype_base 是一个哈希表,其哈希函数以协议标识符为参数,内核通常利用该哈希表判断应当接收传入的网络数据报的协议。
ptype_all 中一般注册的都是抓包程序、raw socket 等,ptype_base 则为 TCP/IP 协议栈,如 ip、arp 等。
以 IPv4 协议为例,其注册的 func 为 ip_rcv:
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
当包交到网络层之后,我们先放一放。下面再分析一下包的发送过程。
当我们用上层协议构建好报文之后,就可以调用 dev_queue_xmit 函数进行数据的发送。
dev_queue_xmit 函数会将数据提交到设备的队列中,dev->qdisc 指向一个队列的实例,里面包含了队列本身以及操作队列的方法(enqueue、dequeue、requeue)。
然后通过调用 qdisc_restart 把数据从队列中取出,提交给驱动程序的 dev->hard_start_xmit 方法进行发送。
这里省略了 dev_queue_xmit 相关的源码,大家可以自己阅读分析。
假如驱动发送数据成功后,就会发生中断,最后通过我们之前注册软中断程序 NET_TX_SOFTIRQ 进行响应,执行 net_rx_action 函数,至于 net_rx_action 的实现就留给读者自己分析了。
8.2 socket 接口层的实现
在了解了 Linux 网络层数据传输的过程后,再来分析一下常用的 sokcet 接口层,sokcet 层一般用于构建 TCP 层的连接,进行数据的读写。在 Linux 一切皆文件的思想下,socket 的读写也是对文件的读写。我们先来看一下进程和 socket 的关系(见图8-4),在进程结构(task_struct)中维护了所有打开的文件句柄 files,其中的 fd_array 字段对应一个 file 结构,file 中的 f_inode 对应的是 socket_alloc 中的 vfs_inode。
图8-4 进程和 socket 的关系
8.2.1 socket 系统初始化
在 Linux 上 socket 实现了类似的文件系统:
static struct file_system_type sock_fs_type = {
.name = "sockfs",
.mount = sockfs_mount,
.kill_sb = kill_anon_super,
};
其中,mount 函数指针定义了如何挂载文件系统,而 kill_sb 函数指针定义了如何删除该超级块。
对 sockfs_mount 超级块的操作进行注册(主要进行 inode 的分配和销毁):
static const struct super_operations sockfs_ops = {
.alloc_inode = sock_alloc_inode,
.destroy_inode = sock_destroy_inode,
.statfs = simple_statfs?
};
对 socket 文件读写则注册为:
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
系统在启动阶段,会进行 socket 子系统的初始化:
core_initcall(sock_init);
static int __init sock_init(void)
{
int err;
…
skb_init(); // 初始化 skb 数据包 slab 缓存
init_inodecache(); // 创建一块用于 socket 相关的 inode 缓存;后面创建 inode、释放
// inode 会使用到
err = register_filesystem(&sock_fs_type); // 将 socket 文件系统注册到内核中
…
sock_mnt = kern_mount(&sock_fs_type); // 挂载 socket fs
…
}
socket_init 主要对 socket 文件系统进行注册和挂载。
8.2.2 socket 创建
Linux 通过提供 sys_socket 系统调用来创建 socket,通过对 socket 的创建,初始化了进程和 socket 文件句柄的上下文:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
...
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
…
return retval;
}
通过 sys_socket 系统调用发现,socket 创建过程主要分为两个步骤:
1)通过 sock_create->__sock_create->sock_alloc 分配 socket 结构。
2)进程通过 sock_map_fd 让 sock_alloc_file 申请 file 结构后,让 socket 和该文件进行关联。
因为 sock_create 执行过程比较长,这里省略了代码,通过图8-5,可以了解 sock_create 执行之后发生的事情。
图8-5 sock_create 执行过程
sock_create 通过3个步骤来完成:
1)通过 alloc_inode 调用 socket_fs 的 sock_alloc_inode,创建了 vfs_inode 结构。
2)通过 AF_INET 协议族的 inet_create 函数创建 TCP 的 sock 对象,并且和 socket 对象挂钩。
3)TCP 协议(tcp_prot)结构的 init 方法初始化 sock 对象。
其中协议族对象需要实现 net_proto_family 接口,协议对象需要实现 proto 接口:
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.enter_memory_pressure = tcp_enter_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem = sysctl_tcp_wmem,
.sysctl_rmem = sysctl_tcp_rmem,
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_DESTROY_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = &tcp_hashinfo,
.no_autobind = true,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_tcp_setsockopt,
.compat_getsockopt = compat_tcp_getsockopt,
#endif
.diag_destroy = tcp_abort,
};
8.2.3 socket 绑定
在 socket 创建之后,还需要对其与指定的地址和端口进行绑定才能使用,Linux 提供一个 sys_bind 系统调用来实现该功能:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
// 根据 fd 获取对应的 soket 结构
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
// 将用户空间的地址拷贝到内核空间
err = move_addr_to_kernel(umyaddr, addrlen, &address);
if (err >= 0) {
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
if (!err)
// 根据指定协议域及 socket 类型进行 bind
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err;
}
sys_bind 的核心操作为 sock->ops->bind,以 inet_stream_ops 为例,bind 函数为 inet_bind:
const struct proto_ops inet_stream_ops = {
…
.bind = inet_bind,
…
};
inet_bind 的主要步骤分为:
1)地址类型检测:
if (!net->ipv4.sysctl_ip_nonlocal_bind &&
!(inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;
2)端口范围检测:
snum = ntohs(addr->sin_port);
err = -EACCES;
if (snum && snum < PROT_SOCK &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;
3)设置源地址和接收地址:
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->inet_saddr = 0;
4)检查端口是否被占用:
if ((snum || !inet->bind_address_no_port) &&
sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
5)初始化目标地址和端口:
inet->inet_sport = htons(inet->inet_num);
inet->inet_daddr = 0;
inet->inet_dport = 0;
8.2.4 socket 监听
在 socket 经过 sys_bind 之后,就可以进行监听操作,Linux 提供一个 sys_listen 系统调用来执行该动作。
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
// 根据文件描述符取得 socket 对象
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
// 根据系统中的设置调整参数 backlog
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
// 调用特定协议簇的 listen 函数
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
sys_listen 通过 sock->ops->listen 来调用特定协议簇的 listen 函数,我们仍旧以 AF_INTET 的 inet_listen 来分析执行过程:
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
…
// 假如状态不是 SS_UNCONNECTED 或者类型不是 SOCK_STREAM 则退出
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
// 这里检查 sock 的状态是否是 TCP_CLOSE 或 TCP_LISTEN
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
// 当 sock 的状态不是 TCP_LISTEN 时,进行监听初始化初始化
if (old_state != TCP_LISTEN) {
…
err = inet_csk_listen_start(sk, backlog);
…
}
// 设置 sock 的最大并发连接请求数
sk->sk_max_ack_backlog = backlog;
…
return err;
}
inet_listen 在 inet_csk_listen_start 中初始化了连接等待队列,并且设置 sock 的状态为 TCP_LISTEN,最后将当前 socket 在 inet_hashinfo 中进行哈希处理,在 socket 的哈希表结构 inet_hashinfo 中,其成员 listening_hash[INET_LHTABLE_SIZE] 用于存放处于 TCP_LISTEN 状态的 sock 当 socket 通过 listen()调用完成等待连接队列的初始化后,需要将当前 sock 放到该结构中。
8.2.5 socket 接受连接
在 socket 处于监听状态之后,Linux 通过 sys_accept 函数来接受新客户端连接的到来:
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags)
{
struct socket *sock, *newsock;
struct file *newfile;
int err, len, newfd, fput_needed;
struct sockaddr_storage address;
…
// 根据 server_fd 获取服务端的 socket 结构
sock = sockfd_lookup_light(fd, &err, &fput_needed);
…
// 给客户端分配一个新的 socket 结构
newsock = sock_alloc();
…
// 把服务端的 socket 类型和 ops 赋给客户端的 socket
newsock->type = sock->type;
newsock->ops = sock->ops;
__module_get(newsock->ops->owner);
// 获取一个未使用的 fd 句柄
newfd = get_unused_fd_flags(flags);
…
// 分配一个新的 file 结构
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
…
err = security_socket_accept(sock, newsock);
…
// 调用对应协议簇的 accept 函数
err = sock->ops->accept(sock, newsock, sock->file->f_flags);
…
if (upeer_sockaddr) {
if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
&len, 2) < 0) {
err = -ECONNABORTED;
goto out_fd;
}
// 拷贝地址到用户态
err = move_addr_to_user(&address,
len, upeer_sockaddr, upeer_addrlen);
…
}
// 把新创建的 fd 和 file 关联
fd_install(newfd, newfile);
err = newfd;
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
out_fd:
fput(newfile);
put_unused_fd(newfd);
goto out_put;
}
sys_accept 的关键就是给客户端分配 socket 结构,然后执行服务端 socket 的 accept 操作,再把客户端的 socket 与新分配的 file 及文件句柄 fd 关联。下面我们仍旧以 af_net 的 inet_accept 函数来分析 accpet:
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
// 如果使用的是 TCP,则 sk_prot 为 tcp_prot,accept 为 inet_csk_accept()获取新连接的 sock
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
…
WARN_ON(!((1 << sk2->sk_state) &
(TCPF_ESTABLISHED | TCPF_SYN_RECV |
TCPF_CLOSE_WAIT | TCPF_CLOSE)));
// 把 sock 和 socket 嫁接起来,让它们能相互索引
sock_graft(sk2, newsock);
// 把新 socket 的状态设为已连接
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2);
do_err:
return err;
}
static inline void sock_graft(struct sock *sk, struct socket *parent)
{
write_lock_bh(&sk->sk_callback_lock);
sk->sk_wq = parent->wq; //设置 sock 的等待队列为 socket 的等待队列
// 相互嫁接
parentsk ->= sk;
sk_set_socket(sk, parent);
security_sock_graft(sk, parent);
write_unlock_bh(&sk->sk_callback_lock);
}
inet_accept 通过调用具体协议层(例如 TCP)的 accept 来获取 sock 结构,然后和 scoket 结构关联起来,并且把 socket 状态设置为已连接。
最后以 TCP 层的 accept 实现 inet_csk_accept 来说明新的 sock 结构是如何获取的:
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
…
error = -EINVAL;
// socket 必须处于监听状态
if (sk->sk_state != TCP_LISTEN)
goto out_err;
// 发现有 ESTABLISHED 状态的连接请求块
if (reqsk_queue_empty(queue)) {
// 获取等待超时时间,如果是非阻塞则为0
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
// 如果是非阻塞的,则直接退出
error = -EAGAIN;
if (!timeo)
goto out_err;
// 阻塞等待,直到有全连接。如果用户有设置等待超时时间,超时后会退出
error = inet_csk_wait_for_connect(sk, timeo);
…
}
// 获取新连接的 sock,释放连接控制块
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
…
out:
release_sock(sk);
if (req)
reqsk_put(req);
return newsk;
…
}
在 TCP 层的 accept 主要还是等待获取可用并且状态为 ESTABLISHED 连接的过程,假如有则从 backlog 队列(全连接队列)中取出一个 ESTABLISHED 状态的连接请求块,返回它所对应的连接 sock。同时更新 backlog 队列的全连接数,通过 reqsk_queue_remove 释放取出的连接控制块:
static inline struct request_sock *reqsk_queue_remove(struct request_sock_
queue *queue, struct sock *parent)
{
struct request_sock *req;
spin_lock_bh(&queue->rskq_lock);
req = queue->rskq_accept_head; // 第一个 ESTABLISHED 状态的连接请求块
if (req) {
sk_acceptq_removed(parent); // 当前 backlog 队列的全连接数减一
queue->rskq_accept_head = req->dl_next;
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_tail = NULL;
}
spin_unlock_bh(&queue->rskq_lock);
return req;
}
8.2.6 新连接的到来
通过分析 sys_accept 函数可以发现,新连接是从 backlog 队列中取出来的,那么队列中的数据又是谁放进去的呢?
在开头通过分析 Linux 网络层数据流转过程可以发现,数据从网卡进来后,最终会调用 ip_rcv 函数,在该函数处理过程中,假如数据应该在本机内部,那么会通过 ip_rcv_finish->dst_input->ip_local_deliver 在本机内处理数据:
int ip_local_deliver(struct sk_buff *skb)
{
…
struct net *net = dev_net(skb->dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
其中ip_local_deliver_finish->ipprot->handler(skb)->tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process->icsk->icsk_af_ops->conn_request(sk,skb)->tcp_v4_conn_request->tcp_conn_request->inet_csk_reqsk_queue_add:
struct sock *inet_csk_reqsk_queue_add(struct sock *sk,
struct request_sock *req,
struct sock *child)
{
struct request_sock_queue *queue = &csk_accept_queuecsk_accept_queue;
spin_lock(&queue->rskq_lock);
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_child_forget(sk, req, child);
child = NULL;
} else {
req->sk = child;
req->dl_next = NULL;
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_head = req;
else
queue->rskq_accept_tail->dl_next = req;
queue->rskq_accept_tail = req;
sk_acceptq_added(sk);
}
spin_unlock(&queue->rskq_lock);
return child;
}
所以,经过从网卡到 IP 层再到 TCP 层的数据流转,最终通过 inet_csk_reqsk_queue_add 把新的 sock 放入到了 csk_accept_queuecsk_accept_queue 队列中。
知道了如何将 socket 放入到 request 队列后,再来看一下前面设置的 backlog 大小有什么用,通过 ip_local_deliver_finish->ipprot->handler(skb)->tcp_v4_rcv->tcp_check_req->inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk,skb,req,NULL,req,&own_req)->tcp_v4_syn_recv_sock->sk_acceptq_is_full 来判断新建立的连接是否有可能被创建出来:
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
在了解了服务端创建 socket,并且进行绑定和监听之后,大家一定会问,客户端是如何进行 connect 操作的呢?连接建立后,数据的读写又是如何进行的呢?限于篇幅,这块工作就交给大家自己完成了,因为整个数据流转的过程已经分析过了,所以都可以依样画葫芦进行分析。
另外,需要说明的是,连接数据的流程和数据读取的流程是类似的,仅仅状态不同而已,对 IP 层来讲,业务的数据和连接建立的数据都是一样的。客户端建立连接 Linux 通过提供一个 sys_connect 操作来完成的,其最终会通过 TCP 层的 tcp_v4_connect 把 sync 数据包通过 netfilter 的 output 阶段发送出去。
8.2.7 socket 整体流程
最后我们通过图8-6总结一下整个 TCP 协议簇下 socket 的总体流程。
图8-6 socket 流程
从图中我们可以发现,TCP 本身是一个面向连接的协议,服务端在启动的时候,步骤大致如下:
1)首先通过 sys_socket 创建监听套接字,并且通过 sys_bind 绑定设备,然后使用 sys_lsten 进行监听。
2)客户端也会通过 sys_socket 创建套接字,然后通过 sys_connect 创建同服务器的连接。
3)服务器最后通过 sys_accept 来接受客户端的请求,建立连接。
以上过程就是 TCP 建立连接的三次握手过程。在连接建立之后,客户端和服务器都可以通过 sys_recvmg 和 sys_endmsg 来互相收发数据,数据最终会经过 Linux 的 netfilter 层进行过滤和转发。