本文將Socket API編程接口、系統調用機制及內核中系統調用相關源代碼、 socket相關係統調用的內核處理函數結合起來分析,並在X86 64環境下Linux5.0以上的內核中進一步跟蹤驗證。linux
1. 系統調用的初始化git
在加電啓動BootLoader運行後,BootLoader對硬件初始化並把內核加載進內存而後將參數傳給內核後,內核將接過系統控制權開始運行,而內核運行的第一個函數入口就是start_kernel這個入口函數。它將進行一系列初始化,其中就包括系統調用初始化,內核最後建立系統的第0號進程rest_init,它作的一件事情是從根文件系統尋找init函數做爲系統第1號進程運行,rest_init則退化爲系統空閒時候運行的idle進程。內核啓動過程流程大體以下:github
而在start_kernel衆多初始化中,有一項初始化 tarp_init()即系統調用初始化,涉及到一些初始化中斷向量,能夠看到它在set_intr_gate
設置到不少的中斷門,不少的硬件中斷,其中有一個系統陷阱門,進行系統調用的。以後還有idt_setup_tarps()即初始化中斷描述表初始化。使用gdb驗證以下:編程
系統調用初始化完成後,咱們的TCP/IP協議棧怎麼加載進內核的呢?咱們看看rest_init源碼:app
393static noinline void __init_refok rest_init(void) 394{ 395 int pid; 396 397 rcu_scheduler_starting(); 398 /* 399 * We need to spawn init first so that it obtains pid 1, however 400 * the init task will end up wanting to create kthreads, which, if 401 * we schedule it before we create kthreadd, will OOPS. 402 */ 403 kernel_thread(kernel_init, NULL, CLONE_FS); 404 numa_default_policy(); 405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 406 rcu_read_lock(); 407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 408 rcu_read_unlock(); 409 complete(&kthreadd_done); 410 411 /* 412 * The boot idle thread must execute schedule() 413 * at least once to get things moving: 414 */ 415 init_idle_bootup_task(current); 416 schedule_preempt_disabled(); 417 /* Call into cpu_idle with preempt disabled */ 418 cpu_startup_entry(CPUHP_ONLINE); 419}
經過rest_init()
新建kernel_init
、kthreadd
內核線程。403行代碼 kernel_thread(kernel_init, NULL, CLONE_FS);
,由註釋得調用 kernel_thread()
建立1號內核線程(在kernel_init
函數正式啓動),kernel_init
函數啓動了init用戶程序。另外405行代碼 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
調用kernel_thread
執行kthreadd
,建立PID爲2的內核線程。rest_init()
最後調用cpu_idle()
演變成了idle進程。更多細節參考:https://github.com/mengning/net/blob/master/doc/tcpip.mdsocket
至此係統調用初始化完成並且socket系統調用的中斷處理程序(TCP/IP協議棧)也已經註冊好了。tcp
2. 用戶態程序發起系統調用函數
當用戶程序調用socket()這個API時,glibc庫中會轉爲調用 __socket()函數,爲何要作這樣一步呢?覺得這樣可讓你們統一隻包括glic這個庫不用再關心其餘頭文件,而實現glic這個髒活累活交給系統級開發人員幹。_socket()這個函數定義在socket.s這個彙編文件中,它完成參數的傳遞,而後ENTER_KERNEL進入內核,代碼以下:spa
movl $SYS_ify(socketcall), %eax /* System call number in %eax. */ /* Use ## so `socket' is a separate token that might be #define'd. */ movl $P(SOCKOP_,socket), %ebx /* Subcode is first arg to syscall. */ lea 4(%esp), %ecx /* Address of args is 2nd arg. */ /* Do the system call trap. */ ENTER_KERNEL
其中線程
SYS_ify宏定義爲
#define SYS_ify(syscall_name) __NR_##syscall_name;
P宏定義爲
#define P(a, b) P2(a, b) #define P2(a, b) a##b
##爲鏈接符號。
#define __NR_socketcall 102
#define SOCKOP_socket 1
所以,中斷號是102,子中斷號是1;
而ENTER_KERNEL是什麼呢?爲啥它就能進入內呢?請看下面定義:
# define ENTER_KERNEL int $0x80
int $0x80是x86的軟中斷指令,使用它會使系統進入內核模式,也就是所謂的內陷。
該指令會跳轉到system_call中斷入口在kernel/arch/x86/kernel/entry_32.S:
syscall_call: call *sys_call_table(,%eax,4)
該指令又會跳轉到對應的
中斷向量表102號中斷:
.long sys_socketcall
進入sys_socketcall()函數,根據子中斷號(socket是1)以決定走哪一個分支:kernel/net/Socket.c:
switch (call) { case : break; case SYS_BIND: …...
上面這張圖是open()的系統調用示意圖,socket與之相似。
3.gdb跟蹤驗證
因爲咱們已經在menu os中集成了replyhi和hello兩個程序,這兩個通訊程序就使用了socket()API,以下圖:
int Replyhi() { char szBuf[MAX_BUF_LEN] = "\0"; char szReplyMsg[MAX_BUF_LEN] = "hi\0"; 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(IP_ADDR); memset(&serveraddr.sin_zero, 0, 8); sockfd = socket(PF_INET,SOCK_STREAM,0); int ret = bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)); if(ret == -1) { fprintf(stderr,"Bind Error,%s:%d\n", __FILE__,__LINE__); close(sockfd); return -1; } listen(sockfd,MAX_CONNECT_QUEUE); while(1) { int newfd = accept( sockfd, (struct sockaddr *)&clientaddr, &addr_len); if(newfd == -1) { fprintf(stderr,"Accept Error,%s:%d\n", __FILE__,__LINE__); } ret = recv(newfd,szBuf,MAX_BUF_LEN,0); if(ret > 0) { printf("recv \"%s\" from %s:%d\n", szBuf, (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); \ } ret = send(newfd,szReplyMsg,strlen(szReplyMsg),0); if(ret > 0) { printf("rely \"hi\" to %s:%d\n", (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); \ } close(newfd); } close(sockfd); return 0; }
其中使用了socket,bind, listen, accpet, recv, send, close等API,都會發生系統調用,咱們只跟蹤socket()這個API的調用棧來驗證便可。運行以下命令:
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s -S
而後,按照咱們前面的socketAPI系統調用棧來打斷點
gdb file vmlinux b sys_call b sys_socketcall b call *syacall_call_table(,%eax,4) b SYS_SOCKET target remote:1234
最後發現
只有sys_socketcall斷點成功,其餘都直接編譯處理掉了。繼續運行內核,出現以下結果:
能夠看到內核啓動過程三次調用sys_socketcall,並且都是子終端號call=1,即SYS_SOCKET中斷處理程序分配了3個socket套接字,用於Bring up interface:lo 和 Bring up interface: etho和List all interfaces。(具體我也不知道幹啥的,之後再探究)
而後內核加載完,咱們在menu os裏面運行 replyhi 程序,能夠看到又發生4次系統調用:
上面要對着子中斷號call具體是什麼,再結合replyhi程序源碼來分析,這裏就先到這兒,至少系統調用棧的驗證算是成功了。
不過這裏有一處好奇的地方:我打的斷點是sys_socketcall, 爲何實際上它在__se_sys_socketcall 出中止運行呢?爲何名稱不同???