Socket与系统调用深度分析
一、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<stdio.h>
#include<arpa/inet.h> /* internet socket */
#include<string.h>
//#define NDEBUG
#include<assert.h>
#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()函数)。

浙公网安备 33010602011771号