Download presentation
Presentation is loading. Please wait.
1
第三章 嵌入式Linux操作系统
2
Linux概况 现在 Linux 广泛用于各类计算应用,不仅包括 IBM 的微型 Linux 腕表、手持设备(PDA 和蜂窝电话)、因特网装置、瘦客户机、防火墙、工业机器人和电话基础设施设备,甚至还包括了基于集群的超级计算机。
3
Linux操作系统开放源代码,可以裁剪内核,并已在x86、Alpha、Sparc、MIPS、PPC、Motorola、NEC和ARM等硬件平台上稳定、高效地运行。
标准的Linux系统在网络服务器领域已经得到了广泛的使用。迄今为止,世界上已有近40%的Internet主机采用Linux作为操作系统,它们全都在长时间、大负荷地工作着。这一事实证明了Linux操作系统的稳定性、安全性和可靠性。
4
嵌入式Linux是一种开放源码、软实时、多任务的嵌入式操作系统。
5
通常它是在标准Linux的基础上针对嵌入式系统进行裁剪和优化后形成的。裁剪和优化后的Linux体积更小,性能更加稳定,而且源代码本身是免费的。这将大大减少开发商的成本,更具市场竞争力。
同时,由于遍布全球的众多Linux爱好者又能给与Linux开发者强大的技术支持,所以众多商家纷纷转向嵌入式Linux的开发。
6
3.2 嵌入式Linux 在嵌入式开发中,随着微处理器的产生,价格低廉、结构小巧的CPU和外设接口提供了稳定可靠的硬件架构。
这样,限制嵌入式系统发展的瓶颈就突出表现在软件方面。 尽管从20世纪80年代末开始,陆续出现了一些嵌入式操作系统,比较著名的有VxWorks,pSOS,Neculeus和Windows CE。这些专用操作系统都是商业化的产品,其高昂的价格使许多生产低端产品的小公司望而却步。
7
高端嵌入式系统要求许多高级的功能,如图形用户界面和网络支持。许多高端RTOS供应商已经提供了这些功能,但其价格也很高,一般人难以接受。微软的Windows CE也有此类功能,却不具备大多数嵌入式系统要求的实时性能,而且难以移植。 现在需要的是一个便宜、成熟并且提供高端嵌入式系统特性所必需的操作系统,但源代码的封闭性又限制了开发者的积极性。
8
Linux为嵌入式操作系统提供了一个极有吸引力的选择,它是一个类UNIX的、以内核为基础的、有完备内存保护机制的、多任务多进程的操作系统。
它支持广泛的计算机硬件,包括x86,Alpha,Sparc,MIPS,PPC,ARM,NEC,Motorola等现有的大部分芯片。其源码全部公开,任何人都可修改并在GNU通用公共许可证(GNU General Public License)下发行。
9
开发人员可以对操作系统进行定制,再也不必担心像Microsoft Windows操作系统中“后门”的威胁。
Linux用户遇到问题时可以通过Internet向网上成千上万的Linux开发者请教,这使最困难的问题也有办法解决。
10
Linux带有UNIX用户所熟悉的完善的开发工具,几乎所有的UNIX系统的应用软件都已移植到了Linux上。
Linux还提供了强大的网络功能,有多种可选择的窗口管理器(X Window)。其强大的语言编译器GCC,G++等也可以很容易得到。GCC,G++编译器不但成熟完善,而且使用方便。
11
由于Linux具有开放性,所以许多人认为Linux非常适合多数Intemet设备。他们认为Linux可以支持不同的设备,支持不同的配置。Linux对厂商不偏不倚而且成本极低,能够很快成为可用于各种设备的操作系统。 如今,业界已经达成共识:即嵌入式Linux是大势所趋,其巨大的市场潜力与无限商机必然会吸引众多的厂商进入这一领域。使用Linux作为一个嵌入式操作系统具有许多的优点。
12
专用嵌入式实时操作系统 vs 嵌入式Linux 操作系统
系统购买费 非常昂贵 商业版本需要一定服务费 使用费 每件产品需都需缴纳 免费 技术支持 开发商一家支持 商业版厂商和Linux社团 网络协议栈 需要额外购买 免费且性能优异 软件移植 难,因为系统封闭 易,因为代码开放 产品开发周期 长,因为参考代码有限 短,应用和参考丰富 实时性能 好 可用RTLinux等模块弥补 稳定性 较好,高性能系统待验证
13
1. 可运行于多种硬件平台 Linux符合IEEE POSIX.1标准,使应用程序具有较好的可移植性。
内核的90%以上的代码是用可移植性好的C语言完成的,少部分的底层相关的代码由汇编语言完成,并根据处理器类型分门别类地放在系统内核源码的LINUX/ARCH/……目录中。
14
目前,Linux内核支持Intel x86,Motorola/IBMPowerPC,Compaq(DEC)Alpha,IA 64,S390等处理器体系结构,并且支持嵌入式领域中广泛使用的ARM和Motorola MC68000系列。 随着Linux越来越广泛地应用于嵌入式领域,它所支持的嵌入式微处理器必定会进一步增加。
15
2. 可裁剪,性能优异,应用软件丰富 Linux的动态模块加载使Linux的裁剪极为方便,高度模块化的部件使添加非常容易。一般来说,经过适当裁剪后的Linux内核的启动部分的目标代码不到500KB。 用户完全可以把Linux内核和root文件系统存放在一张软盘上。也可以利用Linux实现从网络启动,实现网络无盘图形工作站。
16
Linux是一个全面的多任务和真正的32位操作系统。系统运行稳定,功能强大,支持多种硬件平台,应用工具多。
随着Linux的不断发展,基于Linux平台上的应用软件也不断得到扩充。许多著名的商业软件都有了Linux下的版本。例如,Star公司提供的StarOffice办公应用软件、Oracle的数据库、Netscape Navigator的网络浏览器、Apache网络服务器、Adobe Acrobat Reader等。
17
3. 使用成本低 在嵌入式领域,大型的嵌入式实时操作系统,如VxWorks及其开发工具Tornado的购买使用费是极为昂贵的。
几乎所有的商用操作系统,如Microsoft公司的Windows98/NTServer/NTWorkstation系列,都需要为每一个拷贝(或者每一个用户)支付相当数量的费用。在其下的每一个应用软件(如Office办公软件)都需要额外购买。商用的操作系统下的应用软件工具包的购置也是一笔不小的支出。 在嵌入式领域,大型的嵌入式实时操作系统,如VxWorks及其开发工具Tornado的购买使用费是极为昂贵的。
18
Linux是免费软件,只要遵守GPL(GNU General Public License)的规定,就可以免费获得拷贝,并进行开发和商业发行。
19
在全球互联成为一种不可逆转的趋势以后,具有网络互联功能的嵌入式设备需求大增。
嵌入式操作系统是否具有优异的、可裁剪的网络功能成为应用开发商选择操作系统的标准之一。 Linux的网络功能十分强大,更重要的是,Linux的网络功能和协议是以内核可选的模块方式提供的,它允许用户自由地裁剪和优化。
20
4. 强大的网络功能 Linux操作系统最突出的是网络部分,基本上所有的网络协议和网络接口都可以在Linux上找到。
Linux比标准的UNIX能更加高效地处理网络协议,系统的网络吞吐性能也非常好,这也是Linux在网络服务器市场上占据越来越大市场份额的一个原因。
21
Linux内核对网络协议栈的设计是从简捷实用的角度出发的,它有一整套的网络协议模块。
Linux不仅可以支持一般用户需求的文件传输协议FTP,TELNET和RLOGIN协议,还能提供对网络上其他机器内文件的访问(NFS,网络文件系统)。 Linux还支持SLIP(Serial Line Interface Protocol)和PLIP(Parallel Line Interface Protocol)协议,使通过串口和并口线进行连接成为可能。
22
通过AX.25协议,Linux可以提供通过无线电进行连接。
通过在Linux上开发Novell标准的IPX协议,Linux可以访问NetWare网络。如在Apple机的世界里通过AppleTalk协议可以访问Apple的网络;也可以在Windows 9x/NT局域网中通过Samba协议进行Linux和Windows之间的文件共享。 通过Apache公司开发的免费网络服务器,可以利用Linux系统作为强大的网络服务器,提供Internet上电子商务和数据服务。
23
在全球互联成为一种不可逆转的趋势以后,具有网络互联功能的嵌入式设备需求大增。
嵌入式操作系统是否具有优异的、可裁剪的网络功能成为应用开发商选择操作系统的标准之一。 Linux的网络功能十分强大,更重要的是,Linux的网络功能和协议是以内核可选的模块方式提供的,它允许用户自由地裁剪和优化。
24
例如:Zigbee技术 Zigbee是IEEE 协议的代名词。根据这个协议规定的技术是一种短距离、低功耗的无线通信技术。这一名称来源于蜜蜂的八字舞,由于蜜蜂(bee)是靠飞翔和“嗡嗡”(zig)地抖动翅膀的“舞蹈”来与同伴传递花粉所在方位信息,也就是说蜜蜂依靠这样的方式构成了群体中的通信网络。其特点是近距离、低复杂度、自组织、低功耗、低数据速率、低成本。主要适合用于自动控制和远程控制领域,可以嵌入各种设备。简而言之,ZigBee就是一种便宜的,低功耗的近距离无线组网通讯技术。
26
《基于ARM&Linux的ZigBee无线通信系统实现 》
该课题正是把以上热点结合起来,通过ARM S3C2410X和Linux构建一个ZigBee协调器的开发平台,连接几个半功能设备节点,实现ZigBee在智能家居的应用系统。在课题中对如何开发ZigBee网络的应用程序作了初步的探讨,对ARM S3C2410X硬件平台设计和嵌入式Linux操作系统软件平台设计做了相关研究;在课题中还详细研究了Linux操作系统移植到ARM平台、ZigBee数据帧结构、数据帧的收发实现。实现了协调器与节点应用程序设计,系统测试程序设计。
27
5. GUI开发支持 Linux本身有性能优秀的X Window系统,在X Window系统的支持下,能方便地进行图形用户界面的开发。
28
X Window是一个在大多数UNIX工作站上使用的图形用户界面。
它是一种与平台无关的客户机/服务器(Client/Server)模型,可以让用户在一台机器上调用另一台机器的X Window库,打开另一台机器上的窗口,而不需要考虑这两台机器自身的操作系统类型。 正是这种特性使UNIX和Linux系统上的用户和应用程序非常自然地通过网络连接在一起。使用X Window开发GUI时,因为开发环境成熟,开发工具易用,所以可以缩短开发时间,降低开发难度。
29
X Window系统应用于嵌入式系统时,要考虑嵌入式系统的特殊条件。嵌入式系统由于资源有限,不宜使用体积大的操作系统内核,这是因为需要将系统固化在ROM中或者Disk On Chip的FLASH ROM上。 但是,一个X Lib就需要大概10MB ~ 20MB的空间,在一般的嵌入式环境不能满足这样的条件。所以,针对嵌入式领域,X Window进行了必要的裁剪和优化,产生了很多嵌入式GUI系统,如中国的GNU项目――Mini GUl等。
30
6. 丰富的开发技术资源 Linux在这几年中不断成熟,越来越多的人加入了Linux的行列。以前,Linux只是一群黑客的玩具和专家的实验用操作系统,而现在即使普通电脑用户也在用Linux。 为Linux提供服务的公司也开始出现,并为客户提供专业化的技术支持。但实际上不用花钱也可以通过Intemet新闻组得到强大的Linux技术支持和帮助。
31
Linux有一个庞大的支持者群体,其中许多人都编写驱动程序和其他的更新程序,并且免费通过Internet进行共享。
这意味着对新硬件的Linux驱动程序甚至比用于其他UNIX系统(如Solaris的驱动程序)还来得及时。 Linux庞大的志愿者网络在生产“补丁”程序方面反应很快。
32
3.3 嵌入式Linux的组成 最基本的嵌入式Linux系统需要3个基本元素: 系统引导程序,用于完成机器加电后的系统定位引导;
初始化过程,完成基本的初始化。
33
为使这个最小嵌入式系统具有一定的实用性,还需加上硬件的驱动程序及—个或几个应用进程以提供必要的应用功能支持。
如果应用比较复杂,可能还需要添加一个可以在ROM或RAM中使用的文件系统、TCP/IP网络协议栈等。 在PDA领域,还需要加上—个GUl支持。
34
嵌入式系统的存储 在嵌入式系统中使用Flash存储器 文件系统类型: ext2:专用于Linux,引导块、超级块、 inode、数据区。
cramfs:压缩式文件系统,需要时才解压缩。缺点:延迟、写操作困难。 romfs:只读文件系统
35
日志文件系统: 日志记录,先写日志,后写数据。 JFFS:专用于Flash的日志式文件系统。 特点:比如空间回收。 缺陷:……
36
YAFFS文件系统 YAFFS,Yet Another Flash File System,是一种类似于JFFS/JFFS2的专门为Flash设计的嵌入式文件系统。与JFFS相比,JFFS和JFFS2适合NOR FLASH,YAFFS是为NAND FLASH量身定做的,它减少了一些功能,因此速度更快、占用内存更少。 。
37
YAFFS和JFFS都提供了写均衡,垃圾收集等底层操作。它们的不同之处在于: (1)、JFFS是一种日志文件系统,通过日志机制保证文件系统的稳定性。YAFFS仅仅借鉴了日志系统的思想,不提供日志机能,所以稳定性不如JFFS,但是资源占用少。 (2)、JFFS中使用多级链表管理需要回收的脏块,并且使用系统生成伪随机变量决定要回收的块,通过这种方法能提供较好的写均衡,在YAFFS中是从头到尾对块搜索,所以在垃圾收集上JFFS的速度慢,但是能延长NAND的寿命。 (3)、JFFS支持文件压缩,适合存储容量较小的系统;YAFFS不支持压缩,更适合存储容量大的系统。
38
嵌入式Linux的版本 嵌入式Linux到现在已经有许多种版本了,按照实际应用的场合及特殊的功能需求,嵌入式Linux基本上可以分为以下几类:
39
将Linux移植以满足实时要求的实时操作系统,应用于一些关键的控制场合,如Fsmlabs公司的RT Linux,Monta Vista的Hard Hat Linux。
尽可能保留Linux的强大功能,尽可能地减少其体积,以满足许多嵌入式系统对体积的要求,如uClinux。 针对特定嵌入式领域采用整合方案,如Lineo,TimeSys,合肥华恒等。接下来,简要介绍常用的两个嵌入式Linux操作系统。
40
1. 实时Linux(RT Linux) RT Linux是美国新墨西哥州大学计算机系研制开发的。
41
RT Linux实现的内核位于通常的Linux内核之下,这个内核是一个实时内核,它只需要完成底层的任务创建、中断服务程序,并为底层任务、ISR和Linux进程之间进行通信排队。
而且RT Linux将原来的Linux内核作为实时内核下的一个任务。由于这个任务的优先级最低,所以它随时可能被其他的实时任务(运行在RT Linux的实时内核下)抢占。
42
嵌入式Linux实时化技术 RTlinux双内核实时化结构
43
嵌入式Linux实时化技术 RTLinux任务处理流程
44
经过加入实时处理后,RT Linux就完全能够达到硬实时系统的性能指标。在一台386机器上,RT Linux从处理器检测到中断,再到中断处理程序开始工作不会超过15μs;对一个周期性的任务,在35μs内一定会执行。 而通常的Linux内核,一般是在600μs内开始一个中断服务程序,对周期性的任务很可能会超过20μs。
45
目前,RT Linux已经得到了广泛的应用,如好莱坞的电影制作人用RT Linux作为视频编辑工具,NASA用RT Linux开发出来的数据采集系统以采集George飓风中心的风速。
46
2. uClinux uClinux是一个GNU的项目,代码完全开放。
uClinux的英文解释是Micro Control Linux,可理解为“微控制领域中的Linux系统”。 它专门应用于没有MMU的CPU,并专为嵌入式系统做了许多小型化的工作,已支持的微处理器包括Motorola MC68000,MCF5206和MCF5207ColdFire等。
47
针对这种没有MMU的CPU架构,uClinux采用了一种平板式(Flat)的内存模型来去除对MMU的依赖, 并且改变了用户程序的加载方式,开发了运用于uClinux的C函数库--uCLibc.
48
目前,uClinux往往基于两个Linux内核版本,2. 38 是一个比较成熟的版本,2. 4
目前,uClinux往往基于两个Linux内核版本, 是一个比较成熟的版本,2.4.x是最新的版本。 Hitool 套件同时提供了对他们的支持。 一般uClinux的内核大小在500k左右,如果加上一些基本的应用,也就在900k左右.非常适合于嵌入式系统。 uClinux架构如图所示,一些重要的模块在下面描述。
50
启动过程(Bootstrap) Bootstrap负责用来起动Linux内核,包括Chip Selector初始化,系统堆栈的初始化,把压缩的Linux映像从Flash中解压到RAM中,并把控制权交给内核的初始化例程。 这部分工作是与你的硬件高度相关的,所以这部分的代码要尽量精简。
51
内核初始化(Kernel Initialization)
内核初始化的入口地址是: start_kernel(在init/main.c中), 它初始化内核的其它部分,包括 异常(trap)、中断(IRQ)、内存页 (Page)、调度(Scheduling)、 驱动程序等等。并启动“init”进程进入多 任务环境。
52
系统调用处理/异常处理 当“init”程序运行后,内核对整个系统的运行不再进行直接控制,而是通过系统调用给应用程序提供服务和响应外部及内部的异步事件,例如:程序错误,硬件中断等。 用户程序如果想得到系统资源,必须通过系统调用.当用户进程发生中断后,内核获得控制权,取得系统调用的参数,并调用相应的处理程序,而用户一直被挂起,直到内核完成处理并返回。
53
如例所示: swi 0x900004(其中0x900004表示这个系统调用为sys_write)。
在ARM中,系统调用采用swi指令所产生的软件异常来实现, 如例所示: swi 0x900004(其中0x900004表示这个系统调用为sys_write)。
54
<write>: 2550: ef swi 0x 2554: e3700a01 cmn r0, #4096 ; 0x1000 2558: 2afff6b0 bcs 20 <\<>__syscall_error<\>> 255c: e1a0f00e mov pc, lr
55
驱动程序(Device Driver) 驱动程序是整个Linux内核的主要组成部分,它们控制着操作系统和外部设备的交互.例如,串口驱动程序(arch/armnommu/mach-atmel/…….)处理由外部UART发生中断。 Linux的驱动程序是可选的,但是典型的系统应该包括一个控制台(Console),一个通用串口驱动程序,一个块设备 (用来存放根文件系统) 驱动程序。
56
当Linux内核起动的时候,需要一个输出调试信息的设备。
这个设备往往通过串口来实现。 这个调试终端可以通过register-console 这个函数来创建.而所有的调试信息都通过Printk例程通过这个调试终端来输出。
57
进程/线程管理
58
在Linux中,进程以进程号PID(process ID)作为标识。
每个进程都属于一个用户,进程要配备其所属的用户编号UID。 此外,每个进程都属于多个用户组,所以进程还要配备其归属的用户组编号GID的数组
59
进程运行的环境称为进程上下文(context)。
Linux中进程的上下文由进程控制块PCB(process control block)、正文段(text segment)、数据段(data segment)以及用户堆栈(stack)组成。
60
7.2.2 进程状态 进程是—个动态的实体,故而它是有生命的。 从创建到消亡,是一个进程的整个生命周期。
进程状态 进程是—个动态的实体,故而它是有生命的。 从创建到消亡,是一个进程的整个生命周期。 在这个周期中,进程可能会经历各种不同的状态。 一般来说,所有进程都要经历以下3种状态。
61
◆ 就绪(ready)态: 指进程已经获得所有所需的其他资源,并正在申请处理机资源,准备开始运行。 这种情况下,称进程处于就绪态。
62
◆ 阻塞(blocked)态: 指进程因为需要等待所需资源而放弃处理机,或者进程本不拥有处理机,且其他资源也没有满足,从而即使得到处理机资源也不能开始运行。 这种情况下,称进程处于阻塞态。阻塞状态又称休眠状态或者等待状态。
63
◆ 运行态: 进程得到了处理机,并不需要等待其他任何资源,正在执行的状态,称之为运行态。 只有在运行态时,进程才可以使用所申请到的资源。
64
在Linux系统中,将各种状态进行了重新组织,由此得出Linux进程的几个状态(如图7-2所示):
65
图 7-2 Linux 进程状态转换
66
文件系统(File System) 支持多种文件系统是Linux一个重要的特性,uClinux同样把这一特性带进了嵌入式系统中,并针对嵌入式系统作了一些取舍。
67
线程的基本概念 线程是包含在进程中的一种实体.
它有自己的运行线索,可以完成一定的任务,可与其他线程共享进程中的共享变量及部分环境、相互之间协同来完成进程所要完成的任务。
68
线程能够比进程有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。
线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。
69
线程的分类 从调度的角度,线程可以分为用户线程和内核线程两类:
70
1.用户线程 用户线程的实现是通过运行时间系统代码来实现的。 此代码的功能是将线程恢复运行时需要保存的寄存器组信息和其他信息保存在一个数据结构中,并将这个结构放进线程调度队列中,然后从线程队列中选择一个优先级最高的线程来运行,最后将结构中保存的信息恢复,并运行这个线程。
71
这部分代码通常是在用户机线程包中,它将在程序编译时链接到可执行程序中。
线程的切换实际上并不是核心调度器来实现的,而是通过进程内的代码来实现的。
72
2.内核线程 内核线程切换的实现是通过内核调度器来实现的。 内核线程同进程是一样的,都是核心调度器的调度实体
73
线程基础 本节将结合实例说明多线程编程中所使用到的一些函数和基本方法。
74
7.6.1 线程的基本操作函数 以下先讲述4个基本线程函数,在调用它们前均要包括pthread.h头文件。
线程的基本操作函数 以下先讲述4个基本线程函数,在调用它们前均要包括pthread.h头文件。 然后再给出用它们编写的一个程序例子。
75
1. 创建线程函数 int pthread_create(pthread_t *tid,const pthread_attr_t *attr, void *(*func)(void *),void *arg);
76
线程创建函数第一个参数为指向线程标识 符的指针,
第二个参数用来设置线程属性 第三个参数是线程运行函数的起始地址 最后一个参数是运行函数的参数。
77
这里,我们的函数thread 不需要参数,所以最后一个参数设为空指针。
第二个参数我们也设为空指针,这样将生成默认属性的线程。 当创建线程成功时,函数返回0,若不为0 则说明创建线程失败,常见的错误返回代码为EAGAIN 和EINVAL
78
EAGAIN表示系统限制创建新的线程,例如线程数目过多了;
EINVAL表示第二个参数代表的线程属性值非法。 创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
79
2. 等待线程的结束函数 int pthread_join(pthread_t tid,void **status);
80
第一个参数为被等待的线程标识符, 第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。 这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。
81
3. 取自己线程ID函数: pthread_t pthread_self(void);
82
线程都有一个ID以在给定的进程内标识自己。
线程ID由pthread_creat返回,可以取得自己的线程ID。
83
4. 终止线程函数 一个线程的结束有两种途径,一种是函数结束了,调用它的线程也就结束了; 另一种方式是通过函数pthread_exit 来实现。 它的函数原型为: void pthread_exit(void *status);
84
唯一的参数是函数的返回代码,只要pthread_join 中的第二个参数thread_return 不是NULL,这个值将被传递给thread_return。
最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join 的线程则返回错误代码ESRCH。 见 : pthread_join_exit_演示.ppt
85
7.6.2 简单的多线程编程 Linux系统下的多线程遵循POSIX线程接口,称为pthread。
简单的多线程编程 Linux系统下的多线程遵循POSIX线程接口,称为pthread。 编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。 顺便说一下,Linux下pthread 的实现是通过系统调用clone()来实现的。
86
clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。
下面展示一个最简单的多线程程序。
87
#include<stdio.h>
#include<pthread.h> void thread(void) { int i; for(i=0;i<3;i++) printf("This is a pthread.\n"); }
88
int main(void) { pthread_t id; int i,ret; ret=pthread_create(&id,NULL,(void*)thread,NULL); if(ret!=0){ printf("Create pthread error!\n"); exit(); }
89
for(i=0;i<3;i++) printf("This is the main process.\n"); pthread_join(id,NULL); return(0); }
90
运行程序,得到如下结果: This is the main process。 This is a pthread。 This is the main process. This is a pthread.
91
再次运行,可能得到如下结果: This is a pthread. This is the main process. 前后两次结果不一样,这是两个线程争夺CPU资源的最终结果。
92
设置线程的属性 pthread_create函数的第二参数为指向pthread_attr_t结构的指针,设置为NULL表示使用默认属性创建一个线程。事实上,用户完全可以通过填写该结构来创建具有相关属性的线程,POSIX提供了一系列填写该结构的函数,从而设置所有相关属性,
93
pthread_attr_init函数 pthread_attr_init函数的作用是初始化pthread_attr_t结构,用户在调用其他函数设置结构中的参数前,必须先调用本函数对结构进行初始化,其原型为: int pthread_attr_init(pthread_attr_t *attr); 参数attr:待初始化的pthread_attr_t结构; 返回值:0,成功,非0,失败;
94
pthread_attr_destroy函数
pthread_attr_destroy函数的作用是释放pthread_attr_t结构占用的资源,pthread_attr_t结构经过初始化和其他函数设置后可能会占用某些资源,本函数的作用就是释放这些资源,一般在pthread_attr_t结构使用完毕后调用本函数,其原型为: void pthread_attr_destroy(pthread_attr_t *attr); 参数attr:待释放的pthread_attr_t结构; 返回值:0,成功,非0,失败;
95
pthread_attr_setdetachstate函数
pthread_attr_setdetachstate函数的作用是设置线程的detachedstate属性,可以取值PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACHED,前者是默认值,表示其他线程可以使用pthread_join函数等待本线程结束,后者表示其他线程不可以对本线程使用pthread_join,其原型为: int pthread_attr_setdetachstate(pthread_attr_t *attr, int state); 参数attr:待设置的pthread_attr_t结构; 参数state:取值PTHREAD_CREATE_JOINABLE或PTHREAD_CREATE_DETACHED,表示期望的属性值; 返回值:0成功,非0失败;
96
pthread_attr_getdetachstate函数
pthread_attr_getdetachstate函数的作用是获取pthread_attr_t结构中的detachedstate属性,原型如下: int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 参数attr:待获取属性的pthread_attr_t结构; 参数detachstate:属性的返回值; 返回值:0成功,非0失败;
97
pthread_attr_setschedpolicy函数
pthread_attr_setschedpolicy函数的作用是设置schedpolicy属性,即线程调度算法。schedpolicy属性值可以是SCHED_RR、SCHED_FIFO、SCHED_OTHER,其中SCHED_RR表示轮训调度,SCHED_FIFO表示先进先出调度,SCHED_OTHER表示其他。拥有管理员权限的进程才可以创建具有SCHED_RR或SCHED_FIFO调度算法的线程,一般线程的默认调度算法都是SCHED_OTHER。函数原型为: int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); 参数attr:待设置的pthread_attr_t结构; 参数policy:取值SCHED_RR、SCHED_FIFO或SCHED_OTHER; 返回值:0成功,非0失败;
98
pthread_attr_getschedpolicy函数
pthread_attr_getschedpolicy函数的作用是获取pthread_attr_t结构中的schedpolicy属性,原型如下: int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); 参数attr:待获取属性的pthread_attr_t结构; 参数detachstate:属性的返回值; 返回值:0成功,非0失败;
99
pthread_attr_setinheritsched函数
pthread_attr_setinheritsched函数的作用是设置线程调度算法的继承特性,可以取值PTHREAD_EXPLICIT_SCHED或PTHREAD_INHERIT_SCHED,前者表示使用结构pthread_attr_t指定的调度算法,后者表示继承父线程使用的调度算法,默认为前者,原型如下: int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit); 参数attr:待设置的pthread_attr_t结构; 参数policy:取值PTHREAD_EXPLICIT_SCHED或PTHREAD_INHERIT_SCHED; 返回值:0成功,非0失败;
100
pthread_attr_getinheritsched函数
pthread_attr_getinheritsched函数的作用是获取pthread_attr_t结构中的继承属性,原型如下: int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit); 参数attr:待获取属性的pthread_attr_t结构; 参数inherit:继承属性的返回值; 返回值:0成功,非0失败;
101
pthread_attr_setscope函数
pthread_attr_setscope函数的作用是设置线程的在什么范围内竞争CPU资源,可以取值PTHREAD_SCOPE_SYSTEM或PTHREAD_SCOPE_PROCESS,前者表示在整个系统内竞争CPU资源,后者表示在同一进程内竞争CPU资源,默认为前者,原型如下: int pthread_attr_setscope(pthread_attr_t *attr, int scope); 参数attr:待设置的pthread_attr_t结构; 参数policy:取值PTHREAD_SCOPE_SYSTEM或PTHREAD_SCOPE_PROCESS; 返回值:0成功,非0失败;
102
pthread_attr_getscope函数
pthread_attr_getscope函数的作用是获取pthread_attr_t结构中的CPU竞争范围属性,原型如下: int pthread_attr_getscope(const pthread_attr_t *attr, int *scope); 参数attr:待获取属性的pthread_attr_t结构; 参数scope:竞争范围的返回值; 返回值:0成功,非0失败;
103
设置或获取线程栈的大小 pthread_attr_setstacksize函数的作用是设置线程栈的大小,单位是字节,在默认情况下线程的栈是比较大的,原型如下: pthread_attr_getstacksize函数的作用是获取pthread_attr_t结构中的栈的大小
104
例子 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> void *thread_function(void *arg); char message[] = "Hello World"; int thread_finished = 0; int main() { int res; pthread_t a_thread; pthread_attr_t thread_attr; res = pthread_attr_init(&thread_attr); if (res != 0) { perror("Attribute creation failed"); exit(EXIT_FAILURE); } res = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); perror("Setting detached attribute failed");
105
res = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);
if (res != 0) { perror("Setting scheduling policy failed"); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, &thread_attr, thread_function, (void *)message); perror("Thread creation failed"); pthread_attr_destroy(&thread_attr); while(!thread_finished) { printf("Waiting for thread to say it's finished...\n"); sleep(1); printf("Other thread finished, bye!\n"); exit(EXIT_SUCCESS);
106
void *thread_function(void *arg) {
printf("thread_function is running. Argument was %s\n", (char *)arg); sleep(4); printf("Second thread setting finished flag, and exiting now\n"); thread_finished = 1; pthread_exit(NULL); }
107
运行结果: Waiting for thread to say it's finished... thread_function is running. Argument was Hello World Second thread setting finished flag, and exiting now Other thread finished, bye!
108
线程应用中的同步问题
109
和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。
但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。 许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。
110
在函数中声明的静态变量常常会带来一些问题,函数的返回值也会有问题。
因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。
111
在进程中,共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。
为了保护变量,必须使用信号量、互斥等方法来保证对变量的正确使用。 下面,就逐步介绍处理特定线程数据时的有关知识。
112
特定线程数据
113
在单线程的程序里,有两种基本的数据:全局变量和局部变量。
但在多线程程序里,还有第三种数据类型:特定线程数据(TSD:Thread-Specific Data)。 它和全局变量很像,在线程内部,各个函数可以像使用全局变量一样调用它,但它对线程外部的其他线程是不可见的。
114
int func() { char. p; p = strdup(thread-specific-data[1]);. } void
int func() { char *p; p = strdup(thread-specific-data[1]); } void *pthread-1(void *arg) { func() } void *pthread-2(void *arg) { func() } 不同的线程调用func产生的结果是不同的 //strdup:复制字符串,复制完成后返回字符串的指针保存在p中(string.h)
115
这种数据的必要性是显而易见的。例如我们常见的变量errno返回标准的出错信息。
这种数据的必要性是显而易见的。例如我们常见的变量errno返回标准的出错信息。 它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。 要实现诸如此类的变量,就必须使用特定线程数据。
116
我们为每个特定线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代特定线程数据
但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
117
和特定线程数据相关的函数主要有4个:创建一个键、为一个键指定特定线程数据、从一个键读取特定线程数据和删除键。
118
int pthread_key_create(pthread_key_t. KEY, void (
int pthread_key_create(pthread_key_t *KEY, void (*destr_function) (void *)); int pthread_key_delete(pthread_key_t KEY); int pthread_setspecific(pthread_key_t KEY, const void *POINTER); void * pthread_getspecific(pthread_key_t KEY);
119
要注意的是,为一个键指定新的特定线程数据时,必须自己释放原有的特定线程数据以回收空间。
删除键函数仅释放这个键占用的内存,并不释放该键关联的特定线程数据所占用的内存资源。 因此,特定线程数据的释放必须在释放键之前完成。
120
Posix定义了两个API分别用来创建和注销TSD: int pthread_key_create(pthread_key_t
Posix定义了两个API分别用来创建和注销TSD: int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)) 该函数从TSD池中分配一项,将其值赋给key供以后访问使用。如果destr_function不为空,在线程退出(pthread_exit())时将以key所关联的数据为参数调用destr_function(),以释放分配的缓冲区。 不论哪个线程调用pthread_key_create(),所创建的key都是所有线程可访问的,但各个线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。
121
7.7.2 互斥锁 互斥锁用来保证一段时间内只有一个线程在执行一段代码。
互斥锁 互斥锁用来保证一段时间内只有一个线程在执行一段代码。 必要性显而易见:假设各个线程向同一个文件按顺序写入数据,最后得到的一定是灾难性的结果。
122
先看下面一段代码。 这是一个读/写程序,它们公用一个缓冲区,并且假定一个缓冲区只能保存一条信息。 即缓冲区只有两个状态:有信息或没有信息。
123
void reader_function(void);
void writer_function(void); char buffer; int buffer_has_item=0; pthread_mutex_t mutex; struct timespec delay;
124
void main(void){ pthread_t reader; delay.tv_sec=2;/*定义延迟时间*/ delay.tv_nec=0; pthread_mutex_init(&mutex,NULL); /*用默认属性初始化一个互斥锁对象*/ pthread_create(&reader,pthread_attr_default,(void *)&reader_function),NULL); writer_function(); }
125
void writer_function(void){
while(1){ pthread_mutex_lock(&mutex);/*锁定互斥锁*/ if(buffer_has_item==0){ buffer=make_new_item(); buffer_has_item=1; } pthread_mutex_unlock(&mutex);/*打开互斥锁*/ pthread_delay_np(&delay);
126
void reader_function(void){
while(1){ pthread_mutex_lock(&mutex); if(buffer_has_item==1){ consume_item(buffer); buffer_has_item=0; } pthread_mutex_unlock(&mutex); pthread_delay_np(&delay);
127
这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。
函数pthread_mutex_init用来生成一个互斥锁。 NULL参数表明使用默认属性。 如果需要声明特定属性的互斥锁,需调用函数pthread_mutexattr_init。
128
函数pthread_mutexattr_setpshared 和函数pthread_mutexattr_settype用来设置互斥锁属性。
前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED前者用来使不同进程中的线程同步,后者用于同步进程的不同线程。
129
在上面的例子中,使用的是默认属性PTHREAD_PROCESS_PRIVATE。
后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD_MUTEX_DEFAULT。
130
它们分别定义了不同的上锁、解锁机制。 一般情况下,选用最后一个默认属性。
131
PTHREAD_MUTEX_NORMAL
描述: 此类型的互斥锁不会检测死锁。 PTHREAD_MUTEX_ERRORCHECK 描述: 此类型的互斥锁可提供错误检查。如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则会返回错误。
132
PTHREAD_MUTEX_RECURSIVE
描述: 如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则可成功锁定该互斥锁。 即可以多次锁定。多次锁定互斥锁需要进行相同次数的解除锁定才可以释放该锁,然后其他线程才能获取该互斥锁。
133
pthread_mutex_lock 声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止均被上锁,即同一时间只能被一个线程调用执行。
134
在上面的例子中,使用了pthread_delay_np函数,让线程睡眠一段时间,是为了防止一个线程始终占据此函数。
135
上面的例子非常简单,就不再介绍了,需要提出的是,在使用互斥锁的过程中很有可能会出现死锁:即两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁。
例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。
136
此时我们可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。
另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。
137
读写锁 读写锁是从互斥锁中发展下来的,互斥锁会将试图访问我们定义的保护区的所有进程都阻塞掉,但读写锁与此不同,它会将访问中的读操作和写操作区分开来对待,它所遵循的规则如下:
138
◆ 只要没有进程持有某个给定的读写锁用于写,那么任意数目的线程都可以持有该读写锁用于读。
◆ 仅当没有线程持有某个给定的读写锁用于读或写,才能分配该读写锁用于写。 在某些读数据比改数据频繁的应用中,读写锁将会比互斥锁表现出很大的优越性。
139
7.5.4 记录上锁 记录锁实际上是读写锁的一种扩展类型。读写锁是作为数据类型pthread_rwlock_t的变量在内存中分配的。
记录上锁 记录锁实际上是读写锁的一种扩展类型。读写锁是作为数据类型pthread_rwlock_t的变量在内存中分配的。 当读写锁是在单个进程内的各个线程间共享时,这些变量可以在那个进程内; 当读写锁是在共享某个内存区的进程间共享时,这些变量应该在该共享内存区中。
140
记录上锁的基本原理与读写锁不同,记录锁可用于有亲缘关系的或无亲缘关系的进程之间共享某个文件的读与写。
被锁住的文件通过其描述符访问,执行上锁操作的函数是fcntl。
141
这种类型的锁通常是在内核中维护的,其属性主标识为fcntl调用进程的进程ID。
这意味着这些锁用于不同的进程间的上锁,而不是用于同一进程内不同线程间的上锁。
142
7.5.5 条件变量 与互斥锁不同,条件变量是用来等待而不是用来上锁的。
条件变量 与互斥锁不同,条件变量是用来等待而不是用来上锁的。 它是通过一种能够挂起当前正在执行的进程或放弃当前进程直到在共享数据上的一些条件得以满足。
143
条件变量操作过程是:首先通知条件变量,然后等待,同时挂起当前进程直到有另外一个进程通知该条件变量为止。
这样也就可以实现进程间通讯的同步了。
144
使用互斥锁可以实现线程间数据的共享和通信,但互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。
而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。
145
使用时,条件变量用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。
一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 这些线程将重新锁定互斥锁并重新测试条件是否满足。 一般说来,条件变量用来进行线程间的同步。
146
条件变量 int pthread_cond_init(pthread_cond_t *cv,
const pthread_condattr_t *cattr); pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex); pthread_cond_signal(pthread_cond_t *cv);
147
具体函数见实验指导书
148
信号量 信号量(semaphore)是另一种加锁操作,与普通加锁不同的是,信号量记录了一个空闲资源数值,信号量的值表示了当前空闲资源的多少。 信号量本质上是一个非负的整数计数器,它用来控制对公共资源的访问。
149
int sem_init(sem_t *sem, int pshared, unsigned int value);
使用sem_init可以将sem 所指示的未命名信号变量初始化为value。如果pshared 的值为零,则不能在进程之间共享信号。如果pshared 的值不为零,则可以在进程之间共享信号
150
增加信号: 当公共资源增加时,调用函数sem_post()增加信号量。 只有当信号量值大于0时,才能使用公共资源. 减少信号量: 函数sem_wait()减少信号量。 当它变为0时,进程将主动放弃处理器进入等待对列。
151
7.8 本章小结 本章主要介绍了进程概念、进程调度、进程通信和同步。另外介绍了线程定义、多线程同步及示例。
本章小结 本章主要介绍了进程概念、进程调度、进程通信和同步。另外介绍了线程定义、多线程同步及示例。 在实际应用中,多线程的编程方法也是得到了广泛的使用,比如使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,希望大家能用多线程技术写出高效实用的好程序来。
Similar presentations