本文了解的具体内容与核心
TCP/IP 协议体系
- 应用层:为网络用户提供各种服务,例如电子邮件、文件传输等。
- 表示层:为不同主机间的通信提供统一的数据表示形式。
- 会话层:负责信息传输的组织和协调,管理进程会话过程。
- 传输层:管理网络通信两端的数据传输,提供可靠或不可靠的传输服务。
- 网络层:负责数据传输的路由选择和网际互连。
- 数据链路层,负责物理相邻(通过网络介质相连)的主机间的数据传输,主要作用包括物理地址寻址、数据帧封装、差错控制等。该层可分为逻辑链路控制子层(LLC)和介质访问控制子层(MAC)。
- 物理层,负责把主机中的数据转换成电信号,再通过网络介质(双绞线、光纤、无线信
道等)来传输。该层描述了通信设备的机械、电气、功能等特性。
通常,上述的传输层、网络层、数据链路层和物理层又被依次称为第四层、第三层、第二层和第一层。相比于OSI体系,TCPIP 协议体系的架构更加简单实用:
说明:
注意:
- 除了上述标准的四个层次外,TCP/IP协议体系还需要路由协议来管理数据传输路径,ARP协议来管理本地网络寻址。这两部分都在Linux网络内核中实现
- 为支持应用程序访问网络服务,内核提供了编程接口,即套接字接口。(俗称网络编程)
网络数据包的封装与解封
以 FTP 客户/服务器工作过程为例,描述 TCP/IP 协议体系处理流程
文字描述流程:
发送
- 第一,在应用层,FTP 协议模块在文件数据前部添加 FTP 头部(标准的 FTP 协议数据段),把文件数据封装成FTP数据:
- 第二,在传输层,TCP协议模块在FTP数据前部添加TCP头部(标准的 TCP协议数据段),把FTP数据封装成TCP数据(段):
- 第三,在网络层,IP协议模块在 TCP 数据前部添加 IP 头部(标准的 I P协议数据段),把 TCP 数据封装成 IP 数据(分组);
- 第四,在网络介质层,以太网设备驱动程序在数据前部和后部添加以太网头部和尾部(标准的以太网数据段),把IP数据封装成以太网数据(帧)。
- 最后,以太网设备把以太网数据转换成电信号,通过以太网线将数据信号传给客户端。
接收
- 第一,检测到网线上的电平信号后,以太网卡尝试把收到的数据转换成完整的以太网数据:
- 第二,如果以太网卡成功获取一个发给本客户端的数据,则启动驱动程序去掉以太网数据的头部和尾部,得到被封装的IP数据,并将该数据递交给网络层;
- 第三,在网络层,IP 协议模块去掉 IP 数据头部,还原被封装的 TCP 数据,并递交给传输层;
- 第四,在传输层,TCF协议模块去掉 TCP 数据头部,还原被封装的FTP 数据,并递交给应用层:
- 第五,在应用层FTP协议模块去掉 FTP数据头部,还原被封装的文件数据。
分片知识补充
传输文件时,TCP/IP协议体系必须考虑网络介质对网络数据单元的尺寸限制。当文件长度超出了最大尺寸限制时,服务器端把文件数据分成一些小的分片,然后分别封装发送。当接收到这些分片后,客户端把分片重新组合成原始文件。
linux网络内核组成
- 套接字接口:网络内核最顶层是支持应用程序开发的函数接口,这是一系列标准函数。
- 套接字支持多种不同类型的协议族:UNIX域协议族、TCP/IP协议族、IPX协议族等。本书只讨论TCP/IP协议族对应的套接字(INET套接字),该套接字又包括3个基本类型:SOCK_STREAM、SOCK_DGRAM和SOCK_RAW。通过SOCK_STREAM接可以访问TCP协议、SOCK_DGRAM套接字可以访问UDP协议、SOCK_RAW套接字可以直接访问IP协议。可见,套接字接口是网络内核的入口。
- 传输层和网络层。套接字往下依次是传输层和网络层。传输层包括标准的TCP和 UDP协议模块,而网络层包括标准的 IP 协议模块。
- 数据链路层。对于需要逻辑链路的网络,数据链路层提供独立的逻辑链路协议模块比如 PPP、SLIP 等。对于以太网,该层比较简单,主要的以太网协议实现被集成到底层的网卡驱动中。
- 网络设备驱动。由于物理特性的差异,因此不同的网络设备采用不同的设备驱动。
我的linux的版本为:linux-3.0.86:
我罗列了主要的基本框架,希望对你对linux的基本文件及路径的了解有所帮助
补充:
- 套接字缓冲区类型被定义在 include/linux/skbuff,h;
- 接口函数 dev_queue_xmit 被定义在 net/core/dev.c。
套接字缓冲区类型说明:
- struct sk_buff 是 Linux内核中用于表示网络数据包的数据结构。它是在网络编程中非常重要的一个数据结构,被广泛用于网络协议栈的各个层次,包括数据包的接收、发送、路由等操作。
这段的意思是:控制缓冲区是用于存储数据包在整个网络协议栈中的控制信息,它可以被每一层协议自由使用。换句话说,每个网络协议层都可以向控制缓冲区中添加自己的私有变量或控制信息,以便进行特定的处理或传递额外的信息。如果想要在多个网络协议层之间共享私有变量,需要使用 skb_clone() 函数来进行克隆操作。这是因为控制缓冲区是被当前持有数据包的网络协议层所拥有的,当数据包被传递到下一层协议时,控制缓冲区的所有权也会随之转移。如果需要在多个协议层之间共享私有变量,就需要在克隆数据包之后,复制控制缓冲区的内容,这样就能够在克隆的数据包中共享这些私有变量了。
dev_queue_xmit解释说明:
- 准备好要发送的数据包(通常是一个 struct sk_buff 结构体),并将数据包填充好数据和相关的协议头等信息
- 调用 dev_queue_xmit() 函数,将数据包添加到网络设备的发送队列中。
- 当数据包被添加到发送队列后,网络设备驱动程序将会从队列中取出数据包,并将其发送到网络上。
内核中的数据包处理流程
我们把上面的linux网络内核组成的简图延伸下就得到数据包的处理流程:
因为图实在太大了,又想要清楚,就分成了两个图贴出来。
在图中未列出流程经历的所有函数,仅给出几个必要的标志函数。这些函数组成的流程足以反映网络内核处理数据的过程,向下的实线箭头是数据包发送流程,向上的实线箭头是数据包接收流程,还有就是路由查找流程。
数据包发送流程
该流程从网络应用程序的数据包发送函数开始(如套接字接口函数sendto),到网络设备驱动的数据包发送函数结束(如CS89x网卡的函数netsendpacket)。
- 传输层处理。如果采用 UDP 协议发送,数据包由 sendto 经套接字接口进入 UDP 协议模块的 udp_sendmsg。完成UDP协议封装后,再经ip_push pending _fames 进入IP协议模块。如果采用TCP协议发送,数据包由sendto经套接字接口进入TCP协议模块的tcp_sendmsg。完成TCP协议封装后,再经 ip_queue_xmit 进入IP协议模块。为能找到正确的数据发送路径需要调用路由模块的ip_route_output_flow 来查找路由信息。.
- 网络层处理。UDP和TCP处理后的数据包进入IP协议模块的 ip_output。确定发送出口设备后,数据包经 ip_finish_output2 和邻居子系统进入 dev_queue_xmit接口函数。通过该函数,数据包进入网络设备。
- 网络设备处理。根据底层网络设备的类型,dev_queue_xmit 函数把数据包交给设备的发送函数。如果底层设备是CS89x网卡,数据包由net_send_packet函数进入网络。发送前,数据包会被封装成标准帧格式。对于采用了逻辑链路协议的网络,dev_queue_xmit函数会把数据包交给逻辑链路协议模块,再经底层的网络设备发送出去。
数据包接收流程
该流程从网络设备驱动的数据包接收函数开始(如CS89x网卡的函数net_rx),到网络应用程序的数据包接收函数结束(如套接字接口函数recvfrom)。
- 网络设备处理。数据包到达网络设备后,硬件中断会被触发来完成数据的接收工作。中断处理程序调用 net_rx对数据包做进一步处理。解析出帧中封装的内容后,数据包由ip_rcv函数递交给IP协议模块。如果网络设备上层是逻辑链路协议模块,那么数据包必须先被递交到其协议模块,在完成处理后再由ip_rcv函数递交给IP协议模块。
- 网络层处理。数据包进入IP协议模块后,ip_rcv_finish 首先判断数据包是本地接收的数据包还是转发的数据包。如果是本地接收的数据包,会进入ip_local_deliver函数完成IP协议的进一步处理;从IP分组解析出数据内容后,数据包会被iplocal_delive_finish 函数递交给传输层的接收函数(TCP协议是tcp_v4_rcv数,UDP协议是udp_rcv数)。如果ip_rcv_finish 判断是转发的数据包,需要调用路由模块的iprouteinput 查找路由表,确定数据转发路径;然后,将数据包交给ipforward函数,再由ip_forward_finish进入ip_output,为转发数据包做准备。这是IP层的转发过程,从函数ip_rcv_finish 开始,到ip_output 结束
- 传输层处理。如果采用 UDP协议接收,数据包由udp_rcv函数进入 UDP 协议模块,再由 udp_recv_msg经套接字接口进入应用程序的recvfom函数。如果采用 TCP协议接收,数据包由 tcp_v4_rcv函数进入TCP协议模块,再由tcp_recv_msg 经套接字接口进入网络应用程序的 recvfrom 函数。
网络内核的重要数据结构
网络内核提供重要的数据类型来支持网络数据包处理,我们分享三个类型:套接字缓冲区类型 structsk bu用来管理网络数据包,structsk buff head 类型用来管理这些套接字缓冲区,structskb shared info 类型用来管理套接字缓冲区的数据包分片信息。
struct sk buff head 类型:struct sk_buf_head 的定义位于文件 include/linux/Skbuf.h
内核的套接字缓冲区(struct sk_buf结构)被组织在一个双向链表中,并通过struct sk_buf_head 类型来管理。
类型 struct sk buf head 与套接字缓冲区的关系:
这样就清楚明了了
struct sk_buff 类型:struct sk_buf的定义位于文件 include/linux/Skbuff.h
struct sk buf是套接字缓冲区类型,被用来管理网络数据包。该类型不仅为发送和接收网络数据提供存储区域,而且为使用这些数据提供操作方法。
成员变量:
- 数据区指针
在套接字缓冲区中,head 和end指向缓冲区的起始和结束位置,而data和tail指向实际数据内容的起始和结束位置。
headroom表示数据存储区内head与data地址之间的区域,tailroom 表示 tail和 end 地址之间的区域。当数据从协议栈的上层传递到下层时,可向这两个区域填充协议字段内容。 - 头部指针
套接字缓冲区用联合体定义了3个协议头部指针:
其中,transport_header指向第四层协议头部,可定义为 TCP、UDP、ICMP、IGMP 等协议数据包的头部指针:network_header 指向第三层协议头部,可定义为 IPv4、IPv6、ARP 等协议数据包的头部指针;mac_header指向第二层协议头部,可定义为以太网等协议数据包的头部指针:
如下是代码注释:
在数据包处理过程中,data指向当前层的协议头部。在数据包的发送流程中,接收到上层协议传递下来的数据包后,当前层会在数据区内增加协议头部,调整 data指向新增加的协议头部起始位置:在数据包的接收流程中,当前层完成协议处理并准备向上层递交时,调整data指向本层协议头部的结束位置(这也是上层协议的头部起始位置)。
如下图说明了数据包(采用 TCP协议)从第三层递交到第四层时的场景指针的变化情况:
- cb字段:
40 字节的 cb 配合各层协议工作,足够为每层存储必要的控制信息 - pkt_type字段
pkt_type 表示数据帧的类型,由第二层协议模块根据目的地址来确定。如果第二层采用以太网,协议模块将调用 ethtype_trans 函数设置该类型值。如下是对应值和类型:
struct skb_shared_info 类型
类型 struct skb_shared_info用来管理数据包分片信息,它与struct sk_buff的关系可通过宏skb_shinfo 表示出来:
从上述宏定义可见,每个套接字缓冲区通过自己的end 指针指向数据包的分片信息。
套接字缓冲区的操作函数
内核提供了一系列操作缓冲区的函数:上层协议模块需要发送数据包或网络设备准备接收数据包时,调用 alloc_skb函数分配套接字缓冲区,管理数据包内容;不需要处理数据包时调用kfreeskb数释放数据包占用的套接字缓冲区;各个协议模块通过调用skb_put、skb_push、skb_pull、skb_trim、skb_reserve、skb_clone、pskb_copy 和skb _copy 等函数来处
理数据包,比如给数据包添加协议头部。
- alloc_skb 函数: 函数 alloc_skb分配套接字缓冲区,创建一个新的struct sk_buff结构并完成初始化,
分配缓冲区时,alloc_skb建立了套接字缓冲区与struct skb_shared_info 结构的关系,如图 :
函数alloc skb 位于文件 net/core/Skbuff.c - kfree skb 函数:函数 kfree skb用于释放套接字缓冲区,函数kfree skb 位于文件 include/linux/Skbuff.h
- 数据区的操作函数:
函数 skb_put 被用来在数据区末端添加某协议的尾部。该函数需要调整 skb_buf中的 tail指针,并增加 len 长度值。
函数skb_push被用来在数据区前端添加某协议的头部。该函数需要调整sk_buff 中的data指针,增加 len 长度值。
为便于理解上述函数,下面通过图来描述其工作过程:
函数 skb_pull 被用来去掉数据包的协议头部。具体实现是调用函数 skb_pull,调整 data指针,减小长度 len。
函数 skb_trim 被用来去掉数据包的协议尾部。具体实现是调用函数 skb_trim,调整 data指针,减小长度 len。
函数skb_reserve 被用来在数据区创建存储协议头部的空间,以便于skb_push 添加头部也可用于调整数据区大小,保持长度一致。
套接字缓冲区链表的管理函数
- skb_queue_head_init函数:函数 skb_queue_head_init 被用来初始化 structsk buff_head 结构。必须在链表操作之前调
用该函数,且不能重复执行。 - skb_queue_head 函数:函数skb_queue_head在套接字缓冲区链表头部添加一个缓冲区。
- skb_queue_tail 函数:函数skb_queue_ tail在套接字缓冲区链表的尾部添加一个缓冲区。
- skb_dequeue 函数:函数 skb_dequeue 把排在头部的缓冲区从套接字缓冲区链表中移走,并返回该缓冲区如果队列为空,就返回空指针。
- skb_dequeue_tail 函数:函数 skb dequeuetail从套接字缓冲区链表尾部移走一个缓冲区。
- skb_queue_purge 函数:函数 skb_queue_purge 清空套接字缓冲区链表。
- skb_append 函数:函数 skb append 的功能是在指定套接字缓冲区上附加一个缓冲区。
- skb_insert函数:函数 skb insert 向套接字缓冲区链表插入一个缓冲区。
网络设备
介绍网络设备的管理结构和操作方法。由于内容与网络设备驱动程序密切相关。
-
struct net_device类型
struct net_device 是内核用来管理网络设备的数据结构,将其字段分成如下类别:通用字段、硬件配置字段、网络层数据字段、物理层数据字段和设备驱动程序中的函数。其详细定义可参考内核文件 include/linux/netdevice.h,下面仅对主要字段进行解释。
name 字段:描述了网络设备名称。注册网络设备时,需要为设备分配唯一的字符名。对于同一类型的多个设备,可从0到n进行编号,如以太网卡按eth0、eth1等编号。
next字段:用来管理设备链表,连接多个struct net_device 结构。系统中要安装多个网络设备时,需要把这些设备组织成链表,由全局变量dev_base 统一管理。
owner字段:指向由本网络设备创建的模块。
ifindex字段:网络设备的索引值,也可用来标志网络设备。创建新设备后,dev_get_index 为该设备分配一个索引值,以便快速定位网络设备。
ifink 字段:用来发送数据包的网络设备索引,该字段的值通常是ifindex的索引值。
state 字段:网络设备的状态信息。
时间字段:trans start是最近的发包时间,lastrx是最近的收包时间,配合发送数据的定时器watchdog timero和定时器列表watchdog timer。
priv 字段:指向与网络设备特性有关的私有数据结构。
refcnt字段:代表网络设备的引用次数。
同步锁字段:ingress_lock为输入保护锁,xmit_lock为函数指针hard_start_xmit 的同步锁,xmit_lock_owner 为拥有发送锁 xmit_lock的处理器编号,queue_lock为队列保护锁。 -
硬件配置字段
内存共享字段:包括rmem_end、rmem_start、mem_end和mem_start,描述了网络适配器与内核共享的内存空间。地址mem_start和mem_end指定了发送包所在的区域,而rmem_start和rmem_end指定了接收包的区城。
base_addr字段:驱动程序用来搜索设备的 IO基地址。
irq字段:设备使用的中断号。
dma字段:分配给设备的 DMA通道号。
ifport 字段:多端口设备所使用的不同端口,由网络介质类型确定。 -
物理层数据字段
hard_header_length 字段:表示第2层协议头部长度。比如,以太网卡的 hard_header_length字段取值为 14。
mtu 字段:表示最大传输单元。比如,对于以太网,该值为1500字节。当网络层协议模块通过底层设备发送数据包时,必须根据该值进行合理的分片,以避免发出过长的数据包。
tx_queue_len字段:表示网络设备输出队列的最大长度。
地址字段:broadcast 表示广播地址,dev_addr表示硬件地址,addr_len表示硬件地址长度,dev_mc_list表示多播地址表,mc_count是dev_xmc_list中的地址数目。
type 字段:代表网络设备的类型
-
网络层数据字段
协议信息字段:ip_ptr指向 IPv4 协议信息,ip6_ptr指向 IPv6 协议信息,若采用IP网络设备,那么ip ptr指向 struct in_device 类型的结构,管理IP 实例信息和配置参数。
网络地址信息字段:family 表示网络设备采用的协议地址族,pa_alen 表示协议地址长度pa_addr 表示网络设备的地址,pa_baddr 表示广播地址,pa_mask 表示网络掩码,pa_dstaddr表示点对点连接(如 PPP或 SLIP)中对方的地址。对于TCP/IP协议族,family 字段取值为AF INET,而pa_alen 字段是4字节。
flags 字段:是用于设备管理的开关标志,可取如下值
-
设备驱动程序中的函数指针
根据具体驱动程序
网络设备链表
管理网络设备的 struct net_device 结构体有一个next指针,可以用来连接系统中的所有网络设备。内核把这些连接起来的设备组成一个链表,并由全局变量dev_base指向链表的第一个元素:
使用网络设备前,内核必须为该设备创建一个struct net_device 结构体,并注册该设备。为此,内核将先后调用到alloc_netdev、register_netdev等内核函数。关于设备的注册,内核有两种方式:第一,把网络设备驱动程序模块编译进内核,系统启动时自动注册该设备;第二,把网络设备驱动程序编译成可动态加载模块,加载设备时注册该设备。
当不需要网络设备时,需要调用unregister_netdev函数注销该设备。该函数调用unregister_netdevice 执行具体注销操作,如果网络设备仍然处于活动状态,则关闭该设备并从 dev_base 列表中删除。
网络设备的开启与关闭
函数 devopen用来打开网络设备。如果网络设备已经激活或者它还没有被注册,那么函数返回错误信息。该函数调用设备打开函数进行具体的设置工作,然后把NETDEV_UP事件登记到通知链 notifier chain中。
函数 dev_close用来关闭网络设备。如果网络设备并未激活,则不需要关闭。该函数调用设备停止函数进行具体的设置工作,然后把对应事件登记到通知链notifier chain中。
通知链
在执行与网络设备管理有关的操作时,内核会向一个称为通知链的结构登记设备操作过程中发生的事件,比如打开设备时登记NETDEV_UP事件,关闭设备时登记NETDEV_DOWN事件。该结构是一个链表,通过 struct notifier_block指针 netdey_chain 管理。脊记事件对应着链表中的一个元素,而每个元素记录了一个函数。当某事件发生时,会触发其元素记录的函数。由此可见,通知链能根据不同事件执行不同的处理。
- 通知链注册与注销:调用函数 notifer_chain_register 可向通知链中登记一个事件。当事件发生时,可从链表中找到该事件对应的元素,执行记录的函数。调用函数 notifier_chain_unregister 可向通知链中注销一个事件。
- 通知链调用:当有事件发生时,就使用notifier_call_chain向某个通知链表发送消息,按顺序调用链表元素中记录的函数
PS:因为内容实在太多,我只能选些框架思路出来,细节需要自己去debug调试。