在我們寫的socket程序中,一般都會使用網絡套接字API,比如send()、accept()、receive()等函數,那麼這些函數在Linux操作系統中是如何實現的呢?我們將通過分析代碼對其進行深入的研究。本文中所使用的Glibc庫版本爲2.0.111,Linux內核版本爲Linux 1.2.13。
1.用戶層代碼
在程序中我們使用了accept()函數,這個函數的實現是在Glibc庫,即GNU的C庫中實現的。具體代碼是:
1 //glibc-2.0.111\sysdeps\unix\sysv\linux\accept.S 2 #define socket accept 3 #define __socket __libc_accept 4 #define NARGS 3 5 #include <socket.S>
這段與socket.S是accept()從用戶態進入內核態的關鍵代碼。accept.S中將accept定義爲socket,__socket定義爲__libc_accpet,NARGS定義爲3,表示調用參數有3個。接下來包含了socket.S文件。Socket.S的作用與功能按照源文件中的註釋所述:
/* The socket-oriented system calls are handled unusally in Linux. They are all gated through the single `socketcall' system call number. `socketcall' takes two arguments: the first is the subcode, specifying which socket function is being called; and the second is a pointer to the arguments to the specific function. The .S files for the other calls just #define socket and #include this. */
翻譯成中文的大概意思是:
socket系列的系統函數經常被調用。
他們都通過單一的一個socketcall系統調用號(進行調用)。
Socketcall有兩個參數:第一個是子調用碼,指定了哪一個socket函數被調用;第二個參數是一個指向被調用的socket函數所需參數的指針。
其他的(socket系列的)函數的.S文件只需要#define socket 爲某個值和#include 這個文件(指此socket.S)即可
在socket.S中進行了進一步的調用,socket從用戶態進行相應參數的設置,然後使用int指令自陷,調用操作系統提供的中斷服務程序,在內核態執行相應的系統服務,我們將整個函數的代碼粘貼進來,在具體的語句上進行註釋解釋:
1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\socket.S 2 #include <sysdep.h> 3 #include <socketcall.h> 4 // 定義了P(a,b)與P2(a,b)兩個宏,他們的作用都是將a與b連接到一起。 5 #define P(a, b) P2(a, b) 6 #define P2(a, b) a##b 7 8 .text 9 10 #ifndef __socket 11 #ifndef NO_WEAK_ALIAS 12 #define __socket P(__,socket) 13 #else 14 #define __socket socket 15 #endif 16 #endif 17 18 .globl __socket 19 ENTRY (__socket) //這裏開始進行函數的處理 20 21 22 /* 保存ebx的值 */ 23 movl %ebx, %edx 24 25 // SYS_ify宏在sysdep.h中定義。一會兒詳細瞭解它的作用 26 // 下面一條語句的作用是將socketcall的調用號存入寄存器eax 27 movl $SYS_ify(socketcall), %eax /* System call number in %eax. */ 28 29 /* 子調用號放入ebx中,關於下面一條語句的將在下面有詳細解釋 */ 30 movl $P(SOCKOP_,socket), %ebx /* Subcode is first arg to syscall. */ 31 /* 指向調用參數的指針放入ecx中 */ 32 lea 4(%esp), %ecx /* Address of args is 2nd arg. */ 33 34 /* 0x80中斷,自陷進入內核態 */ 35 int $0x80 36 37 /* 恢復ebx寄存器的值 */ 38 movl %edx, %ebx 39 40 /* eax是返回值,如果<0則表示調用出錯,就跳到錯誤處理的代碼中去 */ 41 cmpl $-125, %eax 42 jae SYSCALL_ERROR_LABEL 43 44 /* 成功的話就返回相應的返回值 */ 45 L(pseudo_end): 46 ret 47 48 PSEUDO_END (__socket) 49 50 #ifndef NO_WEAK_ALIAS 51 weak_alias (__socket, socket) 52 #endif
我們首先看movl $SYS_ify(socketcall), %eax這一條語句。SYS_ify在sysdep.h中定義,但是有兩個不同文件夾下的sysdep.h文件。
(1)
按照文件層次來講,應該是按照如下的代碼進行:
1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\sysdep.h 2 ..... 3 #undef SYS_ify 4 #define SYS_ify(syscall_name) __NR_##syscall_name 5 .....
在這段代碼之前有一段註釋:
/* For Linux we can use the system call table in the header file /usr/include/asm/unistd.h of the kernel. But these symbols do not follow the SYS_* syntax so we have to redefine the `SYS_ify' macro here. */
中文的大概意思是:
對於Linux系統,我們可以使用在/usr/include/asm/unistd.h頭文件中的內核系統調用表。
但是這些符號並不是以SYS_符號爲前綴的,所以這裏我們必須重定義SYS_ify宏。
可以看到,通過SYS_ify(socketcall),我們得到了__NR_socketcall。
(2)
按照另外一本書上所講的,在下列位置中存在另外一套代碼:
1 // glibc-2.0.111\sysdeps\unix\sysdep.h 2 …… 3 #ifdef __STDC__ 4 #define SYS_ify(syscall_name) SYS_##syscall_name 5 #else 6 #define SYS_ify(syscall_name) SYS_/**/syscall_name 7 #endif 8 ……
如果是經由這段代碼的處理,那麼我們將得到SYS_socketcall,那麼這又是一個什麼呢?我們查看源代碼是看不到的。而在實際的操作系統(筆者所使用的是Fedora 14)中,/usr/include /bits/syscall.h中則有相應的答案,這個文件是libc在構建時候根據具體的操作系統而生成的。在其中,會有:
1 #ifndef _SYSCALL_H 2 # error "Never use <bits/syscall.h> directly; include <sys/syscall.h> instead." 3 #endif 4 5 #define SYS__llseek __NR__llseek 6 #define SYS__newselect __NR__newselect 7 #define SYS__sysctl __NR__sysctl 8 #define SYS_access __NR_access 9 #define SYS_acct __NR_acct 10 …… 11 #define SYS_socketcall __NR_socketcall 12 ……
可以看到,通過這一部分的處理之後,最後依然會得到__NR_socketcall。
2.內核態代碼
瞭解Linux系統的人都知道,在/linux/include/linux/unistd.h中,我們可以看到,這些內容:
1 // linux/include/linux/unistd.h 2 …… 3 #define __NR_setup 0 /* used only by init, to get system going */ 4 #define __NR_exit 1 5 #define __NR_fork 2 6 #define __NR_read 3 7 #define __NR_write 4 8 …… 9 #define __NR_socketcall 102 10 ……
我們可以看到,__NR_socketcall被定義爲102,上面一行的代碼即是將eax的值賦成102,即此係統調用的調用號。
下面我們看movl $P(SOCKOP_,socket), %ebx這一句。在socketcall.h中有相應的定義:
1 // glibc-2.0.111\sysdeps\unix\sysv\linux\socketcall.h 2 …… 3 #define SOCKOP_socket 1 4 #define SOCKOP_bind 2 5 #define SOCKOP_connect 3 6 #define SOCKOP_listen 4 7 #define SOCKOP_accept 5 8 #define SOCKOP_getsockname 6 9 #define SOCKOP_getpeername 7 10 #define SOCKOP_socketpair 8 11 #define SOCKOP_send 9 12 #define SOCKOP_recv 10 13 #define SOCKOP_sendto 11 14 #define SOCKOP_recvfrom 12 15 #define SOCKOP_shutdown 13 16 #define SOCKOP_setsockopt 14 17 #define SOCKOP_getsockopt 15 18 #define SOCKOP_sendmsg 16 19 #define SOCKOP_recvmsg 17 20 ……
這一句的意思就是將相應的操作碼賦予ebx,此例中是5。下面我們進入操作系統中的代碼進行分析,在entry.S中,有一段中斷處理函數:
1 // linux/arch/i386/kernel/entry.S 2 _system_call: 3 // 保存eax的值 4 pushl %eax # save orig_eax 5 // 保存所有寄存器的值 6 SAVE_ALL 7 movl $-ENOSYS,EAX(%esp) 8 // 比較eax中的調用號是否超過了限定的數值,NR_syscalls,默認是256。 9 cmpl $(NR_syscalls),%eax # compare whether eax>NR_syscalls 10 jae ret_from_sys_call 11 //從系統調用表中找到對應的入口地址,放入eax中 12 movl _sys_call_table(,%eax,4),%eax 13 testl %eax,%eax 14 je ret_from_sys_call 15 // 子調用號放入ebx中 16 movl _current,%ebx 17 andl $~CF_MASK,EFLAGS(%esp) # clear carry - assume no errors 18 movl $0,errno(%ebx) 19 movl %db6,%edx 20 movl %edx,dbgreg6(%ebx) # save current hardware debugging status 21 testb $0x20,flags(%ebx) # PF_TRACESYS 22 jne 1f 23 // 進行系統調用 24 call *%eax 25 movl %eax,EAX(%esp) # save the return value 26 movl errno(%ebx),%edx 27 negl %edx 28 je ret_from_sys_call 29 movl %edx,EAX(%esp) 30 orl $(CF_MASK),EFLAGS(%esp) # set carry to indicate error 31 jmp ret_from_sys_call
具體的語句的作用已經在代碼中進行了標註。我們接下來可以查看處理socket調用的系統函數socket.c:
1 // linux/net/socket.c 2 …… 3 asmlinkage int sys_socketcall(int call, unsigned long *args) 4 { 5 int er; 6 switch(call) 7 { 8 case SYS_SOCKET: 9 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 10 if(er) 11 return er; 12 return(sock_socket(get_fs_long(args+0), 13 get_fs_long(args+1), 14 get_fs_long(args+2))); 15 …… 16 case SYS_ACCEPT: 17 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 18 if(er) 19 return er; 20 return(sock_accept(get_fs_long(args+0), 21 (struct sockaddr *)get_fs_long(args+1), 22 (int *)get_fs_long(args+2))); 23 ……
這個sys_socketcall函數是socket系列函數的分發函數,根據具體調用號,調用不同的處理函數進行處理,至此,我們看到了整個從應用層socket函數到BSDsocket的層的傳遞過程,加深了我們對於此過程的瞭解。