首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

进程的工作状态

我们可以从上下文切换的过程中, 了解到切换进程是一件耗费资源的过程。我们应当尽量降低上下文切换的频率,以提高系统的运行效率。 但为了达到“并行”执行所有任务的效果,我们还要不断地在进程之间进行切换。如何在保证系统正常运行的情况下, 尽可能的降低切换频率,是操作系统的一个主要的问题。

现在让我们来讨论一个问题:我们应当在什么情况下切换进程?

对于第一种情况,实际上是操作系统强制执行的进程切换。这种形式切换出去的进程,在一段时间之后应当重新切换回来。而对于第2种情况, 在条件不能满足的情况下,我们是不需要切换回该进程的。第三种情况,我们将不再需要执行该进程。针对这些各种不同的情况, 我们需要一个状态机来维护进程的工作状态,进而高效的管理系统中的各个进程。

1. 进程状态概述

《现代操作系统》认为一个进程至少有3种状态:

  1. 运行态(Running):进程已经分配了CPU资源
  2. 就绪态(Ready):进程具备运行的条件,等待调度器分配CPU资源
  3. 挂起态(Blocked):进程不能够被运行,需等待特定的外部事件发生
因为计算机的CPU资源是有限的,我们这里选用的M4内核的ARM处理器只有一个CPU,所以同一时间只有一个进程处于运行态。由调度器管理各个进程, 按照一定的调度规则依次为各个就绪态的进程分配计算资源,让其运行一段时间后切换到其它进程。所以逻辑上,进程的运行态和就绪态是类似的,只是占用了CPU资源与否。

下图是从《现代操作系统》中抠下来的图,它描述了进程在三种状态之间的转换关系。

图1. 一般进程状态

在图中说,一个处于运行态的进程等待输入时就会进入阻塞态。这正是本文初所分析的第二种需要切换进程的情况。在阻塞状态下的进程将不再参与进程调度, 直到需要的条件得到满足为止。实际上这里所说的条件是一个很宽泛的概念,不仅仅是等待输入。在一些系统中,我们可以通过一些系统调用挂起一个进程, 然后在需要的时候通过系统调用再将之唤醒。挂起的进程可以是处于运行态的进程自身,也可以是处于就绪态的其它进程。

图中的状态变换2和3则对应了我们的第一种进程切换情况。这个过程是由操作系统的进程调度器来实现的,进程自身不必清楚调度器的存在和运行机制。 调度器是操作系统最基本的工具,它决定了什么时候运行哪个进程,运行多长时间。

进程大体上就是在这三种状态之间来回切换,不同的操作系统的实现形式和名称可能略有差异,但大体形式都是一样的。 下面我们先分析μC/OS和Linux中的进程状态,再来详细设计我们的XTOS的进程状态。

2. μC/OS的进程状态

下图是μC/OS的进程状态机。虽然这里我们看到了5个状态,其实跟我们在图1中看到的没有本质上区别。 TASK RUNNING, TASK READY和TASK WAITING分别对应着进程的运行态、就绪态和挂起态。而TASK DORMANT和ISR RUNNING则是μC/OS实现过程中的中间产物。

图2. μC/OS中进程状态

在μC/OS中,处于TASK DORMANT的进程是指已经加载到ROM或者RAM中,尚不能够由μC/OS使用的进程。根据我们平时使用计算机的经验,用户程序是保存在硬盘上的, 在每次运行的时候都需要先加载到内存中,才可以运行。这里的TASK DORMANT就是一个类似的状态,在嵌入式系统中所有的进程一般都是烧写在MCU片上的FLASH中的。 所以从MCU上电复位完成后的那一刻各个进程就已经存在系统的ROM或者RAM中了。然后我们还需要做一系列的初始化工作后才可以执行实际的任务程序, 在这段时间内,直到我们通过系统调用OSTaskCreate()注册进程到μC/OS中,进程都是处于TASK DORMANT的状态。

而ISR RUNNING状态则是系统的中断服务函数运行时的状态。中断是一个稍微有点功能的系统不可缺少的功能。 如果说MCU是一只蜘蛛,那么PCB就是蜘蛛的网,网上的发生的任何事件,都将转换为引脚上电平的高低变化。针对不同的事件, MCU上都有一系列既定的程序响应,这些程序就是中断服务函数。从事件发生到做出响应的整个过程则是中断过程。 在我看来,中断服务的过程不应该是进程的一部分,它应该是外设驱动的一部分。ISR RUNNING状态是多余的,对于进程而言没什么具体的意义。

3. Linux的进程状态

下面让我们再来看一下从Linux Kernel Development 中抠出来的图。

图3. Linux中进程状态

从图中,我们可以看到在Linux中把运行态和就绪态都看作是TASK_RUNNING的状态,当有优先级更高的进程需要运行时,就会通过系统调用schedule()切换进程。

挂起态则对应了TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两个状态。注意这里的INTERRUPT并不是指硬件上的中断服务,而是说进程处于一种睡眠的状态, TASK_INTERRUPTIBLE状态的进程可以通过信号唤醒,而TASK_UNINTERRUPTIBLE状态的进程则不可以。

所谓的通过信号唤醒,就是指进程需要等待某个特定的事件发生后才可以继续运行,在Linux中这个等待的特定事件常常是通过信号量来标记的。 当事件发生时,就会从对应信号量的等待队列中将一个或者几个进程唤醒。TASK_UNINTERRUPTIBLE状态下的进程不能通过事件唤醒, 但可以通过系统调用wakeup()显式地唤醒。

此外,Linux中的进程还有__TASK_TRACED和__TASK_STOPPED两个状态,主要用于调试,这里不做详细的介绍。

4. XTOS进程状态初设计

通过上述的分析,我们可以得出如下的结论,在系统中的进程本质上只有两种状态:参与调度的进程和不参与调度的进程。 所以我们暂时将进程的状态简单粗暴的划分为RUNNING和SLEEPING两个状态。我们在进程描述符中添加一个字段taskState,来记录进程状态, 并用两个宏定义分别表示进程的两个状态,在头文件xtos.h中相应的位置修改如下:

        #define XTOS_TASK_STATE_RUNNING     ((uint16)0)
        #define XTOS_TASK_STATE_SLEEPING    ((uint16)1)
        
        /*
         * xtos_task_struct - 任务描述符
         */
        typedef struct xtos_task_descriptor {
            uint32 *pTopOfStack;        /* 栈顶地址,该位段不可以更改 */
            uint32 *pBottomOfStack;     /* 栈底地址 */
            uint16 pid;                 /* 进程ID */
            uint16 taskState;           /* 进程状态 */
            struct list_head list;      /* 链表对象 */
        } xtos_task_desp_t;

此外,分别用两个进程队列Sleeping_tasks和L0_tasks装载SLEEPING和RUNNING状态下的进程。其定义如下,并在xtos_init()函数中初始化。

        struct list_head Sleeping_tasks;    // SLEEPING进程,挂起队列
        struct list_head L0_tasks;          // RUNNING进程
        /*
         * xtos_init - 初始化操作系统
         *
         * 1. 初始化任务队列
         */
        void xtos_init() {
            init_list_head(&L0_tasks);
            init_list_head(&Sleeping_tasks);
        }

然后我们定义一对函数分别用于挂起和唤醒进程。

    /*
     * xtos_block_task - 挂起进程
     */
    void xtos_block_task(struct xtos_task_descriptor *tcb) {
        int primask = xtos_lock();
        if (gp_xtos_next_task == tcb)
            gp_xtos_next_task = gp_xtos_cur_task;
    
        list_move_tail(&tcb->list, &Sleeping_tasks);
        tcb->taskState = XTOS_TASK_STATE_SLEEPING;
    
        xtos_unlock(primask);
    }
    /*
     * xtos_wakeup_task - 唤醒进程
     */
    void xtos_wakeup_task(struct xtos_task_descriptor *tcb) {
        if (XTOS_TASK_STATE_SLEEPING != tcb->taskState)
            return;

        int primask = xtos_lock();
        list_move_tail(&tcb->list, &L0_tasks);
        tcb->taskState = XTOS_TASK_STATE_RUNNING;
    
        xtos_unlock(primask);
    }

xtos_block_task()用于挂起任何状态下的进程。首先判定欲挂起的进程是否为下次切换的目标进程。 若不是我们只需要将目标进程描述符移到Sleeping_tasks队列中,并将进程状态标记为SLEEPING, 就可以将目标进程挂起。否则需要先变更进程切换对象为当前正在运行的进程。 xtos_wakeup_task()用于唤醒SLEEPING状态下的进程。只需要将进程描述符移到运行队列L0_tasks中, 并变更进程状态为RUNNING。

对于没有占用CPU的进程,通过xtos_block_task()函数就可以将之挂起,并立即脱离进程调度。 对于当前正占用CPU的进程,上述的xtos_block_task()虽然变更了进程状态和进程队列,但并不能立即使其脱离进程调度。 我们还需要执行一次schedule()函数回收其CPU资源才能完成挂起当前正在运行的进程。所以专门实现了函数xtos_block(),如下:

        /*
         * xtos_block - 挂起当前进程
         */
        void xtos_block() {
            xtos_block_task(gp_xtos_cur_task);
            xtos_schedule();
        }

5. 串口例程

在这个串口例程中,我们将实现两个进程。进程A仍然是以前的功能用于控制三色LED闪烁,进程B则将串口中接收的数据再发送出去。 我们在UART4的接收中断中把收到的数据保存到全局变量gUartByte中,并唤醒进程B。在进程B的超级循环中,先发送gUartByte在将自身挂起。

        void UART4_IRQHandler(void) {
            if (0 != UART4->SR.bits.RXNE) {
                gUartByte = UART4->DR.bits.byte;
                xtos_wakeup_task(&taskB);
            }
        }
        uint8 gUartByte = '0';
        void taskb() {
            while (1) {
                uart4_send_byte(gUartByte);
                xtos_block();
            }
        }

例程烧录到开发板上后, 我们就可以看到三色灯在蓝色和红色之间不断的闪烁,同时上位机发送的任何数据都将原样返回。

6. 总结

为了尽量减少不必要的进程切换,提高系统效率,我们参考了μC/OS和Linux的进程状态,设计了XTOS的进程状态。 我们简单粗暴的定义了两个状态和进程队列。

在现代操作系统中描述了就绪态、运行态和挂起态三种状态。虽然各个操作系统的实现略有不同,但本质上都可以看到这三种状态。 这里我们将就绪态和运行态暂时看作是同一种状态。通过函数xtos_block_task()和xtos_wakeup_task()分别挂起和唤醒进程。 针对当前占用CPU的进程,我们还专门定义了一个函数xtos_block()来挂起自身。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1