一.為何要有系統調用
unix內核分為用戶態和內核態,在用戶態下程序不內直接訪問內核數據結構或者內核程序,只有在內核態下才可訪問。請求內核服務的進程使用系統調用的特殊機制,每個系統調用都設置了一組識別進程請求的參數,通過執行CPU指令完成用戶態向內核態的轉換。
二.系統調用過程
32位系統中,通過int $0x80指令觸發系統調用。其中EAX寄存器用於傳遞系統調用號,參數按順序賦值給EBX、ECX、EDX、ESI、EDI、EBP這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 指令執行從系統調用處理退出
至此,一段系統調用結束
總結
操作系統對於中斷處理流程一般為:
- 關中斷:CPU關閉中段響應,即不再接受其它外部中斷請求
- 保存斷點:將發生中斷處的指令地址壓入堆棧,以使中斷處理完后能正確地返回。
- 識別中斷源:CPU識別中斷的來源,確定中斷類型號,從而找到相應的中斷服務程序的入口地址。
- 保護現場所:將發生中斷處理有關寄存器(中斷服務程序中要使用的寄存器)以及標志寄存器的內存壓入堆棧。
- 執行中斷服務程序:轉到中斷服務程序入口開始執行,可在適當時刻重新開放中斷,以便允許響應較高優先級的外部中斷。
- 恢復現場並返回:把“保護現場”時壓入堆棧的信息彈回原寄存器,然后執行中斷返回指令(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 來存放內核態的代碼和棧的段號和偏移量,從而實現快速跳轉。