本文将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 出中止运行呢?为何名称不同???