linux内核分析总结

来源:互联网 发布:vs2010多线程编程实例 编辑:程序博客网 时间:2024/06/13 02:38

版权声明:本文为博主原创文章,未经博主允许不得转载。
姓名:周毅原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
下面是linux内核学习的博客:
1、通过汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
2、完成一个简单的时间片轮转多道程序内核代码
3、分析Linux内核的启动过程
4、使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
5、分析system_call中断处理过程
6、分析Linux内核创建一个新进程的过程
7、Linux内核如何装载和启动一个可执行程序
8、理解进程调度时机跟踪分析进程调度与进程切换的过程
终于完成了孟老师的mooc课程《linux内核分析》,这门课主要从内核代码的角度分析了linux操作系统的启动过程,从第一个进程到之后的进程,进程的定义与管理,运行与切换,系统调用的过程,代码运行时cpu和内存内容的变化等等;
一、对知识总体总结
第一节课从汇编的角度分析了代码运行时内存以及CPU的变化情况,总结如下:
1、计算机一条一条的执行指令;
2、寄存器%EIP总是存着下一条指令所在内存的地址;
3、每条指令执行时,先从eip取指令地址,然后从内存地址处取指令执行;
4、eip总是存下一条指令(取指后eip自动指向下条指令地址);
5、eip不能直接修改,只能通过转跳指令call,ret,jmp等间接修改eip,即下条指令的执行地址;
6、call指令执行时,计算机先将下条指令地址eip存入栈中,然后改变eip地址为需要执行的地址;
7、ret指令执行时,计算机出栈恢复执行call时的下条指令地址至eip,执行下条指令相当于返回执行,所以call,ret总是成对的;
8、ebp往往作为一个函数的基地址,esp作为栈顶地址,所以函数开始往往有push %ebp,mov %esp,%ebp两条指令;
9、leave与第6条相对,往往在ret前出现,相当于pop %ebp,mov %ebp,%esp两条指令,恢复调用前的基址和栈顶指针。

第二节课通过一个简单的时间片轮转法切换系统来分析进程切换情况:
1、每个进程都有自己的进程控制块PCB,PCB包含进程的关键信息如运行状态,进程调度信息,进程处理信息等,是一个进程存在的标志。
2、操作系统主要根据时间片来使多个进程并发运行,每个进程轮流执行一个时间片;
3、当前进程执行完后,即时间片中断来临时需要处理中断,然后切换到下一个可运行的进程;
4、处理时间片中断的过程实际上是保存当前进程的状态(本实验仅保存堆栈指针和中断点),然后恢复下一个进程的状态(堆栈指针和中断点),从而继续运行;

第三节课从内核代码的层面分析了linux内核的启动过程:
1、计算机通电后,CPU开始从一个固定的地址加载代码并开始执行,这个地址就是BIOS的驱动程序所在的位置,于是BIOS的驱动开始执行,BIOS会初始化启动许多硬件(硬盘、网卡等等);
2、BIOS加载硬盘 MBR 中的 GRUB 后,启动过程就被GRUB2接管,GRUB2加载内核和initrd image,并启动内核;
3、内核接管整个系统后,加载/sbin/init并创建第一个用户态的进程,init进程开始调用一系列的脚本来创建很多子进程,这些子进程负责初始化整个系统;
4、start_kernel( )是Linux内核启动的主函数,在init/main.c中定义,主要初始化了一些必要工作,如0号进程init_task,锁,内存,时钟,中断,进程管理等等;
5、init/init_task.c中静态初始化init_task进程;
6、rest_init()调用kernel_init()初始化1号进程init,然后init_task变为dle进程;
7、kernel_init()通过run_init_process()执行init。
总而言之,task 0 的进程结构(task_struct init_task)由INIT_TASK宏静态定义。该结构体(init_task)在linux启动时被设置为current_task。当初始化到rest_init函数中时,通过kernel_thread函数启动第一个内核线程kernel_init。kernel_init再通过do_execve启动/sbin/init。这就是我们看到的init进程,进程号为1。初始化的最后linux调用scheule()整个系统就运行起来了,init_task则变为idle进程。

第四节课从C和汇编分析了系统调用的实现:
1、由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API)。是应用程序同系统之间的接口。
2、当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数;在Linux中是通过执行int 0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常。
3、system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号; 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号,进入sys_call之后,立即将eax的值压入内核堆栈;
4、寄存器传递参数具有如下限制:
1)每个参数的长度不能超过寄存器的长度,即32位
2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp);

第五节课从内核代码角度分析了system_call的执行过程:
1、system_call在start_kernel中的trap_init处初始化;
2、若有系统调用int 0x80执行,则会从system_call开始执行;
3、system_call主要执行过程:

SAVE_ALL  //保存断点  call *sys_call_table(,%eax,4)  //系统调用表,这里可以看出系统调用号通过eax传递,然后获取对应的系统调用入口 movl %eax,PT_EAX(%esp)      //返回值放在eax中  jne syscall_exit_work   //判断是否跳出  TRACE_IRQS_IRET   //恢复断点 INTERRUPT_RETURN        //中断返回  

4、其中是否跳出syscall_exit_work执行如下过程:

    testl $_TIF_WORK_SYSCALL_EXIT, %ecx  //是否有进程需要调度      jz work_pending             //有则跳到对应位置      jmp resume_userspace    //恢复用户态  

6、work_prnding主要执行了如下过程:

work_pending:      testb $_TIF_NEED_ESSCHED, %c1      jz work_notifysig #处理进程信号  work_resched: #重新调度      call schedule # 执行调度      jz restore_all  work_notifysig:      cmpb %USER_RPL,%bl      jb resume_kernel      call do_notify_resume      jmp resume_userspace #回到用户态  END(work_pending)  

第六节课从内核代码的角度分析了创建一个新进程的过程:
1、在 Linux 内核中,供用户创建进程的系统调用fork()函数的响应函数是 sys_fork()、sys_clone()、sys_vfork()。这三个函数都是通过调用内核函数 do_fork() 来实现的。
2、do_fork在/linux-3.18.6/kernel/fork.c中定义,主要完成了以下过程:

struct task_struct *p; //创建新的进程描述符p = copy_process(clone_flags, stack_start, stack_size,                      child_tidptr, NULL, trace);//复制父进程的进程数据wake_up_new_task(p);//将子进程添加到调度器的队列,使得子进程有机会获得CPU

3、其中copy_process主要完成了以下过程:

struct task_struct *p;//创建子进程PCB结构体p = dup_task_struct(current);  //current为当前进程,复制当前进程PCB//复制完后我们需要修改子进程p内部的一系列数据p->utime = p->stime = p->gtime = 0;p->utimescaled = p->stimescaled = 0;。。。。。。retval = copy_thread(clone_flags, stack_start, stack_size, p);//此过程复制了一些关键数据return p;//返回修改后的子进程

4、dup_task_struct主要完成了以下过程:

struct task_struct *tsk;//新进程PCBstruct thread_info *ti;//新进程堆栈int node = tsk_fork_get_node(orig);tsk = alloc_task_struct_node(node);//分配一个task_structti = alloc_thread_info_node(tsk, node);//分配堆栈空间err = arch_dup_task_struct(tsk, orig);//从orig复制task_struct至tsktsk->stack = ti;//tsk的堆栈指向tisetup_thread_stack(tsk, orig);//复制堆栈内容

5、arch_dup_task_struct主要完成了以下过程:

*dst = *src;  //实际上就是把src的内容复制到dst

6、copy_thread主要完成了以下过程:

struct pt_regs *childregs = task_pt_regs(p);//栈顶地址p->thread.sp = (unsigned long) childregs;//栈顶地址赋给sp*childregs = *current_pt_regs();//父进程的栈内容复制给子进程childregs->ax = 0;//子进程返回值为0p->thread.ip = (unsigned long) ret_from_fork;//子进程的运行入口

第七节课从内核代码角度分析了一个可执行程序的装载,Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数:
1、在linux-3.18.6/fs/exec.c中我们找到sys_execve的系统调用,实际上执行的是do_execve:

return do_execve(getname(filename), argv, envp);

2、继续找到do_execve,最终执行的是do_execve_common:

return do_execve_common(filename, argv, envp);

3、找到do_execve_common,主要执行以下过程:

struct linux_binprm *bprm;// linux_binprm结构体用于维护程序执行过程中所使用的各种数据。//下面是初始化bprm的过程bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);。。。。。。retval = exec_binprm(bprm);//加载程序

4、exec_binprm主要执行以下过程:

ret = search_binary_handler(bprm);//寻找文件格式对应的解析模块

5、search_binary_handler主要执行以下过程:

retval = fmt->load_binary(bprm);//装载文件格式对应的模块

6、load_elf_binary其内部是和ELF文件格式解析:

error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,                elf_prot, elf_flags, 0); //目标文件映射到地址空间elf_entry = load_elf_interp(&loc->interp_elf_ex,                        interpreter,                        &interp_map_addr,                        load_bias);//动态链接时的入口elf_entry = loc->elf_ex.e_entry;//无动态链接的程序入口

第八节课从内核代码的角度分析了进程切换过程:
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程:
1、正在运行的用户态进程X
2、发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
3、SAVE_ALL //保存现场
4、中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
5、标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
6、restore_all //恢复现场
7、iret - pop cs:eip/ss:esp/eflags from kernel stack
8、继续运行用户态进程Y

几种特殊情况:
1、通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
2、内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
3、创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
4、加载一个新的可执行程序后返回到用户态的情况,如execve;

进程主要通过schedule来完成切换:
1、sechedule定义在linux-3.18.6/kernel/sched/core.c中:

asmlinkage __visible void __sched schedule(void){    struct task_struct *tsk = current;//当前进程地址    sched_submit_work(tsk);//提交调度工作    __schedule();//执行调度}

2、可以看到,通过__schedule()执行调度,主要过程如下:

 next = pick_next_task(rq, prev);///进程调度算法都封装这个函数内部 context_switch(rq, prev, next); /* 进程上下文的切换*/

3、context_switch主要过程如下:

struct mm_struct *mm, *oldmm;//地址空间prepare_task_switch(rq, prev, next);//任务切换准备工作mm = next->mm;//修改地址空间oldmm = prev->active_mm;switch_mm(oldmm, mm, next);//地址空间切换switch_to(prev, next, prev);  //进行切换

4、switch_to是一个宏定义,实现了具体切换,实际其过程主要是对prev进程的进程堆栈寄存器和运行断点进行保存,然后切换至next进程的堆栈寄存器和断点,而__switch_to函数主要做了一些硬件环境的切换,至此schedule函数的主要过程执行完毕,然后通过中断的Iret恢复执行next进程。

二、个人总结
通过2个月的学习,感觉收获良多,以前对操作系统只知道一些概念,进程、系统调用、内核态、用户态、进程切换与运行等等只知道大概是什么东西,但是具体起来一无所知;这次学习让我从内核代码的层面上明白了linux内核的框架,并且知道一些常见的操作中(操作系统的启动、创建新进程、系统调用、进程切换等等)操作系统主要都做了什么。
遗憾的是,我只是从以前的不知其然到知其然,但是仍不知其所以然。读内核代码是一个非常庞大的工程,许多时候我也只是跟着老师的脚步去看一些关键代码,了解了那会大概做了什么,然后gdb跟踪一下就了事了;但是对于其它代码部分,都不清楚做了什么,有什么用,有些关键代码也不知道究竟具体是怎么实现的,一切都是一种朦胧的感觉。由此感叹,我对linux内核的认识才刚刚起步,以后的路途任重而道远。

0 0