作者: 守望,Linux應用開發者,目前在公衆号【程式設計珠玑】 分享Linux/C/C++/資料結構與算法/工具等原創技術文章和學習資源。
有些程式我們希望在一台機器上隻有一個執行個體在運作,我在windows下也遇到過很多類似這樣的程式,如QQ,它隻允許同時運作一個。那麼我們在Linux該如何實作這樣的單例運作的程式呢?
思路
實作這樣的程式方法很多,但是總體思路都是類似的:
- 1.啟動程式,檢測标志,判斷是否有同樣的程式運作,是則2,否則3
- 2.程式退出
- 3.程式啟動,并設定标志,以便下次啟動時檢測
實作方法
按照這種思路,實作的方法有很多種,例如使用ps等指令擷取該程序的程序數,大于0 表示已有運作;啟動後寫一個臨時檔案,如果下次啟動時發現有該檔案,則直接退出;建立一個檔案并加鎖,退出時删除檔案,新的程式啟動時試圖加鎖,如果失敗,則說明已有執行個體運作…… 除了上面說到的這些,可能還有一些其他的實際做法,但是本文介紹一種實用并且也是非常通用的做法,即檔案鎖的方法。
基本原理
程式在啟動後,打開一個program.pid檔案(無則建立),然後試圖去設定檔案鎖(如果還不了解鎖的概念,可以簡單了解為,一旦a寫鎖定了,b就無法進一步寫操作了,除非a釋放鎖),如果設定成功,就将該程式的程序ID寫入該檔案;如果加鎖失敗,那麼說明已經有另外一個執行個體在運作了,則退出此次啟動。而目前已經運作的程式如果退出了,該檔案會自動解除鎖定。 實際上,我們觀察一下/var/run/目錄下,有很多類似這樣的檔案:
$ ls -l /var/run/*.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/acpid.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/atd.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/crond.pid-rw-r--r-- 1 root root 5 11月 24 09:08 /var/run/dhclient-wlp3s0.pid-rw-r--r-- 1 root root 4 11月 24 08:19 /var/run/docker.pid
不過這個位置通常隻有root使用者能夠寫入。
實作
在看代碼實作之前,先看下檔案鎖(記錄鎖)和fcntl函數。 flock結構至少包含以下字段 :
struct flock { short l_type; /*鎖類型 F_RDLCK,F_WRLCK, F_UNLCK*/ short l_whence; /* 偏移開始的位置 SEEK_SET, SEEK_CUR, SEEK_END */ off_t l_start; /* 開始鎖定區域*/ off_t l_len; /* 鎖定位元組數 */ pid_t l_pid; /* 記錄鎖的程序ID*/};
從結構體成員可以看到,它不僅可以鎖整個檔案,還可以鎖某個區域,但是本文僅用于鎖定整個檔案。
int fcntl(int fd, int cmd, ... /* arg */
fcntl函數的cmd選項也很多,本文隻用到F_SETLK(或F_SETLKW,),即設定鎖。 結合以上兩者,參考代碼如下:
//來源:公衆号【程式設計珠玑】//部落格:https://www.yanbinghu.com#include #include #include #include #include #include #include #define PROC_NAME "single_instance"#define PID_FILE_PATH "/var/run/"static int lockFile(int fd);static int isRunning(const char *procname);int main(void){ /*判斷是否已經有執行個體在運作*/ if(isRunning(PROC_NAME)) { exit(-1); } printf("run ok
"); sleep(20);//避免程式立即退出 return 0;}/*鎖檔案還可以使用flock,目的是類似的。不過是它是BSD系統調用,并且某些版本不支援NFS,出于移植性考慮,使用fcntl*/static int lockFile(int fd){ struct flock fl; fl.l_type = F_WRLCK;//設定寫鎖 fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; fl.l_pid = -1;//鎖定檔案,設定為-1 return(fcntl(fd, F_SETLK, &fl));}static int isRunning(const char *procname){ char buf[16] = {0}; char filename[128] = {0}; sprintf(filename, "%s%s.pid",PID_FILE_PATH,procname); int fd = open(filename, O_CREAT|O_RDWR );//可讀可寫,不存在時建立 if (fd 0) { printf("open file %s failed!
", filename); return 1; } if (-1 == lockFile(fd)) /*嘗試加鎖*/ { printf("%s is already running
", procname); close(fd); return 1; } else { ftruncate(fd, 0);/*截斷檔案,重新寫入pid*/ sprintf(buf, "%ld", (long)getpid()); write(fd, buf, strlen(buf) + 1); return 0; }}
編譯運作
$ gcc -o single_instance single_instance.c$ ./single_instance #注意root權限運作,或者調整pid檔案位置run ok
檢視pid檔案目錄下已經有了pidfile:
$ ls -al /var/run/single_instance.pid-rw-r--r-- 1 root root 6 11月 24 11:36 /var/run/single_instance.pid
在另外一個終端再次嘗試運作:
$ ./single_instancesingle_instance is already running
如果你想控制同一個目錄下的bin檔案隻能運作一個,那麼可以設定pid檔案的位置為目前目錄。 這種方式有什麼特點呢:
- 簡單可靠
- 可讀可見,相比于信号量或共享記憶體,它更容易觀察
- 無性能要求,啟動時加鎖,結束釋放。
- 一旦出現異常沒有釋放,也可以手動删除檔案
當然對于BSD系統,還可以使用下面的接口來完成:
struct pidfh *pidfile_open(const char *path, mode_t mode, pid_t *pidptr);int pidfile_write(struct pidfh *pfh);int pidfile_close(struct pidfh *pfh);int pidfile_remove(struct pidfh *pfh);
本文就不過多介紹了。
總結
單例運作的基本原理是類似的,而pid檔案是一種常見的單例運作的方式,很多知名的開源元件都是使用類似的方式。對于shell腳本,還可以使用flock指令進行類似的操作。 你一般用什麼方式來實作單例運作呢?歡迎留言分享。
●編号618,輸入編号直達本文
●輸入m擷取文章目錄
C語言與C++程式設計
分享C/C++技術文章