Shared posts

21 May 00:41

RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描 - Milo Yip

by Milo Yip

现在的 CPU 都提供了[单指令流多数据流][单指令流多数据流](single instruction multiple data, SIMD)指令集。最常见的是用于大量的浮点数计算,但其实也可以用在文字处理方面。其中,SSE4.2 包含了一些专为字符串而设的指令。我们通过使用这些指令,可以大幅提升某些 JSON 解析的性能。

本文链接:RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描,转载请注明。

19 May 11:40

40个Java集合面试问题和答案

by 李叶

1.Java集合框架是什么?说出一些集合框架的优点?

每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector、Stack、HashTable和Array。随着集合的广泛使用,Java1.2提出了囊括所有集合接口、实现和算法的集合框架。在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久。它还包括在Java并发包中,阻塞接口以及它们的实现。集合框架的部分优点如下:

(1)使用核心集合类降低开发成本,而非实现我们自己的集合类。

(2)随着使用经过严格测试的集合框架类,代码质量会得到提高。

(3)通过使用JDK附带的集合类,可以降低代码维护成本。

(4)复用性和可操作性。

2.集合框架中的泛型有什么优点?

Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。

3.Java集合框架的基础接口有哪些?

Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。

Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。

List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。

Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。

一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。

4.为何Collection不从Cloneable和Serializable接口继承?

Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。

当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。

在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。

5.为何Map接口不继承Collection接口?

尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。

如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。

6.Iterator是什么?

Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

7.Enumeration和Iterator接口的区别?

Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。但是,与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。

迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。

8.为何没有像Iterator.add()这样的方法,向集合中添加元素?

语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。

9.为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标?

它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。

10.Iterater和ListIterator之间有什么区别?

(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。

(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。

(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

11.遍历一个List有哪些不同的方式?

List<String> strList = new ArrayList<>();
//使用for-each循环
for(String obj : strList){
  System.out.println(obj);
}
//using iterator
Iterator<String> it = strList.iterator();
while(it.hasNext()){
  String obj = it.next();
  System.out.println(obj);
}

使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。

12.通过迭代器fail-fast属性,你明白了什么?

每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。

13.fail-fast与fail-safe有什么区别?

Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。

14.在迭代一个集合的时候,如何避免ConcurrentModificationException?

在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。

15.为何Iterator接口没有具体的实现?

Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部类。

这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而CopyOnWriteArrayList迭代器是fail-safe的。

16.UnsupportedOperationException是什么?

UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。

17.在Java中,HashMap是如何工作的?

HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。下面的图片解释了详细内容。

其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是32,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。

18.hashCode()和equals()方法有何重要性?

HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:

(1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。

(2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。

19.我们能否使用任何类作为Map的key?

我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:

(1)如果类重写了equals()方法,它也应该重写hashCode()方法。

(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。

(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。

(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。

比如,我有一个类MyKey,在HashMap中使用它。

//传递给MyKey的name参数被用于equals()和hashCode()中
MyKey key = new MyKey('Pankaj'); //assume hashCode=1234
myHashMap.put(key, 'Value');
// 以下的代码会改变key的hashCode()和equals()值
key.setName('Amit'); //assume new hashCode=7890
//下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null
myHashMap.get(new MyKey('Pankaj'));

那就是为何String和Integer被作为HashMap的key大量使用。

20.Map接口提供了哪些不同的集合视图?

Map接口提供三个集合视图:

(1)Set keyset():返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

(2)Collection values():返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

(3)Set<Map.Entry<K,V>> entrySet():返回一个map钟包含的所有映射的一个集合视图。这个集合受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

21.HashMap和HashTable有何不同?

(1)HashMap允许key和value为null,而HashTable不允许。

(2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,HashTable适合多线程环境。

(3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。

(4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。

(5)HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用CocurrentHashMap。

22.如何决定选用HashMap还是TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

23.ArrayList和Vector有何异同点?

ArrayList和Vector在很多时候都很类似。

(1)两者都是基于索引的,内部由一个数组支持。

(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。

(3)ArrayList和Vector的迭代器实现都是fail-fast的。

(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。

以下是ArrayList和Vector的不同点。

(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。

(2)ArrayList比Vector快,它因为有同步,不会过载。

(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。

24.Array和ArrayList有何区别?什么时候更适合用Array?

Array可以容纳基本类型和对象,而ArrayList只能容纳对象。

Array是指定大小的,而ArrayList大小是固定的。

Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。

(1)如果列表的大小已经指定,大部分情况下是存储和遍历它们。

(2)对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。

(3)如果你要使用多维数组,使用[][]比List<List<>>更容易。

25.ArrayList和LinkedList有何区别?

ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。

(1)ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。

(2)与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。

(3)LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。

26.哪些集合类提供对元素的随机访问?

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

27.EnumSet是什么?

java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。它也提供了一些有用的方法,比如copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。

28.哪些集合类是线程安全的?

Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。

29.并发集合类是什么?

Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。

30.BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

31.队列和栈是什么,列出它们的区别?

栈和队列两者都被用来预存储数据。java.util.Queue是一个接口,它的实现类在Java并发包中。队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。

栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。

Stack是一个扩展自Vector的类,而Queue是一个接口。

32.Collections类是什么?

Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。

33.Comparable和Comparator接口是什么?

如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。但是,在大多数实际情况下,我们想根据不同参数进行排序。比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段进行排序,我们不能根据对象排序的需要选择字段。Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。

34.Comparable和Comparator接口有何区别?

Comparable和Comparator接口被用来对对象集合或者数组进行排序。Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。

Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。

35.我们如何对一组对象进行排序?

如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。

36.当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?

在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。

37.我们如何从给定集合那里创建一个synchronized的集合?

我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。

38.集合框架里实现的通用算法有哪些?

Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。

39.大写的O是什么?举几个例子?

大写的O描述的是,就数据结构中的一系列元素而言,一个算法的性能。Collection类就是实际的数据结构,我们通常基于时间、内存和性能,使用大写的O来选择集合实现。比如:例子1:ArrayList的get(index i)是一个常量时间操作,它不依赖list中元素的数量。所以它的性能是O(1)。例子2:一个对于数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元素。

40.与Java集合框架相关的有哪些最好的实践?

(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。

(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。

(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。

(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。

(5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。

(6)尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。

相关文章

18 May 06:24

老鸟向新手讲解各种编程比赛

by demo

过去十年间我一直在参加各种编程比赛。我参加了很多比赛,更重要的是,我参加了很多不同类型的比赛。我的冒险起始于经典算法,之后我转到了优化问题。目前我主要参加机器学习竞赛(作为兼职),我也参加一些只是为了好玩的比赛。

考虑到有像我这样广泛经历的人并不多,我想我应该写一个编程(算法?)竞赛流行类型的(相对)简短的总结。这不是一个完整的列表,我只关注了那些最流行的,并且在我看来最有用的竞赛。

这篇文章的结构是以竞赛类型,而不是竞赛网站为导向的。也就是说,我把有相似特征的网站/竞赛归类,而不是呈现给你们仅仅加了我个人描述的随机网站。由于有些网站提供了几种类型的竞赛,我不得不多次列出它们。

如果你对编程竞赛的世界感到困惑或者好奇,这篇文章能给你关于这个主题的详尽综述。

经典算法

有时又被称为二元问题或(带贬义地)消遣算法。提供给你问题陈述,你的目标是写一个通常很短的程序:读入输入,处理它并输出计算结果。任何东西都是按给定的问题陈述来的。大部分情况下, 你要提交程序源代码,源代码在远程服务器上编译并运行一些对你隐藏的数据。下面要提到的所有竞赛的共性是:你的程序要么被认为是完全正确的,要么是完全错误的,没有中间结果。

经典算法竞赛通常是编程竞赛的入门级别。难度从答案显而易见,到只有问题作者知道怎么解答。对于新手程序员,它们提供了小的挑战,是很棒的实践练习。在更高的级别,它们需要高度的专注,很好的长期记忆,解决问题的技巧以及很深的专业知识。如果能在这些竞赛中做好,你会开发出很多可以轻易迁移到计算机科学其他领域的技能。前提是你专注于开发纯粹的技能,而不是把时间花在解决成百上千的问题上以期望今后能遇到类似的问题。(译者注:即不要用题海战术)

竞赛联盟

有两个主要的网站定期举办比赛。第一个是 TopCoder(以下简称 TC),举办Single Round Matches (SRMs)。第二个是 CodeForces(以下简称 CF)。

这两个网站很相似。一周会举办几次比赛(即将到来的比赛链接:TCCF)。比赛持续大约2小时。每种比赛都会用几个(TC 是 3 个,CF 是 5 个)为这轮特别准备的原创经典问题(通常,对于所有的比赛都是这样的,但我想澄清这一点)。你只需提交程序源代码,程序会在服务器上远程执行。你的程序如果想要被认定为正确,需要在指定的时间和内存限制下运行,产生正确的输出。美中不足的是,只有在比赛结束后你才知道你的解答是否正确。当这轮结束后,你的排名会得到更新(类似于 ELO 等级系统),这很准确地代表了你目前的水平。赛前参赛者(根据他们当前的排名)会被分到两个不同的赛区,每个赛区使用根据参赛者水平量身定做的题目。另外,赛后你可以看其他人的提交,也会发布题解(解释了问题的正确解答)。所有的题目会添加到练习区,这样你之后就可以去解答那些你在比赛中未能解答的题目。这使得这两个网站成为完美的训练场。

这些是它们的相似之处,那么不同之处是什么呢?TC 最大的缺点是(除了糟糕的管理,不过那又是另一回事了)它使用一个陈旧的 Java applet 来用于竞赛。尽管这使得参加第一次比赛比它应有的过程更复杂(TC 那设计糟糕的网站对此也没什么帮助),但从长远看来比“HTML5”界面也差不了多少。另一个差别是两个网站的题目的关注点稍有不同。TC 上的任务通常向解决问题倾斜,有时候甚至像谜题,然而 CF 就包含很多基于数据结构的“filler”(缺乏想象力的代名词)问题,但这主要取决于问题作者。基于我的经历,TC问题(平均来说)稍微有趣些,但CF的题目更为多样—主要是因为CF每轮有更多的题目。两种竞赛都有一个特性是提供寻找他人代码中bug的机会(如果你成功找到了会获得额外的分数),但是CF对此的设计很可怕(译者注:表示对此没什么感受),最好就是完全忽略它。

年度现场比赛

有四个大的比赛:Google Code JamFacebook Hacker CupYandex.AlgorithmTopCoder Open(算法组)。它们都很相似。为了取得现场比赛资格,你要参加一系列在线资格赛。通常都是采用淘汰赛的形式,在随后的每一轮减少参赛者的数量。通常对参赛者的身份没有限制(除非你的国家不幸在美国的禁止入境名单上)。每一轮时间都很短,在 90 分钟和 3 个小时之间,只考经典的二元问题(译者注:前面提过了)。如果你取得了现场赛资格,他们会支付你参加比赛的交通食宿费用。他们也给获胜者奖金,但对于大多数人来说,你能赢得的最重要的东西是旅行本身,或者(通过比赛)在顶级 IT 公司找到工作。

这些比赛之间有一些小的差别(题目质量、晋级结构、提交系统),但是它们有两个共性:晋级其中任何一个都非常难(如果你没有两年经验,想要取得资格是极不可能的,即使你聪明且专注于此)。另一个是有个人(Gennady Korotkevich)在2014年赢了个大满贯。考虑到所有这些竞赛要想赢都有很高不确定性,这是一个难以置信的壮举。

伯乐在线补充:Gennady Korotkevich年仅11岁时便参加国际信息学奥林比克竞赛,创造了最年轻选手的记录。在2007-2012年间,总共取得6枚奥赛金牌;2013年美国计算机协会编程比赛冠军队成员;2014年Facebook黑客杯冠军得主。截止目前,稳居俄编程网站Codeforces声望第一的宝座,在TopCoder算法竞赛中暂列榜眼位置。

在线判题系统

这些有很多了,仅列举一些:SPOJUVATimus。通常,它们主要作为过去的ICPC(译者注:即 International Collegiate Programming Contest, 国际大学生程序设计竞赛)竞赛题的存档。

你在这些网站上花时间原因不外乎那么几个:

  • 一:你觉得你很不擅长某些类型的题目,因此你在寻找一些很难的特定题目(如果你以成为世界前100为目标,这甚至都不应该发生),而你在其他地方又找不到这些题目。
  • 二:你参加了一场比赛,那些题目被上传到判题网站,你想继续尝试那些你没有解决的问题或者尝试其他的解题方案。
  • 三:你的算法课老师很懒,他讨厌他的工作。

优化问题

这种问题的标准例子是旅行商问题。这些问题以这样的事实来刻画:根据你的解答质量,你得到不同的分数。它们被(或者至少应该被)设计为不可能获得完美的分数。

有优化问题的比赛通常持续时间更长,因此相对于经典算法比赛关注于不同的技能集,比如心理耐力、时间管理或开箱即用的问题解决技能。通常,优化问题要求你是个多面手,但是你不必擅长于任何特定领域。它们也更接近于做实际研究,因此如果你想把你的职业生涯与软件开发以外的事绑在一起,尝试下它们是个不错的主意。

马拉松比赛

不幸的是,没有太多地方可以让你磨练在这个领域的技能。Topcoder有马拉松比赛,但是他们不再定期举办比赛,几年前他们还这么做。在马拉松比赛旗下举办的大多数比赛都属于机器学习类(在下面描述),例外是年度Top Coder Open比赛中的马拉松类,现场决赛和资格赛都使用优化问题。有传闻他们想重新举办定期比赛,但到目前为止什么都还没改变。

幸运的是,尽管没有太多比赛,TopCCoder有过去比赛题目的存档。因此如果你的目标只是更擅长优化题目,你可以练习这些旧题。好处是你可以获得所有的优胜解答,另外在这些过去的比赛对应的论坛通常有个“发布你的方法”的帖子。

在一些其他的比赛中你也可以遇到优化问题。但不幸的是,我不认为有可以与马拉松比赛相提并论的。这个类别下最流行的比赛大概是 Al Zimmermann 的编程比赛,但是那的题目都很浅,不太有趣。

机器学习

有时被错误地称为数据科学。这是个有大把钱的地方,因为对专长于机器学习的人才有很大需求,至少相比对经典和优化问题人才的需求来说是很大的。

相比其他类型的比赛,机器学习需要的知识要多得多,通常来说也不那么有趣。尽管如此,这些比赛仍然是目前在这个领域获得一些亲身实践经历的最容易的方式。如果你需要一些动力,记住需要机器学习技能的工作的报酬在整个IT界是最高的那部分。

马拉松比赛(再次)

我可能提到上面提过的网站/比赛。Topcoder把优化和机器学习问题结合到一个类别。说得更准确些,在某些时候马拉松比赛类别扩张为包括机器学习比赛。

Topcoder的机器学习比赛通常只以一种方式呈现。由于整个Topcoder是个众包平台(再加上围绕它构建的社区),客户有时候会有用“简单的”软件竞赛不能解决的问题。在有些情况下,他们处理的问题可以包装成一个机器学习比赛,给表现最好的方案大笔钱。由于机器学习比赛仍然是马拉松比赛,整个提交系统的运行方式是和优化问题完全一样的。唯一的差别是机器学习比赛通常不加入到练习区。

Kaggle

Kaggle是个很大程度围绕机器学习比赛而建的网站。我将关注于TC和Kaggle的差别。最大的差别是在Kaggle你只提交你的解答的输出,而不是整个程序,这有很多后果。首先,你获得整个数据集,对你可以使用的语言/库没有限制。没有任何时间限制,如果你想(并且支付得起)的话,甚至可以用整个集群来计算结果。由于大家不用提交源代码,在比赛结束后你不能查看解答。另一方面,社区要活跃得多。当比赛还在进行中时,会有很多“练习赛”,在此人们分享他们的解答。Kaggle的比赛时间也长得多(对于有奖金的比赛通常是两个月)。

总体来说,两个网站各自为稍有不同的目的服务,各有利弊。对于那些对高层知识,而不是各种机器学习技术如何工作的低层内部机制更感兴趣的人来说,Kaggle应该更为友好,然而TC对于有很强算法背景的人来说可能更好。另一种看待这个的方式是,在Kaggle你(通常)使用工具,而在马拉松比赛你(通常)写你自己的工具。

欢乐24小时

15年前,第一届 Challenge24 组织起来了。我不会细说它的历史,因为实际上我也所知甚少。但我会把这个比赛描述为“24小时的疯狂”。它是在布达佩斯举办的年度团队比赛。你有大概15道题。题目的范围很广:经典、优化、TCP/IP之上的游戏、计算机视觉、声音分析、谜题,以及难以用语言描述的东西。你把你自己的硬件带到比赛现场,整个比赛是离线完成的。老实说,这是我参加过的最有趣的比赛。即使我上次参加时发烧了,我还是要这么说。

Challenge24启发了Deadline24,这反过来又启发了Marathon24(两个都是在波兰举行)。它们是Challenge24的简化版,但仍然很有趣。它们没有大量题目,通常只有三个游戏,每轮游戏和比赛同时开始。例如,过去的一个游戏是30个选手同时玩经典的行星游戏。

由于只有有限数量的队伍会被邀请到决赛,这些比赛都举办在线资格赛。由于这些比赛并没有其他年度比赛那么流行,实际上即使没有任何显著的算法(甚至编程)背景,也很可能获得其中之一的资格。

还有一个额外的年度比赛值得一提,它和这些24小时比赛有些类似:互联网问题解决比赛。这个比赛也有很多任务,尽管它们更类似于经典算法。

其他

还有两个大的网站被遗漏了。第一个是CodeChef,有两种不同的月度比赛。两种都是经典算法比赛。另一个是HackerRank,混合了在线判题系统和举办各种类型一次性比赛的功能。我遗漏它们的原因是,它们都因问题陈述的质量低而闻名(译者注:译者使用过这两个网站,对此没什么感受)。考虑到有大量的其他比赛,通常你应该避开它们(尽管有很少的例外)。

如果你还在读高中,对你来说最重要的比赛是国际信息学奥林匹克竞赛。每个国家都有其国家级奥林匹克竞赛和自己的规则。在很多国家,在国家级奥林匹克竞赛中表现优异是进入理想大学最简单和安全的方式。

国际大学生程序设计竞赛(ACM/ICPC)是“经典算法”比赛,在这里所有的比赛/网站中历史最为悠久。它是面向学生的团队比赛。每支队由同一大学的3个人组成。ICPC的主要目标是编程在世界范围的普及。这是用复杂的多层获得资格制度和(同样复杂)的合格标准来达成的。这保证了世界总决赛中队伍的多样性。作为取舍,根据你所在的地方,要想晋级世界总决赛,要么是令人吃惊地容易,要么是近乎不可能。专门针对ICPC来练习可能是把你的时间投资在编程竞赛中最坏的方式,因为它提升的技能集是最窄的,唯一的例外是你生活在那些“幸运的”(译者注:即容易晋级的)地区。

鼓励奖要颁给Hello World Open,因为它创造了年度最大的扯淡比赛,有着很成功的销售计划。如果你仔细看,你会看到在比赛发起后,他们甚至在维基页面加入了他们的链接。我猜如果你制作收入最高的手机app,你会很熟悉这个。说真的,Topcoder应该向他们学习。

另一个鼓励奖要颁给 Imagine Cup。我参加了四次Imagine Cup。两次作为参赛者,一次作为裁判,最后一次作为助手。这些年我看着它从一个聚集多个不同领域(初创,数字艺术,算法/人工智能)的学生的创新年度比赛,到微软产品的傻逼公关。裁判经常是因政治原因被挑选的,绝对不能胜任他们的工作。并且所有没有直接促进微软产品的类别都停掉了。事实是,Imagine Cup是我参加过的最好的现场赛事。同时,现在我不会推荐任何人参加它们。

Project Euler是一种特别的在线判题系统(偏重数学),这在于你以编程为工具来解决问题,而不在于它本身。如果你真的想在没有时间压力的情况下做一些编程题,我建议你做Project Euler,而不是我提到的其他判题系统。另外,由于它很流行,如果你在某个问题卡住了,在网上找到一些帮助要容易得多。

编注:推荐几篇相关文章

老鸟向新手讲解各种编程比赛,首发于博客 - 伯乐在线

18 May 00:36

文章: Java NIO通信框架在电信领域的实践

by 李林锋

1. 华为电信软件技术架构演进

1.1. 电信软件

从广义上看电信软件的范围非常广,细分实际可以分为两大类:系统软件和业务应用软件。

系统软件包括路由器底层的信令机软件、手机操作系统等,业务应用软件主要包括客户关系管理CRM、网上营业厅、融合计费OCS和各类消息网关,例如短信网关、彩信网关等。

本文重点介绍电信业务应用软件的技术变迁历史,以及华为电信软件架构演进和Java NIO框架在技术变迁中起到的关键作用。

1.2. 华为电信软件的技术演进史

1.2.1. C和C++主导的第一代架构

在2005年之前,华为软件公司的核心系统主要以C和C++进行开发,由于C和C++开源框架非常少,加之那个时代开源社区并不成熟,大部分的系统都采用自研开发,包括协议栈、系统调度、数据访问层和日志。

大多数的软件都运行在服务端,对外提供高性能、低时延和高并发的系统调用,协议栈大多数都采用电信私有协议栈,对于部分有前台管理Portal的系统,往往基于原生的HTML或者Struts等WEB框架开发,通过HTTP协议与后端进行交互,它的逻辑架构图如下:

图1-1 华为电信软件V1版逻辑架构图

在那个时代,电信软件绝大多数都部署在高性能的小机中,处理各种信令、电信私有协议的接入和解析、复杂业务逻辑处理,系统对处理性能、时延、多核处理的要求非常高。当时Java主流版本还是JDK 1.4.2(1.4.X),它在传统的Web应用、电子商务网站和政企系统中得到了比较广泛的应用,但是在电信领域并没有大的应用,主要原因如下:

1) 在JDK1.5之前的早期版本中,Java在多线程编程、并行处理等方面能力很差,无法在电信软件服务器端使用;

2) JDK 1.4.X对非阻塞I/O的支持并不好,相关NIO编程的可参考资料和开源框架很少,传统的阻塞I/O模型在电信高性能、高可靠场景中力不从心;

3) 业界很少有Java高性能服务端处理成功的案例,大家普遍对Java 支持电信级应用场景持怀疑态度;

4) 那个时代电信领域的开发者都是C/C++出身,大家对新技术和语言有种天生的排斥。

2005年之后,随着Java在各领域的快速普及和应用,以及基于Java的各种开源框架井喷式增长,华为越来越多的产品开始尝试切换到Java进行开发,主流架构随即演进到了以Java为主的V2版本。

1.2.2. Spring + Struts + Tomcat 的第二代架构

2005年-2008年间,华为电信软件大多数产品线都切换到Java语言进行新产品的设计和开发,当时随着Struts的MVC模式以及Spring对J2EE复杂企业应用对象生命周期的配置式管理的流行,华为电信软件绝大多数产品采用基于Spring + Struts + Tomcat模式进行开发,数据访问中间件主要采用iBatis和Hibernate,它的逻辑架构如下所示: 

图1-2 华为电信软件V2 MVC版逻辑架构图

切换到以Spring + J2EE容器为基础技术框架之后,应用开发的难度迅速降低,开发效率获得了极大提升。短短1-2年时间,公司大多数以C/C++的项目切换到了Java语言和V2 架构上。

1.2.3. 以SOA为中心的第三代架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。

随着电信业务的快速发展,电信原有系统和新建设系统之间存在语言、协议、运行环境等诸多差异。如何整合异构系统,实现高效企业集成,也是一个巨大的挑战,此时,企业服务总线(ESB)是个不错的选择。

为了满足电信业务的需求,华为软件研发了SOA中间件,它的逻辑架构图如下:

图1-3 以SOA服务化为核心的V3架构

SOA是一种粗粒度、松耦合的以服务为中心的架构,接口之间通过定义明确的协议和接口进行通信。SOA帮助工程师们站在一个新的高度理解企业级架构中各种组件的开发和部署形式,它可以帮助企业系统架构师以更迅速、可靠和可重用的形式规划整个业务系统。相比于传统的垂直架构,SOA能够更加从容的应对复杂企业系统集成和需求的快速变化。

1.2.4. 以分布式、云化为核心的第四代架构

随着业务的不断发展,硬件成本的下降,基于X86架构的廉价硬件 + 分布式软件的模式在互联网行业得到了大规模应用,分布式架构日趋成熟。

从运营商业务看,尽管高性能的小机仍然是标配,但是运营商业务向数字化转型和云化降成本逐渐成为一种趋势。

传统SOA架构中的一些缺陷逐步暴露,例如企业集成总线ESB是实体总线,性能线性扩展能力有限;硬件负载均衡器的压力越来越大,不断扩容导致硬件成本增加;随着业务规模的不断增长,传统的数据库、配置中心等逐渐成为单点瓶颈等。

我们需要通过新的分布式架构来解决电信软件面临的成本高、性能无法线性增长等问题,以分布式技术为核心构建的华为分布式中间件应用而生,它主要包括如下组件:

1) 高性能、低时延的分布式服务框架;

2) 分布式消息队列MQ;

3) 分布式缓存;

4) 分布式数据库访问中间件,支持跨库操作,支持异构数据库;

5) 软负载SLB;

6) 分布式日志采集和检索(Flume + ELK);

7) 分布式实时流式计算框架;

8) 分布式消息跟踪系统;

9) 其它......

自从亚马逊的云计算服务面世以来,云计算技术作为应对笨重的传统IT架构的战略,已经成为越来越多的政府和企业的选择, “云”已经成为ICT技术和服务领域的“常态”。

运营商基础设施云化的主要原因如下:

1) IT资源规模比较大,如何高效的使用这些设备,提升效率,虚拟化是个不错的选择;

2) 资源的孤岛现象是比较严重,大部分IT系统,依然采用传统的竖井式的建设模式,IT系统的资源无法在跨系统间进行共享,同时因为各系统建各系统的特点,使得资源的利用率非常低,各个业务有峰值的时候,虽然业务之间峰值不在一块,但是依然起不到消峰的作用,占用的资源比较大;

3) 系统的压力也是不均衡,资源由于没法共享,只能采用被动的采购,使得更大容量的设备采购,来应付电信业务增长所需要的扩容;

4) 系统部署的周期长、运维也比较难。

为了满足运营商云化的需求,华为相继研发了IaaS、PaaS等用于支撑运营商IT和基础设施云化,下面让我们一起看下华为软件云化后的逻辑架构:

图1-4 以分布式、云化为核心的V4架构

第四代技术架构以分布式、云化为核心,相比于前三代架构,它的核心特性如下:

1) 采用分布式技术构建,所有的中间件都没有单点,支持线性增长和弹性伸缩;

2) 以微服务架构为核心,打造电信领域的DevOps(结合华为PaaS平台);

3) 由传统的SOA Governance 向微服务治理和自治演进,提升服务治理效能;

4) 分布式日志采集 + 实时流式计算框架,更快的故障定界,提升大规模、分布式系统中的运维效率;

5) 业务和数据的拆分,分而治之,通过分布式中间件服务向业务屏蔽拆分细节;

6) 架构云化带来的巨大优势:资源池提升硬件利用率、DevOps提升开发和运维效率、应用和服务的自动弹性伸缩、应用和服务故障自动恢复、高HA、自动化运维等。

1.3. 架构演进中的技术

随着架构的演进,Java的版本也在不断升级,技术堆栈不断更新,EJB、Spring、RMI、MQ、Node.js、NIO、Hadoop等。在众多技术堆栈中,我印象最深的就是Java的NIO类库以及业界成熟NIO框架的使用,它在华为软件架构演进中发挥了重大作用,曾立下了汗马功劳。现在,以Netty为代表的NIO框架已经在华为平台产品和业务产品中得到了广泛的应用。

作为华为软件公司最早使用Java NIO技术进行平台开发、2009年即在全球商用成功的亲历者和实践者,我想跟大家分享下Java NIO框架在华为软件以及电信领域的应用和实践。

2. Java NIO 技术的引入

2.1. BIO带给我们深深伤痛

在2008年的时候,我参与设计和开发的一个电信系统在月初出帐期,总是发生大量的连接超时和读写超时异常,业务的失败率相比于平时高了很多,报表中的很多指标都差强人意。后来经过排查,发现问题的主要原因出现在下游网元的处理性能上,月初的时候BSS出帐,在出帐期间BSS系统运行缓慢,由于双方采用了同步阻塞式的HTTP+XML进行通信,导致任何一方处理缓慢都会影响对方的处理性能。按照故障隔离的设计原则,对方处理速度慢或者不回应答,不应该影响系统的其他功能模块或者协议栈,但是在同步阻塞I/O通信模型下,这种故障传播和相互影响是不可避免的,很难通过业务层面解决。

受限于当时Tomcat和Servlet的同步阻塞I/O模型,以及在Java领域异步HTTP协议栈的技术积累不足,当时我们并没有办法完全解决这个问题,只能通过调整线程池策略和HTTP超时时间来从业务层面做规避。由于我们的系统是一个全国级的一级系统,需要对接周边各个网元,同时服务器资源十分有限,即便采用了高峰期间动态修改超时时间、优化线程池模型等多种措施,效果依然差强人意。

每当跟客户开会的时候,客户总会提起这个话题:别人响应慢,为啥会导致你的系统阻塞呢,可以返回处理其它消息啊?!我无法跟客户解释技术细节,因为同步阻塞I/O仅仅是Java I/O的一种实现,操作系统支持非阻塞I/O和异步I/O。

站在技术的角度,客户的需求是合理并且也是可以实现的,当时受限于经验以及其它技术原因,我们无法从根本上解决客户提出的问题,团队有种深深的挫败感,Java BIO同步阻塞通信导致的各种问题给我留下了一些心理阴影,一直挥之不去。

2.2. BIO模型存在的问题

传统同步阻塞通信面临的主要问题如下:

1) 性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制;

2) 可靠性问题:由于I/O操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致I/O线程被挂住,阻塞时间无法预测;

3) 可维护性问题:I/O线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差

传统同步阻塞通信的处理模型图如下:

图2-1 同步阻塞通信模型处理模型图

从上图我们可以看出,每当有一个新的客户端接入,服务端就需要创建一个新的线程(或者重用线程池中的可用线程),每个客户端链路对应一个线程。当客户端处理缓慢或者网络有拥塞时,服务端的链路线程就会被同步阻塞,也就是说所有的I/O操作都可能被挂住,这会导致线程利用率非常低,同时随着客户端接入数的不断增加,服务端的I/O线程不断膨胀,直到无法创建新的线程。

同步阻塞I/O导致的问题无法在业务层规避,必须改变I/O模型,才能从根本上解决这个问题。

2.3. 历史性的引入Java NIO

2.3.1. Java NIO被冷落的原因

从2004年JDK1.4首次提供NIO 1.0类库到现在,已经过去了整整10年。JSR 51的设计初衷就是让Java能够提供非阻塞、具有弹性伸缩能力的异步I/O类库,从而结束Java在高性能服务器领域的不利地位。然而,在相当长的一段时间里,Java的NIO编程并没有流行起来,究其原因如下。

  1. 大多数高性能服务器,被C和C++语言盘踞,由于它们可以直接使用操作系统的异步I/O能力,所以对JDK的NIO并不关心;
  2. 移动互联网尚未兴起,基于Java的大规模分布式系统极少,很多中小型应用服务对于异步I/O的诉求不是很强烈;
  3. 高性能、高可靠性领域,例如银行、证券、电信等依然以C++为主导,Java充当打杂的角色,NIO暂时没有用武之地;
  4. 当时主流的J2EE服务器,几乎全部基于同步阻塞I/O构建,例如Servlet、Tomcat等,由于它们应用广泛,如果这些容器不支持NIO,用户很难具备独立构建异步协议栈的能力;
  5. 异步NIO编程门槛比较高,开发和维护一款基于NIO的协议栈对很多中小型公司来说像是一场噩梦;
  6. 业界NIO框架不成熟,很难商用;
  7. 国内研发界对NIO的陌生和认识不足,没有充分重视。

基于上述几种原因,NIO编程的推广和发展长期滞后,特别是国内,在2009年的时候,几乎无法搜到国内企业成功使用NIO技术的案例。

2.3.2. 华为软件引入Java NIO的原因

从2008年开始,华为软件研发了Java版的业务网关,并迅速占领国内外市场。随着产品的推广,在一些高并发、大业务量的局点相继出现了几起事故,质量回溯的结果都指向了Java BIO通信模型,包括Servlet 2.X的同步阻塞I/O、Tomcat 5.X(当时没使用5.5)的同步I/O、以及其它的同步I/O协议栈。

问题根因已经很清楚,如果不改变同步I/O通信模型,问题会继续发生,对于运营商而言,这是不可能接受的事情。自古华山一条路,即然业界没有成熟的异步I/O协议栈,那我们就自研。

2009年初,由于对技术的热爱,我作为业务骨干被领导派去参加异步高性能网关平台的研发工作,与两位资深的架构师(其中一位工作20年,做华为交换机出身)一起合作。这是我第一次全面接触异步I/O编程和高性能电信级协议栈的开发,眼界大开——异步高性能内部协议栈、异步HTTP、异步SOAP、异步SMPP……所有的协议栈都是异步非阻塞模式。

后来的性能测试表明:基于Reactor模型统一调度的长连接和短连接协议栈,无论是性能、可靠性还是可维护性,都可以“秒杀”传统基于BIO开发的应用服务器和各种协议栈,这种指标差异本质上是一种技术代差。

2009年底,基于异步网关平台研发的XX业务产品在海外某运营商成功上线,它的高性能、低时延和高HA令局方惊叹不已,原来准备的20多台小机最后只使用了3台,为客户节省了一大把$。

2.3.3. 那些年我们踩过的NIO “坑”

在我从事异步NIO编程的2009年,业界还没有成熟的NIO框架,那个时候Mina刚刚开始起步,功能和性能都达不到商用标准。最困难的是,国内Java领域的异步通信还没有流行,整个业界的积累都非常少。那个时候资料匮乏,能够交流和探讨的圈内人很少,一旦踩住“地雷”,就需要夜以继日地维护。在随后2年多的时间里,经历了10多次的在通宵、凌晨被一线的运维人员电话吵醒等种种磨难之后,我们自研的NIO框架才逐渐稳定和成熟。期间,解决的BUG总计20~30个。

为了解决这些Bug,2年中我经历了10几个通宵,现在回想起来仍历历在目,特别是JDK epoll 空轮询导致的 CPU 100%,更是坑中之坑(JDK NIO类库的Bug),曾令多少产品中招,包括Mina、Netty、Jetty等著名开源框架。

2.4. 从Java 原生NIO到NIO框架

从2011年开始,华为软件主要使用NIO框架Netty进行通信软件的开发,为什么不继续使用原声的Java NIO类库,下面给出了我们切换的原因。

2.4.1. JAVA 原生NIO类库的复杂性

在分析Java原生NIO类库复杂性之前,我们首先看下最简单的NIO服务端和客户端创建流程。

最简单的NIO服务端创建程序流程:

图2-2 Java NIO 服务端创建流程

最简单的Java NIO 客户端创建流程如下:

图2-3 Java NIO 客户端创建流程

现在我们总结一下为什么不建议开发者直接使用JDK的NIO类库进行开发,具体原因如下:

(1)NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;

(2)需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;

(3)可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大;

(4)JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。该BUG以及与该BUG相关的问题单可以参见以下链接内容:

◎ http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933

◎ http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719

异常堆栈如下:

java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:210)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69)
        - locked <0x0000000750928190> (a sun.nio.ch.Util$2)
        - locked <0x00000007509281a8> (a java.util.Collections$ UnmodifiableSet)
        - locked <0x0000000750946098> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80)
        at net.spy.memcached.MemcachedConnection.handleIO(Memcached Connection.java:217)
        at net.spy.memcached.MemcachedConnection.run(MemcachedConnection. java:836)

2.4.2. 以Netty为代表的NIO框架已经成熟

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

通过对Netty的分析,我们将它的优点总结如下:

1) API使用简单,开发门槛低;

2) 功能强大,预置了多种编解码功能,支持多种主流协议;

3) 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;

4) 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;

5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;

7) 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架,它也是华为公司首选的Java NIO通信框架,公司已经将其纳入到公司级的优选开源第三方软件库中。

3. Netty在电信领域的实践

电信行业软件的几个特点:

1) 高可靠性:5个9;

2) 高性能、低时延;

3) 大规模组网:例如中国移动、Telfonica 拉美十三国、沃达丰等,业务组网规模都非常大;

4) 复杂的网络形态:对接不同设备提供商的网元和系统。

3.1. 高性能、低时延

3.1.1. 非阻塞I/O模型

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

我们采用Netty的NIO传输模式来提升I/O操作的效率,节省线程等其它资源开销,它的模型如下所示:

图3-1  Netty的非阻塞I/O调度模型

3.1.2. 高性能的序列化框架

在华为软件,对于序列化框架的选择,我们遵循如下几个原则:

1) 序列化后的码流大小(网络带宽的占用);

2) 序列化&反序列化的性能(CPU、内存等资源占用);

3) 是否支持跨语言(异构系统的对接和开发语言切换);

4) 高并发调用时的性能,是否随着线程并发数线性增长。

基于上述的指标,目前最常用的选择是:Google的ProtoBuf和Apache的Thrift。

Netty原生提供了对ProtoBuf序列化框架的支持,它的优点如下:

1) 在谷歌内部长期使用,产品成熟度高;

2) 跨语言、支持多种语言,包括C++、Java和Python;

3) 编码后的消息更小,更加有利于存储和传输;

4) 编解码的性能非常高;

5) 支持不同协议版本的前向兼容;

6) 支持定义可选和必选字段。

Netty ProtoBuf 服务端开发示例如下:

// 配置服务端的NIO线程组

EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
		    ServerBootstrap b = new ServerBootstrap();
		    b.group(bossGroup, workerGroup)
			    .channel(NioServerSocketChannel.class)
			    .option(ChannelOption.SO_BACKLOG, 100)
			    .handler(new LoggingHandler(LogLevel.INFO))
			    .childHandler(new ChannelInitializer<SocketChannel>() {
				@Override
				public void initChannel(SocketChannel ch) {
				    ch.pipeline().addLast(
					    new ProtobufVarint32FrameDecoder());
				    ch.pipeline().addLast(
					    new ProtobufDecoder(
						    SubscribeReqProto.SubscribeReq
							    .getDefaultInstance()));
	ch.pipeline().addLast(
					    new ProtobufVarint32LengthFieldPrepender());
				    ch.pipeline().addLast(new ProtobufEncoder());
				    ch.pipeline().addLast(new SubReqServerHandler());
				}
		    });

Thrift相对复杂一些,需要将编解码框架从Thrift中剥离出来,然后利用Netty编解码框架的扩展性定制实现,在此不再赘述。

3.1.3. 收敛的Reactor线程模型

Java线程采用抢占的方式争夺CPU等资源,当系统线程数增大到一定量级之后,性能不仅没有提升,反而下降。

对于大型的电信应用,如果使用Tomcat等做Web容器,为了保证吞吐量和性能,HTTP线程池的最大线程数往往配置为1024。在系统运行期间我们Dump线程堆栈,发现大量的线程竞争,这不仅导致HTTP协议栈的性能下降,更影响其它业务处理线程的执行效率。

使用Netty之后,我们通过控制NioEventLoopGroup的NioEventLoop个数来收敛线程,防止线程膨胀。NioEventLoop聚合了一个多路复用器Selector,可以高效的处理N个Channel,它的线程模型如下:

图3-1 Netty Reactor线程模型

3.1.4. 其它优化

为了进一步提升性能,降低时延,我们还采用了其它一些优化措施,总结如下:

1) 使用Netty 4的内存池,减少业务高峰期ByteBuf频繁创建和销毁导致的GC频率和时间;

2) 在程序中充分利用Netty提供的“零拷贝”特性,减少额外的内存拷贝,例如使用CompositeByteBuf而不是分别为Head和Body各创建一个ByteBuf对象;

3) TCP参数的优化,设置合理的Send和Receive Buffer,通常建议值为64K - 128K;

4) 软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量;

5) 无锁化串行开发理念:使用Netty 4.X版本,天生支持串行化处理;业务开发过程中,遵循Netty 4的线程模型优化理念,防止人为增加线程竞争。

3.2. 高HA

3.2.1. 内存保护

为了提升内存的利用率,Netty提供了内存池和对象池。但是,基于缓存池实现以后需要对内存的申请和释放进行严格的管理,否则很容易导致内存泄漏。  

如果不采用内存池技术实现,每次对象都是以方法的局部变量形式被创建,使用完成之后,只要不再继续引用它,JVM会自动释放。但是,一旦引入内存池机制,对象的生命周期将由内存池负责管理,这通常是个全局引用,如果不显式释放JVM是不会回收这部分内存的。

对于Netty的用户而言,使用者的技术水平差异很大,一些对JVM内存模型和内存泄漏机制不了解的用户,可能只记得申请内存,忘记主动释放内存,特别是JAVA程序员。

为了防止因为用户遗漏导致内存泄漏,Netty在Pipe line的尾Handler中自动对内存进行释放。

缓冲区内存溢出保护:做过协议栈的读者都知道,当我们对消息进行解码的时候,需要创建缓冲区。缓冲区的创建方式通常有两种:

1) 容量预分配,在实际读写过程中如果不够再扩展;

2) 根据协议消息长度创建缓冲区。

在实际的商用环境中,如果遇到畸形码流攻击、协议消息编码异常、消息丢包等问题时,可能会解析到一个超长的长度字段。笔者曾经遇到过类似问题,报文长度字段值竟然是2G多,由于代码的一个分支没有对长度上限做有效保护,结果导致内存溢出。系统重启后几秒内再次内存溢出,幸好及时定位出问题根因,险些酿成严重的事故。

Netty提供了编解码框架,因此对于解码缓冲区的上限保护就显得非常重要。下面,我们看下Netty是如何对缓冲区进行上限保护的:

1) 在内存分配的时候指定缓冲区长度上限;

2) 在对缓冲区进行写入操作的时候,如果缓冲区容量不足需要扩展,首先对最大容量进行判断,如果扩展后的容量超过上限,则拒绝扩展;

3) 在解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存。

3.2.2. 流量整形

电信系统一般都有多个网元组成,例如参与短信互动,会涉及到手机、基站、短信中心、短信网关、SP/CP等网元。不同网元或者部件的处理性能不同。为了防止因为浪涌业务或者下游网元性能低导致下游网元被压垮,有时候需要系统提供流量整形功能。

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

流量整形的原理示意图如下:

图3-2 Netty 流量整形原理图

Netty内置两种流量整形策略,可以方便的被用户添加和使用:

1) 全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期;

2) 单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略,整形参数与全局流量整形相同。

3.2.3. 其它可靠性措施

其它比较重要的可靠性措施如下:

1) 客户端连接超时控制策略;

2) 链路断连重连策略;

3) 链路异常关闭资源释放;

4) 解码失败的异常处理策略;

5) 链路异常的捕获和处理;

6) I/O线程的释放。

3.3. 华为软件对Netty的优化

针对电信软件的特点,结合华为软件的实际业务需求,我们对Netty进行了优化,优化的策略如下:

1) 能够通过Netty提供的扩展点实现的,通过扩展点实现,不自己造轮子;

2) 不允许修改Netty源码,基于Netty提供的接口,开发华为自己的优化实现类;

3) 华为优化实现类独立打包,对原Netty类库是二进制依赖,不修改Netty原类库;

4) 服务端和客户端创建时,传递华为自己的实现类参数。

华为的主要优化点总结如下:

1) 安全性改造:满足华为公司安全红线、电信运营商的安全需求相关改造;

2) 可靠性增强:消息发送队列的上限保护、链路中断时缓存中待发送消息回调通知业务、增加错误码、异常日志打印抑制、I/O线程健康度检测等;

3) 可定位性增强:单链路的网络吞吐量、接收发送的速度、接收\发送的总字节数、畸形码流检测机制、解码时延超大消息日志打印等。

4. 作者简介

李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通信软件的设计和开发工作,有7年NIO设计和开发经验,精通Netty、Mina等NIO框架和平台中间件,现任华为软件平台架构部架构师,《Netty权威指南》作者。目前从事华为下一代中间件和PaaS平台的架构设计工作。

联系方式:新浪微博 Nettying  微信:Nettying 微信公众号:Netty之家

对于Netty学习中遇到的问题,或者认为有价值的Netty或者NIO相关案例,可以通过上述几种方式联系我。


感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

17 May 07:21

一大波你可能不知道的 Linux 网络工具

by changqi

如果要在你的系统上监控网络,那么使用命令行工具是非常实用的,并且对于 Linux 用户来说,有着许许多多现成的工具可以使用,如: nethogs, ntopng, nload, iftop, iptraf, bmon, slurm, tcptrack, cbm, netwatch, collectl, trafshow, cacti, etherape, ipband, jnettop, netspeed 以及 speedometer。

鉴于世上有着许多的 Linux 专家和开发者,显然还存在其他的网络监控工具,但在这篇教程中,我不打算将它们所有包括在内。

上面列出的工具都有着自己的独特之处,但归根结底,它们都做着监控网络流量的工作,只是通过各种不同的方法。例如 nethogs 可以被用来展示每个进程的带宽使用情况,以防你想知道究竟是哪个应用在消耗了你的整个网络资源; iftop 可以被用来展示每个套接字连接的带宽使用情况,而像 nload 这类的工具可以帮助你得到有关整个带宽的信息。

1) nethogs
nethogs 是一个免费的工具,当要查找哪个 PID (注:即 process identifier,进程 ID) 给你的网络流量带来了麻烦时,它是非常方便的。它按每个进程来分组带宽,而不是像大多数的工具那样按照每个协议或每个子网来划分流量。它功能丰富,同时支持 IPv4 和 IPv6,并且我认为,若你想在你的 Linux 主机上确定哪个程序正消耗着你的全部带宽,它是来做这件事的最佳的程序。

一个 Linux 用户可以使用 nethogs 来显示每个进程的 TCP 下载和上传速率,可以使用命令 nethogs eth0 来监控一个指定的设备,上面的 eth0 是那个你想获取信息的设备的名称,你还可以得到有关正在传输的数据的传输速率信息。

对我而言, nethogs 是非常容易使用的,或许是因为我非常喜欢它,以至于我总是在我的 Ubuntu 12.04 LTS 机器中使用它来监控我的网络带宽。

例如要想使用混杂模式来嗅探,可以像下面展示的命令那样使用选项 -p:

nethogs -p wlan0

假如你想更多地了解 nethogs 并深入探索它,那么请毫不犹豫地阅读我们做的关于这个网络带宽监控工具的整个教程。

2) nload
nload 是一个控制台应用,可以被用来实时地监控网络流量和带宽使用情况,它还通过提供两个简单易懂的图表来对流量进行可视化。这个绝妙的网络监控工具还可以在监控过程中切换被监控的设备,而这可以通过按左右箭头来完成。

正如你在上面的截图中所看到的那样,由 nload 提供的图表是非常容易理解的。nload 提供了有用的信息,也展示了诸如被传输数据的总量和最小/最大网络速率等信息。

而更酷的是你只需要直接运行 nload 这个工具就行,这个命令是非常的短小且易记的:

nload

我很确信的是:我们关于如何使用 nload 的详细教程将帮助到新的 Linux 用户,甚至可以帮助那些正寻找关于 nload 信息的老手。

3) slurm
slurm 是另一个 Linux 网络负载监控工具,它以一个不错的 ASCII 图来显示结果,它还支持许多按键用以交互,例如 c 用来切换到经典模式, s 切换到分图模式, r 用来重绘屏幕, L 用来启用 TX/RX 灯(注:TX,发送流量;RX,接收流量) ,m 用来在经典分图模式和大图模式之间进行切换, q 退出 slurm。

在网络负载监控工具 slurm 中,还有许多其它的按键可用,你可以很容易地使用下面的命令在 man 手册中学习它们。

man slurm

slurm 在 Ubuntu 和 Debian 的官方软件仓库中可以找到,所以使用这些发行版本的用户可以像下面展示的那样,使用 apt-get 安装命令来轻松地下载它:

sudo apt-get install slurm

我们已经在一个教程中对 slurm 的使用做了介绍,不要忘记和其它使用 Linux 的朋友分享这些知识。

4) iftop
当你想显示连接到网卡上的各个主机的带宽使用情况时,iftop 是一个非常有用的工具。根据 man 手册,iftop 在一个指定的接口或在它可以找到的第一个接口(假如没有任何特殊情况,它应该是一个对外的接口)上监听网络流量,并且展示出一个表格来显示当前的一对主机间的带宽使用情况。

通过在虚拟终端中使用下面的命令,Ubuntu 和 Debian 用户可以在他们的机器中轻易地安装 iftop:

sudo apt-get install iftop

在你的机器上,可以使用下面的命令通过 yum 来安装 iftop:

yum -y install iftop

 

5) collectl
collectl 可以被用来收集描述当前系统状态的数据,并且它支持如下两种模式:

  • 记录模式
  • 回放模式

记录模式 允许从一个正在运行的系统中读取数据,然后将这些数据要么显示在终端中,要么写入一个或多个文件或一个套接字中。

回放模式

根据 man 手册,在这种模式下,数据从一个或多个由记录模式生成的数据文件中读取。

Ubuntu 和 Debian 用户可以在他们的机器上使用他们默认的包管理器来安装 colletcl。下面的命令将为他们做这个工作:

sudo apt-get install collectl

还可以使用下面的命令来安装 collectl, 因为对于这些发行版本(注:这里指的是用 yum 作为包管理器的发行版本),在它们官方的软件仓库中也含有 collectl:

yum install collectl

6) Netstat
Netstat 是一个用来监控传入和传出的网络数据包统计数据的接口统计数据命令行工具。它会显示 TCP 连接 (包括上传和下行),路由表,及一系列的网络接口(网卡或者SDN接口)和网络协议统计数据。

Ubuntu 和 Debian 用户可以在他们的机器上使用默认的包管理器来安装 netstat。Netsta 软件被包括在 net-tools 软件包中,并可以在 shell 或虚拟终端中运行下面的命令来安装它:

sudo apt-get install net-tools

CentOS, Fedora, RHEL 用户可以在他们的机器上使用默认的包管理器来安装 netstat。Netstat 软件被包括在 net-tools 软件包中,并可以在 shell 或虚拟终端中运行下面的命令来安装它:

yum install net-tools

运行下面的命令使用 Netstat 来轻松地监控网络数据包统计数据:

netstat

更多的关于 netstat 的信息,我们可以简单地在 shell 或终端中键入 man netstat 来了解:

man netstat

 

7) Netload
netload 命令只展示一个关于当前网络荷载和自从程序运行之后传输数据总的字节数目的简要报告,它没有更多的功能。它是 netdiag 软件的一部分。

我们可以在 fedora 中使用 yum 来安装 Netload,因为它在 fedora 的默认软件仓库中。但假如你运行的是 CentOS 或 RHEL,则我们需要安装 rpmforge 软件仓库。

# yum install netdiag

Netload 是默认仓库中 netdiag 的一部分,我们可以轻易地使用下面的命令来利用 apt 包管理器安装 netdiag:

$ sudo apt-get install netdiag

为了运行 netload,我们需要确保选择了一个正在工作的网络接口的名称,如 eth0, eh1, wlan0, mon0等,然后在 shell 或虚拟终端中运行下面的命令:

$ netload wlan2

注意: 请将上面的 wlan2 替换为你想使用的网络接口名称,假如你想通过扫描了解你的网络接口名称,可以在一个虚拟终端或 shell 中运行 ip link show 命令。

8) Nagios
Nagios 是一个领先且功能强大的开源监控系统,它使得网络或系统管理员可以在服务器的各种问题影响到服务器的主要事务之前,发现并解决这些问题。 有了 Nagios 系统,管理员便可以在一个单一的窗口中监控远程的 Linux 、Windows 系统、交换机、路由器和打印机等。它会显示出重要的警告并指出在你的网络或服务器中是否出现某些故障,这可以间接地帮助你在问题发生前就着手执行补救行动。

Nagios 有一个 web 界面,其中有一个图形化的活动监视器。通过浏览网页 http://localhost/nagios/ 或 http://localhost/nagios3/ 便可以登录到这个 web 界面。假如你在远程的机器上进行操作,请使用你的 IP 地址来替换 localhost,然后键入用户名和密码,我们便会看到如下图所展示的信息:

 

9) EtherApe
EtherApe 是一个针对 Unix 的图形化网络监控工具,它仿照了 etherman 软件。它支持链路层、IP 和 TCP 等模式,并支持以太网, FDDI, 令牌环, ISDN, PPP, SLIP 及 WLAN 设备等接口,以及一些封装格式。主机和连接随着流量和协议而改变其尺寸和颜色。它可以过滤要展示的流量,并可从一个文件或运行的网络中读取数据包。

在 CentOS、Fedora、RHEL 等 Linux 发行版本中安装 etherape 是一件容易的事,因为在它们的官方软件仓库中就可以找到 etherape。我们可以像下面展示的命令那样使用 yum 包管理器来安装它:

yum install etherape

我们也可以使用下面的命令在 Ubuntu、Debian 及它们的衍生发行版本中使用 apt 包管理器来安装 EtherApe :

sudo apt-get install etherape

在 EtherApe 安装到你的系统之后,我们需要像下面那样以 root 权限来运行 etherape:

sudo etherape

然后, etherape 的 图形用户界面 便会被执行。接着,在菜单上面的 捕捉 选项下,我们可以选择 模式(IP,链路层,TCP) 和 接口。一切设定完毕后,我们需要点击 开始 按钮。接着我们便会看到类似下面截图的东西:

10) tcpflow
tcpflow 是一个命令行工具,它可以捕捉 TCP 连接(流)的部分传输数据,并以一种方便协议分析或除错的方式来存储数据。它重构了实际的数据流并将每个流存储在不同的文件中,以备日后的分析。它能识别 TCP 序列号并可以正确地重构数据流,不管是在重发还是乱序发送状态下。

通过 apt 包管理器在 Ubuntu 、Debian 系统中安装 tcpflow 是很容易的,因为默认情况下在官方软件仓库中可以找到它。

$ sudo apt-get install tcpflow

我们可以使用下面的命令通过 yum 包管理器在 Fedora 、CentOS 、RHEL 及它们的衍生发行版本中安装 tcpflow:

# yum install tcpflow

假如在软件仓库中没有找到它或不能通过 yum 包管理器来安装它,则我们需要像下面展示的那样从 http://pkgs.repoforge.org/tcpflow/ 上手动安装它:

假如你运行 64 位的 PC:

# yum install --nogpgcheck http://pkgs.repoforge.org/tcpflow/tcpflow-0.21-1.2.el6.rf.x86_64.rpm

假如你运行 32 位的 PC:

# yum install --nogpgcheck http://pkgs.repoforge.org/tcpflow/tcpflow-0.21-1.2.el6.rf.i686.rpm

我们可以使用 tcpflow 来捕捉全部或部分 tcp 流量,并以一种简单的方式把它们写到一个可读的文件中。下面的命令就可以完成这个事情,但我们需要在一个空目录中运行下面的命令,因为它将创建诸如 x.x.x.x.y-a.a.a.a.z 格式的文件,运行之后,只需按 Ctrl-C 便可停止这个命令。

$ sudo tcpflow -i eth0 port 8000

注意:请将上面的 eth0 替换为你想捕捉的网卡接口名称。

11) IPTraf
IPTraf 是一个针对 Linux 平台的基于控制台的网络统计应用。它生成一系列的图形,如 TCP 连接的包/字节计数、接口信息和活动指示器、 TCP/UDP 流量故障以及局域网内设备的包/字节计数。

在默认的软件仓库中可以找到 IPTraf,所以我们可以使用下面的命令通过 apt 包管理器轻松地安装 IPTraf:

$ sudo apt-get install iptraf

我们可以使用下面的命令通过 yum 包管理器轻松地安装 IPTraf:

# yum install iptraf

我们需要以管理员权限来运行 IPTraf,并带有一个有效的网络接口名。这里,我们的网络接口名为 wlan2,所以我们使用 wlan2 来作为参数:

$ sudo iptraf wlan2

开始通常的网络接口统计,键入:

# iptraf -g

查看接口 eth0 的详细统计信息,使用:

# iptraf -d eth0

查看接口 eth0 的 TCP 和 UDP 监控信息,使用:

# iptraf -z eth0

查看接口 eth0 的包的大小和数目,使用:

# iptraf -z eth0

注意:请将上面的 eth0 替换为你的接口名称。你可以通过运行ip link show命令来检查你的接口。

 

12) Speedometer
Speedometer 是一个小巧且简单的工具,它只用来绘出一幅包含有通过某个给定端口的上行、下行流量的好看的图。

在默认的软件仓库中可以找到 Speedometer ,所以我们可以使用下面的命令通过 yum 包管理器轻松地安装 Speedometer:

# yum install speedometer

我们可以使用下面的命令通过 apt 包管理器轻松地安装 Speedometer:

$ sudo apt-get install speedometer

Speedometer 可以简单地通过在 shell 或虚拟终端中执行下面的命令来运行:

$ speedometer -r wlan2 -t wlan2

注:请将上面的 wlan2 替换为你想要使用的网络接口名称。

13) Netwatch
Netwatch 是 netdiag 工具集里的一部分,它也显示当前主机和其他远程主机的连接情况,以及在每个连接中数据传输的速率。

我们可以使用 yum 在 fedora 中安装 Netwatch,因为它在 fedora 的默认软件仓库中。但若你运行着 CentOS 或 RHEL , 我们需要安装 rpmforge 软件仓库。

# yum install netwatch

Netwatch 是 netdiag 的一部分,可以在默认的软件仓库中找到,所以我们可以轻松地使用下面的命令来利用 apt 包管理器安装 netdiag:

$ sudo apt-get install netdiag

为了运行 netwatch, 我们需要在虚拟终端或 shell 中执行下面的命令:

$ sudo netwatch -e wlan2 -nt

注意: 请将上面的 wlan2 替换为你想使用的网络接口名称,假如你想通过扫描了解你的网络接口名称,可以在一个虚拟终端或 shell 中运行 ip link show 命令。

14) Trafshow
Trafshow 同 netwatch 和 pktstat 一样,可以报告当前活动的连接里使用的协议和每个连接中数据传输的速率。它可以使用 pcap 类型的过滤器来筛选出特定的连接。

我们可以使用 yum 在 fedora 中安装 trafshow ,因为它在 fedora 的默认软件仓库中。但若你正运行着 CentOS 或 RHEL , 我们需要安装 rpmforge 软件仓库。

# yum install trafshow

Trafshow 在默认仓库中可以找到,所以我们可以轻松地使用下面的命令来利用 apt 包管理器安装它:

$ sudo apt-get install trafshow

为了使用 trafshow 来执行监控任务,我们需要在虚拟终端或 shell 中执行下面的命令:

$ sudo trafshow -i wlan2

为了专门监控 tcp 连接,如下面一样添加上 tcp 参数:

$ sudo trafshow -i wlan2 tcp

注意: 请将上面的 wlan2 替换为你想使用的网络接口名称,假如你想通过扫描了解你的网络接口名称,可以在一个虚拟终端或 shell 中运行 ip link show 命令。

15) Vnstat
与大多数的其他工具相比,Vnstat 有一点不同。实际上它运行着一个后台服务或守护进程,并时刻记录着传输数据的大小。另外,它可以被用来生成一个网络使用历史记录的报告。

我们需要开启 EPEL 软件仓库,然后运行 yum 包管理器来安装 vnstat。

# yum install vnstat

Vnstat 在默认软件仓库中可以找到,所以我们可以使用下面的命令运行 apt 包管理器来安装它:

$ sudo apt-get install vnstat

不带有任何选项运行 vnstat 将简单地展示出从该守护进程运行后数据传输的总量。

$ vnstat

为了实时地监控带宽使用情况,使用 ‘-l’ 选项(live 模式)。然后它将以一种非常精确的方式来展示上行和下行数据所使用的带宽总量,但不会显示任何有关主机连接或进程的内部细节。

$ vnstat -l

完成了上面的步骤后,按 Ctrl-C 来停止,这将会得到如下类型的输出:


16) tcptrack
tcptrack 可以展示 TCP 连接的状态,它在一个给定的网络端口上进行监听。tcptrack 监控它们的状态并展示出排序且不断更新的列表,包括来源/目标地址、带宽使用情况等信息,这与 top 命令的输出非常类似 。

鉴于 tcptrack 在软件仓库中,我们可以轻松地在 Debian、Ubuntu 系统中从软件仓库使用 apt 包管理器来安装 tcptrack。为此,我们需要在 shell 或虚拟终端中执行下面的命令:

$ sudo apt-get install tcptrack

我们可以通过 yum 在 fedora 中安装它,因为它在 fedora 的默认软件仓库中。但若你运行着 CentOS 或 RHEL 系统,我们需要安装 rpmforge 软件仓库。为此,我们需要运行下面的命令:

# wget http://apt.sw.be/redhat/el6/en/x86_64/rpmforge/RPMS/rpmforge-release-0.5.3-1.el6.rf.x86_64.rpm
# rpm -Uvh rpmforge-release*rpm
# yum install tcptrack

注:这里我们下载了 rpmforge-release 的当前最新版本,即 0.5.3-1,你总是可以从 rpmforge 软件仓库中下载其最新版本,并请在上面的命令中替换为你下载的版本。

tcptrack 需要以 root 权限或超级用户身份来运行。执行 tcptrack 时,我们需要带上要监视的网络接口 TCP 连接状况的接口名称。这里我们的接口名称为 wlan2,所以如下面这样使用:

sudo tcptrack -i wlan2

假如你想监控特定的端口,则使用:

# tcptrack -i wlan2 port 80

请替换上面的 80 为你想要监控的端口号。注意: 请将上面的 wlan2 替换为你想使用的网络接口名称,假如你想通过扫描了解你的网络接口名称,可以在一个虚拟终端或 shell 中运行 ip link show 命令。

17) CBM
CBM ( Color Bandwidth Meter) 可以展示出当前所有网络设备的流量使用情况。这个程序是如此的简单,以至于都可以从它的名称中看出其功能。CBM 的源代码和新版本可以在 http://www.isotton.com/utils/cbm/ 上找到。

鉴于 CBM 已经包含在软件仓库中,我们可以简单地使用 apt 包管理器从 Debian、Ubuntu 的软件仓库中安装 CBM。为此,我们需要在一个 shell 窗口或虚拟终端中运行下面的命令:

$ sudo apt-get install cbm

我们只需使用下面展示的命令来在 shell 窗口或虚拟终端中运行 cbm:

$ cbm

18) bmon
Bmon ( Bandwidth Monitoring) ,是一个用于调试和实时监控带宽的工具。这个工具能够检索各种输入模块的统计数据。它提供了多种输出方式,包括一个基于 curses 库的界面,轻量级的HTML输出,以及 ASCII 输出格式。

bmon 可以在软件仓库中找到,所以我们可以通过使用 apt 包管理器来在 Debian、Ubuntu 中安装它。为此,我们需要在一个 shell 窗口或虚拟终端中运行下面的命令:

$ sudo apt-get install bmon

我们可以使用下面的命令来运行 bmon 以监视我们的网络状态:

$ bmon

19) tcpdump
TCPDump 是一个用于网络监控和数据获取的工具。它可以为我们节省很多的时间,并可用来调试网络或服务器的相关问题。它可以打印出在某个网络接口上与布尔表达式相匹配的数据包所包含的内容的一个描述。

tcpdump 可以在 Debian、Ubuntu 的默认软件仓库中找到,我们可以简单地以 sudo 权限使用 apt 包管理器来安装它。为此,我们需要在一个 shell 窗口或虚拟终端中运行下面的命令:

$ sudo apt -get install tcpdump

tcpdump 也可以在 Fedora、CentOS、RHEL 的软件仓库中找到。我们可以像下面一样通过 yum 包管理器来安装它:

# yum install tcpdump

tcpdump 需要以 root 权限或超级用户来运行,我们需要带上我们想要监控的 TCP 连接的网络接口名称来执行 tcpdump 。在这里,我们有 wlan2 这个网络接口,所以可以像下面这样使用:

$ sudo tcpdump -i wlan2

假如你只想监视一个特定的端口,则可以运行下面的命令。下面是一个针对 80 端口(网络服务器)的例子:

$ sudo tcpdump -i wlan2 'port 80'

20) ntopng
[ntopng][20] 是 ntop 的下一代版本。它是一个用于展示网络使用情况的网络探头,在一定程度上它与 top 针对进程所做的工作类似。ntopng 基于 libpcap 并且它以可移植的方式被重写,以达到可以在每一个 Unix 平台 、 MacOSX 以及 Win32 上运行的目的。

为了在 Debian,Ubuntu 系统上安装 ntopng,首先我们需要安装 编译 ntopng 所需的依赖软件包。你可以通过在一个 shell 窗口或一个虚拟终端中运行下面的命令来安装它们:

$ sudo apt-get install libpcap-dev libglib2.0-dev libgeoip-dev redis-server wget libxml2-dev build-essential checkinstall

现在,我们需要像下面一样针对我们的系统手动编译 ntopng :

$ sudo wget http://sourceforge.net/projects/ntop/files/ntopng/ntopng-1.1_6932.tgz/download
$ sudo tar zxfv ntopng-1.1_6932.tgz
$ sudo cd ntopng-1.1_6932
$ sudo ./configure
$ sudo make
$ sudo make install

这样,在你的 Debian 或 Ubuntu 系统上应该已经安装上了你编译的 ntopng 。

我们已经有了有关 ntopng 的使用方法的教程,它既可以在命令行也可以在 Web 界面中使用,我们可以前往这些教程来获得有关 ntopng 的知识。

结论
在这篇文章中,我们介绍了一些在 Linux 下的网络负载监控工具,这对于系统管理员甚至是新手来说,都是很有帮助的。在这篇文章中介绍的每一个工具都具有其特点,不同的选项等,但最终它们都可以帮助你来监控你的网络流量。

一大波你可能不知道的 Linux 网络工具,首发于博客 - 伯乐在线

16 May 01:17

JAVA的内存模型及结构

by Jaxon

原文链接   译文链接  作者:Tai Truong    译者:Jaxon

所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?

Java内存模型

Java内存模型在JVM specification, Java SE 7 Edition, and mainly in the chapters “2.5 Runtime Data Areas” and “2.6 Frames”中有详细的说明。对象和类的数据存储在3个不同的内存区域:堆(heap space)、方法区(method area)、本地区(native area)。

堆内存存放对象以及数组的数据,方法区存放类的信息(包括类名、方法、字段)、静态变量、编译器编译后的代码,本地区包含线程栈、本地方法栈等存放线程

JUtH_20121024_RuntimeDataAreas_1_MemoryModel (1)

方法区有时被称为持久代(PermGen)。

JUtH_20121024_RuntimeDataAreas_2_MemoryModel (1)

所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。

方法的执行都是伴随着线程的。原始类型的本地变量以及引用都存放在线程栈中。而引用关联的对象比如String,都存在在堆中。为了更好的理解上面这段话,我们可以看一个例子:

import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.log4j.Logger;

public class HelloWorld {
    private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());

    public void sayHello(String message) {
        SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
        String today = formatter.format(new Date());
        LOGGER.info(today + ": " + message);
    }
}

这段程序的数据在内存中的存放如下:

JUtH_20121024_RuntimeDataAreas_4_MemoryModel

通过JConsole工具可以查看运行中的Java程序(比如Eclipse)的一些信息:堆内存的分配,线程的数量以及加载的类的个数;

JUtH_20121024_RuntimeDataAreas_5_JConsole

 Java内存结构

这里有一份极好的白皮书:Memory Management in the Java HotSpot Virtual Machine。它描述了垃圾回收(GC)触发的内存自动管理。Java的内存结构包含如下部分:

JUtH_20121024_RuntimeDataAreas_6_MemoryModel

堆内存

堆内存同样被划分成了多个区域:

  • 包含伊甸(Eden)和幸存者区域(Survivor Sapce)的新生代(Young generation)
  • 老年代(Old Generation)

不同区域的存放的对象拥有不同的生命周期:

  • 新建(New)或者短期的对象存放在Eden区域;
  • 幸存的或者中期的对象将会从Eden区域拷贝到Survivor区域;
  • 始终存在或者长期的对象将会从Survivor拷贝到Old Generation;

生命周期来划分对象,可以消耗很短的时间和CPU做一次小的垃圾回收(GC)。原因是跟C一样,内存的释放(通过销毁对象)通过2种不同的GC实现:Young GC、Full GC。

为了检查所有的对象是否能够被销毁,Young GC会标记不能销毁的对象,经过多次标记后,对象将会被移动到老年代中。

哪儿的OutOfMemoryError

对内存结构清晰的认识同样可以帮助理解不同OutOfMemoryErrors:

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

原因:对象不能被分配到堆内存中

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space

原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库;

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:创建的数组大于堆内存的空间

Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现;

关于OutOfMemoryError的更多信息可以查看:“Troubleshooting Guide for HotSpot VM”, Chapter 3 on “Troubleshooting on memory leaks”

 

参考链接:

 

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: JAVA的内存模型及结构

16 May 01:14

Twitter开源MySQL集群管理框架Mysos

by 谢丽

Mysos是一个用于运行MySQL实例的Apache Mesos框架。它极大地简化了MySQL集群的管理,具有高可靠性、高可用性及高可扩展性等特点。有关其具体功能,可以查看InfoQ前期的报道

Mysos需要Python 2.7及Mesos Python绑定。其中,后者包含两个Python包。mesos.interface位于PyPI上,可以自动安装。但mesos.native是平台依赖的,用户需要在自己的机器上构建(相关命令),或者下载相应平台的编译版本(Mesosphere提供了部分Linux平台的egg文件)。

Mysos主要包含如下两个组件:

  • mysos_scheduler:用于连接Mesos主节点及管理MySQL集群;
  • mysos_executor:用于启动Mesos从节点(基于mysos_scheduler请求)执行MySQL任务。

这两个组件可以单独构建和部署,也可以使用PEX将二者及其依赖包打包成一个可执行文件(具体过程参见这里)。

Mysos提供了一个REST API,用于在Mesos上创建和管理MySQL集群。下面是集群创建的示例代码:

curl -X POST 192.168.33.7/clusters/test_cluster3 --form "cluster_user=mysos" \ --form "num_nodes=2" --
form "backup_id=foo/bar:201503122000" \ --form 'size={"mem": "512mb", "disk": "3gb", "cpus": 1.0}'

其中,集群名称为test_cluster3,cluster_user指定了对集群中所有MySQL实例都拥有管理员权限的用户,num_nodes指定了集群节点数,backup_id指定了MySQL实例启动时需要从哪个MySQL备份恢复,size指定了分配给实例的资源。该命令会返回用于访问MySQL实例的密码以及集群URL。

Mysos是Twitter和Mesosphere合作的产物。为了该项目的长远发展,在将其开源的同时,Twitter也向Apahce基金会提交了孵化提案,希望以这种方式确保该项目遵循Apache 2.0许可协议,促进Mysos社区的发展壮大。


感谢崔康对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

13 May 10:30

高性能服务端漫谈

by changqi

一、背景

进入多核时代已经很久了,大数据概念也吵得沸沸扬扬,不管你喜欢不喜欢,不管你遇到没遇到,big-data或bigger-data都必须正视.

处理大数据,基本都离不开分布式计算和分布式存储,这其中以hadoop最为使用广泛和经典。

分布式系统,就离不开计算系统、网络系统、文件系统和数据库系统。

这么多系统,之间又是如何协作的呢?
通讯过程又是如何保障高性能的呢?

1.单处理器
在以前的单核心cpu下,我们要实现文件I/O、网络I/O,可以妥妥的使用单线程循环处理任务。

但是如果想“同时”做点其它事情,就得多开线程,比如:

在下载远程文件的同时,显示下载进度。

2.多线程
我们会用主线程a来更新界面元素,这里是更新下载进度条,同时用一个额外的线程b去下载远程文件。

3.阻塞
当需要下载的时候,我们必须使a阻塞,否则,我们的下载线程b将无法获得cpu时间。
而当需要更新界面时,我们必须使b阻塞,原因也是为了获得cpu时间。

阻塞:使一个线程进入阻塞或等待的状态,释放它所占有的cpu时间.

阻塞的原因,是因为任何操作,无论是更新界面还是下载文件(网络I/O+磁盘I/O),都会转化成为一条一条cpu可以执行的指令,而这些指令的读取、执行都需要消耗cpu的时钟周期。

事实上,我们可以使用类似wait、await、sleep、read、write等操作使当前调用的线程进入阻塞。

显然

阻塞的目的是
1.当前有更重要的事情要交给别的线程来做.
2.调用线程可能面临一个长时间的I/O操作,占据cpu时间显然是一种浪费.
3.同步需求.

虽然用户看起来是下载文件和更新界面“同时”运行,但实际上,任何一个时刻,在单核心cpu环境下,都只有一个线程会真正的运行,所以多线程之间是“并发”而非真正的“并行”。

4.多处理器
单核心时代早已过去,多核、多处理无论在企业级服务器还是家用桌面电脑、平板和智能手机上都已经是主流。


注:图片来源intel

如果多处理器的核心是真实的而非虚拟化的,那么多线程就可以真正的并行。

可以看到,t1、t2、t3的运行时间可以出现重叠.
但实际上,操作中运行的进程远不止几个,那么相应的线程数会远大于cpu的核心数,所以即使上图中假设是4核心处理器,那么真正能同时执行的线程也只有4个,所以也会出现运行的中断,即阻塞。

二、高性能通讯

在了解了多核、多处理器、多线程、阻塞的概念后,我们来看看通讯,显然,任何一个通讯框架都试图追求高性能、高吞吐、高tps能力。

但是,任何一个来自用户或其它服务器的请求都不可能只是要求一个简单的echo返回,所以请求执行的任务几乎都会包含:

1.计算 比如MapReduce、SQL、坐标计算等等.
2.I/O 访问数据库、磁盘、缓存、内存或者其它设备.

站在用户的角度,总是希望自己的请求会被优先、快速的响应,而站在服务器的角度,总是希望所有的请求同时能够被处理.

1.同步/异步
同步的意思如字面一般简单:

同步就是多个对象的步调一致,这种步调是一种约定。

比如,时间上约定10点同时到达,先到达的就会等待。
比如,逻辑上约定必须取得结果,调用才能返回。
比如,资源上约定read和write不可同时进行,但read之间可同时执行。

下面的图显示了时间上的同步约定:

而异步,就是步调无须一致:

异步,就是多个对象之间的行为无须遵守显式或隐式的约定。
比如,老婆没到,你也可以进场看电影。
比如,可以不必等结果真正出现,就立即返回。
比如,read和read之间可以乱序访问文件或资源。

2.同步与阻塞的关系
服务器的能力是有限的,为了能够满足所有用户的请求,服务器必须能够进行高并发的处理。这一点可以通过两种方式达到:

1.单线程 + 异步I/O. (node.js)
多线程的建立是需要开销的,线程数越多,线程上下文的切换就会越频繁,而异步I/O在“理想”情况下不会阻塞,调用完毕即返回,通过回调callback或事件通知来处理结果.

2.多线程 + 异步或同步I/O. (nginx)
单线程的一个缺点就是无法充分利用多处理器的并行能力,同时异步I/O不是在任何情况下都是真正异步的。
比如文件在缓存中(通过映射到内存)、文件压缩、扩展、缓冲区拷贝等操作,会使得异步I/O被操作系统偷偷地转换为同步。

假如文件已经在缓存中,使用同步I/O的结果会更快。

这里你可能会疑惑,同步看起来很像“阻塞”,但仔细看本篇中对它们的说明,就会发现:

阻塞是调用线程的一种状态或行为,它的作用是放弃占用的cpu.
同步是多个线程之间的协调机制,它的作用是为了保证操作的顺序是正确可预期。

同步可以使用阻塞来实现,也可以使用非阻塞来实现。
而有的情况下,因为同步是不得已的行为,比如要hold住一个来自其他服务器的session,以防止立即返回后的上下文失效,我们往往会这样:

//还没有结果
bool haveResponse = false;
//调用异步I/O,从远程数据库执行sql,并返回结果
rpc.callAsync(database,sql,
 function(resp){ 
 response = resp;
 haveResponse = true;
 });
//通过循环阻塞来hold住这个线程的上下文和session
while(!response){
 //这里将阻塞100毫秒
 if(!response){
 await(100);
 }else{
 break;
 }
}
//通过请求的session返回结果
httpContext.currentSession.Respond(response);

这是一种 多线程 + 异步 转为了 多线程 + 同步的方式,因为Web应用服务器处理session时采用的往往是线程池技术,而我们又没有服务器推(server push)或者用户的调用请求一直在等待结果,所以,即使访问数据库采用的是异步I/O,也不得不通过这种方法来变成同步。

与其如此,还不如:

//调用同步I/O,从远程数据库执行sql,并返回结果
//调用时,此线程阻塞
response = rpc.callSync(database,sql);
//通过请求的session返回结果
httpContext.currentSession.Respond(response);

 

上面的代码,使用了简单的同步I/O模型,因为一般的访问数据库操作是很费时的操作,所以处理当前session的线程符合被阻塞的目的,那么同步调用就被实现为阻塞的方式。

事实上,从用户的角度来看,用户发出请求后总是期待会返回一个确定的结果,无论服务端如何处理用户的请求,都必须将结果返回给用户,所以采用异步I/O虽然是最理想的状态,但必须考虑整个应用的设计,即使你这里使用了异步,别的地方也可能需要同步,如果这种“额外”同步的设计复杂性远高于使用异步带来的好处,那么请考虑“同步/阻塞式”设计。

如果业务逻辑上,要求依赖性调用,比如DAG,那么同步也是必须的。

三、IOCP和epoll

1. IOCP(完成端口)
windows提供了高效的异步I/O的线程模型,完成端口:

完成端口可以关联很多的文件句柄(这里的文件是广义的,文件、socket或者命名管道都可以是)到一个完成端口上,称为关联完成端口的引用,,这些引用都必须支持(Overlapped I/O,重叠式I/O)。

重叠式I/O是异步I/O的基石,通过进行重叠I/O,可以让调用I/O操作的线程与I/O操作线程并行执行而无须阻塞。

多线程虽然可以充分发挥多处理器的并行优势,但却不是银弹。

当线程数增加,可“同时”处理的请求量上去了,这样吞吐量会很高,但可用于每个用户请求的时间变少,每个用户请求的响应时间随之下降,最后吞吐率下降。

同时,线程的启动和销毁是有开销的,虽然可以通过线程池(ThreadPool)来预先分配一定量的活动线程,但线程越多,其上下文切换(Context Switch)的次数就越频繁。

考虑一种情况:
当线程的栈很大而线程被阻塞的时间很长,操作系统可能会将此线程的堆栈信息置换到硬盘上以节约内存给其它线程使用,这增加了磁盘I/O,而磁盘I/O的速度是非常慢的。

而且,线程的频繁切换也会降低指令和数据的locality(局部性),cpu的缓存命中率会下降,这又加剧了性能的下降。

完成端口的设计目标是:

1.任一给定时刻,对于任一处理器,都有一个活动线程可用。
2.控制活动线程的数量,尽量减少线程上下文的切换。

可以看出,IOCP主要是针对线程模型的优化。

创建完成端口时,需要指定一个Concurrent Value = c的值,来指示:

当活动线程的数量 v >= c,就将其它关联在完成端口上的线程阻塞,直到活动线程的数量 v < c.
当一个活动线程进行I/O时,会阻塞,活动线程数量v就会下降.

这一点是IOCP的精髓。
完成端口的原理是:

在创建了完成端口后,将socket关联到这个端口上,一旦socket上的I/O操作完成,操作系统的I/O管理模块会发送一个通知(Notification)给完成端口,并将I/O操作的完成结果(completion packet)送入完成端口的等待队列WQ,这个队列的顺序是先入先出(FIFO)。

也就是说,调用线程可不比等待socket的I/O操作完成,就立即返回做其它的事情。

而当活动线程的数量下降,小于指定的并发约束(concurrent value)时,操作系统将会唤醒最近被加入阻塞队列BQ的线程,让它从完成包的等待队列中取出一个最老的I/O结果进行处理。这里可以看出,BQ的顺序是后入先出(LIFO)。

IOCP所谓的异步是:

与完成端口关联的文件(file、socket、named pipeline)句柄上的I/O操作是异步的。
调用线程只负责将socket I/O丢给完成端口,然后就可以做其它事。而无需向同步那样等待。

但是,如果一个调用线程在处理这个从完成队列取出的数据后,又在当前线程进行了其它I/O操作,比如读取文件、访问数据库,那么这个调用线程同样会阻塞,但不是阻塞到完成端口的队列上。

这一点,对数据的处理就涉及不同的业务逻辑需求,I/O线程是否应该与逻辑线程分开,分开后,逻辑线程应该是如何控制数量,如果分开,就要求在拿到数据后,要么另起线程处理数据,要么将数据扔进线程池(Threadpool)。无论是何种方式,都会增加线程上下文切换的次数,反过来影响IOCP的可用资源。

所以,要从应用的实际需求出发,来总体控制整个服务器的并发线程数量,否则,无论多么高效的通讯模型,都会被业务模型(往往需要对文件或数据库的访问)所拖累,那么整个应用的性能就会下降。

2. epoll
linux上的高效I/O模型则是epoll.

epoll是对select/poll模型的一种改进.

1.既然是对select/poll的改进,就是一种I/O多路复用模型。
2.支持的文件(同样是广义)描述符fileDescriptor巨大,具体多大与内存大小直接相关。
3.wait调用在活跃socket数目较少时,可高效返回。

在传统的select/poll模型中,内核会遍历所有的fileDescriptor(这里只说socket),而不管socket是否活跃,这样,随着socket数目的增加,性能会很快下降。

而epoll模型,采用了向内核中断处理注册回调的方式,当某个socket上的I/O就绪,中断就会发出,接着就会将这个结果推入一个就绪队列Q中,Q采用单链表实现,所以扩展性是天生的。

同时,由于采用了适宜频繁写的平衡树-红黑树的结构来存储fileDescriptors,所以当需要向fileDescriptors中加入、删除、查找socket时,就会非常高效,另外还有一层内核级页高速缓存。

最后,由于活动的socket比较少时,I/O就绪的中断次数相应减少,所以向就绪队列Q中插入数据的次数相应减少,当wait操作被调用时,内核会考察Q,如果不空就立即返回,同时通过内存映射来讲就绪的I/O数据从内核态拷贝到用户态,达到少而快的效果。

epoll的主要调用接口如下:

/* 创建可保证size个效率epoll,返回epfd*/
int epoll_create(int size); 
/* 设置应当注册的事件类型IN/OUT/ET/LT,并设置用于返回事件通知的events */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
/* epoll进入阻塞,events用于设置返回事件通知的events */
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

边沿触发(ET)

考虑上面的图,随着时间的增加,高低电平交替变化。

所谓边沿触发,就是当达到边沿(一个临界条件)时触发,如同0到1.

epoll中的边沿触发,是指当I/O就绪,中断到达时,执行对应的回调,将结果推入`等待队列Q`中,之后便不再关心这个结果。
这样导致的结果是,当wait调用返回时,如果对应的事件没有被处理完,比如读操作没有将buffer中的数据读取完,就返回,将没有机会再处理剩余的数据。

水平触发(LT)
所谓水平触发,就是每到上边沿时就触发,比如每次到1.

epoll中的边沿触发,是指当I/O就绪,中断到达时,执行对应的回调,将结果推入`等待队列Q`中,当队列被清空后,再次将结果推入队列。
这样的结果是,当wait调用返回时,如果对应的时间没有处理完,比如写数据,写了一部分,就返回,也会在下次wait中收到通知,从而得以继续处理剩余数据。

水平触发流程简单稳定,需要考虑的事情少,且支持阻塞/非阻塞的socket I/O。
而边沿触发,在大并发情况下,更加高效,因为通知只发一次,但只支持非阻塞的socket I/O。

下图是ET方式的epoll简略流程:

高性能服务端漫谈,首发于博客 - 伯乐在线

12 May 06:26

C++静态库与动态库

by promumu

这次分享的宗旨是——让大家学会创建与使用静态库、动态库,知道静态库与动态库的区别,知道使用的时候如何选择。这里不深入介绍静态库、动态库的底层格式,内存布局等,有兴趣的同学,推荐一本书《程序员的自我修养——链接、装载与库》。

 

什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。

所谓静态、动态是指链接。回顾一下,将一个程序编译成可执行程序的步骤:

 

静态库

之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  •  静态库对函数库的链接是放在编译时期完成的。
  •  程序在运行时与函数库再无瓜葛,移植方便。
  •  浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

下面编写一些简单的四则运算C++类,将其编译成静态库给他人用,头文件如下所示:

#pragma once
class StaticMath
{
public:
    StaticMath(void);
    ~StaticMath(void);

    static double add(double a, double b);//加法
    static double sub(double a, double b);//减法
    static double mul(double a, double b);//乘法
    static double div(double a, double b);//除法

    void print();
};

inux下使用ar工具、Windows下vs使用lib.exe,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。一般创建静态库的步骤如图所示:

Linux下创建与使用静态库

Linux静态库命名规则
Linux静态库命名规范,必须是”lib[your_library_name].a”:lib为前缀,中间是静态库名,扩展名为.a。

创建静态库(.a)
通过上面的流程可以知道,Linux创建静态库过程如下:

  • 首先,将代码文件编译成目标文件.o(StaticMath.o)
g++ -c StaticMath.cpp

注意带参数-c,否则直接编译为可执行文件

  •  然后,通过ar工具将目标文件打包成.a静态库文件
ar -crv libstaticmath.a StaticMath.o

生成静态库libstaticmath.a

大一点的项目会编写makefile文件(CMake等等工程管理工具)来生成静态库,输入多个命令太麻烦了。

使用静态库

编写使用上面创建的静态库的测试代码:

#include "StaticMath.h"
#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    double a = 10;
    double b = 2;

    cout << "a + b = " << StaticMath::add(a, b) << endl;
    cout << "a - b = " << StaticMath::sub(a, b) << endl;
    cout << "a * b = " << StaticMath::mul(a, b) << endl;
    cout << "a / b = " << StaticMath::div(a, b) << endl;

    StaticMath sm;
    sm.print();

    system("pause");
    return 0;
}

Linux下使用静态库,只需要在编译的时候,指定静态库的搜索路径(-L选项)、指定静态库名(不需要lib前缀和.a后缀,-l选项)。

# g++ TestStaticLibrary.cpp -L../StaticLibrary -lstaticmath

  • -L:表示要连接的库所在目录
  • -l:指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。

 

Windows下创建与使用静态库

创建静态库(.lib

如果是使用VS命令行生成静态库,也是分两个步骤来生成程序:

  • 首先,通过使用带编译器选项 /c 的 Cl.exe 编译代码 (cl /c StaticMath.cpp),创建名为“StaticMath.obj”的目标文件。
  • 然后,使用库管理器 Lib.exe 链接代码 (lib StaticMath.obj),创建静态库StaticMath.lib。

当然,我们一般不这么用,使用VS工程设置更方便。创建win32控制台程序时,勾选静态库类型;打开工程“属性面板”→”配置属性”→”常规”,配置类型选择静态库。

Build项目即可生成静态库。

使用静态库

测试代码Linux下面的一样。有3种使用方法:

方法一:

在VS中使用静态库方法:

  • 工程“属性面板”→“通用属性”→“框架和引用”→”添加引用”,将显示“添加引用”对话框。 “项目”选项卡列出了当前解决方案中的各个项目以及可以引用的所有库。 在“项目”选项卡中,选择 StaticLibrary。 单击“确定”。

  • 添加StaticMath.h 头文件目录,必须修改包含目录路径。打开工程“属性面板”→”配置属性”→“C/C++”→” 常规”,在“附加包含目录”属性值中,键入StaticMath.h 头文件所在目录的路径或浏览至该目录。

编译运行OK。

如果引用的静态库不是在同一解决方案下的子工程,而是使用第三方提供的静态库lib和头文件,上面的方法设置不了。还有2中方法设置都可行。

方法二:

打开工程“属性面板”→”配置属性”→ “链接器”→ ”命令行”,输入静态库的完整路径即可。

方法三:

  • “属性面板”→”配置属性”→“链接器”→”常规”,附加依赖库目录中输入,静态库所在目录;
  • “属性面板”→”配置属性”→“链接器”→”输入”,附加依赖库中输入静态库名StaticLibrary.lib。

 

动态库

通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?

为什么还需要动态库?

为什么需要动态库,其实也是静态库的特点导致。

  • 空间浪费是静态库的一个问题。

  • 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

动态库特点总结:

  •  动态库把对一些库函数的链接载入推迟到程序运行的时期。
  •  可以实现进程之间的资源共享。(因此动态库也称为共享库)
  •  将一些程序升级变得简单。
  •  甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

Window与Linux执行文件格式不同,在创建动态库的时候有一些差异。

  •  在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。
  •  Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。

与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。

Linux下创建与使用动态库

linux动态库的命名规则

动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为“.so”。

  •  针对于实际库文件,每个共享库都有个特殊的名字“soname”。在程序启动后,程序通过这个名字来告诉动态加载器该载入哪个共享库。
  •  在文件系统中,soname仅是一个链接到实际动态库的链接。对于动态库而言,每个库实际上都有另一个名字给编译器来用。它是一个指向实际库镜像文件的链接文件(lib+soname+.so)。

创建动态库(.so)

编写四则运算动态库代码:

#pragma once
class DynamicMath
{
public:
        DynamicMath(void);
        ~DynamicMath(void);

        static double add(double a, double b);
        static double sub(double a, double b);
        static double mul(double a, double b);
        static double div(double a, double b);
        void print();
};

首先,生成目标文件,此时要加编译器选项-fpic

g++ -fPIC -c DynamicMath.cpp

-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。

  • 然后,生成动态库,此时要加链接器选项-shared
g++ -shared -o libdynmath.so DynamicMath.o

-shared指定生成动态链接库。

其实上面两个步骤可以合并为一个命令:

g++ -fPIC -shared -o libdynmath.so DynamicMath.cpp

使用动态库

编写使用动态库的测试代码:

#include "../DynamicLibrary/DynamicMath.h"

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    double a = 10;
    double b = 2;

    cout << "a + b = " << DynamicMath::add(a, b) << endl;
    cout << "a - b = " << DynamicMath::sub(a, b) << endl;
    cout << "a * b = " << DynamicMath::mul(a, b) << endl;
    cout << "a / b = " << DynamicMath::div(a, b) << endl;

    DynamicMath dyn;
    dyn.print();
    return 0;
}

引用动态库编译成可执行文件(跟静态库方式一样):

g++ TestDynamicLibrary.cpp -L../DynamicLibrary -ldynmath

然后运行:./a.out,发现竟然报错了!!!

可能大家会猜测,是因为动态库跟测试程序不是一个目录,那我们验证下是否如此:

发现还是报错!!!那么,在执行的时候是如何定位共享库文件的呢?

1) 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。

2) 对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段—环境变量LD_LIBRARY_PATH—/etc/ld.so.cache文件列表—/lib/,/usr/lib 目录找到库文件后将其载入内存。

如何让系统能够找到它:

  •  如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
  •  如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:
  •         1. 编辑/etc/ld.so.conf文件,加入库文件所在目录的路径
  •         2. 运行ldconfig ,该命令会重建/etc/ld.so.cache文件

我们将创建的动态库复制到/usr/lib下面,然后运行测试程序。

 

Windows下创建与使用动态库

创建动态库(.dll)

与Linux相比,在Windows系统下创建动态库要稍微麻烦一些。首先,需要一个DllMain函数做出初始化的入口(创建win32控制台程序时,勾选DLL类型会自动生成这个文件):

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

通常在导出函数的声明时需要有_declspec(dllexport)关键字:

#pragma once
class DynamicMath
{
public:
    __declspec(dllexport) DynamicMath(void);
    __declspec(dllexport) ~DynamicMath(void);

    static __declspec(dllexport) double add(double a, double b);//加法
    static __declspec(dllexport) double sub(double a, double b);//减法
    static __declspec(dllexport) double mul(double a, double b);//乘法
    static __declspec(dllexport) double div(double a, double b);//除法

    __declspec(dllexport) void print();
};

生成动态库需要设置工程属性,打开工程“属性面板”→”配置属性”→”常规”,配置类型选择动态库。

Build项目即可生成动态库。

使用动态库

创建win32控制台测试程序:

#include "stdafx.h"
#include "DynamicMath.h"

#include <iostream>
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    double a = 10;
    double b = 2;

    cout << "a + b = " << DynamicMath::add(a, b) << endl;
    cout << "a - b = " << DynamicMath::sub(a, b) << endl;
    cout << "a * b = " << DynamicMath::mul(a, b) << endl;
    cout << "a / b = " << DynamicMath::div(a, b) << endl;

    DynamicMath dyn;
    dyn.print();

    system("pause");
    return 0;
}

方法一:

  • 工程“属性面板”→“通用属性”→“框架和引用”→”添加引用”,将显示“添加引用”对话框。“项目”选项卡列出了当前解决方案中的各个项目以及可以引用的所有库。 在“项目”选项卡中,选择 DynamicLibrary。 单击“确定”。

  • 添加DynamicMath.h 头文件目录,必须修改包含目录路径。打开工程“属性面板”→”配置属性”→“C/C++”→” 常规”,在“附加包含目录”属性值中,键入DynamicMath.h 头文件所在目录的路径或浏览至该目录。

编译运行OK。

方法二:

  •  “属性面板”→”配置属性”→“链接器”→”常规”,附加依赖库目录中输入,动态库所在目录;

  • “属性面板”→”配置属性”→“链接器”→”输入”,附加依赖库中输入动态库编译出来的DynamicLibrary.lib。

这里可能大家有个疑问,动态库怎么还有一个DynamicLibrary.lib文件?即无论是静态链接库还是动态链接库,最后都有lib文件,那么两者区别是什么呢?其实,两个是完全不一样的东西。

StaticLibrary.lib的大小为190KB,DynamicLibrary.lib的大小为3KB,静态库对应的lib文件叫静态库,动态库对应的lib文件叫【导入库】。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。

 

动态库的显式调用

上面介绍的动态库使用方法和静态库类似属于隐式调用,编译的时候指定相应的库和查找路径。其实,动态库还可以显式调用。【在C语言中】,显示调用一个动态库轻而易举!

在Linux下显式调用动态库

#include <dlfcn.h>,提供了下面几个接口:

  • void * dlopen( const char * pathname, int mode ):函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。
  • void* dlsym(void* handle,const char* symbol):dlsym根据动态链接库操作句柄(pHandle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
  • int dlclose (void *handle):dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
  • const char *dlerror(void):当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

在Windows下显式调用动态库

应用程序必须进行函数调用以在运行时显式加载 DLL。为显式链接到 DLL,应用程序必须:

  • 调用 LoadLibrary(或相似的函数)以加载 DLL 和获取模块句柄。
  • 调用 GetProcAddress,以获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接。
  • 使用完 DLL 后调用 FreeLibrary。

显式调用C++动态库注意点

对C++来说,情况稍微复杂。显式加载一个C++动态库的困难一部分是因为C++的name mangling;另一部分是因为没有提供一个合适的API来装载类,在C++中,您可能要用到库中的一个类,而这需要创建该类的一个实例,这不容易做到。

name mangling可以通过extern “C”解决。C++有个特定的关键字用来声明采用C binding的函数:extern “C” 。用 extern “C”声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern “C”,并且不能被重载。尽管限制多多,extern “C”函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern “C”限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。

另外如何从C++动态库中获取类,附上几篇相关文章,但我并不建议这么做:

  • 《LoadLibrary调用DLL中的Class》:http://www.cppblog.com/codejie/archive/2009/09/24/97141.html
  • 《C++ dlopen mini HOWTO》:http://blog.csdn.net/denny_233/article/details/7255673

“显式”使用C++动态库中的Class是非常繁琐和危险的事情,因此能用“隐式”就不要用“显式”,能静态就不要用动态。

 

附件:Linux下库相关命令

g++(gcc)编译选项

  • -shared :指定生成动态链接库。
  • -static :指定生成静态链接库。
  • -fPIC :表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码, 念上就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。
  • -L. :表示要连接的库所在的目录。
  • -l:指定链接时需要的动态库。编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a/.so来确定库的名称。
  • -Wall :生成所有警告信息。
  • -ggdb :此选项将尽可能的生成gdb 的可以使用的调试信息。
  • -g :编译器在编译的时候产生调试信息。
  • -c :只激活预处理、编译和汇编,也就是把程序做成目标文件(.o文件) 。
  • -Wl,options :把参数(options)传递给链接器ld 。如果options 中间有逗号,就将options分成多个选项,然后传递给链接程序。

nm命令

有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。库既可以是静态的也可以是动态的。nm列出的符号有很多,常见的有三种:

  • 一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;
  • 一种是库中定义的函数,用T表示,这是最常见的;
  • 一种是所谓的弱态”符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。

$nm libhello.h

ldd命令

ldd命令可以查看一个可执行程序依赖的共享库,例如我们编写的四则运算动态库依赖下面这些库:

 

总结

二者的不同点在于代码被载入的时刻不同。

  •  静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大。
  •  动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在,因此代码体积较小。

动态库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。带来好处的同时,也会有问题!如经典的DLL Hell问题,关于如何规避动态库管理问题,可以自行查找相关资料。

C++静态库与动态库,首发于博客 - 伯乐在线

12 May 01:07

C++的函数重载

by promumu

—— 每个现象后面都隐藏一个本质,关键在于我们是否去挖掘

写在前面:

函数重载的重要性不言而明,但是你知道C++中函数重载是如何实现的呢(虽然本文谈的是C++中函数重载的实现,但我想其它语言也是类似的)?这个可以分解为下面两个问题

  • 1、声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法,这里就不论述了)
  • 2、当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢)

这两个问题是任何支持函数重载的语言都必须要解决的问题!带着这两个问题,我们开始本文的探讨。本文的主要内容如下:

  • 1、例子引入(现象)
  •      什么是函数重载(what)?
  •      为什么需要函数重载(why)?
  • 2、编译器如何解决命名冲突的?
  •      函数重载为什么不考虑返回值类型
  • 3、重载函数的调用匹配
  •      模凌两可的情况
  • 4、编译器是如何解析重载函数调用的?
  •      根据函数名确定候选函数集
  •      确定可用函数
  •      确定最佳匹配函数
  • 5、总结

 

1、例子引入(现象)

1.1、什么是函数重载(what)?

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

When two or more different declarations are specified for a single name in the same scope,  that name is said to overloaded.  By extension, two declarations in the same scope that declare the same name but with different types are called overloaded declarations. Only function declarations can be overloaded; object and type declarations cannot be overloaded. ——摘自《ANSI C++ Standard. P290》

看下面的一个例子,来体会一下:实现一个打印函数,既可以打印int型、也可以打印字符串型。在C++中,我们可以这样做:

#include<iostream>
using namespace std;

void print(int i)
{
        cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
        cout<<"print a string :"<<str<<endl;
}

int main()
{
        print(12);
        print("hello world!");
        return 0;
}

通过上面代码的实现,可以根据具体的print()的参数去调用print(int)还是print(string)。上面print(12)会去调用print(int),print(“hello world”)会去调用print(string)。

1.2、为什么需要函数重载(why)?

试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!

类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!
通过上面的介绍我们对函数重载,应该唤醒了我们对函数重载的大概记忆。下面我们就来分析,C++是如何实现函数重载机制的。

 

2、编译器如何解决命名冲突的?

为了了解编译器是如何处理这些重载函数的,我们反编译下上面我们生成的执行文件,看下汇编代码(全文都是在Linux下面做的实验,Windows类似,你也可以参考《一道简单的题目引发的思考》一文,那里既用到Linux下面的反汇编和Windows下面的反汇编,并注明了Linux和Windows汇编语言的区别)。我们执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中,然后分析log.txt文件。

发现函数void print(int i) 编译之后为:(注意它的函数签名变为——_Z5printi

发现函数void print(string str) 编译之后为:(注意它的函数签名变为——_Z5printSs

我们可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了,但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?我的第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。但看下面的映射关系:

void print(int i) –> _Z5printi
void print(string str) –> _Z5printSs

进一步猜想,前面的Z5表示返回值类型,print函数名,i表示整型int,Ss表示字符串string,即映射为返回类型+函数名+参数列表。最后在main函数中就是通过_Z5printi、_Z5printSs来调用对应的函数的:

80489bc: e8 73 ff ff ff call 8048934 <_Z5printi>
……………
80489f0: e8 7a ff ff ff call 804896f <_Z5printSs>

我们再写几个重载函数来验证一下猜想,如:

void print(long l) –> _Z5printl
void print(char str) –> _Z5printc

可以发现大概是int->i,long->l,char->c,string->Ss….基本上都是用首字母代表,现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:

#include<iostream>
using namespace std;

int max(int a,int b)
{
        return a>=b?a:b;
}

double max(double a,double b)
{
        return a>=b?a:b;
}
int main()
{
        cout<<"max int is: "<<max(1,3)<<endl;
        cout<<"max double is: "<<max(1.2,1.3)<<endl;
        return 0;
}

int max(int a,int b) 映射为_Z3maxii、double max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代码各种返回类型。更加详细的对应关系,如那个数字对应那个返回类型,哪个字符代表哪重参数类型,就不去具体研究了,因为这个东西跟编译器有关,上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“返回类型+函数名+参数列表”。

既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文),看下面的例子

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
      float fl=sqrt(da);//调用sqrt(double)
      double d=sqrt(da);//调用sqrt(double)

      fl=sqrt(fla);//调用sqrt(float)
      d=sqrt(fla);//调用sqrt(float)
}

如果返回类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数。

至此似乎已经完全分析清楚了,但我们还漏了函数重载的重要限定——作用域。上面我们介绍的函数重载都是全局函数,下面我们来看一下一个类中的函数重载,用类的对象调用print函数,并根据实参调用不同的函数:

#include<iostream>
using namespace std;

class test{
public:
        void print(int i)
        {
                cout<<"int"<<endl;
        }
        void print(char c)
        {
                cout<<"char"<<endl;
        }
};

int main()
{
        test t;
        t.print(1);
        t.print('a');
        return 0;
}

我们现在再来看一下这时print函数映射之后的函数名:

void print(int i) –> _ZN4test5printEi

void print(char c) –> _ZN4test5printEc

注意前面的N4test,我们可以很容易猜到应该表示作用域,N4可能为命名空间、test类名等等。这说明最准确的映射机制为:作用域+返回类型+函数名+参数列表。

 

3、重载函数的调用匹配

现在已经解决了重载函数命名冲突的问题,在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
  • 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
  • 使用标准转换匹配:如int 到double、double到int、double到long double、Derived*到Base*、T*到void*、int到unsigned int;
  • 使用用户自定义匹配;
  • 使用省略号匹配:类似printf中省略号参数

如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:

void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);

void h(char c,int i,short s, float f)
{
     print(c);//精确匹配,调用print(char)
     print(i);//精确匹配,调用print(int)
     print(s);//整数提升,调用print(int)
     print(f);//float到double的提升,调用print(double)

     print('a');//精确匹配,调用print(char)
     print(49);//精确匹配,调用print(int)
     print(0);//精确匹配,调用print(int)
     print("a");//精确匹配,调用print(const char*)
}

定义太少或太多的重载函数,都有可能导致模凌两可,看下面的一个例子:

void f1(char);
void f1(long);

void f2(char*);
void f2(int*);

void k(int i)
{
       f1(i);//调用f1(char)? f1(long)?
       f2(0);//调用f2(char*)?f2(int*)?
}

这时侯编译器就会报错,将错误抛给用户自己来处理:通过显示类型转换来调用等等(如f2(static_cast<int *>(0),当然这样做很丑,而且你想调用别的方法时有用做转换)。上面的例子只是一个参数的情况,下面我们再来看一个两个参数的情况:

int pow(int ,int);
double pow(double,double);

void g()
{
       double d=pow(2.0,2)//调用pow(int(2.0),2)? pow(2.0,double(2))?
}

 

4、编译器是如何解析重载函数调用的?

编译器实现调用重载函数解析机制的时候,肯定是首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。下面介绍一种重载函数解析的方法:编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:

这个四个解析步骤所做的事情大致如下:

  • 由匹配文法中的函数调用,获取函数名;
  • 获得函数各参数表达式类型;
  • 语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
  • 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上

下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。重载函数解析大致可以分为三步:

  • 根据函数名确定候选函数集
  • 从候选函数集中选择可用函数集合
  • 从可用函数集中确定最佳函数,或由于模凌两可返回错误

4.1、根据函数名确定候选函数集

根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载,如下面的代码:

void f(int);

void g()
{
        void f(double);
        f(1); //这里调用的是f(double),而不是f(int)
}

内层作用域的函数会隐藏外层的同名函数!同样的派生类的成员函数会隐藏基类的同名函数。这很好理解,变量的访问也是如此,如一个函数体内要访问全局的同名变量要用“::”限定。

为了查找候选函数集,一般采用深度优选搜索算法:

step1:从函数调用点开始查找,逐层作用域向外查找可见的候选函数
step2:如果上一步收集的不在用户自定义命名空间中,则用到了using机制引入的命名空间中的候选函数,否则结束

在收集候选函数时,如果调用函数的实参类型为非结构体类型,候选函数仅包含调用点可见的函数;如果调用函数的实参类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,候选函数为下面集合的并:

(1)在调用点上可见的函数;
(2)在定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;
(3)该类或其基类的友元函数;

下面我们来看一个例子更直观:

void f();
void f(int);
void f(double, double = 314);
names pace N
{ 
    void f(char3 ,char3);
}
classA{
    public: operat or double() { }
};
int main ( )
{
    using names pace N; //using指示符
    A a;
    f(a);
    return 0;
}

根据上述方法,由于实参是类类型的对象,候选函数的收集分为3步:

(1)从函数调用所在的main函数作用域内开始查找函数f的声明, 结果未找到。到main函数
作用域的外层作用域查找,此时在全局作用域找到3个函数f的声明,将它们放入候选集合;

(2)到using指示符所指向的命名空间 N中收集f ( char3 , char3 ) ;

(3)考虑2类集合。其一为定义该类类型的名字空间或定义该类的基类的名字空间中声明的函
数;其二为该类或其基类的友元函数。本例中这2类集合为空。

最终候选集合为上述所列的 4个函数f。

4.2、确定可用函数

可用的函数是指:函数参数个数匹配并且每一个参数都有隐式转换序列。

  • (1)如果实参有m个参数,所有候选参数中,有且只有 m个参数;
  • (2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
  • (3)所有候选参数中,参数个数超过 m个,当前仅当第m + 1个参数以后都有缺省值。如果可用
  • 集合为空,函数调用会失败。

这些规则在前面的《3、重载函数的调用匹配》中就有所体现了。

4.3、确定最佳匹配函数

确定可用函数之后,对可用函数集中的每一个函数,如果调用函数的实参要调用它计算优先级,最后选出优先级最高的。如对《3、重载函数的调用匹配》中介绍的匹配规则中按顺序分配权重,然后计算总的优先级,最后选出最优的函数。

 

5、总结

本文介绍了什么是函数重载、为什么需要函数重载、编译器如何解决函数重名问题、编译器如何解析重载函数的调用。通过本文,我想大家对C++中的重载应该算是比较清楚了。说明:在介绍函数名映射机制是基于g++编译器,不同的编译器映射有些差别;编译器解析重载函数的调用,也只是所有编译器中的一种。如果你对某个编译器感兴趣,请自己深入去研究。

最后我抛给大家两个问题:

  • 1、在C++中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加、字符串之间的连接,那+算不算是操作符重载呢?换个场景C语言中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加,那算不算操作符重载呢?
  • 2、模板(template)的重载时怎么样的?模板函数和普通函数构成的重载,调用时又是如何匹配的呢?

 

附录:一种C++函数重载机制

这个机制是由张素琴等人提出并实现的,他们写了一个C++的编译系统COC++(开发在国产机上,UNIX操作系统环境下具有中国自己版权的C、C++和FORTRAN语言编译系统,这些编译系统分别满足了ISOC90、AT&T的C++85和ISOFORTRAN90标准)。COC++中的函数重载处理过程主要包括两个子过程:

1、在函数声明时的处理过程中,编译系统建立函数声明原型链表,按照换名规则进行换名并在函数声明原型链表中记录函数换名后的名字(换名规则跟本文上面描述的差不多,只是那个int-》为哪个字符、char-》为哪个字符等等类似的差异)

图附1、过程1-建立函数链表(说明,函数名的编码格式为:<原函数名>_<作用域换名><函数参数表编码>,这跟g++中的有点不一样)

2、在函数调用语句翻译过程中,访问符号表,查找相应函数声明原型链表,按照类型匹配原则,查找最优匹配函数节点,并输出换名后的名字下面给出两个子过程的算法建立函数声明原型链表算法流程如图附1,函数调用语句翻译算法流程如图附2。

附-模板函数和普通函数构成的重载,调用时又是如何匹配的呢?

下面是C++创始人Bjarne Stroustrup的回答:

1)Find the set of function template specializations that will take part in overload resolution.

2)if two template functions can be called and one is more specified than the other, consider only the most specialized template function in the following steps.

3)Do overload resolution for this set of functions, plus any ordinary functions as for ordinary functions.

4)If a function and a specialization are equally good matches, the function is perferred.

5)If no match is found, the call is an error.

C++的函数重载,首发于博客 - 伯乐在线

12 May 01:03

Apache Velocity官方指南译者招募

by 方 腾飞

欢迎各位光临并发编程网,并发网最近几年一直致力于翻译优秀的技术文章,从未间断,并发网从本月开始计划组织翻译各个技术框架的官方指南,本月组织翻译Apache Velocity官方指南。Velocity是在阿里巴巴和支付宝等公司被广泛使用的一种基于Java的模板引擎,有兴趣翻译的同学请在评论中回复翻译章节和完成时间,翻译完之后提交到并发编程网,网站使用指南请参考:如何投稿

  1. 简介
  2. Resources
  3. Velocity是如何工作的
  4. 使用单例还是非单例模式
  5. 容器
  6. Using Velocity in General Applications
  7. Application Attributes
  8. Configuring Event Handlers
  9. Velocity Configuration Keys and Values
  10. Configuring Logging
  11. Configuring the Resource Loaders (template loaders)
  12. 模板编码和国际化
  13. Velocity and XML
  14. Summary

 

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Apache Velocity官方指南译者招募

12 May 01:00

视频演讲: 异步处理在分布式系统中的优化作用

by 赵海平

概要
在大数据时代,如何高效访问数据,提高并发处理能力,是个很大的技术挑战。要找出朋友中在淘宝上买过东西的人,和要找出朋友中在淘宝上买过保时捷的人,代码应该如何编写,又能如何优化?本次演讲将分享异步处理在分布式系统中的优化作用。

12 May 01:00

开源项目运营经验谈

by 谢丽

从GitHub的用户基数来看,开源社区中有超过850万人在向开源软件做贡献。Brian HyderPencilBlue的联合创始人兼首席技术官。近日,他根据自己运营开源项目的经验,探讨了如何吸引开源社区成员为项目做贡献以及如何确保项目的正常运转。

Brian认为,开源项目运营可以归结为以下三个方面。

首先是明确项目愿景。通常,人们为项目做贡献,是因为该项目要解决的问题同他们要解决的问题一致。而且他们会希望,项目目标与他们的目标一致,而不仅仅是满足他们的即时需求。因此,非常有必要在README中明确描述项目愿景。但这还不够,为了使潜在的用户和贡献者对项目有信心,项目运营者还应该提供一个路线图。路线图上的时间不宜太具体,因为有时候用户反馈会导致项目运营者调整开发的优先级。比如,“多媒体服务将在12月份完成”要好过“多媒体服务将在12月份第二个周完成”。另外,贡献者/用户越多,项目愿景的要求就越高。项目运营者需要选择一个对项目而言最好的愿景,而不是对于个人而言最好的愿景。总之,项目必须满足用户的需求。

其次是制定项目贡献流程,确保贡献者总是在项目愿景范围内做贡献,保证代码的可维护性以及减少不必要的审核工作。以下是Brian提供的一些建议:

  • 每个问题或特性创建一个分支
  • 每个分支的名称均要包含问题编号
  • 确保针对分支进行了所有的单元测试
  • 尽量不要合并自己的pull请求
  • 注明为什么采用那种方式,让pull请求成为一个学习过程

最后是要带给人良好的感觉。网站、文档及README会给人第一感觉,它们可能会影响人们对相似项目的选择。代码质量确实可以体现出项目优劣,但人们在选择时通常不会首先研读代码。人们的贡献是项目的脉搏,脉搏稳定也有利于项目的长远发展。另外,可以利用一些工具增加项目的透明度:

除了相应的功能外,这些工具还提供了徽章,标明了项目所处的阶段。这可以使其他人对项目运营者的代码能力产生信心。


感谢崔康对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

11 May 11:36

设计模式问答(3)

by zdpg

简介

这篇文章是设计模式问答系列(1)和(2)的延续。在这篇文章里,我们将会介绍状态模式,策略模式,访问者模式,适配器模式和享元模式。

如果你完全不了解设计模式或者你其实并不想通读这篇文章,你可以在这里看我们免费的视频 design pattern Training and interview questions / answers 

如果你还没有读过我前边的系列,你可以通过下面的链接阅读:

  1. 设计模式问答(1):工程模式,抽象工程模式,构造器模式,原型模式,单例模式和命令模式
  2. 设计模式问答(2):解释器模式,迭代器模式,调停者模式,备忘录模式和观察者模式
  3. 设计模式问答(4):桥接模式、组合模式、外观模式、职责链模式、代理模式以及模板模式

你能解释下状态模式吗?

状态模式允许一个对象根据对象的当前值改变自己的行为。参考下面的图片-“策略模式的例子”。这是一个开关操作的例子。如果灯泡的是关闭的状态,当你按下开关,灯泡会打开。如果灯泡是打开的状态,当你按下开关,灯泡将会关闭。简而言之,状态模式就是根据状态改变行为。

Figure:-策略模式的例子

现在让我们用C#来实现这个灯泡例子。图片“策略模式正在进行”同时显示了类和客户端的代码。我们创建一个叫‘clsState’的类,它包含一个enum类型其有‘on’和‘off’两种状态常量。我们定义了一个方法‘PressSwitch’,它会根据当前的状态切换自己的状态。在同一张图的右手边我们定义了一个客户端,它使用‘clsState’类并调用其‘PressSwitch()’方法。我们使用‘getStatus’函数在文本框中显示当前状态。

当我们点击‘Press Switch’按钮,灯泡将会从当前状态切换到相反的状态。

Figure: – 状态模式正在进行

你能解释下策略模式吗?

策略模式是一个类内置的算法集,可以根据使用的类交换算法。当你想在运行时决定使用的算法,这个模式会有用。

让我们看一个实际中策略模式如何工作的例子。以数学的计算为例,计算有相加和相减的策略。图片“策略模式正在进行”以形象的方式说明同样的情形。已知两个数,根据策略给出结果。所以如果是相加策略,两个数将会相加,如果是相减策略,将会得到相减的结果。这些策略只不过是算法。策略模式不过是对类内算法的封装而已。

Figure: – 策略模式正在进行

所以我们需要深入的第一件事就是如何封装类内的这些算法。下面的图片“封装算法”显示了‘add’算法如何封装在‘clsAddStatergy’类中,substract’算法如何封装在 ‘clsSubstractStatergy’类中。这两个类都继承自类‘clsStratergy’并重定义了‘calculate’方法。

Figure: – 封装算法

现在我们定义了一个叫做‘clsMaths’的类,它包含一个‘clsStatergy’的引用。这个类包含一个函数‘setStrategy’用于指定策略。

Figure: -策略类和包装类

下面的图片‘策略模式客户端代码’ 显示了如何使用包装类以及如何用‘setStatergy’方法在运行时设置策略

对象。

Figure: – 策略模式客户端代码

你能解释下访问者模式吗?

访问者模式允许我们不用改变实际的类就可以改变类的结构。它是分离当前数据结构和逻辑算法的一种方式。正因为如此,你可以不用改变类的结构就能向当前数据结构添加新逻辑。再一,你可以改变结构而不用触碰逻辑。

参考下面的图片“逻辑和数据结构”,其中有一个顾客(Customer)数据结构。每个顾客(Customer)对象包含多个地址(Address)对象,每个地址(address)对象又包含多个电话(Phones)对象。这个数据结构需要用两种不同的格式输出,一种是简单的字符串格式,另一种是XML格式。所以我们实现了两个类,一个是字符串逻辑类,另一个是XML逻辑类。这两个类遍历对象的结构,给出相应部分的输出。简言之访问者包含这些逻辑。

Figure: – 逻辑和数据结构

让我们根据上面顾客的例子,用C#实现相同的逻辑。如果你使用其它的编程语言,你也能够相应地映射到相同的逻辑。我们已经创建了两个访问者类,一个针对字符串逻辑进行解析,另一个针对XML逻辑。这两个类都有一个‘visit’方法来接收每个对象并进行解析。为了维持一致性,我们通过一个共同的接口‘IVisitor’来实现它们

Figure :- 访问者类

上面定义的访问者类会传给数据结构类,例如,顾客(Customer)类。在顾客(Customer)类,我们在‘accept’方法中传入访问者(visitor)类。在同一个方法中我们传入类类型并且调用其‘visit’方法。‘visit’方法是重载的,这样就可以根据传入的类类型来调用相应的‘visit’方法。

Figure: – 在数据结构类中传入的访问者

现在每个顾客(Customer)有多个地址(Address)对象,每个地址(Address)对象有多个电话(Phones)对象。所以,clsCustomer’类中包含一个objAddresses’列表对象,‘clsAddress’类中包含一个‘objPhones’列表对象。每个对象都有一个‘accept’方法接收访问者类,并把自身传入访问者类的‘visit’方法。因为访问者类的‘visit’方法是重载的,所以它会根据多态性调用正确的访问者方法。

现在我们有了访问者类中的逻辑和顾客(Customer)类中的数据结构,是时候在客户端使用它们了。下面的图片‘Visitor client code’显示了使用访问者模式的示例代码段。我们创建了访问者对象并把它传给顾客数据类。如果想以字符串的格式显示顾客对象的结构,我们就创建‘clsVisitorString’;如果想生成XML格式,就创建‘clsXML’对象并把它传给顾客对象的数据结构。你能够很容易的看出逻辑是如何与数据结构分离的。

Figure: – 访问者客户端代码

访问者模式和策略模式之间有什么区别?

访问者模式和策略模式看起来非常的相似因为它们都是处理来自数据的封装的复杂逻辑。可以说访问者模式是策略模式更通用的形式。

在策略模式中,我们只有一个上下文或者单个逻辑数据供多个算法操作。在前面的问题中,我们已经解释了策略模式和访问者模式的基础点。那就让我们用先前已经理解的例子进行理解。在策略模式中我们只有唯一一个上下文,并且多个算法在这个上下文中运行。下面的图片‘Strategy’向我们显示了多个算法是如何在这个上下文中运行。

Figure: – 访问者

简而言之,策略模式是一种特殊的访问者模式。在策略模式中我们只有一个数据上下文和多个算法,而在访问者模式中每个算法都关联一个数据上下文。选择策略模式还是访问者模式的基本准则是参考上下文和算法之间的关系。如果上下文和算法是一对多的关系,那么选择策略模式。如果上下和算法是多对多的关系,则选择访问者模式。

简而言之,策略模式是一种特殊的访问者模式。在策略模式中我们只有一个数据上下文和多个算法,而在访问者模式中每个算法都关联一个数据上下文。选择策略模式还是访问者模式的基本准则是参考上下文和算法之间的关系。如果上下文和算法是一对多的关系,那么选择策略模式。如果上下和算法是多对多的关系,则选择访问者模式。

你可以解释下适配器模式吗?

我们常常会碰到两个类因为接口不兼容而不兼容。适配器通过把已有的类重新封装成一个类从而使类之间能彼此兼容。参考下面的图片“不兼容的接口”,这两个类都是用于保存字符串值的集合。并且它们都有一个方法用于把字符串添加到集合。其中一个类的方法命名为‘Add’,另一个的方法命名为‘push’。一个类使用集合对象,而另一个则使用栈。我们想让栈对象可以和集合对象兼容。

Figure: – 不兼容的接口

有两种方法实现适配器模式,一种使用聚合(这种方式称为对象适配器模式),另一种使用继承(这种方式称为类适配器模式)。让我们先来介绍对象适配器模式。

图片‘对象适配器模式’比较宽泛的显示了如何实现这种模式。我们引入一个新的包装类‘clsCollectionAdapter’,它在‘clsStack’类上进行封装,在新的‘Add’方法里调用‘push’方法,从而使两个类兼容。

Figure: – 对象适配器模式

另一种实现适配器模式的方式是通过继承,也称为类适配器模式。图片‘类适配器模式’显示,通过让类‘clsCollectionAdapter’继承类‘clsStack’从而与类‘clsCollection’兼容。

Figure :- 类适配器模式

什么是享元模式(fly weight pattern)?

当我们需要创建许多对象并且这些对象共享一些相同的数据,享元模式非常有用。参考图片“对象和共同数据”。我们需要给一个机构里所有的员工打印名片。数据有两个部分,一部分是可变数据,例如:员工的姓名,另一部分是静态数据i,例如:地址。我们可以只维护一份静态数据的拷贝,让所有可变数据的对象引用这份拷贝,从而减少内存的使用。因此我们为可变数据创建了不同的拷贝,但是却引用了相同的静态数据拷贝。这样我们能优化内存的使用。

Figure: -“对象和共同数据”

下面C#示例代码显示了享元模式实际上是如何实现的。我们有两个类,‘clsVariableAddress’包含可变数据,第二个类‘clsAddress’包含静态数据。为了确保我们只有‘clsAddress’的一个实例,我们定义了一个包装类‘clsStatic’,并且创建了类‘clsAddress’的一个静态实例。这个对象聚合在类‘clsVariableAddress’里。

Figure: – 享元模式的类视图

从图片‘享元模式客户端代码’可以看到,我们创建了两个类‘clsVariableAddress’对象,但是它们内部的静态数据(例如,地址)却引用同一个实例。

Figure: – 享元模式客户端代码

如果你完全不熟悉设计模式或者你其实并不想读整篇文章,你可以看我们免费的视频 design pattern Training and interview questions / answers 

相关文章

09 May 09:18

Java8中CAS的增强

by trytocatch

几天前,我偶然地将之前写的用来测试AtomicInteger和synchronized的自增性能的代码跑了一下,意外地发现AtomicInteger的性能比synchronized更好了,经过一番原因查找,有了如下发现:

在jdk1.7中,AtomicInteger的getAndIncrement是这样的:

    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

而在jdk1.8中,是这样的:

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

可以看出,在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,没有此方法。(PS:为了找出原因,我反编译了Unsafe,发现CAS的失败重试就是在getAndAddInt方法里完成的,我用反射获取到Unsafe实例,编写了跟getAndAddInt相同的代码,但测试结果却跟jdk1.7的getAndIncrement一样慢,不知道Unsafe里面究竟玩了什么黑魔法,还请高人不吝指点)(补充:文章末尾已有推论)

通过查看AtomicInteger的源码可以发现,受影响的还有getAndAdd、addAndGet等大部分方法。

有了这次对CAS的增强,我们又多了一个使用非阻塞算法的理由。

最后给出测试代码,需要注意的是,此测试方法简单粗暴,compareAndSet的性能不如synchronized,并不能简单地说synchronized就更好,两者的使用方式是存在差异的,而且在实际使用中,还有业务处理,不可能有如此高的竞争强度,此对比仅作为一个参考,该测试能够证明的是,AtomicInteger.getAndIncrement的性能有了大幅提升。

package performance;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class AtomicTest {
	//测试规模,调用一次getAndIncreaseX视作提供一次业务服务,记录提供TEST_SIZE次服务的耗时
	private static final int TEST_SIZE = 100000000;
	//客户线程数
	private static final int THREAD_COUNT = 10;
	//使用CountDownLatch让各线程同时开始
	private CountDownLatch cdl = new CountDownLatch(THREAD_COUNT + 1);

	private int n = 0;
	private AtomicInteger ai = new AtomicInteger(0);
	private long startTime;

	public void init() {
		startTime = System.nanoTime();
	}

	/**
	 * 使用AtomicInteger.getAndIncrement,测试结果为1.8比1.7有明显性能提升
	 * @return
	 */
	private final int getAndIncreaseA() {
		int result = ai.getAndIncrement();
		if (result == TEST_SIZE) {
			System.out.println(System.nanoTime() - startTime);
			System.exit(0);
		}
		return result;
	}

	/**
	 * 使用synchronized来完成同步,测试结果为1.7和1.8几乎无性能差别
	 * @return
	 */
	private final int getAndIncreaseB() {
		int result;
		synchronized (this) {
			result = n++;
		}
		if (result == TEST_SIZE) {
			System.out.println(System.nanoTime() - startTime);
			System.exit(0);
		}
		return result;
	}

	/**
	 * 使用AtomicInteger.compareAndSet在java代码层面做失败重试(与1.7的AtomicInteger.getAndIncrement的实现类似),
	 * 测试结果为1.7和1.8几乎无性能差别
	 * @return
	 */
	private final int getAndIncreaseC() {
		int result;
		do {
			result = ai.get();
		} while (!ai.compareAndSet(result, result + 1));
		if (result == TEST_SIZE) {
			System.out.println(System.nanoTime() - startTime);
			System.exit(0);
		}
		return result;
	}

	public class MyTask implements Runnable {
		@Override
		public void run() {
			cdl.countDown();
			try {
				cdl.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			while (true)
				getAndIncreaseA();// getAndIncreaseB();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		AtomicTest at = new AtomicTest();
		for (int n = 0; n < THREAD_COUNT; n++)
			new Thread(at.new MyTask()).start();
		System.out.println("start");
		at.init();
		at.cdl.countDown();
	}
}

以下是在Intel(R) Core(TM) i7-4710HQ CPU @2.50GHz(四核八线程)下的测试结果(波动较小,所以每项只测试了四五次,取其中一个较中间的值):
jdk1.7
AtomicInteger.getAndIncrement 12,653,757,034
synchronized 4,146,813,462
AtomicInteger.compareAndSet 12,952,821,234

jdk1.8
AtomicInteger.getAndIncrement 2,159,486,620
synchronized 4,067,309,911
AtomicInteger.compareAndSet 12,893,188,541


补充:应网友要求,在此提供Unsafe.getAndAddInt的相关源码以及我的测试代码。
用jad反编译jdk1.8中Unsafe得到的源码:

    public final int getAndAddInt(Object obj, long l, int i)
    {
        int j;
        do
            j = getIntVolatile(obj, l);
        while(!compareAndSwapInt(obj, l, j, j + i));
        return j;
    }
    public native int getIntVolatile(Object obj, long l);
    public final native boolean compareAndSwapInt(Object obj, long l, int i, int j);

openjdk8的Unsafe源码:

    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
    public native int     getIntVolatile(Object o, long offset);
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

我的测试代码(提示:如果eclipse等ide报错,那是因为使用了受限的Unsafe,可以将警告级别从error降为warning,具体百度即可):

...
import sun.misc.Unsafe;
public class AtomicTest {
	....
	private Unsafe unsafe;
	private long valueOffset;
	public AtomicTest(){
		Field f;
		try {
			f = Unsafe.class.getDeclaredField("theUnsafe");
			f.setAccessible(true);
			unsafe = (Unsafe)f.get(null);
			valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
		}catch(NoSuchFieldException e){
		...
		}
	}
	private final int getAndIncreaseD(){
		int result;
		do{
			result = unsafe.getIntVolatile(ai, valueOffset);
		}while(!unsafe.compareAndSwapInt(ai, valueOffset, result, result+1));
		if(result == MAX){
			System.out.println(System.nanoTime()-startTime);
			System.exit(0);
		}
		return result;
	}
	...
}

补充2:对于性能提升的原因,有以下推论,虽不敢说百分之百正确(因为没有用jvm的源码作为论据),但还是有很大把握的,感谢网友@周 可人和@liuxinglanyue!

Unsafe是经过特殊处理的,不能理解成常规的java代码,区别在于:
在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add;
如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap;
这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合:

// The following contain CAS-based Java implementations used on
// platforms not supporting native instructions

Unsafe的特殊处理也就是我上文所说的“黑魔法”。

相关链接:
http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html
http://hg.openjdk.java.net/jdk8u/hs-dev/jdk/file/a006fa0a9e8f/src/share/classes/sun/misc/Unsafe.java

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java8中CAS的增强

08 May 11:39

四张图了解iptables原理和使用

by stone2083
     摘要: 四张图了解iptables原理和使用  阅读全文

stone2083 2015-05-08 13:01 发表评论
08 May 00:42

NIO.2手册(1)

by paddx

Java 7引入了NIO.2,NIO.2是继承自NIO框架,并增加了新的功能(例如:处理软链接和硬链接的功能)。这篇帖子包括三个部分,我将使用NIO.2的一些示例,由此向大家演示NIO.2的基本使用方法。

文件拷贝

Q:怎样拷贝一个文件?

A:你可以使用java.nio.file.Files类的public static Path copy(Path source, Path target, CopyOption… options)方法来实现这个功能,该方法可以实现从源文件到目标文件的拷贝。

默认情况下,如果目标文件已经存在或者是一个符号链接,拷贝就会失败。但是,如果源文件和目标文件是同一个文件,这个拷贝的动作就不会执行。

此外还有一些注意的事项:

  • 文件的属性的拷贝不是必须的。
  • 如果支持符号链接,当源文件是一个符号链接时,拷贝的是最终目标文件的链接。
  • 当源文件是一个目录,copy()方法将目标位置生成一个空目录(目录中的元素不会拷贝)。

每个java.nio.file.CopyOption类型的参数传递到copy()方法的可变参数列表后将改变该方法的行为。该参数是一个java.nio.file.StandardCopyOption类型或java.nio.file.LinkOption枚举常量:

  • COPY_ATTRIBUTES:尝试将文件的属性拷贝到目标文件。这些属性依赖于平台和文件系统,因此是不确定的。但是,至少来说,如果源文件和目标文件的存储都支持最后修改时间属性的话,该属性是会拷贝到目标文件的。不过,需要注意的时,拷贝文件的时间戳的精度可能会有所丢失。
  • NOFOLLOW_LINKS:不一样的符号链接.如果该文件是一个符号链接,拷贝的是符号链接自身而不是其引用的目标文件。它的特殊实现在于是否拷贝文件的属性到新的链接上,换句话说,当拷贝一个符号链接的时候,COPY_ATTRIBUTES可能被忽略。
  • REPLACE_EXISTING:当目标文件已经存在时,目标文件将被替换,除非目标文件是一个非空的目录。当目标文件是一个符号链接并且已经存在的话,仅仅符号链接自身被替换而不改变符号链接所引用的文件。

copy() 方法不支持StandardCopyOption的ATOMIC_MOVE选项,在文件拷贝中该选项是一个无意义的。我将在之后关于文件移动的讨论中介绍ATOMIC_MOVE选项。

非原子性拷贝:

拷贝文件是一个非原子性操作。如果抛出java.io.IOException异常,意味着目标文件可能没有拷贝完成或者文件的属性没有从源文件拷贝过来。如果指定为REPLACE_EXISTING模式并且目标文件已经存在,则目标文件已经被替换了。此外,对文件系统已有文件的存在的检查和新文件的创建的检查可能也不是原子的。

除了java.lang.SecurityException,copy()还会抛出以下某一种异常:

  • java.nio.file.DirectoryNotEmptyException: REPLACE_EXISTING模式下,因目标文件是一个非空的目录文件而不能被替换。
  • java.nio.file.FileAlreadyExistsException:目标文件已经存在,但没有指定REPLACE_EXISTING参数而不能被替换。
  • IOException: I/O异常
  • java.lang.UnsupportedOperationException: 传入的可变参数CopyOptions是不被支持的。

可选的特定异常:

DirectoryNotEmptyException和FileAlreadyExistsException是可选的异常,它们之所以是可选的是因为这些异常的抛出需要底层操作系统能识别,如果不能识别的话,就抛出IOException来替换。

我已经创建了一个小的应用程序来展示copy()方法的最基本方法。列表1展示了该应用程序的源代码:

列表1:Copy.java

import java.io.IOException;

import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Copy
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy source target");
         return;
      }

      Path source = Paths.get(args[0]);
      Path target = Paths.get(args[1]);

      try 
      {
         Files.copy(source, target);
      } 
      catch (FileAlreadyExistsException faee) 
      {
         System.err.printf("%s: file already exists%n", target);
      } 
      catch (DirectoryNotEmptyException dnee) 
      {
         System.err.printf("%s: not empty%n", target);
      } 
      catch (IOException ioe)
      {
         System.err.printf("I/O error: %s%n", ioe.getMessage());
      }
   }
}

列表1的main()方法首先验证命令行确认有两个参数,代表源文件和目标文件,如果没有,则输出相关信息,并结束该程序。

接下来,java.nio.file.Paths类的静态方法Path get(URI uri)方法被调用两次,根据文件名从文件系统获取源文件和目标文件的java.nio.file.Path的实例对象。

Path对象现在被传到了copy()方法。如果方法执行成功了,将不会输出任何信息,否则,将输出适当的错误信息。

编译列表1中的代码(javac Copy.java)然后运行该程序。例如,执行java Copy Copy.java Copy.bak.你可以尝试拷贝一个非空目录到另一个目录.将出现什么现象?

作为练习,可以修改Copy.java增加命令行参数使得该程序能识别CopyOptions,然后传入对应的枚举常量到copy()方法,再观察这些参数对copy()功能的影响。

向后兼容

为了兼容过去的基于流的I/O框架,Files类提供以下两种copy()方法的变种:
public static long copy(InputStream in, Path target, CopyOption… options)
public static long copy(Path source, OutputStream out)

在此系列的后面,我将演示扩展列表1中的copy方法,使得此方法能够拷贝一个目录及子目录到另外一个目录中。

删除文件和目录

Q: 怎样删除一个文件或目录?

A:你可以使用Files类的public static void delete(Path path)方法来删除一个文件或目录,该方法根据文件或目录的路径来删除:

  • 如果该路径引用的文件是一个被使用的打开的文件,某些操作系统将会阻止该文件被删除。
  • 如果该路径引用的是一个目录,该目录必须是空的(除非是特殊操作系统的特殊的文件)。
  • 如果该路径引用的是一个符号链接,该方法只删除符号链接,而不删除符号链接指向的文件。

非原子性删除

该方法的实现是可能需要检查该物理文件是否是在当前目录下。由于这个原因,delete()方法对部分操作系统来说可能不是原子的。为了可移植性和安全性考虑,你不应该认为delete()方法的实现是原子的。

delete()遇到I/O异常时抛出IOException,如果要删除的文件不存在,将抛出java.nio.file.NoSuchFileException,如果目录不为空,则会抛出DirectoryNotEmptyException。

可选的特定异常:

NoSuchFileException是一个可选的特定的异常。

我创建了一个小的应用程序,用于演示怎样使用delete()方式。列表2中列出了该应用程序的源代码。

列表2: Delete.java

import java.io.IOException;

import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Delete
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java Delete file-or-directory");
         return;
      }

      Path path = Paths.get(args[0]);
      try 
      {
         Files.delete(path);
      } 
      catch (NoSuchFileException nsfe) 
      {
         System.err.printf("%s: no such file or directory%n", path);
      } 
      catch (DirectoryNotEmptyException dnee) 
      {
         System.err.printf("%s: not empty%n", path);
      } 
      catch (IOException ioe)
      {
         System.err.printf("I/O error: %s%n", ioe.getMessage());
      }
   }
}

列表2的main()方法首先验证命令行参数,确保有且仅有一个参数被传入,该参数是一个文件或者目录的路径。如果没有,将会输出有用的信息并结束该程序。

接下来,调用Paths类的get()方法获取Path对象,该对象代表着文件系统中的文件或目录。

Path对象现在被传入到delete()方法中。如果该方法执行成功,将不会输出任何信息。但是,如果失败,则会输出适当的错误信息。

编译列表2(javac Delete.java)中的代码并运行该应用程序。试着删一个可读写的文件,一个只读文件,一个非空目录和一个符号链接,然后观察删除的结果。

deleteIfExists方法:

Files类声明一个静态方法boolean deleteIfExists(Path path)作为delete()的变种。这两个方法唯一的区别是deleteIfExists只删除存在的文件,因此,deleteIfExists方法永远不会抛出NoSuchFileException异常。

移动文件:

Q: 怎样移动一个文件?

A:你可以使用Files类的public static Path move(Path source, Path target, CopyOption… options)方法来移动文件,该方法的作用就是从源文件移动到目标文件。

默认情况下,该方法尝试移动源文件到目标文件,当目标文件已经存在的时候会发生异常,除非目标文件是源文件自身,这种情况下,该方法不会起作用。

每个CopyOption类型的参数传递到move()方法的可变参数列表后将改变该方法的行为。该参数是一个java.nio.file.StandardCopyOption类型枚举常量:

  • ATOMIC_MOVE: move方法表现为原子的文件系统操作,其他的参数都会被忽略。当目标文件已经存在的时候,特定的实现表现为该存在的文件是否能够被替换,否则将会抛出IO异常。如果该move方法不能实现原子的文件系统操作,将会抛出java.nio.file.AtomicMoveNotSupportedException异常。
  • REPLACE_EXISTING:当目标文件已经存在的时候,目标文件将会被替换,除非目标文件是一个非空的目录。当目标文件已经存在并且是一个符号链接,只替换该符号链接自身,而不替换符号链接所指向的文件。

除了AtomicMoveNotSupportedException之外,move方法抛出的异常与copy方法一致。

我创建了一个小的应用程序用于演示move方法最基本的使用。列表3列出了该应用程序的源代码,该方法与列表1的代码基本一致。

列表3:Move.java

import java.io.IOException;

import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Move
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Move source target");
         return;
      }

      Path source = Paths.get(args[0]);
      Path target = Paths.get(args[1]);

      try 
      {
         Files.move(source, target);
      } 
      catch (FileAlreadyExistsException faee) 
      {
         System.err.printf("%s: file already exists%n", target);
      } 
      catch (DirectoryNotEmptyException dnee) 
      {
         System.err.printf("%s: not empty%n", target);
      } 
      catch (IOException ioe)
      {
         System.err.printf("I/O error: %s%n", ioe.getMessage());
      }
   }
}

编译列表3并运行该应用程序。例如,假设有一个名为report.txt的文件,执行java Move report.txt report bak。当你移动文件到另一个已经存在的文件的时候将会发生什么?

作为练习,可以修改Move.java增加命令行参数使得该程序能识别CopyOptions,然后传入对应的枚举常量到move()方法,然后观察这些参数对move()功能的影响。

接下来的内容

在第二部分,我将演示路径相关方法(例如获取路径、检索路径信息),文件或目录测试方法(例如测试文件或目录的存在性)以及面向属性的一些方法。

相关文章

08 May 00:42

TCP协议缺陷不完全记录

by nieyong

零。前言

TCP自从1974年被发明出来之后,历经30多年发展,目前成为最重要的互联网基础协议。有线网络环境下,TCP表现的如虎添翼,但在移动互联网和物联网环境下,稍微表现得略有不足。

移动互联网突出特性不稳定:信号不稳定,网络连接不稳定。虽然目前发展到4G,手机网络带宽有所增强,但因其流动特性,信号也不是那么稳定:坐长途公交车,或搭乘城铁时,或周边上网密集时等环境,现实环境很复杂。

以下讨论基于Linux服务器环境,假定环境为移动互联网环境。记录我目前所知TCP的一些不足,有所偏差,请给与指正。

一。三次握手

在确定传递数据之前需要三次握手,显然有些多余,业界提出了TCP Fast Open (TFO)扩展机制,两次握手之后就可以发送正常业务数据了。但这需要客户端和服务器端内核层面都支持才行: Linux内核3.6客户端,3.7支持服务器端。

进阶阅读:TCP Fast Open: expediting web services

二。慢启动

一次的HTTP请求,应用层发送较大HTML页面的数据,需要经过若干个往返循环时间(Round-Trip Time)之后,拥塞窗口才能够扩展到最大适合数值,中间过程颇为冗余。这个参数直接关系着系统吞吐量,吞吐量大了,系统延迟小了。但设置成多大,需要根据业务进行抉择。

3.0内核之前初始化拥塞窗口(initcwnd)大小为3。一个已建立连接初始传输数据时可传递3个MSS,若1个MSS为1400那么一次性可传递4K的数据,若为10,一次性可传递13K的数据。

谷歌经过调研,建议移动互联网WEB环境下建议initcwnd设置成10,linux内核3.0版本之后默认值为10。遇到较低内核,需要手动进行设置。

若是局域网环境有类似大数据或文件的传输需求,可以考虑适当放宽一些。

若长连接建立之后传输的都是小消息,每次传输二进制不到4K,那么慢启动改动与否都是无关紧要的事情了。

进阶阅读:

三。线头阻塞(Head-of-line blocking, HOL)

TCP协议数据传输需要按序传输,可以理解为FIFO先进先出队列,当前面数据传输丢失后,后续数据单元只能等待,除非已经丢失的数据被重传并确认接收以后,后续数据包才会被交付给客户端设备,这就是所谓的线头(HOL,head-of-line blocking)阻塞。比较浪费服务器带宽又降低了系统性能,不高效。

1. 多路复用不理想

HTTP/2提出的业务层面多路复用,虽然在一定程度上解决了HTTP/1.*单路传输问题,但依然受制于所依赖的TCP本身线头阻塞的缺陷。构建于TCP上层协议的多路复用,一旦发生出现线头阻塞,需要小心对待多路的业务数据发送失败问题。

2. TCP Keepalive机制失效

理论上TCP的Keepalive保活扩展机制,在出现线头阻塞的时候,发送不出去被一直阻塞,完全失效。

类似于NFS文件系统,一般采用双向的TCP Keepalive保活机制,用以规避某一端因线头阻塞出现导致Keepalive无效的问题,及时感知一端存活情况。

3. 线头阻塞超时提示

数据包发送了,启动接收确认定时器,超时后会重发,重发依然无确认,后续数据会一直堆积到待发送队列中,这里会有一个阻塞超时,算法很复杂。上层应用会接收到来自内核协议栈的汇报"No route to host"的错误信息,默认不大于16分钟时间。在服务器端(没有业务心跳支持的情况下)发送数据前把终端强制断线,顺便结合TCPDUMP截包,等15分钟左右内核警告"EHOSTUNREACH"错误,应用层面就可以看到"No route to host"的通知。

四。四次摆手

两端连接成功建立之后,需要关闭时,需要产生四次交互,这在移动互联网环境下,显得有些多余。快速关闭,快速响应,冗余交互导致网络带宽被占用。

五。确认机制通知到上层应用?

这是一个比较美好的愿望,上层应用在调用内核层接口发送大段数据,内核完成发送并且收到对方完整确认,然后通知上层应用已经发送成功,那么在一些环境下,可以节省不少业务层面交互步骤。

六。NAT网关超时

IPV4有限,局域网环境借助于NAT路由设备扩展了接入终端设备的数量。当建立一个TCP长连接时,NAT设备需要维护一个内部终端连接外部服务器所使用的内部IP:PORT与出去的IP:PORT映射对应关系。这个关系需要维护,比较耗费内存资源,有超时定时器清理,否则会导致内存撑爆。

不同NAT设备超时值不一样,因此才需要心跳辅助,确保经过NAT设备的连接一直保持,避免因过长的时间被踢掉。比如针对中国移动网络连接持久时间一般设置为不超过5分钟。各种网络略有差异,引入智能心跳机制比较合适。

七。终端IP漫游

手机终端经常在2G/3G/4G和WIFI之间切换,导致IP地址频繁发生改变。这样造成的后果就是已有的网络请求-响应被放弃和终止,需要人工干预或重新发起请求,存在资源浪费现象。

支持Multipath TCP的终端设备,可以同时利用 2G/3G/4G 和 WiFi 建立Mutlpath连接,通过多点优化网络下载,且互为备份。可以很好解决多个网络共存的情况下,一个网络中断不会导致全局请求处理中断,在设备的连接稳定和可靠性方面有所增强。

当然,服务器之间也可以利用Multipath TCP的多个网络增强网络吞吐量。

现状是:

  1. 目前只有IOS 7以及后续版本支持
  2. Linux kernel 3.10实验分支上可以看到其支持身影,但何时合并到主分支上,暂时未知

进阶阅读:A closer look at the scientific literature on Multipath TCP

八。TCP缓存膨胀

当路由器接收到的数据包超越其队列长度时,一般会随机丢包,以减少膨胀。针对上层应用程序而言,延迟增加,或误认为数据丢失,或连接丢失等。

遇到这种情况,一般建议快速发包,以避免丢失的数据部分。内核层面今早升级到最新版,不低于3.6即可。

进阶阅读:Bufferbloat

九。TCP不是绝对可靠的

  1. IP和TCP协议在头部都会有check sum错误校验和机制,16位表示,反码相加,结果求反,具体可参考 TCP校验和的原理和实现。一般错误很轻松可检测出来,但遇到两个16位数字相加后结果不变的情况就一筹莫展了
  2. 以太网帧CRC32校验一般情况下都很OK,但可能遇到两端隔离多个路由器情况下,就有可能出现问题,比如陈硕老师提供的一张图:

    上图中Client向Server发了一个TCP segment,这个segment先被封装成一个IP packet,再被封装成ethernet frame,发送到路由器(图中消息a)。Router收到ethernet frame (b),转发到另一个网段(c),最后Server收到d,通知应用程序。Ethernet CRC能保证a和b相同,c和d相同;TCP header check sum的强度不足以保证收发payload的内容一样。另外,如果把Router换成NAT,那么NAT自己会构造c(替换掉源地址),这时候a和d的payload不能用tcp header checksum校验。

  3. 路由器可能偶然出现硬件/内存故障导致收发IP报文出现多bit/单bit的反转或双字节交换,这个反转如果发生在payload区,那么无法用链路层、网络层、传输层的check sum查出来,只能通过应用层的check sum来检测。因此建议应用层要设法添加校验数据功能。

  4. 大文件下载添加校验保证数据完整性,一般采用MD5,也用于防止安全篡改

参考资料:

十。小结

在这个满世界都是TCP的环境下,要想对TCP动大手术,这个是不太可能的,因为它已经固化到已有的系统内核和固件中。比如升级终端(比如Android/IOS等)系统/固件,Linux服务器内核,中间设备/中介设备(如路由器等),这是一个浩大工程,目前看也不现实。

TCP位于系统内核层,内核空间的升级、修复,最为麻烦。服务器端升级还好说一些,用户终端系统的升级那叫一个难。用户空间/用户核的应用升级、改造相对比来说可控性强,基于此Google专家们直接在UDP协议上进行构建、并且运行在用户空间的QUIC协议,综合了UDP的轻量和TCP的可靠性,是一个比较新颖的方向。

若是对以后底层传输协议有所期望的话:

  • 在用户空间(用户核)出现可以定制的协议,类似于QUIC
  • 传统的TCP/UDP可以运行在用户空间,直接略过内核
  • 完整协议栈以静态链接库形式提供给上层应用
  • 上层应用可以在编译、打包的时包含其所依赖协议栈静态链接库so文件
  • dpdk/netmap等Packet IO框架 + 用户空间协议堆栈,数据将从网卡直接送达上层应用
  • Linux内核重要性降低,常规的SSH系统维护

虽然TCP存在这样、那样的问题,但目前还是无法绕过的网络基础设施,但稍微明白一些不足的地方,或许会对我们当前使用的现状有所帮助。



nieyong 2015-05-07 14:56 发表评论
07 May 00:41

Java 性能优化手册:提高 Java 代码性能的各种技巧

by importnewzz

Java 6,7,8 中的 String.intern – 字符串池

这篇文章将要讨论 Java 6 中是如何实现 String.intern 方法的,以及这个方法在 Java 7 以及 Java 8 中做了哪些调整。

字符串池

字符串池(有名字符串标准化)是通过使用唯一的共享 String 对象来使用相同的值不同的地址表示字符串的过程。你可以使用自己定义的 Map<String, String> (根据需要使用 weak 引用或者 soft 引用)并使用 map 中的值作为标准值来实现这个目标,或者你也可以使用 JDK 提供的 String.intern()

很多标准禁止在 Java 6 中使用 String.intern() 因为如果频繁使用池会失去控制,有很大的几率触发 OutOfMemoryException。Oracle Java 7 对字符串池做了很多改进,你可以通过以下地址进行了解 http://bugs.sun.com/view_bug.do?bug_id=6962931以及 http://bugs.sun.com/view_bug.do?bug_id=6962930

Java 6 中的 String.intern()

在美好的过去所有共享的 String 对象都存储在 PermGen 中 — 堆中固定大小的部分主要用于存储加载的类对象和字符串池。除了明确的共享字符串,PermGen 字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)

Java 6 中字符串池的最大问题是它的位置 — PermGen。PermGen 的大小是固定的并且在运行时是无法扩展的。你可以使用 -XX:MaxPermSize=N 配置来调整它的大小。据我了解,对于不同的平台默认的 PermGen 大小在 32M 到 96M 之间。你可以扩展它的大小,不过大小使用都是固定的。这个限制需要你在使用 String.intern 时需要非常小心 — 你最好不要使用这个方法 intern 任何无法控制的用户输入。这是为什么在 JAVA6 中大部分使用手动管理 Map 来实现字符串池

Java 7 中的 String.intern()

Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变 — 字符串池的位置被调整到 heap 中了。这意味着你再也不会被固定的内存空间限制了。所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。这 个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern()。

字符串池中的数据会被垃圾收集

没错,在 JVM 字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。这是用于所有版本的 Java,这意味着如果 interned 的字符串在作用域外并且没有任何引用 — 它将会从 JVM 的字符串池中被垃圾收集掉。

因为被重新定位到堆中以及会被垃圾收集,JVM 的字符串池看上去是存放字符串的合适位置,是吗?理论上是 — 违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。看起来是一个完美的节省内存的策略?在你回答这个之前,可以肯定的是你 需要知道字符串池是如何实现的。

在 Java 6,7,8 中 JVM 字符串池的实现

字符串池是使用一个拥有固定容量的 HashMap 每个元素包含具有相同 hash 值的字符串列表。一些实现的细节可以从 Java bug 报告中获得 http://bugs.sun.com/view_bug.do?bug_id=6962930

默认的池大小是 1009 (出现在上面提及的 bug 报告的源码中,在 Java7u40 中增加了)。在 JAVA 6 早期版本中是一个常量,在随后的 java6u30 至 java6u41 中调整为可配置的。而在java 7中一开始就是可以配置的(至少在java7u02中是可以配置的)。你需要指定参数 -XX:StringTableSize=N,  N 是字符串池 Map 的大小。确保它是为性能调优而预先准备的大小。

在 Java 6 中这个参数没有太多帮助,因为你仍任被限制在固定的 PermGen 内存大小中。后续的讨论将直接忽略 Java 6

Java 7 (直至 Java7u40)

在 Java7 中,换句话说,你被限制在一个更大的堆内存中。这意味着你可以预先设置好 String 池的大小(这个值取决于你的应用程序需求)。通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有 100 万字符串对象的字符串池分配 8-16M 的内存看起来是比较适合的(不要使用1,000,000 作为 -XX:StringTaleSize 的值 – 它不是质数;使用 1,000,003代替)

你可能期待关于 String 在 Map 中的分配 — 可以阅读我之前关于 HashCode 方法调优的经验。

你必须设置一个更大的 -XX:StringTalbeSize 值(相比较默认的 1009 ),如果你希望更多的使用 String.intern() — 否则这个方法将很快递减到 0 (池大小)。

我没有注意到在 intern 小于 100 字符的字符串时的依赖情况(我认为在一个包含 50 个重复字符的字符串与现实数据并不相似,因此 100 个字符看上去是一个很好的测试限制)

下面是默认池大小的应用程序日志:第一列是已经 intern 的字符串数量,第二列 intern 10,000 个字符串所有的时间(秒)

0; time = 0.0 sec
50000; time = 0.03 sec
100000; time = 0.073 sec
150000; time = 0.13 sec
200000; time = 0.196 sec
250000; time = 0.279 sec
300000; time = 0.376 sec
350000; time = 0.471 sec
400000; time = 0.574 sec
450000; time = 0.666 sec
500000; time = 0.755 sec
550000; time = 0.854 sec
600000; time = 0.916 sec
650000; time = 1.006 sec
700000; time = 1.095 sec
750000; time = 1.273 sec
800000; time = 1.248 sec
850000; time = 1.446 sec
900000; time = 1.585 sec
950000; time = 1.635 sec
1000000; time = 1.913 sec

测试是在 Core i5-3317U@1.7Ghz CPU 设备上进行的。你可以看到,它成线性增长,并且在 JVM 字符串池包含一百万个字符串时,我仍然可以近似每秒 intern 5000 个字符串,这对于在内存中处理大量数据的应用程序来说太慢了。

现在,调整 -XX:StringTableSize=100003 参数来重新运行测试:

50000; time = 0.017 sec
100000; time = 0.009 sec
150000; time = 0.01 sec
200000; time = 0.009 sec
250000; time = 0.007 sec
300000; time = 0.008 sec
350000; time = 0.009 sec
400000; time = 0.009 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.011 sec
600000; time = 0.012 sec
650000; time = 0.015 sec
700000; time = 0.015 sec
750000; time = 0.01 sec
800000; time = 0.01 sec
850000; time = 0.011 sec
900000; time = 0.011 sec
950000; time = 0.012 sec
1000000; time = 0.012 sec

可以看到,这时插入字符串的时间近似于常量(在 Map 的字符串列表中平均字符串个数不超过 10 个),下面是相同设置的结果,不过这次我们将向池中插入 1000 万个字符串(这意味着 Map 中的字符串列表平均包含 100 个字符串)

2000000; time = 0.024 sec
3000000; time = 0.028 sec
4000000; time = 0.053 sec
5000000; time = 0.051 sec
6000000; time = 0.034 sec
7000000; time = 0.041 sec
8000000; time = 0.089 sec
9000000; time = 0.111 sec
10000000; time = 0.123 sec

现在让我们将池的大小增加到 100 万(精确的说是 1,000,003)

1000000; time = 0.005 sec
2000000; time = 0.005 sec
3000000; time = 0.005 sec
4000000; time = 0.004 sec
5000000; time = 0.004 sec
6000000; time = 0.009 sec
7000000; time = 0.01 sec
8000000; time = 0.009 sec
9000000; time = 0.009 sec
10000000; time = 0.009 sec

如你所看到的,时间非常平均,并且与 “0 到 100万” 的表没有太大差别。甚至在池大小足够大的情况下,我的笔记本也能每秒添加1,000,000个字符对象。

我们还需要手工管理字符串池吗?

现在我们需要对比 JVM 字符串池和 WeakHashMap<String, WeakReference<String>> 它可以用来模拟 JVM 字符串池。下面的方法用来替换 String.intern

private static final WeakHashMap<String, WeakReference<String>> s_manualCache = 
    new WeakHashMap<String, WeakReference<String>>( 100000 );

private static String manualIntern( final String str )
{
    final WeakReference<String> cached = s_manualCache.get( str );
    if ( cached != null )
    {
        final String value = cached.get();
        if ( value != null )
            return value;
    }
    s_manualCache.put( str, new WeakReference<String>( str ) );
    return str;
}

下面针对手工池的相同测试:

0; manual time = 0.001 sec
50000; manual time = 0.03 sec
100000; manual time = 0.034 sec
150000; manual time = 0.008 sec
200000; manual time = 0.019 sec
250000; manual time = 0.011 sec
300000; manual time = 0.011 sec
350000; manual time = 0.008 sec
400000; manual time = 0.027 sec
450000; manual time = 0.008 sec
500000; manual time = 0.009 sec
550000; manual time = 0.008 sec
600000; manual time = 0.008 sec
650000; manual time = 0.008 sec
700000; manual time = 0.008 sec
750000; manual time = 0.011 sec
800000; manual time = 0.007 sec
850000; manual time = 0.008 sec
900000; manual time = 0.008 sec
950000; manual time = 0.008 sec
1000000; manual time = 0.008 sec

当 JVM 有足够内存时,手工编写的池提供了良好的性能。不过不幸的是,我的测试(保留 String.valueOf(0 < N < 1,000,000,000))保留非常短的字符串,在使用 -Xmx1280M 参数时它允许我保留月为 2.5M 的这类字符串。JVM 字符串池 (size=1,000,003)从另一方面讲在 JVM 内存足够时提供了相同的性能特性,知道 JVM 字符串池包含 12.72M 的字符串并消耗掉所有内存(5倍多)。我认为,这非常值得你在你的应用中去掉所有手工字符串池。

在 Java 7u40+ 以及 Java 8 中的 String.intern()

Java7u40 版本扩展了字符串池的大小(这是组要的性能更新)到 60013.这个值允许你在池中包含大约 30000 个独立的字符串。通常来说,这对于需要保存的数据来说已经足够了,你可以通过 -XX:+PrintFlagsFinal JVM 参数获得这个值。

我尝试在原始发布的 Java 8 中运行相同的测试,Java 8 仍然支持 -XX:StringTableSize 参数来兼容 Java 7 特性。主要的区别在于 Java 8 中默认的池大小增加到 60013:

50000; time = 0.019 sec
100000; time = 0.009 sec
150000; time = 0.009 sec
200000; time = 0.009 sec
250000; time = 0.009 sec
300000; time = 0.009 sec
350000; time = 0.011 sec
400000; time = 0.012 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.013 sec
600000; time = 0.014 sec
650000; time = 0.018 sec
700000; time = 0.015 sec
750000; time = 0.029 sec
800000; time = 0.018 sec
850000; time = 0.02 sec
900000; time = 0.017 sec
950000; time = 0.018 sec
1000000; time = 0.021 sec

测试代码

这篇文章的测试代码很简单,一个方法中循环创建并保留新字符串。你可以测量它保留 10000 个字符串所需要的时间。最好配合 -verbose:gc JVM 参数来运行这个测试,这样可以查看垃圾收集是何时以及如何发生的。另外最好使用 -Xmx 参数来执行堆的最大值。

这里有两个测试:testStringPoolGarbageCollection 将显示 JVM 字符串池被垃圾收集 — 检查垃圾收集日志消息。在 Java 6 的默认 PermGen 大小配置上,这个测试会失败,因此最好增加这个值,或者更新测试方法,或者使用 Java 7.

第二个测试显示内存中保留了多少字符串。在 Java 6 中执行需要两个不同的内存配置 比如: -Xmx128M 以及 -Xmx1280M (10 倍以上)。你可能发现这个值不会影响放入池中字符串的数量。另一方面,在 Java 7 中你能够在堆中填满你的字符串。

/**
 - Testing String.intern.
 *
 - Run this class at least with -verbose:gc JVM parameter.
 */
public class InternTest {
    public static void main( String[] args ) {
        testStringPoolGarbageCollection();
        testLongLoop();
    }

    /**
     - Use this method to see where interned strings are stored
     - and how many of them can you fit for the given heap size.
     */
    private static void testLongLoop()
    {
        test( 1000 * 1000 * 1000 );
        //uncomment the following line to see the hand-written cache performance
        //testManual( 1000 * 1000 * 1000 );
    }

    /**
     - Use this method to check that not used interned strings are garbage collected.
     */
    private static void testStringPoolGarbageCollection()
    {
        //first method call - use it as a reference
        test( 1000 * 1000 );
        //we are going to clean the cache here.
        System.gc();
        //check the memory consumption and how long does it take to intern strings
        //in the second method call.
        test( 1000 * 1000 );
    }

    private static void test( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++i )
        {
            final String str = "Very long test string, which tells you about something " +
            "very-very important, definitely deserving to be interned #" + i;
//uncomment the following line to test dependency from string length
//            final String str = Integer.toString( i );
            lst.add( str.intern() );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }

    private static final WeakHashMap<String, WeakReference<String>> s_manualCache =
        new WeakHashMap<String, WeakReference<String>>( 100000 );

    private static String manualIntern( final String str )
    {
        final WeakReference<String> cached = s_manualCache.get( str );
        if ( cached != null )
        {
            final String value = cached.get();
            if ( value != null )
                return value;
        }
        s_manualCache.put( str, new WeakReference<String>( str ) );
        return str;
    }

    private static void testManual( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++i )
        {
            final String str = "Very long test string, which tells you about something " +
                "very-very important, definitely deserving to be interned #" + i;
            lst.add( manualIntern( str ) );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; manual time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }
}

总结

  • 由于 Java 6 中使用固定的内存大小(PermGen)因此不要使用 String.intern() 方法
  • Java7 和 8 在堆内存中实现字符串池。这以为这字符串池的内存限制等于应用程序的内存限制。
  • 在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map 的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以 2 (减少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。
  • 在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 参数的值是 1009。Java7u40 以后这个值调整为 60013 (Java 8 中使用相同的值)
  • 如果你不确定字符串池的用量,参考:-XX:+PrintStringTableStatistics JVM 参数,当你的应用挂掉时它告诉你字符串池的使用量信息。

可能感兴趣的文章

06 May 00:32

C/C++编程的现代习惯

by ideawu

相对于汇编语言是一门操作 CPU 寄存器的语言, C/C++ 是一门操作内存的语言, 这是传统的观点. 但现代的程序应用开发, 大多是把 C/C++ 当作一门应用层语言, 所以必须适当地减少对内存的关注. 这也是本文所要讲的 - C/C++ 编程的现代习惯.

1. 不要害怕返回结构体和类的实例

在一些古董级的编程书里, 你绝对看不到返回结构体或者类的实例, 它们告诉你"不能返回局部变量的内存". 事实上, 返回结构体(类)的实例, 并不是把局部变量的内存(指针)返回给调用者使用, 而把局部变量复制到调用者栈上的内存. 而且, 很多情况下编译器会优化, 根本就不会发生内存拷贝.

返回结构体(类)的实例, 比返回 malloc() 分配的内存的指针在实践上具有更多的优势, 既能使代码更清晰, 也可以完全避免内存泄漏.

2. 不要害怕传递 STL 的 string

无论你把 STL 的 string 作为函数返回值还是参数, 都永远不要担心内存拷贝的问题, 永远不要! string 经过了良好的优化, 并且具有写时拷贝特性, 你将 string 传来传去, 就跟整数赋值的成本差不多. 相信我, 你可以这样认为.

3. 利用 string 来减少显式的内存分配和释放

STL 的 string 几乎可以完全替代 malloc/free 内存操作. 它有写时拷贝的特性, 它有自动扩大的特性, 你完全可以在许多场景用它来替代显式的内存分配, 而且利用它在退出作用域时自动释放内存的语言特性(和某些自动锁类似), 避免了内存泄露的可能性.

4. 记住, STL 的 string 不是字符串!

记住, string 不是字符串, 它是一段内存, 内存中的每一个字节可以是任意值, 多个 '\0' 字符也可以出现在 string 中. 只有当你调用了它的 c_str() 方法, 它才和 C 语言的字符串有联系, 在你调用 c_str() 之前, 记住, string 不是字符串!

5. 你要理解代码导致内存的变化, 但不要被内存限制

C/C++ 语言是一门操作内存的语言, 这是永远的基础. 你必须理解你的每一行代码导致的内存的变化, 这样你才能正确地进行 C/C++ 编程. 但是, 理解你的业务, 快速地封装出内存相关的核心代码, 然后把内存忘记.

Related posts:

  1. 小心 int 乘法溢出!
  2. 使用 jemalloc 编译过程出错的问题
  3. 编写JSP/PHP+MySQL留言本
  4. SSDB 使用 jemalloc
  5. 百行代码实现一个简单的Zset(SortedSet)
06 May 00:32

文本三巨头:zsh、tmux 和 vim

by 鸭梨山大

罗马三巨头

公元前62年,凯撒 组建了一个包含了他自己, 政治家克拉苏,以及军事领袖庞培三人的政治联盟。 这三个人一起组成了一个秘密政治小组,称为 Triumvirate(三巨头),来统治罗马共和国。 而文本三巨头则是 zshvimtmux。 这三个令人尊敬的工具本身已经非常强大,然而它们的组合却更加所向披靡,把其他文本编辑组合甩开了 N 条街。本文旨在向刚接触各类工具的新手们简述如何建立一个既强大又容易配置的文本三巨头。我想把主要的篇幅放在如何将 zsh、vim 和 tmux 整合起来,并主要讲述了我如何解决两个常见的问题——复制/粘贴功能和颜色配置。

我的愚见

Rands一样,我对工具非常痴狂。我认为文本三巨头是最强大的文本编辑的工具链。如果你不使用这个工具链,那么我会建议你先干了这杯酒,然后尝试使用文本三巨头。如果你每天花费大量的时间在文本中纠缠,那么你更应该接受我的建议。一开始换工具或许会有些不习惯,但是你的努力会得到回报的。使用 zsh、vim 和 tmux 的好处就在于免费使用,速度快,可任意定制,在任何操作系统上都能使用,可在远程环境中使用,还在于可以实现远程结对编程,以及互相之间,和与 Unix 之间深度的整合。最终纯文本编辑的效率和组织性将会得到很大提升。该工具链可以完全由 git 管理,并且可以再几秒钟的时间内克隆到一台远程服务器或是一台新的机器上。总的来说,它们的这些优点让使我在写作和编程上变得更快,更有效率。

文本三巨头的一个巨大的优势在于对用于管理工作环境的分屏模型的普遍使用。分屏模型管理允许tmux像粘合剂一样组织工作流。通常在一天的结尾,我会发现我留下了一些shell窗口和一大堆的临时文件,数据文件,源代码文件,文档文件,还有打开的数据库。把这些窗口一个个关掉然后第二天再把它们打开是非常痛苦的一件事。tmux和vim支持对一个特定的项目打开大量的窗格和窗口,如果你希望转换到另一个完全不同的项目,你可以从这些窗口分离出来转向另外一个项目,然后再按原样返回这些窗口。在一时间段内,我通常同时在多个工作和个人的项目上进行工作。在多个工作环境中来回切换的能力对我来说非常重要。(Thoughtbot blog 中有对 tmux 中窗口和窗格的使用的讲解)

下面是——包装在tmux中的zsh和vim:

该tmux会话中有三个分别命名为demo、docs和scatch的窗口,然而在截图中只有最上面的窗口是可见的。在这个窗口中有四个分区。左上角的分区是一个zsh窗口,左下角的分区是一个交互的python会话,右上角的窗格是用vim打开的python代码,然后右下角是包含markdown文档的窗格。

外观设置

我建议给文本三巨头设置两种颜色主题——一个主题给工作上的项目而另外一个给个人项目。我是情景依赖记忆的重度使用者,因此使用两个主题在认识和区分工作项目和个人项目上给予我很大的帮助。如图,下面是我的个人主题(左),以及工作主题(右)。两个主题都是Ethan Schoonover 的 solarized 项目中的版本。我在玩的时候使用暗调主题,是因为我通常在清晨或傍晚天空还处在黑暗中时搞自己的项目。暗调主题可以在这些时候让我的眼睛得到舒缓。关于字体,我用的是 14 point 的 Inconsolata

安装

首先要做的事将大写锁定键(Caps Lock)重映射到Control 键上。大写锁定键是个历史遗留问题,这个在键盘上的黄金位置的键需要被更好的利用。在tmux中对Control键的使用非常频繁,因此将Control键重映射到一个符合人体工程学的位置对我们很有帮助。

想要给三巨头创建一个强大的工作环境,我们可以下载 iTerm2 终端模拟器。iTerm2 比普通的终端应用具有更强的性能,更多的特性和更灵活的定制化。当你开始使用iTerm2时,请回头阅读全部文档看看它能为你做什么。其中一个特性是Command-?,显示出一个视窗帮助你快速地找到你当前的光标位置。大部分iTerm2非常酷炫的功能本文都没有提及。请确保你了解了iTerm2的即时回放,正则查询,点击打开URL,以及标记跳转的功能。

当iTerm2安装完成,即可添加亮调和暗调主题。solarized 库中含有iTerm2调色板和 配置iTerm2主题的说明,所以它的安装简洁明了。另一项对使用iTerm2有用的配置是启用系统级别的绑定键,通过该键可以让iTerm2转为最前面的窗口。我发觉设置一个具体的绑定比使用应用切换器(Command-Tab)要快的多。该设置在Preferences > Keys中,而我使用的绑定键是Option-t。关于自定义,我还有一个建议,那就是在 Profiles > Terminal > Notifications中撤销选中iTerm2 的响铃声。

由于文本三巨头的操作高度集中在键盘上,因此,在你配置和形成自己的肌肉记忆之前,将iTerm, zsh, vim, tmux,和其他任何你之前使用的工具之间的快捷键冲突消除是非常明智的选择。做窗口移动时,我使用Option 键。Option-t将iTerm2移到屏幕前,而Option-i将Twitter移到屏幕前,等等。我还使用Moom 作为我在OS X上的平铺式窗口管理器,并将所有的快捷键配置为使用Option 键将窗口移至屏幕上特定的展示窗口或位置上。

接下来,安装Homebrew 然后使用它去安装git,MacVim,tmux和reattach-to-user-namespace(返回用户命名空间)。安装MacVim有两个原因。第一,默认的OS X自带的vim似乎对很多人来说很慢。我发现使用MacVim中的vim比OS X版本的vim要快很多。另外一个安装MacVim的好处是你的系统将得到一个更新版本的vim。第二个原因则是复制/粘贴的使用在OS X版本的vim中并没有得到优化。

安装完git,就可以新建一个存储库来放置文本三巨头的设置文件。我的存储库命名为dotfiles 并存储了我的所有zsh, vim, and tmux配置文件。如果你不知道怎么为你的文件设置版本控制,请阅读Pro Git 或者 Git Immersion

ZSH

已经有很多文章写到了如何使用zsh以及为什么zsh比bash强大。基本上,bash有的功能zsh都有,而且zsh的一些特性bash是没有的。我使用zsh而不是bash是因为它有扩展的globbing(通配符),更好用的tab补全,内建的拼写纠正,一个更好的计算器(zcalc),以及一个内建的批重命名文件工具(zmv)。zsh的另外一个杀手级特色是oh-my-zsh——一个zsh的社区驱动的框架。oh-my-zsh预先打包好了很不错的主题,插件,以及让zsh极度强大的配置。如果你想学习本文的话,请安装iTerm2并将zsh作为你的默认shell。

我将我的 .zshrc、 .vimrc 和 .tmux.conf 配置文件保存在 dotfiles 目录中,并用 symlink 在 home 目录下创建链接。这样我就能只在一个目录里做zsh、vim 和tmux的配置的版本控制了。文本三巨头使用了vim,那么我们应该让zsh和tmux也使用vim以及它的绑定键并将vim设置为默认编辑器。将下面的文本加到.zshrc文件中,让zsh支持vim:

export EDITOR="vim"
bindkey -v 

# vi style incremental search
bindkey '^R' history-incremental-search-backward
bindkey '^S' history-incremental-search-forward
bindkey '^P' history-search-backward
bindkey '^N' history-search-forward

zsh不仅支持大多数bash命令,还支持更多的智能命令。比如,如果你想在bash中移动到一个目录里,你可能会输入cd foo。而在zsh中如果你将下面一行加入到.zshrc中,你只需要输入foo即可。

setopt AUTO_CD

为了设置一个好的命令行提示,我参考了Steve Losh’s excellent prompt然后做了一些小改动。只需要简单地在oh-my-zsh/themes/中创建一个新的主题文件并在你的zshrc文件中添加一行对应你的主题文件的文本(ZSH_THEME=bunsen)。这是我的修改后的Steve的命令行提示:

function virtualenv_info {
    [ $VIRTUAL_ENV ] && echo '('`basename $VIRTUAL_ENV`') '
}

function box_name {
    [ -f ~/.box-name ] && cat ~/.box-name || hostname -s
}

PROMPT='
%{$fg[magenta]%}%n%{$reset_color%} at %{$fg[yellow]%}$(box_name)%{$reset_color%} in %{$
fg_bold[green]%}${PWD/#$HOME/~}%{$reset_color%}$(git_prompt_info)
$(virtualenv_info)%(?,,%{${fg_bold[blue]}%}[%?]%{$reset_color%} )$ '

ZSH_THEME_GIT_PROMPT_PREFIX=" on %{$fg[magenta]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[green]%}!"
ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[green]%}?"
ZSH_THEME_GIT_PROMPT_CLEAN=""

local return_status="%{$fg[red]%}%(?..⤬)%{$reset_color%}"
RPROMPT='${return_status}%{$reset_color%}'

VIM

下面我会将注意力放在vim与文本三巨头的整合而不是vim本身。为了将solarized整合到vim中,你需要安装vim solarized plugin然后将下面的内容放到你的vimrc里:

syntax enable
let g:solarized_termtrans = 1
colorscheme solarized
togglebg#map("<F5>")

终端中的颜色管理会比较复杂。在我的系统中,为了在终端的vim中得到合适的颜色渲染,我特地加了let g:solarized_termtrans = 1。Solarized 提供了内建后台函数,让你可以使用&lt;F5&gt;在亮调和暗调主题之间切换,因此如果你需要这个功能就需要加上上面的内容的最后一行。你还可以在vim里运行:set background=dark 或者 :set background=light去实现同样的功能。

vim对复制/粘贴的处理跟基于GUI的文本编辑器有些不同。vim有许多复制寄存器和一些粘贴模式,而不是一个单一的复制/粘贴机制。我向我的vimrc里添加了下列内容,使复制/粘贴机制更加直观。

" Yank text to the OS X clipboard" 将文本复制到OS X剪贴板中
noremap <leader>y "*y
noremap <leader>yy "*Y

" Preserve indentation while pasting text from the OS X clipboard 在粘贴OS X剪贴板中的文本时保留缩进
noremap <leader>p :set paste<CR>:put *<CR>:set nopaste<CR>

 

上面的映射大幅提升了OS X系统剪贴板的可用性。前两个命令分别复制选中的文本或一行内容到系统剪贴板中。最后一行则使粘贴的文本的格式维持不变。在实践中我发现我并不需要进行很多对vim里外文本的粘贴。如果我需要分享代码,我通常会使用vim gist plugin,这比复制/粘贴要快多了。

TMUX

tmux像胶水一样将文本三巨头紧密联系在一起。我在上个月才开始使用tmux,但我惊讶地发现它现在对我的工作流来说是如此的不可或缺。下面是维基百科对tmux的描述:

tmux是一个用于终端复用的软件,它允许一个用户在一个终端窗口或远程终端会话中使用多个不同的终端会话。在同一个命令行接口处理多个程序,以及将程序从已经开始运行另外的程序的Unix shell中分离出来,是非常有用的。

从本质上来说,tmux允许你创建会话,只要你愿意,你可以随时离开或返回该会话。tmux非常的宝贵,因为你可以根据上下文去安排你的工作。

就像vim一样,设置和使用tmux最难的部分就是颜色管理和用到系统剪贴板的复制/粘贴功能。通过确保tmux知道你使用的256色来创建合适的solarized颜色是非常简单直白的。将下面的内容添加到你的tmux.conf文件中:

set -g default-terminal "screen-256color"

关于复制/粘贴,tmux有一个特别的复制模式。tmux的复制模式命令以一个前缀键开头。默认的前缀键是Control-b。大多数人,包括我自己,都会重映射前缀键为Control-a,因为这样容易使用多了,而且这还是GNU screen的默认绑定键。当你看到我在下面提到prefix,我指的都是Control-a。因此&lt;prefix&gt; c的意思就是:点击Control-a再点击c

tmux里的复制/粘贴在OS X中完全不起作用。幸运的是,Chris Johnsen创建了一个好用的,很容易通过 Homebrew 安装的补丁,名为 reattach-to-user-namespace(返回到用户命名空间)。Thoughtbot 里的人们有一些很实用的博文解释了如何使用tmux和如何使复制/粘贴功能运行起来(看)。然而读完这些教程,我开始还是没搞懂如何在OS X剪切板中使用tmux,因此下面就是你安装完 reattach-to-user-namespace 后,你需要向你的tmux.conf文件中添加的内容:

set -g default-command "reattach-to-user-namespace -l zsh"

set -g mode-mouse on
setw -g mouse-select-window on
setw -g mouse-select-pane on

# Copy mode
setw -g mode-keys vi
bind ` copy-mode
unbind [
unbind p
bind p paste-buffer
bind -t vi-copy v begin-selection
bind -t vi-copy y copy-selection
bind -t vi-copy Escape cancel
bind y run "tmux save-buffer - | reattach-to-user-namespace pbcopy"

第一行设置令tmux使用 wrapper 程序给每个新打开的tmux窗口去启动zsh。接下来的三行是我个人对tmux里鼠标操作的设置。你可以保留或删掉这三行,这取决于你自己的需求。真正的干货在接下来的十行,它们用于处理复制模式。

除了vim和OS X的复制/粘贴缓存外,tmux有它自己的复制/粘贴缓存。为了高效地使用tmux缓存,可以点击 ` 键来进入复制模式。我已经将默认的复制绑定重映射为跟vi类似的绑定。为了将文本放入tmux的复制/粘贴缓存中,可以点击v去做出文本的选定然后点击y复制选中项。此时,所选的文本就被放在tmux复制/粘贴缓存中。输入&lt;prefix&gt; p可以粘贴该文本。不过,如果你想将文本放入OS X的复制/粘贴缓存里,你需要输入&lt;prefix&gt; y

插件

要是我没提及一些非常棒的特别与文本三巨头融合的很好的开源项目,就是我的不对了。我就不深入地一个一个说这些工具了,下面是一些我最喜欢的项目的链接以及简介:

  • Ack—比grep要好
  • Autojump—目录导航
  • Command-t—用于模糊查询的vim插件;(点击链接了解如何在tmux中设置)
  • Pandoc—格式转换
  • Poweline-vim—定制vim状态栏
  • Pianobar—终端Pandora 音乐播放器
  • pdfgrep—PDF文件的grep
  • shelr—shell中的屏幕录制工具
  • vimux—用vim与tmux交互
  • virtualenv—Python虚拟环境创建工具
  • wemux—多用户终端共享器
  • yadr—一套zsh,MacVim,和git 的配置文件

更新

一些朋友问我如何像上面的截屏里一样在tmux中设置漂亮的状态栏。我是从wemux project中学到的。如果你已经安装了vim-powerline 并且正在使用补充的字体,你只需要向你的tmux.conf中加入下面的内容去得到我的状态栏样式。感谢Matt Furden

set -g status-left-length 52
set -g status-right-length 451
set -g status-fg white
set -g status-bg colour234
set -g window-status-activity-attr bold
set -g pane-border-fg colour245
set -g pane-active-border-fg colour39
set -g message-fg colour16
set -g message-bg colour221
set -g message-attr bold
set -g status-left '#[fg=colour235,bg=colour252,bold] ❐ #S
#[fg=colour252,bg=colour238,nobold]⮀#[fg=colour245,bg=colour238,bold] #(whoami)
#[fg=colour238,bg=colour234,nobold]⮀'
set -g window-status-format "#[fg=white,bg=colour234] #I #W "
set -g window-status-current-format
"#[fg=colour234,bg=colour39]⮀#[fg=colour25,bg=colour39,noreverse,bold] #I ⮁ #W
#[fg=colour39,bg=colour234,nobold]⮀"

文本三巨头:zsh、tmux 和 vim,首发于博客 - 伯乐在线

06 May 00:30

Java 容器 & 泛型:五、HashMap 和 TreeMap的自白

by BYScoket

Java 容器的文章这次应该是最后一篇了:Java 容器 系列:HashMap 和 TreeMap的自白 阅读原文 »

The post Java 容器 & 泛型:五、HashMap 和 TreeMap的自白 appeared first on 头条 - 伯乐在线.

06 May 00:29

高级Java程序员值得拥有的10本书

by importnewzz

Java是时下最流行的编程语言之一。市面上也出现了适合初学者的大量书籍。但是对于那些在Java编程上淫浸多时的开发人员而言,这些书的内容未免显得过于简单和冗余了。那些适合初学者的书籍看着真想打瞌睡,有木有。想找高级点的Java书籍吧,又不知道哪些适合自己。

别急,雪中送炭的来了:下面我将分享的书单绝对值得拥有。ps,我也尽力避免列出为特定软件或框架或认证的Java书,因为我觉得那不是纯Java书。

1.《Java in a Nutshell》(Java技术手册)

与其说是必读书籍,还不说是参考文献。

2.《The elements of Java style》(Java编程风格

目标读者就是Java程序员。通过提出一系列的Java从业规则,以及一些标准、惯例和准则,来说明如何有助于编写可靠又易于理解和维护的Java代码。

3.《Effective Java》(通用程序设计)

这本书真的只适合那些深入了解Java的开发人员。它汇集了78种不可或缺的程序员经验法则:为你每天在工作中都会遇到的编程挑战,提出了实践的最佳解决方案。

4.《The Java language specification》(Java编程规范)

作者为Java的发明者,这本书不仅提供了完整和准确的语言覆盖范围,还包含了实际编译行为时的正式语言规则。虽然阅读这本书不能让你学到什么技能,但是如果你想在Java VM更进一步的话,那就非读不可。

5.《Design patterns: elements of reusable object-oriented software》(设计模式:可复用面向对象软件的元素)

其实,这本书中的例子是用C ++和Smalltalk写的,是不是很奇怪为什么我还要推荐它呢?如果你想成长为一个开发人员,那么你就必须知道设计模式,这样才能充分利用他人最佳的实践经验,以及还可以向那些面临过相同问题的开发人员学习。当然其他类似的书籍还有很多,但它们都只能当做一些辅助性的学习。

6.《The Pragmatic Programmer: From Journeyman to Master》(程序员的修炼:从中级到大师)

此书并不只适合于Java开发人员。 “这本书之所以值得推荐,其原因是它大大保持了编程过程的新鲜度,还有助于我们从前人那里汲取力量、不断地自我成长。”

7.《Patterns of Enterprise Application Architecture》(企业应用架构模式)

学会了设计模式之后该如何应用到企业框架中呢?这本书介绍了很多常见的企业设计模式。

8.《Refactoring: Improving the Design of Existing Code》(重构:改善现有代码设计)

如果你已经在编程行业淫浸过几年了,那么你一定得读一读这本书。重构可以使得代码可读性更强,也更容易维护。

9.《OSGi in Action: Creating Modular Applications in Java》(OSGi实战:用Java创建模块化应用)

无论如何,了解一下面向服务的编程是怎么回事,总归不是坏事。这本书的前几章就给出了非常不错的入门介绍和具体的例子。

10.《Clean Code: A Handbook of Agile Software Craftsmanship》(代码整洁之道

最后但并非最不重要的,时不时地检查编码风格总是对的。 开发人员90%的精力是花在维护上的,所以干净的代码真的非常重要。

作为程序员,你爱上读书了吗?

相关文章

06 May 00:29

Java 8里面lambda的最佳实践

by importnewzz

Java 8已经推出一段时间了,越来越多开发人员选择升级JDK,这条热门动弹里面看出,JDK7最多,其次是6和8,这是好事!

在8 里面Lambda是最火的主题,不仅仅是因为语法的改变,更重要的是带来了函数式编程的思想,我觉得优秀的程序员,有必要学习一下函数式编程的思想以开阔思路。所以这篇文章聊聊Lambda的应用场景,性能,也会提及下不好的一面。

Java为何需要Lambda

1996年1月,Java 1.0发布了,此后计算机编程领域发生了翻天覆地的变化。商业发展需要更复杂的应用,大多数程序都跑在更强大的装备多核CPU的机器上。带有高效运行期编译器的Java虚拟机(JVM)的出现,使得程序员将精力更多放在编写干净、易于维护的代码上,而不是思考如何将每一个CPU时钟、每一字节内存物尽其用。

多核CPU的出现成了“房间里的大象”,无法忽视却没人愿意正视。算法中引入锁不但容易出错,而且消耗时间。人们开发了java.util.concurrent包和很多第三方类库,试图将并发抽象化,用以帮助程序员写出在多核CPU上运行良好的程序。不幸的是,到目前为止,我们走得还不够远。

那些类库的开发者使用Java时,发现抽象的级别还不够。处理大数据就是个很好的例子,面对大数据,Java还欠缺高效的并行操作。Java 8允许开发者编写复杂的集合处理算法,只需要简单修改一个方法,就能让代码在多核CPU上高效运行。为了编写并行处理这些大数据的类库,需要在语言层面上修改现有的Java:增加lambda表达式。

当然,这样做是有代价的,程序员必须学习如何编写和阅读包含lambda表达式的代码,但是,这不是一桩赔本的买卖。与手写一大段复杂的、线程安全的代码相比,学习一点新语法和一些新习惯容易很多。开发企业级应用时,好的类库和框架极大地降低了开发时间和成本,也扫清了开发易用且高效的类库的障碍。

如果你还未接触过Lambda的语法,可以看这里

Lambda的应用场景

你有必要学习下函数式编程的概念,比如函数式编程初探,但下面我将重点放在函数式编程的实用性上,包括那些可以被大多数程序员理解和使用的技术,我们关心的如何写出好代码,而不是符合函数编程风格的代码。

1.使用() -> {} 替代匿名类

现在Runnable线程,Swing,JavaFX的事件监听器代码等,在java 8中你可以使用Lambda表达式替代丑陋的匿名类。

//Before Java 8:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Before Java8 ");
    }
}).start();
 
//Java 8 way:
new Thread(() -> System.out.println("In Java8!"));
 
// Before Java 8:
JButton show =  new JButton("Show");
show.addActionListener(new ActionListener() {
     @Override
     public void actionPerformed(ActionEvent e) {
           System.out.println("without lambda expression is boring");
        }
     });
 
 
// Java 8 way:
show.addActionListener((e) -> {
    System.out.println("Action !! Lambda expressions Rocks");
});

2.使用内循环替代外循环

外循环:描述怎么干,代码里嵌套2个以上的for循环的都比较难读懂;只能顺序处理List中的元素;

内循环:描述要干什么,而不是怎么干;不一定需要顺序处理List中的元素

//Prior Java 8 :
List features = Arrays.asList("Lambdas", "Default Method", 
"Stream API", "Date and Time API");
for (String feature : features) {
   System.out.println(feature);
}
 
//In Java 8:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
 "Date and Time API");
features.forEach(n -> System.out.println(n));
 
// Even better use Method reference feature of Java 8
// method reference is denoted by :: (double colon) operator
// looks similar to score resolution operator of C++
features.forEach(System.out::println);
 
Output:
Lambdas
Default Method
Stream API
Date and Time API

3.支持函数编程

为了支持函数编程,Java 8加入了一个新的包java.util.function,其中有一个接口java.util.function.Predicate是支持Lambda函数编程:

public static void main(args[]){
  List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
 
  System.out.println("Languages which starts with J :");
  filter(languages, (str)->str.startsWith("J"));
 
  System.out.println("Languages which ends with a ");
  filter(languages, (str)->str.endsWith("a"));
 
  System.out.println("Print all languages :");
  filter(languages, (str)->true);
 
   System.out.println("Print no language : ");
   filter(languages, (str)->false);
 
   System.out.println("Print language whose length greater than 4:");
   filter(languages, (str)->str.length() > 4);
}
 
 public static void filter(List names, Predicate condition) {
    names.stream().filter((name) -> (condition.test(name)))
        .forEach((name) -> {System.out.println(name + " ");
    });
 }
 
Output:
Languages which starts with J :
Java
Languages which ends with a
Java
Scala
Print all languages :
Java
Scala
C++
Haskell
Lisp
Print no language :
Print language whose length greater than 4:
Scala
Haskell

4.处理数据?用管道的方式更加简洁

Java 8里面新增的Stream API ,让集合中的数据处理起来更加方便,性能更高,可读性更好

假设一个业务场景:对于20元以上的商品,进行9折处理,最后得到这些商品的折后价格。

final BigDecimal totalOfDiscountedPrices = prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(price -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO,BigDecimal::add);
 
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);

想象一下:如果用面向对象处理这些数据,需要多少行?多少次循环?需要声明多少个中间变量?

关于Stream API的详细信息,可以查看我之前写的文章 。

Lambda的性能

Oracle公司的性能工程师Sergey Kuksenko有一篇很好的性能比较的文档: JDK 8: Lambda Performance study, 详细而全面的比较了lambda表达式和匿名函数之间的性能差别。这里是视频。 16页讲到最差(capture)也和inner class一样, non-capture好的情况是inner class的5倍。

lambda开发组也有一篇ppt, 其中也讲到了lambda的性能(包括capture和非capture的情况)。看起来lambda最差的情况性能内部类一样, 好的情况会更好。

Java 8 Lambdas – they are fast, very fast也有篇文章 (需要翻墙),表明lambda表达式也一样快。

Lambda的阴暗面

前面都是讲Lambda如何改变Java程序员的思维习惯,但Lambda确实也带来了困惑

JVM可以执行任何语言编写的代码,只要它们能编译成字节码,字节码自身是充分OO的,被设计成接近于Java语言,这意味着Java被编译成的字节码非常容易被重新组装。

但是如果不是Java语言,差距将越来越大,Scala源码和被编译成的字节码之间巨大差距是一个证明,编译器加入了大量合成类 方法和变量,以便让JVM按照语言自身特定语法和流程控制执行。

我们首先看看Java 6/7中的一个传统方法案例:

// simple check against empty strings
public static int check(String s) {
    if (s.equals("")) {
        throw new IllegalArgumentException();
    }
    return s.length();
}
  
//map names to lengths
  
List lengths = new ArrayList();
  
for (String name : Arrays.asList(args)) {
    lengths.add(check(name));
}

如果一个空的字符串传入,这段代码将抛出错误,堆栈跟踪如下:

at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

再看看Lambda的例子

Stream lengths = names.stream().map(name -> check(name));
  
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

这非常类似Scala,出错栈信息太长,我们为代码的精简付出力代价,更精确的代码意味着更复杂的调试。

但这并不影响我们喜欢Lambda!

总结

在Java世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。

这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。

在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。

总而言之,Java更趋于完美了。

相关文章

05 May 09:45

关于C++引用的一些注意点

by promumu

C++的引用首先跟指针的最大区别就是引用不是一个对象,而指针是一个对象;其次引用在其定义时就要初始化,而指针可以不用。

int    val = 42;
int    &rval = val;

此时rval就绑定了val,其实就是rval就是val的一个别名。你修改了两个其中的一个,其值都会改变。

因为引用在一开始就初始化了,所以一个引用只能引用一个变量。还有,引用只能引用对象,也就是有地址的,不能是一个常数或者表达式。而且类型要匹配。

int    &rval = 10;    
//error: initializer must be an object
double    dval = 3.14;
int    &rdval = dval;   
 //error: initializer must be an int object

 

References to const  常量引用

不同于非常量的引用,常量引用所引用的对象不能修改

const    int    ci = 1024;
const    int    &r1 = ci;
r1 = 42;    
//error: r1 is a reference to const
int    &r2 = ci;    
//error: nonconst reference to a const object

因为ci是一个常量,所以我们不能直接用一个引用来引用ci,因为我们不能修改ci。

上面提到引用要引用正确的自身的类型,但是常量引用可以引用一个非const的对象,一个数,或者表达式。

int    i = 42;
const    int    &r1 = i;       //ok
const    int    &r2 = 42;    //ok 
const    int    &r3 = r1 * 2;  //ok
int    &r4 = r * 2;    
//error: 非常量引用不能引用一个表达式

让我们想想这是为什么?

double    dval = 3014;
const    int    &ri = dval;    //ok

其实编译器帮我们多做了一步

double    dval = 3014;
const    int    temp = dval;    
//创建一个暂时的常量对象来存放dval             
const    int    &ri = temp;      
//将引用到这个暂时的常量

正因为有这个无名的中转存量,所以常量引用才可以引用数,表达式,还有不同类型的对象。

那为什么非常量引用就不能这样呢?

想想,刚才说的编译器帮我们弄了一个中转对象,其实我们引用是引用它,修改也修改它, 但它是无名的,也就是找不到地址,也无法找着。修改了也没用,我们是要修改dval(在上面列子中)。

所以说,只有常量引用才可以引用数字,表达式,不同类型的对象。因为我们不打算修改它,所以那个中转变量真的只是负责传递值,让常量引用初始化而已。

最后一个例子

int    i = 42;
int    &r1 = i;
const    int    &r2 = i;
r1 = 0;    //ok
r2 = 0;    //error

这样非常量引用和常量引用都引用一个值是可以的。非常量引用就不用说了,跟对象绑在一起,是别名,修改引用的同时也就修改了对象本身的内容。而常量引用,就是跟上面一样,编译器帮我们创建了一个无名的中转变量来储存,其实也就是赋值给常量对象初始化。你不能修改它就是了。

关于C++引用的一些注意点,首发于博客 - 伯乐在线

05 May 09:41

理解 TCP/IP 网络栈 & 编写网络应用

by Leo

1.译注

之前在网上看到了这篇描述tcp网络栈原理的文章,感觉不错,决定抽空把这篇文章翻译一下。一来重新温习一下TCP相关知识,二来练练英文。 很久没翻译文章了难免有误,建议有能力的同学还是看一下原文。

2.概述

我们难以想象没有了TCP/IP之后的网络服务。所有我们开发并在NHN使用的网络服务都基于TCP/IP这个坚实的基础。理解数据如何通过网络传输可以帮助你通过调优、排查或引进新技术之类的手段提升性能。

本文将基于Linux OS和硬件层的数据流和控制流来描述网络栈的整体运行方式。

3.TCP/IP的关键特性

我如何设计一个能在快速传输数据的同时保证数据顺序并且不丢失数据的网络协议?TCP/IP在设计时就基于这个考虑。以下是在了解整体网络栈前需要知道的TCP/IP的主要特性:

TCP和IP
从技术上讲,TCP和IP处于不同的层,应该分别解释它们。但在这里我们把他们看做一个整体。

  1. 面向连接首先,传输数据前需要在两个终端之间建立连接(本地和远程)。在这里,“TCP连接标识符(TCP connection identifier)”是两个终端地址的组合,类似本地ip地址,本地端口号,远程ip地址,远程端口号的形式。
  2. 双向字节流通过字节流实现双向数据通信。
  3. 顺序投递接收者在接收数据时与发送者发送的数据顺序相同。因此,数据需要是有序的,为了表示这个顺序,TCP/IP使用了32位的int数据类型。
  4. 通过ACK实现可靠性当发送者向接收者发送数据,但没有收到来自接收方的ACK(acknowledgement,应答)时,发送者的TCP层将重发数据。因此,发送者的TCP层会把接收者还没有应答的数据暂存起来。
  5. 流量控制发送者的发送速度与接收者的接收能力相关。接收者会把它能接收的最大字节数(未使用的缓冲区大小,又叫接收窗口,receive window)告知发送者。发送者发送的最大字节数与接收者的接收窗口大小一致。
  6. 阻塞控制阻塞窗口是不同于接收窗口的另一个概念,它通过限制网络中的数据流的体积来防止网络阻塞。类似于接收窗口,发送者通过通过一些算法(例如TCP Vegas,Westwood,BIC,CUBIC)来计算发送对应的接收者的阻塞窗口能容纳的最多的数据。和流量控制不同,阻塞控制只在发送方实现。(译注:发送者类似于通过ack时间之类的算法判断当前网络是否阻塞,从而调节发送速度)

4.数据传输

网络栈有很多层。图一表示了这些层的类型:

图1:发送数据时网络栈中各层对数据的操作

这些层可以被大致归类到三个区域中:

  1. 用户区
  2. 内核区
  3. 设备区

用户区和内核区的任务由CPU执行。用户区和内核区被叫做“主机(host)”以区别于设备区。在这里,设备是发送和接收数据包的网络接口卡(Network Interface Card,NIC)。它有一个更常用的术语:网卡。

我们来了解一下用户区。首先,应用程序创建要发送的数据(图1中的“User Data”)并且调用write()系统调用来发送数据(在这里假设socket已经被创建了,对应图1中的“fd”)。当系统调用被调用之后上下文切换到内核区。

像Linux或者Unix这类POSIX系列的操作系统通过文件描述符(file descriptor)把socket暴露给应用程序。在这类系统中,socket是文件的一种。文件系统执行简单的检查并调用socket结构中指向的socket函数。

内核中的socket包含两个缓冲区。

  1. 一个用于缓冲要发送的数据
  2. 一个用于缓冲要接收的数据

write()系统调用被调用时,用户区的数据被拷贝到内核内存中,并插入到socket的发送缓冲区末尾。这样来保证发送的数据有序。在图1中,浅灰色框表示在socket缓冲区中的数据。之后,TCP被调用了。

socket会关联一个叫做TCP控制块(TCP Control Block)的结构,TCB包含了处理TCP连接所需的数据。包括连接状态(LISTENESTABLISHEDTIME_WAIT),接收窗口,阻塞窗口,顺序号,重发计时器,等等。

如果当前的TCP状态允许数据传输,一个新的TCP分段(TCP segment,或者叫数据包,packet)将被创建。如果由于流量控制或者其它原因不能传输数据,系统调用会在这里结束,之后会返回到用户态。(换句话说,控制权会交回到应用程序代码)

TCP分段有两部分

  1. TCP头
  2. 携带的数据

图2:TCP帧的结构

TCP数据包的payload部分会包含在socket发送缓冲区里的没有应答的数据。携带数据的最大长度是接收窗口、阻塞窗口和最大分段长度(maximum segment size,MSS)中的最大值。

之后会计算TCP校验值。在计算时,头信息(ip地址、分段长度和端口号)会包含在内。根据TCP状态可发送一个或多个数据包。

事实上,当前的网络栈使用了校验卸载(checksum offload),TCP校验和会由NIC计算,而不是内核。但是,为了解释方便我们还是假设内核计算校验和。

被创建的TCP分段继续走到下面的IP层。IP层向TCP分段中增加了IP头并且执行了IP路由(IP routing)。IP路由是寻找到达目的IP的下一跳IP地址的过程。

在IP层计算并增加了IP头校验和之后,它把数据发送到链路层。链路层通过地址解析协议(Address Resolution Protocol,ARP)搜索下一跳IP地址对应的MAC地址。之后它会向数据包中增加链路头,在增加链路头之后主机要发送的数据包就是完整的了。

在执行IP路由时,会选择一个传输接口(NIC)。接口被用于把数据包传送至下一跳IP。于是,用于发送的NIC驱动程序被调用了。

在这个时候,如果正在执行数据包捕获程序(例如tcpdump或wireshark)的话,内核将把数据包拷贝到这些程序的内存缓冲区中。用相同的方式,接收的数据包直接在驱动被捕获。通常来说,traffic shaper(没懂)函数被实现以在这个层上运行。

驱动程序(与内核)通过请求NIC制造商定义的通讯协议传输数据。

在接收到数据包传输请求之后,NIC把数据包从系统内存中拷贝到它自己的内存中,之后把数据包发送到网络上。在此时,由于要遵守以太网标准(Ethernet standard),NIC会向数据包中增加帧间隙(Inter-Frame Gap,IFG),同步码(preamble)和crc校验和。帧间隙和同步码用于区分数据包的开始(网络术语叫做帧,framing),crc用于保护数据(与TCP或者IP校验和的目的相同)。NIC会基于以太网的物理速度和流量控制决定数据包开始传输的时间。It is like getting the floor and speaking in a conference room.(没看懂)

当NIC发送了数据包,NIC会在主机的CPU上产生中断。所有的中断会有自己的中断号,操作系统会用这个中断号查找合适的程序去处理中断。驱动程序在启动时会注册一个处理中断的函数。操作系统调用中断处理程序,之后中断处理程序会把已发送的数据包返回给操作系统。

到此为止我们讨论了当应用程序执行了write之后,数据流经内核和设备的过程。但是,除了应用程序直接调用write之外,内核也可以直接调用TCP传输数据包。比如当接收到ACK并且得知接收方的接收窗口增大之后,内核会创建包含socket缓冲区剩余数据的TCP片段并且把数据发送给接收者。

5.数据接收

现在我们看一下数据是如何被接收的。数据接收是网络栈如何处理流入数据包的过程。图3展现了网络栈如何处理接收的数据包。

图3:接收数据时网络栈中各层对数据的操作

首先,NIC把数据包写入它自身的内存。通过CRC校验检查数据包是否有效,之后把数据包发送到主机的内存缓冲区。这里说的缓冲区是驱动程序提前向内核申请好的一块内存区域,用于存放接收的数据包。在缓冲区被系统分配之后,驱动会把这部分内存的地址和大小告知NIC。如果主机没有为驱动程序分配缓冲区,那么当NIC接收到数据包时有可能会直接丢弃它。

在把数据包发送到主机缓冲区之后,NIC会向主机发出中断。

之后,驱动程序会判断它是否能处理新的数据包。到目前为止使用的是由制造商定义的网卡驱动的通讯协议。

当驱动应该向上层发送数据包时,数据包必须被放进一个操作系统能够理解和使用的数据结构。例如Linux的sk_buff,或者BSD系列内核的mbuf,或者windows的NET_BUFFER_LIST。驱动会把数据包包装成指定的数据结构,并发送到上一层。

链路层会检查数据包是否有效并且解析出上层的协议(网络协议)。此时它会判断链路头中的以太网类型。(IPv4的以太网类型是0×0800)。它会把链路头删掉并且把数据包发送到IP层。

IP层同样会检查数据包是否有效。或者说,会检查IP头校验和。它会执行IP路由判断,判断是由本机处理数据包还是把数据包发送到其它系统。如果数据包必须由本地系统处理,IP层会通过IP header中引用的原型值(proto value)解析上层协议(传输协议)。TCP原型值为6。系统会删除IP头,并且把数据包发送到TCP层。

就像之前的几层,TCP层检查数据包是否有效,同时会检查TCP校验和。就像之前提到的,如果当前的网络栈使用了校验卸载,那么TCP校验和会由NIC计算,而不是内核。

之后它会查找数据包对应的TCP控制块(TCB),这时会使用数据包中的<源ip,源端口,目的IP,目的端口>做标识。在查询到对应的连接之后,会执行协议中定义的操作去处理数据包。如果接收到的是新数据,数据会被增加到socket的接收缓冲区。根据tcp连接的状态,此时也可以发送新的TCP包(比如发送ACK包)。此时,TCP/IP接收数据包的处理完成。

socket接收缓冲区的大小就是TCP接收窗口。TCP吞吐量会随着接收窗口变大而增加。过去socket缓冲区大小是应用或操作系统的配置的定值。最新的网络栈使用了一个函数去自动决定接收缓冲区的大小。

当应用程序调用read系统调用时,程序会切换到内核区,并且会把socket接收缓冲区中的数据拷贝到用户区。拷贝后的数据会从socket缓冲区中移除。之后TCP会被调用,TCP会增加接收窗口的大小(因为缓冲区有了新空间)。并且会根据协议状态发送数据包。如果没有数据包传送,系统调用结束。

6.网络栈开发方向

到此为止描述的网络栈中的函数都是最基础的函数。在1990年代早期的网络栈函数只比上面描述的多一些。但是最近的随着网络栈的实现层次变高,网络栈增加了很多函数和复杂度。

最新的网络栈可以按以下需求分类:

6.1.操作报文处理过程

它包括类似Netfilter(firewall,NAT)的功能和流量控制。通过在处理流程中增加用户可以控制的代码,可以由用户控制实现不同的功能。

6.2.协议性能

用于改进特定网络环境下的吞吐量、延迟和可靠性。典型例子是增加的流量控制算法和额外的类似SACK的TCP功能。由于已经超出了范围,在这里就不深入讨论协议改进了。

6.3.高效的报文处理

高效的报文处理指的是通过降低处理报文的CPU周期、内存用量和处理报文需要访问内存的次数这些手段,来提升每秒可以处理的报文数量。有很多降低系统延迟的尝试,比如协议栈并行处理(stack parallel processing)、报头预处理(header prediction)、零拷贝(zero-copy)、单次拷贝(single-copy)、校验卸载(checksum offload)、TSO(TCP Segment Offload)、LRO(Large Receive Offload)、RSS(Receive Side Scaling)等。

7.网络栈中的流程控制

现在我们看一下Linux网络栈内部流程的细节。网络栈基于事件驱动的方式运行,在事件产生时做出相应的反应。因此,没有一个独立的线程去运行网络栈。图1和图3展示了最简单的控制流程图。图4说明了更加准确的控制流程。

图4:网络栈中的流程控制

在图4中的Flow(1),应用程序通过系统调用去执行(使用)TCP。例如,调用read系统调用或者wirte系统调用并执行TCP。但是,这一步里没有数据包传输。

Flow(2)跟Flow(1)类似,在执行TCP后需要传输报文。它会创建数据包并且把数据包发送给驱动程序。驱动程序上层有一个队列。数据包首先被放入队列,之后队列的数据结构决定何时把数据包发送给驱动程序。这个是Linux的队列规则(queue discipline,qdisc)。Linux的传输管理函数用来管理队列规则。默认的队列规则是简单的先入先出队列。通过使用其它的队列管理规则,可以执行多种操作,例如人造数据丢包、数据包延迟、通信比率限制,等等。在Flow(1)和Flow(2)中,驱动和应用程序处于相同的线程。

Flow(3)展示了TCP使用的定时器超时的情况。比如,当TIME_WAIT定时器超时后,TCP被调用并删除连接。

类似Flow(3),Flow(4)是TCP的定时器超时并且需要发送TCP执行结果数据包的情况。比如,当重传计时器超时后,会发送“没有收到ACK”数据包。

Flow(3)和Flow(4)展示了定时器软中断的处理过程。

当NIC驱动收到中断时,它将释放已经传输的数据包。在大多数情况下,驱动的执行过程到这里就结束了。Flow(5)是数据包积累在传输队列里的情况。驱动请求软中断,之后软中断的处理程序把传输队列里堆积的数据包发送给驱动程序。

当NIC驱动程序接收到中断并且发现有新接收的数据包时,它会请求软中断。软中断会调用驱动程序处理接收到的数据包并且把他们传送到上一层。在Linux中,上面描述的处理接收到的数据包的过程叫做New API(NAPI)。它和轮询类似,因为驱动并不直接把数据包传送到上一层,而是上层直接来取数据。实际代码中叫做NAPI poll 或 poll。

Flow(6)展示了TCP执行完成,Flow(7)展示了需要更多数据包传输的情况。Flow(5、6、7)的NIC中断都是通过软中断执行的。

8.如何处理中断和接收数据包

中断处理过程是复杂的,但是你需要了解数据包接收和处理流程中的和性能相关的问题。图5展示了一次中断的处理流程。

图5:处理中断、软中断和接收数据

假设CPU0正在执行应用程序。在此时,NIC接收到了一个数据包并且在CPU0上产生了中断。CPU0执行了内核中断处理程序(irq)。这个处理程序关联了一个中断号并且内核会调用驱动里对应的中断处理程序。驱动在释放已经传输的数据包之后调用napi_schedule()函数去处理接收到的数据包,这个函数会请求软中断。

在执行了驱动的中断处理程序后,控制权被转移到内核中断处理程序。内核中的处理程序会执行软中断的处理程序。

在中断上下文被执行之后,软中断的上下文会被执行。中断上下文和软中断上下文会通过相同的线程执行,但是它们会使用不同的栈。并且中断上下文会屏蔽硬件中断;而软中断上下文不会屏蔽硬件中断。

处理接收到的数据包的软中断处理程序是net_rx_action()函数。这个函数会调用驱动程序的poll()函数。而poll()函数会调用netif_receive_skb()函数,并把接收到的数据包一个接一个的发送到上层。在软中断处理结束后,应用程序会从停止的位置重新开始执行。(After processing the softirq, the application restarts execution from the stopped point in order to request a system call.没太明白这一句的system call是啥意思)

因此,接收中断请求的CPU会负责处理接收数据包从始至终的整个过程。在Linux、BSD和Windows中,处理过程基本是类似的。

当你检查服务器CPU利用率时,有时你可以检查服务器的很多CPU中是否只有一个CPU在艰难执行软中断。这个现象产生的原因就是上文所解释的数据包接收的处理过程。为了解决这个问题开发出了多队列NIC(multi-queue NIC)、RSS和RPS。

下面是译者翻译的《理解TCP/IP网络栈&编写网络应用》的下篇,会通过讲解TCP的代码实现帮助大家理解发送、接收数据的流程,也描述了一些网卡、驱动等网络栈底层的原理。

8.数据结构

以下是一些关键数据结构。我们了解一下这些数据结构再开始查看代码。

8.1.sk_buff_structure

首先,sk_buff结构或skb结构代表一个数据包。图6展现了sk_buff中的一些结构。随着功能变得更强大,它们也变得更复杂了。但是还是有一些任何人都能想到的基本功能。

图6:数据包结构

8.1.1.包含数据和元数据

这个结构直接包含或者通过指针引用了数据包。在图6中,一些数据包(从Ethernet到Buffer部分)使用了指针,一些额外的数据(frags)引用了实际的内存页。

一些必要的信息比如头和内容长度被保存在元数据区。例如,在图6中,mac_headernetwork_headertransport_header都有相应的指针,指向链路头、IP头和TCP头的起始地址。这种方式让TCP协议处理过程变得简单。

8.1.2.如何增加或删除头

数据包在网络栈的各层中上升或下降时会增加或删除数据头。为了更有效率的处理而使用了指针。例如,要删除链路头只需要修改head pointer的值。

8.1.3.如何合并或切分数据包

为了更有效率的执行把数据包增到或从socket缓冲区中删除这类操作而使用了链表,或者叫数据包链。next和prev指针用于这个场景。

8.1.4.快速分配和释放

无论何时创建数据包都会分配一个数据结构,此时会用到快速分配器。比如,如果数据通过10Gb的以太网传输,每秒会有超过一百万个对象被创建和销毁。

9.TCP控制块(TCP Control Block)

其次,有一个表示TCP连接的数据结构,之前它被抽象的叫做TCP控制块。Linux使用了tcp_sock这个数据结构。在图7中,你可以看到文件、socket和tcp_socket的关系。

图7:TCP Connection结构

当系统调用发生后,它会找到应用程序在进行系统调用时使用的文件描述符对应的文件。对Unix系的操作系统来说,文件本身和通用文件系统存储的设备都被抽象成了文件。因此,文件结构包含了必要的信息。对于socket来说,使用独立的socket结构保存socket相关的信息,文件结构通过指针来引用socket。socket又引用了tcp_socktcp_sock可以分为sock,inet_sock等等,用来支持除了TCP之外的协议,可以认为这是一种多态。

所有TCP协议用到的状态信息都被存在tcp_sock里。例如顺序号、接收窗口、阻塞控制和重发送定时器都保存在tcp_sock中。

socket的发送缓冲区和接收缓冲区由sk_buff链表组成并被包含在tcp_sock中。为防止频繁查找路由,也会在tcp_sock中引用IP路由结果dst_entry。通过dst_entry可以简单的查找到目标的MAC地址之类的ARP的结果。dst_entry是路由表的一部分,而路由表是个很复杂的结构,在这篇文档里就不再讨论了。用来传送数据的网卡(NIC)可以通过dst_entry找到。网卡通过net_device描述。

因此,仅通过查找文件和指针就可以很简单的查找到处理TCP连接的所有的数据结构(从文件到驱动)。这个数据结构的大小就是每个TCP连接占用内存的大小。这个结构占用的内存只有几kb大小(排除了数据包中的数据)。但随着一些功能被加入,内存占用也在逐渐增加。

最后,我们来看一下TCP连接查找表(TCP connection lookup table)。这是一个用来查找接收到的数据包对应tcp连接的哈希表。系统会用数据包的<来源ip,目标ip,来源端口,目标端口>和Jenkins哈希算法去计算哈希值。选择这个哈希函数的原因是为了防止对哈希表的攻击。

10.追踪代码:如何传输数据

我们将会通过追踪实际的Linux内核源码去检查协议栈中执行的关键任务。在这里,我们将会观察经常使用的两条路径。

首先是应用程序调用了write系统调用时的执行路径。

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, ...)

{

struct file *file;

[...]

file = fget_light(fd, &fput_needed);

[...] ===>

ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);

struct file_operations {

[...]

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, ...)

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, ...)

[...]

};

static const struct file_operations socket_file_ops = {

[...]

.aio_read = sock_aio_read,

.aio_write = sock_aio_write,

[...]

};

当应用调用了write系统调用时,内核将在文件层执行write()函数。首先,内核会取出文件描述符对应的文件结构体,之后会调用aio_write,这是一个函数指针。在文件结构体中,你可以看到file_perations结构体指针。这个结构被通称为函数表(function table),其中包含了一些函数的指针,比如aio_read或者aio_write。对于socket来说,实际的表是socket_file_ops,aio_write对应的函数是sock_aio_write。在这里函数表的作用类似于java中的interface,内核使用这种机制进行代码抽象或重构。

static ssize_t sock_aio_write(struct kiocb *iocb, const struct iovec *iov, ..)

{

[...]

struct socket *sock = file->private_data;

[...] ===>

return sock->ops->sendmsg(iocb, sock, msg, size);

struct socket {

[...]

struct file *file;

struct sock *sk;

const struct proto_ops *ops;

};

const struct proto_ops inet_stream_ops = {

.family = PF_INET,

[...]

.connect = inet_stream_connect,

.accept = inet_accept,

.listen = inet_listen, .sendmsg = tcp_sendmsg,

.recvmsg = inet_recvmsg,

[...]

};

struct proto_ops {

[...]

int (*connect) (struct socket *sock, ...)

int (*accept) (struct socket *sock, ...)

int (*listen) (struct socket *sock, int len);

int (*sendmsg) (struct kiocb *iocb, struct socket *sock, ...)

int (*recvmsg) (struct kiocb *iocb, struct socket *sock, ...)

[...]

};

sock_aio_write()函数会从文件结构体中取出socket结构体并调用sendmsg,这也是一个函数指针。socket结构体中包含了proto_ops函数表。IPv4的TCP实现中,proto_ops的具体实现是inet_stream_ops,sendmsg的实现是tcp_sendmsg

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock,

struct msghdr *msg, size_t size)

{

struct sock *sk = sock->sk;

struct iovec *iov;

struct tcp_sock *tp = tcp_sk(sk);

struct sk_buff *skb;

[...]

mss_now = tcp_send_mss(sk, &size_goal, flags);

/* Ok commence sending. */

iovlen = msg->msg_iovlen;

iov = msg->msg_iov;

copied = 0;

[...]

while (--iovlen >= 0) {

int seglen = iov->iov_len;

unsigned char __user *from = iov->iov_base;

iov++;

while (seglen > 0) {

int copy = 0;

int max = size_goal;

[...]

skb = sk_stream_alloc_skb(sk,

select_size(sk, sg),

sk->sk_allocation);

if (!skb)

goto wait_for_memory;

/*

* Check whether we can use HW checksum.

*/

if (sk->sk_route_caps & NETIF_F_ALL_CSUM)

skb->ip_summed = CHECKSUM_PARTIAL;

[...]

skb_entail(sk, skb);

[...]

/* Where to copy to? */

if (skb_tailroom(skb) > 0) {

/* We have some space in skb head. Superb! */

if (copy > skb_tailroom(skb))

copy = skb_tailroom(skb);

if ((err = skb_add_data(skb, from, copy)) != 0)

goto do_fault;

[...]

if (copied)

tcp_push(sk, flags, mss_now, tp->nonagle);

[...]

}

tcp_sendmsg(译注:原文写的是tcp_sengmsg,应该是笔误)会从socket中取得tcp_sock(也就是TCP控制块,TCB),并把应用程序请求发送的数据拷贝到socket发送缓冲中(译注:就是根据发送数据创建sk_buff链表)。当把数据拷贝到sk_buff中时,每个sk_buff会包含多少字节数据?在代码创建数据包时,每个sk_buff中会包含MSS字节(通过tcp_send_mss函数获取),在这里MSS表示每个TCP数据包能携带数据的最大值。通过使用TSO(TCP Segment Offload)和GSO(Generic Segmentation Offload)技术,一个sk_buff可以保存大于MSS的数据。在这篇文章里就不详细解释了。

sk_stream_alloc_skb函数会创建新的sk_buff,之后通过skb_entail把新创建的sk_buff放到send_socket_buffer的末尾。skb_add_data函数会把应用层的数据拷贝到sk_buff的buffer中。通过重复这个过程(创建sk_buff然后把它加入到socket发送缓冲区)完成所有数据的拷贝。因此,大小是MSS的多个sk_buff会在socket发送缓冲区中形成一个链表。最终调用tcp_push把待发送的数据做成数据包,并且发送出去。

static inline void tcp_push(struct sock *sk, int flags, int mss_now, ...)

[...] ===>

static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, ...)

int nonagle,

{

struct tcp_sock *tp = tcp_sk(sk);

struct sk_buff *skb;

[...]

while ((skb = tcp_send_head(sk))) {

[...]

cwnd_quota = tcp_cwnd_test(tp, skb);

if (!cwnd_quota)

break;

if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))

break;

[...]

if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))

break;

/* Advance the send_head. This one is sent out.

* This call will increment packets_out.

*/

tcp_event_new_data_sent(sk, skb);

[...]

tcp_push函数会在TCP允许的范围内顺序发送尽可能多的sk_buff数据。首先会调用tcp_send_head取得发送缓冲区中第一个sk_buff,然后调用tcp_cwnd_testtcp_send_wnd_test检查堵塞窗口和接收窗口,判断接收方是否可以接收新数据。之后调用tcp_transmit_skb函数来创建数据包。

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,

int clone_it, gfp_t gfp_mask)

{

const struct inet_connection_sock *icsk = inet_csk(sk);

struct inet_sock *inet;

struct tcp_sock *tp;

[...]

if (likely(clone_it)) {

if (unlikely(skb_cloned(skb)))

skb = pskb_copy(skb, gfp_mask);

else

skb = skb_clone(skb, gfp_mask);

if (unlikely(!skb))

return -ENOBUFS;

}

[...]

skb_push(skb, tcp_header_size);

skb_reset_transport_header(skb);

skb_set_owner_w(skb, sk);

/* Build TCP header and checksum it. */

th = tcp_hdr(skb);

th->source = inet->inet_sport;

th->dest = inet->inet_dport;

th->seq = htonl(tcb->seq);

th->ack_seq = htonl(tp->rcv_nxt);

[...]

icsk->icsk_af_ops->send_check(sk, skb);

[...]

err = icsk->icsk_af_ops->queue_xmit(skb);

if (likely(err <= 0))

return err;

tcp_enter_cwr(sk, 1);

return net_xmit_eval(err);

}

tcp_transmit_skb会创建指定sk_buff的拷贝(通过pskb_copy),但它不会拷贝应用层发送的数据,而是拷贝一些元数据。之后会调用skb_push来确保和记录头部字段的值。send_check计算TCP校验和(如果使用校验和卸载checksum offload技术则不会做这一步计算)。最终调用queue_xmit把数据发送到IP层。IPv4中queue_xmit的实现函数是ip_queue_xmit

int ip_queue_xmit(struct sk_buff *skb)

[...]

rt = (struct rtable *)__sk_dst_check(sk, 0);

[...]

/* OK, we know where to send it, allocate and build IP header. */

skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));

skb_reset_network_header(skb);

iph = ip_hdr(skb);

*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));

if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df)

iph->frag_off = htons(IP_DF);

else

iph->frag_off = 0;

iph->ttl = ip_select_ttl(inet, &rt->dst);

iph->protocol = sk->sk_protocol;

iph->saddr = rt->rt_src;

iph->daddr = rt->rt_dst;

[...]

res = ip_local_out(skb);

[...] ===>

int __ip_local_out(struct sk_buff *skb)

[...]

ip_send_check(iph);

return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,

skb_dst(skb)->dev, dst_output);

[...] ===>

int ip_output(struct sk_buff *skb)

{

struct net_device *dev = skb_dst(skb)->dev;

[...]

skb->dev = dev;

skb->protocol = htons(ETH_P_IP);

return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,

ip_finish_output,

[...] ===>

static int ip_finish_output(struct sk_buff *skb)

[...]

if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))

return ip_fragment(skb, ip_finish_output2);

else

return ip_finish_output2(skb);

ip_queue_xmit函数执行IP层的一些必要的任务。__sk_dst_check检查缓存的路由是否有效。如果没有被缓存的路由项,或者路由无效,它将会执行IP路由选择(IP routing)。之后调用skb_push来计算和记录IP头字段的值。之后,随着函数执行,ip_send_check计算IP头校验和并且调用netfilter功能(译注:这是内核的一个模块)。如果使用ip_finish_output函数会创建IP数据分片,但在使用TCP协议时不会创建分片,因此内核会直接调用ip_finish_output2来增加链路头,并完成数据包的创建。

int dev_queue_xmit(struct sk_buff *skb)

[...] ===>

static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q, ...)

[...]

if (...) {

....

} else

if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&

qdisc_run_begin(q)) {

[...]

if (sch_direct_xmit(skb, q, dev, txq, root_lock)) {

[...] ===>

int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q, ...)

[...]

HARD_TX_LOCK(dev, txq, smp_processor_id());

if (!netif_tx_queue_frozen_or_stopped(txq))

ret = dev_hard_start_xmit(skb, dev, txq);

HARD_TX_UNLOCK(dev, txq);

[...]

}

int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, ...)

[...]

if (!list_empty(&ptype_all))

dev_queue_xmit_nit(skb, dev);

[...]

rc = ops->ndo_start_xmit(skb, dev);

[...]

}

最终的数据包会通过dev_queue_xmit函数完成传输。首先,数据包通过排队规则(译注:qdisc,上篇文章简单介绍过)传递。如果使用了默认的排队规则并且队列是空的,那么会跳过队列而直接调用sch_direct_xmit把数据包发送到驱动。dev_hard_start_xmit会调用实际的驱动程序。在调用驱动之前,设备的发送(译注:TX,transmit)被锁定,防止多个线程同时使用设备。由于内核锁定了设备的发送,驱动发送数据相关的代码就不需要额外的锁了。这里下次要讲(译注:这里是说原作者的下篇文章)的并行开发有很大关系。

ndo_start_xmit函数会调用驱动的代码。在这之前,你会看到ptype_alldev_queue_xmit_nitptype_all是个包含了一些模块的列表(比如数据包捕获)。如果捕获程序正在运行,数据包会被ptype_all拷贝到其它程序中。因此,tcpdump中显示的都是发送给驱动的数据包。当使用了校验和卸载(checksum offload)或TSO(TCP Segment Offload)这些技术时,网卡(NIC)会操作数据包,所以tcpdump得到的数据包和实际发送到网络的数据包有可能不一致。在结束了数据包传输以后,驱动中断处理程序会返回发送了的sk_buff

11.追踪代码:如何接收数据

一般来说,接收数据的执行路径是接收一个数据包并把数据加入到socket的接收缓冲区。在执行了驱动中断处理程序之后,首先执行的是napi poll处理程序。

static void net_rx_action(struct softirq_action *h)

{

struct softnet_data *sd = &__get_cpu_var(softnet_data);

unsigned long time_limit = jiffies + 2;

int budget = netdev_budget;

void *have;

local_irq_disable();

while (!list_empty(&sd->poll_list)) {

struct napi_struct *n;

[...]

n = list_first_entry(&sd->poll_list, struct napi_struct,

poll_list);

if (test_bit(NAPI_STATE_SCHED, &n->state)) {

work = n->poll(n, weight);

trace_napi_poll(n);

}

[...]

}

int netif_receive_skb(struct sk_buff *skb)

[...] ===>

static int __netif_receive_skb(struct sk_buff *skb)

{

struct packet_type *ptype, *pt_prev;

[...]

__be16 type;

[...]

list_for_each_entry_rcu(ptype, &ptype_all, list) {

if (!ptype->dev || ptype->dev == skb->dev) {

if (pt_prev)

ret = deliver_skb(skb, pt_prev, orig_dev);

pt_prev = ptype;

}

}

[...]

type = skb->protocol;

list_for_each_entry_rcu(ptype,

&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {

if (ptype->type == type &&

(ptype->dev == null_or_dev || ptype->dev == skb->dev ||

ptype->dev == orig_dev)) {

if (pt_prev)

ret = deliver_skb(skb, pt_prev, orig_dev);

pt_prev = ptype;

}

}

if (pt_prev) {

ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

static struct packet_type ip_packet_type __read_mostly = {

.type = cpu_to_be16(ETH_P_IP),

.func = ip_rcv,

[...]

};

就像之前说的,net_rx_action函数是用于接收数据的软中断处理函数。首先,请求napi poll的驱动会检索poll_list,并且调用驱动的poll处理程序(poll handler)。驱动会把接收到的数据包包装成sk_buff,之后调用netif_receive_skb

如果有模块在请求数据包,那么netif_receive_skb会把数据包发送给那个模块。类似于之前讨论过的发送的过程,在这里驱动接收到的数据包会发送给注册到ptype_all列表的那些模块,数据包在这里被捕获。

之后,根据数据包的类型,不同数据包会被传输到相应的上层。链路头中包含了2字节的以太网类型(ethertype)字段,这个字段的值标识了数据包的类型。驱动会把这个值记录到sk_buff中(skb->protocol)。每一种协议有自己的packet_type结构体,并且会把指向结构体的指针放入ptype_base哈希表中。IPv4使用的是ip_packate_type,类型字段中的值是IPv4类型(ETH_P_IP)。于是,对于IPv4类型的数据包会调用ip_recv函数。

int ip_rcv(struct sk_buff *skb, struct net_device *dev, ...)

{

struct iphdr *iph;

u32 len;

[...]

iph = ip_hdr(skb);

[...]

if (iph->ihl < 5 || iph->version != 4)

goto inhdr_error;

if (!pskb_may_pull(skb, iph->ihl*4))

goto inhdr_error;

iph = ip_hdr(skb);

if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))

goto inhdr_error;

len = ntohs(iph->tot_len);

if (skb->len < len) {

IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);

goto drop;

} else if (len < (iph->ihl*4))

goto inhdr_error;

[...]

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,

ip_rcv_finish);

[...] ===>

int ip_local_deliver(struct sk_buff *skb)

[...]

if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {

if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))

return 0;

}

return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,

ip_local_deliver_finish);

[...] ===>

static int ip_local_deliver_finish(struct sk_buff *skb)

[...]

__skb_pull(skb, ip_hdrlen(skb));

[...]

int protocol = ip_hdr(skb)->protocol;

int hash, raw;

const struct net_protocol *ipprot;

[...]

hash = protocol & (MAX_INET_PROTOS - 1);

ipprot = rcu_dereference(inet_protos[hash]);

if (ipprot != NULL) {

[...]

ret = ipprot->handler(skb);

[...] ===>

static const struct net_protocol tcp_protocol = {

.handler = tcp_v4_rcv,

[...]

};

ip_rcv函数执行IP层必要的操作。它会解析数据包中的长度和IP头校验和。在经过netfilter代码时会执行ip_local_deliver函数。如果需要的话还会装配IP分片。之后会通过netfilter的代码调用ip_local_deliver_finish函数,这个函数通过调用__skb_pull移除IP头,并且通过IP头的protocol值查找上层协议标记。类似于ptype_base,每个传输层协议会在inet_protos中注册自己的net_protocol结构体。IPv4 TCP使用tcp_protocol,于是会调用tcp_v4_rcv函数继续处理。

当数据包到达TCP层时,数据包的处理流程取决于TCP状态和包类型。在这里,我们将看到TCP连接处于ESTABLISHED状态时处理到达的数据包的过程。在没有出现出现丢包或者乱序等异常的情况下,服务器会频繁的执行这条路径。

int tcp_v4_rcv(struct sk_buff *skb)

{

const struct iphdr *iph;

struct tcphdr *th;

struct sock *sk;

[...]

th = tcp_hdr(skb);

if (th->doff < sizeof(struct tcphdr) / 4)

goto bad_packet;

if (!pskb_may_pull(skb, th->doff * 4))

goto discard_it;

[...]

th = tcp_hdr(skb);

iph = ip_hdr(skb);

TCP_SKB_CB(skb)->seq = ntohl(th->seq);

TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +

skb->len - th->doff * 4);

TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);

TCP_SKB_CB(skb)->when = 0;

TCP_SKB_CB(skb)->flags = iph->tos;

TCP_SKB_CB(skb)->sacked = 0;

sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);

[...]

ret = tcp_v4_do_rcv(sk, skb);

首先,tcp_v4_rcv函数验证包的有效性,如果tcp头的大小比数据的偏移大时(th->doff < sizeof(struct tcphdr) /4)则说明包头有错误。(如果没有错误)之后会调用__inet_lookup_skb在存放TCP连接的哈希表里查找数据包所属的连接。从查找到的sock结构体可以找到所有需要的数据结构(比如tcp_sock),也可以取得对应的socket。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)

[...]

if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */

sock_rps_save_rxhash(sk, skb->rxhash);

if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {

[...] ===>

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,

[...]

/*

* Header prediction.

*/

if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&

TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&

!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))) {

[...]

if ((int)skb->truesize > sk->sk_forward_alloc)

goto step5;

NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);

/* Bulk data transfer: receiver */

__skb_pull(skb, tcp_header_len);

__skb_queue_tail(&sk->sk_receive_queue, skb);

skb_set_owner_r(skb, sk);

tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;

[...]

if (!copied_early || tp->rcv_nxt != tp->rcv_wup)

__tcp_ack_snd_check(sk, 0);

[...]

step5:

if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)

goto discard;

tcp_rcv_rtt_measure_ts(sk, skb);

/* Process urgent data. */

tcp_urg(sk, skb, th);

/* step 7: process the segment text */

tcp_data_queue(sk, skb);

tcp_data_snd_check(sk);

tcp_ack_snd_check(sk);

return 0;

[...]

}

实际的协议在tcp_v4_do_rcv函数中处理。如果TCP正处于ESTABLISHED状态则会调用tcp_rcv_established(译注:原文写的是tcp_rcv_esablished,应该是笔误)。由于ESTABLISHED是最常用的状态,所以它被单独处理和优化了。tcp_rcv_established首先会执行头部预测(header prediction)的代码。头部预测会快速检测和处理常见情况。在这里常见的情况是没有在发送数据,实际收到的数据包正是应该收到的下一个数据包,例如TCP接收到了它期望收到的那个顺序号。在这种情况下在把数据加入到socket缓冲区、发送ack之后完成处理。

往前翻的话你会看到比较truesizesk_forward_allow的语句。这个用来检查socket接收缓冲区中是否还有空闲空间来存放刚收到的数据。如果有的话,头部预测为“命中”(预测成功)。之后调用__skb_pull来删除TCP头。再之后调用__skb_queue_tail来把数据包增加到socket接收缓冲区。最终会视情况调用__tcp_ack_snd_check发送ACK。此时处理完成。

如果没有足够的空间,那么会走一条比较慢的路径。tcp_data_queue函数会新分配socket接收缓冲区的空间并把数据增加到缓冲区中。在这种情况下,接受缓冲区的大小在需要时会自动增加。和上一段说的快速路径不同,此时会在可行的情况下会调用tcp_data_snd_check发送一个新的数据回包。最终,如果需要的话会调用tcp_ack_snd_check来发送一个ACK包。

这两条路径下执行的代码量不大,因为这里讨论的都是通常情况。或者说,其它特殊情况的处理会慢的多,比如接收乱序这类情况。

12.驱动和网卡如何通讯

驱动(driver)和网卡(nic)之间的通讯处于协议栈的底层,大多数人并不关心。但是,为了解决性能问题,网卡会处理越来越多的任务。理解基础的处理方式会帮助你理解额外这些优化技术。

网卡和驱动之间使用异步通讯。首先,驱动请求数据传输时CPU不会等待结果而是会继续处理其它任务,之后网卡发送数据包并通知CPU,驱动程序返回通过事件接收的数据包(这些数据包可以看作是异步发送的返回值)。

和数据包传输类似,数据包的接收也是异步的。首先驱动请求接受接收数据包然后CPU去执行其它任务,之后网卡接收数据包并通知CPU,然后驱动处理接收到的数据包(返回数据)。

因此需要有一个空间来保存请求和响应(request and response)。大多数情况网卡会使用环形队列数据结构(ring structure)。环形队列类似于普通的队列,其中有固定数量的元素,每个元素会保存一个请求或一个相应数据。环形队列中元素是顺序的,“环形”的意思是队列虽然是定长的,但是其中的元素会按顺序重用。

图8展示了数据包传输的过程,你会看到如何使用环形队列。

图8:驱动和网卡之间的通讯:如何传输数据包

驱动接收到上层发来的数据包并创建网卡能够识别的描述符。发送描述符(send descriptor)默认会包含数据包的大小和内存地址。这里网卡需要的是内存中的物理地址,驱动需要把数据包的虚拟地址转换成物理地址。之后,驱动会把发送描述符添加到发送环形队列(TX ring)(1),发送环形队列中包含的实际是发送描述符。

之后,驱动会把请求通知给网卡(2)。驱动直接把数据写入指定的网卡内存地址中(译注:这里应该是网卡的寄存器地址,写入的是通知而不是数据包)。这种CPU直接向设备发送数据的传输方式叫做程序化I/O(Programmed I/O,PIO)。

网卡被通知后从主机的发送队列中取得发送描述符(3)。这种设备直接访问内存而不需要调用CPU的内存访问方式叫做直接内存访问(Direct Memory Access,DMA)。

在取得发送描述符后,网卡会得到数据包的地址和大小并且从主机内存中取得实际的数据包(4)。如果有校验和卸载(checksum offload)的话,网卡会在拿到数据后计算数据包的校验和,因此开销不大。

网卡发送数据包(5)之后把发送数据包的数量写入主机内存(6)。之后它会触发一次中断,驱动程序会读取发送数据包的数量并根据数量返回已发送的数据包。

在图9中,你会看到接收数据包的流程。

图9:驱动和网卡之间的通讯:如何接收数据包

首先,驱动会在主机内存中分配一块缓冲区用来接收数据包,之后创建接收描述符(receive descriptor)。接收描述符默认会包含缓冲区的大小和地址。类似于发送描述符,接收描述符中保存的也是DMA使用的物理地址。之后,驱动会把接收描述符加入到接收环形队列(RX ring)(1),接收环形队列保存的都是接收数据的请求。

通过PIO,驱动程序通知网卡有一个新的描述符(2),网卡会把新的描述符从接收队列中取出来,然后把缓冲区的大小和地址保存到网卡内存中(3)。

在接收到数据包时,网卡会把数据包发送到主机内存缓冲区中(5)。如果存在校验和卸载函数,那么网卡会在此时计算校验和。接收数据包的实际大小、校验和结果和其他信息会保存在独立的环形队列中(接收返回环形队列,receive return ring)(6)。接收返回队列保存了接收请求的处理结果,或者叫响应(response)。之后网卡会发出一个中断(7)。驱动程序从接收返回队列中获取接收到的数据包的信息,如果必要的话,还会分配新内存缓冲区并重复(1)和(2)。

调优网络栈时,大部分人会说环形队列和中断的设置需要被调整。当发送环形队列很大时,很多次的发送请求可以一次完成;当接收环形队列很大,可以一次性接收多个数据包。更大的环形队列对于大流量数据包接收/发送是很有用的。由于CPU在处理中断时有大量开销,大量大多数情况下,网卡使用一个计时器来减少中断。为了避免对宿主机过多的中断,发送和接收数据包的时候中断会被收集起来并且定期调用(interrupt coalescing,中断聚合)。

13.网络栈中的缓冲区和流量控制

在网络栈中流量控制在几个阶段被执行。图10展示了传输数据时的一些缓冲区。首先,应用会创建数据并把数据加入到socket发送缓冲区。如果缓冲区中没有剩余空间的话,系统调用会失败或阻塞应用进程。因此,应用程序到内核的发送速率由socket缓冲区大小来限制。

图10:数据发送相关的缓冲

TCP通过传输队列(qdisc)创建并把数据包发送给驱动程序。这是一个典型的先入先出队列,队列最大长度是txqueuelen,可以通过ifconfig命令来查看实际大小。通常来说,大约有几千个数据包。

驱动和网卡之间是传输环形队列(TX ring),它被认为是传输请求队列(transmission request queue)。如果队列中没有剩余空间的话就不会再继续创建传输请求,并且数据包会积累在传输队列中,如果数据包积累的太多,那么新的数据包会被丢弃。

网卡会把要发送的数据包保存在内部缓冲区中。这个队列中的数据包速度受网卡物理速度的影响(例如,1Gb/s的网卡不能承担10Gb/s的性能)。根据以太网流量控制,当网卡的接收缓冲区没有空间时,数据包传输会被停止。

当内核速度大于网卡时,数据包会堆积在网卡的缓冲区中。如果缓冲区中没有空间时会停止处理传输环形队列(TX ring)。越来越多的请求堆积在传输环形队列中,最终队列中空间被耗尽。驱动程序不能再继续创建传输请求数据包会堆积在传输队列(transmit queue)中。压力通过各种缓冲从底向上逐级反馈。

图11展示了接收数据包经过的缓冲区。数据包先被保存在网卡的接收缓冲区中。从流量控制的视角来看,驱动和网卡之间的接收环形缓冲区(RX ring)可以被看作是数据包的缓冲区。驱动程序从环形缓冲区取得数据包并把它们发送到上层。服务器系统的网卡驱动默认会使用NAPI,所以在驱动和上层之间没有缓冲区。因此,可以认为上层直接从接收环形缓冲区中取得数据,数据包的数据部分被保存在socket的接收缓冲区中。应用程序从socket接收缓冲区取得数据。

图11:与接收数据包相关的缓冲

不支持NAPI的驱动程序会把数据包保存在积压队列(backlog queue)中。之后,NAPI处理程序取得数据包,因此积压队列可以被认为是在驱动程序和上层之间的缓冲区。

如果内核处理数据包的速度低于网卡的速度,接收循环缓冲区队列(RX ring)会被写满,网卡的缓冲区空间(NIC internal buffer)也会被写满,当使用了以太流量控制(Ethernet flow control)时,接收方网卡会向发送方网卡发送请求来停止传输或丢弃数据包。

因为TCP支持端对端流量控制,所以不会出现由于socket接收队列空间不足而丢包的情况。但是,当使用UDP协议时,因为UDP协议不支持流量控制,如果应用程序处理速度不够的时候会出现socket接收缓冲区空间不足而丢包的情况。

在图10和图11中展示的传输环形队列(TX ring)和接收环形队列(RX ring)的大小可以用ethtool查看。在大多数看重吞吐量的负载情况下,增加环形队列的大小和socket缓冲区大小会有一些帮助。增加大小会减少高速收发数据包时由于缓冲区空间不足而造成的异常。

14.结论

最初,我计划只解释那些会对你编写网络程序有帮助的东西,执行性能测试和解决问题。即使是我最初的计划,在这篇文档中包含的内容也会很多。我希望这篇文章会帮助你开发网络程序并监控它们的性能。TCP/IP协议本身就十分复杂并有很多异常情况。但是,你不需要理解OS中TCP/IP协议相关的每一行代码来理解性能和分析现象。只需要理解它的上下文对你就会十分有帮助。

随着系统性能和操作系统网络栈实现的持续提升,最近的服务器能承受10-20Gb/s的TCP吞吐量而不出现任何问题。近期也出现了与性能相关的很多的新技术,例如TSO, LRO, RSS, GSO, GRO, UFO, XPS, IOAT, DDIO, TOE等等(译注:这些我就不翻译了……),让我们有些困惑。

在下篇文章中,我会从性能的观点解释关于网络栈,并且讨论这些技术的问题和收益。

By Hyeongyeop Kim, Senior Engineer at Performance Engineering Lab, NHN Corporation.

15.译者最后说几句

本来看着文章内容不多,但是实际翻译下来才发现有这么多字……翻译过程中发现有很多地方自己之前的理解也有一些错误,如果各位发现哪里翻译的有问题的话麻烦告知一下,感激不尽~~

对于TCP协议来说,虽然已经看过很多相关的文章,但是真正要完整的描述一次tcp数据包发送和接收的过程实际上还是很难做到的。对于TCP或者内核之类的文章很多,但是我观察了一下,大部分人的问题并不是看的文章不够多,而是看的不够仔细。

与其每天乐此不疲的mark一堆技术文章然后随便扫几眼,还不如安心把一篇文章完整的读透,甚至再退一步,把文章从头到尾通读一遍,那也是极好的。这也是这次翻译这篇文章的初衷,与各位共勉。

理解 TCP/IP 网络栈 & 编写网络应用,首发于博客 - 伯乐在线

05 May 09:39

不同的垃圾回收器的比较

by importnewzz

4款Java垃圾回收器——错误的选择导致糟糕的性能

现在已经是2014年了,但是对大多数开发人员而言有两件事情仍然是个谜——垃圾回收以及异性(码农又被嘲笑了)。由于我对后者也不是特别了解,我想我还是试着说说前者吧,尤其是随着Java 8的到来,这个领域也发生了许多重大的变化及提升,其中最重要的莫过于持久代(PermGen)的删除以及一些令人振奋的新的优化(后面会陆续提及这些)。

说起垃圾回收,许多人都了解它的概念,也在日常的编程中有所应用。尽管如此,仍有许多我们不太了解的东西,而这正是痛苦的根源。关于JVM最大的误解就是认为它只有一个垃圾回收器,而事实上它有四个不同的回收器,每个都各有其长短。JVM并不会自动地选择某一个,这事还得落在你我的肩上,因为不同的回收器会带来吞吐量及应用的暂停时间的显著的差异。

这四种回收算法的共同之处在于它们都是分代的,也就是说它们将托管的堆分成了好几个区域,它假设堆中的许多对象的生命周期都很短,可以很快被回收掉。介绍这块内容的已经很多了,因此这里我打算直接讲一下这几个不同的算法,以及它们的长处及短处。

1.串行回收器

串行回收器是最简单的一个,你都不会考虑使用它,因为它主要是面向单线程环境的(比如说32位的或者Windows)以及比较小的堆。这个回收器工作的时候会将所有应用线程全部冻结,就这一点而言就使得它完全不可能会被服务端应用所采用。

如何使用它:你可以打开-XX:+UseSerialGC这个JVM参数来使用它。

2.并行/吞吐量回收器

下一个是并行回收器( Parallel collector)。这是JVM的默认回收器。正如它的名字所说的那样,它的最大的优点就是它使用多个线程来扫描及压缩堆。它的缺点就是不管执行的是minor GC还是full GC它都会暂停应用线程。并行回收器最适合那些可以容许暂停的应用,它试图减少由回收器所引起的CPU开销。

3.CMS回收器

并行回收器之后就是CMS回收器了(concurrent-mark-sweep)。这个算法使用了多个线程(concurrent)来扫描堆并标记(mark)那些不再使用的可以回收(sweep)的对象。这个算法在两种情况下会进入一个”stop the world”的模式:当进行根对象的初始标记的时候 (老生代中线程入口点或静态变量可达的那些对象)以及当这个算法在并发运行的时候应用程序改变了堆的状态使得它不得不回去再次确认自己标记的对象都是正确的。

使用这个回收器最大的问题就是会碰到promotion failure,这是指在回收新生代及年老代时出现了竞争条件的情况。如果回收器需要将年轻的对象提升到年老代中,而这个时候年老代没有多余的空间了,它就只能先进行一次STW(Stop The World)的full GC了——这种情况正是CMS所希望避免的。为了确保这种情况不会发生,你要么就是增加老生代的大小(或者增加整个堆的大小),要么就是给回收器分配一些后台线程以便与对象分配的速度进行赛跑。

这个算法的另一个缺点就是和并行回收器相比,它使用的CPU资源会更多,它使用了多个线程来执行扫描和回收,这样才能让应用持续提供更高级别的吞吐量。对于大多数长期运行的程序而言,应用的暂停对它们是很不利的,这个时候可以考虑使用CMS回收器。尽管如此,这个算法也不是默认开启的。你得指定XX:+UseConcMarkSweepGC来启用它。假设你的堆小于4G,而你又希望分配更多的CPU资源以避免应用暂停,那么这就是你要选择的回收器。然而,如果堆大于4G的话,你可能更希望使用最后的这个——G1回收器。

4.G1回收器

G1( Garbage first)回收器在JDK 7update 4中首次引入,它的设计目标是能更好地支持大于4GB的堆。G1回收器将堆分为多个区域,大小从1MB到32MB不等,并使用多个后台线程来扫描它们。G1回收器会优先扫描那些包含垃圾最多的区域,这正是它的名字的由来(Garbage first)。这个回收器可以通过-XX:UseG1GC标记来启用。

这一策略减少了后台线程还未扫描完无用对象前堆就已经用光的可能性,而那种情况回收器就必须得暂停应用,这就会导致STW回收。G1的另一个好处就是它总是会进行堆的压缩,而CMS回收器只有在full GC的时候才会干这事。

过去几年里,大堆一直都是一个充满争议的领域,很多开发人员从单机器单JVM模型转向了单机器多JVM的微服务,组件化的架构。这是许多因素所驱动的,包括隔离程序的组件,简化部署,避免重新加载应用类到内存所产生的开销(Java 8中这点已经得到了改善)。

尽管如此,这么做最主要还是希望能避免大堆的GC中长时期的”stop the world”的暂停(在一次大的回收中需要花费数秒才能完成)。像Docker这样的容器技术也加速了这一进程,它们使得你可以很轻松地在同一台物理机上部署多个应用。

Java 8及G1回收器

Java 8 update 20所引入的一个很棒的优化就是G1回收器中的字符串去重(String deduplication)。由于字符串(包括它们内部的char[]数组)占用了大多数的堆空间,这项新的优化旨在使得G1回收器能识别出堆中那些重复出现的字符串并将它们指向同一个内部的char[]数组,以避免同一个字符串的多份拷贝,那样堆的使用效率会变得很低。你可以使用-XX:+UseStringDeduplication这个JVM参数来试一下这个特性。

Java 8及持久代

Java 8中最大的改变就是持久代的移除,它原本是用来给类元数据,驻留字符串,静态变量来分配空间的。这在以前都是需要开发人员来针对那些会加载大量类的应用来专门进行堆比例的优化及调整。许多年来都是如此,这也正是许多OutOfMemory异常的根源,因此由JVM来接管它真是再好不过了。即便如此,它本身并不会减少开发人员将应用解耦到不同的JVM中的可能性。

每个回收器都有许多不同的开关和选项来进行调优,这可能会增加吞吐量,也可能会减少,这取决于你的应用的具体的行为了。在下一篇文章中我们会深入讲解配置这些算法的关键策略。

相关文章

02 May 09:25

ltask :用于 lua 的多任务库

by 云风

写这个东西的起源是,前段时间我们的平台组面试了一个同学,他最近一个作品叫做luajit.io。面试完了后,他专门找我聊了几个小时他的这个项目。他的核心想法是基于 luajit 做一个 web server ,和ngx_lua类似,但撇开 nginx 。当时他给我抱怨了许多 luajit 的问题,但是基于性能考虑又不想放弃 luajit 而转用 lua 。

我当时的建议是,不要把 lua/luajit 作为嵌入语言而自己写 host 程序,而是想办法做成供 lua 使用的库。这样发展的余地要大很多,也就不必局限于用户使用 lua 还是 luajit 了。没有这么做有很多原因是设计一个库比设计一个 host 程序要麻烦的多,不过麻烦归麻烦,其实还是可以做一下的,所以我就自己动手试了一下。

Lua 的多任务库有很多,有兴趣的同学可以参考一下 lua user wiki

一般有几种做法:要么不使用操作系统的线程,只用 lua 本身的 coroutine 来模拟多任务。用 lua 写一个调度器即可。由于 lua 目前允许从 debug hook 中 yield (但暂时需要用 C 来实现 hook),所以甚至你可以实现一个抢占式的调度器;另一种流行的做法是在每个 os thread 里都开启一个独立的 lua vm ,然后用消息通讯的方式协作。

大多数库实现出来都是为了解决类似 web 服务这种需求,所以同时也都实现了一套配套的网络接口,让网络 IO 可以和多任务系统协调工作。

而我想了一段时间后觉得,如果有一个纯粹的 lua 多任务调度库更好。而且这种库不应该实现成 n:n 的调度器,也就是一个 os thread 配一个 lua vm 。这样就不适合做轻量的任务了。原本创建销毁 lua vm 都是很轻量的操作,而单个 lua vm 所占的内存资源也非常小,基本开销甚至比 os thread 本身的开销小的多。有了 vm 的隔离,实现成 m:n 的调度器应当是理所当然的事。btw,ngx_lua这种让不同连接复用 lua vm 的方式,虽然可以提供响应速度,节省单连接上的内存开销,但从设计上我认为是不太干净的。

解决了任务调度器后,唯一必须的底层协作设施就只有 channel 通讯;而网络 IO ,timer 这些,都应该是更上层的设施,这样的设计可以让多任务库更简单纯粹。比如处理大量网络连接的 lua 库,我的同事就在 libev 的基础上实现过一个levent。完全可以在一个 task 中运行 levent ,然后把收发的网络包通过 channel 传给别的 task 去处理。

我大概花了两天时间做接口设计和实现。目前的代码基本可以运行,放在 github 上,https://github.com/cloudwu/ltask。目前仅提供了一个 M:N 调度器和一组内建的 channel 设置。

你必须在主程序中启动初始化调度器,初始化的时候可以配置使用多少条工作线程。一旦调度器启动起来,将阻塞住永不退出(目前的概念阶段的简化处理),所以在启动之前,你需要启动你的 task 。每个 task 都是一个 lua 文件,可以传递给它任意参数。同一个 lua 文件被启动多次是多个独立的 task 。在 task 运行中也可以启动新的 task ,task 的总数只受内存限制。

调度器会平均把任务分配到固定的工作线程上。task 并没有固定绑定某个特定工作线程。任何一条工作线程空闲时,都会尝试从其它工作线程的队列中抢走待命的 task 。

每个 task 由于是一个独立的 lua vm ,所以不可以共享 lua 数据。但是它们可以通过 channel 协作。channel 从调度器里创建出来,用一个数字 id 来标识,所以 id 可以很方便的在 task 间传递。调度器尽量不复用 channel id (除非 32bit 回绕),所以即使一个 channel 被关闭,读写它也是安全的,并不会错误的操作到其它 channel 上。channel 中每组数据都可以被原子的读写,单条记录可以是任意多个 lua 数据的组合。channel 可以同时有多个读者和写入者。当 channel 存在多个读取者时,调度器会尽量平均的把数据依次分配出去。channel 的写入是非阻塞的,只要内存够用,你可以无限的向 channel 写入数据。

和很多类似框架不同,channel 的读操作并非阻塞的。如果 channel 为空,读操作也会立刻返回。所以在使用时,通常你需要在 lua 层再做一点封装。在读 channel 前应该先 select 它(可以同时 select 多个 channel )。由于 select 也并不阻塞,所以在 select 返回空时,应该立刻调用 coroutine.yield() 让出 cpu 。而最后一次 select 失败,会将当前的 task 标记为 blocked 状态,yield 后不再放回 worker 的处理调度队列中,直到相关 channel 有新的数据写入。

所以,一切的阻塞点都发生在 coroutine.yield 上。在 task 的 main thread 里调用 coroutine.yield 会让出当前 task 对 cpu 的占用。至于 worker 会不会立刻切回来,取决于你之前是否有 select 调用以及 channel 的状态。

ltask 看起来和 skynet 解决了差不多的问题:让 lua 可以充分利用多核系统处理多任务。但它们还是有一些不同的。

skynet 本质上是消息驱动的模型。每个服务(对应 ltask 中的 task )只有一个 channel ,就是它自己。每条消息会唤醒服务一次,赋予服务一小片 CPU 时间。skynet 和 erlang 也不同,它并没有将 channel 实现成一个 mailbox ,所以服务不能自己挑拣消息,而必须按次序消化掉每条消息。

ltask 更接近 erlang 的调度方式。每个 task 可以关心任意多 channel ,task 也可以随时让出 cpu ,而不一定以消息处理来分割时间片。如果你在 task 中设置 debug hook ,也可以很容易的模拟 erlang 那种调度方式,对指令情况做一个简单的记数,超过一定范围后就强制 yield 。而不必让业务层自己来主动调用 yield 。

目前我不太好判断这两种方式的优劣,只能说 skynet 在它设定的模式下,可以实现的效率更高一点点。但从通用性角度看,却不及 ltask 这样简单纯粹。skynet 也很难实现成一个外挂的 lua 库,而使用 ltask 你可以方便的和其它库一起工作来搭建一个你需要的业务处理框架。

能够在 1 天半时间把这个东西搭建好,也是因为之前做了许多铺垫工作。

最后随便提一下:

关于线程池,我没有使用现成的第三方跨平台库。而是自己先实现了一个简单的对 pthread 及 windows threading api 的封装。毕竟我需要用到的 api 很少,所以有个 100 多行的封装库就够了。有兴趣的同学可以看看这个独立的仓库:https://github.com/cloudwu/simplethread

在 ltask 中,task channel 都是暴露出数字 id 供业务层使用的。我觉得这是在多线程环境下最好的方式,比交给用户对象指针要健壮的多。这里我用到了前几天从 skynet 里提出来的另一个模块,之前写过一篇 blog 介绍

02 May 09:22

如何高效的阅读hadoop源代码?

by Dong
作者:Dong | 新浪微博:西成懂 | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
网址:http://dongxicheng.org/mapreduce-nextgen/how-to-read-hadoop-code-effectively/
本博客的文章集合:http://dongxicheng.org/recommend/


本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享hadoop技术内幕,hadoop最新技术进展,发布hadoop相关职位和求职信息,hadoop技术交流聚会、讲座以及会议等。二维码如下:


个人谈谈阅读hadoop源代码的经验。

首先,不得不说,hadoop发展到现在这个阶段,代码已经变得非常庞大臃肿,如果你直接阅读最新版本的源代码,难度比较大,需要足够的耐心和时间,所以,如果你觉得认真一次,认真阅读一次hadoop源代码,一定要有足够的心理准备和时间预期。

其次,需要注意,阅读Hadoop源代码的效率,因人而异,如果你有足够的分布式系统知识储备,看过类似的系统,则能够很快地读它的源代码进行通读,并快速切入你最关注的局部细节,比如你之前看过某个分布式数据库的源代码,对分布式系统的网络通信模块,调度模块等有一定了解,这对阅读hadoop源代码有极大帮助;如果你是一个初学者,对hadoop一无所知,只了解一些java语法,那阅读hadoop源代码是极具挑战的一件事情,尤其是从无到开始入门的过程,是极度煎熬和困惑的,这时候需要你在阅读代码过程中,不断补充缺乏的相关知识(比如RPC,NIO,设计模式等),循序渐进,直到入门。

接下来进入主题,说一下阅读源代码的个人经验。由于我也是从无到入门,再到修改源代码,逐步过渡的,所以,对于很多人而言,具有借鉴意义。

============
第一个阶段:学习hadoop基本使用和基本原理,从应用角度对hadoop进行了解和学习

这是第一个阶段,你开始尝试使用hadoop,从应用层面,对hadoop有一定了解,比如你可以使用hadoop shell对hdfs进行操作,使用hdfs API编写一些程序上传,下载文件;使用MapReduce API编写一个数据处理程序。一旦你对hadoop的基本使用方法比较熟悉了,接下来可以尝试了解它的内部原理,注意,不需要通过阅读源代码了解内部原理,只需看一些博客,书籍,比如《Hadoop权威指南》,对于HDFS而言,你应该知道它的基本架构以及各个模块的功能;对于MapReduce而言,你应该知道其具体的工作流程,知道partition,shuffle,sort等工作原理,可以自己在纸上完整个画完mapreduce的流程,越详细越好。

在这个阶段,建议你多看一些知名博客,多读读《hadoop权威指南》(可选择性看相关的几章)。如果你有实际项目驱动,那是再好不过了,理论联系实际是最好的hadoop学习方法;如果你没有项目驱动,那建议你不要自己一个人闷头学,多跟别人交流,多主动给别人讲讲,最好的学习方式还是“讲给别人听”。

============
第二个阶段:从无到入门,开始阅读hadoop源代码

这个阶段是最困苦和漫长的,尤其对于那些没有任何分布式经验的人。 很多人这个阶段没有走完,就放弃了,最后停留在hadoop应用层面。
这个阶段,第一件要做的事情是,选择一个hadoop组件。如果你对分布式存储感兴趣,那么你可以选择HDFS,如果你读分布式计算感兴趣,你可以选择MapReduce,如果你对资源管理系统感兴趣,你可以选择YARN。

选择好系统后,接下来的经历是最困苦的。当你把hadoop源代码导入eclipse或intellij idea,沏上一杯茶,开始准备优哉游哉地看hadoop源代码时,你懵逼了:你展开那数不尽的package和class,觉得无从下手,好不容易找到了入口点,然后你屁颠屁颠地通过eclipse的查找引用功能,顺着类的调用关系一层层找下去,最后迷失在了代码的海洋中,如同你在不尽的压栈,最后栈溢出了,你忘记在最初的位置。很多人经历过上面的过程,最后没有顺利逃出来,而放弃。

如果你正在经历这个过程,我的经验如下:首先,你要摸清hadoop的代码模块,知道client,master,slave各自对应的模块(hadoop中核心系统都是master/slave架构,非常类似),并在阅读源代码过程中,时刻谨记你当前阅读的代码属于哪一个模块,会在哪个组件中执行;之后你需要摸清各个组件的交互协议,也就是分布式中的RPC,这是hadoop自己实现的,你需要对hadoop RPC的使用方式有所了解,然后看各模块间的RPC protocol,到此,你把握了系统的骨架,这是接下来阅读源代码的基础;接着,你要选择一个模块开始阅读,我一般会选择Client,这个模块相对简单些,会给自己增加信心,为了在阅读代码过程中,不至于迷失自己,建议在纸上画出类的调用关系,边看边画,我记得我阅读hadoop源代码时,花了一叠纸。注意,看源代码过程中,很容易烦躁不安,建议经常起来走走,不要把自己逼得太紧。

在这个阶段,建议大家多看一些源代码分析博客和书籍,比如《Hadoop技术内幕》系列丛书(轩相关网站:Hadoop技术内幕)就是最好的参考资料。借助这些博客和书籍,你可以在前人的帮助下,更快地学习hadoop源代码,节省大量时间,注意,目前博客和书籍很多,建议大家广泛收集资料,找出最适合自己的参考资料。

这个阶段最终达到的目的,是对hadoop源代码整体架构和局部的很多细节,有了一定的了解。比如你知道MapReduce Scheduler是怎样实现的,MapReduce shuffle过程中,map端做了哪些事情,reduce端做了哪些事情,是如何实现的,等等。这个阶段完成后,当你遇到问题或者困惑点时,可以迅速地在Hadoop源代码中定位相关的类和具体的函数,通过阅读源代码解决问题,这时候,hadoop源代码变成了你解决问题的参考书。

============
第三个阶段:根据需求,修改源代码。

这个阶段,是验证你阅读源代码成效的时候。你根据leader给你的需求,修改相关代码完成功能模块的开发。在修改源代码过程中,你发现之前阅读源代码仍过于粗糙,这时候你再进一步深入阅读相关代码,弥补第二个阶段中薄弱的部分。当然,很多人不需要经历第三个阶段,仅仅第二阶段就够了:一来能够通过阅读代码解决自己长久以来的技术困惑,满足自己的好奇心,二来从根源上解决解决自己遇到的各种问题。 这个阶段,没有太多的参考书籍或者博客,多跟周围的同事交流,通过代码review和测试,证明自己的正确性。

============
阅读hadoop源代码的目的不一定非是工作的需要,你可以把他看成一种修养,通过阅读hadoop源代码,加深自己对分布式系统的理解,培养自己踏实做事的心态。

===========

选自我的知乎:http://www.zhihu.com/question/29690410/answer/45588479

原创文章,转载请注明: 转载自董的博客

本文链接地址: http://dongxicheng.org/mapreduce-nextgen/how-to-read-hadoop-code-effectively/

作者:Dong,作者介绍:http://dongxicheng.org/about/

本博客的文章集合:http://dongxicheng.org/recommend/


Copyright © 2013
This feed is for personal, non-commercial use only.
The use of this feed on other websites breaches copyright. If this content is not in your news reader, it makes the page you are viewing an infringement of the copyright. (Digital Fingerprint:
)
02 May 01:40

What are the bad features of Java.

by Peter Lawrey

Overview

When you first learn to develop you see overly broad statements about different features to be bad, for design, performance, clarity, maintainability, it feels like a hack, or they just don't like it.

This might be backed by real world experience where removing the use of the feature improved the code.  Sometimes this is because the developers didn't know how to use the feature correctly, or the feature is inherently error prone (depending whether you like it or not)

It is disconcerting when either fashion, or your team changes and this feature becomes fine or even a preferred methodology.

In this post, I look at some of the feature people like to hate and why I think that used correctly, they should be a force for good.  Features are not as yes/no, good/bad as many like to believe.

Checked Exceptions

I am often surprised at the degree that developers don't like to think about error handling.  New developers don't even like to read error messages. It's hard work, and they complain the application crashed, "it's not working". They have no idea why the exception was thrown when often the error message and stack dump tell them exactly what went wrong if they could only see the clues. When I write out stack traces for tracing purposes, many just see the log shaped like a crash when there was no error.   Reading error messages is a skill and at first it can be overwhelming.

Similarly, handling exceptions in a useful manner is too often avoided.  I have no idea what to do with this exception, I would rather either log the exception and pretend it didn't happen or just blow up and let the operations people or to the GUI user, who have the least ability to deal the error.

Many experienced developers hate checked exceptions as a result.  However, the more I hear this, the more I am glad Java has checked exception as I am convinced they really will find it too easy ignore the exceptions and just let the application die if they are not annoyed by them.

Checked exceptions can be overused of course.  The question should be when throwing a checked exception; do I want to annoy the developer calling the code by forcing them to think a little bit about error handling? If the answer is yes, throw a checked exception.

IMHO, it is a failing of the lambda design that it doesn't handle checked exception transparently. i.e. as a natural block of code would by throwing out any unhandled exception as it does for unchecked exceptions and errors. However, given the history of lambdas and functional programming, where they don't like side effect at all, let alone short cut error handling, it is not surprising.

You can get around the limitation of lambdas by re-throwing a checked exception as if it were an unchecked one.  This works because the JVM has no notion of checked exceptions, it is a compile time check like generics.  My preferred method is to use Unsafe.rethrowException but there is 3 other ways of doing this. Thread.currentThread().stop(e) no longer working in Java 8 despite the fact it was always safe to do.

Was Thread.currentThread().stop(e) unsafe?

The method Thread.stop(Throwable) was unsafe when it could cause another thread to trigger an exception in a random section of code.  This could be a checked exception in a portion of code which didn't expect it, or throw an exception which is caught in some portions of the thread but not others leaving you with no idea what it would do.  

However, the main reason it was unsafe is that it could leave atomic operations in as synchronized of locked section of code in an inconsistent state corrupting the memory in subtle and untestable ways.
To add to the confusion, the stack trace of the Throwable didn't match the stack trace of the thread where the exception was actually thrown.

But what about Thread.currentThread().stop(e)?  This triggers the current thread to throw an exception on the current line.  This is no worse than just using throw exception you are performing an operation the compiler can't check.  The problem is that the compiler doesn't always know what you are doing and whether it is really safe or not.  For generics this is classed as an "unchecked cast" which is a warning which you can disable with an annotation.  Java doesn't support the same sort of operation with checked exception so well and you end up using hacks, or worse hiding the true checked exception as a runtime exception meaning there is little hope the caller will handle it correctly.

Is using static bad?

This is a new "rule" for me.  I understand where it is coming from, but there is more exceptions to this rule than where it should apply.  Let us first consider all the contexts where the overloaded meaning of static can be used.
  1. static mutable fields
  2. static immutable field (final primitive or final fields pointing to objects which are not changed)
  3. static methods.
  4. static classes (which have no implicit reference to an outer instance)
  5. static initialiser blocks.
I would agree that using static mutable fields is likely to be either a newbie bug, or something to be avoided if at all possible. If you see static fields being altered in a constructor, it is almost certainly a bug.(Even if not, I would avoid it)  I believe this is the cause of the statement to avoid all static.

However, in all the other cases, using static is not only more performant, it is clearer.  It shows this field isn't different for each instance, or that the method or class doesn't implicitly depend on than instance.

In short, static is good, and mutable static fields are the exception, not the rule.

Are Singletons bad?

The problems with singletons come from two directions.  They are effectively global mutable state making them difficult to maintain or encapsulte e.g. in a unit test, and they support auto-wiring. i.e. any component can access it making your dependencies unclear and difficult to manage.  For these reasons, some developers hate them.

However, following good dependency injection is a methodology which should be applied to all your components, singletons or not, and you should avoid global mutable state via singletons or not.  

If you exclude global state and self wiring components, you are left with Singletons which are immutable and passed via dependency injection and in this case they can work really elegantly.  A common pattern I use to implement strategies is to use an enum with one instance which implement an interface.

    enum MyComparator implements Comparator {
       INSTANCE;
       public int compare(MyObject o1, MyObject o2) {
           // something a bit too complicated to put in a lambda
       }
    }

This instance can be passed as an implementation of Comparator via dependency injection and without mutable state can be used safely across threads and unit tests.

Can I get a library or framework to do that very simple thing for me?

Libraries and frameworks can save you a lot of time and wasted effort getting you own code to do something which already works else where.

Even if you want to write you own code I strongly suggest you have an understanding of what existing libraries and frameworks do so you can learn from them.  Writing it yourself is not a short cut to avoid having to understand any existing solutions.  A journalist once wrote with despair about an aspiring journalist that; didn't like to read, only to write.  The same applies in software development.

However, I have seen (on Stackoverflow) developers so to great lengths to avoid using their own code for even trivial examples.  They feel like if they use a library it must be better than anything they have written.  The problem with this is it assumes; adding libraries don't come at a cost to complexity, you have a really good understanding of the library, and you will never need to learn to write code you can trust.

Some developers use frameworks to help learn what is actually a methodology.  Often developers use a framework for dependency injection when actually you could just do this in plain Java, but they either don't trust themselves or their team to do this.

In the high performance space, the simpler the code, the less work your application does, the easier it is to maintain with less moving parts and the faster it will go.  You need to use the minimum of libraries and frameworks which are reasonably easy to understand so you can get your system to perform at it best.

Is using double for money bad?

Using fractional numbers without any regard for rounding will give you unexpected results.  On the plus side, for double, are usually obviously wrong like 10.99999999999998 instead of 11.

Some have the view that BigDecimal is the solution.  However, the problem is that BigDecimal has it's own gotchas, is much harder to validate/read/write but worst of all can look correct when it is not.  Take this example

    double d = 1.0 / 3 * 3 + 0.01;
    BigDecimal bd1 = BigDecimal.valueOf(1.0)
            .divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(3))
            .add(BigDecimal.valueOf(0.01))
            .setScale(2, BigDecimal.ROUND_HALF_UP);
    BigDecimal bd2 = BigDecimal.valueOf(1.0)
            .divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(3)
            .add(BigDecimal.valueOf(0.01)))
            .setScale(2, BigDecimal.ROUND_HALF_UP);
    System.out.println("d: " + d);
    System.out.println("bd1: " + bd1);
    System.out.println("bd2: " + bd2);

This produces three different results.  By sight, which one produces the right result?  Can you tell the difference between bd1 and bd2?

This prints

d: 1.01
bd1: 1.00
bd2: 0.99

Can you see from the output which is wrong? Actually the answer should be 1.01.

Another gotcha of BigDecimal is that equals and compareTo do not behave the same.  equals() can be false when compareTo() returns 0. i.e. in BigDecimal 1.0 equals 1.00 is false as the scales are different.

The problem I have with BigDecimal is that you get code which is often harder to understand and produces incorrect results which look like they could be right.  BigDecimal is significantly slower and products lots of garbage.  (This is improving in each version of Java 8) There are situations where BigDecimal is the best solution, but it is not a given as some would protest.

If BigDecimal is not a great alternative, is there any other?  Often int and long are used with fixed precision e.g. whole number of cents instead of a fraction of dollars.  This has some challenges in you have to remember where the decimal place is.  If Java supports values types, it might makes sense to use these as wrappers for money and give you more safety, but the control, clarify and performance of dealing with whole number primitives.

Using null values

For developers new to Java, getting repeated NullPointerException is a draining experience.  Do I really have to create a new instance of every object, every element in an array in Java?  Other language don't require this as it is often done via embedded data structures. (Something which is being considered for Java)

Even experienced Java developers have difficulty dealing with null values and see it as a big mistake to have null in the language. IMHO The problem is that the replacements are often far worse. such as NULL objects which don't NPE, but perhaps should have been initialised to something else.  In Java 8, Optional is a good addition which makes the handling of a non-result clearer.  I think it is useful for those who struggle with NullPointerException as it forces you to consider that there might not be a result at all.  This doesn't solve the problem of uninitialised fields.  

I don't like it personally as it solves a problem which can be solved more generally by handling null correctly, but I recognise that for many it is an improvement.

A common question is;  how was I supposed to know a variable was null?  This is the wrong way around in my mind.  It should be, why assume it couldn't be null?  If you can't answer that, you have to assume it could be null and an NPE shouldn't be any surprise if you don't check for it.

You could argue that Java could do with more syntactic sugar to make code which handles null cleaner such as the Elvis operator, but I think the problem is that developers are not thinking about null values enough. e.g. do you check that an enum variable is null before you switch on it?. (I think there should be a case null: in switch but there isn't or to fall through to default: but it doesn't)

How important is it to write code fast?

Java is not a terse language and without an IDE to write half the code for you, it would be really painful to write esp if you spent all day writing code.

But this is what developers do all day don''t they?  Actually, they don't.  Developers don't spend much of their time writing code, they spend 90% (for new code) to 99% (for legacy code) understanding the problem.

You might say; I write 1000 lines of code all day long and later and re-write the code (often making it shorter) and some time later I fixed the code   However, while the code is still fresh in your mind, if you were to write just the code you needed in the end (or you do this from a print out) and you divide it by the total time you spent on the project, end to end,  you are likely to find it was actually less than 100 lines of code per day, possibly less than 10 lines per day.

So what were you really doing over that time if it wasn't writing the finished product.  It was understand what was required by the end users, and what was required to implement the solution.

Someone once told me; it doesn't matter how fast, how big, how deep or how many holes you dig, if you are digging them in the wrong place.

Conclusion

I hear views from beginners to distinguished developers claiming you shouldn't/I can't imagine why you would/you should be sacked if you use X, you should only use Y.  I find such statements are rarely 100% accurate.  Often there is either edge cases, and sometimes very common cases where such statement are misleading or simply incorrect.

I would treat any such broad comments with scepticism, and often they find they have to qualify what was said once they see that others don't have the same view.