下面以程序的执行为主线,简要分析一下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_signal和signal函数为信号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时,线程组立即结束运行。