一、linux系统调用原理
操作系统通过系统调用为运行于其上的进程提供服务。
当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。
二、调用流程
那么,在应用程序内,调用一个系统调用的流程是怎样的呢?
我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。
如上图,系统调用执行的流程如下:
- 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
- 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
- CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
- 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
三、执行态切换
应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。
Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。
内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。
总结起来, 执行态切换 过程如下:
- 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
- CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
- 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
- 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
- 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
- 系统调用处理函数 执行 ret 指令切换回 用户态
四、实验
在我们的实验中,我们创建的是一个利用socket的基于TCP的连接,接下来我们结合源码,接口来进行整个hello/hi的实现过程的调用分析与追踪。上次实验我们实现了menuos的调试环境的配置。
首先利用qemu模拟器并结合gdb调试器来调试linux内核。其中qemu模拟器和gdb的使用这里就不一一详细叙述了。首先看看刚进入系统之后的部分源代码
/test.c
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; } 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"); } } 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(); }
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
我们发现Replyhi函数中,依次调用了InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()以及最后的ShutdownService()函数,我们依次来看这些函数究竟是如何调用socket API的。
#ifndef _SYS_WRAPER_H_ #define _SYS_WRAPER_H_ #include#include /* internet socket */ #include //#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("rely \"hi\" to %s:%d\n", \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ } #endif /* _SYS_WRAPER_H_ */
首先调用InitializeService(),根据宏定义,最后调用了socket(),bind函数,listen(),这些是是socket编程的一般步骤,可以到网络上查找相应的编程。
然后调用ServiceStart()函数,通过宏定义,调用了accept()函数。然后是RecvMsg()和SendMsg()函数,根据宏定义,调用了recv和send函数
在这里要调试一下怎么进入内核的。下面图片是老师给的系统调用表。
由此可知,我们可以所有的socket系统调用的总入口是sys_socketcall(),在include/linux/Syscalls.h中定义
sys_socketcall()在./net/socket.c中定义,它的部分代码段如下:
… … 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; ……
参数call是具体的操作码,参数args是一个数组指针,我们需要明确从用户空间复制的参数数量,这是根据nargs[]来决定的,以call为下标将会从该数组中找到参数的个数,依据个数来把args处的参数从用户空间即我们的应用程序复制过来。其中接口编号定义在 include/uapi/linux/net.h中
#define NPROTO AF_MAX #define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ #define SYS_CONNECT 3 /* sys_connect(2) */ #define SYS_LISTEN 4 /* sys_listen(2) */ #define SYS_ACCEPT 5 /* sys_accept(2) */ #define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */ #define SYS_GETPEERNAME 7 /* sys_getpeername(2) */ #define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */ #define SYS_SEND 9 /* sys_send(2) */ #define SYS_RECV 10 /* sys_recv(2) */ #define SYS_SENDTO 11 /* sys_sendto(2) */ #define SYS_RECVFROM 12 /* sys_recvfrom(2) */ #define SYS_SHUTDOWN 13 /* sys_shutdown(2) */ #define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */ #define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */ #define SYS_SENDMSG 16 /* sys_sendmsg(2) */ #define SYS_RECVMSG 17 /* sys_recvmsg(2) */ #define SYS_ACCEPT4 18 /* sys_accept4(2) */ #define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */ #define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */其中
#ifdef __ARCH_WANT_SYS_SOCKETCALL /* Argument list sizes for sys_socketcall */ #define AL(x) ((x) * sizeof(unsigned long)) static const unsigned char nargs[21] = { AL(0), AL(3), AL(3), AL(3), AL(2), AL(3), AL(3), AL(3), AL(4), AL(4), AL(4), AL(6), AL(6), AL(2), AL(5), AL(5), AL(3), AL(3), AL(4), AL(5), AL(4) };
系统在调用了sys_socketcall()函数后,根据call参数调用sys_socket()函数
asmlinkage long sys_socket(int family, int type, int protocol) { int retval; ...... retval = sock_create(family, type, protocol, &sock);/*jimix*/ ...... }
然后接着上一个实验,在sys_socketcall处打断点。如下图所示
然后执行replyhi
发现依次调用了4次sys_socketcall,其中call参数的值依次为1、2、4、5,根据接口编号定义和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; }
可知依次执行__sys_socket、__sys_bind、__sys_listen、
__sys_accept
当我们在qemu模拟器中输入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; } -------------------------------------------------------------------------- /*这是 OpenRemoteService() 的宏定义,其中PrepareSocket(IP_ADDR,PORT); 和上文一样依次使用socket()函数使用了一次系统调用。*/ #define OpenRemoteService() \ PrepareSocket(IP_ADDR,PORT); \ InitClient(); \ int newfd = sockfd; ------------------------------------------------ /*InitClient()使用connect()使用了一次系统调用*/ #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; \ }
/*closeRemoteService()也调用了一次系统调用close()函数*/
#define CloseRemoteService() \
close(sockfd);
/*syswrapper.h*/ /*RecvMeg函数中使用了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)); \ }
根据以上所知,hello()函数依次使用了socket()->connect()->send()->recv(),等这些函数对应的call参数的值依次是1->3->9->10.
首先在客户端输入hello,然后gdb中断点处出现了call值为1的socket()函数,紧接着就是call值为3的connect()函数,这是客户端hello函数中的一个OpenRemoteService();语句(宏定义展开后分别是socket()和connect()函数)
第三个call值为9,这是客户端中的send()函数,发送消息给客户端。有另外一种情况出现这个call值也有可能是10,因为多进程缘故,服务器一直在等待消息,可以执行recv()函数,只是没有消息收到
第四个call值为10,这是客户端中的recv()函数,此时客户端没有收到服务器的消息,所以这个时候qemu没有消息出来
第五个call值为10,这个是服务器中的recv()函数,因为此时qemu出现了来自客户端的消息。
接着第六个call值为9,是服务器中的send()函数,将消息发送给客户端。
第七个call值为5,是服务器中ServiceStart();函数(宏定义展开后就是accept()函数)。