基于Xenomai的实时Linux分析与研究

作者:南京航空航天大学 王荣 游有鹏 张少坤

引 言
   
随 着嵌入式设备的快速发展,嵌入式设备的功能和灵活性要求越来越高,很多嵌入式设备中都开始使用操作系统。由于工作的特殊性,很多嵌入式设备要求系统对外部 事件的中断响应必须在事先设定的时限范围内完成,使系统具有可预测性,而通用的桌面操作系统大都是非实时或者是软实时的,无法满足需求,因此就必须使用实 时操作系统(Real—Time Operating System,RTOS)。
    实时操作系统是一个可以在有限确定的时间内,对异步输入进行处理并输出的信息系统。一个高性能的实时操作系统应具备良好的综合性能,包括系统体系结构、基本系统功能支持(如内存和中断管理)、APl支持和稳定性等。
    实时系统又有软实时系统(soft real—time system)和硬实时系统(hard real—time system)之分。软实时系统是指那些在系统负荷较重时,允许发生错过时限(deadline)的情况而且不会造成太大危害的系统,如电视会议系统;而 硬实时系统是指那些对每个任务的调度时间要求非常严格的系统,如果不满足时间限制的要求,则会给系统带来毁灭性的后果。比如数控机床的进给控制系统,在规 定时间内进给系统必须达到预定的位置,否则无法保证加工零件的精度,甚至无法完成加工。
    在嵌入式系统领域,实时系统的核心是实时操作系统。目前已有很多商业实时操作系统,著名的有WindRiver公司的VxWorks,其他的有QNX、 pSOS+等。它们的优点是具有非常好的稳定性、可靠性和实时性,但是一般价格昂贵且互不兼容,而且源代码作为商业秘密而不公开。与之相反,GPL协议下 的Linux操作系统则为开发者在前人基础上进行更深入的研究提供了可能。目前,具有代表性的Linux内核实时性研究项目有RT—Linux、 RTAI、Xenomai等。

1 Linux 2.6内核的实时性分析
    相对于老版本内核,Linux 2.6版本的内核结构做了很大的改动,开发者对很多功能模块的代码都进行了重写。最为显著的改进是在影响系统实时性的进程调度方面,包括采用可抢占内核和新的0(1)调度程序。
    但是Linux在最初的设计是用作个人PC或者小型服务器的操作系统,由于设计要求的针对性,导致了Linux无法提供硬实时环境,直接影响了它的硬实时性能。这主要表现在两方面:
    (1)进程调度方式
    Linux 的进程调度采用的是时间片轮转调度策略。不论进程优先级的高低,Linux在某段时间内都会分配给该进程一个时间片运行,也就是说它的设计更注重任务调度 的公平性。这种情况下,就会出现高优先级进程由于其时间片的耗尽而被迫放弃处理器,处理器被没有耗尽时间片的低优先级进程所占用的现象。这样显然无法适用 于实时性要求比较高的系统。
    (2)时钟粒度粗糙
   
在Linux 2.6版本内核中,时钟中断发生的频率范围为50~1 200Hz,周期不小于0.8 ms,而工业上很多的中断周期都在几十μs之内。
    对于上面提到的影响Linux实时性的问题,目前的解决办法主要有2种:
    ①对Linux内核的内部进行实时改造,即直接修改Linux内核的数据结构、调度方式以及中断方式(主要是时钟中断)。
    采用这种方法,实时化改造后的系统实时性较好,但是工作量大,并且可能会造成系统不稳定。最大的缺点是:原本在Linux上运行的设备驱动程序和应用程序不能直接在改进的内核上运行。典型代表有Kurt-Linux。
    ②对Linux内核的外部实时扩展,这种方法通常是采用双内核的办法。具体是在Linux内核和硬件间加入一个硬件抽象层(Hardware Abstract Layer,HAL),系统所有的硬件中断由这个抽象层控制。新创建一个内核专门用来调度实时进程,而普通进程通过原来的Linux内核进行调度。采用此 方法的最大好处在于对Linux的内核改动很小,而且原Linux上的设备驱动程序和应用程序都能顺利地在此实时系统上运行。其代表有RT—Linux、 RTAI和Xenomai。

2 Xenomai原理与应用
2.1 Xenomai简介及其Adeos实现

    Xenomai是一个自由软件项目,提供了一个基于Linux的实时解决方案。它可以提供工业级RTOS的性能,而且完全遵守GNU/Linux自由软件协议。目前最新稳定版本是2.4.5。
    Xenomai项目起始于2001年。从2003年夏天起,Xenomai和RTAI有了两年时间的合作,期间开发了广为人知的RTAI/fusion项 目分支。到2005年,Xenomai项目又重新独立出来。而从2.0.0版本开始,Xenomai在硬件平台的移植就一直是基于Adeos构架来实现 的。
    在基于Adeos的系统中,分为多个域。每个域中独立运行一个操作系统(或者是实现一定功能的程序模块),每个域可以有独立的地址空间和类似于进程、虚拟 内存等的软件抽象层。在各个域下层有一个Adeos通过虚拟中断等方法来调度上面的各个域。在基于Adeos的系统中,存在着A、B、C、D四种类型的交 互,如图1所示。

    A类交互是各个域直接操作硬件设备,包括访问内存等;B类交互指当Adeos接收到硬件中断后,会根据中断来对相应的域进行中断服务;C类交互指当前域内 的操作系统主动向Adeos请求某些服务;D类交互是指Adeos接收硬件产生的中断和异常,同时也可以直接控制硬件。其中,Adeos实现的功能主要包 括中断管道机制(I—Pipe)、域管理模块和域调度模块功能。
2.2 Xenomai用户层实时的实现
    Xenomai除了在内核层利用Adeos实现了硬实时外,它在用户空间也有很好的实时性。在S3C2410平台上,为了实现用户层的实 时,Xenomai实现了一个硬件计数器——Decrementer。这个硬件计数器可以在用户空问里很好地模拟TSC(Time Stamp Counter,时间戳计数器)。
    同时,Xenomai在Linux内核中加入了一个全新的数据结构__ipipe_tscinfo,可以通过此数据结构变量存放用户层需要的数据。该数据结构组成如下:

    在用户层,应用程序通过系统调用可以迅速得到struct_ipipe_tscinfo结构体中的数据。而且为了避免受到缓存的影响,Xenomai将此结构体变量存放在Linux的向量页中。

    内核通过函数_ipipe_mach_get_tscinfo来填充struct_ipipe_tscinfo结构体变量中的各项内容:

   
    其中,info一>typte说明在S3C2410平台上TSC是基于Decrementer硬件计数方式的;info 一>u.dec.counter用来将Decrementer计数器的物理地址设定为0x51000038;info 一>u.dec.mask掩码用来注明使用Dec—rementet。计数器中的特定位;info一>u.dec.tsc指向存放64位 TSC值的区域。
    在Xenomai用户层的实时程序运行时,程序都会通过系统调用得到内核填充好的struct_ipipe_tscinfo结构体变量。具体实现可参考编 译用户层实时程序时用到的,由Xenomai所提的头文件/usr/xenomai/include/asm/syscall.h。
2.3 Xenomai多API构架
    除了提供Linux硬实时,Xenomai的另一个目的是使基于Linux的实时操作系统能提供与传统的工业级实时操作系统(包括VxWorks、 pSOS+、VRTX或者uITRON)功能相同的API。这样,可以让这些操作系统下的应用程序能够很容易地移植到GNU/Linux环境中,同时保持 很好的实时性。
    Xenomai的核心技术表现为使用一个实时微内核(real—time nucleus)来构建这些实时API,也称作“skin”。在实时核复用的基础上,一个skin可以很好地模拟一种实时操作系统的API。它的结构图可以参考图2。

    图2中,Native是Xenomai自带的API,各类API都有着同等的地位,都独立地基于同一个实时微内核。这样做可以让内核的优点被外层所有的 API很好地继承下来。更重要的是,实时微内核提供的服务被外层各种API以不同的方式表现出来,由此可以增强整个系统的强壮性。
    编制实时程序时,在很多实时操作系统上只能在内核层实现;而编制实时内核模块时,会受到内核的限制,比如有些实时内核不支持浮点运算,模块出错时容易使整 个系统挂起,而且内核模块的调试比较困难。Xenomai能够支持较好的用户层实时,这为编制实时性要求不是非常高的实时程序提供了一个有效途径。下面这 个用户层实时例程使用的是Xenomai提供的Native API:


    从程序中可以看出,Xenomai的用户层实时程序的周期可以轻易地设定到μs级,所以它完全可以适用于一般实时性要求的工程应用。

3 总 结
   
本 文首先简单介绍了实时操作系统,分析了Linux 2.6内核实时性能的不足;然后着重介绍了一个Linux实时化的解决方案——Xenomai,分析了Xenomai的Adeos构架基础,简要说明了 Xenomai用户层实时的实现,以及Xenomai支持多种实时操作系统的API的新特点。本文给出的Xenomai的用户层实时例程已经成功地在多个 平台上运行过,表明Xenomai用户程序在多种硬件平台上有很好的移植性。

Linux基本的键盘输入快捷键

Penna:用Linux这么久,今天才知道<Shift> + <PgUp>可以回滚终端输出,郁闷,每次都眼巴巴的看着内容一闪而过,一定要学好基础知识啊!


<Ctrl><Alt><F1>

切换到第一个文本终端。在Linux下你可以有多达六个不同的终端。这个命令的意思是:“同时按住<Ctrl>键和<Alt>键,然后按<F1>键,再释放所有的键”。

<Ctrl><Alt><Fn> (n=1..6)

切换到第n个文本终端。(你也可以使用不是很经常用到的命令chvt n 来实现,n指的是第n个文本终端)。在文本终端下(不是在X窗口),你也可以简单使用<ALT><Fn>来实现切换,不需要<CTRL>键。

打印出你正在使用的终端名称,如果你希望知道终端的名字,可以使用命令fgconsole。

<Ctrl><Alt><F7>

切换到第一个图形用户界面(一般来说X-window在第七个终端)

<Ctrl><Alt><Fn> (n=7到12)

切换到第n个图形用户街面。根据缺省,第一个X-Window在第7个终端运行,从第8到第12什么也没有,当然你可以逐个启动这些图形用户界面。

<Tab>

(在文本终端下)可以使用TAB自动完成命令,或者显示所有的可选项。这个快捷键真的非常好用,经常使用你会发觉它可以节约你很多的时间。

<ArrowUp>

(在文本终端或者X窗口下)滚动和编辑以前输入的命令。按<ENTER>执行一个历史命令

<ArrowDown>

回滚

<Shift><PgUp>

滚动终端输出。对于登录提示也起作用,所以你可以使用它回滚启动信息。你显卡的内存大小决定你可以回滚多少内容

<Shift><PgDown>

回滚终端输出

<Ctrl><Alt><+>

(在X窗口下) 改变X服务器的屏幕解析率 (如果你设置X服务器有多个不同的屏幕解析率)。比如对于我的标准SVGA卡和显示器,在文件/etc/X11/XF86Config有以下的设置行: (从缺省开始,到可以支持的最大虚拟屏幕解析率)

Modes "1024×768" "800×600" "640×480" "512×384" "480×300" "400×300" "1152×864"Z

当然,首先我必须设置我的X服务器,可以使用using Xconfigurator, xf86config, 也可以手工编辑文件:/etc/X11/XF86Config。XFdrake (Mandrake使用图形用户界面进行配置 )。你也可以参考命令xvidtune和xvidgen。

<Ctrl><Alt><->

(在X窗口下)把X服务器的屏幕解析率修改到上一次的设置。

<Ctrl><Alt><Esc>

(在X窗口,KDE下)关闭我鼠标将要指向的窗口(鼠标的光标形状会有所改变)。同样的效果也可以使用命令xkill(在X终端上)来实现。当一个程序窗口被挂住的时候特别有用。

<Ctrl><Alt><BkSpc>

(在X窗口下) 终止当前的 X窗口服务。如果X窗口不能正常退出时可以使用。

<Ctrl><Alt><Delete>

(适用于文本终端下)关机和重新启动。这是一个在文本终端下的正常关机命令,千万不要按计算机上的reset键来重新关机和重新启动!

<Ctrl>c

终止当前进程(对于一般的小型文本模式的应用程序)

<Ctrl>d

(在一个空白的命令行上输入)退出当前的终端。参加下一个命令。

<Ctrl>d

给当前的进程送文件结束符合。不要按两次否则你会把自己退出系统。

<Ctrl>s

停止终端传输

<Ctrl>q

从新开始终端传输。如果你的终端突然莫名其妙的停止响应,可以参考上一条命令。

<Ctrl>z

把当前进程送到后台处理。

exit

退出系统。我也可以使用logout命令达到同样效果。(如果你启动了第二个shell程序,比如,使用bash,这条命令将使你退出第二个shell回到第一个shell,但是不会退出系统。当然,可以再一次使用exit退出)

reset

恢复崩溃的终端(有些终端显示一些奇怪的字符)到缺省的设置。当你使用cat命令准备显示一个二进制文件时,你可能看不到你所输入的命令,尽管命令仍然照常工作。

<鼠标的中间键>

粘贴当前选择的文本。这是一个常规的Linux“复制-粘贴”操作。它是对被广泛采用的通信用户界面下“复制-粘贴”操作的一个非常有用的扩展(但 是它不支持旧版本的NetScape,因为在旧版的NetScape只能使用MAC/Windows风格的“复制-粘贴”操作。如果你运行“setup” 程序进行设置,你可以在文本终端下使用这个功能。而且它在大多数的对话框里也能很好的工作,真的非常方便!)如果你有一个“Linux-Ready”的鼠 标(logitec或者其类似产品)或者你通过使用“模拟三键鼠标”,这个功能可以得到最大限度的发挥。一般来说,“第三键”的模拟是通过同时点击左右两 个鼠标键来实现。

~

(水文符合) 我的主目录(一般来书是/home/我的登录名)。举例,命令~/my_dir将修改我的工作目录到我的主目录的子目录”my_dir”下。相对于输入”cd ~”,你也可以只输入”cd”,我把我的所有文件都放在自己的主目录下。

.

(点符号) 当前目录。举例:./my_program 将试图执行当前目录下的"my_program"文件

..

(两个点)到当前的上一级目录(也叫“父目录”)。举个例子,命令“cd..”将修改我的当前工作目录到上一级目录。

一些KDE的快捷键(很有用,但不是非要不可的)

<Alt><Tab>

切换不同的程序窗口。往回切换,使用 <Alt><Shift><Tab>

<Ctrl><Tab>  

切换不同的桌面。往回切换,使用 <Ctrl><Shift><Tab>

<Ctrl><Esc>  

显示我系统里正在运行的进程列表。允许我终止那些由我启动的进程(或者送信号给这些进程)

<Alt><F1>    

访问K菜单 (对等于微软Windows的的“开始”菜单).

<Alt><F12>   

使用键盘上的箭号键模拟鼠标的操作

<Alt><鼠标左键>     

拖曳并移动一个窗口。一般来说,我通过拖曳窗口的题目栏来移动一个窗口。有些时候,我用它把

一个窗口移动到屏幕之外。使用这个功能,我可以把窗口移动到屏幕的任何位置。

<Alt><PrintScreen>

把当前屏幕的快照存到剪贴板

<Ctrl><Alt><PrintScreen>

把当前桌面的快照存到剪贴板

<Ctrl><Alt><l>

锁定桌面

<Ctrl><Alt><d>

切换隐藏/显示桌面的功能(当老板走过来的时候迅速藏起你的纸牌接龙游戏非常管用哦!)

<Alt><SysRq><command_key> (非必须掌握)

这是在Linux的内核(底层内核)上支持的一组组合键。它意味着这些组合键在大部分情况下都是有效的。组合键主要是针对开发人员的程序调试目的或 者在紧急场合下才使用;你也可以使用其他的办法,安全第一。<SysRq>键在PC上指的是PrintScreen键。组合键的功能可以通过 设置相对应的内核参数来激活或者取消,比如: echo "1" > /proc/sys/kernel/sysrq

<Alt><SysRq><k>

终止在当前虚拟终端上运行的所有进程(包括X窗口)。这个组合键被称为“安全访问键“(SAK).

<Alt><SysRq><e>

发送 TERM 信号(结束信号)到除了init进程以外的所有运行进程,要求其退出

<Alt><SysRq><i>

发送 KILL (终止信号)到除了init进程以外的所有运行进程, 这个组合键比上一个更加有效,但是也可能引起一些程序被异常终止。

<Alt><SysRq><l>

发送KILL命令到所有运行进程(包括init),系统将不再能够使用

<Alt><SysRq><s>

在所有的挂载文件系统上运行紧急同步(缓存写) 。这可以保护数据丢失

<Alt><SysRq><u>

以只读模式重新挂载所有已加载的文件系统。这和上面的同步组合键有相同的功能,但是另外有一个好处:如果操作顺利完成,下一次硬件重新启动时,fsck将不会重新检查所有的文件系统。

<Alt><SysRq><r> 关闭键盘的原始模式。当你的X窗口程序挂住没有响应时特别有用。使用这个组合键之后,你可能会接着使用<CTRL><ALT><DEL>程序启动。

<Alt><SysRq><b>

不进行同步和卸载就立刻重新启动系统。之后你可能会碰到一下错误信息。

<Alt><SysRq><o>

关机 (如果正确设置以后)

<Alt><SysRq><p>

导出当前的寄存器和标志到控制台

<Alt><SysRq><t>

导出当前任务和相关信息到控制台

<Alt><SysRq><m>

导出内存的内容到控制台

<Alt>SysRq><数字>

“数字”从0到9。设置控制台记录的级别,用于控制哪一些内核信息将被显示在控制台上。举例,“0”意味着只有象PANIC和OOPS的之类的紧急信息才被显示在控制台上。

<Alt><SysRq><h>

显示帮助信息。还有,任何其他不被支持的 <Alt><SysRq><key> 组合键将显示同样的帮助。

 

Linux下网关地址的获取

Linux的网关信息保存在路由表中,获取网关实际上就是路由表的查询。

 用户空间获取网关地址

有两种方法,一个是从/proc/net/route中读取,这是最简单,最直接的,route命令就是这么做的,可以参考net-tools包中route的源码实现。

另一种是用Netlink来实现。利用NETLINK_ROUTE(rtnetlink.c: Routing netlink socket interface)的RTM_GETROUTE指令查找路由,这是从网上找的代码,在Debian (2.6.26内核)下测试通过。

C++代码
  1. #include <arpa/inet.h>  //for in_addr  
  2. #include <linux/rtnetlink.h>    //for rtnetlink  
  3. #include <net/if.h> //for IF_NAMESIZ, route_info  
  4. #include <stdlib.h> //for malloc(), free()  
  5. #include <string.h> //for strstr(), memset()  
  6.   
  7. #include <string>  
  8.   
  9. #define BUFSIZE 8192  
  10.    
  11. struct route_info{  
  12.  u_int dstAddr;  
  13.  u_int srcAddr;  
  14.  u_int gateWay;  
  15.  char ifName[IF_NAMESIZE];  
  16. };  
  17. int readNlSock(int sockFd, char *bufPtr, int seqNum, int pId)  
  18. {  
  19.   struct nlmsghdr *nlHdr;  
  20.   int readLen = 0, msgLen = 0;  
  21.   do{  
  22.     //收到内核的应答  
  23.     if((readLen = recv(sockFd, bufPtr, BUFSIZE – msgLen, 0)) < 0)  
  24.     {  
  25.       perror("SOCK READ: ");  
  26.       return -1;  
  27.     }  
  28.      
  29.     nlHdr = (struct nlmsghdr *)bufPtr;  
  30.     //检查header是否有效  
  31.     if((NLMSG_OK(nlHdr, readLen) == 0) || (nlHdr->nlmsg_type == NLMSG_ERROR))  
  32.     {  
  33.       perror("Error in recieved packet");  
  34.       return -1;  
  35.     }  
  36.      
  37.     /* Check if the its the last message */  
  38.     if(nlHdr->nlmsg_type == NLMSG_DONE)   
  39.     {  
  40.       break;  
  41.     }  
  42.     else  
  43.     {  
  44.       /* Else move the pointer to buffer appropriately */  
  45.       bufPtr += readLen;  
  46.       msgLen += readLen;  
  47.     }  
  48.      
  49.     /* Check if its a multi part message */  
  50.     if((nlHdr->nlmsg_flags & NLM_F_MULTI) == 0)   
  51.     {  
  52.       /* return if its not */  
  53.      break;  
  54.     }  
  55.   } while((nlHdr->nlmsg_seq != seqNum) || (nlHdr->nlmsg_pid != pId));  
  56.   return msgLen;  
  57. }  
  58. //分析返回的路由信息  
  59. void parseRoutes(struct nlmsghdr *nlHdr, struct route_info *rtInfo,char *gateway)  
  60. {  
  61.   struct rtmsg *rtMsg;  
  62.   struct rtattr *rtAttr;  
  63.   int rtLen;  
  64.   char *tempBuf = NULL;  
  65.   struct in_addr dst;  
  66.   struct in_addr gate;  
  67.     
  68.   tempBuf = (char *)malloc(100);  
  69.   rtMsg = (struct rtmsg *)NLMSG_DATA(nlHdr);  
  70.   // If the route is not for AF_INET or does not belong to main routing table  
  71.   //then return.   
  72.   if((rtMsg->rtm_family != AF_INET) || (rtMsg->rtm_table != RT_TABLE_MAIN))  
  73.   return;  
  74.   /* get the rtattr field */  
  75.   rtAttr = (struct rtattr *)RTM_RTA(rtMsg);  
  76.   rtLen = RTM_PAYLOAD(nlHdr);  
  77.   for(;RTA_OK(rtAttr,rtLen);rtAttr = RTA_NEXT(rtAttr,rtLen)){  
  78.    switch(rtAttr->rta_type) {  
  79.    case RTA_OIF:  
  80.     if_indextoname(*(int *)RTA_DATA(rtAttr), rtInfo->ifName);  
  81.     break;  
  82.    case RTA_GATEWAY:  
  83.     rtInfo->gateWay = *(u_int *)RTA_DATA(rtAttr);  
  84.     break;  
  85.    case RTA_PREFSRC:  
  86.     rtInfo->srcAddr = *(u_int *)RTA_DATA(rtAttr);  
  87.     break;  
  88.    case RTA_DST:  
  89.     rtInfo->dstAddr = *(u_int *)RTA_DATA(rtAttr);  
  90.     break;  
  91.    }  
  92.   }  
  93.   dst.s_addr = rtInfo->dstAddr;  
  94.   if (strstr((char *)inet_ntoa(dst), "0.0.0.0"))  
  95.   {  
  96.     printf("oif:%s",rtInfo->ifName);  
  97.     gate.s_addr = rtInfo->gateWay;  
  98.     sprintf(gateway, (char *)inet_ntoa(gate));  
  99.     printf("%sn",gateway);  
  100.     gate.s_addr = rtInfo->srcAddr;  
  101.     printf("src:%sn",(char *)inet_ntoa(gate));  
  102.     gate.s_addr = rtInfo->dstAddr;  
  103.     printf("dst:%sn",(char *)inet_ntoa(gate));   
  104.   }  
  105.   free(tempBuf);  
  106.   return;  
  107. }  
  108.   
  109. int get_gateway(char *gateway)  
  110. {  
  111.  struct nlmsghdr *nlMsg;  
  112.  struct rtmsg *rtMsg;  
  113.  struct route_info *rtInfo;  
  114.  char msgBuf[BUFSIZE];  
  115.    
  116.  int sock, len, msgSeq = 0;  
  117.   
  118.  if((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0)  
  119.  {  
  120.   perror("Socket Creation: ");  
  121.   return -1;  
  122.  }  
  123.    
  124.  /* Initialize the buffer */  
  125.  memset(msgBuf, 0, BUFSIZE);  
  126.    
  127.  /* point the header and the msg structure pointers into the buffer */  
  128.  nlMsg = (struct nlmsghdr *)msgBuf;  
  129.  rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);  
  130.    
  131.  /* Fill in the nlmsg header*/  
  132.  nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); // Length of message.  
  133.  nlMsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table .  
  134.    
  135.  nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump.  
  136.  nlMsg->nlmsg_seq = msgSeq++; // Sequence of the message packet.  
  137.  nlMsg->nlmsg_pid = getpid(); // PID of process sending the request.  
  138.    
  139.  /* Send the request */  
  140.  if(send(sock, nlMsg, nlMsg->nlmsg_len, 0) < 0){  
  141.   printf("Write To Socket Failed…n");  
  142.   return -1;  
  143.  }  
  144.    
  145.  /* Read the response */  
  146.  if((len = readNlSock(sock, msgBuf, msgSeq, getpid())) < 0) {  
  147.   printf("Read From Socket Failed…n");  
  148.   return -1;  
  149.  }  
  150.  /* Parse and print the response */  
  151.  rtInfo = (struct route_info *)malloc(sizeof(struct route_info));  
  152.  for(;NLMSG_OK(nlMsg,len);nlMsg = NLMSG_NEXT(nlMsg,len)){  
  153.   memset(rtInfo, 0, sizeof(struct route_info));  
  154.   parseRoutes(nlMsg, rtInfo,gateway);  
  155.  }  
  156.  free(rtInfo);  
  157.  close(sock);  
  158.  return 0;  
  159. }  
  160.   
  161. int main()  
  162. {  
  163.     char buff[256];  
  164.     get_gateway(buff);  
  165.     return 0;  
  166. }  

内核空间获取网关地址

用户空间的实现,其本质上是内核空间的支持,因此内核空间获取应该更直接点。我参考了NETLINK_ROUTE中的实现来做,即执行一个从本机IP到外网IP的路由查询,获得的路由记录中自然包括网关地址,主要用到ip_route_output_key()函数。下面是我的代码:

C++代码
  1. …  
  2. extern struct net init_net;  
  3. …  
  4. inline void printIP(__u32 uip)  
  5. {  
  6.     printk(NIPQUAD_FMT,NIPQUAD(uip));  
  7. }  
  8. …  
  9. int xxxxxx()  
  10. {  
  11.     …  
  12.     int err;  
  13.     struct rtable * rt = NULL;  
  14.     struct flowi fl = {  
  15.         .nl_u = {  
  16.             .ip4_u = {  
  17.                 .daddr = 0,  
  18.                 .saddr = 0,  
  19.                 .tos = 0,  
  20.             },  
  21.         },  
  22.         .oif = 0,  
  23.     };  
  24.     fl.nl_u.ip4_u.daddr = in_aton("182.168.1.1");  
  25.     fl.nl_u.ip4_u.saddr = in_aton("192.168.0.186");  
  26.     err = ip_route_output_key(&init_net, &rt, &fl);  
  27.     if(rt)  
  28.     {  
  29.         if(rt->idev&&rt->idev->dev&&rt->idev->dev->name)  
  30.             printk(" if:%sn",rt->idev->dev->name);  
  31.         printk(" gw:");  
  32.         printIP(rt->rt_gateway);  
  33.         printk("n dst:");  
  34.         printIP(rt->rt_dst);  
  35.         printk("n src:");  
  36.         printIP(rt->rt_src);  
  37.         printk("n");  
  38.     }  
  39.     else  
  40.         printk("rt = NULL!n");  
  41.     …  
  42. }  

 暂时只找到这种实现方式,有新的发现再来更新:)

Debian下单网卡绑定两个IP配置

所谓的双IP有两种,一种是IP别名( IP aliases)一种是附属IP(secondary IP addresses)
这里给出的是比较常用的IP别名的方法:

…#vim /etc/network/interfaces
 

配置文件内容:

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet static
address 192.168.0.186
netmask 255.255.255.0
broadcast 192.168.0.255
network 192.168.0.0
gateway 192.168.0.51

auto eth0:0
iface eth0:0 inet static
address 192.168.0.187
netmask 255.255.255.0
broadcast 192.168.0.255
network 192.168.0.0

重新启动网络服务:
…#/etc/init.d/networking restart 

80 年代以来的操作系统 GUI 设计进化史

70年代,施乐公司 Xerox Palo Alto Research Center (PARC) 的 研究人员开发了第一个 GUI 图形用户界面,开启了计算机图形界面的新纪元,80年代以来,操作系统的界面设计经历了众多变迁,OS/2, Macintosh, Windows, Linux, Symbian OS ,各种操作系统将 GUI 设计带进新的时代。本文介绍了80年代以来各种操作系统 GUI 界面设计的进化史。

第一个使用现代图形界面的个人电脑是 Xerox Alto,设计于1973年,该系统并未商用,主要用于研究和大学。

1Source: toastytech.com

 

1981-1985

Xerox 8010 Star (1981年推出)

这是第一台全集成桌面电脑,包含应用程序和图形用户界面(GUI),一开始叫 The Xerox Star,后改名为 ViewPoint, 最后又改名为 GlobalView。

Xerox 8010 Star
Xerox 8010 Star, Source: toastytech.com


Apple Lisa Office System 1 (1983)

又称 Lisa OS,这里的 OS 是 Office System 的缩写,苹果开发这款机器的初衷是作为文档处理工作站。不幸的是,这款机器的寿命并不长,很快被更便宜的 Macintosh 操作系统取代。Lisa OS 几个升级包括 1983年的 Lisa OS2, 1984年的 Lisa OS 7/7 3.1。

Apple Lisa 1
Apple Lisa OS 1, Source: GUIdebook


Apple Lisa OS 1
Apple Lisa OS 1, Source: GUIdebook


VisiCorp Visi On (1984)

Visi On 是为 IBM PC 开发的第一款桌面 GUI,该系统面向大型企业,价格昂贵,使用基于鼠标的 GUI,它内置安装程序与帮助系统,但尚未使用图标。

Visi On
VisiCoprt Visi On, Source: toastytech.com


Visi On
VisiCoprt Visi On, Source: toastytech.com


Mac OS System 1.0 (1984)

System 1.0 是最早的 Mac 操作系统 GUI,已经拥有现代操作系统的几项特点,基于窗体, 使用图标。窗体可以用鼠标拖动,文件与文件夹可以通过拖放进行拷贝。

Mac OS 1
Apple Mac System 1.0, Source: toastytech.com


Amiga Workbench 1.0 (1985)

一经发布,Amiga 就领先时代。它的 GUI 包含诸如彩色图形(四色:黑,白,蓝,橙),初级多任务,立体声以及多状态图标(选中状态和未选中状态)

Amiga Workbench 1.0
Amiga Workbench 1.0, Source: GUIdebook


Amiga Workbench 1.0
Amiga Workbench 1.0, Source: GUIdebook


Windows 1.0x (1985)

1985年,微软终于在图形用户界面大潮中占据了一席之地,Windows 1.0 是其第一款基于 GUI 的操作系统 。使用了 32×32 像素的图标以及彩色图形,其最有趣的功能是模拟时钟动画图标。

Windows 1
Microsoft Windows 1.01, Source: makowski-berlin.de


Windows 1
Microsoft Windows 1.01, Source: makowski-berlin.de


1986 – 1990

IRIX 3 (released in 1986, first release 1984)

64为 IRIX 操作系统是为 Unix 设计的,它的一个有趣功能是支持矢量图标,这个功能远在 Max OS X 面世前就出现了。

IRIX 3
Silicon Graphics IRIX 3.0, Source: osnews.com


Windows 2.0x (1987)

这个版本的 Windows 操作系统中对 Windows 的管理有了很大改进,Windows 可以交叠,改变大小,最大化或最小化。

Windows 2
Microsoft Windows 2.03, Source: guidebookgallery.org


Windows 2
Microsoft Windows 2.03, Source: guidebookgallery.org


OS/2 1.x (released in 1988)

OS/2 最早由 IBM 和微软合作开发,然而1991年,随着微软将这些技术放到自己的 Windows 操作系统,两家公司决裂,IBM 继续开发 OS/2,OS/2 使用的 GUI 被称为 “Presentation Manager (展示管理)”,这个版本的 GUI 只支持单色,以及固定图标。

Os 2 1
Microsoft-IBM OS/2 1.1, Source: pages.prodigy.net


Os/2 1
Microsoft-IBM OS/2 1.1, Source: pages.prodigy.net


NeXTSTEP / OPENSTEP 1.0 (1989)

Steve Jobs 心血来潮,想为大学和研究机构设计一款完美的电脑,他的这个设想后来造就了一家叫做  NeXT Computer 的公司。

第一台 NeXT 计算机于1988年发布,不过到了1989年随着 NeXTSTEP 1.0 GUI 的发布才取得显著进展,该 GUI 后来演变成 OPENSTEP。

该 GUI 的图标很大,48×48像素,包含更多颜色,一开始是单色的,从1.0开始支持彩色,下图中已经可以看到现代 GUI 的影子。

Nextstep 1
NeXTSTEP 1.0, Source: kernelthread.com


OS/2 1.20 (1989)

OS/2 的下一个小版本在很多方面都做了改进,图标看上去更好看,窗体也显得更平滑。

Os 2 12
OS/2 1.2, Source pages.prodigy.net


Windows 3.0 (1990)

到 Windows 3.0, 微软真正认识到 GUI 的威力,并对之进行大幅度改进。该操作系统已经支持标准或386增强模式,在增强模式中,可以使用640K以上的扩展内存,让更高的屏幕分辨率和更好的图 形成为可能,比如可以支持 SVGA 800×600 或 1024×768。

同时,微软聘请 Susan Kare 设计 Windows 3.0 的图标,为  GUI 注入统一的风格。

Windows 3
Microsoft Windows 3.0, Source: toastytech.com


Windows 3
Microsoft Windows 3.0, Source: toastytech.com


1991 – 1995

Amiga Workbench 2.04 (1991)

该版 GUI 包含很多改进,桌面可以垂直分割成不同分辨率和颜色深度,在现在看来似乎有些奇怪。默认的分辨率是 640×256,不过硬件支持更高的分辨率。

Amiga Workbench 2
Commodore Amiga Workbench 2.04, Source: guidebookgallery.org


Mac OS System 7 (released in 1991)

Mac OS 7 是第一款支持彩色的 GUI,图标中加入了微妙的灰,蓝,黄阴影。

Macos 7
Apple Mac OS System 7.0, Source: guidebookgallery.org


Macos 7
Apple Mac OS System 7.0, Source: guidebookgallery.org


Windows 3.1 (1992)

该版本的 Windows 支持预装的 TrueType 字体,第一次使 Windows 成为可以用于印刷的系统。Windows 3.0 中,只能通过 Adobe 字体管理器(ATM)实现该功能。该版本同时包含一个叫做 Hotdog Stand 的配色主题。

配色主题可以帮助某些色盲患者更容易看清图形。

windows_311_workspace
Source: Wikipedia


OS/2 2.0 (1992)

这是第一个获得世界认可并通过可用性与可访问性测试的 GUI,整个 GUI 基于面向对象模式,每个文件和文件夹都是一个对象,可以同别的文件,文件夹与应用程序关联。它同时支持拖放式操作以及模板功能。

Os 2 2
IBM OS/2 2.0, Source: toastytech.com


Os 2 2
IBM OS/2 2.0, Source: toastytech.com


Windows 95 (1995)

Windows 3.x 之后,微软对整个用户界面进行了重新设计,这是第一个在窗口上加上关闭按钮的 Windows 版本。图标被赋予了各种状态(有效,无效,被选中等),那个著名的“开始”按钮也是第一次出现。对操作系统和 GUI 而言,这是微软的一次巨大飞跃。

Windows 95
Microsoft Windows 95, Source: guidebookgallery.org


Windows 95
Microsoft Windows 95, Source: guidebookgallery.org


1996 – 2000

OS/2 Warp 4 (1996)

IBM 终于争气地推出了 OS/2 Warp 4。桌面上可以放置图标,也可以自己创建文件和文件夹,并推出一个类似 Windows 回收站和 Mac 垃圾箱的文件销毁器,不过一旦放进去进不能再恢复。

Os 2 Warp 4
IBM OS/2 Warp 4, Source: toastytech.com


Os 2 Warp 4
IBM OS/2 Warp 4, Source: toastytech.com


Mac OS System 8 (1997)

该版本的 GUI 支持默认的256色图标,Mac OS 8 最早采用了伪3D图标,其灰蓝色彩主题后来成为 Mac OS GUI 的标志。

Macos 8
Apple Mac OS 8, Source: guidebookgallery.org


Windows 98 (1998)

图标风格和 Windows 95 几无二致,不过整个 GUI 可以使用超过256色进行渲染,Windows 资源管理器改变巨大,第一次出现活动桌面。

Windows 98
Microsoft Windows 98, Source: toastytech.com


KDE 1.0 (1998)

KDE是 Linux 的一个统一图形用户界面环境。

Kde 1
KDE 1.0, Source: ditesh.gathani.org


GNOME 1.0 (1999)

GNOME 桌面主要为 Red Hat Linux 开发,后来也被别的 Linux 采用。

Gnome 1
Red Hat Linux GNOME 1.0.39, Source: visionfutur.com


2001 – 2005

Mac OS X (released in 2001)

2000年初,苹果宣布推出其 Aqua 界面,2001年,推出全新的操作系统 Mac OS X。默认的 32×32, 48×48 被更大的 128×128 平滑半透明图标代替。

该 GUI 一经推出立即招致大量批评,似乎用户都如此大的变化还不习惯,不过没过多久,他们呢就接受了这种新风格,如今这种风格已经成了 Mac OS 的招牌。

Mac osx 1
Apple Mac OS X 10.1 Source: guidebookgallery.org


Windows XP (released in 2001)

每一次微软推出重要的操作系统版本,其 GUI 也必定有巨大的改变,Windows XP 也不例外,这个 GUI 支持皮肤,用户可以改变整个 GUI 的外观与风格,默认图标为 48×48,支持上百万颜色。

Windows xp
Microsoft Windows XP Professional, Source: guidebookgallery.org


KDE 3 (released in 2002)

自1.0版发布以来,KDE 有了长足的改进,对所有图形和图标进行了改进并统一了用户体验。

Kde 3
KDE 3.0.1, Source: netbsd.org


2007 – 2009 (current)

Windows Vista (released in 2007)

这是微软向其竞争对手做出的一个挑战,Vista 中同样包含很多 3D 和动画,自 Windows 98 以来,微软一直尝试改进桌面,在 Vista 中,他们使用类似饰件的机制替换了活动桌面。

Windows Vista
Microsoft Windows Vista, Source: technology.berkeley.edu

 

 

 

Mac OS X Leopard (released in 2007)

对于第六代 Max OS X,苹果又一次对用户界面做出改进。基本的 GUI 仍是 Aqua,但看上去更 3D 一些,也包含了 3D 停靠坞以及很多动画与交互功能。

Mac osx Leopard
Apple Mac OS X 10.5 Leopard, Source: skattertech.com

 

KDE (v4.0 Jan. 2009, v4.2 Mar. 2009)

KDE 4 的 GUI 提供了很多新改观,如动画的,平滑的,有效的窗体管理,图标尺寸可以很容易调整,几乎任何设计元素都可以轻松配置。相对前面的版本绝对是一个巨大的改进。

kde
Source: Wikipedia

 

鸣谢

延伸阅读

苹果的设计演化史:1977-2008

本文国际来源:http://www.webdesignerdepot.com/2009/03/operating-system-interface-design-between-1981-2009/
中文翻译来源:COMSHARP CMS 官方网站

内存屏障机制及内核相关源代码分析

内存屏障机制及内核相关源代码分析
分析人:余旭
分析版本:Linux Kernel 2.6.14 来自于:www.kernel.org
分析开始时间:2005-11-17-20:45:56
分析结束时间:2005-11-21-20:07:32
编号:2-1 类别:进程管理-准备工作1-内存屏障
Email:yuxu9710108@163.com
版权声明:版权保留。本文用作其他用途当经作者本人同意,转载请注明作者姓名
All Rights Reserved. If for other use,must Agreed By the writer.Citing this text,please claim the writer’s name.
Copyright (C) 2005 YuXu
*************************************************************
内存屏障是Linux Kernel 中常要遇到的问题,这里专门来对其进行研究。一者查阅网上现有资料,进行整理汇集;二者翻阅Linux 内核方面的指导书,从中提炼观点;最
后,自己加以综合分析,提出自己的看法。下面将对个问题进行专题分析。
*****************************************************************************
—————————————————— 专题研究:内存屏障——————————–
———————————————————论坛众人资料汇集分析—————————
set_current_state(),__set_current_state(),set_task_state(),__set_task_state(),rmb(),wmb(),mb()的源代码中的相关疑难问题及众人的论坛观点:
—————————————————————————————————————–
1.—>ymons 在www.linuxforum.net Linux 内核技术论坛发贴问:
set_current_state 和__set_current_state 的区别?
#define __set_current_state(state_value)
do { current->state = (state_value); } while (0)
#define set_current_state(state_value)
set_mb(current->state, (state_value))
#define set_mb(var, value) do { var = value; mb(); } while (0)
#define mb() __asm__ __volatile__ ("" : : : "memory")
在linux 的源代码中经常有这种设置当前进程状态的代码,但我搞不清楚这两种用法的不同?有哪位大虾指点一二,必将感谢不尽!
——————
2.—> chyyuu(chenyu-tmlinux@hpclab.cs.tsinghua.edu.cn) 在www.linuxforum.net 的Linux 内核技术上发贴问:
在kernel.h 中有一个define
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
在内核许多地方被调用,不知到底是生成什么汇编指令????
请教!!!
——————–
3.—>tigerl 02-12-08 10:57 在www.linuxforum.net 的Linux 内核技术提问:
这一句(include/asm-i386/system.h 中)定义是什么意思?
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
4.—>jackcht 01-03-02 10:55 在www.linuxforum.net Linux 内核技术 :
各位大虾,我在分析linux 的时候发现有一个古怪的函数,就是barrier,俺愣是不知道它是干嘛用的,帮帮我这菜鸟吧,感谢感谢!
还有就是下面这句中的("":::"memory")是什么意思呀,我苦!
# define barrier() _asm__volatile_("": : :"memory")
***********************************众人的观点*******************************
ANSWER:
1.jkl Reply:这就是所谓的内存屏障,前段时间曾经讨论过。CPU 越过内存屏障后,将刷新自已对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc 在
barrier()之后刷新寄存器对变量的分配。
2.wheelz 发帖指出:
#define __set_task_state(tsk, state_value)
do { (tsk)->state = (state_value); } while (0)
#define set_task_state(tsk, state_value)
set_mb((tsk)->state, (state_value))
set_task_state()带有一个memory barrier,__set_task_state()则没有,当状态state 是RUNNING 时,因为scheduler 可能访问这个state,因此此时要变成其
他状态(如INTERRUPTIBLE),就要用set_task_state()而当state 不是RUNNING 时,因为没有其他人会访问这个state,因此可以用__set_task_state()反正用
set_task_state()肯定是安全的,但__set_task_state()可能会快些。
自己分析:
wheelz 讲解很清楚,尤其是指出了__set_task_state()速度会快于set_task_state()。这一点,很多贴子忽略了,这里有独到之处。在此,作者专门强调之。
3.自己分析:
1)set_mb(),mb(),barrier()函数追踪到底,就是__asm__ __volatile__("":::"memory"),而这行代码就是内存屏障。
2)__asm__用于指示编译器在此插入汇编语句
3)__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
4)memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要
的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。
5)"":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。在后文将讨论什么叫串行化指令。
6)__asm__,__volatile__,memory 在前面已经解释
7)lock 前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu 的一个内存屏障。
8)addl $0,0(%%esp)表示将数值0 加到esp 寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在
此利用这条无价值的汇编指令来配合lock 指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的内存屏障。
9)set_current_state()和__set_current_state()区别就不难看出。
10)至于barrier()就很易懂了。
11)作者注明:作者在回答这个问题时候,参考了《深入理解LINUX 内核》一书,陈莉君译,中国电力出版社,P174
4.xshell 发贴指出:
#include <asm/system.h>
"void rmb(void);"
"void wmb(void);"
"void mb(void);"
这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb
保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比
如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O 操作进行类似优化很可能造成致命错误。所
以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。其实在指令序列中放一个wmb 的效果是使得指令执行到该处时,把所有缓存的数据写到 该写的地方,
同时使得wmb 前面的写指令一定会在wmb 的写指令之前执行。
5.Nazarite 发贴指出:
__volatitle__是防止编译器移动该指令的位置或者把它优化掉。"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值。lock 前缀
是让cpu 的执行下一行指令之前,保证以前的指令都被正确执行。
再次发贴指出:
The memory keyword forces the compiler to assume that all memory locations in RAM have been changed by the assembly language instruction;
therefore, the compiler cannot optimize the code by using the values of memory locations stored in CPU registers before the asm instruction.
6.bx bird 发贴指出:
cpu 上有一根pin #HLOCK 连到北桥,lock 前缀会在执行这条指令前先去拉这根pin,持续到这个指令结束时放开#HLOCK pin,在这期间,北桥会屏蔽掉一切外设以及
AGP 的内存操作。也就保证了这条指令的atomic。
7.coldwind 发贴指出:
"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值,应该是告诉CPU 内存已经被修改过,让CPU invalidate 所有的cache。
通过以上众人的贴子的分析,自己综合一下,这4 个宏set_current_state(),__set_current_state(),set_task_state(),__set_task_state()和3 个函数
rmb(),wmb(),mb()的源代码中的疑难大都被解决。此处只是汇集众人精彩观点,只用来解决代码中的疑难,具体有序系统的源代码将在后面给出。
————————————————————————————————————–
mfence,mb(),wmb(),OOPS 的疑难问题的突破
————————————————————————————————————–
1.—>puppy love (zhou_ict@hotmail.com )在www.linuxforum.net CPU 与 编译器 问: 在linux 核心当中, mb(x86-64)的实现是 ("mfence":::"memory")
我查了一下cpu 的manual,mfence 用来同步指令执行的。而后面的memory clober 好像是gcc 中用来干扰指令调度的。但还是不甚了了,哪位能给解释解释吗? 或
者有什么文档之类的可以推荐看看的?
ANSWER:
1.classpath 发贴指出:
mfence is a memory barrier supported by hardware, and it only makes sense for shared memory systems.
For example, you have the following codes
<codes1>
mfence
<codes2>
mfence or other memory barriers techniques disallows the code motion (load/store)from codes2 to codes1 done by _hardware_ . Some machines like
P4 can move loads in codes 2 before stores in codes1, which is out-of-order.
Another memory barrier is something like
("":::"memory"),
which disallows the code motion done by _compiler_. But IMO memory access order is not always guaranteed in this case.
—–
2.canopy 发贴指出:
我稍微看了一下x86-64 的手册。mfence 保证系统在后面的memory 访问之前,先前的memory 访问都已经结束。由于这条指令可能引起memory 任意地址上内容
的改变,所以需要用“memory” clobber 告诉gcc 这一点。这样gcc 就需要重新从memory 中load 寄存器来保证同一变量在寄存器和memory 中的内容一致。
——————
3.cool_bird Reply:
内存屏障
MB(memory barrier,内存屏障) :x86 采用PC(处理机)内存一致性模型,使用MB 强加的严格的CPU 内存事件次序,保证程序的执行看上去象是遵循顺序一致性(SC)
模型,当然,即使对于UP,由于内存和设备见仍有一致性问题,这些Mb 也是必须的。在当前的实现中,wmb()实际上是一个空操作,这是因为目前Intel 的CPU 系列
都遵循“处理机一致性”,所有的写操作是遵循程序序的,不会越过前面的读写操作。但是,由于Intel CPU 系列可能会在将来采用更弱的内存一致性模型并且其他体系结
构可能采用其他放松的一致性模型,仍然在内核里必须适当地插入wmb()保证内存事件的正确次序。
见头文件include/asm/system.h
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
#define rmb() mb()
#define wmb() __asm__ __volatile__ ("": : :"memory")
此外,barrier 实际上也是内存屏障。
include/linux/kernel.h:
#define barrier() __asm__ __volatile__("": : :"memory")
内存屏障也是一种避免锁的技术。
它在进程上下文中将一个元素插入一个单向链表:
new->next=i->next;
wmb();
i->next=new;
同时,如果不加锁地遍历这个单向链表。或者在遍历链表时已经可以看到new,或者new 还不在该链表中。Alan Cox 书写这段代码时就注意到了这一点,两个内存写事
件的顺序必须按照程序顺序进行。否则可能new 的next 指针将指向一个无效地址,就很可能出现OOPS!
不论是gcc 编译器的优化还是处理器本身采用的大量优化,如Write buffer, Lock-up free, Non-blocking reading, Register allocation, Dynamic scheduling,
Multiple issues 等,都可能使得实际执行可能违反程序序,因此,引入wmb 内存屏障来保证两个写事件的执行次序严格按程序顺序来执行。
作者说明:原贴子不太清楚,作者作了必要的调整。
**************************************************************************
作者读到这里,不懂OOPS 便又上网查找OOPS 的资料学习如下,以期望搞懂OOPS 后能更好的理解上面这段话。
——————————————OOPS 解释————————————————–
1.网上第一个贴子:
—>殊途同归 发表于 2005-7-26 16:40:00 :掌握 Linux 调试技术 来自中国教育人博客:www.blog.edu.cn/index.html
Oops 分析
Oops(也称panic,慌张)消息包含系统错误的细节,如CPU 寄存器的内容。在 Linux 中,调试系统崩溃的传统方法是分析在发生崩溃时发送到系统控制台的 Oops
消息。一旦您掌握了细节,就可以将消息发送到ksymoops 实用程序,它将试图将代码转换为指令并将堆栈值映射到内核符号。在很多情况下,这些信息就足够您确定错
误的可能原因是什么了。请注意,Oops 消息并不包括核心文件。
2.网上第二个贴子:
—>www.plinux.org 自由飞鸽 上的贴子:System.map 文件的作用 作者:赵炯
gohigh@sh163.net
作者说明:
1.OOPS 和System.map 文件密切相关。所以要研讨System.map 文件。
2.本作者对所引用的文章内容进行了整理,删除了一些次要的部分,插入了一些内容,使文章更清晰。再者对一些内容进行了扩展说明。
—>符号表:
1.什么是符号(Symbols)?
在编程中,一个符号(symbol)是一个程序的创建块:它是一个变量名或一个函数名。如你自己编制的程序一样,内核具有各种符号也是不应该感到惊奇的。当然,区别在
于内核是一非常复杂的代码块,并且含有许多、许多的全局符号。
2.内核符号表(Kernel Symbol Table)是什么东西?
内核并不使用符号名。它是通过变量或函数的地址(指针)来使用变量或函数的,而 不是使用size_t BytesRead,内核更喜欢使用(例如)c0343f20 来引用这个变量。
而另一方面,人们并不喜欢象c0343f20 这样的名字。我们跟喜欢使用象 size_t BytesRead 这样的表示。通常,这并不会带来什么问题。内核主要是用C 语言写成的,
所以在我们编程时编译器/连接程序允许我们使用符号名,并且使内核在运行时使用地址表示。这样大家都满意了。
然而,存在一种情况,此时我们需要知道一个符号的地址(或者一个地址对应的 符号)。这是通过符号表来做到的,与gdb 能够从一个地址给出函数名(或者给出一个
函数名的地址)的情况很相似。符号表是所有符号及其对应地址的一个列表。这里是 一个符号表例子:
c03441a0 B dmi_broken
c03441a4 B is_sony_vaio_laptop
c03441c0 b dmi_ident
c0344200 b pci_bios_present
c0344204 b pirq_table
c0344208 b pirq_router
c034420c b pirq_router_dev
c0344220 b ascii_buffer
c0344224 b ascii_buf_bytes
你可以看出名称为dmi_broken 的变量位于内核地址c03441a0 处。
—>;System.map 文件与ksyms:
1.什么是System.map 文件?
有两个文件是用作符号表的:
/proc/ksyms
System.map
这里,你现在可以知道System.map 文件是干什么用的了。每当你编译一个新内核时,各种符号名的地址定会变化。
/proc/ksyms 是一个 "proc 文件" 并且是在内核启动时创建的。实际上 它不是一个真实的文件;它只是内核数据的简单表示形式,呈现出象一个磁盘文件似 的。如果
你不相信我,那么就试试找出/proc/ksyms 的文件大小来。因此, 对于当前运行的内核来说,它总是正确的..
然而,System.map 却是文件系统上的一个真实文件。当你编译一个新内核时,你原来的System.map 中的符号信息就不正确了。随着每次内核的编译,就会产生一个
新的 System.map 文件,并且需要用该文件取代原来的文件。
—>OOPS:
1.什么是一个Oops?
在自己编制的程序中最常见的出错情况是什么?是段出错(segfault),信号11。
Linux 内核中最常见的bug 是什么?也是段出错。除此,正如你想象的那样,段出 错的问题是非常复杂的,而且也是非常严重的。当内核引用了一个无效指针时,并不称
其为段出错 — 而被称为"oops"。一个oops 表明内核存在一个bug,应该总是提出报告并修正该bug。
2.OOPS 与段违例错的比较:
请注意,一个oops 与一个段出错并不是一回事。你的程序并不能从段出错中恢复 过来,当出现一个oops 时,并不意味着内核肯定处于不稳定的状态。Linux 内核是非
常健壮的;一个oops 可能仅杀死了当前进程,并使余下的内核处于一个良好的、稳定的状态。
3.OOPS 与panic 的比较:
一个oops 并非是内核死循环(panic)。在内核调用了panic()函数后,内核就不能 继续运行了;此时系统就处于停顿状态并且必须重启。如果系统中关键部分遭到破坏 那
么一个oops 也可能会导致内核进入死循环(panic)。例如,设备驱动程序中 出现的oops 就几乎不会导致系统进行死循环。
当出现一个oops 时,系统就会显示出用于调试问题的相关信息,比如所有CPU 寄存器中的内容以及页描述符表的位置等,尤其会象下面那样打印出EIP(指令指针)的内
容:
EIP: 0010:[<00000000>]
Call Trace: []
4.一个Oops 与System.map 文件有什么关系呢?
我想你也会认为EIP 和Call Trace 所给出的信息并不多,但是重要的是,对于内核开发人员来说这些信息也是不够的。由于一个符号并没有固定的地址, c010b860 可
以指向任何地方。
为了帮助我们使用oops 含糊的输出,Linux 使用了一个称为klogd(内核日志后台程序)的后台程序,klogd 会截取内核oops 并且使用syslogd 将其记录下来,并将某些
象c010b860 信息转换成我们可以识别和使用的信息。换句话说,klogd 是一个内核消息记录器(logger),它可以进行名字-地址之间的解析。一旦klogd 开始转换内核
消息,它就使用手头的记录器,将整个系统的消息记录下来,通常是使用syslogd 记录器。
为了进行名字-地址解析,klogd 就要用到System.map 文件。我想你现在知道一个oops 与System.map 的关系了。
———————
作者补充图:
System.map 文件
^
|
|
syslogd 记录——->klogd 解析名字-地址
^
|
|
内核出错—–>OOPS
———————–
深入说明: klogd 会执行两类地址解析活动:
1.静态转换,将使用System.map 文件。 所以得知System.map 文件只用于名字-地址的静态转换。
2.Klogd 动态转换
动态转换,该方式用于可加载模块,不使用System.map,因此与本讨论没有关系,但我仍然对其加以简单说明。假设你加载了一个产生oops 的内核模块。于是就会产
生一个oops 消息,klogd 就会截获它,并发现该oops 发生在d00cf810 处。由于该地址属于动态加载模块,因此在System.map 文件中没有对应条目。klogd 将会在
其中寻找并会毫无所获,于是断定是一个可加载模块产生了oops。此时klogd 就会向内核查询该可加载模块输出的符号。即使该模块的编制者没有输出其符号,klogd
也起码会知道是哪个模块产生了oops,这总比对一个oops 一无所知要好。
还有其它的软件会使用System.map,我将在后面作一说明。
————–
System.map 应该位于什么地方?
System.map 应该位于使用它的软件能够寻找到的地方,也就是说,klogd 会在什么地方寻找它。在系统启动时,如果没有以一个参数的形式为klogd 给出System.map
的位置,则klogd 将会在三个地方搜寻System.map。依次为:
/boot/System.map
/System.map
/usr/src/linux/System.map
System.map 同样也含有版本信息,并且klogd 能够智能化地搜索正确的map 文件。例如,假设你正在运行内核2.4.18 并且相应的map 文件位于/boot/System.map。
现在你在目录/usr/src/linux 中编译一个新内核2.5.1。在编译期间,文件/usr/src/linux/System.map 就会被创建。当你启动该新内核时,klogd 将首先查询
/boot/System.map,确认它不是启动内核正确的map 文件,就会查询/usr/src/linux/System.map, 确定该文件是启动内核正确的map 文件并开始读取其中的符号
信息。
几个注意点:
1.klogd 未公开的特性:
在2.5.x 系列内核的某个版本,Linux 内核会开始untar 成linux-version,而非只是linux(请举手表决–有多少人一直等待着这样做?)。我不知道klogd 是否已经修改
为在/usr/src/linux-version/System.map 中搜索。TODO:查看klogd 源代码。
在线手册上对此也没有完整描述,请看:
# strace -f /sbin/klogd | grep ‘System.map’
31208 open("/boot/System.map-2.4.18", O_RDONLY|O_LARGEFILE) = 2
显然,不仅klogd 在三个搜索目录中寻找正确版本的map 文件,klogd 也同样知道寻找名字为 "System.map" 后加"-内核版本",象 System.map-2.4.18. 这是klogd
未公开的特性。
2.驱动程序与System.map 文件的关系:
有一些驱动程序将使用System.map 来解析符号(因为它们与内核头连接而非glibc 库等),如果没有System.map 文件,它们将不能正确地工作。这与一个模块由于内
核版本不匹配而没有得到加载是两码事。模块加载是与内核版本有关,而与即使是同一版本内核其符号表也会变化的编译后内核无关。
3.还有谁使用了System.map?
不要认为System.map 文件仅对内核oops 有用。尽管内核本身实际上不使用System.map,其它程序,象klogd,lsof,
satan# strace lsof 2>&1 1> /dev/null | grep System
readlink("/proc/22711/fd/4", "/boot/System.map-2.4.18", 4095) = 23
ps,
satan# strace ps 2>&1 1> /dev/null | grep System
open("/boot/System.map-2.4.18", O_RDONLY|O_NONBLOCK|O_NOCTTY) = 6
以及其它许多软件,象dosemu,需要有一个正确的System.map 文件。
4.如果我没有一个好的System.map,会发生什么问题?
假设你在同一台机器上有多个内核。则每个内核都需要一个独立的System.map 文件!如果所启动的内核没有对应的System.map 文件,那么你将定期地看到这样一条
信息:
System.map does not match actual kernel (System.map 与实际内核不匹配)
不是一个致命错误,但是每当你执行ps ax 时都会恼人地出现。有些软件,比如dosemu,可能不会正常工作。最后,当出现一个内核oops 时,klogd 或ksymoops
的输出可能会不可靠。
5.我如何对上述情况进行补救?
方法是将你所有的System.map 文件放在目录/boot 下,并使用内核版本号重新对它们进行命名。
5-1.假设你有以下多个内核:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.13
那么,只需对应各内核版本对map 文件进行改名,并放在/boot 下,如:
/boot/System.map-2.2.14
/boot/System.map-2.2.13
5-2.如果你有同一个内核的两个拷贝怎么办?
例如:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.14.nosound
最佳解决方案将是所有软件能够查找下列文件:
/boot/System.map-2.2.14
/boot/System.map-2.2.14.nosound
但是说实在的,我并不知道这是否是最佳情况。我曾经见到搜寻"System.map-kernelversion",但是对于搜索"System.map-kernelversion.othertext"的情况呢?我
不太清楚。此时我所能做的就是利用这样一个事实:/usr/src/linux 是标准map 文件的搜索路径,所以你的map 文件将放在:
/boot/System.map-2.2.14
/usr/src/linux/System.map (对于nosound 版本)
你也可以使用符号连接:
System.map-2.2.14
System.map-2.2.14.sound
System.map -> System.map-2.2.14.sound
————————————————OOPS 解释完毕———————————————-
学习到这里,OOPS 和system.map 文件,已经有了较深刻的认识。回过头来继续对内存屏障的学习。
******************************************************************************
4.www.21icbbs.com 上的贴子
为了防止编译器对有特定时续要求的的硬件操作进行优化,系统提供了相应的办法:
1,对于由于数据缓冲(比如延时读写,CACHE)所引起的问题,可以把相应的I/O 区设成禁用缓冲。
2,对于编译优化,可以用内存屏障来解决。如:void rmb(void),void wmb(void),void mb(void),分别是读,写,读写 屏障。和void barrier(void).
5.自己分析:
作者查阅了内核注释如下:
———————————————–asm-i386system.h————————————–
内核注释:
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we’re talking
* to devices.
*
* For now, "wmb()" doesn’t actually do anything, as all
* Intel CPU’s follow what Intel calls a *Processor order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU’s to have a weaker ordering,
* but I’d also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases to be a
* nop for these.
*/
自己分析认为:
1.Intel CPU 有严格的“processor order”,已经确保内存按序写,这里的wmb()所以定义的为空操作。
2.内核人员希望Intel CPU 今后能采用弱排序技术,采用真正的内存屏障技术。
3.在非intel 的cpu 上,wmb()就不再为空操作了。
—————————————–内核2.6.14 完整的源代码———————————-
下面的源代码来自于Linux Kernel 2.6.14,开始对其进行一一的全面的分析:
——————————————-includeasm-i386system.h———————————-
—————————————————–alternative()—————————————–
/*
* Alternative instructions for different CPU types or capabilities.
*
* This allows to use optimized instructions even on generic binary kernels.
*
* length of oldinstr must be longer or equal the length of newinstr
* It can be padded with nops as needed.
*
* For non barrier like inlines please define new variants
* without volatile and memory clobber.
*/
#define alternative(oldinstr, newinstr, feature)
asm volatile ("661:nt" oldinstr "n662:n"
".section .altinstructions,"a"n"
" .align 4n"
" .long 661bn" /* label */
" .long 663fn" /* new instruction */
" .byte %c0n" /* feature bit */
" .byte 662b-661bn" /* sourcelen */
" .byte 664f-663fn" /* replacementlen */
".previousn"
".section .altinstr_replacement,"ax"n"
"663:nt" newinstr "n664:n" /* replacement */
".previous" :: "i" (feature) : "memory")
自己分析:
1.alternative()宏用于在不同的cpu 上优化指令。oldinstr 为旧指令,newinstr 为新指令,feature 为cpu 特征位。
2.oldinstr 的长度必须>=newinstr 的长度。不够将填充空操作符。
———————————————————————-
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we’re talking
* to devices.
*
* For now, "wmb()" doesn’t actually do anything, as all
* Intel CPU’s follow what Intel calls a *Processor order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU’s to have a weaker ordering,
* but I’d also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases * to be a nop for these.
*/
/*
* Actually only lfence would be needed for mb() because all stores done by the kernel should be already ordered. But keep a full barrier for now.
*/
自己分析:
这里的内核中的注释,在前面已经作了讲解,主要就是intel cpu 采用Processor order,对wmb()保证其的执行顺序按照程序顺序执行,所以wmb()定义为空操作。如
果是对于对于非intel 的cpu,这时wmb()就不能再是空操作了。
—————————mb()–rmb()–read_barrier_depends()–wmb()——————
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define read_barrier_depends() do { } while(0)
#ifdef CONFIG_X86_OOSTORE
/* Actually there are no OOO store capable CPUs for now that do SSE,but make it already an possibility. */
作者附注:(对内核注释中的名词的解释)
–>OOO:Out of order,乱序执行。
–>SSE:SSE 是英特尔提出的即MMX 之后新一代(当然是几年前了)CPU 指令集,最早应用在PIII 系列CPU 上。
本小段内核注释意即:乱序存储的cpu 还没有问世,故CONFIG_X86_OOSTORE 也就仍未定义的,wmb()当为后面空宏(在__volatile__作用下,阻止编译器重排顺
序优化)。
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define wmb() __asm__ __volatile__ ("": : :"memory")
#endif
————————–
自己分析:
1.lock, addl $0,0(%%esp)在本文开始处已经解决。
lock 前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu 的一个内存屏障。addl $0,0(%%esp)表示将数值0 加到esp 寄存器中,而该寄存器指向栈顶的
内存单元。加上一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock 指令,用作cpu 的内存屏障。
2.mfence 保证系统在后面的memory 访问之前,先前的memory 访问都已经结束。这是mfence 是X86cpu 家族中的新指令。详见后面。
3.新旧指令对比:
——————————-
以前的源代码:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
__asm__用于指示编译器在此插入汇编语句
__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
——————-
现在的源代码:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
————————–
两者比较:
比起以前的源代码来少了__asm__和__volatile__。增加了alternative()宏和mfence 指令。
————————-
而SFENCE 指令(在Pentium III 中引入)和LFENCE,MFENCE 指令(在Pentium 4 和Intel Xeon 处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能。
sfence,lfence,mfence 指令是在后继的cpu 中新出现的的指令。
SFENCE,LFENCE,MFENCE 指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE 指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE 指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE 指令之前的读写操作。
注意:SFENCE,LFENCE,MFENCE 指令提供了比CPUID 指令更灵活有效的控制内存排序的方式。
sfence:在sfence 指令前的写操作当必须在sfence 指令后的写操作前完成。
lfence:在lfence 指令前的读操作当必须在lfence 指令后的读操作前完成。
mfence:在mfence 指令前的读写操作当必须在mfence 指令后的读写操作前完成。
其实这里是用mfence 新指令来替换老的指令串:__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")。
mfence 的执行效果就等效于__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")的执行效果。只不过,__asm__ __volatile__ ("lock; addl
$0,0(%%esp)": : :"memory")是在以前的cpu 平台上所设计的,借助于编译器__asm__,__volatile__,lock 这些指令来实现内存屏障。而在Pentium 4 和Intel Xeon
处理器中由于已经引入了mfence 指令,无须再用这一套指令,直接调用这一条指令即ok。而alternative()宏就是用于这个优化指令的替换,用新的指令来替换老的指
令串。
4.intel cpu 已保证wmb()的顺序完成。wmb()此处定义为空操作。
5.X86_FEATURE_XMM 的解释:
————————————–asm-i386cpufeature.h—————————————-
#define X86_FEATURE_XMM (0*32+25) /* Streaming SIMD Extensions */
************************************************************************
下面对SIMD 进行解释:
————–《计算机系统结构》–郑纬民编–清华大学出版社———
1).指令流:(instruction stream)机器执行的指令序列
2).数据流:(data stream)指令调用的数据序列,包括输入数据和中间结果。
3)Flynn 分类法:
(1)SISD(Single Instrution stream Single Datastream)
单指令流单数据流,对应为传统的顺序处理计算机。
(2)SIMD(Single Instrution stream Multiple Datastream)
单指令流多数据流,对应阵列处理机或并行处理机。
(3)MISD(Multiple Instrution stream Single Datastream)
多指令流单数据流,对应流水线处理机。
(4)MIMD(Multiple Instrution stream Multiple Datastream)
多指令流多数据流,对应多处理机。
*************************************************************************

由于以上几个指令牵涉到多处理器的管理,要彻底弄懂这些代码的原理,必须深入挖掘之,既然遇到了,就一口气吃掉。追根问底,清楚其来龙去脉。
***********************************************************************
—–>来自Baidu 快照,原网页打不开了:多处理器管理
说明:作者对此文进行了参考,由于文章太长,太专业化,作者对其进行了改动处理:
————————————————————————————————
1.IA-32 体系的机制:总线加锁、cache 一致性管理、串行化指令、高级可编程中断控制器、二级缓存、超线程技术:IA-32 体系提供了几种机制来管理和提升连接到同
一系统总线的多个处理器的性能。这些机制包括:
1)总线加锁、cache 一致性管理以实现对系统内存的原子操作、串行化指令(serializing instructions。这些指令仅对pentium4,Intel Xeon, P6,Pentium处理器有效)。
2)处理器芯片内置的高级可编程中断控制器(APIC)。APIC 是在Pentium处理器中被引入IA-32 体系的。
3)二级缓存(level 2, L2)。对于Pentium4,Intel Xeon, P6 处理器,L2 cache 已经紧密的封装到了处理器中。而Pentium,Intel486 提供了用于支持外部L2 cache 的
管脚。
4)超线程技术。这个技术是IA-32 体系的扩展,它能够让一个处理器内核并发的执行两个或两个以上的指令流。
这些机制在对称多处理系统(symmetric-multiprocessing, SMP)中是极其有用的。然而,在一个IA-32 处理器和一个专用处理器(例如通信,图形,视频处理器)共享系统总
线的应用中,这些机制也是适用的。
————————-
2.多处理机制的设计目标是:
1)保持系统内存的完整性(coherency):
当两个或多个处理器试图同时访问系统内存的同一地址时,必须有某种通信机制或内存访问协议来提升数据的完整性,以及在某些情况下,允许一个处理器临时锁定某个内存
区域。
2)保持高速缓存的一致性:
当一个处理器访问另一个处理器缓存中的数据时,必须要得到正确的数据。如果这个处理器修改了数据,那么所有的访问这个数据的处理器都要收到被修改后的数据。
3)允许以可预知的顺序写内存:
在某些情况下,从外部观察到的写内存顺序必须要和编程时指定的写内存顺序相一致。
4)在一组处理器中派发中断处理:
当几个处理器正在并行的工作在一个系统中时,有一个集中的机制是必要的,这个机制可以用来接收中断以及把他们派发到某一个适当的处理器。
5)采用现代操作系统和应用程序都具有的多线程和多进程的特性来提升系统的性能。
—————————
根据本文的需要,将重点讨论内存加锁,串行(serializing instructions)指令,内存排序,加锁的原子操作(locked atomic operations)。
3.系统内存加锁的原子操作:
32 位IA-32 处理器支持对系统内存加锁的原子操作。这些操作常用来管理共享的数据结构(例如信号量,段描述符,系统段页表)。两个或多个处理器可能会同时的修改这些
数据结构中的同一数据域或标志。
处理器应用三个相互依赖的机制来实现加锁的原子操作:
1)可靠的原子操作(guaranteed atomic operations)。
2)总线加锁,使用LOCK#信号和LOCK 指令前缀。
3)缓存完整性协议,保证原子操作能够对缓存中的数据结构执行;这个机制出现在Pentium4,IntelXeon,P6 系列处理器中,这些机制以下面的形式相互依赖。
—>某些基本的内存事务(memory transaction)例如读写系统内存的一个字节)被保证是原子的。也就是说,一旦开始,处理器会保证这个操作会在另一个处理器或总线代
理(bus agent)访问相同的内存区域之前结束。
—>处理器还支持总线加锁以实现所选的内存操作(例如在共享内存中的读-改-写操作),这些操作需要自动的处理,但又不能以上面的方式处理。因为频繁使用的内存数据经
常被缓存在处理器的L1,L2 高速缓存里,原子操作通常是在处理器缓存内部进行的,并不需要声明总线加锁。这里的处理器缓存完整性协议保证了在缓冲内存上执行原子操
作时其他缓存了相同内存区域的处理器被正确管理。
注意到这些处理加锁的原子操作的机制已经像IA-32 处理器一样发展的越来越复杂。于是,最近的IA-32 处理器(例如Pentium 4, Intel Xeon, P6 系列处理器)提供了一
种比早期IA-32 处理器更为精简的机制。
————————————————保证原子操作的情况————————————
4.保证原子操作的情况
Pentium 4, Intel Xeon,P6 系列,Pentium,以及Intel486 处理器保证下面的基本内存操作总被自动的执行:
1)读或写一个字节
2)读或写一个在16 位边界对齐的字
3)读或写一个在32 位边界对齐的双字
Pentium 4, Intel Xeon,P6 系列以及Pentium 处理器还保证下列内存操作总是被自动执行:
1)读或写一个在64 位边界对齐的四字(quadword)
2)对32 位数据总线可以容纳的未缓存的内存位置进行16 位方式访问
(16-bit accesses to uncached memory locations that fit within a 32-bit data bus)
P6 系列处理器还保证下列内存操作被自动执行:
对32 位缓冲线(cache line)可以容纳的缓存中的数据进行非对齐的16 位,32 位,64 位访问.
对于可以被缓存的但是却被总线宽度,缓冲线,页边界所分割的内存区域,Pentium 4, Intel Xeon, P6 family,Pentium以及Intel486 处理器都不保证访问操作是原子的。
Pentium 4, Intel Xeon,P6 系列处理器提供了总线控制信号来允许外部的内存子系统完成对分割内存的原子性访问;但是,对于非对齐内存的访问会严重影响处理器的性
能,因此应该尽量避免。
————————————————————–总线加锁——————————————
5.总线加锁(Bus Locking)
1.Lock 信号的作用:
IA-32 处理器提供了LOCK#信号。这个信号会在某些内存操作过程中被自动发出。当这个输出信号发出的时候,来自其他处理器或总线代理的总线控制请求将被阻塞。软
件能够利用在指令前面添加LOCK 前缀来指定在其他情况下的也需要LOCK 语义(LOCK semantics)。
在Intel386,Intel486,Pentium处理器中,直接调用加锁的指令会导致LOCK#信号的产生。硬件的设计者需要保证系统硬件中LOCK#信号的有效性,以控制多个处理对
内存的访问。
—>注意:
对于Pentium 4, Intel Xeon,以及P6 系列处理器,如果被访问的内存区域存在于处理器内部的高速缓存中,那么LOCK#信号通常不被发出;但是处理器的缓存却要被锁定。
————————————————–自动加锁(Automatic Locking)——- ——————-
6.自动加锁(Automatic Locking)
1.下面的操作会自动的带有LOCK 语义:
1)执行引用内存的XCHG 指令。
2)设置TSS 描述符的B(busy 忙)标志。在进行任务切换时,处理器检查并设置TSS 描述符的busy 标志。为了保证两个处理器不会同时切换到同一个任务。处理器会在
检查和设置这个标志的时遵循LOCK 语义。
3)更新段描述符时。在装入一个段描述符时,如果段描述符的访问标志被清除,处理器会设置这个标志。在进行这个操作时,处理器会遵循LOCK 语义,因此这个描述符不会
在更新时被其他的处理器修改。为了使这个动作能够有效,更新描述符的操作系统过程应该采用下面的方法:
(1)使用加锁的操作修改访问权字节(access-rights byte),来表明这个段描述符已经不存在,同时设置类型变量,表明这个描述符正在被更新。
(2)更新段描述符的内容。这个操作可能需要多个内存访问;因此不能使用加锁指令。
(3)使用加锁操作来修改访问权字节(access-rights byte),来表明这个段描述符存在并且有效。
注意,Intel386 处理器总是更新段描述符的访问标志,无论这个标志是否被清除。Pentium 4, Intel Xeon,P6 系列,Pentium以及Intel486 处理器仅在该标志被清除时才
设置这个标志。
4)更新页目录(page-directory)和页表(page-table)的条目。在更新页目录和页表的条目时,处理器使用加锁的周期(locked cycles)来设置访问标志和脏标志(dirty
flag)。
5)响应中断。发生中断后,中断控制器可能会使用数据总线给处理器传送中断向量。处理器必须遵循LOCK 语义来保证传送中断向量时数据总线上没有其他数据。
————————————————-软件控制的总线加锁—————————————-
7.软件控制的总线加锁
1)总述:
如果想强制执行LOCK 语义,软件可以在下面的指令前使用LOCK 前缀。当LOCK 前缀被置于其他的指令之前或者指令没有对内存进行写操作(也就是说目标操作数在寄存
器中)时,一个非法操作码(invalid-opcode)异常会被抛出。
2)可以使用LOCK 前缀的指令:
1)位测试和修改指令(BTS, BTR, BTC)
2)交换指令(XADD, CMPXCHG, CMPXCHG8B)
3)XCHG 指令自动使用LOCK 前缀
4)单操作数算术和逻辑指令:INC, DEC, NOT, NEG
5)双操作数算术和逻辑指令:ADD, ADC, SUB, SBB, AND, or, XOR
3)注意:
(1)一个加锁的指令会保证对目标操作数所在的内存区域加锁,但是系统可能会将锁定区域解释得稍大一些。
(2)软件应该使用相同的地址和操作数长度来访问信号量(一个用作处理器之间信号传递用的共享内存)。例如,如果一个处理器使用一个字来访问信号量,其他的处理器就
不应该使用一个字节来访问这个信号量。
(3)总线加锁的完整性不受内存区域对齐的影响。在所有更新操作数的总线周期内,加锁语义一直持续。但是建议加锁访问能够在自然边界对齐,这样可以提升系统性能:
任何边界的8 位访问(加锁或不加锁)
16 位边界的加锁字访问。
32 位边界的加锁双字访问。
64 位边界的加锁四字访问。
(4)对所有的内存操作和可见的外部事件来说,加锁的操作是原子的。只有取指令和页表操作能够越过加锁的指令。
(5)加锁的指令能用于同步数据,这个数据被一个处理器写而被其他处理器读。
对于P6 系列处理器来说,加锁的操作使所有未完成的读写操作串行化(serialize)(也就是等待它们执行完毕)。这条规则同样适用于Pentium4 和Intel Xeon 处理器,但有
一个例外:对弱排序的内存类型的读入操作可能不会被串行化。
加锁的指令不应该用来保证写的数据可以作为指令取回。
—————>自修改代码(self-modifying code)
(6)加锁的指令对于Pentium 4, Intel Xeon, P6 family, Pentium, and Intel486 处理器,允许写的数据可以作为指令取回。但是Intel 建议需要使用自修改代码
(self-modifying code)的开发者使用另外一种同步机制。
处理自修改和交叉修改代码(handling self- and cross-modifying code)
处理器将数据写入当前的代码段以实现将该数据作为代码来执行的目的,这个动作称为自修改代码。IA-32 处理器在执行自修改代码时采用特定模式的行为,具体依赖于被修
改的代码与当前执行位置之间的距离。由于处理器的体系结构变得越来越复杂,而且可以在引退点(retirement point)之前推测性地执行接下来的代码(如:P4, Intel Xeon,
P6 系列处理器),如何判断应该执行哪段代码,是修改前地还是修改后的,就变得模糊不清。要想写出于现在的和将来的IA-32 体系相兼容的自修改代码,必须选择下面的两种
方式之一:
(方式1)
将代码作为数据写入代码段;
跳转到新的代码位置或某个中间位置;
执行新的代码;
(方式2)
将代码作为数据写入代码段;
执行一条串行化指令;(如:CPUID 指令)
执行新的代码;
(在Pentium或486 处理器上运行的程序不需要以上面的方式书写,但是为了与Pentium 4, Intel Xeon, P6 系列处理器兼容,建议采用上面的方式。)
需要注意的是自修改代码将会比非自修改代码的运行效率要低。性能损失的程度依赖于修改的频率以及代码本身的特性。
—————>交叉修改代码(cross-modifying code)
处理器将数据写入另外一个处理器的代码段以使得哪个处理器将该数据作为代码执行,这称为交叉修改代码(cross-modifying code)。像自修改代码一样,IA-32 处理器采
用特定模式的行为执行交叉修改代码,具体依赖于被修改的代码与当前执行位置之间的距离。要想写出于现在的和将来的IA-32 体系相兼容的自修改代码,下面的处理器同
步算法必须被实现:
;修改的处理器
Memory_Flag ← 0; (* Set Memory_Flag to value other than 1 *)
将代码作为数据写入代码段;
Memory_Flag ← 1;
;执行的处理器
WHILE (Memory_Flag ≠ 1)
等待代码更新;
ELIHW;
执行串行化指令; (* 例如, CPUID instruction *)
开始执行修改后的代码;
(在Pentium或486 处理器上运行的程序不需要以上面的方式书写,但是为了与Pentium 4, Intel Xeon, P6 系列处理器兼容,建议采用上面的方式。)
像自修改代码一样,交叉修改代码将会比非交叉修改代码的运行效率要低。性能损失的程度依赖于修改的频率以及代码本身的特性。
说明:作者读到这里时,也是对自修改代码和交叉修改代码稍懂一点,再要深入,也备感艰难。
——————————————————-缓存加锁——————————————–
8.缓存加锁
1)加锁操作对处理器内部缓存的影响:
(1)对于Intel486 和Pentium处理器,在进行加锁操作时,LOCK#信号总是在总线上发出,甚至锁定的内存区域已经缓存在处理器cache 中的时候,LOCK#信号也从总
线上发出。
(2)对于Pentium 4, Intel Xeon,P6 系列处理器,如果加锁的内存区域已经缓存在处理器cache 中,处理器可能并不对总线发出LOCK#信号,而是仅仅修改cache 缓存
中的数据,然后依赖cache 缓存一致性机制来保证加锁操作的自动执行。这个操作称为"缓存加锁"。缓存一致性机制会自动阻止两个或多个缓存了同一区域内存的处理器同
时修改数据。
———————————————–访存排序(memory ordering)——– ———————
9.访存排序(memory ordering)
(1)编程排序(program ordering):
访存排序指的是处理器如何安排通过系统总线对系统内存访问的顺序。IA-32 体系支持几种访存排序模型,具体依赖于体系的实现。例如,Intel386 处理器强制执行"编程排
序(program ordering)"(又称为强排序),在任何情况下,访存的顺序与它们出现在代码流中的顺序一致。
(2)处理器排序(processor ordering):
为了允许代码优化,IA-32 体系在Pentium 4, Intel Xeon,P6 系列处理器中允许强排序之外的另外一种模型——处理器排序(processor ordering)。这种排序模型允许
读操作越过带缓存的写操作来提升性能。这个模型的目标是在多处理器系统中,在保持内存一致性的前提下,提高指令执行速度。
—————————–
10.Pentium 和Intel 486 处理器的访存排序:
1)普遍情况:
Pentium和Intel 486 处理器遵循处理器排序访存模型;但是,在大多数情况下,访存操作还是强排序,读写操作都是以编程时指定的顺序出现在系统总线上。除了在下面的
情况时,未命中的读操作可以越过带缓冲的写操作:
—>当所有的带缓冲的写操作都在cache 缓存中命中,因此也就不会与未命中的读操作访问相同的内存地址。
2)I/O 操作访存:
在执行I/O 操作时,读操作和写操作总是以编程时指定的顺序执行。在"处理器排序"处理器(例如,Pentium 4, Intel Xeon,P6 系列处理器)上运行的软件不能依赖Pentium
或Intel486 处理器的强排序。软件应该保证对共享变量的访问能够遵守编程顺序,这种编程顺序是通过使用加锁或序列化指令来完成的。
3)Pentium 4, Intel Xeon, P6 系列处理器的访存排序
Pentium 4, Intel Xeon, P6 系列处理器也是使用"处理器排序"的访存模型,这种模型可以被进一步定义为"带有存储缓冲转发的写排序"(write ordered with
store-buffer forwarding)。这种模型有下面的特点:
———单处理器系统中的排序规则
(1)在一个单处理器系统中,对于定义为回写可缓冲(write-back cacheable)的内存区域,下面的排序规则将被应用:
a.读能够被任意顺序执行。
b.读可以越过缓冲写,但是处理器必须保证数据完整性(self-consistent)。
c.对内存的写操作总是以编程顺序执行,除非写操作执行了CLFUSH 指令以及利用非瞬时的移动指令(MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, MOVNTPD)来执行
流存储操作(streamint stores)。
作者认为:CLFUSH—>CFLUSH,streamint—>streaming???是否原文有误。
d.写可以被缓冲。写不能够预先执行;它们只能等到其他指令执行完毕。
e.在处理器中,来自于缓冲写的数据可以直接被发送到正在等待的读操作。
f.读写操作都不能跨越I/O 指令,加锁指令,或者序列化指令。
g.读操作不能越过LFENCE 和MFENCE 指令。
h.`写操作不能越过SFECE 和MFENCE 指令。
第二条规则(b)允许一个读操作越过写操作。然而如果写操作和读操作都是访问同一个内存区域,那么处理器内部的监视机制将会检测到冲突并且在处理器使用错误的数据
执行指令之前更新已经缓存的读操作。
第六条规则(f)构成了一个例外,否则整个模型就是一个写排序模型(write ordered model)。
注意"带有存储缓冲转发的写排序"(在本节开始的时候介绍)指的是第2 条规则和第6 条规则的组合之后产生的效果。
—————多处理器系统中的排序规则
(2)在一个多处理器系统中,下面的排序规则将被应用:
a.每个处理器使用同单处理器系统一样的排序规则。
b.所有处理器所观察到的某个处理器的写操作顺序是相同的。
c.每个处理器的写操作并不与其它处理器之间进行排序。
例如:在一个三处理器的系统中,每个处理器执行三个写操作,分别对三个地址A, B,C。每个处理器以编程的顺序执行操作,但是由于总线仲裁和其他的内存访问机制,三个处
理器执行写操作的顺序可能每次都不相同。最终的A, B, C 的值会因每次执行的顺序而改变。
——————-
(3)本节介绍的处理器排序模型与Pentium Intel486 处理器使用的模型是一样的。唯一在Pentium 4, Intel Xeon,P6 系列处理器中得到加强的是:
a.对于预先执行读操作的支持。
b.存储缓冲转发,当一个读操作越过一个访问相同地址的写操作。
c.对于长串的存储和移动的无次序操作(out-of-Order Stores)Pentium 4,
——————–
(4)快速串:
Intel Xeon, P6 处理器对于串操作的无次序存储(Out-of-Order Stores)
Pentium 4, Intel
Xeon,P6 处理器在进行串存储的操作(以MOVS 和STOS 指令开始)时,修改了处理器的动作,以提升处理性能。一旦"快速串"的条件满足了(将在下面介绍),处理器将会在
缓冲线(cache line)上以缓冲线模式进行操作。这会导致处理器在循环过程中发出对源地址的缓冲线读请求,以及在外部总线上发出对目标地址的写请求,并且已知了目标地
址内的数据串一定要被修改。在这种模式下,处理器仅仅在缓冲线边界时才会相应中断。因此,目标数据的失效和存储可能会以不规则的顺序出现在外部总线上。
按顺序存储串的代码不应该使用串操作指令。数据和信号量应该分开。依赖顺序的代码应该在每次串操作时使用信号量来保证存储数据的顺序在所有处理器看来是一致的。
"快速串"的初始条件是:
在Pentium III 处理器中,EDI 和ESI 必须是8 位对齐的。在Pentium4 中,EDI 必须是8 位对齐的。
串操作必须是按地址增加的方向进行的。
初始操作计数器(ECX)必须大于等于64。
源和目的内存的重合区域一定不能小于一个缓冲线的大小(Pentium 4 和Intel Xeon 处理器是64 字节;P6 和Pentium处理器是 32 字节)。
源地址和目的地址的内存类型必须是WB 或WC。
—————-
11.加强和削弱访存排序模型(Strengthening or Weakening the Memory ordering Model)
IA-32 体系提供了几种机制用来加强和削弱访存排序模型以处理特殊的编程场合。这些机制包括:
1)I/O 指令,加锁指令,LOCK 前缀,以及序列化指令来强制执行"强排序"。
2)SFENCE 指令(在Pentium III 中引入)和LFENCE,MFENCE 指令(在Pentium 4 和Intel Xeon 处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能。
3)内存类型范围寄存器(memory type range registers (MTRRs))可以被用来加强和削弱物理内存中特定区域的访存排序模型。MTRRs 只存在于Pentium 4, Intel
Xeon, P6 系列处理器。
4)页属性表可以被用来加强某个页或一组页的访存排序("页属性表"Page Attribute Table(PAT))。PAT 只存在于Pentium 4, Intel Xeon,P6 系列处理器。
这些机制可以通过下面的方式使用:
1)内存映射和其他I/O 设备通常对缓冲区写操作的顺序很敏感。I/O 指令(IN,OUT)以下面的方式对这种访问执行强排序。在执行一条I/O 指令之前,处理器等待之前的所
有指令执行完毕以及所有的缓冲区都被写入了内存。只有取指令操作和页表查询(page table walk)能够越过I/O 指令。后续指令要等到I/O 指令执行完毕才开始执行。
2)一个多处理器的系统中的同步机制可能会依赖"强排序"模型。这里,一个程序使用加锁指令,例如XCHG 或者LOCK 前缀,来保证读-改-写操作是自动进行的。加锁操作
像I/O 指令一样等待所有之前的指令执行完毕以及缓冲区都被写入了内存。
3)程序同步可以通过序列化指令来实现。这些指令通常用于临界过程或者任务边界来保证之前所有的指令在跳转到新的代码区或上下文切换之前执行完毕。像I/O 加锁指
令一样,处理器等待之前所有的指令执行完毕以及所有的缓冲区写入内存后才开始执行序列化指令。
4)SFENCE,LFENCE,MFENCE 指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE 指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE 指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE 指令之前的读写操作。
注意:SFENCE,LFENCE,MFENCE 指令提供了比CPUID 指令更灵活有效的控制内存排序的方式。
5)MTRRs 在P6 系列处理器中引入,用来定义物理内存的特定区域的高速缓存特性。下面的两个例子是利用MTRRs 设置的内存类型如何来加强和削弱Pentium 4, Intel
Xeon, P6 系列处理器的访存排序:
(1)强不可缓冲(strong uncached,UC)内存类型实行内存访问的强排序模型:
这里,所有对UC 内存区域的读写都出现在总线上,并且不能够被乱序或预先执行。这种内存类型可以应用于映射成I/O 设备的内存区域来强制执行访存强排序。
(2)对于可以容忍弱排序访问的内存区域,可以选择回写(write back, WB)内存类型:
这里,读操作可以预先的被执行,写操作可以被缓冲和组合(combined)。对于这种类型的内存,锁定高速缓存是通过一个加锁的原子操作实现的,这个操作不会分割缓冲线,
因此会减少典型的同步指令(如,XCHG 在整个读-改-写操作周期要锁定数据总线)所带来的性能损失。对于WB 内存,如果访问的数据已经存在于缓存cache 中,XCHG 指令
会锁定高速缓存而不是数据总线。
(3)PAT 在Pentium III 中引入,用来增强用于存储内存页的缓存性能。PAT 机制通常被用来与MTRRs 一起来加强页级别的高速缓存性能。在Pentium 4, Intel Xeon,P6
系列处理器上运行的软件最好假定是 "处理器排序"模型或者是更弱的访存排序模型。
Pentium 4, Intel Xeon,P6 系列处理器没有实现强访存排序模型,除了对于UC 内存类型。尽管Pentium 4, Intel Xeon,P6 系列处理器支持处理器排序模型,Intel 并没
有保证将来的处理器会支持这种模型。为了使软件兼容将来的处理器,操作系统最好提供临界区(critical region)和资源控制构建以及基于I/O,加锁,序列化指令的API,用
于同步多处理器系统对共享内存区的访问。同时,软件不应该依赖处理器排序模型,因为也许系统硬件不支持这种访存模型。
(4)向多个处理器广播页表和页目录条目的改变:
在一个多处理器系统中,当一个处理器改变了一个页表或页目录的条目,这个改变必须要通知所有其它的处理器。这个过程通常称为"TLB shootdown"。广播页表或页目录
条目的改变可以通过基于内存的信号量或者处理器间中断(interprocessor interrupts, IPI)。
例如一个简单的,但是算法上是正确的TLB shootdown 序列可能是下面的样子:
a.开始屏障(begin barrier)——除了一个处理器外停止所有处理器;让他们执行HALT 指令或者空循环。
b.让那个没有停止的处理器改变PTE or PDE。
c.让所有处理器在他们各自TLB 中修改的PTE, PDE 失效。
d.结束屏障(end barrier)——恢复所有的处理器执行。
(5)串行化指令(serializing instructions):
IA-32 体系定义了几个串行化指令(SERIALIZING INSTRUCTIONS)。这些指令强制处理器完成先前指令对标志,寄存器以及内存的修改,并且在执行下一条指令之前将所
有缓冲区里的数据写入内存。
===>串行化指令应用一:开启保护模式时的应用
例如:当MOV 指令将一个操作数装入CR0 寄存器以开启保护模式时,处理器必须在进入保护模式之前执行一个串行化操作。这个串行化操作保证所有在实地址模式下开始
执行的指令在切换到保护模式之前都执行完毕。
————-
串行化指令的概念在Pentium处理器中被引入IA-32 体系。这种指令对于Intel486 或更早的处理器是没有意义的,因为它们并没有实现并行指令执行。
非常值得注意的是,在Pentium 4, Intel Xeon,P6 系列处理器上执行串行化指令会抑制指令的预执行(speculative execution),因为预执行的结果会被放弃掉。
————-
下面的指令是串行化指令:
1.—>特权串行化指令——MOV(目标操作数为控制寄存器),MOV(目标操作数为调试存器),WRMSR, INVD, INVLPG, WBINVD, LGDT, LLDT, LIDT, LTR。
————————-作者补充——————————
作者:如果上述指令不熟,可以参考《80X86 汇编语言程序设计教程》杨季文编,清华大学出版社。下面作些简单的介绍:以下作者对汇编指令的说明均参考引用了该书。
—->INVLPG 指令:
使TLB(转换后援缓冲器:用于存放最常使用的物理页的页码)项无效。该指令是特权指令,只有在实方式和保护方式的特权级0 下,才可执行该指令。
—————————————————————
2.—>非特权串行化指令——CPUID, IRET, RSM。
3.—>非特权访存排序指令——SFENCE, LFENCE, MFENCE。
当处理器执行串行化指令的时候,它保证在执行下一条指令之前,所有未完成的内存事务都被完成,包括写缓冲中的数据。任何指令不能越过串行化指令,串行化指令也不能越
过其他指令(读,写, 取指令, I/O)。
CPUID 指令可以在任何特权级下执行串行化操作而不影响程序执行流(program flow),除非EAX, EBX, ECX, EDX 寄存器被修改了。
SFENCE,LFENCE,MFENCE 指令为控制串行化读写内存提供了更多的粒度。
在使用串行化指令时,最好注意下面的额外信息:
处理器在执行串行化指令的时候并不将高速缓存中已经被修改的数据写回到内存中。软件可以通过WBINVD 串行化指令强制修改的数据写回到内存中。但是频繁的使用
WVINVD(作者注:当为WBINVD,原文此处有误)指令会严重的降低系统的性能。
—————-作者补充:对WBINVAD 的解释———————–
—–>INVD 指令:
INVD 指令使片上的高速缓存无效,即:清洗片上的超高速缓存。但该指令并不把片上的超高速缓存中的内容写回主存。该指令是特权指令,只有在实方式和保护方式的
特权级0 下,才可执行该指令。
—->WBINVD 指令:
WBINVD 指令使片上的超高速缓存无效即:清洗片上的超高速缓存。但该指令将把片上的超高速缓存中更改的内容写回主存。该指令是特权指令,只有在实方式和保护方
式的特权级0 下,才可执行该指令。
****************************************************************
===>串行化指令应用二:改变了控制寄存器CR0 的PG 标志的应用
当一条会影响分页设置(也就是改变了控制寄存器CR0 的PG 标志)的指令执行时,这条指令后面应该是一条跳转指令。跳转目标应该以新的PG 标志(开启或关闭分页)来进
行取指令操作,但跳转指令本身还是按先前的设置执行。Pentium 4, Intel Xeon,P6 系列处理器不需要在设置CR0 处理器之后放置跳转指令(因为任何对CR0 进行操作的
MOV 指令都是串行化的)。但是为了与其他IA-32 处理器向前和向后兼容,最好是放置一条跳转指令。
=========
作者说明:CR0 的第31 位为PG 标志,PG=1:启用分页管理机制,此时线性地址经过分页管理机制后转换为物理地址;PG=0:禁用分页管理机制,此时线性地址直接
作为物理地址使用。
****************************************************************
在允许分页的情况下,当一条指令会改变CR3 的内容时,下一条指令会根据新的CR3 内容所设置的转换表进行取指令操作。因此下一条以及之后的指令应该根据新的CR3
内容建立映射。
=========
作者说明:CR3 用于保存页目录表的起始物理地址,由于目录表是责对齐的,所以仅高20 位有效,低12 位无效。所以如果向CR3 中装入新值,其低12 位当为0;每当
用mov 指令重置CR3 的值时候,TLB 中的内容会无效。CR3 在实方式下也可以设置,以使分页机制初始化。在任务切换时候,CR3 要被改变。但要是新任务的CR3 的
值==旧任务的CR3 的值,则TLB 的内容仍有效,不被刷新。
******************************************************************************
以上通过这篇文章资料对cpu 的工作机制有了更深刻的了解,从而对我们的Linux Kernel 的学习有极大的帮助。由此对加锁,各类排序,串行化,sfence,mfence,lfence
指令的出现有了清楚的认识。再回头来读读源代码有更深刻的认识。
*****************************************************************************
——————————————smp_mb()—smp_rmb()—smp_wmb()————————-
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
#define set_wmb(var, value) do { var = value; wmb(); } while (0)
———————————————–linuxcompiler-gcc.h————————————–
——————————————————barrier()————————————————-
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
自己分析:
1.如果定义的了CONFIG_SMP,也就是系统为对称多处理器系统。smp_mb(),smp_rmb(),smp_wmb()就是mb(),rmb(),wmb()。
由此可见,多处理器上的内存屏障与单处理器原理一样。
2.barrier()函数并无什么难点,与前面代码一样。
3.如果没有定义CONFIG_SMP,则smp_mb(), smp_rmb(), smp_wmb(), smp_read_barrier_depends( 都是空宏。
**************************************************************************
在本文的代码中有不少下划线的关键字,特此作一研究:
——————————————————–双下划线的解释————————————–
—>摘自gcc 手册
Alternate Keywords ‘-ansi’ and the various ‘-std’ options disable certain keywords。 This causes trouble when you want to use GNU C extensions, or
a general-purpose header file that should be usable by all programs, including ISO C programs。 The keywords asm, typeof and inline are not available
in programs compiled with ‘-ansi’ or ‘-std’ (although inline can be used in a program compiled with ‘-std=c99’)。 The ISO C99 keyword restrict is only
available when ‘-std=gnu99’ (which will eventually be the default) or ‘-std=c99’ (or the equivalent ‘-std=iso9899:1999’) is used。The way to solve
these problems is to put ‘__’ at the beginning and end of each problematical keyword。 For example, use __asm__ instead of asm, and __inline__
instead of inline。
Other C compilers won’t accept these alternative keywords; if you want to compile with another compiler, you can define the alternate keywords as
macros to replace them with the customary keywords。 It looks like this:
#ifndef __GNUC__
#define __asm__ asm
#endif
‘-pedantic’(pedantic 选项解释见下面) and other options cause warnings for many GNU C extensions。 You can prevent such warnings within one
expression by writing __extension__ before the expression。__extension__ has no effect aside from this。
自己分析:
1。我们在程序中使用了很多的gnu 风格,也就是GNU C extensions 或其他的通用的头文件。但是如果程序用’-ansi’或各种’-std’选项编译时候,一些关键字,比如:
asm、typeof、inline 就不能再用了,在这个编译选项下,这此关键字被关闭。所以用有双下划线的关键字,如:__asm__、__typeof__、__inline__,这些编译器通
常支持这些带有双下划线的宏。这能替换这些会产生编译问题的关键字,使程序能正常通过编译。
2。如果是用其他的编译器,可能不认这些带有双下划线的宏,就用以下宏来转换:
#ifndef __GNUC__
#define __asm__ asm
#endif
这样的话,这些其他的编译器没有定义__GUNUC__,也不支持__asm__,__inline__,__typeof__等宏,所以必会,执行#define __asm__ asm等。这样,用
__asm__,__inline__,__typeof__所编写的程序代码,仍能宏展开为asm,inline,typeof,而这此关键字这些其他的编译器支持。所以程序能正常编译。
———————————————–pedantic 选项的解释———————————-
—>摘自gcc 手册Download from www。gnu。org
Issue all the warnings demanded by strict ISO C and ISO C++; reject all programs that use forbidden extensions, and some other programs that do
not follow ISO C and ISO C++。 For ISO C, follows the version of the ISO C standard specified by any ‘-std’ option used。 Valid ISO C and ISO C++
programs should compile properly with or without this option (though a rare few will require ‘-ansi’ or a ‘-std’ option specifying the required version
of ISO C)。 However, without this option, certain GNU extensions and traditional C and C++ features are supported as well。 With this
option, they are rejected。 ‘-pedantic’ does not cause warning messages for use of the alternate keywords whose names begin and end with ‘__’。
Pedantic warnings are also disabled in the expression that follows __extension__。 However, only system header files should use these escape routes;
application programs should avoid them。 See Section 5。38 [Alternate Keywords], page 271。
Some users try to use ‘-pedantic’ to check programs for strict ISO C conformance。They soon find that it does not do quite what they want: it finds
some non-ISO practices, but not all—only those for which ISO C requires a diagnostic, and some others for which diagnostics have been added。 A
feature to report any failure to conform to ISO C might be useful in some instances, but would require considerable additional work and would be quite
different from ‘-pedantic’。 We don’t have plans to support such a feature in the near future。
—————————————————————————————————————–
——————–
姓名:余旭 Linux Kernel 2.6.14 源代码剖析
 

网卡的原理及测试技术

网卡充当计算机和网络缆线之间的物理接口或连线将计算机中的数字信号转换成电或光信号,称为nic( network interface card )。数据在计算机总线中传输是并行方式即数据是肩并肩传输的,而在网络的物理缆线中说数据以串行的比特流方式传输的,网卡承担串行数据和并行数据间的转换。网卡在发送数据前要同接收网卡进行对话以确定最大可发送数据的大小、发送的数据量的大小、两次发送数据间的间隔、等待确认的时间、每个网卡在溢出前所能承受的最大数据量、数据传输的速度。
一、网卡的基本构造
网卡包括硬件和固件程序(只读存储器中的软件例程),该固件程序实现逻辑链路控制和媒体访问控制的功能网卡包括硬件和固件程序(只读存储器中的软件例程),该固件程序实现逻辑链路控制和媒体访问控制的功能,还记录唯一的硬件地址即mac地址,网卡上一般有缓存。网卡须分配中断irq及基本i/o端口地址,同时还须设置基本内存地址(base memory address)和收发器(transceiver)
网卡的控制芯片
是网卡中最重要元件,是网卡的控制中心,有如电脑的cpu,控制着整个网卡的工作,负责数据的的传送和连接时的信号侦测。早期的10/100m的双速网卡会采用两个控制芯片(单元)分别用来控制两个不同速率环境下的运算,而目前较先进的产品通常只有一个芯片控制两种速度。
晶体震荡器
负责产生网卡所有芯片的运算时钟,其原理就象主板上的晶体震荡器一样,通常网卡是使用20或25hz的晶体震荡器。
boot rom插槽
如无特殊要求网卡中的这个插槽处在空置状态。一般是和boot rom芯片搭配使用,其主要作用是引导电脑通过服务器引导进入win9x。
boot rom
就是启动芯片,让电脑可以在不具备硬盘、软驱和光驱的情况下,直接通过服务器开机,成为一个无硬盘无软驱的工作站。没有软驱就无法将资料输出,这样也可以达到资料保密的功能。同时,还可以节省下购买这些电脑部件的费用。在使用boot rom时要注意自己使用何种网络操作系统,通常有boot rom for nt,boot rom for unix,boot rom for netware等,boot rom启动芯片要自行购买。
eprom
从前的老式网卡都要靠设置跳线或是dip开关来设定irq、dma和i/o port等值,而现在的网卡则都使用软件设定,几乎看不见跳线的存在。各种网卡的状态和网卡的信息等数据都存在这颗小小的eeprom里,通过它来自动设置。
内接式转换器
只要有bnc接头的网卡都会有这个芯片,并紧邻在bnc接头旁,它的功能是在网卡和bnc接头之间进行数据转换,让网卡能通过它从bnc接头送出或接收资料。
rj-45和bnc接头
rj-45是采用双绞线作为传输媒介的一种网卡接口,在100mbps网中最常应用。bnc是采用细同轴电缆作为传输媒介
信号指示灯
在网卡后方会有二到三个不等的信号灯,其作用是显示目前网络的连线状态,通常具有tx和rx两个信息。tx代表正在送出资料,rx代表正在接收资料,若看到两个灯同时亮则代表目前是处于全双工的运作状态,也可由此来辨别全双工的网卡是否处于全双工的网络环境中(见上图两个接口的中间部分)。也有部分低速网卡只用一个灯来表示信号,通过不同的灯光变换来表示网络是否导通。
二、网卡的分类
以频宽区分网卡种类
目前的以太网卡分为10mbps、100mbps和1000 mbps三种频宽,目前常见的三种架构有10baset、100basetx与base2,前两者是以rj-45双绞线为传输媒介,频宽分别有 10mbps和100mbps。而双绞线又分为category 1至category 5五种规格,分别有不同的用途以及频宽,category通常简称cat,只要使用cat5规格的双绞线皆可用于10/100mbps频宽的网卡上。而 10base2架构则是使用细同轴电缆作为传输媒介,频宽只有10mbps。这里提到的频宽10或100mbps是指网卡上的最大传送频宽,而频宽并不等于网络上实际的传送速度,实际速度要考虑到传送的距离,线路的品质,和网络上是否拥挤等因素,这里所谈的bps指的是每秒传送的bit(1个byte=8 个bit)。而100mbps则称为高速以太网卡(fast ethernet),多为pci接口。因为其速度快,目前新建的局域网络绝已大多数已采用100mbps的传输频宽,已有渐渐取代10mbps网卡的趋势。当前市面上的pci网卡多具有10/100mbps自动切换的功能,会根据所在的网络连线环境来自动调节网络速度。1000 mbps以太网卡多用于交换机或交换机与服务器之间的高速链路或backbone。
以接口类型区分网卡种类
以接口类型来分,网卡目前使用较普遍的是isa接口、pci接口、usb接口和笔记本电脑专用的pcmcia接口。现在的isa接口的网卡均采用 16bit的总线宽度,其特性是采用programmed i/o的模式传送资料,传送数据时必须通过cpu在i/o上开出一个小窗口,作为网卡与pc之间的沟通管道,需要占用较高的cpu使用率,在传送大量数据时效率较差。pci接口的网卡则采用32bit的总线频宽,采用bus master的数据传送方式,传送数据是由网卡上的控制芯片来控制,不必通过i/o端口和cpu,可大幅降低cpu的占用率,目前产品多为10 /100mbps双速自动侦测切换网卡。
以全双工/半双工来区分网卡种类
网络有半双工(half duplex)与全双工(full duplex)之分,半双工网卡无法同一时间内完成接收与传送数据的动作,如10base2使用细同轴电缆的网络架构就是半双工网络,同一时间内只能进行传送或接收数据的工作,效率较低。要使用全双工的网络就必须要使用双绞线作为传输线才能达到,并且也要搭配使用全双工的集线器,要使用10base或 100basetx的网络架构,网卡当然也要是全双工的产品
以网络物理缆线接头区分网卡
目前网卡常用的网线接头有rj-45与bnc两种,有的网卡同时具有两种接头,可适用于两种网络线,但无法两个接头同时使用。另外还有光纤接口的网卡,通常带宽在1000 mbps。
其他功能wol
有些网卡会有wol的功能,wol网络开机的功能(wake on lan)。它可由另外一台电脑,使用软件制作特殊格式的信息包发送至一台装有具wol功能网卡的电脑,而该网卡接收到这些特殊格式的信息包后,就会命令电脑打开电源,目前已有越来越多的网卡支持网络开机的功能。
其它网卡
从网络传输的物理媒介上还有无线网卡,利用2.4ghz的无线电波来传输数据。目前ieee有两种规范802.11和802.11b,最高传输速率分别为2m和11m,接口有pci、usb和pcmcia几种。
三、网卡测试技术
基于操作系统的测试
网卡一个重要的性能是看其是否支持多种网络操作系统,比较流行的网络操作系统有windowsnt、unix(linux、freebsd、sco、 solaris、hp厎)、novell、dec等。同时网卡应能够支持多种的网络协议,如tcp/ip、ipx/spx、apple、netbeui 等。
基于主机的兼容性测试
硬件上的兼容性也是非常重要的一个方面,尤其在笔记本电脑上兼容性问题比较突出,根据本人的实际经验,甚至某些名牌的网卡在一些笔记本电脑上也存在较为严重的兼容性问题。在服务器或台式电脑方面这些问题不常出现。
网卡传输速率测试(数据吞吐量)
测试网卡的传输速率一般有硬件和软件两种方法,硬件是利用一些专用的仪器如网络分析仪、smartbits smartcards等其他一些设备,利用icmp echo请求和udp数据包来检测数据流量。通常测试的项目有以下几方面:
autonegotiation test
测试网卡速率、全双工/半双工和流控协商。协商决定着是否通过“暂停桢pause frame”来允许流量控制。
arp test
测试网卡是否能对arp请求做出正确回应及是否在规定时间内应答。这个时间由测试者进行设置。
error test
测试网卡处理错误frame的能力,通常在较低的传输速率下进行此项测试(0.5%传输速率),有以下几个方面的测试:
网卡接收正确的frame,作出处理。
网卡接收到存在crc校验错的frame,网卡将其丢弃。
网卡接收到传输顺序错误的frame,网卡将其丢弃。
网卡接收到含有少量错误bits的frame,网卡应全部接收并处理。
网卡接收到超小frame,网卡应将其丢弃。
网卡接收到超长frame,网卡应将其丢弃。
packets loss test
rfc规定测试网卡在各种传输带宽利用率下的处理frame的能力,从初始化数据传输到传输速率的不断变化一直到传输结束,检查frame的丢失情况。
throughput test
数据吞吐量的测试也是rfc规定的一项测试内容,测试的结果反映出传输的最大带宽的利用率,每秒处理的frame和每秒处理的bits数量。
back-to-back test
同样此项测试也为rfc-2544的规定,测试在一个设定的最大传输速率下网卡可处理的并发frame的数量。最终反映出在不丢失数据包的情况下可并发传输的最大frame数量。
利用软件测试通常是利用zd的netbench来测试,一般只利用其测试网卡的最大传输速率。测试时要组成一个网络结构,一台windowsnt server服务器,若干个windows9x或windowsnt station客户端,传输大容量的文件如100mbps,测试的结果将反映出网卡的最大传输速率。另一个测试项目是测试网卡对较小的数据包请求的回应能力,这里有必要讨论一下tcp/ip的ping命令的机制。ping是利用发送和接收icmp echo报文,来检测链路状态和协议设置。数据链路层封装的是frame,大小在64k~1518k之间,当发送frame时,网卡接受到frame时首先要读取桢头和桢尾的mac地址,当mac地址相匹配时再接封装读取ip地址。当网卡连续接收到frame时,要对每一个frame做出处理,当网卡或是系统无法处理这些数据包时,这些数据包将被丢弃。这种情况多发生在连续发送非常小的frame时。ping的机制是发送一个icmp报文,接收到一个 icmp echo后再发送下一个icmp报文。所以较小的连续的frame会对网卡和系统造成较大的压力。在netbench中,有一项测试就是测试网卡或系统对连续的小数据包的处理能力。
稳定性测试
一块好的网卡应该具有良好的稳定性,具体讲就是在不同的工作环境下和不同的工况下应具有稳定的表现。通常测试主要是高温和传输大文件测试。
高温测试一般是在30~35摄氏度下连续运行网卡的测试程序达一定的时间比如2小时以上,检测网卡高温下的稳定性。pcmcia接口的网卡一般有两种32 位的和16位的,前者又称为cardbus网卡,数据带宽由16位增加到32位,使得pcmcia的网卡发热量成为一个显著的问题。
另一个测试是传输大的文件,某些品质较差的网卡在传输大容量的文件比如2gbps以上的文件时容易出错。
综上所述,在测试一块网卡时要进行全面的软、硬件及兼容性测试,可根据具体的应用和不同的要求,有机的选择测试项目,正确反映网卡的性能指标。

网卡的组成工作原理

1.认识网卡,我们上网必备组件之一。
  
    网卡工作在osi的最后两层,物理层和数据链路层,物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。物理层的芯片称之为PHY。数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能。以太网卡中数据链路层的芯片称之为MAC控制器。很多网卡的这两个部分是做到一起的。他们之间的关系是pci总线接mac总线,mac接phy,phy接网线(当然也不是直接接上的,还有一个变压装置)。

    下面继续让我们来关心一下PHY和MAC之间是如何传送数据和相互沟通的。通过IEEE定义的标准的MII/GigaMII(Media Independed Interfade,介质独立界面)界面连接MAC和PHY。这个界面是IEEE定义的。MII界面传递了网络的所有数据和数据的控制。
而MAC 对PHY的工作状态的确定和对PHY的控制则是使用SMI(Serial Management Interface)界面通过读写PHY的寄存器来完成的。PHY里面的部分寄存器也是IEEE定义的,这样PHY把自己的目前的状态反映到寄存器里面,MAC通过SMI总线不断的读取PHY的状态寄存器以得知目前PHY的状态,例如连接速度,双工的能力等。当然也可以通过SMI设置PHY的寄存器达到控制的目的,例如流控的打开关闭,自协商模式还是强制模式等。

    我们看到了,不论是物理连接的MII界面和SMI总线还是PHY的状态寄存器和控制寄存器都是有IEEE的规范的,因此不同公司的MAC和PHY一样可以协调工作。当然为了配合不同公司的PHY的自己特有的一些功能,驱动需要做相应的修改。

    一片网卡主要功能的实现就基本上是上面这些器件了。其他的,还有一颗EEPROM芯片,通常是一颗93C46。里面记录了网卡芯片的供应商ID、子系统供应商ID、网卡的MAC地址、网卡的一些配置,如SMI总线上PHY的地址,BOOTROM的容量,是否启用BOOTROM引导系统等东西。

    很多网卡上还有BOOTROM这个东西。它是用于无盘工作站引导操作系统的。既然无盘,一些引导用必需用到的程序和协议栈就放到里面了,例如RPL、 PXE等。实际上它就是一个标准的PCI ROM。所以才会有一些硬盘写保护卡可以通过烧写网卡的BootRom来实现。其实PCI设备的ROM是可以放到主板BIOS里面的。启动电脑的时候一样可以检测到这个ROM并且正确识别它是什么设备的。AGP在配置上和PCI很多地方一样,所以很多显卡的BIOS也可以放到主板BIOS里面。这就是为什么板载的网卡我们从来没有看到过BOOTROM的原因。

2.工作过程

    PHY在发送数据的时候,收到MAC过来的数据(对PHY来说,没有帧的概念,对它来说,都是数据而不管什么地址,数据还是CRC),每4bit就增加 1bit的检错码,然后把并行数据转化为串行流数据,再按照物理层的编码规则(10Based-T的NRZ编码或100based-T的曼彻斯特编码)把数据编码,再变为模拟信号把数据送出去。收数据时的流程反之。现在来了解PHY的输出后面部分。一颗CMOS制程的芯片工作的时候产生的信号电平总是大于 0V的(这取决于芯片的制程和设计需求),但是这样的信号送到100米甚至更长的地方会有很大的直流分量的损失。而且如果外部网现直接和芯片相连的话,电磁感应(打雷)和静电,很容易造成芯片的损坏。

    再就是设备接地方法不同,电网环境不同会导致双方的0V电平不一致,这样信号从A传到B,由于A设备的0V电平和B点的0V电平不一样,这样会导致很大的电流从电势高的设备流向电势低的设备。我们如何解决这个问题呢?
这时就出现了Transformer(隔离变压器)这个器件。它把PHY送出来的差分信号用差模耦合的线圈耦合滤波以增强信号,并且通过电磁场的转换耦合到连接网线的另外一端。这样不但使网线和PHY之间没有物理上的连接而换传递了信号,隔断了信号中的直流分量,还可以在不同0V电平的设备中传送数据。

    隔离变压器本身就是设计为耐2KV~3KV的电压的。也起到了防雷感应(我个人认为这里用防雷击不合适)保护的作用。有些朋友的网络设备在雷雨天气时容易被烧坏,大都是PCB设计不合理造成的,而且大都烧毁了设备的接口,很少有芯片被烧毁的,就是隔离变压器起到了保护作用。

发送数据时,网卡首先侦听介质上是否有载波(载波由电压指示),如果有,则认为其他站点正在传送信息,继续侦听介质。一旦通信介质在一定时间段内(称为帧间缝隙 IFG=9.6微秒)是安静的,即没有被其他站点占用,则开始进行帧数据发送,同时继续侦听通信介质,以检测冲突。在发送数据期间,如果检测到冲突,则立即停止该次发送,并向介质发送一个“阻塞”信号,告知其他站点已经发生冲突,从而丢弃那些可能一直在接收的受到损坏的帧数据,并等待一段随机时间(CSMA/CD确定等待时间的算法是二进制指数退避算法)。在等待一段随机时间后,再进行新的发送。如果重传多次后(大于16次)仍发生冲突,就放弃发送。
    接收时,网卡浏览介质上传输的每个帧,如果其长度小于64字节,则认为是冲突碎片。如果接收到的帧不是冲突碎片且目的地址是本地地址,则对帧进行完整性校验,如果帧长度大于1518字节(称为超长帧,可能由错误的LAN驱动程序或干扰造成)或未能通过CRC校验,则认为该帧发生了畸变。通过校验的帧被认为是有效的,网卡将它接收下来进行本地处理

嵌入式Linux实时化技术

嵌入式Linux实时化技术

Embedded Linux Real-Time Technology
作者:黄武陵 中科院自动化所,何小庆 北京麦克泰软件技术有限公司,艾云峰 中科院研究生院   时间:2009-02-26  来源:电子产品世界