1.用户态、内核态、中断和系统调用的关系
所谓的中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一事件的程序。等这些特殊事情处理完之后再回去执行之前的程序。中断一般分为三类:
- 由计算机硬件异常或故障引起的中断,称为内部异常中断
- 由程序中执行了引起中断的指令而造成的中断,称为软中断(系统调用就属于软中断)
- 由外部设备请求引起的中断,称为外部中断
进程的执行在系统上有两个级别:用户态和核心态。程序的执行一般是在用户态下执行的,但在此状态下,不能执行特权指令(访问IO设备、访问特殊寄存器、置时钟... ...)。当程序需要使用这些特权指令时,就需要向操作系统发出调用服务的请求,这就是系统调用。 Linux系统有专门的函数库来提供这些请求操作系统服务的入口,这个函数库中包含了操作系统所提供的对外服务的接口。
用户态、内核态、系统调用和中断的关系就在于,当用户态进程发出系统调用申请的时候,会产生一个软中断。产生这个软中断以后,系统会去对这个软中断进行处理,这个时候进程就处于核心态了。
2.Linux中的系统调用
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中找到这段代码,即将0x80这一中断号绑定entry_INT80_32(系统中断处理函数)
SYSG(IA32_SYSCALL_VECTOR,entry_INT80_32);
在Linux中,程序调用一个系统调用的流程如下图所示
(图片来自《UNIX网络编程》)
- 应用程序调用一个库函数xyz()
- 库函数内封装了一个系统调用SYSCALL,它将参数传递给内核并触发中断
- 触发中断后,内核进行中断处理,执行系统调用处理函数 (5.0内核中是entry_INT80_32,不再是system_call了)
- 系统调用处理函数会根据系统调用号,选择相应的系统调用服务例程(在这里是sys_xyz),真正开始处理该系统调用
3.Linux Socket api对应的系统调用是如何被内核处理的
根据老师给出的系统调用列表,我们能够找出Linux的Socket api(封装好的系统调用)和对应的实际系统调用
查阅资料我们得知sys_socketcall几乎是所有与socket操作有关的系统调用的总入口,它是如何充当入口的?例如sys_socket等系统调用和它又是什么关系?
我们可以用gdb调试查看一下
同上一节一样,启动menuOS后另起一个终端,建议先修改menu目录下的Makefile,将qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S末尾的-S去掉,不然会先追踪menuOS启动时的系统调用
gdb file ~/experiment/linux-5.0.1/vmlinux target remote:1234 break sys_socketcall #设置断点在sys_socketcall c
然后执行replyhi,观察是否调用了sys_socketcall
可以看到,replyhi不止一次调用了sys_socketcall(共计调用了4次,参数call分别赋值1,2,4,5)
sys_socketcall对应的系统调用服务例程是SYSCALL_DEFINE2。Linux系统调用的定义部分都被命名为SYSCALL_DEFINEx,末尾数字代表系统调用的参数(sys_socketcall有两个参数,刚好对应)
在net/socket.c中可以找到SYS_DEFINE2的定义
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) { unsigned long a[AUDITSC_ARGS]; unsigned long a0, a1; int err; unsigned int len; if (call < 1 || call > SYS_SENDMMSG) return -EINVAL; call = array_index_nospec(call, SYS_SENDMMSG + 1); len = nargs[call]; if (len > sizeof(a)) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, len)) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = __sys_listen(a0, a1); break; case SYS_ACCEPT: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: err = __sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_GETPEERNAME: err = __sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_SOCKETPAIR: err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], NULL, 0); break; case SYS_SENDTO: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: err = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: err = __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_SENDMMSG: err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], true); break; case SYS_RECVMSG: err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_RECVMMSG: if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME)) err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], (struct __kernel_timespec __user *)a[4], NULL); else err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], NULL, (struct old_timespec32 __user *)a[4]); break; case SYS_ACCEPT4: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err; }
可以看到实现的核心是一个switch语句,通过call参数来处理sys_socket、sys_bind等系统调用,每个case都和上面的系统调用表中的项对应
在上面的调试中我们已经知道了replyhi共计调用了四次sys_socketcall,参数call分别被赋值1,2,4,5,在switch中对应SYS_SOCKET,SYS_BIND,SYS_LISTEN,SYS_ACCEPT。而查阅reply程序的源代码,可以发现它使用的Socket api刚好与之吻合
每一个case所对应的,就是Linux内核态下用来处理Socket系统调用的处理函数(以双下划线开头)。这样,我们就梳理完了从程序调用Socket api-->内核处理系统调用的过程了
4.socket相关系统调用的内核处理函数内部如何通过“多态机制”对不同的网络协议进行封装的
在上一节中我们已经找到了处理Socket系统调用的处理函数,现在我们深入的看一下__sys_socket的实现,解析它是如何对不同的网络协议进行封装的
int __sys_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; retval = sock_create(family, type, protocol, &sock); if (retval < 0) return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); }
标红色的sock_create是对不同网络协议进行处理的关键(传入了family参数,应该是根据这个参数判断协议族),我们找到sock_create的实现函数__socket_create
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 */ 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. */ if (family == PF_INET && type == SOCK_PACKET) { pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET) ", current->comm); family = PF_PACKET; } 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 = 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; #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(); 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. */ if (!try_module_get(pf->owner)) goto out_release; /* Now protected by module ref count */ rcu_read_unlock(); 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. */ 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 */ module_put(pf->owner); 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; }
从上面的代码我们可以看到不同的网络协议是如何利用多态被封装的:
- 内核中有多个网络协议对应的协议族模块,__socket_create会根据传来的family参数,查看net_families数组,判断内核是否支持该协议族
- 若支持,则调用协议族模块的方法创建socket,填充sock
- __sys_socket会返回sock给__sys_bind等方法使用,实际上是使用内核的协议族模块