第18回 函数tslp_tsk的式样
tslp_tsk的式样
函数tslp_tsk和slp_tsk稍微有些不一样, 函数tslp_tsk是带有参数的
ER ercd = tslp_tsk(TMO tmout);
接下来关于函数tslp_tsk的参数tmout进行解说. tmout可以指定正值,TMO_POL(=0)和TMO_FEVR(=-1).
正值:表示超时. 在指定的时间以后将被唤醒.
0(TMO_POL):表示轮循,等待的时候如果出现错误则立刻返回.
-1(TMO_FEVR):永远等待下去.
事实上,如果tslp_tsk的参数指定为(TMO_FEVR)的时候,就和函数slp_tsk的机能一模一样了.因此我认为既然有了tslp_tsk函数的话那么slp_tsk函数不设计也是可以的. 但是如果没有函数tslp_tsk而只有使用slp_tsk函数的情况下,就很难说这些机能(时间事件管理机能,时间事件所对应的内存区域等等)是多余的了. 因此在ASP内核中,把函数tslp_tsk和slp_tsk分开来进行了设计.
那么我们接下来就来看看tslp_tsk里面的代码把.
/kernel/task_sync.c
92 ER
93 tslp_tsk(TMO tmout)
94 {
95 WINFO winfo;
96 TMEVTB tmevtb;
97 ER ercd;
98
99 LOG_TSLP_TSK_ENTER(tmout);
100 CHECK_DISPATCH();
101 CHECK_TMOUT(tmout);
102
103 t_lock_cpu();
104 if (p_runtsk->wupque) {
105 p_runtsk->wupque = FALSE;
106 ercd = E_OK;
107 }
108 else if (tmout == TMO_POL) {
109 ercd = E_TMOUT;
110 }
111 else {
112 p_runtsk->tstat = (TS_WAITING | TS_WAIT_SLP);
113 make_wait_tmout(&winfo, &tmevtb, tmout);
114 LOG_TSKSTAT(p_runtsk);
115 dispatch();
116 ercd = winfo.wercd;
117 }
118 t_unlock_cpu();
119
120 error_exit:
121 LOG_TSLP_TSK_LEAVE(ercd);
122 return(ercd);
123 }
第95行:winfo(等待情报管理区域)以本地函数变量的方式进行申明.这个和函数slp_tsk是一样的.
第96行:tmevtb(时间事件管理区域)以本地函数变量的方式进行申明.这个在函数slp_tsk中是没有的, slp_tsk函数因为没有超时,所以才没有时间事件管理区域相关的声明.但是在函数tslp_tsk中,tmevtb是必要的.
第101行:如果指定的函数参数不正确的时候,WINFO的ErrorCode中就保存E_PAR的值.
第104~107行:该任务对应的唤醒等待队列是TRUE的场合的处理.这个处理和函数slp_tsk相同. 队唤醒等待队列是FALSE的场合, 不进入唤醒等待状态而继续保持执行状态.
第108行:参数tmout指定为TMO_POL的情况下.
第109行:没有进入唤醒等待状态的时候,返回E_TMOUT
第111~117行:如果参数是正数或者等于TMO_FEVR的场合, 这个时候的处理基本上和函数slp_tsk是一样的.相对于在函数slp_tsk里面调用make_wait(&winfo), 在tslp_tsk函数中调用的函数是make_wait_tmout(&winfo, &tmevtb, tmout);. 函数make_wait_tmout()的调用和函数make_wait()的调用一样,都是使状态迁移到等待状态. 相对于函数make_wait()只有一个参数winfo, 函数make_wait_tmout()有三个参数.还有两个参数是事件区域的地址と超时的值.
第115行:现在正在执行中的任务由于已经进入到等待状态,所以dispatch是一定要的.在这里就调用dispatch.
第116行:超时的时候,或者wup_tsk被调用的时候,或者rel_wai(强制等待解除)被调用的时候,就会调用这行代码.这行代码在dispatch()调用好返回后被调用.
接下来我们来看看make_wait_tmout().
在这个函数里面,把TCB和等待情报管理区域(WINFO),时间事件区域(TMEVTB)相连接. 然后把TMEVTB中登陆的函数给呼出来.
55 void
56 make_wait_tmout(WINFO *p_winfo, TMEVTB *p_tmevtb, TMO tmout)
57 {
58 (void) make_non_runnable(p_runtsk);
59 p_runtsk->p_winfo = p_winfo;
60 if (tmout > 0) {
61 p_winfo->p_tmevtb = p_tmevtb;
62 tmevtb_enqueue(p_tmevtb, (RELTIM) tmout,
63 (CBACK) wait_tmout, (void *) p_runtsk);
64 }
65 else {
66 assert(tmout == TMO_FEVR);
67 p_winfo->p_tmevtb = NULL;
68 }
69 }
第58-59行:slp_tsk情况下调用make_wait()是一样的处理.
第58行:函数make_non_runnable()调用后,在等待队列中,对应的任务的TCB就会被取出.然后p_schedtsk将被更新
第59行:WINFO结构的指针p_winfo和TCB向连接.
第60行:tmout大于零的时候,也就是超时的时候.
第61行:时间事件区域的地址tmevtb和所对应的WINFO进行连接.(図1)
【図1 关于函数tslp_tsk的数据结构】
第62行:在函数tmevtb_enqueue中,向时间事件区域登陆必要的信息.在调用到函数tmevtb_enqueue的地方,把时间事件区域p_tmevtb的信息放入时间事件堆栈中.
第65行:tslp_tsk的参数如果是TMO_FEVR的场合,机能和slp_tsk是一样的.
第67行:因为时间事件区域没有被使用,所以把winfo->p_tmevtb设置为NULL.(不进行连接了).
接下来我们来看看tmevtb_enqueue().
在 时间事件区域中, 时间事件堆中的位置,超时的时候的回调函数, 回调函数的参数一共有三个参数.(図2)
【図2 时间事件区域的结构】
关于这个函数,首先回调函数,回调函数的参数要先登陆.
kernel/time_event.h
166 Inline void
167 tmevtb_enqueue(TMEVTB *p_tmevtb, RELTIM time, CBACK callback, void *arg)
168 {
169 assert(time <= TMAX_RELTIM);
170
171 p_tmevtb->callback = callback;
172 p_tmevtb->arg = arg;
173 tmevtb_insert(p_tmevtb, base_time + time);
174 }
第171-172行:在这里首先,回调函数,回调函数的参数向p_tmevtb中登陆.
第173行:在这里定义了一个绝对时间(base_time)作为参数,在这里作为回调函数而登陆的wait_tmout(),当超时的时候,函数wait_tmout将会被调用.
接下来我们对函数tmevtb_insert()进行剖析.
时间事件记录(TimeEventNote)里面,定义了事件发生时刻和时间事件区域两个成员变量.时间事件记录(TimeEventNote)其实就是按照发生时间的顺序所定义的数组结构.(図3)
在函数tmevtb_insert()中可以生成包括了在函数tmevtb_enqueue()中所要用到的时间事件区域和事件发生时刻的时间事件记录(TimeEventNote).而时间事件记录(TimeEventNote)就以堆的形式存放在时间事件堆中.
【図3 时间事件堆的结构】
在以事件发生时刻的顺序排列的堆中,在合适的位置放入合适的时间事件记录(TimeEventNote).
kernel/time_event.c
214 void
215 tmevtb_insert(TMEVTB *p_tmevtb, EVTTIM time)
216 {
217 uint_t index;
218
219 /*
220 * last_index をインクリメントし,そこから上に挿入位置を探す.
221 */
222 index = tmevt_up(++last_index, time);
223
224 /*
225 * タイムイベントを index の位置に挿入する.
226 */
227 TMEVT_NODE(index).time = time;
228 TMEVT_NODE(index).p_tmevtb = p_tmevtb;
229 p_tmevtb->index = index;
230 }
第222行:放入时间事件记录(TimeEventNote)中的位置信息是保存在index变量中.这个是在时间事件堆中的增加的成员变量.
第227行:从参数传进来的事件发生时刻存入时间事件记录(TimeEventNote).
第228行:在时间事件记录(TimeEventNote)中,保存时间事件区域的地址.
第229行:在这个函数中被确定的时间事件堆中的位置向时间事件区域登陆.
接下来,为了如果超时了就被调用.在时间事件区域中登陆了一个回调函数wait_tmout.马上我们就来说明下函数wait_tmout().
在这个函数里面,在结构体WINFO中保存了ErrorCode和E_TMOUT. 接下来判断是否进入双重等待状态,是的话就进入强制等待,不是的话就进入执行状态.
kernel/wait.c
121 void
122 wait_tmout(TCB *p_tcb)
123 {
124 wait_dequeue_wobj(p_tcb);
125 p_tcb->p_winfo->wercd = E_TMOUT;
126 if (make_non_wait(p_tcb)) {
127 reqflg = TRUE;
128 }
129
133 i_unlock_cpu();
134 i_lock_cpu();
135 }
第124行:在信号量等待的场合,需要把对应的任务从信号量等待队列中除去.象这样对应的任务如果正连接在对象等待队列中的话,就调用函数(wait_dequeue_wobj)来实现从队列中除去操作.调用tslp_tsk()函数的时候,如果没有对象等待的话,也就没有必要再调用函数wait_dequeue_wobj了. 但是函数wait_dequeue_wobj在从对象等待中到时间片用完的时候会被调用到,所以是必要的.
第125行:在WINFO的ErrorCode(wercd)里面,保存E_TMOUT的值.
第126行:函数:make_non_wait从等待状态到能够执行的状态,如果有必要dispatch的话,就返回一个TRUE.(详细的内容请参照第4章).
第127行:因为这个函数是从中断开始被调用,所以在这里就没有发dispatch而直接返回一个TRUE.因为中断句柄(非任务contant)的优先度要比dispatch来得高,即使是在处理中是需要中断句柄的,但是中断完了之前是被保留的(也就是延迟调用dispatch). 在中断退出的处理中, 如果reqflg等于TRUE的话,就呼出dispatch.(请参照第10章)
接下来.关于这次没有调用必要,但对象等待的时候存在必要的wait_dequeue_wobj函数进行说明.
105 Inline void
106 wait_dequeue_wobj(TCB *p_tcb)
107 {
108 if (TSTAT_WAIT_WOBJ(p_tcb->tstat)) {
109 queue_delete(&(p_tcb->task_queue));
110 }
111 }
第108行:如果该任务正连接在对象等待队列中的时候.
第109行:调用函数:queue_delete()后,使该任务的TCB从对象等待队列中消除.这次因为调用的是tslp_tsk,所以也就不存在对象等待,当然函数:queue_delete()也就不会被调用到了.
通过函数:tslp_tsk()被调用,函数:wait_dequeue_wobj也会被调用到, 在这个时候,正连接着对象等待队列, 所以第108行的if分支将不会被满足,在什么都没有处理的情况下,被return回来.那么如何在108行进行是否是对象等待队列的呢.
下面的这行代码就是在第108行的宏:TSTAT_WAIT_WOBJ(p_tcb->tstat)的定义
117 #define TSTAT_WAIT_WOBJ(tstat) (((tstat) & TS_WAIT_MASK) >= TS_WAIT_RDTQ)
在第16章的时候已经进行了说明, 任务的状态是在TCB的tstat字段中进行管理的.
在字段tstat中,用了4bit来区分等待的状态.然后在下面定义了各种不同类型的等待原因
/kernel/task.h
72 #define TS_WAIT_DLY (0x00U << 3) /* 時間経過待ち */
73 #define TS_WAIT_SLP (0x01U << 3) /* 起床待ち */
74 #define TS_WAIT_RDTQ (0x02U << 3) /* データキューからの受信待ち */
75 #define TS_WAIT_RPDQ (0x03U << 3) /* 優先度データキューからの受信待ち */
76 #define TS_WAIT_SEM (0x04U << 3) /* セマフォ資源の獲得待ち */
77 #define TS_WAIT_FLG (0x05U << 3) /* イベントフラグ待ち */
78 #define TS_WAIT_SDTQ (0x06U << 3) /* データキューへの送信待ち */
79 #define TS_WAIT_SPDQ (0x07U << 3) /* 優先度データキューへの送信待ち */
80 #define TS_WAIT_MBX (0x08U << 3) /* メールボックスからの受信待ち */
81 #define TS_WAIT_MPF (0x09U << 3) /* 固定長メモリブロックの獲得待ち*/
在这里,等待原因的排列顺序是有关系存在的.
TS_WAIT_MASK的定义请参照下面.
#define TS_WAIT_MASK ( 0x0fU << 3 )
也就是说,通过tstat) & TS_WAIT_MASK,就可以取出保存在tstat中的等待原因. 这里的等待原因就是除了时间经过等待和唤醒等待以外的其他等待. 也就是说, 在各种不同类型的等待中,为了能够这样使用,才有了这样的排列等待的方式.
关于函数tslp_tsk的返回方法和ErrorCode的返回值的说明请看下面.
・设定的时间片终了时间不正确的时候 E_PAR
・参数指定为TMO_POL的时候 E_TMOUT
・时间片结束的时候 E_TMOUT
・通过调用wup_tsk函数,唤醒等待解除 E_OK
・通过调用rel_wai函数,强制地解除等待的场合 E_RLWAI
总结
在最后,让我们来关注一下根据tslp_tsk(TMO tmout)函数的调用,时间片完了后唤醒等待状态的时候,数据构造极其流程.
STEP1:调用
tslp_tsk函数的任务的堆内存区域中,保证WINFO和TMEVTB的内存区域并且都在连接中(図4).
STEP2:
在结构体TMEVTB中, 登陆回调函数和参数. 具体的回调函数登陆的是wait_tmout(),而参数登陆的是p_runtsk(図4).
【図4 STEP1,STEP2】
STEP3:在时间事件堆中, 在事件发生时刻所对应的位置插入TMEVTB(図5).
STEP4:到了事件发生时刻的时候,从时间事件句柄调用已经登陆上结构体:TMEVTB的函数wait_tmout(p_runtsk)(図5).
【図4 STEP3,STEP4】
在图6中,以STEP1~STEP4的时间轴描述了任务和中断句柄之间的关系.
在过了tmout毫秒以后,通过时间事件句柄调用的函数中,reqflg设定为TRUE,中断处理完了以后,发生dispatch,然后任务被唤醒.
【図6 从函数tslp_tsk开始调用到任务被唤醒的流程】