zoukankan      html  css  js  c++  java
  • 深入理解Linux系统调用

    一.为何要有系统调用

    unix内核分为用户态和内核态,在用户态下程序不内直接访问内核数据结构或者内核程序,只有在内核态下才可访问。请求内核服务的进程使用系统调用的特殊机制,每个系统调用都设置了一组识别进程请求的参数,通过执行CPU指令完成用户态向内核态的转换。

    二.系统调用过程

    32位系统中,通过int $0x80指令触发系统调用。其中EAX寄存器用于传递系统调用号,参数按顺序赋值给EBX、ECX、EDX、ESI、EDI、EBP这6个寄存器。

    64位系统则是使用syscall指令来触发系统调用,同样使用EAX寄存器传递系统调用号,RDI、RSI、RDX、RCX、R8、R9这6个寄存器则用来传递参数。

    下面以64位系统中的42号,connect系统调用作为例子

    connect是socket网络通信中的函数,是客户端与服务端连接时所用到的函数,connect接受三个参数,分别是客户端的文件描述符,sockaddr结构体,以及地址长度(ipv4为4)。若成功连接,返回0,否则返回-1

    下面是客户端的源代码

    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netdb.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <memory.h>
    #include <unistd.h>
    #include "rio.h"
    #define MAXLINE 100
    
    int open_clientfd(char*,char*);
    
    int main(int argc,char** argv){
        int clientfd;
        char* host,*port,buf[MAXLINE];
        rio_t rio;
        if(argc != 3){
            fprintf(stderr,"usage: %s <host> <port>\n",argv[0]);
            exit(0);
        }
        host = argv[1];
        port = argv[2];
    
        clientfd = open_clientfd(host,port);
        rio_readinitb(&rio,clientfd);
        while(fgets(buf,MAXLINE,stdin)!=NULL){
            rio_writen(clientfd,buf,strlen(buf));
            rio_readlineb(&rio,buf,MAXLINE);
            fputs(buf,stdout);
        }
        close(clientfd);
        exit(0);
    }
    
    int open_clientfd(char* hostname,char* port){
        int clientfd;
        struct addrinfo hints,*listp,*p;
        memset(&hints,0,sizeof(struct addrinfo));
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_flags = AI_NUMERICSERV;
        hints.ai_flags |= AI_ADDRCONFIG;
        getaddrinfo(hostname,port,&hints,&listp);
        //getaddrinfo会返回所有可用的套接字
        for(p=listp;p;p=p->ai_next){
            if((clientfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0)
                continue;
            if(connect(clientfd,p->ai_addr,p->ai_addrlen)!=-1)//参数分别为客户端的文件描述符,addr地址结构,已经地址长度
                break;//成功建立连接
            close(clientfd);//建立失败,尝试另一个套接字
        }
        freeaddrinfo(listp);
        if(!p) return -1;
        return clientfd;
    }

    服务端是采用基于I/O多路复用的并发事件驱动服务器,基于select函数

      1 #include <sys/socket.h>
      2 #include <sys/types.h>
      3 #include <sys/select.h>
      4 #include <netdb.h>
      5 #include <stdlib.h>
      6 #include <stdio.h>
      7 #include <memory.h>
      8 #include <unistd.h>
      9 #include <errno.h>
     10 #include "rio.h"
     11 
     12 #define LISTENQ 1024
     13 #define MAXLINE 100
     14 
     15 typedef struct{
     16     int maxfd;
     17     fd_set read_set;
     18     fd_set ready_set;
     19     int nready;
     20     int maxi;
     21     int clientfd[FD_SETSIZE];
     22     rio_t clientrio[FD_SETSIZE];
     23 }pool;
     24 
     25 int bytes_cnt = 0;
     26 
     27 int open_listenfd(char*);
     28 void echo(int);
     29 void command();
     30 void init_pool(int,pool*);
     31 void add_client(int,pool*);
     32 void check_clients(pool*);
     33 
     34 int main(int argc,char** argv){
     35     int listenfd,connfd;
     36     socklen_t clientlen;
     37     struct sockaddr_storage clientaddr;
     38     char client_hostname[MAXLINE]; char client_port[MAXLINE];
     39     static pool pool;
     40     
     41     if(argc != 2){
     42         fprintf(stderr,"usage: %s <port>\n",argv[0]);
     43         exit(0);
     44     }
     45 
     46     listenfd = open_listenfd(argv[1]);
     47     init_pool(listenfd,&pool);
     48     
     49     while(1){
     50         pool.ready_set = pool.read_set;
     51         pool.nready = select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
     52 
     53         if(FD_ISSET(listenfd,&pool.ready_set)){
     54             clientlen = sizeof(struct sockaddr_storage);
     55             connfd = accept(listenfd,(struct sockaddr *)&clientaddr,&clientlen);
     56             add_client(connfd,&pool);
     57             getnameinfo((struct sockaddr *)&clientaddr,clientlen,client_hostname,MAXLINE,client_port,MAXLINE,0);
     58             printf("连接到:(%s,%s)\n",client_hostname,client_port);
     59         }
     60         check_clients(&pool);
     61     }
     62 }
     63 
     64 int open_listenfd(char* port){
     65     int listenfd; int optval = 1;
     66     struct addrinfo hints,*listp,*p;
     67     memset(&hints,0,sizeof(struct addrinfo));
     68     hints.ai_socktype = SOCK_STREAM;
     69     hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
     70     hints.ai_flags |= AI_NUMERICSERV;
     71     getaddrinfo(NULL,port,&hints,&listp);
     72     for(p=listp;p;p=p->ai_next){
     73         if((listenfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0) 
     74             continue;
     75         setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(const void*)&optval,sizeof(int));
     76         if(bind(listenfd,p->ai_addr,p->ai_addrlen)==0)
     77             break;
     78         close(listenfd);
     79     }
     80     freeaddrinfo(listp);
     81     if(!p) return -1;
     82     //建立成功,开始监听
     83     //LISTENQ是等待的连接请求队列
     84     if(listen(listenfd,LISTENQ)<0){
     85         close(listenfd);
     86         return -1;
     87     }
     88     return listenfd;
     89 }
     90 
     91 void echo(int connfd){
     92     size_t n;
     93     char buf[MAXLINE];
     94     rio_t rio;
     95     rio_readinitb(&rio,connfd);
     96     while((n = rio_readlineb(&rio,buf,MAXLINE)) != 0){
     97         printf("服务器接受到: %d 字节\n",(int)n);
     98         printf("%s\n",buf);
     99         rio_writen(connfd,buf,n);
    100     }
    101 }
    102 
    103 void command(){
    104     char buf[MAXLINE];
    105     if(!fgets(buf,MAXLINE,stdin))
    106         exit(0);
    107     printf("%s",buf);
    108 }
    109 
    110 void init_pool(int listenfd,pool* p){
    111     int i;
    112     p->maxi = -1;
    113     for(i=0;i<FD_SETSIZE;i++)
    114         p->clientfd[i]=-1;
    115     p->maxfd = listenfd;
    116     FD_ZERO(&p->read_set);
    117     FD_SET(listenfd,&p->read_set);
    118 }
    119 
    120 void add_client(int connfd,pool* p){
    121     int i;
    122     p->nready--;
    123     for(i=0;i<FD_SETSIZE;i++){
    124         if(p->clientfd[i]<0){
    125             p->clientfd[i] = connfd;
    126             rio_readinitb(&p->clientrio[i],connfd);
    127 
    128             FD_SET(connfd,&p->read_set);
    129             if(connfd > p->maxfd)
    130                 p->maxfd = connfd;
    131             if(i > p->maxi)
    132                 p->maxi = i;
    133             break;
    134         }
    135     }
    136     if(i == FD_SETSIZE)
    137         printf("add_client error: 客户端过多");
    138 }
    139 
    140 void check_clients(pool* p){
    141     int i,connfd,n;
    142     char buf[MAXLINE];
    143     rio_t rio;
    144     for(i=0;i<=p->maxi && p->nready>0;i++){
    145         connfd = p->clientfd[i];
    146         rio = p->clientrio[i];
    147 
    148         if((connfd>0) && (FD_ISSET(connfd,&p->ready_set))){
    149             p->nready--;
    150             if((n = rio_readlineb(&rio,buf,MAXLINE))!=0){
    151                 bytes_cnt += n;
    152                 printf("服务器收到 %d (总共%d) 字节 在 文件描述符%d ",n,bytes_cnt,connfd);
    153                 rio_writen(connfd,buf,n);
    154                 printf("内容:%s\n",buf);
    155             }
    156             else{
    157                 close(connfd);
    158                 FD_CLR(connfd,&p->read_set);
    159                 p->clientfd[i] = -1;
    160             }
    161         }
    162     }
    163 }

    修改connect函数,以汇编指令的形式进入系统调用

    通过gdb查看connect函数传参用到的寄存器

    其中connect的系统调用号为0x2a

     

            asm volatile(
                         "movl %1,%%edi\n\t"
                         "movq %2,%%rsi\n\t"
                         "movl %3,%%edx\n\t"
                         "movl $0x2a,%%eax\n\t"
                         "syscall\n\t"
                         "movq %%rax,%0\n\t"
                         :"=m"(ret)
                         :"a"(clientfd),"b"(p->ai_addr),"c"(p->ai_addrlen)
            );

    测试是否通过汇编正常调用connect函数,服务端监听45678端口

     客户端试图连接到45678端口

    看来是可以正常触发的,其中50962是客户端进程的端口号

    接下来重新静态编译客户端程序 gcc clis.c -o ciis -static,如果不是静态编译,在qemu下是不能正常运行的,提示. /not found(缺少lib动态链接库)

    然后重新打包系统根目录rootfs

    打开qemu,通过gdb在entry_SYSCALL_64处打断点

     进入home目录后,执行./ciis localhost 1256

    由于每次按下键盘都会触发一个中断,每个中断都会进入断点,所以调试的过程非常慢

    进入entry_syscall后,会保存寄存器的值到pt_regs结构体中

    ENTRY(entry_SYSCALL_64)
        UNWIND_HINT_EMPTY
        /*
         * Interrupts are off on entry.
         * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
         * it is too small to ever cause noticeable irq latency.
         */
    
        swapgs
        /* tss.sp2 is scratch space. */
        movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
        SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
        movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
    
        /* Construct struct pt_regs on stack */
        pushq    $__USER_DS                /* pt_regs->ss */
        pushq    PER_CPU_VAR(cpu_tss_rw + TSS_sp2)    /* pt_regs->sp */
        pushq    %r11                    /* pt_regs->flags */
        pushq    $__USER_CS                /* pt_regs->cs */
        pushq    %rcx                    /* pt_regs->ip */
    GLOBAL(entry_SYSCALL_64_after_hwframe)
        pushq    %rax                    /* pt_regs->orig_ax */

    进入entry_syscalll_64后,会保存现在寄存器的值放入pt_regs结构体中

    继续单步执行,执行call dosyscallc_64函数

    do_syscall64定义在common.c中

    regs->ax = sys_call_table[nr](regs);

    这句会查找对应的系统调用号,然后传入regs结构体,regs中保存着各个寄存器的值,之后会把调用返回值传给ax寄存器

     最后会执行sysret指令恢复堆栈

    USERGS_SYSERT64是个宏展开,其扩展调用 swapgs 指令交换用户 GS 和内核GS, sysret 指令执行从系统调用处理退出

    至此,一段系统调用结束

    总结

    操作系统对于中断处理流程一般为:

    1. 关中断:CPU关闭中段响应,即不再接受其它外部中断请求
    2. 保存断点:将发生中断处的指令地址压入堆栈,以使中断处理完后能正确地返回。
    3. 识别中断源:CPU识别中断的来源,确定中断类型号,从而找到相应的中断服务程序的入口地址。
    4. 保护现场所:将发生中断处理有关寄存器(中断服务程序中要使用的寄存器)以及标志寄存器的内存压入堆栈。
    5. 执行中断服务程序:转到中断服务程序入口开始执行,可在适当时刻重新开放中断,以便允许响应较高优先级的外部中断。
    6. 恢复现场并返回:把“保护现场”时压入堆栈的信息弹回原寄存器,然后执行中断返回指令(IRET),从而返回主程序继续运行。

    在内核初始化时,会执行trap_init函数,把中断向量表拷贝到指定位置,syscall_64.c中定义着系统调用表sys_call_table,在cpu_init时完成初始化。执行int 0x80时,硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。

    syscall则是64位系统中,为了加速系统调用通过引入新的 MSR 来存放内核态的代码和栈的段号和偏移量,从而实现快速跳转。

  • 相关阅读:
    Android Lock Screen Orientation
    android studio修改新项目package名称
    DevExpress控件GridView挂下拉控件无法对上值
    Datawindow.net+access数据窗口制作方法
    LR11中web_save_timestamp_param
    LR自带网站飞机订票系统 启动
    MySQL ODBC驱动安装和配置数据源
    【笔记】LR配置ODBC连接数据库进行参数化(mysql )未完待续
    【笔记】LR录制方式和常用函数
    【笔记】http1.1支持的7种请求方法
  • 原文地址:https://www.cnblogs.com/ycw0923/p/12913925.html
Copyright © 2011-2022 走看看