在代理進程適配代碼上做開發時,業務場景下注冊了回調函數。回調函數功能就是執行一些列后台命令,最初使用的system()函數,耗時較長,同時發現執行后,當前進程會崩潰。至於system函數為什么會崩潰,網上有少量的相同情況描述(如 http://bbs.csdn.net/topics/350031745 描述可能是glibc組件版本問題,但目前已經是高版本),據了解也有回調超時,強制復位進程之說。針對system()函數本身的問題,也嘗試過http://blog.sina.com.cn/s/blog_8043547601017qk0.html 推薦的基於管道的popen和pclose函數,但問題仍然存在,因此這里認為是回調函數超時導致。
如果是超時機制,那么命令就不能執行耗時太久。根據業務場景分析,實際上也不需要命令的返回結果,也就是無需等待系統命令執行完再返回,完全可以后台執行。
首先,研究system()函數的原理,實際上是3步:
(1)執行使用fork()復制一個子進程
(2)在子進程中執行exec族函數調用系統命令
(3)子進程執行過程中父進程等待子進程執行完然后回收子進程資源,這里父進程會阻塞到子進程結束。
可以通過ps -ef看到system函數的確是創建子進程來執行系統命令。顯然這里不執行第三步,就可以起到后台執行的作用。但是單純的去掉第三步這種方式雖然能夠達到目的,但是父進程不調用wait或waitpid,並且父進程不結束,那么子進程執行完后會變成僵屍進程,占用系統進程資源。因此單純這樣處理是不行的。
避免僵屍進程,網上提供了多種處理方式,包括:
a.父進程通過wait和waitpid等函數等待子進程結束。單純的等待如同system函數,會導致父進程掛起,顯然不可行。
b.執行的命令后添加后添加&,后台執行。這樣就將后台執行責任交給了調用者,一是無法限制后續開發必須加上&以免進程崩潰問題再現,二是參數可以是多個系統命令,這種情況下需要每條命令都追加&,並且這樣還會導致多條命令之間的時序也無法控制。
c.不處理SIGCHLD信號,調用signal(SIGCHLD,SIG_IGN); 忽略 SIGCHLD信號,可以理解為告訴系統我對子進程的結束不感興趣,系統自己回收就可以了。 這種方式實測是可行的,但是該調用效果需要持續到子進程結束,即這是一種進程狀態,相當於修改了進程屬性,可能影響進程其他功能,考慮到對其本質的理解還不夠透徹,沒有采用這種方法。
d. 先創建一個子進程,子進程再創建孫進程,命令由孫進程處理,子進程直接返回,父進程回收子進程。這樣子進程實際上是不會阻塞的,父進程也能夠及時回收子進程而幾乎沒有阻塞。而子進程結束后,孫進程由init接管,孫進程結束后自動由系統回收,不會變為僵屍進程。
最終選擇的方案是d方案,兼顧了a、b、c方案的不足點,應該可以說是比較穩妥的方法了。另外這里選用的vfork函數,而不是fork函數,由於子孫進程只用於執行exec命令,相比fork函數減少了不必要的復制父進程信息的步驟。注意vfork執行成功則在調用進程返回生成的進程的pid,失敗則返回負數,而在生成的進程中返回為0,父子進程基於返回值走不同分支。實現代碼如下,實際測試通過:
void executeCommandAsyn(char* comand){ int ret=vfork(); if(ret==0) { int retGrandSon=vfork(); if(retGrandSon==0){ execl("/bin/bash", "bash", "-c",comand, (char *)0); }else{ _exit(0); } } else{ logger.error("vfork create son process with retcode : %d",ret); if(ret>0){ if(waitpid(ret, NULL, 0) != ret) /* wait for first child */ { logger.error("waitpid fail with return code %d",ret); } } } }