??xml version="1.0" encoding="utf-8" standalone="yes"?>
实模式:
Q即实地址讉K模式Q它?/span>
Intel公司80286及以后的x86(80386,80486?/span>80586{?/span>)兼容处理器(CPUQ的一U操作模式。实模式被特D定义ؓ20位地址内存可访问空间上Q这意味着它的定w?/span>2?/span>20ơ幂Q?/span>1MQ的可访问内存空_物理内存?/span>BIOS-ROMQ,软g可通过q些地址直接讉KBIOSE序和外围硬件。实模式下处理器没有gU的内存保护概念和多道Q务的工作模式。但是ؓ了向下兼容,所?/span>80286及以后的x86pd兼容处理器仍然是开机启动时工作在实模式下?/span>80186和早期的处理器仅有一U操作模式,是后来我们所定义的实模式。实模式虽然能访问到1M的地址I间Q但是由?/span>BIOS的映作用(?/span>BIOS占用了部分空间地址资源Q,所以真正能使用的物理内存空_内存条)Q也是?/span>640k?/span>924k之间?/span>1M
地址I间l成是由
16位的D地址?/span>16位的D内偏移地址l成的。用公式表示为:物理地址=左移4位的D地址+偏移地址?/span>
286处理器体pȝ构引入了地址保护模式的概念,处理器能够对内存及一些其他外围设备做gU的保护讄Q保护设|实质上是屏蔽一些地址的访问)。用这些新的特性,然而必不可一些额外的?/span>80186及以前处理器没有的操作规E。自从最初的x86微处理器规格以后Q它对程序开发完全向下兼容,80286芯片被制作成启动时承了以前版本芯片的特性,工作在实模式下,在这U模式下实际上是关闭了新的保护功能特性,因此能以往的Y件l工作在新的芯片下。直C天,甚至最新的x86处理器都是在计算机加电启动时都是工作在实模式下,它能q行Z前处理器芯片写的E序.
DOS操作pȝQ例?/font>
MS-DOS,DR-DOSQ工作在实模式下Q微?/span>Windows早期的版本(它本质上是运行在DOS上的囑Ş用户界面应用E序Q实际上本nq不是一个操作系l)也是q行在实模式下,直到Windows3.0Q它q行期间既有实模式又有保护模式,所以说它是一U合模式工作。它的保护模式运行有两种不同意义(因ؓ80286q没有完全地实现80386及以后的保护模式功能)Q?/span>
1〉“标准保护模式”:q就是程序运行在保护模式下;
2〉“虚拟保护模式(实质上还是实模式Q是实模式上模拟的保护模式)”:它也使用32位地址d方式?/span>Windows3.1d删除了对实模式的支持。在80286处理器芯片以后,Windows3.1成ؓL操作pȝQ?/span>Windows/80286不是L产品Q。目前差不多所有的X86pd处理器操作系l(LinuxQ?/span>Windows95 and laterQ?/span>OS/2{)都是在启动时q行处理器设|而进入保护模式的?/span>
实模式工作机理:
1> 对于8086/8088来说计算实际地址是用l对地址?/span>1M求模?/span>8086的地址U的物理l构Q?/span>20根,也就是它可以物理d的内存范围ؓ2^20个字节,?/span>1 MI间Q但׃8086/8088所使用的寄存器都是16位,能够表示的地址范围只有0-64KQ这?/span>1M地址I间来比较也太小了,所以ؓ了在8086/8088下能够访?/span>1M内存Q?/span>Intel采取了分D寻址的模式:16位段基地址:16位偏U?/span>EA。其l对地址计算Ҏ为:16位基地址左移4?/span>+16位偏U?/span>=20位地址?/span>
比如Q?/span>DS=1000H EA=FFFFH 那么l对地址׃ؓQ?/span>10000H +
2> 对于80286或以上的CPU通过A20 GATE来控?/span>A20地址U?/span>
?/span>
技术发展到?/span>
80286Q虽然系l的地址ȝ由原来的20根发展ؓ24根,q样能够讉K的内存可以达?/span>2^24=16M,但是Intel在设?/span>80286时提出的目标是向下兼?/span>,所以在实模式下Q系l所表现的行为应该和8086/8088所表现的完全一P也就是说Q在实模式下Q?/span>80386以及后箋pd应该?/span>8086/8088完全兼容仍然使用A20地址Uѝ所以说80286芯片存在一?/span>BUGQ它开?/span>A20地址Uѝ如果程序员讉K100000H-10FFEFH之间的内存,pȝ实际访问这块内存(没有wrap-around技术)Q而不是象8086/8088一样从0开始。我们来看一副图Q?/span>
Z解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址U(?/span>0开始数是第20根) 的有效性,被称?/span>A20 GateQ?/span>
1> 如果A20 Gate被打开Q则当程序员l出100000H-10FFEFH之间的地址的时候,pȝ真正访问这块内存区域;
2 如果A20 Gate被禁止,则当E序员给?/span>100000H-10FFEFH之间的地址的时候,pȝ仍然使用8086/8088的方式即取模方式Q?/span>8086仿真Q。绝大多?/span>IBM PC兼容机默认的A20 Gate是被止的。现在许多新?/span>PC上存在直接通过BIOS功能调用来控?/span>A20 Gate的功能?/span>
上面所q的内存讉K模式都是实模式,?/span>80286以及更高pd?/span>PC中,即A20 Gate被打开Q在实模式下所能够讉K的内存最大也只能?/span>10FFEFHQ尽它们的地址ȝ所能够讉K的能力都大大过q个限制。ؓ了能够访?/span>10FFEFH以上的内存,则必进入保护模式?/span>
保护模式Q?/strong>
l常~写?/span>
p-mode,?/font>
Intel iAPX 286E序员参考手册中Q?/span>iAPX
80286及以后的处理器另一U工作模式是实模式(仅当pȝ启动的一瞬间Q,本着向下兼容的原则屏蔽保护模式特性,从而容许老的软g能够q行在新的芯片上。作Z个设计规范,所有的x86pd处理器,除嵌入式Intel80387之外Q都是系l启动工作在实模式下Q确保遗留下的操作系l向下兼宏V它们都必须被启动程序(操作pȝE序最初运行代码)重新讄而相应进入保护模式的Q在q之前Q何的保护模式Ҏ都是无效的。在C计算ZQ这U匹配进入保护模式是操作pȝ启动时最前沿的动作之一?/span>
在被调停的多道Q务程序中Q它可以从新工作在实模式下是相当可能的。保护模式的Ҏ是L被其他Q务或pȝ内核破坏已经不健全的E序的运行,保护模式也有对硬件的支持Q例如中断运行程序,Udq行q程文档到另一个进E和|空多Q务的保护功能?/span>
386及以后系列处理器不仅h保护模式又具?/span>32位寄存器Q结果导致了处理功能的乱,因ؓ80286虽然支持保护模式Q但是它的寄存器都是16位的Q它是通过自nE序讑֮而模拟出?/span>32位,q32位寄存器处理。归咎于q种混ؕ现象Q它促Windows/386
及以后的版本d抛弃80286的虚拟保护模式,以后保护模式的操作系l都是运行在80386以上Q不再运行在80286Q尽?/span>80286模式支持保护模式Q,所以说80286是一个过渡芯片,它是一个过渡品?/span>
管
286?/span>386处理器能够实C护模式和兼容以前的版本,但是内存?/span>1M以上I间q是不易存取Q由于内存地址的回l,IBM PC XT Q现以废弃)设计一U模拟系l,它能q欺骗手D访问到1M以上的地址I间Q就是开通了A20地址Uѝ在保护模式里,?/span>32个中断ؓ处理器异帔R留,例如Q中?/span>0DQ十q制13Q常规保护故障和中断00是除Cؓ零异常?/span>
如果要访问更多的内存Q则必须q入保护模式Q那么,在保护模式下Q?/font>
A20
Z搞清楚这一点,我们先来看一?/span>A20的工作原理?/span>A20Q从它的名字可以看出来Q其实它是对于A20Q从0开始数Q的Ҏ处理(也就是对W?/span>21根地址U的处理)。如?/span>A20 Gate被禁止,对于80286来说Q其地址?/span>24根地址U,其地址表示?/span>EFFFFFQ对?/span>80386极其随后?/span>32根地址U芯片来_其地址表示?/span>FFEFFFFF。这U表C的意思是Q?/span>
1>
如果A20
2如果A20 Gate被打开。则其第20-bit是有效的Q其值既可以?/span>0Q又可以?/span>1。那么就可以?/span>A20U传递实际的地址信号。如?/span>A20 Gate被打开Q则可以讉K的内存则是连l的?/span>
实模式和保护模式的区别:
从表面上看,保护模式和实模式q没有太大的区别Q二者都使用了内存段、中断和讑֤驱动来处理硬Ӟ但二者有很多不同之处。我们知道,在实模式中内存被划分成段Q每个段的大ؓ
64KB
Q而这LD地址可以?/span>
16
位来表示。内存段的处理是通过和段寄存器相兌的内部机制来处理的,q些D寄存器Q?/span>
CS
?/span>
DS
?/span>
SS
?/span>
ES
Q的内容形成了物理地址的一部分。具体来_最l的物理地址是由
16
位的D地址?/span>
16
位的D内偏移地址l成的。用公式表示为:物理地址
=
左移
4
位的D地址
+
偏移地址?/span>
在保护模式下Q段是通过一pd被称之ؓ
?/span>
描述W表
?/span>
的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两U:全局描述W表
(GDT)
和局部描q符?/span>
(LDT)
?/span>
GDT
是一个段描述W数l,其中包含所有应用程序都可以使用的基本描q符。在实模式中Q段长是固定?/span>
(
?/span>
64KB)
Q而在保护模式中,D长是可变的Q其最大可?/span>
4GB
?/span>
LDT
也是D|q符的一个数l。与
GDT
不同Q?/span>
LDT
是一个段Q其中存攄是局部的、不需要全局׃n的段描述W。每一个操作系l都必须定义一?/span>
GDT
Q而每一个正在运行的d都会有一个相应的
LDT
。每一个描q符的长度是
8
个字节,格式如图
3
所C。当D寄存器被加载的时候,D基地址׃从相应的表入口获得。描q符的内容会被存储在一个程序员不可见的影像寄存?/span>
(shadow register)
之中Q以便下一ơ同一个段可以使用该信息而不用每ơ都到表中提取。物理地址?/span>
16
位或?/span>
32
位的偏移加上影像寄存器中的基址l成。实模式和保护模式的不同可以从下囑־清楚地看出来?/span>
实模式下d方式
保护模式下寻址方式
Boot Sector
l构、系l启动过E简?/b>
? pȝ启动q程?
Q注4Q在分区表的四个记录中,一般来说有且只有一个记录的标记是活动的QMBRQ主要负责从zd分区中装载ƈq行pȝ引导E序Q会L到这个分录,Ҏ记录的v始扇区加载该分区的逻辑 0 扇区Qv始扇区)的内容到 0x
]]>
0FFFFH = 1FFFFH 地址单元
。通过q种Ҏ来实C?/span>16位寄存器讉K1M的地址I间Q这U技术是处理器内部实现的Q通过上述分段技术模式,能够表示的最大内存ؓQ?/span>
FFFFh: FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16BytesQ?/span>1M多余出来的部分被U做高端内存?/span>HMAQ?span style="color: black;">。但8086/8088只有20位地址U,只能够访?/span>1M地址范围的数据,所以如果访?/span>100000h~10FFEFh之间的内存(大于1MI间Q,则必LW?/span>21根地址U来参与dQ?/span>8086/8088没有Q。因此,当程序员l出过1MQ?/span>100000H-10FFEFHQ的地址Ӟ因ؓ逻辑上正常,pȝq不认ؓ其访问越界而生异常,而是自动?/span>0开始计,也就是说pȝ计算实际地址的时候是按照?/span>1M求模的方式进行的Q这U技术被UCؓwrap-around?/span>
286?/span>Intel 80286的另一U叫法)
它又被称作ؓ虚拟地址保护模式。经在Intel 80286手册中已l提Z虚地址保护模式Q但实际上它只是一个指引,真正?/span>32位地址出现?/span>Intel 80386上。保护模式本w是80286及以后兼容处理器序列之后产成的一U操作模式,它具有许多特性设计ؓ提高pȝ的多道Q务和pȝ的稳定性。例如内存的保护Q分|制和g虚拟存储的支持。现代多数的x86处理器操作系l都q行在保护模式下Q包?/span>Linux, Free BSD, ?/span>Windows
3.0Q它也运行在实模式下Qؓ了和Windows 2.x应用E序兼容Q及以后的版本?/span>
Gate对于内存讉K有什么媄响呢Q?/font>
Gate被禁止。则其第A20?/span>CPU做地址讉K的时候是无效的,永远只能被作?/span>0。所以,在保护模式下Q如?/span>A20
Gate被禁止,则可以访问的内存只能是奇?/span>1MD,?/span>1M,3M,5M?span lang="ZH-CN">Q也是00000-FFFFF, 200000-2FFFFF,300000-3FFFFF?/font>
]]>
一. Boot Sector 的组?
Boot Sector 也就是硬盘的W一个扇区(?font face="Times New Roman">1Q?font face="Times New Roman">0柱面Q?font face="Times New Roman">0道Q?font face="Times New Roman">1扇区Q?Q注2Q?font face="Times New Roman"> 1道=16扇区Q?font face="Times New Roman">1扇区=512字节Q? 它由 MBR (Master Boot Record), DPT (Disk Partition Table) ?Boot Record ID 三部分组?
MBR 又称作主引导记录占用 Boot Sector 的前 446 个字?( 0 to 0x1BD ),
存放pȝd导程?(它负责检查硬盘分、寻扑֏引导分区q负责将可引导分区的引导扇区QDBRQ装入内?.
DPT 即主分区表占?64 个字?(0x1BE to 0x1FD), 记录了磁盘的基本分区
信息. d分ؓ四个分区? 每项 16 字节, 分别记录了每个主分区的信?
(因此最多可以有四个d?.
Boot Record ID 卛_导区标记占用两个字节 (0x1FE and 0x1FF), 对于合法
引导? 它等?0xAA55, q是判别引导区是否合法的标志.
Boot Sector 的具体结构如下图所C?
0000 |------------------------------------------------|
| |
| |
| Master Boot Record |
| |
| |
| dD?446字节) |
| |
| |
| |
01BD | |
01BE |------------------------------------------------|
| |
01CD | 分区信息 1(16字节) |
01CE |------------------------------------------------|
| |
01DD | 分区信息 2(16字节) |
01DE |------------------------------------------------|
| |
01ED | 分区信息 3(16字节) |
01EE |------------------------------------------------|
| |
01FD | 分区信息 4(16字节) |
|------------------------------------------------|
| 01FE | 01FF |
| 55 | AA |
|------------------------------------------------|
pȝ启动q程主要׃下几步组?以硬盘启动ؓ?:
1. 开?br /> 2. BIOS 加电自检 ( Power On Self Test -- POST )
内存地址?0ffff:0000
3. 硬盘第一个扇?(0??扇区, 也就是Boot Sector)
d内存地址 0000:
4. ?(WORD) 0000:7dfe 是否{于 0xaa55, 若不{于
则{d试其他启动介? 如果没有其他启动介质则显C?
"No ROM BASIC" 然后L.
5. 跌{?0000:
6. MBR 首先自己复制到 0000:0600 ? 然后l箋执行.
7. 在主分区表中搜烦标志为活动的分区. 如果发现没有zd
分区或有不止一个活动分? 则{停止.
8. 活动分区的W一个扇入内存地址 0000:
9. ?(WORD) 0000:7dfe 是否{于 0xaa55, 若不{于?
昄 "Missing Operating System" 然后停止, 或尝?
软盘启动.
10. 跌{?0000:
11. 启动pȝ ...
以上步骤?2,3,4,5 步是?BIOS 的引导程?/b>完成. 6,7,8,9,10
步由MBR中的引导E序完成.
一般多pȝ引导E序 (?SmartFDISK, BootStar, PQBoot {?
都是标准主引导记录替换成自q引导E序, 在运行系l启动程?
之前让用户选择要启动的分区.
而某些系l自带的多系l引导程?(?lilo, NT Loader {?
则可以将自己的引导程序放在系l所处分区的W一个扇Z, ?Linux
中即?SuperBlock (其实 SuperBlock 是两个扇?.
? 以上各步骤中使用的是标准 MBR, 其他多系l引导程序的引导q程与此不同.
]]>
<linux-2.4.20/kernel/fork.c> |
其中的clone_flags取自以下宏的"?|
<linux-2.4.20/include/linux/sched.h> |
在do_fork()中,不同? clone_flags导致不同的行ؓQ对于LinuxThreadsQ它使用QCLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHANDQ参数来调用clone()创徏"U程"Q表C共享内存、共享文件系l访问计数、共享文件描q符表,以及׃n信号处理方式。本 节就针对q几个参敎ͼ看看Linux内核是如何实现这些资源的׃n的?/p>
1.CLONE_VM
do_fork ()需要调用copy_mm()来设|task_struct中的mm和active_mm,q两个mm_struct数据与进E所兌的内存空间相? 应。如果do_fork()时指定了CLONE_VM开养Icopy_mm()把新的task_struct中的mm和active_mm讄成与 current的相同,同时提高该mm_struct的用者数目(mm_struct::mm_usersQ。也是_轻量U进E与父进E共享内存地 址I间Q由下图C意可以看出mm_struct在进E中的地位:
2.CLONE_FS
task_struct 中利用fsQstruct fs_struct *Q记录了q程所在文件系l的根目录和当前目录信息Qdo_fork()时调用copy_fs()复制了这个结构;而对于轻量q程则仅增加fs- >count计数Q与父进E共享相同的fs_struct。也是_轻量U进E没有独立的文gpȝ相关的信息,q程中Q何一个线E改变当前目录? 根目录等信息都将直接影响到其他线E?/p>
3.CLONE_FILES
一 个进E可能打开了一些文Ӟ在进E结构task_struct中利用filesQstruct files_struct *Q来保存q程打开的文件结构(struct fileQ信息,do_fork()中调用了copy_files()来处理这个进E属性;轻量U进E与父进E是׃n该结构的Qcopy_files() 时仅增加files->count计数。这一׃n使得MU程都能讉Kq程所l护的打开文gQ对它们的操作会直接反映到进E中的其他线E?/p>
4.CLONE_SIGHAND
? 一个Linuxq程都可以自行定义对信号的处理方式,在task_struct中的sigQstruct signal_structQ中使用一个struct k_sigactionl构的数l来保存q个配置信息Qdo_fork()中的copy_sighand()负责复制该信息;轻量U进E不q行复制Q而仅 仅增加signal_struct::count计数Q与父进E共享该l构。也是_子进E与父进E的信号处理方式完全相同Q而且可以怺更改?/p>
do_fork()中所做的工作很多Q在此不详细描述。对于SMPpȝQ所有的q程fork出来后,都被分配C父进E相同的cpu上,一直到该进E被调度时才会进行cpu选择?/p>
?
Linux支持轻量U进E,但ƈ不能说它支持核心U程Q因为Linux?U程"?q程"实际上处于一个调度层ơ,׃n一个进E标识符I间Q这U?
限制使得不可能在Linux上实现完全意义上的POSIXU程机制Q因此众多的LinuxU程库实现尝试都只能可能实现POSIX的绝大部分语义,q在
功能上尽可能D?/p>
![]() ![]() |
![]()
|
LinuxThreads 是目前Linuxq_上用最为广泛的U程库,由Xavier Leroy (Xavier.Leroy@inria.fr)负责开发完成,q已l定在GLIBC中发行。它所实现的就是基于核心轻量q程?一对一"U程模型Q一 个线E实体对应一个核心轻量q程Q而线E之间的理在核外函数库中实现?/p>
LinuxThreads 定义了一个struct _pthread_descr_struct数据l构来描q线E,q用全局数组变量__pthread_handles来描q和引用q程所辖线E。在 __pthread_handles中的前两,LinuxThreads定义了两个全局的系l线E:__pthread_initial_thread 和__pthread_manager_threadQƈ用__pthread_main_thread表征 __pthread_manager_thread的父U程Q初始ؓ__pthread_initial_threadQ?/p>
struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它一个元素,实际 上,__pthread_manager_thread是一个特D线E,LinuxThreads仅用了其中的errno、p_pid? p_priority{三个域。而__pthread_main_thread所在的铑ֈ进E中所有用LE串在了一赗经q一pd pthread_create()之后形成的__pthread_handles数组如下图所C:
新创建的U程首先在__pthread_handles数组中占据一,然后通过数据l构中的链指针连入以__pthread_main_thread为首指针的链表中。这个链表的使用在介l线E的创徏和释攄时候将提到?/p>
LinuxThreads 遵@POSIX1003.1c标准Q其中对U程库的实现q行了一些范围限Ӟ比如q程最大线E数Q线E私有数据区大小{等。在LinuxThreads? 实现中,基本遵@q些限制Q但也进行了一定的改动Q改动的势是放松或者说扩大q些限制Qɾ~程更加方便。这些限定宏主要集中? sysdeps/unix/sysv/linux/bits/local_lim.hQ不同^C用的文g位置不同Q中Q包括如下几个:
? q程的私有数据key敎ͼPOSIX定义_POSIX_THREAD_KEYS_MAX?28QLinuxThreads使用 PTHREAD_KEYS_MAXQ?024Q私有数据释放时允许执行的操作数QLinuxThreads与POSIX一_定义 PTHREAD_DESTRUCTOR_ITERATIONS?Q每q程的线E数QPOSIX定义?4QLinuxThreads增大?024 QPTHREAD_THREADS_MAXQ;U程q行栈最空间大,POSIX未指定,LinuxThreads使用 PTHREAD_STACK_MINQ?6384Q字节)?/p>
" 一对一"模型的好处之一是线E的调度由核心完成了Q而其他诸如线E取消、线E间的同步等工作Q都是在核外U程库中完成的。在LinuxThreads中, 专门为每一个进E构造了一个管理线E,负责处理U程相关的管理工作。当q程W一ơ调用pthread_create()创徏一个线E的时候就会创? Q__clone()Qƈ启动理U程?/p>
在一个进E空间内Q管理线E与其他U程之间通过一?理道 Qmanager_pipe[2]Q?来通讯Q该道在创建管理线E之前创建,在成功启动了理U程之后Q管理管道的ȝ和写端分别赋l两个全局变量 __pthread_manager_reader和__pthread_manager_requestQ之后,每个用户U程都通过 __pthread_manager_request向管理线E发hQ但理U程本nq没有直接? __pthread_manager_readerQ管道的ȝQmanager_pipe[0]Q是作ؓ__clone()的参C一传给理U程的, 理U程的工作主要就是监听管道读端,q对从中取出的请求作出反应?/p>
创徏理U程的流E如下所C:
Q全局变量pthread_manager_request初gؓ-1Q?
? 始化l束后,在__pthread_manager_thread中记录了轻量U进E号以及核外分配和管理的U程idQ? 2*PTHREAD_THREADS_MAX+1q个数g会与M常规用户U程id冲突。管理线E作为pthread_create()的调用者线E的 子线E运行,而pthread_create()所创徏的那个用LE则是由理U程来调用clone()创徏Q因此实际上是管理线E的子线E。(此处? U程的概念应该当作子q程来理解。)
__pthread_manager()是理U程的主循环所在,在进行一pd初始 化工作后Q进入while(1)循环。在循环中,U程?Uؓtimeout查询Q__poll()Q管理管道的ȝ。在处理h前,查其父线E(也就 是创建manager的主U程Q是否已退出,如果已退出就退出整个进E。如果有退出的子线E需要清理,则调用pthread_reap_children ()清理?/p>
然后才是d道中的hQ根据请求类型执行相应操作(switch-caseQ。具体的h处理Q源码中比较清楚Q这里就不赘qC?/p>
在LinuxThreads中,理U程的栈和用LE的栈是分离的,理U程在进E堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作qq行栈?/p>
? LE的栈分配办法随着体系l构的不同而不同,主要Ҏ两个宏定义来区分Q一个是NEED_SEPARATE_REGISTER_STACKQ这个属性仅 在IA64q_上用;另一个是FLOATING_STACK宏,在i386{少数^C使用Q此时用LE栈ql决定具体位|ƈ提供保护。与此同Ӟ 用户q可以通过U程属性结构来指定使用用户自定义的栈。因幅所限,q里只能分析i386q_所使用的两U栈l织方式QFLOATING_STACK方式 和用戯定义方式?/p>
在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配
8MBI间Qi386pȝ~省的最大栈I间大小Q如果有q行限制QrlimitQ,则按照运行限制设|)Q用mprotect()讄其中W一ؓ非访
问区。该8MI间的功能分配如下图Q?/p>
低地址被保护的面用来监测栈溢出?/p>
对于用户指定的栈Q在按照指针对界后,讄U程栈顶Qƈ计算出栈底,不做保护Q正性由用户自己保证?/p>
不论哪种l织方式Q线E描q结构L位于栈顶紧邻堆栈的位|?/p>
每个LinuxThreadsU程都同时具有线Eid和进EidQ其中进Eid是内核所l护的进E号Q而线Eid则由LinuxThreads分配和维护?/p>
__pthread_initial_thread 的线Eid为PTHREAD_THREADS_MAXQ__pthread_manager_thread的是 2*PTHREAD_THREADS_MAX+1Q第一个用LE的U程id为PTHREAD_THREADS_MAX+2Q此后第n个用LE的U程 id遵@以下公式Q?/p>
tid=n*PTHREAD_THREADS_MAX+n+1 |
q种分配方式保证了进E中所有的U程Q包括已l退出)都不会有相同的线EidQ而线Eid的类型pthread_t定义为无W号长整型(unsigned long intQ,也保证了有理qq行旉内线Eid不会重复?/p>
从线Eid查找U程数据l构是在pthread_handle()函数中完成的Q实际上只是线E号按PTHREAD_THREADS_MAX取模Q得到的是该线E在__pthread_handles中的索引?/p>
在pthread_create ()向管理线E发送REQ_CREATEh之后Q管理线E即调用pthread_handle_create()创徏新线E。分配栈、设|thread 属性后Q以pthread_start_thread()为函数入口调用__clone()创徏q启动新U程。pthread_start_thread ()d自n的进Eid号存入线E描q结构中QƈҎ其中记录的调度方法配|调度。一切准备就l后Q再调用真正的线E执行函敎ͼq在此函数返回后调用 pthread_exit()清理现场?/p>
׃Linux内核的限制以及实现难度等{原因,LinuxThreadsq不是完全POSIX兼容的,在它的发行README中有说明?/p>
1)q程id问题
q个不是最关键的不I引v的原因牵涉到LinuxThreads?一对一"模型?/p>
Linux 内核q不支持真正意义上的U程QLinuxThreads是用与普通进E具有同样内核调度视囄轻量U进E来实现U程支持的。这些轻量q程拥有独立的进 EidQ在q程调度、信号处理、IO{方面n有与普通进E一L能力。在源码阅读者看来,是Linux内核的clone()没有实现? CLONE_PID参数的支持?/p>
在内核do_fork()中对CLONE_PID的处理是q样的:
if (clone_flags & CLONE_PID) { |
q段代码表明Q目前的Linux内核仅在pid?的时候认可CLONE_PID参数Q实际上Q仅在SMP初始化,手工创徏q程的时候才会用CLONE_PID参数?/p>
按照POSIX定义Q同一q程的所有线E应该共享一个进Eid和父q程idQ这在目前的"一对一"模型下是无法实现的?/p>
2)信号处理问题
׃异步信号是内总q程为单位分发的Q而LinuxThreads的每个线E对内核来说都是一个进E,且没有实?U程l?Q因此,某些语义不符合POSIX标准Q比如没有实现向q程中所有线E发送信PREADMEҎ作了说明?/p>
? 果核心不提供实时信号QLinuxThreads用SIGUSR1和SIGUSR2作ؓ内部使用的restart和cancel信号Q这样应用程序就 不能使用q两个原本ؓ用户保留的信号了。在Linux kernel 2.1.60以后的版本都支持扩展的实时信P从_SIGRTMIN到_SIGRTMAXQ,因此不存在这个问题?/p>
某些信号的缺省动作难以在现行体系上实玎ͼ比如SIGSTOP和SIGCONTQLinuxThreads只能一个线E挂P而无法挂h个进E?/p>
3)U程L问题
LinuxThreads每个进E的U程最大数目定义ؓ1024Q但实际上这个数D受到整个pȝ的总进E数限制Q这又是׃U程其实是核心进E?/p>
在kernel 2.4.x中,采用一套全新的总进E数计算ҎQ得总进E数基本上仅受限于物理内存的大小Q计公式在kernel/fork.c的fork_init()函数中:
max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 8 |
在i386 上,THREAD_SIZE=2*PAGE_SIZEQPAGE_SIZE=2^12Q?KBQ,mempages=物理内存大小/PAGE_SIZEQ? 对于256M的内存的机器Qmempages=256*2^20/2^12=256*2^8Q此时最大线E数?096?/p>
但ؓ了保证每个用P除了rootQ的q程L不至于占用一半以上物理内存,fork_init()中l指定:
init_task.rlim[RLIMIT_NPROC].rlim_cur = max_threads/2; |
q些q程数目的检查都在do_fork()中进行,因此Q对于LinuxThreads来说Q线EL同时受这三个因素的限制?/p>
4)理U程问题
理U程Ҏ成ؓ瓉Q这是这U结构的通病Q同Ӟ理U程又负责用LE的清理工作Q因此,管理U程已经屏蔽了大部分的信P但一旦管理线E死亡,用户U程׃得不手工清理了,而且用户U程q不知道理U程的状态,之后的线E创建等h无人处理?/p>
5)同步问题
LinuxThreads中的U程同步很大E度上是建立在信号基上的Q这U通过内核复杂的信号处理机制的同步方式Q效率一直是个问题?/p>
6Q其他POSIX兼容性问?/b>
Linux中很多系l调用,按照语义都是与进E相关的Q比如nice、setuid、setrlimit{,在目前的LinuxThreads中,q些调用都仅仅媄响调用者线E?/p>
7Q实时性问?/b>
U程的引入有一定的实时性考虑Q但LinuxThreads暂时不支持,比如调度选项Q目前还没有实现。不仅LinuxThreads如此Q标准的Linux在实时性上考虑都很?/p>
![]() ![]() |
![]()
|
LinuxThreads 的问题,特别是兼Ҏ上的问题,严重ȝ了Linux上的跨^台应用(如ApacheQ采用多U程设计Q从而得Linux上的U程应用一直保持在比较? 的水q뀂在LinuxC中,已经有很多h在ؓ改进U程性能而努力,其中既包括用LU程库,也包括核心和用L配合改进的线E库。目前最Zh看好的有 两个目Q一个是RedHat公司牵头研发的NPTLQNative Posix Thread LibraryQ,另一个则是IBM投资开发的NGPTQNext Generation Posix ThreadingQ,二者都是围l完全兼容POSIX 1003.1cQ同时在核内和核外做工作以而实现多对多U程模型。这两种模型都在一定程度上弥补了LinuxThreads的缺点,且都是重L灶全新设 计的?/p>
1.NPTL
NPTL的设计目标归U_归纳Z下几点:
? 技术实CQNPTL仍然采用1:1的线E模型,q合glibc和最新的Linux Kernel2.5.x开发版在信号处理、线E同步、存储管理等多方面进行了优化。和LinuxThreads不同QNPTL没有使用理U程Q核心线E? 的管理直接放在核内进行,q也带了性能的优化?/p>
主要是因为核心的问题QNPTL仍然不是100%POSIX兼容的,但就性能而言相对LinuxThreads已经有很大程度上的改q了?/p>
2.NGPT
IBM的开放源码项目NGPT?003q??0日推ZE_?.2.0版,但相关的文档工作q差很多。就目前所知,NGPT是基于GNU PthQGNU Portable ThreadsQ项目而实现的M:N模型Q而GNU Pth是一个经典的用户U线E库实现?/p>
按照2003q?月NGPT官方|站上的通知QNGPT考虑到NPTL日益q泛Cؓ人所接受Qؓ避免不同的线E库版本引v的乱,今后不再进行进一步开发,而今q行支持性的l护工作。也是_NGPT已经攑ּ与NPTL竞争下一代Linux POSIXU程库标准?/p>
3.其他高效U程机制
?
处不能不提到Scheduler
Activations。这?991q在ACM上发表的多线E内核结构媄响了很多多线E内核的设计Q其中包括Mach3.0、NetBSD和商业版?
Digital UnixQ现在叫Compaq True64
UnixQ。它的实质是在用用LU程调度的同Ӟ可能地减少用户U对核心的系l调用请求,而后者往往是运行开销的重要来源。采用这U结构的U程?
Ӟ实际上是l合了用LU程的灵z高效和核心U线E的实用性,因此Q包括Linux、FreeBSD在内的多个开放源码操作系l设计社区都在进行相关研
IӞ力图在本pȝ中实现Scheduler Activations?/p>
![]() |
||
|
![]() |
杨沙zԌ目前在国防科技大学计算机学院攻读Y件方向博士学位?/p> |
按照教科书上的定义,q程是资源管理的最单位,U程是程序执行的最单位。在操作pȝ设计上,从进E演化出U程Q最主要的目的就是更好的支持SMP以及减小Q进E?U程Q上下文切换开销?/b>
? 论按照怎样的分法,一个进E至需要一个线E作为它的指令执行体Q进E管理着资源Q比如cpu、内存、文件等{)Q而将U程分配到某个cpu上执行。一? q程当然可以拥有多个U程Q此Ӟ如果q程q行在SMP机器上,它就可以同时使用多个cpu来执行各个线E,辑ֈ最大程度的q行Q以提高效率Q同Ӟ即 是在单cpu的机器上Q采用多U程模型来设计程序,正如当年采用多进E模型代替单q程模型一P使设计更z、功能更完备Q程序的执行效率也更高,例如? 用多个线E响应多个输入,而此时多U程模型所实现的功能实际上也可以用多进E模型来实现Q而与后者相比,U程的上下文切换开销比q程要小多了Q从语义? 来说Q同时响应多个输入这L功能Q实际上是׃n了除cpu以外的所有资源的?/p>
针对U程模型的两大意义,分别开发出了核 心U程和用LU程两种U程模型Q分cȝ标准主要是线E的调度者在核内q是在核外。前者更利于q发使用多处理器的资源,而后者则更多考虑的是上下文切? 开销?/b>在目前的商用pȝ中,通常都将两者结合v来用,既提供核心线E以满smppȝ的需要,也支持用U程库的方式在用h实现另一套线E机Ӟ此时一 个核心线E同时成为多个用h线E的调度者。正如很多技术一P"混合"通常都能带来更高的效率,但同时也带来更大的实现难度,Z"?的设计思\Q? Linux从一开始就没有实现混合模型的计划,但它在实C采用了另一U思\?混合"?/p>
在线E机制的具体实现上,可以? 操作pȝ内核上实现线E,也可以在核外实现Q后者显然要求核内至实Cq程Q而前者则一般要求在核内同时也支持进E。核心U程模型昄要求前者的? 持,而用LU程模型则不一定基于后者实现。这U差异,正如前所qͼ是两U分cL式的标准不同带来的?/p>
当核内既支持q程? 支持U程Ӟ可以实现线E?q程?多对?模型Q即一个进E的某个U程由核内调度,而同时它也可以作为用LU程池的调度者,选择合适的用户U线E在 其空间中q行。这是前面提到?混合"U程模型Q既可满_处理机系l的需要,也可以最大限度的减小调度开销。绝大多数商业操作系l(如Digital Unix、Solaris、IrixQ都采用的这U能够完全实现POSIX1003.1c标准的线E模型。在核外实现的线E又可以分ؓ"一对一"?多对 一"两种模型Q前者用一个核心进E(也许是轻量进E)对应一个线E,线E调度等同于q程调度Q交l核心完成,而后者则完全在核外实现多U程Q调度也在用 h完成。后者就是前面提到的单纯的用LU程模型的实现方式,昄Q这U核外的U程调度器实际上只需要完成线E运行栈的切换,调度开销非常,但同时因 为核心信P无论是同步的q是异步的)都是以进Eؓ单位的,因而无法定位到U程Q所以这U实现方式不能用于多处理器系l,而这个需求正变得来大Q因 此,在现实中Q纯用户U线E的实现Q除法研究目的以外Q几乎已l消׃?/p>
Linux内核只提供了轻量q程的支持,限制?
更高效的U程模型的实玎ͼ但Linux着重优化了q程的调度开销Q一定程度上也I补了q一~陷。目前最行的线E机制LinuxThreads所采用的就
是线E?q程"一对一"模型Q调度交l核心,而在用户U实C个包括信号处理在内的U程理机制。Linux-LinuxThreads的运行机制正是本
文的描述重点?/p>