当前位置: 首页 > 科技观察

设置Linux进程的休眠和唤醒

时间:2023-03-21 14:39:28 科技观察

在Linux中,只等待CPU时间的进程称为就绪进程,它们被放入一个运行队列中,就绪进程的状态标志为TASK_RUNNING。一旦一个正在运行的进程的时间片用完,Linux内核的调度器就会剥夺该进程对CPU的控制权,从运行队列中选择一个合适的进程运行。当然一个进程也可以主动释放CPU的控制权。函数schedule()是一个调度函数,可以被一个进程主动调用来调度其他进程占用CPU。一旦主动让出CPU的进程被重新调度占用CPU,它就会从上次停止执行的位置开始执行,也就是从下一行调用schedule()的代码开始执行.有时,进程需要等到特定事件发生,例如设备初始化完成、I/O操作完成或定时器超时。在这种情况下,进程必须从运行队列中移除并添加到等待队列中,此时进程进入睡眠状态。Linux中进程睡眠状态的分类为可中断睡眠状态,其状态标志为TASK_INTERRUPTIBLE;另一种是不间断睡眠状态,其状态标志为TASK_UNINTERRUPTIBLE。处于可中断睡眠状态的进程会一直休眠,直到某个条件成立,比如产生硬件中断,释放进程等待的系统资源,或者传递一个信号都可以成为唤醒进程的条件。不可中断睡眠状态与可中断睡眠状态类似,但有一个例外,即向该睡眠状态传递信号的进程不能改变自己的状态,即不响应唤醒信号。不可中断的休眠状态一般用得较少,但这种状态在某些特定情况下还是很有用的,例如:进程必须等待,不能被中断,直到特定事件发生。在现代Linux操作系统中,进程一般通过调用schedule()进入睡眠状态。以下代码演示了如何将正在运行的进程置于睡眠状态。sleeping_task=current;set_current_state(TASK_INTERRUPTIBLE);schedule();func1();/*Restofthecode...*/在第一条语句中,程序中存储了一个进程结构体指针sleeping_task,current是一个宏,指向该结构体正在执行的进程。set_current_state()将进程的状态从执行状态TASK_RUNNING改变为睡眠状态TASK_INTERRUPTIBLE。如果schedule()被一个状态为TASK_RUNNING的进程调度,那么schedule()会调度另一个进程占用CPU;如果schedule()被状态为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的进程调度,那么将执行一个额外的步骤:当前正在执行的进程在另一个进程被调度之前从运行队列中移除,这导致正在运行的进程转到睡眠,因为它不再在运行队列中。我们可以使用下面的函数来唤醒刚刚进入休眠状态的进程。wake_up_process(睡眠任务);调用wake_up_process()后,休眠进程的状态会被设置为TASK_RUNNING,调度器会将其加入到运行队列中。当然,这个进程只有在下次被调度器调度时才能真正投入运行。Invalidwakeup几乎所有情况下,进程都会在检查一些条件发现不满足条件后进入休眠状态。但是有时候判断条件为真后进程会开始休眠。如果是这样的话,进程就会休眠很长时间。这就是所谓的无效唤醒问题。在操作系统中,当多个进程试图对共享数据执行某些处理,而最终结果取决于进程运行的顺序时,就会出现竞争条件。这是操作系统中的典型问题。无效唤醒正是由于竞争条件造成的。假设有两个进程A和B,进程A正在处理一个链表。它需要检查链表是否为空。如果不为空,则会对链表中的数据进行一些操作。与此同时,进程B也在向链表中添加节点。当链表为空时,因为没有数据可以操作,此时A进程会进入休眠状态。当B进程向链表中添加节点时,会唤醒A进程。代码如下:A进程:spin_lock(&list_lock);if(list_empty(&list_head)){spin_unlock(&list_lock);set_current_state(TASK_INTERRUPTIBLE);schedule();spin_lock(&list_lock);}/*Restofthecode...*/spin_unlock(&list_lock);Bprocess:spin_lock(&list_lock);list_add_tail(&list_head,new_node);spin_unlock(&list_lock);wake_up_process(processa_task);这里会有一个问题,如果当进程A执行到第3行和第4行之前,进程B被另一个处理器调度运行。在这个时间片中,B进程已经执行完了自己所有的指令,于是尝试唤醒A进程,而此时A进程还没有进入睡眠状态,所以唤醒操作无效。之后进程A继续执行,它会误认为此时链表还是空的,于是设置状态为TASK_INTERRUPTIBLE,然后调用schedule()进入休眠状态。因为错过了进程B的唤醒,所以会休眠最长时间。这就是无效唤醒的问题,因为即使链表中有数据需要处理,进程A仍然处于休眠状态。避免无效唤醒如何避免无效唤醒?我们发现无效唤醒主要发生在检查条件之后,进程状态设置为睡眠状态之前。本来,进程B的wake_up_process()提供了一个机会,可以将进程A的状态设置为TASK_RUNNING。不幸的是,此时进程A的状态仍然是TASK_RUNNING,所以wake_up_process()将进程A的状态从休眠状态变为运行状态的努力并没有起到预期的作用。要解决这个问题,必须使用一种保证机制,使判断链表为空和设置进程状态为休眠状态成为一个不可分割的步骤,即必须消除竞争条件的根本原因,这样wake_up_process()可以起到唤醒处于休眠状态的进程的作用。找到原因后,重新设计进程A的代码结构,避免上例中的无效唤醒问题。进程:set_current_state(TASK_INTERRUPTIBLE);spin_lock(&list_lock);if(list_empty(&list_head)){spin_unlock(&list_lock);schedule();spin_lock(&list_lock);}set_current_state(TASK_RUNNING);/*Restofthecode...*/spin_unlock(&list_lock);可以看出,这段代码在测试条件之前将当前执行进程状态设置为TASK_INTERRUPTIBLE,在链表不为空时将自身设置为TASK_RUNNING状态。这样,如果A进程检查链表为空后,B进程调用wake_up_process(),那么A进程的状态就会自动从原来的TASK_INTERRUPTIBLE变为TASK_RUNNING。状态是TASK_RUNNING,所以还是不会从运行队列中移除,所以不会误入sleep,当然也就避免了无效唤醒的问题。Linux内核的一个例子在Linux操作系统中,内核的稳定性非常重要。为了避免Linux操作系统内核出现无效唤醒问题,Linux内核在进程需要休眠时应该使用类似如下的操作:/*'q'是我们希望休眠的等待队列*/DECLARE_WAITQUEUE(wait,current);add_wait_queue(q,&wait);set_current_state(TASK_INTERRUPTIBLE);/*或TASK_INTERRUPTIBLE*/while(!condition)/*'condition'是等待的条件*/schedule();set_current_state(TASK_RUNNING);remove_wait_queue(q,&wait);以上操作通过以下一系列步骤使进程安全地将自己添加到一个等待队列中进行睡眠:首先调用DECLARE_WAITQUEUE()创建一个等待队列的项,然后调用add_wait_queue()将自己添加到等待队列中,并将进程的状态设置为TASK_INTERRUPTIBLE或TASK_INTERRUPTIBLE。然后循环检查条件是否为真:如果为真,则无需睡眠,如果条件不为真,则调用schedule()。当满足进程检查的条件时,进程将自己设置为TASK_RUNNING并调用remove_wait_queue()将自己从等待队列中移除。从上面可以看出,Linux内核代码维护者在进程检查条件之前,也是先将进程的状态设置为休眠,然后循环检查条件。如果在进程开始休眠前满足条件,则循环退出,并通过set_current_state()将其状态设置为就绪,这也保证了进程不会有进入休眠的错误倾向,当然也不会导致无效唤醒问题。下面通过Linux内核中的一个例子,看看Linux内核是如何避免无效睡眠的。这段代码来自Linux2.6内核(linux-2.6.11/kernel/sched.c:4254):);}__set_current_state(TASK_RUNNING);return0;以上代码属于迁移服务线程migration_thread,该线程不断检查kthread_should_stop(),直到kthread_should_stop()返回1才退出循环,也就是说进程会休眠多久因为kthread_should_stop()返回0。从代码中我们可以看出,检查kthread_should_stop()确实是在进程状态设置为TASK_INTERRUPTIBLE之后执行的。因此,如果另一个进程在条件检查之后但在schedule()之前尝试唤醒它,则该进程的唤醒操作不会失效。总结通过上面的讨论可以发现,在Linux中避免进程无效唤醒的关键是在进程检查条件之前将进程的状态设置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,如果检查的条件满足,它应该将其状态重置为TASK_RUNNING。这样无论进程的等待条件是否满足,进程都不会因为被移出就绪队列而误进入睡眠状态,从而避免了无效唤醒的问题。