《Linux内核设计与实现》与《Linux内核源代码情景分析》读书笔记

第一章:内核简介

处理器在任何指定时间点上的活动范围:

a,运行于内核空间,处于进程上下文,代表某个特定的进程执行;

b,运行于内核空间,处于中断上下文,于任何进程无关,处理某个特定的中断;

c,运行于用户空间,执行用户进程。

当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称 为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的 所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结 构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程 的执行。

第三章:进程管理

1,内核把进程存放在叫做任务队列(task list)的双向链表中,链表中每一项都是类型为task_struct,称为进程描述符(process descriptor)的结构,此结构包含一个具体进程的所有信息。

2,内核通过一个惟一的进程标识值或PID来标志每个进程,内核把每个进程的PID存放在它们各自的进程描述符中。

3,x86系统寄存器较少只能通过在该进程内核栈的栈顶或栈底创建thread_info结构,通过计算偏移间接的查找task_struct结构。

4,进程状态:

扫描二维码关注公众号,回复: 10726096 查看本文章

a,TASK_RUNNING进程正在执行或在运行队列中等待执行;

b,TASK_INTERRUPTIBLE进程正在睡眠,等到某些条件达成,内核就会把进程状态设置为运行,或因为接收到信号而提前被唤醒并投入运行。

c,TASK_UNINTERRUPTIBLE进程正在睡眠,且不响应信号。

d,TASK_ZOMBIE该进程已经结束,为使父进程获知它的消息,子进程的进程描述符仍被保留;

e,TASK_STOPPED进程停止执行,接收到SIGSTOP、SIGTTIN、SIGTTOU、SIGTSTP等信号时发生。

5,系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行,对内核的所有访问都必须通过这些接口。

6,进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct叫做parent的指针,和一个称为children的子进程链表。

7,写时拷贝:fork()后内核让父进程和子进程共享同一个拷贝,只有在父子进程需要写入的时候,数据才会被复制。

8,fork()和vfork()调用clone(),clone()调用do_fork(),do_fork()调用copy_process(),具体见p27,p28。

9,线程在内核中是一个普通的进程,致使该进程和其他进程共享一些资源,每个线程拥有属于自己的task_struct,线程的创建也是调用clone()。

10,内核线程和普通进程的区别是没有独立的地址空间,只在内核空间运行,会将它在创建时得到的函数永远执行下去,该函数通常有一个循环,再需要的时候,该内核线程会被唤醒和执行,完成任务会自行休眠。

11,进程终结,do_exit()系统调用p31,wait()函数通过系统调用wait4()实现的,最终释放进程描述符时,release_task()会被调用,p32;

12,内核对孤儿进程的处理:给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做其父进程,遍历子进程链表和prace子进程链表;

13,当一个程序执行了系统调用或者触发了某个异常,它就会陷入内核空间,此时内核代表进程运行,处于进程上下文中。此时,进程可以睡眠和调用调度程序。可以通过current宏关联当前进程。

第四章:进程调度

1,在抢占式多任务模式下,由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。这个强制的挂起动作叫做抢占。进程在被抢占之前能够运行的时间的预先设置好的,叫进程的时间片。

2,策略决定调度程序在何时让什么进程运行。I/O消耗型进程大部分时间用来提交I/O请求或是等待I/O请求,经常处于可运行状态,但通常都是运行短短一会儿,这里说的I/O是指任何类型的可阻塞资源;处理器消耗型进程把时间大部分用在执行代码上,除非被抢占否则就一直在执行。

3,调度策略在两个目标中找平衡:进程响应迅速和最大系统利用率。

4,进程优先级:根据进程的价值和其对处理器时间需求来对进程分级。Linux两种优先级:nice值和实时优先级,nice值作为权重将调整进程所使用的处理器时间比,nice值越高的进程被赋予低权重,丧失一部分处理器使用比;CFS调度器的抢占时机取决于新的可执行程序消耗的多少处理器使用比,若比当前进程小,则新进程立刻投入运行,抢占当前进程。举例来说,一个文字编辑程序和一个视频编码程序是某一时刻仅有的两个可执行程序,有相同的nice值,因为文本编辑器将更多时间用于等待用户输入,所以它的处理器使用比肯定低于50%,低于视频编码程序的使用比,所以CFS会在用户输入即文本编辑器被唤醒时,将其立即投入运行,抢占视频编码程序,处理完程后,又一次进入睡眠等待用户的下一次输入。

5,linux的调度器是以模块方式提供的,允许不同类型的进程选择不同的调度算法,这种模块化结构成为调度器类,完全公平调度(CFS)是针对普通进程的调度类,它的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。

6,调度器实体结构struct sched_entity作为一个名为se的成员嵌入在进程描述符struct task_struct内,se里面的vruntime变量是进程花在运行上的时间和,CFS调度算法的核心:选择具有最小vruntime的任务。

CFS使用红黑树组织可运行队列,CFS的进程选择算法总结为运行rbtree树中最左边叶子节点所代表的那个进程。向树中加入进程发生在进程变为可运行状态或者通过fork()调用第一次创建进程时;从树中删除动作发生在进程阻塞(变为不可运行状态)或者终止时。

调度器的入口时schedule()函数,它以优先级为序,从最高的调度类开始,每个调度类要有自己的可运行队列,从队列中获取下一个可运行的进程。

睡眠:进程把自己标记成睡眠状态,从可执行红黑数中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程;

唤醒:进程被设置为可运行状态,再从等待队列中移到可执行红黑树中;

等待队列是由等待某些事件发生的进程组成的简单链表。P50进程加入等待队列的详细步骤;

上下文切换:context_switch()函数处理,调用switch_mm()将虚拟内存从上一个进程映射切换到新进程中;调用switch_to()将上一个进程的处理器状态切换到新进程的处理器状态,包括保存、恢复栈信息和寄存器信息。

7,内核提供一个need_resched标志来表明是否需要重新执行一次调度。

8,用户抢占发生在:从系统调用返回用户空间时和从中断处理程序返回用户空间时;内核抢占发生在:中断处理程序正在执行,且返回内核空间之前;内核代码再一次具有可抢占性的时候;如果内核中的任务显示的调用schedule();如果内核中的任务阻塞(这同样也会调用schedule());

第五章:系统调用

1,在Linux中,系统调用是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核唯一的合法入口。Unix的系统调用抽象出了用于完成某种确定的目的的函数,至于这些函数怎么用完全不需要内核去关心。提供机制而不是策略。

2,在Linux中,每个系统调用被赋予一个系统调用号,内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。

3,用户空间的程序无法直接执行内核代码,因为内核驻守在受保护的地址空间上,应用程序通过软中断的机制通知内核,通过引发一个异常促使系统切换到内核态去执行异常处理程序即系统调用处理程序system_call(),系统调用陷入内核要将系统调用号通过eax寄存器传递给内核,参数和返回值都是通过寄存器传递。系统调用返回的时候,system_call负责切换到用户空间,并让用户进程继续执行下去。

第六章:内核数据结构

1,内核链表和普通链表的区别:普通链表的链表节点包含业务内容,而内核链表将业务内容和链表分离,单独成为一个节点,并且将链表节点包含在其中;

i = (int) (&(((struct AdvAdvTeacher *)0)->age ));获取业务内容的偏移量。

2,内核红黑树:详见http://blog.csdn.net/yang_yulei/article/details/26066409

第七章:中断和中断处理

1,中断是一种由硬件产生的电信号,并直接送入中断控制器,中断控制器会给处理器发送一个电信号,处理器通知操作系统已经产生中断,操作系统再对中断进行处理。

2,内核随时可能因为新到的中断而被打断。硬件发生中断是为了引起内核的关注。

3,每个中断对应一个中断值称为IRQ中断请求线,每个IRQ都关联一个数值量。

4,异常与中断的区别:异常要与处理器时钟同步。异常是由软件引起的,中断是由硬件产生的。

5,内核响应中断的特定函数叫中断处理函数或中断服务例程ISR,一个设备的ISR是它的设备驱动程序的一部分,设备驱动程序是用于管理设备的内核代码。

6,当执行一个ISR时,内核处于中断上下文中,中断上下文与进程无关,无current宏无关,不可以睡眠,因为没有后备进程所以无法调用调度程序。

7,中断处理程序是上半部,中断处理程序打断了其他的代码(甚至可能打断了在其他中断线上的另一中断处理程序),正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速简洁,尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。

8,中断处理程序拥有自己的栈,每个处理器一个,大小为一页。即中断栈。

第八章:下半部和推后执行的工作

1,要尽量减少中断处理程序中需要完成的工作量,因为它在运行的时候,当前的中断线在所有的处理器上都会被屏蔽。缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。下半部执行的关键在于它们运行的时候,允许响应所有的中断。

2,软中断:一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是中断处理程序,相同类型的其他软中断可以在其他处理器上同时运行。

3,tasklet

4,工作队列把工作交由一个内核线程去执行,它总是会在进程上下文中执行,即允许重新调度和睡眠。

第九章:内核同步介绍

1,Linux内核是抢占式内核,在没有保护的的情况下,调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他的进程执行。

2,各种锁机制之间的区别主要在于:当锁已经被其他线程持有,因而不可用时的行为表现——一些锁被争用时会简单地执行忙等待,而另外一些锁会使当前任务睡眠直到锁可用为止。

3实际上同步就是调用模块等待一个被调用体返回后,再继续下一步;而异步是调用模块发起调用之后,不用等待调用返回就继续下一步了。

4,内核中造成并发执行的原因:a,中断;b,软中断和tasklet;c,内核抢占;d,睡眠及与用户空间的同步;e,对称多处理。

5,大多数内核数据结构都需要加锁,要给数据而不是代码加锁。

6,自死锁,如果一个执行线程试图去获得一个自己已经拥有的锁,它将不得不等待锁被释放。abba死锁每个线程都在等待其他线程持有的锁,但是没有一个线程会释放他们一开始就持有的锁。避免死锁的规则:a按顺序加锁,b防止发生饥饿,c不要重复请求同一个锁,d设计力求简单。以获得所的相反顺序释放锁。

第十章:内核同步方法

1,自旋锁最多只能被一个可执行线程持有,如果一个线程试图获得一个被其他线程持有的自旋锁,那么该线程就会一直进行忙循环——旋转——等待锁重新可用。这样特别浪费处理器时间,所以自旋锁不应给长时间被持有。

2,自旋锁可以用在中断处理程序中,而信号量不可以,因为信号量会导致睡眠,在中断处理程序中使用自旋锁时,一定要在获取锁之前,禁止本地中断(当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁,这样,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕前不可能运行。这就是双重请求死锁。

3,下半部和进程上下文共享数据时,因为下半部可以抢占进程上下文,所以要对进程上下文中的共享数据进行保护,加锁的同时还要禁止下半部执行;中断处理程序和下半部共享数据时,由于中断处理程序可以抢占下半部,必须在获取恰当的锁的同时还要禁止中断。同类的tasklet不能同时运行,所以对于同类tasklet中的共享数据不需要保护。当数据被两个不同种类的taskLet共享时,就需要在访问下半部中的数据前先获得自旋锁,不需要禁止下半部,因为同一个处理器上tasklet不会相互抢占。数据被软中断共享和tasklet一样。

4,读写自旋锁:一个或多个读任务可以并发的持有读者锁,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。

5,信号量:如果一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,这是处理器能重获自由,去执行其他代码,当持有的信号量可用后,处于等待队列中的那个任务将被唤醒,并获得该信号量。

6,信号量适用于锁被长期持有的情况。只能在进程上下文中获取信号量锁。

7,读写信号量,只要没有写者,并发持有读锁的读者数不限,相反,只有唯一的写者可以在没有读者时获得写锁。

8,互斥锁,使用计数始终为1的互斥信号量。

9,完成变量、顺序锁、屏障;

第九章:内存管理

1,程序代码产生出的是逻辑地址,CPU要将一个逻辑地址转换为物理地址,需要两步:首先CPU利用段式内存管理单元,将逻辑地址转换成线性地址,再利用页式内存管理单元,把线性地址最终转换为物理地址。

2,Linux采用页式存储管理机制,由于i386CPU的向下兼容,所以Linux内核只不过是在对付本来就毫无必要却又非得如此的例行公事而已,即每个段都是从0地址开始的整个4GB虚存空间,虚拟地址到线性地址的映射保持原值不变。

3,每个进程都拥有4G字节的虚存空间,较低的3G字节为自己的用户空间,最高的1G字节则为与所有进程以及内核共享的系统空间。虽然系统空间占据了每个虚存空间中最高的1G字节,在物理的内存中却是从最低的地址(0)开始。

对于系统空间来说,其地址映射就是简单的线性映射,给定一个虚地址x,其物理地址是从x中减去PAGE_OFFSET=0xC0000000,相应的,给定一个物理地址x,其虚拟地址是x+PAGE_OFFSET;不管什么进程,一旦进入系统空间,都有相同的页面映射。

对于用户空间,其地址映射就是页式管理的精髓了。Linux页式映射机制分为三层:页面目录PGD,中间目录PMD,页面表PT,PT中的表项成为PTE。每个进程都有自己的PGD,PMD,PT,这三者均为数组。

一个地址为0000 1000 0000 0100 1000 0101 0110 1000,最高十位是十进制32,所以i386CPU就以32为下标去页面目录中找到其目录项,这个目录项的高20位指向一个页面表,CPU在这20位后面添上12个0就得到该页面表的指针,(每个页面表占一个页面,所以自然就是4K字节边界对齐的,其起始地址的低12位一定是0,),找到页面表以后,CPU再来看线性地址中的中间10位,即72,CPU就以此为下标在已经找到的页表中找到相应的表项,与目录项类似,32位的页面表项中的高20位指向一个物理内存页面,在后面添上12个0就得到这物理内存页面的起始地址,在其起始地址上加上线性地址的最低12位就得到了最终的物理内存地址。

4,越界访问:1,相应的页面目录项或页面表项为空;2,相应的物理页面不在内存中;3,指令中规定的访问方式与页面的权限不符;此时CPU会产生一个page fault exception页面出错异常,进而执行预定的页面异常处理程序,并向该进程发送SIGSEGV信号,进程每次从中断/异常返回之前,都要检查当前进程是否有悬而未决的信号需要处理,即输出Segment Fault,进程结束。

5,用户堆栈的扩展:

假设进程运行过程中,已经用尽了为本进程分配的堆栈空间,即堆栈指针已经指向了堆栈空间的起始地址esp,假设现在需要调用某个子程序,CPU需将其返回地址压入堆栈,即要将返回地址写入esp-4的地方,而那个地方是空洞;因堆栈操作引起的越界是作为特殊情况对待的,需要检查发生异常的地址是否紧挨着堆栈指针所指的地方标准是esp-32,如果不是,那就是非法越界访问,如果是那就在空洞的顶端开始分配若干页面建立映射,并将之并入堆栈空间,使其得到扩展。

6,中断和异常的区别:当中断以及自陷发生时,CPU都会将下一条指令,也就是本来应该执行的指令的地址压入堆栈作为中断服务程序的返回地址,异常发生时,CPU将因无法完成而夭折的指令本身的地址(不是下一条指令的地址)压入堆栈,这就可以在从异常处理返回时完成未竟的事业。

7,内存分配算法:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28820980&id=3848787

发布了30 篇原创文章 · 获赞 13 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qiaominghe/article/details/50866850