面试笔记

#c++ 面试知识点总结

11-21

  • 虚函数表

    用于多态, 在每个类的内存最开始地方, 都有一个虚函数表, 存放着虚函数指针,[父类虚函数],[子类虚函数]
    若子类实现了父类虚函数, 则直接替代掉父类虚函数的数据。
    若没加vitrual, 则要看具体是什么类型进行调用该函数的, 父类指针调用父函数, 子类指针调用子函数

    #include <iostream>
    
    using namespace std;
    
    class A {
        public :
            virtual void funA1() {
                cout << "A1" << endl;
            }
    
            void funA2() {
                cout << "A2" << endl;
            }
    };
    
    class B : public A{
        public:
            virtual void funA1 () {
                cout << "B1" << endl;
            }
            void funA2() {
                cout << "B2" << endl;
            }
    };
    
    int main(void) {
        A *a = new B();
        B *b = new B();
        a->funA1();
        a->funA2();
        b->funA1();
        b->funA2();
    }
    
    /* 
        输出:
        B1
        A2
        B1
        B2
    */ 
    
    
  • 智能指针和普通指针的区别, 以及四个智能指针

    1. 普通指针存在忘记调用析构函数的风险, 会造成内存泄漏。
      智能指针会在作用域结束的时候自动调用析构函数。

    2. 四种智能指针

      1. auto_ptr : 独占, 存在p1 = p2的问题, 如果程序员不加注意就调用复制后的p2, 会出现问题。
      2. unique_ptr: 独占, 不存在p1 = p2的问题, 若要赋值,要写成p1 = move(p2)形式, p2指向为空。
      3. shared_ptr: 共享, 会维护一个use_count, 当use_count为0时, 进行析构。 存在死锁的问题。 p1和p2互相指向的时候, p1 和 p2 的use_count都为2。
      4. weak_ptr: 共享, 并且不会让use_count增加。

      在使用智能指针的时候, 推荐使用make_shared< T >(t)来进行赋值。

  • 析构函数

    当对象的生存周期结束的时候, 会自动调用该函数进行收尾工作。 一般来说析构函数都是虚函数, 因为防止当有子类继承它, 一个父类指针指向的析构函数只能析构父类的内存空间,从而导致内存泄漏(原因看上个问题)。

  • 四种类型转换

    • static_cast: 一般来说, 用于各种隐式转换, 例如 非const->const, void* -> int*, 也可以向上转型, 但不保证向下转型是否成功。(向上转换:指的是子类向基类的转换;向下转换:指的是基类向子类的转换)
    • dynamic_cast: 用于具有虚函数的类之间的, 向上转换和向下转换。 只能转指针或引用。
    • const_cast: 去掉const
    • reinterpret_cast:将一个指针、引用、算术类型、函数指针或者成员指针转换成一个整数,也可以把一个整数转换成一个指针
  • 内存分布图

    1. 环境变量区
    2. 静态变量去
    3. 代码区(包括常量区)

    栈的默认大小可以通过ulimit来查看和修改

  • C++中拷贝赋值函数的形参不能进行值传递

    不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

11-22

  • Linux进程间通讯技术:

    • Unix套接字
    • 信号量
    • 无名管道
    • 文件锁
    • 共享内存
  • 死锁

    1. 有序分配锁资源可以预防死锁

    2. 银行家算法是用于避免死锁的

  • 顺序容器删除元素, 后部元素会向前移动, 因此要注意下标的指向

  • 类的大小

    • 空类为1
    • 虚函数为4(有了虚函数, 就不需要1字节了)

    对象大小 = 虚函数指针 + 所有非静态数据成员大小 + 因对齐而多占的字节

  • 若父类析构函数没有加vitrual, 则子类无法析构其父类空间

11-24

  • atomic 原子操作 C11标准
    在一个一个来, 谁也别抢, 急也没用. 只要用于对内存数据的原子保护.

  • 可变参数模板

    template<typename T, typename Args>
    void fun(T head, Args... args) {
        cout << head << endl;
        fun(args...);
    }
    
    template<typename T>
    void fun() {
        cout << "end" << endl;
    }
    
  • RTTI(Run-Time Type Identification)

    通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。

  • 右值!!!**超重要**

    右值分为纯右值(也就是临时变量)和将亡值。
    举个例子:

        Copyable ReturnRvalue() {
            return Copyable(); 
        }
        void AcceptRef(const Copyable& a) {
    
        }
    

    此时的Copyable()的返回值就是一个右值, 因为如果不加以利用, 它就消失了。 我们可以通过右值引用来让它的生命周期延长。(左值引用会报错, 但是 常量左值引用 没有问题, 常量左值引用是一个很万能的类型)。

    那为什么不能使用 -> Copyable a <- 来接受参数呢

    回答:

    当使用Copyable a来接受参数的话, 会执行两次构造函数。 而使用引用类型, 只会执行一次构造函数。 (使用-fno-elide-constructors 来关闭返回值优化)

    • 移动构造函数

      有时候一些生成的临时字符串,我们可以使用右值引用来提高效率。

      • 普通参数

        两次构造函数, 效率最慢

      • 左值引用

        一次构造函数, 但是还是会拷贝, 类似于ctrl+c ctrl+v

      • 右值引用

        一次构造函数, ‘窃取’掉原来的数据, 类似于ctrl+x ctrl+v

    • 通用引用

          template<typename T>
          void fun(T&& t) {
              // code
          }
      

      这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

    • 完美转发

      // 一个不完美转发的例子
      void myforward(int&& i){
          process(std::forward<int>(i));
      }
      
      myforward(2); // process(int&&):2
      

      上述代码可以转发右值, 但是不可以转发左值。 可以通过通用引用来解决

      template<typename T>
      void myforward(T &&i) {
          process(std::forward<T> (i))
      }
      

11-25

  • 访问控制符

    • public继承(保持不变):派生类中基类部分的成员(函数或变量)的访问控制权限和其在基类中声明的权限一样;

    • protected继承(各自降低一个权限):基类中public成员在派生类中变成protected权限,其他成员都变为private权限;

    • private继承(全部私有化):派生类中基类部分的成员(函数或变量)的访问控制权限全部变为private成员。

    在public继承中, 派生类能访问的成员变量如下

    • 基类部分的public成员。请注意,尽管派生类的内存模型分为两部分,但这两部分都属于派生类对象。所以派生类(类本身)的所有成员函数(不管哪一种访问控制符)都能访问基类部分的public成员(函数或变量),同时派生类对象也直接可以访问(通过成员访问操作符)。

    • 基类部分的protected成员。派生类(类本身)的所有成员函数(不管哪一种访问控制符)都能访问基类部分的protected成员(函数或变量),但派生类对象不能直接访问(基类对象自己都不能访问它的protected成员,派生类当然不可以)。

    • 基类部分的private成员。派生类(类本身)的所有成员函数(不管哪一种访问控制符)都不能访问基类部分的private成员(函数或变量),同样派生类对象也不能直接访问。

习题笔记

  • 数组 作为 函数的参数是会退化为函数指针的,想想看,数组作为函数参数的时候经常是需要传递数组大小的

  • sizeof(new obj())时, static成员变量不会占据空间

  • 拷贝构造函数和赋值运算符的生效时间

    • 当变量不存在的时候,调用拷贝构造函数

      Obj obj = 2; // 调用拷贝构造函数

    • 当变量存在的时候, 调用赋值运算符函数

      Obj obj;
      obj = 2; // 调用赋值运算符函数

  • 当返回值为指针, 而函数实现里返回的是数组,此时结果不确定

    char *ret() {
        char str[] = "hello world";
        return str;
    }   //此时结果不确定
    

11-26

内存

一、进程中的mm_struct

首先每个进程在内核中都存在着一个mm_struct结构体。

在这个结构体里存放着各种的存储地址的变量, 具体看下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tk3oAXyb-1595378357919)(./mm_struct.png)]

二、malloc空间配置算法

malloc和STL空间配置器的思路相同,根据申请不同大小的空间而采用不同的分配方式

小于128k

从堆的开始地址起, 当有小于128k的需求, malloc会采用brk来分配空间,然后_edata指针增加,返回新建空间的最低地址(首地址)。

malloc使用brk来创建空间, 所有分配的区域都是连续的。 我们在程序中随机的调用free()会产生一些内存碎片。为了解决这种浪费情况, malloc会维护一个空闲链表。

下面是个例子:

1. void *A = malloc(30);    // _edata = 30
2. void *B = malloc(40);    // _mdata = 70
3. void *C = malloc(50);    // _mdata = 120
4. free(B);                 // _mdata = 120 但会出现40的内存碎片
5. void *D = malloc(40);    // _mdata = 120(不保证)
6. free(C);                 // _mdata = 70

参考算法: 首次适应算法, 最佳适应算法

大于128k

当有大于128k的需求, malloc采用mmap来分配空间。

采用mmap的原因是:只有出现高地址的free, _mdata才能减少, 而mmap可以单独释放

空闲链表

在brk起作用的时候, 内存中会有内存碎片, malloc会编织为一个空闲节点链表。以后申请小空间时, malloc会循着链表去寻找合适的空间来分配。(如果大于要申请的数目,把剩下的空间加入空闲链表)。
最后会出现非常多的小碎片,这时malloc会对其进行重新整理(仅整理相邻的节点, 要保证指向正在使用的空间的指针不会失效。)

缺页中断

在malloc申请后, 内核并不会马上给进程分配物理内存, 只是分配了虚拟内存地址,只有当其要进行写入操作的时候,才会产生缺页中断!

缺页中断:

查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

一般的中断过程:

  1. 保护现场
  2. 执行中断处理程序
  3. 恢复现场
  4. 执行下一条指令

缺页中断是一个特殊的中断, 过程如下:

  1. 保护现场
  2. 执行中断处理程序
  3. 恢复现场
  4. 重新执行产生中断的指令
x86架构Linux内存管理

PGD(page global directory)

页目录, 1024 * 4, 存放了1024个页表的信息

PT(page table)

页表, 前20位指向页, 后12个为标识位选项

offset

指向具体的地址

采用二级页表的初衷是为了减少无意义的页表的存在。
一个12M的进程其实仅仅需要一个PGD和三个PTE来存放。

OS缺页置换算法

当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:

先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。

最近最少使用(LRU)算法: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。

LFU(最不经常访问淘汰算法):每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。

当前最常采用的就是LRU算法。

进程线程

进程&线程之间的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o3moVdPq-1595378357921)(./进程线程的区别.png)]

由于有些方面比较简单,就不再赘述。

通信:

  • 进程(以下代码实现, 后期补充):

    • 管道(半双工)
    • 信号量
    • 共享内存
    • socket
    • 消息队列
    • 信号
  • 线程:

    • 信号量
    • 互斥量
    • 条件变量
    • 临界区

11-27

进程通信

管道

无名管道(pipe)

系统调用:pipe(int filedes[2])

filedes[0]为读取端, filedes[1]为写入端。

有名管道(FIFO)

系统调用:mkfifo(const char *pathname, mode_t mode)

    mkfifo("fifo_path", S_IFIFO|0666);
    int fd = open("fifo_path", O_RDWR);// 一端只读, 一端只写
    close(fd);
    unlink("fifo_path");    //使用完, 删除管道
共享内存

mmap函数

#include <sys.mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
// 成功返回映射区的起始地址, 若出错返回MAP_FAILED

由于linux的页式内存管理结构, 所以每次分配都是4k页面。

有名内存映射

    int fd = open("./sharedmap", O_CREAT|O_RDWR, 0644);
    int zero = 0;
    int *ptr;
    ptr = (int *)mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);  // 使用普通文件以提供内存映射
    close(fd);
    sem_t *mutex = sem_open("mysem", O_CREAT|O_EXCL, 0644, 1);
    sem_unlink("mysem");
    cout << ptr << endl;
    (*ptr) = 66;
    cout << (*ptr) << endl;
    cout << "start fork" << endl;
    if(fork() == 0) {
        for(int i = 0; i < 5; i++) {
            sem_wait(mutex);
            printf("child %d\n", (*ptr)++);
            sem_post(mutex);
        }
    }else {
        for(int i = 0; i < 5; i++) {
            sem_wait(mutex);
            printf("parent %d\n", (*ptr)++);
            sem_post(mutex);
        }

    }
}

匿名内存映射
和有名内存映射不同的只有fd选项, 设为-1

以上只是有亲缘关系的进程之间共享内存, 以下是无亲缘关系的进程间共享内存

shmget函数
创建一个新的共享内存区,或者访问一个已存在的共享内存区。

#include <sys/shm.h>
int shmget(key_t key, size_t size, int oflag);
// 成功返回shmid, 否则返回-1

key 可以通过ftok来完成。 ftok(filename, id);

shmat函数
由shmget得到shmid后, 通过shmat得到起始地址

void *shmat(int shmid, const void *shmaddr, int flag);
// shmaddr为空, 内核选择地址
// shmaddr不为空, 如果没有SHM_RND, 则直接续接到shmaddr。 有SHMRND, 则向下再取一个SHMLAB,低端边界地址。

shmdt函数
断接内存区, 但内核不会删除

int shmdt(const void *shmaddr);

具体使用:

int *p;
int shmid = shmget(IPC_PRIVATE, sizeof(int), SHM_R|SHM_W);
p = shmat(shmid, NULL, 0);
*p = 123;

进程状态转换图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htAutETl-1595378357922)(./进程七种状态转移图.png)]

并发

死锁产生的必要条件
  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

其他

  • 磁盘最短寻道需要先排序

11-28

网络

  • TCP拥塞控制

    慢启动: 从1开始, 二的指数级速度增长, 一直增长到ssthresh(慢开始阀值)

    拥塞避免: 当cwnd的值大于ssthresh之后, 每一个RTT就会增加1

    快速重传: 当收到三次DupACK后, 立即重传, 而不是采用超时重传的方式

    快速恢复: 收到DupACK(>=3次)后, ssthresh = cwnd/2, cwnd = ssthresh + 3SMSS。

  • TCP重传机制:

    超时重传:保持一个计时器, 如果到期仍未收到ACK, 则认为是超时,立即重传。

    DupACK: 收到三次及以上的重复的ACK, 认为是数据包丢失, 立即重传(时间应该比RTT短)。

    SACK: 在ACK回复中显示最近收到的包序号, 若无此选项, 则DupACK每次重传一个数据包, 由此选项, 可以连续发送多个数据包。

    Partial ACK: 当出现DupACK时,此刻的发送方已发送的序列号为K, 发送方重新发送DupACK所要求的数据包, 然后收到新的ACK, 但此ACK确认的序列号小于K, 这种情况下, 发送方认为还是存在丢失的数据包, 继续重传(此时的重传不需要三次以上的ACK)。

02-06

什么是虚函数?什么是纯虚函数?

虚函数是C++实现多态的一种设计, 具体的实现是在子类对象的内存起始处存放着一个虚函数表, 里面包含着父类的虚函数地址, 当子类重写了父类的虚函数,同时也会覆盖掉虚函数表里的地址。(子对象自己的虚函数是存放在第一个父类的虚函数表后的)
https://p-blog.csdn.net/images/p_blog_csdn_net/haoel/15190/o_vtable3.JPG

虚析构函数

当实现多态的时候, 要使用父类指针来销毁子类的内存空间, 只能使用虚函数的析构函数来进行实现。否则会存在内存泄漏的问题

vector的reserve和capacity的区别

vector 的设计为 数据区+缓冲备用区。 reserve就是改变缓冲备用区的大小, 而capacity则是返回 数据区+缓冲备用区的整体大小

static 和const分别怎么用,类里面static和const可以同时修饰成员函数吗

static 是静态修饰符, 被它修饰的变量或函数有以下的特点:

  • 自动初始化
  • 作用范围只在此源代码里
  • 和进程的生命周期一样长
  • 不在栈区, 而在特定的内存区域
    在C++中, 静态成员变量和静态成员函数都不属于某个对象, 因此静态成员函数没有this指针

const 是常量修饰符,const 类型 代表声明了一个某类型的常量, 指针比较特殊: const 类型* 指向常量类型的指针, 指向的内存区的内容不能改变; 类型 const * 指向某类型的指针,但指针的指向不能发生改变。

const static 可以共同修饰成员变量, 类似于类的常量值。 但是不能共同修饰成员函数, 因为const 会维护一个const this*, 而static 修饰的成员函数没有this

指针和引用的区别

  • 指针可以赋空值, 也可以先声明,后赋值。 引用必须在声明的同时赋值
  • 指针可以多级指向, 引用只是一层
  • 指针的指向可以改变, 引用不能
  • 解引用时, 指针要加*, 引用不用
  • sizeof 时, 指针取得值为指针的大小, 引用不是
  • 指针自增自减的含义和引用不同
  • 指针是声明了一个变量, 而引用只是取了别名

多态是什么?

C++有两种多态:静态多态(重载)和动态多态(虚函数)
静态多态是在编译期间就可以确定调用的函数地址,而动态多态则是运行时根据虚函数表来进行绑定的。
多态的目的是为了让接口重用

重载是 相同函数名, 不同参数和返回值。
重写是 覆盖掉父类的虚函数
隐藏是 父类的函数和子类的函数名相同,但参数不同; 父类的函数和子类的函数名和参数都相同,但是父类并没有加vitrual关键字

new和malloc的区别

  • new是运算符,而malloc是函数
  • new可以调用构造函数, 但是malloc不可以
  • new可以返回特定的指针类型,但是malloc只能返回void *
  • new可以被重载, 但是malloc不行

map的几种形式?

  • map
  • multimap
  • unordered_map
  • unordered_multimap

前两种为红黑树, 后两种为哈希表存放。
1 3不允许重复, 2 4允许重复

02-07

命令模式

将命令和实现者分离开, 命令是单独的类, 而具有命令功能的类仅作为命令的参数,类似于 KeyUp(tkeyborad;
https://blog.csdn.net/li1325169021/article/details/90717327

02-08

CPU调度算法

  • 最短时间优先
  • 最短剩余时间优先
  • 先来先服务
  • 优先级调度
  • 轮转调度

Linux进程调度算法

  • 进程调度优先级

    每个进程都有一个nice值, nice值越高, 优先权越低, 反之越高。

  • 分时调度策略(policy = SCHED_OTHER)

    每个进程的PCB中都存放着一个counter值和nice值, 根据counter - nice + 20(动态优先级)来进行排序, counter - nice + 20大的进程先运行, 当其进程的counter减为0(或者等待资源), 则让其他进程运行, 所有的进程时间片结束后, 进行下一轮的排序

  • 实时调度策略,先到先服务(policy = SCHED_FIFO)

    每个进程的PCB中都存放着一个rt_priority(1-99), 选择权值最高的进程运行, 如果有更高优先级的进程加入(或者等待资源), 则被剥夺运行权。

  • 实时调度策略,时间片轮转(policy = SCHED_RR)

    内核在就绪队列中找优先级最高的进程, 令其运行一个时间片(counter - nice + 20), 运行完则加入到等待队列尾, 然后内核重新寻找进程运行。

  • RR 和 FIFO 的区别

    FIFO中, 进程如果运行起来, 除非自己放弃或者有更高优先级的进程剥夺, 否则不会交出CPU执行权。 而RR则保证相同优先级的其他进程也可以执行

  • OTHER 最悲剧

    当实时进程进入, CPU会立即剥夺分时进程的运行权。

02-20

select 和 epoll 的优缺点

首先select是给程序员提供了可以一次性完成多个socket读写事件监控的一个工具, 但是他的缺点很多, 并不是一个很高效的工具

然后再介绍一下, 每个socket文件描述符中都有一个等待队列, 当发生IO事件, 则会唤醒其队列中的进程,进行操作

缺点:

  • select每次都要给要监控的socket的等待队列中添加自己, 时间复杂度O(n)
  • select返回的时候, 又会从要监控的socket的等待队列中删除自己, 时间复杂度O(n)
  • select要监控的socket每次都要从用户态拷贝到内核, 再从内核拷贝到用户态
  • 程序员需要遍历FDS数组才能得知哪些socket有IO事件, 时间复杂度O(n)

epoll则是对其进行了一系列的优化和改进, 其实要监听的文件描述符不会有很大的改变, 所以不需要每次执行IO后重新插入。 而且我们还可以将有事件的socket保存在数组中, 不需要让程序猿去遍历。

epoll 采用了双向链表来作为保存有IO事件的文件描述符的数据结构, 然后将自己维护的socket用红黑树来保存。
这样子新的socket加入监听和删除监听socket的时间复杂度就成了O(logn)

优点:

  • 将阻塞和添加等待队列分离, ready list 和 rbt 各司其职
  • 添加了就绪列表, 不需要O(n)遍历
  • 使用红黑树和双向链表来优化存储的数据结构

服务器不使用accept, tcp的三次握手能完成嘛?

可以完成, 使用代码实践, 是完成了的。

listen的backlog保存的就是未accept的完成三次握手的文件描述符个数

没有完成三次握手的文件描述符个数则是需要查看/proc/sys/net/ipv4/tcp_max_syn_backlog

再探页表

仅讨论x86的二级页表

第一级:页目录, 一个页目录4kb, 里面有1k个页表项(一个页表项4byte = 32 bit, 刚好是一个内存地址)。 逻辑地址前10位就是对应的某个页表项

第二级: 页表, 一个页表4kb, 里面有1k个表项, 每个表4byte(32bit), 前20位表示物理地址前二十位, 后面12位分别表示页表的性质, 例如缺页等,

第三级: 物理地址, 拿到页表中的前20位, 再加上逻辑地址最后的12位, 组成真实的物理地址, 12位刚好为4k,也就是一个内存页的大小。

会话, 控制终端, 进程组

  • 会话: 打开的shell就可以看作是一个会话
  • 进程组:。进程组是一个或多个进程的集合,通常它们与一组作业相关联,可以接受来自同一终端的各种信号。

守护进程的创建过程:

  • 摆脱当前会话,创建新的会话
  • 摆脱当前进程组
  • chdir
  • 关闭2以上的文件描述符
  • 屏蔽信号

伙伴系统

段式内存管理系统

首先先介绍一下外部碎片和内部碎片。

  • 外部碎片:操作系统未分配, 但是由于可用内存太小而不能分配的内存浪费, 段式管理系统会出现此问题

  • 内部碎片:操作系统分配给进程, 但是进程并未使用的内存浪费, 页式管理系统会出现此问题

逻辑地址: [段号, 段内偏移]

段内存访问: (段基地址 + 段内偏移), 先找到段表, 然后通过段表可以找到具体的某一个段表项, 然后根据段表项的基地址加偏移量得到物理地址

段表项: [段长, 基地址] 按顺序存储着不同段的信息

段页式管理系统

在段式管理系统的内部, 使用页式管理系统。

  • 逻辑地址:[段号, 页号, 页内偏移]

  • 段表项: [页表地址]

  • 页表项: [内存的基址]

段页内存访问: 先从寄存器+段号中得到段表地址, 然后根据段表的页表地址得到页表, 页表项中有内存的基址, 加上偏移量就得到物理地址

伙伴系统

将内存全部划分为2^n的大小, 系统维护着所有2^n类的链表, 例如2byte, 4byte, 8byte…

然后当有内存需求的时候, 给他分配最小的2^n的内存空间,若没有, 则向上寻找2^n+1, 如果有, 则分割, 此时出现两个内存块, 称之为伙伴内存, 一部分提供使用, 另一部分加入到2^n的链表中, 如果没有, 则继续向上寻找。

宏定义和inline的区别

inline : 与函数的实现放在一起 inline void fun() {}
区别:

  • inline更安全, define不安全, 因为不会进行参数类型检查
  • inline更像是函数, 而define只是单纯的替换 例如 plus(a + b) * plus(c + d)
  • inline是编译时替换, define是预编译时替换
  • inline可以作为类的成员函数

B树,B+树

B树的规则:

  • 根节点的孩子个数为[2, m]
  • 除根节点以外的节点的孩子个数为[m/2, m]
  • 节点中关键值个数为孩子数-1
  • 叶子结点都在一层

B+树的规则:

  • 数据只存在于叶子结点中
  • 节点的关键值个数为孩子数, 开闭区间
  • 叶子结点相互串成链表
  • 节点的关键值为左孩子中的最大值

都为矮胖型树, 适合做数据库的索引数据结构, 因为减少了IO, B树适合查找特定值, B+树适合查找区间, 为了所有的功能都最优, 所以数据库采用B+树来存储索引。

纯虚函数可以自己实现吗?

可以, 当父类要作为抽象类,但是没有虚函数时, 可以将析构函数设为虚函数, 然后在类外写实现(可以运行

redis持久化的方法

RDB(快照): fork出子进程进行持久化, 利用写时复制的特性, 可以完成某一节点前的数据完全备份

AOF(保存写状态): fork子进程进行持久化, 父进程持续将变化加入到原来的AOF和内存中, 父进程收到子进程重写AOF后, 又开始给新的AOF里写。然后旧的AOF就被替换了。

Reator模型

客户端的接入请求由accptor来处理, 然后分配给其他的工作线程。

02-23

HTTP的请求方式有哪些?

  • GET

    向制定资源请求数据,会被缓存, 一般在url中包含,有长度限制

  • POST
    向制定资源提交要被处理数据, 不会被缓存, 不再url中包含, 在HTTP的消息主体中, 没有长度限制

阻塞和非阻塞, 异步和同步

阻塞和非阻塞的概念是指: 当我们进行IO操作的时候, 是否要一直阻塞等待, 比如如果要进行读操作, 阻塞则是一直等待,区间CPU不给分配时间片,而非阻塞则会直接返回, 有数据则读取,没有数据则返回E_WOULDBLOCK

同步和异步的概念是指: 同步是发起一个调用时, 不允许离开, 必须得到结果之后,才能返回,而异步则是可以离开, 等调用结束之后, 发送信号执行回调函数。

IO的阻塞和非阻塞主要的区别是:是否一直监听IO是否有事件, 也就是我的调用是否可以立即返回。非阻塞IO发现有数据可读时, 也会等到从内核拷贝到用户态之后才能返回

IO的同步和异步主要的区别是:从内核拷贝到用户态的时候, 需不需要程序一直去监听。

02-26

RPC(远程过程调用):通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。

过程:

Client-》CLientStub-》-》ServerStub-》Server
客户端调用服务, 需要通过注册中心
对象要进行序列化, 在Server端进行反序列化
传输数据的网络协议(再次不止使用TCP, 可能为了性能便利性等因素,使用其他的协议)

Dubbo:

  • provide:暴露服务的服务方
  • container:服务运行的容器
  • consumer: 调用远程服务的客户方
  • register:服务注册于发现的注册中心
  • monitor: 监控服务调用次数和调用时间的监控中心

步骤:

  • container 启动provide
  • container 向register注册服务
  • register 给 consumer通知
  • consumer 调用 provide
  • monitor 对此次调用进行监控

QA:
用什么做注册中心?
推荐使用zookeeper

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值