Socket与系统调用深度分析

一、linux系统调用原理

操作系统通过系统调用为运行于其上的进程提供服务。

当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

二、调用流程

那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

Socket与系统调用深度分析_第1张图片

 

如上图,系统调用执行的流程如下:

  1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
  2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
  3. CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
  4. 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

 三、执行态切换

应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。

Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。

内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

总结起来, 执行态切换 过程如下:

  1. 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
  2. CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
  3. 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
  4. 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
  5. 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
  6. 系统调用处理函数 执行 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与系统调用深度分析_第2张图片

 

 由此可知,我们可以所有的socket系统调用的总入口是sys_socketcall(),在include/linux/Syscalls.h中定义

Socket与系统调用深度分析_第3张图片

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处打断点。如下图所示

Socket与系统调用深度分析_第4张图片

 

 然后执行replyhi

Socket与系统调用深度分析_第5张图片

 

 发现依次调用了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.

Socket与系统调用深度分析_第6张图片

 

 首先在客户端输入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出现了来自客户端的消息。

 Socket与系统调用深度分析_第7张图片

接着第六个call值为9,是服务器中的send()函数,将消息发送给客户端。

第七个call值为5,是服务器中ServiceStart();函数(宏定义展开后就是accept()函数)。

你可能感兴趣的:(Socket与系统调用深度分析)