(1)内核调度的对象是线程,而不是进程,进程是处于执行器的程序以及相关资源的总称。
(2)每个进程都拥有一组独立的进程计数器、进程栈、进程寄存器。
(3)Linux进程也叫任务Task
(4)进程有两种虚拟机制:虚拟处理器和虚拟内存
(5)wait4()可以查询子进程是否终结,子进程退出执行后处于僵死状态,直到父进程调用wait()或waitpid()为止。
(1)进程的列表放在一个叫任务列表的双向循环链表中,链表的每一项都是一个task_struct类型的成为进程描述符的结构体,描述进程的所有信息。
(2)Linux通过slab动态生成task_struct,thread-info在内核栈尾端生成,Thread_info的task域指向任务实际的task_struct
内核通过进程标识号(PID)标识每个进程
(3)current指向当前正在运行的进程的进程描述符
(4)进程状态
task_struct,state域描述进程状态,有五种状态。
TASK_RUNNING(运行)--进程或者在运行,或者在运行队列等待执行。
TASK_INTERRUPTABLE(可中断)--进程在睡眠,等待某些条件达成,一旦接收到信号便立刻执行或准备执行。
TASK_UNINTERRUPTABLE(不可中断)--除了即使收到信号也不会执行或准备执行之外,其他的与可中断状态相同。
TASK_TRACE--进程被跟踪,比如ptrace调试
TASK_STOPOED--进程停止运行。通常在收到各种停止信号之后。在调试期间收到任何信号也会进入这种状态。
set_task_state(task,state)设置状态
(1)fork()拷贝当前进程,子进程除了pid ppid以及一些计量值与父进程不一样外,其他都与父进程一样。然后子进程调用exec()读取并加载可执行文件。两个函数组合起来的效果跟单一的函数效果相似。fork之后,exec之前,父子进程拥有共同的物理地址,不同的虚拟地址。内核有意地让子进程先执行
(2)写时拷贝,调用fork()时并不立即拷贝,而是在写入的时候才拷贝。因为大多数子进程会调用exec()去执行其他的可运行文件,这时就不用拷贝了。所以内核有意地让子进程先执行。
(3)进程创建的过程:dup_task_struct为新进程创建一个内核栈,thread_info,task-struct,此时子进程完全与父进程相同。。。
(1)线程的创建方式与进程的创建类似,只是在调用clone()时需要传递一些参数。
(2)内核线程与普通线程的区别在于内核线程没有独立的地址空间。
(3)kthread_create()创建,wake_up-process唤醒。kthread_run将两者合二为一。do_exit()和ktread_stop停止
(1)do_exit()进程终结
它占用的所有内存就是内核栈、thread_info结构和task_struct结构
(2)wait()->wait4()->release_task();
【1】通过fork()创建新进程并为新进程分配进程栈、task_struct和thread_info,新进程与父进程除了PID、PPID以及一些计数器不一样外,其他的都一样。fork()返回两次,一次返回父进程另一次返回子进程。fork的调用过程是fork()->clone->do_fork()->copy_process.
【2】写时拷贝,新进程创建后大部分都执行exec运行别的可执行文件。所以在新进程创建时并不立刻进程拷贝。而是在进行写操作的时候才拷贝。内核有意让子进程先运行。
【3】当进程结束时,调用exit()->do_exit退出进程并释放占有的资源。之后所占有的仅仅是进程栈、thread_info和task_struct,此时进程存在的唯一目的就是向父进程提供消息。
【4】父进程收到消息后,调用wait->wait4(0->release_task()来删除子进程的进程标识符。
(1)CFS:完全公平调度算法,更倾向于优先调度I/O消耗型进程
(2)nice值,-20---+19,值越大,优先级越小。nice代表时间片比例。动态优先级是0到99,值越大优先级越大。任何实时进程的优先级都高于普通的进程?
调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符内。
(3)调度器类:Linux调度器以模块化的方式提供,允许对不同的进程动态地选择调度器,这种模块化的结构被称为调度器类。调度器类有优先级。系统会遍历调度器类,选择胜出者选择下一个要执行的进程。CFS针对普通的进程调度类,SCHED_NORMAL.
(4)CFS
时间片底限:1ms
任何进程所获得时间片是由他自己和其他所有可运行进程的nice值相对差决定的。
时间记账:
vruntime变量记录虚拟运行时间,单位是ns,时间是经过标准化的时间,记录进程已经运行了的时间和还需要再运行多久
update_curr会被定期地调用并对运行时间进行加权运算更新vruntime
进程选择:
选择vruntime最小的进程,使用红黑树来组织进程队列。
调度器入口:
schedule()选择一个最高优先级的调度器,每一个调度器类都有自己的进程队列,然后再选择一个最高优先级的进程。
CFS是普通进程的调度器类,而大部分是普通进程。
唤醒和睡眠
进程把自己标记成休眠状态,从可执行红黑树中移除,加入等待队列。唤醒的过程刚好相反。
(1)上下文切换由context_switch()实现,每当选择一个新进程执行时,shedule()就会调用该函数。分为两部分:switch_mm():将虚拟内存从上一个进程映射到新的进程中。switch_to():将处理器状态从上一个进程切换到新进程,包括保存、恢复栈信息和寄存器信息。
内核在返回用户空间以及从中断返回时会检查need_resched标志,如果被设置,内核会在继续执行之前调用进程调度程序。每一个进程都有need_reshed标志。因为访问进程描述符的值要比访问一个全局变量快。
(2)用户抢占
内核即将返回用户空间时,如果need_rescheld被设置,导致schedule()被调用,此时就会发生用户抢占。即:从系统调用返回用户空间时或从中断处理程序返回用户空间时。
(3)内核抢占
只要没有锁,内核就可以抢占。发生内核抢占的时间:
【1】中断处理程序正在执行,返回内核空间之前。【2】内核代码再一次具有可抢占性的时候。【3】内核任务显示调用Schedule。【4】内核中任务阻塞
实时调度:
SCHDE_FIFO,优先级大于SCHDE_NORMAL,除非自己受阻塞或显示地释放处理器,只有更高级别的SCHDE_RR或SCHDE_FIFO才能抢占。
SCHDE_RR,本质上和FIFO差不多,只不过具有时间片,轮流地调用
非实时:
SCHDE_NORMAL:
(1)系统调用是用户进程与硬件设备之间的中间层,主要作用
【1】是不同硬件设备的抽象接口
【2】起到保护系统的作用
【3】系统调用是用户空间访问内核的唯一合法入口
C库是POSIX标准的API
(1)通过C库函数访问系统调用,系统调用需要一或多个参数,返回一个long型变量,0表示成功。
(2)每个系统调用都绑定一个系统调用号、一旦分配就不能更改,sys_call_table中记录着系统调用号
用户空间不能直接调用内核代码,所以要通过软中断引发异常陷入内核来调用异常处理程序即系统调用处理程序 通过Int $x080指令调用system_call();系统调用号存储在eax寄存器中,其他参数存储在别的寄存器中。
(1)尽量简洁、参数尽量少、考虑向前向后的兼容性。
(2)参数验证。【1】参数必须指向用户空间【2】参数指向的地址必须在进程地址内【3】如果是读,要有读的权限,如果是写要有写的权限。通过capable()验证是否有权限返回0表示无权操作。copy_to_user()将数据拷贝到用户空间。copy_from_user()将数据拷贝到内核空间。假设进程的页被换出,可能会发生阻塞。
(3)权限验证
(4)绑定系统调用号
(1)系统调用处于进程上下文中,cureent指向当前指针。系统调用返回后仍然处于system_call()的控制权中,最终切换到用户空间。系统调用可以休眠并且可以被中断。这两点为毛重要?
(2)绑定系统调用的步骤
【1】在系统调用表(位于entry.s文件)中添加一个表项。【2】分配系统调用号,必须在
(1)陷入内核,通过$0x80触发128号软中断,内核调用system_call
(2)系统调用号和参数传递,系统调用号通过eax寄存器传递,参数通过其它五个寄存器传递,当参数超过五个时,选择一个单独的寄存器存放指向用户参数的指针
(3)系统调用处理程序验证系统调用号,是否大于NR_syscalls,执行系统调用函数并把返回值带回用户空间
(1)中断处理程序是设备驱动程序的一部分,设备驱动程序是内核代码,是C函数
(2)中断上下文执行代码不能被阻塞
(3)中断处理程序分为上半部和下半部,上半部处理具有较强时间要求的任务,比如通知设备接收到中断。
(1)驱动程序Request_irp()分配一条中断线成功返回0,可以注册中断处理程序并激活响应的中断线,卸载驱动程序时要注销中断处理程序并释放中短线。
(2)request_irq会调用kmallox()可能会睡眠,所以不能在中断上下文和其他不允许阻塞的地方调用该函数。
(3)初始化硬件与注册中断处理程序顺序必须正确,防止硬件未初始化就执行中断处理程序。
(4)释放中断处理程序free_irq()
(5)中断是不可重入的,当一个中断处理程序正在执行时,该中短线在所有处理器上都会被屏蔽掉,防止同一个中断线接受另一个新的中断。不怕处理器接收到新的中断吗?
(1)staitc irqreturnt intr_handler(int irq,void *dev)
(2)内核收到一个中断后,会依次调用中断线上的每一个处理程序,因此中断处理程序必须知道是否应该对这个中断号负责。
(1)中断上下文与任何进程毫无瓜葛,与curent宏也无关,不可以睡眠。
(2)中断处理程序可能是打断了另一中断线上的另一中断处理函数吗?
(3)中断处理程序拥有自己的中断栈,每个处理器一个,大小为一页。
(1)设备发出通过总线向中断处理器发出中断信号,如果是中断线是激活的,中断处理器把中断发给处理器,除非处理器禁止该中断,否则处理器会停下当前正在做的事,关闭中断,然后跳到内核预定义的中断处理程序入口。
(2)入口点检索IRQ号并放在栈中,调用do_IRQ().do_IRQ判断该中断线上是否有该中断处理程序,如果存在则在该栈上运行中断处理程序,否则返回内核运行中断的代码。
(3)中断处理程序处理完之后返回do_IRQ,清理工作并返回到入口点,入口点再调到ret_from_intr().
Linux提供一组接口用来控制中断状态,比如禁止中断或者屏蔽某条中断线。
my_timer.expires = jiffies + delay; /* 定时器超时时的节拍数 */
my_timer.data = 0; /* 给定时器处理函数传入0值 */
my_timer.function = my_function; /* 定时器超时时调用的函数*/
【4】最后激活定时器add_timer(&mytimer)
struct page {
unsigned long flags;//页的状态,是不是脏的,可同时描述 32种状态
atomic_t count;//被引用的次数
atomic_t mapcount;
unsigned long private;
struct address_space *mapping;
pgoff t index;
stroct list head lru;
void *virtual; //虚拟地址}
(2)Linux将内存分为四个区
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)//该函数分配
2order(1<
void * page_address(struct page * page)//把给定的页转换为逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask. unsigned int order)//与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,所以其他页也会紧随其后。
struct page * alloc_page(gfp_t gfp_mask)
unsigned long get_zeroed_page(unsigned int gfp_mask)//返回的页的内容全为0
void __free_pages(struct page *page, unsigned int order)
void * kmalloc(size_t size, gfp_t flags)
(1)行为修饰符、区修饰符及类型标志
void * kmap_automic(struct *page,enum km_type type)//type描述了映射的目的
void kunmap_automic(void *kvaddr,enum km_type)//解除映射
(4)VMA操作
(5)C库是共享的内存区域,0页里全是0
(1)内核通过do_mmap()创建新的线性空间,如果和相邻的空间权限一致,就会合并为一个。
(2)用户空间通过mmap2()调用do_mmap
Linux使用三级页表,页全局目录(PGD)、中间页目录(PMD)、简单也目录(PTE)
VFS为用户提供通用的文件访问接口
(1)VFS抽象层之所以能衔接各种各样的文件系统,是因为定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。而实际的文件系统将接口与VFS保持一致。
(2)ret=write(fd,buf,len).首先给通用的系统调用sys_write处理,sys_write函数找到fd所在的文件系统实际给出哪个写操作然后再执行。
(1)提供了四个抽象概念:文件、目录项、索引节点、安装点。
(2)
【1】文件系统安装在安装点上。
【2】文件是基于字节流的文件,有头有尾有名字。
【3】文件通过目录组织起来,相当于文件夹,目录还可以包含子目录,VFS把目录当成文件看待
【4】文件的元数据存在单独的数据结构里inode,称为索引结点
(1)VFS采用面向对象的设计思想,却是使用C语言编写的,数据结构都使用结构体实现,结构体中拥有指向函数的指针
(2)VFS存在四个主要对象
【1】超级块对象,代表一个具体的文件系统,存储特定文件系统的信息
【2】索引结点,代表一个文件(磁盘上的文件,内存存储不连续),存储内核在操作文件或目录时所需要的全部信息
【3】目录对象,代表一个目录项
目录项对象有三种有效状态:被使用、未被使用和负状态
被使用说明正在被使用,不能删除。未被使用说明没被使用,但是依然保存在缓存中以便在需要时再使用它。一个负状态的目录项没有对应的有效索引接点,但是依然保留以便快速解析以后的路径查询。
【4】文件对象,代表一个由进程打开了的文件(内存中的文件,存储连续)
(3)四个操作对象,每个主要对象对应一个操作对象,表示内核可以对主要对象进行的操作。super_oprations对象,inode_operations对象,dentry_operations对象,file_operations对象
(4)目录项对象有三种有效状态:被使用、未被使用和负状态
被使用说明正在被使用,不能删除。未被使用说明没被使用,但是依然保存在缓存中以便在需要时再使用它。一个负状态的目录项没有对应的有效索引接点,但是依然保留以便快速解析以后的路径查询。
(5)目录项缓存,内核将目录项缓存在目录项缓存中(dcache)
(6)文件对象,代表一个文件。同一个文件可能有多个文件对象,对应的索引结点和目录项是唯一的
(1)file_system_type描述每种文件系统的功能和行为
(2)vfsmount结构体在安装点被创建,代表文件系统的一个实例,也就是安装点。
(1)进程描述符的files域指向file_struct,所有与进程相关的信息都在其中。
(2)进程描述符中的fs域指向fs_struct,包含文件系统和进程相关的信息
(3)进程描述符中的mmt_namespace指向namespace结构体,使每一个进程在系统中都看到唯一的文件系统。
(1)块设备最小的寻址单元是扇区,常见为512字节,块设备无法对比扇区还小的单元进行寻址和操作。
(2)内核块大小要是扇区大小的2的整倍数,但不能超过一页,通常为512B,1KB或4KB
(1)块被调入内存后,存储在缓存区中,一个缓存区对应一个块,缓存区头buffer_header存储相关控制信息,缓存区头在于描述磁盘块与物理内存缓存区之间的映射关系
缓冲区头仅能描述单个缓冲区,当作为所有IO的容器使用时,缓冲区头会促使内核把对大块数据的IO操作分解为多个缓冲区头结构,这样造成了负担和空间浪费。
(1)目前内核中块I/O操作的基本容器由bio结构表示,代表正在现成的以片段(segment)链表形式组织的IO操作,一个片段是一小块连续的缓冲区域。
(2)bi_io_vec指向一个bio_vec结构体数组,表示一个操作所需要的片段集合,每个bio_vec结构是一个形式如(pages,offset,len)的向量
(3)bi_vcnt描述bio_io_vec所指向的bio_vec数组中的向量数目
(4)bi_idx指向数组的当前索引
(5)总而言之,每一个块I/O请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。I/O操作的第一个片段由b_io_vec结构体所指向,其他的片段在其后依次放置,共有bi_vcnt个片段。 当块I/O层开始执行请求、需要使用各个片段时,bi_idx域会不断更新,从而总指向当前片段。
(6)bio结构与缓冲区头的差别
bio结构体代表的是I/O操作,它可以包括内存中的一个或多个页;buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。
缓冲区头负责描述磁盘块到页面的映射。bio结构体不包含任何和缓冲区相关的状态信息——它仅仅是一个矢量数组,描述一个或多个单独块I/O操作的数据片段和相关信息。
(1)块设备将他们挂起的块IO请求放在请求队列中request_queue,高层的代码将请求放进去,设备驱动程序从中获取请求并执行。
(2)请求由request表示,可能操作多个连续的磁盘块,所以可能对应多个bio结构体
(1)内核不会按接受顺序直接给硬盘。而是先执行合并与排序操作。在内核中称提交IO请求的子系统为IO调度程序
(2)调度程序将请求队列中挂起的请求进行合并排序,进行优化。合并,多个请求合并为一个。排序,按照扇区增长方向排序,即电梯调度。
(3)Linus电梯
【1】 如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已经存在的请求合并成一个请求。
【2】如果队列中存在一个驻留时间过长的请求,那么新请求将被插入到队列尾部,以防止其 他旧的请求饥饿发生。
【3】如果队列中以扇区方向为序存在合适的插入位置,那么新的请求将被插入到该位置,保 证队列中的请求是以被访问磁盘物理位置为序进行排列的。
【4】如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部。
(4)最后期限IO调度程序
电梯调度会带来饥饿问题。
【1】所以为每个请求设置一个超时时间,默认情况下读超时为500ms,写为5s。
【2】最后期限调度在合并与排序方法上跟电梯调度一样。
【3】但是最后期限IO调度会按照请求的类型将其插入到额外的队列中。读请求按次序被插入到特定的读FIFO队列中,写请求被插入到特定的写FIFO队列中。
【4】如果FIFO队列头超时了,调度程序便从中提取请求进行服务
(5)预测IO调度程序
最后期限虽然略屌,但是降低了系统的吞吐量。预测IO调度程序妄想保持良好的响应的同时提供良好的全局吞吐量
【1】预测调度程序基于最后期限,增加了预测启发能力
【2】请求提交后并不直接返回处理其他请求,而是会有意空闲片刻,在这空闲期间,任何相邻磁盘位置的请求都会立刻得到处理。
【4】预测IO调度程序更踪并且统计每个应用程序块IO的习惯行为,以便正确预测。
(6)完全公平的排队IO调度程序
【1】请求按照进程组成队列并执行合并排序操作
【2】以时间片轮转调度队列,每个队列中选区请求数,然后进行下一轮调度
(6)空操作的IO调度程序
【1】基本什么都不做,不进行排序。
【2】但是执行合并操作,维护一个近似于FIFO的队列
【3】它之所以啥也不做是因为他打算用在真正的随机访问设备,比如闪存卡。
(1)页高速缓存是由内存中的物理页面组成的,对应物理磁盘上的物理块,能够动态地改变大小。
(2)写缓存有三种策略【1】不缓存【2】自动更新缓存同时更新硬盘文件【3】回写,先把缓存中页面标记为脏,周期性地写回。
(3)缓存回收策略:选择干净的页进行写回,如果没有干净的页则强制地进行回收操作。如果选择写回的页:
【1】最近最少使用LRU
【2】双链策略:Linux实现的是一个修改过得LRU。维护两个链表、活跃链表和非活跃链表,非活跃链表上的页面可以换出。页面从尾部加入,从头部移除。如果活跃链表变得过多而超过了非活跃链表,那么活跃链表的头页面将被重新移回到非活跃链表中。
(1)address_space结构体
当一个文件可以被10个vm_area_struct结构体标识(比如有5个进程,每个调用mmap()映射它两次),那么这个文件只能有一个address_space数据结构——也就是文件可以有多个虚拟地址,但是只能在物理内存有一份。
(2)address_space操作
这些方法指针指向那些为指定缓存对象实现的页I/O操作,这里面readpage()和writepage()两个方法最为重要。
(3)查找页
find_get_page()方法负责完成这个检查动作。一个address_space对象和一个偏移量会传给find_get_page()方法,用于在页高速缓存中搜索需要的数据:
page * find_get_page(mapping , index);
(4)当文件被修改
SetPageDirty(page);
(1)独立的磁盘块IO缓冲也要被存入页高速缓存。
(1)在以下3种情况发生时,脏页被写回磁盘
【1】当空闲页内存低于一个特定的阈值时,内核必须将脏页写回磁盘以释放内存。因为只有干净内存才可以被回收,当内存干净后,内核就可以从缓存清理数据然后收缩缓存最终释放出更多的内存。
【2】当脏页在内存中驻留时间超过一定阈值时,内核必须将超时的脏页写回磁盘。
【3】当用户调用sync()和sync()系统调用时。
(2)一群flusher线程执行三种工作
【1】flusher线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新写回磁盘。当可用物理内存低的时候写回脏数据。当空闲内存比dirty_background_radio还低时,内核便会调用函数flusher_threads唤醒一个或多个线程写回脏页。直到满足以下两个条件{1、已经有指定的最小数目的页被写回,2、空闲内存说已经上升,超过了阈值dirty_background_ratio}
【2】flusher线程会周期性地被唤醒。
(3)膝上型计算机模式
特殊的写回策略,将硬盘转动的机械行为最小化。flusher会根据时机写回数据