目錄

互聯網公司大廠 – 面試八股文彙整

目錄

前言

因為 2021/03 月份我已經是大三了,我開始在春招找實習 Offer,在找實習的期間我在牛課網以及各大平台的面經驗分享中整理出一份八文面試題,整理的過程整整花了我將近一週的時間,希望我整理的內容對各位有幫助。

註:這篇文章是針對 C/C++ 崗位的面試題,數據結構、算法、操作系統、計算機網路、數據庫對大部分崗位是通用的。

重要度

重要性: 語言基礎 >= 數據結構 == 算法 > 操作系統 > 計算機網路 > 數據庫 >= 機器學習

語言基礎

C++ 三大特性

封装性是基础,继承性是关键,多态性是补充,并且多态性存在于继承的环境中。 C++语言中支持数据封装,类是支持数据封装的工具,对象是数据封装的实现。 在封装中,还提供一种对数据访问的控制机制,使得一些数据被隐藏在封装体内,因此具有隐藏性。

封裝、繼承、多態,簡單說明

封装性:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person
{ 
private://数据私有
   string bame;
   int num;
public://方法公有 
    void getName()
    {
        return name;
    }
};

繼承:继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。 继承的作用:避免公用代码的重复开发,减少代码和数据冗余

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
 
using namespace std;
class Base
{
public:
    void printBase(void)
    {
        cout<<"Base中的printBase"<<endl;
    }
};
class Son:public Base
{
 
};
int main(int argc, char *argv[])
{
    Son ob;
    ob.printBase();
    return 0;
}

多態:多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。比如函数重载、运算符重载、虚函数等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
 
using namespace std;
class Base
{
public:
    virtual void printMsg(void)
    {
        cout<<"Base中的printMsg"<<endl;
    }
};
class Son:public Base
{
public:
    virtual void printMsg(void)
    {
        cout<<"Son中的printMsg"<<endl;
    }
};
int main(int argc, char *argv[])
{
    Base *p = new Son;
    p->printMsg();
    return 0;
}

引用和指针的区别

指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。 而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

智能指针有哪些

C++ 标准模板库STL(Standard Template Library) 一共给我们提供了四种智能指针auto_ptr、unique_ptr、shared_ptr 和weak_ptr,其中auto_ptr 是C++98 提出的,C++11 已将其摒弃,并提出了unique_ptr 替代auto_ptr。

智能指针的实现原理

智能指针将一个计数器与类指向的对象相关联引用计数跟踪共有多少个类对象共享同一指针,每次创建类的新对象时,初始化指针并将引用计数置为1. 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数。

使用智能指针可能发生内存泄漏吗,如何解决

會。

这里在 C++11 前有的是 auto_ptr,它是怎么做的呢? 利用auto_ptr声明一个智能指针p1并且指向一个内存地址,再次调用生成一个智能指针p2,无指向。p2=p1,此时不会报错,但是我们调用p1是不对的,因为他已经赋值给p2了,也就是说他成了个野指针,这样就产生了内存泄漏的可能。 因此在c++11中引入unique_ptr的概念,重复上述操作,但是他要求一段内存地址同一时间仅能有一个智能指针指向,因此p2=p1就会报错,这样就可以防止p1赋值给p2成为野指针带来的潜在危险,即内存泄漏。

new 和 malloc 的区别

屬性: new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

參數: 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸

返回类型: new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

分配失败: new内存分配失败时,会抛出bac_alloc异常malloc分配内存失败时返回NULL

自定义类型: new会先调用operator new函数申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

重载: C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载

内存区域: new操作符从自由存储区(free store)上为对象动态分配内存空间而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

静态成员函数和普通函数的区别

类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

静态函数只有当程序结束的时候才从内存消失。而非静态则是动态加载到内存,不需要的时候就从内存消失。 据个例子,调用类中的静态函数,你不需要创建对象就可以调用。而对于非静态的函数,你必须要先创建对象,才能够由对象调用。

静态成员函数和普通的静态函数的区别,可以通过对象实例访问吗

c++静态成员函数与静态数据成员

皆可以透過對象實例訪問

類名:可以調用靜態成員函數,不能調用非靜態成員函數 對象實例:可以調用靜態成員函數,可以調用非靜態成員函數

C++ 的内存管理

這個問題有難度,可以說很難,…看以下知乎回答

https://zhuanlan.zhihu.com/p/51855842

什么是多态,实现多态的机制

虚函数是C++中用于实现多态(polymorphism)的机制。 核心理念就是通过基类访问派生类定义的函数。 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。 C++支持两种多态性:编译时多态性,运行时多态性。

虚函数表存放的内容

https://www.jianshu.com/p/64f3b9c22898

虚函数的地址被存储一张叫做虚表的东西里,我们其实很容易拿到这个虚表。

什么是纯虚函数

纯虚函数,在虚函数后加“=0”,如 virtual void func()=0

當類裡面有純虛函數,則該類無法被實例化,是作為一個抽象的概念。

构造函数可以是虚函数吗,为什么

构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 构造函数不能是虚成员函数,但析构函数可以是虚成员函数。

析构函数可以是虚函数吗,为什么

析构函数可以为虚函数,主要是基类指针指向子类对象的情况下,在基类销毁时,只调用基类的析构函数而不调用子类的析构函数,从而导致内存泄漏,所以需要虚函数机制来帮助系统' 识别’需要释放资源

struct 和 class 的区别

class 和struct 最本质的区别: class 是引用类型,它在堆中分配空间,栈中保存的只是引用;而struct 是值类型,它在栈中分配空间。 什么是class? class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,通常包含字段、属性、方法、构造函数、索引器、操作符等。

1.默认的继承访问权。class默认的是private,strcut默认的是public。 2.默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。 3.“class”这个关键字还用于定义模板参数,就像“typename”。但关建字**“struct”不用于定义模板参数** 4.class和struct在使用大括号{ }上的区别 关于使用大括号初始化 1.)class和struct如果定义了构造函数的话,都不能用大括号进行初始化 2.)如果没有定义构造函数,struct可以用大括号初始化。 3.)如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化

C 和 C++ struct 的区别

在C中struct只单纯的用作数据的复合类型,也就是说,在结构体声明中只能将数据成员放在里面,而不能将函数放在里面

1
2
3
4
5
6
struct Base{            //public
	char v1;
	int v2;
	double v3;
	void show();    //error!
};

并且在C结构体中所有成员**默认均是公有类型(public)**也就是说在结构体外部可以直接通过结构体变量对成员进行访问,例如下面C代码:

1
2
3
4
struct Base s1;		//在C中定义结构体变量方式
s1.v1 = 'A';
s1.v2 = 10;
s1.v3 = 95.6;

在C结构体声明中不能使用C++访问修饰符,如:public、protected、private 而在C++中可以使用,例如下面C++代码:

1
2
3
4
5
6
7
8
9
struct BasePlus{       //public
    int v0;
private:
    char v1;
protected:
    float v2;
public:
    double v3;
};

C的结构体不能继承(没有这一概念)而C++的结构体可以继承并且还可以定义成模版。

抽象类有什么作用

抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。 抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。

强制类型转换有哪些,区别和使用方法

C++的四种强制类型转换,所以C++不是类型安全的。分别为:static_cast , dynamic_cast , const_cast , reinterpret_cast

static_cast: 可以实现C++中内置基本数据类型之间的相互转换;如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。

1
int c=static_cast<int>(7.987); //基本数据类型之间相互转换
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class A //类类型之间转换
{}; 
class B:public A 
{}; 
class C 
{}; 
     
int main() 
{ 
    A* a=new A; 
    B* b; 
    C* c; 
    b=static_cast<B>(a);  // 编译不会报错, B类继承A类 
    c=static_cast<B>(a);  // 编译报错, C类与A类没有任何关系 
    return 1; 
}

const_cast: const_cast操作不能在不同的种类间转换。相反,它仅仅把一个它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉

reinterpret_cast: ** 有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型,也可以转化任何指针类型为其他的类型**。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用。

dynamic_cast: ** (1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。 (2)不能用于内置的基本数据类型的强制转换。 (3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL**。 (4)使用dynamic_cast进行转换的,**基类中一定要有虚函数,否则编译不通过**。 B中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。 这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表。 (5)在类的转换时,在类层次间进行上行转换时,**dynamic_cast和static_cast的效果是一样的**。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。向上转换即为指向子类对象的向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。 参考例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<iostream> 
#include<cstring> 
using namespace std; 
class A 
{ 
   public: 
   virtual void f() 
   { 
       cout<<"hello"<<endl; 
   }; 
}; 
 
class B:public A 
{ 
    public: 
    void f() 
    { 
        cout<<"hello2"<<endl; 
        }; 
 
}; 
 
class C 
{ 
  void pp() 
  { 
      return; 
  } 
}; 
 
int fun() 
{ 
    return 1; 
} 
int main() 
{ 
    A* a1=new B;//a1是A类型的指针指向一个B类型的对象 
    A* a2=new A;//a2是A类型的指针指向一个A类型的对象 
    B* b; 
    C* c; 
    b=dynamic_cast<B*>(a1);//结果为not null,向下转换成功,a1之前指向的就是B类型的对象,所以可以转换成B类型的指针。 
    if(b==NULL) 
    { 
        cout<<"null"<<endl; 
    } 
    else
    { 
        cout<<"not null"<<endl; 
    } 
    b=dynamic_cast<B*>(a2);//结果为null,向下转换失败 
    if(b==NULL) 
    { 
        cout<<"null"<<endl; 
    } 
    else
    { 
        cout<<"not null"<<endl; 
    } 
    c=dynamic_cast<C*>(a);//结果为null,向下转换失败 
    if(c==NULL) 
    { 
        cout<<"null"<<endl; 
    } 
    else
    { 
        cout<<"not null"<<endl; 
    } 
    delete(a); 
    return 0; 
}

virtual关键字有哪些用法

1.可以表明該函數方法是虛函數,也可以不寫,也可以將析構函數定義為虛函數。

2.纯虚函数表明,加前綴

3.虚拟继承(virtual public)

在多继承下,虚继承就是为了解决菱形继承中,B,C都继承了A,D继承了B,C,那么D关于 A的引用只有一次,而不是 普通继承的 对于A引用了两次……

格式:可以采用public、protected、private三种不同的继承关键字进行修饰,只要确保包含virtual就可以了。

1
2
3
4
5
6
7
class A
{
  void f1(){};
};
class B : public virtual  A{
 void f2(){};
};

虚继承:在继承定义中包含了virtual关键字的继承关系;

虚基类:在虚继承体系中的通过virtual继承而来的基类,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include 
using namespace std;
class Person{
   public:    Person(){ cout<<"Person构造"<<ENDL; }
           ~Person(){ cout<<"Person析构"<<ENDL; }
};
class Teacher : virtual public Person{
   public:    Teacher(){ cout<<"Teacher构造"<<ENDL; }
            ~Teacher(){ out<<"Teacher析构"<<ENDL; }
};
class Student : virtual public Person{
  public:      Student(){ cout<<"Student构造"<<ENDL; }
             ~Student(){ cout<<"Student析构"<<ENDL; }
};
class TS : public Teacher,  public Student{
public:            TS(){ cout<<"TS构造"<<ENDL; }
                 ~TS(){ cout<<"TS析构"<<ENDL; }
};
int main(int argc,char* argv[])
{
  TS ts;
  return 0;
}

這問題感覺有點玄,看下面的文章

https://zhuanlan.zhihu.com/p/147601339

继承时的访问权限控制符有啥区别

public、protected、private

訪問權限 public protected private
对本类 可见 可见 可见
对子类 可见 可见 不可见
对外部(调用方) 可见 不可见 不可见

什么是右值引用

右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的方式使用 C++ 右值。

左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:

1
2
int a = 5;
5 = a; //错误,5 不能为左值

其中,变量 a 就是一个左值,而字面量 5 就是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:

1
2
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用

std::move 的使用

std::move函数可以以非常简单的方式将左值引用转换为右值引用

用法: 原lvalue值被moved from之后值被转移,所以为空字符串.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
}

輸出:

1
2
3
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"

const 关键字的使用

const修饰符可以把对象转变成常数对象,意思就是说利用const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用!任何修改该变量的尝试都会导致编译错误

這題感覺簡單 但又覺得有點陷阱,看下面長文章

https://www.cnblogs.com/jiabei521/p/3335676.html

sort()底层是怎么实现的

数据量大时采用快速排序 Quick Sort,分段递归排序。一旦分段后的数据量小于某个阈值,为避免Quick Sort的递归调用带来过大的额外开销,就改用插入排序 Insertion Sort。如果递归层次过深,还会改用堆排序 Heap Sort

java,python,C++的区别?

python:适合小工具小程序快速开发,无论是网站还是小游戏都非常方便。但python的脚本的运行效率较低,不适合对运行效率要求较高的程序;

JAVA:采用严格的面向对象编程方法,同时有很多大型的开发框架,比较适合企业级应用;

C++:C++是多范式编程语言。它不仅支持传统的面向过程编程,也支持面向对象编程,而且引入范形编程,C++运行效率较高,同时能够比较容易地建立大型软件,适合对效率要求高的软件,比如机器学习中的神经网络,大型游戏内核编程等等。

从语言特性来说:

Python是一种脚本语言,是解释执行的,不需要经过编译,所以很方便快捷,且能够很好地跨平台,写一些小工具小程序特别合适。 而C++则是一种需要编译后运行语言,在特定的机器上编译后在特定的机上运行,运行效率高,安全稳定。但编译后的程序一般是不跨平台的

java既可以是解释执行也可以是编译执行

从垃圾回收机制:

C++需要程序员收到回收,而JAVA和Python都有自己的垃圾回收机制GC。具体两者又有不同,Python的垃圾收集机制主要使用的引用计数方式

inline 的作用

在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。

栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。

在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭

C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方

对内联函数进行任何修改都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数

如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符

在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
 
using namespace std;

inline int Max(int x, int y)
{
   return (x > y)? x : y;
}

// 程序的主函数
int main( )
{

   cout << "Max (20,10): " << Max(20,10) << endl;
   cout << "Max (0,200): " << Max(0,200) << endl;
   cout << "Max (100,1010): " << Max(100,1010) << endl;
   return 0;
}

輸出

1
2
3
Max (20,10): 20
Max (0,200): 200
Max (100,1010): 1010

慎用 inline

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下情况不宜使用内联: (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。 (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

> 下面為後續追加延伸

数组与指针的区别

数组和指针本质上都代表一块内存数组比较”直接“,数组名即代表这块内存的地址,而指针比较”含蓄“,其本身不代表任何有意义的内容,只有给它赋值后,它才真正的表示一块有意义的内存地址。 这就引出了指针和数组的一个区别:定义的时机不同;数组在编译时就已经被确定下来,而指针直到运行时才能被真正的确定到底指向何方

-1在内存中的表示

這題太難了,連 Google 都沒有答案…被問到只能回答不知道

問群裡大老是說 無法判斷 程序員想怎麼定義就怎麼定義

如何判断float类型变量是否等于0

看到一篇好文章:https://www.cnblogs.com/kubixuesheng/p/4107309.html

fabs () 是 浮點數取絕對值,abs() 是取整數絕對值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
double dd = sin(3.141592653589793 / 6);
/*if (dd == 0.5)
{取决于不同的编译器或者机器平台……这样写,即使有时候是对的,但是就怕习惯,很容易出错。
}*/

if (fabs(dd - 0.5) < DBL_EPSILON)
{
    //满足这个条件,我们就认为dd和0.5相等,否则不等
    puts("ok");//打印了ok
}

虚函数的实现细节

這裡大概率延伸考 vTable,接著考 vTable 存儲項,難度有點高

对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。因此有必要知道虚函数在内存中的分布。

https://jacktang816.github.io/post/virtualfunction/

智能指针有哪些,auto_ptr讲一下

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用

auto_ptr是C++标准库中(utility)为了解决资源泄漏的问题提供的一个智能指针类模板(注意:这只是一种简单的智能指针) auto_ptr的实现原理其实就是RAII(Resource Application Immediately Initialize),在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。 使用auto_ptr作为成员变量,以避免资源泄漏。

1、auto_ptr存储的指针应该为NULL或者指向动态分配的内存块。

2、auto_ptr存储的指针应该指向单一物件(是new出来的,而不是new[]出来的)。

3、两个auto_ptr对象不会同时指向同一块内存块。要明白2个auto_ptr对象赋值会发生什么。

4、千万不要把auto_ptr对象放在容器中。

5、当将auto_ptr作为函数参数时,最好声明为const auto_ptr&(by const ref).当函数返回值可以简单的传值(by value).

auto_ptr 被 unique_ptr 取代原因:

auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。 例如参考《Effective STL》第8条,sort的快排实现中有将元素复制到某个局部临时对象中,但对于auto_ptr,却将原元素置为null,这就导致最后的排序结果中可能有大量的null。而现在C++11的对move语义的支持,使得这样的资源转移通常只会在必要的场合发生,例如转移一个临时变量(右值)给某个named variable(左值),或者一个函数的返回(右值)这也就是用unique_ptr代替auto_ptr的原因,本质上来说,就是unique_ptr禁用了copy,而用move替代。之所以说通常,是因为,也可以用std:move来实现左值move给左值。

shared_ptr的原理?为什么引用智能指针?

(1)

刚才说到,当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete。这是怎么实现的呢?答案是:引用计数(reference counting)。引用计数指的是,所有管理同一个裸指针(raw pointer)的shared_ptr,都共享一个引用计数器,每当一个shared_ptr被赋值(或拷贝构造)给其它shared_ptr时,这个共享的引用计数器就加1,当一个shared_ptr析构或者被用于管理其它裸指针时,这个引用计数器就减1,如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源

在底层实现中,这个引用计数器保存在某个内部类型里(这个类型中还包含了deleter,它控制了指针的释放策略,默认情况下就是普通的delete操作),而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中。shared_ptr重载了赋值运算符,在赋值和拷贝构造另一个shared_ptr时,这个指针被另一个shared_ptr共享。在引用计数归零时,这个内部类型指针与shared_ptr管理的资源一起被释放。此外,为了保证线程安全性,引用计数器的加1,减1操作都是原子操作,它保证shared_ptr由多个线程共享时不会爆掉

簡要回答版本

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

1.shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。 2.在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。 3.如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 4.如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

(2)

我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源

用shared_ptr会造成什么问题?怎么解决?

(1) std::shared_ptr的线程安全问题

1.智能指针对象中引用计数是多个智能指针对象共享的两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。 2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题

std::shared_ptr的循环引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include<memory>
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	system("pause");
	return 0;
}

輸出

1
2
3
4
1
1
2
2

循环引用分析:

1.node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。 2.node1的_next指向node2,node2的_prev指向node1,引用计数变成2。 3.node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。 4.也就是说_next析构了,node2就释放了。 5.也就是说_prev析构了,node1就释放了。 6.但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 仿函数的删除器
template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	FreeFunc<int> freeFunc;
	shared_ptr<int> sp1((int*)malloc(4), freeFunc);
	DeleteArrayFunc<int> deleteArrayFunc;
	shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
	return 0;
}

怎样用会造成循环引用?简单说一下

shared_ptr的一个最大的缺点,或者说,引用计数策略最大的缺点,就是循环引用(cyclic reference),下面是一个典型的事故现场:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Observer; // 前向声明
class Subject {
private:
    std::vector<shared_ptr<Observer>> observers;
public:
    Subject() {}
    addObserver(shared_ptr<Observer> ob) {
        observers.push_back(ob);
    }
    // 其它代码
    ..........
};
class Observer {
private:
    shared_ptr<Subject> object;
public:
    Observer(shared_ptr<Object> obj) : object(obj) {}
    // 其它代码
    ...........
};

目标(Subject)类连接着多个观察者(Observer)类,当某个事件发生时,目标类可以遍历观察者数组observers,对每个观察者进行"通知",而观察者类中,也保存着目标类的shared_ptr,这样多个观察者之间可以以目标类为桥梁进行沟通,除了会发生内存泄漏以外,这是很不错的设计模式嘛!等等,不是说用了shared_ptr管理资源后就不会内存泄漏了吗?怎么又漏了?

这就是引用计数模型失效的唯一的情况:循环引用。循环引用指的是,一个引用通过一系列的引用链,竟然引用回自身,上面的例子中,Subject->Observer->Subject就是这么一条环形的引用链。假设我们的程序中只有一个变量shared_ptr p,此时,p指向的对象不仅通过该shared_ptr引用自己,还通过它包含的Observer中的object成员变量引用回自己,于是它的引用计数是2,每个Observer的引用计数都是1。当p析构时,它的引用计数减1,变成2-1=1(大于0!),p指向对象的析构函数将不会被调用,于是p和它包含的每个Observer对象在程序结束时依然驻留在内存中没被delete,形成内存泄漏

weak_ptr解決循環引用:

为了解决这一问题,标准库提供了std::weak_ptr(弱引用),它也位于中。

weak_ptr是shared_ptr的"观察者",它与一个shared_ptr绑定,但却不参与引用计数的计算,在需要时,它还能摇身一变,生成一个与它所"观察"的shared_ptr共享引用计数器的新shared_ptr。总而言之,weak_ptr的作用就是:在需要时变出一个shared_ptr,在其它时候不干扰shared_ptr的引用计数

在上面的例子中,我们只需简单地将Observer中object成员的类型换成std::weak_ptr即可解决内存泄漏的问题,此刻(接着上面的例子),p指向对象的引用计数为1,所以在p析构时,Subject指针将被delete,其中包含的observers数组在析构时,内部的Observer对象的引用计数也将变为0,故它们也被delete了,资源释放得干干净净。

下面,是weak_ptr的使用方法:

1
2
3
4
5
6
7
std::shared_ptr<int> sh = std::make_shared<int>();
// 用一个shared_ptr初始化
std::weak_ptr<int> w(sh);
// 变出shared_ptr
std::shared_ptr<int> another = w.lock();
// 判断weak_ptr所观察的shared_ptr的资源是否已经释放
bool isDeleted = w.expired();

如何判断机器是大端存储还是小端存储,如何将大端转换成小端?

(1)

第一次看這題感覺有點迷 看這篇文章 https://blog.csdn.net/lwfcgz/article/details/50476051

计算机有little endian(小端)和big endian(大端)之分,两张从 维基百科盗来的图就可以说明它们的区别:

对于32位的整数,大端机器会在内存的低地址存储高位,在高地址存储低位

小端机器恰好相反,内存的低地址存储低位,在高地址存储高位。

大端表示法和人的直观比较相符,从低地址向高地址看过去,就是原先的数;小端表示法更便于计算机的操作,地址增加和个十百千万的增加是一致的。

如何判断自己的计算机是little endian还是big endian呢?intel的机器基本全是little endian,也可以运行简单的代码判断。

方法一

1
python -c "import sys; print(sys.byteorder)"

终端运行上述代码,我的本本上输出little就表示是小端机器。

方法二

写一个简单的C程序,下面这个是从nginx源码抄来的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
int main() {
    int i = 0x11223344;
    char *p;

    p = (char *) &i;
    if (*p == 0x44) {
        printf("Little endian\n");
    }
    else {
        printf("Big endian\n");
    }
    return 0;
}

(2)

https://blog.csdn.net/szchtx/article/details/42834391

判断大端模式和小端模式

使用联合,通过判断首个成员的值,确定是大端还是小端模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
bool IsBigEndian(){  
    union NUM{
        int a;  
        char b;  
    }num;  
    num.a = 0x1234;  
    if( num.b == 0x12 ){  
        return true;  
    }   
    return false;  
}

大端模式和小端模式转换

对32位的数,即4个字节,大端转换成小端:

方法1:使用移位运算。

1
2
3
4
uint32_t reversebytes_uint32t(uint32_t value){
    return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 | 
        (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24; 
}

上述代码中,将低8位(0~8位)左移24位,变成了高8位(24~32位),8~16位左移8位变成了(16~24位)。将原高8位和高16位右移,变成了新的低8位和低16位。

这种方法效率采用了移位运算,效率很高。而且该方法亦可用于小端模式转成大端模式。

有了32位的转换方法,对64位,即8个字节的转换同理。不过直接写移位运算未免麻烦,可以直接使用上述函数:

1
2
3
4
5
6
7
// 先将64位的低32位转成小端模式,再将64位的高32位转成小端模式
// 在将原来的低32位放置到高32位,原来的高32位放置到低32位
uint64_t reversebytes_uint64t(uint64_t value){
    uint32_t high_uint64 = uint64_t(reversebytes_uint32t(uint32_t(value)));         // 低32位转成小端
    uint64_t low_uint64 = (uint64_t)reversebytes_uint32t(uint32_t(value >> 32));    // 高32位转成小端
    return (high_uint64 << 32) + low_uint64;
}

**方法2:对每个字节依次处理。 **

比如0x12345678,小端模式下可认为是12*(2^32) + 34*(2^16) + 56*(2^8) + 78*(2^0)。在大端模式下,排列顺序发生了变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
uint32_t changeEndian_uint32t(uint32_t value){
    char* ptr = (char*)(&value);
    
    uint64_t base[4];                   // 设置基
    base[0] = 1;
    for(int i = 1; i < 4; ++i){
        base[i] = base[i-1] * 256;
    }
 
    uint32_t res = 0;
    for(int i = 0; i < sizeof(value); ++ i){
        res += uint8_t(ptr[i]) * base[4-i-1];
    }
 
    return res;
}

上述代码中,第一句将输入的uint32_t的变量强制转换成字符类型数组,以便一个字节一个字节的处理。

C++内存分配堆栈在内存中的大小,使用malloc函数,在32位机器上1G的物理内存能获取到的内存大小。

受限於

  1. Lib C库的实现
  2. 操作系统
  3. 硬件

扣去內核的使用,你可以分超過系統剩下的記憶體

Linux 下由 vm.overcommit_memory* 控制

https://en.wikipedia.org/wiki/Virtual_address_space

构造和析构中可以调用虚函数么?调用虚函数的话,执行的是父类还是子类的虚函数呢?

C++面试题1:构造函数和虚构函数中能否调用虚函数? CSDN

1.构造函数跟虚构函数里面都可以调用虚函数,编译器不会报错。 2.C++ primer中说到最好别用 3.由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,虚函数是不会呈现出多态的 4.类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态 5.因此如果在基类中声明的纯虚函数并且在基类的析构函数中调用之,编译器会发生错误

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。

在有继承关系的父子类中,构建和析构一个子类对象时,父子构造函数和析构函数的执行顺序分别是怎样的?

经测试,继承下构造函数与析构函数顺序(包括虚析构函数),结果如下:

普通继承或虚函数继承子类指针指向子类实例

父类构造函数»>子类构造函数 子类析构函数»>父类析构函数

普通继承,父类指针指向子类实例

父类构造函数»>子类构造函数 父类析构函数

虚函数继承,父类指针指向子类实例

父类构造函数»>子类构造函数 子类析构函数»>父类析构函数

由以上结果及测试情况得出以下结论:

  1. 无论如何继承,指针如何指向构造函数都以最终实例化为准,顺序始终是先父类后子类
  2. 析构函数遵从类的多态性非虚析构函数则以指针类型为准虚析构函数则以最终实例为准,存在继承关系时顺序是先子类后父类
  3. 虚析构函数与普通虚函数还是有不同的,普通虚函数仅按最终实例执行一次,而虚析构函数按最终实例执行后仍会依次向上逐个执行其父类析构函数
  4. 可以通过"父类::函数名"来在子类中访问父类的函数,此时不论该函数是否虚函数,都会直接调用父类对应的函数

在有继承关系的类体系中,父类的构造函数和析构函数一定要申明为 virtual 吗?如果不申明为 virtual 会怎样?

C++类有继承时,析构函数必须为虚函数 - CSDN

虚函数与多态一文中讲了虚函数的用法和要点,但少讲了一点,就是虚函数在析构中的用法,本文就是修复一bug的。

C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内在泄漏的问题。

假设我们有这样一种继承关系:

1
2
class BaseClass {};
class SubClass : public BaseClass {};

如果我们以这种方式创建对象:

1
2
SubClass* pObj = new SubClass();
delete pObj;

不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放;

如果我们以这种方式创建对象:

1
2
BaseClass* pObj = new SubClass();
delete pObj;

1.若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放; 2.若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类

什么是虚表?虚表的内存结构布局如何?虚表的第一项(或第二项)是什么?

https://blog.csdn.net/lihao21/article/details/50688337

菱形继承(类D同时继承B和C,B和C又继承自A)体系下,虚表在各个类中的布局如何?如果类B和类C同时有一个成员变了m,m如何在D对象的内存地址上分布的?是否会相互覆盖?

菱形繼承是考虛表的情況下可能會問

如何解决菱形继承问题

两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承。采用虚继承可以解决这个问题。

什么是右值引用?为什么引进右值引用?好处?与左值引用的区别是什么?

(1) 右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的方式使用 C++ 右值。

c++11为什么要引入右值引用 - CSDN (2)(3) c++11之前,有一些让人们蛋疼的地方。

需求1:需要转递引用来提高效率,那么我们函数定义是这样的$void foo(Test &t){…}$。

需求2:现在我们想这样调用函数,foo(Test())。what?编译不过。稍微解释一下,将引用绑定到一个匿名对象,完全没有意义,因为它可能很快就不在了。访问一个不在的对象,是多么恐怖的事情。

我们又想出了其他的办法,重载函数void foo(Test t){…}。what?还编译不过。因为这样重载又二义性,编译器不知道你到底要调用哪个。

最后没辙了,我们只能放大招了,void foo(const Test& t){…}只能这样了。const不是只读的意思吗,怎么还可以这样用。对的const干的活比较多,const Test& t{Test()};这种用法就退化回const Test t{Test()};,这两种写法都会创建一个新的对象,所以const干了一个不属于它的活,这样即保障了传引用的高效又可以传入匿名对象。

需求3:但是我们又需要改变它的值,那好吧,我们只能用const_cast<Test&>(t)强转后来改变他的值。

c++11引入了右值引用来帮助const分担工作。

现在完全可以用void foo(Test&& t)和void foo(Test& t)两个函数来区分传入值是否是将亡值,并且可以重载,无二义性。

注意下面情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;


class Test {
public:
    Test() : x(0) {cout << "构造函数 this = " << this << endl;}
    Test(int x) : x(x) {cout << "构造函数 this = " << this << endl;}
    Test(const Test& another) : x(another.x) {cout << "拷贝构造 this = " << this << " from " << &another << endl;}
    Test(const Test&& another) noexcept : x(another.x) {cout << "移动构造 this = " << this << " from " << &another << endl;}
    ~Test() {cout << "析构函数 this = " << this << endl;}

    int x;
};

ostream& operator<<(ostream& out, const Test& t) {
    out << "&t = " << &t << ", x = " << t.x;
    return out;
}

class Aa {
public:
    Aa(const Test& t) : t(t) { cout << t << endl; cout << this->t << endl;}
    //Aa(const Test&& t) = delete; //这行就可以禁止将亡值来赋值,使编译时报错。
    void foo() {cout << t << endl;}

private:
    const Test &t;
};

int main()
{
    Aa a{Test()};
    a.foo();
    return 0;
}

輸出:

1
2
3
4
5
构造函数 this = 0x61fe0c
&t = 0x61fe0c, x = 0
&t = 0x61fe0c, x = 0
析构函数 this = 0x61fe0c
&t = 0x61fe0c, x = 0

解释

第一步:入参$const Test& t{Test()};$产生了一个临时的匿名只读对象,还是将亡值。

第二步:初始化列表t(t)相当于将刚刚产生的匿名对象,赋值给成员对象&t。

第三步:构造函数结束,匿名对象析构,之后再使用Aa时很危险的

将移动构造函数显视删除,可以避免这一点。

移动构造函数当然不只是这一点功能,它主要是在stl中和std::move配合提高效率。

簡要

右值引用的好处,直接寄存器读取值

要用双&&,传递的实参是计算,这样才是寄存器的值

(4)

左值引用是起别名,如果这个对象已经析构,那么这个别名也应该一起失效。言外之意就是左值引用一定要保证它的生命周期小于等于它被引用的对象

当将亡值出现的时候,左值引用表示无能为力,所以右值引用出现了。

右值引用也可以看作起名,只是它起名的对象是一个将亡值。然后延续这个将亡值的生命,直到这个的右值的生命也结束了。

除了入参时可以用到右值引用外,其他右值引用都显得多余。

比如Test&& t{Test()};它和Test t{Test()}是一样的,甚至可以这样Test&& t{}它们都只一个普通对象,只调用一次构造函数。

總結

1.左值引用只能起别名,但不能给匿名对象起名

2.右值引用其实就是给匿名(天生匿名或者通过std::move将名字失效,这样的对象即将被析构)对象重新起名字。

3.我们一直所说的将亡值其实就是所谓的右值,其它有名字的都是左值,左值引用与左值配合,右值引用与右值配合。

看下面的代码,说一下输出:

1
2
3
4
5
6
std::string str = "Hello";
std::vector<std::string> vec;
 
vec.push_back(std::move(str));
std::cout << "String: " << str << std::endl;
std::cout << "Vector: " << vec[0] << std::endl;

這是右值引入的範例,輸出應該如下:

1
2
String: 
Vecter: "Hello"

说一下push_back和emplace_back的区别

在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)的时候,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题是临时变量申请的资源就浪费。 引入了右值引用,转移构造函数(请看这里)后,push_back()右值时就会调用构造函数和转移构造函数。在这上面有进一步优化的空间就是使用emplace_back,在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <vector>  
#include <string>  
#include <iostream>  
 
struct President  
{  
    std::string name;  
    std::string country;  
    int year;  
    
    //构造函数
    President(std::string p_name, std::string p_country, int p_year)  
        : name(std::move(p_name)), country(std::move(p_country)), year(p_year)  
    {  
        std::cout << "I am being constructed.\n";  
    }
    
    //复制构造函数
    President(const President& other)
        : name(std::move(other.name)), country(std::move(other.country)), year(other.year)
    {
        std::cout << "I am being copy constructed.\n";
    }
 
    //转移构造函数
    President(President&& other)  
        : name(std::move(other.name)), country(std::move(other.country)), year(other.year)  
    {  
        std::cout << "I am being moved.\n";  
    }
 
    //赋值运算符函数  
    President& operator=(const President& other);  
};  
 
int main()  
{  
    std::vector<President> elections;  
    std::cout << "emplace_back:\n";  
    elections.emplace_back("Nelson Mandela", "South Africa", 1994); //没有类的创建  
 
    std::vector<President> reElections;  
    std::cout << "\npush_back:\n";  
    reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));  
 
    std::cout << "\nContents:\n";  
    for (President const& president: elections) {  
       std::cout << president.name << " was elected president of "  
            << president.country << " in " << president.year << ".\n";  
    }  
    for (President const& president: reElections) {  
        std::cout << president.name << " was re-elected president of "  
            << president.country << " in " << president.year << ".\n";  
    }
 
}

輸出:

1
2
3
4
5
6
7
8
9
emplace_back:
I am being constructed.
 
push_back:
I am being constructed.
I am being moved.
 
Contents:
Nelson Mandela was elected president of South Africa in 1994.

計算機網路

OSI 七层模型

https://baike.baidu.com/item/%E4%B8%83%E5%B1%82%E6%A8%A1%E5%9E%8B

https://i1.kknews.cc/SIG=5q4c6g/14190003400pp597q594.jpg

应用层 网络服务与最终用户的一个接口。 协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP

表示层 数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层) 格式有,JPEG、ASCll、EBCDIC、加密格式等

会话层 建立、管理、终止会话。(在五层模型里面已经合并到了应用层) 对应主机进程,指本地主机与远程主机正在进行的会话

传输层 定义传输数据的协议端口号,以及流控和差错校验。 协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层

网络层 进行逻辑地址寻址,实现不同网络之间的路径选择。 协议有:ICMP IGMP IP(IPV4 IPV6)

数据链路层 建立逻辑连接、进行硬件地址寻址、差错校验 等功能。(由底层网络定义协议) 将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。

物理层 建立、维护、断开物理连接。(由底层网络定义协议)

TCP/IP 四層模型

https://i2.kknews.cc/SIG=277r2s4/141s000548s956rs60pp.jpg

1983年1月1日,在網際網路的前身(ARPA網)中,TCP/IP替換舊的網絡控制協議(NCP,Network Control Protocol),從而成為今天的網際網路的基石。最早的TCP/IP由文頓·瑟夫和羅伯特·卡恩兩位開發,慢慢地通過競爭戰勝其他一些網絡協議的方案,比如國際標準化組織ISO的OSI模型。TCP/IP的蓬勃發展發生在1990年代中期。當時一些重要而可靠的工具的出世,例如頁面描述語言HTML和瀏覽器Mosaic,促成了網際網路應用的飛速發展。 隨著網際網路的發展,目前流行的IPv4協議(網際協議版本四)已經接近它的功能上限。IPv4最致命的兩個缺陷在於:

地址只有32位,IP位址空間有限;

不支持服務質量(Quality of Service,QoS)的想法,無法管理帶寬和優先級,故而不能很好的支持現今越來越多實時的語音和視頻應用。因此IPv6(網際協議版本六)浮出水面,用以替換IPv4。

TCP/IP成功的另一個因素在於對為數眾多的低層協議的支持。這些低層協議對應OSI模型中的第一層(物理層)和第二層(數據鏈路層)。每層的所有協議幾乎都有一半數量支持TCP/IP,例如:乙太網(Ethernet)、令牌環(Token Ring)、光纖數據分布接口(FDDI)、端對端協議(PPP)、X.25、幀中繼(Frame Relay)、ATM、Sonet、SDH等。

TCP/IP參考模型是一個抽象的分層模型,所有的TCP/IP系列網絡協議都被歸類到4個抽象的"層"中。每一抽象層創建在低一層提供的服務上,並且為高一層提供服務。 完成一些特定的任務需要眾多的協議協同工作,這些協議分布在參考模型的不同層中的,因此有時稱它們為一個協議棧。

傳輸控制協議(TCP)和網際網路協議(IP)是最先被定義出的。其中IP協議只關心如何使得數據能夠跨越本地網絡邊界的問題,高層邏輯上與用戶更為接近,所處理數據更為抽象,它們依賴於低層將數據轉換成最終能夠進行實體控制的形式。

由於TCP/IP和OSI模型組不能精確地匹配,所以如何將TCP/IP參考模型映射到OSI模型 還沒有一個完全正確的答案。 另外,OSI模型下層還不具備能夠真正占據真正層的位置的能力;在傳輸層和網絡層之間還需要另外一個層(網絡互連層)。

三次握手和四次挥手

TCP連接6種標識

SYN(synchronous):建立連接

ACK(acknowledgement):確認

PSH(push):傳送

FIN(finish):結束

RST(reset):重置

URG(urgent):緊急

Sequence number:順序號碼

數據是被拆成多個數據包來發送,序列號就是對每個數據包進行編號,這樣接受方才能對數據包進行再次拼接。

Acknowledge number:確認號碼,代表下一個數據包編號

原文網址:https://kknews.cc/code/b83bma9.html

三次握手

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手

1.第一次握手(SYN=1, seq=x):

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。 发送完毕后,客户端进入 SYN_SEND 状态。

2.第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

服务器发回确认包(ACK)应答即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放 到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

3.第三次握手(ACK=1,ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束

三次握手簡要理解

三次握手 三次握手的本质是确认通信双方收发数据的能力

首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。

于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。

然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。

四次揮手

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

1.第一次挥手(FIN=1,seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据

发送完毕后,客户端进入 FIN_WAIT_1 状态。

2.第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

3.第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

4.第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态

可以两次握手吗

不行。

三次握手(A three way handshake)是必须的,** 因为 sequence numbers(序列号)没有绑定到整个网络的全局时钟**(全部统一使用一个时钟,就可以确定这个包是不是延迟到的)以及 TCPs 可能有不同的机制来选择 ISN(初始序列号)。**接收方接收到第一个 SYN 时,没有办法知道这个 SYN 是是否延迟了很久了**,除非他有办法记住在这条连接中,最后接收到的那个sequence numbers(然而这不总是可行的)。这句话的意思是:一个 seq 过来了,跟现在记住的 seq 不一样,我怎么知道他是上条延迟的,还是上上条延迟的呢?所以,接收方一定需要跟发送方确认 SYN。

TCP 里 time close 和 time wait状态有什么意义

TIME WAIT

主动关闭方在收到被关闭方的FIN后会处于并长期(2个MSL时间,建议值是2min)的状态,大约是1-4分钟。然后由操作系统回头连接并将TCP连接设为CLOSED初始状态

CLOSE WAIT

被动关闭连接形成的,被动端收到主动端的FIN时候,发送ACK确认,并进入CLOSE_WAIT的状态,如果不指定close()方法,那么就不能从 CLOSE_WAIT 迁移到LAST_ACK,则系统中会有很多的CLOSE_WAIT状态的链接。

FAQ

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

因为建立连接的时候,对方可以把ACK和SYN放在一个报文里发送

2.为什么 TIME_WAIT 状态还需要等 2MSL 后才能返回到 CLOSED 状态?

因为不确定最后发送的 ACK 被对方收到了,所以先进入 TIME_WAIT 状态,作用是用来重发可能丢失的 ACK 报文。

如何排查

可以通过 netstat -n 结合 awk 打印这两个数值

意义

如果这两种状态过多,将会出现较高的负载,导致新的连接无法建立,或者导致socket资源耗尽。可以修改内核 TIME_WAIT 的值一定程度解决。

TCP 和 UDP 的区别

https://zhuanlan.zhihu.com/p/24860273

首先咱们弄清楚,TCP协议和UDP协议与TCP/IP协议的联系,很多人犯糊涂了, 一直都是说TCP协议与UDP协议的区别,我觉得这是没有从本质上弄清楚网络通信!

TCP/IP协议是一个协议簇。里面包括很多协议的,UDP只是其中的一个, 之所以命名为TCP/IP协议,因为TCP、IP协议是两个很重要的协议,就用他两命名了。

TCP/IP协议集包括应用层,传输层,网络层,网络访问层。

TCP/IP 可以看前面問題

UDP(User Data Protocol,用户数据报协议)

1、UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。

2、 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。

3、UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。

4、吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制

5、UDP使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。

6、UDP是面向报文的。发送方的UDP对应用程序交下来的报文, 在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界, 因此,应用程序需要选择合适的报文大小。

我们经常使用**“ping”命令来测试两台主机之间TCP/IP通信是否正常**, 其实“ping”命令的原理就是**向对方主机发送UDP数据包**,然后对方主机确认收到数据包, 如果数据包是否到达的消息及时反馈回来,那么网络就是通的。

小结TCP与UDP的区别:

1、基于连接与无连接;

2、对系统资源的要求(TCP较多,UDP少);

3、UDP程序结构较简单

4、流模式与数据报模式 ;

5、TCP保证数据正确性,UDP可能丢包

6、TCP保证数据顺序,UDP不保证

UDP 的优点,有哪些基于UDP的协议

TCP 的优点:

可靠,稳定。

TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。

TCP 的缺点:

慢,效率低,占用系统资源高,易被攻击。

TCP 在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的 CPU、内存等硬件资源。

而且,因为 TCP 有确认机制、三次握手机制,这些也导致 TCP 容易被人利用,实现 DOS、DDOS、CC 等攻击。

UDP 的优点:

快,比 TCP 稍安全。

UDP 没有 TCP 的握手、确认、窗口、重传、拥塞控制等机制,UDP 是一个无状态的传输协议,所以它在传递数据时非常快。没有 TCP 的这些机制,UDP 较 TCP 被攻击者利用的漏洞就要少一些。但 UDP 也是无法避免攻击的,比如:UDP Flood 攻击。

UDP 的缺点:

不可靠,不稳定。

因为 UDP 没有 TCP 那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。

基于上面的优缺点,那么,TCP 和 UDP 的应用场景都有哪些呢?

TCP 应用场景:

当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如 HTTP、HTTPS、FTP 等传输文件的协议,POP、SMTP 等邮件传输的协议。

在日常生活中,常见使用 TCP 协议的应用如下: 浏览器用的 HTTP, FlashFXP 用的 FTP,Outlook 用的 POP、SMTP,Putty 用的 Telnet、SSH,QQ 文件传输。

UDP 应用场景:

当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用 UDP。

比如,日常生活中,常见使用 UDP 协议的应用如下:QQ 语音,QQ 视频,TFTP 等。有些应用场景对可靠性要求不高会用到 UPD,比如长视频,要求速率。

TCP 与 UDP 区别总结:

1、TCP 面向连接(如打电话要先拨号建立连接); UDP 是无连接的,即发送数据之前不需要建立连接。

2、TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。

3、TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的。UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条 TCP 连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

5、TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节。

6、TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道。

  • TCP 实现可靠的机制

https://zhuanlan.zhihu.com/p/112317245

**保证数据安全的方法: **

TCP主要提供了检验和、序列号/确认应答、超时重传、最大消息长度、滑动窗口控制等方法实现了可靠性传输

检验和

通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。TCP在计算检验和时,会在TCP首部加上一个12字节的伪首部。检验和总共计算3部分:TCP首部、TCP数据、TCP伪首部

序列号/确认应答

这个机制类似于问答的形式。比如在课堂上老师会问你“明白了吗?”,假如你没有隔一段时间没有回应或者你说不明白,那么老师就会重新讲一遍。其实计算机的确认应答机制也是一样的,发送端发送信息给接收端,接收端会回应一个包,这个包就是应答包。

只要发送端有一个包传输,接收端没有回应确认包(ACK包),都会重发。或者接收端的应答包,发送端没有收到也会重发数据。这就可以保证数据的完整性。

超时重传

超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。那么我们该如何确认这个时间值呢?

我们知道,一来一回的时间总是差不多的,都会有一个类似于平均值的概念。比如发送一个包到接收端收到这个包一共是0.5s,然后接收端回发一个确认包给发送端也要0.5s,这样的两个时间就是RTT(往返时间)。然后可能由于网络原因的问题,时间会有偏差,称为抖动(方差)

从上面的介绍来看,超时重传的时间大概是比往返时间+抖动值还要稍大的时间

但是在重发的过程中,假如一个包经过多次的重发也没有收到对端的确认包,那么就会认为接收端异常,强制关闭连接。并且通知应用通信异常强行终止。

最大消息长度

在建立TCP连接的时候,双方约定一个最大的长度(MSS)作为发送的单位,重传的时候也是以这个单位来进行重传。理想的情况下是该长度的数据刚好不被网络层分块。

滑动窗口控制

我们上面提到的超时重传的机制存在效率低下的问题,发送一个包到发送下一个包要经过一段时间才可以。所以我们就想着**能不能不用等待确认包就发送下一个数据包呢?**这就提出了一个滑动窗口的概念。

https://pic4.zhimg.com/80/v2-dd845d31441f1b89c3f2852a2c815c73_720w.jpg

窗口的大小就是在无需等待确认包的情况下,发送端还能发送的最大数据量。这个机制的实现就是使用了大量的缓冲区,通过对多个段进行确认应答的功能。通过下一次的确认包可以判断接收端是否已经接收到了数据,如果已经接收了就从缓冲区里面删除数据

在窗口之外的数据就是还未发送的和对端已经收到的数据。那么发送端是怎么样判断接收端有没有接收到数据呢?或者怎么知道需要重发的数据有哪些呢?通过下面这个图就知道了。

https://pic3.zhimg.com/80/v2-733c3bf13ac2d7f367e4c2a74d813b9a_720w.jpg

如上图,接收端在没有收到自己所期望的序列号数据之前,会对之前的数据进行重复确认。发送端在收到某个应答包之后,又连续3次收到同样的应答包,则数据已经丢失了,需要重发。

拥塞控制

窗口控制解决了 两台主机之间因传送速率而可能引起的丢包问题,在一方面保证了TCP数据传送的可靠性。然而如果网络非常拥堵,此时再发送数据就会加重网络负担,那么发送的数据段很可能超过了最大生存时间也没有到达接收方,就会产生丢包问题。为此TCP引入慢启动机制,先发出少量数据,就像探路一样,先摸清当前的网络拥堵状态后,再决定按照多大的速度传送数据

此处引入一个拥塞窗口:

发送开始时定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;而在每次发送数据时,发送窗口取拥塞窗口与接送段接收窗口最小者。

慢启动:在启动初期以指数增长方式增长;设置一个慢启动的阈值,当以指数增长达到阈值时就停止指数增长,按照线性增长方式增加至拥塞窗口;线性增长达到网络拥塞时立即把拥塞窗口置回1,进行新一轮的“慢启动”,同时新一轮的阈值变为原来的一半。

https://pic4.zhimg.com/80/v2-7b2c178ea186b2c7310206b7d918a183_720w.jpg

TCP 的有序性是什么来保证的

TCP是一种面向连接的、可靠的基于字节流服务。“面向连接”意味着使用TCP协议的应用在建立联系之前,彼此需要先建立TCP联系;而TCP协议确保传输过程中数据的顺序性则体现其“可靠”的特性,具体如下:

TCP协议将数据切分为多个小片段(数据被划分为合理长度),小片段由头部(header)和数据(payload)组成,为了确保抵达数据的顺序,TCP协议给每个片段的头部(header)都分配了 序列号 ,方便后期按照序列号排序

https://static.coonote.com/e2ee9466914ab382.png

当某个片段按照顺序发送后,发送方会将已发送的数据片段暂时保存在 缓冲区 内,并为每个已发送的数据设置一个 时间区间 。

当接收方收到正确的符合顺序的数据片段后,会优先对数据片段做完整检验,如确认无误,再把数据片段交给上层协议,并给发送方一个 TCP片段反馈信息用来告知(ACK acknowledge)发送方:我已经接收到这个片段了。这个TCP片段被称为 ACK回复 。举个例子:发送的第一个片段序列号为 T,其对应的ACK回复则为T+1,也就是接收方要接收的下一个发送片段的序列号。

https://static.coonote.com/5d0cc5d1874a4a9f.png

假设在规定的时间区间之内发送方收到接收方的 TCP片段反馈信息,则发送方可以释放缓冲区的数据,如若超时未收到应答,发送方则重新发送数据,直到收到应答,或者重发数据次数达到上限为止。

TCP协议的种种机制保证了数据传输的顺序,然而TCP报文段作为IP数据来传输,在IP数据报的到达可能会失序,因此TCP报文段的到达也存在失序的可能。特殊情况下,TCP将对收到的数据进行重新排列,确保顺序正确后再交给应用层。

UDP 如何实现可靠性

https://www.jianshu.com/p/6c73a4585eba

概述

UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。

传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。

**最简单的方式是在应用层模仿传输层TCP的可靠性传输。**下面不考虑拥塞处理,可靠UDP的简单设计。

1、添加seq/ack机制,确保数据发送到对端 2、添加发送和接收缓冲区,主要是用户超时重传。 3、添加超时重传机制。

详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。

目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。 开源程序

1、RUDP(Reliable User Datagram Protocol)

RUDP 提供一组数据服务质量增强机制,如拥塞控制的改进、重发机制及淡化服务器算法等,从而在包丢失和网络拥塞的情况下, RTP 客户机(实时位置)面前呈现的就是一个高质量的 RTP 流。在不干扰协议的实时特性的同时,可靠 UDP 的拥塞控制机制允许 TCP 方式下的流控制行为。

2、RTP(Real Time Protocol)

RTP为数据提供了具有实时特征的端对端传送服务,如在组播或单播网络服务下的交互式视频音频或模拟数据。

应用程序通常在 UDP 上运行 RTP 以便使用其多路结点和校验服务;这两种协议都提供了传输层协议的功能。但是 RTP 可以与其它适合的底层网络或传输协议一起使用。如果底层网络提供组播方式,那么 RTP 可以使用该组播表传输数据到多个目的地。

RTP 本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于底层服务去实现这一过程。 RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性。 RTP 实行有序传送, RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置,例如:在视频解码中,就不需要顺序解码。

3、UDT(UDP-based Data Transfer Protocol)

基于UDP的数据传输协议(UDP-basedData Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。

顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等。

TCP 如何实现端口复用

有難度

FreeBSD 上 SO_REUSEPORT

https://blog.csdn.net/ctthuangcheng/article/details/39014675

Nginx 中配置端口複用

https://blog.csdn.net/yang1018679/article/details/106819681

http 1.0 和 1.1 的区别

https://www.jianshu.com/p/95a521b006a8

长连接(PersistentConnection)

HTTP 1.1支持长连接(PersistentConnection)

HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。

HTTP 1.1则支持持久连接Persistent Connection, 并且默认使用persistent connection.在同一个tcp的连接中可以传送多个HTTP请求和响应. 多个请求和响应可以重叠,多个请求和响应可以同时进行. 更加多的请求头和响应头(比如HTTP1.0没有host的字段).

在1.0时的会话方式:

1.建立连接 2.发出请求信息 3.回送响应信息 4.关掉连接

HTTP 1.1的持续连接,也需要增加新的请求头来帮助实现,例如,Connection请求头的值为Keep-Alive时,客户端通知服务器返回本次请求结果后保持连接;Connection请求头的值为close时,客户端通知服务器返回本次请求结果后关闭连接。HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。

流水线(Pipelining)

请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。 HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容。

host字段

在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。

HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。此外,服务器应该接受以绝对路径标记的资源请求。

100(Continue) Status

HTTP/1.1加入了一个新的状态码100(Continue)。客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized);如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。100 (Continue) 状态代码的使用,允许客户端在发request消息body之前先用request header试探一下server,看server要不要接收request body,再决定要不要发request body。

Chunked transfer-coding

HTTP/1.1中引入了Chunked transfer-coding来解决上面这个问题,发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载

cache

HTTP/1.1在1.0的基础上加入了一些cache的新特性,当缓存对象的Age超过Expire时变为stale对象,cache不需要直接抛弃stale对象,而是与源服务器进行重新激活(revalidation)

HTTP 1.0、1.1、2.0

http 1.0

  • 短连接
  • 每一个请求建立一个TCP连接,请求完成后立马断开连接。这将会导致2个问题:连接无法复用,head of line blocking
  • 连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。head of line blocking会导致带宽无法被充分利用,以及后续健康请求被阻塞。

http 1.1

  • 为解决HTTP 1.0 的痛点而产生。
  • 长连接
  • 通过http pipelining实现。多个http 请求可以复用一个TCP连接,服务器端按照FIFO原则来处理不同的Request
  • 增加connection header
  • 该header用来说明客户端与服务器端TCP的连接方式,若connection为close则使用短连接,若connection为keep-alive则使用长连接
  • 身份认证
  • 状态管理
  • Cache缓存等机制相关的请求头和响应头
  • 增加Host header

http 2.0

  • 多路复用 (Multiplexing)

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。在 HTTP/1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源 CDN 域名的原因之一,拿 Twitter 为例,http://twimg.com,目的就是变相的解决浏览器针对同一域名的请求限制阻塞问题。而 HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。

  • 二进制分帧

HTTP/2在 应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。在不改动 HTTP/1.x 的语义、方法、状态码、URI 以及首部字段的情况下, 解决了HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。

HTTP/2 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。在过去, HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我调谐,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。HTTP/2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。

这种单连接多资源的方式,减少服务端的链接压力,内存占用更少,连接吞吐量更大;而且由于 TCP 连接的减少而使网络拥塞状况得以改善,同时慢启动时间的减少,使拥塞和丢包恢复速度更快。

  • 首部压缩(Header Compression)

HTTP/1.1并不支持 HTTP 首部压缩,为此 SPDY 和 HTTP/2 应运而生, SPDY 使用的是通用的DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法。

  • 服务端推送(Server Push)

服务端推送是一种在客户端请求之前发送数据的机制。在 HTTP/2 中,服务器可以对客户端的一个请求发送多个响应。Server Push 让 HTTP1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源,不过与之相比,服务器推送还有一个很大的优势:可以缓存!也让在遵循同源的情况下,不同页面之间可以共享缓存资源成为可能。

HTTP 與 HTTPS

https://tw.alphacamp.co/blog/http-https-difference

相信大多數的讀者,對這個畫面應該都不陌生吧?當前最熱門的瀏覽器 Google Chrome 從 Chrome 69 開始,會在網站使用 HTTP 作為傳輸協定時,在網址列提示使用者「不安全」,甚至出現上圖的提示畫面,藉此來要求網站開發者盡快將網站轉為透過 HTTPS 傳輸資料,甚至 在今年底的 Chrome 79,將會逐步封鎖 HTTPS 網頁中以 HTTP 下載的內容;但 HTTP 與 HTTPS 的差別到底是什麼?HTTPS 又是如何讓資料傳遞變安全的呢?

(瞭解網路應用程式操作原理,包括 HTTP、網域、RESTful API、MVC 設計等核心概念)

HTTP 的資料傳輸

HTTP 全名是 超文本傳輸協定(HyperText Transfer Protocol),內容只規範了客戶端請求與伺服器回應的標準,實際上是藉由 TCP 作為資料的傳輸方式。

更多 TCP 的傳輸協議內容及詳細的過程可以參考 這篇文章。

例如使用者送出了一個請求,經過 TCP 的三次握手之後,資料便能透過 TCP 傳遞給伺服器,並等待伺服器回應;然而這個一來一往的傳輸過程,資料都是 明文;如果傳遞的過程中有惡意竊聽者,資料便有機會被窺探、盜用。

什麼?你說沒有人會這麼無聊?其實非常多。像是 惡意使用者偽裝成公用無線網路來釣魚,當使用者連上之後,便可以擷取封包,窺探傳輸的內容;再說,即使扣除掉這種不知名的免費無線網路,你也沒辦法確認網路連線到目標伺服器的路上,每個節點都不會窺探、側錄你所傳遞的資料。

傳遞的內容不加密,就有如在網路上裸奔一樣;為了避免這些事情,我們需要把資料加密。

加密

加密 指的是把明文資料轉換成無法讀取的內容 - 密文,並且密文能藉由特定的解密過程,將其回復成明文。

讓部分開發者時常混淆在一起的是 雜湊,可以參考 這一篇。

共用金鑰加密

像是可能大家都有聽過的 凱薩加密法,就是一個非常基本的加密方式:將明文的字母全部位移固定的距離,解密時再位移回來;例如明文是 「EGG」,位移距離(金鑰)為 3,那麼加密後的密文就會是「HJJ」。

當然,真實的環境不會用這種很容易被解出來的加密方式,而是會透過例如 AES 等方式進行加密;但兩者同樣的是,都會透過同一個金鑰來進行加密與解密,因此我們把這類的加密方式稱為「共用金鑰加密」,或是「對稱式加密」。

經過共用金鑰加密,資料傳遞就安全了嗎?其實不然。想像一下網路通訊的情況,如果通訊的兩人需要透過共用金鑰加密進行通訊,勢必要先讓兩人都知道要用來加密的金鑰是什麼,那麼當其中一方決定要發起通訊時,就必須要先直接傳遞金鑰給對方,但這個金鑰是沒有加密、可能會被窺探的,如果竊聽者在通取開始前就開始竊聽,便能得到密鑰,後面傳遞的密文,也就可能會被竊聽者窺探。

https://ithelp.ithome.com.tw/upload/images/20191014/20111380SnvPeAGC9F.png

圖片來自 演算法圖鑑 - 第 5 章:安全性演算法

看來要安全的進行通訊,就需要其他的加密方式;例如 迪菲-赫爾曼密鑰交換,或是我們接下來要談的「公開金鑰加密」。

公開金鑰加密

公開金鑰加密,也有人之稱為「非對稱式加密」;在這個加密規則中,每個通訊者都會有成對的兩把鑰匙:一把「公鑰」,一把「私鑰」,顧名思義,公鑰是所有人都看得到的,而私鑰是只有通訊者自己才看得到的;每個資料被任意一把鑰匙加密後,必須要透過與之配對的另一把鑰匙才能解密;例如我用我的私鑰加密的密文,就只能被我的公鑰解密,反之亦同。

在這樣的規則下,進行通訊會發生什麼事呢?假想一下:Alice 和 Bob 準備進行通訊,而 Eve 是不懷好意的竊聽者;Alice 把要傳遞的明文經過 Bob 的公鑰進行加密後,再進行傳遞,由於 Bob 的私鑰只有 Bob 擁有,即使 Eve 竊取到了密文,也無法將其解密回明文。

這樣子是不是就可以安心進行通訊了呢?很遺憾的,還是沒辦法;因為通訊的雙方,雖然看得到對方的公鑰,但沒辦法證明這個公鑰是通訊的對方所擁有。

我們設想另一個情況:Alice 和 Bob 準備進行通訊,而 Eve 是不懷好意的竊聽者,且 Alice 和 Bob 都把 Eve 當成是通訊的對方;這樣的情境下,Alice 把明文用 Eve 的公鑰加密後,將密文傳遞出來,隨即被 Eve 攔截、解密後,再用 Bob 的公鑰重新加密明文,再傳遞給 Bob,反之亦同,這樣子 Alice 和 Bob 都不會知道有 Eve 的存在,但 Eve 卻成功的取得了通訊內容;這就叫做 中間人攻擊。

https://ithelp.ithome.com.tw/upload/images/20191014/20111380msbqu0Yn9V.png

圖片來自 演算法圖鑑 - 第 5 章:安全性演算法

那怎麼辦呢?我們需要一個能證明公鑰屬於誰的方法。

數位憑證

因此就出現了 憑證頒發機構,例如 Alice 和 Bob 要準備進行通訊;在開始之前,Alice 必須先提供公鑰 & Email,向憑證頒發機構申請憑證,憑證頒發機構核可後,便會透過 數位簽章 包裹 Alice 提供的資料,製作成 數位憑證。

憑證的詳細格式可以參考 Wiki - X.509

當通訊開始時,Alice 會先傳遞數位憑證給 Bob,而 Bob 便可以透過數位簽章,來證明憑證的內容確實是屬於 Alice 的;如此一來,證明公鑰是屬於誰的問題就被解決了,即使竊聽者想要從中竊聽,也因為憑證頒發機構的數位簽章,竊聽者將無從介入通訊過程。

這樣聽起來,憑證頒發機構非常重要啊,全世界網站這麼多,需要進行通訊的請求自然也非常多,不可能全部都詢問同一個機構吧?沒錯,實際上的憑證頒發,會如同下圖一樣:

終端數位憑證由中介機構簽發、中介機構的憑證由更上游的中介機構簽發,直到源頭,它的憑證由自己簽發,這樣就形成了一個 信任鏈。

簡單的說,因為我信任 A,所以 A 擔保的 B、C、D,以及 B 擔保的 E、F、G,我全部都相信。

HTTPS

說了這麼多加密,所以到底什麼是 HTTPS?

HTTPS 全名 超文本傳輸安全協定,那個 S 就是 Secure 的意思;HTTPS 透過 HTTP 進行通訊,但通訊過程使用 SSL/TLS 進行加密,藉由類似於前述的加密方式,在 HTTP 之上定義了相對安全的資料傳輸方法。

由於非對稱加密的運算量較高,傳遞回應較慢;實際的架構上,會透過公開金鑰加密傳遞出共用的金鑰,再透過共用金鑰加密進行後續的傳遞,兼顧了安全性及傳遞速度。 結語

今天從加密的方式出發,考慮每種加密通訊過程中可能受到的攻擊,逐步演變成現今最普遍的加密方式,並藉此來說明 HTTP 與 HTTPS 之間的差異,希望能幫助讀者您理解網路通訊中最基礎的安全知識。

非對稱加密

https://academy.binance.com/zt/articles/symmetric-vs-asymmetric-encryption

你知道,HTTPS用的是对称加密还是非对称加密?

https://zhuanlan.zhihu.com/p/96494976

拥塞控制

前面的問題[TCP 实现可靠的机制]有回答到

  • 超时重传的时间怎么确定

https://blog.csdn.net/qq_35733751/article/details/80173022

流量控制

感覺內容有點多 https://notfalse.net/24/tcp-flow-control https://zhuanlan.zhihu.com/p/37379780

什么是流量控制?流量控制的目的?

如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。

如何实现流量控制?

滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送

流量控制引发的死锁?怎么避免死锁的发生?

当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。 为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。

拥塞控制和流量控制的区别

拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:( 1 )慢开始、拥塞避免( 2 )快重传、快恢复。

流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。

输入一个 url 到显示网页的过程

1、输入地址 2、浏览器查找域名的 IP 地址 3、浏览器向 web 服务器发送一个 HTTP 请求 4、服务器的永久重定向响应 6、服务器处理请求 7、服务器返回一个 HTTP 响应 8、浏览器显示 HTML 9、浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)

NAT 的原理

https://www.itread01.com/content/1541919215.html

介紹

NAT英文全稱是“Network Address Translation”,中文意思是“網路地址轉換”,它是一個IETF(Internet Engineering Task Force, Internet工程任務組)標準,允許一個整體機構以一個公用IP(Internet Protocol)地址出現在Internet上。顧名思義,它是一種把內部私有網路地址(IP地址)翻譯成合法網路IP地址的技術。因此我們可以認為,NAT在一定程度上,能夠有效的解決公網地址不足的問題。

NAT有三種類型:靜態NAT(Static NAT)、動態地址NAT(Pooled NAT)、網路地址埠轉換NAPT(Port-Level NAT)。

原理

1.地址轉換

NAT的基本工作原理是,當私有網主機和公共網主機通訊的IP包經過NAT閘道器時,將IP包中的源IP或目的IP在私有IP和NAT的公共IP之間進行轉換。

如下圖所示,NAT閘道器有2個網路埠,其中公共網路埠的IP地址是統一分配的公共 IP,為202.20.65.5;私有網路埠的IP地址是保留地址,為192.168.1.1。私有網中的主機192.168.1.2向公共網中的主機202.20.65.4傳送了1個IP包(Dst=202.20.65.4,Src=192.168.1.2)。

https://img-blog.csdnimg.cn/20181108205036335.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dkMDMwNg==,size_16,color_FFFFFF,t_70

當IP包經過NAT閘道器時,NAT Gateway會將IP包的源IP轉換為NAT Gateway的公共IP並轉發到公共網,此時IP包(Dst=202.20.65.4,Src=202.20.65.5)中已經不含任何私有網IP的資訊。由於IP包的源IP已經被轉換成NAT Gateway的公共IP,Web Server發出的響應IP包(Dst= 202.20.65.5,Src=202.20.65.4)將被髮送到NAT Gateway。

這時,NAT Gateway會將IP包的目的IP轉換成私有網中主機的IP,然後將IP包(Des=192.168.1.2,Src=202.20.65.4)轉發到私有網。對於通訊雙方而言,這種地址的轉換過程是完全透明的。轉換示意圖如下。

https://img-blog.csdnimg.cn/20181108205059531.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dkMDMwNg==,size_16,color_FFFFFF,t_70

如果內網主機發出的請求包未經過NAT,那麼當Web Server收到請求包,回覆的響應包中的目的地址就是私網IP地址,在Internet上無法正確送達,導致連線失敗。

2.連線跟蹤

在上述過程中,NAT Gateway在收到響應包後,就需要判斷將資料包轉發給誰。此時如果子網內僅有少量客戶機,可以用靜態NAT手工指定;但如果內網有多臺客戶機,並且各自訪問不同網站,這時候就需要連線跟蹤(connection track)。如下圖所示:

https://img-blog.csdnimg.cn/20181108205120304.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dkMDMwNg==,size_16,color_FFFFFF,t_70

在NAT Gateway收到客戶機發來的請求包後,做源地址轉換,並且將該連線記錄儲存下來,當NAT Gateway收到伺服器來的響應包後,查詢Track Table,確定轉發目標,做目的地址轉換,轉發給客戶機。

3.埠轉換

以上述客戶機訪問伺服器為例,當僅有一臺客戶機訪問伺服器時,NAT Gateway只須更改資料包的源IP或目的IP即可正常通訊。但是如果Client A和Client B同時訪問Web Server,那麼當NAT Gateway收到響應包的時候,就無法判斷將資料包轉發給哪臺客戶機,如下圖所示。

https://img-blog.csdnimg.cn/20181108205143905.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dkMDMwNg==,size_16,color_FFFFFF,t_70

NAT協議的應用 NAT主要可以實現以下幾個功能:資料包偽裝、平衡負載、埠轉發和透明代理。

資料偽裝: 可以將內網資料包中的地址資訊更改成統一的對外地址資訊,不讓內網主機直接暴露在因特網上,保證內網主機的安全。同時,該功能也常用來實現共享上網。 埠轉發: 當內網主機對外提供服務時,由於使用的是內部私有IP地址,外網無法直接訪問。因此,需要在閘道器上進行埠轉發,將特定服務的資料包轉發給內網主機。 負載平衡: 目的地址轉換NAT可以重定向一些伺服器的連線到其他隨機選定的伺服器。(不是很明白) 失效終結: 目的地址轉換NAT可以用來提供高可靠性的服務。如果一個系統有一臺通過路由器訪問的關鍵伺服器,一旦路由器檢測到該伺服器當機,它可以使用目的地址轉換NAT透明的把連線轉移到一個備份伺服器上。(如何轉移的?) 透明代理: NAT可以把連線到因特網的HTTP連線重定向到一個指定的HTTP代理伺服器以快取資料和過濾請求。一些因特網服務提供商就使用這種技術來減少頻寬的使用而不用讓他們的客戶配置他們的瀏覽器支援代理連線。(如何重定向的?)

长连接和短连接的区别,各自使用场景

Http长连接和短连接

长连接: 客户端和服务端建立连接后不进行断开,之后客户端再次访问这个服务器上的内容时,继续使用这一条连接通道。

短连接: 客户端和服务端建立连接,发送完数据后立马断开连接。下次要取数据,需要再次建立连接。

在HTTP/1.0中,默认使用的是短连接。但从 HTTP/1.1起,默认使用长连接。

Http长连接和TCP长连接的区别

Http长连接 和 TCP长连接的区别在于:** TCP 的长连接需要自己去维护一套心跳策略**。,而Http只需要**在请求头加入keep-alive:true即可实现长连接**。

了解哪些http状态码

状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值,如下:

1xx:信息性状态码,表示服务器已接收了客户端请求,客户端可继续发送请求。

1.100 Continue 2.101 Switching Protocols

2xx:成功状态码,表示服务器已成功接收到请求并进行处理。

1.200 OK 表示客户端请求成功 2.204 No Content 成功,但不返回任何实体的主体部分 3.206 Partial Content 成功执行了一个范围(Range)请求

3xx:重定向状态码,表示服务器要求客户端重定向。

1.301 Moved Permanently 永久性重定向,响应报文的Location首部应该有该资源的新URL 2.302 Found 临时性重定向,响应报文的Location首部给出的URL用来临时定位资源 3.303 See Other 请求的资源存在着另一个URI,客户端应使用GET方法定向获取请求的资源 4.304 Not Modified 服务器内容没有更新,可以直接读取浏览器缓存 5.307 Temporary Redirect 临时重定向。与302 Found含义一样。302禁止POST变换为GET,但实际使用时并不一定,307则更多浏览器可能会遵循这一标准,但也依赖于浏览器具体实现

4xx:客户端错误状态码,表示客户端的请求有非法内容。

1.400 Bad Request 表示客户端请求有语法错误,不能被服务器所理解 2.401 Unauthonzed 表示请求未经授权,该状态代码必须与 WWW-Authenticate 报头域一起使用 3.403 Forbidden 表示服务器收到请求,但是拒绝提供服务,通常会在响应正文中给出不提供服务的原因 4.404 Not Found 请求的资源不存在,例如,输入了错误的URL

5xx:服务器错误状态码,表示服务器未能正常处理客户端的请求而出现意外错误。

1.500 Internel Server Error 表示服务器发生不可预期的错误,导致无法完成客户端的请求 2.503 Service Unavailable 表示服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常

301 和 302 的区别。

301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点。

他们的不同在于。301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;

302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。SEO302好于301

重定向原因:

1.网站调整(如改变网页目录结构); 2.网页被移到一个新地址; 3.网页扩展名改变(如应用需要把.php改成.Html或.shtml)。

这种情况下,如果不做重定向,则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404页面错误信息,访问流量白白丧失;再者某些注册了多个域名的网站,也需要通过重定向让访问这些域名的用户自动跳转到主站点等。

什么时候进行301或者302跳转呢?

当一个网站或者网页24—48小时内临时移动到一个新的位置,这时候就要进行302跳转,而使用301跳转的场景就是之前的网站因为某种原因需要移除掉,然后要到新的地址访问,是永久性的。

清晰明确而言:使用301跳转的大概场景如下:

**域名到期不想续费(或者发现了更适合网站的域名),想换个域名。**在搜索引擎的搜索结果中出现了不带www的域名,而带www的域名却没有收录,这个时候可以用301重定向来告诉搜索引擎我们目标的域名是哪一个。空间服务器不稳定,换空间的时候。

http 緩存機制

https://segmentfault.com/a/1190000021716418

get与 post的区别

(本标准答案参考自w3schools)

1.GET在浏览器回退时是无害的,而POST会再次提交请求。

2.GET产生的URL地址可以被Bookmark,而POST不可以。

3.GET请求会被浏览器主动cache,而POST不会,除非手动设置。

4.GET请求只能进行url编码,而POST支持多种编码方式。

5.GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

6.GET请求在URL中传送的参数是有长度限制的,而POST么有。

7.对参数的数据类型,GET只接受ASCII字符,而POST没有限制。

8.GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

9.GET参数通过URL传递,POST放在Request body中

https://www.oschina.net/news/77354/http-get-post-different

GET和POST还有一个重大区别,简单的说:

GET产生一个TCP数据包;POST产生两个TCP数据包。

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?

  1. GET与POST都有自己的语义,不能随便混用。

  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次

DNS 的解析过程

https://img-blog.csdn.net/20180828140314143?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NtYWxsU3VuTA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70

第一步:检查浏览器缓存中是否缓存过该域名对应的IP地址 第二步:如果在浏览器缓存中没有找到IP,那么将继续查找本机系统是否缓存过IP 第三步:向本地域名解析服务系统发起域名解析的请求 第四步:向根域名解析服务器发起域名解析请求 第五步:根域名服务器返回gTLD域名解析服务器地址 第六步:向gTLD服务器发起解析请求 第七步:gTLD服务器接收请求并返回Name Server服务器 第八步:Name Server服务器返回IP地址给本地服务器 第九步:本地域名服务器缓存解析结果 第十步:返回解析结果给用户

ping 的过程

我们发起一个了从开发板到百度www.baidu.com 的ping 请求。(这里路由1作为局域网的默认网关) 1.首先开发板要解析百度的域名,获取到百度主机的IP 地址,涉及到DNS协议,传输层用的是UDP协议。 2.DNS 主机利用UDP 协议,回复百度的IP 给开发板(这里也涉及了ARP 协议暂时不讲) 3.现在开发板要发送Ping 请求包给百度主机,但是发现百度主机IP 与自己不在同一网段,因此要发送Ping 请求包给默认网关。 4.要发送给默认网关的时候,如果发现并没有默认网关对应的MAC 地址,因此发送一个ARP 广播包,如果交换机存储了默认网关的MAC 地址,就直接告诉开发板默认网关的MAC 地址,否则向所有端口发送ARP 广播。 5.路由1收到了ARP请求报文后,单播自己的MAC 地址给开发板。 6.这样开发板就可以把Ping 包发送给默认网关(路由1)了。 7.然后路由1 通过路由协议,经过一个个路由的转发,最后发送到了百度的主机上。百度主机检测到IP 是自己的IP,接收并处理Ping 请求,接着百度主机发送一个Ping 回应报文给开发板。

ICMP协议

ICMP是“Internet Control Message Ptotocol”的缩写。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。

控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。在网络中经常会使用到ICMP协议。例如经常用于检查网络不通的ping命令,这个ping的过程实际上就是ICMP协议工作的过程。还有跟踪路由的trancert命令也是基于ICMP协议的。

ARP协议

网络层以上的协议用IP地址来标识网络接口,但以太数据帧传输时,以物理地址来标识网络接口。因此我们需要进行IP地址与物理地址之间的转化。对于IPv4来说,我们使用ARP地址解析协议来完成IP地址与物理地址的转化(IPv6使用邻居发现协议进行IP地址与物理地址的转化,它包含在ICMPv6中).ARP协议提供了网络层地址(IP地址)到物理地址(mac地址)之间的动态映射。ARP协议 是地址解析的通用协议。

MAC 地址

什麼是MAC Address ? 每一個網路介面卡都有一個獨一無二的識別碼,這個識別碼是由六組16進位數字組成的物理位置,也稱為MAC(Media Access Control)位址。 這個位址分為兩個部分,前三組數字是廠商ID;後三組數字則是網路卡的卡號,理論上全世界沒有兩張網路卡的MAC位址是相同的。

以下為延伸面試題

socket 基础 API 的使用

https://blog.csdn.net/zh13544539220/article/details/44832639

创建套接字──socket()

应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:

1
2
3
4
SOCKET PASCAL FAR socket(
int af, 
int type, 
int protocol);

指定本地地址──bind()

当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:

1
2
3
4
int PASCAL FAR bind(
SOCKET s, 
const struct sockaddr FAR * nam e,
int namelen);

参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。

参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。

namelen表明了name的长度.如果没有错误发生,bind()返回 0。否则返回SOCKET_ERROR。

建立套接字连接──connect()与accept()

这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接 字建立“连接”,便能判断该端靠纪纪可操作。而accept()用于使服务器等待来自某客户进程的实际连接。

connect()的调用格式如下:

1
2
3
4
int PASCAL FAR connect(
SOCKET s, 
const struct sockaddr FAR * name,
int namelen);

参数s是欲建立连接的本地套接字描述符。

参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。

accept()的调用格式如下:

1
2
3
4
SOCKET PASCAL FAR accept(
SOCKET s, 
struct sockaddr FAR* addr, 
int FAR* addrlen);

参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。

addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值 INVALID_SOCKET。

四个套接字系统调用,socket()、bind()、 connect()、accept(),可以完成一个完全五元相关的建立。

socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面 向连接无关。

bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用 bind(),若采用面向连接,则可以不调用bind(),

而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一 的地址。

监听连接──listen()

此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:

1
2
3
    int PASCAL FAR listen(
	SOCKET s, 
	int backlog);

参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。

backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。

数据传输──send()与recv()

重點

send()调用用于钥纪纪数s指定的已连接的数据报或流套接字上发送输出数据,格式如下:

1
2
3
4
5
    int PASCAL FAR send(
	SOCKET s,
  	const char FAR *buf, 
	int len, 
	int flags);

参数s为已连接的本地套接字描述符。

buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。

flags 指定传输控制方式,如是否发送带外数据等。

如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。

recv()调用用于s指定的已连接的数据报或流套接字上接收输入数据,格式如下:

1
2
3
4
5
    int PASCAL FAR recv(
	SOCKET s, 
	char FAR *buf, 
	int len, 
	int flags);

参数s 为已连接的套接字描述符。

buf指向接收输入数据缓冲区的指针,其长度由len 指定。F

lags 指定传输控制方式,如是否接收带外数据等。

如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回 SOCKET_ERROR。

输入/输出多路复用──select()

select() 调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结 构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:

1
2
3
4
5
6
int PASCAL FAR select(
int nfds, 
fd_set FAR * readfds, 
fd_set FAR * writefds, 
fd_set FAR * exceptfds,
 const struct timeval FAR * timeout);

参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。

参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。

参数writefds 指向要做写检测的套接字描述符集合的指针。

exceptfds指向要检测是否出错的套接字描述符集合的指针。

timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操 作。

select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。

关闭套接字──closesocket()

closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:

1
BOOL PASCAL FAR closesocket(SOCKET s);

参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。

以下是实现一个简单的客户端服务端链接的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
server端  
    
#include <WINSOCK2.H>  
#include <stdio.h>  
#pragma comment(lib,"ws2_32.lib")  
void main()  
{  
 //创建套接字  
 WORD myVersionRequest;  
 WSADATA wsaData;  
 myVersionRequest=MAKEWORD(1,1);  
 int err;  
 err=WSAStartup(myVersionRequest,&wsaData);  
 if (!err)  
 {  
  printf("已打开套接字\n");  
 }   
 else  
 {  
  //进一步绑定套接字  
  printf("嵌套字未打开!");  
  return;  
 }  
 SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//创建了可识别套接字  
 //需要绑定的参数  
 SOCKADDR_IN addr;  
 addr.sin_family=AF_INET;  
 addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址  
 addr.sin_port=htons(6000);//绑定端口  
    
 bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//绑定完成  
 listen(serSocket,5);//其中第二个参数代表能够接收的最多的连接数  
    
 //  
 //开始进行监听  
 //  
 SOCKADDR_IN clientsocket;  
 int len=sizeof(SOCKADDR);  
 while (1)  
 {  
  SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//如果这里不是accept而是conection的话。。就会不断的监听  
  char sendBuf[100];  
     
  sprintf(sendBuf,"welcome %s to bejing",inet_ntoa(clientsocket.sin_addr));//找对对应的IP并且将这行字打印到那里  
  send(serConn,sendBuf,strlen(sendBuf)+1,0);  
  char receiveBuf[100];//接收  
  recv(serConn,receiveBuf,strlen(receiveBuf)+1,0);  
  printf("%s\n",receiveBuf);  
  closesocket(serConn);//关闭  
 WSACleanup();//释放资源的操作  
 }  
}  
    
    
    
client端  
    
   
#include <WINSOCK2.H>  
#include <stdio.h>  
#pragma comment(lib,"ws2_32.lib")  
void main()  
{  
 int err;  
 WORD versionRequired;  
 WSADATA wsaData;  
 versionRequired=MAKEWORD(1,1);  
 err=WSAStartup(versionRequired,&wsaData);//协议库的版本信息  
 if (!err)  
 {  
  printf("客户端嵌套字已经打开!\n");  
 }  
 else  
 {  
  printf("客户端的嵌套字打开失败!\n");  
  return;//结束  
 }  
 SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0);  
 SOCKADDR_IN clientsock_in;  
 clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");  
 clientsock_in.sin_family=AF_INET;  
 clientsock_in.sin_port=htons(6000);  
 //bind(clientSocket,(SOCKADDR*)&clientsock_in,strlen(SOCKADDR));//注意第三个参数  
 //listen(clientSocket,5);  
 connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//开始连接  
 char receiveBuf[100];  
 recv(clientSocket,receiveBuf,101,0);  
 printf("%s\n",receiveBuf);  
 send(clientSocket,"hello,this is client",strlen("hello,this is client")+1,0);  
 closesocket(clientSocket);  
 WSACleanup();  
}  

Tcp连接主动关闭与被动关闭的区别。

TIME_WAIT

TIME_WAIT 是主动关闭链接时形成的,等待2MSL时间,约4分钟。主要是防止最后一个ACK丢失。 由于TIME_WAIT 的时间会非常长,因此server端应尽量减少主动关闭连接

CLOSE_WAIT

CLOSE_WAIT是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。

服务端可以建立连接的最大数目由什么决定

根據

客戶端和服務器端的 ip : port

可以有 (2^128x65536)^2

然后还有些目前没在用但是也可以用来区分连接的,那就更能塞了,ipv6还可以自己塞一堆header

那基本就是infinite

但還受限於服務器內存

https://www.sohu.com/a/221661481_216613

client最大tcp连接数

client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short,因此本地端口个数最大只有65536,端口0有特殊含义,不能使用,这样可用端口最多只有65535,所以在全部作为client端的情况下,一个client最大tcp连接数为65535,这些连接可以连到不同的serverip。

server最大tcp连接数

server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remoteip(也就是clientip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方。

实际的tcp连接数

上面给出的是理论上的单机最大连接数,在实际环境中,受到机器资源、操作系统等的限制,特别是sever端,其最大并发tcp连接数远不能达到理论上限。在unix/linux下限制连接数的主要因素是内存和允许的文件描述符个数(每个tcp连接都要占用一定内存,每个socket就是一个文件描述符),另外1024以下的端口通常为保留端口。

对server端,通过增加内存、修改最大文件描述符个数等参数,单机最大并发TCP连接数超过10万,甚至上百万是没问题的

端口在传输过程中会变么

這裡要分情境,從傳輸到接收過程端口對外不會變,除非段開重新連接。

如果是应用层协议的传输的话那随便怎么乱搞了。。

NAT 的情況是端口映射 只會對外不變 在裏面隨便變都沒所謂

TCP四次挥手中,如果第三次丢失了会怎么办

不確定性很高 重新傳輸?

重传也不是一定能到嘛 xmppbot 就遇到过中间丢了一个数据包,然后卡在那里的情况 所以网络程序都要有超时设置。曾经 rsync 忘记写超时也是卡在那不动弹 有些卡住的状态一定时间之后会被清除,有些不会

TCP两次握手可以不?第三次握手可以传输数据吗?

TCP 不可以兩次握手,前面有同樣這個面試問題。

第一次和第二次是不可以携带数据的,但是第三次是可以携带数据的。 假如第一次握手可以携带数据的话,那对于服务器是不是太危险了,有人如果恶意攻击服务器,每次都在第一次握手中的SYN报文中放入大量数据。

**第三次握手,此时客户端已经处于ESTABLISHED状态。**对于客户端来说,他已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的。所以也就可以携带数据了。

TCP缺点?慢启动如何优化

(1)

TCP的缺點:** 慢,效率低,占用系統資源高**,易被攻擊TCP在傳遞數據之前,要先建連接,這會消耗時間,而且在數據傳遞時,確認機制、重傳機制、擁塞控制機制等都會消耗大量的時間,而且要在每台設備上維護所有的傳輸連接,事實上,每個連接都會占用系統的CPU、內存等硬體資源。

(2)

https://blog.csdn.net/yusiguyuan/article/details/21458135

什么是慢启动

最初的TCP的实现方式是, 在连接建立成功后便会向网络中发送大尺寸的数据包,假如网络出现问题,很多这样的大包会积攒在路由器上, 很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此现在的TCP协议规定了, 新建立的连接不能够一开始就发送大尺寸的 数据包,而只能从一个小尺寸的包开始发送,在发送和数据被对方确认的过程中去计算对方的接收速度,来 逐步增加每次发送的数据量(最后到达一个稳定的值,进入高速传输阶段。相应的,慢启动过程中,TCP通道处在低速传输阶段 ),以避免上述现象的发生。这个策略就是慢启动。 画个简单的图从原理上粗略描述一下

http://blog.chinaunix.net/attachment/201309/17/29075379_1379350656yy03.jpg

慢启动引起的性能问题

在海量用户高并发访问的大型网站后台,有一些基本的系统维护需求。比如迁移海量小文件,就是从一些机器拷贝海量小碎文件到另一些机器,来完成一些系统维护的基本需求。

请不要小看这样的需求,这是服务器领域乃至云计算领域几个最复杂的问题之一,量变到质变,由量大引起的难题。今天在我这篇文章中,我只说这个如何避免慢启动来提升TCP层的传输加速问题。 言归正传,慢启动为什么会对拷贝海量小文件的需求造成重大性能损失?

举个简单的例子,我们对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝就是这个例子的实际场景,很常见的用法)。那么工作过程应该是,每传输一个文件建立一个连接,然后连接处于慢启动阶段,传输小文件,每个小文件几乎都处于独立连接的慢启动阶段被传输,这样传输过程所用的TCP包的总量就会增多。更细致的说一说这个事,如果在慢启动过程中传输一个小文件,我们可能需要2至3个小包,而在一个已经完成慢启动的TCP通道中(TCP通道已进入在高速传输阶段),我们传输这个文件可能只需要1个大包。网络拷贝文件的时间基本上全部消耗都在网络传输的过程中(发数据过去等对端ACK,ACK确认归来继续再发,这样的数据来回交互相比较本机的文件读写非常耗时间),撇开三次握手和四次握手那些包,粗略来说,慢启动阶段传输这些文件所用的包的数目是高速通道传输这些文件的包的数目的2-3倍!那么时间上应该也是2-3倍的关系!如果文件的量足够大,这个总时间就会被放大到需求难以忍受的地步。

因此,在迁移海量小文件的需求下,我们不能使用“对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝)“这样的策略,它会使每个文件的传输都处于在一个独立TCP的慢启动阶段。

如何避免慢启动,进而提升性能

很简单,尽量把大量小文件放在一个TCP连接中排队传输。起初的一两个文件处于慢启动过程传输,后续的文件传输全部处于高速通道中传输,用这样的方式来减少发包的数目,进而降低时间消耗。 题外话,实际上这种传输策略带来的性能提升的功劳不仅仅归于避免慢启动,事实上也避免了大量的3次握手和四次握手,这个对海量小文件传输的性能消耗也非常致命,但是这是另一个问题,本篇不多加介绍。

随着多核服务器的兴起,以及现代网卡的多通道技术的迅猛发展,现在我们解决这一问题的通常做法是绑定多CPU的多核到网卡的多个通道,然后由CPU的核来均分传输这些小文件,每个核用一个TCP连接来排队发送分到的小文件。

讲到这儿,我想大家对于大文件的传输策略应该也心里有数了,(不考虑网卡带宽的前提下)就是分块传输,在目标机器合并。

以下題目偏難,中級面試者,面試官會問到,暫時不整理

  • select 函数的用法

  • 非阻塞 connect 函数的写法

  • epoll 的水平和边缘模式

  • 阻塞socket与非阻塞socket的区别

  • send/recv函数的返回值情形

  • reuse_addr选项

數據結構

堆和栈的区别

队列、堆、栈、堆栈的区别

堆栈:先进后出(就像放在箱子的衣服,先放进去的后拿出来)

队列:先进先出(就像一条路,有一个入口和一个出口,先进去的就可以先出去)

进程中每个线程都有自己的堆栈,这是一段线程创建时保留下的地址区域。我们的“栈内存”即在此。 至于“堆”内存,我个人认为在未用new定义时,堆应该就是未“保留”未“提交”的自由空间,new的功能是在这些自由空间中保留(并提交)出一个地址范围。

栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。在编程中,例如C/C++中,所有的局部变量都是从栈中分配内存空间,实际上也不是什么分配,只是从栈顶向上用就行,在退出函数的时候,只是修改栈指针就可以把栈中的内容销毁,所以速度最快。    堆(Heap)是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁时都要占用时间,所以用堆的效率低的多!但是堆的好处是可以做的很大,C/C++对分配的Heap是不初始化的。    在Java中除了简单类型(int,char等)都是在堆中分配内存,这也是程序慢的一个主要原因。但是跟C/C++不同,Java中分配Heap内存是自动初始化的。在Java中所有的对象(包括int的wrapper Integer)都是在堆中分配的,但是这个对象的引用却是在Stack中分配。也就是说在建立一个对象时从两个地方都分配内存,在Heap中分配的内存实际建立这个对象,而在Stack中分配的内存只是一个指向这个堆对象的指针(引用)而已。

堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。{堆是指程序运行是申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。}

栈是先进后出的,但是于堆而言却没有这个特性,两者都是存放临时数据的地方。 对于堆,我们可以随心所欲的进行增加变量和删除变量,不要遵循什么次序,只要你喜欢。

栈为什么比堆快一些

https://blog.csdn.net/AlbenXie/article/details/103824830

在栈上分配的内存系统会自动地为其释放,例如在函数结束时,局部变量将不复存在,就是系统自动清除栈内存的结果。但堆中分配的内存则不然:一切由你负责,即使你退出了new表达式的所处的函数或者作用域,那块内存还处于被使用状态而不能再利用。好处就是如果你想在不同模块中共享内存,那么这一点正合你意,坏处是如果你不打算再利用这块内存又忘了把它释放掉,那么它就会霸占你宝贵的内存资源直到你的程序退出为止。

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyorbelt)一样,StackPointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快,当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时. 看看LINUX内核源代码的存储管理部分,就知道操作系统是如何管理内存资源的。每个进程都有独立的地址空间,不过这只是虚地址,这也是我们通常所看到的地址,所以我们现在写程序时不会像早期的程序员担心内存不够用,对于LINUX用户进程最大可用3G的地址空间,另外的1G留给内核。这里讨论的堆或栈都是在虚拟地址空间上,各个进程都有自己独立堆、栈空间,否则,就象楼上一些哥们说的,一个进程飞了,那所有进程都得死。

至于堆和栈哪个更快,从两方面来考虑:

栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

而且 栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放 堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定

1.分配和释放,堆在分配和释放时都要调用函数(MALLOC,FREE),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成空洞),这些都会花费一定的时间,具体可以看看MALLOC和FREE的源代码,他们做了很多额外的工作,而栈却不需要这些。 2.访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。

综上所述,站在操作系统以上的层面来看,栈的效率比堆高,对于应用程序员,这些都是透明的,操作系统做了很多我们看不到的东西。

多线程和堆和栈

很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G。 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,Windows进程还有所谓进程默认堆,用户也可以创建自己的堆。

堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

map 的底层原理

Map数据结构

Map也是容器的一种,那么我们以前看到的每一种容器,都有响应的数据结构,例如数组是一组连续的存储空间,链表是无序的,包含指针域和值域的容器。

Map的每一个元素叫做键值对,所谓键值对其实就是 “键” 和 “值” 组成的一对。

map的主要实现类是hashmap和treemap,在java开发过程中主要用到的是hashmap。下面简单介绍一下hashmap原理

数组

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

哈希表

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组”    https://img-blog.csdnimg.cn/20190520163454369.?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pfcm9zZV9xdWVlbg==,size_16,color_FFFFFF,t_70

hashmap底层原理

HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小×loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16×0.75=12的 时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

map的内存分配策略

https://blog.csdn.net/weixin_30740581/article/details/97633011

代碼內容過長….

功能说明: map的内存分配机制分析。 代码说明: map所管理的内存地址可以是不连续的。如果key是可以通过<排序的,那么,map最后的结果是有序的。它是通过一个平衡二叉树来保存数据。所以,其查找效率极高。 实现方式:

限制条件或者存在的问题: 无

B树和B+树的区别

https://segmentfault.com/a/1190000020416577

自閉中

B+树相对于B树有一些自己的优势,可以归结为下面几点。

1.单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了。 2.所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。 3.所有的叶子节点形成了一个有序链表,更加便于查找。

什么是平衡二叉树

https://zhuanlan.zhihu.com/p/56066942

二叉搜索树,平衡二叉树,红黑树的区别

https://blog.csdn.net/Hansry/article/details/100537495

HashMap和Hashtable的區別

https://sziyu.pixnet.net/blog/post/30233792

HashMap 和 Hashtable 的比較是Java面試中的常見問題,用來考驗程序員是否能夠正確使用集合類以及是否可以隨機應變使用多種思路解決問題。HashMap的工作原理、ArrayList與Vector的比較以及這個問題是有關Java 集合框架的最經典的問題。Hashtable是個過時的集合類,存在於Java API中很久了。在Java 4中被重寫了,實現了Map接口,所以自此以後也成了Java集合框架中的一部分。Hashtable和HashMap在Java面試中相當容易被問到,甚至成為了集合框架面試題中最常被考的問題,所以在參加任何Java面試之前,都不要忘了準備這一題。

這篇文章中,我們不僅將會看到HashMap和Hashtable的區別,還將看到它們之間的相似之處。 HashMap和Hashtable的區別

HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。 HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。 另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。 由於Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那麼使用HashMap性能要好過Hashtable。 HashMap不能保證隨著時間的推移Map中的元素次序是不變的。

要注意的一些重要術語:

  1. sychronized意味著在一次僅有一個線程能夠更改Hashtable。就是說任何線程要更新Hashtable時要首先獲得同步鎖,其它線程要等到同步鎖被釋放之後才能再次獲得同步鎖更新Hashtable。

  2. Fail-safe和iterator迭代器相關。如果某個集合對象創建了Iterator或者ListIterator,然後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程可以通過set()方法更改集合對象是允許的,因為這並沒有從「結構上」更改集合。但是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

  3. 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。 我們能否讓HashMap同步?

HashMap可以通過下面的語句進行同步:

1
Map m = Collections.synchronizeMap(hashMap);

結論

Hashtable和HashMap有幾個主要的不同:線程安全以及速度。僅在你需要完全的線程安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

什么是哈希表,哈希函数,怎么解决冲突

哈希表是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

什么是hash冲突?

假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10

简单计算一下:hash(5)=5, 所以数据5应该放在hash表的第5个槽里;hash(28)=1,所以数据28应该放在hash表的第1个槽里;hash(19)=1,也就是说,数据19也应该放在hash表的第1个槽里——于是就造成了碰撞(也称为冲突,collision)。

常用的Hash冲突解决方法有以下几种:

1.开放定址法

这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:

Hi=(H(key)+di)% m i=1,2,…,n

其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:

线性探测再散列

dii=1,2,3,…,m-1

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

次探测再散列

di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )

这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

机探测再散列

di=伪随机数序列。

具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。

如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。

如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。

如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

再哈希法

这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key) i=1,2,…,k

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

3.链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

4.建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

拉链法与开放地址法相比的缺点:

拉链法的优点

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点

拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

哈希表構造方式

1.直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。

  1. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

  2. 平方取中法:当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。

  3. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。

  4. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,即H(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。

  5. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

順序表

顺序表是采用顺序存储结构的线性表,顺序表可理解为采用一维数组存储的线性结构(数组也是一种数据结构)顺序表的存储结果如下图所示:

https://img-blog.csdn.net/20180601233017339

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# include <stdio.h>
# include <stdlib.h> 
# include <malloc.h>
# include <string.h>
# define TRUE 1
# define FALSE 0
# define OK 1
# define ERROR 0
# define INFEASIBLE -1
# define OVERFLOW -2
# define LIST_INIT_SIZE 100
# define LISTINCREMENT 10
typedef int ElemType;
typedef int Status;
typedef struct 
{
	ElemType *elem;
	int length;
	int listsize;
}SqList;
void InitList(SqList &L)
{
	L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType));
	memset(L.elem,0,sizeof(ElemType)*LIST_INIT_SIZE);	//初始化为0
	L.length = 0;
	L.listsize = LIST_INIT_SIZE;
}
void DestroyList(SqList &L)
{
	free (L.elem);
}
void ClearList(SqList &L)
{
	L.length = 0;
}
Status ListEmpty(SqList L)
{
	if (L.length == 0)
		return TRUE;
	else
		return FALSE;
}
int ListLength(SqList L)
{
	return L.length;
}
Status GetElem(SqList L,int i,ElemType &e)
{
	//判断元素位置是否合法
    if(i > L.length-1 || i < 0){
        printf("查找的位置不正确 \n");
        return ERROR; 
    }
    //判断线性表是否为空
    if(ListEmpty(L)){
        return ERROR;
    } 
	e = L.elem[i];
	return OK;
}
Status LocateElem(SqList L,ElemType e)
{
	int i = 0;
	ElemType *p = L.elem;
	while(i<L.length)
	{
		if (*p != e)
		{
			p++;
			i++;
		}
		else 
			break;
	}
	if (i<L.length)
		return i;
	else 
		return -1;
}
Status PriorElem(SqList L,ElemType cur_e,ElemType &pre_e)
{
	if (LocateElem(L,cur_e)!=-1&&LocateElem(L,cur_e)!=0)
	{
		pre_e = L.elem[LocateElem(L,cur_e)-1];
		return OK;
	}
	else 
		return ERROR;
}
Status NextElem(SqList L,ElemType cur_e,ElemType &next_e)
{
	if (LocateElem(L,cur_e)!=-1&&LocateElem(L,cur_e)!=L.length-1)
	{
		next_e = L.elem[LocateElem(L,cur_e)+1];
		return OK;
	}
	else 
		return ERROR;
}
Status ListInsert(SqList &L,int i,ElemType e)
{

	if (i>0&&i<L.length+1)
	{

		if (L.length == L.listsize)
		{
			ElemType *newbase = (ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof (ElemType));
    		if (!newbase) exit(OVERFLOW);  // 存储分配失败           
    		L.elem = newbase;                // 新基址
   			L.listsize += LISTINCREMENT; // 增加存储容量
		}
		ElemType *q = &(L.elem[i]);
		for (ElemType *p = &(L.elem[L.length-1]);p>=q;p--)
		{
			*(p+1)=*p;
		}
		*q=e;
		L.length++;
		return OK;
	}
	else 
		return ERROR;
}
Status ListInsert(SqList &L,ElemType e)
{
	int i,j;
	for (j = 0; j < L.length; ++j)
	{
		if(L.elem[j]>=e)
		{
			i=j;
			break;
		}
	}
		if (L.length == L.listsize)
		{
			ElemType *newbase = (ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof (ElemType));
    		if (!newbase) exit(OVERFLOW);  // 存储分配失败           
    		L.elem = newbase;                // 新基址
   			L.listsize += LISTINCREMENT; // 增加存储容量
		}
		ElemType *q = &(L.elem[i]);
		for (ElemType *p = &(L.elem[L.length-1]);p>=q;p--)
		{
			*(p+1)=*p;
		}
		*q=e;
		L.length++;
		return OK;	
}
Status ListDelete(SqList &L,int i,ElemType &e)
{

	if (i>0&&i<L.length)
	{
		ElemType *q = &(L.elem[i]);
		e = *q;
		ElemType *p = &(L.elem[L.length-1]); 
		for(;q<p;q++)
		{
			*q=*(q+1);
		}
		L.length--;
		return OK;
	}
	else 
		return ERROR;
}
void ListTraverse(SqList L)
{
	for (int i = 0; i <L.length;i++)
		printf("%d ",L.elem[i]);
}
Status createList(SqList &L)
{
    int i;
  	InitList(L);
    for(i = 0; i < 10; i++){
        L.elem[i] = i;
    } 
    L.length = 10;
    return OK;
} 
void reverse(SqList &L)
{
	int i,j;
	int temp;
	for (int i = 0,j = L.length - 1; i < j; i++,j--)
	{
		temp = L.elem[i];
		L.elem[i] = L.elem[j];
		L.elem[j] = temp;
	}
}
Status DeleteK(SqList &a,int i,int k)
{
	if(i<1||k<0||i+k-1>a.length)
		return INFEASIBLE;
	int count;
	for ( count= 0; count < a.length-k; ++count)
	{
		a.elem[i-1+count] = a.elem[i-1+k+count];
	}
	a.length-=k;
}
void InsertSort(SqList &L)
{
	int i,j;
	ElemType temp;
	for(i=1; i<L.length; ++i)
	{
		if(L.elem[i]<L.elem[i-1])
		{
			temp = L.elem[i];
			for(j=i-1;L.elem[j]>=temp&&j>=0;--j)
				L.elem[j+1]=L.elem[j];
			L.elem[j+1] = temp;
		}
	 } 
}
Status MergeList(SqList &A,SqList &B)
{
	int i,j,flag;
	ElemType e;
	for (int i = 0; i < B.length; ++i)
	{	
		e = B.elem[i];				
		for (j = 0; j < A.length; ++j)
		{
			if(A.elem[j]>e)			//寻找插入点
			{
				flag = j;
				ListInsert(A,flag,e);
				break;
			}
			if (A.elem[j] == e)		//剔除重复元素
				break;
		}
		if(j == A.length)
			ListInsert(A,A.length,e);
	}
	return OK;	
}

順序棧

栈是一种先进后出的线性结构。只允许在栈的一端进行插入和删除操作,称为栈顶,栈的另一端称为栈底。栈顶的当前位置是动态变化的,由栈顶指针的位置指示,栈底指向栈的末尾。顺序栈使用顺序表实现,亦或者说是采用数组实现。顺序栈的示意图如下:

https://img-blog.csdn.net/2018060200260233

图示top==0 表示栈空的顺序栈,每次,入栈时先使元素入栈,然后栈顶指针+1,出栈时,先将栈顶指针top–,然后元素出栈。还可以用top==-1表示栈空,入栈时先使栈顶指针top++,然后元素入栈;出栈时先将栈顶指针top–,然后元素出栈。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
# define INFEASIBLE -1
# define OVERFLOW -2
#define STACK_INIT_SIZE 100
#define STACKINCREMENT 10
typedef int SElemType;
typedef int Status;
typedef struct 
{
	SElemType *base;
	SElemType *top;
	int stacksize;
}SqStack;
Status InitStack(SqStack &S)
{
	S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
	if (!S.base)
		exit(OVERFLOW);
	S.top = S.base;
	S.stacksize = STACK_INIT_SIZE;
	return OK;
}
Status DestroyStack(SqStack &S)
{
	if (!S.base)
		return ERROR;
	else
	{
		free(S.base);
		return OK;
	}
}
Status ClearStack(SqStack &S)
{
	if (!S.base)
		return ERROR;
	else
	{
		S.top = S.base;
		return OK;
	}
}
Status StackEmpty(SqStack S)
{
	if (S.base&&S.top == S.base)
		return TRUE;
	else
		return FALSE;
}
Status StackLength(SqStack S)
{
	if (S.base)
		return S.top-S.base;
	else 
		return ERROR;
}
Status GetTop(SqStack S, SElemType &e)
{
	if (S.top == S.base)
		return ERROR;
	else
	{
		e = *(S.top-1);
		return OK;
	}
}
Status Push(SqStack &S, SElemType e)
{
	if (S.top - S.base >= S.stacksize)
	{
		S.base = (SElemType *)realloc(S.base, (S.stacksize + STACKINCREMENT)*sizeof(SElemType));
		if (!S.base)
			exit(OVERFLOW);
		S.top = S.base + S.stacksize;
		S.stacksize += STACKINCREMENT;
	}
	*S.top = e;
	S.top++;
	return OK;
}
Status Pop(SqStack &S, SElemType &e)
{
	if (S.top == S.base)
		return ERROR;
	else
	{
		S.top--;
		e = *(S.top);
		return OK;
	}
}
Status StackTraverse(SqStack S)
{
	SElemType *p; 
	p = S.base;
	for (;p < S.top; p++)
		printf("%d ",*p);
}

鏈表

单链表是采用链式存储结构的线性表。数据元素存储在非连续的内存单元中,通过指针将各个内存单元链接在一起,最有一个节点的指针指向 NULL 。单链表不需要提前分配固定大小存储空间,当需要存储数据的时候分配一块内存并将这块内存插入链表中。

单链表是由一系列结点组成的,通过指针域把结点按照线性表中的逻辑元素连接在一起。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息,这个信息称为指针(pointer)或链(link)。这两部分组成了链表中的结点结构,单链表的结点结结构如下图:

https://img-blog.csdn.net/20180601170808432

使用箭头表示链表中的指针,单链表的可以表示成用箭头连接起来的结点序列,如下图所示:

https://img-blog.csdn.net/20180601234915158

图示为带头结点的单链表,实际上单链表是没有头结点的,头结点只是为了操作方便,在单链表的第一个结点前附设的一个结点使用头指针指向头结点。不带头结点的单链表,头指针指向第一个结点,判空条件为pHead == NULL;

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# include <stdio.h>
# include <stdlib.h> 
# include <malloc.h>
# include <string.h>
# define TRUE 1
# define FALSE 0
# define OK 1
# define ERROR 0
# define INFEASIBLE -1
# define OVERFLOW -2
typedef int ElemType;
typedef int Status;
typedef struct Node
{
    ElemType data;
    struct Node *next;
}Node;
typedef struct Node *LinkList; 
Status InitList(LinkList &L) 
{ 
    L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */
    if(L==NULL) /* 存储分配失败 */
            return ERROR;
    (L)->next=NULL; /* 指针域为空 */
    return OK;
}
void DestroyList(LinkList &L)
{
	LinkList p,q;
	p=L->next;           /*  p指向第一个结点 */
	while(p)                /*  没到表尾 */
	{
		q=p->next;
		free(p);
		p=q;
	}
	free(L);        /* 释放头节点的空间 */
}
Status ClearList(LinkList &L)/*只留下头节点,其余释放*/
{ 
	LinkList p,q;
	p=L->next;           /*  p指向第一个结点 */
	while(p)                /*  没到表尾 */
	{
		q=p->next;
		free(p);
		p=q;
	}			
	L->next=NULL; 
	return OK;
}
Status ListEmpty(LinkList L)
{
	if (L->next==NULL)
		return TRUE;
	else
		return FALSE;
}
int ListLength(LinkList L)
{
	int i=0;
	LinkList p=L->next;
	while(p)
	{
		i++;
		p=p->next;
	}
	return i;
}
Status GetElem(LinkList L,int i,ElemType *e)
{
	int j;
	LinkList p;		/* 声明一结点p */
	p = L->next;		/* 让p指向链表L的第一个结点 */
	j = 1;		/*  j为计数器 */
	while (p && j<i)  /* p不为空或者计数器j还没有等于i时,循环继续 */
	{   
		p = p->next;  /* 让p指向下一个结点 */
		++j;
	}
	if ( !p || j>i ) 
		return ERROR;  /*  第i个元素不存在 */
	*e = p->data;   /*  取第i个元素的数据 */
	return OK;
}
Status LocateElem(LinkList L,ElemType e)
{
	int i = 0;
	LinkList p=L->next;
	while(p)
	{
		i++;
		if (p->data!=e)
			p=p->next;
		else
			break;
	}
	if (i>0&&i<ListLength(L))
		return i;
	else
	{
		printf("查找失败!");
		return FALSE;
	}
}
Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e)
{
	LinkList p=L->next;
	if (cur_e==p->data)
	{
		printf("不存在前驱!");
		return ERROR;
	}
	else
	{
		while(p)
		{
			if (p->next->data==cur_e)
			{
				pre_e=p->data;
				break;
			}
			else
				p=p->next;
		}
	}	
	if (!p)
	{
		printf("无此数据!");
		return ERROR;
	}
}
Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e)
{
	LinkList p=L->next;
	while(p)
	{
		if (p->data==cur_e)
		{
 
			if (p->next!=NULL)
			{
				next_e=p->next->data;
				break;
			}
			else
			{
				printf("不存在后继!");
				return ERROR;
			}	
		}
		else
			p=p->next;
	}
	if (!p)
	{
		printf("无此数据!");
		return ERROR;
	}
}
Status ListInsert(LinkList &L,int i,ElemType e)
{
	LinkList p,s;
	p = L;   
	int j = 0;
	while (p && j < i-1)     /* 寻找第i-1个结点 */
	{
		p = p->next;
		++j;
	} 
	if (!p || j > i-1) 
		return ERROR;   /* 第i-1个元素不存在 */
	s = (LinkList)malloc(sizeof(Node)); 
	s->data = e;  
	s->next = p->next;      /* 将p的后继结点赋值给s的后继  */
	p->next = s;          /* 将s赋值给p的后继 */
	return OK;
}
Status ListDelete(LinkList L,int i,ElemType &e)
{
	int j;
	LinkList p,q;
	p = L;
	j = 0;
	while (p->next && j < i-1)	/* 寻找前驱,即第i-1个元素 */
	{
        p = p->next;
        ++j;
	}
	if (!(p->next) || j > i) 
	    return ERROR;           /* 第i个元素不存在 */
	q = p->next;
	p->next = q->next;			/* 将q的后继赋值给p的后继 */
	e = q->data;               /* 将q结点中的数据给e */
	free(q);                    /* 让系统回收此结点,释放内存 */
	return OK;
}
void creatList(LinkList &L)
{
	int i;
	LinkList p;
	InitList(L);
	for (i = 9; i >=0; i--)
	{
		p = (LinkList)malloc(sizeof(Node));
		p->data = i;
		p->next = L->next;
		L->next = p;
	}
}
void ListTraverse(LinkList L)
{
	LinkList p=L->next;  
    while(p)  
    {  
        printf("%d ",p->data); 
        p=p->next; 
    }    
}
void reverse(LinkList &L)
{
	LinkList p,q;
	p = L->next;L->next = NULL;
	while(p)
	{
		q = p->next;
		p->next = L->next;
		L->next = p;
		p = q;
	}
}
void InsertSort(LinkList &L)
{
	LinkList p,q,temp,p_prior;
	LinkList first;
	first = L->next->next;
	L->next->next = NULL;
	while(first)
	{
		p_prior = L;
		p = p_prior->next;		//重置为L的表头
		temp = first;
		first = first->next;
		temp->next = NULL;
		while(p->next!=NULL&&p->data < temp->data)
		{
			p_prior = p;
			p = p->next;
		}
		if(p->data >= temp->data)
		{
			temp->next = p_prior->next;
			p_prior->next = temp;
		}
		else 
		{
			temp->next = NULL;
			p->next = temp;
		}
	}
}
Status MergeList(LinkList &A,LinkList &B)
{
	int i,j;
	ElemType e;
	LinkList p = B->next;
	LinkList q;
	while(p)
	{
		e = p->data;
		j = 1;
		q = A->next;				
		while(q)
		{
			if(q->data>e)			//寻找插入点
			{
				ListInsert(A,j,e);
				break;
			}
			if (q->data == e)		//剔除重复元素
				break;
			j++;
			q = q->next;
		}
		if(q == NULL)
			ListInsert(A,j,e);
		p = p->next;
	}
	return OK;	
}
Status LOCATE(LinkList &L,int x)
{
	LinkList p,q;
	p = L->next;
	q = L->next;
	while(p->data!=x&&p!=L)
		p = p->next;
	if(p == L)
		return ERROR;
	else
	{
		p->freq++;
		while(q!=L&&q->freq>=p->freq)
			q = q->next;
		if(q!=p->next)
		{
			p->pre->next = p->next;
			p->next->pre = p->pre;
			p->pre = q->pre;
			q->pre->next = p;
			q->pre = p;
			p->next = q;	
		} 	
	} 
	return OK; 
}

鏈棧

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
# define INFEASIBLE -1
# define OVERFLOW -2
#define STACK_INIT_SIZE 100
#define STACKINCREMENT 10
typedef int SElemType;
typedef int Status;
typedef struct Node
{
	SElemType data;
	struct Node *next;
}StackNode;
typedef struct Node *LinkStack; 
Status InitStack(LinkStack &top)
{
	top = (LinkStack)malloc(sizeof(StackNode));
	if (top == NULL)
		exit(OVERFLOW);
	else
	{
		top->next = NULL;
		return OK;
	}
}
Status DestroyStack(LinkStack &top)
{
	LinkStack p;
	if (!top)	
		return INFEASIBLE;
	else
	{
		while(top)
		{
			p = top;
			top = top->next;
			free(p);
		}
		return OK;
	}
}
Status ClearStack(LinkStack &top)
{
	LinkStack p = top->next, q;
	if (!top)
		return INFEASIBLE;
	else
	{
		while(p)
		{
			q = p->next;
			free(p);
			p = q;
		}
		top->next = NULL;
	}
}
Status StackEmpty(LinkStack top)
{
	if (!top)
		return INFEASIBLE;
	else if (top->next == NULL)
		return TRUE;
	else
		return FALSE;
}
Status StackLength(LinkStack top)
{
	LinkStack p;
	int i = 0; 
	if (!top)
		return INFEASIBLE;
	else 
	{
		p = top->next;
		while(p)
		{
			i++;
			p = p->next;
		}
	}
	return i;
}
Status GetTop(LinkStack top, SElemType &e)
{
	if (!top)
		return INFEASIBLE;
	else 
	{
		if(top->next != NULL)
		{
			e = top->next->data;
			return OK;
		}
		else
			return ERROR;
	}
}
Status Push(LinkStack &top, SElemType e)
{
	if (!top)
		return INFEASIBLE;
	else
	{
		LinkStack p = (LinkStack)malloc(sizeof(StackNode));
		p->data = e;
		p->next = top->next;
		top->next = p;
		return OK;
	}
}
Status Pop(LinkStack &top, SElemType &e)
{
	LinkStack p;
	if (!top||!top->next)
		return INFEASIBLE;
	else
	{
		p = top->next;
		top->next = p->next;
		e = p->data;
		free(p);
		return OK;
	}
}
Status StackTraverse(LinkStack top)
{
	LinkStack p = top->next; 
	while(p)
	{
		printf("%d ",p->data);
		p = p->next;
	}
}

單鏈隊列

队列是一种先进先出的线性结构,只允许在表的一端进行插入和删除操作,当然:双端队列除外,允许插入的一端称为队尾,允许删除的一端称为队头。

https://img-blog.csdn.net/2018060201010485

由于在入队和出队的过程中队头指针和队尾指针只增加不减小,致使被删除元素的空间无法被重新利用,因此,可能会存在这样一种情况:尽管,队列中实际元素个数远远小于数组大小(队列长度)但可能尾指针已超出数组空间的上界,而不能进行入队操作,这种现象,称之为“假溢出”。

为了充分利用存储空间,消除这种”假溢出”,可以采用的方法是:将为队列分配的空间看成为一个首尾相接的圆环,并称这种队列为循环队列。 在循环队列中当队尾指针rear 达到最大值Maxsize - 1 时,其队尾指针加1操作,使其指向队头指针,这一过程可以使用数学中的取余运算来实现。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include<stdio.h>
#include<stdlib.h>
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
# define INFEASIBLE -1
# define OVERFLOW -2
typedef int ElemType;
typedef int Status;
typedef struct QNode
{
	ElemType data;
	struct QNode *next;
}QNode,* QueuePtr;
typedef struct
{
	QueuePtr front;	//队头指针
	QueuePtr rear;	//队尾指针
}LinkQueue;
Status InitQueue(LinkQueue &Q)
{
	Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
	if (!Q.front)
		exit(OVERFLOW);
	Q.front->next = NULL;
	return OK;
}
Status DestroyQueue(LinkQueue &Q)
{
	while(Q.front)
	{
		Q.rear = Q.front->next;
		free (Q.front); 
		Q.front = Q.rear;
	}
	return OK;
}
Status ClearQueue(LinkQueue &Q)
{
	QueuePtr p,q;
	p = Q.front->next;
	while(p)
	{
		q = p->next;
		free (p);
		p = q;
	}
	Q.rear = Q.front;
}
Status QueueEmpty(LinkQueue Q)
{
	if (Q.front == Q.rear)
		return TRUE;
	else
		return FALSE;
}
int QueueLength(LinkQueue Q)
{
	QueuePtr p;
	int i = 0;
	p = Q.front->next;
	while(p)
	{
		i++;
		p = p->next;
	}
	return i;
}
Status GetHead(LinkQueue Q, ElemType &e)
{
	if (!QueueEmpty(Q))
	{
		Q.front = Q.front->next;
		e = Q.front->data;
		return OK;
	}
	else
		return ERROR;
}
Status EnQueue(LinkQueue &Q, ElemType e)
{
	QueuePtr p = (QueuePtr)malloc(sizeof(QNode));
	if(!p)
		exit(OVERFLOW);
	p->data = e;
	p->next = NULL;
	Q.rear->next = p;
	Q.rear = p;
	return OK;
}
Status DeQueue(LinkQueue &Q, ElemType &e)
{
	QueuePtr p;
	if (Q.front == Q.rear)
		return ERROR;
	else
	{
		p = Q.front->next;
		e = p->data;
		Q.front->next = p->next;
		if (Q.rear == p)	//出队结点为队尾结点
			Q.rear = Q.front;
		free (p);
		return OK;
	}
}
Status QueueTraverse(LinkQueue Q)
{
	QueuePtr p;
	p = Q.front->next;
	while(p)
	{
		printf("%d ",p->data);
		p = p->next;
	}
}

二插樹

二叉树(Binary Tree)是包含n个节点的有限集合,该集合或者为空集(此时,二叉树称为空树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。

一棵典型的二叉树如下图所示:

https://img-blog.csdn.net/20170115182928565?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29vZ2xlMTk4OTAxMDI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

由上述的定义可以看出,二叉树中的节点至多包含两棵子树,分别称为左子树和右子树,而左子树和右子树又分别至多包含两棵子树。由上述的定义,二叉树的定义是一种递归的定义。

一些常见的二叉树

  • 满二叉树

对于一棵二叉树,如果每一个非叶子节点都存在左右子树,并且二叉树中所有的叶子节点都在同一层中,这样的二叉树称为满二叉树。

一棵满二叉树如下图所示:

https://img-blog.csdn.net/20170115203230653?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29vZ2xlMTk4OTAxMDI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

  • 完全二叉树

对于一棵具有n个节点的二叉树按照层次编号,同时,左右子树按照先左后右编号,如果编号为i的节点与同样深度的满二叉树中编号为i的节点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。

一棵完全二叉树如下图所示:

https://img-blog.csdn.net/20170115203812672?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29vZ2xlMTk4OTAxMDI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

二叉树的一些性质

对于二叉树,包含一些性质:

  • 在二叉树中,第 i层上至多有$2^(i−1)$个节点(i≥1)
  • 深度为k的二叉树至多有$2^(k−1)$个节点(k≥1)
  • 对一棵二叉树,如果叶子节点的个数为$n_0$,度为2的节点个数为$n_2$,则$n_0=n_2+1$
  • 具有n个节点的完全二叉树的深度为$⌊log_2 n⌋+1$
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#include <iostream>
#include <stdlib.h>
#include <stack>
#include <queue> 
using namespace std;
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef char ElemType;
typedef int Status;
typedef struct BiTNode
{
	ElemType data;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
typedef struct
{	
	BiTree ptr; 
	bool tag;		//0为左子数 1为右子树 
}stacknode;
Status CreateBiTree(BiTree &T)
{
	char ch;
	scanf("%c",&ch);
	if(ch == ' ')
		T = NULL;
	else
	{
		if(!(T = (BiTNode *)malloc(sizeof(BiTNode))))
			exit(OVERFLOW);
		T->data = ch;
		CreateBiTree(T->lchild);
		CreateBiTree(T->rchild);
	}
	return OK;
}
int BiTreeDepth(BiTree &T)			//求树的深度 
{
	if(T == NULL)
		return 0;
	else
	{
		int lDepth,rDepth;
		lDepth = BiTreeDepth(T->lchild);
		rDepth = BiTreeDepth(T->rchild);
		if(lDepth > rDepth)
			return lDepth + 1;
		else
			return rDepth + 1;
	 } 
}
int BiTreeWidth(BiTree &T)
{
	if(T == NULL)
		return 0;
	int nLastLevelWidth = 0;//记录上一层的宽度  
    int nTempLastLevelWidth = 0;  
    int nCurLevelWidth = 0;//记录当前层的宽度  
    int nWidth = 1;//二叉树的宽度  
    BiTree cur;
    queue<BiTree> q;  
    q.push(T);
    nLastLevelWidth = 1;
    nWidth = 1;
    while(!q.empty())
    {
   		nTempLastLevelWidth = nLastLevelWidth;  
        while (nTempLastLevelWidth != 0)  
        {  
            cur = q.front();//取出队列头元素  
            q.pop();		//将队列头元素出对  
            if (cur->lchild != NULL)  
            {  
                q.push(cur->lchild);  
            }  
            if (cur->rchild != NULL)  
            {  
                q.push(cur->rchild);  
            }  
            nTempLastLevelWidth--;  
        }  
        nCurLevelWidth = q.size();  
        nWidth = nCurLevelWidth > nWidth ? nCurLevelWidth : nWidth;  
        nLastLevelWidth = nCurLevelWidth;  
    }  
    return nWidth;  
 } 
void PreOrderTraverse(BiTree T)
{
	stack<BiTree> s;
	BiTree p;
	p = T;
	while(p != NULL||!s.empty())
	{
		while(p != NULL)
		{
			printf("%c ", p->data);
			s.push(p);
			p = p->lchild;
		}
		if(!s.empty())
		{
			p = s.top();
			s.pop();
			p = p->rchild;
		}
	}
}
void InOrderTraverse(BiTree T)
{
	stack<BiTree> s;
	BiTree p;
	p = T;
	while(p != NULL||!s.empty())
	{
		while(p != NULL)
		{
			s.push(p);
			p = p->lchild;
		}
		if(!s.empty())
		{
			p = s.top();
			s.pop();
			printf("%c ", p->data);
			p = p->rchild;
		}
	}
}
void PostOrderTraverse(BiTree T)
{
	stacknode x;
	stack<stacknode> s;
	BiTree p;
	p = T;
	while(p)
	{
		x.ptr = p;
		x.tag = 0;
		s.push(x);
		p = p->lchild;
	}
	while(!s.empty()){
		x = s.top();
		if(x.ptr->rchild == NULL||x.tag == 1){	//没有右子树或者右子树已经访问过 
			s.pop();
			printf("%c ",x.ptr->data);
		}
		else{
			s.top().tag = 1;
			p = x.ptr->rchild;
			while(p)
			{
				x.ptr = p;
				x.tag = 0;
				s.push(x);
				p = p->lchild;
			 } 
		}
	}
}
void LevelOrderTraverse(BiTree T)		//层次遍历 
{
	BiTree p;
    p = T;
    if (!T) {
        return;
    }
    queue<BiTree> Q; 
    Q.push(p);
    while (!Q.empty()) {
        p = Q.front();
        Q.pop();
        printf("%c ",p->data);
        if (p->lchild)
        	Q.push(p->lchild);
        if (p->rchild)
        	Q.push(p->rchild);
    }
}
void PreOrderTraverse_recursion(BiTree T)
{
	if(T!=NULL)
    {
        printf("%c ",T->data);
        PreOrderTraverse_recursion(T->lchild);
        PreOrderTraverse_recursion(T->rchild);
    }
}
void InOrderTraverse_recursion(BiTree T)      //递归中序遍历
{
    if(T!=NULL)
    {
        InOrderTraverse_recursion(T->lchild);
        printf("%c ",T->data);
        InOrderTraverse_recursion(T->rchild);
    }
}
void PostOrderTraverse_recursion(BiTree T)    //递归后序遍历
{
    if(T!=NULL)
    {
        PostOrderTraverse_recursion(T->lchild);
        PostOrderTraverse_recursion(T->rchild);
        printf("%c ",T->data); 
    }    
} 
int LeafNode(BiTree T)							//求叶子结点个数
{
	if(T == NULL)
		return 0;
	if(T->lchild == NULL && T->rchild == NULL) 
		return 1;
	return LeafNode(T->lchild) + LeafNode(T->rchild);
}
void exchange(BiTree T)							//交换左右子树 
{
	BiTree temp = NULL;
 	if(T->lchild == NULL && T->rchild == NULL)
        return;
 	else{
       temp = T->lchild;
       T->lchild = T->rchild;
       T->rchild = temp;
 	}
 	if(T->lchild)
      	exchange(T->lchild);
 	if(T->rchild)
      	exchange(T->rchild);
}
Status IsCompleteBinaryTree(BiTree T) 
{
	bool flag = FALSE;
	BiTree p;
    p = T;
    if (!T) {
        return FALSE;
    }
    queue<BiTree> Q; 
    Q.push(p);
    while (!Q.empty()) {
    	if(flag == TRUE)
    	{
    		if(p->lchild != NULL || p->rchild != NULL)
    			return FALSE;
		}
        p = Q.front();
        Q.pop();
        if(p->lchild == NULL && p->rchild != NULL)
        	return FALSE;
        if((p->lchild != NULL && p->rchild == NULL) || (p->lchild == NULL && p->rchild == NULL))
        	flag = TRUE;
		if (p->lchild)
        	Q.push(p->lchild);
        if (p->rchild)
        	Q.push(p->rchild);
    }
    return TRUE;
}
void CopyTree(BiTree S,BiTree &T){
    if (!S) T=NULL;
    else{
    	BiTree lptr,rptr;
        CopyTree(S->lchild,lptr);//复制左子树到lptr
        CopyTree(S->rchild,rptr);//复制右子树到rptr
        T = (BiTree)malloc(sizeof(BiTNode));
        T->data = S->data;
        T->lchild = lptr;
		T->rchild = rptr;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <iostream>
using namespace std;

int capacity = 10;

//最大堆
void insertHeap(int *heap, int newValue)
{
    if (heap[0] >= capacity)
    {
        cout << "the heap is full" << endl;
        exit(0);
    }

    heap[0]++;
    heap[heap[0]] = newValue;
    int index = heap[0];
    int parentIndex = heap[0] / 2;
    while (index != 1 && heap[parentIndex] < heap[index])
    {
        int tmp = heap[parentIndex];
        heap[parentIndex] = heap[index];
        heap[index] = tmp;
        index = parentIndex;
        parentIndex = index / 2;
    }
}

int deleteMax(int *heap)
{
    if (heap[0] == 0)
    {
        cout << "the heap is empty";
        exit(0);
    }

    int max = heap[1];
    heap[1] = heap[heap[0]];
    heap[0]--;
    int index = 1;
    int leftIndex = index * 2;
    int rightIndex = leftIndex + 1;
    int maxIndex = heap[rightIndex] > heap[leftIndex] ? rightIndex : leftIndex;

    while (heap[index] < heap[maxIndex] && index <= heap[0] - (heap[0]+1) / 2)
    {
        int tmp = heap[index];
        heap[index] = heap[maxIndex];
        heap[maxIndex] = tmp;
        index = maxIndex;
        leftIndex = index * 2;
        rightIndex = leftIndex + 1;
        maxIndex = heap[rightIndex] > heap[leftIndex] ? rightIndex : leftIndex;
    }

    return max;
}
void printHeap(int *heap)
{

    for (int i = 1; i <= heap[0]; i++)
    {
        cout << heap[i] << " ";
    }
    cout << endl;
}
int main()
{
    int *heap = new int[capacity + 1];
    heap[0] = 0;
    insertHeap(heap, 1);
    insertHeap(heap, 8);
    insertHeap(heap, 10);
    insertHeap(heap, 3);
    printHeap(heap);
    cout << deleteMax(heap) << endl;
    printHeap(heap);
    delete[] heap;
} 

數據庫

熟悉基本 SQL 操作

我的blog筆記 : https://huangno1.github.io/mysql_learn_note_basic/

1.包括增删改查(insert、delete、update、select语句),排序 order,条件查询(where 子语句),限制查询结果数量(LIMIT语句)等 2.稍微高级一点的 SQL 操作(如Group by,in,join,left join,多表联合查询,别名的使用,select 子语句等) 3.索引的概念、索引的原理、索引的创建技巧 4.数据库本身的操作,建库建表,数据的导入导出 5.数据库用户权限控制(权限机制)

SQL 优化技巧

https://www.jianshu.com/p/25c958196a0b

1、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

2、对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

3、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num is null

可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

select id from t where num=0

4、尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num=10 or num=20

可以这样查询:

select id from t where num=10

union all

select id from t where num=20

5、下面的查询也将导致全表扫描:(不能前置百分号)

select id from t where name like ‘�c%’

若要提高效率,可以考虑全文检索。

6、in 和 not in 也要慎用,否则会导致全表扫描,如:

select id from t where num in(1,2,3)

对于连续的数值,能用 between 就不要用 in 了:

select id from t where num between 1 and 3

7、如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

select id from t where num=@num

可以改为强制查询使用索引:

select id from t with(index(索引名)) where num=@num

8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num/2=100

应改为:

select id from t where num=100*2

9、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where substring(name,1,3)=’abc’–name以abc开头的id

select id from t where datediff(day,createdate,’2005-11-30′)=0–’2005-11-30′生成的id

应改为:

select id from t where name like ‘abc%’

select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′

10、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。

12、不要写一些没有意义的查询,如需要生成一个空表结构:

select col1,col2 into #t from t where 1=0

这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:

create table #t(…)

13、很多时候用 exists 代替 in 是一个好的选择:

select num from a where num in(select num from b)

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

14、并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段 sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

15、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

17、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

18、尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

19、任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

20、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

21、避免频繁创建和删除临时表,以减少系统表资源的消耗。

22、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使 用导出表。

23、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。

29、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

30、尽量避免大事务操作,提高系统并发能力。

Mysql中有哪两种存储类型有什么区别?

https://blog.csdn.net/qq_35181209/article/details/78030110

InnoDB:

(1)具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。

(2)支持外键。

(3)InnoDB 中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行。注意的是,当count()语句包含 where条件时,两种表的操作是一样的。

(4)对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引。

(5)DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。

MyISAM:

(1)不支持事务操作。

(2)不支持外键。

(3)MyISAM保存表的具体行数,执行select count(*) from table时只要简单的读出保存好的行数即可。

(4)对于AUTO_INCREMENT类型的字段,在MyISAM表中,可以和其他字段一起建立联合索引。

(5)MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个MyISAM表的读锁,同时另一 个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!

myisam是有读锁和写锁(2个锁都是表级别锁)。

MySQL表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。什么意思呢,就是说对MyISAM表进行读操作时,它不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写操作;而对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作。

innodb,所以为什么使用B+树,而不用B树,在InnoDB中存储数据用什么作为主键?可不可以使用UUID,为什么?

自閉中

为什么mysql innodb索引是B+树数据结构

https://blog.csdn.net/xuehuagongzi000/article/details/78985844?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=1328696.1340.16166728491645397&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

一、为什么mysql innodb索引是B+树数据结构?言简意赅,就是因为: 1.文件很大,不可能全部存储在内存中,故要存储到磁盘上 2.索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。) 3、B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来,这样遍历叶子节点就能获得全部数据。

二、什么是聚簇索引? 像innodb中,主键的索引结构中,既存储了主键值,又存储了行数据,这种结构称为”聚簇索引”

三、为什么MongoDB采用B树索引,而Mysql用B+树做索引 先从数据结构的角度来答。 题主应该知道B-树和B+树最重要的一个区别就是B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域。 这就决定了B+树更适合用来存储外部数据,也就是所谓的磁盘数据。 从Mysql(Inoodb)的角度来看,B+树是用来充当索引的,一般来说索引非常大,尤其是关系性数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。 那么Mysql如何衡量查询效率呢?磁盘IO次数,B-树(B类树)的特定就是每层节点数目非常多,层数很少,目的就是为了就少磁盘IO次数,当查询数据的时候,最好的情况就是很快找到目标索引,然后读取数据,使用B+树就能很好的完成这个目的,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时啊!),原因1:B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。 原因2:B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据。

至于MongoDB为什么使用B-树而不是B+树,可以从它的设计角度来考虑,它并不是传统的关系性数据库,而是以Json格式作为存储的nosql,目的就是高性能,高可用,易扩展。首先它摆脱了关系模型,上面所述的优点2需求就没那么强烈了,其次Mysql由于使用B+树,数据都在叶节点上,每次查询都需要访问到叶节点,而MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于Mysql(但侧面来看Mysql至少平均查询耗时差不多)。

总体来说,Mysql选用B+树和MongoDB选用B-树还是以自己的需求来选择的。

MySQL InnoDB索引的存储结构

https://zhuanlan.zhihu.com/p/110178282

数据库三大范式是什么

第一范式:每个列都不可以再拆分。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

什么是索引?

索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

索引有哪些优缺点?

索引的优点

  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

索引的缺点

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
  • 空间方面:索引需要占物理空间。

索引使用场景(重点)

where

select * from innodbl where id < 20 根据id查询记录,因为 id 字段仅建立了主键索引,因此此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。

1
2
3
4
-- 增加一个没有建立索引的字段
alter table innodb1 add sex char(1);
-- 按sex检索时可选的索引为null
EXPLAIN SELECT * from innodb1 where sex='男';

可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(alter table 表名 add index(字段名)),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。

order by

当我们使用 order by 将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。

但是如果我们对该字段建立索引 alter table 表名 add index(字段名),那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的)

join

join 语句匹配关系(on)涉及的字段建立索引能够提高效率

索引覆盖

如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在 select只写必要的查询字段,以增加索引覆盖的几率。

这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。

索引有哪几种类型?

主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引

  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引

普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

  • 可以通过 ALTER TABLE table_name ADD INDEX index_name (column); 创建普通索引

  • 可以通过 ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3); 创建组合索引

全文索引: 是目前搜索引擎使用的一种关键技术。

  • 可以通过 ALTER TABLE table_name ADD FULLTEXT (column); 创建全文索引

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理很简单,就是把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序

  2. 对排序结果生成倒排表

  3. 在倒排表内容上拼上数据地址链

  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引算法有哪些?

索引算法有 BTree 算法和 Hash 算法

BTree算法

BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:

1
2
3
4
-- 只要它的查询条件是一个不以通配符开头的常量
select * from user where name like 'jack%'; 
-- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如: 
select * from user where name like '%jack'; 

Hash算法

Hash Hash 索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

索引设计的原则?

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
  2. 基数较小的类,索引效果较差,没有必要在此列建立索引
  3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则(重中之重)

索引虽好,但也不是无限制的使用,最好符合一下几个原则

1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2)较频繁作为查询条件的字段才去创建索引

3)更新频繁字段不适合创建索引

4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即 6)定义有外键的数据列一定要建立索引。

7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8)对于定义为text、image和bit的数据类型的列不要建立索引。

什么是数据库事务?

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。

假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

事物的四大特性(ACID)介绍一下?

关系性数据库需要遵循ACID规则,具体内容如下:

https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzIwLzE2MzdiMDhiOTg2MTk0NTU?x-oss-process=image/format,png

  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

什么是事务的隔离级别?MySQL的默认隔离级别是什么?

为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED O O O
READ-COMMITTED X O O
REPEATABLE-READ X X O
SERIALIZABLE X X X

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容),但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。

InnoDB 存储引擎在分布式事务的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。

按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法

在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎)

MyISAM 和 InnoDB 存储引擎使用的锁:

  • MyISAM采用表级锁(table-level locking)。
  • InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

行级锁,表级锁和页级锁对比

行级锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。

特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

表级锁

表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。

页级锁

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

从锁的类别上分MySQL都有哪些锁呢?

像上面那样子进行锁定岂不是有点阻碍并发效率了

从锁的类别上来讲,有共享锁和排他锁。

共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。

排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。

锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。

他们的加锁开销从大到小,并发能力也是从大到小。

数据库的乐观锁和悲观锁是什么?怎么实现的?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适

主键使用自增ID还是UUID?

推荐使用自增ID,不要使用UUID。

因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。

总之,在数据量大一些的情况下,用自增主键性能会好一些。

关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键。

操作系統

进程和线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

**资源开销:**每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立

**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

进程间通讯方式

进程间通信的概念

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

https://upload-images.jianshu.io/upload_images/1281379-76c95f147203c797.png?imageMogr2/auto-orient/strip|imageView2/2/w/222

內容太多,請直接看這個網站 https://www.jianshu.com/p/c1015f5ffa74

簡要回答

1,管道分为命名管道和无名管道,在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,都可以看成一种特殊的文件,具有固定的读端和写端,也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中;无名管道一般使用fork函数实现父子进程的通信,命名管道用于没有血缘关系的进程也可以进程间通信;面向字节流、自带同步互斥机制、半双工,单向通信,两个管道实现双向通信。

2,消息队列,在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列;消息队列独立于发送与接收进程,可以通过顺序和消息类型读取,也可以fifo读取;消息队列可实现双向通信 。

3,信号量 , 在内核中创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1,通过对临界资源进行保护实现多进程的同步

4,共享内存,将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。目前最快的IPC形式,不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以,共享内存是临界资源,所以需要操作时必须要保证原子性。使用信号量或者互斥锁都可以。

5,socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据。socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种”打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个”文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。是一种可以网间通信的方式。

进程切换的上下文细节

https://zhuanlan.zhihu.com/p/52845869

在 Linux 这边,线程基本上是用户态使用的。内核态很少用这个词,调度单位叫 task 然后用户态所谓的进程,内核里叫 task group 然后用户态里也可以实现自己的线程,也就是所谓的绿色线程、协程之类,也有把这种东西叫进程的,比如 Erlang

什么是 CPU 上下文

CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。

CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

什么是 CPU 上下文切换

就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

CPU 上下文切换的类型

根据任务的不同,可以分为以下三种类型 - 进程上下文切换 - 线程上下文切换 - 中断上下文切换

进程上下文切换

Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。

内核空间(Ring 0)具有最高权限,可以直接访问所有资源;用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

https://pic2.zhimg.com/80/v2-d831951a5e41bbfb1e0e1151a8a2b649_720w.jpg

进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

系统调用

从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。

在这个过程中就发生了 CPU 上下文切换,整个过程是这样的: 1、保存 CPU 寄存器里原来用户态的指令位 2、为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。 3、跳转到内核态运行内核任务。 4、当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。

所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)

不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。

所以,系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。

进程上下文切换跟系统调用又有什么区别呢

首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

如下图所示,保存上下文和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。

https://pic3.zhimg.com/80/v2-440bb1699b2fa0f0340b38eabcbd7452_720w.jpg

进程上下文切换潜在的性能问题

根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是导致平均负载升高的一个重要因素。

另外,我们知道, Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。

发生进程上下文切换的场景

1.为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。 2.进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。 3.当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行 4.发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。

线程上下文切换

线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。

所以,对于线程和进程,我们可以这么理解: - 当进程只有一个线程时,可以认为进程就等于线程。 - 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。 - 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

发生线程上下文切换的场景

1.前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。 2.前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。

跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。

另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。

线程切换的上下文细节

答案在上面的問題

Linux 用户态切换到内核态的 3 种方式

https://zhuanlan.zhihu.com/p/279354447

  • 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如 fork() 实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如 Linux 的 int 80h 中断。

  • 中断

当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

  • 异常

当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

https://pic4.zhimg.com/80/v2-30923424421b862e06320403402ae7a3_720w.jpg

当时大神们在写 Linux 内核的时候,估计还不知道将来虚拟机会大放异彩,大神们想,操作系统一共分两级特权,一个内核态,一个用户态,而 CPU 却有四个等级,好奢侈,好富裕,就敞开了用,内核态运行在第 0 等级,用户态运行在第 3 等级,占了两头,太不会过日子了。

如果用户态程序做事情,就将扳手掰到第 3 等级,一旦要申请使用更多的资源,就需要申请将扳手掰到第 0 等级,内核才有高权限访问这些资源,申请完资源,返回到用户态,扳手再掰回去,这个程序一直非常顺利的运行着,直到虚拟机的出现。

什么是协程

官方定义如下:

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

用我蹩脚的英语来翻译一下就是: 学过计算机组成原理的都知道,当 CPU 在多个进程间切换时,那些后台程序就会处于这种暂停用英文的 Suspend 或许更恰当)的状态,所以早年的电脑即使用一个 CPU 也可以同时处理多个进程任务,这是一种“伪多线程”的技术。 除此之外比较重要的一点是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程那样需要上下文切换来消耗资源,因此协程的开销远远小于线程的开销。 注意,这里要划一个重点,协程是一种“伪多线程”,始终记得这一点,可以帮助我们来理解协程会这个概念。

线程同步的机制

http://kaiyuan.me/2015/08/20/thread_sync/

当一个程序涉及到多个线程的时候,线程之间的同步和互斥就显得尤其重要。为了避免一个线程中的敏感数据被另一个线程修改,程序中往往需要显式的使用线程同步机制来保证线程运行环境的安全。当前的编程库环境(例如pthread库)就为多线程的同步和互斥提供了多种不同的机制,包括互斥锁,读写锁,自旋锁,条件变量和信号量等等。本文简要记载使用不同的同步机制时应该注意的问题。

互斥锁

互斥锁指的就是不同的锁之间相互排斥。对于某个临界区而言,任何时间都只能有一个互斥锁的锁住该临界区。当该互斥锁未释放之前,任何其他企图对该临界区进行加锁访问的线程都会被挂起而阻塞。等到该临界区上的锁释放的时候,其他进程才能被唤醒来访问该临界区。

在使用互斥锁的时候需要注意以下几个问题:

但临界区非常下的时候,一般不适合使用互斥锁。原因是互斥锁一旦加锁失败往往会将线程挂起阻塞,而线程的上下文切换也是需要开销的。如果临界区比较小并且需要频繁加锁,那么使用自旋锁更合适一些。(当然网上有资料表明pthread中的互斥锁mutex在加锁失败之后会自旋一段时间,然后再阻塞,基本代替了自旋锁的作用。但由于本文只讨论不同锁的特性,不依赖于具体的实现,因此还是加上这条)。

使用互斥锁的时候及其容易引起死锁。如果加锁顺序和释放锁的顺序不当,互斥锁是比较容易引起死锁的。解决死锁问题一方面可以通过在程序编写中小心注意而避免,另一方面可以通过非阻塞的申请锁的方式而避免。非阻塞申请锁一般在申请失败后就直接返回而不阻塞,避免了因为阻塞而引起的死锁。

任何同步的场景都可以使用互斥锁等价的实现,但是仅仅使用互斥锁有时候严重影响性能,比如下文要提到的读写锁就可以提高临界区访问的并发性。

读写锁

在上文中已经提到,尽管互斥锁会极大地简化线程并发中所涉及的到的临界区访问的控制逻辑,在某些场合,其可能会极大地影响到线程并发的效率,例如在多线程并发读写临界区,并且读多写少得场景下。

直观来看,允许多个read-only的线程并发的访问数据并不会影响程序的运行逻辑,因为他们不会对读取的数据进行任何修改,而要写数据的线程与其他的线程则需要进行绝对的并发控制。读写锁就是在这种背景下产生的。读写锁一般分为读锁和写锁,其互斥逻辑如下:1)同一个临界区可以同时加上多个读锁;2)同一个临界区有且只能有一个写锁;3)读锁和写锁不能共存与一个临界区。

在了解这些特性之后可以看到,当一个线程已经给某个临界区加上读锁之后,后续的读线程仍然能够给此临界区加上读锁进行访问。只有当临界区上的所有锁释放之后,写线程才能成功的给临界区加上写锁。这种逻辑会带来一个直观的问题,就是写线程的饥饿:一旦不同读线程持续不断的给临界区加锁,那么写线程将永远没有机会进入临界区。因此,在使用读写锁的同时需要给予写线程比较高的优先级,从而防止写线程的饥饿。

自旋锁

自旋锁可以理解成为“空转锁”。相比于互斥锁而言,自旋锁一旦加锁失败,其线程并不会被调度挂起进入阻塞状态,而是阻塞在一个空循环上。这种性质决定了自旋锁节省了线程切换的开销,却浪费了CPU时间。(阿里的笔试题就考到了这个问题)

在使用自旋锁的时候需要注意的是,加锁的临界区要尽量的小。如果临界区很长需要花费很多时间,那么大部分的阻塞在自旋上的线程会大量的消耗掉CPU的时间,那么使用自旋锁显然就是不合适的。此外Linux内核在进行临界区处理的时候大量的使用了自旋锁。尽管在真实的编程环境下自旋锁使用的不多,但其仍然是线程同步机制中一种重要的方式。

条件变量

在多线程的环境下我们往往有如下的需求:当A线程完成某件事情之前B线程需要阻塞,当A线程完成该事件之后通知B线程之后,B线程才能被唤醒继续执行。显然这种场景可以使用互斥锁或者信号量来实现,但是一种逻辑上更自然的做法就是使用条件变量。

条件变量一般都有wait()和signal()两个函数,其中wait()函数在未接受到任何信号的嘶吼将会阻塞当前线程,而signal()函数负责唤醒等待在当前条件变量上的线程。注意signal函数具体唤醒多少线程并没有一个特定的限制,其依赖于具体的实现而定。

不同于互斥锁,自旋锁和信号量,等待在某个条件变量上的线程将完全依赖于signal()函数发出的信号而唤醒。一旦发出的信号丢失,该阻塞进程将再也没有机会被唤醒了。因此条件变量一般都需要配合互斥锁或者信号量使用,一般情况下等待线程的逻辑如下:

1
2
3
4
5
6
mutex_lock.lock();
...
if(conditions are not satisfied)
	cond_var.wait(&mutex_lock); //wait之后,mutex_lock会被自动释放
...
mutex_lock.unlock();

而发送信号的线程如下:

1
2
3
4
5
mutex_lock.lock();
...
cond_var.signal(&mutex_lock);
...
mutex_lock.unlock();

再次提醒一下,条件变量一定要配合互斥锁使用。不管是wait函数还是signal函数,都最好在加锁之后的区域调用,否则很可能会产生意想不到的问题。

信号量

信号量是Dijkstra在1965年提出的一种同步的方案。这种方案使用一种特殊的被称作信号量的整形结构来记录某一临界区操作的次数。信号量的实现中一般具有两种操作,分别是P操作和V操作。对一个信号量进行P操作时,首先检查其值是否大于0,如果大于0,则将其值减一之后返回进行后续操作,如果值小于等于0,则该进程将进行阻塞。P操作可以由以下逻辑表示:

1
2
3
4
5
6
function P(semaphore &s) {
    s.value--;
    if(s.value < 0){
        wait(s.list)
    }
}

执行V操作时,首先将信号量的值加一,然后检查信号量的值是否大于0,如果大于0,则直接返回执行其他的操作,否则唤醒一个等待在该信号量上的进程。

1
2
3
4
5
6
function V(semaphore &s) {
    s.value++;
    if(s.value <= 0){
        wake(s.list);
    }
}

在上述操作中,P和V操作都是原子的。这意味着P函数或者V函数中的语句要么都不执行,要么都执行。在单核操作系统中,这种原子性可以通过关中断而保证,而在多核系统中,可以通过锁总线操作而保证。另外需要注意的是,虽然信号量可以针对进程使用,但是在具体应用中,由于线程之间共享数据更加方便,使用信号量进行线程同步的场景更多。

事实上,信号量可以同时用来进行互斥和同步的操作,通过信号量赋予不同的初始值,可以使用信号量模拟互斥锁的行为。

select,poll,epoll 的区别

https://www.cnblogs.com/anker/p/3265058.html

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。关于这三种IO多路复用的用法,前面三篇总结写的很清楚,并用服务器回射echo程序进行了测试。连接如下所示:

select:http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html

poll:http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html

epoll:http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

今天对这三种IO多路复用进行对比,参考网上和书上面的资料,整理如下:

1、select实现

select的调用过程如下所示:

https://images0.cnblogs.com/blog/305504/201308/17201205-8ac47f1f1fcd4773bd4edd947c0bb1f4.png

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

总结:

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

2、poll实现

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

关于select和poll的实现分析,可以参考下面几篇博文:

http://blog.csdn.net/lizhiguo0532/article/details/6568964#comments

http://blog.csdn.net/lizhiguo0532/article/details/6568968

http://blog.csdn.net/lizhiguo0532/article/details/6568969

http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-

http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml

3、epoll

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

epoll水平触发和边缘触发的区别

https://blog.csdn.net/lihao21/article/details/67631516

epoll也是实现I/O多路复用的一种方法,为了深入了解epoll的原理,我们先来看下epoll水平触发(level trigger,LT,LT为epoll的默认工作模式)与边缘触发(edge trigger,ET)两种工作模式。

使用脉冲信号来解释LT和ET可能更加贴切。Level是指信号只需要处于水平,就一直会触发;而edge则是指信号为上升沿或者下降沿时触发。说得还有点玄乎,我们以生活中的一个例子来类比LT和ET是如何确定读操作是否就绪的。

水平触发

儿子:妈妈,我收到了500元的压岁钱。 妈妈:嗯,省着点花。 儿子:妈妈,我今天花了200元买了个变形金刚。 妈妈:以后不要乱花钱。 儿子:妈妈,我今天买了好多好吃的,还剩下100元。 妈妈:用完了这些钱,我可不会再给你钱了。 儿子:妈妈,那100元我没花,我攒起来了 妈妈:这才是明智的做法! 儿子:妈妈,那100元我还没花,我还有钱的。 妈妈:嗯,继续保持。 儿子:妈妈,我还有100元钱。 妈妈:…

接下来的情形就是没完没了了:只要儿子一直有钱,他就一直会向他的妈妈汇报。LT模式下,只要内核缓冲区中还有未读数据,就会一直返回描述符的就绪状态,即不断地唤醒应用进程。在上面的例子中,儿子是缓冲区,钱是数据,妈妈则是应用进程了解儿子的压岁钱状况(读操作)。

边缘触发

儿子:妈妈,我收到了500元的压岁钱。 妈妈:嗯,省着点花。 (儿子使用压岁钱购买了变形金刚和零食。) 儿子: 妈妈:儿子你倒是说话啊?压岁钱呢?

这个就是ET模式,儿子只在第一次收到压岁钱时通知妈妈,接下来儿子怎么把压岁钱花掉并没有通知妈妈。即儿子从没钱变成有钱,需要通知妈妈,接下来钱变少了,则不会再通知妈妈了。在ET模式下, 缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程。

我们再详细说明LT和ET两种模式下对读写操作是否就绪的判断。

水平触发

  1. 对于读操作

只要缓冲内容不为空,LT模式返回读就绪。 2. 对于写操作

只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

  1. 对于读操作

(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。

(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。

(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。 2. 对于写操作

(1)当缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。

(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

epoll 什么时候可写,什么时候可读,监听数量有限制吗

難度高,google 不到答案

https://man7.org/linux/man-pages/man7/epoll.7.html https://kernel.taobao.org/2019/12/epoll-is-fundamentally-broken/ 擷取自 https://eli.thegreenplace.net/2017/concurrent-servers-part-3-event-driven/

As an example, let’s look at epoll, Linux’s solution to the high-volume I/O event notification problem. The key to epoll’s efficiency is greater cooperation from the kernel. Instead of using a file descriptor set, epoll_wait fills a buffer with events that are currently ready. Only the ready events are added to the buffer, so there is no need to iterate over all the currently watched file descriptors in the client. This changes the process of discovering which descriptors are ready from O(N) in select’s case to O(1).

A full presentation of the epoll API is not the goal here - there are plenty of online resources for that. As you may have guessed, though, I am going to write yet another version of our concurrent server - this time using epoll instead of select. The full code sample is here. In fact, since the vast majority of the code is the same as select-server, I’ll only focus on the novelty - the use of epoll in the main loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct epoll_event accept_event;
accept_event.data.fd = listener_sockfd;
accept_event.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listener_sockfd, &accept_event) < 0) {
  perror_die("epoll_ctl EPOLL_CTL_ADD");
}

struct epoll_event* events = calloc(MAXFDS, sizeof(struct epoll_event));
if (events == NULL) {
  die("Unable to allocate memory for epoll_events");
}

while (1) {
  int nready = epoll_wait(epollfd, events, MAXFDS, -1);
  for (int i = 0; i < nready; i++) {
    if (events[i].events & EPOLLERR) {
      perror_die("epoll_wait returned EPOLLERR");
    }

    if (events[i].data.fd == listener_sockfd) {
      // The listening socket is ready; this means a new peer is connecting.
      ...
    } else {
      // A peer socket is ready.
      if (events[i].events & EPOLLIN) {
        // Ready for reading.
        ...
      } else if (events[i].events & EPOLLOUT) {
        // Ready for writing.
        ...
      }
    }
  }
}

We start by configuring epoll with a call to epoll_ctl. In this case, the configuration amounts to adding the listening socket to the descriptors epoll is watching for us. We then allocate a buffer of ready events to pass to epoll for modification. The call to epoll_wait in the main loop is where the magic’s at. It blocks until one of the watched descriptors is ready (or until a timeout expires), and returns the number of ready descriptors. This time, however, instead of blindly iterating over all the watched sets, we know that epoll_write populated the events buffer passed to it with the ready events, from 0 to nready-1, so we iterate only the strictly necessary number of times.

To reiterate this critical difference from select: if we’re watching 1000 descriptors and two become ready, epoll_waits returns nready=2 and populates the first two elements of the events buffer - so we only “iterate” over two descriptors. With select we’d still have to iterate over 1000 descriptors to find out which ones are ready. For this reason epoll scales much better than select for busy servers with many active sockets.

The rest of the code is straightforward, since we’re already familiar with select-server. In fact, all the “business logic” of epoll-server is exactly the same as for select-server - the callbacks consist of the same code.

This similarity is tempting to exploit by abstracting away the event loop into a library/framework. I’m going to resist this itch, because so many great programmers succumbed to it in the past. Instead, in the next post we’re going to look at libuv - one of the more popular event loop abstractions emerging recently. Libraries like libuv allow us to write concurrent asynchronous servers without worrying about the greasy details of the underlying system calls.

如何实现内存池

https://zhuanlan.zhihu.com/p/64719710

为什么要用内存池

C++程序默认的内存管理(new,delete,malloc,free)会频繁地在堆上分配和释放内存,导致性能的损失,产生大量的内存碎片,降低内存的利用率。默认的内存管理因为被设计的比较通用,所以在性能上并不能做到极致。

因此,很多时候需要根据业务需求设计专用内存管理器,便于针对特定数据结构和使用场合的内存管理,比如:内存池。

内存池原理

内存池的思想是,在真正使用内存之前,预先申请分配一定数量、大小预设的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,当内存释放后就回归到内存块留作后续的复用,使得内存使用效率得到提升,一般也不会产生不可控制的内存碎片。

内存池设计

算法原理:

1.预申请一个内存区chunk,将内存中按照对象大小划分成多个内存块block 2.维持一个空闲内存块链表,通过指针相连,标记头指针为第一个空闲块 3.每次新申请一个对象的空间,则将该内存块从空闲链表中去除,更新空闲链表头指针 4.每次释放一个对象的空间,则重新将该内存块加到空闲链表头 5.如果一个内存区占满了,则新开辟一个内存区,维持一个内存区的链表,同指针相连,头指针指向最新的内存区,新的内存块从该区内重新划分和申请

如图所示:

https://pic4.zhimg.com/80/v2-72fd972a4be6a953024e114ee1fe45f7_720w.jpg

cpu占用 100%,怎么排查问题

https://blog.csdn.net/pengjunlee/article/details/107150785

一、引子

对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考讨论提高。

二、问题复现

线上系统突然运行缓慢,CPU飙升,甚至到100%,以及Full GC次数过多,接着就是各种报警:例如接口超时报警等。此时急需快速线上排查问题。 三、问题排查

不管什么问题,既然是CPU飙升,肯定是查一下耗CPU的线程,然后看看GC。

3.1核心排查步骤

1.执行top命令:查看所有进程占系统CPU的排序。极大可能排第一个的就是咱们的java进程(COMMAND列)。PID那一列就是进程号。

2.执行top -Hp 进程号命令:查看java进程下的所有线程占CPU的情况。

3.执行printf “%x\n 10命令 :后续查看线程堆栈信息展示的都是十六进制,为了找到咱们的线程堆栈信息,咱们需要把线程号转成16进制。例如,printf “%x\n 10-》打印:a,那么在jstack中线程号就是0xa.

4.执行 jstack 进程号 | grep 线程ID 查找某进程下-》线程ID(jstack堆栈信息中的nid)=0xa的线程状态。如果"VM Thread” os_prio=0 tid=0x00007f871806e000 nid=0xa runnable,第一个双引号圈起来的就是线程名,如果是“VM Thread”这就是虚拟机GC回收线程了

5.执行jstat -gcutil 进程号 统计间隔毫秒 统计次数(缺省代表一致统计),查看某进程GC持续变化情况,如果发现返回中FGC很大且一直增大-》确认Full GC! 也可以使用jmap -heap 进程ID查看一下进程的堆内从是不是要溢出了,特别是老年代内从使用情况一般是达到阈值(具体看垃圾回收器和启动时配置的阈值)就会进程Full GC。

6.执行jmap -dump:format=b,file=filename 进程ID,导出某进程下内存heap输出到文件中。可以通过eclipse的mat工具查看内存中有哪些对象比较多。

3.2原因分析

1.内存消耗过大,导致Full GC次数过多

执行步骤1-5:

多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程-》上一节步骤2

通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。–》上一节步骤5

确定是Full GC,接下来找到具体原因:

生成大量的对象,导致内存溢出-》执行步骤6,查看具体内存对象占用情况。

内存占用不高,但是Full GC次数还是比较多,此时可能是代码中手动调用 System.gc()导致GC次数过多,这可以通过添加 -XX:+DisableExplicitGC来禁用JVM对显示GC的响应。

2.代码中有大量消耗CPU的操作,导致CPU过高,系统运行缓慢;

执行步骤1-4:在步骤4jstack,可直接定位到代码行。例如某些复杂算法,甚至算法BUG,无限循环递归等等。

3.由于锁使用不当,导致死锁。

执行步骤1-4:如果有死锁,会直接提示。关键字:deadlock.步骤四,会打印出业务死锁的位置。

造成死锁的原因:最典型的就是2个线程互相等待对方持有的锁。

4.随机出现大量线程访问接口缓慢。

代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;平时消耗的CPU不多,而且占用的内存也不高。

系统调用和函数调用的区别

系统调用

操作系统服务的编程接口 通常由高级语言编写(C或C++) 程序访问通常通过高层次 的API接口(C标准库的库函数)而不是直接进行系统调用 每个系统调用对应一个系统调用编号

系统调用与函数调用的区别

系统调用

1.使用INT和IRET指令,内核和应用程序使用的是不同的堆栈,因此存在堆栈的切换,从用户态切换到内核态,从而可以使用特权指令操控设备 2.依赖于内核,不保证移植性 3.在用户空间和内核上下文环境间切换,开销较大

  1. 是操作系统的一个入口点

函数调用

1.使用CALL和RET指令,调用时没有堆栈切换 2.平台移植性好 3.属于过程调用,调用开销较小 4.一个普通功能函数的调用

圖表

函数库调用 系统调用
在所有的ANSI C编译器版本中,C库函数是相同 各个操作系统的系统调用是不同
它调用函数库中的一段程序(或函数) 它调用系统内核的服务
用户程序相联系 操作系统的一个入口点
在用户地址空间执行 在内核地址空间执行
它的运行时间属于**“用户时间”** 它的运行时间属于**“系统时间”**
属于过程调用,调用开销较小 需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数 在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc 典型的系统调用:chdir fork write brk;

动态库和静态库的区别

https://zhuanlan.zhihu.com/p/71372182

什么是静态库

前面所提到可重定位目标文件以一种特定的方式打包成一个单独的文件,并且在链接生成可执行文件时,从这个单独的文件中“拷贝”它自己需要的内容到最终的可执行文件中。这个单独的文件,称为静态库。linux中通常以.a(archive)为后缀

还是拿前面的例子来说,我们使用静态链接构建我们的可执行文件:

1
2
$ gcc -c main.c
$ gcc -static -o main main.o -lm

在这个过程中,就会用到系统中的静态库libm.a。这个过程做了什么呢?首先第一条命令会将main.c编译成可重定位目标文件main.o,第二条命令的static参数,告诉链接器应该使用静态链接,-lm参数表明链接libm.a这个库(类似的,如果要链接libxxx.a,使用-lxxx即可)。由于main.c中使用了libm.a中的exp函数,因此链接时,会将libm.a中需要的代码“拷贝”到最终的可执行文件main中。

特别注意,必须把-lm放在后面。放在最后时它是这样的一个解析过程:

链接器从左往右扫描可重定位目标文件和静态库扫描main.o时,发现一个未解析的符号exp,记住这个未解析的符号扫描libm.a,找到了前面未解析的符号,因此提取相关代码最终没有任何未解析的符号,编译链接完成

那如果将-lm放在前面,又是怎样的情况呢?

链接器从左往右扫描可重定位目标文件和静态库扫描libm.a,由于前面没有任何未解析的符号,因此不会提取任何代码扫描main.o,发现未解析的符号exp扫描结束,还有一个未解析的符号,因此编译链接报错

如果把-lm放在前面,编译结果如下:

1
2
3
4
$ gcc -static -lm -o main main.o 
main.o: In function `main':
main.c:(.text+0x2f): undefined reference to `exp'
collect2: error: ld returned 1 exit status

更详细的解释也可以参考《一个奇怪的链接问题》。

我们看看最终生成的文件大小:

1
2
$ ls -lh main
-rwxrwxr-x 1 hyb hyb 988K 6月  27 20:22 main

生成的可执行文件大小为988k。ls的高级用法可参考《ls命令常见实用用法》。

由于最终生成的可执行文件中已经包含了exp相关的二进制代码,因此这个可执行文件在一个没有libm.a的linux系统中也能正常运行。

什么是动态库

动态库和静态库类似,但是它并不在链接时将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程。linux中通常以.so(shared object)作为后缀。

通常我们编译的程序默认就是实用动态链接:

1
$ gcc -o main main.c -lm  #默认使用的是动态链接

我们来看最终生成的文件大小:

1
2
$ ls -lh main
-rwxrwxr-x 1 hyb hyb 8.5K 6  27 20:25 main

可以看到,通过动态链接的程序只有8.5k!

另外我们还可以通过ldd命令来观察可执行文件链接了哪些动态库:

1
2
3
4
5
$ ldd main
    linux-vdso.so.1 =>  (0x00007ffc7b5a2000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fe9642bf000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe963ef5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe9645c8000)

正因为我们并没有把libm.so中的二进制代码“拷贝”可执行文件中,我们的程序在其他没有上面的动态库时,将无法正常运行。

有什么区别

到这里我们大致了解了静态库和动态库的区别了,静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的),而动态库与它相反,它的目标代码在运行时或者加载时链接。正是由于这个区别,会导致下面所介绍的这些区别。

可执行文件大小不一样

从前面也可以观察到,静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。

占用磁盘大小不一样

如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。

扩展性与兼容性不一样

如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。

依赖不一样

静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。

即便如此,系统中一班存在一些大量公用的库,所以使用动态库并不会有什么问题。

复杂性不一样

相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。这也不在本文的讨论范围。

加载速度不一样

由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。

总结

静态库和动态库具体是何如链接的已经超出了本文的介绍范围,本文仅简单介绍了一些静态库和动态库的区别,另外文中提到的在其他的linux系统,也指的是同样处理器架构的系统。但是了解这些基本信息,就能够帮助我们解决很多编译问题了。更多内容可自己阅读装载,链接方面的书籍。后面的文章也会介绍更多相关信息。

阻塞和非阻塞的区别

https://blog.csdn.net/lengxiao1993/article/details/78154467

进程间的通信时通过 send() 和 receive() 两种基本操作完成的。具体如何实现这两种基础操作,存> 在着不同的设计。 消息的传递有可能是阻塞的非阻塞的 – 也被称为同步异步的:

阻塞式发送(blocking send). 发送方进程会被一直阻塞, 直到消息被接受方进程收到。 非阻塞式发送(nonblocking send)。 发送方进程调用 send() 后, 立即就可以其他操作。 阻塞式接收(blocking receive) 接收方调用 receive() 后一直阻塞, 直到消息到达可用。 非阻塞式接受(nonblocking receive) 接收方调用 receive() 函数后, 要么得到一个有效的结果, 要么得到一个空值, 即不会被阻塞。

上述不同类型的发送方式和不同类型的接收方式,可以自由组合。

我们所说的 “阻塞”是指进程在发起了一个系统调用(System Call) 后, 由于该系统调用的操作不能立即完成,需要等待一段时间,于是内核将进程挂起为**等待 (waiting)**状态, 以确保它不会被调度执行, 占用 CPU 资源。

I/O System Call 的阻塞/非阻塞, 同步/异步

这里再重新审视 阻塞/非阻塞 IO 这个概念, 其实阻塞和非阻塞描述的是进程的一个操作是否会使得进程转变为“等待”的状态, 但是为什么我们总是把它和 IO 连在一起讨论呢?

原因是, 阻塞这个词是与系统调用 System Call 紧紧联系在一起的, 因为要让一个进程进入 等待(waiting) 的状态, 要么是它主动调用 wait() 或 sleep() 等挂起自己的操作, 另一种就是它调用 System Call, 而 System Call 因为涉及到了 I/O 操作, 不能立即完成, 于是内核就会先将该进程置为等待状态, 调度其他进程的运行, 等到 它所请求的 I/O 操作完成了以后, 再将其状态更改回 ready 。

操作系统内核在执行 System Call 时, CPU 需要与 IO 设备完成一系列物理通信上的交互, 其实再一次会涉及到阻塞和非阻塞的问题, 例如, 操作系统发起了一个读硬盘的请求后, 其实是向硬盘设备通过总线发出了一个请求,它即可以阻塞式地等待IO 设备的返回结果,也可以非阻塞式的继续其他的操作。 在现代计算机中,这些物理通信操作基本都是异步完成的, 即发出请求后, 等待 I/O 设备的中断信号后, 再来读取相应的设备缓冲区。 但是,大部分操作系统默认为用户级应用程序提供的都是阻塞式的系统调用 (blocking systemcall)接口, 因为阻塞式的调用,使得应用级代码的编写更容易(代码的执!行顺序和编写顺序是一致的)。

但同样, 现在的大部分操作系统也会提供非阻塞I/O 系统调用接口(Nonblocking I/O system call)。 一个非阻塞调用不会挂起调用程序, 而是会立即返回一个值, 表示有多少bytes 的数据被成功读取(或写入)。

非阻塞I/O 系统调用( nonblocking system call )的另一个替代品是 异步I/O系统调用 (asychronous system call)。 与非阻塞 I/O 系统调用类似,asychronous system call 也是会立即返回, 不会等待 I/O 操作的完成, 应用程序可以继续执行其他的操作, 等到 I/O 操作完成了以后,操作系统会通知调用进程(设置一个用户空间特殊的变量值 或者 触发一个 signal 或者 产生一个软中断 或者 调用应用程序的回调函数)。

此处, 非阻塞I/O 系统调用( nonblocking system call ) 和 **异步I/O系统调用 (asychronous system call)**的区别是:

1.一个非阻塞I/O 系统调用 read() 操作立即返回的是任何可以立即拿到的数据, 可以是完整的结果, 也可以是不完整的结果, 还可以是一个空值。 2.而异步I/O系统调用 read()结果必须是完整的, 但是这个操作完成的通知可以延迟到将来的一个时间点。

下图展示了同步I/O 与 异步 I/O 的区别 (非阻塞 IO 在下图中没有绘出).

https://img-blog.csdn.net/20171003191809025?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVuZ3hpYW8xOTkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

*注意, 上面提到的 非阻塞I/O 系统调用( nonblocking system call ) 和 异步I/O系统调用 都是非阻塞式的行为(non-blocking behavior)。 他们的差异仅仅是返回结果的方式和内容不同。

总结:

1.阻塞/非阻塞, 同步/异步的概念要注意讨论的上下文:

在进程通信层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。

发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。

在 IO 系统调用层面( IO system call )层面, 非阻塞IO 系统调用 和 异步IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )

2.非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。

死锁的概念,发生的必要条件

https://blog.csdn.net/ZWE7616175/article/details/79881236

一、死锁概念

死锁是指两个或多个进程在执行的过程中,因为竞争资源而造成互相等待的现象,若无外力作用,它们都无法推进下去。 1.在等待对方时占有不可抢占的资源 举个例子,假设有P1,P2两个进程,都需要A和B两个资源,两个都等待另一个资源而不肯释放资源,就这样无限等待中,这就形成死锁。这只是死锁的一种情况,就是在等待对方时占有不可抢占的资源。 2.竞争可消耗资源引起死锁 有P1,P2,P3三个进程,P1向P2发送消息并接受P3消息,P2向P3发送消息并接受P2消息,P3向P1发送消息并接受P2消息,如果设置是先接到消息后发送消息,则所有的消息都不能发送,也造成了死锁。 3.进程推进顺序不当引起死锁 有进程P1,P2,都需要资源A,B,本来可以P1运行A,P1运行B,P2运行B,P2运行A,P2运行B,但顺序换了,P1运行A时P2运行B,容易引发死锁,属于第一种的资源抢占问题。

二、产生死锁的四个必要条件

1.互斥条件 一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所使用。此时如果有其他进程请求该资源,则请求进程只能等待。

2.请求与保持条件 进程中已经保持了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时请求进程被阻塞,但对自己已经获得资源保持不放。

3.不可剥夺条件 进程未使用完的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。

4.循环等待条件 若干进程间形成首尾相接循环等待资源的关系。在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请。

注意:这四个条件是死锁的必然条件,只要系统发生死锁,这些条件必然成立。只要有上述条件有一条不满足,就不会发生死锁。

三、死锁的预防

我们可以通过破坏产生死锁的四个必要条件来预防死锁,由于资源互斥是固有特性无法改变的。

1.破坏“请求与保持”条件 方法一:静态分配,每个进程在开始执行时就申请他所需要的全部资源。 方法二:动态分配,每个进程在申请所需要的资源时他本身不占用系统资源。

2.破坏“不可剥夺”条件 一个进程不可获得其所需要的全部资源便处于等待状态,等待期间他占用的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。

3.破坏“循环等待”条件 采用资源有序分配的基本思想。将系统中的资源顺序进行编号,将紧缺的、稀少的资源采用较大的编号,申请资源时必须按照编号的顺序执行,一个进程只有较小编号的进程才能申请较大编号的进程。

四、死锁的避免

基本思想:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则分配。这是一种动态策略。典型的避免死锁的算法试银行家算法。

五、死锁的检测及解除

无需采取任何措施,允许进程在运行过程中发生死锁。通过系统的检测机构及时的检测出死锁的发生,然后采取某种措施解除死锁。

https://img-blog.csdn.net/20180410144930427?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1pXRTc2MTYxNzU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70

死锁的预防和避免都属于事先预防策略,但预防死锁的限制条件较为严格,实现起来较为简单,但往往导致资源利用率低。避免死锁的限制条件相对宽松,资源分配后需要通过算法来判断是否进入不安全状态,实现起来较为复杂。

怎么处理死锁

在上個問題中

如何实现多线程的数据同步

https://www.jianshu.com/p/54290e809f68

1.应用背景

程序在设计当中如果采取多线程操作的时候,如果操作的对象是一个的话,由于多个线程共享同一块内存空间,因此经常会遇到数据安全访问的问题,下面看一个经典的问题,银行取钱的问题:

1)、你有一张银行卡,里面有5000块钱,然后你到取款机取款,取出3000,当正在取的时候,取款机已经查询到你有5000块钱,然后正准备减去300块钱的时候

2)、你的老婆拿着那张银行卡对应的存折到银行取钱,也要取3000.然后银行的系统查询,存折账户里还有6000(因为上面钱还没扣),所以它也准备减去3000,

3)、你的卡里面减去3000,5000-3000=2000,并且你老婆的存折也是5000-3000=2000。

4)、结果,你们一共取了6000,但是卡里还剩下2000。

不难发现,当多个线程访问同一数据并操作的时候非常容易出现类似的问题,。为了避免这样的事情发生,我们要保证线程同步互斥,所谓同步互斥就是:并发执行的多个线程在某一时间内只允许一个线程在执行以访问共享数据。

2.同步互斥锁

同步锁原理:Java会为每个对象内置同步锁,通过使用synchronized来获取一个对象的同步锁,synchronized的使用方式,是在一段代码块中,加上synchronized(object){ … }

当线程首次执行到synchronized语句块时候会获得对象的同步锁(锁最开始属于对象,后被线程持有),在当前线程不释放同步锁时候,其他线程获取该对象同步锁的行为是被阻塞的,直到该锁被释放。以下几种情况下,线程才会释放掉对象的同步锁

1.线程执行完synchronized修饰的语句块。

2.线程主动执行wait()来释放同步锁。

同步锁虽然可以解决多并发引起的数据安全问题,但是会在一定程度上影响程序运行的效率,也会引起死锁问题。因此慎重使用。下面是别人描述的死锁,做引用。

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不能正常运行。简单的说就是:线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源。这里举一个通俗的例子:如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。

导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。

一个死锁的造成很简单,比如有两个对象A 和 B 。第一个线程锁住了A,然后休眠1秒,轮到第二个线程执行,第二个线程锁住了B,然后也休眠1秒,然后有轮到第一个线程执行。第一个线程又企图锁住B,可是B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线程执行。第二个线程又企图锁住A,可是A已经被第一个线程锁定了,所以第二个线程也进入阻塞状态。就这样,死锁造成了。

3.显式锁

为了符合Java面向对象的设计原则,在JDK1.5中,引入了显示锁的概念。

程序设计过程中如果提前发现可能会引起多线程数据访问的安全问题,可以通过Lock lock =newReentrantLock();来获取一个锁,并将该锁作为运行参数传入其中,在关键数据块之前使用lock.lock();来控制对竞争资源并发访问的控制,显式锁的优点是可以知道持有锁的对象,比同步锁也清晰好多。当然使用完之后也要主动释放(lock.unlock())。

4.读写锁

读写锁是在显示锁的基础上对读写进行分离的一种锁,可以认为是为了提高并发效率的一种优化。使用方法类比显式锁

初始化:ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

四种操作: rwl.readLock().lock(); rwl.readLock().unlock() rwl.writeLock().lock(); rwl.writeLock().unlock()

关于读写锁之间的互斥:

1,读锁是排写锁操作的,读锁不排读锁操作,多个读锁可以并发不阻塞。即在读锁获取后和读锁释放之前,写锁并不能被任何线程获得,多个读锁同时作用期间,试图获取写锁的线程都处于等待状态,当最后一个读锁释放后,试图获取写锁的线程才有机会获取写锁。

2,写锁是排写锁、排读锁操作的。当一个线程获取到写锁之后,其他试图获取写锁和试图获取读锁的线程都处于等待状态,直到写锁被释放。

3,在写锁状态中,可以获取读锁 即线程持有写锁的状态下是可以继续申请读锁的。即一线程同时持有读锁和写锁

4,读锁是不能够获得写锁的,如果要加写锁,本线程必须释放所持有的读锁。

5.volatile

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作,可以看作是一种轻量级的synchronized,但是是尽量保证每次读取的是最新的,并不绝对保证。

fork 的原理

在Linux中fork函数是非常重要的函数,它的作用是从已经存在的进程中创建一个子进程,而原进程称为父进程。

https://zhuanlan.zhihu.com/p/36872365

fork的原理

先通过下面这段代码简单的介绍一下fork这个函数,了解一下它的功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <unistd.h>
#include <stdio.h>

int main()
{
    int pid = fork();

    if (pid == -1)
        return -1;

    if (pid)
    {
        printf("I am father, my pid is %d\n", getpid());
        return 0;
    }
    else
    {
        printf("I am child, my pid is %d\n", getpid());
        return 0;
    }
}

下图是这段代码的运行结果

https://pic2.zhimg.com/80/v2-a7238e1167cc0c163a5d9b7aee69d98d_720w.jpg

看到这个结果是不是很奇怪,为什么if的分支执行到了,else的分支也执行到了。这明显不符合程序执行最基本的原理。这个放到后面再来解释,先来了解一下fork这个函数

1
pid_t fork();

上面是fork函数的原型,它有三个返回值

  • 该进程为父进程时,返回子进程的pid
  • 该进程为子进程时,返回0
  • fork执行失败,返回-1

那么问题来了,fork它是如何知道一个进程是父进程还是子进程的。

这个就涉及到fork本身的功能了,它的作用是克隆进程,也就是将原先的一个进程再克隆出一个来,克隆出的这个进程就是原进程的子进程,这个子进程和其他的进程没有什么区别,同样拥有自己的独立的地址空间。不同的是子进程是在fork返回之后才开始执行的,就像一把叉子一样,执行fork之后,父子进程就分道扬镳了,所以fork这个名字就很形象,叉子的意思。

这幅图就非常形象

https://pic3.zhimg.com/80/v2-c5c3ba7e5e1f3eb0127683faef9cce32_720w.jpg

接下来同过ps命令查看一下是否真的出现了两个一样的进程

https://pic2.zhimg.com/80/v2-d45d9fa41d32aaf3aa5bb06e79e4219d_720w.jpg

透过这些现象,来看一下fork的本质。

fork在执行之后,会创建出一个新的进程,这个新的进程内部的数据是原进程所有数据的一份拷贝。因此fork就相当于把某个进程的全部资源复制了一遍,然后让cs:eip指向新进程的指令部分。

fork给父进程返回子进程pid,给其拷贝出来的子进程返回0,这也是他的特点之一,一次调用,两次返回。两次返回看上去有点神秘,实质是在子进程的栈中构造好数据后,子进程从栈中获取到的返回值。

fork的应用

fork的应用场景非常多,这里只讨论在这里kernel中的应用。

在之后的内容中,将会实现shell,那么这个shell由谁来调用呢。比如说内建的shell命令,他是写死在程序中的,本质上就是一个函数。肯定要有一个东西来调用它

在这个kernel的设计中,会有一个init进程,通过这个init进程fork出一个子进程,这个子进程就专门来处理我们的shell。

下一节就会实现shell了。终于从内核层到了用户层,可以直观的看出效果了。

服务端可以建立连接的最大数目由什么决定

在計算機網路區面試題有提到這問題

以下為延伸題

知道的系统调用有哪些

概述

在计算机中,系统调用(通常缩写为syscall)是计算机程序从操作系统的内核请求服务的程序化方式。这可能包括与硬件相关的服务(例如,访问硬盘驱动器),创建和执行新进程,以及与内核服务(如进程调度)的通信。系统调用提供了进程和操作系统之间的重要接口。

在大多数系统中,系统调用只能由用户空间的进程来进行,而在某些系统中,例如在OS/360及其继任者中,有特权的系统代码也会发出系统调用。

系统调用的类别

系统调用大致可以分为六大类:

1.过程控制

a.创建进程

b.终止进程

c.载入、执行

d.获取/设置过程属性

e.等待时间、等待事件、信号事件

f.分配和释放内存

2.文件管理

a.建文件、删文件

b.打开文件、关闭文件

c.读、写、调位置

d.获取/设置文件属性

3.设备管理

a.请求设器、释放设器

b.读、写、调位位置

c.获取/设置设备属性

d.连接或断开设备

4.信息维护

a.获取/设定时间或日期

b.获取/设置系统数据

c.获取/设置进程、文件或设备属性

5.通信

a.建立、断开通信

b.收发信息

c.转移状态信息

d.连接和断开远程设备

6.保护措施

a.获取/设置权限

安全模型

除了一些嵌入式系统外,大多数现代处理器的架构都涉及到安全模型。例如,环形模型指定了软件可以执行的多个权限级别:一个程序通常被限制在自己的地址空间内,这样它就不能访问或修改其他运行中的程序或操作系统本身,并且通常被阻止直接操作硬件设备(如帧缓冲区或网络设备)。

但是,很多时候许多应用程序需要访问这些组件来完成自己的任务,因此操作系统就提供了系统调用,为这类操作提供定义良好的、实现安全的通信方式。

操作系统以最高级别的权限执行,允许应用程序通过系统调用请求使用服务,而系统调用通常是通过中断发起的。中断会自动使CPU进入某种高权限级别,然后将控制权传递给内核,由内核决定调用程序是否应该被授予所请求的服务。

如果请求服务被允许的话,内核会执行一组特定的指令来完成任务。而调用程序对内核没有直接控制权,任务完成后将控制权返回给调用程序。

程序库API

一般来说,系统环境会提供了一个程序库来暴露一些API,这些API可以在应用程序和操作系统之间完成通信任务。

在类似Unix的系统中,这些API通常是C程序库库(libc)实现的一部分,例如glibc,它为系统调用提供了封装函数,通常与系统调用的名称相同。

在Windows NT上,这个API是Native API的一部分,在ntdll.dll库中;这是一个未公开发行的API,被常规Windows API的实现所使用,也被Windows上的一些系统程序直接使用。该库的封装函数层提供了一个普通函数调用的方法(是汇编级的子程序调用),用于使用系统调用,这也使得系统调用更加模块化。

在这里,封装层的主要功能是将所有要传递给系统调用的参数都放在相应的处理器寄存器中(有时也可以放在调用栈中),同时也为内核设置一个唯一的系统调用编号来调用。这样一来,存在于操作系统和应用程序之间的库就增加了可移植性。

对库函数本身的调用不会导致切换到内核模式,通常是正常的子程序调用(例如,在某些指令集架构(ISA)中使用 “CALL"汇编指令)。

实际的系统调用确实将控制权转移到了内核(而且这比抽象的库调用更依赖于具体的实现和平台环境)。例如,在类似Unix的系统中,fork和execve是C库函数,这些函数反过来调用fork和exec这些系统调用的指令。

直接在应用程序代码中进行系统调用比较复杂,可能需要使用嵌入式汇编代码(在C和C++中),并且需要了解系统调用操作的底层二进制接口,而这些二进制接口可能会随着时间的推移而变化,它们不是应用程序二进制接口的一部分,而程序库函数的作用就是为了抽象出这些逻辑而创建的。

在基于exokernel的系统中,库作为中介的作用尤为重要。在exokernels上,库将用户应用屏蔽在非常低级的内核API中,并提供抽象层和资源管理。

由OS/360和DOS/360衍生出来的IBM操作系统,包括z/OS和z/VSE,都是通过汇编语言宏程序库来实现系统调用的。它们的起源于汇编语言编程比高级语言更普遍的年代。因此,IBM的系统调用不能被高级语言程序直接调用,而是需要一个封装的可调用的汇编语言子程序。

系统中的示例和检测工具

**在Unix、Unix-like和其他兼容POSIX的操作系统上,常用的系统调用有open、read、write、close、wait、exec、exc、fork、exit和kill。**许多现代操作系统都有数百个系统调用。例如,Linux和OpenBSD各有300多个不同的调用,NetBSD有近500个,FreeBSD有500多个,Windows 7有近700个,而Plan9有51个。

有些工具如strace、ftrace和truss等可以从进程一开始就跟踪报告该进程调用的所有系统调用,或者可以把这些工具绑定附加到一个已经运行的进程上来跟踪其进程调用情况。只要该追踪操作不违反用户的权限,就可以拦截该进程所做的任何系统调用。这些程序工具的这种特殊能力通常也是通过系统调用来实现的,例如strace是通过ptrace或procfs中的文件的系统调用来实现。

多个父子进程不断循环申请内存,总数量超出内存大小,会发生什么

自閉中

只要不写入,不无限fork,理论上没影响 申请了不用(触发缺页中断,真正分配内存)是不会发生任何事情的 操作系统一般允许 overcommit…要是没开 overcommit 的话分配会失败,什么也不会发生 不过加了个循环fork,所以就不一样了 fork 是指的是 fork bomb 那种量级的 fork?😂 是的,最终会内存 or fd 爆炸而结束

Overcommit

Memory Overcommit的意思是作業系統承諾給進程的內存大小超過了實際可用的內存。一個保守的作業系統不會允許memory overcommit,有多少就分配多少,再申請就沒有了,這其實有些浪費內存,因為進程實際使用到的內存往往比申請的內存要少,比如某個進程malloc()了200MB內存,但實際上只用到了100MB,按照UNIX/Linux的算法,物理內存頁的分配發生在使用的瞬間,而不是在申請的瞬間,也就是說未用到的100MB內存根本就沒有分配,這100MB內存就閒置了。下面這個概念很重要,是理解memory overcommit的關鍵:commit(或overcommit)針對的是內存申請,內存申請不等於內存分配,內存只在實際用到的時候才分配。

file descriptor

https://kkc.github.io/2020/08/22/file-descriptor/

file descriptor (fd) 基本上是一層介面,可以讓我們去操作 file 和其他 input/output interface (例如 pipe & socket)。

申请超出总内存大小的内存后,在每个进程中堆内存进行读写,会发生什么

自閉中

視乎情況會先使用 swap / 分頁檔 再不行會觸發 OOM Killer / 應用崩潰

多个父子进程同时对同一个文件进行修改,发生什么

自閉中

https://meik2333.com/posts/linux-many-proc-write-file/

recv返回值的含义

recv方法:

模型:

1
2
3
4
5
 #include <sys/types.h>

 #include <sys/socket.h>

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

sockfd创建的文件描述符fd,buf接收数据的缓冲区,len接收数据的长度,flags表示信息,默认设置为0

当应用程序调用recv接收数据的时候,recv函数会等待sockfd中发送数据的缓冲区的协议发送完数据,如果在等待过程中出现网络错误,则会返回SOCKET_ERROR。如果sockfd中的缓冲区中没有数据或者协议已经发送完数据,则recv会检查sockfd的接受缓冲区,如果该缓冲区正在接受数据,则recv会一直等待,知道缓冲区接受数据完毕,之后recv将数据从缓冲区拷贝一份值buf中,数据通过协议转发的,recv只是将数据从缓冲区拷贝过来。注,如果recv在拷贝数据时出现错误,则返回SOCKET_ERROT,如果在协议传输数据中出现网络错误,则返回0。

阻塞与非阻塞recv返回值没有区别,都是:

  • <0 出错

  • =0 对方调用了close API来关闭连接

  • >0 接收到的数据大小,

特别地:返回值<0时并且$(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)$的情况下认为连接是正常的,继续接收。

但是如下特点:

只是阻塞模式下recv会一直阻塞直到接收到数据,非阻塞模式下如果没有数据就会返回,不会阻塞着读,因此需要循环读取)。

返回说明:

(1)成功执行时,返回接收到的字节数。

(2)若另一端已关闭连接则返回0,这种关闭是对方主动且正常的关闭

(3)失败返回-1,errno被设为以下的某个值

EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时

EBADF:sock不是有效的描述词

ECONNREFUSE:远程主机阻绝网络连接

EFAULT:内存空间访问出错

EINTR:操作被信号中断

EINVAL:参数无效

ENOMEM:内存不足

ENOTCONN:与面向连接关联的套接字尚未被连接上

ENOTSOCK:sock索引的不是套接字

send方法:

模型:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

sockfd:创建的sockfd文件描述符,buf发送数据所在的数据区,len发送数据的长度,flags标志位默认设置为0

当程序使用send方法的时候,send会首先检查协议sockfd中的发送缓冲区中是否有数据发送,send会比较发送数据的buf长度和sockfd发送数据的缓冲区长度,如果len大于sockfd的发送长度,则send返回SOCKET_ERROR,如果发送缓冲区的大小足够,则将数据buf中的数据发送至发送缓冲区中,确认send函数将数据拷贝至发送换区中,另外,如果send检测发送缓冲区有数据但是还未发送,就比较该缓冲区的剩余空间和和len的大小,如果len大于剩余空间,就一直等待,直到缓冲区中的数据发送玩为止,如果len < 缓冲区剩余的大小,就将发送的数据拷贝至该缓冲区中,如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。(send函数只是将数据拷贝至发送缓冲中,就返回,此刻数据不一定发送至接收端)。

总结,不管是send还是recv方法,都是数据的缓冲区和发送的缓冲区的拷贝操作过程,真正发送数据的是协议功能,注意三种返回值的可能性,>0表示成功,返回实际发送或接受的字节数,=0表示超时,对方主动关闭了连接过程,<0出错,此种情况可能出现过重情况,如上所示。其中errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN这三种是特殊情况,实际使用中表示继续正常接受数据即可。

如图所示通信过程:

https://img-blog.csdn.net/201807110854298?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2MTA1Mzk3/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70

send()方法的行为

对于send方法,将需要发送的数据拷贝至发送缓冲区,否则进入阻塞或者进入超时等待。如果改变这种状态,将发送缓冲区大小设置为0,这样,当send方法返回是,所发送的数据就都到达目的机器。但是,只是到达目标服务器的接受缓冲区,并不保证数据以被应用层所接收。另外, 在发送数据时,协议根据滑动窗口和MSS值来确定tcp报文段的数据字段大小,这样就能保证接收缓冲区不会溢出。如果接收方的滑动窗口为0,但是发送方还有数据尚未发送完成,就是用探测机制,一方面检测对方方的滑动窗口的大小变化(探测机制是通过每次发送一个字节来进行检测,由先前的30s到之后的1分钟,最终达到2分钟间隔)),另一方面检测对方的连接是否异常。

push标志指示接收端应尽快将数据提交给应用层。如果send函数提交的待发送数据量较小,例如小于1460B(参照MSS值确定),那么协议层会将该报文中的TCP头部的push字段置为1;如果待发送的数据量较大,需要拆成多个数据段发送时,协议层只会将最后一个分段报文的TCP头部的push字段置1。

recv()方法的行为

对recv方法来说,将接收缓冲区中的数据拷贝至应用层的缓冲区中,当应用缓冲区满或者接受缓冲区数据接收完,就会返回。如果将接受缓冲区大小设置为0,那么该方法会直接从协议中的滑动窗口中获取数据。要么缓冲区接收满为止。要么当push标志位1的时候 ,recv返回实际接收的数据大小。

协议层收到TCP数据包后(保存在滑动窗口区),本方的滑动窗口合拢(窗口值减小);当协议层将数据拷贝到接收缓冲区(滑动窗口区—>接收缓冲区),或者应用层调用recv接收数据(接收缓冲区—>应用层缓冲区,滑动窗口区—>应用层缓冲区)后,本方的滑动窗口张开(窗口值增大)。收到数据更新window后,协议层向对方发送ACK确认。

Linux

  • Linux 的 mutex、semphore、condition_variable、read-write-lock 等操作系统API。

Linux 常用指令

https://www.runoob.com/w3cnote/linux-common-command-2.html

特別注意 查看系統指令

1、ls命令

就是 list 的缩写,通过 ls 命令不仅可以查看 linux 文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限)查看目录信息等等。

常用参数搭配:

ls -a 列出目录所有文件,包含以.开始的隐藏文件 ls -A 列出除.及..的其它文件 ls -r 反序排列 ls -t 以文件修改时间排序 ls -S 以文件大小排序 ls -h 以易读大小显示 ls -l 除了文件名之外,还将文件的权限、所有者、文件大小等信息详细列出来

2、cd 命令

cd(changeDirectory) 命令语法:

cd [目录名]

说明:切换当前目录至 dirName。

3、pwd 命令

pwd 命令用于查看当前工作目录路径。

4、mkdir 命令 mkdir 命令用于创建文件夹。

可用选项:

  • -m: 对新建目录设置存取权限,也可以用 chmod 命令设置;
  • -p: 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不在的目录,即一次可以建立多个目录。

5、rm 命令

删除一个目录中的一个或多个文件或目录,如果没有使用 -r 选项,则 rm 不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状。

rm [选项] 文件…

6、rmdir 命令

从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对其父目录的写权限。

注意:不能删除非空目录

7、mv 命令

移动文件或修改文件名,根据第二参数类型(如目录,则移动文件;如为文件则重命令该文件)。

当第二个参数为目录时,第一个参数可以是多个以空格分隔的文件或目录,然后移动第一个参数指定的多个文件到第二个参数指定的目录中。

8、cp 命令

将源文件复制至目标文件,或将多个源文件复制至目标目录。

注意:命令行复制,如果目标文件已经存在会提示是否覆盖,而在 shell 脚本中,如果不加 -i 参数,则不会提示,而是直接覆盖!

  • -i 提示
  • -r 复制目录及目录内所有项目
  • -a 复制的文件与原文件时间一样

9、cat 命令

cat 主要有三大功能:

1.一次显示整个文件:

cat filename

2.从键盘创建一个文件:

cat > filename

只能创建新文件,不能编辑已有文件。

3.将几个文件合并为一个文件:

cat file1 file2 > file

  • -b 对非空输出行号
  • -n 输出所有行号

10、more 命令

功能类似于 cat, more 会以一页一页的显示方便使用者逐页阅读,而最基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示。

命令参数:

+n 从笫 n 行开始显示 -n 定义屏幕大小为n行 +/pattern 在每个档案显示前搜寻该字串(pattern),然后从该字串前两行之后开始显示 -c 从顶部清屏,然后显示 -d 提示“Press space to continue,’q’ to quit(按空格键继续,按q键退出)”,禁用响铃功能 -l 忽略Ctrl+l(换页)字符 -p 通过清除窗口而不是滚屏来对文件进行换页,与-c选项相似 -s 把连续的多个空行显示为一行 -u 把文件内容中的下画线去掉

常用操作命令:

Enter 向下 n 行,需要定义。默认为 1 行 Ctrl+F 向下滚动一屏 空格键 向下滚动一屏 Ctrl+B 返回上一屏 = 输出当前行的行号 :f 输出文件名和当前行的行号 V 调用vi编辑器 !命令 调用Shell,并执行命令 q 退出more

11、less 命令

less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。

常用命令参数:

-i 忽略搜索时的大小写 -N 显示每行的行号 -o <文件名> 将less 输出的内容在指定文件中保存起来 -s 显示连续空行为一行 /字符串:向下搜索“字符串”的功能 ?字符串:向上搜索“字符串”的功能 n:重复前一个搜索(与 / 或 ? 有关) N:反向重复前一个搜索(与 / 或 ? 有关) -x <数字> 将“tab”键显示为规定的数字空格 b 向后翻一页 d 向后翻半页 h 显示帮助界面 Q 退出less 命令 u 向前滚动半页 y 向前滚动一行 空格键 滚动一行 回车键 滚动一页 [pagedown]: 向下翻动一页 [pageup]: 向上翻动一页

实例:

(1)ps 查看进程信息并通过 less 分页显示

ps -aux | less -N

(2)查看多个文件

less 1.log 2.log

12、head 命令

head 用来显示档案的开头至标准输出中,默认 head 命令打印其相应文件的开头 10 行。

常用参数:

-n<行数> 显示的行数(行数为复数表示从最后向前数)

13、tail 命令

用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。

常用参数:

-f 循环读取(常用于查看递增的日志文件) -n<行数> 显示行数(从后向前)

(1)循环读取逐渐增加的文件内容

1
ping 127.0.0.1 > ping.log &

后台运行:可使用 jobs -l 查看,也可使用 fg 将其移到前台运行。

tail -f ping.log

(查看日志)

14、which 命令

在 linux 要查找某个文件,但不知道放在哪里了,可以使用下面的一些命令来搜索:

which 查看可执行文件的位置。 whereis 查看文件的位置。 locate 配合数据库查看文件位置。 find 实际搜寻硬盘查询文件名称。

which 是在 PATH 就是指定的路径中,搜索某个系统命令的位置,并返回第一个搜索结果。使用 which 命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。

常用参数:

-n  指定文件名长度,指定的长度必须大于或等于所有文件中最长的文件名。

17、find 命令

用于在文件树中查找文件,并作出相应的处理。

命令格式:

find pathname -options [-print -exec -ok …]

命令参数:

pathname: find命令所查找的目录路径。例如用.来表示当前目录,用/来表示系统根目录。 -print: find命令将匹配的文件输出到标准输出。 -exec: find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为’command' { } ;,注意{ }和\;之间的空格。 -ok: 和-exec的作用相同,只不过以一种更为安全的模式来执行该参数所给出的shell命令,在执行每一个命令之前,都会给出提示,让用户来确定是否执行。

命令选项:

-name 按照文件名查找文件 -perm 按文件权限查找文件 -user 按文件属主查找文件 -group 按照文件所属的组来查找文件。 -type 查找某一类型的文件,诸如: b - 块设备文件 d - 目录 c - 字符设备文件 l - 符号链接文件 p - 管道文件 f - 普通文件

-size n :[c] 查找文件长度为n块文件,带有c时表文件字节大小 -amin n 查找系统中最后N分钟访问的文件 -atime n 查找系统中最后n24小时访问的文件 -cmin n 查找系统中最后N分钟被改变文件状态的文件 -ctime n 查找系统中最后n24小时被改变文件状态的文件 -mmin n 查找系统中最后N分钟被改变文件数据的文件 -mtime n 查找系统中最后n*24小时被改变文件数据的文件 (用减号-来限定更改时间在距今n日以内的文件,而用加号+来限定更改时间在距今n日以前的文件。 ) -maxdepth n 最大查找目录深度 -prune 选项来指出需要忽略的目录。在使用-prune选项时要当心,因为如果你同时使用了-depth选项,那么-prune选项就会被find命令忽略 -newer 如果希望查找更改时间比某个文件新但比另一个文件旧的所有文件,可以使用-newer选项

18、chmod 命令

用于改变 linux 系统文件或目录的访问权限。用它控制文件或目录的访问权限。该命令有两种用法。一种是包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法。

每一文件或目录的访问权限都有三组,每组用三位表示,分别为文件属主的读、写和执行权限;与属主同组的用户的读、写和执行权限;系统中其他用户的读、写和执行权限。可使用 ls -l test.txt 查找。

以文件 log2012.log 为例:

-rw-r–r– 1 root root 296K 11-13 06:03 log2012.log

第一列共有 10 个位置,第一个字符指定了文件类型。在通常意义上,一个目录也是一个文件。如果第一个字符是横线,表示是一个非目录的文件。如果是 d,表示是一个目录。从第二个字符开始到第十个 9 个字符,3 个字符一组,分别表示了 3 组用户对文件或者目录的权限。权限字符用横线代表空许可,r 代表只读,w 代表写,x 代表可执行。

常用参数:

-c 当发生改变时,报告处理信息 -R 处理指定目录以及其子目录下所有文件

权限范围:

u :目录或者文件的当前的用户 g :目录或者文件的当前的群组 o :除了目录或者文件的当前用户或群组之外的用户或者群组 a :所有的用户及群组

权限代号:

r :读权限,用数字4表示 w :写权限,用数字2表示 x :执行权限,用数字1表示 - :删除权限,用数字0表示 s :特殊权限

19、tar 命令

用来压缩和解压文件。tar 本身不具有压缩功能,只具有打包功能,有关压缩及解压是调用其它的功能来完成。

弄清两个概念:打包和压缩。打包是指将一大堆文件或目录变成一个总的文件;压缩则是将一个大的文件通过一些压缩算法变成一个小文件

常用参数:

-c 建立新的压缩文件 -f 指定压缩文件 -r 添加文件到已经压缩文件包中 -u 添加改了和现有的文件到压缩包中 -x 从压缩包中抽取文件 -t 显示压缩文件中的内容 -z 支持gzip压缩 -j 支持bzip2压缩 -Z 支持compress解压文件 -v 显示操作过程

有关 gzip 及 bzip2 压缩:

1
2
3
4
5
gzip 实例:压缩 gzip fileName .tar.gz 和.tgz  解压:gunzip filename.gz 或 gzip -d filename.gz
          对应:tar zcvf filename.tar.gz     tar zxvf filename.tar.gz

bz2实例:压缩 bzip2 -z filename .tar.bz2 解压:bunzip filename.bz2或bzip -d filename.bz2
       对应:tar jcvf filename.tar.gz         解压:tar jxvf filename.tar.bz2

20、chown 命令

chown 将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户 ID;组可以是组名或者组 ID;文件是以空格分开的要改变权限的文件列表,支持通配符。

-c 显示更改的部分的信息 -R 处理指定目录及子目录

21、df 命令

显示磁盘空间使用情况。获取硬盘被占用了多少空间,目前还剩下多少空间等信息,如果没有文件名被指定,则所有当前被挂载的文件系统的可用空间将被显示。默认情况下,磁盘空间将以 1KB 为单位进行显示,除非环境变量 POSIXLY_CORRECT 被指定,那样将以512字节为单位进行显示:

-a 全部文件系统列表 -h 以方便阅读的方式显示信息 -i 显示inode信息 -k 区块为1024字节 -l 只显示本地磁盘 -T 列出文件系统类型

实例:

(1)显示磁盘使用情况

1
df -l

23、ln 命令

功能是为文件在另外一个位置建立一个同步的链接,当在不同目录需要该问题时,就不需要为每一个目录创建同样的文件,通过 ln 创建的链接(link)减少磁盘占用量。

链接分类:软件链接及硬链接

软链接:

1.软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
2.软链接可以 跨文件系统 ,硬链接不可以
3.软链接可以对一个不存在的文件名进行链接
4.软链接可以对目录进行链接 

硬链接:

1.硬链接,以文件副本的形式存在。但不占用实际空间。
2.不允许给目录创建硬链接
3.硬链接只有在同一个文件系统中才能创建 

需要注意:

第一:ln命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化;
第二:ln的链接又分软链接和硬链接两种,软链接就是ln –s 源文件 目标文件,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接 ln 源文件 目标文件,没有参数-s, 它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。
第三:ln指令用在链接文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则会把前面指定的所有文件或目录复制到该目录中。若同时指定多个文件或目录,且最后的目的地并非是一个已存在的目录,则会出现错误信息。 

常用参数:

-b 删除,覆盖以前建立的链接 -s 软链接(符号链接) -v 显示详细处理过程

实例:

(1)给文件创建软链接,并显示操作信息

ln -sv source.log link.log

(2)给文件创建硬链接,并显示操作信息

ln -v source.log link1.log

(3)给目录创建软链接

ln -sv /opt/soft/test/test3 /opt/soft/test/test5

26、grep 命令

强大的文本搜索命令,grep(Global Regular Expression Print) 全局正则表达式搜索。

grep 的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。

命令格式:

grep [option] pattern file|dir

常用参数:

-A n –after-context显示匹配字符后n行 -B n –before-context显示匹配字符前n行 -C n –context 显示匹配字符前后n行 -c –count 计算符合样式的列数 -i 忽略大小写 -l 只列出文件内容符合指定的样式的文件名称 -f 从文件中读取关键词 -n 显示匹配内容的所在文件中行数 -R 递归查找文件夹

grep 的规则表达式:

^ #锚定行的开始 如:'^grep’匹配所有以grep开头的行。 $ #锚定行的结束 如:‘grep$‘匹配所有以grep结尾的行。 . #匹配一个非换行符的字符 如:‘gr.p’匹配gr后接一个任意字符,然后是p。
* #匹配零个或多个先前字符 如:'grep’匹配所有一个或多个空格后紧跟grep的行。 . #一起用代表任意字符。
[] #匹配一个指定范围内的字符,如’[Gg]rep’匹配Grep和grep。 [^] #匹配一个不在指定范围内的字符,如:'[^A-FH-Z]rep’匹配不包含A-R和T-Z的一个字母开头,紧跟rep的行。
(..) #标记匹配字符,如’(love)',love被标记为1。
\< #锚定单词的开始,如:'<grep’匹配包含以grep开头的单词的行。 \> #锚定单词的结束,如’grep>‘匹配包含以grep结尾的单词的行。 x{m} #重复字符x,m次,如:‘0{5}‘匹配包含5个o的行。 x{m,} #重复字符x,至少m次,如:‘o{5,}‘匹配至少有5个o的行。
x{m,n} #重复字符x,至少m次,不多于n次,如:‘o{5,10}‘匹配5–10个o的行。
\w #匹配文字和数字字符,也就是[A-Za-z0-9],如:‘G\w*p’匹配以G后跟零个或多个文字或数字字符,然后是p。
\W #\w的反置形式,匹配一个或多个非单词字符,如点号句号等。
\b #单词锁定符,如: ‘\bgrep\b’只匹配grep。

实例:

(1)查找指定进程

ps -ef | grep svn

(2)查找指定进程个数

ps -ef | grep svn -c

(3)从文件中读取关键词

cat test1.txt | grep -f key.log

(4)从文件夹中递归查找以grep开头的行,并只列出文件

grep -lR ‘^grep’ /tmp

(5)查找非x开关的行内容

grep ‘^[^x]’ test.txt

(6)显示包含 ed 或者 at 字符的内容行

grep -E ‘ed|at’ test.txt

28、ps 命令

ps(process status),用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用 top

linux上进程有5种状态:

1. 运行(正在运行或在运行队列中等待)
2. 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
3. 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
4. 僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
5. 停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行) 

ps 工具标识进程的5种状态码:

D 不可中断 uninterruptible sleep (usually IO) R 运行 runnable (on run queue) S 中断 sleeping T 停止 traced or stopped Z 僵死 a defunct (”zombie”) process

命令参数:

-A 显示所有进程 a 显示所有进程 -a 显示同一终端下所有进程 c 显示进程真实名称 e 显示环境变量 f 显示进程间的关系 r 显示当前终端运行的进程 -aux 显示所有包含其它使用的进程

实例:

(1)显示当前所有进程环境变量及进程间关系

ps -ef

(2)显示当前所有进程

ps -A

(3)与grep联用查找某进程

ps -aux | grep apache

(4)找出与 cron 与 syslog 这两个服务有关的 PID 号码

ps aux | grep ‘(cron|syslog)’

30、kill 命令

发送指定的信号到相应进程。不指定型号将发送SIGTERM(15)终止指定进程。如果任无法终止该程序可用”-KILL” 参数,其发送的信号为SIGKILL(9) ,将强制结束进程,使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。

常用参数:

-l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称 -a 当处理当前进程时,不限制命令名和进程号的对应关系 -p 指定kill 命令只打印相关进程的进程号,而不发送任何信号 -s 指定发送信号 -u 指定用户

实例:

(1)先使用ps查找进程pro1,然后用kill杀掉

kill -9 $(ps -ef | grep pro1)

31、free 命令

显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。

命令参数:

-b 以Byte显示内存使用情况 -k 以kb为单位显示内存使用情况 -m 以mb为单位显示内存使用情况 -g 以gb为单位显示内存使用情况 -s<间隔秒数> 持续显示内存 -t 显示内存使用总合

实例:

(1)显示内存使用情况

free free -k free -m

(2)以总和的形式显示内存的使用信息

free -t

(3)周期性查询内存使用情况

free -s 10

算法

非常重要:記得一定要刷 LeetCode 的劍指 Offer。

考試概率: 快速排序 > 冒泡排序 > 归并排序 > 桶排序

十大经典排序算法 https://www.runoob.com/w3cnote/ten-sorting-algorithm.html

排序的稳定性,概念

这个说错了,我还以为试是问时r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

氣泡排序

低概率

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来 说并没有什么太大作用。

  1. 算法步骤

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
void bubble_sort(int arr[], int len) {
    int i, j, temp;
    for (i = 0; i < len - 1; i++)
        for (j = 0; j < len - 1 - i; j++)
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
}
int main() {
    int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    bubble_sort(arr, len);
    int i;
    for (i = 0; i < len; i++)
       printf("%d ", arr[i]);
    return 0;
}

插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

  1. 算法步骤

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void insertion_sort(int arr[], int len){
    int i,j,key;
    for (i=1;i<len;i++){
        key = arr[i];
        j=i-1;
        while((j>=0) && (arr[j]>key)) {
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = key;
    }
}

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:

However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.

然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

  1. 算法步骤
  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置;

  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

  • 重复步骤 3 直到某一指针达到序列尾;

  • 将另一序列剩下的所有元素直接复制到合并序列尾。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int min(int x, int y) {
    return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
    int *a = arr;
    int *b = (int *) malloc(len * sizeof(int));
    int seg, start;
    for (seg = 1; seg < len; seg += seg) {
        for (start = 0; start < len; start += seg * 2) {
            int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
            int k = low;
            int start1 = low, end1 = mid;
            int start2 = mid, end2 = high;
            while (start1 < end1 && start2 < end2)
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
            while (start1 < end1)
                b[k++] = a[start1++];
            while (start2 < end2)
                b[k++] = a[start2++];
        }
        int *temp = a;
        a = b;
        b = temp;
    }
    if (a != arr) {
        int i;
        for (i = 0; i < len; i++)
            b[i] = a[i];
        b = a;
    }
    free(b);
}

快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

  1. 算法步骤
  • 从数列中挑出一个元素,称为 “基准”(pivot);

  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

循環解法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typedef struct _Range {
    int start, end;
} Range;

Range new_Range(int s, int e) {
    Range r;
    r.start = s;
    r.end = e;
    return r;
}

void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort(int arr[], const int len) {
    if (len <= 0)
        return; // 避免len等於負值時引發段錯誤(Segment Fault)
    // r[]模擬列表,p為數量,r[p++]為push,r[--p]為pop且取得元素
    Range r[len];
    int p = 0;
    r[p++] = new_Range(0, len - 1);
    while (p) {
        Range range = r[--p];
        if (range.start >= range.end)
            continue;
        int mid = arr[(range.start + range.end) / 2]; // 選取中間點為基準點
        int left = range.start, right = range.end;
        do {
            while (arr[left] < mid) ++left;   // 檢測基準點左側是否符合要求
            while (arr[right] > mid) --right; //檢測基準點右側是否符合要求
            if (left <= right) {
                swap(&arr[left], &arr[right]);
                left++;
                right--;               // 移動指針以繼續
            }
        } while (left <= right);
        if (range.start < right) r[p++] = new_Range(range.start, right);
        if (range.end > left) r[p++] = new_Range(left, range.end);
    }
}

遞歸

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort_recursive(int arr[], int start, int end) {
    if (start >= end)
        return;
    int mid = arr[end];
    int left = start, right = end - 1;
    while (left < right) {
        while (arr[left] < mid && left < right)
            left++;
        while (arr[right] >= mid && left < right)
            right--;
        swap(&arr[left], &arr[right]);
    }
    if (arr[left] >= arr[end])
        swap(&arr[left], &arr[end]);
    else
        left++;
    if (left)
        quick_sort_recursive(arr, start, left - 1);
    quick_sort_recursive(arr, left + 1, end);
}

void quick_sort(int arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

二分搜索

重要

在二分搜尋法中,從數列的中間開始搜尋,如果這個數小於我們所搜尋的數,由於數列已排序,則該數左邊的數一定都小於要搜尋的對象,所以無需浪費時間在左邊的數;如果搜尋的數大於所搜尋的對象,則右邊的數無需再搜尋,直接搜尋左邊的數。

所以在二分搜尋法中,將數列不斷的分為兩個部份,每次從分割的部份中取中間數比對,例如要搜尋92於以下的數列,首先中間數索引為(0+9)/2 = 4(索引由0開始): [3 24 57 57 67 68 83 90 92 95]

由於67小於92,所以轉搜尋右邊的數列: 3 24 57 57 67 [68 83 90 92 95]

由於90小於92,再搜尋右邊的數列,這次就找到所要的數了: 3 24 57 57 67 68 83 90 [92 95]

C 的寫法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
//recursive binary search

int bisearch(int key,int *list,int right,int left)
{
	int middle=(left+right)/2;

	if(left<right)
	{
		if(key==list[middle])
		{
			return middle;
		}
		else if(key<list[middle])
		{
			right=middle-1;
			return bisearch(key,list,right,left);
		}
		else
		{
			left=middle+1;
			return bisearch(key,list,right,left);
		}
	}
	return -1;
}

int main()
{
	int list[10]={5,12,34,56,76,81,99,123,145,168};
	int right=9;
	int left=0;
	int middle;
	int key;
	printf("請輸入要搜尋的key:\n");
	scanf("%d",&key);
	printf("key:%d在第%d個位置\n",key,bisearch(key,list,right,left));
}

C++ 的寫法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int binary_search(vector<int> &nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // array 長度 -1
    while (left <= right) {
        int mid = (left + right) / 2; // 用 int 的性質做無條件捨去
        if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            return mid; // 剛好找到 target
        }
    }
    return -1;
}int main() {
    vector<int> nums = {1,3,4,7,8,10};
    cout << binary_search(nums, 0)  << endl; // -1
    cout << binary_search(nums, 1)  << endl; // 0
    cout << binary_search(nums, 3)  << endl; // 1
    cout << binary_search(nums, 4)  << endl; // 2
    cout << binary_search(nums, 5)  << endl; // -1
    cout << binary_search(nums, 7)  << endl; // 3
    cout << binary_search(nums, 8)  << endl; // 4
    cout << binary_search(nums, 10) << endl; // 5
    cout << binary_search(nums, 11) << endl; // -1
}

二叉树DFS/BFS实现(C++)

https://blog.csdn.net/usstmiracle/article/details/107021776

深度优先搜索算法(Depth First Search)

DFS是搜索算法的一种。它沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8zMTYzNjE1LTkyYzExNDE3M2NkNTQ4MzgucG5n?x-oss-process=image/format,png

如上图所示的二叉树:

A 是第一个访问的,然后顺序是 B、D,然后是 E。接着再是 C、F、G。那么,怎么样才能来保证这个访问的顺序呢?

分析一下,在遍历了根结点后,就开始遍历左子树,最后才是右子树。因此可以借助堆栈的数据结构,由于堆栈是后进先出的顺序,由此可以先将右子树压栈,然后再对左子树压栈,这样一来,左子树结点就存在了栈顶上,因此某结点的左子树能在它的右子树遍历之前被遍历。

广度优先搜索算法(Breadth First Search)

又叫宽度优先搜索,或横向优先搜索。是从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。

https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8zMTYzNjE1LTkyYzExNDE3M2NkNTQ4MzgucG5n?x-oss-process=image/format,png

如上图所示的二叉树,A 是第一个访问的,然后顺序是 B、C,然后再是 D、E、F、G。那么,怎样才能来保证这个访问的顺序呢? 借助队列数据结构,由于队列是先进先出的顺序,因此可以先将左子树入队,然后再将右子树入队。这样一来,左子树结点就存在队头,可以先被访问到。

代码实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include<iostream>
#include <queue>
#include<stack>
using namespace std;

struct Node
{
    int nVal;
    Node *pLeft;
    Node *pRight;
 
    Node(int val,Node* left=NULL,Node * right=NULL):nVal(val),pLeft(left),pRight(right){}; //构造
};
// 析构
void DestroyTree(Node *pRoot)   
{
    if (pRoot==NULL)
        return;
 
    Node* pLeft=pRoot->pLeft;
    Node* pRight=pRoot->pRight;
 
    delete pRoot;
    pRoot =NULL;
 
    DestroyTree(pLeft);
    DestroyTree(pRight);
 
}
 
 
// 用queue实现的BFS
void BFS(Node *pRoot)
{
    if (pRoot==NULL)
        return;
 
    queue<Node*> Q;
 
    Q.push(pRoot);
 
    while(!Q.empty())
    {
        
        Node *node = Q.front();
 
        cout<<node->nVal<<"->";
        if (node->pLeft!=NULL)
        {
            Q.push(node->pLeft);
        }
 
        if (node->pRight!=NULL)
        {
            Q.push(node->pRight);
        }
 
        Q.pop();
    }
 
    cout<<endl;
}
 
 
// DFS的递归实现
void DFS_Recursive(Node* pRoot)
{
    if (pRoot==NULL)
        return;
 
    cout<<pRoot->nVal<<" ";
 
    if (pRoot->pLeft!=NULL) 
        DFS_Recursive(pRoot->pLeft);
 
 
    if (pRoot->pRight!=NULL)
        DFS_Recursive(pRoot->pRight);
    
}
 
// DFS的迭代实现版本(stack)
void DFS_Iterative(Node* pRoot)
{
    if (pRoot==NULL)
        return;
 
    stack<Node*> S;
    S.push(pRoot);
 
    while (!S.empty())
    {
        Node *node=S.top();
        cout<<node->nVal<<",";
 
        S.pop();
 
        if (node->pRight!=NULL)
        {
            S.push(node->pRight);
        }
 
        if (node->pLeft!=NULL)
        {
            S.push(node->pLeft);
        }
        
    }
 
}
 
 
// 打印树的信息
void PrintTree(Node* pRoot)
{
    if (pRoot==NULL)
        return;
 
    cout<<pRoot->nVal<<" ";
 
    if (pRoot->pLeft!=NULL)
    {
        PrintTree(pRoot->pLeft);
    }
 
    if (pRoot->pRight!=NULL)
    {
        PrintTree(pRoot->pRight);
    }
}
 
int main()
{
    Node *node1=new Node(4);
    Node *node2=new Node(5);
    Node *node3=new Node(6);
 
    Node* node4=new Node(2,node1,node2);
    Node* node5=new Node(3,node3);
    Node* node6=new Node(1,node4,node5);
 
 
    Node* pRoot = node6;
    //PrintTree(pRoot);
    //DFS_Recursive(pRoot);
 
    DFS_Iterative(pRoot);
    DestroyTree(pRoot);
 
    return 0;
}

dijkstra算法求最短路径

https://zhuanlan.zhihu.com/p/40338107

看这个算法的时候,虽然也是看到各种例子,但是对例子的说明,很多博客写的让我一脸懵,真为自己的智商感到着急。接下去我也将用一个例子来说明这个算法,希望初学者看到我的这篇可以更加浅显易懂。

先引用别人的关于该算法的定义,有耐心的可以看看,也可以直接跳到例子。

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

基本思想

  1. 通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。
  2. 此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
  3. 初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是”起点s到该顶点的路径”。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 … 重复该操作,直到遍历完所有顶点。

操作步骤

  1. 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。
  2. 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。
  3. 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
  4. 重复步骤(2)和(3),直到遍历完所有顶点。

单纯的看上面的理论可能比较难以理解,下面通过实例来对该算法进行说明。以D为开头,求D到各个点的最短距离。

https://pic2.zhimg.com/80/v2-a7df4cc75079e02c5607e3e5ddb1c56d_720w.jpg

第1步:初始化距离,其实指与D直接连接的点的距离。dis[c]代表D到C点的最短距离,因而初始dis[C]=3,dis[E]=4,dis[D]=0,其余为无穷大。设置集合S用来表示已经找到的最短路径。此时,S={D}。现在得到D到各点距离{D(0),C(3),E(4),F(),G(),B(),A()},其中*代表未知数也可以说是无穷大,括号里面的数值代表D点到该点的最短距离。

第2步:不考虑集合S中的值,因为dis[C]=3,是当中距离最短的,所以此时更新S,S={D,C}。接着我们看与C连接的点,分别有B,E,F,已经在集合S中的不看,dis[C-B]=10,因而dis[B]=dis[C]+10=13,dis[F]=dis[C]+dis[C-F]=9,dis[E]=dis[C]+dis[C-E]=3+5=8>4(初始化时的dis[E]=4)不更新。此时{D(0),C(3),E(4),F(9),G(),B(13),A()}。

第3步:在第2步中,E点的值4最小,更新S={D,C,E},此时看与E点直接连接的点,分别有F,G。dis[F]=dis[E]+dis[E-F]=4+2=6(比原来的值小,得到更新),dis[G]=dis[E]+dis[E-G]=4+8=12(更新)。此时{D(0),C(3),E(4),F(6),G(12),B(13),A(*)}。

第4步:在第3步中,F点的值6最小,更新S={D,C,E,F},此时看与F点直接连接的点,分别有B,A,G。dis[B]=dis[F]+dis[F-B]=6+7=13,dis[A]=dis[F]+dis[F-A]=6+16=22,dis[G]=dis[F]+dis[F-G]=6+9=15>12(不更新)。此时{D(0),C(3),E(4),F(6),G(12),B(13),A(22)}.

第5步:在第4步中,G点的值12最小,更新S={D,C,E,F,G},此时看与G点直接连接的点,只有A。dis[A]=dis[G]+dis[G-A]=12+14=26>22(不更新)。{D(0),C(3),E(4),F(6),G(12),B(13),A(22)}.

第6步:在第5步中,B点的值13最小,更新S={D,C,E,F,G,B},此时看与B点直接连接的点,只有A。dis[A]=dis[B]+dis[B-A]=13+12=25>22(不更新)。{D(0),C(3),E(4),F(6),G(12),B(13),A(22)}.

第6步:最后只剩下A值,直接进入集合S={D,C,E,F,G,B,A},此时所有的点都已经遍历结束,得到最终结果{D(0),C(3),E(4),F(6),G(12),B(13),A(22)}.

图的构建

问了几个图存储结构。有邻接矩阵、邻接多重表、邻接表、十字链表等。

結語

整理到後面發現根本記不下來全部。(〒︿〒)