朱建伟 2019-12-19
请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。
完成一篇图文并茂、逻辑严谨、代码详实的实验报告
计算机系统的各种硬件资源是有限的,操作系统的发展过程大体上就是一个想方设法不断提高资源利用率的过程,为了更好的管理这些资源,进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,想要使用这些资源就要通过操作系统提供的系统调用(System Call)。
系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?
答案是中断。操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。如I/O中断、时钟中断等。在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。
中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。
一般地,系统调用都是通过中断实现的,系统调用运行在系统的内核态。通过系统调用的方式来使用系统功能,可以保证操作系统的稳定性和安全性,防止用户随意更改或访问系统的数据和命令。接下来就来看一下Linux下系统调用具体的实现过程。
用户通过操作系统运行上层程序(如用户自编程序),此时CPU在用户态下运行,当用户程序试图执行一条内核态才能运行的命令时,如系统调用,就会触发中断处理程序,通过中断机制进入内核态,由内核态程序接管CPU,执行系统调用,执行完毕后,退出中断处理程序,返回通胡程序断点处继续执行。可以画个图表示一下:
上面我们知道了系统调用的过程,系统调用也像一个个函数,但是程序员在编程中是直接通过系统调用来实现功能吗?答案很明显是否,一般情况下,程序员编写程序通过编程语言中的应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。
以Socket API 为例,用户编程调用socket API,程序执行时API调用系统调用,进入内核态执行系统调用,功能完成后返回用户态,程序从断点处继续执行。
一个API定义了一组应用程序使用的编程接口。它们可以由一个系统调用实现,也可以通过调用多个系统调用来实现。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。
在Unix世界中,最流行的应用编程接口是基于POSIX标准的,其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX而定义的API函数和系统调用之间有着直接关系。
Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供如下图所示。C库实现了 Unix系统的主要API,包括标准C库函数和系统调用。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。
从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。
使用上次实验时构建的menuOS系统,进行系统调用分析,重新启动qemu虚拟机,注意本次实验需要重新编译x86_64内核,并且修改相关的menuOS以及lab3的Makefile文件,以启动64位linux系统,修改如下:
1 qemu-system-x86_64 -kernel ../../linux-5.0.1/arch/x86_64/boot/bzImage -initrd ../rootfs.img
执行hello/hi,结果如下:
执行正常,打开lab3中的main.c,查看功能是如何实现的;
main函数:
int main() { BringUpNetInterface(); PrintMenuOS(); SetPrompt("MenuOS>>"); MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL); MenuConfig("quit","Quit from MenuOS",Quit); MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi); MenuConfig("hello", "Hello TCP Client", Hello); ExecuteMenu(); }
可以看到main函数会打印出一些信息,运行系统,等待输入
Replyhi函数
int StartReplyhi(int argc, char *argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr, "Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ Replyhi(); printf("Reply hi TCP Service Started!\n"); } else { /* parent process */ printf("Please input hello...\n"); } }
当输入Replyhi之后,且条件满足(pid==0),就会继续调用Replyhi函数;
int Replyhi() { char szBuf[MAX_BUF_LEN] = "\0"; char szReplyMsg[MAX_BUF_LEN] = "hi\0"; InitializeService(); while (1) { ServiceStart(); RecvMsg(szBuf); SendMsg(szReplyMsg); ServiceStop(); } ShutdownService(); return 0; }
运行上述函数,可以发现依次调用了InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()以及ShutdownService()函数,在头文件“syswrapper.h”可以找到这些函数;
1 #define InitializeService() 2 PrepareSocket(IP_ADDR,PORT); 3 InitServer();
1 #define ServiceStart() 2 int newfd = accept( sockfd, 3 (struct sockaddr *)&clientaddr, 4 &addr_len); 5 if(newfd == -1) 6 { 7 fprintf(stderr,"Accept Error,%s:%d\n", 8 __FILE__,__LINE__); 9 }
1 #define RecvMsg(buf) 2 ret = recv(newfd,buf,MAX_BUF_LEN,0); 3 if(ret > 0) 4 { 5 printf("recv \"%s\" from %s:%d\n", 6 buf, 7 (char*)inet_ntoa(clientaddr.sin_addr), 8 ntohs(clientaddr.sin_port)); 9 }
1 #define SendMsg(buf) 2 ret = send(newfd,buf,strlen(buf),0); 3 if(ret > 0) 4 { 5 printf("send \"hi\" to %s:%d\n", 6 (char*)inet_ntoa(clientaddr.sin_addr), 7 ntohs(clientaddr.sin_port)); 8 }
1 #define ServiceStop() 2 close(newfd);
1 #define ShutdownService() 2 close(sockfd);
继续追溯代码中的函数调用,可以看到程序最终是通过调用操作系统的socket API,来实现了socket的通信功能,
考察hello函数,与上述过程类似,就不在赘述:
int Hello(int argc, char *argv[]) { char szBuf[MAX_BUF_LEN] = "\0"; char szMsg[MAX_BUF_LEN] = "hello\0"; OpenRemoteService(); SendMsg(szMsg); RecvMsg(szBuf); CloseRemoteService(); return 0; }
以上让我们又在代码范围重新温习了一遍socket通信的过程,与之前自己编写的通信程序思路完全吻合;
接下来启动调试模式,跟踪分析系统调用:
1 qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -s -S -append nokaslr
在另一个终端启动gdb调试,设置断点,跟踪系统调用:
四个断点均已设置(第五个断点我多打了一遍。。),执行程序,可以看到程序依次在断点处停止,可见程序执行时系统调用过程为:start_kernel --> trap_init --> cpu_init --> syscall_init
执行socket通信过程,在sys_socketcall处打上断点,查看通信过程中的系统调用;
执行hello/hi 通信,可以看到程序共进行了14次中断:
查看SYSCALL_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: #call=1 err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: #call=2 err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: #call=3 err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: #call=4 err = __sys_listen(a0, a1); break; case SYS_ACCEPT: #call=5 err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: #call=6 err = __sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_GETPEERNAME: #call=7 err = __sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_SOCKETPAIR: #call=8 err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: #call=9 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], NULL, 0); break; case SYS_SENDTO: #call=10 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: #call=11 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: #call=12 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: #call=13 err = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: #call=14 err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: #call=15 err = __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: #call=16 err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_SENDMMSG: #call=17 err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], true); break; case SYS_RECVMSG: #call=18 err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_RECVMMSG: #call=19 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: #call=20 err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err; }
程序通过判断call的值,执行对应的系统调用,值序列为:1,1,1,1,2,4,5,1,3,10,9,10,9,5。
对应的系统调用分别为:socket,socket,socket,socket,bind,listen,accept,socket,connect,sendto,send,sendto,send,accept。
前三次系统调用socket为系统初始化,首先是服务端的初始化:第4-8个系统调用,之后是客户端的初始化:7-14个系统调用。从第四个socket开始是socket的初始化,依次是bind,listen,accept;之后进入客户端,依次是socket初始化,connet,以及在两端之间输入的两句话hello和hi,对应两个sendto和send,最后是accept表示套接字建立完成。
以上为socket的hello/hi通信过程的追踪。