1.前言
本文分析的是Linux 2.6.32版的进程模型以及调度器分析。在线查看 源码下载
本文主要讨论以下几个问题:
什么是进程?进程是如何产生的?进程都有那些?
在操作系统中,进程是如何被管理以及它们是怎样被调用的?
2.进程模型
2.1进程的概念
在我的理解中,一个程序就相当于一个进程,程序的启动意味着产生了一个新的进程,程序的关闭也就意味着一个进程的消亡。
那么专业定义应该是:
在计算中,进程是正在执行的计算机程序的一个实例。 它包含程序代码及其当前活动。 根据操作系统(OS),一个进程可能由多个并行执行指令的执行线程组成。
进程不是空洞的,其实我们也是可以看到的
比如在Linux下它是这样的:
在Windows下它是这样的:
2.2进程的产生
了解了什么是进程之后,那么一个进程他是怎么产生的呢?在这里我们要知道的是,在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系,其中有两个特殊的进程,分别是swapper,init。
swapper:
swapper也叫0号进程或者idle进程,他是所有进程的祖先,它是在 Linux 的初始化阶段从无到有的创建的一个内核线程。这个祖先进程使用静态分配的数据结构。
init :
init也叫1号进程它由进程0创建的内核线程执行init() 函数,init() 一次完成内核的初始化。init()调用execve()系统调用装入可执行程序init ,结果 ,init 内核线程变成一个普通的进程,且拥有自己的每个进程内核数据结构。在系统关闭之前,init 进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
在Linux中我们可以通过pstree查看进程树模型:
2.3进程的分类
Linux把进程区分为实时进程和非实时进程(普通进程), 其中非实时进程进一步划分为交互式进程和批处理进程。
类型 |
描述 |
实例 |
交互式进程(interactive process) |
此类进程经常与用户进行交互,,因此需要花费很多时间等待键盘和鼠标操作。当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝 |
shell, 文本编辑程序和图形应用程序 |
批处理进程(batch process) |
此类进程不必与用户交互, 因此经常在后台运行. 因为这样的进程不必很快相应, 因此常受到调度程序的怠慢 |
程序语言的编译程序, 数据库搜索引擎以及科学计算 |
实时进程(real-time process) |
这些进程由很强的调度需要, 这样的进程绝不会被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短 |
视频音频应用程序, 机器人控制程序以及从物理传感器上收集数据的程序 |
2.4进程标识符
知道进程的产生之后,很自然的会联想到在同一个时段系统运行的进程那么多,操作系统要怎么样对这么多的进程进行有效的管理呢?为了有效的管理在Linux中运用了一个task_struct的数据结构对一个进程做了一个完整的描述。
Linux中对进程的描述多达300行代码,定义了非常的成员,大致可分为以下几个部分:
-进程状态(State)
-进程调度信息(Scheduling Information)
-各种标识符(Identifiers)
-进程通信有关信息(IPC)
-时间和定时器信息(Times and Timers)
-进程链接信息(Links)
-文件系统信息(File System)
-虚拟内存信息(Virtual Memory)
-页面管理信息(page)
-对称多处理器(SMP)信息
-和处理器相关的环境(上下文)信息(Processor Specific Context)
-其它信息
所以接下来将对task_struct结构中的一小部分成员进行分析。
2.4.1亲属关系
在前面讲到,进程在Linux中是层次结构的,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。那么在task_struct中它是这么被定义的:
/* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */ struct task_struct *real_parent; /* real parent process */ struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */ /* * children/sibling forms the list of my natural children */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */ struct task_struct *group_leader; /* threadgroup leader */
字段 | 描述 |
real_parent | 指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程 |
parent | 指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同 |
children | 表示链表的头部,链表中的所有元素都是它的子进程 |
sibling | 用于把当前进程插入到兄弟链表中 |
group_leader | 指向其所在进程组的领头进程 |
注:这里的real_parent相当于是亲爹,parent呢是干爹,大部分情况下亲爹干爹是一个人,同ps命令看到的是干爹,什么时候亲爹干爹不一样的?当亲爹死了,但是呢又得有一个父进程,比如1号进程就会被当成父进程。
2.4.2标识符
pid_t pid;
pid_t tgid;
在Linux中,操作系统为了更好地区分内核中每一个进程或者是每一个用户,引入了标识符的概念。每一个进程有进程标识符,用户标识符,组标识符等等。那么在这里我们着重地谈一下进程标识符PID。PID是32位的无符号整数,它被顺序编号,新创建进程的PID通常是前一个进程的PID加1。当一个活跃的程序结束后,再次开启他的pid将被赋予不同于上一次的值。Linux中pid规定最大为32767,当要创建第32768个pid时系统将启用正在闲置的pid。
域名 | 含义 |
Pid | 进程标识符 |
Uid、gid | 用户标识符、组标识符 |
Euid、egid | 有效用户标识符、有效组标识符 |
Suid、sgid | 有效用户标识符、有效组标识符 |
Fsuid、fsgid | 文件系统用户标识符、文件系统组标识符 |
2.4.3进程标记
unsigned int flags; /* per process flags, defined below */
进程标记是反应进程状态的信息,但不是运行状态,用于内核识别进程当前的状态,以便下一步操作,flags成员的可能取值如下:
/* * Per process flags */ #define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */ /* Not implemented yet, only for 486*/ #define PF_STARTING 0x00000002 /* being created */ #define PF_EXITING 0x00000004 /* getting shut down */ #define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */ #define PF_VCPU 0x00000010 /* I'm a virtual CPU */ #define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */ #define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */ #define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ #define PF_DUMPCORE 0x00000200 /* dumped core */ #define PF_SIGNALED 0x00000400 /* killed by a signal */ #define PF_MEMALLOC 0x00000800 /* Allocating memory */ #define PF_FLUSHER 0x00001000 /* responsible for disk writeback */ #define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */ #define PF_FREEZING 0x00004000 /* freeze in progress. do not account to load */ #define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */ #define PF_FROZEN 0x00010000 /* frozen for system suspend */ #define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */ #define PF_KSWAPD 0x00040000 /* I am kswapd */ #define PF_OOM_ORIGIN 0x00080000 /* Allocating much memory to others */ #define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */ #define PF_KTHREAD 0x00200000 /* I am a kernel thread */ #define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */ #define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */ #define PF_SPREAD_PAGE 0x01000000 /* Spread page cache over cpuset */ #define PF_SPREAD_SLAB 0x02000000 /* Spread some slab caches over cpuset */ #define PF_THREAD_BOUND 0x04000000 /* Thread bound to specific cpu */ #define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */ #define PF_MEMPOLICY 0x10000000 /* Non-default NUMA mempolicy */ #define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */ #define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezeable */ #define PF_FREEZER_NOSIG 0x80000000 /* Freezer won't send signals to it */
2.4.4进程的优先级
进程的优先级是一种针对cpu占用的Qos机制,优先级高占有cpu时间高,优先级低的占有cpu时间低,甚至高优先级进程可以抢占低优先级进程占用cpu。
在谈优先级之前不得不先说"nice"值,nice值它是反应一个进程“优先级”状态的值,其取值范围是-20至19,一共40个级别,默认取中间值0。这个值越小,表示进程”优先级”越高,而值越大“优先级”越低,也即负值表示高优先级,正值表示低优先级。也许你一开始你会记混,但其实从名字上我们就很好的理解这个机制,在英语中,如果我们形容一个人nice,那一般说明这个人的人缘比较好。什么样的人人缘好?往往是谦让、有礼貌的人。比如,你跟一个nice的人一起去吃午饭,点了两个一样的饭,先上了一份后,nice的那位一般都会说:“你先吃你先吃!”,但是如果另一份上的很晚,那么这位nice的人就要饿着了。这说明什么?越nice的人越不会去抢占资源,而越不nice的人就越会抢占。这就是nice值大小的含义,nice值越低,说明进程越不nice,抢占cpu的能力就越强,也就意味着优先级就越高。
优先级也分为多种,如下图:
它们在task_struct中是这样定义的:
int prio, static_prio, normal_prio; unsigned int rt_priority;
/*
* static_prio = MAX_RT_PRIO + nice + 20
* rt_priority = 0~MAX_RT_PRIO-1
* prio = 0~MAX_PRIO-1 其中 0~MAX_RT_PRIO-1 (MAX_RT_PRIO 定义为100)属于实时进程范围,MAX_RT_PRIO~MX_PRIO-1属于非实时进程。数值越大,表示进程优先级越小。
* normal_prio 由静态优先级以及调度决策决定
*/
它们的取值定义是:
/* * Priority of a process goes from 0..MAX_PRIO-1, valid RT * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority * values are inverted: lower p->prio value means higher priority. * * The MAX_USER_RT_PRIO value allows the actual maximum * RT priority to be separate from the value exported to * user-space. This allows kernel threads to set their * priority to a value higher than any user task. Note: * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO. */ #define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40) #define DEFAULT_PRIO (MAX_RT_PRIO + 20)
2.5进程状态及转换
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
他可能的取值有:
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32 /* in tsk->state again */ #define TASK_DEAD 64 #define TASK_WAKEKILL 128 #define TASK_WAKING 256 /* Convenience macros for the sake of set_task_state */ #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) /* Convenience macros for the sake of wake_up */ #define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE) #define TASK_ALL (TASK_NORMAL | __TASK_STOPPED | __TASK_TRACED) /* get_task_state() */ #define TASK_REPORT (TASK_RUNNING | TASK_INTERRUPTIBLE | \ TASK_UNINTERRUPTIBLE | __TASK_STOPPED | \ __TASK_TRACED)
2.5.1五个互斥状态
state域能够取5个互为排斥的值。系统中的每个进程都必然处于以上所列进程状态中的一种。
- 运行状态(TASK_RUNNING):表示进程要么正在执行,要么正要准备执行(已经就绪),正在等待cpu时间片的调度。
- 可中断睡眠状态(TASK_INTERRUPTIBLE):进程因为等待一些条件而被挂起(阻塞)而所处的状态。一旦等待的条件成立,进程就会从该状态(阻塞)迅速转化成为就绪状态TASK_RUNNING。
- 不可中断睡眠状态(TASK_UNINTERRUPTIBLE):与TASK_INTERRUPTIBLE类似,除了不能通过接受一个信号来唤醒以外,对于处于TASK_UNINTERRUPIBLE状态的进程,哪怕我们传递一个信号或者有一个外部中断都不能唤醒他们。只有它所等待的资源可用的时候,他才会被唤醒。
- 暂停状态(TASK_STOPPED):进程被暂停执行,当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态。
- 追踪状态(TASK_TRACED):表示进程被debugger等进程监视,进程执行被调试程序所暂停,当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。
2.5.2两个终止状态
- 僵尸状态(EXIT_ZOMBIE):进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息,此时进程成为僵尸进程。
- 死亡状态(EXIT_DEAD):当子进程死亡,父进程调用wait(),进入最终死亡状态。
2.5.3状态转换图
3.进程调度
3.1什么是进程调度
进程调度器,其一般原理是, 按所需分配的CPU计算能力, 向系统中每个进程提供最大的公正性, 而同时又要考虑不同的任务优先级;或者从另外一个角度上说, 他试图确保没有进程被亏待。
3.2调度器的演变
一开始的调度器是复杂度为O(n)O(n)的始调度算法(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的O(1)O(1)调度器
然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在O(1)O(1)调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是CFS调度器Completely Fair Scheduler. 这个也是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器,O(1)O(1)调度器被抛弃了, 其实CFS的发展也是经历了很多阶段,最早期的楼梯算法(SD), 后来逐步对SD算法进行改进出RSDL(Rotating Staircase Deadline Scheduler), 这个算法已经是”完全公平”的雏形了, 直至CFS是最终被内核采纳的调度器, 它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越。
字段 |
版本 |
O(n) |
linux-0.11~2.4 |
O(1) |
linux-2.5 |
CFS |
linux2.6~至今 |
3.3CFS算法(完全公平调度算法)
CFS思路很简单,就是根据各个进程的权重分配运行时间进程的运行时间计算公式为:
分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和 (公式1)
调度周期很好理解,就是将所有处于TASK_RUNNING态进程都调度一遍的时间,差不多相当于O(1)调度算法中运行队列和过期队列切换一次的时间(对O(1)调度算法看得不是很熟,如有错误还望各位大虾指出)。举个例子,比如只有两个进程A, B,权重分别为1和2,调度周期设为30ms,那么分配给A的CPU时间为:30ms * (1/(1+2)) = 10ms;而B的CPU时间为:30ms * (2/(1+2)) = 20ms。那么在这30ms中A将运行10ms,B将运行20ms。
公平怎么体现呢?它们的运行时间并不一样阿?这就需要我们接下来要说到的虚拟运行时间的概念了
3.3.1虚拟运行时间(vruntime)
CFS定义了一种新的模型,它给cfs_rq(cfs的run queue)中的每一个进程安排一个虚拟时钟,vruntime。如果一个进程得以执行,随着时间的增长(也就是一个个tick的到来),其vruntime将不断增大。没有得到执行的进程vruntime不变。而调度器总是选择vruntime跑得最慢的那个进程来执行。这就是所谓的“完全公平”。为了区别不同优先级的进程,优先级高的进程vruntime增长得慢,以至于它可能得到更多的运行机会。
3.3.2调度实体(sched entiy)
sched_entity是调度实体描述,描述可被调度的对象:
struct sched_entity { struct load_weight load; /* for load-balancing */ struct rb_node run_node; struct list_head group_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 last_wakeup; u64 avg_overlap; u64 nr_migrations; u64 start_runtime; u64 avg_wakeup; u64 avg_running; #ifdef CONFIG_SCHEDSTATS u64 wait_start; u64 wait_max; u64 wait_count; u64 wait_sum; u64 iowait_count; u64 iowait_sum; u64 sleep_start; u64 sleep_max; s64 sum_sleep_runtime; u64 block_start; u64 block_max; u64 exec_max; u64 slice_max; u64 nr_migrations_cold; u64 nr_failed_migrations_affine; u64 nr_failed_migrations_running; u64 nr_failed_migrations_hot; u64 nr_forced_migrations; u64 nr_forced2_migrations; u64 nr_wakeups; u64 nr_wakeups_sync; u64 nr_wakeups_migrate; u64 nr_wakeups_local; u64 nr_wakeups_remote; u64 nr_wakeups_affine; u64 nr_wakeups_affine_attempts; u64 nr_wakeups_passive; u64 nr_wakeups_idle; #endif #ifdef CONFIG_FAIR_GROUP_SCHED struct sched_entity *parent; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; #endif };
3.3.3红黑树
与之前的 Linux 调度器不同,它没有将任务维护在运行队列中,CFS 维护了一个以时间为顺序的红黑树(参见下图)。 红黑树 是一个树,具有很多有趣、有用的属性。首先,它是自平衡的,这意味着树上没有路径比任何其他路径长两倍以上。 第二,树上的运行按 O(log n) 时间发生(其中 n 是树中节点的数量)。这意味着您可以快速高效地插入或删除任务。
任务存储在以时间为顺序的红黑树中(由 sched_entity 对象表示),对处理器需求最多的任务 (最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,调度器先选取红黑树最左端的节点调度为下一个以便保持公平性。任务通过将其运行时间添加到虚拟运行时, 说明其占用 CPU 的时间,然后如果可运行,再插回到树中。这样,树左侧的任务就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。 因此,每个可运行的任务都会追赶其他任务以维持整个可运行任务集合的执行平衡。
3.3.4CFS 组调度
CFS 另一个有趣的地方是组调度 概念(在 2.6.24 内核中引入)。组调度是另一种为调度带来公平性的方式,尤其是在处理产生很多其他任务的任务时。 假设一个产生了很多任务的服务器要并行化进入的连接。不是所有任务都会被统一公平对待, CFS 引入了组来处理这种行为。产生任务的服务器进程在整个组中(在一个层次结构中)共享它们的虚拟运行时,而单个任务维持其自己独立的虚拟运行时。这样单个任务会收到与组大致相同的调度时间。
4.体会
对于 Linux 技术而言,惟一不变的就是永恒的变化。CFS 是 2.6 Linux 调度器; 或许今后就会是另一个新的调度器或一套可以被静态或动态调用的调度器。 CFS、RSDL 以及内核背后的进程中还有很多深刻的含义与秘密,等待我们去探索。
5.参考资料
https://blog.csdn.net/yiyeguzhou100/article/details/50994232
https://blog.csdn.net/gatieme/article/details/51702662
https://www.cnblogs.com/yysblog/archive/2012/11/20/2779120.html