`

《Linux Kernel Development》读书笔记

阅读更多

原文:http://www.cppblog.com/luckycat/archive/2010/03/03/108836.html

 

 

chapter 1
1.当应用程序向内核请求调用一个系统调用时,我们说内核正在代其执行,如果进一步解释,在这种情况下,应用程序被称为通过系统调用在内核空间运行;而内核则被称为运行在进程上下文中.
2.硬件与内核的交互:当硬件设备想和系统进行交互时,它首先要向CPU发送一个异步的中断信号,然后由CPU去打断内核当前正在执行的工作,中断通常对应着一个中断号,内核通过这个中断号来查找对应的中断处理程序,并调用这个找到的中断处理程序来处理中断.为了保证同步,内核可以停用中断,也就是忽略某个中断,既可以停止所有的中断处理程序,也可以有选择性地停止某些中断处理程序。许多操作系统的中断处理程序都不在进程上下文中,而是在一个单独的与所有的进程都无关的中断上下文中执行,这样做是为了保证中断处理程序在第一时间响应和处理中断信号,并快速退出.
3.任一时刻,CPU的活动范围为以下三者之一:
 a.运行于内核空间,处理进程上下文,代表某个特定的进程执行.
 b.运行于内核空间,处理中断上下文,与任何进程无关,处理某个特定的中断信号.
 c.运行于用户空间,执行用户程序.
4.单内核与多内核:操作系统的内核设计分为两大阵营:单内核和微内核(以及在科研中的外内核)
 单内核:就是把内核在整体上做为一个单独的大过程来实现,并同时运行在一个单独的地址空间中。因此这样的内核通常以单个静态的二进制文件的形式存储于磁盘,所有内核服务都在这样一个大的内核空间中运行,内核之间的通信是微不足道的,因为大家都运行在内核态,并处于同一地址空间,内核可以直接调用函数(其它的内核服务中的功能函数),这与用户空间没有什么区别,这种模式的好处在于:简单和高性能。
大部分的UNIX和Linux是单内核系统.Linux同时也吸收了微内核的优点:内核模块化设计,抢占式内核,支持内核线程,以及动态内核模块加载和卸载.
 微内核:微内核并不将内核作为一个单独的大过程来实现,相反,微内核的功能被划分为独立的过程,每个过程叫做一个服务。理想情况下,只有强烈请求特权服务的服务器运行在特权模式下,其它的服务都运行在用户空间.不过所有的服务都保持独立并运行在各自的地址空间,因此就不能像单内核那样直接调用函数,而是通过消息传递处理微内核服务之间的通信:系统采用进程间通信(IPC)机制,各种服务器之间通过IPC机制互通消息,互换服务.服务器的各自独立有效地避免了一个服务器的失效祸及另一个.Windows和Max OS X 都是微内核.Windows NT内核和Mac OS 的内核都将所有的内核服务程序运行于内核特权模式下,这一点违背了微内核的设计思想,但是减少了内核服务之间的通信之间的消息机制产生的开销.
5.Linux内核并不区分进程和线程,对于内核来说,只有进程,而且所有的进程都一样,只不过是有的进程共享一些资源而已.
6.Linux内核的版本号:x.y.z
 x:是主版本号;y:从版本号;z:修订版本号
 z:如果为偶数,那么它是一个稳定的版本;如果为奇数,那么它是一个开发版.
 x.y 用于描述内核系列.


chapter 2
7.内核源码文件结构:
 arch:    特定体系结构的源码
 crypto:   Crypto API
 Documentation: 内核源码文档
 drivers:  设备驱动程序
 fs:    VFS和各种文件系统
 include:   内核头文件
 init:   内核引导和初始化
 ipc:   进程间通信代码
 kernel:   内核核心子系统
 lib:   通用内核函数
 mm:    内存管理子系统和VM
 net:   网络子系统
 scripts:   编译内核所用的脚本
 secrity:  Linux安全模块
 sound:   语音子系统
 usr:   早期用户空间代码(所谓的initramfs)

8.内核中的内核空间都不分页,所以,如果内核空间使用了一个字节的内存,那么实际的可用的物理内存就少了一个字节。
9.在内核中没有内存保护机制.
10.不要轻易在内核中使用浮点数.在用户空间进行浮点数操作时,内核会完成从整数操作到浮点数操作的模式转换,在执行浮点数操作时到底会做些什么,因体系结构的不同,内核的选择也会不同,但是内核通常捕获陷阱并做相应的处理.和用户空间进程不同,内核并不能完美支持浮点操作,因为它本身不能陷入.在内核使用浮点数时,除了要人工保存和恢复浮点计数器,还有其它的一些琐碎的事情要做.所以:不要在内核中使用浮点数.
11.内核开发中,不能使用内核源代码之外的其它的外部库文件.
12.内核中没有printf函数,但是有printk函数可以用于打印调试信息.
13.内核的栈空间很小:内核栈的准确大小随体系结构而变.在X86系统中,栈的大小可以在编译时配置,可以是4KB,也可以是8KB.从历史上说,内核栈的大小是两页,这也就意味着,在32位系统上内核栈是8K,在64位系统上,内核栈是16K,这是固定不变的,每个处理器都有自己的栈.
14.硬件中断是异步到来的,由CPU发送给内核,完全不顾及内核当前的操作.
15.Linux内核中常用的用于解决并发产生的竞争的办法是:自旋锁和信号量.

chapter 3
15.进程:就是包含各种资源的处于执行期的程序.
16.线程:进程中的活动对象.每个线程都有一个独立的程序计数器,进程栈和一组进程寄存器.
17.内核调度的是线程而不是进程.
18.Linux中,进程与线程并不特别进行区分,对于内核而言,线程只不过是一种特殊的进程而已.
19.进程的5种状态:
 a. TASK_RUNNING(运行状态)--进程是可以执行的,它或者正在执行,或者在运行队列中等待执行,这是进程在用户空间中执行的唯一状态,也可以应用到内核空间中正在执行的进程.
 b. TASK_INTERRUPTIBLE(可中断)--进程正在睡眠(也就是说它被阻塞),等待某些条件的达成.一旦这些条件达成,内核就会把进程设置为运行,处于此状态的进程也会因为收到信号而提前被唤醒而投入运行.
 c. TASK_UNINTERRUPTIBLE(不可中断)--除了不会因为接收到信号而被唤醒而投入运行之外,这个状态与TASK_INTERRUPTIBLE(可中断)状态相同.这个状态通常在进程正在等待地不受干扰或等待事件很快就会发生时,由于此状态不对信号进行响应,所以,较之TASK_INTERRUPTIBLE(可中断)使用得比较少.
 d. TASK_ZOMBIE(僵死)--该进程已经结束,但是父进程还没有调用wait4()系统调用,为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4,该僵死的子进程的进程描述符就会被释放.
 e. TASK_STOPPED(停止)--进程停止执行:进程没有投入运行,也不能投入运行.通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候,此外,在调试期间接收到的任何信号,都会使进程进入这种状态.
20.UNIX创建进程的方式:
 许多其它的操作系统都提供了spawn进程的机制:先在新的地址空间里创建进程,读入可执行文件,最后开始执行.
 UNIX采用了与众不同的机制:把上述步骤分解到两个单独的函数中去执行(fork和exec)。首先fork通过拷贝当前的进程创建一个子进程。子进程与父进程的区别仅仅在于PID,PPID和某些资源以及统计量;exec函数负责读入可执行文件并加载到新创建的子进程的地址空间并开始运行.
21.Linux的fork系统调用采用写时拷贝(copy-on-write)机制.写时拷贝是一种推迟甚至是免除数据拷贝的技术.内核并不是复制整个进程地址空间,而是让父进程和子进程以只读的方式共用一个拷贝。只在在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝.
22.fork系统调用是通过clone系统调用来实现的.
23.fork系统调用后,系统有意让子进程先执行。因为一般子进程会马上调用exec函数,这样可以避免写时拷贝的额外开销,如果父进程先执行,有可能会开始向地址空间写入.
24.vfork:vfork系统调用和fork系统调用的功能类似,除了不拷贝父进程的页表项,子进程作为父进程的一个单独的线程执行,父进程被阻塞,直到子进程退出运行,子进程不能向地址空间中写入。
25.concurrent与parallelism的区别:
 concurrent是并发,如果在单核的系统上,并不是同时多个任何同时运行,只有在多核的系统上,才可能有多个进程同时并行运行,这才是真正意义上的并发,也就是并行.
 parallelism是并行,只针对于多核系统而言,指在任意一个时刻,系统上有多个的任务在不同的CPU上同时运行.
26.Linux把所有的线程都当作是进程来处理.线程仅被视作一个与其它的进程共享一些资源的进程
27.Windows和Sun Solaris则是从内核的角度来支持线程.
28.内核线程:独立运行于内核空间的标准进程,用于执行一些后台操作.内核线程与用户线程的区别在于,它们没有独立的地址空间,它们只运行于内核空间,从来不会切换到用户空间去运行.内核线程也可以被调度和抢占.
29.孤儿进程的处理:在当前进程的进程组内找一个进程作为父进程,如果不行,那么就让init做为它们的父进程.

chapter 4
30.调度程序:可以看作是在可运行态的进程之间分配有限的处理器时间的内核子系统.
31.多任务系统分为:非抢占式和抢占式多任务系统.
32.抢占式多任务:由调度程序来决定什么时候停止一个正在运行的进程而使得其它的进程有可执行的机会.
33.进程时间片:分配给每个可运行进程的处理器时间段.
34.非抢占式多任务:除非进程自已退出运行,否则它会一直点用处理器.
35.进程分为:处理器消息耗型和I/O消耗型.
36.优先级高的进程所获得的时间片也更长,调度程序总是调度时间片未用尽而优先级又最高的进程运行.
37.调度程序会提高I/O消耗进程的优先级,降低处理器消耗进程的优先级.
38.Linux内核提供两组独立的优先级范围:
 a. nice值,从-20到19,nice级越大,优先级越低.
 b. 实时优先级,其值是可以配置的,默认情况下是从0到99,任何实时进程的优先级都高于普通进程.
39.进程时间片:它是一个整数值,表示进程在被抢占之前可以连续运行的最长的时间.进程的时间片不需要一次性用尽,可以分多次用完,这样,一个进程可以被调度运行多次,这对于I/O消耗类型的进程非常有利
40.当一个进程的时间片耗尽时,就认为进程到期了。没有时间处的进程不会再次被调度运行,要等到其它的所有的进程都耗尽了它们的时间片,也就是说剩余的时间片为0,在那个时间,所有的进程的时间片会被重新计算.
50.每个处理器都有一个任务队列,这个任务队列里面有一个“可运行进程优先队列”和一个“已过期进程优先级队列”,当一个进程的时间片耗尽时,它会被从“可运行队列”移动到“已过期队列”,在移动在过期队列之前,它的新的时间片会被重新计算好。当“可运行队列”为空,也即是当前的CPU上的可执行的进程的时间片都已经耗尽时,这个时候会交换“可运行队列”和“过期队列”,这样“过期队列”中的所有的“已重新计算好时间片”的进程已可以重新投入运行,这就是Linux内核中O(1)调度程序的核心.
51.每一个CPU都有一个对应的schedule调度函数,用于决定当前的CPU上下一个可以执行的进程.
52.对于内核而言,如果一个进程的睡眠时间比运行时间长,那么这个进程是I/O消耗型的;如果一个进程的运行时间比睡眠时间长,那么这个进程是处理器消耗型的.
53.当一个新的子进程创建时,子进程会和父进程均分剩余的时间片,这样就可以避免用户通过不停地创建子进程来不停地攫取时间片.
54.重新计算进程的时间片时,只依据进程的静态优先级,这个优先级在进程状态时由用户指定,一旦指定,这个优先级就不会被改变.优先级越高,进程所获得的进程运行时间片就越长.
55.内核对进程“休眠”和“唤醒”的处理:
 当进程“休眠”时,进程将自己标识为“休眠”状态,把自己从“可执行队列”移动到“等待队列”。
 当进程“唤醒”时,进程被设置为“可执行状态”,然后被从“等待队列”移动到“可执行队列”.
56.进程的用户抢占:是指进程在内核空间返回到用户空间时或是从中断处理程序中返回到用户空间时,如果进程的need_resched标志被重新标记了,那么进程就需要被重新调度.
57.在Linux系统中,内核进程也可以被抢占.只要没有锁,内核就可以进行抢占.
 内核抢占发生的时间:
 a. 当从中断处理程序正在执行,且返回内核空间之前.
 b. 当内核代码再一次具有可抢占性的时候.
 c. 如果内核中的任务显示地高度schedule。
 d. 如果内核中的任务阻塞(这同样也会导致调用schedule).


chapter 5
58.系统调用的作用:
 a. 为用户空间提供了一种硬件的抽象接口.
 b. 系统调用保证了系统的稳定和安全.
 c. 系统调用是用户空间访问内核的唯一手段.
59.UNIX的设计原则是:提供机制而不是策略.换句话说,UNIX系统抽象出了用于完成某种确定目的的函数,至于这些函数怎么用完全不需要内核去关心.
60.系统调用的实现原理:
 用户空间无法直接执行内核代码,它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上,如果进程可以直接在内核地址空间上读写的话,系统安全就会失败控制.
 所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了.
 通知内核的机制是靠软中断:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。X86系统上的软中断由int 0x80指令产生.这条指令触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序是系统调用处理程序.这个异常处理程序的名字叫作system_call。最近x86增加了一条叫做sysenter的指令,与int中断指令相比,这条指令提供了更快,更专业的陷入内核执行系统调用的方式.
61.当内核接收一个用户空间的指针时,内核 必须保证:
 a. 指针指向的内存区域属于用户空间,进程决不能哄骗内核去读取内核空间的数据.
 b. 指针指向的内存区域在进程的地址空间里,进程决不能哄骗内核去读取其它的进程空间的数据.
 c. 如果是读,该内核区域必须被标记为可读,如果是写,该内存区域必须被标记为可写。进程决不能绕过内存访问控制.
62.硬件与内核通信的机制是:中断机制,当硬件需要和系统通信时,硬件向系统发送一个中断请求,中断在本质上是一种特殊的电信号。这个中断请求实际上并不是硬件直接发送给内核的,而是硬件先发送给CPU,再由CPU向内核发送中断信号,内核再中断当前的工作调用相应的中断处理程序,从而完成内核与硬件的通信.硬件的中断可以随时产生,因此,处理器也需要随时响应中断信号.硬件中断不用考虑与处理器的时钟同步问题,但是“异常”需要考虑与处理器的时钟同步问题.
63.每个中断信号都有一个唯一的数值.这样内核才能区分中断信号来自于哪个硬件,更进一步,内核才能为对应的硬件中断信号调用其中断处理程序.
64.中断信号的值被称为“中断请求线”(IRQ).
65."异常"必须考虑与处理器的时钟同步问题,因此“异常”也被称作为“同步中断”.在处理器遇到编程失误或是特殊情况而需要由内核来处理的时候,处理器就产生一个“异常”,因为许多体系结构处理“异常”与处理“中断”的方式很相似,所以,内核对于它们的处理也很相似.
66.“中断”必须是由硬件产生的,而“异常”可以是由软件产生的.
67.在响应一个中断的时候,内核会执行一个函数,这个函数叫做“中断处理程序”(interrupt handler)或者是“中断服务例程”(interrupt service routine,ISR)。中断处理程序是和特定的中断关联,而不是和硬件关联的,这样一来,如果一个硬件可以产生多种中断,那么它的设备驱动程序就要提供多个中断处理程序.
68.中断处理程序与其它的内核函数的区别在于:中断处理程序是被内核用来响应中断的,而且它们只运行在我们称之为“中断上下文”的特殊上下文中.
69.中断处理程序一般被切分为两部分完成:上半部和下半部.
 a. 上半部用于完成有严格的时限的工作.
 b. 中断处理程序中能够被推迟到稍后完成的工作就被放到下半部中.
70.设备驱动程序:实际上就是对设备所产生的中断进行处理的中断处理程序的集合,所有的这个硬件设备的中断处理程序被一起提供给内核用于内核完成该硬件设备的中断处理.
71.中断共享:中断共享的函数就是“一个中断信号值可以由多个硬件产生”.
72.如果某个中断信号被屏蔽,那么当硬件设备向处理器发送对应的中断电信号时,在处理器将这个中断通知内核时,内核就不会去响应这个中断信号.
73.Linux中的中断处理程序是无需重入的.当一个给定的中断处理程序正在执行时,这个中断在所有的处理器上都会被屏蔽掉,以防止在同一个中断线上接收另一个新的同样的中断信号.通常情况下,所有的其它的中断都没有被屏蔽而是打开的,所有这些不同的中断线上的其它中断都能够被响应,但当前的中断线总是被禁止的.由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断,这极大地简化了中断处理程序的编写.
73.内核接收到中断后,会依次调用当前的中断线上注册的每一个中断处理程序.
74.进程上下文是针对内核而言的,它是指内核所处的操作模式,此时内核代表进程执行.
 中断上下文.中断上下文与进程没有瓜葛,因为没有进程背景,所有中断上下文不可以睡眠--否则又怎能被重新调度呢?因此不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在中断处理程序中调用它,这是对什么表函数可以在中断上下文中调用的限制.
 中断上下文有严格的时间限制,因为它打断的其它的代码的执行.中断上下文中的代码应当快速简洁,尽量不要使用循环去处理复制的工作,有一点非常重要,请永远牢记:中断处理程序打断了其它的代码(甚至可能是其它的中断线上的另一个中断处理程序).
75.中断处理机制的实现.
 中断处理系统在Linux系统中的实现是依赖于体系结构的,想必你对此不会感到特别惊讶。实现依赖于处理器,所使用的中断处理器的类型,体系结构的设计及机器本身.
 设备产生中断,通过中断控制总线把电信号发送给中断控制器,如果中断线是激活的(它们允许被屏蔽),那么中断控制器就会把中断发往处理器.在大多数的体系结构上,这个工作就是通过电信号给处理器的特定的管脚发送一个电信号。除非在处理器上禁止该中断,否则,处理器就会停止它正在做的事,关闭中断系统,然后跑到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口.
 在内核中,中断的旅程开始于预定义的入口点,这类似于系统调用通过预定义的异常句柄进入内核,对于每条中断线,处理器都会跳到一个唯一的位置,这样,内核就可以知道所接收的中断的IRQ号了,初始入口只是在栈中保存这个号,并存放当前的寄存器的值,然后内核调用do_IRQ.从这里开始,大多数的中断处理代码都是用C写的.
76.Linux内核提供了一组接口用于操作机器上的中断状态,这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力.


chapter 7
77.中断处理流程分为:上半部和下半部两部分.
 上半部:中断处理程序.
 下半部:Linux中将响应中断的一部分操作推迟到之后完成的机制.
78.上半部和下半部的区分取决于开发者自己的判断,通常的规则是:
 a. 如果一个任务对时间非常敏感,将其入在中断处理程序中执行.
 b. 如果一个任务和硬件相关,将其入在中断处理程序中执行.
 c. 如果一个任务要保证不被其它的中断(特别是相同的中断)打断,将其入在中断处理程序中执行.
 d. 其它的任务,考虑放置在下半部中执行.
79. Linux内核提供的3种下半部机制:
 a. 软中断:是在编译时指定的softirq_action结构体数组,一般有32项.一个软中断不会去抢占另一个软中断,实际上,唯一可以抢占软中断的是中断处理程序,不过其它的软中断,甚至是其它的相同类型的软中断,可以在其它的处理器上同时执行.
执行软中断:一个注册的软中断必须在被标记为才会执行,这被称作“触发软中断”,通常中断处理程序会在返回前标记它的软中断,使其在稍后执行.在以下时刻,软中断会被检查和执行:
 a). 从一个硬件中断代码返回时.
 b). 在ksoftirqd内核线程中.
 c). 在那些显示检查和执行待处理的软中断的代码中,如网络子系统中.
内核定时器和tasklet都是建立在软中断的基础之上的.
 b. tasklet:基于软中断实现.
 c. 工作队列:可以把中断处理程序中的后续工作推迟到后续去完成,交由一个内核线程去执行,这个下半部在“进程上下文”中执行.工作队列允许重新调度甚至是睡眠.
 如果推迟的工作可以需要睡眠,那么使用任务队列;如果推迟的工作不能进行睡眠,那么使用软中断或tasklet.工作队列最基本的表现形式就成了把需要推迟执行的任务交给一个特定的通用线程来处理这样的一个接口。默认的工作者线程叫做:events/n。这里的n是处理器的编号;每个处理器一个对应的线程.比如,用于处理工作队列的工作者线程可以自行创建,但是有一个默认的工作者线程.


80.当你需要保证工作被推迟到某一个指定的时间去执行时,那么你需要使用内核定时器机制.
81.每个处理器都有一个对应的“软中断辅助处理线程”:ksoftirqd/n。
82.临界区:就是访问和操作共享数据的代码断.代码在执行完成之前不能够被打断.
83.处理器会保证任何的两个处理器原子指令不会同时执行.也就是说:如果有两个原子A,B指令要执行,那么要么A执行完之后再执行B,要么B执行完后再执行A,不可以出现A执行一半再执行B然后再执行A,最后又执行B的情况.
84.内核中有可能造成并发执行原因:
 a. 中断--中断几乎可以在任何时刻发生,也就是可能打断当前正在执行的代码.
 b. 软中断和tasklet--内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码.
 c. 内核抢占--因为内核具有抢占性,所以内核中的任务可能会另外一个任务抢占.
 d. 睡眠及与用户空间的同步--在内核执行的进程可能睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行.
85.对称多处理器--两个或多个处理器可以同时执行代码.以及如何安排同步的顺序.
86.并发开发的难点就在于找出所有的潜在的可能发生竞争和数据同步的地方.
87.几种安全代码:
 a. 中断安全代码:在中断处理程序中能避免被并发访问的代码.
 b. 对称多处理器安全代码:在对称多处理器中能避免被并发访问的代码.
 c. 抢占安全代码:在内核抢占时能避免并发访问的代码.
88.在并发开发中:我们实际上要保护的是数据而不是代码.
89.线程中的局部数据不需要加锁,因为线程上的局部数据是存储在线程的栈空间的中的,特定于每一个线程的栈空间中都有一份自己的线程局部数据,它们互不影响.一条原则时,如果公有数据可能同时被多个线程访问,那么这些公有数据需要被访问.
90.避免死锁的方法:
 a.操作公共数据的线程都以相同的顺序去获取锁.
 b. 对锁的获取和释放加上序号.
 c. 不要在一个线程中重复请求同一个锁.
 d. 以获取锁的相反顺序来释放锁.
 e. 加锁的精髓在于力求简单的方案.
91.原子操作:执行过程不会被打断的操作.
92.读写内存中的一个字的操作是原子操作,也就是说,在对这个内存字的读的过程中,不会出现对该字的写的过程,在对该字的写的过程中,不会出现对该字的读的过程.
93.原子性:确保指令执行期间不会被其它的操作打断,也就是说这个指令一次性完成,在这条指令完成的过程中,不会有其它的指令执行.
94.顺序性:两条或多条指令出现在独立的执行线程中,甚至独立的处理器上,但它们本该执行的顺序依然要得到保持.
95.自旋锁为多处理器机器上提供了防止并发访问的数据所需要的锁保护机制.
96.Linux下的自旋锁是不可递归的.
97.当一个线程在试图获取自旋锁时,如果这个锁已被其它的线程所获取,那么这个等待的线程不会因为等待这个自旋锁而休眠,相反,这个等待的线程会一直尝试去获取这个自旋锁。
98.自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断程序中调用 自旋锁时,一定要在获取锁之前首先禁用本地中断(在当前的处理器上的中断请求),否则,中断处理程序可能会打断正持有锁的内核代码,有可能会试图争用这个已经被持有的自旋锁,这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理过程处理完成之前不可能运行,这正是我们在前一章节中提到的“双重请求死锁”。注意,需要关闭的只是当前的处理器上的中断,如果中断发生在不同的机器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同的处理器上)最终释放锁.
99.要记住一点:在任何时候,我们加锁是为了锁住数据,而不是为了锁住代码.
100.对于自旋锁和下半部:
 在与下半部配合使用时,必须小心地使用锁机制。函数spin_lock_bh用于获取指定的锁,同时它们禁止所有的下半部的执行,相应的spin_unlock_bh函数执行相反的操作.
 由于下半部可以抢占进程上下文中的代码,所以,当下半部与进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行(不然下半部又可以会抢占进程上下文)。同样,由于中断处理程序可以抢占下半部,所以,如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断.
 同类的tasklet不可能同时运行,所以对于同类的tasklet中的共享数据不需要加锁保护,因为同类的tasklet的任何必定是严格的串行完成,如果同一个处理器上的同类的tasklet任务A和B,在A没有完成之前,B绝对不会开始运行,只有在A完全完成之后,B才开始运行.但是当数据被不同类的tasklet共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁。这里不需要禁止下半部,因为在同一个处理器上,决不会有tasklet相互抢占的情况发生.
 对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护,因为即使是同种类型的两个软中断也可以同时运行在一个系统的多个处理器上,但是,同一处理器上的一个软中断绝不会抢占另一个软中断,因此根本没有必须抢占软中断.
101.信号量:信号量支持两个原子操作P和V,前者叫做测试操作,后者叫做增加操作,后来系统把这两个操作分别叫做down和up。down通过对信号量计数减1来请示获得一个信号量,如果结果是0或者大于0,那么获取锁成功,进入到临界区中.如果结果是负数,那么任务就会被放入到等待队列,对应的进程也会进入休眠.处理器此时可以执行其它的操作.相反,在临界区中的操作完成之后,通过up操作来释放信号量,该操作也被称作是提升信号量的值,因为它会增加信号量的计数,如果在该信号量上的等待队列不为空,那么处于队列中的等待的任务就会被唤醒同时获得该信号量.
102.内核中的 barrier的作用是保证:一个操作必须在另一个操作之前完成这一点不会被编译器或者是处理器改变.

chapter 10
103.系统定时器:一种可编程的硬件芯片,它能以固定频率产生中断,这种中断就是所谓的“定时器中断”.它所对应的中断处理程序负责更新系统时间,还负责执行需要周期性执行的任务.系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢.
104.动态定时器: 一种用来推迟执行程序的工具,内核可以动态创建或销毁动态定时器.
105.系统定时器以某种固定的频率自动触发时钟中断,这种频率可以通过编程预定,称作节拍率,当时钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理.因为预编的节拍率对内核来说是已知的,所以内核知道两次连接的时钟中断间隔的时间,这个间隔时间就被称作:节拍(tick)。它等于节拍频率分之一,内核就是靠这种时钟间隔来计算墙上时间和系统时间.
106.jiffies:记录自系统启动以来的时钟节拍数.
107.实时时钟:RTC,是用来持久存放系统时间的设备,即使系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时,在PC体系结构中,RTC和CMOS集成在一起,而且RTC的运行和BIOS的保存设置都是通过同一个电池供电的.
108.实时时钟的最大作用是在启动时初始化xtime变量.
109.系统定时器:通过对电子晶振进行分频来实现系统定时器.
110.在X86系统中,主要采用可编程中断时钟(PIT)。在X86系统中还包括本地APIC时钟和时间戳计数(TSC)等时钟资源.
111.时钟处理程序:时钟处理程序可以分为两部分,体系结构相关的部分和体系结构无关的部分。
 与体系结构相关的部分作为系统定时器的中断处理程序而注册到内核中,以便产生时钟中断时,它能够相应地运行。
112.动态定时器:它并不周期性地运行,它在超时后就自行销毁,这也是这种定时器被称为动态定时器的原因 ,动态定时器不断创建和销毁,而且它的运行次数也不受限制。
113.定时器会在指定的定时值到达之后开始运行.在运行完成之后,这个定时器会被删除,所以如果你想要一个定时函数周期性地运行下去,那么你需要在定时器超时后重新设定定时器,也就是在定时器处理函数的最后重新设定这个定时器.
114.volatile变量可以强制使得编译器在每次访问变量时都重新从主内存中获取而不是通过寄存器中的变量的别名来获取,这样就可以保证变量的值永远都是最新的.
115.经验表明,不要使用udelay来处理超过1毫秒的延迟,在延迟超过1毫秒的情况下,使用mdelay更为安全.这些函数的实现使用基于循环的忙等待.
116.MIPS:Million Instruction Per Second。处理器每秒执行的百万条指令数.

chapter 11
117.内存页:内核把物理内存页作内存管理的基本单位,尽管处理器的最小可寻址单位通常是字(甚至字节),但是,内存管理单元(MMU:管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位来进行处理,正因为如此,MMU大小为单位来管理系统中的页表(这也是页表名的由来),从物理内存的CPU角度来看,内存的最小单位是字节,但是从虚拟内存的角度来看,页就是最小单位.
118.在32位机上,一个内存页的大小是4K,而在64位机上,内存页的大小为8K。所以在32位机上,1G的内存会被分成262144个页.
119.内存区:内核并不是对所有的内存页都一致对待,内核使用区对具有相似特性的页进行分组。
 ZONE_DMA:这个内存区的内存页可以执行DMA(直接内存访问操作).X86上为:0 ~ 16M
 ZONE_NORMAL: 这个区包含的正常可寻址的页.X86上为: 16M ~896M
 ZNOE_HIGHMEM: 这个区中的页并不能永久地映射到内核地址空间。X86上为: > 896M
120.内核slab层:slab层把不同的对象划分为所谓的“调整缓存”组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应于一个高速缓存,例如,一个高速缓存用于存放进程描述符,另一个高速缓存用于存放索引节点.slab由一个或多个物理上连续的页组成.

 

chapter 12
121.虚拟文件系统:有时也称作虚拟文件交换(VFS),采用面向对象的设计思路,作为内核子系统,为用户空间提供了系统相关的接口,系统中所有的文件系统不但依赖于VFS共存,而且也依靠VFS系统协同工作。
122.UNIX文件系统:UNIX使用了4种与文件系统相关的传统抽象概念:
 文件:可以看作是一个有序的字节串,字节串中的每一个字节是文件的头,最后一个字节是文件的尾。
 目录项:用来容纳文件的结构.
 索引节点:文件相关信息,有时被称作文件的元数据(也就是说文件的相关的数据),被存储在一个单独的数据结构中,这个结构被称作索引结点( inode ).
 安装点:安装文件系统的特定的安装点.
123.文件系统:从本质上讲,文件系统是特殊的数据分层存储结构,包含文件,目录和相关的控制信息。在UNIX系统中,文件系统被安装在一个特定的安装点上,该安装点在全局层次结构中被称作命名空间,所有已安装的文件系统都作为根文件系统树的枝叶出现在系统中.
124.UNIX是面向流的文件系统,其它的操作系统中有面向记录的文件系统.UNIX将相关信息和文件本身这两个概念加以区分.
125.VFS中的4种主要对象:
 a. 超级块 对象:它代表一个已安装的文件系统.该对象存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块.
 b. 索引节点 对象:它代表一个文件.包含了内核在操作文件或目录时所需要的全部的信息.对于UNIX风格的文件系统来说,这些信息可以从磁盘的索引节点中直接读入.一个索引节点代表文件系统中(虽然索引节点仅当文件被访问时才在内存中被创建)的一个文件,它也可以是设备或管道这样的特殊的文件。
 c. 目录项 对象:它代表一个目录项,是路径的一个组成部分.路径中的每一个组成部分都由一个索引节点对象表示。虽然它们可以统一由索引节点表示。目录项对象没有对应的磁盘数据结构,VFS根据字符串的形式的路径来现场创建它们。
 d. 文件对象:它代表由进程打开的文件.是打开的物理文件在内存中的表示.同一个物理文件如果被多个进程打开,那么就会有多个对应的文件对象.文件对象只在内存中存在,并不存储在磁盘上.
     VFS中将目录当作一种特殊的文件来看待,所以不存在“目录对象”

126.目录项对象的状态:被使用,未被使用,负状态.
 a. 被使用状态:一个使用的目录项对应于一个有次的索引节点.
 b. 未被使用状态:未被使用的目录项对应于一个有效的索引节点,但是应指明,当前的VFS并没有使用它,该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早删除,所以,在以后需要它时,不必重新创建,从而使用路径查找更迅速,如果要回收内存的话,可以销毁未使用的目录项.
 c. 负状态:目录项没有对应的有效的索引节点,因为索引节点已被删除(也即是物理文件被删除),或路径不再正确了,但是目录项仍然保留,以便快速解析以后的路径查询。虽然负状态的目录项有些用处,但是如果需要的话,可以销毁它。
127.如果系统中有大量的进程都要打开超过32个文件,为了优化性能,管理员可以适当增大NR_OPEN_DEFAULT的值.

chapter 13
128.Linux系统中的设置类型分为“块设备”和“字符设备”
块设备:能够随机的访问固定大小数据片的设备,如果磁盘,软盘驱动器,CD-ROM。它们都是以安装文件系统的方式使用。
字符设备:字符设备按照字符流的方式被访问,像串口和键盘就属于字符设备。
这两种设备的本质区别在于是否可以进行随机访问.
129.块设备的最小的可寻址单元是扇区,扇区大小一般是2的倍数,而最常见的大小是512字节,扇区的大小是设备的物理属性,扇区是所有的块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区,虽然大多数设备的扇区的大小都是512字节,不过其它大小的扇区也是很常见的,比如,CD-ROM的扇区大小就是2K.
130.虽然物理磁盘都是按扇区级进行寻址的,但是内核却是基于块的方式来操作磁盘的,所以块必须是扇区大小的整数倍,而且要小于页面的大小,所以通常块的大小是512字节或是4K.
131.为了优化高度程序,内核会在提交I/O请求到磁盘之前所将这些请求进行“合并与排序”,从而每次I/O请求所消耗的时间.

 

chapter 14
132.进程的地址空间包括:
a.代码段:可执行文件代码的内存映射
b.数据段:可执行文件的已初始化全局变量的内存映射.
c.BSS的零页:包含未初始化全局变量的内存映射.
d.进程用户栈:不要和进程的内核栈混淆,进程的内核栈独立存在并由内核维护。
e.每一个诸如C库或动态连接程序等共享库的代码段,数据段和bss也会被载入进程的地址空间。
f.任何内存映射文件.
g.任何共享内存段.
h.任何匿名的内存映射,比如由malloc分配的内存.
进程地址空间中的任何有效地址都只能位于唯一一个区域,这些内存区域并不能相互覆盖,可以看到,在执行的地址中,每个不同的内存片段都对应一个独立的内存区域:栈,对象代码,全局变量,被映射的文件等等.
133.Linux中线程与进程的唯一区别几乎是:是否共享地址空间.
134.内核线程没有进程地址空间,也没有相关的内存描述符,所以内核线程对应的进程描述符中的MM域为空,事实上,这也正是内核线程的真实含义--它们没有用户上下文。
135.平坦地址空间:描述的是地址空间范围是一个独立的连续空间(比如从0扩展到429496729的32位地址空间)。
136.进程的地址空间之间互不相干.两个不同的进程可以在相同的地址空间上存放相同的数据,但是进程之间也可以共享地址空间,我们称这样的进程为线程.
137.VMA:虚拟内存区域.
138.页表:虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存,所以,当程序访问一个虚拟地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求,地址的转换工作需要通过查询页表来完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向下一级别的页表或者指向最终的物理页面.
139.Linux中使用三级页表完成地址转换。得用多级页表能够节约地址转换所需要占用的空间,但如果利用三级页表转换地址,即使是64位机器,占用的地址空间也是很有限的,但是如果使用静态数组来实现页表,那么即使是在32位机器上,该数组也将占用巨大的存放空间。Linux对所有的体系结构,所括对那些不支持三级页表的体系结构都使用三级页表进行管理.
140.三级页表结构:   
   每个进程的地址空间描述符
         |
     顶级页表(PGD)   --> 页全局目录
     |
     二级页表(PMD)   --> 中间页目录 
     |
     三级页表(PTE)   --> 指向实际的物理内存页面

 


chapter 15
141.页高度缓存:是Linux内核实现的一种主要的磁盘缓存,这主要用来减少对磁盘的IO操作,具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。
142.磁盘高速缓存的价值主要存在于两个方面:
a. 访问磁盘的速度要远远低于访问内存的速度,因此,从内存访问数据比从磁盘访问速度更快。
b. 数据一旦被访问,就很有可能在短期内再次被访问到。这种短时期内集中访问同一片数据的原理被称作“临时局部原理(temporal locality)”,临时局部原理能够保证,如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中(访问高速缓存中的数据).
143.页高速缓存是由RAM中的物理页组成的,缓存中的每一页对应着磁盘中的多个块,每当执行一次磁盘操作时,会首先检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免了磁盘访问.
144.缓冲区高速缓存:通过I/O缓冲区把独立的磁盘块与页高速缓存联系在一起,一个缓冲就是一个单独物理磁盘块在内存中的表示,缓冲就是内存到磁盘块的映射描述符,因此通过缓存磁盘块以及缓冲I/O操作,页高速缓存也可以减少对磁盘的访问量.缓冲区高速缓存实际上并不是一个独立的缓存,而是页高速缓存的一部分.
145.当页高速缓存中的数据比后台磁盘中的对就数据更新时,那么调整缓存中的这些缓存数据被称作“脏数据”,需要在后面写回到磁盘.


chapter 16
146.Linux是“单块内核”(monolithic)的操作系统--也就是说,整个系统都运行于一个单独的保护域中,但是Linux内核是模块化的,它允许在运行时动态地向其中插入或是从中删除代码.这些代码--包括子例程,数据,函数入口和函数出口被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块,或被简称为“模块”。支持模块的好处是基本内核镜像可以尽可能小,因为可选的功能和驱动程序可以利用模块的形式再提供,模块允许我们方便地删除和重新载入内核代码,也方便了调试。
147.模块被载入后,就会动态连接到内核,注意,它与用户空间的动态连接库类似,只有当显示被导出后的外部函数,才可以被动态调用。在内核中,导出内核函数需要使用特殊的命令:EXPORT_SYMBOL和
EXPORT_SYMBOL_GPL。导出的内核函数可以被模块调用,而未导出的函数模块则无法被调用,模块代码的链接和调用规则相比核心内核镜像中的代码而言,要更加严格,核心代码在内核中可以调用任意非静态接口,因为所有的核心代码文件被链接成了同一个镜像,当然,被导出的符号表所含的函数必然也是非静态的.导出的符号表被看作是导出的内核接口,甚至称为“内核API”.


chapter 17
148.设备模型:设备模型专门提供了一种独立的机制来专门表示设备,并描述其在系统中的拓扑结构。保证能以正确的顺序关闭各设备的电源是设备模型的最初动机.
149.内核事件层:实现了内核到用户的消息通知系统,就是建立在上文一直讨论的kobjects基础之上的.


chapter 18
150.内核提供了printk这个函数用于显示调试信息.在任何时候,任何地方都可以调用它,它可以在中断上下文中调用,可以在进程上下文中调用,可以在持有锁时调用,可以在多处理器上同时调用,而且调用者连锁都不必使用.
151.神奇的SysRq
在i386和PPC上,它可以通过: ALT + PrintScreen 来访问:该功能可以通过CONFIG_MAGIX_SYSRQ配置选项来启用。
SysRq-b 重新启动机器
SysRq-e 向init之外的所有的进程发送SIGTERM信号
SysRq-h 在控制台显示SysRq
SysRq-i 向init之外的所有的进程发送SIGKILL信号
SysRq-k 安全访问键,杀死这个控制台上的所有程序
SysRq-l 向包括init的所有的进程发送SIGKILL信号
SysRq-m 所内核信息输出到控制台
SysRq-o 关闭机器
SysRq-p 所寄存器的信息输出到控制台
SysRq-r 关闭键盘原始模式
SysRq-s 把所有已安装文件系统刷新到磁盘
SysRq-t 所任务信息输出到控制台
SysRq-u 卸载所有已安装文件系统.
152.内核调试的利器: kdb

chapter 19
153.人们通常所说的机器是多少位,它们其实说的是机器的字长是多少位,也就是一个字的bit数.
154.处理器的通用寄存器的大小和它的字长是相同的。对于一般的体系结构来说,它的各个部件的宽度,比如,内存总线--最少要和它的字长一样大,地址空间的大小也等于字长.
155.Linux类型总对应于机器的字长.所以,我们可以通过 sizeof( long ) 为4还是8来判断是32位机还是64位机.一个指针变量的大小与寄存器的字节一致。32位机上是4字节,64位机上是8字节.
156.一个char的长度恒为 8 bit。在Linux支持的所有的系统上,int 为 32 bit
157.数据对齐:
 对齐是跟数据块在内存中的位置相关的话题,如果一个变量的内存地址正好是它的长度的整数倍,那么它就被称为自然对齐.举例来说,对于一个32位类型的数据,如果它在内存中的地址刚好可以被4整除,也就是地址的最低两位为0,那它就是自然对齐的,也就是说同个大小为2n字节的数据类型,它的地址的最低有效痊的后N位都应该是0.一些体系结构对对齐的要求非常严格,通常基于RISC的系统,载入未对齐的数据会导致处理器陷入(一种可处理的错误).还有一些系统可以访问没有对齐的数据,只不过性能会下降,编写可移植的代码要避免对齐问题,保证所有的类型都能够自然对齐.
158.避免对齐引发的问题
通常编译器会通过让所有的数据自然对齐来避免引发对齐问题,实际上,内核开发者不用在对齐上花费太多心思,只有搞GCC的那些老兄才应该为此犯愁。可是当程序员使用指针太多时,数据的访问方式走出编译器的预期时,就会引发问题了。
一个数据类型长度较小,它本来是对齐的,如果你用一个指针进行类型转换,并且转换后的类型长度较大,那么通过解引用指针进行数据访问时就会引发对齐问题(无论如何,对于某些体系结构确实存在这种问题),也就是说,下面的代码是错误的:
char dog[10];
char *p = &dog[ 1 ];
unsigned long l = *( unsigned long *)p;
这个盒子将一个指向char型的指针当作指向unsigned long型的指针来用,这会引起问题,因为此时试图从一个不能被4整除的内存地址上载入32位的unsigned long 型的数据.
159.非标准类型的对齐
前面说到了,对于标准数据类型来说,它的地址只要是长度的整数倍就对齐了,而非标准类型的C结构体按照下列规则对齐:
 a. 对于数组,只要按照基本数据类型对齐就可以了(其实随后的所有的元素自然能够对齐)
 b. 对于联合,只要它包含的长度最大的数据类型能够对齐就可以了。
 c. 对于结构体,只要它包含的长度最大的数据类型能够对齐就可以了,也就是结构体整体上来按结构体中长度最大的一个成员来对齐,这就是说,结构体的最终的大小要是其长度最大的成员的大小的整数倍;同时,对于结构体中的每一个成员,都要自身按照对应的规则进行对齐.
160.为了保证结构体中的每一个成员都能够自然对齐,结构体要进行“对齐填补”.

161.ANSI C 标准明确规定:不允许编译器改变结构体成员的顺序.
162.内核开发者需要注意结构体填补问题,特别是在整体使用时,这是指当需要通过网络发送它们或是需要将它们写入文件的时候,因为不同的体系结构之间需要的填补也不尽相同,这也是为什么C没有提供一个内建的结构体比较操作符的原因之一,结构体内的填充字节可能会包含垃圾信息,所以,在结构体之间进行一字节一字节的比较就不大可能了。
163.因为结构体可能有填充对齐的问题,所以,对于不同的相同类型的结构体对象,不能直接使用
memcmp来比较,而要直接成员之间的比较.
164.字节序:
 字节序是指一个字中各个字节的顺序。处理器在对字取值时,既可能将最低有效位所在的字节作为每一个字节(最左边的字节),也可能将其作为最后一个字节(最右边的字节).如果最高有效位放在最高位上,其它的字节依次放在低字节位置上,那么这种字节序称为:高位优化(big endian);如果最低有效位放在最高位上,那么这种字节序称为:低位优先(little endian )
165.编写可移植的代码
a. 编码尽量取最大公因子:假定任何事情都可能发生,任何潜在的约束也都存在
b. 编码尽量选取最小公约数:不要假定给定的内核特性是可用的,且仅仅需要最小的体系结构功能.


chapter 20
166.内核开发的优秀站点:
 www.kernel.org
 www.kernelnew-bies.org
 www.lwn.org
 www.kerneltraffix.org

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics