一文讲解Linux内核之进程管理(图例解析)

进程的组成:

• 进程控制块( PCB ):进程描述信息、进程控制、管理信息、资源分配清单和 处理机相关信息

• 程序段:能被调度程序调度到 CPU 执⾏的程序代码段

• 数据段:原始数据和执⾏过程中产⽣的数据

为了减少程序在并发执⾏时所付出的时空开销,提供操作系统的并发性能,⽽引⼊ 线程

线程由

• 线程 ID 、

• 程序计数器、

• 寄存器集合

• 堆栈组成

线程是进程中的⼀个实体,是被系统独⽴调度和分派的基本单位,线程⾃⼰不拥有 系统资源,只拥有⼀点在运⾏中必不可少的资源, 但它可与同属于⼀个进程的其他线程共享进程所拥有的全部资源

进程和线程的区别:

1. 调度

2. 拥有资源

3. 并发性

4. 系统开销

5. 地址空间

6. 通信

内核用进程组标识号(pgid)来标识一组相关的进程,这组进程对于某些事件会收到相同的信号

Wait如何处理僵尸进程

1. 取任一僵尸子进程

2. 将子进程的 CPU 使用量加到父进程

3. 释放子进程的进程表项

4. Return (子进程标识号,子进程退出码);

内核需要存储每个进程的PCB信息, linux内核是支持不同体系的的, 但是不同的体系结构可能进程需要存储的信息不尽相同,

这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部门进行分离,用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

一个进程从代码到二进制到运行时的过程图

创建进程和创建线程在用户态和内核态的不同

创建进程和创建线程在用户态和内核态的不同。

在 Linux 内核中,进程和线程都是⽤ task_struct 结构体表示的,区别在于线程的 task_struct 结构体⾥部 分资源是共享了进程已创建的资源,⽐如内存地址空间、代码段、⽂件描述符等,所以 Linux 中的线程也 被称为轻量级进程,因为线程的 task_struct 相⽐进程的 task_struct 承载的 资源⽐较少,因此以「轻」得 名。

进程管理 task_struct 的结构图

进程管理 task_struct 的结构图

structtask_struct{unsignedlongstate;intprio;unsignedlongpolicy;structtask_struct*parent;structlist_headtasks;pid_tpid;…};structthread_info{structtask_struct*task;structexec_domain*exec_domain;___u32flags;___u32status;};

进程:一段可执行的程序代码(text section),打开的文件,挂起的信号,内核内部数据,处理机状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),全局变量的数据段等。

线程:是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。

对Linux而言,进程只不过是一种特殊的线程罢了?

现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存;而虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。

调用fork的进程称为父进程,新产生的进程称为子进程,fork系统调用从内核返回两次,一次回到父进程,一次回到新产生的子进程。

创建新的进程都是为了立即执行新的,不同的程序,而接着调用exec这组函数就可以创建新的地址空间,并把新的程序载入其中。

进程的另一个名字是任务(task)

关于时间,一个进程从创建到终止叫做该进程的生存期,进程在其生存期内使用CPU时间,内核都需要进行记录,进程耗费的时间分为两部分,一部分是用户模式下耗费的时间,一部分是在系统模式下耗费的时间.

cputime_t utime, stime, utimescaled, stimescaled;

cputime_t gtime; cputime_t prev_utime, prev_stime;//记录当前的运行时间(用户态和内核态)

unsigned long nvcsw, nivcsw; //自愿/非自愿上下文切换计数

struct timespec start_time; //进程的开始执行时间

struct timespec real_start_time; //进程真正的开始执行时间

struct signal_struct *signal;//指向进程信号描述符

struct sighand_struct *sighand;//指向进程信号处理程序描述符 sigset_t blocked, real_blocked;//阻塞信号的掩码

sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */

struct sigpending pending;//进程上还需要处理的信号

unsigned long sas_ss_sp;//信号处理程序备用堆栈的地址

size_t sas_ss_size;//信号处理程序的堆栈的地址

static_prio用于保存静态优先级,可以通过nice系统调用来进行修改。 rt_priority用于保存实时优先级。

normal_prio的值取决于静态优先级和调度策略(进程的调度策略有:先来先服务,短作业优先、时间片轮转、高响应比优先等等的调度算法)

prio用于保存动态优先级。

policy表示进程的调度策略,目前主要有以下五种:

#define SCHED_NORMAL 0//按照优先级进行调度(有些地方也说是CFS调度器)

#define SCHED_FIFO 1//先进先出的调度算法

#define SCHED_RR 2//时间片轮转的调度算法

#define SCHED_BATCH 3//用于非交互的处理机消耗型的进程#define SCHED_IDLE 5//系统负载很低时的调度算法

#define SCHED_RESET_ON_FORK 0x40000000

struct signal_struct *signal;//指向进程信号描述符

struct sighand_struct *sighand;//指向进程信号处理程序描述符 sigset_t blocked, real_blocked;//阻塞信号的掩码

sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */

struct sigpending pending;//进程上还需要处理的信号

unsigned long sas_ss_sp;//信号处理程序备用堆栈的地址

size_t sas_ss_size;//信号处理程序的堆栈的地址

/* filesystem information */

struct fs_struct *fs;//文件系统的信息的指针

/* open file information */

struct files_struct *files;//打开文件的信息指针

//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER unsigned long policy;

描述进程状态的字段

进程表中的字段:

• 状态字段,标识进程的状态

• 进程和其 u 区在内存或在二级存储器中的位置,进程大小,以让内核知道应该为该进程分配多少空间

• 若干用户标识符( UID ),决定进程的各种特权

• 若干进程标识号( PID ),说明进程间的关系

• 事件描述符字段

• 调度参数:内核用它们决定若干个进程转换到核心态和用户态的次序

• 软中断信号字段,记录发向一个进程的所有未处理的软中断信号

• 各种计时字段,给出进程执行的时间和内核资源的利用情况。 这些信息用于为进程记账和计算进程调度优先权 。

Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)_CHENG Jian的博客-CSDN博客_exit_state

进程亲缘关系

进程亲缘关系

整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。

从我们之前讲的创建进程的过程,可以看出,任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。

struct task_struct __rcu *real_parent; /* real parent process */

struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */

struct list_head children; /* list of my children */

struct list_head sibling; /* linkage in my parent's children list */

parent 指向其父进程。当它终止时,必须向它的父进程发送信号。children 表示链表的头部。链表中的所有元素都是它的子进程。sibling 用于把当前进程插入到兄弟链表中。

real_parent 和 parent 是一样的,但是也会有另外的情况存在。

例如,bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 parent,bash 是这个进程的 real_parent。

进程数据结构之内核栈

task_struct 的其他成员变量都是和进程管理有关的,内核栈是和进程运行有关系的。

• 在用户态,应用程序进行了至少一次函数调用。 32 位和 64 的传递参数的方式稍有不同, 32 位的就是用函数栈, 64 位的前 6 个参数用寄存器,其他的用函数栈。

• 在内核态, 32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上。

• 在内核态, 32 位和 64 位的内核栈和 task_struct 的关联关系不同。 32 位主要靠 thread_info , 64 位主要靠 Per-CPU 变量 。

对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中

一个是内核态的进程堆栈,

另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。

用户态进程所用的栈,是在进程线性地址空间中;

而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。

进程创建的过程

进程调度的数据结构

一 CPU 上有一个队列,CFS 的队列是一棵红黑树,树的每一个节点都是一个 sched_entity,每个 sched_entity 都属于一个 task_struct,task_struct 里面有指针指向这个进程属于哪个调度类。

在 task_struct 里面,还有这样的成员变量:

const struct sched_class *sched_class;

stop_sched_class 优先级最高的任务会使用这种策略,会中断所有其他线程,且不会被其他任务打断;

dl_sched_class 就对应上面的 deadline 调度策略;

rt_sched_class 就对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的 task_struct->policy 指定;

fair_sched_class 就是普通进程的调度策略;

idle_sched_class 就是空闲进程的调度策略。

进程的调度算法

内核在系统中的每个进程与一个调度优先权联系起来。进程优先权的赋值发生在进程进入睡眠时,或定期地发生在时钟中断处理程序中。

进程在进入睡眠时所获得的优先权是一个固定的值,它取决于该进程当时正在执行的内核算法在时钟中断处理程序中(或在进程从核心态

返回到用户态时)所赋的优先权取决于该进程最近使用了多少CPU事件;如果它最近使用了CPU,将得到较低的优先权;否则,将得到

较高的优先权。系统调用nice允许用户调整计算进程优先权值的公式中的一个参数。

实时进程

普通进程

task_struct中有个成员变量 policy ,称作调度策略,定义如下:

#define SCHED_NORMAL 0

#define SCHED_FIFO 1

#define SCHED_RR 2

#define SCHED_BATCH 3

#define SCHED_IDLE 5

#define SCHED_DEADLINE 6

实时调度策略:

SCHED_FIFO:高优先级的进程可以抢占低优先级的进程,而相同优先级的进程,我们遵循先来先得。

SCHED_RR 轮流调度算法:采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,而高优先级的任务也是可以抢占低优先级的任务。

SCHED_DEADLINE:是按照任务的 deadline 进行调度的。当产生一个调度点的时候,DL 调度器总是选择其 deadline 距离当前时间点最近的那个任务,并调度它执行。

普通调度策略:

SCHED_NORMAL 是普通的进程

SCHED_BATCH 是后台进程,几乎不需要和前端进行交互。

SCHED_IDLE 是特别空闲的时候才跑的进程

对于实时进程,优先级的范围是 0~99;对于普通进程,优先级的范围是 100~139。数值越小,优先级越高。

进程和线程

在 Linux 内核中,进程和线程都是⽤ task_struct 结构体表示的,区别在于线程的 task_struct 结构体⾥部 分资源是共享了进程已创建的资源,⽐如内存地址空间、代码段、⽂件描述符等,所以 Linux 中的线程也 被称为轻量级进程,因为线程的 task_struct 相⽐进程的 task_struct 承载的 资源⽐较少,因此以「轻」得 名。

⼀般来说,没有创建线程的进程,是只有单个执⾏流,它被称为是主线程。如果想让进程处理更多的事 情,可以创建多个线程分别去处理,但不管怎么样,它们对应到内核⾥都是task_struct。

Linux 内核⾥的调度器,调度的对象就是 task_struct ,接下来我们就把这个数据结构统称为任务。

在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越⼩,优先级越⾼:

实时任务,对系统的响应时间要求很⾼,也就是要尽可能快的执⾏实时任务,优先级在 0~99 范围 内的就算实时任务;

普通任务,响应时间没有很⾼的要求,优先级在 100~139 范围内都是普通任务级别

进程状态变迁图

• NULL -> 创建状态:⼀个新进程被创建时的第⼀个状态;

• 创建状态 -> 就绪状态:当进程被创建完成并初始化后,⼀切就绪准备运⾏时,变为就绪状态,这个 过程是很快的;

• 就绪态 -> 运⾏状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运⾏ 该进程;

• 运⾏状态 -> 结束状态:当进程已经运⾏完成或出错时,会被操作系统作结束状态处理;

• 运⾏状态 -> 就绪状态:处于运⾏状态的进程在运⾏过程中,由于分配给它的运⾏时间⽚⽤完,操作 系统会把该进程变为就绪态,接着从就绪态选中另外⼀个进程运⾏;

• 运⾏状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;

• 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

进程的上下文组成:用户地址空间的内容,硬件寄存器的内容以及与该进程相关的内核数据结构组成

更严格的:进程的上下文由 用户及上下文、寄存器上下文、系统级上下文组成

寄存器上下文的组成:

• 程序计数器,指出 CPU 将要执行的下条指令的地址;该地址是内核中或用户存储空间中的虚地址

• 处理机状态寄存器,给出机器与该进程相关联时的硬件状态

• 栈指针,含有栈中下一项的当前地址

• 通用寄存器,进程在运行期间产生的数据

系统级上下文的组成:

• 一个进程的进程表表项

• 一个进程的 u 区,其中含有进程的控制信息

• 本进程区表表项、区表及页表

• 核心栈

• 进程的系统级上下文的动态部分由一些“层”组成

Unix系统的进程状态信息被保存在其进程表和u区中。一个进程的上下文由它的用户级上下文和它的系统级上下文组成。

用户级上下文包括进程的正文、数据、用户栈和共享存储区;系统级上下文由静态部分和动态部分组成。静态部分包括

进程表项、u区和存储器映射信息,动态部分由核心栈和保存的前一层系统上下文层的寄存器组成。动态部分随着进程

执行系统调用、处理中断和做上下文切换而被压入和弹出。进程的用户级上下文被分成若干独立的区,它们是由虚地址

上连续的区域构成,并被看作被保护和共享的可区分实体。

描述进程没有占⽤实际的物理内存空间的情况,这个状态就是挂起状态。 这跟阻塞状态是不⼀样,阻塞状态是等待某个事件的返回。

挂起状态可以分为两种:

• 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;

• 就绪挂起状态:进程在外存(硬盘),但只要进⼊内存,即刻⽴刻运⾏

导致进程挂起的原因不只是因为进程所使⽤的内存空间不在物理内存,还包括如下情况:

• 通过 sleep 让进程间歇性挂起,其⼯作原理是设置⼀个定时器,到期后唤醒进程。

• ⽤户希望挂起⼀个程序的执⾏,⽐如在 Linux 中⽤ Ctrl+Z 挂起进程

内核进程状态

state(状态)可以取的值定义在 include/linux/sched.h 头文件中

/* Used in tsk->state: */#define TASK_RUNNING                    0
#define TASK_INTERRUPTIBLE              1
#define TASK_UNINTERRUPTIBLE            2
#define __TASK_STOPPED                  4
#define __TASK_TRACED                   8
/* Used in tsk->exit_state: */#define EXIT_DEAD                       16
#define EXIT_ZOMBIE                     32
#define EXIT_TRACE                      (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */#define TASK_DEAD                       64
#define TASK_WAKEKILL                   128
#define TASK_WAKING                     256
#define TASK_PARKED                     512
#define TASK_NOLOAD                     1024
#define TASK_NEW                        2048
#define TASK_STATE_MAX                  4096

从定义的数值很容易看出来,state 是通过 bitset 的方式设置的,也就是说,当前是什么状态,哪一位就置一。

TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。

TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。

TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。

一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。

进程退出执行后被设置为僵死状态,直到它的父进程调用wait或waitpid为止

从定义可以看出,TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而 TASK_KILLABLE 相当于这两位都设置了。#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)TASK_STOPPED 是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。

TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。一旦一个进程要结束,先进入的是

EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。

EXIT_DEAD 是进程的最终状态。

EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。

每次从就绪队列选择最先进⼊队列的进程,然后⼀直运⾏,直到进程退出或被阻 塞,才会继续从队列中选择第⼀个进程接着运⾏。 这似乎很公平,但是当⼀个⻓作业先运⾏了,那么后⾯的短作业等待的时间就会很⻓,不利于短作业。 FCFS 对⻓作业有利,适⽤于 CPU 繁忙型作业的系统,⽽不适⽤于 I/O 繁忙型作业的系统

最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运⾏时间最短的进 程来运⾏,这有助于提⾼系统的吞吐量。

这显然对⻓作业不利,很容易造成⼀种极端现象。 ⽐如,⼀个⻓作业在就绪队列等待运⾏,⽽这个就绪队列有⾮常多的短作业,那么就会使得⻓作业不断的 往后推,周转时间变⻓,致使⻓作业⻓期不会被运⾏。

前⾯的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和⻓作业。 那么,⾼响应⽐优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和⻓作业。 每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏

从上⾯的公式,可以发现:

如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应⽐」就越⾼,这样短作业的 进程容易被选中运⾏; 如果两个进程「要求的服务时间」相同时,「等待时间」越⻓,「响应⽐」就越⾼,这就兼顾到了⻓ 作业进程,因为进程的响应⽐可以随时间等待的增加⽽提⾼,当其等待时间⾜够⻓时,其响应⽐便可 以升到很⾼,从⽽获得运⾏的机会

每个进程被分配⼀个时间段,称为时间⽚(Quantum),即允许该进程在该时间段中运⾏

如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外⼀个进 程; 如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换

另外,时间⽚的⻓度就是⼀个很关键的点:

如果时间⽚设得太短会导致过多的进程上下⽂切换,降低了 CPU 效率;

如果设得太⻓⼜可能引起对短作业进程的响应时间变⻓。

将 通常时间⽚设为 20ms~50ms 通常是⼀个⽐较合理的折中值。

从就绪 队列中选择最⾼优先级的进程进⾏运⾏,这称为最⾼优先级(Highest Priority First,HPF)调度算法。

静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化; 动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果 进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程 的优先级。

⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程。 抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏。

多级反馈队列(Multilevel Feedback Queue)调度算法是「时间⽚轮转算法」和「最⾼优先级算法」的 综合和发展。

「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。

「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优 先级⾼的队列

它是如何⼯作的:

• 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越 短;

• 新的进程会被放⼊到第⼀级队列的末尾,按先来先服务的原则排队等待被调度,如果在第⼀级队列规 定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成;

• 当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程进⼊ 较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运 ⾏;

对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理不完, 可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也会更⻓了,所以该算法很好的兼 顾了⻓短作业,同时有较好的响应时间

Deadline 和 Realtime 这两个调度类,都是应⽤于实时任务的,这两个调度类的调度策略合起来共有这三 种,它们的作⽤如下:

SCHED_DEADLINE:是按照 deadline 进⾏调度的,距离当前时间点最近的 deadline 的任务会被优 先调度;

SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,但是优先级更⾼的任务,可以抢占低 优先级的任务,也就是优先级⾼的可以「插队」;

SCHED_RR:对于相同优先级的任务,轮流着运⾏,每个任务都有⼀定的时间⽚,当⽤完时间⽚的任 务会被放到队列尾部,以保证相同优先级任务的公平性,但是⾼优先级的任务依然可以抢占低优先级 的任务

SCHED_NORMAL用于普通进程,通过CFS调度器实现;

SCHED_BATCH用于非交互的处理器消耗型进程;

SCHED_IDLE是在系统负载很低时使用;

SCHED_FIFO(先入先出调度算法)和SCHED_RR(轮流调度算法)都是实时调度策略.

⽽ Fair 调度类是应⽤于普通任务,都是由 CFS 调度器管理的,分为两种调度策略: SCHED_NORMAL:普通任务使⽤的调度策略;

SCHED_BATCH:后台任务的调度策略,不和终端进⾏交互,因此在不影响其他需要交互的任务,可 以适当降低它的优先

对于普通任务来说,公平性最重要,在 Linux ⾥⾯,实现了⼀个基 于 CFS 的调度算法,也就是完全公平调度(Completely Fair Scheduling)。

NICE_0_LOAD=1024,它表示nice为0的进程的权重

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

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

= 调度周期 * 1024 / 所有进程总权重

权重跟进程nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换,nice值越大,权重越低。

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

这个算法的理念是想让分配给每个任务的 CPU 时间是⼀样,于是它为每个任务安排⼀个虚拟运⾏时间 vruntime,如果⼀个任务在运⾏,其运⾏的越久,该任务的 vruntime ⾃然就会越⼤,⽽没有被运⾏的任 务,vruntime 是不会变化的。

在 CFS 算法调度的时候,会优先选择 vruntime 少的任务,以保证每个任务的公平性

注意权重值并不是优先级的值,内核 中会有⼀个 nice 级别与权重值的转换表,nice 级别越低的权重值就越⼤,

不⽤管 NICE_0_LOAD 是什么,你就认为它是⼀个常量,那么在「同样的实际运⾏时间」⾥,⾼权 重任务的 vruntime ⽐低权重任务的 vruntime 少

priority(new) = priority(old) + nice。

,priority 的范围是 0~139,值越低,优先级越⾼,其中前⾯的 0~99 范围是提供给实时任务使⽤的,⽽ nice 值是映射到 100~139,这个范围是提供给普通任务⽤的,因此 nice 值调整的是普通任务的优先级

nice 调整的是普通任务的优先级,所以不管怎么缩⼩ nice 值,任务永远都是普通任务,如果某些任务要求 实时性⽐较⾼,那么你可以考虑改变任务的优先级以及调度策略,使得它变成实时任务

总结:

1、进程的nice值越小, 权重越大, 所分到的运行时间也越多.

2、两个权重不相同的进程, 运行相同的实际时间, 权重大的进程vruntime值增加的要少一些.

3、但是在一个调度周期内, 所有进程的vruntime值都是一样大的, 当每个 进程都把分配给自己的运行时间运行完了时, 它们的vruntime值是一样大的. 所以一个进程的vruntime值越大, 表示进程已运行的时间占调度器分配给它的运行时间的比重也越大.

详细解释,请看:

每个 CPU 都有⾃⼰的运⾏队列(Run Queue, rq),⽤于描述在此 CPU 上所运⾏的所有进程, 其队列包含三个运⾏队列,Deadline 运⾏队列 dl_rq、实时任务运⾏队列 rt_rq 和 CFS 运⾏队列 cfs_rq, 其中 cfs_rq 是⽤红⿊树来描述的,按 vruntime ⼤⼩来排序的,最左侧的叶⼦节点,就是下次会被调度的 任务

调度类是有优先级的,优先级如下:Deadline > Realtime > Fair,Linux先从 dl_rq ⾥选择任务,然后从 rt_rq ⾥选择任 务,最后从 csf_rq ⾥选择任务。因此,实时任务总是会⽐普通任务优先被执⾏

管道:可用于具有亲缘关系的父子进程间的通信,有名管道还可以在无亲缘关系的进程间通信

消息队列:消息的链接表,存放在内核中。一个消息队列由一个标识符来标记。

信号量:用于实现进程间的互斥和同步,而不是用于存储进程间的通信数据

信号:用于通知接收进程某个事件已经发生

共享内存:信号量和共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

线程间通信的方式:

• 临界区

• 互斥量

• 信号量

• 事件:通过通知的方式来保持多线程同步

死锁产⽣的四个必要条件:(其中⼀个条件不成⽴,死锁就不会发⽣)

• 互斥条件:⼀段时间内某资源仅为⼀个进程所占⽤

• 不剥夺条件:进程所获的的资源在未使⽤完毕之前,不能被其他进程强⾏夺⾛,即 只能由获得该资源的进程⾃⼰来释放

• 请求和保持:进程已经保持了⾄少⼀个资源,但⼜提出了新的资源请求,⽽该资源 已被其他进程占有,此时请求进程被阻塞,但对⾃⼰已获得的资源保持不放

• 循环等待条件:存在⼀种进程资源的循环等待链,链中每⼀个进程已获得的资源同 时被链中其他进程所请求。

死锁预防:设置某些限制条件,破坏产⽣死锁的四个必要条件中的⼀个或⼏个,以 防⽌发⽣死锁 “破坏循环等待条件”⼀般采⽤顺序资源分配法,⾸先给系统的资源编号, 规定每个进程必须按编号递增的顺序请求资源,也就是限制了⽤户请求 资源 的顺序。

避免死锁:在资源的动态分配过程中,⽤某种⽅法防⽌系统进⼊不安全状态,从⽽ 避免死锁

死锁的检测和解除:允许进程在运⾏过程中发⽣死锁,通过系统的检测机构及时的 检测出死锁的发⽣,然后采取某些措施解除死锁。

银⾏家算法:最著名的死锁避免算法 为避免系统进⼊不安全状态。

在每次进⾏资源分配时,它⾸先检查系统是否有⾜够 的资源满⾜要求,如果有,则先进⾏分配,并对分配后的状态进⾏安全性检查,如 果新状态安全,则正式分配上述资源,否则拒绝分配上述资源。这样,它保证了系 统始终处于安全状态,从⽽避免死锁现象的发⽣。 如果系统现存的资源可以满⾜它的最⼤需求量则按当前的申请量分配资源,否则就 退出分配。当进程在执⾏中继续申请资源时,先测试该进程已占⽤的 资源数与本次申请的资源数之和是否超过了该进程对资源的最⼤需求量,若超过, 则拒绝分配资源,若没有超过则再测试系统现存的资源能否满⾜该进程尚需的最⼤ 资源量,若能满⾜则按当前的申请量分配资源,否则也要推迟分配。

任何⼀个进程,如果只有主线程,那 pid 是⾃⼰,tgid 是⾃⼰,group_leader 指向 的还是⾃⼰。但是,如果⼀个进程创建了其他线程,那就会有所变化了。线程有⾃ ⼰的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。 有了 tgid,我们就知道 tast_struct 代表的是⼀个进程还是代表⼀个线程了。 ???

那内核通过什么来知道这个线程属于哪个进程呢?答案是task_sruct.tgid

在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用返回的是当前进程的tgid值而不是pid值。(线程是程序运行的最小单位,进程是程序运行的基本单位。)

总结:进程管理,主要将task_struct结构体中的重要字段弄明白,然后重要讲一下CFS(完全公平调度算法)。

你可能感兴趣的:(linux)