Linux内核源代码:tcp/ip协议栈的调用
1 Linux概述1.1 Linux操作系统架构简介Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统
1 Linux概述
1.1 Linux操作系统架构简介
Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统。
内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。我们所调研的工作,就是在Linux内核层面进行分析。
1.2 协议栈简介
OSI(Open System Interconnect),即开放式系统互联。一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。
ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。
OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。如下图。
每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。
osi七层模型已经成为了理论上的标准,但真正运用于实践中的是TCP/IP五层模型。
TCP/IP五层协议和osi的七层协议对应关系如下:
在每一层实现的协议也各不同,即每一层的服务也不同.下图列出了每层主要的协议。
1.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网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰。
其中的两个“无关”层的设计非常棒,通过这两个“无关”层,其协议栈可以非常轻松的进行扩展。在我们自己的软件设计中,可以吸收这种设计方法。
2 代码简介
本文采用的测试代码是一个非常简单的基于socket的客户端服务器程序,打开服务端并运行,再开一终端运行客户端,两者建立连接并可以发送hellohi的信息,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] = '';
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");
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!", 14, 0,
(struct sockaddr *)&server_addr,
sizeof(server_addr))) == -1)
{
perror("sendto");
exit(1);
}
close(sockfd);
return true;
}
简单来说,主要流程如下图所示:
3 应用层流程
3.1 发送端
网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message_sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
对于 TCP ,调用 tcp_sendmsg 函数。对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。
下面我们具体结合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进行调试验证:
刚好符合我们的分析。
3.2 接收端
每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
对 TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
对 UDP 来说,从 user space 中可以调用三个 system call recv()/recvfrom()/recvmsg() 中的任意一个来接收 UDP package,这些系统调用最终都会调用内核中的 udp_recvmsg 方法。