记录一些面试中遇到的问题

记录一些面试中遇到的问题


##总结下自己在面试中遇到的一些常见问题##

一、C++基础知识

1.1 内联函数

1.1.1 内联函数与普通函数的区别

引入内联函数的目的是为了解决程序中函数调用的效率问题(减少运行时的开销),这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代(普通函数在被调用时,系统要首先跳跃到该函数的入口地址,执行函数体,执行完后在返回到函数调用的地方)。这其实就是空间换时间的思想。所以内联函数一般都是1-5行的小函数。
注:1、在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
2、内联函数可以是递归函数,但是一旦被定义为递归函数,这个函数就是去了inline的功能,因为在编译阶段,编译器无法知道递归的深度。
3、内联函数的定义必须出现在内联函数第一次调用之前。

1.1.2 内联函数与宏定义的区别

宏定义本身不是函数,但用起来像函数,预处理器用复制宏代码的方式替换函数调用,省去了参数压栈、调用、return等过程,从而提高了速度。
内敛函数是代码被插入到调用者代码处的函数,它只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句(如while、switch),并且内敛函数本身不能直接电泳递归函数。
总的来说两者区别主要表现在以下两个方面:第一,宏定义是在预处理阶段进行的代码替换,而内联函数是在编译阶段插入代码;第二,宏定义没有类型检查,而内敛函数有类型检查

1.2 虚函数和纯虚函数

1.2.1 虚函数

虚函数的作用是在程序的运行阶段动态地选择合适的成员函数,定义了虚函数后,可以在派生类中对虚函数进行重新定义。在派生类中重新定义的虚函数应与基类的虚函数具有相同的形参个数和形参类型(顺序也要一致),以实现统一的接口。如果派生类中没有对虚函数进行重新定义,则它继承基类的虚函数。

虚函数的实现原理:

  • 有虚函数的类内部有一个称为“虚表”的指针(有多少个虚函数就有多少个指针),这个就是用来指向这个类虚函数。也就是用它来确定调用该那个函数。

  • 实际上在编译的时候,编译器会自动加入“虚表”。虚表的使用方法是这样的:如果派生类在自己的定义中没有修改基类的虚函数,就指向基类的虚函数;如果派生类改写了基类的虚函数(就是自己重新定义),这时虚表则将原来指向基类的虚函数的地址替换为指向自身虚函数的指针。那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。

  • 每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,

  • 虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。

注:
1、只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。
2、将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
3、如果声明了某个成员函数为虚函数,则在该类中不能出现与这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。
4、非类的函数不能定义为虚函数,全局函数以及类的静态成员函数和构造函数不能定义为虚函数,但析构函数可以是虚函数。(静态成员函数不能是虚函数,因为他属于整个类而不是某个对象;构造函数不能是虚函数,因为构造函数是在对象完全构造之前运行的。换句话说,在运行构造函数之前对象还有没有生成,更谈不上动态类型绑定了。)
5、基类的析构函数应该定义为虚函数,这样在实现多态时不会造成内存泄露,因为如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。

1.2.2 纯虚函数

纯虚函数是一种特殊的虚函数,如果基类中有纯虚函数,那么自雷中必须实现这个纯虚函数,否则子类无法被实例化,也无法实现多态。含有纯虚函数的类称为抽象类,抽象类不能生成对象。纯虚函数永远不会被调用,他们主要用来统一管理子类对象。

1.3 内存池

首先内存池出现的原因是要管理内存碎片,当我们通过构造函数实例化一个对象,或使用new创建一个对象的时候,其实是进行了两步操作:
1、首先是使用allocate进行内存分配,这一步是与操作系统相关的,操作系统将一块闲置的内存指针返回给用户,这一步是比较耗时的;
2、然后是使用构造函数初始化该内存。
由于分配内存比较耗时,那么可以一次性的分配一块很大的内存,然后在需要的时候将其中一部分分给用户,用来管理这一块大内存的数据结构称为内存池

内存的申请流程是由一个一级分配器和一个二级分配器组成的,当要分配的内存大于128k的时候使用一级分配器;当小于等于128k时使用二级分配器。
二级分配器使用自由链表(free-list)技巧,共有16个freelist。各自管理大小分别为8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128 bytes的小额区块。

所以最终内存池的思路是:

1. 使用allocate向内存池请求size大小的内存空间, 如果需要请求的内存大小大于128bytes, 直接使用malloc.

2. 如果需要的内存大小小于128bytes, allocate根据size找到最适合的自由链表.

  a. 如果链表不为空, 返回第一个node, 链表头改为第二个node.

  b. 如果链表为空, 使用blockAlloc请求分配node.

    x. 如果内存池中有大于一个node的空间, 分配竟可能多的node(但是最多20个), 将一个node返回, 其他的node添加到链表中.

    y. 如果内存池只有一个node的空间, 直接返回给用户.

    z. 若果如果连一个node都没有, 再次向操作系统请求分配内存.

      ①分配成功, 再次进行b过程

      ②分配失败, 循环各个自由链表, 寻找空间

        I. 找到空间, 再次进行过程b

        II. 找不到空间, 抛出异常(代码中并未给出, 只是给出了注释)

3. 用户调用deallocate释放内存空间, 如果要求释放的内存空间大于128bytes, 直接调用free.

4. 否则按照其大小找到合适的自由链表, 并将其插入.

特点是这样的 :

1. 刚开始初始化内存池的时候, 其实内存池中并没有内存, 同时所有的自由链表都为空链表.

2. 只有用户第一次向内存池请求内存时, 内存池会依次执行上述过程的 1->2->b->z来完成内存池以及链表的首次填充, 而此时, 其他未使用链表仍然是空的.

3. 所有已经分配的内存在内存池中没有任何记录, 释放与否完全靠程序员自觉.

4. 释放内存时, 如果大于128bytes, 则直接free, 否则加入相应的自由链表中而不是直接返还给操作系统.

1.4 C++11的新特性

这个有很多,在面试的时候如果问到你,你当然也不可能全说出来,挑一些常用的说出来就行。在这里我就不写了,后面我会专门写一个博客,把C++primer(第五版)中列出的C++11的新特性一一列举出来。

后来才发现有一个很好的开源在线文档《现代C++教程:高速上手C++11/14/17/20》里面列出来C++11以来的一些新特性,并且也总结出了常用的。
https://changkun.de/modern-cpp/zh-cn/00-preface/index.html

1.5 C++的智能指针及其原理

在C++中,动态内存的管理是通过new与delete来完成的,然而动态内存的使用很容易出问题,因为确保在正确的时间释放内存是比较困难的。有时我们会忘记释放内存,这样就会出现内存的泄露;有时在尚有指针引用内存的情况下我们就释放了它,这种情况下就会产生引用非法内存的指针(即野指针)。
为了更容易也更安全的使用动态内存,C++11提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。这两者都被定义在memory头文件中。

最安全的分贝方式和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在memory头文件中。
定义方式如下:

// 指向一个值为42的int的shared_ptr
shared_ptr<int> p1 = make_shared<int> (42);
// 指向一个值为“999999999”的string
shared_ptr<string> p2 = make_shared<string> (10, '9');
// 指向一个值初始化的int
shared_ptr<int> p3 = make_shared<int> ();
// 指向一个动态分配的空vector<string>
auto p4 = make_shared<vector<string>> ();

为什么智能指针能够自动释放所指向的对象?
因为shared_ptr的析构函数会递减它所指向的对象的引用计数,当引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。p.count()返回与p共享对象的智能指针数量,也就是引用计数数量。

// 简单来看个例子:
#include <memory>
class Test
{
public:
    Test()
    {
        std::cout << "Test()" << std::endl;
    }
    ~Test()
    {
        std::cout << "~Test()" << std::endl;
    }
};
int main()
{
    std::shared_ptr<Test> p1 = std::make_shared<Test>();
    std::cout << "1 ref:" << p1.use_count() << std::endl;
    {
        std::shared_ptr<Test> p2 = p1;
        std::cout << "2 ref:" << p1.use_count() << std::endl;
    }
    std::cout << "3 ref:" << p1.use_count() << std::endl;
    return 0;
}

运行结果:随着引用对象的增加std::shared_ptr p2 = p1,指针的引用计数有1变为2,当p2退出作用域后,p1的引用计数变回1,当main函数退出后,p1离开main函数的作用域,此时p1被销毁,当p1销毁时,检测到引用计数已经为1,就会在p1的析构函数中调用delete之前std::make_shared创建的指针。
在这里插入图片描述

1.6 lambda表达式

lambda表达式也是C++11的新特性之一,一个lambda表达式表示一个可以调用的代码单元。我们可以将其理解为一个未命名的函数,一一般函数类似,一个lambda具有一个返回值类型、一个参数列表和一个函数体。但与一般函数不同,lambda可能定义在函数内部,一个lambda表达式具有如下形式:

[捕获列表] (参数列表) -> return type {函数体}
// 举个例子:
int main()
{
    auto add= [](int a, int b) ->int {
        return a + b;
    };
    int ret = add(1,2);
    std::cout << "ret:" << ret << std::endl;
    return 0;
}

解释:

[]:中括号用于控制main函数与内,lamda表达式之前的变量在lamda表达式中的访问形式;

(int a,int b):为函数的形参

->int:lamda表达式函数的返回值定义

{}:大括号内为lamda表达式的函数体。

在这里插入图片描述注:1、一个lambda表达式只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用改变量。
2、当需要为一个lambda定义返回值类型时,必须使用尾置返回类型。

1.7 虚继承

虚继承是为了解决多重继承中存在的一些问题而出现的。在多重继承中,可能会存在多个派生类继承同一个基类的情况(菱形继承),这个时候,不仅会浪费存储空间,而且还会导致二义性
普通继承:

#include <iostream>
using namespace std;

class Animal
{
public:
	int age;

};
//普通继承
class Sheep: public Animal
{
public:
	
};

class Tuo: public Animal
{
public:

};

class SheepTuo :public Sheep, public Tuo
{
public:

};

void test01()
{
    SheepTuo yt;
    yt.Sheep::age = 10;
    yt.Tuo::age = 20;
    cout << yt.Sheep::age << endl;
    cout << yt.Tuo::age << endl;

}

int main()
{
    test01();
    system("pause");
    return EXIT_SUCCESS;
}

这个时候Sheep类继承了一个age,Tuo类也继承了一个age。他们都各自保有了一份age拷贝,就会导致age不同。
在这里插入图片描述虚继承:

#include <iostream>
using namespace std;

class Animal
{
public:
	int age;

};
//这里添加的virtual对于Sheep和Tuo本身没影响,只对他们的子类有影响。
class Sheep: virtual public Animal
{
public:
	
};

class Tuo: virtual public Animal
{
public:

};

class SheepTuo :public Sheep, public Tuo
{
public:

};

class SheepTuo :public Sheep, public Tuo继承的时候,把Sheep和Tuo的vbptr都继承了,然后通过他们类距离虚基类中的公共成员age的偏移量发现他们指向的是同一个age,所以就不会拷贝两份,SheepTuo只保留一份age。至于虚继承底层实现原理则与编译器相关。

1.8 迭代器与指针的区别

迭代器实际上是对“遍历容器”这一操作进行了封装。
在编程中我们往往会用到各种各样的容器,但由于这些容器的底层实现各不相同,所以对他们进行遍历的方法也是不同的。例如,数组使用指针算数就可以遍历,但链表就要在不同节点直接进行跳转。

1、在范围上,pointer 属于 iterator 的一种(random access iterator)。
2、在功能上,iterator 有着比 pointer 更细的划分并对应能力不同的功能(重载不同的运算符)。
3、在行为上,iterator 比 pointer 更统一和良好的用法(更轻易使用 begin()、end()且不用担心越界)。

1.9 怎样用C++设计一个不被继承的类

其实就是后面设计模式中说的单例模式,例子如下:

class FinalClass1
{
public :
      static FinalClass1* GetInstance() {
            return new FinalClass1;
      }
 
      static void DeleteInstance( FinalClass1* pInstance) {
            delete pInstance;
            pInstance = 0;
      }
 
private :
      FinalClass1() {}
      ~FinalClass1() {}
};

1.10 静态链接和动态链接的区别

首先给出C/C++程序编译的过程:
在这里插入图片描述
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。
1、静态链接
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。
由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快
2、动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

二、操作系统相关

2.1 进程与线程

可以参考这里:
链接: 进程和线程的深入理解

进程间通信机制:

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

线程共享的环境包括:

  1.进程代码段 

  2.进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯) 

  3.进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。

线程独立的资源包括:

1.线程ID

每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

2.寄存器组的值

由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

3.线程的堆栈

堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。

4.错误返回码

由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该 线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。

5.线程的信号屏蔽码

由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都 共享同样的信号处理器。

6.线程的优先级

由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

多进程与多线程的区别
重点 面试官最最关心的一个问题,必须从cpu调度,上下文切换,数据共享,多核cup利用率,资源占用,等等各方面回答,然后有一个问题必须会被问到:哪些东西是一个线程私有的?答案中必须包含寄存器,否则悲催。

维度多进程多线程总结
数据共享、同步数据是分开的:共享复杂;同步简单多线程共享进程数据:共享简单;同步复杂各有优势
内存、CPU占用内存多,切换复杂,CPU利用率低占用内存少,切换简单,CPU利用率高线程占优
创建销毁、切换创建销毁、切换复杂,速度慢创建销毁、切换简单,速度快线程占优
编程调试编程简单,调试简单编程复杂,调试复杂进程占优
可靠性进程间不会相互影响一个线程挂掉将导致整个进程挂掉进程占优
分布式适用于多核、多机分布;如果一台机器不够,扩展到多台机器比较简单适用于多核分布进程占优

2.2 守护进程

2.2.1 守护进程的概念

守护进程就是一个脱离于控制终端、进程组与会话并且在后台运行的进程。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。

进程组:每个进程除了有一进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合,每一个进程有一个唯一的进程组ID。进程组ID类似于进程ID——它是一个正整数,并可存放再pid_t数据类型中。可用函数getpgrp返回进程的进程组ID。

会话:一个或多个进程组的集合。

总结一下守护进程的特点:1、在后台运行的一个进程。2、守护进程脱离原来的控制终端、进程组与会话。3、不受用户登陆注销影响。4、周期性执行某任务。

2.2.2 守护进程创建步骤

1. fork子进程,让父进程终止。

2. 子进程调用 setsid() 创建新会话

3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。

4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。  022 -- 755	0345 --- 432   r---wx-w-   422

5. 通常根据需要,关闭/重定向 文件描述符

6. 守护进程 业务逻辑。while()

2.2.3 实现守护进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<signal.h>
#include<sys/time.h>
#include<time.h>
#include<fcntl.h>

int main() {
	pid_t pid = fork(); //fork()一个子进程
	if(pid < 0) {
		perror(fork());
		exit(1);
	}
	if(pid > 0) { //终止父进程
		exit(1);
	} else if(pid == 0) {
		setsid(); //子进程调用setsid()创建新的会话
		if(pid = fork()) exit(1); //终止子进程
		chdir("/home/notang"); //更改目录,放在根目录下
		umask(0); //重设文件创建掩码
		//关闭输入、输出、错误文件描述符
		close(STDIN_FILENO);
		close(STDOUT_FILENO);
		close(STDERR_FILENO);
		while(1); //让守护进程一直进行下去
	}
}

2.3 死锁

死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

2.3.1 死锁产生的原因

(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

2.3.2 产生死锁的必要条件

(1)互斥条件:一个资源每次只能被一个进程使用。
(2)占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

2.3.3 死锁的预防

(1)互斥:它是设备的固有属性所决定的,不仅不能改变,还应该加以保证。

(2)占有且等待:

为预防占有且等待条件,可以要求进程一次性的请求所有需要的资源,并且阻塞这个进程直到所有请求都同时满足。这个方法比较低效。

(3)不可抢占:

预防这个条件的方法:

如果占有某些资源的一个进程进行进一步资源请求时被拒绝,则该进程必须释放它最初占有的资源。

如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另外一个进程,要求它释放资源。

(4)循环等待:通过定义资源类型的线性顺序来预防。

如果一个进程已经分配了R类资源,那么接下来请求的资源只能是那些排在R类型之后的资源类型。该方法比较低效。

2.3.4 死锁的避免

(1)进程启动拒绝
如果一个进程的请求会导致死锁,则不启动该进程。

(2)资源分配拒绝
如果一个进程增加的资源请求会导致死锁,则不允许此分配(银行家算法)
银行家算法的实质就是要设法保证系统动态分配资源后不进入不安全状态,以避免可能产生的死锁。即每当进程提出资源请求且系统的资源能够满足该请求时,系统将判断满足此次资源请求后系统状态是否安全,如果判断结果为安全,则给该进程分配资源,否则不分配资源,申请资源的进程将阻塞。

2.4 Linux的内存管理机

2.4.1 三种内存地址

要了解Linux的内存管理机制,首先要了解它的三种内存地址:虚拟地址(逻辑地址)、线性地址和物理地址。
1、虚拟内存, 允许程序使用的内存大于计算机的实际内存; 虚拟内存技术本质上将部分磁盘空间映射到内存中的一种技术,这样使得程序可以使用的内存空间变大了, 如果程序访问的地址不在内存中时(放在外部磁盘空间中),就需要将访问地址所对应磁盘空间的程序内容加载到内存中,在加载的过程中可能面临着旧程序内容的置换。虚拟内存中的地址就叫做虚拟地址。(用更口语的话来说——虚拟内存的概念,是指将内存中暂时不需要的部分写入硬盘,看上去硬盘扩展了内存的容量,所以叫做“虚拟”内存。使用虚拟内存,应用程序可以使用比实际物理内存更大的内存空间。可以认为这个更大的内存空间就在硬盘上,只有将某一部分需要被用到时,才被写入真实内存;当它暂时不再被用到时,又被写回硬盘。)
2、线性地址空间,是指一端连续的不分段的范围为0到4G的地址空间,一个线性地址就是线性地址空间的一个绝对地址。
3、物理地址,我们将主板上的物理内存条所提供的内存空间定义为物理内存空间,其中每个内存单元的实际地址就是物理地址。

Linux的分段机制是的虚拟地址与线性地址总是一致的,线性空间在32位平台上为4G的固定大小,也就是说虚拟内存空间也是这么大。Linux内核将这4G的空间分为两部分:较低的0~3G供各个进程使用,称为用户空间;较高的3~4G供内核使用,称为内核空间。因为每个进程可以通过系统调用进入内核,因此Linux内核空间由系统内的所有进程共享。从具体进程的角度看,每个进程可以拥有4G的虚拟地址空间(也叫虚拟内存)。

2.4.2 内存寻址方式

Linux的内存寻址过程大致为:将虚拟地址通过段机制转化为线性地址,再经过分页机制转换为物理地址

2.4.3 段机制和分页机制

段机制把虚拟地址转换为线性地址,分页机制在段机制之后执行,进一步把线性地址转换为物理地址。

1、段机制

  • 代码中使用到的逻辑地址由两部分组成: 1). 段选择符: 16 位长的字段; 2). 段内偏移地址:32 位长的字段(最大的段大小为4GB)。
  • 段描述符, 段描述符存放在全局描述符表(GDT)和局部描述符表(LDT)中。
  • 快速访问段描述符, 由段选择符来索引实际的段描述符还需要查找 GDT 或者 LDT 表,
    为了加速逻辑地址到线性地址的转换过程,80x86提供了一种附加的非编程寄存器,即每当一个段选择符装入寄存器时,相对应的段描述符就由内存装入到这个非编程寄存器中。
  • 地址转换的过程, 一个逻辑地址由段选择符和段内偏移组成,在转换成线性地址时, 分段单元执行以下操作:
    • 先检查段选择符的 TI 字段,判断这个段的段描述符是存放在 GDT 中还是 LDT 中。
    • 然后由段选择符中的 Index 索引到实际的段描述符。
    • 将 Index 索引到实际的段描述符的地址 * 8,再加上 gdtr 或者 ldtr 寄存器中的值。这个过程就完成了段起始的位置的计算。
    • 最后把计算的结果加上逻辑地址中的段内偏移就得到了线性地址。

2、分页机制

  • 页, 对应着线性地址,线性地址被分成以固定大小为单位的组,称为页,并给各页加以编号,从0开始,如第0页、第1页等。80x86支持的表中页大小为4K。
  • 页框, 对应着物理地址,物理地址也就是 RAM,被分成固定大小的页框,每个页框对应一个实际的页。
  • 页表, 用来将页映射到具体的页框中的数据结构。
  • 分页
    • 每个页的大小为 4KB,为了映射 4GB 的物理空间,页表中将会有 1MB(2^20) 的映射项, 避免每个程序保存大页表,引出了多级页表的概念,也称为页目录。
    • 线性地址的转换过程需要两步, 1). 查找页目录找到具体的页表; 2). 然后查找页表,找到具体的页。
    • 线性地址, 分为3部分; 1). Directory:决定页目录中的目录项,10位大小; 2). Table:决定页表中的表项,10位大小; 3). Offset:页框的相对位置。

2.4.4 页面调度算法

为什么要进行页面调度?
举个例子:假设某一时刻内存页已经被写满了,但这时又需要将一个页写到物理内存中,就需要将原本在物理内存中的某一页换出来。如果置换不当,就会导致刚刚被写入到内存的内容又被换出到硬盘中,减慢系统运行的速度。页面置换算法就是考虑将哪一页换出来以获得优良性能的方法。
1、最优算法
首先介绍最优算法,它需要知道以后要被用到的页,然后将不会被用到的页换出内存;如果所有页都会被用到,就把需要使用时间离现在最长的页换出,以尽量使不好的情况晚发生。这种方法能使系统获得最佳性能,但是,它是不可能实现的…因为当前无法获知以后哪些页要被用到。不过最优算法还是能够作为其他算法优秀程度的衡量。
2、先进先出算法(FIFO)
FIFO算法的思想很简单,就是置换出当前已经待在内存里时间最长的那个页。FIFO算法的运行速度很快,不需要考虑其他的因素,需要的开销很少。但是正是由于没有考虑页面的重要性的问题,FIFO算法很容易将重要的页换出内存。
3、最近最少使用算法(Least Recently Used,LRU)
为获得对最优算法的模拟,提出了LRU算法。由于当前时间之后需要用到哪些页无法提前获知,于是记录当前时间之前页面的使用情况,认为之前使用过的页面以后还会被用到。在置换时,将最近使用最少的页面换出内存。此种方法的开销比较大。

2.5 进程调度算法

进程调度:在操作系统中调度是指一种资源分配。

调度算法是指: 根据系统的资源分配策略所规定的资源分配算法。

操作系统管理了系统的有限资源,当有多个进程(或多个进程发出的请求)要使用这些资源时,因为资源的有限性,必须按照一定的原则选择进程(请求)来占用资源。这就是调度。目的是控制资源使用者的数量,选取资源使用者许可占用资源或占用资源。
1、先来先服务
如果早就绪的进程排在就绪队列的前面,迟就绪的进程排在就绪队列的后面,那么先来先服务(FCFS: first come first service)总是把当前处于就绪队列之首的那个进程调度到运行状态。也就说,它只考虑进程进入就绪队列的先后,而不考虑它的下一个CPU周期的长短及其他因素。
2、时间片轮转法
系统将所有的可运行(即就绪)进程按先来先服务的原则排成一个队列,每次调度时把CPU分配给队首进程,并令其执行一个时间片。当执行的时间片用完时,将它送到队列末尾等待下一次执行。
3、优先权调度算法
为了照顾到紧迫型进程在进入系统后便能获得优先处理,引入了最高优先权调度算法。系统把CPU分配给运行队列中优先权最高的进程(想到了STL中的优先级队列),这时又可以把该算法分成两种方式:

  • 非抢占式优先权算法(又称不可剥夺调度)。在这种方式下,系统一旦将CPU分配给运行队列中优先权最高的进程后,进程便一直执行下去,直至完成。这种调度算法主要用在批处理系统中。
  • 抢占式优先权调度算法(又称可剥夺调度)。在这种方式下,系统同样把CPU分给优先权最高的进程,但是,当出现另一个优先权更高的进程后,就暂停原优先权最高的进程,而将CPU给新出现的优先权最高的进程。采用这种调度算法时,每当出现新的可运行进程时,就将它与当前运行进程进行优先权比较。这种方式的调度算法能更好的处理紧迫型进程的要求,常用于实时性要求较高的系统中,Linux目前就采用这种方式。

4、多级反馈队列算法
这是一种折中的调度算法,本质是总和了时间轮转调度和抢占式优先权调度的优点,即优先权高的进程先运行给定的时间片,相同优先权的进程轮流运行给定时间片。

2.6 常见的信号

一、信号共性:
简单、不能携带大量信息、满足条件才发送。

二、信号的特质:
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的,一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

所有信号的产生及处理全部都是由【内核】完成的。

三、信号相关的概念:
产生信号:

1. 按键产生

2. 系统调用产生

3. 软件条件产生

4. 硬件异常产生

5. 命令产生

概念:

未决:产生与递达之间状态。  

递达:产生并且送达到进程。直接被内核处理掉。

信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)


阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。

未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。

四、信号的四要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。

五、Linux常规信号
之前我们说了信号的处理方式有:系统默认处理动作、忽略和捕捉。

在系统执行默认处理动作时又有五种方式:
Term:终止进程
Ign:忽略信号(默认即时对该种信号忽略操作)
Core:终止进程,生成core文件(查验进程死亡原因,用于gdb调试)
Stop:停止(暂停)进程
Cont:继续运行进程

Linux中常见的信号如下:
1)SIGHUP:当用于退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程。
2)SIGINT:当用户按下了<Ctrl+c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。
3)SIGQUIT:当用户按下了<Ctrl+>组合键时,用户终端向正在运行中的由该终端启动的程序发出这些信号,默认动作为终止进程。
4)SIGILL:CPU检测到某进程执行了非法指令,默认动作为终止进程并产生core文件。
5)SIGTRAP:该信号由断点指令或其它trap指令产生,默认动作为终止进程并产生core文件。
6)SIGABRT:调用abort函数时产生该信号,默认处理动作为终止里程并产生core文件。
7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认处理动作为终止进程并产生core文件。
8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误,默认处理动作为终止进程并产生core文件。
9)SIGKILL:无条件终止进程,本信号不能被忽略、处理和阻塞。默认处理动作为终止进程,它向系统提供了可以杀死任何进程的方法。
10)SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号,默认处理动作为终止进程。
11)SIGSEGV:进程进行了无效内存访问(段错误),默认处理动作为终止进程并产生core文件。
12)SIGUSR2:另一个用户自定义信号,程序员可以在程序中定义并使用该信号,默认处理动作为终止进程。
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据,默认处理动作为终止进程。
14)SIGALRM:定时器超时,超时的时间有系统调用alarm设置,默认处理动作为终止进程。
15 )SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来表示程序正常退出。是不带参数的kill默认发送的信号,默认处理动作为终止进程。
16)SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容,默认处理动作为终止进程。
17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认处理动作为忽略这个信号。
18)SIGCONT:如果进程已停止,信号使其继续运行,默认处理动作为继续/忽略这个信号。
19)SIGSTOP:停止进程的执行,信号不能被忽略、处理和阻塞,默认处理动作为暂停进程。
20)SIGTSTP:停止终端交互进程的运行,按下<Ctrl+z>组合键时发出这个信号,默认处理动作为暂停进程。

2.7 系统怎样将一个信号通知到进程

Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id、状态、工作目录、用户id、组id、文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将会被推后(解除屏蔽后)。
未决信号集:1)信号产生时,未决信号集中描述该信号的位立即翻转为1,表示信号处于未决状态。当信号被处理后对应位翻转为0,这一时刻往往非常短暂。2)信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集,在屏蔽解除前信号一直处于未决状态。
在这里插入图片描述

2.8 用户态和内核态的区别

内核态其实从本质上说就是我们所说的内核,它是一种特殊的软件程序,用于控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用

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

在linux中,将程序的运行空间分为内核与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。
但是你会说:我明明可以用C库中提供的函数如fopen、fread、fwrite、fclose等对磁盘中的文件进行操作啊。其实这是因为这些库函数的操作是通过系统调用来完成的。
1、库函数
顾名思义是把函数放到库里,是把一些常用到的函数编完放到一个文件里,供别人用。别人用的时候把所在的文件名用#include<>加到里面就可以了,一般放到lib文件里。
库函数调用是系统无关的,因此可移植性好。
2、系统调用
什么是系统调用?由操作系统实现并提供给外部应用程序的接口。是应用程序同系统之间数据交互的桥梁。
系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。

系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据,这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。
在这里插入图片描述

2.10 Linux系统的各类同步机制

在这里插入图片描述

2.11 Linux系统的各类异步机制

三、计算机网络相关

3.1 TCP和UDP区别

TCP:面向连接的传输控制协议
提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
特点:可靠,面向连接,时延大,适用于大文件。

UDP:无连接的用户数据报协议
UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快
特点:不可靠,无连接,时延小,适用于小文件。

区别:
	基于连接与无连接
	对系统资源的要求(TCP较多,UDP少)
	UDP程序结构较简单
	流模式与数据报模式
	TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证

3.2 TCP和UDP头部字节定义

TCP头部20字节,UDP头部8字节。具体如下:

3.2.1 TCP头部字节定义

在这里插入图片描述
头部长度一般为4*5=20字节,选项最多40字节,限制60字节。

头部中各部分的作用:

  • 1、16位源端口号和16位目的端口号:告知主机该报文段是从哪来的(源端口号),以及传给那个上层协议或应用程序(目的端口号)。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用致命服务端口号(比如DNS协议对应端口53,HTTP协议对应端口号80)。
  • 2、32位序号:表示所发送数据的第一个字节的序号。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化为某个随机值ISN(初始序号值)。那么在传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025,另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。
  • 3、32位确认号:用作对另一方发送来的TCP报文段的响应,其值是收到的TCP报文段的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送来的报文段也同时携带自己的序号和对A发送来的报文段的确认号。
  • 4、4位头部长度:标识该TCP头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。
  • 5、6位标志位(包含如下几项):
    • URG标志:表示紧急指针是否有效。
    • ACK标志:表示确认号是否有效。我们称携带ACK标识的TCP报文段为确认报文段
    • PSH标志:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就一直停留在TCP接收缓冲区中)。
    • RST标志:表示要求对方重新建立链接。我们称携带RST标志的TCP报文段为复位报文段
    • SYN标志:表示请求建立一个连接。我们称携带SYN标志的TCP报文段为连接请求报文段
    • FIN标志:表示通知对方本端要关闭连接了。我们称携带FIN标志的TCP报文段为结束报文段
  • 6、16位窗口大小:是TCP流量控制的一个手段。这里说的窗口指的是接收通告窗口,它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
  • 7、16位校验和:由发送端填充,接收端对TCP报文段进行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。
  • 8、16位紧急指针:URG=1时才有意义,指出本报文段中紧急数据的字节数。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

3.2.2 UDP头部字节定义

在这里插入图片描述头部中各部分的作用:

  1. 16位源端口号:记录源端口号,在需要对方回信时选用,不需要时可全用0。
  2. 16位目的端口号:记录目标端口号,在终点交付报文时必须使用到。
  3. 16位UDP长度:UDP数据报的长度(包括数据和首部)最小值为8B(即仅有首部没有数据的情况)。
  4. 16位的UDP校验和:检测UDP数据报在传输中是否有错,有错就丢弃。该字段是可选的,当源主机不想计算校验和,则直接令该字段全为0。当传输层从IP层收到UDP数据报时,就根据首部中的目的端口,把UDP数据报通过相应的端口,上交给进程。如果接收方UDP发现收到的报文中目的端口号不正确(即不存在对应端口号的应用进程),就丢弃该报文,并由ICMP发送“端口不可达”差错报文交给发送方。

3.3 TCP三次握手和四次挥手

3.3.1 三次握手

假设运行在一台主机(客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接,客户应用进程首先通知客户TCP,他想建立一个与服务器上某个进程之间的连接,客户中的TCP会用以下步骤与服务器中的TCP建立连接:
在这里插入图片描述
ROUND1:客户端发送连接请求报文段无应用层数据。SYN = 1,seq = x(随机)。

ROUND2:服务器端为该TCP连接分配缓存和变量,并向客户端返回确认报文段,允许连接,无应用层数据。SYN = 1,ACK = 1,seq = y(随机),ack = x+1。

ROUND3:客户端为该TCP连接分配缓存和变量,并向服务端返回确认的确认可以携带数据。SYN = 0,ACK = 1,seq = x+1,ack = y+1。

3.3.2 四次挥手

参与一条TCP连接的两个进程中的任何一个都能终止该连接,连接结束后,主机中的“资源”(缓存和变量)将被释放。
在这里插入图片描述
ROUND1:客户端发送连接释放报文段,停止发送数据,主动关闭TCP连接。FIN = 1,seq = u。
ROUND2:服务器端回送一个确认报文段,客户到服务器这个方向的连接就释放了——半关闭状态。ACK = 1,seq = v,ack = u+1。(半关闭就是TCP在断开连接时需要四次挥手的直接原因)
ROUND3:服务器发完数据,就发出连接释放报文段,主动关闭TCP连接。FIN = 1,ACK = 1,seq = w,ack = u+1。
ROUND4:客户端回送一个确认报文段,再等到时间等待计时器设置的2MSL(最长报文段寿命)后,连接彻底关闭。ACK = 1,seq = u+1,ack = w+1。

半关闭补充:一个socket套接字内部有两个缓冲区(写缓冲区和读缓冲区),半关闭时不是断开两个套接字之间的连接,而是关闭了客户端套接字中的写缓冲区,只是不能向服务器写数据了,但依然可以向服务器发送ACK。

3.4 什么是超时重传

TCP的发送方在规定时间内没有收到确认就要重传已发送的报文段,这就是超时重传。规定时间就是重传时间,TCP采用自适应算法,动态改变重传时间RTTs(加权平均往返时间)。

TCP还有一种快速重传机制——冗余确认(冗余ACK):
每当比期望序号大的失序报文段到达时,发送一个冗余ACK,指明下一个期待字节的序号。
如:发送方已发送1,2,3,4,5报文段
接收方收到1,返回给1的确认(确认号为2的第一个字节)
接收方收到3,返回给1的确认(确认号为2的第一个字节)
接收方收到4,返回给1的确认(确认号为2的第一个字节)
接收方收到5,返回给1的确认(确认号为2的第一个字节)
发送方收到3个对于报文段1 的冗余ACK,则认为2报文段丢失,重传2号报文段,这就是快速重传。

3.5 什么是滑动窗口

再说滑动窗口前首先要知道什么是TCP的流量控制。
流量控制:让发送方慢点,要让接收方来得及接收。
TCP利用滑动窗口机制实现流量控制。
在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,即接收窗口rwnd(接收方设置确认报文段的窗口字段来将rwnd通知给发送方),发送方的发送窗口取接收窗口rwnd和拥塞窗口cwnd的最小值。如果接收方发送的窗口是0(即rwnd=0),那么发送方就会启动持续计时器,若计时器设置的时间到期,就发送一个零窗口探测报文段。接收方收到探测报文段时给出现在的窗口值,若窗口值仍为0,那么发送方就重新设置计时器。

3.6 列举你所知道的TCP选项

TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节,因为TCP头部最长是60字节(其中还包含前面讨论的20字节的固定部分)。典型的TCP头部选项结构如图所示。
在这里插入图片描述

  • 选项的第一个字段kind说明选项的类型,有的TCP选项没有后面两个字段,仅包含1字节的kind字段。
  • 第二个字段length(如果有的话)指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。
  • 第三个字段info(如果有的话)是选项的具体信息。

常见的TCP选项有7种:
在这里插入图片描述

  1. kind=0,选项表结束(EOP)选项
    一个报文段仅用一次。放在末尾用于填充,用途是说明:首部已经没有更多的消息,应用数据在下一个32位字开始处。
  2. kind=1,空操作(NOP)选项
    没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
  3. kind=2,最大报文段长度(MSS)选项
    TCP连接初始化时,通信双方使用该选项来协商最大报文段长度。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)字节。
  4. kind=3,窗口扩大因子选项
    TCP连接初始化时,通信双方使用该选项来协商接收窗口的扩大因子。在TCP的头部中,接收窗口大小是用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。
    假设TCP头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小是N*2M,或者说N左移M位。注意,M的取值范围是0~14。
    和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收窗口大小就是该TCP报文段的实际接收窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。
  5. kind=4,选择性确认(Selective Acknowledgment,SACK)选项
    TCP通信时,如果某个TCP报文段丢失,则TCP会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP只重新发送丢失的TCP报文段,而不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。
  6. kind=5,SACK实际工作的选项
    该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
  7. kind=8,时间戳选项
    该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。

3.7 connect会阻塞,怎么解决

建立socket后默认connect()函数为阻塞连接状态,在大多数实现中,connect的超时时间在75s至几分钟之间,想要缩短超时时间,可解决问题的两种方法:

方法一、将socket句柄设置为非阻塞状态。
方法二、采用信号处理函数设置阻塞超时控制。

1、将socket句柄设置为非阻塞状态:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
......
ioctl(sockfd, FIONBIO, &ul); //设置为非阻塞模式

2、采用信号处理函数设置阻塞超时控制:

sigset(SIGALRM, u_alarm_handler);
alarm(2);
code = connect(socket_fd, (struct sockaddr*)&socket_st, sizeof(struct sockaddr_in));
alarm(0);
sigrelse(SIGALRM);

首先定义一个中断信号处理函数u_alarm_handler,用于超时后的报警处理,然后定义一个2秒的定时器,执行connect,当系统connect成功,则系统正常执行下去;如果connect不成功阻塞在这里,则超过定义的2秒后,系统会产生一个信号,触发执行u_alarm_handler函数, 当执行完u_alarm_handler后,程序将继续从connect的下面一行执行下去。

3.8 socket什么情况下可读

一、 满足下列四个条件中的任何一个时,一个套接字准备好读。

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位。对于TCP和UDP套接字而言,缓冲区低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT套接字选项(参见setsockopt函数)来设置该套接字的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0的值(即可读数据的大小)。
  2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0(也就是EOF)。
  3. 该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。对这样的套接字进行accept操作通常不会阻塞。
  4. 有一个错误套接字待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

二、满足下列四个条件中的任何一个时,一个套接字准备好写。

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而套接字默认的发送缓冲区大小是8k,这就意味着一般一个套接字连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWAT套接字选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该套接字为非阻塞,对该套接字进行写操作(如write,send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。
  2. 该连接的写半部关闭(主动发送FIN包的TCP连接)。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。
  3. 使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。即connect有结果了。
  4. 有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
    在这里插入图片描述

3.9 keepalive是什么,如何使用

3.10 长连接和短连接

3.11 UDP中使用connect的好处

3.12 C/S模型

客户/服务器模型
服务器:提供计算服务的设备
特点:1、永久提供服务;
2、永久性访问地址/域名。
客户机:请求计算服务的主机
特点:1、与服务器通信,使用服务器提供的服务;
2、间歇性接入网络;
3、可能使用动态IP地址;
4、不与其他客户机直接通信。
C/S模型的应用:Web、文件传输FTP、远程登录、电子邮件等。

3.13 DNS系统

域名系统是互联网的一项服务,它作为将域名和IP地址相互映射的一个分布式数据库,能够诗人更方便的访问互联网。

3.14 文件传送协议FTP

FTP提供不同种类主机系统(硬、软件体系等都可以不同)之间的文件传输能力。
FTP是基于客户/服务器(C/S)的协议,用户通过一个客户机程序连接至在远程计算机上运行的服务器程序。依照FTP协议提供服务,进行文件传送的计算机就是FTP服务器;连接FTP服务器,遵循FTP协议与服务器传送文件的电脑就是FTP客户端。在TCP/IP协议中,FTP标准命令TCP端口号为21,Port方式数据端口为20。

FTP的工作方式
FTP支持两种模式,一种方式叫做Standard (也就是 PORT方式,主动方式),一种是 Passive (也就是PASV,被动方式)。 Standard模式 FTP的客户端发送 PORT 命令到FTP服务器。Passive模式FTP的客户端发送 PASV命令到 FTP Server。

下面介绍一个这两种方式的工作原理:
1、主动方式:FTP 客户端首先和FTP服务器的TCP 21端口建立连接,通过这个通道发送命令,客户端需要接收数据的时候在这个通道上发送PORT命令。 PORT命令包含了客户端用什么端口接收数据。在传送数据的时候,服务器端通过自己的TCP 20端口连接至客户端的指定端口发送数据。 FTP server必须和客户端建立一个新的连接用来传送数据。
2、被动方式:在建立控制通道的时候和Standard模式类似,但建立连接后发送的不是Port命令,而是Pasv命令。FTP服务器收到Pasv命令后,随机打开一个高端端口(端口号大于1024)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口进行数据的传送,这个时候FTP server不再需要建立一个新的和客户端之间的连接。

FTP的传输模式:
1、文本模式:ASCII模式,以文本序列传输数据。
2、二进制模式:Binary模式,以二进制序列传输数据。

3.15 http协议

http协议定义了浏览器(万维网客户进程)怎样向万维网服务器请求万维网文档,以及服务器怎样把文档传送给浏览器。
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。

HTTP 请求/响应的步骤如下:

 1. 客户端连接到Web服务器
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.baidu.com。

 2. 发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。

 3. 服务器接受请求并返回HTTP响应
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。

 4. 释放连接TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;

 5. 客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。

3.16 五种I/O模式

这里参考了Richard Stevens的《UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking 》,6.2节“I/O Models ”
文中介绍了五种I/O模式:阻塞I/O、非阻塞I/O、I/O多路复用、信号驱动I/O、异步I/O。
在介绍这五种模式前,先介绍下一个I/O操作发生时通常包含的两个不同阶段:

1、等待数据准备好
2、从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

3.16.1 阻塞I/O

在linux中,默认情况下所有的socket都是阻塞的,一个典型的读操作流程大概是这样:
在这里插入图片描述
这里使用UDP而不是TCP作为例子的原因在于,就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,会比较复杂。
这里把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不论它如何实现,一般都会从在应用进程空间中运行切换到在内核空间中运行,一段时间后再切换回来。
在图中,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的,recvfrom成功返回后,应用进程开始处理数据报。

3.16.2 非阻塞I/O

进程把一个套接字设置成非阻塞是在通知内核:当所请求的IO操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
在这里插入图片描述
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回,我们接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,通常在专门提供某一种功能的系统中才有。
所以,用户进程其实是需要不断的主动询问内核数据好了没有。

3.16.3 I/O多路复用

IO多路复用,又称IO多路转接。多路复用,意思就是本来一条链路上一次只能传输一个数据流,如果要实现两个源之间多条数据流同时传输,那就得需要多条链路了,但是复用技术可以通过将一条链路划分频率,或者划分传输的时间,使得一条链路上可以同时传输多条数据流。

多路IO转接的字面意思:原本使用socket套接字编程时,是服务器(应用程序)一直在阻塞等待客户端的连接,这样服务器端(应用程序)的压力太大。于是服务器请来了助手,即select、poll、epoll等,这几个函数借助内核来替服务器监视有无客户端的连接请求,当有客户端的连接请求时,再经select、poll、epoll等助手转接给服务器端处理,这样可以有效减轻服务器的压力。
在这里插入图片描述
当用户进程调用了select,那么整个进程会被阻塞,而同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而blocking IO只调用了一个系统调用 (recvfrom)。但是,用select的优势在于它可以同时处理多个连接。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为非阻塞,但是,如上图所示,整个用户的进程其实是一直被block的。只不过进程是被select这个函数阻塞,而不是被socket IO给阻塞。

3.16.4 信号驱动I/O

我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式IO。
在这里插入图片描述
我们首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说他没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

3.16.5 异步I/O

信号驱动式IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时可以完成。
在这里插入图片描述
我们调用aio_read函数给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待IO完成期间,我们的进程不会被阻塞。

3.16.6 总结

五种IO的比较:前四种模型的主要区别在于第一阶段,因为他们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步IO模型在这两个阶段都要处理,从而不同于其它四种模型。

一般来讲:阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。只有异步IO模型是符合POSIX异步IO操作含义的,不管在阶段1还是阶段2都可以干别的事。
在这里插入图片描述

同步IO与异步IO对比:同步IO操作导致请求进程阻塞,知道IO操作完成;异步IO操作不导致请求进程阻塞。

3.17 select模型和poll模型、epoll模型

3.17.1 select模型

  • select支持的文件描述符数量太小了,默认是1024,虽然可以调整,但是描述符数量越大,效率将更低,调整的意义不大。
  • 每次调用select,都需要把fdset从用户态拷贝到内核。
  • 同时在线的大量客户端有事件发生的可能很少,但还是需要遍历fdset,因此随着监视的描述符数量的增长,其效率也会线性下降。

3.17.2 poll模型

  • poll和select在本质上没有差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
  • select采用fdset采用bitmap,poll采用了数组。
  • poll和select同样存在一个缺点就是,文件描述符的数组被整体复制于用户态和内核态的地址空间之间,而不论这些文件描述符是否有事件,它的开销随着文件描述符数量的增加而线性增大。
  • 还有poll返回后,也需要遍历整个描述符的数组才能得到有事件的描述符。

3.17.3 epoll模型

epoll解决了select和poll所有的问题(fdset拷贝和轮询),采用了最合理的设计和实现方案。

3.18 socket服务端的实现

3.19 select和epoll的区别

3.20 epoll有哪些出发模式,有什么区别

四、设计模式

4.1 单例模式

单例模式的作用:保证一个类只有一个实例,并提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例。

应用:常用于管理资源,如日志、线程池

实现要点:
在类中,要构造一个实例,就必须调用类的构造函数,并且为了保证全局只有一个实例,需防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为private,同时阻止拷贝创建对象时赋值时拷贝对象,因此也将它们声明并权限标记为private;另外,需要提供一个全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。

class Singleton{
public:
	static Singleton& getInstance() {
		static Singleton instance;
		return instance;
	}
	void printTest() {
		cout<<"do something"<<endl;
	}
private:
	Singleton(){}//防止外部调用构造创建对象
	Singleton(Singleton const &singleton);//阻止拷贝创建对象
	Singleton& operator=(Singleton const &singleton);//阻止赋值对象
};

int main() {
	Singleton &a=Singleton::getInstance();
	a.printTest();
	return 0;
}

首先,构造函数声明成private的目的是只允许内部调用,getInstance()中的静态局部变量创建时调用,但不允许外部调用构造创建第二个实例;然后,拷贝构造和拷贝赋值符是声明成了private而不给出定义,其目的是阻止拷贝,如果企图通过拷贝构造来创建第二个实例,编译器会报错。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值