现在的位置: 首页 > 综合 > 正文

基于uClinux的NPTL线程库移植

2018年01月30日 ⁄ 综合 ⁄ 共 5056字 ⁄ 字号 评论关闭

摘要:在Linux2.6中,NPTL(native posix thread library)已取代LinuxThreads成为glibc的首选线程库,但是在嵌入式操作系统中普遍使用的基于POSIX 标准的线程库仍是LinuxThreads。分析了NPTL线程库的内存管理机制,基于嵌入式操作系统uClinux无MMU的特性,修改了线程栈及 uClibe库,实现了NPTL在uClinux上的移植,并在兼容性与效率两方面相对于LinuxThreads线程库进行了测试。

  0 引言
   
  与进程相比,线程是一种非常“节俭”的多任务操作方式且线程间拥有更加方便的通信机制 。因此,线程的引入对于日趋复杂的操作系统而言意义重大。
   
  目前,在嵌入式操作系统中普遍使用的基于POSIX标准的线程库是LinuxThreads。虽然这种实现机制已经在不少的应用当中表现出了较好的性能,但仍存在一定的问题。NPTL(nativeposixthread Ubrary) 是RedHat公司牵头研发的新一代线程库,它在一定程度上弥补了LinuxThreads的缺点。本文将实现NPTL在嵌入式操作系统uClinux上的移植。

  1 NPTL内存管理机制分析
   
  uClinux同标准Linux的最大区别就在于内存管理。对于uClinux来说,其设计针对没有MMU的处理器,所以 uClinux采用实存储器管理策略,所有程序中访问的地址都是实际的物理地址。根据uClinux的特点,NPTL的移植工作将主要集中在内存管理上。
   
  NPTL定义了一个struct pthread数据结构来描述线程。在此数据结构中与内存管理相关的几项属性有:

  Struct pthread{
  ……
  list tlist;/*用于将线程栈链入一个双循环链表中*/
  bool user stack;/*标识线程是否为用户自定义方式*/
  void *stackblock;/*线程栈起始地址*/
  size_t stackblock_size;/*线程栈大小*/
  size_t guardsize;/*保护区大小*/
  ……
  };

  为了节省开销,NPTL采用了以下两项措施来优化内存管理。

  (1)合并必要的内存块。线程描述数据结构与线程局部存储都放在堆栈上,可用的堆栈从这两个结构向下开始(如果是向上的堆栈,则从满足这两个结构的内存向上开始)。线程栈的分配方法随着体系结构的不同而不同,这里只分析i386等平台上(包括一般嵌入式平台)所使用的两种栈组织方式:系统分配方式和用户自定义方式。
   
  在系统分配方式下,NPTL利用mmap()建立一定大小的从物理内存空间到进程虚存区间的映射,并使用mprotect()设置其中第一页为非访问区,用来监测栈溢出。其线程栈空间的功能分配如图1所示。


图1 栈结构

  对于用户自定义方式,按照用户所指定的地址与大小,计算出线程栈顶。在这种方式下并不调用mprotect()进行保护,正确性由用户自己保证。

  (2)缓存已结束线程的线程栈。内存处理,尤其是存储单元的分配比较慢,因此,用于堆栈和线程描述数据结构的内存块在线程结束时并未马上释放,而是保留在特定队列中。NPTL中为每个进程维护着3个静态队列:

  (1)stack_cache:已结束线程的线程栈缓存队列;
  (2)stack_used:未结束线程的线程栈队列,其中的线程都是在系统分配方式下创建;
  (3)stack user:未结束线程的线程栈队列,其中的线程都是在用户自定义方式下创建。
   
  与这些队列相关的静态变量有:
  (1)stack_cache_maxsize:stack cache队列大小的上限;
  (2)stack_cache_actsize:当前stack_cache队列的大小。
   
  这些队列都采用双循环链表结构,利用线程描述数据结构中的list_t list链接。队列结构如图2所示。


图2 线程栈队列

  系统分配方式下,在创建线程时,首先尝试从stack_cache队列中找出一个合适的栈结构,将其用作新线程的栈结构而无需重新分配。若stack_cache队列为空或找不到合适的栈结构,再调用mmap函数进行分配。在对stack_cache队列进行操作时,要注意调整 stack_cache_actsize的大小。若stack_cache_actsize超过stack_cache_maxsize的限制,就要从 stack_cache队列的尾部开始,释放一部分内存块。通过这种方法,避免了内存块的频繁分配与释放,而且在线程结束时,线程描述结构中的某些信息仍处于有用状态,当这些栈结构被重用时就不用重新初始化这些信息,节约了系统时间,提高了系统效率。

  2 基于uClinux的移植

  2.1 线程栈的分配
   
  基于无MMU特性,uClinux重新定义封装了原有的内存分配函数。mmap()系统调用最终由内核函数kmalloc()实现,分配一块物理内存区,返回的是该物理内存区的起始地址。uClibc中所定义的mallocO函数不再以系统调用brk()实现,而是以设置好特定参数的mmap()实现。因此,在将NTPL移植到uClibc中时,可直接调用malloc()函数分配指定大小的线程栈,相应地,用free()函数进行内存的释放。由于uClinux对内存空间没有保护,因此在调用malloc分配了线程栈空间后,并不调用mproteet()设置保护区。这样,程序的正确性就不得不由开发人员来保证。根据嵌入式操作系统的特点,在分配线程栈时可将其默认大小_default_stacksize的值设为16K。修改后的线程栈空间的功能分配如图3所示。


图3 修改后的栈结构

  2.2 与线程栈队列相关的修改
   
  在线程库中,频繁地使用了THREAD_SELF宏,此宏用于表示当前运行线程的线程描述数据结构指针。由于提供了对 TLS(thread local storage)支持,在i386中,通过寄存器GS可以很容易地得到当前运行线程的线程描述数据结构指针。但在uCliux中,并未实现对TLS的支持,因此,要重新定义THREAD_SELF宏。这主要是通过在stack_used和_stack_user两个队列中寻找包含当前运行线程堆栈地址的线程栈结构来实现的。HREAD_SELF宏定义的算法如下:

  #define THREAD_SELF \
  {
  struct pthread *results=NULL;
  CURRENT_STACK_FRAME /*利用硬件寄存器取出当前运行线程堆栈地址*/
  list_for_each(entry,&stack_used)
  {/*从队列头开始遍历stack_used队列 */
  struct pthread *curr;
  /*利用结构成员list在数据结构struct pthread中的偏移量,计算出其属主数据结构struct pthread的地址 */
  curr=list_entry(entry,struct pthread,list);
  if(CURRENT_STACK_FRAME>=curr->stackblock&&
  CURRENT_STACK_FRAME<(curr->stackblock+curp->stackblock_size))
  {/*找到当前运行线程的线程栈结构 */
  result=curr;
  return result;
  }
  }
  list_for_each(entry, &_stack_user)
  { /*按上述步骤从队列头开始遍历stack_user队列 */
  ……
  }
  return result;
  )
   
  NPTL中利用静态变量stack_cache_maxsize限定了stack_cache的最大容量。在i386体系中 stack_cache_maxsize的大小定义为40MB。显然,这对于uClinux而言是不可能实现的,应针对具体系统资源进行适当调整。

  2.3 c库的修改
   
  uClinux小型化的另一个做法是精简了应用程序库,这使得线程库中一些重要的系统调用在uClibc中缺少必要的接口,而无法得以实现。在本系统中,需要增加clone与futex两个系统调用接口到uClibc中,以保证线程库的正确运行。在NPTL中利用静态变量 stack_cache_lock与lll_lock()/lll_unlock()这对宏保证了线程栈队列操作的原子性。在glibc 中,lll_lock()/lll_unlock()最终是通过核心的futex(Fast User Space Mutex)机制实现的。Futex是一种序列化事件使得它们不会相互冲突的机制,它能令调用者在内核中等待,也可以在中断或在超时以后被唤醒。在具体实现时需重定义这对宏,并添加到uClibc中。lll_lock()/lll_unlock()的宏定义算法如下:

  #define lll_lock(stack_cache_lock) \
  {
  atomic inc(stack_cache_lock); /*将stack_cache_lock原子地加1 */
  int val= stack_cache_lock;
  /*若此时stack_cache_lock不等于1,说明已有别的线程在对队列进行操作,则利用系统调用futex挂起当前线程等待 */
  if(val !=1)
  futex(&stack_cache_lock,FUTEX_WAIT,val);
  }
  #define lll_unlock(stack_cache_lock) \
  {
  atomic dec(stack_cache_lock); /*将stack_cache_lock原子地减1*/
  /*若stack_cache_lock不等于0,说明还有别的线程在等待,则利用系统调用futex唤醒一个等待线程 */
  if(stack_cache_lock!=0)
  futex(&stack_cache_lock,FUTEX_WAKE,1);
  }

  3 性能测试与分析
  
  测试的硬件平台采用了ADI公司的ADSP-BF533处理器,32MB的内存,4MB大小Flash。软件平台选用uClinux2.6内核。

  3.1 兼容性
   
  在将NPTL移植到uClinux上后,就其POSIX兼容性进行了一系列测试,主要结果如表1所示。


表1 兼容性测试结果对照

  从表1中的结果可以看出,在NPTL中已经可以实现线程组的概念,而且特定信号的发送将会影响整个进程,这样就可以实现对多线程进程的工作控制。虽然因为核心的限制,NPTL仍然不是100%POSIX兼容的,但相对LinuxThreads已经有很大程度上的改进了。

  3.2 效率
   
  为了进行对比分析,本测试将分别完成对NPTL与LinuxThreads两种线程库在线程创建/销毁时间上的统计。测试程序交替创建一定量线程,创建的线程不进行任何实际操作,创建后即刻返回。在主线程中调用pthread_ join函数来等待子线程返回。利用clock0函数来统计所消耗的处理机时间,clock()返回值的单位为微秒。
   
  将测试程序分别静态编译链接LinuxThreads线程库与NPTL线程库,并将生成的可执行程序置于目标板上运行。经过多次测试并平均后:LinuxThreads线程库线程的创建/销毁时间约为153 us,而NPTL线程库线程的创建/销毁时间约为68us。
   
  可见,NPTL 在线程创建/销毁方面的开销要明显低于LinuxThreads。这主要是因为NPTL不再像LinuxThreads那样需要使用用户级的管理线程来维护线程的创建和销毁,同时,stack cache队列的应用使得在多个线程交替运行时内存块的分配与释放不再那么频繁。

  4 结束语
   
  由于嵌入式系统软硬件条件上的限制,使得NPTL的高效性并不能完全得以体现。但相对于LinuxThreads而言,在性能上仍有一定的提高。更重要的是,NPTL相对于LinuxThreads在很大程度上改进了与POSIX 的兼容性。可以预见,随着Linux在嵌入式领域的扩展,NPTL也将在嵌入式系统中发挥越来越重要的作用。

抱歉!评论已关闭.