转载:http://blog.csdn.net/qq405180763/article/details/8797236
实际上skb_buf结构只是一块已经申请好的套接字缓冲区的指针和属性数据的描述集合,netdev_alloc_skb函数申请到一块套接字缓冲区后,返回记录这块缓冲区信息的skb_buf结构,在各个网络层传输的只是skb_buf结构,换句话说,仅仅是该套接字缓冲区的指针而已,各个网络层根据传来的指针,对指针进行操作,往已经申请好的固定套接字缓冲区里读写数据。
sk_buff结构的成员skb->head指向一个已分配的空间的头部,即申请到的整个缓冲区的头,skb->end指向该空间的尾部,这两个成员指针从空间创建之后,就不能被修改。skb->data指向分配空间中数据的头部,skb->tail指向数据的尾部,这两个值随着网络数据在各层之间的传递、修改,会被不断改动。刚开始接触skb_buf的时候会产生一种错误的认识,就是以为协议头都会是放在skb->head和skb->data这两个指针之间,但实际上skb_buf的操作函数都无法直接对这一段内存进行操作,所有的操作函数所做的就仅仅是修改skb->data和skb->tail这两个指针而已,向套接字缓冲区拷贝数据也是由其它函数来完成的,所以不管是从网卡接受的数据还是上层发下来的数据,协议头都是被放在了skb->data到skb->tail之间,通过skb_push前移skb->data加入协议头,通过skb_pull后移skb->data剥离协议头。四个指针间存在如下关系:
sk_buff结构被不同的网络层(MAC或者其他二层链路协议,三层的IP,四层的TCP或UDP等)使用,并且其中的成员变量(一般是数据指针)在结构从一层向另一层传递时改变。L4向L3传递前会添加一个L4的头部,同样,L3向L2传递前,会添加一个L3的头部。添加头部比在不同层之间拷贝数据的效率更高。由于在缓冲区的头部添加数据意味着要修改指向缓冲区的指针,这是个复杂的操作,所以内核提供了一个函数skb_reserve将数据部分往后移。协议栈中的每一层在往下一层传递缓冲区前,第一件事就是调用skb_reserve在缓冲区的头部给协议头预留一定的空间,再用skb_push函数将协议头查到skb->data指针之前。
skb_reserve同样被设备驱动使用来对齐接收到包的包头。如果缓冲区向上层协议传递,旧的协议层的头部信息就没什么用了。例如,L2的头部只有在网络驱动处理L2的协议时有用,L3是不会关心它的信息的。但是,内核并没有把L2的头部从缓冲区中删除,而是用sk_buff结构中对应的头指针指向这个头,再把有效荷载的指针指向L3的头部,这样做,可以节省CPU时间。当接收一个包时,处理n层协议头的函数从n-1层收到一个缓冲区,因为在n-1层处理的最后,skb->data是指向n-1层的数据开始指针,所以到了n层后skb->data就是指向n层协议的头。处理n层协议的函数把本层的指针(例如,L3对应的是skb- >nh指针)初始化为skb->data,取出本层的协议头指针,在处理n层协议的函数结束时,在把包传递给n+1层的处理函数前,它会把skb->data指针指向n层协议头的末尾,这正好是n+1层协议的协议头(参见下图)。
skb_buf在各个网络层传输过程。
在网络数据发送过程中,由netdev_alloc_skb申请得到套接字缓冲区后,skb_reserve将整个数据块往后移动MAX_TCP_HEADER个字节,预留出skb中协议头的最大长度,确保在套接字从上层不断往下层传递的过程中,有足够的协议头空间。在数据每传到一个网络层的时候,通过skb_push函数前移skb->data将协议头插入数据区,一直到链路层将所有层的协议头加入到套接字中,skb_push函数就是用来前移skb->data,在数据头部插入协议头的。最后由驱动程序把数据(skb->data到skb->tail之间部分)发给网卡,释放掉套接字缓存,网卡负责把数据发到网络上。发送中加协议头的方式如下图:
在网络数据接收过程中,网卡收到数据触发中断,驱动程序响应中断接收网卡缓存中数据,因为网卡中以太网帧头的长度为14个字节,而紧接着的IP协议头需要在16字节对齐的边界上,所以在申请到套接字缓存后,用skb_reserve函数先预留出2字节的空间,确保IP协议头的16字节对齐。刚申请到的套接字缓存,其sbk_buf的skb->data和skb->tail指向同一地址,是没有数据的,用skb_put函数将skb->tail往后拉数据包长度个字节空间,通过返回的原skb->tail地址,把网络数据从网卡中拷贝到套接字缓存里,skb_put函数就是在skb->tail和skb->end之间加数据。接着将数据往上层发送,在每一层中用skb_pull函数或者移动skb->data指针的方式,将各个层对应的协议头从数据中剥离开来,一直到最上面的应用层,剩下真正的从其他机器发过来的有用数据。