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 为例,介绍一次系统调用的所有环节。
如上图,系统调用执行的流程如下:
- 应用程序 代码调用系统调用( 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()函数声明如下:
1 | asmlinkage long sys_socket( int , int , int ); |
同样地,sys_socket()函数实现为:
1. sys_socket()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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()函数。
1 2 3 4 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 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)\n" , 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\n" ); 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; } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
· .NET 进程 stackoverflow异常后,还可以接收 TCP 连接请求吗?
· SQL Server统计信息更新会被阻塞或引起会话阻塞吗?
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 本地部署 DeepSeek:小白也能轻松搞定!
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 普通人也能轻松掌握的20个DeepSeek高频提示词(2025版)