【Linux 进程管理】(1)进程基础

    所有的现代操作系统都能够同时运行若干进程,至少用户错觉上是这样。如果系统只有一个 处理器,那么在给定时刻只有一个程序可以运行。在多处理器系统中,可以真正并行运行 的进程数目,取决于物理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操作系统变体上都是可用。

 

 
 
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值