Linux进程管理(三)进程调度之主动调度

Linux进程管理

Linux进程管理(一)进程数据结构

Linux进程管理(二)进程调度

Linux进程管理(三)进程调度之主动调度

Linux进程管理(四)进程调度之抢占式调度

Linux进程管理(三)进程调度之主动调度


在上一篇文章中,我们讲了Linux进程调度的总体内容,接下来的两篇文章我们将来讨论进程调度具体是什么时候发生的

一、抢占式调度和主动调度

前面我们说过,进程的切换总是通过 shedule 函数发生的,而 schedule 函数可以是在系统调用返回、中断返回等时机被调用,也可以进程在驱动程序中主动调用

我们把在系统调用返回等时机调用 schedule 函数的这种非进程自愿情况称为抢占式调度。把进程在驱动程序中主动调用 schedule 函数来发生进程切换的这种情况称为主动调度

本文将讨论主动调度,抢占式调度将在下一篇文章中讲解

二、主动调度的发生的情况

主动调度一般在应用程序读取某个设备时,设备此时数据还没有准备好,进程就进入睡眠,发生进程调度切换到其它进程运行

例如应用想从网卡读取数据,但是此时网卡没有数据,那么驱动程序就会让进程睡眠,然后发生进程调度。又或者应用想读取按键,但是按键还没有被按下,此时驱动程序也会让进程睡眠,然后发生进程调度

在驱动程序中,对应的实现如下

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

/* 网卡的驱动程序 */
tap_do_read(...)
{
    ...
	DEFINE_WAIT(wait); //定义一个等待队列
    
	while(!condition)
    {
        add_wait_queue(&wq_head, &wait); //将进程添加到等待队列中
        set_current_state(TASK_UNINTERRUPTIBLE); //设置进程的状态为睡眠态
        ...
            
        /* 主动调度 */
        schedule();
    
    	...
    }
	...
}
/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

/* 按键的驱动程序 */
button_do_read(...)
{
    ...
	DEFINE_WAIT(wait); //定义一个等待队列
    
	while(!condition)
    {
        add_wait_queue(&wq_head, &wait); //将进程添加到等待队列中
        set_current_state(TASK_UNINTERRUPTIBLE); //设置进程的状态为睡眠态
        ...
            
        /* 主动调度 */
        schedule();
        
    	...
    }
	...
}

如果你看不懂 schedule 前的代码也没有关系,只需要知道那是进程睡眠前做的一些准备动作就行

真正的进程切换发生在 schedule 函数中,调用 schedule 函数,会发生进程调度,切换到其它进程运行,当前进程进入睡眠

这就是进程主动调度的一般情况,接下来我们看一看 schedule 函数做了什么,具体是怎么实现进程切换的

三、schedule 函数

schedule 函数的定义如下

asmlinkage __visible void __sched schedule(void)
{
    ...
	__schedule(false);
	...
}

schedule 函数最主要的是调用 __schedule 函数,下面看一下 __schedule 函数的定义

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    struct rq *rq;
    int cpu;
    
    /* 获取运行队列 */
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    prev = rq->curr;
    
    ...
	/* 从运行队列中挑选下一个运行的进程 */
    next = pick_next_task(rq, prev, &rf);
    ...
    
    ...
	/* 发生进程切换 */
    context_switch(rq, prev, next, &rf);
	...
}
  • 首先获取CPU对应的运行队列,前面我们说过,每个CPU都有其自己对应的运行队列
  • 然后通过 pick_next_task 来获取下一个运行的进程
  • 最后通过 context_switch 来实现进程切换

所以 schedule 函数可以总结成两件事,第一件事就是从运行队列中挑选下一个运行的进程,第二件事就是实现进程切换

挑选下一个运行的进程

首先我们来看如何通过 pick_next_task 来获取下一个运行的进程,其定义如下

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    ...
    
	for_each_class(class) {
		p = class->pick_next_task(rq, prev, rf);
		if (p) {
			return p;
		}
	}
}

按优先级遍历所有的调度类,从对应的运行队列中找到下一个运行的任务,上一篇文章对这部分已经做了详细的讲解,这里不再细说了,如果不记得的,可以回忆一下下面这张图

接下来我们看第二件事,实现进程切换

进程切换

在 schedule 通过 pick_next_task 找到下一个进程后,会调用 context_switch 来实现进程切换,其定义如下

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
    /* 切换内存空间 */
    mm = next->mm;
    oldmm = prev->active_mm;
    ...
    switch_mm_irqs_off(oldmm, mm, next);
    
    /* 切换进程 */
    switch_to(prev, next, prev);
    
    /* 对上一个进程进行清理 */
    return finish_task_switch(prev);
}
  • 首先是通过 switch_mm_irqs_off 来进行进程地址空间的切换,其中的 mm 表示下一个进程的地址空间,oldmm 表示当前进程的地址空间,switch_mm_irqs_off 主要做的是重新加载页表
  • 然后会调用 switch_to 进行进程切换,switch_to 返回后,进程已经切换完毕
  • 最后调用 finish_task_switch 对上一个进程做一些清理工作

下面我们来看 switch_to 做了什么,它其实是一个宏定义,如下

switch_to(prev, next, prev);

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

__switch_to 函数的具体内容这里就不看了,它里面做的最重要的一件事就是切换内核栈,将栈顶指针寄存器指向新进程的内核栈

到这里,进程的切换就已经完成了

真的这样就完成了吗?

我们先想一下,进程切换主要做了什么

  • 进程地址空间的切换

    进程与进程之间的地址是相互独立的,所以需要切换进程的地址空间

  • 指令指针的切换

    进程切换后,要恢复进程原本执行的地方

我们下面看看 context_switch 主要做了哪些事情

  • 切换进程用户地址空间,重新加载页表
  • 在 switch_to 中切换进程的内核栈
  • 切换内核栈后继续执行,此时已经算是进程切换完毕了

从上面我们可以看到,我们已经完成了进程地址空间的切换

但是我们并没有看到指令指针的修改,我们说一旦内核栈切换完后,就算进程切换完毕,这是为什么呢?

我们前面一直强调,进程切换都是调用 schedule 函数来实现的,schedule 函数中会调用 switch_to 来进行进程切换。对于每个进程来说,都是在 switch_to 函数中被切换掉的,所以当进程再次被运行的时候,也是从 switch_to 函数中继续运行是没毛病的

为了让你理解进程切换的过程,我打算把从应用层到进程切换过程给复盘一遍

我将讨论这样一个场景,现在有一个进程,它通过系统调用读取网卡数据,但是网卡此时没有数据,所以它会睡眠。当网卡有数据的时候,它又被唤醒重新开始运行,然后返回用户态

  • 当进程A发生系统调用的时候,会将进程A在用户态运行的时候的寄存器保存下来(栈顶指针、指令指针等等)

    还记得内核栈的模样吗?它长下面这个样子

    其中的 pt_regs 就用来保存进程在用户态运行时寄存器的值

  • 发生系统调用进入内核态后,进程最终会调用到网卡驱动的读函数

    网卡的读函数大概是这个样子

    /*
     * 本文作者:_JT_
     * 博客地址:https://blog.csdn.net/weixin_42462202
     */
    
    /* 网卡的驱动程序 */
    tap_do_read(...)
    {
        ...
    	DEFINE_WAIT(wait); //定义一个等待队列
        
    	while(!condition)
        {
            add_wait_queue(&wq_head, &wait); //将进程添加到等待队列中
            set_current_state(TASK_UNINTERRUPTIBLE); //设置进程的状态为睡眠态
            ...
                
            /* 主动调度 */
            schedule();
        
        	...
        }
    	...
    }
    
  • 当网卡没有数据的时候,进程A就会进入睡眠,调用 schedule 函数发生进程切换,schedule 又会调用 switch_to 来真正完成进程切换,此时该进程的内核栈就变成下面这个样子

    在这里插入图片描述

    在进程A的内核栈中,在调用 schedule 函数的时候,会保存下来 tap_do_read 的返回地址,在调用 switch_to 的时候,会保存 schedule 函数的返回地址

    而在调用 switch_to 后,栈顶指针就会指向新进程的内核栈,所以进程A的函数栈就保存成上面的样子,直到被唤醒重新运行

  • 在 switch_to 函数中切换进程栈后,就算进程切换完毕了。假如我们此时切换到进程B,如果进程B当初是准备读取按键,由于按键没有被按下,所以进入睡眠,进程B也是通过 schedule 函数来实现进程切换的。那么进程B内核栈的内容跟进程A的内核也是相似的,如下

    在这里插入图片描述

    所以切换到进程B后,还是在 switch_to 函数中继续运行,之后函数调用返回,从栈中弹出返回地址,最后会返回到 button_do_read 函数,这也是进程B在内核态运行时候的

  • 进程B当初也是通过系统调用进入内核的,现在进程B读取到按键数据后,要返回用户空间,此时内核会将进程B的内核栈中 pt_reg 里面所有保存下来的寄存器恢复,例如会重新设置栈指针寄存器,指令指针寄存器。而进程B的用户地址空间映射在 schedule 函数中已经修改了,这样子,进程B又回到了原来用户空间运行的位置继续运行下去

  • 同理,当某个时刻调用了 schedule 函数,切换到进程A,也是一样的过程

到此,你应该对进程切换有所了解了

下面再来讨论一个问题,为什么进程切换涉及到三个进程,而不是两个进程?

进程切换不是只是从进程A切换到进程B吗,为什么在 switch_to 中是三个进程

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

switch_to(prev, next, prev);

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

其实进程切换涉及到的是三个进程,为何?下面我来为你讲解

假设进程A切换到进程B,进程B又进行了多次进程切换,最后切换到进程C,进程C又切换到进程A,如下图所示

在这里插入图片描述

你看,跟进程A相关的进程有两个,一个是进程B,一个是进程C

进程A切换到进程B,进程A又从进程C切换过来,所以这个过程涉及到三个进程A、B、C

在 switch_to 中,有三个变量,在进程A被唤醒的时候,prev 表示进程A,next 表示进程B,last 表示进程C

/*
 * 本文作者:_JT_
 * 博客地址:https://blog.csdn.net/weixin_42462202
 */

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

那么 switch_to 是怎么实现的呢?

prev 和 next 在进程被切换前就保存在进程的内核栈中,所以进程再被唤醒的时候很自然通过局部变量就可以得到它们

而 last 对于被唤醒的进程,又不存在于它的内核栈中,那么 last 对于进程来说是怎么获取的呢?

你可以注意到,last 是通过 __switch_to 的函数返回值获取的

以进程C切换到进程A为例,进程C将自己的进程描述符地址放到寄存器中,然后切换到进程A,进程A得到 __switch_to 返回值,__switch 的返回值其实就是寄存器的值,也就是进程C的进程描述符地址

这样子进程就知道自己是从哪一个进程切换过来的,那么为什么进程需要直到它是从哪一个进程切换过来的呢?

因为在进程切换后,新进程有必要对它上一个进程做一些清理工作

好了,这篇文章到这里就差不多结束了,下面进入总结时刻

四、总结

  • 进程发生切换总是调用 schedule 函数进行的,进程调度分抢占式调度和主动调度,主动调度表示的是进程主动调用 schedule 函数发生进程切换
  • schedule 函数主要做了两件事,第一件事是将通过调度类从运行队列中挑选下一个运行的进程,第二件事是进行进程切换
  • 进程切换会切换进程地址空间,重新加载页表,还有切换内核栈
  • 进程切换涉及三个进程,新进程需要对上一个进程做一些清理工作
### 回答1: schedule函数通常用于在Python中定时执行某个函数或任务。它可以帮助我们在指定的时间间隔内定期执行函数,而不需要手动调用该函数。该函数可以通过使用time模块中的time.sleep()函数来实现。schedule函数的基本语法如下: ```python schedule.every(interval).unit.do(job) ``` 其中, - `interval`:表示时间间隔,可以是一个整数或浮点数,单位由`unit`参数决定。 - `unit`:表示时间间隔的单位,可以是'weeks'、'days'、'hours'、'minutes'、'seconds'、'microseconds'、'milliseconds'中的任意一个。 - `job`:表示要执行的函数或任务。 例如,如果我们想要每隔10秒钟执行一次函数`my_func()`,可以使用如下代码: ```python import schedule import time def my_func(): print("Hello, World!") schedule.every(10).seconds.do(my_func) while True: schedule.run_pending() time.sleep(1) ``` 在上面的代码中,我们首先定义了一个`my_func()`函数,然后使用`schedule.every(10).seconds.do(my_func)`语句来设置每隔10秒钟执行一次该函数。最后,在一个无限循环中,我们不断地调用`schedule.run_pending()`函数来检查是否有任务需要执行,然后使用`time.sleep(1)`函数来让程序休眠1秒钟,以避免CPU占用过高。 ### 回答2: schedule函数的功能是用于实现任务的调度管理。它可以根据预定的时间表或规则,自动分配和安排多个任务的执行顺序和时间。具体来说,schedule函数可以完成以下几个方面的功能: 1. 创建任务:schedule函数可以接收任务的参数,并将其创建为一个任务对象。任务可以是一个函数、一个方法或一个可调用对象。 2. 设定触发条件:schedule函数可以设置任务的触发条件,例如设置任务的执行时间、执行间隔、定时执行等。这些触发条件可以是时间、日期、事件等。 3. 任务执行:schedule函数可以按照设定的触发条件,在适当的时间触发任务的执行。任务可以是单次执行或者周期性执行,可以是同步执行或者异步执行,可以是并发执行或者顺序执行,具体执行方式根据调度函数的参数而定。 4. 任务管理:schedule函数可以对已创建的任务进行管理,例如取消任务、暂停任务、恢复任务等。这样可以灵活控制任务的执行情况。 通过上述功能,schedule函数可以方便地实现任务的自动化管理调度,帮助用户高效地处理多个任务。它常用于需要定时执行、自动化处理和后台任务等应用场景,如定时备份、定时数据清理、定时邮件发送等。 ### 回答3: shedule函数是编程中常用的一个函数,它主要用于控制程序的运行顺序和时间安排。shedule函数可以通过指定不同的参数来实现不同的功能。 其中最常见的功能是定时执行任务。通过设置shedule函数的时间参数,我们可以让程序在一定的时间间隔或者指定的时间点执行特定的任务。这个功能常用于需要程序周期性地执行某些操作,比如定时备份数据、定时发送邮件、定时下载文件等。 另外,shedule函数还可以实现延时执行任务的功能。通过设置shedule函数的延时时间参数,我们可以让程序在指定的延时时间之后执行特定的任务。这个功能常用于需要程序等待一段时间后再执行某些操作,比如在用户点击某个按钮后等待几秒钟再弹出对话框。 此外,shedule函数还可以实现并发执行任务的功能。通过设置shedule函数的并发参数,我们可以让多个任务同时执行。这个功能常用于需要同时执行多个耗时任务,以提高程序的执行效率。 总结起来,shedule函数的功能主要包括定时执行任务、延时执行任务和并发执行任务。这些功能可以根据实际需求灵活地使用,以满足程序的不同需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值