进程及进程主动调度

进程

基本概念

  • 进程 = 程序 + 执行,进程可以说是某种类型的活动,它包含程序、输入、输出及状态等

  • 线程是操作系统调度(资源及时间片)的最小单元。一个进程可以拥有多个线程。

  • 进程和线程的区别在于进程拥有独立的资源空间,即进程地址空间,而同一进程的线程则共享进程地址空间。在Linux内核中,对于进程及线程都使用相同的数据结构task_struct

  • 进程是资源分配的最小单位,而线程是CPU调度的的最小单位。

  • 进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,没有用户虚拟地址空间的进程称为内核线程。

在这里插入图片描述

task_struct

Linux内核使用task_struct结构来抽象,该结构包含了进程的各类信息及所拥有的资源,是进程管理中最重要的数据结构。task_struct结构很复杂,定义在include/linux/sched.h中,深入了解task_struct点击

struct task_struct {
    
    /* 进程运行的状态 */
    volatile long			state;

    /* 调度优先级相关,策略相关 */
	int				prio;				//普通进程的调度优先级
	int				static_prio;		//普通进程的静态优先级
	int				normal_prio;		//普通进程的动态优先级
	unsigned int			rt_priority;//普通进程的实时优先级
    unsigned int			policy;		//进程的调度策略
    
    /* 调度类,调度实体相关,任务组相关等 */
    const struct sched_class	*sched_class;	//进程所属调度器类
	struct sched_entity		se;					//进程的普通调度实体
	struct sched_rt_entity		rt;				//进程的实时调度实体

	struct sched_dl_entity		dl;				//进程的限时调度实体
    
    /* 进程之间的关系相关 */
	struct task_struct __rcu	*real_parent;
	struct task_struct __rcu	*parent;
	struct list_head		children;
	struct list_head		sibling;
	struct task_struct		*group_leader;
    
    cpumask_t cpus_mask;  						//描述进程得CPU亲和性得位掩码,标识进程可以在哪些CPU上运行

}

进程运行状态

内核中主要的状态字段主要有state和exit_state,定义如下:

/*活动时状态:exit_state = 0,   state取值如下:*/

#define TASK_RUNNING			0x0000
#define TASK_INTERRUPTIBLE		0x0001
#define TASK_UNINTERRUPTIBLE		0x0002
#define TASK_PARKED			0x0040
#define TASK_WAKEKILL			0x0100
#define TASK_WAKING			0x0200
#define TASK_NOLOAD			0x0400
#define TASK_NEW			0x0800
#define TASK_STATE_MAX			0x1000
#define TASK_KILLABLE			(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED			(TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED			(TASK_WAKEKILL | __TASK_TRACED)
#define TASK_IDLE			(TASK_UNINTERRUPTIBLE | TASK_NOLOAD)

/*死亡后状态 state=TASK_DEAD  exit_state取值如下:*/
#define EXIT_DEAD			0x0010
#define EXIT_ZOMBIE			0x0020
#define EXIT_TRACE			(EXIT_ZOMBIE | EXIT_DEAD)

进程调度

所谓调度,就是按照某种调度的算法,从进程的就绪队列中选取进程分配CPU,主要是协调对CPU等的资源使用。进程调度的目标是最大限度利用CPU时间。进程调度的本质是让进程更好地分时复用处理器的资源。因此,可以说,进程调度是包括两个部分的,一个是调度策略,另一个是进程的切换。

基本概念

  • 时间片:指的是分时复用过程中每个进程允许持续运行的最大时间配额单位。
  • 优先级:指的是在所有进程中,谁更有资格优先获得CPU资源,优先运行。
    • static_prio:一般在进程创建的时候确定,取值范围100-139,也可以通过nice或sched_setscheduler()来改变,值越小,优先级越高。
    • nomal_prio:对普通进程来说,nomal_prio初始值 = 静态优先级,对于实时进程来说,值得范围在99 - rt_priority。nomal_prio得取值范围为0-139,值随着进程的运行不断调整改变,值越小,优先级越高。调度策略选择进程时考虑的是动态优先级
    • rt_priority:实时进程的静态实时优先级,取值范围0-99,值越大,优先级越高。
  • 抢占调度:指的是高优先级进程是否可以强行夺取低优先级进程的处理器资源。如果可以,则为抢占调度。

scheduler 调度器

内核默认提供了5个调度器,Linux内核使用struct sched_class来对调度器进行抽象

  1. Stop调度器, stop_sched_class:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占,同一时刻同一逻辑CPU仅有一个本类内核线程

  2. Deadline调度器, dl_sched_class:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;

  3. RT调度器, rt_sched_class:实时调度器,为每个优先级维护一个队列;

  4. CFS调度器, cfs_sched_class:完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念;

  5. IDLE-Task调度器, idle_sched_class:空闲调度器,每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程;

Linux内核提供了一些调度策略供用户程序来选择调度器,其中Stop调度器IDLE-Task调度器,仅由内核使用,用户无法进行选择:

  • SCHED_DEADLINE:限期进程调度策略,使task选择Deadline调度器来调度运行;
  • SCHED_RR:实时进程调度策略,时间片轮转,进程用完时间片后加入优先级对应运行队列的尾部,把CPU让给同优先级的其他进程;
  • SCHED_FIFO:实时进程调度策略,先进先出调度没有时间片,没有更高优先级的情况下,只能等待主动让出CPU;
  • SCHED_NORMAL:普通进程调度策略,使task选择CFS调度器来调度运行;
  • SCHED_BATCH:普通进程调度策略,批量处理,使task选择CFS调度器来调度运行;
  • SCHED_IDLE:普通进程调度策略,使task以最低优先级选择CFS调度器来调度运行;

sched_entity调度实体

调度实体是CFS调度器中得一个重要得数据结构,内容如下:

struct sched_entity {

    struct load_weight		load;               // 负载权重,用于负载均衡
    unsigned long			runnable_weight;    // 可运行的权重
    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				nr_migrations;      // 迁移次数

    struct sched_statistics		statistics;         // 调度统计信息
};

当每个调度实体获得CPU资源开始运行时,exec_start更新为当前时间,记录最近一次调度实体获得CPU得起始运行时间

runqueue 运行队列

在这里插入图片描述

  • 每个CPU都有一个运行队列,每个调度器都作用于运行队列;

  • 分配给CPU的task,作为调度实体加入到运行队列中;

  • task首次运行时,如果可能,尽量将它加入到父task所在的运行队列中(分配给相同的CPU,缓存affinity会更高,性能会有改善);

Linux内核使用struct rq结构来描述运行队列,关键字段如下:

/*
 * This is the main, per-CPU runqueue data structure.
 *
 * Locking rule: those places that want to lock multiple runqueues
 * (such as the load balancing or the thread migration code), lock
 * acquire operations must be ordered by ascending &runqueue.
 */
struct rq {
	/* runqueue lock: */
	raw_spinlock_t lock;

	/*
	 * nr_running and cpu_load should be in the same cacheline because
	 * remote CPUs use both these fields when doing load calculation.
	 */
	unsigned int nr_running;
    
    /* 三个调度队列:CFS调度,RT调度,DL调度 */
	struct cfs_rq cfs;
	struct rt_rq rt;
	struct dl_rq dl;

    /* stop指向迁移内核线程, idle指向空闲内核线程 */
    struct task_struct *curr, *idle, *stop;
    
    /* ... */
}    

主动调度

调度器得主函数是schedule()/__schedule()

schedule

asmlinkage __visible void __sched schedule(void)
{
	// 获取当前运行的任务的task_struct结构体指针
	struct task_struct *tsk = current;

	// 提交工作到内核
	sched_submit_work(tsk);

	// 循环执行调度
	do {
		// 禁用抢占,防止递归
		preempt_disable();
		// 执行真正的调度函数,false表示是主动调度
		__schedule(false);
		// 重新启用抢占,但不重新调度
		sched_preempt_enable_no_resched();
	// 打开抢占,并检查是否需要重新调度,但不立即重调度
	} while (need_resched());
	// 更新工作队列中的任务
	sched_update_worker(tsk);
}

sched_submit_work将工作项提交给内核工作队列,由于当前进程执行schedule后,有可能会进入休眠,所以在休眠之前需要处理当前进程的工作,防止发生死锁。为了确保当前进程在进入休眠状态之前不会被抢占,禁止内核抢占,让进程处于不中断情况下,如果有合适进程那么CPU会进行选择并且执行,调用__schedule()函数进行调度。启用内核抢占但不会立即进行进程调度。

__schedule精简代码

static void __sched notrace __schedule(bool preempt)
{
	cpu = smp_processor_id();           // 获取当前CPU的ID
	rq = cpu_rq(cpu);                   // 获取当前CPU的运行队列

	prev = rq->curr;                    // 获取当前运行的任务,即切换前的进程

	update_rq_clock(rq);                // 更新运行队列的时钟

	switch_count = &prev->nivcsw;       // 获取前一个任务的非虚拟上下文切换计数器

	// 如果不是由于抢占并且前一个任务的状态为非运行状态,说明本次调度为主动调度
	if (!preempt && prev->state) {
		// 如果前一个任务有信号待处理
		if (signal_pending_state(prev->state, prev)) {
			prev->state = TASK_RUNNING; // 设置prev进程的状态为运行
		} else {
			// 否则,从运行队列中移除prev进程
			deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
		}
	}

	// 从运行队列中选择下一个要运行的任务
	next = pick_next_task(rq, prev, &rf);
	clear_tsk_need_resched(prev);        // 清除前一个任务的重新调度标志
	clear_preempt_need_resched();        // 清除抢占的重新调度标志

	// 如果要切换的两个任务不同
	if (likely(prev != next)) {
		rq->nr_switches++;               // 增加运行队列的上下文切换计数器
		RCU_INIT_POINTER(rq->curr, next);// 更新运行队列的当前任务指针
		++*switch_count;                 // 增加前一个任务的上下文切换计数器
		trace_sched_switch(preempt, prev, next); // 跟踪调度切换事件
		rq = context_switch(rq, prev, next, &rf); // 执行上下文切换
	} 

	balance_callback(rq);                // 调用平衡回调函数
}

在这里插入图片描述

上图是该函数的一个大概树形图。该函数的核心函数分别为:pick_next_task()和context_switch(),__schedule的大概流程是:

  • 首先获取当前cpu、该cpu的运行队列、当前进程等一些信息
  • 然后更新当前cpu运行队列的时钟
  • 接着判断该调度是否是主动调度
    • 被动调度:执行pick_next_task()来选择运行队列中最高优先级的任务
    • 主动调度:将当前进程从运行队列中移除,并执行pick_next_task()
  • 判断选择的下一个任务和前一个任务是否相同,不同的话则执行context_switch()来切换进程

下面分析一下几个重要的函数,来理解__schedule函数

pick_next_task

该函数位于kernel/sched/core.c文件中,其功能是在运行队列当中挑选一个最高优先级的任务。该函数的参数为本地运行队列rq,以及当前的进程prev,和rq的标志位

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  /*检查前一个任务是否属于idle或cfs完全公平调度类,如果前一个是fair调度类,则该cpu整个运行队列中的进程数量 = cfs就绪队列中的进程数量,说明该CPU就绪队列中只有普通进程没有其它调度类进程*/
    if (likely((prev->sched_class == &idle_sched_class ||
                prev->sched_class == &fair_sched_class) &&
               rq->nr_running == rq->cfs.h_nr_running)) {

        // 使用cfs调度类的操作集进行pick_next_task
        p = fair_sched_class.pick_next_task(rq, prev, rf);

        // 如果pick_next_task返回RETRY_TASK,重新尝试任务选择
        if (unlikely(p == RETRY_TASK))
            goto restart; // 如果任务选择失败,重新开始循环。

        // 如果没有从cfs调度类选择到任务,尝试从idle调度类选择。
        p = idle_sched_class.pick_next_task(rq, prev, rf);
        // 使用idle调度类的操作集进行pick_next_task

        return p; 
    }

restart:
    // 释放前一个任务。
    put_prev_task(rq, prev);

    // 遍历所有调度类以查找下一个任务。
    for_each_class(class) {
        p = class->pick_next_task(rq, NULL, NULL);
        if (p)
            return p; // 如果找到选定的任务,则返回。
    }
}

在这里插入图片描述

该函数大概功能是:先判断prev进程所属调度类,如果是idle或fair调度类,并且cpu整个运行队列中的进程数量 = cfs就绪队列中的进程数量,则执行该调度类的pick_next_task()回调函数,如果没有找到下一个task,则执行put_prev_task()回调函数,试图停止对prev进程的引用。再按照优先级遍历每个调度类,并调用他们的pick_next_task()回调函数。

context_switch进程切换

该函数位于kernel/sched/core.c文件中,其功能是完成上下文的切换。该函数的参数为本地运行队列rq,切换前的进程prev,切换后的进程next,和rq的标志位。

注意:

  • 一个内存描述符代表一个地址空间,进程有独立的地址空间而同一进程内部的线程共享同一个地址空间
  • 每个进程描述符拥有一个mm字段和一个active_mm字段,前者是拥有的内存描述符而后者是实际有效的内存描述符
  • 用户进程的mm和active_mm取值相同,而内核线程的mm为空, active_mm 沿用之前进程的内存描述符。
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
	// 准备任务切换
	prepare_task_switch(rq, prev, next);

	// 启动上下文切换
	arch_start_context_switch(prev);
    
	//若next->mm为空,则说明next进程为内核线程,则需要借用进程地址空间
	if (!next->mm) {  
		//启用懒惰tlb模式,可以减少无用的tlb刷新
		enter_lazy_tlb(prev->active_mm, next);    
		//借用进程地址空间
		next->active_mm = prev->active_mm;
        
		// 如果前一个任务的mm不为空,说明前一个任务是普通进程,则从用户空间切换到内核
		if (prev->mm) { 
			// 增加前一个任务的active_mm引用计数
			mmgrab(prev->active_mm);
		} else {
            
			// prev和next都是内核线程,prev要被换出,因此prev的active_mm置空
			prev->active_mm = NULL;
		}
        // next为普通进程,代表要切换到用户空间
	} else {  
        
		// 切换内存管理器
		membarrier_switch_mm(rq, prev->active_mm, next->mm);
		switch_mm_irqs_off(prev->active_mm, next->mm, next);

		if (!prev->mm) {  // 前一个任务是内核线程,则从内核空间切换到用户空间
            
			// 保存前一个任务active_mm,并将前一个任务的active_mm置空
			rq->prev_mm = prev->active_mm;
			prev->active_mm = NULL;
		}
	}
	// 准备锁切换
	prepare_lock_switch(rq, next, rf);

	// 执行切换
	switch_to(prev, next, prev);
	// 完成任务切换
	return finish_task_switch(prev);
}

在这里插入图片描述

switch_to

该函数定义在<include/asm-generic/switch to.h>中,具体是将相关重要寄存器中的值进行保存和切换,该函数具体是通过调用__switch_to来实现的,这个函数当中引入了thread_info的概念。switch_to函数中prev和next为输入参数,而last为输出的参数。prev是当前进程,next为要切换的进程,而last为切换到当前进程的进程。

#define switch_to(prev, next, last)					\
	do {								\
		((last) = __switch_to((prev), (next)));			\
	} while (0)

#endif /* __ASM_GENERIC_SWITCH_TO_H */

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值