《趣谈Linux》总结八:网络系统

在这里插入图片描述

33 Socket通信

无论是用socket操作TCP,还是UDP,首先都要调用socket函数,socket函数用于创建一个socket的文件描述符,唯一标识一个socket;把它叫作文件描述符,是因为在内核中会创建类似文件系统的数据结构,并且后续的操作都有用到它:

int socket(int domain, int type, int protocol);

三个参数的含义:

domain:表示使用什么IP层协议。AF_INET表示IPv4,AF_INET6表示IPv6。

type:表示socket类型。SOCK_STREAM是TCP面向流的,SOCK_DGRAM是UDP面向数据报的,SOCK_RAW可以直接操作IP层,或者非TCP和UDP的协议。例如ICMP。

protocol表示的协议,包括IPPROTO_TCP、IPPTOTO_UDP。

TCP编程模式:
在这里插入图片描述
其总体状态:用户态加内核态如图
在这里插入图片描述
UDP编程模式:
在这里插入图片描述

34 Socket内核数据结构

在这里插入图片描述
首先,Socket系统调用会有三级参数family、type、protocal,通过这三级参数,分别在net_proto_family表
中找到type链表,在type链表中找到protocal对应的操作。

这个操作分为两层,对于TCP协议来讲,第一层是inet_stream_ops层,第二层是tcp_prot层。
于是,接下来的系统调用规律就都一样了:

bind第一层调用inet_stream_ops的inet_bind函数,第二层调用tcp_prot的inet_csk_get_port函数;

listen第一层调用inet_stream_ops的inet_listen函数,第二层调用tcp_prot的inet_csk_get_port函数;

accept第一层调用inet_stream_ops的inet_accept函数,第二层调用tcp_prot的inet_csk_accept函数;

connect第一层调用inet_stream_ops的inet_stream_connect函数,第二层调用tcp_prot的tcp_v4_connect函数。

在内核中,为每个Socket维护两个队列:
一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态;
一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于syn_rcvd的状态。

三次连接体现在connect函数中:
在这里插入图片描述

35 发送网络包

35.1 (上)ip层以上是如何发送数据的

socket对于用户来讲,是一个文件一样的存在,拥有一个文件描述符。
因而对于网络包的发送,可以使用对于socket文件的写入系统调用,也就是write系统调用。

根据tcp_prot的定义,tcp的write调用的是tcp_sendmsg。

为了减少内存拷贝的代价,有的网络设备支持分散聚合(Scatter/Gather)I/O:
IP层没必要通过内存拷贝进行聚合,直接让散的数据零散的放在原处,在设备层进行聚合。
如果使用这种模式,网络包的数据就不会放在连续的数据区域,而是放在struct skb_shared_info结构里面指向的离散数据,skb_shared_info的成员变量skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了:
在这里插入图片描述

  • 总结
    在这里插入图片描述
    到ip层,这个过程分成几个层次:

VFS层:write系统调用找到struct file,根据里面的file_operations的定义,调用sock_write_iter函数。
sock_write_iter函数调用sock_sendmsg函数。

Socket层:从struct file里面的private_data得到struct socket,根据里面ops的定义,调用inet_sendmsg函数。

Sock层:从struct socket里面的sk得到struct sock,根据里面sk_prot的定义,调用tcp_sendmsg函数。

TCP层:tcp_sendmsg函数会调用tcp_write_xmit函数,tcp_write_xmit函数会调用tcp_transmit_skb,在这里实现了TCP层面向连接的逻辑。

IP层:扩展struct sock,得到struct inet_connection_sock,根据里面icsk_af_ops的定义,调用ip_queue_xmit函数。

35.2 (下)总过程

总过程:
在这里插入图片描述
VFS层:write系统调用找到struct file,根据里面的file_operations的定义,调用sock_write_iter函数。
sock_write_iter函数调用sock_sendmsg函数。

Socket层:从struct file里面的private_data得到struct socket,根据里面ops的定义,调用inet_sendmsg
函数。

Sock层:从struct socket里面的sk得到struct sock,根据里面sk_prot的定义,调用tcp_sendmsg函数。

TCP层:tcp_sendmsg函数会调用tcp_write_xmit函数,tcp_write_xmit函数会调用tcp_transmit_skb,在这
里实现了TCP层面向连接的逻辑。

IP层:扩展struct sock,得到struct inet_connection_sock,根据里面icsk_af_ops的定义,调用
ip_queue_xmit函数。

IP层:ip_route_output_ports函数里面会调用fib_lookup查找路由表。FIB全称是Forwarding Information
Base,转发信息表,也就是路由表。
还要填写IP层的头、通过iptables规则。

MAC层:IP层调用ip_finish_output进行MAC层。
MAC层需要ARP获得MAC地址,因而要调用___neigh_lookup_noref查找属于同一个网段的邻居,他会调用
neigh_probe发送ARP。
有了MAC地址,就可以调用dev_queue_xmit发送二层网络包了,它会调用__dev_xmit_skb会将请求放入
队列。

设备层:网络包的发送回触发一个软中断NET_TX_SOFTIRQ来处理队列中的数据。这个软中断的处理函数
是net_tx_action。
在软中断处理函数中,会将网络包从队列上拿下来,调用网络设备的传输函数ixgb_xmit_frame,将网络
包发的设备的队列上去。

36 接收网络包

网卡作为一个硬件,接收到网络包,应该怎么通知操作系统,这个网络包到达了呢?
可以触发一个中断。
但是这里有个问题,就是网络包的到来,往往是很难预期的。网络吞吐量比较大的时候,网络包的到达会十分频繁。这个时候,如果非常频繁地去触发中断,是个很大的灾难。
比如说,CPU正在做某个事情,一些网络包来了,触发了中断,CPU停下手里的事情,去处理这些网络包,处理完毕按照中断处理的逻辑,应该回去继续处理其他事情。
这个时候,另一些网络包又来了,又触发了中断,CPU手里的事情又要停下来,去处理网络包。
能不能要来的一起来,把网络包好好处理一把,然后再回去集中处理其他事情呢?
“网络包能不能一起来”,这个没法控制,但是可以有一种集中时间段的机制:
就是当一些网络包到来触发了中断,内核处理完这些网络包之后,我们可以先进入主动轮询poll网卡的方式,主动去接收到来的网络包,如果一直有,就一直处理,等处理告一段落,就返回干其他的事情。
当再有下一批网络包到来的时候,再中断,再轮询poll。这样就会大大减少中断的数量,提升网络处理的效率,这种处理方式称为NAPI

  • 内核接收网络包的过程

硬件网卡接收到网络包之后,通过DMA技术,将网络包放入Ring Buffer;

硬件网卡通过中断通知CPU新的网络包的到来;

网卡驱动程序会注册中断处理函数ixgb_intr;

中断处理函数处理完需要暂时屏蔽中断的核心流程之后,通过软中断NET_RX_SOFTIRQ触发接下来的处理
过程;

NET_RX_SOFTIRQ软中断处理函数net_rx_action,net_rx_action会调用napi_poll,进而调用
ixgb_clean_rx_irq,从Ring Buffer中读取数据到内核struct sk_buff

调用netif_receive_skb进入内核网络协议栈,进行一些关于VLAN的二层逻辑处理后,调用ip_rcv进入三层
IP层;

在IP层,会处理iptables规则,然后调用ip_local_deliver交给更上层TCP层;

在TCP层调用tcp_v4_rcv,这里面有三个队列需要处理:
如果当前的Socket不是正在被用户态进程读取,则放入backlog队列;
如果正在被读取,不需要很实时的话,则放入prequeue队列;
其他情况调用tcp_v4_do_rcv;

在tcp_v4_do_rcv中:
如果是处于TCP_ESTABLISHED状态,调用tcp_rcv_established;
其他的状态,调用tcp_rcv_state_process;

在tcp_rcv_established中,调用tcp_data_queue,如果序列号能够接的上,则放入sk_receive_queue队列;
如果序列号接不上,则暂时放入out_of_order_queue队列,等序列号能够接上的时候,再放入sk_receive_queue队列。

  • 用户态读取网络包的过程

VFS层:read系统调用找到struct file,根据里面的file_operations的定义,调用sock_read_iter函数。
sock_read_iter函数调用sock_recvmsg函数。

Socket层:从struct file里面的private_data得到struct socket,根据里面ops的定义,调用inet_recvmsg函
数。

Sock层:从struct socket里面的sk得到struct sock,根据里面sk_prot的定义,调用tcp_recvmsg函数。

TCP层:tcp_recvmsg函数会依次读取receive_queue队列、prequeue队列和backlog队列。
在这里插入图片描述
用户态和内核态核心就是通过三个队列来交互数据

发布了235 篇原创文章 · 获赞 264 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/103156181