本次实验以Menu OS的replyhi/hello聊天小程序为研究对象,通过gdb调试跟踪揭示socket编程api与系统调用之间的关系。
一、实验原理
典型的 TCP 客户机和服务器应用程序会使用诸如 socket()
、bind()
、listen()
、accept()
、send()
和 receive()这样的API来实现网络通信功能
。复杂的网络通信过程被封装在了这几个函数之下,简化了编程也屏蔽了细节。如今广泛使用的TCP/IP协议具有5层结构,socket层之下是复杂的tcp/ip协议层,更底层还需要网卡等硬件的协同工作。操作系统作为硬件的管理者和软件的协调者,就像一个面面具到的管家在幕后统筹着这一切。
之前所说的api只是用户态的函数,它们的主要作用是发起系统调用通知内核,具体的工作还是得由运行在内核态的内核函数来完成。下图展示了TCP/IP应用的层级结构。
下面我们在基于Linux-5.0.1内核的64位Menu OS,以一个简单的hello/replyhi聊天程序为线索,看看在程序的执行过程中有哪些系统调用和内核函数参与其中。
三、实验过程
简要回顾一下内核的编译:下载Linux-5.0.1的源码后,按默认配置生成.config文件。为了生成符号表用于调试,别忘了在.config文件中开启将相关选项:
CONFIG_DEGUB_INFO=y
在虚拟机之前,修改Menu OS根目录下的Makefile文件,在后面追加-append nokaslr参数:
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd ../rootfs.img -S -s -append nokaslr
准备工作完成后,我们不妨先来看一下,在hello聊天程序中可能调用到的内核函数。以下为linux/net/socket.c源文件中负责请求分派的方法,由宏定义实现。可以看到,用户的各种socket请求最终都被派分给相应的内核函数进行实现。
/* * System call vectors. * * Argument checking cleaned up. Saved 20% in size. * This function doesn't need to set the kernel lock because * it is set by the callees. */ 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; }
以此为据,我们不妨先给这些内核函数都打上断点:
当然,别忘了我们的小程序。
客户端代码如下:
#include"syswrapper.h" #define MAX_CONNECT_QUEUE 1024 int main() { char szBuf[MAX_BUF_LEN] = "\0"; char szMsg[MAX_BUF_LEN] = "hello\0"; OpenRemoteService(); SendMsg(szMsg); RecvMsg(szBuf); CloseRemoteService(); return 0; }
服务端代码如下:
#include"syswrapper.h" #define MAX_CONNECT_QUEUE 1024 int main() { 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; }
具体的实现通过宏定义:
#ifndef _SYS_WRAPER_H_ #define _SYS_WRAPER_H_ #include#include /* internet socket */ #include<string.h> //#define NDEBUG #include #define PORT 5001 #define IP_ADDR "127.0.0.1" #define MAX_BUF_LEN 1024 /* private macro */ #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); #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; \ } /* public macro */ #define InitializeService() \ PrepareSocket(IP_ADDR,PORT); \ InitServer(); #define ShutdownService() \ close(sockfd); #define OpenRemoteService() \ PrepareSocket(IP_ADDR,PORT); \ InitClient(); \ int newfd = sockfd; #define CloseRemoteService() \ close(sockfd); 断电 #define ServiceStart() \ int newfd = accept( sockfd, \ (struct sockaddr *)&clientaddr, \ &addr_len); \ if(newfd == -1) \ { \ fprintf(stderr,"Accept Error,%s:%d\n", \ __FILE__,__LINE__); \ } #define ServiceStop() \ close(newfd); #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)); \ } #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)); \ } #endif /* _SYS_WRAPER_H_ */
那么下面让程序跑起来吧。
首先在虚拟机终端中输入以下命令启动服务器。
$ replyhi
观察gdb调试窗口,可以看到程序首先在断点:__sys_socket 处暂停,该内核函数对应于用户程序中socket创建代码:
sockfd = socket(PF_INET,SOCK_STREAM,0);
在gdb终端输入指令 c 继续replyhi程序的运行:
$ (gdb) c
这时程序止步于断点: __sys_bind 处,这是由用户程序对bind的调用引起。
int ret = bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr));
此时服务器端的socket初始化完成:端口已经绑定。虚拟机控制台等待用户新的命令输入。这时我们继续执行hello程序:
$ hello
int ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr));
最后程序来到断点:__sys_accept4处,这对应服务端使用的accept函数:
int newfd = accept( sockfd, (struct sockaddr *)&clientaddr, &addr_len);
总体上来说,socket编程的api函数与内核实现函数有着很好的对应关系。