TCP/IP 协议栈在 Linux 内核中的 运行时序分析
------------恢复内容开始------------
TCP/IP协议栈在Linux内核中的运行时序分析
姓名:陈庆港
学号:sa20225120
调研要求
1.在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
2.编译、部署、运行、测评、原理、源代码分析、跟踪调试等。
3.应该包括时序图。
1.Linux概述
1.1 Linux操作系统架构简介
Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统。
内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。

1.2 协议栈简介
计算机网络分层模型有OSI七层模型和TCP/IP分层模型,
OSI从逻辑上,把一个网络系统分为功能上相对独立的7个有序的子系统,这样OSI体系结构就由功能上相对独立的7个层次组成,如图所示。它们由低到高分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

Linux内核采用的TCP/IP分层模型来设计网络结构,其结构如图所示

1.3 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,驱动程序层,建立与硬件的接口层。

2.测试代码
我们测试代码是基于socket的采用TCP协议通信的客户端服务器程序,两者建立连接可以发送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; }
TCP Socket处理流程如下图

3.流程分析
3.1 socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在 Linux 系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读 取,使得对网络的控制和对文件的控制一样方便。
Socket结构如下:
// socket_state: socket 状态 // type: socket type // flags: socket flags // 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; };
3.2 socket的创建
网络应用调用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。
函数调用图如下:

涉及到的数据结构如下图:

每个数据结构的意义:
1) socket, sock, inet_sock, tcp_sock的关系
创建完sk变量后,回到inet_create函数中:
这里是根据sk变量得到inet_sock变量的地址;这里注意区分各个不同结构体。
a. struct socket:这个是基本的BSD socket,面向用户空间,应用程序通过系统调用开始创建的socket都是该结构体,它是基于虚拟文件系统创建出来的;
类型主要有三种,即流式、数据报、原始套接字协议;
b. struct sock:它是网络层的socket;对应有TCP、UDP、RAW三种,面向内核驱动;
其状态相比socket结构更精细:
c. struct inet_sock:它是INET域的socket表示,是对struct sock的一个扩展,提供INET域的一些属性,如TTL,组播列表,IP地址,端口等;
d. struct raw_socket:它是RAW协议的一个socket表示,是对struct inet_sock的扩展,它要处理与ICMP相关的内容;
e. sturct udp_sock:它是UDP协议的socket表示,是对struct inet_sock的扩展;
f. struct inet_connection_sock:它是所有面向连接的socket表示,是对struct inet_sock的扩展;
g. struct tcp_sock:它是TCP协议的socket表示,是对struct inet_connection_sock的扩展,主要增加滑动窗口,拥塞控制一些TCP专用属性;
h. struct inet_timewait_sock:它是网络层用于超时控制的socket表示;
i. struct tcp_timewait_sock:它是TCP协议用于超时控制的socket表示;
3.4 连接的建立
在服务器和客户端进行通信之前首先要通过三次握手建立TCP连接,下图是tcp的状态转换

因为socket 函数并没有为套接字绑定本地地址和端口号,对于服务器端则必须显性绑定地址和端口号。通过在应用层调用bind函数把一个本地协议地址赋予套接字。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); /*sockfd是由socket函数返回的套接口描述字,第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度*/
然后在应用层通过入口函数 sys_socket调用BSD Socket 层的ock_bind 函数,源码如下
/* * Bind a name to a socket. Nothing much to do here since it's * the protocol's responsibility to handle the local address. * * We move the socket address to kernel space before we call * the protocol layer (having also checked the address is ok). */ //bind函数对应的BSD层函数,用于绑定一个本地地址,服务器端 //umyaddr表示需要绑定的地址结构,addrlen表示改地址结构的长度 //这里的fd,即为套接字描述符 static int sock_bind(int fd, struct sockaddr *umyaddr, int addrlen) { struct socket *sock; int i; char address[MAX_SOCK_ADDR]; int err; //套接字参数有效性检查 if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) return(-EBADF); //获取fd对应的socket结构 if (!(sock = sockfd_lookup(fd, NULL))) return(-ENOTSOCK); //将地址从用户缓冲区复制到内核缓冲区,umyaddr->address if((err=move_addr_to_kernel(umyaddr,addrlen,address))<0) return err; //转调用bind指向的函数,下层函数(inet_bind) if ((i = sock->ops->bind(sock, (struct sockaddr *)address, addrlen)) < 0) { return(i); } return(0); }
sock_bind 函数主要就是将用户缓冲区的地址结构复制到内核缓冲区,然后转调用下一层的bind函数。INET Socket 层inet_bind 函数,源码如下
/* this needs to be changed to disallow the rebinding of sockets. What error should it return? */ //完成本地地址绑定,本地地址绑定包括IP地址和端口号两个部分 static int inet_bind(struct socket *sock, struct sockaddr *uaddr,int addr_len) { struct sockaddr_in *addr=(struct sockaddr_in *)uaddr; struct sock *sk=(struct sock *)sock->data, *sk2; unsigned short snum = 0 /* Stoopid compiler.. this IS ok */; int chk_addr_ret; /* check this error. */ //在进行地址绑定时,该套接字应该处于关闭状态 if (sk->state != TCP_CLOSE) return(-EIO); //地址长度字段校验 if(addr_len<sizeof(struct sockaddr_in)) return -EINVAL; //非原始套接字类型,绑定前,没有端口号,则绑定端口号 if(sock->type != SOCK_RAW) { if (sk->num != 0)//从inet_create函数可以看出,非原始套接字类型,端口号是初始化为0的 return(-EINVAL); snum = ntohs(addr->sin_port);//将地址结构中的端口号转为主机字节顺序 /* * We can't just leave the socket bound wherever it is, it might * be bound to a privileged port. However, since there seems to * be a bug here, we will leave it if the port is not privileged. */ //如果端口号为0,则自动分配一个 if (snum == 0) { snum = get_new_socknum(sk->prot, 0);//得到一个新的端口号 } //端口号有效性检验,1024以上,超级用户权限 if (snum < PROT_SOCK && !suser()) return(-EACCES); } //下面则进行ip地址绑定 //检查地址是否是一个本地接口地址 chk_addr_ret = ip_chk_addr(addr->sin_addr.s_addr); //如果指定的地址不是本地地址,并且也不是一个多播地址,则错误返回 if (addr->sin_addr.s_addr != 0 && chk_addr_ret != IS_MYADDR && chk_addr_ret != IS_MULTICAST) return(-EADDRNOTAVAIL); /* Source address MUST be ours! */ //如果没有指定地址,则系统自动分配一个本地地址 if (chk_addr_ret || addr->sin_addr.s_addr == 0) sk->saddr = addr->sin_addr.s_addr;//本地地址绑定 if(sock->type != SOCK_RAW) { /* Make sure we are allowed to bind here. */ cli(); //for循环主要是检查检查有无冲突的端口号以及本地地址,有冲突,但不允许地址复用,肯定错误退出 //成功跳出for循环时,已经定位到了哈希表sock_array指定索引的链表的末端 for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)]; sk2 != NULL; sk2 = sk2->next) { /* should be below! */ if (sk2->num != snum) //没有重复,继续搜索下一个 continue;//除非有重复,否则后面的代码将不会被执行 if (!sk->reuse)//端口号重复,如果没有设置地址复用标志,退出 { sti(); return(-EADDRINUSE); } if (sk2->num != snum) continue; /* more than one */ if (sk2->saddr != sk->saddr) //地址和端口一个意思 continue; /* socket per slot ! -FB */ //如果状态是LISTEN表明该套接字是一个服务端,服务端不可使用地址复用选项 if (!sk2->reuse || sk2->state==TCP_LISTEN) { sti(); return(-EADDRINUSE); } } sti(); remove_sock(sk);//将sk sock结构从其之前的表中删除,inet_create中 put_sock,这里remove_sock put_sock(snum, sk);//然后根据新分配的端口号插入到新的表中。可以得知系统在维护许多这样的表 sk->dummy_th.source = ntohs(sk->num);//tcp首部,源端口号绑定 sk->daddr = 0;//sock结构所代表套接字的远端地址 sk->dummy_th.dest = 0;//tcp首部,目的端口号 } return(0); }
inet_bind 函数即为bind函数的最底层实现,该函数实现了本地地址和端口号的绑定,其中还针对上层传过来的地址结构进行校验,检查是否冲突可用。需要清楚的是 sock_array数组,这其实是一个链式哈希表,里面保存的就是各个端口号的sock结构,数组大小小于端口号,所以采用链式哈希表存储。bind 函数的各层分工很明显,主要就是inet_bind函数了,在注释里说的很明确了,bind 是绑定本地地址,它不负责对端地址,一般用于服务器端,客户端是系统指定的。
然后调用listen函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求。
应用层listen 函数源码如下
#include <sys/socket.h> int listen(int sockfd, int backlog); /*sockfd是bind之后的套接口描述字,第二个参数规定了内核应该为相应套接口排队的最大连接个数*/
向下调用BSD Socket 层sock_listen 函数,源码如下
/* * Perform a listen. Basically, we allow the protocol to do anything * necessary for a listen, and if that works, we mark the socket as * ready for listening. */ //服务器端监听客户端的连接请求 //fd表示bind后的套接字文件描述符,backlog表示排队的最大连接个数 //listen函数把一个未连接的套接字转换为一个被动套接字, //指示内核应接受该套接字的连接请求 static int sock_listen(int fd, int backlog) { struct socket *sock; if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) return(-EBADF); //给定文件描述符返回socket结构以及file结构指针,这里file参数为NULL,则表明对这个不感兴趣 if (!(sock = sockfd_lookup(fd, NULL))) return(-ENOTSOCK); //前提是没有建立连接,该套接字已经是连接状态了,自然不需要 if (sock->state != SS_UNCONNECTED) { return(-EINVAL); } //调用底层实现函数(inet_listen) if (sock->ops && sock->ops->listen) sock->ops->listen(sock, backlog); sock->flags |= SO_ACCEPTCON;//设置标识字段,表示正在监听 return(0); }
调用底层实现函数INET Socket 层inet_listen 函数
/* * Move a socket into listening state. */ //sock_listen的下层调用函数 //这个函数主要是对sock结构中state字段的设置。listen函数到这层完成处理 static int inet_listen(struct socket *sock, int backlog) { //获取sock数据结构 struct sock *sk = (struct sock *) sock->data; //如果sock的端口号为0(未绑定任何端口号),则自动绑定一个本地端口号(新的未使用的最小的端口号) //如果事先已经绑定了一个端口号,那么这个代码将不会执行 if(inet_autobind(sk)!=0) return -EAGAIN; /* We might as well re use these. */ /* * note that the backlog is "unsigned char", so truncate it * somewhere. We might as well truncate it to what everybody * else does.. */ //等待的最大数.内核限制最大连接数是5 if ((unsigned) backlog > 5) backlog = 5; sk->max_ack_backlog = backlog;//缓存的最大未应答数据包个数 if (sk->state != TCP_LISTEN)//如果不是listen状态,则置位listen状态 { sk->ack_backlog = 0;//缓存的未应答数据包个数清0 sk->state = TCP_LISTEN; } return(0); }
listen 函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,其内部实现归根到底就是设置 sock 结构的状态,设置其为 TCP_LISTEN。之后TCP客户可以用 connect 函数来建立与 TCP 服务器的连接,其实是客户利用 connect 函数向服务器端发出连接请求。下面分析一下connect函数。
在应用层调用connect 函数 int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
向下在BSD Socket 层调用sock_connect 函数,再向下在NET Socket 层调用inet_connect 函数,客户端套接字的端口号是在这个函数中绑定的。源码如下
/* * Connect to a remote host. There is regrettably still a little * TCP 'magic' in here. */ //完成套接字的连接请求操作,这是客户端主动向服务器端发送请求 //sock是客户端套接字,后面的uaddr,addr_len则是对端服务器端的地址信息 static int inet_connect(struct socket *sock, struct sockaddr * uaddr, int addr_len, int flags) { struct sock *sk=(struct sock *)sock->data; int err; sock->conn = NULL; //正在与远端取得连接,且tcp对应的状态 if (sock->state == SS_CONNECTING && tcp_connected(sk->state)) { sock->state = SS_CONNECTED;//直接设置字段为已经连接 /* Connection completing after a connect/EINPROGRESS/select/connect */ return 0; /* Rock and roll */ } //正在取得连接,且是tcp协议,非阻塞 if (sock->state == SS_CONNECTING && sk->protocol == IPPROTO_TCP && (flags & O_NONBLOCK)) { if (sk->err != 0) { err=sk->err; sk->err=0; return -err; } //返回正在进行状态 return -EALREADY; /* Connecting is currently in progress */ } //不是处于正在连接处理状态(现在进行时态) if (sock->state != SS_CONNECTING) { /* We may need to bind the socket. */ //自动绑定一个端口号,客户端自动绑定端口号是在connect函数中实现的 if(inet_autobind(sk)!=0) return(-EAGAIN); if (sk->prot->connect == NULL) //不支持该项操作,没有指定操作函数 return(-EOPNOTSUPP); //转调用connect函数(传输层 tcp_connect函数) err = sk->prot->connect(sk, (struct sockaddr_in *)uaddr, addr_len); if (err < 0) return(err); sock->state = SS_CONNECTING;//设置状态字段,表示正在连接过程中 } //这个状态下,这是关闭信号。各个状态描述,参考下面链接 http://blog.csdn.net/wenqian1991/article/details/40110703 //清楚,这里有两个state,一个是socket的state(套接字所处的连接状态),一个是sock的state(涉及到协议,比如tcp的状态) //上面调用下层connect函数,会更新sk->state,如果出现>TCP_FIN_WAIT2,表明连接过程出现了异常 if (sk->state > TCP_FIN_WAIT2 && sock->state==SS_CONNECTING) { sock->state=SS_UNCONNECTED;//连接未建立 cli(); err=sk->err; sk->err=0; sti(); return -err; } //没有建立,就是在正在建立的路上 if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK)) return(-EINPROGRESS);//过程正在处理 cli(); /* avoid the race condition */ //这里的while实则是等待下层函数(前面的connect调用)的返回 //正常退出while循环,表示连接成功 while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) { interruptible_sleep_on(sk->sleep);//添加到sk中的等待队列中,直到资源可用被唤醒 if (current->signal & ~current->blocked) { sti(); return(-ERESTARTSYS); } /* This fixes a nasty in the tcp/ip code. There is a hideous hassle with icmp error packets wanting to close a tcp or udp socket. */ if(sk->err && sk->protocol == IPPROTO_TCP) { sti(); sock->state = SS_UNCONNECTED; err = -sk->err; sk->err=0; return err; /* set by tcp_err() */ } } sti(); sock->state = SS_CONNECTED;//成功建立连接 if (sk->state != TCP_ESTABLISHED && sk->err) //出错处理 { sock->state = SS_UNCONNECTED; err=sk->err; sk->err=0; return(-err); } return(0); }
实质操作落到了下一层传输层tcp_connect 函数客户端通过这个函数获得对端的地址信息(ip地址和端口号),另外本地ip地址也是在这个函数中指定的。三次握手阶段起于 connect 函数,自然地,在该函数指定目的地址,以及设置标志字段,定时器以后,就需要向服务器端发送连接请求数据包,对应操作在该函数最后。源码如下
/* * This will initiate an outgoing connection. */ //同accept; connect->sock_connect->inet_connect->tcp_connect //connect就是客户端向服务器端发出连接请求 //参数:sk:客户端套接字;usin和addrlen分别是一个指向服务器端套接口地址结构的指针和该结构的大小 static int tcp_connect(struct sock *sk, struct sockaddr_in *usin, int addr_len) { struct sk_buff *buff; struct device *dev=NULL; unsigned char *ptr; int tmp; int atype; struct tcphdr *t1;//tcp首部 struct rtable *rt;//ip路由表 if (sk->state != TCP_CLOSE) //不是关闭状态就是表示已经建立连接了 { return(-EISCONN);//连接已建立 } //地址结构大小检查 if (addr_len < 8) return(-EINVAL); //地址簇检查,INET域 if (usin->sin_family && usin->sin_family != AF_INET) return(-EAFNOSUPPORT); /* * connect() to INADDR_ANY means loopback (BSD'ism). */ if(usin->sin_addr.s_addr==INADDR_ANY)//指定一个通配地址 usin->sin_addr.s_addr=ip_my_addr();//本地ip地址(dev_base设备) /* * Don't want a TCP connection going to a broadcast address */ //检查ip传播地址方式。广播、多播均不可行 if ((atype=ip_chk_addr(usin->sin_addr.s_addr)) == IS_BROADCAST || atype==IS_MULTICAST) return -ENETUNREACH; //sk已经具备本地地址信息,这里在赋值目的地址信息,这样sock套接字就具备了本地与对端两者的地址信息 //知道住哪了,就知道怎么去了 sk->inuse = 1;//加锁 sk->daddr = usin->sin_addr.s_addr;//远端地址,即要请求连接的对端服务器地址 sk->write_seq = tcp_init_seq();//初始化一个序列号,跟当前时间挂钩的序列号 sk->window_seq = sk->write_seq;//窗口大小,用write_seq初始化 sk->rcv_ack_seq = sk->write_seq -1;//目前本地接收到的对本地发送数据的应答序列号,表示此序号之前的数据已接收 sk->err = 0;//错误标志清除 sk->dummy_th.dest = usin->sin_port;//端口号赋值给tcp首部目的地址 release_sock(sk);//重新接收暂存的数据包 buff = sk->prot->wmalloc(sk,MAX_SYN_SIZE,0, GFP_KERNEL);//分配一个网络数据包结构 if (buff == NULL) { return(-ENOMEM); } sk->inuse = 1; buff->len = 24;//指定数据部分长度(头+数据) buff->sk = sk;//绑定套接字 buff->free = 0;//发送完数据包后,不立即清除,先缓存起来 buff->localroute = sk->localroute;//路由类型 //buff->data 是指向数据部分的首地址(包括首部),这里是传输层,对应的数据部分为 // TCP Hearder | data;buff->data则是指向其首地址 t1 = (struct tcphdr *) buff->data;//tcp首部数据 //buff->data中保存的是数据包的首部地址,在各个层对应不同的首部 /* * Put in the IP header and routing stuff. */ //查找合适的路由表项 rt=ip_rt_route(sk->daddr, NULL, NULL); /* * We need to build the routing stuff from the things saved in skb. */ //这里是调用ip_build_header(ip.c),结合前面可以看出prot操作函数调用的一般都是下一层的函数 //build mac header 然后 build ip header,该函数返回时,buff的data部分已经添加了ip 首部和以太网首部 //返回这两个首部大小之和 tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev, IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); if (tmp < 0) { sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); release_sock(sk); return(-ENETUNREACH); } //connect 函数是向指定地址的网络端发送连接请求数据包,最终数据包要被对端的硬件设备接收 //所以需要对端的ip地址 mac地址。 buff->len += tmp;//数据帧长度更新,即加上创建的这两个首部长度 t1 = (struct tcphdr *)((char *)t1 +tmp);//得到tcp首部 //t1指针结构中对应的内存布局为:mac首部+ip首部+tcp首部+数据部分 //t1是该结构的首地址,然后偏移mac首部和ip首部大小位置,定位到tcp首部 memcpy(t1,(void *)&(sk->dummy_th), sizeof(*t1));//拷贝缓存的tcp首部 t1->seq = ntohl(sk->write_seq++);//32位序列号,序列号字节序转换 //下面为tcp保证可靠数据传输使用的序列号 sk->sent_seq = sk->write_seq;//将要发送的数据包的第一个字节的序列号 buff->h.seq = sk->write_seq;//该数据包的ack值,针对tcp协议而言 //tcp首部控制字设置 t1->ack = 0; t1->window = 2;//窗口大小 t1->res1=0;//首部长度 t1->res2=0; t1->rst = 0; t1->urg = 0; t1->psh = 0; t1->syn = 1;//同步控制位 t1->urg_ptr = 0; t1->doff = 6; /* use 512 or whatever user asked for */ //窗口大小,最大传输单元设置 if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) sk->window_clamp=rt->rt_window;//窗口大小钳制值 else sk->window_clamp=0; if (sk->user_mss) sk->mtu = sk->user_mss;//mtu最大传输单元 else if(rt!=NULL && (rt->rt_flags&RTF_MTU)) sk->mtu = rt->rt_mss; else { #ifdef CONFIG_INET_SNARL if ((sk->saddr ^ sk->daddr) & default_mask(sk->saddr)) #else if ((sk->saddr ^ sk->daddr) & dev->pa_mask) #endif sk->mtu = 576 - HEADER_SIZE; else sk->mtu = MAX_WINDOW; } /* * but not bigger than device MTU */ if(sk->mtu <32) sk->mtu = 32; /* Sanity limit */ sk->mtu = min(sk->mtu, dev->mtu - HEADER_SIZE);//mtu取允许值 /* * Put in the TCP options to say MTU. */ //这里不是很清楚 ptr = (unsigned char *)(t1+1); ptr[0] = 2; ptr[1] = 4; ptr[2] = (sk->mtu) >> 8; ptr[3] = (sk->mtu) & 0xff; //计算tcp校验和 tcp_send_check(t1, sk->saddr, sk->daddr,sizeof(struct tcphdr) + 4, sk); /* * This must go first otherwise a really quick response will get reset. */ //connect发起连接请求时,开始tcp的三次握手,这是第一个状态 tcp_set_state(sk,TCP_SYN_SENT);//设置tcp状态 sk->rto = TCP_TIMEOUT_INIT;//延迟时间值 #if 0 /* we already did this */ init_timer(&sk->retransmit_timer); #endif //重发定时器设置 sk->retransmit_timer.function=&retransmit_timer; sk->retransmit_timer.data = (unsigned long)sk; reset_xmit_timer(sk, TIME_WRITE, sk->rto); /* Timer for repeating the SYN until an answer */ sk->retransmits = TCP_SYN_RETRIES; //前面地址信息,标识字段,查询路由表项等事务都已经完成了,那么就是发送连接请求数据包的时候了 //下面这个函数将转调用ip_queue_xmit 函数(ip层),这是个数据包发送函数 sk->prot->queue_xmit(sk, dev, buff, 0); reset_xmit_timer(sk, TIME_WRITE, sk->rto); tcp_statistics.TcpActiveOpens++; tcp_statistics.TcpOutSegs++; //那么下面就是一个数据包接收函数了,(可能有的名字已经占用了,就勉强用这个不相关的名字) //这个函数将内部调用 tcp_rcv 函数 release_sock(sk);//重新接收数据包 return(0); }
上面函数最后调用了queue_xmit 函数(ip_queue_xmit 函数)和 release_sock 函数,进行数据包的发送和接收。另外,在inet_connect 函数中调用了 build_header 函数(ip层的 ip_build_header 函数)。
客户端通过connect系统调用给服务器发送一个同步报文段,使连接客户端状态转移到SYN_SENT状态,服务器端在收到客户端发来的SYN报文后状态转移到SYN_RCVD状态。数据报文段的发送和接收在 ip_queue_xmit 函数和 release_sock 函数中实现。客户端在收到确认报文段后状态转为ESTABLISHED,服务器端在收到来自客户端的报文段后也转为ESTABLISHED。连接建立
最后服务器端在应用层调用accept 函数该函数返回一个已建立连接的可用于数据通信的套接字。应用层向下调用BSD Socket 层sock_accept 函数,再向下调用INET Socket 层inet_accept 函数,我们看下net_accept 函数源码,
/* * Accept a pending connection. The TCP layer now gives BSD semantics. *///sock为监听套接字,newsock为连接成功后实际用于通信的sock static int inet_accept(struct socket *sock, struct socket *newsock, int flags) { struct sock *sk1, *sk2; int err; sk1 = (struct sock *) sock->data; /* * We've been passed an extra socket. * We need to free it up because the tcp module creates * its own when it accepts one. */ //如果sock->data 已经指向了对应的sock结构,则把它销毁 //销毁旧的,后面指向新的accept后的 if (newsock->data) { struct sock *sk=(struct sock *)newsock->data; newsock->data=NULL; sk->dead = 1; destroy_sock(sk);//销毁旧的socket对应的sock结构 } if (sk1->prot->accept == NULL) //没有对应的操作函数集,退出 return(-EOPNOTSUPP); /* Restore the state if we have been interrupted, and then returned. */ //如果套接字在等待连接的过程中被中断,则监听套接字与中断的套接字关联,下次优先处理该套接字 if (sk1->pair != NULL ) { sk2 = sk1->pair; sk1->pair = NULL; } else { //这里调用下层处理函数tcp_accept,首次调用inet_accept,sk1->pair 肯定是为NULL的,所以一开始就会执行下面的代码 sk2 = sk1->prot->accept(sk1,flags);//交给下层处理函数 if (sk2 == NULL) { if (sk1->err <= 0) printk("Warning sock.c:sk1->err <= 0. Returning non-error.\n"); err=sk1->err; sk1->err=0; return(-err); } } //socket sock建立关联 newsock->data = (void *)sk2;//指向新的,sk2为下层函数tcp_accept返回的套接字 sk2->sleep = newsock->wait;//等待队列 sk2->socket = newsock;//回绑,指向上层的socket结构 newsock->conn = NULL;//还没有连接客户端 if (flags & O_NONBLOCK) return(0); cli(); /* avoid the race. */ //三次握手中间过程,tcp SYN序列号接收 while(sk2->state == TCP_SYN_RECV) { //被中断了 interruptible_sleep_on(sk2->sleep); if (current->signal & ~current->blocked) { sti(); sk1->pair = sk2;//存入pair,下次优先处理 sk2->sleep = NULL; sk2->socket=NULL; newsock->data = NULL; return(-ERESTARTSYS); } } sti(); //连接失败,三次握手失败 if (sk2->state != TCP_ESTABLISHED && sk2->err > 0) { err = -sk2->err; sk2->err=0; sk2->dead=1; /* ANK */ destroy_sock(sk2);//销毁新建的sock结构 newsock->data = NULL; return(err); } newsock->state = SS_CONNECTED;//已经建立了连接 return(0); }
3.4应用层流程分析
3.4.1socket的发送
创建完socket后就可以调用send来发送数据,映射到内核中会通过send,sendto,sendmsg这些系统调用来发送数据,而上述三个函数底层都调用了sock_sendmsg。


这里创建了两个结构体,分别是:struct msghdr msg和struct iovec iov;结构体struct msghdr,用于接收来自应用层的数据包,下面是结构体struct msghdr的定义:
struct msghdr { void *msg_name; //存数据包的目的地址,网络包指向sockaddr_in //向内核发数据时,指向sockaddr_nl int msg_namelen; //地址长度 struct iovec *msg_iov; __kernel_size_t msg_iovlen; void *msg_control; __kernel_size_t msg_controllen; unsigned msg_flags; };
结构体struct msghdr的内容可以分为四组:
(1) 第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址。
msg_name是指向一个结构体struct sockaddr的指针。
(2) 第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际上,确切地说,应该是一个结构体struct iovec的数组。下面是该结构体的定义:
struct iovec { void __user *iov_base; __kernel_size_t iov_len; };
iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)
(3) 第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要送。暂时略过。
(4) 第四组是msg_flags.其值即为传入的参数flags。raw协议不支持MSG_OOB向标志,即带外数据。
__sys_sendto函数其实做了3件事:
1.通过fd获取了对应的struct socket
2.创建了用来描述要发送的数据的结构体struct msghdr。
3.调用了sock_sendmsg来执行实际的发送。
构建完这些后,调用sock_sendmsg继续执行发送流程,传入参数为struct msghdr和数据的长度。忽略中间的一些不重要的细节,sock_sendmsg继续调用sock_sendmsg(),sock_sendmsg()最后调用struct socket->ops->sendmsg,即对应套接字类型的sendmsg()函数,所有的套接字类型的sendmsg()函数都是 sock_sendmsg,该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。
源码如下所示:



利用gdb来进行调试验证:

3.4.2 应用层socket的接收
调用__sys_recvfrom函数源码如下,



sock->ops->recvmsg即inet_recvmsg,最后在inet_recvmsg中调用的是tcp_recvmsg

gdb调试验证:

3.5 传输层流程分析
3.5.1 传输层发送流程
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。
传输层发送数据调用函数过程:send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec。在应用层调用的是inet_sendmsg函数,在传输层调用的是sock->ops-sendmsg这个函数。而sendmsg为一个宏,调用的是tcp_sendmsg。而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()函数。
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); }
tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle)函数,源码如下
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); }
__tcp_push_pending_frames又调用了tcp_write_xmit函数
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; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; ......
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,源码如下
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); tcp_options_write((__be32 *)(th + 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out += tcp_skb_pcount(skb); tp->bytes_sent += skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out += tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); ...... }
tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层。
gdb调试如下

3.5.1 传输层接受流程
调用函数__sys_recvfrom,然后__sys_recvfrom调用了sock_recvmsg来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags)。
__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 = 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); ..... }
最后实际调用的是调用的其实是tcp_rcvmsg函数,源码如下
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);
在 tcp_rcvmsg函数里维护了三个队列prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter。
__skb_datagram_iter源码如下:
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; }
gdb调试验证如下:
Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0, addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>) at net/socket.c:1990 1990 { (gdb) c Continuing. Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28, flags=0) at net/socket.c:891 891 { (gdb) c Continuing. Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28, len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4) at net/ipv4/tcp.c:1933 1933 { (gdb) c Continuing. Breakpoint 4, __skb_datagram_iter (skb=0xffff8880068714e0, offset=0, to=0xffffc900001efe38, len=2, fault_short=false, cb=0xffffffff817ff860 <simple_copy_to_iter>, data=0x0 <fixed_percpu_data>) at net/core/datagram.c:414 414 {
3.6 IP层流程
3.6.1 发送端
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。
IP 栈基本处理过程如下所示:
1.首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
2.接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
3.中间的一些分片基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
4.接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取
入口函数是ip_queue_xmit:

接着调用__ip_queue_xmit函数:

发现调用了skb_rtable函数,实际上是开始找路由缓存

调用ip_local_out进行数据发送:

调用__ip_local_out函数,在__ip_local_out函数返回一个nf_hook函数,里面调用了dst_output,这个函数实质上是调用ip_finish__output函数:

如果分片就调用ip_fragment,否则就调用IP_finish_output2函数:

gdb验证如下:

3.6.2 接收端
IP 层的接收入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。

调用ip_rcv_finish函数:

调用dst_input函数,实际上是调用ip_local_deliver函数:

如果分片,就调用ip_defrag函数,没有则调用ip_local_deliver_finish函数:

最后调用ip_protocol_deliver_rcu函数:

gdb验证如下:

3.6 数据链路层流程
3.6.1 发送端
数据链路层在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。
发送端调用dev_queue_xmit,这个函数实际上调用__dev_queue_xmit:


调用了dev_hard_start_xmit函数:

调用xmit_one:

调用trace_net_dev_start_xmit,实际上调用__net_dev_start_xmit函数:

gdb调试如下:

3.6.2 接收端
一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
入口函数net_rx_action:

调用napi_poll,实质上调用napi_gro_receive函数,napi_gro_receive 会直接调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。
gdb验证如下:

3.7 物理层流程
3.7.1 发送端
物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
3.7.2 接收端
- 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
- 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
- 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
- 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
- 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
- netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。
时序图展示

------------恢复内容结束------------

浙公网安备 33010602011771号