当前位置 博文首页 > 吾心似秋月666:TCP/IP协议栈在Linux内核中的运行时序分析
网络程序设计调研报告
TCP/IP协议栈在Linux内核中的运行时序分析
姓名:柴浩宇
学号:SA20225105
班级:软设1班
2021年1月
调研要求
目录
1 调研要求
2 目录
3 Linux概述
3.1 Linux操作系统架构简介
3.2 协议栈简介
3.3 Linux内核协议栈的实现
4 本次调研采取的代码简介
5 应用层流程
5.1 发送端
5.2 接收端
6 传输层流程
6.1 发送端
6.2 接收端
7 IP层流程
7.1 发送端
7.2 接收端
8 数据链路层流程
8.1 发送端
8.2 接收端
9 物理层流程
9.1 发送端
9.2 接收端
10 时序图展示和总结
11 参考资料
正文
3 Linux概述
3.1 Linux操作系统架构简介
Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统。
内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。我们所调研的工作,就是在Linux内核层面进行分析。
3.2 协议栈简介
3.3 Linux内核协议栈
Linux的协议栈其实是源于BSD的协议栈,它向上以及向下的接口以及协议栈本身的软件分层组织的非常好。
Linux的协议栈基于分层的设计思想,总共分为四层,从下往上依次是:物理层,链路层,网络层,应用层。
物理层主要提供各种连接的物理设备,如各种网卡,串口卡等;链路层主要指的是提供对物理层进行访问的各种接口卡的驱动程序,如网卡驱动等;网路层的作用是负责将网络数据包传输到正确的位置,最重要的网络层协议当然就是IP协议了,其实网络层还有其他的协议如ICMP,ARP,RARP等,只不过不像IP那样被多数人所熟悉;传输层的作用主要是提供端到端,说白一点就是提供应用程序之间的通信,传输层最著名的协议非TCP与UDP协议末属了;应用层,顾名思义,当然就是由应用程序提供的,用来对传输数据进行语义解释的“人机界面”层了,比如HTTP,SMTP,FTP等等,其实应用层还不是人们最终所看到的那一层,最上面的一层应该是“解释层”,负责将数据以各种不同的表项形式最终呈献到人们眼前。
Linux网络核心架构Linux的网络架构从上往下可以分为三层,分别是:
用户空间的应用层。
内核空间的网络协议栈层。
物理硬件层。
其中最重要最核心的当然是内核空间的协议栈层了。
Linux网络协议栈结构Linux的整个网络协议栈都构建与Linux Kernel中,整个栈也是严格按照分层的思想来设计的,整个栈共分为五层,分别是 :
1,系统调用接口层,实质是一个面向用户空间应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。
2,协议无关的接口层,就是SOCKET层,这一层的目的是屏蔽底层的不同协议(更准确的来说主要是TCP与UDP,当然还包括RAW IP, SCTP等),以便与系统调用层之间的接口可以简单,统一。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个SOCKET,这个SOCKET其实是一个巨大的sock结构,它和下面一层的网络协议层联系起来,屏蔽了不同的网络协议的不同,只吧数据部分呈献给应用层(通过系统调用接口来呈献)。
3,网络协议实现层,毫无疑问,这是整个协议栈的核心。这一层主要实现各种网络协议,最主要的当然是IP,ICMP,ARP,RARP,TCP,UDP等。这一层包含了很多设计的技巧与算法,相当的不错。
4,与具体设备无关的驱动接口层,这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如open,close,init等,这一层可以屏蔽底层不同的驱动程序。
5,驱动程序层,这一层的目的就很简单了,就是建立与硬件的接口层。
可以看到,Linux网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰。
其中的两个“无关”层的设计非常棒,通过这两个“无关”层,其协议栈可以非常轻松的进行扩展。在我们自己的软件设计中,可以吸收这种设计方法。
4 本次调研采取的代码简介
本文采用的测试代码是一个非常简单的基于socket的客户端服务器程序,打开服务端并运行,再开一终端运行客户端,两者建立连接并可以发送hello\hi的信息,server端代码如下:
#include <stdio.h> /* perror */ #include <stdlib.h> /* exit */ #include <sys/types.h> /* WNOHANG */ #include <sys/wait.h> /* waitpid */ #include <string.h> /* memset */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h> #include <errno.h> #include <arpa/inet.h> #include <netdb.h> /* gethostbyname */ #define true 1 #define false 0 #define MYPORT 3490 /* 监听的端口 */ #define BACKLOG 10 /* listen的请求接收队列长度 */ #define BUF_SIZE 1024 int main() { int sockfd; if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } struct sockaddr_in sa; /* 自身的地址信息 */ 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); } struct sockaddr_in their_addr; /* 连接对方的地址信息 */ unsigned int sin_size = 0; char buf[BUF_SIZE]; int ret_size = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&their_addr, &sin_size); if(ret_size == -1) { perror("recvfrom"); exit(1); } buf[ret_size] = '\0'; printf("recvfrom:%s", buf); }
client端代码如下:
#include <stdio.h> /* perror */ #include <stdlib.h> /* exit */ #include <sys/types.h> /* WNOHANG */ #include <sys/wait.h> /* waitpid */ #include <string.h> /* memset */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h> #include <errno.h> #include <arpa/inet.h> #include <netdb.h> /* gethostbyname */ #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; /* 对方地址信息 */ 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_DGRAM, 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 ((numbytes = sendto(sockfd, "Hello, world!\n", 14, 0, (struct sockaddr *)&server_addr, sizeof(server_addr))) == -1) { perror("sendto"); exit(1); } close(sockfd); return true; }
简单来说,主要流程如下图所示:
5 应用层流程
5.1 发送端
下面我们具体结合Linux内核源码进行一步步仔细分析:
根据上述分析可知,发送端首先创建socket,创建之后会通过send发送数据。具体到源码级别,会通过send,sendto,sendmsg这些系统调用来发送数据,而上述三个函数底层都调用了sock_sendmsg。见下图:
我们再跳转到__sys_sendto看看这个函数干了什么:
我们可以发现,它创建了两个结构体,分别是:struct msghdr msg和struct iovec iov,这两个结构体根据命名我们可以大致猜出是发送数据和io操作的一些信息,如下图:
我们再来看看__sys_sendto调用的sock_sendmsg函数执行了什么内容:
发现调用了sock_sendmsg_nosec函数:
发现调用了inet_sendmsg函数:
至此,发送端调用完毕。我们可以通过gdb进行调试验证:
刚好符合我们的分析。
5.2 接收端
我们结合源码进行仔细分析:
接收端调用的是__sys_recvfrom函数:
__sys_recvfrom函数具体如下:
发现它调用了sock_recvmsg函数:
发现它调用了sock_recvmsg_nosec函数:
发现它调用了inet_recvmsg函数:
最后调用的是tcp_recvmsg这个系统调用。至此接收端调用分析完毕。
下面用gdb打断点进行验证:
验证结果刚好符合我们的调研。
6 传输层流程
6.1 发送端
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。TCP 协议栈的大致处理过程如下图所示:
TCP 栈简要过程:
UDP 栈简要过程:
下面我们结合代码依次分析:
根据我们对应用层的追查可以发现,传输层也是先调用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,我们看下sock_sendmsg_nosec这个函数:
在应用层调用的是inet_sendmsg函数,在传输层根据后面的断点可以知道,调用的是sock->ops-sendmsg这个函数。而sendmsg为一个宏,调用的是tcp_sendmsg,如下;
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect