分类: 嵌入式
2014-06-06 08:44:46
原文地址:VxWorks之认识 作者:qingfenghao
Tornado是WindRiver公司开发的用于嵌入式开发的一组产品,它包括32位实时操作系统VxWorks,集成开发环境(IDE,包括编译器等)。下面是我阅读完VxWorks相关文档后的一些个人认识。
一. VxWorks操作系统
VxWorks时实时多任务的嵌入式操作系统,它主要包括任务调度、I/O功能、文件系统、中断管理、内存管理、网络功能、内存管理、BSP(系统启动模块)等。它的多任务的实现是由中断驱动的,即在每个系统时钟中断中,实现任务的调度。VxWorks中的任务有优先级的概念。与其它嵌入式操作系统相比,它有如下优点:
1. 任务之间的切换快,任务间通信手段多样;
2. 中断响应的延时短;
3. 内存管理安全:VxWorks把内存分成很多区域,包括内核区、用户区,并且采用虚拟内存管理的方法,这样大大提高了系统的安全性,并且增加了堆栈溢出的判断;
4. I/O功能丰富,硬件驱动全面;
5. 文件系统强大;
6. 网络功能全面;
7. 对任务的实时监控;
8. BSP(启动模块)的支持;
9. 多CPU的支持;
10.系统各模块是单独的库,可以根据需要加载。
下面就详细讲述各部分内容。
1. 多任务功能:
VxWorks的任务有优先级的概念,其任务调度也是基于优先级考虑的,是抢占式的,VxWorks的任务有256个等级,0—255,数目越小表示优先级越高。高优先级的任务可以打断低优先级的任务而抢先执行,只有在高优先级的任务执行完后,低优先级的任务才可以执行。其调度算法有两种:完全抢占式的和循环分配式的。完全抢占式的是除了高优先级任务可以打断低优先级任务外,在相同优先级的任务之间,不可以相互打断,并且同优先级任务不是同时执行的,只有等该任务执行完后,与其相同优先级的任务才可以执行;循环分配式除了具有抢占功能之外,相同优先级的任务是可以同时执行的,即系统时间片是在它们之间平均分配的,这样,相同优先级的任务可以同时执行。可以调用kernelTimeSlice()函数来设定该调度方式,并且参数是相同优先级任务执行的时间片。
另外任务在执行的过程中,可以调用taskLock()函数来中止任务调度,从而保证该任务的执行不会被其它任务所打断,只有在调用taskUnlock()后才会恢复任务调度。需要注意的是,如果调用taskLock()的任务在调用taskUnlock()之前又被挂起了(可能堵塞在信号量上等),那么系统就会搜索其它任务中优先级最高的那一个,并执行它,而在调用taskLock()的任务恢复执行后,任务调度又会被中止。
VxWorks中任务具有很多种状态,如下表所示:
状态 |
描述 |
READY |
此任务状态在等待执行 |
PEND |
此任务状态是由于一些资源不可用而被阻塞 |
DELAY |
此任务状态是休眠一段时间 |
SUSPEND |
此任务状态是挂起 |
BREAK |
此任务状态是停止(暂停),通常是在任务中设置了断点。 |
各种状态的组合 |
详细请见原文档 |
为了防止任务被误删除,VxWorks还提供了taskSafe()函数和taskUnsafe()函数,调用taskSafe()的任务不能被删除,应用在其它地方调用taskDelete()函数时会出错。只有该任务在调用了taskUnsafe()后,任务才能被删除。另外,VxWorks还可以对任务的优先级动态的修改,即可以在执行过程中,调用taskPrioritySet()函数对任务的优先级进行修改。对于任务的创建,VxWorks还提供了不同的方法,用户可以调用taskSpawn()函数在创建完任务后立即执行它,或者调用taskCreate()和taskActivate()函数把创建任务和执行任务分成两步来实行,即在调用taskCreate()后,任务只是被创建而没有执行,所以用户可以在需要的时候调用taskActivate()来恢复任务的执行。下面是VxWorks任务相关函数一览表:
函数名 |
功能 |
kernelTimeSlice( ) |
控制轮询式调度程序 |
taskLock( ) |
取消任务的再调度 |
taskUnlock( ) |
允许任务的再调度 |
taskSpawn( ) |
生成(创建和激活)一个新任务 |
taskCreate( ) |
创建一个新任务,但不激活它。 |
taskActivate( ) |
激活一个已经创建的任务 |
taskSuspend( ) |
挂起一个任务 |
taskResume( ) |
恢复挂起任务的执行 |
taskRestart( ) |
重新开始一个任务的执行(即从头执行) |
taskDelay( ) |
延时任务,延时单位是时间片 |
taskIdSelf( ) |
得到调用任务的id(正在运行的) |
taskIdVerify( ) |
验证一个指定任务是否存在 |
taskOptionsGet( ) |
获得用户自定义任务参数 |
taskOptionsSet( ) |
设置用户自定义任务参数 |
taskIdListGet( ) |
将所有活动状态的任务id填写到个数组中 |
taskInfoGet( ) |
得到一个任务的信息 |
taskPriorityGet( ) |
获得任务的优先级 |
taskPrioritySet( ) |
改变任务优先级 |
taskRegsSet( ) |
设置一个任务的寄存器(但是不能被当前任务使用) |
taskIsSuspended( ) |
检查一个任务是否在悬挂状态(suspended.) |
taskIsReady( ) |
检查一个任务是否准备运行就绪 |
exit( ) |
结束正在运行任务,释放内存* |
taskDelete( ) |
结束制定的任务,释放内存* |
taskSafe() |
保护当前任务,防止被删除 |
taskUnsafe( ) |
取消taskSafe( )操作,即能够删除当前任务 |
nanosleep( ) |
延时任务,延时单位是时间片 |
另外除了用户创建的任务外,VxWorks本身也创建了很多系统任务。包括BSP(系统启动)任务、异常处理任务、系统登记任务、Telnet任务、代理任务、RPC(远程进程调用)任务等。
其中BSP任务在系统启动完成后就自动被删除;异常处理任务是当系统因非法指令、总线错误等而产生异常时,系统会调用该任务来处理,系统处理是挂起发生异常的任务,保存其运行状态,而其它任务的执行状态不变,用户也可以自定义异常处理函数,方法是使用信号处理功能,这样发生异常时,系统会调用用户定义的信号处理函数来处理;系统登记任务是系统用来实现内部消息在当前运行任务上下文中的登记的,它使得消息的登记不用通过I/O来实现,从而提高了效率;telnet任务是用来实现远程机器登录的,它检测用户名、密码等,在用户登录完成后,该任务即被删除;代理任务是用来实现本机和目标服务器通信使用的,当目标服务器需要操纵本机(发送、获取信息等)是通过该任务来实现的,当目标服务其需要数据时,它会将请求发送给该任务来获取数据;RPC任务是用来处理远程机器对本机(服务器)进程调用的,远程机器通过后台程序来发送调用请求,而RPC任务就是处理该请求。
与Windows下类似,VxWorks就任务模块为开发者提供了钩子(hook)功能,开发者可以在任务的创建、删除、或者任务切换时设定钩子函数,这样在每个任务创建/删除时,系统都会调用用户设定的钩子函数,从而用户可以监测系统中每个任务的创建、删除等。
另外,用户可以调用exit()、return;来退出本任务的执行,可以调用taskDelete()来删除其它任务从而中止其执行。值得注意的是,在调用exit()退出本任务的执行时,本任务所占用的内存并不会被释放,更为严重的是,如果本任务正在使用信号量等资源,那么其它想获得该信号量的任务将永远不会得到它,从而可能会永远挂起。任务在执行过程中,如果为了不被中断所打断,可以调用intLock()来屏蔽所有中断,而调用intUnlock()将解除屏蔽。
VxWorks为任务间的通信和同步提供了丰富的手段,包括信号量、消息队列、管道、信号等。
VxWorks信号量是分为三种:
(1) 二进制形式的:最快的也是最简单的信号量,用于任务间同步;
(2) 公用体互斥的:一种特定的二进制信号量,用于公用体互斥,并且具有优先级继承的功能,删除安全。
(3) 可计数的信号量:与二进制形式的类似,所不同的就是同时有多个信号量用于同步。
一个信号量有两种状态,满的(可获取)和空的(不可获取),用户可以调用semTake()
来获取一个信号量,如果该信号量当前状态是满的,则函数返回成功,否则该任务挂起,挂起时间由用户指定,可以永远挂起直到获得该信号量。因此任务在使用完信号量后,一定要调用semGive()释放,否则其它任务将无法获得该信号量。
可以有多个任务同时挂起在同一个信号量。在出现这种情况时,如果拥有该信号量的任务释放了它,那么下一个获得该信号量的任务由两种规则决定:FIFO(先进先出)和按优先级顺序。如果是按FIFO规则,那么第一个等待该信号量的任务将获得该信号量而执行;如果是按优先级顺序,那么挂起在该信号量上的优先级最高的任务将获得该信号量而执行。
任务如果在指定时间内没有获得指定的信号量,那么任务将继续执行,而semTake()函数将返回超时错误。
需要注意的是,有可能会出现下面的非正常情况:
Task3 |
Task3 |
Task3 |
Task1
Task1 |
Task1 |
Task2
Task2
等待信号量1 释放信号量1
pos5 pos2 pos1 pos3 pos4 时间
如上图所示,其中优先级顺序是,task3 > task2 > task1,task1获得了信号量1,而task3调用semTake()想获得该信号量,挂起;正常情况下,task1在虚线框pos1处释放该信号量,从而task3也可执行,但在task1执行过程中,被优先级更高的task2在pos2处打断,而task2在pos3处退出,从而task1在pos4处才能释放该信号量,而task1也只有在pos4处才可以执行。但这是不正常的,因为task1的优先级比task2高,而task1却由于task2的执行而被推迟执行,这样就出现了“优先级倒置”的情况,公用体互斥信号量就是为了解决此问题的。
在当前任务获得某公用体互斥信号量时,如果有其它任务也想获得该信号量从而挂起,并且它们有些优先级是比当前任务要高的,那么当前任务的优先级将变成跟其中优先级最高的任务一样。这样就防止被其它低优先级任务(但高于当前任务)所打断 。如下图所示:
Task3
Task1
Task1
Task3
Task1
优先级
Task2 |
Task2 |
优先级升高 优先级恢复
Task1
pos5 pos1
时间
在pos5处task1优先级跃迁成与task3一样,在pos1处task1释放完信号量1后,
其优先级又恢复正常,task2没有得到机会执行。
计数信号量其实就是多个二进制信号量,即该信号量的个数不止一个,可以有多个
任务同时获得它。比如创建时指定的计数器是3,那么在任务调用semTake()函数时,系统会判断当前信号量个数,如果是>0的,那么就可以获得,否则挂起。只要当前有不超过3个任务获得该信号量,那么semTake()函数都是成功的。
VxWorks提供了 semFlush()函数,可以释放指定的信号量,并且挂起在该信号量上的所有任务全部恢复正常执行。
需要注意的是,semGive()函数可以在中断函数中调用,但是semTake()函数则不可以,因为semTake()函数可以挂起,而中断是不能被挂起的。
VxWorks的消息队列为任务间的数据传输同样提供了便捷。VxWorks消息队列是FIFO的,并且消息大小和队列总长度可以由用户在创建消息队列时指定。用户调用 msgQSend()函数向指定消息队列中发送消息,如果当前消息队列没满,那么消息被放进队列中,函数成功返回;否则根据用户传进的参数,调用msgQSend()的任务可能被挂起、立即返回或延时一段时间后返回。类似的,用户调用msgQReceive()函数从指定队列获得消息,如果当前消息队列不是空,那么系统把消息拷进用户指定的buffer中,成功返回;否则根据用户传进的参数,调用msgQReceive()的任务可能被挂起、立即返回或延时一段时间后返回。
同样,对于挂起在消息队列的任务,可以有两种方式决定首先恢复哪一个执行:FIFO和按优先级顺序。
需要注意的是,msgQSend()函数可以在中断函数中调用,但是msgQReceive()函数则不可以,因为msgQReceive()函数可以挂起,而中断是不能被挂起的。
VxWorks中的管道类似于消息队列,也是先进先出的。所不同的就是任务可以同时从多个管道中发送数据(或从多个管道中获得数据),从而挂起在多个管道上,而其中任何一个管道如果满足了任务的要求,那么任务就会恢复执行。
VxWorks中信号一般被用来作为异常处理,用户可以把自己的处理函数通过调用sigvec()连接在特定的信号上,所以信号与中断是类似的,例如总线错、非法指令等都会置上一个特定的信号。
2.VxWorks的中断管理
VxWorks的中断延时短,可以满足实时的要求,而用户可以用C语言编写中断服务程序,而无需掌握汇编。用户只用调用intConnect()函数即可把自己的中断服务函数和特定的中断挂接起来。事实上,VxWorks并不会把用户的中断函数地址直接填到指定的中断向量处,而是在VxWorks本身的中断服务函数中,调用用户的中断函数。
需要注意的是,用户中断函数所使用的栈和应用任务的栈并不相同,而是由用户指定的另外一段空间,这样在一定程度上保证了系统的安全性。
用户可以调用intLock()屏蔽所有中断,从而保证当前的中断服务程序不被打断。调用intUnlock()解除中断。
3. VxWorks的内存管理
VxWorks中有虚拟内存的概念,内存是分页管理的。即用户所操作的内存地址实际上是虚存,实际物理内存对于用户而言是不可见的。 在VxWorks中,内存被分成许多域,有内核域、应用域。内核域用来执行操作系统内核代码和存放操作系统内核数据(如信号量、任务控制结构等),应用域用来 执行用户任务代码和存放用户任务数据。各个域之间的数据是不可见的,除非有共享数据区,并且对用户而言,各个域的内存地址分布有可能是一样的,逻辑上是重叠的,但是对应到物理地址,是不一样的。
每个应用域都有自己的堆和栈,而且都有内核域在本域中的映像。在一个应用域中,可以创建多个任务,它们共用该应用域的内存空间,它们的堆和栈都是在本应用域中的。简单地说,应用域类似于其它操作系统中的进程,而任务则类似于线程,任务是VxWorks中调度的最小单元。
每个应用域都有自己的虚拟地址<—>物理地址的映射表,用户的任何内存操作都要通过该表转换到物理内存地址 后执 行。而用户可以设定某些内存页面的属性为只读、读写等。例如中断向量表区、程序区等应该设成只读属性。如果程序中有对只读内存进行写入操作时,那么系统会抛出异常。
系统内存和I/O地址资源的分配在00region.sdf中定义,但不管怎样,都要包含一个kernelMemPool的内存域,这是内核域。
VxWorks支持标准内存分配接口:malloc(),free()和realloc(),并且分配的内存只是在本应用域中。应用域的内存空间可以根据需要增大或者缩小,这实现用户可以手动完成也可以由系统自动完成。需要注意的是,内存相关函数不可以在中断中调用,因为其中有信号量的操作。
另外,各个应用域可以有共享的数据区域,其大小可由用户指定,这样,这些应用域中的所有任务都可以对该共享数据区域进行读写操作。各个应用域还可以有共享代码库,这样这些应用域中的所有任务都可以调用库中代码,但库中的数据对各应用域是不可见的。应用域调用Attach()/Deattach()来把自己和某个共享代码库连接/断开。在一个共享代码库可以从内存中删除前,必须保证其没有被任何一个任务所使用。
应用域可以被动态的创建、卸载。当一个应用域被删除时,其中所有任务都停止运行,并从内存 中删除;而如果其中所有的任务都被删除后,该应用域将被自动从内存中删除。
VxWorks提供了API接口来察看某个应用域中内存的使用状况,如memShow()。
应用域中的任务在执行时可能有两种状态,用户态 和超级用户态,用户态只能操作本应用域中的内存,而超级用户态可以操作包括内核域在内的所有域中的数据。任务在调用系统API函数时,将自动变成超级用户态,而在调用完成后,将恢复为用户态。创建任务时,系统默认任务状态是用户态,但用户也可以手动设置某任务的状态为超级用户态,但前提是该任务所在的应用域必须是特权态(privilege)。
4.I/O功能及硬件驱动
VxWorks提供了丰富的I/O功能(包括低层硬件驱动)供使用,包括:硬盘驱动,键盘驱动,网卡驱动,显卡驱动,软盘驱动和并行口驱动等。并且驱动程序还可以由开发者自己动态添加、删除而不用重新启动系统。用户可调用iosDrvInstall()函数来添加自己的硬件驱动,从而系统在操作相应设备时,会调用用户的驱动来实现。
VxWorks提供的I/O接口统一简单,如下表所示:
函数名 |
功能 |
create() |
创建设备 |
remove() |
删除设备 |
open() |
打开设备 |
close() |
关闭设备 |
read() |
从设备中读取数据 |
write() |
向设备中写入数据 |
ioctl() |
控制设备(例如设置波特率等) |
另外,VxWorks还可以在内存中创建虚拟磁盘,方法是调用ramDevCreate()函数。可以指定内存的起使位置和大小、扇区大小等。
VxWorks还提供了对网络文件系统设备的支持,用户可以调用nfsMount()函数来打
开远程机器上的某一文件,在调用标准I/O接口对其进行读写操作。
VxWorks还支持标准的I/O函数,printf和sprinf等
VxWorks把所有的I/O设备都看成是文件,在用户打开一个设备时,如果成功函数会返回其ID,用户以后对该设备的读、写等操作都通过该ID来实现。该ID对于其它所有的任务都是可见的,其它任务也可以通过该ID对该设备进行操作。
注意:ID值0、1、2是系统保留的,分别用来表示标准输入、标准输出和标准错误输出设备,当然用户可以指定自己的标准输入、输出和出错输出设备。
《返璞归真--UNIX技术内幕》在全国各大书店及网城均有销售: