所有的现代操作系统都能够同时运行若干进程,至少用户错觉上是这样。如果系统只有一个 处理器,那么在给定时刻只有一个程序可以运行。在多处理器系统中,可以真正并行运行 的进程数目,取决于物理CPU
的数目。 内核和处理器建立了多任务
的错觉,即可以并行做几种操作,这是通过以很短的间隔在系统运行 的应用程序之间不停切换而做到的。由于切换间隔如此之短,使得用户无法注意到短时间内的停滞, 从而在感观上觉得计算机能够同时做几件事情。
一、进程优先级
进程的运行按时间片调度,分配给进程的时间片份额与 其相对重要性相当。系统中时间的流动对应于圆盘的转动,而CPU
则由圆周旁的“扫描器”表示。最 终效果是,尽管所有的进程都有机会运行,但重要的进程会比次要的得到更多的CPU
时间。
这种方案称之为
抢占式多任务处理(preemptive multitasking)
,各个进程都分配到一定的时间段 可以执行。时间段到期后,内会从进程收回控制权,让一个不同的进程运行,而不考虑前一进程所执行的上一个任务。被抢占进程的运行时环境,即所有CPU寄存器的内容和页表,都会保存起来,因此其执行结果不会丢失。在该进程恢复执行时,其进程环境可以完全恢复。时间片的长度会根据进程重要性(以及因此而分配的优先级)的不同而变化。
二、进程生命周期
当调度器在进程之间切换时,必须知道系统中每个进程的状态。将
CPU
时间分配到无事可做的进 程,显然是没有意义的。进程在各个状态之间的转换也同样重要。
进程可能有以下几种状态。
- 运行:该进程此刻正在执行。
- 等待:进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次 任务切换时选择该进程。
- 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换 时选择该进程。
系统将所有进程保存在一个进程表中,无论其状态是运行、睡眠或等待。但睡眠进程会特别标记 出来,调度器会知道它们无法立即运行(具体实现看之后介绍
)。睡眠进程会分类到若干队列中, 因此它们可在适当的时间唤醒,例如在进程等待的外部事件已经发生时。
上文没有列出的一个特殊的进程状态是所谓的
“僵尸”状态
。顾名思义,这样的进程已经死亡, 但仍然以某种方式活着。实际上,说这些进程死了,是因为其资源(内存、与外设的连接,等等)已 经释放,因此它们无法也决不会再次运行。说它们仍然活着,是
因为进程表中仍然有对应的表项
。
僵尸是如何产生的?其原因在于
UNIX
操作系统下进程创建和销毁的方式。在两种事件发生时, 程序将终止运行。第一,程序必须由另一个进程或一个用户杀死(通常是通过发送SIGTERM
或
SIGKILL 信号来完成,这等价于正常地终止进程);进程的父进程在子进程终止时必须调用或已经调用wait4 (读做wait for
)系统调用。 这相当于向内核证实父进程已经确认子进程的终结。该系统调用使得内核 可以释放为子进程保留的资源。 只有在第一个条件发生(程序终止)而第二个条件不成立的情况下(wait4
),才会出现“僵尸” 状态。在进程终止之后,其数据尚未从进程表删除之前,进程总是暂时处于“僵尸”状态。有时候(例
如,如果父进程编程极其糟糕,没有发出
wait
调用),僵尸进程可能稳定地寄身于进程表中,直至下 一次系统重启。从进程工具(如ps
或
top
)的输出,可以看到僵尸进程。因为残余的数据在内核中占 据的空间极少,所以这几乎不是问题。
三、抢占多任务处理
内核的抢占调度模型建立了一个层次结构,用于判断哪些进程状态可以由其他状态抢占。
- 普通进程总是可能被抢占,甚至是由其他进程抢占。在一个重要进程变为可运行时,例如编 辑器接收到了等待已久的键盘输入,调度器可以决定是否立即执行该进程,即使当前进程仍 然在正常运行。对于实现良好的交互行为和低系统延迟,这种抢占起到了重要作用。
- 如果系统处于核心态并正在处理系统调用,那么系统中的其他进程是无法夺取其CPU时间的。 调度器必须等到系统调用执行结束,才能选择另一个进程执行,但中断可以中止系统调用。
- 中断可以暂停处于用户状态和核心态的进程。中断具有最高优先级,因为在中断触发后需要 尽快处理。
在内核
2.5
开发期间,一个称之为
内核抢占
(
kernel preemption
)的选项添加到内核。 该选项支持在紧急情况下切换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)。 尽管内核会试图尽快执行系统调用,但对于依赖恒定数据流的应用程序来说,系统调用所需的时间仍 然太长了。内核抢占可以减少这样的等待时间,因而保证“更平滑的”程序执行。但该特性的代价是 增加内核的复杂度,因为接下来有许多数据结构需要针对并发访问进行保护,即使在单处理器系统上 也是如此。
四、进程结构体
task_struct
定义如下,当然,这里是简化版本:
<sched.h>
struct task_struct {
volatile long state; /* -1
表示不可运行,
0
表示可运行,
>0
表示停止
*/
void *stack;
atomic_t usage;
unsigned long flags; /*
每进程标志,下文定义
*/
unsigned long ptrace;
int lock_depth;
/*
大内核锁深度
*/
int prio, static_prio, normal_prio;
struct list_head run_list;
const struct sched_class *sched_class;
struct sched_entity se;
unsigned short ioprio;
unsigned long policy;
cpumask_t cpus_allowed;
unsigned int time_slice;
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif
struct list_head tasks;
/*
* ptrace_list/ptrace_children
链表是
ptrace
能够看到的当前进程的子进程列表。
*/
struct list_head ptrace_children;
struct list_head ptrace_list;
struct mm_struct *mm, *active_mm;
/*
进程状态
*/
struct linux_binfmt *binfmt;
long exit_state;
int exit_code, exit_signal;
int pdeath_signal; /*
在父进程终止时发送的信号
*/
unsigned int personality;
unsigned did_exec:1;
pid_t pid;
pid_t tgid;
/*
*
分别是指向(原)父进程、最年轻的子进程、年幼的兄弟进程、年长的兄弟进程的指针。
*
(
p->father
可以替换为
p->parent->pid
)
*/
struct task_struct *real_parent; /*
真正的父进程(在被调试的情况下)
*/
struct task_struct *parent;
/*
父进程
*/
/*
* children/sibling
链表外加当前调试的进程,构成了当前进程的所有子进程
*/
struct list_head children; /*
子进程链表
*/
struct list_head sibling; /* 兄弟进程链表 */
/*
连接到父进程的子进程链表
*/
struct task_struct *group_leader; /*
线程组组长
*/
/*
PID
与
PID
散列表的联系。
*/
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct completion *vfork_done; /*
用于
vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
unsigned long rt_priority;
cputime_t utime, stime, utimescaled, stimescaled;
unsigned long nvcsw, nivcsw; /*
上下文切换计数
*/
struct timespec start_time;
/*
单调时间
*/
struct timespec real_start_time; /*
启动以来的时间
*/
/*
内存管理器失效和页交换信息,这个有一点争论。它既可以看作是特定于内存管理器的,
也可以看作是特定于线程的
*/
unsigned long min_flt, maj_flt;
cputime_t it_prof_expires, it_virt_expires;
unsigned long long it_sched_expires;
struct list_head cpu_timers[3];
/*
进程身份凭据
*/
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
struct group_info *group_info;
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
unsigned keep_capabilities:1;
struct user_struct *user;
char comm[TASK_COMM_LEN]; /*
除去路径后的可执行文件名称 用[gs]et_task_comm
访问(其中用
task_lock()
锁定它) 通常由flush_old_exec
初始化
*/
/*
文件系统信息
*/
int link_count, total_link_count;
/*
ipc
相关
*/
struct sysv_sem sysvsem;
/*
当前进程特定于
CPU
的状态信息
*/
struct thread_struct thread;
/*
文件系统信息
*/
struct fs_struct *fs;
/*
打开文件信息
*/
struct files_struct *files;
/*
命名空间
*/
struct nsproxy *nsproxy;
/*
信号处理程序
*/
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /*
用
TIF_RESTORE_SIGMASK
恢复
*/
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
#ifdef CONFIG_SECURITY
void *security;
#endif
/*
线程组跟踪
*/
u32 parent_exec_id;
u32 self_exec_id;
/*
日志文件系统信息
*/
void *journal_info;
/*
虚拟内存状态
*/
struct reclaim_state *reclaim_state;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /*
由
ptrace
使用。
*/
...
};
要弄清楚该结构中信息的数量诚然很困难。但该结构的内容可以分解为各个部分,每个部分表示 进程的一个特定方面。
- 状态和执行信息,如待决信号、使用的二进制格式(和其他系统二进制格式的任何仿真信息)、 进程ID号(pid)、到父进程及其他有关进程的指针、优先级和程序执行有关的时间信息(例如 CPU时间)。
- 有关已经分配的虚拟内存的信息。2.3 进程表示
- 进程身份凭据,如用户ID、组ID以及权限①等。可使用系统调用查询(或修改)这些数据。在 描述相关的特定子系统时,我会更详细地阐述。
- 使用的文件包含程序代码的二进制文件,以及进程所处理的所有文件的文件系统信息,这些 都必须保存下来。
- 线程信息记录该进程特定于CPU的运行时间数据(该结构的其余字段与所使用的硬件无关)。
- 在与其他应用程序协作时所需的进程间通信有关的信息。
- 该进程所用的信号处理程序,用于响应到来的信号。
state
指定了进程的当前状态,可使用下列值(这些是预处理器常数,定义在
<sched.h>
中)。
- TASK_RUNNING意味着进程处于可运行状态。这并不意味着已经实际分配了CPU。进程可能会 一直等到调度器选中它。该状态确保进程可以立即运行,而无需等待外部事件。
- TASK_INTERRUPTIBLE是针对等待某事件或其他资源的睡眠进程设置的。在内核发送信号给该 进程表明事件已经发生时,进程状态变为TASK_RUNNING,它只要调度器选中该进程即可恢复 执行。
- TASK_UNINTERRUPTIBLE用于因内核指示而停用的睡眠进程。它们不能由外部信号唤醒,只能 由内核亲自唤醒。
- TASK_STOPPED表示进程特意停止运行,例如,由调试器暂停。
- TASK_TRACED本来不是进程状态,用于从停止的进程中,将当前被调试的那些(使用ptrace机制)与常规的进程区分开来。 下列常量既可以用于struct task_struct的进程状态字段,也可以用于exit_state字段,后者 明确地用于退出进程。
- EXIT_ZOMBIE如上所述的僵尸状态。
- EXIT_DEAD状态则是指wait系统调用已经发出,而进程完全从系统移除之前的状态。只有多 个线程对同一个进程发出wait调用时,该状态才有意义。
(1)进程类型
新进程是使用
fork
和 exec系统调用产生的。
- fork生成当前进程的一个相同副本,该副本称之为子进程。原进程的所有资源都以适当的方 式复制到子进程,因此该系统调用之后,原来的进程就有了两个独立的实例。这两个实例的 联系包括:同一组打开文件、同样的工作目录、内存中同样的数据(两个进程各有一份副本), 等等。此外二者别无关联。①
- exec从一个可执行的二进制文件加载另一个应用程序,来代替当前运行的进程。换句话说, 加载了一个新程序。因为exec并不创建新进程,所以必须首先使用fork复制一个旧的程序, 然后调用exec在系统上创建另一个应用程序。 上述两个调用在所有UNIX操作系统变体上都是可用。