后台开发常见问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhougb3/article/details/81148630
  1. 内核空间和用户空间:

    内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 内核和进程驻留在各自独立的虚拟空间中。每个进程可以通过系统调用进入内核。

    操作系统将物理内存划分为用户空间和内核空间,保证了数据上的隔离,有效地保护了系统。操作系统的内核由系统内所有进程共享,映射到所有进程的最高部分的虚拟地址空间。通过系统调用,异常和中断可以从用户态切换到内核态。

    参考:
    https://blog.csdn.net/zhangskd/article/details/6956638
    内核态(内核空间)和用户态(用户空间)的区别和联系

  2. 用户态和内核态

    当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

  3. socket编程:

    1. 完整的套接字格式是一个五元组,指定了源地址,源端口,目标地址,目标端口,协议类型(TCP/UDP)

    2. 当监听套接字和连接套接字使用同一个Ip地址和端口时,TCP无法仅仅通过查看目的端口号来分离外来的分节到不同的端点。它必须查看套接字对的所有4个元素才能确定由哪个端点接收某个到达的分节。如果没有对应的源地址和源端口,则会与监听套接字连接。

    3. TCP协议栈在内核空间维护着两个socket缓冲区:send buffer和recv buffer。

    4. 对于服务端而言,先调用socket()函数生成一个套接字,再通过bind()函数绑定一个地址和端口,再调用listen函数让套接字变成监听状态。listen()函数还维护了两个队列:连接未完成队列和连接已完成队列。当监听者接收到某个客户端发来的SYN并回复了SYN+ACK之后,就会在未完成连接队列的尾部创建一个关于这个客户端的条目,并设置它的状态为SYN_RECV。显然,这个条目中必须包含客户端的地址和端口相关信息。当服务端再次收到这个客户端发送的ACK信息之后,监听者线程通过分析数据就知道这个消息是回复给未完成连接队列中的哪一项的,于是将这一项移入到已完成连接队列,并设置它的状态为ESTABLISHED。

    5. 客户端也是调用socket()函数生成一个套接字,然后使用connect()函数向某个已监听的套接字发起连接请求(成功则返回0,失败返回-1)。在调用connect时要带上源地址,源端口,目标地址,目标端口。于是,TCP连接的两端的套接字都已经成了五元组的完整格式。

    6. accpet()函数的作用是读取已完成连接队列中的第一项(读完就从队列中移除),并对此项生成一个用于后续连接的套接字描述符。

    7. send()函数是将数据从app buffer复制到send buffer中(当然,也可能直接从内核的kernel buffer中复制),recv()函数则是将recv buffer中的数据复制到app buffer中。当然,使用write()和read()函数替代它们并没有什么不可以,只是send()/recv()的针对性更强而已。可以使用select()/poll()/epoll去监控对应的文件描述符(对应socket buffer则监控该socket描述符),当满足条件时,再去调用send()/recv()就可以正常操作了。

    8. 通用的close()函数可以关闭一个文件描述符,当然也包括面向连接的网络套接字描述符。当调用close()时,将会尝试发送send buffer中的所有数据。但是close()函数只是将这个套接字引用计数减1,就像rm一样,删除一个文件时只是移除一个硬链接数,只有这个套接字的所有引用计数都被删除,套接字描述符才会真的被关闭,才会开始后续的四次挥手中。对于父子进程共享套接字的并发服务程序,调用close()关闭子进程的套接字并不会真的关闭套接字,因为父进程的套接字还处于打开状态,如果父进程一直不调用close()函数,那么这个套接字将一直处于打开状态,见一直进入不了四次挥手过程。

      而shutdown()函数专门用于关闭网络套接字的连接,和close()对引用计数减一不同的是,它直接掐断套接字的所有连接,从而引发四次挥手的过程。可以指定3种关闭方式:

    9. syn flood :如果监听者发送SYN+ACK后,迟迟收不到客户端返回的ACK消息,监听者将对客户端重新发送SYN+ACK消息。解决方法是缩小listen()维护的两个队列的最大长度,减少重发syn+ack的次数,增大重发的时间间隔等。

    10. https://www.cnblogs.com/f-ck-need-u/p/7623252.html(值得认真看)

  4. 内存对齐

    1. 原因:

      1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
      2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    2. 为什么不对齐得进行两次内存访问:

      1. 对于32位CPU而言,它的每次读取是4个字节(硬件设备决定了只能从0.4,8,12…内存地址开始读)。如果一个32位int类型变量没有对齐,那么CPU就要进行两次读取操作,分别获得前面部分字节和后面部分字节,然后再通过移位操作和组合得到所要的变量。为了让数据在任何平台上(32,64)都尽可能少的执行内存访问,对于一个字段而言他的首地址都是字段大小的倍数。
    3. 结构体对齐的方法:

      1. 大小为 size 的字段,他的结构内偏移 offset 需符合 offset mod size 为 0.

      2. 整个结构的大小必须是其中最大字段大小的整数倍。(最后面也要进行填充,因为声明这个结构体的数组)

      3. 详细:http://www.cnblogs.com/TenosDoIt/p/3590491.html

  5. 网络IO模型:当一个读操作发生时,首先会先等待数据准备,然后将数据从内核空间拷贝到进程中。

    1. 阻塞IO:在调用读操作时,两个阶段都被阻塞。
    2. 非阻塞IO:如果数据还没准备好,就返回一个error。进程需要需要不断询问,数据准备好了还是要阻塞等待将数据拷贝到用户空间。
    3. IO多路复用(事件驱动IO):使用select/poll/epoll去轮询负责的socket,当某个socket有数据到达了就通知进程。整个进程阻塞在select这里。(好处是可以同时处理多个连接)
    4. 异步IO:在调用read操作之后就返回,等两个阶段都执行完了之后会以信号等方式通知进程。
  6. 文件描述符and文件指针:其实只是一个整数,是文件描述符表(每个进程都有一个)上的索引。通过这个可以找到该fd对应的文件指针。文件描述符表中每个表项都有一个指向已打开文件的指针。现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。

  7. select:调用时传入的第一个参数表示的是文件描述符的数量,所以是最大fd加1。

    1. 特点:
      1. select使用位域的方式传递文件描述符,文件描述符的个数有限。数目与系统内存有关。
      2. select需要一个数据结构保留监控的fd,用于在调用select前添加到集合中去,以及在select调用后循环数组使用FD_ISSET判断是否有事件发生。
      3. select效率低下。内存拷贝,每次都要轮询。
    2. select机制:
      1. Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。
      2. 当我们调用select函数时,其实是调用了内部的sys_select函数,这个函数主要是从用户进程拷贝超时时间,再进行转化后传给函数core_sys_select,这个函数主要是将文件描述符集合拷贝到内核空间。在拷贝前会先分配文件描述符集合6倍大小的空间, 如果栈空间不足就使用堆进行分配,然后进行拷贝。拷贝完之后就会调用最核心的do_select函数。
      3. do_select函数中,遍历所有n个fd,对每一个fd调用对应驱动程序中的poll函数。poll函数调用poll_wait函数,poll_wait函数调用__pollwait(),这个函数会初始化等待队列项(有个pollwake函数),并将该等待队列项添加到从驱动程序中传递过来的等待队列头中去。驱动程序在得知设备有IO事件时(通常是该设备上IO事件中断),会调用wakeup,wakeup –> _wake_up_common -> curr->func(即pollwake)。pollwake函数里面调用_pollwake函数, 通过pwq->triggered = 1将进程标志为唤醒。再调default_wake_function(&dummy_wait, mode, sync, key)这个默认的通用唤醒函数唤醒调用select的进程。 请注意,poll操作会返回一个mask码值,通过这个值我们可以判断是否可读写。
      4. 当do_select返回之后,接着就会将修改后的可读写的文件描述符集合拷贝到用户空间。
      5. select在第一次调用poll函数时,会将进程挂载到fd上,如果此时没有监听事件发生会进入睡眠,等待下次唤醒之后再遍历集合,这次无需重复挂载,只需收集事件即可。在退出do_select函数时会删除挂载点。
  8. poll:poll和select的机制一样,但是它是基于链表实现的,所以没有数量限制,此外它在每次调用后不需要清空文件描述符集合(其实是一个pollfd结构体集合,包含文件描述符,查询的事件掩码,返回的事件掩码)。

  9. epoll:

    1. epoll就是用来解决select/poll存在的问题。它有epoll_create,epoll_ctl,epoll_wait 3个系统调用。
    2. 调用epoll_create创建一个epoll的句柄。
    3. epoll_ctl是用来实现对需要监听的fds的修改。做到了有变化才修改,不用像select一样进行大块内存拷贝。使用一颗红黑树实现了快速的增删改查。
    4. epoll_wait是一种高频的操作,因此他使用的是内存映射解决了就绪集合的拷贝问题。
    5. epoll引入了一个中间层,一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list)
    6. 在调用epoll_ctl时,会构建一个睡眠实体与socket关联,并设置其回调函数将关联socket排入epoll的就绪队列,并且唤醒epoll的单独睡眠队列上的进程(从队列上移除)。将睡眠实体排入socket的睡眠队列。
    7. 在调用epoll_wait的时候,构建睡眠实体与当前进程关联,回调函数将遍历就绪队列,挨个调用poll逻辑收集事件并通过event数组回传,唤醒进程。判断epoll就绪队列是否为空,空则将睡眠实体插入epoll的单独睡眠队列上。
  10. 边沿触发与水平触发:

    1. 边沿触发:遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件。(如果文件描述符自上次状态检查以来有了新的I/O活动(比如新的输入),此时需要触发通知,我们在监听socket读事件时都是socket有新的数据到来才会触发,调用回调函数)
    2. 水平触发: 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件。 如果该sk的poll函数返回了关心的事件(对于可读事件来说,就是POLL_IN事件),那么该sk被重新加入到epoll的ready_list中。
    3. ET 不一定比LT性能好,因为epoll主要用于服务器有海量socket,但是活跃socket较少的情况下,一次读取不一定能将socket读取完,并且可能在用户处理socket数据时有新的数据到来,所以对该sk的再次遍历并不多余。
    4. 在阻塞模式下,在epoll_wait返回的时候,我们对某个socket_fd调用recv或read读取并返回了一些数据的时候,我们不能再次直接调用recv或read,因为,如果socket_fd已经无数据可读的时候,进程就会阻塞在该socket_fd的recv或read调用上,这样就影响了IO多路复用的逻辑(我们希望是阻塞在所有被监控socket的epoll_wait调用上,而不是单独某个socket_fd上),造成其他socket饿死,即使有数据来了,也无法处理。
    5. 在阻塞模式下,不能用ET。ET下,我们必须采用非阻塞模式,一直read直到EAGAIN。
    6. 在绝大多少情况下,ET模式并不会比LT模式更为高效。因此,建议还是采用LT模式来编程更为舒爽。
  11. 如果 select 返回可读,结果只读到 0 字节,什么情况?
    对端已关闭,我们只需要关闭连接

  12. socket 什么情况下可读?

    1. 缓冲区数据量大于最低水位; 2. 收到一个 FIN; 3. listen fd 有连接; 4. 发送错误

  13. 数据库的ACID特性:

    1. 原子性:一个事物中的操作要么全部成功,要么全部失败。

    2. 一致性:事务执行前后都处于合法的状态,它只能保证数据库中的所有数据都不会违反定义好的规则,但不能保证结果跟程序员想的一样。

    3. 隔离性:多个并发事务之间要互相隔离。数据库提供了多种隔离级别。

    4. 持久性:事务一旦被提交,那么对数据库中的数据的改变就是永久性的。

  14. 不考虑事务的隔离性,会发生的问题:

    1. 脏读:一个事务读取了另一个未提交事务中的数据(未提交事务对这个数据进行了修改)。

    2. 不可重复的读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

    3. 虚读(幻读):幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

  15. 现在来看看MySQL数据库为我们提供的四种隔离级别:

    ① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

    ② Repeatable read (可重复读):可避免脏读、不可重复读的发生。

    ③ Read committed (读已提交):可避免脏读的发生。

    ④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。

  16. Innodb引擎

    1. 支持事务的ACID原则,实现了SQL四种隔离级别。

    2. 提供了行级锁和外键约束。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。

    3. 它没有保存表的行数。

  17. MyISAM引擎

    1. 不提供对数据库事务的支持。

    2. 不支持行级锁和外键,写操作需要锁定整个表,效率便会低一些。

    3. 存储了表的行数

  18. 两种引擎对比:

    1. MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。InnoDB表比MyISAM表更安全。

    2. .MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择。

    3. InnoDB用于事务处理应用程序,具有众多特性,包括ACID事务支持。如果应用中需要执行大量的INSERT或UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。

  19. 聚簇索引和非聚簇索引:

    聚簇索引是对磁盘上实际数据重新组织以按指定的一个或多个列的值排序的算法。特点是存储数据的顺序和索引顺序一致。一般情况下主键会默认创建聚簇索引,且一张表只允许存在一个聚簇索引。

  20. B树和B+树(更加矮胖,读取的磁盘页更少一些)

    1. B树(MongoDB):

      1. 非叶子结点的关键字个数=儿子数-1;
      2. 关键字集合分布在整颗树中;
      3. 任何一个关键字出现且只出现在一个结点中;
      4. 搜索有可能在非叶子结点结束;
      5. 其搜索性能等价于在关键字全集内做一次二分查找;
    2. B+树(MySQL):

      1. 有n棵子树的非叶子结点中含有n个关键字(b树是n-1个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b树是每个关键字都保存数据)。
      2. 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
      3. 所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
      4. 通常在b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
      5. 同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素。
    3. B+树优势:

      1. b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
      2. b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢);
      3. 对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历,如下两图:
      4. 因为B树键位置不定,且在整个树结构中只出现一次,虽然可以节省存储空间,但使得在插入、删除操作复杂度明显增加。B+树相比来说是一种较好的折中。
    4. 为什么不用二叉树:数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树的特征之一,它的每个节点最多包含m个孩子,m称为b树的阶,m的大小取决于磁盘页的大小。

    5. 参考:https://blog.csdn.net/login_sonata/article/details/75268075

  21. UNIQUE KEY可以为NULL,但是NULL只能有一个。

  22. new和malloc:

    1. 两者都是在堆上动态申请和释放内存。

    2. C的内存管理有malloc和free,C++内存管理有new和delete。

    3. new和delete属于操作符,而malloc和free其实是两个系统函数。

    4. new在申请内存时候返回的是指定类型的指针,申请内存大小自动计算;malloc在申请内存时候返回的是void*指针,并且在申请时要指明申请内存的大小,以参数形式传进去。

    5. new在申请完内存后会调用该类型的构造函数,也就是说new在申请完内存后会初始化这段内存;malloc则单纯的多,申请完指定大小的内存就完事,至于这段内存要用作何用,malloc并不关心。

    6. 非内置类型只能用new申请内存(new和delete的出现是为了适应class的构造与析构)。

  23. 红黑树问题:https://blog.csdn.net/gao1440156051/article/details/51581394

  24. TCP的装包和拆包问题

    1. TCP是一个流协议,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

    2. 问题产生的原因有三个,分别如下。

      (1)应用程序write写入的字节大小大于套接口发送缓冲区大小;

      (2)进行MSS大小的TCP分段;

      (3)以太网帧的payload大于MTU进行IP分片。

    3. 由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

      (1)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;

      (2)在包尾增加回车换行符进行分割,例如FTP协议;

      (3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;

      (4)更复杂的应用层协议。

  25. TCP

    1. TCP提供一种面向连接的、可靠的字节流服务。( UDP 无连接、不可靠的数据报服务)

    2. 面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据
      之前必须先建立一个TCP连接。在一个TCP连接中,仅有两方进行彼此通信。

    3. TCP通过定时器,确认,首部的校验和,流量控制等方式提供可靠性。

    4. TCP提供字节流服务,发送方可以先发送10字节,又发送20字节,而接收端不一定分两次进行接收。

  26. TCP选项:

    1. 窗口扩大因子(WSopt):用来增加 TCP 接收窗口的大小。

    2. SACK 选择确认选项:使 TCP 只重新发送丢失的包,不用发送后续所有的包,而且提供相
      应机制使接收方能告诉发送方哪些数据丢失,哪些数据重发了,哪些数据已经提前收到等。

    3. MSS:最大分段大小

  27. 三次握手:

    1. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)
    2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手 。
    4. 而之所以存在 3-way hanshake 的说法,是因为 TCP 是双向通讯协议,作为响应一方(Responder) 要想初始化发送通道,必须也进行一轮 SYN + ACK。由于 SYN ACK 在 TCP 分组头部是两个标识位,因此处于优化目的被合并了。所以达到双方都能进行收发的状态只需要 3 个分组。
  28. 四次挥手:

    1. 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
    2. 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
    3. 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
    4. 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

  29. TIME_WAIT状态存在的原因:

    1. TIME_WAIT状态停留的时间为2倍的报文段最大生存时间,这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。

    2. 保证让迟来的TCP报文段有足够的时间被识别并丢弃。

  30. 滑动窗口协议

    1. 进行流量控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。滑动窗口用于加速传输。

    2. 滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。

    3. 发送窗口将发送报文分为了发送并确认了,发送但没确认的,可以发送的报文,直到窗口移动才可以发送的报文四个部分。

    4. 当发送端接受到接收端对一个报文的ACK时,如果这个ACK是在发送窗口左边,那么是一个重复的ACK就丢弃,否则窗口就会合拢,即窗口左边沿向右边沿靠近,同时根据ACK宣告的窗口大小将窗口右边沿向右边移动。

    5. 如果左边沿到达右边沿,则称其为一个0窗口,此时发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。

  31. 慢启动

    1. 慢启动为发送方的TCP增加了另一个窗口:拥塞窗口 (congestion window),记为cwnd。

    2. 首先,拥塞窗口被初始化为1个报文段,每收到一个ACK就增加一个报文段。

      理想的状态:

      1. 发送方开始发送一个报文,然后等待ACK。
      2. 当收到该ACK时,拥塞窗口从1增加到2,即可以发送2个报文段。
      3. 发送方再发送2个报文段,然后等待ACK,当收到这两个报文段的ACK时,拥塞窗口就增加为4。这是一种指数增加的关系。

      注意,发送方取拥塞窗口和通告窗口中的最小值作为发送上限。

  32. 拥塞避免算法:

    1. 拥塞避免算法和慢启动算法对每个连接维持两个变量: 拥塞窗口( cwnd ) 和 慢启动门限( ssthresh )

    2. cwnd初始化为1,执行慢启动,当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半

    3. 拥塞避免算法在每个RTT内增加 1/cwnd 个报文,成线性增长.

  33. 快速重传和快速恢复算法

    如果收到3个重复ACK,可认为该报文段已经丢失,此时无需等待超时定时器溢出,直接重传丢失的包,这就叫【快速重传算法】.而接下来执行的不是慢启动而是拥塞避免算法,这就叫【快速恢复算法】.

  34. 定时器:

    1. 对每个连接, TCP管理4个不同的定时器。

    2. 重传定时器适用于当希望收到另一端的确认。

    3. 坚持(persist)定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。

    4. 保活(keepalive)定时器可检测到一个空闲连接的另一端何时崩溃或重启。

    5. 2MSL定时器测量一个连接处于TIME_WAIT状态的时间。

  35. 图的遍历算法:

    1. 深度优先遍历
      基本思想:首先从图中某个顶点v0出发,访问此顶点,然后依次从v0相邻的顶点出发深度优先遍历,直至图中所有与v0路径相通的顶点都被访问了;若此时尚有顶点未被访问,则从中选一个顶点作为起始点,重复上述过程,直到所有的顶点都被访问。可以看出深度优先遍历是一个递归的过程。

    2. 广度优先遍历其深度优先遍历得到的序列为:基本思想:首先,从图的某个顶点v0出发,访问了v0之后,依次访问与v0相邻的未被访问的顶点,然后分别从这些顶点出发,广度优先遍历,直至所有的顶点都被访问完。

    3. Dijkstra(迪杰斯特拉算法)算法
      又称迪杰斯特拉算法,是一个经典的最短路径算法,主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止,使用了广度优先搜索解决赋权有向图的单源最短路径问题,算法最终得到一个最短路径树。时间复杂度为O(N^2)
      ①先取一点v[0]作为起始点,初始化dis[i],d[i]的值为v[0]到其余点v[i]的距离w[0][i],如果直接相邻初始化为权值,否则初始化为无限大;
      ②将v[0]标记,vis[0] = 1(vis一开始初始化为0);
      ③找寻与v[0]相邻的最近点v[k],将v[k]点记录下来,v[k]与v[0]的距离记为min;
      ④把v[k]标记,vis[k]=1;
      ⑤查询并比较,让dis[j]与min+w[k][j]进行比较,判断是直接v[0]连接v[j]短,还是经过v[k]连接v[j]更短,即dis[j]=MIN(dis[j],min+w[k][j]);
      ⑥继续重复步骤③与步骤⑤,知道找出所有点为止。

    4. 弗洛伊德算法(解决多源最短路径):时间复杂度O(n^3),空间复杂度O(n^2)
      如果要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2b->或者a->k1->k2…->k->i…->b。比如上图中从4号城市到3号城市(4->3)的路程e[4][3]原本是12。如果只通过1号城市中转(4->1->3),路程将缩短为11(e[4][1]+e[1][3]=5+6=11)。其实1号城市到3号城市也可以通过2号城市中转,使得1号到3号城市的路程缩短为5(e[1][2]+e[2][3]=2+3=5)。所以如果同时经过1号和2号两个城市中转的话,从4号城市到3号城市的路程会进一步缩短为10。通过这个的例子,我们发现每个顶点都有可能使得另外两个顶点之间的路程变短。好,下面我们将这个问题一般化。

  36. 进程间通信:

    1. 进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
    2. 匿名管道:调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,然后通过参数传出给用户进程两个文件描述符,,filedes[0]指向管道的读端,filedes[1]指向管道的写端。父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道 。父进程关闭fds[0],子进程关闭fds[1]。实现父子进程间的单向通信。
    3. 无名管道、有名管道、流管道、消息队列、信号、信号量、socket、共享内存
  37. 虚函数

    1. 每个类用了一个虚表,每个类的对象用了一个虚指针指向类的虚表
    2. 当我们调用基类指针指向一个子类对象时,然后用基类指针调用一个虚函数时,调用的是虚函数的子类版本。因为在调用的过程中,我们先是得到基类指针指向对象中的虚函数指针,这个指针指向的子类的虚函数表,所以我们得到的会是子类版本的虚函数。
  38. 构造函数、析构函数抛出异常的问题

    1. 构造函数可以抛出异常。C++标准指明析构函数不能、也不应该抛出异常。

    2. 如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分

    3. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

    4. 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

    5. 那么当无法保证在析构函数中不发生异常时, 该怎么办?

      其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。

  39. volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

  40. 如果c++类的构造函数有一个参数,那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象。C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的。被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。我鼓励你遵循相同的政策。

  41. 头文件不用被编译。头文件中可以写const对象的定义,内联函数的定义,类的定义。(参考:https://blog.csdn.net/xupan_jsj/article/details/7855090)(声明,定义,实现是不一样的)

  42. 模板声明与定义要放在同一文件中,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。

  43. C++类模板的三种特化:一是特化为绝对类型; 二是特化为引用,指针类型;三是特化为另外一个类模板。

    参考:http://www.cppblog.com/SmartPtr/archive/2007/07/04/27496.html

    模板特化是通过”给模板中的所有模板参数一个具体的类”的方式来实现的.而模板偏特化则是通过”给模板中的部分模板参数以具体的类,而留下剩余的模板参数仍然使用原来的泛化定义”的方式来实现的;

    定义了int特化的话,对double类型的模板无效

  44. 虚拟继承:虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。(参考:http://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html

  45. public TreeNode inorderSucc(TreeNode n){  
        if(n == null) return null;  
    
        //找到右子结点,则返回右子树里最左边的结点。  
        if(n.right != null){  
            return leftMostChild(n.right);  
        }else{  
            TreeNode q = n;  
            TreeNode x = q.parent;  
            //向上直至位于左边而不是右边  
            while(x != null && x.left !=q){//此处需要仔细分析  
                q = x;  
                x = x.parent;  
            }  
            return x;  
        }  
    }  
    
    public TreeNode leftMostChild(TreeNode n){//返回当前子树的最左边结点。  
        if(n == null){  
            return null;  
        }  
        while(n.left != null){  
            n=n.left;  
        }  
        return n;  
    }  
  46. IO多路复用,多进程,多线程比较

    1. 多进程模型

      多进程优点:

      每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

      通过增加CPU,就可以容易扩充性能;

      可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;

      每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大

      多进程缺点:

      逻辑控制复杂,需要和主程序交互;

      需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算

      多进程调度开销比较大;

    2. 多线程的优点

      无需跨进程边界;

      程序逻辑和控制方式简单;

      所有线程可以直接共享内存和变量等;

      线程方式消耗的总资源比进程方式好;

      多线程缺点:

      每个线程与主程序共用地址空间,受限于2GB地址空间;

      线程之间的同步和加锁控制比较麻烦;

      一个线程的崩溃可能影响到整个程序的稳定性;

      到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;

      线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU

  47. HTTP协议

    1. 超文本传输协议,基于TCP/IP通信协议来传递数据,一般使用的是B/S模式,浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。HTTP是一个无状态协议。get和post,状态码。
  48. map和hash_map的使用选择

    总 体来说,hash_map 查找速度会比map快,而且查找速度基本和数据量大小无关,属于常数级别;而map的查找速度是log(n)级别。hash还有hash函数的耗时。当有100w条记录的时候,map也只需要20次的比较,200w也只需要21次的比较!所以并不一定常数就比log(n) 小!hash_map对空间的要求要比map高很多,所以是以空间换时间的方法。map的最大优点之一就是有序性。hash_map中没有hash表的实现(C++11中用unordermap内部实现了hash表)。

猜你喜欢

转载自blog.csdn.net/zhougb3/article/details/81148630