ortp数据收发调度器源码分析
rtp在传输中需要定时的去收发数据,ortp库中提供了这种调度器机制,这篇文章主要着眼于ortp的调度器的代码进行分析。
架构
ortp的调度器分三个部分。
- 第一个部分在每个rtpsession
中,有一个WaitPoint
结构体,其中有估算的下一次接收或发送的时间,唤醒用的条件变量和一个唤醒标识。
- 第二个部分是一个全局的调度器,这个调度器是ortp库全局唯一的,整个程序里只能使用一个。用户可以将rtpsession
加入这个调度器,调度器的作用就是不断的死循环检查是否有哪个rtpsession
的下一次接收或发送时间到了,如果到了就设置调度器里的session集合中对应这个rtpsession
的标识量。
- 第三个部分是select
,ortp库模仿实现了一个socket select
,这个select
的作用就是不断查询调度器里的session集合的标识量,select
会把所有标识量被设置了的复制到结果session集合中。用户调用select
后就会知道哪个rtpsession
到该处理的时间了,对这个rtpsession
调用接收或发送函数即可,接收或发送函数中会更新下一次的估计接收或发送时间和唤醒标识。以备下一次的循环。
结构
SessionSet
#define ORTP__FD_SETSIZE 1024
typedef long int ortp__fd_mask;
#define ORTP__NFDBITS (8 * sizeof (ortp__fd_mask))
#define ORTP__FDELT(d) ((d) / ORTP__NFDBITS)
#define ORTP__FDMASK(d) ((ortp__fd_mask) 1 << ((d) % ORTP__NFDBITS))
typedef struct
{
ortp__fd_mask fds_bits[ORTP__FD_SETSIZE / ORTP__NFDBITS];
# define ORTP__FDS_BITS(set) ((set)->fds_bits)
} ortp_fd_set;
struct _SessionSet
{
ortp_fd_set rtpset;
};
typedef struct _SessionSet SessionSet;
session集合的结构是SessionSet结构体,看起来复杂,其实很简单。rtpset
里面其实就是一个long型的数组,长度是1024/(8*sizeof(long))
,因为ortp用一个bit位标识一个rtpsession
,第几个bit位就代表第几个rtpsession
。rtpset
里面的数组计算出的长度刚好是1024个bit位也就是32个字节,也就是说一个SessionSet最多只能管理1024个rtpsession
。在后面的分析中,可以看到全局的调度器刚好最多也就能处理1024个rtpsession
。
RtpScheduler
struct _RtpScheduler {
RtpSession *list; /* list of scheduled sessions*/
SessionSet all_sessions; /* mask of scheduled sessions */
int all_max; /* the highest pos in the all mask */
SessionSet r_sessions; /* mask of sessions that have a recv event */
int r_max;
SessionSet w_sessions; /* mask of sessions that have a send event */
int w_max;
SessionSet e_sessions; /* mask of session that have error event */
int e_max;
int max_sessions; /* the number of position in the masks */
/* GMutex *unblock_select_mutex; */
ortp_cond_t unblock_select_cond;
ortp_mutex_t lock;
ortp_thread_t thread;
int thread_running;
struct _RtpTimer *timer;
uint32_t time_; /*number of miliseconds elapsed since the start of the thread */
uint32_t timer_inc; /* the timer increment in milisec */
};
typedef struct _RtpScheduler RtpScheduler;
这个就是调度器的结构。
- list
是调度器管理的rtpsession
链表
- all_sessions
是调度器管理的所有的rtpsession
的session集合
- all_max
是调度器曾经管理的rtpsession
的最大的数量,也就是all_sessions
集合中最多使用了多少个bit位,检查标志位时只要检查不超过这个数的bit位就行。
- r_sessions
是调度器管理的所有的用来接收数据的rtpsession
的session集合
- r_max
本意应该和all_max
对应,但是实际上并没有使用。
- w_sessions
是调度器管理的所有的用来发送数据的rtpsession
的session集合
- w_max
同r_max
- e_sessions
本意应该是调度器管理的发生错误的rtpsession
的session集合,但是实际上并没有使用。
- e_max
同r_max
- max_sessions
是调度器管理的rtpsession
数量的上限
- unblock_select_cond
是调度器更新了标识量后用来通知select
函数继续轮询的条件变量,后面针对函数代码的分析中会具体分析。
- lock
是操作调度器的互斥锁
- thread
是用来不断检查时间是否到期后更新标识量的线程
- thread_running
是thread
线程是否需要继续循环的标识量
- timer
是调度器的定时器
- time_
是调度器的计时器,记录了从调度器开始运行线程后过去的时间(毫秒)
- timer_inc
是定时器timer
的时间间隔(毫秒)
WaitPoint
typedef struct _WaitPoint
{
ortp_mutex_t lock;
ortp_cond_t cond;
uint32_t time;
bool_t wakeup;
} WaitPoint;
这个结构是一个用来记录等待信息的结构。
- lock
是互斥锁
- cond
是用来唤醒阻塞状态的条件变量
- time
是用来记录等待的时间点的时间(毫秒),此值记录的时间是和RtpScheduler
的time_
成员比较的。
- wakeup
是标志是否在等待唤醒的标志
RtpSession
struct _RtpSession
{
RtpSession *next; /* next RtpSession, when the session are enqueued by the scheduler */
int mask_pos; /* the position in the scheduler mask of RtpSession : do not move this field: it is part of the ABI since the session_set macros use it*/
struct {
RtpProfile *profile;
int pt;
unsigned int ssrc;
WaitPoint wp;
} snd,rcv;
unsigned int inc_ssrc_candidate;
int inc_same_ssrc_count;
int hw_recv_pt; /* recv payload type before jitter buffer */
int recv_buf_size;
int target_upload_bandwidth; /* Target upload bandwidth at nework layer (with IP and UDP headers) in bits/s */
RtpSignalTable on_ssrc_changed;
RtpSignalTable on_payload_type_changed;
RtpSignalTable on_telephone_event_packet;
RtpSignalTable on_telephone_event;
RtpSignalTable on_timestamp_jump;
RtpSignalTable on_network_error;
RtpSignalTable on_rtcp_bye;
struct _OList *signal_tables;
struct _OList *eventqs;
msgb_allocator_t allocator;
RtpStream rtp;
RtcpStream rtcp;
OrtpRtcpXrStats rtcp_xr_stats;
RtpSessionMode mode;
struct _RtpScheduler *sched;
uint32_t flags;
int dscp;
int multicast_ttl;
int multicast_loopback;
float duplication_ratio; /* Number of times a packet should be duplicated */
float duplication_left ; /* Remainder of the duplication ratio */
void * user_data;
/* FIXME: Should be a table for all session participants. */
struct timeval last_recv_time; /* Time of receiving the RTP/RTCP packet. */
mblk_t *pending;
/* telephony events extension */
int tev_send_pt; /*telephone event to be used for sending*/
mblk_t *current_tev; /* the pending telephony events */
mblk_t *minimal_sdes;
mblk_t *full_sdes;
queue_t contributing_sources;
int64_t lost_packets_test_vector;
unsigned int interarrival_jitter_test_vector;
unsigned int delay_test_vector;
float rtt;/*last round trip delay calculated*/
int cum_loss;
OrtpNetworkSimulatorCtx *net_sim_ctx;
bool_t symmetric_rtp;
bool_t permissive; /*use the permissive algorithm*/
bool_t use_connect; /* use connect() on the socket */
bool_t ssrc_set;
bool_t reuseaddr; /*setsockopt SO_REUSEADDR */
unsigned char avpf_features; /**< A bitmask of ORTP_AVPF_FEATURE_* macros. */
};
typedef struct _RtpSession RtpSession;
因为本文主要着重与调度器的源码分析,所以对于RtpSession
结构只分析和调度器相关的成员。
- next
可以用来构建链表,调度器中的RtpSession
链表就使用了这个成员来构建链表,如果不用调度器管理,那么置NULL。见RtpScheduler
结构的list
成员。
- mask_pos
记录了此RtpSession
在调度器中的Session集合中的位置,即Session集合中第几个bit位代表这个RtpSession
。如果不用调度器管理,那么这个成员无用。
- snd,rcv
是存储发送和接受关键数据的的一个结构体,其中我们主要关心结构体中的wp
。wp
成员是前文介绍过的WaitPoint
结构。
- sched
表示此RtpSession
属于的调度器,如果不属于调度器管理则为NULL。
- flags
是一个标识量,其中每一位bit都代表一个状态。
函数
rtp_scheduler_set_timer
void rtp_scheduler_set_timer(RtpScheduler *sched,RtpTimer *timer)
{
if (sched->thread_running){
ortp_warning("Cannot change timer while the scheduler is running !!");
return;
}
sched->timer=timer;
/* report the timer increment */
sched->timer_inc=(timer->interval.tv_usec/1000) + (timer->interval.tv_sec*1000);
}
设置调度器的定时器,同时将调度器的时间间隔和定时器的时间间隔统一。
rtp_scheduler_init
void rtp_scheduler_init(RtpScheduler *sched)
{
sched->list=0;
sched->time_=0;
/* default to the posix timer */
#if !defined(ORTP_WINDOWS_UNIVERSAL)
rtp_scheduler_set_timer(sched,&posix_timer);
#endif
ortp_mutex_init(&sched->lock,NULL);
ortp_cond_init(&sched->unblock_select_cond,NULL);
sched->max_sessions=sizeof(SessionSet)*8;
session_set_init(&sched->all_sessions);
sched->all_max=0;
session_set_init(&sched->r_sessions);
sched->r_max=0;
session_set_init(&sched->w_sessions);
sched->w_max=0;
session_set_init(&sched->e_sessions);
sched->e_max=0;
}
初始化调度器,此函数主要是将调度器结构内部的成员置为初始状态。
其中值得注意的是将全局唯一的posix_timer
定时器对象设为调度器的定时器。(关于定时器我在另一篇文章中已经讲过,这个posix_timer
定时器是唯一可用的定时器,周期为50毫秒,而且贯穿ortp源码也只有这里使用了这个定时器。)
还有max_sessions
的值是SessionSet
的空间乘8,因为每个字节占8bit空间。而上文可得到SessionSet
的结构占据32个字节,max_sessions
就是1024。说明调度器最多管理1024个rtpsession
。
RtpScheduler * rtp_scheduler_new()
{
RtpScheduler *sched=(RtpScheduler *) ortp_malloc(sizeof(RtpScheduler));
memset(sched,0,sizeof(RtpScheduler));
rtp_scheduler_init(sched);
return sched;
}
新建调度器,简单的分配内存空间,然后置空后调用rtp_scheduler_init
初始化。
rtp_scheduler_start
void rtp_scheduler_start(RtpScheduler *sched)
{
if (sched->thread_running==0){
sched->thread_running=1;
ortp_mutex_lock(&sched->lock);
ortp_thread_create(&sched->thread, NULL, rtp_scheduler_schedule,(void*)sched);
ortp_cond_wait(&sched->unblock_select_cond,&sched->lock);
ortp_mutex_unlock(&sched->lock);
}
else ortp_warning("Scheduler thread already running.");
}
启动调度器,设置线程运行状态后启动调度器的工作线程,线程入口函数为rtp_scheduler_schedule
。
rtp_scheduler_schedule
void * rtp_scheduler_schedule(void * psched)
{
RtpScheduler *sched=(RtpScheduler*) psched;
RtpTimer *timer=sched->timer;
RtpSession *current;
/* take this lock to prevent the thread to start until g_thread_create() returns
because we need sched->thread to be initialized */
ortp_mutex_lock(&sched->lock);
ortp_cond_signal(&sched->unblock_select_cond); /* unblock the starting thread */
ortp_mutex_unlock(&sched->lock);
timer->timer_init();
while(sched->thread_running)
{
/* do the processing here: */
ortp_mutex_lock(&sched->lock);
current=sched->list;
/* processing all scheduled rtp sessions */
while (current!=NULL)
{
ortp_debug("scheduler: processing session=0x%p.\n",current);
rtp_session_process(current,sched->time_,sched);
current=current->next;
}
/* wake up all the threads that are sleeping in _select() */
ortp_cond_broadcast(&sched->unblock_select_cond);
ortp_mutex_unlock(&sched->lock);
/* now while the scheduler is going to sleep, the other threads can compute their
result mask and see if they have to leave, or to wait for next tick*/
//ortp_message("scheduler: sleeping.");
timer->timer_do();
sched->time_+=sched->timer_inc;
}
/* when leaving the thread, stop the timer */
timer->timer_uninit();
return NULL;
}
这个函数是调度器的工作函数,其主要任务就是检查是否有rtpsession
到了需要唤醒的时间并更新相应session集合的状态。首先启动定时器,然后遍历调度器上存储rtpsession
的链表,对每个rtpsession
调用rtp_session_process
这个处理函数,然后用条件变量唤醒所有可能在等待的select
。最后调用定时器的等待函数睡眠并更新运行时间。
rtp_scheduler_add_session
void rtp_scheduler_add_session(RtpScheduler *sched, RtpSession *session)
{
RtpSession *oldfirst;
int i;
if (session->flags & RTP_SESSION_IN_SCHEDULER){
/* the rtp session is already scheduled, so return silently */
return;
}
rtp_scheduler_lock(sched);
/* enqueue the session to the list of scheduled sessions */
oldfirst=sched->list;
sched->list=session;
session->next=oldfirst;
if (sched->max_sessions==0){
ortp_error("rtp_scheduler_add_session: max_session=0 !");
}
/* find a free pos in the session mask*/
for (i=0;i<sched->max_sessions;i++){
if (!ORTP_FD_ISSET(i,&sched->all_sessions.rtpset)){
session->mask_pos=i;
session_set_set(&sched->all_sessions,session);
/* make a new session scheduled not blockable if it has not started*/
if (session->flags & RTP_SESSION_RECV_NOT_STARTED)
session_set_set(&sched->r_sessions,session);
if (session->flags & RTP_SESSION_SEND_NOT_STARTED)
session_set_set(&sched->w_sessions,session);
if (i>sched->all_max){
sched->all_max=i;
}
break;
}
}
rtp_session_set_flag(session,RTP_SESSION_IN_SCHEDULER);
rtp_scheduler_unlock(sched);
}
这个函数用来将rtpsession
添加到调度器中受其管理。首先检查这个rtpsession
是否已经添加到了调度器里,不能重复添加。这里可以看出rtpsession
的flag
是否包含RTP_SESSION_IN_SCHEDULER
标识表示是否添加到了调度器。然后将这个rtpsession
添加到list
这个链表里面。
接下来是一个for循环,这个循环主要的作用是找到all_sessions
这个session集合中空闲的位置。可以看到循环的次数最多是管理rtpsession
的最大数量。循环从all_sessions
的第一位开始测试,找到最靠前的空闲位置,然后把这个位置序号写入rtpsession
的mask_pos
成员,这样以后就可以根据这个成员从session集合找到这个rtpsession
。之后将这个rtpsession
加入all_sessions
。然后根据这个rtpsession
是发送类型还是接收类型将这个rtpsession
加入相应的session集合。最后更新调度器的管理的rtpsession
的最大数量和将rtpsession
的flag
添加RTP_SESSION_IN_SCHEDULER
标识。
这个函数通常不会被直接调用,而是通过rtp_session_set_scheduling_mode
这个函数将rtpsession
加入调度器。
rtp_scheduler_remove_session
void rtp_scheduler_remove_session(RtpScheduler *sched, RtpSession *session)
{
RtpSession *tmp;
int cond=1;
return_if_fail(session!=NULL);
if (!(session->flags & RTP_SESSION_IN_SCHEDULER)){
/* the rtp session is not scheduled, so return silently */
return;
}
rtp_scheduler_lock(sched);
tmp=sched->list;
if (tmp==session){
sched->list=tmp->next;
rtp_session_unset_flag(session,RTP_SESSION_IN_SCHEDULER);
session_set_clr(&sched->all_sessions,session);
rtp_scheduler_unlock(sched);
return;
}
/* go the position of session in the list */
while(cond){
if (tmp!=NULL){
if (tmp->next==session){
tmp->next=tmp->next->next;
cond=0;
}
else tmp=tmp->next;
}else {
/* the session was not found ! */
ortp_warning("rtp_scheduler_remove_session: the session was not found in the scheduler list!");
cond=0;
}
}
rtp_session_unset_flag(session,RTP_SESSION_IN_SCHEDULER);
/* delete the bit in the mask */
session_set_clr(&sched->all_sessions,session);
rtp_scheduler_unlock(sched);
}
这个函数用来将rtpsession
移出调度器。首先还是检查这个rtpsession
是否已经添加到了调度器里,如果没添加过是不能删除的。
接着检查list
链表中的第一个rtpsession
是否就是这个要移除的rtpsession
,如果是那么很幸运直接让list
指向下一个rtpsession
从而从链表中删除这个rtpsession
,然后从all_sessions
中清除这个rtpsession
和清除rtpsession
中flag
的RTP_SESSION_IN_SCHEDULER
标识。
如果list
链表中的第一个rtpsession
不是要找的目标,那么就要遍历list
链表,找到后从链表中移除这个rtpsession
。然后同样从all_sessions
中清除这个rtpsession
和清除rtpsession
中flag
的RTP_SESSION_IN_SCHEDULER
标识。
rtp_session_process
void rtp_session_process (RtpSession * session, uint32_t time, RtpScheduler *sched)
{
wait_point_lock(&session->snd.wp);
if (wait_point_check(&session->snd.wp,time)){
session_set_set(&sched->w_sessions,session);
wait_point_wakeup(&session->snd.wp);
}
wait_point_unlock(&session->snd.wp);
wait_point_lock(&session->rcv.wp);
if (wait_point_check(&session->rcv.wp,time)){
session_set_set(&sched->r_sessions,session);
wait_point_wakeup(&session->rcv.wp);
}
wait_point_unlock(&session->rcv.wp);
}
这个函数是在调度器的工作函数中处理每一个rtpsession
的函数。首先调用wait_point_check
函数检查发送等待点的时间是否到了,这里用来比较的wp
里的时间是在发送函数中计算好的,time
是从参数传入的,从rtp_scheduler_schedule
函数的介绍中可以看出这个时间是当前时间。如果时间到了那么就在调度器的w_sessions
发送集合中将代表这个rtpsession
的bit位置1,然后唤醒可能在等待的wp
里的条件变量。下面是对接收等待点做同样的工作,不在赘述。
wait_point_check
#define RTP_TIMESTAMP_IS_NEWER_THAN(ts1,ts2) \
((uint32_t)((uint32_t)(ts1) - (uint32_t)(ts2))< (uint32_t)(1<<31))
#define TIME_IS_NEWER_THAN(t1,t2) RTP_TIMESTAMP_IS_NEWER_THAN(t1,t2)
bool_t wait_point_check(WaitPoint *wp, uint32_t t){
bool_t ok=FALSE;
if (wp->wakeup){
if (TIME_IS_NEWER_THAN(t,wp->time)){
wp->wakeup=FALSE;
ok=TRUE;
}
}
return ok;
}
这个函数是用来检查等待点的时间是否到了函数,首先检查这个等待点的wakeup
是否是真来决定是否需要被唤醒,然后比较传入的时间是否新于等待点中存储的时间,如果是就代表需要唤醒并且把wakeup
置为false防止重复唤醒。
wait_point_wakeup_at
void wait_point_wakeup_at(WaitPoint *wp, uint32_t t, bool_t dosleep){
wp->time=t;
wp->wakeup=TRUE;
if (dosleep) ortp_cond_wait(&wp->cond,&wp->lock);
}
这个函数就是设置等待点中的等待时间并且重置可唤醒状态,同时根据参数如果需要等待,会一直等待条件变量被唤醒。
ortp_scheduler_init
RtpScheduler *__ortp_scheduler;
void ortp_scheduler_init()
{
static bool_t initialized=FALSE;
if (initialized) return;
initialized=TRUE;
#ifdef __hpux
/* on hpux, we must block sigalrm on the main process, because signal delivery
is ?random?, well, sometimes the SIGALRM goes to both the main thread and the
scheduler thread */
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGALRM);
sigprocmask(SIG_BLOCK,&set,NULL);
#endif /* __hpux */
__ortp_scheduler=rtp_scheduler_new();
rtp_scheduler_start(__ortp_scheduler);
}
这个函数是初始化调度器的函数,可以看出只是简单的分配空间并启动调度器,关键在于创建出来的__ortp_scheduler
调度器是一个全局变量。说明全局只有这么一个唯一的调度器。如果要使用调度器功能必须先调用一次这个函数,ortp库不会自动调用这个函数,必须用户手动调用,否则就无法使用调度器功能。
ortp_get_scheduler
RtpScheduler * ortp_get_scheduler()
{
if (__ortp_scheduler==NULL) ortp_error("Cannot use the scheduled mode: the scheduler is not "
"started. Call ortp_scheduler_init() at the begginning of the application.");
return __ortp_scheduler;
}
这个函数是用来获取全局唯一的调度器的,当调用过ortp_scheduler_init
之后,就可以调用此函数来获取可用的调度器了。在ortp中使用的调度器只能通过这个函数来获取,不能自己新建调度器,因为调度器中使用了一些全局唯一的资源,如果有多个调度器运行就会造成冲突。
session_set_new
SessionSet * session_set_new()
{
SessionSet *set=(SessionSet *) ortp_malloc(sizeof(SessionSet));
session_set_init(set);
return set;
}
这个函数是你用来新建SessionSet
的,其中只是简单的分配空间,然后初始化。
count_power_items_fast
int count_power_items_fast(int v)
{
int c = 0;
while(v) {
c += (v & 1);
v >>= 1;
}
return c;
}
这个函数是用来检测一个4字节的数据上有多少个bit位被置1。
session_set_and
int session_set_and(SessionSet *sched_set, int maxs, SessionSet *user_set, SessionSet *result_set)
{
uint32_t *mask1,*mask2,*mask3;
int i=0;
int ret=0;
mask1=(uint32_t*)(void*)&sched_set->rtpset;
mask2=(uint32_t*)(void*)&user_set->rtpset;
mask3=(uint32_t*)(void*)&result_set->rtpset;
while(i<maxs+1){
*mask3=(*mask1) & (*mask2); /* computes the AND between the two masks*/
/* and unset the sessions that have been found from the sched_set */
*mask1=(*mask1) & (~(*mask3));
ret += count_power_items_fast(*mask3);
i+=32;
mask1++;
mask2++;
mask3++;
}
//printf("session_set_and: ret=%i\n",ret);
return ret;
}
这个函数是检测两个SessionSet
之间有多少个相同的bit位都被置为1。结果存放在第四个参数中,函数会将第一个参数和第三个参数中相同bit位置上都被置1的数据都复制第四个参数中。也就是说如果第一个参数和第三个参数中某个bit位上都是1,那么第四个参数中这个bit位也会被置1。第二个参数max
表示传入的第一个参数和第三个参数中含有多少个需要比较的bit位。
首先取出需要比较的两个SessionSet
和存放结果的字节数组的第一个元素。然后遍历比较,先将待比较的两个SessionSet
的元素做位与运算存放在结果的SessionSet
的元素中,这一步其实就是找出在两个元素中同一bit位上都是1的元素。然后将刚才的结果取反和第一个元素做位与,存回第一个元素中,这一步的作用就是在第一个元素中将刚才复制到结果中的位置0,也就是说一旦存放到结果集中那么原来的位置就要清零。然后调用count_power_items_fast
函数计算出结果元素中有多少位置1,并累加记录。然后就是比较数组中的下一个元素。至于i
为什么加上32,是因为i
作为控制循环结束的关键变量代表的是结果集中剩余的bit位数,每个元素是4个字节也就是32个bit位。循环结束后,result_set
集合中就存放了结果,并且函数返回结果集合中置1的个数。
session_set_select
int session_set_select(SessionSet *recvs, SessionSet *sends, SessionSet *errors)
{
int ret=0,bits;
SessionSet temp;
RtpScheduler *sched=ortp_get_scheduler();
/*lock the scheduler to not read the masks while they are being modified by the scheduler*/
rtp_scheduler_lock(sched);
while(1){
/* computes the SessionSet intersection (in the other words mask intersection) between
the mask given by the user and scheduler masks */
if (recvs!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->r_sessions,sched->all_max,recvs,&temp);
ret+=bits;
/* copy the result set in the given user set (might be empty) */
if (ret>0) session_set_copy(recvs,&temp);
}
if (sends!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->w_sessions,sched->all_max,sends,&temp);
ret+=bits;
if (ret>0){
/* copy the result set in the given user set (might be empty)*/
session_set_copy(sends,&temp);
}
}
if (errors!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->e_sessions,sched->all_max,errors,&temp);
ret+=bits;
if (ret>0){
/* copy the result set in the given user set */
session_set_copy(errors,&temp);
}
}
if (ret>0){
/* there are set file descriptors, return immediately */
//printf("There are %i sessions set, returning.\n",ret);
rtp_scheduler_unlock(sched);
return ret;
}
//printf("There are %i sessions set.\n",ret);
/* else we wait until the next loop of the scheduler*/
ortp_cond_wait(&sched->unblock_select_cond,&sched->lock);
}
return -1;
}
这个函数就是调度器模块关键函数,模仿了SOCKET
的select
,参数也模仿了select
,有接收(读)、发送(写)、错误集合。
首先调用ortp_get_scheduler
获取全局唯一的调度器,然后循环开始。
初始化一个临时的SessionSet
,调用session_set_and
比较传入的接收集合参数和调度器的接收集合,session_set_and
函数的含义已在上文介绍,此处传入的接收集合参数代表着外部的调用者关心哪些rtpsession
,此时调用session_set_and
函数就是要找出传入的接收集合代表的rtpsession
有哪些在调度器的接收集合中也被置为1。通过前文的介绍我们知道当一个rtpsession
的等待点中的时间到点后就会将调度器中相应的session集合中把对应的bit位置1。所以调用session_set_and
函数得到的结果就是等待点的时间已经到点的rtpsession
,可以进行接收数据。可以看到结果是存在临时的SessionSet
的,之后要将临时结果复制到参数传入的接收集合。并累加记录找到的可以开始接收数据的rtpsession
数量。
接下来和接受集合类似,是对发送集合和错误集合的处理,过程一致。这里指的一提的是错误集合的处理,虽然这里有错误集合的处理,但是实际上我们分析了调度器的工作函数后发现调度器并没有对错误集合做任何处理,也就是说这里对错误集合的处理其实是无用功,每次的结果都是空集,估计又是作者挖下的坑忘了填。
最后关注一下函数的结束,如果有找到可以开始发送或者接收的rtpsession
,那么久返回结果。否则就继续循环,注意循环尾部有一个等待条件变量,这个变量前面提到过,为什么要等到这个条件变量呢?因为如果没找到结果那么这个函数就会死循环工作,但是其中调度器的集合中的bit位更新是定时的,如果调度器没有更新集合的数据那么再次循环没有任何意义,只会浪费CPU。而上文中可以看到调度器每次更新集合后会尝试唤醒这个条件变量,此时让循环继续才有意义,这样就可以免得CPU空转。
ortp的文档中并没有详细说明怎么使用这个函数,那么这个函数该怎么使用呢?socket select
的使用需要先构造socket
集合,然后把需要查询状态的socket
加入集合,把集合传给socket select
。它的使用方式和socket select
类似,也需要先构造3个集合,构造集合需要使用session_set_new
函数,然后使用session_set_set
宏将rtpsession
加入对应状态的集合,传入session_set_select
后得到结果,然后用session_set_is_set
宏挨个检查是否每个rtpsession
是否在session_set_select
函数的结果中,如果在接收集合中发现某个rtpsession
被设置,那么说明此时可以在这个rtpsession
上接收数据了,相应的发送集合也是一样。
session_set_timedselect
int session_set_timedselect(SessionSet *recvs, SessionSet *sends, SessionSet *errors, struct timeval *timeout)
{
int ret=0,bits;
int remainingTime; // duration in ms
SessionSet temp;
RtpScheduler *sched;
if (timeout==NULL)
return session_set_select(recvs, sends, errors);
sched=ortp_get_scheduler();
remainingTime = timeout->tv_usec/1000 + timeout->tv_sec*1000;
/*lock the scheduler to not read the masks while they are being modified by the scheduler*/
rtp_scheduler_lock(sched);
do {
/* computes the SessionSet intersection (in the other words mask intersection) between
the mask given by the user and scheduler masks */
if (recvs!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->r_sessions,sched->all_max,recvs,&temp);
ret+=bits;
/* copy the result set in the given user set (might be empty) */
if (ret>0) session_set_copy(recvs,&temp);
}
if (sends!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->w_sessions,sched->all_max,sends,&temp);
ret+=bits;
if (ret>0){
/* copy the result set in the given user set (might be empty)*/
session_set_copy(sends,&temp);
}
}
if (errors!=NULL){
session_set_init(&temp);
bits=session_set_and(&sched->e_sessions,sched->all_max,errors,&temp);
ret+=bits;
if (ret>0){
/* copy the result set in the given user set */
session_set_copy(errors,&temp);
}
}
if (ret>0){
/* there are set file descriptors, return immediately */
//printf("There are %i sessions set, returning.\n",ret);
rtp_scheduler_unlock(sched);
return ret;
}
//printf("There are %i sessions set.\n",ret);
/* else we wait until the next loop of the scheduler*/
ortp_cond_wait(&sched->unblock_select_cond,&sched->lock);
remainingTime -= sched->timer_inc;
} while (remainingTime>0);
rtp_scheduler_unlock(sched);
return -1;
}
这个函数和上一个函数的作用一样,唯一的区别就是上一个函数如果没找到结果就会一直循环,而此函数可以传入一个代表时间长度的参数,如果寻找过程超过这个时长还没找到结果那么就会返回。
rtp_session_set_scheduling_mode
void
rtp_session_set_scheduling_mode (RtpSession * session, int yesno)
{
if (yesno)
{
RtpScheduler *sched;
sched = ortp_get_scheduler ();
if (sched != NULL)
{
rtp_session_set_flag (session, RTP_SESSION_SCHEDULED);
session->sched = sched;
rtp_scheduler_add_session (sched, session);
}
else
ortp_warning
("rtp_session_set_scheduling_mode: Cannot use scheduled mode because the "
"scheduler is not started. Use ortp_scheduler_init() before.");
}
else
rtp_session_unset_flag (session, RTP_SESSION_SCHEDULED);
}
这个函数的作用是设置rtpsession
的调度器根据参数,如果是true就获取全局唯一的调度器,然后设置RTP_SESSION_SCHEDULED
标识量并调用rtp_scheduler_add_session
。否则就取消RTP_SESSION_SCHEDULED
标识量。
rtp_session_recvm_with_ts
mblk_t *
rtp_session_recvm_with_ts (RtpSession * session, uint32_t user_ts)
{
……
if (session->flags & RTP_SESSION_SCHEDULED) {
session->rtp.rcv_time_offset = sched->time_;
//ortp_message("setting snd_time_offset=%i",session->rtp.snd_time_offset);
}
……
if (session->flags & RTP_SESSION_SCHEDULED)
{
/* if we are in blocking mode, then suspend the calling process until timestamp
* wanted expires */
/* but we must not block the process if the timestamp wanted by the application is older
* than current time */
wait_point_lock(&session->rcv.wp);
packet_time =
rtp_session_ts_to_time (session,
user_ts -
session->rtp.rcv_query_ts_offset) +
session->rtp.rcv_time_offset;
ortp_debug ("rtp_session_recvm_with_ts: packet_time=%i, time=%i",packet_time, sched->time_);
if (TIME_IS_STRICTLY_NEWER_THAN (packet_time, sched->time_))
{
wait_point_wakeup_at(&session->rcv.wp,packet_time, (session->flags & RTP_SESSION_BLOCKING_MODE)!=0);
session_set_clr(&sched->r_sessions,session);
}
else session_set_set(&sched->r_sessions,session); /*to unblock _select() immediately */
wait_point_unlock(&session->rcv.wp);
}
……
}
这个函数是ortp库中用来接收数据的函数,因为源码太长,所以这里只截取了和调度器相关的部分分析,其它的部分省略。注释部分有snd
字样应该是作者写错了。
首先检查rtpsession
如果有RTP_SESSION_SCHEDULED
标识说明是由调度器管理,那么将rtp.rcv_time_offset
成员更新为调度器的当前时间。
然后从网络上接收数据,接收处理完毕后计算下一次接收的预计时间。user_ts
传入参数是此次接收期望收到的数据的时间戳,注意这里的时间戳不是时间。rcv_query_ts_offset
是这个rtpsession
第一次接收数据时传入的user_ts
,两个值相减可以得到时间戳差值,然后调用rtp_session_ts_to_time
根据负载类型将时间戳转换成时长。把这个时长加上rtp.rcv_time_offset
就可以估计出下一次的接收时间。然后检查这个时间是否比当前的时间更新,如果这个时间更新那么将这个时间设置到接收等待点中,并且清空调度器中接收集合中相应的位。如果计算出的时间比当前时间早说明可能漏掉了接收,将调度器中接收集合中相应的位置1,使得下次select
可以立即再次选出这个rtpsession
接收数据。
相应的发送函数__rtp_session_sendm_with_ts
的代码中也有处理调度器的代码,和接收函数中的大同小异,区别在于发送函数中先处理调度器的相关事务再进行发送,这点和接收函数相反。其他的只是对接收变量的操作变为针对对应的发送变量。
此处要注意,下一个包的预计传输时间和用户调用函数时传入的user_ts
密切相关,所以要想调度器正常运行必须要传入正确的user_ts
值。
总结
调度器源码的分析到此为止,本文详细的分析了ortp库调度器的函数、结构、用法。可以看到,ortp库仿照socket
精心够构造了一套调度体系,水平很高,其中有很多值得我们学习的地方。但是ortp的文档稀少,相关的博客也基本是浅尝辄止,很多使用方法和注意点文档中都并没有提到,需要透彻的理解代码后才能正确使用。所以我在分析代码的过程中对这些知识顺便做个介绍,希望能够帮助到其他ortp的使用者。