C++面试考点

2020-12-13 04:16

阅读:582

标签:野指针   class   因此   操作   分析   aci   prim   ror   构造函数   

后端开发面试题
===================



#后端开发面试知识点大纲:
##语言类(C++):
###关键字作用解释:
volatile作用

	Volatile关键词的第一个特性:易变性。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。

	Volatile关键词的第二个特性:“不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
	Volatile关键词的第三个特性:”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。
	C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。哪怕将所有的变量全部都声明为volatile,哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力
	针对这个多线程的应用,真正正确的做法,是构建一个happens-before语义。

	[C/C++ Volatile关键词深度剖析](http://hedengcheng.com/?p=725)
	
static

	控制变量的存储方式和可见性。 
	
	(1)修饰局部变量
	
	一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。
	
	(2)修饰全局变量
	
	对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。
	
	(3)修饰函数
	
	用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。
	
	(4)C++中的static
	
	如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。	
		
const的含义及实现机制

	 const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。
	 
	(1)const修饰基本数据类型
	
	 1.const修饰一般常量及数组
	  
	 基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。 
	  
	 2.const修饰指针变量*及引用变量&  
	 
	如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;

	如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
	
	(2)const应用到函数中,  
	
	 1.作为参数的const修饰符
	 
	 调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。
     [注意]:参数const通常用于参数为指针或引用的情况; 
     
     2.作为函数返回值的const修饰符
     
     声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
	
	(3)const在类中的用法
	
	不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行
	类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量。
	
	(4)const修饰类对象,定义常量对象 
	常量对象只能调用常量函数,别的成员函数都不能调用。
	
	http://www.cnblogs.com/wintergrass/archive/2011/04/15/2015020.html

extern

	在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
	
	注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。
	
	在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。
			
宏定义和展开、内联函数区别,

	内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。	内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。

	宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token的替换和连接等操作,在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性。
	对于问题:有了函数要它们何用?答案是:一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回)

	内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样

	内联函数必须是和函数体申明在一起,才有效。

	[宏定义和内联函数区别](http://www.cnblogs.com/chengxuyuancc/archive/2013/04/04/2999844.html)
###库函数实现:
malloc,strcpy,strcmp的实现,常用库函数实现,哪些库函数属于高危函数

###STL原理及实现:
STL各类型容器实现,STL共有六大组件

STL提供六大组件,彼此可以组合套用:

    1、容器(Containers):各种数据结构,如:序列式容器vector、list、deque、关联式容器set、map、multiset、multimap。用来存放数据。从实现的角度来看,STL容器是一种class template。
    
    2、算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last)
    
    3、迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
    
    4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。
    
    5、配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。
    
    6、配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。
    
    这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数)
    
    序列式容器:
	vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
	list-单链表。
	deque-分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址.记住这个map仅仅保存的是数组的地址,真正的数据在数组中存放着.deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。当map也不够时重新分配内存当作新的map,把原来map中的内容copy的新map中。所以使用deque的复杂度要大于vector,尽量使用vector。
	
	stack-基于deque。
	queue-基于deque。
	heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。
	priority_queue-基于heap。
	slist-双向链表。
	
	关联式容器:
	set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。
	
	hash table-散列表。将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。有些数据的key经过算法的转换可能是同一个数组的索引值(碰撞问题,可以用线性探测,二次探测来解决),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。
	
	hash_map,hash_set,hash_multiset,hash_multimap-基于hashtable。
    
 [STL六大组件] (http://blog.csdn.net/chenguolinblog/article/details/30336805)  
什么是“标准非STL容器”?

list和vector有什么区别?

	vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。
	list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。

###虚函数:
虚函数的作用和实现原理,什么是虚函数,有什么作用? 

	C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。	
		
	虚函数实现原理:包括虚函数表、虚函数指针等 

	虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。

结论:

	每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl
	同时该类的每个对象都会包含一个vptr去指向该vtbl
	虚函数按照其声明顺序放于vtbl表中, vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数
	如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置
	在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中
	
衍生问题:为什么 C++里访问虚函数比访问普通函数慢? 

	单继承时性能差不多,多继承的时候会慢

调用性能方面

	从前面虚函数的调用过程可知。当调用虚函数时过程如下(引自More Effective C++):

	通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。
	找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。
	调用第二步找到的的指针所指向的函数。
	在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承的情况下,由于会根据多个父类生成多个vptr,在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈。 虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。

占用空间方面


	在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表vtbl的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的vtbl,如果子类只覆盖父类的一两个虚函数接口,子类vtbl的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些GUI库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致UI库的占用内存明显变大。 由于虚函数指针vptr的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。

[C++虚函数浅析](http://glgjing.github.io/blog/2015/01/03/c-plus-plus-xu-han-shu-qian-xi/)

纯虚函数,为什么需要纯虚函数?

	纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
	
	virtual void funtion1()=0
	
	原因:
	1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
	2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
	
	为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
	
	定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
	纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
	
	[虚函数和纯虚函数的区别](http://blog.csdn.net/hackbuteer1/article/details/7558868)
为什么需要虚析构函数,什么时候不需要?父类的析构函数为什么要定义为虚函数

	一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
    当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

内联函数、构造函数、静态成员函数可以是虚函数吗?

	inline, static, constructor三种函数都不能带有virtual关键字。
	inline是编译时展开,必须有实体;
	static属于class自己的,也必须有实体;
	virtual函数基于vtable(内存空间),constructor函数如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例,class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。
	
	虚函数实际上不能被内联:虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。
	
	构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 
	
	静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制。			
构造函数中可以调用虚函数吗?
			

最后,总结一下关于虚函数的一些常见问题:

	1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 
	
	2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 
	
	3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
	 
	4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 
	
	5) 纯虚函数通常没有定义体,但也完全可以拥有。
	
	6)  析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 
	
	7) 非纯的虚函数必须有定义体,不然是一个错误。 
	
	8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
[虚析构函数(√)、纯虚析构函数(√)、虚构造函数(X)](http://www.cnblogs.com/chio/archive/2007/09/10/888260.html)

为什么需要虚继承?虚继承实现原理解析,

	虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
	如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类,虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

	虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示,
[C++虚拟继承](http://blog.csdn.net/hyg0811/article/details/11951855)
###设计模式:

C++单例模式写法:

	静态化并不是单例 (Singleton) 模式:
	第一, 静态成员变量初始化顺序不依赖构造函数, 得看编译器心情的, 没法保证初始化顺序 (极端情况: 有 a b 两个成员对象, b 需要把 a 作为初始化参数传入, 你的类就 必须 得要有构造函数, 并确保初始化顺序).
	
	第二, 最严重的问题, 失去了面对对象的重要特性 -- "多态", 静态成员方法不可能是 virtual 的. Log 类的子类没法享受 "多态" 带来的便利.
	class Log {
	public:
	  static void Write(char const *logline);
	  static bool SaveTo(char const *filename);
	private:
	  static std::list<:string> m_data;
	};
	
	In log.cpp we need to add
	
	std::list<:string> Log::m_data;
	
	饿汉模式:
	饿汉模式 是指单例实例在程序运行时被立即执行初始化:
	
	class Log {
	public:
	  static Log* Instance() {
	    return &m_pInstance;
	  }
	
	  virtual void Write(char const *logline);
	  virtual bool SaveTo(char const *filename);
	
	private:
	  Log();              // ctor is hidden
	  Log(Log const&);    // copy ctor is hidden
	
	  static Log m_pInstance;
	  static std::list<:string> m_data;
	};
	
	// in log.cpp we have to add
	Log Log::m_pInstance;
	这种模式的问题也很明显, 类现在是多态的, 但静态成员变量初始化顺序还是没保证.
	
	
	懒汉模式 (堆栈-粗糙版)
	单例实例只在第一次被使用时进行初始化:
	
	class Log {
	
	public:
	  static Log* Instance() {
	    if (!m_pInstance)
	      m_pInstance = new Log;
	    return m_pInstance;
	  }
	
	  virtual void Write(char const *logline);
	  virtual bool SaveTo(char const *filename);
	
	private:
	  Log();        // ctor is hidden
	  Log(Log const&);    // copy ctor is hidden
	
	  static Log* m_pInstance;
	  static std::list<:string> m_data;
	};
	
	// in log.cpp we have to add
	Log* Log::m_pInstance = NULL;
	Instance() 只在第一次被调用时为 m_pInstance 分配内存并初始化. 嗯, 看上去所有的问题都解决了, 初始化顺序有保证, 多态也没问题.
	程序退出时, 析构函数没被执行. 这在某些设计不可靠的系统上会导致资源泄漏, 比如文件句柄, socket 连接, 内存等等
	对于这个问题, 比较土的解决方法是, 给每个 Singleton 类添加一个 destructor() 方法:
	
	
	懒汉模式 (局部静态变量-最佳版)
	
	它也被称为 Meyers Singleton [Meyers]:
	
	class Log {
	public:
	  static Log& Instance() {
	    static Log theLog;
	    return theLog;
	  }
	
	  virtual void Write(char const *logline);
	  virtual bool SaveTo(char const *filename);
	
	private:
	  Log();          // ctor is hidden
	  Log(Log const&);      // copy ctor is hidden
	  Log& operator=(Log const&);  // assign op is hidden
	
	  static std::list<:string> m_data;
	};
	
	在 Instance() 函数内定义局部静态变量的好处是, theLog `` 的构造函数只会在第一次调用 ``Instance() 时被初始化, 达到了和 "堆栈版" 相同的动态初始化效果, 保证了成员变量和 Singleton 本身的初始化顺序.
	
	它还有一个潜在的安全措施, Instance() 返回的是对局部静态变量的引用, 如果返回的是指针, Instance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁. 构造函数和拷贝构造函数也私有化了, 这样类的使用者不能自行实例化.
	
	另外, 多个不同的 Singleton 实例的析构顺序与构造顺序相反.
	
	[C++ Singleton (单例) 模式最优实现](http://blog.yangyubo.com/2009/06/04/best-cpp-singleton-pattern/)
	
用C++设计一个不能被继承的类。

	构造函数或析构函数为私有函数,所以该类是无法被继承的,
	
如何定义一个只能在堆上定义对象的类?栈上呢

	只能在堆内存上实例化的类:将析构函数定义为private,在栈上不能自动调用析构函数,只能手动调用。也可以将构造函数定义为private,但这样需要手动写一个函数实现对象的构造。
	
	只能在栈内存上实例化的类:将函数operator new和operator delete定义为private,这样使用new操作符创建对象时候,无法调用operator new,delete销毁对象也无法调用operator delete。
	

	[设计一个只能在堆上或栈上实例化的类](http://www.cnblogs.com/luxiaoxun/archive/2012/08/03/2621827.html)

满足上述3个条件

	[C++中的单例模式](http://www.cnblogs.com/xiehongfeng100/p/4781013.html)
	
	
多重类构造和析构的顺序

	先调用基类的构造函数,在调用派生类的构造函数
	
	先构造的后析构,后构造的先析构
###内存分配:

内存分配方式有三种:

	(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
	
	(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
	
	(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

c++运行时各类型内存分配(堆,栈,静态区,数据段,BSS,ELF),BSS段,
sizeof一个类求大小(字节对齐原则)、
			
C++四种强制类型转换,
			
int char float,long long long类型长度
###指针:

防止指针的越界使用,

	必须让指针指向一个有效的内存地址, 
	1 防止数组越界 
	2 防止向一块内存中拷贝过多的内容 
	3 防止使用空指针 
	4 防止改变const修改的指针 
	5 防止改变指向静态存储区的内容 
	6 防止两次释放一个指针 
	7 防止使用野指针. 

什么是指针退化及防止、

	如果用一个数组作为函数入参 
	比如 
	void fun(char a[100]) 
	{ 
	coutm_data63356) cout 高地址
	0x12  |  0x34  |  0x56  |  0x78
	2)小端模式:
	低地址 ------------------> 高地址
	0x78  |  0x56  |  0x34  |  0x12
	
	32bit宽的数0x12345678在Little-endian模式以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:
	内存地址	小端模式存放内容	大端模式存放内容
	0x4000	0x78	0x12
	0x4001	0x56	0x34
	0x4002	0x34	0x56
	0x4003	0x12	0x78
	
	4)大端小端没有谁优谁劣,各自优势便是对方劣势:
	小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
	大端模式 :符号位的判定固定为第一个字节,容易判断正负。


	BOOL IsBigEndian()  
	{  
	    int a = 0x1234;  
	    char b =  *(char *)&a;  //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分  
	    if( b == 0x12)  
	    {  
	        return TRUE;  
	    }  
	    return FALSE;  
	}

	联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写:

	BOOL IsBigEndian()  
	{  
	    union NUM  
	    {  
	        int a;  
	        char b;  
	    }num;  
	    num.a = 0x1234;  
	    if( num.b == 0x12 )  
	    {  
	        return TRUE;  
	    }  
	    return FALSE;  
	}

	一般操作系统都是小端,而通讯协议是大端的。
	常见CPU的字节序
	Big Endian : PowerPC、IBM、Sun
	Little Endian : x86、DEC
	ARM既可以工作在大端模式,也可以工作在小端模式。

常见的信号、系统如何将一个信号通知到进程、

	信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。
	进程之间可以互相通过系统调用kill发送软中断信号。
	SIGHUP 1 A 终端挂起或者控制进程终止 
	SIGINT 2 A 键盘中断(如break键被按下) 
	SIGQUIT 3 C 键盘的退出键被按下 
	SIGILL 4 C 非法指令 
	SIGABRT 6 C 由abort(3)发出的退出指令 
	SIGFPE 8 C 浮点异常 
	SIGKILL 9 AEF Kill信号 
	SIGSEGV 11 C 无效的内存引用 
	SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道 
	
	信号机制是异步的;当一个进程接收到一个信号时,它会立刻处理这个信号,而不会等待当前函数甚至当前一行代码结束运行。信号有几十种,分别代表着不同的意义。信号之间依靠它们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。在Linux系统中,这些信号和以它们的名称命名的常量均定义在/usr/include/bits/signum.h文件中。(通常程序中不需要直接包含这个头文件,而应该包含。)

	信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
	
	发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 
	
	进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,
			
linux系统的各类同步机制、linux系统的各类异步机制、
		
如何实现守护进程

	守护进程最重要的特性是后台运行。		

	1. 在后台运行。
	
	为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
	
	if(pid=fork())
	exit(0); //是父进程,结束父进程,子进程继续
	2. 脱离控制终端,登录会话和进程组
	
	有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
	
	setsid();
	
	说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
	
	3. 禁止进程重新打开控制终端
	
	现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
	
	if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
	
	4. 关闭打开的文件描述符
	
	进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
	
	for(i=0;i 关闭打开的文件描述符close(i);>
	
	5. 改变当前工作目录
	
	进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir("/")
	
	6. 重设文件创建掩模
	
	进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
	
	7. 处理SIGCHLD信号
	
	处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。
	
	signal(SIGCHLD,SIG_IGN);
	
	这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

标准库函数和系统调用的区别,

	1、系统调用
	
	系统调用提供的函数如open, close, read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为 size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如fd=open(/"/dev/video/", O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。
	
	系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。
	
	系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。
	
	系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。
	
	这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。
	2、库函数调用
	
	标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose,fflush, fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_t fwrite(const void *buffer,size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen(/"~/proj/filename/",/"w/")。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。
	
	库函数调用通常用于应用程序中对一般文件的访问。
	
	库函数调用是系统无关的,因此可移植性好。
	
	由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作
	

		
fd和PCB,
		
32位系统一个进程最多有多少堆内存,
		
五种I/O 模式,

	五种I/O 模式:
	【1】       阻塞I/O           (Linux下的I/O操作默认是阻塞I/O,即open和socket创建的I/O都是阻塞I/O)
	【2】       非阻塞 I/O        (可以通过fcntl或者open时使用O_NONBLOCK参数,将fd设置为非阻塞的I/O)
	【3】       I/O 多路复用     (I/O多路复用,通常需要非阻塞I/O配合使用)
	【4】       信号驱动 I/O    (SIGIO)
	【5】        异步 I/O

Apache 模型(Process Per Connection,简称PPC),TPC(ThreadPer Connection)模型,以及 select 模型和 poll 模型,epoll模型



	一般来说,程序进行输入操作有两步:
	1.等待有数据可以读
	2.将数据从系统内核中拷贝到程序的数据区。
	
	对于sock编程来说:
	
	         第一步:   一般来说是等待数据从网络上传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;
	
	         第二步:   是从内核中把数据拷贝到程序的数据区中。
	
	 
	
	阻塞I/O模式                           //进程处于阻塞模式时,让出CPU,进入休眠状态
	        阻塞 I/O 模式是最普遍使用的 I/O 模式。是Linux系统下缺省的IO模式。
	
	       大部分程序使用的都是阻塞模式的 I/O 。
	
	       一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为Linux系统默认的IO模式是阻塞模式)
	
	
	对于一个UDP 套接字来说,数据就绪的标志比较简单:
	(1)已经收到了一整个数据报
	(2)没有收到。
	而 TCP 这个概念就比较复杂,需要附加一些其他的变量。
	
	       一个进程调用 recvfrom  ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。(如果系统调用收到一个中断信号,则它的调用会被中断)
	
	   我们称这个进程在调用recvfrom一直到从recvfrom返回这段时间是阻塞的。当recvfrom正常返回时,我们的进程继续它的操作
	 
	
	非阻塞模式I/O                          //非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源。
	       当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核: “当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
	      我们开始对 recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。
	
	      第四次我们调用 recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后 recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
	      当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling(轮询))。应用程序不停的 polling 内核来检查是否 I/O操作已经就绪。这将是一个极浪费 CPU资源的操作。这种模式使用中不是很普遍。
	
	 
	
	 例如:
	
	         对管道的操作,最好使用非阻塞方式!

	 
	
	I/O多路复用                            //针对批量IP操作时,使用I/O多路复用,非常有好。
	
	       在使用 I/O 多路技术的时候,我们调用select()函数和 poll()函数或epoll函数(2.6内核开始支持),在调用它们的时候阻塞,而不是我们来调用 recvfrom(或recv)的时候阻塞。
	       当我们调用 select函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用 recvfrom函数来将数据拷贝到我们的程序缓冲区中。
	        对于单个I/O操作,和阻塞模式相比较,select()和poll()或epoll并没有什么高级的地方。
	
	       而且,在阻塞模式下只需要调用一个函数:
	
	                            读取或发送函数。
	
	                  在使用了多路复用技术后,我们需要调用两个函数了:
	
	                             先调用 select()函数或poll()函数,然后才能进行真正的读写。
	
	       多路复用的高级之处在于::
	
	             它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
	
	 
	
	IO 多路技术一般在下面这些情况中被使用:
	1、当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。
	2、当程序需要同时进行多个套接字的操作的时候。
	3、如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
	4、如果一个服务器程序同时使用 TCP 和 UDP 协议。
	5、如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd就是这样的)。
	
	
	异步IO模式有::
	
	      1、信号驱动I/O模式
	
	       2、异步I/O模式
	
	信号驱动I/O模式                                                  //自己没有用过。
	
	       我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。
	
	为了在一个套接字上使用信号驱动 I/O 操作,下面这三步是所必须的。
	(1)一个和 SIGIO信号的处理函数必须设定。
	(2)套接字的拥有者必须被设定。一般来说是使用 fcntl 函数的 F_SETOWN 参数来
	进行设定拥有者。
	(3)套接字必须被允许使用异步 I/O。一般是通过调用 fcntl 函数的 F_SETFL 命令,O_ASYNC为参数来实现。
	
	       虽然设定套接字为异步 I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定产生 SIGIO信号发送给套接字属主的时候,程序处在什么状态。
	
	1.UDP 套接字的 SIGIO 信号                   (比较简单)
	在 UDP 协议上使用异步 I/O 非常简单.这个信号将会在这个时候产生:
	
	1、套接字收到了一个数据报的数据包。
	2、套接字发生了异步错误。
	        当我们在使用 UDP 套接字异步 I/O 的时候,我们使用 recvfrom()函数来读取数据报数据或是异步 I/O 错误信息。
	2.TCP 套接字的 SIGIO 信号                  (不会使用)
	          不幸的是,异步 I/O 几乎对 TCP 套接字而言没有什么作用。因为对于一个 TCP 套接字来说,SIGIO 信号发生的几率太高了,所以 SIGIO 信号并不能告诉我们究竟发生了什么事情。
	
	在 TCP 连接中, SIGIO 信号将会在这个时候产生:
	l  在一个监听某个端口的套接字上成功的建立了一个新连接。
	l  一个断线的请求被成功的初始化。
	l  一个断线的请求成功的结束。
	l  套接字的某一个通道(发送通道或是接收通道)被关闭。
	l  套接字接收到新数据。
	l  套接字将数据发送出去。
	
	l  发生了一个异步 I/O 的错误。
	
	一个对信号驱动 I/O 比较实用的方面是NTP(网络时间协议 Network TimeProtocol)服务器,它使用 UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。
	
	因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费的时间。图 6-8 表示了怎样建立这样的一个 UDP 服务器。
	
	 
	
	 
	
	异步I/O模式             //比如写操作,只需用写,不一定写入磁盘(这就是异步I/O)的好处。异步IO的好处效率高。
	      当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。
	异步 I/O 和  信号驱动I/O的区别是:
	        1、信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。
	
	        2、异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。
	
	select,poll,epoll
	
	. Epoll 是何方神圣?
	
	Epoll 可是当前在 Linux 下开发大规模并发网络程序的热门人选, Epoll 在 Linux2.6 内核中正式引入,和 select 相似,其实都 I/O 多路复用技术而已,并没有什么神秘的。
	
	其实在Linux 下设计并发网络程序,向来不缺少方法,比如典型的 Apache 模型( Process Per Connection ,简称PPC ), TPC ( ThreadPer Connection )模型,以及 select 模型和 poll 模型,那为何还要再引入 Epoll 这个东东呢?那还是有得说说的 …
	
	2. 常用模型的缺点
	
	如果不摆出来其他模型的缺点,怎么能对比出 Epoll 的优点呢。
	
	2.1 PPC/TPC 模型
	
	这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。
	
	2.2 select 模型
	
	1. 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,www.linuxidc.com 由FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …
	
	2. 效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了??!!
	
	3. 内核 / 用户空间内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。
	
	2.3 poll 模型
	
	基本上效率和select 是相同的,select 缺点的 2 和 3 它都没有改掉。
	
	3. Epoll 的提升
	
	把其他模型逐个批判了一下,再来看看 Epoll 的改进之处吧,其实把 select 的缺点反过来那就是 Epoll 的优点了。
	
	3.1. Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 察看。
	
	3.2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。
	
	3.3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。
	
	
	4. Epoll 为什么高效
	
	Epoll 的高效和其数据结构的设计是密不可分的,这个下面就会提到。
	
	首先回忆一下select 模型,当有I/O 事件到来时,select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;代码像下面这样:
	
	int res = select(maxfd+1, &readfds,NULL, NULL, 120);
	
	if (res > 0)
	
	{
	
	    for (int i = 0; i  /proc/sys/net/ipv4/tcp_tw_reuse
	#让TIME_WAIT尽快回收,我也不知是多久,观察大概是一秒钟
	echo "1" > /proc/sys/net/ipv4/tcp_tw_recycle
	
	很多文档都会建议两个参数都配置上,但是我发现只用修改tcp_tw_recycle就可以解决问题的了,TIME_WAIT重用TCP协议本身就是不建议打开的。
	
	不能重用端口可能会造成系统的某些服务无法启动,比如要重启一个系统监控的软件,它用了40000端口,而这个端口在软件重启过程中刚好被使用了,就可能会重启失败的。linux默认考虑到了这个问题,有这么个设定:
	
	#查看系统本地可用端口极限值
	cat /proc/sys/net/ipv4/ip_local_port_range
	
	用这条命令会返回两个数字,默认是:32768 61000,说明这台机器本地能向外连接61000-32768=28232个连接,注意是本地向外连接,不是这台机器的所有连接,不会影响这台机器的80端口的对外连接数。但这个数字会影响到代理服务器(nginx)对app服务器的最大连接数,因为nginx对app是用的异步传输,所以这个环节的连接速度很快,所以堆积的连接就很少。假如nginx对app服务器之间的带宽出了问题或是app服务器有问题,那么可能使连接堆积起来,这时可以通过设定nginx的代理超时时间,来使连接尽快释放掉,一般来说极少能用到28232个连接。
	
	因为有软件使用了40000端口监听,常常出错的话,可以通过设定ip_local_port_range的最小值来解决:
	
	echo "40001 61000" > /proc/sys/net/ipv4/ip_local_port_range
	
	但是这么做很显然把系统可用端口数减少了,这时可以把ip_local_port_range的最大值往上调,但是好习惯是使用不超过32768的端口来侦听服务,另外也不必要去修改ip_local_port_range数值成1024 65535之类的,意义不大。
	
	因为使用了nginx代理,在windows下也会造成大量TIME_WAIT,当然windows也可以调整:
	
	在注册表(regedit)的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters上添加一个DWORD类型的值TcpTimedWaitDelay,值就是秒数,即可。
	
	windows默认是重用TIME_WAIT,我现在还不知道怎么改成不重用的,本地端口也没查到是什么值,但这些都关系不大,都可以按系统默认运作。
	------------------------------------------------------------------------------------------------------------------------
	TIME_WAIT状态
	根据TCP协议,主动发起关闭的一方,会进入TIME_WAIT状态,持续2*MSL(Max Segment Lifetime),缺省为240秒,在这个post中简洁的介绍了为什么需要这个状态。
	值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压240*1000=240,000个TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。
	HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个request/response,一个主要原因就是发现了这个问题。还有一个方法减缓TIME_WAIT压力就是把系统的2*MSL时间减少,因为240秒的时间实在是忒长了点,对于Windows,修改注册表,在HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Services\ Tcpip\Parameters上添加一个DWORD类型的值TcpTimedWaitDelay,一般认为不要少于60,不然可能会有麻烦。
	对于大型的服务,一台server搞不定,需要一个LB(Load Balancer)把流量分配到若干后端服务器上,如果这个LB是以NAT方式工作的话,可能会带来问题。假如所有从LB到后端Server的IP包的source address都是一样的(LB的对内地址),那么LB到后端Server的TCP连接会受限制,因为频繁的TCP连接建立和关闭,会在server上留下TIME_WAIT状态,而且这些状态对应的remote address都是LB的,LB的source port撑死也就60000多个(2^16=65536,1~1023是保留端口,还有一些其他端口缺省也不会用),每个LB上的端口一旦进入Server的TIME_WAIT黑名单,就有240秒不能再用来建立和Server的连接,这样LB和Server最多也就能支持300个左右的连接。如果没有LB,不会有这个问题,因为这样server看到的remote address是internet上广阔无垠的集合,对每个address,60000多个port实在是够用了。
	一开始我觉得用上LB会很大程度上限制TCP的连接数,但是实验表明没这回事,LB后面的一台Windows Server 2003每秒处理请求数照样达到了600个,难道TIME_WAIT状态没起作用?用Net Monitor和netstat观察后发现,Server和LB的XXXX端口之间的连接进入TIME_WAIT状态后,再来一个LB的XXXX端口的SYN包,Server照样接收处理了,而是想像的那样被drop掉了。翻书,从书堆里面找出覆满尘土的大学时代买的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中间提到一句,对于BSD-derived实现,只要SYN的sequence number比上一次关闭时的最大sequence number还要大,那么TIME_WAIT状态一样接受这个SYN,难不成Windows也算BSD-derived?有了这点线索和关键字(BSD),找到这个post,在NT4.0的时候,还是和BSD-derived不一样的,不过Windows Server 2003已经是NT5.2了,也许有点差别了。
	做个试验,用Socket API编一个Client端,每次都Bind到本地一个端口比如2345,重复的建立TCP连接往一个Server发送Keep-Alive=false的HTTP请求,Windows的实现让sequence number不断的增长,所以虽然Server对于Client的2345端口连接保持TIME_WAIT状态,但是总是能够接受新的请求,不会拒绝。那如果SYN的Sequence Number变小会怎么样呢?同样用Socket API,不过这次用Raw IP,发送一个小sequence number的SYN包过去,Net Monitor里面看到,这个SYN被Server接收后如泥牛如海,一点反应没有,被drop掉了。
	按照书上的说法,BSD-derived和Windows Server 2003的做法有安全隐患,不过至少这样至少不会出现TIME_WAIT阻止TCP请求的问题,当然,客户端要配合,保证不同TCP连接的sequence number要上涨不要下降。
	----------------------------------------------------------------------------------------------------------------------------
	Socket中的TIME_WAIT状态
	在高并发短连接的server端,当server处理完client的请求后立刻closesocket此时会出现time_wait状态然后如果client再并发2000个连接,此时部分连接就连接不上了,用linger强制关闭可以解决此问题,但是linger会导致数据丢失,linger值为0时是强制关闭,无论并发多少多能正常连接上,如果非0会发生部分连接不上的情况!(可调用setsockopt设置套接字的linger延时标志,同时将延时时间设置为0。)
	TCP/IP的RFC文档。TIME_WAIT是TCP连接断开时必定会出现的状态。
	是无法避免掉的,这是TCP协议实现的一部分。
	在WINDOWS下,可以修改注册表让这个时间变短一些
	
	time_wait的时间为2msl,默认为4min.
	你可以通过改变这个变量:
	TcpTimedWaitDelay 
	把它缩短到30s
	TCP要保证在所有可能的情况下使得所有的数据都能够被投递。当你关闭一个socket时,主动关闭一端的socket将进入TIME_WAIT状态,而被动关闭一方则转入CLOSED状态,这的确能够保证所有的数据都被传输。当一个socket关闭的时候,是通过两端互发信息的四次握手过程完成的,当一端调用close()时,就说明本端没有数据再要发送了。这好似看来在握手完成以后,socket就都应该处于关闭CLOSED状态了。但这有两个问题,首先,我们没有任何机制保证最后的一个ACK能够正常传输,第二,网络上仍然有可能有残余的数据包(wandering duplicates),我们也必须能够正常处理。
	通过正确的状态机,我们知道双方的关闭过程如下
	
	图
	假设最后一个ACK丢失了,服务器会重发它发送的最后一个FIN,所以客户端必须维持一个状态信息,以便能够重发ACK;如果不维持这种状态,客户端在接收到FIN后将会响应一个RST,服务器端接收到RST后会认为这是一个错误。如果TCP协议能够正常完成必要的操作而终止双方的数据流传输,就必须完全正确的传输四次握手的四个节,不能有任何的丢失。这就是为什么socket在关闭后,仍然处于 TIME_WAIT状态,因为他要等待以便重发ACK。
	如果目前连接的通信双方都已经调用了close(),假定双方都到达CLOSED状态,而没有TIME_WAIT状态时,就会出现如下的情况。现在有一个新的连接被建立起来,使用的IP地址与端口与先前的完全相同,后建立的连接又称作是原先连接的一个化身。还假定原先的连接中有数据报残存于网络之中,这样新的连接收到的数据报中有可能是先前连接的数据报。为了防止这一点,TCP不允许从处于TIME_WAIT状态的socket建立一个连接。处于TIME_WAIT状态的socket在等待两倍的MSL时间以后(之所以是两倍的MSL,是由于MSL是一个数据报在网络中单向发出到认定丢失的时间,一个数据报有可能在发送图中或是其响应过程中成为残余数据报,确认一个数据报及其响应的丢弃的需要两倍的MSL),将会转变为CLOSED状态。这就意味着,一个成功建立的连接,必然使得先前网络中残余的数据报都丢失了。
	由于TIME_WAIT状态所带来的相关问题,我们可以通过设置SO_LINGER标志来避免socket进入TIME_WAIT状态,这可以通过发送RST而取代正常的TCP四次握手的终止方式。但这并不是一个很好的主意,TIME_WAIT对于我们来说往往是有利的。
	客户端与服务器端建立TCP/IP连接后关闭SOCKET后,服务器端连接的端口
	状态为TIME_WAIT
	是不是所有执行主动关闭的socket都会进入TIME_WAIT状态呢?
	有没有什么情况使主动关闭的socket直接进入CLOSED状态呢?
	主动关闭的一方在发送最后一个 ack 后
	就会进入 TIME_WAIT 状态 停留2MSL(max segment lifetime)时间
	这个是TCP/IP必不可少的,也就是“解决”不了的。
	
	也就是TCP/IP设计者本来是这么设计的
	主要有两个原因
	1。防止上一次连接中的包,迷路后重新出现,影响新连接
	   (经过2MSL,上一次连接中所有的重复包都会消失)
	2。可靠的关闭TCP连接
	   在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发
	   fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以
	   主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。
	
	TIME_WAIT 并不会占用很大资源的,除非受到攻击。
	
	还有,如果一方 send 或 recv 超时,就会直接进入 CLOSED 状态
	socket-faq中的这一段讲的也很好,摘录如下:
	2.7. Please explain the TIME_WAIT state.
		
什么是滑动窗口,超时重传,
		
列举你所知道的tcp选项,
		
connect会阻塞检测及防止,socket什么情况下可读?

connect会阻塞,怎么解决?(必考必问)

	最通常的方法最有效的是加定时器;也可以采用非阻塞模式。
	
	设置非阻塞,返回之后用select检测状态)
如果sel


评论


亲,登录后才可以留言!