2020-02-22
關鍵字:alarm()、setitimer()、攔截定時器信號、定時器信號有效范圍
在 Linux 中實現定時器功能的比較簡單且好用的系統內置的方法有兩種:
1、alarm() 函數
2、setitimer() 函數
這兩種定時方式都是通過信號(signal)來通知定時到期的。
1、alarm() 函數實現定時功能
alarm()函數的簽名如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds);
參數1 是你想定時的秒數。
返回值是在你調用這個函數的時刻,上一次的 alarm() 所剩余的秒數。例如,你上一次調用了 alarm(10),7秒鍾以后你再次調用了alarm(6),那么在 alarm(6) 時返回的值就是 3 。
alarm() 的定時是一次性的,若想實現循環定時功能,則需要手動在本次定時結束時再次啟動 alarm() 定時。alarm() 在定時到時后會發出一個 SIGALRM 信號,所以還需要我們攔截這一信號才能接收到定時回調結果。
以下貼出通過 alarm() 實現定時功能的示例源碼:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> static int counter; void sig_alarm_handler(int); int main() { printf("hello world.\n"); printf("pid number:%d\n", getpid()); //攔截定時器信號。 sighandler_t *pre = signal(SIGALRM, sig_alarm_handler); printf("signal() return ret address:%p,my sig_alm_handler:%p\n", pre, sig_alarm_handler); //pre應該是空才對。 //設定定時器。 int remaing = alarm(1); printf("alarm remaing:%d\n", remaing);//remaing 應該是0才對。 counter = 0; while(counter < 7) { usleep(500); } //主動關閉定時器。 alarm(0); printf("bye\n"); return 0; } void sig_alarm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d\n", __FUNCTION__, sig_num, counter); if(sig_num = SIGALRM) { counter++; int remaing = alarm(1); printf("re-alarm remaing:%d\n", remaing); } }
這一程序的運行結果打印如下:
root@xxx:/system/bin # ./myblog hello world. pid number:4120 signal() return ret address:0x0,my sig_alm_handler:0xb6fca47d alarm remaing:0 sig_alarm_handler, signal number:14, counter:0 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:1 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:2 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:3 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:4 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:5 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:6 re-alarm remaing:0 bye root@xxx:/system/bin #
2、setitimer() 函數實現定時功能
setitimer()函數的簽名如下所示:
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
參數1 表示要啟動的定時器類型,setitimer 方式為每個進程提供了三種類型的定時器:1、ITIMER_REAL;2、ITIMER_VIRTUAL;3、ITIMER_PROF;第1種定時器一旦啟動立即執行定時計時,定時結束后產生一個 SIGALRM 信號並可根據啟動前的配置決定是否自動重啟下一輪定時。第2種定時器在啟動后將只在進程處於運行態時會工作,當進程處於非運行態時,定時工作也將暫停。且這種定時器在定時結束后會產生一個 SIGVTALRM 信號。第3種定時器類型暫且不理。
參數2 是用於描述定時器下一輪要執行的規則的,說白了就是你想定時多久就在這里設置。結構體 struct itimerval 的原型如下圖所示:
參數3 一般不用理會。
返回值為0表示啟動定時器成功,失敗時返回-1並設置相應的錯誤信息。
setitimer() 實現定時的方式會稍微復雜一點,但同樣它的功能相較於 alarm() 也更強大一點。
alarm() 方式的定時精度只能達到“秒級”,但 setitimer() 卻可以達到 “微秒級”。同時,setitimer() 還可以根據進程的不同運行狀態來控制定時功能的運行狀態。setitimer() 是一個可以在本輪定時任務完成后自動重啟下一輪定時的定時機制,當然是否自動重啟取決於啟動定時器時參數2的值。在上面函數參數2的釋義中,struct itimerval 中的 it_value 成員表示定時器當前的值,它可以設置一個秒值及微秒值,其實就是你想讓這個定時器在多少時間以后啟動,立即啟動則將 it_value 的值全設成0。而 it_interval 則表示你想定時多少時間。假如我們給 it_value 設置了一個值,但 it_interval 卻全設成0,就表示定時器將在 it_value 設置的時間以后發出一個定時到時信號,此后就不會再自動重啟下一輪定時任務了。
取消一個正在運行的 itimerval 定時器的方法是調用以下函數:
//方式一 setitimer(type, NULL, NULL);//type為 ITIMER_REAL 或 ITIMER_VIRTUAL 或 ITIMER_PROF //方式二 struct itimerval itv; itv.it_interval.tv_sec = 0; itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 0; itv.it_value.tv_usec = 0; setitimer(type, &itv, NULL);
如果想查詢正在運行着的 itimerval 定時器的狀態信息,比如當前運行到什么狀態了,還有多少時間定時結束,下一輪定時時間被設成多少,則可以調用以下函數:
#include <sys/time.h> int getitimer(int which, struct itimerval *curr_value);
參數1 是定時器的類型,即前面描述的三種類型之一。
參數2 是一個用於保存結果的結構體對象,需要注意這里不能傳空指針,否則將查詢不到結果。
返回值為0表示查詢成功,可以通過 curr_value 查看結果。返回值為-1則表示查詢失敗。
以下是一個通過 setitimer() 函數實現的定時器功能的示例源碼:
#include <stdio.h> #include <signal.h> #include <sys/time.h> void sig_alm_handler(int sig_num); static int counter; int main() { printf("hello world.\n"); //攔截定時器信號。 sighandler_t *pre = signal(SIGALRM, sig_alm_handler); printf("pre-sighandler address:%p\n", pre); //pre應該是NULL. struct itimerval olditv; struct itimerval itv; itv.it_interval.tv_sec = 1; //定時周期為1秒鍾。 itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 3; //定時器啟動以后將在3秒又500微秒以后正式開始計時。 itv.it_value.tv_usec = 500; setitimer(ITIMER_REAL, &itv, &olditv); while(counter < 7) { usleep(500); } //try to cancle the timer. int counter2 = 0; while(counter2 < 7) { /* 通過這個while可以監測到定時器是否成功被停止, 若未停止,則仍舊可以在每秒看到定時信號回調函數中的打印。 通過這個while可以確保定時器是真的被我們的設置而停掉的 而非因為程序結束才強制停止的。 */ sleep(1); //try to stop the timer. setitimer(ITIMER_REAL, NULL, NULL); printf("try cancled!,counter2:%d\n", counter2); counter2++; } printf("\nBye.\n"); return 0; } void sig_alm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d\n", __FUNCTION__, sig_num, counter); if(sig_num = SIGALRM) { counter++; } }
這段程序運行的結果打印如下:
root@xxx:/system/bin # ./myblog hello world. pre-sighandler address:0x0 sig_alm_handler, signal number:14, counter:0 sig_alm_handler, signal number:14, counter:1 sig_alm_handler, signal number:14, counter:2 sig_alm_handler, signal number:14, counter:3 sig_alm_handler, signal number:14, counter:4 sig_alm_handler, signal number:14, counter:5 sig_alm_handler, signal number:14, counter:6 sig_alm_handler, signal number:14, counter:7 try cancled!,counter2:0 try cancled!,counter2:1 try cancled!,counter2:2 try cancled!,counter2:3 try cancled!,counter2:4 try cancled!,counter2:5 try cancled!,counter2:6 Bye. root@xxx:/system/bin #
3、番外篇
這兩種定時器的的有效范圍都僅在本進程內。不必擔心調用系統定時器並產生的 SIGALRM 信號會被其它進程通過攔截定時信號而接收到。以下有相應的源碼與打印可以證明。
證明的原理也不難,就是在兩種定時方式將定時器啟動以后 fork() 一個子進程出來,並在定時信號回調函數中打印出當前進程號,查看這個回調是哪一個進程中調用的。同時在 fork() 出的子進程中主動發送一個定時信號以證明在子進程中定時信號攔截也是生效的。
以下是 alarm() 方式的源碼與打印:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> static int counter; static int is_sub_process; void sig_alarm_handler(int); int main() { printf("hello world.\n"); printf("pid number:%d\n", getpid()); is_sub_process = 0; //攔截定時器信號。 sighandler_t *pre = signal(SIGALRM, sig_alarm_handler); printf("signal() return ret address:%p,my sig_alm_handler:%p\n", pre, sig_alarm_handler); //pre應該是空才對。 //設定定時器。 alarm(1); counter = 0; pid_t pno = fork(); printf("pno:%d\n", pno); is_sub_process = !pno; if(is_sub_process) { raise(SIGALRM);//子進程中主動發一個定時結束信號。 } while(counter < 3) { usleep(500); } //主動關閉定時器。 alarm(0); printf("bye\n"); return 0; } void sig_alarm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d,pid:%d\n", __FUNCTION__, sig_num, counter, getpid()); if(sig_num = SIGALRM) { counter++; if(!is_sub_process) { alarm(1); } } }
打印如下:
root@xxx:/system/bin # ./myblog hello world. pid number:5272 signal() return ret address:0x0,my sig_alm_handler:0xb6efe4d5 pno:5273 pno:0 sig_alarm_handler, signal number:14, counter:0,pid:5273 sig_alarm_handler, signal number:14, counter:0,pid:5272 sig_alarm_handler, signal number:14, counter:1,pid:5272 sig_alarm_handler, signal number:14, counter:2,pid:5272 bye root@xxx:/system/bin #
通過打印可以很明顯地發現,主、子進程都有在攔截 SIGALRM 信號,並且只有主進程能接收到定時信號。且在主進程定時結束程序退出以后,通過 ps 仍能發現子進程在不斷運行當中,如下圖:
子進程不退出的原因就是因為子進程沒有定時信號,counter 變量一直不增加,導致 while(1) 一直無法退出。
以下是 setitimer() 方式的源碼與打印:
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/time.h> void sig_alm_handler(int sig_num); static int counter; static int is_sub_process; int main() { printf("hello world.\n"); is_sub_process = 0; //攔截定時器信號。 sighandler_t *pre = signal(SIGALRM, sig_alm_handler); printf("pre-sighandler address:%p\n", pre); //pre應該是NULL. pid_t pno = fork(); printf("pno:%d\n", pno); if(pno >= 0) { is_sub_process = !pno; if(is_sub_process) { //子進程模擬一個SIGALRM信號出來。 raise(SIGALRM); } else { //僅主進程啟動定時器。 struct itimerval itv; itv.it_interval.tv_sec = 1; //定時周期為1秒鍾。 itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 3; //定時器啟動以后將在3秒又500微秒以后正式開始計時。 itv.it_value.tv_usec = 500; setitimer(ITIMER_REAL, &itv, NULL); } } while(counter < 3) { usleep(500); } if(!is_sub_process) { //僅主進程才需要停止 struct itimerval curitv; memset(&curitv, 0, sizeof(struct itimerval)); setitimer(ITIMER_REAL, NULL, &curitv);//定時器停止之前的定時計數值狀態將被保存在 curitv 中。 printf("The status before timer cancle is sec:%d, usec:%d, isec:%d, iusec:%d\n", curitv.it_value.tv_sec, curitv.it_value.tv_usec, curitv.it_interval.tv_sec, curitv.it_interval.tv_usec); } printf("\nBye.\n"); return 0; } void sig_alm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d,pid:%d\n", __FUNCTION__, sig_num, counter, getpid()); if(sig_num = SIGALRM) { counter++; } }
setitimer() 的驗證機制與 alarm() 的稍有不同,它是在主進程中才去啟動定時器,但得到的結果卻與 alarm() 的方式完全一樣。這兩份代碼在某種程度上可以認為是一個雙盲對照實驗了,由此可以充分證明 alarm() 與 setitimer() 所啟動的定時器及產生的定時信號均只在本進程內有效。
以下是 setitimer() 方式驗證的打印:
root@xxx:/system/bin # ./myblog hello world. pre-sighandler address:0x0 pno:5323 pno:0 sig_alm_handler, signal number:14, counter:0,pid:5323 sig_alm_handler, signal number:14, counter:0,pid:5322 sig_alm_handler, signal number:14, counter:1,pid:5322 sig_alm_handler, signal number:14, counter:2,pid:5322 The status before timer cancle is sec:0, usec:999417, isec:1, iusec:0 Bye. root@xxx:/system/bin #
在主進程因定時結束而退出以后仍能通過 ps 發現子進程卡在 while(1) 中退不出來,如下圖所示: