走进Linux内核网络 高屋建瓴—stack layer model

前言

作为本系列的第一篇文章,让我们首先聊聊协议栈模型(stack layer model)。还记得在大学课堂里的时候学TCP/IP的时候,我其实并不能分清楚ISO/OSI分层模型和TCP/IP分层模型二者的区别. 就是下面这幅图
在这里插入图片描述
可以看出,虽然层次划分方式有区别,但大体上还是能对应上的。那么,为什么要这么分层?

为什么要分层

分层的好处在于,每一层可以各司其职,只关心自己这一层的功能,而不去关心上面下面需要做什么。每一层使用下面提供的接口,同时为上一层提供服务。每一层可能有多种实现,比如传输层(Transport)可以使用TCP,也可以使用UDP,它们都可以使用IP作为下层网络层(Network)的实现。他们就像乐高积木一样, 你可以将不同的积木拼接起来.

报文在层间穿梭

这样说或许还比较抽象,我们来理一理报文在协议栈中的穿梭流程

以一个标准的使用TCP/IPHTTP报文为例
在这里插入图片描述
它由多个Header和一个Data部分组成,这些Header就是协议栈中各层对在报文上留下的痕迹,越上层Header越靠近Data

发送方向

发送数据时,应用层在原始数据前面贴上HTTP Header,结果作为整体下发给传输层(TCP),TCP才不管报文的组成,对它来说,现在的报文都将作为TCPPayload。然后TCP在现在的报文前面贴上TCP Header,再作为payload下发给下层(IP). 以此类推,报文前方又贴上了IP HeaderMAC Header,最终从实际设备发送出去了。

那么问题来了,为什么在上面的过程中,报文走的是传输层走的是TCP而不是UDP,应用层走的是IP而不是其他网络层实现 ,最后贴的以太头MAC是贴的什么?

答案是,在应用层其实就指定好了。有过一点socket编程经验应该知道,创建套接字的的接口是

int socket(int domain, int type, int protocol)

这里的domainprotocol就决定了网络层用哪个,比如IP其他。
type决定了传输层使用什么,比如是TCP还是UDP

由此可见, 向外发送的报文通过传输层和网络层的路径在socket创建的时候就已经决定好了.
MAC Header中的源MAC是根据实际发送报文的出接口决定的,这会涉及一些路由相关的东西,比如对于一台有两个网卡的电脑, 每个网卡有自己的MAC地址, 路由过程会根据目的地选择其中一个网卡作为报文的实际出接口,那么MAC Header中的SMAC就是该网卡的MAC地址

layer
至此, 我们就可以描绘出发送过程报文在内核协议栈中的穿梭路径了

接收方向

再来看接收方向, 当报文由网卡驱动程序递交给协议栈时, 就要开始它的协议栈旅程了. 和发送过程正好相反, 发送是一层一层贴Header, 接收是一层一层脱Header了.

还是上面的例子, 假设现在设备驱动程序收到完整的HTTP 以太帧了, 将报文上送给协议栈, 协议栈最下面的链路层(姑且这么称呼) 拿到报文了, 那么他是要将其上送给网络层 (比如IP ) ? 还是 ARP或者 RARP ?

回想上面的报文图, 强调一下: 由于层与层是各司其职的, 所以这里链路层只能看到MAC Header和后面的Payload of Ethernet Frame, 而且, 它只能去理解前者, 对于后者, 它根本无法理解. 所以它必须从MAC Header就能分析出该把这个报文给谁.

2

显然, Dest MACSrc MAC是用来描述设备地址的, 那么答案很明显了, 只有type字段。type字段描述了该以太帧的类型, 比如IP就是0x0800, ARP0x0806 也就是说,当我解析到报文type0x0800时, 就要将报文脱去MAC Header后, 扔给IP.

IP收到后怎么办呢, 在进行自己的处理之后, 它需要把报文上送给上层(传输层) . 给传输层谁呢, 是TCP还是UDP还是其他 ? 一样的道理, 这个信息就在IP Header里, 下面是IPv4版本的Header (IPv6一样的道理)

在这里插入图片描述
看! 关键就是protocol字段, 它指明了该使用的传输层协议. 比如TCP6 , UDP17. 在我们上面的例子中, 收到报文中这个字段一定是 6, 所以IP将报文脱掉IP Header后就会上送给TCP.

TCP收到后怎么办呢, 首先是自己的处理(这部分内容很多, 但我不打算在本文中谈), 最后, TCP会将报文上送给应用层. 一台主机上,使用TCP的应用可能有很多,那么TCP如何正确分发呢。 还是一样,秘密就在TCP Header中。
在这里插入图片描述

在一台主机上,每个应用程序使用的端口号是不同的,正是通过端口号的唯一性,TCP就能知道该送给哪个应用层。从这里可以推断出, 内核一定是有一张类似表的结构,记录了使用的端口号和对应应用程序的信息。其实这个信息正是保存在内核的socket结构里的。在发送过程中,我们没有关注TCP Header中的src portdst port,但其实 这两个端口号也是保存在内核socket结构中,TCP在填写TCP Header时, 也正是根据它.

所以, TCP根据Header中的dst port, 会去看哪个socket结构上的src port与它一致, 一致就说明应该这个报文脱掉TCP Header后丢给这个socket,而应用层程序就可以通过这个socket读取到数据了

松耦合

内核协议栈层与层之间是松耦合的. 比如对于网络层IP来说, 它的报文上送代码一定不是这样:

ip_local_deliver(skb)
{
    protocol = get_protocol(skb)
    switch (protocol):
    case TCP: 
        tcp_rcv(skb);
        break;
    case UDP:
        udp_rcv(skb);
        break;
    ......
}

这样并不是不能工作, 只是如果这样, 当新添加一种传输层协议时, 还要在IP的代码里新加一个case, 这就不合理了. 所以, 内核采用的办法是, 各种传输层实现一组形式一样的接口, 注册到内核中, IP根据Header中的protocol, 一个一个看这个protocol是否已经注册了, 找到了就调用注册的接收函数. 像这样:

static struct net_protocol tcp_protocol {
   .handler = tcp_v4_rcv,
}
static struct net_protocol udp_protocol {
   .handler = udp_v4_rcv,
}
{
    inet_add_protocol(&tcp_protocol, 6)
    inet_add_protocol(&tcp_protocol, 17)
}

ip_local_deliver(skb)
{
    protocol = get_protocol(skb) // TCP报文会返回 6  UDP报文会返回 17
    transport = get(protocol);  //  TCP报文会返回 tcp_protocol UDP报文会返回 udp_protocol
    transport->handler(skb); 
}

这里的struct net_protocol就是每种传输层实现需要实现的接口. 这种统一的形式(比如上面都实现了handler函数)使得IP可以不用管系统中究竟有实现了哪些传输层.后续即使添加新的传输层实现, IP的逻辑也不用变. 这就是松耦合带来的好处.

上面只是网络层<->传输层的例子, 实际上, 协议栈的层与层之间都是以这种形式工作的. 这也是内核网络的设计美学.

总结

  1. 协议栈划分了不同的层次,每个层次都有多种实现. 无论是发送还是接收,报文都按顺序经过每一层的一种实现
  2. 发送过程穿梭路径在套接字创建的时候就已经确定了, 层层贴头
  3. 接收过程层层去头, 头中的信息指明了报文该送到上层哪个实现
  4. 协议栈层与层之间的松耦合体现了协议栈的设计美学
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章