690 likes | 868 Views
第 2 章 Linux 内核. —— 进程管理. 本章主要介绍: 进程概念 进程的组成 进程的状态和调度 进程间关系 中断处理与定时器 系统调用 进程间通信. 1 进程概念. 20 世纪 60 年代,进程 (process) 一词首先在麻省理工学院的 MULTICS 和 IBM 的 CTSS/360 系统中被引入。 对进程下个准确定义不容易,但有必要强调一下进程具有的两个重要特性。. 1. 独立性. 进程 是系统中独立存在的实体,它可以拥有自己独立的资源,比如文件和设备描述符等。
E N D
第2章 Linux内核 ——进程管理
本章主要介绍: • 进程概念 • 进程的组成 • 进程的状态和调度 • 进程间关系 • 中断处理与定时器 • 系统调用 • 进程间通信
1 进程概念 20世纪60年代,进程(process)一词首先在麻省理工学院的MULTICS和IBM的CTSS/360系统中被引入。 对进程下个准确定义不容易,但有必要强调一下进程具有的两个重要特性。
1. 独立性 进程是系统中独立存在的实体,它可以拥有自己独立的资源,比如文件和设备描述符等。 在没有经过进程本身允许的情况下,其他进程不能访问到这些资源。这一点上和线程有很大的不同。 线程是共享资源的程序实体,创建一个线程所花费的系统开销要比创建一个进程小得多。
2. 动态性 进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。 在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
由于以上两个性质,又可以衍生出进程的第三个重要特性,即并发性。由于以上两个性质,又可以衍生出进程的第三个重要特性,即并发性。 若干个进程可以在单处理机状态上并发执行。注意并发性(concurrency)和多处理机并行(parallel)是两个不同的概念。
并行指在同一时刻内,有多条指令在多个处理机上同时执行;并行指在同一时刻内,有多条指令在多个处理机上同时执行; 并发指在同一时刻内可能只有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
2 进程的组成 作为申请系统资源的基本单位,进程必须有一个对应的物理内存空间。 而对这样的一块空间,首先要用数据结构进行描述,才能进一步对之进行管理。
在Linux中,进程以进程号PID(process ID)作为标识。 任何对进程进行的操作都要给予其相应的PID号。 每个进程都属于一个用户,进程要配备其所属的用户编号UID。 此外,每个进程都属于多个用户组,所以进程还要配备其归属的用户组编号GID的数组
UID和GID都分4种, UID包括uid,euid,suid和fsuid, GID包括gid,egid,sgid和fsgid。 一般来说 uid=euid=fsuid, gid=egid=fsgid。
进程标识: • uid和gid • euid和egid又称为有效的uid和gid。 • suid和sgid • fsuid和fsgid
进程运行的环境称为进程上下文(context)。 Linux中进程的上下文由进程控制块PCB(process control block)、正文段(text segment)、数据段(data segment)以及用户堆栈(stack)组成。
—个称做进程表(process table)的链表结构将系统中所有的PCB块联系起来,如图2-1所示。
系统每次访问一个进程时,内核根据PID在进程表中查找相应的进程PCB块(具体查找过程通过一个PID的hash表实现),再通过PCB块找到其对应的代码段与数据段,并进行操作。系统每次访问一个进程时,内核根据PID在进程表中查找相应的进程PCB块(具体查找过程通过一个PID的hash表实现),再通过PCB块找到其对应的代码段与数据段,并进行操作。
Linux系统信号 信号主要用于通知进程异步事件的发生。在Linux中可以识别29种不同的信号,这些信号中的大部分都有了预先定义好的意义, 进程可以显式的用kill或killpg系统调用来向另一个进程发信号。
进程可以通过提供信号处理函数来取代对于任意信号的缺省反应,这种缺省反应一般都是终止进程。进程可以通过提供信号处理函数来取代对于任意信号的缺省反应,这种缺省反应一般都是终止进程。 信号发生时,内核中断当前的进程,进程执行处理函数来响应信号,结束后恢复正常的进程处理。 信号有自己的名称和特定的编号,见表3-1所示。
进程状态 进程是一个动态的实体,故而它是有生命的。 从创建到消亡,是一个进程的整个生命周期。 在这个周期中,进程可能会经历各种不同的状态。 一般来说,所有进程都要经历以下3种状态。
◆ 就绪(ready)态: 指进程已经获得所有所需的其他资源,并正在申请处理机资源,准备开始运行。 这种情况下,称进程处于就绪态。
◆ 阻塞(blocked)态: 指进程因为需要等待所需资源而放弃处理机,或者进程本不拥有处理机,且其他资源也没有满足,从而即使得到处理机资源也不能开始运行。 这种情况下,称进程处于阻塞态。阻塞状态又称休眠状态或者等待状态。
◆ 运行态: 进程得到了处理机,并不需要等待其他任何资源,正在执行的状态,称之为运行态。 只有在运行态时,进程才可以使用所申请到的资源。
进程结构 • task_struct数据结构 • 进程标志符PID • 进程所占的内存区域 • 相关文件的文件描述符 • 安全信息 • 进程环境 • 信号处理 • 资源安排 • 同步处理 • 进程状态 include/Linux/sched.h
进程的状态 • task_struct 中的state 表示进程当前的状态 Linux中的进程主要有5个状态:
◆ RUNNING: 正在运行,或者在就绪队列中等待运行的进程。 也就是上面提到的运行态和就绪态进程的综合。 一个进程处于RUNNING状态,并不代表它一定在被执行。
由于在多任务系统中,各个就绪进程需要并发执行,所以在某个特定时刻,这些处于运行状态的进程之中,只有一个能够得到处理机,而其他进程必须在一个就绪队列中等待。由于在多任务系统中,各个就绪进程需要并发执行,所以在某个特定时刻,这些处于运行状态的进程之中,只有一个能够得到处理机,而其他进程必须在一个就绪队列中等待。 即使是在多处理机的系统中,Linux也只能同时让一个处理机执行任务。
◆ UNINTERRUPTABLE: 不可中断阻塞状态。 处于这种状态的进程正在等待队列中,当资源有效时,可由操作系统进行唤醒,否则,将一直处于等待状态。
◆ INTERRUPTABLE: 可中断阻塞状态。 与不可中断阻塞状态一样,处于这种状态的进程也在等待队列中,当资源有效时,可以由操作系统进行唤醒。 与不可中断阻塞状态有所不同的是,处于此状态中的进程亦可被其他进程的信号和定时中断唤醒。
◆ STOPPED: 挂起状态。 进程被暂停,需要通过其他进程的信号才能被唤醒。 导致这种状态的原因有两种。 其一是受到了相关信号(SIGSTOP、SIGSTP、SIGTTIN 或SIGTTOU)的反应; 其二是受到父进程ptrace调用的控制,而暂时将处理机交给控制进程。
◆ ZOMBIE: 僵尸状态。 表示进程结束但尚未消亡的一种状态。 此时进程已经结束运行并释放大部分资源,但尚未释放进程控制块。
进程调度 调度程序(scheduler)用来实现进程状态之间的转换。 在Linux中,调度程序由系统调用schedule()来完成。 schedule()是一个怪异的函数,它与一般C语言函数不同,因为它的调用和返回不在同一个进程中。
用户进程由fork()系统调用实现。用户进程由do_fork()函数创建,它也是fork系统调用的执行者。用户进程由fork()系统调用实现。用户进程由do_fork()函数创建,它也是fork系统调用的执行者。 fork()创建一个新的进程,继承父进程的现有资源,初始化进程时钟、信号、时间等数据。 完成子进程初始化后,父进程将它挂到就绪队列,返回子进程的PID。
进程创建时的状态为不可中断阻塞,在fork()结束前被父进程唤醒后,变为RUNNING。进程创建时的状态为不可中断阻塞,在fork()结束前被父进程唤醒后,变为RUNNING。 处于RUNNING状态的进程被移到就绪队列中,在适当时候由schedule()按处理机调度算法选中,获得处理机。
获得处理机而正在运行的进程若申请不到某个资源,则调用sleep()进行休眠,其PCB挂到相应的等待队列,状态变为不可中断阻塞或者可中断阻塞。获得处理机而正在运行的进程若申请不到某个资源,则调用sleep()进行休眠,其PCB挂到相应的等待队列,状态变为不可中断阻塞或者可中断阻塞。 sleep()将调用schedule()函数把休眠进程释放的处理机分配给就绪队列中的某个进程.
状态为可中断阻塞的休眠进程当它申请的资源有效时被唤醒,也可以由信号或定时中断唤醒。状态为可中断阻塞的休眠进程当它申请的资源有效时被唤醒,也可以由信号或定时中断唤醒。 而状态为不可中断阻塞的休眠进程只有当它申请的资源有效时被唤醒,不能被信号和定时中断唤醒。 唤醒后,进程状态改为RUNNING,并进入就绪队列。
进程执行系统调用exit()或收到外部的杀死进程信号SIG_KILL时,进程状态变为ZOMBIE,释放所申请资源。进程执行系统调用exit()或收到外部的杀死进程信号SIG_KILL时,进程状态变为ZOMBIE,释放所申请资源。 同时启动schedule()把处理机分配给就绪队列中其他进程。
若进程通过系统调用设置了跟踪标志位,则在系统调用返回前,进入跟踪状态,进程状态变为STOPPED,处理机分配给就绪队列中其他进程。若进程通过系统调用设置了跟踪标志位,则在系统调用返回前,进入跟踪状态,进程状态变为STOPPED,处理机分配给就绪队列中其他进程。 只有通过其他进程发送SIG_KILL信号或继续信号SIG_CONT,才能把STOPPED进程唤醒。重新进入就绪队列。
对每一个进程,其PCB块中都可以记录一种调度策略。对每一个进程,其PCB块中都可以记录一种调度策略。 进程调度算法可采用先进先出算法(FIFO)或轮转法(round-robin),有实时(这里的“实时”,只是一种说法。实际上,未经改造的Linux很难实现“实时”)和非实时两种形式。
若采用Linux的轮转法,当时间片到时(10ms的整数倍),由时钟中断触发,引起新一轮调度,把当前进程挂到就绪队列队尾。若采用Linux的轮转法,当时间片到时(10ms的整数倍),由时钟中断触发,引起新一轮调度,把当前进程挂到就绪队列队尾。 在schedule()中有一个goodness()函数,可以用来保证实时的进程可以得到优先调用。 然而这只是在调用上优先,事实上在内核态下,实时进程并不能对普通进程进行抢占。 所以Linux中的实时并不是真正意义上的实时。
4 进程间关系 Linux中除了0号进程是启动时由系统创建,其余进程都是由其他进程自行创建的。 为了表示这种创建关系,用父进程指代缔造者,用子进程指代被创建出的新进程。 如果进程A是进程B的间接父进程,则A称做B的祖先,B为A的后代。 既然提到了父子关系,那么这两个进程之间自然是有着如同父子一样的继承性。
进程的“宗族”关系 • 树型组织 • task_struct中的 struct task_struct *p_pptr, *p_cptr, *p_ysptr, *p_osptr; • p_pptr: parent (父进程) • p_cptr: child (指向自己最年轻、最新的子进程) • p_ysptr:指向比自己年轻的兄弟进程 • p_osptr:指向比自己老的兄弟进程
在数据结构上,父进程PCB中的指针p_cptr指向最近创建的一个子进程的PCB块,而每个子进程PCB中的指针p_pptr都指向其父进程的PCB块。在数据结构上,父进程PCB中的指针p_cptr指向最近创建的一个子进程的PCB块,而每个子进程PCB中的指针p_pptr都指向其父进程的PCB块。 这一对指针构成了进程的父子关系,如图2-3所示。
task[]数组(实际是双向链表指针) • 包含指向系统中所有task_struct结构的指针 • 数组大小限制了系统中的进程数目 • 将所有任务串连起来 • Pid hash表 • 通过pid查找进程时,利用hash快速定位双向指针 • run_list • 动态的将任务链入prio_array中的某个优先级队列中
current指针 • 当前运行的进程的结构用current指针表示 • init进程 • 系统初始化后,建立的第一个进程 • 第一个task_struct: INIT_TASK
系统启动时,内核被加载到内存后,由start_kernel函数(完成内核初始化工作)从无到有地自行创建了一个内核进程,叫做0号进程,其所运行的代码是init_task()函数,在很多链表中起表头的作用。只有当没有其他进程处于可运行状态时,调度程序才选择0号进程。系统启动时,内核被加载到内存后,由start_kernel函数(完成内核初始化工作)从无到有地自行创建了一个内核进程,叫做0号进程,其所运行的代码是init_task()函数,在很多链表中起表头的作用。只有当没有其他进程处于可运行状态时,调度程序才选择0号进程。 该进程的作用是作为一切其他进程的父进程,就像亚当夏娃是一切人类的祖先那样。 0号进程不能自动生成,必须手工将其设置到进程表中去,才能启动进程管理机制。
在启动进程管理机制以后,就可以由进程自行创建新的子进程。在启动进程管理机制以后,就可以由进程自行创建新的子进程。 创建新进程的调用是fork()。fork一词在英文中是“分叉”的意思。 同样,在Linux中,fork()调用也起了一个“分叉”的作用。 当进程A调用fork()生成进程B时,fork()函数同时在A和B两个进程中返回。 其中,父进程A里的fork()返回了子进程的PID,而子进程B里的fork()返回0。如果出现错误,fork()返回一具负值。
然而,fork()函数究竟做了些什么呢? 我们发现,经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈,就像父进程把自己克隆了一遍。 事实上,父进程只复制了自己的PCB块,而代码段、数据段、用户堆栈内存空间并没有复制一份,而是与子进程共享 . fork函数演示见文件fork.swf
只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。 由于父进程的PCB和子进程的一样,所以在PCB中所记录的父进程占有的资源,也是与子进程共享使用的。 这里的“共享”一词就意味着“竞争”。
有时候为了避免父进程和子进程竞争相同的资源或者出于代码串行性考虑,我们希望父进程可以等待子进程运行结束后再继续执行。有时候为了避免父进程和子进程竞争相同的资源或者出于代码串行性考虑,我们希望父进程可以等待子进程运行结束后再继续执行。 调用vfork()可以使在子进程创建后,随即向父进程发送SIG_STOP信号,使父进程进入挂起状态,直到子进程发送信号表示其已经结束为止。