TCP/IP协议栈在Linux内核中的运行时序分析
1.调研要求
-
-
编译、部署、运行、测评、原理、源代码分析、跟踪调试等
-
应该包括时序图
2.目录
3.网络栈总体架构
3.1 网络栈总体架构分析
通常我们说到网络栈, 指的是为了进行网络数据包的收发, 由内核实现的一套函数集合。 所谓栈, 即一层层的结构, 这仅仅是软件上的一种说法。我们不好从字面上对网络栈作出解 释, 但可从其实现的功能上进行解释: 网络栈按照预先设置的一套规则对用户数据进行封装 从而达到网络上主机之间数据交换的目的。从定义上看, 网络栈是由一套网络协议构成, 而 网络协议本质上就是规则: 为了实现网络上两台独立主机之间交换信息, 必须遵循一套数据 封装格式, 这是网络逐渐进入应用后的必要步骤。
对于规则的设置, 我们可以“ 一步到位” , 直接给出数据帧的封装格式, 每个发送主机在 将数据发送到网络介质上之前, 首先必须将数据封装成规定的数据帧格式, 这样接收端接收 到数据后, 按预先规定的格式进行数据解释, 从而完成数据的接收。但是这种一步到位的封 装格式也给后期扩展带来了极大的困难, 如果有了一种新的更有效的封装格式, 则为了支持 这种新的到装格式, 网络上所有主机必须重新设计网络栈。
为了支持动态扩展, 以及传送数据之外的其他服务( 如QoS 服务), 数据的封装格式必 须是可扩展的, 可动态支持新的规则的加入。所以网络栈规则的设计在起初就设计为分层结 构, 数据在每层上都进行本层的封装, 之后传递给其下一层。这种分层设计方式首先对在不 同层次上加入新的服务提供了可扩展性, 其次使得数据封装格式的设计具有了很大的灵活性: 我们可以独立于其他层专门对某个层次进行规则的创建( 这里的规则就是我们通常所说的协 议) , 这种分层独立设计规则的方式也为后期加入新的规则提供了方便。
按照这种分层设计思想, 早期网络栈被分为了4 层, 从上到下依次命名为应用层、传输 层、网络层、链路层, 面且在每个层次都定义了数据的封装方式( 应用层封装格式完全由用 户定制) 。单从网络栈定义来看, 各层之间除了通过预先定义好的一些接口函数进行交互外, 应不存在其他耦合关系, 但基本所有的网络栈实现都无法做到这一点, 各层之间都或多或少 地具有某种紧密的关联, 但是从总体上看, 实现都大多遵循了网络栈的分层结构。
可以说, 现在绝大多数网络栈实现基本都是遵循早先的四层网络栈分层结构进行设计的。

Linux内核网络协议栈实现层次
应用层: 该层次定义了一套用户调用接口函数, 即socket 套接字接口函数, 如socket 、bind 、accept 等。这套接口函数由Linux 操作系统提供( 更确切地说是由glicbc 库提供) 。
传输层: 该层次定义了一系列协议, 如比较常见的TCP 、UDP 协议, 所以对传输层的实现也就集 中在实现TCP 协议和UDP 协议上。ICMP 、IGMP 单从理论上定义属于网络层协议, 但实现 上是作为传输层协议实现的, 因为它们都需要封装在IP 报文中进行收发。 这层涉及的文件主要有tcp.c 、udp.c 、icmp•c 、igmp.c 、snmp.h。
网络层: 数据包路由功能实现在这一层, 这一层也是IP 协议所在层次。另外我们将RAW 套接字 也划分在这一层, 使用RAW 套接字时, 用户必须自己创建IP 首部和传输层协议首部。 ARP 协议作为IP 协议的辅助协议, 完成IP 地址到硬件地址之间的映射工作, 从其实现来看, 其建立在链路层模块之上, 与IP 协议平行, 但一般我们都将ARP 协议作为链路层协议来看待。 我们无意于这种学究性的对某个协议的分层, 姑且将ARP 协议实现作为链路层的一部分。 这层涉及的文件主要有ip.c 、route.c 、raw.c 、packet.c。
链路层: 对于链路层而言, 我们一般不关心该层定义何种协议, 而只关心数据帧在这一层的封装 方式。从实现上看, 链路层实现是作为网络栈与底层硬件驱动的一个接口层而存在的。 该层涉及的文件主要有dev.c 、psnap.c 、eth.c、arp.c 。
“ 驱动层” : OSI 模型中没有定义这一层。我们将驱动程序所在层次定义为“ 驱动层” , 该层与链路层进行交 互完成数据包的收发工作。 该层涉及的文件是定义在drivers 了目录下的一些网络设备的驱动程序。
物理层: 网络栈实现不涉及物理层。
3.2 系统调用接口到内核的请求传递
内核网络栈实现最上层为BSD socket 层, 对应accept 系统调用该层次对应实现函数为sock-accept , sock-accept 函数。 我们知道, Linux 系统使用0x80 软中断支持系统调用, 那么首先不可避免的, 对于网络套接字编程中的系统调用, 从用户态进入到内核态也是由int $ 0x80 指令完成的。一般而言,对于每一个系统调用, 都有一个内核入口函数相对应, 如文件打开open 系统调用, 对应入口函数为sys-open , write 对应sys write , 而read 对应sys read , close 对应sys-close , 诸如此类。那么我们自然会想到对于网络套接字函数集合也应如此, 即socket 对应sys-socket ( 具体名称可能不是这样, 但一定有一个这样对应的函数) , bind 对应sys-bind' listen 对应sys-listen等。实际不然, 网络套接字内核入口函数的组织与其他系统调用函数不同, 所有的网络套接字系统调用函数( socket 、bind 、listen 、connect 等) 都使用一个共同的入口函数: sys-socketcall 。 这个sys-socketcall 函数定义在net/socket.c 文件中, 可知该文件实现为BSD socket 层。
从sys-socketcall 函数实现来看, 该函数根据具体调用的函数( call 参数表示) 将请求分 配给具体的实现函数, 对应系统调用。此时对应accept 系统调用的具体实现函数为sock accept 函数。 我们注意到sys-socketcall 函数接收两个参数, 第一个参数call 表示具体被调用的应用层 接口函数( 如accept 函数) , 第二参数是一个指针, 指向具体被调用函数( 如accept 函数) 所需的参数。这些用户在进行系统调用时传入的参数将原封不动地传递给内核网络栈相关底 层实现函数使用( 如对应accept 函数的底层实现函数为sock-accept 函数) 。
accept系统调用到达sys_socketcall函数要经过三层入口,第一层是accept.S文件,第二层是socket.S文件,第三层是entry.S文件,
4 应用层流程
4.1 socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。在 Linux 系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读 取,使得对网络的控制和对文件的控制一样方便。
socket结构:
// 普通的 BSD 标准 socket 结构体
// socket_state: socket 状态, 连接?不连接?
// type: socket type (%SOCK_STREAM, etc)
// flags: socket flags (%SOCK_NOSPACE, etc)
// ops: 专用协议的socket的操作
// file: 与socket 有关的指针列表
// sk: 负责协议相关结构体,这样就让这个这个结构体和协议分开。
// wq: 等待队列
struct socket {
socket_state state;
kmemcheck_bitfield_begin(type);
short type;
kmemcheck_bitfield_end(type);
unsigned long flags;
struct socket_wq __rcu *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
-
state是socket的状态,比如CONNECTED,type是类型,比如TCP下使用的流式套接字SOCK_STREAM.
-
flags是标志位,负责一些特殊的设置,比如SOCK_ASYNC_NOSPACE
-
ops则是采用了和超级块设备操作表一样的逻辑,专门设置了一个数据结构来记录其允许的操作。
-
Sk是非常重要的,也是非常大的,负责记录协议相关内容。这样的设置使得socket具有很好的协议无关性,可以通用。
-
file是与socket相关的指针列表,wq是等待队列。
4.2 socket 的创建
Socket()本质上是一个glibc中的函数,执行实际上是是调用sys_socketcall()系统调用。sys_socketcall()是几乎所有socket相关函数的入口,即是说,bind,connect等等函数都需要sys_socketcall()作为入口。
而对于创建socket,自然会在switch中调用到sys_socket()系统调用,而这个函数仅仅是调用sock_create()来创建socket和sock_map_fd()来与文件系统进行关联。
sock_create() 内部的主要结构是 socket 结构体,其主要负责socket 结构体的创建(sock_alloc())和初始化,以及指定socket套接字的类型和操作函数集,然后分配一个文件描述符作为socket套接字的操作句柄,该描述符就是我们常说的套接字描述符。socket 的创建主要是分配一个inode 对象来说实现的。inode 对面内部有一个 union 类型变量,里面包含了各种类型的结构体,这里采用的 socket 类型,然后二者建立关联,inode中的union采用socket,socket结构中的inode指针指向该inode对象。
inet_create() 内部的主要结构是 sock 结构体,sock 结构体比socket 结构更显复杂,其使用范围也更为广泛,socket 结构体是一个通用的结构,不涉及到具体的协议,而sock 结构则与具体的协议挂钩,属于具体层面上的一个结构。inet_create 函数的主要功能则是创建一个 sock 结构(kmalloc())然后根据上层传值下来的协议(通常是类型与地址族组合成成对应的协议)进行初始化。最后将创建好的 sock 结构插入到 sock 表中。
socket创建的调用树如下图:

4.3 发数据
对于send函数,首先TCP是面向连接的,会有三次握手,建立连接成功,即代表两个进程可以用send和recv通信,作为发送信息的一方,肯定是接收到了从用户程序发送数据的请求,即send函数的参数之一,接收到数据后,若数据的大小超过一定长度,肯定不可能直接发送除去,因此,首先要对数据分段,将数据分成一个个的代码段,其次,TCP协议位于传输层,有响应的头部字段,在传输时肯定要加在数据前,数据也就被准备好了。当然,TCP是没有能力直接通过物理链路发送出去的,要想数据正确传输,还需要一层一层的进行。所以,最后一步是将数据传递给网络层,网络层再封装,然后链路层、物理层,最后被发送除去。
当调用send()函数时,内核封装send()为sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto。
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg;
struct iovec iov;
int fput_needed;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
这里定义了一个struct msghdr msg,他是用来表示要发送的数据的一些属性。
struct msghdr {
void *msg_name; /* 接收方的struct sockaddr结构体地址 (用于udp)*/
int msg_namelen; /* 接收方的struct sockaddr结构体地址(用于udp)*/
struct iov_iter msg_iter; /* io缓冲区的地址 */
void *msg_control; /* 辅助数据的地址 */
__kernel_size_t msg_controllen; /* 辅助数据的长度 */
unsigned int msg_flags; /*接受消息的表示 */
struct kiocb *msg_iocb; /* ptr to iocb for async requests */
};
__sys_sendto函数其实做了3件事:
1.通过fd获取了对应的struct socket
2.创建了用来描述要发送的数据的结构体struct msghdr。
3.调用了sock_sendmsg来执行实际的发送。
sys_sendto构建完这些后,调用sock_sendmsg继续执行发送流程,传入参数为struct msghdr和数据的长度。忽略中间的一些不重要的细节,sock_sendmsg继续调用sock_sendmsg(),sock_sendmsg()最后调用struct socket->ops->sendmsg,即对应套接字类型的sendmsg()函数,所有的套接字类型的sendmsg()函数都是 sock_sendmsg,该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。
4.4 收数据
对于recv函数,与send类似,自然也是recvfrom的特殊情况,调用的也就是__sys_recvfrom,整个函数的调用路径与send非常类似:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
......
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
.....
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don't copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
.....
}
__sys_recvfrom调用了sock_recvmsg来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg.接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock);
lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
这里共维护了三个队列:prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter,这里同样用了struct msghdr *msg来实现。
int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;
/* 拷贝tcp头部 */
if (copy > 0) {
if (copy > len)
copy = len;
n = cb(skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
/* 拷贝数据部分 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
WARN_ON(start > offset + len);
end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
struct page *page = skb_frag_page(frag);
u8 *vaddr = kmap(page);
if (copy > len)
copy = len;
n = cb(vaddr + frag->page_offset +
offset - start, copy, data, to);
kunmap(page);
offset += n;
if (n != copy)
goto short_copy;
if (!(len -= copy))
return 0;
}
start = end;
}
5 传输层流程
该层是网络栈实现中核心的一层, TCP 、UDP 实现都定义在这一层。虽然一般我们从理论上将ICMP 、IGMP 都作为网络层协议看待,但实现上它们都是封装在IP 协议报文中, 所以从实现上我们将它们作为传输层协议看待。
5.1 发数据
tcp_sendmsg实际上调用的是tcp_sendmsg_locked函数
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/
struct sk_buff *skb;
flags = msg->msg_flags;
......
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now,
TCP_NAGLE_PUSH, size_goal);
}
在tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。
struct sock{
...
struct sk_buff_head sk_write_queue;/*指向skb队列的第一个元素*/
...
struct sk_buff *sk_send_head;/*指向队列第一个还没有发送的元素*/
}
在tcp协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,置一的过程如下:
static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
skb = tcp_write_queue_tail(sk);
if (!skb)
return;
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTLED bit is already set */
if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
}
/* It is possible TX completion already happened
* before we set TSQ_THROTTLED.
*/
if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle);
}
首先struct tcp_skb_cb结构体存放的就是tcp的头部,头部的控制位为tcp_flags,通过tcp_mark_push会将skb中的cb,也就是48个字节的数组,类型转换为struct tcp_skb_cb,这样位于skb的cb就成了tcp的头部。
struct sk_buff {
...
char cb[48] __aligned(8);
...
struct tcp_skb_cb {
__u32 seq; /* Starting sequence number */
__u32 end_seq; /* SEQ + FIN + SYN + datalen */
__u8 tcp_flags; /* tcp头部标志,位于第13个字节tcp[13]) */
......
};
然后,tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);随后又调用了tcp_write_xmit来发送数据:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)
{
if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
sk_gfp_mask(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk);
}
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
int result;
bool is_cwnd_limited = false, is_rwnd_limited = false;
u32 max_segs;
/*统计已发送的报文总数*/
sent_pkts = 0;
......
/*若发送队列未满,则准备发送报文*/
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
/* "skb_mstamp_ns" is used as a start point for the retransmit timer */
skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
tcp_init_tso_segs(skb, mss_now);
goto repair; /* Skip network transmission */
}
if (tcp_pacing_check(sk))
break;
tso_segs = tcp_init_tso_segs(skb, mss_now);
BUG_ON(!tso_segs);
/*检查发送窗口的大小*/
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
if (push_one == 2)
/* Force out a loss probe pkt. */
cwnd_quota = 1;
else
break;
}
.......
}
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb。
tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);
......
/* 构建TCP头部和校验和 */
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(rcv_nxt);
......
}
5.2 收数据
tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数;其协议操作函数结构如下所示,其中handler即为IP层向TCP传递数据包的回调函数,设置为tcp_v4_rcv;
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
tcp_v4_rcv函数只要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段;
之后,调用的也就是__sys_recvfrom,整个函数的调用路径与send非常类似。整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags),同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg .接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock);
lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
}
这里共维护了三个队列:prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter,这里同样用了struct msghdr *msg来实现。
int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;
/* 拷贝tcp头部 */
if (copy > 0) {
if (copy > len)
copy = len;
n = cb(skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
/* 拷贝数据部分 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
WARN_ON(start > offset + len);
end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
struct page *page = skb_frag_page(frag);
u8 *vaddr = kmap(page);
if (copy > len)
copy = len;
n = cb(vaddr + frag->page_offset +
offset - start, copy, data, to);
kunmap(page);
offset += n;
if (n != copy)
goto short_copy;
if (!(len -= copy))
return 0;
}
start = end;
}
6 网络层
网络层模块实现, 主要是指IP 协议实现文件, 在该层一个重要的概念就是IP 路由: 即根据最终目的伊地址选择合适的传输路径。在IP 协议实现中, 凡是发送数据包, 都要查询路由表, 选择合适的路由项, 确定下一站的地址, 由此构造MAC 首部, 进而将数据包发往下一层( 链路层) 进行处理。
另外防火墙功能也基于网络层实现, 在IP 协议实现中, 夹杂着大量使用防火墙函数对数据包进行过滤的函数调用。另外我们将RAW 类型套接字也放在网络层进行分析,使用RAW 套接字时, 用户必须自行创建网络层和传输层首部。PACKET 类型套接字要求用户自行完成整个数据帧的格式化工作, 由于PACKET 类型套接字直接使用链路层模块接口函数进行数据包的发送。
6.1 发数据
入口函数是ip_queue_xmit,ip_queue_xmit是 ip 层提供给 tcp 层发送回调函数。ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成。ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)。
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
return __ip_queue_xmit(sk,skb,fl,iner)sk(sk)-->tos);
}
函数ip_queue_xmit调用函数__ip_queue_xmit
/* Note: skb->sk can be different from sk, in case of tunnels */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
__u8 tos)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
rcu_read_lock();
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
//獲取skb中的路由緩存
rt = skb_rtable(skb);
if (rt)
goto packet_routed;
/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) {
__be32 daddr;
Skb_rtable(skb)获取 skb 中的路由缓存,然后判断是否有缓存,如果有缓存就直接进行packet_routed函数,否则就 执行ip_route_output_ports查找路由缓存。
/* TODO : should we use skb->sk here instead of sk ? */
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(net, sk, skb);
rcu_read_unlock();
return res;
最后调用 ip_local_out得到返回值res:
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);
return err;
}
同函数ip_queue_xmit一样,ip_local_out函数内部调用__ip_local_out。
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
/* if egress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_out(sk, skb);
if (unlikely(!skb))
return 0;
skb->protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);
}
发现返回一个nf_hook函数,里面调用了dst_output,这个函数实质上是调用ip_finish__output函数,ip_finish__output函数内部在调用__ip_finish_output函数
static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
unsigned int mtu;
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
/* Policy lookup after SNAT yielded a new policy */
if (skb_dst(skb)->xfrm) {
IPCB(skb)->flags |= IPSKB_REROUTED;
return dst_output(net, sk, skb);
}
#endif
mtu = ip_skb_dst_mtu(sk, skb);
if (skb_is_gso(skb))
return ip_finish_output_gso(net, sk, skb, mtu);
if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
return ip_finish_output2(net, sk, skb);
}
如果分片就调用ip_fragment,否则就调用ip_fragment函数:
static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
unsigned int mtu,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
struct iphdr *iph = ip_hdr(skb);
if ((iph->frag_off & htons(IP_DF)) == 0)
return ip_do_fragment(net, sk, skb, output);
if (unlikely(!skb->ignore_df ||
(IPCB(skb)->frag_max_size &&
IPCB(skb)->frag_max_size > mtu))) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(mtu));
kfree_skb(skb);
return -EMSGSIZE;
}
return ip_do_fragment(net, sk, skb, output);
}
在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output进行输出。最后调用dev_queue_xmit函数进行向下层发送包。
gdb验证如下:

6.2 收数据
IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃
/*
* IP receive entry point
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
ip_rcv函数内部会调用ip_rcv_finish函数:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* if ingress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_rcv(skb);
if (!skb)
return NET_RX_SUCCESS;
ret = ip_rcv_finish_core(net, sk, skb, dev);
if (ret != NET_RX_DROP)
ret = dst_input(skb);
return ret;
}
dst_input函数会进一步调用ip_local_deliver函数:
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
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_protocol_deliver_rcu函数:
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
const struct net_protocol *ipprot;
int raw, ret;
resubmit:
raw = raw_local_deliver(skb, protocol);
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
return;
}
nf_reset_ct(skb);
}
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
} else {
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb(skb);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}
gdb验证如下:

7 数据链路层

链路层首部封装格式
链路层( 或者说设备层) 模块实现文件为dev.c 文件, 该文件实现的模块作为驱动程序和网络层( 以及LLC 控制层) 之间的接凵模块而存在。以太网首部创建模块实现文件为eth.c。
7.1 发数据
发送端调用dev_queue_xmit,函数在调用__dev_queue_xmit:
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev){
.......
* Check this and shot the lock. It is not prone from deadlocks.
*Either shot noqueue qdisc, it is even simpler 8)
*/
if (dev->flags & IFF_UP) {
int cpu = smp_processor_id(); /* ok because BHs are off */
if (txq->xmit_lock_owner != cpu) {
if (dev_xmit_recursion())
goto recursion_alert;
skb = validate_xmit_skb(skb, dev, &again);
if (!skb)
goto out;
HARD_TX_LOCK(dev, txq, cpu);
if (!netif_xmit_stopped(txq)) {
dev_xmit_recursion_inc();
skb = dev_hard_start_xmit(skb, dev, txq, &rc);
dev_xmit_recursion_dec();
if (dev_xmit_complete(rc)) {c
HARD_TX_UNLOCK(dev, txq);
goto out;
}
}
}
__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb。
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
struct netdev_queue *txq, int *ret)
{
struct sk_buff *skb = first;
int rc = NETDEV_TX_OK;
while (skb) {
struct sk_buff *next = skb->next;
skb_mark_not_on_list(skb);
rc = xmit_one(skb, dev, txq, next != NULL);
if (unlikely(!dev_xmit_complete(rc))) {
skb->next = next;
goto out;
}
skb = next;
if (netif_tx_queue_stopped(txq) && skb) {
rc = NETDEV_TX_BUSY;
break;
}
}
out:
*ret = rc;
return skb;
}
rc = xmit_one(skb, dev, txq, next != NULL);在xmit_one中调用__net_dev_start_xmit函数。一旦网卡完成报文发送,将产生中断通知 CPU,然后驱动层中的中断处理程序就可以删 除保存的 skb 。
gdb调试如下:

7.2 收数据
接受数据的入口函数是net_rx_action
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
unsigned long time_limit = jiffies +
usecs_to_jiffies(netdev_budget_usecs);
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;
if (list_empty(&list)) {
if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
goto out;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
进入函数napi_poll,进一步调用napi_gro_receive函数
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
gro_result_t ret;
skb_mark_napi_id(skb, napi);
trace_napi_gro_receive_entry(skb);
skb_gro_reset_offset(skb);
ret = napi_skb_finish(dev_gro_receive(napi, skb), skb);
trace_napi_gro_receive_exit(ret);
return ret;
}
napi_gro_receive 会直接调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。
8 系统网络栈初始化
我们从int/main.c 文件出发, 在执行了前面一系列具体处理器架构初始化代码之后, 最终将进入到init/main.c 中的start_kernel 函数执行, 系统网络栈初始化总入口函数将在start_kernel 中被调用: 即sock_init 函数, 就从该函数出发完成整个网络栈的初始化过程。
8.1 网络栈初始化流程
sock_init函数定义在net/socket.c 中, 进行如下操作。 (1 ) 调用proto_ init 进行协议实现模块初始化。 ( 2 ) 调用dev_init 进行网络设各驱动层模块初始化。 ( 3 ) 初始化专门用于网络处理的下半部分执行函数net_bh , 并使能之。 ( 4 ) 其将由变量pops 指向的域操作吆数集合( 如INET 域、UNIX 域) 数组清零, 在此 后的网络栈初始化中会对其进行正确的初始化。 其中网络设备驱动层模块初始化函数dev_init。
proto 函数定义在net/socket.c 中, 其遍历由protocols 全局变量( 定义在 中) 指向的域初始化函数集, 进行各域的初始化, 如INET 域的初始化函数为inet_proto_init , 其将在proto_init 函数被调用以进行INET 域的初始化。当然除了INET 域外, 还有其他域类 型, 如UNIX 域, 木章我们只讨论标准的网络栈实现] NET 域。 inet-proto 函数定义在net/inet/af_inet.c 中, 其调用如下函数。 (1 ) inet_protocol_add : 进行传输层和网络层之间的衔接。具体是通过将由 inet-protocol base 全局变量指向的inet_protocol 结构队列中元索散列到inet_protos 数组中, 从而被网络层使用。inet_protocol_base 和inet_protos 变量以及inet_protocol_add 函数均定义 在net/inet/protocol.c 中, inet_protocol 结构的作用类似于packet_type 结构, 都是作为两层之 间的衔接之用, 只不过inet _protocol 结构用于传输层和网络层之间, 而packet_type 结构用于 网络层和链路层之间。当网络层处理函数结束本层的处理后, 其将查询inet_protos 数组, 匹 配合适的传输层协议, 调用其对应inet_protocol 结构中注册的接收函数, 从血将数据包传递 给传输层协议处理。当然原则上, 我们在网络层也可以直接查询由inet_protocol 指向的 队列, 但由于传输层协议较多, 采用数组散列方式可以提高效率也便于进行代码的扩展。有 关网络层向传输层的数据包传递过程请参考ip_rcv 函数(net/inet/ip.c)。 ( 2 ) arp_init : ARP 协议初始化函数, ARP 协议是网络层协议, 为了从链路层接收数据包, 其必须定义一个packet_type 结构, 并调用dev_add_pack 函数(dev.c) 向链路层模块注册 , arp_init 函数主要完成这个注册工作, 同时注册事件通知句柄函数, 监听网络设备状态变化事件, 从而对ARP 缓存进行及时刷新, 以维护ARP 缓存内容的有效性。
( 3 ) ip_init : IP协议初始化函数, 同样完成该协议接收数对链路层的注册, 同时由于 路由表项与网络设备绑定的原因, 其也心须注册网络设备状态变化事件侦听函数对此类事件 进行侦听。 IP协议和ARP 协议都作为网络层协议, 它们的初始化函数完成的功能基本相同, 主要有以下2 个方面。
( 1 ) 向链路层注册数据包接收函数, 完成网络层和链路层之间的衔接。 ( 2 ) 由于ARP 表项以及路由表项都与某个网络设备接口绑定, 故为了保证这些表项的有 效性, 二者都必须对网络设备状态变化事件进行检测, 以便在某个网络设备状态变化时作出 反应, 所以在ARP 协议、IP 协议初始化代码中都注册了网络设各事件侦听函数。 至此, 网络栈初始化工作全部完成, 经过inet_proto_init 、arp_init , ip_init 函数的执行, 系统完成了由下而上的各层接口之间的衔接。 ( 1) 链路层和网络层通过ptype_base 指向的packet_type 结构队列进行衔接, 每个 packet_type 结构表示一个网络层协议, 结构中定义有网络层协议号及其接收函数, 链路层模 块将根据链路层首部中标识的网络层协议号在队列中进行查找, 从而调用对应的接收函数将 数据包上传给网络层协议进行处理。 ( 2 ) 网络层和传输层通过lnet_protos 散列表( 实际就是一个数组) 进行衔接, 表中每个 表项指向一个inet_protocol 结构队列, 每个inet_protocol 结构表示一个传输层协议。结构中 定义有传输层协议号及其接收函数, 网络层模块将根据网络层协议首部中标示的传输层协议 号在inet_protos 表中进行匹配查询, 从而得到传输层协议对应的inet_protocol 结构, 调用结 构中注册的接收函数, 将数据包上传给传输层进行处理。
8.2 数据包传送通道解析
综上所述, 我们得到网络栈自下而上的数据包传输通道, 具体如下。 ( 1 ) 硬件监听物理传输介质, 进行数据的接收, 当完全接收一个数据包后, 产生中断( 注 意这个过程完全由网络设备硬件负责)。 ( 2 ) 中断产生后, 系统调用驱动程序注册的中断处理程序进行数据接收, 一般在接收例 程中, 完成数据从硬件缓冲区到内核缓冲区的复制, 将其封装为内核特定结构( sk_ buff结构) , 最后调用链路层提供的接口函数netif_rx , 将数据包由驱动层传递给链路层。 ( 3 ) 在netif_rx 中, 为了减少中断执行时间( 注意netif_rx 函数一般在驱动层中断例程中 被调用) , 该函数将数据包暂存于链路层维护的一个数据包队列中( 具体的由变量指 向) , 并启动专门进行网络处理的下半部分对backlog 队列中数据包进行处理。 ( 4 ) 网络下半部分执行函数net_bh , 从backlog 队列中取数据包, 在进行完本层相关处 理后, 遍历ptype_base 指向的网络层协议( 由packet_type 结构表示) 队列, 进行协议号的匹 配。找到协议号匹配的packet_type 结构, 调用结构中接收函数, 完成数据包从链路层到网络 层的传递。对于ARP 协议, 那么调用的就是arp_rcv 函数, IP 协议则是ip_rcv 函数, 以下我 们以IP 协议为例。 (5) 假设数据包使用的是IP 协议, 那么从链路层传递到网络层时, 将进入ip_rcv 总入口 函数, ip_rcv 完成本层的处理后, 以本层首部( 即IP 首部) 中标识的传输层协议号为散列值, 对inet_protos 散列表进行匹配查询, 以寻找到合适的inet_rotocol 结构, 进而调用结构中接 收函数, 完成数据包从网络层到传输层的传递。对于UDP 协议, 调用udp_rcv 函数, TCP 协议调用tcp_rcv 函数, ICMP 协议调用icmp_rcv 函数, IGMP 协议调用igmp_rcv 函数。注意 一般我们将ICMP 、IGMP 协议都称为网络层协议, 但它们都封装在IP 协议中, 所以实现上, 是作为传输层协议看待的。 ( 6 ) 令数据包使用的是TCP 传输层协议, 那么此时将进入tcp rcv 函数。所有使用TCP 协议的套接字对应sock 结构都被挂入tcp prot 个局变量表示的proto 结构之sock array 数组中, 采用以本地端口号为索引的插入方 式, 所以当tcp_rcv 函数接收到一个数据包。在完成必要的检查和处理后, 其将以TCP 协议 首部中目的端口号( 对于一个接收的数据包而言, 其目的端口号就是本地所使用的端口号) 为索引, 在对应sock 结构之array 数组中得到正确的sock 结构队列, 再辅之以 其他条件遍历该队列进行对应sock 结构的查询, 在得到匹配的sock 结构后, 将数据包挂入 该sock 结构中的缓存队列中( 由sock 结构中receivequeue 字段指向),从而完成数据包的最 终接收。 ( 7 ) 当用户需要读取数掘时, 其首先根据文件描述符得到对应的节点( inode 结构表示) , 由节点得到对应的socket 结构( 作为结构中union 类型字段存在) , 进而得到对应的sock 结构(socket 和sock 结构之间维护有相互指向的指针字段) 。之后即从scwk 结构queue 指向的队列中取数据包, 将数据包中数据拷贝到用户缓冲区, 从而完成数据的读取。
时序图



浙公网安备 33010602011771号