Mjpeg-streamer源碼分析(一)

 Mjpeg-streamer源碼分析
     
--------------------------------------------------------------------------------------------------
基礎知識:
 條件變量:
  條件變量是利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:
  一個線程等待"條件變量的條件成立"而掛起;另一個線程使"條件成立"(給出條件成立信號)。
  爲了防止競爭,條件變量的使用總是和一個互斥鎖結合在一起。
  
  當程序進入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();
--------------------------------------------------------------------------------------------------

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

發佈了2 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章