WCF基础教程——vs2013创建wcf应用程序
引言最近在项目中见到了师哥们经常谈到的WCF这方面的知识,当时在敲代码的时候也没有理解wcf到底是个什么东西?以及我们为什么在项目中会采用这样的框架来实现,只是按照师哥他们写好的代码编写相同格式的代码,随着代码敲的越来越多就慢慢的懂得了怎样实现客户端和服务端的连同,但是wcf真正的运行机制还是一点也不懂,最近在听师哥们讲课的时候有讲解了一些关于WCF的知识,自己感觉应该好好理解这方面的知识了,并且这个东西在项目中是非常常见的,下面就结合我实现的一个小Demo来和大家分享一下。其中一些最基本的概念,大家可以参考百度百科关于wcf的介绍,在这里给大家两张图对比一下,能很好的体现出wcf的用途:基于ASP.net的应用程序与面向服务开发在基于Asp.net 的应用程序开发中,我们由客户机的浏览器访问应用程序服务器,然后通过应用程序服务器中的数据库连接去连接数据库服务器,读取或是操作数据,有时候可能会多一个文件服务器。大家可以观察到,基本上所有的应用都放在了一台服务器上,但对于一个,由于业务上的需要(如:与外部系统交互),一台服务器很难支持所有的应用。下面来看这样一张图客户机使用浏览器访问服务器A,服务器A为了业务需要与其他各种应用部署在服务器B、C、D....再通过WCF技术互相通信,相互访问...然而面向服务的好处不仅仅在此,他还提供了不同语言不同操作系统的可交互性..下面我们就开始创建一个客户端和服务端分离的wcf应用程序: 1、创建我们的wcf的服务端——在新建项目的时候选择WCF服务应用程序2、从图中我们可以看出,在我们建立好的解决方案中有默认的两个service.svc和Iservice.cs文件,我们可以将这两 个文件删除,然后添加我们自己的wcf服务,步骤如图: 3、在解决方案里面我们可以看见,当我们添加了一个User.svc后,vs会自动生成WCF借口IUser.cs,这是我们需要在IUser.cs定义WCF方法ShowName,在User.svc.cs对该接口的方法进行实现。代码如下using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace DemoServiceWCF
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IUser”。
[ServiceContract]
public interface IUser
{
[OperationContract]
string ShowName(string name);
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace DemoServiceWCF
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码、svc 和配置文件中的类名“User”。
// 注意: 为了启动 WCF 测试客户端以测试此服务,请在解决方案资源管理器中选择 User .svc 或 User .svc.cs,然后开始调试。
public class User : IUser
{
public string ShowName(string name)
{
string wcfName = string.Format("WCF服务,显示姓名:{0}", name);
return wcfName;
}
}
}大家在wcf接口中看到与平时我们不同的就是它多了两个标签,其余的和我们正常的学习的接口是一样的,添加这两 个标签是依赖System.ServiceModel的引用。[ServiceContract],来说明接口是一个WCF的接口,如果不加的话,将不能被外部调用。[OperationContract],来说明该方法是一个WCF接口的方法,不加的话同上。 其实简单来说这两个标签就是区分普通接口和普通方法的一个标示,就像我们提高班的第十期和第十一期还有十二期一样,例如现在十期有一个叫张三的同学,十一期也有一个叫张三的同学,这样我们找人的时候必须得有这个不同的期号来区分。 4、下面来看一下运行的效果,将User.svc设为起始页后运行。这样我们的wcf服务端基本上就可以用了,剩下的就是将这个服务端发布然后部署到IIS上,就可以供客户端使用了,在这我就不给大家详解怎样在IIS上发布wcf,我们就使用一个简单的办法就是直接预览user.svc,如图:其实WCF的应用场景可以大概可以这样简单的描述一下:我们设计的场景是在生产中经常应用的场景,把WCF程序寄宿在IIS之上。假设场景如下:A服务器和B服务器。我们把我们刚刚建立的WCF程序“部署”在B服务器上(本教程的A,B服务器都放是我自己的一台机器),我们的目标是在A服务器的应用程序来访问B服务器的WCF程序,实现服务器端的应用程序通讯。 下面来看我们客户端的创建 1、首先我在客户端创建了一个web应用程序; 2、下面我们需要添加一个服务引用如图,填写好地址后点击转到按钮,然后点击确定 引用完成后,我们在解决方案中会发现多出一个文件:3、页面中的代码如下<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="User.aspx.cs" Inherits="DemoClientWCF.User" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:TextBox ID="txtName" runat="server"></asp:TextBox><br />
<asp:Button ID="btnSubmit" runat="server" Text="测试WCF服务" OnClick="btnClick" />
</form>
</body>
</html>using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using DemoClientWCF.ServiceReference;
namespace DemoClientWCF
{
public partial class User : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnClick(object sender, EventArgs e)
{
UserClient user = new UserClient();
string result = user.ShowName(this.txtName.Text);
Response.Write(result);
}
}
}4、下面给大家解释几个比较重要的代码1、using DemoClientWCF.ServiceReference;这个引用中 DemoClientWCF为我们客户端的命名空间,ServiceReference为我们在添加服务引用的时候,需要我们记住的那个名称。 2 UserClient user = new UserClient();UserClient就是我们在添加引用的时候生成的服务端User类的客户端代理类,一般客户端的代理类名称都会是***Client。其中User就是我们服务端添加的那个wcf服务(user.svc)的名称。到这我们就完成了一个很简单的WCF的小Demo,同时完成了由A服务器的应用小B服务器中WCF提供的方法的调用。小结这个小Demo算是我们学习WCF的最基础的入门,在我看来对于刚入门的读者们来说应该是很容易接受的,也算是完成了一个基础的教程,随着对wcf的更深入的学习,我会继续更新此类博客来和大家交流,如果此博客有什么不懂的地方可以留言交流。
程序员:一些该知道的英文缩写
【XML】可扩展标记语言 (Extensible Markup Language, XML) ,用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。【DOM】Document Object Model,文档对象模型,DOM可以以一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说,这是表示和处理一个HTML或XML文档的常用方法。有一点很重要,DOM的设计是以对象管理组织(OMG)的规约为基础的,因此可以用于任何编程语言。最初人们把它认为是一种让JavaScript在浏览器间可移植的方法,不过DOM的应用已经远远超出这个范围。Dom技术使得用户页面可以动态地变化,如可以动态地显示或隐藏一个元素,改变它们的属性,增加一个元素等,Dom技术使得页面的交互性大大地增强。【WSDL】Web Services Description Language的缩写,是一个用来描述Web服务和说明如何与Web服务通信的XML语言。为用户提供详细的接口说明书。【ATL】Active Template Library活动模板库,是一种微软程序库,支持利用C++语言编写ASP代码以及其它ActiveX程序。通过活动模板库,可以建立COM组件,然后通过ASP页面中的脚本对COM对象进行调用。这种COM组件可以包含属性页、对话框等等控件。【OO】OO(Object Oriented,面向对象)是当前计算机界关心的重点,它是90年代软件开发方法的主流。面向对象的概念和应用已超越了程序设计和软件开发,扩展到很宽的范围。如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。【PHP】PHP,是英文超级文本预处理语言Hypertext Preprocessor的缩写。PHP 是一种 HTML 内嵌式的语言,是一种在服务器端执行的嵌入HTML文档的脚本语言,语言的风格有类似于C语言,被广泛的运用。【OOD】面向对象设计(Object-Oriented Design,OOD)方法是OO方法中一个中间过渡环节。其主要作用是对OOA分析的结果作进一步的规范化整理,以便能够被OOP直接接受。【CSS】级联样式表(Cascading Style Sheet)简称“CSS”,通常又称为“风格样式表(Style Sheet)”,它是用来进行网页风格设计的。比如,如果想让链接字未点击时是蓝色的,当鼠标移上去后字变成红色的且有下划线,这就是一种风格。通过设立样式表,可以统一地控制HMTL中各标志的显示属性。级联样式表可以使人更能有效地控制网页外观。使用级联样式表,可以扩充精确指定网页元素位置,外观以及创建特殊效果的能力。【MFC】MFC(Microsoft Foundation Classes微软基础类库),是一个微软公司提供的类库(class libraries),以C++类的形式封装了Windows的API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。【WPF】WPF(Windows Presentation Foundation)是微软推出的基于Windows Vista的用户界面框架,属于.NET Framework 3.0的一部分。它提供了统一的编程模型、语言和框架,真正做到了分离界面设计人员与开发人员的工作;同时它提供了全新的多媒体交互用户图形界面。【COM】COM是Component Object Model (组件对象模型)的缩写。【WCF】Windows Communication Foundation(WCF)是由微软发展的一组数据通信的应用程序开发接口 可以翻译为Windows通讯接口,它是.NET框架的一部分,由 .NET Framework 3.0 开始引入,与 Windows Presentation Foundation及 Windows Workflow Foundation并行为新一代 Windows 操作系统以及 WinFX 的三个重大应用程序开发类库。【ASP】ASP是Active Server Page的缩写,意为“动态服务器页面”。ASP是微软公司开发的代替CGI脚本程序的一种应用,它可以与数据库和其它程序进行交互,是一种简单、方便的编程工具。ASP的网页文件的格式是。asp,现在常用于各种动态网站中。另外阿斯匹林、天门冬氨酸、阿里软件销售合作伙伴、美国武装系统暨程序公司等的缩写也都为ASP。【ASP.net】ASP 是一项微软公司的技术,是一种使嵌入网页中的脚本可由因特网服务器执行的服务器端脚本技术。 指 Active Server Pages(动态服务器页面) ,运行于 IIS 之中的程序 。【JVM】JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。编译虚拟机的指令集与编译微处理器的指令集非常类似。【EXE】EXE File(可执行程序),一种可在操作系统存储空间中浮动定位的可执行程序。MS-DOS和MS-WINDOWS下,此类文件扩展名为.exe 2.WINDOWS操作系统中的二进制可执行文件,可执行文件分两种一种是后辍名为.COM另一种就是.EXE 。【ADO】ADO (ActiveX Data Objects) 是一个用于存取数据源的COM组件。它提供了编程语言和统一数据访问方式OLE DB的一个中间层。允许开发人员编写访问数据的代码而不用关心数据库是如何实现的,而只用关心到数据库的连接。【AWS】AWS BPM业务流程管理开发平台是一个易于部署和使用的业务流程管理基础平台软件,AWS平台提供了从业务流程梳理、建模到运行、监控、优化的全周期管理和面向角色的BPM Total Solution。【CMMI】CMMI全称是Capability Maturity Model Integration, 即软件能力成熟度模型集成,是由美国国防部与卡内基-梅隆大学和美国国防工业协会共同开发和研制的,其目的是帮助软件企业对软件工程过程进行管理和改进,增强开发与改进能力,从而能按时地、不超预算地开发出高质量的软件。【PIN】PIN码(PIN1)-全称Personal Identification Number,就是SIM卡的个人识别密码。如果未经使用者修改,运营商设置的原始密码是1234或0000。如果启用了开机PIN码,那么每次开机后就要输入4位数PIN码,PIN码是可以修改的,用来保护自己的SIM卡不被他人使用。【OTP】OTP全称叫One-time Password,也称动态口令,是根据专门的算法每隔60秒生成一个与时间相关的、不可预测的随机数字组合,每个口令只能使用一次,每天可以产生43200个密码,其中被广泛关注的当属上海众人网络安全技术有限公司独立研发的手机动态口令。【WXHZ】无线数据分析【.NET】.NET 是 Microsoft XML Web services 平台。XML Web services 允许应用程序通过 Internet 进行通讯和共享数据,而不管所采用的是哪种操作系统、设备或编程语言。Microsoft .NET 平台提供创建 XML Web services 并将这些服务集成在一起之所需。对个人用户的好处是无缝的、吸引人的体验。【.NET Framework】NET Framework又称 .Net框架。是由微软开发,一个致力于敏捷软件开发(Agile software development)、快速应用开发(Rapid application development)、平台无关性和网络透明化的软件开发平台。.NET是微软为下一个十年对服务器和桌面型软件工程迈出的第一步。.NET包含许多有助于互联网和内部网应用迅捷开发的技术。.NET框架是微软公司继Windows DNA之后的新开发平台。.NET框架是以一种采用系统虚拟机运行的编程平台,以通用语言运行库(Common Language Runtime)为基础,支持多种语言(C#、VB、C++、Python等)的开发。NET也为应用程序接口(API)提供了新功能和开发工具。这些革新使得程序设计员可以同时进行Windows应用软件和网络应用软件以及组件和服务(web服务)的开发。.NET提供了一个新的反射性的且面向对象程序设计编程接口。.NET设计得足够通用化从而使许多不同高级语言都得以被汇集。.NET Framework中的所有语言都提供基类库(BCL)。【IIS】Internet Information Services(IIS,互联网信息服务),是由微软公司提供的基于运行Microsoft Windows的互联网基本服务。最初是Windows NT版本的可选包,随后内置在Windows 2000、Windows XP Professional和Windows Server 2003一起发行,但在Windows XP Home版本上并没有IIS。【CLR、CIL】CLR常用简写词语,CLR是公共语言运行时,Common Language Runtime和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。CIL通用中间语言(Common Intermediate Language,简称CIL)(曾经被称为微软中间语言或MSIL)是一种属于通用语言架构和.NET框架的低阶(lowest-level)的人类可读的编程语言。目标为.NET 框架的语言被编译成CIL,然后汇编成字节码。CIL类似一个面向对象的汇编语言,并且它是完全基于堆栈的。它运行在虚拟机上,其主要的语言有C#、Visual Basic .NET、C++/CLI以及 J#。 在.NET语言的测试版中,CIL原本叫做微软中间语言(MSIL)。由于C#和通用语言架构的标准化,字节码现在已经官方地成为了CIL。因此,CIL仍旧经常与MSIL相提并论,特别是那些.NET语言的老用户。【C#】C#(C Sharp)是微软(Microsoft)为.NET Framework量身订做的程序语言,C#拥有C/C++的强大功能以及Visual Basic简易使用的特性,是第一个组件导向(Component-oriented)的程序语言,和C++与Java一样亦为对象导向(object-oriented)程序语言。C sharp(音标 [∫a:p] )(又被简称为"C#")是微软公司在2000年6月发布的一种新的编程语言,并定于在微软职业开发者论坛(PDC)上登台亮相。C#是微软公司研究员Anders Hejlsberg的最新成果。C#看起来与Java有着惊人的相似;它包括了诸如单一继承、界面、与Java几乎同样的语法,和编译成中间代码再运行的过程。但是C#与Java有着明显的不同,它借鉴了Delphi的一个特点,与COM(组件对象模型)是直接集成的,而且它是微软公司.NET windows网络框架的主角。
操作系统实验一到实验九合集(哈工大李治军)(一)
操作系统实验作者寄语操作系统实验的学习是一个循序渐进的过程,初次看linux-0.11中的代码,看着满屏的汇编语言,确实头疼。但通过学习赵炯博士的Linux内核0.11完全注释,结合着王爽老师的汇编语言一书,我逐渐理解每段汇编语言的含义和作用。本文主要是通过对哈工大李治军配套实验的实现,着重解释每一段的汇编代码,使读者对实验的整体脉络有一个初步的认识,不再因为畏惧汇编而不放弃实验。本文只是抛砖引玉,希望读者可以深入研究我下文提供的参考资料,做到理论与实践兼具。参考资料视频:操作系统书籍:现代操作系统实验:操作系统原理与实践书籍:王道操作系统书籍:Linux内核完全注释书籍:汇编语言(第3版) 王爽著实验一 熟悉实验环境只是熟悉实验环境,我没有使用蓝桥云课的实验环境,而是通过阿里云服务器搭建了linux环境,有需要的可以看以下2篇文章阿里云服务器Ubuntu14.04(64位)安装图形化界面_leoabcd12的博客-CSDN博客阿里云ubuntu系统配置linux-0.11(哈工大 李治军)实验环境搭建_leoabcd12的博客-CSDN博客实验二 操作系统的引导Linux 0.11 文件夹中的 boot/bootsect.s、boot/setup.s 和 tools/build.c 是本实验会涉及到的源文件。它们的功能详见《Linux内核0.11完全注释》的 6.2、6.3 节和 16 章。汇编知识简要整理了一下这次实验所需的基础汇编知识,可以在下文阅读代码是碰到再回过头来看!int 0x10注意,这里ah要先有值,代表内部子程序的编号功能号 a h = 0 x 03 ah=0x03ah=0x03,作用是读取光标的位置输入:bh = 页号返回:ch = 扫描开始线;cl = 扫描结束线;dh = 行号;dl = 列号功能号 a h = 0 x 13 ah=0x13ah=0x13,作用是显示字符串输入:al = 放置光标的方式及规定属性,下文 al=1,表示目标字符串仅仅包含字符,属性在BL中包含,光标停在字符串结尾处;es:bp = 字符串起始位置;cx = 显示的字符串字符数;bh = 页号;bl = 字符属性,下文 bl = 07H,表示正常的黑底白字;dh = 行号;dl = 列号功能号 a h = 0 x 0 e ah=0x0eah=0x0e,作用是显示字符输入:al = 字符int 0x13在DOS等实模式操作系统下,调用INT 13h会跳转到计算机的ROM-BIOS代码中进行低级磁盘服务,对程序进行基于物理扇区的磁盘读写操作。功能号 a h = 0 x 02 ah=0x02ah=0x02,作用是读磁盘扇区到内存输入:寄存器含义ah读磁盘扇区到内存al需要读出的扇区数量ch磁道cl扇区dh磁头dl驱动器es:bx数据缓冲区的地址返回:ah = 出错码(00H表示无错,01H表示非法命令,02H表示地址目标未发现…);CF为进位标志位,如果没有出错 C F = 0 功能号 a h = 0 x 00,作用是磁盘系统复位输入:dl = 驱动器返回:如果操作成功———— C F = 0 CF=0CF=0,a h = 00 H ah=00Hah=00H这里我只挑了下文需要的介绍,更多内容可以参考这篇博客BIOS系统服务 —— 直接磁盘服务(int 0x13)int 0x15功能号 a h = 0 x 88 ah=0x88ah=0x88,作用是获取系统所含扩展内存大小输入:ah = 0x88返回:ax = 从0x100000(1M)处开始的拓展内存大小(KB)。若出错则CF置位,ax = 出错码。int 0x41在PC机中BIOS设定的中断向量表中int 0x41的中断向量位置 (4 ∗ 0 x 41 = 0 x 0000 : 0 x 0104 4*0x41 = 0x0000:0x01044∗0x41=0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于100%兼容的BIOS来说,这里存放着硬盘参数表阵列的首地址0xF000:0E401,第二个硬盘的基本参数表入口地址存于int 0x46中断向量位置处.每个硬盘参数表有16个字节大小.位移大小说明0x00字柱面数0x02字节磁头数………0x0E字节每磁道扇区数0x0F字节保留CF要了解CF,首先要知道寄存器中有一种特殊的寄存器————标志寄存器,其中存储的信息通常被称为程序状态字。以下简称为flag寄存器。flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。flag的1、3、5、12、13、14、15位在8086CPU中没有使用,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。CF就是flag的第0位————进位标志位。在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。jnc在 C F = 0 CF=0CF=0 的时候,进行跳转,即不进位则跳转,下文就是在读入没有出错时,跳转到ok_load_setupjl小于则跳转lds格式: LDS reg16,mem32其意义是同时给一个段寄存器和一个16位通用寄存器同时赋值举例:地址100H101H102H103H内容00H41H02H03HLDS AX,[100H]
! 结果:AX=4100H DS=0302H可以把上述代码理解为这样一个过程,但实际上不能这么写mov AX,[100H]
mov DS,[100H+2]即把低字(2B)置为偏移地址,高字(2B)置为段地址DF标志和串传送指令flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、di的增减。df=0:每次操作后si、di递增df=1:每次操作后si、di递减来看一个串传送指令格式:movsb功能:相当于执行了如下2步操作( ( e s ) ∗ 16 + ( d i ) ) = ( ( d s ) ∗ 16 + s i ) (如果df=0:(si)=(si)+1,(di)=(di)+1如果df=1:(si)=(si)-1,(di)=(di)-1可以看出,movsb的功能是将 d s : s i 指向的内存单元中的字节送入 e s : d i 中,然后根据标志寄存器df位的值,将si和di递增或递减。也可以传送一个字格式:movsw功能:相当于执行了如下2步操作( ( e s ) ∗ 16 + ( d i ) ) = ( ( d s ) ∗ 16 + s i ) 如果df=0:(si)=(si)+2,(di)=(di)+2如果df=1:(si)=(si)-2,(di)=(di)-2可以看出,movsw的功能是将 d s : s i 指向的内存单元中的字节送入 e s : d i 中,然后根据标志寄存器df位的值, 将si和di递增2或递减2。movsb和movsw进行的是串传送操作的一个步骤,一般配合rep使用格式如下:rep movsb用汇编语法描述:s:movsb
loop s可见rep的作用是根据cx的值,重复执行串传送指令。由于每执行一次movsb指令si和di都会递增或递减指向后面一个单元或前面一个单元,则 rep movsb就可以循环实现(cx)个字符的传送。call(1) 将当前IP或CS和IP压入栈中(2) 转移CPU执行“call 标号”时,相当于进行:push IP
jmp near ptr 标号retret指令用栈中的数据,修改IP的内容,从而实现近转移(1) ( I P ) = ( ( s s ) ∗ 16 + ( s p ) )(2) ( s p ) = ( s p ) + 2 CPU执行ret指令时,相当于进行:pop IP改写bootsect.s打开 bootsect.sLoading system ...就是开机时显示在屏幕上的字,共16字符,加上3个换行+回车,一共是24字符。我将要修改他为Hello OS world, my name is WCF,30字符,加上3个换行+回车,共36字符。所以图一代码修改为mov cx.#36。将 .org 508 修改为 .org 510,是因为这里不需要 root_dev: .word ROOT_DEV,为了保证 boot_flag 一定在引导扇区最后两个字节,所以要修改 .org。.org 510 表示下面语句从地址510(0x1FE)开始,用来强制要求boot_flag一定在引导扇区的最后2个字节中(第511和512字节)。完整的代码如下:entry _start
_start:
mov ah,#0x03 ! 设置功能号
xor bh,bh ! 将bh置0
int 0x10 ! 返回行号和列号,供显示串用
mov cx,#52 !要显示的字符串长度
mov bx,#0x0007 ! bh=0,bl=07(正常的黑底白字)
mov bp,#msg1 ! es:bp 要显示的字符串物理地址
mov ax,#0x07c0 ! 将es段寄存器置为#0x07c0
mov es,ax
mov ax,#0x1301 ! ah=13(设置功能号),al=01(目标字符串仅仅包含字符,属性在BL中包含,光标停在字符串结尾处)
int 0x10 ! 显示字符串
! 设置一个无限循环(纯粹为了能一直看到字符串显示)
inf_loop:
jmp inf_loop
! 字符串信息
msg1:
.byte 13,10 ! 换行+回车
.ascii "Welcome to the world without assembly language"
.byte 13,10,13,10 ! 换行+回车
! 将
.org 510
! 启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中
boot_flag:
.word 0xAA55Ubuntu 上先从终端进入 ~/oslab/linux-0.11/boot/目录执行下面两个命令编译和链接 bootsect.s:$ as86 -0 -a -o bootsect.o bootsect.s
$ ld86 -0 -s -o bootsect bootsect.o其中 bootsect.o 是中间文件。bootsect 是编译、链接后的目标文件。需要留意的文件是 bootsect 的文件大小是 544 字节,而引导程序必须要正好占用一个磁盘扇区,即 512 个字节。造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式,这样的可执行文件处理文本段、数据段等部分以外,还包括一个 Minix 可执行文件头部,它的结构如下:struct exec {
unsigned char a_magic[2]; //执行文件魔数
unsigned char a_flags;
unsigned char a_cpu; //CPU标识号
unsigned char a_hdrlen; //头部长度,32字节或48字节
unsigned char a_unused;
unsigned short a_version;
long a_text; long a_data; long a_bss; //代码段长度、数据段长度、堆长度
long a_entry; //执行入口地址
long a_total; //分配的内存总量
long a_syms; //符号表大小
};6 char(6 字节)+ 1 short(2 字节) + 6 long(24 字节)= 32,正好是 32 个字节,去掉这 32 个字节后就可以放入引导扇区了。对于上面的 Minix 可执行文件,其 a_magic[0]=0x01,a_magic[1]=0x03,a_flags=0x10(可执行文件),a_cpu=0x04(表示 Intel i8086/8088,如果是 0x17 则表示 Sun 公司的 SPARC),所以 bootsect 文件的头几个字节应该是 01 03 10 04。为了验证一下,Ubuntu 下用命令hexdump -C bootsect可以看到:去掉这 32 个字节的文件头部$ dd bs=1 if=bootsect of=Image skip=32生成的 Image 就是去掉文件头的 bootsect。去掉这 32 个字节后,将生成的文件拷贝到 linux-0.11 目录下,并一定要命名为“Image”(注意大小写)。然后就“run”吧!# 当前的工作路径为 /oslab/linux-0.11/boot/
# 将刚刚生成的 Image 复制到 linux-0.11 目录下
$ cp ./Image ../Image
# 执行 oslab 目录中的 run 脚本
$ ../../runbootsect.s读入setup.s首先编写一个 setup.s,该 setup.s 可以就直接拷贝前面的 bootsect.s(还需要简单的调整),然后将其中的显示的信息改为:“Now we are in SETUP”。和前面基本一样,就不注释了。entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs ! 这里的cs其实就是这段代码的段地址
mov es,ax
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg2:
.byte 13,10
.ascii "Now we are in SETUP"
.byte 13,10,13,10
.org 510
boot_flag:
.word 0xAA55接下来需要编写 bootsect.s 中载入 setup.s 的关键代码所有需要的功能在原版 bootsect.s 中都是存在的,我们要做的仅仅是将这些代码添加到新的 bootsect.s 中去。除了新增代码,我们还需要去掉在 bootsect.s 添加的无限循环。SETUOLEN=2 ! 读入的扇区数
SETUPSEG=0x07e0 ! setup代码的段地址
entry _start
_start:
mov ah,#0x03 ! 设置功能号
xor bh,bh ! 将bh置0
int 0x10 ! 返回行号和列号,供显示串用
mov cx,#52 !要显示的字符串长度
mov bx,#0x0007 ! bh=0,bl=07(正常的黑底白字)
mov bp,#msg1 ! es:bp 要显示的字符串物理地址
mov ax,#0x07c0 ! 将es段寄存器置为#0x07c0
mov es,ax
mov ax,#0x1301 ! ah=13(设置功能号),al=01(目标字符串仅仅包含字符,属性在BL中包含,光标停在字符串结尾处)
int 0x10 ! 显示字符串
! 将setup模块从磁盘的第二个扇区开始读到0x7e00
load_setup:
mov dx,#0x0000 ! 磁头=0;驱动器号=0
mov cx,#0x0002 ! 磁道=0;扇区=2
mov bx,#0x0200 ! 偏移地址
mov ax,#0x0200+SETUPLEN ! 设置功能号;需要读出的扇区数量
int 0x13 ! 读磁盘扇区到内存
jnc ok_load_setup ! CF=0(读入成功)跳转到ok_load_setup
mov dx,#0x0000 ! 如果读入失败,使用功能号ah=0x00————磁盘系统复位
mov ax,#0x0000
int 0x13
jmp load_setup ! 尝试重新读入
ok_load_setup:
jmpi 0,SETUPSEG ! 段间跳转指令,跳转到setup模块处(0x07e0:0000)
! 字符串信息
msg1:
.byte 13,10 ! 换行+回车
.ascii "Welcome to the world without assembly language"
.byte 13,10,13,10 ! 换行+回车
! 将
.org 510
! 启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中
boot_flag:
.word 0xAA55再次编译$ make BootImage有 Error!这是因为 make 根据 Makefile 的指引执行了 tools/build.c,它是为生成整个内核的镜像文件而设计的,没考虑我们只需要 bootsect.s 和 setup.s 的情况。它在向我们要 “系统” 的核心代码。为完成实验,接下来给它打个小补丁。cbuild.c 从命令行参数得到 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image。其中 system 是第三个参数(argv[3])。当 “make all” 或者 “makeall” 的时候,这个参数传过来的是正确的文件名,build.c 会打开它,将内容写入 Image。而 “make BootImage” 时,传过来的是字符串 “none”。所以,改造 build.c 的思路就是当 argv[3] 是"none"的时候,只写 bootsect 和 setup,忽略所有与 system 有关的工作,或者在该写 system 的位置都写上 “0”。修改工作主要集中在 build.c 的尾部,可长度以参考下面的方式,将圈起来的部分注释掉。重新编译$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../runsetup.s获取基本硬件参数这里把一些难以理解的代码单独列出来获得磁盘参数这里花了我很长时间,原因是概念没有搞清楚,我觉得老师在实验指导书上写的也不是很清楚,CSDN上都只是草草复制的代码,感觉他们可以压根没有理解这一段。先来回顾一下上文的一个概念:int 0x41在PC机中BIOS设定的中断向量表中int 0x41的中断向量位置 (4 ∗ 0 x 41 = 0 x 0000 : 0 x 0104 4*0x41 = 0x0000:0x01044∗0x41=0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于100%兼容的BIOS来说,这里存放着硬盘参数表阵列的首地址0xF000:0E401,第二个硬盘的基本参数表入口地址存于int 0x46中断向量位置处.每个硬盘参数表有16个字节大小.这段话是重点,我之前误理解为磁盘参数就存放在以0x0000:0x0104为首地址的单元中,总共占16个字节,但实际上,只存了4个字节,里面存放的是磁盘参数表的偏移地址和段地址,也就是上文所说这里存放着硬盘参数表阵列的首地址0xF000:0E401。lds si,[4*0x41]再看这行代码就可以理解了,这里是把0x0000:0x0104单元存放的值(表示硬盘参数表阵列的首地址的偏移地址)赋给si寄存器,把0x0000:0x0106单元存放的值(表示硬盘参数表阵列的首地址的段地址)赋给ds寄存器。参数以十六进制方式显示先说说浪费我很长时间的我的错误:我想的是一个ASCII码8位,为什么答案里是4位4位输出,这里是搞清楚显示的目的。显示的是存在内存单元里的16进制数,例如某个字(2个字节)中的数值为 019 A 019A019A,我所要显示的不是01和9A表示的ASCII码,而是显示019A本身,所以要4位4位显示。以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 4 位二进制数和 1 位十六进制数存在一一对应关系),显示时只需将原二进制数每 4 位划成一组,按组求对应的 ASCII 码送显示器即可。ASCII 码与十六进制数字的对应关系为:0x30 ~ 0x39 对应数字 0 ~ 9,0x41 ~ 0x46 对应数字 a ~ f。从数字 9 到 a,其 ASCII 码间隔了 7h,这一点在转换时要特别注意。为使一个十六进制数能按高位到低位依次显示,实际编程中,需对 bx 中的数每次循环左移一组(4 位二进制),然后屏蔽掉当前高 12 位,对当前余下的 4 位(即 1 位十六进制数)求其 ASCII 码,要判断它是 0 ~ 9 还是 a ~ f,是前者则加 0x30 得对应的 ASCII 码,后者则要加 0x37 才行,最后送显示器输出。以上步骤重复 4 次,就可以完成 bx 中数以 4 位十六进制的形式显示出来。下面是提供的参考代码INITSEG = 0x9000 ! 参数存放位置的段地址
entry _start
_start:
! 打印 "NOW we are in SETUP"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs
mov es,ax
mov ax,#0x1301
int 0x10
! 获取光标位置
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10 ! 返回:dh = 行号;dl = 列号
mov [0],dx ! 存储到内存0x9000:0处
! 获取内存大小
mov ah,#0x88
int 0x15 ! 返回:ax = 从0x100000(1M)处开始的扩展内存大小(KB)
mov [2],ax ! 将扩展内存数值存放在0x90002处(1个字)
! 读第一个磁盘参数表复制到0x90004处
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41] ! 把低字(2B)置为偏移地址,高字(2B)置为段地址
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10 ! 重复16次,即传送16B
rep
movsb ! 按字节传送
! 打印前的准备
mov ax,cs
mov es,ax
mov ax,#INITSEG
mov ds,ax
! 打印"Cursor position:"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#18
mov bx,#0x0007
mov bp,#msg_cursor
mov ax,#0x1301
int 0x10
! 打印光标位置
mov dx,[0]
call print_hex
! 打印"Memory Size:"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#14
mov bx,#0x0007
mov bp,#msg_memory
mov ax,#0x1301
int 0x10
! 打印内存大小
mov dx,[2]
call print_hex
! 打印"KB"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#2
mov bx,#0x0007
mov bp,#msg_kb
mov ax,#0x1301
int 0x10
! 打印"Cyls:"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#7
mov bx,#0x0007
mov bp,#msg_cyles
mov ax,#0x1301
int 0x10
! 打印柱面数
mov dx,[4]
call print_hex
! 打印"Heads:"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#8
mov bx,#0x0007
mov bp,#msg_heads
mov ax,#0x1301
int 0x10
! 打印磁头数
mov dx,[6]
call print_hex
! 打印"Sectors:"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#10
mov bx,#0x0007
mov bp,#msg_sectors
mov ax,#0x1301
int 0x10
mov dx,[18]
call print_hex
inf_loop:
jmp inf_loop
! 上面的call都转到这里
print_hex:
mov cx,#4 ! dx(16位)可以显示4个十六进制数字
print_digit:
rol dx,#4 ! 取 dx 的高4比特移到低4比特处
mov ax,#0xe0f ! ah = 请求的功能值(显示单个字符),al = 半字节(4个比特)掩码
and al,dl ! 前4位会被置为0
add al,#0x30 ! 给 al 数字加上十六进制 0x30
cmp al,#0x3a ! 比较看是否大于数字十
jl outp ! 是一个不大于十的数字则跳转
add al,#0x07 ! 否则就是a~f,要多加7
outp:
int 0x10 ! 显示单个字符
loop print_digit ! 重复4次
ret
! 打印换行回车
print_nl:
mov ax,#0xe0d ! CR
int 0x10
mov al,#0xa ! LF
int 0x10
ret
msg2:
.byte 13,10
.ascii "NOW we are in SETUP"
.byte 13,10,13,10
msg_cursor:
.byte 13,10
.ascii "Cursor position:"
msg_memory:
.byte 13,10
.ascii "Memory Size:"
msg_cyles:
.byte 13,10
.ascii "Cyls:"
msg_heads:
.byte 13,10
.ascii "Heads:"
msg_sectors:
.byte 13,10
.ascii "Sectors:"
msg_kb:
.ascii "KB"
.org 510
boot_flag:
.word 0xAA55经过漫长的调试,得到如下结果Memory Size 是 0x3C00KB,算一算刚好是 15MB(扩展内存),加上 1MB 正好是 16MB,看看 Bochs 配置文件 bochs/bochsrc.bxrc:这些都和上面打出的参数吻合,表示此次实验是成功的。天道酬勤实验二总共花费25小时,因为没有汇编基础,花费了大量时间在理解代码上,希望下面的实验可以越做越快吧。实验三 系统调用提醒这次实验涉及的宏过于复杂,加上本人能力有限,我也没有花大量时间去研究每一段代码,只是理解到每一段代码做了什么这一程度。实验目的此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。1.iam()第一个系统调用是 iam(),其原型为:int iam(const char * name);完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。2.whoami()第二个系统调用是 whoami(),其原型为:int whoami(char* name, unsigned int size);它将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)。返回值是拷贝的字符数。如果 size 小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。应用程序如何调用系统调用在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:把系统调用的编号存入 EAX;把函数参数存入其它通用寄存器;触发 0x80 号中断(int 0x80)。linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。我们不妨看看 lib/close.c,研究一下 close() 的 API:#define __LIBRARY__
#include <unistd.h>
_syscall1(int, close, int, fd)其中 _syscall1 是一个宏,在 include/unistd.h 中定义。#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}将 _syscall1(int,close,int,fd) 进行宏展开,可以得到:int close(int fd)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}这就是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。其中 __NR_close 就是系统调用的编号,在 include/unistd.h 中定义:#define __NR_close 6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/而在应用程序中,要有:
*/
/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__
/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"
/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);
/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami 和 __NR_iam 两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。从“int 0x80”进入内核函数int 0x80 触发后,接下来就是内核的中断处理了。先了解一下 0.11 处理 0x80 号中断的过程。在内核初始化时,主函数在 init/main.c 中,调用了 sched_init() 初始化函数:void main(void)
{
// ……
time_init();
sched_init();
buffer_init(buffer_memory_end);
// ……
}sched_init() 在 kernel/sched.c 中定义为:void sched_init(void)
{
// ……
set_system_gate(0x80,&system_call);
}set_system_gate 是个宏,在 include/asm/system.h 中定义为:#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)_set_gate 的定义是:#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。接下来看 system_call。该函数纯汇编打造,定义在 kernel/system_call.s 中:!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……
.globl system_call
.align 2
system_call:
! # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
! # push %ebx,%ecx,%edx,是传递给系统调用的参数
pushl %ebx
! # 让ds, es指向GDT,内核地址空间
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax)
jne reschedule
cmpl $0,counter(%eax)
je reschedulesystem_call 用 .globl 修饰为其他函数可见。call sys_call_table(,%eax,4) 之前是一些压栈保护,修改段选择子为内核段,call sys_call_table(,%eax,4) 之后是看看是否需要重新调度,这些都与本实验没有直接关系,此处只关心 call sys_call_table(,%eax,4) 这一句。根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx。显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iam 和 sys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。同时还要仿照此文件中前面各个系统调用的写法,加上:extern int sys_whoami();
extern int sys_iam();不然,编译会出错的。实现 sys_iam() 和 sys_whoami()添加系统调用的最后一步,是在内核中实现函数 sys_iam() 和 sys_whoami()。每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。比如在 fs/open.c 中的 sys_close(int fd):int sys_close(unsigned int fd)
{
// ……
return (0);
}它没有什么特别的,都是实实在在地做 close() 该做的事情。所以只要自己创建一个文件:kernel/who.c,然后实现两个函数就万事大吉了。按照上述逻辑修改相应文件通过上文描述,我们已经理清楚了要修改的地方在哪里1.添加iam和whoami系统调用编号的宏定义(_NR_xxxxxx),文件:include/unistd.h2.修改系统调用总数, 文件:kernel/system_call.s3.为新增的系统调用添加系统调用名并维护系统调用表,文件:include/linux/sys.h4.为新增的系统调用编写代码实现,在linux-0.11/kernel目录下,创建一个文件 who.c#include <asm/segment.h>
#include <errno.h>
#include <string.h>
char _myname[24];
int sys_iam(const char *name)
{
char str[25];
int i = 0;
do
{
// get char from user input
str[i] = get_fs_byte(name + i);
} while (i <= 25 && str[i++] != '\0');
if (i > 24)
{
errno = EINVAL;
i = -1;
}
else
{
// copy from user mode to kernel mode
strcpy(_myname, str);
}
return i;
}
int sys_whoami(char *name, unsigned int size)
{
int length = strlen(_myname);
printk("%s\n", _myname);
if (size < length)
{
errno = EINVAL;
length = -1;
}
else
{
int i = 0;
for (i = 0; i < length; i++)
{
// copy from kernel mode to user mode
put_fs_byte(_myname[i], name + i);
}
}
return length;
}修改 Makefile要想让我们添加的 kernel/who.c 可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 就可以编译整个代码树,是因为 make 完全按照 Makefile 里的指示工作。Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile。需要修改两处。(1)第一处OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o改为:OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o who.o添加了 who.o。(2)第二处### Dependencies:
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h改为:### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h添加了 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h。Makefile 修改后,和往常一样 make all 就能自动把 who.c 加入到内核中了。编写测试程序到此为止,内核中需要修改的部分已经完成,接下来需要编写测试程序来验证新增的系统调用是否已经被编译到linux-0.11内核可供调用。首先在oslab目录下编写iam.c,whoami.c/* iam.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);
int main(int argc, char *argv[])
{
/*调用系统调用iam()*/
iam(argv[1]);
return 0;
}/* whoami.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
#include <stdio.h>
_syscall2(int, whoami,char *,name,unsigned int,size);
int main(int argc, char *argv[])
{
char username[64] = {0};
/*调用系统调用whoami()*/
whoami(username, 24);
printf("%s\n", username);
return 0;
}以上两个文件需要放到启动后的linux-0.11操作系统上运行,验证新增的系统调用是否有效,那如何才能将这两个文件从宿主机转到稍后虚拟机中启动的linux-0.11操作系统上呢?这里我们采用挂载方式实现宿主机与虚拟机操作系统的文件共享,在 oslab 目录下执行以下命令挂载hdc目录到虚拟机操作系统上。sudo ./mount-hdc 再通过以下命令将上述两个文件拷贝到虚拟机linux-0.11操作系统/usr/root/目录下,命令在oslab/目录下执行:cp iam.c whoami.c hdc/usr/root如果目标目录下存在对应的两个文件则可启动虚拟机进行测试了。编译[/usr/root]# gcc -o iam iam.c
[/usr/root]# gcc -o whoami whoami.c运行测试[/usr/root]# ./iam wcf
[/usr/root]# ./whoami命令执行后,很可能会报以下错误:这代表虚拟机操作系统中/usr/include/unistd.h文件中没有新增的系统调用调用号为新增系统调用设置调用号#define __NR_whoami 72
#define __NR_iam 73再次执行:实验成功为什么这里会打印2次?因为在系统内核中执行了 printk() 函数,在用户模式下又执行了一次 printf() 函数。要知道到,printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()。printk() 和 printf() 的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。天道酬勤实验三总共花费7小时,看的不是特别仔细,没有特别深入的学习宏展开和内联汇编。但基本理解了系统调用的目的和方式,Linus永远的神!
操作系统实验一到实验九合集(哈工大李治军)(二)
实验四 进程运行轨迹的跟踪与统计实验目的掌握 Linux 下的多进程编程技术;通过对进程运行轨迹的跟踪来形象化进程的概念;在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验。实验内容进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出 CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……本次实验包括如下内容:基于模板 process.c 编写多进程的样本程序,实现如下功能: + 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒; + 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;在 Linux0.11 上实现进程运行轨迹的跟踪。 + 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序进行统计。修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。/var/process.log 文件的格式必须为:pid X time其中:pid 是进程的 ID;X 可以是 N、J、R、W 和 E 中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(W) 和退出(E);time 表示 X 发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick);三个字段之间用制表符分隔。例如:12 N 1056
12 J 1057
4 W 1057
12 R 1057
13 N 1058
13 J 1059
14 N 1059
14 J 1060
15 N 1060
15 J 1061
12 W 1061
15 R 1061
15 J 1076
14 R 1076
14 E 1076
......编写process.c文件提示在 Ubuntu 下,top 命令可以监视即时的进程状态。在 top 中,按 u,再输入你的用户名,可以限定只显示以你的身份运行的进程,更方便观察。按 h 可得到帮助。在 Ubuntu 下,ps 命令可以显示当时各个进程的状态。ps aux 会显示所有进程;ps aux | grep xxxx 将只显示名为 xxxx 的进程。更详细的用法请问 man。在 Linux 0.11 下,按 F1 可以即时显示当前所有进程的状态。文件主要作用process.c文件主要实现了一个函数:/*
* 此函数按照参数占用CPU和I/O时间
* last: 函数实际占用CPU和I/O的总时间,不含在就绪队列中的时间,>=0是必须的
* cpu_time: 一次连续占用CPU的时间,>=0是必须的
* io_time: 一次I/O消耗的时间,>=0是必须的
* 如果last > cpu_time + io_time,则往复多次占用CPU和I/O,直到总运行时间超过last为止
* 所有时间的单位为秒
*/
cpuio_bound(int last, int cpu_time, int io_time);下面是 4 个使用的例子:// 比如一个进程如果要占用10秒的CPU时间,它可以调用:
cpuio_bound(10, 1, 0);
// 只要cpu_time>0,io_time=0,效果相同
// 以I/O为主要任务:
cpuio_bound(10, 0, 1);
// 只要cpu_time=0,io_time>0,效果相同
// CPU和I/O各1秒钟轮回:
cpuio_bound(10, 1, 1);
// 较多的I/O,较少的CPU:
// I/O时间是CPU时间的9倍
cpuio_bound(10, 1, 9);修改此模板,用 fork() 建立若干个同时运行的子进程,父进程等待所有子进程退出后才退出,每个子进程按照你的意愿做不同或相同的 cpuio_bound(),从而完成一个个性化的样本程序。它可以用来检验有关 log 文件的修改是否正确,同时还是数据统计工作的基础。wait() 系统调用可以让父进程等待子进程的退出。关键函数解释fork()这里摘录某位博主的详解操作系统之 fork() 函数详解 - 简书 (jianshu.com)fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函数返回的值
int count=0;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d/n",getpid());
printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
count++;
}
else {
printf("i am the parent process, my process id is %d/n",getpid());
printf("我是孩子他爹/n");
count++;
}
printf("统计结果是: %d/n",count);
return 0;
}运行结果:i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:1)在父进程中,fork返回新创建子进程的进程ID;2)在子进程中,fork返回0;3)如果出现错误,fork返回一个负值;在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0. fork出错可能有两种原因: 1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。 2)系统内存不足,这时errno的值被设置为ENOMEM。 创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。 每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。 fork执行完毕后,出现两个进程有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。 执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。 还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。struct tms 结构体struct tms 结构体定义在 <sys/times.h> 头文件里,具体定义如下:引用
/* Structure describing CPU time used by a process and its children. */
struct tms
{
clock_t tms_utime ; /* User CPU time. 用户程序 CPU 时间*/
clock_t tms_stime ; /* System CPU time. 系统调用所耗费的 CPU 时间 */
clock_t tms_cutime ; /* User CPU time of dead children. 已死掉子进程的 CPU 时间*/
clock_t tms_cstime ; /* System CPU time of dead children. 已死掉子进程所耗费的系统调用 CPU 时间*/
};用户CPU时间和系统CPU时间之和为CPU时间,即命令占用CPU执行的时间总和。实际时间要大于CPU时间,因为Linux是多任务操作系统,往往在执行一条命令时,系统还要处理其他任务。另一个需要注意的问题是即使每次执行相同的命令,所花费的时间也不一定相同,因为其花费的时间与系统运行相关。数据类型 clock_t关于该数据类型的定义如下:#ifndef _CLOCK_T_DEFINED
typedef long clock_t;
#define _CLOCK_T_DEFINED
#endifclock_t 是一个长整型数。在 time.h 文件中,还定义了一个常量 CLOCKS_PER_SEC ,它用来表示一秒钟会有多少个时钟计时单元,其定义如下:#define CLOCKS_PER_SEC ((clock_t)1000)下文就模拟cpu操作,定义 H Z = 100 HZ=100HZ=100,内核的标准时间是jiffy,一个jiffy就是一个内部时钟周期,而内部时钟周期是由100HZ的频率所产生中的,也就是一个时钟滴答,间隔时间是10毫秒(ms).计算出来的时间也并非真实时间,而是时钟滴答次数,乘以10ms可以得到真正的时间。代码实现下面给出代码:#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
pid_t n_proc[10]; /*10个子进程 PID*/
int i;
for(i=0;i<10;i++)
{
n_proc[i] = fork();
/*子进程*/
if(n_proc[i] == 0)
{
cpuio_bound(20,2*i,20-2*i); /*每个子进程都占用20s*/
return 0; /*执行完cpuio_bound 以后,结束该子进程*/
}
/*fork 失败*/
else if(n_proc[i] < 0 )
{
printf("Failed to fork child process %d!\n",i+1);
return -1;
}
/*父进程继续fork*/
}
/*打印所有子进程PID*/
for(i=0;i<10;i++)
printf("Child PID: %d\n",n_proc[i]);
/*等待所有子进程完成*/
wait(&i); /*Linux 0.11 上 gcc要求必须有一个参数, gcc3.4+则不需要*/
return 0;
}
/*
* 此函数按照参数占用CPU和I/O时间
* last: 函数实际占用CPU和I/O的总时间,不含在就绪队列中的时间,>=0是必须的
* cpu_time: 一次连续占用CPU的时间,>=0是必须的
* io_time: 一次I/O消耗的时间,>=0是必须的
* 如果last > cpu_time + io_time,则往复多次占用CPU和I/O
* 所有时间的单位为秒
*/
void cpuio_bound(int last, int cpu_time, int io_time)
{
struct tms start_time, current_time;
clock_t utime, stime;
int sleep_time;
while (last > 0)
{
/* CPU Burst */
times(&start_time);
/* 其实只有t.tms_utime才是真正的CPU时间。但我们是在模拟一个
* 只在用户状态运行的CPU大户,就像“for(;;);”。所以把t.tms_stime
* 加上很合理。*/
do
{
times(&current_time);
utime = current_time.tms_utime - start_time.tms_utime;
stime = current_time.tms_stime - start_time.tms_stime;
} while ( ( (utime + stime) / HZ ) < cpu_time );
last -= cpu_time;
if (last <= 0 )
break;
/* IO Burst */
/* 用sleep(1)模拟1秒钟的I/O操作 */
sleep_time=0;
while (sleep_time < io_time)
{
sleep(1);
sleep_time++;
}
last -= sleep_time;
}
}答疑❓ 为什么说它可以用来检验有关 log 文件的修改是否正确,同时还是数据统计工作的基础?每个子进程都通过 cpuio_bound 函数实现了占用CPU和I/O时间的操作,并且可以精确的知道每个操作的时间。所以下面的 log 文件(日志文件)正确与否可以借此推算。尽早打开log文件操作系统启动后先要打开 /var/process.log,然后在每个进程发生状态切换的时候向 log 文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。为了能尽早开始记录,应当在内核启动时就打开 log 文件。内核的入口是 init/main.c 中的 main(),其中一段代码是://……
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
//……这段代码在进程 0 中运行,先切换到用户模式,然后全系统第一次调用 fork() 建立进程 1。进程 1 调用 init()。在 init()中:// ……
//加载文件系统
setup((void *) &drive_info);
// 打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);
// 让文件描述符1也和/dev/tty0关联
(void) dup(0);
// 让文件描述符2也和/dev/tty0关联
(void) dup(0);
// ……这段代码建立了文件描述符 0、1 和 2,它们分别就是 stdin、stdout 和 stderr。这三者的值是系统标准(Windows 也是如此),不可改变。可以把 log 文件的描述符关联到 3。文件系统初始化,描述符 0、1 和 2 关联之后,才能打开 log 文件,开始记录进程的运行轨迹。为了能尽早访问 log 文件,我们要让上述工作在进程 0 中就完成。所以把这一段代码从 init() 移动到 main() 中,放在 move_to_user_mode() 之后(不能再靠前了),同时加上打开 log 文件的代码。修改后的 main() 如下://……
move_to_user_mode();
/***************添加开始***************/
setup((void *) &drive_info);
// 建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);
//文件描述符1也和/dev/tty0关联
(void) dup(0);
// 文件描述符2也和/dev/tty0关联
(void) dup(0);
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
/***************添加结束***************/
if (!fork()) { /* we count on this going ok */
init();
}
//……打开 log 文件的参数的含义是建立只写文件,如果文件已存在则清空已有内容。文件的权限是所有人可读可写。这样,文件描述符 0、1、2 和 3 就在进程 0 中建立了。根据 fork() 的原理,进程 1 会继承这些文件描述符,所以 init() 中就不必再 open() 它们。此后所有新建的进程都是进程 1 的子孙,也会继承它们。但实际上,init() 的后续代码和 /bin/sh 都会重新初始化它们。所以只有进程 0 和进程 1 的文件描述符肯定关联着 log 文件,这一点在接下来的写 log 中很重要。小结其实就是为了尽早打开log日志文件开始记录,那必须满足在用户模式且可以进行文件读写,因此最前的位置只能在 move_to_user_mode() 之后(不能再靠前了),并且建立文件描述符 0、1 和 2,它们分别就是 stdin、stdout 和 stderr。编写fprintk()函数log 文件将被用来记录进程的状态转移轨迹。所有的状态转移都是在内核进行的。在内核状态下,write() 功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf(),只能调用 printk()。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk() 和 sys_write() 而写成的:#include "linux/sched.h"
#include "sys/stat.h"
static char logbuf[1024];
int fprintk(int fd, const char *fmt, ...)
{
va_list args;
int count;
struct file * file;
struct m_inode * inode;
va_start(args, fmt);
count=vsprintf(logbuf, fmt, args);
va_end(args);
/* 如果输出到stdout或stderr,直接调用sys_write即可 */
if (fd < 3)
{
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
/* 注意对于Windows环境来说,是_logbuf,下同 */
"pushl $logbuf\n\t"
"pushl %1\n\t"
/* 注意对于Windows环境来说,是_sys_write,下同 */
"call sys_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (count),"r" (fd):"ax","cx","dx");
}
else
/* 假定>=3的描述符都与文件关联。事实上,还存在很多其它情况,这里并没有考虑。*/
{
/* 从进程0的文件描述符表中得到文件句柄 */
if (!(file=task[0]->filp[fd]))
return 0;
inode=file->f_inode;
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $logbuf\n\t"
"pushl %1\n\t"
"pushl %2\n\t"
"call file_write\n\t"
"addl $12,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (count),"r" (file),"r" (inode):"ax","cx","dx");
}
return count;
}因为和 printk 的功能近似,建议将此函数放入到 kernel/printk.c 中。fprintk() 的使用方式类同与 C 标准库函数 fprintf(),唯一的区别是第一个参数是文件描述符,而不是文件指针。例如:// 向stdout打印正在运行的进程的ID
fprintk(1, "The ID of running process is %ld", current->pid);
// 向log文件输出跟踪进程运行轨迹
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies);jiffies,滴答jiffies 在 kernel/sched.c 文件中定义为一个全局变量:long volatile jiffies=0;它记录了从开机到当前时间的时钟中断发生次数。在 kernel/sched.c 文件中的 sched_init() 函数中,时钟中断处理函数被设置为:set_intr_gate(0x20,&timer_interrupt);而在 kernel/system_call.s 文件中将 timer_interrupt 定义为:timer_interrupt:
! ……
! 增加jiffies计数值
incl jiffies
! ……这说明 jiffies 表示从开机时到现在发生的时钟中断次数,这个数也被称为 “滴答数”。另外,在 kernel/sched.c 中的 sched_init() 中有下面的代码:// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);这三条语句用来设置每次时钟中断的间隔,即为 LATCH,而 LATCH 是定义在文件 kernel/sched.c 中的一个宏:// 在 kernel/sched.c 中
#define LATCH (1193180/HZ)
// 在 include/linux/sched.h 中
#define HZ 100再加上 PC 机 8253 定时芯片的输入时钟频率为 1.193180MHz,即 1193180/每秒,LATCH=1193180/100,时钟每跳 11931.8 下产生一次时钟中断,即每 1/100 秒(10ms)产生一次时钟中断,所以 jiffies 实际上记录了从开机以来共经过了多少个 10ms。注意这里是 H Z = 100 HZ=100HZ=100 的情况,前文也介绍过。所以时间其实就是近似等于中断次数乘以 1 / H Z 1/HZ1/HZ寻找状态切换点必须找到所有发生进程状态切换的代码点,并在这些点添加适当的代码,来输出进程状态变化的情况到 log 文件中。此处要面对的情况比较复杂,需要对 kernel 下的 fork.c、sched.c 有通盘的了解,而 exit.c 也会涉及到。例子 1:记录一个进程生命期的开始第一个例子是看看如何记录一个进程生命期的开始,当然这个事件就是进程的创建函数 fork(),由《系统调用》实验可知,fork() 功能在内核中实现为 sys_fork(),该“函数”在文件 kernel/system_call.s 中实现为:sys_fork:
call find_empty_process
! ……
! 传递一些参数
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
! 调用 copy_process 实现进程创建
call copy_process
addl $20,%esp所以真正实现进程创建的函数是 copy_process(),它在 kernel/fork.c 中定义为:int copy_process(int nr,……)
{
struct task_struct *p;
// ……
// 获得一个 task_struct 结构体空间
p = (struct task_struct *) get_free_page();
// ……
p->pid = last_pid;
// ……
// 设置 start_time 为 jiffies
p->start_time = jiffies;
// ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
TASK_RUNNING(0),被全局变量 current 指向的
是正在运行的进程。*/
p->state = TASK_RUNNING;
return last_pid;
}因此要完成进程运行轨迹的记录就要在 copy_process() 中添加输出语句。这里要输出两种状态,分别是“N(新建)”和“J(就绪)”。例子 2:记录进入睡眠态的时间第二个例子是记录进入睡眠态的时间。sleep_on() 和 interruptible_sleep_on() 让当前进程进入睡眠状态,这两个函数在 kernel/sched.c 文件中定义如下:void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
// ……
tmp = *p;
// 仔细阅读,实际上是将 current 插入“等待队列”头部,tmp 是原来的头部
*p = current;
// 切换到睡眠态
current->state = TASK_UNINTERRUPTIBLE;
// 让出 CPU
schedule();
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
// 在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!!
if (tmp)
tmp->state=0;
}
/* TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的区别在于不可中断的睡眠
* 只能由wake_up()显式唤醒,再由上面的 schedule()语句后的
*
* if (tmp) tmp->state=0;
*
* 依次唤醒,所以不可中断的睡眠进程一定是按严格从“队列”(一个依靠
* 放在进程内核栈中的指针变量tmp维护的队列)的首部进行唤醒。而对于可
* 中断的进程,除了用wake_up唤醒以外,也可以用信号(给进程发送一个信
* 号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合
* 适的时候处理这一位。感兴趣的实验者可以阅读有关代码)来唤醒,如在
* schedule()中:
*
* for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
* if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
* (*p)->state==TASK_INTERRUPTIBLE)
* (*p)->state=TASK_RUNNING;//唤醒
*
* 就是当进程是可中断睡眠时,如果遇到一些信号就将其唤醒。这样的唤醒会
* 出现一个问题,那就是可能会唤醒等待队列中间的某个进程,此时这个链就
* 需要进行适当调整。interruptible_sleep_on和sleep_on函数的主要区别就
* 在这里。
*/
void interruptible_sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
…
tmp=*p;
*p=current;
repeat: current->state = TASK_INTERRUPTIBLE;
schedule();
// 如果队列头进程和刚唤醒的进程 current 不是一个,
// 说明从队列中间唤醒了一个进程,需要处理
if (*p && *p != current) {
// 将队列头唤醒,并通过 goto repeat 让自己再去睡眠
(**p).state=0;
goto repeat;
}
*p=NULL;
//作用和 sleep_on 函数中的一样
if (tmp)
tmp->state=0;
}总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。修改fork.c文件fork.c文件在kernel目录下,这里要输出两种状态,分别是“N(新建)”和“J(就绪)”,下面做出两处修改:int copy_process(int nr,……)
{
struct task_struct *p;
// ……
// 获得一个 task_struct 结构体空间
p = (struct task_struct *) get_free_page();
// ……
p->pid = last_pid;
// ……
// 设置 start_time 为 jiffies
p->start_time = jiffies;
//新增修改,新建进程
fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);
// ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
TASK_RUNNING(0),被全局变量 current 指向的
是正在运行的进程。*/
p->state = TASK_RUNNING;
//新增修改,进程就绪
fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
return last_pid;
}修改sched.c文件文件位置:kernel/sched.c修改schedule函数//这里仅仅说一下改动了什么
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE){
(*p)->state=TASK_RUNNING;
fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
}
}
while (1) {
c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS];
// 找到 counter 值最大的就绪态进程
while (--i) {
if (!*--p) continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
// 如果有 counter 值大于 0 的就绪态进程,则退出
if (c) break;
// 如果没有:
// 所有进程的 counter 值除以 2 衰减后再和 priority 值相加,
// 产生新的时间片
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
//切换到相同的进程不输出
if(current->pid != task[next] ->pid)
{
/*新建修改--时间片到时程序 => 就绪*/
if(current->state == TASK_RUNNING)
fprintk(3,"%d\tJ\t%d\n",current->pid,jiffies);
fprintk(3,"%d\tR\t%d\n",task[next]->pid,jiffies);
}
// 切换到 next 进程
switch_to(next);修改sys_pause函数int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
/*
*修改--当前进程 运行 => 可中断睡眠
*/
if(current->pid != 0)
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
return 0;
}修改sleep_on函数void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
/*
*修改--当前进程进程 => 不可中断睡眠
*/
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (tmp)
{
tmp->state=0;
/*
*修改--原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
}
}修改interruptible_sleep_on函数void interruptible_sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp=*p;
*p=current;
repeat: current->state = TASK_INTERRUPTIBLE;
/*
*修改--唤醒队列中间进程,过程中使用Wait
*/
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (*p && *p != current) {
(**p).state=0;
/*
*修改--当前进程 => 可中断睡眠
*/
fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
goto repeat;
}
*p=NULL;
if (tmp)
{
tmp->state=0;
/*
*修改--原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
}
}修改wake_up函数void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
/*
*修改--唤醒 最后进入等待序列的 进程
*/
fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
*p=NULL;
}
}修改exit.c文件当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占用的系统资源。这包括进程运行时打开的文件、申请的内存等。当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit()。该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的i节点进行同步操作。如果进程有子进程,则让init进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端(如果按照实验的数据,此时就应该打印了),并向属于该会话的所有进程发送挂断信号 SIGHUP,这通常会终止该会话中的所有进程。然后把进程状态置为僵死状态 TASK_ZOMBIE。并向其原父进程发送 SIGCHLD 信号,通知其某个子进程已经终止。最后 do_exit()调用调度函数去执行其他进程。由此可见在进程被终止时,它的任务数据结构仍然保留着。因为其父进程还需要使用其中的信息。在子进程在执行期间,父进程通常使用wait()或 waitpid()函数等待其某个子进程终止。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中。最终释放已终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。int do_exit(long code)
{
int i;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
// ……
current->state = TASK_ZOMBIE;
/*
*修改--退出一个进程
*/
fprintk(3,"%d\tE\t%d\n",current->pid,jiffies);
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}
// ……
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
int flag, code;
struct task_struct ** p;
// ……
// ……
if (flag) {
if (options & WNOHANG)
return 0;
current->state=TASK_INTERRUPTIBLE;
/*
*修改--当前进程 => 等待
*/
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}小结总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。为了让生成的 log 文件更精准,以下几点请注意:进程退出的最后一步是通知父进程自己的退出,目的是唤醒正在等待此事件的父进程。从时序上来说,应该是子进程先退出,父进程才醒来。系统无事可做的时候,进程 0 会不停地调用 sys_pause(),以激活调度算法。此时它的状态可以是等待态,等待有其它可运行的进程;也可以叫运行态,因为它是唯一一个在 CPU 上运行的进程,只不过运行的效果是等待。编译重新编译make all编译运行process.c将process.c拷贝到linux0.11系统中,这个过程需要挂载一下系统硬盘,挂载拷贝成功之后再卸载硬盘,然后启动模拟器进入系统内编译一下process.c文件,过程命令及截图如下:// oslab目录下运行
sudo ./mount-hdc
cp ./test3/process.c ./hdc/usr/root/
sudo umonut hdc进入linux-0.11gcc -o process process.c
./process
sync使用./process即可运行目标文件,运行后会生成log文件,生成log文件后一定要记得刷新,然后将其拷贝到oslab/test3目录,命令如下:sudo ./mount-hdc
cp ./hdc/var/process.log ./test3/
sudo umonut hdcprocess.log自动化分析实验楼stat_log.py下载地址只要给 stat_log.py 加上执行权限(使用的命令为 chmod +x stat_log.py)就可以直接运行它。在结果中我们可以看到各个进程的周转时间(Turnaround,指作业从提交到完成所用的总时间)、等待时间等,以及平均周转时间和等待时间。修改时间片MOOC哈工大操作系统实验3:进程运行轨迹的跟踪与统计_ZhaoTianhao的博客-CSDN博客这段没有耐心实现了,摘录了一位博主的解释linux0.11采用的调度算法是一种综合考虑进程优先级并能动态反馈调整时间片的轮转调度算法。 那么什么是轮转调度算法呢?它为每个进程分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程;如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。那什么是综合考虑进程优先级呢?就是说一个进程在阻塞队列中停留的时间越长,它的优先级就越大,下次就会被分配更大的时间片。进程之间的切换是需要时间的,如果时间片设定得太小的话,就会发生频繁的进程切换,因此会浪费大量时间在进程切换上,影响效率;如果时间片设定得足够大的话,就不会浪费时间在进程切换上,利用率会更高,但是用户交互性会受到影响,举一个很直观的例子:我在银行排队办业务,假设我要办的业务很简单只需要占用1分钟,如果每个人的时间片是30分钟,而我前面的每个人都要用满这30分钟,那我就要等上好几个小时!如果每个人的时间片是2分钟的话,我只需要等十几分钟就可以办理我的业务了,前面没办完的会在我之后轮流地继续去办。所以时间片不能过大或过小,要兼顾CPU利用率和用户交互性。时间片的初始值是进程0的priority,是在linux-0.11/include/linux/sched.h的宏 INIT_TASK 中定义的,如下:我们只需要修改宏中的第三个值即可,该值即时间片的初始值。#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;修改完后再次编译make all,进入模拟器后编译运行测试文件process.c,然后运行统计脚本stat_log.py查看结果,与之前的结果进行对比。问题回答问题1:单进程编程和多进程编程的区别?1.执行方式:单进程编程是一个进程从上到下顺序进行;多进程编程可以通过并发执行,即多个进程之间交替执行,如某一个进程正在I/O输入输出而不占用CPU时,可以让CPU去执行另外一个进程,这需要采取某种调度算法。2.数据是否同步:单进程的数据是同步的,因为单进程只有一个进程,在进程中改变数据的话,是会影响这个进程的;多进程的数据是异步的,因为子进程数据是父进程数据在内存另一个位置的拷贝,因此改变其中一个进程的数据,是不会影响到另一个进程的。3.CPU利用率:单进程编程的CPU利用率低,因为单进程在等待I/O时,CPU是空闲的;多进程编程的CPU利用率高,因为当某一进程等待I/O时,CPU会去执行另一个进程,因此CPU的利用率高。4.多进程用途更广泛。问题2:仅针对样本程序建立的进程,在修改时间片前后,log 文件的统计结果都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?依次将时间偏设为1,5,10,15,20,25,50,100,150后,经统计分析log文件可以发现:1)在一定的范围内,平均等待时间,平均完成时间的变化随着时间片的增大而减小。这是因为在时间片小的情况下,cpu将时间耗费在调度切换上,所以平均等待时间增加。2)超过一定的范围之后,这些参数将不再有明显的变化,这是因为在这种情况下,RR轮转调度就变成了FCFS先来先服务了。随着时间片的修改,吞吐量始终没有明显的变化,这是因为在单位时间内,系统所能完成的进程数量是不会变的。警示编译好后进入linux-0.11直接报了内核错误,这肯定是之前的代码打错了。那么多代码,我怎么知道错误在哪。但是注意以前正常情况下会打印剩余空间。而先前改代码的时候发现这段打印的代码就在进程1 init() 函数内,所以推断是修改进程0时出现了错误好家伙,顺序反了。之前说了,文件系统初始化,描述符 0、1 和 2 关联之后,才能打开 log 文件。这里却直接先打开 log 文件了。😢天道酬勤粗略的了解了进程运行的方式,多进程的好处显而易见,大大节省了效率,但其调度算法如何实现可以尽可能缩短浪费的时间还是需要好好思考的。实验五 基于内核栈切换的进程切换实验目的深入理解进程和进程切换的概念;综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题;开始建立系统认识。实验内容现在的 Linux 0.11 采用 TSS 和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。本次实验包括如下内容:编写汇编程序 switch_to:完成主体框架;在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。用修改后的 Linux 0.11 仍然可以启动、可以正常使用。TSS 切换在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。具体的说,在设计“Intel 架构”(即 x86 系统结构)时,每个任务(进程或线程)都对应一个独立的 TSS,TSS 就是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像。有一个任务寄存器(Task Register,简称 TR)指向当前进程对应的 TSS 结构体,所谓的 TSS 切换就将 CPU 中几乎所有的寄存器都复制到 TR 指向的那个 TSS 结构体中保存起来,同时找到一个目标 TSS,即要切换到的下一个进程对应的 TSS,将其中存放的寄存器映像“扣在” CPU 上,就完成了执行现场的切换,如下图所示。Intel 架构不仅提供了 TSS 来实现任务切换,而且只要一条指令就能完成这样的切换,即图中的 ljmp 指令。具体的工作过程是:(1)首先用 TR 中存取的段选择符在 GDT 表中找到当前 TSS 的内存位置,由于 TSS 是一个段,所以需要用段表中的一个描述符来表示这个段,和在系统启动时论述的内核代码段是一样的,那个段用 GDT 中的某个表项来描述,还记得是哪项吗?是 8 对应的第 1 项。此处的 TSS 也是用 GDT 中的某个表项描述,而 TR 寄存器是用来表示这个段用 GDT 表中的哪一项来描述,所以 TR 和 CS、DS 等寄存器的功能是完全类似的。(2)找到了当前的 TSS 段(就是一段内存区域)以后,将 CPU 中的寄存器映像存放到这段内存区域中,即拍了一个快照。(3)存放了当前进程的执行现场以后,接下来要找到目标进程的现场,并将其扣在 CPU 上,找目标 TSS 段的方法也是一样的,因为找段都要从一个描述符表中找,描述 TSS 的描述符放在 GDT 表中,所以找目标 TSS 段也要靠 GDT 表,当然只要给出目标 TSS 段对应的描述符在 GDT 表中存放的位置——段选择子就可以了,仔细想想系统启动时那条著名的 jmpi 0, 8 指令,这个段选择子就放在 ljmp 的参数中,实际上就 jmpi 0, 8 中的 8。(4)一旦将目标 TSS 中的全部寄存器映像扣在 CPU 上,就相当于切换到了目标进程的执行现场了,因为那里有目标进程停下时的 CS:EIP,所以此时就开始从目标进程停下时的那个 CS:EIP 处开始执行,现在目标进程就变成了当前进程,所以 TR 需要修改为目标 TSS 段在 GDT 表中的段描述符所在的位置,因为 TR 总是指向当前 TSS 段的段描述符所在的位置。上面给出的这些工作都是一句长跳转指令 ljmp 段选择子:段内偏移,在段选择子指向的段描述符是 TSS 段时 CPU 解释执行的结果,所以基于 TSS 进行进程/线程切换的 switch_to 实际上就是一句 ljmp 指令:#define switch_to(n) {
struct{long a,b;} tmp;
__asm__(
"movw %%dx,%1"
"ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
)
}
#define FIRST_TSS_ENTRY 4
#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))GDT 表的结构如下图所示,所以第一个 TSS 表项,即 0 号进程的 TSS 表项在第 4 个位置上,4<<3,即 4 * 8,相当于 TSS 在 GDT 表中开始的位置,TSS(n)找到的是进程 n 的 TSS 位置,所以还要再加上 n<<4,即 n * 16,因为每个进程对应有 1 个 TSS 和 1 个 LDT,每个描述符的长度都是 8 个字节,所以是乘以 16,其中 LDT 的作用就是上面论述的那个映射表,关于这个表的详细论述要等到内存管理一章。TSS(n) = n * 16 + 4 * 8,得到就是进程 n(切换到的目标进程)的 TSS 选择子,将这个值放到 dx 寄存器中,并且又放置到结构体 tmp 中 32 位长整数 b 的前 16 位,现在 64 位 tmp 中的内容是前 32 位为空,这个 32 位数字是段内偏移,就是 jmpi 0, 8 中的 0;接下来的 16 位是 n * 16 + 4 * 8,这个数字是段选择子,就是 jmpi 0, 8 中的 8,再接下来的 16 位也为空。所以 swith_to 的核心实际上就是 ljmp 空, n*16+4*8,现在和前面给出的基于 TSS 的进程切换联系在一起了。本次实验的内容虽然用一条指令就能完成任务切换,但这指令的执行时间却很长,这条 ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。具体的说,在设计“Intel 架构”(即 x86 系统结构)时,每个任务(进程或线程)都对应一个独立的 TSS,TSS 就是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像。有一个任务寄存器(Task Register,简称 TR)指向当前进程对应的 TSS 结构体,所谓的 TSS 切换就将 CPU 中几乎所有的寄存器都复制到 TR 指向的那个 TSS 结构体中保存起来,同时找到一个目标 TSS,即要切换到的下一个进程对应的 TSS,将其中存放的寄存器映像“扣在”CPU 上,就完成了执行现场的切换。要实现基于内核栈的任务切换,主要完成如下三件工作:(1)重写 switch_to;(2)将重写的 switch_to 和 schedule() 函数接在一起;(3)修改现在的 fork()。正式修改代码前小结下面开始正式修改代码。其实主要就修改3个文件,但我不会按照每个文件一次性修改的顺序,而是按照实验逻辑,跳转修改,请保持清晰的逻辑。在截图的右下角有当前位置所在行数,注意观察不要修改错位置。schedule 与 switch_to目前 Linux 0.11 中工作的 schedule() 函数是首先找到下一个进程的数组位置 next,而这个 next 就是 GDT 中的 n,所以这个 next 是用来找到切换后目标 TSS 段的段描述符的,一旦获得了这个 next 值,直接调用上面剖析的那个宏展开 switch_to(next);就能完成如图 TSS 切换所示的切换了。现在,我们不用 TSS 进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的 switch_to 中将用到当前进程的 PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。由于 Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址;另外,由于当前进程的 PCB 是用一个全局变量 current 指向的,所以只要告诉新 switch_to()函数一个指向目标进程 PCB 的指针就可以了。同时还要将 next 也传递进去,虽然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的,也就是说,现在每个进程不用有自己的 TSS 了,因为已经不采用 TSS 进程切换了,但是每个进程需要有自己的 LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到 LDT 的切换。综上所述,需要将目前的 schedule() 函数(在 kernel/sched.c 中)做稍许修改,即将下面的代码:if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
//......
switch_to(next);修改为:if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;
//.......
switch_to(pnext,_LDT(next));注意 pnext 是指向 pcb 的指针struct tast_struct *pnext = &(init_task.task);使用 switch_to 需要添加函数声明extern long switch_to(struct task_struct *p, unsigned long address);实现 switch_to实现 switch_to 是本次实践项目中最重要的一部分。由于要对内核栈进行精细的操作,所以需要用汇编代码来完成函数 switch_to 的编写。这个函数依次主要完成如下功能:由于是 C 语言调用汇编,所以需要首先在汇编中处理栈帧,即处理 ebp 寄存器;接下来要取出表示下一个进程 PCB 的参数,并和 current 做一个比较,如果等于 current,则什么也不用做;如果不等于 current,就开始进程切换,依次完成 PCB 的切换、TSS 中的内核栈指针的重写、内核栈的切换、LDT 的切换以及 PC 指针(即 CS:EIP)的切换。可以很明显的看出,该函数是基于TSS进行进程切换的(ljmp指令),现在要改写成基于堆栈(内核栈)切换的函数,就需要删除掉该语句,在include/linux/sched.h 文件,我们将它注释掉。然后新的switch_to()函数将它作为一个系统调用函数,所以要将函数重写在汇编文件kernel/system_call.s:.align 2
switch_to:
//因为该汇编函数要在c语言中调用,所以要先在汇编中处理栈帧
pushl %ebp
movl %esp,%ebp
pushl %ecx
pushl %ebx
pushl %eax
//先得到目标进程的pcb,然后进行判断
//如果目标进程的pcb(存放在ebp寄存器中) 等于 当前进程的pcb => 不需要进行切换,直接退出函数调用
//如果目标进程的pcb(存放在ebp寄存器中) 不等于 当前进程的pcb => 需要进行切换,直接跳到下面去执行
movl 8(%ebp),%ebx
cmpl %ebx,current
je 1f
/** 执行到此处,就要进行真正的基于堆栈的进程切换了 */
// PCB的切换
movl %ebx,%eax
xchgl %eax,current
// TSS中内核栈指针的重写
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
//切换内核栈
movl %esp,KERNEL_STACK(%eax)
movl 8(%ebp),%ebx
movl KERNEL_STACK(%ebx),%esp
//LDT的切换
movl 12(%ebp),%ecx
lldt %cx
movl $0x17,%ecx
mov %cx,%fs
cmpl %eax,last_task_used_math
jne 1f
clts
//在到子进程的内核栈开始工作了,接下来做的四次弹栈以及ret处理使用的都是子进程内核栈中的东西
1: popl %eax
popl %ebx
popl %ecx
popl %ebp
ret逐条解释基于堆栈切换的switch_to()函数四段核心代码:// PCB的切换
movl %ebx,%eax
xchgl %eax,current
起始时eax寄存器保存了指向目标进程的指针,current指向了当前进程,
第一条指令执行完毕,使得ebx也指向了目标进程,
然后第二条指令开始执行,也就是将eax的值和current的值进行了交换,最终使得eax指向了当前进程,current就指向了目标进程(当前状态就发生了转移)// TSS中内核栈指针的重写
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
中断处理时需要寻找当前进程的内核栈,否则就不能从用户栈切到内核栈(中断处理没法完成),
内核栈的寻找是借助当前进程TSS中存放的信息来完成的,(当然,当前进程的TSS还是通过TR寄存器在GDT全局描述符表中找到的)。
虽然此时不使用TSS进行进程切换了,但是Intel的中断处理机制还是要保持。
所以每个进程仍然需要一个TSS,操作系统需要有一个当前TSS。
这里采用的方案是让所有进程共用一个TSS(这里使用0号进程的TSS),
因此需要定义一个全局指针变量tss(放在system_call.s中)来执行0号进程的TSS:
struct tss_struct * tss = &(init_task.task.tss);
此时唯一的tss的目的就是:在中断处理时,能够找到当前进程的内核栈的位置。
在内核栈指针重写指令中有宏定义ESP0,所以在上面需要提前定义好 ESP0 = 4,
(定义为4是因为TSS中内核栈指针ESP0就放在偏移为4的地方)
并且需要将: blocked=(33*16) => blocked=(33*16+4)kernel/system_call.s 文件重写TSS中的内核栈指针ESP0 = 4
KERNEL_STACK = 12
state = 0 # these are offsets into the task-struct.
counter = 4
priority = 8
kernelstack = 12
signal = 16
sigaction = 20 # MUST be 16 (=len of sigaction)
blocked = (37*16)kernel/sched.c 文件struct tss_struct * tss = &(init_task.task.tss);//切换内核栈
movl %esp,KERNEL_STACK(%eax)
movl 8(%ebp),%ebx
movl KERNEL_STACK(%ebx),%esp
第一行:将cpu寄存器esp的值,保存到当前进程pcb的eax寄存器中(保存当前进程执行信息)
第二行:获取目标进程的pcb放入ebx寄存器中
第三行:将ebx寄存器中的信息,也就是目标进程的信息,放入cpu寄存器esp中
但是之前的进程控制块(pcb)中是没有保存内核栈信息的寄存器的,所以需要在sched.h中的task_struct(也就是pcb)中添加kernelstack,
但是添加的位置就有讲究了,因为在某些汇编文件(主要是systen_call.s中),有操作这个结构的硬编码,
一旦结构体信息改变,那这些硬编码也要跟着改变,
比如添加kernelstack在第一行,就需要改很多信息了,
但是添加到第四行就不需要改很多信息,所以这里将kernelstack放到第四行的位置:
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
/** add kernelstack */
long kernelstack;
...
}
改动位置及信息:
将
#define INIT_TASK \
/* state etc */ { 0,15,15,\
/* signals */ 0,{{},},0, \
...
改为:
#define INIT_TASK \
/* state etc */ { 0,15,15, PAGE_SIZE+(long)&init_task,\
/* signals */ 0,{{},},0, \
...
在执行上述切换内核栈的代码之前(也就是switch_to()函数前),要设置栈的大小:KERNEL_STACK = 12
然后就执行上面的三行代码,就可以完成对内核栈的切换了。include/linux/sched.h 文件long kernelstack;由于这里将 PCB 结构体的定义改变了,所以在产生 0 号进程的 PCB 初始化时也要跟着一起变化, 需要修改 #define INIT_TASK,即在 PCB 的第四项中增加关于内核栈栈指针的初始化。代码如下:将
#define INIT_TASK \
/* state etc */ { 0,15,15,\
/* signals */ 0,{{},},0, \
...
改为:
#define INIT_TASK \
/* state etc */ { 0,15,15, PAGE_SIZE+(long)&init_task,\
/* signals */ 0,{{},},0, \
...kernel/system_call.sKERNEL_STACK = 12//LDT的切换
movl 12(%ebp),%ecx
lldt %cx
movl $0x17,%ecx
mov %cx,%fs
前两条语句的作用(切换LDT):
第一条:取出参数LDT(next)
第二条:完成对LDTR寄存器的修改
然后就是对PC指针(即CS:EIP)的切换:
后两条语句的含有就是重写设置段寄存器FS的值为0x17
补:FS的作用:通过FS操作系统才能访问进程的用户态内存。
这里LDT切换完成意味着切换到了新的用户态地址空间,所以需要重置FS。修改fork()系统调用现在需要将新建进程的用户栈、用户程序地址和其内核栈关联在一起,因为TSS没有做这样的关联fork()要求让父子进程共享用户代码、用户数据和用户堆栈虽然现在是使用内核栈完成任务的切换(基于堆栈的进程切换),但是fork()的基本含义不应该发生变化。综合分析:修改以后的fork()要使得父子进程共享同一块内存空间、堆栈和数据代码块。fork() 系统调用的代码放在 system_call.s 汇编文件中,先来看看已经写好的代码:.align 2
sys_fork:
call find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process//跳转到copy_process()函数
addl $20,%esp
1: ret可以看到fork()函数的核心就是调用了 copy_process(),接下来去看copy_process()copy_process()函数定义在kernel/fork.c中,代码和分析见注释:int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();//用来完成申请一页内存空间作为子进程的PCB
...
/** 很容易看出来下面的部分就是基于tss进程切换机制时的代码,所以将此片段要注释掉
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
...
*/
/** 然后这里要加上基于堆栈切换的代码(对frok的修改其实就是对子进程内核栈的初始化 */
long *krnstack;
krnstack = (long)(PAGE_SIZE +(long)p);//p指针加上页面大小就是子进程的内核栈位置,所以这句话就是krnstack指针指向子进程的内核栈
//初始化内核栈(krnstack)中的内容:
//下面的五句话可以完成对书上那个图(4.22)所示的关联效果(父子进程共有同一内存、堆栈和数据代码块)
/*
而且很容易可以看到,ss,esp,elags,cs,eip这些参数来自调用该函数的进程的内核栈中,
也就是父进程的内核栈,所以下面的指令就是将父进程内核栈的前五个内容拷贝到了子进程的内核栈中
*/
*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;
*(--krnstack) = ds & 0xffff;
*(--krnstack) = es & 0xffff;
*(--krnstack) = fs & 0xffff;
*(--krnstack) = gs & 0xffff;
*(--krnstack) = esi;
*(--krnstack) = edi;
*(--krnstack) = edx;
*(--krnstack) = (long) first_return_kernel;//处理switch_to返回的位置
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
//把switch_to中要的东西存进去
p->kernelstack = krnstack;
...kernel/fork.c 文件注释tss进程切换片段添加代码long *krnstack;
krnstack = (long)(PAGE_SIZE +(long)p);
*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;
*(--krnstack) = ds & 0xffff;
*(--krnstack) = es & 0xffff;
*(--krnstack) = fs & 0xffff;
*(--krnstack) = gs & 0xffff;
*(--krnstack) = esi;
*(--krnstack) = edi;
*(--krnstack) = edx;
*(--krnstack) = (long) first_return_kernel;
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
p->kernelstack = krnstack;上面的first_return_kernel(系统调用)的工作:"内核级线程切换五段论"中的最后一段切换,即完成用户栈和用户代码的切换,依靠的核心指令就是iret,当然在切换之前应该恢复一下执行现场,主要就是eax,ebx,ecx,edx,esi,gs,fs,es,ds这些寄存器的恢复,要将first_return_kernel(属于系统调用,而且是一段汇编代码)写在kernel/system_call.s头文件里面:首先需要将first_return_kernel设置在全局可见:.globl switch_to,first_return_kernel将具体的函数实现放在system_call.s头文件里面:.align 2
first_return_kernel:
popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret最后要记得是在 kernel/fork.c 文件里使用了 first_return_kernel 函数,所以要在该文件里添加外部函数声明extern void first_return_kernel(void);编译运行天道酬勤有些人不会写文章就不要发,参考的几篇全有错误,最后居然还能有运行成功的截图,你们编译怎么通过的?真牛!虽然我文章都是看的别人的,但我至少保证自己独立运行一遍,能保证运行通过。幸好最后碰到一位博主,看了他的文章自惭形秽。写的特别简洁但重点突出,有自己的理解。反观自己废话连篇,文章东拼西凑。实验六 信号量的实现和应用实验目的加深对进程同步与互斥概念的认识;掌握信号量的使用,并应用它解决生产者——消费者问题;掌握信号量的实现原理。实验内容本次实验的基本内容是:在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;在 0.11 中实现信号量,用生产者—消费者程序检验之。用信号量解决生产者—消费者问题在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:建立一个生产者进程,N 个消费者进程(N>1);用文件建立一个共享缓冲区;生产者进程依次向缓冲区写入整数 0,1,2,…,M,M>=500;消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;缓冲区同时最多只能保存 10 个数。一种可能的输出效果是:10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。pc.c 中将会用到 sem_open()、sem_close()、sem_wait() 和 sem_post() 等信号量相关的系统调用,请查阅相关文档。实现信号量Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。sem_t 是信号量类型,根据实现的需要自定义。name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。在 kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。什么是信号量?信号量,英文为 semaphore,最早由荷兰科学家、图灵奖获得者 E. W. Dijkstra 设计,任何操作系统教科书的“进程同步”部分都会有详细叙述。Linux 的信号量秉承 POSIX 规范,用man sem_overview可以查看相关信息。本次实验涉及到的信号量系统调用包括:sem_open()、sem_wait()、sem_post() 和 sem_unlink()。生产者—消费者问题生产者—消费者问题的解法几乎在所有操作系统教科书上都有,其基本结构为:Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}显然在演示这一过程时需要创建两类进程,一类执行函数 Producer(),另一类执行函数 Consumer()。思路大家可以参考这个演示视频,先总体梳理一遍。如果实验三系统调用学的不错的话,这里的系统调用编写和编译应该是很清晰的。通俗的讲,在用户程序中想要使用内核态中的系统调用命令,需要在用户态执行对系统调用命令的调用通过宏展开由此通过著名的 int 0x80 中断进入内核态用户程序 pc.c知识点文件操作信号量作用mutex 是保证互斥访问缓存池empty 是缓冲池里空位的剩余个数,即空缓冲区数,初始值为nfull 是用来记录当前缓冲池中已经占用的缓冲区个数,初始值为0代码展示#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
const char *FILENAME = "/usr/root/buffer_file"; /* 消费生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5; /* 消费者的数量 */
const int NR_ITEMS = 50; /* 产品的最大量 */
const int BUFFER_SIZE = 10; /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *metux, *full, *empty; /* 3个信号量 */
unsigned int item_pro, item_used; /* 刚生产的产品号;刚消费的产品号 */
int fi, fo; /* 供生产者写入或消费者读取的缓冲文件的句柄 */
int main(int argc, char *argv[])
{
char *filename;
int pid;
int i;
filename = argc > 1 ? argv[1] : FILENAME;
/* O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
* 0222 和 0444 分别表示文件只写和只读(前面的0是八进制标识)
*/
fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222); /* 以只写方式打开文件给生产者写入产品编号 */
fo = open(filename, O_TRUNC| O_RDONLY, 0444); /* 以只读方式打开文件给消费者读出产品编号 */
metux = sem_open("METUX", 1); /* 互斥信号量,防止生产消费同时进行 */
full = sem_open("FULL", 0); /* 产品剩余信号量,大于0则可消费 */
empty = sem_open("EMPTY", BUFFER_SIZE); /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */
item_pro = 0;
if ((pid = fork())) /* 父进程用来执行消费者动作 */
{
printf("pid %d:\tproducer created....\n", pid);
/* printf()输出的信息会先保存到输出缓冲区,并没有马上输出到标准输出(通常为终端控制台)。
* 为避免偶然因素的影响,我们每次printf()都调用一下stdio.h中的fflush(stdout)
* 来确保将输出立刻输出到标准输出。
*/
fflush(stdout);
while (item_pro <= NR_ITEMS) /* 生产完所需产品 */
{
sem_wait(empty);
sem_wait(metux);
/* 生产完一轮产品(文件缓冲区只能容纳BUFFER_SIZE个产品编号)后
* 将缓冲文件的位置指针重新定位到文件首部。
*/
if(!(item_pro % BUFFER_SIZE))
lseek(fi, 0, 0);
write(fi, (char *) &item_pro, sizeof(item_pro)); /* 写入产品编号 */
printf("pid %d:\tproduces item %d\n", pid, item_pro);
fflush(stdout);
item_pro++;
sem_post(full); /* 唤醒消费者进程 */
sem_post(metux);
}
}
else /* 子进程来创建消费者 */
{
i = NR_CONSUMERS;
while(i--)
{
if(!(pid=fork())) /* 创建i个消费者进程 */
{
pid = getpid();
printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
fflush(stdout);
while(1)
{
sem_wait(full);
sem_wait(metux);
/* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
if(!read(fo, (char *)&item_used, sizeof(item_used)))
{
lseek(fo, 0, 0);
read(fo, (char *)&item_used, sizeof(item_used));
}
printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
fflush(stdout);
sem_post(empty); /* 唤醒生产者进程 */
sem_post(metux);
if(item_used == NR_ITEMS) /* 如果已经消费完最后一个商品,则结束 */
goto OK;
}
}
}
}
OK:
close(fi);
close(fo);
return 0;
}修改内核编写 sem.h文件位置:oslab/linux-0.11/include/linux#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEMTABLE_LEN 20
#define SEM_NAME_LEN 20
typedef struct semaphore{
char name[SEM_NAME_LEN];
int value;
struct task_struct *queue;
} sem_t;
extern sem_t semtable[SEMTABLE_LEN];
#endif编写 sem.c#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>
sem_t semtable[SEMTABLE_LEN];
int cnt = 0;
sem_t *sys_sem_open(const char *name,unsigned int value)
{
char kernelname[100];
int isExist = 0;
int i=0;
int name_cnt=0;
while( get_fs_byte(name+name_cnt) != '\0')
name_cnt++;
if(name_cnt>SEM_NAME_LEN)
return NULL;
for(i=0;i<name_cnt;i++)
kernelname[i]=get_fs_byte(name+i);
int name_len = strlen(kernelname);
int sem_name_len =0;
sem_t *p=NULL;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name) )
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
p=(sem_t*)(&semtable[i]);
//printk("find previous name!\n");
}
else
{
i=0;
for(i=0;i<name_len;i++)
{
semtable[cnt].name[i]=kernelname[i];
}
semtable[cnt].value = value;
p=(sem_t*)(&semtable[cnt]);
//printk("creat name!\n");
cnt++;
}
return p;
}
int sys_sem_wait(sem_t *sem)
{
cli();
while( sem->value <= 0 )
sleep_on(&(sem->queue));
sem->value--;
sti();
return 0;
}
int sys_sem_post(sem_t *sem)
{
cli();
sem->value++;
if( (sem->value) <= 1)
wake_up(&(sem->queue));
sti();
return 0;
}
int sys_sem_unlink(const char *name)
{
char kernelname[100];
int isExist = 0;
int i=0;
int name_cnt=0;
while( get_fs_byte(name+name_cnt) != '\0')
name_cnt++;
if(name_cnt>SEM_NAME_LEN)
return NULL;
for(i=0;i<name_cnt;i++)
kernelname[i]=get_fs_byte(name+i);
int name_len = strlen(name);
int sem_name_len =0;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name))
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
int tmp=0;
for(tmp=i;tmp<=cnt;tmp++)
{
semtable[tmp]=semtable[tmp+1];
}
cnt = cnt-1;
return 0;
}
else
return -1;
}文件位置:oslab/linux-0.11/kernel添加系统调用号/* 添加的系统调用号 */
#define __NR_sem_open 72
#define __NR_sem_wait 73
#define __NR_sem_post 74
#define __NR_sem_unlink 75文件位置:oslab/linux-0.11/include/unistd.h改写系统调用数nr_system_calls = 76文件位置:oslab/linux-0.11/kernel/system_call.s添加系统调用的定义/* ... */
extern int sys_setregid();
/* 添加的系统调用定义 */
#include <linux/sem.h>
extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
/* 在sys_call_table数组中添加系统调用的引用: */
fn_ptr sys_call_table[] =
{ sys_setup, sys_exit, sys_fork, sys_read,……, sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink},文件位置:oslab/linux-0.11/include/linux/sys.h修改工程文件的编译规则/* 第一处 */
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o sem.o
/* 第二处 */
### Dependencies:
sem.s sem.o: sem.c ../include/linux/kernel.h ../include/unistd.h \
../include/linux/sem.h ../include/linux/sched.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h文件位置:oslab/linux-0.11/kernel/MakefileDebug反复查看 sem.h 中的代码,找不到错误。后来看了一下 sem.h 文件,发现拙劣的拼写错误修改后,编译成功!挂载文件将已经修改的 unistd.h 和 sem.h 文件以及用户文件 pc.c 拷贝到linux-0.11系统中,用于测试实现的信号量sudo ./mount-hdc
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
cp ./pc.c ./hdc/usr/root/
sudo umount hdc/测试启动新编译的linux-0.11内核,用pc.c测试实现的信号量gcc -o pc pc.c
./pc > wcf.txt
sync结果展示:天道酬勤Debug好几天,也看不出来哪里有问题,索性完全复制了一份别人的,这是做的最失败的一次实验,只能保证运行结果成功。复制的博主源码
.NET、C#基础知识
前言: 学习是一个循序渐进的过程,作为一名.Net软件工程师我们需要学习和掌握的东西非常的多,本章主要是记录下前段时间面试中经常遇到的一些基础常识,这里只是大致的概括还有很多需要学习的东西需要不断的学习和积累。访问修饰符:public:公有访问,不受任何限制。private:私有访问,只限于本类成员访问。protected:受保护的,只限于本类和子类访问。internal:内部访问,只限于本项目内访问,其他的不能访问protected internal:内部保护访问,只限于本项目或是子类访问,其他的不能访问 枚举: 是由一组特定常量构成的一组数据结构,是值类型的一种特殊形式,当需要一个由指定常量集合组成的数据类型时,使用枚举类型。枚举声明可以显式地声明 byte、sbyte、short、ushort、int、uint、long 或 ulong 类型作为对应的基础类型。没有显式地声明基础类型的枚举声明意味着所对应的基础类型是 int,在代码中使用枚举,可以将以前笨拙的代码变得优雅简单,更加直观,方便记忆。 枚举在什么地方适用呢? 一条普遍规则是,任何使用常量的地方,例如目前用 switch 代码切换常量的地方。如果只有单独一个值(例如,鞋的最大尺寸,或者笼子中能装猴子的最大数目),则还是把这个任务留给常量吧。但是,如果定义了一组值,而这些值中的任何一个都可以用于特定的数据类型,那么将枚举用在这个地方最适合不过。WebAPI 和 WebService的区别: (https://blog.csdn.net/cysong168/article/details/51433986)webapi使用的是http协议,而webservices采用的是soap协议webapi是无状态的,相对于webserives更轻量级。webapi支持get,和post等http请求http soap关系: http是一个客户端和服务器端请求和应答的标准(TCP)。http协议其目的是为了提供一种发布和接收htttp页面的方法一http协议的客户端与服务器的交互:由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如”HTTP/1.1 200 OK”,和(响应的)消息,消息的消息体可能是请求的文件、错误消息、或者其它一些信息。soap 协议:它描述了一种在分散或分布式的环境中如何交换信息的轻量级协议。soap在http协议的基础上,一个基于XML的协议。不同:都是底层的通信协议,请求包的格式不同而已,soap包是XML格式,http纯文本格式。WCF和WEB API我该选择哪个? 1、当你想创建一个支持消息、消息队列、双工通信的服务时,你应该选择WCF 2、当你想创建一个服务,可以用更快速的传输通道时,像TCP、Named Pipes或者甚至是UDP(在WCF4.5中),在其他传输通道不可用的时候也可以支持HTTP 3、当你想创建一个基于HTTP的面向资源的服务并且可以使用HTTP的全部特征时(比如URIs、request/response头,缓存,版本控制,多种内容格式),你应该选择Web API 4、当你想让你的服务用于浏览器、手机、iPhone和平板电脑时,你应该选择Web APIhashmap和hashTable的区别:1.hashTable是Dictionary的子类,HashMap是Map接口的一个实现的类2.Hashtable中的方法是同步的,而HashMap中的方法缺省情况为非同步3.HashTabe不允许null值,而hashmap允许有null4.hashtable默认长度是11,增长方式是0ld*2+1,而hashmap则默认是16,而且一定是2的指数如何理解委托和事件?委托相当于c或者c++中的指针,委托能够把方法当做参数传递,并且提高程序的可扩展性。 事件:以一种特殊的委托委托具有如下特点:1.委托相当于c++中的指针,但是他是安全类型2.委托可以把方法当做参数传递3.委托可以定义回调方法4.委托可以链接在一起,例如:可以对多个事件调用多个方法5.方法不需要与委托前面精确匹配UDP连接和tcp连接的异同: 两者都可以实现远程通讯,而主要区别在于tcp需要保持连接,而Udp不需要,由此可以看出udp更高的效率和占用资源更少,但是tcp传输数据更可靠.Net身份验证的方式有哪几种: 默认值为 Windows。 在Web.config 文件中,通过 <authentication> 节可以配置 ASP.NET 使用的安全身份验证模式,以标识传入的用户。 <authentication mode="[Windows|Forms|Passport|None]"><forms>...</forms><passport/></authentication><authentication> 节的mode 为必选的属性。指定应用程序的默认身份验证模式。此属性可以为下列值之一: Windows :将 Windows 验证指定为默认的身份验证模式。将它与以下任意形式的 Microsoft Internet 信息服务 (IIS) 身份验证结合起来使用:基本、摘要、集成 Windows 身份验证 (NTLM/Kerberos) 或证书。在这种情况下,您的应用程序将身份验证责任委托给基础 IIS。 Forms :将 ASP.NET 基于窗体的身份验证指定为默认身份验证模式。Passport :将 Microsoft Passport Network 身份验证指定为默认身份验证模式。 None :不指定任何身份验证。您的应用程序仅期待匿名用户,否则它将提供自己的身份验证。在c#中object类:1、类Object是 .NET Framework 中所有类、结构、枚举和委托的最终基类。2、C#通常不要求类显示声明从 Object 的继承,因为继承是隐式的。3、因为 .NET Framework 中的所有类均从 Object 派生,所以 Object 类中定义的每个方法可用于系统中的所有对象。 派生类也可以重写这些方法中的某些。高内聚,低耦合的理解:内聚:是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系;耦合:是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。C#面向对象思想主要包括:封装,继承,多态:封装:用抽象的数据类型将数据类型和基本的方法操作封装在一起,在数据保护在抽象类型内部 (封装就是把数据或者函数集合在一个个的类中,封装的对象被称为抽象对象,封装的意义是防止或保护代码被我们无意的破坏,提高代码的重用性,职责分配)继承:子类拥有父类的所有数据和操作多态:一个程序中同名的不同方法的共存情况(方法重载和重写) C#面向切面编程(AOP): 这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(https://www.cnblogs.com/landeanfen/p/4782370.html)一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。重载和重写的区别:重载:方法名相同参数个数和参数类型不同重写:当子类继承父类,子类中的方法与父类中的方法名,参数类型和参数个数完全相同则称为子类重写了父类方法(需要一override字标示)(对基类成员的 重写)概述反射和序列化: 反射:程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性 (就如:实例化对应的类,然后通过实例字段获取对应的方法)序列化:序列化是将对象转换为容易传输的格式的过程。例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象。在另一端,反序列化将从该流重新构造对象。C#中的构造函数: 每次创建类或结构,将会调用其构造函数。类或结构中可以创建多个不个不同参数的构造函数,程序员可以对不同构造函数设置默认值,约束条件,并编写灵活易读的代码。构造函数的特点: a:构造函数与类名相同 b:构造函数没有返回值 c:可以带参数,也可以不带参数 d:每个类中必须要有都应一个构造函数,一般假如不需要经常用到的话可以不需要声明构造函数,因为程序在编译的时候会自己调用无参的构造函数。 类成员有多少种访问方式:a:实例成员,可以通过类的实例访问b:静态成员可以直接通过类访问简单描述 string str=null;string str="",请尽量用文字说明区别(要点:说明详细的内存空间分配)前者声明变量不会分配内存空间;后者创建了一个空的字符类型,并且分配了对应的内存空间String str=new String("加油");创建了几个string object?创建了两个,一个对象是加油 一个是指向"加油"的strString 类是否可以被继承:不能,因为String类是selead类简单描述.NET里class和struct(结构)异同: 类和结构有很多相似之处:结构可以实现接口,并且具有于类相同的成员类型不同:结构是值类型而非引用类型,结构不能继承,结构存储在堆栈或者内联上。值类型:整数,浮点数,高精度浮点数,布尔,字符,结构,枚举 引用类型:对象(Object),字符串,类,接口,委托,数组sealed(又称密封类)修饰的类有哪些特点: sealed修饰的类防止此类被派生出其他的类,如果密封类被指定为其它的基类则编译出错(密封类同时不能为抽象类)C#.NET里面抽象类和接口有什么区别? 声明方法的存在而不去实现它的类被叫做抽象类(abstract class),它用于要创建一个体现某些基本行为的类,并为该类声明方法,但不能在该类中实现该类的情况。不能创建abstract 类的实例。然而可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类为。取而代之,在子类中实现该方法。知道其行为的其它类可以在类中实现这些方法。接口(interface)是抽象类的变体。在接口中,所有方法都是抽象的。多继承性可通过实现这样的接口而获得。接口中的所有方法都是抽象的,没有一个有程序体。接口只可以定义static final成员变量。接口的实现与子类相似,除了该实现类不能从接口定义中继承行为。当类实现特殊接口时,它定义(即将程序体给予)所有这种接口的方法。然后,它可以在实现了该接口的类的任何对象上调用接口的方法。由于有抽象类,它允许使用接口名作为引用变量的类型。通常的动态联编将生效。引用可以转换到接口类型或从接口类型转换,instanceof 运算符可以用来决定某对象的类是否实现了接口。接 口:(1) 接口不能被实例化 (2) 接口只能包含方法声明 (3) 接口的成员包括方法、属性、索引器、事件 (4) 接口中不能包含常量、字段(域)、构造函数、析构函数、静态成员。(5) 接口中的所有成员默认为public,因此接口中不能有private修饰符 (6) 派生类必须实现接口的所有成员 (7) 一个类可以直接实现多个接口,接口之间用逗号隔开 (8) 一个接口可以有多个父接口,实现该接口的类必须实现所有父接口中的所有成员 抽象类和接口 : 相同点和不同点(http://www.cnblogs.com/ronli/archive/2011/10/26/2224654.html)相同点: 1. 都不能被直接实例化,都可以通过继承实现其抽象方法。 2. 都是面向抽象编程的技术基础,实现了诸多的设计模式。不同点:1. 接口支持多继承;抽象类不能实现多继承。2. 接口只能定义抽象规则;抽象类既可以定义规则,还可能提供已实现的成员。3. 接口是一组行为规范;抽象类是一个不完全的类,着重族的概念。4. 接口可以用于支持回调;抽象类不能实现回调,因为继承不支持。5. 接口只包含方法、属性、索引器、事件的签名,但不能定义字段和包含实现的方法;抽象类可以定义字段、属性、包含实现的方法.net中dataset和datareader的区别: 首先,datareader是在线对象,用来以只读只进方式读取数据。只读,就是只能通过它获取数据而不能修改数据,只进,就是读取记录的游标只会不断前进,比如说读取了第5条记录以后就不能返回去读取第2条记录而dataset是离线对象,它就好像是一个数据库,也可以说是数据库的一个副本,你可以对dataset进行离线编辑修改,之后提交回数据库,实现数据的离线操作sql语句中int转varchar:2011-02-12 10:39SELECT parent_id_list+','+cast(id AS varchar(10)) FROM mytable或者cast也可以用convert来代替同时看到有网友有这样的需求,它想把1转成varchar的”000001″这样的串,但是cast函数是只会转成”1″,所以要借用right函数select right(’00000000′ CAST(@i as varchar) 1).数据库多表连接查询可以分为哪几种?并且各有什么作用?主要分为五种: a:主键条件关联查询(表中至少有一个匹配与inner join...on 相似) b:inner join ...on内连接查询(表中有至少一个匹配,则返回匹配的行) c:left join....on 左连接查询(右边表没有匹配,也从左边表返回所有的行) d:right join...on 右连接查询(左边表没有匹配,也从右边表返回所有行) e:full join...on 全连接查询(就是返回两个表中的所有行)数据库中的存储过程和sql语句有优缺点: 数据库存储过程优点:a:代码精简一致,代码重用性强,可以实现模块化程序设计 b:运行速度快,提高程序的性能, 因为存储程序只在创建时进行编译,以后每次执行存储过程都不需再重新编译,而一般SQL语句每执行一次就编译一次, 所以使用存储过程可提高数据库执行速度 c:可维护性高,只需创建存储过程一次并将其存储在数据库中,以后即可在程序中调用该过程任意次。存储过程可独立于程序源代码而单独修改,而不需要 更 改 ,测试以及重新部署程序集。d: 有安全机制,可授予没有直接执行存储过程中语句的权限的用户,也可执行该存储过程的权限。另外可以防止用户直接访问表,强制用户使用存储过程 执行特定的任务。e: 减少网络流量,在网络中要发送的数百行代码,可由一条执行其存储过程代码的一条单独语句就可实现(多条sql语句这里简化成立一个存储过程) 不足:a:架构不清醒不利于面向对象: 存储过程不太适合面向对象的设计,无法采用面向对象的方式将业务逻辑进行封装,业务逻辑在存储层实现,增加了业务和存储的耦合,代码的可读性也会降低。b:可移植性差:过多的使用存储过程会降低系统的移植性。在对存储进行相关扩展时,可能会增加一些额外的工作。 T-sql语句:可移植性强,语句灵活查询速度比存储过程要慢数据库存储过程和T-sql语句的选择:在一些新的项目开发过程中一般不推荐优先使用数据库存储过程,一般数据库存储过程适用于一般业务逻辑复杂,涉及到了多表操作,并且查询大批量的数据的时候。例如后台大批量查询,以及定期更新等。 (1)当一个事务涉及到多个SQL语句时或者涉及到对多个表的操作时可以考虑应用存储过程 (2)在一个事务的完成需要很复杂的商业逻辑时可以考虑应用存储过程 (3)比较复杂的统计和汇总可以考虑应用后台存储过程数据库常用类型及其含义:注:(Unicode 是「字符集」,UTF-8 是「编码规则」) Char,NChar 定长,速度快,占空间大,需处理 Varchar,Nvarchar,text 不定长,空间小,速度慢,无需处理Nchar、NVarchar、NTEXT处理Unicode码 char、varchar最多能存储8000个英文,4000个汉字。可以看出使用nchar、nvarchar数据类型时不用担心输入的字符是英文还是汉字,较为方便但在存储英文时数量上有些损失。 如何使用这些类型: 如果你肯定存储的数据长度,而且不包中文的,可以选择char类型。 如果肯定存储的数据长度,但可能包括中文,可以选择nchar类型。 如果不确定存储的数据长度,存储只有英文、数字的最好用varchar 如果不确定存储的数据长度,也有可能有中文,可以选择nvarchar类型XML和json数据传输格式的优缺点: 什么是 XML?XML 指可扩展标记语言(EXtensible Markup Language)XML 是一种标记语言,很类似 HTMLXML 的设计宗旨是传输数据,而非显示数据XML 标签没有被预定义。您需要自行定义标签。XML 被设计为具有自我描述性。XML 是 W3C 的推荐标准 什么是json?JSON 是纯文本JSON 具有“自我描述性”(人类可读)JSON 具有层级结构(值中存在值)JSON 可通过 JavaScript 进行解析JSON 数据可使用 AJAX 进行传输 区别:xml(可扩展性标记语言)和json两者都是一种跨平台,跨语言的数据传输格式,xml可以自定义标签而json是有规定的文本格式,json 的适用范围有限而xml没有,但是xml看起来比较复杂,json简单。 XSLT?是一种可扩展的样式表文件,使用xsl可以格式化xml格式,并且能够将xml转化为另一种格式的输出用.net做B/S结构的系统,您是用几层结构来开发,每一层之间的关系以及为什么要这样分层? 答:从下至上分别为:数据访问层、业务逻辑层(又或成为领域层)、表示层 数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问业务逻辑层:是整个系统的核心,它与这个系统的业务(领域)有关 是表现层与数据访问的的桥梁(起到了逻辑判断)表示层:是系统的UI部分,负责使用者与整个系统的交互。优点: 分工明确,条理清晰,易于调试,而且具有可扩展性缺点: 增加成本。分层式结构究竟其优势何在? 1、开发人员可以只关注整个结构中的其中某一层;2、可以很容易的用新的实现来替换原有层次的实现; 3、可以降低层与层之间的依赖;4、有利于标准化; 5、利于各层逻辑的复用。 概括来说,分层式设计可以达至如下目的:分散关注、松散耦合、逻辑复用、标准定义、易于拓展分层式结构也不可避免具有一些缺陷: 1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。 MVC 编程模式MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式:Model(模型)表示应用程序核心(比如数据库记录列表)View(视图)显示数据(数据库记录)Controller(控制器)处理输入(写入数据库记录)MVC 模式同时提供了对 HTML、CSS 和 JavaScript 的完全控制。Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据View(视图)是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的。Controller(控制器)是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。MVC 分层有助于管理复杂的应用程序,因为您可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。MVC 分层同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。 mvc的优点:1.通过把项目分成model view和controller,使得复杂项目更加容易维护。2.没有使用view state和服务器表单控件,可以更方便的控制应用程序的行为3.应用程序通过controller来控制程序请求,可以提供丰富的url重写。4.对单元测试的支持更加出色5.在团队开发模式下表现更出众MVC的不足: (1)增加了系统结构和实现的复杂性。对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。 (2)视图与控制器间的过于紧密的连接。视图与控制器是相互分离,但确实联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。 (3)视图对模型数据的低效率访问。依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。 asp.net如何实现MVC模式,举例说明!web/business/dataaccess 列举ASP.NET 页面之间传递值的几种方式1.response. Redirect() QueryString()...............2.使用Session变量 3.使用Server.Transfer(只能在同一站点中进行url重定向,而且url和url中的参数不会在浏览器中显示)4.application和Cookie请说明在.net中常用的几种页面间传递参数的方法,并说出他们的优缺点QueryString 传递一个或多个安全性要求不高或是结构简单的数值。但是对于传递数组或对象的话,就不能用这个方法了 session(viewstate) 简单,但易丢失 作用于用户个人,过量的存储会导致服务器内存资源的耗尽。application 对象的作用范围是整个全局,也就是说对所有用户都有效。其常用的方法用Lock和UnLock cookie 简单,但可能不支持,可能被伪造 Cookie是存放在客户端的,而session是存放在服务器端的。而且Cookie的使用要配合ASP.NET内置对象Request来使用 input ttype="hidden" 简单,可能被伪造 url参数简单,显示于地址栏,长度有限 Server.Transfer 把流程从当前页面引导到另一个页面中,新的页面使用前一个页面的应答流 数据库稳定,安全,但性能相对弱 什么是Viewstate?它有什么作用?ViewState用来保存页面状态,就是说提交之后我们还可以看到文本框里面的内容就是ViewState保存的功劳。 ViewState只维护当前页面的状态,不同页面之间不能共享,Session可以。 ViewState你可以理解为一个隐藏控件ASP.Net页面生命周期 :(重要)https://www.cnblogs.com/xhwy/archive/2012/05/20/2510178.html当我们在浏览器地址栏中输入网址,回车查看页面时,这时会向服务器端(IIS)发送一个request请求,服务器就会判断发送过来的请求页面, 完全识别 HTTP 页面处理程序类后,ASP.NET 运行时将调用处理程序的 ProcessRequest 方法来处理请求,来创建页面对象。通常情况下,无需更改此方法的实现,因为它是由 Page 类提供的。接下来被创建页面对象的ProcessRequest方法使页面经历了各个阶段:初始化、加载视图状态信息和回发数据、加载页面的用户代码以及执行回发服务器端事件。之后,页面进入显示模式:收集更新的视图状态,生成 HTML 代码并随后将代码发送并输出控制台。最后,卸载页面,并认为请求处理完毕。其中页面对象ProcessRequest方法 完成的这一系列事件的处理过程就是Asp.Net页面生命周期。我的理解:首先在对应的浏览器中输入对应的网址对服务器发送请求(request)服务器识别浏览器的http请求,通过调用processrequest方法来处理请求,创建页面对象然后通过创建processrequest方法使页面经历了:初始化,加载试图状态信息,和回发数据,加 载用户代码及执行返回服务器端的事件。之后页面进入显示模式:收集更新的试图状态,生成html代码将代码发送输出在浏览器中展示。最后卸载页面,并认为请求处理完毕每个页面的生命周期为用户的每一次访问,也就是说每一次客户端与服务器之间的一个往返过程.全局变量的生命周期在此之间.1. Page_Init(); 2. Load ViewState and Postback data; 3. Page_Load(); 4. Handle control events; 5. Page_PreRender(); 6. Page_Render(); 7. Unload event; 8. Dispose method called; ADO.net中常用的对象有哪些?分别描述一下。Connection 数据库连接对象 Command 执行数据库命令DataAdapter 连接数据,执行数据库命令,填充DataSetDataSet 数据在内存中的缓存,数据结构 DataReader 只读向前的读取数据库 DataReader和DataSet的异同 1.与数据库连接:DataReader:面向连接,只读,只进,只能向前读,读完数据就断开连接;(无法对获取的数据进行操作)DataSet:非面向连接,把数据加载到sql缓存池中,然后断开连接;2.处理数据速度:DataReader:速度快;DataSet:速度慢;3.更新数据库:DataReader:更新后,没有办法还原到原来的数据库;DataSet:更新后,可以还原回原来的数据库;4.支持分页排序:DataReader:不支持分页与排序;DataSet:支持分页与排序,从内存中直接读取前多少条(假分页);5.占用内存:DataReader:占用内存小;DataSet:占用内存大;DataSet可以比作一个内存中的数据库,DataTable是一个内存中的数据表,DataSet里可以存储多个DataTable说出你所了解的数据库访问组件(例如ADO,至少4种) 答:ADO,ADO.Net,MDAC(Microsoft Data Access Components),Microsoft SQL Server OLE DB Provider,Microsoft Jet OLE DB Provider,Desktop Database Drivers ODBC Driver,Visual FoxPro ODBC Driver 什么是装箱和拆箱? 答:从值类型接口转换到引用类型装箱。从引用类型转换到值类型拆箱。装箱(boxing)是将 值类型的数据转化成引用类型,int i=3; object o = i ;便是装箱过程,而拆箱(unboxing)是将饮用类型数据转换值类型,比如int j = (int)o;属于拆箱接口含义:接口是指一组函数成员而不实现他们的引用类型,通常只能通过接和类的继承,继承接口要实 其全部方法什么时候使用抽象类,什么时候用接口 :接口用于规范,抽象类用于共性。接口中只能声明方法,属性,事件,索引器。而抽象类中可以有方法的实现,也可以定义非静态的类变量。抽象类是类,所以只能被单继承,但是接口却可以一次实现多个。抽象类可以提供某些方法的部分实现,接口不可以.抽象类的实例是它的子类给出的。接口的实例是实现接口的类给出的。再抽象类中加入一个方法,那么它的子类就同时有了这个方法。而在接口中加入新的方法,那么实现它的类就要重新编写(这就是为什么说接口是一个类的规范了)。接口成员被定义为公共的,但抽象类的成员也可以是私有的、受保护的、内部的或受保护的内部成员(其中受保护的内部成员只能在应用程序的代码或派生类中访问)。此外接口不能包含字段、构造函数、析构函数、静态成员或常量。 枚举的使用:https://www.cnblogs.com/claspcn/p/5218520.html委托和事件:(http://www.tracefact.net/tech/009.html)委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易。它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去的人每次见到委托和事件就觉得心里别(biè)得慌,混身不自在。本文中,我将通过两个范例由浅入深地讲述什么是委托、为什么要使用委托、事件的由来、.Net Framework中的委托和事件、委托和事件对Observer设计模式的意义,对它们的中间代码也做了讨论。 const和readonly有什么区别? const关键字用来声明编译时常量,readonly用来声明运行时常量。用sealed修饰的类有什么特点 sealed 修饰符用于防止从所修饰的类派生出其它类,如果一个密封类被指定为其他类的基类,则会发生编译时错误。 密封类不能同时为抽象类。 sealed 修饰符主要用于防止非有意的派生,但是它还能促使某些运行时优化。具体说来,由于密封类永远不会有任何派生类,所以对密封类的实例的虚拟函数成员的调用可以转换为非虚拟调用来处理。 虚函数的用法 1)virtual指明一成员函数为虚函数,而virtual仅用于类的定义里,在类外可不加此关键字. 2)一个类的成员函数被定义为虚函数时,子类该函数仍保持虚函数特征. 3)子类覆盖此函数时,定义里可不加virtual关键字,但函数声明要和基类的完全一致!且此声明是必须的. 4)不是纯虚函数时,父类的虚函数必须要实现; 而若将父类的虚函数设定为纯虚函数时,子类必需要覆盖之而且必须要实现之! 解释virtual、sealed、override和abstract的区别 virtual申明虚方法的关键字,说明该方法可以被重写 sealed说明该类不可被继承 override重写基类的方法 abstract申明抽象类和抽象方法的关键字,抽象方法不提供实现,由子类实现,抽象类不可实例化。 c#继承:base表示对象基类的实例(使用base可以调用基类中的成员)base 表示当前对象基类的实例(使用base关键字可以调用基类的成员)this表示当前类的实例 在静态方法中不可以使用base和this关键字 派生类会继承基类所有的成员但是构造函数和析构函数不会被继承 什么是析构函数:析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)。注意如果派生类的方法和基类的方法同名则基类中的方法将会被隐藏如果需要隐藏则可以使用关键字new来隐藏如果不写new关键字默认处理为隐藏虽然基类中同名的方法被隐藏了但是还是可以通过base关键字来调用 进程和线程的区别?进程是系统进行资源分配和调度的单位;线程是CPU调度和分配的单位,一个进程可以有多个线程,这些线程共享这个进程的资源。堆和栈的区别: 栈:由编译器自动分配、释放。在函数体中定义的变量通常在栈上。堆:一般由程序员分配释放。用new、malloc等分配内存函数分配得到的就是在堆上。 概述:栈(Stack)由系统管理生存期,存储代码执行和调用路径,执行或调用完毕即从栈中清除; 堆(Heap)中保存值和对象,调用完毕之后依然存在,由垃圾回收器查找栈中有无指向该值或对象的引用,无则从堆中删除C# ref与out区别:1、使用ref型参数时,传入的参数必须先被初始化。对out而言,必须在方法中对其完成初始化。2、使用ref和out时,在方法的参数和执行方法时,都要加Ref或Out关键字。以满足匹配。3、out适合用在需要retrun多个返回值的地方,而ref则用在需要被调用的方法修改调用者的引用的时候。 你对泛型了解吗?简单说明一下泛型的有什么好处?泛型:通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用 好处是——类型安全和减少装箱、拆箱。提高性能、类型安全和质量,减少重复性的编程任务 C#中所有对象共同的基类是什么? System.Object. 如何在C#中实现继承? 在类名后加上一个冒号,再加上基类的名称。 私有成员会被继承么? 会,但是不能被访问。所以看上去他们似乎是不能被继承的,但实际上确实被继承了。 (是有成员能够被继承,但是不能够被访问)new的三种用法 答:实例化对象,隐藏基类方法。运算符(创建对象,创建值类型的默认构造函数)修饰作用(隐藏基类中的继承对象,子类中的对象覆盖基类中的版本)string是值类型还是引用类型?引用类型 String类与StringBuilder类有什么区别?为什么在.Net类库中要同时存在这2个类?(简答)stringBuilder比string更节约内存,所以stringBuilder更快String 对象是不可改变的。每次使用 System.String 类中的方法之一或进行运算时(如赋值、拼接等)时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。而 StringBuilder 则不会。在需要对字符串执行重复修改的情况下,与创建新的 String 对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用 System.Text.StringBuilder 类。例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder 类可以提升性能。 Session有什么重大BUG,微软提出了什么方法加以解决? 答:是iis中由于有进程回收机制,系统繁忙的话Session会丢失,可以用Sate server或SQL Server数据库的方式存储Session不过这种方式比较慢,而且无法捕获Session的END事件 c#中的三元运算符是 ?: .能用foreach遍历访问的对象需要实现(Ienumber) 接口或声明 ( GetEnumerator)方法的类型。.<%# %> 和 <% %> 有什么区别?<%# %>表示绑定的数据源 <% %>是服务器端代码块 C#可否对内存进行直接的操作?在.net下,.net引用了垃圾回收(GC)功能,它替代了程序员 不过在C#中,不能直接实现Finalize方法,而是在析构函数中调用基类的Finalize()方法 DateTime是否可以为null?不能,因为其为Struct类型,而结构属于值类型,值类型不能为null,只有引用类型才能被赋值null DateTime.Parse(myString); 这行代码有什么问题?有问题,当myString不能满足时间格式要求的时候,会引发异常,建议使用DateTime.TryParse() net的错误处理机制是:采用try->catch->finally结构, 为什么不提倡catch(Exception) try..catch在出现异常的时候影响性能; 应该捕获更具体得异常,比如IOExeception,OutOfMemoryException等 catch(Exception e){throw e;}和catch(Exception e){throw;}的区别将发生的异常对象抛出,另一个只是抛出异常,并没有抛出原异常对象) 我应该如何允许连接池?对于.NET应用程序而言,默认为允许连接池。(这意味着你可以不必为这件事情做任何的事情)当然,如果你可以在SQLConnection对象的连接字符串中加进Pooling=true;确保你的应用程序允许连接池的使用。(pooling=true)5. 我应该如何禁止连接池?ADO.NET默认为允许数据库连接池,如果你希望禁止连接池,可以使用如下的方式:1) 使用SQLConnection对象时,往连接字符串加入如下内容:Pooling=False;2) 使用OLEDBConnection对象时,往连接字符串加入如下内容:OLE DB Services=-4; 提高.NET的性能 1 使用异步方式调用Web服务和远程对象只要有可能就要避免在请求的处理过程中对Web服务和远程对象的同步调用,因为它占用的是的ASP.NET 线程池中的工作线程,这将直接影响Web服务器响应其它请求的能力。2 使用适当的Caching策略来提高性能3 判断字符串,不要用""比较。//避免 if(strABC!=null && strABC!="") {}//推荐 if(!strABC.IsNullOrEmpty) {}4 页面优化5 用完马上关闭数据库连接6 尽量使用存储过程,并优化查询语句 7 只读数据访问用SqlDataReader,不要使用DataSet请解释转发与跳转的区别 转发就是服务端的跳转A页面提交数据到B页面,B页面进行处理然后从服务端跳转到其它页面 跳转就是指客户端的跳转 什么叫应用程序域? 答:应用程序域可以理解为一种轻量级进程。起到安全的作用。占用资源小。 CTS、CLS、CLR分别作何解释? 答:CTS:通用语言系统。CLS:通用语言规范。CLR:公共语言运行库。 请解释web.config文件中的重要节点appSettings包含自定义应用程序设置system.web 系统配置 compilation动态调试编译设置customErrors自定义错误信息设置 authentication身份验证,此节设置应用程序的身份验证策略 authorization授权, 此节设置应用程序的授权策略.请解释ASP。NET中的web页面与其隐藏类之间的关系? ASP.NET页面一般都对应一个隐藏类,一般都在ASP.NET页面的声明中指定了隐藏类例如一个页面Tst1.aspx的页面声明如下 <%@ Page language="c#" Codebehind="Tst1.aspx.cs" AutoEventWireup="false" Inherits="T1.Tst1" %> Codebehind="Tst1.aspx.cs" 表明经编译此页面时使用哪一个代码文件 Inherits="T1.Tst1" 表用运行时使用哪一个隐藏类 当发现不能读取页面上的输入的数据时很有可能是什么原因造成的?怎么解决? 很有可能是在Page_Load中数据处理时没有进行Page的IsPostBack属性判断 请解释什么是上下文对象,在什么情况下要使用上下文对象 上下文对象是指HttpContext类的Current 属性,当我们在一个普通类中要访问内置对象(Response,Request,Session,Server,Appliction等)时就要以使用此对象 解释一下ajax及实现原理 答:Ajax的核心是JavaScript对象XmlHttpRequest,它使您可以使用JavaScript向服务器提出请求并处理响应, 而不阻塞用户,异步请求。
Service Stack 与 WCF
本文转载自
http://www.cnblogs.com/shanyou/p/3348347.html
ServiceStack是一个开源的、支持.NET与Mono平台的REST Web Services框架。同类产品微软自有的是WCF。
Web服务通常有远程过程调用(RPC)和RESTful (HTTP)两类,现在占据主导地位的Web服务是RESTful (HTTP)。
ServiceStack的组件见下:
包含高性能Razor Engine的Web应用框架;
支持例如HTML,XML,JSON,SOAP等多种格式的基于消息的Web服务框架;
包含内建IOC(Inversion of Control)的容器;若干内建库文件,诸如:Text serializer,Redis Client,ORM以及caching providers;
除了ASP.NET Hosting和Mono Hosting之外,还包含self-hosting选项。
2年前REST就已经成为Web API部署方式的主流了,而且一直保持这种发展势头,现在基本上都是REST服务,SOAP在企业内网还存在.
远程过程调用 (RPC) ,每个请求旨在类似于函数调用:
public interface IService{ string DoSomething(int input);}
RPC 方法对服务的修改非常不友好。 例如前面的代码段,如果要求从客户端来执行更高版本的 Web 服务的 DoSomething 方法的两个输入参数 — 或需要返回字符串值之外的另一个字段 —— 给老客户重大更改是不可避免的。 当然,您始终可以创建平行的 DoSomething_v2 方法,要带两个输入的参数,但久而久之会搞乱您的 Web 服务接口和消费者,服务变得越来越丑,用WCF实现的Web服务就是属于这种情况。
本文假定您有一些熟悉 WCF 和.NET 框架。 为了更好地展示WCF 概念可以如何转化为 ServiceStack 的概念,首先会在WCF中实现服务层。我会告诉你如何通过将WCF Web 服务移植到等效的使用 ServiceStack 转换为跨平台的 Web 服务。
ServiceStack 规定每个唯一的请求是对象所标识唯一的请求,这意味着你不能重用 DTO 跨多个服务实现与 ServiceStack 的请求。ServiceStack 支持不同的操作,如有 Get 和 Post。 您的选择在这里仅影响的 HTTP 请求。 指定任何 Web 服务请求是指可以通过 HTTP GET 和 HTTP POST 调用操作。 这种强制措施,简化了 rest 风格的 Web 服务实现。要将您的 ServiceStack Web 服务变成 rest 风格的 Web 服务,只需添加 URL [Route(...)]向您的 Web 服务请求声明属性。
以ASP.NET Hosting承载ServiceStack,创建一个空的ASP.NET应用,使用 NuGet 包管理器控制台将 ServiceStack 引用添加到 ServiceStack.Host.AspNet中所示
ServiceStack Web 应用程序启动时,您的服务合同列出作为元数据操作,如图所示:
WinVista新技术 WCF开发指南之构建服务
一. 引言 Windows通讯基础(简称为WCF)是一种SDK,用于让你使用典型的CLR编程结构(例如用于发布和消费服务的类和接口等)来构建Windows面向服务的应用程序。WCF的编程模型是声明性的并且大部分是属性驱动的。 WCF为通讯服务提供了一种运行时刻环境,使你能够把CLR类型暴露为服务并且以CLR类型来消费服务。 尽管在理论上你可以不用WCF来构建服务;但是,在实践中,WCF能够显著地简化这一任务。WCF是微软的一组工业标准的实现,该标准定义了服务交互、 类型转化、编排和多种协议的管理。因此,WCF提供了服务间的互操作性并且提高了开发效率(包括几乎任何应用程序所要求实现的基本的常规的繁重任务)。本 文将描述WCF块及其架构的基本概念和构建,从而使你能够构建简单的服务。 二. 什么是WCF服务? 一个服务是一个暴露给外界的功能单元。从编程模型的发展历史来说,它经历了从函数到对象再到组件最后到服务的过程;而WCF服务正代表了下一代的革命性 的Windows编程模型。面向服务(SO)是一组原则的抽象集和针对于构建SO应用程序的最好实践,但这其中的一大部分已经超出了本文的范围。 一个面向服务的应用程序(SOA)把服务聚合成单个逻辑的应用程序(见图1),这类似于一个面向组件的应用程序聚合组件或一个面向对象的应用程序聚合对 象的方式。服务可以是本地的也可以是远程的,可以由多种团队使用任何技术开发而成,它们可以被独立地进行版本化管理,甚至可以在不同的时间进度上执行。在 一个服务内部,你可以使用例如语言,技术,平台,版本和框架等概念;然而,在服务之间,只允许使用规定的通讯模式。
图1:一个面向服务的应用程序的框架。
客户端通过发送和接收消息与服务进行交互。消息可以从客户端直接或经中介传输到服务。在WCF中,所有的消息都是SOAP消息。注意,这些消息独立于传输协议——不象Web服务,WCF服务可以通过多种传输协议进行通讯,而不仅是HTTP。 在WCF中,客户端从不直接与服务进行交互,即使在当处理一个本地的内存中服务时。而是,客户端总是使用一个代理来把该调用转发给服务。WCF允许客户 端跨越所有执行边界与服务进行通讯。在同一台计算机上(见图2),客户端可以跨越同一进程中的应用程序域或进程来消费同一个应用程序域中的服务。通过跨越 计算机边界(图3),客户端能够在企业内网或跨越因特网与服务进行交互。
图3.跨机器通讯:这里是一个跨机器使用WCF通讯的例子。
图2.使用WCF在同一台机器上通讯。
因为所有的交互是经由一个代理实现的,所以对于本地和远程情况下,WCF保持相同的编程模型,这样以来不仅能够使你进行位置切换而不影响客户端,而且显 著地简化应用程序编程模型。大多数WCF功能被包括到位于System.ServiceModel命名空间的单个的程序集 System.ServiceModel.dll中。 三. 服务地址 在WCF中,每一个服务都与 唯一一个地址相联系。该地址提供了两个重要的元素:服务的位置和用于与服务进行通讯的传输协议。地址的位置部分指出目标计算机名,站点或网络,一个通讯端 口,管道或队列,还有一个可选的特定的路径或URI。至于传输,WCF 1.0支持下列: · HTTP · TCP · 端对端网络 · IPC(通过命名管道进行的进程间通讯) · MSMQ 地址总是使用如下格式:[base address]/[optional URI]
其中,基地址总是使用如下格式:[transport]://[machine or domain][:optional port]
下面是一些可能的服务地址:[url]http://localhost:8001[/url]
[url]http://localhost:8001/MyService[/url]
net.tcp://localhost:8002/MyService
net.pipe://localhost/MyPipe
net.msmq://localhost/private/MyService
四. 服务合同
在WCF中,所有的服务都暴露合同。合同是一种描述服务所实现功能的平台中立的标准的方式。WCF定义了四种类型的合同:
· 服务合同描述你可以在服务上执行哪些操作。
· 数据合同定义哪些数据类型被传入和传出服务。WCF为内置类型定义隐式合同,例如int和string,但是你可以容易地为定制类型定义显式的选入式数据合同。
· 错误合同定义哪些错误将被该服务所激发,以及该服务怎样处理错误信息和把如何把它们传播到客户端。
· 消息合同允许服务直接与消息进行交互。消息合同可以被类型化或非类型化,并且有点类似于CLR中的迟绑定调用。不过,消息合同很少为SOA开发者所用。
在这4种类型的合同中,本文将集中讨论服务合同。
你可以使用ServiceContractAttribute来定义一个服务合同,并且你可以把该属性应用于一个接口或一个类,如列表1(见本文相应下载源码)所示。
服务合同独立于接口或类可见性-公共或内部可见性是一个CLR概念,而不是WCF概念。在一个内部接口上应用 ServiceContractAttribute将把该接口暴露为一个公共服务合同(可以跨越服务边界进行消费)。没有 ServiceContractAttribute的话,该接口对WCF客户端是不可见的,这与面向服务的宗旨一致(服务边界是显式的)。为了强制实现这 一点,所有的合同必须是严格选入的。
OperationContractAttribute仅能被应用到方法(而不是属性,索引器或 事件,这都是一些CLR概念)中。OperationContractAttribute把一个合同方法暴露为在服务合同上执行的一种逻辑操作。该接口上 的其它不具有OperationContractAttribute属性的方法不会成为合同的一部分。这可以强制实现显式的服务边界,并且,对于操作本身 来说,保持一种选入模型。注意,合同操作独立于方法可见性。列表1展示了通过定义一个合同接口把服务合同与其实现分离开来的最好应用。
另外,你还可以直接把ServiceContractAttribute和OperationContractAttribute应用于类,在这种情况 下,WCF使用OperationContractAttribute从类中推断出一个服务合同和方法。这是一种应该尽量避免使用的技术: //尽量避免使用
[ServiceContract]
class MyService
{
[OperationContract] //可见性并不要紧
string MyMethod(string text)
{
return "Hello " + text;
}
public string MyOtherMethod(string text)
{
return "Cannot call this method over WCF";
}
}
这个ServiceContractAttribute把CLR接口(或推断的接口)映射到一个技术中立的WCF合同上。通过派生和实现多个带有 ServiceContractAttribute的接口,单个类可以支持多个合同。类能够通过隐式或显式方式实现这个接口,因为该方法可见性对WCF没 有任何影响。然而,存在许多实现约束:避免使用参数化的构造器,因为WCF仅使用默认的构造器。尽管该类能够使用内部属性,索引器和静态成员,但是没有 WCF客户端能够存取它们。五. 宿主
每个WCF服务必须宿主在一个Windows进程中(称为宿主进程)。单个宿主进程可以宿主多个服务,而相同的服务类型可以宿主在多个进程中。WCF并不要求是否该宿主进程也是客户端进程。
显然,应该有一个独立的进程支持错误和安全的隔离。另外,谁提供进程或调用哪种类型的进程都不是实质性的问题。这个宿主可以由IIS或Windows Vista中的Widows活动服务(WAS)或由开发者作为应用程序的一部分来提供。六. IIS宿主
在IIS中宿主一个服务的主要优点是,在发生客户端请求时宿主进程会被自动启动,并且你可以依靠IIS来管理宿主进程的生命周期。IIS宿主的主要不利 在于,你仅仅可以在IIS5和IIS6上使用HTTP传输数据;而且当使用IIS5时,你仅可以使用80端口。在IIS上宿主非常类似于宿主一个典型的 ASMX Web服务。你需要在IIS下创建一个虚拟的目录并且提供一个.svc文件。这个.svc文件的功能就象一个被用来标识服务的code-behind文件 和类的.asmx文件一样。<%@ ServiceHost
Language = "C#"
Debug = "true"
CodeBehind = "~/App_Code/MyService.cs"
Service = "MyService"
%>
你甚至可以把服务代码以内联方式注入到.svc文件中,但是不建议这样用(就象对于ASMX的情形一样)。一旦你准备好了.svc文件,你就可以使用一个浏览器来观看它。如果一切顺利,那么你将得到一个确认页面。
Visual Studio 2005能够为你生成一个新的IIS宿主的服务。这只要从File菜单下选择"New Website",然后从"New Web Site"对话框中选择WinFX服务。这使得Visual Studio 2005创建一个新的Web站点,服务代码和匹配的.svc文件。另外,Web站点配置文件必须列举出你想要暴露的服务类型。你需要使用完全限定类型名 (包括程序集名),如果类型来自于一个未引用的程序集的话。<system.serviceModel>
<services>
<service name="MyNamespace.MyService">
...
</service>
</services>
</system.serviceModel>
七. 自宿主
自宿主是当开发者负责提供和管理宿主进程的生命周期时使用的技术名词。自宿主被应用在位于客户端和服务之间的一个进程(或计算机)边界环境中,以及当使 用进程中服务的情况下(也就是说,与客户端处于相同的进程中)。你需要提供的进程可能是任何Windows进程,例如,一个Windows表单应用程序, 一个控制台应用程序或一个Windows NT服务。注意,该进程必须在客户端调用服务之前先运行起来;典型情况下,这意味着,你必须预先启动它。对于NT进程中服务来说这并不是一个问题。
类似于IIS宿主,宿主应用程序配置文件必须列出你想宿主的服务的类型并且暴露给外界。而且,该宿主进程必须在运行时刻显式地注册服务类型并且打开该宿主以便于客户端调用。典型地,这是在Main()方法中使用如下定义的助理类ServiceHost实现的:public interface ICommunicationObject : IDisposable
{
void Open();
void Close();
//更多成员
}
public abstract class CommunicationObject :
ICommunicationObject
{...}
public class ServiceHostBase : CommunicationObject,...
{...}
public class ServiceHost : ServiceHostBase,...
{
public ServiceHost(Type serviceType,
params Uri[]baseAddresses);
//更多成员
}
提供给ServiceHost的构造函数的信息有:服务类型和(可选)默认的基地址。该基地址集可以是一个空集(以后,你可以配置不同的基地址)。拥有 一组基地址能够使服务接受在多个地址和协议上的调用。注意,每个ServiceHost实例都关联与一个特定的服务类型,并且如果宿主进程需要宿主多个类 型的服务的话,你需要一些匹配的ServiceHost实例。通过调用宿主中的ServiceHost.Open()方法,你允许调入(call- in);并且通过调用ServiceHost.Close()方法,你可以体面地退出宿主实例并完成到当前客户端的数据发送,并且还要拒绝未来的客户端调 用-即使宿主进程仍在运行中。典型地,关闭操作是在宿主进程关闭时实现的。例如,为了把这个服务宿主在一个Windows表单应用程序中:[ServiceContract]
interface IMyContract
{...}
class MyService : IMyContract
{...}
你可以编写:
public static void Main()
{
Uri baseAddress = new Uri("http://localhost:8000/");
ServiceHost serviceHost;
serviceHost = new ServiceHost(typeof(MyService),baseAddress);
serviceHost.Open();
//能够拦截调用:
Application.Run(new MyForm());
serviceHost.Close();
}
注意,你可以在调用ServiceHost.Open()之后拦截调用,因为该宿主接收在工作者线程上的所有调用。对ServiceHost.Open()的调用将加载WCF运行时刻并且支持接收客户端调用。该宿主能注册多个基地址,只要它们至少在传输方面存在不同:Uri tcpBaseAddress = new Uri("net.tcp://localhost:8001/");
Uri httpBaseAddress = new Uri("http://localhost:8002/");
ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress,httpBaseAddress);
通过从"Add New Item"对话框中选择WCF服务,Visual Studio 2005允许你把一个WCF服务添加到任何应用程序工程。以此方式添加的服务当然是进程中服务(相对于宿主进程来说),但是也可以由外部客户端存取。 八. WAS宿主
Windows活动服务(WAS)是可用于Windows Vista中的一种系统服务。WAS是IIS7的一部分,但是可以被独立地配置。为了使用WAS来宿主通讯WCF服务,你需要提供一个.svc文件。该 WAS提供与IIS和自宿主相比更多的优点,包括空闲时间管理、标识管理、应用程序池、隔离等等,并且是选择的宿主进程(在可用的情况下)。而且,这种自 宿主的进程为进程内宿主提供独特的优点:处理未知的客户环境、依赖于TCP或IPC(当只有IIS可用时)、利用HTTP上的多个端口(当只有IIS 6可用时)以及通过编程方式存取一些高级宿主特征。 九. 绑定
与任何给定的服务进行通讯都存在多方面的问题。首先,存在许多可能的通讯模式:消息可能是同步请求/响应或异步式的"激活-忘记"模式(fire-and-forget);消息也可能是双向的;消息能够被立即传输或队列化操作而该队列可能是持久性的或易破坏性的。
还存在许多可能的消息传输协议:例如HTTP(或HTTPS),TCP,P2P(端对端网络),IPC(命名管道)或MSMQ。也存在一些可能的消息编 码选项:你可以选择普通文本以支持互操作性,二进制编码以便于优化性能,或MTOM(消息传输优化机制)以便处理巨大载荷。
也存在一 些消息保护选项:你可以选择不对之进行保护,你也可以使用它们来仅提供传输级安全或提供消息级隐私和安全,并且当然,也存在很多种方式用于对客户端实现认 证和授权。消息传输在跨越中间媒体和中断连接时可能是不可靠的或可靠的端到端式,并且消息可能是以其发送的方式传输的或是以其接收方式传输的。
通讯服务可能需要与其它服务或仅能够使用基本Web服务协议的客户端互操作,或者它们能够使用WS-*现代协议的核心(例如WS-安全和WS-原子事 务)。通讯服务可能需要与旧式的客户端通过原始的MSMQ消息进行互操作,或你可能想限制通讯服务以便仅与另一个WCF服务或客户端互操作。
简言之,通讯存在许多方面的内容,包括大量的参数和决策点。其中,一些选择可能是互斥的,而另一些选择可能要求必须使用另外的相应选择。很明显,客户端 和服务必须在所有这些选项上相吻合,以达到正确交流的目的。为了简化并使其更具可管理性,WCF小组共同在绑定中提供了一个这样的通讯方面集合。
个绑定仅仅是对于相协调的传输协议、消息编码、通讯模式、可靠性、安全性、事务传播和互操作性的预封装。理想情况下,你能够从通讯服务代码中"提取"所 有的这些繁重的任务方面并且允许它专注于实现业务逻辑。这样做可以使你在相当不同的繁重任务方面使用相同的服务逻辑,而绑定正好使你能够实现这一目的。
你可以使用WCF提供的绑定,也就是说,你或者可以"浓缩"它们的属性,或者是从头编写通讯自己的定制绑定。一个服务在它的元数据中出版它的绑定选择, 这使得客户端能够查询这种类型和绑定的特定属性,因为客户端必须使用与服务完全一样的绑定。单个服务能够支持在独立的地址上的多个绑定。
通常,服务并不指定关于绑定本身。WCF定义了列举于表格1中的共9种标准绑定。基于文本的编码使一个WCF服务(或客户端)能够通过HTTP与任何其 它服务(或客户端)进行交流而不考虑它的技术;然而,通过TCP或IPC的二进制编码传输能够产生最优的性能,但是却以失去极广泛的互操作性为代价(因为 它必须使用WCF到WCF的通讯)。
表格1:WCF标准绑定
名称
传输
编码
Interop
BasicHttpBinding
HTTP/HTTPS
Text
+
NetTcpBinding
TCP
Binary
-
NetPeerTcpBinding
P2P
Binary
-
NetNamedPipeBinding
IPC
Binary
-
WSHttpBinding
HTTP/HTTPS
Text,MTOM
+
WSFederationBinding
HTTP/HTTPS
Text,MTOM
+
WSDualHttpBinding
HTTP
Text,MTOM
+
NetMsmqBinding
MSMQ
Binary
-
MsmqIntegrationBinding
MSMQ
Binary
+
为一个传输协议选择MSMQ能够强制实现WCF到WCF或WCF到MSQM的通讯,但是,这仅是针对非连接的离线工作情况提供的。典型情况下,为通讯服务选择一个绑定应该遵循如图4所示的策略活动图。
图4.策略活动图:该图展示了选择一个绑定的过程。
你应该问自己的第一个问题是,是否通讯服务需要与非WCF客户进行交互。如果回答"是",并且如果客户端是一个旧的MSMQ客户端,那么应该选择 NetMsmqBinding-它可以使通讯服务与这样的一个客户端通过MSMQ进行互操作。如果你需要与一非WCF客户端进行互操作并且该客户端期望使 用基本的Web服务协议(ASMX Web服务),那么,你可以选择BasicHttpBinding-它能够把通讯WCF服务暴露到外界,就好象它是一个ASMX Web服务一样。
缺点是你不能利用任何现代WS-*协议。然而,如果非WCF客户端能理解这些标准,那么,你可以选择WS绑定之一,例如WSHttpBinding, WSFederationBinding或WSDualHttpBinding。如果你可以假定客户端是一个WCF客户端,但它要求离线或非连接性交互, 那么你可以选择使用MSMQ的NetMsmqBinding来传输消息。如果客户端需要连接的通讯但能够跨越计算机边界被调用,那么你可以选择通过TCP 进行通讯的NetTcpBinding。
如果客户端位于与服务同一台计算机上,那么你可以选择使用命名管道的 NetNamedPipeBinding来(IPC)最优化性能。注意,一个使用NetNamedPipeBinding的服务不能接受除它自己以外的来 自任何其它计算机的调用,并且这样也会更为安全。你可以基于其它标准(例如,回调需要(WSDualHttpBinding),端对端网络 (NetPeerTcpBinding)或联盟安全(WSFederationBinding))来详细地调整绑定选择。十. 端点
每一个服务都关联于一个定义了该服务所在位置的地址,一个定义了如何与服务进行通讯的绑定和一个定义了该服务所实现功能的合同。
事实上,WCF用端点的形式来形式化描述这种关系。该端点是地址、合同和绑定的一个结合(见图5)。每一个服务必须具有三个端点,而且由服务暴露该端点。从逻辑上讲,端点是服务的接口,并且类似于一个CLR或COM接口。
图5.该端点是地址、合同和绑定的结合
每一个服务必须暴露至少一个业务端点,并且每一个端点都具有一个相同的合同。在一个服务上的所有的端点都具有唯一的地址,而单个服务可以暴露多个端点。 这些端点能够使用相同的或不同的绑定并能暴露相同的或不同的合同。你可以使用一个配置文件来以管理方式配置端点或以编程方式来实现端点配置。
十一. 管理端点配置
请考虑下列服务定义:namespace MyNamespace
{
[ServiceContract]
interface IMyContract
{...}
Class MyService : IMyContract
{...}
}
列表2(见本文相应下载源码)展示了在宿主进程配置文件中要求的入口。管理配置是在大多数情况下的配置,因为它提供灵活性来实现改变服务地址、绑定甚至暴露合同而不必重新构建和重新发布服务。
源码中的列表3展示了一个配置文件-它定义暴露多个端点的单个服务。注意,这些端点必须提供一个与绑定相一致的基地址(例如,对于HTTP使用 WSHttpBinding绑定)。每一个不匹配都会导致在服务加载时刻抛出一个异常。只要URI是不同的,那么你可以使用相同的基地址来配置多个端点:<service name="MyNamespace.MyService">
<endpoint Address =
"net.tcp://localhost:8001/Service1/"
...
/>
<endpoint
address="net.tcp://localhost:8001/Service2/"
...
/>
</service>
还可以省略地址-在这种情况下,该服务使用与宿主一起注册的基地址(宿主必须提供一个匹配的基地址):<endpoint
binding="wsHttpBinding"
contract="MyNamespace.IMyContract"
/>
可以仅提供一个URI-在这种情况下,地址是在基地址下的相对地址(并且宿主必须提供一个匹配的基地址):<endpoint
address="SubAddress"
...
/>
当提供一个基地址时,该端点覆盖宿主所提供的任何基地址:<endpoint
address="http://localhost:8000/MyService/"
...
/>
注意,当使用IIS进行宿主时,服务必须使用IIS基地址(在HTTP中使用计算机名+虚拟目录)。
十二. 端点配置编程
以编程方式实现端点配置完全等价于管理配置;然而,它不必依赖于一个配置文件而是可以通过编程调用来把端点添加到ServiceHost实例。再次强调 的是,这些调用总是位于服务代码的范围之外。ServiceHost提供AddServiceEndpoint()方法的重载版本:public class ServiceHost : ServiceHostBase
{
public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding,String address);
//另外的成员
}
列表4展示了与在列表3中的端点一样的可编程的配置。为了依赖宿主基地址,只需要提供如地址一样的URI即可:Uri tcpBaseAddress = new Uri("http://localhost:8000/");
ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress);
Binding tcpBinding = new NetTcpBinding();
//使用基地址作为地址
serviceHost.AddServiceEndpoint(typeof(IMyContract) ,tcpBinding,"");
//添加相对地址
serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding,"MyService");
//忽略基地址
serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding,
"net.tcp://localhost:8001/MyService");
serviceHost.Open();
十三. 小结
在本篇中,我们全面介绍了构建一个WCF服务所需要的基本概念,有关完整的WCF服务的例子请参考本文相应源码。在下篇中,我们将给出一个使用WCF进行Windows开发的客户端案例分析。
本文转自朱先忠老师51CTO博客,原文链接:http://blog.51cto.com/zhuxianzhong/60086 ,如需转载请自行联系原作者
Vista新技术:WCF开发指南之构建服务
一. 引言 Windows通讯基础(简称为WCF)是一种SDK,用于让你使用典型的CLR编程结构(例如用于发布和消费服务的类和接口等)来构建Windows面向服务的应用程序。WCF的编程模型是声明性的并且大部分是属性驱动的。 WCF为通讯服务提供了一种运行时刻环境,使你能够把CLR类型暴露为服务并且以CLR类型来消费服务。 尽管在理论上你可以不用WCF来构建服务;但是,在实践中,WCF能够显著地简化这一任务。WCF是微软的一组工业标准的实现,该标准定义了服务交互、 类型转化、编排和多种协议的管理。因此,WCF提供了服务间的互操作性并且提高了开发效率(包括几乎任何应用程序所要求实现的基本的常规的繁重任务)。本 文将描述WCF块及其架构的基本概念和构建,从而使你能够构建简单的服务。 二. 什么是WCF服务? 一个服务是一个暴露给外界的功能单元。从编程模型的发展历史来说,它经历了从函数到对象再到组件最后到服务的过程;而WCF服务正代表了下一代的革命性 的Windows编程模型。面向服务(SO)是一组原则的抽象集和针对于构建SO应用程序的最好实践,但这其中的一大部分已经超出了本文的范围。 一个面向服务的应用程序(SOA)把服务聚合成单个逻辑的应用程序(见图1),这类似于一个面向组件的应用程序聚合组件或一个面向对象的应用程序聚合对 象的方式。服务可以是本地的也可以是远程的,可以由多种团队使用任何技术开发而成,它们可以被独立地进行版本化管理,甚至可以在不同的时间进度上执行。在 一个服务内部,你可以使用例如语言,技术,平台,版本和框架等概念;然而,在服务之间,只允许使用规定的通讯模式。
图1:一个面向服务的应用程序的框架。
客户端通过发送和接收消息与服务进行交互。消息可以从客户端直接或经中介传输到服务。在WCF中,所有的消息都是SOAP消息。注意,这些消息独立于传输协议——不象Web服务,WCF服务可以通过多种传输协议进行通讯,而不仅是HTTP。 在WCF中,客户端从不直接与服务进行交互,即使在当处理一个本地的内存中服务时。而是,客户端总是使用一个代理来把该调用转发给服务。WCF允许客户 端跨越所有执行边界与服务进行通讯。在同一台计算机上(见图2),客户端可以跨越同一进程中的应用程序域或进程来消费同一个应用程序域中的服务。通过跨越 计算机边界(图3),客户端能够在企业内网或跨越因特网与服务进行交互。
图3.跨机器通讯:这里是一个跨机器使用WCF通讯的例子。
图2.使用WCF在同一台机器上通讯。
因为所有的交互是经由一个代理实现的,所以对于本地和远程情况下,WCF保持相同的编程模型,这样以来不仅能够使你进行位置切换而不影响客户端,而且显 著地简化应用程序编程模型。大多数WCF功能被包括到位于System.ServiceModel命名空间的单个的程序集 System.ServiceModel.dll中。 三. 服务地址 在WCF中,每一个服务都与 唯一一个地址相联系。该地址提供了两个重要的元素:服务的位置和用于与服务进行通讯的传输协议。地址的位置部分指出目标计算机名,站点或网络,一个通讯端 口,管道或队列,还有一个可选的特定的路径或URI。至于传输,WCF 1.0支持下列: · HTTP · TCP · 端对端网络 · IPC(通过命名管道进行的进程间通讯) · MSMQ 地址总是使用如下格式:[base address]/[optional URI]
其中,基地址总是使用如下格式:[transport]://[machine or domain][:optional port]
下面是一些可能的服务地址:[url]http://localhost:8001[/url][url]http://localhost:8001/MyService[/url]net.tcp://localhost:8002/MyServicenet.pipe://localhost/MyPipenet.msmq://localhost/private/MyService 四. 服务合同 在WCF中,所有的服务都暴露合同。合同是一种描述服务所实现功能的平台中立的标准的方式。WCF定义了四种类型的合同: · 服务合同描述你可以在服务上执行哪些操作。 · 数据合同定义哪些数据类型被传入和传出服务。WCF为内置类型定义隐式合同,例如int和string,但是你可以容易地为定制类型定义显式的选入式数据合同。 · 错误合同定义哪些错误将被该服务所激发,以及该服务怎样处理错误信息和把如何把它们传播到客户端。 · 消息合同允许服务直接与消息进行交互。消息合同可以被类型化或非类型化,并且有点类似于CLR中的迟绑定调用。不过,消息合同很少为SOA开发者所用。 在这4种类型的合同中,本文将集中讨论服务合同。 你可以使用ServiceContractAttribute来定义一个服务合同,并且你可以把该属性应用于一个接口或一个类,如列表1(见本文相应下载源码)所示。 服务合同独立于接口或类可见性-公共或内部可见性是一个CLR概念,而不是WCF概念。在一个内部接口上应用 ServiceContractAttribute将把该接口暴露为一个公共服务合同(可以跨越服务边界进行消费)。没有 ServiceContractAttribute的话,该接口对WCF客户端是不可见的,这与面向服务的宗旨一致(服务边界是显式的)。为了强制实现这 一点,所有的合同必须是严格选入的。 OperationContractAttribute仅能被应用到方法(而不是属性,索引器或 事件,这都是一些CLR概念)中。OperationContractAttribute把一个合同方法暴露为在服务合同上执行的一种逻辑操作。该接口上 的其它不具有OperationContractAttribute属性的方法不会成为合同的一部分。这可以强制实现显式的服务边界,并且,对于操作本身 来说,保持一种选入模型。注意,合同操作独立于方法可见性。列表1展示了通过定义一个合同接口把服务合同与其实现分离开来的最好应用。 另外,你还可以直接把ServiceContractAttribute和OperationContractAttribute应用于类,在这种情况 下,WCF使用OperationContractAttribute从类中推断出一个服务合同和方法。这是一种应该尽量避免使用的技术: //尽量避免使用
[ServiceContract]
class MyService
{
[OperationContract] //可见性并不要紧
string MyMethod(string text)
{
return "Hello " + text;
}
public string MyOtherMethod(string text)
{
return "Cannot call this method over WCF";
}
}
这个ServiceContractAttribute把CLR接口(或推断的接口)映射到一个技术中立的WCF合同上。通过派生和实现多个带有 ServiceContractAttribute的接口,单个类可以支持多个合同。类能够通过隐式或显式方式实现这个接口,因为该方法可见性对WCF没 有任何影响。然而,存在许多实现约束:避免使用参数化的构造器,因为WCF仅使用默认的构造器。尽管该类能够使用内部属性,索引器和静态成员,但是没有 WCF客户端能够存取它们。 五. 宿主 每个WCF服务必须宿主在一个Windows进程中(称为宿主进程)。单个宿主进程可以宿主多个服务,而相同的服务类型可以宿主在多个进程中。WCF并不要求是否该宿主进程也是客户端进程。 显然,应该有一个独立的进程支持错误和安全的隔离。另外,谁提供进程或调用哪种类型的进程都不是实质性的问题。这个宿主可以由IIS或Windows Vista中的Widows活动服务(WAS)或由开发者作为应用程序的一部分来提供。 六. IIS宿主 在IIS中宿主一个服务的主要优点是,在发生客户端请求时宿主进程会被自动启动,并且你可以依靠IIS来管理宿主进程的生命周期。IIS宿主的主要不利 在于,你仅仅可以在IIS5和IIS6上使用HTTP传输数据;而且当使用IIS5时,你仅可以使用80端口。在IIS上宿主非常类似于宿主一个典型的 ASMX Web服务。你需要在IIS下创建一个虚拟的目录并且提供一个.svc文件。这个.svc文件的功能就象一个被用来标识服务的code-behind文件 和类的.asmx文件一样。<%@ ServiceHost
Language = "C#"
Debug = "true"
CodeBehind = "~/App_Code/MyService.cs"
Service = "MyService"
%>
你甚至可以把服务代码以内联方式注入到.svc文件中,但是不建议这样用(就象对于ASMX的情形一样)。一旦你准备好了.svc文件,你就可以使用一个浏览器来观看它。如果一切顺利,那么你将得到一个确认页面。 Visual Studio 2005能够为你生成一个新的IIS宿主的服务。这只要从File菜单下选择"New Website",然后从"New Web Site"对话框中选择WinFX服务。这使得Visual Studio 2005创建一个新的Web站点,服务代码和匹配的.svc文件。另外,Web站点配置文件必须列举出你想要暴露的服务类型。你需要使用完全限定类型名 (包括程序集名),如果类型来自于一个未引用的程序集的话。<system.serviceModel>
<services>
<service name="MyNamespace.MyService">
...
</service>
</services>
</system.serviceModel>
七. 自宿主 自宿主是当开发者负责提供和管理宿主进程的生命周期时使用的技术名词。自宿主被应用在位于客户端和服务之间的一个进程(或计算机)边界环境中,以及当使 用进程中服务的情况下(也就是说,与客户端处于相同的进程中)。你需要提供的进程可能是任何Windows进程,例如,一个Windows表单应用程序, 一个控制台应用程序或一个Windows NT服务。注意,该进程必须在客户端调用服务之前先运行起来;典型情况下,这意味着,你必须预先启动它。对于NT进程中服务来说这并不是一个问题。 类似于IIS宿主,宿主应用程序配置文件必须列出你想宿主的服务的类型并且暴露给外界。而且,该宿主进程必须在运行时刻显式地注册服务类型并且打开该宿主以便于客户端调用。典型地,这是在Main()方法中使用如下定义的助理类ServiceHost实现的:public interface ICommunicationObject : IDisposable
{
void Open();
void Close();
//更多成员
}
public abstract class CommunicationObject :
ICommunicationObject
{...}
public class ServiceHostBase : CommunicationObject,...
{...}
public class ServiceHost : ServiceHostBase,...
{
public ServiceHost(Type serviceType,
params Uri[]baseAddresses);
//更多成员
}
提供给ServiceHost的构造函数的信息有:服务类型和(可选)默认的基地址。该基地址集可以是一个空集(以后,你可以配置不同的基地址)。拥有 一组基地址能够使服务接受在多个地址和协议上的调用。注意,每个ServiceHost实例都关联与一个特定的服务类型,并且如果宿主进程需要宿主多个类 型的服务的话,你需要一些匹配的ServiceHost实例。通过调用宿主中的ServiceHost.Open()方法,你允许调入(call- in);并且通过调用ServiceHost.Close()方法,你可以体面地退出宿主实例并完成到当前客户端的数据发送,并且还要拒绝未来的客户端调 用-即使宿主进程仍在运行中。典型地,关闭操作是在宿主进程关闭时实现的。例如,为了把这个服务宿主在一个Windows表单应用程序中:[ServiceContract]
interface IMyContract
{...}
class MyService : IMyContract
{...}
你可以编写:
public static void Main()
{
Uri baseAddress = new Uri("http://localhost:8000/");
ServiceHost serviceHost;
serviceHost = new ServiceHost(typeof(MyService),baseAddress);
serviceHost.Open();
//能够拦截调用:
Application.Run(new MyForm());
serviceHost.Close();
}
注意,你可以在调用ServiceHost.Open()之后拦截调用,因为该宿主接收在工作者线程上的所有调用。对ServiceHost.Open()的调用将加载WCF运行时刻并且支持接收客户端调用。该宿主能注册多个基地址,只要它们至少在传输方面存在不同:Uri tcpBaseAddress = new Uri("net.tcp://localhost:8001/");
Uri httpBaseAddress = new Uri("http://localhost:8002/");
ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress,httpBaseAddress);
通过从"Add New Item"对话框中选择WCF服务,Visual Studio 2005允许你把一个WCF服务添加到任何应用程序工程。以此方式添加的服务当然是进程中服务(相对于宿主进程来说),但是也可以由外部客户端存取。 八. WAS宿主 Windows活动服务(WAS)是可用于Windows Vista中的一种系统服务。WAS是IIS7的一部分,但是可以被独立地配置。为了使用WAS来宿主通讯WCF服务,你需要提供一个.svc文件。该 WAS提供与IIS和自宿主相比更多的优点,包括空闲时间管理、标识管理、应用程序池、隔离等等,并且是选择的宿主进程(在可用的情况下)。而且,这种自 宿主的进程为进程内宿主提供独特的优点:处理未知的客户环境、依赖于TCP或IPC(当只有IIS可用时)、利用HTTP上的多个端口(当只有IIS 6可用时)以及通过编程方式存取一些高级宿主特征。 九. 绑定 与任何给定的服务进行通讯都存在多方面的问题。首先,存在许多可能的通讯模式:消息可能是同步请求/响应或异步式的"激活-忘记"模式(fire-and-forget);消息也可能是双向的;消息能够被立即传输或队列化操作而该队列可能是持久性的或易破坏性的。 还存在许多可能的消息传输协议:例如HTTP(或HTTPS),TCP,P2P(端对端网络),IPC(命名管道)或MSMQ。也存在一些可能的消息编 码选项:你可以选择普通文本以支持互操作性,二进制编码以便于优化性能,或MTOM(消息传输优化机制)以便处理巨大载荷。 也存在一 些消息保护选项:你可以选择不对之进行保护,你也可以使用它们来仅提供传输级安全或提供消息级隐私和安全,并且当然,也存在很多种方式用于对客户端实现认 证和授权。消息传输在跨越中间媒体和中断连接时可能是不可靠的或可靠的端到端式,并且消息可能是以其发送的方式传输的或是以其接收方式传输的。 通讯服务可能需要与其它服务或仅能够使用基本Web服务协议的客户端互操作,或者它们能够使用WS-*现代协议的核心(例如WS-安全和WS-原子事 务)。通讯服务可能需要与旧式的客户端通过原始的MSMQ消息进行互操作,或你可能想限制通讯服务以便仅与另一个WCF服务或客户端互操作。 简言之,通讯存在许多方面的内容,包括大量的参数和决策点。其中,一些选择可能是互斥的,而另一些选择可能要求必须使用另外的相应选择。很明显,客户端 和服务必须在所有这些选项上相吻合,以达到正确交流的目的。为了简化并使其更具可管理性,WCF小组共同在绑定中提供了一个这样的通讯方面集合。 个绑定仅仅是对于相协调的传输协议、消息编码、通讯模式、可靠性、安全性、事务传播和互操作性的预封装。理想情况下,你能够从通讯服务代码中"提取"所 有的这些繁重的任务方面并且允许它专注于实现业务逻辑。这样做可以使你在相当不同的繁重任务方面使用相同的服务逻辑,而绑定正好使你能够实现这一目的。 你可以使用WCF提供的绑定,也就是说,你或者可以"浓缩"它们的属性,或者是从头编写通讯自己的定制绑定。一个服务在它的元数据中出版它的绑定选择, 这使得客户端能够查询这种类型和绑定的特定属性,因为客户端必须使用与服务完全一样的绑定。单个服务能够支持在独立的地址上的多个绑定。 通常,服务并不指定关于绑定本身。WCF定义了列举于表格1中的共9种标准绑定。基于文本的编码使一个WCF服务(或客户端)能够通过HTTP与任何其 它服务(或客户端)进行交流而不考虑它的技术;然而,通过TCP或IPC的二进制编码传输能够产生最优的性能,但是却以失去极广泛的互操作性为代价(因为 它必须使用WCF到WCF的通讯)。 表格1:WCF标准绑定
名称
传输
编码
Interop
BasicHttpBinding
HTTP/HTTPS
Text
+
NetTcpBinding
TCP
Binary
-
NetPeerTcpBinding
P2P
Binary
-
NetNamedPipeBinding
IPC
Binary
-
WSHttpBinding
HTTP/HTTPS
Text,MTOM
+
WSFederationBinding
HTTP/HTTPS
Text,MTOM
+
WSDualHttpBinding
HTTP
Text,MTOM
+
NetMsmqBinding
MSMQ
Binary
-
MsmqIntegrationBinding
MSMQ
Binary
+
为一个传输协议选择MSMQ能够强制实现WCF到WCF或WCF到MSQM的通讯,但是,这仅是针对非连接的离线工作情况提供的。典型情况下,为通讯服务选择一个绑定应该遵循如图4所示的策略活动图。
图4.策略活动图:该图展示了选择一个绑定的过程。
你应该问自己的第一个问题是,是否通讯服务需要与非WCF客户进行交互。如果回答"是",并且如果客户端是一个旧的MSMQ客户端,那么应该选择 NetMsmqBinding-它可以使通讯服务与这样的一个客户端通过MSMQ进行互操作。如果你需要与一非WCF客户端进行互操作并且该客户端期望使 用基本的Web服务协议(ASMX Web服务),那么,你可以选择BasicHttpBinding-它能够把通讯WCF服务暴露到外界,就好象它是一个ASMX Web服务一样。 缺点是你不能利用任何现代WS-*协议。然而,如果非WCF客户端能理解这些标准,那么,你可以选择WS绑定之一,例如WSHttpBinding, WSFederationBinding或WSDualHttpBinding。如果你可以假定客户端是一个WCF客户端,但它要求离线或非连接性交互, 那么你可以选择使用MSMQ的NetMsmqBinding来传输消息。如果客户端需要连接的通讯但能够跨越计算机边界被调用,那么你可以选择通过TCP 进行通讯的NetTcpBinding。 如果客户端位于与服务同一台计算机上,那么你可以选择使用命名管道的 NetNamedPipeBinding来(IPC)最优化性能。注意,一个使用NetNamedPipeBinding的服务不能接受除它自己以外的来 自任何其它计算机的调用,并且这样也会更为安全。你可以基于其它标准(例如,回调需要(WSDualHttpBinding),端对端网络 (NetPeerTcpBinding)或联盟安全(WSFederationBinding))来详细地调整绑定选择。 十. 端点 每一个服务都关联于一个定义了该服务所在位置的地址,一个定义了如何与服务进行通讯的绑定和一个定义了该服务所实现功能的合同。 事实上,WCF用端点的形式来形式化描述这种关系。该端点是地址、合同和绑定的一个结合(见图5)。每一个服务必须具有三个端点,而且由服务暴露该端点。从逻辑上讲,端点是服务的接口,并且类似于一个CLR或COM接口。
图5.该端点是地址、合同和绑定的结合
每一个服务必须暴露至少一个业务端点,并且每一个端点都具有一个相同的合同。在一个服务上的所有的端点都具有唯一的地址,而单个服务可以暴露多个端点。 这些端点能够使用相同的或不同的绑定并能暴露相同的或不同的合同。你可以使用一个配置文件来以管理方式配置端点或以编程方式来实现端点配置。 十一. 管理端点配置 请考虑下列服务定义:namespace MyNamespace
{
[ServiceContract]
interface IMyContract
{...}
Class MyService : IMyContract
{...}
}
列表2(见本文相应下载源码)展示了在宿主进程配置文件中要求的入口。管理配置是在大多数情况下的配置,因为它提供灵活性来实现改变服务地址、绑定甚至暴露合同而不必重新构建和重新发布服务。 源码中的列表3展示了一个配置文件-它定义暴露多个端点的单个服务。注意,这些端点必须提供一个与绑定相一致的基地址(例如,对于HTTP使用 WSHttpBinding绑定)。每一个不匹配都会导致在服务加载时刻抛出一个异常。只要URI是不同的,那么你可以使用相同的基地址来配置多个端点:<service name="MyNamespace.MyService">
<endpoint Address =
"net.tcp://localhost:8001/Service1/"
...
/>
<endpoint
address="net.tcp://localhost:8001/Service2/"
...
/>
</service>
还可以省略地址-在这种情况下,该服务使用与宿主一起注册的基地址(宿主必须提供一个匹配的基地址):<endpoint
binding="wsHttpBinding"
contract="MyNamespace.IMyContract"
/>
可以仅提供一个URI-在这种情况下,地址是在基地址下的相对地址(并且宿主必须提供一个匹配的基地址):<endpoint
address="SubAddress"
...
/>
当提供一个基地址时,该端点覆盖宿主所提供的任何基地址:<endpoint
address="http://localhost:8000/MyService/"
...
/>
注意,当使用IIS进行宿主时,服务必须使用IIS基地址(在HTTP中使用计算机名+虚拟目录)。 十二. 端点配置编程 以编程方式实现端点配置完全等价于管理配置;然而,它不必依赖于一个配置文件而是可以通过编程调用来把端点添加到ServiceHost实例。再次强调 的是,这些调用总是位于服务代码的范围之外。ServiceHost提供AddServiceEndpoint()方法的重载版本:public class ServiceHost : ServiceHostBase
{
public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding,String address);
//另外的成员
}
列表4展示了与在列表3中的端点一样的可编程的配置。为了依赖宿主基地址,只需要提供如地址一样的URI即可:Uri tcpBaseAddress = new Uri("http://localhost:8000/");
ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress);
Binding tcpBinding = new NetTcpBinding();
//使用基地址作为地址
serviceHost.AddServiceEndpoint(typeof(IMyContract) ,tcpBinding,"");
//添加相对地址
serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding,"MyService");
//忽略基地址
serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding,
"net.tcp://localhost:8001/MyService");
serviceHost.Open();
十三. 小结 在本篇中,我们全面介绍了构建一个WCF服务所需要的基本概念,有关完整的WCF服务的例子请参考本文相应源码。在下篇中,我们将给出一个使用WCF进行Windows开发的客户端案例分析。
本文转自朱先忠老师51CTO博客,原文链接: http://blog.51cto.com/zhuxianzhong/60087,如需转载请自行联系原作者
WCF分布式开发步步为赢(1):WCF分布式框架基础概念
众所周知,系统间的低耦合一直是大型企业应用系统集成追寻的目标,SOA面向服务架构的出现为我们的如何利用现有企业系统资源进行企业ERP系统设计和实现提供了重要的参考原则。SOA如此炙手可热,各大厂商都推出了自己的中间件产品,比如Oracle Fusion和 SAP NetWeaver,IBM、BEA等企业也推出了自己基于SOA的解决方案。基于J2EE平台的SOA架构设计中的一个重要概念就是EJB企业服务总线,作用是实现各个系统的数据交互。而.NET平台上,WCF就是微软为各个系统的数据交互提供通讯基础框架。由于论文课题的原因,我对产生了SOA(Service-oriented architecture,面向服务架构)有了接触,并且产生了浓厚的兴趣。故此对.NET平台上基于SOA的企业应用系统集成和实现技术进行潜心学习,总结心得和体会,整理出布式开发的系列文章,与大家分享。
下面介绍一下《WCF分布式开发步步为赢》系列文章的结构。首先会是WCF分布式开发基础概念的介绍,其次会设计各个知识点详细的介绍和应用,中间会穿插与以前技术的对比分析,涉及服务契约、数据契约、操作契约、事物、安全、消息队列等重要概念。同样作为老徐一贯的风格,我会尽量做到文章的调理清晰、系统详细、通俗易懂!也会放出标注的详细代码供大家参考学习。 本节文章的结构是:1.WCF基础概念介绍2.WCF新的特性3.WCF框架模型4.实现代码分析5.总结。
【1】WCF基础概念介绍
要学习WCF,首先我们要清楚WCF的基本概念,那么什么是WCF呢?
WCF全称Windows Communication Foundation,是Microsoft为构建面向服务的应用提供的分布式通信编程框架,是.NET Framework 3.5的重要组成部分。使用该框架,开发人员可以构建跨平台、安全、可靠和支持事务处理的企业级互联应用解决方案。
根据微软官方的解释,WCF(之前的版本名为“Indigo”)是使用托管代码建立和运行面向服务(Service Oriented)应用程序的统一框架。它使得开发者能够建立一个跨平台的安全、可信赖、事务性的解决方案,且能与已有系统兼容协作。WCF是微软分布式应用程序开发的集大成者,它整合了.Net平台下所有的和分布式系统有关的技术,如Enterprise Sevices(COM+).Net Remoting、Web Service(ASMX)、WSE3.0和MSMQ消息队列。以通信(Communiation)范围而论,它可以跨进程、跨机器、跨子网、企业网乃至于 Internet;以宿主程序而论,可以以ASP.NET,EXE,WPF,Windows Forms,NT Service,COM+作为宿主(Host)。WCF可以支持的协议包括TCP,HTTP,跨进程以及自定义,安全模式则包括SAML, Kerberos,X509,用户/密码,自定义等多种标准与模式。也就是说,在WCF框架下,开发基于SOA的分布式系统变得容易了,微软将所有与此相关的技术要素都包含在内,掌握了WCF,就相当于掌握了叩开SOA大门的钥匙。(参考MSDN)
这里SOA概念我们要明确一下,SOA(Service-oriented architecture,面向服务架构)是一种架构模式和设计原则。在<Programming WCF Services >By Juval Lowy 一书中解释为SOA(Service-oriented applicatoin)面向服务的应用程序,两者应该不冲突,后者应该是采用SOA面向服务架构设计和实现的应用程序(Service-oriented applicatoin)。
WCF的强大之处就是因为整合了.Net平台下所有的和分布式系统有关的Enterprise Sevices(COM+).Net Remoting、Web Service(ASMX)、WSE3.0和MSMQ消息队列等技术,我在之前的WCF分布式开发必备知识和WSE3.0构建Web服务安全系列文章中都给出了详细的介绍,这里就不在详述,有兴趣的朋友可以自己查阅。以后具体介绍WCF相关知识点的时候需要的地方我会在进行讲解。
【2】WCF新的特性
Indigo 有三项突出的特性:与多种现有 Microsoft 技术的统一性,对跨供应商互操作性的支持,以及显式的面向服务特性。
【2.1】与 Microsoft 分布式计算技术的统一性
Microsoft以前的分布式计算技术特性和应用领域不同。要构建基本的可互操作的 Web 服务,最佳选择是 ASP.NET Web 服务(ASMX)。要连接两个基于 .NET Framework 的应用程序,选择 .NET Remoting。如果应用程序需要分布式事务和其他更高级的服务,其创建者一般会使用企业服务Enterprise Sevices,即COM+ 的继任者。要构建Web 服务安全,使用WS-Addressing 和 WS-Security规范,开发人员可以构建采用 WSE3.0的应用程序。而要创建基于消息的排队式应用程序,基于 Windows 的开发人员则应使用 Microsoft 消息队列 (MSMQ)。 各个技术的具体特性和WCF对比如下表:
ASMX
.NET Remoting
企业服务
WSE
MSMQ
Indigo
Web 服务
X
X
.NET - .NET 通信
X
X
分布式事务等。
X
X
WS-* 规范支持
X
X
队列消息传输
X
X
WCF支持了以前分布式技术的所有的特性,因而对于建立分数是应用更加的灵活和全面。
【2.2】与非 Microsoft 应用程序的互操作性
由于 Indigo 的基本通信机制是 SOAP,因此 Indigo 应用程序可与大量运行于各种上下文环境的其他软件进行通信。基于 Indigo 构建的应用程序可与下列所有程序进行交互:
1)运行于同一 Windows 计算机上不同进程中的 Indigo 应用程序。
2)运行于另一 Windows 计算机上的 Indigo 应用程序。
3)基于其他技术构建的应用程序,如基于 Java 2 企业版 (J2EE) 构建的、支持标准 Web 服务的应用程序服务器。
4)Indigo 应用程序还可以与基于 Indigo 以前的 .NET Web 服务(ASMX)构建的应用程序进行互操作。
这样通过标准的SOAP消息WCF可以与其他应用程序进行交互,很好地支持了跨平台跨操作系统的的目标。当然WCF中支持安全、事物、MTOM特性,这些都以前WSE中的主要概念,我们发现WCF中也进行了扩展和支持。利于技术的向前兼容。
【2.3】对面向服务开发的显式支持
面向服务是一个广泛的领域,它包含面向服务应用程序和更广泛的面向服务体系结构 (SOA) 的概念。在设计此项技术的过程中始终遵守四项基本原则:
1.共享架构,而不是类:与旧的分布式对象技术不同,服务仅通过精心定义的 XML 接口与其客户端进行交互。跨越服务边界传递完整的类、方法及全部之类的行为被禁止。
2.服务具有自主性:服务及其客户端同意它们之间的接口,但相互独立。它们可以采用不同的语言编写,可以使用不同的运行时环境(如 CLR 和 Java 虚拟机),可以运行在不同操作系统上,还可以存在其他方面的不同。
3.边界是显式的:分布式对象技术 [如分布式 COM (DCOM)] 的目标之一是使远程对象尽可能看上去像本地对象一样。虽然这种方法通过提供一种通用编程模型在某些方面简化了开发,但也同时隐藏了本地对象与远程对象之间不可避免的区别。服务通过使服务与其客户端之间的交互更明显而避免了这一问题。隐藏分布式特性并非目的。
4.采用基于策略的兼容性:决定在系统之间使用哪些选项应取决于基于 WS-Policy 的机制。
WS-Policy 在WSE3.0里我们应该比较常见,通过策略配置来支持和实现Web服务的安全。这里很多技术概念我们都可以从早期的分布式技术里找到原型。
【3】WCF服务框架模型
下面我们来介绍一下WCF服务的基本结构。 WCF服务的基本结构如下图所示:
每个 Indigo 服务均由三个部分构成:
1.服务类:采用 C# 或 VB.NET 或其他基于 CLR 的语言编写,实现一个或多个方法。通常包括服务契约、操作契约和数据契约。
2.宿主:一种应用程序域和进程,服务将在该环境中运行。ASP.NET,EXE,WPF,Windows Forms,NT Service,COM+作为宿主(Host) 。
3.终结点:服务暴露出来的地址,由客户端用于访问服务。通常提及的概念就是ABC,即地址、绑定和契约的第一个字母的简称。 如图:
终结点中重要的概念就是绑定,它是实现通信的关键部分。这里定义消息通讯的协议包括HTTP、TCP、UDP、MSMQ等,用户可以根据自己的需要定义。
【4】实现代码分析
上面介绍了WCF相关的一些基本概念、特性和框架模型,现在我们来具体实现一个WCF的应用实例。客户端和WCF服务通信的过程如图:
客户端通过代理调用WCF服务,代理通过服务地址找到特定的服务,调用执行特定的服务方法。
我们代码的演示部分也分为3个部分,首先是服务类(契约部分),其次是宿主(自己定义一个控制台程序),客户端(控制台)。
【4.1】契约(Contract)
定义实现的代码包括服务契约IWCFService、操作契约OperationContract、和数据契约DataContract。具体代码实现如下:
//ServiceContract 属性以及 Indigo 使用的所有其他属性均在 System.ServiceModel 命名空间中定义, //因此本例开头使用 using 语句来引用该命名空间。 namespace WCFService { //1.服务契约 [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")] public interface IWCFService { //操作契约 [OperationContract] string SayHello(string name); //操作契约 [OperationContract] string SayHelloToUser(User user); } //2.服务类,集成接口。实现契约 public class WCFService : IWCFService { //实现接口定义的方法 public string SayHello(string name) { Console.WriteLine("Hello! {0},Using string ", name); return "Hello! " + name; } //实现接口定义的方法 public string SayHelloToUser(User user) { Console.WriteLine("Hello! {0} {1},Using DataContract ", user.FirstName, user.LastName); return "Hello! " + user.FirstName + " " + user.LastName; } } //3.数据契约//序列化为XML,作为元数据封装到服务里 [DataContract] public struct User { [DataMember] public string FirstName; [DataMember] public string LastName; } }
【4.2】宿主(托管进程)
WCFHost宿主进程托管服务WCFService,项目类型也为控制台应用程序(你也可以创建其他的项目类型)。这里要添加对WCFService类库的引用。托管采用反射方式动态创建服务的实例。具体代码如下:
//采用自托管方式,也可以是IIS、WAS,Windows服务等用户自定义程序托管服务 public class WCFHost { static void Main(string[] args) { //反射方式创建服务实例, //Using方式生命实例,可以在对象生命周期结束时候,释放非托管资源 using (ServiceHost host = new ServiceHost(typeof(WCFService.WCFService))) { //相同的服务注册多个基地址 //添加服务和URI,用户资源标识符 //Uri tcpAddress = new Uri("http://localhost:8001/WCFService"); Uri httpAddress = new Uri("http://localhost:8002/WCFService"); //Uri httpAddressLisening = new Uri("http://localhost:8002/WCFService"); //host.AddServiceEndpoint(typeof(WCFService.IWCFService), new WSHttpBinding(), tcpAddress); host.AddServiceEndpoint(typeof(WCFService.IWCFService), new WSHttpBinding(), httpAddress); //判断是否以及打开连接,如果尚未打开,就打开侦听端口 //if (host.State !=CommunicationState.Opening) host.Open(); //显示运行状态 Console.WriteLine("Host is runing! and state is {0}",host.State); //等待输入即停止服务 Console.Read(); } } }
这里值得注意的就是绑定协议要和配置文件里的设置一样,不然会出现编译错误或者运行时异常。编译结束会生成可执行文件EXE。
【4.3】客户端
客户端要添加对以上服务的引用,如果你初次建立服务,配置不当很容易出现引用服务失败的问题,配置服务相关的错误和解决办法我也整理到WCF分布式开发常见错误解决里了,大家需要的话可以参考,是在遇到苦难可以留言给我一起交流。
客户端的具体实现代码如下:
class WCFClient
{
static void Main(string[] args)
{
//IWCFService proxy = ChannelFactory<IWCFService>.CreateChannel(new WSHttpBinding(), //////new EndpointAddress("net.tcp://localhost:8001/WCFService"));
////new EndpointAddress("http://localhost:8002/WCFService")); WCFServiceClient wcfServiceProxy = new WCFServiceClient("WSHttpBinding_IWCFService1");
//通过代理调用SayHello服务 Console.WriteLine(wcfServiceProxy.SayHello("Frank Xu Lei"));
////通过代理调用调用SayHelloToUser,传递对象 User user = new User();
user.FirstName = "Lei";
user.LastName = "Frank";
Console.WriteLine(wcfServiceProxy.SayHelloToUser(user));
Console.Read();
}
}
这里可以通过两种方式调用WCF服务,一种是自己编码实现客户端服务代理,要正确添加服务的地址。第二种是当客户端添加服务成功以后,VS2008集成开发工具会在客户端自动生成服务端的操作契约、服务契约等信息,当然包含客户端服务代理类,这个比较关键。具体方法就是在客户端项目右键添加服务应用。输入服务侦听的地址。查找到服务后添加引用会生成一系列服务引用的文件,如图:
【4.4】运行结果:
首先要启动服务宿主进程WCFhost.如图:
其次运行客户端,结果如图所示:
【5】总结
结果显示调用成功,包括直接调用简单的SayHello方法和通过数据契约调用SayHellotoUser方法。
此系列文章的取名《WCF分布式开发步步为赢》。在此之前,我曾经整理出WCF分布式开发必备知识和WSE3.0构建Web服务安全系列文章,主要系统介绍了微软分布式开发的相关的技术:COM+(Enterprise Services)、.Net Remoting、MSMQ消息队列、Web 服务、WSE3.0等相关知识。目的也就是为了学习WCF打下坚实的基础。步步为营,步步为赢!这个也是本系列文章取名的真正目的原因! 如果你是.NET开发的程序员,老徐的建议是,有时间的话,看看微软早期的分布式开发:COM+(Enterprise Services)、.Net Remoting、MSMQ消息队列、Web 服务、WSE3.0等相关技术。这些技术看是陈旧,实际目前还是应用广泛。而你想要真正成为一个企业应用系统集成领域的专家,这些技术必不可少。
最后给出本节文章的示例代码,供大家参考。下载连接:/Files/frank_xl/WCFServiceFrankXuLei.rar。希望能对大家有所帮助,也欢迎留言交流~如果感觉不错,希望大家支持和继续关注《WCF分布式开发步步为赢》的后续文章,谢谢~
参考:MSDN
本文转自 frankxulei 51CTO博客,原文链接:http://blog.51cto.com/frankxulei/320400,如需转载请自行联系原作者
WCF
服务这个词,对于任何人来说都不陌生,那么什么是服务呢?在现实世界中,服务可以理解为做一些事情,比如说去饭店吃饭,服务人员的服务就是给你那菜单,帮你上菜等,其实这就是一种功能。在编程的世界里,服务是暴露在外的一组功能的集合。在WCF中,经常会看到ABC这三个词,其实WCF框架的核心是ABC,其他的配置,接口,属性,工具,模板等都是围绕ABC在展开。
A:Address Where
B:Binding How
C:Contract What
WCF的全称是:Windows Communication Foundation。从本质上来说,它是一套软件开发包,是微软公司推出的符合SOA思想的技术框架。
WCF为程序员提供了丰富的功能,其中包括:托管、服务实例管理、异步、安全、事务管理、离线队列等。并且WCF对产业中的标准协议进行了封装和定义,它把程序员从繁琐的通信、格式编码中解放出来,使得程序员能够专注于业务逻辑的实现。同时,WCF统一了微软公司之前推出的多种分布式技术,其中包括:
1. Web服务和WSE。
2. .Net Remoting。
3. .Net 企业服务。
4. 微软消息队列(MSMQ)。
WCF对这些技术的集成包括两个方面:
1. WCF的架构本身吸取了这些技术的精华。
2. WCF开发的服务/客户端可以和现有的Web服务、MSMQ程序进行交互。
.Net Framework的四大体系架构
.Net Framework的四大体系包括:WCF、WF、WPF、WCS。
WCF(Windows Communication Foundation):主要是用来做松耦合的分布式通讯的,它还有另外一个名字叫Indigo,是微软迈向SOA一个重要的标志。WCF是.Net Framework的一个子集。
WF(Windows Wordflow Foundation):工作流引擎。
WPF(Windows Presenttation Foundation):WPF使用矢量绘图引擎,提供了一种声明式编程语言XAML,用来开发具有炫目视觉效果的应用程序。
WCS(Windows Card Space):采用了一种新的数字标识技术来实现类似网银单点登录的功能,主要用来防止钓鱼式攻击等,从网络安全方面提供了一个保障。
WCF体系框架
从图上来看,WCF的体系基本包含了4个方面,分别为契约、服务运行时、消息和寄宿。
1 契约(能干什么)
从SOA的概念上来看,契约属于一个服务公开接口的一部分。一个服务的契约,定义了服务端公开的服务方法、使用的传输协议、可访问的地址、传输的消息格式等内容。
基本上,契约的定义描述了该服务的功能和作用,它告诉SOA系统中的其它节点这个服务是“干什么”的。
2 服务运行时(怎么干)
服务运行时定义了服务在运行时的具体行为。如果说契约描述了服务是“干什么”的,那么服务运行时就在一定程度上描述了服务是“怎么干”的。
3 消息
消息方面包含了消息的传输方式、消息的编码与解码。消息方面的内容基本属于服务边界以内的具体实现。具体的传递时限,必须符合在契约中定义的绑定协议。
4 激活和宿主(在哪干)
激活和宿主属于WCF程序的部署方式。一个WCF服务需要在一个可运行的程序中寄宿,我们可以把宿主理解为WCF运行的容器。常用的寄宿方式包括自寄宿、IIS寄宿、Windows激活服务、Windows服务、Com+组件等。根据SOA的原则,激活和宿主类型的变化不会影响服务本身的特性和外部对该服务的访问,而WCF在这一方面也确实做的非常出色。
参考:http://blog.csdn.net/kntao/article/details/5699283
http://blog.csdn.net/kntao/article/details/5699283
http://blog.sina.com.cn/s/blog_570379cc0100newa.html
http://blog.csdn.net/fangxing80/article/details/6106228
http://www.cnblogs.com/wayfarer/archive/2007/08/24/867987.html