I want to be a complete engineer - technical genius and sensitive humanist all in one!

Wednesday, July 15, 2009

不要脸的中国社会和不要脸的中国人

不要告诉我说中国是什么社会主义或资本主义,这些跟中国扯不上边,现在的中国就是一个不要脸的社会,中国人就是一群不要脸的人。
中国的不要脸首先体现在中国人价值观和是非观的丧失。国人社会价值的衡量标准就是钱的多寡;判断是非的标准就是是否符合经济效益;一个人社会地位的高低取决于其拥有财富的多少,而不管这些财富是如何来的。不要脸的中国人一边不断地诅咒这那些拥有财富的人一边盘算着如何效仿他们以便挤入他们的行列。
在中国最不要脸的组织就是我们的政府,榨取农民几十年的血汗,一旦免除了农民的农业税就把自己装扮成一个施恩者。整天在各种场合大谈关心农民和农民工就是迟迟不动林林种种的不合理体制,甚至不承认这些不合理。“以租待征”怎么就不行了,为什么到了现在还要喝我们农民的血?农民的金融合作就这么危险吗?
其次不要脸的组织是国有垄断企业,凭借着行政授予的垄断权大肆榨取消费者的血汗。更让人气愤的这些人一边喝着我们的血还一边一本正经地挖苦我们太不理解他们的困难。不是某位石油大亨的代言人说了嘛:“中国石油企业的利润率还不到50%”,我不知道这个人是不是白痴,综观发达国家的石油企业,还有谁的利润率能达到10%!无知的言论透露出的是这些部门的猖狂和无耻。有银行系统的代言人说话了:“银行员工的工资之所以高是因为银行属于高危险行业”。奶奶的!不要脸是不?要是这么危险你们怎么不和高空作业的农民工换换,还挤破头地把自己的小子闺女七大姑八大姨家的孩子往“火坑”里推?公路局也跟着不要脸,不就是坐着收费吗,凭什么你们一个月工资就是别人工资的十倍?电信局坐不住了,迫不及待地加入了不要脸的行列,无线上网就这么难吗?电信就是不发展,为什么?控制啊!要说不要脸的可别忘记我们的邮电局,不要脸的商业银行退出农村了,这个不要脸的进来了,光吸收存款不发放贷款,贫穷的中国农民又一次在为中国的城市作贡献了。

还有一个不要脸的组织就是大部分民营企业,资本家的血腥在中国的最近20多年已经把中国人浸透了,黑工厂、黑煤窑、黑砖窑、黑中介,所有的行业都有透着铜臭和血腥的组织。对于民营企业,除了个别的,有几个是干净的?榨取劳动者的血汗,规避法制,偷税漏税,使得朱总理为某个会计学院题词时实在想不起来其他更好的话,没办法才写了个“不做假帐!”这个就不说了,地球人都知道了!以至于美国工会过来为中国工人讨说法,丢人啊,中国政府,中国人!!
要说不要脸的,还有我们自己。胆小怯懦,目送英雄归西;狼狈为奸,冷眼事态炎凉;不负责任,事不关己高高挂起;极度自私,人不为己天诛地灭!
十年文革把中国旧有的价值观念砸的粉碎,改革开放使得国外的思想潮流蜂拥而至。中国是个没有宗教信仰的国家,中华民族是个没有精神寄托的民族。粉碎了旧有的价值体系,新的价值体系根本没有建立起来,每个人都在迷惘,都在彷徨,吃饱了肚子的人精神空虚的如同行尸走肉,于是,李洪志来了,说跟他走,搞什么真善美,于是,邪教诞生了!
他妈的,神奇的国度!神奇的事件!

Tuesday, July 14, 2009

TCP/IP 应用程序的通信连接模式

TCP/IP 应用层与应用程序

TCP/IP 起源于二十世纪 60 年代末美国政府资助的一个分组交换网络研究项目,它是一个真正的开放协议,很多不同厂家生产各种型号的计算机,它们运行完全不同的操作系统,但 TCP/IP 协议组件允许它们互相进行通信。现在 TCP/IP 已经从一个只供一些科学家使用的小实验网成长为一个由成千上万的计算机和用户构成的全球化网络,TCP/IP 也已成为全球因特网(Internet)的基础,越来越多的 TCP/IP 互联网应用和企业商业应用正在改变着世界。

TCP/IP 通讯协议采用了四层的层级模型结构(注:这与 OSI 七层模型不相同),每一层都调用它的下一层所提供的网络任务来完成自己的需求。TCP/IP 的每一层都是由一系列协议来定义的。这 4 层分别为:

 

  • 应用层 (Application):应用层是个很广泛的概念,有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应用。
  • 传输层 (Transport):传输层包括 UDP 和 TCP,UDP 几乎不对报文进行检查,而 TCP 提供传输保证。
  • 网络层 (Network):网络层协议由一系列协议组成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。
  • 链路层 (Link):又称为物理数据网络接口层,负责报文传输。

图1显示了 TCP/IP 层级模型结构,应用层之间的协议通过逐级调用传输层(Transport layer)、网络层(Network Layer)和物理数据链路层(Physical Data Link)而可以实现应用层的应用程序通信互联。

应用层需要关心应用程序的逻辑细节,而不是数据在网络中的传输活动。应用层其下三层则处理真正的通信细节。在 Internet 整个发展过程中的所有思想和着重点都以一种称为 RFC(Request For Comments)的文档格式存在。针对每一种特定的 TCP/IP 应用,有相应的 RFC 文档。一些典型的 TCP/IP 应用有 FTP、Telnet、SMTP、SNTP、REXEC、TFTP、LPD、SNMP、NFS、INETD 等。RFC 使一些基本相同的 TCP/IP 应用程序实现了标准化,从而使得不同厂家开发的应用程序可以互相通信。


图 1 TCP/IP 层级模型结构

然而除了这些已经实现标准化的系统级 TCP/IP 应用程序外,在企业商业应用和互联网应用开发中,存在着大量的商业应用程序通信互联问题。如图 1 显示,其中的应用层所包含应用程序主要可以分成两类,即系统级应用和商业应用,互联网商业应用是商业应用中的主要形式之一。

不同开发商和用户在开发各自商业应用通信程序时也存在有许多不同的设计方式。关于 TCP/IP 应用层以下的技术文献与书籍早已是汗牛充栋,但是关于 TCP/IP 应用本身,尤其是关于商业应用的通信设计模式技术讨论方面的文章还是比较少的。TCP/IP 应用通信设计模式实际上是在 TCP/IP 基础编程之上的一种应用编程设计方式,也属于一种应用层协议范畴,其可以包含有 TCP/IP 地址族模式设计、I/O 模式设计、通信连接模式设计以及通信数据格式设计等。鉴于目前讨论 TCP/IP 商业应用程序设计模式问题这方面的文章还很少见,本文尝试给出一些通信连接模式设计中共同的概念与一些典型的设计模式,在以后的文章中将继续讨论地址族模 式设计、I/O 模式设计、以及通信数据格式设计等方面的模式设计实现话题。

通信连接模式设计主要考虑内容有:

  • 通信两端程序建立通信方式
  • 通信连接方式
  • 通信报文发送与接收方式

以下内容将介绍建立通信的 Client/Server 模型,然后逐一介绍通信连接模式设计所需要考虑的这些内容。

传输层接口 APIs 与 TCP/IP 应用程序 C/S 模型

传输层接口 APIs

TCP/IP 应用层位于传输层之上,TCP/IP 应用程序需要调用传输层的接口才能实现应用程序之间通信。目前使用最广泛的传输层的应用编程接口是套接字接口(Socket)。Socket APIs 是于 1983 年在 Berkeley Socket Distribution (BSD) Unix 中引进的。 1986 年 AT&T 公司引进了另一种不同的网络层编程接口 TLI(Transport Layer Interface),1988 年 AT&T 发布了一种修改版的 TLI,叫做 XTI(X/open Transport interface)。XTI/TLI 和 Socket 是用来处理相同任务的不同方法。关于 TCP/IP APIs 使用文章与书籍已相当多,本文则是侧重于如何组合使用这些 APIs 来进行 TCP/IP 应用程序连接模式设计,并归纳出几种基本应用连接模式。

如图 2 显示,应用层是通过调用传输层接口 APIs(Socket 或 XTI/TLI)来与传输层和网络层进行通信的。


图 2 传输层接口

不管是使用何种编程接口,要在两个机器或两个程序之间建立通信,通信双方必须建立互相一致的通信模式。如果双方的通信设计模式不一致就无法建立有效的通信连接。

以下是经常使用的 socket APIs,是建立 TCP/IP 应用程序的标准接口,也是影响 TCP/IP 应用程序通信方式的几个主要 APIs,不同 APIs 组合再结合系统调用可以实现不同方式的应用。Sockets 支持多种传输层和网络层协议,支持面向连接和无连接的数据传输,允许应用分布式工作。

  • socket():是用来创建一个 socket,socket 表示通信中的一个节点,其可以在一个网络中被命名,用 socket 描述符表示,socket 描述符类似于 Unix 中的文件描述符。
  • bind():是用来把本地 IP 层地址和 TCP 层端口赋予 socket。
  • listen() :把未连接的 socket 转化成一个等待可连接的 socket,允许该 socket 可以被请求连接,并指定该 socket 允许的最大连接数。
  • accept():是等待一个连接的进入,连接成功后,产生一个新的 socket 描述符,这个新的描述符用来建立与客户端的连接。
  • connect():用来建立一个与服务端的连接。
  • send():发送一个数据缓冲区,类似 Unix 的文件函数 write()。另外 sendto() 是用在无连接的 UDP 程序中,用来发送自带寻址信息的数据包。
  • recv():接收一个数据缓冲区,类似 Unix 的文件函数 readI()。另外 recvfrom() 是用在无连接的 UDP 程序中,用来接收自带寻址信息的数据包。
  • close():关闭一个连接

Client/Server 模型

Sockets 是以 Client 和 Server 交互通信方式来使用的。典型的系统配置是把 Server 放在一台机器中,而把 Client 放在另一台机器中,Client 连接到 Server 交换信息。一个 socket 有一系列典型的事件流。例如,在面向连接的 Client/Server 模型中,Server 端的 socket 总是等待一个 Client 端的请求。要实现这个请求,Server 端首先需要建立能够被 Client 使用的地址,当地址建立后,Server 等待 Client 请求服务。当一个 Client 通过 socket 连接到 Server 后,Client 与 Server 之间就可以进行信息交换。Client/Server 是通信程序设计的基本模式。从软件开发的角度讲,TCP/IP 应用程序都是基于 Client/Server 方式的。注意本篇文章以下 Client/Server 概念是针对程序内部调用 Socket API 所讲的概念,与针对整个程序甚至针对机器而讲的客户端 / 服务器概念有所不同。用 Server APIs 建立的程序可以被当作客户端使用,用 Client APIs 建立的程序也可以被用作服务器端使用。建立 Server 需要的 APIs 有 socket(), bind(), listen(), accept(),建立 Client 需要的 APIs 有 Socket(), Connect()。在实际应用开发中,同一个程序里往往同时可以有 Client 和 Server 的代码,或者多种形式的组合。在实际应用编程中,针对 Socket APIs 不同有效组合,结合系统调用可以有多种复杂的设计变化。

面向连接的应用编程存在三类基本的不同级别的设计方式范畴,根据 Socket APIs 从上到下顺序依次是:

  • Client/Server 通信建立方式
  • Client/Server 通信连接方式
  • Client/Server 通信发送与接收方式

下面内容以面向连接的 Socket 应用编程为例来说明这几种不同通信范畴的设计实现。

Client/Server 建立方式设计概述

一个 Client 连接一个 Server

如果只有两台机器之间连接,那么一个是 Client,另一个是 Server,如下面图 3 所示。这是最简单的 TCP/IP 的应用,也是 TCP/IP 应用早期的 Peer to Peer (P2P) 概念。其流程基本如图 4 所示。


图 3 TCP/IP 应用单点 Client/Server

图 4 显示了 TCP/IP 应用编程最基本的 Client/Server 模式,显示了基本的 Client/Server 通信所需要调用的 Socket APIs 以及顺序。


图 4 TCP/IP 应用编程基本 Client/Server 模式

多个 Client 连接一个 Server

多个 Client 同时连接一个 Server 是 TCP/IP 应用的主流形式,如图 5 所示,其中 Client 连接数可以从几个到成千上万。


图 5 TCP/IP 应用多 Client 端的 Client/Server

由于 socket APIs 缺省方式下都是阻塞方式的,实现多个 Client 同时连接一个 Server 就需要特别的设计。其实现方式可以有多种不同的设计,这其中也涉及 I/O 模式设计。下面将展开介绍其中几种设计形式。

利用一个 Client 连接一个 Server 形式实现多 Client 连接

从程序设计角度讲,只要 Client 和 Server 端口是一对一形式,那么就属于一个 Client 连接一个 Server 形式。在处理多个 Client 端连接时,Server 端轮流使用多个端口建立多个 Client-Server 连接,连接关闭后,被释放端口可以被循环使用。在这种多连接形式中需要谨慎处理 Client 端如何获取使用 Server 端的可用端口。比如图 6 显示 Server 有一个服务于所有进程的进程可以先把 Server 端的可用端口发送给 Client 端,Client 端再使用该端口建立连接来处理业务。Server 针对每一个 Client 连接用一个专门的进程来处理。由于可用端口数有限,Server 用一个有限循环来处理每一个可用的端口连接。由于新端口需要用 bind() 来绑定,所以需要从 bind() 开始到 close() 结束都需要包含在循环体内。


图 6 利用一对一 Client-Server 模式实现多 Client 连接

使用多个 accept() 实现多 Client 连接

多进程 Server 一般有一个专注进程是服务于每一个连接的。当 Client 端完成连接后,专注进程可以循环被另外的连接使用。使用多个 accept() 也可以实现处理多 Client 连接。多 accept() 的 Server 也只有一个 socket(),一个 bind(),一个 listen(),这与通常情况一样。但是它建立许多工作子进程,每一个工作子进程都有 accept(),这样可以为每一个 Client 建立 socket 描述符。如图 7 所示,由于 accept() 连接成功后,会产生一个新的 socket 描述符,这样通过循环多进程利用 accept() 产生的多 socket 描述符就可以与多个 Client 进行连接通信。循环体是从 accept() 开始到 close() 结束的。


图 7 使用多 accept() 实现多 Client 连接

使用并发 Server 模式实现多 Client 连接

并发服务器模式曾经是 TCP/IP 的主流应用程序设计模式,得到广泛使用,目前互联网上仍有相当多的应用使用此种模式。其设计思路是在 accept 之后 fork 出一个子进程。因为 socket 会产生监听 socket 描述符 listenfd,accept 会产生连接 socket 描述符 connfd。连接建立后,子进程继承连接描述符服务于 Client,父进程则继续使用监听描述符等待另外一个 Client 的连接请求,以产生另外一个连接 socket 描述符和子进程。如图 8 所示,accept() 接收到一个 Client 连接后,产生一个新的 socket 描述符,通过 fork() 系统调用,用一个子进程来处理该 socket 描述符的连接服务。而父进程可以立即返回到 accept(),等待一个新的 Client 请求,这就是典型的并发服务器模式。并发服务器模式同时处理的最大并发 Client 连接数由 listen() 的第二个参数来指定。


图 8 TCP/IP 应用并发 Server

使用 I/O 多路技术实现多 Client 连接

以上三种连接设计,多 Server 端口、多 accept() 和并发服务器模式,都是通过 fork() 系统调用产生多进程来实现多 Client 连接的。使用 I/O 多路技术也可以同时处理多个输入与输出问题,即用一个进程同时处理多个文件描述符。I/O 多路技术是通过 select() 或 poll() 系统调用实现的。poll() 与 select() 功能完全相同,但是 poll() 可以更少使用内存资源以及有更少的错误发生。select() 调用需要与操作文件描述符集的 APIs 配合使用。select() 系统调用可以使一个进程检测多个等待的 I/O 是否准备好,当没有设备准备好时,select() 处于阻塞状态中,其中任一设备准备好后,select() 函数返回调用。select() API 本身也有一个超时时间参数,超时时间到后,无论是否有设备准备好,都返回调用。其流程如图 9 所示。在 socket APIs listen() 和 accept() 之间插入 select() 调用。使用这三个宏 FD_ZERO()、FD_CLR() 和 FD_SET(),在调用 select() 前设置 socket 描述符屏蔽位,在调用 select() 后使用 FD_ISSET 来检测 socket 描述符集中对应于 socket 描述符的位是否被设置。 FD_ISSET() 就相当通知了一个 socket 描述符是否可以被使用,如果该 socket 描述符可用,则可对该 socket 描述符进行读写通信操作。通常,操作系统通过宏 FD_SETSIZE 来声明在一个进程中 select() 所能操作的文件或 socket 描述符的最大数目。更详细的 I/O 多路技术实现,可以参考其他相关文献。


图 9 I/O 多路技术实现多连接的 Server

一个 Client 连接多个 Server

一个 Client 连接多个 Server 这种方式很少见,主要用于一个客户需要向多个服务器发送请求情况,比如一个 Client 端扫描连接多个 Server 端情况。如图 10 所示。此种方式设计主要是 Client 端应用程序的逻辑设计,通常需要在 Client 端设计逻辑循环来连接多个 Server,在此不做更多描述。


图 10 单 Client 对多 Server

复杂 Client/Server 设计与现代 P2P

最近几年,对等网络技术 ( Peer-to-Peer,简称 P2P) 迅速成为计算机界关注的热门话题之一,以及影响 Internet 未来的科技之一。与早期点对点 (Peer to Peer) 的 Client/Server 模式不同,现在的 P2P 模式是指每个结点既可充当服务器,为其他结点提供服务,同时也可作为客户端享用其他结点提供的服务。实际上 P2P 模式仍然是基于 Client/Server 模式的,每个通信节点都既是 Server,又是 Client,P2P 是基于复杂 Client/Server 设计的 TCP/IP 应用。图 11 显示 P2P 模式下两个用户 PC 之间的对等连接。


图 11 P2P 模式

在技术上,P2P 本身是基于 TCP/IP Client/Server 技术的一种设计模式思想, P2P 也属于网络应用层技术,与 Web 和 FTP 等应用是并列的。只是 P2P 应用在设计实现上更要复杂的多。P2P 技术实现的协同工作是无需专门的服务器支持的 (Serverless),这里的服务器概念与 Client/Server 中的 Server 概念是不一样的。在传统意义上中心服务器机器上往往运行的是 TCP/IP 应用的 Server 端程序,所以传统意义上的 Server 概念在机器与应用上是重合的。如果更改 TCP/IP 的应用设计,使应用程序既可做 Server 又可做 Client,就可以实现无中心服务器的 P2P 模式。

在设计模式上,P2P 模式实现了网络终端用户不依赖中心服务器或者服务商而直接进行信息和数据交换的可能,因此 P2P 正在改变着整个互联网的一些基础应用,从而极大地增加了用户之间的信息沟通和交流能力。目前互联网的 P2P 应用与网络都正在飞速发展,一些典型的 P2P 应用程序比如有 BitTorrent, eDonkey 等,另外一些即时通信(IM)类软件比如 MSN、QQ 等也正在向无中心服务器模式转变。无中心服务器的 Internet 应用程序大大降低应用提供商的运营成本,而且减少人们对于 Server 稳定性的依赖。

Client/Server 通信连接方式设计

Client/Server 通信方式建立后,下一步就需要考虑通信连接的方式,主要有两种方式的连接,即长连接通信与短连接通信。通信连接方式涉及到的 APIs 主要是 connect() 和 accept()。要实现某种 Client/Server 方式,就必须考虑用某种特定的连接方式。

短连接通信

短连接通信是指 Client 方与 Server 方每进行一次通信报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于多个 Client 连接一个 Server 情况,常用于机构与用户之间通信,比如 OLTP(联机事务处理)类应用。在短连接情况下,Client 端完成任务后,就关闭连接并退出。在 Server 端,可以通过循环 accept(),使 Server 不会退出,并连续处理 Client 的请求。图 12 显示了一般情况下短连接通信模式的 Socket 事件流,不同设计的连接多 Client 的 Server 有不同的循环流程。


图 12 短连接模式通信

长连接通信

长连接通信是指 Client 方与 Server 方先建立通讯连接,连接建立后不会断开,然后再进行报文发送和接收,报文发送与接收完毕后,原来连接不会断开而继续存在,因此可以连续进行交易报文的发送 与接收。这种方式下由于通讯连接一直存在,其 TCP/IP 状态是 Established,可以用操作系统的命令 netstat 查看连接是否建立。由于在长连接情况下,Client 端和 Server 端一样可以固定使用一个端口,所以长连接下的 Client 也需要使用 bind() 来绑定 Client 的端口。在长连接方式下,需要循环读写通信数据。为了区分每一次交易的通信数据,每一次交易数据常常需要在数据头部指定该次交易的长度,接收 API 需要首先读出该长度,然后再按该长度读出指定长度的字节。长连接方式常用于一个 Client 端对一个 Server 端的通讯,一般常用于机构与机构之间的商业应用通信,以处理机构之间连续的大量的信息数据交换。或者说可用于两个系统之间持续的信息交流情况。通常为了加 快两个系统之间的信息交流,通常还需要建立几条长连接的并行通信线路。图 13 显示了一般情况下长连接通信模式的 socket 事件流,可见其最大特点是 Client 和 Server 都有循环体,而且循环体只包含读写 APIs。


图 13 长连接模式通信

Client/Server 通信发送与接收方式设计

在通信数据发送与接收之间也存在不同的方式,即同步和异步两种方式。这里的同步和异步与 I/O 层次的同异步概念不同。主要涉及 socket APIs recv() 和 send() 的不同组合方式。

同步发送与接收

从应用程序设计的角度讲,报文发送和接收是同步进行的,既报文发送后,发送方等待接收方返回消息报文。同步方式一般需要考虑超时问题,即报文发出去后发送 方不能无限等待,需要设定超时时间,超过该时间后发送方不再处于等待状态中,而直接被通知超时返回。同步发送与接收经常与短连接通信方式结合使用,称为同 步短连接通信方式,其 socket 事件流程可如上面的图 12 所示。

异步发送与接收

从应用程序设计的角度讲,发送方只管发送数据,不需要等待接收任何返回数据,而接收方只管接收数据,这就是应用层的异步发送与接收方式。要实现异步方式, 通常情况下报文发送和接收是用两个不同的进程来分别处理的,即发送与接收是分开的,相互独立的,互不影响。异步发送与接收经常与长连接通信方式结合使用, 称为异步长连接通信方式。从应用逻辑角度讲,这种方式又可分双工和单工两种情况。

异步双工

异步双工是指应用通信的接收和发送在同一个程序中,而有两个不同的子进程分别负责发送和接收,异步双工模式是比较复杂的一种通信方式,有时候经常会出现在 不同机构之间的两套系统之间的通信。比如银行与银行之间的信息交流。它也可以适用在现代 P2P 程序中。如图 14 所示,Server 和 Client 端分别 fork 出两个子进程,形成两对子进程之间的连接,两个连接都是单向的,一个连接是用于发送,另一个连接用于接收,这样方式的连接就被称为异步双工方式连接。


图 14 长连接异步双工模式

异步单工

应用通信的接收和发送是用两个不同的程序来完成,这种异步是利用两对不同程序依靠应用逻辑来实现的。图 15 显示了长连接方式下的异步单工模式,在通信的 A 和 B 端,分别有两套 Server 和 Client 程序,B 端的 Client 连接 A 端的 Server,A 端的 Server 只负责接收 B 端 Client 发送的报文。A 端的 Client 连接 B 端的 Server,A 端 Client 只负责向 B 端 Server 发送报文。


图 15 长连接异步单工模式


典型通信连接模式

综上所述,在实际 TCP/IP 应用程序设计中,就连接模式而言,我们需要考虑 Client/Server 建立方式、Client/Server 连接方式、Client/Server 发送与接收方式这三个不同级别的设计方式。实际 TCP/IP 应用程序连接模式可以是以上三类不同级别 Client/Server 方式的组合。比如一般 TCP/IP 相关书籍上提供的 TCP/IP 范例程序大都是同步短连接的 Client/Server 程序。有的组合是基本没有实用价值的,比较常用的有价值的组合是以下几种:

  • 同步短连接 Server/Client
  • 同步长连接 Server/Client
  • 异步短连接 Server/Client
  • 异步长连接双工 Server/Client
  • 异步长连接单工 Server/Client

其中异步长连接双工是较为复杂的一种通信方式,有时候经常会出现在不同银行或不同城市之间的两套系统之间的通信,比如国家金卡工程。由于这几种通信方式比较固定,所以可以预先编制这几种通信方式的模板程序。

总结

本文探讨了 TCP/IP 应用程序中连接模式的设计。在以后的文章中还将继续讨论 TCP/IP 应用程序设计中的其他方面的设计话题,包括地址族模式设计、I/O 模式设计、以及通信数据格式设计等。

come from:http://www.ibm.com/developerworks/cn/aix/library/0807_liugb_tcpip/

Linux2.6 内核的 Initrd 机制解析

1.什么是 Initrd

initrd 的英文含义是 boot loader initialized RAM disk,就是由 boot loader 初始化的内存盘。在 linux内核启动前, boot loader 会将存储介质中的 initrd 文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的 initrd 文件系统。在 boot loader 配置了 initrd 的情况下,内核启动被分成了两个阶段,第一阶段先执行 initrd 文件系统中的"某个文件",完成加载驱动模块等任务,第二阶段才会执行真正的根文件系统中的 /sbin/init 进程。这里提到的"某个文件",Linux2.6 内核会同以前版本内核的不同,所以这里暂时使用了"某个文件"这个称呼,后面会详细讲到。第一阶段启动的目的是为第二阶段的启动扫清一切障爱,最主要的是 加载根文件系统存储介质的驱动模块。我们知道根文件系统可以存储在包括IDE、SCSI、USB在内的多种介质上,如果将这些设备的驱动都编译进内核,可 以想象内核会多么庞大、臃肿。

Initrd 的用途主要有以下四种:

1. linux 发行版的必备部件

linux 发行版必须适应各种不同的硬件架构,将所有的驱动编译进内核是不现实的,initrd 技术是解决该问题的关键技术。Linux 发行版在内核中只编译了基本的硬件驱动,在安装过程中通过检测系统硬件,生成包含安装系统硬件驱动的 initrd,无非是一种即可行又灵活的解决方案。

2. livecd 的必备部件

同 linux 发行版相比,livecd 可能会面对更加复杂的硬件环境,所以也必须使用 initrd。

3. 制作 Linux usb 启动盘必须使用 initrd

usb 设备是启动比较慢的设备,从驱动加载到设备真正可用大概需要几秒钟时间。如果将 usb 驱动编译进内核,内核通常不能成功访问 usb 设备中的文件系统。因为在内核访问 usb 设备时, usb 设备通常没有初始化完毕。所以常规的做法是,在 initrd 中加载 usb 驱动,然后休眠几秒中,等待 usb设备初始化完毕后再挂载 usb 设备中的文件系统。

4. 在 linuxrc 脚本中可以很方便地启用个性化 bootsplash。

2.Linux2.4内核对 Initrd 的处理流程

为 了使读者清晰的了解Linux2.6内核initrd机制的变化,在重点介绍Linux2.6内核initrd之前,先对linux2.4内核的 initrd进行一个简单的介绍。Linux2.4内核的initrd的格式是文件系统镜像文件,本文将其称为image-initrd,以区别后面介绍 的linux2.6内核的cpio格式的initrd。 linux2.4内核对initrd的处理流程如下:

1. boot loader把内核以及/dev/initrd的内容加载到内存,/dev/initrd是由boot loader初始化的设备,存储着initrd。

2. 在内核初始化过程中,内核把 /dev/initrd 设备的内容解压缩并拷贝到 /dev/ram0 设备上。

3. 内核以可读写的方式把 /dev/ram0 设备挂载为原始的根文件系统。

4. 如果 /dev/ram0 被指定为真正的根文件系统,那么内核跳至最后一步正常启动。

5. 执行 initrd 上的 /linuxrc 文件,linuxrc 通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。

6. /linuxrc 执行完毕,真正的根文件系统被挂载。

7. 如果真正的根文件系统存在 /initrd 目录,那么 /dev/ram0 将从 / 移动到 /initrd。否则如果 /initrd 目录不存在, /dev/ram0 将被卸载。

8. 在真正的根文件系统上进行正常启动过程 ,执行 /sbin/init。 linux2.4 内核的 initrd 的执行是作为内核启动的一个中间阶段,也就是说 initrd 的 /linuxrc 执行以后,内核会继续执行初始化代码,我们后面会看到这是 linux2.4 内核同 2.6 内核的 initrd 处理流程的一个显著区别。


3.Linux2.6 内核对 Initrd 的处理流程

linux2.6 内核支持两种格式的 initrd,一种是前面第 3 部分介绍的 linux2.4 内核那种传统格式的文件系统镜像-image-initrd,它的制作方法同 Linux2.4 内核的 initrd 一样,其核心文件就是 /linuxrc。另外一种格式的 initrd 是 cpio 格式的,这种格式的 initrd 从 linux2.5 起开始引入,使用 cpio 工具生成,其核心文件不再是 /linuxrc,而是 /init,本文将这种 initrd 称为 cpio-initrd。尽管 linux2.6 内核对 cpio-initrd和 image-initrd 这两种格式的 initrd 均支持,但对其处理流程有着显著的区别,下面分别介绍 linux2.6 内核对这两种 initrd 的处理流程。

cpio-initrd 的处理流程

1. boot loader 把内核以及 initrd 文件加载到内存的特定位置。

2. 内核判断initrd的文件格式,如果是cpio格式。

3. 将initrd的内容释放到rootfs中。

4. 执行initrd中的/init文件,执行到这一点,内核的工作全部结束,完全交给/init文件处理。

image-initrd的处理流程

1. boot loader把内核以及initrd文件加载到内存的特定位置。

2. 内核判断initrd的文件格式,如果不是cpio格式,将其作为image-initrd处理。

3. 内核将initrd的内容保存在rootfs下的/initrd.image文件中。

4. 内核将/initrd.image的内容读入/dev/ram0设备中,也就是读入了一个内存盘中。

5. 接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。

6. .如果/dev/ram0被指定为真正的根文件系统,那么内核跳至最后一步正常启动。

7. 执行initrd上的/linuxrc文件,linuxrc通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。

8. /linuxrc执行完毕,常规根文件系统被挂载

9. 如果常规根文件系统存在/initrd目录,那么/dev/ram0将从/移动到/initrd。否则如果/initrd目录不存在, /dev/ram0将被卸载。

10. 在常规根文件系统上进行正常启动过程 ,执行/sbin/init。

通 过上面的流程介绍可知,Linux2.6内核对image-initrd的处理流程同linux2.4内核相比并没有显著的变化, cpio-initrd的处理流程相比于image-initrd的处理流程却有很大的区别,流程非常简单,在后面的源代码分析中,读者更能体会到处理的 简捷。

4.cpio-initrd同image-initrd的区别与优势

没有找到正式的关于cpio-initrd同image-initrd对比的文献,根据笔者的使用体验以及内核代码的分析,总结出如下三方面的区别,这些区别也正是cpio-initrd的优势所在:

cpio-initrd的制作方法更加简单

cpio-initrd的制作非常简单,通过两个命令就可以完成整个制作过程


#假设当前目录位于准备好的initrd文件系统的根目录下
bash# find . | cpio -c -o > ../initrd.img
bash# gzip ../initrd.img

而传统initrd的制作过程比较繁琐,需要如下六个步骤


#假设当前目录位于准备好的initrd文件系统的根目录下
bash# dd if=/dev/zero of=../initrd.img bs=512k count=5
bash# mkfs.ext2 -F -m0 ../initrd.img
bash# mount -t ext2 -o loop ../initrd.img /mnt
bash# cp -r * /mnt
bash# umount /mnt
bash# gzip -9 ../initrd.img

本文不对上面命令的含义作细节的解释,因为本文主要介绍的是linux内核对initrd的处理,对上面命令不理解的读者可以参考相关文档。

cpio-initrd的内核处理流程更加简化

通过上面initrd处理流程的介绍,cpio-initrd的处理流程显得格外简单,通过对比可知cpio-initrd的处理流程在如下两个方面得到了简化:

1. cpio-initrd并没有使用额外的ramdisk,而是将其内容输入到rootfs中,其实rootfs本身也是一个基于内存的文件系统。这样就省掉了ramdisk的挂载、卸载等步骤。

2. cpio-initrd启动完/init进程,内核的任务就结束了,剩下的工作完全交给/init处理;而对于image-initrd,内核在执行完 /linuxrc进程后,还要进行一些收尾工作,并且要负责执行真正的根文件系统的/sbin/init。通过图1可以更加清晰的看出处理流程的区别:


图1内核对cpio-initrd和image-initrd处理流程示意图
图1内核对cpio-initrd和image-initrd处理流程示意图

cpio-initrd的职责更加重要

如 图1所示,cpio-initrd不再象image-initrd那样作为linux内核启动的一个中间步骤,而是作为内核启动的终点,内核将控制权交给 cpio-initrd的/init文件后,内核的任务就结束了,所以在/init文件中,我们可以做更多的工作,而不比担心同内核后续处理的衔接问题。 当然目前linux发行版的cpio-initrd的/init文件的内容还没有本质的改变,但是相信initrd职责的增加一定是一个趋势。

5.linux2.6内核initrd处理的源代码分析

上 面简要介绍了Linux2.4内核和2.6内核的initrd的处理流程,为了使读者对于Linux2.6内核的initrd的处理有一个更加深入的认 识,下面将对Linuxe2.6内核初始化部分同initrd密切相关的代码给予一个比较细致的分析,为了讲述方便,进一步明确几个代码分析中使用的概 念:

rootfs: 一个基于内存的文件系统,是linux在初始化时加载的第一个文件系统,关于它的进一步介绍可以参考文献[4]。

initramfs: initramfs同本文的主题关系不是很大,但是代码中涉及到了initramfs,为了更好的理解代码,这里对其进行简单的介绍。Initramfs 是在 kernel 2.5中引入的技术,实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个 cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显 的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。Linux 2.6.12内核的 initramfs还没有什么实质性的东西,一个包含完整功能的initramfs的实现可能还需要一个缓慢的过程。对于initramfs的进一步了解 可以参考文献[1][2][3]。

cpio-initrd: 前面已经定义过,指linux2.6内核使用的cpio格式的initrd。

image-initrd: 前面已经定义过,专指传统的文件镜像格式的initrd。

realfs: 用户最终使用的真正的文件系统。

内 核的初始化代码位于 init/main.c 中的 static int init(void * unused)函数中。同initrd的处理相关部分函数调用层次如下图,笔者按照这个层次对每一个函数都给予了比较详细的分析,为了更好的说明,下面列 出的代码中删除了同本文主题不相关的部分:


图2 initrd相关代码的调用层次关系图
图2 initrd相关代码的调用层次关系图

init函数是内核所有初始化代码的入口,代码如下,其中只保留了同initrd相关部分的代码。


static int init(void * unused){
[1] populate_rootfs();

[2] if (sys_access((const char __user *) "/init", 0) == 0)
execute_command = "/init";
else
prepare_namespace();
[3] if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
[4] if (execute_command)
run_init_process(execute_command);
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}

代码[1]:populate_rootfs函数负责加载initramfs和cpio-initrd,对于populate_rootfs函数的细节后面会讲到。

代码[2]:如果rootfs的根目录下中包含/init进程,则赋予execute_command,在init函数的末尾会被执行。否则执行prepare_namespace函数,initrd是在该函数中被加载的。

代码[3]:将控制台设置为标准输入,后续的两个sys_dup(0),则复制标准输入为标准输出和标准错误输出。

代 码[4]:如果rootfs中存在init进程,就将后续的处理工作交给该init进程。其实这段代码的含义是如果加载了cpio-initrd则交给 cpio-initrd中的/init处理,否则会执行realfs中的init。读者可能会问:如果加载了cpio-initrd, 那么realfs中的init进程不是没有机会运行了吗?确实,如果加载了cpio-initrd,那么内核就不负责执行realfs的init进程了, 而是将这个执行任务交给了cpio-initrd的init进程。解开fedora core4的initrd文件,会发现根目录的下的init文件是一个脚本,在该脚本的最后一行有这样一段代码:


………..
switchroot --movedev /sysroot

就是switchroot语句负责加载realfs,以及执行realfs的init进程。

对cpio-initrd的处理

对cpio-initrd的处理位于populate_rootfs函数中。


void __init populate_rootfs(void){
[1] char *err = unpack_to_rootfs(__initramfs_start,
__initramfs_end - __initramfs_start, 0);
[2] if (initrd_start) {
[3] err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 1);

[4] if (!err) {
printk(" it is\n");
unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 0);
free_initrd_mem(initrd_start, initrd_end);
return;
}
[5] fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start,
initrd_end - initrd_start);
sys_close(fd);
free_initrd_mem(initrd_start, initrd_end);
}
}

代码[1]:加载initramfs, initramfs位于地址__initramfs_start处,是内核在编译过程中生成的,initramfs的是作为内核的一部分而存在的,不是 boot loader加载的。前面提到了现在initramfs没有任何实质内容。

代码[2]:判断是否加载了initrd。无论哪种格式的initrd,都会被boot loader加载到地址initrd_start处。

代码[3]:判断加载的是不是cpio-initrd。实际上 unpack_to_rootfs有两个功能一个是释放cpio包,另一个就是判断是不是cpio包, 这是通过最后一个参数来区分的, 0:释放 1:查看。

代码[4]:如果是cpio-initrd则将其内容释放出来到rootfs中。

代码[5]:如果不是cpio-initrd,则认为是一个image-initrd,将其内容保存到/initrd.image中。在后面的image-initrd的处理代码中会读取/initrd.image。

对image-initrd的处理 在prepare_namespace函数里,包含了对image-initrd进行处理的代码,相关代码如下:


void __init prepare_namespace(void){
[1] if (initrd_load())
goto out;
out:
umount_devfs("/dev");
[2] sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
security_sb_post_mountroot();
mount_devfs_fs ();
}

代码[1]:执行initrd_load函数,将initrd载入,如果载入成功的话initrd_load函数会将realfs的根设置为当前目录。

代码[2]:将当前目录即realfs的根mount为Linux VFS的根。initrd_load函数执行完后,将真正的文件系统的根设置为当前目录。

initrd_load函数负责载入image-initrd,代码如下:


int __init initrd_load(void)
{
[1] if (mount_initrd) {
create_dev("/dev/ram", Root_RAM0, NULL);
[2] if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
sys_unlink("/initrd.image");
handle_initrd();
return 1;
}
}
sys_unlink("/initrd.image");
return 0;
}

代码[1]:如果加载initrd则建立一个ram0设备 /dev/ram。

代 码[2]:/initrd.image文件保存的就是image-initrd,rd_load_image函数执行具体的加载操作,将image- nitrd的文件内容释放到ram0里。判断ROOT_DEV!=Root_RAM0的含义是,如果你在grub或者lilo里配置了 root=/dev/ram0 ,则实际上真正的根设备就是initrd了,所以就不把它作为initrd处理 ,而是作为realfs处理。

handle_initrd()函数负责对initrd进行具体的处理,代码如下:


 static void __init handle_initrd(void){
[1] real_root_dev = new_encode_dev(ROOT_DEV);
[2] create_dev("/dev/root.old", Root_RAM0, NULL);
mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
[3] sys_mkdir("/old", 0700);
root_fd = sys_open("/", 0, 0);
old_fd = sys_open("/old", 0, 0);
/* move initrd over / and chdir/chroot in initrd root */
[4] sys_chdir("/root");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
mount_devfs_fs ();
[5] pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
if (pid > 0) {
while (pid != sys_wait4(-1, &i, 0, NULL))
yield();
}
/* move initrd to rootfs' /old */
sys_fchdir(old_fd);
sys_mount("/", ".", NULL, MS_MOVE, NULL);
/* switch root and cwd back to / of rootfs */
[6] sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);
umount_devfs("/old/dev");
[7] if (new_decode_dev(real_root_dev) == Root_RAM0) {
sys_chdir("/old");
return;
}
[8] ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();
[9] printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay\n");
else {
int fd = sys_open("/dev/root.old", O_RDWR, 0);
printk("failed\n");
printk(KERN_NOTICE "Unmounting old root\n");
sys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = sys_ioctl(fd, BLKFLSBUF, 0);
sys_close(fd);
}
printk(!error ? "okay\n" : "failed\n");
}

handle_initrd函数的主要功能是执行initrd的linuxrc文件,并且将realfs的根目录设置为当前目录。

代码[1]:real_root_dev,是一个全局变量保存的是realfs的设备号。

代码[2]:调用mount_block_root函数将initrd文件系统挂载到了VFS的/root下。

代码[3]:提取rootfs的根的文件描述符并将其保存到root_fd。它的作用就是为了在chroot到initrd的文件系统,处理完initrd之后要,还能够返回rootfs。返回的代码参考代码[7]。

代码[4]:chroot进入initrd的文件系统。前面initrd已挂载到了rootfs的/root目录。

代码[5]:执行initrd的linuxrc文件,等待其结束。

代码[6]:initrd处理完之后,重新chroot进入rootfs。

代码[7]:如果real_root_dev在 linuxrc中重新设成Root_RAM0,则initrd就是最终的realfs了,改变当前目录到initrd中,不作后续处理直接返回。

代码[8]:在linuxrc执行完后,realfs设备已经确定,调用mount_root函数将realfs挂载到root_fs的 /root目录下,并将当前目录设置为/root。

代码[9]:后面的代码主要是做一些收尾的工作,将initrd的内存盘释放。

到此代码分析完毕。

6.结束语

通过本文前半部分对cpio-initrd和imag-initrd的阐述与对比以及后半部分的代码分析,我相信读者对Linux 2.6内核的initrd技术有了一个较为全面的了解。在本文的最后,给出两点最重要的结论:

1. 尽管Linux2.6既支持cpio-initrd,也支持image-initrd,但是cpio-initrd有着更大的优势,在使用中我们应该优先考虑使用cpio格式的initrd。

2. cpio-initrd相对于image-initrd承担了更多的初始化责任,这种变化也可以看作是内核代码的用户层化的一种体现,我们在其它的诸如 FUSE等项目中也看到了将内核功能扩展到用户层实现的尝试。精简内核代码,将部分功能移植到用户层必然是linux内核发展的一个趋势。

come from:http://www.ibm.com/developerworks/cn/linux/l-k26initrd/

Linux 多线程应用中如何编写安全的信号处理函数

在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 pthread_mutex 去保护全局变量。如果应用中使用了信号,而且信号的产生不是因为程序运行出错,而是程序逻辑需要,譬如 SIGUSR1、SIGRTMIN 等,信号在被处理后应用程序还将正常运行。在编写这类信号处理函数时,应用层面的开发人员却往往忽略了信号处理函数执行的上下文背景,没有考虑编写安全的 信号处理函数的一些规则。本文首先介绍编写信号处理函数时需要考虑的一些规则;然后举例说明在多线程应用中如何构建模型让因为程序逻辑需要而产生的异步信 号在指定的线程中以同步的方式处理。

线程和信号

Linux 多线程应用中,每个线程可以通过调用 pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。

当一个线程调用 pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。

POSIX.1 标准定义了一系列线程函数的接口,即 POSIX threads(Pthreads)。Linux C 库提供了两种关于线程的实现:LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已经过时,一些函数的实现不遵循POSIX.1 规范。NPTL 依赖 Linux 2.6 内核,更加遵循 POSIX..1 规范,但也不是完全遵循。

基于 NPTL 的线程库,多线程应用中的每个线程有自己独特的线程 ID,并共享同一个进程ID。应用程序可以通过调用 kill(getpid(),signo) 将信号发送到进程,如果进程中当前正在执行的线程没有阻碍此信号,则会被中断,线号处理函数会在此线程的上下文背景中执行。应用程序也可以通过调用 pthread_kill(pthread_t thread, int sig) 将信号发送给指定的线程,则线号处理函数会在此指定线程的上下文背景中执行。

基于 LinuxThreads 的线程库,多线程应用中的每个线程拥有自己独特的进程 ID,getpid() 在不同的线程中调用会返回不同的值,所以无法通过调用 kill(getpid(),signo) 将信号发送到整个进程。


编写安全的异步信号处理函数

信号的产生可以是:

  • 用户从控制终端终止程序运行,如 Ctrk + C 产生 SIGINT;
  • 程序运行出错时由硬件产生信号,如访问非法地址产生 SIGSEGV;
  • 程序运行逻辑需要,如调用 killraise 产生信号。

因为信号是异步事件,即信号处理函数执行的上下文背景是不确定的,譬如一个线程在调用某个库函数时可能会被信号中断,库函数提前出错返回,转而去执 行信号处理函数。对于上述第三种信号的产生,信号在产生、处理后,应用程序不会终止,还是会继续正常运行,在编写此类信号处理函数时尤其需要小心,以免破 坏应用程序的正常运行。关于编写安全的信号处理函数主要有以下一些规则:

  • 信号处理函数尽量只执行简单的操作,譬如只是设置一个外部变量,其它复杂的操作留在信号处理函数之外执行;
  • errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;
  • 信号处理函数只能调用可以重入的 C 库函数;譬如不能调用 malloc(),free()以及标准 I/O 库函数等;
  • 信号处理函数如果需要访问全局变量,在定义此全局变量时须将其声明为 volatile,以避免编译器不恰当的优化。

从整个 Linux 应用的角度出发,因为应用中使用了异步信号,程序中一些库函数在调用时可能被异步信号中断,此时必须根据errno 的值考虑这些库函数调用被信号中断后的出错恢复处理,譬如socket 编程中的读操作:

     rlen = recv(sock_fd, buf, len, MSG_WAITALL);
if ((rlen == -1) && (errno == EINTR)){
// this kind of error is recoverable, we can set the offset change
//‘rlen’ as 0 and continue to recv
}

在指定的线程中以同步的方式处理异步信号

如上文所述,不仅编写安全的异步信号处理函数本身有很多的规则束缚;应用中其它地方在调用可被信号中断的库函数时还需考虑被中断后的出错恢复处理。这让程序的编写变得复杂,幸运的是,POSIX.1 规范定义了sigwait()、 sigwaitinfo() pthread_sigmask() 等接口,可以实现:

这种在指定的线程中以同步方式处理信号的模型可以避免因为处理异步信号而给程序运行带来的不确定性和潜在危险。

sigwait

sigwait() 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:

  • 多个相同的信号可不可以在信号队列中排队?
  • 如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?
  • 实时信号和非实时信号在处理时有没有什么区别?

笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。


清单 1. sigwait_test.c
#include signal.h
#include /
errno.h
#include /
pthread.h
#include /
unistd.h
#include /
sys/types.h

void sig_handler(int signum)
{
printf("Receive signal. %d\n", signum);
}

void* sigmgr_thread()
{
sigset_t waitset, oset;
int sig;
int rc;
pthread_t ppid = pthread_self();

pthread_detach(ppid);

sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGRTMIN+2);
sigaddset(&waitset, SIGRTMAX);
sigaddset(&waitset, SIGUSR1);
sigaddset(&waitset, SIGUSR2);

while (1) {
rc = sigwait(&waitset, &sig);
if (rc != -1) {
sig_handler(sig);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}


int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;

sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGRTMIN+2);
sigaddset(&bset, SIGRTMAX);
sigaddset(&bset, SIGUSR1);
sigaddset(&bset, SIGUSR2);

if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");

kill(pid, SIGRTMAX);
kill(pid, SIGRTMAX);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGUSR2);
kill(pid, SIGUSR2);
kill(pid, SIGUSR1);
kill(pid, SIGUSR1);

// Create the dedicated thread sigmgr_thread() which will handle signals synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);

sleep(10);

exit (0);
}

程序编译运行在 RHEL4 的结果如下:


图 1. sigwait 测试程序执行结果
sigwait 测试程序执行结果

从以上测试程序发现以下规则:

  • 对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。
  • 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。

sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。

Linux 多线程应用中的信号处理模型

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 sigwait()使用同步模型进行处理。其程序流程如下:

  1. 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
  2. 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
  3. 主线程创建工作线程。

图 2. 在指定的线程中以同步方式处理异步信号的模型

代码示例

以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。

主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程 worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()

程序编译:gcc -o signal_sync signal_sync.c -lpthread

程序执行:./signal_sync

从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。


清单 2. signal_sync.c
#include signal.h
#include /
errno.h
#include /
pthread.h
#include /
unistd.h
#include /
sys/types.h

void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
// used to show which thread the signal is handled in.

if (signum == SIGUSR1) {
printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
//SIGRTMIN should not be considered constants from userland,
//there is compile error when use switch case
} else if (signum == SIGRTMIN) {
printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}

void* worker_thread()
{
pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {
printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}

void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();

pthread_detach(ppid);

sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);

while (1) {
rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {
printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}


int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;


// Block SIGRTMIN and SIGUSR1 which will be handled in
//dedicated thread sigmgr_thread()
// Newly created threads will inherit the pthread mask from its creator
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");

// Create the dedicated thread sigmgr_thread() which will handle
// SIGUSR1 and SIGRTMIN synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);

// Create 5 worker threads, which will inherit the thread mask of
// the creator main thread
for (i = 0; i < i =" 0;">

注意事项

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:

  • 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
  • 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  • 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
  • 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
  • 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。

小结

在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全; 同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。

come from:http://www.ibm.com/developerworks/cn/linux/l-cn-signalsec/

Monday, July 13, 2009

Linux 线程模型LinuxThreads 和 NPTL的比较

目前手头的工作需要用到linux线程编程,所以就草草的查找了点关于线程方面的资料,针对目前linux下面最主要的两大线程库做一简要的描述,以便将来查阅。
当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个 线程模型也不符合 POSIX 的要求。

LinuxThreads 设计细节

线程 将应用程序划分成一个或多个同时运行的任务。线程与传统的多任务进程 之间的区别在于:线程共享的是单个进程的状态信息,并会直接共享内存和其他资源。同一个进程中线程之间的上下文切换通常要比进程之间的上下文切换速度更 快。因此,多线程程序的优点就是它可以比多进程应用程序的执行速度更快。另外,使用线程我们可以实现并行处理。这些相对于基于进程的方法所具有的优点推动 了 LinuxThreads 的实现。

LinuxThreads 最初的设计相信相关进程之间的上下文切换速度很快,因此每个内核线程足以处理很多相关的用户级线程。这就导致了一对一 线程模型的革命。

让我们来回顾一下 LinuxThreads 设计细节的一些基本理念:

  • LinuxThreads 非常出名的一个特性就是管理线程(manager thread)。管理线程可以满足以下要求:

    • 系统必须能够响应终止信号并杀死整个进程。
    • 以堆栈形式使用的内存回收必须在线程完成之后进行。因此,线程无法自行完成这个过程。
    • 终止线程必须进行等待,这样它们才不会进入僵尸状态。
    • 线程本地数据的回收需要对所有线程进行遍历;这必须由管理线程来进行。
    • 如果主线程需要调用 pthread_exit(),那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在所有线程都被杀死之后来唤醒这个主线程。

  • 为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。

  • 原语的同步是使用信号 来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。

  • 在克隆系统的最初设计之下,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。

  • 终止信号可以杀死所有的线程。LinuxThreads 接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。

  • 根据 LinuxThreads 的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程。如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起。这是因为管理线程无法将这个信号发送给进程;相反,每个线程都是作为一个进程在执行。

  • 线程之间的调度是由内核调度器来处理的。

LinuxThreads 及其局限性

LinuxThreads 的设计通常都可以很好地工作;但是在压力很大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。下面让我们来看一下 LinuxThreads 设计的一些局限性:

  • 它使用管理线程来创建线程,并对每个进程所拥有的所有线程进行协调。这增加了创建和销毁线程所需要的开销。

  • 由于它是围绕一个管理线程来设计的,因此会导致很多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。

  • 由于管理线程只能在一个 CPU 上运行,因此所执行的同步操作在 SMP 或 NUMA 系统上可能会产生可伸缩性的问题。

  • 由于线程的管理方式,以及每个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其他与 POSIX 相关的线程库并不兼容。

  • 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守 POSIX 中处理信号的方法。

  • LinuxThreads 中对信号的处理是按照每线程的原则建立的,而不是按照每进程的原则建立的,这是因为每个线程都有一个独立的进程 ID。由于信号被发送给了一个专用的线程,因此信号是串行化的 —— 也就是说,信号是透过这个线程再传递给其他线程的。这与 POSIX 标准对线程进行并行处理的要求形成了鲜明的对比。例如,在 LinuxThreads 中,通过 kill() 所发送的信号被传递到一些单独的线程,而不是集中整体进行处理。这意味着如果有线程阻塞了这个信号,那么 LinuxThreads 就只能对这个线程进行排队,并在线程开放这个信号时在执行处理,而不是像其他没有阻塞信号的线程中一样立即处理这个信号。

  • 由于 LinuxThreads 中的每个线程都是一个进程,因此用户和组 ID 的信息可能对单个进程中的所有线程来说都不是通用的。例如,一个多线程的 setuid()/setgid() 进程对于不同的线程来说可能都是不同的。

  • 有一些情况下,所创建的多线程核心转储中并没有包含所有的线程信息。同样,这种行为也是每个线程都是一个进程这个事实所导致的结果。如果任何线程 发生了问题,我们在系统的核心文件中只能看到这个线程的信息。不过,这种行为主要适用于早期版本的 LinuxThreads 实现。

  • 由于每个线程都是一个单独的进程,因此 /proc 目录中会充满众多的进程项,而这实际上应该是线程。

  • 由于每个线程都是一个进程,因此对每个应用程序只能创建有限数目的线程。例如,在 IA32 系统上,可用进程总数 —— 也就是可以创建的线程总数 —— 是 4,090。

  • 由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的访问速度都很慢。另外一个缺点是用户无法可信地指定堆栈的大小,因为用户可能会意外地将堆栈地址映射到本来要为其他目的所使用的区域上了。按需增长(grow on demand) 的概念(也称为浮动堆栈 的概念)是在 2.4.10 版本的 Linux 内核中实现的。在此之前,LinuxThreads 使用的是固定堆栈。

关于 NPTL

NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。

Ulrich Drepper 和 Ingo Molnar 是 Red Hat 参与 NPTL 设计的两名员工。他们的总体设计目标如下:

  • 这个新线程库应该兼容 POSIX 标准。

  • 这个线程实现应该在具有很多处理器的系统上也能很好地工作。

  • 为一小段任务创建新线程应该具有很低的启动成本。

  • NPTL 线程库应该与 LinuxThreads 是二进制兼容的。注意,为此我们可以使用 LD_ASSUME_KERNEL,这会在本文稍后进行讨论。

  • 这个新线程库应该可以利用 NUMA 支持的优点。

NPTL 的优点

与 LinuxThreads 相比,NPTL 具有很多优点:

  • NPTL 没有使用管理线程。管理线程的一些需求,例如向作为进程一部分的所有线程发送终止信号,是并不需要的;因为内核本身就可以实现这些功能。内核还会处理每个 线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理,这样可以避免僵尸进程的问题。

  • 由于 NPTL 没有使用管理线程,因此其线程模型在 NUMA 和 SMP 系统上具有更好的可伸缩性和同步机制。

  • 使用 NPTL 线程库与新内核实现,就可以避免使用信号来对线程进行同步了。为了这个目的,NPTL 引入了一种名为 futex 的新机制。futex 在共享内存区域上进行工作,因此可以在进程之间进行共享,这样就可以提供进程间 POSIX 同步机制。我们也可以在进程之间共享一个 futex。这种行为使得进程间同步成为可能。实际上,NPTL 包含了一个 PTHREAD_PROCESS_SHARED 宏,使得开发人员可以让用户级进程在不同进程的线程之间共享互斥锁。

  • 由于 NPTL 是 POSIX 兼容的,因此它对信号的处理是按照每进程的原则进行的;getpid() 会为所有的线程返回相同的进程 ID。例如,如果发送了 SIGSTOP 信号,那么整个进程都会停止;使用 LinuxThreads,只有接收到这个信号的线程才会停止。这样可以在基于 NPTL 的应用程序上更好地利用调试器,例如 GDB。

  • 由于在 NPTL 中所有线程都具有一个父进程,因此对父进程汇报的资源使用情况(例如 CPU 和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。

  • NPTL 线程库所引入的一个实现特性是对 ABI(应用程序二进制接口)的支持。这帮助实现了与 LinuxThreads 的向后兼容性。这个特性是通过使用 LD_ASSUME_KERNEL 实现的,下面就来介绍这个特性。

LD_ASSUME_KERNEL 环境变量

正如上面介绍的一样,ABI 的引入使得可以同时支持 NPTL 和 LinuxThreads 模型。基本上来说,这是通过 ld (一个动态链接器/加载器)来进行处理的,它会决定动态链接到哪个运行时线程库上。

举例来说,下面是 WebSphere® Application Server 对这个变量所使用的一些通用设置;您可以根据自己的需要进行适当的设置:

  • LD_ASSUME_KERNEL=2.4.19:这会覆盖 NPTL 的实现。这种实现通常都表示使用标准的 LinuxThreads 模型,并启用浮动堆栈的特性。
  • LD_ASSUME_KERNEL=2.2.5:这会覆盖 NPTL 的实现。这种实现通常都表示使用 LinuxThreads 模型,同时使用固定堆栈大小。

我们可以使用下面的命令来设置这个变量:

export LD_ASSUME_KERNEL=2.4.19

注意,对于任何 LD_ASSUME_KERNEL 设置的支持都取决于目前所支持的线程库的 ABI 版本。例如,如果线程库并不支持 2.2.5 版本的 ABI,那么用户就不能将 LD_ASSUME_KERNEL 设置为 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 则需要 2.4.1。

如果您正运行的是一个启用了 NPTL 的 Linux 发行版,但是应用程序却是基于 LinuxThreads 模型来设计的,那么所有这些设置通常都可以使用。

GNU_LIBPTHREAD_VERSION 宏

大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:

$ getconf GNU_LIBPTHREAD_VERSION

在我的ubuntu9.04下面的输出结果:

NPTL 2.9

VerilogHDL里面的阻塞与非阻塞赋值

接触过Verilog HDL的都对阻塞与非阻塞赋值略知一二,这也是经常强调的重点之一。在编写可综合代码的时候,建议大家不要忘了打开RTL网表查看器看看自己综合出来的电路是不是想要的逻辑。

1、连续赋值

连续赋值语句的硬件实现是:从赋值语句(=)右边提取出的逻辑,用于驱动赋值语句左边的线网(net)连续赋值语句

module continousassignment(a,b,c);
input a,b;
output c;
assign c=a&b;
endmodule


综合以后,通过网表查看器为上图的结果,线网c由赋值语句的右边的逻辑是组合逻辑a&b简单驱动

2、过程赋值

过程赋值语句的硬件实现是,从赋值语句的(=或<=)右边提取出的逻辑用于驱动赋值语句左边的变量(必需是reg类 型)。必须注意的是虽然过程赋值语句是可以出现在initial语句中(仅用于仿真),也可以出现在“always”块语句中,但是只有“always” 中的过程赋值语句才能被综合。有两种类型的过程赋值语句:阻塞赋值语句(Blocking Assignment statement)、非阻塞赋值语句(non-Blocking Assignment statement)

2.1、阻塞赋值语句

阻塞赋值语句可以简单描述为,在一个always块中,语句按照从上到下的顺序执行

module blockingassignment (clk ,q1,q2);
input clk;
output [2:0] q1,q2;
reg[2:0] q1,q2;
always @ (posedge clk)
begin
q1=q1+3'b1;
q2=q1;
end
endmodule

综合后的RTL视图如上图所示,每个时钟上升沿触发后,变量q2、q1的值是同步的,q2被赋予了q1更新后的值。

2.2、非阻塞赋值语句

非阻塞赋值语句简述为:在一个always 块中,语句是并行执行的

module nonblockingassignment (clk ,q1,q2);
input clk;
output [2:0] q1,q2;
reg[2:0] q1,q2;
always @ (posedge clk)
begin
q1<=q1+3'b1; q2<=q1; end endmodule

从综合结果的RTL查看器可以看到如上图所示,每个时钟触发后,q2被赋予的q1值时上个时钟周期生成的值。从上面可以看出,第一条语句综合出来的结果是一样的,阻塞与非阻塞不同在于它们会影响到后面引用该条语句的逻辑。

3、阻塞与非阻塞建模建模原则

1)、组合逻辑使用阻塞语句、时序逻辑使用非阻塞语句;

2)、在同一个模块里,同一个变量不能既有阻塞赋值,又有非阻塞赋值。

ARM VS X86 的时代

自从订阅了以“arm+linux”为关键字的google新闻简报以来,这几年每天都可以收到几个相关的报道,我也时刻关心着技术的发展方向,毕竟身在江湖嘛!
嵌入式处理器从十年前的群雄并起到现在ARM和Intel两强争雄,波澜壮阔,演绎了一出纷繁芜杂而又让人感慨万千的大戏!
自从英特尔携凌动杀入嵌入式市场,就与在该市场获得广泛应用的ARM及相关DSP平台展开激烈的市场争夺战。
两大平台相互学习,似乎将殊途同归,应用的交叉会更多。不同的人会选择不同的方案。将来,可能是用英特尔的平台也可以,用ARM的平台也可以。但有一点很重要,硬件只不过是一个躯壳,软件才是产生价值的灵魂。项目会不会有竞争力,并不取决于你采用的是英特尔还是ARM,我认为取决于Design,即软件的价值,还有商业模式、生态环境和创新的功能。在这方面,苹果的成功就是一个例子。我们今天都是站在巨人的肩膀上前行,一个idea出来,一个创新出来了,附加值产生了,这个附加值很可能就是软件带来的。

Thursday, July 9, 2009

团队-TEAM

这个世界上没有完美的人,但是有些人热爱追求完美的事物,所以他门走到了一起。他们用他人的长处来弥补自己的短处,这个队伍称之为团队。

Labels

Followers