zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    java通过ST4使用模板字符串
    使用 docker创建redis实例并且连接
    Docker 认证成功后还是无法push构建好的镜像
    记录一次在openwrt中折腾docker
    全局模式、PAC模式、直连模式的区别
    Vue Router中调用this.$router.push() 时,location使用path无法传入params
    liunx之系统
    liunx之通配符&正则表达式
    liunx之基础
    liunx之find命令
  • 原文地址:https://www.cnblogs.com/xqqu/p/12066339.html
Copyright © 2011-2022 走看看