0%

2020年4月6日 下午10:34

为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

  1. 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏
  2. C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

C++中析构函数的作用

  1. 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
  2. 析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  3. 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
  4. 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
  5. 类析构顺序:
    1. 1)派生类本身的析构函数;
    2. 2)对象成员析构函数;
    3. 3)基类析构函数。

静态函数和虚函数的区别

  1. 静态函数在编译的时候就已经确定运行时机
  2. 虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

C++中static关键字的作用

  1. 代码块外部:对于函数定义和代码块之外的变量声明
    1. static修改标识符的链接属性,由默认的external变为internal
    2. 作用域和存储类型不改变,这些符号只能在声明它们的源文件中访问。
  2. 代码块内部:对于代码块内部的变量声明
    1. static修改标识符的存储类型,由自动变量改为静态变量,作用域和链接属性不变。
    2. 这种变量在程序执行之前就创建,在程序执行的整个周期都存在。
  3. 函数:对于被static修饰的普通函数
    1. 其只能在定义它的源文件中使用,不能在其他源文件中被引用
  4. 类:对于被static修饰的类成员变量和成员函数
    1. 它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。

虚函数和多态

  1. 多态的实现主要分为静态多态和动态多态
    1. 静态多态主要是重载,在编译的时候就已经确定;
    2. 动态多态是用虚函数机制实现的,在运行期间动态绑定。
    3. 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  2. 虚函数的实现
    1. ::在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。::
    2. 如果在基类中使用vitual虚函数,::那么编译器看的是指针的内容,而不是它的类型::。 C++ 多态 :一起看1

多态机制深入:

C++多态的实现方式总结_c/c++_小凡的专栏-CSDN博客
虚函数在C++中的实现机制就是用虚表和虚指针。也就是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{public:
virtual void f();
virtual void g();
private:
int a
};

class B : public A
{
public:
void g();
private:
int b;
};//A、B实现省略
  1. 因为A有virtual void f()和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:
    1
    2
    A::f 的地址
    A::g 的地址
  2. B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:
    1
    2
    A::f 的地址
    B::g 的地址
    • 注意:因为B: :g是重写了的,所以B的虚表的g放的是B: :g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
  • 然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:
    1
    2
    3
    vptr : 指向B的虚表vtableB
    int a: 继承A的成员
    int b: B成员

重载 重写

  1. 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
  2. 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

2020年4月6日 下午9:14

C/C++ 中指针和引用的区别?

  1. c++中,引用和指针的区别是什么? - 知乎
    1. 从对象的角度来解释:
      1. 从对象开始讲起:
        1. 对象是指一块能存储数据并具有某种类型的内存空间
        2. 一个对象a,它有地址&a
      2. 指针p也是对象
        1. 它同样有地址&p和存储的值p,只不过,p存储的数据类型是数据的地址
      3. 对象有常量(const)和变量之分
        1. 指针常量是指,指针这个对象所存储的地址是不可以改变的
        2. 而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。
    2. 从常量指针引出到引用:
      1. 可以把引用看做是通过一个常量指针来实现的,它只能绑定到初始化它的对象上
    3. 如何选择:
      1. 引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率
  2. 散乱版本,根本记不住:
    1. 指针有自己的一块空间,是一种类型,而引用只是一个别名
    2. 初始化:
      1. 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
    3. 赋值:
      1. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
      2. C++中引用不能重新赋值的理解_c/c++_liujianfei526的专栏-CSDN博客
    4. 参数传递:
      1. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
    5. sizeof :
      1. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
    6. const:
      1. 可以有const指针
      2. const引用和非const引用
        1. const引用是必须指向const对象的引用
    7. 指针可以有多级指针(p),而引用至于一级;
    8. 指针和引用使用++运算符的意义不一样;
    9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

c++中的smart pointer四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr

  1. C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
  2. 为什么要使用智能指针:
    1. 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
  3. auto_ptr(c++98的方案,cpp11已经抛弃)
    1. 采用所有权模式。
      1
      2
      3
      auto_ptr< string> p1 (new string (“I reigned lonely as a cloud.”));
      auto_ptr<string> p2;
      p2 = p1; //auto_ptr不会报错.
    2. 此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
  4. unique_ptr(替换auto_ptr)
    1. unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
    2. 采用所有权模式,还是上面那个例子
      1
      2
      3
      unique_ptr<string> p3 (new string (“auto”));   //#4
      unique_ptr<string> p4;                       //#5
      P4 = p3;//此时会报错!!
    3. 编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
    4. 另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
      1
      2
      3
      4
      5
      unique_ptr<string> pu1(new string (“hello world”));
      unique_ptr<string> pu2;
      pu2 = pu1;                                      // #1 not allowed
      unique_ptr<string> pu3;
      pu3 = unique_ptr<string>(new string (“You”));   // #2 allowed
    5. 其中# 1留下悬挂的unique_ptr(pu1),这可能导致危害。而 # 2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
    6. 注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
      1
      2
      3
      4
      5
      unique_ptr<string> ps1, ps2;
      ps1 = demo(“hello”);
      ps2 = move(ps1);
      ps1 = demo(“Alexia”);
      cout << *ps2 << *ps1 << endl;
  5. shared_ptr
    1. shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
    2. shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
    3. 成员函数:
      1. use_count 返回引用计数的个数
      2. unique 返回是否是独占所有权( use_count 为 1)
      3. Swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
      4. Reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
      5. Get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
  6. weak_ptr
    1. weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr.
    2. weak_ptr只是提供了对管理对象的一个访问手段
    3. weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
    4. weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      class B;
      class A
      {
      public:
      shared_ptr<B> pb_;
      ~A()
      {
      cout<<“A delete\n”;
      }
      };

      class B
      {
      public:
      shared_ptr<A> pa_;
      ~B()
      {
      cout<<“B delete\n";
      }
      };

      void fun()
      {
      shared_ptr<B> pb(new B());
      shared_ptr<A> pa(new A());
      pb->pa_ = pa;
      pa->pb_ = pb;
      cout<<pb.use_count()<<endl;
      cout<<pa.use_count()<<endl;
      }

      int main()
      {
      fun();
      return 0;
      }
    5. 可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用)
    6. 如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
    7. 注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print();英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

智能指针的内存泄漏如何解决

  1. 智能指针的内存泄露是指?
    1. shared_ptr的循环引用
  2. 为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针
    1. weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

野指针是什么?

  • 野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

函数指针

  1. 定义
    1. 函数指针是指向函数的指针变量。
    2. 函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
    3. C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
  2. 用途:
    1. 调用函数和做函数的参数,比如回调函数。
  3. 示例:
    1
    2
    3
    4
    char * fun(char * p)  {…}       // 函数fun
    char * (*pf)(char * p);             // 函数指针pf
    pf = fun;                        // 函数指针pf指向函数fun
    pf(p);                        // 通过函数指针pf调用函数fun

2020年4月5日 上午10:56

作为软件工程出身的人,我从子系统分解的角度,将操作系统分解,看看这个庞大的系统中包含哪些子系统

  1. 项目管理子系统(泛指)
    1. 项目应该有运行中的状态
      1. TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态。这个时候,要看 CPU 小伙伴有没有空,有空就运行他,没空就得等着。
      2. 有时候,进程运行到一半,需要等待某个条件才能运行下去,这个时候只能睡眠。睡眠状态有两种。一种是 TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等条件成熟,进程可以被唤醒。
      3. 另一种睡眠是 TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被唤醒,只能死等条件满足。有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,他的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号,也即虽然在深度睡眠,但是可以被干掉。
      4. 一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候他的父进程还没有使用 wait() 等系统调用来获知他的终止信息,此时进程就成了僵尸进程。
  2. 权限管理子系统
  3. (编译子系统)
    1. 不属于操作系统的一部分
    2. 编译其实是一个需求分析和需求转换的过程
    3. 最后生成ELF 格式的项目执行计划书,这个项目执行计划书有总论 ELF Header 的部分,有包含指令的代码段的部分,有包含全局变量的数据段的部分
    4. “你看,每次你接一个项目,总要写成项目执行计划书,CPU 小伙伴们才能执行吧,项目计划书中的一行一行指令运行过程中,免不了要产生一些数据。这些数据要保存在一个地方,这个地方就是会议室(内存)。会议室(内存)被分成一块一块儿的,都编好了号。例如 3F-10,就是三楼十号会议室。这个地址是实实在在的地址,通过这个地址我们就能够定位到物理内存的位置。”
  4. 任务管理子系统(专指内核态)
    1. 在 Linux 里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务,由一个统一的结构 task_struct 进行管理。
  5. 任务调度子系统
    1. 调度要解决的第一个问题是,每一个 CPU 小伙伴每过一段时间,都要想一下,白板上这么多项目,我应该干哪一个?CPU 的队列里面有这么多的进程或者线程,应该取出哪一个来执行?
    2. 调度要解决的第二个问题是,什么时候切换任务?也即,什么时候,CPU 小伙伴应该停下一个进程,换另一个进程运行?
  6. 内存管理子系统
    1. 第一,物理内存的管理,相当于会议室管理员管理会议室;
      1. 对物理内存的管理系统,我们称为伙伴系统
    2. 第二,虚拟地址的管理,也即在项目组的视角,会议室的虚拟地址应该如何组织;
      1. 有了虚拟地址的管理这个规定以后,项目执行计划书ELF要写入数据的时候,就需要符合里面的规定了,数据不能随便乱放了。规定具体如下:
      2. 首先,这么大的虚拟空间一切二,一部分用来放内核的东西,称为内核空间;一部分用来放进程的东西,称为用户空间。
      3. 用户空间:
        1. 我们从最低位开始排起,先是 Text Segment、Data Segment 和 BSS Segment。Text Segment 是存放二进制可执行代码的位置,Data Segment 存放静态常量,BSS Segment 存放未初始化的静态变量。这些都是在项目执行计划书里面有的。
        2. 接下来是堆段。堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的。
        3. 接下来的区域是 Memory Mapping Segment。这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中。
        4. 再下面就是栈地址段了,主线程的函数调用的函数栈就是用这里的。
      4. 如果需要进行更高权限的工作,就需要调用系统调用,进入内核。
        1. 到了内核里面,无论是从哪个进程进来的,看到的是同一个内核空间,看到的是同一个进程列表。
        2. 虽然内核栈是各用各的,但是如果想知道的话,还是能够知道每个进程的内核栈在哪里的。所以,如果要访问一些公共的数据结构,需要进行锁保护。
        3. 内核的代码访问内核的数据结构,大部分的情况下都是使用虚拟地址的。
        4. 虽然内核代码权限很大,但是能够使用的虚拟地址范围也只能在内核空间,也即内核代码访问内核数据结构
        5. 在内核里面也会有内核的代码,同样有 Text Segment、Data Segment 和 BSS Segment,内核代码也是 ELF 格式的。
    3. 第三,虚拟地址和物理地址如何映射的问题,也即会议室管理员如果管理映射表。
      1. 两级:
        1. 虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
        2. 32 位环境下,虚拟地址空间共 4GB。如果分成 4KB 一个页,那就是 1M 个页。每个页表项需要 4 个字节来存储,那么整个 4GB 空间的映射就需要 4MB 的内存来存储映射表。如果每个进程都有自己的映射表,100 个进程就需要 400MB 的内存。对于内核来讲,有点大了 。
      2. 三级:32位
        1. 页目录有 1K 项,用 10 位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项,也即 4K 的页表项。每个页表项也是 4 个字节,因而一整页的页表项是 1k 个。再用 10 位就可以表示访问页表项的哪一项,页表项中的一项对应的就是一个页,是存放数据的页,这个页的大小是 4K,用 12 位可以定位这个页内的任何一个位置
      3. 四级:64位
  7. 文件管理系统
    1. 第一点,文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。
    2. 第二点,文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置。
    3. 第三点,如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层。
    4. 第四点,文件应该用文件夹的形式组织起来,方便管理和查询。
    5. 第五点,Linux 内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用。
      1. 对于每一个进程,打开的文件都有一个文件描述符。files_struct 里面会有文件描述符数组。每个一个文件描述符是这个数组的下标,里面的内容指向一个 struct file 结构,表示打开的文件。这个结构里面有这个文件对应的 inode,最重要的是这个文件对应的操作 file_operation。如果操作这个文件,就看这个 file_operation 里面的定义了。
  8. 输入和输出系统子系统
    1. (设备控制管理子系统)
      1. 第一层,用设备控制器屏蔽设备差异。
      2. 这里需要注意的是,设备控制器不属于操作系统的一部分,但是设备驱动程序属于操作系统的一部分。
      3. 操作系统的内核代码可以像调用本地代码一样调用驱动程序的代码,而驱动程序的代码需要发出特殊的面向设备控制器的指令,才能操作设备控制器。
    2. 设备驱动管理子系统
      1. 第二层,用驱动程序屏蔽设备控制器差异。
    3. 接受外部信息(中断)管理子系统
      1. 第三,用中断控制器统一外部事件处理。
    4. 对外输出信息管理子系统(在文件子系统中集成)
      1. 第四,用文件系统接口屏蔽驱动程序的差异。
    5. 一般的流程是,一个设备驱动程序初始化的时候,要先注册一个该设备的中断处理函数。咱们讲进程切换的时候说过,中断返回的那一刻是进程切换的时机。中断的时候,触发的函数是 do_IRQ。这个函数是中断处理的统一入口。在这个函数里面,我们可以找到设备驱动程序注册的中断处理函数 Handler,然后执行他进行中断处理。
  9. 异常处理子系统
    1. 信号可以在任何时候发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就可以了。
  10. 网络子系统:
    1. 操作系统对于网络协议的实现模式是这样的:
      1. 二到四层的处理代码在内核里面,七层的处理代码让应用自己去做。两者需要跨内核态和用户态通信,就需要一个系统调用完成这个衔接,这就是 Socket。应用层和内核互通的机制,就是通过 Socket 系统调用
    2. 整个网络过程:
      1. 如果公司想要和其他公司沟通,我们将请求封装为 HTTP 协议,通过 Socket 发送到内核。内核的网络协议栈里面,在 TCP 层创建用于维护连接、序列号、重传、拥塞控制的数据结构,将 HTTP 包加上 TCP 头,发送给 IP 层,IP 层加上 IP 头,发送给 MAC 层,MAC 层加上 MAC 头,从硬件网卡发出去。
      2. 最终网络包会被转发到目标服务器,它发现 MAC 地址匹配,就将 MAC 头取下来,交给上一层。IP 层发现 IP 地址匹配,将 IP 头取下来,交给上一层。TCP 层会根据 TCP 头中的序列号等信息,发现它是一个正确的网络包,就会将网络包缓存起来,等待应用层的读取。
      3. 应用层通过 Socket 监听某个端口,因而读取的时候,内核会根据 TCP 头中的端口号,将网络包发给相应的应用。
  11. 虚拟化子系统:
    1. 第一种方式,完全虚拟化。
      1. 其实说白了,这是一种“骗人”的方式。虚拟化软件会模拟假的 CPU、内存、网络、硬盘给到虚拟机,让虚拟机里面的内核自我感觉良好,感觉他终于又像个内核了。在 Linux 上,一个叫作 qemu 的工具可以做到这一点。
      2. qemu 向虚拟机里面的客户机操作系统模拟 CPU 和其他的硬件,骗客户机,GuestOS 认为自己和硬件直接打交道,其实是同 qemu 模拟出来的硬件打交道,qemu 会将这些指令转译给真正的硬件。由于所有的指令都要从 qemu 里面过一手,因而性能就会比较差。
    2. 第二种方式,硬件辅助虚拟化
      1. 可以使用硬件 CPU 的 Intel-VT 和 AMD-V 技术,需要 CPU 硬件开启这个标志位(一般在 BIOS 里面设置)。当确认开始了标志位之后,通过内核模块 KVM,GuestOS 的 CPU 指令将不用经过 Qemu 转译,直接运行,大大提高了速度。qemu 和 KVM 融合以后,就是 qemu-kvm。
    3. 第三种方式称为半虚拟化
      1. 对于网络或者硬盘的访问,我们让虚拟机内核加载特殊的驱动,重新定位自己的身份。虚拟机操作系统的内核知道自己不是物理机内核,没那么高的权限。他很可能要和很多虚拟机共享物理资源,所以学会了排队。虚拟机写硬盘其实写的是一个物理机上的文件,那我的写文件的缓存方式是不是可以变一下。我发送网络包,根本就不是发给真正的网络设备,而是给虚拟的设备,我可不可以直接在内存里面拷贝给它,等等等等。
      2. 网络半虚拟化方式是 virtio_net,存储是 virtio_blk。客户机需要安装这些半虚拟化驱动。客户机内核知道自己是虚拟机,所以会直接把数据发送给半虚拟化设备,然后经过特殊处理(例如排队、缓存、批量处理等性能优化方式),最终发送给真正的硬件。这在一定程度上提高了性能。

一个子系统:系统都是服务行业,被别人调用的

  • 系统结构:有哪些组成部分,各个部分之间的关系
  • 系统运转流程图:这个过程中可能会涉及到不同的数据结构算法

2020年4月4日 下午12:13

PyTorchPyTorch的C++前端和模型部署
PyTorch直观认识torch.jit模块

总结:

  1. 其实这节的内容不属于不同框架之间模型转化,讲的是针对于pytorch模型如何用C++进行加载
    • 是换语言(其实依然使用的是pytorch),而不是换框架
    • #include <torch/script.h>这个就是pytorch的C++前端接口
  2. 稍后的业界的算法同学的工作流程可能就会变成这样:
    • 论文发布->PyTorch开源代码(或者自己实现)->训练模型->导出模型->载入模型(C++_Python_其他框架/其他硬件平台)

2020年4月4日 上午9:46

tensorflow

GitHub - vahidk/EffectiveTensorflow: TensorFlow 1.x and 2.x tutorials and best practices.

pytorch[持续更新中]

GitHub - vahidk/EffectivePyTorch: PyTorch tutorials and best practices.

作者从pytorch和numpy的对比出发开始讲解:

  • The most important advantage of PyTorch over NumPy is its automatic differentiation functionality which is very useful in optimization applications such as optimizing parameters of a neural network.
  • PyTorch allows you to perform your computations on CPUs, GPUs, and TPUs without any material change to your code.
  • PyTorch also makes it easy to distribute your computation across multiple devices or machines

    Part I: PyTorch Fundamentals

  1. PyTorch basics
    1. 解释pytorch中的每轮迭代训练,更新参数W的过程
  2. Encapsulate your model with Modules
    1. In the previous example we used bare bone tensors and tensor oeprations to build our model. To make your code slightly more organized it’s recommended to use PyTorch’s modules. A module is simply a container for your parameters and encapsulates model operations.
  3. Broadcasting the good and the ugly
    1. when you have a singular dimension. PyTorch implicitly tiles the tensor across its singular dimensions to match the shape of the other operand
      1. 只有singular dimension时,才会有broadcasting
    2. when rank of two tensors don’t match, PyTorch automatically expands the first dimension of the tensor with lower rank before the elementwise operation, so the result of addition would be [[2, 3], [3, 4] ], and the reducing over all parameters would give us 12.
      1. 我理解就是从内到外的顺序
      2. A:[ [1.], [2.] ] ——> [ [1.,1.], [2.,2.] ]
      3. B:[1., 2.] -> [ [1., 2.] [1., 2.] ]
      4. A + B = [[2, 3], [3, 4] ]
        1
        2
        3
        a = torch.tensor([[1.], [2.]])
        b = torch.tensor([1., 2.])
        c = torch.sum(a + b)
  4. Take advantage of the overloaded operators
    1. 理解加速的原因?
      1. the reason is that we are calling the slice op 500 times
      2. A better choice would have been to use torch.unbind op to slice the matrix into a list of vectors all at once
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        z = -x  # z = torch.neg(x)
        z = x + y # z = torch.add(x, y)
        z = x - y
        z = x * y # z = torch.mul(x, y)
        z = x / y # z = torch.div(x, y)
        z = x // y
        z = x % y
        z = x ** y # z = torch.pow(x, y)
        z = x @ y # z = torch.matmul(x, y)
        z = x > y
        z = x >= y
        z = x < y
        z = x <= y
        z = abs(x) # z = torch.abs(x)
        z = x & y
        z = x | y
        z = x ^ y # z = torch.logical_xor(x, y)
        z = ~x # z = torch.logical_not(x)
        z = x == y # z = torch.eq(x, y)
        z = x != y # z = torch.ne(x, y)
  5. Optimizing runtime with TorchScript
    1. 补充学习: pytorch模型如何用C++进行加载
  6. Numerical stability in PyTorch
    1. LogSumExp这一机器学习中常见的模式-半导体新闻-摩尔芯球
      1. 这篇文章详细的讲解了logSumExp的推理过程
      2. 涉及到两个数学知识:
        1. 幂的乘法法则
        2. 对数的和差公式

2020年4月4日 下午2:58

解释pytorch三个内置函数的功能:

What does the backward() function do? - autograd - PyTorch Forums

  1. loss.backward() computes dloss/dx for every parameter x which has requires_grad=True. These are accumulated into x.grad for every parameter x. In pseudo-code:
    • x.grad += dloss/dx
  2. optimizer.step updates the value of x using the gradient x.grad. For example, the SGD optimizer performs:
    • x += -lr * x.grad
  3. optimizer.zero_grad() clears x.grad for every parameter x in the optimizer. It’s important to call this before loss.backward(), otherwise you’ll accumulate the gradients from multiple passes.
  4. If you have multiple losses (loss1, loss2) you can sum them and then call backwards once:
    • loss3 = loss1 + loss2
    • loss3.backward()

重要

  1. 在每轮训练的过程中,其实还是数学结论的代码实现,代码只负责数学的结论,不体现你在稿纸上的推理结论过程
  2. 在每轮训练的过程中尝试问自己一个问题?
    1. 为什么 x += -lr * x.grad 就可以让W越来越接近正确的结果?
      • 这里的x,其实写成 w += -lr * w.grad,我觉得更加正确,loss函数的自变量应该是w,而不是样本x,样本应该是已知量。
      • 首先需要认识到这是一个从数学推导出来的结论,这个推导过程叫做SGD(梯度下降),这个loss就是f(w),在SGD中我们对他进行了泰勒展开,得出了更新w的结论w += -lr * w.grad
    2. 这个问题必须得问出来,否则犯了一个本末倒置的问题
      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
      import numpy as np
      import torch

      # Assuming we know that the desired function is a polynomial of 2nd degree, we
      # allocate a vector of size 3 to hold the coefficients and initialize it with
      # random noise.
      w = torch.tensor(torch.randn([3, 1]), requires_grad=True)

      # We use the Adam optimizer with learning rate set to 0.1 to minimize the loss.
      opt = torch.optim.Adam([w], 0.1)

      def model(x):
      # We define yhat to be our estimate of y.
      f = torch.stack([x * x, x, torch.ones_like(x)], 1)
      yhat = torch.squeeze(f @ w, 1)
      return yhat

      def compute_loss(y, yhat):
      # The loss is defined to be the mean squared error distance between our
      # estimate of y and its true value.
      loss = torch.nn.functional.mse_loss(yhat, y)
      return loss

      def generate_data():
      # Generate some training data based on the true function
      x = torch.rand(100) * 20 - 10
      y = 5 * x * x + 3
      return x, y

      def train_step():
      x, y = generate_data()

      yhat = model(x)
      loss = compute_loss(y, yhat)

      opt.zero_grad()
      loss.backward()
      opt.step()

      for _ in range(1000):
      train_step()

      print(w.detach().numpy())

2020年3月31日 下午9:42

资料

大会演讲PPT合集
10 道大厂面试必考的计算机网络问题_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

问题:

  1. 请详细介绍下TCP的三次握手机制,为什么要三次握手?
    1. 为什么要有握手?
      1. 动画讲解TCP,再不懂请来打我
      2. 第一,为了确认双方的接收与发送能力是否正常。
      3. 第二,指定自己的初始化序列号,为后面的可靠传送做准备。
      4. 第三,如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。
    2. 为什么是三次?为什么不是四次?
      1. TCP中建立连接的过程是三次握手
        1. client发出第三次握手后,不等待server返回,就直接断开连接了
      2. 连接断开的过程是四次握手
        1. 发起端发出第三次握手后,还得等待server返回,在这个过程中是连接的半打开状态,被动端处于close_wait状态,被动端可以向主动端发送消息,并且主动端是可以接收的。在linux,这个半打开连接状态可以是无限时长
  2. 简单介绍下HTTP协议中缓存的处理流程?
    1. 缓存的应用流程是什么?
      1. 304 与 200的区别:
        1. 304:如果服务器验证摘要没有发生变化,那么我们直接返回304,此时是返回的http是没有body的,这样就节省了带宽
        2. 200:如果服务器验证摘要发生变化,那么我们需要在返回的body中添加新的摘要信息

    2. 与缓存相关的HTTP头部有哪些?
  3. 在地址栏键入URL后,网络世界发生么什么?
    1. 这是一道开放的题,可以进行多角度的回答:讲述正向、反向、DNS的过程,转换为http协议头,计算机网络分层结构、每一个层的作用
  4. 使用HTTP长连接有哪些优点?
    1. 减少握手次数
    2. 减少慢启动时间
    3. 缺点:因为http长连接是依靠于单个tcp,而在单个tcp中文档中的每个字段在服务端接受的时候需要严格的顺序,串行的文件传输。这样就会导致大量的重传。
  5. CLOSE_WAIT状态产生的原因?
  6. 介绍下多播是怎样实现的?
    1. 多播是依靠UDP来实现的,TCP是一对一的连接,不能使用
    2. 使用了多播以后,可以降低client在内存中的数据拷贝,极大的节省了client的效率,这部分拷贝是让网络中的支持多播的路由器等设备进行拷贝。
  7. 服务器的最大并发连接数是多少?
    1. 详解Linux服务器最大tcp连接数 - 陌上归人的博客 - 博客园
    2. server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方
  8. TCP和UDP协议该如何选择?
    1. 老师在视频里讲解了多个场景
  9. TLS/SSL协议是如何保障信息安全的?
  10. HTTP2协议有哪些优点?
    1. Http系列(二) Http2中的多路复用 - 掘金
    2. 从 Http/0.9 到 Http/2 要发送多个请求,从多个 Tcp 连接=>keep-alive=>管道化=>多路复用不断的减少多次创建 Tcp 等等带来的性能损耗。
    3. 多个 Tcp 连接
      1. 在最早的时候没有keep-alive只能创建多个Tcp连接来做多次请求。多次 http 请求效果如下图所示:
      2. 一次请求完成就会关闭本次的 Tcp 连接,下个请求又要从新建立 Tcp 连接传输完成数据再关闭,造成很大的性能损耗。
    4. Keep-Alive
      1. Keep-Alive解决的核心问题是: 一定时间内,同一域名多次请求数据,只建立一次 HTTP 请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题。
      2. Keep-Alive# 还是存在如下问题:
        • 串行的文件传输。
        • 同域并行请求限制带来的阻塞(6~8)个
    5. 管线化
      1. HTTP 管线化可以克服同域并行请求限制带来的阻塞,它是建立在持久连接之上,是把所有请求一并发给服务器,但是服务器需要按照顺序一个一个响应,而不是等到一个响应回来才能发下一个请求,这样就节省了很多请求到服务器的时间。
      2. 不过,HTTP 管线化仍旧有阻塞的问题,若上一响应迟迟不回,后面的响应都会被阻塞到。
    6. 多路复用
      1. 多路复用代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP 连接并发完成。因为在多路复用之前所有的传输是基于基础文本的,在多路复用中是基于二进制数据帧的传输、消息、流,所以可以做到乱序的传输。多路复用对同一域名下所有请求都是基于流,所以不存在同域并行的阻塞。多次请求如下图:
    7. 总结:
      1. 在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)
      2. 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流
      3. HTTP2 采用二进制数据帧传输,取代了 HTTP1.x 的文本格式,二进制格式解析更高效

收获:

  1. 在面试的时候,要知道面试官问题的这个问题对应与哪几个知识点
  2. 很多题是开放性的,可以从多个角度进行分析
    1. 在地址栏键入 URL 后,网络世界发生了什么?
    2. 这个问题就是最典型的可以多角度分析的问题
  3. 并发是一个很好的总结这些网络知识的角度,尤其对于大厂来说