罗晓波 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
本片内容接上一篇,是系统调用的续篇,也就是简要分析一下系统调用的处理过程。同样,以一个实验开始。
本实验同样在实验楼环境下完成。下面先介绍一下实验:
1.实验:
将上一个系统调用函数和asm版本的实现整合进入menu的内核中:
int GetPid()
{
int pid = getpid();
printf("The Current Progress pid is : %d\n",pid);
return 0;
}
int GetPidAsm()
{
int pid;
asm volatile(
"mov $0,%%ebx\n\t"
"mov $0x14,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
: "=m" (pid)
);
printf("The Current Progress pid asm is : %d\n",pid);
return 0;
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
MenuConfig("getpid","Show Current Progress id",GetPid);
MenuConfig("getpid-asm","Show Current Progress asm id",GetPidAsm);
ExecuteMenu();
}
简单介绍一下上面的一段代码,MenuConfig这个函数是菜单(也就是制作出来的内核)的初始化配置函数,第一个参数是命令,第二个参数是该命令的描述,第三个参数是这个命令相对应的handler,也就是回调函数,是通过一个函数指针进行实现的。ExecuteMenu这个函数是为了启动这个menu引擎,其实是一个循环等待用户输入命令的过程。
在添加了getpid和getpid-asm这两个命令之后,make一下,再重新编译一下内核,make rootfs。再启动一下qemu:
接下来,我们开始调试这个系统调用函数,对应着getpid这个系统调用,在sys_getpid这个内核函数这里打一个断点。continue之后,输入getpid-asm或者getpid这个命令会停在sys_getpid();
可以看到,在kernel/sys.c中有这么一个sys_getpid()内核函数,这个也就是系统调用的服务例程。跟进这个task_tgid_vnr(current)函数,之后finish之后的返回值是1,也就是init进程的进程号。
当然打印出来的current pid 也是1 。
有点遗憾的是我没有找到让gdb在systemcall这个汇编代码处停止的办法,希望小伙伴可以友情提示。下面来进一步分析从systemcall到系统调用结束之后,iret之间的过程。
2.从systemcall到iret过程:
Systemcall内核源码(汇编代码)进行简要分析:
490ENTRY(system_call)
491 RING0_INT_FRAME # can't unwind into user space anyway
492 ASM_CLAC
493 pushl_cfi %eax # save orig_eax
494 SAVE_ALL
495 GET_THREAD_INFO(%ebp)
496 # system call tracing in operation / emulation
497 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
498 jnz syscall_trace_entry
499 cmpl $(NR_syscalls), %eax
500 jae syscall_badsys
首先在内核栈中,压栈eax,也就是用户态传递给内核态的系统调用号,之后SaveAll这个指令就开始保存现场了,这里GET_THREAD_INFO(%ebp)是将这个进程的threadinfo结构体的地址指针放在了ebp寄存器里,下面对于这个结构体的访问也就可以顺理成章的用ebp的地址偏移进行访问就行了。比如下一句的TI_flags这个宏代表的其实就是一个offset,这个testl 指令主要是为了检查是否存在调试程序对这个系统调用正在调试,如果是,则跳转到syscall_trace_entry标签处,这个标签我一会来分析。接着往下看,cmpl $(NR_syscalls),%eax 这个指令主要是为了防止eax传递参数越界了,也就是超过了系统调用符号表中的值,如果出错了就跳到syscall_badsys标签处。
501syscall_call:
502 call *sys_call_table(,%eax,4)
503syscall_after_call:
504 movl %eax,PT_EAX(%esp) # store the return value
505syscall_exit:
506 LOCKDEP_SYS_EXIT
507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
508 # setting need_resched or sigpending
509 # between sampling and the iret
在系统调用退出之前,在restoreall之前,会将threadinfo中的许多标志位进行比较,看是否需要在返回用户态之前做点什么事,比如我们看着一段:
在恢复用户态之前的一段代码:
350 TRACE_IRQS_OFF
351 movl TI_flags(%ebp), %ecx
352 andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
353 # int/exception return?
354 jne work_pending
355 jmp restore_all
这里检查了TIF_WORK_MASK,有其他的int值或者异常要返回?这个时候我们跳到workpending,这个workpending我们贴一下代码看看:
593work_pending:
594 testb $_TIF_NEED_RESCHED, %cl
595 jz work_notifysig
596work_resched:
597 call schedule
598 LOCKDEP_SYS_EXIT
599 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
600 # setting need_resched or sigpending
601 # between sampling and the iret
602 TRACE_IRQS_OFF
603 movl TI_flags(%ebp), %ecx
604 andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
605 # than syscall tracing?
606 jz restore_all
workpending主要是为了检测重调度标志,下面的work_resched标签是如果进程切换的请求被挂起了,辣么,在内核态开始一次进程调度,选择一个进程运行,当前面的进程要恢复的时候在跳转回到resume_userpace处,也即回到用户态的标签处。
在restoreall之后,就将中断的程序被恢复了。 下面是一张简要流程图: