Webbench源码分析之多进程(三)

概述:前面我们把参数输入,http协议以及socket客户端编程部分都说了,今天就把多进程这一块内容学习过程记录一下。同时今天学习的这一部分也是webbench的核心部分了。

知识点:
1,多进程的创建。
2,多进程无名管道pipe通信。
3,多进程信号。

还是老样子,在学习源码之前先把用到的知识点搞懂,然后再进行学习,这样就事半功倍了。

1,多进程的创建
说到多进程,那fork()函数是跑不掉了,我这里也就简单说一下啦。fork函数就是一个创建子进程的作用,它的返回值有三种情况。
1,小于0,失败。
2,等于0,表示当前运行进程为子进程,返回值是0。
3,大于0,表示当前运行进程为父进程,返回值是子进程的进程id。

一个简单例子解释一下fork函数。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

 int value = 100;

 int main()
 {
     pid_t pid;
     printf("start\n");
     /*创建子进程*/
     pid = fork();
     printf("hahaha\n");
     /*判断是否创建失败*/
     if (pid < 0) {
         printf("fork failed\n");
         return -1;
     }
     /*返回值大于0,表示父进程*/
     if (pid > 0) {
         printf("parent process value = %d\n",value);
         sleep(1);
     }
     /*返回值等于0,表示子进程*/
     if (0 == pid) {
         value = 99;
         printf("child process value = %d\n",value);
     }
     return 0;
  }

我们看一下运行结果:
这里写图片描述

首先我们看到start打印了一次,而hahaha打印了两次。不是说子进程和父进程是完全一样的,为何start只打印出一次,这就要说子进程执行位置了,确实子进程会完全拷贝父进程,但是子进程开始执行的顺序是fork以后,所以hahaha就会打印两次了。其次,我们发现返回值大于0的执行代码运行了,等于0的执行代码也运行了,其实就是父子进程都开始运行fork之后的代码,而父进程的返回值大于0,所以打印出parent process value = 100,子进程也运行相同的代码,但是其返回值为0,所以就打印了child process value = 100.最后,我们看到value是我们之前定义的全局变量,子进程中改变了value值,父进程值还是100,其实这两个值已经不相干了,他们是独立的,不会互相影响,不像多线程需要加锁等同步机制。
关于fork()函数面试题也很多,可以看下这篇博客

2,多进程无名管道pipe通信。
这里我就用个人理解来说一下pipe()函数。管道通信,我们的pipe()是只能在有亲缘关系的进程间完成通信。比如父子进程,兄弟进程。我把webbench中用到的pipe相关代码抠出来。

int mypipe[2];
/* create pipe */
if(pipe(mypipe))
 {
     perror("pipe failed.");
     return 3;
 }

我们看到我们定义了一个int型两个元素的数组mypipe[2];然后通过pipe()函数创建了一个管道。管道创建完成后,其实我们得到了两个文件描述符,一个写端一个读端。mypipe[0]就表示读端,mypipe[1]表示写端。
pipe创建的管道是半双工的,也就是说在同一时间,某进程使用这个管道时只能往里面读或写,但不能同时读写。

那到底是怎么实现进程通信的呢,其实看webbench源码会发现,创建管道是在fork()之前,原因是什么?
就是因为子进程是完全复制父进程的,所以子进程就也有了这两个文件描述符,这样父子进程就可以通过这个管道通信啦,一个在管道一边写,一个在一边读。webbench就是这样使用的,子进程把数据写入管道,父进程一直读数据。

通过man查看pipe函数发现其中说,在使用管道前,首先关闭你不需要的操作,比如你要读,那么就要先close(mypipe[1]),反之就close(mypipe[0]).但是在webbench源码中没有关闭,应该是有一点点小bug吧,个人推测。然后我也具体看了为何要首先关闭管道一端,stackoverflow有这样的答案,有兴趣可以看一下,地址在这:Is it really necessary to close the unused end of the pipe in a process

3,多进程信号。
在源码中我们看到使用了sigaction函数,那么我们先了解一下这个函数。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //函数原型 

稍微解释一下参数,signum:表示要操作的信号值;act:表示新的信号处理方式,oldact表示原来的信号处理方式。这里我们发现结构体struct sigaction.我们看一下这个结构体的定义。

 struct sigaction
 {
  void     (*sa_handler)(int);
  void     (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t  sa_mask;
  int       sa_flags;
  void     (*sa_restorer)(void);
 };

第一个函数指针就是我们一般用到的,指向我们的信号处理函数。第二个函数指针同样也是指向我们的信号处理函数,不过这个更加详细,需要sa_flags
包含了SA_SIGINFO标志才会以第二个去进行处理,bench中用到的就是第一种 基本用法。详细可以看下这篇博客sigaction 函数

我这里就写了一个和bench中几乎相同的sigaction的小例子,代码如下:

/*示例功能:实际就是一个定时器的作用,我们设置的时间间隔到了的时候,alarm会发出SIGALRM信号,我们的sigaction函数收到信号,执行我们对应的处理函数alarm_handler(),然后打印出hello world并改变value的值为1,main函数中while循环一直检测value的值是否不为0,不为0程序退出*/
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>

int value = 0;
/*我们的信号处理函数*/
static void alarm_handler(int signal)
{
    printf("hello world\n");    
    value = 1;
}

int main(int argc, char *argv[])
{
    if (2 != argc) {
        return -1;  
    }
    /*用户输入定时的时间,单位s*/
    int times = atoi(argv[1]);
    times = times > 0 ? times : 30;
    /*声明变量,赋值*/
    struct sigaction sa;
    sa.sa_handler = alarm_handler;
    sa.sa_flags = 0;
    /*信号注册及对应信号处理方式,接收到SIGALRM就会执行我们上面赋值的alarm_handler()函数*/
    if (sigaction(SIGALRM, &sa, NULL) < 0) {
        return -1;  
    }
    /*达到设置的时间产生信号SIGALRM*/
    alarm(times);
    while (1) {
        if (value) {
            printf("%ds time up...\n",times);
            return 0;   
        }   
        usleep(200000);
    }
    return 0;
}

前面知识点已经说的差不多了,现在就开始看源码吧,这也是webbench最核心的部分了,我就直接在源码上注释了,因为用到的知识点我前面都说了,就不再废话啦。

/*源码*/
/*SIGLARM信号处理函数,将timerexpired置1*/
static void alarm_handler(int signal)
{
    timerexpired=1;
}   

static int bench(void)
{
    int i,j,k;  
    pid_t pid=0;
    FILE *f;

    /*检查服务器是否有效,可否连接*/
    i=Socket(proxyhost==NULL?host:proxyhost,proxyport);
    if(i<0) { 
        fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n");
        return 1;
    }
    close(i);

    /* 创建管道 */
    if(pipe(mypipe))
    {
        perror("pipe failed.");
        return 3;
    }

    /* 创建子进程*/
    for(i=0;i<clients;i++)
    {
        pid=fork();
        if(pid <= (pid_t) 0)
        {
            /* child process or error*/
            sleep(1); /* make childs faster */
            break;
        }
    }
    /*当pid<0表示fork创建失败,程序退出*/
    if( pid < (pid_t) 0)
    {
        fprintf(stderr,"problems forking worker no. %d\n",i);
        perror("fork failed.");
        return 3;
    }
    /*pid == 0 子进程的运行部分*/
    if(pid == (pid_t) 0)
    {
        /* I am a child */
        /*进行测试,有代理则使用代理服务器,无代理则直接使用设置的主机地址*/
        if(proxyhost==NULL)
            benchcore(host,proxyport,request);
        else
            benchcore(proxyhost,proxyport,request);
        /* write results to pipe */
        /*把最终测试结果写入管道,传送给父进程*/
        f=fdopen(mypipe[1],"w");
        if(f==NULL)
        {
            perror("open pipe for writing failed.");
            return 3;
        }
        /* fprintf(stderr,"Child - %d %d\n",speed,failed); */
        fprintf(f,"%d %d %d\n",speed,failed,bytes);
        fclose(f);

        return 0;
    } 
    else
    {
        /*父进程执行部分*/
        f=fdopen(mypipe[0],"r");
        if(f==NULL) 
        {
            perror("open pipe for reading failed.");
            return 3;
        }

        setvbuf(f,NULL,_IONBF,0);

        speed=0;
        failed=0;
        bytes=0;
        /*循环读取管道数据,进行结果统计*/
        while(1)
        {
            pid=fscanf(f,"%d %d %d",&i,&j,&k);
            if(pid<2)
            {
                fprintf(stderr,"Some of our childrens died.\n");
                break;
            }

            speed+=i;
            failed+=j;
            bytes+=k;

            /* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */
            if(--clients==0) break;
        }

        fclose(f);
        /*打印出最终结果*/
        printf("\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n",
            (int)((speed+failed)/(benchtime/60.0f)),
            (int)(bytes/(float)benchtime),
            speed,
            failed);
    }

    return i;
}

/*bench的核心代码*/
void benchcore(const char *host,const int port,const char *req)
{
    int rlen;
    char buf[1500];
    int s,i;
    struct sigaction sa;

    /*这里就是和我写的那个例子几乎相同,设置了信号SIGALRM的处理函数*/
    sa.sa_handler=alarm_handler;
    sa.sa_flags=0;
    if(sigaction(SIGALRM,&sa,NULL))
        exit(3);
    /*等待信号产生*/
    alarm(benchtime); 

    rlen=strlen(req);
    nexttry:while(1)
    {
        /*当timerexpired为1时表示压测时间到,子进程退出*/
        if(timerexpired)
        {
            if(failed>0)
            {
                /* fprintf(stderr,"Correcting failed by signal\n"); */
                failed--;
            }
            return;
        }
        /*连接服务器,返回socket文件描述符,用于通信*/
        s=Socket(host,port);                          
        if(s<0) { failed++;continue;}  /*判断是否连接失败*/
        if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;} /*判断是否发送数据失败*/
        /*当http版本为0.9,是否关闭socket写输入失败*/
        if(http10==0) 
        if(shutdown(s,1)) { failed++;close(s);continue;}
        /*force == 0表示等待服务器的数据*/
        if(force==0) 
        {
            /* read all available data from socket */
            /*读取服务器返回的数据*/
            while(1)
            {
                if(timerexpired) break; 
                i=read(s,buf,1500);
                /* fprintf(stderr,"%d\n",i); */
                if(i<0) 
                { 
                    failed++;
                    close(s);
                    goto nexttry;
                }
                else
                if(i==0) break;
                else
                bytes+=i;
            }
        }
        if(close(s)) {failed++;continue;}
        speed++;
    }
}

总结:
Webbench源码到这里也全都看完了,首先学习到了挺多的知识点,包括信号,http协议,多进程的知识。还有就是一个设计方法,webbench是用子进程去处理测试,父进程去统计数据,在我们的代码设计中,有时候也可以考虑这样的方式去做,父进程控总端,子进程去干活,子进程挂了也不会导致父进程死掉,只要父进程在就能不死不灭,哈哈。但是,说到webbench的代码书写,我是觉得不太好的,看着很凌乱,我是不建议这样去写代码。总体来说读源码还是不错的一个学习方式,继续前进吧。Keep & Peace & Love.

猜你喜欢

转载自blog.csdn.net/dancer__sky/article/details/78865050
今日推荐