系统软件工程师面试题

1、c++语言部分html

1. extern c

将让 C++ 中的函数名具有 C-linkage 性质,目的是让 C 代码在调用这个函数时,能正确的连接到具体的地址。前端

C调用C++,使用extern "C"则是告诉编译器依照C的方式来编译封装接口,固然接口函数里面的C++语法仍是按C++方式编译。java

而C++调用C,extern "C" 的做用是:让C++链接器找调用函数的符号时采用C的方式node

函数的具体定义可有可无,仍旧使用 C++ 编译react

------------- 额外的废话linux

C++ 中函数有重载,使用函数名 + 参数信息做为连接时的惟一 ID。ios

C 中函数没有重载,只使用函数名做为连接时的惟一 ID。c++

C编译器编译代码生成的obj文件的符号表内,函数名称保持原样,好比int add(int,int)函数在符号表内就叫作add;C++编译器编译C++代码生成的obj文件符号表内,由于有overload的存在,函数名称的符号再也不是原来的好比add,而是相似_Z3addii这样的(这是个人g++结果)。git

那么,一个C程序须要使用某个C++库内的add函数时,C程序这边指望的是add,但C++库内是_Z3addii这样的,不匹配嘛对不对,因此连接阶段要报错,说找不到add这个函数。程序员

一样,一个C++程序须要使用某个C库内的add函数,C++程序这边指望的是_Z3addii,但C库内是add这样的,一样不匹配,连接阶段也是报错,此次是说找不到_Z3addii。

extern "C"的意思,是让C++编译器(不是C编译器,并且是编译阶段,不是连接阶段)在编译C++代码时,为被extern “C”所修饰的函数在符号表中按C语言方式产生符号名(好比前面的add),而不是按C++那样的增长了参数类型和数目信息的名称(_Z3addii)。

展开来细说,就是:

若是是C调用C++函数,在C++一侧对函数声明加了extern "C"后符号表内就是add这样的名称,C程序就能正常找到add来调用;若是是C++调用C函数,在C++一侧在声明这个外部函数时,加上extern "C"后,C++产生的obj文件符号表内就也是标记为它须要一个名为add的外部函数,这样配合C库,就一切都好。

总结:

无论是C代码调用C++编译器生成的库函数,仍是C++代码调用C编译器生成的库函数,都须要在C++代码一侧对相应的函数进行extern “C”申明。

复制代码 代码以下:

extern "C"  
{  
    int func(int);  
    int var;  
}  


它的意思就是告诉编译器将extern “C”后面的括号里的代码当作C代码来处理,固然咱们也能够以单条语句来声明

复制代码 代码以下:

extern "C" int func(int);  
extern "C" int var;  

这样就声明了C类型的func和var。不少时候咱们写一个头文件声明了一些C语言的函数,而这些函数可能被C和C++代码调用,当咱们提供给C++代码调用时,须要在头文件里加extern “C”,不然C++编译的时候会找不到符号,而给C代码调用时又不能加extern “C”,由于C是不支持这样的语法的,常见的处理方式是这样的,咱们以C的库函数memset为例

复制代码 代码以下:

#ifdef __cplusplus  
extern "C" {  
#endif  
  
void *memset(void*, int, size_t);  
  
#ifdef __cplusplus  
}  
#endif  


其中__cplusplus是C++编译器定义的一个宏,若是这份代码和C++一块儿编译,那么memset会在extern "C"里被声明,若是是和C代码一块儿编译则直接声明,因为__cplusplus没有被定义,因此也不会有语法错误。这样的技巧在系统头文件里常常被用到。

2. volatile/memory barriar

做者:Gomo Psivarh
连接:https://www.zhihu.com/question/31459750/answer/52069135
来源:知乎

C/C++多线程编程中不要使用volatile。
(注:这里的意思指的是期望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,否则就会出错)

C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操做或者互斥锁。
C和C++中的volatile并非用来解决多线程竞争问题的,而是用来修饰一些由于程序不可控因素致使变化的变量,好比访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。

多线程场景下能够参考《Programming with POSIX threads》的做者Dave Butenhof对
Why don't I need to declare shared variables VOLATILE?
这个问题的解释:
comp.programming.threads FAQ

简单的来讲,对访问共享数据的代码块加锁,已经足够保证数据访问的同步性,再加volatile彻底是画蛇添足。
若是光对共享变量使用volatile修饰而在可能存在竞争的操做中不加锁或使用原子操做对解决多线程竞争没有任何卵用,由于volatile并不能保证操做的原子性,在读取、写入变量的过程当中仍然可能被其余线程打断致使意外结果发生。

 

做者:Name5566
连接:https://www.zhihu.com/question/20228202/answer/24959876
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

首先须要明确的是,程序在运行起来,内存访问的顺序和程序员编写的顺序不必定一致,基于这个前提下,Memory barrier 就有存在的必要了。看一个例子:

x = r; y = 1; 

这里,y = 1 在实际运行中可能先于 x = r 进行。实际上,在单线程环境中,这两句谁先执行谁后执行都没有任何关系,它们之间不存在依赖关系,可是若是在多线程中 x 和 y 的赋值存在隐式依赖时:

// thread 1 while (!x); // memory barrier assert(y == r); // thread 2 y = r; // memory barrier x = 1; 

此代码断言就可能失败。

Memory barrier 可以保证其以前的内存访问操做先于其后的完成。若是说到 Memory barrier 经常使用的地方,那么包括:

  1. 实现锁机制
  2. 用于驱动程序
  3. 编写无锁的代码

这里篇幅有限,若是你做为程序员,你能够从 一文入手研究,若是这个还不能知足你,能够进一步深刻硬件来研究多 CPU 间内存乱序访问的问题:

我我的也对 Memory barrier 作了一点小研究,主要写了几个例子验证乱序的存在:

 

3. dynamic cast

reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有彻底相同的比特位

reinterpret_cast用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。

MSDN的Visual C++ Developer Center 给出了它的使用价值:用来辅助哈希函数。下边是MSNDN上的例子:

                // expre_reinterpret_cast_Operator.cpp // compile with: /EHsc #include <iostream> // Returns a hash code based on an address unsigned short Hash( void *p ) { unsigned int val = reinterpret_cast<unsigned int>( p ); return ( unsigned short )( val ^ (val >> 16)); } using namespace std; int main() { int a[20]; for ( int i = 0; i < 20; i++ ) cout << Hash( a + i ) << endl; }

C++中的类型转换分为两种:

1.隐式类型转换;
2.显式类型转换。

而对于隐式变换,就是标准的转换,在不少时候,不经意间就发生了,好比int类型和float类型相加时,int类型就会被隐式的转换位float类型,而后再进行相加运算。而关于隐式转换不是今天总结的重点,重点是显式转换。在标准C++中有四个类型转换符:static_cast、dynamic_cast、const_cast和reinterpret_cast;下面将对它们一一的进行总结。

static_cast

static_cast的转换格式:static_cast <type-id> (expression)

将expression转换为type-id类型,主要用于非多态类型之间的转换,不提供运行时的检查来确保转换的安全性。主要在如下几种场合中使用:

1.用于类层次结构中,基类和子类之间指针和引用的转换;
当进行上行转换,也就是把子类的指针或引用转换成父类表示,这种转换是安全的;
当进行下行转换,也就是把父类的指针或引用转换成子类表示,这种转换是不安全的,也须要程序员来保证;

2.用于基本数据类型之间的转换,如把int转换成char,把int转换成enum等等,这种转换的安全性须要程序员来保证;

3.把void指针转换成目标类型的指针,是及其不安全的;

注:static_cast不能转换掉expression的const、volatile和__unaligned属性。

dynamic_cast

dynamic_cast的转换格式:dynamic_cast <type-id> (expression)

将expression转换为type-id类型,type-id必须是类的指针、类的引用或者是void *;若是type-id是指针类型,那么expression也必须是一个指针;若是type-id是一个引用,那么expression也必须是一个引用。

在C++的面对对象思想中,虚函数起到了很关键的做用,当一个类中拥有至少一个虚函数,那么编译器就会构建出一个虚函数表(virtual method table)来指示这些函数的地址,假如继承该类的子类定义并实现了一个同名并具备一样函数签名(function siguature)的方法重写了基类中的方法,那么虚函数表会将该函数指向新的地址。此时多态性就体现出来了:当咱们将基类的指针或引用指向子类的对象的时候,调用方法时,就会顺着虚函数表找到对应子类的方法而非基类的方法。

固然虚函数表的存在对于效率上会有必定的影响,首先构建虚函数表须要时间,根据虚函数表寻到到函数也须要时间。

由于这个缘由若是没有继承的须要,通常没必要在类中定义虚函数。可是对于继承来讲,虚函数就变得很重要了,这不只仅是实现多态性的一个重要标志,同时也是dynamic_cast转换可以进行的前提条件。

假如去掉上个例子中Stranger类析构函数前的virtual,那么语句
Children* child_r = dynamic_cast<Children*> (stranger_r);

在编译期就会直接报出错误,具体缘由不是很清楚,我猜想多是由于当类没有虚函数表的时候,dynamic_cast就不能用RTTI来肯定类的具体类型,因而就直接不经过编译。

对于从子类到基类的指针转换,static_cast和dynamic_cast都是成功而且正确的(所谓成功是说转换没有编译错误或者运行异常;所谓正确是指方法的调用和数据的访问输出是指望的结果),这是面向对象多态性的完美体现。

从基类到子类的转换,static_cast和dynamic_cast都是成功的,可是正确性方面,我对二者的结果都先进行了是否非空的判别:dynamic_cast的结果显示是空指针,而static_cast则是非空指针。但很显然,static_cast的结果应该算是错误的,子类指针实际所指的是基类的对象,而基类对象并不具备子类的Study()方法(除非妈妈又想去接受个"继续教育")。

对于没有关系的两个类之间的转换,输出结果代表,dynamic_cast依然是返回一个空指针以表示转换是不成立的;static_cast直接在编译期就拒绝了这种转换。

 

四、malloc/ new 

new的功能是在堆区新建一个对象,并返回该对象的指针。

所谓的【新建对象】的意思就是,将调用该类的构造函数,由于若是不构造的话,就不能称之为一个对象。

而malloc只是机械的分配一块内存,若是用mallco在堆区建立一个对象的话,是不会调用构造函数的

 

linux采用的是glibc中堆内存管理ptmalloc实现,虚拟内存的布局规定了malloc申请位置以及大小, malloc一次性能申请小内存(小于128KB),分配的是在堆区(heap),用sbrk()进行对齐生长,而 malloc一次性申请大内存(大于128KB时)分配到的是在映射区,而不是在堆区,采用的mmap()系统调用进行映射。固然虚拟地址只是规定了一种最理想的状态,实际分配仍是要考虑到物理内存加交换内存总量的限制,由于每次分配,特别是大内存分配采用mmap()映射内存须要记录物理内存加交换内存地址,全部物理内存加交换内存限制了malloc实际分配。
malloc的实现与物理内存天然是无关的,内核为每一个进程维护一张页表,页表存储进程空间内每页的虚拟地址,页表项中有的虚拟内存页对应着某个物理内存页面,也有的虚拟内存页没有实际的物理页面对应。不管malloc经过sbrk仍是mmap实现, 分配到的内存只是虚拟内存,并且只是虚拟内存的页号,表明这块空间进程能够用,实际上尚未分配到实际的物理页面。等你的进程访问到这个 新分配的内存空间的时候,若是其尚未对应的物理页面分配,就会产生缺页中断,内核这个时候会给进程分配实际的物理页面,以与这个未被映射的虚拟页面对应起来

连接:https://www.zhihu.com/question/20220583/answer/28490955

五、内存对齐

  2.为何要字节对齐  
  为何呢?简单点说:为了提升存取效率。字节是内存空间分配的最小单位, 在程序中,咱们定义的变量能够放在任何位置。其实不一样架构 的CPU在访问特定类型变量时是有规律的,好比有的CPU访问int型变量时,会从偶数地址开始读取的,int类型占用4个字节(windows平台)。 0X0000,0X0004,0X0008.....这样只须要读一次就能够读出Int类型变量的值。相反地,则须要读取二次,再把高低字节相拼才能获得 int类型的值,这样子看的话,存取效率固然提升了。  一般写程序的时候,不须要考虑这些状况,编译都会为咱们考虑这些状况,除非针对那些特别架构的 CPU编程的时候的则须要考虑 。固然用户也能够手工控制对齐方式。
 3.编译器对字节对齐的一些规则    

  我从下面三条说明了编译器对字节处理的一些原则。固然除了一些特殊的编译器在处理字节对齐的方式也不同, 这些状况我未碰到过,就不做说明了。

  a. 关于数据类型自身的对齐值,不一样类型会按不一样的字节来对齐。
类型 对齐值(字节)
char 1
short 2
int 4
float 4
double 4
      b. 类、结构体的自身对齐字节值。对于结构体类型与类对象的对齐原则:使用成员当中最大的对齐字节来对齐。好比在Struct A中,int a的对齐字节为4,比char,short都大,因此A的对齐字节为4
     c. 指定对齐字节值。意思是指使用了宏 #pragma pack(n)来指定的对齐值

     d. 类、结构及成员的有效对齐字节值。有效对齐值=min(类/结构体/成员的自身对齐字节值,指定对齐字节值)。   有效对齐值决定了数据的存放方 式,sizeof 运算符就是根据有效对齐值来计算成员大小的。简单来讲, 有效对齐其实就是要求数据成员存放的地址值能被有效对齐值整除,即:地址值%有效对齐值=0

 

六、stl

vector/set/map/unorderedmap, 
vector/list区别: 

1.vector数据结构
vector和数组相似,拥有一段连续的内存空间,而且起始地址不变。
所以能高效的进行随机存取,时间复杂度为o(1);
但由于内存空间是连续的,因此在进行插入和删除操做时,会形成内存块的拷贝,时间复杂度为o(n)。
另外,当数组中内存空间不够时,会从新申请一块内存空间并进行内存拷贝。

2.list数据结构
list是由双向链表实现的,所以内存空间是不连续的。
只能经过指针访问数据,因此list的随机存取很是没有效率,时间复杂度为o(n);
但因为链表的特色,能高效地进行插入和删除。

1.说说std::vector的底层(存储)机制。

 vector就是一个动态数组,里面有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另外一片更大的空间(通常是增长当前容量的100%),而后把原来的数据拷贝过去,接着释放原来的那片空间;当释放或者删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

2.std::vector的自增加机制。

当已经分配的空间不够装下数据时,分配双倍于当前容量的存储区,把当前的值拷贝到新分配的内存中,并释放原来的内存。

3.说说std::list的底层(存储)机制。

以结点为单位存放数据,结点的地址在内存中不必定连续,每次插入或删除一个元素,就配置或释放一个元素空间

4.什么状况下用vector,什么状况下用list。

vector能够随机存储元素(便可以经过公式直接计算出元素地址,而不须要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。

list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁。

 

说说std::map底层机制。

map以RB-TREE为底层机制。RB-TREE是一种平衡二叉搜索树,自动排序效果不错。

经过map的迭代器不能修改其键值,只能修改其实值。因此map的迭代器既不是const也不是mutable。

七、c++内存布局, 虚表

C语言的内存模型
 
C语言的内存模型

程序代码区(code area)

存放函数体的二进制代码

静态数据区(data area)

也称全局数据区,包含的数据类型比较多,如全局变量、静态变量、通常常量、字符串常量。其中:

  • 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。
  • 常量数据(通常常量、字符串常量)存放在另外一个区域。

注意:静态数据区的内存在程序结束后由操做系统释放。

堆区(heap area)

通常由程序员分配和释放,若程序员不释放,程序运行结束时由操做系统回收。malloc()、calloc()、free()等函数操做的就是这块内存。

注意:这里所说的堆区与数据结构中的堆不是一个概念,堆区的分配方式却是相似于链表。

栈区(stack area)

由系统自动分配释放,存放函数的参数值、局部变量的值等。其操做方式相似于数据结构中的栈。

命令行参数区

存放命令行参数和环境变量的值,如经过main()函数传递的值。

 
C语句的个部分会出如今哪些段中

C++对象的内存布局

C++语言在C的基础上添加了面向对象的概念,引入了封装,继承,多态。而一个对象的内存布局就相对于C语言的结构体等在内存的布局要复杂的多。
在C++中,有两种数据成员(class data members):static 和nonstatic,以及三种类成员函数(class member functions):static、nonstatic和virtual:

非继承下的C++对象模型

概述:在此模型下,nonstatic 数据成员被置于每个类对象中,而static数据成员被置于类对象以外。static与nonstatic函数也都放在类对象以外,而对于virtual 函数,则经过虚函数表+虚指针来支持,具体以下:

    • 每一个类生成一个表格,称为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外,后面会讨论。
    • 每一个类对象都拥有一个虚表指针(vptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也便是构造函数、析构函数、赋值操做符)来完成。vptr的位置为编译器决定,传统上它被放在全部显示声明的成员以后,不过如今许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容,在后面会详细分析。
      另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象自己的描述等,只有具备虚函数的对象在会生成。
 
C++数据成员及成员函数类型

如今咱们有一个类Base,它包含了上面这5中类型的数据或函数:

class Base { public: Base(int i) :baseI(i){}; int getI(){ return baseI; } static void countI(){}; virtual void print(void){ cout << "Base::print()"; } virtual ~Base(){} private: int baseI; static int baseS; }; 
 
Base类图
 
Base内存布局

能够看到,对一个C++对象来讲,它的内存布局仅有虚表指针和非静态成员,而其余的静态成员,成员函数(静态,非静态),虚表等都是布局在类上的。


##################
A. tips

Aclass* ptra=new Bclass;
 98    int ** ptrvf=(int**)(ptra);
 99    RTTICompleteObjectLocator str=
100        *((RTTICompleteObjectLocator*)(*((int*)ptrvf[0]-1)));

能够明显看到,虚表地址减1以后才获得类型信息。

结论:vptr指向的第一个位置是第一个虚函数的地址,不是type_info。

B. tips

1. 空类
class A
{
};
 
void main()
{
    printf("sizeof(A): %d\n", sizeof(A));
    getchar();
}
 获得结果为:1。
 类的实例化就是给每一个实例在内存中分配一块地址。空类被实例化时,会由编译器隐含的添加一个字节。因此空类的size为1。

2.虚函数
class A
{
    virtual void FuncA();<br>        virtual void FuncB(); 
};
 获得结果:4
当C++ 类中有虚函数的时候,会有一个指向虚函数表的指针(vptr),在32位系统分配指针大小为4字节。因此size为4.

3.静态数据成员
class A
{
  int a;
  static int b;
  virtual void FuncA();
};
 获得结果:8
静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员.可是它不影响类的大小,无论这个类实际产生了多少实例,仍是派生了多少新的类,静态成员数据在类中永远只有一个实体存在。

而类的非静态数据成员只有被实例化的时候,他们才存在.可是类的静态数据成员一旦被声明,不管类是否被实例化,它都已存在.能够这么说,类的静态数据成员是一种特殊的全局变量.
因此该类的size为:int a型4字节加上虚函数表指针4字节,等于8字节。

4.普通成员函数
class A
{
          void FuncA();
}
 结果:1
类的大小与它的构造函数、析构函数和其余成员函数无关,只已它的数据成员有关。

5.普通继承
class A
{
    int a;
};
class B
{
  int b;
};
class C : public A, public B
{
  int c;
};
 结果为:sizeof(C) =12.
可见普通的继承,就是基类的大小,加上派生类自身成员的大小。

6.虚拟继承

class C : virtual public A, virtual public B
{
  int c;
};
 结果:16.

当存在虚拟继承时,派生类中会有一个指向虚基类表的指针。因此其大小应为普通继承的大小(12字节),再加上虚基类表的指针大小(4个字节),共16字节。

###########################


做者:启发禅悟
连接:https://www.jianshu.com/p/0c10b662ef09
來源:简书
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

八、shared_ptr

When using unique_ptr, there can be at most one unique_ptr pointing at any one resource. When that unique_ptr is destroyed, the resource is automatically reclaimed. Because there can only be one unique_ptr to any resource, any attempt to make a copy of a unique_ptr will cause a compile-time error. For example, this code is illegal:

unique_ptr<T> myPtr(new T); // Okay unique_ptr<T> myOtherPtr = myPtr; // Error: Can't copy unique_ptr

However, unique_ptr can be moved using the new move semantics:

unique_ptr<T> myPtr(new T); // Okay unique_ptr<T> myOtherPtr = std::move(myPtr); // Okay, resource now stored in myOtherPtr

Similarly, you can do something like this:

unique_ptr<T> MyFunction() { unique_ptr<T> myPtr(/* ... */); /* ... */ return myPtr; }

This idiom means "I'm returning a managed resource to you. If you don't explicitly capture the return value, then the resource will be cleaned up. If you do, then you now have exclusive ownership of that resource." In this way, you can think of unique_ptr as a safer, better replacement for auto_ptr.

shared_ptr, on the other hand, allows for multiple pointers to point at a given resource. When the very last shared_ptr to a resource is destroyed, the resource will be deallocated. For example, this code is perfectly legal:

shared_ptr<T> myPtr(new T); // Okay shared_ptr<T> myOtherPtr = myPtr; // Sure! Now have two pointers to the resource.

Internally, shared_ptr uses reference counting to track how many pointers refer to a resource, so you need to be careful not to introduce any reference cycles.

In short:

  1. Use unique_ptr when you want a single pointer to an object that will be reclaimed when that single pointer is destroyed.
  2. Use shared_ptr when you want multiple pointers to the same resource.

unique_ptr

unique_ptr 这个类的关键点在于这个定义:

unique_ptr(const unique_ptr&) = delete;

它把拷贝构造函数干掉了,这样的话,就不能直接这样用了:

unique_ptr<A> pa(new A());
    unique_ptr<A> pb = pa;

这样也挺好,既然auto_ptr是由于多个变量持有同一个指针引发的,那么我尽可能避免这种拷贝就行了。

唉,但这是C++啊,不留点口子确定不是C++的风格,因此unique_ptr还留下了move赋值这种东西,这个咱们不去看了。只知道有这么一回事就好了。咱们今天的重点是shared_ptr

shared_ptr

shared_ptr也是对auto_ptr的一种改进,它的思路是,使用引用计数来管理指针。若是一个指针被屡次使用了,那么引用计数就加一,若是减小一次使用,引用计数就减一。当引用计数变为0,那就能够真正地删除指针了。先看一下基本用法:

#include <iostream>
#include <memory>
using namespace std;

class A { 
private:
    int a;
public:
    A() {
        cout << "create object of A" << endl;
        a = 1;
    }   

    ~A() {
        cout << "destroy an object A" << endl;
    }   

    void print() {
        cout << "a is " << a << endl;
    }   
};

int main() {
    shared_ptr<A> pa(new A());
    shared_ptr<A> pb = pa;
    return 0;
}

你们能够与上节课的auto_ptr比较一下,就发现它们的区别了,固然了,这样写仍是不行:

int main() { A * a = new A(); shared_ptr<A> pa(a); shared_ptr<A> pb(a); return 0; } 

这种写法仍是会让指针被 delete 两次。

它的基本原理是在智能指针中引入一个引用计数,在拷贝构造中对引用计数加一,在析构函数中,对引用计数减一。我写一个简单的例子模拟shared_ptr以下:

template <typename V>
class SmartPtr {
private:
    int * refcnt;
    V * v;
public:
    SmartPtr(V* ptr): v(ptr) {
        refcnt = new int(1);
    }   

    SmartPtr(const SmartPtr& ptr) {
        this->v = ptr.v;
        this->refcnt = ptr.refcnt;
        *refcnt += 1;
    }   

    ~SmartPtr() {
        cout << "to delete a smart pointer" << endl;
        *refcnt -= 1;

        if (*refcnt == 0) {
            delete v;
           delete refcnt;
       }
    }
};

int main() {
    A * ptrA = new A();
    SmartPtr<A> sp1(ptrA);
    SmartPtr<A> sp2 = sp1;

    return 0;
}

这个例子中中须要注意的点是引用计数是全部管理同一个指针的智能指针所共享的,因此在这个例子中,sp1和sp2的引用计数指向的是相同的一个整数。

咱们看一下这个例子的输出:

# g++ -o smart myShare.cpp 
# ./smart 
create object of A
to delete a smart pointer
to delete a smart pointer
destroy an object A

能够看到,这个和shared_ptr同样能够正确地delete指针。

 

 

网络部分

一、tcp/udp区别

二、tcp 三次握手/ connect/ accept 关系, read返回0

三、select/ epoll

ET/LT

在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 若是这是一个阻塞socket, 操做将被block,perror输出: Resource temporarily unavailable

总结:
这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种状况,若是是阻塞socket,read/write就要阻塞掉。而若是是非阻塞socket,read/write当即返回-1, 同时errno设置为EAGAIN。
因此,对于阻塞socket,read/write返回-1表明网络出错了。但对于非阻塞socket,read/write返回-1不必定网络真的出错了。多是Resource temporarily unavailable。这时你应该再试,直到Resource available。

综上,对于non-blocking的socket,正确的读写操做为:
读:忽略掉errno = EAGAIN的错误,下次继续读
写:忽略掉errno = EAGAIN的错误,下次继续写

对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。

 

epoll的两种模式LT和ET
两者的差别在于level-trigger模式下只要某个socket处于readable/writable状态,不管何时进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

因此,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

正确的读

n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
}
if (nread == -1 && errno != EAGAIN) {
    perror("read error");
}

 

正确的写

int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
        if (nwrite == -1 && errno != EAGAIN) {
            perror("write error");
        }
        break;
    }
    n -= nwrite;
}

 

正确的accept,accept 要考虑 2 个问题
(1) 阻塞模式 accept 存在的问题
考虑这种状况:TCP链接被客户端夭折,即在服务器调用accept以前,客户端主动发送RST终止链接,致使刚刚创建的链接从就绪队列中移出,若是套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其余某个客户创建一个新的链接为止。可是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其余描述符都得不处处理。

解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept以前停止某个链接时,accept调用能够当即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其余实现把errno设置为ECONNABORTED或者EPROTO错误,咱们应该忽略这两个错误。

(2)ET模式下accept存在的问题
考虑这种状况:多个链接同时到达,服务器的TCP就绪队列瞬间积累多个就绪链接,因为是边缘触发模式,epoll只会通知一次,accept只处理一个链接,致使TCP就绪队列中剩下的链接都得不处处理。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的全部链接后再退出循环。如何知道是否处理完就绪队列中的全部链接呢?accept返回-1而且errno设置为EAGAIN就表示全部链接都处理完。

综合以上两种状况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用方式为:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
}

 

一道腾讯后台开发的面试题
使用Linuxepoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?

第一种最广泛的方式:
须要向socket写数据的时候才把socket加入epoll,等待可写事件。
接受到可写事件后,调用write或者send发送数据。
当全部数据都写完后,把socket移出epoll。

这种方式的缺点是,即便发送不多的数据,也要把socket加入epoll,写完后在移出epoll,有必定操做代价。

一种改进的方式:
开始不把socket加入epoll,须要向socket写数据的时候,直接调用write或者send发送数据。若是返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,所有数据发送完毕后,再移出epoll。

这种方式的优势是:数据很少的时候能够避免epoll的事件处理,提升效率。

四、timeout wait过多, 2MSL

  1. netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'    

它会显示例以下面的信息:

TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1

经常使用的三个状态是:ESTABLISHED 表示正在通讯,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。

若是服务器出了异常,百分之八九十都是下面两种状况:

1.服务器保持了大量TIME_WAIT状态

2.服务器保持了大量CLOSE_WAIT状态

由于linux分配给一个用户的文件句柄是有限的(能够参考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT两种状态若是一直被保持,那么意味着对应数目的通道就一直被占着,并且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就没法被处理了,接着就是大量Too Many Open Files异常,

 

1.服务器保持了大量TIME_WAIT状态

这种状况比较常见,一些爬虫服务器或者WEB服务器(若是网管在安装的时候没有作内核参数优化的话)上常常会遇到这个问题,这个问题是怎么产生的呢?

从 上面的示意图能够看得出来,TIME_WAIT是主动关闭链接的一方保持的状态,对于爬虫服务器来讲他自己就是“客户端”,在完成一个爬取任务以后,他就 会发起主动关闭链接,从而进入TIME_WAIT的状态,而后在保持这个状态2MSL(max segment lifetime)时间以后,完全关闭回收资源。为何要这么作?明明就已经主动关闭链接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定 的,主要出于如下两个方面的考虑:

1.防止上一次链接中的包,迷路后从新出现,影响新链接(通过2MSL,上一次链接中全部的重复包都会消失)
2. 可靠的关闭TCP链接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会从新发fin, 若是这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。因此主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短期内接受大量请求或者受到攻击。

关于MSL引用下面一段话:

[plain]  view plain copy print ?
  1. MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你能够想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,所以我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其余連線使用。  

再引用网络资源的一段话:

[plain]  view plain copy print ?
  1. 值 得一说的是,对于基于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不会太费时间,可是有这么多状态要维护老是很差。  
  2. HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP链接传输多个 request/response,一个主要缘由就是发现了这个问题。  

也就是说HTTP的交互跟上面画的那个图是不同的,关闭链接的不是客户端,而是服务器,因此web服务器也是会出现大量的TIME_WAIT的状况的。
 
如今来讲如何来解决这个问题。
 
解决思路很简单,就是让服务器可以快速回收和重用那些TIME_WAIT的资源。
 
下面来看一下咱们网管对/etc/sysctl.conf文件的修改:
[plain]  view plain copy print ?
  1. #对于一个新建链接,内核要发送多少个 SYN 链接请求才决定放弃,不该该大于255,默认值是5,对应于180秒左右时间   
  2. net.ipv4.tcp_syn_retries=2  
  3. #net.ipv4.tcp_synack_retries=2  
  4. #表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改成300秒  
  5. net.ipv4.tcp_keepalive_time=1200  
  6. net.ipv4.tcp_orphan_retries=3  
  7. #表示若是套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间  
  8. net.ipv4.tcp_fin_timeout=30    
  9. #表示SYN队列的长度,默认为1024,加大队列长度为8192,能够容纳更多等待链接的网络链接数。  
  10. net.ipv4.tcp_max_syn_backlog = 4096  
  11. #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少许SYN攻击,默认为0,表示关闭  
  12. net.ipv4.tcp_syncookies = 1  
  13.   
  14. #表示开启重用。容许将TIME-WAIT sockets从新用于新的TCP链接,默认为0,表示关闭  
  15. net.ipv4.tcp_tw_reuse = 1  
  16. #表示开启TCP链接中TIME-WAIT sockets的快速回收,默认为0,表示关闭  
  17. net.ipv4.tcp_tw_recycle = 1  
  18.   
  19. ##减小超时前的探测次数   
  20. net.ipv4.tcp_keepalive_probes=5   
  21. ##优化网络设备接收队列   
  22. net.core.netdev_max_backlog=3000   
[plain]  view plain copy print ?
  1.   
修改完以后执行/sbin/sysctl -p让参数生效。
 
这里头主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
这几个参数。
 
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。
net.ipv4.tcp_fin_timeout这个时间能够减小在异常状况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测链接存活的相关配置。
 
2.服务器保持了大量CLOSE_WAIT状态
休息一下,喘口气,一开始只是打算说说TIME_WAIT和CLOSE_WAIT的区别,没想到越挖越深,这也是写博客总结的好处,总能够有意外的收获。
 
TIME_WAIT状态能够经过优化服务器参数获得解决,由于发生TIME_WAIT的状况是服务器本身可控的,要么就是对方链接的异常,要么就是本身没有迅速回收资源,总之不是因为本身程序错误致使的。
但 是CLOSE_WAIT就不同了,从上面的图能够看出来,若是一直保持在CLOSE_WAIT状态,那么只有一种状况,就是在对方关闭链接以后服务器程 序本身没有进一步发出ack信号。换句话说,就是在对方链接关闭以后,程序里没有检测到,或者程序压根就忘记了这个时候须要关闭链接,因而这个资源就一直 被程序占着。我的以为这种状况,经过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
 
若是你使用的是HttpClient而且你遇到了大量CLOSE_WAIT的状况,那么这篇日志也许对你有用: http://blog.csdn.net/shootyou/article/details/6615051
在那边日志里头我举了个场景,来讲明CLOSE_WAIT和TIME_WAIT的区别,这里从新描述一下:
服 务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常状况下,若是请求成功,那么在抓取完 资源后,服务器A会主动发出关闭链接的请求,这个时候就是主动关闭链接,服务器A的链接状态咱们能够看到是TIME_WAIT。若是一旦发生异常呢?假设 请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭链接的请求,服务器A就是被动的关闭了链接,若是服务器A被动关闭链接以后程序员忘了 让HttpClient释放链接,那就会形成CLOSE_WAIT的状态了。
 
因此若是将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。由于问题出在服务器程序里头啊。

五、RST出现缘由

TCP异常终止的常见情形

咱们在实际的工做环境中,致使某一方发送reset报文的情形主要有如下几种:

1,客户端尝试与服务器未对外提供服务的端口创建TCP链接,服务器将会直接向客户端发送reset报文。

 

 

2,客户端和服务器的某一方在交互的过程当中发生异常(如程序崩溃等),该方系统将向对端发送TCP reset报文,告之对方释放相关的TCP链接,以下图所示:

 

 

3,接收端收到TCP报文,可是发现该TCP的报文,并不在其已创建的TCP链接列表内(好比server机器直接宕机),则其直接向对端发送reset报文,以下图所示:

 

TCP_NODelay

做者:Pengcheng Zeng
连接:https://www.zhihu.com/question/42308970/answer/123620051
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

参考 tcp(7): TCP protocol
TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
TCP/IP协议中针对TCP默认开启了 Nagle算法。Nagle算法经过减小须要传输的数据包,来优化网络。关于Nagle算法,@ 郭无意 同窗的答案已经说了很多了。在内核实现中,数据包的发送和接受会先作缓存,分别对应于写缓存和读缓存。
那么针对题主的问题,咱们来分析一下。
启动TCP_NODELAY,就意味着禁用了Nagle算法,容许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。好比,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来讲,绝对不是在一个量级上的,因此数据传输很是少;而又要求用户的输入可以及时得到返回,有较低的延时。若是开启了Nagle算法,就极可能出现频繁的延时,致使用户体验极差。固然,你也能够选择在应用层进行buffer,好比使用java中的buffered stream,尽量地将大包写入到内核的写缓存进行发送;vectored I/O(writev接口)也是个不错的选择。
对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到必定量以后,才会被发送出去,这样明显提升了网络利用率(实际传输数据payload与协议头的比例大大提升)。可是这由不可避免地增长了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。固然这个问题只有在连续进行两次写操做的时候,才会暴露出来。
咱们看一下摘自Wikipedia的Nagle算法的伪码实现:
if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if
经过这段伪码,很容易发现连续两次写操做出现问题的缘由。而对于读-写-读-写这种模式下的操做,关闭TCP_NODELAY并不会有太大问题。
The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

连续进行屡次对小数据包的写操做,而后进行读操做,自己就不是一个好的网络编程模式;在应用层就应该进行优化。
对于既要求低延时,又有大量小数据传输,还同时想提升网络利用率的应用,大概只能用UDP本身在应用层来实现可靠性保证了。好像企鹅家就是这么干的。

 

算法部分

一、数组中两个数A,B之和等于第三个数C,求最大的C

二、两个有序数组求中位数

从程序员面试角度来讲,经典的问题包括如下内容:

算法部分

二分搜索 Binary Search 
分治 Divide Conquer 
宽度优先搜索 Breadth First Search 
深度优先搜索 Depth First Search
回溯法 Backtracking 
双指针 Two Pointers 
动态规划 Dynamic Programming 
扫描线 Scan-line algorithm
快排 Quick Sort

数据结构部分

栈 Stack
队列 Queue
链表 Linked List 
数组 Array 
哈希表 Hash Table
二叉树 Binary Tree  
堆 Heap
并查集 Union Find
字典树 Trie

根据2017年校招的状况,我整理了2017校招的常考算法类型,以及对应的典型题目。

另附参考答案地址:LINTCODE / LEETCODE 参考答案查询

数学

尾部的零
斐波纳契数列
x的平方根
x的平方根 2
大整数乘法
骰子求和
最多有多少个点在一条直线上
超级丑数

比特位操做

将整数A转换为B更新二进制位
二进制表示
O(1)时间检测2的幂次
二进制中有多少个1

动态规划

编辑距离正则表达式匹配
交叉字符串
乘积最大子序列
二叉树中的最大路径和
不一样的路径
通配符匹配

滑动窗口的中位数数据流中位数
最高频的K个单词
接雨水
堆化
排序矩阵中的从小到大第k个数

二叉树

二叉树中序遍历二叉树的序列化和反序列化
子树
最近公共祖先
二叉树的层次遍历
将二叉树拆成链表
在二叉查找树中插入节点

二分法

经典二分查找问题二分查找
两数组的交
区间最小数
寻找旋转排序数组中的最小值
搜索排序区间
寻找峰值

分治法

快速幂两个排序数组的中位数
合并K个排序链表

哈希表

变形词子串哈希函数
短网址
复制带随机指针的链表
最小子串覆盖

矩阵

搜索二维矩阵旋转图像
岛屿的个数
螺旋矩阵

宽度优先搜索

克隆图被围绕的区域
拓扑排序
单词接龙

链表

实现一个链表的反转链表求和 II
删除链表中的元素
LRU缓存策略
合并两个排序链表
两个链表的交叉
翻转链表 II
复制带随机指针的链表
带环链表

枚举法

统计数字名人确认
最长连续上升子序列
最大子数组差
最长公共前缀

排序

快排摆动排序
最大间距
最接近零的子数组和
最大数
四数之和
数组划分
第K大元素
排颜色

深度优先搜索

N皇后问题图是不是树
带重复元素的排列
分割回文串

数组

数组划分逆序对
合并区间
搜索旋转排序数组
最大子数组
删除排序数组中的重复数字
第二大的数组
先递增后递减数组中的最大值
两数和 - 输入的数据是有序的
两个排序数组的中位数
在大数组中查找
颜色分类
合并排序数组
无序数组K小元素
中位数
奇偶分割数组

贪心

主元素寻找缺失的数
买卖股票最佳时机
加油站
删除数字
落单的数
最大子数组差

线段树

线段树查询线段树的构造
线段树的修改
区间求和
统计比给定整数小的数的个数

带最小值操做的栈用栈实现队列
有效的括号序列
简化路径

整数

反转整数将整数A转换为B
整数排序

字符串处理

罗马数字转整数回文数
乱序字符串
有效回文串
翻转字符串
最长无重复字符的子串
字符串压缩
比较字符串编辑距离II

欢迎关注个人微信公众号:九章算法(ninechapter),帮助你了解IT技术前沿,经过面试、拿到offer、找到好工做!

操做系统

一、进程、线程

进程概念 
  进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行本身的程序,系统就建立一个进程,并为它分配资源,包括各类表格、内存空间、磁盘空间、I/O设备等。而后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。因此,进程是系统中的并发执行的单位。 
  在Mac、Windows NT等采用微内核结构的操做系统中,进程的功能发生了变化:它只是资源分配的单位,而再也不是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。所以,实现并发功能的单位是线程。
线程概念 
  线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。若是把进程理解为在逻辑上操做系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操做系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程当中,用户又能够输人数据库查询请求,这又是一个子任务。这样,操做系统则把每个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程能够在处理器上独立调度执行,这样,在多处理器环境下就容许几个线程各自在单独处理器上进行。操做系统提供线程就是为了方便而有效地实现这种并发性 
引入线程的好处 
(1)易于调度。 
(2)提升并发性。经过线程可方便有效地实现并发性。进程可建立多个线程来执行同一程序的不一样部分。 
(3)开销少。建立线程比建立进程要快,所需开销不多。。 
(4)利于充分发挥多处理器的功能。经过建立多线程进程(即一个进程可具备两个或更多个线程),每一个线程在一个处理器上运行,从而实现应用程序的并发性,使每一个处理器都获得充分运行。 
进程和线程的关系 
(1)一个线程只能属于一个进程,而一个进程能够有多个线程,但至少有一个线程。 
(2)资源分配给进程,同一进程的全部线程共享该进程的全部资源。 
(3)处理机分给线程,即真正在处理机上运行的是线程。 
(4)线程在执行过程当中,须要协做同步。不一样进程的线程间要利用消息通讯的办法实现同步。

二、进程间通讯的方式?

    (1)管道(pipe)及有名管道(named pipe):管道可用于具备亲缘关系的父子进程间的通讯,有名管道除了具备管道所具备的功能外,它还容许无亲缘关系进程间的通讯。

    (2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通讯方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上能够说是一致的。

    (3)消息队列(message queue):消息队列是消息的连接表,它克服了上两种通讯方式中信号量有限的缺点,具备写权限得进程能够按照必定得规则向消息队列中添加新信息;对消息队列有读权限得进程则能够从消息队列中读取信息。

    (4)共享内存(shared memory):能够说这是最有用的进程间通讯方式。它使得多个进程能够访问同一块内存空间,不一样进程能够及时看到对方进程中对共享内存中数据得更新。这种方式须要依靠某种同步操做,如互斥锁和信号量等。

    (5)信号量(semaphore):主要做为进程之间及同一种进程的不一样线程之间得同步和互斥手段。

    (6)套接字(socket):这是一种更为通常得进程间通讯机制,它可用于网络中不一样机器之间的进程间通讯,应用很是普遍。

三、线程同步

多线程的同步

      有了上面的基本函数还不足以完成本题的要求,为何呢?由于题目要求按照ABCABC...的方式打印,而3个线程却在抢占资源,因此没法控制排列顺序。这时就须要用到多线程编程中的同步技术。

      对于多线程编程来讲,同步就是同一时间只容许一个线程访问资源,而其余线程不能访问。多线程有3种同步方式:

  • 互斥锁
  • 条件变量
  • 读写锁

1.互斥锁

      互斥锁是最基本的同步方式,它用来保护一个“临界区”,保证任什么时候刻只由一个线程在执行其中的代码。这个“临界区”一般是线程的共享数据

      下面三个函数给一个互斥锁上锁和解锁:

int  pthread_mutex_lock(pthread_mutex_t *mptr);
 
int  pthread_mutex_trylock(pthread_mutex_t *mptr);
 
int  pthread_mutex_unlock(pthread_mutex_t *mptr);

  假设线程2要给已经被线程1锁住的互斥锁(mutex)上锁(即执行pthread_mutex_lock(mutex)),那么它将一直阻塞直到到线程1解锁为止(即释放mutex)。

      若是互斥锁变量时静态分配的,一般初始化为常值PTHREAD_MUTEX_INITIALIZER,若是互斥锁是动态分配的,那么在运行时调用pthread_mutex_init函数来初始化。

2.条件变量

      互斥锁用于上锁,而条件变量则用于等待,一般它都会跟互斥锁一块儿使用。

int  pthread_cond_wait(pthread_cond_t *cptr,pthread_mutex_t *mptr);
int  pthread_cond_signal(pthread_cond_t *cptr);

  一般pthread_cond_signal只唤醒等待在相应条件变量上的一个线程,如有多个线程须要被唤醒呢,这就要使用下面的函数了:

int  pthread_cond_broadcast(pthread_cond_t *cptr);

3.读写锁

      互斥锁将试图进入连你姐去的其余简称阻塞住,而读写锁是将读和写做了区分,读写锁的分配规则以下:

      (1)只要没有线程持有某个给定的读写锁用于写,那么任意数目的线程能够持有该读写锁用于读;

      (2)仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配该读写锁用于写。

int  pthread_rwlock_rdlock(pthread_relock_t *rwptr);
int  pthread_rwlock_wrlock(pthread_relock_t *rwptr);
int  pthread_rwlock_unlock(pthread_relock_t *rwptr);
 
 

pthread_cond_wait 为何须要传递 mutex 参数

做者:吴志强
连接:https://www.zhihu.com/question/24116967/answer/26747608
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

首先须要明白两点:
  • wait()操做一般伴随着条件检测,如:
    while(pass == 0) pthread_cond_wait(...); 
  • signal*()函数一般伴随着条件改变,如:
    pass = 1; pthread_cond_signal(...) 
因为此两处都涉及到变量pass,因此为了防止Race Condition,必须得加锁。因此代码会变成下面这样:
// 条件测试 pthread_mutex_lock(mtx); while(pass == 0) pthread_cond_wait(...); pthread_mutex_unlock(mtx); // 条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(...); 

而后,咱们假设wait()操做不会自动释放、获取锁,那么代码会变成这样:
// 条件测试 pthread_mutex_lock(mtx); while(pass == 0) { pthread_mutex_unlock(mtx); pthread_cond_just_wait(cv); pthread_mutex_lock(mtx); } pthread_mutex_unlock(mtx); // 条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(cv); 

长此以往,程序员发现unlock, just_wait, lock这三个操做始终得在一块儿。因而就提供了一个pthread_cond_wait()函数来同时完成这三个函数。

另一个证据是,signal()函数是不须要传递mutex参数的,因此关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。

因此个人结论是:传递的mutex并非为了防止wait()函数内部的Race Condition!而是由于调用wait()以前你老是得到了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),而且这个mutex在你调用wait()以前必须得释放掉,调用wait()以后必须得从新获取。


因此,pthread_cond_wait()函数不是一个细粒度的函数,倒是一个实用的函数。

 

生产者、消费者模型

Producer:

While(TRUE)

Mutex_Lock(mutex_p)

if (item_size < FULL)

  PREDOCE

Mutex_UnLock(mutex_p)

 

Mutex_Lock(mutex_c)

if (item_size == 0)

      Cond_Signal(cond)

item_size++;

Mutex_UnLock(mutex_c)

 

Consumer:

Mutex_Lock(mutex_c)

while (item_size == 0)

  Cond_Wait(mutex_c, cond)

 item_size--;

Mutex_UnLock(mutex_c)

相关文章
相关标签/搜索