摘 要 多线程技术是一项新的微处理器体系结构技术,它在传统并行指令集的基础上,添加了并行线程集,对操作系统来说,一个具有多线程的微处理器就相当于两个逻辑处理器。任何微处理器的使用都需要有操作系统的支持,Linux是UNIX类的操作系统,其发展十分迅猛,已经成为使用最为广泛的网络服务器操作系统。Linux提供的稳定性和强大网络支持能力必将使多线程技术发挥得更为快捷和顺畅。本文介绍了多线程技术的概念,Linux内核的技术特点等,并分析了在Linux下多线程技术的实现方式,以及遇到的问题。
关键词 Linux内核;多线程;互斥
中图分类号TP39 文献标识码A 文章编号 1674-6708(2013)94-0201-02
1多线程技术及优势
进程是程序执行的动态过程,是系统分配资源的最小单位。而线程是更小的执行实体,是进程的分支,其本身不占用系统资源,而是与其所属的进程共享系统的软硬件资源。创建和撤销另一个进程的工作可以由同一个线程完成,多个线程可以同时进行。就绪、阻塞、运行,这三种状态是线程的基本状态。
多线程技术旨在提高CPU的性能,一个CPU同能能执行多个程序,能分享同一个CPU的资源。但是多线程技术并不等于是多个CPU,当两个线程同时需要同一个资源的时候,只有一个能够得到,另一个就要等待,进而暂时停止,直到资源限制方可继续。
使用多线程技术的原因有三,其一,与进程相比,线程这种多任务操作方式,能最大程度的节约系统的耗费,其耗费大约是进程的1/30左右。其二,线程之间方便通信。其三,多线程作为一种多任务并发的工作机制,能进一步提高应用程序响应,改善程序结构,提高多CPU效率。
2 Linux内核的技术特点
1)Linux采用分页式内存管理,对于基本物理页面的管理采用Buddy算法,对于任意长度的内核数据结构采用动态分配,使用Slab算法;
2)Linux引入了虚拟文件系统层作为物理文件系统的接口,支持文件延迟写、顺序访问、预读和内存影射文件,使用buffer cache和page cache分别以盘块和页面为单位的两类磁盘访问数据;
3)Linux支持虚拟内存的使用,共享和私有页面可以交换执行;
4)Linux在进程管理中内使用的核设计原则是不可重入式的。进程在内核态运行时,仅在时间片用完、请求数据或等待I/O完成时触发调度,需要的自旋锁和信号量很少。
3 Linux内核多线程的实现
3.1 Linux内核为多线程技术的实现提供的支持
多线程的实现需要操作系统的有效识别和支持,Linux2.5系列的内核针对多线程引起的调度问题专门进行了设计和优化。
首先必须对Linux内核进行正确的设置,才能真正达到多线程处理的效果,在配置时应将对称多处理器SMP勾选上,并在BOOT中将通过引导选项acpismp=force 来指定使用超线程。支持多线程的Linux对于信息的处理速度是实际物理处理器的一倍。
Linux内核对多线程处理器提供的支持如下:
启动时Linux将自动检测是否是多线程处理器,如是,启动逻辑处理器。
可重用旋转等待(spin-wait)的优化和同步变量的对齐。当使用多线程系统的时候,为了取得良好的性能,首先是对spin-wait loops的方式进行编程,第二个是同步变量的对齐。
进程用户栈和内核栈的切换。进程因为中断或者系统调用而陷入内核态时,进程所使用的堆栈也要从用户栈转到内核栈。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态时,在内核态的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
3.2调度器功能的优化
Linux2.6版本从调度系统的应用上,其设计理念是满足实时性的要求和多处理机并行执行。因而新调度系统兼具有2.4版本交互式作业优先、高 CPU 使用率、轻载条件下调度/唤醒的高性能、基于优先级调度、公平共享、SMP 高效亲和、实时调度和 cpu 绑定等调度手段等优势,并具有其自身的特点:如可扩展性高、更好的实时性能、O1调度算法、子进程在父进程之前运行、新的SMP亲和方案等等。在诸多方面进行了进一步的改善。
3.3 Linux中的线程实现机制
Linux内核仍然使用进程模式,真正线程的实现是在用户空间中,Linux内核只是为线程的创建提供了一个进程的模型。Linux为用户线程提供了可用的线程库,本文以LinuxThreads线程库为例进行阐述。
Linux内核中的进程控制块是task_struct数据结构,它记录着进程所占用的系统资源。
在LinuxTreads线程库中,Linux定义了struct_pthread_descr_struc用来对线程的数据结构进行描述,并定义了一个全局数组变量,名为_pthread_handles,用来对进程所属线程进行描述和引用。此外,还定义了两个全局系统线程,名为_pthread_initial_thread和Pthread_manager_thread,位于_pthread_handles的前两位。
创建线程是,虽然线程库产生的仍然是独立的进程,但是因为是通过克隆系统调用clone(),而不是fork()产生的,因此产生的线程具有轻负载的特点。
用_pthread_main_thread表征_pthread_manager_thread的父线程,所有克隆的线程,预期父进程共享资源。struct _pthread_descr_struct属于双环链表结构,_pthread_manager_thread所在的链表仅包括它一个元素,实际上,_pthread_manager_thread是一个特殊线程, errno、p_pid、p_priority三个域在LinuxThreads中被使用。进程中用户线程均在_pthread_main_thread中被连接了起来。
经过一系列pthread_create()之后形成的_pthread_handles数组将如下所示。创建的线程先在_pthread_handles中出现,然后连入以_pthread_main_thread为首指针的链表中(如图所示)。
4 Linux多线程互斥问题
各线程之间可以共享系统的软硬件资源,但有些资源一次只能为一个线程使用,例如打印机、变量等,这类资源称为临界资源。对于这些资源的调用,被安排在某一代码段中,此代码段可能被反复执行,若有两个或两个以上这样的代码段要竞争共享资源,此代码段即为临界区。线程之间的互斥可以保证资源的正确和完整性。线程之间通过互斥关系相互竞争,但这与其本身无关。
Linux解决内核态中互斥问题,有四种机制:屏蔽中断、原子操作、信号量和自旋锁。原子操作和中断屏蔽是硬件在处理互斥问题中最常用的做法。原子操作和等待队列可以看做是信号量互斥解法的基础层面,此外,信号量的实现又是以自旋锁作为基础。
4.1屏蔽中断
cpu在必要的时候可以进行屏蔽中断。通过中断处理程序完成的称谓软中断。中断属于异步事件,发生的先后与正在运行中的进程无关。中断的事件长短很重要,过长的中断很容易造成数据的丢失。
因此,在中断屏蔽后,要求当前的内核应尽快执行完临界区代码。但如果开中断,又容易造成互斥,Linux在解决这项矛盾时,采用的是分段处理的方法。将关键性操作放在前段完成,后段可以稍后执行,或可以把多次后段执行部分中的相关部分合并执行。
对当前正在运行的进程进行中断,不是随机发生的,要满足严格的规则:
1)正在内核态中被执行的进程代码是不能被中断的,也就是只有在用户态中被执行的 进程代码才能被中断;
2)对于当前进程引起的异常处理代码,不能影响中断处理程序的执行;
3)中断处理程序可以中断任何一个进程,也可以中断异常处理代码的执行。同时也可以若干中断处理程序交错执行。
4.2原子操作
原子操作是一条汇编语言指令,其大部分均属于芯片级。是指那些在执行的过程中不会被其他代码中断的操作。
Linux内核实现原子操作的函数很多,分为正对Bit变量和整数变量两类,不论是哪类,在被内核代码调用后,均不会被中断处理代码中断。这些函数的操作形式均与所属Cpu构架相关。原子操作包括读、写、加、减的功能等一系列操作。
4.3信号量
Linux为了保护临界区,设置了若干信号量,只有得到信息的进程才能执行代码,使用临界资源,否则将进入等待队列。
信号量的结构被定义在semaphore.h中
Struct semaphore {atomic_t Count;
Int sleepers;
Wait_queue_head_t wait;}
核内的信息量有down()和up(),分别表示申请资源和释放资源。
1)count 计数器。用以记录资源的个数。当为正数时,表示有可用资源,初始值为1;当为0或负数时,表示无可用资源,并有进程处于等待状态。当资源被使用时,count-1;当释放资源后,count+1;
2)Wakeup() 正在等待资源的进程处于休眠状态,当资源被释放,Count为1时,linux内核会选在一个进程进行唤醒并使用此资源;
3)Wait_queue 是Linux内核的一个基本功能单位。当没有可用资源,进城被挂起,并进入到队列中。
信号量的操作过程:Linux把count的初始值设为1,Linux选择一个进程使用此资源,count作减一操作count=0,表示没有资源可用,此时所有请求使用资源的进程进入到等待队列,进入到休眠状态,当有资源被释放时,count做加一的操作count=1,wakeup函数将进行唤醒操作,被唤醒的进程使用资源。
4.4自旋锁
自旋锁(spin lock,当临界代码较短时,使用自旋锁是非常方便的,因为它可以节省上下文切换的事件。当加锁时,若已处于锁定状态,应用一个死循环测试锁的状态,直到成功取得锁。
自旋锁有两个基本组成部分:spin_lock_string,用于对锁进行减1操作,并循环检查锁值,直到大于0;spin_unlock_ string,用于对锁负值为1。当有进程在一个cpu上运行时,自旋锁被锁定。spin_lock的值减1,当另一个cpu要运行此进程时,会发现锁已经被锁上,只有当锁解开,值大于0时,才能被另一个cpu使用。
特别需注意的是,自旋锁非常容易出现死锁的情况。如果发生,整个系统就会被挂起。
因此锁定的事件要尽可能短;不要在锁定时,调用如访问用户内存、内核态分配内核、使用信号量等易引起休眠的操作。
参考文献
[1]陈李君.Linux 操作系统内核分析[M].北京:人民邮电出版社,2000.
[2]Linux Torva lds Linux Kernel Source Code2.4.2[Z].
[3]李善平.Linux内核2.4版源代码分析大全[M]。机械工业出版社,2002.