实验要求:
- 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 为例,介绍一次系统调用的所有环节。
如上图,系统调用执行的流程如下:
- 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
- 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
- CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
- 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
执行态切换:
应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。
Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。
内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。
总结起来, 执行态切换 过程如下:
- 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
- CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
- 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
- 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
- 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
- 系统调用处理函数 执行 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; }