LinuxKernelDevelopment_读书笔记

来源:互联网 发布:数据服务 编辑:程序博客网 时间:2024/05/20 13:17
一.内核概念
1.CPU在任何时间点上的活动都处于如下情况中:
A.运行于内核空间, 在进程上下文中
B.运行于内核空间, 在中断上下文中
C.运行于用户空间, 在用户进程中
即使CPU空闲时,它也在某个空进程中,属于A情况

2.IPC--进程间通信


3.Linux内核版本只分两种: 稳定版 和 开发版
版本命名采用三段式: 主版本.从版本.修订版 (从版本为偶数表示稳定版, 奇数则为开发版, 如2.6.3则为稳定版)
主版本和从版本一起构成"内核系列", 如2.6版内核系列

4.内核栈是固定的, 32位机的内核栈为8KB, 64位机的内核栈为16KB


5.同步和并发: 当并发的访问共享数据时,就要求有同步机制保证不会出现竞争条件
如:多处理器系统中, 多个处理器同时访问同一个资源
如:中断异步到来时, 当前正在访问某个资源, 而中断处理也是去访问该资源

解决竞争的常用方法: 自旋锁 和 信号量


二.进程管理
6.进程(Linux中也叫任务task):
A.不仅仅包含可执行程序代码段, 还包含用到的其他资源, 如打开的文件, 挂起的信号, 内核内部数据, 处理器状态, 地址空间, 一个或多个线程等.
B.程序本身并不是进程, 进程 = 处于执行期的程序 + 包含的所有资源, 可以简单地抽象理解为Gradle概念的SourceSet.
C.实际上多个进程完全可能执行的是同一个程序, 并且多个进程也可以共享如文件, 地址空间等资源.

7.进程的创建:
调用fork()系统调用通过复制一个现有进程来创建一个全新的进程, 而调用者称为父进程, 创建出来进程称为子进程. 接着调用exec*()这族函数创建新地址空间, 并把可执行文件载入到该地址空间中执行.
一旦fork()函数执行结束时, 父进程恢复执行, 子进程开始执行.
所有的进程都是PID为1的init进程的后代, 故每个进程必有一个父进程, 相应的, 每个进程可有0个或多个子进程
拥有同一个父进程的所有进程称为兄弟, 进程间的关系存放在进程描述符中, 故可通过这种继承体系从任意进程出发查找到任意指定的其他进程.

注: Linux中创建进程采用 写时拷贝(copy-on-write) 技术, 即调用fork()时, 内核并不复制整个父进程地址空间, 而是让父进程以只读的方式共享给子进程, 只有当子进程
中的页需要写入时, 父进程中的数据才会被复制, 这种技术使得地址空间上页的拷贝被推迟到实际发生写入的时候才进行.
故在页不需要被写入的情况下, 如fork()后立即调用exec(), fork()的实际开销仅是为子进程创建唯一的进程描述符

clone()函数是fork()的实现. 故调clone()与fork()是一样的

新创建的子进程与父进程平分父进程剩余的时间片.

8.进程的结束:
调用exit()系统调用结束进程, 释放其占用的资源(但仍保留进程描述符, 供父进程获知它的信息, 只有当父进程通知内核它并不关注那些信息后, 进程描述符才会被释放), 并告知父进程. 
父进程也可以通过wait4()系统调用来查询子进程是否结束, 这使得父进程拥有等待子进程执行完毕的能力.
进程结束后被设置为僵死状态, 直到父进程调用wait()或waitpid()为止.

9.孤儿进程:
如果父进程在子进程之前退出, 则子进程退出时永远处于僵死状态, 白白耗费内存.
解决: 给子进程在当前线程组内找一个线程作为父亲, 如果不行, 就让init进程做为它们的父进程.

9.线程:
是进程中的活动对象, 每个线程都拥有独立的程序计数器, 进程栈, 进程寄存器. 内核调度的对象是线程, 而不是进程.
而Linux并不对线程和进程做特别区分, 认为线程只不过是一种特殊的进程, 每个线程都拥有自己唯一的进程描述符.
故对内核来说, 线程就是一个普通的进程(只是该进程与其他进程共享某些资源, 如地址空间, 打开的文件等)

10.线程的创建:
与进程的创建一样, 唯一的区别仅在于调用clone()时, 需传递一些参数来指明哪些资源被共享.
如: clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0) 表明父子俩共享地址空间, 文件系统资源, 文件描述符, 信号处理程序
即: 对Linux内核来说, 在创建新进程时, 若指明了共享资源, 则新创建的子进程就是所谓的线程.

11.进程的两种虚拟机制: 虚拟处理器 和 虚拟内存
虚拟处理器: 虽然实际上可能是许多进程正在共用一个处理器, 但虚拟处理器给进程一种假象, 让这些进程觉得自己在独享处理器
虚拟内存:   让进程在获取和使用内存时觉得自己拥有整个系统的所有内存资源

注: 同一个进程中的多个线程可以共享虚拟内存, 但拥有各自的虚拟处理器.

12.进程描述符:
内核把所有进程存放在 任务队列(task list) 的双向循环链表中, 该链表中的每一项都是 进程描述符 的结构, 
进程描述符包含一个具体进程的所有信息, 如PID, 打开的文件, 地址空间, 挂起的信号, 进程状态, 进程间关系等

13.PID:
内核通过一个唯一的进程标识值PID(process identification value)来标识每个进程, PID可取的最大值表示系统允许同时存在的进程最大数
由于任务队列是双向循环链表, 故PID最大值越小, 转一圈就越快

14.进程状态: 每个进程都必然处于五种进程状态中
A.运行(Task_Running)--------------位于可执行队列中
B.可中断(Task_Interruptible)------休眠的一种, 如果接收到一个信号会被提前唤醒并响应该信号, 不一定非要等到事件来唤醒, 即伪唤醒
C.不可中断(Task_Uninterruptible)--休眠的另一种, 会忽略信号, 只能由事件唤醒, 它和B位于同一个等待队列中
D.僵死(Task_Zombie)-----指进程调用exit()之后的状态, 内核释放其占用的资源, 不再投入运行, 但仍保留进程描述符供父进程获知其信息
E.停止(Task_Stopped)----指D状态的进程, 且父进程通知内核它并不关注其信息后, 内核进一步释放剩余的进程描述符, 真正死亡

三.进程调度
15.进程调度本质: 是使所有进程有效工作的代码段, 故也称调度程序.

16.进程调度功能: 负责选择下一个要运行的进程, 负责分配CPU的时间资源.
即只要系统中进程数比处理器数多, 在某指定时刻必然有一些进程在等待执行, 在这些等待进程中选择一个来执行, 是进程调度的基本工作.

17.单处理器: CPU在多个进程间切换执行, 对用户层面会产生多个进程同时运行的幻觉, 并不是真正意义上的并行.
  多处理器: 多个进程在不同的处理器上真正同时, 并行地运行.
  
18.Linux提供抢占式多任务模式: 即由调度程度来决定什么时候停止一个进程的运行以便其他进程能够得到执行机会, 这个强制的挂起动作就叫抢占.
  进程在被抢占之前能够运行的时间是预先设置好的, 即时间片, 而Linux采用动态方法来计算时间片.
  
  相反, 在非抢占式多任务模式下(现只有Mac OS 9之前采用), 除非进程自己主动停止运行, 否则它会一直执行, 进程主动挂起自己的操作叫让步. 这种模式有很多缺点如:
  A.调度程序无法对每个进程该执行多长时间做出统一规定
  B.一个决不做出让步的悬挂进程能使系统崩溃
  
19.I/O消耗型进程: 响应快, 执行短(如键盘I/O, 磁盘I/O), CPU大部分时间处于等待(如键盘I/O, 无论用户打字速度多快, 都不可能赶上处理速度)----影响响应速度(即影响用户交互)
  处理器消耗型进程:    除非被抢占, 否则一直占用CPU执行(如视频解码, 除最开始从磁盘上读出原始数据流和最后把处理好的视频输出)----影响最大吞吐量(希望分到时间片越长越好)
  
  判断标准: 根据CPU执行该进程时, 用于休眠和用于执行的时间长短来判断. 
如果某个进程大部分时间都在休眠, 那么它就是I/O消耗型.
如果某个进程执行的时间比休眠的时间长, 则为处理器消耗型.
  
20.Linux使用动态优先级的调度方法, 优先级高的进程先执行, 得到时间片也较长. 即最初设置为正常的优先级, 当调度程序判断该进程偏向于I/0消息型, 则会动态提高
  其优先级, 相反, 如果该进程偏向于处理器消耗型, 则调度程序会动态地降低其优先级.
  
21.Linux调度程序提供的时间片范围为: 5ms 至 800ms, 默认状态下为20ms, 根据动态优先级动态分配时间片的长短.
  且进程并不一定非要一次用完它所属的时间片, 如一个拥有100ms时间片的进程, 可以通过重复调度, 分5次每次20ms用完这些时间片. 这样的好处是: 能保证进程尽可能长的时间处于可运行状态.
  当一个进程时间片耗尽时, 它会被抢占, 也不会再投入运行, 除非等到其他所有进程都耗尽了它们的时间片, 此时所有进程的时间片会被重新计算.

22.Linux调度优化:
A.充分实现O(1)调度: 不管有多少进程, 调度程序采用的每个算法都能在恒定时间内完成.
B.实现SMP的可扩展性: 每个处理器拥有自己的锁和自己的可执行队列.
C.强化SMP的亲和力: 尽量将相关的一组任务分配给一个CPU进行连续执行, 只有在需要平衡任务队列大小时才在CPU之间移动进程(即负载平衡程序).
D.加强交互性能: 即使系统处于高负载情况下, 也能保证系统的响应, 并立即调度交互式进程.

22.可执行队列:
  每个处理器拥有自己唯一的可执行队列, 而每个可运行的进程都唯一地归属于一个可执行队列. 此外可执行队列中包含每个处理器的调度信息.
  处理器在对可执行队列进行操作前, 应该先锁住它. 故很少出现一个处理器需要锁其他处理器的可执行队列的情况(但确实可能出现)
  
  为避免死锁, 要锁住多个可执行队列必须总是按照同样的顺序获取这些锁, 同样嵌套的锁必须以相同的顺序操作.
  
  可执行队列的结构体如下:
  struct runqueue {
spinlock_t lock; //保护可执行队列的自旋锁
unsigned long nr_running; //可执行任务数目
unsigned long nr_switches; //上下文切换数目
unsigned long expired_timestamp; //队列最后被换出时间
unsigned long nr_uninterruptible; //处于不可中断睡眠状态的任务数目
unsigned long longtimestamp_last_tick;//最后一个调度程序的节拍
struct task_struct*curr; //当前执行任务
struct task_struct  *idle;//该处理器的空任务
struct mm_struct*prev_mm; //最后执行任务的结构体
struct prio_array*active; //活动数组
struct prio_array*expired; //过期数组
struct prio_array   arrays[2];//实际优先级数组
struct task_struct*migration_thread; //移出线程
struct list_head*migration_queue; //移出队列
atomic_t nr_iowait; //等待I/O操作的任务数目
  }
  
23.自旋锁: 用于防止多个任务同时对可执行队列进行操作.
它的作用就开门一样, 最开始, 有个任务到了大门前, 它拿起钥匙开了门, 走进去后回身把大门锁上. 如果这时第二个任务到了大门前, 它发现门
锁着(已经有一个进程在里面了), 就坐在门口等, 直到里面的任务走出来交出钥匙. 这个等待过程(即不断循环检测锁是否可用)叫自旋.

自旋锁的特点: 由于门外等待的线程不断循环, 所以特别浪费处理器时间, 故自旋锁不应该被长期持有.

若1号任务先锁住了甲, 再想锁住乙; 2号任务先锁住了乙, 再想锁住甲. 那么这两个任务就会一起永远等待下去, 即死锁.

23.读写自旋锁: 由于锁是针对数据而不是针对代码的, 故自旋锁又分为 读自旋锁, 写自旋锁, 普通自旋锁.
读自旋锁: 读某数据时, 不允许其它线程写这些数据, 即需要自旋, (但允许其他线程读这些数据, 此时不需要自旋, 因为这并不影响安全).----故读自旋锁可以被多个线程同时持有----故读锁也叫 共享锁
写自旋锁: 写某数据时, 即不允许其它线程写这些数据也不允许读这些数据(必须自旋).----故写自旋锁只能被一个线程持有----故写锁也叫 排斥锁

23.睡眠锁: 让门外等待的线程睡眠(而不是旋转), 等锁释放后再唤醒它.

24.重新计算时间片:
A.Linux的可执行队列中维护了两个数组, 即 活动数组 和 过期数组. 其中活动数组中的进程的时间片有剩余, 过期数组中的进程的时间片已用完. 
B.当一个进程的时间片耗尽(为0)时, 它会被移至过期数组中. 但在此之前, 时间片已经给它重新计算好了. 当活动数组中没有剩余进程时, 这两个数组就会交换,
 即活动数组变成过期数组, 过期数组变为活动数组
C.但如果一个进程的交互性非常强, 那么当它的时间片用完后, 会被再放置到活动数组中, 而不是过期数组中.

25.休眠和唤醒:
当一个进程由可执行状态变为休眠状态时, 会把自己从可执行队列移出, 放入等待队列.
当处于休眠状态的进程被唤醒时, 会把自己从等待队列中移到可执行队列.

26.负载平衡程序: 负责保证多处理器各自的可执行队列之间的负载处于均衡状态.
哪某个处理器的可执行队列中有20个进程, 而另一个处理器的可执行队列中只有5个进程, 这时就需要负载平衡程序来保证再平衡. 
即将相对繁忙的队列中的进程抽到悠闲的队列中来, 理想情况下, 每个队列中的进程数目应该相等.

负载平衡程序被调用的情况有三种:
A.某个可执行队列为空时, 会被调用.
B.被定时器调用: 系统空闲时每隔1ms调用一次
C.被定时器调用: 其他情况下每隔200ms调用一次

注: 在单处理器系统中, 负载平衡程序不会被调用, 它甚至不会被编译进内核, 因为那里只有一个可执行队列, 因此根本没有平衡负载的必要.
负载平衡程序被调用时, 需要锁住当前处理器的可执行队列并且屏蔽中断, 以免可执行队列被并发也访问.

针对A情况: 比较容易实现
针对B情况: a.首先, 找到进程数最多的可执行队列, 且必须比当前队列中的进程数多25%及以上 
b.其次, 如果存在a条件的可执行队列, 则选择其过期数组(因为该数组里的进程已经有相对较长的时间没有运行了), 如果过期数组为空, 则只能选择活动数组.
c.接着, 从b步选择的数组中, 找到含有进程且优先级最高的链表(因为把优先级高的进程分散开来才是最重要的).
d.然后, 分析c中找到的所有优先级相同的进程, 选择一个不是正在执行, 也不会因为处理器相关性而不可移动, 并且不在高速缓存中的进程, 将其从所在队列抽取到当前队列中.
e.接着, 只要可执行队列之间仍然不平衡, 就会重复c,d两个步骤, 继续从繁忙队列抽取进程到当前队列(每次只抽取一个).
f.最后, 当不平衡消除时, 解除对当前可执行队列的锁定.

四.系统调用: 是用户空间访问内核的唯一手段, 除异常和陷入外, 它是内核唯一的合法入口.
27.内核为了和用户空间上运行的进程进行交互, 内核提供了一组接口, 应用程序可以通过这些接口访问硬件设备和其他内核资源.

28.一般情况下, 应用程序通过API而不是直接通过系统调用来编程, 且API并不需要和内核提供的系统调用对应. 
  实际上, 不同的操作系统完全可以共用一套API, 只是API在各操作系统上的实现不同而已.
  
  如: 应用程序  C库 内核
 调用printf()----> C库中的printf()---->C库中的write()----> write()系统调用


  故: 对程序员来说, 无需关注内核, 只需与API打交道; 对内核来说, 无需关心C库与应用程序怎么使用系统调用, 只与系统调用打交道
  
29.系统调用号:
在Linux中, 每个系统调用被赋予一个独一无二的系统调用号, 该号一旦分配就不能再有任何变更. 
此外, 如果一个系统调用被删除(罕见), 它所属的系统调用号也不允许被回收利用.

30.系统调用处理程序(system_call()):
用户空间的程序无法直接调用内核代码, 只能以 软中断 的方式来通知系统, 让内核替自己执行某个系统调用.

31.软中断:
用户空间的程序通过引发一个异常来促使系统切换到内核态去执行异常处理程序, 而该异常处理程序实际上就是 系统调用处理程序.
在这之前用户程序就已经把 系统调用号 放入eax寄存器中, 当 系统调用处理程序 一旦运行, 就可以从eax寄存器获取 系统调用号, 从而去执行对应的系统调用.

当除 系统调用号 以外, 其他额外的参数也是通过寄存器来传递.

五.中断和中断处理程序
32.中断机制: Linux内核要对连接到该计算机上的所有硬件设备进行管理, 而内核与硬件的交互通过 硬件主动向内核发出请求来完成, 这就是中断机制.
中断本质上是一种特殊的电信号, 由硬件设备产生, 并直接送入中断控制器的输入引脚上, 硬件发出中断信号时并不考虑与处理器的时钟同步.
每个中断都通过一个唯一的数字来标识(IRQ), 故操作系统根据该数字就可知道哪个硬件产生的哪个中断, 并提供不同的中断处理程序.
如在PC上, IRQ0是时钟中断, IRQ1是键盘中断...
 
33.异常: 处理器执行程序出错时由处理器本身产生, 必须与处理器时钟同步, 故也称同步中断.

而内核对 由硬件产生的异步中断 和 由处理器本身产生的同步中断 的处理类似.

34.中断处理程序: 
一个中断对应一个中断处理程序, 若某个硬件可以产生多个中断, 那么该硬件就对应有多个中断处理程序. 
相应的, 该硬件的驱动程序就需要准备多个这样的函数.
一个硬件的中断处理程序是该硬件驱动程序的一部分, 硬件驱动程序是用于对硬件进行管理的内核代码.

中断处理程序与其它内核函数的本质区别在于: 中断处理程序是被内核调用来响应中断的, 它们运行于特殊的中断上下文中.

35.注册中断处理程序: 硬件的驱动程序负责向内核注册它包含的所有中断处理程序

36.释放中断处理程序: 卸载驱动程序时, 需要注销相应的中断处理程序, 并释放IRQ

37.中断上下文: 
中断上下文具有严格的时间限制, 因为它打断了其他代码的执行(甚至是另一个中断处理程序的执行). 故中断上下文中的代码应该迅速简洁, 且不可休眠.
针对该特点, 中断处理程序被划分为两部分: 上半部分 和 下半部分.

上半部分: 立即执行, 包括应答硬件, 复位硬件, 拷贝硬件数据包到内存-----也就是中断处理程序
下关部分: 延后执行, 包括对拷贝内存的数据进行分析, 处理, 返回结果等---至于何时执行, 关键在于当它们执行时, 已经允许响应所有的中断

38.中断控制: 所有中断都可以被禁止或屏蔽, 以确保某个中断处理程序不会抢占当前正在执行的代码, 当然禁止的中断也可以说被激活.
如自旋锁的使用必然会禁止所有的中断.
 
39.开发驱动程序的原则:
A.如果一个任务对时间非常敏感, 将其放在中断处理程序中(即上半部分)
B.如果一个任务和硬件相关, 将其放在中断处理程序中(即上半部分)
C.如果一个任务要保证不被其他中断打断, 将其放在中断处理程序中(即上半部分), 因为内核在调用中断处理程序时, 至少会锁住当前中断线, 甚至所有的本地中断线(当前处理器管理的所有中断).
D.其他所有任务, 放在下关部分中.

六.内核同步
40.临界区: 所有的共享资源都不允许被并发访问, 访问共享资源的代码段称为临界区.

41.原子操作: 为避免临界区中并发访问共享资源, 必须保证这些代码原子地执行, 即代码段在执行结束前不可被打断(中断, 抢占等任何情况),
相当于整个临界区是一个不可分割的指令.

42.竞争: 在某个时刻, 多个线程在同一个临界区

43.并发: 在某个时刻, 不同的临界区访问同一个共享资源

43.同步: 避免并发访问共享资源 和 竞争 称为同步(实质就是加锁).

44.锁机制: 防止并发访问共享资源, 执行线程进入房间后随手锁住大门, 即任一时刻, 房间里只能有一个执行线程存在, 
  另外的线程若要进入房间必须等里面的线程执行结束, 并出来交出锁后才能进入. 且锁是采用原子操作实现的.
  
  由于锁的作用是使程序以串行方式对共享资源进行访问, 故使用锁会降低性能
  
45.可能造成并发访问的原因:
A.中断 B.软中断C.内核抢占 D.睡眠及与用户空间的同步E.多处理器

46.难点: 其实, 真正使用锁来保护共享资源并不困难. 真正困难的是辨认出需要共享的资源和相应的临界区, 即在写某段代码时该不该加锁, 加哪种锁才是关键.

47.死锁的情况: 
A.n个线程和n个锁, 而每个线程都在等待其中的一把锁, 但所有的锁都已经被占用了, 故所有线程都在相互等待对方持有的锁, 永远不会释放锁, 于是所有线程都无法继续执行, 即发生死锁.
B.自死锁: 即如果一个执行线程试图去获取一个自己已经持有的锁, 它将不得不等待该锁被释放. 但由于它一直忙着等待该锁, 故自己永远也不会有机会释放该锁, 最终就是死锁.
 
48.死锁的预防:
A.使用嵌套的锁时必须保证以相同的顺序获取锁(如有ABC三个锁, 某次请求时以CBA的顺序加锁, 那么以后其他函数请求它们时也必须按CBA的顺序去请求)
B.不要重复请求同一个锁
C.越复杂的加锁方案越有可能造成死锁


49.锁的争用: 指当锁正在被占用时, 其他线程试图获得该锁. 被高度争用的锁会成为系统的瓶颈, 严重降低系统性能.

50.锁的粒度: 加锁粒度用来描述被加锁保护的数据规模, 一个过粗的锁保护一大块数据. 相反, 一个粒度细点的锁保护一小块数据.
锁的粒度需合理的设计, 当锁争用严重时, 加锁太粗会造成系统性能瓶颈. 当锁争用不明显时, 加锁过细会加大系统开锁, 带来浪费.
 
七.内核同步方法
51.锁的分类总结:
自旋锁: 让门外等待的线程不断循环直到锁被释放, 浪费处理器时间, 适用于持有锁时间短的情况.
读自旋锁: 多个读者可以同时持有该锁(即不用自旋), 但写者必须自旋
写自旋锁: 不管是读者还是写者都必须自旋
普通自旋锁:
A.中断上下文中只能使用自旋锁

睡眠锁(信号量): 
读信号量: 读者无数量限制, 写者必须少于定义
写信号量: 读者和写者都必须少于定义
普通信号量:
A.让门外等待的线程睡眠(而不是旋转), 锁被释放后再唤醒它(有更高的处理器利用率, 但信号量比自旋锁有更大的开销, 因为要睡眠, 维护等待队列和唤醒), 适用于持有锁时间长的情况.
 即当某个进程试图获取被占用的信号量时, 系统会将该进程放入等待队列使其睡眠, 此时处理器可以去执行其他代码, 当该信号量被释放时, 再唤醒该进程获取该信号量.
B.只能在进程上下文中获取信号量(即进程能在可执行队列和等待队列间调度), 因为中断上下文是不能被调度的.
C.门外等待信号量的线程可以选择睡眠, 也可以不去睡眠.
D.信号量可以允许任意多个进程同时持有, 在声明信号量时由count变量指定同时持有的进程数.
获取信号量: down()操作, 即将count减1, 若结果大于等于0, 则获取该信号量, 否则(以可中断状态)放入等待队列.
释放信号量: up()操作, 即将count加1, 此时若该信号量上的等待队列非空, 则队列中的进程被唤醒的同时获取该信号量.

注: 信号量和自旋锁不能同时持有. 因为持有信号量时, 请求线程有可能睡眠. 而持有自旋锁是不允许请求线程睡眠的(必须旋转).

BKL(大内核锁): 全局全旋锁, 用于Linux从2.0更好的向2.2过滤, 现已不建议使用

SEQ锁: 即序列锁, Linux2.6引入
  只有在写时, 序列计数器才会加1(初值从0开始), 释放时变偶数. 故写时该值为奇数, 读时该值为偶数.
  读时, 读之前和读之后都会读取序列值, 若二者相同, 说明读操作过程中没有被写操作打断过.
  

52.信息号的定义: 
static DECLARE_SEMAPHORE_GENERIC (name, count)  name是信号名, count是使用者数量
static DECLARE_MUTEX(name) 互斥信号量, 即任意时刻只允许一个使用者

53.自旋锁和信号量的使用原则:
A.低开销加锁: 优先自旋锁
B.短期加锁: 优先自旋锁
C.中断上下文中加锁: 只能自旋锁
D.长期加锁: 优先信号量
E.请求者需要睡眠: 只能信号量

54.屏障: 确保操作的顺序性
读屏障rmb(): 即读时, rmb()之前的操作不会被重排到rmb()之后, 同样, rmb()之后的操作不会被重排到rmb()之前
写屏障wmb(): 即写时, wmb()之前的操作不会被重排到wmb()之后, 同样, wmb()之后的操作不会被重排到wmb()之前
屏障mb(): 不管读还是写, mb()之前的操作不会被重排到mb()之后, 同样, mb()之后的操作不会被重排到mb()之前

八.时间管理
55.系统定时器: 用于管理周期性任务(如屏幕刷新, 可执行队列间的再平移, 管理时间片等), 系统日期 和 系统时间
  本质原理: 提供一种周期性触发的中断机制. 有的系统是通过对电子晶振分频来实现, 有的是通过衰减测量器来实现(当从初值衰减到0时触发一个中断).

56.动态定时器: 用于管理延后任务(如中断下半部, 闹钟等), 执行完指定任务后自行销毁.

57.时间中断处理程序: 有的需要每次时间中断都执行, 有的需要每n次时间中断执行一次. 分两大类: 与系统有关, 与系统无关
与系统有关: 作为系统定时器的中断处理程序注册到内核中.

与系统无关:

58.时间中断频率: 现在Linux的时间中断频率是1000HZ, 即1秒钟发出1000个时间中断, 也即每ms发出一个时间中断

59.墙上时间: 即实际世界时间

60.系统运行时间: 自系统启动开始计时, 已运行的时间


61.实时时钟RTC: 用于持久存放系统时间的设备, 即使系统关机后, 它仍可以靠主板上的微型电池计时. 当系统启动时, 内核通过读取RTC来初始化墙上时间.


九.内核空间的内存管理
62.页: 内核把物理页作为内存管理的基本单位, 尽管处理器的最小可寻址单位通常是字(甚至字节).
  架构不同, 页的大小也不同. 大多数32位机的页为4KB, 64位机的页为8KB. 如1GB内存在32位机上被划分为1024*1024/4 = 262144页
  
  内核用struct_page结构来描述每个页, 该结构定义在<linux/mm.h>中:
  struct_page {
page_flags_t flags; //表示页的状态, 每一位表示一种状态, 故至少可以同时表示出32种状态(如是否脏等)
  atomic_t _count; //表示被引用了多少次, 当为0时, 表示新分配时可以使用它
  atomic_t _mapcount;
  unsigned long private;
  struct_address_space*mapping;
  pgoff_t index;
  struct list_headlru;
  void *virtual; //该页的虚拟地址
  }
  
63.区: 内核对具有相似特性的页进行(逻辑上)分组, 划分为不同的区. 就可以根据用途来分配页

A.ZONE_DMA: 这个区包含的页能用来执行DMA(直接访问内存)操作.
B.ZONE_NORMAL:这个区包含的都是能正常映射的页.
C.ZONE_HIGHMEM: 由于有的架构的物理寻址比虚拟寻址大得多, 故部分页不能永久地映射到内核地址空间上, 这些页就属于本区.

如要x86上, A为<16MB的页, B为16MB~896MB的页, C为>896MB的页

64.内核栈: Linux内核给每个进程分配一个固定大小的内核栈.

  以前: 每个进程有两页的内核栈, 即32位和64位的进程内核栈分别为8KB和16KB.
  现在: 每个进程有单页的内核栈, 即32位和64位的进程内核栈分别为4KB和8KB. (减少每个进程的内存消耗, 寻找两个未分配且连续的页越来越困难)
 
65.中断栈: 内核为每个进程提供专用于载入中断处理程序的栈, 大小为一页.

十.虚拟文件系统VFS----包括open(), read(), write()等系统调用
66.元数据: 即文件的相关信息, 如访问控制权限, 大小, 拥有者, 创建时间等.

67.索引节点: 存储元数据的数据结构.

68.VFS采用的是面向对象的设计思路, 包含四个主要的对象类型:
A.超级块对象: 代表一个已安装文件系统
B.索引节点对象: 代表一个文件
C.目录项对象: 代表一个目录项, 即文件夹
D.文件对象: 代表由进程打开的文件

十一.块I/O层: 管理块设备和管理对块的请求.
69.块设备: 能够随机(不需要按顺序)访问固定大小数据片的设备(如磁盘, 硬盘).

70.块: 69中的那些数据片就称为块, 是扇区的整数倍, 且是2的整数倍, 且必须小于1页的大小. 故常见为512B, 1KB, 4KB. 是对扇区的逻辑抽象, 是VFS系统的最小寻址单元.

71.字符设备: 按顺序访问数据的设备(如键盘).

72.扇区: 块设备中的物理最小寻址单元, 大小一般是2的整数倍(最常见的是512B), 块设备无法对比扇区还小的单元进行寻址和操作.


十二.进程地址空间
78.平坦地址空间: 即地址范围是连续(如从0扩展到429496729的32位地址空间), 每个进程都有唯一的32位或64位平坦地址空间.

79.进程地址空间之间彼此独立, 互不相干. 但也可以选择共享地址空间(此时, 这些进程就是通常说的线程, 对内核来说线程仅仅是一个共享特定资源的进程).

80.内存区域: 进程可以访问的合法地址区间, 若进程访问了不在合法范围内的地址, 则内核会终止该进程.

81.内存描述符: 描述与进程地址空间有关的全部信息.

82.页表: 虚拟内存地址 通过多级页表转换为 物理内存地址. Linux使用三级页表.

 




























































































原创粉丝点击