从linux5.9看网络层的设计

前言:
很久没有看内核的代码了,假期开始看一下,之前看了一下0.11和1.2.13的代码,虽然大致了解了一些原理,但是毕竟比较旧了,再者很多功能还没有实现,比如epoll,所以这次选取的是5.9的版本,再也不怕过时了,当然,现在内核的代码量级非常大,不可能看得完也不可能都看,只是选取自己感兴趣的一些点看一下。看内核代码,总的来说是非常有趣的,不仅是因为知其然知其所以然,而且看到朴素的c语言,还有世界级大佬写代码的思路、思想,甚至注释,都是非常有意思的事情。

今天分析的内容是从socket函数开始,看看linux网络层的设计。下面我们看一下我们平时写网络编程代码时的用法。

#include <sys/socket.h>
int fd = socket(...);
bind(fd, ...);
lisnten(fd);

我们看到网络编程中的一系列函数都是来自sys/socket.h这个头文件。这个是glibc提供的,glibc通过系统调用的方式使用操作系统提供的API。

网络层API的调用

在网络层设计中,内核并没有给每一个网络函数都提供一个系统调用,而是提供了一个统一的入口socketcall,也就是说,我们使用的网络函数,透过glibc,最后到达socketcall入口,然后进行分发,我们来看一下代码。

// call就是调用方使用的函数
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
	unsigned long a[AUDITSC_ARGS];
	unsigned long a0, a1;
	int err;
	unsigned int len;
	call = array_index_nospec(call, SYS_SENDMMSG + 1);
	len = nargs[call];
	if (copy_from_user(a, args, len))
		return -EFAULT;

	err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
	if (err)
		return err;

	a0 = a[0];
	a1 = a[1];

	switch (call) {
	case SYS_SOCKET:
		err = __sys_socket(a0, a1, a[2]);
		break;
	case SYS_BIND:
		err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
		break;
	// 忽略很多其他case
	default:
		err = -EINVAL;
		break;
	}
	return err;
}

我们看到socketcall的逻辑就是分发。下面我们以socket函数为例,继续分析网络层的设计。socket是linux网络编程中最重要的概念,socket又叫套接字,他是内核设计者对底层协议的抽象,然后提供给用户的入口,他类似工厂模式,当我们调用socket函数的时候,传入对应的参数,就可以得到不同类型的socket,对调用方来说简化了网络编程的难度,而在抽象的底层,socket包含了非常多复杂的逻辑,比如TCP、UDP、IP协议的实现。我们来看看面对复杂的网络协议,内核设计者是如何设计网络层的架构的。

网络层和文件系统的关系

我们知道Linux万物皆文件,socket也不例外,当调用socket函数的时候,我们拿到的不是socket本身,而是一个文件描述符fd。那么网络层是如何和文件系统关联起来的呢?这得益于Linux的VFS(虚拟文件系统),VFS为文件系统抽象了一套API,实现了该系列API就可以把对应的资源当作文件使用,我们来看看网络层中关于这部分的实现。我们知道文件系统有以下关系。

我们来看看inode和socket是如何关联起来的。以下是网络层中,关于超级块接口的实现

static const struct super_operations sockfs_ops = {
	.alloc_inode	= sock_alloc_inode,
	.free_inode	= sock_free_inode,
	.statfs		= simple_statfs,
};

当调用socket函数的时候就需要新建一个inode,然后就会调用sock_alloc_inode。

struct socket_alloc {
	struct socket socket;
	struct inode vfs_inode;
};
static struct inode *sock_alloc_inode(struct super_block *sb)
{
	struct socket_alloc *ei;

	ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
	// 忽略初始化字段的逻辑
	return &ei->vfs_inode;
}

我们看到sock_alloc_inode函数的逻辑非常简单,就是分配一个socket_alloc结构体,socket_alloc结构体中有两个字段,分别是socket和inode,inode是给文件系统使用的,socket是网络层使用的。所以有以下关系。

网络层的初始化

从socket函数的定义中我们看到有family和type两个参数,这两个属性都会对应不同的实现。我们先看看family的实现。

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;

net_families是个结构体数组,那么值是什么呢?net_families的值是通过sock_register设置的。

int sock_register(const struct net_proto_family *ops)
{
	int err;
	spin_lock(&net_family_lock);
	// 把ops加入到数组中
	rcu_assign_pointer(net_families[ops->family], ops);
	spin_unlock(&net_family_lock);
	return err;
}

那么哪里会调用sock_register呢?在网络层初始化的时候会调用inet_init,在inet_init中会调用sock_register。

static const struct net_proto_family inet_family_ops = {
	.family = PF_INET,
	.create = inet_create,
	.owner	= THIS_MODULE,
};
static int __init inet_init(void) {
	(void)sock_register(&inet_family_ops);
}

接着我们看看type的实现。

static struct inet_protosw inetsw_array[] =
{
	{
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		// 给sock结构体使用
		.prot =       &tcp_prot,
		// 给socket结构体使用
		.ops =        &inet_stream_ops,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},

	{
		.type =       SOCK_DGRAM,
		.protocol =   IPPROTO_UDP,
		.prot =       &udp_prot,
		.ops =        &inet_dgram_ops,
		.flags =      INET_PROTOSW_PERMANENT,
       },

       {
		.type =       SOCK_DGRAM,
		.protocol =   IPPROTO_ICMP,
		.prot =       &ping_prot,
		.ops =        &inet_sockraw_ops,
		.flags =      INET_PROTOSW_REUSE,
       },

       {
	       .type =       SOCK_RAW,
	       .protocol =   IPPROTO_IP,	/* wild card */
	       .prot =       &raw_prot,
	       .ops =        &inet_sockraw_ops,
	       .flags =      INET_PROTOSW_REUSE,
       }
};

在网络层初始化的时候会注册以上的定义。

	for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
		inet_register_protosw(q);

我们看一下inet_register_protosw

void inet_register_protosw(struct inet_protosw *p)
{
	struct list_head *lh;
	struct inet_protosw *answer;
	int protocol = p->protocol;
	struct list_head *last_perm;

	spin_lock_bh(&inetsw_lock);
	last_perm = &inetsw[p->type];
	list_for_each(lh, &inetsw[p->type]) {
		answer = list_entry(lh, struct inet_protosw, list);
		if ((INET_PROTOSW_PERMANENT & answer->flags) == 0)
			break;
		if (protocol == answer->protocol)
			goto out_permanent;
		last_perm = lh;
	}
	list_add_rcu(&p->list, last_perm);
}

inet_register_protosw主要是把inet_protosw结构体逐个加入到队列中,后面会用到。接下来我们就可以分析socket函数的实现了。

socket函数的实现

socket函数对应的实现是__sys_socket。

int __sys_socket(int family, int type, int protocol)
{
	int retval;
	struct socket *sock;
	int flags;
	/*
	  type这个字段中一部分数据是标记协议的类型的,比如流式类型、数据包类型,
	  另一部分数据是标记socket的特性的,比如非阻塞SOCK_NONBLOCK,这个是新
	  内核支持的,以前需要使用两个函数完成
	*/
	flags = type & ~SOCK_TYPE_MASK;
	if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
		return -EINVAL;
	type &= SOCK_TYPE_MASK;

	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
	// 
	retval = sock_create(family, type, protocol, &sock);
	if (retval < 0)
		return retval;
	// 获取一个fd
	return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

socket函数实现中,主要分为两个部分,分别是sock_create和sock_map_fd。sock_create顾名思义是用于创建一个socket结构体的。

int sock_create(int family, int type, int protocol, struct socket **res)
{
	return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

继续看__sock_create

int __sock_create(struct net *net, int family, int type, int protocol,
			 struct socket **res, int kern)
{
	int err;
	struct socket *sock;
	const struct net_proto_family *pf;
	// 申请一个socket结构体
	sock = sock_alloc();
	sock->type = type;
	rcu_read_lock();
	// 根据socket类型(协议簇类型)拿到对应的处理函数集
	pf = rcu_dereference(net_families[family]);
	pf->create(net, sock, protocol, kern);
	*res = sock;
	return 0;
}

__sock_create函数调用sock_alloc分配一个结构体

struct socket *sock_alloc(void)
{
	struct inode *inode;
	struct socket *sock;
	// 申请一个inode
	inode = new_inode_pseudo(sock_mnt->mnt_sb);
	// inode和socket在同一个结构体中,取出来
	sock = SOCKET_I(inode);
	// 设置inode号和属性
	inode->i_ino = get_next_ino();
	inode->i_mode = S_IFSOCK | S_IRWXUGO;
	inode->i_uid = current_fsuid();
	inode->i_gid = current_fsgid();
	inode->i_op = &sockfs_inode_ops;
	return sock;
}

struct inode *new_inode_pseudo(struct super_block *sb)
{
	struct inode *inode = alloc_inode(sb);

	if (inode) {
		spin_lock(&inode->i_lock);
		inode->i_state = 0;
		spin_unlock(&inode->i_lock);
		INIT_LIST_HEAD(&inode->i_sb_list);
	}
	return inode;
}

static struct inode *alloc_inode(struct super_block *sb)
{
	const struct super_operations *ops = sb->s_op;
	struct inode *inode;
	// alloc_inode指向sock_alloc_inode函数
	if (ops->alloc_inode)
		inode = ops->alloc_inode(sb);

	return inode;
}

static struct inode *sock_alloc_inode(struct super_block *sb)
{
	struct socket_alloc *ei;
	ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
	return &ei->vfs_inode;
}

sock_alloc申请了一个socket_alloc结构体,然后返回socket_alloc结构体中的socket结构体。接着我们看看又做了什么操作。

	// 根据socket类型拿到对应的处理方式
	pf = rcu_dereference(net_families[family]);
	pf->create(net, sock, protocol, kern);

前面我们已经分析过net_families,这里会调family对应的create函数。

static int inet_create(struct net *net, struct socket *sock, int protocol,
		       int kern)
{
	struct sock *sk;
	struct inet_protosw *answer;
	struct inet_sock *inet;
	struct proto *answer_prot;
	unsigned char answer_flags;
	int try_loading_module = 0;
	int err;
	sock->state = SS_UNCONNECTED;

	/* Look for the requested type/protocol pair. */
lookup_protocol:
	err = -ESOCKTNOSUPPORT;
	rcu_read_lock();
	// 遍历找到对应类型的处理方式
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

		err = 0;
		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			/* Check for the two wild cases. */
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}
	// 操作函数集
	sock->ops = answer->ops;
	answer_prot = answer->prot;
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
	// 关联socket和sock结构体
	sock_init_data(sock, sk);
	if (sk->sk_prot->init) {
		err = sk->sk_prot->init(sk);
	}
}

struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
		      struct proto *prot, int kern)
{
	struct sock *sk;
	// 创建一个sock结构体
	sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
	if (sk) {
		sk->sk_family = family;
		// 赋值函数集
		sk->sk_prot = sk->sk_prot_creator = prot;
	}

	return sk;
}

inet_create函数主要的逻辑主要是根据type找到对应协议的处理函数集,然后把对应的函数集赋值给socket结构体(后面会用到),接着创建一个sock结构体并把对应的函数集合赋值给sock结构体。接着把sock和socket结构体关联起来。最后调用sock的init函数。我们以UDP为例。

int udp_init_sock(struct sock *sk)
{
	skb_queue_head_init(&udp_sk(sk)->reader_queue);
	sk->sk_destruct = udp_destruct_sock;
	return 0;
}

只是做了一些初始化的处理。最后形成以下架构。

很多同学应该都知道Linux万物皆文件的哲学思想,当我们调用socket拿到一个结构体后,并不是把这个结构体返回给调用方,而是返回一个文件描述符fd。这个fd就像一个索引一样,后续就可以通过该fd找到对应的socket结构体。我们看看是怎么处理的。

static int sock_map_fd(struct socket *sock, int flags)
{
	struct file *newfile;
	// 获取一个可用的fd
	int fd = get_unused_fd_flags(flags);
	// 获取一个可用的file结构体
	newfile = sock_alloc_file(sock, flags, NULL);
	// 关联fd和file结构体
	if (!IS_ERR(newfile)) {
		fd_install(fd, newfile);
		return fd;
	}
	put_unused_fd(fd);
	return PTR_ERR(newfile);
}

至此网络层的整体架构就分析完了,我们再看一下这种架构的好处是什么,比如我们后续会调用bind函数。

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;
	// 根据fd找到对应的socket结构体
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		err = move_addr_to_kernel(umyaddr, addrlen, &address);
		if (!err) {
			err = security_socket_bind(sock,
						   (struct sockaddr *)&address,
						   addrlen);
			if (!err)
				// 调用函数集的bind函数
				err = sock->ops->bind(sock,
						      (struct sockaddr *)
						      &address, addrlen);
		}
		fput_light(sock->file, fput_needed);
	}
	return err;
}

我能看到后续的调用的网络函数中,会调用底层对应的函数,当我们新增一个协议的时候,只需要实现对应的API就可以。我们看到整个网络层的实际中,主要分为socket层、af_inet层和具体协议层(TCP、UDP等)。当我使用网络编程的时候,首先会创建一个socket结构体(socket层),socket结构体是最上层的抽象,然后通过协议簇类型创建一个对应的sock结构体,sock是协议簇抽象(af_inet层),同一个协议簇下又分为不同的协议类型,比如TCP、UDP(具体协议层),然后根据socket的类型(流式、数据包)找到对应的操作函数集并赋值到socket和sock结构体中,后续的操作就调用对应的函数就行,调用某个网络函数的时候,会从socket层到af_inet层,af_inet做了一些封装,必要的时候调用底层协议(TCP、UDP)对应的函数。而不同的协议只需要实现自己的逻辑就能加入到网络协议中。

总结,网络层的设计VFS有点相似,通过在上层定义抽象的API,底层实现具体的API,这样就可以灵活地拓展。但是总的来说,网络层的实现是非常复杂的,尤其到了新版的内核,本文做了个大致的介绍,后续有时间继续深入分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要在Linux上安装Qt5.9,您可以按照以下步骤进行操作: 1. 首先,您需要从Qt官网下载Qt5.9的安装包。 2. 下载完成后,解压缩安装包到您想要安装的目录。 3. 打开终端,进入解压缩后的目录。 4. 运行./configure命令,进行配置。 5. 配置完成后,运行make命令进行编译。 6. 编译完成后,运行make install命令进行安装。 7. 安装完成后,您可以在终端中输入qtchooser -list-versions命令,查看已安装的Qt版本。 8. 如果您需要在Qt Creator中使用Qt5.9,您还需要在Qt Creator中配置Qt版本。打开Qt Creator,选择Tools -> Options -> Build & Run -> Kits,点击Add按钮,选择您安装的Qt版本,然后点击Apply按钮即可。 希望这些步骤能够帮助您成功安装Qt5.9。 ### 回答2: 在Linux(Ubuntu)系统下安装Qt 5.9非常简单,只需要几个步骤就可以完成操作: 1. 下载Qt 5.9开源安装包:从Qt官网的下载页面下载适用于您操作系统的安装包,您可以选择Qt官方提供的在线安装程序,也可以下载离线的安装包,然后将其解压。 2. 安装Qt 5.9:为了避免权限问题,建议将解压后的安装包放到用户目录下,然后通过终端进入安装包所在目录中,使用以下命令安装: ./configure -prefix ~/Qt/5.9.0 && make && make install 执行此命令将在用户目录下创建一个目录“~/Qt/5.9.0”并将Qt 5.9安装在此目录下。 3. 配置Qt Creator:安装完成后,打开Qt Creator,选择“Tools” - “Options” - “Build & Run” - “Kits”,点击“Add”按钮创建新的编译工具链,设置好Qt使用的编译器路径和Qt库路径。 4. 许可证检查:在第一次启动Qt Creator后,会弹出一个许可证检查窗口。使用您的Qt帐户登录,接受许可协议即可。 安装完成后,您就可以开始使用Qt 5.9来开发应用程序了。同时,您还可以通过安装qt4-default、qtcreator等软件包来更方便地使用Qt。 ### 回答3: 在Linux操作系统中安装Qt 5.9需要经过以下几个步骤: 1.下载Qt 5.9安装包 从Qt官方网站下载最新版本的Qt 5.9安装包。根据自己操作系统的位数以及需求选择相应的版本进行下载。 2.安装依赖软件包 在开始Qt 5.9的安装过程之前,需要安装一些依赖软件包来支持Qt的运行。可以通过以下命令在终端中安装: sudo apt-get install build-essential libgl1-mesa-dev 这条命令将安装本地编译所需的构建工具和OpenGL相关的依赖软件包。 3.解压Qt 5.9安装包 将下载的Qt 5.9安装包解压到任意目录中。这里以在家目录下创建一个文件夹用于存放解压后的QT安装包,并以qt-5.9.9为例进行说明: mkdir ~/qt-5.9.9 cd ~/qt-5.9.9 tar xzvf ~/Downloads/qt-opensource-linux-x64-5.9.9.run 4.运行安装程序 解压后,进入qt-5.9.9文件夹,执行以下命令启动安装界面: ./qt-opensource-linux-x64-5.9.9.run 根据提示信息进行安装,选择需要安装的组件和安装路径,最后进行安装。 5.配置Qt环境变量 安装完毕后需要配置Qt环境变量。将以下代码添加到~/.profile文件中: export PATH=$PATH:/path/to/qt-5.9.9/bin export LD_LIBRARY_PATH=/path/to/qt-5.9.9/lib 以上是在普通用户用户主目录下配置Qt环境变量的方法,在root用户下时请使用~/.bashrc文件进行配置。 6.测试Qt版本 在终端窗口中输入以下命令,检查安装的Qt版本: qmake -v 如果出现如下所示的信息,说明Qt 5.9的安装已经成功: QMake version 3.1 Using Qt version 5.9.9 in /path/to/qt-5.9.9/lib 通过以上六个步骤,我们可以在Linux系统中成功安装Qt 5.9,并为以后的应用程序开发工作奠定基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值