操作系统看网络发包过程

Java语言发送网络包代码

发送网络包,client发送给server,或者server发送给client流程都是一样的。

image.png

第一步:创建套接字

客户端通过new Socket()创建,服务单通过new ServerSocket()、accept()获取。

第二部:获取输出流

要想发送数据需要先获取套接字的输出流(OutputStream),这是Java语言定义的规范,实际上其他语言发送数据根本不需要这一步,比如C,直接socket.write就发送了。

第三步:发送数据

使用输出流的write方法进行数据发送。

创建的socket存放在哪?

要了解这个过程,操作系统是必不可少的前提。

Linux中一切接文件,磁盘、网络均是以文件形式存在。当新建一个socket会在创建套接字进程元信息中存放socket相关的文件信息。

首先,操作系统中启动一个进程时,内核地址空间就会有一块内存存放该进程的元数据信息,如进程的pid、进程打开的文件列表等信息。这里的文件不仅包含磁盘文件、还包含网络文件(即socket)。

我们先来思考一下,socket文件信息存放了哪些东西:

  1. 等待队列。在调用write后,会立即就直接发送吗?学过计算机网络后,如果发送的数据大小很小,可能不会直接发送,而是存起来,和下次发送的数据一起发送,从而减少发送网络包的次数,提高效率。所以,我们需要一个队列来暂存数据。
  2. 哪种网络IO。该socket是aio?nio?bio?
  3. 哪种协议?tcp?udp?
  4. 接受队列。收到网络包的数据要干什么?当然是把网络包数据给应用程序啊。然而当网络包数量非常多的时候,应用程序会处理不过来,所以需要一个队列来存放接收到的数据,然后操作系统慢慢处理。

下面就是需要一个数据结构来存放上面的信息了。

image.png

这个地方我们只需要关注socket信息中的两个队列即可。队列中的元素就是一个一个的ISO模型中的网络包。其结构长这个样子:

image.png

发送数据时,ISO模型从上网络逐层处理,首先是最开始的应用发送的数据data,随后经过传输层的tcp协议(或者是udp)处理(超时重传、拥塞控制),加上tcp头、再经过网络层ip协议处理(网络路由),加上ip头,再经过数据链路层处理,加上mac头信息。最后整个数据通过网卡转为01电信号从网线发送。整个数据通常称为skb(socket buffer)。

网络包发送过程

等待队列和接收队列是发送必不可少的流程之一,所以必须先提到。下面看一下整体流程。

1.语言转换

在Linux操作系统中提供套接字接口为C语言代码,所以任何语言使用套接字就必须转换到C来调用。而Java中write方法可以看到最后是调用到native执行。

说个题外话,直接看源码看到native虽然知道是调用到jvm中的C代码实现,但是如何调用过去的呢?不知道大家是如何理解这个跨语言调用。学了操作系统之后,我是这样理解:不论任何语言,最终的执行都是靠CPU执行,CPU也只能执行CPU认识的指令集,所以任何语言要执行,必须转换为指令集。而与指令集最为接近的是汇编。在这个基础上,JVM执行时候,将java方法和C方法都一起编译加载到内存中。当执行到native方法时候,直接一个jmp调到c代码执行的内存地址就ok。

2.系统调用

经过第一步C语言调用套接字发送数据的系统调用,随后陷入内核,由用户态转为内核态。

怎么理解为什么要从用户态转为内核态?这个需要从操作系统的需求说起,由于硬件资源有限,比如CPU运算速度有限、内存空间大小有限、磁盘存储空间有限、网卡速度有限。所以如果让一个程序能够直接与这些硬件交互就会直接独占这些硬件资源,无法满足多个任务同时运行的需求,一个人任务运行必须等待上一个占用该硬件资源的任务停止占用才行。所以操作系统设计出了用户态和内核态,用户程序运行在用户态,不能直接与硬件交互。操作系统运行在内核态,可以直接与硬件交互。如果用户程序需要调用硬件,就需要使用操作系统提供的一些系统调用的函数,如磁盘读写的函数、网络IO读写的函数、分配内存的函数等等。用户程序进行系统调用会从用户进程切换到内核进程执行系统调用。操作系统相当于一个中间层,用于管理硬件资源的分配。

在进行系统调用,会触发预先设定的操作系统的中断函数。随后保存进程的上下文变量(程序计数器等)保存到进程的内核栈中,将系统调用中的指针参数指向的内存,复制到内核空间中。


为什么系统调用要复制数据?内核直接通过指针去访问数据不是很方便么?主要是一些安全问题不能直接使用指针,抛开特殊情况不拷贝也是可以的,但Linux代码为了通用,就直接拷贝数据了。特殊情况:比如set_host_name(设置主机密码)的系统调用,通常会校验字符串长度、密码是否包含复杂字符等等校验。如果内核直接使用用户空间的指针,那么就会出现直接修改用户空间指针指向的内存数据从而直接修改主机名称。为了避免这种特殊情况,所以在陷入内核时候需要拷贝数据。当从内核恢复到进程执行时,也需要拷贝数据,这是因为内核数据占用的内存需要释放,所以系统调用的返回值需要从内核拷贝到用户空间,之后内核的数据占用的内存可以释放。

3.传输层协议处理

进行完系统调用后,执行权转交给了操作系统,首先将发送的数据封装为一个skb的数据结构。操作系统通过进程元数据信息获取发送数据的套接字信息,是TCP还是UDP。如果是TCP,则在skb结构中添加TCP头信息,然后将skb放入等待队列中。这个时候会判断是否可以发送,如果不能发送(比如数据太小)则直接返回。

如果是TCP,则还需要进行拥塞控制、超时重传。

4.网络层处理

给skb结构添加IP头信息。网络层主要是通过IP地址来路由到目标地址。

5.数据链路层处理

给skb结构添加MAC头信息。

6.发送数据

能够操控网卡的硬件是网卡对应的驱动程序,而驱动是从ring buffer获取skb数据进行发送的。所以下一步需要将ring buffer中的元素关联到等待队列skb中,随后通过DMA将ring buffer数据传输到网卡中,然后通过网卡将数据包发送出去。

7.清理数据

当发送完成后还没有结束,因为对于TCP来说还可能有重传处理。并且还需要清理ring buffer等数据,释放内存等处理。所以在发送完成后还要触发一次中断,进行处理。如果传输层使用TCP协议,则在TCP协议处理时候还会将skb再复制一次,因为如果释放skb数据就没有备份数据进行重传。

总结

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享