- 所有现代操作系统都可以"同时"运行若干进程。
- 只有一个处理器的系统只能在给定时刻运行一个程序。
- 在多处理器系统中可以真正并行运行的进程数目取决于物理CPU的数目。
- 为什么内核和处理器可以给我们建立多任务的错觉,即可以并行运行多个程序?
- 短时间内系统运行的应用程序不停切换。切换时间间隔很短,短到用户无法察觉。
- 这种系统管理方式引起了几个问题,内核必须解决下面这些问题
- 除非明确地要求,否则应用程序不能彼此干扰 。由于
Linux
是一个多用户系统,它也必须确保程序不能读取或修改其他程序的内存 ,否则就很容易访问其他用户的私有数据。(可以使用存储保护实现,将在第3
章进行处理) - CPU时间必须在各种应用程序之间尽可能公平地共享。
- 除非明确地要求,否则应用程序不能彼此干扰 。由于
- 本章主要介绍内核共享CPU时间 的方法,以及如何在进程之间切换 。这里有两个任务,其执行是相对独立的。
- 内核必须决定为各个进程分配多长时间,何时切换到下一个进程 。这又引出了哪个进程是下一个的问题。此类决策是平台无关的。
- 在内核从进程
A
切换到进程B
时,必须确保进程B
的执行环境与上一次撤销其处理器资源时完全相同 。例如,处理器寄存器的内容和虚拟地址空间的结构必须与此前相同 。(这项工作与处理器极度相关。不能只用C语言实现,还需要汇编代码的帮助) - 上面两个任务是称之为调度器的内核子系统的职责。
CPU
时间如何分配取决于调度器策略 ,这与用于在各个进程之间切换的任务切换机制完全无关。
1、进程优先级
-
进程可分为实时进程 和非实时进程 ,而实时进程又可以分为硬实时进程 和软实时进程。
- 硬实时进程 :有严格的时间限制,某些任务必须在指定的时限内完成 。
- 一个例子:飞控软件
- 注意,这并不意味着所要求的时间范围特别短,而是要保证在极端的情况下也决不会超过某一时间范围。
- Linux在主流的内核中不支持 硬实时进程,而一些修改版本比如
RTLinux
则支持。
- 软实时进程 :是硬实时进程的一种弱化形式。尽管仍然需要快速得到结果,但稍微晚一点不会造成重大后果。
- 普通进程:大多数进程,没有特定时间约束。但仍然可以根据重要性来分配优先级。
- 硬实时进程 :有严格的时间限制,某些任务必须在指定的时限内完成 。
-
CPU时间分配简图
- 进程的运行按时间片调度,分配给进程的时间片份额与其相对重要性相当。
- 系统中时间的流动对应于圆盘的转动,CPU由圆周旁的"扫描器"表示。
- 这种方案称之为
抢占式多任务处理(preemptive multitasking)
,各个进程都分配到一定的时间段可以执行。时间段到期后,内核会从进程收回控制权,让一个不同的进程运行,而不考虑前一进程所执行的上一个任务。 - 被抢占进程的运行时环境,即所有CPU寄存器的内容和页表,都会保存起来,因此其执行结果不会丢失。在该进程恢复执行时,其进程环境可以完全恢复。时间片的长度会根据进程重要性(以及因此而分配的优先级)的不同而变化。
-
上图的CPU时间分配简图是不准确的,因为它没有考虑下面几个重要问题
- 进程在某些时间可能因为无事可做而无法立即执行,这样会导致CPU资源的巨大浪费,所以这样的进程应该避免。图中假定的所有进程都是可以立即运行的,显然不现实。
- Linux支持不同的调度类别(在进程之间完全公平的调度和实时调度),调度时也必须考虑到这一点。
- 此外,在有重要的进程变为就绪状态可以运行时,有一种选项是抢占当前的进程,图中也没有反映出这一点。
-
调度器代码的更新迭代
Linux2.5
使用的是O(1)
调度器。O(1)
调度器一个特别的性质是,它可以在常数时间内完成其工作,不依赖于系统上运行的进程数目。Linux2.6.23
使用的是完全公平调度器(CFS,completely fair scheduler)
,该调度器的关键特性是,它试图尽可能地模仿理想情况下的公平调度 。此外,它不仅可以调度单个进程,还能够处理更一般性的调度实体(scheduling entity)
。例如,该调度器分配可用时间时,可以首先在不同用户之间分配,接下来在各个用户的进程之间分配。
2、进程生命周期
- 进程可能有以下几种状态。
- 运行:该进程此刻正在执行。
- 等待:进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。
- 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。
- 系统将所有进程保存在一个进程表中,无论其状态是运行、睡眠或等待 。但睡眠进程会特别标记出来,调度器会知道它们无法立即运行(具体实现,请参考
3
节)。睡眠进程会分类到若干队列中,因此它们可在适当的时间唤醒,例如在进程等待的外部事件已经发生时。 - 下图描述了进程的几种状态及其转换。
- 路径① :如果进程必须等待事件,则其状态
运行
改变为睡眠
。 - 路径② :在调度器决定从该进程收回CPU资源时(可能的原因稍后讲述),过程状态从
运行
改变为等待
, - 路径③ :在所等待的事件发生后,处于
睡眠
状态进程先变回到等待
状态,然后重新回到正常循环。 - 路径④ :在分配CPU时间之后,进程由
等待
状态改变为运行
- 路径⑤ :在程序执行终止(例如,用户关闭应用程序)后,过程状态由
运行
变为终止
- 路径① :如果进程必须等待事件,则其状态
- 上文没有列出
僵尸
状态,下面是关于僵尸
状态的解释:- 什么是
僵尸
状态?- 进程已经死亡,但仍然以某种方式活着。实际上,说这些进程死了,是因为其资源(内存、与外设的连接,等等)已经释放 ,因此它们无法也决不会再次运行。说它们仍然活着,是因为进程表中仍然有对应的表项。
僵尸
状态产生的原因- 条件1 :程序必须由另一个进程或一个用户杀死(通常是通过发送
SIGTERM
或SIGKILL
信号来完成,这等价于正常地终止进程); - 条件2 :进程的父进程在子进程终止时必须调用或已经调用
wait4
(读做wait for
)系统调用。 这相当于向内核证实父进程已经确认子进程的终结。该系统调用使得内核可以释放为子进程保留的资源。 - 只有在条件1 发生(程序终止)而条件2 不成立的情况下(
wait4
),才会出现"僵尸"状态
- 条件1 :程序必须由另一个进程或一个用户杀死(通常是通过发送
僵尸
进程残留带来的影响- 在进程终止之后,其数据尚未从进程表删除之前,进程总是暂时处于
僵尸
状态。 - 有时候(例如,如果父进程编程极其糟糕,没有发出
wait
调用),僵尸
进程可能稳定地寄身于进程表中,直至下一次系统重启。 - 从进程工具(如
ps
或top
)的输出,可以看到僵尸
进程。因为残余的数据在内核中占据的空间极少,所以这几乎不是问题。
- 在进程终止之后,其数据尚未从进程表删除之前,进程总是暂时处于
- 什么是
补充A:抢占式多任务处理
- Linux进程管理的结构还需要另外两种进程状态选项:用户状态 和核心态 ,什么是用户状态和核心态?
- 进程通常都处于用户状态,只能访问自身的数据,无法干扰系统中的其他应用程序,甚至也不会注意到自身之外其他程序的存在。
- 如果进程想要访问系统数据或功能(后者管理着所有进程之间共享的资源,例如文件系统空间),则必须切换到核心态。
- 用户状态切换到核心态的方法有哪些?
- 第一种方法:系统调用。系统调用是由用户应用程序有意调用的。
- 第二种方法:中断。发生中断时,其发生或多或少是不可预测的,用户状态切换到核心态的过程是自动触发的。处理中断的操作,通常与中断发生时执行的进程无关。
- 内核的抢占调度模型建立了一个层次结构,用于判断哪些进程状态可以由其他状态抢占。
- 普通进程总是可能被抢占,甚至是由其他进程抢占。在一个重要进程变为可运行时,例如编辑器接收到了等待已久的键盘输入,调度器可以决定是否立即执行该进程,即使当前进程仍然在正常运行。对于实现良好的交互行为和低系统延迟,这种抢占起到了重要作用。
- 如果系统处于核心态并正在处理系统调用,那么系统中的其他进程是无法夺取其CPU时间的 。调度器必须等到系统调用执行结束,才能选择另一个进程执行,但中断可以中止系统调用(在进行重要的内核操作时,可以停用几乎所有的中断)。
- 中断可以暂停处于用户状态和核心态的进程 。中断具有最高优先级,因为在中断触发后需要
尽快处理。
- Linux内核支持
内核抢占(kernel preemption)
选项。该选项支持在紧急情况下切换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)。
3、进程表示
-
Linux
内核涉及进程和程序的所有算法都围绕一个名为task_struct
的数据结构建立,该结构定义在include/sched.h
中。task_struct
包含很多成员,将进程与各个内核子系统联系起来,下面是task_struct
的定义(简化版本):
<sched.h>cstruct 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
时间)。 - 有关已经分配的虚拟内存的信息。
- 进程身份凭据,如用户
ID
、组ID
以及权限等。可使用系统调用查询(或修改)这些数据。 - 使用的文件包含程序代码的二进制文件,以及进程所处理的所有文件的文件系统信息,这些都必须保存下来。
- 线程信息记录该进程特定于
CPU
的运行时间数据(该结构的其余字段与所使用的硬件无关)。 - 在与其他应用程序协作时所需的进程间通信有关的信息。
- 该进程所用的信号处理程序,用于响应到来的信号。
- 状态和执行信息,如待决信号、使用的二进制格式(和其他系统二进制格式的任何仿真信息)、进程
- 本章将介绍
task_struct
中对进程管理的实现特别重要的一些成员,暂时忽略其他变量。
- 该结构非常复杂,可将其分解成各个部分:
-
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
调用时,该状态才有意义。
-
-
Linux提供资源限制(
resource limit
,rlimit
)机制,对进程使用系统资源施加某些限制。该机制利用了task_struct
中的rlim
数组,数组项类型为struct rlimit
。这部分内容在《UNIX环境高级编程》7.11节中已经介绍到,可以结合起来一起看。
<resource.h>cstruct rlimit { unsigned long rlim_cur; unsigned long rlim_max; }
-
rlim_cur
是进程当前的资源限制,也称之为软限制(soft limit)。 -
rlim_max
是该限制的最大容许值,因此也称之为硬限制(hard limit) -
系统调用
setrlimit
来增减当前限制,但不能超出rlim_max
指定的值。getrlimits
用于检查当前限制。
-
因为限制涉及内核的各个不同部分,内核必须确认子系统遵守了相应限制。这也是为什么在本书以后几章里我们会屡次遇到
rlimit
的原因。 -
如果某一类资源没有使用限制(几乎所有资源的默认设置),则将
rlim_max
设置RLIM_INFINITY
。例外情况包括下面所列举的。- 打开文件的数目(
RLIMIT_NOFILE
,默认限制在1024
)。 - 每用户的最大进程数(
RLIMIT_NPROC
),定义为max_threads/2
。max_threads
是一个全局变量,指定了在把1/8
可用内存用于管理线程信息的情况下,可以创建的线程数目。在计算时,提前给定了20
个线程的最小可能内存用量。
- 打开文件的数目(
-
init
进程的限制在系统启动时即生效,定义在include/asm-generic-resource.h
中的INIT_RLIMITS
。 -
可以用过以下方式查看当前的
rlimit
值。bashlh@LH_LINUX:~$ cat /proc/self/limits Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes Max processes 31574 31574 processes Max open files 1024 1048576 files Max locked memory 65536 65536 bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 31574 31574 signals Max msgqueue size 819200 819200 bytes Max nice priority 0 0 Max realtime priority 0 0 Max realtime timeout unlimited unlimited us
-
3.1、进程类型
- 典型的
UNIX
进程包括:由二进制代码组成的应用程序 、单线程 (计算机沿单一路径通过代码,不会有其他路径同时运行)、分配给应用程序的一组资源 (如内存、文件等)。新进程是使用fork
和exec
系统调用产生的。相关内容在《UNIX环境高级编程》8.3和8.10节中已经介绍到。fork
生成当前进程的一个相同副本,该副本称之为子进程。原进程的所有资源都以适当的方式复制到子进程,因此该系统调用之后,原来的进程就有了两个独立的实例。这两个实例的联系包括:同一组打开文件、同样的工作目录、内存中同样的数据(两个进程各有一份副本) ,等等。此外二者别无关联。(Linux使用了写时复制机制 ,直至新进程对内存页执行写操作才会复制内存页面 ,这比在执行fork
时盲目地立即复制所有内存页要更高效。父子进程内存页之间的联系,只有对内核才是可见的,对应用程序是透明的。)exec
从一个可执行的二进制文件加载另一个应用程序,来代替当前运行的进程 。换句话说,加载了一个新程序。因为exec
并不创建新进程,所以必须首先使用fork
复制一个旧的程序,然后调用exec
在系统上创建另一个应用程序。
Linux
还提供了clone
系统调用。clone
的工作原理基本上与fork
相同,但新进程不是独立于父进程的,而可以与其共享某些资源。可以指定需要共享和复制的资源种类 ,例如,父进程的内存数据、打开文件或安装的信号处理程序。此外,clone
用于实现线程(需要用户空间库才能提供完整的实现)。