UNIX上进程池的实现
一、概要
在现实应用中,针对应用系统的交易量和性等性能要求,大多数系统在服务端采用了多进程技术。本文描述了进程处理的一些技术点,分析了传统的多进程使用方法,设计出一种更能提高系统利用率和运行效率的进程池技术,并着重分析了进程池技术的实现方法。
二、关键词
进程(process)、共享存储(Shared Memory)、信号(signal)。
三、技术分析
1、进程(process)
进程是程序对某个数据集的执行过程,是分配资源的基本单位,一个随执行过程不断变化的实体在操作系统中执行特定的任务。和程序要包含指令和数据一样,进程也包含程序计数器和所有CPU寄存器的值,同时它的堆栈中存储着如子程序参数、返回地址以及变量之类的临时数
据。进程具有独立的权限与职责。每个进程运行在其各自的虚拟地址空间中,通过核心控制下可靠的通讯机制,它们之间才能发生联系。如果系统中某个进程崩溃,它不会影响到其余的进程。   
进程在生命期内将使用系统中的资源。它利用系统中的CPU来执行指令,在物理内存来放置指令和数据。使用文件系统提供的功能打开并使用文件,同时直接或者间接的使用物理设备。操作系统必须跟踪系统中每个进程以及资源,以便在进程间实现资源的公平分配。         
现在的Unix系统如SCO OpenServer、HP_Unix和IBM AIX等,甚至时Linux操作系统,他们都是一个多处理操作系统。而作为多处理操作系统,它的基本目的是任何时刻系统中的每个CPU上都有任务执行,从而提高CPU的利用率。如果进程个数多于CPU的个数,则有些进程必须等待到CPU空闲时才可以运行。多处理的思路很简单当进程需要某个系统资源时它将停止执行并等待到资源可用时才继续运行。单处理系统中,如DOS,此时CPU将处于空等状态,这个时间将被浪费掉。在多处理系统中,因为可以同时存在多个进程,所以当某个进程开始等待时,操作系统将把CPU控制权拿过来并交给其它可以运行的进程。调度器负责选择适当的进程来运行,操作系统使用一些调度策略以保证CPU分配的公平性。        
一个现存进程调用fork函数是UNIX内核创建一个新进程的唯一方法(这并不适用于交换进程、init进程和页精灵进程,这些进程是由内核作为自举过程的一部分以特殊方式创建的)
pid_t fork(void); 
返回:子进程中为0,父进程中为子进程ID,出错为-1
fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。父进程继续执行,操作系统对fork的实现,使这个调用在父进程中返回刚刚创建的子进程的pid将子进程ID返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程ID子进程在之后的某个时候得到调度,它的上下文被换入,占据 CPU,操作系统对fork的实现,使得子进程中fork调用返回0子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID
调用fork函数,系统就为一个新的进程准备了三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新
的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用进程间通讯技术共享存储消息对流或信号灯等),在本文中使用了共享存储技术,在后面的文章对共享存储进行了介绍。
2、共享存储(Shared Memory
共享存储是多个进程之间进行数据交换的一种进程通讯机制,允许两个或多个进程共享一给定的存储区进行数据信息的存取,减少了数据流动所带来的硬软件开销。若进程甲将数据放入共享存储区,进程乙可以获取这些数据。而进程对共享存储的同步控制则要依靠一些特殊的机制如信号灯等来完成。
创建一个新的共享存储段,通常使用shmget函数,获得一个整数型的共享存储标识符,
int shmget(key_t key, int size, int flag);
返回:若成功则为共享存储ID,若出错则为- 1
shmget函数创建一个共享区键值为key、空间大小指定为size新共享存储段,并返回新段的整型标识符。
一旦创建了一个共享存储段,进程就可调用shmat函数将其连接到它的地址空间中。
void *shmat(int shmid, void *addr, int flag);
返回:若成功则为指向共享存储段的指针,若出错则为- 1
把标识符为shmid的共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_RND位有关。平常设置add为零,把共享存储段连接到由内核选择的第一个可用地址上。
当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统中删除其标识符以及其数据结构,该标识符仍然存在。
int shmdt(void *addr) ;
返回:若成功则为0,若出错则为- 1
函数中的addr参数是以前调用shmat时的返回值。
平常调用shmctl函数进行对共享存储段的查询或设置等操作,也可以对共享存储段执行删除操作。
int shmctl(int shmid, int cmd, struct shmid_ds *buf) ;
返回:若成功则为0,若出错则为- 1
shmctl函数对标识符为shmid的共享存储进行各种操作,当cmd参数为IPC_RMID时,从系统中删除标识符和共享存储段。
3、信号(signal
信号是软件中断。很多比较重要的应用程序都需处理信号。产生信号的事件对进程而言是随机出现的,进程不能只是测试一个变量(例如errno )来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。
每个信号都有一个名字,这些名字都以三个字符SIG开头。系统在很多条件下可以产生一个
信号,如当用户按某些终端键时、硬件异常产生信号、用kill命令将信号发送给其他进程或进程的撤销等。本文中使用到了SIGCLDSIGTERM信号。
SIGCLD 在一个进程终止或停止时,SIGCLD信号被送给其父进程。
SIGTERM 这是由kill(1)命令发送的系统默认终止信号。
系统接收到信号时,可以进行忽略此信号、捕捉信号和执行系统默认动作的三种操作。
unix文件系统 忽略此信号。大多数信号都可使用这种方式进行处理,但信号SIGKILLSIGSTOP这两种是决不能被忽略,因为它们是向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的。
捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
执行系统默认动作。对大多数信号的系统默认动作是终止该进程。
平常应用中,使用比较多的是忽略此信号和捕捉信号两种操作。
在正常情况下,调用kill函数实现将信号发送给进程或进程组。如SIGTERM 这是由kill(1)命令发送的系统默认终止信号。
int kill(pid_tp pid, int signo);
两个函数返回:若成功则为0,若出错则为-1
killpid参数有四种不同的情况:
• pid > 0 将信号发送给进程IDpid的进程。
• pid == 0 将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程。这儿的“所有进程”并不包括系统进程集中的进程。
• pid < 0 将信号发送给其进程组I D等于p i d绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程。
   
四、传统的多进程机制
平常,主进程根据需要调用fork函数,产生一个子进程来处理新服务,能比较好的实现多任务的并发处理。
#include <stdio.h>
void Process_Out(int)
{
    ……;
    exit(0);
}
void main()
{
    ......; 
    signal(SIGTERM, Process_Out);
    signal(SIGCLD,  SIG_IGN);
    while(1)
    {
        if (!触发)
            continue;
       
        if (fork() < 0 )  /* fork失败 */
        {   
            exit(-1); 
        }
        else if (pid == 0 )  /* 子进程 */
        {   
            ......;          /* 子进程处理过程 */
            exit(0);        /* 子进程exit*/
        }
       
        ......;  /* 父进程处理过程 */
    } /*end while*/
}
分析上面的代码,从main函数开始,主进程捕捉本进程要求被终止时的SIGTERM信号,调用Process_Out函数,处理相应的过程,如清理数据或被使用的地址空间。而忽略了对捕捉到的子进程终止信号SIGCLD的处理,使子进程将不产生僵死进程,除非主进程希望改变子进程的运行状态。从下面while函数可见主进程是一个长驻进程,当主进程监控到有被触发请求时,fork子进程。之后,子进程开始执行代码,处理完任务后exit。同时,主进程则继续处理任务,并循环监控是否由新的被触发请求。
上述的例子,使用fork技术就实现了服务端的多进程机制,能满足一些业务的需求。然而,在系统上fork进程时,产生的子进程会复制主进程的代码段、数据段和堆栈段,申请系统空间,可见每次fork消耗系统资源,而且花费的系统时间也比较大,会给对实时性和并发性要求都比较高的应用带来瓶颈。因此,考虑能否减少fork进程的频繁程度,来实现应用中对实时性和并发性的要求?答案是肯定的,使用我们设计的进程池技术,可以满足减轻系统开销、提高系统效率的要求。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。