1. 程式人生 > >MJPG-Streamer原始碼分析(一)

MJPG-Streamer原始碼分析(一)

--------------------------------------------------------------------------------------------------

另一片篇推薦的博文:http://www.armbbs.net/forum.php?mod=viewthread&tid=17434
基礎知識:
 條件變數:
  條件變數是利用執行緒間共享的全域性變數進行同步的一種機制,主要包括兩個動作:
  一個執行緒等待"條件變數的條件成立"而掛起;另一個執行緒使"條件成立"(給出條件成立訊號)。
  為了防止競爭,條件變數的使用總是和一個互斥鎖結合在一起。
  
  當程式進入pthread_cond_wait等待後,將會把g_mutex進行解鎖,
  當離開pthread_cond_wait之前,g_mutex會重新加鎖。所以在main中的g_mutex會被加鎖。
  
 動態連結庫的操作函式:
  #include <dlfcn.h>
  void *dlopen(const char *filename, int flag); /* 開啟動態連結庫,返回動態庫控制代碼handle */
  char *dlerror(void);    /* 返回由於dlopen(),dlsym()或者dlclose()產生的錯誤 */
  void *dlsym(void *handle, const char *symbol); /* 通過handle,獲得動態庫內函式的地址,之後通過該地址呼叫動態庫內的函式 */
  int dlclose(void *handle);   /* 關閉動態庫 */
  Link with -ldl.     /* 注意,程式在編譯的時候要用-ldl */
  
 字串操作函式:
  #include <string.h>
  char *strchr(const char *s, int c);  /* 返回字串s第一次出現c的指標 */
  char *strrchr(const char *s, int c);  /* 返回字串s最後一次出現c的指標 */
  char *strdup(const char *s);   /* 複製字串s,返回指向新字串的指標(malloc\free) */
  char *strndup(const char *s, size_t n);  /* 複製字串s最多n個字元,如果s正好有n個,'\0'將自動被新增 */
  char *strdupa(const char *s);   /* 呼叫alloca函式在站內分配記憶體 */
  char *strndupa(const char *s, size_t n); /* 呼叫alloca函式在站內分配記憶體 */


  
 守護程序:
  守護程序最重要的特性是後臺執行;其次,守護程序必須與其執行前的環境隔離開來。
  這些環境包括未關閉的檔案描述符,控制終端,會話和程序組,工作目錄以及檔案建立掩模等。
  這些環境通常是守護程序從執行它的父程序(特別是shell)中繼承下來的;
  最後,守護程序的啟動方式有其特殊之處------它可以在 Linux系統啟動時從啟動指令碼/etc/rc.d中啟動,
  可以由作業規劃程序crond啟動,還可以由使用者終端(通常是shell)執行。
  總之,除開這些特殊性以外,守護程序與普通程序基本上沒有什麼區別,
  因此,編寫守護程序實際上是把一個普通程序按照上述的守護程序的特性改造成為守護程序。
  
 守護程序的程式設計要點
  1. 在後臺執行。
  為避免掛起控制終端將Daemon放入後臺執行。方法是在程序中呼叫fork使
  父程序終止, 讓Daemon在子程序中後臺執行。
  if(pid=fork()) exit(0); //是父程序,結束父程序,子程序繼續
  2. 脫離控制終端,登入會話和程序組
  有必要先介紹一下Linux中的程序與控制終端,登入會話和程序組之間的關係:
  程序屬於 一個程序組,程序組號(GID)就是程序組長的程序號(PID)。
  登入會話可以包含多個程序組。這些程序組共享一個控制終端。這個控制終端通常是建立程序的登入終端。 
  控制終端,登入會話和程序組通常是從父程序繼承下來的。我們的目的就是要擺脫它們 ,使之不受它們的影響。
  方法是在第1點的基礎上,呼叫setsid()使程序成為會話組長:
  setsid();
  說明:當程序是會話組長時setsid()呼叫失敗。但第一點已經保證程序不是會話組長。
  setsid()呼叫成功後,程序成為新的會話組長和新的程序組長,並與原來的登入會話和程序組脫離。
  由於會話過程對控制終端的獨佔性,程序同時與控制終端脫離。
  3. 禁止程序重新開啟控制終端
  現在,程序已經成為無終端的會話組長。但它可以重新申請開啟一個控制終端。
  可以通過使程序不再成為會話組長來禁止程序重新開啟控制終端:
  if(pid=fork()) exit(0); //結束第一子程序,第二子程序繼續(第二子程序不再是會話組長)
  4. 關閉開啟的檔案描述符
  程序從建立它的父程序那裡繼承了開啟的檔案描述符。
  如不關閉,將會浪費系統資源, 造成程序所在的檔案系統無法卸下以及引起無法預料的錯誤。
  按如下方法關閉它們:
  for(i=0;i 關閉開啟的檔案描述符close(i);
  5. 改變當前工作目錄
  程序活動時,其工作目錄所在的檔案系統不能卸下。一般需要將工作目錄
  改變到根目錄 。對於需要轉儲核心,寫執行日誌的程序將工作目錄改變到特定目錄如/tmp
  chdir("/")
  6. 重設檔案建立掩模
  程序從建立它的父程序那裡繼承了檔案建立掩模。它可能修改守護程序所建立的檔案的存取位。
  為防止這一點,將檔案建立掩模清除:
  umask(0);
  7. 處理SIGCHLD訊號
  處理SIGCHLD訊號並不是必須的。但對於某些程序,特別是伺服器程序往往在請求到來時生成子程序處理請求。
  如果父程序不等待子程序結束,子程序將成為殭屍程序(zombie )從而佔用系統資源。
  如果父程序等待子程序結束,將增加父程序的負擔,影響伺服器 程序的併發效能。
  在Linux下可以簡單地將SIGCHLD訊號的操作設為SIG_IGN。
  signal(SIGCHLD,SIG_IGN);
  這樣,核心在子程序結束時不會產生殭屍程序。
  這一點與BSD4不同,BSD4下必須顯式等待子程序結束才能釋放殭屍程序.
  
   關於/dev/null及用途
    把/dev/null看作"黑洞". 它非常等價於一個只寫檔案. 
    所有寫入它的內容都會永遠丟失. 而嘗試從它那兒讀取內容則什麼也讀不到. 
    然而, /dev/null對命令列和指令碼都非常的有用.
    禁止標準輸出.
  1 cat $filename >/dev/null
     2 # 檔案內容丟失,而不會輸出到標準輸出.
  禁止標準錯誤
  1 rm $badname 2>/dev/null
     2 # 這樣錯誤資訊[標準錯誤]就被丟到太平洋去了.
  禁止標準輸出和標準錯誤的輸出.
  1 cat $filename 2>/dev/null >/dev/null
     2 # 如果"$filename"不存在,將不會有任何錯誤資訊提示.
     3 # 如果"$filename"存在, 檔案的內容不會列印到標準輸出.
     4 # 因此Therefore, 上面的程式碼根本不會輸出任何資訊.
     5 # 當只想

測試命令的退出碼而不想有任何輸出時非常有用。
     6 #-----------測試命令的退出 begin ----------------------#
     7 # ls dddd 2>/dev/null 8 
     8 # echo $?    //輸出命令退出程式碼:0為命令正常執行,1-255為有出錯。  
     9 #-----------測試命令的退出 end-----------#  
     10# cat $filename &>/dev/null 
     11 #也可以, 由 Baris Cicek 指出.
  清除日誌檔案內容
  1 cat /dev/null > /var/log/messages
     2 #  : > /var/log/messages   有同樣的效果, 但不會產生新的程序.(因為:是內建的)
     3 
     4 cat /dev/null > /var/log/wtmp
  例子 28-1. 隱藏cookie而不再使用
  1 if [ -f ~/.netscape/cookies ]  # 如果存在則刪除.
     2 then
     3   rm -f ~/.netscape/cookies
     4 fi
     5 
     6 ln -s /dev/null ~/.netscape/cookies
     7 # 現在所有的cookies都會丟入黑洞而不會儲存在磁碟上了.
    
--------------------------------------------------------------------------------------------------
首先,分析該軟體的結構體:
--------------------------------------------------------------------------------------------------
globals結構體:
--------------------------------------------------------------------------------------------------
 typedef struct _globals globals; /* mjpg-streamer只支援一個輸入外掛,多個輸出外掛 */
 struct _globals {
   int stop;    /* 一個全域性標誌位 */
   pthread_mutex_t db;   /* 互斥鎖,資料鎖 */
   pthread_cond_t  db_update;  /* 條件變數,資料更新的標誌 */
   unsigned char *buf;   /* 全域性JPG幀的緩衝區的指標 */
   int size;    /* 緩衝區的大小 */
   input in;    /* 輸入外掛,一個輸入外掛可對應多個輸出外掛 */
   output out[MAX_OUTPUT_PLUGINS];  /* 輸出外掛,以陣列形式表示 */
   int outcnt;    /* 輸出外掛的數目 */
 };
--------------------------------------------------------------------------------------------------
input結構體:
 /* structure to store variables/functions for input plugin */
 typedef struct _input input;
 struct _input {
   char *plugin;    /* 動態連結庫的名字,或者是動態連結庫的地址 */
   void *handle;    /* 動態連結庫的控制代碼,通過該控制代碼可以呼叫動態庫中的函式 */
   input_parameter param;  /* 外掛的引數 */
   int (*init)(input_parameter *); /* 四個函式指標 */
   int (*stop)(void);
   int (*run)(void);
   int (*cmd)(in_cmd_type, int);  /* 處理命令的函式 */
 };
 
/* parameters for input plugin */
 typedef struct _input_parameter input_parameter;
 struct _input_parameter {
   char *parameter_string;
   struct _globals *global;
 };
-------------------------------------------------------------------------------------------------- 
output結構體:
 /* structure to store variables/functions for output plugin */
 typedef struct _output output;
 struct _output {
   char *plugin;    /* 外掛的名字 */
   void *handle;    /* 動態連結庫的控制代碼,通過該控制代碼可以呼叫動態庫中的函式 */
   output_parameter param;  /* 外掛的引數 */
   int (*init)(output_parameter *); /* 四個函式指標 */
   int (*stop)(int);
   int (*run)(int);
   int (*cmd)(int, out_cmd_type, int); /* 處理命令的函式 */
 };

/* parameters for output plugin */
 typedef struct _output_parameter output_parameter;
 struct _output_parameter {
   int id;    /* 用於標記是哪一個輸出外掛的引數 */
   char *parameter_string;
   struct _globals *global;
 };
--------------------------------------------------------------------------------------------------
現在開始分析main()函式:
--------------------------------------------------------------------------------------------------
預設情況下,程式會將video0作為輸入,http的8080埠作為輸出  (fps = frames per second)
 char *input  = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0";
 char *output[MAX_OUTPUT_PLUGINS];  /* 一個輸入最大可以對應10個輸出 */
 output[0] = "output_http.so --port 8080"; /* 將video0作為輸入,http的8080埠作為輸出 */
-------------------------------------------------------------------------------------------------- 
下面是一個while()迴圈,來解析main()函式後面所帶的引數
 /* parameter parsing */
 while(1) {
 int option_index = 0, c=0;
 static struct option long_options[] = \ /* 長選項表,進行長選項的比對 */
 {
  {"h", no_argument, 0, 0},  /* 第一個引數為選項名,前面沒有短橫線。譬如"help"、"verbose"之類 */
  {"help", no_argument, 0, 0},  /* 第二個引數描述了選項是否有選項引數 |no_argument 0 選項沒有引數|required_argument 1 選項需要引數|optional_argument 2 選項引數可選|*/
  {"i", required_argument, 0, 0},  /* 第三個引數指明長選項如何返回,如果flag為NULL,則getopt_long返回val。
  {"input", required_argument, 0, 0},  * 否則返回0,flag指向一個值為val的變數。如果該長選項沒有發現,flag保持不變.
  {"o", required_argument, 0, 0},   */
  {"output", required_argument, 0, 0}, /* 第四個引數是發現了長選項時的返回值,或者flag不是NULL時載入*flag中的值 */
  {"v", no_argument, 0, 0},
  {"version", no_argument, 0, 0},
  {"b", no_argument, 0, 0},  /* 每個長選項在長選項表中都有一個單獨條目,該條目裡需要填入正確的數值。陣列中最後的元素的值應該全是0。
  {"background", no_argument, 0, 0},  *陣列不需要排序,getopt_long()會進行線性搜尋。但是,根據長名字來排序會使程式設計師讀起來更容易. 
  {0, 0, 0, 0}     */     
 };
--------------------------------------------------------------------------------------------------
 c = getopt_long_only(argc, argv, "", long_options, &option_index);
--------------------------------------------------------------------------------------------------
下面重點分析一下getopt_long_only函式:
 int getopt_long_only(int argc, char * const argv[],const char *optstring,const struct option *longopts, int *longindex);
 該函式每解析完一個選項,就返回該選項字元。
 如果選項帶引數,引數儲存在optarg中。如果選項帶可選引數,而實際無引數時,optarg為NULL。
 當遇到一個不在optstring指明的選項時,返回字元'?'。如果在optstring指明某選項帶引數而實際沒有引數時,返回字元'?'或者字元':',視optstring的第一個字元而定。這兩種情況選項的實際值被儲存在optopt中。
 當解析錯誤時,如果opterr為1則自動列印一條錯誤訊息(預設),否則不列印。
 當解析完成時,返回-1。
 每當解析完一個argv,optind就會遞增。如果遇到無選項引數,getopt預設會把該引數調後一位,接著解析下一個引數。如果解析完成後還有無選項的引數,則optind指示的是第一個無選項引數在argv中的索引。
 最後一個引數longindex在函式返回時指向被搜尋到的選項在longopts陣列中的下標。longindex可以為NULL,表明不需要返回這個值
-------------------------------------------------------------------------------------------------- 
 /* no more options to parse */
 if (c == -1) break;
 /* unrecognized option */
 if(c=='?'){ help(argv[0]); return 0; }
 switch (option_index) {
 /* h, help */
 case 0:
 case 1:
 help(argv[0]);
 return 0;
 break;
 /* i, input */
 case 2:
 case 3:
 input = strdup(optarg);
 break;
 /* o, output */
 case 4:
 case 5:
 output[global.outcnt++] = strdup(optarg);
 break;
 /* v, version */
 case 6:
 case 7:
 printf("MJPG Streamer Version: %s\n" \
 "Compilation Date.....: %s\n" \
 "Compilation Time.....: %s\n", SOURCE_VERSION, __DATE__, __TIME__);
 return 0;
 break;
 /* b, background */
 case 8:
 case 9:
 daemon=1;
 break;
 default:
 help(argv[0]);
 return 0;
 }
 }
--------------------------------------------------------------------------------------------------
好,現在分析一下該程式是否需要成為守護程序:
  /* fork to the background */
  if ( daemon ) {     /* 當命令後面設定了b命令時,daemon就會被置為1 */
   LOG("enabling daemon mode");
   daemon_mode(); 
  }
 現在看一看daemon_mode()時如何建立守護程序的
   void daemon_mode(void) {
     int fr=0;
     fr = fork();
     if( fr < 0 ) {   /* fork失敗  */
       fprintf(stderr, "fork() failed\n");
       exit(1);
     }
     if ( fr > 0 ) {   /* 結束父程序,子程序繼續  */
       exit(0);
     }
     if( setsid() < 0 ) {   /* 建立新的會話組,子程序成為組長,並與控制終端分離 */
       fprintf(stderr, "setsid() failed\n");
       exit(1);
     }
     fr = fork();    /* 防止子程序(組長)獲取控制終端 */ 
     if( fr < 0 ) {   /* fork錯誤,退出 */
       fprintf(stderr, "fork() failed\n");
       exit(1);
     }
     if ( fr > 0 ) {   /* 父程序,退出 */
       fprintf(stderr, "forked to background (%d)\n", fr);
       exit(0);
     }     /* 第二子程序繼續執行 , 第二子程序不再是會會話組組長*/
     umask(0);    /* 重設檔案建立掩碼 */
     chdir("/");    /* 切換工作目錄 */ 
     close(0);    /* 關閉開啟的檔案描述符*/
     close(1);
     close(2);
     open("/dev/null", O_RDWR);  /* 將0,1,2重定向到/dev/null */  
     dup(0);
     dup(0);
   }
--------------------------------------------------------------------------------------------------
初始化global全域性變數
 global.stop      = 0;
 global.buf       = NULL;
 global.size      = 0;
 global.in.plugin = NULL;
--------------------------------------------------------------------------------------------------
同步全域性影象緩衝區:
 pthread_mutex_init(&global.db, NULL);
 pthread_cond_init(&global.db_update, NULL);
--------------------------------------------------------------------------------------------------
忽略SIGPIPE訊號(當關閉TCP sockets時,OS會發送該訊號)
 signal(SIGPIPE, SIG_IGN);
--------------------------------------------------------------------------------------------------
註冊<CTRL>+C訊號處理函式,來結束該程式
 signal(SIGINT, signal_handler);
 ------------------------------------------------------------------------------------------
  void signal_handler(int sig)
  {
    int i;
    /* signal "stop" to threads */
    LOG("setting signal to stop\n");
    global.stop = 1;
    usleep(1000*1000);
    /* clean up threads */
    LOG("force cancelation of threads and cleanup ressources\n");
    global.in.stop();
    for(i=0; i<global.outcnt; i++) {
      global.out[i].stop(global.out[i].param.id);
    }
    usleep(1000*1000);
    /* close handles of input plugins */
    dlclose(&global.in.handle);
    for(i=0; i<global.outcnt; i++) {
      dlclose(global.out[i].handle);
    }
    DBG("all plugin handles closed\n");
    pthread_cond_destroy(&global.db_update);
    pthread_mutex_destroy(&global.db);
    LOG("done\n");
    closelog();
    exit(0);
    return;
  }
 ------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
開啟輸入外掛:
   tmp = (size_t)(strchr(input, ' ')-input);     
   global.in.plugin = (tmp > 0)?strndup(input, tmp):strdup(input); /* 在命令中獲得動態庫 */
   global.in.handle = dlopen(global.in.plugin, RTLD_LAZY);  /* 開啟動態連結庫 */
   global.in.init = dlsym(global.in.handle, "input_init");  /* 獲得動態庫內的input_init()函式 */
   global.in.stop = dlsym(global.in.handle, "input_stop");  /* 獲得動態庫內的input_stop()函式 */
   global.in.run = dlsym(global.in.handle, "input_run");   /* 獲得動態庫內的input_run()函式 */
   /* try to find optional command */
   global.in.cmd = dlsym(global.in.handle, "input_cmd");   /* 獲得動態庫內的input_cmd()函式 */
   global.in.param.parameter_string = strchr(input, ' ');  /* 將命令引數的起始地址賦給para.parameter_string,已經去掉前賣弄的動態庫 */
   global.in.param.global = &global;     /* 將global結構體的地址賦給param.global */
   global.in.init(&global.in.param);     /* 傳遞global.in.param給init,進行初始化 */
   }
--------------------------------------------------------------------------------------------------
開啟輸出外掛:
 for (i=0; i<global.outcnt; i++) {   /* 因為是一個輸入對應多個輸出,所以輸出採用了for迴圈 */
  tmp = (size_t)(strchr(output[i], ' ')-output[i]);
  global.out[i].plugin = (tmp > 0)?strndup(output[i], tmp):strdup(output[i]);
  global.out[i].handle = dlopen(global.out[i].plugin, RTLD_LAZY);
  global.out[i].init = dlsym(global.out[i].handle, "output_init");
  global.out[i].stop = dlsym(global.out[i].handle, "output_stop");
  global.out[i].run = dlsym(global.out[i].handle, "output_run");
  /* try to find optional command */
  global.out[i].cmd = dlsym(global.out[i].handle, "output_cmd");
  global.out[i].param.parameter_string = strchr(output[i], ' ');
  global.out[i].param.global = &global;
  global.out[i].param.id = i;
  global.out[i].init(&global.out[i].param);
 }
--------------------------------------------------------------------------------------------------
開始執行輸入外掛的run()函式:
 global.in.run();
--------------------------------------------------------------------------------------------------
開始執行輸出外掛的run()函式:
 for(i=0; i<global.outcnt; i++) 
  global.out[i].run(global.out[i].param.id);
--------------------------------------------------------------------------------------------------
執行完以上函式,該程序進入休眠狀態,等待使用者按下<CTRL>+C結束所有的程序:
 pause();
--------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------

整個mjpg-streamer.c流程圖


花了一下午加一晚上的時間閱讀和理解mjpg-streamer的原始碼。結合上圖對它的理解就是:output外掛預設用http協議。server_thread主要做socket的初始化操作,當有一個socket連線上之後,就將這個socket的服務放到一個client_thread中處理。雖說Http是一次應答式,即有一次請求才有一次相應,但是因為mjpg-streamer是一個守護程序,所以會一直解析http請求併發送video stream.

才疏學淺,不知道這樣理解對不對,有熟悉mjpg-streamer的大神請賜教!