最详细的 Linux TCP/IP 协议栈源码分析
一.linux 内核网络栈代码的准备知识
1. linux 内核 ipv4 网络部分分层结构:
BSD socket 层: 这一部分处理 BSD socket 相关操作,每个 socket 在内核中以 struct socket 结构体现。这一部分的文件
主要有:/net/socket.c /net/protocols.c etc INET socket 层:BSD socket 是个可以用于各种网络协议的接口,而当用于 tcp/ip,即建立了 AF_INET 形式的 socket 时,
还需要保留些额外的参数,于是就有了 struct sock 结构。文件主要
有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc TCP/UDP 层:处理传输层的操作,传输层用 struct inet_protocol 和 struct proto 两个结构表示。文件主要
有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c /net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c /net/ipv4/tcp_timer.c
etc IP 层:处理网络层的操作,网络层用 struct packet_type 结构表示。文件主要有:/net/ipv4/ip_forward.c
ip_fragment.c ip_input.c ip_output.c etc. 数据链路层和驱动程序:每个网络设备以 struct net_device 表示,通用的处理在 dev.c 中,驱动程序都在/driver/net 目录下。
tcp/ip 协议栈视频解析:
https://www.bilibili.com/video/BV1SA411i7uF/
https://www.bilibili.com/video/BV1tA411i7ke/
2. 两台主机建立 udp 通信所走过的函数列表
^ | sys_read fs/read_write.c | sock_read net/socket.c | sock_recvmsg net/socket.c | inet_recvmsg net/ipv4/af_inet.c | udp_recvmsg net/ipv4/udp.c | skb_recv_datagram net/core/datagram.c | ------------------------------------------- | sock_queue_rcv_skb include/net/sock.h | udp_queue_rcv_skb net/ipv4/udp.c | udp_rcv net/ipv4/udp.c | ip_local_deliver_finish net/ipv4/ip_input.c | ip_local_deliver net/ipv4/ip_input.c | ip_recv net/ipv4/ip_input.c | net_rx_action net/dev.c | ------------------------------------------- | netif_rx net/dev.c | el3_rx driver/net/3c309.c | el3_interrupt driver/net/3c309.c ========================== | sys_write fs/read_write.c | sock_writev net/socket.c | sock_sendmsg net/socket.c | inet_sendmsg net/ipv4/af_inet.c | udp_sendmsg net/ipv4/udp.c | ip_build_xmit net/ipv4/ip_output.c | output_maybe_reroute net/ipv4/ip_output.c | ip_output net/ipv4/ip_output.c | ip_finish_output net/ipv4/ip_output.c | dev_queue_xmit net/dev.c | -------------------------------------------- | el3_start_xmit driver/net/3c309.c V
二.linux 的 tcp-ip 栈代码的详细分析
1.数据结构(msghdr,sk_buff,socket,sock,proto_ops,proto)
bsd 套接字层,操作的对象是 socket,数据存放在 msghdr 这样的数据结构:
创建 socket 需要传递 family,type,protocol 三个参数,创建 socket 其实就是创建一个 socket 实例,然后创建一个文件描述符结构,并且互相建立一些关联,即建立互相连接的指针,并且初始化这些对文件的写读操作映射到 socket 的 read,write 函数上来。
同时初始化 socket 的操作函数(proto_ops 结构),如果传入的 type 参数是 STREAM 类型,那么就初始化为 SOCKET->ops 为 inet_stream_ops,如果是 DGRAM 类型,则 SOCKET-ops 为 inet_dgram_ops。对于 inet_stream_ops 其实是一个结构体,包含了 stream 类型的 socket 操作的一些入口函数,在这些函数里主要做的是对 socket 进行相关的操作,同时通过调用下面提到的 sock 中的相关操作完成 socket 到 sock 层的传递。比如在 inet_stream_ops 里有个 inet_release 的操作,这个操作除了释放 socket 的类型空间操作外,还通过调用 socket 连接的 sock 的 close 操作,对于 stream 类型来说,即 tcp_close 来关闭 sock 释放 sock。
创建 socket 同时还创建 sock 数据空间,初始化 sock,初始化过程主要做的事情是初始化三个队列,receive_queue(接收到的数据包 sk_buff 链表队列),send_queue(需要发送数据包的 sk_buff 链表队列),backlog_queue(主要用于 tcp 中三次握手成功的那些数据包,自己猜的),根据 family、type 参数,初始化 sock 的操作,比如对于 family 为 inet 类型的,type 为 stream 类型的,sock->proto 初始化为 tcp_prot.其中包括 stream 类型的协议 sock 操作对应的入口函数。
在一端对 socket 进行 write 的过程中,首先会把要 write 的字符串缓冲区整理成 msghdr 的数据结构形式(参见 linux 内核 2.4 版源代码分析大全),然后调用 sock_sendmsg 把 msghdr 的数据传送至 inet 层,对于 msghdr 结构中数据区中的每个数据包,创建 sk_buff 结构,填充数据,挂至发送队列。一层层往下层协议传递。一下每层协议不再对数据进行拷贝。而是对 sk_buff 结构进行操作。
inet 套接字及以下层 数据存放在 sk_buff 这样的数据结构里:
路由:
在 linux 的路由系统主要保存了三种与路由相关的数据,第一种是在物理上和本机相连接的主机地址信息表,第二种是保存了在网络访问中判断一个网络地址应该走什么路由的数据表;第三种是最新使用过的查询路由地址的缓存地址数据表。
1.neighbour 结构 neighbour_table{ }是一个包含和本机所连接的所有邻元素的信息的数据结构。该结构中有个元素是 neighbour 结构的数组,数组的每一个元素都是一个对应于邻机的 neighbour 结构,系统中由于协议的不同,会有不同的判断邻居的方式,每种都有 neighbour_table{}类型的实例,这些实例是通过 neighbour_table{}中的指针 next 串联起来的。在 neighbour 结构中,包含有与该邻居相连的网络接口设备 net_device 的指针,网络接口的硬件地址,邻居的硬件地址,包含有 neigh_ops{}指针,这些函数指针是直接用来连接传输数据的,包含有 queue_xmit(struct * sk_buff)函数入口地址,这个函数可能会调用硬件驱动程序的发送函数。
2.FIB 结构 在 FIB 中保存的是最重要的路由规则,通过对 FIB 数据的查找和换算,一定能够获得路由一个地址的方法。系统中路由一般采取的手段是:先到路由缓存中查找表项,如果能够找到,直接对应的一项作为路由的规则;如果不能找到,那么就到 FIB 中根据规则换算传算出来,并且增加一项新的,在路由缓存中将项目添加进去。
3.route 结构(即路由缓存中的结构)
数据链路层:
net_device{}结构,对应于每一个网络接口设备。这个结构中包含很多可以直接获取网卡信息的函数和变量,同时包含很多对于网卡操作的函数,这些直接指向该网卡驱动程序的许多函数入口,包括发送接收数据帧到缓冲区等。当这些完成后,比如数据接收到缓冲区后便由 netif_rx(在 net/core/dev.c 各种设备驱动程序的上层框架程序)把它们组成 sk_buff 形式挂到系统接收的 backlog 队列然后交由上层网络协议处理。同样,对于上层协议处理下来的那些 sk_buff。便由 dev_queue_xmit 函数放入网络缓冲区,交给网卡驱动程序的发送程序处理。
在系统中存在一张链表 dev_base 将系统中所有的 net_device{}结构连在一起。对应于内核初始化而言,系统启动时便为每个所有可能支持的网络接口设备申请了一个 net_device{}空间并串连起来,然后对每个接点运行检测过程,如果检测成功,则在 dev_base 链表中保留这个接点,否则删除。对应于模块加载来说,则是调用 register_netdev()注册 net_device,在这个函数中运行检测过程,如果成功,则加到 dev_base 链表。否则就返回检测不到信息。删除同理,调用 unregister_netdev。
2.启动分析
2.1 初始化进程 :start-kernel(main.c)---->do_basic_setup(main.c)---->sock_init(/net/socket.c)---->do_initcalls(main.c)
2.2 do_initcalls() 中做了其它的初始化,其中包括协议初始化,路由初始化,网络接口设备初始化
(例如 inet_init 函数以_init 开头表示是系统初始化时做,函数结束后跟 module_init(inet_init),这是一个宏,在 include/linux/init.c 中定义,展开为_initcall(inet_init),表示这个函数在 do_initcalls 被调用了)
2.3 协议初始化
此处主要列举 inet 协议的初始化过程。
2.4 路由初始化(包括 neighbour 表、FIB 表、和路由缓存表的初始化工作)
2.4.1 rtcache 表 ip_rt_init()函数 在 net/ipv4/ip_output 中调用,net/ipv4/route.c 中定义
2.4.2 FIB 初始化 在 ip_rt_init()中调用 在 net/ipv4/fib_front.c 中定义
2.4.3 neigbour 表初始化 arp_init()函数中定义
2.5 网络接口设备初始化
在系统中网络接口都是由一个 dev_base 链表进行管理的。通过内核的启动方式也是通过这个链表进行操作的。在系统启动之初,将所有内核能够支持的网络接口都初始化成这个链表中的一个节点,并且每个节点都需要初始化出 init 函数指针,用来检测网络接口设备。然后,系统遍历整个 dev_base 链表,对每个节点分别调用 init 函数指针,如果成功,证明网络接口设备可用,那么这个节点就可以进一步初始化,如果返回失败,那么证明该网络设备不存在或是不可用,只能将该节点删除。启动结束之后,在 dev_base 中剩下的都是可以用的网络接口设备。
2.5.1 do_initcalls---->net_dev_init()(net/core/dev.c)------>ethif_probe()(drivers/net/Space.c,在 netdevice{}结构的 init 中调用,这边 ethif_probe 是以太网卡针对的调用)
3.网络设备驱动程序(略)
4.网络连接
4.1 连接的建立和关闭
tcp 连接建立的代码如下:
连接的初始化与建立期间主要发生的事情如下:
1)sys_socket 调用:调用 socket_creat(),创建出一个满足传入参数 family、type、和 protocol 的 socket,调用 sock_map_fd()获取一个未被使用的文件描述符,并且申请并初始化对应的 file{}结构。
2)sock_creat():创建 socket 结构,针对每种不同的 family 的 socket 结构的初始化,就需要调用不同的 create 函数来完成。对应于 inet 类型的地址来说,在网络协议初始化时调用 sock_register()函数中完成注册的定义如下:
所以 inet 协议最后会调用 inet_create 函数。
3)inet_create: 初始化 sock 的状态设置为 SS_UNCONNECTED,申请一个新的 sock 结构,并且初始化 socket 的成员 ops 初始化为 inet_stream_ops,而 sock 的成员 prot 初始化为 tcp_prot。然后调用 sock_init_data,将该 socket 结构的变量 sock 和 sock 类型的变量关联起来。
4)在系统初始化完毕后便是进行 connect 的工作,系统调用 connect 将一个和 socket 结构关联的文件描述符和一个 sockaddr{}结构的地址对应的远程机器相关联,并且调用各个协议自己对应的 connect 连接函数。对应于 tcp 类型,则 sock->ops->connect 便为 inet_stream_connect。
5)inet_stream_connect: 得到 sk,sk=sock->sk,锁定 sk,对自动获取 sk 的端口号存放在 sk->num 中,并且用 htons()函数转换存放在 sk->sport 中。然后调用 sk->prot->connect()函数指针,对 tcp 协议来说就是 tcp_v4_connect()函数。然后将 sock->state 状态字设置为 SS_CONNECTING,等待后面一系列的处理完成之后,就将状态改成 SS_CONNECTTED。
6) tcp_v4_connect():调用函数 ip_route_connect(),寻找合适的路由存放在 rt 中。ip_route_connect 找两次,第一次找到下一跳的 ip 地址,在路由缓存或 fib 中找到,然后第二次找到下一跳的具体邻居,到 neigh_table 中找到。然后申请出 tcp 头的空间存放在 buff 中。将 sk 中相关地址数据做一些针对路由的变动,并且初始化一个 tcp 连接的序列号,调用函数 tcp_connect(),初始化 tcp 头,并设置 tcp 处理需要的定时器。一次 connect()建立的过程就结束了。
连接的关闭主要如下:
1)close: 一个 socket 文件描述符对应的 file{}结构中,有一个 file_operations{}结构的成员 f_ops,它的初始化关闭函数为 sock_close 函数。
2)sock_close:调用函数 sock_release(),参数为一个 socket{}结构的指针。
3)sock_release:调用 inet_release,并释放 socket 的指针和文件空间
4)inet_release: 调用和该 socket 对应协议的关闭函数 inet_release,如果是 tcp 协议,那么调用的是 tcp_close;最后释放 sk。
4.2 数据发送流程图
各层主要函数以及位置功能说明:
1)sock_write:初始化 msghdr{}结构 net/socket.c
2)sock_sendmsg:net/socket.c
3)inet_sendmsg:net/ipv4/af_net.c
4)tcp_sendmsg:申请 sk_buff{}结构的空间,把 msghdr{}结构中的数据填入 sk_buff 空间。net/ipv4/tcp.c
5)tcp_send_skb:net/ipv4/tcp_output.c
6)tcp_transmit_skb:net/ipv4/tcp_output.c
7)ip_queue_xmit:net/ipv4/ip_output.c
8)ip_queue_xmit2:net/ipv4/ip_output.c
9)ip_output:net/ipv4/ip_output.c
10)ip_finish_output:net/ipv4/ip_output.c
11)ip_finish_output2:net/ipv4/ip_output.c
12)neigh_resolve_output:net/core/neighbour.c
13)dev_queue_xmit:net/core/dev.c
4.3 数据接收流程图
各层主要函数以及位置功能说明:
1)sock_read:初始化 msghdr{}的结构类型变量 msg,并且将需要接收的数据存放的地址传给 msg.msg_iov->iov_base. net/socket.c
2)sock_recvmsg: 调用函数指针 sock->ops->recvmsg()完成在 INET Socket 层的数据接收过程.其中 sock->ops 被初始化为 inet_stream_ops,其成员 recvmsg 对应的函数实现为 inet_recvmsg()函数. net/socket.c
3)sys_recv()/sys_recvfrom():分别对应着面向连接和面向无连接的协议两种情况. net/socket.c
4)inet_recvmsg:调用 sk->prot->recvmsg 函数完成数据接收,这个函数对于 tcp 协议便是 tcp_recvmsg net/ipv4/af_net.c
5)tcp_recvmsg:从网络协议栈接收数据的动作,自上而下的触发动作一直到这个函数为止,出现了一次等待的过程.函数 tcp_recvmsg 可能会被动地等待在 sk 的接收数据队列上,也就是说,系统中肯定有其他地方会去修改这个队列使得 tcp_recvmsg 可以进行下去.入口参数 sk 是这个网络连接对应的 sock{}指针,msg 用于存放接收到的数据.接收数据的时候会去遍历接收队列中的数据,找到序列号合适的.
但读取队列为空时 tcp_recvmsg 就会调用 tcp_v4_do_rcv 使用 backlog 队列填充接收队列.
6)tcp_v4_rcv:tcp_v4_rcv 被 ip_local_deliver 函数调用,是从 IP 层协议向 INET Socket 层提交的"数据到"请求,入口参数 skb 存放接收到的数据,len 是接收的数据的长度,这个函数首先移动 skb->data 指针,让它指向 tcp 头,然后更新 tcp 层的一些数据统计,然后进行 tcp 的一些值的校验.再从 INET Socket 层中已经建立的 sock{}结构变量中查找正在等待当前到达数据的哪一项.可能这个 sock{}结构已经建立,或者还处于监听端口、等待数据连接的状态。返回的 sock 结构指针存放在 sk 中。然后根据其他进程对 sk 的操作情况,将 skb 发送到合适的位置.调用如下:
TCP 包接收器(tcp_v4_rcv)将 TCP 包投递到目的套接字进行接收处理. 当套接字正被用户锁定,TCP 包将暂时排入该套接字的后备队列(sk_add_backlog).这时如果某一用户线程企图锁定该套接字(lock_sock),该线程被排入套接字的后备处理等待队列(sk->lock.wq).当用户释放上锁的套接字时(release_sock,在 tcp_recvmsg 中调用),后备队列中的 TCP 包被立即注入 TCP 包处理器(tcp_v4_do_rcv)进行处理,然后唤醒等待队列中最先的一个用户来获得其锁定权. 如果套接字未被上锁,当用户正在读取该套接字时, TCP 包将被排入套接字的预备队列(tcp_prequeue),将其传递到该用户线程上下文中进行处理.如果添加到 sk->prequeue 不成功,便可以添加到 sk->receive_queue 队列中(用户线程可以登记到预备队列,当预备队列中出现第一个包时就唤醒等待线程.) /net/tcp_ipv4.c
7)ip_rcv、ip_rcv_finish:从以太网接收数据,放到 skb 里,作 ip 层的一些数据及选项检查,调用 ip_route_input()做路由处理,判断是进行 ip 转发还是将数据传递到高一层的协议.调用 skb->dst->input 函数指针,这个指针的实现可能有多种情况,如果路由得到的结果说明这个数据包应该转发到其他主机,这里的 input 便是 ip_forward;如果数据包是给本机的,那么 input 指针初始化为 ip_local_deliver 函数./net/ipv4/ip_input.c
8)ip_local_deliver、ip_local_deliver_finish:入口参数 skb 存放需要传送到上层协议的数据,从 ip 头中获取是否已经分拆的信息,如果已经分拆,则调用函数 ip_defrag 将数据包重组。然后通过调用 ip_prot->handler 指针调用 tcp_v4_rcv(tcp)。ip_prot 是 inet_protocol 结构指针,是用来 ip 层登记协议的,比如由 udp,tcp,icmp 等协议。 /net/ipv4/ip_input.c
Linux 通过同时对多种通信协议的支持来提供通用的底层基础服务。它的第一个网络模型的版本是 4.3 BSD,也称为 Net/1,今天的 Linux 已经使用 Net/4 (Linux 2.2),其中大多数代码已经完全和 BSD 的版本不同,但是它依然支持 UINX 平台之间程序的移植。
Linux 网络套接字实现的模式是 UNIX 下的普遍标准。同时,Net/4 的网络层是完全另起炉灶重写的。首先,新的网络层尽可能地实行并行处理, 因此其伸缩性比起以前的版本,不可同日而语。其次,它包括了许多的优化,以便绕过不少流行操作系统网络实现中的不合理处(例如 Windows)。到目前为止,Linux 是唯一与 IPv4 和 IPv6 协议标准完全保持兼容的操作系统,而 Linux2.4 的 IPv4 伸缩性又大有提高。
Linux 支持的六种不同通信协议族:
1) TCP/IP (使用 TCP/IP 的 Internet 协议族),本文讨论的重点。
2) UNIX 域协议 (一种进程间通信的协议)
3) X25 协议
4) AX25 协议 (业余无线 X25)
5)IPX 协议 (Novell IPX)
6) APPLETALK 协议 (AppleTalk DDP)
1.1 内核源代码的组织
表 1 是本文要使用的 Linux Net/4 网络源代码的,其中大部分位于目录/usr/src/linux-2.2.x/net,列表如下,
插口层 BSD Socket /net/socket.c /net/protocols.c INET Socket /ipv4/protocol.c /ipv4/af_inet.c /net/ipv4/core/sock.c 协议层 TCP/UDP /net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp_input.c /net/ipv4//tcp_output.c /net/ipv4/tcp.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_timer.c etc... IP /net/ipv4/ip_forward.c /net/ipv4/ip_fragment.c /net/ipv4/ip_input.c /net/ipv4/ip_output.c 接口层 Ethernet ......
1.2 Linux 中 TCP/IP 网络层次结构与实现 Linux 通过一组相邻的软件层实现了 TCP/IP 模型,它由 BSD Socket 层、INET
Socket 层、传输层、网络层,和链路层构成。应用程序使用系统调用向内核函数传递参数和数据从而进入内核空间,由内核中注册的内核函数对相应的数据结构进行处理。Linux 的 TCP/IP 层次结构和实现方式如图所示。
![](https://static001.geekbang.org/infoq/2e/2ebec12a1d766619bad9468b6811b042.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
需要 C/C++ Linux 服务器架构师学习资料扫描下方二维码(资料包括 C/C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redisfastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)
![](https://static001.geekbang.org/infoq/cf/cf17b202b9d6062db839a3273cf31c41.png)
评论 (1 条评论)