天天看点

【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

继续阅读