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...

时间: 2024-08-08 22:06:11

Linux内核设计与实现 读书笔记的相关文章

Linux内核设计与实现 读书笔记 转

Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://blog.csdn.net/yrj/article/category/718110 Linux内存管理和性能学习笔记(一) :内存测量与堆内存 第一篇 内存的测量 2.1. 系统当前可用内存 # cat /proc/meminfoMemTotal:        8063544 kBMemFree:       

Linux内核设计与实现读书笔记——第三章

Linux内核设计与实现读书笔记——第三章 进程管理 20135111李光豫 3.1进程 1.进程即处于执行期的程序,并不局限于一个可执行的代码,是处于执行期程序以及其相关资源的总称. 2.Linux系统中,对于进程和线程并没有明显的区分,线程是一种特殊的进程. 3.Linux系统中,常用fork()进程创建子进程.调用fork()进程的成之为其子进程的父进程. 4.fork()继承实际上由clone()系统调用实现.最后通过exit()退出执行. 3.2任务描述符及任务结构 1.任务队列实质上

Linux内核设计与实现读书笔记——第十八章

第18章 调试 调试工作艰难是内核级开发区别于用户级开发的一个显著特点,相比于用户级开发,内核调试的难度确实要艰苦得多.更可怕的是,它带来的风险比用户级别更高,内核的一个错误往往立刻就能让系统崩溃. 18.1 准备开始 一个bug.听起来很可笑,但确实需要一个确定的bug.如果错误总是能够重现的话,那对我们会有很大的帮助(有一部分错误确实如此).然而不幸的是,大部分bug通常都不是行为可靠而且定义明确的. 一个藏匿bug的内核版本.如果你知道这个bug最早出现在哪个内核版本中那就再理想不过了.

《Linux内核设计与实现读书笔记之系统调用》

1.系统调用的概念 为了和用户空间上运行的进程进行交互,内核提供了一组借口.透过该接口,应用程序可以访问硬件设备和其他操作系统资源.这组借口在应用程序和内核之间扮演着使者的角色.同时,这组接口也保证了系统稳定可靠,避免应用程序肆意妄行,惹出麻烦.Linux系统的系统调用作为C库的一部分提供,其调用过程中的实例如下图所示: 从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了.相反,内核只跟系统调用打交道,库函数以及应用程序是怎么使用系统调用不是内核所关心的. 2.系统调用的处理程

Linux内核设计与实现——读书笔记2:进程管理

1.进程: (1)处于执行期的程序,但不止是代码,还包括各种程序运行时所需的资源,实际上进程是正在执行的 程序的实时结果. (2)程序的本身并不是进程,进程是处于执行期的程序及其相关资源的总称. (3)两个或两个以上并存的进程可以共享诸如打开的文件,地址空间等共享资源. (4)在Linux中通常是调用fork()系统函数的结果,通过复制一个现有的进程来创建一个新的子进程. fork()系统函数 (5)fork在这个系统调用结束时,在同一位置上返回两次(从内核返回两次),父进程恢复运行,子进程开始

《Linux内核设计与实现》笔记-1-linux内核简介

一.Linux内核相对于传统的UNIX内核的比较: (1):Linux支持动态内核模块.尽管Linux内核也是整体式结构,可是允许在需要的时候动态哦卸除(rmmod xxx)和加载内核模块(insmod  xxx.ko). (2):Linux支持对称多处理(SMP)机制,尽管许多UNIX的变体也支持SMP,但是传统的UNIX并不支持这种机制. (3):Linux内核可以抢占(preemptive).在Linux 2.4以及以前的版本都是不支持内核抢占的,在Linux 2.6以及以后就支持了. (

《Linux内核设计与实现》笔记——内核同步简介

相关概念 竞争条件 多个执行线程(进程/线程/中断处理程序)并发(并行)访问共享资源,因为执行顺序不一样造成结果不一样的情况,称为竞争条件(race condition) 举例说明 #include<thread> using namespace std; int i = 0; void thread1(){ //for(int x=0;x<100000;x++) i++; } void thread2(){ //for(int x=0;x<100000;x++) i++; } i

Linux内核架构与底层--读书笔记

linux中管道符"|"的作用 命令格式:命令A|命令B,即命令1的正确输出作为命令B的操作对象(下图应用别人的图片) 1. 例如: ps aux | grep "test"  在 ps aux中的結果中查找test. 2. 例如:   find . -name "*.txt" | xargs grep "good" -n --color=auto   把find的结果当成参数传入到grep中,即在那些文件内部查找good关键

Linux内核设计与实现 阅读笔记:8、下半部和推后执行的工作

By:Ailson Jack Date:2016.04.10 个人博客:www.only2fire.com 本文在我博客的地址是:http://www.only2fire.com/archives/871.html,排版更好,便于学习. 上一章简单的讲了一下中断的上半部(中断处理程序),这一章就讲讲中断的下半部以及下半部的几种实现机制,最后简单的写了几个测试的例子来测试软中断.tasklet和工作队列. 测试程序下载地址:. 1.下半部简述 中断下半部的任务是执行与中断处理密切相关但中断处理程序