2021SC@SDUSC TencentOS Tiny源码分析(十一) 邮箱队列模块二

2021SC@SDUSC


上周我们分析了邮箱队列的相关实现,了解了如何静态或动态的创建和销毁邮箱队列,最后介绍了邮箱队列的冲洗相关内容,这周我们首先接着上周的内容,从消息在邮箱队列中的存取开始继续分析邮箱队列的内容:

一. 从邮箱队列中获取邮件

(一)具体实现分析

话不多说,我们首先来看一下源码:

和其他邮箱队列实现函数一样,从邮箱队列中获取邮件和往邮箱队列中存入邮件的函数都在tos_mail_queue.c文件中,下面就不再重复说了

__API__ k_err_t tos_mail_q_pend(k_mail_q_t *mail_q, void *mail_buf, size_t *mail_size, k_tick_t timeout)
{
    TOS_CPU_CPSR_ALLOC();
    k_err_t err;

    TOS_IN_IRQ_CHECK();
    TOS_PTR_SANITY_CHECK(mail_q);
    TOS_PTR_SANITY_CHECK(mail_buf);
    TOS_OBJ_VERIFY(mail_q, KNL_OBJ_TYPE_MAIL_QUEUE);

    TOS_CPU_INT_DISABLE();

    if (tos_ring_q_dequeue(&mail_q->ring_q, mail_buf, mail_size) == K_ERR_NONE) {
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }

    if (timeout == TOS_TIME_NOWAIT) {
        *mail_size = 0;
        TOS_CPU_INT_ENABLE();
        return K_ERR_PEND_NOWAIT;
    }

    if (knl_is_sched_locked()) {
        TOS_CPU_INT_ENABLE();
        return K_ERR_PEND_SCHED_LOCKED;
    }

    k_curr_task->mail = mail_buf;
    pend_task_block(k_curr_task, &mail_q->pend_obj, timeout);

    TOS_CPU_INT_ENABLE();
    knl_sched();

    err = pend_state2errno(k_curr_task->pend_state);
    if (err == K_ERR_NONE) {
        *mail_size              = k_curr_task->mail_size;
        k_curr_task->mail       = K_NULL;
        k_curr_task->mail_size  = 0;
    }

    return err;
}

我们首先来看这些参数,mail_q是邮箱队列句柄,mail_buf是用来承载邮件内容的buffer地址,mail_size是获取到的邮件长度,timeout是等待超时参数,当返回K_ERR_NONE时表示从邮箱队列获取邮件成功,mail_buf中被写入邮件内容,msg_size获取到的邮件长度;当返回K_ERR_PEND_NOWAIT时表示未能从邮箱队列中获取到邮件,并且timeout参数为TOS_TIME_NOWAIT(表示获取不到邮件时立即返回);当返回K_ERR_PEND_SCHED_LOCKED时表示未能从邮箱队列中获取到邮件,并且系统调度处于锁定状态。

我们来具体看这个函数的实现过程,首先这个函数调用TOS_CPU_CPSR_ALLOC(),这个函数调用的实现如下:

/* Allocates CPU status register word. */
#define TOS_CPU_CPSR_ALLOC()    cpu_cpsr_t cpu_cpsr = (cpu_cpsr_t)0u

我们发现,这里是采用宏定义的方式实现一个函数,这是令我很感到意外的一个地方,我预估这是一个在实际生产中很实用的一个技巧,于是我上网搜索了一下这种技巧,并总结在接下来的C语言中利用宏定义实现简单函数的编程技巧总结版块,稍后我们会细说。然后我们再回到这个函数,这个函数的具体含义就是分配了一个CPU的状态寄存器字。之后我们通过调用三个check性质的函数,分别是TOS_IN_IRQ_CHECK();TOS_PTR_SANITY_CHECK(mail_q);TOS_PTR_SANITY_CHECK(mail_buf);来做参数的检查,然后调用TOS_CPU_INT_DISABLE()函数,这个函数的实现如下:

/* Save CPU status word & disable interrupts.*/
#define TOS_CPU_INT_DISABLE() \
    do { \
        cpu_cpsr = tos_cpu_cpsr_save(); \
    } while (0)

同样也是采用了宏定义来实现这个函数的定义,并且这里用到了do while(0)在宏定义中使用的技巧,这个技巧我们也会在下面介绍。返回到这个函数的本意,是保存CPU的当前状态字,并且设置不可被中断,类似于关中断的作用(下面我们还会看到开中断的)

接下来就是调用队列的出队API–tos_ring_q_dequeue()来取出邮件,我们再一次发现这个邮箱队列的底层还是基于环形队列的一些API(包括创建、销毁、出入队)来实现的,当完成后就会调用TOS_CPU_INT_ENABLE()开中断,并且返回之前所提到的成功标志K_ERR_NONE,然后如果并没有出队成功,就会进行接下来检查,是否队列中没有消息并且time标志是没有消息则立即返回(如果是则会立即返回),是否内核调度被加锁了(处于锁定状态),最后就是把检查结果返回,开中断,并且进行一次内核调度。

(二)C语言中利用宏定义实现简单函数的编程技巧总结

在介绍这种技巧之前,首先,我们来看一个简单的例子:

#define MAX(a, b) ((a) > (b) ? (a):(b))

int max(int a, int b)
{
  return (a > b ? a : b);
}

上面的宏定义等同于下面的具体函数实现,也就是说,比较两个数的大小一共有这两种实现方式,那么我们采用哪一种更合适呢?显然,我们会选择第一种,原因有两个:

  • 函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。使用宏可以避免这样额外的开销
    • 这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹
  • 函数的参数必须被声明为一种特定的类型,所以函数调用只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的

下面我们来看宏定义的一段经典应用:

//宏定义
#define MALLOC(n, type) \
((type *) malloc((n)* sizeof(type))
//使用宏定义
int *ptr;
ptr = MALLOC( 5, int );
//宏定义展开
ptr = (int *) malloc ( (5) * sizeof(int) );

首先我们要知道,参数类型(比如int double)没有办法作为参数传递给函数,所以上面这段代码中是无法用等效的函数调用来取代的,只有通过这种宏定义的方式才能实现将参数类型作为参数传递给函数

那么什么情况下不适合用宏定义来实现函数呢?

  • 原函数比较长或者比较复杂时不适合,否则使用宏会大幅度增加程序的长度。
  • 如果宏定义所定义的代码需要在程序中的多个地方中出现,那么更好的办法是将其实现为一个函数,这也是发明函数调用最初的初心

(三)C语言中do{…}while(0)编程技巧的使用

这个编程技巧在之前上戴鸿君老师的计算机系统原理课程时,戴老师在将do{…}while循环时讲到过这一点,并且给我们推荐了两篇博客,那么我就基于戴老师的支持,对这个编程技巧再一次结合这里的应用做总结。

我们在刚才介绍从邮箱队列中获取邮件时,遇到一个函数的宏定义实现如下:

/* Save CPU status word & disable interrupts.*/
#define TOS_CPU_INT_DISABLE() \
    do { \
        cpu_cpsr = tos_cpu_cpsr_save(); \
    } while (0)

由此引出C语言中do{…}while(0)编程技巧的第一个用法:帮助定义复杂的宏以避免错误,比如说:

你想要定义一个这样的宏:

#define DOSOMETHING() foo1(); foo2();

那么当你使用的使用就有可能会出现这样的错误:

if(a>0)
    DOSOMETHING();
//宏定义展开
if(a>0)
    foo1();
    foo2();

导致后面的foo2()函数会被放到if条件分支外,起到我们不想要的效果

如果我们使用do{…}while(0)来进行如下的宏定义:

#define DOSOMETHING() \
        do{ \
          foo1();\
          foo2();\
        }while(0)\

do能确保大括号里的逻辑能被执行,而while(0)能确保该逻辑只被执行一次,就像没有循环语句一样,这样就可以帮助我们保证宏定义的安全性。

实际上在Linux和其它代码库里的,很多宏实现都使用do/while(0)来包裹他们的逻辑,这样不管在调用代码中怎么使用分号和大括号,而该宏总能确保其行为是一致的。

经过这次源码学习,也让我真正感受到了课上讲的一些技巧竟然真的会在实际的应用(TencentOS Tiny开发)中被用到,再次给我提了醒,让我意识到课堂上的知识是很重要的

二. 往邮箱队列中存入邮件

往队列中放邮件有两种方式,一个是放入邮件,并唤醒等待队列上的一个任务,一个是放入邮件,并唤醒等待队列上的所有任务,我们分别来看各自的实现和区别,下面是这两者的源码:

__API__ k_err_t tos_mail_q_post(k_mail_q_t *mail_q, void *mail_buf, size_t mail_size)
{
    return mail_q_do_post(mail_q, mail_buf, mail_size, OPT_POST_ONE);
}

__API__ k_err_t tos_mail_q_post_all(k_mail_q_t *mail_q, void *mail_buf, size_t mail_size)
{
    return mail_q_do_post(mail_q, mail_buf, mail_size, OPT_POST_ALL);
}

我们发现这两个函数的函数体都是只有一个函数调用,调用了mail_q_do_post函数,只是最后一个参数不同,前者是OPT_POST_ONE,后者是OPT_POST_ALL,这也就是我们刚才说的唤醒等待队列上的任务数目不同

我们具体来看一下mail_q_do_post这个函数的实现:

__STATIC__ k_err_t mail_q_do_post(k_mail_q_t *mail_q, void *mail_buf, size_t mail_size, opt_post_t opt)
{
    TOS_CPU_CPSR_ALLOC();
    k_err_t err;
    k_task_t *task, *tmp;

    TOS_PTR_SANITY_CHECK(mail_q);
    TOS_PTR_SANITY_CHECK(mail_buf);
    TOS_OBJ_VERIFY(mail_q, KNL_OBJ_TYPE_MAIL_QUEUE);

    TOS_CPU_INT_DISABLE();

    if (pend_is_nopending(&mail_q->pend_obj)) {
        err = tos_ring_q_enqueue(&mail_q->ring_q, mail_buf, mail_size);
        if (err != K_ERR_NONE) {
            TOS_CPU_INT_ENABLE();
            return err;
        }
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }

    if (opt == OPT_POST_ONE) {
        mail_task_recv(TOS_LIST_FIRST_ENTRY(&mail_q->pend_obj.list, k_task_t, pend_list),
                            mail_buf, mail_size);
    } else { // OPT_POST_ALL
        TOS_LIST_FOR_EACH_ENTRY_SAFE(task, tmp, k_task_t, pend_list, &mail_q->pend_obj.list) {
            mail_task_recv(task, mail_buf, mail_size);
        }
    }

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return K_ERR_NONE;
}

我们会发现在23行之前的内容和从邮箱队列中获取邮件的函数大致相同,主要不同在后半部分,因此前半部分就不过多赘述了。在关中断后,调用pend_is_nopending()函数判断一下是否有任务在等待信号量,如果有则调用环形队列的入队API–tos_ring_q_enqueue:如果入队过程中报错信息显示有错误则立即返回错误结束函数执行(如果返回错误信息是K_ERR_RING_Q_FULL则表示队列已满),如果无错则开中断,返回正确执行入队的信息;如果没有任务在等待信号量则唤醒最后一个参数设置的数量对应数量的任务

本周的内容到此就结束了,我认为比起理解TencentOS Tiny里具体某个部分的实现是怎样,更重要的是学习他们的编程技巧,在这一过程的这一周学习中,我很幸运发现了和戴鸿君老师上课讲的内容有相同的部分,再一次贯通了知识的脉络,让我意识到这些技巧在实际生产中也是用处广泛。邮箱队列到此就结束了,下一周我们将学习TencentOS Tiny的其他内容

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值