OSKit

目 录

  1. OSKit
    1. 线程机制
      1. 前言
      2. 第一章 绪 论
      3. 第二章 线程初始化
      4. 第三章 线程通信分析
      5. 第四章 线程调度
      6. 第五章 OSKit的应用实例
      7. 第六章 结束语
    2. 部件对象模型
      1. 前言
      2. 第一章 OSKIT介绍
      3. 第二章 COM规范介绍
      4. 第三章 COM机制的实现
      5. 第四章 接口注册机制
      6. 第五章 启动及系统初始化
      7. 第六章 应用实例
      8. 第七章 结束语
    3. 包装系统驱动程序
      1. 前 言
      2. 第一章 OSKit概述
      3. 第二章 OS Environment概述
      4. 第三章 OS Environment包装Linux驱动程序的方法分析
      5. 第四章 OS Environment自行开发驱动程序的方法
      6. 第五章 OSKit的应用实例
      7. 第六章 结束语


OSKit

  OSKIT是美国犹它大学计算机科学系FLUX研究组编写的一套用于架构操作系统内核、服务器和其他OS级软件的框架及模块化的部件和库程序。OSKIT的编写者认为,操作系统中有很大一部分模块是系统必须的,但并不是开发者所感兴趣的,例如系统装入模块,各种标准驱动模块等。使用OSKIT的目的就是使操作系统的开发者集中精力开发他们操作系统中有特色的,或者他们感兴趣的部分,而不必考虑一些繁琐而乏味的细节。为了达到这个目的,OSKit在设计时借用了COM的思想,把操作系统的各个部分设计成尽量独立的COM模块,以方便操作系统的开发者使用或替换。因此,当开发人员使用这套工具时,可以把它当作一个完整的操作系统来使用,也可以根据需要使用其中的一部分,它还可以作为一套动态链接库,由操作系统及支持程序对它进行调用。
[目录]


线程机制

OSKit的线程机制 目录
2000年12月12日 23:07


作 者: 汤海京


导师:陈朔鹰

前 言

第一章 绪 论
 § 1.1 简介
 § 1.2 安装与配置OSKIT
 § 1.3 使用OSKIT
 § 1.4 OSKIT 导航图
    1.4.1 接口
    1.4.2 函数库
    1.4.3 部件库
    1.4.4 执行环境

第二章 线程初始化
 § 2.1 线程初始化分析
    2.1.1 线程的创建
    2.1.2 线程的存储
    2.1.3 线程的数据结构
 § 2.2 PTHREADS/PTHREAD_CREATE.C
    2.2.1 创建线程
    2.2.2 创建内部线程
    2.2.3 为主进程创建一个备份线程
    2.2.4 初始化创建线程的线程
    2.2.5 为等待和休眠的线程创建一个等待时间
 § 2.3 PTHREADS/PTHREAD_ATTR.C
    2.3.1 线程初始化的数据结构
    2.3.2 线程属性的撤销
    2.3.3 设置线程的警戒堆栈大小
    2.3.4 得到警戒堆栈的大小
    2.3.5 设置线程的分离状态
    2.3.6 得到线程的分离状态
    2.3.7 设置线程的继承关系
    2.3.8 得到线程的继承状态
    2.3.9 设置线程的调度参数
    2.3.10 得到线程调度的参数
    2.3.11 设置线程调度策略
    2.3.12 得到线程调度策略
    2.3.13 设置堆栈地址
    2.3.14 得到堆栈的地址
    2.3.15 设置堆栈的大小
    2.3.16 得到堆栈的大小
    2.3.17 设置线程的优先级
    2.3.18 准备接受调度
    2.3.19 声明要进行调度的线程
    2.3.20 设置mutex的属性
    2.3.21 撤销mutex的属性
    2.3.22 设置muxtex的协议
    2.3.23 得到线程调度的mutex协议
    2.3.24 设置mutex的种类
    2.3.25得到mutex的类型
 本 章 小 结

第三章 线程通信分析
 § 3.1 线程间通信
    3.1.1消息队列
    3.1.2信号
 § 3.2 PTHREADS/PTHREAD_IPC.C
    3.2.1 消息发送:
    3.2.2 send的算法
    3.2.3 消息接收
    3.2.4 receive的算法
 § 3.3 PTHREADS/PTHREAD_SIGNAL.C
    3.3.1 检测并更改阻塞的信号
    3.3.2杀线程信号
    3.3.3 关线程锁
    3.3.4 在目标线程的信号等待队列中加一个信号
    3.3.5 信号完成的动作
    3.3.6 测试或改变(或两者兼有)主调进程的信号掩码
    3.3.7 杀线程
    3.3.8 等待信号的线程队列
    3.3.9 等待内部信号
    3.3.10 线程等待信号
    3.3.11 线程等待信号(一般指内部信号)
    3.3.12 线程等待信号(有时间限制)
    3.3.13 线程等待信号,如果无信号发给它,则无限期等待
    3.3.14 从一个陷入发送信号给线程
    3.3.15 发出线程阻塞信号
    3.3.16 不会被阻塞的信号发送
    3.3.17 信号初始化
 § 3.4 PTHREADS/PTHREAD_COND.C
    3.4.1条件变量的初始化数据结构
    3.4.2撤销条件变量
    3.4.3 线程等待条件变量
    3.4.4 安全的等待
    3.4.5 等待条件变量,但有时间限制
    3.4.6 等待捐赠条件
    3.4.7 等待条件超时
    3.4.8 线程发送信号条件
    3.4.9 线程广播条件
 本 章 小 结

第四章 线程调度
 § 4.1 线程调度算法分析
    4.1.1 线程调度算法的总体描述
    4.1.2 优先级逆转法
 § 4.2 PTHREAD/PTHREAD_SCHEDULER.C
    4.2.1清空等待调度的线程队列
    4.2.2得到等待队列中优先级最高的线程的优先级
    4.2.3得到指向下一个要执行线程的指针
    4.2.4在线程的执行队列的队尾追加一个线程
    4.2.5在线程的执行队列的队头追加一个线程
    4.2.6将等待队列中优先级最高的线程提出队列执行
    4.2.7从等待队列中删除独占资源的线程
    4.2.8此时用到的调度算法
    4.2.9将一个线程变为运行态
    4.2.10终止该线程目前的调度算法
    4.2.11为一个新的线程创建调度参数
    4.2.12改变线程的调度状态
    4.2.13优先级迁移
    4.2.14基于某种原因,线程被送回等待队列
    4.2.15返回优先级最高的处于等待状态的线程
    4.2.16改变线程的优先级
    4.2.17优先级继承
    4.2.18不继承优先级
    4.2.19优先级递减
 本 章 小 结

第五章 OSKIT的应用实例 一个简单系统的设计与实现
 5.1设计目的
 5.2系统的功能
 5.3 我们自己所完成的工作:
    5.3.1 系统的启动
    5.3.2 线程管理
    5.3.3 外设(串口)
 5.4 用户手册
    5.4.1 安装
    5.4.2 用户编程接口
    5.4.3 应用程序示例
 本 章 小 结

第六章 结束语

后 记

[目录]


前言

OSKit的线程机制 前言
2000年12月18日 22:28


作 者: 汤海京


导师:陈朔鹰


课题名称:基于面向对象操作系统开发平台(OSKit)的分析与程序设计
课题来源:自拟题目

如果说自由软件的出现是一个偶然的话,那么,席卷全球的Linux热潮则是一个奇迹,它正以势不可挡的趋势迅猛发展,其前途不可限量。

Linux内核源代码的开放给希望深入操作系统内部世界的人们提供了可能,但随之而来的问题是,当我们要开发自己的操作系统时,由谁来读系统的kernel部分呢。对这部分的处理从逻辑上分析不外乎三种方式:全部保留、对其进行裁减、全部推倒重来。很显然,最后一种方法是不可能的,而如果我们采用的是第一种方法,其结果当然一定可以满足我们的要求,但是,最后编译出来的核心将十分的庞大,尤其是对嵌入式操作系统的开发者来说,是不能忍受的,所以,大多数开发者采用的第二条路。

但是,第二条路也非平坦的大道,道理很简单,你要想对kernel进行裁减,首先你应该将全部的源代码阅读一遍,并且将其中的相关性理顺,然后才能谈到裁减,所以工作量也十分的庞大。

然而,OSKit的出现改变了这一切,它使得我们不需要将精力集中在kernel源代码的阅读上,因为kernel部分的源程序已经由OSKit的开发人员替你分析过了,他们将源码全部模块化,并将所有模块之间的相关性写在了文档之中,呈现在你的面前,这与你自己分析源代码的结果是一样的。

OSKit最本质的东西和Linux一样,体现在"自由"和"开放"的思想,"自由"意味着世界范围内的知识共享,由于OSKit出现在Linux之后,其设计思想继承了Linux的精髓,所以说它的出现并不完全是美国犹它大学计算机科学系FLUX研究组的功劳,而应该是"自由"的结果。"开放"则意味着OSKit对所有的人都敞开大门,在这种开放而自由的天地里,你可以中分发挥自己的创造才能。

在后面的几章里,我将向大家系统地介绍我的全部研究成果-OSKit的线程机制,希望读者能在最短的时间内了解OSKit,更希望大家能本着"自由"与"开放"的精神加入到我们的研究中来,为中国的自由软件事业贡献自己的一份热情。

我写这篇论文力求达到一个目的,那就是让这篇论文在搞研究的人眼里是一本参考书,而在程序设计者眼中是一本函数手册,既有理论指导,又不是泛泛的空谈理论。

由于本人能力有限,论文中的有些术语表达可能不妥,内容也可能不够准确,敬请各位老师和同学批评指正,本人不胜感激。


汤海京 2000年6月14日

[目录]


第一章 绪 论

OSKit的线程机制 第一章
2000年12月18日 22:34


作 者: 汤海京


导师:陈朔鹰


第一章 绪 论


1.1 简介

OSKit是由美国犹它大学计算机科学系FLUX研究组编写的一套模块化部件和库函数,用于架构操作系统内核、服务器以及其他的OS级软件。我们设想一下,在一个操作系统的研发项目中,底层模块的开发工作会占去大部分时间,并耗费掉开发人员的大部分精力。而OSKit的出现恰恰弥补了这个缺陷,其设计意图是提供一套可重用的模块,让使用者避开复杂的底层,把精力集中在他们感兴趣的问题上,也就是说,当开发人员拿到OSKit之后,便立刻拥有了一个完整而且安全的核心,使他们可以集中精力研发操作系统的高层次问题,如作业控制、虚存、IPC、文件系统、系统安全以及高级语言(如Java、Lisp或ML)等。这样可以大大丰富操作系统的应用层,为用户提供更多更好的服务,提高操作系统的运行效率,增强操作系统的安全性和稳定性,从而使你的操作系统更加具有魅力。

对于站在操作系统技术最前沿的多线程编程和成熟的作业控制系统,以及时下最流行的嵌入式操作系统,OSKit都提供了支持。通过几个月以来对美国犹它大学计算机科学系FLUX研究组网站的追踪,我们注意到OSKit的版本大约每三个月就更新一次,我们在毕业设计初期拿到的是99年7月的版本,而我们写演示程序是用的2000年5月的版本,在此期间又出现了99年12月版和2000年2月版,而且在每次发布的版本里都有许多新的算法公布,还有许多老版本中的BUG被修改;这说明OSKit不但一直处于操作系统开发平台的最前沿,而且其自身也在不断的完善。

通过对OSKit深入细致的分析与研究,我们发现犹他大学的开发人员们从一开始就确定了自己的目标,那就是对OSKit进行模块化,仿佛Windows中的动态连接库一样,让后来者即使不使用OSKit中的某一部分,仍然可以使用其余部分来完成他们的目标。这样的设计思路十分灵活,为开发者和使用者都提供了便利。


1.2 安装与配置OSKit

当我们从犹他大学得到OSkit的五月版时,它是个压缩包,使用tar在linux下解开后,就得到了源码,这需要我们在linux下重新编译。

目前,编译OSKit需要以下的工具:

GNU make
GNU CC (gcc) 2.95.2版。

注意:到目前为止,我们使用的Redhat Linux 6.2中的GCC并不是2.95.2,如果要成功编译OSKit,需要先将我们的GCC升级。当然,如果你使用的是Turbo linux 6.0的话,它提供的就是GCC 2.95.2。

在开始编译OSKit之前,我们必须注意一点,那就是要对源码进行一点改动,让它支持实时操作,即将 /oskit20000505/threads/MakeFlage 中的:
# OSKIT_CFLAGS + = -DPTHREAD_SCHED_EDF -DPTHREAD_REALTIME 中的注释符号 # 去掉。

由于我们的环境是Linux,所以论文中的根目录用"/"表示,而不是Windows中的"\"。

由于我们是在intel 586上编译,而我们的目标机要支持到intel 386,所以我们应该在OSKit的目录下键入:

./configure ――host=i586 ――target=i386
此后键入make,编译OSKit。

当OSKit编译好以后,你可以用"make install"命令来安装它。在缺省的情况下,这些库会被安装到/usr/local/lib中,而头文件会被安装到/usr/local/include中,除非你在配置时使用了--prefix选项来指定它。所有的OSKit头文件都安装在oskit/子目录中(例如/usr/local/include/oskit),不会和已经存在的任何头文件冲突。即使是库被安装在主库的目录中(如/usr/local/lib),所有的库文件都有前缀oskit_,这可以避免和其它不相关的库混淆在一起。


1.3 使用OSKit

一开始,为了能让大家尽快熟悉OSKit,犹他大学的研发人员们给出了许多很具代表性的例子供使用者进行试验,而我们的课题组也编写了一个小型的操作系统,供学习研究之用。

OSKit中库的设计是很经典的,开发者们花费了大量时间清楚地标出了每个库与其它库的依赖关系,这使得OSKit不但保留了操作系统的原汁原味,而且还把库之间的相关性降低到了最低限度,使得使用者可以随意增减其中的库函数。实际上,在很多情况下,特别是在某些函数库中,为了有效的使用OSKit,必须替换掉其中的一些函数或符号,以适应我们的执行环境。要重载某个库中的函数或者符号,只要在你的核心或应用程序中再定义一遍就可以了,LINK程序会确保去使用你所定义的函数和符号。由于在Linux下的编译器是靠读取Makefile文件来确定编译连接的对象以及规则,所以我们认为直接修改Makefile文件要比修改OSKit源码更加方便而且安全,这与Windows下的工程文件十分相似。

如果使用OSKit所提供的模块化部件和库函数编写出了自己的操作系统核心代码,并且对在LINUX下用GCC编译源码有所了解的话,就可以借助OSKit为使用者提供的一整套核心制作命令和LINUX提供的GCC来生成自己的核心文件zImage,然后使用LINUX命令DD将核心写入一张1.44MB的软盘,重新启动计算机,从软盘引导就可进入我们自己编写的操作系统了。

另外,考虑到用户环境复杂多样的实际情况,OSKit还提供了对许多种操作系统的支持,包括Linux、Mach 、BSD或者MS-DOS,当我们用GCC将源码编译成"filename.o"的格式之后,使用OSKit提供的不同的核心制作命令来生成不同操作系统下的核心,其具体实现是由mkbsdimage、mklinuximage和mkdosimage来完成的。这些脚本在OSKit的安装和配置阶段会根据你指定的环境自动安装完毕。在每一个脚本运行的时候都有各自不同的参数。


1.4 OSKit 导航图

OSKit所提供的功能被分成三类:接口、函数库和部件库

1.4.1 接口

OSKit的接口非常清晰,并且是面向对象的。这些接口全部被定义在C语言的头文件中,我们分别对其加了必要的注释,以方便使用者快速掌握OSKit。这些接口被OSKit的各个部件共享,从而体现了各部件之间的一致性。操作系统的开发人员既可以直接使用它,也可以在其上定义目标操作系统的接口结构。

1.4.2 函数库

OSKit的函数库以C语言中面向函数的方式提供基本的服务。一个库中的函数对另一个库中函数的依赖关系已经被降至最低的程度,如果这种依赖是不可避免的话,那他们已经被很清楚地标注出来,展现给了操作系统的开发人员。函数库很少使用OSKit的面向对象的COM接口,取而代之的是在C的头文件中定义自己的面向函数的接口。这种设计策略为操作系统开发人员提供了灵活性和极大的技术支持;要适应任何特殊的操作系统环境,底层的OSKit工具必须具有可扩展性。

1.4.3 部件库

OSKit的部件库使使用者能站在更高的层次来编程,即面向对象的"黑盒"方式。尽管OSKit的"部件"在缺省情况下也是被打包成为普通的链接库,但它们的结构则是被设计程面向对象的,而不是传统的面向函数的方式。与OSKit的函数库相比,部件库通常只对外开放一些相关的公用调用接口,而不是大量的功能。例如,在Linux和BSD的驱动程序部件库里,每一个完整的驱动程序仅呈现为一个单独的函数调用,这个调用用来初始化并注册驱动程序。

1.4.4 执行环境

OSKit中的许多部件在核心和用户方式下都可以使用,这就需要对部件的执行环境作出定义,例如部件什么时候可以嵌套进入等。此外,OSKIT使用了许多其它操作系统的代码,例如设备驱动程序和网络协议栈,都是原封不动的从原有的核心如BSD和Linux中借用来的,OSKit通过附加代码模拟原始执行环境使的这些执行模块比它们原始执行环境更简单,用户也不需要详细了解原执行环境的细节。下面对OSKIT的每种执行模块进行简单的介绍。

纯执行模块:这是OSKit执行环境中最简单的一个模块,这些部件或函数中没有全局变量或静态变量,只使用和处理由目标环境传递来的数据。例如函数strlen,它只通过目标环境传递给它的字符串指针求出字符串的长度,并将其返回,它只接触参数数据域,而不影响目标环境。当这些函数使用的数据集是分离的,它们可以安全地同时被调用,而不需要同步,反之则不行。例如对重叠的目标缓冲区并发使用memcpy调用时不安全的。

非纯执行模块:这些模块中使用了全局变量或有可能改变全局共享状态,例如liboskit_kern(核心支持库)中的许多函数建立和访问全局处理器寄存器和数据结构,因此它们是非纯执行模块。非纯执行模块有以下特点:非纯函数和部件可能依赖于全局状态,如全局变量,静态变量,处理器中的特殊寄存器等。除非有明确的声明,非纯函数和部件是不可重入的,并且运用在多线程系统中也是不安全的。为了在一个多线程/多处理器环境中使用这些函数和部件,目标操作系统必须提供适当的同步代码。

阻塞模块:它扩展了非纯模块来支持非抢先的多线程,这些模块中有一类可重入的函数称为阻塞函数,在这模块中,除非明确声明为非阻塞函数,否则函数是阻塞的。为了在一个完全抢先的、可中断的或者多处理器的环境中使用阻塞模块,必须在进入模块前加锁,在退出模块时将锁释放。

可中断阻塞模块:在它之中每个部件都是一个单线程的执行域,在一个给定的时刻,只有一个(虚拟的或者物理的)CPU可以执行部件中的代码。例如:在一个多处理器系统中,在进程级别下,任意时刻在一个部件集内只有一个CPU被允许执行;这能够通过在部件前后放置全局锁来实现。

在一段时间内,部件中可以存在多个活动进程,但在某时刻只有一个进程被执行。目标操作系统给每个活动进程提供一个独立堆栈,这个堆栈在阻塞函数运行时被保留。只有在操作完成后,对部件的调用返回时才放弃该堆栈。部件中的代码总是运行在两个级别中之一,进程级或中断级。有意思的是一些部件的函数和方法只能在进程级别被调用,而另一些只能在中断级别被调用,还有的能在任何级别被调用。调用的细节属于接口描述的一部分。

部件中无论进程级别或中断级别的操作都能被部件中的中断处理程序中断,除非代码调用osenv_intr_disable屏蔽了中断。当部件在进程级运行时,OSKIT假定中断开放,部件在处理过程中可能临时屏蔽掉中断,但必须在返回到目标操作系统前重新激活。同样,当部件在中断级运行时,OSKIT假定中断被屏蔽,但是部件可以在目标操作系统允许其它中断级别的活动中断该部件时重新激活中断。

当目标操作系统在一个部件内中断一个进程级别的活动时,在继续这个活动前,操作系统必须执行完这个中断级别的活动。同理,若一个中断级的活动被中断,那么最近的中断级别的活动必须在继续前一个中断级别的活动之前完成。部件中运行在中断级别的代码不能调用目标操作系统提供的阻塞回调函数;只有非阻塞的回调函数能够在中断级别被调用。


[目录]


第二章 线程初始化

《OSKit的线程机制 第二章》


OSKit的线程机制 第二章2000年12月18日22:59


作 者: 汤海京


导师:陈朔鹰


第二章 线程初始化


前面我们曾经谈到,在OSKit中资源分配和调度的单位是线程而不是传统意义上的进程,所以我们有必要花大量的时间对其线程机制做全面的分析。当然,要分析线程部分,首先应该从线程的初始化入手,然后才能涉及到线程间的通信以及线程调度,所以本章将向各位全面系统地介绍OSKit的线程初始化。

为了让大家能清楚地掌握OSKit的线程部分,在以后的三章内,我将采用理论实际相结合的论述方式,即在每章的开始先论述本章所要讨论的线程机制,以期让大家在概念上对本章有总体上的认识,然后再针对OSKit的源代码做进一步的阐述,希望能理论实践双丰收。


2.1 线程初始化分析

2.1.1线程的创建

在OSKit中,用户创建一个新线程的唯一方法就是调用创建线程的线程pthread_create。

内核为系统调用pthread_create完成如下操作:

(1) 为新线程在线程表项中分配一个空槽。
(2) 赋给子线程一个唯一的标识号
(3)制作一个父线程上下文的逻辑副本。由于线程的某些部分,如正文区,可能被几个线程所共享,所以内核只要增加某个区的引用计数即可,而不用真的将该区拷贝到一个新的内存物理区。
(4)增加与该线程相关的文件表和索引节点表的引用数。
(5) 向父线程返回子线程的线程号。

系统对一个用户可以同时运行的线程数有一个限制(这个限制是可以改变的),因而任何用户不能使用过多的线程表项,不然的话,就会妨碍其他用户创建新线程,而且,OSKit为了防止死锁,规定了普通用户不能占用线程表项中的最后一个线程,这条规定是从UNIX中演变而来的。

2.1.2线程的存储

OSKit的线程存储方案与UNIX十分相似,由三个逻辑段组成,它们是正文段、数据段和栈。正文段含有一个线程所执行的指令集合,正文段的地址包括正文地址(用于分支和子程序调用)、数据地址(用于存取全局数据变量)和栈地址(为了存取子程序的数据结构)。

若机器将生成的地址看作物理存储器中的地址,那么,两个线程在它们所生成的地址集合重叠了的情况下,是不可能并发执行的。虽然编译器可以产生在程序之间不重叠的地址,但这样的程序对于通用计算机是行不通的,因为一种机器上的存储容量是有限的,而所有可能被编译的程序集合是无限的。采取此种方法可以在很大程度上避免数据的丢失。

2.1.3线程的数据结构

以下我列出了OSKit中线程的完整的数据结构,其中大部分是从UNIX中继承来的,但也有一些是OSKit自己的,例如,为了制作例子程序所提供的CPU资源分配的部分。

为了使各位一目了然,我对此数据结构做了全部注释,并按照其不同应用领域划分成了几个部分,分别加在了下面的注释之中。

queue_chain_t   runq;        /* 释放队列链 */
pcb_t       *ppcb;        /*指向PCB的指针 */
oskit_u32_t    pstk;        /*指向所分配的堆栈的指针*/
void       *(*func)(void *);  /* 指向执行函数的指针*/
size_t      ssize;        /* 所分配的堆栈的大小*/
size_t      guardsize;      /*警戒堆栈的大小 */
pthread_t     tid;         /* 线程号 */
oskit_u32_tflags;        /* 线程状态标志 */
pthread_lock_t  lock;        /* 锁*/
queue_chain_t   chain;        /* 队列链 */
int        preempt;       /*线程优先级 */
/*以下用于对死锁的处理*/
pthread_mutex_t  mutex;        /* 互斥位(死锁保护)*/
pthread_cond_t  cond;        /* 等待死锁标志=1 */
int        dead;        /*死锁了 */
/*例子所用到的资源*/
int        cputime;       /* 占用CPU的时间 */
intcpticks;       /* 上一秒所占的节拍数 */
oskit_u32_t    pctcpu;       /* CPU的占用率*/
int        childtime;      /* 子线程所占用的CPU的开销*/
/*发送信息所用的变量*/
pthread_lock_t  waitlock;      /* 等待锁*/
oskit_u32_twaitflags;      /* 等待标志位 */
pthread_cond_t  *waitcond;      /* 等待条件变量*/
struct osenv_sleeprec  *sleeprec;  /*一个osenv_sleep中的线程*/
/*以下是进程通讯要用到的*/
void       *msg;        /* 指向要发送的消息的指针*/
oskit_size_t   msg_size;      /* 消息的大小 */
pthread_t     tid;         /*线程号 */
void       *reply;       /* 指向响应信息的指针*/
oskit_size_t   reply_size;     /* 响应内容的大小*/
queue_head_t   senders;       /* 发送队列中的发送者 */
queue_chain_tsenders_chain;    /* 发送队列指针 */
/* 以下是所用到的时钟 */
structoskit_timer *condtimer;
/* 线程的休眠和唤醒是用一个单独的计时器实现的 */
structoskit_timer *sleeptimer;
/* 以下是所用到的信号 */
pthread_lock_t  siglock;       /*保护锁信号 */
sigset_t     sigmask;       /* 阻塞信号*/
sigset_t     sigpending;     /* 未决信号 */
sigset_t     sigwaiting;     /*等待信号的信号 */
oskit_u32_t    eip;         /* 页处理故障信号 */
/* 键值通常是一个固定的队列*/
void       *keyvalues[PTHREAD_KEYS_MAX]; /* 键值 */
/* 以下用于清除操作*/
pthread_cleanup_t *cleanups;     /* 清楚操作链*/
char       cancelstate;     /* 取消状态 */
char       canceltype;     /*取消类型 */
/* 以下是单独用于调度的锁*/
pthread_lock_t  schedlock;
void       *rtai_priv;
intpolicy;       /* 调度策略 */
int        priority;      /* 当前的优先级 */
structscheduler_entry *scheduler;  /* 调度程序入口 */
int        policy;       /* 调度策略*/
int        priority;      /* 当前优先级 */
int        base_priority;    /*初始优先级 */
int        ticks;        /* 调度所省下的节拍*/
oskit_timespec_t start;        /* 下次运行的时间*/
oskit_timespec_t deadline;      /* 下次运行的时间*/
oskit_timespec_t period;       /* 两次执行见的间隔*/
queue_head_t   waiters;       /* 正在等待的线程 */
queue_chain_twaiters_chain;    /* 线程队列链 */
struct pthread_thread *waiting_for;  /* 所等待的线程*/
struct pthread_thread *inherits_from; /* 线程从何继承*/
/*以下是CPU的继承*/
struct pthread_thread *scheduler;   /* 线程调度程序人口*/
schedmsg_t    unblockmsg;
schedmsg_queue_t *msgqueue;
sched_wakecond_t wakeup_cond;/* 唤醒条件 */
schedflags_t   schedflags;     /* 标志 */
intdonate_rc;      /* 从捐赠线程返回的值 */
int        timeout;       /* 毫秒*/
queue_head_t   donors;       /* 捐赠资源的线程 */
queue_chain_tdonors_chain;    /* 捐赠队列链 */
struct pthread_thread *donating_to;  /* 被捐赠的线程*/
struct pthread_thread *inherits_from; /* 线程从何继承 */
structpthread_thread *nextup;    /* 下一个要执行的线程 */


2.2 pthreads/pthread_create.c

此源码文件包括了一套完整的线程创建机制,它是全部线程的根源所在,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程创建的。我认为在上一节的理论指导下对代码进行分析,要比泛泛的阐述理论更容易让读者接受,更能加深在读者脑海中的印象。

2.2.1 创建线程
说明:这就是我们在第一节中提到的创建线程的函数,所有由用户完成的创建线程的操作,都要调用它来实现。
intpthread_create ( pthread_t *tid, const pthread_attr_t *attr,
void * (*function )( void * ), void *argument )
tid: 指向线程存储位置的指针
attr:指向线程属性的指针
*(*function)(void *): 当线程初始化时所调用的函数
*argument: 函数功能

2.2.2创建内部线程
说明:该函数用于系统核心部分创建核心线程,而一般的用户没有调用它的权利,只有系统核心才可以调用。
pthread_thread_t*pthread_create_internal ( void * ( *function)( void *), void *argument,constpthread_attr_t *attr)
注:内部线程创建时,将阻塞所有的信号
sigfillset(pthread->sigmask)

2.2.3为主进程创建一个备份线程
说明:出于系统安全的原因,OSKit定义了此函数,用于对主线程进行备份。由于线程创建子线程,子线程又创建自己的子线程,如此下去,将产生一个以主线程为根节点的树形结构,所以一旦主线程丢失,将有可能导致系统崩溃,所以应对其进行备份。
pthread_thread_t*pthread_init_mainthread ( pthread_thread_t *pthread )

2.2.4初始化创建线程的线程
说明:这和UNIX中那个创建进程的进程有些类似,它唯一的工作就是为主线程创建子线程,并且,每个线程都包含一个该线程。
pthread_thread_t*thread_init_mainthread ( pthread_thread_t *pthread )

2.2.5为等待和休眠的线程创建一个等待时间
说明:由于调度的原因,一般线程不可能一直占用CPU,也不可能永远被挂起,所以OSKit为规定线程的等待时间定义了此函数。
voidpthread_prepare_timer ( pthread_thread_t *pthread )


2.3 pthreads/pthread_attr.c

此源码文件包括了一套完整的线程属性的初始化机制,它与上一节的线程创建共同作用,规定了线程在创建之初的属性,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程属性的初始化。

2.3.1线程初始化的数据结构
说明:系统在创建新线程的时候,会调用此函数来规定被创建线程的属性,所以此函数在线程属性机制中站有最主要的地位,是一切线程属性函数调用的源泉,希望读者给以充分的重视。
intpthread_attr_init ( pthread_attr_t *attr )
  attr->detachstate =PTHREAD_CREATE_JOINABLE; /* 线程与其他线程关联*/
  attr->priority =PRIORITY_NORMAL; /* 线程的优先级 */
  attr->stacksize = PTHREAD_STACK_MIN; /*线程所占堆栈的 */
  attr->guardsize = DEFAULT_STACKGUARD; /* 警戒堆栈的大小 */
  attr->stackaddr = 0; /* 堆栈的大小 */
  attr->policy = SCHED_RR; /*线程调度类型 */
  attr->inherit = PTHREAD_EXPLICIT_SCHED; /* 线程继承类型 */
  注:memset 的数据结构 /* \oskit\libc\string\memset.c */
  void *memset(void *tov, intc, size_t len)
  {  register char *to = tov;
     while (len-- >0)
     *to++ = c;
     return tov;
  }
  tov: 指向内存的首地址 c: 分配类型 len:分配长度

2.3.2线程属性的撤销
说明:这个函数是上一个函数的逆操作,在撤销一个线程的同时,其属性也应相应地被撤销,所以OSKit定义了此函数来实现该功能。
intpthread_attr_destroy ( pthread_attr_t *attr )
  memset((void *) attr, -1,sizeof(*attr)); /* 撤销指针并释放内存*/

2.3.3设置线程的警戒堆栈大小
说明:警戒堆栈在操作系统中被广泛使用,它是源于UNIX的进程间通信,在UNIX系统中,接受进程的消息缓冲可能小于发送进程。所以,当消息到来的时候,为了避免溢出造成的信息丢失,系统创建了警戒堆栈,用来存储溢出的消息。而OSKit把它用于线程间通信的保护,但用法与UNIX大同小异。
intpthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize )

2.3.4得到警戒堆栈的大小
说明:由于OSKit为每个接受线程设置警戒堆栈,所以定义此函数,为的是系统能方便的得到每个接受线程的警戒堆栈的大小,这同样是出于对安全的考虑。
intpthread_attr_getguardsize ( const pthread_attr_t *attr, size_t *guardsize )

2.3.5设置线程的分离状态
说明:上边曾经提到,系统中的线程树,而线程与线程之间很可能并不是完全独立的,即使他们处于不同的层次,也可能有一定的相关性或互斥性,所以定义此函数用来规定线程之间的互斥性,即分离状态是完全有必要的。
intpthread_attr_setdetachstate( pthread_attr_t *attr, int detachstate )

2.3.6得到线程的分离状态
说明:在规定了分离状态之后,若A线程要与B线程发生关联的时候,它必须先看看B线程和其他线程的关系,所以要定义此函数来得到B线程与其他线程的分离状态,提供给A,然后A在根据具体情况决定是否与B发生关系。
intpthread_attr_getdetachstate ( const pthread_attr_t *attr, int *detachstate)
  *detachstate = attr->detachstate;

2.3.7设置线程的继承关系
说明:当线程创建子线程的时候,上边我提到的那个创建线程的线程会为他们父子之间规定继承关系,即子线程继承父线程。
intpthread_attr_setinheritsched ( pthread_attr_t *attr, int inheritstate)
  attr->inherit = inheritstate;

2.3.8得到线程的继承状态
说明:当系统要在复杂的线程树中得到继承关系的时,调用此函数便能达到预期的目的。OSKit定义此函数,为的是能在复杂的线程树中一眼看穿某两个线程之间的关系。
intpthread_attr_getinheritsched ( const pthread_attr_t *attr, int *inheritstate)
  *inheritstate = attr->inherit;

2.3.9设置线程的调度参数
说明:OSKit提供了多种线程调度方式供选择,而且还为每个线程在创建的时候规定了一种调度算法,系统在创建线程的时候调用下面的函数来完成这项工作。
intpthread_attr_setschedparam ( pthread_attr_t *attr,const struct sched_param*param )
  int pri      = param->priority;
  attr->start    =param->start; /* 线程开始参数 */
  attr->period    = param->period; /*线程的周期 */
  attr->deadline   = param->deadline; /* 线程运行的最终时限 */

2.3.10得到线程调度的参数
说明:在OSKit中,每个线程都有其自身的调度方式,换句话说,就是线程的调度方式仿佛是线程的一个属性,在创建线程的时候就随线程被规定了,所以调度程序可以通过下面的函数来得到某个线程的调度算法和此时它的优先级。
intpthread_attr_getschedparam ( pthread_attr_t *attr, struct sched_param *param)
  param->priority = attr->priority;

2.3.11设置线程调度策略
说明:在某些方面,线程的调度策略的确很像线程的属性,但它却是可以在调度中被更改的,系统可以调用此函数完成对线程调度策略的重定义。这样的规定,使得线程调度灵活了许多,当然这种灵活是用增加系统开销的代价换回来的,但却是值得的。
intpthread_attr_setschedpolicy ( pthread_attr_t *attr, int policy )
  switch(policy) {
    case SCHED_FIFO:      /* 先到先处理 */
    caseSCHED_RR:       /* 时间片轮转 */
    case SCHED_EDF       /* 先死先处理 */
    caseSCHED_RMS:       /* 效率优先 */
       attr->policy = policy;
break;
    case SCHED_DECAY:
       printf("pthread_attr_setschedpolicy:"
        "SCHED_DECAY not supported yet\n");
  }

2.3.12得到线程调度策略
说明:负责调度的线程通过该函数得到被调度线程的调度策略,然后再根据此时系统内的情况,决定是否改变其调度算法。
intpthread_attr_getschedpolicy ( const pthread_attr_t *attr, int *policy)
  *policy = attr->policy;

2.3.13设置堆栈地址
说明:调用此函数,可以实现对线程属性堆栈的底层操作,即设置指向该堆栈的指针,并保存起来,为以后的操作奠定基础。
intpthread_attr_setstackaddr ( pthread_attr_t *attr, oskit_u32_t stackaddr)
  attr->stackaddr = stackaddr;

2.3.14 得到堆栈的地址
说明:返回指向线程属性堆栈的指针。
intpthread_attr_getstackaddr(const pthread_attr_t *attr, oskit_u32_t*stackaddr)
  *stackaddr = attr->stackaddr;

2.3.15设置堆栈的大小
说明:由于系统中的线程功能、大小各不相同,所以其属性堆栈的大小也不尽相同,调用此函数可以设置线程属性堆栈大小。
intpthread_attr_setstacksize ( pthread_attr_t *attr, size_t stacksize)
  attr->stacksize = stacksize;

2.3.16 得到堆栈的大小
说明:返回某个线程堆栈的大小。
int pthread_attr_getstacksize (const pthread_attr_t *attr, size_t *stacksize )
  *stacksize =attr->stacksize;

2.3.17设置线程的优先级
说明:在操作系统的线程调度中,优先级调度的使用最为广泛,OSKit定义了下面的函数来规定线程的优先级。
intoskit_pthread_attr_setprio ( pthread_attr_t *attr, int pri)
  attr->priority = pri;

2.3.18准备接受调度
说明:由于多态性原因,不是所有的线程都要参与调度,只是某些线程参加,所以,系统必须调用此函数,告诉该线程准备参加调度,而被调度的方式在上边的几个函数里已经规定好了。
intpthread_attr_setopaque ( pthread_attr_t *attr, oskit_u32_t opaque)
  attr->opaque = opaque;

2.3.19 声明要进行调度的线程
说明:调用此函数是为了告诉系统,哪些线程是要被调度的。
intpthread_attr_setscheduler ( pthread_attr_t *attr, pthread_t tid)
  attr->scheduler = tid;

2.3.20设置mutex的属性
说明:在OSKit中,广泛使用了mutex,主要是为了更好的使用临界资源,有效地避免了死锁,换句话说,mutex就像临界资源的数量一样,线程对其进行PV操作。而当一个线程占有了一个mutex,并且在没释放它之前该线程又创建了子线程,此时,子线程理所当然地从父线程手中继承了mutex的所有权,OSKit定义了下面的函数来实现所叙述的功能。
intpthread_mutexattr_init ( pthread_mutexattr_t *attr)
  attr->priority_inherit = PTHREAD_PRIO_NONE; /* THREAD_PRIO_NONE=0 */

2.3.21撤销mutex的属性
说明:当占有mutex的线程放弃了它的时候,调用该函数来撤销mutex的属性。
intpthread_mutexattr_destroy ( pthread_mutexattr_t *attr )
  memset((void *)attr, -1, sizeof(*attr));

2.3.22设置muxtex的协议
说明:mutex的使用并不是简单的占用与撤销,OSKit还提供了更加完善的mutex协议,它根据inherit的不同,将mutex多样化。
intthread_mutexattr_setprotocol ( pthread_mutexattr_t *attr, int inherit)
  switch (inherit) {
    case PTHREAD_PRIO_NONE:
    casePTHREAD_PRIO_INHERIT:
      attr->priority_inherit =inherit;
      break;
    default:
      returnEINVAL;
  }
注:在线程调度时,根据mutex协议产生mutex变量,将线程按照优先级排队

2.3.23 得到线程调度的mutex协议
说明:好像对协议的解析一样,调用此函数,可以返回该线程的mutex。
intpthread_mutexattr_getprotocol ( const pthread_mutexattr_t *attr, int*inherit)
  *inherit = attr->priority_inherit;

2.3.24设置mutex的种类
说明:mutex在OSKit中是可以多种多样的,通过调用下面的函数,可以规定线程到底占用了哪种mutex。
intpthread_mutexattr_settype ( pthread_mutexattr_t *attr, int type )
  switch(type) {
    case PTHREAD_MUTEX_NORMAL: /* 普通 */
    casePTHREAD_MUTEX_ERRORCHECK: /* 错误检查 */
    case PTHREAD_MUTEX_RECURSIVE: /* 递归*/
    case PTHREAD_MUTEX_DEFAULT: /* 默认 */
      attr->type =type;
      break;
    default:
      return EINVAL;
  }

2.3.25得到mutex的类型
说明:当系统想要知道某个线程占用的是什么类型的mutex时,调用此函数来返回mutex的类型。
intpthread_mutexattr_gettype ( const pthread_mutexattr_t *attr, int *type)
  *type = attr->type;


本 章 小 结

本章从线程的创建、存储和数据结构到各种属性以及对它们的操作(设置、撤销和得到)入手,系统地阐述了OSKit线程机制的根源--线程初始化。

在第一节主要从三个角度归纳了OSKit中线程所涉及的概念,它们是创建、存储以及线程的数据结构。第二节中以源代码中的/pthreads/pthread_creat.c 为例,说明了第一节中概念的具体实现。第三节以源代码中的 /pthreads/pthread_attr.c为例,说明了Oskit对线程属性的全部操作,包括线程初始化的数据结构、属性的撤销、线程间关系、对mutex的操作以及对属性堆栈的操作等。

[目录]


第三章 线程通信分析

《OSKit的线程机制 第三章》


OSKit的线程机制 第三章2000年12月25日22:11


作 者: 汤海京


导师:陈朔鹰


第三章 线程通信分析


众所周知,在Linux中,进程为了能在同一项任务上协调工作,彼此之间必须能够进行通信。例如,在一个shell管道中,第一个进程的输出必须传输到第二个进程,这样沿着管道传递下去。因此在需要通信的进程之间,应该使用一种结构较好的通信方式。

Linux支持许多不同形式的进程间通信机制,在特定的情况下,它们各有自己的优缺点,如利用管道进行通信的最大缺点就是,只有成为系统调用pipe的进程的子孙后代的进程才可以使用管道,因此,相互无关的进程不能通过管道通信。

OSKit的开发人员们充分考虑了各种通信方式的利弊,扬长避短,采用了如下的两种线程间通信方式:消息队列和信号量。虽然这还有很多不足之处,但较之SystemV以有所改进。


3.1 线程间通信

3.1.1消息队列

OSKit提供的消息队列机制于UNIX十分相似,一个和多个线程可向消息队列写入消息,而一个或多个线程可从消息队列中读取消息,这种线程间通信的机制就像客户机/服务器一样,客户机向服务器发送请求消息,服务器读取消息并执行相应的请求。

OSKit将消息描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC标识号唯一的标识。

3.1.2信号

尽管大多数线程间通信是计划好的,但是同时还需要处理不可预知的通信问题。例如,用户使用文本编辑器要求列出一个大文件的全部内容,但随即他认识到该操作并不重要,这是就需要一种方法来中止编辑器的工作,比如说,我们可以按DEL键来实现。按DEL键实际上是向编辑器发送一个信号,编辑器收到此信号便立即停止打印文件的内容。信号还可以用来报告硬件捕获到的特定的中断,如非法指令和浮点运算溢出、超时也通过信号实现的。

在OSKit中,信号是一种重要的线程间通信机制,线程通过信号知道系统中正在出现的事件。信号的一个特点就是它的异步性,这表示线程在它的执行期间的任何时候都可以接受到信号,甚至可能当线程正在执行系统调用的时候接到信号,因此,线程必须随时为相应信号作好准备。

下面我们再谈谈信号的生成与交付,当引起信号的事件首次出现时,就认为信号为线程而生成,也就是说信号被发送到线程。这类事件的例子,包括:时间片到时,终端激活,硬件故障的检测,kill()的调用。在某些情况下,一个事件为多个可以为多个线程生成信号。每个线程采用样应的动作来响应系统定义的每种信号(见信号量表3.1)。

在信号的生成与交付之间,该信号被挂起,但在用户一级是检测不到这个间隔的。每个线程都拥有一个信号掩码(sig_mask),它规定了当前被阻塞而不能交付给线程的信号集。线程的信号掩码是从父线程继承而来的。我前面提到sigaction(),sigprocmask( )和sigsuspend( )函数,都是对信号掩码的操作。

信号的动作与信号有关的动作分为三种:SIG_DFL,SIG_IGN或指向函数的指针。在最开始,进入main()之前,所有信号都将被置成SIG_DFL或SIG_IGN。

(1)SIG_DFL信号专用的默认动作:
  (a)如果默认动作是暂停线程,则该线程的执行被暂时挂起。当线程暂停期间,发送给线程的任何附加信号都不交付,直到该线程开始执行,但是SIGKILL除外。
  (b)把挂起信号的信号动作设置成SIG_DFL,且其默认动作是忽略信号(SIGCHLD)。

(2)SIG_IGN忽略信号
  (a)该信号的交付对线程没有影响
  (b)系统不允许把SIGKILL或SIGTOP信号的动作设置为SIG_DFL

(3)指向函数的指针--捕获信号
  (a)信号一经交付,接收线程就在指定地址上执行信号捕获程序。在信号捕获函数返回后,接受线程必须在被中断点恢复执行。
  (b)用C语言函数调用的方法进入信号捕捉程序:
    void func(signo)
    int signo;
    func()是指定的信号捕捉函数,signo是正被交付信号的编码
  (c)如果SIGFPE,SIGILL或SIGSEGV信号不是由C标准定义的kill()或raise()函数所生成,则从信号SIGFPE,SIGILL,SIGSEGV的信号捕获函数正常返回后线程的行为是未定义的。
  (d)系统不允许线程捕获SIGKILL和SIGSTOP信号。
  (e)如果线程为SIGCHLD信号建立信号捕获函数,而该线程有未被等待的以终止的子线程时,没有规定是否要生成SIGCHLD信号来指明那个子线程。

每一种信号都被OSKit给予了一个符号名,对于32位的i386平台而言,一个字32位,因而信号有32种。下面的表给出了常用的符号名、描述和它们的信号值。

符号名  信号值描述                是否符合POSIX
SIGHUP  1   在控制终端上检测到挂断或控制线程死亡  是
SIGINT  2   交互注意信号              是
SIGQUIT3   交互中止信号              是
SIGILL  4   检测到非法硬件的指令          是
SIGTRAP5   从陷阱中回朔              否
SIGABRT6   异常终止信号              是
SIGEMT  7   EMT指令                否
SIGFPE  8   不正确的算术操作信号          是
SIGKILL9   终止信号                是
SIGBUS  10   总线错误                否
SIGSEGV  11检测到非法的内存调用          是
SIGSYS  12   系统call的错误参数           否
SIGPIPE  13在无读者的管道上写           是
SIGALRM  14   报时信号                是
SIGTERM  15终止信号                是
SIGURG  16   IO信道紧急信号             否
SIGSTOP  17暂停信号                是
SIGTSTP  18   交互暂停信号              是
SIGCONT  19如果暂停则继续             是
SIGCHLD  20   子线程终止或暂停            是
SIGTTIN  21后台线程组一成员试图从控制终端上读出  是
SIGTTOU  22   后台线程组的成员试图写到控制终端上   是
SIGIO   23允许I/O信号               否
SIGXCPU  24   超出CPU时限               否
SIGXFSZ25   超出文件大小限制            否
SIGVTALRM 26   虚时间警报器              否
SIGPROF27   侧面时间警报器             否
SIGWINCH 28   窗口大小的更改             否
SIGINFO29   消息请求                否
SIGUSR1  30   保留作为用户自定义的信号1        是
SIGUSR2 31   保留作为用户自定义的信号        是

请求按默认的规则进行信号处理
SIG_DFL (void (*)(int))0
请求忽略信号
SIG_IGN (void (*)(int)) 1
注意:信号队列中最多允许有64个信号


3.2 pthreads/pthread_ipc.c

此源码文件包括了一套完整的消息队列型线程通信机制,消息队列是线程通信的主要手段,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现信号队列,从而进行线程通信的。

3.2.1消息发送:
说明:当一个线程要向另一个线程发送消息的时候,OSKit使用下面定义的函数来实现一个线程向另一个线程传递消息。
oskit_error_toskit_ipc_send ( pthread_t dst,void *msg,
        oskit_size_t msg_size,oskit_s32_t timeout)
  dst:目标地址              *msg:指向消息的指针
  msg_size:消息大小(32位无符号整形)    timeout:超时

3.2.2send的算法
发送一个同步消息,直到接收者发送终止消息或超时才结束
为避免死锁,给没个人加一个锁,并关中断。
assert_interrupts_enabled();
disable_interrupts( );
pthread_lock(target->waitlock);
通过条件,检测目标线程是否在等待接受:
条件: a:目标线程的等待标志 THREAD_WS_IPCRECV_WAIT(线程接收等待位)
    b:目标线程号存在
c:目标线程等于任意一个线程
    d:消息大小不为0
条件成立: a  ( b || c)  d== 1 才可以发送
   if (target->waitflags THREAD_WS_IPCRECV_WAIT
     (target->ipc_state.tid ==pthread->tid||target->ipc_state.tid== ANYTID))
   { if(msg_size)
     {Memcpy(target->ipc_state.msg,
        msg,MIN(msg_size,target->ipc_state.msg_size));
        if(msg_size > target->ipc_state.msg_size)
           err =OSKIT_ERANGE;
     }
   }
把数据的大小和发送者的ID告诉接受线程:
target->ipc_state.msg_size= msg_size;
target->ipc_state.tid =pthread->tid;
清除接受位:
target->waitflags =~THREAD_WS_IPCRECV_WAIT;
pthread_unlock ( target->waitlock);
注意: flagA  = ~flagB实际上就是将flagB中等于1的位在flagB中清0
发送队列:
queue_enter (target->ipc_state.senders,pthread,pthread_thread_t*,
ipc_state.senders_chain)
target->ipc_state.senders:发送线程的IDpthread:
pthread_thread_t*:指向消息的指针是pthread_thread_t类型
ipc_state.senders_chain:发送队列的指针
队列间参数传递:
pthread->ipc_state.msg= msg; /* 消息 */
pthread->ipc_state.msg_size = msg_size; /* 消息的长度*/
pthread->ipc_state.tid = dst; /* 消息的目的地址 */
pthread->waitflags =THREAD_WS_IPCSEND_WAIT;
/* 消息目的等待位置1 */

3.2.3消息接收
说明:当一个线程要从其消息队列中开始接受消息的时候,OSKit提供了下面的函数供线程调用。
oskit_error_toskit_ipc_recv ( pthread_t src,void *msg,
     oskit_size_t msg_size,oskit_size_t *actual,oskit_s32_t timeout )
  src: 发送线程的ID *msg: 指向消息的指针
  msg_size: 消息大小(32位无符号整形)
  actual: 实际消息的大小 timeout: 超时

3.2.4receive的算法
等待从发送线程来的消息
通过条件,检测是否接受消息
a:有等待发送的线程
b:线程等待位THREAD_WS_IPCSEND_WAIT(发送等待标志)
c:发送线程存在
条件成立:a(b||c)
   pthread_lock(source->waitlock);
   if(source->waitflags  THREAD_WS_IPCSEND_WAIT
          source->ipc_state.tid == pthread->tid){
      *actual = MIN(msg_size, source->ipc_state.msg_size);
      if(*actual) {
          memcpy(msg, source->ipc_state.msg,*act0ual);
          if (source->ipc_state.msg_size >msg_size)
              err =OSKIT_ERANGE;
              }
   }
如果没有等待发送的进程,则让接受进程等待
当发送队列为空且timeout=0时解锁,如果发送了但没接受,则重新初始化发送进程,重新发送。


3.3 pthreads/pthread_signal.c

此源码文件包括了一套完整的信号量型线程通信机制,信号量是线程通信的另一个重要的手段,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现信号量,从而进行线程通信的。

3.3.1检测并更改阻塞的信号
说明:当一个信号被发送出来的时候,并不一定马上能得到相应,一般来说,它要被阻塞一段时间,但系统也是就不管这些阻塞信号了,系统会定时执行此线程,用以检测阻塞的到底是什么信号,它的含义是什么,然后根据需求作出决定,可以将信号阻塞或者响应。
intpthread_sigmask ( int how, const sigset_t *set, sigset_t *oset )
  how:SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
  set:如果是空指针,则指向下一个信号
  oset:如果是空指针,则指向上一个信号记录

3.3.2杀线程信号
说明:OSKit以及所有的操作系统,都包含这个函数,因为它是操作系统所必须具备的基本功能,也就是说,当一个线程出于无响应状态很长时间,或者因为其他的什么原因线程不能得到执行,则调用它,将该线程杀死。
intpthread_kill(pthread_t tid, int signo)

3.3.3关线程锁
说明:当线程接受到了消息后的响应过程中,有些时候是不希望被其他的线程打断的,所以OSKit提供了此函数调用,用来将线程锁起来,也可以理解为将线程保护了起来。
intpthread_kill_locked ( pthread_thread_t *pthread, int signo )

3.3.4在目标线程的信号等待队列中加一个信号
说明:在信号发送和接受的过程中,线程间通讯并不一定是同步的,此时,OSKit提供了一个消息等待队列,将不能及时得到响应的消息用指针链接成为一个类似与链表的数据结构。当接受线程要响应信号的时候可以直接从消息队列里提取,这样作既可以提高通信效率,有可以增加通信的稳定性。
intsigaddset ( pthread->sigpending, signo )

3.3.5信号完成的动作
说明:在用信号方式通信的时候,线程间传递的信号并不是要做的动作,而是动作的代码,而什么信号对应什么动作,还要有此汉说来进行解析,这样作是为了规范线程间通信,同时还可以节约空间,节省发送和接受的时间,从而大大提高系统的效率。
intsigaction ( int sig, const struct sigaction *act, struct sigaction *oact)
  sig:指定信号 act:指向与指定信号向联系的规定信号动作的结构
  oact:指向存储先前与该信号相联系的动作的结构

3.3.6测试或改变(或两者兼有)主调进程的信号掩码
说明:所谓掩码就是信号的屏蔽码,比如说,当一个信号被发送的时候,它被自动加载到接收线程的信号掩码中,如果此时又向该线程发送了同样的信号,则阻塞该信号,知道前一个信号响应完毕。
OSKit采用下面的函数完成的对信息掩码的测试和改变。
intsigprocmask ( int how, const sigset_t *set, sigset_t *oset )
  how:指明改变信号集的方式
  SIG_BLOCK: 结果是当前集与实参set所指向的信号集的联合
  SIG_UNBLOCK:结果集是当前集与实参set所指向的信号集的补集的交
  SIG_SETMASK: 结果集是实参set所指向的信号集
  set:指向要用来改变当前被阻塞的信号集 oset: 存储当前的掩码

3.3.7杀线程
说明:此函数和前面介绍过的杀线程信号并不完全一样,源线程是通过看是否有向目的线程发信号的权限,如果有的话就杀掉它
intkill ( pid_t pid, int signo )
  pid: 目标线程 signo: 所要发的信号

3.3.8等待信号的线程队列
说明:由于线程间通信不一定同步的原因,调用此函数创建信号等待队列是十分有必要的。
int sigqueue( pid_t pid, int signo, const union sigval value )

3.3.9等待内部信号
说明:OSKit将信号分为内部信号和外部信号,这是十分有意义的,因为很多情况下,它们的处理方式是不一样的,有的外部消息可以不加理睬,但一般来说,内部消息是一定要及时响应的,系统通过调用下面的函数实现了等待一个内部信号,一旦等到,马上响应。
oskit_error_toskit_sigwait_internal ( const sigset_t *set,siginfo_t
        *info,constoskit_timespec_t *timeout )
  sigset_t: 信号装置,无符号整型
  siginfo_t:信号信息不会被外部的信号打断

3.3.10线程等待信号
说明:在线程调度时,调度程序经常会发送此信号让执行完毕的线程等待还没执行完的线程,这是为了保持线程之间的同步。
intsigwait ( const sigset_t *set, int *sig )

3.3.11 线程等待信号(一般指内部信号)
说明:同样的,核心内的线程一样也要保持同步。
intsigwaitinfo ( const sigset_t *set, siginfo_t *info )

3.3.12线程等待信号(有时间限制)
说明:此外为了完善系统的线程管理,OSKit还提供了这条函数,用来设定线程的等待时间,如果在规定的时间内没有信号到来的话,则不再等待。
intsigtimedwait ( const sigset_t *set,siginfo_t *info,
     const structoskit_timespec *timeout )

3.3.13线程等待信号,如果无信号发给它,则无限期等待
说明:但有的时候,线程要等待的信号对于该线程来说是十分重要的,所以它将无限期的等待下去,同时调度程序会将该线程阻塞起来。
intsigsuspend ( const sigset_t *sigmask)

3.3.14从一个陷入发送信号给线程
说明:当系统内部发生陷入的时候,系统会给正在执行的线程发送信号,OSKit正是调用了此函数来实现。
voidoskit_libc_sendsig ( int sig, int code, struct sigcontext *scp )

3.3.15发出线程阻塞信号
说明:如果当前正在执行的线程由于某种突发情况被阻塞了,它将调用此函数,来通知与其相关的线程。
voidoskit_deliver_pending_signals ( void )

3.3.16不会被阻塞的信号发送
说明:在操作系统中,有许多十分重要的消息是不能被阻塞的,OSKit所提供的此条函数调用便是用来发送此种信号的。
voidreally_deliver_signal ( int sig, siginfo_t *info, struct sigcontext *scp )

3.3.17信号初始化
说明:与线程类似,信号并不是从天而降,它也要先进行初始化,然后再发送给接受线程。
voidpthread_init_signals ( )


3.4 pthreads/pthread_cond.c

此源码文件包括了一套完整的线程通信的条件变量,它是线程通信的重要组成部分,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程通信的条件变量。

3.4.1条件变量的初始化数据结构
说明:在OSKit的线程间通信机制中,经常用到条件变量。所谓条件变量,就是两个线程通过条件变量来达到同步。例如,A线程我们让它等待一个条件变量,B线程我们让它发送一个信号,满足A线程所等待的条件,这样就实现了A、B线程之间的同步。
intpthread_cond_init ( pthread_cond_t *c, const pthread_condattr_t *attr)
  pthread_cond_t: 条件变量 pthread_condattr_t: 条件变量属性

3.4.2撤销条件变量
说明:条件变量并不是永远发生作用的,所以有创建就得有撤销,OSKit正是调用了这个函数来实现条件变量撤销。
intpthread_cond_destroy(pthread_cond_t *c)

3.4.3线程等待条件变量
说明:OSKit设定条件变量就是为了通过让线程等待它,从而实现线程之间的相互协调。
intpthread_cond_wait ( pthread_cond_t *c, pthread_mutex_t *m)
  pthread_mutex_t:为什么而等待

3.4.4安全的等待
说明:等待条件变量的方式也是多种多样,此函数提供的是一种安全的等待方式,即等待条件变量的线程不会被中断。
intpthread_cond_wait_safe ( pthread_cond_t *c, pthread_mutex_t *m )
  注:不会被中断

3.4.5等待条件变量,但有时间限制
说明:另一种等待条件变量的方式是在一定的时间限制内等待,一旦超时,线程便放弃对该条件变量的请求。
intpthread_cond_timedwait ( pthread_cond_t *c, pthread_mutex_t
             *m,oskit_timespec_t *abstime )
  struct oskit_timespec{
      oskit_time_t tv_sec; /* 秒 */
      oskit_s32_t tv_nsec; /* 纳秒*/
  };

3.4.6等待捐赠条件
说明:在操作系统中线程之间总有一些比较有趣的调度算法,捐赠就是其中之一,在OSKit里,出于同一个等待目的的线程之间,为了总体上的最优化,某些线程可以将自己占有的资源捐赠给其他线程,让其尽早满足执行的条件。
intpthread_cond_donate_wait ( pthread_cond_t *c, pthread_mutex_t
       *m,oskit_timespec_t *abstime, pthread_t donee_tid )
  pthread_tdonee_tid:被捐赠线程号

3.4.7等待条件超时
说明:此函数是OSKit的出错信息,它是在等待条件变量超时的情况下才被调用的。
oskit_error_tpthread_condwait_timeout ( struct oskit_iunknown *listener, void *arg )

3.4.8线程发送信号条件
说明:在线程间通信中,线程并不是什么时候都可以发送信号的,OSKit定义了下面这个函数,规定了信号发送是所要等待的条件;换句话说就是只有满足了此函数调用所设定的条件,线程之间才可进行通信。
intpthread_cond_signal ( pthread_cond_t *c )

3.4.9线程广播条件
说明:有些时候,某些线程需要对系统中所有的线程发送同一条消息,这样就用到了OSKit提供的线程广播的函数。
intpthread_cond_broadcast ( pthread_cond_t *c )


本 章 小 结

本章从线程间的两种通信机制入手,系统地阐述了OSKit的线程通信机制。

在第一节主要描述了OSKit所用到的两种线程间通信机制,以及它为什么没有使用UNIX中的管道通信,另外,我还以表格的形式详细列出了OSKit提供的各种信号量、以及它们与POSIX标准之间的关系。第二节中以源代码中的/pthreads/pthread_ipc.c 为例,说明了第一节中消息队列的具体实现。第三节以源代码中的 /pthreads/pthread_signal.c为例,说明了Oskit第一节中信号量的具体实现。第四节中又以源代码/pthreads/pthread_cond.c为例,阐述了条件变量的概念以及用条件变量进行辅助通信的具体实现。

[目录]


第四章 线程调度

《OSKit的线程机制 第四章》


OSKit的线程机制 第四章2000年12月25日22:20


作 者: 汤海京


导师:陈朔鹰


第四章 线程调度

线程调度是操作系统内核中的主要内容之一,它对整个操作系统的执行效率至关重要。OSKit当然也包括此项内容,而且由于线程的调度更加频繁,所以调度成学这部分在操作系统中的比重要比它在UNIX中大许多。


4.1 线程调度算法分析

4.1.1 线程调度算法的总体描述

在分时系统中,内核给每个线程分配一段CPU时间,这段时间称为时间片,当这段时间过去之后,内核将调度另一个线程让其变为执行态。这就是所谓的时间片轮转法。

与UNIX中的线程调度十分相似的是,OSKit的调度程序也采用了一种被称为多级反馈循环调度,此种算法属于操作系统调度程序中最常用的一种。其核心思想是:内核给线程分一个时间片,并把该线程反馈回若干优先级队列中的某一个。一个线程在结束它之前,可能需要多次通过多次的"反馈循环"。当内核作上下文切换和恢复的时候,调度程序必须保证该线程从原来挂起的地方继续执行,否则这种调度算法是不能采用的。

因此,我认为有必要向大家重申一下线程的上下文切换和恢复。首先,所谓线程的上下文是指由用户地址空间的内容、硬件寄存器的内容以及与该线程有关的内核数据结构组成的。更加严格的讲,线程的上下文是由它的用户级上下文、寄存器上下文以及系统级上下文组成。

4.1.2 优先级逆转法

OSKit中采用了操作系统设计中比较精彩的优先级逆转算法,即在一个优先级较低的线程占有mutex的同时,另一个优先级较高的线程又来申请此mutex,这样,优先级较高的线程就被比他低的线程阻塞。

此时我们引入了优先级逆转算法,也就是说,当上述情况发生的时候,线程调度程序就将优先级较低的线程的优先级提升至与较高优先级线程一样,这样可以使得占有mutex的线程快速执行。

如果现在需要调度的线程不止两个的话,该算法允许进行递归,也就是说,那个优先级低的线程执行还需要另外一个mutex,而此mutex被另一个优先级更低的线程占有,所以,调度程序就依次提升优先级低的线程的优先级,这就是递归提升。

但是,如过我们支持递归提升优先级的话,就可能出现死锁,所以我们又必须引入一套检验死锁的程序,这大大增加了操作系统核心的大小和系统的开销,但使得操作系统更加完善,并可以处理实时线程。


4.2 pthread/pthread_scheduler.c

此源码文件包括了一套完整的线程调度机制,OSKit的全部线程调度算法都集中在了这个文件之中,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程调度的,并且能对我在前面叙述的概念的理解有所加深。

4.2.1清空等待调度的线程队列
说明:为了能让大家真正从概念上了解此函数的作用,我想有必要先说明一下什么是线程等待队列。由于调度程序在操作系统中也是作为一个线程出现的,并且同一时间,它只能调度一个线程,所以就会造成有许多线程等待调度但无法接受调度的情况,OSKit提供的这个函数正弥补了这个缺陷,他将所有的等待调度的线程用链表的形式链接在一起,这就是所谓的等待调度的线程队列。显然,清空它实际上就是释放链表的指针。
staticinline int posix_runq_empty ( void )

4.2.2得到等待队列中优先级最高的线程的优先级
说明:当调度线程完成手头的工作,要对下一个线程进行调度时,就将面临一个问题,到底应该调度哪个等待调度的线程?OSKit提供的这个函数可以返回此时在等待队列中优先级最高的线程,供调度线程调度。
staticinline int posix_runq_maxprio ( void )return PRIORITY_MAX - ( prio - 1 );

4.2.3得到指向下一个要执行线程的指针
说明:当某个线程要转执行态的时候,调度程序并不知道这个线程挂起在了什么地方,我们可以通过调用OSKit提供的这个函数来找到指向要执行线程的指针。
staticinline int posix_runq_onrunq ( pthread_thread_t *pthread )

4.2.4在线程的执行队列的队尾追加一个线程
说明:将优先级最低的线程,或者是由于其他原因要最后执行的线程添加到线程执行队列的最后。
staticinline void posix_runq_insert_tail ( pthread_thread_t *pthread )

4.2.5在线程的执行队列的队头追加一个线程
说明:将优先级最高的线程或者是由于某种原因要现执行的线程添加到线程执行队列的最前面。
staticinline void posix_runq_insert_head ( pthread_thread_t *pthread )

4.2.6将等待队列中优先级最高的线程提出队列执行
说明:当操作系统要执行在线程执行队列里等待线程的时候,理所当然应该从头开始,我们考调有此函数来实现线程出对执行的功能。
staticinline pthread_thread_t * posix_runq_dequeue ( void )
  注: 返回指向下一个等待线程的指针

4.2.7从等待队列中删除独占资源的线程
说明:为了使操作系统能够在总体最优化的前提下运行,OSKit规定不允许某一个线程长时间独占系统资源,如果发现此类时间,就可以调用这个函数来将独占资源的线程从执行队列中删除,以保持线程调度的公平合理。
staticinline void posix_runq_remove ( pthread_thread_t *pthread )

4.2.8此时用到的调度算法
说明:在前面的章节里,我曾经提到,线程的调度算法仿佛其属性一样,在初始化的时候就赋给了它,但它却是可以更改的,OSKit就是通过调用下面的这个函数来实现在线程创建之后来改变调度算法的。
intposix_sched_schedules ( int policy )
  if ( policy == SCHED_RR || policy ==SCHED_FIFO )
    return 1;
  return 0;

4.2.9将一个线程变为运行态
说明:众所周知,线程在系统中一般有三种状态,OSKit通过调用该函数实现线程转执行态的功能。
intposix_sched_setrunnable ( pthread_thread_t *pthread )

4.2.10终止该线程目前的调度算法
说明:这又是在线程创建之后改变其调度算法的一种函数调用,它的功能是中止该线程目前的调度算法。
voidposix_sched_disassociate ( pthread_thread_t *pthread )
{
  if(posix_runq_onrunq(pthread)) {
    /* * On the scheduler queue, so its notrunning. */
    posix_runq_remove ( pthread );
  }
}

4.2.11为一个新的线程创建调度参数
说明:所谓调度参数就是调度程序决定是否执行某个线程的依据。例如,某用户态线程最近使用CPU时间的统计等。
voidposix_sched_init_schedstate ( pthread_thread_t *pthread,const struct sched_param*param )

4.2.12改变线程的调度状态
说明:如果其调度参数符合调度条件,调度程序会通过调用OSKit提供这个函数来改变此线程的调度状态,并将它转为准备调度态。此时该线程被锁住,而且要关中断
intposix_sched_change_state ( pthread_thread_t *pthread , const struct sched_param*param )

4.2.13优先级迁移
说明:这是我在上面提到的优先级逆转法的具体实现,OSKit就是通过调用它来完成对线程优先级的动态变更的。
intposix_sched_priority_bump ( pthread_thread_t *pthread, int newprio)
  注意:这只是暂时的,仅仅适用于实时线程

4.2.14基于某种原因,线程被送回等待队列
说明:当线程在操作系统中执行的时候,完全有可能由于用户的原因造成当线程执行完它的时间片后返回执行队列时,位置改变了。下边是我对OSKit提供的几种改变原因的注解。
intposix_sched_dispatch ( resched_flags_t reason, pthread_thread_t *pthread)
  switch (reason) {
    case RESCHED_USERYIELD:posix_runq_insert_tail(pthread);
      /* 如果由用户造成线程为空闲态,则插入队尾 */
    caseRESCHED_YIELD: posix_runq_insert_head(pthread);
      /* 如果由用户造成线程为空闲态,则插入队尾*/
    case RESCHED_PREEMPT:
      /* 根据线程拥有的优先级和其调度算法来具体分析 */
      if(pthread->policy == SCHED_RR) {
        if (--pthread->ticks == 0){
          posix_runq_insert_tail(pthread);
      /*如果是时间片轮转发调度且只有一个ticks,则插入队尾 */
          pthread->ticks =SCHED_RR_INTERVAL;
        }
        else
          posix_runq_insert_head(pthread);
      /*虽然是时间片轮转法,但拥有的ticks不止一个,则插入对头 */
      }
      else if (pthread->policy== SCHED_FIFO)
        posix_runq_insert_head(pthread);
      /*如果是先来先处理法,则插入对头 */
    case RESCHED_INTERNAL: /* 使线程变为空闲态*/
    default:
      if (pthread == IDLETHREAD)
      /*停止所有的调度算法,将线程变成空闲态 */
        panic("posix_sched_dispatch:Idlethread!\n");
  }

4.2.15返回优先级最高的处于等待状态的线程
说明:当调度程序要将一个出于等待状态的线程加入到执行队列中的时候,就需要有一个函数,它能返回优先级最高的等待线程,下面的函数正好就完成了这项任务。
pthread_thread_t* posix_sched_thread_next ( void )
  {  if ( posix_runq_empty( ))
      return 0;
     return posix_runq_dequeue( );
  }

4.2.16改变线程的优先级
说明:实质是将线程从队列中删除,重新赋给优先级,再插入到队尾。
OSKIT_INLINEvoid threads_change_priority (pthread_thread_t *pthread, int newprio)

4.2.17优先级继承
说明:所谓优先级继承,其理论的实质就是所有出于等待状态的线程,在等待过程中,它的优先级会随时间的推移慢慢提高。
voidpthread_priority_inherit ( pthread_thread_t *pwaiting_for )

4.2.18不继承优先级
说明:这其实与上一个优先级继承正好相反,无论等待了多长时间,线程的优先级都不会因此而改变。
voidpthread_priority_uninherit ( pthread_thread_t *punblocked )

4.2.19优先级递减
说明:如果一个线程总是处于执行状态,则出于总体最优化的考虑,OSKit会调用此函数将其优先级逐步递减,从而使得更多的线程有被执行的可能。
voidpthread_priority_decreasing_recompute ( pthread_thread_t *pthread)


本 章 小 结

本章从线程调度的一般性概括到具体实现,系统地阐述了OSKit的线程调度机制。

在第一节主要是对一般的线程调度概念的描述,并且对OSKit中最为精彩的优先级逆转法的理论作了详细的论述。第二节中以源代码中的pthreads/pthread_scheduler.c为例,从等待调度的线程队列、线程的执行队列到优先级继承、优先级递减等,完整的阐明了OSKit的线程调度机制。

[目录]


第五章 OSKit的应用实例

《OSKit的线程机制 第五章》


OSKit的线程机制 第五章2000年12月25日22:31


作 者: 汤海京


导师:陈朔鹰


第五章 OSKit的应用实例 一个简单系统的设计与实现


5.1设计目的

OSKit是一个用来帮助我们研发操作系统的工具,如果你想更好的掌握它,除了对其本身进行分析之外,还有一个更加实际的方法,那就是利用这个工具开发一个我们自己的操作系统。

我们开发这个系统的初衷主要是为了学习,因此并没有想把它设计成像Windows、UNIX那么复杂。我们的目标是很明确的,那就是在大致搞明白OSKit应当如何使用之后,做一次尝试,用较短的时间,设计出一个具有一些最基本功能的系统。这对于我们课题组的每名成员来说,都意味着挑战。


5.2系统的功能

这个系统支持些什么?这是在开始设计之前我们要首先确定的一个问题。下面列出了一些我们希望实现的功能。

通过Multiboot或者LILO进行系统引导:这个系统可以通过所有支持Multiboot的引导程序进行引导,或者通过引导Linux的LILO进行引导。引导方式可以根据用户的要求,选择是通过软盘还是硬盘。

多线程:用户可以通过我们提供的系统调用,创建或杀死自己的线程。由于并不支持文件系统,因此新的线程恐怕只能是一个函数了。从理论上讲,我们可以让这个系统支持很多线程,但因为这仅仅是一个简单的演示性系统,因此我们把线程数限制在32个,当然这个数字也可根据用户的需求任意更改。

进程间通信:这个系统可以支持一些简单的进程间通信机制,比如信号量、消息队列等等。

外设:在众多的外设之中,我们选择了应用范围比较广的串口。利用目前OSKit所提供的机制,支持更多的设备是不成问题的,但加在这个系统中意义并不十分的大。另外,需要强调的是,访问串口的方法,是由我们自己规定,不允许用户更改。

简单的内存管理:应用程序需要的内存可以通过malloc函数动态申请。


总体构想

在该操作系统中,我们规定用户只能创建用户级线程,所有对于核心的操作都将由系统来完成。线程采用RR调度算法,也就是时间片轮转法,并允许抢占,线程在该系统中有四种状态,它们是运行态、阻塞态、睡眠态和就绪态,用户可以设定和改变线程的优先级。在线程中用户可以动态申请内存,但是需要由用户来释放。线程间提供简单的通信机制,包括信号量和消息队列。

在设备方面,对于终端,本系统将提供简单的输入输出功能。而对于串口的功能,本系统可以完整的实现,也就是说,用户既可以从串口读也可以向串口写。

为了方便起见,我们为这个系统起一个很简单的名字,叫acos,这个名字并没有什么实际的意义,仅仅为了叫起来方便。

虽然OSKit已经为我们提供了很多设计操作系统所必须的功能,比如内存分配中的malloc函数,通过Multiboot引导等等。然而,仍有许多工作是要由我们自己完成。


5.3 我们自己所完成的工作:

5.3.1 系统的启动

这里我们所说的系统启动,并不是指通常人们所说的通过软盘或硬盘引导,而是在引导结束之后,操作系统的核心进程如何启动第一个用户进程。通常,在UNIX系统中,内核启动之后,内核程序会去执行文件系统上的init程序,这个程序是整个操作系统中的第一个用户进程,剩下的进程,都必须通过这个进程才能启动。在acos中,我们规定,用户必须要提供一个acos_init() 函数,系统在启动初始化工作全部完成之后,acos_init( )函数将被作为系统中的第一个进程启动。这之后的工作,就完全由用户来完成了。

当进程acos_init( ) 结束时,整个系统也将终止运行。

在acos_init( )启动之前,究竟还有哪些事情需要做呢?下面我们就列出要做的事情:

对整个系统进行初始化,注册设备,检测设备,初始化设备需要的数据结构,初始化线程库,创建线程需要的数据结构。

5.3.2 线程管理

所谓操作系统的线程管理,一般包括了一下的三大部分:线程的创建,线程间通信和线程调度。

线程的创建包括了初始化线程和创建线程的属性。

线程间通信中,我们使用了信号量,条件变量。

在线程调度中,用户线程只能使用时间片轮转法作为调度算法,并且规定在调度过程中调度算法不允许更改。

5.3.3 外设(串口)

在UNIX系统中,所有的设备都可以通过文件系统进行访问,读、写设备就像读写文件一样。在我们设计的这个acos中,并没有对文件系统提供支持,也不提供像读写文件系统一样的方法来访问设备。但是,我们提供一套基本的函数,供用户线程对串口进行读写操作,这些函数专门用于处理串口。

一个设备在同一时刻只能被一个线程使用,因此,在使用设备以前要申请,只有申请成功的线程才能够使用设备。


5.4 用户手册

5.4.1 安装

用户需要在Linux下键入 tar -zxvf acos-0.0.1.tar.gz 。
./setup,详细的安装信息请参见README文件。
进入usr/local/bin/acos之后,根据我们所提供的API手册编写自己的应用程序,然后将这些应用程序的源文件编译成一个"filename.o",用户通过执行acbuildfilename.o命令将应用程序链入操作系统内核,根据你所使用的核心格式执行mkXXXimage命令生成所需要的核心映像,选择恰当的装入程序装入启动核心映像。

5.4.2 用户编程接口

如果您对操作系统的概念有所了解,并详细阅读了我们为您提供的技术文档之后,便可以利用用户编程接口自行开发了。以下我将向各位详细介绍每个接口所提供的功能,希望对您能有所帮助。

一、管理线程的API

1.1 初始化线程
ac_init_pthreads( );

1.2 线程创建
ac_thread_create ( void *name, void *arg,
int *out_thread_id);

1.3 设置线程的优先级
ac_thread_setprio(int thread_id, int pri);

1.4 撤销线程
ac_thread_cancel(int thread_id);

1.5 线程休眠
ac_thread_sleep(ac_s64_t millisecond);

1.6 将该线程与某一个线程相关联
ac_thread_join(int tid, void **status);

1.7 撤销该线程与某线程的关联
pthread_attr_setdetachstate ( pthread_attr_t *attr, intdetachstate )

二、管理互斥量的API

2.1 初始化互斥量
ac_mutex_init ( ac_pthread_mutex_t* mutex,
ac_pthread_mutexattr_t* attr);

2.2 设置互斥量协议
ac_mutexattr_setprotocol ( ac_pthread_mutexattr_t* attr, intprotocol);

2.3 设置互斥量类型
ac_mutexattr_settype ( ac_pthread_mutexattr_t* attr, inttype);

2.4初始化互斥量属性
ac_mutexattr_init(ac_pthread_mutexattr_t* attr);

2.5互斥量加锁
ac_mutex_lock(ac_pthread_mutex_t *mutex);

2.6互斥量解锁
ac_mutex_unlock(ac_pthread_mutex_t *mutex);

2.7撤销互斥量
ac_mutex_destroy(ac_pthread_mutex_t *mutex);

三、管理IPC的API

3.1消息发送( IPC机制 )
ac_ipc_send ( int thread_id, void* msg,ac_size_tmsgsize,ac_s32_t timeout);

3.2消息接受( IPC机制 )
ac_ipc_recv(int thread_id, void* msg,ac_size_tmsgsize,ac_size_t *actual, ac_s32_t timeout);

3.3消息等待( IPC机制 )
ac_ipc_wait(int *src_id, void *msg,ac_size_tmsgsize,ac_size_t *actual, ac_s32_t timeout);

3.4接受同步消息
ac_ipc_reply(int desthread, void *msg, ac_size_t msg_size);

3.5发送同步消息
ac_ipc_call(int dst, void *send_msg, ac_size_tsend_msg_size,
  void *recv_msg, ac_size_t recv_msg_size,
  ac_size_t*actual,ac_s32_t timeout);

四、管理条件变量的API

4.1条件变量初始化
ac_cond_init(ac_pthread_cond_t *cond, ac_pthread_condattr_t*attr);

4.2某线程等待条件变量
ac_cond_wait(ac_pthread_cond_t *cond, ac_pthread_mutex_t*mutex);

4.3唤醒等待该条件变量的线程
ac_cond_signal(ac_pthread_cond_t *cond);

4.4符合条件变量的线程广播
ac_cond_broacast(ac_pthread_cond_t *cond);

五、硬件部分

5.1打开串口
ac_serial_open(int serial_num, ac_u32_t flags);

5.2监听串口
ac_serial_listen(int serial_num, ac_u32_t flags);

5.3关闭串口
ac_serial_close(ac_ttystream_t* a_stream);

5.4从串口读
ac_serial_read(ac_ttystream_t* s, void *buf, ac_u32_t len,ac_u32_t*out_actual);

5.5往串口写
ac_serial_write(ac_ttystream_t* s, const void *buf,ac_u32_t len,ac_u32_t *out_actual);


5.4.3 应用程序示例

为了能让各位老师对我们的演示系统一目了然,我们特地选取了在操作系统中比较有代表性的4个例子供大家欣赏,它们是,哲学家算法,时钟演示,线程调度和中断响应。

所需的资源:演示前三个例子需要一台intel386或更新的计算机,一张1.44MB的软盘;演示第四个例子除了上述设备之外,还需要一根串口线,一台装有Windows9x的计算机作发送机,并在其上安装有能向串口发送信息的软件。

1.哲学家算法

哲学家算法也称哲学家进餐的同步问题,最早是在1965年由Dijkstra提出并解决的。下面我就简单描述一下这个经典IPC问题的大意:7个哲学家围坐在一张圆桌周围,每个哲学家面前有一个碟通心面,由于面条很滑,所以要用两把叉子才能夹住。相邻两个碟子之间有一把叉子。哲学家的生活包括三种状态:即吃饭,思考和挨饿,当一个哲学家觉得饿时,他试图分两次去取他左边和右边的叉子,每次拿一把,但不分次序。如果成功地获得了两把叉子,他就开始吃饭,吃完以后放下叉子继续思考。这里的问题是:为每一个哲学家写一段程序来描述起行为,要求不能死锁。

在这个例子中我们设定了7个哲学家,且不允许哲学家饿死。我们为每个哲学家创建一个线程,把叉子看作条件变量,让线程等待条件变量,如果满足条件,则线程执行(哲学家的吃饭状态),执行一定时间之后,释放条件变量,进入休眠状态(哲学家的思考状态),当休眠时间过去之后,线程被唤醒,重新等待条件变量(哲学家的挨饿状态)。

2.时钟演示

计算机是以严格精确的时间进行数值运算和数据处理的,最基本的时间单元是时钟周期,OSKit最小可以精确到纳秒一级,其时钟系统的特点是允许在系统时钟之上创建自己的时钟,用于控制周期性线程。

我们的演示系统的源代码看似比较简单,但实际上却是建立在一个庞大的时间系统之上的,这个时间系统是整个操作系统活动的动力。

我们的演示程序将演示控制计算机的时钟计时( 每400毫秒显示一次 ),计时10次后返回初始菜单。

3.线程调度

在此个演示程序中,一开始有两个线程,它们所采取的调度算法是时间片轮转法,与此同时还有一个优先级最高的线程在休眠,它每400毫秒睡醒一次,打断此时处于运行状态的线程,那个优先级最高的线程转执行态,等它执行完它的时间片之后,前两个线程继续执行。

4.中断响应

这个例子演示了操作系统对外设的支持,我们选择了串口。演示这个例子,需要两台计算机和一根串口线,其中一台计算机安装我们的演示系统,另一台需要有一个向串口发送信息的软件,此软件我们是用Delphi编写的,运行在Windows98系统下,两台计算机用我们自己制作的串口线相连。演示程序的一开始,有两个优先级相同的线程在时间片轮转。当我们从另一台装有Windows98的机器上给演示机发送信息时,原来处于运行状态的两个线程被中断,演示机会成功的收到发送机发送的信息,然后等待操作人员的处理,是否继续运行该例子或者退出。


本 章 小 结

本章是为最后提供给各位老师欣赏的演示系统所写的,针对每个例子,先从理论上论述了它的概念以及我们是怎么实现的,然后附上了部分程序。

注意:由于演示系统所使用的OSKit的系统调用基本在前三章已经逐一做了详细的介绍。

[目录]


第六章 结束语

《OSKit的线程机制 第六章》


OSKit的线程机制 第六章2000年12月25日22:34


作 者: 汤海京


导师:陈朔鹰


第六章 结束语

OSKit的线程机制既遵照了POSIX标准,又在一些地方有所突破,诸如实时部分,这些都是开发人员对操作系统以及自由软件的贡献。

OSKit虽然还有许多的不足,但由于它出现在Linux之后,所以其许多设计思想弥补了Linux中的不足(比如在线程通信部份OSKit就没有照搬Linux,而是扬长避短,去掉了管道型通信方式),而又由于OSKit每三个月更新一次,不断的增加一些新的算法和设计思路(如对实时操作系统的支持,这在Linux下是很少出现的,这是因为Linux的设计者们并不认为实时操作在单机系统的日常应用中能占多大的比重,但OSKit的设计者们从操作系统完整性的角度出发,为我们提供了许多实时的系统调用),这使得它总能站在操作系统设计的最前沿。

其实,自由软件的魅力远远超过了微软的视窗,但现在的开发人员总是把一部分精力集中在了对kernel的裁剪之上,而OSKit的出现恰恰弥补了这项缺陷,仿佛为自由软件事业的腾飞插上了一双翅膀,使得我们更加有理由相信自由软件事业终将蓬勃发展。

OSKit在美国的一些大学里已经得到了的应有的重视,并有一个研究组用OSKit开发完成了一个JAVAOS,而且在美国犹它大学计算机科学系FLUX研究组的主页上,还有一个专门的链接,里面都是用OSKit开发的项目简介,可见其已经有了一定的用途。但在中国的大学里,了解程度还远远不够,据我们所知,目前只有中科院的一个课题组用OSKit完成了一个小型的操作系统。

众所周知,国内在自由软件领域的成就是远远落后于国外的,所以我们课题组对OSKit做了一定的分析和探索,算是抛砖引玉,希望国内操作系统的爱好者们能给予充分重视。让我们共同努力,推进中国自由软件事业的发展。


后 记

以上是我对我们课题组所做研究工作,以及我个人工作的介绍。通过几个月以来我对OSKit的分析和研究,加深了对操作系统原理的认识,并对其实现有了具体的认识,提高了我的分析能力,丰富了实践经验。

我衷心地感谢我的导师陈朔鹰老师给予我的悉心指导,张丽芬以及赵小林老师也给予我们课题组以热心的帮助,我们对901教研室的全体老师表示衷心的感谢。


参考文献

·OSKIT文档:http://www.cs.utah.edu/projects/flux/oskit/
·《操作系统的设计与实现》,北京理工大学出版社,张丽芬著
·《COM原理与应用》,清华大学出版社,潘爱民著
·《计算机环境的可移植操作系统界面POSIX.1》,电子工业出版社,中软总公司第二开发部译
·《Linux操作系统内核分析》,人民邮电出版社,陈莉君编著
·《Linux编程白皮书》,机械工业出版社,(美)David A.Rusling等著,朱珂等译
·《操作系统︰设计与实现(第二版)》,OPERATING SYSTEMS Design and Implementation(SecondEdition),电子工业出版社,ANDREW S.TANENBAUM,ALBERT S. WOODUHULL 著,王鹏,尤晋元,朱鹏,敖青云译校
·《UNIX操作系统设计与实现》电子工业出版社,李建国主编
·《UNIX操作系统设计》The Design of the UNIX Operation System,机械工业出版社,(美)Maurice J.Bsvh 著 王旭等译
·《实用UNIX编程》, 机械工业出版社,(美)Kay A. Robbins,Steven Robbins 著,刘宗田等译
·《Advance Programming in the UNIX Environment》,(美) W. Richard Stevens
·《Linux 上的C 编程》,中国电力出版社,怀石工作室编著
·《Linux从入门到精通》,The Linux A-Z,电子工业出版社,(英)Phil Cornes 著,童寿彬等译,夏道藏审校
·《Linux 操作系统及实验教程》,机械工业出版社,李善平等编著
·《Linux 常用技术大全》,Linux Complete,电子工业出版社,(美) Grant Taylor 著,邱仲潘等译
·《计算机操作系统原理与技术》,西安交通大学出版社,陆丽娜,齐勇,白恩华主编
·《UNIX通信与Internet》,UNIX Communications and the Internet (第三版),沈奇,王健 李玲等译
·《Linux权威指南》,中国电力出版社,怀石工作室编著
·《Using Threads in Interactive Systems:A Case Study》, HASUDR, C., JACOBI,C.,THEIMER
·《POSIX Programmer's Guide》,O'Reilly, LEWINE


[目录]


部件对象模型

OSKit的部件对象模型 目录
2000年11月30日 19:38


作 者: 洪宁


导师:陈朔鹰

目录
----------------------------------------------
前言

第一章 OSKIT介绍
1.1 OSKIT概述
1.2 OSKIT的组成
1.3 OSKIT的设计原则
1.4 OSKIT的配置
1.5 OSKIT的编译
1.6 OSKIT的使用
1.7 OSKit的执行环境

第二章 COM规范介绍
2.1 COM的基本概念
2.1.1 组件技术
2.1.2 什么是COM
2.1.3 COM结构
2.1.4客户/服务器模型
2.2 COM对象
2.2.1 COM对象的标识-CLSID
2.3 COM接口
2.3.1 接口的定义和标识
2.3.2 接口的一些特点
2.4 Iunknown接口
2.4.1 引用计数
2.4.2 实现引用计数
2.4.3 接口查询
2.4.4 COM对象的接口原则
2.5 COM特性
2.5.1语言无关性
2.5.2 进程透明特性
2.5.3 可重用性

第三章 OSKIT中COM机制的实现
3.1 OSKIT中COM的基本定义
3.2 实例分析Iunknown接口的实现
3.2.1 C环境对象的结构
3.2.2 C环境接口及其对Iunknown接口的实现

第四章 OSKIT中的接口注册机制
4.1 概述
4.2 数据库对象
4.3 服务接口的定义
4.4 数据库服务接口的实现
4.4.1 创建数据库
4.4.2 在数据库中注册接口
4.4.3 在数据库中注销接口
4.4.4 查找用某iid注册的第一个接口
4.4.5 查找用某iid注册的所有接口
4.4.6 克隆数据库

第五章 OSKIT的启动及系统初始化
5.1 概述
5.2 核心装入程序-Multiboot
5.2.1 Multiboot简介
5.2.2 Multiboot规范
5.2.3 Multiboot进行初始化的步骤
5.3 OSKIT中软件环境的初始化
5.3.1 软件环境初始化的步骤
5.3.2 软件环境初始化的实现

第六章OSKit的应用实例:一个简单系统的设计与实现
6.1设计目的
6.2系统的功能
6.3我们自己所完成的工作:
6.3.1系统的启动
6.3.2线程管理
6.3.3 外设(串口)
6.3.4 应用程序示例

第七章 结束语

后 记

参考文献

[目录]


前言

《OSKit的部件对象模型 前言》


OSKit的部件对象模型 前言2000年11月30日19:38


作 者: 洪宁


导师:陈朔鹰

课题名称:基于面向对象的操作系统开发平台(OSKit)的分析与程序设计

课题来源:自拟题目

如果说自由软件的出现是一个偶然的话,那么,席卷全球的Linux热潮则是一个奇迹,他正以势不可挡的趋势迅猛发展,其前途不可限量。

Linux内核源代码的开放给希望深入操作系统内部世界的人们提供了可能,但随之而来的问题是,当我们要开发自己的操作系统时,由谁来读系统的kernel部分呢。对这部分的处理从逻辑上分析不外忽三种方式:全部保留,对其进行裁减,全部推倒重来,很显然,最后一种方法是不可能的,而如果我们采用的是第一种方法,其结果当然一定可以满足我们的要求,但是,最后编译出来的核心将十分的庞大,尤其是对嵌入式操作系统的开发者来说,是不能忍受的,所以,大多数开发者采用的第二条路。

但是,第二条路也非平坦的大道,道理很简单,你要想对kernel进行裁减,首先你应该将全部的源代码阅读一遍,并且将其中的相关性理顺,然后才能谈到裁减,所以工作量也十分的庞大。

然而,OSKit的出现改变了这一切,它使得我们不需要将精力集中在kernel源代码的阅读上,因为kernel部分的源程序已经由OSKit的开发人员替你分析过了,他们将源码全部模块化,并将所有模块之间的相关性写在了文档之中,呈现在你的面前,这与你自己分析源代码的结果是一样的。

OSKit最本质的东西和Linux一样,体现在"自由"和"开放"的思想,"自由"意味着世界范围内的知识共享,由于OSKit出现在Linux之后,其设计思想继承了Linux的精髓,所以说它的出现并不完全是美国犹它大学计算机科学系FLUX研究组的功劳,而应该是"自由"的结果。"开放"则意味着OSKit对所有的人都敞开大门,在这种开放而自由的天地里,你可以中分发挥自己的创造才能。

在后面的几章里,我将向大家系统地介绍我的全部研究成果-OSKit的COM机制及初始化过程,希望读者能在最短的时间内了解OSKit,更希望大家能本着"自由"与"开放"的精神加入到我们的研究中来,为中国的自由软件事业贡献自己的一份热情。

由于本人能力有限,论文中的有些术语表达可能不妥,内容也可能不够准确,敬请各位老师和同学批评指正,本人不胜感激。

这篇论文主要论述了四部分内容:
第一部分:OSKit简介
第二部分:COM规范介绍
第三部分:OSKit中COM机制的实现
第四部分:OSKit的启动及初始化过程

洪宁2000年6月19日

[目录]


第一章 OSKIT介绍

《OSKit的部件对象模型 第一章》


OSKit的部件对象模型 第一章2000年11月30日19:55


作 者: 洪宁


导师:陈朔鹰

第一章 OSKIT介绍

1.1 OSKIT概述
  OSKIT是美国犹它大学计算机科学系FLUX研究组编写的一套用于架构操作系统内核、服务器和其他OS级软件的框架及模块化的部件和库程序。OSKIT的编写者认为,操作系统中有很大一部分模块是系统必须的,但并不是开发者所感兴趣的,例如系统装入模块,各种标准驱动模块等。使用OSKIT的目的就是使操作系统的开发者集中精力开发他们操作系统中有特色的,或者他们感兴趣的部分,而不必考虑一些繁琐而乏味的细节。为了达到这个目的,OSKit在设计时借用了COM的思想,把操作系统的各个部分设计成尽量独立的COM模块,以方便操作系统的开发者使用或替换。因此,当开发人员使用这套工具时,可以把它当作一个完整的操作系统来使用,也可以根据需要使用其中的一部分,它还可以作为一套动态链接库,由操作系统及支持程序对它进行调用。

1.2OSKIT的组成
  OSKIT把给开发者提供的功能分为三个部分:接口、函数库和部件库。
  OSKIT的接口非常清晰,而且是面向对象的,它采用了部件对象模型(COM)的架构,在文章的后续部分会对OSKIT中的COM接口的组织和结构进行详细的讨论。
  OSKIT的函数库使用传统的C语言,并采用面向函数的方式向用户提供服务。例如基本的C库函数,这些函数易于使用和控制,目标操作系统能很方便的使用其中的某个函数,而且各个函数之间的依赖关系已被缩减至最小。这些函数库中很少使用OSKit自己的COM接口,取而代之的是在普通的C头文件中定义面向函数的接口。这种设计策略为操作系统设计人员提供了最大程度的支持和灵活性,以适合任何特殊的操作系统环境。
  下面是OSKIT目前提供的主要函数库列表,以及它们的简介:
  liboskit_c:一个简单的最小限度的C库,它在一个受限制的OS环境中提供了通用的C库支持,并使对环境和其他模块的依赖达到最小限度。在需要一个更完整的、类POSIX的C函数库的情况下,可以使用FreeBSD的C函数库,甚至是用自己编写的函数库来代替这个最小化的C库。
  liboskit_kern:建立一个基本的操作系统内核运行环境所需的内核支持代码,该函数库为陷入和中断等提供缺省的处理程序。这个库包括很多在写核心代码时非常有用的通用工具,如访问某个CPU寄存器,建立并维护页表,在不同的处理器模式间转换等。
  liboskit_smp:内核支持代码,这个库可建立一个便于操作系统使用的多处理器系统环境。
  liboskit_com:处理COM接口的工具函数和一套通用的包装部件。
  liboskit_dev:这个库提供了驱动程序和由其它操作系统引入的部件(如网络、文件系统)在OSKit中使用时所需要的转换函数的缺省实现,这些函数工作在OSKit驱动程序框架之中。这些函数的缺省实现是为使用了liboskit_kern库的简单内核所设计的,开发者在必要时可以重写部分或全部函数。
  OSKIT的部件库提供了一个一般情况下更高层的功能,这种功能以一种更加标准、精心修整的、面向对象的"黑盒"方式设计。尽管OSKit的"部件"缺省情况下也是被打包成为普通的链接库,但它们的结构则是被设计成面向对象的,而不是传统的面向函数的方式。与OSKit的函数库相比,部件库通常只对外开放一些相关的公用调用接口,而不是大量的功能。例如,在Linux和BSD驱动程序部件库,每一个完整的驱动程序仅实现成为一个单独的函数调用,这个调用用来初始化并注册驱动程序。目标系统通过OSKit的面向对象的COM接口来与这些部件进行交互,这允许很多部件及部件的实例共存,并以操作系统开发人员定义的方式交互。这种设计的策略有利于将已有系统(如BSD和Linux)中的代码并入OSKit,那时,隐藏原有环境的细节要比提供灵活性更重要。
  下面是目前OSKit所提供的主要部件库的概要。
  liboskit_posix:增加对用POSIX构造的系统通常会实现的系统调用的支持。例如:open、read和write等。这些POSIX操作被映射到相应的OSKitCOM接口上。最小化的C库和FreeBSDC库都依赖POSIX库来提供所需要的系统级操作。
  liboskit_freebsd_c:源于FreeBSD的完整的类POSIXC库,提供了对单线程和多线程的支持。在需要支持类POSIX函数或线程安全时,这个库可以用来替换最小化的C库。
  liboskit_freebsd_m:完整的标准数学库。可以提供浮点数支持。
  liboskit_fsnamespace:文件系统名字空间库,为应用程序提供了命名接口,可以将多种部件的绝对和相对路径名转换成为oskit_file和oskit_dir的COM对象。
  liboskit_rtld:运行时链接、装入库,允许ELF格式的OSKit内核装入共享库。
  liboskit_lmm:一个灵活的存储管理库,它可以管理物理内存或虚拟内存。
  liboskit_amm:地址映射管理库,用于管理系统资源。
  liboskit_svm:简单虚存库,它使用AMM库定义了一个简单的用于单独地址空间的虚存接口,提供内存保护和为块设备如一个硬盘分区进行分页。
  liboskit_threads:这个库提供了多线程的内核支持,包括POSIX线程、同步、调度和栈保护。
  liboskit_diskpart:一个能够识别多种常见的磁盘分区方案并生成一个完整映射的通用库。这个库提供了一个简单的方法让操作系统可以找到它所"感兴趣的"磁盘分区。
  liboskit_fsread:一个简单只读文件系统解释库,它可用于不同的文件系统,包括BSDFFS、Linuxext2fs和MINIX的文件系统。它通常与liboskit_diskpart一起使用,为操作系统提供从硬盘或软盘上读取程序和数据的功能。这个库也可用于构造装入程序。
  liboskit_exec:一个通用的可执行程序解释器和装入器,它支持流行的可执行程序格式,如a.out和ELF。
  liboskit_linux_fs:对Linux内核2.0.29版的文件系统部分代码进行包装。
  liboskit_netbsd_fs:对NetBSD1.2版的文件系统部分代码进行包装。
  liboskit_memfs:一个简单的基于内存的文件系统,对外提供了标准的OSKit文件系统接口。
  liboskit_freebsd_net:对FreeBSD2.1.7.1版网络代码的包装。
  liboskit_linux_dev:对Linux内核2.0.29版的设备驱动程序进行包装。将其包装在OSKit的设备驱动架构中。
  liboskit_freebsd_dev:对FreeBSD2.1.7.1版的设备驱动程序进行包装。将其包装在OSKit的设备驱动架构中。

1.3OSKIT的设计原则
  尽管当前OSKIT的目标平台是X86体系,但将来它会被移植到其它的体系,如StrongArm。
  OSKIT的接口被尽可能地设计成便于移植。尽管很大一部分的OSKit代码是与平台相关的,但它很多的功能接口都是通用的,并且可以在很多其它的体系结构或平台上使用。例如,OSKit的设备驱动程序接口是完全可以移植的。
  OSKIT在每一个库中记录模块间的依赖信息。对于函数库,这意味着要标明不同的函数间的依赖关系;对于部件库,这意味着要标明每一个库中不同部件集之间的依赖关系。

1.4OSKIT的配置
  OSKit遵从GNU的配置、编译和安装习惯:可以参考源代码根目录中的INSTALL文件,在该文件中有使用GNU配置文件的通用方法。简单的说,需要运行OSKit根目录中的配置文件,这个文件将会估计出当前系统的类型以及各种所需工具(如C编译器)的位置。OSKit可以配置成在它自己的目录中编译,只要在OSKit源码的根目录中用./configure命令就可以了。或者可以将OSKit编译并安装到一个单独的目标目录中,如果需要这样,只要在那个新的目录中运行配置文件就可以了。例如,当源文件存在NFS分区上时,使用一个单独的目标目录,可让你把目标文件放在本地的硬盘上。
  此外,OSKit的配置还可以保留多份(当然是在不同的目录中),每一套配置有自己的目标文件,但共享一份代码。
  当需要为另一种体系结构对OSKit进行交叉编译时,需要指定宿主机(需要运行OSKit的机器)的类型和编译机(编译OSKit的机器)的类型,这可以用configure中的-build=machine和-host=machine选项来实现。由于OSKit是一个单独的软件包,并且不使用任何它自己以外的包含文件和库文件,因此宿主机的操作系统部件与OSKit的配置并没有直接的关联。然而,在配置脚本运行时会需要以对宿主机的指定作为一个前缀来寻找各种交叉编译工具。例如,如果指定"-host=i486-linux",配置脚本会去寻找名为i486-inux-gcc、i486-linux-ar和i486-linux-ld等的编译工具。在其它工具中,具体使用哪一个工具,由目标文件的格式来决定(如ix86-linux-*工具可以建立ELF格式,而ix86-mach-*工具建立a.out格式)。
  OSKit的配置脚本支持很多基本的选项,要得到一份完整的选项列表,可以运行./configure-help。除了这些基本的命令之外,配置脚本还支持下面的这些OSKit特有的选项:
  -enable-debug:在编译时使用这个选项,将会把调试信息包含进来,这会影响一些运行的性能,但当错误有可能发生时,会提高检测错误的速度。
  -enable-profiling:生成剖析版的OSKit库,为了与习惯的标准保持一致,剖析版的库将会有-p作为后缀。
  -disable-asserts:不使用assert()调用
  -enable-unixexamples:生成在Unix的用户态下运行OSKit的一些部件的支持代码和例子程序,目前支持FreeBSD和Linux。
  -enable-doc:在编译时生成OSKit的文档。这样做除了要花费很多时间以外,还需要LaTex和dvips。已经生成的.ps和.html格式的文档在源码目录中。
  在开始实际编译OSKit之前,可以做以下的一些事情忽略那些不需要的部分,使编译工作完成的更快一些:
  修改<oskit/dev/linux_ethernet.h>,使其只包括需要的行。除了可以让编译速度更快以外,对于某些不兼容的硬件,也需要这样做,以防止死机。
  修改<oskit/dev/linux_scsi.h>,使其只包括需要的驱动程序。
  修改<oskit/fs/linux_filesystem.h>,使其只包含需要的文件系统。
  修改源码根目录下的GNUmakefile,设定SUBDIRS,使其只有需要的目录。只有在对OSKit有了一些使用经验之后并且知道你需要哪些目录以后,才可以这样做。直接在GNUmakefile.in中指定subdirs也可以达到这样的目的,那样做的好处是防止以后再次进行配置时覆盖曾经做过的修改。
  设定环境变量CFLAGS为-O1-pipe。这会让编译器在编译时所做的优化工作比较少,并且在编译器不把临时文件写到内存虚拟的文件系统时提高编译的速度。

1.5 OSKIT的编译
  目前,编译OSKit需要以下的工具:
  GNU make
  GNU CC (gcc)2.95.2版。较新版本的gcc和egcs应当也可以使用,但没有测试过。
  GNU binutils 2.8.x或2.9.1 with BFD2.9.1。
  要编译OSKit,可以到源码的根目录(或者是用户指定的独立的目标目录)中运行GNUmake(在Linux系统上是make,在BSD系统上是gmake)。请注意OSKit需要GNUmake,不能用其它的make工具来代替它。为了防止混淆,OSKit的makefile都命名为GNUmakefile,而不是Makefile,这样的话,当用户错误地运行了其它的make工具时,它将报告找不到makefile的错误,而不是一些其它的错误。
  要想只编译或重新编译OSKit的一部分代码(例如某个库),只需进入放置该部分的目标目录,然后在该目录中运行GNUmake。根目录中的GNUmakefile实际上不做任何事情,只是在其它各个目录中递归调用make命令。一些OSKit目录的编译需要其它的部分首先被编译,例如核心的例子在它们需要的OSKit库被编译之前是无法编译的。但OSKit的大部分库都可以在完全不需要其它库的情况下被编译。
  当OSKit编译好以后,用户可以用"makeinstall"命令来安装它。缺省的情况下,这些库会被安装到/usr/local/lib中,而头文件会被安装到/usr/local/include中,除非用户在配置时使用了-prefix选项来指定目标目录。所有的OSKit头文件都安装在oskit/子目录中(例如/usr/local/include/oskit),这使它们不会和已经存在的任何头文件冲突。即使是OSKit库被安装在主库的目录中(如/usr/local/lib),所有的OSKit库文件都有前缀oskit_,这可避免与其它不相关的库相混淆。例如,OSKit的最小化C库命名为liboskit_c.a,而不是libc.a,这使用户可以在同样的目录中安装一个实际的C库。
  标准的make变量如CFLAGS和LDFLAGS被OSKit的Build工具所使用,但是在OSKit的makefile中没有定义,它们可以在命令行上被使用。例如,你可以用命令"makeCFLAGS="-save-temps""来使GCC把它编译过程中产生的中间文件留在目标目录中(我们内部规定,OSKit的makefile中的变量都以OSKIT_作为前缀。)

1.6OSKIT的使用
  要使用OSKit,可以在开发核心时使用OSKit提供的部件库、函数库,并在编译时将所使用的库链接到核心映象中。在OSKit的examples目录下有许多的例子核心,开发者可以通过查看以及运行这些核心来学习OSKit中各种库和映象Makefiels的编写方法。
  OSKit中的库都进行了很好的设计,用户可以根据需要替换其中的任何一个库,由于在OSKit的文档中对每个库与其它库的依赖关系进行了详细的说明,因此用户可以很轻松的替换掉某个部件,而不会带来麻烦。实际上,在很多情况下,特别是在某些函数库中,为了有效的使用OSKit,是必须要替换掉其中的一些函数或者符号的。要重载某个库中的函数或者符号,只要在核心或应用程序中再定义一遍就可以了,链接程序会确保去使用所定义的函数和符号。OSKIT设计者强烈建议使用链接的方法去替换掉OSKit的某个部件,而不是直接修改OSKit的源码(除了修改源码中的Bug)。保持自己的核心和OSKit分开,会使升级到新版本的OSKit更加容易。
  当完成核心的编写工作后,使用gcc中的-c选项把核心源码编译成一个或多个.o文件,然后创建Gnumakerules文件,把写好的.o文件与核心中使用的其它库文件按gcc的规范写入这个文件中。运行make,这些文件会被链接成一个核心。
  启动核心有几种方法:当OSKit配置成在Linux或其它的使用ELF可执行文件结构的主机上使用时,就可以使用OSKIT提供的mklinuximage将核心制作成一个标准的可以从LILO或其它Linux引导程序引导的内核映象。当OSKit配置成在Mach或BSD系统上工作时,就可以使用OSKIT提供的mkbsdimage建立一个a.out格式的映象,这个映象可以从任何BSD或Mach的引导程序引导。需要注意的是mkbsdimage需要GNU ld能够正常的工作,在BSD系统上,通常都是不使用GNU的ld的,用户必须要自己手动编译并安装GNU的ld。当目标机以DOS为基础(如i386-msdos或i386-moss)时,就要使用mkdosimage。和mkbsdimage一样,mkdosimage也需要GNU的ld,必须要首先安装GNU的ld,并且将其配置成为可以交叉编译MS-DOS的文件,然后mkdosimage才能正常的工作。此外,OSKIT还提供了mkmbimage脚本,与其它的脚本不同,它不会做任何的转换,它只是允许将内核与其它的文件合成一个MultiBoot(有关Multiboot的问题,请见第五章)映象,这个映象可以和MultiBoot引导程序(如GRUB和NetBoot)一起使用。如果在你的系统上安装了Perl,就可以用一个同样功能的程序mkmb2来代替它,这个程序工作的更快一些。它的用处和mkmbimage是一样的。
  当用OSKIT制作的核心启动时可以附加命令行参数,不同的引导程序可以将它们转化成自己的命令行格式,OSKIT对命令行参数规定了格式:
  progname[<boot-opts and env-vars> -]<args tomain>
  注意,如果没有"-"的话,所有的参数都将被传递给main函数。
  缺省的OSKitMultiboot启动代码会把这些字符串转换成为C风格的argv/argc参数,环境变量数组,和记录在全局变量oskit_bootargv/osit_bootargc中的启动参数。
  argv/argc和环境变量数组会传递给main程序,后者通常作为一个名为envp的第三参数。oskit_bootargv/oskit_bootargc中的引导参数会被缺省的OSKit控制台启动代码解释,并且下面的标志有着特殊的意义:
  -h:使用串口作为控制台,参考-f标志。具体使用的串口由libkern中的base_console.c中的变量cons_com_port来控制。
  -d:激活通过串口使用GDB,具体使用的串口由libkern中的base_console.c中的变量cons_com_port来控制。这个串口可以不同于控制台的串口,实际上最好是这样。
  -p:激活剖析功能。操作系统的内核必须是编译成为支持剖析功能的。
  -k:打开killswitch支持,这允许向第二个串口上传送字符以中止内核的运行。
  -f:当使用一个串行的控制台时,使其工作在115200的速率而不是缺省的9600。这是Utah的扩展而并不在BSD之中。
  这些参数都是以BSD为中心指定的,这是因为在犹它大学OSKIT的设计者通常都是用FreeBSD的引导程序引导内核。
  此外,如果使用了NetBoot引导程序,在oskit_bootargv中还会有另一个参数:
  -retaddraddress
  这个参数指定一个OSKit可以跳到并且将控制权还给NetBoot的物理内存地址。Libkern中的bas_console.c中的缺省的_exit函数在退出时使用这个值。

1.7OSKit的执行环境
  OSkit中的许多部件在核心和用户方式下都可以使用,这就需要对部件的执行环境作出定义,例如部件什么时候可以嵌套进入等。此外,OSKIT使用了许多其它操作系统的代码,例如设备驱动程序和网络协议栈,都是原封不动的从原有的核心如BSD和Linux中借用来的,OSKIT通过附加代码模拟原始执行环境使得这些执行模块比它们原始执行环境更简单,用户也不需要详细了解原执行环境的细节。下面对OSKIT的每种执行模块进行简单的介绍。
  纯执行模块。这是OSKIT执行环境中最简单的模块。这些部件或函数中没有全局变量或静态变量,只使用和处理目标环境传递的数据。例如函数strlen,它只通过目标环境传递给它的字符串指针求出字符串的长度,并将其返回,它只接触参数数据域而不影响目标环境。当这些函数使用的数据集是分离的,它们可以安全地同时被调用,而不需要同步,反之则不行。例如对重叠的目标缓冲区并发使用memcpy调用是不安全的。
  非纯执行模块。这些模块中使用了全局变量或有可能改变全局共享状态,例如liboskit_kern(核心支持库)中的许多函数建立和访问全局处理器寄存器和数据结构,因此它们是非纯执行模块。非纯执行模块有以下特点:
  *非纯函数和部件可能依赖于全局状态,如全局变量、静态变量、处理器中特殊寄存器等。
  *除非有明确的声明,非纯函数和部件是不可重入的,并且运用在多线程系统中也是不安全的。为了在一个多线程/多处理器环境中使用这些函数和部件,目标操作系统必须提供适当的同步代码。
  阻塞模块。它扩展了非纯模块以支持非抢占的多线程,这些模块中有一类可重入的函数称为阻塞函数,在这模块中,除非明确声明为非阻塞函数,否则函数是阻塞的。为了在一个可抢占的、可中断的或者多处理器的环境中使用阻塞模块,必须在进入模块前加锁,在退出模块时将锁释放。
  可中断阻塞模块。在它之中每个部件都是一个单线程的执行域,在一个给定的时刻,只有一个(虚拟的或者物理的)CPU可以执行部件中的代码。例如:在一个多处理器系统中,在进程级,任意时刻在一个部件集内只有一个CPU被允许执行;这能够通过在部件前后放置全局锁来实现。
  此外,OSKit的执行环境还有以下特点:
  在一段时间内,部件中可以存在多个活动进程,但在某时刻只有一个进程被执行。
  目标操作系统给每个活动进程提供一个独立堆栈,这个堆栈在阻塞函数运行时被保留。只有在操作完成后,对部件的调用返回时才放弃该堆栈。
  部件中的代码总是运行在两个级别中之一,进程级或中断级。有意思的是一些部件的函数和方法只能在进程级被调用,而另一些只能在中断级被调用,还有的能在任何级别被调用。调用的细节属于接口描述的一部分。
  部件中无论进程级或中断级的操作都能被部件中的中断处理程序中断,除非代码调用osenv_intr_disable屏蔽了中断。
  当部件在进程级运行时,OSKIT假定中断开放,部件在处理过程中可能临时屏蔽掉中断,但必须在返回到目标操作系统前重新激活。同样,当部件在中断级运行时,OSKIT假定中断被屏蔽,但是部件可以在目标操作系统允许其它中断级别的活动中断该部件时重新激活中断。
  当目标操作系统在一个部件内中断一个进程级的活动时,在继续这个活动前,操作系统必须执行完这个中断级别的活动。同理,若一个中断级的活动被中断,那么最近的中断级别的活动必须在继续前一个中断级别的活动之前完成。
  部件中运行在中断级别的代码不能调用目标操作系统提供的阻塞回调函数;只有非阻塞的回调函数能够在中断级别被调用。


[目录]


第二章 COM规范介绍

《OSKit的部件对象模型 第二章》


OSKit的部件对象模型 第二章2000年12月3日21:33


作 者: 洪宁


导师:陈朔鹰


第二章 COM规范介绍

OSKit作为一套开发操作系统的工具,其最大的特色是在操作系统的整体设计中采用了COM的思想,将操作系统中各个不同功能的部分设计成独立的COM模块,使得操作系统的开发者能很方便地按照COM的规范开发出符合自己需要的功能模块,将OSKIT中的相应模块替换,但却能继续使用OSKIT中的其它模块。而且对每个COM模块内部的接口也可以进行改写,添加其未实现的功能,删除不必要的功能,最终实现真正满足自己要求的操作系统。这些OSKIT特性的实现无一不是有赖于COM的机制。因此,本章对COM中的基本概念和COM规范进行简单的介绍。

2.1 COM的基本概念

由于COM是由组件技术发展而来,因此在介绍COM机制之前有必要先对组件进行必要的说明,然后再对COM的基本概念进行说明。

2.1.1 组件技术

在计算机软件发展的早期,一个应用系统往往是一个单独的应用程序。应用越复杂,程序就越庞大,系统开发的难度也就越大。而且,一旦系统的某个版本完成后,在下个版本出来之前,应用程序不会再有所改变。而对于庞大的程序来讲,更新版本的周期很长,在两个版本之间,如果由于操作系统发生了变化,或者硬件平台有了变化,则应用系统就很难适应这种变化。所以这类单体应用程序已经不能满足计算机软硬件的发展需要。

从软件模型角度来考虑,一个很自然的想法就是把一个庞大的应用程序分成多个模块,每个模块保持一定的功能独立性,在协同工作时,通过相互之间的接口完成实际的任务。我们把每一个这样的模块称为组件,一个设计良好的应用系统往往被切分成一些组件,这些组件可以单独开发,单独编译,甚至单独调试和测试。当所有的组件开发完成后,把它们组合在一起就得到了完整的应用系统。当系统的软硬件环境发生变化或者用户的需求有所更改时,并不需要对所有的组件进行修改,而只需对受影响的组件进行修改,然后重新组合得到新的升级软件。这种组件化程序设计技术,不同于传统的结构化程序设计技术,也不同于现在被广泛采用的面向对象程序设计技术。可以说,组件化程序设计位于这两者之上,它更注重于系统的全局,要求从系统的全方位进行考察。

2.1.2 什么是COM

COM,即组件对象模型,是一种以组件为发布单元的对象模型,这种模型使各软件组件可以用一种统一的方式进行交互。COM既提供了组件之间进行交互的规范,也提供了实现交互的环境,因为组件对象之间交互的规范不依赖于任何特定的语言,所以COM也可以是不同语言协作开发的一种标准。COM不仅仅提供了组件之间的接口标准,它还引入了面向对象的思想。在COM规范中,把对象称为COM对象。组件模型为COM对象提供了活动的空间,COM对象以接口的方式提供服务,下图表明了COM组件、COM对象和COM接口三者之间的关系。

一个组件程序可以包含多个COM对象,而且每个COM对象可以实现多个接口。当另外的组件或普通程序(即组件的客户程序)调用组件的功能时,它首先创建一个COM对象或者通过该对象所实现的COM接口调用它所提供的服务。当所有的服务结束后,如果客户程序不再需要该COM对象,那么应该释放掉对象所占有的资源,包括对象自身。

2.1.3 COM结构

前面已经提到过,COM为组件和应用程序之间进行通信提供了统一的标准,它为组件程序提供了一个面向对象的活动环境。

COM标准包括规范和实现两大部分,规范部分定义了组件和组件之间通信的机制,这些规范不依赖于任何特定的语言和操作系统,只要按照该规范,任何语言都可以使用。在这里主要讨论COM规范,至于其在OSKIT中的实现,在后面的章节中讨论。

COM主要是由对象和接口两部分组成。对象是某个类(class)的一个实例;而类则是一组相关的数据和功能组合在一起的一个定义。使用对象的应用(或另一个对象)称为客户,有时也称为对象的客户。接口是一组逻辑上相关的函数集合,其函数称为接口成员函数。按照习惯,接口名常以"I"为前缀,例如"IUNKNOWN".对象通过接口和成员函数为客户提供各种形式的服务。

2.1.4客户/服务器模型

可以很容易看出,对象和客户之间的相互作用是建立在客户/服务期模型基础上的,客户/服务器模型的一个很大的优点是稳定性好,而稳定性正是COM模型的目标,尤其对于跨进程的程序通信,稳定性更会带来性能上的高可靠性。

客户/服务器模型是一种发展比较成功的软件模型,因为这种模型有以下一些优势:

稳定性好、可靠性高。客户/服务器模型简化了应用,把任务进行分离,客户和服务器各司其职,共同完成任务。

软件的可扩展性好。一个服务器进程可以为多个客户提供服务,客户也可以连接到不同的服务器上,这种模型的连接非常灵活。

可以很容易看出,对象和客户之间的相互作用是建立在客户/服务器模型的基础上的,客户/服务器模型的一个很大优点是稳定性好,而稳定性正是COM模型的目标,尤其对于跨进程的程序通信,稳定性更会带来性能上的高可靠性。

然而,COM不仅仅是一种简单的客户/服务器模型,有时客户也可以反过来提供服务,或者服务方本身也需要其他对象的一些功能,在这些情况下,一个对象可能既是服务器也是客户。COM能够有效地处理这种情况。

2.2 COM对象

在COM规范中,并没有对COM对象进行严格的定义,但COM提供的是面向对象的组件模型,COM组件提供给客户的是以对象形式封装起来的实体。客户程序与COM组件程序进行交互的实体是COM对象,它并不关心组件模块的名称和位置(即位置透明性),但它必须知道自己在与那个COM对象进行交互。

类似于C++语言中类(class)的概念,COM对象也包括属性(也包括状态)和方法(也称为操作),对象的状态反映了对象的存在,也是区别于其他对象的要素;而对象所提供的方法就是对象提供给外界的接口,客户必须通过接口才能获得对象的服务。对于COM对象来说,接口是它与外界交互的唯一途径,因此,封装特性是COM对象的基本特征。

COM对象可以由多种语言来实现,例如C++,JAVA,C(正如在OSKIT中)。如果用C++来实现COM对象,则很自然可以用类(class)来定义COM对象,类的每个实例代表一个COM对象,类的数据成员可用于反映对象的属性,而接口自然可以定义成类的成员函数。但在非面向对象语言,例如C语言中,对象的概念可能变成一个逻辑概念,如果两个对象同时存在,则在接口实现中必须明确知道所进行的操作是针对哪个对象的,这个过程可由COM接口的定义来保证。

2.2.1 COM对象的标识-CLSID

前面已经说过,COM组件的位置对客户来说是透明的,因为客户并不直接去访问COM组件,客户程序通过一个全局标识符进行对象的创建和初始化工作。如果从标识符的可读性来考虑,使用字符串是最简单的方法,但这样做会增加名字冲突的可能性,这样组件的唯一性就很难保证,所以不能采用这种方法。而如果按照TCP/IP网络协议标识计算机采用的IP地址标识方法,那么每个组件对象都应该分配一个整数,该整数唯一标识了组件对象。问题在于,为了保证唯一性,必须有一个专门的权威机构为COM组件分配整数标识符,对于COM组件的开发和使用,显然不能满足实际需要。

使用定长位数的整数来标识组件对象是合理的,为了在没有中心机构管理的情况下保证唯一性,COM规范采用了128位全局唯一标识符GUID,这是一个随机数,并不需要专门机构进行分配和管理。因为GUID是随机数,所以并不绝对保证唯一性,但发生标识符相重的可能性非常小。从理论上讲,如果一台机器每秒产生10000 000个GUID,则可以保证(概率意义上),3240年不重复。

下面是一个GUID的例子:

{54BF6567--1007--11D1--B0AA--444553540000}。
在C/C++语言中可以用这样的结构来描述:
 typedefstruct_GUID
 {
   DWORDData1;
   WORD  Data2;
   WORD  Data3;
   Byte  Data4[8];
 }GUID;

于是前面的GUID例子可以定义为
extern "c" const GUID CLSID_MYCLASID =
{0x54bf6567,0x1007,0x11d1,{0xb0,0xaa,0x44,0x45,0x53,0x54,0x00,0x00}};

CLSID是用来标识COM对象的GUID,因此,CLSID在结构定义上GUID一致。GUID并不是专门用来定义COM对象标识符的,它也用于定义其它实体的标识符,比如接口标识符。

2.3 COM接口

因为COM对象的客户与对象的服务之间通过接口进行交互,所以组件之间接口的定义至关重要。COM规范的核心内容是关于接口的定义。

2.3.1 接口的定义和标识

从技术上讲,接口是包含了一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的所有信息,客户程序利用这些函数获得组件对象的服务。

客户程序用一个指向接口数据结构的指针来调用接口成员函数。如图所示,接口指针实际上又指向另一个指针,这第二个指针指向一组函数,称为接口函数表,接口函数表中每一项为4个字节长的函数指针,每个函数指针与对象的具体实现连接起来。通过这种方式,客户只要获得了接口指针,就可以调用到对象的实际功能。

通常,接口函数表被称为虚函数表(virtual functiontable,简称vtable),在OSKIT中称为函数动态派遣表。对于一个接口来说,它的虚函数表是确定的,因此接口的成员函数个数是不变的,而且成员函数的先后顺序也是不变的;对于每个成员函数来说,其参数和返回值也是确定的。在一个接口的定义中,所有这些信息都必须在二进制一级确定,不管什么语言,只要能支持这样的内存结构描述,就可以定义接口。此外,成员函数除了参数类型是确定的,还要使用同样的调用习惯。客户程序在调用成员函数之前,必须先把参数压入栈中,然后再进入成员函数中,成员函数依次把参数从栈中取出来,在函数返回之前或返回后,必须恢复栈的当前位置,才能保证函数正常运行。由于OSKIT是用C语言来实现的,因此客户程序只需要包含接口声明的头文件,就可以调用COM对象的接口,而组件程序必须提供具体的实现过程,也就是说,如果一个COM对象实现了这个接口,则它提供的接口指针所指向的结构中,每个成员必须是有效的函数指针。

因为接口被用于组件程序和客户程序的通信桥梁,所以接口应该具有不变性,一个COM对象可以支持多个接口。为了让客户程序标识每个接口,类似于COM对象的标识方法,COM接口也使用全局唯一标识符,它被称为接口标识符(IID,interfaceidentifier)。

例如:
extern "c" const IID IID_Iunknown =
{ 0x00000000,0x0000,0x0000, {0xc0,0x00,0x00,0x00, 0x00, 0x00, 0x00,0x46} };

如果客户程序要使用一个COM对象的某个接口,则它必须知道该接口的IID和接口所能提供的方法(即接口成员函数)。

2.3.2 接口的一些特点

*二进制特性
接口规范并不建立在任何编程语言的基础上,而是规定了二进制一级的标准。任何语言只要有足够的数据表达能力,它就可以对接口进行描述,从而可以用于与组件程序有关的开发。

*接口不变性
接口是组件客户程序和组件对象之间的桥梁,接口如果经常发生变化,则客户程序和组件程序也要跟着变化,这对于系统的开发非常不利,也不符合组件化程序设计的思想。因此,接口应该保持不变,只要客户程序和组件程序都按照既定的接口设计进行开发,则可以保证在两者独立开发结束后,它们的协作运行能力能达到预期的效果。

*继承性
COM接口具有不变性,但接口也需要发展。类似与C++中的继承性,接口也可以继承发展。与C++中的继承不同,接口继承只是说明继承,即派生的接口只继承了基接口的成员函数说明,并没有继承基接口的实现,因为接口定义不包括函数实现部分,而且接口继承只允许单继承,不允许多重继承。根据COM规范,所有的接口都必须从Iunknown接口派生,而且一般的接口都是直接派生于Iunknown接口。

*多态性-运行过程中的多态性
多态性是面向对象系统的重要特征,COM对象也具有多态性,其多态性通过COM接口体现。多态性时客户程序可以用统一的方法处理不同的对象,甚至是不同类型的对象,只要它们实现了同样的接口。如果几个不同的COM对象实现了同一个接口,则客户程序可以用同样的代码调用这些COM对象。因为COM规范允许一个对象实现多个接口,因此,COM对象的多态性可以在每个接口上得到体现。

2.4 Iunknown接口

COM规范说明,COM定义的每一个接口都必须从Iunknown继承过来,其原因在于Iunknown接口提供了两个非常重要的特征:生存期控制和接口查询。客户程序只能通过接口与COM对象进行通信,需要控制对象的存在与否。如果客户还要继续对对象进行操作,则它必须保证对象能一直存在于内存中;如果客户对对象的操作已经完成,而且不再需要该对象,则它必须及时地把对象释放掉,以提高资源的利用率。Iunknown引入了"引用计数"(referencecounting)方法,可以有效地控制对象的生存周期。

另一方面,如果一个COM对象实现了多个COM接口,在初始时刻,客户程序不大可能得到该对象所有的接口指针,它只会拥有一个接口指针。Iunknown使用了"接口查询"(QueryInterface)的方法来完成接口之间的跳转。

Iunknown包含了三个成员函数:QueryInterface、Addref和Release。函数QueryInterface用于查询COM对象的其它接口指针,函数Addref和Release用于对引用计数进行操作。

2.4.1 引用计数

COM采用了"引用计数"技术来解决内存管理的问题,COM对象通过引用计数来决定是否继续生存下去,对于一个COM对象来说,只要有任一个逻辑模块还需要使用它,那么它就必须驻留在内存中,不能释放自己。因此,每一个COM对象都记录了一个称为"引用计数"的数值,该数值的含义是有多少个有效指针在引用该COM对象。当客户得到了一个指向该对象的接口指针时,引用计数增1;当客户用完了该接口指针后,引用计数减1。当引用计数减到0时,COM对象就应该把它自己从内存中清除掉。当客户程序对一个接口指针进行了复制,则引用计数也应该相应增加。Iunknown的接口成员函数Addref和Release分别完成引用计数的加1和减1操作。通过引用计数,COM对象的客户程序可以通过接口指针很好地控制对象的生存期。

2.4.2 实现引用计数

按照COM规范,一个COM组件可以实现多个COM对象,而且每个COM对象又可以支持多个COM接口。因此可以实现在COM组件一级,COM对象一级,甚至于对象的每个接口一级设置引用计数。

如果在组件一级设置引用计数,那么可以控制组件模块的生存周期,但不能控制COM对象的生存周期。如果一个组件有两个COM对象,则必须等到所有的COM对象都使用完以后,所有的COM对象才可以一起被释放。这样做降低了系统资源的利用率。

如果在接口一级设置引用计数,可以跟踪客户对象COM对象的使用情况。对于实现多个接口的对象,很有可能某些接口没有被客户使用,那么这些接口相关的资源可以不被占用。但每当一个接口的引用计数减到0时,它必须给对象发出通知,对象在接到通知后,需要判断是否所有的接口引用计数为0,若是,就把自己释放,然后再进一步通知组件程序,组件程序接到通知后判断是否所有的对象都已被清除,若是,则它可以被卸出内存。这个过程比较繁琐,而且也需要占用一部分的时间。

从折中的角度出发,比较合理的方案是采用对象一级的引用计数以便控制对象和组件的生存周期。这样使多个对象的组件程序可以有效地提高系统资源利用率。当一个对象被释放掉以后,它必须通知组件程序,如果组件程序发现已经没有对象存在了,则组件模块应该可以从内存中卸出。因此,组件程序应该保持一份有效对象的记录,可以用一个全局的对象计数值来控制组件的生存周期。

2.4.3 接口查询

按照COM规范,一个COM对象可以实现多个接口,客户程序可以在运行时刻对COM对象的接口进行询问,只有对象实现了该接口,对象才能提供这样的接口的服务。要实现接口查询,就要使用Iunknown的成员函数QueryInterface。

QueryInterface函数的IDL(interface description language,接口描述语言)语言说明:

HRESULT QueryInterface([in] REFIID iid,[out] void **ppv);

函数的输入参数iid为接口标识符IID,它是与GUID一样的128位整数,用来标识一个COM对象所支持的接口。输出参数ptv为查询得到的结果接口指针,如果对象没有实现iid所标识的接口,则输出参数ptv指向空(NULL)。当客户创建了COM对象后,创建函数总会返回一个接口指针,因为所有的接口都继承于Iunknown,所以,所有的接口都有QueryInterface成员函数,于是,在得到了初始的接口指针之后,可以通过它的QueryInterface函数获得该对象支持的任何一个接口指针。

2.4.4 COM对象的接口原则

COM规范对接口的查询给出了以下一些规则:

*对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同。也就是说,每个对象的Iunknown接口指针是唯一的,因此,对两个接口指针,可以通过判断其查询到的Iunknown接口是否相等来判断它们是否指向同一个对象。
*接口对称性。对一个接口查询其自身总应该成功。
*接口自反性。如果从一个接口指针查询到另一个接口指针,则从第二个接口指针再查询第一个接口指针必定成功。
*接口传递性。如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,则从第三个接口指针可以查询到第一个接口指针。
*时间无关性。如果一个接口在某一个时刻可以查询到另一个接口指针,则以后任何时候再查询相同的接口指针,一定可以查询成功。

2.5 COM特性

COM规范所定义的组件模型,除了前面提到的面向对象的特性和客户/服务器特性这两个基本特性外,值得重点说明的就是COM规范的语言无关性,对进程的透明性和它的可重用机制。

2.5.1语言无关性

COM对象的定义不依赖于特定的语言,因此,编写组件对象所使用的语言与编写客户程序的语言可以有所不同,只要它们都能生成符合COM规范的可执行代码即可。COM标准与面相对象的编程语言不同,它所采用的是一种二进制代码级的标准,而不是源代码级的标准。因此,COM的语言无关性实际上为跨语言合作开发提供了统一标准。在OSKIT中,所有的COM对象都是用C语言写成的,但只要依据COM规范,完全可以用C++改写其中的某些模块,然后单独编译该模块,而可以继续使用OSKIT中其它的原有模块。

2.5.2 进程透明特性

COM所提供的服务组件对象在实现时有两种进程模型,进程内对象和进程外对象。如果是进程内对象,则它在客户进程空间运行;如果是进程外对象,则它运行在同一机器上的另一个进程空间,或者在远程机器的进程空间中。虽然COM对象有不同的进程模型,但这种区别对于客户机来说是透明的,因此客户程序在使用组件对象时可以不管这种区别的存在,只要遵循COM规范即可。然而,在实现COM对象时,还是应该慎重选择进程模型。进程内模型的优点是效率高,但组件不稳定会引起客户进程崩溃,因此组件进程可能会危及客户;进程外模型的优点是稳定性好,组件进程不会危及客户程序,一个组件进程可以为多个客户程序提供服务,但进程外组件开销大,而且调用效率相对低一些。

实现这种进程透明性的关键在于COM库,COM库负责组件程序的定位,管理组件对象的创建和对象与客户之间的通信。当客户创建组件对象时,COM库负责装入组件模块或者启动组件进程。因此,客户程序可以不管组件对象的进程模型,即使组件的进程模型发生了变化,客户程序也不需要重新编译。

2.5.3 可重用性

可重用性是任何对象模型的实现目标,尤其是大型系统,可重用性非常重要。而且,由于COM标准是建立在二进制代码级的,因此COM对象的可重用性与一般的面向对象语言有所不同。

对于COM对象的客户程序来说,它只是通过接口使用对象提供的服务,它并不知道对象内部的实现过程,因此,组件对象的重用性建立在组件对象的行为方式上,而不是具体的实现上,这是重用的关键。

COM用两种方式实现对象的重用。假定有两个COM对象,对象1希望能重用对象2的功能,对象1称为外部对象,对象2称为内部对象。

包容方式。对象1包含了对象2,当对象1需要用到对象2的功能时,它可以简单地把实现交给对象2来完成,虽然对象1和对象2实现同样的接口,但对象1在实现接口时实际上调用了对象2的实现。

聚合方式。对象1只需简单地把对象2的接口递交给客户即可,对象1并没有实现对象2的接口,但它把对象2的接口也暴露给客户程序,而客户程序并不知道内部对象2的存在。

对象重用是COM规范很重要的一个方面,它保证COM可用于构造大型的软件系统,而且,它使复杂系统简化为一些简单的对象模块,体现了面向对象的思想。

[目录]


第三章 COM机制的实现

《OSKit的部件对象模型 第三章》


OSKit的部件对象模型 第三章2000年12月3日22:07


作 者: 洪宁


导师:陈朔鹰


第三章 OSKIT中COM机制的实现

3.1 OSKIT中COM的基本定义

在OSKIT中有关COM的基本定义在com.h中,在该文件中定义了OSKIT中的iid,COM的调用规范,以及OSKIT中的Iunknown接口。

OSKIT中GUID的定义如下:
struct oskit_guid{
   oskit_u32_t  data1;
   oskit_u16_t  data2;
   oskit_u16_t  data3;
   oskit_u8_tdata4[8];
};
其中oskit_u32_t、oskit_u16_t、oskit_u8_t分别是32位、16位、8位无符号整形。OSKIT使用这样的定义而不使用例如win32中的DWORD有两方面的原因:首先,这样定义使OSKIT保持一致的风格,向开发人员呈现出一致的命名规则。其次,如果oskit在win32或相似的环境中使用,能避免与实际的WIN32头文件冲突。

OSKIT中的调用规范如下:
#define OSKIT_COMCALL   OSKIT_STDCALL
#defineOSKIT_COMDECL   oskit_error_t  OSKIT_COMCALL
#defineOSKIT_COMDECL_U  oskit_u32_t   OSKIT_COMCALL
#defineOSKIT_COMDECL_V  voidOSKIT_COMCALL
其中,OSKIT_STDCALL是在compile.h中定义的有关编译器设置的宏,这个宏在OSKIT安装前的configure步骤中被赋值(configure步骤请见第一章)。OSKIT这样定义调用规范,是为了用不同的编译器都可以编译OSKIT,而不必在编写程序时考虑这些调用的细节。而OSKIT_COMDECL就是带错误返回码的调用,OSKIT_COMDECL_U是返回值为32位无符号整型的调用,OSKIT_COMDECL_V是不带返回值的调用。

OSKIT中Iunknown接口的定义如下:
struct oskit_iunknown
{
   structoskit_iunknown_ops *ops;
};
typedef struct oskit_iunknownoskit_iunknown_t;
struct oskit_iunknown_ops
{ / *查询对象是否支持某个特定的接口,若支持则返回该接口的指针*/
   OSKIT_COMDECL (*query)(oskit_iunknown_t*obj,
   const oskit_iid_t *iid, void **out_ihandle);
   /*增加某个接口的引用计数*/
   OSKIT_COMDECL_U (*addref)(oskit_iunknown_t *obj);
   /*将对某个接口或对象的引用计数减1,若引用计数为0,则释放该接口或对象 */
   OSKIT_COMDECL_U(*release)(oskit_iunknown_t*obj);
};
其中,query的参数中obj是要查询的COM对象,iid是要查询的接口的全局标识符,若查询成功,out_ihandle返回指向被查询接口的指针,若查询失败,则返回空指针。

上面可以看到,oskit_iunknown结构中存放着指向函数表格的指针,称为ops,而函数表格是通过一个称为oskit_iunknown_ops的结构实现的,这个结构中定义了指向该接口所实现的所有成员函数的指针。只要对该结构进行赋值,就可以通过这个结构调用该接口的所有成员函数。

由于Iunknown接口是由各个COM对象自己实现的,因此在COM.H中没有对该结构进行赋值,后面章节中将会详细剖析OSKIT中一个COM对象的实例,那时再对该结构的赋值进行讨论。

COM.H还通过宏定义对Iunknown接口的成员函数进行了包装,如下所示:

#define oskit_iunknown_query(obj, iid, out_ihandle)\
 ((obj)->ops->query((oskit_iunknown_t *)(obj),(iid),
 (out_ihandle)))
#define oskit_iunknown_addref(obj)\
 ((obj)->ops->addref((oskit_iunknown_t *)(obj)))
#defineoskit_iunknown_release(obj) \
 ((obj)->ops->release((oskit_iunknown_t*)(obj)))

这使开发人员可以对它们的调用更加方便。此外,顺便提一句,OSKIT的一大特色就是对宏定义的灵活使用。在这方面值得大家借鉴。

3.2 实例分析Iunknown接口的实现

OSKIT中并不是每个接口都允许被引用多次,比如内存接口,当系统初始化时便对定义好的静态内存结构进行赋值,然后所有对内存的操作都依赖于该内存接口,也就是说,在该内存接口中不存在实现Iunknown接口的问题。但也有可以存在可以引用多次的接口,如流接口(stream),C环境接口(OSKIT为核心提供了一个最小的C库,开发这也可以使用自己定义的C库加以替换)。本节就以C环境接口为例分析OSKIT中Iunknown接口的实现。

3.2.1 C环境对象的结构

OSKIT中C环境对象的结构如下:
struct genv {
   oskit_libcenv_t   libcenvi; /* C环境接口*/
   int         count; /* 引用计数*/
   oskit_fsnamespace_t *fsn;
   charhostname[256];
   oskit_ttystream_t  *console;
   void(*exit)(int);
   void         (*siginit)(int (*func)(int, int, void*));
   #ifndefPTHREADS
   oskit_timer_t    *sleep_timer;
   oskit_osenv_sleep_t *sleep_iface;
   osenv_sleeprec_t*sleeprec;
   #endif
};

3.2.2 C环境接口及其对Iunknown接口的实现

C环境接口是在libcenv.h中定义的,为了让大家对OSKIT中COM接口有一个完整的认识,现将整个接口的说明列出:

struct oskit_libcenv {
   struct oskit_libcenv_ops *ops; /* 指向函数表格的指针*/
};
typedef struct oskit_libcenv oskit_libcenv_t;

下面是函数表格:
struct oskit_libcenv_ops {
/* 对Iunknown接口的继承*/
OSKIT_COMDECL_IUNKNOWN(oskit_libcenv_t)
/* 获得及设置根文件系统*/
OSKIT_COMDECL (*getfsnamespace)(oskit_libcenv_t*s,
    oskit_fsnamespace_t **out_dir);
OSKIT_COMDECL(*setfsnamespace)(oskit_libcenv_t *s,
    oskit_fsnamespace_t *dir);
/*设置及获得主机名 */
OSKIT_COMDECL (*gethostname)(oskit_libcenv_t *s,
    char*hostname, int len);
OSKIT_COMDECL (*sethostname)(oskit_libcenv_t*s,
    const char *hostname, int len);
/* 调用及设置退出函数 */
OSKIT_COMDECL_V(*exit)(oskit_libcenv_t *s,
oskit_u32_t exitval);
OSKIT_COMDECL(*setexit)(oskit_libcenv_t *s,
void (*exitfunc)(int));
/* 设置及获得终端对象*/
OSKIT_COMDECL (*getconsole)(oskit_libcenv_t *s,
    oskit_ttystream_t**out_ttystream);
OSKIT_COMDECL (*setconsole)(oskit_libcenv_t*s,
    oskit_ttystream_t *ttystream);
/* 初始化信号库 */
OSKIT_COMDECL(*signals_init)(oskit_libcenv_t *s,
    int (*func)(int, int, void*));
OSKIT_COMDECL (*setsiginit)(oskit_libcenv_t *s,
    void(*sigfunc)(int (*func)(int,int,void *)));
/* 睡眠/唤醒接口 */
OSKIT_COMDECL_V(*sleep_init)(oskit_libcenv_t *s,
    osenv_sleeprec_t*sleeprec);
OSKIT_COMDECL_U (*sleep)(oskit_libcenv_t *s,
osenv_sleeprec_t*sleeprec,
struct oskit_timespec *timeout);
OSKIT_COMDECL_V(*wakeup)(oskit_libcenv_t *s,
    osenv_sleeprec_t *sleeprec);
/* 克隆整个对象*/
OSKIT_COMDECL (*clone)(oskit_libcenv_t *s,
oskit_libcenv_t**intf);
};

下面是定义C环境接口的iid
extern const struct oskit_guidoskit_libcenv_iid;
#define OSKIT_LIBCENV_IID OSKIT_GUID(0x4aa7dfe9, 0x7c74,0x11cf, 0xb5, 0x00, 0x08, 0x00, 0x09, 0x53, 0xad, 0xc2)

下面是对C库接口成员函数的包装,因为长度缘故,只写出部分:
#define oskit_libcenv_query(s, iid,out_ihandle) \
((s)->ops->query((oskit_libcenv_t *)(s), (iid),(out_ihandle)))
#define oskit_libcenv_addref(s)\
((s)->ops->addref((oskit_libcenv_t *)(s)))
#defineoskit_libcenv_release(s) \
((s)->ops->release((oskit_libcenv_t *)(s)))

下面是C库接口的实现:
static struct genv default_libcenv;
/* 定义静态的C环境对象 */

在OSKIT中,经常在初始化时大量使用静态定义,因为这样定义出来的结构在编译时就分配好了空间,使系统在启动时就可以使用这些结构,而不需要等到内存对象初始化完之后再动态分配空间。

Query 成员函数的流程图见下一页:

下面是Query成员函数的实现:
staticOSKIT_COMDECL
libcenv_query(oskit_libcenv_t *s, const oskit_iid_t *iid,
void **out_ihandle)
{
struct genv *g = (struct genv *) s;
/*将指向C库环境接口的指针转化为指向C库环境对象的指 */
if (memcmp(iid, oskit_iunknown_iid,sizeof(*iid)) == 0 ||
    memcmp(iid, oskit_libcenv_iid, sizeof(*iid))== 0)
/* 通过比较要查询的iid和iunknown接口的iid以及c库环境接口的iid
相比较,如果相等,则表明该C库环境对象支持该接口*/
  { *out_ihandle = g->libcenvi;/*返回指向C环境接口的指针*/
    g->count++; /*将引用计数加1*/
    return 0;
  }
  *out_ihandle = 0;
  return OSKIT_E_NOINTERFACE; /* 如果找不到,则返回出错信息*/
};
/* 增加引用计数 */
static OSKIT_COMDECL_Ulibcenv_addref(oskit_libcenv_t *s)
{ /* 将C库环境的接口指针转化为C库环境对象指针 */
  structgenv *g = (struct genv *) s;
  assert(g->count);
  return++g->count; /*增加引用计数*/
}

Release 函数的流程见下一页:

/* 释放引用计数*/
libcenv_release(oskit_libcenv_t *s)
{ /* 将C库环境接口指针转化为C库环境对象指针*/
  struct genv g = (struct genv ) s;
  assert(g->count);
/*以下语句先将对象的引用计数减1,然后判断计数是否为0,若是则释
放对与其相关的接口的引用*/
  if (--g->count == 0){
    if (g->fsn) oskit_fsnamespace_release(g->fsn);
    if(g->console) oskit_ttystream_release(g->console);
  #ifndefPTHREADS
    if(g->sleep_timer)
      oskit_timer_release(g->sleep_timer);
    if(g->sleep_iface)
      oskit_osenv_sleep_release(g->sleep_iface);
  #endif
    sfree(g,sizeof(g));
  }
  return g->count; /*返回该对象的引用计数*/
}

现在对上面Iunknown接口的三个成员函数的实现思路作一定的解释。在OSKIT中,客户所能使用的只是对象提供给用户的接口指针,当用户在该接口中调用Query方法时,例如libcenv_query,并对该对象中的某个接口进行查询,这时,由于对象是否实现所查询接口是在对象结构中定义的,因此首先把接口指针进行强制类型转换,变为指向接口所属对象的指针,然后再将所要查询的接口iid与对象所实现的所有接口的iid逐一比较,如果与其中某一个接口的iid相等,则返回指向该接口的指针,如果无一相等,则返回错误信息。当用户使用addref方法时,同样也是先进行类型转换,然后将这个对象中的引用计数加1,若用户调用release方法,在进行类型转换后,将对象的引用计数减1,如果发现引用计数位0,则将释放整个对象的内存空间。

[目录]


第四章 接口注册机制

《OSKit的部件对象模型 第四章》


OSKit的部件对象模型 第四章2000年12月3日22:49


作 者: 洪宁


导师:陈朔鹰


第四章 OSKIT中的接口注册机制

4.1 概述

为了管理OSKIT中的接口,OSKIT系统引入了接口注册的机制。所谓接口注册,就是在系统中建立一个服务数据库,里面记载了所有在该数据库中注册的接口信息,包括对象名、接口的iid、对象的引用计数等。系统提供给用户一个指向服务接口的指针,通过该指针,用户可以实现创建自己的数据库,在某个数据库中注册接口,在数据库中注销先前注册的某个接口,获得用某个iid注册的第一个接口,获得用某个iid注册的所有接口。系统在初始化时自动创建了一个特殊的数据库,称为全局数据库,在该数据库中注册了许多对于系统来说十分关键的接口,例如C库环境接口,内存接口,并提供了一些其它的函数以方便对全局数据库的操作,如直接将接口注册入全局数据库,以及直接在全局数据库中进行接口查询等。通过这些函数,系统能快捷方便地管理全局数据库。

4.2 数据库对象

OSKIT中的数据库对象是在service.c中定有的,其定义如下:

struct db {
  oskit_services_t  servi; /* 服务接口 */
  int       count; /*引用计数 */
  struct iidnode  *iids; /* iid链指针 */
  oskit_mem_t   *memi; /*所要使用的内存对象指针*/
};

其中用到的iidnode结构如下:

struct iidnode {
  struct iidnode *next;/*下一个iid节点*/
  oskit_guid_t  iid; /*全局iid*/
  struct objnode *objs;/*对象节点指针*/
  int objconut /*对象引用计数*/
};

而objnode的定义如下:

struct objnode {
  struct objnode  *next;/*下一个objnode节点*/
  oskit_iunknown_t *intf; /*iunknown接口类型指针*/
};

由此,可以得出该数据库的结构图为:

4.3 服务接口的定义

服务接口定义如下:

struct oskit_services {
  struct oskit_services_ops *ops;/*指向函数表格的指针*/
};
typedef struct oskit_services oskit_services_t;
structoskit_services_ops {
/*接口查询*/
OSKIT_COMDECL (*query)(oskit_services_t*s,
    const struct oskit_guid *iid,void**out_ihandle);
/*增添引用计数*/
OSKIT_COMDECL_U (*addref)(oskit_services_t*s);
/*释放引用计数*/
OSKIT_COMDECL_U (*release)(oskit_services_t*s)
/*在数据库中注册接口*/
OSKIT_COMDECL (*addservice)(oskit_services_t*s,
    const struct oskit_guid *iid, void*intf);
/*在数据库中撤销先前注册接口*/
OSKIT_COMDECL (*remservice)(oskit_services_t*s,
    const struct oskit_guid *iid, void*intf);
/*在数据库中查询以某个iid注册的所有接口*/
OSKIT_COMDECL (*lookup)(oskit_services_t*s,
constoskit_guid_t *iid,void***out_interface_array);
/*在数据库中查询以某个iid注册的第一个接口*/
OSKIT_COMDECL(*lookup_first)(oskit_services_t *s,
    const oskit_guid_t *iid, void**out_intf);
/*克隆整个数据库*/
OSKIT_COMDECL(*clone)(oskit_services_t*s,
oskit_services_t **intf);
};

另外在OSKIT中,还为操作全局数据库提供了一些函数,它们的定义如下;

/*创建全局数据库*/
oskit_error_toskit_global_registry_create
    (structoskit_mem*memobject);
/*获得全局数据库*/
oskit_services_t*oskit_get_services(void);
/*在全局数据库中注册接口*/
oskit_error_toskit_register(const struct oskit_guid*iid,
    void*interface);
/*在全局数据库中撤销先前注册的接口*/
oskit_error_toskit_unregister(const struct oskit_guid
    *iid,void*interface);
/*查询用某个iid注册的所有接口*/
oskit_error_toskit_lookup(const oskit_guid_t *iid, void
    ***out_interface_array);
/*查询用某个iid注册的第一个接口*/
oskit_error_toskit_lookup_first(const oskit_guid_t *iid,
    void**out_interface);

4.4 数据库服务接口的实现

4.4.1 创建数据库

OSKIT中创建数据库的实现思路是:使用某个内存对象分配出数据库对象所需的存储空间,然后创建数据库,给数据库对象的每个成员赋值,最后返回数据库的服务接口指针。其流程图见下一页:

其实现的源代码如下:

oskit_error_t oskit_services_create
(oskit_mem_t *memi, oskit_services_t**out_intf)
{  struct db *s;/*定义数据库对象*/
  /*如果没有提供内存对象,则在全局数据库中查找*/
  if (!memi){
    /*在全局数据库中查找第一个内存对象*/
    oskit_lookup_first(oskit_mem_iid,(void*) memi);
    if (!memi)
      panic("oskit_services_create:Nullmemory object!");
    }
    /* 给数据库对象分配内存空间 */
    s =oskit_mem_alloc(memi, sizeof(*s), 0);
    if (s == NULL)
      returnOSKIT_E_OUTOFMEMORY;
    s->count = 1; /*给引用计数赋值*/
    s->memi =memi; /*给内存对象指针赋值*/
    /*给数据库对象中的服务接口指针赋值*/
    s->servi.ops =services_ops;
    s->iids =0;
    oskit_mem_addref(memi);
    /* 在全局数据库中增加该内存对象的引用计数*/
    *out_intf = s->servi; /*返回服务接口指针*/
    return 0;
}

4.4.2 在数据库中注册接口

OSKIT在数据库中注册接口的过程是这样的,首先将服务接口指针转换为数据库对象指针,然后根据要注册的接口的iid,在数据库中查找已经注册的iid,若该iid已注册,则在该iid下查找用这个iid注册的接口名,如果找到则返回,否则,生成一个新的obj节点,并将要注册的接口赋给obj节点中的接口指针。如果要注册的iid找不到,则生成一个新的iid节点,在该iid节点下新建一个obj节点,并对它们进行赋值。在程序执行过程中,如果在分配内存时发现内存不够,则返回一个内存不足错误,由调用程序来处理。下面是注册接口的程序流程:

下面是注册接口的源程序:

参数:si:数据库指针
   iid:需要进行注册的全局iid指针
   interface:需要进行注册的接口指针
OSKIT_COMDECLservices_addservice(oskit_services_t *si,
     const struct oskit_guid *iid,void *interface)
{  /*将服务接口指针转换为数据库指针*/
  struct db *s = (struct db *) si;
  /*将需要注册的接口指针转换为iunknown接口的指*/
  oskit_iunknown_t *iu =(oskit_iunknown_t*)interface;
  struct iidnode *in;/*定义iid节点指针*/
  struct objnode *on, **onp; /*定义obj节点指针*/
  /*查找或创建相应的iid节点 */
  for (in = s->iids; ; in = in->next) {
    if (in== NULL) {
      /*如果没有找到,则创建新的iid节点*/
      in =oskit_mem_alloc(s->memi, sizeof(*in), 0);
      if (in == NULL)/*分配内存失败*/
        return OSKIT_E_OUTOFMEMORY;/*返回内存不足错误*/
      in->iid = *iid;
      in->objs =NULL;
      in->objcount = 0;
      in->next =s->iids;/*在iid链中插入该iid节点*/
      s->iids =in;
      break;
    }
    if (memcmp(in->iid, iid,sizeof(*iid)) == 0)
    break; /*找到了该iid节点,退出循环*/
  }
  /*在该iid的obj链中查找要注册的接口*/
  for (onp = in->objs; *onp; onp =(*onp)->next) {
    if ((*onp)->intf == interface)
      return 0; /*找到后,直接返回*/
  }
  /* 为这个接口创建一个新的obj节点*/
  on =oskit_mem_alloc(s->memi, sizeof(*on), 0);
  if (on == NULL)
    returnOSKIT_E_OUTOFMEMORY; /*返回内存不足错误*/
  on->next =NULL;
  /*增添对该接口的引用计数*/
  on->intf = iu; oskit_iunknown_addref(iu);
  *onp = on;
  in->objcount++; /*增加该iid节点的接口计数*/
  return 0;
}

4.4.3 在数据库中注销接口

在数据库中注销接口所做的工作与注册接口相反:首先,在数据库中找到该iid节点,然后在该iid节点的接口链上查找所要注销的接口节点,如果找到,将其释放并对需要改动的数据库参数进行修改。在这过程中,如果发现iid节点或要释放的接口节点没找到则返回无效错误。

下面是在数据库中注销节点的源程序:

参数与在数据库中注册节点一致。

OSKIT_COMDECL services_remservice(oskit_services_t *si,
      const structoskit_guid *iid, void *interface)
{  /*将服务接口指针转换为数据库指针*/
  struct db *s =(struct db *) si;
  struct iidnode *in; /*定义iid节点指针*/
  struct objnode*on, **onp; /*定义接口节点指针*/
  /* 找到相应的iid节点*/
  for (in = s->iids; ; in =in->next) {
    if (in == NULL)
      returnOSKIT_E_INVALIDARG;/*找不到,返回无效错误*/
    if (memcmp(in->iid, iid,sizeof(*iid)) == 0)
      break;
  }
  /* 找到并删除需要注销的接口节点*/
  for(onp = in->objs; ; onp = on->next) {
    on = *onp;
    if(on == NULL)
      return OSKIT_E_INVALIDARG; /*没找到接口,返回无效错误*/
    if(on->intf == interface)
      break;
  }
  *onp = on->next; /*将该接口节点从接口节点链中删除 */
  oskit_iunknown_release(on->intf); /* 释放对该接口的引用*/
  /* 释放该接口节点占所占内存 */
  oskit_mem_free(s->memi, (void *) on,sizeof(*on), 0);
  in->objcount--; /* 将这个iid的接口引用计数减1 */
  return0;
}

4.4.4 查找用某iid注册的第一个接口

在某些时候,系统或用户需要得到用指定iid注册的接口,因此系统提供了lookupfirst和lookup函数。Lookupfirst是用来查找以指定iid注册的第一个接口,如果找到,则返回该接口的指针。其工作过程是:首先,将服务接口指针转换为数据库对象指针。然后,在数据库的iid链中查找相应的iid节点,如果找到,则返回该iid节点的接口链中第一个接口指针,否则就返回空指针。其程序流程见下一页:

下面是该函数的源程序:

参数: si:数据库指针
   iid:指定的iid
   out_intf:找到的接口指针,用于返回
OSKIT_COMDECLservices_lookup_first(oskit_services_t *si,
      const oskit_guid_t *iid,void **out_intf)
{
  /*将服务接口指针转换为数据库指针*/
  struct db *s = (struct db )si;
  struct iidnode *in; /*iid节点指针*/
  oskit_iunknown_t *intf;/*用于返回的接口指针*/
  /* 查找相应的iid节点*/
  for (in = s->iids; ; in =in->next) {
  if (in == NULL) {
  *out_intf = NULL;/*找不到,返回空指针*/
    return 0;
  }
  if (memcmp(in->iid, iid,sizeof(*iid)) == 0)
    break; /*找到该节点,退出循环*/
  }
  if (in->objcount== 0) {
    *out_intf = NULL; /*该iid节点下无注册接口,返回空指针*/
    return0;
  }
  *out_intf = intf = in->objs->intf;/*返回找到的接口*/
  oskit_iunknown_addref(intf); /*增添的对该接口的引用*/
  return 0;
}

4.4.5 查找用某iid注册的所有接口

系统还提供了函数,在数据库中查找用指定iid注册的所有接口,该函数将用指定的iid注册的所有接口放在一个数组中,并返回该数组的地址。其实现过程是:首先在数据库中找到指定的iid节点,然后根据该iid节点的接口计数给需要返回的接口数组分配内存空间,再将用该iid注册的所有接口指针拷贝到数组中,最后将数组地址赋给out_interface_array,并返回该iid下注册的接口个数。

以下是该函数的源程序:

参数:si:数据库指针
   iid:指定的iid
   out_interface_array:返回的接口数组指针
OSKIT_COMDECLservices_lookup(oskit_services_t *si,
      const oskit_guid_t *iid, void***out_interface_array)
{
  /*将服务接口的指针转换为数据库指针*/
  struct db *s =(struct db *) si;
  struct iidnode *in;
  struct objnode *on;
  void**arr; /*定义存放数组地址的变量*/
  int i;
  /* 找到相应的iid节点*/
  for (in =s->iids; ; in = in->next) {
    if (in == NULL){
      *out_interface_array = NULL; /*没找到,返回空指针*/
      return0;
    }
    if (memcmp(in->iid, iid, sizeof(*iid)) ==0)
      break;
  }
  if (in->objcount == 0){
    *out_interface_array = NULL; /*若接口数为0,返回空指针*/
    return0;
  }
  /* 给用于返回接口指针的数组分配内存空间,调用者负责释放该数组所占用
  的内存空间 , 按照接口数分配内存空间*/
  arr = malloc(sizeof(*arr)*in->objcount);
  if (arr ==NULL)
    return OSKIT_E_OUTOFMEMORY; /*内存不足,返回错误*/
  /*将接口填入数组中*/
  for (i = 0, on = in->objs; i < in->objcount; i++, on =on->next){
    assert(on != NULL);
    arr[i] = on->intf;/*给数组赋值*/
    oskit_iunknown_addref(on->intf);/*增添对该接口的引用*/
  }
  assert(on == NULL);
  *out_interface_array = arr;/*将数组地址传递给调用者*/
  return in->objcount; /*返回以该iid注册的接口数*/
}

4.4.6 克隆数据库

OSKIT还提供了整个数据库拷贝的功能,也就是所谓的克隆。该函数首先创建一个数据库,然后在新数据库中将在原数据库中注册的所有接口重新注册一遍,最后返回新数据库对象的服务接口指针,以下是这个函数的实现过程:

参数:si:源数据库对象服务接口指针
   out_intf:新数据库服务接口指针
OSKIT_COMDECLservices_clone(oskit_services_t *si,
      oskit_services_t**out_intf)
{
  struct db *s = (struct db *) si;/*获得源数据库对象指针*/
  struct db *ns; /*新数据库对象指针*/
  struct iidnode *in;/*定义iid节点指针*/
  struct objnode *on; /*定义接口节点指针*/
  oskit_error_trc;
  /*使用源数据库的存储对象给新数据库对象分配内存空间*/
  ns = oskit_mem_alloc(s->memi,sizeof(*ns), 0);
  if (ns == NULL)
    return OSKIT_E_OUTOFMEMORY;/*返回内存不足错误*/
  ns->count = 1;
  ns->memi = s->memi;
  /*将服务接口函数表地址赋给新数据库*/
  ns->servi.ops = services_ops;
  ns->iids = 0;
  oskit_mem_addref(ns->memi);/*增加对该内存对象的引用计数*/
  in = s->iids;
  while (in) {
    on =in->objs;
    while (on){
    /*对在源数据库中注册的接口在新数据库中重新注册*/
      if((rc =services_addservice(ns->servi,in->iid,
        on->intf))!=NULL) {
          panic("services_clone");
      }
    on =on->next;
    }
  in = in->next;
  }
  *out_intf =ns->servi; /*返回新数据库的服务接口指针*/
  return0;
}


[目录]


第五章 启动及系统初始化

《OSKit的部件对象模型 第五章》


OSKit的部件对象模型 第五章 2000年12月4日21:58


作 者: 洪宁


导师:陈朔鹰


第五章 OSKIT的启动及系统初始化

5.1 概述

OSKIT的初始化分为两个阶段,第一阶段是由称为Multiboot的装入程序先初始化CPU,检测系统内存等系统环境,将所得的结果存入一个结构中,并将该结构的地址传递给核心中的主函数,然后主函数根据这些参数完成系统的硬件初始化,构造出整个系统的运行环境,最后再初始化系统运行所需的软件数据结构,完成核心的初始化。

5.2 核心装入程序-Multiboot

5.2.1 Multiboot简介

Multiboot是为在x86PC上运行的32位操作系统所制定的装入规范。它所制定的原因是每个操作系统都需要做装入的工作,现在各个操作系统都要自己编写代码来完成这项工作。Multiboot实际上希望成为操作系统与装入程序的标准接口,那样,只要操作系统在编写时遵循这个规范,那么它就可以使用遵循Multiboot规范编写的出来的装入程序完成核心的装入工作,这样便可以节省大量的时间和人力,也避免了重复劳动。

5.2.2 Multiboot规范

Multiboot规范详细定义了操作系统/装入程序的接口中三个方面的内容,分别是?
?
* 装入程序所要装入的操作系统的核心映象格式
*操作系统启动时机器所处的状态
* 装入程序传递给操作系统信息的格式

核心映象格式:操作系统格式分为两种,一种是a.out,另一种是elf。首先讨论a.out格式。由于各种操作系统中a.out格式是有区别的,例如linux中的ZMAGIC格式与mach中的ZMAGIC格式很难区分。因此,Multiboot规范指定了一个称为Multiboot_header的结构,存放在可执行文件的开始处,来获得不同的映象格式信息。而且这个结构必须完全包含在可执行文件的开始8192个字节处,这使得Multiboot装入程序能够发现a.out格式文件的文本段,而不必事先知道a.out中的详细变量。Multiboot_header的结构如下:

 0    magic: 0x1BADB002   (必需)
 4    flags         (必需)
 8checksum        (必需)
 8    header_addr
 12   load_addr
 16   load_end_addr     (只有标志中的第16位被设置才有效)
 20   bss_end_addr
 24   entry_addr

Multiboot_header中的magic是用来标识该结构的,其值必须是十六进制的0x1BADB002。Flag标志是用来标识操作系统要求装入程序提供的系统信息。其中,0到15位用来标识装入程序必须提供的信息,如果由于某种原因,装入程序不能提供这些信息,装入程序将不能完成系统的装入工作。而第16位到31位是可选的信息,如果装入程序不能得到这些信息,它将忽略这些位,继续装入工作。下面对这个标志中的各位进行解释。

如果标志中第0位被设置,则表明所有随操作系统装入的模块必须在4k的范围内。如果位1被设置,则表明multiboot_info结构中的内存部分有效。如果位16被设置,则表明multiboot_header中第8到24偏移量中的数据是有效的。由于第16位被设置而有效的地址信息都是物理地址。

Header_addr:存储着multiboot_header结构开始地址。

Load_addr:存储着正文段的开始物理地址,header_addr-load_addr得出的偏移量表明装入程序应该由核心映象文件中的什么位置开始装载操作系统。

Load_end_addr:存储着数据段的结束物理地址,(Load_end_addr -Load_addr)表明需要装载多少数据,在a.out格式的核心映象中,正文段和数据段是相接的。

Bss_end_addr:存储着bss段的结束物理地址,初始化程序将这段区域置为0,并保留它所占用的内存空间,防止其中放置了启动模块以及其它与操作系统相关的数据。

Entry:存储着操作系统的开始地址,装入程序应该跳转到该地址开始操作系统的执行。

Checksum是一个32位的无符号值,该值与multiboot_header中的其它必需项(flag,magic)相加时,应该得到一个32位的无符号0。

当32位操作系统开始运行时,机器应该处于以下状态:

·CS必需是一个32位的可读可执行的代码段,其偏移量为0,而且其最大可达到0xffffffff;
·DS,ES,FS,GS,SS必需是32位的可读可写数据段,其偏移量为0,而且其最大可达到0xffffffff;
·20根地址线必须可用于形成pc中标准的32位存储器地址;
·CPU中的页功能必须被关闭;
·CPU中的可中断标志将被置为不可中断;
·EAX寄存器中必须存放着magic值:0x2BADB002,这个值表明操作系统是由一个与multiboot相兼容的装入程序来完成装入的;
·EBX寄存器中必须存放着multiboot_info结构的32位物理地址,这个结构是由装入程序提供给操作系统的;

CPU中所有其它的寄存器和标志位都没有定义,这之中尤其需要注意的是:

·ESP,当需要自己的堆栈时,32位的操作系统必须立即创建一个;
·GDTR,即使段寄存器如上面所说的那样设置,GDTR有可能是无效的,因此操作系统有可能无法读取任何段寄存器的值,直到操作系统设置好自己的GDTR;
·IDTR,操作系统必须禁止中断,直到它设置好自己的IDTR;

其它的机器状态应该处于"正常态",就像是由bios初始化后一样。即使装入程序在创建32位环境时改变了PIC的值,它必须使PIC的值还原为通常的BIOS/DOS值。

当操作系统开始运行时,EBX寄存器中存放着"multiboot_info"结构的物理地址,通过这个结构,装入程序向操作系统传递一些关键信息。操作系统可以使用或忽略该结构中的任何部分。Multiboot_info结构及其子结构可以由装入程序放置在内存中的任意位置(除了预留给核心和启动模块的内存空间)。操作系统负责不覆盖这个结构所占用的内存,直到它已经使用完该结构。

Multiboot_info结构的格式如下所示:

 0     flags        (必须)
 4     mem_lower      (标志位0设置时有效)
 8mem_upper      (标志位0设置时有效)
 12    boot_device     (标志位1设置时有效)
 16    cmdline       (标志位2设置时有效)
 20    mods_count(标志位3设置时有效)
 24 mods_addr      (标志位3设置时有效)
 28 - 40  syms(标志位4或5设置时有效)
 44    mmap_length     (标志位6设置时有效)
 48    mmap_addr      (标志位6设置时有效)

如上所示,flags标识结构中的各个部分是否有效。Flags中所有未定义的位由装入程序置为0,而操作系统不理解的位将被忽略。Flags同时也起到对multiboot进行版本控制的作用,为将来multiboot_info结构的扩充留下余地。

如果flags中的位0被设置,则mem_lower和mem_upper将有效。Mem_lower和mem_upper分别以kb表示系统中低端和高端的内存数量。低端内存由地址0开始,高端内存由地址1兆开始。低端内存的最大值为640kb,高端内存的值是高端第一个内存空洞的地址减去1兆。这里,multiboot规范并不保证高端内存的返回值。

如果flags中的位1被设置,则boot_device将有效。Boot_device表明装入程序是从哪个bios磁盘设备装载操作系统。如果操作系统不是由bios磁盘设备装入,则该位将被清0。操作系统可以根据boot_device的值来确定它的根设备,但这不是必须的。

Boot_device区由四个子区组成,每个子区1字节,如下所示:

Driver|part1|part2|part3

Driver部分表明能被bios的INT13中断接口所能获得的磁盘设备。例如,0x00代表第一个软盘,0x80代表第一快硬盘。剩下的3个字节标识启动分区。

如果flags中的位2被设置,命令行启动参数将有效,cmdline部分包含着传递给核心的命令行参数所存放的物理地址。命令行采用C风格的null终结字符串格式。

如果flags中的位3被设置,mods区将告诉核心那些启动模块和核心映象一起被装入,以及这些模块的位置。Mods_count部分表明模块的数量。Mods_addr表明第一个模块结构的物理地址,每一个模块结构的格式如下:

 0 mod_start 模块开始地址
 4 mod_end  模块结束地址
 8 string   模块标识字符串
 12reserve  保留

值得注意的是,位4和位5是互斥的。如果位4被设置,则multiboot_info中从第28字节处开始的4个部分有效。这4个部分为:
 28tabsize
 32 strsize
 36 addr
 40 reserve

这4部分表明a.out核心映象格式中标志表的存放位置。其中,addr表明存放a.out核心映象格式中nlist结构数组大小的物理地址,tabsize与a.out格式中symbol部分的size参数相等,strsize与a.out格式中string部分的size参数相等。值得注意的是,即使位4被设置,tabsize也可以是0,表明没有symbol

如果位5被设置,则multiboot_info中从第28字节处开始的4个部分有效。这4个部分为:
 28 num
 32size
 36 addr
 40 shndx

这些部分表明在elf格式的核心映象中,header表的位置以及表中各项的大小,表项的数量,以及作为索引的字符串表格。这部分的每一项都与elf格式程序的header部分中以shdr开始的部分相对应。

如果位6被设置,则multiboot_info中mmap_length,mmap_addr部分有效。这两部分标明了由bios所提供的机器内存分布图所存放的地址以及大小。内存分布图中存放着一个或多个size/struture对,size被用来跳到下一个size/struture。每一个size/struture对的结构如下:
 -4size
 1  BaseAddrLow
 4  BaseAddrHigh
 5  LengthLow
 6  LengthHigh
 16Type
 Size表明该结构的字节大小。
 BaseaddrLow表明低32位开始地址。
 BaseAddrHigh表明高32位开始地址,因此开始地址一共是64位。
 LengthLow表明内存大小的低32位。
 LengthHigh表明内存大小的高32位,因此内存大小的长度也是64位。
 Type是一个代表地址范围的变量,它的值为1表示可供使用的RAM。
 其它值现在还未定义。

5.2.3 Multiboot进行初始化的步骤

Multiboot的启动过程分为四个步骤:首先,执行汇编程序multiboot.S,初始化结构boot_info,并将该结构开始地址存入寄存器ebx,然后调用multiboot_main函数。

Multiboot_main函数根据boot_info结构初始化机器的硬件环境,例如完成对内存的管理等,接着调用main函数。

Main函数将核心映象由硬盘或软盘上装入内存的一个临时位置,稍后它会被复制到其最终位置。建立一个新的multiboot_info结构,退出时以新multiboot_info结构地址为参数调用boot_start函数。

Boot_start函数将核心映象复制到最终位置,然后开始操作系统核心的执行。

5.3 OSKIT中软件环境的初始化

OSKIT的硬件环境初始化是指系统装入程序完成系统的硬件初始化工作,例如给CPU中的寄存器赋初值,检测系统的内存大小并完成有关的设置工作,操作系统将获得一个32位的运行环境。软件初始化所做的工作就是在此之后初始化系统所必须的数据结构,为下面系统中各个模块的运转作好准备

5.3.1 软件环境初始化的步骤

OSKIT中的软件初始化分为两类:

* 一类是单线程系统初始化
* 另一类是多线程系统初始化

在这里只对单线程系统的初始化进行讨论。

单线程系统的初始化主要完成以下工作:

·获得内存接口指针
·创建全局数据库
·将内存接口在全局数据库中注册
·创建C库环境,获得C库接口指针
·将C库接口在全局数据库中注册
·在全局数据库中获得C库接口指针

5.3.2软件环境初始化的实现

OSKIT的整个软件初始化由函数oskit_clientos_init完成,现在就对这个函数及其子函数进行分析。下面是oskit_clientos_init的源程序:

oskit_error_toskit_clientos_init(void)
{
  oskit_libcenv_t  *libcenv;  /* 定义C库接口指针*/
  oskit_mem_t    *memi;    /* 定义存储接口指针 */

  /* 获得存储接口指针*/
  memi = oskit_mem_init();
  assert(memi);
  /* 使用上面获得的存储接口创建全局数据库*/
  if (oskit_global_registry_create(memi))
  panic("oskit_clientos_init:Problem creating global
    registry");
  /*将存储接口在全局数据库中注册*/
  oskit_register(oskit_mem_iid,(void *) memi);
  /*创建C库环境,并将C库接口在全局数据库中注册*/
  oskit_libcenv_create(libcenv);
  oskit_register(oskit_libcenv_iid,(void *) libcenv);
  /* 获得全局数据库中注册的C库接口 */
  oskit_load_libc(oskit_get_services());
  return 0;
}

现在对oskit_clientos_init实现的细节进行讨论:

·存储接口的初始化:mem_init,这个函数很简短如下所示

oskit_mem_t oskit_mem_init(void)
{ return oskit_mem;
}

调用这个函数的目的是为了得到存储接口,下面是OSKIT中的存储接口:

static oskit_mem_t oskit_mem = { mem_ops };
static structoskit_mem_ops mem_ops = {
  mem_query,     /* 查询存储接口 */
  mem_addref,/* 增加对存储接口的引用 */
  mem_release,    /* 释放对存储接口的引用 */
  mem_alloc,     /*分配内存空间 */
  mem_realloc,    /* 改变先前分配的空间的大小 */
  mem_alloc_aligned, /*分配页倍数大小的内存空间 */
  mem_free,      /* 释放内存空间 */
  mem_getsize,    /*返回某内存节点的大小 */
  mem_alloc_gen,   /*分配一段符合指定地址和尺寸限制的内存*/
  mem_avail,     /* 获得某内存对象中空闲空间的大小*/
  mem_dump,      /*返回某内存对象中的所有内存区域,供调试用*/
};
该接口中的每个成员都是一个存储管理函数的地址,分别实现注释所述的功能。mem_init函数被首先调用的原因是操作系统中几乎所有数据结构的建立都需要分配内存空间(除去在初始化前已经用静态声明了的结构),而要分配空间必须先得到内存对象的接口,因此这个函数被首先调用。

·创建全局数据库:oskit_global_registry_create,该函数如下所示:

oskit_error_t oskit_global_registry_create(
oskit_mem_t*memobject)
{
  oskit_error_t rc;
  assert(memobject);
  /*创建全局数据库,并获得该数据库的服务接口 */
  if ((rc = oskit_services_create(memobject,
    global_registry)))
  return rc;
  return0;
}
全局数据库是操作系统中为记录与系统直接相关的接口使用情况所建立的数据库,系统中使用的关键接口都需要在该数据库中注册。有关数据库以及接口注册的问题请参考第4章的有关内容,这里不再重复。

·初始化C库环境:oskit_libcenv_create,该函数如下
static struct genvdefault_libcenv;
oskit_error_t oskit_libcenv_create (
oskit_libcenv_t**out_iface )
{
  struct genv *g =default_libcenv;/*给genv类型指针g赋值*/
  if (g->count) {
    *out_iface= g->libcenvi;/*若该结构已经初始化则返回*/
    return 0;
  }
  g->libcenvi.ops = libcenv_ops;
  /* 给结构g中C库接口指针赋值*/
  g->count = 10000;
  /*给终端接口指针赋值*/
  g->console =(oskit_ttystream_t *)
    default_console_stream;
  g->exit =oskit_libc_exit; /*给退出函数指针赋值*/
  /*给信号初始化函数指针赋值*/
  g->siginit =oskit_sendsig_init;
  /*返回C库对象的接口指针
  initial_clientos_libcenv =*out_iface = g->libcenvi;
  return 0;
}

Genv结构的定义是:
struct genv {
  oskit_libcenv_t    libcenvi;    /* C库接口指针*/
  int          count;      /* 引用计数*/
  oskit_fsnamespace_t  *fsn;      /* 文件命名空间接口指针 */
  charhostname[256];  /* 主机名 */
  oskit_ttystream_t   *console;    /* 终端接口指针*/
  void          (*exit)(int);  /* 退出函数指针 */
  /* 信号初始化函数指针*/
  void  (*siginit) (int (*func)(int, int, void *));
  #ifndefPTHREADS
  /* 用于在单线程系统中*/
  oskit_timer_t     *sleep_timer;
  oskit_osenv_sleep_t  *sleep_iface;
  osenv_sleeprec_t*sleeprec;
  #endif
};

C库接口的结构如下:

static struct oskit_libcenv_ops libcenv_ops = {
  libcenv_query,       /*C库接口查询函数 */
  libcenv_addref,      /* C库接口增加引用函数*/
  libcenv_release,      /* C库接口释放函数 */
  libcenv_getfsnamespace,  /*获得文件命名空间接口 */
  libcenv_setfsnamespace,  /* 设置文件命名空间接口*/
  libcenv_gethostname,    /* 获得主机名 */
  libcenv_sethostname,    /*设置主机名 */
  libcenv_exit,       /* 应用程序退出函数 */
  libcenv_setexit,      /*设置应用程序退出函数 */
  libcenv_getconsole,    /* 获得终端流接口*/
  libcenv_setconsole,    /* 设置终端流接口 */
  libcenv_signals_init,   /*POSIX的信号初始化函数 */
  libcenv_setsiginit,    /* 设置POSIX的信号初始化函数*/
  libcenv_sleep_init,    /* 睡眠结构初始化函数 */
  libcenv_sleep,       /* 睡眠函数*/
  libcenv_wakeup,      /* 唤醒函数 */
  libcenv_clone,       /* C库对象克隆函数*/
};

·在全局数据库中获得C库接口,完成这项工作需要调用两个函数:

oskit_get_services,它返回全局数据库的地址

oskit_load_lib,调用了函数oskit_services_lookup_first,在全局数据库中查找以oskit_libcenv_iid注册的第一个接口,并返回该接口地址。

[目录]


第六章 应用实例

《OSKit的部件对象模型 第六章》


OSKit的部件对象模型 第六章 2000年12月4日22:02


作 者: 洪宁


导师:陈朔鹰


第六章OSKit的应用实例:一个简单系统的设计与实现

6.1设计目的

OSKit是一个用来帮助我们研发操作系统的工具,如果你想更好的掌握它,除了对其本身进行分析之外,还有一个更加实际的方法,那就是利用这个工具开发一个我们自己的操作系统。

我们开发这个系统的初衷主要是为了学习,因此并没有想把它设计成像Windows、UNIX那么复杂。我们的目标是很明确的,那就是在大致搞明白OSKit应当如何使用之后,做一次尝试,用较短的时间,设计出一个具有一些最基本功能的系统。这对于我们课题组的每名成员来说,都意味着挑战。

6.2系统的功能

这个系统支持些什么?这是在开始设计之前我们要首先确定的一个问题。下面列出了一些我们希望实现的功能。

通过Multiboot或者LILO进行系统引导:这个系统可以通过所有支持Multiboot的引导程序进行引导,或者通过引导Linux的LILO进行引导。引导方式可以根据用户的要求,选择是通过软盘还是硬盘。

多线程:用户可以通过我们提供的系统调用,创建或杀死自己的线程。由于并不支持文件系统,因此新的线程恐怕只能是一个函数了。从理论上讲,我们可以让这个系统支持很多线程,但因为这仅仅是一个简单的演示性系统,因此我们把线程数限制在32个,当然这个数字也可根据用户的需求任意更改。

进程间通信:这个系统可以支持一些简单的进程间通信机制,比如信号量、消息队列等等。

外设:在众多的外设之中,我们选择了应用范围比较广的串口。利用目前OSKit所提供的机制,支持更多的设备是不成问题的,但加在这个系统中意义并不十分的大。另外,需要强调的是,访问串口的方法,是由我们自己规定,不允许用户更改。

简单的内存管理:应用程序需要的内存可以通过malloc函数动态申请。

总体构想

在该操作系统中,我们规定用户只能创建用户级线程,所有对于核心的操作都将由系统来完成。线程采用RR调度算法,也就是时间片轮转法,并允许抢占,线程在该系统中有四种状态,它们是运行态、阻塞态、睡眠态和就绪态,用户可以设定和改变线程的优先级。在线程中用户可以动态申请内存,但是需要由用户来释放。线程间提供简单的通信机制,包括信号量和消息队列。

在设备方面,对于终端,本系统将提供简单的输入输出功能。而对于串口的功能,本系统可以完整的实现,也就是说,用户既可以从串口读也可以向串口写。

为了方便起见,我们为这个系统起一个很简单的名字,叫acos,这个名字并没有什么实际的意义,仅仅为了叫起来方便。

虽然OSKit已经为我们提供了很多设计操作系统所必须的功能,比如内存分配中的malloc函数,通过Multiboot引导等等。然而,仍有许多工作是要由我们自己完成。

6.3我们自己所完成的工作:

6.3.1系统的启动

这里我们所说的系统启动,并不是指通常人们所说的通过软盘或硬盘引导,而是在引导结束之后,操作系统的核心进程如何启动第一个用户进程。通常,在UNIX系统中,内核启动之后,内核程序会去执行文件系统上的init程序,这个程序是整个操作系统中的第一个用户进程,剩下的进程,都必须通过这个进程才能启动。在acos中,我们规定,用户必须要提供一个acos_init() 函数,系统在启动初始化工作全部完成之后,acos_init( )函数将被作为系统中的第一个进程启动。这之后的工作,就完全由用户来完成了。

当进程acos_init( ) 结束时,整个系统也将终止运行。

在acos_init( )启动之前,究竟还有哪些事情需要做呢?下面我们就列出要做的事情:

对整个系统进行初始化,注册设备,检测设备,初始化设备需要的数据结构,初始化线程库,创建线程需要的数据结构。

6.3.2线程管理

所谓操作系统的线程管理,一般包括了一下的三大部分:线程的创建,线程间通信和线程调度。

线程的创建包括了初始化线程和创建线程的属性。

线程间通信中,我们使用了信号量,条件变量。

在线程调度中,用户线程只能使用时间片轮转法作为调度算法,并且规定在调度过程中调度算法不允许更改。

6.3.3 外设(串口)

在UNIX系统中,所有的设备都可以通过文件系统进行访问,读、写设备就像读写文件一样。在我们设计的这个acos中,并没有对文件系统提供支持,也不提供像读写文件系统一样的方法来访问设备。但是,我们提供一套基本的函数,供用户线程对串口进行读写操作,这些函数专门用于处理串口。

一个设备在同一时刻只能被一个线程使用,因此,在使用设备以前要申请,只有申请成功的线程才能够使用设备。

6.3.4 应用程序示例

为了能让各位老师对我们的演示系统一目了然,我们特地选取了在操作系统中比较有代表性的4个例子供大家欣赏,它们是,哲学家算法,时钟演示,线程调度和中断响应。

所需的资源:演示前三个例子需要一台intel386或更新的计算机,一张1.44MB的软盘;演示第四个例子除了上述设备之外,还需要一根串口线,一台装有Windows9x的计算机作发送机,并在其上安装有能向串口发送信息的软件。

1.哲学家算法

哲学家算法也称哲学家进餐的同步问题,最早是在1965年由Dijkstra提出并解决的。下面我就简单描述一下这个经典IPC问题的大意:7个哲学家围坐在一张圆桌周围,每个哲学家面前有一个碟通心面,由于面条很滑,所以要用两把叉子才能夹住。相邻两个碟子之间有一把叉子。哲学家的生活包括三种状态:即吃饭,思考和挨饿,当一个哲学家觉得饿时,他试图分两次去取他左边和右边的叉子,每次拿一把,但不分次序。如果成功地获得了两把叉子,他就开始吃饭,吃完以后放下叉子继续思考。这里的问题是:为每一个哲学家写一段程序来描述起行为,要求不能死锁。

在这个例子中我们设定了7个哲学家,且不允许哲学家饿死。我们为每个哲学家创建一个线程,把叉子看作条件变量,让线程等待条件变量,如果满足条件,则线程执行(哲学家的吃饭状态),执行一定时间之后,释放条件变量,进入休眠状态(哲学家的思考状态),当休眠时间过去之后,线程被唤醒,重新等待条件变量(哲学家的挨饿状态)。

2.时钟演示

计算机是以严格精确的时间进行数值运算和数据处理的,最基本的时间单元是时钟周期,OSKit最小可以精确到纳秒一级,其时钟系统的特点是允许在系统时钟之上创建自己的时钟,用于控制周期性线程。

我们的演示系统的源代码看似比较简单,但实际上却是建立在一个庞大的时间系统之上的,这个时间系统是整个操作系统活动的动力。

我们的演示程序将演示控制计算机的时钟计时( 每400毫秒显示一次 ),计时10次后返回初始菜单。

3.线程调度

在此个演示程序中,一开始有两个线程,它们所采取的调度算法是时间片轮转法,与此同时还有一个优先级最高的线程在休眠,它每400毫秒睡醒一次,打断此时处于运行状态的线程,那个优先级最高的线程转执行态,等它执行完它的时间片之后,前两个线程继续执行。

4.中断响应

这个例子演示了操作系统对外设的支持,我们选择了串口。演示这个例子,需要两台计算机和一根串口线,其中一台计算机安装我们的演示系统,另一台需要有一个向串口发送信息的软件,此软件我们是用Delphi编写的,运行在Windows98系统下,两台计算机用我们自己制作的串口线相连。演示程序的一开始,有两个优先级相同的线程在时间片轮转。当我们从另一台装有Windows98的机器上给演示机发送信息时,原来处于运行状态的两个线程被中断,演示机会成功的收到发送机发送的信息,然后等待操作人员的处理,是否继续运行该例子或者退出。


[目录]


第七章 结束语

《OSKit的部件对象模型 第七章》


OSKit的部件对象模型 第七章 2000年12月4日22:07


作 者: 洪宁


导师:陈朔鹰


第七章 结束语

OSKit中所使用的COM机制是OSKit作为一个操作系统开发平台所具有的最突出特点,这使得操作系统的各部分高度模块化,有利于将操作系统各个模块单独改写和替换而不影响其它模块以及整个系统的结构。但是,这也要在操作系统中付出时间和空间的代价来实现COM机制。一方面,初始化时系统要对所使用的COM接口进行注册,使用接口前需要查询,因此操作系统运行的速度会受到影响;另一方面,系统需要额外的内存空间来管理系统中的COM接口,因此操作系统要开销额外的内存空间。总的来说,这样做是有利有弊,从OSKit作为操作系统的开发平台的角度来说,则是利大于弊。OSKit在操作系统模块化方面所做的探索和尝试,以及它所体现的思想,值得我们借鉴和研究。希望该论文对于操作系统开发者有所启迪和帮助。

后记

以上是我对我们课题组所做研究工作,以及我个人工作的介绍。通过几个月以来我对OSKit的分析和研究,加深了对操作系统原理的认识,并对其实现有了具体的认识,提高了我的分析能力,丰富了实践经验。

我衷心地感谢我的导师陈朔鹰老师给予我的悉心指导,张丽芬教授和赵小林老师也给予我们课题组以热心的帮助,同时,我也要感谢我的合作伙伴韩斌,李凌,汤海京对我的帮助。此外,还要感谢赵艺伟,马亮,杨震等同学在毕业设计期间对我生活和学习上的关心,他们的支持和鼓励使我顺利地完成了毕业设计。最后,我对901教研室的全体老师表示衷心的感谢。


[目录]


包装系统驱动程序

OSKit包装系统驱动程序 目录
2000年12月9日 21:09


作 者: 李凌


导师:陈朔鹰


前 言

第一章 OSKit概述
1. OSKit简介
2. OSKit构成
  2.1 结构概述
  2.2 接口
  2.3 函数库
  2.4 部件库
  2.5 OSKit的整体结构图
3. OSKit的运行环境

第二章 OS Environment概述
1. 简介和基本设计思路
  1.1 简介
  1.2 组织
2. 驱动-内核界面(device-kernel interface)
  2.1 驱动程序申请内存的管理
  2.2 DMA的管理
  2.3 I/O端口的管理
  2.4 硬件中断
  2.5 睡眠与唤醒
  2.6 定时器
  2.7 ISA设备的注册
  2.8 驱动程序的注册
3. 驱动程序界面(device driver interface)
  3.1 块设备驱动程序界面
  3.2 TTY设备驱动程序界面
  3.3 TTY流界面

第三章 OS Environment包装Linux驱动程序的方法分析
1. Linux内核为驱动程序提供的支持
  1.1 内存分配函数
  1.2 DMA
  1.3 I/O端口
  1.4 中断号的申请
  1.5 打印函数
2. OSKit替换Linux内核-驱动界面的方法
3. OSKit包装Linux驱动程序的实际例子
  3.1 DMA
  3.2 内存管理
4. 小结

第四章 OS Environment自行开发驱动程序的方法
1. 概述
2. 基本方法
3. 驱动程序必须实现的部分
  3.1 OS Environment中驱动程序的接口及数据结构
  3.2 OS Environment中驱动程序的注册方法
  3.3 OS Environment中驱动程序注册、自检的全过程

第五章 OSKit的应用实例--一个简单系统的设计与实现
1. 设计目的
2. 系统的功能
3. 总体构想
4. 我们自己所完成的工作:
  4.1 系统的启动
  4.2 线程管理
  4.3 外设(串口)
5. 使用方法
  5.1 安装
  5.2 使用
  5.3 用户编程接口
  5.4 应用程序示例

第六章 结束语

后 记

参考文献

[目录]


前 言

《OSKit包装系统驱动程序 前言》


OSKit包装系统驱动程序 前言2000年12月9日21:11


作 者: 李凌


导师:陈朔鹰

前 言

课题名称:基于面向对象的操作系统开发平台OSKit的分析与程序设计
题目来源:自拟题目

开发操作系统是一项非常复杂且工作量很大的工作,如果不利用任何地已经存在的源码,而从第一行代码写起的话,那么也许需要几年甚至更长的时间才能够开发出一个新的操作系统。

对于新的操作系统的开发人员来说,会有很多的时间并不是花在真正的操作系统的问题上,而是花在了写硬件初始化、系统启动等等问题上。通常这样的代码所做的工作是固定不变的,因此写这些代码是枯燥乏味的。同时,也许如果是在研究操作系统的理论,那么也就没有必要去实现整个的操作系统。然而,仅仅实现操作系统的一部分,整个系统又无法工作。这就给研究者和开发者都造成了很大的困难。

OSKit的出现改变了这一切,这主要是因为OSKit已经将操作系统的各个部分进行了模块化,利用OSKit开发新的系统时,可以仅仅开发希望开发或研究的部分,而其余部分,则使用OSKit提供的模块。它使得开发人员可以不必花半年或者更长的时间去为系统写启动代码,而是可以将主要的精力都集中在真正的操作系统的问题上。OSKit是由美国犹他大学计算机科学系的Flux研究小组开发的,它以GNUPublic License进行发布,因此任何人都可以使用它,并且对它进行完善。

在后面的几章里,我将向大家系统地介绍我对OSKit所进行的分析和研究的成果--OSKit包装其它操作系统的驱动程序的原理和方法。希望这些能够为操作系统的开发和研究人员更好的理解和使用OSKit有所帮助。更希望大家能本着"自由"与"开放"的精神加入到自由软件创作者的行列,为中国的自由软件及软件事业的发展作出自己的一份贡献。

由于本人能力有限,论文中的有些术语表达可能不妥,内容也可能不够准确,敬请各位老师和同学批评指正,本人不胜感激。


李凌
2000年6月20日

[目录]


第一章 OSKit概述

《OSKit包装系统驱动程序 第一章》


OSKit包装系统驱动程序 第一章2000年12月9日21:34


作 者: 李凌


导师:陈朔鹰

第一章 OSKit概述

1. OSKit简介

通常情况下,开发一个操作系统是一件非常复杂的事情,操作系统的开发人员不仅要开发那些他们感兴趣的部分,还要花费大量的时间去开发一些他们并不感兴趣而不得不去开发的部分,如装入程序、启动程序、以及一些底层硬件的驱动程序等等。OSKit的开发者们开发它的初衷也正是为了解决这个问题。

OSKit是由美国犹它大学Flux研究小组开发的一套设计操作系统的工具包。它之所以被称之为一个工具包而不是一个操作系统,是因为它最初的设计意图就是要为操作系统的开发者们提供一套可重用的模块,使操作系统的开发者们可以很快的把精力集中于实际的操作系统的问题,如作业控制、进程间通信、文件系统和安全机制等等。

对于很多种情况,OSKit本身就是一个操作系统,它提供了操作系统中所需要的几乎所有模块,那么作为一个操作系统的工具包,它和真正的操作系统的差别又在哪里呢?按照OSKit的开发人员的话说就是"在我们看来,'操作系统'和'操作系统工具包'之间的分界线是:哪些是操作系统的开发人员真正想写的,而哪些是他们不得不写而实际上并不想写的。"从这一点我们也可以看出,对于不同的操作系统开发人员来说,因为他们感兴趣的部分不同,他们所使用的部分自然也就不同。OSKit是一个模块化的工具包,使用它的时候并不一定要使用它的全部,而可以仅仅去使用其中的一部分。

在这里,有一点是需要说明一下的,通常情况下,操作系统的源码是不能依赖于某些函数库的,因为C语言库(如GNU CLibrary)实际上是要依靠操作系统提供的某些功能才能够正常工作的。OSKit与这些函数库之间的一点差别就是:OSKit本身在设计时就注意到了这个问题,其中的函数在运行时都没有对其运行环境作出任何的假设,因此,这些函数是可以在操作系统的代码中调用的。

OSKit是一个自由软件,也可以被称为"开放源代码"软件,OSKit的主要部分是在GNU PublicLicense(GPL)下发布的。但因为OSKit中使用了大量的其它操作系统(如Mach、FreeBSD、Linux等)的源代码,因此OSKit中这些部分则受到它们原有的版权协议的保护。


2. OSKit构成

2.1 结构概述

OSKit从整体上可以分为三个部分,第一部分是接口,第二部分是函数库,第三部分是部件库。但实际上,因为这三部分之间相辅相成的关系,在源代码中它们的划分并不是十分的清晰,在很多情况下,划分是非常随意的。不过,下面我们姑且仍然按照这样的划分来描述一下OSKit的整体结构。

2.2 接口

OSKit的接口是面向对象的,它采用了部件对象模型(COM)来定义。COM的基础是界面(Interfaces),所谓界面,就是一套方法,通过它们,无需知道对象的内部结构就可以访问直接访问对象。COM接口与部件及对象的实现无关,完全是独立的。用C语言是如何实现COM机制的呢?由于本文的重点不在这个方面,因此这里我只作一个简单的介绍。

OSKit中的COM接口是一个不透明的struct结构,它的实际大小和内容对于调用者来说都是未知的。它的第一个元素必须是一个指向一个函数表格的指针,这个函数表格中是该COM对象可以使用的方法。实际上,这个函数表格也是一个struct结构,结构中的每一个元素都是一个指向函数的指针。下面是一个很简单的例子,它定义了OSKit中块输入/输出对象(blkio)的界面。

/* 这是用户看到的COM接口 */
struct oskit_blkdev
{
  struct oskit_blkdev_ops*ops;
};
typedef struct oskit_blkdev oskit_blkdev_t;

/* 这是函数表,该COM接口所支持的方法 */
struct oskit_blkdev_ops
{
  /*COM-specified IUnknown interface operations */
  OSKIT_COMDEC*(*query)(oskit_blkdev_t *dev,
        const struct oskit_guid*iid,
        void **out_ihandle);
  OSKIT_COMDECL_U(*addref)(oskit_blkdev_t *dev);
  OSKIT_COMDECL_U (*release)(oskit_blkdev_t*dev);

  /* Base fdev device interface operations */
  OSKIT_COMDEC*(*getinfo)(oskit_blkdev_t *fdev,
        oskit_devinfo_t*out_info);
  OSKIT_COMDEC* (*getdriver)(oskit_blkdev_t*fdev,
        oskit_driver_t **out_driver);

  /* Block device interface operations */
  OSKIT_COMDEC·(*open)(oskit_blkdev_t *dev, unsigned mode,
        struct oskit_blkio**out_blkio);
};

/* OS Environment中块设备接口的GUID */
extern const struct oskit_guidoskit_blkdev_iid;
#define OSKIT_BLKDEV_IID OSKIT_GUID(0x4aa7df82,0x7c74,\
    0x11cf, 0xb5, 0x00, 0x08, 0x00, 0x09, 0x53, 0xad, 0xc2)

/* 以下定义的是较为友好的访问上述接口的方法 */
#define oskit_blkdev_query(dev, iid,out_ihandle) \
    ((dev)->ops->query((oskit_blkdev_t *)(dev),\
    (iid), (out_ihandle)))
#define oskit_blkdev_addref(dev)\
    ((dev)->ops->addref((oskit_blkdev_t *)(dev)))
#defineoskit_blkdev_release(dev) \
    ((dev)->ops->release((oskit_blkdev_t*)(dev)))
#define oskit_blkdev_getinfo(fdev, out_info)\
    ((fdev)->ops->getinfo((oskit_blkdev_t *)(fdev),\
    (out_info)))
#define oskit_blkdev_getdriver(fdev, out_driver)\
    ((fdev)->ops->getdriver((oskit_blkdev_t *)(fdev),\
    (out_driver)))
#define oskit_blkdev_open(dev, mode, out_blkio)\
    ((dev)->ops->open((oskit_blkdev_t *)(dev), (mode),\
    (out_blkio)))

OSKit中定义的接口非常多,本文将要详细描述的,也正是OSKit中的一套非常重要的接口OSEnvironment(osenv)。它实际上是系统内核与驱动程序之间的接口,通过OSEnvironment,系统内核为驱动程序的运行提供了必要的支持。同时,系统也通过驱动程序所提供的COM接口来访问设备。

2.3 函数库

OSKit的函数库为操作系统提供最基本的低级服务。在OSKit的函数库中,很少使用OSKit的COM接口,而是用普通的C语言的函数实现的。这样操作系统的设计者只要直接调用这些函数就可以了。OSKit的函数库在设计上尽量公开,可以让使用者很容易的看到这些函数的设计细节。

OSKit中的函数库主要有一下几个:

·liboskit_c:这是一个非常小的C语言库,它在受限制的OS环境中提供了一些通用的C函数,比如基本的字符串、内存和格式化的输入、输出工具等。如果需要更强大的C库,OSKit中还提供了FreeBSD的C库liboskit_freebsd_c。
·liboskit_kern:这个函数库中包括建立一个基本的操作系统内核运行环境,为陷入、中断等提供了缺省的处理程序。这个库包括了很多在写核心代码时非常有用的通用函数,如访问某个特殊的处理器寄存器的函数,建立并维护页表,在不同的处理器模式之间转换(如x86的实模式和保护模式)等。此外它还提供了便于的在开发时使用的源码级内核调试机制。
·liboskit_smp:更多的内 核支持代码,用于多处理器的系统环境。
·liboskit_com:处理COM 接口的工具函数和一套通用的包装部件。
·liboskit_dev:这个库提供了驱动程序和由其它操作系统引入的部件(如网络、文件系统)所需要的"gluecode"中的函数的缺省实现。在OSKit的文档中,把这一部分放在了函数中,但通过对OSKit的源代码的分析,这一部分实际上使用了大量的COM接口。这一部分中的函数的实现是为了使用liboskit_kern函数库的简单内核所设计的,当需要精心制作的内核时,目标系统必须要重写此函数库中的部分或全部代码。


2.4 部件库

OSKit的部件库提供了比较高层的功能,这些高层的功能以标准的、面向对象的"黑盒"方式设计。部件库通常只向使用者公开一些相关的公共调用接口。例如,在Linux和BSD驱动程序部件库中,每一个完整的驱动程序仅仅实现为一个单独的函数调用,这个函数调用用来初始化并注册驱动程序。目标系统将通过OSKit的面向对象的COM接口来与这些部件进行交互。

OSKit部件库的这种设计策略,可以有效的将大量已经存在的系统(如Linux和BSD)中的已经写好的代码合并进来,并隐藏原有环境的细节,使操作系统的设计者们方便的使用它们。

OSKit中的主要的部件库有:

·liboskit_posix:增加对一个POSIX构造的系统会实现的系统调用的支持。例如:open、read和write等。这些POSIX操作被映射到相应的OSKitCOM接口上。最小化的C库和FreeBSD C库都依赖POSIX库来提供所需要的系统级操作。
·liboskit_freebsd_c:起源于FreeBSD的完整的类POSIXC库,提供了单线程和多线程的配置。在需要支持那些调用了类POSIX的函数或需要线程安全时,这个库可以用来替换最小化的C库liboskit_c。FreeBSD的"系统调用"层由POSIX库liboskit_posix提供,而高层的代码则没有作改动。
·liboskit_freebsd_m:完整的标准数学库(从FreeBSD的libm中取得)。通常那些使用了浮点数的程序都需要这个库中的函数的支持。
·liboskit_fsnamespace:文件系统名字空间库,为应用程序提供了"namei"风格的转换,如高层的mount和unmount兼容。多种部件的绝对和相对路径名(有斜线的)被转换成为oskit_file和oskit_dir COM对象。
·liboskit_rtld:运行时链接、装入库,允许ELF格式的OSKit内核装入共享库(.so文件)。
·liboskit_lmm:一个灵活的存储管理库,它可以管理物理内存或虚拟内存。这个库支持很多OS级代码需要的功能,例如多种内存类型、分配优先权、和随意的放置已经分配的块的约束。
·liboskit_amm:地址映射管理库用于管理收集到的资源,通常收集的每一个元素都有一个名字(地址)和一些属性。有可能被地址映射管理的资源的例子有交换空间和进程虚拟地址空间。
·liboskit_svm:简单虚存库。使用AMM库定义了一个简单的用于单独地址空间的虚存接口,它提供内存保护和为块设备如一个硬盘分区进行分页。(不被支持的redzone.c为单线程的内核提供了一个栈redzone,不需要SVM。)
·liboskit_threads:这个库提供了多线程的内核支持,包括POSIX线程、同步、调度和栈保护。调度使用了标准的POSIXRound-Robin和FIFO。试验性地支持CPU遗传调度(CPU inheritancescheduling),这是一种用于随意的调度策略的分层架构,但它还不是很完整很强壮的。
·liboskit_memdebug:这个库提供了一个用于调试的malloc,它可以检测多种与内存分配有关的错误(如overruns、使用已经释放的内存块等)。
·liboskit_gprof:使OSKit的内核可以收集关于它自己的剖析数据并在运行结束时报告的的运行期支持代码。当编译内核时使用"-pg"选项就可以收集剖析数据。
·liboskit_diskpart:一个能够识别多种常见的磁盘分区方案并生成一个完整的映射的通用库。这个库提供了一个简单的方法让操作系统可以找到相关的或者是"感兴趣的"磁盘分区,就如提供简单的高级方法使用不同的命名策略去访问任何磁盘分区,BSD和Linux兼容的名字策略在缺省的情况下是被支持的。
·liboskit_fsread:一个简单只读文件系统解释库,它不同的常见的文件系统,包括BSD FFS、Linuxext2fs和MINIX的文件系统。这个库典型地被用于和分区库一起使用,为操作系统提供一种简单的方法从硬盘或软盘上读取程序和数据。此外,即使是在一些不需要它的操作系统里,这个功能经常在启动时需要。这部分代码在构造bootloader时也是很有用的。
·liboskit_exec:一个通用的可执行程序解释器和装入器,它支持流行的可执行程序格式,如a.out和ELF。(即使是通常不需要装入可执行程序的微内核系统,通常也必须有一种方法装入第一个用户态的程序,OSKit的小巧、简单的可执行程序解释器很适合于这个目的。)
·liboskit_linux_fs:包装了Linux 2.0.29的文件系统部分的代码。包括LinuxVFS层支持的ext2fs(一个Linux系统上主要使用的文件系统)和其它很多PC上的被Linux支持的文件系统。
·liboskit_netbsd_fs:包装了NetBSD 1.2的文件系统部分的代码,包括BSDVFS层支持的本地的FFS文件系统。
·liboskit_memfs:一个基于内存的文件系统。
·liboskit_freebsd_net:包装了FreeBSD 2.1.7.1的网络代码。包括套接字层和协议。
·liboskit_bootp:这个库提供了一个简单的接口执行BOOTP协议(RFC1048/1533),以客户的以太网卡的硬件地址为基础,从服务器取回一套规范的参数。
·liboskit_linux_dev:包装了Linux2.0.29的设备驱动程序代码。当前包括超过50种块设备(SCSI、IDE)和网络驱动,它被包装成使用OSKit的设备驱动架构。。
·liboskit_freebsd_dev:包装了FreeBSD2.1.7.1的设备驱动程序代码。当前包括8各TTY(虚拟控制台和串口线,包括鼠标)驱动。
·liboskit_wimpi:基于MGR的简单的分层窗口系统,只用简单的画图和窗口管理操作。
·liboskit_*video*:基本的视频支持,有两种实现:一种包装了全部SVGALIB 1.3.0,另一种基于XFree863.3.1,但只支持S3的驱动程序。


2.5 OSKit的整体结构图

上面我们介绍了OSKit的构成,那么OSKit的整体结构图是什么样的呢?这里我们给出一个OSKit的整体结构图,这个整体结构图并没有包括OSKit的全部部件,而只是其中一些比较主要的部分,但是从中我们仍然可以加深对于OSKit的了解。

本文要详细说明的就是上图中左下角用黑线框住的部分。


3. OSKit的运行环境

OSkit中的许多部件在核心和用户方式下都可以使用,这就需要对部件的执行环境作出定义,例如部件什么时候可以嵌套进入等。此外,OSKit使用了许多其它操作系统的代码,例如设备驱动程序和网络协议栈,都是原封不动的从原有的核心如BSD和Linux中借用来的,OSKit通过附加代码模拟原始执行环境使得这些执行模块比它们原始执行环境更简单,用户也不需要详细了解原执行环境的细节。下面对OSKIT的每种执行模块进行简单的介绍。

纯执行模块。这是OSKIT执行环境中最简单的模块。这些部件或函数中没有全局变量或静态变量,只使用和处理目标环境传递的数据。例如函数strlen,它只通过目标环境传递给它的字符串指针求出字符串的长度,并将其返回,它只接触参数数据域而不影响目标环境。当这些函数使用的数据集是分离的,它们可以安全地同时被调用,而不需要同步,反之则不行。例如对重叠的目标缓冲区并发使用memcpy调用是不安全的。

非纯执行模块。这些模块中使用了全局变量或有可能改变全局共享状态,例如liboskit_kern(核心支持库)中的许多函数建立和访问全局处理器寄存器和数据结构,因此它们是非纯执行模块。非纯执行模块有以下特点:


·非纯函数和部件可能依赖于全局状态,如全局变量、静态变量、处理器中特殊寄存器等。
·除非有明确的声明,非纯函数和部件是不可重入的,并且运用在多线程系统中也是不安全的。为了在一个多线程/多处理器环境中使用这些函数和部件,目标操作系统必须提供适当的同步代码。


阻塞模块。它扩展了非纯模块以支持非抢占的多线程,这些模块中有一类可重入的函数称为阻塞函数,在这模块中,除非明确声明为非阻塞函数,否则函数是阻塞的。为了在一个可抢占的、可中断的或者多处理器的环境中使用阻塞模块,必须在进入模块前加锁,在退出模块时将锁释放。

可中断阻塞模块。在它之中每个部件都是一个单线程的执行域,在一个给定的时刻,只有一个(虚拟的或者物理的)CPU可以执行部件中的代码。例如:在一个多处理器系统中,在进程级,任意时刻在一个部件集内只有一个CPU被允许执行;这能够通过在部件前后放置全局锁来实现。

此外,OSKit的执行环境还有以下特点:

在一段时间内,部件中可以存在多个活动进程,但在某时刻只有一个进程被执行。

目标操作系统给每个活动进程提供一个独立堆栈,这个堆栈在阻塞函数运行时被保留。只有在操作完成后,对部件的调用返回时才放弃该堆栈。

部件中的代码总是运行在两个级别中之一,进程级或中断级。有意思的是一些部件的函数和方法只能在进程级被调用,而另一些只能在中断级被调用,还有的能在任何级别被调用。调用的细节属于接口描述的一部分。

部件中无论进程级或中断级的操作都能被部件中的中断处理程序中断,除非代码调用osenv_intr_disable屏蔽了中断。

当部件在进程级运行时,OSKit假定中断开放,部件在处理过程中可能临时屏蔽掉中断,但必须在返回到目标操作系统前重新激活。同样,当部件在中断级运行时,OSKIT假定中断被屏蔽,但是部件可以在目标操作系统允许其它中断级别的活动中断该部件时重新激活中断。

当目标操作系统在一个部件内中断一个进程级的活动时,在继续这个活动前,操作系统必须执行完这个中断级别的活动。同理,若一个中断级的活动被中断,那么最近的中断级别的活动必须在继续前一个中断级别的活动之前完成。

部件中运行在中断级别的代码不能调用目标操作系统提供的阻塞回调函数;只有非阻塞的回调函数能够在中断级别被调用。


[目录]


第二章 OS Environment概述

《OSKit包装系统驱动程序 第二章》


OSKit包装系统驱动程序 第二章2000年12月9日21:56


作 者: 李凌


导师:陈朔鹰


第二章 OS Environment概述


1. 简介和基本设计思路

1.1 简介

在操作系统的开发过程中,为外围设备设计驱动程序是一个繁重的任务。随着Linux、BSD等开放源代码的操作系统的发展,这些系统已经支持越来越多的外围设备,那么是否有一种方法能够将这些系统中已经存在的驱动程序直接在新的操作系统中使用呢?OSEnvironment则提供了这样一种方法。

OS Environment在最初被称为"device driverframework"即驱动程序框架,它实际上是一个驱动程序的设计规范,通过这个规范,就可以将大量已经存在的被用于实际系统中的驱动程序,以源码的方式利用到新设计的操作系统,或其它需要驱动程序的应用中。

在驱动程序方面有一些标准如DDI/DKI和UDI等,这些接口和OSKit的OSEnvironment接口有什么差别呢?UDI等接口是为了那些从头写的驱动程序而设计的,它对于驱动程序有着特殊的要求,比如UDI要求所有驱动程序在非阻塞的中断模式下工作,从理论上讲,通过这个限制UDI驱动程序就可以在进程模型或者中断模型中运行,但这同时也使得很多已经存在的驱动程序无法在UDI接口中工作。OSEnvironment的设计目的就是为了要使用已经存在的驱动程序,因此OS Environment使用了折中的方案。

由于已经存在的驱动程序非常的多,很多驱动程序对其运行环境作出了假设,比如假定所有实存是直接映射到系统核心的虚存空间上的。然而新的系统并不一定就提供了这样的环境,因此,在OSEnvironment中有两种驱动,一种是完全兼容的,还有一种是部分兼容的。

1.2 组织

下面我们给出OSKit中OS Environment中驱动程序部分的结构图,从这个结构图中,我们可以更清楚的看出OSKit的这一部分是如何组织的。

在上图中的两条比较粗的线,就是OSEnvironment中的两类接口,上面的是驱动程序界面,下面的是驱动-内核界面或简称内核界面。驱动程序界面为操作系统提供使用驱动程序的方法,而内核界面是操作系统为驱动程序提供一些支持,使得驱动程序可以合法的使用硬件资源。也可以说内核使用驱动程序界面而提供内核界面,驱动程序使用内核界面提供驱动程序界面。

通常,由操作系统内核提供,由驱动程序使用的代码对于不同的目标系统来说是要重新写的,至少目标系统要提供一些包装,以使得驱动程序可以正常的工作。

Linux驱动程序和FreeBSD驱动程序是直接以源码的方式包含进来的,为了让这些源码可以正常的工作,OSKit为它们提供了"glue"代码,实际上是让这些驱动程序可以通过内核界面和驱动程序界面正常的工作。


2. 驱动-内核界面(device-kernel interface)

驱动程序是要和系统中的硬件打交道的,它们要直接访问系统中的硬件资源,如DMA、I/O端口,有些还要通过IRQ的驱动才能工作。此外,由于驱动程序的运行也需要内存,因此驱动程序还需要操作系统来为它提供内存。在不同的系统中,管理硬件资源的方法是不同的,管理内存等系统资源的方法也是不同的,为了让各种驱动程序能够在OSKit中协调工作,就必须要有一个统一的内核与驱动程序之间的接口,由系统内核去分配管理操作系统中的资源供驱动程序使用。驱动-内核界面就是被定义用来完成这个工作的。

下面,我们按照驱动-内核界面中的不同模块来阐述OS Environment中系统内核为驱动程序提供的接口。

2.1 驱动程序申请内存的管理

2.1.1 概述

驱动程序在工作时是需要申请一些系统中的内存供它们自己使用的,比如将这些内存作为缓冲区等等。在OSEnvironment中有一组函数用来完成为驱动程序分配内存的工作。

为驱动程序分配内存是一项复杂的工作,随着设备总线接口的不同和设备使用资源的不同,驱动程序需要使用的内存也是不同的。比如,如果一块内存是用于DMA传输的,那么这块内存在物理地址上就必须是连续的,有一些设备还要求这部分内存是必须在低16M中的。为了区分驱动程序申请内存的目的的不同,OSEnvironment为这些函数指定了一组标志,用来指出申请的内存的要求或者内存应当如何被分配。

在OSEnvironment中,这些函数的缺省实现是在libdev库中的,实际上,由于这组函数对于目标系统进行了很多不现实的假设,因此,在多数的目标系统中,都需要重新实现这组函数以取代OSKit所提供缺省函数。上面提到的假设包括以下这些:

·通过使用malloc_lmm来分配内存
·内存的申请和释放永远都不会被阻塞住
·所有的内存分配函数在中断时可以被调用
·所申请的内存如果它们在逻辑上是相邻的,则它们在物理上也是
·虚存地址和实存地址空间是一样的
·不支持分页


OS Environment中为什么要有这些假设呢?了解这一点对于为目标系统重新设计这些函数是非常必要的。

在OSKit中,有一个缺省的内存管理模块lmm(Linear MemoryManagement),即线性内存管理。lmm是一种不支持虚存的内存管理方法。Lmm库是OSKit中核心的内存管理模块,这个OSKit在缺省的情况下都是通过它来工作的。因此,libdev模块也是不例外的,如果要替换掉lmm库,则也必须要对libdev模块进行相应的修改。通常,如果我们不引入虚存管理,则是没有必要去替换掉lmm的。

为了简单,在lmm中,没有提供申请或释放内存失败的处理,因此,也就无法提供一种在申请或释放内存时进行阻塞的机制。如果希望将这种机制引入,则目标系统必须对lmm和线程库进行修改或重写。

在Linux、FreeBSD等系统中都是使用了虚存管理的,然而lmm并不提供虚存的管理,所以当把各种驱动程序引入的时候,自然就存在一个使这些驱动程序在实存环境中能够工作的问题。而这个问题最简单的解决方法就是使实存与虚存在地址空间上一致,这样就不会出现问题。

在DMA传送的过程中,可能会用到多于一个的内存块,硬件进行DMA传送时是不会自动进行虚地址的转换的,因此,就要求用于一次DMA传送的实际内存地址必须是相邻的,否则,DMA传送将无法正常的工作。

2.1.2 内存分配标志

在OSEnvironment中,为了正确分配内存,每一个函数都有一个标志参数,其数据类型为osenv_memflags_t。该类型在oskit/dev/dev.h中定义,同时在该文件定义了每一个标志。这些标志有:

OSENV_AUTO_SIZE:使用这个标志告诉内存的管理者必须要记住分配的内存块的大小,在这种情况下,当释放相应的内存时传递给osenv_memory_free函数的关于内存大小的参数就是无效的。如果没有使用这个标志,则内存的申请者必须要记住自己申请的内存块的大小,到释放内存时就要将这个数值作为参数传递给osenv_mem_free函数。
OSENV_NONBLOCKING:这个标志说明分配内存的工作不能被阻塞。在中断处理程序中申请内存时,必须有这个参数。
OSENV_PHYS_WIRED:必须是非页式的,对内存的访问不能无效。
OSENV_PHYS_CONTIG:内存必须连续。
OSENV_VIRT_EQ_PHYS:虚存地址必须与实存地址相同。
OSENV_ISADMA_MEM:指出系统的DMA控制器将会访问此内存区,实际上就是规定此段内存区必须连续,并且在系统的低16M之中,并且不能超过64K的界限。
OSENV_X861MB_MEM:因为有可能某些驱动程序会需要使用1MB以下的内存区,进行实模式的操作,因此设定此项。

2.1.3 内存分配函数

我们大概介绍一下内存分配函数,这些函数在dev/mem.c中定义:

osenv_mem_alloc:驱动程序用来申请内存的函数。
osenv_mem_free:释放内存的函数。
osenv_mem_get_phys:返回一块内存区的实地址。
osenv_mem_get_virt:返回一块内存区的虚地址。
osenv_mem_phys_max:返回内存地址的最大值。
osenv_mem_map_phys:分配内核虚地址并将调用者的实地址映射到这个虚地址。

2.2 DMA的管理

如果系统希望使用DMA控制器,那么就必须提供允许访问DMA控制器的基本函数。目前在OSEnvironment中,是通过oskit_osenv_isa接口来对DMA进行管理的,在该接口中提供了两个对DMA进行申请和释放的工作,它们是isadma_alloc和isadma_free。这个接口中还有一些处理ISA设备注册、注销的方法,将在后面介绍。这个接口在dev/osenv_isa.c中定义。

由于Linux的驱动程序是通过宏及内嵌的汇编直接访问DMA的,因此在OSKit中使用的所有驱动程序集中,只能有一个使用DMA控制器,因为OSKit无法去判断究竟谁占用了资源。

2.3 I/O端口的管理

在很多的系统中,都有输入输出端口的概念,通常的情况下不同的硬件,要使用不同的输入输出端口,当然这也有一点例外。

输入输出的管理,是通过COM对象完成的,对象的类型是oskit_osenv_ioport。此对象提供了三个用于输入、输出的方法,它们是osenv_io_avail、osenv_io_alloc和osenv_io_free,这三个函数分别用于查询一个端口是否被占用、申请端口及释放端口。

2.4 硬件中断

2.4.1 中断支持概述

设备利用中断来通知软件可以对它进行操作,因此管理硬件的一个很重要的任务就是管理中断资源。在OSKit中提供了一套常用的中断管理方法,以支持包装Linux、FreeBSD等系统的驱动程序。此外,OSKit还提供了一个支持实时应用的库,在里面提供了另外一种功能更强的处理中断的方法。

OSEnvironment支持不同设备之间共享一个中断,当然,要求这些设备本身支持共享中断。由于OSKit处理中断的方法,要求每一个硬件的驱动程序都提供一种检测是否是该硬件发出的中断信号,如果是,就进行处理,如果不是则要立即返回以便让下一个设备的驱动程序继续处理。

一般情况下,如果不同的驱动程序集都被链接到了一个内核文件之中,如果在一个驱动程序集中如果屏蔽了中断信号,那么对于所有的驱动程序集来说中断信号就都被屏蔽了。

2.4.2 OS Environment中处理中断的方法

为了更好的理解在OS Environment中应当如何去注册中断处理程序,这里我们大概介绍一下OS Environment中处理中断的方法。

在OSEnvrionment中,使用了一个数组来存储每一个中断处理函数队列中第一个元素的指针,当申请一个中断时,实际上是将一个中断处理函数加入到相应的队列中,当释放一个中断时,就是将该处理函数的指针从处理队列中删除。在OSEnvironment中,有一个缺省的适用于所有中断的处理程序,这个程序写得非常短小,它负责在中断来时,顺序调用该中断相应的中断处理函数队列中的所有的处理函数。

当然还有几个数组用来进行一些辅助工作,如一个中断号是否可以被共享等。

2.4.3 OS Environment中与中断相关的函数

在OSEnvironment中是通过COM对象来管理中断及中断处理函数的。这两个对象分别是oskit_osenv_intr和oskit_osenv_irq。

COM对象oskit_osenv_intr是在dev/x86/osenv_intr.c中定义的,实际的处理函数是在dev/x86/synth.c中定义的。oskit_osenv_intr中提供的用来操作中断控制器的方法是:

intr_enable: 开中断。
intr_disable: 关中断。
intr_enabled:返回当前中断标志。
intr_savedisable:关中断并返回关中断之前的中断标志。

COM对象oskit_osenv_irq是在dev/x86/osenv_irq.c中定义的,实际的处理函数在dev/x86/irq.c、kern/x86/pc/pic.c和dev/x86/pic.c中定义,它提供了以下的用于中断的方法:
intosenv_irq_alloc ( int irqnum, void (*handler)(void *),
void *data, int flags)

分配中断。此函数有四个参数,分别是中断号、处理程序、一个数据和标志。当发生中断时,CPU要把这里给出的数据data传递给中断处理程序。这里的标志flags目前只有OSENV_IRQ_SHAREABLE。
voidosenv_irq_free ( int irqnum, void (*handler)(void *),
void *data )

释放中断。这里的三个参数应当与分配中断时使用的前三个参数相同。
  irq_disable: 关闭一个中断号。
  irq_enable:激活一个中断号。
  irq_pending: 查看一个中断号。

2.5 睡眠与唤醒

在当前的OS Environment模型中,只允许一个线程或请求进入驱动程序集。当驱动程序在等待一个外部事件发生时,可以睡眠以允许其它的请求运行。

当前睡眠与唤醒的操作由COM对象oskit_osenv_sleep提供,它提供了三个方法,分别是:

  osenv_sleep_init: 初始化一个睡眠对象。
  osenv_sleep:让一个进程进入睡眠。
  osenv_wakeup:   唤醒一个睡眠的进程。

2.6 定时器

在OSKit中,通过COM接口oskit_osenv_timer来提供,这个接口为我们提供了以下的方法来操作定时器。

  timer_init:    初始化定时器
  timer_shutdown:  关闭定时器
  timer_spin:    以非阻塞的方式等待一段时间。
  timer_register:  注册一个定时调用的函数。
  timer_unregister: 取消注册。

2.7 ISA设备的注册

要将设备加入系统使其能够使用,就要为设备注册。在OSEnvironment中通过oskit_osenv_isa接口进行对设备的注册。在OSKit中,注册的设备的地址及驱动程序以一个链表来表示。首先要将设备的驱动程序加入这个链表然后,OSEnvironment可以统一的对注册过的设备进行初始化。

oskit_osenv_isa中对设备进行注册和注销的方法是:
  isa_bus_init:对ISA总线进行初始化。
  isa_bus_getbus:得到系统总线数据接口的地址。
  isa_bus_addchild:添加一个子设备。
  isa_bus_remchild:删除一个子设备。

2.8 驱动程序的注册

为了对驱动程序进行注册,OS Environment中提供了一个COM接口oskit_osenv_driver,这个接口提供了三个方法,分别是:

  driver_register:用于注册一个驱动程序。
  driver_unregister:用于注销一个驱动程序。
  driver_lookup:查找驱动程序,这个方法可以用来查找一类驱动程序,如设备ISA设备的驱动。


3. 驱动程序界面(device driver interface)

3.1 块设备驱动程序界面

OS Environment所提供的块输入/输出对象的接口是和POSIX兼容的。这里给出的只是最基本的几个,为了提高性能,对象也可以提供更多的接口。

通过这个接口对块输入/输出对象进行访问时,要知道改对象所使用的最小的块的大小,所有的读写操作都必须以这个大小的单元进行。

3.1.1 getblocksize
函数原型:
OSKIT_COMDECL_U(*getblocksize)(oskit_blkio_t*io);
用途:
得到这个块输入/输出对象的最小的块的大小,这个大小必须是2的幂,并且它将用于这个对象存在的整个生命期之中,所有读、写操作所使用的缓冲区的大小必须是此对象的整数倍。

3.1.2 read
函数原型:
OSKIT_COMDECl (*read)(oskit_blkio_t *io, void*buf,
  oskit_off_t offset, oskit_size_t amount,
  oskit_size_t*out_actual);
用途:
从某个绝对的位移处开始读取指定数量的内容,实际读取的字节数将被存放于out_actual所指向的地址之中。

3.1.3 write
函数原型:
OSKIT_COMDECl (*write)(oskit_blkio_t *io,const void *buf,
oskit_off_t offset, oskit_size_t amount,
oskit_size_t*out_actual);
用途:
从一个绝对的位移处开始写一个块输入/输出对象,如果在写的过程中超出了该对象原有的大小,并且对象的大小无法被扩展的话,则实际写入的字节数会比要求写入的少。实际写入的字节数放在out_actual所指向的内存单元中。

3.1.4 getsize
函数原型:
OSKIT_COMDECl (*getsize)(oskit_blkio_t*io,
oskit_off_t*out_size);
用途:
得到一个块设备当前的以字节计的大小。通常情况下块设备都是固定大小的,但也其大小也可以是可变化的,因此在不同的时候得到的值有可能不同。

3.1.5 setsize
函数原型:
OSKIT_COMDECl (*setsize)(oskit_blkio_t*io,
oskit_off_tnew_size);
设定一个块输入/输出对象的大小,如果设定的值比原有的值小,则超出的部分将被丢弃,如果设定的值比原有的大,则不足的部分将以0值填充。如果此对象的大小是不可变的,则失败。

3.2 TTY设备驱动程序界面

TTY设备的COM接口为oskit_ttydev,它提供下面这些方法,在oskit/dev/tty.h中定义。这个接口可以用来实现传统的POSIX/UnixTTY式的设备如终端或串口。这是一个比较高级的接口并且继承了很多UNIX系统中的较为复杂的东西,因此它并不是一个非常理想的接口。这个接口的功能实际上是需要由驱动程序提供,由于我们使用的驱动程序取自FreeBSD、Linux等系统,而这些系统都采用了POSIX/UNIXTTY式的设备接口,因此这些驱动程序可以直接支持oskit_ttydev接口。

oskit_ttydev,从oskit_device_t继承而来。这个接口仅仅可以访问TTY设备的一些信息,要真正对TTY设备进行读写,就必需要调用open()方法获得一个ttystream对象(参考下一节)。对一个oskit_ttydev对象进行open操作,就表示要对这个对象进行读写,这通常会使驱动程序为访问设备而申请更多的内存。实际上,驱动程序没有职责去做任何的事情,它可以仅仅提供将原本就属于设备节点的ttystream接口,并将对此接口的引用作为open()操作的结果返回。

以下详细叙述接口中每一个方法的用途和用法。

3.2.1 getinfo(oskit_ttydev_getinfo)
函数原型:
OSKIT_COMDECl(*getinfo)(oskit_ttydev_t *fdev,
oskit_devinfo_t*out_info);
用途:通过此函数可以取得TTY设备的描述信息。
参数:
fdev - 这是要取得信息的TTY设备。
out_info - 将取得的信息放于此参数所指向的oskit_devinfo_t结构中。

3.2.2 getdriver(oskit_ttydev_getdriver)
函数原型:
OSKIT_COMDECl(*getdriver)(oskit_ttydev_t *fdev,
oskit_driver_t**out_driver);
用途:获得某个设备的驱动程序接口的引用。
参数:
fdev -要取得驱动的TTY设备。
out_driver - 驱动接口的内存地址。

3.2.3 open(oskit_ttydev_open)
函数原型:
OSKIT_COMDECl(*open)(oskit_ttydev_t *dev, oskit_u32_t flags,
struct oskit_ttystream**out_ttystream);
用途:打开设备并使其工作在标准的"呼叫"方式,如果设备没有被使用,则open操作通常会迅速结束,否则,将返回OSKIT_EBUSY错误。
参数:
dev:要读取的设备
flags:读取时使用的标志。
out_ttystream:将返回的ttystream接口的地址放在这个参数所指向地址的指针变量中。

3.2.4 listen(oskit_ttydev_listen)
函数原型:
OSKIT_COMDECl(*listen)(oskit_ttydev_t *dev,
oskit_u32_t flags,
struct oskit_ttystream**out_ttystream);
用途:将此设备以被动的等待方式打开。打开之后,进程将会暂停,直到有一个设备来调用它。如果设备不支持这种被动的callin的方式,则会返回错误码OSKIT_E_NOTIMPL。
参数:
dev:要读取的设备。
flags:读取时使用的标记。
out_ttystream:将返回的ttystream接口的地址放在这个参数所指向地址的指针变量中。

3.3 TTY流界面

TTY流的COM接口为oskit_ttystream。这个接口实现了POSIX.1规定的终端流规范。下面详细叙述接口中的每一个方法。

3.3.1 read(oskit_ttystream_read)
函数原型:
OSKIT_COMDECl(*read)(oskit_ttystream_t *s,
void *buf,
oskit_u32_t len,
oskit_u32_t*out_actual);
说明:
此函数从TTY流中读入指定的字节,并放入一个缓冲区中。
参数:
s:要操作的ttystream接口。
buf:存放输入的缓冲区。
len:最多读入的字节数。
out_actual:将实际读入的字节数存放到此指针所指向的单元。

3.3.2 write(oskit_ttystream_write)
函数原型:
OSKIT_COMDECl(*write)(oskit_ttystream_t *s,
const void *buf,
oskit_u32_tlen,
oskit_u32_t*out_actual);
说明:
向一个TTY流中写入指定的字节。
参数:
s:要操作的ttystream接口。
buf:存放输出的缓冲区。
len:希望输出的字节数。
out_actual:将实际输出的字节数存入此指针所指向的地址。

3.3.3 seek(oskit_ttystream_seek)
函数原型:
OSKIT_COMDECl(*seek)(oskit_ttystream_t *s,
oskit_s64_t ofs,
oskit_seek_twhence,
oskit_u64_t*out_newpos);
说明:
将流指针定位到一个指定的位置。
参数:
s:要操作的流对象。
ofs:定位所采用的参考位置。
where:定位的目的位置。
out_newpos:将定位的最终位置存入此指针所指向的内存单元。

3.3.4 copyto(oskit_ttystream_copyto)
函数原型:
OSKIT_COMDECl(*copyto)(oskit_ttystream_t *s,
oskit_ttystream_t *dst,
oskit_u64_tsize,
oskit_u64_t *out_read,
oskit_u64_t*out_written);
说明:
从一个流中读出指定的字节并写入另一个流中。
参数:
s:要操作的源TTY流。
dst:要操作的目的TTY流。
size:要复制的字节数。
out_read:将实际读出的字节数写入此指针指向的内存单元。
out_written:将实际输出的字节数写入此指针所指向的内存单元。

3.3.5 clone(oskit_ttystream_clone)
函数原型:
OSKIT_COMDECl(*clone)(oskit_ttystream_t *s,
oskit_ttystream_t**out_stream);
说明:
复制一个TTY流。
参数:
s:要被复制的TTY流
out_stream:将新的TTY流的地址写入此指针所指向的内存单元。

3.3.6 getattr(oskit_ttystream_getattr)
函数原型:
OSKIT_COMDECl(*getattr)(oskit_ttystream_t *s,
struct oskit_termios*out_attr);
说明:
得到一个TTY流的termios属性。
参数:
s:要操作的TTY流。
out_attr:将此TTY流的属性放入此指针所指向的内存单元。termios是在POSIX中定义的输入输出参数的结构,该结构定义如下。
structoskit_termios {
  oskit_tcflag_t iflag;
  oskit_tcflag_toflag;
  oskit_tcflag_t cflag;
  oskit_tcflag_t lflag;
  oskit_cc_tcc[OSKIT_NCCS];
  oskit_speed_t ispeed;
  oskit_speed_t ospeed;
};

下面将大概介绍一下termios结构中各个字段的用法。

Iflag:这个字段的值描述基本的中断输入控制。他们由该表中所示的屏蔽码的按位"或"构成,这些屏蔽码是按位不同的。下表中给出屏蔽码的符号和简单的含义:

屏蔽码符号   屏蔽码值  含义
OSKIT_IGNBRK  0x00000001 忽略中止信号
OSKIT_BRKINT  0x00000002 中止时的中断信号
OSKIT_IGNPAR  0x00000004 忽略有奇偶错的字符
OSKIT_PARMRK  0x00000008 标记奇偶错
OSKIT_INPCK0x00000010 启用输入奇偶校验
OSKIT_ISTRIP  0x00000020 剥取字符
OSKIT_INLCR0x00000040 输入时将NL映射为CR
OSKIT_IGNCR   0x00000080 忽略CR
OSKIT_ICRNl0x00000100 输入时将CR映射为NL
OSKIT_IXON   0x00000200 启用启动/暂停输出控制
OSKIT_IXOFF0x00000400 启用启动/暂停输入控制
OSKIT_IXANY   0x00000800 中止后任意字符可重新开始

oflag字段的值描述基本的终端输出控制,它由下表中的屏蔽码按位"或"构成。

屏蔽码符号   屏蔽码值  含义
OSKIT_OPOST0x00000001 执行输出控制
OSKIT_ONLCR   0x00000002 将NL映射为CR-NL

cflag字段的值描述基本的中断硬件控制,它的值由下面给出的屏蔽码按位"或"构成。

屏蔽码符号   屏蔽码值  含义
OSKIT_CSIZE0x00000300 字节位数掩码
OSKIT_CS5    0x00000000 5位
OSKIT_CS60x00000100 6位
OSKIT_CS7    0x00000200 7位
OSKIT_CS80x00000300 8位
OSKIT_CSTOPB  0x00000400 发送两个停止位,无此码时发送一个
OSKIT_CREAD0x00000800 启用接收器
OSKIT_PARENB  0x00001000 启用奇偶校验
OSKIT_PARODD  0x00002000 奇校验,无此位时是偶校验
OSKIT_HUPCl0x00004000 最后一次关闭时挂断
OSKIT_CLOCAl  0x00008000 忽略调制解调器的状态线

lflag字段的值描述对局部方式的控制,它由一下的屏蔽码按位"或"构成。

屏蔽码符号   屏蔽码值  含义
OSKIT_ECHOE0x00000002 回显删除字符(erase char)
OSKIT_ECHOK0x00000004 回显KILL
OSKIT_ECHO   0x00000008 启用回显
OSKIT_ECHONl  0x00000010 回显"\n"
OSKIT_ISIG   0x00000080 启用信号
OSKIT_ICANON  0x00000100 加工方式输入
OSKIT_IEXTEN  0x00000400 启用扩展的功能(DISCARD和LNEXT)
OSKIT_TOSTOP  0x00400000 停止后台作业的输出
OSKIT_NOFLSH  0x80000000 中断、退出或中止后不清除

3.3.7 setattr(oskit_ttystream_setattr)
函数原型:
OSKIT_COMDECl(*setattr)(oskit_ttystream_t *s,
int actions,
const struct oskit_termios*attr);
说明:
设定一个TTY流的termios属性。
参数:
s;要操作的TTY流。
actions:要进行的操作,可以是以下值:OSKIT_TCSANOW,使改变立即生效;OSKIT_TCSADRAIN,全部输出后使改变生效;OSKIT_TCSAFLUSH,将输出、输出全部完成后使改变生效。
attr:一个termios结构,此结构中存放希望为此TTY流设定的属性,关于termios结构可以参考getattr方法的说明。

3.3.8 sendbreak(oskit_ttystream_sendbreak)
函数原型:
OSKIT_COMDECl(*sendbreak)(oskit_ttystream_t *f,
oskit_u32_tduration);
说明:
在一段规定的时间内连续输出0值位。
参数:
f:要进行操作的TTY流。
Duration:输出0值位的时间,如果是0,则0值位的传输时间至少持续0.25秒,但不超过0.5秒。

3.3.9 drain(oskit_ttystream_drain)
函数原型:
OSKIT_COMDECl(*drain)(oskit_ttystream_t*s);
说明:
此函数将等待,直到所有写到TTY流s上的输出都已经完成为止。
参数:
s:要进行操作的TTY流。

3.3.10 flush(oskit_ttystream_flush)
函数原型:
OSKIT_COMDECl(*flush)(oskit_ttystream_t *s,
intqueue_selector);
说明:
将一个TTY流中的缓冲区中的数据清空,即将已经接受但尚未读入的数据读入或者是将已经输出但尚未发送的数据发送,或者两者都有。
参数:
s:要操作的TTY流。
queue_selector:用来指定是对输入还是对输出进行操作。可以是OSKIT_TCIFLUSH,对输入进行操作;OSKIT_TCOFLUSH对输出进行操作;或者OSKIT_TCIOFLUSH,对输入和输出都进行操作。

3.3.11 flow(oskit_ttystream_flow)
函数原型:
OSKIT_COMDECl(*flow)(oskit_ttystream_t *s, intaction);
说明:
这个函数使在TTY流s上的输出暂停或开始。
参数:
s:要操作的TTY流。
action:要进行的操作。可以是OSKIT_TCOOFF,暂停输出;OSKIT_TCOON,重新开始输出;OSKIT_TCIOFF,发送一个停止字符;OSKIT_TCION,发送一个开始字符。

3.3.12 ttyname(oskit_ttystream_ttyname)
函数原型:
OSKIT_COMDECl(*ttyname)(oskit_ttystream_t *s,
char**out_name);
说明:得到TTY终端的名称。
参数:
s:要操作的TTY流对象。
out_name:将名称字符串的地址放在这个内存单元中。

关于以上讲述的OSKit的TTY流的接口的更多信息,可以参考POSIX中的相关部分。

[目录]


第三章 OS Environment包装Linux驱动程序的方法分析

《OSKit包装系统驱动程序 第三章》


OSKit包装系统驱动程序 第三章2000年12月9日22:05


作 者: 李凌


导师:陈朔鹰


第三章 OS Environment包装Linux驱动程序的方法分析


1. Linux内核为驱动程序提供的支持

为了让驱动程序能够正常的工作,操作系统内核必须要为驱动程序提供一系列的支持,这些支持包括许多方面。举例来说,驱动程序需要像内核申请使用系统内存,驱动程序需要向内核使用系统硬件资源(如端口、DMA控制器),驱动程序需要向内核注册自己,尤其是那些占用中断资源的驱动程序。

要将其它的系统的驱动程序包装进来,就首先要了解其它的系统的内核是如何为自己系统的驱动程序提供支持的,然后通过OSEnvironment为这些驱动提供支持就可以了。由于我们是在Linux上使用OSKit的,所以就顺便选择了分析一下Linux大概通过哪些函数为内核的驱动程序提供支持。我们并没有将所有的支持函数都找出来,只是找了一些比较重要比较典型的,为的是学习OSKit中包装别人的驱动程序的方法。

Linux中这些函数很多在kernel目录下,在这个目录下的dma.c、resource.c、printk.c中,分配内存的函数在mm目录下。下面我们大概看一下Linux中的情况。

1.1 内存分配函数

在Linux中,驱动程序通常使用kmalloc函数申请内存,当然,也可以使用其它的函数申请。但因为kmalloc函数使用的比较多,我们重点说一下它。

void *kmalloc(size_t size, int flags)

kmalloc函数和malloc函数相似,它有两个参数,一个参数是size,即申请内存块的大小,这个参数比较简单,就像malloc中的参数一样。第二个参数是一个标志,在里面可以指定优先权之类的信息。在Linux中,有以下的一些优先权:

GFP_KERNEL,它的意思是该内存分配是由运行在内核模式的进程调用的,即当内存低于min_free_pages的时候可以让该进程进入睡眠;

GFP_ATOMIC,原子性的内存分配允许在实际内存低于min_free_pages时继续分配内存给进程。

GFP_DMA:此标志位需要和GFP_KERNEL、GFP_ATOMIC等一起使用,用来申请用于直接内存访问的内存页。

1.2 DMA

Linux的设备驱动程序在使用DMA通道以前必须申请,在使用之后必须要释放,这样从理论上讲,就允许几个设备同时使用一个DMA通道了,当然,很多设备是无法这样使用的。

Linux中申请和释放DMA通道的函数是request_dma( )和free_dma( )。

1.3 I/O端口

Linux中,系统维护着一个输入输出端口的设备注册表,设备驱动程序在使用输入输出端口前,也必须要进行申请。只有申请成功,才能够使用相应的端口。Linux内核提供了三个函数用于申请、释放端口及检查端口是否被占用。这三个函数是:

void request_region(unsigned long from,
      unsigned long num, constchar *name)
这个函数用来申请一块输入输出区域。

void release_region(unsigned long from, unsigned longnum)
这个函数用来释放一块输入输出区域。

int check_region(unsigned long from, unsigned longnum)
这个函数用来检测一块输入输出区域是否被占用。

1.4 中断号的申请

中断是非常宝贵的系统资源,在Linux中,系统维护着一个象输入输出设备注册表一样的中断资源注册表。任何驱动程序在使用一个中断,即作为中断处理程序之前,要申请一个中断通道,并且,在处理完成之后,可以释放它。在Linux中,对于i386平台,这些函数定义在arch/i386/kernel/irq.c中,申请和释放一个中断通道的函数是:

int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs*),
     unsigned long irqflags,
     const char * devname,
     void*dev_id)
void free_irq(unsigned int irq, void *dev_id)

其中各个参数的含义如下:

irq: 要申请的中断资源的中断号。
handler: 指向要安装的中断处理函数的指针。
irqflags:这是一个与中断管理有关的各种选项的字节掩码。此项可选值如下:SA_INTERRUPT、SA_SHIRQ、SA_SAMPLE_RANDOM。如果不置SA_INTERRUPT,则标明这个中断处理程序时一个"慢速"的中断处理程序,如果置了这一位,这个处理程序就是一个"快速"的中断处理程序。
devname:传递给request_irq的字符串,可以通过Linux的proc文件系统显示中断地拥有者的名字。
dev_id:这个参数用于共享中断号,驱动程序可以自由的任意的使用dev_id,除非强制使用中断共享,dev_id通常被置为NULL。

1.5 打印函数

内核程序为了输出一些信息,就需要一个内核打印函数,Linux中的在内核中使用的打印函数是printk,这个函数的参数同普通的printf是一样的。


2. OSKit替换Linux内核-驱动界面的方法

大致了解了Linux内核为其驱动程序提供的支持之后,我们就可以来看一看OSKit究竟是采用什么方法来使用Linux的驱动程序的源码的。OSKit使用Linux驱动程序的方法,并不是去修改Linux驱动程序的源代码。

看了上面Linux内核所提供的部分支持,再看看前面OSEnvironment所提供的支持,不难发现它们有着很多的相似之处。Linux的驱动程序要使用内核提供的支持,就必须要包含内核的头文件,因此,只要根据OSKit的接口所提供的功能,重写一下Linux驱动程序需要的那些应当由内核提供的支持函数,就可以让Linux的驱动程序作为OSKit的一部分进行编译了。

当然,仅仅能够编译还是不够的,还必须要让驱动程序能够正常的工作,这一方面要在包装原有的代码上面下功夫,比如要能够正确的处理函数传递过来的各种参数,尤其是各种标志掩码。另一方面,还要为驱动程序建立一个可以运行的环境,这一部分内容就是OSKit整体设计的问题了,在前面的OSKit介绍中已经大概讲过了。当真正要设计一个目标系统的时候,这一点就是必须要考虑得非常仔细了。


3. OSKit包装Linux驱动程序的实际例子

上面的内容,完全是对OSKit包装Linux驱动程序的方法的理论上的叙述,下面我们具体看两个OSKit中的例子,这样就可以对这种方法有更深刻的了解。

3.1 DMA

OSKit为Linux的驱动程序提供了支持,提供了Linux驱动程序需要的两个函数,request_dma和free_dma,下面是这两个函数在OSKit中的实现。此段代码在linux/dev/dma.c中。
/*
*Allocate a DMA channel.
*/
int
request_dma(unsigned int drq, const char*name)
{
  struct task_struct *cur = current;
  int chan;

  chan = osenv_isadma_alloc(drq);

  current = cur;
  return chan;
}

/*
* Free a DMA channel.
*/
void
free_dma(unsigned intdrq)
{
  struct task_struct *cur = current;

  osenv_isadma_free(drq);

  current = cur;
}

从上面的两个程序,我们可以看出,OSKit所提供的request_dma( )函数实际上是调用了OSEnvironment所提供的osenv_isadma_alloc( )函数,而free_dma( )则是调用了osenv_isadma_free( )函数。

DMA的例子时非常简单的,下面我们看一个比较复杂一点的例子。

3.2 内存管理

void *
kmalloc(unsigned int size, int priority)
{
  intflags;
  void *p;

  flags = KMALLOC_FLAGS;
  if (priority  GFP_ATOMIC)
    flags |=OSENV_NONBLOCKING;
  if (priority  GFP_DMA)
    flags |=OSENV_ISADMA_MEM;
  p = oskit_linux_mem_alloc(size, flags, 0);
  return(p);
}

上面的程序段,就是OSKit提供的由Linux驱动程序使用的用来进行内存分配的函数。这个函数并没有直接调用OSEnvironment中的osenv_mem_alloc函数,这是有原因的,我们会在后面介绍。

这里值得注意的是OSKit用自己的标志代替了Linux驱动程序所使用的标志。比如,如果Linux驱动程序给出了标志GFP_ATOMIC,则用相应的OSENV_NONBLOCKING来代替,而GFP_DMA则用相应的OSENV_ISADMA_MEM来代替。

下面我们来看一下上面的程序段中用到的oskit_linux_mem_alloc函数。

void *
oskit_linux_mem_alloc(oskit_size_t size, osenv_memflags_t flags,unsigned align)
{
  struct task_struct *cur = current;
  void*mem;
  mem = osenv_mem_alloc(size, flags, align);
  current =cur;
  return mem;
}

这个函数,实际调用了OSKit所提供的内存分配函数,在调用osenv_mem_alloc之前,保存了当前进程的进程控制块的指针,在内存申请之后再恢复,这是因为内存申请的请求有可能被阻塞。

另外还有一个原因,就是Linux实际提供的内存分配函数并不止kmalloc一个,还有get_free_pages和vmalloc等多个,然而在OSKit的缺省的实现中,只是通过osenv_mem_alloc来进行内存分配的。

OSKit为Linux所提供的内存分配函数是在linux/share/kmem.c中定义的。


4. 小结

通过上面的两个例子,不难看出OSKit包装Linux驱动程序的基本方法,输入输出端口、中断和内核打印输出等部分和这些部分实现的基本原理是相同的,当然随着支持的不同以及OSKit同Linux所提供的底层支持之间的差异,程序的写法是不同的。我们所分析的这几个例子是比较简单的,同时也并没有把Linux的所有函数都列出来,我们分析这些例子,目的是学到一种方法及思想。

OSKit除了使用了Linux的设备驱动程序,还是用了FreeBSD的设备驱动程序、Linux的网络设备驱动程序等等很多其它操作系统中的模块,我们选择Linux的设备驱动程序进行分析是因为这一段的程序代码相对比较清晰简单,那些部分同这个部分所使用的思想和方法是大致相同的,只是接口不同、数据结构不同而已。


[目录]


第四章 OS Environment自行开发驱动程序的方法

《OSKit包装系统驱动程序 第四章》


OSKit包装系统驱动程序 第四章2000年12月12日22:45


作 者: 李凌


导师:陈朔鹰

第四章 OS Environment自行开发驱动程序的方法


1. 概述

上文中,我们用大量的篇幅介绍了OS Environment所提供的接口以及OSKit去包装其它系统的驱动程序的方法。那么我们是否可以在OSEnvironment下开发自己的驱动程序集呢?回答是肯定的,原因也是很简单的,OSEnvironment为我们提供了足够强大的接口,让我们去访问系统的资源,只要我们去调用这些接口,然后根据硬件设备的说明书去写驱动程序就可以了。

然而,有一点在这里是需要考虑的,那就是OSEnvironment的设计意图是什么,因为这个环境的设计意图将直接影响到在这个环境中去开发驱动程序的效果。按照OSKit设计者的说法,OSEnvironment并非是为了新写的驱动程序去设计的,因此,OSEnvironment中关于驱动程序的思想也就不会是很先进的,同样,也不会有其它的系统为兼容OSEnvironment而去重新改进自己,因此如果想重写很多设备的驱动程序的话,那么最好的选择可能不是在OSEnvironment下开发,可以选择去兼容驱动程序的DDI/DKI标准或UDI等标准,因为这些标准从设计的思想上来说就是作为通用的驱动程序接口,它们会考虑到更多的系统对它们的兼容性等方面的问题。另外,也可以选择Linux等系统作为开发平台,因为这些系统内核的实现细节是公开的,更有利于写某些特殊的设备的驱动程序。

由于在什么平台上写驱动程序已经超出了本文要讨论的范围,因此这里我们不再多说,我们还是仔细讨论一下在OSEnvironment这个环境下,一个驱动程序应当如何去写。


2. 基本方法

写驱动程序是一件非常复杂的事情,不仅要对操作系统的知识有所了解,还要了解,还要对计算机的体系结构,以及所要驱动的设备非常的熟悉。一个驱动程序究竟是写成支持标准的POSIX标准呢,还是仅仅为了一些较为特殊的应用,按照自己的要求去写,这些都是非常复杂的问题,对于驱动程序的作者来说,是必须要考虑好的。

一个驱动程序,通常是要调用操作系统提供的接口,并且为操作系统使用此设备提供一套接口。由于OSEnvironment最初的设计目的与设计思路,它是与UNIX类的系统的操作系统关于驱动程序的设计思想保持兼容的。UNIX的一个基本特征就是它抽象了设备的处理,所有的硬件设备都与常规的文件十分相似,可以通过与操作文件完全一样的标准系统调用去打开、关闭、读、写一个设备。如果你希望你的操作系统最终能够让用户按照类UNIX的方式去访问设备,那么为了达到这个目的,驱动程序就必须为操作系统提供这样的功能。但是,如果你希望操作系统的用户可以用其它的更为简单或者更加出色的方式去访问设备,那么你也可以按照自己的方式去写。这完全是由目标操作系统及应用来决定的。

如果已经决定了要写一个驱动程序,希望它所提供的接口是类UNIX的,那么这个驱动程序所要提供的接口就可以在前面第二章中找到,因为实际上OSEnvironment对外提供的接口都是要驱动程序来支持的。

但无论你的驱动程序的具体接口按照哪一种方法去实现,都有一些调用时必须要提供给系统的,现在我们就来讨论一下这些必须提供的调用的问题。


3. 驱动程序必须实现的部分

3.1 OS Environment中驱动程序的接口及数据结构

3.1.1 基本的驱动程序接口

OSEnvironment中,所有的设备驱动程序最终都将抽象为一个统一的接口,通过这个接口,通过这个接口,可以获得设备的基本信息。这个COM接口的名称是oskit_driver。这个接口的定义如下:

struct oskit_driver_ops {
  OSKIT_COMDECL (*query)(oskit_driver_t*drv,
          const struct oskit_guid *iid,
          void**out_ihandle);
  OSKIT_COMDECL_U (*addref)(oskit_driver_t*drv);
  OSKIT_COMDECL_U (*release)(oskit_driver_t *drv);
  OSKIT_COMDECL(*getinfo)(oskit_driver_t *drv,
          oskit_devinfo_t *out_info);
};

这个接口中,除了三个所有COM接口都支持的方法以外,有一个getinfo方法,这个方法可以获得驱动程序的信息,驱动程序的信息会被存储在out_info所指向的内存单元中。关于oskit_devinfo_t这个结构,我们会在下一节中介绍。

这个接口是一个基类,当一个驱动程序要去实现这个接口的时候,要继承这个接口,并且提供一个新的方法probe。probe方法的作用是对驱动程序进行初始化,检测系统中是否存在此设备,如果存在,则要把设备注册到系统总线上,把设备的信息注册到系统COM数据库中等工作。

3.1.2 表示驱动程序信息的数据结构

在上面的函数中,用到了一个数据结构oskit_devinfo,这个数据结构是用来记录驱动程序的基本信息的,在OSEnvironment中经常用到,并且如果编写一个驱动程序,则也要提供这个结构,因此,在这里详细介绍一下,这结构的定义如下:

struct oskit_devinfo {
  const char *name;
  const char*description;
  const char *vendor;
  const char *author;
  const char*version;
};

这个结构中所有的指针所返回的字符串都应当是常量,并且对于设备节点存在时总能够使用。除了name以外的其它元素都可以是NULL以表示该信息不详。

name是设备的一个简单标识,例如"aha1542",name字符串的长度不能超过OSKIT_DEVNAME_MAX所定义的长度,在OSKit中,这个常量的缺省值是15。此字符串只能包含7位的字符,即字符的ASCII码值在0-127之间,并且第一位必须是字母。

description:  对于设备的详细描述,如"Adaptec 1542 SCSIcontroller"。
vender:    设备生产商的名字,例如"Adaptec"。
author:    设备驱动程序的作者。
version:驱动程序的来源及版本,例如"Linux 1.3.36"。

3.1.3 基本的设备接口

在OSKit中,基本的设备接口是oskit_device,实现了这个接口的每一个对象对应着一个实际的设备。通常,一个设备驱动程序可以操纵多个设备,但设备驱动程序对象与设备对象之间是没有关系的,设备对象并非驱动程序对象的一部分。当然,如果你愿意,也可以将OSEnvironment改写成那样。设备接口的方法的定义如下:

struct oskit_device_ops {
  OSKIT_COMDECL (*query)(oskit_device_t*dev,
          const struct oskit_guid *iid,
          void**out_ihandle);
  OSKIT_COMDECL_U (*addref)(oskit_device_t*dev);
  OSKIT_COMDECL_U (*release)(oskit_device_t *dev);
  OSKIT_COMDECL(*getinfo)(oskit_device_t *dev,
          oskit_devinfo_t*out_info);
  OSKIT_COMDECL (*getdriver)(oskit_device_t*dev,
          oskit_driver_t **out_driver);
};

出了基本的COM方法以外,此接口有两个自己的方法,一个是getinfo,通过这个方法可以得到设备的信息。设备接口的getinfo方法和驱动程序接口的getinfo方法基本上是相同的,但是,同一个设备的设备接口和驱动接口所返回的信息也并不一定要完全相同,例如,可以让驱动程序接口返回"aha15xx",以表示此驱动程序支持一系列的设备,而让设备接口返回"aha1542"以表示此设备的具体型号。

getdriver方法将一个指向该设备的驱动程序的指针放入out_driver所指向的内存单元中。

3.2 OS Environment中驱动程序的注册方法

驱动程序的注册,包括驱动程序本身的注册和设备的注册。驱动程序本身的注册先进行,而设备注册等程序则应当写在驱动程序的probe代码中,在检测设备的时候进行注册。

驱动程序本身的注册可以通过OSEnvironment所提供的接口来进行,这个接口已经在前面第二章介绍过了,注册驱动程序的方法是driver_register,注销驱动程序的方法是driver_unregister。

OSEnvironment也为注册设备而提供了接口,它们是device_register和device_unregister。设备除了要注册到COM对象的数据库中之外,还应当注册到系统的总线上,这样,OSEnvironment就可以实现一个设备树。

3.3 OS Environment中驱动程序注册、自检的全过程

OSEnvironment中驱动程序的注册、自检的过程,可以说是COM在OSKit中的应用的一个很好的表现。在OSKit中,用来表示驱动程序的COM数据库有两个,一个是driver_registery,另一个是device_registery。如上一小节所述,要首先将驱动程序注册到driver_registery中,然后在对系统中所有设备进行检测时,再利用驱动程序的probe函数将相应的设备注册到device_registery中。下图表现了这一过程,并标明了各个工作分别应当在何时完成。

在上图中isa_bus_probe()及pci_bus_probe() 的部分是由OSEnvrionment提供的,在进行检测的时候,他们并不知道这个系统支持多少设备(注册了多少驱动),完全通过驱动程序的COM数据库,来获取这个信息。

上图中,背景色较深的方框表示要由驱动程序的作者实现的部分,而其余则是OSEnvrionment的功能。可以看出,驱动程序的作者除了程序本身的功能外,要提供一个初始化驱动和一个检测设备的方法。这两个程序的写法,可以参考OSKit包装Linux或FreeBSD的驱动程序时使用的代码。


[目录]


第五章 OSKit的应用实例

《OSKit包装系统驱动程序 第五章》


OSKit包装系统驱动程序 第五章2000年12月12日22:50


作 者: 李凌


导师:陈朔鹰

第五章 OSKit的应用实例--一个简单系统的设计与实现


1. 设计目的

OSKit是一个用来帮助我们研发操作系统的工具,如果你想更好的掌握它,除了对其本身进行分析之外,还有一个更加实际的方法,那就是利用这个工具开发一个我们自己的操作系统。

我们开发这个系统的初衷主要是为了学习,因此并没有想把它设计成像Windows、UNIX那么复杂。我们的目标是很明确的,那就是在大致搞明白OSKit应当如何使用之后,做一次尝试,用较短的时间,设计出一个具有一些最基本功能的系统。这对于我们课题组的每名成员来说,都意味着挑战。


2. 系统的功能

这个系统支持些什么?这是在开始设计之前我们要首先确定的一个问题。下面列出了一些我们希望实现的功能。

·通过Multiboot或者LILO进行系统引导:这个系统可以通过所有支持Multiboot的引导程序进行引导,或者通过引导Linux的LILO进行引导。引导方式可以根据用户的要求,选择是通过软盘还是硬盘。
·多线程:用户可以通过我们提供的系统调用,创建或杀死自己的线程。由于并不支持文件系统,因此新的线程恐怕只能是一个函数了。从理论上讲,我们可以让这个系统支持很多线程,但因为这仅仅是一个简单的演示性系统,因此我们把线程数限制在32个,当然这个数字也可根据用户的需求任意更改。
·进程间通信:这个系统可以支持一些简单的进程间通信机制,比如信号量、消息队列等等。
·外设:在众多的外设之中,我们选择了应用范围比较广的串口。利用目前OSKit所提供的机制,支持更多的设备是不成问题的,但加在这个系统中意义并不十分的大。另外,需要强调的是,访问串口的方法,是由我们自己规定,不允许用户更改。
·简单的内存管理:应用程序需要的内存可以通过malloc函数动态申请。

3. 总体构想

在该操作系统中,我们规定用户只能创建用户级线程,所有对于核心的操作都将由系统来完成。线程采用RR调度算法,也就是时间片轮转法,并允许抢占,线程在该系统中有四种状态,它们是运行态、阻塞态、睡眠态和就绪态,用户可以设定和改变线程的优先级。在线程中用户可以动态申请内存,但是需要由用户来释放。线程间提供简单的通信机制,包括信号量和消息队列。

在设备方面,对于终端,本系统将提供简单的输入输出功能。而对于串口的功能,本系统可以完整的实现,也就是说,用户既可以从串口读也可以向串口写。

为了方便起见,我们为这个系统起一个很简单的名字,叫acos,这个名字并没有什么实际的意义,仅仅为了叫起来方便。

虽然OSKit已经为我们提供了很多设计操作系统所必须的功能,比如内存分配中的malloc函数,通过Multiboot引导等等。然而,仍有许多工作是要由我们自己完成。


4. 我们自己所完成的工作:

4.1 系统的启动

这里我们所说的系统启动,并不是指通常人们所说的通过软盘或硬盘引导,而是在引导结束之后,操作系统的核心进程如何启动第一个用户进程。通常,在UNIX系统中,内核启动之后,内核程序会去执行文件系统上的init程序,这个程序是整个操作系统中的第一个用户进程,剩下的进程,都必须通过这个进程才能启动。在acos中,我们规定,用户必须要提供一个acos_init()函数,系统在启动初始化工作全部完成之后,acos_init()函数将被作为系统中的第一个进程启动。这之后的工作,就完全由用户来完成了。

当进程acos_init() 结束时,整个系统也将终止运行。

在acos_init()启动之前,究竟还有哪些事情需要做呢?下面我们就列出要做的事情:

对整个系统进行初始化,注册设备,检测设备,初始化设备需要的数据结构,初始化线程库,创建线程需要的数据结构,初始化使串口能在多线程环境下正常工作的数据结构。

4.2 线程管理

所谓操作系统的线程管理,一般包括了一下的三大部分:线程的创建,线程间通信和线程调度。

线程的创建包括了初始化线程和创建线程的属性。

线程间通信中,我们使用了信号量,条件变量。

在线程调度中,用户线程只能使用时间片轮转法作为调度算法,并且规定在调度过程中调度算法不允许更改。

4.3 外设(串口)

在UNIX系统中,所有的设备都可以通过文件系统进行访问,读、写设备就像读写文件一样。在我们设计的这个acos中,并没有对文件系统提供支持,也不提供像读写文件系统一样的方法来访问设备。但是,我们提供一套基本的函数,供用户线程对串口进行读写操作,这些函数专门用于处理串口。

一个设备在同一时刻只能被一个线程使用,因此,在使用设备以前要申请,只有申请成功的线程才能够使用设备。


5. 使用方法

5.1 安装

使用者需要在Linux下键入 tar -zxvf acos-0.0.1.tar.gz 。

./setup ,详细的安装信息请参见README文件。

5.2 使用

根据我们所提供的API手册编写自己的应用程序,然后将这些应用程序的源文件编译成一个filename.o,用户通过执行acbuildfilename.o命令将该应用程序与操作系统内核相链接。根据你所希望使用的引导方法执行mkXXXimage命令生成所需要的核心映像,选择恰当的装入程序装入启动核心映像。

5.3 用户编程接口

如果您对操作系统的概念有所了解,并详细阅读了我们为您提供的技术文档之后,便可以利用用户编程接口自行开发了。以下我将向各位详细介绍每个接口所提供的功能,希望对您能有所帮助。

5.3.1 管理线程的API :

线程创建
ac_thread_create ( void *name, void *arg, int *out_thread_id);
设置线程的优先级
ac_thread_setprio(int thread_id, intpri);
撤销线程
ac_thread_cancel(intthread_id);
线程休眠
ac_thread_sleep(ac_s64_tmillisecond);
将该线程与某一个线程相关联
ac_thread_join(int tid, void **status);

5.3.2 管理互斥量的API

初始化互斥量
ac_mutex_init ( ac_pthread_mutex_t*mutex,
ac_pthread_mutexattr_t* attr);
设置互斥量协议
ac_mutexattr_setprotocol( ac_pthread_mutexattr_t* attr,
intprotocol);
设置互斥量类型
ac_mutexattr_settype ( ac_pthread_mutexattr_t*attr,
int type);
初始化互斥量属性
ac_mutexattr_init(ac_pthread_mutexattr_t*attr);
互斥量加锁
ac_mutex_lock(ac_pthread_mutex_t*mutex);
互斥量解锁
ac_mutex_unlock(ac_pthread_mutex_t*mutex);
撤销互斥量
ac_mutex_destroy(ac_pthread_mutex_t *mutex);

5.3.3 管理IPC的API

消息发送( IPC机制 )
ac_ipc_send ( int thread_id, void* msg, ac_size_tmsgsize,
ac_s32_t timeout);
3.2消息接受( IPC机制 )
ac_ipc_recv(int thread_id,void* msg, ac_size_t msgsize,
ac_size_t *actual, ac_s32_t timeout);
消息等待(IPC机制 )
ac_ipc_wait(int *src_id, void *msg, ac_size_t msgsize,
ac_size_t*actual, ac_s32_t timeout);
接受同步消息
ac_ipc_reply(int desthread, void *msg,ac_size_t msg_size);
发送同步消息
ac_ipc_call(int dst, void *send_msg, ac_size_tsend_msg_size,
void *recv_msg, ac_size_t recv_msg_size,
ac_size_t*actual,ac_s32_t timeout);

5.3.4 管理条件变量的API

条件变量初始化
ac_cond_init(ac_pthread_cond_t *cond,
ac_pthread_condattr_t*attr);
某线程等待条件变量
ac_cond_wait(ac_pthread_cond_t*cond,
ac_pthread_mutex_t*mutex);
唤醒等待该条件变量的线程
ac_cond_signal(ac_pthread_cond_t*cond);
符合条件变量的线程广播:
ac_cond_broacast(ac_pthread_cond_t *cond);

5.3.5 硬件部分

打开串口
ac_serial_open(int serial_num, ac_u32_tflags);
监听串口
ac_serial_listen(int serial_num, ac_u32_tflags);
关闭串口
ac_serial_close(ac_ttystream_t*a_stream);
从串口读
ac_serial_read(ac_ttystream_t* s, void *buf, ac_u32_tlen,
ac_u32_t *out_actual);
往串口写
ac_serial_write(ac_ttystream_t* s,const void *buf,
ac_u32_t len, ac_u32_t *out_actual);

5.4 应用程序示例

为了让使用者能够对我们的这个小系统有更加深入的了解,我们制作了一些例子供使用者参考。这些例子包括最简单的"HelloWorld",串口读写、线程调度、线程间通信,哲学家算法等等。

应用程序的示例可以在examples目录下找到。


[目录]


第六章 结束语

《OSKit包装系统驱动程序 第六章》


OSKit包装系统驱动程序 第六章2000年12月12日23:02


作 者: 李凌


导师:陈朔鹰

第六章 结束语

通过对OSKit进行的分析,我对于OSKit所提供的OS Environment有了更加深刻的了解。OSEnvironment通过对驱动程序提供各种支持,使得其它系统的驱动程序可以在不修改源代码的前提下直接可以工作。

如果我们不想重新实现操作系统中最底层的和硬件直接打交道的部分,那么我们就可以直接去使用OS Environment,如果要重新实现那些部分,则只要按照OSEnvironment的思想和方法,去为驱动程序提供必要的支持就可以了。

OSKit不仅仅为我们提供了一套工具,更重要的是它为我们提供了一种思想,并且其中也有很多先进的理论。这会对开发操作系统的人有很大的帮助。

OSKit的本身,也并不是在各个方面都做得很出色,它也在不断地完善和发展之中。OSKit是一个自由软件,自由软件的精神是鼓励任何人去对它进行改进的,随着人们对于这个开发平台的了解和使用的增多,OSKit也将会被不断地完善。

在国外,已经有很多的研究操作系统的组织在使用OSKit,开发了著名的Java虚拟机kaffe的公司在OSKit的基础上实现了一个JavaOS,美国犹他大学也用OSKit开发了微内核操作系统Fluke。在国内,对OSKit的应用还并不是很多,我们希望我们小组的工作能够对国内的希望使用OSKit的研究人员和开发人员有所帮助。


后 记

以上就是我对我们的课题组所做的研究工作以及我个人工作的介绍。通过几个月以来我对OSKit的分析和研究,加深了对操作系统原理的认识,并对其实现有了具体的认识,提高了我的分析能力,丰富了实践经验。

我衷心地感谢我的导师陈朔鹰老师给予我的悉心指导,也感谢张丽芬以及赵小林老师也给予我们课题组以热心的帮助,并对901教研室的全体老师表示衷心的感谢。同时,我还要感谢我们课题组的其他成员韩斌、洪宁、汤海京对我的帮助以及他们的辛勤工作。最后,我也感谢我的很多同学和网友给予我的精神上的支持和鼓励。


参考文献


·OSKit文档:http://www.cs.utah.edu/flux/oskit/
·《操作系统的设计与实现》,北京理工大学出版社,张丽芬著
·《16位-32位微机组成原理》,中国科学技术大学出版社,史杏荣、杨寿保 编著
·《Linux设备驱动程序》,中国电力出版社,ALESSANDRO RUBINI著,LISOLEG译
·《Linux编程白皮书》,机械工业出版社,(美)David A.Rusling等著,朱珂等译
·《程序员经典参考手册》,学苑出版社,Peter Norton、Peter Aitken、Richard Wilton著,张大勇、吴正斌、吕杰
·《Linux编程24学时教程》,机械工业出版社 Warren W. Gay著,潇湘工作室译
·《计算机环境的可移植操作系统界面POSIX.1》,电子工业出版社,《COM原理与应用》,清华大学出版社,潘爱民著
·中软总公司第二开发部译
·《Linux操作系统内核分析》,人民邮电出版社,陈莉君编著
·《操作系统︰设计与实现(第二版)》,OPERATING SYSTEMS Design and Implementation(SecondEdition),电子工业出版社,ANDREW S.TANENBAUM,ALBERT S. WOODUHULL 著,王鹏,尤晋元,朱鹏,敖青云译校
·《UNIX操作系统设计与实现》电子工业出版社,李建国主编
·《UNIX操作系统设计》The Design of the UNIX Operation System,机械工业出版社,(美)Maurice J.Bsvh 著 王旭等译
·《实用UNIX编程》, 机械工业出版社,(美)Kay A. Robbins,Steven Robbins 著,刘宗田等译
·《Advance Programming in the UNIX Environment》,(美) W. Richard Stevens
·《Linux 上的C 编程》,中国电力出版社,怀石工作室编著
·《Linux从入门到精通》,电子工业出版社,(英)Phil Cornes 著,童寿彬等译,夏道藏审校
·《Linux 操作系统及实验教程》,机械工业出版社,李善平等编著
·《Linux 常用技术大全》,Linux Complete,电子工业出版社,(美) Grant Taylor 著,邱仲潘等译


[目录]


[ 本文件由良友·收藏家自动生成 ]