1.2 充分理解进程
进程的出现使得计算机世界内部复杂的处理过程和逻辑更加透明。比如,UNIX或Linux操作系统的内核管理可以分为进程的管理和文件的管理两大类。在计算机网络方面,进程隐藏了数据传输层以下的网络层、数据链路层和物理层的一系列处理过程,程序员只需要关心传输层的进程和端口号即可。
从数据库的角度看,Oracle的优化在某种程度上就是进程(CPU进程间的调度和切换)的优化、SQL(存取效率)的优化及对象(对象的存储结构)的优化。从数据库大局出发,Oracle的管理也可以分为进程的管理和存储(内存和外存)的管理两大方面。因此,接下来会对进程的概念进一步说明。
1.2.1 何为进程
为了从宏观上保证多个程序能够同时利用计算机的CPU、内存等有限资源,提高计算机系统中各种资源的使用效率,现代操作系统采用了多道程序(multi-programming)技术。进程就是正在运行的程序或程序中的某个模块,进程包含了正在运行的程序的所有状态信息。
显然,进程和程序是两个既有联系又有区别的概念,两者不能混为一谈。程序是一个静态的概念,它由两部分组成:代码和数据。进程是一个动态的概念,它除了包含程序文件中的指令数据以外,还会在系统内核中通过某种数据结构来存放进程的相关属性,以便更好地管理和调度进程,从而完成多进程协作的任务。一个程序可能创建多个进程,通过多个进程的交互协作来完成任务,比如Oracle程序。
一般来说,Linux系统下的进程包含以下两个要素:可执行程序和专用的系统堆栈空间。内核中有进程的控制块来描述进程所占用的资源,这样,进程才能接受内核的调度,具有独立的存储空间。
在Linux系统里,进程是一个结构体对象;在Oracle的内部,库缓存(Library Cache)中的游标(Cursor)实际上是一个与进程类似的对象。进程是操作系统中的程序代码及数据的载体,我们只有理解了UNIX系统的进程管理机制,才能深入理解Oracle中游标的概念。因此正确理解进程及其所需的内存资源对理解Oracle的运行原理非常重要,这也是本章介绍操作系统范畴的进程概念的原因之一。
一个进程是一个独立的实体,是计算机系统资源的使用单位。每个进程都有“自己”的寄存器(如PC,即Program Counter,用来指示下一条将要运行的指令)和内部状态,它在运行的时候独立于其他进程,这里的“自己”是指在物理上CPU中只有一套寄存器(如物理的PC寄存器只有一个),但是每个进程都有属于自己的逻辑上寄存器(如逻辑上的PC)。要想将程序代码及数据读到内存中,必须提前分配内存空间。在Linux系统中,可以通过“cat/proc/<进程PID>/maps”命令查看进程占用的内存空间。一个进程所需的内存空间结构如图1-4所示。
图1-4 进程内存空间结构示意图
Oracle数据库管理系统实际上是一个用C语言编写的由多个模块组成的大型程序。Oracle数据库管理系统分为代码和数据两部分。其中,代码部分就是在Oracle的安装过程中出现“仅安装软件”选项时所安装的那一部分(代码也包含相关元数据,在此可暂时忽略这一点);数据部分则是通过DBCA(DataBase Configuration Assistant,数据库配置助手)创建的某个具体的数据库。当我们安装完软件并创建完数据库时,从静态层面看到的是在操作系统中一系列目录下的程序文件和数据文件。当我们通过startup命令运行该程序的时候,操作系统为该程序(进程)分配相应的内存,然后将磁盘中那些静态的程序代码读入SGA(System Global Area,系统全局区)和PGA(Program Global Area,程序全局区)中每个相应进程的内存段里,从而完成相关的初始化工作。这时,内存中的这些程序代码及数据就会变为进程。在操作系统中通过ps命令可以看到这些进程的信息。
那么,UNIX系统是怎么管理进程的呢?每个进程在内核进程表中都有一个表项,并且每个进程都会被分配一个所谓的U区,U区包含仅被内核操纵的私有数据,如图1-5所示。进程表包含(或指向)一个本进程区表,本进程区表的表项指向系统区表的表项。区是指进程地址空间中连续的区域,如代码区、数据区及栈区等。系统区表登记项用于描述区的属性,例如,它是否包含代码或数据,它是共享的还是私有的,以及区的“数据”位于内存的何处,等等。本进程区表与系统区表是多级链接关系(即多对多的链接关系),这些链接关系允许彼此独立的进程共享系统区表。图1-5展示了一个与运行中的进程相关的数据结构:进程表指向本进程区表,本进程区表又指向系统区表,系统区表包含该进程的代码区、数据区或栈区在内存中具体地址的指针。
图1-5 进程相关数据结构示意图
因此,在内存中一个进程实际上是相互指向(或链接)的多个内存区域的组合,而不是一块简单的连续内存空间。UNIX系统采用这样的设计为进程的灵活性和可扩展性打好了基础。
1.2.2 进程的状态
通过前面的内容可知,CPU实际上是被多个进程轮流使用的。进程执行时的间断性决定了进程可能具有多种状态。事实上,运行中的进程一般具有3种基本状态,即就绪状态、运行状态和等待或阻塞状态,如图1-6所示。
图1-6 进程在运行中的3种基本状态
1.就绪状态
在就绪状态下,进程已获得除CPU以外的所需资源,等待分配CPU资源。只要分配到了CPU资源,进程就可以被执行。就绪进程可以按优先级划入不同的队列。例如,由于时间片用完而进入就绪状态的进程将排入低优先级队列,因I/O操作完成而进入就绪状态的进程就会排入高优先级队列。
2.运行状态
在运行状态下,进程正在占用CPU资源,处于此状态的进程数目小于或等于CPU的数目。在没有其他进程可以执行时(如所有进程都处于等待状态),系统的空闲进程通常会自动执行。
3.等待或阻塞状态
有时进程需要等待某种条件(如I/O操作或进程同步),在条件满足之前,即使把CPU资源分配给该进程,它也无法继续运行,这时就会进入等待或阻塞状态。
在实际的系统中,进程的状态往往不止这三种,比如,UNIX System V就为进程设置了9种状态:用户态运行、内核态运行、内存中就绪、内存中睡眠、就绪且换出、睡眠且换出、被剥夺状态、创建状态和僵死状态,如图1-7所示。
图1-7 UNIX System V进程的9种状态及转换示意图
理解进程的这些状态对于DBA在Linux数据库服务器环境下进行故障诊断或性能分析有很大的帮助。比如,通过Linux系统下的vmstat命令工具,我们能够查看进程的5种状态(vmstat的使用将在2.3节中详细说明)。
1.2.3 进程切换
进程切换就是从正在运行的进程中收回CPU,然后将其分配给待运行的进程。这里所说的从某个进程中收回CPU,实质上就是把进程存放在CPU寄存器中的中间数据找个地方存起来,从而把CPU的寄存器腾出来让其他进程使用。那么被中止运行进程的中间数据应该存在何处呢?答案是进程的私有堆栈中(详情参见第9章)。
让进程占用CPU,实质上是把某个进程存放在私有堆栈中的数据(前一次该进程被中止时的中间数据)再恢复到CPU的寄存器中去,并把待运行进程的断点送入CPU的PC寄存器中。于是CPU就开始运行该进程,即该进程已经占有CPU的使用权了。
在切换时,进程存储在CPU各寄存器中的中间数据称为进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用CPU时,进程的上下文是存储在进程的私有堆栈中的。在性能优化过程中,上下文切换是非常消耗CPU资源的,所以应尽量避免。
进程的切换可以通过中断技术来实现,即在调度器获得了待运行进程的控制块之后,立即用软中断指令来中止当前进程,并保存当前进程的PC寄存器值和PSW(Program Status Word,程序状态字)寄存器值。其后,使用压栈指令把CPU中其他寄存器的值压入进程的私有堆栈中。接着,从待运行进程的进程控制块中取出私有SP(堆栈指针)的值并存入CPU的寄存器SP中,至此SP就指向了待运行进程的私有堆栈,然后从待运行进程的私有堆栈中弹出上下文进入CPU。最后,利用中断返回指令实现从待运行进程的私有堆栈中弹出PSW寄存器值和PC寄存器值的功能。
这是一个完整的软中断处理过程,只不过在保护现场和恢复现场的工作中,保护的是被中止运行进程的现场,恢复的是待运行进程的现场,这一切都依赖于SP的切换。
1.2.4 进程间通信
进程间通信(Inter Process Communication,IPC)机制允许多个进程之间相互交换数据与信息。常用的进程间通信有两种基本模型:共享内存(Shared Memory)模型和消息传递(Message Passing)模型。共享内存模型会建立一块共享内存区域供相互通信的进程使用,进程通过向此共享内存区域读出或写入数据来交换信息。Oracle的SGA本质上是为多进程共享及进行交互通信而创建的区域。消息传递模型通过在进程之间进行消息交换来实现通信。图1-8给出了这两种模型的对比。
消息传递模型对于交换量较少的数据很有用,因为无须避免冲突,尤其是在分布式系统中,消息传递模型比共享内存模型更易于实现。共享内存模型比消息传递模型速度更快,这是因为消息传递模型通常是采用系统调用的方式实现的,需要消耗更多的时间以便内核介入。与之相反,共享内存模型仅在建立共享内存区域时需要使用系统调用,一旦建立起了共享内存,所有访问都可以作为常规内存访问,无须借助内核。共享内存模型往往与其他通信机制(如信号量)结合使用以实现进程间的同步及互斥。信号量(semaphore)通信机制主要作为进程间或者同一进程不同线程之间的同步手段。不同计算机之间的进程间通信一般采用Socket方式实现,这也是常见的进程间通信机制。
图1-8 常用的进程间通信方式
在Oracle的SGA中,进程间通信可以通过Lock、Latch、Mutex等方式来实现同步和互斥。相关内容将在第二篇展开说明。
了解了进程的基础概念后,接下来讨论Oracle的进程组织方案。