《Linux操作系统分析》之Linux系统的理解及学习Linux内核的心得

经过这一段时间的学习,自己对linux也有了一定的认识,今天这篇博客对以往的知识进行一个总结吧。

以往linux学习的博客,从上而下是学习深入的过程,我的博客链接如下:

第一篇:《Linux操作系统分析》之分析计算机如何启动以及如何工作运行

第二篇:《Linux操作系统分析》之分析精简的Linux的内核中断和时间片轮询

第三篇:《Linux操作系统分析》之跟踪分析Linux内核的启动过程

第四篇:《Linux操作系统分析》之使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

第五篇:《Linux操作系统分析》之分析系统调用system_call的处理过程

第六篇:《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

第七篇:《Linux操作系统分析》之分析Linux内核如何装载和启动一个可执行程序

第八篇:《Linux操作系统分析》之理解进程调度时机跟踪分析进程调度与进程切换的过程


任何形式的学习都不是一个一蹴而就的过程,要经历一段痛苦的磨练期。

在接触linux开始,各种命令繁多,不过经过一段时间自己也可以记得下来。关于命令,我会写另一篇博客将我学习鸟哥的私房菜的一些基本命令做个总结。

言归正传,现在对我学习深入理解linux内核这本书,做一个阶段性的总结。

我将对linux的以下方面做个总结。包括计算机的启动、内核加载、进程、中断和异常、进程调度、系统调用、程序的执行。其他的部分在接下来学习完成后在进行分析和总结。

一、计算机的启动

正如我linux第一篇博客所言,我们会好奇计算机是怎么启动的。如何从一个铁疙瘩变成一个计算速度秒杀人类的机器那。我在第一篇博客中借用了一句话:"pull oneself up by one's bootstraps"字面意思是”拽着鞋带把自己拉起来”。这个过程想必大家也不能做到。其实计算机的启动之初我们需要第一条指令,然后一直等待输入指令。那么第一条指令是什么时候输入的那。在BIOS中电脑在固定位置被写入了第一条指令。当第一条指令加载后,就可以执行后续的行为。

简单的来说计算机的运行,就是一个人一直等待着做事情,有事情了他就去做,没事情了,就自己发呆。如果有多个事情同时来了。这个人就要根据自己的需要判断先后执行的顺序。

详细的分析见我的第一篇博客。

二、内核加载

内核加载的流程大概如下:执行asmlinkage __visible void __init start_kernel(void)这个函数,在其中的有一个set_task_stack_end_magic(&init_task);函数,这个函数中该结构体(init_task)在linux启动时被设置为current_task。(此时idle进程已经启动)然后对其他的信息也进行初始化。接着进行到了rest_init();这个地方。当初始化到rest_init函数中时调用kernel_thread(kernel_init, NULL, CLONE_FS);函数启动第一个内核线程kernel_init。由kernel_init再通过do_execve启动/sbin/init。这就是我们看到的init进程,进程号为1。初始化完成后linux调用scheule整个系统就运行起来了。

所以我们可以看出来:idle是一个进程,其pid为0。是Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程。主处理器上的idle由原始进程(pid=0)演变而来。从处理器上的idle由init进程fork得到,但是它们的pid都为0。Idle进程为最低优先级,且不参与调度,只是在运行队列为空的时候才被调度。Idle循环等待need_resched置位。1号进程是init 进程,由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程

详细的分析见我的第三篇博客。

三、进程。

通常来讲进程就是程序执行时的一个实例,可以把它看作是充分描述程序已经执行到何种程度的数据结构的汇集。

进程和人类相似:它们会被创建,而后有或多或少的有效的生命,可以产生一个或者多个子进程,最终都是要死亡的。当然程序没有性别,只有一个父亲。

我们从内核的观点来看的话:进程的目的就是担当分配系统资源的实体。

一个进程可以和其他的进程共享代码,但是它们有各自独立的数据拷贝(栈和堆),相互之间不影响对方。

每个进程都有自己的进程描述符(task_struct类型结构),在这里面有:进程的基本信息、指向内存区描述符的指针、与进程相关的tty、当前目录、指向文件描述符的指针、所接受的信号等等。

进程有以下的状态:可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、跟踪状态。还有两个进程的状态既可以存放在state字段中,也可以放在exit_state字段中。只有当进程的执行被终止的时,进程的状态才会变为这两种状态中的一种。即:僵死状态、僵死撤销状态。

当然每个进程都有一个唯一的进程描述符(process ID)。

进程切换:为了控制进程的执行,内核必须有能力挂起正在cpu上运行的进程,并恢复以前挂起的某个进程的执行。详细的分析见我的第八篇博客。这里我们简单的说,用人来为例说明,进程的切换就是说:一个人在做一件事情A的时候,发生了另一件事情B。此时他需要去处理另一件事情B。这时候他要切换两个事情了,首先他要知道做完B事情后继续做A事情的话,应该从哪里做起,这就要保存A事情执行的信息了。其次他要知道B事情从哪里开始,去哪里找工具,这些信息需要读入了。当这个保存和读入的过程完成,他就可以去做B事情,这就完成了进程的切换。

关于这章我有以下的总结:

一、对于每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据节后thread_info,叫做线程描述符。(数据类型是union,通常是8192字节即两个页框)。线程描述符驻留于这个内存区的开始,而栈从末端(高地址)开始增长。因为thread_info是52字节长,所以内核栈能扩展到8140字节。

二、当一个进程创建时,它与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享程序代码的页,但是它们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)。
三、进程的创建过程大概就是这样的:sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork。

详细的分析见我的第六篇博客。

四、中断和异常

详细的分析见我的第二篇博客。

中断简单的来说就是在你做一件事的时候,被打断去执行其他的事情。举个日常生活中的例子,比如说我正在厨房用煤气烧一壶水,这样就只能守在厨房里,苦苦等着水开——如果水溢出来浇灭了煤气,有可能就要发生一场灾难了。等啊等啊,外边突然传来了惊奇的叫声“怎么不关水龙头?”于是我惭愧的发现,刚才接水之后只顾着抱怨这份无聊的差事,居然忘了这事,于是慌慌张张的冲向水管,三下两下关了龙头,声音又传到耳边,“怎么干什么都是这么马虎?”。伸伸舌头,这件小事就这么过去了,我落寞的眼神又落在了水壶上。门外忽然又传来了铿锵有力的歌声,我最喜欢的古装剧要开演了,真想夺门而出,然而,听着水壶发出“咕嘟咕嘟”的声音,我清楚:除非等到水开,否则没有我享受人生的时候。

中断有两种:

同步中断:当指令执行时由CPU控制单元产生的,之所以成为同步,是因为只有在一条指令终止执行后CPU才会发出中断。

异步中断:由其他硬件设备依照CPU始终信号随机产生的。

中断信号:提供一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号达到时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。(这就需要在内核堆栈保存程序计数器的当前值,即eip和cs寄存器的内容,并且要把与中断类型相关的一个地址放进程序计算器)

中断和异常的分类:

中断:可屏蔽中断、非屏蔽中断。

异常:

处理器探测异常:故障、陷阱、异常中止。

编程异常(也叫软中断):在编程者发出请求时发生,由int或int3指令触发。

五、进程调度

详细的分析见我的第八篇博客。

简单的来说,进程的调度涉及到以下过程:schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换 
next = pick_next_task(rq, prev);   进程调度算法都封装这个函数内部,通过这个函数选出一个进程作为下一个执行的进行。
context_switch(rq, prev, next);  进程上下文切换
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程,来进行进程的切换。

在学习了这段后,我有以下总结:

一、进程调度的时机
1、中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
2、内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
3、用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
二、进程的切换
1、为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
2、挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
3、进程上下文包含了进程执行需要的所有信息
3.1、用户地址空间: 包括程序代码,数据,用户堆栈等
3.2、控制信息 :进程描述符,内核堆栈等
3.3、硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
4、进程切换执行的流行
 schedule() -> pick_next_task()->context_switch() -> switch_to -> __switch_to()
三、几种特殊情况
1、通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
2、内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
3、创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork。(对应switch_to流程中的第6步,此时返回到ret_from_fork)
4、加载一个新的可执行程序后返回到用户态的情况,如execve。(对应switch_to流程中的第6步,此时静态链接返回到程序的start处,动态链接则返回到动态链接器处)

六、系统调用

详细的分析见我的第四篇和第五篇博客。

Linux中是通过寄存器%eax传递系统调用号,所以具体调用fork的过程是:将20存入%eax中,然后进行系统调用.对于参数传递,Linux是通过寄存器完成的。Linux最多允许向系统调用传递6个参数,分别依次由%ebx,%ecx,%edx,%esi,%edi和%ebp这个6个寄存器完成。进程运行在用户态和内核态时使用不同的栈,分别叫做用户栈和内核栈。两者各自负责相应特权级别状态下的函数调用。系统调用时,进程不仅是用户态到内核态的切换,同时也要切换栈,这样内核态的系统调用才能在内核栈上完成调用。系统调用返回时,还要切换回用户栈,继续完成用户态下的函数调用。

我们也可以看到system_call的过程如下:

1)系统调用的初始化的顺序是:start_kernel()->trap_init()->set_system_trap_gates(SYSCALL_VECTOR,&system_call);
2)用户态到内核态通过0x80进行中断,在内核初始化期间调用trap_init(),用函数set_system_trap_gates(),建立了对应于向量128的中断描述符表表项,从而进入相应的中断服务。
3)system_call()函数首先将系统调用号或中断处理程序需要用到的所有的CPU寄存器保存到相应的栈中。然后进行服务的处理。当系统调用服务例程结束时,system_call()函数从eax获得它的返回值。然后进行一系列的检查,最后恢复用户态进程的执行。

七、程序的执行

详细的分析见我的第七篇博客。

对于程序的执行有以下的总结:

一、可执行程序的装载是一个系统调用。可执行程序执行时,由execve系统调用后便陷入到内核态里,而后加载可执行文件,把当前进程的可执行程序覆盖掉,当execve系统调用返回的时,返回的则是新的可执行程序(执行起点main处)。
 二、execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但是并没有创建一个新的进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有的文件描述符。
三、execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数串即可执行目标的名字。envp变量结构与argv变量一样,不同的是每个环境变量串是形如:“NAME=VALUE”的名字-值对。
四、execve函数与fork函数的差异:只有当出现错误的时候,execve才会返回到调用程序。也就是说execve系统调用和fork系统调用的区别是前者成功不返回,不成功返回-1,后者是返回两次。

总结:

经过了一段时间的学习,自己对linux也有了一定的认识。

也经历了看山是山,看山不是山,看山还是山的过程。最后还是觉得学的越多,自己知道的越少。

不过庆幸的是我收获了相关的知识,也加强了自己克服困难的能力。

当然在这过程中也有一定的遗憾,那就是学习的枯燥导致自己有时候对自己的要求放松,没有做到持之以恒。

备注:
杨峻鹏 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 


评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值