Linux Kernel Development 学习

处理器的活动可以分为3类:

运行于用户空间,执行用户进程

运行于内核空间,处于进程上下文,代表某个特定的进程进行

运行于内核空间,处于中断上下文,与任何进程都无关,处理某个特定的中断

 

包含了所有情况,边边角角也不例外。例如CPU空闲时,内核就运行一个空进程,处于进程上下文,但运行于内核空间

 

微内核架构(Micro kernel)和单内核架构(Monolithic kernel)的区别。

 

--微内核——

 

最常用的功能被设计在内核模式(x86上为 0权限下),其他不怎么重要的功能都作为单独的进程运行在用户模式下(3权限下),通过

消息传递进行通讯(windows采用进程间通信IPC机制,IPCInter Process Communication) 最基本的思想是尽量的小,通常微内核只

包括进程调度,内存管理和进程间通信这几个基本功能。

 

好处:增加了灵活性,易于维护,易于移植。其他的核心功能模块都只依赖于微内核模块和其他模块,并不直接依赖硬件。

由于模块化设计,不包含在微内核内的驱动程序可以动态的加载或者卸载。

还具有的好处就是实时性、安全性较好,并且更适合于构建分布式操作系统和面向对象操作系统。

典型的操作系统中例子:Mach(非原生的分布式操作系统,被应用在Max OS X上)、IBM AIX、BeOS以及Windows NT

 

--单内核--

 

单内核是个很大的进程,内部又被分为若干的模块(或层次,或其他),但在运行时,是一个单独的大型二进制映像/因为在同一个进程

内,其模块间的通讯是通过直接调用其他模块中的函数实现的,而不是微内核中多个进程间的消息传递,运行效率上单内核有一定的好处。

 

典型的操作系统中的例子:大部分Linux、包括BSD在内的所有Linux(编译过 Linux的人知道Linux内核有数十MB)

 

 

即:

IPC机制的开销多于函数调用,又因为会涉及内核空间与用户空间的上下文切换,因此消息传递需要一定的周期,而单内核中的函数调用则没有这些开销。

结果实际上基于微内核的系统都让大部分或者全部服务器位于内核,这样可以直接调用函数,消除频繁的上下文切换。

 

Linux与传统Unix不同的地方:

1. Linux支持动态的加载内核模块。虽然是单内核的, 但是允许在需要的时候动态地卸载和加载部分内核代码。

2. Linux支持对称多处理(SMP)机制,传统的Unix并不支持这种机制。

3. Linux内核可抢占。Linux内核具有允许在内核运行的任务优先执行的能力。

4.Linux对线程的支持:并不区分线程和其他的一般进程。

 

内核配置要么是2选1,要么是3选1:

2选1就是yes或no,3选1的话为yes, no和module

module代表被选定,但编译时这部分的功能实现代码是模块,yes选项代表把代码编译进主内核模块中,而不是作为一个模块。

驱动程序一般选用3选1的配置。

 

Linux输出重定向:

> 直接把内容生成到指定文件,会覆盖源文件中的内容,还有一种用途是直接生成一个空白文件,相当于touch命令
>> 尾部追加,不会覆盖掉文件中原有的内容

make程序能把编译过程拆分成多个并行的作业。其中每个作业独立并发的运行,这有助于极大地加快多处理器系统上的编译过程,也有利于改善处理器的利用率。

 

内联函数的定义: inline int add_int(int x, int y, int z){return x+y+z};

通常将对时间要求比较高,本身长度比较短的函数定义成内联函数。若函数较大且会被反复调用,不赞成定义为内联函数。

在程序中,调用其函数时,该函数在编译时被替代(即将调用表达式用内联函数体来替换),而不是在运行时被调用。

要注意:内联函数内不允许用循环和开关语句。

有意思:#define MAX(a, b) (a) > (b) ? (a) : (b)

如果你在代码中这样写:

int a = 10, b = 5;

// int max = MAX(++a, b); // a自增了两次

// int max = MAX(++a, b+10); // a自增了一次

 

进程管理:

每个线程都有一个独立的程序计数器,进程栈和一组进程寄存器。

内核调度的对象是线程。

 

--进程描述符和任务结构--

进城的列表放在任务队列(task list)的双向循环链表中,每一项的结构都是task_struct,称为进程描述符的(process discriptor)结构,包含一个具体进程的所有信息。

task_struct相对较大,32位机器上大约有1.7KB,包括:所打开的文件,挂起的信号,进程的状态等

 

Linux通过slab分配器分配task _struct结构,各个进程的task _struct存放在内核栈的尾端,只需在栈底(对于向下增长而言)创建一个新的结构  struct thread_info  (这个结构使得汇编中计算其偏移变得十分容易)

 

内核中大部分处理进程的代码都是直接通过 task_struct 进行的。PowerPC中当前的 task_struct 专门保存在一个寄存器中,x86需要在尾端创建 thread_info 来计算偏移间接查找 task_struct 结构。

 

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的访问都需要经过这些接口。

 

内核经常需要在后台执行一些操作,这种任务可以通过内核线程(kernel thread)完成——独立运行在内核空间的标准进程。和不同进程的区别在于:没有独立的地址空间(指向地址空间的mm指针被定为NULL),只在内核空间运行,从来不切换到用户空间中去。内核进程和普通进程一样,可以被调度和抢占。

 

进程的终结发生在进程调用exit()系统调用,可能显式调用,也可能从某个程序的主函数返回。

在删除进程描述符之前,进程存在的唯一目的就是向父进程提供信息。

即进程终结时所需的清理工作和进程描述符的删除被分开执行。

最后的工作为从任务列表中删除此进程,同时释放进程内核栈和thread_info所占的页,并且释放task_struct所占的slab高速缓存。

 

在为没有父进程的子进程寻找父进程时使用的两个链表:子进程链表和ptrace子进程链表

当一个进程被跟踪时,临时父亲会被设置为调试进程。如果此时他的父进程退出了,则系统会子进程和其兄弟找一个新的父进程。

以前处理这个过程需要遍历系统来寻找子进程,现在在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程——用两个较小的链表减轻了遍历带来的消耗。

 

进程调度:

 

多任务系统分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)

 

抢占式:进程被抢占之前能够运行的时间是设定好的,叫做进程的时间片(timeslice)

非抢占式:进程主动挂起自己的操作称为让步(yielding)

 

Linux的2.5版本调度称为O(1),但其对于调度那些响应时间敏感的程序却有一些先天的不足,这些程序称为交互进程。2.6版本为了提高对交互程序的调度性能引入了新的进程调度算法,最为著名的是“反转楼梯最后期限调度算法”(Rotating Staircase Deadline scheduler)(RSDL),此算法吸收了队列理论,将公平调度的概念引入了Linux调度程序。此刻完全替代了O(1)调度算法,被称为“完全公平调度算法”,或者简称CFS。

 

 

--策略--

I/O消耗型和处理器消耗型的进程:

前者指的是进程的大部分时间都用来提交I/O请求或者是等待I/O请求。这样的进程经常处于可运行状态,但是都是运行短短的一会,因为等待请求时总会阻塞。

后者把时间用在执行代码上,除非被抢占,通常都一直不断的运行。从系统响应速度考虑,调度起不应该经常让它们运行,策略通常是降低调度频率,同时延长运行时间。(极限例子是无限循环的执行)

 

调度策略通常需要在两者间找到平衡:进程响应迅速(响应时间短)和最大系统利用率(吞吐量)

 

总结:一个是等待I/O,一个是数学计算,Linux倾向于优先调度I/O消耗型

 

 

 

进程优先级:

Linux采用两种优先级

  1. nice值。范围从-20到+19,值越大优先级越低
  2. 实时优先级。从0到99,包括0和99,数值越高优先级越高

 

实时优先级和nice优先级处于互不相交的两个范畴,任何实时进程的优先级都高于普通的进程。

 

一般情况下默认的时间片都很短,通常为10ms。但是Linux的CFS调度器并没有直接分配时间片到进程,而是将处理器的使用比划分给了进程。这个比例还会受到nice值的影响,nice值作为权重将调整进程所使用的处理器时间使用比。同时Linux中的抢占时机也取决于新的可运行程序消耗了多少处理器使用比。若比当前进程小,则立即投入,抢占当前进程,否则推迟。

 

通过文本编辑和视频处理例子了解处理器使用比!

 

--Linux调度算法--

 

Linux的调度器是以模块方式提供的,允许不同类型的进程有针对性的选择调度算法。

这种模块化结构称为调度器类(scheduler classes)。基础的调度器会按照优先级顺序遍历调度类

 

完全公平调度(CFS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL(POSIX中称为SCHED_OTHER)

 

 

基于nice值的调度算法可能出现的问题:

1.进程切换无法最优化:两个同等低优先级的进程均只能获得很短的时间片,需要进行大量上下文切换

2.根据优先级不同,获得的处理器时间差异很大(99和100差1,1和2也差一,但是2倍)

3.涉及到时间片的映射,需要分配一个绝对时间片,时间片也可能会随着定时器节拍的改变而改变。

4.优化唤醒时打破公平原则,获得更多处理器时间的同时,损害其他进程的利益

 

 

公平调度:

每个进程能获得1/n的处理器时间--n指的是可运行进程的数量。

不再采取给每个进程时间片的做法,而是在所有可运行进程的总数基础上计算一个进程应该运行多久。不依靠nice来计算时间片,而是计算作为进程获得的处理器运行比的权重。nice低,权重高

 

CFS为无限小小调度周期的近似值设定了目标,称为“目标延迟”

小的调度周期带来了好的交互性,但是必须承受高的切换代价和更差的系统总吞吐能力

 

假设目标延迟为20ms,若有2个相同优先级的任务,每个运行10ms,4个则为5ms

当任务趋于无限的时候,处理器使用比趋于0,为此引入了时间片底线,称为“最小粒度”,默认1ms

即可运行进程的数量趋于无限时,每个进程最少获得1ms的运行时间

 

CFS中几个重要概念:

1. nice值越小,进程的权重越大。同时nice值的相差使得权重间程倍数关系。

2. CFS调度器的一个调度周期值是固定的,由sysctl_sched_latency变量保存。

3. 进程在一个调度周期中的运行时间为:

分配给进程的运行时间 = 调度周期 * 权重 / 所有进程的权重之和

    即权重越大,分配到的时间越多

4. 一个进程的实际运行时间和虚拟时间(vruntime)的关系

vruntime = 实际运行时间 * NICE_0_LOAD(1024)/ 进程权重

    进程权重越大,运行相同的实际时间,vruntime增长的越慢

5. 一个进程在一个调度周期内的虚拟运行时间大小代入前两式可得

vruntime = 调度周期 * 1024 / 所有进程进程权重(定值!!)

    即所有进程的vruntime都是一样的。

http://blog.csdn.net/liuxiaowu19911121/article/details/47070111

 

6. 红黑树中均为可执行的进程,若标记为休眠状态,则从树中移出。

------------------------------

内核通过need_resched标识来表明是否需要重新执行一次调度。每个进程都包含有这个标志,因为访问进程描述符内的数值比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中)

 

 

用户抢占发生在:

  1. 从系统调用返回用户空间时
  2. 从中断处理程序返回用户空间时

 

内核抢占:

只要没有锁,内核就可以进行抢占。

通过设置thread_info中的preempt_count计数器,有锁的时候加1,释放-1,数值为0的时候内核可以进行抢占。

内核抢占发生在:

  1. 中断处理程序在执行,且返回内核空间之前
  2. 内核代码再一次具有可抢占性的时候
  3. 内核任务显示调用schedule()
  4. 内核中的任务阻塞(导致调用schedule())

Linux中的实时调度策略:SCHED_FIFO和SCHED_RR,非实时的调度策略时SCHED_NORMAL

实时策略并不被CFS管理,而是用一个特殊的实时调度器管理

 

SCHED_FIFO不是用时间片,可运行态的SCHED_FIFO比任何SCHED_NORMAL都先得到调度

只要其在运行,较低级别的进程只有等待其变成不可运行状态后才有机会执行。

 

SCHED_RR是带有时间片的SCHED_FIFO,只有消耗事先分配的时间片后就不能继续执行

 

实时优先级范围从0-99,SCHED_NORMAL进程的nice值共享了这个取值空间,即其-19到20为100到139的实时优先级范围

 

 

 

 

 

 

Linux 的5个段:

BSS段:(bss segment) 通常用来存放程序中未初始化的全局变量的一块内存区域。BSS是Block Started by Symbol 的简称,BSS段属于静态内存分配。

数据段:(data segment):通常用来存放程序中已初始化的全局变量的一块内存区域,属于静态分配

代码段:(code segment):通常指的是存放程序执行代码的一块内存区域。区域的大小在程序执行前就已经确定,内存区域属于只读。(某些架构支持可写,即允许修改程序)

堆:(heap):用于存放进程运行中被动态分配的内存段,大小不确定,可以动态的扩张或缩减。调用malloc等函数分配内存时,新分配的内存被动态添加到堆上;free程序释放内存时,被释放的内存从堆中被剔除。(堆的位置在BSS的后面,并从其后开始增长)

栈:(stack):用户存放程序临时创建的变量,即函数{}中存放的变量(但不包括static声明的变量,其存放在数据段中);同时函数被调用时,参数也会被压入被调用的进程栈中。是由操作系统分配的,内存的申请和回收都由OS管理。

 

PS:

 

bss段(未手动初始化的段)并不给该段分配空间,只是记录数据所需空间的大小

data段(已手动初始化的数据)为数据分配空间,数据段包括经过初始化的全局变量以及它们的值。BSS的大小可以从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段后面。包括数据块和BSS段的整个区段成为数据区。

 

 

 

定时器和时间管理:

 

默认的系统定时器频率根据体系结构的不同会有所不同,但基本上都是100HZ

 

更高的时钟中断频度和更高的准确度使得依赖定时值执行的系统调用,譬如poll(), select()能够以更高的精度运行;对诸如资源消耗和系统运行时间的测量会有更精细的解析度;提高进程抢占的准确度。

劣势在于:时钟中断的频率越高,系统的负担越重,中断处理程序占用的处理器的时间越多。

 

jiffies:全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后每次时钟中断处理程序就会增加改变该变量的值。

把时钟作为秒经常会用在内核和用户空间进程交互的时候:

unsigned long next_stick = jiffies + 5 * HZ     /*从现在开始5s*/

注意:jiffies的类型为无符号长整形(unsigned long)(32位),用其他任何类型存放它都不正确

 

jiffies的回绕(wrap around),超过最大范围后就会发生溢出。

内核提供了4个宏来帮忙比较节拍计数。

#define time_after(unknown, known)   ——   ( (long)(known) - (long)(unknown) < 0 )

#define time_before(unknown, known)   ——   ( (long)(unknown) - (long)(known) < 0 )

其中

time_after(unknown, known)当unknown超过指定的known时,返回真,否则返回假;

time_before相反 

 

实时时钟(RTC)用来持久存放系统时间的设备。在PC体系结构中,RTC和CMOS集成在一起,而且RTC的运行和BIOS的保存设置都是通过同一个电池供电的。RTC最主要的作用是启动时初始化xtime变量。

 

墙上时间代表了当前的实际时间。

 

内核对于进程进行时间计数时,时根据中断发生时处理器所处的模式进行分类统计的。

x86机器上时钟中断处理程序每秒执行100次或者1000次

 

定时器结构由timer_list表示,内核提供了一组和定时器相关的接口来简化定时器操作,并不需要深入了解其数据结构:

创建时定义:struct timer_list my_timer;

 

初始化定时器数据结构内部的值

init_timer(&my_timer);

 

填充数据结构中的值:

my_timer.expires = jiffies + delay;  /*定时器超时时的节拍数*/

my_timer.data = 0; /*给定时器处理函数传入0值*/

my_timer.function = my_function; /*定时器超时时调用的函数*/

 

data参数可以利用同一个处理函数注册多个定时器

 

最后激活定时器:

add_timer(&my_timer);

 

 

 

--延时执行的方法--

1.忙等待:

  unsigned long timeout = jiffies + 10;

  while (time_before(jiffies, timeout))

          ;

处理器只能原地等待,不会去处理其他任务,所以基本不采用此方法。

 

2.更好的方法应该是在等待时允许内核重新调度执行其他任务:

  unsigned long delay = jiffies + 5*HZ;

  while(time_before(jiffies,delay))

  cond_resched();

 

cond_resched()函数讲调度一个新程序投入运行,但只有设置完成need_resched标志才能生效

另外由于其需要调用调度程序,所以不能在中断上下文中使用--只能在进程上下文中使用。

 

我们要求jiffies在每次循环时都必须重新装载,因为在后台jieffies会随着时钟中断的发生而不断增加。为了解决这个问题,<linux/jiffies.h>中的jiffies变量被标记为关键字volatile, 指示编译器在每次访问变量时都能重新从主内存中获得,而不是通过寄存器中的变量别名来访问,从而确保循环中的jieffies每次被读取都会重新载入。

 

短延迟

jiffies的节拍间隔可能超过10ms,不能用于短延迟。

内核提供了3个延迟函数,void udelay(), void ndelay() 以及void mdelay()。

 

系统调用:

 

内核提供了用户进程与内核进行交互的一组接口,应用程序提供各种请求,而内核负责满足这些请求。

 

作用:

  1. 为用户空间提供了一种硬件的抽象接口。例如读写文件时无需考虑磁盘类型和介质等
  2. 保证了系统的安全和稳定。内核可以基于权限,用户类型等对访问进行裁决

 

Linux 中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。

 

应用程序通过用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程

一个API定义了一组应用程序使用的编程接口,可通过一个或多个甚至不使用系统调用来实现

 

Unix接口设计格言:提供机制而非策略

机制:mechanism(需要提供什么样的功能) 策略:policy(怎么实现这样的功能)

 

 

--系统调用--

 

线程組leader的PID(也就是线程组中头一个轻量级线程的PID)被线程共享,保存在thread_info->tgid中。 getpid()返回tgid的值,而不是PID值。对thread group leader来说,tgid = pid

 

Linux中每个系统调用被赋予一个系统调用号。系统调用号一旦分配就不能再有任何变更;如果被删除,占用的系统号也不允许被回收利用。

系统中所有注册过的系统调用,储存在sys_call_table中。

 

系统调用的性能:Linux 的系统调用比其他许多操作系统执行的要快。Linux很短的上下文切换时间是一个重要原因;同时系统调用处理程序和每个系统调用本身也都非常简洁。

 

用关空间的程序无法直接执行内核代码,需要通知内核自己需要执行一个系统调用。

实现方法是软中断:通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理就是系统调用处理程序。

x86系统上预定义的软中断是中断号128,处理程序的名字叫system_call()

同时系统调用号是通过eax寄存器传递给内核的。

 

写系统调用时,要时刻注意可移植性和健壮性。

 

参数验证:系统调用在内核空间执行,如果有不合法的输入传递给内核,系统的安全和稳定将面临极大的考验。

与I/O相关的系统调用必须检查文件描述符是否有效,与进程相关的函数必须检查提供的PID是否有效。进程不应当让内核去访问那些它无权访问的资源。

最重要的检查是检查用户提供的指针是否有效,内核必须保证:

 

  • 指针指向的内存区域属于用户空间,不允许读取内核空间的数据
  • 指针指向的内存区域在进程的地址空间里。绝不能哄骗内核去读其他进程的数据
  • 如果为读/写/执行,该内存应该被标记为可读/可写/可执行。不能绕过内存访问限制

 

内核提供了两个方法来完成必须的检查,以及内核空间和用户空间之间数据的来回拷贝。

1.写数据提供了copy_to_user(),读数据提供了copy_from_user()

均需要三个参数,进程空间中的内存地址,内核空间的原地址,需要拷贝的数据长度(字节数)

 

2.最后一项检查是否具有合法权限。

老版本Linux内核需要超级用户权限的系统调用来调用suser()函数来完成检查。(这个函数只检查是否为超级用户)

 

新的系统允许检查针对特定资源的特殊权限:

调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,返回非0则有权操作,返回0则无权操作。

例如capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。

参见<linux/capability.h>,其中包含一份所有这些权能和其对应的权限的列表。

 

建立一个新的系统调用十分容易,但是不提倡这么做。

替代方法:实现一个设备节点,对此实现read()和write()。使用ioctl()对特定的设置进行操作或者对特定的信息进行检索。

 

 

内核数据结构:

 

1.链表数据结构

Linux内核方式与众不同,它不是将数据结构塞入链表,而是将链表节点塞入数据结构!

即将现有的数据结构改造成链表,通过塞入链表节点!

 

内核提供的链表操作例程,比如list_add()方法加入一个新节点到链表中。这些方法都有一个特点:只接受list_head结构作为参数

 

使用宏container_of()我们可以很方便地从链表指针找到父结构中包含的任何变量。因为C语言中,一个给定结构的变量偏移在编译时地址就被ABI固定下来了。

使用container,定义list_entry(),就可以返回包含list_head的父类型的结构体

同时内核提供了创建,操作以及其他链表管理的各种例程。

 

内核链表的特性在于,所有的struct节点都是无差别的--每一个包含一个list_head(),即可以从任何一个节点遍历链表。不过有时需要一个特殊指针索引。

内核提供的函数都是用C语言以内联函数的方式实现的,原型存在于文件<linux/list.h>

 

指向一个链表结构的指针通常是无用的;我们需要的是一个指向包含list_head的结构体的指针。

 

 

2.队列

Linux内核通用队列实现称为kfifo,提供两个主要的操作:enqueue(入队列)和dequeue(出队列)

kfifo对象维护了两个偏移量:入口偏移和出口偏移

入口偏移指的是入队列的位置,出口偏移指的是出队列的位置。

出口偏移总是小于入口偏移。

 

enqueue操作拷贝数据到入口偏移位置,动作完成后,入口偏移加上推入的元素数目;

dequeue操作从队列出口偏移拷贝数据,动作完成后,出口偏移减去摘取的元素数目。

 

--创建队列--

动态创建:

int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t, gfp_mask);

该函数会初始化一个大小为size的kfifo,内核使用gfp_mask标识分配队列,成功返回0,失败返回错误码

struct kfifo fifo;

int ret;

 

ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);

if ( ret )

return ret;

若自己想分配缓冲,可以调用:

void kfifo_init(struct knife *fifo, void *buffer, unsigned int size);

该函数创建并初始化一个kfifo对象,它将使用由buffer指向的size字节大小的内存,对于kfifo_alloc()和kfifo_init(),size必须是2的幂

 

 

 

转载于:https://www.cnblogs.com/lzhdcyy/p/6554884.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值