Socket与系统调用深度分析

实验要求:

  • Socket API编程接口之上可以编写基于不同网络协议的应用程序;
  • Socket接口在用户态通过系统调用机制进入内核;
  • 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
  • socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;

请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。

实验环境:vmware 15.5下的ubuntu16.04虚拟机

基于内核:linux 5.0.1

内核编译方式:x86-64

调用流程:

操作系统通过系统调用为运行于其上的进程提供服务。

当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

如上图,系统调用执行的流程如下:

  1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
  2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
  3. CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
  4. 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

执行态切换:

应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。

Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。

内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

总结起来, 执行态切换 过程如下:

  1. 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
  2. CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
  3. 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
  4. 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
  5. 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
  6. 系统调用处理函数 执行 ret 指令切换回 用户态 ;
API、POSIX和C库

1、一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。一个API定义了一组应用程序使用的编程接口。它可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在任何问题。

2、在Unix系统中,最流行的应用程序编程接口是基于POSIX标准的。

3、Linux的系统调用作为C库的一部分提供。C库实现了Unix系统主要API,包括标准C库函数和系统调用接口。

4、应用编程与系统调用无关紧要,但内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用的,不是内核所关心的。

5、Unix接口设计有一句格言:“提供机制而不提供策略”,换句话说,Unix系统调用抽象出了用于完成某种确定目的的函数。至于这些函数怎么使用完全不用内核关心。

系统调用

系统调用(在Linux种常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或者多个参数,而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。系统调用还会通过一个long类型的返回值来表示成功或者错误。通常,用一个负的返回值来表示错误。返回一个0值表示成功。Unix系统调用在出现错误的时候,C库会把错误码写入errno全局变量,通过调用perror()库函数,可以把变量翻译成用户可以理解的错误字符串。

socket相关系统调用内核函数和跟踪验证:

在上次实验中我们已经将 TCP 网络程序的服务端 replyhi 集成到 MenuOS 中了,而且可以正常的启动 TCP 服务,方便我们跟踪  API 接口到内核处理函数

打开MenuOS,效果如下:

 

打开gdb调试,将与socket相关的函数们都打上断点

在本体系结构中,函数们如下:

 我们先在MenuOS中输入replyhi,这时gdb中不在保持持续的continue,而是运行到了下一个断点,此时MenuOS中虽然提示输入please input hello,但实际上此时并不可以输入任何命令,如上图所示,此时我们在gdb中结合c和n,直到MenuOS可以输入命令。

一. socket()函数系统调用过程

在sys_socketcall()函数中可以看到,socket系统调用最终调用的是sys_socket()函数

sys_socket()函数声明如下:

asmlinkage long sys_socket(int, int, int);

同样地,sys_socket()函数实现为:

1. sys_socket()

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
	int retval;
	struct socket *sock;
	int flags;
 
	/* Check the SOCK_* constants for consistency.  */
	BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
	BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
 
	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;
	
	/*创建socket及inode*/
	retval = sock_create(family, type, protocol, &sock);
	if (retval < 0)
		goto out;
	
	/*创建file,完成fd与file绑定,file与socket绑定*/
	retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
	if (retval < 0)
		goto out_release;
 
out:
	/* It may be already another descriptor 8) Not kernel problem. */
	return retval;
 
out_release:
	sock_release(sock);
	return retval;
}

2. sock_create()函数:

这个函数是对__socket_create函数的封装,直接调用__sock_create()函数。

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

3. __sock_create()函数

创建socket及inode

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;
 
	/*
	 *      Check protocol is in range
	 */
	/*family和type字段范围检查*/
	if (family < 0 || family >= NPROTO)
		return -EAFNOSUPPORT;
	if (type < 0 || type >= SOCK_MAX)
		return -EINVAL;
 
	/* Compatibility.
	   This uglymoron is moved from INET layer to here to avoid
	   deadlock in module load.
	 */
	/*兼容性考虑,IPv4协议族的SOCK_PACKET已经废弃,当family ==F_INET && type == SOCK_PACKET时,
	强制把family改为PF_PACKET。*/
	if (family == PF_INET && type == SOCK_PACKET) {
		static int warned;
		if (!warned) {
			warned = 1;
			pr_info("%s uses obsolete (PF_INET,SOCK_PACKET)
",
				current->comm);
		}
		family = PF_PACKET;
	}
	
	/*安全模块对套接口的创建做检查,安全模块不是网络中必需的组成部门,不做讨论。*/
	// 检查权限,并考虑协议集、类型、协议,以及 socket 是在内核中创建还是在用户空间中创建
    // 可以参考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
	err = security_socket_create(family, type, protocol, kern);
	if (err)
		return err;
 
	/*
	 *	Allocate the socket and allow the family to set things up. if
	 *	the protocol is 0, the family is instructed to select an appropriate
	 *	default.
	 */
	/*调用sock_alloc()在sock_inode_cache缓存中分配与套接口关联的i结点和套接口,同时
	初始化i结点和套接口,失败则直接返回错误码。*/
	sock = sock_alloc();
	if (!sock) {
		net_warn_ratelimited("socket: no more sockets
");
		return -ENFILE;	/* Not exactly a match, but its the
				   closest posix thing */
	}
 
	sock->type = type;
 
/*如果协议族支持内核模块动态加载,但在创建此协议族类型的套接字时,内核模块并未被加载,则调用
request_module()进行内核模块的动态加载。*/
#ifdef CONFIG_MODULES
	/* Attempt to load a protocol module if the find failed.
	 *
	 * 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
	 * requested real, full-featured networking support upon configuration.
	 * Otherwise module support will break!
	 */
	if (rcu_access_pointer(net_families[family]) == NULL)
		request_module("net-pf-%d", family);
#endif
 
	rcu_read_lock();
	
	/*获取对应协议的net_proto_family指针*/
	pf = rcu_dereference(net_families[family]);
	err = -EAFNOSUPPORT;
	if (!pf)
		goto out_release;
 
	/*
	 * We will call the ->create function, that possibly is in a loadable
	 * module, so we have to bump that loadable module refcnt first.
	 */
	/*如果对应协议族模块是动态加载到内核中去的,则对此内核模块的应用计数+1,以防
	在创建过程中,该模块被卸载,造成严重的后果。*/
	if (!try_module_get(pf->owner))
		goto out_release;
 
	/* Now protected by module ref count */
	rcu_read_unlock();
	
	/*在IPv4协议族中调用inet_create()对已创建的socket继续进行初始化,同时创建网络层socket。*/
	err = pf->create(net, sock, protocol, kern);
	if (err < 0)
		goto out_module_put;
 
	/*
	 * Now to bump the refcnt of the [loadable] module that owns this
	 * socket at sock_release time we decrement its refcnt.
	 */
	/*如果proto_ops结构实例所在模块以内核模块方式动态加载进内核,
	则增加该模块的引用计数,在sock_release时,减小该计数。*/
	if (!try_module_get(sock->ops->owner))
		goto out_module_busy;
 
	/*
	 * Now that we're done with the ->create function, the [loadable]
	 * module can have its refcnt decremented
	 */
	/*调用完inet_create函数后,对此模块的引用计数减一。*/
	module_put(pf->owner);
	
	/*安全模块对创建后的socket做安全检查,不做讨论。*/
	err = security_socket_post_create(sock, family, type, protocol, kern);
	if (err)
		goto out_sock_release;
	*res = sock;
 
	return 0;
 
out_module_busy:
	err = -EAFNOSUPPORT;
out_module_put:
	sock->ops = NULL;
	module_put(pf->owner);
out_sock_release:
	sock_release(sock);
	return err;
 
out_release:
	rcu_read_unlock();
	goto out_sock_release;
}
原文地址:https://www.cnblogs.com/xqqu/p/12066339.html