Iperf源代码分析(九)

下面以程序的执行为主线,简要分析一下Iperf源代码的实现。

main函数

main函数在文件main.cpp中定义,它是程序的入口点。

#define HEADERS()
#include "headers.h"
#include "Client.hpp"
#include "Settings.hpp"
#include "Listener.hpp"
#include "Speaker.hpp"
#include "Locale.hpp"
#include "Condition.hpp"
#include "List.h"
#include "util.h"
/* -------------------------------------------------------------------
 * prototypes
 * ------------------------------------------------------------------- */

void waitUntilQuit( void );

void sig_quit( int inSigno );

void cleanup( void );

/* -------------------------------------------------------------------
 * global variables
 * ------------------------------------------------------------------- */
#define GLOBAL()

Condition gQuit_cond;

/* -------------------------------------------------------------------
 * sets up signal handlers
 * parses settings from environment and command line
 * starts up server or client thread
 * waits for all threads to complete
 * ------------------------------------------------------------------- */

int main( int argc, char **argv ) {

    Listener *theListener = NULL;
    Speaker  *theSpeaker  = NULL;

    // signal handlers quietly exit on ^C and kill
    // these are usually remapped later by the client or server
    // 对SIGTERM、SIGINT信号安装信号处理程序
    my_signal( SIGTERM, sig_exit );
    my_signal( SIGINT,  sig_exit );

    signal(SIGPIPE,SIG_IGN);

    // perform any cleanup when quitting Iperf
    // 在退出Iperf时调用Cleanup程序进行扫尾工作
    atexit( cleanup );

    ext_Settings* ext_gSettings = new ext_Settings;
    Settings* gSettings = NULL;

    // read settings from environment variables and command-line interface
    // 从系统环境变量和输入命令行读取相关设置信息
    // 相关参数保存在argc和argv中
    gSettings = new Settings( ext_gSettings );
    gSettings->ParseEnvironment();
    gSettings->ParseCommandLine( argc, argv );
    
    // start up client or server (listener)
    // 启动客户端还是服务器

    if ( gSettings->GetServerMode() == kMode_Server ) {
        // 此时iperf启动模式为服务器模式
        // 服务器模式的一般流程:
        // socket()-->bind()-->listen()-->accept()-->......
        // 在这里,由于采用Listener,socket()/bind()/listen()功能合并
        // start up a listener
        theListener = new Listener( ext_gSettings );
        //Listener继承于Thread,此处调用Thread类成员函数DeleteSelfAfterRun(),即在
        //线程结束后释放所有变量
        theListener->DeleteSelfAfterRun();

        // Start the server as a daemon(守护进程)
        // 守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。
        // 它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务
        // 或等待处理某些发生的事件。
        if ( gSettings->GetDaemonMode() == true ) {
            theListener->runAsDaemon(argv[0],LOG_DAEMON);
        }
    
        // 启动Listener进程
        // 在这里,Listener的Run方法的运行时通过下面方式来实现:
        // (1)、Listener调用Start(),在Start()中,有一句语句:
        // int err = pthread_create( &mTID, NULL, Run_Wrapper, ptr );<有线程的
        // 情况下>
        // 或者 Run_Wrapper( ptr );<无线程的情况下>
        // (2)、Run_Wrapper( ptr )中,
        // Thread* objectPtr = (Thread*) paramPtr;
        // objectPtr->Run();
        // 其中,Run()是一个虚函数。在某基类中声明为 virtual 并在一个或多个派生类中被
        // 重新定义的成员函数,virtual函数返回类型( 函数名(参数表) {函数体;}),实现
        // 多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
        // 于是我们可以运行Listener中定义的Run()函数,从而实现了所有的启动工作
        theListener->Start();

        if ( ext_gSettings->mThreads == 0 ) {
            // ???个人对sNum到底指代的是什么还是有些不是很明白。在此标记!!!!!(后同)
            // 进程为守护进程,setDaemon中通过条件变量机制实现sNum--,从而将产生的总线程数减1,
            // 使得系统认为此进程已经结束或者已经Run成功,依此实现将此进程与进程组分离开来的目
            // 的(这只是将进程设置成为守护进程的一方面操作)。这样,Listener转为后台进程,可
            // 以一直监听。
            theListener->SetDaemon();

            // the listener keeps going; we terminate on user input only
            waitUntilQuit();

#ifdef HAVE_THREAD
            // 在这里,NumUserThreads()返回的值是sNum,即产生的总进程数。在此,我们希望所有的
            // 线程全部停止、Run_Wrapper或setDaemon。这时,系统便认为服务器端已完成所有线程。
            if ( Thread::NumUserThreads() > 0 ) {
                printf( wait_server_threads );
                fflush( 0 );
            }
#endif
        }
    } 
    else if ( gSettings->GetServerMode() == kMode_Client ) {
        // 此时iperf启动模式为客户端模式
#ifdef HAVE_THREAD
        //在Speaker中会设置一个Listener来监听
        //这里涉及到一些关于Speaker的知识,后面会进行更加深入的说明
        theSpeaker = new Speaker(ext_gSettings);
        theSpeaker->OwnSettings();
        theSpeaker->DeleteSelfAfterRun();
        theSpeaker->Start();
#else
        // If we need to start up a listener do it now
        ext_Settings *temp = NULL;
        Settings::GenerateListenerSettings( ext_gSettings, &temp );
        if ( temp != NULL ) {
            theListener = new Listener( temp );
            theListener->DeleteSelfAfterRun();
        }
        Client* theClient = new Client( ext_gSettings );
        theClient->InitiateServer();
        theClient->Start();
        if ( theListener != NULL ) {
            theListener->Start();
        }
#endif
    } 
    else {
        // neither server nor client mode was specified
        // print usage and exit
        printf( usage_short, argv[0], argv[0] );
    }

    // wait for other (client, server) threads to complete
    // Joinall函数进行的操作是等待所有线程完成
    Thread::Joinall();
    DELETE_PTR( gSettings );  // modified by qfeng
    // all done!
    return 0;
} // end main


/* -------------------------------------------------------------------
 * Any necesary cleanup before Iperf quits. Called at program exit,
 * either by exit() or terminating main().
 * ------------------------------------------------------------------- */
void cleanup( void ) {
    extern Iperf_ListEntry *clients;
    Iperf_destroy ( &clients );

} // end cleanup

    程序首先通过my_signalsignal函数为信号SIGTERM、SIGINT及SIGPIPE安装信号处理程序,这里的my_signal函数在文件lib/signal.c中定义,它完成的功能与signal是一样的,但提高了兼容性。atexit函数可以使程序退出时自动执行cleanup函数完成一些扫尾工作(主要是将服务器维护的客户信息链表释放)。ext_Settings是一个保存设定的参数的结构体,这些值在测量时使用的参数有两种设置方式,或为用户通过命令行指定,或使用默认值。将信息组织为一个结构体有利于信息在各对象间共享和传递。ext_Settings实现对信息的集中封装的好处是,对于多Client和多Server的结构,我们可以很好的通过传递ext_Settings结构体来交流相关信息。Settings类对 ext_Settings结构和在其上进行的操作进行了封装。关于ext_Settings和Settings可以参见src/Settings.hpp 和src/Settings.cpp(后面有具体讲解)。在生成实例gSettings之后,main分表调用gSettings对象的ParseEnvironment方法和ParseCommandLine方法从环境变量命令行读取参数填充到结构ext_gSettings中。由调用的顺序可见,命令行参数的设定将覆 盖环境变量的设定。

    如果是服务器模式,main生成Listener实例,DeleteSelfAfterRun方法是基类Thread实现的方法,它将成员变量 mDeleteSelf设为true,在线程的主过程(Run函数)结束之后,系统会自动释放内存空间(见对Thread类的讨论)。若用户指定 Iperf作为守护进程运行,则main调用runAsDaemon方法使自己在后台运行。Start方法启动Listener线程开始监听,在 Start方法中会调用pthread_create系统调用创建新线程,此后程序就存在两个执行线程了(见前文对Thread类的讨论)。新线程执行Listerner类的Run函数。原线程从Start函数中返回。ext_gSettings的mThreads在客户端表示并行执行的线程数,在服务器端表示服务器总共接收的请示数,为0表示服务器永远运行下去。若为后一种情况,main函数调用Thread基类的SetDaemon函数使自身不再受线程接收同步机制的限制(所有的Thread类的派生类线程的执行结束可以通过Thread类的static成员函数Joinall进行同步, SetDaemon函数使调用线程不在Joinall函数的“join”范围之内,参见前文对Thread类的讨论),之后调用 waitUntilQuit函数。main函数将在waitUntilQuit函数中阻塞,直到所有的线程结束。

    Iperf的服务器端结束过程比较复杂。在一般情况下,Iperf服务器将一直运行,直到用户按下中断键(Ctrl-C等)时程序退出。由于存在多个线程,有的操作系统会把传向进程的信号传递给进程里的每一个线程,因此在主程序退出时必须考虑所有线程的同步问题,这是通过信号处理程序实现的。

sig_exit函数

    在main函数的开始,为SIGTERM和SIGINT信号安装的信号处理函数是sig_exit,其代码如下(在文件lib/signal.c中)。

/* -------------------------------------------------------------------
 * sig_exit
 *
 * Quietly exits. This protects some against being called multiple
 * times. (TODO: should use a mutex to ensure (num++ == 0) is atomic.)
 * ------------------------------------------------------------------- */
void sig_exit( int inSigno ) {
    static int num = 0;
    if ( num++ == 0 ) {
        // 清除读写缓冲区,需要立即把输出缓冲区的数据进行物理写入时
        fflush( 0 );
        exit( 0 );
    }
} /* end sig_exit */

    这里的num声明为静态变量,初始化为0,因为所有线程均使用同一个处理函数(同一份copy),因此当按下Ctrl+C时,至少有一个线程会执行sig_exit函数,此时num值为0,程序调用exit退出。当任一线程调用exit函数时,整个进程(包括它的所有线程)退出。如果在程序退出前有其他的线程也执行了sig_exit函数(在有多个线程收到同一信号的多个副本的情况下),num不为0,将不再调用exit函数。在这里对num是否为0的检验是为了保证在有多个线程收到信号的情况下,exit函数仅被调用一次。

waitUntilQuit函数

/* -------------------------------------------------------------------
 * Blocks the thread until a quit thread signal is sent
 * ------------------------------------------------------------------- */

void waitUntilQuit( void ) {
#ifdef HAVE_THREAD

    // signal handlers send quit signal on ^C and kill
    gQuit_cond.Lock();
    my_signal( SIGTERM, sig_quit );
    my_signal( SIGINT,  sig_quit );

#ifdef HAVE_USLEEP

    // this sleep is a hack to get around an apparent bug? in IRIX
    // where pthread_cancel doesn't work unless the thread
    // starts up before the gQuit_cand.Wait() call below.
    // A better solution is to just use sigwait here, but
    // then I have to emulate that for Windows...
    usleep( 10 );
#endif

    // wait for quit signal
    gQuit_cond.Wait();
    gQuit_cond.Unlock();
#endif
} // end waitUntilQuit

    这里的gQuit_cond是一个条件变量类的实例,条件变量类的定义和实现在lib/Condition.hpp中(所有的方法都实现为inline函数,提高效率)。它封装了POSIX线程间同步的条件变量机制。gQuit_cond的作用就是同步所有线程的结束。在waitUntilQuit中,首先对gQuit_cond加锁,之后将SIGTERM和SIGINT的信号处理函数改为sig_quit, 之后调用条件变量类的Wait方法在gQuit_cond上等待,直到有线程调用gQuit_cond的Signal方法,之后waitUntilQuit退出,主线程回到main函数。

sig_quit函数:

/* -------------------------------------------------------------------
 * Sends a quit thread signal to let the main thread quit nicely.
 * ------------------------------------------------------------------- */

void sig_quit( int inSigno ) {
#ifdef HAVE_THREAD

    // if we get a second signal after 1/10 second, exit
    // some implementations send the signal to all threads, so the 1/10 sec
    // allows us to ignore multiple receipts of the same signal
    static Timestamp* first = NULL;
    if ( first != NULL ) {
        Timestamp now;
        if ( now.subSec( *first ) > 0.1 ) {
            sig_exit( inSigno );
        }
    } else {
        first = new Timestamp();
    }

    // with threads, send a quit signal
    gQuit_cond.Signal();

#else

    // without threads, just exit quietly, same as sig_exit()
    sig_exit( inSigno );

#endif
} // end sig_quit

    sig_quit完成的主要功能是:当某个线程收到第一个中断信号时,调用gQuit_cond的Signal方法唤醒在此变量上等待的主 线程(此时主线程正在waitUntilQuit函数中阻塞)。若在很短时间(0.1s)内有其他线程执行了sig_quit函数,是同一个信号的副本, 因此不进行任何操作;若相隔了一段时间,则说明用户在此按下了Ctrl-C键,要求程序强行退出,此时直接通过sig_exit函数调用exit函数退出。

    回到main函数,到主线程从waitUntilQuit后,如果发现此时线程组内还有正在执行的线程,则打印如下语句:

Waiting for server threads to complete. Interrupt again to force quit.

    如果此时用户在此按下Ctrl-C,则发生上面所述的第二种情况,程序立即结束运行。否则,主线程调用Thread类的Joinall方法(Joinall为Thread类的静态函数成员)等待所有线程的结束,在释放了内存后退出。

    在客户端,main函数的工作比较简单。它生成Speaker类(说者监控线程)的实例,由后者负责生成其他的Client类(说者线程)事例向服务器端发送数据,之后main函数退出。但用户按下Ctrl-C时,线程组立即结束运行。


你可能感兴趣的:(Iperf源代码分析(九))