一.前言
1.用户态和内核态
为了限制不同的程序之间的访问能力,防止它们获取其它程序的内存数据,或者获取外围设备的数据,并发送到网络,CPU划分出两个权限等级,分别是用户态和内核态。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
2.用户态和内核态的转换
用户态转换到内核态主要有如下3种方式:
(1)系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,而系统调用的一个核心机制还是使用了操作系统为用户开放的一个中断来实现。
(2)异常
当CPU在执行运行在用户态的程序时,发生了某些异常,就会触发当前进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
(3)外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出中断信号,此时CPU会暂停执行下一条将要执行的指令,转向中断信号对应的处理程序去执行,如果前面执行的是用户态下程序的指令,自然就完成了从用户态到内核态的转换。
这三种方式是系统在运行时从用户态转为内核态的主要方式,其中系统调用用户进程主动发起的,而另外两种则是被动的。
3.系统调用概述
操作系统为用户态进程与硬件设备进行交互提供了一组接口系统调用,有利于提高系统安全性,将用户从底层的硬件编程中给解放了出来,便于用户程序可移植性。
系统调用的库函数就是我们使用的操作系统提供的API,系统调用通过软中断向内核发出中断请求,int指令触发中断请求。
libc函数库内部定义的一些API内部就使用了系统调用的封装。
每个系统调用对应一个系统调用的封装例程,函数库再使用这些封装例程定义给出程序员可以调用的API一个API可能只对应一个系统调用,也可能由多个系统调用实现。
二.Socket与系统调用深度分析
首先进入lab3目录下,看下tcp通信程序的主要函数实现。
main函数的实现如下,可以看到当我们输入replyhi时,调用到了StartReplyhi这个函数,输入hello时,调用到了Hello函数。
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(); }
接下来,我们看下StartReplyhi函数的实现,可以看到在子进程中调用到了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这个函数的实现,可以看到在Peplyhi函数中调用到了InitializeService,ServiceStart,RecvMsg,SendMsg,ServiceStop,ShutdownService这几个函数,接下来让我们看下这几个函数内部的实现。
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函数又调用到了PrepareSocket,InitServer函数,而PrepareSocket函数内部又调用了socket函数,InitServer函数内部调用到了bind、listen函数
#define InitializeService() \ PrepareSocket(IP_ADDR,PORT); \ InitServer(); #define PrepareSocket(addr,port) \ int sockfd = -1; \ struct sockaddr_in serveraddr; \ struct sockaddr_in clientaddr; \ socklen_t addr_len = sizeof(struct sockaddr); \ serveraddr.sin_family = AF_INET; \ serveraddr.sin_port = htons(port); \ serveraddr.sin_addr.s_addr = inet_addr(addr); \ memset(&serveraddr.sin_zero, 0, 8); \ sockfd = socket(PF_INET,SOCK_STREAM,0); #define InitServer() \ int ret = bind( sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Bind Error,%s:%d\n", \ __FILE__,__LINE__); \ close(sockfd); \ return -1; \ } \ listen(sockfd,MAX_CONNECT_QUEUE);
ServiceStart函数内部调用到了accept函数
#define ServiceStart() \ int newfd = accept( sockfd, \ (struct sockaddr *)&clientaddr, \ &addr_len); \ if(newfd == -1) \ { \ fprintf(stderr,"Accept Error,%s:%d\n", \ __FILE__,__LINE__); \ }
RecvMsg函数内部调用到了recv函数
#define RecvMsg(buf) \ ret = recv(newfd,buf,MAX_BUF_LEN,0); \ if(ret > 0) \ { \ printf("recv \"%s\" from %s:%d\n", \ buf, \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ }
SendMsg函数内部调用到了send函数
#define SendMsg(buf) \ ret = send(newfd,buf,strlen(buf),0); \ if(ret > 0) \ { \ printf("send \"hi\" to %s:%d\n", \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ }
ServiceStop函数内部调用到了close函数
#define ServiceStop() \ close(newfd);
ShutdownService函数内部调用到了close函数
#define ShutdownService() \ close(sockfd);
看完了StartReplyhi函数的调用过程,再来看下Hello函数的内部实现,可以看到在Hello函数内部调用了OpenRemoteService、SendMsg、RecvMsg、CloseRemoteService这几个函数。
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; }
首先看下OpenRemoteService函数,调用了PrepareSocket、InitClient函数,PrepareSocket函数内部调用了socket函数,InitClient函数内部调用了connect函数
#define OpenRemoteService() \ PrepareSocket(IP_ADDR,PORT); \ InitClient(); \ int newfd = sockfd; #define PrepareSocket(addr,port) \ int sockfd = -1; \ struct sockaddr_in serveraddr; \ struct sockaddr_in clientaddr; \ socklen_t addr_len = sizeof(struct sockaddr); \ serveraddr.sin_family = AF_INET; \ serveraddr.sin_port = htons(port); \ serveraddr.sin_addr.s_addr = inet_addr(addr); \ memset(&serveraddr.sin_zero, 0, 8); \ sockfd = socket(PF_INET,SOCK_STREAM,0); #define InitClient() \ int ret = connect(sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Connect Error,%s:%d\n", \ __FILE__,__LINE__); \ return -1; \ }
SendMsg函数,RecvMsg函数的内部实现之前已经提及,CloseRemoteService函数内部则是调用了close函数
#define CloseRemoteService() \ close(sockfd);
至此,可以看到Replyhi函数涉及到的系统调用为socket,bind,listen,accept,recv,send,Hello函数涉及到的系统调用为socket,connect,send,recv
接下来对其进行分析
首先看下32位系统调用列表
接下来进行gdb调试跟踪,来看下前面涉及到的socket、bind、listen、accept、send、receive、connect等函数对应的内核处理函数是什么
cd LinuxKernel/linuxnet qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S #打开一个新的终端 gdb file ~/LinuxKernel/linux-5.0.1/vmlinux target remote:1234 b __ia32_compat_sys_socketcall
可以看到一共捕获到了3次__ia32_compat_sys_socketcall,如图所示。
此时,我们需要在MenuOS中输入replyhi才能继续往下执行
net/compat.c部分源码
switch (call) { case SYS_SOCKET: ret = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: ret = __sys_bind(a0, compat_ptr(a1), a[2]); break; case SYS_CONNECT: ret = __sys_connect(a0, compat_ptr(a1), a[2]); break; case SYS_LISTEN: ret = __sys_listen(a0, a1); break; case SYS_ACCEPT: ret = __sys_accept4(a0, compat_ptr(a1), compat_ptr(a[2]), 0); break; case SYS_GETSOCKNAME: ret = __sys_getsockname(a0, compat_ptr(a1), compat_ptr(a[2])); case SYS_GETPEERNAME: ret = __sys_getpeername(a0, compat_ptr(a1), compat_ptr(a[2])); break; case SYS_SOCKETPAIR: ret = __sys_socketpair(a0, a1, a[2], compat_ptr(a[3])); break; case SYS_SEND: ret = __sys_sendto(a0, compat_ptr(a1), a[2], a[3], NULL, 0); break; case SYS_SENDTO: ret = __sys_sendto(a0, compat_ptr(a1), a[2], a[3], compat_ptr(a[4]), a[5]); break; case SYS_RECV: ret = __compat_sys_recvfrom(a0, compat_ptr(a1), a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: ret = __compat_sys_recvfrom(a0, compat_ptr(a1), a[2], a[3], compat_ptr(a[4]), compat_ptr(a[5])); break; case SYS_SHUTDOWN: ret = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: ret = __compat_sys_setsockopt(a0, a1, a[2], compat_ptr(a[3]), a[4]); break; case SYS_GETSOCKOPT: ret = __compat_sys_getsockopt(a0, a1, a[2], compat_ptr(a[3]), compat_ptr(a[4])); break; case SYS_SENDMSG: ret = __compat_sys_sendmsg(a0, compat_ptr(a1), a[2]); break; case SYS_SENDMMSG: ret = __compat_sys_sendmmsg(a0, compat_ptr(a1), a[2], a[3]); break; case SYS_RECVMSG: ret = __compat_sys_recvmsg(a0, compat_ptr(a1), a[2]); break; case SYS_RECVMMSG: ret = __sys_recvmmsg(a0, compat_ptr(a1), a[2], a[3] | MSG_CMSG_COMPAT, NULL, compat_ptr(a[4])); break; case SYS_ACCEPT4: ret = __sys_accept4(a0, compat_ptr(a1), compat_ptr(a[2]), a[3]); break; default: ret = -EINVAL; break; } return ret; }
继续设置断点跟踪看看调用了哪些接口
服务端完成了socket的创建,涉及到的系统调用如下
我们需要继续在MenuOS中输入hello才能继续往下执行
客户端创建了socket
客户端发起连接
服务端等待接收数据
客户端向服务端发送数据
客户端等待服务端发送数据
服务端向客户端发送数据
服务端继续调用accept等待接受下一个端口
至此,完成了一次通信过程
可以看到,总共涉及到了14次系统调用。前3次系统调用完成了系统初始化工作,然后服务端调用了socket、bind、listen、accept,客户端调用了socket、connect,服务端调用recv,客户端调用send,服务端调用send,客户端调用recv,服务端又继续调用accept。
至此,完成了socket的hello/hi的追踪验证。