<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Solaris</title>
	<atom:link href="http://tonybai.com/tag/solaris/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 20 Apr 2026 23:16:50 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go语言对ARM架构的支持与未来[译]</title>
		<link>https://tonybai.com/2020/12/18/go-ports-until-202012/</link>
		<comments>https://tonybai.com/2020/12/18/go-ports-until-202012/#comments</comments>
		<pubDate>Fri, 18 Dec 2020 07:55:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[arm64]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-build]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[M1]]></category>
		<category><![CDATA[NetBSD]]></category>
		<category><![CDATA[OpenBSD]]></category>
		<category><![CDATA[plan9]]></category>
		<category><![CDATA[RISC-V]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[x86]]></category>
		<category><![CDATA[x86-64]]></category>
		<category><![CDATA[交叉编译]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[移植性]]></category>
		<category><![CDATA[苹果]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3041</guid>
		<description><![CDATA[本文翻译自Go官方博客文章《Go on ARM and Beyond》(https://blog.golang.org/ports)。 最近业界关于非x86处理器的讨论沸沸扬扬，所以我们认为值得简单的写一篇关于Go语言对这些非x86处理器的支持情况的文章。 对我们来说，Go的可移植性一直很重要，我们不会过度去适配任何特定的操作系统或架构。Go最初的开源版本包括对两种操作系统（Linux和MacOSX）和三种架构（64位x86、32位x86和32位ARM）的支持。 多年来，我们已经增加了对更多操作系统和架构组合的支持： Go1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。 Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。 Go 1.4（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。 Go 1.5（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。 Go 1.6（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。 Go 1.7（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。 Go 1.8（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。 Go 1.9（2017年8月）增加了对64位ARM上Linux的官方二进制下载。 Go 1.12（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。 Go 1.14（2019年2月）增加了对64位RISC-V上Linux的支持。 虽然x86-64的移植在Go的早期得到了大部分的关注，但今天我们所有的目标架构都得到了我们基于SSA的编译器后端的良好支持，并生成了优秀的代码。我们一路走来得到了许多贡献者的帮助，包括来自Amazon、ARM、Atos、IBM、Intel和MIPS的工程师。 Go支持对所有这些系统进行开箱即用的交叉编译，而且只需付出最小的努力。例如，要在一个64位Linux系统中构建一个基于32位x86的Windows应用，我们只需执行下面命令： GOARCH=386 GOOS=windows go build myapp # 编译生成myapp.exe 在过去的一年里，几家主要的厂商都宣布了用于服务器、笔记本电脑和开发者机器的新ARM64硬件。Go在这些方面适配的很好。多年来，Go一直在ARM64 Linux服务器上为Docker、Kubernetes和Go生态系统的其他部分，以及ARM64 Android和iOS设备上的移动应用提供支持。 自今年夏天苹果宣布Mac过渡到苹果芯片以来，苹果和谷歌一直在合作，以确保Go和更广泛的Go生态系统在其上运行良好，无论是在Rosetta 2下运行Go x86二进制文件，还是运行原生Go ARM64二进制文件。本周早些时候，我们发布了第一个Go 1.16测试版，其中包括了对使用M1芯片的Mac的原生支持。您可以在Go下载页面上下载并试用适用于M1 Mac和所有其他系统的Go 1.16测试版。当然，这是一个测试版，就像所有的测试版一样，它肯定有我们不知道的bug。如果你遇到任何问题，请在golang.org/issue/new上报告）。 在本地开发中使用与生产中相同的CPU架构总是很好的，这样可以消除两种环境之间的差异。如果你部署到ARM64生产服务器上，Go也可以轻松在ARM64 Linux和Mac系统上进行开发。但当然，无论你是在x86系统上工作并部署到ARM上，还是在Windows上工作并部署到Linux上，或者其他组合，在一个系统上工作并交叉编译部署到另一个系统上仍然和以前一样容易。 我们希望添加支持的下一个目标是ARM64 Windows 10系统。如果你有专业知识并愿意提供帮助，我们正在golang.org/issue/36439上协调工作。 “Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！ 我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！ [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自Go官方博客文章<a href="https://blog.golang.org/ports">《Go on ARM and Beyond》</a>(https://blog.golang.org/ports)。</p>
<p>最近业界关于非x86处理器的讨论沸沸扬扬，所以我们认为值得简单的写一篇关于Go语言对这些非x86处理器的支持情况的文章。</p>
<p>对我们来说，<a href="https://tonybai.com/2017/06/27/an-intro-about-go-portability/">Go的可移植性</a>一直很重要，我们不会过度去适配任何特定的操作系统或架构。<a href="https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html">Go最初的开源版本</a>包括对两种操作系统（Linux和MacOSX）和三种架构（64位x86、32位x86和32位ARM）的支持。</p>
<p>多年来，我们已经增加了对更多操作系统和架构组合的支持：</p>
<ul>
<li>Go1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。</li>
<li>Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。</li>
<li><a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。</li>
<li><a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。</li>
<li><a href="https://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6</a>（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。</li>
<li><a href="https://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7</a>（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。</li>
<li><a href="https://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8</a>（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。</li>
<li><a href="https://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9</a>（2017年8月）增加了对64位ARM上Linux的官方二进制下载。</li>
<li><a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12">Go 1.12</a>（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。</li>
<li><a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14">Go 1.14</a>（2019年2月）增加了对64位RISC-V上Linux的支持。</li>
</ul>
<p>虽然x86-64的移植在Go的早期得到了大部分的关注，但今天我们所有的目标架构都得到了我们<a href="https://www.youtube.com/watch?v=uTMvKVma5ms">基于SSA的编译器后端</a>的良好支持，并生成了优秀的代码。我们一路走来得到了许多贡献者的帮助，包括来自Amazon、ARM、Atos、IBM、Intel和MIPS的工程师。</p>
<p>Go支持对所有这些系统进行开箱即用的<a href="https://tonybai.com/2014/10/20/cross-compilation-with-golang/">交叉编译</a>，而且只需付出最小的努力。例如，要在一个64位Linux系统中构建一个基于32位x86的Windows应用，我们只需执行下面命令：</p>
<pre><code>GOARCH=386 GOOS=windows go build myapp  # 编译生成myapp.exe
</code></pre>
<p>在过去的一年里，几家主要的厂商都宣布了用于服务器、笔记本电脑和开发者机器的新ARM64硬件。Go在这些方面适配的很好。多年来，Go一直在ARM64 Linux服务器上为Docker、Kubernetes和Go生态系统的其他部分，以及ARM64 Android和iOS设备上的移动应用提供支持。</p>
<p>自今年夏天苹果宣布Mac过渡到苹果芯片以来，苹果和谷歌一直在合作，以确保Go和更广泛的Go生态系统在其上运行良好，无论是在Rosetta 2下运行Go x86二进制文件，还是运行原生Go ARM64二进制文件。本周早些时候，我们发布了第一个Go 1.16测试版，其中包括了对使用M1芯片的Mac的原生支持。您可以在<a href="https://golang.google.cn/dl/#go1.16beta1">Go下载页面</a>上下载并试用适用于M1 Mac和所有其他系统的Go 1.16测试版。当然，这是一个测试版，就像所有的测试版一样，它肯定有我们不知道的bug。如果你遇到任何问题，请在golang.org/issue/new上报告）。</p>
<p>在本地开发中使用与生产中相同的CPU架构总是很好的，这样可以消除两种环境之间的差异。如果你部署到ARM64生产服务器上，Go也可以轻松在ARM64 Linux和Mac系统上进行开发。但当然，无论你是在x86系统上工作并部署到ARM上，还是在Windows上工作并部署到Linux上，或者其他组合，在一个系统上工作并交叉编译部署到另一个系统上仍然和以前一样容易。</p>
<p>我们希望添加支持的下一个目标是ARM64 Windows 10系统。如果你有专业知识并愿意提供帮助，我们正在golang.org/issue/36439上协调工作。</p>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家https://tonybai.com/<br />
smspush:可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展；短信内容你来定，不再受约束,接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5GRCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1coreCPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687开启你的DO主机之路。</p>
<p>GopherDaily(Gopher每日新闻)归档仓库-https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github:https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/12/18/go-ports-until-202012/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>追求极简：Docker镜像构建演化史</title>
		<link>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/</link>
		<comments>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/#comments</comments>
		<pubDate>Wed, 20 Dec 2017 23:31:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[busybox]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[CSDN]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[dotCloud]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[namespaces]]></category>
		<category><![CDATA[scratch]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[unionfs]]></category>
		<category><![CDATA[多阶段构建]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[程序员杂志]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2495</guid>
		<description><![CDATA[本文首发于CSDN《程序员》杂志2017.12期，这里是原文地址。 本文为《程序员》杂志授权转载，谢绝其他转载。全文如下： 自从2013年dotCloud公司(现已改名为Docker Inc)发布Docker容器技术以来，到目前为止已经有四年多的时间了。这期间Docker技术飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。 对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。 一、镜像：继承中的创新 谈镜像构建之前，我们先来简要说下镜像。 Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Sun发布了Solaris Container技术，从此开启了内核容器之门。 2008年，以Google公司开发人员为主导实现的Linux Container(即LXC)功能在被merge到Linux内核中。LXC是一种内核级虚拟化技术，主要基于Namespaces和Cgroups技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的创新之处在于其基于Union File System技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为镜像（即image），原理见下图（引自Docker官网）： 图1：Docker镜像原理 镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。 与Solaris Container、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。 二、“镜像是个筐”：初学者的认知 “镜像是个筐，什么都往里面装” &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码： //httpserver.go package main import ( "fmt" "net/http" ) func main() { fmt.Println("http daemon start") fmt.Println(" -&#62; listen on port:8080") http.ListenAndServe(":8080", nil) } 接下来，我们来编写一个用于构建目标image的Dockerfile： From ubuntu:14.04 RUN [...]]]></description>
			<content:encoded><![CDATA[<p>本文首发于<a href="https://www.csdn.net/">CSDN</a><a href="http://programmer.csdn.net/">《程序员》</a>杂志<a href="http://blog.csdn.net/qq_40027052/article/details/78720370">2017.12期</a>，这里是<a href="https://mp.weixin.qq.com/s/6--iyRTiAtpSpsLd0Tgf8w">原文地址</a>。</p>
<p>本文为《程序员》杂志授权转载，谢绝其他转载。全文如下：</p>
<p>自从2013年<a href="https://en.wikipedia.org/wiki/DotCloud">dotCloud公司</a>(现已改名为<a href="https://en.wikipedia.org/wiki/Docker,_Inc.">Docker Inc</a>)发布<a href="http://tonybai.com/tag/docker">Docker容器技术</a>以来，到目前为止已经有四年多的时间了。这期间<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。</p>
<p>对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。</p>
<h3>一、镜像：继承中的创新</h3>
<p>谈镜像构建之前，我们先来简要说下<strong>镜像</strong>。</p>
<p>Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在<a href="https://en.wikipedia.org/wiki/Sun_Microsystems">Sun公司</a>的<a href="https://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>上，<a href="http://tonybai.com/tag/solaris">Solaris</a>是当时最先进的服务器操作系统。2005年Sun发布了<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>技术，从此开启了内核容器之门。</p>
<p>2008年，以Google公司开发人员为主导实现的Linux Container(即<a href="https://en.wikipedia.org/wiki/LXC">LXC</a>)功能在被merge到<a href="https://www.kernel.org/">Linux内核</a>中。LXC是一种内核级虚拟化技术，主要基于<a href="https://en.wikipedia.org/wiki/Cgroups#NAMESPACE-ISOLATION">Namespaces</a>和<a href="https://en.wikipedia.org/wiki/Cgroups">Cgroups</a>技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的<strong>创新之处</strong>在于其基于<a href="https://en.wikipedia.org/wiki/UnionFS">Union File System</a>技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为<strong>镜像</strong>（即image），原理见下图（引自Docker官网）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-layers-and-container.png" alt="img{512x368}" /><br />
图1：Docker镜像原理</p>
<p>镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。</p>
<p>与<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。</p>
<h3>二、“镜像是个筐”：初学者的认知</h3>
<p><strong>“镜像是个筐，什么都往里面装”</strong> &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码：</p>
<pre><code>//httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -&gt; listen on port:8080")
        http.ListenAndServe(":8080", nil)
}

</code></pre>
<p>接下来，我们来编写一个用于构建目标image的Dockerfile：</p>
<pre><code>From ubuntu:14.04

RUN apt-get update \
      &amp;&amp; apt-get install -y software-properties-common \
      &amp;&amp; add-apt-repository ppa:gophers/archive \
      &amp;&amp; apt-get update \
      &amp;&amp; apt-get install -y golang-1.9-go \
                            git \
      &amp;&amp; rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      &amp;&amp; chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>构建这个Image：</p>
<pre><code># docker build -t repodemo/httpd:latest .
//...构建输出这里省略...

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB
</code></pre>
<p>整个镜像的构建过程因环境而定。如果您的网络速度一般，这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿，基于repodemo/httpd:latest这个镜像的容器可以正常运行：</p>
<pre><code># docker run repodemo/httpd
http daemon start
  -&gt; listen on port:8080

</code></pre>
<p>一个Dockerfile最终生产出一个镜像。Dockerfile由若干Command组成，每个Command执行结果都会单独形成一个layer。我们来探索一下构建出来的镜像：</p>
<pre><code># docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB
... ...
aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       &amp;&amp; apt-get...   356MB
.... ...
&lt;missing&gt;           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

</code></pre>
<p>我们去除掉那些Size为0或很小的layer，我们看到三个size占比较大的layer，见下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-2.png" alt="img{512x368}" /><br />
图2：Docker镜像分层探索</p>
<p>虽然Docker引擎利用r缓存机制可以让同主机下非首次的镜像构建执行得很快，但是在Docker技术热情催化下的这种构建思路让docker镜像在存储和传输方面的优势荡然无存，要知道一个ubuntu-server 16.04的虚拟机ISO文件的大小也就不过600多MB而已。</p>
<h3>三、”理性的回归”：builder模式的崛起</h3>
<p>Docker使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示，我们发现最终镜像中包含构建环境是多余的，我们只需要在最终镜像中包含足够支撑httpd运行的运行环境即可，而base image自身就可以满足。于是我们应该去除不必要的中间层：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-1.png" alt="img{512x368}" /><br />
图3：去除不必要的分层</p>
<p>现在问题来了！如果不在同一镜像中完成应用构建，那么在哪里、由谁来构建应用呢？至少有两种方法：</p>
<ol>
<li>在本地构建并COPY到镜像中；</li>
<li>借助构建者镜像(builder image)构建。</li>
</ol>
<p>不过方法1本地构建有很多局限性，比如：本地环境无法复用、无法很好融入持续集成/持续交付流水线等。借助builder image进行构建已经成为Docker社区的一个最佳实践，Docker官方为此也推出了各种主流编程语言的官方base image，比如：<a href="http://tonybai.com/tag/go">go</a>、<a href="http://tonybai.com/tag/java">java</a>、node、<a href="http://tonybai.com/tag/python">python</a>以及<a href="http://tonybai.com/tag/ruby">ruby</a>等。借助builder image进行镜像构建的流程原理如下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-2.png" alt="img{512x368}" /><br />
图4：借助builder image进行镜像构建的流程图</p>
<p>通过原理图，我们可以看到整个目标镜像的构建被分为了两个阶段：</p>
<ol>
<li>第一阶段：构建负责编译源码的构建者镜像；</li>
<li>第二阶段：将第一阶段的输出作为输入，构建出最终的目标镜像。</li>
</ol>
<p>我们选择golang:1.9.2作为builder base image，构建者镜像的Dockerfile.build如下：</p>
<pre><code>// Dockerfile.build

FROM golang:1.9.2

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go
</code></pre>
<p>执行构建：</p>
<pre><code># docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .
</code></pre>
<p>构建好的应用程序httpd放在了镜像repodemo/httpd-builder中的/go/src目录下，我们需要一些“胶水”命令来连接两个构建阶段，这些命令将httpd从<strong>构建者镜像</strong>中取出并作为下一阶段构建的输入：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-builder
</code></pre>
<p>通过上面的命令，我们将编译好的httpd程序拷贝到了本地。下面是目标镜像的Dockerfile：</p>
<pre><code>//Dockerfile.target
From ubuntu:14.04

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>接下来我们来构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd:latest -f Dockerfile.target .
</code></pre>
<p>我们来看看这个镜像的“体格”：</p>
<pre><code># docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB
</code></pre>
<p>200MB！目标镜像的Size降为原来的 1/2 还多。</p>
<h3>四、“像赛车那样减去所有不必要的东西”：追求最小镜像</h3>
<p>前面我们构建出的镜像的Size已经缩小到200MB，但这还不够。200MB的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重，减到尽可能的小，就像赛车那样，为了能减轻重量将所有不必要的东西都拆除掉：我们仅保留能支撑我们的应用运行的必要库、命令，其余的一律不纳入目标镜像。当然不仅仅是Size上的原因，小镜像还有额外的好处，比如：内存占用小，启动速度快，更加高效；不会因其他不必要的工具、库的漏洞而被攻击，减少了“攻击面”，更加安全。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-1.png" alt="img{512x368}" /><br />
图5：目标镜像还能更小些吗？</p>
<p>一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的，开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-size.png" alt="img{512x368}" /><br />
图6：一些base image的Size比较(来自imagelayers.io截图)</p>
<p>从图中看，我们有两个选择：<a href="https://www.busybox.net/">busybox</a>和<a href="https://alpinelinux.org/">alpine</a>。</p>
<p>单从image的size上来说，busybox更小。不过busybox默认的libc实现是uClibc，而我们通常运行环境使用的libc实现都是glibc，因此我们要么选择静态编译程序，要么使用busybox:glibc镜像作为base image。</p>
<p>而 alpine image 是另外一种蝇量级 base image，它使用了比 glibc 更小更安全的 <a href="http://www.musl-libc.org/">musl libc</a> 库。 不过和 busybox image 相比，alpine image 体积还是略大。除了因为 musl比uClibc 大一些之外，alpine还在镜像中添加了自己的包管理系统apk，开发者可以使用apk在基于alpine的镜像中添 加需要的包或工具。因此，对于普通开发者而言，alpine image显然是更佳的选择。不过alpine使用的libc实现为<a href="http://www.musl-libc.org/">musl</a>，与基于glibc上编译出来的应用程序不兼容。如果直接将前面构建出的httpd应用塞入alpine，在容器启动时会遇到下面错误，因为加载器找不到glibc这个动态共享库文件：</p>
<pre><code>standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>对于Go应用来说，我们可以采用静态编译的程序，但一旦采用静态编译，也就意味着我们将失去一些libc提供的原生能力，比如：在linux上，你无法使用系统提供的DNS解析能力，只能使用Go自实现的DNS解析器。</p>
<p>我们还可以采用基于alpine的builder image，golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base image的极小目标镜像。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-2.png" alt="img{512x368}" /><br />
图7：借助 alpine builder image 进行镜像构建的流程图</p>
<p>我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile：Dockerfile.build.alpine 和Dockerfile.target.alpine：</p>
<pre><code>//Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// Dockerfile.target.alpine
From alpine

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

</code></pre>
<p>构建builder镜像：</p>
<pre><code>#  docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB
</code></pre>
<p>执行“胶水”命令：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder
</code></pre>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB
</code></pre>
<p>16.2MB！目标镜像的Size降为不到原来的十分之一。我们得到了预期的结果。</p>
<h3>五、“要有光，于是便有了光”：对多阶段构建的支持</h3>
<p>至此，虽然我们实现了目标Image的最小化，但是整个构建过程却是十分繁琐，我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。作为Docker用户，我们希望用一个Dockerfile就能解决所有问题，于是就有了Docker引擎对多阶段构建(multi-stage build)的支持。注意：这个特性非常新，只有Docker 17.05.0-ce及以后的版本才能支持。</p>
<p>现在我们就按照“多阶段构建”的语法将上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一个Dockerfile中：</p>
<pre><code>//Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd

ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>Dockerfile的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了，每个From语句开启一个构建阶段，并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据，比如这里传递的httpd应用，这个工作之前我们是使用“胶水”代码完成的。</p>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-multi-stage .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB
</code></pre>
<p>我们看到通过多阶段构建特性构建的Docker Image与我们之前通过builder模式构建的镜像在效果上是等价的。</p>
<h3>六、来到现实</h3>
<p>沿着时间的轨迹，Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区 的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器，从此构建 出极简的镜像将不再困难。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go语言TCP Socket编程</title>
		<link>https://tonybai.com/2015/11/17/tcp-programming-in-golang/</link>
		<comments>https://tonybai.com/2015/11/17/tcp-programming-in-golang/#comments</comments>
		<pubDate>Tue, 17 Nov 2015 09:27:11 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Block]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[epoll]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[IO]]></category>
		<category><![CDATA[kqueue]]></category>
		<category><![CDATA[libev]]></category>
		<category><![CDATA[Libevent]]></category>
		<category><![CDATA[libuv]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[lock]]></category>
		<category><![CDATA[MacOSX]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[netpoller]]></category>
		<category><![CDATA[Non-Block]]></category>
		<category><![CDATA[Poll]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Scheduler]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[Server]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[多路复用]]></category>
		<category><![CDATA[套接字]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[用友网络]]></category>
		<category><![CDATA[网络编程]]></category>
		<category><![CDATA[调度器]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[锁]]></category>
		<category><![CDATA[阻塞]]></category>
		<category><![CDATA[非阻塞]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1907</guid>
		<description><![CDATA[Golang的主要 设计目标之一就是面向大规模后端服务程序，网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中，我们也可以看到Go中的net以及其subdirectories下的包均是“高频+刚需”，而TCP socket则是网络编程的主流，即便您没有直接使用到net中有关TCP Socket方面的接口，但net/http总是用到了吧，http底层依旧是用tcp socket实现的。 网络编程方面，我们最常用的就是tcp socket编程了，在posix标准出来后，socket在各大主流OS平台上都得到了很好的支持。关于tcp programming，最好的资料莫过于W. Richard Stevens 的网络编程圣经《UNIX网络 编程 卷1：套接字联网API》 了，书中关于tcp socket接口的各种使用、行为模式、异常处理讲解的十分细致。Go是自带runtime的跨平台编程语言，Go中暴露给语言使用者的tcp socket api是建立OS原生tcp socket接口之上的。由于Go runtime调度的需要，golang tcp socket接口在行为特点与异常处理方面与OS原生接口有着一些差别。这篇博文的目标就是整理出关于Go tcp socket在各个场景下的使用方法、行为特点以及注意事项。 一、模型 从tcp socket诞生后，网络编程架构模型也几经演化，大致是：“每进程一个连接” &#8211;> “每线程一个连接” &#8211;> “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化，服务程序愈加强大，可以支持更多的连接，获得更好的处理性能。 目前主流web server一般均采用的都是”Non-Block + I/O多路复用”（有的也结合了多线程、多进程）。不过I/O多路复用也给使用者带来了不小的复杂度，以至于后续出现了许多高性能的I/O多路复用框架， 比如libevent、libev、libuv等，以帮助开发者简化开发复杂性，降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂，且有悖于“一般逻辑”设计，为此Go语言将该“复杂性”隐藏在Runtime中了：Go开发者无需关注socket是否是 non-block的，也无需亲自注册文件描述符的回调，只需在每个连接对应的goroutine中以“block I/O”的方式对待socket处理即可，这可以说大大降低了开发人员的心智负担。一个典型的Go server端程序大致如下： //go-tcpsock/server.go func handleConn(c net.Conn) { defer c.Close() [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/tag/go">Golang</a>的主要 设计目标之一就是面向大规模后端服务程序，网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中，我们也可以看到Go中的net以及其subdirectories下的包均是“高频+刚需”，而TCP socket则是网络编程的主流，即便您没有直接使用到net中有关TCP Socket方面的接口，但net/http总是用到了吧，http底层依旧是用tcp socket实现的。</p>
<p>网络编程方面，我们最常用的就是tcp socket编程了，在posix标准出来后，socket在各大主流OS平台上都得到了很好的支持。关于tcp programming，最好的资料莫过于<a href="http://en.wikipedia.org/wiki/W._Richard_Stevens">W. Richard Stevens</a> 的网络编程圣经《<a href="http://book.douban.com/subject/4859464/">UNIX网络 编程 卷1：套接字联网API</a>》 了，书中关于tcp socket接口的各种使用、行为模式、异常处理讲解的十分细致。Go是自带runtime的跨平台编程语言，Go中暴露给语言使用者的tcp socket api是建立OS原生tcp socket接口之上的。由于Go runtime调度的需要，golang tcp socket接口在行为特点与异常处理方面与OS原生接口有着一些差别。这篇博文的目标就是整理出关于Go tcp socket在各个场景下的使用方法、行为特点以及注意事项。</p>
<h3>一、模型</h3>
<p>从tcp socket诞生后，网络编程架构模型也几经演化，大致是：“每进程一个连接”  &#8211;>  “每线程一个连接”  &#8211;>  “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化，服务程序愈加强大，可以支持更多的连接，获得更好的处理性能。</p>
<p>目前主流web server一般均采用的都是”Non-Block + I/O多路复用”（有的也结合了多线程、多进程）。不过I/O多路复用也给使用者带来了不小的复杂度，以至于后续出现了许多高性能的I/O多路复用框架， 比如<a href="http://libevent.org/">libevent</a>、<a href="http://software.schmorp.de/pkg/libev.html">libev</a>、<a href="https://github.com/joyent/libuv">libuv</a>等，以帮助开发者简化开发复杂性，降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂，且有悖于“一般逻辑”设计，为此Go语言将该“复杂性”隐藏在Runtime中了：Go开发者无需关注socket是否是 non-block的，也无需亲自注册文件描述符的回调，只需在每个连接对应的goroutine中以<strong>“block I/O”</strong>的方式对待socket处理即可，这可以说大大降低了开发人员的心智负担。一个典型的Go server端程序大致如下：</p>
<pre><code>//go-tcpsock/server.go
func handleConn(c net.Conn) {
    defer c.Close()
    for {
        // read from the connection
        // ... ...
        // write to the connection
        //... ...
    }
}

func main() {
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        c, err := l.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            break
        }
        // start a new goroutine to handle
        // the new connection.
        go handleConn(c)
    }
}
</code></pre>
<p>用户层眼中看到的goroutine中的“block socket”，实际上是通过Go runtime中的netpoller通过Non-block socket + I/O多路复用机制“模拟”出来的，真实的underlying socket实际上是non-block的，只是runtime拦截了底层socket系统调用的错误码，并通过netpoller和goroutine 调度让goroutine“阻塞”在用户层得到的Socket fd上。比如：当用户层针对某个socket fd发起read操作时，如果该socket fd中尚无数据，那么runtime会将该socket fd加入到netpoller中监听，同时对应的goroutine被挂起，直到runtime收到socket fd 数据ready的通知，runtime才会重新唤醒等待在该socket fd上准备read的那个Goroutine。而这个过程从Goroutine的视角来看，就像是read操作一直block在那个socket fd上似的。具体实现细节在后续场景中会有补充描述。</p>
<h3>二、TCP连接的建立</h3>
<p>众所周知，TCP Socket的连接的建立需要经历客户端和服务端的三次握手的过程。连接建立过程中，服务端是一个标准的Listen + Accept的结构(可参考上面的代码)，而在客户端Go语言使用net.Dial或DialTimeout进行连接建立：</p>
<p>阻塞Dial：</p>
<pre><code>conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
    //handle error
}
// read or write on conn
</code></pre>
<p>或是带上超时机制的Dial：</p>
<pre><code>conn, err := net.DialTimeout("tcp", ":8080", 2 * time.Second)
if err != nil {
    //handle error
}
// read or write on conn

</code></pre>
<p>对于客户端而言，连接的建立会遇到如下几种情形：</p>
<hr />
<h4>1、网络不可达或对方服务未启动</h4>
<p>如果传给Dial的Addr是可以立即判断出网络不可达，或者Addr中端口对应的服务没有启动，端口未被监听，Dial会几乎立即返回错误，比如：</p>
<pre><code>//go-tcpsock/conn_establish/client1.go
... ...
func main() {
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")
}
</code></pre>
<p>如果本机8888端口未有服务程序监听，那么执行上面程序，Dial会很快返回错误：</p>
<pre><code>$go run client1.go
2015/11/16 14:37:41 begin dial...
2015/11/16 14:37:41 dial error: dial tcp :8888: getsockopt: connection refused
</code></pre>
<h4>2、对方服务的listen backlog满</h4>
<p>还有一种场景就是对方服务器很忙，瞬间有大量client端连接尝试向server建立，server端的listen backlog队列满，server accept不及时((即便不accept，那么在backlog数量范畴里面，connect都会是成功的，因为new conn已经加入到server side的listen queue中了，accept只是从queue中取出一个conn而已)，这将导致client端Dial阻塞。我们还是通过例子感受Dial的行为特点：</p>
<p>服务端代码：</p>
<pre><code>//go-tcpsock/conn_establish/server2.go
... ...
func main() {
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Println("error listen:", err)
        return
    }
    defer l.Close()
    log.Println("listen ok")

    var i int
    for {
        time.Sleep(time.Second * 10)
        if _, err := l.Accept(); err != nil {
            log.Println("accept error:", err)
            break
        }
        i++
        log.Printf("%d: accept a new connection\n", i)
    }
}
</code></pre>
<p>客户端代码：</p>
<pre><code>//go-tcpsock/conn_establish/client2.go
... ...
func establishConn(i int) net.Conn {
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Printf("%d: dial error: %s", i, err)
        return nil
    }
    log.Println(i, ":connect to server ok")
    return conn
}

func main() {
    var sl []net.Conn
    for i := 1; i &lt; 1000; i++ {
        conn := establishConn(i)
        if conn != nil {
            sl = append(sl, conn)
        }
    }

    time.Sleep(time.Second * 10000)
}
</code></pre>
<p>从程序可以看出，服务端在listen成功后，每隔10s钟accept一次。客户端则是串行的尝试建立连接。这两个程序在Darwin下的执行 结果：</p>
<pre><code>$go run server2.go
2015/11/16 21:55:41 listen ok
2015/11/16 21:55:51 1: accept a new connection
2015/11/16 21:56:01 2: accept a new connection
... ...

$go run client2.go
2015/11/16 21:55:44 1 :connect to server ok
2015/11/16 21:55:44 2 :connect to server ok
2015/11/16 21:55:44 3 :connect to server ok
... ...

2015/11/16 21:55:44 126 :connect to server ok
2015/11/16 21:55:44 127 :connect to server ok
2015/11/16 21:55:44 128 :connect to server ok

2015/11/16 21:55:52 129 :connect to server ok
2015/11/16 21:56:03 130 :connect to server ok
2015/11/16 21:56:14 131 :connect to server ok
... ...
</code></pre>
<p>可以看出Client初始时成功地一次性建立了128个连接，然后后续每阻塞近10s才能成功建立一条连接。也就是说在server端 backlog满时(未及时accept)，客户端将阻塞在Dial上，直到server端进行一次accept。至于为什么是128，这与darwin 下的默认设置有关：</p>
<pre><code>$sysctl -a|grep kern.ipc.somaxconn
kern.ipc.somaxconn: 128
</code></pre>
<p>如果我在ubuntu 14.04上运行上述server程序，我们的client端初始可以成功建立499条连接。</p>
<p>如果server一直不accept，client端会一直阻塞么？我们去掉accept后的结果是：在Darwin下，client端会阻塞大 约1分多钟才会返回timeout：</p>
<pre><code>2015/11/16 22:03:31 128 :connect to server ok
2015/11/16 22:04:48 129: dial error: dial tcp :8888: getsockopt: operation timed out
</code></pre>
<p>而如果server运行在ubuntu 14.04上，client似乎一直阻塞，我等了10多分钟依旧没有返回。 阻塞与否看来与server端的网络实现和设置有关。</p>
<h4>3、网络延迟较大，Dial阻塞并超时</h4>
<p>如果网络延迟较大，TCP握手过程将更加艰难坎坷（各种丢包），时间消耗的自然也会更长。Dial这时会阻塞，如果长时间依旧无法建立连接，则Dial也会返回“ getsockopt: operation timed out”错误。</p>
<hr />
<p>在连接建立阶段，多数情况下，Dial是可以满足需求的，即便阻塞一小会儿。但对于某些程序而言，需要有严格的连接时间限定，如果一定时间内没能成功建立连接，程序可能会需要执行一段“异常”处理逻辑，为此我们就需要DialTimeout了。下面的例子将Dial的最长阻塞时间限制在2s内，超出这个时长，Dial将返回timeout error：</p>
<pre><code>//go-tcpsock/conn_establish/client3.go
... ...
func main() {
    log.Println("begin dial...")
    conn, err := net.DialTimeout("tcp", "104.236.176.96:80", 2*time.Second)
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")
}

</code></pre>
<p>执行结果如下（需要模拟一个延迟较大的网络环境）：</p>
<pre><code>$go run client3.go
2015/11/17 09:28:34 begin dial...
2015/11/17 09:28:36 dial error: dial tcp 104.236.176.96:80: i/o timeout
</code></pre>
<h3>三、Socket读写</h3>
<p>连接建立起来后，我们就要在conn上进行读写，以完成业务逻辑。前面说过Go runtime隐藏了I/O多路复用的复杂性。语言使用者只需采用goroutine+Block I/O的模式即可满足大部分场景需求。Dial成功后，方法返回一个net.Conn接口类型变量值，这个接口变量的动态类型为一个*TCPConn：</p>
<pre><code>//$GOROOT/src/net/tcpsock_posix.go
type TCPConn struct {
    conn
}
</code></pre>
<p>TCPConn内嵌了一个unexported类型：conn，因此TCPConn”继承”了conn的Read和Write方法，后续通过Dial返回值调用的Write和Read方法均是net.conn的方法：</p>
<pre><code>//$GOROOT/src/net/net.go
type conn struct {
    fd *netFD
}

func (c *conn) ok() bool { return c != nil &amp;&amp; c.fd != nil }

// Implementation of the Conn interface.

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
    if !c.ok() {
        return 0, syscall.EINVAL
    }
    n, err := c.fd.Read(b)
    if err != nil &amp;&amp; err != io.EOF {
        err = &amp;OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

// Write implements the Conn Write method.
func (c *conn) Write(b []byte) (int, error) {
    if !c.ok() {
        return 0, syscall.EINVAL
    }
    n, err := c.fd.Write(b)
    if err != nil {
        err = &amp;OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}
</code></pre>
<p>下面我们先来通过几个场景来总结一下conn.Read的行为特点。</p>
<hr />
<h4>1、Socket中无数据</h4>
<p>连接建立后，如果对方未发送数据到socket，接收方(Server)会阻塞在Read操作上，这和前面提到的“模型”原理是一致的。执行该Read操作的goroutine也会被挂起。runtime会监视该socket，直到其有数据才会重新<br />
调度该socket对应的Goroutine完成read。由于篇幅原因，这里就不列代码了，例子对应的代码文件：go-tcpsock/read_write下的client1.go和server1.go。</p>
<h4>2、Socket中有部分数据</h4>
<p>如果socket中有部分数据，且长度小于一次Read操作所期望读出的数据长度，那么Read将会成功读出这部分数据并返回，而不是等待所有期望数据全部读取后再返回。</p>
<p>Client端：</p>
<pre><code>//go-tcpsock/read_write/client2.go
... ...
func main() {
    if len(os.Args) &lt;= 1 {
        fmt.Println("usage: go run client2.go YOUR_CONTENT")
        return
    }
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")

    time.Sleep(time.Second * 2)
    data := os.Args[1]
    conn.Write([]byte(data))

    time.Sleep(time.Second * 10000)
}

</code></pre>
<p>Server端：</p>
<pre><code>//go-tcpsock/read_write/server2.go
... ...
func handleConn(c net.Conn) {
    defer c.Close()
    for {
        // read from the connection
        var buf = make([]byte, 10)
        log.Println("start to read from conn")
        n, err := c.Read(buf)
        if err != nil {
            log.Println("conn read error:", err)
            return
        }
        log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
    }
}
... ...

</code></pre>
<p>我们通过client2.go发送”hi”到Server端：<br />
运行结果:</p>
<pre><code>$go run client2.go hi
2015/11/17 13:30:53 begin dial...
2015/11/17 13:30:53 dial ok

$go run server2.go
2015/11/17 13:33:45 accept a new connection
2015/11/17 13:33:45 start to read from conn
2015/11/17 13:33:47 read 2 bytes, content is hi
...
</code></pre>
<p>Client向socket中写入两个字节数据(“hi”)，Server端创建一个len = 10的slice，等待Read将读取的数据放入slice；Server随后读取到那两个字节：”hi”。Read成功返回，n =2 ，err = nil。</p>
<h4>3、Socket中有足够数据</h4>
<p>如果socket中有数据，且长度大于等于一次Read操作所期望读出的数据长度，那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了：Read将用Socket中的数据将我们传入的slice填满后返回：n = 10, err = nil。</p>
<p>我们通过client2.go向Server2发送如下内容：abcdefghij12345，执行结果如下：</p>
<pre><code>$go run client2.go abcdefghij12345
2015/11/17 13:38:00 begin dial...
2015/11/17 13:38:00 dial ok

$go run server2.go
2015/11/17 13:38:00 accept a new connection
2015/11/17 13:38:00 start to read from conn
2015/11/17 13:38:02 read 10 bytes, content is abcdefghij
2015/11/17 13:38:02 start to read from conn
2015/11/17 13:38:02 read 5 bytes, content is 12345
</code></pre>
<p>client端发送的内容长度为15个字节，Server端Read buffer的长度为10，因此Server Read第一次返回时只会读取10个字节；Socket中还剩余5个字节数据，Server再次Read时会把剩余数据读出（如：情形2）。</p>
<h4>4、Socket关闭</h4>
<p>如果client端主动关闭了socket，那么Server的Read将会读到什么呢？这里分为“有数据关闭”和“无数据关闭”。</p>
<p>“有数据关闭”是指在client关闭时，socket中还有server端未读取的数据，我们在go-tcpsock/read_write/client3.go和server3.go中模拟这种情况：</p>
<pre><code>$go run client3.go hello
2015/11/17 13:50:57 begin dial...
2015/11/17 13:50:57 dial ok

$go run server3.go
2015/11/17 13:50:57 accept a new connection
2015/11/17 13:51:07 start to read from conn
2015/11/17 13:51:07 read 5 bytes, content is hello
2015/11/17 13:51:17 start to read from conn
2015/11/17 13:51:17 conn read error: EOF
</code></pre>
<p>从输出结果来看，当client端close socket退出后，server3依旧没有开始Read，10s后第一次Read成功读出了5个字节的数据，当第二次Read时，由于client端 socket关闭，Read返回EOF error。</p>
<p>通过上面这个例子，我们也可以猜测出“无数据关闭”情形下的结果，那就是Read直接返回EOF error。</p>
<h4>5、读取操作超时</h4>
<p>有些场合对Read的阻塞时间有严格限制，在这种情况下，Read的行为到底是什么样的呢？在返回超时错误时，是否也同时Read了一部分数据了呢？这个实验比较难于模拟，下面的测试结果也未必能反映出所有可能结果。我们编写了client4.go和server4.go来模拟这一情形。</p>
<pre><code>//go-tcpsock/read_write/client4.go
... ...
func main() {
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")

    data := make([]byte, 65536)
    conn.Write(data)

    time.Sleep(time.Second * 10000)
}

//go-tcpsock/read_write/server4.go
... ...
func handleConn(c net.Conn) {
    defer c.Close()
    for {
        // read from the connection
        time.Sleep(10 * time.Second)
        var buf = make([]byte, 65536)
        log.Println("start to read from conn")
        c.SetReadDeadline(time.Now().Add(time.Microsecond * 10))
        n, err := c.Read(buf)
        if err != nil {
            log.Printf("conn read %d bytes,  error: %s", n, err)
            if nerr, ok := err.(net.Error); ok &amp;&amp; nerr.Timeout() {
                continue
            }
            return
        }
        log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
    }
}

</code></pre>
<p>在Server端我们通过Conn的SetReadDeadline方法设置了10微秒的读超时时间，Server的执行结果如下：</p>
<pre><code>$go run server4.go

2015/11/17 14:21:17 accept a new connection
2015/11/17 14:21:27 start to read from conn
2015/11/17 14:21:27 conn read 0 bytes,  error: read tcp 127.0.0.1:8888-&gt;127.0.0.1:60970: i/o timeout
2015/11/17 14:21:37 start to read from conn
2015/11/17 14:21:37 read 65536 bytes, content is

</code></pre>
<p>虽然每次都是10微秒超时，但结果不同，第一次Read超时，读出数据长度为0；第二次读取所有数据成功，没有超时。反复执行了多次，没能出现“读出部分数据且返回超时错误”的情况。</p>
<hr />
<p>和读相比，Write遇到的情形一样不少，我们也逐一看一下。</p>
<hr />
<h4>1、成功写</h4>
<p>前面例子着重于Read，client端在Write时并未判断Write的返回值。所谓“成功写”指的就是Write调用返回的n与预期要写入的数据长度相等，且error = nil。这是我们在调用Write时遇到的最常见的情形，这里不再举例了。</p>
<h4>2、写阻塞</h4>
<p>TCP连接通信两端的OS都会为该连接保留数据缓冲，一端调用Write后，实际上数据是写入到OS的协议栈的数据缓冲的。TCP是全双工通信，因此每个方向都有独立的数据缓冲。当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后，Write就会阻塞。我们来看一个例子：client5.go和server.go。</p>
<pre><code>//go-tcpsock/read_write/client5.go
... ...
func main() {
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")

    data := make([]byte, 65536)
    var total int
    for {
        n, err := conn.Write(data)
        if err != nil {
            total += n
            log.Printf("write %d bytes, error:%s\n", n, err)
            break
        }
        total += n
        log.Printf("write %d bytes this time, %d bytes in total\n", n, total)
    }

    log.Printf("write %d bytes in total\n", total)
    time.Sleep(time.Second * 10000)
}

//go-tcpsock/read_write/server5.go
... ...
func handleConn(c net.Conn) {
    defer c.Close()
    time.Sleep(time.Second * 10)
    for {
        // read from the connection
        time.Sleep(5 * time.Second)
        var buf = make([]byte, 60000)
        log.Println("start to read from conn")
        n, err := c.Read(buf)
        if err != nil {
            log.Printf("conn read %d bytes,  error: %s", n, err)
            if nerr, ok := err.(net.Error); ok &amp;&amp; nerr.Timeout() {
                continue
            }
        }

        log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
    }
}
... ...
</code></pre>
<p>Server5在前10s中并不Read数据，因此当client5一直尝试写入时，写到一定量后就会发生阻塞：</p>
<pre><code>$go run client5.go

2015/11/17 14:57:33 begin dial...
2015/11/17 14:57:33 dial ok
2015/11/17 14:57:33 write 65536 bytes this time, 65536 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 131072 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 196608 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 262144 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 327680 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 393216 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 458752 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 524288 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 589824 bytes in total
2015/11/17 14:57:33 write 65536 bytes this time, 655360 bytes in total

</code></pre>
<p>在Darwin上，这个size大约在679468bytes。后续当server5每隔5s进行Read时，OS socket缓冲区腾出了空间，client5就又可以写入了：</p>
<pre><code>$go run server5.go
2015/11/17 15:07:01 accept a new connection
2015/11/17 15:07:16 start to read from conn
2015/11/17 15:07:16 read 60000 bytes, content is
2015/11/17 15:07:21 start to read from conn
2015/11/17 15:07:21 read 60000 bytes, content is
2015/11/17 15:07:26 start to read from conn
2015/11/17 15:07:26 read 60000 bytes, content is
....

client端：

2015/11/17 15:07:01 write 65536 bytes this time, 720896 bytes in total
2015/11/17 15:07:06 write 65536 bytes this time, 786432 bytes in total
2015/11/17 15:07:16 write 65536 bytes this time, 851968 bytes in total
2015/11/17 15:07:16 write 65536 bytes this time, 917504 bytes in total
2015/11/17 15:07:27 write 65536 bytes this time, 983040 bytes in total
2015/11/17 15:07:27 write 65536 bytes this time, 1048576 bytes in total
.... ...

</code></pre>
<h4>3、写入部分数据</h4>
<p>Write操作存在写入部分数据的情况，比如上面例子中，当client端输出日志停留在“write 65536 bytes this time, 655360 bytes in total”时，我们杀掉server5，这时我们会看到client5输出以下日志：</p>
<pre><code>...
2015/11/17 15:19:14 write 65536 bytes this time, 655360 bytes in total
2015/11/17 15:19:16 write 24108 bytes, error:write tcp 127.0.0.1:62245-&gt;127.0.0.1:8888: write: broken pipe
2015/11/17 15:19:16 write 679468 bytes in total
</code></pre>
<p>显然Write并非在655360这个地方阻塞的，而是后续又写入24108后发生了阻塞，server端socket关闭后，我们看到Wrote返回er != nil且n = 24108，程序需要对这部分写入的24108字节做特定处理。</p>
<h4>4、写入超时</h4>
<p>如果非要给Write增加一个期限，那我们可以调用SetWriteDeadline方法。我们copy一份client5.go，形成client6.go，在client6.go的Write之前增加一行timeout设置代码：</p>
<pre><code>conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))
</code></pre>
<p>启动server6.go，启动client6.go，我们可以看到写入超时的情况下，Write的返回结果：</p>
<pre><code>$go run client6.go
2015/11/17 15:26:34 begin dial...
2015/11/17 15:26:34 dial ok
2015/11/17 15:26:34 write 65536 bytes this time, 65536 bytes in total
... ...
2015/11/17 15:26:34 write 65536 bytes this time, 655360 bytes in total
2015/11/17 15:26:34 write 24108 bytes, error:write tcp 127.0.0.1:62325-&gt;127.0.0.1:8888: i/o timeout
2015/11/17 15:26:34 write 679468 bytes in total
</code></pre>
<p>可以看到在写入超时时，依旧存在部分数据写入的情况。</p>
<hr />
<p>综上例子，虽然Go给我们提供了阻塞I/O的便利，但在调用Read和Write时依旧要综合需要方法返回的n和err的结果，以做出正确处理。net.conn实现了io.Reader和io.Writer接口，因此可以试用一些wrapper包进行socket读写，比如bufio包下面的Writer和Reader、io/ioutil下的函数等。</p>
<h4>Goroutine safe</h4>
<p>基于goroutine的网络架构模型，存在在不同goroutine间共享conn的情况，那么conn的读写是否是goroutine safe的呢？在深入这个问题之前，我们先从应用意义上来看read操作和write操作的goroutine-safe必要性。</p>
<p>对于read操作而言，由于TCP是面向字节流，conn.Read无法正确区分数据的业务边界，因此多个goroutine对同一个conn进行read的意义不大，goroutine读到不完整的业务包反倒是增加了业务处理的难度。对与Write操作而言，倒是有多个goroutine并发写的情况。不过conn读写是否goroutine-safe的测试不是很好做，我们先深入一下runtime代码，先从理论上给这个问题定个性：</p>
<p>net.conn只是*netFD的wrapper结构，最终Write和Read都会落在其中的fd上：</p>
<pre><code>type conn struct {
    fd *netFD
}
</code></pre>
<p>netFD在不同平台上有着不同的实现，我们以net/fd_unix.go中的netFD为例：</p>
<pre><code>// Network file descriptor.
type netFD struct {
    // locking/lifetime of sysfd + serialize access to Read and Write methods
    fdmu fdMutex

    // immutable until Close
    sysfd       int
    family      int
    sotype      int
    isConnected bool
    net         string
    laddr       Addr
    raddr       Addr

    // wait server
    pd pollDesc
}

</code></pre>
<p>我们看到netFD中包含了一个runtime实现的fdMutex类型字段，从注释上来看，该fdMutex用来串行化对该netFD对应的sysfd的Write和Read操作。从这个注释上来看，所有对conn的Read和Write操作都是有fdMutex互斥的，从netFD的Read和Write方法的实现也证实了这一点：</p>
<pre><code>func (fd *netFD) Read(p []byte) (n int, err error) {
    if err := fd.readLock(); err != nil {
        return 0, err
    }
    defer fd.readUnlock()
    if err := fd.pd.PrepareRead(); err != nil {
        return 0, err
    }
    for {
        n, err = syscall.Read(fd.sysfd, p)
        if err != nil {
            n = 0
            if err == syscall.EAGAIN {
                if err = fd.pd.WaitRead(); err == nil {
                    continue
                }
            }
        }
        err = fd.eofError(n, err)
        break
    }
    if _, ok := err.(syscall.Errno); ok {
        err = os.NewSyscallError("read", err)
    }
    return
}

func (fd *netFD) Write(p []byte) (nn int, err error) {
    if err := fd.writeLock(); err != nil {
        return 0, err
    }
    defer fd.writeUnlock()
    if err := fd.pd.PrepareWrite(); err != nil {
        return 0, err
    }
    for {
        var n int
        n, err = syscall.Write(fd.sysfd, p[nn:])
        if n &gt; 0 {
            nn += n
        }
        if nn == len(p) {
            break
        }
        if err == syscall.EAGAIN {
            if err = fd.pd.WaitWrite(); err == nil {
                continue
            }
        }
        if err != nil {
            break
        }
        if n == 0 {
            err = io.ErrUnexpectedEOF
            break
        }
    }
    if _, ok := err.(syscall.Errno); ok {
        err = os.NewSyscallError("write", err)
    }
    return nn, err
}
</code></pre>
<p>每次Write操作都是受lock保护，直到此次数据全部write完。因此在应用层面，要想保证多个goroutine在一个conn上write操作的Safe，需要一次write完整写入一个“业务包”；一旦将业务包的写入拆分为多次write，那就无法保证某个Goroutine的某“业务包”数据在conn发送的连续性。</p>
<p>同时也可以看出即便是Read操作，也是lock保护的。多个Goroutine对同一conn的并发读不会出现读出内容重叠的情况，但内容断点是依 runtime调度来随机确定的。存在一个业务包数据，1/3内容被goroutine-1读走，另外2/3被另外一个goroutine-2读 走的情况。比如一个完整包：world，当goroutine的read slice size &lt; 5时，存在可能：一个goroutine读到 “worl”,另外一个goroutine读出”d”。</p>
<h3>四、Socket属性</h3>
<p>原生Socket API提供了丰富的sockopt设置接口，但Golang有自己的网络架构模型，golang提供的socket options接口也是基于上述模型的必要的属性设置。包括</p>
<ul>
<li>SetKeepAlive</li>
<li>SetKeepAlivePeriod</li>
<li>SetLinger</li>
<li>SetNoDelay （默认no delay）</li>
<li>SetWriteBuffer</li>
<li>SetReadBuffer</li>
</ul>
<p>不过上面的Method是TCPConn的，而不是Conn的，要使用上面的Method的，需要type assertion：</p>
<pre><code>tcpConn, ok := c.(*TCPConn)
if !ok {
    //error handle
}

tcpConn.SetNoDelay(true)
</code></pre>
<p>对于listener socket, golang默认采用了 SO_REUSEADDR，这样当你重启 listener程序时，不会因为address in use的错误而启动失败。而listen backlog的默认值是通过获取系统的设置值得到的。不同系统不同：mac 128, linux 512等。</p>
<h3>五、关闭连接</h3>
<p>和前面的方法相比，关闭连接算是最简单的操作了。由于socket是全双工的，client和server端在己方已关闭的socket和对方关闭的socket上操作的结果有不同。看下面例子：</p>
<pre><code>//go-tcpsock/conn_close/client1.go
... ...
func main() {
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    conn.Close()
    log.Println("close ok")

    var buf = make([]byte, 32)
    n, err := conn.Read(buf)
    if err != nil {
        log.Println("read error:", err)
    } else {
        log.Printf("read % bytes, content is %s\n", n, string(buf[:n]))
    }

    n, err = conn.Write(buf)
    if err != nil {
        log.Println("write error:", err)
    } else {
        log.Printf("write % bytes, content is %s\n", n, string(buf[:n]))
    }

    time.Sleep(time.Second * 1000)
}

//go-tcpsock/conn_close/server1.go
... ...
func handleConn(c net.Conn) {
    defer c.Close()

    // read from the connection
    var buf = make([]byte, 10)
    log.Println("start to read from conn")
    n, err := c.Read(buf)
    if err != nil {
        log.Println("conn read error:", err)
    } else {
        log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
    }

    n, err = c.Write(buf)
    if err != nil {
        log.Println("conn write error:", err)
    } else {
        log.Printf("write %d bytes, content is %s\n", n, string(buf[:n]))
    }
}
... ...

</code></pre>
<p>上述例子的执行结果如下：</p>
<pre><code>$go run server1.go
2015/11/17 17:00:51 accept a new connection
2015/11/17 17:00:51 start to read from conn
2015/11/17 17:00:51 conn read error: EOF
2015/11/17 17:00:51 write 10 bytes, content is

$go run client1.go
2015/11/17 17:00:51 begin dial...
2015/11/17 17:00:51 close ok
2015/11/17 17:00:51 read error: read tcp 127.0.0.1:64195-&gt;127.0.0.1:8888: use of closed network connection
2015/11/17 17:00:51 write error: write tcp 127.0.0.1:64195-&gt;127.0.0.1:8888: use of closed network connection
</code></pre>
<p>从client1的结果来看，在己方已经关闭的socket上再进行read和write操作，会得到”use of closed network connection” error；<br />
从server1的执行结果来看，在对方关闭的socket上执行read操作会得到EOF error，但write操作会成功，因为数据会成功写入己方的内核socket缓冲区中，即便最终发不到对方socket缓冲区了，因为己方socket并未关闭。因此当发现对方socket关闭后，己方应该正确合理处理自己的socket，再继续write已经无任何意义了。</p>
<h3>六、小结</h3>
<p>本文比较基础，但却很重要，毕竟golang是面向大规模服务后端的，对通信环节的细节的深入理解会大有裨益。另外Go的goroutine+阻塞通信的网络通信模型降低了开发者心智负担，简化了通信的复杂性，这点尤为重要。</p>
<p>本文代码实验环境：go 1.5.1 on Darwin amd64以及部分在ubuntu 14.04 amd64。</p>
<p>本文demo代码在<a href="https://github.com/bigwhite/experiments/tree/master/go-tcpsock">这里</a>可以找到。</p>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/11/17/tcp-programming-in-golang/feed/</wfw:commentRss>
		<slash:comments>44</slash:comments>
		</item>
		<item>
		<title>为阻塞型函数调用添加超时机制</title>
		<link>https://tonybai.com/2013/10/25/add-timeout-to-blocking-function-call/</link>
		<comments>https://tonybai.com/2013/10/25/add-timeout-to-blocking-function-call/#comments</comments>
		<pubDate>Thu, 24 Oct 2013 16:24:01 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alarm]]></category>
		<category><![CDATA[APUE]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[longjmp]]></category>
		<category><![CDATA[OCI]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[setitimer]]></category>
		<category><![CDATA[setjmp]]></category>
		<category><![CDATA[siglongjmp]]></category>
		<category><![CDATA[sigsetjmp]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[中断]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1427</guid>
		<description><![CDATA[我们产品中的一个子模块在进行Oracle实时数据库查询时，常常因数据库性能波动或异常而被阻塞在OCI API的调用上，为此我们付出了&#8220;惨痛&#8221;的代价。说来说去还是我们的程序设计的不够完善，在此类阻塞型函数调用方面缺少微小粒度的超时机制。 调用阻塞多发生在I/O操作（磁盘、网络、低速设备）、第三方API调用等方面。对于文件/网络I/O操作，我们可利用在非阻塞文件描述符上select /poll的超时机制来替代针对阻塞型文件描述符的系统调用；但在第三方API方面，多数时候是无法用select/poll来进行超时的，我们可以选择 另外一种方法：利用setjmp和longjmp的非局部跳转机制来为特定阻塞调用添加超时机制。其原理大致是：利用定时器(alarm、setitimer)设置超时时间，在SIGALRM的handler中利用longjmp跳到阻塞型调用之前，达到超时跳出阻塞型函数调用的效果。同时这种方法通用性更好些。 这个机制实现起来并不难，但有些细节还是要考虑周全，否则很容易出错。我们的产品是需要运行在Linux和Solaris两个平台下的，因此机制的实现还要考虑移植性的问题。下面简要说说在实现这一机制过程中出现的一些问题与解决方法。 一、第一版 考虑到阻塞型函数的原型各不相同，且我们的产品中对阻塞调用有重试次数的要求，因此打算将这个机制包装成一个宏，大致是这个模样： #define add_timeout_to_func(func, n, interval, ret, &#8230;) \&#8230; 其中func是函数名；n是重试的次数；interval是超时的时间，单位是秒；ret是函数成功调用后的返回值，若失败，也是这个宏的返回值。 我们可以像下面这样使用这个宏： /* example.c */ int main() { &#160;&#160;&#160; #define MAXLINE 1024 &#160;&#160;&#160; char line[MAXLINE]; &#160;&#160;&#160; int ret = 0; &#160;&#160;&#160; int try_times = 3; &#160;&#160;&#160; int interval = 1000; &#160;&#160;&#160; add_timeout_to_func(read, try_times, interval, ret, STDIN_FILENO, line, MAXLINE); &#160;&#160;&#160; if [...]]]></description>
			<content:encoded><![CDATA[<p>我们产品中的一个子模块在进行<a href="http://tonybai.com/2009/07/31/a-bug-of-oracle-oci-lib/">Oracle</a>实时数据库查询时，常常因数据库性能波动或异常而被阻塞在<a href="http://tonybai.com/2009/07/31/a-bug-of-oracle-oci-lib/">OCI API</a>的调用上，为此我们付出了&ldquo;惨痛&rdquo;的代价。说来说去还是我们的程序设计的不够完善，在此类阻塞型函数调用方面缺少微小粒度的超时机制。</p>
<p>调用阻塞多发生在I/O操作（磁盘、网络、低速设备）、第三方API调用等方面。对于文件/网络I/O操作，我们可利用在非阻塞文件描述符上select /poll的超时机制来替代针对阻塞型文件描述符的系统调用；但在第三方API方面，多数时候是无法用select/poll来进行超时的，我们可以选择 另外一种方法：利用setjmp和longjmp的非局部跳转机制来为特定阻塞调用添加超时机制。其原理大致是：<b>利用定时器(alarm、setitimer)设置超时时间，在SIGALRM的handler中利用longjmp跳到阻塞型调用之前，达到超时跳出阻塞型函数调用的效果</b>。同时这种方法通用性更好些。</p>
<p>这个机制实现起来并不难，但有些细节还是要考虑周全，否则很容易出错。我们的产品是需要运行在<a href="http://tonybai.com/tag/Linux">Linux</a>和<a href="http://tonybai.com/tag/Solaris">Solaris</a>两个平台下的，因此机制的实现还要考虑移植性的问题。下面简要说说在实现这一机制过程中出现的一些问题与解决方法。</p>
<p><b>一、第一版</b></p>
<p>考虑到阻塞型函数的原型各不相同，且我们的产品中对阻塞调用有重试次数的要求，因此打算将这个机制包装成一个<a href="http://tonybai.com/2008/05/17/examples-for-macro-definition-switch-and-mask/">宏</a>，大致是这个模样：</p>
<p><font face="Courier New">#define add_timeout_to_func(func, n, interval, ret, &#8230;) \&#8230;</font></p>
<p>其中func是函数名；n是重试的次数；interval是超时的时间，单位是秒；ret是函数成功调用后的返回值，若失败，也是这个宏的返回值。</p>
<p>我们可以像下面这样使用这个宏：</p>
<p><font face="Courier New">/* example.c */<br />
	int<br />
	main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; #define MAXLINE 1024<br />
	&nbsp;&nbsp;&nbsp; char line[MAXLINE];</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; int ret = 0;<br />
	&nbsp;&nbsp;&nbsp; int try_times = 3;<br />
	&nbsp;&nbsp;&nbsp; int interval = 1000;<br />
	&nbsp;&nbsp;&nbsp; add_timeout_to_func(read, try_times, interval, ret, STDIN_FILENO, line, MAXLINE);<br />
	&nbsp;&nbsp;&nbsp; if (ret == E_CALL_TIMEOUT) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;invoke read timeouts for 3 times\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; } else if (ret == 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;invoke read ok\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;add_timeout_to_func error = %d\n&quot;, ret);<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>add_timeout_to_func中为阻塞型函数添加的超时机制是利用setjmp/longjmp与信号的处理函数合作完成的。</p>
<p><font face="Courier New">/* timeout_wrapper.h */</font><br />
	&nbsp;</p>
<pre><font face="Courier New">#include &lt;setjmp.h&gt;
#include &lt;stdarg.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdio.h&gt;
#include &lt;signal.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;

extern volatile int invoke_count;
extern jmp_buf invoke_env;

void timeout_signal_handler(int sig);
typedef void (*sighandler_t)(int);
#define E_CALL_TIMEOUT (-9)

#define add_timeout_to_func(func, n, interval, ret, ...) \
    { \
        invoke_count = 0; \
        sighandler_t h = signal(SIGALRM, timeout_signal_handler); \
        if (h == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigjmp(invoke_env) != 0) { \
            if (invoke_count &gt;= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        alarm(interval);\
        ret = func(__VA_ARGS__);\
        alarm(0); \
err:\
        signal(SIGALRM, h);\
end:\
        ;\
    }</font>
</pre>
<p><font face="Courier New">/* timeout_wrapper.c */<br />
	#include &quot;timeout_wrapper.h&quot;</font></p>
<p><font face="Courier New">volatile int invoke_count = 0;<br />
	jmp_buf invoke_env;</font></p>
<p><font face="Courier New">void<br />
	timeout_signal_handler(int sig)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; invoke_count++;<br />
	&nbsp;&nbsp;&nbsp; longjmp(invoke_env, 1);<br />
	}</font></p>
<p>编译运行这个程序，分别在Solaris、Linux下运行，遗憾的是两个平台下都以失败告终。</p>
<p>先说一下在Linux下的情况。在Linux下，程序居然不响应第二次SIGALRM信号了。通过strace也可以看出，当alarm被第二次调用后， 系统便阻塞在了read上，没有实现为read增加超时机制的目的。原因何在呢？我在《<a href="http://book.douban.com/subject/4292217/">The Linux Programming Interface</a>》一书中找到了原因。原因大致是这样的，我们按照代码的执行流程来分析：</p>
<p>* add_timeout_to_func宏首先设置了信号的handler，保存了env信息(setjmp)，调用alarm设置定时器，然后阻塞在read调用上；<br />
	* 1s后，定时器信号SIGALRM产生，中断发生，代码进入信号处理程序，即timeout_signal_handler; Linux上的实现是当进入处理程序时，内核会自动屏蔽对应的信号(SIGALRM)以及此时act.sa_mask字段中的所有信号；在离开 handler后，内核取消这些信号的屏蔽。<br />
	* 问题在于我们是通过longjmp调用离开handler的，longjmp对应的invoke_env是否在setjmp时保存了这些被屏蔽的信号呢？ 答案是：在Linux上没有。这样longjmp跳到setjmp后也就无法恢复对SIGALRM的屏蔽；当再次产生SIGALRM信号时，程序将无法处 理，也就一直阻塞在read调用上了。</p>
<p>解决方法：将setjmp/longjmp替换为sigsetjmp和siglongjmp，后面这组调用在sigsetjmp时保存了屏蔽信号，这样在 siglongjmp返回时可以恢复到handler之前的信号屏蔽集合，也就是说SIGALRM恢复自由了。在Solaris 下，setjmp/longjmp是可以恢复被屏蔽的信号的。</p>
<p>再说说在Solaris下的情况。在Solaris下，程序在第二次SIGALRM到来之际，居然退出了，终端上显示：&ldquo;闹钟信号&rdquo;。这是因为在 Solaris下，通过signal函数设置信号的处理handler仅是一次性的。在应对完一次信号处理后，信号的handler被自动恢复到之前的处 理策略设置，对于SIGALRM来说，也就是程序退出。解决办法：通过多次调用signal设置handler或通过sigaction来长效设置 handler。考虑到移植性和简单性，我们选择了sigaction。在Linux平台下，signal函数底层就是用sigaction实现的，是简洁版的sigaction，因此它的设置不是一次性的，而是长效的。</p>
<p><b>二、第二版</b></p>
<p>综上问题的修改，我们有了第二版代码。</p>
<p><font face="Courier New">/* timeout_wrapper.h */</font></p>
<p><font face="Courier New">extern volatile int invoke_count;<br />
	extern sigjmp_buf invoke_env;</font></p>
<p><font face="Courier New">void timeout_signal_handler(int sig);<br />
	typedef void sigfunc(int sig);<br />
	sigfunc *my_signal(int signo, sigfunc* func);<br />
	#define E_CALL_TIMEOUT (-9)</font></p>
<p><font face="Courier New">#define add_timeout_to_func(func, n, interval, ret, &#8230;) \<br />
	&nbsp;&nbsp;&nbsp; { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; invoke_count = 0; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sf == SIG_ERR) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = errno; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto end; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }&nbsp; \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigsetjmp(invoke_env, SIGALRM) != 0) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (invoke_count &gt;= n) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = E_CALL_TIMEOUT; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto err; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; alarm(interval); \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = func(__VA_ARGS__);\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; alarm(0); \<br />
	err:\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; my_signal(SIGALRM, sf); \<br />
	end:\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ;\<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">/* timeout_wrapper.c */</font></p>
<p><font face="Courier New">volatile int invoke_count = 0;<br />
	sigjmp_buf invoke_env;</font></p>
<p><font face="Courier New">void<br />
	timeout_signal_handler(int sig)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; invoke_count++;<br />
	&nbsp;&nbsp;&nbsp; siglongjmp(invoke_env, 1);<br />
	}</font></p>
<p><font face="Courier New">sigfunc *<br />
	my_signal(int signo, sigfunc *func)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; struct sigaction act, oact;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; act.sa_handler = func;<br />
	&nbsp;&nbsp;&nbsp; sigemptyset(&amp;act.sa_mask);<br />
	&nbsp;&nbsp;&nbsp; act.sa_flags = 0;<br />
	&nbsp;&nbsp;&nbsp; if (signo == SIGALRM) {<br />
	#ifdef SA_INTERRUPT<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; act.sa_flags |= SA_INTERRUPT;<br />
	#endif<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	#ifdef SA_RESTART<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; act.sa_flags |= SA_RESTART;<br />
	#endif<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; if (sigaction(signo, &amp;act, &amp;oact) &lt; 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return SIG_ERR;<br />
	&nbsp;&nbsp;&nbsp; return oact.sa_handler;<br />
	}</font></p>
<p>这里从《<a href="http://book.douban.com/subject/1788421/">Unix高级环境编程</a>》中借了一段代码，就是那段my_signal的实现。这样修改后，程序在Linux和Solaris下工作都蛮好的。但目前唯一的缺点就是超时时间粒度太大，alarm仅支持秒级定时器，我们至少要支持毫秒级，接下来我们要换掉alarm。</p>
<p><b>三、第三版</b></p>
<p>setitimer与alarm是同出一门，共享一个定时器的。不同的是setitimer可以支持到微秒级的粒度，因此我们就用setitimer替换alarm，第三版仅改动了add_timeout_to_func这个宏：</p>
<p><font face="Courier New">#define add_timeout_to_func(func, n, interval, ret, &#8230;) \<br />
	&nbsp;&nbsp;&nbsp; { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; invoke_count = 0; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sf == SIG_ERR) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = errno; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto end; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }&nbsp; \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigsetjmp(invoke_env, SIGALRM) != 0) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (invoke_count &gt;= n) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = E_CALL_TIMEOUT; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto err; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; struct itimerval tick;&nbsp; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; struct itimerval oldtick;&nbsp; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tick.it_value.tv_sec = interval/1000; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tick.it_value.tv_usec = (interval%1000) * 1000; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tick.it_interval.tv_sec = interval/1000; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tick.it_interval.tv_usec = (interval%1000) * 1000; \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (setitimer(ITIMER_REAL, &amp;tick, &amp;oldtick) &lt; 0) { \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = errno; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto err; \<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } \<br />
	\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret = func(__VA_ARGS__);\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; setitimer(ITIMER_REAL, &amp;oldtick, NULL); \<br />
	err:\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; my_signal(SIGALRM, sf); \<br />
	end:\<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ;\<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p>至此，一个为阻塞型函数调用添加的超时机制的雏形基本实现完毕了，但要放在产品代码里还需要更细致的打磨。至少目前只是在单进程单线程中跑过，而且要求每个函数中只能调用add_timeout_to_func一次，否则就会有编译错误。</p>
<p>以上完整代码我都放到github上的<a href="https://github.com/bigwhite/experiments">experiments repository</a>中了，有兴趣的朋友可以下载细看。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/10/25/add-timeout-to-blocking-function-call/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>也谈Commit log</title>
		<link>https://tonybai.com/2013/05/09/also-talk-about-commit-log/</link>
		<comments>https://tonybai.com/2013/05/09/also-talk-about-commit-log/#comments</comments>
		<pubDate>Thu, 09 May 2013 09:18:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Chinglish]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Mercurial]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[模板]]></category>
		<category><![CDATA[版本控制]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1264</guid>
		<description><![CDATA[在版本控制工具大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如Subversion、Git、Mercurial等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&#8220;commit context&#8221;。在每次commit时，这些上下文信息只能通过commit log来体现。 一、Commit Context 今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。 前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。 commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。 [example1 - Linux Kernel] audit: catch possible NULL audit buffers It&#39;s possible for audit_log_start() to return NULL.&#160; Handle it in the various callers. Signed-off-by: Kees Cook [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="http://tonybai.com/2011/02/18/put-everything-under-version-control/">版本控制工具</a>大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如<a href="http://subversion.apache.org">Subversion</a>、<a href="http://git-scm.com">Git</a>、<a href="http://mercurial.selenic.com/‎">Mercurial</a>等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&ldquo;commit context&rdquo;。在每次commit时，这些上下文信息只能通过commit log来体现。</p>
<p><b>一、Commit Context</b></p>
<p>今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。</p>
<p>前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。</p>
<p>commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与<b>项目过程、质量保证流程以及项目使用的一些工具系统</b>有 关联。我们来看两个知名开源项目的commit log样例吧。</p>
<p><i>[example1 - Linux Kernel]</i></p>
<p><font face="Courier New">audit: catch possible NULL audit buffers<br />
	It&#39;s possible for audit_log_start() to return NULL.&nbsp; Handle it in the<br />
	various callers.</font></p>
<p><font face="Courier New">Signed-off-by: Kees Cook <a class="moz-txt-link-rfc2396E" href="mailto:keescook@chromium.org">&lt;keescook@chromium.org&gt;</a><br />
	Cc: Al Viro <a class="moz-txt-link-rfc2396E" href="mailto:viro@zeniv.linux.org.uk">&lt;viro@zeniv.linux.org.uk&gt;</a><br />
	Cc: Eric Paris <a class="moz-txt-link-rfc2396E" href="mailto:eparis@redhat.com">&lt;eparis@redhat.com&gt;</a><br />
	Cc: Jeff Layton <a class="moz-txt-link-rfc2396E" href="mailto:jlayton@redhat.com">&lt;jlayton@redhat.com&gt;</a><br />
	Cc: &quot;Eric W. Biederman&quot; <a class="moz-txt-link-rfc2396E" href="mailto:ebiederm@xmission.com">&lt;ebiederm@xmission.com&gt;</a><br />
	Cc: Julien Tinnes <a class="moz-txt-link-rfc2396E" href="mailto:jln@google.com">&lt;jln@google.com&gt;</a><br />
	Cc: Will Drewry <a class="moz-txt-link-rfc2396E" href="mailto:wad@google.com">&lt;wad@google.com&gt;</a><br />
	Cc: Steve Grubb <a class="moz-txt-link-rfc2396E" href="mailto:sgrubb@redhat.com">&lt;sgrubb@redhat.com&gt;</a><br />
	Cc: Andrea Arcangeli <a class="moz-txt-link-rfc2396E" href="mailto:aarcange@redhat.com">&lt;aarcange@redhat.com&gt;</a><br />
	Signed-off-by: Andrew Morton <a class="moz-txt-link-rfc2396E" href="mailto:akpm@linux-foundation.org">&lt;akpm@linux-foundation.org&gt;</a><br />
	Signed-off-by: Linus Torvalds <a class="moz-txt-link-rfc2396E" href="mailto:torvalds@linux-foundation.org">&lt;torvalds@linux-foundation.org&gt;</a></font></p>
<p>这是<a href="https://www.kernel.org">Linux Kernel</a>项目的一个commit log的内容。从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的内核模块范围audit<br />
	- 修改的原因summary: to catch possible NULL audit buffers<br />
	- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表<br />
	- 这个patch由Who sign-off的。</p>
<p>将mail list放入到commit log中，这是Linux Kernel开发过程规范所要求的，同样也是质量保证的一个方法。在《<a href="http://tonybai.com/2012/03/27/how-to-participate-linux-community-section-1/">如何加入Linux内核开发社区</a>》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。</p>
<p><i>[example2 - Apache Subversion]</i></p>
<p><font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font></p>
<p><font face="Courier New">* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c<br />
	&nbsp; (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_first_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked<br />
	&nbsp;&nbsp;&nbsp; and we are in interactive mode but have no unlock prompt function, don&#39;t<br />
	&nbsp;&nbsp;&nbsp; throw a &quot;GNOME Keyring is locked and we are non-interactive&quot; error;<br />
	&nbsp;&nbsp;&nbsp; instead, continue without unlocking it, so that the unlocking may be<br />
	&nbsp;&nbsp;&nbsp; handled by the default GNOME Keyring unlock dialog box.</font></p>
<p>这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c，包括括号中的函数名列表， 这个显然更为细致。<br />
	- 修改的原因summary: <font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font><br />
	- 这个patch与问题跟踪系统的关联性 -<font face="Courier New">issue #3498</font>。</p>
<p>通过这个commit log，我们可以快速找到此patch对应的问题跟踪系统中的条目#3498，这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。</p>
<p>综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的&ldquo;全面&rdquo;是project-dependent的，是需要能够体现出涉及project的一切必要信息的：过程的、质量的、工具 的。</p>
<p><b>二、Commit log格式</b></p>
<p>Commit log没有放之四海而皆准的统一格式，而是project-dependent的。就我个人而言，我会在下面的几个问题上有纠结。</p>
<p><b><i>* 语言</i></b></p>
<p>不得不承认在创造编程语言方面，西方文化占了主导，语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式，但却是很少见到在源文件中使用非英语命名变量或函数的，这也影响了我在commit log中对语言的选择 &#8211; 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的，你用中文提交也是没有任何问题的，尤其是在国内商业项目中，使用中文描述起来，理解上快且歧义少。我是不反对用中文写commit log的，但反感的是中英文混合写commit log（有些人用中文，有些人用英文）。每当批量看commit log时，中英文混在一起，一点美感都没有了。</p>
<p>commit log不是给最终用户看的，而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利，精确全面地传达context。即便 应用是要发布给非洲人民，但若开发人员都是中国人，一样可以用中文编写commit log。</p>
<p><b><i>* 地道</i></b></p>
<p>说到&ldquo;地道&rdquo;，主要是针对你选择<b>外语</b>（大多数情况是英语）作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的，我们在用英语编写commit log时也要学会选用&ldquo;地道&rdquo;的词汇，远离<a href="http://en.wikipedia.org/wiki/Chinglish">Chinglish</a>。当然想立即做到&ldquo;地道&rdquo;也不是那么容易，毕竟我们一直以来就按照Chinglish的思维去学 习英语的，一个比较好的方式就是多看看知名开源项目（比如linux kernel）的commit log，看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少，看多了也就找猫画虎的学会了。</p>
<p><b><i>* 规范</i></b></p>
<p>&ldquo;没有规矩，不成方圆&rdquo;，无论是商业软件项目，还是大型开源项目，莫不如此。如果要想很好的传达commit context，一个设计规范，内容全面的commit log格式是必不可少的。我们无需从头做起，很多开源项目在这方面都已经有一些良好的实践，比如上面提到的linux kernel的commit log convention，再比如这里有Apache Subversion的<a href="http://subversion.apache.org/docs/community-guide/conventions.html#coding-style">Commit log要求</a>。TYPO3和FLOW3也有自己详细的<a href="http://wiki.typo3.org/CommitMessage_Format_(Git)">Commit log说明</a>。</p>
<p>制定规范时总体来说，注意以下几点：<br />
	&#8211; 格式简明扼要，只保留必要的项；<br />
	&#8211; 注意与项目过程、质量保证流程的结合，以及与第三方工具的关联（注意序号或ID的唯一性）；<br />
	&#8211; 对于规模较大的系统，可以考虑在log中体现影响的涉及的&ldquo;子模块&rdquo;或&ldquo;子目录&rdquo;名字或者逻辑功能的名字（比如前面linux kernel例子中的audit），这样便于快速定位本地commit的影响范畴。</p>
<p><b>三、Commit模板</b></p>
<p>如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成（比如问题跟踪系统、代码评审系统等）时，建议设置Commit log template(模板)以简化开发者commit log编写的工作。</p>
<p><b><i>* Subversion命令行客户端支持commit log模板</i></b></p>
<p>Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能：</p>
<p>- 创建commit log模板log.tmpl，放在特定目录下，本例中放在用户的$HOME目录下<br />
	- 添加并导出环境变量SVN_EDITOR<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">export SVN_EDITOR=&quot;rm svn-commit.tmp &amp;&amp; cp ~/log.tmpl svn-commit.tmp &amp;&amp; vi &quot;</font></p>
<p>svn commit时，svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令，而svn-commit.tmp文件已经被替换为我们的模板文件，开发者只需按模板填写内容，并保存退出即可。如果 commit成功，svn客户端会删除当前目录下的svn-commit.tmp，否则svn-commit.tmp不会被删除，这将导致下次再提交 时，svn客户端检测到svn-commit.tmp的存在，从而新建立一个svn-commit.2.tmp的新文件，导致模板失效，这也是这个方法的 一个瑕疵。</p>
<p><b><i>* Git命令行支持commit log模板</i></b></p>
<p>Git是目前very hot的分布式版本管理工具，起步晚，但起点高，因此已经内置了对模板的支持，只需将模板文件配置一下即可。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font face="Courier New"> git config &#8211;global commit.template ~/log.tmpl</font></p>
<p><b>四、良好格式commit log</b><b>的实施</b></p>
<p>即便有了良好格式的commit log的模板定义，但就我经验而言，实施起来也还会遇到诸多问题。commit行为是客户端发起的，要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署<a href="http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/">pre-commit hook</a>，对提交的log格式进行检查，不符合模板格式的予以拒绝等。</p>
<p>对于与问题跟踪系统有关联的log格式，还要注意保持问题跟踪系统id或序号的唯一性，这显然是管理和过程方面的工作。</p>
<p>对于开源项目，一般merge到trunk需要owner的检查，所以反倒实施起来容易了些，只要有一篇内容丰富的 developer/community guide或convention之类的文档即可，多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的，为这些project提交patch前是要好好阅读这些文档的，不能坏了规矩^_^。&nbsp; &nbsp; &nbsp;<br />
	&nbsp;</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/05/09/also-talk-about-commit-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>libiconv库链接问题一则</title>
		<link>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/</link>
		<comments>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/#comments</comments>
		<pubDate>Thu, 25 Apr 2013 10:04:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[iconv]]></category>
		<category><![CDATA[ld]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libiconv]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1258</guid>
		<description><![CDATA[与在Solaris系统上不同，Linux的libc库中包含了libiconv库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题，到底是怎样一回事呢？这里分享一下问题查找过程。 一、现场重现 这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程： /*test.c */ &#8230; #include &#60;iconv.h&#62; int main(void) { &#160;&#160;&#160; int r; &#160;&#160;&#160; char *sin, *sout; &#160;&#160;&#160; size_t lenin, lenout; &#160;&#160;&#160; char *src = &#34;你好!&#34;; &#160;&#160;&#160; char dst[256] = {0}; &#160;&#160;&#160; iconv_t c_pt;&#160;&#160; &#160;&#160;&#160; sin = src; &#160;&#160;&#160; lenin = strlen(src)+1; &#160;&#160;&#160; sout = dst; &#160;&#160;&#160; lenout = 256; &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>与在<a href="http://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/">Solaris</a>系统上不同，<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Linux</a>的libc库中包含了<a href="http://tonybai.com/2009/10/31/internal-code-transform-by-iconv/">libiconv</a>库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的<a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/">链接问题</a>，到底是怎样一回事呢？这里分享一下问题查找过程。</p>
<p><b>一、现场重现</b></p>
<p>这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：<br />
	/*test.c */</p>
<p>&#8230;<br />
	<font face="Courier New">#include &lt;iconv.h&gt;</font></p>
<p><font face="Courier New">int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int r;<br />
	&nbsp;&nbsp;&nbsp; char *sin, *sout;<br />
	&nbsp;&nbsp;&nbsp; size_t lenin, lenout;<br />
	&nbsp;&nbsp;&nbsp; char *src = &quot;你好!&quot;;<br />
	&nbsp;&nbsp;&nbsp; char dst[256] = {0};<br />
	&nbsp;&nbsp;&nbsp; iconv_t c_pt;&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sin = src;<br />
	&nbsp;&nbsp;&nbsp; lenin = strlen(src)+1;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sout = dst;<br />
	&nbsp;&nbsp;&nbsp; lenout = 256;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((c_pt = iconv_open(&quot;UTF-8&quot;, &quot;GB2312&quot;)) == (iconv_t)(-1)){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv_open error!. errno[%d].\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((r = iconv(c_pt, (char **)&amp;sin, &amp;lenin, &amp;sout, &amp;lenout)) != 0){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv error!. errno[%d].\n&quot;, r);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; iconv_close(c_pt);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; printf(&quot;SRC[%s], DST[%s].\n&quot;, src, dst);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>根据之前的经验，我们按如下命令编译该程序：</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c</font></p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>咦，这是咋搞的呢？怎么找不到iconv库的符号！！！显式加上iconv的链接指示再试试。</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c -liconv</font></p>
<p>这回编译OK了。的确如那位同事所说出现了怪异的情况。</p>
<p><b>二、现场取证</b></p>
<p>惯性思维让我<b>首先</b>提出疑问：难道是这台机器上的<a href="http://www.gnu.org/s/libc/">libc</a>版本有差异，检查一下<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">libc</a>中是否定义了iconv相关符号。</p>
<p><font face="Courier New">$ nm /lib64/libc.so.6 |grep iconv<br />
	000000397141e040 T iconv<br />
	000000397141e1e0 T iconv_close<br />
	000000397141ddc0 T iconv_open</font></p>
<p>iconv的函数都定义了呀！怎么会链接不到？</p>
<p>我们<b>再来</b>看看已经编译成功的那个test到底连接到哪个iconv库了。</p>
<p><font face="Courier New">$ ldd test<br />
	&nbsp;&nbsp;&nbsp; linux-vdso.so.1 =&gt;&nbsp; (0x00007fff77d6b000)<br />
	&nbsp;&nbsp;&nbsp; libiconv.so.2 =&gt; /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)<br />
	&nbsp;&nbsp;&nbsp; libc.so.6 =&gt; /lib64/libc.so.6 (0&#215;0000003971400000)<br />
	&nbsp;&nbsp;&nbsp; /lib64/ld-linux-x86-64.so.2 (0&#215;0000003971000000)</font></p>
<p>哦，系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了，但gcc怎么会链接到这里了呢？</p>
<p><b>三、</b><b>大侦探的分析^_^</b></p>
<p><a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>到底做了什么呢？我们看看其verbose的输出结果。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -v<br />
	使用内建 specs。<br />
	目标：x86_64-redhat-linux<br />
	配置为：../configure &#8211;prefix=/usr &#8211;mandir=/usr/share/man &#8211;infodir=/usr/share/info &#8211;enable-shared &#8211;enable-threads=posix &#8211;enable-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; checking=release &#8211;with-system-zlib &#8211;enable-__cxa_atexit &#8211;disable-libunwind-exceptions &#8211;enable-libgcj-multifile &#8211;enable-languages=c,c++,&nbsp;&nbsp; objc,obj-c++,java,fortran,ada &#8211;enable-java-awt=gtk &#8211;disable-dssi &#8211;disable-plugin &#8211;with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre &#8211;with-cpu=generic &#8211;host=x86_64-redhat-linux<br />
	线程模型：posix<br />
	gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/&nbsp;&nbsp;&nbsp;&nbsp; ccypZm0v.s<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)<br />
	&nbsp;&nbsp;&nbsp; 由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。<br />
	GGC 准则：&#8211;param ggc-min-expand=100 &#8211;param ggc-min-heapsize=131072<br />
	Compiler executable checksum: ef754737661c9c384f73674bd4e06594<br />
	&nbsp;as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s<br />
	GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 &#8211;eh-frame-hdr -m elf_x86_64 &#8211;hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.&nbsp; 2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/&nbsp;&nbsp; lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64<br />
	-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed -lc -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o</font></p>
<p>从这个结果来看，gcc在search iconv.h这个头文件时，首先找到的是/usr/local/include/iconv.h，而不是/usr/include/iconv.h。这两个文件有啥不同么？</p>
<p>在/usr/local/include/iconv.h中，我找到如下代码：</p>
<p><font face="Courier New">&#8230;</font><br />
	<font face="Courier New">#ifndef LIBICONV_PLUG<br />
	#define iconv_open libiconv_open<br />
	#endif<br />
	extern iconv_t iconv_open (const char* tocode, const char* fromcode);<br />
	&#8230;</font></p>
<p>libiconv_open vs iconv_open，卧槽！！！再对比一下前面编译时输出的错误信息：</p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>大侦探醒悟了！大侦探带你还原一下真实情况。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c</font>时， 根据gcc -v中include search dir的顺序，gcc首先search到的是/usr/local/include/iconv.h，而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数，而这些函数符号显然在libc中是无法找到的，libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了，没有细致查看gcc的编译错误信息中的内容，这就是问题所在！</p>
<p>而<font face="Courier New">gcc -g -o test test.c -liconv</font>为何可以顺利编译通过呢？gcc是如何找到/usr/local/lib下的libiconv的呢？大侦探再次为大家还原一下真相。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c -liconv</font>时，gcc同 样首先search到的是/usr/local/include/iconv.h，然后编译test.c源码，ok；接下来启动ld程序进行链接；ld找 到了libiconv，ld是怎么找到iconv的呢，libiconv在/usr/local/lib下，ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径：</p>
<p><font face="Courier New">$&gt; ld -verbose|grep SEARCH<br />
	SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib64&quot;); SEARCH_DIR(&quot;/lib64&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib&quot;); SEARCH_DIR(&quot;/lib&quot;); SEARCH_DIR(&quot;/usr/lib&quot;);</font></p>
<p>ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的)，因此找到libiconv就不足为奇了。</p>
<p><b>四、问题解决</b></p>
<p>我们不想显式的加上-liconv，那如何解决这个问题呢？我们是否可以强制gcc先找到/usr/include/iconv.h呢？我们先来做个试验。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;<b>/home/</b><b>tonybai</b><b>/include</b><br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。</font></p>
<p><font face="Courier New">&#8230;</font></p>
<p>试验结果似乎让我们觉得可行，我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	忽略重复的目录&ldquo;/usr/include&rdquo;<br />
	&nbsp; 因为它是一个重复了系统目录的非系统目录<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	&#8230;</font></p>
<p>糟糕！/usr/include被忽略了！还是从/usr/local/include开始，方案失败。</p>
<p>似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉！那就这么做吧^_^！</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>关于编译阶段符号多重定义的问题</title>
		<link>https://tonybai.com/2012/04/11/multiple-definitions-of-the-compiling-phase/</link>
		<comments>https://tonybai.com/2012/04/11/multiple-definitions-of-the-compiling-phase/#comments</comments>
		<pubDate>Tue, 10 Apr 2012 16:36:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[共享库]]></category>
		<category><![CDATA[动态库]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[链接器]]></category>
		<category><![CDATA[静态库]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=858</guid>
		<description><![CDATA[印象中关于编译以及链接的问题早已是老生常谈了。但今天又遇到了一个这样的问题，这里还总想提及一下下^_^。 &#160; 这次要说的问题依旧发生在使用lcut进行单元测试的过程中。一位同事在编译使用了mock函数的测试用例代码时出现了&#34;multiple definition of &#39;xxx&#39;&#8220;的错误。这里简单模拟其场景如下： &#160; /* testall.c */ &#160; /* mock lib function */ int lib_function(&#8230;) { &#160; &#160; &#8230; &#160; &#160; return (int)LCUT_MOCK_RETV(); } &#160; int function_to_be_tested(&#8230;) { &#160; &#160; &#8230; &#160; &#160; ret = lib_function(&#8230;); &#160; &#160; &#8230; } &#160; void test_case(lcut_tc_t *tc, void *data) { &#160; &#160; ret = function_to_be_tested(&#8230;); [...]]]></description>
			<content:encoded><![CDATA[<p>印象中关于编译以及链接的问题早已是老生常谈了。但今天又遇到了一个这样的问题，这里还总想提及一下下^_^。</p>
<div>&nbsp;</div>
<div>这次要说的问题依旧发生在使用<a href="http://code.google.com/p/lcut">lcut</a>进行<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/">单元测试</a>的过程中。一位同事在编译使用了<a href="http://tonybai.com/2010/10/29/lcut-add-mock-support/">mock</a>函数的测试用例代码时出现了&quot;multiple definition of &#39;xxx&#39;&ldquo;的错误。这里简单模拟其场景如下：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">/* testall.c */</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">/* mock lib function */</span></div>
<div><span style="font-family:courier new,courier,monospace;">int lib_function(&#8230;) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; return (int)LCUT_MOCK_RETV();</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">int function_to_be_tested(&#8230;) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ret = lib_function(&#8230;);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">void test_case(lcut_tc_t *tc, void *data) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ret = function_to_be_tested(&#8230;);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_INT_EQUAL(tc, 0, ret);</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div>lib_function是静态<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">共享库</a>中的一个接口，但这里被mock了。不过由于一些其他test_case使用了静态共享库(.a)的其他接口，因此在编译时程序链接了这个静态共享库。但结果编译器却报错：lib_function被多重定义了。</div>
<div>&nbsp;</div>
<div>经过各种排查(编译器命令行中的目标文件与库链接顺序是否正确等)，我们发现编译器报错的原因居然是忘记mock几个与lib_function同属一库模块(xx.o)的接口。</div>
<div>&nbsp;</div>
<div>这里就不拐弯抹角了。由于漏掉了一些本该mock的接口，因此程序在编译testall.c时有许多unresolved的符号需要到静态共享库中去查找。这里又涉及到了符号resolve的过程，而更为重要的一点是要弄清楚编译器是如何对待静态共享库中那个拥有testall.o中未resolved的符号的库模块的(一个静态库.a文件实际上是由诸多库模块.o文件组合而成的)。我们来看看下面例子。</div>
<div>&nbsp;</div>
<div>一个libcommon.a的组成如下：</div>
<div><span style="font-family:courier new,courier,monospace;">libcommon.a</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8211; foo.o</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; &#8211; function: foo1</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; &#8211; function: foo2</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8211; bar.o</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; &#8211; function: bar1</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; &#8211; function: bar2&nbsp;</span></div>
<div>&nbsp;</div>
<div>我们来看一下一个调用了foo1函数且链接了libcommon.a的可执行文件(a.out，对应的源文件main.c)中都有哪些已定义的符号：</div>
<div><span style="font-family:courier new,courier,monospace;">$ nm a.out</span></div>
<div><span style="font-family:courier new,courier,monospace;">&#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">080483d4 T foo1</span></div>
<div><span style="font-family:courier new,courier,monospace;">080483b4 T main</span></div>
<div><span style="font-family:courier new,courier,monospace;">080483e2 T foo2</span></div>
<div><span style="font-family:courier new,courier,monospace;">&#8230;</span></div>
<div>&nbsp;</div>
<div>通过nm输出的结果可以看到，最终的可执行程序中居然包含了程序并未调用的函数foo2的定义。似乎一切都清晰了：编译器在libcommon.a的foo.o中找到了unresolved的符号foo1，但编译器并非只是将foo1的定义放入最终的可执行文件中，而是将foo.o从libcommon.a中取出，并将其与main.o放在一处同等对待，编译器会扫描foo.o中所有的符号，并确保其中的符号都是具有定义的，这样才能保证最终的可执行程序中所有的符号都是具有定义的。</div>
<div>&nbsp;</div>
<div>现在我们可以回过头来回答本文开始处所遇到的那个&quot;多重定义&quot;的问题了。因为testall.c中存在未resolved的符号，即那些被漏掉的未mock的库接口，因此编译器找到了静态共享库中定义了这些库接口的库模块(某个.o文件)，但编译器并非只是处理这些符号，和上面的例子一样，编译器还会扫描这个库模块文件中的所有符号以确保所有符号都有定义。而就在这个过程中编译器发现了其中有的符号(比如lib_function)的定义与testall.c中mock的同名接口(lib_function)定义相冲突，从而才作出了错误提示。</div>
<div>&nbsp;</div>
<div>之前写过一篇文章《<a href="http://tonybai.com/2010/10/11/start-with-mock-malloc/">从mock malloc说起</a>》，其中有关于编译过程中符号resolve的详细说明，有兴趣的朋友不妨看看。</div>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/04/11/multiple-definitions-of-the-compiling-phase/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>偿还N年前的一笔技术债</title>
		<link>https://tonybai.com/2011/07/21/pay-for-a-tech-debt-of-several-year-ago/</link>
		<comments>https://tonybai.com/2011/07/21/pay-for-a-tech-debt-of-several-year-ago/#comments</comments>
		<pubDate>Thu, 21 Jul 2011 04:28:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Tree]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[共享库]]></category>
		<category><![CDATA[内存对齐]]></category>
		<category><![CDATA[动态库]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[树]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[调试]]></category>
		<category><![CDATA[链接器]]></category>
		<category><![CDATA[静态库]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/07/21/%e5%81%bf%e8%bf%98n%e5%b9%b4%e5%89%8d%e7%9a%84%e4%b8%80%e7%ac%94%e6%8a%80%e6%9c%af%e5%80%ba/</guid>
		<description><![CDATA[<p>记得刚来公司时曾参与过一个项目，项目中用到了部门基础库中的一个B+树接口。不过在程序调试过程中我们发现可执行程序总是dump core（在sparc solaris上），经初步分析，断定问题就出在B+树接口处，但一时又找不到问题原因。还好这个B+树的实现者就坐在我的旁边。他...</p>]]></description>
			<content:encoded><![CDATA[<p>记得刚来公司时曾参与过一个项目，项目中用到了部门基础库中的一个<a href="http://en.wikipedia.org/wiki/B%2B_tree" target="_blank">B+树</a>接口。不过在程序调试过程中我们发现可执行程序总是dump core（在sparc <a href="http://tonybai.com/2009/09/10/something-about-installing-solaris-10/" target="_blank">solaris</a>上），经初步分析，断定问题就出在B+树接口处，但一时又找不到问题原因。还好这个B+树的实现者就坐在我的旁边。他分析后告诉我：这个B+树接口要求用户自定义的索引结构体的size应该为4的整数倍。按照他的说法，我为结构体打了padding，以满足结构体size为4的整数倍的要求。修改后果然不再dump core了。当时项目进度紧，我也没有求甚解，这件事也就过去了。</p>
<p>一晃N年过去了。今天在做程序的64位移植过程中我再次遇到了这个问题。问题的表象就是程序运行时dump core，通过<a href="http://tonybai.com/2006/01/08/debug-multiple-process-program-using-gdb/" target="_blank">gdb</a>或pstack查看core的内容，发现程序是在B+ Tree初始化时出的core。显然这又是一个内存违规访问的问题，且在Sparc上出现（x86 Linux上运行正常）十有八九与<a href="http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/" target="_blank">内存对齐</a>有关。</p>
<p>B+ Tree出问题首先让我想到了N年前的那个解决方法。我先查看了自定义的索引结构体(usr_idx)：</p>
<p>struct usr_idx {<br />
	&nbsp;&nbsp;&nbsp; unsigned int usr;<br />
	};</p>
<p>不过sizeof(usr_idx)无论是32bit编译还是64bit编译，其值都是4。那按照B+树原作者的说法，这显然不足以让B+树出现问题。事实也的确如此，32bit编译的程序在Sparc Solaris下运行良好，只是目前改为了64bit编译，才dump core，那问题到底出现在哪呢？</p>
<p>到这里，我也只能从代码着手了，把N年前没弄清楚的原因找出来，顺便也把这个存在了N年的Bug彻底解决掉，把这笔<a href="http://en.wikipedia.org/wiki/Technical_debt" target="_blank">技术债</a>还了。pstack的输出告诉我问题出在一个名为bptree_create_node的函数中，嫌疑最大的一处代码大致是这样的：</p>
<p>for (i = 0; i rank; i++) {<br />
	&nbsp;&nbsp;&nbsp; (elem_base(tree, tmp_bn, i))-&gt;key = key_base(tree, tmp_bn, i);<br />
	&nbsp;&nbsp;&nbsp; (elem_base(tree, tmp_bn, i))-&gt;pointer = NULL;<br />
	}</p>
<p>直觉告诉我问题出在elem_base这个宏里，elem_base的定义如下：</p>
<p>#define elem_base(tree, eb, index) ((xx_bptree_elem*)((char *)&amp;(eb)-&gt;e_base.mw_cp + ((SIZEOF_bptree_elem + (tree)-&gt;keysize))*(index)))</p>
<p>很显然这个定义最终是想得到一个xx_bptree_elem*类型的指针。从内存地址角度来说，我们会得到了一个内存地址，且这个地址被认为是一个xx_bptree_element元素的起始地址。那么是否所有地址作为xx_bptree_element元素的起始地址都合法呢？答案是不一定，至少在Sparc平台上不是所有地址都可以作为xx_bptree_elem的起始地址的。</p>
<p>那么什么样地址可以作为xx_bptree_element的起始地址呢？在Sparc上这取决于结构体的对齐系数。xx_bptree_elem结构的定义如下：</p>
<p>union mem_word {<br />
	&nbsp;&nbsp;&nbsp; void&nbsp; *mw_vp;<br />
	&nbsp;&nbsp;&nbsp; void (*mw_fp)(void);<br />
	&nbsp;&nbsp;&nbsp; char&nbsp; *mw_cp;<br />
	&nbsp;&nbsp;&nbsp; long&nbsp;&nbsp; mw_l;<br />
	&nbsp;&nbsp;&nbsp; double mw_d;<br />
	};<br />
	typedef union mem_word mem_word;<br />
	#define SIZEOF_mem_word (sizeof(mem_word))</p>
<p>struct xx_bptree_elem {<br />
	&nbsp;&nbsp;&nbsp; void&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *key;<br />
	&nbsp;&nbsp;&nbsp; void&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *pointer;<br />
	&nbsp;&nbsp;&nbsp; mem_word&nbsp;&nbsp; base;<br />
	};<br />
	typedef struct xx_bptree_item xx_bptree_item;<br />
	#define SIZEOF_bptree_elem&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (sizeof(xx_bptree_elem)-sizeof(mem_word))</p>
<p>在32bit编译的情况下，系统默认对齐系数为4(参见/usr/include/sys/isa_defs.h中的宏_MAX_ALIGNMENT)，则该结构体的对齐系数 = min(max(sizeof(key), sizeof(pointer), sizeof(base)), 4) = 4。这样xx_bptree_elem在32bit下的有效起始地址为可被4整除的内存地址。</p>
<p>而在用64bit编译时，系统默认的对齐系数为16（同参见isa_defs.h），但由于xx_bptree_elem中size最大的字段(base)的size为8，则结构体的对齐系数就等于8。即xx_bptree_elem元素的有效起始地址为可被8整除的地址。</p>
<p>好了，我们再回过头来看看elem_base宏在不同编译情况下能否总是返回合法的地址。</p>
<p>#define elem_base(tree, eb, index) ((xx_bptree_elem*)((char *)&amp;(eb)-&gt;e_base.mw_cp + ((SIZEOF_bptree_elem + (tree)-&gt;keysize))*(index)))</p>
<p>这个宏中有三个元素决定返回地址，分别是&quot;基址&quot;：&amp;(eb)-&gt;e_base.mw_cp、偏移量SIZEOF_bptree_elem和(tree)-&gt;keysize。其中基址是另外一个结构体xx_bptree_node中一个mem_word类型字段的地址，你知道的，mem_word这种手法可以保证其起始地址严格按照其内部最大字段的对齐系数对齐的，也就是说mem_word的对齐系数与double的对齐系数一致，即无论是32bit编译还是64bit编译，其对齐系数都是8，也就是说我们可以确保这个&rdquo;基址&ldquo;是可以被8整除的；至于偏移量SIZEOF_bptree_elem，我们可以直接可以得出其大小：</p>
<p>32bit下，SIZEOF_bptree_elem = 8<br />
	64bit下，SIZEOF_bptree_elem = 16</p>
<p>可以看出无论是32bit还是64bit编译，SIZEOF_bptree_elem的值都是8的倍数；显然这两个值都不会影响elem_base最终返回地址的合法性。</p>
<p>现在剩下的就是(tree)-&gt;keysize了。keysize是由xx_bptree_init接口传进来的，它在上层实际上就是用户自定义的索引结构体的大小，显然这个大小不一定就是8的倍数。在我们的系统中，keysize = sizeof(usr_idx) =<br />
	4。这个keysize在32bit编译下是没有问题的，因为32bit编译只需要elem_base返回的地址可以被4整除即可，这也是为什么我们的程序在32bit编译下运行正常的原因。回想一下N年前的那个问题，其真正原因也就在这里：当时我定义的索引结构体的大小无法被4整除。在64bit编译下，keysize显然不能满足被8整除的要求，导致elem_base返回的地址只能被4整除。而xx_bptree_elem这个结构体的地址是严格要求必须可被8整除的。将一个只能被4整除而不能被8整除的地址强制转换为xx_bptree_elem元素地址并通过该强制类型转换后的地址访问xx_bptree_elem内部的元素显然就会导致core的出现了。</p>
<p>现在看来当初我的同事并未真正理解该B+ tree为何要求用户自定义结构体的大小必须为4的整数倍了，他只是通过现象得到了那条经验罢了，这笔技术债务也就从那时留了下来。</p>
<p>解决该问题并不难，作为基础库，我们无论如何都不应该依赖用户的自觉，我们在接口实现中增加一个转换就可以解决这一隐藏了若干年的Bug，将外面传入的keysize经align_word转换后再赋给tree-&gt;keysize，这样就可以保证elem_base始终返回合法的地址了。</p>
<p>突然想起了那句话：&rdquo;出来混，总是要还的&ldquo;，我们欠的<a href="http://en.wikipedia.org/wiki/Technical_debt" target="_blank">技术债务</a>也不例外。</p>
<p style='text-align:left'>&copy; 2011 &#8211; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/07/21/pay-for-a-tech-debt-of-several-year-ago/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>为函数添加enter和exit级trace</title>
		<link>https://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/</link>
		<comments>https://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/#comments</comments>
		<pubDate>Wed, 13 Jul 2011 15:46:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[共享库]]></category>
		<category><![CDATA[动态库]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[调试]]></category>
		<category><![CDATA[链接器]]></category>
		<category><![CDATA[静态库]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/07/13/%e4%b8%ba%e5%87%bd%e6%95%b0%e6%b7%bb%e5%8a%a0enter%e5%92%8cexit%e7%ba%a7trace/</guid>
		<description><![CDATA[<p>日常开发中，我们为了辅助程序调试常常在每个函数的出入口(entry/exit)增加Trace，一般我们多用宏来实现这些Trace语句，例如：<br />
<br />
#ifdef XX_DEBUG_<br />
<br />
#define TRACE_ENTER() printf("Enter %s\n", __FUNCTION__)<br />
<br />
#define TRACE_EXIT() printf("Exit %s...</p>]]></description>
			<content:encoded><![CDATA[<p>日常开发中，我们为了辅助程序调试常常在每个函数的出入口(entry/exit)增加Trace，一般我们多用宏来实现这些Trace语句，例如：</p>
<p>#ifdef XX_DEBUG_<br />
	#define TRACE_ENTER() printf(&quot;Enter %s\n&quot;, __FUNCTION__)<br />
	#define TRACE_EXIT() printf(&quot;Exit %s\n&quot;, __FUNCTION__)<br />
	#else<br />
	#define TRACE_ENTER()<br />
	#define TRACE_EXIT()<br />
	#endif</p>
<p>有了TRACE_ENTER和TRACE_EXIT后，你就可以在你的函数中使用它们了。例如：<br />
	void foo(&#8230;) {<br />
	&nbsp;&nbsp;&nbsp; TRACE_ENTER();<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; TRACE_EXIT();<br />
	}</p>
<p>这样你就可以很容易看到函数的调用关系。不过这种手法用起来却不轻松。首先你需要在每个函数中手工加入TRACE_ENTER和TRACE_EXIT，然后再利用XX_DEBUG_宏控制其是否生效。特别是对于初期未添加函数级Enter/Exit Trace的项目，后期加入工作量很大。</p>
<p>不过<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/" target="_blank">Gcc</a>给我们提供了另外一种方便的手法：使用GCC的-finstrument-functions选项。-finstrument-functions使得GCC在生成代码时自动为每个函数在入口和出口生成__cyg_profile_func_enter和__cyg_profile_func_exit两个函数调用。我们要做的就是给出一份两个函数的实现即可。最简单的实现莫过于打印出被调用函数的地址了：</p>
<p>/* func_trace.c */<br />
	__attribute__((no_instrument_function))<br />
	void __cyg_profile_func_enter(void *this_fn, void *call_site) {<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;enter func =&gt; %p\n&quot;, this_fn);<br />
	}</p>
<p>__attribute__((no_instrument_function))<br />
	void __cyg_profile_func_exit(void *this_fn, void *call_site) {<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;exit func &lt;= %p\n&quot;, this_fn);<br />
	}</p>
<p>我们将这两个函数放入libfunc_trace.so：gcc -fPIC -shared -o libfunc_trace.so func_trace.c</p>
<p>我们为下面例子添加enter/exit级Trace：</p>
<p>/* example.c */<br />
	static void foo2() {</p>
<p>}</p>
<p>void foo1() {<br />
	&nbsp;&nbsp;&nbsp; foo2();<br />
	}</p>
<p>void foo() {<br />
	&nbsp;&nbsp;&nbsp; chdir(&quot;/home/tonybai&quot;);<br />
	&nbsp;&nbsp;&nbsp; foo1();<br />
	}</p>
<p>int main(int argc, const char *argv[]) {<br />
	&nbsp;&nbsp;&nbsp; foo();<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>$ gcc -g example.c -o example -finstrument-functions<br />
	$ LD_PRELOAD=libfunc_trace.so example<br />
	enter func =&gt; 0&#215;8048524<br />
	enter func =&gt; 0x80484e5<br />
	enter func =&gt; 0x80484b2<br />
	enter func =&gt; 0&#215;8048484<br />
	exit func &lt;= 0&#215;8048484<br />
	exit func &lt;= 0x80484b2<br />
	exit func &lt;= 0x80484e5<br />
	exit func &lt;= 0&#215;8048524</p>
<p>不过只输出函数地址很难让人满意，根据这些地址我们无法得知到底对应的是哪个函数。那我们就尝试一下将地址转换为函数名后再输出，这方面GNU依旧给我们提供了工具，它就是addr2line。addr2line是<a href="http://www.gnu.org/s/binutils" target="_blank">binutils</a>包中的一个工具，它可以根据提供的地址在可执行文件中找出对应的函数名、对应的源码文件名以及行数。我们改造一下func_trace.c中的两个函数的实现：</p>
<p>/* func_trace.c */<br />
	static char path[PATH_MAX];</p>
<p>__attribute__((constructor))<br />
	static void executable_path_init() {<br />
	&nbsp;&nbsp;&nbsp; char&nbsp;&nbsp;&nbsp; buf[PATH_MAX];</p>
<p>&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));<br />
	&nbsp;&nbsp;&nbsp; memset(path, 0, sizeof(path));</p>
<p>#ifdef _SOLARIS_TRACE<br />
	&nbsp;&nbsp;&nbsp; getcwd(buf, PATH_MAX);<br />
	&nbsp;&nbsp;&nbsp; sprintf(path, &quot;%s/%s&quot;, buf, getexecname());<br />
	#elif _LINUX_TRACE<br />
	&nbsp;&nbsp;&nbsp; readlink(&quot;/proc/self/exe&quot;, path, PATH_MAX);<br />
	#else<br />
	&nbsp;&nbsp;&nbsp; #error &quot;The OS has not been supported!&quot;<br />
	#endif<br />
	}</p>
<p>__attribute__((no_instrument_function))<br />
	void __cyg_profile_func_enter(void *this_fn, void *call_site) {<br />
	&nbsp;&nbsp;&nbsp; char buf[PATH_MAX];<br />
	&nbsp;&nbsp;&nbsp; char cmd[PATH_MAX];</p>
<p>&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));<br />
	&nbsp;&nbsp;&nbsp; memset(cmd, 0, sizeof(cmd));</p>
<p>&nbsp;&nbsp;&nbsp; sprintf(cmd, &quot;addr2line %p -e %s -f|head -1&quot;, this_fn, path);</p>
<p>&nbsp;&nbsp;&nbsp; FILE *ptr = NULL;<br />
	&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));</p>
<p>&nbsp;&nbsp;&nbsp; if ((ptr = popen(cmd, &quot;r&quot;)) != NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fgets(buf, PATH_MAX, ptr);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;enter func =&gt; %p:%s&quot;, this_fn, buf);<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; (void) pclose(ptr);<br />
	}</p>
<p>__attribute__((no_instrument_function))<br />
	void __cyg_profile_func_exit(void *this_fn, void *call_site) {<br />
	&nbsp;&nbsp;&nbsp; char buf[PATH_MAX];<br />
	&nbsp;&nbsp;&nbsp; char cmd[PATH_MAX];</p>
<p>&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));<br />
	&nbsp;&nbsp;&nbsp; memset(cmd, 0, sizeof(cmd));</p>
<p>&nbsp;&nbsp;&nbsp; sprintf(cmd, &quot;addr2line %p -e %s -f|head -1&quot;, this_fn, path);</p>
<p>&nbsp;&nbsp;&nbsp; FILE *ptr = NULL;<br />
	&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));</p>
<p>&nbsp;&nbsp;&nbsp; if ((ptr = popen(cmd, &quot;r&quot;)) != NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fgets(buf, PATH_MAX, ptr);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;exit func &lt;= %p:%s&quot;, this_fn, buf);<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; (void) pclose(ptr);<br />
	}</p>
<p>在我的Ubuntu 10.04下，我们编译和执行</p>
<p>$ gcc -D_LINUX_TRACE -fPIC -shared -o libfunc_trace.so func_trace.c<br />
	$ gcc -g example.c -o example -finstrument-functions<br />
	$ LD_PRELOAD=libfunc_trace.so example<br />
	$ example<br />
	enter func =&gt; 0&#215;8048524:main<br />
	enter func =&gt; 0x80484e5:foo<br />
	enter func =&gt; 0x80484b2:foo1<br />
	enter func =&gt; 0&#215;8048484:foo2<br />
	exit func &lt;= 0&#215;8048484:foo2<br />
	exit func &lt;= 0x80484b2:foo1<br />
	exit func &lt;= 0x80484e5:foo<br />
	exit func &lt;= 0&#215;8048524:main</p>
<p>关于这个实现，还有几点要说道说道：<br />
	首先libfunc_trace.so是<a href="http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/" target="_blank">动态链接</a>到你的可执行程序中的，那么如何获取addr2line所需要的文件名是一个问题；另外考虑到可执行程序中可能会调用chdir这样的接口更换当前工作路径，所以我们需要在初始化时就得到可执行文件的绝对路径供addr2line使用，否则会出现无法找到可执行文件的错误。在这里我们利用了GCC的__attribute__扩展：<br />
	__attribute__((constructor))</p>
<p>这样我们就可以在main之前就将可执行文件的绝对路径获取到，并在__cyg_profile_func_enter和__cyg_profile_func_exit中直接引用这个路径。</p>
<p>在不同平台下获取可执行文件的绝对路径的方法有不同，像Linux下可以利用&quot;readlink /proc/self/exe&quot;获得可执行文件的绝对路径，而Solaris下则用getcwd和getexecname拼接。</p>
<p>再总结一下，如果你想使用上面的libfunc_trace.so，你需要做的事情有：<br />
	1、将编译好的libfunc_trace.so放在某路径下，并export LD_PRELOAD=PATH_TO_libfunc_trace.so/libfunc_trace.so<br />
	2、你的环境下需要安装binutils的addr2line<br />
	3、你的应用在编译时增加-finstrument_functions选项。</p>
<p>我已经将这个小工具包放到了Google Code上，有兴趣的朋友可以在<a href="http://code.google.com/p/bigwhite-code/" target="_blank">这里</a>下载完整源码包（20110715更新：支持输出函数所在源文件路径以及所在行号，前提编译你的程序时务必加上-g选项）。</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>也谈共享库2</title>
		<link>https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/</link>
		<comments>https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/#comments</comments>
		<pubDate>Thu, 07 Jul 2011 03:35:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[共享库]]></category>
		<category><![CDATA[动态库]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[链接器]]></category>
		<category><![CDATA[静态库]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/07/07/%e4%b9%9f%e8%b0%88%e5%85%b1%e4%ba%ab%e5%ba%932/</guid>
		<description><![CDATA[<p>我之前写过一篇名为"也谈共享库"的博文，对共享库的查找和符号解析机制做了还算比较详细的说明，不过百密一疏，总有一些意想不到的情况发生。这不今天我又遇到了一个有关共享库的新问题，这里将这个问题及其解决过程记录下来，也算是对上一篇文章中未涉及内容的一个...</p>]]></description>
			<content:encoded><![CDATA[<p>我之前写过一篇名为&quot;<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/" target="_blank">也谈共享库</a>&quot;的博文，对共享库的查找和<a href="http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/" target="_blank">符号解析</a>机制做了还算比较详细的说明，不过百密一疏，总有一些意想不到的情况发生。这不今天我又遇到了一个有关共享库的新问题，这里将这个问题及其解决过程记录下来，也算是对上一篇文章中未涉及内容的一个补充吧。</p>
<p>N年前我曾参与过部门的一个可复用系统的设计开发，当时我们设计了一种插件式的系统结构，其中所谓的&quot;插件&quot;是以共享库的形式提供。主程序通过读取配置，获取插件的位置，并在运行期利用dlopen动态加载插件(.so文件)，用dlsym查找、绑定并执行.so中的特定业务函数。</p>
<p>我们可以用下面样例代码简单地模拟出这种设计：</p>
<p>/*<br />
	&nbsp;* 主程序 main.c */<br />
	&nbsp;* 需include dlfcn.h、link.h等标准头文件，这里省略<br />
	&nbsp;*/<br />
	typedef int (*PLUGIN_MAIN_FUNC)(void);</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; void *handle = NULL;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *dso = &quot;plugin.so&quot;;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *func_name = &quot;plugin_main&quot;;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PLUGIN_MAIN_FUNC func = NULL;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handle = dlopen(dso, RTLD_LAZY);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (handle == NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;dlopen (%s)失败!\n&quot;, dso);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; func = dlsym(handle, func_name);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (func == NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;dlsym (%s)失败!\n&quot;, func_name);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d\n&quot;, my_add(4, 8));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d\n&quot;, func());</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dlclose(handle);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>以下my_add接口可以理解为主程序所使用的底层库，亦可为plugin程序使用。</p>
<p>/* add.h */<br />
	int my_add(int a, int b);</p>
<p>/* add.c */<br />
	int my_add(int a, int b) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return a + b;<br />
	}</p>
<p>/* 以下是plugin.so的源代码 */<br />
	/* plugin.c */<br />
	#include &quot;add.h&quot;</p>
<p>int plugin_main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return my_add(5, 6);<br />
	}</p>
<p>在Solaris 10 for x86, Gcc 3.4.6下编译plugin和主程序：<br />
	$ gcc -fPIC -shared -o plugin.so plugin.c<br />
	$ gcc -o main main.c add.c -ldl</p>
<p>执行main，我们得到了期望的结果：<br />
	12<br />
	11</p>
<p>将该样例拿到Solaris 10 for sparc平台上编译运行一样没有问题。最后，我把源代码拿到了我的<a href="http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/" target="_blank">Ubuntu 10.04</a>下，Gcc的版本是4.4.3，编译过程很顺利，但是执行的结果却与预期不符，执行main后得到的结果是：<br />
	12<br />
	main: symbol lookup error: ./plugin.so: undefined symbol: my_add</p>
<p>居然提示无法找到符号my_add！在Solaris上明明可以正确执行的程序，搬到Linux下却出错。这种问题十分对我的胃口，开始&ldquo;破案&rdquo;^_^。</p>
<p>我们先来收集证据，先看看plugin.so的符号表：</p>
<p>$ nm -f sysv plugin.so</p>
<p>Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Value&nbsp;&nbsp; Class&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp;&nbsp; Line&nbsp; Section<br />
	&#8230; &#8230;<br />
	my_add&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp; U&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NOTYPE|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; |*UND*<br />
	plugin_main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |0000046c|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|0000002c|&nbsp;&nbsp;&nbsp;&nbsp; |.text</p>
<p>my_add符号的确是Undefined（未定义）的，也就是说在主程序获得my_add符号并准备执行时(注意我们在dlopen的参数中使用了RTLD_LAZY)，加载器需要在此时为my_add这个符号寻找其定义。main这个可执行文件中是定义了这个符号的，我们可以通过nm命令看到这一情况：</p>
<p>$ nm -f sysv main</p>
<p>Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Value&nbsp;&nbsp; Class&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp;&nbsp; Line&nbsp; Section<br />
	&#8230; &#8230;<br />
	main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |080484f4|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|000000ee|&nbsp;&nbsp;&nbsp;&nbsp; |.text<br />
	my_add&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |080485e4|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|0000000e|&nbsp;&nbsp;&nbsp;&nbsp; |.text</p>
<p>按照我原先的理解，加载器在为my_add符号寻找定义时，是应该可以将main中的my_add定义与之相绑定的，但是事实却是加载器无法找到my_add这个符号的定义，导致执行出错。</p>
<p>你也许会立刻想出一种解决方法，将add.c与plugin.c一起编译：<br />
	$ gcc -fPIC -shared -o plugin.so plugin.c<br />
	这样编译后的plugin.so中的确有了my_add的定义：</p>
<p>$ nm -f sysv plugin.so<br />
	Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Value&nbsp;&nbsp; Class&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp;&nbsp; Line&nbsp; Section<br />
	&#8230; &#8230;<br />
	my_add&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |00000498|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|0000000e|&nbsp;&nbsp;&nbsp;&nbsp; |.text<br />
	plugin_main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |0000046c|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|0000002c|&nbsp;&nbsp;&nbsp;&nbsp; |.text</p>
<p>main也可以正确执行了。但这显然不是我么想要的结果。作为一个plugin，其编译时很可能无法得到add.c或者add.c对应的静态库，也许只能得到add.h，所以这种方法很局限。另外这个方案也在plugin源码与主程序源码之间无端建立一个耦合，导致后续的一些不方便。</p>
<p>接下来，我使用readelf工具对main的ELF格式做了一次全面检查：<br />
	$ readelf -a main</p>
<p>在readelf输出的内容中，我发现了两个&ldquo;符号表(Symbol table)&rdquo;：</p>
<p>Symbol table &#039;.dynsym&#039; contains 9 entries:<br />
	&nbsp;&nbsp; Num:&nbsp;&nbsp;&nbsp; Value&nbsp; Size Type&nbsp;&nbsp;&nbsp; Bind&nbsp;&nbsp; Vis&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Ndx Name<br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 3: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlclose@GLIBC_2.0">dlclose@GLIBC_2.0</a> (2)<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 4: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:__libc_start_main@GLIBC_2.0">__libc_start_main@GLIBC_2.0</a> (3)<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 5: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlsym@GLIBC_2.0">dlsym@GLIBC_2.0</a> (2)<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 6: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlopen@GLIBC_2.1">dlopen@GLIBC_2.1</a> (4)<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 7: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:printf@GLIBC_2.0">printf@GLIBC_2.0</a> (3)<br />
	&nbsp;&nbsp; &#8230; &#8230;</p>
<p>Symbol table &#039;.symtab&#039; contains 70 entries:<br />
	&nbsp;&nbsp; Num:&nbsp;&nbsp;&nbsp; Value&nbsp; Size Type&nbsp;&nbsp;&nbsp; Bind&nbsp;&nbsp; Vis&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Ndx Name<br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; 52: 080485e4&nbsp;&nbsp;&nbsp; 14 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp;&nbsp; 14 my_add<br />
	&nbsp;&nbsp;&nbsp; 54: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlclose@@GLIBC_2.0">dlclose@@GLIBC_2.0</a><br />
	&nbsp;&nbsp;&nbsp; 55: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:__libc_start_main@@GLIBC">__libc_start_main@@GLIBC</a>_<br />
	&nbsp;&nbsp;&nbsp; 58: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlsym@@GLIBC_2.0">dlsym@@GLIBC_2.0</a><br />
	&nbsp;&nbsp;&nbsp; 60: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:dlopen@@GLIBC_2.1">dlopen@@GLIBC_2.1</a><br />
	&nbsp;&nbsp;&nbsp; 63: 00000000&nbsp;&nbsp;&nbsp;&nbsp; 0 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp; UND <a href="mailto:printf@@GLIBC_2.0">printf@@GLIBC_2.0</a><br />
	&nbsp;&nbsp;&nbsp; 68: 080484f4&nbsp;&nbsp; 238 FUNC&nbsp;&nbsp;&nbsp; GLOBAL DEFAULT&nbsp;&nbsp; 14 main<br />
	&nbsp;&nbsp; &#8230; &#8230;</p>
<p>仔细观察一下这两个表，你会发现有些函数是重复的，如dlopen在两个表里面都有，但my_add却只在.symtab中出现。也许问题就在这里。迅速翻阅了一些资料（比如&quot;<a href="http://book.douban.com/subject/4083265" target="_blank">Linkers and Loaders</a>&quot;），发现这两个符号表的功用确有不同。</p>
<p>.symtab中的符号也称为normal symbol，表中包含了所有ELF文件中涉及的所有符号，用于普通的链接器；.dynsym中的符号则是由未定义的动态链接符号以及该ELF文件本身导出(export)的用于动态链接的符号组成。说到这里，头绪渐渐明晰。在本例中，.symtab这个普通符号表中虽然包含了my_add符号，但是这并不能说明my_add是main导出的用于动态链接的符号(dynamic symbol)，只有my_add出现在.dynsym中时，加载器才能在符号查找时看到my_add，而本例中my_add恰恰没有出现在.dynsym表中。</p>
<p>使用nm -D命令，我们也可以查看.dynsym符号表：<br />
	$ nm -D -f sysv main</p>
<p>Symbols from main:</p>
<p>Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Value&nbsp;&nbsp; Class&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp;&nbsp; Line&nbsp; Section<br />
	&#8230; &#8230;<br />
	dlclose&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp; U&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; |*UND*<br />
	dlopen&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp; U&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; |*UND*<br />
	dlsym&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp; U&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; |*UND*<br />
	&#8230; &#8230;</p>
<p>让我奇怪的是为何在Solaris上main的执行是没有问题的呢，换到Solaris下，我们同样使用nm -D查看上面的main文件：</p>
<p>$ nm -D main<br />
	main:</p>
<p>[Index]&nbsp;&nbsp; Value&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp; Type&nbsp; Bind&nbsp; Other Shndx&nbsp;&nbsp; Name<br />
	&#8230;<br />
	[10]&nbsp;&nbsp;&nbsp; | 134547364|&nbsp;&nbsp;&nbsp;&nbsp; 305|FUNC |GLOB |0&nbsp;&nbsp;&nbsp; |10&nbsp;&nbsp;&nbsp;&nbsp; |main<br />
	[19]&nbsp;&nbsp;&nbsp; | 134547353|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 11|FUNC |GLOB |0&nbsp;&nbsp;&nbsp; |10&nbsp;&nbsp;&nbsp;&nbsp; |my_add<br />
	&#8230;</p>
<p>从结果可以看出，Solaris上main文件的.dynsym符号表中是包含了my_add符号的，这也就是main在Solaris上可以正常执行的原因。</p>
<p>难道与Gcc版本有关系？Solaris上的Gcc是3.4.6，而我的Ubuntu上的Gcc是4.4.3。&quot;<a href="http://book.douban.com/subject/4251048" target="_blank">Binary Hacks</a>&quot;一书中曾提到使用-rdynamic选项可为可执行文件留下可用于动态连接的符号。向gcc传入-rdynamic，则链接器会得到-export-dynamic选项。我在Ubuntu下试一下这个选项：</p>
<p>$ gcc -o main main.c add.c -ldl -rdynamic<br />
	$ main<br />
	12<br />
	11</p>
<p>问题果然解决了。我们再用nm -D查看一下这个新版main文件：<br />
	$ nm -D -f sysv main<br />
	Symbols from main:</p>
<p>Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Value&nbsp;&nbsp; Class&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp;&nbsp; Line&nbsp; Section<br />
	&#8230; &#8230;<br />
	dlsym&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp; U&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; |*UND*<br />
	main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |080486e4|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|000000ee|&nbsp;&nbsp;&nbsp;&nbsp; |.text<br />
	my_add&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |080487d4|&nbsp;&nbsp; T&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FUNC|0000000e|&nbsp;&nbsp;&nbsp;&nbsp; |.text<br />
	&#8230;</p>
<p>果然，.dynsym表扩大了好多，my_add也出现在了该表中，这样在main执行时加载器就可以为plugin.so中的my_add符号绑定到其定义了。</p>
<p>我在Solaris下的gcc命令行上也增加-rdynamic选项，但编译后得到的结果却是：<br />
	gcc: unrecognized option `-rdynamic&#039;</p>
<p>查看了<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/" target="_blank">Gcc</a>官方的<a href="http://gcc.gnu.org/onlinedocs" target="_blank">Manual</a>后发现，在Gcc 4.1.2版本之前的Manual中都无法找到-rdynamic这一选项，也就是说这个选项是后加入Gcc中的。之前我们看到Solaris上main文件的dynsym表默认就包含了my_add，而4.1.2版本后的Gcc则默认不将自定义的全局函数导出。这是为什么呢？也许是为了提升可执行程序动态链接的性能，这个性能估计与dynsym表的大小不无关系。表越小，需要动态链接的符号越少，符号解析和绑定的速度也就越快；同时由于该表的内容需要在执行时加载到内存，这样表越小，加载的时间以及内存的占用也都很少，所以GCC更改了策略，默认选择不导出自定义的全局符号，并提供-rdynamic让程序员选择是否导出已定义的符号用于动态链接。</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
