目录
网络协议栈的部分底层实现
文件到网络的过渡
我们从进程pcb开始梳理
- pcb里的files字段存放了对应进程所有打开的文件信息 -> files_struct里的fd_array字段就是我们熟悉的文件描述符表 -> 文件结构体struct file
- 它里面有一个重要的字段 -- private_data
private_data
是一个通用指针,用于存储与文件相关的特定私有数据,这个字段在文件打开、读写、关闭等操作中被广泛使用
- 使得不同类型的文件操作能够共享同一个struct file结构,而无需在每次操作时重新分配和初始化特定数据结构
socket结构体
这里的private_data字段就指向struct socket结构体,而该结构体内部有一file指针,指回了file结构体
- 这样在文件操作过程中,如果使用了socket对象内的数据,可以在使用完后再返回文件操作
除了这个,socket里面还有一个等待队列(struct socket_wq),由wq字段指向该队列
- 如果当前所需的网络资源没有就绪,就将当前进程挂接到等待队列中(队头后面跟着若干等待队列项),等待资源就绪
以及由ops字段指向的操作方法集struct proto_ops
- 都是和连接相关的接口+udp协议收发接口(sendmsg,recvmsg)
当我们打开网络文件时
- 其实是将套接字挂接在了struct file下
- 这样在上层看来,通过fd就可以操作网络套接字了
从进程pcb开始,一直到socket结构体,实现了从文件到网络的过渡
- 而从socket结构体中的sock结构开始,才是网络的开始,它会指向具体协议的结构体
socket 和 file 中的方法集
如果我们打开的是网络文件
- 那么file中的f_op字段指向的就不再是磁盘相关的操作方法了,而是与网络相关
- 定义了文件的基本操作(打开,读写,关闭等),适用于所有文件类型
但是,socket中也有网络文件操作方法集opts
- 它里面包含的是专门用于套接字操作/处理网络相关的操作+udp收发
- 注意!没有tcp的send和recv
- 因为tcp报文需要进一步发送到sock中的sk_write(发送缓冲区)中
- 而udp可以直接使用socket中的sendmsg将数据发送到下一层,而不链入队列
这两者有什么区别呢?
- 其实,file,socket,sock等等,每一层都有自己的方法集,用于将数据处理后交给下层,所以他们是上下层关系
网络的开始
我们使用socket()创建套接字时,需要传入参数来表明自己是哪个类型的套接字
- 比如:SOCK_STREAM SOCK_...
- 而在底层,则会根据参数为我们创建对应的结构体 -- struct tcp_socket / udp_socket等
这些协议结构体的第一个成员就是struct sock
- 所以,实际上struct socket结构体中的sock类型指针sk,指向的是tcp/udp_socket类型中的sock结构体
- 所以,实际上sock结构体是一个通用的网络套接字抽象
传输层和网络层,都使用了特定数据结构来表示协议
- 也都定义了与特定协议匹配的方法集
如何区分sk字段指向的类型
那如何识别究竟指向的是什么类型呢?
- socket结构体中包含一些字段,比如type啦,flag啦,它们可以帮助我们区分sock类型指针指向的究竟是哪个协议的结构体
假如以tcp_socket为例
- sk字段指向的仅仅是sock类型
- 如果想要访问tcp协议的其他属性,只需对sk强转即可:
这就相当于在c语言中实现了多态
管理报文(sk_buff结构体)
计算机会收到大量的报文,不同协议类型的/不同服务对应的,它们可能会挤压在计算机里的不同层
- 这么多的报文,自然也就需要管理起来
- 也就是系统定义的sk_buff结构体
可以看到,以sock结构体中的发送缓冲区为例
- 它是以双链表结构将报文组织起来
- 并且配有自旋锁,因为发送缓冲区可以被多个进程访问
其中,sk_buf结构体中定义了多个字段,指向当前报文的不同位置:
- head指向报头字段的开始,end是整个报文的结束
- data指向有效载荷的开始,tail是有效载荷的结束
- 所以,可以看出来,sk_buff和报文实际存放位置是分开的,通过指针来找到数据 -- 对报文的修改都是在同一块区域做的,所以传递报文时不需要拷贝
因为sk_buf里有上面这四个指针决定了各部分数据的范围
- 所以,封装报头/解包就是在移动head指针
- 而如何移动,由所在层的协议决定
实际在代码中,上下传递报文就是在传递sk_buff
在网络层时,报文作为一个整体被处理 ; 到了传输层,需要根据对应协议将报文分发给各自的接收缓冲区,所谓分发,就是将sk_buf组织到双链表中
如果要从上层读取报文,本质上就是通过一大堆的数据结构,到sock结构中的双链表读取sk_buf,最后拿到上层
以上操作均由os完成
总结
通过上面的梳理,我们可以真切理解到,为什么说建立/维护连接是有成本的
- 每新增一个连接,就要分配一个文件描述符,并创建其之后串联的一大堆数据结构,并更新一些结构中的连接关系