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 函數主要做了兩件事,第一件事是將通過調度類從運行隊列中挑選下一個運行的進程,第二件事是進行進程切換
  • 進程切換會切換進程地址空間,重新加載頁表,還有切換內核棧
  • 進程切換涉及三個進程,新進程需要對上一個進程做一些清理工作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章