硬件层面接收到报文之后,做一系列的初始化 *** 作,之后驱动才开始把一个封包封装为skb。
当然这是在x86架构下,如果是在cavium架构下,封包是wqe形式存在。
不管是skb还是wqe,都仅仅是一种手段,一种达到完成报文传输所采用的一种解决方案,一种方法而已。
或许处理方案的具体实现细节差别万千,但是基本的原理,都是殊途同归,万变不离其宗。
skb的产生,让Linux协议栈旅程的开启,具备了最基本的条件,接下来的协议栈之旅,才会更加精彩。
写作本文的原因是现在本机网络 IO 应用非常广。
在 php 中 一般 nginx 和 php-fpm 是通过 127.0.0.1 来进行通信的;
在微服务中,由于 side car 模式的应用,本机网络请求更是越来越多。
所以,如果能深度理解这个问题在各种网络通信应用的技术实践中将非常的有意义。
今天咱们就把 127.0.0.1 本机网络通信相关问题搞搞清楚!
为了方便讨论,我把这个问题拆分成3问:
1)127.0.0.1 本机网络 IO 需要经过网卡吗?
2)和外网网络通信相比,在内核收发流程上有啥差别?
3)使用 127.0.0.1 能比 192.168.x.x 更快吗?
在上面这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。
当数据包到达另外一台机器的时候,Linux 数据包的接收过程开始了。
当网卡收到数据以后,CPU发起一个中断,以通知 CPU 有数据到达。
当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。
ksoftirqd 检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。
当协议栈处理完并把数据放到接收队列的之后,唤醒用户进程(假设是阻塞方式)。
关于跨机网络通信的理解,可以通俗地用下面这张图来总结一下:
前面,我们看到了跨机时整个网络数据的发送过程 。
在本机网络 IO 的过程中,流程会有一些差别。
为了突出重点,本节将不再介绍整体流程,而是只介绍和跨机逻辑不同的地方。
有差异的地方总共有两个,分别是路由和驱动程序。
对于本机网络 IO 来说,特殊之处在于在 local 路由表中就能找到路由项,对应的设备都将使用 loopback 网卡,也就是我们常见的 lO。
从上述结果可以看出,对于目的是 127.0.0.1 的路由在 local 路由表中就能够找到了。
对于是本机的网络请求,设备将全部都使用 lo 虚拟网卡,接下来的网络层仍然和跨机网络 IO 一样。
本机网络 IO 需要进行 IP 分片吗?
因为和正常的网络层处理过程一样,如果 skb 大于 MTU 的话,仍然会进行分片。
只不过 lo 的 MTU 比 Ethernet 要大很多。
通过 ifconfig 命令就可以查到,普通网卡一般为 1500,而 lO 虚拟接口能有 65535。
为什么我把“驱动”加个引号呢,因为 loopback 是一个纯软件性质的虚拟接口,并没有真正意义上的驱动。
在邻居子系统函数中经过处理,进入到网络设备子系统,只有触发完软中断,发送过程就算是完成了。
在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。
而在本机的网络 IO 过程中,由于并不真的过网卡,所以网卡实际传输,硬中断就都省去了。直接从软中断开始,送进协议栈。
网络再往后依次是传输层,最后唤醒用户进程,这里就不多展开了。
我们来总结一下本机网络通信的内核执行流程:
回想下跨机网络 IO 的流程:
通过本文的叙述,我们确定地得出结论,不需要经过网卡。即使了把网卡拔了本机网络是否还可以正常使用的。
总的来说,本机网络 IO 和跨机 IO 比较起来,确实是节约了一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。
但是在内核其它组件上可是一点都没少:系统调用、协议栈(传输层、网络层等)、网络设备子系统、邻居子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东东)。所以即使是本机网络 IO,也别误以为没啥开销。
先说结论:我认为这两种使用方法在性能上没有啥差别。
我觉得有相当大一部分人都会认为访问本机server 的话,用 127.0.0.1 更快。原因是直觉上认为访问 IP 就会经过网卡。
其实内核知道本机上所有的 IP,只要发现目的地址是本机 IP 就可以全走 loopback 回环设备了。
本机其它 IP 和 127.0.0.1 一样,也是不用过物理网卡的,所以访问它们性能开销基本一样!
How SKBs work - Linux kernel
http://vger.kernel.org/~davem/skb.html
一篇解读Linux网络协议栈
https://zhuanlan.zhihu.com/p/475319464
你真的了解127.0.0.1和0.0.0.0的区别?
http://www.52im.net/thread-2928-1-1.html
深入 *** 作系统,彻底搞懂127.0.0.1本机网络通信
http://www.52im.net/thread-3590-1-1.html
/*************************************文件名: server.c
linux 下socket网络编程简例 - 服务端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
作者:kikilizhm#163.com (将#换为@)
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sfp,nfp /* 定义两个描述符 */
struct sockaddr_in s_add,c_add
int sin_size
unsigned short portnum=0x8888 /* 服务端使用端口 */
printf("Hello,welcome to my server !\r\n")
sfp = socket(AF_INET, SOCK_STREAM, 0)
if(-1 == sfp)
{
printf("socket fail ! \r\n")
return -1
}
printf("socket ok !\r\n")
/* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */
bzero(&s_add,sizeof(struct sockaddr_in))
s_add.sin_family=AF_INET
s_add.sin_addr.s_addr=htonl(INADDR_ANY) /* 这里地址使用全0,即所有 */
s_add.sin_port=htons(portnum)
/* 使用bind进行绑定端口 */
if(-1 == bind(sfp,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("bind fail !\r\n")
return -1
}
printf("bind ok !\r\n")
/* 开始监听相应的端口 */
if(-1 == listen(sfp,5))
{
printf("listen fail !\r\n")
return -1
}
printf("listen ok\r\n")
while(1)
{
sin_size = sizeof(struct sockaddr_in)
/* accept服务端使用函数,调用时即进入阻塞状态,等待用户进行连接,在没有客户端进行连接时,程序停止在此处,
不会看到后面的打印,当有客户端进行连接时,程序马上执行一次,然后再次循环到此处继续等待。
此处accept的第二个参数用于获取客户端的端口和地址信息。
*/
nfp = accept(sfp, (struct sockaddr *)(&c_add), &sin_size)
if(-1 == nfp)
{
printf("accept fail !\r\n")
return -1
}
printf("accept ok!\r\nServer start get connect from %#x : %#x\r\n",ntohl(c_add.sin_addr.s_addr),ntohs(c_add.sin_port))
/* 这里使用write向客户端发送信息,也可以尝试使用其他函数实现 */
if(-1 == write(nfp,"hello,welcome to my server \r\n",32))
{
printf("write fail!\r\n")
return -1
}
printf("write ok!\r\n")
close(nfp)
}
close(sfp)
return 0
} /*************************************
文件名: client.c
linux 下socket网络编程简例 - 客户端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
作者:kikilizhm#163.com (将#换为@)
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int cfd /* 文件描述符 */
int recbytes
int sin_size
char buffer[1024]={0} /* 接受缓冲区 */
struct sockaddr_in s_add,c_add /* 存储服务端和本端的ip、端口等信息结构体 */
unsigned short portnum=0x8888 /* 服务端使用的通信端口,可以更改,需和服务端相同 */
printf("Hello,welcome to client !\r\n")
/* 建立socket 使用因特网,TCP流传输 */
cfd = socket(AF_INET, SOCK_STREAM, 0)
if(-1 == cfd)
{
printf("socket fail ! \r\n")
return -1
}
printf("socket ok !\r\n")
/* 构造服务器端的ip和端口信息,具体结构体可以查资料 */
bzero(&s_add,sizeof(struct sockaddr_in))
s_add.sin_family=AF_INET
s_add.sin_addr.s_addr= inet_addr("192.168.1.104") /* ip转换为4字节整形,使用时需要根据服务端ip进行更改 */
s_add.sin_port=htons(portnum) /* 这里htons是将short型数据字节序由主机型转换为网络型,其实就是
将2字节数据的前后两个字节倒换,和对应的ntohs效果、实质相同,只不过名字不同。htonl和ntohl是
*** 作的4字节整形。将0x12345678变为0x78563412,名字不同,内容两两相同,一般情况下网络为大端,
PPC的cpu为大端,x86的cpu为小端,arm的可以配置大小端,需要保证接收时字节序正确。
*/
printf("s_addr = %#x ,port : %#x\r\n",s_add.sin_addr.s_addr,s_add.sin_port) /* 这里打印出的是小端
和我们平时看到的是相反的。 */
/* 客户端连接服务器,参数依次为socket文件描述符,地址信息,地址结构大小 */
if(-1 == connect(cfd,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("connect fail !\r\n")
return -1
}
printf("connect ok !\r\n")
/*连接成功,从服务端接收字符*/
if(-1 == (recbytes = read(cfd,buffer,1024)))
{
printf("read data fail !\r\n")
return -1
}
printf("read ok\r\nREC:\r\n")
buffer[recbytes]='\0'
printf("%s\r\n",buffer)
getchar() /* 此句为使程序暂停在此处,可以使用netstat查看当前的连接 */
close(cfd) /* 关闭连接,本次通信完成 */
return 0
}
Netlink套接字是用以实现 用户进程 与 内核进程 通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。
一般来说用户空间和内核空间的通信方式有三种: /proc、ioctl、Netlink 。而前两种都是单向的,而Netlink可以实现双工通信。
Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言,具有以下优点:
Netlink协议基于BSD socket和 AF_NETLINK 地址簇,使用32位的端口号寻址,每个Netlink协议通常与一个或一组内核服务/组件相关联,如 NETLINK_ROUTE 用于获取和设置路由与链路信息、 NETLINK_KOBJECT_UEVENT 用于内核向用户空间的udev进程发送通知等。
用户态应用使用标准的 socket API有sendto(),recvfrom(), sendmsg(), recvmsg()。
Netlink通信跟常用UDP Socket通信类似, struct sockaddr_nl 是netlink通信地址,跟普通 socket struct sockaddr_in 类似。
netlink_kernel_create内核函数用于创建内核socket与用户态通信
首先将编译出来的Netlink内核模块插入到系统当中(insmod netlink_test.ko),然后运行应用程序,可以看到如下输出:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)