linux定时中断的三种实现

前言

本文主要讲述linux应用层三种定时中断实现的方法。我们可以利用定时中断在linux应用层实现一些对时间频率要求不是很高的驱动,虽然有些不规范,但是也是有其适用的场合的。因为应用层不涉及到硬件,不同平台可移植性更高。
本文涉及到的内容有:

  1. 多线程间信号的处理
  2. 三种定时中断的实现
  3. 测试结果与讨论

一、多线程间信号的处理

我们先下两个结论,并在后面的测试程序中给出证明。

  1. 结论一:默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的。
  2. 结论二:对信号的处理是进程中所有的线程共享的。

为什么要先讨论线程间的信号的处理呢?这和定时中断的实现有何关系?
放在前面讨论是因为之前踩了一个坑,先把坑分享出来,以免有人掉到坑里。
在项目开发中,对步进电机的控制,一开始我使用的是linux setitimer函数,目的是为了定时给出脉冲到步进电机,实现在应用层控制马达的转动。事实证明,是能够在应用层实现的。但是,后来的测试中,其他功能报了一个错误,查看log发现是sleep函数不生效。查看相关资料才发现,他们之前都使用了同种信号,导致了sleep函数接收了setitimer函数发出的信号。是有想过用select函数实现自己的sleep函数,这样可以解决问题,而且select函数比sleep函数更加准确。但是,项目搜索出来的sleep函数实在太多了,每个都修改实在烦。所以,只能另寻他路。果然,linux还能用POSIX中内置的定时器,使用函数timer_create 和函数timer_settime可以解决上面问题。

二、定时中断的三种实现

三种定时中断实现:

  1. setitimer 函数产生SIGALRM信号,同时使用signal函数注册
  2. 使用timer_create 和 timer_settime函数可以指定任意产生任意信号,同时使用signal函数注册
  3. 使用timer_create 和 timer_settime函数 通知线程

程序实现的代码如下:

//timer_test.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 


// return: 自CPU上电以来的时间,单位:毫秒
unsigned long long GetCpuRunTimeMs(void)
{
    unsigned long long  uptime = 0;
    struct timespec on;
    if(clock_gettime(CLOCK_MONOTONIC, &on) == 0)
    {
        uptime = on.tv_sec * 1000ul;
        uptime += on.tv_nsec / 1000000ul;
    }

    return uptime;
}
//信号处理函数
void signal_func(int signo)
{
    static unsigned long long start_time = 0;

    switch(signo)
    {
        case SIGALRM:
            printf("%s:1s-----> take %llu ms,signo=SIGALRM(%d)\n",__FUNCTION__,GetCpuRunTimeMs()-start_time,signo);
            break;
        case SIGUSR1:
            printf("%s:1s-----> take %llu ms,signo=SIGUSR1(%d)\n",__FUNCTION__,GetCpuRunTimeMs()-start_time,signo);
            break;
        default:
            break;
    }
    start_time = GetCpuRunTimeMs();
}
//setitimer函数相关初始化
void vTimerInit(void)
{
    struct itimerval value, old_value;

    signal(SIGALRM, signal_func);
    value.it_value.tv_sec = 1; //设置距离首次中断的时间
    value.it_value.tv_usec = 0;
    value.it_interval.tv_sec = 1; //时间间隔
    value.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &value, &old_value);    
}
//timer_create初始化相关,注册信号SIGUSR1
int vTimerCreateInit(void)
{
    struct sigevent evp;
    struct itimerspec ts;
    timer_t timer;
    int ret;
    memset(&evp, 0, sizeof(struct sigevent));   //清零初始化
    evp.sigev_value.sival_ptr = &timer;
    evp.sigev_notify = SIGEV_SIGNAL;
    evp.sigev_signo = SIGUSR1;
    signal(SIGUSR1, signal_func);

    ret = timer_create(CLOCK_REALTIME, &evp, &timer);
    if( ret )
        perror("timer_create");

    ts.it_interval.tv_sec = 1;
    ts.it_interval.tv_nsec = 0;
    ts.it_value.tv_sec = 1;
    ts.it_value.tv_nsec = 0;

    ret = timer_settime(timer, 0, &ts, NULL);
    if( ret )
        perror("timer_settime");

    return ret;
}

void timer_thread(union sigval v)
{
    static unsigned long long start_time = 0;

    printf("%s:1s-----> take %llu ms, v.sival_int = %d\n",__FUNCTION__,GetCpuRunTimeMs()-start_time,v.sival_int);
    start_time = GetCpuRunTimeMs();

}
//不采用信号通知的方式,直接通知线程
int vTimerPthreadInit(void)
{
    timer_t timerid;
    struct sigevent evp;
    int ret;
    memset(&evp, 0, sizeof(struct sigevent));   //清零初始化

    evp.sigev_value.sival_int = 1111;       
    evp.sigev_notify = SIGEV_THREAD;        //线程通知的方式,派驻新线程
    evp.sigev_notify_function = timer_thread;   //线程函数地址

    ret = timer_create(CLOCK_REALTIME, &evp, &timerid);
    if( ret )
        perror("timer_create");

    struct itimerspec it;
    it.it_interval.tv_sec = 1;  //间隔1s
    it.it_interval.tv_nsec = 0;
    it.it_value.tv_sec = 1;     
    it.it_value.tv_nsec = 0;

    ret = timer_settime(timerid, 0, &it, NULL);
    if( ret )
        perror("timer_settime");

    return ret;

}

//线程,用于测试信号接收
void *vTimerProc(void *arg)
{
    printf("pthread func:%s\n",__FUNCTION__);

    unsigned long long start_time = 0;
    while(1)
    {
        start_time = GetCpuRunTimeMs();
        sleep(5);
        printf("%s:sleep 5s take %llu ms\n",__FUNCTION__,GetCpuRunTimeMs()-start_time);
    }    
    return NULL;

}
//线程,用于测试信号接收
void *vSleepProc(void *arg)
{
    printf("pthread func:%s\n",__FUNCTION__);

    unsigned long long start_time = 0;
    while(1)
    {
        start_time = GetCpuRunTimeMs();
        sleep(5);
        printf("%s:sleep 5s take %llu ms\n",__FUNCTION__,GetCpuRunTimeMs()-start_time);
    }    
    return NULL;

}

//线程初始化函数
void  vPthreadInit(void)
{
    printf("func:%s\n",__FUNCTION__);

    pthread_t timer_thread_id = 0;
    pthread_create(&timer_thread_id, NULL, vTimerProc, NULL);

    pthread_t sleep_thread_id = 0;
    pthread_create(&sleep_thread_id, NULL, vSleepProc, NULL);

}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("please input eg: %s 1|2|3\n",argv[0]);
        printf("1:user SIGALRM\n");
        printf("2:user SIGUSR1\n");
        printf("3:user thread\n");
        return -1;
    }
    printf("func:%s ,process id is %d\n",__FUNCTION__,getpid());
    struct itimerval value, old_value;
    unsigned long long start_time = 0;
    unsigned long long end_time = 0;

    int ctrl = atoi(argv[1]);
    switch(ctrl)
    {
        case 1:
            vTimerInit();
            break;
        case 2:
            vTimerCreateInit();
            break;
        case 3:
            vTimerPthreadInit();
            break;
        default:
            return -1;
            break;
    }

    vPthreadInit();

    while(1)
    {
        start_time = GetCpuRunTimeMs();
        sleep(5);
        printf("%s:sleep 5s take %llu ms\n",__FUNCTION__,GetCpuRunTimeMs()-start_time);
    }
}

以上程序 ,并没有所有函数都有做判断,如果是写到项目中,是需要加多错误判断和处理的。
以下三个函数包含了定时中断三种方式。

int vTimerPthreadInit(void)//不采用信号通知的方式,直接通知线程
int vTimerCreateInit(void)//timer_create初始化相关,注册信号SIGUSR1
int vTimerPthreadInit(void)//不采用信号通知的方式,直接通知线程

在测试程序中使用参数1,2,3区分调用。在main函数中处理如下:

int ctrl = atoi(argv[1]);
switch(ctrl)
{
    case 1:
        vTimerInit();
        break;
    case 2:
        vTimerCreateInit();
        break;
    case 3:
        vTimerPthreadInit();
        break;
    default:
        return -1;
        break;
}

三、测试结果与讨论

接下来对程序进程测试。
编译,需要加多两个库:-lpthread -lrt

ubuntu@ubuntu:~/test/timer_create$ gcc timer_test.c -o timer_test -lpthread -lrt

先执行测试一下,发现测试程序已经有提示了,按照提示来测试。

ubuntu@ubuntu:~/test/timer_create$ ./timer_test
please input eg: ./timer_test 1|2|3
1:user SIGALRM
2:user SIGUSR1
3:user thread

首先,测试setitimer函数,它在程序中会产生SIGALRM信号

ubuntu@ubuntu:~/test/timer_create$ ./timer_test 1
func:main ,process id is 7186
func:vPthreadInit
pthread func:vSleepProc
pthread func:vTimerProc
signal_func:1s-----> take 535951046 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
vSleepProc:sleep 5s take 5001 ms
vTimerProc:sleep 5s take 5000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGALRM(14)
main:sleep 5s take 1000 ms
^C

从以上的测试结果看出,在main函数使用sleep 5秒,但是打印main:sleep 5s take 1000 ms,也就是睡眠5秒,结果1秒就被打断。这是因为线程发出的SIGALRM信号被sleep函数接收了。如果想知道具体是什么终止了sleep函数,可以去看看它的实现。在测试程序中,我是把注册信号的函数放在了主线程。其实在测试的时候,我有把初始化信号函数vTimerInit放在线程vTimerProc中测试,结果也是一样的。这就可以证明结论二。而子线程vSleepProc和子线程vTimerProc中调用sleep函数并没有被终止就可以证明结论一。

  1. 结论一:默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的。
  2. 结论二:对信号的处理是进程中所有的线程共享的。

接着再测试timer_create函数,注册信号是SIGUSR1。

ubuntu@ubuntu:~/test/timer_create$ ./timer_test 2
func:main ,process id is 7189
func:vPthreadInit
pthread func:vSleepProc
pthread func:vTimerProc
signal_func:1s-----> take 535963773 ms,signo=SIGUSR1(10)
main:sleep 5s take 1001 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 999 ms,signo=SIGUSR1(10)
main:sleep 5s take 999 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
vSleepProc:sleep 5s take 5000 ms
vTimerProc:sleep 5s take 5000 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
signal_func:1s-----> take 1000 ms,signo=SIGUSR1(10)
main:sleep 5s take 1000 ms
^C

结果竟然和上一个结果是一样的。说明使用信号SIGUSR1还是会中断main函数的sleep函数。目前来说,我并不清楚为什么会影响到。
最后,测试不采用信号通知的方式,直接通知线程的方式:

ubuntu@ubuntu:~/test/timer_create$ ./timer_test 3
func:main ,process id is 7192
func:vPthreadInit
pthread func:vSleepProc
pthread func:vTimerProc
timer_thread:1s-----> take 535974740 ms, v.sival_int = 1111
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
main:sleep 5s take 5001 ms
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
vSleepProc:sleep 5s take 5001 ms
vTimerProc:sleep 5s take 5001 ms
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
timer_thread:1s-----> take 1000 ms, v.sival_int = 1111
timer_thread:1s-----> take 999 ms, v.sival_int = 1111
main:sleep 5s take 5000 ms
timer_thread:1s-----> take 1001 ms, v.sival_int = 1111
vSleepProc:sleep 5s take 5000 ms
vTimerProc:sleep 5s take 5000 ms
timer_thread:1s-----> take 999 ms, v.sival_int = 1111
timer_thread:1s-----> take 1001 ms, v.sival_int = 1111
^C

从以上结果看,使用这种方式,并没有使sleep函数终止,无论是主线程还是两个子线程,说明在应用层使用这种方式是可行的。

最后的最后,由于项目中的嵌入式linux系统中的时钟被设置为10ms,所以在应用层只能产生最高100hz的脉冲,这对步进电机来说还是有点慢,所以,没办法还是得用硬件定时中断,实现linux设备驱动。不过,对于I2C这种时钟要求不高的情况,或许可以一试。还有,提醒一点:usleep函数在应用层中是相当不精准的,select函数实现延时效果较好,但还是误差挺大的,所以才需要软件定时器。

后续

linux一些系统调用是跟信号紧密相关的,如system函数是需要捕获SIGCHLD 信号的,如果程序为了避免产生僵尸进程,而把SIGCHLD 信号设置为忽略,那system调用就会受到影响。下次再讨论system相关的技术。

你可能感兴趣的:(linux系统编程)