linux操作系统常用面试题目

博客围绕Linux运维面试展开,虽未给出具体内容,但从标签可知涉及Linux系统相关知识及运维技能,对于准备Linux运维面试的人有一定参考价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

请描述一下用多线程怎么实现生产者消费者模型

知道nginx的惊群现象吗?怎么解决?

惊群效应概述

在Linux系统中,惊群效应常见于使用accept系统调用和epoll等多路复用机制的场景。例如,当一个父进程监听一个端口,并fork出多个子进程,所有子进程都尝试通过accept或epoll_wait等待新连接的到来。当新连接请求到达时,所有子进程可能都会被唤醒,但只有一个能成功处理新连接,其他则重新休眠。

Nginx的解决方案

Nginx通过以下策略解决惊群效应:

1. 主进程监听,工作进程处理

Nginx采用master-worker模型,其中master进程负责监听端口和分发连接请求,而worker进程负责处理实际的连接请求。master进程监听socket,当有新的连接请求到达时,master进程通过一定的策略(如轮询)将连接请求分配给其中一个空闲的worker进程。这种单一监听者模式避免了多个worker进程同时监听同一个socket的情况,从而减少了惊群效应的发生。

2. 锁机制(accept_mutex)

Nginx引入了一个互斥锁(accept_mutex)来控制对新连接的接受。当配置文件中启用了accept_mutex时,只有成功获取到锁的worker进程才能处理新连接请求。具体实现中,Nginx使用原子操作和共享内存来管理锁的状态,确保锁的安全性和高效性。

复制

// 伪代码示例
if (ngx_use_accept_mutex) {
    if (ngx_trylock_accept_mutex(cycle) == NGX_OK) {
        // 获取锁成功,处理新连接
        flags |= NGX_POST_EVENTS; // 设置事件延迟处理标志
    } else {
        // 获取锁失败,不处理新连接
    }
}
3. 负载均衡

Nginx通过负载均衡策略确保各个worker进程能够均匀分担工作负载。除了使用accept_mutex外,Nginx还通过监控每个worker进程的连接数和负载情况,动态调整新连接的分发策略。当一个worker进程的连接数达到其最大容量的7/8时,Nginx会停止向该进程分发新连接请求,直到其负载减轻。

复制

// 伪代码示例
if (ngx_accept_disabled > 0) {
    ngx_accept_disabled--; // 减少过载标志
} else {
    // 处理新连接请求
}
4. 利用内核特性

随着Linux内核的发展,一些内核特性也被用于减少惊群效应。例如,Linux 2.6及之后的版本在accept系统调用中引入了互斥等待变量,避免了不必要的唤醒。此外,Linux 4.5及以后的版本在epoll中增加了EPOLLEXCLUSIVE标志,允许用户设置只有一个进程或线程被唤醒来处理事件。Nginx在较新版本中利用这些内核特性来进一步优化性能。

5. EPOLL和SO_REUSEPORT

Nginx使用epoll作为其主要的事件驱动机制。每个worker进程都有自己的epoll实例,用于监听和处理事件。在Nginx 1.9.1及以后的版本中,还引入了SO_REUSEPORT选项,允许多个进程监听同一个端口,内核会自动将连接请求分发给其中一个进程,进一步减少了惊群效应。

结论

Nginx通过主进程监听、互斥锁、负载均衡、利用内核特性以及EPOLL和SO_REUSEPORT等多种策略有效解决了惊群效应,从而提高了服务性能和系统资源利用率。这些策略不仅减少了不必要的进程唤醒和上下文切换,还确保了各个worker进程能够公平地分担工作负载,为Nginx的高性能表现提供了有力支持。

请说一下epoll的内核实现,都涉及哪些数据结构?

epoll_create:创建内核事件表用来存放描述符和事件。它的数据结构为:struct eventpoll。其中包括了两个重要成员,一个是红黑树,也就是内核事件表。另一个重要成员是用于存放就绪事件的队列。

epoll_ctl:红黑树添加节点操作:ep_insert。红黑树移除节点操作:ep_remove。红黑树修改节点操作:ep_modify。每个节点都是一个描述符和事件的结构体。

epoll_wait:负责收集就绪事件。

注意:关于epoll_wait是如何收集就绪事件的,大致是这样一个思路:添加事件和描述符时,注册回调函数。当描述符上有事件就绪时,把描述符和事件添加到队列中

select和epoll的区别?

第一部分:select和epoll的任务
关键词:应用程序 文件句柄 用户态 内核态 监控者

要比较epoll相比较select高效在什么地方,就需要比较二者做相同事情的方法。

要完成对I/O流的复用需要完成如下几个事情:

1.用户态怎么将文件句柄传递到内核态?

2.内核态怎么判断I/O流可读可写?

3.内核怎么通知监控者有I/O流可读可写?

4.监控者如何找到可读可写的I/O流并传递给用户态应用程序?

5.继续循环时监控者怎样重复上述步骤?

搞清楚上述的步骤也就能解开epoll高效的原因了。

select的做法:

步骤1的解法:select创建3个文件描述符集,并将这些文件描述符拷贝到内核中,这里限制了文件句柄的最大的数量为1024(注意是全部传入---第一次拷贝);

步骤2的解法:内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作和select无关;

步骤3的解法:内核在检测到文件句柄可读/可写时就产生中断通知监控者select,select被内核触发之后,就返回可读可写的文件句柄的总数;

步骤4的解法:select会将之前传递给内核的文件句柄再次从内核传到用户态(第2次拷贝),select返回给用户态的只是可读可写的文件句柄总数,再使用FD_ISSET宏函数来检测哪些文件I/O可读可写(遍历);

步骤5的解法:select对于事件的监控是建立在内核的修改之上的,也就是说经过一次监控之后,内核会修改位,因此再次监控时需要再次从用户态向内核态进行拷贝(第N次拷贝)

epoll的做法:

步骤1的解法:首先执行epoll_create在内核专属于epoll的高速cache区,并在该缓冲区建立红黑树和就绪链表,用户态传入的文件句柄将被放到红黑树中(第一次拷贝)。

步骤2的解法:内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作与epoll无关;

步骤3的解法:epoll_ctl执行add动作时除了将文件句柄放到红黑树上之外,还向内核注册了该文件句柄的回调函数,内核在检测到某句柄可读可写时则调用该回调函数,回调函数将文件句柄放到就绪链表。

步骤4的解法:epoll_wait只监控就绪链表就可以,如果就绪链表有文件句柄,则表示该文件句柄可读可写,并返回到用户态(少量的拷贝);

步骤5的解法:由于内核不修改文件句柄的位,因此只需要在第一次传入就可以重复监控,直到使用epoll_ctl删除,否则不需要重新传入,因此无多次拷贝。

简单说:epoll是继承了select/poll的I/O复用的思想,并在二者的基础上从监控IO流、查找I/O事件等角度来提高效率,具体地说就是内核句柄列表、红黑树、就绪list链表来实现的。

fork()都会做哪些复制?

什么是写时拷贝?Fork以后,父进程打开的文件指针位置在子进程里面是否一样?

首先我们先来回忆一下fork()函数:

函数头文件:#include<unistd.h>

函数原型:pid_t fork(void);其中pid_t是一个宏定义:#define pid_t int

返回值:

fork函数调用一次返回两次:

在父进程中返回生成子进程的id(因为父进程不止拥有一个子进程)

在子进程返回一个整数0(因为子进程只有一个父进程,可以通过调用getppid()得到父进程的进程ID)

当然,如果创建失败,则返回-1;

复制内容:

传统的fork函数之后会做这样的举动:

例:

现在有一个进程1,在它的虚拟地址空间有:.text   .data    .bss    heap    stack

相应,内核会为这几个部分分配各自的物理空间。

我们现在用fork给进程1创建子进程,名为进城2

--->现在子进程开始复制这几个部分,也就是虚拟地址空间:

--->为子进程的这几个部分分配物理块:

可以看出:fork()之后会进行数据段,堆区,栈区的复制(.text共享)当然还有打开的文件描述符。

大家可以看到:传统的fork系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单

并且效率低下,因为它拷贝的数据也许并不共享,如果这时子进程执行exec函数系统调用,那么

拷贝也就没有什么意义了。

所以呢:::::接下来就有了一个新的技术叫写时拷贝:

fork()之后父进程的虚拟空间拷贝给子进程,在虚拟空间与物理页表建立映射的过程中使用了写时拷贝

使得子进程共享父进程的物理空间,当父子进程其中一个对该区域进行写入时,子进程复制一个新的

物理页表并建立映射,使得父子进程相互独立,同时节省了很多物理内存。子进程和父进程拥有相同

的相互独立的虚拟空间(不同的进程都拥有自己独立的虚拟地址空间),

但是却没有复制物理页表

写时拷贝:

在网上看到这样几句话分享给大家:

fork()之后exec之前两个进程用的是相同的物理空间(当然我们上面已经说过了),子进程的代码段,数据段

堆栈都是指向父进程的物理空间(物理页面为只读模式),也就是说,两者的虚拟空间不同,但对应的物理

空间是一样的,如果不是exec,内核会为子进程的数据段,堆,栈分配物理空间,而代码段继续共享父进程

的物理空间,而如果是因为exec,由于两者的代码不同,子进程的代码段也会分配到独立的物理空间。

接下来我们再来看一下vfork函数,看看vfork函数和fork函数有什么区别呢??????

vfork函数的做法更加简单粗暴,内核连子进程的虚拟空间也不创建了,直接共享了父进程的虚拟空间,也就间接

的共享了物理空间,保证子进程先运行,在它调用exec或exit之后父进程才可能被调度。

接下来呢还有一个问题就是父进程打开的文件指针位置在子进程里面是否一样??????

父进程的打开文件指针存放在PCB中,PCB被复制到子进程中后,子进程对应相应的文件描述符也能对文件

进行操作,该描述符指向同一个文件表项,文件表项引用计数加1;

打开文件的内核数据结构

两个独立进程各自打开同一个文件

注:此图中可以看出,对于同一个文件,打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。
 而每个进程都有自己的文件表项的一个理由是:使每个进程都有它自己的对该文件的当前偏移量。


注:文件描述符标志的作用域:只在一个进程中
    文件状态标志的作用域:任何进程中的所有描述符都可指向同一个文件状态标志。

父子进程的每一个相同的打开文件描述符共享一个文件表项

注:文件表项只有在所有引用它的fd(即文件描述符)全部关闭的情况下才会真正关闭,5如果子进程关闭父、子进程共享的文件描述符后父进程仍可以使用对应的文件表项。

iocp是什么

IOCP (Input/Output Completion Ports) 是 Windows 操作系统提供的一种高性能、可扩展的异步 I/O 模型。这种模型基于事件驱动,可以大幅提升 I/O 操作的效率和并发量,尤其适合处理大量连接和大量数据流的网络应用。IOCP 主要包含三个组件:I/O 端口、完成端口和工作者线程池。I/O 端口用来描述文件句柄或套接字,完成端口用来存储已完成的 I/O 请求以及通知工作者线程处理请求,工作者线程池则负责执行 I/O 操作和相关的业务逻辑。

iocp基本原理

IOCP 的基本原理是通过事件驱动的方式来处理 I/O 请求,以避免传统的阻塞式 I/O 操作带来的性能问题。具体来说,IOCP 通过以下几个步骤完成异步 I/O 操作:

  1. 应用程序首先创建一个或多个 I/O 端口,并将它们关联到套接字或文件句柄上。
  2. 当应用程序需要进行 I/O 操作时,它调用系统级别的 API,将请求提交到 I/O 端口上。
  3. 操作系统内核将 I/O 请求与相应的 I/O 端口关联,并立即返回,使得应用程序可以继续执行其他操作。
  4. 内核在后台异步地执行 I/O 操作,并将结果存储在完成队列中。
  5. 当 I/O 操作完成时,内核会通知完成端口,并将完成信息添加到完成队列中。
  6. 应用程序通过调用 GetQueuedCompletionStatus() 函数获取完成队列中的已完成请求,并按需处理它们。
  7. 如果完成队列为空,则应用程序可以等待新的完成事件发生,或者继续执行其他操作。

以上过程中,工作者线程负责从完成队列中取出已完成的 I/O 请求,并进行相应的处理。由于这些线程都是从线程池中获取的,因此可以有效地管理和控制线程的数量和使用情况,从而提高系统的效率和稳定性。

应用场景

IOCP 的高性能、可扩展特性使其广泛应用于以下几个领域:

  1. 网络编程。IOCP 可以帮助网络应用程序实现高并发、低延迟的数据传输,尤其适合处理大量连接和大量数据流的场景,如网络游戏、在线聊天、视频流媒体等。
  2. 数据库操作。IOCP 可以加速数据库操作中的文件读写、网络传输等 I/O 操作,提升数据库系统的性能和稳定性。
  3. 高性能服务器开发。IOCP 可以作为一种高效的事件驱动模型,帮助服务器应用程序快速响应客户端请求,提高系统的吞吐量和并发度。
  4. 多线程编程。IOCP 可以帮助开发者实现多线程编程中的任务分配和负载均衡,提高代码执行效率和并行度。

总之,IOCP 在需要高性能、高并发、低延迟的应用场景下具有很大的优势,并且可以有效地避免传统阻塞式 I/O 带来的性能问题。

tcp/ip的四层协议,为什么要有传输层和网络层?

TCP/IP(Transmission Control Protocol/Internet Protocol)是一组用于互联网通信的协议。它是互联网的基础架构,支持各种应用程序进行数据传输和网络连接。本文将详细介绍TCP/IP的工作原理、协议栈以及网络层的功能。

工作原理

TCP/IP是一种面向连接的通信协议,它使用客户端/服务器模型进行通信。在TCP/IP中,数据被划分为小的数据包,并在网络中通过IP地址进行传输。TCP/IP协议栈中的每个协议层都有特定的任务,以确保数据的传输和接收。

协议栈

TCP/IP协议栈是一组协议按照特定顺序工作以保证数据的传输。它由四个主要层组成,分别是:

  1. 应用层:应用层协议负责定义各种应用程序使用的数据格式和通信规则。例如,HTTP协议用于在Web浏览器和Web服务器之间进行通信。
  2. 传输层:传输层协议负责可靠地传输数据。其中,TCP(Transmission Control Protocol)提供面向连接、可靠和有序的数据传输,而UDP(User Datagram Protocol)提供无连接、不可靠和不保序的数据传输。
  3. 网络层:网络层协议处理在网络中传输数据包的路由和转发。其中,IP(Internet Protocol)负责将数据包从源主机传输到目标主机。
  4. 数据链路层:数据链路层协议负责将数据包从一个网络设备传输到另一个网络设备。其中,以太网(Ethernet)是使用最广泛的数据链路层协议。

网络层的功能

网络层是TCP/IP协议栈中的核心层,它负责处理网络互联和数据传输。以下是网络层的一些主要功能:

  1. IP寻址:网络层使用IP地址来唯一标识在互联网中的每个设备。IP地址由四个八位数字组成,例如192.168.0.1。这样的地址结构使得数据包能够准确地路由到目标设备。
  2. 路由选择:网络层根据目标IP地址选择最佳的路由路径,以保证数据的快速传输。路由选择是通过路由协议实现的,例如RIP(Routing Information Protocol)和OSPF(Open Shortest Path First)。
  3. 分片与重组:网络层负责将大的数据包分成更小的数据片段,以便能够在不同的网络中传输。目标设备通过网络层将这些数据片段重新组装成完整的数据包。
  4. 数据包的传输:通过网络层,数据包从源主机传输到目标主机。这个过程中,网络层负责将数据包逐一传递给下一跳路由器,直到到达目标主机。
tcp/ip三次握手和四次挥手过程以及信令流程,画出来!

tcp三次握手哪一个阶段会抛出异常?为什么不能两次握手,说下原因?

三次握手的概念

三次握手是TCP协议用于建立连接的一种机制。它涉及到客户端和服务器之间的三个步骤,确保双方都能够正常通信。

  • 第一次握手(SYN): 客户端向服务器发送一个SYN(同步)标志,表示客户端希望建立连接。
  • 第二次握手(SYN + ACK): 服务器接收到客户端的SYN后,回应一个带有SYN和ACK(确认)标志的报文,表示服务器已准备好接受连接请求。
  • 第三次握手(ACK): 客户端接收到服务器的响应后,发送一个带有ACK标志的报文,表示客户端也已准备好建立连接。这样,通过三次握手,双方确认彼此都能够正常通信,建立了可靠的连接。

为什么是三次握手?

为什么不是两次握手或四次握手呢?这涉及到建立连接的可靠性和防止网络中的不确定性。让我通过一个实际的案例来理解为什么三次握手是必要的。

案例分析:网络延迟引发的问题

假设我只有两次握手,而不是三次。客户端发送SYN,服务器回应SYN + ACK,看起来连接已经建立。但在这之后,由于网络延迟或其他原因,客户端并没有收到服务器的确认,导致客户端以为连接已建立,而服务器并不知情。

这种情况下,客户端和服务器之间的连接状态将变得不一致,可能导致各种问题,如资源浪费、连接超时等。为了防止这种情况,引入第三次握手可以确保双方都能够确认连接已经建立,避免了不确定性带来的问题。

安全性考虑

三次握手在一定程度上提高了连接的安全性。通过要求客户端和服务器都发送和确认连接请求,它减少了未经授权的连接建立的可能性。如果只有两次握手,可能会容易受到一些网络攻击,例如SYN洪泛攻击,因为服务器无法确认客户端是否真的要建立连接。

可靠性和状态同步

三次握手的每一步都具有明确定义的状态。第一次握手表示客户端希望建立连接,第二次握手表示服务器接受连接,并准备好接收数据,第三次握手表示客户端也确认连接建立。这种状态同步确保了双方都了解连接的当前状态。

处理网络延迟

三次握手的过程中,如果某一步的消息由于网络延迟未能及时到达,发送方会在一段时间后重新发送。这种机制有助于处理因为网络延迟引起的消息丢失,确保了连接的可靠性。

为什么不是两次握手?

如果只有两次握手,存在一些潜在的问题。例如,在两次握手中,服务器接收到连接请求后立即回应,这时连接就建立了。但如果这个回应由于网络延迟而在一段时间后才到达,客户端就无法得知连接是否真的建立成功。这可能导致客户端错误地认为连接已经建立,而服务器并不知情。

为什么不是四次握手?

四次握手是在连接关闭时使用的,与连接建立时的三次握手不同。在连接关闭时,需要双方确认彼此都已准备好断开连接。而在连接建立时,通过三次握手就能够确保连接的可靠性和安全性。

让我通过更详细的步骤来理解三次握手的建立连接过程。

  1. 客户端向服务器发送SYN: 客户端初始处于CLOSED状态,向服务器发送一个SYN标志的TCP报文,进入SYN-SENT状态。
  2. 服务器接收SYN并回应SYN+ACK: 服务器接收到客户端的SYN后,回应一个带有SYN和ACK标志的报文,表示服务器已准备好接受连接。服务器进入SYN-RECEIVED状态。
  3. 客户端发送ACK: 客户端接收到服务器的响应后,发送一个带有ACK标志的报文,表示客户端也确认连接建立。客户端和服务器都进入ESTABLISHED状态,连接建立成功。

详细步骤:关闭连接

当连接建立后,双方通信完成后可能需要关闭连接。下面是关闭连接的详细步骤:

  1. 主动关闭方发送FIN: 主动关闭方(可以是客户端或服务器)发送一个带有FIN标志的TCP报文,表示它已经完成了数据的发送。
  2. 被动关闭方接收FIN并回应ACK: 被动关闭方接收到FIN后,回应一个带有ACK标志的报文,表示接收到了关闭请求。此时,被动关闭方进入CLOSE-WAIT状态。
  3. 主动关闭方接收ACK: 主动关闭方接收到ACK后,进入FIN-WAIT-1状态。
  4. 被动关闭方发送FIN: 被动关闭方也可能在完成数据的发送后发送FIN,进入LAST-ACK状态。
  5. 主动关闭方回应ACK: 主动关闭方接收到被动关闭方的FIN后,回应一个ACK,进入TIME-WAIT状态。
  6. 连接关闭: 在一定时间后,主动关闭方进入CLOSED状态,连接关闭。

什么是虚拟进程?

Linux下进程都有哪些通信方式?项目中使用全双工和半双工通信的区别?

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

进程和线程的区别,那你知道的都说一下!

1、根本区别

进程和线程的根本区别是进程是操作系统(OS)资源分配的基本单位,而线程是处理器(CPU)任务调度和执行的基本单位。

2、资源开销:

每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

3、包含关系:

如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线
同完成的;线程是进程的一部分,所行过程不是一条线的,而是多条线(线耗)其被称为轻权进程或者轻量级进程。

4、内存分配:

同一进程的线程共享本进分享截屏空间和资源,而进程之间的地址空间和资源是相互独立的。

5、影响关系:

一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

6、执行过程:

每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

✌️ 补充

一、进程、线程、协程的概念

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
协程:是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子例程,或者说不带返回值的函数调用。

二、进程和线程的区别

地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
资源:线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
可并发性:
两者均可并发执行。
切换时:
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
其他:线程是处理器调度的基本单位,但是进程不是。

三、协程和线程的区别

协程避免了无意义的调度,由此可以提高性能,但程序员必须自己承担调度的责任。同时,协程也失去了标准线程使用多CPU的能力。
线程相对独立有自己的上下文切换受系统控制;协程相对独立有自己的上下文切换由自己控制,由当前协程切换到其他协程由当前协程来控制。

四、何时使用多进程,何时使用多线程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

五、为什么会有线程?

每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。

什么是同步/异步?你项目中写的半同步/半异步是什么意思?

同步和异步强调的是消息通信机制 (synchronous communication/ asynchronous communication)。 所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。 但是一旦调用返回,就得到返回值了。 换句话说,就是由“调用者”主动等待这个“调用”的结果。 而异步则是相反,"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。

同步和阻塞的区别

epoll的ET/LT模式在实现上有什么区别?内核上是两种模式是怎么实现的?

LT模式
对于读事件 EPOLLIN,只要socket上有未读完的数据,EPOLLIN 就会一直触发;对于写事件 EPOLLOUT,只要socket可写(一说指的是 TCP 窗口一直不饱和,我觉得是TCP缓冲区未满时,这一点还需验证),EPOLLOUT 就会一直触发。

在这种模式下,大家会认为读数据会简单一些,因为即使数据没有读完,那么下次调用epoll_wait()时,它还会通知你在上没读完的文件描述符上继续读,也就是人们常说的这种模式不用担心会丢失数据。

而写数据时,因为使用 LT 模式会一直触发 EPOLLOUT 事件,那么如果代码实现依赖于可写事件触发去发送数据,一定要在数据发送完之后移除检测可写事件,避免没有数据发送时无意义的触发。

ET模式
对于读事件 EPOLLIN,只有socket上的数据从无到有,EPOLLIN 才会触发;对于写事件 EPOLLOUT,只有在socket写缓冲区从不可写变为可写,EPOLLOUT 才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)

这种模式听起来清爽了很多,只有状态变化时才会通知,通知的次数少了自然也会引发一些问题,比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,否则如果数据残留到下一次消息来到时就会造成延迟现象。

这种模式下写事件触发后,后续就不会再触发了,如果还需要下一次的写事件触发来驱动发送数据,就需要再次注册一次检测可写事件。

数据的读取和发送
关于数据的读比较好理解,无论是LT模式还是ET模式,监听到读事件从socket开始读数据就好了,只不过读的逻辑有些差异,LT模式下,读事件触发后,可以按需收取想要的字节数,不用把本次接收到的数据收取干净,ET模式下,读事件触发后通常需要数据一次性收取干净。

而数据的写不太容易理解,因为数据的读是对端发来数据导致的,而数据的写其实是自己的逻辑层触发的,所以在通过网络发数据时通常都不会去注册监可写事件,一般都是调用 send 或者 write 函数直接发送,如果发送过程中, 函数返回 -1,并且错误码是 EWOULDBLOCK 表明发送失败,此时才会注册监听可写事件,并将剩余的服务存入自定义的发送缓冲区中,等可写事件触发后再接着将发送缓冲区中剩余的数据发送出去。

vi的基本命令?

Linux上查看系统内存使用情况的命令?

1、free命令

2、 vmstat命令

3、 /proc/meminfo 命令

4、 top命令

5、 htop 命令

Linux上查看系统版本的命令?

系统基本信息:uname
Linux发行版信息: lsb_release
CPU详细信息:lscpu
内存使用情况:free
系统实时进程状态: top
网络接口信息: ifconfig
网络连接相关信息:netstat
无线网络接口信息:iwconfig
磁盘分区信息:fdisk
磁盘使用情况: df
系统主机名等信息:hostnamectl
PCI设备信息:lspci
USB设备信息:lsusb
系统硬件详细信息:dmidecode
—————————

进程状态的命令?系统所启动服务的命令?

Linux上查看linuxCPU的命令?
进程池和线程池的具体实现写一下!
Linux  调试核心转储文件,程序断点是如何实现的(问我会不会汇编)?

fwrire和write的区别,sendfile的内部实现?

在介绍write()和fwrite()的区别和联系之前,先介绍什么是系统调用,系统调用执行期间所经历的每个步骤,以及为了提升系统性能而设置的缓冲机制。最后,给出write()和fwrite()的区别于联系。

系统调用

  系统调用时受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。以应用程序编程接口(API)的形式,内核提供一系列服务供程序访问。

  在深入系统调用的运作方式之前,务必关注以下几点:

  • 系统调用将处理器从用户态切换到内核态,以便CPU访问受保护的内核内存。
  • 系统调用的组成是固定的,每个系统调用都有一个唯一的数字编号。(程序通过名称来标识系统调用,对这一编号往往一无所知。)
  • 每个系统调用有其确定的参数,对用户空间(亦即进程的虚拟地址空间)与内核空间之间(相互)传递的信息加以规范。

  从编程的角度来看,系统调用与C语言函数的调用很相似。但是,在执行系统调用时,幕后会经历诸多步骤,下面以一个具体的硬件平台——x86-32为例,按事件发生的顺序对这些步骤加以分析。

  1. 应用程序通过调用C语言函数库中的外壳(wrapper)函数,来发起系统调用。
  2. 对系统调用中断处理程序来说,外壳函数必须保证所有的系统调用参数可用。外壳函数将堆栈参数复制到寄存器。
  3. 由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的CPU寄存器(%eax)中。
  4. 外壳函数执行一条中断机器指令(init 0x80),引发处理器从用户态切换到内核态,并执行系统中断0x80(十进制数128)的中断矢量所指向的代码1。
  5. 为响应中断0x80,内核调用system_call()例程(位于汇编文件arch/i386/entry.S中)来处理这次中断,具体如下:
    1. 在内核栈中保存寄存器值。
    2. 审核系统调用编号的有效性。
    3. 以系统调用编号对存放所有调用服务例程的列表(内核变量sys_call_table)进行索引,发现并调用相应的系统调用服务例程。若服务例程带有参数,那么将首先检查参数的有效性。例如,检查地址指向用户空间的内存位置是否有效。随后,该服务例程会执行必要的任务,这可能涉及对特定参数中指定地址处的值进行修改,以及用户内存和内核内存间传递数据。最后,该服务例程会将结果状态返回给system_call()例程。
    4. 从内核栈中恢复各寄存器值,并将系统调用返回值置于栈中。
    5. 返回至外壳函数,同时将处理器切换回用户态。
  6. 若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量errno2。然后,外壳函数会返回到调用程序,并同时返回一个整数值,以表明系统调用是否成功。

1. 较新的x86-32硬件平台实现了sysenter指令,较之传统的init 0x80中断指令,sysenter指令进入内核的速度更快。2.6内核已经glibc 2.3.2以后的版本都支持sysenter指令。

2. 在Linux上,系统调用服务例程遵循的惯例是调用成功则返回非负值。发生错误时,例程会对相应errno常量取反,返回一个负值。C语言函数库的外壳函数随即对其再次取反,将结果拷贝至errno,同时以-1作为外壳函数的返回值返回,向调用程序表明有错误发生。

库函数

C语言标准库函数由许多库函数组成,其用途多种多样,可以用来执行以下任务:打开文件、将时间转换为可读格式,以及进行字符串比较等等。许多库函数不会使用任何系统调用(如字符串相关操作),在用户态即可完成相关逻辑,但另外一些库函数,在建立在系统调用之上,是系统调用在用户态的封装。之所以设计库函数,是为了提供比直接调用库函数更为灵活方便的接口。当然,有时为了更进一步,希望能够比直接调用系统调用付出更小的开销。

标准C语言函数库的实现随UNIX的实现而异。GNU C语言函数库(glibc)是Linux上最常用的实现。

write()

在介绍了系统调用和库函数之后,回到本文的主题:write()函数和fwrite()函数。简单来说,write()函数是系统调用,是文件通用I/O API,fwrite()是glibc库函数,是系统调用write()在用户态的封装。所谓通用I/O模型,是因为在Linux系统中,所有设备都抽象为文件,那么,open()、read()、write()和close()可以对所有类型的文件执行I/O操作。

所有执行I/O操作的系统调用都以文件描述符,一个非负整数(通常是小整数)来指向打开的文件。write()系统调用将数据写入一个已打开的文件中。

 
    
#include <unistd.h>
// Returns number of bytes written, or -1 on error
ssize_t write(int fd, void *buffer, size_t count);

buffer参数为要写入文件中的数据的内存地址,count参数为要写入文件的字符数,fd参数为目的文件的描述符。如果调用成功,将返回实际写入文件的字节数,该返回值可能小于count值,这称为“部分写”。对磁盘文件来说,造成“部分写”的原因可能是磁盘已满,或者是因为进程资源对文件大小的限制。

内核缓冲

出于速度和效率考虑,系统I/O调用(即内核)和标准C语言库I/O函数,在操作磁盘文件时,会对数据进行缓冲。

write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存(kernel buffer cache)之间复制数据。例如,如下调用将3个字节的数据从用户空间内存传递到内核空间的缓冲区中,write()随即返回:

 
    
write(fd, "abc", 3);

在后续某个时间,内核会将其从缓冲区写入(刷新)到磁盘。(因此,可以说系统调用与磁盘操作并不同步。)如果在此期间,另一进程试图读取该文件的这几个字节,那么内核将自动从缓冲高速缓存中提供这些数据,而不是从文件中(读取过期的内容)。通过缓存,减少了内核必须执行磁盘操作传输数据的次数,提升了性能。

Linux内核对缓冲区高速缓存的大小没有固定上限。内核会分配尽可能多的缓冲区高速缓存页,而仅受限于两个因素:可用的物理内存总量,以及出于其他目的对物理内存的需求(例如,需要将正在运行进程的文本和数据页保留在物理内存中)。若可用内存不足,则内核会将一些修改过的缓冲区高速缓存页内容刷新到磁盘,并释放其供系统使用。

更确切地说,从内核2.4开始,Linux不再维护一个单独的缓冲区高速缓存。相反,会将文件I/O缓冲区置于页面高速缓存中,其中还含有诸如内存映射文件的页面。然而,依然沿用了“缓冲区高速缓存(buffer cache)”这一术语。

fwrite()

正如上文所述,fwrite()是glibc库函数,是系统调用write()的封装。

 
    
#include <stdio.h>
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);

buffer指向要写入的缓存数组中第一个对象,size为每个对象的大小,count表示写入对象的数量,stream指向输出流。

如果调用成功,函数返回写入的对象的数量;如果有错误发生,函数返回值可能小于count值。如果size或者count为0,函数返回0并且不会有任何操作。

 库缓冲

当操作磁盘文件时,库函数将大块数据缓冲在用户空间以减少系统调用,如fprintf()、fscanf()、fgets()、fputc()、fgetc()、fputs()。因此,stdio库可以是编程者免于自行处理对数据的缓冲。

当然,stdio库提供函数允许设置缓冲模式。

 
    
#include <stdio.h>
// Returns 0 on success, or nonzero on error
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

参数stream标识将要修改哪个文件流的缓冲。打开流后,必须在调用任何其他stdio函数之前先调用setvbuf()。setvbuf()调用将影响后续在指定流上进行的所有stdio操作。

参数buf和size则针对参数stream要使用的缓冲区,有两种方式:

  • buf不为NULL,那么其指向size大小的内存块以作为stream的缓冲区,该缓冲区应该以动态或者静态在堆中为该缓冲区分配一块空间(类似malloc()函数),而不是分配在栈上的本地变量。否则,函数返回时将销毁其栈帧,从而导致混乱。
  • buf为NULL,stdio库则会为stream自动分配一块缓冲区(除非是非缓冲的IO)。glibc在此场景下自动忽略size参数。

参数mode指定了缓冲类型,并具有下列值之一:

_IONBF

不对IO进行缓冲。每个stdio库函数将立即调用write()或者read(),并且忽略buf和size参数,可以分别指定两个参数为NULL和0。stderr默认属于这一类型,从而保证错误能立即输出。

_IOLBF

采用行缓冲IO。指代终端设备的流默认属于这一类型。对于输出流,在输出一个换行符(除非缓冲区已经填满)前将缓冲数据,对于输入流,每次读取一行数据。

_IOFBF

采用全缓冲IO。单次读、写数据(通过read()或者write()系统调用)的大小与缓冲区相同,指代磁盘的流默认采用此模式。

setbuf()和setbuffer()和setvbuf()类似。

无论当前采用何种缓冲模式,在任何时候,都可以使用fflush()库函数强制将stdio输出流中的数据(即通过write())刷新到内核缓冲区中。此函数会刷新指定stream的输出缓冲区。

总结

write()和fwrite()到底有什么区别和联系呢?

  1. fwrite()是glibc库函数,write()是系统调用,是系统内核为外部访问提供的服务访问接口。
  2. fwrite()是系统调用write()的库函数再封装。
  3. fwrite()在用户空间会对数据提供可自定义的缓冲空间,write()会在内核态提供内核缓冲空间。(对于硬盘,内核空间缓存转移到硬盘中的缓存。)数据写入缓冲区后即可返回。
libevent、Rector模式、服务器网络模型
服务器瓶颈的定位?怎么测试定位?如何设计解决瓶颈问题?

IO瓶颈的解决方案都有哪些?

压测过程中TPS上不去?请求响应时间过长?

硬件资源不足:服务器的CPU、内存、磁盘等硬件资源不足,无法支撑高并发的请求处理。可以通过增加硬件资源或者优化服务器配置来提升TPS。

网络带宽限制:网络带宽不足,导致服务器无法及时响应请求。可以通过增加网络带宽或者优化网络传输方式来提升TPS。

数据库性能问题:数据库的读写性能不足,无法满足高并发的请求。可以通过优化数据库结构、索引、查询语句等方式来提升数据库性能。

代码逻辑问题:应用程序中存在性能瓶颈或者死循环等问题,导致请求处理速度变慢。可以通过代码优化来提升性能。也可能存在死锁或其他同步问题导致线程或进程阻塞,从而影响TPS。

压测/肉机配置问题:压测工具的配置(如压测肉机数量、压测脚本、场景设计等)可能影响TPS的表现。如压力机可能无法虚拟出预期的用户数,或者压测脚本和场景设计不合理,都会导致TPS无法提升。

负载均衡:负载均衡器配置不当可能导致请求分配不均,从而影响性能。检查负载均衡策略,优化配置。

依赖服务问题:应用程序依赖的第三方服务性能不稳定或者响应速度较慢,导致整体性能受限。可以通过替换或者优化第三方服务来提升性能。
中间件瓶颈:如消息队列堵塞,或有其他排队机制导致无法绕过队列进行高并发请求。

服务配置问题:系统的配置参数不合理,如线程池大小、连接池配置等,也会导致响应时间增。

1、应用cpu瓶颈如何定位分析

定位:
导致应用cpu使用率高,并且使用核数超出机器的核数(如:申请的机器是4核16G,但核数最大显示6核),有可能是经常上下文切换频繁导致的(上下文切换:就是当前进程请求某种资源(如IO操作)而无法立即得到满足,需要切换到其他进程执行,等待资源就绪后再切换回来);
程序使用复杂的算法如加/解密,或代码bug导致。

分析:
减少上下文切换频繁,可以让配置进程数与压测的机器核数一样(因为一个核就有一个进程),配置完成再查看;

如果配置好重新压测,cpu使用率依旧还是高,可以看看io是否很高(io操作:读取和写入磁盘上的文件或数据),如果io高,那么有可能出现堵塞;
如果压测的请求响应时间也比较大,io和cpu也很高,那么有可能是代码有问题,查看火焰图,查看代码耗时是否比较大,还有mysql监控是否有慢查询等,或者连接数比较少导致;

如果火焰图代码部分,有耗时比较大并且有慢查询的,可以进行sql语句优化;
如果连接数比较少,设置大一点连接数去重新压,再观察性能情况;
如果这些都配置好都修改好,还是有问题,可以考虑加资源或者考虑该业务是否可以进行异步处理来提高性能。

2、应用内存瓶颈如何定位分析

定位:
在压测过程中,可以借助使用监控工具实时监测服务的内存使用情况,定期获取内存快照,监控的主要指标包括内存占用率、内存使用量、内存泄漏等。

分析:
查看内存在压测完成后,过一段时间是否有下降趋势。如果没有下降趋势,查看火焰图,查看代码属于哪个函数使用的内存占比大导致,然后优化应用/服务代码后重新压,压测完成再过一段时间查看内存是否有下降,对比优化前后内存使用情况,确认优化措施是否有效。

3、应用磁盘瓶颈如何定位分析

定位:
监控磁盘I/O,可使用系统监控工具(如Linux的 iostat、iotop,Windows的Resource Monitor等)实时监控磁盘读写速度、等待时间等指标,看是否存在过高延迟或读写速率过低的情况。

分析:
一般磁盘瓶颈就是io高,io高的话一般都是磁盘读写比较大导致,导致磁盘读写比较大,可能以下几种原因导致:

读写的数据太大导致;
开发写的代码有问题;
可能带宽比较小导致;

sql语句写的不规范导致,或者数据库连接数比较少导致,也有可能是mysql配置参数问题,如:数据库缓存配置小,导致不够用,才一直进行磁盘的读写操作。

如果代码都没问题,连接数都没问题,myslq配置也正确,但io还是很高的,可以增加资源来解决后继续进行压测。

4、数据库内存瓶颈如何定位分析

定位:
监控数据库内存使用情况:在压测过程中,使用监控工具实时监测数据库的内存使用情况,包括内存占用量、缓冲池使用情况等;可以通过数据库管理工具或者操作系统工具来监控。

分析数据库缓冲池:查看数据库的缓冲池配置,了解缓冲池的大小、命中率等;如果缓冲池命中率低或者缓冲池过小,可能会导致频繁的磁盘读取,从而影响性能。

检查数据库索引:检查数据库表的索引情况,确定是否存在缺失或者无效的索引;缺失或者无效的索引可能导致数据库执行大量的全表扫描,增加了磁盘IO的负担。

分析数据库查询语句:分析服务对数据库的查询语句,了解查询的复杂度和效率;复杂且低效的查询语句可能会导致数据库内存的过度消耗,从而影响性能。

查看数据库日志:查看数据库的错误日志和慢查询日志,了解是否存在异常和性能较差的查询;通过分析日志可以找出潜在的性能问题和优化的方向。

检查数据库配置:检查数据库的配置参数,例如内存限制、并发连接数等;如果配置参数不合理,可能会导致内存不足或者并发连接过多,从而影响性能。

分析:
数据库使用内存高,一般是请求接口需大量的内存进行存储数据,如果内存不够用,会导致请求的接口失败,接口请求失败会导致响应慢等,还有可能会一直分配内存,导致有阻塞从而导致io高,磁盘读写也高,cpu使用率也高,网络io也会高,导致失败不断请求,网络io肯定高,可以新增资源通过负载均衡来解决。

根据分析结果,优化数据库查询语句和索引,例如调整查询条件、添加合适的索引等;同时,也可以考虑调整数据库的配置参数,如增加内存限制、调整并发连接数等。

5、网络io瓶颈如何定位分析

定位:
可以通过使用网络监控工具如 Wireshark、netstat 命令等,来查看网络流量、带宽使用率、包丢失率、延迟等指标。如果这些指标异常,可能是网络IO瓶颈。

检查网络设备,如路由器、交换机、防火墙等,查看其配置和性能。确保设备的带宽和性能能够满足需求,如果设备配置不合适或者存在故障,可能会引起网络I/O瓶颈。

分析:
网络io高,一般是写入读取的数据量大,需要很大的网络流量,如申请的带宽比较小也会导致,可将测机器的带宽网络申请至万兆以上来解决。

6、带宽瓶颈如何定位分析

定位:
排查系统/服务运行在云环境中,检查云服务商的网络带宽限制;
检查系统有多个服务器,检查负载均衡策略是否合理。不恰当的负载分配可能导致某些服务器的带宽过度使用。

分析:
出现带宽比较小的情况,通过增加带宽来解决遇到的瓶颈;
可以增加并发连接数进行测试,观察带宽利用率的变化。如果带宽利用率随着并发连接数的增加而增加,说明带宽瓶颈可能是由于连接数的限制引起的。

7、redis内存瓶颈如何定位分析

定位:
使用Redis的监控工具或者第三方监控工具,实时监测Redis的内存使用情况,包括内存占用量、内存碎片、最大使用内存等指标。

查看Redis的日志文件,观察是否有内存相关的警告或错误信息,如内存使用过高的问题。

分析:
如果压测完成,redis的内存不变,一直不会下降,有可能应用/服务存储的key值没有设置过期时间导致,这很容易导致内存泄漏,需检查对应的代码,看是否存储内存的值未设过期时间;检查缓存策略是否有过多的数据被缓存,或者缓存更新策略导致了大量的无效内存占用。

以上方法无法解决内存瓶颈问题,可以考虑扩容Redis集群,增加节点的数量和内存容量,以提高整个系统的内存处理能力。

Linux怎么调试内存?

描述符对于服务器有什么用,感觉是TCP底层(不太会)

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。 同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。

文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4......

Linux内核对所有打开的文件有一个文件描述符表格,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表这个数组的下标,数组的内容就是指向一个个打开的文件的指针。

上面只是简单理解,实际上关于文件描述符,Linux内核维护了3个数据结构:

进程级的文件描述符表
系统级的打开文件描述符表
文件系统的i-node表
一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。进程级的描述符表的每一条记录了单个进程所使用的文件描述符的相关信息,进程之间相互独立,一个进程使用了文件描述符3,另一个进程也可以用3。除了进程级的文件描述符表,系统还需要维护另外两张表:打开文件表、i-node 表。这两张表存储了每个打开文件的打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息。

系统级的打开文件描述符表:

当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
打开文件时的标识(open()的flags参数)
文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
与信号驱动相关的设置
对该文件i-node对象的引用,即i-node 表指针
文件系统的i-node表:

文件类型(例如:常规文件、套接字或FIFO)和访问权限
一个指针,指向该文件所持有的锁列表
文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
文件描述符、打开的文件句柄以及i-node之间的关系如下图:

在进程 A 中,文件描述符 1 和 20 都指向了同一个打开文件表项,标号为 23(指向了打开文件表中下标为 23 的数组元素),这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。
进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件,这可能是在调用 fork() 后出现的(即进程 A、B 是父子进程关系),或者是不同的进程独自去调用 open() 函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项,但这些表项均指向 i-node 表的同一个条目(标号为 1976);换言之,它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。 
这就说明:同一个进程的不同文件描述符可以指向同一个文件;不同进程可以拥有相同的文件描述符;不同进程的同一个文件描述符可以指向不同的文件(一般也是这样,除了 0、1、2 这三个特殊的文件);不同进程的不同文件描述符也可以指向同一个文件。

epoll的触发模式
页缓存他说的pagecache?Linux内核物理页面的页面缓存机制

Linux进程虚拟地址空间的分布

1、参数说明

程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。

初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。

未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。

栈(Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。

堆 (Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。

2、注意事项

  1. Text, BSS, Data段在编译时已经决定了进程将占用多少M,可以通过size知道这些信息。
  2. 正常情况下,Linux进程不能对用来存放程序代码的内存区域执行写操作,即程序代码是以只读的方式加载到内存中,但它可以被多个进程安全的共享。
epoll触发模式(二面又问了,问了2遍)
Linux进程核心转储文件的调试coredump
进程通信
Libevent的实现机制
Linu查看进程堆栈命令
LRU的实现策略
malloc底层的实现是什么?
半同步/半异步模式和work-master模式是什么?
进程间通信共享内存的底层原理是什么?
说说gdb常用的调试命令都有哪些?
Linux的内存分布(4G空间)?
tcp三次握手,四次挥手,为什么是三次?为什么是四次?time_wait出现在什么时候,它的作用是什么?画出tcp报头?tcp的滑动窗口满,返回什么?

进程间通信有几种方式?你都在什么情况用到?

进程间通信的几种方式包括:

  1. 管道:半双工通信,只能在具有亲缘关系的进程间使用。
  2. 命名管道(FIFO):允许无亲缘关系进程间的通信。
  3. 消息队列:由消息的链表存放在内核中,用于进程间通信。
  4. 共享内存:解决了消息队列的内核态和用户态数据拷贝开销。
  5. 信号量:用于多个进程同时往共享内存写入数据。
  6. 信号:常规状态下的工作模式。
  7. Socket:用于跨网络和不同主机上的进程通信线程同步的方式1、互斥锁      当有多个线程需要访问同一个资源时,如果不做处理,有时候就会出现问题,比如有两个线程需要使用打印机,进程A正在使用,而进程B也要使用打印机,此时打印出来的东西就是错乱的。互斥锁就是控制对共享资源的使用。互斥锁只有两种状态:加锁、解锁。互斥锁的特点:原子性:把互斥量锁定位一个原子操作,这就保证了如果同一时间只会有一个线程锁定共享资源唯一性:如果一个线程锁定了某个互斥量,那么只有该线程可以使用这个被锁定的互斥量非繁忙等待:如果一个线程锁定了某互斥量,另一个线程又来访问该互斥量,则第二个线程会被挂起,当第一个线程解锁该互斥量后唤醒第二个线程对该互斥量进行访问。2、条件变量:       与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来锁定一个线程,直到某个特殊的条件发生为止。通常条件变量和互斥锁同时发生。条件变量可以是我们睡眠等待某种情况的发生。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”而挂起另一个线程使条件成立      条件的检测是在互斥锁的保护下进行的。线程在改变条件变量的状态之前必须先锁定互斥量。如果一个条件为假,则线程自动阻塞,并释放等待状态改变的互斥锁。如果另外一个线程改变了条件,它发信号给相关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。条件变量的操作流程如下:初始化条件变量等待条件成立激活条件变量清除条件变量3、信号量      使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。      信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。信号量的使用及实例:信号量4、读写锁      读写锁和互斥锁类似,不过读写锁允许更改的并行性。互斥锁要么是加锁状态,要么是不加锁状态。而读写锁可以有三种状态:读模式下的加锁、写模式下的加锁、不加锁 状态。一次只有一个线程可以占有写模式下的读写锁,但是可以有多个线程占有读模式下的读写锁。读写锁的特点:如果有线程读数据,则允许其他线程读数据,但不允许写如果有线程写数据,则不允许其他线程进行读和写5、自旋锁      自旋锁和互斥锁的功能一样,但是互斥锁在线程阻塞时会让出cpu,而自旋锁则不会让出cpu,一直等待,直到得到锁。

页面置换算法有哪些

页面置换算法是操作系统中用于管理内存的一种技术。当系统的物理内存不足时,页面置换算法决定哪些内存页面将被移出,以便为新的页面腾出空间。这些算法的目标是最小化页面置换的频率,从而提高系统的性能。

常见的页面置换算法

最优页面置换算法(OPT)

最优页面置换算法是一种理想化的算法,它选择将来最长时间不会被访问的页面进行置换。虽然在实际中无法实现,因为无法预知未来的页面访问情况,但OPT算法提供了一个性能评估的标准1

先进先出页面置换算法(FIFO)

先进先出算法是一种简单的页面置换策略,它淘汰最早进入内存的页面。FIFO算法容易实现,但可能会导致Belady异常,即系统给进程分配的物理内存增加时,缺页率反而上升2

最近最少使用页面置换算法(LRU)

最近最少使用算法基于“最近未被使用的页面在将来也不太可能被使用”的假设,选择最近最少被访问的页面进行置换。LRU算法的性能接近最优算法,但实现起来较为复杂3

第二次机会页面置换算法(SC)

第二次机会算法是对FIFO算法的改进,它给每个页面一个机会证明自己最近是否被访问过。如果页面最近被访问过,则给予第二次机会,否则进行置换2

时钟页面置换算法(CLOCK)

时钟算法是第二次机会算法的变种,它通过维护一个循环列表来实现,效率更高。时钟算法检查指针指向的页面,如果该页面最近被访问过,则指针前移,否则置换该页面2

最不常用页面置换算法(NFU)

最不常用算法通过为每个页面维护一个计数器来跟踪页面的访问频率,每次置换时选择计数器值最小的页面2

老化算法

老化算法是NFU算法的改进版,它通过对计数器进行右移和加权来模拟页面的老化过程,从而更准确地选择置换页面2

工作集页面置换算法

工作集算法基于一个进程当前正在使用的页面集合,即工作集。它试图保持进程的工作集在内存中,以减少缺页中断2

工作集时钟页面置换算法(WSClock)

工作集时钟算法结合了工作集和时钟算法的特点,通过检查页面的工作集状态和访问位来决定页面置换,这种算法在实际中得到了广泛应用2

总结

页面置换算法是操作系统内存管理的重要组成部分,它们通过不同的策略来优化内存的使用效率。虽然没有一种算法能够适用于所有情况,但理解这些算法的原理和适用场景对于设计和优化操作系统至关重要。

负载均衡常用算法

常见的负载均衡算法包括:

  1. 轮询法
  2. 随机法
  3. 源地址哈希法
  4. 加权轮询法
  5. 最少活跃连接算法
  6. 前言
    (1)轮转调度(Round-Robin Scheduling)算法
    (2)加权轮转调度(Weighted Round-Robin Scheduling)算法
    (3)随机均衡调度(Random Scheduling)算法
    (4)加权随机均衡调度(Weighted Random Scheduling)算法
    (5)最小连接调度(Least-Connection Scheduling)算法
    (6)加权最小连接调度(Weighted Least-Connection Scheduling)算法
    (7)目标地址散列调度(Destination Hashing Scheduling)算法
    (8)源地址散列调度(Source Hashing Scheduling)算法
    (9)基于局部性的最少链接调度(Locality-Based Least ConnectionsScheduling)算法
    (10)带复制的基于局部性最少链接调度(Locality-Based Least Connectionswith Replication Scheduling)算法
    (11)响应速度均衡调度(Response Time Scheduling)算法
    (12)处理能力均衡调度(Processing Capacity Scheduling)算法
    (13)DNS均衡调度(DNS Scheduling)算法
    前言
    所谓负载,一般指处理节点的CPU负载、MEM利用率、网络负载、可用的缓冲区、应用系统负载、用户数量以及其他的各种系统资源的当前状态信息。所谓负载均衡,是指处理节点的负载信息通过某代理软件传递给均衡器,由均衡器做出决策并对负载进行动态分配,从而使集群中各处理节点的负载相对趋于平衡。
    要实现
    (1)为用户提供更好的访问质量。
    (2)提高服务器响应速度。
    (3)提高服务器及其他资源的利用效率。
    (4)避免了网络关键部位出现单点失效。
    (5)解决网络拥塞问题,服务就近提供,实现地理位置无关性。
    衡量服务器性能和当前运行情况的有效衡量指标包括:
    (1)CPU利用率。
    (2)内存使用率。
    (3)带宽利用率。
    (4)硬盘IO吞吐率和网络IO吞吐率。
    (5)单位时间内完成的服务次数。
    (6)单位时间内连接客户数
    (7)完成一个请求任务所用的响应时间。
    理想的负载指标应满足以下条件:测量开销低,能体现所有竞争资源上的负载,各负载指标在测量及控制上彼此独立。
    在引入负载均衡方案时不管是采用哪种方式都需要考虑以下问题:
    (1)采用负载均衡方案后,服务器接收和转发数据报的速度及负载均衡的整体检测能力是首先要考虑的重点问题。
    (2)负载均衡方案应能满足网络流量不断增长的需求,能均衡不同操作系统和硬件平台之间的负载,能均衡不同流量的负载。
    (3)负载均衡设备自身出现故障时应该有良好的冗余解决方案,保证可用性,避免系统遭受重大损失。
    (4)采用灵活、直观和安全的管理方式,这样便于安装、配置、维护和监控,提高工作效率,避免差错
    下面介绍几种常见的负载均衡算法:

    (1)轮转调度(Round-Robin Scheduling)算法
    假设所有服务器处理性能相同,将外部请求按顺序轮流分配到集群中的服务器上。这种算法的优点是其简洁性,无需记录当前所有连接的状态,是一种无状态调度算法,但是不适用于服务器组中处理性能不一的情况,而且当请求服务时间变化较大时,容易导致服务器间的负载不平衡。

    (2)加权轮转调度(Weighted Round-Robin Scheduling)算法
    为保证处理能力强的服务器处理更多的访问流量,用相应的权值表示服务器的处理性能,将请求数目按权值的比例分配给各服务器。调度器可以自动询问服务器的负载情况,并动态地调整其权值。这种均衡算法也是一种无状态调度算法,但可以解决服务器间性能不一的情况,能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。

    (3)随机均衡调度(Random Scheduling)算法
    把来自网络的请求随机分配给各个服务器。

    (4)加权随机均衡调度(Weighted Random Scheduling)算法
    此种均衡算法类似于加权轮转算法,不过在处理请求分担时是个随机选择的过程。

    (5)最小连接调度(Least-Connection Scheduling)算法
    该算法是一种动态调度算法,通过服务器中当前所活跃的连接数来估计服务器的负载情况,把新的连接请求分配到当前连接数最小的服务器。但是,当各个服务器的处理能力不同时,该算法并不理想。

    (6)加权最小连接调度(Weighted Least-Connection Scheduling)算法
    用相应的权值表示各个服务器的处理性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询服务器的负载情况,并动态地调整其权值。

    (7)目标地址散列调度(Destination Hashing Scheduling)算法
    根据请求的目标 IP地址,将其作为散列键(Hash Key),通过散列(Hash)函数将这个目标IP地址映射到一台可用且未超载的服务器,将请求发送到该服务器,属于静态映射算法。

    (8)源地址散列调度(Source Hashing Scheduling)算法
    与目标地址散列调度算法相反,根据请求的源 IP地址,作为散列键(HashKey),通过散列函数将请求的源IP地址映射到一台可用且未超的服务器,将请求发送到该服务器。除了将请求的目标IP地址换成请求的源IP地址,该算法采用的散列函数与目标地址散列调度算法相同,算法流程与目标地址散列调度算法的基本相似。在实际应用中,源地址散列调度和目标地址散列调度可以结合使用在防火墙集群中,它们可以保证整个系统的唯一出入口。

    (9)基于局部性的最少链接调度(Locality-Based Least ConnectionsScheduling)算法
    找出请求的目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;否则用“最少链接”的原则选出一个可用的服务器。算法的设计目标是在服务器的负载基本平衡情况下将相同目标IP地址的请求调度到同一台服务器,来提高各台服务器的访问局部性和主存Cache命中率。

    (10)带复制的基于局部性最少链接调度(Locality-Based Least Connectionswith Replication Scheduling)算法
    与基于局部性的最少链接调度算法的不同之处在于,它要维护从一个目标IP地址到一组服务器的映射,而不是从一个目标IP地址到一台服务器的映射。该算法找出请求的目标IP地址对应的服务器组,按“最少链接”原则从服务器组中选出一台服务器,若服务器可用且没有超载,将请求发送到该服务器;否则按“最少链接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,再将请求发送到该服务器。另外,若该服务器组有一段时间未被修改,则将最忙的服务器从服务器组中删除,以降低复制的程度。

    (11)响应速度均衡调度(Response Time Scheduling)算法
    负载均衡设备对内部各服务器发出一个探测请求,然后由对探测请求响应最快的一台服务器来响应客户端的服务请求。

    (12)处理能力均衡调度(Processing Capacity Scheduling)算法
    负载均衡设备记录集群内部处理负荷(根据服务器CPU型号、CPU数量、内存大小及当前连接数等换算而成),将服务请求分配给负荷最轻的服务器。由于考虑到了内部服务器的处理能力及当前网络运行状况等不同情形,因此这种均衡算法相对来说更加精确,尤其适合运用到第7层(应用层)负载均衡的情况,但附加开销也较大。

    (13)DNS均衡调度(DNS Scheduling)算法
    分处在不同地理位置的负载均衡设备,收到同一个客户端的域名解析请求,并在同一时间内,把此域名解析成各自相对应服务器的IP地址,并返回给客户端,客户端将以最先收到的域名解析IP地址来继续请求服务,而忽略其他的IP地址响应。

心跳包机制

send的返回值是什么?你刨析过什么源码?

不管是send还是recv方法,都是数据的缓冲区和发送的缓冲区的拷贝操作过程,真正发送数据的是协议功能,注意三种返回值的可能性,>0表示成功,返回实际发送或接受的字节数,=0表示超时,对方主动关闭了连接过程,<0出错,此种情况可能出现过重情况,如上所示。其中errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN这三种是特殊情况,实际使用中表示继续正常接受数据即可。

线程同步的机制(四种锁,信号量,屏障,条件变量)

1.互斥锁(mutex):互斥锁是最常见的一种锁,用来保护共享资源的互斥访问。一次只有一个线程可以获得互斥锁。如果其他线程试图获得已经被锁定的互斥锁,他们将被阻塞,直到锁被释放
2.递归锁(recursive lock):递归锁是一种特殊的互斥锁,允许同一个线程多次获得同一个锁,而不会导致死锁。递归锁维护了一个锁的计数器和一个拥有锁的线程标识
3.读写锁(read-write lock):读写锁允许多个线程同时读取共享资源,但再写入时需要互斥访问,这可以提高在读操作远多于写操作的场景下的性能
4.自旋锁(spinlock):自旋锁是一种特殊的锁,当锁已经被其他线程获得时,尝试获得锁的线程将忙等待(在一个循环中不断尝试获得锁),而不是被阻塞,自旋锁适用于锁持有时间非常短的场景

线程同步的方式1、互斥锁      当有多个线程需要访问同一个资源时,如果不做处理,有时候就会出现问题,比如有两个线程需要使用打印机,进程A正在使用,而进程B也要使用打印机,此时打印出来的东西就是错乱的。互斥锁就是控制对共享资源的使用。互斥锁只有两种状态:加锁、解锁。互斥锁的特点:原子性:把互斥量锁定位一个原子操作,这就保证了如果同一时间只会有一个线程锁定共享资源唯一性:如果一个线程锁定了某个互斥量,那么只有该线程可以使用这个被锁定的互斥量非繁忙等待:如果一个线程锁定了某互斥量,另一个线程又来访问该互斥量,则第二个线程会被挂起,当第一个线程解锁该互斥量后唤醒第二个线程对该互斥量进行访问。2、条件变量:       与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来锁定一个线程,直到某个特殊的条件发生为止。通常条件变量和互斥锁同时发生。条件变量可以是我们睡眠等待某种情况的发生。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”而挂起另一个线程使条件成立      条件的检测是在互斥锁的保护下进行的。线程在改变条件变量的状态之前必须先锁定互斥量。如果一个条件为假,则线程自动阻塞,并释放等待状态改变的互斥锁。如果另外一个线程改变了条件,它发信号给相关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。条件变量的操作流程如下:初始化条件变量等待条件成立激活条件变量清除条件变量3、信号量      使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。      信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。信号量的使用及实例:信号量4、读写锁      读写锁和互斥锁类似,不过读写锁允许更改的并行性。互斥锁要么是加锁状态,要么是不加锁状态。而读写锁可以有三种状态:读模式下的加锁、写模式下的加锁、不加锁 状态。一次只有一个线程可以占有写模式下的读写锁,但是可以有多个线程占有读模式下的读写锁。读写锁的特点:如果有线程读数据,则允许其他线程读数据,但不允许写如果有线程写数据,则不允许其他线程进行读和写5、自旋锁      自旋锁和互斥锁的功能一样,但是互斥锁在线程阻塞时会让出cpu,而自旋锁则不会让出cpu,一直等待,直到得到锁。

自旋锁的存在的问题以及自旋锁的底层实现

互斥锁(Mutex Lock):
底层实现原理

依赖于操作系统提供的原语或者系统调用,如信号量、互斥量等。
可能使用硬件提供的原子操作,但不依赖CAS指令。
工作过程

当一个线程尝试获取锁时,它会使用原语或系统调用尝试获取锁。
如果锁是可用的,线程成功获取锁并进入临界区执行任务。
如果锁已被其他线程持有,当前线程会被阻塞,并被放入锁的等待队列中。
等待队列中的线程在锁释放时被唤醒,并有机会竞争锁。
特点

使用操作系统提供的原语或者系统调用来实现。
可能涉及线程的阻塞和唤醒,导致较高的开销。
适用于长期占用临界资源的情况。
自旋锁(Spin Lock):
底层实现原理

依赖于硬件提供的原子操作,通常使用CAS指令。
工作过程

当一个线程尝试获取锁时,它会循环检查锁的状态。
如果锁是可用的,线程成功获取锁并进入临界区执行任务。
如果锁已被其他线程持有,当前线程会在一个循环中等待,直到获取到锁。
特点

使用硬件提供的原子操作来实现,通常使用CAS指令。
不会涉及线程的阻塞和唤醒,减少了线程切换的开销。
适用于短期占用临界资源的情况。
总结比较:
共同点:
都是用于实现多线程同步的机制,确保临界资源的互斥访问。
都可以通过原子操作来保证线程安全。
不同点:
实现原理不同:互斥锁依赖于操作系统提供的原语或系统调用,而自旋锁依赖于硬件提供的原子操作。
阻塞方式不同:互斥锁会导致线程阻塞和唤醒,而自旋锁会在循环中等待。
适用场景不同:互斥锁适用于长期占用临界资源的情况,而自旋锁适用于短期占用临界资源的情况。
阻塞 vs 自旋等待:
互斥锁在获取锁时会导致线程阻塞,线程会被放入阻塞队列中,并在锁释放时被唤醒。这会引起线程上下文切换的开销。
自旋锁在获取锁时会循环检查锁的状态,直到获取到锁为止,期间线程会一直占用CPU资源,但不会进入阻塞状态,也不会加入到阻塞队列中。
开销:
由于互斥锁涉及到线程的阻塞和唤醒,它的开销相对较高。在高并发场景下,频繁的线程切换会导致性能下降。
自旋锁在获取锁时不会引起线程的阻塞和切换,因此在低竞争和短期占用临界资源的情况下,自旋锁的开销可能更低。

读写锁的特点,底层实现

读写锁允许多个线程同时读取共享资源,但在写入共享资源时只允许一个线程进行 。 它把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。

一堆数据,需要线程同步,如何实现,比较方法的优劣

自己对虚拟内存的理解,把你知道的都说出来!

tcp和udp的区别,要实现一个简单的聊天程序,选那个?

epoll的两种模式的特点

进程和线程的区别(一直问还有没有补充的二)

select和epoll的区别?(epoll内核源码看过,从内核实现角度回答,所以回答的不错)

Linux相关CPU,内存,网络方面相关指令

父子进程fork时,打开的文件的偏移量是否是相同的?

详细说明Linux虚拟地址空间

time_wait的危害,三次握手,四次断开

epoll的ET模式时,如果数据只读了一半,也就是缓冲区的数据
   只读了一点,然后又有新事件来了,怎么办?

Linux内核解决惊群问题

管道为什么是半双工的?

Linux下文件的组织形式?

Linux下有哪些锁机制?信号量的原理和进程间的通信?

三次握手和四次挥手的状态转换,问的很细,timewait,clostwait的特点

tcp/udp协议的区别?

线程和进程的区别

tcp/ip协议的拥塞控制是怎样的
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值