转自:https://www.ibm.com/developerworks/cn/linux/l-task-killable/index.html
新的睡眠状态允许 TASK_UNINTERRUPTIBLE 响应致命信号
Linux® kernel 2.6.25 引入了一种新的进程状态,名为 TASK_KILLABLE
,用于将进程置为睡眠状态,它可以替代有效但可能无法终止的 TASK_UNINTERRUPTIBLE
进程状态,以及易于唤醒但更加安全的 TASK_INTERRUPTIBLE
进程状态。2002 年,OpenAFS 文件系统驱动程序在阻塞所有信号之后等待事件中断时遇到了问题,而 TASK_KILLABLE
就是因此而被推出的。这种新的睡眠状态允许 TASK_UNINTERRUPTIBLE
响应致命信号。在本文中,作者将介绍这方面的内容,并结合 2.6.26 和早期版本 2.6.18 中的示例来讨论 Linux 内核发生的相关变化以及这些变化带来的新 API。
0 评论
在 IBM Bluemix 云平台上开发并部署您的下一个应用。
类似于文件,进程是任何 UNIX® 操作系统的基本元素。进程是执行可执行文件的指令的动态实体。除了执行其指令之外,进程有时还会管理 打开文件、处理器上下文、地址空间以及与程序相关的数据等。Linux 内核将关于进程的完整信息保存在进程描述符 中,它的结构被定义为 struct task_struct
。您可以在 Linux 内核源文件 include/linux/sched.h 中看到 struct task_struct
的各个字段。
关于进程状态
在进程的生命周期内,可能会经历一系列互斥的状态。内核将进程的状态信息保存在 struct task_struct
的 state 字段中。图 1 展示了进程状态之间的转换。
图 1. 进程状态转换
我们先来了解一下各种进程状态:
TASK_RUNNING
:进程当前正在运行,或者正在运行队列中等待调度。TASK_INTERRUPTIBLE
:进程处于睡眠状态,正在等待某些事件发生。进程可以被信号中断。接收到信号或被显式的唤醒呼叫唤醒之后,进程将转变为TASK_RUNNING
状态。TASK_UNINTERRUPTIBLE
:此进程状态类似于TASK_INTERRUPTIBLE
,只是它不会处理信号。中断处于这种状态的进程是不合适的,因为它可能正在完成某些重要的任务。 当它所等待的事件发生时,进程将被显式的唤醒呼叫唤醒。TASK_STOPPED
:进程已中止执行,它没有运行,并且不能运行。接收到SIGSTOP
和SIGTSTP
等信号时,进程将进入这种状态。接收到SIGCONT
信号之后,进程将再次变得可运行。TASK_TRACED
:正被调试程序等其他进程监控时,进程将进入这种状态。EXIT_ZOMBIE
:进程已终止,它正等待其父进程收集关于它的一些统计信息。EXIT_DEAD
:最终状态(正如其名)。将进程从系统中删除时,它将进入此状态,因为其父进程已经通过wait4()
或waitpid()
调用收集了所有统计信息。
有关进程状态转换的详细信息,请参阅 参考资料 一节中的 UNIX 操作系统设计。
如前所述,进程状态 TASK_UNINTERRUPTIBLE
和 TASK_INTERRUPTIBLE
都是睡眠状态。现在,我们来看看内核如何将进程置为睡眠状态。
内核映射
Linux 内核提供了两种方法将进程置为睡眠状态。
将进程置为睡眠状态的普通方法是将进程状态设置为 TASK_INTERRUPTIBLE
或 TASK_UNINTERRUPTIBLE
并调用调度程序的 schedule()
函数。这样会将进程从 CPU 运行队列中移除。如果进程处于可中断模式的睡眠状态(通过将其状态设置为 TASK_INTERRUPTIBLE
),那么可以通过显式的唤醒呼叫(wakeup_process()
)或需要处理的信号来唤醒它。
但是,如果进程处于非可中断模式的睡眠状态(通过将其状态设置为 TASK_UNINTERRUPTIBLE
),那么只能通过显式的唤醒呼叫将其唤醒。除非万不得已,否则我们建议您将进程置为可中断睡眠模式,而不是不可中断睡眠模式(比如说在设备 I/O 期间,处理信号非常困难时)。
当处于可中断睡眠模式的任务接收到信号时,它需要处理该信号(除非它已被屏弊),离开之前正在处理的任务(此处需要清除代码),并将 -EINTR
返回给用户空间。再一次,检查这些返回代码和采取适当操作的工作将由程序员完成。因此,懒惰的程序员可能比较喜欢将进程置为不可中断模式的睡眠状态,因为信号不会唤醒这类任务。但需要注意的一种情况是,对不可中断睡眠模式的进程的唤醒呼叫可能会由于某些原因不会发生,这会使进程无法被终止,从而最终引发问题,因为惟一的解决方法就是重启系统。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。
现在,我们在内核中实现了一种新的睡眠方法!
新睡眠状态:TASK_KILLABLE
Linux Kernel 2.6.25 引入了一种新的进程睡眠状态,TASK_KILLABLE
:当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE
,只不过可以响应致命信号。清单 1 给出了内核 2.6.18 与内核 2.6.26 进程状态(定义在 include/linux/sched.h 中)之间的比较:
清单 1. 2.6.18 和 2.6.26 进程状态之间的比较
Linux Kernel 2.6.18 Linux Kernel 2.6.26
================================= ===================================
#define TASK_RUNNING 0 #define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1 #define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2 #define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4 #define __TASK_STOPPED 4
#define TASK_TRACED 8 #define __TASK_TRACED 8
/* in tsk->exit_state */ /* in tsk->exit_state */
#define EXIT_ZOMBIE 16 #define EXIT_ZOMBIE 16
#define EXIT_DEAD 32 #define EXIT_DEAD 32
/* in tsk->state again */ /* in tsk->state again */
#define TASK_NONINTERACTIVE 64 #define TASK_DEAD 64
#define TASK_WAKEKILL 128
注意,状态 TASK_INTERRUPTIBLE
和 TASK_UNINTERRUPTIBLE
并未修改。 TASK_WAKEKILL
用于在接收到致命信号时唤醒进程。
清单 2 展示了状态 TASK_STOPPED
和 TASK_TRACED
的修改之处(以及 TASK_KILLABLE
的定义):
清单 2. 内核 2.6.26 中的新状态定义
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
换句话说,TASK_UNINTERRUPTIBLE
+ TASK_WAKEKILL
= TASK_KILLABLE
。
使用 TASK_KILLABLE 的新内核 API
关于 完成 的一些信息
完成机制的适用情况是:您希望将某个任务置为睡眠状态,但随后需要在某些事件完成时唤醒它。它提供了一种简单的、无竞态条件的同步机制。例程 wait_for_completion(struct completion *comp)
将使调用任务处于不可中断睡眠状态,除非完成已经发生。它要求通过 complete(struct completion *comp)
或 complete_all(struct completion *comp)
函数来唤醒进程。
除了 wait_for_completion_killable()
之外,其他正在等待的例程包括:
wait_for_completion_timeout()
wait_for_completion_interruptible()
wait_for_completion_interruptible_timeout()
有关完成结构的定义,请参阅 include/linux/completion.h。
让我们来看看这种新状态中的新函数。
int wait_event_killable(wait_queue_t queue, condition);
该函数定义在 include/linux/wait.h 中;它将处于可终止睡眠状态的调用进程置于queue
中,直到condition
等于 true。long schedule_timeout_killable(signed long timeout);
该函数定义在 kernel/timer.c 中;该例程主要用于将当前任务的状态设置为TASK_KILLABLE
并调用
schedule_timeout()
,它用于让调用任务睡眠timeout
个 jiffies。(在 UNIX 系统中,jiffy 主要表示两个连续时钟计时单元之间的时间。int wait_for_completion_killable(struct completion *comp);
此例程定义在 kernel/sched.c 中,它用于等待进程在事件完成后变为可终止状态。如果没有等待的致命信号,该函数将调用
schedule_timeout()
维持
MAX_SCHEDULE_TIMEOUT
(指定为等于LONG_MAX
)个 jiffies 时间。int mutex_lock_killable(struct mutex *lock);
定义在 kernel/mutex.c 中,该例程用于获取互斥锁。但是,如果锁不可用并且任务正在等待获得锁,与此同时又接收到一个致命信号,则该任务将从等待互斥锁以处理信号的等待者列表中删除。int down_killable(struct semaphore *sem);
定义在 kernel/semaphore.c 中,它用于获取信号量sem
。如果信号量不可用,它将被置为睡眠状态;如果向它传递了一个致命信号,则会将它从等待者列表中删除,并且需要响应此信号。获取信号量的另外两种方法是使用例程down()
或
down_interruptible()
。down()
函数现在已不建议使用。您应该使用down_killable()
或
down_interruptible()
函数。
NFS 客户机代码中的变化
NFS 客户机代码也使用了这种新进程状态。清单 3 显示了 Linux 内核 2.6.18 和 2.6.26 在 nfs_wait_event
宏方面的差异。
清单 3. nfs_wait_event 因 TASK_KILLABLE 而发生的变化
Linux Kernel 2.6.18 Linux Kernel 2.6.26
========================================== =============================================
#define nfs_wait_event(clnt, wq, condition) #define nfs_wait_event(clnt, wq, condition)
({ ({
int __retval = 0; int __retval =
wait_event_killable(wq, condition);
if (clnt->cl_intr) { __retval;
sigset_t oldmask; })
rpc_clnt_sigmask(clnt, &oldmask);
__retval =
wait_event_interruptible(wq, condition);
rpc_clnt_sigunmask(clnt, &oldmask);
} else
wait_event(wq, condition);
__retval;
})
清单 4 显示了 nfs_direct_wait()
函数在 Linux Kernels 2.6.18 与 2.6.26 中的定义
清单 4. nfs_direct_wait() 因 TASK_KILLABLE 而发生的变化
Linux Kernel 2.6.18
=================================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED; /* Async requests don't wait here */
if (dreq->iocb)
goto out; result = wait_for_completion_interruptible(&dreq->completion); if (!result)
result = dreq->error;
if (!result)
result = dreq->count; out:
kref_put(&dreq->kref, nfs_direct_req_release);
return (ssize_t) result;
} Linux Kernel 2.6.26
=====================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED;
/* Async requests don't wait here */
if (dreq->iocb)
goto out; result = wait_for_completion_killable(&dreq->completion);
if (!result)
result = dreq->error;
if (!result)
result = dreq->count;
out:
return (ssize_t) result;
}
要了解 NFS 客户机中的更多变化,以便于更好地掌握这种新功能,请参阅 参考资料 一节中的 Linux Kernel Mailing List 条目。
早期的 NFS 挂载选项 intr
可以帮助解决 NFS 客户机进程中断并等待某些事件的问题,但它允许所有中断,而不仅仅是通过致命信号(如 TASK_KILLABLE
)。
结束语
尽管此特性是对现有选项的改进 — 毕竟,它是解决死进程的另一种方法 — 但它要得到普遍应用还有待时日。记住,除非真的非常有必要 禁止显式唤醒呼叫(通过传统的 TASK_UNINTERRUPTIBLE
)之外的任何中断,否则请使用新的 TASK_KILLABLE
。
参考资料
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
TASK_KILLABLE
进程状态 是因为 David Howells 于 2002 年提出的一个问题而诞生的;他发现 OpenAFS 文件系统驱动程序在阻塞所有可中断信号时会等待某个事件, 而它们实际上应该在TASK_UNINTERRUPTIBLE
状态中等待。- Jonathan Corbet 就
TASK_KILLABLE
(LWN.net,2008 年 7 月)展开的讨论是非常有用的入门信息。 - “Kernel Korner:Kernel 中的睡眠”(Linux Journal,2005 年 7 月)解释了睡眠在 Linux 内核中的应用。
- 在 UNIX 操作系统设计(Prentice Hall,1986,Maurice J. Bach)中,第 6 章详细介绍了关于进程状态转换的详细信息。
- Linux Kernel 邮件列表 中的 “NFS Killable tasks request comments on patch” 条目演示了 NFS 客户机在利用新 2.6.26
TASK_KILLABLE
函数方面的更多变化。 - 阅读 “Linux 内核剖析”(developerWorks,2007 年 6 月),了解内核构成方式的概要信息。
- 在 developerWorks Linux 专区 可以找到更多针对 Linux 开发人员的资源。浏览我们的 最流行的文章和教程。
- 在 developerWorks 上查阅所有 Linux 技巧 和 Linux 教程。
- 随时关注 developerWorks 技术活动和网络广播。
获得产品和技术
- 使用可直接从 developerWorks 下载的 IBM 试用软件 构建您的下一个 Linux 开发项目。
讨论
- 通过 blog、论坛、podcast 和空间加入 developerWorks 社区。