天天看點

【Linux】利用信号實作sleep函數mysleep1mysleep2

在另一篇文章Linux信号中,介紹了信号的産生與處理方式,以及一系列信号集函數的使用。

本文使用信号機制,模拟實作sleep函數并了解競态條件。

在此之前先介紹一波需要用到的函數。

sigaction函數

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
           

可以讀取和修改于指定信号相關聯的處理動作。

參數 signnum 為指定信号的編号。

若act指針非空,則根據 act 修改信号的處理動作,oldact可以為空,或者傳出原來的處理動作。act和oldact都指向下面的結構體:

struct sigaction
{
    void    (*sa_handler) (int) // 信号處理函數  SIG_IGN 表示忽略 SIG_DFL 表示預設動作
    sigset_t sa_mask;  // 額外要屏蔽信号集
    int     sa_flags;  // 一般為0
    void (*sa_sigaction)(int, siginfo_t *, void *); // 實時信号處理函數
}
           

當某個信号的處理函數被調用時,核心自動将目前信号加入程序的信号屏蔽字,當信号處理函數傳回時自動恢複原來的信号屏蔽字。保證在處理某個信号時,如果這種信号再次産生,那麼它會被阻塞到目前處理結束為止。如果在調動信号處理函數時,還需要屏蔽别的信号,則可以通過sa_mask 指定。

pause 函數

#include <unistd.h>
int pause(void);
           

挂起調用程序直到有信号遞達。 如果信号的處理動作是終止程序,則程序終止。如果信号的處理動作是忽略,則程序繼續處于挂起狀态。隻有信号的處理動作是捕捉pause函數才會傳回。

mysleep1

首先來實作 sleep 版本1:

1. 調用

sigaction()

捕捉信号SIGALRM

2. 調用

alarm()

設定鬧鐘

3. 調用

pause()

挂起等待

4. 取消鬧鐘

5. 恢複捕捉動作

#include <stdio.h>
#include <signal.h>

// 什麼事情也不做
void handler(int signum)
{}


unsigned  mysleep(unsigned seconds)
{
    // 1. 捕捉信号 SIGALRM
    struct sigaction act, oldact;
    act.sa_handler = handler;
    act.sa_flags = ;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, &oldact); // 設定act 并傳出原來的act

    // 2. 設定鬧鐘
    alarm(seconds);

    // 3. 挂起等待信号遞達 
    pause();

    // 4. 取消鬧鐘
    unsigned ret = alarm();

    // 恢複捕捉
    sigaction(SIGALRM, &oldact, NULL);

    return ret;
}

int main()
{
    while()
    {
        printf(" 我正在睡覺 zzz \n");
        mysleep();
    }
    return ;
}
           
【Linux】利用信号實作sleep函數mysleep1mysleep2

關于mysleep1有幾個問題:

Q1:信号處理函數handler函數什麼都不幹,為什麼還要注冊它作為SIGALRM的處理函數?不注冊信号處理函數可以嗎*?

答:不可以。因為pause() 函數使程序挂起等待,直到有信号遞達并且要執行自定義的信号處理函數才有機會傳回。

Q2:為什麼在mysleep函數傳回前要恢複SIGALRM信号原來的sigaction?

答:main函數作為調用者隻想睡一下覺,沒叫你在它睡覺的時候把它打的鼻青臉腫的,是以調用之前什麼樣就給恢複成什麼樣子。

Q3:mysleep函數的傳回值表示什麼含義? 什麼情況下傳回非0值?

答:mysleep 傳回值表示鬧鐘剩餘時間。 在取消鬧鐘時,上一個鬧鐘傳回為0時。

mysleep2

重新審視上面代碼,會發現有一個bug,假如在設定鬧鐘後,出現大量優先級較高的程序需要執行,測試該程序就會被切出去,即CPU資源被配置設定給了别的程序,如果時間很長的話,當該程序再次或者CPU資源的時候,鬧鐘時間已過,而pause永遠不會傳回。

出現這個問題的原因是系統執行代碼的時序是不确定的。如果在寫程式時考慮不周密,可能由于時序問題而導緻錯誤,這叫做競态條件(Race Condition)。

我們可以在設定鬧鐘之前,屏蔽信号 SIGALRM, 在讓程序挂起時,解除對該程序的屏蔽,然後在讓程序挂起等待信号遞達。可以利用 sigsuspend函數 幫我們圓夢。

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
           

和pause 函數一樣,該函數沒有成傳回值,隻有執行一個信号處理函數之後,才會傳回。

調用sigsuspend時,程序的信号屏蔽字由sigmask參數指定,可以通過指定sigmask來臨時解除對某個信号的屏蔽,然後挂起等待,傳回時,程序的信号屏蔽字恢複為原來的值。

下面的從新實作的mysleep2:

1. 捕捉SIGALRM信号

2. 屏蔽SIGALRM信号,讓該信号處于未決狀态,直接解除對該信号的屏蔽

3. 調用

alarm()

設定鬧鐘

4. 調用

sigsuspend( )

臨時解除型号,并挂起等待執行完信号處理函數傳回

5. 取消鬧鐘

6. 恢複信号捕捉

7. 恢複信号屏蔽

#include <stdio.h>
#include <signal.h>

// 什麼事情也不做
void handler(int signum)
{}

unsigned mysleep(unsigned seconds)
{
    struct sigaction act, oldact;
    sigset_t newmask, oldmask, suspmask;
    // 1. 信号捕捉
    act.sa_handler = handler;
    act.sa_flags = ;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, &oldact);

    // 2. 信号屏蔽
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    // 3. 設定鬧鐘
    alarm(seconds);

    // 4. 臨時解除屏蔽并挂起等待
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM); // 確定SIGALRM 不會被阻塞
    sigsuspend(&suspmask);
    // 5. 取消鬧鐘
    unsigned ret = alarm();

    // 6. 恢複捕捉動作
    sigaction(SIGALRM, &oldact, NULL);

    // 7. 恢複信号屏蔽
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return ret;
}

int main()
{
    while()
    {
        printf("我正在睡覺。。\n");
        mysleep();
    }
    return ;
}
           
【Linux】利用信号實作sleep函數mysleep1mysleep2

繼續閱讀