2020秋招_C++开发面试记录-2

文章目录

海康威视

初筛

项目中用到的C++技术?

华为软挑开发的系统有没有用到多线程?

没有

现在来看加入多线程对系统性能提升有帮助吗?

没有,多线程主要解决并发问题。系统按时间片顺序执行,并不能拆解成多个小任务去同时执行。

类中的成员函数后用const修饰有什么作用?

被修饰的成员函数不能修改调用它的对象的成员变量

用什么关键字修饰成员变量可有让const成员函数修改?

mutable关键字

一面

inline关键字作用

cout输出的原理

重载了 << 运算符

负数计算机底层怎么实现的?冯诺依曼结构了解吗?

虚拟内存讲一下

作用:连续地址 -> 物理内存中离散地址,连续的地址方便程序员管理内存。
实现:页号+页偏移,页表(页码->页框码),MMU(计算物理地址,快表)…

如何处理多并发情况

epoll + 线程池

如何减少分配内存过程中产生的内存碎片?

内存池,预分配内存,讲述了下malloc底层的实现。(C++中用什么可以实现?)

一个相机拍摄视屏,有多个客户端获取视频数据,怎么设计?

buffer + 读写锁

OPPO

一面

C++与java\C语言的区别

算法题

题目:1~1000这1000个整数放入大小为999的数组,用O(n)的时间复杂度和O(1)的空间复杂度找到这个缺失的数。
思路:1~1000用求和公式求和,然后遍历数组,依次减去每个元素,最后剩下的值即为缺失的数。

字节-教育业务

一面

C++ unordered_map底层实现

C++ STL无序容器底层实现原理(深度剖析)
总结:链地址法解决哈希冲突。vector存放链表的头指针,各键值对真正的存储位置是各个链表的节点。

C++ STL 标准库中,不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用“链地址法”(又称“开链法”)解决数据存储位置发生冲突的哈希表。
无序容器
当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。

其中,Pi 表示存储的各个键值对。
注意,STL 标准库通常选用 vector 容器存储各个链表的头指针。

链表过长,遍历效率低下怎么解决? -> 用红黑树,节点存放键值对。

epoll和select区别

区别 select epoll
事件集合 用户通过三个参数分别传入感兴趣的可读、可写及异常事件 内核通过一个事件表直接管理用户感兴趣的所有事件
最大支持文件描述符数 一般有最大值限制 系统允许打开的最大文件描述符数目(65535)
工作模式 LT LT和ET
内核实现和工作效率 采用轮询的方式检测就绪事件O(n) 采用回调方式检测就绪事件O(k)

http返回状态码

  • 200 -> OK
  • 400 -> Bad Request(请求报文语法错误),403 -> Forbidden(服务器拒绝执行此请求),404 -> Not Found(服务器无法根据客户端的请求找到资源)
  • 502 -> Bad Gateway(作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应), 500 -> Internal Server Error(服务器内部错误,无法完成请求)

https加密过程

HTTPS在传统的HTTP和TCP之间加了一层用于加密解密的SSL/TLS层(安全套接层Secure Sockets Layer/安全传输层Transport Layer Security)层。使用HTTPS必须要有一套自己的数字证书(包含公钥和私钥)。
HTTPS加密过程

  • 是客户端向服务端请求https连接,服务端接收到请求后返回证书(公钥);
  • 客户端产生随机密钥,并用公钥对密钥加密产生私钥,然后向服务端发送加密后的私钥;服务端收到私钥,返回确认。
  • 客户端和服务端用私钥加密的密文通信,并用私钥解密密文。

http2.0了解么

HTTP发展史(HTTP1.1,HTTPS,SPDY,HTTP2.0,QUIC,HTTP3.0)

ctrl+c怎么终止服务器的

Linux关闭与切换进程相关的信号:SIGINT、SIGKILL、SIGTERM、SIGSTOP

  1. ctr+c触发:(kill foreground process) 发送 SIGINT 信号给前台进程组中的所有进程,强制终止程序的执行;(INT -> interrupt) -> 只能终止前台进程组中的所有进程。
  2. ctrl+z触发: ( suspend foreground process ) 发送 SIGSTOP 信号给前台进程组中的所有进程,常用于挂起一个进程,而并非结束进程,用户可以使用使用fg/bg操作恢复执行前台或后台的进程。
  3. kill <PID>发送SIGTERM信号到指定进程,如果进程没有捕获该信号,则进程终止。SIGTERM是程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。
    SIGKILL:kill -9 <PID> 会发送这个信号,用来强制使进程立即结束。
    SIGKILL是不能被捕获的,程序收到这个信号后,一定会退出。这就是kill -9一定能保证将程序杀死的原因。

算法题:反转链表中部分结点

92. 反转链表 II
思路一:pre为节点m前面一个节点(如果m等于1,就建立一个辅助节点),tail为节点n后面一个节点(tail可能为空)。
pre->next = tail;
然后在pre和tail之间用头插法插入first开始的节点,直到first指向tail结束。

    ListNode* reverseBetween(ListNode* head, int m, int n) {
    
    
        ListNode *pre, *tail, *first; 
        ListNode *h = head;
        if(m==1){
    
    
            pre = new ListNode(0);
        }
        // 如果n为链表最后一个节点,那么tail为nullptr,因此tail初始化为nullptr
        tail = nullptr; 
        int cnt = 0;
        while(h!=nullptr){
    
    
            cnt++;
            if(cnt == m-1){
    
    
                pre = h;
            }else if(cnt == m){
    
    
                first = h;
            }else if(cnt == n+1){
    
    
                tail = h;
            }
            h = h->next;
        }yizu
        if(pre==nullptr){
    
     // m大于链表长度情况
            return head;
        }
        pre->next = tail;
        // 头插法
        ListNode* tmp, *p = first;
        while(p!=tail){
    
    
            tmp = p->next;
            p->next = pre->next;
            pre->next = p;
            p = tmp;;
        }

        return m==1? pre->next : head;
    }

思路二:一次遍历做到。
在这里插入图片描述
注意:如果m=1,则需要一个辅助节点pre。

    ListNode* reverseBetween(ListNode* head, int m, int n) {
    
    
        ListNode* p = head, *pre, *cur, *tail, *tmp;
        if(m==1){
    
    
            pre = new ListNode(0);
            pre->next = head;
        }
        int cnt = 0;
        while(p!=nullptr){
    
    
            cnt++;
            if(cnt<=m){
    
    
                if(cnt==m-1){
    
    
                    pre = p;
                }else if(cnt==m){
    
    
                    cur = p; 
                    tail = p;
                }
                p = p->next;
            }else if(cnt>m && cnt<=n){
    
    
                tmp = p->next;
                p->next = cur;
                cur = p;
                p = tmp;
            }else{
    
    
                break;
            }
        }
        pre->next = cur;
        tail->next = p;
        return m==1? pre->next : head;
    }

相似题目:
24. 两两交换链表中的节点
25. K 个一组翻转链表

阿里云-边缘计算

一面

TCP三次握手,四次挥手

反转单词顺序

剑指 Offer 58 翻转单词顺序

接雨水,leetcode42

    int trap(vector<int>& height) {
    
    
        int n = height.size();
        // l[i]表示i左边最高的柱子,r[i]表示i右边最高的柱子
        vector<int> l(n,0),r(n,0);
        int m = 0;
        for (int i = 1; i < n; i++) {
    
    
            m = max(height[i-1],m);
            l[i] = m;
        }
        m = 0;
        for (int i = n-2; i >= 0; i--) {
    
    
            m = max(height[i+1],m);
            r[i] = m;
        }
        int h = 0, count = 0;
        // 当前位置能储蓄多少水,取决于左右两边最高的的柱子中相对比较低的那一根柱子的高度
        for(int i = 0; i < n; i++) {
    
    
            h = min(l[i],r[i]);
            count += max(h-height[i],0);
        }
        return count;
    }

二面

聊实习和项目

虚函数的内存实现?多继承情况下呢?

腾讯云(CSIG )

手撕代码

  1. 有序数组,判断是否存在两个元素和为n
  2. 有序数组,找到不大于n的最大整数返回
  3. 有不同面值的硬币,凑出面值为n的方案数,要求不能有重复(完全背包问题,一维dp时,硬币在外层,求组合数)

迭代器的种类

C++五种类型的迭代器

迭代器 读、写 运算
Input iterator(输入迭代器) 读,不能写 只支持自增运算
Output iterator(输出迭代器) 写,不能读 只支持自增运算
Forward iterator(前向迭代器) 读和写 只支持自增运算
Bidirectional iterator(双向迭代器) 读和写 支持自增和自减运算
Random access iterator(随机访问迭代器) 读和写 支持完整的迭代器算术运算

vector、deque 和string 迭代器是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。

    vector<int> v{
    
    1,2,3,4,5,6,7,8,9};
    vector<int>::iterator iter = v.begin()+1;
    cout << iter[3] << endl;  // iter[3]表示*(iter+3)
    cout << *(iter+3) << endl;
    return 0;

map、vector、list迭代器删除节点之后其他迭代器会不会失效

map、list是链式存储,vector是顺序存储。

  • map删除当前结点之后,之后的迭代器所指的元素不会改变;但是由于是链式存储,删除节点的时候iter++,并不会指向下一个节点(程序没崩溃??)。
  • vector删除当前结点之后,后面的元素都向前移动,之后迭代器所指的元素改变,这会造成逻辑的错误;因此删除节点的时候,不用执行iter++,会自动指向下一个元素。(vector要注意插入元素后进行动态扩容的情况,换了新的地址,所有迭代器都会失效!
  • list删除iter所指的节点之后,当前位置之后的迭代器所指的元素不会改变;但是不能iter++,因为iter所指的元素已经失效,iter++相当于node = node->next,程序会奔溃。

解决方案:
删除元素时返回下一个元素的迭代器:
iter = xxx.erase(iter);
插入元素时返回当前插入元素的迭代器:
iter = xxx.insert(iter);

#include <iostream>
#include <map>
#include <vector>
#include <list>
using namespace std;

int main() {
    
    
    map<int,string> mp;
    mp[0] = "zero"; mp[1] = "one";mp[2] = "two";mp[3] = "three";
    for(map<int,string>::iterator iter = mp.begin(); iter!=mp.end(); ) {
    
    
        if(iter->second == "one"){
    
    
            iter = mp.erase(iter); // erase返回的就是下一个有效的元素了,因此不用++了
        }else {
    
    
            iter++;
        }
    }
    for(map<int,string>::iterator iter = mp.begin(); iter!=mp.end(); iter++) {
    
    
        cout << iter->second <<" ";
    }
    cout << endl;

    vector<int> vec{
    
    1,2,3,4};
    for(vector<int>::iterator iter = vec.begin(); iter!=vec.end();) {
    
    
        if(*iter == 2){
    
    
            // vec.erase(iter); // 删除后iter指向下个有效的元素(因为后面的元素向前移动了)
            // cout << *iter << endl; // 返回3
            iter = vec.erase(iter);
        }else {
    
    
            iter++;
        }
    }
    for(vector<int>::iterator iter = vec.begin(); iter!=vec.end(); iter++) {
    
    
        cout << *iter << " ";
    }
    cout << endl;
    auto iter = vec.begin();
    // 在iter所指元素之前插入,运行完后iter指向新插入的元素(这与list不同)
    vec.insert(iter,2); 
    iter++;
    cout << *iter << endl;

    list<int>  l;
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    l.push_back(4);
    l.push_back(5);

    list<int> ::iterator it=l.begin();
    // while(it!=l.end()){
    
    
    //     if(*it%2==0){
    
    
    //         l.erase(it); // erase之后节点被释放,但当前迭代器还是指向该节点     
    //     }
    //     it++; // 循环中进行++操作时,其实是执行了node=node->next,访问非法地址,程序会崩掉。
    // }
    while(it!=l.end()){
    
     // 删除偶数结点
        if(*it%2==0){
    
    
            it = l.erase(it); // 与vector不同的是,list必须返回it
        }else{
    
    
            it++;
        } 
    }
    it=l.begin();
    while(it!=l.end()){
    
     // 删除偶数结点
        cout << *it << " "; 
        it++;
    }
    cout << endl;
    it=l.begin();
    // 在iter所指节点之前插入节点,运行完后iter仍指向当前节点(这与vector不同)
    l.insert(it,0);
    it++;
    cout << *it << endl;
    return 0;
}

单例模式,实现方法

饿汉模式、懒汉模式,懒汉模式可以用局部静态变量、双检测的方式实现。

常见的锁有哪些

互斥锁、条件锁、自旋锁、读写锁

epoll的两种工作模式

LT和ET

主线程读写,工作线程处理报文,主线程怎么知道是否读取了一个完整的报文?

用户进程缓冲区和内核缓冲区
从状态机负责读取报文一行,主状态机负责对该行数据进行解析。

  • line_status 为 LINE_OPEN 时会跳出循环,返回 NO_REQUEST,表示请求不完整,需要继续读取请求报文数据。然后在 http_conn::process() 会向 epollfd 注册当前 sockfd 为读事件。

网易杭研院

一面

红黑树和B+树的区别,为什么map用红黑树而不用B+树?

map是STL标准库中的关联式容器。B树节点上有很多数据,每个节点中的数据是顺序存储,插入、删除操作将使插入、删除位置之后的元素移动,造成迭代器失效;红黑树一个节点存放一个数据,插入、删除一个节点,不会使其它节点的迭代器失效。

B树:从磁盘中查找数据(节点数据先读取到内存、后查找)的过程中,可以减少磁盘 IO 的次数,从而提升查找速度。
不考虑是内存IO还是磁盘IO,红黑树和B树的查找原理是一样的,都是二分查找。

new/delete和malloc/free的区别

  • 本质区别:new/delete是操作符,由编译器控制,new的时候自动调用构造函数,delete的时候自动调用析构函数;malloc/free是库函数,不会自动调用构造和析构函数;new/delete由于是操作符可以重载,malloc/free不行。
  • 异常返回:分配失败new抛出bad_alloc异常,malloc返回NULL;
  • 返回类型:malloc成功返回空指针,需要强制类型转换成所需要的类型指针;
  • 内存区域:new是自由存储区,malloc是堆。自由存储区不完全等于堆。(通常new在堆中分配内存,但是布局new可以让程序员自己指定分配内存的地方)

linux中栈内存和堆内存的区别

  • 管理方式:栈编译器管理,堆人工管理
  • 空间大小
  • 是否产生内存碎片
  • 分配方式、效率
  • 生长方式:堆向着内存地址增加的方向;栈向着内存地址减少的方向。

栈溢出的原因?怎么解决?

栈保存函数内部的局部变量,利用栈的先进后出特性,可以实现函数的嵌套调用。
栈溢出几种情况及解决方案

原因:

  1. 数组越界
  2. 函数递归调用层数太深
  3. 函数内局部数组过大

解决:

  • 针对数组越界,可以进行数组越界的检查;
  • 针对函数递归调用层数太深,可以增大栈空间,或者进行一些剪枝、打表的操作,减少函数递归调用层数;
  • 针对函数内局部数组过大,可以改用动态分配,使用堆上的内存。

linux内存管理方式

浅谈Linux内存管理

  • 虚拟内存管理
  • 段页机制
  • 缓存机制

编译时链接库和运行时链接库的区别?应用场景?

区别:静态库在程序编译时就会被连接到目标文件中生成可执行文件,每个进程都会复制一份,因此静态库在内存中存在多分拷贝;动态库在运行的时候才载入,内存中只有一份,被进程之间所共享。

  • 静态库的优缺点
    1、优点
    静态链接相当于复制一份库文件到可执行程序中,不需要像动态库那样有动态加载和识别函数地址的开销,也就是说采用静态链接编译的可执行程序运行更快
    2、缺点
    1)静态链接生成的可执行程序比动态链接生成的大很多,运行时占用的内存也更多。
    2)库文件的更新不会反映到可执行程序中,可执行程序需要重新编译。
  • 动态库的优缺点
    1、优点
    1)相对于静态库,动态库在时候更新(修复bug,增加新的功能)不需要重新编译。
    2)全部的可执行程序共享动态库的代码,运行时占用的内存空间更少。
    2、缺点
    1)使可执行程序在不同平台上移植变得更复杂,因为它需要为每个不同的平台提供相应平台的共享库。
    2)增加可执行程序运行时的时间和空间开销,因为应用程序需要在运行过程中查找依赖的库函数,并加载到内存中。
  • 编译的优先级
    静态库与动态库各有优缺点,该怎么选择,要看应用的场景。
    所谓有得必有失,动态库在程序运行时被链接,故程序的运行速度和链接静态库的版本相比必然会打折扣。然而瑕不掩瑜,动态库的不足相对于它带来的好处在现今硬件下简直是微不足道的,所以链接程序在链接时一般是优先链接动态库的,除非用-static参数指定链接静态库。

TCP流量控制

端到端的过程。当接收方来不及处理发送方的数据,通过滑动窗口,提示发送方降低发送的速率,防止包丢失。

C++11新特性

  • 语法:初始化列表、nullptr、auto自动类型推导、基于范围的for循环、lambda表达式
  • 右值引用和移动语义(std::move将左值转化为右值,减少了不必要的临时对象创建、拷贝和销毁);std::unique_ptr和std::thread禁止了拷贝和赋值构造,只允许移动拷贝和移动复制构造。
  • 智能指针std::unique_ptr、std::shared_ptr
  • 线程std::thread

手撕代码

面试题 16.05. 阶乘尾数
思路:一个2和一个5就可以得到10,2的个数肯定多余5,因此题目可以转换为找5的个数。
5的倍数的个数+25的倍数的个数+125的倍数的个数…

    // O(logn)
    int trailingZeroes(int n) {
    
    
        int count = 0;
        while (n > 0) {
    
    
            count += n/5;
            n/=5;
        }
        return count;
    }

二面

手撕代码:生产者,消费者模式

fork子进程,内存会拷贝一份嘛?

fork之后子进程到底复制了父进程什么
“写时复制“技术
假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。
(注1:在理解时,你可以认为fork后,这两个相同的虚拟地址指向的是不同的物理地址,这样方便理解父子进程之间的独立性)
(注2:但实际上,linux为了提高 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页框),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))

如何检查程序内存泄露?

内存泄漏的场景分析和避免方法总结,C语言内存泄漏详解
如何避免内存泄漏:使用智能指针、引用代替指针、RAII

由于栈上的内存的分配和回收都是由编译器控制的,所以在栈上是不会发生内存泄露的,只会发生栈溢出(Stack Overflow),也就是分配的空间超过了规定的栈大小。

C++ 如何避免内存泄漏
C程序内存泄露检测工具——Valgrind

C++中的RAII机制 -> RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。(编译器自动调用)

gc如何判断内存可以回收?

聊聊Java的GC机制 -> 可达性分析与GC Roots

hr面

问项目相关…

腾讯音乐

一面

哈希冲突解决的办法?

解决哈希冲突的常用方法分析
开放定址法(线性探测、二次探测、双散列函数探测)、链地址法、再哈希法

链地址法接的链表太长,查询的事件复杂度会不会过大?

【面试必备】2020最新Java集合容器面试题
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  • 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  • 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  • 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

腾讯微信

一面

连续子数组最大和

实现strcpy函数

面试题之strcpy/strlen/strcat/strcmp的实现

进程的内存布局,共享内存所在区域?

进程间的通信方式(一):共享内存
概念:共享内存是在物理内存上开辟一块区域,被多个进程映射到自己进程的虚拟地址空间上,这些进程就可以直接访问该共享内存区域,从而通过该区域实现各进程间的通信。共享内存是进程间最快的一种通信方式,一个进程向共享内存上面写数据,共享这块内存的所有进程都可以看到其中的内容,这块共享内存的页面,出现在所用共享该页面进程的页表中,给人一种就是在访问自己地址空间里面的数据一样。共享内存映射图:
共享内存
可以看到进程A和进程B共享了一块内存,分别将共享内存所在的物理页加入到自己的页表中,访问时就像访问自己的东西一样,所以它是最快的一种通信方式。但是会有一个问题,就是可能存在多个进程同时访问这块区域,此时共享内存区域就成了临界资源,所以我们在使用共享内存时需要对它进行同步控制才能保证安全的使用。比如信号量、加锁等方式。

虚拟内存技术,MMU,页表结构?

【Linux】Linux的虚拟内存详解(MMU、页表结构)
以存储单元为单位来管理显然不现实,因此Linux把虚存空间分成若干个大小相等的存储分区,Linux把这样的分区叫做。为了换入、换出的方便,物理内存也就按也得大小分成若干个块。由于物理内存中的块空间是用来容纳虚存页的容器,所以物理内存中的块叫做页框。页与页框是Linux实现虚拟内存技术的基础。
页模式下虚拟地址、物理地址转换:(p页码,f页框码、d偏移量)
页模式下虚拟地址、物理地址转换

系统请页
系统请页
Linux系统使用最近最少使用(LRU)页面的衰老算法。这种策略根据系统中每个页面被访问的频率,为物理页框中的页面设置了一个叫做年龄的属性。页面被访问的次数越多,则页面的年龄最小;相反,则越大。而年龄较大的页面就是待换出页面的最佳候选者。

快表和MMU
MMU
页保护
页表实际上是由虚拟空间转到物理空间的入口。因此,为了保护页面内容不被没有该页面访问权限的程序所破坏,就应在页表的表项中设置一些访问控制字段,用于指明对应页面中的内容允许何种操作,从而禁止非法访问。
页表项
Linux的页表结构
为了通用,Linux系统(32位)使用了三级页表结构:页目录、中间页目录和页表。PGD为顶级页表,是一个pgd_t数据类型(定义在文件linux/include/page.h中)的数组,每个数组元素指向一个中间页目录;PMD为二级页表,是一个pmd_t数据结构的数组,每个数组元素指向一个页表;PTE则是页表,是一个pte_t数据类型的数组,每个元素中含有物理地址。
三级页表

为什么使用多级页表?

线程池实现原理?惊群怎么解决?连接数很大线程数量有限,处理不过来了怎么办?

线程池实现原理:任务队列tasks;互斥锁queue_mtx保护任务队列;条件变量not_empty_cond任务队列为空时阻塞空闲的工作线程,有任务时发出任务队列非空信号唤醒工作线程;工作线程组workers。
线程池构造时预创建工作线程,由于任务队列为空,所有线程阻塞 -> 有新的任务到来加入任务队列enqueue() -> notify_one()随机唤醒一个工作线程执行任务。

惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。

惊群解决:实际上现在的Linux内核实现中不会出现惊群了,只会有一个进程被唤醒(Linux2.6内核)。使用mutex锁住多个线程是不会惊群的,在某个线程解锁后,只会有一个线程会获得锁,其它的继续等待。
闲扯Nginx的accept_mutex配置

  • accept_mutex的意义:当一个新连接到达时,如果激活了accept_mutex,那么多个Worker将以串行方式来处理,其中有一个Worker会被唤醒,其他的Worker继续保持休眠状态;如果没有激活accept_mutex,那么所有的Worker都会被唤醒,不过只有一个Worker能获取新连接,其它的Worker会重新进入休眠状态,这就是「惊群问题」。
  • 简单点说:Apache动辄就会启动成百上千的进程,如果发生惊群问题的话,影响相对较大;但是对Nginx而言,一般来说,worker_processes会设置成CPU个数,所以最多也就几十个,即便发生惊群问题的话,影响相对也较小。

并发的连接数很大怎么办?-> IO多路复用,epoll,单个线程处理多个连接。

线程池线程数设置多少?

线程数,射多少更舒适?
主要考虑IO密集型还是CPU密集型。web服务器是IO密集型。

  • 对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率,所以对于一个 8 核的 CPU,每个核一个线程,理论上创建 8 个线程就可以了。
  • 对于 IO 密集型任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

服务器在什么情况下系统cpu占有率比较高?

mpstat指令监测多处理器上每个CPU的使用情况。
%usr:系统上所有进程运行在用户空间的时间占CPU总运行时间的比例。
%sys:系统上所有进程运行在内核空间的时间占CPU总运行时间比例。
%usr、%sys、%idle分别反应了业务逻辑代码和系统调用所占的比例,以及系统还能承受多大的负载。
在进行压力测试的时候,%sys比%usr要多,因为在压力测试的时候在不停地执行recv/send系统调用来收发数据。

两阶段锁了解吗

两阶段锁协议

  1. 一次性锁协议,事务开始时,即一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。一次性锁协议不会产生死锁的问题,但事务的并发度不高。
  2. 两阶段锁协议,整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。

MVCC又称为乐观锁,它在读取数据项时,不加锁;在更新数据项时,直到最后要提交时,才会加锁。与MVCC相对,基于锁的并发控制机制称为悲观锁,因为它认为其他事务修改自己正在使用的数据项的概率很高,因此对数据项加锁以阻塞其他事务的读和写。

MySql-两阶段加锁协议

inode了解吗

inode 和 block 概述
inode包含很多的文件元信息,但不包含文件名,例如:字节数、属主UserID、属组GroupID、读写执行权限、时间戳等。
而文件名存放在目录当中,但Linux系统内部不使用文件名,而是使用inode号码识别文件。对于系统来说文件名只是inode号码便于识别的别称。

拼多多

一面

IO多路复用的作用?select、poll、epoll的区别?

同步IO和异步IO的区别?

反转链表(貌似有点错误)

双指针:每次反转一个节点
最开始注意检查特殊情况,如链表是否为空!
nullptr(pre) 1§ ->2(tmp)->3->nullptr
反转第一个节点:nullptr<-1(pre) 2§<-3(tmp)->nullptr
反转第二个节点:nullptr<-1<-2(pre) 3§->nullptr(tmp)
此时p->next==nullptr,退出循环,循环再来一步p->next =pre
nullptr<-1<-2(pre) <-3§ nullptr(tmp)

    ListNode* reverseList(ListNode* head) {
    
    
        if(head==nullptr){
    
    
            return nullptr;
        }
        ListNode* p = head, *pre = nullptr, *tmp = p->next;
        while(p->next!=nullptr){
    
    
            p->next = pre;
            pre = p;
            p = tmp;
            tmp = p->next;
        }
        p->next = pre;
        return p;
    }

整型数组中乘积最大的三个数(陷阱题)

数组有正有负、乘积可能会越界、数组个数是否小于三个数
思路:排序数组,取最大的三个数的乘积,与最小两个数和最大一个数的乘积的大者为乘积最大的三个数。(这么选取是因为两个负数相乘得正,所以也要考虑最大的两个负数)
-2 -1 3 4 5 这种情况乘积最大的三个数为 3 4 5
-3 -2 -1 5 6 这种情况乘积最大的三个数为 -3 -2 6
-100 -50 0 1 2 3 这种情况乘积最大的三个数为 -100 -50 3
-5 -4 -3 -2 -1 这种情况乘积最大的三个数为 -3 -2 -1 (全为负数也不例外)

特殊情况:数组个数小于三个需要在最开始进行单独判断

对于排序后的数组 m n a b c d e fg h i j k x y z
考虑两个数相乘可能会越界,怎么办?
由于乘积最大只可能是两种情况(mnz或者xyz),两种情况返回值分别设置为0和1
首先判断最小两个数是否都为负,不是的话乘积最大就是最大的三个数(返回1);
如果最大的数为负,则乘积最大就是最大的三个数(返回1);
如果第二大和第三大两个数不是全为正(一正一负或者两负),则乘积最大就是最小两个数和最大一个数(返回0);
如果第三大第二大两个数为两正(x,y),最小两个数为两负(m,n),则可以首先比较m/x的绝对值是否大于y,大于y则一定是m与n的乘积大(返回0);小于y,再比较(m/x)*(n/y)是否大于1,大于1则m与n的乘积大(返回0),小于1则x与y的乘积大(返回1)。

大华

一面

stl是线程安全的吗?举例线程不安全的情况

STL线程安全情况:多个线程同时读取一个容器中的内容是线程安全的;多个线程对不同容器的同时写入是线程安全的。
对于同一容器当有线程写,有线程读时,是线程不安全的,需要程序要通过加锁等方式保证线程安全。
线程不安全情况举例:当调用map的任何接口时,比如 end(), begin(), find()等,可能会返回一个iterator,如果有别的线程正在修改这个map,你的iterator就变得无效了,再用这个iterator行为就可能出问题。或者在find()函数内部,会访问到map内部的红黑树的数据结构,而这个红黑树是有可能被别的线程调整的(比如别的现在往map中插入一个不存在的记录)。

虚函数作用

C++中使用虚函数是为了实现运行时多态,即父类指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,实现“同一个接口多种不同的实现形式”。父类中定义纯虚函数,子类中定义同名同参的虚函数来实现。

虚表原理

C++实现虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表(virtual function table, vtbl)。(C++的编译器会保证虚函数表的指针存在于对象实例中最前面的位置)

虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址(可以理解为数组里存放着指向每个虚函数的指针)。即:每个类使用一个虚函数表,每个类对象用一个虚表指针

哈希map原理?会有什么问题?怎么解决?

解决哈希冲突的常用方法分析
内部维护一张哈希表,通过查找哈希表实现key-value的映射。会出现哈希冲突,通过开放定址法(线性探测、二次探测、双散列函数探测)、链地址法、再哈希法、建立公共溢出区。哈希map的key是无序的

哈希算法:根据设定的哈希函数H(key)和处理冲突方法将一组关键字映象到一个有限的地址区间上的算法。也称为散列算法。
双散列函数探测:探查序列的步长值是同一关键字的另一散列函数的值。
链地址法:链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。

快速排序原理?最坏情况?基准如何选取?

什么是非阻塞socket

阻塞的意思是指,当试图对该文件描述符进行读写时,如果当时没有东西可读或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。** 非阻塞的意思是,当没有东西可读或者不可写时,读写函数就马上返回,而不会等待**。

为什么要用epoll?select什么情况下会优于epoll?

select、poll、epoll之间的区别(搜狗面试)
epoll可以同时处理多个连接,并且通过在内核中维护红黑树、就绪链表,回调函数通知机制。
Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

自旋锁、读写锁、互斥锁的区别?

页面置换算法

在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。

  1. FIFO 先进先出。选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。
  2. OPT 最佳置换。下一次被访问时间最靠后的页面最先被置换。(实际上操作系统无法知道各个页面下一次是在什么时候被访问,因此这只是在理想情况下的算法)
  3. LRU 最近最少使用。当需要置换一页时,选择在之前一段时间里最久没有使用过的页面予以置换。

LRU缓存机制实现原理

LRU 有序的双向链表(按最近使用时间排序,最近刚使用过的排在最前面)和哈希映射实现。哈希map通过key映射到双向链表的节点,使得双向链表的插入和删除操作时间复杂度都为O(1)。
需要访问的页面如果在链表中存在,则从链表中删除,然后插入在链表头部;在链表中不存在,则先删除链表尾部页面,然后在链表头部插入该页面。

mysql四种隔离级别?幻读怎么发生的?

幻读是指在一个事务内两次查询中数据行数不一致。导致该问题的原因是查询的过程中其它的事务做了添加操作
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读可能发生在update,delete操作中(需要行锁保护记录的更改和删除,从而可重复读),而幻读发生在insert操作中(行锁无法阻止数据的插入)。

二面

聊实习和项目

链表如何排序

猜你喜欢

转载自blog.csdn.net/XindaBlack/article/details/107875071