基础语言知识方面:
1、使用struct关键字和class关键字定义类以及在类的继承方面有啥区别?
(1)定义类的差别:
C语言中的struct 关键字也可以实现类,用class 关键字和struct 关键字定义类的唯一差别就在于默认访问级别不同:
默认情况下,struct 成员的访问级别为public,而class 成员的访问级别是private 。语法使用方面都是相同,直接将class 换成struct 即可。
(2)类的继承的差别:
使用class 关键字定义的类,它的派生类默认具有private 继承,而使用struct 关键字定义的类,它的派生类默认具有public 继承。其他的方面没有区别。
因此,主要就两个区别:默认的访问级别和默认的继承级别:class 都是private的,struct 都是public的。
2、派生类和虚函数的概述?
(1)基类中的虚函数被派生类继承过去之后,是希望派生类根据自己的实际需求进行重新定义,以实现特定的功能。如果派生类没有重新定义基类中的某个虚函数,则在调用的时候会使用基类中定义的版本。
(2)派生类中函数的声明必须和基类中定义的方式完全匹配。
(3)基类中被声明为虚函数,在派生类中也依然是虚函数。
3、虚函数和纯虚函数的区别?
(1)带有纯虚函数的类被称之为虚基类,也叫做抽象基类,这种类型的类是不能直接生成对象的,只能被继承。
继承之后,在派生类中对纯虚函数进行重新定义,然后这个派生类才是我们常见的正常的类。如果在派生类中没有对这个纯虚函数重新定义,那么这个派生类也将成为虚基类。
(2)虚函数在派生类中是可以不重新被定义的,但是纯虚函数在派生类中必须得被重新定义。
4、深拷贝和浅拷贝的区别?
(1)举例来说就是:
浅拷贝:
- char p1[]="hello";
- char *p2=p1;
- char p1[]="hello";
- char *p2=new char[];
- p2=p1;
浅拷贝只是对指针的拷贝,拷贝之后,两个指针同时指向同一个内存。
深拷贝不但对指针进行拷贝,还对指针所指向的内容进行拷贝,源指针和经过深拷贝之后的指针是指向两个不同的地址的。
(2)浅拷贝可能出现的问题:
- 浅拷贝只是拷贝了指针而已,使得两个指针同时指向同一个内存地址,这样在对象结束调用析构函数时,会造成同一个资源被释放两次,造成程序崩溃。
- 浅拷贝使得两个指针都指向同一块内存,任何一方的变动都会对另一方造成影响。
5、STL各个容器的实现原理(必考查)?
(1)vector,指的是顺序容器,它是一个动态数组,支持元素的随机插入、删除、查找等操作。
vector 在内存中是一块连续存储的内存空间,当在旧内存空间不够用的情况下,它会自动分配另一个大小是旧内存空间 2倍 的新内存空间,然后把旧内存空间中的所有数据都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行构造新元素,并且同时释放旧内存空间,并且,由于vector 空间的重新配置,导致旧vector 的所有迭代器都失效了。
vector 中数据的随机存取效率很高,O(1)的时间的复杂度,但是在vector 中随机插入元素,需要移动的元素数量较多,效率比较低下。
(2)map,指的是关联容器,它是以“键值对”的形式进行存储的,方便根据关键字来迅速查找其对应的值。
关键字起到索引的作用,值则表示与索引相关联的数据。它的底层实现结构是红黑树,插入元素、删除元素等操作都在O(logN)的时间复杂度。
(3)set,指的是关联容器,set 中存放的是关键字,也即值,也就是说在set 中,关键字就是值,两者融为一体。
set 的底层实现结构也是红黑树,它同样支持高效的元素插入、元素删除等操作。另外,set支持高校的关键字检查是否在set 中。
6、STL有7 种主要的容器:分别是:vector、list、deque、map、multimap、set、multiset。
7、C++的特点是什么?多态实现的机制是什么?多台作用是什么?两个必要条件是什么?
(1)C++ 中的多态机制主要体现在两个方面:一个是函数的重载,一个是接口的重写。
函数重载,体现的是静态多态。
接口多态,指的是“一个接口多种形态”的意思。每一个对象内部都有一个虚函数表指针,该虚函数表指针指向该类的虚函数表,所以在程序中,不管你的对象类型如何转换,但是该对象内部的虚函数表指针的指向始终是固定的,所以,才能实现动态的对象函数调用,这就是C++ 多态实现的原理。
多态的基础是继承,需要类中虚函数的支持,派生类继承基类的大部分资源,但是不能继承基类的构造函数、析构函数、拷贝构造函数、operator=函数、友元函数等。
(2)多态作用:
a、隐藏了函数的实现细节,代码能够模块化。b、函数接口重用:为了类在继承和派生的时候正确使用。
(3)必要条件:
一个是基类中要有虚函数,另一个是基类指针或基类引用要指向派生类的对象。
8、类的多重继承有什么问题?怎么样才能消除多重继承中的二义性?
(1)增加了程序的复杂度,使得程序的编写和维护比较困难,很容易出现。
(2)多重继承,使得派生类和基类中的同名函数产生二义性问题,对于同名函数的调用,不知道调用的是派生类自己的还是基类的,要是基类的,是哪一个基类的,这是引发的问题。C++ 中使用虚继承来消除这个二义性问题。或者使用成员限定符“作用域运算符::”来避免这个问题。
9、求两个数的乘积和商数,怎么用宏定义来实现?
(1)#define product(a,b) ((a)*(b))
(2)#define divide(a,b) ((a)/(b))
10、什么叫做静态关联,什么叫做动态关联?
多态中,静态关联指的是:程序在编译阶段,就能确定实际执行的动作,比如你是用类的对象来调用类的函数成员。
动态关联指的是:程序运行阶段才能确定执行的动作,比如多态的使用。
11、什么叫做智能指针?常用的智能指针有哪些?智能指针的实现是怎样的?
(1)C++11新标准中,引入了智能指针的概念。智能指针,是一个存储指向动态分配(堆)对象指针的类,构造函数传入普通指针,析构函数释放指针。栈上分配,函数或程序结束后自动释放,防止内存泄露。
(2)使用引用计数器,类与指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。
创建类的新对象时,初始化指针并将引用计数置为1;
当对象作为另一对象的副本而创建时,引用计数 +1;
对一个对象进行赋值时,引用计数 -1(当引用计数减至 0 时,则删除基础对象),并增加右操作数所指对象的引用计数 +1;
调用析构函数时,构造函数减少引用计数,当引用计数减至 0 时,则删除基础对象。
(3)智能指针如下:
std::auto_ptr,不支持复制(拷贝构造函数)和赋值(operator =),编译不会提示出错。
unique_ptr, 不支持复制和赋值,但比 auto_ptr 好,直接赋值会编译出错。
shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为 0 的时候这个内存会被释放。
12、析构函数可以抛出异常么?为什么不能抛出异常?除了资源泄漏,还有其他需要考虑的么?
(1)C++ 标准明确规定,类的析构函数不能抛出异常、也不应该抛出异常。
(2)如果对象在运行期间出现了异常,C++ 异常处理机制则有责任去清除那些由于出现异常而导致已经失效了的对象,并释放对象原来所分配的资源,这其实就是调用对象的析构函数来完成资源的释放任务,所以从这个意义上来讲,析构函数已经变成了异常处理机制中的一部分。
- 如果析构函数抛出了异常,则异常点之后的语句都不会被执行,如果析构函数在异常点之后执行了某些必要的动作比如是释放内存资源的动作,那么则会导致这些释放资源的动作不会被执行,那么便会造成资源泄露的现象发生。
- 通常异常发生时,C++ 机制会调用已经构造的对象的析构函数来释放资源,此时,若析构函数本身抛出异常,则前一个异常在尚未处理完全的情况下又出现了新的异常,会造成程序的崩溃。
13、拷贝构造函数的作用及其用途(拷贝构造函数什么时候会被调用?)?什么时候需要自定义拷贝构造函数?
(1)拷贝构造函数原型的一般形式:类名(const 类名 &形参对象名) { };
构造函数的形参是本类对象的引用而不是本类的对象,是为了防止引起拷贝构造函数无休止地递归调用。
拷贝构造函数在如下三种情况下会被调用:
- 程序新建立一个对象,并且用另外一个已存在同类的对象对它进行初始化时。
- 当函数的形式参数是类的对象时。(在调用函数时需要将实参对象完整的传递给形参,也就是需要建立一个实参的拷贝,这就是按照实参复制一个形参,系统就是通过调用拷贝构造函数来实现的,以保证形参和实参具有完全相同的值。)
- 当函数的返回值是类的对象时。(在函数调用完成之后需要将返回值带回给函数调用处,此时需要将函数中的对象复制一个临时对象并传递给该函数的调用处。)
(2)如下所示:如果在类的构造函数中存在动态内存分配,那么则必须定义“拷贝构造函数”,否则会导致两个对象成员指向同一个地址,出现“指针悬挂”问题。(指针悬挂:指的是,如果两个指针同时指向同一个内存地址,若我们通过其中一个指针释放掉了该内存单元,则该内存便无效了,然而此时还有另外一个指针指向这个无效的内存单元,我们就说这个指针是悬挂指针,其指向的内存单元是不可预期的。)
- class A
- {
- private:
- int *p;
- public:
- A( ) //默认构造函数
- {
- p = new int(3);
- }
- };
14、sizeof() 函数求一个类的大小?(注意变量成员的顺序,函数成员,虚函数,继承等对大小的影响。)
(1)
(2)
(3)
15、静态函数成员和静态数据成员有什么意义?
(1)非静态数据成员,每个对象都有自己的一份拷贝。
而静态数据成员,则被当做是类的成员,在程序中只被分配一份内存,是该类的所有对象所共同享有的,它的值一旦被更新,所有对象都可见这个更新。
静态数据成员存储在全局静态存储区,所以不能在类中声明定义,应该在类外定义。由于它不属于某一个特定类的对象,在没有产生类对象时作用域就可见,所以在没有产生类对象的时候也可以使用类名+作用域运算符::来调用静态数据成员。
(2)静态函数成员,与静态数据成员是一样的,都是属于整个类的,而不是属于某一个具体的类的对象的。
对于每一个普通函数成员,它都是属于某一个具体的类对象的,都有自己的 this 指针指向这个对象。而静态函数成员没有 this 指针,它无法调用属于类的非静态数据成员和非静态函数成员。
静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态函数成员;非静态函数成员可以任意地访问静态数据成员和静态函数成员。
(3)没有了 this 指针的额外开销,静态函数成员与类的普通函数相比,执行速度上会有少许的增长。16、模板是怎么实现的?模板的作用是什么?
(1)实现一个求三个数的最大值的模板函数:
- template<typename T1, typename T2> //T1和T2不能重名
- 另:
- template<typename T>
- T max(T a, T b, T c)
- {
- if(b>a)
- a=b;
- if(c>a)
- a=c;
- return a;
- }
(2)作用:将算法和具体的对象分离开,与类型无关,比较通用,节省精力。
17、内存的静态分配和动态分配有什么区别?
(1)时间不同。静态分配是发生在程序编译期间的。动态分配则是发生在程序执行期间的。
(2)空间不同。堆区域是动态分配的,没有静态分配的堆区域。栈区域有 2种 分配方式:静态分配栈和动态分配栈。
静态分配栈,是由编译器完成的,比如函数局部变量的分配、比如函数形式参数的分配。
allocate类 可以从栈里分配动态内存,不用担心内存泄漏问题,当函数返回时,通过allocate 申请的内存就会被自动释放。
18、深入谈谈堆和栈?
(1) 分配和管理方式不同。
- 堆内存是动态分配的,其空间的分配和释放都是由程序员手动完成的。
- 栈内存是由编译器管理的。栈有 2 种分配方式:静态分配和动态分配。 静态分配是由编译器完成,比如函数局部变量的分配、比如函数形式参数的分配。 allocate类 可以从栈里分配动态内存,不用担心内存泄漏问题,当函数返回时,通过allocate 申请的内存就会被自动释放。但是,这里要注意:栈的动态分配和堆内存的动态分配是不同的,它的动态分配是由编译器释放的,无需手工控制。
(2)产生内存碎片不同。
- 对于堆内存,频繁的new/delete 运算符和malloc/free 函数调用,势必会造成内存空间的不连续,造成大量的内存碎片,使程序效率降低。
- 对于栈内存,则不存在内存碎片问题,这是因为栈这种数据结构是先进后出的,不可能有一个内存块从栈的中间位置弹出。
(3)生长方向不同。
- 堆内存,是向着内存地址增加的方向增长的,从内存空间的低地址向高地址的方向依次增长。
- 栈内存,是向着内存地址减小的方向增长的,从内存空间的高地址向低地址增长。
- 在内存中,“堆”和“栈”共用全部的自由空间,只不过各自的起始地址和增长方向不同,它们之间并没有一个固定的界限,如果在运行时,“堆”和 “栈”增长到发生了相互覆盖时,称为“栈堆冲突”,系统肯定垮台。
低 ->|-----------------|
| 全局量(所有已初始化量 .data, |
| 未初始化量 .bss ) |
堆起始->|-----------------|
| 堆向高地址增长 |
| |
| |
| 自由空间 |
| |
| |
| 栈向低地址增长 |
高 栈起始->|-----------------|
19、explicit 关键字 是干什么用的?
(0)参考博客:http://www.cnblogs.com/ymy124/p/3632634.html
(1)首先,C++ 中的 explicit 关键字只能用于修饰只有一个参数的类构造函数,它的作用是表明该构造函数是显示的,而非隐式的。 跟它相对应的另一个关键字是 implicit,意思是该构造函数是隐藏的。类构造函数默认情况下即声明为implicit( 隐式 )。
(2)explicit 关键字的作用就是防止 类构造函数的隐式自动转换。
上面也已经说过了,explicit 关键字只对有一个参数的类构造函数有效,如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的,所以explicit关键字也就无效了。
但是,也有一个例外,就是当除了第一个参数以外的其他参数都有默认值的时候,explicit关键字依然有效,此时,当调用构造函数时只传入一个参数,等效于只有一个参数的类构造函数。
(3)上面的代码中, "CxString string2 = 10;" 这句为什么是可以的呢?
在C++中,如果类的构造函数只有一个参数时,那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象。也就是说 "CxString string2 = 10;" 这段代码,编译器会自动将整型转换为CxString类对象,实际上等同于下面的操作:
CxString string2(10); 或者:
CxString temp( 10 );
CxString string2=temp;
20、内联函数和宏定义的区别?
(1)内联函数是指在普通函数的前面加一个关键字 inline 来标识。对于函数调用而言,每一次函数调用都会消耗时间,所以,对于对于语句比较短小的函数,若是被频繁调用,这时所花费的时间会远大于“把该函数直接写进程序执行流中,而不是去调用它”所花费的时间,因此这是很有益的。
(2)宏定义,不检查函数参数、返回值什么的,只是简单的进行宏展开操作;相对来说,内联函数会检查参数类型,所有更安全。
(3)宏是由预处理器进行宏展开,函数内联是通过编译器来控制实现。
21、内存对齐的原则是什么?
(1)结构体的整体空间大小是占用空间最大的成员(的类型)所占用空间字节数的整数倍。
(2)数据对齐原则:内存按照结构体成员的先后顺序排列,当排列到该成员时,前面已经摆放的空间大小必须是该成员类型大小的整数倍,如果不够则补齐之后然后再摆放该成员。
22、C 语言同意一些令人震惊的结构,下面的结构合法吗?如果合法,则它会做些什么?
int a=5, b=7, c;
c= a+++b;
等同于:c=a++ +b;
则执行之后:a=6,b=7,c=12。
23、string 的实现?(必须会!)
- class mystring
- {
- public:
- //普通构造函数
- <span style="font-size:14px;"></span>
- mystring(const char *str = NULL);
- mystring(const mystring &other);
- mystring & operator=(mystring &other) ;
- mystring::mystring(const char* str)
- {
{ m_str = new char[1]; //分配一个字节 *m_str = ‘\0′; //赋一个’\0′ }
else
{ str = new char[strlen(str) + 1]; //分配空间容纳str内容 strcpy(m_str, str); //复制str到私有成员m_str中 }} //析构函数mystring::~mystring()
{ if(m_str!=NULL) //如果m_str不为NULL,释放堆内存
{ delete [] m_str; m_str = NULL; }} //拷贝构造函数mystring::mystring(const mystring &other)
{ m_str = new char[strlen(other.m_str)+1]; //分配空间容纳str内容 strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中 } //赋值函数mystring & mystring::operator=(mystring &other)
{ if(this == &other) //若对象与other是同一个对象,直接返回本
{ return *this; } delete [] m_str; //否则,先释放当前对象堆内存, m_str = new char[strlen(other.m_str)+1]; //再分配空间容纳str内容, strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中。 return *this;}
24、用变量 p 给出如下定义:
一个有10个指针的数组,每个指针都指向一个函数,该函数有一个整形参数并且返回一个整数:
int ( *p[10] ) ( int );
25、使用预处理指令#define声明一个常数:用来表明一年有多少秒?
#define SECOND_PERYEAR 365*24*60*60(UL)
26、头文件中的ifndef/define/endif 干什么用的?
(1)预处理模块时使用。可以防止头文件被重复使用。
extern c 的作用?
(1)告诉编译器该段代码使用C语言进行编译。
27、volatile 是干什么用的?使用实例有哪些?( 必须将CPU 的寄存器缓存机制回答的很透彻。考察!)
(1)访问寄存器比访问内存单元要快,编译器会优化减少内存的读取,声明变量为volatile 类型的,编译器不会再对访问该变量的代码优化,仍然从内存读取,使访问稳定。
volatile 关键字,会影响编译器编译的结果,用 volatile 关键字声明的变量便是该变量随时可能发生变化,与该变量有关的运算,不再进行编译优化,以免出错。
28、
29、
30、
数据结构和算法方面:
1、100 万个32位的整数,如何最快的找到中位数?能够保证每个数都是唯一的,如何实现O(n)的算法?
(0)中位数定义:数字排序之后,位于中间的那个数。比如将100亿个数字进行排序,排序之后,位于第50亿个位置的那个数 就是中位数。
(1)内存足够的时候,可以使用快速排序法,时间复杂度是O(NlongN),排完序之后直接找到那个处于中间位置的数字即可。
(2)内存不足的时候,可以使用分桶法。
-
假设 100万 个数字保存在一个大文件中,首先依次读一部分文件到内存 (不超过内存的限制), 将每个数字用二进制表示, 比较二进制的最高位 ( 第32位,符号位,0是正,1是负 ), 如果数字的最高位为 0,则将这个数字写入 file_0 文件中;如果最高位为 1,则将该数字写入 file_1 文件中。
-
从而将 100万 个数字分成了两个文件:假设 file_0文件中有 60万 个数字,file_1文件中有 40万 个数字。 那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10万 个数字。(这是因为: file_1 中的数都是负数,file_0 中的数都是正数,也即这里一共只有 40万 个负数,那么排序之后的第 50万 个数一定位于 file_0 中。)
-
现在,我们只需要处理 file_0 文件就可以了(不需要再考虑file_1文件)。 对于 file_0 文件,同样采取上面的措施处理:将file_0 文件依次读一部分到内存 ( 不超内存限制 ), 将每个数字用二进制表示, 比较二进制的 次高位(第31位),如果数字的次高位为 0, 写入 file_0_0 文件中;如果次高位为 1, 写入 file_0_1 文件 中。
-
现假设 file_0_0 文件中有 30万 个数字,file_0_1中也有 30万 个数字,则中位数就是:file_0_0 文件中的数字从小到大排序之后的第 10万 个数字。
-
抛弃 file_0_1 文件,继续对 file_0_0 文件 根据 次次高位( 第30位 ) 划分,假设此次划分的两个文件为:file_0_0_0 中有5万个数字,file_0_0_1中有25万个数字,那么中位数就是 file_0_0_1 文件中的所有数字排序之后的 第 5万 个数。
-
按照上述思路, 直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。
2、
3、
4、
5、
6、
7、
8、
9、
10、
服务器编程方面:
1、多线程和多进程的区别(考察!)
(从CPU调度、上下文切换、数据共享、多核CPU利用率、资源占用等方面回答。然后,有一个问题必须会被问到:哪些东西是一个线程私有的?答案中一定得包含寄存器。)
(1)进程与进程之间的数据空间是分开的,如果要在进程之间进行通信,需要使用特殊的IPC机制,比如管道、信号量、共享内存、消息队列。
线程是存在于进程内的,线程之间共享进程的堆区间、全局静态存储区,而各自享有自己独立的栈空间。线程之间共享数据比较简单,但是线程之间的同步比较复杂,线程同步方法比如说使用互斥量mutex、信号量semaphore。
(2)进程的创建、销毁、切换复杂,速度比较慢。线程的创建、销毁、切换简单,速度快。
(3)进程占用内存多,CPU利用率较低。线程占用内存少,CPU利用率高。
(4)进程之间不会相互影响。进程的一个线程挂掉则会导致整个进程挂掉。
(5)进程适应于多核多机分布,线程适应于多核。
线程所私有的内容:
线程ID号、寄存器的值、栈内存、线程的调度策略、线程的私有数据、信号屏蔽字、errno 变量。
2、多线程的锁的种类有哪些?
(1)互斥锁mutex、自旋锁spin lock、读写锁read/write。
3、自旋锁和互斥锁的区别是什么?(考察!)
(1)自旋锁的定义和优缺点:
自旋锁,它不会引起调用者睡眠,如果自旋锁已经被别的某一线程保持,那么该调用者不会进入睡眠状态,而是一直循环在那里看着 该自旋锁的保持者是否释放了该自旋锁。它的作用就是为了某项资源的互斥使用。因为自旋锁不会引起调用者进入睡眠状态,所以自旋锁的效率远高于互斥锁。虽然自旋锁的效率比较高,但是它仍然有一些不足之处:
- 自旋锁的调用者在未获得锁的情况下还一直在“自旋运行”、占用CPU,如果不能在短时间内获得所需要的“锁”,这无疑会使CPU的利用率下降。所以,自旋锁适用于锁使用者保持锁时间比较短的情况下。
- 使用自旋锁时,在递归调用的时候有可能造成死锁。
(2)两种锁的加锁原理:
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换、CPU的抢占、信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁)、死循环检测锁的标志位、机制不复杂。
(3)闲等锁和忙等锁:
互斥锁属于 sleep-waiting 类型的锁。(闲等待的锁)
自旋锁是属于 busy-waiting 类型的锁。(忙等待的锁)
(要能说出来这个例子!)
例如,在一个双核的机器上有两个线程 ( 线程A和线程B ),它们分别运行在 内核Core0 和 内核Core1上。
现在假设,线程A 想要通过 pthread_mutex_lock 操作 去得到一个临界区的锁,而此时这个锁正被线程B 所持有,那么线程A 就会被阻塞 (blocking),内核Core0 会在此时进行上下文切换 ( Context Switch ) 将线程A 置于等待队列中,此时 内核Core0 就可以运行其他的任务 ( 例如另一个线程C ) 而不必进行忙等待。
但是,自旋锁则不然,它是属于 busy-waiting 类型的锁(忙等待的锁)。如果线程A 是使用 pthread_spin_lock 操作 去请求锁,那么线程A 就会一直在 内核Core0 上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
(4)两种锁的应用场景:
自旋锁:主要就是用在临界区持锁时间非常短、而且CPU资源不紧张的情况下,自旋锁一般用于多核的服务。
- 互斥锁:主要用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑:1)临界区有IO操作。2)临界区循环量比较大。3)单核处理器。
另:
线程同步是并行编程中非常重要的通讯手段,其中最典型的应用就是用
Pthreads 提供的锁机制(lock)来对多个线程之间的共享临界区(Critical Section)进行保护。
Pthreads提供了多种锁机制:
- Mutex(互斥量):pthread_mutex_t
- Spin lock(自旋锁): pthread_spin_t
- Condition Variable(条件变量): pthread_cond_t
- Read/Write lock(读写锁):pthread_rwlock_t
Pthreads提供的Mutex锁操作相关的API主要有:
- pthread_mutex_lock(pthread_mutex_t *mutex);
- pthread_mutex_trylock(pthread_mutex_t *mutex);
- pthread_mutex_unlock(pthread_mutex_t *mutex);
Pthreads提供的Spin Lock锁操作相关的API主要有:
- pthread_spin_lock(pthread_spinlock_t *lock);
- pthread_spin_trylock(pthread_spinlock_t *lock);
- pthread_spin_unlock(pthread_spinlock_t *lock);
4、进程之间通信的方式有哪些?线程之间通信的方式有哪些?(考察!)
7 种进程间的通信方式:
(1) 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。
(2)有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
(3)信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
(4)消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
(5)信号处理机制(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
(6)共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
(7)套接字(socket):套接字也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同及其间的进程通信。
3种 线程间的通信机制:
(1)锁机制:
1.1 互斥锁 mutex:提供了以排它方式阻止数据结构被并发修改的方法。
1.2 读写锁 read/write:允许多个线程同时读共享数据,而对写操作互斥。
1.3 条件变量 condtion variable:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁mutex 的保护下进行的。条件变量始终与互斥锁一起使用。
(2)信号量机制:比如常用的二进制信号量0和1。
(3)信号处理器机制:类似于进程间的信号处理。
线程之间的通信,主要目的是用于线程之间的同步,所以,线程没有象进程通信中用于数据交换的通信机制。
5、进程和线程的区别?(考察!)
(1)定义:
- 进程,它是操作系统进行资源分配的一个独立单位,它是具有一定独立功能的程序关于某个数据集合上的一次执行过程。
- 线程,它是进程内部的一个实体,是CPU 调度的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,它只拥有一点在运行期间必不可少的资源(如程序计数器PC,一组寄存器和栈等)。但是,它可以与同属于一个进程的其他线程 共享进程所拥有的资源。
(2)关系:
- 一个线程可以创建和撤销另一个线程。同一个进程内部的多个线程之间可以并发执行。
- 相对进程而言,线程是一个更加接近于代码执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己独立的栈空间,拥有独立的执行序列。
(3)区别:
- 进程和线程的主要差别在于,它们是不同的操作系统资源管理方式。
- 进程具有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
- 而线程,它只是一个进程内部的不同代码执行流。线程有自己的栈空间(存储局部变量),但是,线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
- 所以,多进程的程序要比多线程的程序更健壮。但是在不同的进程之间切换时,耗费资源较大,效率要差一些。
- 对于一些要求同时执行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1)简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
2)线程的划分尺度小于进程,使得多线程程序的并发性高。
3)另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4)线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
6、多线程程序架构,线程数量应该如何设置?
(1)在多线程程序架构中,线程的数量应该和主机CPU 的核数相等,或者应该等于CPU核数+1 的个数。
另:
在进行进一步深入讨论之前,先以提问的方式就一些共性认知达成一致。
提问:工作线程数是不是设置的越大越好?
回答:不是的。
1)服务器的CPU核数 有限,同时并发的线程数也是有限的,1核CPU设置10000个工作线程没有意义。
2)线程之间的切换是有开销的,如果线程切换过于频繁,反而会使性能降低。
提问:调用sleep() 函数的时候,线程是否一直占用CPU?
回答:不占用,调用sleep() 等待时会把CPU 让出来,给其他需要CPU 资源的线程使用。
不止调用sleep() 函数,在进行一些阻塞调用,例如网络编程中的阻塞 accept()【等待客户端连接】和 阻塞 recv()【等待下游回包】也不占用CPU资源。
提问:如果CPU是单核,设置多线程有意义么,能提高并发性能么?
回答:即使是单核,使用多线程也是有意义的。
1)多线程编码可以让我们的服务/代码更加清晰:有些IO线程收发包,有些Worker线程进行任务处理,有些Timeout线程进行超时检测。
2)如果有一个任务一直占用CPU资源在进行计算,那么此时增加线程并不能增加并发。
例如这样的一个代码:
while(1) { i++; }
该代码一直不停的占用CPU资源进行计算,会使CPU占用率达到100%。
3)通常来说,Worker线程一般不会一直占用CPU进行计算,此时即使CPU是单核,增加Worker线程也能够提高并发,因为这个线程在休息的时候,其他的线程可以继续工作。
7、socket 套接字编程中,如果客户端client 突然断电了,那么服务器如何快速的知道呢?
(1)使用定时器(适合有数据流动的情况)。
(2)使用socket选项SO_KEEPALIVE(适合没有数据流动的情况)。
心跳包的发送,通常有两种技术:
心跳包技术:心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。
- 方法1:应用层自己实现的心跳包。
由应用程序自己发送心跳包来检测连接是否正常。
大致的方法是:服务器端在一个 定时事件中 定时向客户端发送一个短小的数据包,然后启动一个线程,在该线程当中不断检测客户端的ACK应答包。如果在定时时间内收到了客户端的ACK应答包,说明客户端与服务器端的TCP连接仍然是可用的。但是,如果定时器已经超时、而服务器仍然没有收到客户端的ACK应答包,即可以认为客户端已经断开。
同样道理,如果客户端在一定时间内没有收到服务器的心跳包,则也会认为改TCP连接不可用了。
- 方法2:TCP协议的KeepAlive保活机制。
因为要考虑到一个服务器通常会连接很多个客户端,因此,由用户在应用层自己实现心跳包,代码较多而且稍显复杂。
而利用TCP/IP协议层的内置的KeepAlive功能来实现心跳功能则简单得多。不论是服务器端还是客户端,只要一端开启KeepAlive功能后,就会自动的在规定时间内向对端发送心跳包, 而另一端在收到心跳包后就会自动回复,以告诉对端主机我仍然在线。
因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认是不开启KeepAlive功能的。尽管这微不足道,但是在按流量计费的环境下增加了费用,另一方面,KeepAlive设置不合理的话有可能会 因为短暂的网络波动而断开健康的TCP连接。
并且,默认的KeepAlive超时需要即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。
8、基于UDP协议的服务器端和客户端通信,调用connect 函数有什么作用?
(1)因为UDP协议可以一对一通信、一对多通信或者多对一通信,所以在每次调用数据IO函数 sendto( )或者recvfrom( ) 的时候都必须要指定目标主机的IP地址和端口号。
通过调用connect( )函数来建立一个端到端的UDP连接,就可以像TCP一样使用send( )或者recv( ) 传递数据了,而不需要每次都指定目标主机的IP地址和端口号。但是它和TCP不同的是它没有三次握手的过程。
(2)可以通过在已经建立连接的UDP套接字上调用connect( ) 函数来实现指定新的IP地址和端口号,来建立新的UDP连接。
9、socket 套接字在什么情况下是可读的?
(1)首先,先来介绍几个概念:每个套接字有一个接收缓冲区低水位和一个发送缓冲区低水位。他们是由select函数使用!
- 用于读的 接收缓冲区低水位标记:是让select函数返回"可读"时套接字接收缓冲区当中所需要的数据量。对于TCP而言,其默认值为1字节。
- 用于写的 发送缓冲区低水位标记: 是让select函数返回"可写"时套接字发送缓冲区当中所需要的可用空间大小。对于TCP而言,其默认值常为2048字节。
通俗讲:套接字发送缓冲区的作用就是,发送应用程序的数据到发送缓冲区当中,然后一起发给对端。当然了,只有当缓冲区剩余的空间大小达到一定程度(发送低水位标记:eg: 2048)的时候,你才能写数据进去,不然可能导致写空间不够而出现问题。
(2)下列四个条件中的任何一个满足时,socket准备好读:
- socket套接字的接收缓冲区当中的数据量大于等于该套接字的接收缓冲区低水位标记的当前大小时。对这样的socket的读操作将不阻塞、并返回一个大于0的值(也就是返回准备好读入的数据)。
- 该连接的“读功能”这一条线被关闭(也就是接收了FIN数据包的TCP连接)。对这样的socket的读操作将不阻塞、并返回0。
- socket是一个监听套接字,并且已经完成的连接数为非0。这样的监听套接字处于可读状态,是因为socket收到了对方的connect请求,执行了三次握手中的第一步:对方发送SYN请求过来,使监听套接字处于可读状态。
- 有一个socket有异常错误条件待处理。 对于这样的socket的读操作将不会阻塞,并且会返回一个错误(-1),errno全局变量则设置成明确的错误条件。这些待处理的错误也可以通过指定socket选项SO_ERROR调用getsockopt来取得并清除。
(3)下列三个条件中的任何一个满足时,socket准备好写:
socket的发送缓冲区当中的数据量大于等于该socket的发送缓冲区低水位标记的当前大小时。对这样的socket的写操作将不阻塞、并返回一个大于0的值(也就是返回准备好写入的数据量)。对于TCP和UDP的socket而言,其缺省值为2048。
该连接的“写功能”这一条线被关闭。对这样的socket的写操作将产生SIGPIPE信号,该信号的缺省行为是终止进程。
有一个socket异常错误条件待处理。 对于这样的socket的写操作将不会阻塞并且返回一个错误(-1),errno则设置成明确的错误条件。这些待处理的错误也可以通过指定socket选项SO_ERROR调用getsockopt函数来取得并清除。
10、有一个计数器,多个线程都需要更新,会遇到什么情况,原因是什么,应该如何做呢?
(1)某有可能一个线程更新的数据已经被另外一个线程更新过了,更新的数据就会出现异常。
(2)方法:可以对这个计数器的操作代码加锁,保证计数器的更新只会被一个线程完成。
11、什么是原子操作?
(1)原子操作,指的是不会被线程调度机制打断的操作。这种操作一旦开始,就会一直运行下去直到结束,中间是不会有任何的上下文切换的。
(2)如果原子操作过程中出现了异常,那么之前所做的操作全部都原样撤回,撤回到执行这次原子操作之前的初始状态。
12、网络编程设计模式,reactor模式、proactor模式的区别?(考察!)
(1)reactor模式,是一种同步IO模式。proactor模式,指的是异步IO模式。
Reactor模式和Proactor模式,最主要的区别就是:真正的读取和写入操作是由谁来完成的。
Reactor模式是需要应用程序自己读取数据或者写入数据的。
Proactor模式中,应用程序是不需要进行实际的读写操作的,直接可以获得读写操作的结果。
(2)Reactor模式:
主线程向epoll 例程当中注册套接字socket 读请求事件,然后主线程调用epoll_wait 函数来等待这个读请求事件的发生。当某一时刻套接字socket 上有可读数据时,主线程便把套接字socket 上的这个可读事件放入服务器端的请求队列中。然后,睡眠在请求队列上的某个工作线程被唤醒,由这个被唤醒的工作线程来处理客户端请求,然后再向epoll 例程当中注册这个套接字socket 写请求事件,之后主线程会调用epoll_wait 函数来等待这个写请求事件的发生。当有事件可写的时候,主线程便把套接字socket 上的可写事件也放入请求队列中,之后睡眠在请求队列上的某个工作线程被唤醒,由它来处理客户端的请求。
(3)Proactor模式:
主线程调用aio_read 函数向epoll 例程当中注册socket 读完成事件,并告诉内核用户读缓冲区的位置、以及读完成之后如何通知应用程序的方式,之后,主线程则继续处理其他逻辑。一旦当socket 上的数据完全被读入用户缓冲区之后,通过信号告诉应用程序数据已经读取完成、可以使用了。应用程序则直接使用这个结果。
应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后调用aio_write 函数向epoll 例程当中注册socket 写完成事件,并告诉内核写缓冲区的位置、以及写完成时如何通知应用程序的方式,之后,主线程则处理其他逻辑。一旦当用户缓冲区的数据被写入socket 之后,内核便向应用程序发送一个信号,来通知应用程序数据已经发送完成。应用程序预先定义的数据处理函数就会完成工作。
(4)半同步半异步模式:
上层的任务(比如数据文件的传输)使用同步IO模式,简化了编写并行程序的难度。
底层的任务(比如网络控制器的中断处理)使用异步IO模式,提高了执行效率。
13、阻塞IO、非阻塞同步IO、非阻塞异步IO的区别?(考察!)
(1)系统IO操作可分为 阻塞IO、 非阻塞同步IO、非阻塞异步型IO 三种。
(2)阻塞IO,意味着控制权直到调用操作结束之后才会回到调用者手里。 当调用函数时,调用者被阻塞了, 这段时间内调用者做不了任何其它事情。更郁闷的是,在等待IO结果的这段时间里,调用者所在线程此时也无法腾出手来去响应其它客户端请求,比较浪费资源。
比如系统调用read( )操作:调用此函数的代码会一直僵在此处,直到它所读的socket缓存中有数据到来为止。
(3)非阻塞同步IO,是会立即返回控制权给调用者的。 调用者不需要等待调用操作结束这么一个过程,它可以立即从调用的函数那里获取两种结果:要么此次系统调用成功进行了,返回成功的结果;要么系统调用返回错误标识errno 来告诉调用者当前资源不可用。
比如系统调用read( )操作:如果当前socket有数据可读,则调用成功了,直接返回读取数据的字节数。但是,如果当前socket无数据可读,则会立即返回EAGIN信号,告诉调用者“数据还没有准备好,请稍后再试”。
(4)非阻塞异步IO,与非阻塞同步IO稍有不同。系统调用在立即返回的时候,它还告诉调用者,这次请求已经开始了。系统便会使用另外的资源或者线程来完成这次请求操作,并在完成的时候告诉调用者(比如通过回调函数的方式),这次请求已经完成,来读取你的调用结果吧。
比如说对于aio_read( )函数,调用者调用该函数之后,该函数会立即返回,操作系统便会在后台同时开始读操作,并且会在读操作结束的时候通知调用者来取调用结果。
在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。