Socket与系统调用深度分析

Posted ustc-bluesky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。

一、linux系统调用原理

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

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

二、调用流程

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

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

技术图片

 

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

  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] = "";
	char szReplyMsg[MAX_BUF_LEN] = "hi";
	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!
");
	}
	else
	{
		/* 	parent process	 */
		printf("Please input hello...
");
	}
}


int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
    ExecuteMenu();
}

我们发现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
",                                    __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
",                     __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
",                                  __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
",                      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
",                        (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] = "";
    char szMsg[MAX_BUF_LEN] = "hello";
    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
", __FILE__,__LINE__); return -1; }
/*closeRemoteService()也调用了一次系统调用close()函数*/

  根据以上所知,SendMsg(szMsg);RecvMsg(szBuf);这两个函数也分别调用了

技术图片

以上是关于Socket与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket系统调用Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析