Socket与系统调用深度分析
一:常见的知识点回顾
- 用户态:指非特权状态,不能执行特权指令(访问IO设备、访问特殊寄存器、置时钟... ...),所有用户程序都是运行在用户态的
- 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境
- 系统调用(CPU中又称为陷阱指令):为了让应用程序有能力访问系统资源,每个操作系统都提供了一套接口,以供应用程序使用,这就是系统调用。它规定了用户进程进入内核的具体位置。它本身并非内核函数,但它是由内核函数实现,进入内核后,不同的系统调用会找到各自对应的内核函数(根据系统调用号),这些内核函数被称为系统调用的“服务例程”。
系统调用的流程如下:
- 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
- 用户态程序执行陷阱指令
- CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
- 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
- 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果
- API:是一些预先定义的函数,或指软件系统不同组成部分衔接的约定。 目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力
- 系统调用和api的区别:api是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。

二:Linux中系统调用
系统调用使得操作系统从用户态进入了内核态,从而使用了内核中的一些功能,linux x86-64系统通过int 80调用syscall中断来进入内核态。在使用socket编程中,系统先通过中断进入内核态,再调用底层功能来使得可以与网络通信协议产生连接。

x86-64Linux系统启动时依次调用以下过程:start_kernel --> trap_init --> cpu_init --> syscall_init,而syscall_init函数实现了系统调用的初始化将中断向量与服务例程进行绑定。除此之外,还要进行系统调用表的初始化。
简单的验证一下:
-
进入menu目录,打开menuos
make rootfs

2.进入linux-5.0.1目录,打开一个新中断,执行以下命令
gdb
file vmlinux
target remote:1234 #用其和menuos连接
b start_kernel
b trap_init
b cpu_init
b syscall_init
设置断点后,验证了其系统调用初始化的过程

意外的是我执行b syscall_init告诉我该函数没有定义
CPU为不同的中断设置了不同的中断号,以Intel i386为例,它共有256种中断,每个中断都用一个0~255之间的数来表示,Intel将前32个中断号(0~31)已经固定设定好或者保留未用。中断号32~255分配给操作系统和应用程序使用,Linux将系统调用设为中断号128,即0x80。
初始化流程为:start_kernel --> trap_init --> idt_setup_traps --> 0x80绑定entry_INT80_32
在idt.c源文件我们可找到上面的的绑定的系统中断处理函数
SYSG(IA32_SYSCALL_VECTOR,entry_INT80_32);
我们用一个例子来展示一个简单的系统调用的流程:

- 应用程序调用一个库函数xyz()
- 库函数内封装了一个系统调用SYSCALL,它将参数传递给内核并触发中断
- 触发中断后,内核进行中断处理,执行系统调用处理函数 (5.0内核中是entry_INT80_32,不再是system_call了)
- 系统调用处理函数会根据系统调用号,选择相应的系统调用服务例程(在这里是sys_xyz),真正开始处理该系统调用
三、Socket调用初步分析
#用gdb连接Menu OS服务器,端口1234,开始调试Menu OS系统
gdb
file vmlinux
target remote:1234
# 设置断点:由于不知道用的哪个send和recv内核处理函数,所以多设置两个关于send、recv的断点;
b __sys_socket
b __sys_bind
b __sys_listen
b __sys_connect
b __sys_accept4
b __sys_recvmsg
b __sys_sendmsg
b __sys_recvfrom
b __sys_sendto
b __sys_shutdown
#查看断点
info breakpoints
在实验之前,首先要修改上次的menu/Makefile文件
cd LinuxKernel/linux-5.0.1/menu
gedit Makefile
#修改内容
make rootfs



现在在gdb中按c运行Menu OS,然后在Menu OS中打开服务器,即输入replyhi:


然后我们在设置断点的中断中输入c,我们可以看到服务器运行到第一个断点__sys_socket函数处停止,在gdb中输入list或l 可以看到函数源代码,继续往下运行:

replyhi一直运行到sys_accept4处停止,等待Menu OS继续运行,这时候服务端已经调用sys_accept4,准备好接受客户端的连接请求了,现在需要在Menu OS中输入hello打开客户端,
gdb捕捉到了sys_socket断点,只不过这次是客户端hello的sys_socket内核处理函数.
追踪完毕,发现后续hello客户端发出请求连接sys_connect,与服务器建立连接,然后客户端、服务器互发消息,即调用了sys_recvfrom和sys_sendto,通过以上的跟踪发现简直与TCP下的C/S socket通信如出一辙,如果想看相应的内核处理函数,只需在断点处输入list即可,比如sys_socket源码如下:
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
(gdb)
/* 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;
(gdb)
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;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
浙公网安备 33010602011771号