目录
目录
Last-Modified 和 If-Modified-Since
day8 HTTP1.0和HTTP1.1的区别; HTTP2.0与HTTP1.1的区别?; HTTP3.0有了解过吗?; HTTPS和HTTP有哪些区别; HTTPS工作原理
day13 TCP和UDP的区别; TCP连接如何确保可靠性; UDP怎么实现可靠传输; 三次握手的过程,为什么是三次;四次挥手的过程,为什么是四次
day18 HTTP的Keep-Alive是什么? TCP的Keepalive和HTTP的Keep-Alive是一个东西吗?
HTTP Keep-Alive vs TCP Keepalive
day21 Cookie和Session是什么?有什么区别?
5. 多级反馈队列(Multilevel Queue Scheduling)
7. 内存映射文件 (Memory-Mapped File)
day27 解释一下进程同步和互斥,以及如何实现进程同步和互斥
2.1 银行家算法 (Banker’s Algorithm)
1. 互斥锁(Mutex,Mutual Exclusion Lock)
6. 轻量级锁(Futex,Fast Userspace Mutex)
互斥锁(Mutex,Mutual Exclusion Lock)
如何查看某个端口有没有被占用 说- -下select、poll、 epoll 一条SQL查询语句 是如何执行的?
数据库的事务隔离级别有哪些?事务的四大特性有哪些?MySQL的执行引擎有哪些?MySQL为什么使用B+树来作索引
undo log、redo log、binlog 有什么用?什么是慢查询?原因是什么?可以怎么优化?
String、StringBuffer、 StringBuilder的区别
Java的集合类有哪些,那些是线程安全的,那些是线程不安全的 ArrayList和Array有什么区别? ArrayList 和LinkedList的区别是什么?
Java中的HashMap了解吗? HashMap的底层实现是什么? 解决Hash冲突的方法有哪些? HashMap是如何解决hash冲突的
day1-14
day1 TCP/IP模型和OSI模型
TCP/IP(传输控制协议/互联网协议)模型和OSI(开放系统互连)模型是两种用于描述和规范计算机网络通信的架构模型。
OSI模型
OSI模型是由国际标准化组织(ISO)在20世纪80年代后期提出的。它将网络通信过程分为七个层次,每一层各司其职,彼此独立又互相协作。以下是OSI模型的各层及其功能:
- 物理层(Physical Layer): 负责定义物理设备的标准,如电缆类型、电压、电流、数据传输速率等基本通信硬件特性。
- 数据链路层(Data Link Layer): 提供节点间的数据传输和纠错功能,负责帧的传输和接收。以太网和Wi-Fi等协议工作在这一层。
- 网络层(Network Layer): 负责数据包在不同网络之间的路由和转发。IP协议(Internet Protocol)就在此层工作。
- 传输层(Transport Layer): 提供端到端的通信服务,负责确保数据传输的完整性和可靠性。TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)属于这一层。
- 会话层(Session Layer): 管理和维护应用之间的会话(即对话),提供建立、管理和终止会话的功能。
- 表示层(Presentation Layer): 负责数据的格式化和翻译,处理数据加密、解密、压缩等功能。
- 应用层(Application Layer): 提供网络服务,如文件传输、电子邮件、远程登录等。HTTP、FTP、SMTP等协议在这一层工作。
TCP/IP模型
TCP/IP模型是由美国国防部的高级研究计划局(DARPA)在20世纪60年代末到70年代初开发的。它是互联网的基础协议标准。TCP/IP模型较为简化,由四个层次构成:
- 网络接口层(Network Interface Layer): 类似于OSI模型的物理层和数据链路层,负责数据的物理传输和链路层间的通信。
- 互联网层(Internet Layer): 对应OSI模型的网络层,负责路由和寻址。核心协议是IP。
- 传输层(Transport Layer): 与OSI模型的传输层相同,负责端到端的数据传输和通信控制。主要协议是TCP和UDP。
- 应用层(Application Layer): 综合了OSI模型的会话层、表示层和应用层,直接为用户提供应用服务。HTTP、FTP、SMTP等协议在这一层工作。
主要区别
- 层次划分: OSI模型有七层,而TCP/IP模型只有四层。
- 功能定位: OSI模型的每一层都有明确的功能和界限,而TCP/IP模型则更加实用和简化,一些功能结合在一起。
- 发展历史: OSI模型是一种理论性模型,虽广为模式化教学使用,但在实际网络中应用较少;TCP/IP模型则是从实际网络需求出发发展而来的,目前被广泛应用于互联网。
尽管这两个模型存在不同,但它们在网络通信的基本理念和目标上是一致的,都强调了分层管理和模块化设计的原则。
day2 从输入URL到页面展示发生了什么
当你在浏览器中输入一个URL并按下回车键,会触发一系列复杂的过程,最后导致网页的显示。这个过程中包括多个步骤,从域名解析到页面渲染。以下是这个过程的简要描述:
1. URL解析
检查浏览器缓存:浏览器首先会查看其缓存中是否已经存储有与这个URL相关的内容。如果缓存中有适用的副本,并且仍未过期,就直接显示缓存的内容。
2. DNS解析(域名解析)
DNS Lookup:如果内容不在缓存中,浏览器需要将域名(例如 "www.example.com")转换为IP地址。这通常通过查询DNS服务器完成。
- 首先浏览器会检查本地缓存。
- 如果本地缓存没有相应记录,那么会向本地的DNS服务器发起查询。
- 如果本地的DNS服务器也没有记录,它会进行递归查询,直到找到相应的IP地址。
3. 建立TCP连接
TCP Handshake:使用找到的IP地址,浏览器会尝试与目标服务器建立TCP连接。该过程包括三次握手:
- 客户端发送一个SYN包(请求建立连接)。
- 服务器响应一个SYN-ACK包(确认请求)。
- 客户端再发送一个ACK包(确认响应)。
4. TLS/SSL握手(如果使用HTTPS)
TLS Handshake:如果URL以HTTPS开头,还需要进行TLS/SSL握手以建立加密连接。该过程包括:
- 客户端向服务器发送一个ClientHello消息,包含支持的加密套件。
- 服务器响应一个ServerHello消息,选择一个加密套件。
- 双方交换证书并验证。
- 最后,客户端和服务器生成对称密钥,使用该密钥加密后续的数据传输。
5. 发送HTTP请求
HTTP Request:连接建立后,浏览器发送HTTP请求到服务器。这个请求可能是GET、POST或其他HTTP方法,取决于请求的类型。
6. 服务器响应
HTTP Response:服务器处理请求并返回HTTP响应。响应通常包含响应状态码(如200 OK)、响应头部和响应体(包含实际的HTML内容)。
7. 渲染页面
解析HTML:浏览器开始解析HTML文档,并根据标记构建DOM(Document Object Model)树。
处理CSS:紧接着,浏览器会处理CSS文件,生成CSSOM(CSS Object Model)树。
执行JavaScript:浏览器解析和执行任何JavaScript代码。JavaScript可能会修改DOM树或CSSOM树。
构建渲染树:浏览器将DOM树和CSSOM树结合,构建渲染树。渲染树不包含不可见元素(如用"display: none"隐藏的元素)。
布局和绘制:浏览器计算每个元素的几何位置和尺寸,进行布局。然后将页面绘制在屏幕上。
8. 后续资源加载
请求额外资源:HTML文档可能引用了其他资源,如图像、CSS文件、JavaScript文件等。浏览器将继续下载和解析这些资源。
9. 页面呈现
整个过程完成后,用户看到完整的网页。响应时间和呈现速度受到网络条件、服务器性能、浏览器性能等多种因素的影响。
10. 用户交互
在页面加载完成后,用户可以与页面交互,触发事件(如点击、输入)导致新的HTTP请求或者JavaScript执行。
这就是从输入URL到页面展示过程中涉及的主要步骤。每个步骤都有其细节和复杂性,这是创建快速和响应式网络体验的基础。
day3 HTTP请求报文和响应报文是怎样的
HTTP请求报文和响应报文是HTTP协议中使用的两种主要通信机制,它们有不同的结构和内容。
HTTP请求报文
HTTP请求报文由请求行、请求头部字段、空行和请求体(可选)组成。
1. 请求行
请求行由HTTP方法、请求目标(URI)和HTTP版本组成。示例:
复制代码
GET /index.html HTTP/1.1
- HTTP方法:指定要执行的操作,例如GET、POST、PUT、DELETE等。
- 请求目标(URI):指示请求的资源路径。
- HTTP版本:指明所使用的HTTP协议版本,例如HTTP/1.1。
2. 请求头部字段
请求头部字段提供关于客户端环境、所请求的资源等信息,每个字段包括一个字段名和一个字段值。示例:
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
3. 空行
头部字段之后是一个空行(CRLF),指示请求头部的结束。
4. 请求体(可选)
在某些请求方法(如POST、PUT)中,请求体包含需要传输给服务器的数据。例如,表单数据或者JSON对象。
完整示例
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP响应报文
HTTP响应报文由状态行、响应头部字段、空行和响应体组成。
1. 状态行
状态行包含HTTP版本、状态码和状态短语。示例:
HTTP/1.1 200 OK
- HTTP版本:指明所使用的HTTP协议版本。
- 状态码:三位数字,指示响应的结果,例如200(成功)、404(未找到)。
- 状态短语:对状态码进行简短描述,例如OK、Not Found。
2. 响应头部字段
响应头部字段提供关于服务器及所响应资源的额外信息。示例:
Content-Type: text/html; charset=UTF-8
Content-Length: 138
Server: Apache/2.4.1 (Unix)
3. 空行
头部字段之后是一个空行(CRLF),指示响应头部的结束。
4. 响应体
响应体包含实际的资源数据,例如HTML文档、图像或者其他文件。
完整示例
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 138
Server: Apache/2.4.1 (Unix)
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
这是一个简单的HTTP响应报文,返回一个HTML页面。
总结
- HTTP请求报文:由请求行、请求头部字段、空行和请求体(可选)组成。
- HTTP响应报文:由状态行、响应头部字段、空行和响应体组成。
HTTP请求报文和响应报文的格式是固定的,但内容可以非常灵活,取决于具体的请求和响应的资源类型和HTTP方法。
day4 HTTP请求方式有哪些
HTTP请求方式(也称为HTTP方法)定义了客户端和服务器之间进行操作的类型。以下是HTTP协议中常见的请求方式:
1. GET
描述:GET方法用于请求指定资源的表示形式。请求使用数据参数(通常在URL中),不应修改服务器上的数据。
特点:
- 用于请求数据而不是修改数据。
- 参数通常附加在URL中(查询字符串)。
- 安全且幂等。
示例
GET /resource?id=123 HTTP/1.1
Host: www.example.com
2. POST
描述:POST方法用于向服务器提交数据,例如表单提交,经常用于创建新的资源。
特点:
- 数据包含在请求体中。
- 通常用于提交表单和上传文件。
- 不是幂等的(同样的POST请求可能会导致服务器上生成不同的结果)。
示例:
POST /submit-form HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
field1=value1&field2=value2
3. PUT
描述:PUT方法用于向服务器上传指定资源的完整表示。通常用于更新资源。
特点:
- 数据包含在请求体中。
- 幂等(同样的PUT请求多次执行应产生相同结果)。
示例:
PUT /resource/123 HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 52
{
"field1": "new value1",
"field2": "new value2"
}
4. DELETE
描述:DELETE方法用于删除指定的资源。
特点:
- 幂等。
示例:
复制代码
DELETE /resource/123 HTTP/1.1
Host: www.example.com
5. PATCH
描述:PATCH方法用于对资源应用部分修改。类似于PUT但更具局部性。
特点:
- 数据包含在请求体中。
- 不是必然幂等,取决于实现和补丁内容。
示例:
复制代码
PATCH /resource/123 HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 27
{
"field1": "patched value"
}
6. HEAD
描述:HEAD方法与GET方法类似,但服务器只响应请求头,不返回请求体。通常用于获取元信息(如资源的Content-Length)。
特点:
- 用于获取资源头信息。
- 安全且幂等。
示例:
复制代码
HEAD /resource HTTP/1.1
Host: www.example.com
7. OPTIONS
描述:OPTIONS方法用于描述服务器所支持的通信选项(或针对某些资源的)。
特点:
- 用于获取服务器功能信息。
- 安全且幂等。
示例:
复制代码
OPTIONS /resource HTTP/1.1
Host: www.example.com
8. CONNECT
描述:CONNECT方法用于将请求连接转换为透明的TCP/IP通道,通常用于HTTPS请求通过HTTP代理进行隧道连接。
示例:
复制代码
CONNECT www.example.com:443 HTTP/1.1
9. TRACE
描述:TRACE方法用于执行请求消息的回显,主要用于调试目的,用以查看请求在执行过程中是否被修改。
特点:
- 安全且幂等。
- 很少使用,存在潜在的安全风险(如跨站跟踪攻击)。
示例:
复制代码
TRACE /resource HTTP/1.1
Host: www.example.com
每种HTTP请求方式都有其特定的用途,在不同的场景下选择合适的请求方式可以更有效地与服务器进行交互。
day5 GET请求和POST请求的区别
GET请求和POST请求是HTTP协议中最常用的两种请求方式。它们有各自的特点和使用场景。以下是它们的主要区别:
1. 请求参数的传递方式
GET请求:
- 参数通过URL传递,通常附加在URL的查询字符串部分,例如:
复制代码
GET /search?q=example HTTP/1.1 Host: www.example.com
- 因为参数在URL中传递,因此容易被缓存、记录或分享。
POST请求:
- 参数包含在请求体(body)中,而不是在URL中传递,例如:
复制代码
POST /submit-form HTTP/1.1 Host: www.example.com Content-Type: application/x-www-form-urlencoded Content-Length: 27 field1=value1&field2=value2
- 不会直接显示在URL中,参数相对更隐蔽。
2. 数据传输的安全性
GET请求:
- 因为参数暴露在URL中,这些数据可以被浏览器缓存、服务器日志记录、网络监控工具捕捉。
- 不适合传输敏感信息,如密码等。
POST请求:
- 参数在请求体中传输,不会出现在URL中,相对更安全。
- 适合传输包括敏感信息在内的较大数据量。
3. 数据限制
GET请求:
- URL长度有一定限制(不同浏览器和服务器有不同的最大长度限制),因此GET请求适合于传输少量数据。
- URL的长度限制在几千字符左右,具体限制因浏览器和服务器而异。
POST请求:
- 请求体没有明确的大小限制(受服务器配置的限制,如最大上传文件大小),适合传输大量数据或包括文件上传的请求。
4. 缓存
GET请求:
- GET请求的数据非常适合被缓存,因为它们不会对服务器资源产生修改,因此可以提高性能。
- 默认情况下,很多浏览器和中间缓存(如CDN)会缓存GET请求的响应。
POST请求:
- POST请求通常不会被缓存,因为它们主要用于提交数据或修改服务器资源,缓存不符合其使用目的。
5. 幂等性
GET请求:
- 幂等,意思是重复多次相同的GET请求应该产生相同的结果。
- 适用于只读取不修改服务器资源的操作。
POST请求:
- 非幂等,重复相同的POST请求可能会导致不同的结果。例如,多次提交同一个表单可能会创建多个记录。
- 适用于创建或更新服务器资源的操作。
6. 浏览器行为
GET请求:
- 浏览器的默认行为是通过刷新页面重新发起GET请求。
- 表单默认使用GET请求提交。
POST请求:
- 浏览器的默认行为是通过刷新页面提醒用户可能导致表单重复提交的风险。
- 表单需要显式指定method="POST"来使用POST请求提交。
7. 使用场景
GET请求:
- 适用于获取资源,如网页、图像、数据等。
- 非法改变服务器资源的操作,例如浏览网页、获取数据。
POST请求:
- 适用于提交数据到服务器,例如表单提交、文件上传、创建或更新资源等需要向服务器发送数据的操作。
示例
GET 请求:
复制代码
GET /search?q=example HTTP/1.1
Host: www.example.com
POST 请求:
复制代码
POST /submit HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
name=John&age=30
总的来说,GET请求和POST请求各有优缺点,选择合适的请求方式可以提高应用程序的安全性和性能。
day6 HTTP请求中常见的状态码
HTTP状态码是服务器在处理客户端请求时返回的响应码,用于表示请求的结果。状态码由三位数字组成,第一位数字表示状态码的类别,后两位数字表示具体的状态。以下是HTTP请求中常见的状态码:
1xx: 信息响应
这些状态码表示临时响应,并要求请求者继续操作。常见的有:
- 100 Continue:表示到目前为止一切正常,客户端应继续请求或忽略此状态码,如果请求已经完成。
- 101 Switching Protocols:表示服务器根据客户端的请求切换到不同的协议。
2xx: 成功响应
这些状态码表示请求成功被服务器接收、理解并处理。常见的有:
- 200 OK:请求已成功,并返回请求的资源(通常用于GET和POST请求)。
- 201 Created:请求成功并且资源已经被创建(通常用于POST请求)。
- 202 Accepted:请求已接受,但尚未处理(请求可能会被异步处理)。
- 204 No Content:服务器成功处理了请求,但没有返回任何内容(通常用于DELETE请求)。
3xx: 重定向
这些状态码表示客户端需要采取进一步的操作以完成请求。常见的有:
- 301 Moved Permanently:请求的资源已被永久移动到新URL,响应中应提供新的URL。
- 302 Found:请求的资源临时被移动到新URL,客户端应继续使用原有URL。
- 303 See Other:请求已处理,客户端应使用GET方法访问另一个URL获取响应。
- 304 Not Modified:请求的资源未修改,客户端可以使用缓存的版本。
- 307 Temporary Redirect:请求的资源临时被移动到新URL,但客户端应继续使用原有URL(与302不同,307要求请求方法不变)。
- 308 Permanent Redirect:请求的资源已被永久移动到新URL,客户端应使用新的URL(与301不同,308要求请求方法不变)。
4xx: 客户端错误
这些状态码表示客户端看起来可能发生了错误,阻止了服务器的处理。常见的有:
- 400 Bad Request:服务器无法理解请求(如语法错误)。
- 401 Unauthorized:请求需要用户认证(通常应提供身份验证凭据)。
- 403 Forbidden:服务器理解请求,但拒绝执行(用户可能没有权限)。
- 404 Not Found:请求的资源不存在或找不到。
- 405 Method Not Allowed:请求方法不被允许,服务器知道资源不支持这是验方法。
- 408 Request Timeout:服务器等待客户端发送请求的时间超时。
- 409 Conflict:请求与服务器的资源的当前状态发生冲突(例如,资源已经被编辑,造成版本冲突)。
- 410 Gone:请求的资源已被永久删除。
5xx: 服务器错误
这些状态码表示服务器在处理请求时发生了错误。常见的有:
- 500 Internal Server Error:服务器遇到未预料到的状况,无法完成请求。
- 501 Not Implemented:服务器不支持请求的方法。
- 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效响应。
- 503 Service Unavailable:服务器目前无法处理请求,常见原因是服务器过载或正在维护。
- 504 Gateway Timeout:服务器作为网关或代理,无法在规定时间内从上游服务器获得响应。
- 505 HTTP Version Not Supported:服务器不支持请求所使用的HTTP协议版本。
总结
- 1xx 信息响应:指示临时响应,客户端需继续请求。
- 2xx 成功响应:指示请求成功处理。
- 3xx 重定向:指示需要进一步操作完成请求。
- 4xx 客户端错误:指示请求可能含有错误。
- 5xx 服务器错误:指示服务器在处理请求时出错。
了解这些状态码可以帮助你在调试和开发过程中,更好地理解服务器的响应,以及如何改进客户端和服务器之间的交互。
day7 什么是强缓存和协商缓存
在HTTP缓存机制中,强缓存和协商缓存是两种常见的缓存策略,它们帮助浏览器在访问资源时更高效地使用缓存,提高页面加载速度,并减少对服务器的请求。
强缓存 (Strong Cache)
强缓存是指浏览器在规定的缓存时间内可以直接使用缓存资源,而不需要向服务器发送请求验证缓存的有效性。强缓存主要通过HTTP头部的 Cache-Control
和 Expires
字段来实现。
Cache-Control
这是HTTP/1.1规范中定义的缓存控制指令,常见的指令包括:
- max-age=n:指定资源被缓存的最大时间(以秒为单位),例如
max-age=3600
表示资源可以缓存3600秒(1小时)。 - public:指示该响应可以被任何缓存区存储。
- private:指示该响应仅为单个用户缓存,不能作为共享缓存。
- no-cache:强制每次请求直接向服务器验证缓存的有效性。
- no-store:禁用缓存,每次请求都从服务器获取。
示例:
复制代码
Cache-Control: max-age=3600
Expires
这是HTTP/1.0规范中定义的字段,用于指定资源过期的具体时间点。
示例:
复制代码
Expires: Wed, 21 Oct 2023 07:28:00 GMT
强缓存示例
以下是响应头包含了强缓存的例子:
复制代码
HTTP/1.1 200 OK
Date: Wed, 21 Oct 2023 07:28:00 GMT
Cache-Control: max-age=3600
Expires: Wed, 21 Oct 2023 08:28:00 GMT
Content-Type: text/html
Content-Length: 1234
协商缓存 (Negotiated Cache)
协商缓存是指浏览器在使用缓存之前需要向服务器发送请求,通过缓存验证机制确定缓存资源是否仍然有效。如果资源没有改变,服务器会指示浏览器继续使用缓存;如果资源已经改变,服务器会返回新的资源。协商缓存主要通过 Last-Modified
/If-Modified-Since
和 ETag
/If-None-Match
头部字段来实现。
Last-Modified 和 If-Modified-Since
- Last-Modified:服务器在响应中指示资源的最后修改时间。
- If-Modified-Since:浏览器在后续请求中附带该字段,询问服务器指定时间之后资源是否有更新。
示例:
复制代码
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
后续请求中:
复制代码
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
ETag 和 If-None-Match
- ETag:服务器在响应中指示资源的唯一标识符(通常是资源的哈希值)。
- If-None-Match:浏览器在后续请求中附带该字段,询问服务器资源标识符是否有变化。
示例:
复制代码
ETag: "5d8c72a5edda3"
后续请求中:
复制代码
If-None-Match: "5d8c72a5edda3"
协商缓存示例
初次请求响应头:
复制代码
HTTP/1.1 200 OK
Date: Wed, 21 Oct 2023 07:28:00 GMT
ETag: "5d8c72a5edda3"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
Content-Type: text/html
Content-Length: 1234
后续请求的请求头:
复制代码
GET /index.html HTTP/1.1
Host: www.example.com
If-None-Match: "5d8c72a5edda3"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
如果资源未改变,服务器的响应头:
复制代码
HTTP/1.1 304 Not Modified
Date: Wed, 21 Oct 2023 08:00:00 GMT
如果资源已改变,服务器会返回 200 OK
,并携带新的资源数据以及更新的缓存控制头部。
总结
- 强缓存:浏览器在缓存有效期内无需向服务器发送请求直接使用缓存,通过
Cache-Control
和Expires
实现。 - 协商缓存:浏览器每次使用缓存前需向服务器验证缓存的有效性,通过
Last-Modified
/If-Modified-Since
和ETag
/If-None-Match
实现。
两种缓存机制各有优势,强缓存减少了网络请求,提高了页面加载速度,但可能会使用到过期的资源。协商缓存保证资源最新,但需要额外的网络请求。具体使用哪种缓存机制取决于应用场景和对资源新鲜度的要求。
day8 HTTP1.0和HTTP1.1的区别; HTTP2.0与HTTP1.1的区别?; HTTP3.0有了解过吗?; HTTPS和HTTP有哪些区别; HTTPS工作原理
HTTP/1.0 与 HTTP/1.1 的区别
1. 连接的管理
- HTTP/1.0:默认每个请求/响应周期建立一个新的TCP连接。
- HTTP/1.1:引入了持久连接(Persistent Connection),默认在一个连接上可以进行多个请求响应(且使用
Connection: keep-alive
保持连接)。
2. 缓存机制
- HTTP/1.0:使用
Expires
头字段来实现缓存控制。 - HTTP/1.1:引入了更复杂和强大的缓存控制机制,使用
Cache-Control
头字段,可以指定更多缓存策略。
3. 带宽优化及网络连接的使用
- HTTP/1.0:所有的请求都需要包含完整的URL。
- HTTP/1.1:引入了分块传输编码(Chunked Transfer Encoding),允许服务器分块发送响应数据,使得服务器可以在生成动态内容时提前开始发送响应。
4. 错误状态码
- HTTP/1.0:定义了基本的状态码。
- HTTP/1.1:引入了更多的状态码,如 100 Continue, 206 Partial Content 等。
5. 请求头和响应头的增强
- HTTP/1.1:引入了多个新头字段,如
Host
,Allow
,Range
,Transfer-Encoding
等,提高了协议的灵活性和信息传达能力。
6. 主机头
- HTTP/1.0:没有明确要求,缺省支持单一域名。
- HTTP/1.1:引入
Host
头字段,支持多个域名共享同一IP地址(即虚拟主机)。
HTTP/2.0 与 HTTP/1.1 的区别
1. 多路复用
- HTTP/1.1:每个请求/响应对应一个TCP连接,多个请求头会造成HTTP队头阻塞(Head-of-Line Blocking)。
- HTTP/2.0:支持多路复用,多个请求和响应可以在同一个TCP连接上并行进行,提升了传输效率。
2. 二进制分帧
- HTTP/1.1:文本协议,解析和处理相对复杂。
- HTTP/2.0:采用二进制分帧层,所有数据包括头部和数据体都在二进制帧中传输,提高效率和解析速度。
3. 头部压缩
- HTTP/1.1:每次请求都会携带完整的头部信息,可能造成大量重复数据。
- HTTP/2.0:采用HPACK压缩算法,对头部进行压缩,减少带宽占用。
4. 服务器推送
- HTTP/1.1:仅在客户端请求后,服务器才能响应。
- HTTP/2.0:支持服务器推送,可以在客户端请求前,服务器主动推送资源,提高性能。
5. 流量控制
- HTTP/2.0:引入了对单个连接、单个流进行流量控制的机制,可以更精细地调配资源。
HTTP/3.0 有了解过吗?
1. 基于QUIC协议
- HTTP/3.0:在传输层使用基于UDP的QUIC协议,而不是TCP。QUIC提供更快的连接建立速度和更好的数据传输性能。
2. 内置TLS
- HTTP/3.0:具有内置的TLS(版本 1.3),减少额外的握手开销,增强安全性。
3. 改进的多路复用
- HTTP/3.0:进一步改进了HTTP/2中的多路复用,避免了TCP层面上的队头阻塞问题。
4. 更低延迟
- HTTP/3.0:由于基于UDP,具有更快的连接建立和恢复时间,尤其在高延迟和不稳定网络条件下表现更优。
HTTPS 和 HTTP 有哪些区别
1. 传输层的安全性
- HTTP:明文传输,数据容易被窃听和篡改。
- HTTPS:在HTTP的基础上增加了SSL/TLS协议,数据在传输过程中被加密,提供机密性、完整性和身份验证。
2. URL 前缀
- HTTP:URL 以
http://
开头。 - HTTPS:URL 以
https://
开头。
3. 端口号
- HTTP:默认使用端口80。
- HTTPS:默认使用端口443。
4. 性能
- HTTP:由于没有加密和解密开销,理论上比HTTPS快,但实际应用中差别不明显。
- HTTPS:有加密和解密开销,但由于协议优化(如HTTP/2的广泛应用,证书续签等方式),性能接近HTTP,甚至在某些情况下更优。
5. SEO 优势
- HTTPS:搜索引擎(如Google)优先索引HTTPS站点,HTTPS站点在搜索结果中的排名可能更高。
HTTPS 工作原理
HTTPS通过SSL/TLS协议加密HTTP数据传输,以下是其基本工作原理:
1. SSL/TLS 握手过程
-
客户端发起请求:客户端通过
https://
URL 访问服务器,向服务器发送一个ClientHello消息,其中包含客户端支持的加密算法、TLS版本等信息。 -
服务器响应:服务器收到ClientHello后,发送ServerHello消息,选择加密算法、协议版本,并发送服务器的数字证书。同时,服务器还会发送ServerHelloDone消息,表示握手第一阶段完成。
-
验证证书:客户端收到服务器证书后,验证其真实性(通常通过CA证书链验证),验证通过后生成一个随机数,并用服务器的公钥加密该随机数,发送给服务器。
-
生成密钥:服务器使用私钥解密客户端发送的随机数,并与自己的随机数组合生成对称会话密钥。
-
完成握手:双方使用这个对称密钥进行后续的加密通信,握手结束。
2. 数据加密传输
-
客户端发送请求:客户端使用对称会话密钥对HTTP请求数据进行加密,并发送给服务器。
-
服务器响应:服务器使用相同的会话密钥解密请求数据,处理后加密响应数据,并发送给客户端。
-
客户端处理响应:客户端使用会话密钥解密并处理服务器的响应数据。
通过这种机制,HTTPS确保了数据在传输过程中不会被窃听和篡改,同时提供了身份验证功能,确保客户端连接的是可信服务器。
总结:
- HTTP/1.0 与 HTTP/1.1:主要区别在连接管理、缓存机制、新增头部等方面。
- HTTP/2.0:引入了多路复用、二进制分帧、头部压缩以及服务器推送等新特性。
- HTTP/3.0:基于QUIC协议,具有更低延迟和更好的多路复用性能。
- HTTPS和HTTP:HTTPS在HTTP基础上增加了SSL/TLS加密,提供了更好的安全性。
- HTTPS工作原理:通过SSL/TLS协议进行握手、生成对称密钥、加密传输数据。
了解这些差异和工作原理,有助于选择和配置适合的HTTP版本和安全机制,从而提升网站的性能和安全性。
day13 TCP和UDP的区别; TCP连接如何确保可靠性; UDP怎么实现可靠传输; 三次握手的过程,为什么是三次;四次挥手的过程,为什么是四次
TCP和UDP的区别
1. 协议类型和用途
- TCP(传输控制协议):面向连接的协议,提供可靠的、顺序传输的数据流,适用于需要高可靠性的数据传输,如网页、电子邮件和文件传输。
- UDP(用户数据报协议):无连接的协议,不保证数据包的可靠送达和顺序,适用于需要快速传输和低延迟的应用,如视频流、在线游戏和实时语音通信。
2. 连接管理
- TCP:需要三次握手建立连接,四次握手断开连接,有连接状态。
- UDP:不需要建立连接,发送数据报(Datagram)时无需握手,保持无状态。
3. 可靠性
- TCP:提供可靠的数据传输,通过确认(ACK)、重传、顺序控制、流量控制和拥塞控制等机制。
- UDP:不提供可靠的数据传输,不进行重传和确认,没有顺序控制和流量控制。
4. 数据传输单元
- TCP:传输的是字节流,通过TCP分段(segment)进行传输。
- UDP:传输的是数据报(Datagram),每个数据报是一个独立的传输单元。
5. 头部开销
- TCP:头部开销较大,20字节基本头部(不包括选项部分)。
- UDP:头部开销较小,仅8字节。
6. 速度和效率
- TCP:由于需要建立连接、确认、重传等处理,传输速度较慢,效率相对低。
- UDP:头部较小,无需建立连接,传输速度快,效率高,但可能会有数据丢失。
TCP连接如何确保可靠性
TCP通过多个机制来确保数据传输的可靠性:
1. 序列号和确认机制
- 序列号:每个数据段都带有序列号,用于保证数据按顺序传输。
- 确认应答:接收方在接收到数据后,会发送一个确认应答(ACK)返回发送方,表示已经接收到某序列号之前的数据。
2. 超时重传
- 超时:如果发送方在一定时间内没有收到确认应答,就会重传该数据段,以确保数据最终被接收。
- 重传次数:通常有一定的重传次数限制,如果超过一定次数后仍未收到应答,可能会认为连接出错并终止连接。
3. 滑动窗口
- 窗口大小:发送方和接收方通过滑动窗口机制来控制发送的数据量,以便接收方能够来得及处理。
- 流量控制:滑动窗口实现流量控制,防止发送方的数据发送速度超过接收方的处理能力。
4. 拥塞控制
- 拥塞避免:通过算法(如慢启动、拥塞避免、快速重传和快速恢复)来适应网络状况,避免网络拥塞导致的数据丢失。
- 窗口调整:根据网络状况及拥塞情况动态调整窗口大小,提高网络利用效率。
UDP怎么实现可靠传输
虽然UDP本身不提供可靠传输机制,但可以通过以下方法在应用层实现增强的可靠性:
1. 应用层协议
- 确认应答(ACK):应用层实现数据包接收后的确认应答机制。
- 超时重传:应用层根据超时机制重传未收到确认的数据包。
2. 数据包序列号
- 序列号:在数据包中附加序列号,用于检测数据包的顺序及重传情况。
3. 校验和
- 错误检测:在应用层使用更加强大的校验和(如CRC)来检测传输错误,确保数据完整性。
4. 重传策略
- 重传次数限制:设置重传尝试的次数限制,控制重传次数及间隔时间。
- 选择性重复:应用层实现发送仅未确认部分的数据包,类似TCP的选择性确认(SACK)。
5. 数据包大小控制
- 数据分片和重组:在应用层将大数据分成多个小片段发送,并在接收端重组数据,有助于减少网络丢包概率。
三次握手的过程,为什么是三次
三次握手过程
-
第一次握手(SYN):
- 客户端发送一个SYN(同步序列编号)包到服务器,表示请求建立连接,并包含初始序列号SEQ。
- 标志位:SYN=1
-
第二次握手(SYN-ACK):
- 服务器收到SYN包后,回传一个SYN-ACK包给客户端,表示同意建立连接,同时确认收到的序列号,并发送一个自己的初始序列号。
- 标志位:SYN=1, ACK=1,确认号ACK=客户端的SEQ+1
-
第三次握手(ACK):
- 客户端收到SYN-ACK包后,再发送一个确认包(ACK)给服务器,确认双方的序列号和确认号有效,连接建立。
- 标志位:ACK=1,确认号ACK=服务器的SEQ+1
为什么是三次
- 防止重复连接初始化的混乱:三次握手可以确保客户端和服务器双方均已知对方的存在和数据传输能力。尤其是确保服务器知道客户端的请求是最近的(有效的),而不是旧的(无效的)。
- 确认双向通信:通过三次握手确认双方序列号的同步性和传输路径的可靠性,如果任何一方没有收到预期的信号,可以终止连接,防止不稳定连接。
四次挥手的过程,为什么是四次
四次挥手过程
-
第一次挥手(FIN):
- 客户端发送一个FIN(终止连接)包给服务器,表示客户端没有数据再发送,并请求关闭连接。
- 标志位:FIN=1, SEQ=客户端的序列号
-
第二次挥手(ACK):
- 服务器收到FIN包后,回传一个ACK包,表示确认收到客户端的请求,并进入关闭等待状态。
- 标志位:ACK=1,确认号ACK=客户端的SEQ+1
-
第三次挥手(FIN):
- 服务器确认没有数据再发送后,发送一个FIN包给客户端,表示同样没有数据再发送并请求关闭连接。
- 标志位:FIN=1,SEQ=服务器的序列号
-
第四次挥手(ACK):
- 客户端收到FIN包后,发送一个ACK包,确认服务器的关闭请求。此时客户端处于TIME_WAIT状态,保证最后的ACK包能被服务器收到。
- 标志位:ACK=1,确认号ACK=服务器的SEQ+1
为什么是四次
- 确保双向数据传输都已完成:四次挥手保证双方都可以彼此独立地完成数据传输后,再关闭连接。发送FIN后的一方进入半关闭状态,可以继续接收数据,但不能再发送。
- 处理最终确认:最后的ACK确保所有数据都确实已经传输完毕,客户端在TIME_WAIT状态可以处理因网络延迟可能重传的FIN包,确保连接安全关闭。
总结
了解TCP和UDP协议及其机制,以及三次握手和四次挥手的过程和原因,对于理解网络通信原理及设计高效可靠的网络应用至关重要。TCP通过复杂的握手和传输控制机制保证可靠性,而UDP则通过应用层协议来弥补其不可靠性,从而适应不同的应用场景。
day18 HTTP的Keep-Alive是什么? TCP的Keepalive和HTTP的Keep-Alive是一个东西吗?
HTTP的Keep-Alive和TCP的Keepalive机制虽然名称相似,但它们属于不同的层次,并且实现不同的功能。以下是对两者的详细解释:
HTTP的Keep-Alive
概念
HTTP的Keep-Alive(也称为持久连接)是一种用于在同一TCP连接上复用多个HTTP请求和响应的机制。它主要是为了减少连接建立和断开带来的开销,提升性能。
工作原理
- 建立连接:客户端和服务器建立TCP连接后,通过在HTTP请求头中添加
Connection: keep-alive
字段来请求保持连接。 - 保持连接:服务器响应中也包含
Connection: keep-alive
字段,表示同意保持连接。 - 复用连接:在一个连接上,可以连续发送多个HTTP请求和接收响应,无需每次请求都建立新的TCP连接。
- 关闭连接:当其中一方决定不再复用连接时,可以通过发送
Connection: close
字段来通知对方,然后关闭该连接。
配置和限制
- HTTP/1.0:默认情况下不启用Keep-Alive,需要显式指明
Connection: keep-alive
。 - HTTP/1.1:默认启用Keep-Alive,除非指定
Connection: close
。 - 超时时间和最大请求数:为防止连接长时间保持而占用资源,需要设置超时时间和最大请求数。服务器可以通过响应头中的
Keep-Alive
字段指定这些参数,比如Keep-Alive: timeout=5, max=100
,表示连接保持5秒或100次请求。
TCP的Keepalive
概念
TCP的Keepalive是一种用于检测TCP连接是否空闲或失效的心跳机制。它在传输层工作,通过在没有数据传输的期间发送探测包,来确认连接的健康状态。
工作原理
- 空闲检测:如果一个TCP连接在一段时间内(通常默认是7200秒,2小时)没有任何数据传输,TCP层会启动Keepalive探测。
- 发送探测包:如果连接空闲超过了Keepalive空闲时间,TCP层会发送一个Keepalive探测包,包含当前连接的序列号但没有数据负载。
- 探测响应:对方接收到探测包后,如果连接正常,会响应一个确认包(ACK)。如果没有收到响应,TCP层会重试探测包,重试次数通常是3次。
- 断开检测:如果超过重试次数仍未收到响应,TCP会认为连接失效并关闭连接。
配置和限制
- 空闲时间:默认7200秒,可以通过系统参数配置(如Linux系统的
/proc/sys/net/ipv4/tcp_keepalive_time
)。 - 重试次数:通常默认配置为3次,可以通过系统参数配置。
- 重试间隔:探测包之间的间隔时间,与空闲时间一样可以配置。
HTTP Keep-Alive vs TCP Keepalive
相同点
- 两者都与保持连接状态有关。
不同点
-
层次不同:
- HTTP Keep-Alive:工作在应用层(HTTP协议),通过HTTP头部字段实现,在同一TCP连接上复用多个HTTP请求和响应。
- TCP Keepalive:工作在传输层(TCP协议),通过发送心跳探测包来确认连接是否空闲或失效。
-
目的不同:
- HTTP Keep-Alive:旨在减少频繁建立和断开连接的开销,提高HTTP通信效率。
- TCP Keepalive:旨在检测长时间空闲的TCP连接是否仍然有效,以便及时关闭失效的连接。
-
机制不同:
- HTTP Keep-Alive:通过在HTTP头部字段中指定,保持应用层的连接复用。
- TCP Keepalive:通过发送心跳探测包,保证传输层的连接有效性。
示意图
HTTP Keep-Alive:
复制代码
Client Server
| |
|--- TCP Handshake --|
| |
| HTTP Request |
| Connection: keep-alive |
| |
| HTTP Response |
| Connection: keep-alive |
| |
| HTTP Request |
| |
| HTTP Response |
| |
| TCP Connection Close |
| |
TCP Keepalive:
复制代码
Client Server
| |
| ---- (TCP Connection) ----|
| |
| <--- (no data for 2 hrs) ->|
| |
|--- Keepalive Probe --->|
| |
|<---- ACK Keepalive ----|
| |
|--- Keepalive Probe --->|
| |
总结
- HTTP Keep-Alive:应用层的机制,通过复用同一TCP连接来处理多个HTTP请求,提高性能。
- TCP Keepalive:传输层的机制,通过发送探测包来检测空闲连接的有效性,确保连接健在。
虽然两者名字相似,目的都是为了保持连接,但它们在协议层次、工作机制和使用场景上都有显著区别。
day19 DNS查询过程
DNS(Domain Name System)查询过程是将用户输入的域名转换为相应的IP地址的过程,以便浏览器或其他网络应用程序能够与指定的服务器通信。DNS查询过程涉及多个步骤和多个DNS服务器,以下是DNS查询的详细过程:
DNS查询过程
1. 客户端缓存查询
- 本地缓存:浏览器会首先查看本地缓存(DNS缓存)中是否已经存储有对应域名的IP地址。如果找到且该记录未过期,则直接返回结果,无需进行进一步查询。
2. 操作系统缓存查询
- 操作系统缓存:如果浏览器缓存中不存在该域名的记录,操作系统(比如 Windows 的
DNS Client
服务或 Unix 系统中的nscd
)会进一步检查操作系统级别的DNS缓存。
3. 主机文件查询
hosts
文件:操作系统会查找本地的hosts
文件,看该文件是否包含目标域名与其IP地址的映射。如果找到匹配的记录,直接使用该IP地址。
4. DNS递归查询
如果在上述步骤中未能找到匹配的IP地址,系统会发起DNS递归查询。递归查询过程中的主要流程如下:
4.1 向本地DNS服务器查询
- 本地DNS服务器:操作系统将DNS查询请求发送给本地DNS解析器(通常由互联网服务提供商(ISP)提供)。
4.2 本地DNS服务器缓存查询
- 缓存查询:本地DNS解析器首先查询自身缓存是否有对应域名的记录。若有则直接返回结果。
4.3 向根域名服务器查询
如果本地DNS解析器缓存中不包含目标域名的记录,递归查询过程会继续,如下:
-
根域名服务器:本地DNS解析器向根域名服务器发送查询请求。根域名服务器管理互联网的最高层,负责返回顶级域名(如
.com
,.net
,.org
等)对应的域名服务器的地址。根域名服务器的响应:
复制代码
根域名服务器 > 返回顶级域名服务器的地址(如 .com 的域名服务器)
4.4 向顶级域名服务器查询
-
顶级域名服务器:本地DNS解析器向顶级域名服务器(TLD Name Server)发送查询请求,询问目标域名的权威DNS服务器的地址。
顶级域名服务器的响应:
复制代码
顶级域名服务器 > 返回目标域名所在的权威DNS服务器地址
4.5 向权威域名服务器查询
-
权威域名服务器:本地DNS解析器向域名的权威DNS服务器发送查询请求,询问具体的IP地址。
权威DNS服务器的响应:
复制代码
权威DNS服务器 > 返回目标域名的IP地址
5. 返回结果
- 结果返回:本地DNS解析器将查询到的IP地址缓存,并将结果返回给操作系统,操作系统再将结果返回给请求的浏览器或应用程序。
6. 浏览器连接
得到IP地址后,浏览器可以使用该IP地址与目标服务器建立连接,进行后续的HTTP请求。
DNS查询过程示意图
-
客户端缓存查询:
复制代码
Browser Cache | V
-
操作系统缓存查询:
复制代码
OS Cache | V
-
hosts
文件查询:复制代码
/etc/hosts or C:\Windows\System32\Drivers\etc\hosts | V
-
DNS递归查询:
复制代码
Local DNS Resolver (ISP) | V Root DNS Server | V TLD DNS Server (e.g., .com) | V Authoritative DNS Server | V
全流程示例
复制代码
1. 浏览器(客户端)查询本地缓存。
2. 本地缓存无结果,查询操作系统缓存。
3. 操作系统缓存未命中,检查 `hosts` 文件。
4. `hosts` 文件无匹配,向本地DNS解析器发送查询请求。
5. 本地DNS解析器未命中缓存,查询根DNS服务器。
6. 根DNS服务器返回顶级域名服务器地址。
7. 本地DNS解析器查询顶级域名服务器。
8. 顶级域名服务器返回权威域名服务器地址。
9. 本地DNS解析器查询权威域名服务器。
10. 权威域名服务器返回目标域名的IP地址。
11. 本地DNS解析器缓存结果,返回给客户端。
12. 浏览器(客户端)使用IP地址建立连接。
注意事项
- 缓存优先:DNS查询过程中的多级缓存(浏览器、操作系统、本地DNS解析器)能显著提高查询效率,减少网络请求。
- 递归与迭代:本地DNS解析器进行递归查询,而顶级DNS服务器到权威DNS服务器的查询通常是迭代查询。
- TTL(Time to Live):DNS记录的生存时间(TTL)影响缓存持久性。低TTL值会导致更频繁的查询,高TTL值则减少查询频率但可能增加缓存旧记录的风险。
通过以上步骤,DNS查询能够快速且高效地解析域名,获取IP地址,以便浏览器能够建立与目标服务器的连接。这个过程在后台进行,无需用户干预,但对网络访问的性能和可靠性至关重要。
day20 CDN是什么
CDN(Content Delivery Network,内容分发网络)是一种分布式网络架构,用于加速内容交付,提高内容的可用性和可靠性。CDN通过将内容分发到全球多个地理位置的节点(也称为边缘服务器),使得用户可以从离他们最近的服务器获取内容,从而减少延迟和提高访问速度。
CDN的工作原理
CDN通过如下步骤来实现内容的高效分发:
1. 内容缓存和分发
- 缓存内容:网站的静态资源(如HTML、CSS、JavaScript文件,图像,视频等)被复制并存储到CDN网络中的多个边缘节点上。
- 分发内容:当用户请求某一资源时,CDN会将请求重定向到距离用户最近或者最优的边缘节点,以提供最快的响应时间。
2. 智能路由
CDN网络采用智能路由算法来动态选择最佳路径,使数据以最优的路径传输,从而减少传输中的延迟和丢包。
3. 负载均衡
CDN通过负载均衡技术,将用户请求分配到多个分布式的服务器上,避免单个服务器过载,提升系统的可靠性和稳定性。
4. 动态内容加速
虽然CDN主要用于缓存和分发静态内容,但一些先进的CDN网络也提供动态内容加速服务,通过优化传输路径和协议(如TCP加速),加速动态和个性化内容的交付。
CDN的优势
1. 提升性能
- 降低延迟:CDN使内容更接近用户,从而显著降低内容传输的延迟,提高用户体验。
- 减少服务器压力:CDN通过负载均衡和内容缓存,减少源服务器的负载和压力。
2. 提高可用性和可靠性
- 冗余和容错机制:CDN网络中的多个节点提供冗余,任何单点故障都不会影响整个系统的可用性。
- DDoS防护:通过分布式架构和流量分散,CDN可以有效地抵御分布式拒绝服务(DDoS)攻击。
3. 降低带宽成本
通过缓存流量,CDN大大减少了源服务器的直接请求,从而降低了带宽消耗和成本。
4. 全球覆盖
CDN在全球范围内提供内容分发节点,为不同地域的用户提供一致的访问体验。
CDN的组成部分
1. 边缘服务器
位于网络边缘的缓存服务器,用于存储和分发内容。边缘服务器是用户请求的第一响应点。
2. 主服务器(源服务器)
存储网站的原始内容,当CDN节点没有缓存要求的内容时,会向主服务器获取。
3. 分布式网络
许多的边缘服务器通过一个高度可伸缩和分布式的网络连接在一起,使得内容可以迅速地从一个节点分发到另一个节点。
4. 智能DNS解析
CDN服务商利用智能DNS解析技术,将用户请求解析到最近或者最优节点,使得请求能够快速得到响应。
5. 内容管理和监控系统
提供管理和监控功能,能够实时监控内容分发状态、节点健康状态、流量统计和用户行为分析等。
CDN的类型
1. 基于缓存的CDN
专注于静态内容的缓存和分发,如HTML、CSS、JavaScript文件、图像和视频等。适用于大多数典型的内容分发需求。
2. 基于流媒体的CDN
专注于视频点播和直播流媒体的传输优化。采用特定的协议和技术,以确保流媒体的高质量传输和低延迟。
3. 基于安全加速的CDN
除了传统的内容加速外,还提供安全服务,如DDoS防护、WAF(网页应用防火墙)等,保障内容安全。
CDN服务商
一些知名的CDN服务商包括:
- Akamai
- Cloudflare
- Amazon CloudFront
- Google Cloud CDN
- Microsoft Azure CDN
- Fastly
- Alibaba Cloud CDN
- Tencent Cloud CDN
工作流程示例
举个简单的例子来说明CDN工作的具体流程:
- 用户请求:用户通过浏览器请求一个页面上的资源(例如,图片)。
- DNS解析:请求首先会经过DNS解析,CDN的智能DNS解析服务会把请求定位到离用户最近的边缘服务器。
- 缓存查找:边缘服务器查找缓存中是否有该资源:
- 如果有缓存(命中缓存):直接返回资源给用户。
- 如果没有缓存(未命中缓存):边缘服务器向主服务器(源服务器)请求资源,并将资源缓存下来,以备将来使用。
- 响应用户:边缘服务器将资源返回给用户的浏览器,完成请求。
总结
CDN(内容分发网络)通过在全球范围内的分布式网络架构,实现内容的高效分发和加速。通过缓存、智能路由和负载均衡等技术,CDN显著提升了内容交付的速度、可靠性和可用性,成为现代互联网基础设施的重要组成部分。无论是提高网站的访问速度,还是确保流媒体的平滑播放,CDN都扮演着至关重要的角色。
day21 Cookie和Session是什么?有什么区别?
Cookie和Session是两种用于在客户端和服务器之间保存状态和信息的机制,它们在Web开发中经常被使用,用于用户身份验证、会话管理和存储用户偏好等。以下是对Cookie和Session的详细解释及它们之间的主要区别:
Cookie
概念
Cookie是由服务器发送、存储在客户端(通常是浏览器)中的一小段数据。每次客户端向同一服务器发送请求时,都会携带相应的Cookie信息,从而实现状态的保持。
工作原理
- 服务器生成Cookie:当客户端(浏览器)首次请求服务器时,服务器生成一个Cookie,并在HTTP响应头中包含该Cookie。
- 浏览器存储Cookie:浏览器接收到响应后,会将Cookie存储在本地存储中。
- 携带Cookie请求服务器:在后续的请求中,浏览器会在HTTP请求头中包含该Cookie,发送给服务器以保持状态。
属性
- Name:Cookie的名称。
- Value:Cookie的值。
- Domain:Cookie所属的域。
- Path:Cookie生效的路径。
- Expires/Max-Age:Cookie的过期时间,决定了Cookie的有效期限。
- Secure:如果设置了Secure属性,Cookie只能通过HTTPS协议传输。
- HttpOnly:如果设置了HttpOnly属性,Cookie不能通过JavaScript访问,增加安全性。
示例
服务器响应头:
复制代码
Set-Cookie: sessionId=abc123; Max-Age=3600; HttpOnly
客户端请求头:
复制代码
Cookie: sessionId=abc123
Session
概念
Session是一种在服务器端保存用户会话数据的机制。每个用户会话在服务器端有一个唯一的Session ID标识,用于在客户端和服务器之间保持会话状态。
工作原理
- 服务器生成Session:客户端首次请求服务器时,服务器创建一个新的Session,并生成一个唯一的Session ID。
- 服务器返回Session ID:服务器在响应头中返回这个Session ID,通常通过设置一个Cookie(如
Set-Cookie: JSESSIONID=xyz456
)。 - 客户端存储并发送Session ID:客户端存储这个Cookie,并在后续请求中将Session ID发送给服务器。
- 服务器端管理Session数据:服务器通过Session ID来识别用户,并在服务器端存储与该Session相关的数据。
属性
- Session ID:会话的唯一标识符。
- 有效期:服务器上的Session有一个默认的超时时间,超过时间未访问Session将会失效。
- 存储位置:Session数据通常存储在服务器内存、文件系统或数据库中。
示例
服务器响应头:
复制代码
Set-Cookie: JSESSIONID=xyz456; Path=/; HttpOnly
客户端请求头:
复制代码
Cookie: JSESSIONID=xyz456
Cookie和Session的区别
1. 存储位置
- Cookie:数据存储在客户端(浏览器)本地。
- Session:数据存储在服务器端,客户端仅存储一个Session ID。
2. 安全性
- Cookie:由于数据存储在客户端,容易受到XSS(跨站脚本)攻击和篡改。如果未设置HttpOnly属性,JavaScript可以访问并操纵Cookie。
- Session:数据存储在服务器,更加安全。客户端只有一个Session ID,不存储实际会话数据。
3. 数据量限制
- Cookie:每个Cookie的大小通常限制在4KB以下,不适合存储大量数据。
- Session:服务器端没有大小限制,可以存储大量会话数据(受服务器资源限制)。
4. 生命周期
- Cookie:通过Expires或Max-Age属性设置过期时间,可以是持久性(存储在磁盘)或会话性(浏览器关闭即失效)。
- Session:通常会话期(会话结束或者过期时间到达即失效)。默认情况下,Session在用户关闭浏览器或超过服务器的超时时间后会失效。
5. 负载
- Cookie:在每次请求中,所有相关的Cookie都会被发送到服务器,增加请求负载。
- Session:每次请求只需发送Session ID,减少数据传输量,服务器承担了状态管理的负担。
6. 使用场景
- Cookie:适用于需要在客户端存储小型、非敏感数据,便于跨请求乃至跨会话使用的数据,如用户偏好设置、跟踪标识、选项等。
- Session:适用于存储需要保护和维持的用户数据,如登录状态、用户信息、购物车内容等。
总结
- Cookie:一种客户端存储的小数据,用于保持状态和存储少量信息,存在于客户端并随每个请求发送到服务器。
- Session:一种服务器端的会话管理机制,数据存储在服务器,通过Session ID来识别和管理用户的会话状态。
Cookie和Session各自有其优势和适用场景。在实际应用中,可以结合使用它们,既利用Cookie的客户端存储优势,又确保Session的安全性和数据管理能力。
day22 进程和线程的区别
进程和线程是现代操作系统中管理和执行程序的基本单元,两者在操作系统中的角色和作用有所不同。以下是进程和线程的详细解释及它们之间的主要区别:
进程(Process)
概念
进程是一个具有独立资源和执行上下文的运行中的程序实例。每个进程都有自己的内存空间和系统资源。
主要特征
- 独立性:进程是操作系统分配资源的基本单位,具有独立的地址空间(包括文本段、数据段、堆和栈)。
- 资源分配:每个进程都有自己独立的系统资源,如文件描述符、网络连接、内存空间等。
- 安全性和稳定性:由于进程拥有独立的地址空间,一个进程崩溃不会影响其他进程,提高了系统的可靠性。
创建与销毁
-
创建:进程由操作系统的调度程序创建,一般通过系统调用(如fork、exec等)实现。
-
销毁:进程通过正常退出或被操作系统强制终止而销毁,操作系统会回收其占用的资源。
示例
在Unix/Linux系统中,创建一个进程的示例(用C语言):
c
复制代码
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("I am the child process.\n");
} else {
// 父进程
printf("I am the parent process. Child PID: %d\n", pid);
}
return 0;
}
线程(Thread)
概念
线程是进程中的一个执行单元,它是CPU调度和分配的基本单位。一个进程可以包含多个线程,线程共享进程的系统资源,但有独立的执行上下文。
主要特征
- 共享资源:线程共享同一进程的地址空间和系统资源,如全局变量和文件描述符等。
- 轻量级:线程比进程更轻量级,创建和销毁开销较小,切换速度更快。
- 并发执行:在多核系统上,不同线程可以真正并发执行,利用多核CPU提高性能。
创建与销毁
-
创建:线程由进程的主线程或其他线程创建,通过线程库(如POSIX线程、Java线程等)实现。
-
销毁:线程完成其任务后会正常退出,或被其他线程终止,操作系统会回收其使用的部分资源。
示例
在Unix/Linux系统中,创建一个线程的示例(用C和POSIX线程库):
c
复制代码
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
printf("I am the new thread.\n");
return NULL;
}
int main() {
pthread_t thread_id;
pthread_create(&thread_id, NULL, thread_function, NULL);
printf("I am the main thread.\n");
pthread_join(thread_id, NULL); // 等待线程完成
return 0;
}
进程与线程的主要区别
1. 内存和资源分配
- 进程:拥有独立的内存空间和系统资源,每个进程有自己的地址空间。
- 线程:共享进程的内存空间和系统资源,多个线程在同一地址空间内运行。
2. 创建和销毁
- 进程:创建和销毁的开销较大,需要分配和回收完整的进程资源。
- 线程:创建和销毁的开销较小,只需分配和回收线程上下文。
3. 运行和切换
- 进程:进程间的切换(上下文切换)开销较大,需要切换内存地址空间等。
- 线程:线程间的切换开销较小,只需保存和恢复线程上下文(如寄存器状态)。
4. 通信和同步
- 进程:进程通过进程间通信(IPC)机制(如信号、管道、消息队列、共享内存等)进行通信和同步。
- 线程:线程通过共享内存进行通信和同步,使用锁(如互斥锁、读写锁)、信号量等同步机制。
5. 调度
- 进程:由操作系统内核的调度程序进行调度,通常使用优先级或时间片轮转等算法。
- 线程:可以由操作系统调度(内核线程)或用户级线程库调度(用户线程),在内核线程的情况下也受操作系统调度器管理。
6. 并发和并行
- 进程:可以实现并发,但多个进程间通信和共享数据较复杂,一般用于相互独立或大规模并行计算任务。
- 线程:更适合需要频繁通信和共享数据的任务,可以通过多线程实现真正的并行处理,提高多核CPU的利用率。
总结
- 进程:是一种独立的资源分配和管理单元,具有独立的内存空间和资源,创建和切换开销较大,适用于相互独立的并发任务。
- 线程:是进程内的执行单元,多个线程共享进程的内存空间和资源,创建和切换开销较小,适用于需要共享数据和频繁通信的并行任务。
进程和线程各自有其适用场景和优势,了解它们的区别和特点,可以选择合适的机制来实现高效的并发和并行计算。
day23 并行和并发有什么区别
并行(Parallelism)和并发(Concurrency)是计算机科学中常用的术语,尤其在多任务处理和多线程编程领域,它们描述了不同类型的任务执行方式。尽管这两个概念有时在日常使用中被混淆,但它们有明确的区别。
并行(Parallelism)
概念
并行是指同时执行多个任务,这些任务在同一时间点上确实同时运行。并行通常依赖于多核处理器或多处理器系统,其中每个任务被分配到一个独立的处理单元(如CPU核心)上。
特点
- 同时执行:多个任务在同一时间内被真正地并行执行。
- 硬件支持:需要多个处理单元(多核或多处理器)来同时处理多个任务。
- 任务独立:适用于任务之间相对独立,互不依赖的情况,比如大规模数据处理或科学计算。
示例
-
多核处理器上的并行计算:在一个四核CPU系统上,四个独立的任务(比如矩阵运算、图像处理等)可以在同一时间分别在四个核心上运行。
复制代码
Core 1: Task A Core 2: Task B Core 3: Task C Core 4: Task D
应用场景
- 科学计算和数据分析中的大规模并行算法。
- 多媒体处理(如视频编码/解码)。
- Web服务器处理多个独立的请求。
并发(Concurrency)
概念
并发是指在同一时间段内管理和处理多个任务,这些任务之间的执行是交替的(时间片),而不是同时的。并发系统通过将多个任务分成细小的子任务,然后交替地执行这些子任务,使得所有任务在宏观上似乎是同时进行的。
特点
- 交替执行:通过时间切片(time slicing)技术,使得多个任务在同一时间段内交替运行,这些任务在微观上是分时进行的。
- 任务切换:依靠操作系统的调度算法来快速切换任务,以使程序表面上感觉到“同时”运行。
- 资源共享:多个任务共享同一套计算资源(如CPU、内存)。
示例
-
单核CPU上的多线程:在一个单核CPU系统上,操作系统通过时间片轮转(time slicing)的方法调度多个线程,使得每个线程在微观时间片内运行一小段时间,然后切换到下一个线程。
复制代码
Time Slice 1: Task A Time Slice 2: Task B Time Slice 3: Task A Time Slice 4: Task B
应用场景
- 多用户操作系统中多个应用程序的执行。
- 图形用户界面(GUI)应用程序中处理用户事件(如按钮点击、窗口移动)。
- Web服务器处理多个请求,但在单个线程上实现I/O操作的并发。
并行与并发的区别
1. 执行方式
- 并行:真正的同时执行多个任务。
- 并发:交替地执行多个任务,通过快速切换,使任务看起来似乎是同时进行的。
2. 硬件依赖
- 并行:需要多核处理器或多处理器系统来实现任务的同时执行。
- 并发:通过时间片调度和快速任务切换,在单核处理器上也可以实现。
3. 任务独立性
- 并行:适合独立的任务,彼此之间没有太多依赖。
- 并发:适合相互依赖或需要频繁交互的任务。
4. 复杂性
- 并行:涉及复杂的并行计算模型和数据划分,多个任务之间通常没有太多同步要求。
- 并发:涉及任务之间的同步和资源共享问题,处理竞争条件、死锁、和线程安全等问题相对复杂。
示例对比
并行示例
在多核处理器上进行矩阵相乘:
python
复制代码
# Pseudo-code for parallel matrix multiplication
matrix1 = [[...], [...], [...]]
matrix2 = [[...], [...], [...]]
def multiply_row_by_column(row, column):
return sum(x * y for x, y in zip(row, column))
# Distribute the computation across multiple cores
parallel for each row in matrix1 and each column in matrix2:
result = multiply_row_by_column(row, column)
并发示例
在单核处理器上下载多张图片:
python
复制代码
# Pseudo-code for concurrent image downloading
def download_image(url):
# simulate downloading an image
pass
image_urls = ["url1", "url2", "url3", ...]
threads = []
for url in image_urls:
thread = create_thread(target=download_image, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
在并发示例中,多个下载任务在单个核上交替进行,通过线程实现所谓的“并发”处理。
总结
- 并行(Parallelism):涉及多个任务同时运行,必要条件是多核或多处理器硬件支持,适合独立的、大规模数据处理任务。
- 并发(Concurrency):涉及多个任务交替运行,在一个时间段内管理和处理多个任务,适合需要交互或共享资源的任务。
理解并行和并发的区别,可以更好地设计和优化程序,根据具体的应用场景选择适当的并发或并行模型,实现高效的数据处理和任务执行。
day24 解释一下用户态和核心态
用户态(User Mode)和核心态(Kernel Mode)是操作系统中运行程序的两种最基本的执行模式,它们通过不同的权限级别来控制程序对系统资源的访问。这种机制是为了保护操作系统内核和关键资源免受用户程序的直接干扰和潜在的破坏。
核心态(Kernel Mode)
概念
核心态,也称为内核态或特权态,是运行操作系统内核及其核心组件的特权模式。在这种模式下,代码可以执行任何CPU指令并访问所有的计算机资源,包括物理硬件(如内存、I/O设备)和系统关键数据结构。
主要特征
- 高级权限:核心态下运行的代码具有访问硬件和系统资源的最高权限,可以执行所有CPU指令集,包括特权指令。
- 内存访问:可以直接访问所有的内存地址,包括内核空间和用户空间的内存地址。
- 系统调用处理:处理来自用户态的系统调用请求,执行操作系统的核心功能,如进程管理、内存管理、文件系统操作等。
- 硬件交互:可以与硬件设备直接交互,执行输入/输出操作。
示例
在操作系统启动时,内核代码运行在核心态中。这包括但不限于内存管理、进程调度、中断处理和设备驱动程序等核心功能。
用户态(User Mode)
概念
用户态是运行用户应用程序的受限模式。在这种模式下,程序只能执行特定的非特权CPU指令,并且只能访问受限制的内存地址和系统资源。用户态的首要目的是保护操作系统核心态及其他用户程序的稳定性和安全性,防止用户程序直接访问或修改系统关键资源。
主要特征
- 受限权限:用户态代码不能执行特权指令,受限于特定的CPU指令集。
- 内存访问受限:只能访问用户空间的内存地址,而不能访问内核空间的内存地址。
- 系统调用:当用户态程序需要执行特权操作(如文件读写、进程管理)时,必须通过系统调用接口请求核心态内核来完成。
- 隔离性:不同用户态进程之间的内存空间是隔离的,防止相互干扰。
示例
用户态运行的典型程序包括文本编辑器、网络浏览器、办公软件等。用户态程序的所有特权操作都必须通过系统调用与内核互动。
用户态和核心态的转换
由于用户态程序不能直接访问内核资源,系统提供了一种机制,允许用户态程序通过系统调用来请求内核服务。这会导致一种状态转换:从用户态切换到核心态,处理完请求后,再从核心态切换回用户态。
工作流程
- 用户态:用户程序运行在用户态,并通过系统调用请求内核服务(如文件访问、网络操作)。
- 系统调用:内核捕获系统调用并将程序状态从用户态切换到核心态。
- 核心态:内核代码在核心态下运行,执行所请求的系统操作。
- 返回用户态:内核执行完请求后,将结果返回给用户程序,并将状态切换回用户态。
示例
以文件读取为例,用户态和核心态之间的转换过程如下:
用户程序(用户态):
c
复制代码
int fd = open("file.txt", O_RDONLY);
char buffer[100];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
close(fd);
系统调用过程:
- 用户程序调用:用户程序调用
read
系统调用。 - 陷入内核:系统调用触发中断或异常,将控制权交给内核,切换到核心态。
- 内核处理:内核执行
read
操作,从文件中读取数据到内存缓冲区。 - 返回用户态:操作完成后,内核将数据返回给用户程序,并切换回用户态。
用户态和核心态的区别
1. 权限级别
- 用户态:受限权限,不能执行特权指令或访问关键系统资源。
- 核心态:高级权限,可以执行所有指令并访问所有资源。
2. 内存访问
- 用户态:只能访问有限的用户空间内存,不能直接访问内核空间。
- 核心态:可以访问所有内存,包括用户空间和内核空间。
3. 操作类型
- 用户态:用于运行应用程序,进行普通计算和逻辑操作。
- 核心态:用于执行操作系统核心功能,如进程管理、内存管理、设备驱动和系统调用等。
4. 稳定性和安全性
- 用户态:保护用户程序和系统的稳定性,防止恶意或错误代码影响系统关键资源。
- 核心态:关键系统代码运行在核心态,以确保操作系统的稳定运行和系统资源的正确管理。
总结
用户态和核心态是现代操作系统中至关重要的概念,它们通过不同的权限级别来确保系统资源的安全和稳定性。用户态用于运行普通应用程序,具有受限权限;核心态用于运行操作系统内核及其关键组件,具有最高权限。两者之间通过系统调用进行状态转换,从而实现用户程序与系统资源的安全交互。了解这些概念对于理解操作系统的工作原理和编写高效、安全的程序至关重要。
day25 进程调度算法你了解多少
进程调度算法是操作系统中的重要组成部分,它决定了多个进程在CPU上的执行顺序和时间分配,从而影响系统的效率和响应时间。不同的调度算法适用于不同的应用场景,满足不同的性能需求。以下是一些常见的进程调度算法及其特点和适用场景:
1. 先来先服务(FCFS)
特点
- 顺序调度:按进程到达的顺序调度,先到达的进程先获得CPU。
- 非抢占式:一旦进程获得CPU,它将一直运行到完成或主动放弃。
优点
- 简单易实现。
- 对长时间运行任务有较好的表现。
缺点
- 不适合短作业和实时系统。
- 可能导致“短作业长等待”问题,即短时间的进程可能要等待很长时间。
适用场景
- 批处理系统:较少时间限制要求的任务处理。
2. 最短作业优先(SJF)
特点
- 选择最短作业:优先调度执行时间最短的进程。
- 非抢占式或抢占式:可以是非抢占的,也可以是抢占的(Shortest Remaining Time First, SRTF)。
优点
- 在理想情况下,可以降低平均等待时间。
- 提高系统吞吐率。
缺点
- 难以准确预测每个进程的执行时间。
- 可能导致“饥饿”问题,长作业可能一直得不到调度。
适用场景
- 适合对任务执行时间有良好预估的系统。
3. 时间片轮转(RR)
特点
- 轮转调度:每个进程按顺序轮流获得CPU执行的机会。
- 时间片限制:每个进程在一个固定的时间片内运行,时间片结束强制切换。
优点
- 简单公平。
- 响应时间较快。
缺点
- 时间片设置不合理时会影响性能:时间片太大退化为FCFS,时间片太小会增加上下文切换开销。
适用场景
- 时间共享系统:如交互式系统,用户响应时间要求高。
4. 优先级调度(Priority Scheduling)
特点
- 优先级调度:每个进程分配一个优先级,优先级高的进程先调度。
- 可以是抢占式或非抢占式。
优点
- 灵活性高,可以根据进程的重要性或紧急程度进行调度。
缺点
- 可能导致“优先级逆转”或“饥饿”问题,即低优先级进程可能一直得不到调度。
适用场景
- 实时系统:对优先级要求较高的应用场景。
5. 多级反馈队列(Multilevel Queue Scheduling)
特点
- 多个队列:根据不同优先级分成多个队列,不同队列有不同调度策略。
- 队列迁移:进程可以在不同队列间移动,根据其行为调整优先级。
优点
- 结合了多种调度算法的优点。
- 灵活调度策略,适应多样化任务需求。
缺点
- 复杂性较高,需要参数调优。
适用场景
- 综合性系统:如需要同时处理交互任务和批处理任务的系统。
6. 最短剩余时间优先(SRTF)
特点
- 剩余时间最短:优先调度剩余执行时间最短的进程,是SJF的抢占式版本。
优点
- 有效降低平均等待时间。
缺点
- 计算复杂度高。
- 难以预测进程的剩余时间。
适用场景
- 对执行时间有较好预测的系统。
7. 最高响应比优先(HRRN)
特点
- 响应比:计算每个进程的响应比,优先响应比最高的进程。
- 响应比公式:
响应比 = (等待时间 + 服务时间) / 服务时间
优点
- 兼顾等待时间和执行时间,较好平衡长短作业。
- 不会产生饥饿现象。
缺点
- 实现复杂度较高。
适用场景
- 需要平衡长短作业的系统。
8. 最短求值站点优先(STCF)
特点
- 站点调度:优先调度计算站点最低的进程,类似SJF但适用于分布式计算或云计算环境。
优点
- 合理调度分布式环境中的计算资源。
缺点
- 需要考虑网络延迟和计算均衡性。
适用场景
- 分布式系统或云计算平台。
示例对比
示例进程
假设有以下进程到达系统,分别带有不同的到达时间和执行时间:
复制代码
进程 | 到达时间 | 执行时间
P1 | 0 | 8
P2 | 1 | 4
P3 | 2 | 9
P4 | 3 | 5
调度示例
-
FCFS:按到达时间顺序调度:
复制代码
时间: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 进程: P1| P1| P1| P1| P1| P1| P1| P1| P2| P2| P2| P2| P3| P3| P3| P3| P3| P3| P3| P3| P3| P4| P4| P4| P4| P4
-
SJF:优先调度执行时间最短的进程:
复制代码
时间: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 进程: P1| P1| P1| P1| P1| P1| P1| P1| P2| P2| P2| P2| P4| P4| P4| P4| P4| P3| P3| P3| P3| P3| P3| P3| P3| P3| P3| P3
-
RR:时间片为2:
复制代码
时间: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 进程: P1| P1| P2| P2| P3| P3| P4| P4| P1| P1| P2| P2| P3| P3| P4| P4| P1| P1| P2| P2| P3| P3| P4| P4| P1| P1| P3| P3| P4| P4| P3| P3
总结
进程调度算法是操作系统中至关重要的组成部分,不同的算法满足不同的应用需求和性能目标:
- FCFS:简单但可能导致长等待时间。
- SJF/SRTF:优化平均等待时间但难以预估作业时间。
- RR:适合时间共享系统,响应时间较快。
- 优先级调度/HPRN:灵活但需处理优先级相关问题。
- 多级反馈队列:综合多种策略的优点,适应性强但实现复杂。
理解和应用这些调度算法,可以更好地优化系统性能和用户体验。
day26 进程间有哪些通信方式
进程间通信(IPC,Inter-Process Communication)是操作系统允许不同进程之间进行数据交换和协调的机制。这在多任务操作系统中尤其重要,可以提高应用程序的并行处理能力和资源利用率。不同的操作系统和编程环境提供了多种IPC方式,下面是一些常见的进程间通信方式及其特点:
1. 管道 (Pipe)
无名管道 (Anonymous Pipe)
概念
无名管道是单向数据通道,用于连接两个亲缘进程(通常是父子进程),使得一个进程的输出可以作为另一个进程的输入。
特点
- 单向通信:数据只能单向传输。
- 亲缘关系:只能在有亲缘关系的进程间使用。
- 面向字节流:以字节流的形式传递数据。
示例
c
复制代码
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buffer[20];
pid_t pid;
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == 0) { // 子进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
printf("Child process read: %s\n", buffer);
} else { // 父进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello, world!", 14);
}
return 0;
}
有名管道 (Named Pipe 或 FIFO)
概念
有名管道(FIFO,First In First Out)类似于无名管道,但它具有文件路径,可以在任何不相关的进程间通信。
特点
- 双向通信:数据可以双向传输。
- 不要求亲缘关系:在任何进程间使用,通过命名文件进行通信。
- 持久性:在文件系统中持久存在。
示例
c
复制代码
// 创建有名管道
mkfifo("/tmp/myfifo", 0666);
// 进程A
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello", 6);
close(fd);
// 进程B
int fd = open("/tmp/myfifo", O_RDONLY);
char buffer[6];
read(fd, buffer, 6);
printf("Read from FIFO: %s\n", buffer);
close(fd);
2. 消息队列 (Message Queue)
概念
消息队列是一种进程通信机制,允许多个进程通过发送和接收消息来进行数据交换。它们通过一个消息队列标识符进行引用。
特点
- 双向通信:允许发送和接收消息。
- 灵活性高:可以指定消息优先级。
- 持久性:消息持久化,直到被接收。
示例
c
复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msg_buffer {
long msg_type;
char msg_text[100];
};
int main() {
key_t key;
int msgid;
key = ftok("progfile", 65);
msgid = msgget(key, 0666 | IPC_CREAT);
struct msg_buffer message;
message.msg_type = 1;
printf("Write data: ");
fgets(message.msg_text, sizeof(message.msg_text), stdin);
msgsnd(msgid, &message, sizeof(message), 0);
printf("Data sent is: %s \n", message.msg_text);
return 0;
}
3. 共享内存 (Shared Memory)
概念
共享内存允许多个进程共享同一块内存,以进行高效的数据交换。这是最快的一种IPC方式,因为数据不需要在进程间拷贝。
特点
- 高效快速:通过共享内存进行直接访问,速度快。
- 同步机制:需要使用信号量(Semaphore)或互斥锁(Mutex)来解决同步和互斥问题,防止竞态条件。
示例
c
复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {
key_t key = ftok("shmfile", 65);
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
char *str = (char*) shmat(shmid, (void*)0, 0);
printf("Write data: ");
fgets(str, 1024, stdin);
printf("Data written in memory: %s\n", str);
shmdt(str);
return 0;
}
4. 信号量 (Semaphore)
概念
信号量是一种计数器式的同步机制,用于解决进程间的互斥问题。它是一个用于管理资源的计数器,涉及两个操作:P(等待,wait)和V(信号,signal)。
特点
- 同步机制:主要用于实现进程同步和互斥。
- 方便资源管理:用于控制对共享资源的访问。
示例
c
复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main() {
key_t key = ftok("semfile", 65);
int semid = semget(key, 1, 0666 | IPC_CREAT);
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = -1; // P操作
sem_op.sem_flg = 0;
semop(semid, &sem_op, 1);
printf("In Critical Section\n");
sem_op.sem_op = 1; // V操作
semop(semid, &sem_op, 1);
return 0;
}
5. 套接字 (Socket)
概念
套接字是一种广泛使用的网络通信机制,既可以用于本地进程间通信,也可以用于远程进程间通信。常用的套接字类型有TCP(面向连接的)和UDP(无连接的)。
特点
- 灵活性高:适用于本地和远程进程间通信。
- 支持多种协议:如TCP、UDP等。
- 复杂性较高:需要处理网络编程的复杂性。
示例(本地套接字通信)
c
复制代码
// 服务器端
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/unix_socket");
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
int clientfd = accept(sockfd, NULL, NULL);
char buf[100];
read(clientfd, buf, 100);
printf("Received: %s\n", buf);
close(clientfd);
close(sockfd);
return 0;
}
// 客户端
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/unix_socket");
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
write(sockfd, "Hello, socket!", 15);
close(sockfd);
return 0;
}
6. 信号 (Signal)
概念
信号是一种用于通知进程发生某些事件的异步通信机制。信号可以是来自操作系统的通知,也可以是进程之间发送的通知。
特点
- 异步通信:可以异步地通知进程发生了某种事件。
- 轻量级:不适合传输大量数据,只适用于通知和简单的控制。
示例
c
复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signal_num) {
printf("Received signal %d\n", signal_num);
}
int main() {
signal(SIGUSR1, signal_handler);
pid_t pid = fork();
if (pid == 0) { // 子进程
sleep(1); // 等待父进程设置好信号处理器
kill(getppid(), SIGUSR1);
} else { // 父进程
pause(); // 等待信号
}
return 0;
}
7. 内存映射文件 (Memory-Mapped File)
概念
内存映射文件允许多个进程将同一文件映射到各自的地址空间,从而共享该文件的数据。它结合了文件系统和共享内存的优点,用于高效的数据交换。
特点
- 高效:利用操作系统的内存管理机制,提高数据访问效率。
- 持久性:因为基于文件系统,数据可持久存在。
- 同步机制:需要同步和互斥控制,防止并发访问冲突。
示例
c
复制代码
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("shared_file.txt", O_RDWR | O_CREAT, 0666);
ftruncate(fd, 1024);
char *map = (char*)mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fork() == 0) { // 子进程
strncpy(map, "Hello from child!", 1024);
munmap(map, 1024);
} else { // 父进程
sleep(1); // 等待子进程写入
printf("Read from mapped memory: %s\n", map);
munmap(map, 1024);
}
close(fd);
return 0;
}
总结
不同的IPC机制各有优劣,适合不同的应用场景:
- 管道(无名管道、有名管道):简单易用,适合短小数据的单向通信。
- 消息队列:适合需要优先级和消息持久化的通信。
- 共享内存:速度最快,适合大数据量的交互,需注意同步机制。
- 信号量:主要用于同步和互斥控制,简单高效。
- 套接字:灵活性高,适合本地和网络通信,复杂度较高。
- 信号:用于异步通知,不适合大数据量的传输。
- 内存映射文件:结合了文件和内存的优点,适合高效的数据共享。
根据具体的需求和应用场景,选择合适的IPC机制,可以有效提高系统的并发处理能力和效率。
day27 解释一下进程同步和互斥,以及如何实现进程同步和互斥
进程同步和互斥是用于协调多个进程或线程之间的访问共享资源和执行顺序的机制。在多任务操作系统中,正确的进程同步和互斥机制是保证数据一致性和系统稳定性的关键。
进程同步与互斥
进程同步
- 概念:进程同步是指协调多个进程的执行顺序,以确保它们按照某种预期的顺序执行,用于解决多个进程之间的依赖关系问题。
- 目标:保证多个进程在访问共享数据或资源时按照确定的顺序执行。
进程互斥
- 概念:进程互斥是指确保在任何时候只允许一个进程进入临界区(Critical Section),从而防止多个进程同时访问共享资源导致冲突。
- 目标:防止多个进程同时执行临界区代码,从而保证共享资源的正确性和一致性。
实现进程同步和互斥的方法
1. 信号量(Semaphore)
概念
信号量是一种同步机制,由Dijkstra发明,是一个用于控制访问共享资源的计数器。信号量有两种基本操作:P操作(等待,wait)和V操作(信号,signal)。
使用方法
- 互斥信号量:初值为1,用于控制对共享资源的互斥访问。
- 计数信号量:初值为可用资源的数量,用于控制对多个资源的访问。
示例
以下示例展示了如何使用信号量实现互斥:
c
复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t mutex;
void* thread_function(void* arg) {
sem_wait(&mutex); // P操作,等待信号量
printf("Thread %d is in critical section\n", *(int*)arg);
// 访问临界区
sem_post(&mutex); // V操作,释放信号量
return NULL;
}
int main() {
pthread_t threads[3];
int thread_ids[3];
sem_init(&mutex, 0, 1); // 初始化信号量,初值为1
for (int i = 0; i < 3; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&mutex); // 销毁信号量
return 0;
}
2. 互斥锁(Mutex)
概念
互斥锁(Mutex,Mutual Exclusion)是一种用于确保只有一个进程或线程能进入临界区的锁。它通常用于保护共享数据结构,防止并发访问引起的数据不一致问题。
使用方法
- 加锁(Lock):进入临界区之前获取锁,确保其他线程无法进入。
- 解锁(Unlock):离开临界区时释放锁,允许其他线程进入。
示例
以下示例展示了如何使用互斥锁实现互斥:
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
printf("Thread %d is in critical section\n", *(int*)arg);
// 访问临界区
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t threads[3];
int thread_ids[3];
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
for (int i = 0; i < 3; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
3. 条件变量(Condition Variable)
概念
条件变量是一种高级的同步机制,允许线程在条件不满足时等待,直到被其他线程唤醒。它通常与互斥锁一起使用,实现更复杂的线程同步。
使用方法
- 等待(Wait):进入等待状态,直到条件满足。
- 通知(Signal):通知等待的线程条件已满足。
示例
以下示例展示了如何使用条件变量实现同步:
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
pthread_cond_wait(&cond, &mutex); // 等待条件变量
printf("Thread %d is proceeding\n", *(int*)arg);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t threads[3];
int thread_ids[3];
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_cond_init(&cond, NULL); // 初始化条件变量
for (int i = 0; i < 3; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}
printf("Main thread is sleeping for 2 seconds\n");
sleep(2);
pthread_mutex_lock(&mutex); // 加锁
pthread_cond_broadcast(&cond); // 唤醒所有等待线程
pthread_mutex_unlock(&mutex); // 解锁
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
pthread_cond_destroy(&cond); // 销毁条件变量
return 0;
}
4. 自旋锁(Spinlock)
概念
自旋锁是一种忙等待锁,当一个线程试图获取自旋锁时,如果该锁被其他线程持有,它将不断地循环检查锁的状态,而不是进入休眠状态。
特点
- 忙等待:在获取锁之前,会一直占用CPU资源。
- 高效场景:适用于锁定时间非常短的临界区,因为它避免了上下文切换的开销。
示例
以下示例展示了如何使用自旋锁实现互斥:
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_spinlock_t spinlock;
void* thread_function(void* arg) {
pthread_spin_lock(&spinlock); // 获取自旋锁
printf("Thread %d is in critical section\n", *(int*)arg);
// 访问临界区
pthread_spin_unlock(&spinlock); // 释放自旋锁
return NULL;
}
int main() {
pthread_t threads[3];
int thread_ids[3];
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE); // 初始化自旋锁
for (int i = 0; i < 3; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_spin_destroy(&spinlock); // 销毁自旋锁
return 0;
}
5. 读写锁(Read-Write Lock)
概念
读写锁是一种允许多个读者同时读,但写者必须独占的锁。它用于读多写少的场景,以提高并发性和性能。
使用方法
- 读锁:多个线程可以同时持有读锁,以执行读操作。
- 写锁:只有一个线程可以持有写锁,以执行写操作。持有写锁时,不允许任何读操作。
示例
以下示例展示了如何使用读写锁实现同步:
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
void* reader_function(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("Reader %d is reading\n", *(int*)arg);
// 读取操作
pthread_rwlock_unlock(&rwlock); // 释放读锁
return NULL;
}
void* writer_function(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
printf("Writer %d is writing\n", *(int*)arg);
// 写入操作
pthread_rwlock_unlock(&rwlock); // 释放写锁
return NULL;
}
int main() {
pthread_t readers[3], writers[3];
int ids[3];
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
for (int i = 0; i < 3; i++) {
ids[i] = i;
pthread_create(&readers[i], NULL, reader_function, &ids[i]);
pthread_create(&writers[i], NULL, writer_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(readers[i], NULL);
pthread_join(writers[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
总结
进程同步和互斥是确保多进程或多线程程序中共享资源安全访问和执行顺序控制的重要机制。
- 进程同步:用于协调进程的执行顺序,保证多个进程按照预期的顺序执行。
- 进程互斥:用于保证共享资源的独占访问,确保同一时刻只有一个进程能够访问临界区。
实现上述目标的常用方法包括信号量、互斥锁、条件变量、自旋锁和读写锁等。这些机制提供了强大的工具,帮助开发者在设计并发程序时处理同步和互斥问题,从而确保系统的正确性和高效性。根据具体的应用场景和需求,选择合适的同步和互斥机制,可以实现最佳的性能和稳定性。
day28 什么是死锁,如何预防死锁?
什么是死锁?
死锁(Deadlock)是指两个或多个进程在执行过程中因争夺资源而互相等待,导致永久阻塞的一种现象。死锁问题在多任务和多线程系统中尤其常见,主要由于以下四个必要条件的同时存在:
- 互斥(Mutual Exclusion):资源不能同时被多个进程占用。
- 持有与等待(Hold and Wait):一个进程已持有资源并等待其他资源,而这些资源被其他进程占用。
- 不剥夺(No Preemption):资源不能被强行剥夺,只能由占有它的进程主动释放。
- 循环等待(Circular Wait):存在一个进程集合,使得每个进程都在等待另一个进程所持有的资源,形成一个环。
如何预防死锁?
预防死锁的方法包括资源分配策略和一些设计范式。主要策略有四种:死锁的预防、死锁的避免、死锁的检测和死锁的恢复。下面是一些具体的方法和措施来防止死锁的发生。
1. 死锁预防 (Deadlock Prevention)
通过违反死锁的四个必要条件之一来预防死锁。
1.1 破坏互斥
虽然大部分资源本质上是互斥的,但可以设计共享资源,将资源转换为非互斥资源(如读写锁)。
1.2 破坏持有与等待
进程在开始执行时就一次性申请它所需的所有资源,进程在持有资源期间不再申请新的资源。
- 优点:简单直接。
- 缺点:可能导致资源利用率低,因为进程可能提前持有不需要的资源。
1.3 破坏不剥夺
允许资源动态剥夺。如果一个进程申请资源未获批准,则强制释放它已有的资源,然后重新申请所有资源。
- 优点:可以灵活管理资源。
- 缺点:比较复杂,可能增加系统开销。
1.4 破坏循环等待
为所有资源排序,并要求进程按序申请资源,确保不会形成环路。
- 优点:简单易行,有效破坏循环等待条件。
- 缺点:需要给资源分配全局序号,增加编程复杂度。
2. 死锁避免 (Deadlock Avoidance)
通过动态检测和分配资源来避免死锁。比较著名的方法是银行家算法。
2.1 银行家算法 (Banker’s Algorithm)
银行家算法是一种资源分配与回收算法,通过模拟资源分配情况,判断当前分配是否合法,以此动态避免死锁。
- 思路:进程在申请资源时,系统判断分配资源后是否处于安全状态,如果不安全,则不予分配。
示例:
python
复制代码
def is_safe_state(available, allocations, max_requests):
n = len(allocations)
m = len(available)
work = available[:]
finish = [False] * n
while True:
found = False
for i in range(n):
if not finish[i] and all(allocations[i][j] + work[j] >= max_requests[i][j] for j in range(m)):
work = [work[j] + allocations[i][j] for j in range(m)]
finish[i] = True
found = True
if not found:
break
return all(finish)
3. 死锁检测 (Deadlock Detection)
允许系统进入死锁状态,并通过检测算法发现死锁及恢复措施。
3.1 资源分配图
通过资源分配图来检测死锁,图中有进程和资源节点,并用有向边表示资源分配和请求。
- 检测:定期运行检测算法,发现死锁环路。
示例:
python
复制代码
def detect_deadlock(resource_allocation_graph):
# 伪代码,检验资源分配图中是否存在环
pass
4. 死锁恢复 (Deadlock Recovery)
检测到死锁之后,采取措施恢复系统。
4.1 资源剥夺
强制剥夺某些进程的资源,分配给其他进程。
- 策略:选择代价最小的进程进行剥夺,防止对系统造成较大影响。
4.2 回滚进程
将某些进程回滚到以前的安全状态,释放其资源并重新申请。
4.3 终止进程
终止一个或多个进程,释放全部资源以破坏循环等待条件。
- 策略:选择代价最小的进程进行终止,防止对系统造成较大影响。
预防死锁的设计原则
- 资源有序分配:设计系统时尽量确保资源分配有序,避免循环等待。
- 最大资源需求预测:对进程可能的最大资源需求进行预测,确保系统有足够资源满足进程需求。
- 避免长时间锁定:保证进程不会长时间持有资源,尽快释放。
- 优先级和剥夺策略:合理设计资源优先级和剥夺策略,确保系统快速响应。
通过以上策略和方法,预防死锁在复杂系统设计中显得尤为重要,可以确保系统高效、稳定地运行。对于实际应用,通常采用多种方法组合应用,以实现最佳的死锁预防和恢复方案。
day29 介绍- -下几种典型的锁
在并发编程中,锁(Lock)是实现同步和互斥的重要工具。锁用来保护共享资源,使得同时只有一个线程或进程能够访问临界区,防止数据错乱和竞态条件的发生。下面介绍几种常见的锁和它们的应用场景。
1. 互斥锁(Mutex,Mutual Exclusion Lock)
概念
互斥锁是一种最基本的锁,用于确保在任何时间点,最多只有一个线程能够访问临界区。它是实现互斥的主要手段。
特点
- 加锁与解锁:在进入临界区前加锁,离开临界区后解锁。
- 非递归锁:大多数互斥锁是非递归的,即同一个线程不能多次加锁。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
// 访问临界区
printf("Thread %d in critical section\n", *(int*)arg);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
2. 自旋锁(Spinlock)
概念
自旋锁在等待锁的过程中不断地循环(“自旋”),直到锁被释放。由于不会导致线程休眠,自旋锁适用于会在短时间内迅速释放的锁定操作。
特点
- 忙等待:在等待锁的过程中占用CPU资源。
- 效率高:适用于锁定时间很短的场景,避免了上下文切换的开销。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_spinlock_t spinlock;
void* thread_function(void* arg) {
pthread_spin_lock(&spinlock); // 获取自旋锁
// 访问临界区
printf("Thread %d in critical section\n", *(int*)arg);
pthread_spin_unlock(&spinlock); // 释放自旋锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE); // 初始化自旋锁
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_spin_destroy(&spinlock); // 销毁自旋锁
return 0;
}
3. 读写锁(Read-Write Lock)
概念
读写锁允许多个线程同时读取共享数据,但在写操作期间必须独占锁,防止读写冲突。读写锁适用于读多写少的场景。
特点
- 读锁:允许多个线程同时持有读锁。
- 写锁:写操作时,只有一个线程能够持有写锁,禁止其他读写操作。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
void* reader_function(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
// 执行读取操作
printf("Reader %d reading\n", *(int*)arg);
pthread_rwlock_unlock(&rwlock); // 释放读锁
return NULL;
}
void* writer_function(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
// 执行写操作
printf("Writer %d writing\n", *(int*)arg);
pthread_rwlock_unlock(&rwlock); // 释放写锁
return NULL;
}
int main() {
pthread_t readers[3], writers[3];
int ids[3] = {1, 2, 3};
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
for (int i = 0; i < 3; i++) {
pthread_create(&readers[i], NULL, reader_function, &ids[i]);
pthread_create(&writers[i], NULL, writer_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(readers[i], NULL);
pthread_join(writers[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
4. 递归锁(Recursive Lock)
概念
递归锁允许同一个线程多次获取同一个锁,而不会导致死锁。这在递归调用时特别有用。
特点
- 同一线程多次加锁:允许同一线程在没有释放锁的情况下,多次获取同一锁。
- 计数器:记录获取锁的次数,只有当计数为0时才真正释放锁。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t recursive_mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
void* recursive_function(void* arg, int level) {
pthread_mutex_lock(&recursive_mutex); // 加锁
printf("Thread %d in recursive level %d\n", *(int*)arg, level);
if (level > 0) {
recursive_function(arg, level - 1);
}
pthread_mutex_unlock(&recursive_mutex); // 解锁
return NULL;
}
void* thread_function(void* arg) {
recursive_function(arg, 3);
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&recursive_mutex); // 销毁递归锁
return 0;
}
5. 条件变量(Condition Variable)
概念
条件变量允许线程在某个条件不满足时等待,并在条件满足时被唤醒。这通常与互斥锁一起使用,以实现更复杂的同步。
特点
- 等待和唤醒:当条件不满足时,线程等待在条件变量上,并被其他线程唤醒。
- 互斥锁配合:通常与互斥锁配合使用,防止竞态条件。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int ready = 0;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
while (!ready) {
pthread_cond_wait(&cond, &mutex); // 等待条件变量
}
// 访问临界区
printf("Thread %d proceeding\n", *(int*)arg);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_cond_init(&cond, NULL); // 初始化条件变量
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
sleep(2); // 模拟一些处理
pthread_mutex_lock(&mutex); // 加锁
ready = 1;
pthread_cond_broadcast(&cond); // 唤醒所有等待线程
pthread_mutex_unlock(&mutex); // 解锁
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
pthread_cond_destroy(&cond); // 销毁条件变量
return 0;
}
6. 轻量级锁(Futex,Fast Userspace Mutex)
概念
轻量级锁是一种优化的互斥锁,主要在用户态解决争锁问题,只有在竞争激烈时才陷入内核态。Futex
是 Linux 提供的一种轻量级锁。
特点
- 用户态操作:大多数时间在用户态操作,减少系统调用开销。
- 内核态切换:只有在竞争激烈时才切换到内核态,处理唤醒和休眠。
示例
c
复制代码
// 使用 Futex 的代码会涉及较复杂的 Linux 系统调用,通常使用库函数封装
#include <linux/futex.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <stdatomic.h>
atomic_int futex_var;
void futex_wait(atomic_int *futexp, int val) {
syscall(SYS_futex, futexp, FUTEX_WAIT, val, NULL, NULL, 0);
}
void futex_wake(atomic_int *futexp, int n) {
syscall(SYS_futex, futexp, FUTEX_WAKE, n, NULL, NULL, 0);
}
void* thread_function(void* arg) {
int expected = 0;
if (atomic_compare_exchange_strong(&futex_var, &expected, 1)) {
// Critical section
printf("Thread %d in critical section\n", *(int*)arg);
atomic_store(&futex_var, 0);
futex_wake(&futex_var, 1);
} else {
futex_wait(&futex_var, 1);
}
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
7. 信号量(Semaphore)
概念
信号量是一种用于控制对资源访问的计数器,主要用于进程同步和解决进程间的竞争问题。可以分为二元信号量(二进制信号量,类似于互斥锁)和计数信号量。
特点
- 计数器:可以用于计数资源的使用情况,避免资源竞争。
- 同步和互斥:用于解决资源的同步和互斥问题。
示例
c
复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t semaphore;
void* thread_function(void* arg) {
sem_wait(&semaphore); // P操作,等待信号量
// 访问临界区
printf("Thread %d in critical section\n", *(int*)arg);
sem_post(&semaphore); // V操作,释放信号量
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
sem_init(&semaphore, 0, 1); // 初始化信号量,初值为1
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&semaphore); // 销毁信号量
return 0;
}
总结
不同类型的锁在特定场景中都有其独特的优点和适用性。选择合适的锁机制,可以有效地解决并发编程中的同步和互斥问题,从而提高程序的性能和稳定性:
- 互斥锁(Mutex):用于保护临界区,防止多个线程同时访问共享资源。
- 自旋锁(Spinlock):适用于短时间锁定操作,通过忙等待提高效率。
- 读写锁(Read-Write Lock):适用于读多写少的场景,提高并发性。
- 递归锁(Recursive Lock):允许同一线程多次获取同一锁,适用于递归操作。
- 条件变量(Condition Variable):用于线程间的条件同步,配合互斥锁使用。
- 轻量级锁(Futex):通过用户态和内核态的结合,提高性能。
- 信号量(Semaphore):用于资源计数和同步,适用于资源竞争问题。
理解并选择合适的锁机制,是并发编程中解决同步与互斥问题的重要一环。
day30 讲一讲你理解的虚拟内存
虚拟内存(Virtual Memory)是现代操作系统中一项关键技术,它通过将物理内存抽象为一个更大且连续的虚拟地址空间,使得程序可以更灵活、高效地使用内存资源。虚拟内存不仅提高了内存的利用率,还增强了系统的稳定性和安全性。
虚拟内存的基本概念
1. 虚拟地址空间
虚拟内存通过提供一个虚拟地址空间,使得每个进程都认为自己拥有独立且连续的内存区域。这些虚拟地址被映射到物理内存地址,操作系统和硬件负责完成这种映射。
2. 页面和页表
虚拟内存将内存划分为固定大小的块,称为页(Page)。对应的物理内存也划分为同样大小的页框(Page Frame)。映射关系由页表(Page Table)管理,页表记录了虚拟页和物理页框的对应关系。
3. 地址转换
地址转换即虚拟地址到物理地址的转换。在CPU执行指令时,需要将虚拟地址转换为物理地址。这通常由硬件中的内存管理单元(MMU,Memory Management Unit)和页表完成。
虚拟内存的优点
1. 内存保护
通过将每个进程的地址空间隔离,虚拟内存提高了系统的安全性。一个进程无法访问另一个进程的内存,从而防止恶意代码和应用程序的错误操作。
2. 虚拟内存扩展
程序可以使用比物理内存更大的地址空间,这是通过将部分页面调出到硬盘上的交换空间(Swap Space)实现的。当物理内存不足时,操作系统可以将不活跃的页面调出,并在需要时重新调入物理内存。
3. 内存共享
允许不同进程共享相同的内存区域,例如共享库(Shared Libraries)和共享内存区域(Shared Memory)。通过映射同一物理页面到多个进程的虚拟地址空间,实现高效内存利用。
4. 独立地址空间
每个进程拥有独立的虚拟地址空间,简化了内存管理。程序员无需担心内存空间冲突,提高了并发程序的稳定性和可开发性。
虚拟内存的实现机制
1. 分页(Paging)
分页是虚拟内存实现的一种方法。将虚拟内存和物理内存都划分为固定大小的页面,通过页表跟踪虚拟页和物理页框的对应关系。
页表项
页表中的每一项记录了一个虚拟页对应的物理页框,以及一些控制信息(如是否有效、读写权限等)。当访问一个虚拟地址时,MMU通过页表查找对应的物理地址。
页表结构
- 单级页表:简单但在大内存下缺乏效率。
- 多级页表:在内存分配上更加高效,减少页表项需要的存储。
- 反向页表:以物理内存为基础建立表项,适用于大型物理内存系统。
2. 页框置换算法
当物理内存满时,虚拟内存需要选择一个页面调出,将其内容存储到交换空间,释放页框给新的页面。页框置换算法决定了如何选择要调出的页面。
常见页框置换算法
- 先进先出(FIFO):选择最早被加载的页面。
- 最近最少使用(LRU):选择最近最少被访问的页面。
- 最不常用(LFU):选择访问次数最少的页面。
- 最佳置换(Optimal):选择接下来最长时间不会被访问的页面(理论上的最优解)。
3. 懒加载(Lazy Loading)
懒加载是一种优化内存使用的方法。只有在真正需要访问页面时,才调入页面到物理内存。这种方法减少了不必要的内存开销,提高了系统性能。
4. 缺页异常(Page Fault)
当访问的虚拟页不在物理内存中时,会触发缺页异常。操作系统在处理缺页异常时,会根据页表和页框置换算法,将所需页面调入物理内存,并更新页表。
虚拟内存的工作流程
- 地址生成:CPU根据程序指令生成虚拟地址。
- 页表查找:MMU在页表中查找虚拟页对应的物理页框。
- 地址转换:将虚拟地址转换为物理地址。
- 内存访问:根据物理地址访问内存内容。如果引发缺页异常,则操作系统进行缺页处理,将页面调入物理内存,并重试访问。
示例
以下是一个简化的流程示例,展示了虚拟内存的工作机制。
场景假设
- 操作系统将虚拟内存划分为 4KB 的页面。
- 有一个进程虚拟地址空间为 0x0000 - 0xFFFF。
- 物理内存有 16KB,总共有 4 个页框。
页表内容
虚拟页 | 物理页框 | 有效 |
---|---|---|
0 | 2 | 是 |
1 | 0 | 是 |
2 | 1 | 是 |
3 | - | 否 |
访问地址
假设进程访问虚拟地址 0x1234
(位于第 1 号虚拟页)。
转换流程
-
提取页号和偏移量:
- 页号 =
0x1234 / 0x1000 = 1
- 偏移量 =
0x1234 % 0x1000 = 0x234
- 页号 =
-
页表查找:
- 页表显示虚拟页 1 对应物理页框 0。
-
物理地址计算:
- 物理地址 =
物理页框号 * 页大小 + 偏移量
- 物理地址 =
0 * 0x1000 + 0x234 = 0x0234
- 物理地址 =
-
内存访问:
- 根据物理地址
0x0234
访问内存内容。
- 根据物理地址
总结
虚拟内存通过将物理内存抽象为更大的虚拟地址空间,实现了内存保护、内存共享和地址空间独立等多种功能。分页和页面置换等机制提高了内存利用率,而地址转换和缺页处理确保了系统性能和稳定性。
理解虚拟内存的概念和工作机制,有助于更好地设计、开发和优化软件,提高程序在多任务操作系统中的运行效率和可靠性。
day31 你知道的线程同步的方式有哪些?
线程同步是指在多线程程序中,协调线程之间的运行顺序以防止资源争用、数据竞争和其他并发问题。以下是一些常见的线程同步方式及其用法:
互斥锁(Mutex,Mutual Exclusion Lock)
概念
互斥锁是一种基本的同步机制,用于确保在任何时间点,至多只有一个线程可以进入临界区,从而防止多个线程同时访问共享资源。
用法
- 加锁:进入临界区前获取锁。
- 解锁:离开临界区后释放锁。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 获取锁
// 临界区代码
printf("Thread %d in critical section\n", *(int*)arg);
pthread_mutex_unlock(&mutex); // 释放锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
自旋锁(Spinlock)
概念
自旋锁是一种忙等待锁,当线程尝试获取锁失败时,会不断循环检查锁的状态,而不是进入阻塞状态。自旋锁适用于短时间锁定的操作。
特点
- 忙等待:等待时不断占用CPU资源。
- 高效:用于锁定时间很短的场景,避免了上下文切换开销。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_spinlock_t spinlock;
void* thread_function(void* arg) {
pthread_spin_lock(&spinlock); // 获取自旋锁
// 临界区代码
printf("Thread %d in critical section\n", *(int*)arg);
pthread_spin_unlock(&spinlock); // 释放自旋锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE); // 初始化自旋锁
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_spin_destroy(&spinlock); // 销毁自旋锁
return 0;
}
读写锁(Read-Write Lock)
概念
读写锁允许多个线程同时读取共享数据,但在写操作时必须独占锁。读写锁适用于读多写少的情况。
用法
- 读锁:允许多个线程同时获取读锁。
- 写锁:写操作时,需要独占写锁,禁止其他读写操作。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
void* reader_function(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
// 读操作代码
printf("Reader %d reading\n", *(int*)arg);
pthread_rwlock_unlock(&rwlock); // 释放读锁
return NULL;
}
void* writer_function(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
// 写操作代码
printf("Writer %d writing\n", *(int*)arg);
pthread_rwlock_unlock(&rwlock); // 释放写锁
return NULL;
}
int main() {
pthread_t readers[3], writers[3];
int ids[3] = {1, 2, 3};
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
for (int i = 0; i < 3; i++) {
pthread_create(&readers[i], NULL, reader_function, &ids[i]);
pthread_create(&writers[i], NULL, writer_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(readers[i], NULL);
pthread_join(writers[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
条件变量(Condition Variable)
概念
条件变量用于线程间的条件同步。线程可以在条件变量上等待,直到其他线程通知条件满足。通常与互斥锁一起使用,以防止竞态条件。
用法
- 等待:在条件变量上等待,通常需要与互斥锁同时使用。
- 通知:通知等待条件变量的线程,条件已满足。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int ready = 0;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
while (!ready) {
pthread_cond_wait(&cond, &mutex); // 等待条件变量
}
// 临界区代码
printf("Thread %d proceeding\n", *(int*)arg);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_cond_init(&cond, NULL); // 初始化条件变量
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
// 模拟主线程的某些操作
sleep(2); // 模拟一些工作
pthread_mutex_lock(&mutex); // 加锁
ready = 1;
pthread_cond_broadcast(&cond); // 唤醒所有等待线程
pthread_mutex_unlock(&mutex); // 解锁
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
pthread_cond_destroy(&cond); // 销毁条件变量
return 0;
}
信号量(Semaphore)
概念
信号量是一种用于控制对某些资源访问的计数器,可以用于解决进程/线程间的互斥和同步问题。信号量可以是二元信号量(Binary Semaphore,类似于互斥锁)或者计数信号量(Counting Semaphore)。
用法
- P操作(等待,wait):减少信号量的值,如果信号量的值为0,则阻塞。
- V操作(信号,signal):增加信号量的值,如果有线程阻塞在信号量上,则唤醒。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t semaphore;
void* thread_function(void* arg) {
sem_wait(&semaphore); // P操作,等待信号量
// 临界区代码
printf("Thread %d in critical section\n", *(int*)arg);
sem_post(&semaphore); // V操作,释放信号量
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
sem_init(&semaphore, 0, 1); // 初始化信号量,初值为1
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&semaphore); // 销毁信号量
return 0;
}
栅栏(Barrier)
概念
栅栏是一种同步机制,用于让一组线程在某个"栅栏点"等待,直到全部线程都到达栅栏点,才能继续执行。这对于需要多个线程同时进入下一步操作的场景非常有用。
用法
- 等待:线程调用等待函数,当所有线程都调用等待函数后,所有线程才继续执行。
示例(C语言使用pthread库)
c
复制代码
#include <stdio.h>
#include <pthread.h>
pthread_barrier_t barrier;
void* thread_function(void* arg) {
printf("Thread %d before barrier\n", *(int*)arg);
pthread_barrier_wait(&barrier); // 等待栅栏
printf("Thread %d after barrier\n", *(int*)arg);
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
pthread_barrier_init(&barrier, NULL, 3); // 初始化栅栏,同步点为3个线程
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_barrier_destroy(&barrier); // 销毁栅栏
return 0;
}
总结
线程同步机制的选择取决于具体的应用场景和需求。以下是一些常见同步方式及其适用场景:
- 互斥锁(Mutex):用于保护临界区,防止多个线程同时访问共享资源。适合简单的互斥场景。
- 自旋锁(Spinlock):适用于短时间锁定操作,通过忙等待提高效率,但不适合长时间等待。
- 读写锁(Read-Write Lock):适用于读多写少的场景,允许多个线程同时读取数据,但在写操作时必须独占。
- 条件变量(Condition Variable):用于线程间的条件同步,配合互斥锁使用,适合复杂的同步逻辑。
- 信号量(Semaphore):用于资源计数和同步,适用于控制资源访问和解决生产者-消费者问题。
- 栅栏(Barrier):用于让一组线程在某个点同步,适用于需要所有线程同时进入下一步操作的场景。
理解和选择合适的线程同步机制,是实现高效、安全并发程序的关键。通过合理运用这些同步工具,可以有效解决数据竞争、死锁和其他并发问题,提高系统的性能和可靠性。
day32 有哪些页面置换算法
day15-28
熟悉哪些Linux命令
作为一个大型语言模型,我熟悉许多常见的Linux命令。以下是一些常用的Linux命令及其简要说明:
文件和目录操作
ls
:列出目录内容。cd
:改变当前工作目录。pwd
:显示当前工作目录路径。mkdir
:创建新目录。rmdir
:删除空目录。rm
:删除文件或目录。cp
:复制文件或目录。mv
:移动或重命名文件或目录。touch
:创建一个新的空文件或更新文件的时间戳。find
:搜索文件和目录。
文件内容操作
cat
:连接文件并打印到标准输出。more
和less
:分页查看文件内容。head
:显示文件的前几行。tail
:显示文件的后几行。grep
:搜索文本中的模式。
权限和所有权
chmod
:更改文件模式(权限)。chown
:更改文件所有者。chgrp
:更改文件的组所有者。
系统信息
uname
:显示操作系统信息。df
:显示文件系统磁盘空间使用情况。du
:显示文件和目录的磁盘使用情况。top
:显示任务管理器。free
:显示内存使用情况。
网络
ping
:测试网络连接。ifconfig
和ip
:显示或配置网络接口。wget
和curl
:下载文件并进行基于HTTP请求的操作。
进程管理
ps
:显示当前进程列表。kill
:终止进程。killall
:通过名称终止进程。bg
和fg
:后台和前台管理进程。
压缩和解压
tar
:创建、提取归档文件。gzip
和gunzip
:压缩和解压缩文件。zip
和unzip
:打包和解压缩文件。
用户管理
adduser
和useradd
:添加用户。passwd
:更改用户密码。deluser
和userdel
:删除用户。groupadd
:创建新用户组。
其他有用命令
echo
:显示一段文本。history
:显示命令历史。alias
:创建命令别名。sudo
:以超级用户权限执行命令。
这些命令只是冰山一角,还有很多其他命令和工具可用,以满足不同需求。使用man
命令(如man ls
)可以查看每个命令的详细使用手册。
如何查看某个端口有没有被占用 说- -下select、poll、 epoll 一条SQL查询语句 是如何执行的?
如何查看某个端口有没有被占用
在Linux中,有几种方法查看某个端口是否被占用:
-
使用
netstat
命令:sh
复制代码
netstat -tuln | grep <port_number>
例如,要查看端口8080是否被占用:
sh
复制代码
netstat -tuln | grep 8080
-
使用
ss
命令:sh
复制代码
ss -tuln | grep <port_number>
例如:
sh
复制代码
ss -tuln | grep 8080
-
使用
lsof
命令:sh
复制代码
lsof -i:<port_number>
例如:
sh
复制代码
lsof -i:8080
Select、Poll、Epoll
1. Select
select
是最古老的I/O多路复用机制。它允许程序监视多个文件描述符,等待它们变为"准备好"状态,以执行I/O操作。一个典型的调用select
的代码样例如下:
c
复制代码
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(socket_fd, &readfds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int result = select(socket_fd + 1, &readfds, NULL, NULL, &timeout);
if (result > 0) {
// 有描述符准备好
} else if (result == 0) {
// 超时
} else {
// 出错
}
- 优点:跨平台支持性好。
- 缺点:能监视的文件描述符数量有限,效率较低(每次调用都要线性扫描所有描述符)。
2. Poll
poll
是select
的改进版本,从API设计上放宽了文件描述符数量的限制。典型代码样例如下:
c
复制代码
struct pollfd fds[2];
fds[0].fd = socket_fd1;
fds[0].events = POLLIN;
fds[1].fd = socket_fd2;
fds[1].events = POLLIN;
int result = poll(fds, 2, 5000);
if (result > 0) {
for(int i = 0; i < 2; ++i) {
if (fds[i].revents & POLLIN) {
// 这个文件描述符可读
}
}
} else if (result == 0) {
// 超时
} else {
// 出错
}
- 优点:没有文件描述符数量的限制。
- 缺点:仍需要线性扫描,效率不高。
3. Epoll
epoll
是Linux特有的I/O多路复用机制,比select
和poll
效率高,适用于大量文件描述符场合。典型代码样例如下:
c
复制代码
int epfd = epoll_create(1);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = socket_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);
struct epoll_event events[MAX_EVENTS];
int event_count = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < event_count; ++i) {
if (events[i].events & EPOLLIN) {
// 这个文件描述符可读
}
}
- 优点:效率高,适合大量文件描述符。
- 缺点:只能在Linux系统上使用。
一条SQL查询语句的执行过程
SQL查询的执行过程可以分为以下几个步骤:
- 解析(Parsing):将SQL语句解析为内部数据结构。包括词法分析和语法分析,以确保SQL语句的正确性。
- 预处理(Preprocessing):对SQL语句进行进一步检查(如权限检查,表名和列名的存在性检查)。
- 优化(Optimization):生成一个优化的执行计划。包括选择最优的索引,最小化执行代价,以及重新排列联接顺序等。
- 执行(Execution):根据优化器生成的执行计划逐步执行。访问底层数据,进行检索或修改操作。
- 返回结果(Fetching Results):将查询结果返回给客户端。
例如,对于一条简单的SQL查询:
sql
复制代码
SELECT * FROM users WHERE age > 30;
执行过程包括:
- 解析:检查语法是否合法。
- 预处理:检查
users
表和age
列是否存在,检查用户是否有权限执行该查询。 - 优化:如果
age
列上有索引,优化器会创建一个执行计划使用该索引。 - 执行:根据执行计划,扫描
users
表,筛选出age
大于30的记录。 - 返回结果:将符合条件的记录返回给客户端。
每个数据库系统(如MySQL、PostgreSQL、Oracle等)在具体实现细节上可能有所不同,但主要过程大致相同。
数据库的事务隔离级别有哪些?事务的四大特性有哪些?MySQL的执行引擎有哪些?MySQL为什么使用B+树来作索引
数据库的事务隔离级别
事务隔离级别决定了事务操作之间的相互影响,具体有四种隔离级别,由低到高如下:
-
未提交读(Read Uncommitted):
- 允许脏读,即一个事务可以读取另一个未提交事务的数据变化。
-
提交读(Read Committed):
- 不允许脏读,一个事务只能读取另一个已提交事务的数据。
- 该隔离级别仍会出现不可重复读。
-
可重复读(Repeatable Read):
- 保证在同一个事务中,多次读取同一数据结果是一致的。
- 解决了脏读和不可重复读的问题,但可能出现幻读。
- MySQL的InnoDB引擎默认使用可重复读隔离级别。
-
可串行化(Serializable):
- 最高隔离级别,通过加锁实现。所有事务逐个顺序执行。
- 解决脏读、不可重复读和幻读的问题,但性能最差。
事务的四大特性 (ACID)
数据库事务的四大特性包括:
-
原子性(Atomicity):
- 事务中的所有操作要么全部执行成功,要么全部回滚,不会停留在中间状态。
-
一致性(Consistency):
- 事务执行前和执行后,数据一致。事务将数据库从一种有效状态转变为另一种有效状态。
-
隔离性(Isolation):
- 保证事务之间互不干扰,一个事务的执行不受其他并发事务的干扰。
-
持久性(Durability):
- 一旦事务提交,其结果是永久性的,即使系统发生故障也不会丢失。
MySQL的执行引擎
MySQL支持多个存储引擎,每个存储引擎有不同的特点。常见的存储引擎包括:
-
InnoDB:
- 默认存储引擎,支持事务、外键、行级锁定等。
- 支持ACID特性,适用于大多数应用场景。
-
MyISAM:
- 不支持事务和外键,支持表级锁定。
- 适用于以读操作为主的应用。
-
MEMORY:
- 将数据存储在内存中,速度快,适用于临时数据表。
- 数据在服务器重启时会丢失。
-
CSV:
- 将数据以CSV文件格式存储。
- 便于数据导出和导入,但不支持索引。
-
ARCHIVE:
- 适用于存储历史归档数据,支持高压缩率。
- 只支持INSERT和SELECT操作。
-
NDB:
- MySQL Cluster使用的引擎,支持分布式数据存储。
MySQL为什么使用B+树来作索引
MySQL使用B+树作为索引结构的主要原因包括以下几点:
-
高效的查询性能:
- B+树是一种平衡树结构,所有叶子节点处于同一层级。这使得在找某个键值的过程中,每一步操作复杂度稳定为O(log n),查询速度非常快。
-
支持范围查询:
- B+树的叶子节点通过链表相连,这使得对范围查询(如查找一段区间内的数据)非常高效,只需要顺序扫描叶子节点即可。
-
磁盘I/O优化:
- B+树内部节点存储更多的键值,减少树的深度,降低了磁盘I/O次数。因为数据库索引大多存储在磁盘中,每次读取节点可能都需要一次I/O操作。
-
节点分裂和合并成本低:
- B+树在插入或删除操作时通过节点分裂和合并来保持树的平衡,节点间的分裂和合并相对简单和高效。
-
对大型数据集的适应性强:
- B+树结构可以很好的处理大型数据集情况,分支因子的设计使数据分布更均匀,减少了树的高度,使得性能在数据量增大时依然保持稳定。
综上所述,B+树是非常适合用来构建数据库索引的结构,由其在查询、插入和删除上对于磁盘I/O操作的优化,使其成为MySQL等数据库系统的首选索引结构。
说一下索引失效的场景?
在MySQL中,索引的设计目的是为了提高查询性能。但在某些情况下,索引可能会失效,使数据库系统无法利用索引进行高效查询,转而进行全表扫描。下面列出了一些常见的索引失效的场景:
-
使用函数或表达式: 当在索引列上使用函数或者其他任何表达式,索引无法被利用。
sql
复制代码
SELECT * FROM users WHERE YEAR(birthdate) = 1990;
上述查询中,如果
birthdate
列有索引,由于使用了YEAR
函数,该索引将失效。 -
隐式类型转换: 如果列的数据类型与查询中的常量数据类型不匹配,可能会导致索引失效。
sql
复制代码
SELECT * FROM users WHERE phone_number = 12345;
如果
phone_number
列是字符串类型,而查询中的12345
是数字类型,隐式类型转换会导致索引失效。将常量改为字符串'12345'
可以避免这种情况。 -
范围条件在复合索引中的顺序: 在复合索引中,范围条件(如
<
,>
,BETWEEN
,LIKE
)会导致后续的索引列失效。sql
复制代码
SELECT * FROM users WHERE age > 30 AND name = 'Alice';
如果有复合索引
(age, name)
,由于范围条件age > 30
,name
列的索引会失效。 -
前缀模糊查询: 使用前缀通配符的
LIKE
查询会导致索引失效。sql
复制代码
SELECT * FROM users WHERE name LIKE '%Alice';
上述查询中,
name
列的索引无效。而以下查询的索引是有效的:sql
复制代码
SELECT * FROM users WHERE name LIKE 'Alice%';
-
OR条件: 当
OR
条件中有一个条件没有索引时,整个OR
语句的索引将失效。sql
复制代码
SELECT * FROM users WHERE age = 30 OR name = 'Alice';
如果
age
列没有索引,那么即使name
列有索引,整个查询还是会进行全表扫描。 -
不等于操作符: 使用不等于操作符(
!=
或者<>
)会导致索引失效。sql
复制代码
SELECT * FROM users WHERE age != 30;
这个查询中的
age
列索引将无效。 -
NULL值检查: 使用
IS NULL
或IS NOT NULL
进行查询时,索引可能无效。sql
复制代码
SELECT * FROM users WHERE age IS NULL;
-
前导列缺失的复合索引: 在使用复合索引时,如果查询中不包含索引的前导列,索引将失效。
sql
复制代码
CREATE INDEX idx_user_info ON users (first_name, last_name); SELECT * FROM users WHERE last_name = 'Smith';
因为缺少复合索引的前导列
first_name
,该查询无法利用索引。 -
条件在某些操作中的改变顺序: 某些情况下,优化器可能调换查询条件的顺序,这可能导致索引失效。
sql
复制代码
SELECT * FROM users WHERE name = 'Alice' AND age > 30;
即使为
(name, age)
建立了索引,但优化器可能会优先处理age > 30
,导致索引失效。
了解这些场景能够帮助在日常数据库设计与优化过程中,避免索引失效的问题,从而提升查询性能。
undo log、redo log、binlog 有什么用?什么是慢查询?原因是什么?可以怎么优化?
Undo Log、Redo Log和Binlog
在MySQL中,undo log
、redo log
和 binlog
是关键的日志类型,每种日志都有不同的作用:
1. Undo Log
作用:
- 用于事务回滚。
undo log
记录了事务开始前的数据状态,以便在需要回滚时恢复初始状态。 - 支持MVCC(多版本并发控制),使得在读操作时可以获取一致性视图。
工作原理:
- 当执行一个更新操作时,会先将当前的数据状态记录到
undo log
中。如果事务中途出错,需要回滚时,MySQL会根据undo log
将数据恢复到原来的状态。
2. Redo Log
作用:
- 确保事务的持久性。
redo log
是物理日志,记录数据页的物理变化,用于崩溃恢复。 - 提高性能,通过先写日志后写磁盘的方式减少写盘次数。
工作原理:
- 在事务提交之前,先将修改操作写入
redo log
。即使系统崩溃,通过redo log
也可以在恢复时重新应用这些修改操作,确保数据一致性。
3. Binlog
作用:
- 用于数据库的复制和恢复。
binlog
是一种逻辑日志,记录每一个数据变更的具体SQL语句。 - 支持数据的基于时间点的恢复。
工作原理:
binlog
记录所有更改操作(如INSERT
、UPDATE
、DELETE
),并在事务提交后写入。这个日志文件通常用于主-从数据库复制和数据恢复过程中。
什么是慢查询?
慢查询(Slow Query)是指执行时间超过设定阈值的SQL查询。MySQL使用慢查询日志
记录这些查询,以帮助数据库管理员识别和解决性能瓶颈。
原因:
慢查询的原因通常包括但不限于以下几个方面:
- 缺少适当的索引:最常见的原因是表的设计或查询设计没有利用索引,导致全表扫描。
- 复杂的查询语句:查询语句中包含复杂的联表、子查询或其他复杂操作。
- 数据量大:随着表的数据量增长,查询速度逐渐变慢。
- 服务器配置问题:不合理的内存配置、I/O性能等硬件环境问题。
- 锁竞争:高并发的情况下,大量的锁竞争导致查询速度变慢。
如何优化慢查询?
-
使用适当的索引:
- 检查慢查询日志,分析哪些查询没有使用索引。
- 创建或优化索引,使查询语句、特别是
WHERE
条件、JOIN
操作和排序操作(ORDER BY
)能够充分利用索引。
-
查询优化:
- 使用
EXPLAIN
命令分析查询执行计划,找出潜在的性能问题。 - 尽量避免复杂的联表和子查询,可以通过视图或存储过程优化查询。
- 减少返回的行数,只选择需要的字段,不要使用
SELECT *
。
- 使用
-
分页优化:
- 对返回大量数据的查询进行分页。
- 优化复杂分页查询,结合索引和限制,避免全表扫描。
-
缓存技术:
- 使用查询缓存(MySQL 8.0已移除此功能)或第三方缓存(如Redis,Memcached)来减少数据库的重复查询。
- 使用程序中缓存已经计算好的数据.
-
数据分区:
- 对于非常大的数据表,可以使用分区技术,将表分割成多个更小的子表,以提高性能。
-
硬件优化:
- 增加内存,提升磁盘I/O性能,使用SSD等。
-
调整MySQL配置:
- 根据具体的应用场景调整MySQL配置参数,如
innodb_buffer_pool_size
,query_cache_size
,table_open_cache
等。
- 根据具体的应用场景调整MySQL配置参数,如
-
优化事务管理:
- 尽量减少事务的大小和运行时间,以减少锁争用。
- 使用
LOCK IN SHARE MODE
或FOR UPDATE
来把事务锁在较小的范围内。
慢查询的优化是数据库性能优化的重要组成部分,通过仔细分析慢查询日志和执行计划,采取合理的优化措施,可以显著提升数据库的性能。
MySQL和Redis的区别?
MySQL 和 Redis 是两种非常不同类型的数据库管理系统,主要区别如下:
1. 类型和用例
-
MySQL:
- 关系型数据库管理系统(RDBMS)。
- 烦entar的SQL语句进行操作。
- 适用于需要复杂查询和事务管理的应用,如ERP系统,银行系统。
- 支持ACID特性,保证数据的一致性和持久性。
-
Redis:
- NoSQL数据库和内存数据结构存储。
- 主要缓存存储系统,支持多种数据结构(字符串、列表、集合、散列、排序集合等)。
- 适用于需要快速访问和高并发的场景,如缓存、高速队列、会话管理、实时统计等。
- 在内存中操作数据,提供极高的读写性能。
2. 数据模型
-
MySQL:
- 基于关系数据模型,使用表、行和列来存储数据。
- 数据包含在模式中,强制执行通过表间的关系。
-
Redis:
- 基于键值对的数据模型,支持多种数据类型(string、hash、list、set、sorted set等)。
- 没有固定模式,更适合灵活和快速变化的数据需求。
3. 持久性
-
MySQL:
- 默认持久性,数据存储在磁盘中。
- 提供基于事务提交和日志(如上文所述的
redo log
和binlog
)的持久化和恢复功能。
-
Redis:
- 默认是内存存储,可以通过快照(RDB)和日志追加(AOF)实现持久化。
- 在持久化设置得当时可保数据的持久性,但主要是为了性能优化不配置完全持久性。
4. 读写性能
-
MySQL:
- 磁盘I/O操作较多,性能受限制。
- 高并发情况下,性能可能会急剧下降,需要调优配置和硬件支持。
-
Redis:
- 数据存储在内存中,速度非常快,提供毫秒级的读写操作。
- 支持高并发,适用于需要快速读写访问数据的应用。
5. 事务支持
-
MySQL:
- 完整的事务支持,遵循ACID原则(原子性、一致性、隔离性和持久性)。
- 存在复杂的事务隔离级别和回滚处理。
-
Redis:
- 提供简单的事务支持,通过MULTI、EXEC、WATCH等命令实现。
- 没有MySQL那么复杂的事务管理,通常在应用程序层进行控制。
6. 数据结构和功能
-
MySQL:
- 适合结构化数据存储和复杂查询。
- 支持索引、视图、存储过程、触发器等高级功能。
-
Redis:
- 适合灵活的数据存储,不限定数据结构。
- 提供原子操作、发布/订阅、Lua脚本、事务等功能。
7. 应用场景
-
MySQL:
- 适用于金融系统、电子商务、内容管理系统等需要复杂事务和高数据一致性的场景。
- 适合用于持久存储以及和关系业务逻辑密切关联的数据查询。
-
Redis:
- 适用于缓存、会话存储、实时数据分析、任务队列、计数器等需要高并发和快速响应的场景。
- 常作为主数据库的补充,用于提高系统性能。
总结
MySQL 和 Redis 并不是对立的,而是通常互补的。MySQL 适用于需要可靠、持久存储和复杂查询的场景,Redis 主要用于需要快速访问和灵活数据存储的场景。两者经常结合使用:MySQL 作为主数据库,Redis 用于缓存、快速读写和处理高并发。理解它们的各自特性和适用场景,有助于在实际项目中做出最佳选择。
Redis有什么优缺点?为什么用Redis查询会比较快
Redis 是一种内存数据存储系统,以其高性能和丰富的功能集著称。以下是它的主要优缺点以及查询速度快的原因。
Redis的优缺点
优点
-
高性能:
- 内存存储:因为数据存储在内存中,相比于基于磁盘的存储系统(如关系型数据库),读写速度非常快。
- 单线程架构:简单直观,没有额外的线程切换和上下文切换成本,在大多数场景下表现良好。
-
丰富的数据结构:
- Redis 支持多种数据类型,例如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位数组(Bitmap)、地理空间(geo)等,适合多种应用场景。
-
简单易用:
- 提供丰富的命令操作,可以方便地进行数据存取和管理。
- 支持事务、发布/订阅、Lua脚本等高级功能。
-
持久化支持:
- 提供 RDB 快照和 AOF 日志两种持久化机制,允许在数据内存和数据持久性之间取得平衡。
-
分布式支持:
- 原生支持主从复制,容易扩展。
- 提供 Redis Cluster 模式,支持分布式存储和高可用性。
-
良好的社区和丰富的生态系统:
- 有强大的社区支持和多种客户端库。
- 丰富的第三方工具和扩展,如 Redis Sentinel 用于高可用性管理,RedisGears、RedisAI 等用于扩展 Redis 的功能。
缺点
-
内存成本高:
- 因为数据存储在内存中,当数据量非常大时,内存的成本较高。
- 不适合存储大规模的、需要长久保存的数据。
-
数据一致性:
- 主从复制下存在短暂的不一致性,不能保证严格的强一致性。
- 在 Redis Cluster 模式下,一些命令的原子性和事务需要额外的处理。
-
单线程限制:
- 虽然单线程架构在大部分情况下表现良好,但在某些 CPU 密集型操作或非常高的并发情况下,可能成为瓶颈。
- 需要通过水平扩展(如使用 Redis Cluster)来解决单机性能限制问题。
为什么 Redis 查询快
-
内存存储:
- 数据完全存储在内存中,极大地减少了存储设备的I/O延迟,查询速度可以达到亚毫秒级别。
-
单线程模型:
- Redis 使用单线程模型来处理请求,避免了频繁的上下文切换以及多线程竞争锁资源的问题,从而减少了延迟,提高了性能。
-
高效的数据结构:
- Redis 内部的多种数据结构(如哈希表、跳表等)都经过高度优化,能以最佳性能进行数据存储和查找。
- 各种数据类型的操作都有专门的命令进行优化。
-
高效的序列化与反序列化:
- Redis 的 RDB 快照和 AOF 日志采用高效的序列化方法,在进行持久化及恢复时效率较高。
-
内存管理机制:
- Redis 使用 jemalloc 作为内存分配器,该分配器高效、稳定,以减小内存碎片,提高内存使用效率。
-
管道和批量操作:
- Redis 支持命令管道(pipeline),允许客户端批量发送多个命令并减少网络往返次数,从而提高性能。
-
合理的使用场景:
- Redis 通常用作缓存、消息队列、实时分析等场景,这些场景本身数据量适中且更注重快速读写性能。
综上所述,Redis 的高性能来源于它在多个层面的优化,包括内存存储、单线程模型、高效的数据结构和优秀的内存管理机制等,使其在处理高并发读写和实时需求的应用中表现尤为突出。
Redis的数据类型有那些?
Redis 是一个支持多种数据类型的内存数据存储系统,这些数据类型使 Redis 适用于各种不同的场景和需求。以下是 Redis 支持的主要数据类型及其简单介绍:
1. String(字符串)
- 描述:最基本的数据类型,可以存储任何格式的数据,包括文字、数字、二进制数据等。
- 常用命令:
SET key value
:设置键值。GET key
:获取键值。INCR key
:将键值加1(适用于整数类型)。APPEND key value
:在键值后追加字符串。
2. Hash(哈希)
- 描述:类似于字典或对象,以字段(field)和值(value)的形式存储数据,适用于存储对象或结构化的数据。
- 常用命令:
HSET key field value
:设置字段值。HGET key field
:获取字段值。HGETALL key
:获取所有字段和值。HDEL key field
:删除字段。
3. List(列表)
- 描述:有序的字符串列表,可以从头部或尾部推入和弹出元素,适用于实现消息队列、待办事项列表等。
- 常用命令:
LPUSH key value
:从头部推入元素。RPUSH key value
:从尾部推入元素。LPOP key
:从头部弹出元素。RPOP key
:从尾部弹出元素。LRANGE key start stop
:获取指定范围的元素。
4. Set(集合)
- 描述:无序的唯一字符串集合,适用于需要保证唯一性的场景,如标签、用户角色等。
- 常用命令:
SADD key value
:添加元素到集合。SREM key value
:从集合中移除元素。SMEMBERS key
:获取集合中的所有元素。SISMEMBER key value
:判断元素是否在集合中。
5. Sorted Set(有序集合)
- 描述:与 Set 类似,但每个元素关联一个评分(score),按照评分进行排序,可实现排行榜、带权重的数据存储等。
- 常用命令:
ZADD key score value
:添加元素和评分。ZREM key value
:移除元素。ZRANGE key start stop [WITHSCORES]
:获取指定范围的元素(可选返回评分)。ZRANK key value
:返回元素的排名。
6. Bitmap(位图)
- 描述:按位存储的数据类型,可以用于实现布尔值操作、位运算等,适用于用户签到、状态记录等场景。
- 常用命令:
SETBIT key offset value
:设置指定偏移位置的位值。GETBIT key offset
:获取指定偏移位置的位值。BITCOUNT key [start end]
:统计指定范围内位值为1的个数。
7. HyperLogLog
- 描述:适用于基数统计的概率性数据结构,可以近似计算不重复元素的数量,占用空间极小,但有误差。
- 常用命令:
PFADD key element
:添加元素。PFCOUNT key
:返回估算的基数。
8. Geo(地理空间)
- 描述:存储地理空间信息,提供地理位置的操作命令,如添加地点、计算距离、查找附近位置等。
- 常用命令:
GEOADD key longitude latitude member
:添加地理位置。GEODIST key member1 member2 [unit]
:计算两地之间的距离。GEORADIUS key longitude latitude radius m|km
:查找指定范围内的地理位置。
9. Stream(流)
- 描述:高效的消息队列,可用于记录日志、事件流处理等场景,类似Kafka、RabbitMQ等消息系统。
- 常用命令:
XADD key * field value [field value ...]
:添加消息到流。XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key...] ID [ID...]
:读取消息。
这些丰富的数据类型使得 Redis 可以高效地处理各种不同的应用需求,从简单的缓存和计数器,到复杂的消息队列和地理位置服务。Redis 不仅仅是一个键值存储系统,它更多的是一个多功能内存数据存储解决方案。
Redis是单线程的还是多线程的,为什么?
Redis 的核心是单线程的,尽管在最新的 Redis 版本中,引入了一些多线程功能来处理特定的任务(如 I/O 操作),但总体上,Redis 仍然是单线程架构。以下是详细解析:
Redis 是单线程的吗?
是的,Redis 的核心操作是单线程的。
为什么 Redis 选择单线程架构?
-
简单性和易维护:
- 单线程模型使得代码简单、易于理解和维护。避免了在多线程环境下出现的复杂同步问题,如死锁、竞态条件等。
- 单线程架构不需要频繁地进行线程切换,减少了系统的开销。
-
高效利用 CPU:
- 在大多数常见的 Redis 使用场景中,性能瓶颈通常在网络 I/O 或内存带宽,而不是 CPU。因此,即使是单线程模型,对于大多数应用程序而言,性能已经足够。
- 使用高效的数据结构和算法,使得每个命令的执行速度非常快,从而单线程也能提供高性能。
-
避免了频繁的上下文切换:
- 多线程中频繁的线程切换会带来上下文切换的开销,而单线程架构由于没有线程切换,避免了这部分性能损耗。
单线程的 Redis 如何处理高并发?
-
事件驱动的 I/O 多路复用:
- Redis 使用 epoll(Linux)和 kqueue(macOS)等高效的 I/O 多路复用机制,通过一个事件循环来处理多个连接请求。这使得单线程可以高效地处理大量并发连接。
-
非阻塞 I/O 和短命令:
- Redis 所有的命令都是原子的,设计为快速执行,避免了阻塞操作,使单线程能够短时间内处理大量请求。
-
使用内存存储:
- 数据存储在内存中,数据存取速度极快,远高于基于磁盘的存储。这使得单线程模型能够高效完成数据操作。
Redis 的多线程功能
在 Redis 的最新版本(如 Redis 6.0 及以上),引入了一些多线程功能,主要用于网络 I/O 操作。多线程功能的引入并没有改变 Redis 的单线程执行模式,而是通过多线程处理网络请求的读写,减少了单线程在高并发情况下的 I/O 阻塞,进一步提高了性能。
- I/O 多线程:引入了多线程来处理网络数据的读写。这些线程负责接收客户端请求并将请求数据送交给主线程处理。主线程仍然是执行所有命令的唯一线程,确保命令执行的原子性和一致性。
综上所述
Redis 核心是单线程架构,选择单线程架构主要是为了保持简单、避免复杂的多线程同步问题,并充分利用内存和高效的数据结构来提供快速的数据访问。虽然 Redis 在最新版本中引入了多线程功能,主要用于网络 I/O 操作,但整体执行模型仍然是单线程的,确保了其高效和一致性。这种设计使得 Redis 在绝大多数的应用场景中表现非常出色。