当前位置: 首页 > article >正文

Linux网络:基于文件的网络架构

Linux网络:基于文件的网络架构

    • 网络架构
    • TCP全连接队列


网络架构

Linux中提供了多种系统调用,完成网络操作。比如TCP连接的建立,各种报文的收发等等。但是所有的Linux网络操作,都源于系统调用socket

在这里插入图片描述

Linuxman手册中,对这个系统调用做了描述,表示返回值是一个file descriptor文件描述符。后续建立连接、监听连接、收发报文都要基于这个返回值,也就是说:Linux中网络被当作一个文件处理,这符合Linux的一切皆文件理念。

接下来以TCP为例,讲解Linux中的网络架构,源码版本为Linux 2.6.26

  • struct tcp_sock

TCP由结构体struct tcp_sock管理。

struct tcp_sock {
	struct inet_connection_sock	inet_conn;
	u16	tcp_header_len;
	u16	xmit_size_goal;
	// ...
};

这是TCP最底层的结构体,每创建一个TCP连接,底层都会维护一个这样的结构体。它记录了TCP的部分信息。

tcp_header_len这个成员记录了 TCP 头部的长度,单位是字节。 TCP 头部长度是可变的,因为它可以包含多个 TCP 选项。这个值用于在发送数据时确定需要预留多少空间给 TCP 头部。

xmit_size_goal这个成员定义了输出数据包的目标大小。它是发送数据时的一个目标值,用于分段输出数据。这个值可以帮助 TCP 层决定每个数据包应该发送多少数据。

其中最重要的是inet_conn,它的类型是struct inet_connection_sock

  • struct inet_connection_sock
struct inet_connection_sock {
	/* inet_sock has to be the first member! */
	struct inet_sock	  icsk_inet;
	struct request_sock_queue icsk_accept_queue;
	//...
};

在这个结构体中,包含了各种连接的相关信息,比如拥塞控制算法的相关参数,报文的重传次数,退避算法的退避时长等等。

其中icsk_accept_queue称为全连接队列,这个会在后文专门讲解。

其第一个成员icsk_inet也是一个结构体,类型为struct inet_sock

  • struct inet_sock
struct inet_sock {
	/* sk and pinet6 has to be the first two members of inet_sock */
	struct sock		sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	struct ipv6_pinfo	*pinet6;
#endif
	/* Socket demultiplex comparisons on incoming packets. */
	__be32			daddr;
	__be32			rcv_saddr;
	__be16			dport;
	__u16			num;
	__be32			saddr;
	//...
}

该结构体是 Linux 内核中处理基于 IP 套接字的通用结构体,它包含了 IP 层的套接字信息。

其中#if#endif是一个条件编译信息,如果使用IPv6协议,就会启用内部的指针pinet6,指向一个管理IPv6的结构体。

其还存储了一些IP协议的相关信息,比如daddr是目标地址,dport是目标端口,saddr是源地址等等。

这个结构体的第一个成员sk,类型为struct sock

  • struct sock
struct sock {
	/*
	 * Now struct inet_timewait_sock also uses sock_common, so please just
	 * don't add nothing before this first member (__sk_common) --acme
	 */
	struct sock_common	__sk_common;

	//...
	struct sk_buff_head	sk_receive_queue;
	struct sk_buff_head	sk_write_queue;
	//...
}

struct sock 结构体是所有类型套接字的通用结构体,包括 TCPUDP 和原始套接字等。

其中sk_receive_queue指向接收缓冲区,用于存储已经接收到,但是未被用户读取走的数据。sk_write_queue指向发送缓冲区,也就是用户交给操作系统,但是尚未被发送到网络的数据。

至此,已经大致打通了一个套接字管理的四个结构体:

在这里插入图片描述

自顶向下为:

  1. struct sock:套接字的通用结构体
  2. struct inet_sock:管理IP层的相关信息
  3. struct inet_connection_sock:管理网络连接
  4. struct tcp_sock:管理TCP连接

但是目前好像还没有看到和文件相关的内容?

其实网络与文件的衔接,是由struct socket完成的。

struct socket {
	socket_state		state;
	unsigned long		flags;
	const struct proto_ops	*ops;
	struct fasync_struct	*fasync_list;
	struct file		*file;
	struct sock		*sk;
	wait_queue_head_t	wait;
	short			type;
};

Linux 中,struct socket 结构体是用户空间与内核空间之间的接口,用于实现网络通信。

例如第一个成员state表示套接字的状态,比如已连接、未连接、正在连接等等。ops是一个结构体指针,指向的结构体是一个方法集,内部包含套接字的函数指针。

struct proto_ops {
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags);
	int		(*listen)    (struct socket *sock, int len);
	int		(*setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int optlen);
	int		(*sendmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len);
	int		(*recvmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len,
				      int flags);
	// ...
};

在这个opt内部,可以看到很多系统调用的身影,比如listenbindsendmsgrecvmsg等等,TCP中收发信息使用的sendtorecvfrom系统调用,就是通过这些指针来实现的。

当使用套接字收发消息的时候,就会通过传入的文件描述符找到struct socket,进而找到opt,在opt内部查找收发消息的函数,完成数据传递。

那么为什么不能直接把这些函数写在struct sock内部,而是要使用一个struct proto_ops

这个struct sock既可以管理TCP也可以管理UDP,这两个协议所使用的函数当然不同。以bind为例,TCPstruct sock调用bind时,调用的其实是sock->opt->bind()UDP同理也去调用sock->opt->bind()。只要定义套接字时,opt这个指针指向不同的struct proto_ops,那么TCPUDP就会调用到不同的函数。

可以发现,这其实是C语言的一种多态实现方式,不同对象调用相同的方法,结果不一样。

struct socket还有一个sk成员,其类型为struct sock*,也就是指向之前四个层次的顶层。

在这里插入图片描述

struct socket中,还有一个和网络看似毫不相关的成员file,其类型为struct file*。这是Linux内核这中,描述一个文件的结构。也就是说struct socket对上将网络操作转化为文件操作,对下管理网络信息,并提供网络通信所需的函数

struct file中,有一个private_data成员:

struct file {
	//...
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;
	
	//...
};

其类型为void*,经过条件编译产生。如果一个文件指向网络操作,那么这个private_data就会启用,并指向struct socket

至此,就可以看清Linux的网络架构全貌了:

在这里插入图片描述

当用户调用接口创建套接字,会得到一个文件描述符sockfd,它指向一个文件。当通过sockfd操作网络时,会去查找该描述符对应的文件,在文件内部通过private_data找到struct socket,这里面包含各种网络操作的具体函数。再往下可以通过sk找到管理一个连接的struct sock,这里面包含这个连接的各种信息。

有了操作网络的方法和一个网络连接的相关信息,那么操作网络就可以实现了。

不过从用户的角度出发,用户看不到底层的网络信息,也无法直接接触socket->opt。也就是说Linux把网络完全隐藏起来了,对用户只表现为一个文件描述符sockfd以及文件操作。


TCP全连接队列

最后简单讲解一下TCP的全连接队列,全连接队列就是inte_connection_sock下的icsk_accept_queue。它完全由Linux内核管理,按理来说用户是无需关心这个内容的,但是在TCPlisten函数中,第二个参数backlog与其相关,所以再此要提一下。

listen函数声明:

int listen(int sockfd, int backlog);

这个函数用于让一个套接字开始进行TCP连接的监听,第一个参数传入文件描述符sockfd,第二个参数是一个int,这其实和全连接队列相关。

TCP连接的建立需要经过三次握手,在三次握手的过程中,双方主机要记录当前连接创建到哪一步了,为此Linux使用了全连接队列半连接队列来管理TCP连接。

在这里插入图片描述

以服务端为例,在三次握手过程中,server共有四种状态:等待连接、收到SYN、收到ACK、用户accept该连接。

其中accept由用户完成,剩下的由操作系统自动完成。一个套接字中很可能同时维护了多个TCP连接,不同连接又可能处于不同的状态,为此套接字使用了两个队列来维护这些状态。

在这里插入图片描述

当一个TCP连接收到SYN报文后,进入半连接队列,即这个连接创建了一半。当收到ACK报文后,说明连接创建完毕,此时离开半连接队列,进入全连接队列。

也就是说,全连接队列中存储的是已经完成三次握手,但是还没有被用户accept的数据

在一个套接字中,用户能同时处理的TCP连接是有限的,那么超出能力范围的TCP连接,就会暂存在全连接队列中,当用户处理完一个连接,再去全连接队列读取出一个连接。

假设服务器同时接收到大量的网络请求,整个操作系统就要创建很多TCP连接,进行很多次握手,这让本就繁忙的CPU雪上加霜。因此操作系统不能允许大量的TCP连接同时建立,就算建立好了连接,用户也不一定会读取这个连接。

因此全连接队列与半连接队列是有长度限制的,如果队列满了之后,操作系统收到新的TCP连接,操作系统会直接丢弃这个连接请求,或者返回一个RST报文,表示拒绝这个请求。

listen第二个参数backlog用于指定全连接队列的长度,但也不完全取决于这个参数。操作系统也会给出一个全连接队列的最大长度somaxconn,其在路径/proc/sys/net/core/somaxconn下。

在这里插入图片描述

最终的全连接队列长度会取min(backlog, somaxconn)

全连接队列内部的连接,需要通过accept函数接收,而accept函数的返回值是一个sockfd,这其实说明了Linux中网络系统转化到文件系统的时机。

TCP三次握手过程中,会预先创建struct sock,存储一些网络的相关信息,但是这个时候并不会创建struct file,因为Linux操作网络无需通过文件。

当用户accept一个连接的时候,Linux得知用户需要操作这个连接,那么就会为这个连接创建struct file,并完成文件系统到网络系统的映射关系,把文件描述符sockfd返回给用户。

也就是说,从网络系统到文件系统的映射,是在用户accept的时候完成的。



http://www.kler.cn/a/400386.html

相关文章:

  • 网络安全之国际主流网络安全架构模型
  • 网络协议之UDP
  • Tailscale 自建 Derp 中转服务器
  • 【视觉SLAM】2-三维空间刚体运动的数学表示
  • Area-Composition模型部署指南
  • vs2022搭建opencv开发环境
  • Object.defineProperty和响应式
  • 学习笔记025——Git基本基本命令
  • tcp 超时计时器
  • NLP论文速读(多伦多大学)|利用人类偏好校准来调整机器翻译的元指标
  • 华为OD机试-日志采集 E100
  • 线程(二)【线程控制】
  • YOLOv7-0.1部分代码阅读笔记-test.py
  • 使用Python编写一个简单的网站爬虫,从网站上抓取新闻标题和链接。
  • Bufferevent and SSL
  • 利用 `OpenCV` 和 `Matplotlib` 库进行图像读取、颜色空间转换、掩膜创建、颜色替换
  • Gin HTML 模板渲染
  • FPGA 第7讲 简单组合逻辑译码器
  • 案例精选 | 某知名教育集团基于安全运营平台的全域威胁溯源实践
  • 解决Ubuntu18.04及以上版本高分辨率下导致字体过小问题
  • linux开机不显示转到window
  • 鸿蒙中位置权限和相机权限
  • 远程jupyter lab的配置
  • H.265流媒体播放器EasyPlayer.js H.264/H.265播放器chrome无法访问更私有的地址是什么原因
  • ubuntu24.04设置开机自启动Eureka
  • VSCode 常用的快捷键