learn.lianglianglee.com/专栏/重学操作系统-完/20 线程的调度:线程调度都有哪些方法?.md.html
2022-05-11 18:57:05 +08:00

1067 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>20 线程的调度:线程调度都有哪些方法?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/重学操作系统-完/00 开篇词 为什么大厂面试必考操作系统?.md.html">00 开篇词 为什么大厂面试必考操作系统?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/00 课前必读 构建知识体系,可以这样做!.md.html">00 课前必读 构建知识体系,可以这样做!.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/01 计算机是什么:“如何把程序写好”这个问题是可计算的吗?.md.html">01 计算机是什么:“如何把程序写好”这个问题是可计算的吗?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/02 程序的执行:相比 32 位64 位的优势是什么?(上).md.html">02 程序的执行:相比 32 位64 位的优势是什么?(上).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/03 程序的执行:相比 32 位64 位的优势是什么?(下).md.html">03 程序的执行:相比 32 位64 位的优势是什么?(下).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/04 构造复杂的程序:将一个递归函数转成非递归函数的通用方法.md.html">04 构造复杂的程序:将一个递归函数转成非递归函数的通用方法.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/05 存储器分级L1 Cache 比内存和 SSD 快多少倍?.md.html">05 存储器分级L1 Cache 比内存和 SSD 快多少倍?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/05 (1) 加餐 练习题详解(一).md.html">05 (1) 加餐 练习题详解(一).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/06 目录结构和文件管理指令rm -rf 指令的作用是?.md.html">06 目录结构和文件管理指令rm -rf 指令的作用是?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/07 进程、重定向和管道指令xargs 指令的作用是?.md.html">07 进程、重定向和管道指令xargs 指令的作用是?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/08 用户和权限管理指令: 请简述 Linux 权限划分的原则?.md.html">08 用户和权限管理指令: 请简述 Linux 权限划分的原则?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/09 Linux 中的网络指令:如何查看一个域名有哪些 NS 记录?.md.html">09 Linux 中的网络指令:如何查看一个域名有哪些 NS 记录?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/10 软件的安装: 编译安装和包管理器安装有什么优势和劣势?.md.html">10 软件的安装: 编译安装和包管理器安装有什么优势和劣势?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/11 高级技巧之日志分析:利用 Linux 指令分析 Web 日志.md.html">11 高级技巧之日志分析:利用 Linux 指令分析 Web 日志.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/12 高级技巧之集群部署:利用 Linux 指令同时在多台机器部署程序.md.html">12 高级技巧之集群部署:利用 Linux 指令同时在多台机器部署程序.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/12 (1)加餐 练习题详解(二).md.html">12 (1)加餐 练习题详解(二).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/13 操作系统内核Linux 内核和 Windows 内核有什么区别?.md.html">13 操作系统内核Linux 内核和 Windows 内核有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/14 用户态和内核态:用户态线程和内核态线程有什么区别?.md.html">14 用户态和内核态:用户态线程和内核态线程有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/15 中断和中断向量Javajs 等语言为什么可以捕获到键盘输入?.md.html">15 中断和中断向量Javajs 等语言为什么可以捕获到键盘输入?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/16 WinMacUnixLinux 的区别和联系:为什么 Debian 漏洞排名第一还这么多人用?.md.html">16 WinMacUnixLinux 的区别和联系:为什么 Debian 漏洞排名第一还这么多人用?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/16 (1)加餐 练习题详解(三).md.html">16 (1)加餐 练习题详解(三).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/17 进程和线程:进程的开销比线程大在了哪里?.md.html">17 进程和线程:进程的开销比线程大在了哪里?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/18 锁、信号量和分布式锁:如何控制同一时间只有 2 个线程运行?.md.html">18 锁、信号量和分布式锁:如何控制同一时间只有 2 个线程运行?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/19 乐观锁、区块链:除了上锁还有哪些并发控制方法?.md.html">19 乐观锁、区块链:除了上锁还有哪些并发控制方法?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/重学操作系统-完/20 线程的调度:线程调度都有哪些方法?.md.html">20 线程的调度:线程调度都有哪些方法?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/21 哲学家就餐问题:什么情况下会触发饥饿和死锁?.md.html">21 哲学家就餐问题:什么情况下会触发饥饿和死锁?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/22 进程间通信: 进程间通信都有哪些方法?.md.html">22 进程间通信: 进程间通信都有哪些方法?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/23 分析服务的特性:我的服务应该开多少个进程、多少个线程?.md.html">23 分析服务的特性:我的服务应该开多少个进程、多少个线程?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/23 (1)加餐 练习题详解(四).md.html">23 (1)加餐 练习题详解(四).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/24 虚拟内存 :一个程序最多能使用多少内存?.md.html">24 虚拟内存 :一个程序最多能使用多少内存?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/25 内存管理单元: 什么情况下使用大内存分页?.md.html">25 内存管理单元: 什么情况下使用大内存分页?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/26 缓存置换算法: LRU 用什么数据结构实现更合理?.md.html">26 缓存置换算法: LRU 用什么数据结构实现更合理?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/27 内存回收上篇:如何解决内存的循环引用问题?.md.html">27 内存回收上篇:如何解决内存的循环引用问题?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/28 内存回收下篇:三色标记-清除算法是怎么回事?.md.html">28 内存回收下篇:三色标记-清除算法是怎么回事?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/28 (1)加餐 练习题详解(五).md.html">28 (1)加餐 练习题详解(五).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/29 Linux 下的各个目录有什么作用?.md.html">29 Linux 下的各个目录有什么作用?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/30 文件系统的底层实现FAT、NTFS 和 Ext3 有什么区别?.md.html">30 文件系统的底层实现FAT、NTFS 和 Ext3 有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/31 数据库文件系统实例MySQL 中 B 树和 B+ 树有什么区别?.md.html">31 数据库文件系统实例MySQL 中 B 树和 B+ 树有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/32 HDFS 介绍:分布式文件系统是怎么回事?.md.html">32 HDFS 介绍:分布式文件系统是怎么回事?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/32 (1)加餐 练习题详解(六).md.html">32 (1)加餐 练习题详解(六).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/33 互联网协议群TCPIP多路复用是怎么回事.md.html">33 互联网协议群TCPIP多路复用是怎么回事.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/34 UDP 协议UDP 和 TCP 相比快在哪里?.md.html">34 UDP 协议UDP 和 TCP 相比快在哪里?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/35 Linux 的 IO 模式selectpollepoll 有什么区别?.md.html">35 Linux 的 IO 模式selectpollepoll 有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/36 公私钥体系和网络安全:什么是中间人攻击?.md.html">36 公私钥体系和网络安全:什么是中间人攻击?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/36 (1)加餐 练习题详解(七).md.html">36 (1)加餐 练习题详解(七).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/37 虚拟化技术介绍VMware 和 Docker 的区别?.md.html">37 虚拟化技术介绍VMware 和 Docker 的区别?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/38 容器编排技术:如何利用 K8s 和 Docker Swarm 管理微服务?.md.html">38 容器编排技术:如何利用 K8s 和 Docker Swarm 管理微服务?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/39 Linux 架构优秀在哪里.md.html">39 Linux 架构优秀在哪里.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/40 商业操作系统:电商操作系统是不是一个噱头?.md.html">40 商业操作系统:电商操作系统是不是一个噱头?.md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/40 (1)加餐 练习题详解(八).md.html">40 (1)加餐 练习题详解(八).md.html</a>
</li>
<li>
<a href="/专栏/重学操作系统-完/41 结束语 论程序员的发展——信仰、选择和博弈.md.html">41 结束语 论程序员的发展——信仰、选择和博弈.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>20 线程的调度:线程调度都有哪些方法?</h1>
<p><strong>这一讲我带来的面试题目是:线程调度都有哪些方法</strong></p>
<p>所谓<strong>调度</strong>,是一个制定计划的过程,放在线程调度背景下,就是操作系统如何决定未来执行哪些线程?</p>
<p>这类型的题目考察的并不是一个死的概念,面试官会通过你的回答考量你对知识进行加工和理解的能力。这有点类似于设计技术方案,要对知识进行系统化、结构化地思考和分类。就这道题目而言,可以抓两条主线,第一条是形形色色调度场景怎么来的?第二条是每个调度算法是如何工作的?</p>
<h3>先到先服务</h3>
<p>早期的操作系统是一个个处理作业Job比如很多保险业务每处理一个称为一个作业Job。处理作业最容易想到的就是<strong>先到先服务First Come First ServiceFCFS</strong>,也就是先到的作业先被计算,后到的作业,排队进行。</p>
<p>这里需要用到一个叫作队列的数据结构,具有<strong>先入先出First In First OutFIFO性质</strong>。先进入队列的作业,先处理,因此从<strong>公平性</strong>来说,这个算法非常朴素。另外,一个作业完全完成才会进入下一个作业,作业之间不会发生切换,从<strong>吞吐量</strong>上说,是最优的——因为没有额外开销。</p>
<p>但是这样对于等待作业的用户来说,是有问题的。比如一笔需要用时 1 天的作业 ,如果等待了 10 分钟,用户是可以接受的;一个用时 10 分钟的作业,用户等待一天就要投诉了。 因此如果用时 1 天的作业先到,用时 10 分钟的任务后到,应该优先处理用时少的,也就是<strong>短作业优先Shortest Job FirstSJF</strong></p>
<h3>短作业优先</h3>
<p>通常会同时考虑到来顺序和作业预估时间的长短,比如下面的到来顺序和预估时间:</p>
<p><img src="assets/Ciqc1F-uUwyAXKj6AABwvcEuVH0735.png" alt="Lark20201113-173325.png" /></p>
<p>这样就会优先考虑第一个到来预估时间为 3 分钟的任务。 我们还可以从另外一个角度来审视短作业优先的优势,就是平均等待时间。</p>
<p><strong>平均等待时间 = 总等待时间/任务数</strong></p>
<p>上面例子中,如果按照 3,3,10 的顺序处理,平均等待时间是:(0 + 3 + 6) / 3 = 3 分钟。 如果按照 10,3,3 的顺序来处理,就是( 0+10+13 )/ 3 = 7.66 分钟。</p>
<p>平均等待时间和用户满意度是成反比的,等待时间越长,用户越不满意,因此<strong>在大多数情况下,应该优先处理用时少的,从而降低平均等待时长</strong></p>
<p>采用 FCFS 和 SJF 后,还有一些问题没有解决。</p>
<ol>
<li>紧急任务如何插队?比如老板安排的任务。</li>
<li>等待太久的任务如何插队?比如用户等太久可能会投诉。</li>
<li>先执行的大任务导致后面来的小任务没有执行如何处理?比如先处理了一个 1 天才能完成的任务,工作半天后才发现预估时间 1 分钟的任务也到来了。</li>
</ol>
<p>为了解决上面的问题,我们设计了两种方案, 一种是优先级队列PriorityQueue另一种是抢占Preemption</p>
<h3>优先级队列PriorityQueue</h3>
<p>刚才提到老板安排的任务需要紧急插队,那么下一个作业是不是应该安排给老板?毫无疑问肯定是这样!那么如何控制这种优先级顺序呢?一种方法是用优先级队列。优先级队列可以给队列中每个元素一个优先级,优先级越高的任务就会被先执行。</p>
<p>优先级队列的一种实现方法就是用到了堆Heap这种数据结构更最简单的实现方法就是每次扫描一遍整个队列找到优先级最高的任务。也就是说Heap可以帮助你在 O(1) 的时间复杂度内查找到最大优先级的元素。</p>
<p>比如老板的任务,就给一个更高的优先级。 而对于普通任务,可以在<strong>等待时间W</strong><strong>预估执行时间P</strong> 中,找一个数学关系来描述。比如:优先级 = W/P。W 越大,或者 P 越小,就越排在前面。 当然还可以有很多其他的数学方法,利用对数计算,或者某种特别的分段函数。</p>
<p>这样,关于紧急任务如何插队?等待太久的任务如何插队?这两个问题我们都解决了,接下来我们来看先执行的大任务导致后面来的小任务没有执行的情况如何处理?</p>
<h3>抢占</h3>
<p>为了解决这个问题,我们需要用到<strong>抢占Preemption</strong></p>
<p>抢占就是把执行能力分时,分成时间片段。 让每个任务都执行一个时间片段。如果在时间片段内,任务完成,那么就调度下一个任务。如果任务没有执行完成,则中断任务,让任务重新排队,调度下一个任务。</p>
<p>拥有了抢占的能力,再结合之前我们提到的优先级队列能力,这就构成了一个基本的线程调度模型。线程相对于操作系统是排队到来的,操作系统为每个到来的线程分配一个优先级,然后把它们放入一个优先级队列中,优先级最高的线程下一个执行。</p>
<p><img src="assets/CgqCHl-uUx2AZFakAACjU3Bi2eE649.png" alt="Lark20201113-173328.png" /></p>
<p>每个线程执行一个时间片段,然后每次执行完一个线程就执行一段调度程序。</p>
<p><img src="assets/Ciqc1F-uUyaAUVSDAAB3mZmSb3A937.png" alt="Lark20201113-173330.png" /></p>
<p>图中用红色代表调度程序,其他颜色代表被调度线程的时间片段。调度程序可以考虑实现为一个单线程模型,这样不需要考虑竞争条件。</p>
<p>上面这个模型已经是一个非常优秀的方案了,但是还有一些问题可以进一步处理得更好。</p>
<ol>
<li>如果一个线程优先级非常高,其实没必要再抢占,因为无论如何调度,下一个时间片段还是给它。那么这种情况如何实现?</li>
<li>如果希望实现最短作业优先的抢占,就必须知道每个线程的执行时间,而这个时间是不可预估的,那么这种情况又应该如何处理?</li>
</ol>
<p>为了解决上面两个问题,我们可以考虑引入多级队列模型。</p>
<h3>多级队列模型</h3>
<p>多级队列,就是多个队列执行调度。 我们先考虑最简单的两级模型,如图:</p>
<p><img src="assets/CgqCHl-uUzCAVhhzAAFSttJfDs4355.png" alt="Lark20201113-173333.png" /></p>
<p>上图中设计了两个优先级不同的队列,从下到上优先级上升,上层队列调度紧急任务,下层队列调度普通任务。只要上层队列有任务,下层队列就会让出执行权限。</p>
<ul>
<li>低优先级队列可以考虑抢占 + 优先级队列的方式实现,这样每次执行一个时间片段就可以判断一下高优先级的队列中是否有任务。</li>
<li>高优先级队列可以考虑用非抢占(每个任务执行完才执行下一个)+ 优先级队列实现,这样紧急任务优先级有个区分。如果遇到十万火急的情况,就可以优先处理这个任务。</li>
</ul>
<p>上面这个模型虽然解决了任务间的优先级问题,但是还是没有解决短任务先行的问题。可以考虑再增加一些队列,让级别更多。比如下图这个模型:</p>
<p><img src="assets/Ciqc1F-uUzqAMYY-AADMHX-2Dso456.png" alt="Lark20201113-173318.png" /></p>
<p>紧急任务仍然走高优队列,非抢占执行。普通任务先放到优先级仅次于高优任务的队列中,并且只分配很小的时间片;如果没有执行完成,说明任务不是很短,就将任务下调一层。下面一层,最低优先级的队列中时间片很大,长任务就有更大的时间片可以用。通过这种方式,短任务会在更高优先级的队列中执行完成,长任务优先级会下调,也就类似实现了最短作业优先的问题。</p>
<p>实际操作中,可以有 n 层,一层层把大任务筛选出来。 最长的任务,放到最闲的时间去执行。要知道,大部分时间 CPU 不是满负荷的。</p>
<h3>总结</h3>
<p><strong>那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:线程调度都有哪些方法</strong></p>
<p><strong>【解析】</strong> 回答这个问题你要把握主线,千万不要教科书般的回答:任务调度分成抢占和非抢占的,抢占的可以轮流执行,也可以用优先级队列执行;非抢占可以先到先服务,也可以最短任务优先。</p>
<p>上面这种回答可以用来过普通的程序员岗位,但是面试官其实更希望听到你的见解,这是初中级开发人员与高级开发人员之间的差异。</p>
<p>比如你告诉面试官:非抢占的先到先服务的模型是最朴素的,公平性和吞吐量可以保证。但是因为希望减少用户的平均等待时间,操作系统往往需要实现抢占。操作系统实现抢占,仍然希望有优先级,希望有最短任务优先。</p>
<p>但是这里有个困难,操作系统无法预判每个任务的预估执行时间,就需要使用分级队列。最高优先级的任务可以考虑非抢占的优先级队列。 其他任务放到分级队列模型中执行,从最高优先级时间片段最小向最低优先级时间片段最大逐渐沉淀。这样就同时保证了小任务先行和高优任务最先执行。</p>
<p>以上的回答,并不是一种简单的概括,还包含了你对问题的理解和认知。在面试时,正确性并不是唯一的考量指标,面试官更看重候选人的思维能力。这也是为什么很多人面试问题都答上来了,仍然没有拿到 offer 的原因。如果面试目标是正确性,为什么不让你开卷考试呢? 上维基百科看不是更正确吗?</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/重学操作系统-完/19 乐观锁、区块链:除了上锁还有哪些并发控制方法?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/重学操作系统-完/21 哲学家就餐问题:什么情况下会触发饥饿和死锁?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997d7abc493cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>