开始
PJSIP是一个开放源代码的SIP协议栈。它支持多种SIP的扩展功能,目前可说算是最流行的sip协议栈之一了。
构成
- PJSIP - Open Source SIP Stack[开源的SIP协议栈]
- PJMEDIA - Open Source Media Stack[开源的媒体栈]
- PJNATH - Open Source NAT Traversal Helper Library[开源的NAT-T辅助库]
- PJLIB-UTIL - Auxiliary Library[辅助工具库]
- PJLIB - Ultra Portable Base Framework Library[基础框架库]
PJLIB
要理解好PJSIP,就不得不先说说PJLIB,PJLIB算的上是这个库中最基础的库,正是这个
库的优美实现,才让PJSIP变得如此优越。
运行的平台有:
- Win32/x86 (Win95/98/ME, NT/2000/XP/2003, mingw).
- arm, WinCE and Windows Mobile.
- Linux/x86, (user mode and as kernel module(!)).
- Linux/alpha
- Solaris/ultra.
- MacOS X/powerpc
- RTEMS (x86 and powerpc).
PJLIB提供了一系列特征,涉及到:
非动态内存分配[No Dynamic Memory Allocations]
实现了内存池,获取内存是从与分配的内存池中获取,高性能程序多会自己构造内存池
,后面我们会解释该内存池的使用以及基本的原理。根据作者的比较,是常规的 malloc(
)/free()函数的30倍。OS抽象[Operating System Abstraction]
- 线程[Threads.]
- 线程本地存储[Thread Local Storage.]
- 互斥[Mutexes.]
- 信号灯[Semaphores.]
- 原子变量[Atomic Variables.]
- 临届区[Critical sections.]
- 锁对象[Lock Objects.]
- 事件对象[Event Object.]
- 时间管理[Time Data Type and Manipulation.]
- 高解析的时间戳[High Resolution Timestamp.]
低层的网络相关IO[Low-Level Network I/O]
- Socket抽象[Socket Abstraction.]
- 网络地址解析[Network Address Resolution.]
- 实现针对Socket的select API[Socket select() API.]
时间管理[Timer Management]
这主要涉及到两个部分,一个时定时器的管理,还有就是时间解析的精度(举例说来,就是能精确到哪个时间等级,比如 POSIX sleep(),就只能以秒为单位,而使用select()则可以实现毫秒级别的计时)
各种数据结构[Various Data Structures
]- 针对字符串的操作[String Operations]
- 数组辅助[Array helper]
- Hash表[Hash Tabl]
- 链表[Linked List]
- 红黑平衡树[Red/Black Balanced Tree]
异常处理[Exception Construct]
LOG机制[Logging Facility]
随机数以及GUID的产生[Random and GUID Generation]
内存池模块
没有动态分配内存,PJLIB的中心思想是,为了使应用程序尽可能快地运行,它根本不应该使用malloc(),而应该从预分配的存储池中获取内存。使用此方法可以优化的一些:
- 复杂度[alloc() is a O(1) operation.]
- no mutex is used inside alloc(). It is assumed that synchronization will be used in higher abstraction by application anyway.
- no free() is required. All chunks will be deleted when the pool is destroyed.
函数
函数名 | 说明 |
---|---|
pj_init() | 初始化PJ库。在使用库之前,必须先调用此函数。此函数的目的是初始化静态库数据(例如用于随机字符串生成的字符表),以及初始化依赖于操作系统的功能(例如Windows中的WSAStartup())。 |
pj_caching_pool_init() | Create the pool factory.创建池工厂 |
pj_caching_pool_destroy() | destroy caching pool.销毁缓存池 |
pj_pool_create() | Must create pool before we can allocate anything.必须创建池在我们分配前 |
pj_pool_release() | 释放池内存 |
pj_pool_alloc() | allocate some memory chunks,分配内存 |
源代码解析
首先是几个结构体的介绍:
struct pj_pool_t
内存池的结构
1 | /** |
PJ_MAX_OBJ_NAME = 32,就是说内存池的名称大小要在32个字符以内
increment_size 是当空间不足时,扩容每个block的大小,比如increment=4,申请9时会,给出4*3个block并将这3个block合并
上面有个链表结构:
1 |
|
看传入的类型,分配对应类型的链表。
struct pj_pool_block
内存池块block结构:
1 | typedef struct pj_pool_block |
struct pj_caching_pool
缓存池
1 | /** |
pj_list是一个双向链表结构,记录前后节点.free_list大小为16,就是说有16条链表结构。free_list是空闲池列表,表示该表中的内存池处于空闲状态,used_list应用分配的内存池列表也就是当前正在使用的内存池列表。capacity表示当前空闲池的容量大小,max_capacity表示空闲池容量的最大大小。
struct pj_pool_factory
1 | /** |
struct pj_pool_factory_policy
缓存池工厂策略
1 | typedef struct pj_pool_factory_policy |
pj_caching_pool_init()
初始化缓存池
1 | /** 创建池工厂 |
pj_list_init
,对链表进行初始化
1 | /** |
将当前节点前后节点都指向当前节点。
policy,管理策略,工厂只是用来管理内存池对象,对于如何分配内存(有kmalloc,malloc,new),则由策略组件来实现,默认是malloc.但是C 里并没有new,默认地分配块函数如下:
1 | static void *default_block_alloc(pj_pool_factory *factory, pj_size_t size) |
pj_pool_create()
创建新池的函数指向了cpool_create_pool
:
1 | static pj_pool_t *cpool_create_pool(pj_pool_factory *pf, |
pj_list_empty判断该节点是否为空:
1 | /** |
这里判断node的下个节点是否指向自己,所以为空时应该返回true,但是这里注释说为空时返回0,应该是有问题的
上面的初始化池大小策略是:
1 | PJ_DEF(pj_pool_t *) pj_pool_create_int(pj_pool_factory *f, const char *name, |
这里插入节点的函数实现:
1 | PJ_IDEF(void) pj_list_insert_after(pj_list_type *pos, pj_list_type *node) |
分析其形成了循环链表,头指向尾指向头。
将分配的空间节点从空闲池中移除
1 | /* Internal */ |
实际上就是将头节点的next指向头节点的next的next,将头节点的next的next的pre指向头节点,然后让头节点的next的pre和next都指向自己,简而言之就是将头节点的next移出来,将其该节点孤立。
pj_list_insert_before(&cp->used_list, pool);
将分配的内存插入到used_list
实现:
1 | PJ_IDEF(void) pj_list_insert_before(pj_list_type *pos, pj_list_type *node) |
这里pj_list_insert_after
代码之前已经提及到,这里就是说将当前节点插入到used_list的最后末尾,也是一个循环双向链表。
pool_release_pool()
释放池pool_release_pool指向了cpool_release_pool.
1 | static void cpool_release_pool(pj_pool_factory *pf, pj_pool_t *pool) |
查找该池是否在used_list里:
1 | PJ_IDEF(pj_list_type*) pj_list_find_node(pj_list_type *list, pj_list_type *node) |
因为是个双向循环链表,所以当p不等于头节点并且p不等于目标节点才进入循环,当循环退出,判断p是否等于目标节点即可。
销毁内存池pj_pool_destroy_int(pool)
:
1 | PJ_DEF(void) |
重置内存池的实现:
1 | /* |
从上面可以看出重置的时候是将最后一个节点保留了下来,把之前的节点全部释放。
pj_pool_alloc()
分配内存
1 | PJ_IDEF(void *) |
首先会在该内存池中block_list后的第一块block中找是否有size的空间。如果有那么直接返回ptr,否则在后面的block寻找。
1 | PJ_IDEF(void *) |
这里对size进行了变换,编程以4个字节的倍数大小size,因为整数类型会占到4个字节. 如果没有足够的空间则返回NULL,如果是0则不分配内存。
这里是对第一块没有足够空间的情况下:
1 | /* |
以上先遍历block_list的看是否有足够的空间,如果空间不足,则看是否可以扩展,如果可以扩展,那么看扩展block大小是否过小,如果过小要考虑分块存储,将分的块合并成一块,然后开始创建block.在从创建好的block寻找空间
这里又对第一块block也就是block_list.next 找了一遍实际上没有必要,因为之前已经找过一次了
1 | /* |
将创建好的block接在block_list的后面,然后返回该block指针。
pj_caching_pool_destroy()
销毁缓存池
1 | PJ_DEF(void) |
会先遍历free_list销毁空闲内存池,然后遍历used_list销毁应用分配得内存池,然后将锁销毁。
dump_status()
在初始化时dump_status指向了cpool_dump_status
1 | static void cpool_dump_status(pj_pool_factory *factory, pj_bool_t detail) |
如果detail为false,就只会打印简要信息,会打印空闲容量的大小,和空闲容量的最大大小,以及应用持有的内存池数量。如果detail为true,那么还会打印应用持有的每个内存池的名称,已经内存池使用的大小,以及内存池容量,以及占用的百分比,还有应用全部持有的内存池大小,以及全部内存池的容量,以及其占用百分比。
定时器模块
pjlib定时器是从ACE网络库移植过来的。实现在timer.h和timer.c,定时器的原理是有个将来的超时时间,这个时间就是现在时间加上定时器时长。
源代码解析
struct pj_timer_entry
计时器项
1 | /** |
_timer_id就是指entry在timer_ids的下标即index。
struct pj_timer_heap_t
计时器堆
1 | struct pj_timer_heap_t |
timer_ids 中的值表示entry在二叉树heap数组中的位置,负数是timer_ids初始状态,并且初始的状态的值是其timer_ids下标index的加一的相反数,也就是说是记录了自己下一位的index的相反数,timer_ids[0]不记录,timer_ids从1开始依次记录entry,free_list指向了还未被entry占位的timer_ids的下标index,所以每次加入entry时要将entry的_timer_id = free_list,然后让free_list向后移一位,也就是此时timer_ids的index的值的相反数。就是timer_ids跟heap相互记录了对方的位置,便于相互查询。
pj_timer_heap_create()
创建计时器堆
1 | /** |
初始化状态,free_list=1,表示如果有entry加入进来,那么entry在heap堆的位置将记录在timer_ids[1]的位置。这里的lock并没有设置,所以可以为其配置lock.
1 | PJ_DEF(void) |
pj_timer_entry_init()
初始化entry。
1 | PJ_DEF(pj_timer_entry *) |
此时的_timer_id = -1,因为此时还没有插入到计时器堆里,所以设置为-1,其值代表该entry在timer_ids的下标位置。
pj_timer_heap_schedule()
调度计时器,即将entry加入到计时器堆里。
1 | PJ_DEF(pj_status_t) |
pj_gettimeofday
获取现在的时间给expires,PJ_TIME_VAL_ADD
是现在的时间向后加delay的时间。
1 | static pj_status_t schedule_entry(pj_timer_heap_t *ht, |
这里会将free_list赋值给entry的_timer_id,然后free_list等于timer_ids[free_list]的相反数,就是将free_list向后移一位。然后才开始将entry插入到堆里。
1 |
|
如果堆大小不够了,那么将其扩展。
1 | static void grow_heap(pj_timer_heap_t *ht) |
可以看出这里扩展堆是将堆大小扩展到了之前的大小的两倍。
这里仅仅只是将指针重新指向新的堆跟timer_ids,但是并没有将原来的内存释放掉。
如果堆大小足够,那么就开始插入操作,reheap_up代表从堆的底部开始插入,然后根据规则进行向上调整移动。
1 | static void reheap_up(pj_timer_heap_t *ht, pj_timer_entry *moved_node, |
slot当前位置,parent代表父节点的位置,如果slot大于0意思就是当前的heap不为空那么进入循环,如果插入的entry的时间小于父节点,那么则跟父节点复制到slot的位置上,然后依次往上走,直到父节点的时间小于当前的entry,退出循环将entry复制到slot当前位置。
1 | static void copy_node(pj_timer_heap_t *ht, int slot, pj_timer_entry *moved_node) |
将这里将moved_node赋值给heap[slot],并且要更新timer_ids对entry的记录位置。
pj_timer_heap_poll()
轮询计时器堆并将过期的计时器移除,并进行回调,next_delay用来记录,移除后堆顶entry时间跟现在的时间差。
1 | PJ_DEF(unsigned) |
这里移除entry,并进行了回调,这里next记录堆顶过期时间与现在的时间差。
这里计算next_delay时间的时候,会判断时间是这里之所以会出现
next_delay->sec < 0 || next_delay->msec < 0
,因为设定了max_entries_per_poll
,当移除了这么多entry后,也会跳出循环。
1 | static pj_timer_entry *remove_node(pj_timer_heap_t *ht, size_t slot) |
这里将要移除的entry取出,然后将freelist的指向它并且将它的值设置为原free_list的相反数,这样下次加进来的entry会加入到该移除的位置,再下一个free_list又会回到原来的位置。
1 | static void push_freelist(pj_timer_heap_t *ht, pj_timer_id_t old_id) |
设置free_list之后,在将二叉树数组最后节点一个entry复制到该位置,然后开始移动根据父节点时间小于子节点时间的规则,如果该节点小于父节点,那么就往上移动,如果该节点大于父节点,那么该节点就往下判断跟自己的子节点比较,往下移动:
1 | static void reheap_down(pj_timer_heap_t *ht, pj_timer_entry *moved_node, |
因为子节点有两个,所以先比较两个子节点的大小,小的那个替代父节点,这样依次往下移动。
pj_timer_heap_cancel()
取消计时器项,将其从计时器堆里移除
1 | PJ_DEF(int) |
这里的dont_call==0,则执行回调函数,否则不执行。
pj_timer_heap_mem_size()
计算计时器堆的大小
1 | /* |
结构体pj_timer_heap_t的大小,在加上(size+2)个entry指针的和timer_ids的大小,以及132用来lock等大小。
- 本文作者: Veng
- 本文链接: http://veng0923.github.io/2020/09/02/pjlib-内存池-计时器解析/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!