深入理解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 來存放內核態的代碼和棧的段號和偏移量,從而實現快速跳轉。

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM