TCP/IP协议栈在Linux内核中的运行时序分析
TCP/IP协议栈在Linux内核中的运行时序分析
0.调研要求
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 应该包括时序图
1.TCP/IP协议栈
TCP/IP协议是用于计算机通信的一组协议,通常被称为TCP/IP协议栈,以它为基础组建的互联网是目前国际上规模最大的计算机网络。TCP/IP协议模型可分为应用层、传输层、网络层及网络接口层。网络接口层对应OSI模型中的物理层和数据链路层,是TCP/IP协议模型的最底层,通常在描述TCP/IP模型时还是会划分具体为物理层和数据链路层。
1.1 应用层
应用层的主要工作为:规定应用进程在通信时需要遵循的协议。每个应用层协议都是为了解决某一类应用问题,而问题的解决又往往是通过位于不同主机中的多个应用进程之间的通信和协同工作来完成的。常见的应用层协议有:FTP、HTTP、DNS及TELNET。
1.2 传输层
传输层的主要工作为:向应用层提供通信服务。当网络的边缘部分中的两个主机使用网络的核心部分的功能进行端到端的通信时,只有位于网络边缘部分的主机的协议栈才有传输层,而网络核心部分中的路由器在转发分组时都只用到下三层的功能。传输层主要有TCP、UDP这2种传输协议。
1.3 网络层
网络层的主要工作为:向上提供简单灵活的、无连接的、尽最大努力交付的数据报服务。网络层不提供服务质量的承诺。即所传送的分组可能出错、丢失、重复和失序(不按序到达终点),当然也不保证分组传送的时限。如果主机(即端系统)中的进程之间的通信需要是可靠的,那么就由网络的主机中的传输层负责(包括差错处理、流量控制等)。常见的网络层协议有IP、ARP、ICMP及IGMP。
1.4 物理层和数据链路层(网络接口层)
物理层主要工作为:在连接计算机的传输媒体上传输比特流。网络设备、传输媒体多种多样,通信方式也各自不同,物理层正是尽可能地屏蔽掉这些差异,而让上层数据链路层无须顾虑,专心完成自己的协议和服务。
数据链路层主要工作为:当需要通过一条线路传输数据时,除了必须有一条物理线路外,还必须有一些必要的通信协议来控制数据的传输。把实现这些协议的硬件和软件加到链路上就构成了数据链路。网卡是数据链路层的典型实现。
2.Linux内核模型
内核空间模型如下图所示(本文流程图、模型图均使用WPS Mind制作,自带水印,非网图):
2.1 系统调用接口
所谓系统调用接口(System call interface)就是内核提供给用户的调用Api接口。
网络子系统调用接口为用户空间提供两种调用接口:一种是提供特有的调用进入系统内核,然后进一步调用sys_socketcall结束该进程,其中sys_socketcall根据系统调用号调用具体功能;另一种是通过socket系统调用,通过普通的文件操作来访问子系统。常用的系统调用有:socket、send、accept、connect、listen、read、write等。
2.2 协议无关接口
socket 层是一个协议无关接口(Protocol agnostic interface),它提供了一组通用函数来支持各种不同协议。socket 层不但可以支持典型的 TCP 和 UDP 协议,而且还支持RAW套接口、RAW以太网和其他协议。
只要是通过网络栈进行的通信,都需要对 socket 进行操作。
socket这个概念至关重要,在第三部分将对相关概念、相关数据结构进行详细介绍。
2.3 网络协议
网络协议(Network protocols)这一层对一些可用的特定网络协议作出了定义,如TCP、UDP等。这些协议都是在 linux/net/ipv4/af_inet.c 文件中的inet_init的函数中初始化的。net_init函数使用proto_register函数注册每个内嵌协议,而proto_register函数是在 linux/net/core/sock.c 中定义的,除了可以将这个协议添加到活动协议列表中之外,如果需要,还可以选择分配一到多个 slab 缓存。通过 linux/net/ipv4路径下udp.c和raw.c 文件中的 proto 接口,可以了解各个协议是如何标识自己的。每个协议接口都按照类型和协议映射到 inetsw_array数组中,该数组将内嵌协议与操作映射到一起...关于这一层还有许多内容可以说明,将在下文继续进行分析。
2.4 设备无关接口
网络协议层下面是另外一个无关接口层——设备无关接口(Device agnostic interface),其将协议与具有多功能的硬件设备连接在一起,提供了一组通用的函数供底层设备和上层协议栈调用,上层协议栈和底层驱动设备都不必关心对方实现,从而屏蔽掉了差异性。
2.5 设备驱动程序
底部是负责管理物理网络设备的设备驱动程序(Device drivers)。例如,SLIP 驱动程序以及以太网设备使用的以太网驱动程序均位于该层。在进行初始化时,设备驱动程序会分配一个 net_device 结构,然后使用相关程序对其进行初始化,这些程序中有一个为hard_start_xmit,参数为sk_buff,定义了上层应该如何对 sk_buff 排队进行传输。这个函数的操作取决于底层硬件,但是通常情况下sk_buff 所描述的报文都会被移动到硬件环或队列中。
网络设备可以动态的注册到系统中,提高了驱动设备的灵活性。register_netdevice和unregister_netdevice是驱动设备注册和解注册的函数。net_device结构中的hard_start_xmit在初始化时设置该接口,实现向网络设备输出数据包。
3 Socket
3.1 Socket定义
Socket,中文名为套接字,它是通信的基石,是支持TCP/IP协议通信的基本操作单元。我们可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据,各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
如下图所示,Socket是应用层与TCP/IP协议栈通信的中间软件抽象层,它是一组接口。在设计模式中,它把复杂的TCP/IP协议族隐藏在Socket接口后面。可以理解为:TCP/IP协议栈作为Linux内核的一个软件模块嵌入到Linux内核中,TCP/IP协议栈向上对接Socket套接口,向下对接二层数据链路层。
Linux 中的socket结构是struct sock,这个结构是在linux/include/net/sock.h中定义的。这个结构中包含了特定socket所需要的所有状态信息,其中包括socket所使用的特定协议和在socket上可以执行的一些操作。我们可以在linux/include/net/sock.h中看到,每个协议都维护了一个名为proto的结构,这个结构定义了可以在从socket层到传输层中执行特定的socket操作,如:创建socket、建立socket连接、关闭socket 等。
3.2 Socket API
Socket API库函数是操作系统提供的用于网络通信的 API。Socket API库函数内部使用了系统调用的封装例程,其主要目的是发布系统调用,使程序员在写代码时不需要用汇编指令和寄存器传递参数来触发系统调用。下面展示一些常见API,本文以tcp数据发送接收为例,udp的数据接收API在此不进行详细介绍。
3.2.1 常见API
- socket()
- 原型:int socket (int domain, int type, int protocol)
- 功能描述:初始化创建socket对象,通常是第一个调用的socket函数。 成功时,返回非负数的socket描述符;失败是返回-1。socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用socket()函数时,socket执行体将建立一个socket,为一个socket数据结构分配存储空间。
- bind()
- 原型:int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)
- 功能描述:将创建的socket绑定到指定的IP地址和端口上,通常是第二个调用的socket函数。返回值:0代表成功,-1代表出错。当socket函数返回一个描述符时,只是存在于其协议族的空间中,并没有分配一个具体的协议地址(这里指IPv4/IPv6和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。通常服务器在启动的时候都会绑定一个众所周知的协议地址,用于提供服务,客户就可以通过它来接连服务器;而客户端可以指定IP或端口也可以都不指定,未分配则系统自动分配。
- listen()
- 原型:int listen(int sockfd, int backlog)
- 功能描述:listen()函数仅被TCP类型的服务器程序调用,实现监听服务。当socket()创建socket时,被假设为主动式套接字,也就是说它是一个将调用connect()发起连接请求的客户端套接字;函数listen()将套接口转换为被动式套接字,指示内核接受向此套接字的连接请求,调用此系统调用后tcp 状态机由close转换到listen。
- accept()
- 原型: int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen)
- 功能描述:accept()函数仅被TCP类型的服务器程序调用,从已完成连接队列返回下一个建立成功的连接,如果已完成连接队列为空,线程进入阻塞态睡眠状态。成功时返回套接字描述符,错误时返回-1。如果accpet()执行成功,返回由内核自动生成的一个全新socket描述符,用它引用与客户端的TCP连接。通常我们把accept()第一个参数成为监听套接字,把accept()功能返回值成为已连接套接字。
- connect()
- 原型: int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
- 功能描述:connect()通常由TCP类型客户端调用,用来与服务器建立一个TCP连接,实际是发起3次握手过程,连接成功返回0,连接失败返回1。
- send()
- 原型:int send(int sockfd, const void *msg, int len, int flags)
- 功能描述:TCP类型的数据发送。每个TCP套接口都有一个发送缓冲区,它的大小可以用SO_SNDBUF这个选项来改变。调用send函数的过程,实际是内核将用户数据拷贝至TCP套接口的发送缓冲区的过程:若len大于发送缓冲区大小,则返回-1;否则,查看缓冲区剩余空间是否容纳得下要发送的len长度,若不够,则拷贝一部分,并返回拷贝长度;若缓冲区满,则等待发送,有剩余空间后拷贝至缓冲区;若在拷贝过程出现错误,则返回-1。
- recv()
- 原型:int recv(int sockfd, void *buf, int len, unsigned int flags)
- 功能描述:TCP类型的数据接收。recv()从接收缓冲区拷贝数据。成功时,返回拷贝的字节数,失败返回-1。阻塞模式下,recv/recvfrom将会阻塞到缓冲区里至少有一个字节(TCP)/至少有一个完整的UDP数据报才返回,没有数据时处于休眠状态。若非阻塞,则立即返回,有数据则返回拷贝的数据大小,否则返回错误-1。
3.2 Socket编程实例
以下的程序使用到了上文所提及的Socket API,本文使用以下TCP网络程序为范例,进行send和recv过程中的时序分析。
3.2.2 服务端程序
/* server.c */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit */
#include <sys/types.h>
#include <string.h> /* memset */
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include "socketwrapper.h"
#define true 1
#define false 0
#define MYPORT 3490 /* 监听的端口 */
#define BACKLOG 10 /* listen的请求接收队列长度 */
int main()
{
int sockfd, new_fd; /* 监听端口,数据端口 */
struct sockaddr_in sa; /* 自身的地址信息 */
struct sockaddr_in their_addr; /* 连接对方的地址信息 */
unsigned int sin_size;
#ifdef _WINDOWS
Start
#endif // _WINDOWS
if ((sockfd = Socket(PF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
sa.sin_family = AF_INET;
sa.sin_port = Htons(MYPORT); /* 网络字节顺序,小端转大端 */
sa.sin_addr.s_addr = INADDR_ANY; /* 自动填本机IP */
memset(&(sa.sin_zero), 0, 8); /* 其余部分置0 */
if (Bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1)
{
perror("bind");
exit(1);
}
if (Listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}
/* 主循环 */
while (1)
{
sin_size = sizeof(struct sockaddr_in);
new_fd = Accept(sockfd,
(struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1)
{
perror("accept");
continue;
}
printf("Got connection from %s\n",
Inet_ntoa(their_addr.sin_addr));
if (Send(new_fd, "Hello, world!\n", 14, 0) == -1)
perror("send");
char reply[BUFSIZ] = { 0 };
int nrecv = Recv(new_fd, reply, BUFSIZ, 0);
reply[nrecv] = '\0';
printf("From client: %s\n", reply);
Close(new_fd);
}
Close(sockfd);
#ifdef _WINDOWS
End
#endif // _WINDOWS
return true;
}
3.2.3 客户端程序
/* client.c */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit */
#include <sys/types.h> /* WNOHANG */
#include <string.h> /* memset */
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include "socketwrapper.h"
#define true 1
#define false 0
#define PORT 3490 /* Server的端口 */
#define MAXDATASIZE 100 /* 一次可以读的最大字节数 */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he; /* 主机信息 */
struct sockaddr_in server_addr; /* 对方地址信息 */
#ifdef _WINDOWS
Start
#endif // _WINDOWS
if (argc != 2)
{
fprintf(stderr, "usage: client hostname\n");
exit(1);
}
/* get the host info */
if ((he = Gethostbyname(argv[1])) == NULL)
{
/* 注意:获取DNS信息时,显示出错需要用herror而不是perror */
/* herror 在新的版本中会出现警告,已经建议不要使用了 */
perror("gethostbyname");
exit(1);
}
if ((sockfd = Socket(PF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = Htons(PORT); /* short, NBO */
server_addr.sin_addr = *((struct in_addr *)he->h_addr_list[0]);
memset(&(server_addr.sin_zero), 0, 8); /* 其余部分设成0 */
if (Connect(sockfd, (struct sockaddr *)&server_addr,
sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(1);
}
if ((numbytes = Recv(sockfd, buf, MAXDATASIZE, 0)) == -1)
{
perror("recv");
exit(1);
}
buf[numbytes] = '\0';
printf("Received: %s", buf);
// 这里添加一个发送hi
char hi[] = "Hi";
if (Sendto(sockfd, hi, sizeof(hi) / sizeof(char), 0, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
perror("send error");
Close(sockfd);
#ifdef _WINDOWS
End
#endif // _WINDOWS
return true;
}
实际运行效果如上。
2.发送
2.1 应用层
首先,示例程序server.c中循环调用了Send
函数:
...
if (Send(new_fd, "Hello, world!\n", 14, 0) == -1)
perror("send");
...
在socketwrapper.h中有Send
的宏函数定义:
#define Send(a,b,c,d) send(a,b,c,d) //Send函数原型为send,原程序只是为了系统兼容性设置宏函数。
在socket.h中,我们可以看到send
函数的声明:
/* Send N bytes of BUF to socket FD. Returns the number sent or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);
大致过程为:send
函数内部使用了系统调用的封装例程(libc库中),触发系统调用,执行sys_call
汇编代码,并根据系统调用号调用相应的处理函数sys_send
(通过EAX寄存器传递系统调用号),执行完后返回ret_from_sys_call()
,然后返回用户态继续执行。
以x86-64系统为例,arch/x86/entry/syscalls/syscall_64.tbl 定义了x86-64的系统调用内核处理函数,它们通过脚本转换按照系统调用号存入sys_call_table
数组中。do_syscall_64
执行系统调用内核处理函数,以sys_call_table
数组函数指针的方式调用相应的Linux内核实现代码。
do_syscall_64
函数代码如下,在arch/x86/entry/common.c可见:
#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;
enter_from_user_mode();
local_irq_enable();
ti = current_thread_info();
if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
nr = syscall_trace_enter(regs);
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
/* sys_call_table数组存放了系统调用号 */
regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
} else if (likely((nr & __X32_SYSCALL_BIT) &&
(nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
X32_NR_syscalls);
regs->ax = x32_sys_call_table[nr](regs);
#endif
}
__x64_sys_
是由SYSCALL_DEFINE
一类的宏定义来的实现的,在net/socket.c下可以找到:
/* 该函数充当了socket多路分配器,将系统调用转到其他具体的函数执行,并传递参数,后者中的每个函数都实现了一个更小的系统调用 */
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
...
switch (call) {
...
/* 根据参数call,选择调用的语句 */
case SYS_SEND:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
NULL, 0);
...
}
return err;
}
从以上程序可以看出,实际上调用的是_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;
/* 关键的一句,调用了sock_sendmsg进行消息发送 */
err = sock_sendmsg(sock, &msg);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
可见,该函数最终调用了sock_sendmsg
进行消息发送,我们进入sock_sendmsg
函数:
int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{
//安全检查
int err = security_socket_sendmsg(sock, msg,
msg_data_left(msg));
return err ?: sock_sendmsg_nosec(sock, msg);
}
简单检查后就进入sock_sendmsg_nosec
:
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
inet_sendmsg, sock, msg,
msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
/* sock结构体 */
struct socket {
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
实际调用的是inet_sendmsg
函数,该函数在net/ipv4/af_inet.c文件下:
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
if (unlikely(inet_send_prepare(sk)))
return -EAGAIN;
return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg,
sk, msg, size);
}
对于TCP而言,sk->sk_prot
指向tcp_prot
,调用的是tcp_sendmsg
。
2.2 传输层
在net/ipv4/tcp.c中,tcp_sendmsg
函数如下所示:
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
int ret;
lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);
return ret;
}
进入tcp_sendmsg_locked
进一步分析:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
...
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
/* 调用函数__tcp_push_pending_frames进行数据的发送 */
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
wait_for_sndbuf:
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now,
TCP_NAGLE_PUSH, size_goal);
err = sk_stream_wait_memory(sk, &timeo);
if (err != 0)
goto do_error;
mss_now = tcp_send_mss(sk, &size_goal, flags);
}
···
tcp_sendmsg
是在处理用户数据的存放,优先考虑报文的线性区,然后是分页区,必要时需要使用新skb或者新分页来存放用户数据。拷贝好用户数据后,接下来就是发送数据,主要涉及__tcp_push_pending_frames
、tcp_push_one
、tcp_push
这三个发送函数。实际上tcp_push_one
是tcp_push
的一种特殊形式,且tcp_push
简单封装后也会调用__tcp_push_pending_frames
。
进入到__tcp_push_pending_frames
函数中,该函数在net/tcp_output.c文件下:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)
{
/* If we are closed, the bytes will have to remain here.
* In time closedown will finish, we empty the write queue and
* all will be happy.
*/
if (unlikely(sk->sk_state == TCP_CLOSE))
return;
if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
sk_gfp_mask(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk);
}
调用了tcp_write_xmit
:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
···
/* Argh, we hit an empty skb(), presumably a thread
* is sleeping in sendmsg()/sk_stream_wait_memory().
* We do not want to send a pure-ack packet and have
* a strange looking rtx queue with empty packet(s).
*/
if (TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq)
break;
/* 关键函数 */
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
···
那么再进一步:
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
tcp_sk(sk)->rcv_nxt);
}
查看__tcp_transmit_skb
:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
···
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
···
}
tcp_transmit_skb
函数负责将tcp数据发送出去,TCP协议栈通过调用这个icsk->icsk_af_ops->queue_xmit
函数指针来触发IP协议栈代码发送数据。queue_xmit
函数指针实际指向的函数为__ip_queue_xmit
函数,下面我们进行进一步研究探索。
2.3 网络层
在net/ip_output.c文件中我们可以看到函数__ip_queue_xmit
函数的定义:
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
__u8 tos)
{
···
/* Use correct destination address if we have options. */
daddr = inet->inet_daddr;
if (inet_opt && inet_opt->opt.srr)
daddr = inet_opt->opt.faddr;
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS_TOS(sk, tos),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &rt->dst);
}
skb_dst_set_noref(skb, &rt->dst);
packet_routed:
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
goto no_route;
//构建报头
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
···
//关键
res = ip_local_out(net, sk, skb);
···
}
该函数调用了ip_local_out
设置数据包的总长度和校验和,那么我们查看该函数的内容:
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;
}
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;
//设置协议为ip协议
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);
}
检查通过,继续查看dst_output
函数的内容:
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
//如果是单播数据包,设置的是ip_output()
//如果是组播数据包,设置的是ip_mc_output().dev_queue_xmit
return skb_dst(skb)->output(net, sk, skb);
}
实际上调用的是ip数据包输出函数ip_output
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);
//设置数据包的输出网络设备和数据包网络层协议类型。
skb->dev = dev;
skb->protocol = htons(ETH_P_IP);
//经netfilter处理后,调用ip_finish_output()继续IP数据包的输出
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
查看NF_HOOK_COND
宏定义:
NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
那么我们继续看ip_finish_output
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int ret;
ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb);
switch (ret) {
case NET_XMIT_SUCCESS:
return __ip_finish_output(net, sk, skb);
case NET_XMIT_CN:
return __ip_finish_output(net, sk, skb) ? : ret;
default:
kfree_skb(skb);
return ret;
}
}
调用了__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);
//skb_is_gso的逻辑很简单,返回skb_shinfo(skb)->gso_size,gso_size表示生产GSO大包时的数据包长度,一般是mss的整数倍。
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);
}
查看数据包长度是否大于mtu,大于的话就使用ip_fragment
分段再发送,在ip_fragment
函数内部调用了ip_do_fragment
,这个函数内部是详细的分片流程,整个过程分为快速分片和慢速分片两种,如果存在分片列表frag_list
,并且通过检查,则走快速路径,复制每个分片的ip头等信息之后,发送出去;如果不存在分片列表,或者分片列表检查失败,则走慢速路径。无论分不分片,都会最终执行ip_finish_output2
函数,所以我们继续查看ip_finish_output2
函数:
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
···
/* if crossing protocols, can not use the cached header */
res = neigh_output(neigh, skb, is_v6gw);
···
}
我们来看看neigh_output
这个函数:
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
const struct hh_cache *hh = &n->hh;
// 头部存在,使用缓存输出
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb);
// 使用邻居项的输出回调函数输出,在连接或者非连接状态下有不同的输出函数 */
else
return n->output(n, skb);
}
该函数调用了neigh_hh_output
,找到该函数,进行分析:
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
unsigned int seq;
unsigned int hh_len;
//拷贝二层头到skb
do {
seq = read_seqbegin(&hh->hh_lock);
hh_len = hh->hh_len;
//二层头部<DATA_MOD,直接使用该长度拷贝
if (likely(hh_len <= HH_DATA_MOD)) {
/* this is inlined by gcc */
memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD);
}
//二层头部>=DATA_MOD,对齐头部,拷贝
else {
unsigned int hh_alen = HH_DATA_ALIGN(hh_len);
memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
}
} while (read_seqretry(&hh->hh_lock, seq));
skb_push(skb, hh_len);
//发送
return dev_queue_xmit(skb);
}
ip层在构造好ip头,检查完分片之后,会调用邻居子系统的输出函数neigh_output
进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output
进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。最后调用了dev_queue_xmit
进行发送,这就到了链路层了。
2.4 链路层
首先我们在net/core/dev.c中找到dev_queue_xmit
函数:
int dev_queue_xmit(struct sk_buff *skb)
{
return __dev_queue_xmit(skb, NULL);
查看__dev_queue_xmit
函数:
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
···
if (!netif_xmit_stopped(txq)) {
dev_xmit_recursion_inc();
//关键,这个地方判断一下txq不是stop状态,那么就直接调用dev_hard_start_xmit函数来发送数据
skb = dev_hard_start_xmit(skb, dev, txq, &rc);
dev_xmit_recursion_dec();
if (dev_xmit_complete(rc)) {
HARD_TX_UNLOCK(dev, txq);
goto out;
}
}
···
}
进而查看dev_hard_start_xmit
函数
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);
//调用xmit_one来发送一个到多个数据包
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;
}
dev_hard_start_xmit
调用xmit_one
来发送一个到多个数据包:
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, bool more)
{
unsigned int len;
int rc;
if (dev_nit_active(dev))
dev_queue_xmit_nit(skb, dev);
len = skb->len;
trace_net_dev_start_xmit(skb, dev);
rc = netdev_start_xmit(skb, dev, txq, more);
trace_net_dev_xmit(skb, rc, dev, len);
return rc;
}
使用trace_net_dev_start_xmit
、netdev_start_xmit
、trace_net_dev_xmit
送到驱动的tx函数,至此,发送过程结束。netdev_start_xmit
内部调用__netdev_start_xmit
,物理层收到发送请求。数据完整的输出到物理层设备上,转化为了比特流的形式。
2.5 gdb调试
gdb调试结果如上,符合预期。
3.接收
接收过程与发送过程分析思路相似,部分代码不展开叙述。
3.1 链路层
首先要从中断开始看,物理网卡收到包后触发irq中断通知cpu,中断上半部处理里将网卡设备的napi->poll_list
加入到softnet_data->poll_list
(中断上半部处理过程非本文论述重点,不展开描述),中断流程触发软中断后,结束中断上半部,进入中断下半部处理流程,rx软中断处理函数为net_rx_action
(在net/core/dev.c文件内定义)。
中断处理函数net_rx_action
调用设备的poll方法(默认为process_backlog),而process_backlog
函数将进一步调用netif_receive_skb()
将数据包传上协议栈,如果设备自身注册了poll
函数,也将调用netif_receive_skb()
函数。
static int process_backlog(struct napi_struct *napi, int quota)
{
struct softnet_data *sd = container_of(napi, struct softnet_data, backlog);
bool again = true;
int work = 0;
if (sd_has_rps_ipi_waiting(sd)) {
local_irq_disable();
net_rps_action_and_irq_enable(sd);
}
napi->weight = dev_rx_weight;
while (again) {
struct sk_buff *skb;
//在这个循环里面,系统将调用__skb_dequeue不断从设备上取出SKB,直到skb处理完为止
while ((skb = __skb_dequeue(&sd->process_queue))) {
rcu_read_lock();
//关键
__netif_receive_skb(skb);
rcu_read_unlock();
input_queue_head_incr(sd);
if (++work >= quota)
return work;
}
local_irq_disable();
rps_lock(sd);
if (skb_queue_empty(&sd->input_pkt_queue)) {
napi->state = 0;
again = false;
} else {
skb_queue_splice_tail_init(&sd->input_pkt_queue,
&sd->process_queue);
}
rps_unlock(sd);
local_irq_enable();
}
return work;
}
netif_receive_skb
函数根据不同的协议调用不同的协议处理函数,网桥的处理入口netif_receive_skb
里面,针对IP协议,ip_rcv
函数被调用,如下所示:
static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
{
struct net_device *orig_dev = skb->dev;
struct packet_type *pt_prev = NULL;
int ret;
ret = __netif_receive_skb_core(skb, pfmemalloc, &pt_prev);
if (pt_prev)
ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb,
skb->dev, pt_prev, orig_dev);
return ret;
}
3.2 网络层
首先在net/ipv4/ip_input.c查看ip_rcv
定义:
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_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
如下所示:
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
该函数调用了输入处理函数,调用 ip_local_deliver
函数,如果没有分片就调用ip_local_deliver_finish
,进一步调用ip_protocol_deliver_rcu
,对于 TCP 来说,函数tcp_v4_rcv
函数会被调用,从而处理流程进入 TCP 栈,如下所示:
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,skb);
3.3 传输层
在net/ipv4/tcp_ipv4.c文件中,我们可以见到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,
};
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
//非本机
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
__TCP_INC_STATS(net, TCP_MIB_INSEGS);
//检查头部数据,若不满足,则拷贝分片
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
//取tcp头
th = (const struct tcphdr *)skb->data;
//长度过小
if (unlikely(th->doff < sizeof(struct tcphdr) / 4))
goto bad_packet;
//检查头部数据,若不满足,则拷贝分片
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;
//取tcp头
th = (const struct tcphdr *)skb->data;
//取ip头
iph = ip_hdr(skb);
//移动ipcb
memmove(&TCP_SKB_CB(skb)->header.h4, IPCB(skb),
sizeof(struct inet_skb_parm));
barrier();
//获取开始序号
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
//获取结束序号,syn与fin各占1
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
//获取确认序号
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
//获取标记字节,tcp首部第14个字节
TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)->tcp_tw_isn = 0;
//获取ip头的服务字段
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
lookup:
//查找控制块
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, &refcounted);
if (!sk)
goto no_tcp_socket;
process:
//TIME_WAIT转过去处理
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
//TCP_NEW_SYN_RECV状态处理
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
struct sock *nsk;
//获取控制块
sk = req->rsk_listener;
if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
sk_drops_add(sk, skb);
reqsk_put(req);
goto discard_it;
}
//不是listen状态
if (unlikely(sk->sk_state != TCP_LISTEN)) {
//从连接队列移除控制块
inet_csk_reqsk_queue_drop_and_put(sk, req);
//根据skb参数重新查找控制块
goto lookup;
}
sock_hold(sk);
refcounted = true;
//处理第三次握手ack,成功返回新控制块
nsk = tcp_check_req(sk, skb, req, false);
//失败
if (!nsk) {
reqsk_put(req);
goto discard_and_relse;
}
//未新建控制块,进一步处理
if (nsk == sk) {
reqsk_put(req);
}
//有新建控制块,进行初始化等
else if (tcp_child_process(sk, nsk, skb)) {
//失败发送rst
tcp_v4_send_reset(nsk, skb);
goto discard_and_relse;
} else {
sock_put(sk);
return 0;
}
}
//TIME_WAIT和TCP_NEW_SYN_RECV以外的状态
//ttl错误
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
__NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard_and_relse;
//初始化nf成员
nf_reset(skb);
//tcp过滤
if (tcp_filter(sk, skb))
goto discard_and_relse;
//取tcp和ip头
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
//清空设备
skb->dev = NULL;
//LISTEN状态处理
if (sk->sk_state == TCP_LISTEN) {
//关键
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
//TIME_WAIT和TCP_NEW_SYN_RECV和LISTEN以外的状态
//记录cpu
sk_incoming_cpu_update(sk);
bh_lock_sock_nested(sk);
//分段统计
tcp_segs_in(tcp_sk(sk), skb);
ret = 0;
//未被用户锁定
if (!sock_owned_by_user(sk)) {
//未能加入到prequeue
if (!tcp_prequeue(sk, skb))
//进入tcpv4处理
ret = tcp_v4_do_rcv(sk, skb);
}
//已经被用户锁定,加入到backlog
else if (tcp_add_backlog(sk, skb)) {
goto discard_and_relse;
}
bh_unlock_sock(sk);
put_and_return:
//减少引用计数
if (refcounted)
sock_put(sk);
return ret;
no_tcp_socket:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (tcp_checksum_complete(skb)) {
csum_error:
__TCP_INC_STATS(net, TCP_MIB_CSUMERRORS);
bad_packet:
__TCP_INC_STATS(net, TCP_MIB_INERRS);
} else {
//发送rst
tcp_v4_send_reset(NULL, skb);
}
discard_it:
kfree_skb(skb);
return 0;
discard_and_relse:
sk_drops_add(sk, skb);
if (refcounted)
sock_put(sk);
goto discard_it;
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
//校验和错误
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
//TIME_WAIT入包处理
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
//收到syn
case TCP_TW_SYN: {
//查找监听控制块
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
&tcp_hashinfo, skb,
__tcp_hdrlen(th),
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
//删除tw控制块
inet_twsk_deschedule_put(inet_twsk(sk));
//记录监听控制块
sk = sk2;
refcounted = false;
goto process;
}
/* Fall through to ACK */
}
//发送ack
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
//发送rst
case TCP_TW_RST:
tcp_v4_send_reset(sk, skb);
//删除tw控制块
inet_twsk_deschedule_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
可以从源码中看到,该函数调用了tcp_v4_do_rcv
:
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
···
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
tcp_rcv_established(sk, skb);
return 0;
}
···
}
建立连接之后调用tcp_rcv_established
来进行数据的接收,而tcp_rcv_established
函数又调用了tcp_queue_rcv
:
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
而tcp_queue_rcv
有这样一个语句:
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
将发送的消息添加到队列的最尾端,发送之后进行系统调用唤醒socket,再使用tcp_recvmsg
函数去进行数据处理。
3.4 应用层
传输层的tcp_rcv_established
唤醒了程序进程,现在分析client.c的recv
函数,与之前的发送过程类似,recv
函数首先会经过系统调用,对于recv
函数,调用的是__sys_recvfrom
:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
···
err = sock_recvmsg(sock, &msg, flags);
···
}
可以看到,调用了sock_recvmsg
,而sock_recvmsg
又调用了sock_recvmsg_nosec
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,int flags)
{
return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
inet_recvmsg, sock, msg, msg_data_left(msg), flags);
}
可得,sock->ops->recvmsg
即inet_recvmsg
,在inet_recvmsg中
调用的是tcp_recvmsg
。
3.5 gdb调试
gdb调试结果如上,符合预期。
4.时序图
左侧数据从应用层流向物理层为发送过程,右侧数据从物理层流向应用层为接收过程。
5.参考资料
1.王雷,TCP/IP网络编程基础教程,北京理工大学出版社,2017.02,第4页
2.周怡,孟实,林雷主编,计算机网络基础实验指导,浙江工商大学出版社,2018.09,第150页
3.创客诚品,刘慧欣,孟令一编著,C语言从入门到精通 全新精华版,北京希望电子出版社,2017.10,第377页~第378页
4.https://blog.csdn.net/pashanhu6402/article/details/96428887
5.https://blog.csdn.net/yam_sunshine/article/details/95502205
6.孟宁老师的课件