0引言: QNX是由加拿大QNX软件有限系统公司开发的一种多任务、分布式、可嵌入的实时操作系统。它有着轻巧的微内核,可以对进程进行全面的地址保护,可剪裁,模块化程度高,实时性强,安全可靠。符合POSIX标准的API使它成为一个开放式互联系统,便于与UNIX/LINUX系统的移植。QNX有着不同于 UNIX或LINUX的模块化设计思想,并不是UNIX或LINUX的一种演化,而是完全不同的一种全新的实时操作系统。由于其独特的体系结构,QNX广泛应用于嵌入式系统、机器人工程、工业控制、航空航天等各个领域。 在QNX中,线程是一个单一的控制执行流。从程序的最低层角度考虑,线程包括当前指令位置指针(也称为计数器或PC)、栈顶指针(SP)和一些寄存器,而进程占据一定的内存空间,是一个或多个线程的集合。在同一进程中的线程共享许多资源,在QNX系统中共享的资源有:内存中储存在栈区以外的变量——即非局部变量;信号处理器;信号忽略屏蔽字;通道——建立于服务器端;连接——建立于客户端,而在不同进程中的线程除了CPU之外,几乎不共享任何资源。当然 QNX提供了shm_open()函数来使不同进程中的线程共享一段内存。 在早期的QNX版本如QNX4中,对于线程的支持是比较弱的,在当时的条件下,处理大型、复杂的并发多任务问题时,常常将问题分解为多个进程以降低问题的复杂性。而且QNX提供了与UNIX类似的进程间通讯IPC手段如消息、代理、信号灯等,功能也相对比较成熟、完善。1999年以后 QNX软件公司推出了QNX/Neutrino实时操作系统的Neutrino2.0、Neutrino6.0增加了对于POSIX线程的支持,标准的 API不但使它易于扩展,而且也使得编写多线程程序变得容易。由于线程具上下文较轻、切换较快、在创建多个线程时系统的开销比较小、通讯手段灵活多样、共享资源丰富等优点,在处理大型并发多任务问题时多线程有了明显的优势。QNX是抢先式多任务系统,这种系统决定了多个线程在访问共享资源时线程执行的次序变得不可预期,所以线程间的同步就显得极为重要。QNX提供了多种同步机制以保证多线程程序的安全、可靠。 1 QNX多线程库函数简介 QNX与LINUX不同,没有单独的线程库,与线程有关的API是作为C语言库函数的一部分使用的,头文件是,同样方便地提供线程的创建、终止和同步等功能。QNX不仅在C语言库函数中提供了符合POSIX1003.1c标准的与线程相关的API,而且还提供了很多POSIX标准没有的扩展功能,使得多线程编程变得更加容易。 1.1线程的创建、取消和终止 1.11线程的创建 QNX通过pthread_create()函数创建线程,API定义如下: int pthread_create( pthread t* thread, const pthread attr t* attr, void* (*start routine)(void* ), void* arg ); pthread_create()创建的线程执行start routine() 函数,thread返回创建的线程描述符,而attr是创建线程时设置的线程属性,arg可以作为任意类型的参数传给start routine()函数。QNX对创建线程前需要设置的线程属性进行了扩展,增加了POSIX标准无法设置的属性如:可以禁止一个线程的取消(终止操作);可以设置一个线程的取消类型;可以指定当一个线程接到信号时,它如何操作。 1.12 线程的取消 QNX通过调用int pthread_cancel(pthread_t thread)函数取消由thread指定的线程,如果成功则返回0,否则为非0,成功并不意味着thread会终止,要视取消的状态和类型而定。QNX 提供了设定取消状态和类型的API pthread_setcancelstate()和pthread_setcanceltype(),取消的状态有两种:PTHREAD_CANCEL_ENABLE表示将线程设为取消状态,PTHREAD_CANCEL_DISABLE表示忽略取消状态;取消的类型也有两种: THREAD_CANCEL_DEFFERED表示执行到取消点取消,PTHREAD_CANCEL_ASYCHRONOUS表示立即取消。 1.13 线程的终止 QNX中终止一个线程需要调用pthread exit(),其API定义: void pthread exit( void* value ptr ); 当一个线程在执行了start routine()函数后返回时,系统自动隐式调用pthread exit()使其退出,start routine()的返回值,作为线程的退出状态。在一个线程中也可以显式调用pthread exit()退出,对于单线程进程而言,调用pthread exit()与调用exit(0)是等效的。 1.2 线程的常用控制函数 pthread_self() API: pthread_t pthread_self(void); 说明:返回线程描述符,pthread_create()返回值相同。 pthread_equal() API: int pthread_equal(pthread_t t1,pthread_t t2); 说明:t1,t2为线程描述符,可调用pthread_self()和pthread_create()得到。此函数功能为比较两个线程的描述符,不管线程描述符是否合法。如果返回值为非零说明两个线程是同一线程,为零说明两个线程不是同一线程。 pthread_join() API: int pthread_join(pthread_t thread, void** value_ptr); 说明:thread 为等待终止的目标线程,value_ptr为一指针,当值不为NULL时指向一个内存空间,这个空间用来存放目标线程传给pthread_exit()的数据。调用pthread_join()的线程将被挂起,直到目标线程终止。一个线程仅允许唯一的线程使用pthread_join()等待它的终止,并且被等待的线程应该处于非DETACHED状态。QNX也提供了非POSIX的 pthread_timedjoin(),不同之处是线程在给定时间里没有被join时,此函数会返回一个错误信息。 pthread_detach() API: int pthread_detach(pthread_t thread); 说明:此函数功能是将一给定线程thread分离,当一个出于分离状态的线程终止时,线程拥有的所有系统资源将被立即释放。 2 QNX线程的互斥和同步机制 线程间的互斥操作是指对于特定的一段代码或一个变量,在程序运行时只能有一个线程对其进行操作,其他线程不能同时进入代码或修改变量。线程间的同步操作是指若干个线程都等待某个事件的发生,当这个事件发生时,所有的线程同时进行下一步工作。为了防止竞争条件和数据被破坏的情况发生,QNX提供了多种互斥和同步机制,包括互斥体、条件变量、信号灯、屏障、读/写锁、sleepon锁等,其中最主要的是互斥体和条件变量,其余的同步机制都是由他们组合而成的,当然你也可以根据自己的要求构建自己的同步机制。 互斥体——QNX使用了互斥体来实现互斥操作,在初始化互斥体后,将给定的代码或变量的前后进行加锁、解锁操作,线程在访问之前要先得到互斥体,这样就可以保证只有一个线程能访问到代码或变量,而其余的线程会处于阻塞状态直到互斥体被释放。这种机制保证了线程对资源访问的互斥性,达到了对代码或变量的保护。 条件变量——QNX的条件变量用来同步线程,所有线程都会等待一个条件变量可用,当条件满足时,一个线程发出广播或信号来同步所有的线程或某一线程。为了防止多个线程同时申请等待而产生竞争,条件变量通常要与互斥体联合使用。 信号灯——QNX信号灯也是一种符合POSIX标准的的同步机制,它是由互斥体和条件变量结合一些数据构造而成的,QNX系统将其封装在C语言库函数中,头文件是。它的功能很强大,可以允许多个线程访问同一资源,可以通过设定灯值来限定线程的个数,灯值为一时它就是互斥体。 屏障——POSIX 1003.1j提议的内容,主要由互斥体、条件变量和计数器构造而成。作用是停止某些线程,当所要求的线程数量到达屏障时,所有的线程被允许继续运行。屏障通常被用来确保某些并行算法中的所有合作线程在任何线程可以继续运行以前到达算法中的一个特定点。 读/写锁——POSIX 1003.1j提议的内容,主要由一个互斥体和两个条件变量构造,两个条件变量分别控制读写操作。读/写锁允许多个线程同时读数据,但是禁止任何线程修改正在被其他线程读或修改的数据,也可以让一个线程独占写访问,但此时任何读访问的线程都不能继续,直到读/写锁被释放,所以读/写锁被用来保护经常需要读但是通常不需要修改的信息。 Sleepon锁——QNX6所独有的一种同步机制,由一个互斥体和一些数据构造而成,与条件变量相似但是用法比较简单。它与条件变量的不同在于当有N个线程阻塞在M个对象的时候,如果用条件变量来同步时需要用到M个条件变量,而sleepon锁为每个线程自动分配条件变量,只需要N个条件变量,使用合适可以节约系统资源。 3 多线程程序的设计分析与基本结构 3.1多线程程序的设计分析 1)确定完成任务所需的最少线程个数。 多余的线程只会使程序的复杂性增加,出错的可能性也随之增加。设计程序时要遵循简单、高效、安全的原则,如果用单线程能够很好的完成任务,那么一定不要用多线程。 2)分析多线程需要共享的数据。 在多线程程序中常常需要共享一些数据,通常是一些全局变量,如果数据量很大可能需要开辟共享内存区。 3) 根据共享数据的特点选择需要的保护机制。 多个线程需要写操作的变量可以用互斥体保护,经常需要读操作而很少进行写操作的可以用读/写锁保护等。 4) 分析工作线程需要访问资源。 工作线程可能需要访问硬件,等待硬件响应、可能需要访问某一数据库、也可能不访问任何资源只是进行一些计算等等。这时需要考虑相应的同步机制,可以用条件变量结合互斥体,也可以用更为简单的sleepon锁。 5)进行线程的清理工作。 线程完成工作后可能会自动退出,也可能会阻塞在某处,甚至工作线程还没完成工作的时候主线程已经退出,造成整个进程的结束,使程序失败。有多种方法可以完成线程的清理工作,可以让主线程调用pthread_join()函数清理工作线程,可以用屏障同步机制清理,也可以用条件变量来完成清理工作。 3.2多线程程序的基本结构 多线程编程的结构有很多种,但基本的编程结构总结起来有三种:流水线结构、工作组结构、客户端/服务器结构。这三种结构可以以任意方式组合,来满足实际工程的需求。 1) 流水线结构 在流水线结构中,需要处理的“数据”串行地被一组线程顺序处理,每个线程依次在每个数据元素上执行一个特定的操作,完成操作后将结果传递给流水线中的下一个线程。如图1所示。
2) 工作组结构 在工作组结构中,数据有一组线程分别独立地处理,每个线程处理不同的部分。由于所有的工作线程在不同的数据部分上执行相同的操作,这种模式通常被称为 SIMD(单指令多数据流)并行处理。但是工作组中的线程可以不使用SIMD模型,他们可以在不同的数据上执行不同的操作。工作组结构是多线程程序应用较多的一种结构。如图2所示
图2 工作组结构 3) 客户端/服务器结构 在客户/服务器结构中,客户请求服务器对一组数据执行某个操作。客户端独立地执行操作,而客户端或者等待服务器执行的结果,或者并行执行另外的任务,在后面需要时在查找结果。这种结构又是一种对某些公共资源同步管理的简单方式。如图3
4 QNX Neutrino内核对于线程功能的扩展 具有Neutrino内核的QNX6操作系统对线程的功能进行了扩展,提供了一些POSIX标准没有提供的功能。 1)POSIX标准规定使用互斥体的线程必须在同一进程内,作为扩展QNX支持在不同进程中的线程使用互斥体。如果在两个进程间创建一块共享内存,并在内存中初始化一个互斥体,那么两个进程之间的线程可以用这个互斥体来进行同步操作,这是POSIX做不到的。 2) QNX操作系统还提出了一种独特的“线程池”概念。当程序需要很多线程同时工作时,利用“线程池”可以将线程的个数限定在一定的范围内。“高水位”、“低水位”的概念分别对应着程序中的最大线程数和最小线程数。当程序中线程数目小于“低水位”时,“线程池”会自动创建新的线程进行工作,当线程数目大于“高水位”时,“线程池”会“清除”多于的线程,以防止溢出。这样程序将始终保持着一定数量的线程在工作,“线程池”特别适用客户端/服务器结构,可以很好地保护服务器的资源。QNX提供了专门的程序库来管理“线程池”头文件是,相应的API主要有:thread_pool_create(),用于建立一个线程池,thread_pool_destroy()程序运行结束后用它来清除线程池,thread_pool_start()用来启动一个线程池。 5 QNX系统下实用编译方法 笔者编制了QNX环境下的通用Makefile,用于编译多线程程序,当然也适用于单线程程序的编译,而这个Makefile稍加改动便可以用于 LINUX/UNIX系统中,笔者在RedHat Linux7下试验通过。用法,首先将此Makefile 和所要编译的c/c++程序(支持多个c/c++程序)/头文件放置于一个目录中,在终端上键入make,此Makefile将自动把所有相关源代码连接编译成名为go的可执行文件,要运行编译好的程序,只需在终端上键入./go便可。在终端上键入make clean将把所有的编译产生的临时文件删除,只留下原始文件和make文件,在终端上键入make depend将检查文件的依赖性,源代码如下: EXECUTABLE=go LINKCC=$(CC) #如果用于LINUX/UNIX系统,要求安装可移植线程库,并装入下列函数库 #LIBS=-lm –lpthread -lsocket #如果用于QNX系统装入下面的函数库 LIBS=-lm –lsocket #如果编译c++程序将下面gcc改为g++ CC=gcc CFLAGS=-Wall -g CXX=g++ CXXFLAGS=$(CFLAGS) SRCS:=$(wildcard *.c) $(wildcard *.cc) $(wildcard *.C) OBJS:=$(patsubst %.c,%.o,$(wildcard *.c))\ $(patsubst %.cc,%.o,$(wildcard *.cc))\ $(patsubst %.C,%.o,$(wildcard *.C)) DEPS:=$(patsubst %.o,%.d,$(OBJS)) all:$(EXECUTABLE) $(EXECUTABLE):$(DEPS) $(OBJS) $(LINKCC) $(LDFLAGS) -o $(EXECUTABLE) $(OBJS) $(LIBS) %.d:%.c $(CC) -M $(CPPFLAGS) $< >$@ $(CC) -M $(CPPFLAGS) $< |sed s/\\.o/.d/>$@ %.d:%.cc $(CXX) -M $(CPPFLAGS) $< >$@ $(CXX) -M $(CPPFLAGS) $< |sed s/\\.o/.d/>$@ %.d:%.C $(CXX) -M $(CPPFLAGS) $< >$@ $(CXX) -M $(CPPFLAGS) $< |sed s/\\.o/.d/>$@ clean: -rm $(OBJS) $(EXECUTABLE) $(DEPS) # -rm ./*.* 如果有一些临时的记录文件无法自动去掉加在这里 depend: $(DEPS) @echo "Dependencies are now up-to-date." 6 总结 QNX实时操作系统的实时性很好,上下文切换时间、中断延时都非常微小,本身提供了对于多线程技术的强大支持,如果在QNX下使用多线程编程技术来解决大型并发多任务系统的控制调度,其优势是很大的,前景也是很广阔的。