Linux 网络协议及其协议栈
一、 协议和协议栈的区别?
1.通信协议就是通信双方事前约定好的通信规则,可以简单的理解为各个计算机之间进行相互会话所使用的共同语言。
2.协议栈是协议的具体的实现形式,我们通俗的来讲就是用代码实现的库函数,从而方便开发人员的调用。
协议栈是网络中各层协议的总和,其形象的反映了 一个网络中文件传输过程;由上层协议到底层协议,再由底层协议到上层协议。
二、 linux网络栈结构
linux网络栈的层次结构非常清晰,并没有按照OSI七层模型来实现,而是压缩并扩展了一些层。如下图中的所示:
从上而下,依次为应用层,系统调用接口层,协议无关接口层,网络栈层,设备无关接口层,设备驱动层。因为linux的网络栈中的socket是继承自BSD的,socket插口为应用层使用网络服务提供了简单的方法,它屏蔽了具体的下层协议族的差异。下面重点说一下中间的4层。
系统调用接口层。系统调用接口层提供了socket接口的系统调用。
协议无关接口层。为什么会有这一层呢?协议无关指的又是什么无关?首先呢,我们得知道,网络世界里是有很有种协议族的,比如我们最常用的tcp/ipv4协议族,但是除此之外还有很多协议族存在,比如netlink,unix等,因此,为了使用上的方便,抽象了一个协议无关接口层,只需要在创建socket时,传入对应的参数,就能创建出对应的协议族socket类型。具体的可以看一下socket函数的参数:socket(int domain, int type, int protocol);第一个参数就定义了使用的协议族,ipv4的?ipv6的?unix的?等等。第二个参数就是指定socket类型,是流式套接字还是用户数据报?还是原始套接字?一般来说,前两个参数选定了,就能确定一个socket的类型和使用的传输层协议了,如流式套接字对应使用tcp/ip中的tcp协议,用户数据包对应使用tcp/ip中的udp协议。
网络栈层。这一层就是具体的各类协议的实现了。包括传输层和网络层。对于我们最经常使用的tcp/ip来说,传输层主要包括tcp和udp协议。网络层就是ip协议。这一部分也是这个系列重点需要解释的,后面仔细说。
设备无关接口层。这一层夹在网络栈和驱动层之间,至于为什么会有这么一层存在?可以想象一下,网络设备种类多样,当收到数据包时,怎么传递给网络栈?如果没有设备无关接口层的抽象,势必会导致两层之间的调用花样百出,因此,有必要抽象出设备无关层,如驱动向上的传递接口,通用设备表示等。从这个设计来看,给我们很多启示,联想上面的协议无关接口层,可以看出,在一对多这种情况下,设计一个通用层会有很多好处。
三.、linux网络栈文件分布层次
在第一节对实现网络设计结构做了说明之后,这一节说明一下实现文件的分布。
1. 文件的实现主要在/linux/net目录下。
2. /linux/net目录下的几乎每个文件夹就是一个协议族的集合。如ipv6,ipv4,802,ATM。
3. 对于ipv4的网络层,传输层的实现分布在/linux/net/ipv4中。
4. 协议无关接口层和设备无关接口层分布在/linux/net/core文件夹中。
socket操作系统调用
我们在上一节中说到过,在应用层和协议无关层之间,是一个系统调用接口层。系统调用接口如下:
- socketcall socket系统调用
- socket 建立socket
- bind 绑定socket到端口
- connect 连接远程主机
- accept 响应socket连接请求
- send 通过socket发送信息
- sendto 发送UDP信息
- sendmsg 参见send
- recv 通过socket接收信息
- recvfrom 接收UDP信息
- recvmsg 参见recv
- listen 监听socket端口
- select 对多路同步I/O进行轮询
- shutdown 关闭socket上的连
- getsockname 取得本地socket名字
- getpeername 获取通信对方的socket名字
- getsockopt 取端口设置
- setsockopt 设置端口参数
- sendfile 在文件或端口间传输数据
- socketpair 创建一对已联接的无名socket
当在应用中调用socket()函数时,就会触发系统调用,跟socket相关的操作函数都会被映射到sys_socketcall的系统调用中(32位系统),在文件unistd_32.h中有其系统调用号表。对于64位系统,系统调用号会不一样,在文件unistd_64.h中,跟socket相关的系统调用会直接对应,不用都映射到sys_socketcall(实际上,64位系统中会通过定义__NO_STUBS宏屏蔽这个调用号)。具体的系统调用过程可以参考如下链接:http://lib.csdn.net/article/embeddeddevelopment/55382
因为网络栈是在内核态,所以从socket api到操作socket插口存在一个系统调用层。在本文中,我们看到了当使用socket api时,是怎么调用到系统调用的,在下一篇中,将介绍对应的系统的调用是怎么操作socket插口的。
一. 从socket api看协议无关层
前面的系列已经说了系统调用接口层,在应用层使用socket api,填充对应的参数,就能创建出想要使用的socket类型。这个过程就是协议无关层完成的。简单的说过程就是:根据参数,匹配注册的协议族,使用对应的协议。接下来重点分析几个socket api的BSD无关层实现,来更深一步理解这个问题。
1.1 socket()
在应用层调用socket()后,就会触发sys_socket
系统调用。那我们就去看看在这个系统调用函数中都做了什么事:net/socket.c
文件
SYSCALL_DEFINE3(socket,
int, family,
int, type,
int, protocol)
{
int retval;
struct socket *sock;
int flags;
/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
if (retval <
0)
goto out;
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
if (retval <
0)
goto out_release;
out:
/* It may be already another descriptor 8) Not kernel problem. */
return retval;
out_release:
sock_release(sock);
return retval
}
省略前面的检查,实际上只有2个函数:sock_create()
和sock_map_fd()
。接着看sock_create()
,sock_create()
->__sock_create()
.
security_socket_create()
这是创建安全的socket,没定义的话,就是空操作。sock_alloc()
创建了一个struct socket,注意这个结构,后面还有其他的很相似的结构。struct socket就是BSD层维护的socket对象。因为UNIX秉承一切皆文件的思想,所以,这个socket对象也是和inode结构一起创建并绑定到一起的。创建之后,填充socket的类型:sock->type = type;
- 根据协议族和类型,创建对应类型的socket(struct sock)。如下代码有删减。
pf = rcu_dereference(net_families[family]);
err = pf->create(net, sock, protocol);
if (err <
0)
goto out_module_put;
pf->create()
到这里才是真正的无关层体现的地方,即根据协议族和类型,创建socket。但这里的socket实际指的是struct sock结构,这个结构贯穿了整个协议栈。那它和struct socket有啥区别和联系呢?这里先卖个关子,到数据结构那节再具体说。现在接着说pf->create
,pf是从net_families[family]
中取出来的,pf的结构如下:
struct net_proto_family {
int family;
int (*create)(
struct net *net,
struct socket *sock,
int protocol);
struct module *owner;
};
这实际上是代表了一个协议族,每个协议族有自己的create函数,如inet就是inet_create()
。那肯定要问了,那这些协议族是什么时候注册的呢?答案是在协议族初始化的时候。还拿inet族举例,就是在inet_init()时,再往上追就是fs_initcall(inet_init);
。
我们看到这么一句话:
(
void)sock_register(&inet_family_ops);
你瞧,inet_family_ops
(
struct net_proto_family)
把自己注册进"活着"的协议族列表,仿佛在说:“hi,BSD,我INET协议族来报道啦!”
int sock_register(const struct net_proto_family *ops)
{
int err;
if (ops->family >= NPROTO) {
printk(KERN_CRIT
"protocol %d >= NPROTO(%d)\n", ops->family,
NPROTO);
return -ENOBUFS;
}
spin_lock(&net_family_lock);
if (net_families[ops->family])
err = -EEXIST;
else {
net_families[ops->family] = ops;
err =
0;
}
spin_unlock(&net_family_lock);
printk(KERN_INFO
"NET: Registered protocol family %d\n", ops->family);
return err;
}
看那net_families[ops->family]
不就是前面提到的么?这就是“活着”的协议族的集合。
4.inet_create(),创建socket。
list_for_each_entry_rcu(answer, &inetsw[sock->type],
list) {
err =
0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
}
else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
先根据sock->type
找到要创建的socket的模板,模板是什么,怎么来的呢?对于inet协议族而言,模板就是都在inetsw这个链表里存着啦,自然会想到是不是也是在inet_init()
时添加进去的呢?没错,我们回过头去看:
for (r = &inetsw[
0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
到这里不得不说一下socket类型这个东西,站在BSD的角度,抽象了几种socket类型出来:
enum sock_type {
SOCK_STREAM =
1,
SOCK_DGRAM =
2,
SOCK_RAW =
3,
SOCK_RDM =
4,
SOCK_SEQPACKET =
5,
SOCK_DCCP =
6,
SOCK_PACKET =
10,
};
对于具体的协议族怎么实现这些对应的类型,BSD是不关心的,比如在inet协议族,tcp对应的是SOCK_STREAM
类型,udp对应的是SOCK_DGRAM
等。对于其他的协议族而言,要对应起自己的协议。再看上面的两句,初始化了每种inet每种socket类型的链表。然后inet_register_protosw()
把inetsw_array
数组中对应的类型注册到inetsw对应类型的链表中。这个inetsw_array
数组其实就是每个协议族连接BSD和自己的传输层的桥梁。就拿其中一个元素来看:
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.capability =
-1,
.no_check =
0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
就是说,如果BSD要创建SOCK_STREAM
类型的套接字,那么就对应于inet族的tcp协议,自然对于BSD层的操作,也有抽象出来的操作集,就是struct proto_ops
表示。那么为什么还会有一个struct ops
的结构呢?很明显,这个是用于inet层的操作集,上文提到,每一种类型的socket都有自己的协议链表,也就是对于inet的流式套接字而言,也可能有多种协议(当然,现在只注册了tcp一种)。
接着刚才的创建往下看,找到对应的类型后,就填充其中的成员:
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_no_check = answer->no_check;
answer_flags = answer->flags;
然后就分配了struct sock
对象和struct inet_sock
对象,
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
。初始化sock对象和inet_sock对象。最后调用sock对象的init。主要是针对tcp和raw的,需要初始化,udp就没有init操作。
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
5.映射socket到文件描述符。在完成socket创建后,就把这个socket和一个文件描述符fd关联起来,以后就可以直接用读写文件的方式访问这个socket了。sock_map_fd()
。这一步操作过后,socket创建就算完成了。
1.2 sendto()
1.3 recvfrom()
这两个接口的介绍,就放到下一篇中进行,选着两个主要就是想说一下数据包从内核态到用户态的过程。本篇已经很长了,原谅我先挖个坑吧 :)
六、相关数据结构
struct socket
与struct sock
与struct inet_sock
这两个数据结构实际上是分工协作的,不单纯是分层的关系。struct socket代表的是BSD层上对所有socket的抽象,而struct sock代表的是每个协议族对socket的抽象。这是两个很重要的层次关系,我们说协议无关层下面就是传输层,但并不是说sock是对传输层的socket的抽象,所以,无论是对BSD层的抽象还是对每个协议族的抽象,都应该属于协议无关层,只是对协议无关层两个方面的抽象。这时候就有疑问了:既然都属于协议无关层的抽象,为什么不合并这两个结构呢?因为struct socket是使用inode关联的,我们也看到struct sock结构庞大,如果把struct sock合并到struct socket中,岂不是建每个文件inode都需要占用很大空间咯?所以,把套接字中的跟文件有关的部分拿出来就是struct socket,而把剩余的跟socket数据有关的都放到struct sock中。而struct inet_sock
则是为了更加方便使用inet族,在struct sock的基础上,又封装而成的。struct proto_ops
与struct proto
自然,BSD层既然抽象了socket,就会有抽象的操作集,struct proto_ops就是抽象的操作集,它操作的就是协议族;struct proto就是对协议族层面的操作集的抽象,它操作的就是实际的传输层协议。这个可以跟上面的struct socket和struct sock进行类比。
这些结构共同实现了网络栈协议无关层。所以,可以看到,这一篇没有说到具体传输层的东西,比如tcp,udp操作等。
七. 文件分布
协议无关层的文件分布主要在:
net/socket.c
net/core/sock.c
八. 小结
linux网络栈非常庞大,包罗万象,支持各种各样的协议,为了能够让应用层简单方便的使用socket,linux提供了抽象层,把跟协议无关的东西拿出来,让应用层仅仅提供参数,就能在适配合适的协议族和使用的协议。大大减小了应用层的复杂度。
为什么 Linux 不将网络协议栈在用户态实现?
做为一个宏内核操作系统,将网卡驱动程序放在内核态无可厚非。但是,为什么Linux要将庞大的网络协议栈也放在内核呢?
在内核态实现网络协议栈,不仅代码与网卡驱动程序紧耦合,网卡驱动程序代码BUG可能误踩协议栈内存使整个系统挂死;而且使用户态应用程序拦截报文时不得不通过各种内核钩子(例如netfilter)才能将报文trap到用户态,对性能本身也是损耗。
当初Linux设计时,可能是因为考虑到内核态和用户态之间切换损耗性能而将协议栈在内核实现。但是现代CPU都已经有专门的内核态和用户态切换指令,切换速度已经很快。那么为什么Linux仍然不考虑将庞大的网络协议栈移到用户态呢?
目前很多通讯行业的产品,包含俩套协议栈,一个在内核态,一个在用户态。用户态的一般绕过内核,负责数据的处理也就是常说的数据面。而内核态保留原有功能,负责控制面的一些功能。比如ftp加载版本,telnet,ssh,ping(普通),syslog告警等等。工作在数据面采用dpdk则是常态!
linux网络协议是什么?该如何去理解?
网络协议有很多,但大多是针对windows的,那么linux网络协议你是怎么样理解的呢?本文和大家一起探讨这个问题。
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网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰。 其中的两个“无关”层的设计非常棒,通过这两个“无关”层,其协议栈可以非常轻松的进行扩展。在我们自己的软件设计中,可以吸收这种设计方法。
以上就是如何理解Linux网络协议的方法,根据Linux网络协议四层相互之间的关系了解性的记忆会比较有帮助,谢谢大家的阅读。
理解 Linux 网络栈(1):Linux 网络协议栈简单总结
- Linux 网络路径
1.1 发送端
1.1.1 应用层
(1) Socket
应用层的各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的。Linux Socket 是从 BSD Socket 发展而来的,它是 Linux 操作系统的重要组成部分之一,它是网络应用程序的基础。从层次上来说,它位于应用层,是操作系统为应用程序员提供的 API,通过它,应用程序可以访问传输层协议。
- socket 位于传输层协议之上,屏蔽了不同网络协议之间的差异
- socket 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体
在Linux系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便。
(2)应用层处理流程
- 网络应用调用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() 函数。
1.1.2 传输层
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。TCP 协议栈的大致处理过程如下图所示:
TCP 栈简要过程:
tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。
构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。
构造 TCP header。
计算 TCP 校验和(checksum)和 顺序号 (sequence number)。
TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。
TCP的校验和是必需的
发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。
UDP 栈简要过程:
UDP 将 message 封装成 UDP 数据报
调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。
1.1.3 IP 网络层 - 添加header 和 checksum,路由处理,IP fragmentation
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。
1.1.4 数据链路层
功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。
实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。
1.1.5 物理层 - 物理层封装和发送
物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。
一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
1.1.6 简单总结
1.2 接收端
1.2.1 物理层和数据链路层
简要过程:
一个 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包,进入第三层网络层处理。
1.2.2 网络层
IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用 dev_queue_xmit,进入链路层处理流程。
1.2.3 传输层 (TCP/UDP)
传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
1.2.4 接收端 - 应用层
每当用户应用调用 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 方法。
1.2.5 报文接收过程简单总结
2. Linux sk_buff struct 数据结构和队列(Queue)
2.1 sk_buff
(本章节摘选自 http://amsekharkernel.blogspot.com/2014/08/what-is-skb-in-linux-kernel-what-are.html)
2.1.1 sk_buff 是什么
当网络包被内核处理时,底层协议的数据被传送更高层,当数据传送时过程反过来。由不同协议产生的数据(包括头和负载)不断往下层传递直到它们最终被发送。因为这些操作的速度对于网络层的表现至关重要,内核使用一个特定的结构叫 sk_buff, 其定义文件在 skbuffer.h。Socket buffer被用来在网络实现层交换数据而不用拷贝来或去数据包 –这显著获得速度收益。
sk_buff 是 Linux 网络的一个核心数据结构,其定义文件在 skbuffer.h。
socket kernel buffer (skb) 是 Linux 内核网络栈(L2 到 L4)处理网络包(packets)所使用的 buffer,它的类型是 sk_buffer。简单来说,一个 skb 表示 Linux 网络栈中的一个 packet;TCP 分段和 IP 分组生产的多个 skb 被一个 skb list 形式来保存。
struct sock 有三个 skb 队列(sk_buffer queue),分别是 rx , tx 和 err。
它的主要结构成员:
struct sk_buff {
/* These two members must be first. */ # packet 可以存在于 list 或者 queue 中,这两个成员用于链表处理
struct sk_buff *next;
struct sk_buff *prev;
struct sk_buff_head *list; #该 packet 所在的 list
...
struct sock *sk; #跟该 skb 相关联的 socket
struct timeval stamp; # packet 发送或者接收的时间,主要用于 packet sniffers
struct net_device *dev; #这三个成员跟踪该 packet 相关的 devices,比如接收它的设备等
struct net_device *input_dev;
struct net_device *real_dev;
union { #指向各协议层 header 结构
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h;
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;
union {
unsigned char *raw;
} mac;
struct dst_entry *dst; #指向该 packet 的路由目的结构,告诉我们它会被如何路由到目的地
char cb[40]; # SKB control block,用于各协议层保存私有信息,比如 TCP 的顺序号和帧的重发状态
unsigned int len, #packet 的长度
data_len,
mac_len, # MAC header 长度
csum; # packet 的 checksum,用于计算保存在 protocol header 中的校验和。发送时,当 checksum offloading 时,不设置;接收时,可以由device计算
unsigned char local_df, #用于 IPV4 在已经做了分片的情况下的再分片,比如 IPSEC 情况下。
cloned:1, #在 skb 被 cloned 时设置,此时,skb 各成员是自己的,但是数据是shared的
nohdr:1, #用于支持 TSO
pkt_type, #packet 类型
ip_summed; # 网卡能支持的校验和计算的类型,NONE 表示不支持,HW 表示支持,
__u32 priority; #用于 QoS
unsigned short protocol, # 接收 packet 的协议
security;
2.1.2 skb 的主要操作
(1)分配 skb = alloc_skb(len, GFP_KERNEL)
(2)添加 payload (skb_put(skb, user_data_len))
(3)使用 skb->push 添加 protocol header,或者 skb->pull 删除 header
2.2 Linux 网络栈使用的驱动队列(driver queue)
(本章节摘选自 Queueing in the Linux Network Stack by Dan Siemon)
2.2.1 队列
在 IP 栈和 NIC 驱动之间,存在一个 driver queue (驱动队列)。典型地,它被实现为 FIFO ring buffer,简单地可以认为它是固定大小的。这个队列不包含 packet data,相反,它只是保存 socket kernel buffer (skb)的指针,而 skb 的使用如上节所述是贯穿内核网络栈处理过程的始终的。
该队列的输入时 IP 栈处理完毕的 packets。这些packets 要么是本机的应用产生的,要么是进入本机又要被路由出去的。被 IP 栈加入队列的 packets 会被网络设备驱动(hardware driver)取出并且通过一个数据通道(data bus)发到 NIC 硬件设备并传输出去。
在不使用 TSO/GSO 的情况下,IP 栈发到该队列的 packets 的长度必须小于 MTU。
2.2.2 skb 大小 - 默认最大大小为 NIC MTU
绝大多数的网卡都有一个固定的最大传输单元(maximum transmission unit, MTU)属性,它是该网络设备能够传输的最大帧(frame)的大小。对以太网来说,默认值为 1500 bytes,但是有些以太网络可以支持巨帧(jumbo frame),最大能到 9000 bytes。在 IP 网络栈内,MTU 表示能发给 NIC 的最大 packet 的大小。比如,如果一个应用向一个 TCP socket 写入了 2000 bytes 数据,那么 IP 栈需要创建两个 IP packets 来保持每个 packet 的大小等于或者小于 1500 bytes。可见,对于大数据传输,相对较小的 MTU 会导致产生大量的小网络包(small packets)并被传入 driver queue。这成为 IP 分片 (IP fragmentation)。
下图表示 payload 为 1500 bytes 的 IP 包,在 MTU 为 1000 和 600 时候的分片情况:
备注:
以上资料是从网络上获取的各种资料整理而来
这一块本身就比较复杂,而且不同的 linux 内核的版本之间也有差异,文中的内容还需要进一步加工,错误在所难免。
物理设备接收到数据后,基本步骤是:
1. 硬件发出中断
2. 中断处理程序在主内存总分配DMA buffer
3. 硬件向 buffer 写入数据。在完成时发出另一个中断。
4. 中断处理程序再见 DMA buffer 中的数据分发出去。
那么在发出时,也应该是类似的两个步骤:
1. 将数据从主内存通过DMA 拷贝到网卡的 buffer 里面
2. 中断处理程序将网卡buffer里面的数据发出去
user space data ----> copy to DMA buffer & add header
DMA buffer copy done --> issue DMA
DMA buffer ----(DMA interrupt)---> Card data register
转自:https://www.cnblogs.com/sammyliu/p/5225623.html