《Linux 进程管理剖析 创建、管理、调度和销毁》由会员分享,可在线阅读,更多相关《Linux 进程管理剖析 创建、管理、调度和销毁(10页珍藏版)》请在金锄头文库上搜索。
1、Linux 进程管理剖析创建、管理、调度和销毁M. Tim Jones, 顾问工程师, Emulex Corp.2009 年 1 月 20 日Linux? 的用户空间进程的创建和管理所涉及的原理与 UNIX? 有很多共同点,但也有一些特定于 Linux 的独特之处。在本文中,了解 Linux 进程的生命周期,探索用户进程创建、内存管理、调度和销毁的内核内幕。Linux 是一种动态系统,能够适应不断变化的计算需求。Linux 计算需求的表现是以进程 的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程及其调度进行一般管理就显得极为重要。在用
2、户空间,进程是由进程标识符(PID)表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会更改,但 PID 可以在进程销毁后被重新使用,所以对它们进行缓存并不见得总是理想的。在用户空间,创建进程可以采用几种方式。可以执行一个程序(这会导致新进程的创建),也可以在程序内,调用一个 fork 或 exec 系统调用。fork 调用会导致创建一个子进程,而 exec 调用则会用新程序代替当前进程上下文。接下来,我将对这几种方法进行讨论以便您能很好地理解它们的工作原理。在本文中,我将按照下面的顺序展开对进程的介绍,首先展示进程的内核表示以及它们
3、是如何在内核内被管理的,然后来看看进程创建和调度的各种方式(在一个或多个处理器上),最后介绍进程的销毁。进程表示 阅读 developerWorks 上 Tim Jones 所撰写的更多文章Tim 的剖析 系列文章 在 Linux 内核内,进程是由相当大的一个称为 task_struct 的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(父和子)。对 task_struct 的完整介绍超出了本文的范围,清单 1 给出了 task_struct 的一小部分。这些代码包含了本文所要探索的这些特定元素。task_s
4、truct 位于 ./linux/include/linux/sched.h。清单 1. task_struct 的一小部分struct task_struct volatile long state;void *stack;unsigned int flags;int prio, static_prio;struct list_head tasks;struct mm_struct *mm, *active_mm;pid_t pid;pid_t tgid;struct task_struct *real_parent;char commTASK_COMM_LEN;struct thread_
5、struct thread;struct files_struct *files;.; 在清单 1 中,可以看到几个预料之中的项,比如执行的状态、堆栈、一组标志、父进程、执行的线程(可以有很多)以及开放文件。我稍后会对其进行详细说明,这里只简单加以介绍。state 变量是一些表明任务状态的比特位。最常见的状态有:TASK_RUNNING 表示进程正在运行,或是排在运行队列中正要运行;TASK_INTERRUPTIBLE 表示进程正在休眠、TASK_UNINTERRUPTIBLE 表示进程正在休眠但不能叫醒;TASK_STOPPED 表示进程停止等等。这些标志的完整列表可以在 ./linux/i
6、nclude/linux/sched.h 内找到。flags 定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用 comm(命令)字段。每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。tasks 字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个 next 指针(指向下一个任务)。进程的地址空间由 mm 和 active_mm 字段表示。
7、mm 代表的是进程的内存描述符,而 active_mm 则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。thread_struct 则用来标识进程的存储状态。此元素依赖于 Linux 在其上运行的特定架构,在 ./linux/include/asm-i386/processor.h 内有这样的一个例子。在此结构内,可以找到该进程自执行上下文切换后的存储(硬件注册表、程序计数器等)。 回页首 进程管理 最大进程数在 Linux 内虽然进程都是动态分配的,但还是需要考虑最大进程数。在内核内最大进程数是由一个称为 max_threads 的符号表示的,它可以在 ./linux/kern
8、el/fork.c 内找到。可以通过 /proc/sys/kernel/threads-max 的 proc 文件系统从用户空间更改此值。 现在,让我们来看看如何在 Linux 内管理进程。在很多情况下,进程都是动态创建并由一个动态分配的 task_struct 表示。一个例外是 init 进程本身,它总是存在并由一个静态分配的 task_struct 表示。在 ./linux/arch/i386/kernel/init_task.c 内可以找到这样的一个例子。Linux 内所有进程的分配有两种方式。第一种方式是通过一个哈希表,由 PID 值进行哈希计算得到;第二种方式是通过双链循环表。循环表
9、非常适合于对任务列表进行迭代。由于列表是循环的,没有头或尾;但是由于 init_task 总是存在,所以可以将其用作继续向前迭代的一个锚点。让我们来看一个遍历当前任务集的例子。任务列表无法从用户空间访问,但该问题很容易解决,方法是以模块形式向内核内插入代码。清单 2 中所示的是一个很简单的程序,它会迭代任务列表并会提供有关每个任务的少量信息(name、pid 和 parent 名)。注意,在这里,此模块使用 printk 来发出结果。要查看具体的结果,可以通过 cat 实用工具(或实时的 tail -f /var/log/messages)查看 /var/log/messages 文件。nex
10、t_task 函数是 sched.h 内的一个宏,它简化了任务列表的迭代(返回下一个任务的 task_struct 引用)。清单 2. 发出任务信息的简单内核模块(procsview.c)#include #include #include int init_module( void ) /* Set up the anchor point */ struct task_struct *task = &init_task; /* Walk through the task list, until we hit the init_task again */ do printk( KERN_INF
11、O * %s %d parent %sn,task-comm, task-pid, task-parent-comm ); while ( (task = next_task(task) != &init_task ); return 0;void cleanup_module( void ) return; 可以用清单 3 所示的 Makefile 编译此模块。在编译时,可以用 insmod procsview.ko 插入模块对象,也可以用 rmmod procsview 删除它。清单 3. 用来构建内核模块的 Makefileobj-m += procsview.oKDIR := /lib
12、/modules/$(shell uname -r)/buildPWD := $(shell pwd)default:$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules 插入后,/var/log/messages 可显示输出,如下所示。从中可以看到,这里有一个空闲任务(称为 swapper)和 init 任务(pid 1)。Nov 12 22:19:51 mtj-desktop kernel: 8503. * swapper 0 parent swapperNov 12 22:19:51 mtj-desktop kernel: 8503. * init 1 pa
13、rent swapperNov 12 22:19:51 mtj-desktop kernel: 8503. * kthreadd 2 parent swapperNov 12 22:19:51 mtj-desktop kernel: 8503. * migration/0 3 parent kthreadd. 注意,还可以标识当前正在运行的任务。Linux 维护一个称为 current 的符号,代表的是当前运行的进程(类型是 task_struct)。如果在 init_module 的尾部插入如下这行代码:printk( KERN_INFO, Current task is %s %d, cu
14、rrent-comm, current-pid ); 会看到:Nov 12 22:48:45 mtj-desktop kernel: 10233. Current task is insmod 6538 注意到,当前的任务是 insmod,这是因为 init_module 函数是在 insmod 命令执行的上下文运行的。current 符号实际指的是一个函数(get_current)并可在一个与 arch 有关的头部中找到(比如 ./linux/include/asm-i386/current.h 内找到)。 回页首 进程创建 系统调用函数您可能已经看到过系统调用的模式了。在很多情况下,系统调用都被命名为 sys_* 并提供某些初始功能以实现调用(例如错误检查或用户空间的行为)。实际的工作常常会委派给另外一个名为 do_* 的函数。 让我们不妨亲自看看如何从用户空间创建一个进程。用户空间任务和内核任务的底层机制是一致的,因为二者最终都会依赖于一个名为 do_fork 的函数来创建新进程。在创建内核线程时,内核会调用一个名为 kernel_thread 的函数(参见 ./linux/arch/i386/kernel/process.c),此函数执行某些初始化后会调用 do_fork。创建用户空间进程的情况与此类似。在用户空间,一个程序会调用 f