linux实现机制(linux打开软件命令)

这篇文章给大家聊聊关于linux实现机制,以及linux打开软件命令对应的知识点,希望对各位有所帮助,不要忘了收藏本站哦。

浅析linux 内核 高精度定时器(hrtimer)实现机制(二)

分析linux内核高精度定时器(hrtimer)的实现机制时,首先介绍的是定时器的迁移过程switch_hrtimer_base。该函数会尝试选择一个新的hrtimer_cpu_base结构体,用于定时器的激活。get_target_base函数被用于挑选新的迁移位置,这个函数的代码与分析低分辨率定时器层时的定时器迁移概念相似。timers_migration_enabled变量在切换到NO_HZ模式时变为True,退出NO_HZ模式时变为False,用于判断是否可以进行迁移。只有在切换到NO_HZ模式且定时器未绑定到特定CPU的情况下,才会进行迁移选择。get_nohz_timer_target函数会判断当前CPU是否处于空闲状态,如果不是,则返回当前CPU编号,如果是空闲,则会找到最近一个忙碌的处理器并返回其编号。所有条件不满足时,会直接返回传入的hrtimer_cpu_base结构体指针。

接下来分析hrtimer_callback_running函数,用于检查要迁移的定时器是否正是当前正在处理的定时器。hrtimer_check_target函数则用于检查定时器的到期时间是否早于要迁移到的CPU上即将到期的时间。如果高分辨率定时器的到期时间比目标CPU上的所有定时器到期时间还要早,并且目标CPU不是当前CPU,那么激活目标CPU会涉及到通知该CPU重新编程定时器,这通常不如直接在当前CPU上激活定时器来得简单。因此,如果迁移操作与实际激活操作没有关系,即使从get_target_base函数获得的base与定时器中指定的base相同,迁移操作也会进行。

在迁移过程中,内核会临时将定时器的hrtimer_clock_base结构体变量设置为全局变量migration_base的指针。这个全局变量仅用于在获得定时器所属CPU的hrtimer_cpu_base结构体变量时,通过判断base变量是否等于migration_base的指针来判断定时器是否正在迁移。这样的设计可以在未正式加锁之前过滤掉很多情况,从而提高速度。

【文章福利】提供Linux内核技术交流群,包含学习书籍、视频资料,前100名可额外获得价值699的内核资料包(含视频教程、电子书、实战项目及代码)。

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

在低精度模式下,高分辨率定时器层通过普通(低分辨率)定时器层驱动。当Tick到来时,其处理函数会调用hrtimer_run_queues函数通知高分辨率定时器层。每次调用该函数时,都会判断是否可以切换到高精度模式。如果可以切换,会调用hrtimer_switch_to_hres完成切换并退出。如果不需要切换,则从时间维护层获得当前时间和各种偏移值,并设置到所有的hrtimer_clock_base结构体中。如果当前时间不早于softirq_expires_next变量的值,表示“软”定时器已到期,需要激活软中断处理程序。在软中断处理程序中,首先调用hrtimer_update_base函数更新当前时间,并在适当时候执行,处理到期的“软”定时器。该处理程序会遍历所有指定类型(“软”或“硬”)的到期定时器,判断定时器的“软”到期时间是否已到,处理到期定时器,并循环取下一个要到期的定时器。最后,会调用hrtimer_reprogram函数对底层定时事件设备进行重编程。

在高精度模式下,周期处理函数hrtimer_interrupt在定时事件设备到期后调用。处理过程包括激活HRTIMER_SOFTIRQ软中断处理程序,处理所有“软”定时器,并对底层定时事件设备进行重编程。重编程确保设备在到期后能正确触发中断,同时避免在一次中断中处理过多定时器,以防止超时。通过查找和设置到期时间时使用“硬”到期时间,而在处理定时器时使用“软”到期时间,内核能尽量减少中断调用,提高性能。

低精度模式切换到高精度模式的hrtimer_switch_to_hres函数通过调用tick_init_highres函数实现切换,将定时事件设备切换到单次触发模式,并设置中断处理函数为hrtimer_interrupt。一旦完成切换,底层定时事件设备将始终工作在单次触发模式。切换成功后,必须找到最近到期的定时器,并用其到期事件对定时事件设备进行重编程,确保设备能正确响应到期。

在高精度模式下,中断处理程序通过直接调用__hrtimer_run_queues函数处理所有“硬”定时器,并激活HRTIMER_SOFTIRQ软中断处理程序来处理所有“软”定时器。在高精度模式下,底层定时事件设备始终处于单次触发模式,因此在到期后必须进行重编程。如果编程失败,重试三次后,适当延迟到期事件后再次尝试编程,并强制执行。

使用实例展示了高精度定时器在实际应用中的精度,时间戳显示其定时精度可达到ms级别。

Linux系统中的进程调度介绍

操作系统要实现多进程,进程调度必不可少。

有人说,进程调度是操作系统中最为重要的一个部分。我觉得这种说法说得太绝对了一点,就像很多人动辄就说"某某函数比某某函数效率高XX倍"一样,脱离了实际环境,这些结论是比较片面的。

而进程调度究竟有多重要呢?首先,我们需要明确一点:进程调度是对TASK_RUNNING状态的进程进行调度(参见《linux进程状态浅析》)。如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系。

所以,如果你的系统负载非常低,盼星星盼月亮才出现一个可执行状态的进程。那么进程调度也就不会太重要。哪个进程可执行,就让它执行去,没有什么需要多考虑的。

反之,如果系统负载非常高,时时刻刻都有N多个进程处于可执行状态,等待被调度运行。那么进程调度程序为了协调这N个进程的执行,必定得做很多工作。协调得不好,系统的性能就会大打折扣。这个时候,进程调度就是非常重要的。

尽管我们平常接触的很多计算机(如桌面系统、网络服务器、等)负载都比较低,但是linux作为一个通用操作系统,不能假设系统负载低,必须为应付高负载下的进程调度做精心的设计。

当然,这些设计对于低负载(且没有什么实时性要求)的环境,没多大用。极端情况下,如果CPU的负载始终保持0或1(永远都只有一个进程或没有进程需要在CPU上运行),那么这些设计基本上都是徒劳的。

优先级

现在的操作系统为了协调多个进程的“同时”运行,最基本的手段就是给进程定义优先级。定义了进程的优先级,如果有多个进程同时处于可执行状态,那么谁优先级高谁就去执行,没有什么好纠结的了。

那么,进程的优先级该如何确定呢?有两种方式:由用户程序指定、由内核的调度程序动态调整。(下面会说到)

linux内核将进程分成两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同。

实时进程的调度

实时,原本的涵义是“给定的操作一定要在确定的时间内完成”。重点并不在于操作一定要处理得多快,而是时间要可控(在最坏情况下也不能突破给定的时间)。

这样的“实时”称为“硬实时”,多用于很精密的系统之中(比如什么火箭、导弹之类的)。一般来说,硬实时的系统是相对比较专用的。

像linux这样的通用操作系统显然没法满足这样的要求,中断处理、虚拟内存、等机制的存在给处理时间带来了很大的不确定性。硬件的cache、磁盘寻道、总线争用、也会带来不确定性。

比如考虑“i++;”这么一句C代码。绝大多数情况下,它执行得很快。但是极端情况下还是有这样的可能:

1、i的内存空间未分配,CPU触发缺页异常。而linux在缺页异常的处理代码中试图分配内存时,又可能由于系统内存紧缺而分配失败,导致进程进入睡眠;

2、代码执行过程中硬件产生中断,linux进入中断处理程序而搁置当前进程。而中断处理程序的处理过程中又可能发生新的硬件中断,中断永远嵌套不止……;

等等……

而像linux这样号称实现了“实时”的通用操作系统,其实只是实现了“软实时”,即尽可能地满足进程的实时需求。

如果一个进程有实时需求(它是一个实时进程),则只要它是可执行状态的,内核就一直让它执行,以尽可能地满足它对CPU的需要,直到它完成所需要做的事情,然后睡眠或退出(变为非可执行状态)。

而如果有多个实时进程都处于可执行状态,则内核会先满足优先级最高的实时进程对CPU的需要,直到它变为非可执行状态。

于是,只要高优先级的实时进程一直处于可执行状态,低优先级的实时进程就一直不能得到CPU;只要一直有实时进程处于可执行状态,普通进程就一直不能得到CPU。

那么,如果多个相同优先级的实时进程都处于可执行状态呢?这时就有两种调度策略可供选择:

1、SCHED_FIFO:先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。在这种策略下,先来的进程可以执行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程;

2、SCHED_RR:轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;

强调一下,这两种调度策略以及sched_yield系统调用都仅仅针对于相同优先级的多个实时进程同时处于可执行状态的情况。

在linux下,用户程序可以通过sched_setscheduler系统调用来设置进程的调度策略以及相关调度参数;sched_setparam系统调用则只用于设置调度参数。这两个系统调用要求用户进程具有设置进程优先级的能力(CAP_SYS_NICE,一般来说需要root权限)(参阅capability相关的文章)。

通过将进程的策略设为SCHED_FIFO或SCHED_RR,使得进程变为实时进程。而进程的优先级则是通过以上两个系统调用在设置调度参数时指定的。

对于实时进程,内核不会试图调整其优先级。因为进程实时与否?有多实时?这些问题都是跟用户程序的应用场景相关,只有用户能够回答,内核不能臆断。

综上所述,实时进程的调度是非常简单的。进程的优先级和调度策略都由用户定死了,内核只需要总是选择优先级最高的实时进程来调度执行即可。唯一稍微麻烦一点的只是在选择具有相同优先级的实时进程时,要考虑两种调度策略。

普通进程的调度

实时进程调度的中心思想是,让处于可执行状态的最高优先级的实时进程尽可能地占有CPU,因为它有实时需求;而普通进程则被认为是没有实时需求的进程,于是调度程序力图让各个处于可执行状态的普通进程和平共处地分享CPU,从而让用户觉得这些进程是同时运行的。

与实时进程相比,普通进程的调度要复杂得多。内核需要考虑两件麻烦事:

一、动态调整进程的优先级

按进程的行为特征,可以将进程分为“交互式进程”和“批处理进程”:

交互式进程(如桌面程序、服务器、等)主要的任务是与外界交互。这样的进程应该具有较高的优先级,它们总是睡眠等待外界的输入。而在输入到来,内核将其唤醒时,它们又应该很快被调度执行,以做出响应。比如一个桌面程序,如果鼠标点击后半秒种还没反应,用户就会感觉系统“卡”了;

批处理进程(如编译程序)主要的任务是做持续的运算,因而它们会持续处于可执行状态。这样的进程一般不需要高优先级,比如编译程序多运行了几秒种,用户多半不会太在意;

如果用户能够明确知道进程应该有怎样的优先级,可以通过nice、setpriority系统调用来对优先级进行设置。(如果要提高进程的优先级,要求用户进程具有CAP_SYS_NICE能力。)

然而应用程序未必就像桌面程序、编译程序这样典型。程序的行为可能五花八门,可能一会儿像交互式进程,一会儿又像批处理进程。以致于用户难以给它设置一个合适的优先级。

再者,即使用户明确知道一个进程是交互式还是批处理,也多半碍于权限或因为偷懒而不去设置进程的优先级。(你又是否为某个程序设置过优先级呢?)

于是,最终,区分交互式进程和批处理进程的重任就落到了内核的调度程序上。

调度程序关注进程近一段时间内的表现(主要是检查其睡眠时间和运行时间),根据一些经验性的公式,判断它现在是交互式的还是批处理的?程度如何?最后决定给它的优先级做一定的调整。

进程的优先级被动态调整后,就出现了两个优先级:

1、用户程序设置的优先级(如果未设置,则使用默认值),称为静态优先级。这是进程优先级的基准,在进程执行的过程中往往是不改变的;

2、优先级动态调整后,实际生效的优先级。这个值是可能时时刻刻都在变化的;

二、调度的公平性

在支持多进程的系统中,理想情况下,各个进程应该是根据其优先级公平地占有CPU。而不会出现“谁运气好谁占得多”这样的不可控的情况。

linux实现公平调度基本上是两种思路:

1、给处于可执行状态的进程分配时间片(按照优先级),用完时间片的进程被放到“过期队列”中。等可执行状态的进程都过期了,再重新分配时间片;

2、动态调整进程的优先级。随着进程在CPU上运行,其优先级被不断调低,以便其他优先级较低的进程得到运行机会;

后一种方式有更小的调度粒度,并且将“公平性”与“动态调整优先级”两件事情合而为一,大大简化了内核调度程序的代码。因此,这种方式也成为内核调度程序的新宠。

强调一下,以上两点都是仅针对普通进程的。而对于实时进程,内核既不能自作多情地去动态调整优先级,也没有什么公平性可言。

普通进程具体的调度算法非常复杂,并且随linux内核版本的演变也在不断更替(不仅仅是简单的调整),所以本文就不继续深入了。

调度程序的效率

“优先级”明确了哪个进程应该被调度执行,而调度程序还必须要关心效率问题。调度程序跟内核中的很多过程一样会频繁被执行,如果效率不济就会浪费很多CPU时间,导致系统性能下降。

在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序需要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);

在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每一个链表代表一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出位于链表头的进程即可。这样就大大提高了调度程序的效率,复杂度为O(1);

在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(可以想象成平衡二叉树)中。每次调度,调度程序需要从树中找出优先级最高的进程。复杂度为O(logN)。

那么,为什么从linux 2.6早期到近期linux 2.6版本,调度程序选择进程时的复杂度反而增加了呢?

这是因为,与此同时,调度程序对公平性的实现从上面提到的第一种思路改变为第二种思路(通过动态调整优先级实现)。而O(1)的算法是基于一组数目不大的链表来实现的,按我的理解,这使得优先级的取值范围很小(区分度很低),不能满足公平性的需求。而使用红黑树则对优先级的取值没有限制(可以用32位、64位、或更多位来表示优先级的值),并且O(logN)的复杂度也还是很高效的。

调度触发的时机

调度的触发主要有如下几种情况:

1、当前进程(正在CPU上运行的进程)状态变为非可执行状态。

进程执行系统调用主动变为非可执行状态。比如执行nanosleep进入睡眠、执行exit退出、等等;

进程请求的资源得不到满足而被迫进入睡眠状态。比如执行read系统调用时,磁盘高速缓存里没有所需要的数据,从而睡眠等待磁盘IO;

进程响应信号而变为非可执行状态。比如响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;

2、抢占。进程运行时,非预期地被剥夺CPU的使用权。这又分两种情况:进程用完了时间片、或出现了优先级更高的进程。

优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒,或因为释放互斥对象(如释放锁)而被唤醒;

内核在响应时钟中断的过程中,发现当前进程的时间片用完;

内核在响应中断的过程中,发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒。比如CPU收到网卡中断,内核处理该中断,发现某个socket可读,于是唤醒正在等待读这个socket的进程;再比如内核在处理时钟中断的过程中,触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程。

所有任务都采用linux分时调度策略时:

1,创建任务指定采用分时调度策略,并指定优先级nice值(-20~19)。

2,将根据每个任务的nice值确定在cpu上的执行时间(counter)。

3,如果没有等待资源,则将该任务加入到就绪队列中。

4,调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算权值(counter+20-nice)结果,选择计算结果最大的一个去运行,当这个时间片用完后(counter减至0)或者主动放弃cpu时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃cpu)中。

5,此时调度程序重复上面计算过程,转到第4步。

6,当调度程序发现所有就绪任务计算所得的权值都为不大于0时,重复第2步。

所有任务都采用FIFO时:

1,创建进程时指定采用FIFO,并设置实时优先级rt_priority(1-99)。

2,如果没有等待资源,则将该任务加入到就绪队列中。

3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu,该FIFO任务将一直占有cpu直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。

4,调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务堆栈中保存当前cpu寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到cpu,此时高优先级的任务开始运行。重复第3步。

5,如果当前任务因等待资源而主动放弃cpu使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。

所有任务都采用RR调度策略时:

1,创建任务时指定调度参数为RR,并设置任务的实时优先级和nice值(nice值将会转换为该任务的时间片的长度)。

2,如果没有等待资源,则将该任务加入到就绪队列中。

3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu。

4,如果就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤3。

5,当前任务由于等待资源而主动退出cpu,则其加入等待队列中。重复步骤3。

系统中既有分时调度,又有时间片轮转调度和先进先出调度:

1,RR调度和FIFO调度的进程属于实时进程,以分时调度的进程是非实时进程。

2,当实时进程准备就绪后,如果当前cpu正在运行非实时进程,则实时进程立即抢占非实时进程。

3,RR进程和FIFO进程都采用实时优先级做为调度的权值标准,RR是FIFO的一个延伸。FIFO时,如果两个进程的优先级一样,则这两个优先级一样的进程具体执行哪一个是由其在队列中的未知决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务的调度策略都设为RR,则保证了这两个任务可以循环执行,保证了公平。

Ingo Molnar-实时补丁

为了能并入主流内核,Ingo Molnar的实时补丁也采用了非常灵活的策略,它支持四种抢占模式:

1.No Forced Preemption(Server),这种模式等同于没有使能抢占选项的标准内核,主要适用于科学计算等服务器环境。

2.Voluntary Kernel Preemption(Desktop),这种模式使能了自愿抢占,但仍然失效抢占内核选项,它通过增加抢占点缩减了抢占延迟,因此适用于一些需要较好的响应性的环境,如桌面环境,当然这种好的响应性是以牺牲一些吞吐率为代价的。

3.Preemptible Kernel(Low-Latency Desktop),这种模式既包含了自愿抢占,又使能了可抢占内核选项,因此有很好的响应延迟,实际上在一定程度上已经达到了软实时性。它主要适用于桌面和一些嵌入式系统,但是吞吐率比模式2更低。

4.Complete Preemption(Real-Time),这种模式使能了所有实时功能,因此完全能够满足软实时需求,它适用于延迟要求为100微秒或稍低的实时系统。

实现实时是以牺牲系统的吞吐率为代价的,因此实时性越好,系统吞吐率就越低。

linux为什么主要采用分页机制来实现虚拟存储管理

1分页机制

在虚拟内存中,页表是个映射表的概念,即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address).

很显然,这个页表是需要常驻内存的东西,以应对频繁的查询映射需要(实际上,现代支持VM的处理器都有一个叫TLB的硬件级页表缓存部件,本文不讨论)。

1.1为什么使用多级页表来完成映射

但是为什么要使用多级页表来完成映射呢?

用来将虚拟地址映射到物理地址的数据结构称为页表,实现两个地址空间的关联最容易的方式是使用数组,对虚拟地址空间中的每一页,都分配一个数组项.该数组指向与之关联的页帧,但这会引发一个问题,例如, IA-32体系结构使用4KB大小的页,在虚拟地址空间为4GB的前提下,则需要包含100万项的页表.这个问题在64位体系结构下,情况会更加糟糕.而每个进程都需要自身的页表,这回导致系统中大量的所有内存都用来保存页表.

设想一个典型的32位的X86系统,它的虚拟内存用户空间(user space)大小为3G,并且典型的一个页表项(page table entry, pte)大小为4 bytes,每一个页(page)大小为4k bytes。那么这3G空间一共有(3G/4k=)786432个页面,每个页面需要一个pte来保存映射信息,这样一共需要786432个pte!

如何存储这些信息呢?一个直观的做法是用数组来存储,这样每个页能存储(4k/4=)1K个,这样一共需要(786432/1k=)768个连续的物理页面(phsical page)。而且,这只是一个进程,如果要存放所有N个进程,这个数目还要乘上N!这是个巨大的数目,哪怕内存能提供这样数量的空间,要找到连续768个连续的物理页面在系统运行一段时间后碎片化的情况下,也是不现实的。

为减少页表的大小并容许忽略不需要的区域,计算机体系结构的涉及会将虚拟地址分成多个部分.同时虚拟地址空间的大部分们区域都没有使用,因而页没有关联到页帧,那么就可以使用功能相同但内存用量少的多的模型:多级页表

但是新的问题来了,到底采用几级页表合适呢?

1.2 32位系统中2级页表

从80386开始, intel处理器的分页单元是4KB的页, 32位的地址空间被分为3部分

单元

描述

页目录表Directory最高10位

页中间表Table中间10位

页内偏移最低12位

即页表被划分为页目录表Directory和页中间表Tabl两个部分

此种情况下,线性地址的转换分为两步完成.

第一步,基于两级转换表(页目录表和页中间表),最终查找到地址所在的页帧

第二步,基于偏移,在所在的页帧中查找到对应偏移的物理地址

使用这种二级页表可以有效的减少每个进程页表所需的RAM的数量.如果使用简单的一级页表,那将需要高达220个页表,假设每项4B,则共需要占用220?4B=4MB的RAM来表示每个进程的页表.当然我们并不需要映射所有的线性地址空间(32位机器上线性地址空间为4GB),内核通常只为进程实际使用的那些虚拟内存区请求页表来减少内存使用量.

1.3 64位系统中的分页

正常来说,对于32位的系统两级页表已经足够了,但是对于64位系统的计算机,这远远不够.

首先假设一个大小为4KB的标准页.因为1KB覆盖210个地址的范围, 4KB覆盖212个地址,所以offset字段需要12位.

这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory.如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB,即可达到256TB的寻址空间).剩下的48-12=36位被分配给Table和Directory字段.即使我们现在决定位两个字段各预留18位,那么每个进程的页目录和页表都包含218个项,即超过256000个项.

基于这个原因,所有64位处理器的硬件分页系统都使用了额外的分页级别.使用的级别取决于处理器的类型

平台名称

页大小

寻址所使用的位数

分页级别数

线性地址分级

alpha 8KB 43 3 10+ 10+ 10+ 13

ia64 4KB 39 3 9+ 9+ 9+ 12

ppc64 4KB 41 3 10+ 10+ 9+ 12

sh64 4KB 41 3 10+ 10+ 9+ 12

x86_64 4KB 48 4 9+ 9+ 9+ 9+ 12

阅读剩余
THE END