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  来源:电子产品世界

内核组件初始化体系结构

Understanding Linux Network Internals 第七章 翻译稿

【翻译】内核组件初始化体系结构

为了全面了解内核组件,你不仅需要了解特定的程序做了什么,也要知道这些程序什么时候被谁调用。内核子系统的初始化是一项基本任务,这些任务由内核根据它自己的模式来处理。这个体系结构值得我们学习并有助于理解网络堆栈的核心组件,包括网络设备驱动程序是如何初始化的。

本章的目的在于展示内核怎样处理用于初始化内核组件的函数,既包含静态嵌入内核的组件,也包括作为内核模块加载的组件,特别是网络设备。我们将会弄明白如下几点:
l初始化函数是如何被特殊的宏来命名与标识
l这些基于内核配置的宏如何被定义,以优化内存使用,确保各种初始化以正确的顺序被执行
l函数什么时候、怎样执行

我们并不讨论初始化体系结构的所有细节,不过你可以快速浏览一遍并舒适的阅读源代码了

7.1、启动时内核选项

Linux 允许用户传递内核配置选项给启动程序,启动程序再把这些选项传递给内核。有经验的用户可以利用这个机制在系统启动时调整内核。在启动阶段,内核有两次调用 parse_args函数(译者注:本书是基于2.6内核,在2.4内核中采用parse_options函数)处理启动时的配置输入。接下来我们在“两 次调用解析中解释”parse_args为何被调用两次。

你可以在Linux BootPrompt HOWTO中找到一些使用启动选项的文档或例子

parse_args是解析具有形如“变量名=值”的输入字符串的函数,它查找关键字并调用相应的处理函数。在加载模块、解析模块命令行参数时,parse_args也会被调用。

我们不必知道parse_args如何实现解析功能的细节,但是我们对内核组件如何为关键字注册处理函数以及处理函数如何被调用感兴趣。为了有一个清晰的认识,我们需要了解:
l在启动字符串中含有关键字时,内核组件如何注册关键字及相对应的执行函数
l内核如何解析关键字和处理函数之间的关联关系,我们将提供一个内核如何解析输入字符串的高级用法。
l网络设备子系统如何使用这个特性

所有的解析代码都在kernel/params.c中,我们在接下来的部分逐步讲述。

7.1.1、注册关键字
内核组件用__setup宏来注册关键字及相关联的处理函数,__setup宏在include/linux/init.h中定义,其原型如下:
__setup(string, function_handler)
其 中:string是关键字,function_handler是关联处理函数。__setup只是告诉内核在启动时输入串中含有string时,内核要去 执行function_handler。String必须以“=”符结束以使parse_args更方便解析。紧随“=”后的任何文本都会作为输入传给 function_handler。
下面的例子来自于net/core/dev.c,其中netdev_boot_setup作为处理程序被注册给“netdev=”关键字:
__setup("netdev=", netdev_boot_setup);
不 同的关键字可以注册相同的处理函数,例如在net/ethernet/eth.c中为“ether =”关键字注册了同样的处理函数 netdev_boot_setup。当代码作为模块被编译时,__setup宏被忽视,你可以在include/linux/init.h中看到 __setup宏是怎样变化的,不管后续包含它的文件是否是模块,include/linux/init.h都是独立的。
start_kernel两次调用parse_args解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用值能够兼顾到其中一类:

缺省选项:
绝大多数选项归于此类,这些选项由__setup宏定义并在第二次调用parse_args时处理。
先期(处理)选项:
在 内核启动阶段,有些选项要在其它选项之前被处理,内核提供了early_param宏以代替__setup宏申明此类选项。这些选项由 parse_early_params函数解析。early_param宏和__setup宏仅有的不同就是前者设置了一个特殊标志让内核能够区分两种不 同的状况。这个标志是我们将在“.init.setup内存区”小节中看到的obs_kernel_param结构的一部分。
启动时选项在内核 2.6中的处理方式已经改变,但并非所有的内核代码都因此而更新。在最近一次改变之前,还仅用__setup宏。因此,遗留下来将被更新的代码现在使用 __obsolete_setup宏。但用户用__obsolete_setup宏定义的选项给内核时,内核打印一条警告消息说明它已是废弃状态,并提供 一个文件指针和随后被公告的源代码行信息。
图7-1概述了几个宏之间的关系:它们都包裹了普通的__setup_param函数。

图7-1 setup_param宏及其包裹物
  

注意:传给__setup宏的程序被放到.init.setup内存节,这样,在“启动时初始化代码”一节中可以很清晰的看出这样做的效果。

7.1.2、两次解析
因 为早期的内核版本(译者注:所谓的早期是相对于2.6内核而言的)中,启动时选项用于做不同的处理,并且这些选项没有全部移植到新的内核版本,所以新内核 要处理这两者情况。当新的体系结构不能够识别关键字,它会采用废弃的体系结构来处理。当废弃的体系结构也处理失败时,系统将把关键字和值一起由 run_init_process函数传递给init进程处理,而init进程是在init内核线程处理后期被调用。然后关键字和值要麽被加入到arg参 数列表,要麽被加到envp环境变量列表。
前面解释了,为了支持先期选项以特定的顺序提前处理,启动串解析和处理调用被做了两次,如图7-2所示(此图是第五章介绍的start_kernel的快照):
1、第一次处理看上去似乎是有特殊标记标识的高优先权的选项先处理(early标志)
2、第二次处理其它的选项,绝大多数选项归于此类,废弃的所有选项都在这次被处理。
第 二次首先检测在新体系结构下是否有匹配的选项处理,这些选项存储在kernel_param结构中,由第五章介绍的module_param宏填充。 module_param宏还确保所有的这些结构数据被放到特殊的内存节(__param),并由 __start___param 和 __stop___param指针分隔。
当识别了这些选项时,相关参数被初始化给选项的值,即启动选项串的function_handler;而当没有选项匹配时,unknown_bootoption函数试着看是否废弃的处理模型能够处理这些选项,如下图示:

图7-2 两次调用选项解析
 

废弃的和新模型选项放在两个不同的内存区:

__setup_start … __setup_end:
我们在后面的章节中会看到,这个区域在启动阶段结束时被释放:一旦内核启动,这些选项就不在需要,用户也不能够在运行时查看或修改它们。
__start___param … __stop___param:
这个区域不会被释放,它的内容被导入/sys文件系统,以暴露给用户。
第五章有更多关于模块参数的细节。

注意:所有作废模式选项,不管它是否有优先处理的特殊标志,都被放到__setup_start和__setup_end内存区。

7.1.3、.init.setup内存区

我们前面章节介绍的传给__setup宏的两个输入参数被放入obs_kernel_param类型的数据结构中,它在include/linux/init.h中定义:

struct obs_kernel_param {
    const char *str;
    int (*setup_func)(char*);
    int early;
};
其中,str是关键字,setup_func是处理函数,early是“两次解析”小节中介绍的两次调用中的特殊标志。
__setup_param宏把所有的obs_kernel_params实例放到专门的内存区,这样做主要有两个原因:
l当查找基于str的关键字时,通过所有的实例,系统更容易处理具体的实例。我们将明白在查找关键字时内核如何使用分别代表先前提及的内存区的开始和结束的__setup_start 和 __setup_end两个指针。
l当不再需要时,内核能够快速释放所有数据结构。我们可以在后面要讲的“内存最优化”小节看到这点。

7.1.4、用启动选项配置网络设备

按照前面章节所述,我们接下来看看网络代码怎样使用启动选项的。

我 们在“注册关键字”一节中注意到ether= 和 netdev=关键字都用同一函数netdev_boot_setup注册。当调用 netdev_boot_setup函数处理输入参数(紧随匹配的关键字后的字符串)时,函数将把处理结果存储在include/linux /netdevice.h中定义的netdev_boot_setup结构中,处理函数和结构碰巧是同名的,因此你要注意不要混淆两者:
struct netdev_boot_setup {
    char name[IFNAMSIZ];
    struct ifmap map;
};
其中,name是设备名,ifmap在in include/linux/if.h中定义,是存储输入配置的数据结构:
struct ifmap
{
    unsigned long mem_start;
    unsigned long mem_end;
    unsigned short base_addr;
    unsigned char irq;
    unsigned char dma;
    unsigned char port;
    /* 3 bytes spare */
};

同一关键字可以在启动时字符串选项中多次出现(对不同设备),如下例所示:
LILO: linux ether=5,0×260,eth0 ether=15,0×300,eth1
但是,这种机制下,能够在启动时配置的设备的最大数是NETDEV_BOOT_SETUP_MAX常量,它也是用于存储配置的静态数组dev_boot_setup的大小:
static struct netdev_boot_setup dev_boot_setup[NETDEV_BOOT_SETUP_MAX];

netdev_boot_setup相当简单:它从字符串中提取输入参数中,填充到ifmap结构中,并通过netdev_boot_setup_add函数将ifmap信息加入到dev_boot_setup数组中。
启动阶段结束时,网络代码会调用netdev_boot_setup_check函数检查给定的接口是否与启动时配置有关联,在dev_boot_setup数组中查找时基于设备名dev->name:

int netdev_boot_setup_check(struct net_device *dev)
{
    struct netdev_boot_setup *s = dev_boot_setup;
    int i;

    for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
        if (s[i].name[0] != ‘’ && s[i].name[0] != ‘ ‘ &&
            !strncmp(dev->name, s[i].name, strlen(s[i].name))) {
            dev->irq        = s[i].map.irq;
            dev->base_addr  = s[i].map.base_addr;
            dev->mem_start  = s[i].map.mem_start;
            dev->mem_end    = s[i].map.mem_end;
            return 1;
        }
    }
    return 0;
}

有些设备具有特殊容量、特征与限制,在需要额外的参数时,可以定义它自己的关键字和处理函数,这些关键字和函数紧接着ether= 和 netdev=提供的基本数据之后(PLIP设备驱动程序就是这样做的)

7.2、模块初始化代码

由于下面的章节的例子经常提及模块,所以有必要弄清楚一对初始化概念:
内 核代码要麽静态连接到主映象文件,要麽在需要时作为模块动态加载。并不是所有的内核组件都适合编译成模块,设备驱动程序和基本功能扩展是内核组件被编译为 模块的一个好的例子。你可以参考Linux Device Drivers一书了解模块利弊以及内核需要时动态加载模块而不再需要时卸载模块的原理。

每个模块都必须提供两个函数:init_module 和 cleanup_module,前者在模块加载时初始化模块,后者在内核卸载模块时被调用以释放被模块使用时分配的资源(包括内存)。

内核提供了两个宏:module_init 和 module_exit,它允许开发人员随意命名初始化和卸载函数,下面是3COM公司的3c59x网卡驱动(drivers/net/3c59x.c)中的一个例子:
module_init(vortex_init);
module_exit(vortex_cleanup);
在 “内存最优化”一节,我们将看到这两个宏如何定义以及其定义如何被内核配置项所改变。绝大多数内核使用这两个宏,但极少数模块仍然使用旧的缺省名 init_module 和 cleanup_module。在本章后续内容中,我们用module_init 和 module_exit来指代初始化 和卸载清除函数。

首先我们来看看用旧的内核模型是如何编写模块初始化代码的,然后看看基于新的宏集的内核如何工作的。

7.2.1、旧模式:条件编译代码

不管内核组件是编译成模块还是静态编译到内核中,它都需要初始化。因此,内核组件初始化代码必须依靠条件指示符告诉编译器区分这两种情况。在旧模式下,这将强迫开发者在所有这些地方使用类似#ifdef的条件编译指示符。

下面是2.2.14内核3c59x网卡驱动(drivers/net/3c59x.c)中的一段:注意#ifdef MODULE 和 #if defined (MODULE)使用了多次。


#if defined(MODULE) && LINUX_VERSION_CODE > 0x20115
MODULE_AUTHOR("Donald Becker <becker@cesdis.gsfc.nasa.gov>");
MODULE_DESCRIPTION("3Com 3c590/3c900 series Vortex/Boomerang driver");
MODULE_PARM(debug, "i");

#endif

#ifdef MODULE

int init_module(void)
{
    …
}
#else
int tc59x_probe(struct device *dev)
{
    …
}
#endif  /* not MODULE */

static int vortex_scan(struct device *dev, struct pci_id_info pci_tbl[])
{
    …
#if defined(CONFIG_PCI) || (defined(MODULE) && !defined(NO_PCI))
    …
#ifdef MODULE
    if (compaq_ioaddr) {
        vortex_probe1(0, 0, dev, compaq_ioaddr, compaq_irq,
                compaq_device_id, cards_found++);
        dev = 0;
    }
#endif

    return cards_found ? 0 : -ENODEV;
}

#ifdef MODULE
void cleanup_module(void)
{
    … … …
}
#endif

上面代码表明旧模式如何让程序员根据代码是编译成模块还是静态连接到内核映象来指定做不同的事情:
初始化代码被区别执行
代码显示cleanup_module函数仅当驱动程序被编译成模块时被定义(因此被使用)
代码块被包含或排斥在模块之外
例如:仅当驱动程序被编译成模块时vortex_scan 调用 vortex_probe1

这种模型使代码很难扩展与调试,而且在每个模块中都重复相同的逻辑。

7.2.2、新模式:基于宏的标记

现在我们来对比一下上节代码与2.6内核中相匹配的同样的文件

static char version[] _ _devinitdata = DRV_NAME " … ";

static struct vortex_chip_info {
    …
} vortex_info_tbl[] _ _devinitdata = {
    {"3c590 Vortex 10Mbps",
    … … …
}

static int _ _init vortex_init (void)
{
    …
}
static void _ _exit vortex_cleanup (void)
{
    …
}

module_init(vortex_init);
module_exit(vortex_cleanup);

你可以看到:#ifdef指示符不再需要。
为 了移除这些混乱的条件编译代码,以使代码有良好的可读性,内核开发者引入了一组模块开发者现在能够用于写更清晰初始化代码的宏(绝大多数驱动程序都是这些 宏的使用者),上述代码展示了其中的几个宏:__init, __exit, 和 __devinitdata的用法。

后面的章节将讲述这些宏如何被使用以及它们是如何工作的。

对每个模块来说,这些宏允许内核在后台决定那些代码被包含在内核映象中,那些代码因为不需要被排斥,那些代码仅仅在初始化时被执行,等等。这样就去除了每个程序员在每个模块都要复制相同的逻辑的麻烦。[*]
[*]注意,用宏并不是消除了使用条件编译指示符,内核仍然用条件指示符设置用户编译时可配置的开关选项

很明显,就像上面章节例子展示的,这些宏允许程序员替换条件编译指示符,他们必须提供如下两个服务:

l定义新的内核组件加载时需要执行的函数,要麽由于它是静态包含进内核,要麽由于它作为模块动态加载。
l在初始化函数间定义某种顺序,以便内核组件之间的相互依赖互不相关。

7.3、优化基于宏的标记

    Linux 内核使用各种各样的宏来标识函数和数据结构的特殊属性,例如:标识初始化函数。绝大多数宏都在include/linux/init.h文件中定义,这些 宏很多是用于告诉连接器把这些具有特殊属性的代码或数据结构放到特殊的、专用的内存区(或内存节section)。这样做,内核能够以一种简单的方式很容 易访问一类具有特殊属性的对象(程序或数据结构)。我们在“内存最优化”一节会看到这样的例子。
图7-3展示了一些内核内存节:

图7-3 初始化代码使用的一些内存节
 

图片展示了初始化代码所使用的部分内存区(section)示意图。左边是分隔每个区或节的开始与结束部分的指针名,右边部分是用于将数据或代码放到相关内存区的宏的名字,图片展示仅限于部分而非全部的内存区以及部分而非全部宏

表 7-1和表7-2列出了用于分别标记程序或数据的一些宏并给出了简单的描述。限于篇幅,我们不会全部说明他们,但在”xxx_initcall宏"一节中 会花一定的篇幅讲述xxx_initcall宏,在"__init and __exit 宏"一节中会花一定的篇幅讲述__init宏和 __exit 宏。
本节的目的不是描述如何创建内核映象,如何处理模块等等,而是给你一些关于模块为什么存在的一些原因,以及设备驱动程序通常如何使用它们的。

表7-1 修饰函数的宏
宏使用宏的函数说明
__init启动时初始化函数: 用于启动阶段后期不再需要的函数,这些信息在某种情况下用于移除函数
__exit和__init匹配. 相关内核组件卸载时调用,常用于module_exit所修饰的函数,这些信息在某种情况下用于移除函数
core_initcall  postcore_initcall  arch_initcall    subsys_initcall fs_initcall device_initcall late_initcall 宏的集合,用于标记启动时需要执行的初始化函数
__initcall废弃的宏,定义为device_initcall的别名
__exitcalla标识退出函数,相关内核组件卸载时调用,迄今为止,它仅用于标记module_exit程序
a __exitcall和__initcall在__exit_call和__init_call之前定义

表7-2 初始化数据结构的宏
宏使用宏的数据(结构)说明
__initdata仅在启动时用于已初始化的数据结构
__exitdata仅被由__exitcall修饰的函数使用的数据结构,也有一层意思是:如果被__exitcall修饰的函数即时不被使用,由__exitdata修饰的数据也是正确的。因此,各种优化也可以被用于__exitdata和__exitcall

在了解上面两张表中一些宏的细节之前,我们有必要强调几点:
l绝大多数宏都是相配对的:一个修饰初始化加载,则与其相配对的修饰卸载过程,例如:__exit和_ _init匹配; __exitcalls和__initcall匹配
l宏要兼顾两方面:一是当函数被执行时(如:__initcall, __exitcall);另一面就是函数或数据放置的内存区。
l同样的函数可以被多个宏标记。例如:下面的代码表明:pci_proc_init可以在启动时运行(__initcall),一旦运行后就可以被释放(__init)

static int __init pci_proc_init(void)
{

}

__initcall(pci_proc_init)

7.3.1、设备初始化函数宏

表 7-3列出了用于标记函数的一些普通的宏,它们被设备驱动程序用于初始化设备,并且在内核不支持热拔插时可以使内存最优化。在第六章“网卡驱动程序注册实例”一节中你可以看到这样使用的例子,在后面“其它优化”一节,你可以看到表7-3的宏使内核优化变得容易。

表 7-3 设备初始化函数宏
名称描述
__devinit用于标记初始化设备的函数,例如,对于PCI驱动程序,用于初始化的函数pci_driver->probe就是用此宏标识的。被其它由_devinit标记的函数调用的函数通常也由_devinit标记。
__devexit用于标记设备卸载时被调用的函数。
__devexit_p用于初始化由__devexit 标记的函数的指针。如果内核既支持模块也支持热拔插,则__devexit_p(fn)返回fn,否则返回NULL。可以参考“其它优化”一节
__devinitdata用于标记函数使用的已初始化的数据,而这些函数兼顾设备初始化(如被_devinit标记),因此共享其属性。
__devexitdata与__devinitdata类似但与__devexit关联匹配.

7.4、启动时初始化代码

绝大多数初始化代码有两个有趣的特点:
l启动时,当所有内核组件初始化后,它们必须被执行
l一旦执行之后就不需要它了。
下一小节“xxx_initcall宏”描述了启动时运行初始化函数的原理,并考虑到模块之间的属性与优先权。后面一节”内存最优化”展示了不再需要的函数和数据是如何在连接或运行时通过巧妙的标记而释放的。

7.4.1、xxx_initcall宏

内核启动阶段前期,要考虑两个主要的初始化块:
l各种关键的、必不可少的子系统的初始化需要以特殊的顺序执行,例如:内核在初始化PCI层之前不能够初始化PCI设备。后面“初始化程序相互依赖的例子”小节中有各例子说明。
l其它一些不必按照严格顺序的内核组件的初始化:相同优先级的函数可以以任意顺序执行。

第 一个初始化块可由来自第五章图5-1的do_initcalls函数代码验证,第二个初始化块可由同一章中调用do_initcalls的函数 do_basic_setup的结尾处来验证,第二部分的初始化函数是基于其角色以及优先权来分类,内核从放在高优先级节(core_initcall) 的函数开始逐个执行这些初始化函数,这些需要被调用的函数的地址放在图7-3中由xxx_initcall宏标记的.initcallN.init内存节 中。这个区域用于存储由xxx_initcall标记的函数地址,并由开始地址(__initcall_start)和结束地址 (__initcall_end)分隔。在下面摘录的do_initcalls函数代码中,你会看到,很容易从这个区域轻松取得函数地址并执行其指向的函 数:

static void _ _init do_initcalls(void)
{
        initcall_t *call;
        int count = preempt_count( );

        for (call = _ _initcall_start; call < _ _initcall_end; call++) {
            … … …
            (*call)( );
            … … …
        }
        flush_scheduled_work( );
}

由 do_initcalls调用的函数不应该改变其优先权状态和禁止IRQs。因此,每个函数执行后,do_initcalls会检查函数是否做了任何变 化,如果有必要,它会校正优先权和IRQ状态。对于xxx_initcall函数,确定其后发生的工作也是可能的,这意味着由这些函数处理的任务有可能在 未知时间异步中止,flush_scheduled_work函数调用用于确保do_initcalls在返回前等待这些异步任务结束。

注意:do_initcalls自己用__init标记:因为它仅在启动阶段被do_basic_setup调用一次,内核之后一旦调用就会丢弃它。
__exitcall与__initcall相对。它们并不常用,相当多的是由其它宏作为其别名,如:module_exit,它在“模块初始化代码”一节中介绍

7.4.1.1、__initcall和__exitcall程序例子:模块

我们曾说过,在“模块初始化代码”一节中,module_init和module_exit宏分别用于标记模块初始化(若编译进内核则在启动时,若是单独加载则是在运行时)或卸载时要被执行的函数。

这使得对于__initcall和__exitcall宏来说,模块是非常完美的选择。正如我们说得,下面的代码来自于include/linux/init.h文件,其中关于module_init 和 module_exit宏的定义就来得很自然了(不令人惊奇):
#ifndef MODULE
… … …
#define module_init(x)    __initcall(x);
#define module_exit(x)    __exitcall(x);

#else
… … …
#endif

对于静态连接进内核的代码来说,module_init是__initcall的别名,它的输入函数被归到启动时初始化函数一类。
module_exit与此一致:当代码被编译进内核,module_exit变成了卸载函数,同时,卸载函数在系统关闭时不会被调用,但是代码允许这样放置.[*]
[*]用户模式下的linux是实际使用卸载函数的仅有的体系结构。它并不用__exitcall宏,而是定义它自己的宏,__uml_exitcall,用户模式linux项目的主页是::URL::http://user-mode-linux.sourceforge.net

7.4.1.2、初始化程序相互依赖的例子

第 五章介绍了net_dev_init,设备驱动程序在内核中用module_init函数注册,正如第六章描述的,由网络代码注册设备。内置到驱动程序的 net_dev_init和各种module_init标记的函数启动时由do_initcalls调用。因此,内核必须确保在net_dev_init 执行之前没有设备注册发生,这显然是强制的,因为设备驱动程序初始化函数被device_initcall宏(或别名__initcall)标记,而 net_dev_init被subsys_initcall宏标记。在图7-3中,你可以看到subsys_initcall函数比 device_initcall更早执行(其内存节以优先的顺序分类)。

7.4.1.3、遗留下来的代码
在引入 xxx_initcall宏集前,仅有一个标记初始化函数的宏:__initcall。这个单个的宏的使用带来严重的局限性:宏修饰的函数不能强制限定执 行顺序,在很多情况下,这个局限性由于模块内部独立或其它一些考虑变得不可接收。所以使用__initcall对所有的初始化函数没有扩展性。
__initcall主要被设备驱动程序使用。为了使一些尚未升级到新模式的代码向后兼容,它仍然存在而且被简单的定义为device_initcall的别名。
还有一个局限性经常在当前模型中出现,就是没有参数提供给初始化函数,但是这似乎不是很严重的局限。

7.5、内存最优化
不像用户空间代码和数据,内核的代码和数据永远保留在主内存中。因此在各种可能的方面减少内存的浪费变的很重要了。初始化代码对于内存优化是个很好的选择。绝大多数初始化函数要麽只执行一次,要麽根本就不执行,这视内核配置而定。例如:
lmodule_init标记的函数仅当关联模块加载时被执行一次,若模块是静态包含到内核的,则内核启动过程中,在模块运行后,内核能够完全释放module_init程序。
l当模块是静态包含到内核的时,module_exit修饰的函数决不会执行。因此,在这种情况下,没有必要在内核映象中包含此模块函数(也就是说,这个函数在连接时就可以丢弃)。
第一种情况是运行时优化,第二种是连接时优化。
启动时使用随后不需要的代码和数据放在图7-3所示的某个内存节,一旦内核完成了初始化过程,它会丢弃整个内存区,这靠调用第五章图5-1所示的free_init_mem 函数完成。不同的宏用于把代码放到图7-3所示的不同的内存区。
如果你看了前面的“新模式:基于宏的标记”一节,你会明白这两个module_init 和 module_exit函数通常是被__init 和__exit分别标记:利用本章开始所提及的两个属性正是这样做的。

7.5.1、__init和__exit宏

在内核前期阶段执行的初始化函数由__init标记
象前面小节提及的,绝大多数module_init输入函数用这个宏标记,例如:第五章图5-1(在调用free_initmem之前)的绝大多数函数用__init标记,正如其定义的那样,__init宏把输入函数放进.text.init内存节:
#define __init    __attribute__ ((__section__ (".text.init")))

这个节是运行时由free_initmem函数释放的内存区之一。
__exit 与__init相对,用于卸载的函数放在.text.exit节。对于直接编译进内核的模块而言,这个节在连接时就被丢弃。但是,有少数体系结构在运行时 处理交叉引用时会丢弃它。注意,对于单独加载的模块,若内核不支持模块卸载,同样的内存节在加载时就可以被移除(有个内核选项阻止用户卸载模块)。

7.5.2、xxx_initcall和__exitcall节
内核存放地址给由xxx_initcall 和 __exitcall宏标记的函数的内存节可以被丢弃:
l图7-3所示的xxx_initcall节在运行时由free_initmem丢弃
l.text.exit节用于__exitcall标记的函数,在连接时被丢弃,因为内核在系统关机时不会马上调用__exitcall函数(也就是说,它不会采用类似do_initcalls的机制).

7.5.3、其它优化
表7-3包含了其它优化的例子
__devinit 
 当内核在编译时不支持热拔插,则由__devinit修饰的函数在启动阶段结束时不再需要了(所有设备已被初始化)。因此,当不支持热拔插时__devinit变成了__init的别名。
__devexit  
当PCI驱动程序被编译进内核且不支持热拔插时,pci_driver->remove所指的函数被初始化,且由__devexit标记的函数因为不需要而被丢弃。当模块被加载到不支持模块卸载的内核中时,函数也被丢弃。
__devinitdata  
当 不支持热拔插时,数据也只在启动时需要。通常,在设备初始化时,设备驱动程序也用这个宏标记pci_driver->probe函数搜索到的字符 串。例如:PCI设备驱动程序用__devinitdata标记pci_device_id表:一旦系统启动结束且不支持热拔插,内核将不在需要这个表。
本节仅给出了一些丢弃代码的例子,你也可以阅读源代码以了解更多。

7.5.4、动态宏定义
前 面一节介绍了几个宏,如__init和几个版本的xxx_initcall,我们也看到了module_init宏修饰的函数被__initcall宏标 记。由于绝大多数内核组件要麽作为模块编译,要麽静态连接到内核,所以很多前面章节介绍的方法可供选择,以改变这些宏定义并用于内存优化。

特别说明,我们在include/linux/init.h中看到的表7-1宏的定义,它根据下面的符号是否在包含include/linux/init.h的文件的作用域范围而变化。

CONFIG_MODULE
当内核支持可加载模块时定义(可加载模块配置选项)
MODULE
当文件所属的内核组件作为模块编译时
CONFIG_HOTPLUG
当内核支持热拔插选项编译时定义("General setup"设置中的选项)

MODULE对不同的文件中有不同的值,而另外两个宏则具有内核作用域属性,因此它们在内核全局范围内要麽一直被设置,要麽不设置。
表 7-1和表7-2所示的宏中,我们最感兴趣的是下面与网卡驱动初始化相关的几个:__init, __exit, __initcall和 __exitcall。概括一下迄今为止我们讨论的内容:基于符号MODULE和 CONFIG_HOTPLUG是否被定义(我们假设内核支持可加载模 块,也就是说,CONFIG_MODULE被定义),图7-4展示了在节约内存方面前面列出的几个宏的效果。如下图所看到的,内核不支持模块动态加载和热 拔插与内核支持所有选项相比较有很多情况:你会有更多的限制,你可以获得更好的优化。

图7-4   表 7-1中的宏的效果

 

我们逐个看看图7-4中的1~6点的含义,谨记前面“新模式:基于宏的标记”小节中看到的设备驱动程序通用结构和前面“内存最优化”一节中看到的__initcall和__exitcall定义。

下面是把模块编译为内核的一部分时可以采用的优化:
1、module_exit函数从不被使用。因此由__exit标记它们,程序员要确保它们在连接时不会在内核映象中包含它们。
2、module_init函数仅在系统启动是执行一次,因此由__init标记它们,一旦它们执行了程序员就可以丢弃它们。
3、module_init(fn)是__initcall(fn)的别名,它们能够确保fn被do_initcalls被执行,这在“xxx_initcall宏”一节中可以看到
4、module_exit(fn)是__exitcall(fn)的别名,它们把输入函数的地址放在.exitcall.exit内存节,这使得内核在卸载时可以方便的执行fn函数。实际上,在连接时这个节就被丢弃了。
我们用PCI驱动来验证一下,看看需要热拔插支持的哪些优化被引入。这涉及到pci_driver->remove函数,它在模块卸载时调用,每个模块注册的设备驱动都调用一次。
5、不管MODULE是否被定义,当内核不支持热拔插时,设备不能够在系统运行时被移除。因此,remove函数决不会被PCI层调用,并被初始化为空指针(NULL).这由__devexit_p宏标记。
6、当内核不支持热拔插和模块时,模块不需要初始化pci_driver->remove函数的驱动程序,这是由__devexit宏标记的。注意,当模块支持模块时这是不正确的,因为用户允许加载或卸载模块,所以内核必须要remove函数。

注意:第五点是第六点的结果:如果你在内核没有包含初始化函数,你就不能够引用它(也就是说,你不能够为函数初始化指针)。

7.6、用/proc文件系统配置参数
本章没有与/proc系统有关内容
7.7、本节涉及的函数和变量

表7-4概括了本章介绍的函数、宏、数据结构和变量

表7-4  本章介绍的函数、宏、数据结构和变量
名称描述
函数和宏 
__init,__exit,__initcall,__exitcall,__initdata,__exitdata,__devinit,__devexit,__devexit_p,_devinitdata,__devexitdata,xxx_initcall 用于标记具有特殊要求的函数,这些宏标记可以优化内核映象大小,例如,移除不需要的代码.
do_initcalls启动时执行所有由xxx_initcall 宏标记的函数
init_module, cleanup_module, module_init, module_exit前两个宏是每个模块都要提供的用于单独初始化或卸载模块的函数名.另外两个是允许设备驱动程序编写人随意使用命名初始化或卸载模块的函数名。
netdev_boot, setup_check,neTDev_boot_setup_add应用于特殊设备启动时配置
module_param定义加载模块时提供的可选模块参数
数据结构 
kernel_param存储module_param 宏的入参
obs_kernel_param存储__setup宏的入参
netdev_boot_setup, ifmapnetdev_boot_setup为ether=和netdev=存储启动时参数,ifmap 是结构netdev_boot_setup的一个域.
变量 
dev_boot_setupnetdev_boot_setup结构数组
NETDEV_BOOT_SETUP_MAXdev_boot_setup数组大小

7.8、本节涉及的文件和目录
图7-5列出了本章参考的文件和目录

图7-5 本章参考的文件和目录
 

顺序和屏障

当处理多处理器之间或硬件设备之间的同步问题时,有时需要在程序代码中以指定的顺序发出读内存(读入)和写内存(存储)指令。在和硬件交互时,时常 需要确保一个给定的读操作发生在其他读或写操作之前。另外,在多处理上,可能需要按写数据时的顺序读数据(通常确保后来以同样的顺序进行读取)。但是编译 器和处理器为了提高效率,可能对读和写重新排序。   

所有可能重新排序和写的处理器提供了及其指令来确保顺序要求。同样也可以指示编译器不要对给定的点周围的指令进行重新排序,这些确保顺序的指令称为屏障(barrier)。

编译器会在编译时按代码的顺序编译,这种顺序是静态的。但是处理器会重新动态排序,因为处理器在执行指令期间,会在取值和分派时,把表面上看似无关的指令按自认为最好的顺序排列。这种重排序的发生是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序。

  不管是编译器还是处理器都不知道其他上下文中的相关代码。偶然情况下,有必要让写操作被其他代码识别,也让我们所期望的指定顺序之外的代码识别。这种情况常常发生在硬件设备上,但那是在多处理器机器上也很常见。

 

rmb()方法提供了一个“读”内存屏障,它确保跨越rmb()的载入动作不会发生重排序。在rmb()之前的载入操作不会被重新排在该调用之后;在rmb()之后的载入操作不会被重新排列在该调用之前。

  1. 在<System.h(includeasm-i386)>中
  2. #define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2) 

 

  1. 在<Alternative.h(includeasm-i386)>中
  2. /*
  3.  * Alternative instructions for different CPU types or capabilities.
  4.  *
  5.  * This allows to use optimized instructions even on generic binary
  6.  * kernels.
  7.  *
  8.  * length of oldinstr must be longer or equal the length of newinstr
  9.  * It can be padded with nops as needed.
  10.  *
  11.  * For non barrier like inlines please define new variants
  12.  * without volatile and memory clobber.
  13.  */
  14. #define alternative(oldinstr, newinstr, feature)    
  15.     asm volatile ("661:nt" oldinstr "n662:n"             
  16.               ".section .altinstructions,"a"n"            
  17.               "  .align 8n"                       
  18.               "  .quad 661bn"            /* label */          
  19.               "  .quad 663fn"        /* new instruction */ 
  20.               "  .byte %c0n"             /* feature bit */    
  21.               "  .byte 662b-661bn"       /* sourcelen */      
  22.               "  .byte 664f-663fn"       /* replacementlen */ 
  23.               ".previousn"                 
  24.               ".section .altinstr_replacement,"ax"n"     
  25.               "663:nt" newinstr "n664:n"   /* replacement */ 
  26.               ".previous" :: "i" (feature) : "memory")

wmb()方法提供了一个“写”内存屏障。该函数与rmb()类型,区别仅仅是它是针对存储而非载入。它确保跨越屏障的存储不发生重新排序。如果一个体系结构不执行打乱存储(比如Intel x86芯片就不会),那么wmb()就什么也补做。

 

  1. /*
  2.  * Force strict CPU ordering.
  3.  * And yes, this is required on UP too when we’re talking
  4.  * to devices.
  5.  *
  6.  * For now, "wmb()" doesn’t actually do anything, as all
  7.  * Intel CPU’s follow what Intel calls a *Processor Order*,
  8.  * in which all writes are seen in the program order even
  9.  * outside the CPU.
  10.  *
  11.  * I expect future Intel CPU’s to have a weaker ordering,
  12.  * but I’d also expect them to finally get their act together
  13.  * and add some real memory barriers if so.
  14.  *
  15.  * Some non intel clones support out of order store. wmb() ceases to be a
  16.  * nop for these.
  17.  */

 

  1. #ifdef CONFIG_X86_OOSTORE
  2. /* Actually there are no OOO store capable CPUs for now that do SSE, 
  3.    but make it already an possibility. */
  4. #define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
  5. #else
  6. #define wmb()   __asm__ __volatile__ ("": : :"memory")
  7. #endif

rmb()和wmb()方法相当于指令,它们告诉处理器在继续执行前提交所有尚未处理的载入或存储指令。

 

mb()方法既提供了读屏障也提供了写屏障。载入和存储动作都不会跨越屏障重新排序。这是因为一条单独的指令(通常和rmb()使用同一个指令)既可以提供载入屏障,也可以提供存储屏障。

 

  1. /* 
  2.  * Actually only lfence would be needed for mb() because all stores done 
  3.  * by the kernel should be already ordered. But keep a full barrier for now. 
  4.  */
  5.  
  6. #define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)

read_barrier_depends()是rmb()的变种,它提供一个读屏障,但是仅仅是针对后续读操 作锁依赖的那些载入。因为屏障后的读操作依赖于屏障前的读操作,因此,该屏障确保屏障前的读操作在屏障后的读操作之前完成。基本上说,该函数设置一个读屏 障,如rmb(),但是只真对特定的读—也就是那些相互依赖的读操作。

 

  1. /**
  2.  * read_barrier_depends – Flush all pending reads that subsequents reads
  3.  * depend on.
  4.  *
  5.  * No data-dependent reads from memory-like regions are ever reordered
  6.  * over this barrier.  All reads preceding this primitive are guaranteed
  7.  * to access memory (but not necessarily other CPUs’ caches) before any
  8.  * reads following this primitive that depend on the data return by
  9.  * any of the preceding reads.  This primitive is much lighter weight than
  10.  * rmb() on most CPUs, and is never heavier weight than is
  11.  * rmb().
  12.  *
  13.  * These ordering constraints are respected by both the local CPU
  14.  * and the compiler.
  15.  *
  16.  * Ordering is not guaranteed by anything other than these primitives,
  17.  * not even by data dependencies.  See the documentation for
  18.  * memory_barrier() for examples and URLs to more information.
  19.  *
  20.  * For example, the following code would force ordering (the initial
  21.  * value of "a" is zero, "b" is one, and "p" is "&a"):
  22.  *
  23.  * <programlisting>
  24.  *  CPU 0               CPU 1
  25.  *
  26.  *  b = 2;
  27.  *  memory_barrier();
  28.  *  p = &b;             q = p;
  29.  *                  read_barrier_depends();
  30.  *                  d = *q;
  31.  * </programlisting>
  32.  *
  33.  * because the read of "*q" depends on the read of "p" and these
  34.  * two reads are separated by a read_barrier_depends().  However,
  35.  * the following code, with the same initial values for "a" and "b":
  36.  *
  37.  * <programlisting>
  38.  *  CPU 0               CPU 1
  39.  *
  40.  *  a = 2;
  41.  *  memory_barrier();
  42.  *  b = 3;              y = b;
  43.  *                  read_barrier_depends();
  44.  *                  x = a;
  45.  * </programlisting>
  46.  *
  47.  * does not enforce ordering, since there is no data dependency between
  48.  * the read of "a" and the read of "b".  Therefore, on some CPUs, such
  49.  * as Alpha, "y" could be set to 3 and "x" to 0.  Use rmb()
  50.  * in cases like this where there are no data dependencies.
  51.  **/
  52.  
  53. #define read_barrier_depends()  do { } while(0)

第2个<programlisting>主要是为了说明read_barrier_depends()用于数据依赖的读操作,由于y和 x不是数据依赖的,因为没有成功设置读屏障,导致x=a在y=b之前运行,于是a可能还是0的时候就被赋给x了。此时,对于没有数据依赖的读操作应该使用 rmb()来提供读屏障。

宏smp_rmb()、smp_wbm()、smp_mb()和smp_read_barrier_depends()提供了一个有用的优化。在SMP 内核中,它们被定义成常用的内存屏障,而在单处理器内核中,它们被定义成编译器的屏障

 

  1. #ifdef CONFIG_SMP    //在SMP 内核中,它们被定义成常用的内存屏障
  2. #define smp_mb()    mb()
  3. #define smp_rmb()   rmb()
  4. #define smp_wmb()   wmb()
  5. #define smp_read_barrier_depends()  read_barrier_depends()
  6. #define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
  7. #else    //在单处理器内核中,它们被定义成编译器的屏障
  8. #define smp_mb()    barrier()
  9. #define smp_rmb()   barrier()
  10. #define smp_wmb()   barrier()
  11. #define smp_read_barrier_depends()  do { } while(0)
  12. #define set_mb(var, value) do { var = value; barrier(); } while (0)
  13. #endif

barrier()方法可以防止编译器跨越屏障对载入或存储操作进行优化。编译器不会重新组织存储或载入操作而防止改变C代码的效果和现有数据的依赖关系。但是,它不知道当前上下文之外会发生什么事。前面讨论的内存屏障可以完成编译器屏障的功能,但是后者比前者轻量得多。实际上,编译器屏障机会是空闲的,因为它只是防止编译器可能重排指令。

  1. 在<Compiler.h(includelinux)>中
  2. /* Optimization barrier */
  3. #ifndef barrier
  4. # define barrier() __memory_barrier()
  5. #endif
  6.  

 

为最坏的情况(即排序能力最弱的处理器)使用恰当的内存屏蔽,这样代码才能在编译时执行针对体系结构的优化。