项目熟背知识点

项目相关知识点

​ 包括 QT epoll 非活跃定时器 压测 线程池 数据库读写分离 调试 等

1. QT

1.1 讲述Qt信号槽机制与优势与不足

机制:

​ Qt提供了信号和槽机制用于完成界面操作的响应,是完成任意两个Qt对象之间的通信机制。
​ 其中,信号会在某个特定情况或动作下被触发,槽是等同于接收并处理信号的函数。

​ 优点: ①类型安全。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。

​ ②松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用,即使关联的对象在运行时被删除。程序也不会奔溃。

​ ③灵活性。一个信号可以关联多个槽,或多个信号关联同一个槽。

​ 不足:速度较慢。与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍。

​ 1)需要定位接收信号的对象。
​ 2)安全地遍历所有的关联。
​ 3)编组/解组传递的参数。
​ 4)多线程的时候。信号可能需要排队等待。

1.2 Qt信号和槽的本质是什么

​ 回调函数。信号或是传递值,或是传递动作变化;槽函数响应信号或是接收值,或者根据动作变化来做出对应操作。

1.3 描述QT下多线程的两种使用方法, 以及注意事项

第一种方法:
\1. 创建一个类从QThread类派生
\2. 在子线程类中重写 run 函数, 将处理操作写入该函数中
\3. 在主线程中创建子线程对象, 启动子线程, 调用start()函数

多线程使用注意事项:
* 1. 业务对象, 构造的时候不能指定父对象
* 2. 子线程中不能处理ui窗口(ui相关的类)
* 3. 子线程中只能处理一些数据相关的操作, 不能涉及窗口

1.4 描述UDP 之 UdpSocket通讯

​ UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。在网络质量令人十分不满意的环境下,UDP协议数据包丢失严重。由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。

    在Qt中提供了QUdpSocket 类来进行UDP数据报(datagrams)的发送和接收。Socket简单地说,就是一个IP地址加一个port端口  。

    流程:①创建QUdpSocket套接字对象 ②如果需要接收数据,必须绑定端口 ③发送数据用writeDatagram,接收数据用 readDatagram 。

五、多线程使用使用方法
方法一:①创建一个类从QThread类派生②在子线程类中重写 run 函数, 将处理操作写入该函数中 ③在主线程中创建子线程对象, 启动子线程, 调用start()函数

    方法二:①将业务处理抽象成一个业务类, 在该类中创建一个业务处理函数②在主线程中创建一QThread类对象 ③在主线程中创建一个业务类对象 ④将业务类对象移动到子线程中 ⑤在主线程中启动子线程 ⑥通过信号槽的方式, 执行业务类中的业务处理函数

多线程使用注意事项:

    1. 业务对象, 构造的时候不能指定父对象
    1. 子线程中不能处理ui窗口(ui相关的类)
    1. 子线程中只能处理一些数据相关的操作, 不能涉及窗口

1.5 多线程下,信号槽分别在什么线程中执行,如何控制

​ 可以通过connect的第五个参数进行控制信号槽执行时所在的线程

Qt::AutoConnection(默认) 自动连接
(默认)信号发射对象如果和槽的执行对象在同一个线程,那么将是直连方式,否则就是队列方式。

Qt::DirectConnection 直接连接
相当于直接调用槽函数,但是当信号发出的线程和槽的对象不再一个线程的时候,则槽函数是在发出信号的线程中执行的。

Qt::QueuedConnection 队列连接
信号发射后,当事件循环返回到接收线程时槽函数就执行了,也就是说这种连接方式不是立即触发槽函数的,而是要排队等的,并且是在槽函数的线程中执行。

Qt::BlockingQueuedConnection 阻塞队列连接
在槽函数返回之前信号发射所在的线程都是阻塞的。即发送完信号,发送线程会阻塞,直到槽函数线程运行完毕。

Qt::UniqueConnection 唯一连接
防止重复连接。如果当前信号和槽已经连接过了,就不再连接了,即connect连接失效,可通过按位或 | 与以上四个结合在一起使用。

2. EPOLL

2.1 I/O多路复用思路

​ 可以由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
目的:复用IO的基本思路就是通过slect或poll、epoll 来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。

2.2 epoll与select、poll的对比

1、 用户态将文件描述符传入内核的方式
select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。
2、内核态检测文件描述符读写状态的方式
select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。
poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。
epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
3、找到就绪的文件描述符并传递给用户态的方式
select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。

4、重复监听的处理方式
select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。
poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。
epoll:无需重新构建红黑树,直接沿用已存在的即可。

2.3 epoll的ET和LT区别?

(1)水平触发LT:epoll默认的设置,并且同时支持block和no-block socket.|
接受缓冲区不为空,对应fd一直处于“读就绪”状态;发送缓冲区不满,对应fd一直处于“写就绪”状态(关于这里的缓冲区:读多少就少多少,写多少就多多少)。所以水平触发,只要数据没读完,那么在epoll_wait中,就一直认为该fd处于就绪状态。
采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件,当下一次调用epoll_wait是,epoll_wait还会将此事件通告应用程序。
如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率
(2)边沿触发ET:只支持no-block socket
如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理。
当fd对应的读缓冲区从”有“变为了”无“时,对应fd才处于”读就绪“。当fd对应的写缓冲区从”无“变为了”有“时,对应fd才处于”写就绪“。注意:一定是在”有“和”无“之间变化才能触发。
当epoll_wait检测到其上有事件发生并将此通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不在向应用程序通知这一事件。
ET模式降低了同意epoll事件被触发的次数,效率比LT模式高。

2.4 Linux epoll ET模式下 EPOLLOUT和EPOLLIN触发场景?

1)EPOLLOUT事件:
EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那你要先准备好下面条件:
1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。
2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。
简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果你真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。
2)EPOLLIN事件:
EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。

3)epoll的ET模式下,正确的读写方式
读: 只要可读, 就一直读,直到返回0,或者 errno = EAGAIN或者EWOULDBLOCK
写:只要可写, 就一直写,直到数据发送完,或者 errno = EAGAIN或者EWOULDBLOCK
从字面上看, 意思是:
EAGAIN: 再试一次
EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block

2.5 ET模式下的accept问题

请思考以下一种场景:在某一时刻,有多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。在这种情形下,我们应该如何有效的处理呢?

解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。

2.6 EPOLL为什么要用红黑树?

​ epoll 内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用 epoll_ctl 函数使用EPOLL_CTL_ADD 宏来插入,epoll_wait 也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。

3. 定时器相关

3.1 为什么要用定时器?

为了定期删除非活跃事件,防止连接资源的浪费。

非活跃,是指浏览器与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。

定时事件,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。

3.2 说一下定时器的工作原理?

定时器利用结构体将定时事件进行封装起来。定时事件,即定期检测非活跃连接。

服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序双向链表将所有定时器串联起来,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

(信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。)

信号通知的逻辑:创建管道,其中管道写端写入信号值,管道读端通过I/O复用系统监测读事件

为什么管道写端要非阻塞?

send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。

3.3 定时任务处理函数逻辑

使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。

(1)链表容器是升序排列,当前时间小于定时器的超时时间,后面的定时器也没有到期

(2)当前定时器到期,则调用回调函数,执行定时事件

(3)将处理后的定时器从链表容器中删除,并重置头结点

若有数据传输,则将定时器往后延迟3个单位

(回调函数用来方便删除定时器时将对应的HTTP连接关闭

3.4 如果有10亿数据,即使load到本地hash也很耗时,你要怎么优化?

首先,将10亿的用户信息,利用大致缩小1000倍的hash算法进行hash,这时就获得了100万的hash数据,每一个hash数据代表着一个用户信息块(一级);

而后,再分别对这100万的hash数据再进行hash,例如最终剩下1000个hash数据(二级)。

在这种方式下,服务器只需要保存1000个二级hash数据,当用户请求登录的时候,先对用户信息进行一次hash,找到对应信息块(二级),在读取其对应的一级信息块,最终找到对应的用户数据

4. 线程池

4.1 为什么使用线程池?

每个请求对应一个线程方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。

线程池是为了避免创建和销毁线程所产生的开销,避免活动的线程消耗的系统资源;

提高响应速度,任务到达时,无需等待线程即可立即执行;

4.2 线程池工作原理

​ 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

​ 线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:

​ 任务队列,存储需要处理的任务,由工作的线程来处理这些任务
​ 通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
​ 已处理的任务会被从任务队列中删除
线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
​ 工作的线程(任务0任务的消费者) ,N个
线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理
​ 工作的线程相当于是任务队列的消费者角色,
如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)
如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
管理者线程(不处理任务队列中的任务),1个
它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
当任务过多的时候,可以适当的创建一些新的工作线程
当任务过少的时候,可以适当的销毁一些工作的线程

4.3 你的线程池处理完一个任务后状态是什么?

(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态

(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。

4.4 如果一个客户请求需要占用线程很久,会不会影响接下来客户请求呢,有什么策略?

会,因为线程池内线程的数量时有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。

应对策略:

我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。

4.5 每一个线程占多大的内存?

32位系统,分配4G的虚拟内存给进程,每个线程约占10M的内存

4.6 线程池中有多少个线程,线程池数量如何设定?

默认8个

调整线程池中的线程数量的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。

Ncpu 表示 CPU的数量。

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 Ncpu+1能够实现最优的CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因 导致暂停时,额外的这个线程就能顶上去,保证CPU 时钟周期不被浪费

如果是IO密集型任务,参考值可以设置为 2 * Ncpu。因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导致资源浪费

最佳线程数量 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)* CPU个数。

由公式可得,线程等待时间所占比例越高,需要越多的线程,线程CPU时间所占比例越高,所需的线程数越少。

4.7 简单说一下服务器使用的并发模型?

事件:I/O事件、信号及定时事件

(1)reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),将socket可读写事件放入请求队列,交给工作线程处理,即读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现(epoll_wait)。

(2)proactor模式中,主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现(aio_read/aio_write)。

由于异步I/O并不成熟,实际中使用较少,本服务器采用:同步I/O模拟Proactor模式

同步I/O模型的工作流程如下(epoll_wait为例):

主线程往epoll内核事件表注册socket上的读就绪事件。

主线程调用epoll_wait等待socket上有数据可读

当socket上有数据可读,epoll_wait通知主线程,主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。

睡眠在请求队列上某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件

主线程调用epoll_wait等待socket可写。

当socket上有数据可写,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。

(3) 主从Reactor模式:核心思想是,主反应堆线程只负责分发Acceptor连接建立,已连接套接字上的I/O事件交给sub-reactor负责分发。其中 sub-reactor的数量,可以根据CPU的核数来灵活设置。

主反应堆线程一直在感知连接建立的事件,如果有连接成功建立,主反应堆线程通过accept方法获取已连接套接字,接下来会按照一定的算法选取一个从反应堆线程,并把已连接套接字加入到选择好的从反应堆线程中。主反应堆线程唯一的工作,就是调用accept获取已连接套接字,以及将已连接套接字加入到从反应堆线程中。

4.8 线程池的同步机制有哪些?

线程同步有4种机制:

  • 临界区
  • 互斥量
  • 事件
  • 信号量

临界区
临界区是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
PS:私人浴室(没有管理员)只有一间淋浴房,我想洗澡,我时不时来看下淋浴房空了没,空了我就去洗。

互斥量
功能上跟临界区类似,不过可用于不同进程间的线程同步。

PS:公共浴室(有管理员)只有一间淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果有,管理员就让我洗,否则管理员就让我先去休息室睡一觉,等有空的淋浴房了叫醒我去洗澡。

事件
触发重置事件对象,那么等待的所有线程中将只有一个线程能唤醒,并同时自动的将此事件对象设置为无信号的;它能够确保一个线程独占对一个资源的访问。和互斥量的区别在于多了一个前置条件判定。

PS:公共浴室(有管理员)只有一间淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果淋浴房没人洗而且打扫完了(等待的事件),管理员就让我洗,否则管理员就让我先去休息室睡一觉,等没人洗而且打扫完了叫醒我去洗澡。

信号量
信号量用于限制对临界资源的访问数量,保证了消费数量不会大于生产数量。

PS:公共浴室(有管理员)有N间(资源数量限制)淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果有,管理员就让我洗,否则管理员就让我先去休息室睡一觉,等有空的淋浴房了叫醒我去洗澡。

使用建议
尽量使用用户模式下的临界区机制,避免使用需要用户态到内核态切换的同步机制。

5. 组播

组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。

224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;

224.0.1.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;

239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效

6. 流量控制

令牌桶原理

令牌桶算法是经典的网络限流算法,它可以限制带宽,使流量以一个较为均匀的速度向外发送。我们可以把令牌桶算法想象成一个有固定容量的桶,每个数据包都要经过这个桶处理。如果当前数据包的大小 大于 桶内的令牌数,则放行该数据包,否则丢掉该数据包,或者延时发送

7. GDB调试

一、用GBD 调试多进程程序
如果一个进程通过fork系统调用创建了子进程,gdb会继续调试原来的进程,子进程则正常运行。
那么如何调试子进程呢?

1、单独调试子进程
子进程本质也是一个进程,因此也可通过gdb来调试,首先找到目标子进程的PID,再将其附加(attach)到
gdb调试器上,具体操作如下:
$ps -ef|grep 进程名 //找到待调试进程的PID
$gdb
(gdb) attach “PID”

2、使用调试器选项follow-fork-mode
gdb调试器的选项follow-fork-mode允许我们选择程序在执行fork系统调用后是继续调试父进程还是调试子进程。
用法:
(gdb)set follow-fork-mode m
m 可以选parent 或 child, 分别表示调试父进程和子进程
举例:
$gdb XX
(gdb) set follow-fork-mode child
(gdb) b process.h:264
//打断点

二、用gdb调试多线程程序
gdb有一组命令可辅助多线程程序的调试。
info threads
显示当前可调试的所有线程。gdb会为每个线程分配一个ID,我们可以使用这个ID来操作对应的线程。
ID前面有“*”的线程是当前被调试的线程。

thread ID
调试目标ID指定的线程。

set scheduler-locking[off|on|step]
调试多线程程序时,默认除了被调试的线程在执行外,其他线程也在继续执行,
但有的时候我们希望只让被调试的线程运行。这可以通过这个命令来实现。
该命令设置sceduler-locking的值:
off表示不锁定任何线程,即所有线程都可以继续执行,这是默认值。
on表示只有当前被调试的线程会继续执行。
step表示在单步执行的时候,只有当前线程会执行。

三 用gdb工具分析core文件
在Unix系统下,应用程序崩溃,一般会产生core文件,如何根据core文件查找问题的所在,
并做相应的分析和调试,是非常重要的。

通常情况下,Core 文件会包含程序运行时的内存、寄存器状态、堆栈指针、
内存管理信息还有各种函数调用堆栈信息等。
我们可以理解为是程序工作当前状态存储生成第一个文件,
许多程序出错时都会产生一个Core 文件,通过工具分析这个文件,
我们可以定位到程序异常退出时对应的堆栈调用等信息,找出问题所在并进行及时解决。

段错误,就是大名鼎鼎的Segmentation Fault,这通常是由指针错误引起的。
简而言之,产生段错误就是访问了错误的内存段,一般是你没有权限,
或者根本就不存在对应的物理内存,尤其常见的是访问0 地址。
在编程中有以下几种做法容易导致段错误,基本都是错误地使用指针引起的:

访问系统数据区,尤其是往系统保护的内存地址写数据,最常见的就是给指针以0地址
内存越界(数据越界、变量类型不一致等)访问到不属于你的内存区域
程序在运行过程中如果出现段错误,那么就会收到SIGSEGV 信号,
SIGSEGV 默认handler 的动作是打印“段错误”的出错信息,并产生Core 文件。

1、core文件开关
ulimit -c 查看core开关,如果为0表示关闭,不会生成core文件;
使用 ulimit -c [filesize] 设置core文件大小,当最小设置为4之后才会生成core文件;
使用 ulimit -c unlimited 设置core文件大小为不限制,这是常用的做法;
如果需要开机就执行,则需要将这句命令写到 /etc/profile 等文件。
core文件路径一般在可执行文件同一目录

2、core文件命名和保存路径
core文件有默认的名称和路径,但为了方便,我们通常会自己命名和指定保存路径;
可以通过 /proc/sys/kernel/core_pattern 设置 core 文件名和保存路径,方法如下:
echo “/corefile/core-%e-%p-%t” > /proc/sys/kernel/core_pattern

命名的参数列表:
%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名。

3、分析Core 文件
设置Core文件大小,运行程序生成Core文件
执行ulimit -c unlimited表示不限制生成的Core 文件的大小,

4、运行这个有bug 的程序,可以在当前目录下生成core文件
执行 gdb [exec file] [core file]启动gdb后,
调用gdb的where或bt命令可以查看当时的调用栈信息,
进入某个栈:f N,f是frame的缩写,N是栈号,如0、1、2、3…
进入到某个栈后,才能通过p命令查看这个栈的临时变量,否则只能查看全局变量。

四、调试一个正在运行的程序使用gdb -p PID命令,PID即程序的pid。
需要注意的是,gdb调试正在运行的程序会导致程序挂起,因此请记住不要gdb调试正在运行的在线服务。

五、设置断点的方式有很多种,最常见的有两种:一是设置程序运行到源代码的某一行,二是设置程序运行到某个函数。
设置程序运行到某一行,通过“文件名:行号”的形式:
b test.cpp:100
设置程序运行到某个函数,通过“名字空间::函数名”的形式:
gdb namespace_a::func

六、查看断点:info b
删除断点:d N,d是delete的缩写,N是断点的编号,可以通过info b查看。
无论哪种方式设置断点,都要执行c命令(continue),让程序继续运行。

七、在调试程序时,最常用的gdb命令是:n、s、p
n即next,单步执行,执行下一步的意思,遇到函数会调用函数。
s即step,也是单步执行,但是会进入函数内部,然后结合n命令来调试函数。
p即print,打印变量,最常用的命令。p可以打印普通变量、std::string字符串、指针、数组等。

8. 哈希表

哈希表是非常重要的一种数据结构,例如HashMap集合底层也是基于哈希表来实现的;关于哈希表的知识点也是经常在面试中被问到,通过这几天对于哈希表的学习,包括看了哈希表的源码,以及手动编写了一个简单的哈希表,加深了对哈希表的理解,并在此进行总结!

  1. 什么是哈希表

Hash表也称散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。它基于数组,通过把关键字映射到数组的某个下标来加快查找速度,但是又和链表、树等数据结构不同,在这些数据结构中查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。

哈希表也可以当做一种缓存产品来使用,我们知道,频繁的访问数据库会造成非常大的系统开销,因而出现了很多的缓存产品,例如redis;我们也可以将频繁访问的数据存放在哈希表中,这样每次获取哈希表的值就不用从数据库获取,减少系统开销。

  1. 哈希函数
    为什么需要哈希函数?哈希函数就是关键字转换为数组的下标,这个转换的函数称为哈希函数(也称散列函数),转换的过程称为哈希化。

哈希函数就是把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就是哈希表。

  1. 哈希冲突
    多个key映射到相同的数组下标,即发生了哈希冲突;常见解决冲突的方法有:开放地址法、链地址法、桶

  2. 开放地址法
    若数据项不能直接存放在由哈希函数所计算出来的数组下标时,就要寻找其他的位置。分别有三种方法:线性探测、二次探测以及再哈希法。

4.1 线性探测
线性探测,指的就是线性的查找空白单元,例如我们要插入的key对应哈希表数组的下标是3,并且这个位置3已经被其它数据占用了,那么会查看下一个位置4是否被占用,若被占用,继续往下递增查找,直到找到一个空白的位置。

4.2 二次探测
二次探测的思想是探测相距较远的单元,而不是和原始位置相邻的单元,二次探测可以防止聚集的产生;但是二次探测法也会导致二次聚集的产生。

线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。

4.3 再哈希法
再哈希法是为了消除聚集和二次聚集提出来的;因为线性探测和二次探测产生的探测序列步长总是固定的,容易产生聚集,而再哈希法是指出现冲突后,把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长。对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。

  1. 链地址法
    链地址法的实现原理就是使用数组加链表,在哈希表每个单元中设置链表,当出现冲突后,不需要在原始的数组中寻找空位,而是将其他同样映射到这个位置的数据项加到链表中。


  2. 类似于链地址法,它是在每个数据项中使用子数组,而不是链表。这样的数组称为桶。

这个方法显然不如链表有效,因为桶的容量不好选择,如果容量太小,可能会溢出,如果太大,又造成性能浪费,而链表是动态分配的,不存在此问题。所以一般不使用桶。

  1. 聚集和装填因子
  2. 1 聚集
    当哈希表变得比较满时,我们每插入一个新的数据,都要频繁的探测插入位置,因为可能很多位置都被前面插入的数据所占用了,这称为聚集。数组填的越满,聚集越可能发生。

7.2 装填因子
已填入哈希表的数据项和哈希表容量的比率叫做装填因子

通过查看HashMap的源码可以看到,HashMap默认的装填因子就是0.75。

// 默认的加载因子 (扩容因子)
static final float DEFAULT_LOAD_FACTOR = 0.75f;

装填因子也叫加载因子、扩容因子或负载因子,用来判断什么时候进行扩容的,假如加载因子是0.5,HashMap的初始化容量是16,那么当HashMap中有16*0.5=8个元素时,HashMap就会进行扩容。

那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?

这其实是出于容量和性能之间平衡的结果:

当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。

  1. 关于哈希表的扩容
    回顾数组的基本知识,我们知道,数组的大小是固定的,无法进行扩展,所以哈希表的扩容只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。

但是需要注意的是:哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上。因此不能直接拷贝,需要按顺序遍历老数组,并使用insert方法向新数组中插入每个数据项。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 351134995@qq.com

×

喜欢就点赞,疼爱就打赏