读书笔记之《linux内核设计与实现》...
时间:2010-08-13 来源:potty15
Robert love著
第一章 Linux内核简介
1. 处理器活动范围为:
运行于内核空间,处于进程上下文
运行于内核空间,处于中断上下文
运行于用户空间,执行用户进程。
2. 单内核与微内核
Windows NT和Mac OS X的Mach都是微内核,Linux是单内核,但是吸取了微内核的精华:模块化设计,抢占式内核,支持内核线程及动态装载模块。
第二章 从内核出发
1. 编译内核时会在内核代码树的根目录下创建一个System.map文件,是一份符号对照表,用以将内核符号和他们的起始地址对应起来。
2. 内联函数需要在使用前定义好,例如static inline void foo()。
3. Gcc中可以使用likely()和unlikely()分别标记出发生频率较高和较低的分支,以优化程序执行效率。
4. 不要在内核使用浮点数。
第三章 进程管理
1. 进程是Unix操作系统最基本的抽象之一,另一个是文件。
2. 每个线程有一个独立的程序计数器,进程栈和一组进程寄存器,内核调度对象是线程,不是进程。
3. 进程存放在叫做任务队列的双向循环链表中,链表每一项都是task_struct类型,称为进程描述符。
4. 在进程上下文中,current宏获得当前进程是有效的。
5. 写时拷贝(COW)是一种推迟甚至免除在fork调用时所需要的数据拷贝操作。内核不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入时,所涉及到的页才被复制。因此在fork之后子进程如果马上调用exec,系统开销就只是复制父进程的页表以及给子进程创建唯一的进程描述符。
6. Clone是一个实现fork的系统调用,通过一系列参数来指明父,子进程之间需要共享的资源。
7. 在创建子进程后,内核有意首先执行子进程。
8. Vfork与fork相同,唯一不同在于vfork只拷贝父进程页表,不拷贝页表项,他们共享数据,父进程被阻塞直到子进程退出或执行execl。
9. Linux中线程同样被当作进程,只是他们之间共享资源。
10. 内核线程只在内核空间运行,但是与普通进程一样可以被调度,被抢占。
第四章 进程调度
1. 进程调度来决定什么时候停止一个进程的运行以便其他进程能够得到执行的机会,这个强制的停止,挂起动作叫做抢占。
2. 进程可以被分为I/O消耗型和处理器消耗型。I/O消耗型应该多被调度,而处理器消耗型应该降低其运行频率,延长运行时间。
3. 基于进程优先级的调度,Linux实现了基于动态优先级的调度方法:一开始进程设置基本的优先级,然后根据执行情况来加,减优先级。常常处于等待I/O的I/O消耗型增加,常常迅速用完处理器时间片的处理器消耗型减少优先级。
4. Linux使用-20~+19的nice值,以及0~99的实时优先级来决定一个进程的优先级。
5. 当一个进程时间片耗尽时,就不会再运行,除非其他所有进程都耗尽了时间片,那是,所有进程的时间片被重新计算。
6. 一个进程进入TASK_RUNNING状态时,内核会检查其优先级是否高于当前进程优先级,如果高于,调度进程被唤醒,抢占发生;或者当进程时间片为0时,同样会唤醒调度进程,将其抢占。
7. 调度程序最基本的是运行队列,每个处理器一个,表示该处理器上可执行进程的链表。
8. Cpu_rq(processor)宏用于返回指定CPU上的运行队列,this_rq()返回当前CPU的。
9. 在对可执行队列操作前需要用task_rq_lock等函数对其加锁。
10. 每个运行队列都有2个优先级数组,一个活跃的,一个过期的。每个数组都有一个优先级位图,可以提高查找当前系统内最高优先级的可执行进程的效率。每个数组还都有一个list_head队列,包含有多个链表,每个链表与一个给定的优先级对应,即一个链表表示一个优先级上的所有可执行进程,这样能快速找到某个优先级上的进程。
11. 每个运行队列的2个优先级数组:活动和过期数组。活动数组内的可执行队列上的进程都还有时间片剩余,没有时间片的都被移至过期数组,在移动前已经对进程算好了下一轮执行的时间片。最后当活动数组可执行队列没进程时,只需要与过期数组切换就可以了。
12. 调度程序由schedule()完成,独立于每个处理器运行,每个CPU都要使用它来判断下一个要执行的进程。调度基本过程为:在活动数组中找到第一个被设置的位,选择这个级别链表的第一个进程,这时比较调度前后2个进程是否相同,如果不同,调用context_switch()切换上下文。
13. 时间片计算方式:静态优先级nice值和动态优先级同时作用。Nice值越小,优先级越高,交互性越强。动态优先级通过一个关于静态优先级和进程交互性的函数关系计算而来。一般是nice值为基础,根据交互性有-5至+5的一个偏移。
14. 判断交互性的方式:记录一个进程用于休眠和用于执行的时间,保存在task_struct的sleep_avg域,休眠时数值增加,执行时数值减少,由此判断进程是交互性高的还是处理器消耗型。
15. 进程创建时,父,子进程平分父进程剩余的时间片。防止不断建立进程来获取时间片。
16. 如果一个进程交互性非常强,时间片用完后仍被放回活动数组。
17. 休眠分为可中断和不可中断2种。区别在于可中断休眠如果接收到一个信号会被提前唤醒并相应该信号。
18. 内核中休眠—唤醒的一般过程:
DECLARE_WAITQUEUE (wait, current);
Add_wait_queue (q, &wait);//添加入等待队列
While (! condition) {//如果条件没发生
Set_current_state (TASK_INTERRUPTABLE);//设置为可中断或不中断休眠
If(signal_panding(current)) {//如果可中断,就需要判断并处理信号
/* 处理信号 */
}
Schedule ();//继续休眠
}
Set_current_state (TASK_RUNNING);//事件发生,设置进程运行
Remove_wait_queue (q, &wait);//移出等待队列
19. 负载平衡load_balance(),在对称多处理器中有2种调用方法:在schedule()执行时,如果可执行队列为空,调用。或者被定时器调用,系统空闲时间隔1毫秒一次,其他情况下200毫秒一次。
20. 上下文切换context_swith()完成的工作:调用switch_mm(),把虚拟内存从上一个进程映射切换到新进程中;调用switch_to(),从上一个处理器状态切换到新的,包括保存,恢复堆栈信息和寄存器信息。
21. Need_resched标志标明是否需要重新执行一次调度,每个进程都包含这个标志。某个进程耗尽时间片或者优先级高的进程进入可执行状态时,都这个标志被设置。
22. 用户抢占:在完成系统调用返回用户空间或者从中断返回用户空间时,内核会检查need_resched标志,如果被设置,则发生用户抢占。
23. 只要重新调度是安全的(没有持有锁),内核就可以在任何时间抢占任何任务。
24. 内核抢占发生在:
*当中断处理程序正在执行,返回内核空间之前;
*内核代码再一次具有可抢占性时;
*内核中的任务显示调用schedule()
*内核任务阻塞。
25. Linux提供两种实时调度策略:SHED_FIFO和SHED_RR(带有时间片的FIFO)。普通非实时调度策略为SHED_NORMAL。调度优先度SHED_NORMAL < SHED_FIFO < 较高优先级的SHED_FIFO < SHED_RR。
26. Shed_yield()系统调用能够主动让出进程的处理器时间,把自己放入过期数组。但实时进程不会过期,因此只是被移到优先级队列最后面。
第五章 系统调用
1. 系统调用特点:所有系统调用函数声明都需要加asmlinkage限定词,通知编译器仅从栈中提取该函数的参数;系统调用内部命名规则sys_xxx();
2. 每个syscalls都有系统调用号,一旦分配不能更变,一旦删除也不能被从新利用,而是用sys_ni_syscall()填充。
3. 内核记录了系统调用表中的所有已注册过的系统调用的列表,储存在sys_call_table中。
4. 应用程序通过软中断来通知内核,具体实现是引发一个异常使系统切换到内核态去执行异常处理程序。X86系统用int $0x80产生软中断。
5. X86系统中,系统调用号通过eax寄存器传递给内核。每个系统调用在系统调用表中占4字节,因此将在系统调用表中偏移eax *4就是系统调用函数的入口地址。
6. X86系统中,ebx,ecx,edx,esi和edi按顺序存放系统调用前5个参数。给用户空间的返回值地址保存在eax寄存器中。
7. 系统调用的参数验证:
*指针指向的内存区域属于用户空间。
*指针指向的内存区域中进程的地址空间。
*如果是读,内存应标记为可读,如果是写,应标记为可写。
8. 注册系统调用:
*在系统调用表最后加入一项。
*在<asm/unistd.h>中定义系统调用号。
*系统调用编译入内核镜像。
9. 对系统调用的直接访问,通过宏_syscall1()~_syscall6(),1~6表示要传递的参数个数。
例如对于long open(const char *filename, int flags, int mode);
直接进行系统调用就可以写成_syscall3(long, open, const char *, filename, int, flags, int, mode).