处理器的活动能够分为3类:linux
运行于用户空间,执行用户进程web
运行于内核空间,处于进程上下文,表明某个特定的进程进行算法
运行于内核空间,处于中断上下文,与任何进程都无关,处理某个特定的中断编程
包含了全部状况,边边角角也不例外。例如CPU空闲时,内核就运行一个空进程,处于进程上下文,但运行于内核空间windows
微内核架构(Micro kernel)和单内核架构(Monolithic kernel)的区别。缓存
--微内核——安全
最经常使用的功能被设计在内核模式(x86上为 0权限下),其余不怎么重要的功能都做为单独的进程运行在用户模式下(3权限下),经过服务器
消息传递进行通信(windows采用进程间通讯IPC机制,IPC(Inter 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采用两种优先级
实时优先级和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宏速度很快而且描述符一般都在高速缓存中)
用户抢占发生在:
内核抢占:
只要没有锁,内核就能够进行抢占。
经过设置thread_info中的preempt_count计数器,有锁的时候加1,释放-1,数值为0的时候内核能够进行抢占。
内核抢占发生在:
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()。
系统调用:
内核提供了用户进程与内核进行交互的一组接口,应用程序提供各类请求,而内核负责知足这些请求。
做用:
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的幂