你的位置:首页 > 操作系统

[操作系统]Linux内核设计与实现 读书笔记

第三章 进程管理


1. fork系统调用从内核返回两次: 一次返回到子进程,一次返回到父进程
2. task_struct结构是用slab分配器分配的,2.6以前的是放在内核栈的栈底的;所有进程的task_struct连在一起组成了一个双向链表
3. 2.6内核的内核栈底放的是thread_info结构,其中有指向task_struct的指针;
4. current宏可以找到当前进程的task_struct;X86是通过先找到thread_info结构,而PPC是有专门的寄存器存当前task_struct(R2)
5. 内核栈大小一般是8KB
6. 进程的五种状态:TASK_RUNNING,  TASK_INTERRUPTABLE, TASK_UNINTERRUPTABLE(处于改状态的进程不能被kill,因为它可能正在等待很关键的数据,或者持有了信号量等), TASK_TRACED(被其他进程跟踪状态,具体状态表现不明),  TASK_STOPPED(收到SIG_STOP信号,停止进程,相当于暂停进程,但是也可以恢复)
7. 运行上下文分为“进程上下文”和“中断上下文”。系统调用时内核代表进程在进程上下文中执行代码,这时current宏是有效的,指向进程的task_struct,而且系统调用时内核使用的页表是用户态进程的页表;而在中断上下文内核不代表任何进程执行代码,而是执行一个中断处理程序,不会有进程去干预这些中断上下文,所以此时不存在进程上下文。
8. 系统调用在陷入内核的瞬间应该是在中断上下文的,因为是软中断,只是陷入内核后又用了进程上下文
9. 在每个进程的task_struct结构中,有一个parent指针指向其父进程,有一个链表表示其所有的子进程,用这样的结构构成了整个系统进程关系树。
10. 内核的双向列表专用结构struct list_head
11. 进程创建分为两个步骤:fork和exec,fork用来创建进程的结构,通过写时复制,父子进程共享进程空间(页表),父进程和子进程的区别仅仅是PID,PPID,某些资源,统计量(task_struct的结构);  exec读出程序代码并执行之。
通过写时复制,只有在需要写入进程地址空间时,才为子进程创建自己的进程地址空间。
*** fork的开销其实就是复制父进程的页表 和 为子进程分配task_struct 结构
fork的过程: fork() -> clone() -> do_fork() -> copy_process()
copy_process()的过程:
a) 为子进程分配内核栈,创建thread_info结构,创建一份与父进程相同的task_struct结构
b) 更改thread_info ,task_struct结构中的部分字段,将子进程与父进程区分开来
c) 将子进程的状态设置为TASK_UNINTERRUPTIBLE
d) 为子进程分配一个可用的PID(alloc_pid())
e) 拷贝或共享父进程打开的文件,信号处理函数,进程地址空间等
f) 设置子进程状态为RUNNING
g) 返回一个指向子进程的指针
一般系统会优先唤醒子进程,因为如果优先唤醒父进程,父进程就有可能会写入,这样会触发写时复制,而子进程一般会调用exec
 
12. vfork 
vfork保证父进程在创建子进程后被阻塞,除非子进程执行了exec,或者子进程退出
vfork在fork有写时复制功能后的好处只有一个,就是:vfork不用拷贝子进程的页表
 
13. 线程
线程在linux内核中的实现就是一个进程,只是线程会与别的线程共享进程地址空间,共享信号等等
创建一个有四个线程的进程,会有四个进程被创建(四个内核栈和四个task_struct),只要指明这些task_struct中共享同一个进程地址空间即可
线程的创建和进程的创建的代码上的对比:
线程创建:  clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
进程创建:  clone(SIG_CHILD, 0)
从线程创建的代码也可以看出:线程创建时是共享父进程的  进程地址空间(CLONE_VM), 打开的文件(CLONE_FILES),文件系统信息(CLONE_FS), 信号处理函数(CLONE_SIGHAND)
 
14. 内核线程
内核进程需要在后台执行一些操作,所以需要创建一些内核线程(kernel thread)
内核线程和普通线程的主要区别是: 内核线程没有独立的地址空间(task_struct中的mm指针是NULL),共享使用内核态的页表; 内核线程只在内核运行,从来不会调度到用户态;
内核线程和普通线程的相同点有: 同样有状态,被调度,也可以被抢占
有哪些内核线程:flush,ksoftirqd等, 用ps -ef可以查看,其中CMD栏是[]扩起来的都是内核线程
创建方法:  kthread_create(); wake_up_process()用来唤醒创建的线程;  kthread_run()可以创建并使之运行; kthread_stop()停止内核线程
 
15. 孤儿进程
如果父进程比子进程先执行完,父进程要在线程组中找一个新的进程作为子进程的父进程,或者找init进程作为子进程的父进程
 
16. 进程消亡
进程消亡时是通过exit()来实现的,进行exit之后内核依然会保留其task_struct结构,直到其父进程调用wait()或waitpid()回收
 
 


 
第四章  进程调度
 
1. linux是抢占式多任务系统
2. 通过调度程序选择一个进程来执行, 调度程序来决定什么时候挂起一个进程的运行,以便让其他进程得到允许机会,这种挂起操作叫做抢占。
3. 进程在被抢占之前能获得运行的时间叫做进程的时间片。进程的时间片是固定的,预先设置好的。
4. yield(), 进程可以通过该函数让渡被调度权
5. 调度算法
a) O(1)的调度程序
O(1)调度程序对大服务器的工作负载下应用很理想,但是在交互式场景下不理想
b) CFS完全公平调度算法
改进了linux对交互式场景的不足
 
6. IO消耗性进程和CPU消耗性进程
linux调度程序通常更倾向于优先调度IO消耗性进程,但是也并未忽略CPU消耗性进程
 
7. 进程优先级
Linux采用了两种表示进程优先级的方法:
a) nice值,nice值本来是Unix的标准做法。在linux中,nice值代表的是时间片的比例,nice值越大优先级越低,范围是-20到19
b) 实时优先级,范围是0到99,越大优先级越高
 
8. 时间片
时间片过长,导致对IO消耗性进程的支持不好;时间片过短,进程调度就花去了更多的时间
 
9. CFS调度算法实际上分给每个进程的是处理器占用比,这个占用比也会受到nice值得影响
例子:假设系统只有两个进程,一个文本编辑程序(IO消耗性),一个视频编解码程序(CPU消耗性),系统初始时他们有同样的nice值,所以在启动后给它们分配的处理时间都是一样的,都是50%,因为文本编辑器消耗很少的CPU,所以它的CPU时间占比远小于本应该分配给他的50%,而视频程序就占用了超过50%的CPU时间,所以当文本编辑程序需要运行时,调度程序发现它的CPU时间比它应得的少很多,所以马上让他抢占运行;当文本编辑器运行完毕后,又进入等待,所以它消耗的CPU时间依然少,这样系统就能不断的马上响应文本编辑程序。
CFS调度算法的主要思想是保证系统的公平使用,通过了这种方法可以自动的发现各个进程的CPU使用情况,根据这个使用情况动态的调整进程的调度和分配。
CFS为每个进程被抢占前能运行的时间片的最小值是1ms。
 
问题来了,一个运行了很久的IO消耗性进程和一个刚开始运行的CPU消耗性进程相比,可能会让IO消耗性进程被调度的可能性变慢,所以是不是说如果进程执行的时间过长了,要重启一下?
 
10. Linux有多种调度器算法
不同的进程被归入不同的调度器类中
schedule()从最高优先级的调度器中选择一个最高优先级的进程来调度
完全公平调度CFS是一种针对普通进程的调度器,linux中称为SCHED_NORMAL
还有实时进程调度器
 
11. Linux是何时运行调度器的?
a)
b) linux是通过need_resched这个标识来表明是否要进行执行一次调度的,哪些地方会设置这个标志:schedule_tick(),  try_to_wake_up()等;  need_resched标志保存在进程的thread_info里,这是因为访问current比访问全局变量更快
c) 在返回用户空间或者中断返回的时候,内核也会检查need_resched标志,如果被设置,系统会在继续运行之前调用调度程序
d) 抢占发生的时间:
d.1) 用户抢占
d.1.1) 从系统调用返回用户空间时
d.1.2) 从中断服务程序返回用户空间时
 
d.2) 内核抢占
d.2.1) 从中断服务程序返回内核空间时
d.2.2) 内核代码再一次具有可抢占性的时候:这里包含以下的含义: 只有进程没有持有锁就可以被抢占,如果持有了锁,系统是不可抢占的,在释放锁的时候且preempt_count减少到0的时候,说明当前可以被安全的抢占了,这时检查need_resched标志进行抢占。
d.2.3) 内核显式调用schedule()
d.2.4) 内核任务阻塞
 
调度器入口:schedule()函数,作用是从最高优先级的调度器中选择一个最高优先级的进程来调度
 
12,睡眠和唤醒
当进程要等待时将自己的进程状态改成INTERRUPTIABLE或者UNINTERRUPTIABLE状态,并把自己从调度红黑树中移出到等待队列中,再调用schedule()调度下一个进程来运行
睡眠时将进程挂到相应的等待队列上:
DEFINE_WAIT(wait);
add_wait_queue(q, &wait);
while(!condition)
{
    prepare_to_wait(&q, &wait, TASK_INTERRUPTIABLE);    if(signal_pending(current))
        ...    schedule();
}
finish_wait(&q, &wait);  //把自己移出等待队列
 
唤醒
wake_up()函数唤醒挂在等待队列上的所有进程,把这些进程的状态改成TASK_RUNNING,并把它加入到调度红黑树上,如果被唤醒的进程优先级比当前的优先级高,还要设置need_reschedule标志。
唤醒要注意的是:可能存在虚假唤醒,可能是收到了信号唤醒了进程。所以在等待时要用一个while循环检查是否满足了条件,如果不满足可能是虚假唤醒,必须继续wait。
 
13. 抢占和上下文切换
上下文切换就是从一个进程切换到另一个进程去,用context_switch()函数完成,该函数在schedule()中被调用,
该函数主要完成两个工作:
switch_mm():把进程的虚拟地址空间切换
switch_to():切换进程的处理器状态,保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息
 
14. 实时调度器
两种实时调度策略:SCHED_FIFO和SCHED_RR
SCHED_FIFO:先进先出,一直执行,直到自己释放CPU或者等待,无时间片概念
SCHED_RR: 与SCHED_FIFO类似,但是有时间片概念,在耗尽预先分配给它的时间片之后就重新调度
如何设置进程是实时进程?
 


 
第五章  系统调用
 
1. 系统调用是什么?为什么要引入系统调用?
系统调用是用户进程和硬件设备之间的一个中间层
引入系统调用有三个原因:
a) 给用户提供一个统一的,抽象的接口和硬件设备打交道
b) 通过系统调用这个中间层,防止用户异常操作硬件设备
c) 虚拟化的思想,用户进程都是作为一个个单独的实体运行在虚拟空间中,在系统和用户进程之间提供这样的一层接口也是出于这个考虑,类似一个硬件上安装了多个虚拟机一样
 
2. API, POSIX, C库
POSIX是一套通用的API接口标准
C库实现了POSIX规定的绝大部分API
用户态调用流程:应用程序 -> C库 -> 系统调用
Linux系统调用也是作为C库的一部分提供
 
3. Unix接口设计的名言
“提供机制而不是策略”—— 含义是:系统调用抽象了用于完成某种确定的目的的函数,至于这些函数怎么用完全不需要内核关心,是应用程序和C库来关心的。
其实设计任何API都有这样的需求:只提供完成特定任务的接口,具体如何使用这个API是由使用者来关心的
 
区别机制和策略会简化开发,机制是“需要提供什么功能”,策略是“怎样实现这些功能”。这样可以利用相同的API来适应不同的需求。
 
4. syscall table
sys_call_table中保存了所有系统调用号的处理函数
 
5. 中断陷入
a) 通过软中断
中断号是128
int $0x80
 
b) sysenter指令
x86提供的新的进入系统调用的方法,更快更专业
 
6. 系统调用的返回值和errno
每个系统调用都会有返回值,返回值一般是long类型,为0表示成功,负数表示失败;返回值除了表示成功失败以外,根据系统调用的具体实现,可以返回功能结果,如getpid()系统调用就返回pid
errno全局变量内保存的是错误号,可以通过perror()来获得错误描述。
errno作为全局变量如何在多核上使用呢?
 
7. 系统调用参数和返回值的传递
系统调用时需要传递系统调用号和参数。系统调用号总是用eax传递,当参数个数小于5个时,用寄存器传递(ebx, ecx, edx, edi, esi), 当超过5个时,应该用一个单独的寄存器存放指向所有参数的用户空间地址的指针
返回值是通过eax传递的
 
8. 用户空间和内核空间的数据拷贝
copy_to_user(dst, src, size);
copy_from_user(dst, src, size);
其实直接拷贝也是可以的。这两个函数主要是加了一些使用检查,对用户提供的指针进行检查,不让用户空间通过系统调用来操作内核空间的地址
注意copy_to_user和copy_from_user都可能引起阻塞,当数据被交换到硬盘上时就会发生这种情况,此时,进程就会休眠,直到被唤醒后继续执行或者调用调度程序
 
9. 系统调用要做很多检查工作,因为输入来自用户态,不能让用户态的错误操作导致内核态数据的错误
capable()函数可以做一些权限检查
 
10. 系统调用是 可睡眠的 和 可抢占的
可睡眠的保证了系统调用可以使用大部分的内核接口
 
11. 函数可重入性
系统调用要保证实现时可重入的,因为系统调用时允许被抢占的,所以当新的进程也调用该系统调用时保证可重入才不会出错。
 
12. 不靠C库的支持,直接使用系统调用的方法
例如:使用open系统调用
#define NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)
 
_syscall3是一个宏,它会设置好寄存器,并调用陷入指令。
通过这个宏就创建了一个open()函数,返回值是long,有三个参数,这时就可以直接使用  long fd = open(filename, flags, mode);   调用系统调用了
 
13. 最好不要新加系统调用,而是使用一些替代方案
替代方案:
a) 对于设备节点,可以使用ioctl自定义命令进行操作
b) 对于信号量这种,其实也是文件描述符,所以也可以用ioctl
c) 利用/proc或者/sysfs文件系统来和内核交互
 


第六章  关键内核数据结构
 
1. 链表,队列,映射,二叉树
 
2. 链表
经典的list_head循环双向链表
struct list_head
{
    struct list_head *next;
    struct list_head *prev;
};
 
container_of宏,list_entry宏,可以通过这个宏方便的找到list_head所在的结构的首地址
offset_of(type, member):获得member在type结构中的offset偏移,container_of中用到了这个宏
 
#define offsetof(struct_t,member) ((size_t)(char *)&((struct_t *)0)->member)
 
#define container_of(ptr, type, member) ({                      \ 
const typeof( ((type *)0)->member ) *__mptr = (ptr);    \ 
(type *)( (char *)__mptr - offsetof(type,member) );})
 
#define list_entry(ptr, type, member) /
    container_of(ptr, type, member)
 
INIT_LIST_HEAD(struct list_head);  // 初始化list_head
 
链表头:LIST_HEAD()
 
操作方法:
list_add(new, head);
list_add_tail(new, head);
list_del(ptr);
list_del_init(ptr);  //删除并初始化该list_head
list_move(list, head);  // 从一个链表中删除list,并加到head链表后面
list_move_tail(list, head);  // 从一个链表中删除
list_empty(head);  //判断是否为空
 
遍历链表:
struct fox
{
    int i;
    struct list_head *list;
};
struct list_head *head = ...;
struct fox *f;
struct list_head *p;
list_for_each(p, head) {   // 循环遍历链表
    f = list_entry(p, struct fox, list);
}
 
另一个宏:list_for_each_entry(f, head, list); 可以实现和上面 list_for_each{ }  块一样的功能
该宏的声明如下:
list_for_each_entry(pos, head, member);
还有一个反向遍历的:
list_for_each_entry_reverse(pos, head, member);
还有如果遍历的同时要删除的:
list_for_each_entry_safe(pos, next, head, member);  // 多了一个next的struct list_head指针,用来记录next
list_for_each_entry_safe_reverse(pos, next, head, member);
 
如果要并发操作链表,必须使用锁。
 
3. 队列
FIFO。生产者消费者模型
kfifo是内核的通用实现。
 
创建队列:
struct kfifo fifo;
int ret;
int size = PAGE_SIZE;
ret = kfifo_alloc(&fifo, size, GFP_KERNEL);  //size必须是2的幂
 
char buffer[PAGE_SIZE];
kfifo_init(&fifo, &buffer, size);
 
入队列:
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);   // 拷贝from指向的len大小的数据到fifo中
出队列:
unsigned int kfifo_out(struct kfifo *fifo, void * to, unsigned int len); //拷贝出长度为len大小的数据到to中
 
其他操作:见书本
 
4. 映射
与std::map类似,有以下操作:Add, Remove, Find
linux实现了一个专用的类似map的结构:是一个唯一的UID到一个指针的映射
 
5. 二叉树
rbtree (看一下红黑树的原理)
查找操作多的情况可以使用,如果查找的少,不如用链表
 


第七章  中断和中断处理
 
1. 中断上下文又称原始上下文,该上下文中不可阻塞
2. 内核在收到中断之后要设置设备的寄存器 关闭中断,设备的配置空间一般有中断位,采用level中断方式,必须把这个中断位设置
3. 上半部与下半部
例如,网卡,上半部在收到中断进入中断上下文后要设置硬件寄存器,同时把数据快速的拷贝到内核空间
4. request_irq注册中断
IRQF_SHARED标志:共享中断线
IRQF_DISABLED标志:处理中断时关闭其他中断
 
free_irq()释放中断
 
5. 中断上下文
与进程无关,不能睡眠
中断上下文的栈:有两种:每个cpu单独的中断栈;或者使用被中断进程的内核栈
一般进程的内核栈是两页,在32位机器上就是8KB,在64位的机器上就是16KB
 
6. /proc/interrupts
 
7. 中断控制
禁止中断 : local_irq_disable()   local_irq_enable();
这两个函数有缺陷:不能嵌套调用,所以有下面两个函数:
local_irq_save(flags)  local_irq_restore(flags)   它们会保存中断状态(就是之前是被禁止还是被启用的状态)
这些函数是当作临界区锁来用的,当软中断上下文和中断上下文有共享数据时,就要用这些函数来充当锁
 
local_irq_disable()禁止了所有的中断
禁止指定irq上的中断:disable_irq(irq)  disable_irq_nosync(irq)  enable_irq(irq)  synchronized_irq(irq)
这些函数可以嵌套,所以调用多少次disable就要调用多少次enable
共享中断线的irq不能被禁止,所以这些API主要在老式的设备上采用,PCIe设备强制要求共享中断线
 
8. 判断当前是否在中断上下文
in_interrupt() : 内核正在执行中断处理程序或者下半部时返回非0
in_irq()  : 内核正在执行中断处理程序时返回非0

 
9. 中断处理程序只能在一个CPU上运行
 

第八章  下半部
 
1. 下半部是什么
下半部是比中断处理程序稍缓的任务,可以在中断处理程序处理完最紧急的任务之后处理的任务
 
2. 下半部可以有多种实现方法
上半部只能用中断处理程序实现,而下半部可以用下列方法实现:软中断,tasklet ,工作队列
 
3. 软中断
系统最多能注册32个软中断,目前内核总共用的软中断有9个
一个软中断不能抢占另一个软中断。唯一可以抢占软中断的是中断处理程序。不过,其他的软中断,即使是相同类型的软中断,也可以在别的处理器同时运行。
 
通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行。这个步骤叫触发软中断。
 
那么,何时会执行待处理的软中断呢?
a)  在中断返回时
b) 在ksoftirqd内核线程中
c) 在显示的检查和执行待处理的软中断的代码中,如网络子系统中
 
do_softirq()是唤醒软中断的函数,其简化代码如下:

 
目前只有两个子系统直接使用软中断:网络子系统和SCSI子系统; tasklet也是用软中断实现的

注册软中断处理程序:
open_softirq(softirq_no, softirq_handler);
raise_softirq(softirq_no);
软中断不能睡眠,能被中断处理程序抢占。如果同一个软中断在它被执行的同时再次被触发了,那么在其他CPU上可以同时执行其处理程序,这意味着要在软中断的上下文要采用锁来保护,但是如果加锁的话,使用软中断的意义就不大了。所以软中断中一般使用的是单处理器数据(仅属于某一个处理器的数据)。
 
所以软中断作为BH用的比较少,一般采用tasklet。
 
4. tasklet
tasklet也是用软中断实现的,有两个软中断和tasklet有关:HI_SOFTIRQ,  TASKLET_SOFTIRQ
tasklet有两个单处理器数据结构: tasklet_vec 和 tasklet_hi_vec
tasklet 可以保证同一时间里给定类别中只有一个tasklet会被执行,不同类别的tasklet可以同时执行,所以使用tasklet可以不用过多的考虑锁的问题
tasklet的使用:
声明:(name是tasklet的类别)
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, funct, data);  //声明默认disable的tasklet
 
tasklet处理程序的格式:
void tasklet_handler(unsigned long data)
 
调度自己定义的tasklet
tasklet_schedule(&my_tasklet);
 
启用和禁用tasklet
tasklet_disable(&my_tasklet);   // 如果指定的tasklet正在执行,则等到执行结束再返回
tasklet_enable(&my_tasklet);
 
一个tasklet总在调度它的CPU上执行,这是希望能更好的利用处理器的高速缓存
一个tasklet只要在一个CPU上被执行了,就不会同时在另一个CPU上执行它,(因为有可能一个tasklet在执行的时候,中断处理程序在另一个CPU上又激活了这个tasklet,这样在另一个CPU上如果发现这个tasklet正在被执行,便不会再执行了)
 
5. 软中断调度时机和ksoftirqd/n内核线程
由于软中断可以被自行重新触发,所以如果软中断中不断触发软中断,而且软中断立即被检查执行,那么就会导致系统的CPU被软中断占用的过多。
另一个方案是:自行重新触发的软中断并不马上被检查执行,而是在下一次中断处理程序返回后检查执行。
 
ksoftirqd/n是在每个cpu上都有一个的内核线程,只要有未处理的软中断,在空闲CPU上就会被调度执行。
 
6. 工作队列
工作队列把工作交给一个内核线程去执行,总是在进程上下文中执行。
而软中断和tasklet可能在中断上下文执行,也可能在进程上下文执行。
因为总是在进程上下文执行,所以工作队列可以重新调度甚至睡眠。
如果下半部中要允许重新调度,那么可以使用工作队列。
 
工作者线程:  events/n
worker_thread()是核心函数:
 
下面是run_workqueue()

workqueue_struct,  cpu_workqueue_struct, 以及work_struct之间的关系

 
工作队列的使用:
DECLARE_WORK(name, void (*func) (void *), void *data);   //静态创建
INIT_WORK(struct work_struct *work, void (*func) (void *), void *data);  //动态创建
 
工作队列处理函数
void work_handler(void * data);
 
调度工作执行
schedule_work(&work);
schedule_delayed_work(&work, delay);
 
问题:每个CPU上都有一个events/n内核线程,那么schedule_work(&work)时是把work挂到哪个CPU上的events list上的呢?
 
7. 禁止下半部
local_bh_disable()
local_bh_enable()
这两个函数可以嵌套,即disable几次就需要enable几次;这是因为这两个函数使用了preempt_count(抢占计数)来记录引用计数
 


第九章  内核同步
 
1. 造成并发的原因
a) 中断
b) 软中断和tasklet
c)  内核抢占:内核任务别抢占
d) 睡眠以及与用户空间的同步:内核任务被用户进程抢占
e) SMP CPU:多处理器并行执行
 
用户态进程一般只需要考虑两个因素:SMP CPU  和 进程抢占
SMP CPU可能会并行执行
进程抢占需要代码支持可重入
 
这两种情况都主要是怕两个进程(线程)同时访问全局数据或共享数据,如果两个进程(线程)不共享数据那么就肯定是安全代码,如果要共享数据,就必须加锁
 
2. 中断安全代码,SMP安全代码,抢占安全代码(即可重入代码)
 
3. 只要是共享数据,就要加锁;所以尽量不要共享数据;(个人想法:在要进入IO消耗性代码时,可以考虑共享数据,例如:在要访问数据库时,因为访问数据库时肯定要有IO操作,多个线程要访问同一个数据库连接时要加锁,因为反正进程要切换,而且用一个数据库连接还可以做缓存。如果采用多个数据库连接,那么缓存也变成了问题)
 
4. 记住:给数据加锁,不要给代码加锁
 
5.

 
6. 死锁
死锁的原因:多个进程,多个资源;多个进程分别持有了一部分资源,又要请求其它资源;结果互相等待。
 
7. 锁的争用和加锁粒度
高度争用的锁容易造成系统的瓶颈
加锁粒度:细粒度的锁可以降低锁的争用
 

第十章  内核同步方法
 
1. atomic_t 原子操作
 
2. 自旋锁
DEFINE_SPINLOCK(mr_lock);  // 静态初始化
spin_lock_init(&mr_lock);  //动态初始化
spin_lock(&mr_lock);
...
spin_unlock(&mr_lock);
 
注意:在中断上下文要用
spin_lock_irqsave(&mr_lock, flags);
...
spin_unlock_irqrestore(&mr_lock, flags);
 
如果能确定中断在加锁之前是激活的,那么可以用下面的API
spin_lock_irq(&mr_lock);
...
spin_unlock_irq(&mr_lock);
 
非阻塞操作
spin_try_lock(&mr_lock);
spin_is_locked(&mr_lock);
 
自旋锁和下半部,在下半部中要用下面的API:
spin_lock_bh()
spin_unlock_bh()
注意:
如果下半部和进程上下文共享数据,就要用这个来加锁
如果下半部和中断上下文共享数据,就要用spin_lock_irqsave/spin_unlock_irqrestore
 
读写自旋锁
DEFINE_RWLOCK(lock)
read_lock/read_unlock
write_lock/write_unlock
 
3. 信号量(semaphore)
DECLARE_MUTEX(name)
sema_init(sem, count)
init_MUTEX(sem)
init_MUTEX_LOCKED(sem)
 
获取信号量:
down_interruptible()  如果无法获得,该函数把进程状态设为TASK_INTERRUPTIABLE,并进入睡眠
down_trylock()  非阻塞获得sem
down()  无法获得时进入不可中断睡眠(TASK_UNINTERRUPTIABLE)
up()  唤醒
 
读写信号量
 
4. 互斥体(mutex)
计数是1的信号量
DEFINE_MUTEX(mutex)
mutex_init(&mutex)
mutex_lock(&mutex)
mutex_unlock(&mutex)
 
比较:
信号量和互斥体:一般应用中互斥体,除非互斥体无法满足需求
信号量和自旋锁:中断上下文只能用自旋锁

 
5. 完成变量
思想和信号量类似,只是一种针对更简单问题的一种解决方案
init_completion(struct completion *);
wait_for_completion()
complete()
 
6. 大内核锁
lock_kernel()
unlock_kernel()
尽量少用
 
7. 顺序锁
特定情况下使用,一般是写很少,读很多的时候,而且数据操作简单时较好
 
8. 禁止抢占
preempt_disable()
preempt_enable()
preempt_enable_no_resched()
preempt_count()
 
9. 内存屏障
由于编译器会进行读写重排序,所以加入内存屏障来确保读写顺序
 
在SMP上常见,多个CPU操作同一个数据时,可能一个CPU似乎已经写入了,但是另一个CPU读出的还是原来的值,常常出现在多线程共享数据而又没有加锁的情况下
rmb()  确保跨越rmb()的读操作不会被重排序
wmb()  确保跨越wmb()的写操作不会被重排序
mb()   确保跨越mb()的读写操作都不会被重排序
smp_rmb() / smp_wmb() / smp_mb()

 

第12章  内存管理
 
1. 整页的分配和释放

 

 
2. kmalloc和slab


 

 
kmalloc是基于slab分配器实现的。
slab分配器主要解决的是分配不规则字节内存造成的内存碎片的,还有加快内存的分配和释放时间
kmem_cache_create()
 
3. vmalloc
vmalloc和kmalloc类似,都是分配物理内存,但是kmalloc分配的物理内存和虚拟内存都一定是连续的;而vmalloc分配的虚拟内存是连续的,物理内存可能不连续
另外,kmalloc的性能更好,因为vmalloc必须建立专门的页表项,而kmalloc因为是直接映射的,所以无需页表
(TLB:缓存虚拟地址到物理地址的高速缓存)
 
所以vmalloc在内核中一般只是在要分配大块内存时使用,例如加载模块时
 
vfree(void *)是否vmalloc分配的内存
 
4. 栈上的内存管理
32位和64位体系结构的页面大小分别是4KB和8KB,一般内核进程的栈是2页,所以一般是8KB和16KB
千万不要在内核栈上面分配大量数据,容易造成栈溢出
 
5. 高端内存的映射
使用alloc_pages()分配的高端内存返回的是page*结构,因为高端内存没有直接的逻辑地址映射,所以要建立页表来映射
永久映射:   kmap/kunmap (可能睡眠)
临时映射: (在不能睡眠的情况下使用)  kmap_atomic/kunmap_atomic  (其实是有一组保留的映射地址)
 
6. 分配函数的选择
一般用kmalloc和_get_free_pages
如果要从高端内存分配,用alloc_pages + kmap
如果分配的内存较大,而且无需物理上连续,就用vmalloc
如果要做分配管理,请用slab分配器
 

第十三章  虚拟文件系统
 
1. VFS文件系统抽象层

2. Unix的四种和文件系统相关的传统抽象概念
文件,目录项,索引节点(inode,存储文件的元数据),mount节点
 
另外,文件系统的控制信息被放在了超级块中
 
===摘抄===
大部分UNIX文件系统种类具有类似的通用结构,即使细节有些变化。其中心概念是超级块superblock, i节点inode, 数据块data block,目录块directory block, 。其中超级块中包含了关于该硬盘或分区上的文件系统的整体信息,如文件系统的大小(其准确信息依赖文件系统)等。 i节点包括除了名字外的一个文件的所有信息,名字与i节点数目一起存在目录中,目录条目包括文件名和文件的i节点数目。 i节点包括几个数据块的数目,用于存储文件的数据。 i节点中只有少量数据块数的空间,如果需要更多,会动态分配指向数据块的指针空间。这些动态分配的块是间接块;为了找到数据块,这名字指出它必须先找到间接块的号码。 
=========
 3. 超级块
超级块记录文件系统基本信息
 
超级块的操作主要是CRUD inode
 
4. inode
inode存放的是文件或目录的所有信息:几个时间,引用计数,uid,gid,文件大小,权限
一个inode就代表了一个文件:可以是普通文件,也可以是管道,块设备,字符设备等
 
inode的操作:CRUD文件,修改权限,truncate,mkdir, mknod, rename等等
通过inode可以寻找到dentry对象
 
5. dentry目录项对象
目录和普通文件都是一个目录项对象
主要用来解析文件路径
目录项结构中维护了整个文件目录树
 
dentry的操作:判断目录对象是否有效,比较文件名,查找文件等
 
6. 文件对象
文件对象存放的是文件信息:文件路径,文件操作file_operations, 文件offset,页高速缓存地址
文件的操作:  read/write/lseek/ioctl/fsync/open/close/mmap...