Chinaunix首页 | 论坛 | 博客
  • 博客访问: 259448
  • 博文数量: 74
  • 博客积分: 1470
  • 博客等级: 上尉
  • 技术积分: 793
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-25 21:01
文章分类

全部博文(74)

文章存档

2011年(1)

2010年(32)

2009年(32)

2008年(9)

我的朋友

分类: LINUX

2010-05-16 04:41:16

这是我们的第一个“增强”型TCP服务器程序,使用成为预先派生子进程(preforking)的技术。
该技术不象传统意义上的并发服务器那样为每个客户现场派生一个子进程,而是在服务程序启动阶段预先派生一定数量的子进程,当各个客户连接到达时,这些子进程立即就能服务客户了。
这种技术的优点在于无须引入父进程执行fork的开销就能够处理新到的客户请求,缺点是父进程必须在启动阶段猜测需要预先派生多少子进程。如果某个时刻客户数恰好等于子进程总数,那么新到的客户将被“忽略”,直到至少有一个子进程重新可用。然后,我们直到这些客户并未完全被忽略,内核将为每个新到的客户完成三次握手,直到到达相应套接口上listen函数调用的backlog队列数为止,然后在服务器调用accept时把这些已完成的连接传递给它。这样一来,客户就能感觉到服务器在响应时间上的恶化,因为尽管它的connect调用可能立即返回,但是它的第一次请求可能是在一段时间之后才被服务器处理的。
我们可以增加一些代码,来使服务器能够应对客户负载的变动。比如,父进程监视可用子进程数,一旦该值降低到某个阈值就派生额外的子进程。同样,一旦该值超过另一个阈值,就终止一些过剩的子进程。

我们来看一看这类服务器程序的基本结构:
serv02.c
--------------------------

/* include serv02 */
#include "global.h"

static int nchildren;
static pid_t *pids;

int
main(int argc, char **argv)
{
    int listenfd, i;
    socklen_t addrlen;
    void sig_int(int);
    pid_t child_make(int, int, int);

    if (argc == 3)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 4)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: serv02 [ ] <#children>");
    nchildren = atoi(argv[argc-1]);
    pids = Calloc(nchildren, sizeof(pid_t));

    for (= 0; i < nchildren; i++)
        pids[i] = child_make(i, listenfd, addrlen); /* parent returns */

    Signal(SIGINT, sig_int);

    for ( ; ; )
        pause(); /* everything done by children */
}
/* end serv02 */

/* include sigint */
void
sig_int(int signo)
{
    int i;
    void pr_cpu_time(void);

        /* 4terminate all children */
    for (= 0; i < nchildren; i++)
        kill(pids[i], SIGTERM);
    while (wait(NULL) > 0) /* wait for all children */
        ;
    if (errno != ECHILD)
        err_sys("wait error");

    pr_cpu_time();
    exit(0);
}
/* end sigint */



pids指向存放各个子进程ID的数组,用于在父进程即将终止时由main函数终止所有子进程。

child_make()函数由main调用以派生各个子进程
child02.c
-----------------------------

/* include child_make */
#include "global.h"

pid_t
child_make(int i, int listenfd, int addrlen)
{
    pid_t pid;
    void child_main(int, int, int);

    if ( (pid = Fork()) > 0)
        return(pid); /* parent */

    child_main(i, listenfd, addrlen); /* never returns */
}
/* end child_make */

/* include child_main */
void
child_main(int i, int listenfd, int addrlen)
{
    int connfd;
    void web_child(int);
    socklen_t clilen;
    struct sockaddr *cliaddr;

    cliaddr = Malloc(addrlen);

    printf("child %ld starting\n", (long) getpid());
    for ( ; ; ) {
        clilen = addrlen;
        connfd = Accept(listenfd, cliaddr, &clilen);

        web_child(connfd); /* process the request */
        Close(connfd);
    }
}
/* end child_main */



child_make()调用fork派生子进程后只有父进程返回,子进程调用child_main()函数,它是一个无限循环,每个子进程调用accept返回一个已连接套接口,然后调用web_child处理客户请求,最后关闭连接。子进程一直在这个循环中反复,直到父进程终止。

或许你会有一个疑问,多个进程在同一个监听描述字上调用accept,你可能会想知道这到底是如何工作的。我们暂且偏离一下主题。
服务器进程在程序启动阶段派生N个子进程,它们各自调用accept并因此均被内核投入睡眠。当第一个客户连接到达时,所有N个子进程均被唤醒,这是因为所有N个子进程所用的监听套接字指向同一个socket结构,致使它们在同一个等待通道(socket结构的so_timeo成员上)进行睡眠。尽管所有N个子进程均被唤醒,其中只有最先运行的子进程获得那个客户连接,其余N-1个子进程又继续睡眠。这就是有时候称为“惊群”问题。尽管如此,这段代码依然可以工作,只是每当一个仅有一个连接准备好被接受时却唤醒太多进程的做法会导致性能损失。(某些unix内核有一个往往命名为wakeup_one的函数,它只唤醒等待某个事件的多个进程中的一个,而不是唤醒所有等待该事件的进程)

这种预先派生子进程方式的服务器程序执行结果如下:
$ ./serv02 173.26.100.162 12345 10
child 10654 starting
child 10655 starting
child 10656 starting
child 10657 starting
child 10658 starting
child 10659 starting
child 10660 starting
child 10661 starting
child 10662 starting
child 10663 starting

user time = 0.004, sys time = 0.096006

$ ./client 173.26.100.162 12345 5 500 4000
child 0 done
child 1 done
child 2 done
child 3 done
child 4 done

我们接着可以查看全体客户连接在阻塞于accept调用中的可用子进程上的分布。为了采集这些信息,我们把main函数改为在共享内存区中分配一个长整数计数器数组,每个子进程一个计数器。
serv02m.c
-------------------------------

#include "global.h"

static int nchildren;
static pid_t *pids;
long *cptr, *meter(int); /* for counting #clients/child */

int
main(int argc, char **argv)
{
    int listenfd, i;
    socklen_t addrlen;
    void sig_int(int);
    pid_t child_make(int, int, int);

    if (argc == 3)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 4)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: serv02 [ ] <#children>");
    nchildren = atoi(argv[argc-1]);
    pids = Calloc(nchildren, sizeof(pid_t));

        /* add for counting client assign */
    cptr = meter(nchildren);

    for (= 0; i < nchildren; i++)
        pids[i] = child_make(i, listenfd, addrlen); /* parent returns */

    Signal(SIGINT, sig_int);

    for ( ; ; )
        pause(); /* everything done by children */
}

void
sig_int(int signo)
{
    int i;
    void pr_cpu_time(void);

        /* terminate all children */
    for (= 0; i < nchildren; i++)
        kill(pids[i], SIGTERM);
    while (wait(NULL) > 0) /* wait for all children */
        ;
    if (errno != ECHILD)
        err_sys("wait error");

    pr_cpu_time();

        /* add for counting client assign */
    for (= 0; i < nchildren; i++)
        
printf("child %d, %ld connections\n", i, cptr[i]);

    exit(0);
}




meter.c
------------------------------

#include "global.h"
#include <sys/mman.h>

/*
 * Allocate an array of "nchildren" longs in shared memory that can
 * be used as a counter by each child of how many clients it services.
 * See pp. 467-470 of "Advanced Programming in the Unix Environment."
 */


long *
meter(int nchildren)
{
    int fd;
    long *ptr;

#ifdef MAP_ANON
    ptr = Mmap(0, nchildren*sizeof(long), PROT_READ | PROT_WRITE,
               MAP_ANON | MAP_SHARED, -1, 0);
#else
    fd = Open("/dev/zero", O_RDWR, 0);

    ptr = Mmap(0, nchildren*sizeof(long), PROT_READ | PROT_WRITE,
               MAP_SHARED, fd, 0);
    Close(fd);
#endif

    return(ptr);
}



cptr指向这个共享内存区,然后在child_make函数里让每个子进程在accept返回后递增各自的计数器,在sig_int()函数中当所有子进程终止之后显示每个计数器数组.

child02m.c
----------------------------

#include "global.h"

pid_t
child_make(int i, int listenfd, int addrlen)
{
    pid_t pid;
    void child_main(int, int, int);

    if ( (pid = Fork()) > 0)
        return(pid); /* parent */

    child_main(i, listenfd, addrlen); /* never returns */
}

void
child_main(int i, int listenfd, int addrlen)
{
    int connfd;
    void web_child(int);
    socklen_t clilen;
    struct sockaddr *cliaddr;
    extern long *cptr;

    cliaddr = Malloc(addrlen);

    printf("child %ld starting\n", (long) getpid());
    for ( ; ; ) {
        clilen = addrlen;
        connfd = Accept(listenfd, cliaddr, &clilen);

                /* add for counting client assign */
        cptr[i]++;

        web_child(connfd); /* process the request */
        Close(connfd);
    }
}


执行结果如下:
$ ./serv02m 173.26.100.162 12345 10
child 13172 starting
child 13173 starting
child 13174 starting
child 13175 starting
child 13176 starting
child 13177 starting
child 13178 starting
child 13179 starting
child 13180 starting
child 13181 starting

user time = 0.008, sys time = 0.096006
child 0, 247 connections
child 1, 249 connections
child 2, 252 connections
child 3, 252 connections
child 4, 247 connections
child 5, 250 connections
child 6, 248 connections
child 7, 251 connections
child 8, 250 connections
child 9, 254 connections


程序源代码包见<<客户/服务器程序设计范式---迭代服务器程序>>后的附件
阅读(860) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~