文档中心

TD开发平台_基础

03.任务运行系统

任务运行系统以任务为单位进行调度和运行,开发人员创建各种任务,当任务被触发需要运行时,就加入到一个就绪队列上,然后由若干个线程摘取就绪队列上的任务,并执行该任务。这些执行线程的具体数量可以根据硬件核心的多少来确定,甚至可以在运行时动态添加和减少。
首先要解决的问题是时间的定义:

typedef Tint TTimeout;
#define TTIMEOUT_DIFF(a, b) ((Tint)((Tint)(a) - (Tint)(b)))

TTimeout定义为有符号整数,表示逝去的时间(毫秒为单位),可以理解为一个时间点,也可以理解为离0时刻的时间段。单个 TTimeout值因为回绕,无较大实际意义,但对任意两个 TTimeout值,只要这两个时间点在一轮计数的一半时间里 (231ms),那么这两个值的差值可以正确表示它们之间的时间段。例如判定时刻 a是否在 b的前面,就是 TTIMEOUT_DIFF(a, b) <= 0,而不能直接用 (a < b)。
用下面的函数得到系统当前运行的时间(毫秒数),注意该函数永远不会返回 0,时间回绕到 0时会返回 1。

TTimeout TTaskGetTimeout(void);

用户可以创建三类任务:定时器任务,文件描述符任务和简单任务。对于每一个任务都有如下几种状态变换:

所有任务在创建后都处于等待状态,只有被触发才进入就绪状态等待运行。对于不同种类的任务,它们的触发方式不同:定时器任务是每隔固定时间就自动触发一次;文件描述符任务就是当某个文件句柄上有数据可读或异常时自动触发;简单任务就是需要开发人员去主动触发调用。若干个执行线程在空闲时会执行这些处于就绪状态的任务(执行任务就是运行创建这些任务时设置的一个回调函数),任务执行完毕(即回调函数返回)后回到等待状态。任务在任意状态中都可以被销毁,甚至在它的回调函数中都可以销毁自己。
在整个任务系统的运转中,如果开发人员安排有多个执行线程,那么怎么分配任务具体在哪个线程上运行是不可控的,即目前没有任务和线程的绑定,有可能同一个任务上一次触发和下一次触发运行在两个不同的线程上。任务是粗粒度的并发单位。但是任务系统有一个基本的保证:在一个任务被触发直到运行完毕,这个任务是不能被再次触发的,即任务的回调函数不会重入。
具体的任务类型:

1)定时器任务
typedef void (*TTaskTimer_Func) (TTaskTimer *ptimer, TTimeout timeout,void *arg);
TTaskTimer * TTaskAddTimer(TTimeout timeout, TTaskTimer_Func callback, void *arg);
void TTaskEnableTimer(TTaskTimer *timer, Tbool if_enable);
void TTaskResetTimer(TTaskTimer *timer);
void TTaskChangeTimeout(TTaskTimer *timer, TTimeout timeout);
TTaskTimer_Func是定时器任务的回调函数原型,TTaskAddTimer函数创建一个定时器任务,参数 timeout是自动触发的时间间隔(毫秒为单位);参数 callback就是定时器任务的回调函数;参数 arg是开发人员设置的一个任意值,该值会在调用回调函数时,传给回调函数的 arg参数。
注意一个定时器任务被创建后是永远不会被触发的,这时它的状态是无效状态(无效状态是定时器任务特有的)。必须通过 TTaskEnableTimer函数使之有效后,才开始计时;同理以后也可以再使之进入无效状态。当一个定时器任务被无效时,即使它已经处于就绪状态,也将返回等待状态,不会被运行;如果它已经在运行状态,那么它的运行不受影响,回调函数仍然会执行完。
TTaskResetTimer函数用于重置定时器任务的计时,如一个定时器的间隔时间为 5秒,这时已经过去了 3秒,即 2秒之后该定时器会被触发运行,如果这时调用TTaskResetTimer函数,那么将重新计时 5秒才会被触发运行。对于已经在就绪或运行状态的定时器,该函数不会取消,只是影响下一次的触发时间。
TTaskChangeTimeout函数改变定时器的间隔时间,但是注意这个改变并不影响即将到来的触发时间,只是影响下下次的触发时间,如果要立即影响即将到来的触发时间,只需要再调用 TTaskResetTimer即可。
下面是使用定时器的例子:
#include <TCore/TCore.h>
void timer1_cb(TTimer *ptimer, TTimeout timeout, void *arg)
{
    static int num=0;
    printf("timer1 : %d\n", ++num);
    return;
}
void timer2_cb(TTimer *ptimer, TTimeout timeout, void *arg)
{
    printf("timer2 come, exit\n");
    exit(0);
}
int main(int argc, char **argv)
{
    TTaskTimer *ptimer1, *ptimr2;
    ptimer1 = TTaskAddTimer(1000, timer1_cb, NULL);
    TTaskEnableTimer(ptimer1, TRUE);
    ptimer2 = TTaskAddTimer(5000, timer2_cb, NULL);
    TTaskEnableTimer(ptimer2, TRUE);
    while(1) TTaskLoopOnce(0);
    return 0;
}
例子中的 TTaskLoopOnce函数会等待就绪任务的产生,并执行任务。简单的循环调
用该函数就是任务运行系统的事件循环。 TTaskLoopOnce函数在后面会进一步介绍。
2)文件描述符任务
当打开一个文件(普通文件或设备文件)、或创建一个管道或 socket,都会得到一个文件描述符,即文件句柄。文件描述符任务就是当这个文件上有指定事件发生时,自动触发调用的一种任务。相关操作函数如下:
#define T_IO_READ 0x01
#define T_IO_PRI 0x02
#define T_IO_WRITE 0x04
#define T_IO_ERR 0x08
#define T_IO_HUP 0x10
typedef void (*TTaskFile_Func)(TTaskFile *pfd, Tint fd, Tint type, void*arg);
TTaskFile * TTaskAddFile(Tint fd, TTaskFile_Func callback, Tint type, void*arg);

首先定义文件描述符上发生的事件类型:
● T_IO_READ:该文件上有数据可读;
● T_IO_PRI:该文件上有紧急的数据可读(只用于 socket文件句柄);
● T_IO_WRITE:该文件可以写入数据(不常用);
● T_IO_ERR:该文件异常,出错;
● T_IO_HUP:该文件被断开(常用于管道、socket和设备文件)

TTaskAddFile函数创建一个文件描述符任务,参数 fd是要监视的文件句柄,参数 type指明要监视的事件类型,可以同时监视多个事件类型( T_IO_READ|T_IO_PRI),参数 callback是任务的回调函数,参数 arg在执行任务时传给回调函数的参数 arg。回调函数原型中的参数 type表示实际发生的事件类型,注意:即使开发人员没有要求监视 T_IO_ERR和 T_IO_HUP,它们也可能会发生,即 type可能等于 T_IO_ERR或 T_IO_HUP。
事件是电平触发的,例如开发人员监视 T_IO_READ事件,假如开发人员在执行函数中没有把文件中的数据读完,那么在该任务执行完后,又会被立即触发,直到数据被读完。所以从性能上考虑,每次执行都应该把数据读完。
下面是一个测试系统最大吞吐率的例子,通过 pipe系统调用创建两个管道,然后创建两个文件描述符任务来监视每个管道是否有数据可读,在每个任务的执行函数中都把读到的数据再发给另一个管道,再创建一个 1秒的定时器打印数据吞吐率。

#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <TCore/TCore.h>
#define TESTN (16*1024)
static int fd1[2], fd2[2];
static char fd1_buf[TESTN], fd2_buf[TESTN];
static int fd1_n, fd2_n;
void fd_cb(TTaskFile *pfd, Tint fd, Tint type, void *arg)
{
    int n;
    if(fd == fd1[0]) {
        n = read(fd, fd1_buf, TESTN);
        write(fd2[1], fd1_buf, n);
        fd1_n += n;
    } else {
        n = read(fd, fd2_buf, TESTN);
        write(fd1[1], fd2_buf, n);
        fd2_n += n;
    }
    return;
}
void timer_cb(TTaskTimer *ptimer, TTimeout timeout, void *arg)
{
    printf("fd1 read %d, fd2 read %d\n" fd1_n, fd2_n);
    fd1_n = 0;
    fd2_n = 0;
    return;
}
int main(int argc, char **argv)
{
    pipe(fd1);
    pipe(fd2);
    TTaskRegisterFd(fd1[0], fd_cb, T_IO_READ, 0);
    TTaskRegisterFd(fd2[0], fd_cb, T_IO_READ, 0);
    TTaskEnableTimer(TTaskAddTimer(1000, timer_cb, (void *)0),TRUE);
    write(fd1[1], fd1_buf, TESTN); /*启动传输*/
    while(1) TTaskLoopOnce(0); /*执行事件循环*/
    return 0;
}
在CPU 为P4(1.7G),内存为DDR2(1G),系统为ubuntu7.04 的环境下运行,打印的 fd1_n和 fd2_n每次都在 320M左右。
3)简单任务
简单任务就是由开发人员主动触发执行的任务,相关函数如下:
typedef void (*TTaskOnce_Func)(TTaskOnce *ponce, Tint n, void *arg);
TTaskOnce * TTaskAddOnce(TTaskOnce_Func callback, void *arg);
void TTaskTriggerOnce(TTaskOnce *once, Tint n);
void TTaskDestroyOnce(TTaskOnce *once);
TaskAddOnce创建一个简单任务,参数 callback是任务的执行函数,参数 arg在执行任务时传给执行函数的参数 arg。简单任务创建后永远不会自动触发。
TTaskTriggerOnce用于主动触发简单任务,该函数有一个参数 n,这个 n会传给执行函数的参数 n。当连续调用两次 TTaskTriggerOnce触发同一个简单任务时(触发函数的参数 n分别为 n1和 n2),那么该简单任务可能被执行 1次,也可能被执行 2次。如果是执行两次,那么执行函数的参数 n分别为 n1和 n2;如果是执行一次,那么执行函数的参数 n为(n1+n2)。也就是触发函数总是在累计 n,执行函数总是消耗完当前的 n,只要 n大于 0就会触发任务执行。注意在 TTaskTriggerOnce中并不会实际执行任务,仅仅只是触发。
在其他基于事件响应的运行系统中,如 libevent,glib等,都提供了定时器任务和文件描述符任务,并没有提供简单任务,但是这种由开发人员触发的任务和其他任务配合起来使用更加灵活,也可以用在多任务的同步协作上。定时器任务和文件描述符任务就像操作系统里的中断处理函数,它们被自动触发调用,但是为了快速响应中断,它们读取到外设的数据后并不立即处理,而是触发另一个核心任务处理。
4)事件循环
在应用程序创建任务和做好其他初始化工作之后,必须进入一个循环处理过程。在这个过程中,应用程序等待各种事件来触发预先设定好的任务运行,在没有事件发生时,开发人员进程是休眠的,不占用 CPU。任务被触发后,也是在事件循环过程中执行的。
void TTaskLoopOnce(TTimeout check_awake_timepoint);
例如:
while(1) TTaskLoopOnce(0);
TTaskLoopOnce函数是本系统的核心函数,如果已经有任务被触发,该函数会执行完所有的任务,然后就返回;如果目前没有任务被触发,该函数会等待直到有一个任务被触发,然后就返回。所以循环调用该函数就是本系统的事件循环。
本系统支持多个线程来并发执行任务,具体实现就是创建多个线程同时循环调用 TTaskLoopOnce函数,该函数内部会自动分配就绪任务到多个线程上执行,并且协调多个线程的同步和等待。所以 TTaskLoopOnce是支持重入的。
TTaskLoopOnce的参数 check_awake_timepoint一般为 0,具体用法和多任务的同步和互斥实现有关。