如果系统只有一个处理器,那么给定时刻只有一个程序可以运行。在多处理器系统中,真正并行运行的进程数目取决于物理CPU的数目。内核和处理器建立了多任务的错觉,是通过以很短的间隔在系统运行的应用程序之间不停切换做到的。由此,以下两个问题必须由内核解决:除非明确要求,否则应用程序不能彼此干扰;CPU时间必须在各种应用程序之间尽可能公平共享(一些程序可能比其他程序更重要)。本篇博文主要涉及内核共享CPU时间的方法以及如何在进程之间切换(内核为各进程分配时间,保证切换之后从上次撤销其资源时执行环境完全相同)。

一、进程优先级

并非所有进程的重要程度都相同,对于进程,首先比较粗糙的划分,进程可以分为实时进程和非实时进程。

  • 硬实时进程:有严格的时间限制,某些任务必须在指定时限内完成。比如汽车的安全气囊,一旦发生碰撞,必须保证在一定时间内触发,超过时限则产生灾难性的后果。主流的Linux内核不支持实时处理,但有一些修改版本如RTLinux、Xenomai、RATI提供了这些特性,这些方案中,将Linux内核作为独立的“进程”运行处理次要软件,实时工作在Linux内核外部完成。
  • 软实时进程:软实时进程是硬实时进程的一种弱化形式,优先于其他普通进程,稍微晚一点不会造成巨大影响。比如对CD的写入操作。
  • 普通进程:没有特定时间约束的进程,可以根据重要性对其进行分配优先级。

深入Linux内核架构——进程管理和调度(上)

图1 通过时间片分配CPU时间调度示意图

图1是CPU分配时间的一个简图。进程运行按时间片调度,分配进程的时间片额与其相对重要性相当。系统中时间的流动对应于圆盘的转动,重要的进程会比次要的进程得到更多CPU时间,进程被切换时,所有的CPU寄存器内容和页表都会被保存,下次该进程恢复执行时,其执行环境可以完全恢复。这种简化模型忽略了一些进程状态相关的信息,不能使CPU时间利益回报尽可能最大化。但是为调度器的质量确立一种定量标准非常困难。自Linux内核诞生以来,调度器的代码已经重写了好几次。按时间先后顺序,主要有O(n)调度器,O(1)调度器和CFS(completely fair scheduler)调度器。详细区别戳这里

二、进程生命周期

进程并不总是可以立即运行,有时候它需要等待来自外部信号源、不受其控制的事件(如文本编辑等待输入)。在调度器进行进程切换时,必须知道每个进程的状态,因为将CPU事件分配给无事可做的进程没有意义,进程在各个状态之间的转换也同样重要。

深入Linux内核架构——进程管理和调度(上)

图2 进程状态之间的切换示意图

进程可能存在的状态有:运行、等待和睡眠。图2描述了进程的几种状态及其转换。除了图中所示的几种状态以外,还有一种状态被称为僵尸态。

  • 运行:该进程正在运行。
  • 等待(就绪):进程能够运行,但没有得到许可,因为CPU分配给了另一个进程。调度器可能在下一次任务切换时选择该进程。
  • 睡眠:进程正在睡眠无法运行(“睡眠”状态有两种)。因为它在等待一个外部事件,调度器无法在任务切换时选择该进程。
  • 僵尸:进程已经死亡,但是它的数据还没有从进程表中删除。(在UNIX操作系统下销毁进程需要两步,第一步由另一个进程或一个用户杀死(通过信号完成);第二步是进程的父进程在子进程终止时必须调用或已经调用wait4系统调用,使内核释放为子进程保留的资源。当条件一发生,第二个条件不成立的情况,便会出现“僵尸”状态)

为了维持系统中现存的各个进程,防止它们与系统其他部分相互干扰,Linux进程管理结构中还需要两种进程状态选项:用户状态核心态。进程通常处于用户状态,只能访问自身的数据,无法干扰系统中其他进程。如果进程想要访问系统数据,则必须切换到核心态,这种访问必须经由明确定义的路径(系统调用)。从用户状态进入核心态的第二种方法是通过中断,此时切换是自动触发的,处理中断操作,通常与中断发生时执行的程序无关。(系统调用是由用户应用程序有意调用的,中断则是不可预测的)

内核的抢占调度模型是优先让优先级高的进程占用CPU,它建立了一个层次结构,用于判断哪些进程状态可由其他状态抢占。

  • 普通进程总是可能被抢占,甚至由其他进程抢占。
  • 如果系统处于核心态并正在处理系统调用,那么其他进程是无法夺取CPU时间的。
  • 中断可以暂停处于用户态和和心态的进程,具有最高优先级。

在内核2.5开发期间,内核抢占(kernel preemption)选项被添加到内核,它支持紧急情况下切换到另一个进程,甚至当前进程处于系统调用也行。内核抢占可以减少等待时间,但代价是增加了内核的复杂度,因为抢占时有许多数据结构需要针对并发访问进行保护。

三、进程表示

Linux内核涉及进程和程序的所有算法都围绕数据结构task_struct建立,该结构定义在include/sched.h中。task_struct包含很多成员,将进程与各内核子系统联系起来。task_struct定义的简化版本如下:

  1 struct task_struct {
  2     volatile long state; /* -1表示不可运行,0表示可运行,>0表示停止 */
  3     void *stack;
  4     atomic_t usage;
  5     unsigned long flags; /* 每进程标志,下文定义 */
  6     unsigned long ptrace;
  7     int lock_depth; /* 大内核锁深度 */
  8     int prio, static_prio, normal_prio;
  9     struct list_head run_list;
 10     const struct sched_class *sched_class;
 11     struct sched_entity se;
 12     unsigned short ioprio;
 13     unsigned long policy;
 14     cpumask_t cpus_allowed;
 15     unsigned int time_slice;
 16 #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
 17     struct sched_info sched_info;
 18 #endif
 19     struct list_head tasks;
 20 /*
 21 * ptrace_list/ptrace_children链表是ptrace能够看到的当前进程的子进程列表。
 22 */
 23     struct list_head ptrace_children;
 24     struct list_head ptrace_list;
 25     struct mm_struct *mm, *active_mm;
 26 /* 进程状态 */
 27     struct linux_binfmt *binfmt;
 28     long exit_state;
 29     int exit_code, exit_signal;
 30     int pdeath_signal; /* 在父进程终止时发送的信号 */
 31     unsigned int personality;
 32     unsigned did_exec:1;
 33     pid_t pid;
 34     pid_t tgid;
 35 /*
 36 * 分别是指向(原)父进程、最年轻的子进程、年幼的兄弟进程、年长的兄弟进程的指针。
 37 *(p->father可以替换为p->parent->pid)
 38 */
 39     struct task_struct *real_parent; /* 真正的父进程(在被调试的情况下) */
 40     struct task_struct *parent; /* 父进程 */
 41 /*
 42 * children/sibling链表外加当前调试的进程,构成了当前进程的所有子进程
 43 */
 44     struct list_head children; /* 子进程链表 */
 45     struct list_head sibling; /* 连接到父进程的子进程链表 */
 46     struct task_struct *group_leader; /* 线程组组长 */
 47 /* PID与PID散列表的联系。 */
 48     struct pid_link pids[PIDTYPE_MAX];
 49     struct list_head thread_group;
 50     struct completion *vfork_done; /* 用于vfork() */
 51     int __user *set_child_tid; /* CLONE_CHILD_SETTID */
 52     int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
 53     unsigned long rt_priority;
 54     cputime_t utime, stime, utimescaled, stimescaled;
 55     unsigned long nvcsw, nivcsw; /* 上下文切换计数 */
 56     struct timespec start_time; /* 单调时间 */
 57     struct timespec real_start_time; /* 启动以来的时间 */
 58 /* 内存管理器失效和页交换信息,这个有一点争论。它既可以看作是特定于内存管理器的,
 59 也可以看作是特定于线程的 */
 60     unsigned long min_flt, maj_flt;
 61     cputime_t it_prof_expires, it_virt_expires;
 62     unsigned long long it_sched_expires;
 63     struct list_head cpu_timers[3];
 64 /* 进程身份凭据 */
 65     uid_t uid,euid,suid,fsuid;
 66     gid_t gid,egid,sgid,fsgid;
 67     struct group_info *group_info;
 68     kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
 69     unsigned keep_capabilities:1;
 70     struct user_struct *user;
 71     char comm[TASK_COMM_LEN]; /* 除去路径后的可执行文件名称-用[gs]et_task_comm访问(其中用task_lock()锁定它)-通常由flush_old_exec初始化 */
 72 /* 文件系统信息 */
 73     int link_count, total_link_count;
 74 /* ipc相关 */
 75     struct sysv_sem sysvsem;
 76 /* 当前进程特定于CPU的状态信息 */
 77     struct thread_struct thread;
 78 /* 文件系统信息 */
 79     struct fs_struct *fs;
 80 /* 打开文件信息 */
 81     struct files_struct *files;
 82 /* 命名空间 */
 83     struct nsproxy *nsproxy;
 84 /* 信号处理程序 */
 85     struct signal_struct *signal;
 86     struct sighand_struct *sighand;
 87     sigset_t blocked, real_blocked;
 88     sigset_t saved_sigmask; /* 用TIF_RESTORE_SIGMASK恢复 */
 89     struct sigpending pending;
 90     unsigned long sas_ss_sp;
 91     size_t sas_ss_size;
 92     int (*notifier)(void *priv);
 93     void *notifier_data;
 94     sigset_t *notifier_mask;
 95 #ifdef CONFIG_SECURITY
 96     void *security;
 97 #endif
 98 /* 线程组跟踪 */
 99     u32 parent_exec_id;
100     u32 self_exec_id;
101 /* 日志文件系统信息 */
102     void *journal_info;
103 /* 虚拟内存状态 */
104     struct reclaim_state *reclaim_state;
105     struct backing_dev_info *backing_dev_info;
106     struct io_context *io_context;
107     unsigned long ptrace_message;
108     siginfo_t *last_siginfo; /* 由ptrace使用。*/
109 ...
110 };
task_struct

相关文章: