演示如何建立 POSIX 相容的間隔定時器

大雄45發表於2023-02-13
導讀 這是一個演示如何建立 POSIX 相容的間隔定時器的教程。

對開發人員來說,定時某些事件是一項常見任務。定時器的常見場景是看門狗、任務的迴圈執行,或在特定時間安排事件。在這篇文章中,我將演示如何使用 timer_create(…) 建立一個 POSIX 相容的間隔定時器。

準備 Qt Creator

我使用 Qt Creator 作為該樣例的 IDE。為了在 Qt Creator 執行和除錯樣例程式碼,請克隆 GitHub 上的倉庫,開啟 Qt Creator,在 “檔案File -> 開啟檔案或專案……Open File or Project…” 並選擇 “CMakeLists.txt”:

演示如何建立 POSIX 相容的間隔定時器演示如何建立 POSIX 相容的間隔定時器
在 Qt Creator 中開啟專案

選擇工具鏈之後,點選 “配置專案Configure Project”。這個專案包括三個獨立的樣例(我們在這篇文章中將只會用到其中的兩個)。使用綠色標記出來的選單,可以在每個樣例的配置之間切換,併為每個樣例啟用在終端執行 “在終端中執行Run in terminal”(用黃色標記)。當前用於構建和除錯的活動示例可以透過左下角的“除錯Debug” 按鈕進行選擇(參見下面的橙色標記)。

演示如何建立 POSIX 相容的間隔定時器演示如何建立 POSIX 相容的間隔定時器專案配置

執行緒定時器

讓我們看看 simple_threading_timer.c 樣例。這是最簡單的一個。它展示了一個呼叫了超時函式 expired 的間隔定時器是如何被建立的。在每次過期時,都會建立一個新的執行緒,在其中呼叫函式 expired:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
void expired(union sigval timer_data);
pid_t gettid(void);
struct t_eventData{
    int myData;
};
int main()
{
    int res = 0;
    timer_t timerId = 0;
    struct t_eventData eventData = { .myData = 0 };
/* sigevent 指定了過期時要執行的操作 */
    struct sigevent sev = { 0 };
/* 指定啟動延時時間和間隔時間
* it_value和it_interval 不能為零 */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };
    printf("Simple Threading Timer - thread-id: %d\n", gettid());
    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = &expired;
    sev.sigev_value.sival_ptr = &eventData;
/* 建立定時器 */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);
    if (res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }
/* 啟動定時器 */
    res = timer_settime(timerId, 0, &its, NULL);
    if (res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }
    printf("Press ETNER Key to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}
void expired(union sigval timer_data){
    struct t_eventData *data = timer_data.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

這種方法的優點是在程式碼和簡單除錯方面用量小。缺點是由於到期時建立新執行緒而增加額外的開銷,因此行為不太確定。

中斷訊號定時器

超時定時器通知的另一種可能性是基於 核心訊號。核心不是在每次定時器過期時建立一個新執行緒,而是向程式傳送一個訊號,程式被中斷,並呼叫相應的訊號處理程式。

由於接收訊號時的預設操作是終止程式(參考 signal 手冊頁),我們必須要提前設定好 Qt Creator,以便進行正確的除錯。

當被除錯物件接收到一個訊號時,Qt Creator 的預設行為是:

  1. 中斷執行並切換到偵錯程式上下文。
  2. 顯示一個彈出視窗,通知使用者接收到訊號。

這兩種操作都不需要,因為訊號的接收是我們應用程式的一部分。

Qt Creator 在後臺使用 GDB。為了防止 GDB 在程式接收到訊號時停止執行,進入 “工具Tools -> 選項Options” 選單,選擇 “偵錯程式Debugger”,並導航到 “本地變數和表示式Locals & Expressions”。新增下面的表示式到 “定製除錯助手Debugging Helper Customization”:

handle SIG34 nostop pass

演示如何建立 POSIX 相容的間隔定時器演示如何建立 POSIX 相容的間隔定時器
Sig 34 時不停止

你可以在 GDB 文件 中找到更多關於 GDB 訊號處理的資訊。

接下來,當我們在訊號處理程式中停止時,我們要抑制每次接收到訊號時通知我們的彈出視窗:

演示如何建立 POSIX 相容的間隔定時器演示如何建立 POSIX 相容的間隔定時器
Signal 34 彈出視窗

為此,導航到 “GDB” 標籤並取消勾選標記的核取方塊:

演示如何建立 POSIX 相容的間隔定時器演示如何建立 POSIX 相容的間隔定時器
定時器訊號視窗

現在你可以正確的除錯 signal_interrupt_timer。真正的訊號定時器的實施會更復雜一些:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define UNUSED(x) (void)(x)
static void handler(int sig, siginfo_t *si, void *uc);
pid_t gettid(void);
struct t_eventData{
    int myData;
};
int main()
{
    int res = 0;
    timer_t timerId = 0;
    struct sigevent sev = { 0 };
    struct t_eventData eventData = { .myData = 0 };
/* 指定收到訊號時的操作 */
    struct sigaction sa = { 0 };
/* 指定啟動延時的時間和間隔時間 */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };
    printf("Signal Interrupt Timer - thread-id: %d\n", gettid());
    sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &eventData;
/* 建立定時器 */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);
    if ( res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }
/* 指定訊號和處理程式 */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;
/* 初始化訊號 */
    sigemptyset(&sa.sa_mask);
    printf("Establishing handler for signal %d\n", SIGRTMIN);
/* 註冊訊號處理程式 */
    if (sigaction(SIGRTMIN, &sa, NULL) == -1){
        fprintf(stderr, "Error sigaction: %s\n", strerror(errno));
        exit(-1);
    }
/* 啟動定時器 */
    res = timer_settime(timerId, 0, &its, NULL);
    if ( res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }
    printf("Press ENTER to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}
static void
handler(int sig, siginfo_t *si, void *uc)
{
    UNUSED(sig);
    UNUSED(uc);
    struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

與執行緒定時器相比,我們必須初始化訊號並註冊一個訊號處理程式。這種方法效能更好,因為它不會導致建立額外的執行緒。因此,訊號處理程式的執行也更加確定。缺點顯然是正確除錯需要額外的配置工作。

總結

本文中描述的兩種方法都是接近核心的定時器的實現。不過,即使 timer_create(…) 函式是 POSIX 規範的一部分,由於資料結構的細微差別,也不可能在 FreeBSD 系統上編譯樣例程式碼。除了這個缺點之外,這種實現還為通用計時應用程式提供了細粒度控制。

原文來自:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2934508/,如需轉載,請註明出處,否則將追究法律責任。

相關文章