mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-10 00:41:37 +08:00
Compare commits
3 Commits
51e4380716
...
932a7b0bb7
Author | SHA1 | Date | |
---|---|---|---|
|
932a7b0bb7 | ||
|
5926c56dba | ||
|
148ef34874 |
@ -132,7 +132,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
|
||||
|
||||
### 操作系统
|
||||
|
||||
- [操作系统常见问题总结!](./docs/cs-basics/operating-system/operating-system-basic-questions-01.md)
|
||||
- [操作系统常见面试题总结(上)](./docs/cs-basics/operating-system/operating-system-basic-questions-01.md)
|
||||
- [操作系统常见面试题总结(下)](./docs/cs-basics/operating-system/operating-system-basic-questions-02.md)
|
||||
- [后端程序员必备的 Linux 基础知识总结](./docs/cs-basics/operating-system/linux-intro.md)
|
||||
- [Shell 编程基础知识总结](./docs/cs-basics/operating-system/shell-intro.md)
|
||||
|
||||
|
@ -190,6 +190,7 @@ export const sidebarConfig = sidebar({
|
||||
icon: "caozuoxitong",
|
||||
children: [
|
||||
"operating-system-basic-questions-01",
|
||||
"operating-system-basic-questions-02",
|
||||
"linux-intro",
|
||||
"shell-intro",
|
||||
],
|
||||
|
@ -16,6 +16,7 @@ export const aboutTheAuthor = [
|
||||
icon: "chat",
|
||||
collapsable: false,
|
||||
children: [
|
||||
"writing-technology-blog-six-years",
|
||||
"my-article-was-stolen-and-made-into-video-and-it-became-popular",
|
||||
"dog-that-copies-other-people-essay",
|
||||
"zhishixingqiu-two-years",
|
||||
|
173
docs/about-the-author/writing-technology-blog-six-years.md
Normal file
173
docs/about-the-author/writing-technology-blog-six-years.md
Normal file
@ -0,0 +1,173 @@
|
||||
---
|
||||
title: 坚持写技术博客六年了!
|
||||
category: 走近作者
|
||||
tag:
|
||||
- 杂谈
|
||||
---
|
||||
|
||||
坚持写技术博客已经有六年了,也算是一个小小的里程碑了。
|
||||
|
||||
一开始,我写技术博客就是简单地总结自己课堂上学习的课程比如网络、操作系统。渐渐地,我开始撰写一些更为系统化的知识点详解和面试常见问题总结。
|
||||
|
||||

|
||||
|
||||
许多人都想写技术博客,但却不清楚这对他们有何好处。有些人开始写技术博客,却不知道如何坚持下去,也不知道该写些什么。这篇文章我会认真聊聊我对记录技术博客的一些看法和心得,或许可以帮助你解决这些问题。
|
||||
|
||||
## 写技术博客有哪些好处?
|
||||
|
||||
### 学习效果更好,加深知识点的认识
|
||||
|
||||
**费曼学习法** 大家应该已经比较清楚了,这是一个经过实践证明非常有效的学习方式。费曼学习法的命名源自 Richard Feynman,这位物理学家曾获得过诺贝尔物理学奖,也曾参与过曼哈顿计划。
|
||||
|
||||
所谓费曼学习法,就是当你学习了一个新知识之后,想象自己是一个老师:用最简单、最浅显直白的话复述、表达复杂深奥的知识,最好不要使用行业术语,让非行业内的人也能听懂。为了达到这种效果,最好想象你是在给一个 80 多岁或 8 岁的小孩子上课,甚至他们都能听懂。
|
||||
|
||||

|
||||
|
||||
看书、看视频这类都属于是被动学习,学习效果比较差。费曼学习方法属于主动学习,学习效果非常好。
|
||||
|
||||
**写技术博客实际就是教别人的一种方式。** 不过,记录技术博客的时候是可以有专业术语(除非你的文章群体是非技术人员),只是你需要用自己的话表述出来,尽量让别人一看就懂。**切忌照搬书籍或者直接复制粘贴其他人的总结!**
|
||||
|
||||
如果我们被动的学习某个知识点,可能大部分时候都是仅仅满足自己能够会用的层面,你并不会深究其原理,甚至很多关键概念都没搞懂。
|
||||
|
||||
如果你是要将你所学到的知识总结成一篇博客的话,一定会加深你对这个知识点的思考。很多时候,你为了将一个知识点讲清楚,你回去查阅很多资料,甚至需要查看很多源码,这些细小的积累在潜移默化中加深了你对这个知识点的认识。
|
||||
|
||||
甚至,我还经常会遇到这种情况:**写博客的过程中,自己突然意识到自己对于某个知识点的理解存在错误。**
|
||||
|
||||
**写博客本身就是一个对自己学习到的知识进行总结、回顾、思考的过程。记录博客也是对于自己学习历程的一种记录。随着时间的流逝、年龄的增长,这又何尝不是一笔宝贵的精神财富呢?**
|
||||
|
||||
知识星球的一位球友还提到写技术博客有助于完善自己的知识体系:
|
||||
|
||||

|
||||
|
||||
### 帮助别人的同时获得成就感
|
||||
|
||||
就像我们程序员希望自己的产品能够得到大家的认可和喜欢一样。我们写技术博客在某一方面当然也是为了能够得到别人的认可。
|
||||
|
||||
**当你写的东西对别人产生帮助的时候,你会产生成就感和幸福感。**
|
||||
|
||||

|
||||
|
||||
这种成就感和幸福感会作为 **正向反馈** ,继续激励你写博客。
|
||||
|
||||
但是,即使受到很多读者的赞赏,也要保持谦虚学习的太多。人外有人,比你技术更厉害的读者多了去,一定要虚心学习!
|
||||
|
||||
当然,你可以可能会受到很多非议。可能会有很多人说你写的文章没有深度,还可能会有很多人说你闲的蛋疼,你写的东西网上/书上都有。
|
||||
|
||||
**坦然对待这些非议,做好自己,走好自己的路就好!用行动自证!**
|
||||
|
||||
### 可能会有额外的收入
|
||||
|
||||
写博客可能还会为你带来经济收入。输出价值的同时,还能够有合理的经济收入,这是最好的状态!
|
||||
|
||||
为什么说是可能呢? **因为就目前来看,大部分人还是很难短期通过写博客有收入。我也不建议大家一开始写博客就奔着赚钱的目的,这样功利性太强了,效果可能反而不好。就比如说你坚持了写了半年发现赚不到钱,那你可能就会坚持不下去了。**
|
||||
|
||||
我自己从大二开始写博客,大三下学期开始将自己的文章发布到公众号上,一直到大四下学期,才通过写博客赚到属于自己的第一笔钱。
|
||||
|
||||
第一笔钱是通过微信公众号接某培训机构的推广获得的。没记错的话,当时通过这个推广为自己带来了大约 **500** 元的收入。虽然这不是很多,但对于还在上大学的我来说,这笔钱非常宝贵。那时我才知道,原来写作真的可以赚钱,这也让我更有动力去分享自己的写作。可惜的是,在接了两次这家培训机构的广告之后,它就倒闭了。
|
||||
|
||||
之后,很长一段时间我都没有接到过广告。直到网易的课程合作找上门,一篇文章 1000 元,每个月接近一篇,发了接近两年,这也算是我在大学期间比较稳定的一份收入来源了。
|
||||
|
||||

|
||||
|
||||
老粉应该大部分都是通过 JavaGuide 这个项目认识我的,这是我在大三开始准备秋招面试时创建的一个项目。没想到这个项目竟然火了一把,一度霸占了 Github 榜单。可能当时国内这类开源文档教程类项目太少了,所以这个项目受欢迎程度非常高。
|
||||
|
||||

|
||||
|
||||
项目火了之后,有一个国内比较大的云服务公司找到我,说是要赞助 JavaGuide 这个项目。我既惊又喜,担心别人是骗子,反复确认合同之后,最终确定以每月 1000 元的费用在我的项目首页加上对方公司的 banner。
|
||||
|
||||
随着时间的推移,以及自己后来写了一些比较受欢迎、比较受众的文章,我的博客知名度也有所提升,通过写博客的收入也增加了不少。
|
||||
|
||||
### 增加个人影响力
|
||||
|
||||
写技术博客是一种展示自己技术水平和经验的方式,能够让更多的人了解你的专业领域知识和技能。持续分享优质的技术文章,一定能够在技术领域增加个人影响力,这一点是毋庸置疑的。
|
||||
|
||||
有了个人影响力之后,不论是对你后面找工作,还是搞付费知识分享或者出书,都非常有帮助。
|
||||
|
||||
拿我自己来说,已经很多知名出版社的编辑找过我,协商出一本的书的事情。这种机会应该也是很多人梦寐以求的。不过,我都一一拒绝了,因为觉得自己远远没有达到能够写书的水平。
|
||||
|
||||

|
||||
|
||||
其实不出书最主要的原因还是自己嫌麻烦,整个流程的事情太多了。我自己又是比较佛系随性的人,平时也不想把时间都留给工作。
|
||||
|
||||
## 怎样才能坚持写技术博客?
|
||||
|
||||
**不可否认,人都是有懒性的,这是人的本性。我们需要一个目标/动力来 Push 一下自己。**
|
||||
|
||||
就技术写作而言,你的目标可以以技术文章的数量为标准,比如:
|
||||
|
||||
- 一年写多少篇技术文章。我个人觉得一年的范围还是太长了,不太容易定一个比较合适的目标。
|
||||
- 每月输出一篇高质量的技术文章。这个相对容易实现一些,每月一篇,一年也有十二篇了,也很不错了。
|
||||
|
||||
不过,以技术文章的数量为目标有点功利化,文章的质量同样很重要。一篇高质量的技术文可能需要花费一周甚至半个月的业余时间才能写完。一定要避免自己刻意追求数量,而忽略质量,迷失技术写作的本心。
|
||||
|
||||
我个人给自己定的目标是:**每个月至少写一篇原创技术文章或者认真修改完善过去写的三篇技术文章** (像开源项目推荐、开源项目学习、个人经验分享、面经分享等等类型的文章不会被记入)。
|
||||
|
||||
我的目标对我来说比较容易完成,因此不会出现为了完成目标而应付任务的情况。在我状态比较好,工作也不是很忙的时候,还会经常超额完成任务。下图是我今年 3 月份完成的任务(任务管理工具:Microsoft To-Do)。除了 gossip 协议是去年写的之外,其他都是 3 月份完成的。
|
||||
|
||||

|
||||
|
||||
如果觉得以文章数量为标准过于功利的话,也可以比较随性地按照自己的节奏来写作。不过,一般这种情况下,你很可能过段时间就忘了还有这件事,开始慢慢抵触写博客。
|
||||
|
||||
写完一篇技术文章之后,我们不光要同步到自己的博客,还要分发到国内一些常见的技术社区比如博客园、掘金。**分发到其他平台的原因是获得关注进而收获正向反馈(动力来源之一)与建议,这是技术写作能坚持下去的非常重要的一步,一定要重视!!!**
|
||||
|
||||
说实话,当你写完一篇自认为还不错的文章的幸福感和成就感还是有的。**但是,让自己去做这件事情还是比较痛苦的。** 就好比你让自己出去玩很简单,为了达到这个目的,你可以有各种借口。但是,想要自己老老实实学习,还是需要某个外力来督促自己的。
|
||||
|
||||
## 写哪些方向的博客比较好?
|
||||
|
||||
通常来说,写下面这些方向的博客会比较好:
|
||||
|
||||
1. **详细讲解某个知识点** :一定要有自己的思考而不是东拼西凑。不仅要介绍知识点的基本概念和原理,还需要适当结合实际案例和应用场景进行举例说明。
|
||||
2. **问题排查/性能优化经历** :需要详细描述清楚具体的场景以及解决办法。一定要有足够的细节描述,包括出现问题的具体场景、问题的根本原因、解决问题的思路和具体步骤等等。同时,要注重实践性和可操作性,帮助读者更好地学习理解。
|
||||
3. **源码阅读记录** :从一个功能点出发描述其底层源码实现,谈谈你从源码中学到了什么。
|
||||
|
||||
最重要的是一定要重视 Markdown 规范,不然内容再好也会显得不专业。
|
||||
|
||||
Markdown 规范请参考:**https://javaguide.cn/javaguide/contribution-guideline.html** (很重要,尽量按照规范来,对你工作中写文档会非常有帮助)
|
||||
|
||||
## 有没有什么写作技巧分享?
|
||||
|
||||
### 句子不要过长
|
||||
|
||||
句子不要过长,尽量使用短句(但也不要太短),这样读者更容易阅读和理解。
|
||||
|
||||
### 尽量让文章更加生动有趣
|
||||
|
||||
尽量让文章更加生动有趣,比如你可以适当举一些形象的例子、用一些有趣的段子、歇后语或者网络热词。
|
||||
|
||||
不过,这个也主要看你的文章风格。
|
||||
|
||||
### 使用简单明了的语言
|
||||
|
||||
避免使用阅读者可能无法理解的行话或复杂语言。
|
||||
|
||||
注重清晰度和说服力,保持简单。简单的写作是有说服力的,一个五句话的好论点会比一百句话的精彩论点更能打动人。为什么格言、箴言这类文字容易让人接受,与简洁、直白也有些关系。
|
||||
|
||||
### 使用视觉效果
|
||||
|
||||
图表、图像等视觉效果可以让朴素的文本内容更容易理解。记得在适当的地方使用视觉效果来增强你的文章的表现力。
|
||||
|
||||

|
||||
|
||||
### 技术文章配图色彩要鲜明
|
||||
|
||||
下面是同样内容的两张图,都是通过 drawio 画的,小伙伴们更喜欢哪一张呢?
|
||||
|
||||
我相信大部分小伙伴都会选择后面一个色彩更鲜明的!
|
||||
|
||||
色彩的调整不过花费了我不到 30s 的时间,带来的阅读体验的上升却是非常之大!
|
||||
|
||||

|
||||
|
||||
### 确定你的读者
|
||||
|
||||
写作之前,思考一下你的文章的主要受众全体是谁。受众群体确定之后,你可以根据受众的需求和理解水平调整你的写作风格和内容难易程度。
|
||||
|
||||
### 审查和修改
|
||||
|
||||
在发表之前一定要审查和修改你的文章。这将帮助你发现错误、澄清任何令人困惑的信息并提高文档的整体质量。
|
||||
|
||||
**好文是改出来的,切记!!!**
|
||||
|
||||
## 总结
|
||||
|
||||
总的来说,写技术博客是一件利己利彼的事情。你可能会从中收获到很多东西,你写的东西也可能对别人也有很大的帮助。但是,写技术博客还是比较耗费自己时间的,你需要和工作以及生活做好权衡。
|
@ -1 +1 @@
|
||||
<mxfile host="Electron" modified="2022-08-21T07:59:47.276Z" agent="5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.4.5 Chrome/83.0.4103.122 Electron/9.1.0 Safari/537.36" etag="QasVtwjBaJyNZ0lqSJkR" version="13.4.5" type="device"><diagram id="tmrIvn8UqugqhCCRZjQR" name="Page-1">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</diagram></mxfile>
|
||||
<mxfile host="Electron" modified="2023-04-01T13:53:49.741Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="tIdtzWxSp10nsOGoVtAv" version="20.3.0" type="device"><diagram id="tmrIvn8UqugqhCCRZjQR" name="Page-1">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</diagram></mxfile>
|
@ -12,118 +12,6 @@ head:
|
||||
|
||||
简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。
|
||||
|
||||
## 从认识操作系统开始
|
||||
|
||||

|
||||
|
||||
正式开始 Linux 之前,简单花一点点篇幅科普一下操作系统相关的内容。
|
||||
|
||||
### 操作系统简介
|
||||
|
||||
我通过以下四点介绍什么是操作系统:
|
||||
|
||||
1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。**
|
||||
2. **操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。** 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。
|
||||
3. **操作系统存在屏蔽了硬件层的复杂性。** 操作系统就像是硬件使用的负责人,统筹着各种相关事项。
|
||||
4. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理**。
|
||||
|
||||
> 内核(Kernel)在后文中会提到。
|
||||
|
||||

|
||||
|
||||
### 操作系统简单分类
|
||||
|
||||
#### Windows
|
||||
|
||||
目前最流行的个人桌面操作系统 ,不做多的介绍,大家都清楚。界面简单易操作,软件生态非常好。
|
||||
|
||||
_玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Windows 用于玩游戏,一台 Mac 用于平时日常开发和学习使用。_
|
||||
|
||||

|
||||
|
||||
#### Unix
|
||||
|
||||
最早的多用户、多任务操作系统 。后面崛起的 Linux 在很多方面都参考了 Unix。
|
||||
|
||||
目前这款操作系统已经逐渐逐渐退出操作系统的舞台。
|
||||
|
||||

|
||||
|
||||
#### Linux
|
||||
|
||||
**Linux 是一套免费使用、开源的类 Unix 操作系统。** Linux 存在着许多不同的发行版本,但它们都使用了 **Linux 内核** 。
|
||||
|
||||
> 严格来讲,Linux 这个词本身只表示 Linux 内核,在 GNU/Linux 系统中,Linux 实际就是 Linux 内核,而该系统的其余部分主要是由 GNU 工程编写和提供的程序组成。单独的 Linux 内核并不能成为一个可以正常工作的操作系统。
|
||||
>
|
||||
> **很多人更倾向使用 “GNU/Linux” 一词来表达人们通常所说的 “Linux”。**
|
||||
|
||||

|
||||
|
||||
#### Mac OS
|
||||
|
||||
苹果自家的操作系统,编程体验和 Linux 相当,但是界面、软件生态以及用户体验各方面都要比 Linux 操作系统更好。
|
||||
|
||||

|
||||
|
||||
### 操作系统的内核(Kernel)
|
||||
|
||||
我们先来看看维基百科对于内核的解释,我觉得总结的非常好!
|
||||
|
||||
> **内核**(英语:Kernel,又称核心)在计算机科学中是一个用来管理软件发出的数据 I/O(输入与输出)要求的电脑程序,将这些要求转译为数据处理的指令并交由中央处理器(CPU)及电脑中其他电子组件进行处理,是现代操作系统中最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并由内核决定一个程序在什么时候对某部分硬件操作多长时间。 **直接对硬件操作是非常复杂的。所以内核通常提供一种硬件抽象的方法,来完成这些操作。有了这个,通过进程间通信机制及系统调用,应用进程可间接控制所需的硬件资源(特别是处理器及 IO 设备)。**
|
||||
>
|
||||
> 早期计算机系统的设计中,还没有操作系统的内核这个概念。随着计算机系统的发展,操作系统内核的概念才渐渐明晰起来了!
|
||||
|
||||
简单概括两点:
|
||||
|
||||
1. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。**
|
||||
2. **操作系统的内核是连接应用程序和硬件的桥梁,决定着操作系统的性能和稳定性。**
|
||||
|
||||
### 中央处理器(CPU,Central Processing Unit)
|
||||
|
||||
关于 CPU 简单概括三点:
|
||||
|
||||
1. **CPU 是一台计算机的运算核心(Core)+控制核心( Control Unit),可以称得上是计算机的大脑。**
|
||||
2. **CPU 主要包括两个部分:控制器+运算器。**
|
||||
3. **CPU 的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。**
|
||||
|
||||
### CPU vs Kernel(内核)
|
||||
|
||||
很多人容易无法区分操作系统的内核(Kernel)和中央处理器(CPU),你可以简单从下面两点来区别:
|
||||
|
||||
1. 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
|
||||
2. CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。
|
||||
|
||||
下图清晰说明了应用程序、内核、CPU 这三者的关系。
|
||||
|
||||

|
||||
|
||||
### 系统调用
|
||||
|
||||
介绍系统调用之前,我们先来了解一下用户态和系统态。
|
||||
|
||||
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
|
||||
|
||||
1. **用户态(user mode)** : 用户态运行的进程或可以直接读取用户程序的数据。
|
||||
2. **系统态(kernel mode)**: 可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
|
||||
|
||||
**说了用户态和系统态之后,那么什么是系统调用呢?**
|
||||
|
||||
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!
|
||||
|
||||
也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
|
||||
|
||||
这些系统调用按功能大致可分为如下几类:
|
||||
|
||||
- **设备管理** :完成设备的请求或释放,以及设备启动等功能。
|
||||
- **文件管理** :完成文件的读、写、创建及删除等功能。
|
||||
- **进程控制** :完成进程的创建、撤销、阻塞及唤醒等功能。
|
||||
- **进程通信** :完成进程之间的消息传递或信号传递等功能。
|
||||
- **内存管理** :完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
|
||||
|
||||
我在网上找了一个图,通过这个图可以很清晰的说明用户程序、系统调用、内核和硬件之间的关系。(_太难了~木有自己画_)
|
||||
|
||||

|
||||
|
||||
## 初探 Linux
|
||||
|
||||
### Linux 简介
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 操作系统常见面试题总结
|
||||
title: 操作系统常见面试题总结(上)
|
||||
category: 计算机基础
|
||||
tag:
|
||||
- 操作系统
|
||||
@ -14,8 +14,6 @@ head:
|
||||
|
||||
很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如**进程管理**、**内存管理**、**虚拟内存**等等。
|
||||
|
||||
文章形式通过大部分同学比较喜欢的面试官和求职者之间的对话形式展开。另外,我也只是在大学的时候学习过操作系统,不过基本都忘了,为了写这篇文章这段时间看了很多相关的书籍和博客。
|
||||
|
||||
这篇文章只是对一些操作系统比较重要概念的一个概览,深入学习的话,建议大家还是老老实实地去看书。另外, 这篇文章的很多内容参考了《现代操作系统》第三版这本书,非常感谢。
|
||||
|
||||
开始本文的内容之前,我们先聊聊为什么要学习操作系统。
|
||||
@ -25,69 +23,208 @@ head:
|
||||
|
||||
**简单来说,学习操作系统能够提高自己思考的深度以及对技术的理解力,并且,操作系统方面的知识也是面试必备。**
|
||||
|
||||
关于如何学习操作系统,可以看这篇回答:[https://www.zhihu.com/question/270998611/answer/1640198217](https://www.zhihu.com/question/270998611/answer/1640198217)。
|
||||
|
||||
## 操作系统基础
|
||||
|
||||
面试官顶着蓬松的假发向我走来,只见他一手拿着厚重的 Thinkpad ,一手提着他那淡黄的长裙。
|
||||

|
||||
|
||||
### 什么是操作系统?
|
||||
|
||||
👨💻**面试官** : 先来个简单问题吧!**什么是操作系统?**
|
||||
通过以下四点可以概括操作系统到底是什么:
|
||||
|
||||
🙋 **我** :我通过以下四点向您介绍一下什么是操作系统吧!
|
||||
1. 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。
|
||||
2. 操作系统本质上是一个运行在计算机上的软件程序 ,主要用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。
|
||||
3. 操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使用的负责人,统筹着各种相关事项。
|
||||
4. 操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。
|
||||
|
||||
1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。**
|
||||
2. **操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。** 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。
|
||||
3. **操作系统存在屏蔽了硬件层的复杂性。** 操作系统就像是硬件使用的负责人,统筹着各种相关事项。
|
||||
4. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理**。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。
|
||||
很多人容易把操作系统的内核(Kernel)和中央处理器(CPU,Central Processing Unit)弄混。你可以简单从下面两点来区别:
|
||||
|
||||
1. 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
|
||||
2. CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。
|
||||
|
||||
下图清晰说明了应用程序、内核、CPU 这三者的关系。
|
||||
|
||||

|
||||
|
||||
### 系统调用
|
||||
|
||||
👨💻**面试官** :**什么是系统调用呢?** 能不能详细介绍一下。
|
||||
|
||||
🙋 **我** :介绍系统调用之前,我们先来了解一下用户态和系统态。
|
||||
### 操作系统主要有哪些功能?
|
||||
|
||||
从资源管理的角度来看,操作系统有 6 大功能:
|
||||
|
||||
1. **进程和线程的管理** :进程的创建、撤销、阻塞、唤醒,进程间的通信等。
|
||||
2. **存储管理** :内存的分配和管理、外存(磁盘等)的分配和管理等。
|
||||
3. **文件管理** :文件的读、写、创建及删除等。
|
||||
4. **设备管理** :完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。
|
||||
5. **网络管理** :操作系统负责管理计算机网络的使用。网络是计算机系统中连接不同计算机的方式,操作系统需要管理计算机网络的配置、连接、通信和安全等,以提供高效可靠的网络服务。
|
||||
6. **安全管理** :用户的身份认证、访问控制、文件加密等,以防止非法用户对系统资源的访问和操作。
|
||||
|
||||
### 常见的操作系统有哪些?
|
||||
|
||||
#### Windows
|
||||
|
||||
目前最流行的个人桌面操作系统 ,不做多的介绍,大家都清楚。界面简单易操作,软件生态非常好。
|
||||
|
||||
_玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Windows 用于玩游戏,一台 Mac 用于平时日常开发和学习使用。_
|
||||
|
||||

|
||||
|
||||
#### Unix
|
||||
|
||||
最早的多用户、多任务操作系统 。后面崛起的 Linux 在很多方面都参考了 Unix。
|
||||
|
||||
目前这款操作系统已经逐渐逐渐退出操作系统的舞台。
|
||||
|
||||

|
||||
|
||||
#### Linux
|
||||
|
||||
**Linux 是一套免费使用、开源的类 Unix 操作系统。** Linux 存在着许多不同的发行版本,但它们都使用了 **Linux 内核** 。
|
||||
|
||||
> 严格来讲,Linux 这个词本身只表示 Linux 内核,在 GNU/Linux 系统中,Linux 实际就是 Linux 内核,而该系统的其余部分主要是由 GNU 工程编写和提供的程序组成。单独的 Linux 内核并不能成为一个可以正常工作的操作系统。
|
||||
>
|
||||
> **很多人更倾向使用 “GNU/Linux” 一词来表达人们通常所说的 “Linux”。**
|
||||
|
||||

|
||||
|
||||
#### Mac OS
|
||||
|
||||
苹果自家的操作系统,编程体验和 Linux 相当,但是界面、软件生态以及用户体验各方面都要比 Linux 操作系统更好。
|
||||
|
||||

|
||||
|
||||
### 用户态和内核态
|
||||
|
||||
#### 什么是用户态和内核态?
|
||||
|
||||
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
|
||||
|
||||
1. 用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。
|
||||
2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
|
||||
- **用户态(User Mode)** : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
|
||||
- **内核态(Kernel Mode)** :内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
|
||||
|
||||
说了用户态和系统态之后,那么什么是系统调用呢?
|
||||

|
||||
|
||||
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!
|
||||
内核态相比用户态拥有更高的特权级别,因此能够执行更底层、更敏感的操作。不过,由于进入内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查),应该尽量减少进入内核态的次数,以提高系统的性能和稳定性。
|
||||
|
||||
#### 为什么要有用户态和内核态?只有一个内核态不行么?
|
||||
|
||||
- 在 CPU 的所有指令中,有一些指令是比较危险的比如内存分配、设置时钟、IO 处理等,如果所有的程序都能使用这些指令的话,会对系统的正常运行造成灾难性地影响。因此,我们需要限制这些危险指令只能内核态运行。这些只能由操作系统内核态执行的指令也被叫做 **特权指令** 。
|
||||
- 如果计算机系统中只有一个内核态,那么所有程序或进程都必须共享系统资源,例如内存、CPU、硬盘等,这将导致系统资源的竞争和冲突,从而影响系统性能和效率。并且,这样也会让系统的安全性降低,毕竟所有程序或进程都具有相同的特权级别和访问权限。
|
||||
|
||||
因此,同时具有用户态和内核态主要是为了保证计算机系统的安全性、稳定性和性能。
|
||||
|
||||
#### 用户态和内核态是如何切换的?
|
||||
|
||||

|
||||
|
||||
用户态切换到内核态的 3 种方式:
|
||||
|
||||
1. **系统调用(Trap)** :用户态进程 **主动** 要求切换到内核态的一种方式,主要是为了使用内核态才能做的事情比如读取磁盘资源。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现。
|
||||
2. **中断(Interrupt)** :当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
|
||||
3. **异常(Exception)**:当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
|
||||
|
||||
在系统的处理上,中断和异常类似,都是通过中断向量表来找到相应的处理程序进行处理。区别在于,中断来自处理器外部,不是由任何一条专门的指令造成,而异常是执行当前指令的结果。
|
||||
|
||||
### 系统调用
|
||||
|
||||
#### 什么是系统调用?
|
||||
|
||||
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的内核态级别的子功能咋办呢?那就需要系统调用了!
|
||||
|
||||
也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
|
||||
|
||||

|
||||
|
||||
这些系统调用按功能大致可分为如下几类:
|
||||
|
||||
- 设备管理。完成设备的请求或释放,以及设备启动等功能。
|
||||
- 文件管理。完成文件的读、写、创建及删除等功能。
|
||||
- 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
|
||||
- 进程通信。完成进程之间的消息传递或信号传递等功能。
|
||||
- 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
|
||||
- 设备管理:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。
|
||||
- 文件管理:完成文件的读、写、创建及删除等功能。
|
||||
- 进程管理:进程的创建、撤销、阻塞、唤醒,进程间的通信等功能。
|
||||
- 内存管理:完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
|
||||
|
||||
系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核态,而普通的库函数调用由函数库或用户自己提供,运行于用户态。
|
||||
|
||||
总结:系统调用是应用程序与操作系统之间进行交互的一种方式,通过系统调用,应用程序可以访问操作系统底层资源例如文件、设备、网络等。
|
||||
|
||||
#### 系统调用的过程了解吗?
|
||||
|
||||
系统调用的过程可以简单分为以下几个步骤:
|
||||
|
||||
1. 用户态的程序发起系统调用,因为系统调用中涉及一些特权指令(只能由操作系统内核态执行的指令),用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。
|
||||
2. 发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调用。
|
||||
3. 内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
|
||||
|
||||

|
||||
|
||||
## 进程和线程
|
||||
|
||||
### 进程和线程的区别
|
||||
### 什么是进程和线程?
|
||||
|
||||
👨💻**面试官**: 好的!我明白了!那你再说一下: **进程和线程的区别**。
|
||||
- **进程(Process)** 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
|
||||
- **线程(Thread)** 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
|
||||
|
||||
🙋 **我:** 好的! 下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧!
|
||||
### 进程和线程的区别是什么?
|
||||
|
||||
下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧!
|
||||
|
||||

|
||||
|
||||
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
|
||||
|
||||
**总结:** 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
|
||||
**总结:**
|
||||
|
||||
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
|
||||
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
|
||||
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
|
||||
|
||||
### 有了进程为什么还需要线程?
|
||||
|
||||
- 进程切换是一个开销很大的操作,线程切换的成本较低。
|
||||
- 线程更轻量,一个进程可以创建多个线程。
|
||||
- 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
|
||||
- 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
|
||||
|
||||
### 为什么要使用多线程?
|
||||
|
||||
先从总体上来说:
|
||||
|
||||
- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
|
||||
- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
|
||||
|
||||
再深入到计算机底层来探讨:
|
||||
|
||||
- **单核时代**: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
|
||||
- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
|
||||
|
||||
### 线程间的同步的方式有哪些?
|
||||
|
||||
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
|
||||
|
||||
下面是几种常见的线程同步的方式:
|
||||
|
||||
1. **互斥锁(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。
|
||||
2. **读写锁(Read-Write Lock)**:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
|
||||
3. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
|
||||
4. **屏障(Barrier)** :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。
|
||||
5. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
|
||||
|
||||
### PCB 是什么?包含哪些信息?
|
||||
|
||||
**PCB(Process Control Block)** 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。
|
||||
|
||||
当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。
|
||||
|
||||
PCB 主要包含下面几部分的内容:
|
||||
|
||||
- 进程的描述信息,包括进程的名称、标识符等等;
|
||||
- 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
|
||||
- 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
|
||||
- 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
|
||||
- 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
|
||||
- ......
|
||||
|
||||
### 进程有哪几种状态?
|
||||
|
||||
👨💻**面试官** : 那你再说说**进程有哪几种状态?**
|
||||
|
||||
🙋 **我** :我们一般把进程大致分为 5 种状态,这一点和[线程](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md#6-%E8%AF%B4%E8%AF%B4%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%92%8C%E7%8A%B6%E6%80%81)很像!
|
||||
我们一般把进程大致分为 5 种状态,这一点和线程很像!
|
||||
|
||||
- **创建状态(new)** :进程正在被创建,尚未到就绪状态。
|
||||
- **就绪状态(ready)** :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
|
||||
@ -95,43 +232,23 @@ head:
|
||||
- **阻塞状态(waiting)** :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
|
||||
- **结束状态(terminated)** :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
|
||||
|
||||
> 订正:下图中 running 状态被 interrupt 向 ready 状态转换的箭头方向反了。
|
||||

|
||||
|
||||

|
||||
|
||||
### 进程间的通信方式
|
||||
|
||||
👨💻**面试官** :**进程间的通信常见的的有哪几种方式呢?**
|
||||
|
||||
🙋 **我** :大概有 7 种常见的进程间的通信方式。
|
||||
### 进程间的通信方式有哪些?
|
||||
|
||||
> 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74) 这篇文章,推荐阅读,总结的非常不错。
|
||||
|
||||
1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
|
||||
1. **有名管道(Named Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循**先进先出(first in first out)**。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
|
||||
1. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
|
||||
1. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。**
|
||||
1. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
|
||||
1. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
|
||||
1. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
|
||||
2. **有名管道(Named Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循**先进先出(first in first out)**。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
|
||||
3. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
|
||||
4. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。**
|
||||
5. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
|
||||
6. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
|
||||
7. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
|
||||
|
||||
### 线程间的同步的方式
|
||||
### 进程的调度算法有哪些?
|
||||
|
||||
👨💻**面试官** :**那线程间的同步的方式有哪些呢?**
|
||||
|
||||
🙋 **我** :线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式:
|
||||
|
||||
1. **互斥量(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
|
||||
1. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
|
||||
1. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
|
||||
|
||||
### 进程的调度算法
|
||||
|
||||
👨💻**面试官** :**你知道操作系统中进程的调度算法有哪些吗?**
|
||||
|
||||
🙋 **我** :嗯嗯!这个我们大学的时候学过,是一个很重要的知识点!
|
||||
|
||||
为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是:
|
||||
这是一个很重要的知识点!为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是:
|
||||
|
||||
- **先到先服务(FCFS)调度算法** : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
|
||||
- **短作业优先(SJF)的调度算法** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
|
||||
@ -139,22 +256,52 @@ head:
|
||||
- **多级反馈队列调度算法** :前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。
|
||||
- **优先级调度** : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
|
||||
|
||||
### 什么是死锁
|
||||
### 什么是僵尸进程和孤儿进程?
|
||||
|
||||
👨💻**面试官** :**你知道什么是死锁吗?**
|
||||
在 Unix/Linux 系统中,子进程通常是通过 fork()系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。
|
||||
|
||||
🙋 **我** :死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
|
||||
当一个进程调用 exit()系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程对应的 PCB 依然存在于系统中。这些信息只有在父进程调用 wait()或 waitpid()系统调用时才会被释放,以便让父进程得到子进程的状态信息。
|
||||
|
||||
### 死锁的四个必要条件
|
||||
这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。
|
||||
|
||||
👨💻**面试官** :**产生死锁的四个必要条件是什么?**
|
||||
- **僵尸进程** :子进程已经终止,但是其父进程仍在运行,且父进程没有调用 wait()或 waitpid()等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。这种情况下,子进程被称为“僵尸进程”。避免僵尸进程的产生,父进程需要及时调用 wait()或 waitpid()系统调用来回收子进程。
|
||||
- **孤儿进程** :一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。孤儿进程通常是由于父进程意外终止或未及时调用 wait()或 waitpid()等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。
|
||||
|
||||
🙋 **我** :
|
||||
### 如何查看是否有僵尸进程?
|
||||
|
||||
- **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
|
||||
- **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
|
||||
- **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
|
||||
- **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。
|
||||
Linux 下可以使用 Top 命令查找,`zombie` 值表示僵尸进程的数量,为 0 则代表没有僵尸进程。
|
||||
|
||||

|
||||
|
||||
下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:
|
||||
|
||||
```bash
|
||||
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
|
||||
```
|
||||
|
||||
## 死锁
|
||||
|
||||
### 什么是死锁?
|
||||
|
||||
死锁(Deadlock)描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
|
||||
|
||||
### 能列举一个操作系统发生死锁的例子吗?
|
||||
|
||||
假设有两个进程 A 和 B,以及两个资源 X 和 Y,它们的分配情况如下:
|
||||
|
||||
| 进程 | 占用资源 | 需求资源 |
|
||||
| ---- | -------- | -------- |
|
||||
| A | X | Y |
|
||||
| B | Y | X |
|
||||
|
||||
此时,进程 A 占用资源 X 并且请求资源 Y,而进程 B 已经占用了资源 Y 并请求资源 X。两个进程都在等待对方释放资源,无法继续执行,陷入了死锁状态。
|
||||
|
||||
### 产生死锁的四个必要条件是什么?
|
||||
|
||||
1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
|
||||
2. **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
|
||||
3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
|
||||
4. **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。
|
||||
|
||||
**注意 ⚠️** :这四个条件是产生死锁的 **必要条件** ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
|
||||
|
||||
@ -162,6 +309,62 @@ head:
|
||||
|
||||
> 如果没有事物情况 A,则必然没有事物情况 B,也就是说如果有事物情况 B 则一定有事物情况 A,那么 A 就是 B 的必要条件。从逻辑学上看,B 能推导出 A,A 就是 B 的必要条件,等价于 B 是 A 的充分条件。
|
||||
|
||||
### 能写一个模拟产生死锁的代码吗?
|
||||
|
||||
下面通过一个实际的例子来模拟下图展示的线程死锁:
|
||||
|
||||

|
||||
|
||||
```java
|
||||
public class DeadLockDemo {
|
||||
private static Object resource1 = new Object();//资源 1
|
||||
private static Object resource2 = new Object();//资源 2
|
||||
|
||||
public static void main(String[] args) {
|
||||
new Thread(() -> {
|
||||
synchronized (resource1) {
|
||||
System.out.println(Thread.currentThread() + "get resource1");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println(Thread.currentThread() + "waiting get resource2");
|
||||
synchronized (resource2) {
|
||||
System.out.println(Thread.currentThread() + "get resource2");
|
||||
}
|
||||
}
|
||||
}, "线程 1").start();
|
||||
|
||||
new Thread(() -> {
|
||||
synchronized (resource2) {
|
||||
System.out.println(Thread.currentThread() + "get resource2");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println(Thread.currentThread() + "waiting get resource1");
|
||||
synchronized (resource1) {
|
||||
System.out.println(Thread.currentThread() + "get resource1");
|
||||
}
|
||||
}
|
||||
}, "线程 2").start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output
|
||||
|
||||
```text
|
||||
Thread[线程 1,5,main]get resource1
|
||||
Thread[线程 2,5,main]get resource2
|
||||
Thread[线程 1,5,main]waiting get resource2
|
||||
Thread[线程 2,5,main]waiting get resource1
|
||||
```
|
||||
|
||||
线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 `resource2` 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
|
||||
|
||||
### 解决死锁的方法
|
||||
|
||||
解决死锁的方法可以从多个角度去分析,一般的情况下,有**预防,避免,检测和解除四种**。
|
||||
@ -244,206 +447,11 @@ head:
|
||||
3. **逐个撤销涉及死锁的进程,回收其资源直至死锁解除。**
|
||||
4. **抢占资源** :从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。
|
||||
|
||||
## 操作系统内存管理基础
|
||||
|
||||
### 内存管理介绍
|
||||
|
||||
👨💻 **面试官**: **操作系统的内存管理主要是做什么?**
|
||||
|
||||
🙋 **我:** 操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。
|
||||
|
||||
### 常见的几种内存管理机制
|
||||
|
||||
👨💻 **面试官**: **操作系统的内存管理机制了解吗?内存管理有哪几种方式?**
|
||||
|
||||
🙋 **我:** 这个在学习操作系统的时候有了解过。
|
||||
|
||||
简单分为**连续分配管理方式**和**非连续分配管理方式**这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 **块式管理** 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如**页式管理** 和 **段式管理**。
|
||||
|
||||
1. **块式管理** : 远古时代的计算机操作系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
|
||||
2. **页式管理** :把主存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理的划分粒度更小,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
|
||||
3. **段式管理** : 页式管理虽然提高了内存利用率,但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的,段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
|
||||
|
||||
简单来说:页是物理单位,段是逻辑单位。分页可以有效提高内存利用率,分段可以更好满足用户需求。
|
||||
|
||||
👨💻**面试官** : 回答的还不错!不过漏掉了一个很重要的 **段页式管理机制** 。段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 **段页式管理机制** 中段与段之间以及段的内部的都是离散的。
|
||||
|
||||
🙋 **我** :谢谢面试官!刚刚把这个给忘记了~
|
||||
|
||||
### 快表和多级页表
|
||||
|
||||
👨💻**面试官** : 页表管理机制中有两个很重要的概念:快表和多级页表,这两个东西分别解决了页表管理中很重要的两个问题。你给我简单介绍一下吧!
|
||||
|
||||
🙋 **我** :在分页内存管理中,很重要的两点是:
|
||||
|
||||
1. 虚拟地址到物理地址的转换要快。
|
||||
2. 解决虚拟地址空间大,页表也会很大的问题。
|
||||
|
||||
#### 快表
|
||||
|
||||
为了提高虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
|
||||
|
||||
使用快表之后的地址转换流程是这样的:
|
||||
|
||||
1. 根据虚拟地址中的页号查快表;
|
||||
2. 如果该页在快表中,直接从快表中读取相应的物理地址;
|
||||
3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
|
||||
4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
|
||||
|
||||
看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。
|
||||
|
||||
#### 多级页表
|
||||
|
||||
引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。
|
||||
|
||||
多级页表属于时间换空间的典型场景。
|
||||
|
||||
#### 总结
|
||||
|
||||
为了提高内存的空间性能,提出了多级页表的概念;但是提到空间性能是以浪费时间性能为基础的,因此为了补充损失的时间性能,提出了快表(即 TLB)的概念。 不论是快表还是多级页表实际上都利用到了程序的局部性原理,局部性原理在后面的虚拟内存这部分会介绍到。
|
||||
|
||||
### 分页机制和分段机制的共同点和区别
|
||||
|
||||
👨💻**面试官** : **分页机制和分段机制有哪些共同点和区别呢?**
|
||||
|
||||
🙋 **我** :
|
||||
|
||||
1. **共同点** :
|
||||
- 分页机制和分段机制都是为了提高内存利用率,减少内存碎片。
|
||||
- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
|
||||
2. **区别** :
|
||||
- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
|
||||
- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
|
||||
|
||||
### 逻辑(虚拟)地址和物理地址
|
||||
|
||||
👨💻**面试官** :你刚刚还提到了**逻辑地址和物理地址**这两个概念,我不太清楚,你能为我解释一下不?
|
||||
|
||||
🙋 **我:** em...好的嘛!我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。
|
||||
|
||||
### CPU 寻址了解吗?为什么需要虚拟地址空间?
|
||||
|
||||
👨💻**面试官** :**CPU 寻址了解吗?为什么需要虚拟地址空间?**
|
||||
|
||||
🙋 **我** :这部分我真不清楚!
|
||||
|
||||
于是面试完之后我默默去查阅了相关文档!留下了没有技术的泪水。。。
|
||||
|
||||
> 这部分内容参考了 Microsoft 官网的介绍,地址:<https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/virtual-address-spaces?redirectedfrom=MSDN>
|
||||
|
||||
现代处理器使用的是一种称为 **虚拟寻址(Virtual Addressing)** 的寻址方式。**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** 实际上完成虚拟地址转换为物理地址的硬件是 CPU 中含有一个被称为 **内存管理单元(Memory Management Unit, MMU)** 的硬件。如下图所示:
|
||||
|
||||

|
||||
|
||||
**为什么要有虚拟地址空间呢?**
|
||||
|
||||
先从没有虚拟地址空间的时候说起吧!没有虚拟地址空间的时候,**程序直接访问和操作的都是物理内存** 。但是这样有什么问题呢?
|
||||
|
||||
1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃。
|
||||
2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成微信这个程序会崩溃。
|
||||
|
||||
**总结来说:如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。**
|
||||
|
||||
通过虚拟地址访问内存有以下优势:
|
||||
|
||||
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
|
||||
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
|
||||
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
|
||||
|
||||
## 虚拟内存
|
||||
|
||||
### 什么是虚拟内存(Virtual Memory)?
|
||||
|
||||
👨💻**面试官** :再问你一个常识性的问题!**什么是虚拟内存(Virtual Memory)?**
|
||||
|
||||
🙋 **我** :这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。**为什么可以这样呢?** 正是因为 **虚拟内存** 的存在,通过 **虚拟内存** 可以让程序拥有超过系统物理内存大小的可用内存空间。另外,**虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)**。这样会更加有效地管理内存并减少出错。
|
||||
|
||||
**虚拟内存**是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。**虚拟内存的重要意义是它定义了一个连续的虚拟地址空间**,并且 **把内存扩展到硬盘空间**。推荐阅读:[《虚拟内存的那点事儿》](https://juejin.im/post/59f8691b51882534af254317)
|
||||
|
||||
维基百科中有几句话是这样介绍虚拟内存的。
|
||||
|
||||
> **虚拟内存** 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。From:<https://zh.wikipedia.org/wiki/虚拟内存>
|
||||
|
||||
### 局部性原理
|
||||
|
||||
👨💻**面试官** :要想更好地理解虚拟内存技术,必须要知道计算机中著名的**局部性原理**。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。
|
||||
|
||||
🙋 **我** :局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。
|
||||
|
||||
> 以下内容摘自《计算机操作系统教程》 第 4 章存储器管理。
|
||||
|
||||
早在 1968 年的时候,就有人指出我们的程序在执行的时候往往呈现局部性规律,也就是说在某个较短的时间段内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域。
|
||||
|
||||
局部性原理表现在以下两个方面:
|
||||
|
||||
1. **时间局部性** :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
|
||||
2. **空间局部性** :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
|
||||
|
||||
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
|
||||
|
||||
### 虚拟存储器
|
||||
|
||||
> **勘误:虚拟存储器又叫做虚拟内存,都是 Virtual Memory 的翻译,属于同一个概念。**
|
||||
|
||||
👨💻**面试官** :~~都说了虚拟内存了。你再讲讲**虚拟存储器**把!~~
|
||||
|
||||
🙋 **我** :
|
||||
|
||||
> 这部分内容来自:[王道考研操作系统知识点整理](https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html)。
|
||||
|
||||
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大得多的存储器——**虚拟存储器**。
|
||||
|
||||
实际上,我觉得虚拟内存同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。不得不感叹,程序世界几乎不是时间换空间就是空间换时间。
|
||||
|
||||
### 虚拟内存的技术实现
|
||||
|
||||
👨💻**面试官** :**虚拟内存技术的实现呢?**
|
||||
|
||||
🙋 **我** :**虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。** 虚拟内存的实现有以下三种方式:
|
||||
|
||||
1. **请求分页存储管理** :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
|
||||
2. **请求分段存储管理** :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。
|
||||
3. **请求段页式存储管理**
|
||||
|
||||
**这里多说一下?很多人容易搞混请求分页与分页存储管理,两者有何不同呢?**
|
||||
|
||||
请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序所需的全部地址空间都装入主存,这也是请求分页存储管理可以提供虚拟内存的原因,我们在上面已经分析过了。
|
||||
|
||||
它们之间的根本区别在于是否将一作业的全部地址空间同时装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存。
|
||||
|
||||
不管是上面那种实现方式,我们一般都需要:
|
||||
|
||||
1. 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了;
|
||||
2. **缺页中断**:如果**需执行的指令或访问的数据尚未在内存**(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段**调入到内存**,然后继续执行程序;
|
||||
3. **虚拟地址空间** :逻辑地址到物理地址的变换。
|
||||
|
||||
### 页面置换算法
|
||||
|
||||
👨💻**面试官** :虚拟内存管理很重要的一个概念就是页面置换算法。那你说一下 **页面置换算法的作用?常见的页面置换算法有哪些?**
|
||||
|
||||
🙋 **我** :
|
||||
|
||||
> 这个题目经常作为笔试题出现,网上已经给出了很不错的回答,我这里只是总结整理了一下。
|
||||
|
||||
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。
|
||||
|
||||
> **缺页中断** 就是要访问的**页**不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。
|
||||
|
||||
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
|
||||
|
||||
- **OPT 页面置换算法(最佳页面置换算法)** :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。
|
||||
- **FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)** : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
|
||||
- **LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
|
||||
- **LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)** : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
|
||||
|
||||
## 参考
|
||||
|
||||
- 《计算机操作系统—汤小丹》第四版
|
||||
- [《深入理解计算机系统》](https://book.douban.com/subject/1230413/)
|
||||
- [https://zh.wikipedia.org/wiki/输入输出内存管理单元](https://zh.wikipedia.org/wiki/输入输出内存管理单元)
|
||||
- [https://baike.baidu.com/item/快表/19781679](https://baike.baidu.com/item/快表/19781679)
|
||||
- https://www.jianshu.com/p/1d47ed0b46d5
|
||||
- <https://www.studytonight.com/operating-system>
|
||||
- <https://www.geeksforgeeks.org/interprocess-communication-methods/>
|
||||
- <https://juejin.im/post/59f8691b51882534af254317>
|
||||
- 王道考研操作系统知识点整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html
|
||||
- 《深入理解计算机系统》
|
||||
- 《重学操作系统》
|
||||
- 操作系统为什么要分用户态和内核态:https://blog.csdn.net/chen134225/article/details/81783980
|
||||
- 从根上理解用户态与内核态:https://juejin.cn/post/6923863670132850701
|
||||
- 什么是僵尸进程与孤儿进程:https://blog.csdn.net/a745233700/article/details/120715371
|
||||
|
@ -0,0 +1,412 @@
|
||||
---
|
||||
title: 操作系统常见面试题总结(下)
|
||||
category: 计算机基础
|
||||
tag:
|
||||
- 操作系统
|
||||
head:
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: 操作系统,进程,进程通信方式,死锁,操作系统内存管理,块表,多级页表,虚拟内存,页面置换算法
|
||||
- - meta
|
||||
- name: description
|
||||
content: 很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如进程管理、内存管理、虚拟内存等等。
|
||||
---
|
||||
|
||||
## 内存管理
|
||||
|
||||
### 内存管理主要做了什么?
|
||||
|
||||

|
||||
|
||||
操作系统的内存管理非常重要,主要负责下面这些事情:
|
||||
|
||||
- **内存的分配与回收** :对进程所需的内存进行分配和释放,malloc 函数:申请内存,free 函数:释放内存。
|
||||
- **地址转换** :将程序中的虚拟地址转换成内存中的物理地址。
|
||||
- **内存扩充** :当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。
|
||||
- **内存映射** :将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
|
||||
- **内存优化** :通过调整内存分配策略和回收算法来优化内存使用效率。
|
||||
- **内存安全** :保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。
|
||||
- ......
|
||||
|
||||
### 什么是内存碎片?
|
||||
|
||||
内存碎片是由内存的申请和释放产生的,通常分为下面两种:
|
||||
|
||||
- **内部内存碎片(Internal Memory Fragmentation,简称为内存碎片)** :已经分配给进程使用但未被使用的内存。导致内部内存碎片的主要原因是,当采用固定比例比如 2 的幂次方进行内存分配时,进程所分配的内存可能会比其实际所需要的大。举个例子,一个进程只需要 65 字节的内存,但为其分配了 128(2^7) 大小的内存,那 63 字节的内存就成为了内部内存碎片。
|
||||
- **外部内存碎片(External Memory Fragmentation,简称为外部碎片)** :由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并为分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。
|
||||
|
||||

|
||||
|
||||
内存碎片会导致内存利用率下降,如何减少内存碎片是内存管理要非常重视的一件事情。
|
||||
|
||||
### 常见的内存管理方式有哪些?
|
||||
|
||||
内存管理方式可以简单分为下面两种:
|
||||
|
||||
- **连续内存管理** :为一个用户程序分配一个连续的内存空间,内存利用率一般不高。
|
||||
- **非连续内存管理** :允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。
|
||||
|
||||
#### 连续内存管理
|
||||
|
||||
**块式管理** 是早期计算机操作系统的一种连续内存管理方式,存在严重的内存碎片问题。块式管理会将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为内部内存碎片。除了内部内存碎片之外,由于两个内存块之间可能还会有外部内存碎片,这些不连续的外部内存碎片由于太小了无法再进行分配。
|
||||
|
||||
在 Linux 系统中,连续内存管理采用了 **伙伴系统(Buddy System)算法** 来实现,这是一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。伙伴系统的主要思想是将内存按 2 的幂次划分(每一块内存大小都是 2 的幂次比如 2^6=64 KB),并将相邻的内存块组合成一对伙伴(注意:**必须是相邻的才是伙伴**)。
|
||||
|
||||
当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就将其一分为二,分成两个大小相等的伙伴块。如果还是大的话,就继续切分,直到到达合适的大小为止。
|
||||
|
||||
假设两块相邻的内存块都被释放,系统会将这两个内存块合并,进而形成一个更大的内存块,以便后续的内存分配。这样就可以减少内存碎片的问题,提高内存利用率。
|
||||
|
||||

|
||||
|
||||
虽然解决了外部内存碎片的问题,但伙伴系统仍然存在内存利用率不高的问题(内部内存碎片)。这主要是因为伙伴系统只能分配大小为 2^n 的内存块,因此当需要分配的内存大小不是 2^n 的整数倍时,会浪费一定的内存空间。举个例子:如果要分配 65 大小的内存快,依然需要分配 2^7=128 大小的内存块。
|
||||
|
||||

|
||||
|
||||
对于内部内存碎片的问题,Linux 采用 **SLAB** 进行解决。由于这部分内容不是本篇文章的重点,这里就不详细介绍了。
|
||||
|
||||
#### 非连续内存管理
|
||||
|
||||
非连续内存管理存在下面 3 种方式:
|
||||
|
||||
- **段式管理** :以段(—段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
|
||||
- **页式管理** :把物理内存分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页,现代操作系统广泛使用的一种内存管理方式。
|
||||
- **段页式管理机制** :结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
|
||||
|
||||
### 虚拟内存
|
||||
|
||||
#### 什么是虚拟内存?有什么用?
|
||||
|
||||
**虚拟内存(Virtual Memory)** 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。
|
||||
|
||||

|
||||
|
||||
总结来说,虚拟内存主要提供了下面这些能力:
|
||||
|
||||
- **隔离进程** :物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
|
||||
- **提升物理内存利用率** :有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
|
||||
- **简化内存管理** :进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
|
||||
- **多个进程共享物理内存**:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。
|
||||
- **提高内存使用安全性** :控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
|
||||
- **提供更大的可使用内存空间** : 可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。
|
||||
|
||||
#### 没有虚拟内存有什么问题?
|
||||
|
||||
如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。
|
||||
|
||||
**具体有什么问题呢?** 这里举几个例子说明(参考虚拟内存提供的能力回答这个问题):
|
||||
|
||||
1. 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。
|
||||
2. 同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。
|
||||
3. 程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。
|
||||
4. ......
|
||||
|
||||
#### 什么是虚拟地址和物理地址?
|
||||
|
||||
**物理地址(Physical Address)** 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是 **虚拟地址(Virtual Address)** 。
|
||||
|
||||
也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。
|
||||
|
||||
操作系统一般通过 CPU 芯片中的一个重要组件 **MMU(Memory Management Unit,内存管理单元)** 将虚拟地址转换为物理地址,这个过程被称为 **地址翻译/地址转换(Address Translation)** 。
|
||||
|
||||

|
||||
|
||||
通过 MMU 将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。
|
||||
|
||||
MMU 将虚拟地址翻译为物理地址的主要机制有两种: **分段机制** 和 **分页机制** 。
|
||||
|
||||
#### 什么是虚拟地址空间和物理地址空间?
|
||||
|
||||
- 虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。
|
||||
- 物理地址空间是物理地址的集合,是物理内存的范围。
|
||||
|
||||
#### 虚拟地址与物理内存地址是如何映射的?
|
||||
|
||||
MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
|
||||
|
||||
1. 分段机制
|
||||
2. 分页机制
|
||||
3. 段页机制
|
||||
|
||||
其中,现代操作系统广泛采用分页机制,需要重点关注!
|
||||
|
||||
### 分段机制
|
||||
|
||||
**分段机制(Segmentation)** 以段(—段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
|
||||
|
||||
#### 段表有什么用?地址翻译过程是怎样的?
|
||||
|
||||
分段管理通过 **段表(Segment Table)** 映射虚拟地址和物理地址。
|
||||
|
||||
分段机制下的虚拟地址由两部分组成:
|
||||
|
||||
- **段号** :标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。
|
||||
- **段内偏移量** :相对于该段起始地址的偏移量。
|
||||
|
||||
具体的地址翻译过程如下:
|
||||
|
||||
1. MMU 首先解析得到虚拟地址中的段号;
|
||||
2. 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项);
|
||||
3. 从段信息中取出该段的起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址。
|
||||
|
||||

|
||||
|
||||
段表中还存有诸如段长(可用于检查虚拟地址是否超出合法范围)、段类型(该段的类型,例如代码段、数据段等)等信息。
|
||||
|
||||
**通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?**
|
||||
|
||||
不一定。段表项可能并不存在:
|
||||
|
||||
- **段表项被删除** :软件错误、软件恶意行为等情况可能会导致段表项被删除。
|
||||
- **段表项还未创建** :如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。
|
||||
|
||||
#### 分段机制为什么会导致内存外部碎片?
|
||||
|
||||
分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。从而造成物理内存资源利用率的降低。
|
||||
|
||||
举个例子:假设可用物理内存为 5G 的系统使用分段机制分配内存。现在有 4 个进程,每个进程的内存占用情况如下:
|
||||
|
||||
- 进程 1:0~1G(第 1 段)
|
||||
- 进程 2:1~3G(第 2 段)
|
||||
- 进程 3:3~4.5G(第 3 段)
|
||||
- 进程 4:4.5~5G(第 4 段)
|
||||
|
||||
此时,我们关闭了进程 1 和进程 4,则第 1 段和第 4 段的内存会被释放,空闲物理内存还有 1.5G。由于这 1.5G 物理内存并不是连续的,导致没办法将空闲的物理内存分配给一个需要 1.5G 物理内存的进程。
|
||||
|
||||

|
||||
|
||||
### 分页机制
|
||||
|
||||
**分页机制(Paging)** 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。
|
||||
|
||||
**注意:这里的页是连续等长的,不同于分段机制下不同长度的段。**
|
||||
|
||||
在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。
|
||||
|
||||
#### 页表有什么用?地址翻译过程是怎样的?
|
||||
|
||||
分页管理通过 **页表(Page Table)** 映射虚拟地址和物理地址。我这里画了一张基于单级页表进行地址翻译的示意图。
|
||||
|
||||

|
||||
|
||||
在分页机制下,每个应用程序都会有一个对应的页表。
|
||||
|
||||
分页机制下的虚拟地址由两部分组成:
|
||||
|
||||
- **页号** :通过虚拟页号可以从页表中取出对应的物理页号;
|
||||
- **页内偏移量** :物理页起始地址+页内偏移量=物理内存地址。
|
||||
|
||||
具体的地址翻译过程如下:
|
||||
|
||||
1. MMU 首先解析得到虚拟地址中的虚拟页号;
|
||||
2. 通过虚拟页号去该应用程序的页表中取出对应的物理页号(找到对应的页表项);
|
||||
3. 用该物理页号对应的物理页起始地址(物理地址)加上虚拟地址中的页内偏移量得到最终的物理地址。
|
||||
|
||||

|
||||
|
||||
页表中还存有诸如访问标志(标识该页面有没有被访问过)、页类型(该段的类型,例如代码段、数据段等)等信息。
|
||||
|
||||
**通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?**
|
||||
|
||||
不一定!可能会存在 **页缺失** 。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。关于页缺失的内容,后面会详细介绍到。
|
||||
|
||||
#### 单级页表有什么问题?为什么需要多级页表?
|
||||
|
||||
以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,2^20 * 2^2/1024*1024= 4MB。也就是说一个程序啥都不干,页表大小就得占用 4M。
|
||||
|
||||
系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。
|
||||
|
||||
为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表也前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
|
||||
|
||||
这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。
|
||||
|
||||
假设只需要 2 个二级页表,那两级页表的内存占用情况为: 4KB(一级页表占用) + 4KB \* 2(二级页表占用) = 12 KB。
|
||||
|
||||

|
||||
|
||||
多级页表属于时间换空间的典型场景,利用增加页表查询的次数减少页表占用的空间。
|
||||
|
||||
#### TLB 有什么用?使用 TLB 之后的地址翻译流程是怎样的?
|
||||
|
||||
为了提高虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **转址旁路缓存(Translation Lookasjde Buffer,TLB,也被称为快表) ** 。
|
||||
|
||||

|
||||
|
||||
在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 (Memory Management Unit,内存管理单元) 内部的单元,本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页号)对的哈希表。
|
||||
|
||||
使用 TLB 之后的地址翻译流程是这样的:
|
||||
|
||||
1. 用虚拟地址中的虚拟页号作为 key 去 TLB 中查询;
|
||||
2. 如果能查到对应的物理页的话,就不用再查询页表了,这种情况称为 TLB 命中(TLB hit)。
|
||||
3. 如果不能查到对应的物理页的话,还是需要去查询主存中的页表,同时将页表中的该映射表项添加到 TLB 中,这种情况称为 TLB 未命中(TLB miss)。
|
||||
4. 当 TLB 填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
|
||||
|
||||

|
||||
|
||||
由于页表也在主存中,因此在没有 TLB 之前,每次读写内存数据时 CPU 要访问两次主存。有了 TLB 之后,对于存在于 TLB 中的页表数据只需要访问一次主存即可。
|
||||
|
||||
TLB 的设计思想非常简单,但命中率往往非常高,效果很好。这就是因为被频繁访问的页就是其中的很小一部分。
|
||||
|
||||
看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。
|
||||
|
||||
#### 换页机制有什么用?
|
||||
|
||||
换页机制的思想是当物理内存不够用的时候,操作系统选择将一些物理页的内容放到磁盘上去,等要用到的时候再将它们读取到物理内存中。也就是说,换页机制利用磁盘这种较低廉的存储设备扩展的物理内存。
|
||||
|
||||
这也就解释了一个日常使用电脑常见的问题:为什么操作系统中所有进程运行所需的物理内存即使比真实的物理内存要大一些,这些进程也是可以正常运行的,只是运行速度会变慢。
|
||||
|
||||
这同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的物理内存空间来支持程序的运行。
|
||||
|
||||
#### 什么是页缺失?
|
||||
|
||||
根据维基百科:
|
||||
|
||||
> 页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。
|
||||
|
||||
常见的页缺失有下面这两种:
|
||||
|
||||
- **硬性页缺失(Hard Page Fault)** :物理内存中没有对应的物理页。于是,Page Fault Hander 会指示 CPU 从已经打开的磁盘文件中读取相应的内容到物理内存,而后交由 MMU 建立相应的虚拟页和物理页的映射关系。
|
||||
- **软性页缺失(Soft Page Fault)**:物理内存中有对应的物理页,但虚拟页还未和物理页建立映射。于是,Page Fault Hander 会指示 MMU 建立相应的虚拟页和物理页的映射关系。
|
||||
|
||||
发生上面这两种缺页错误的时候,应用程序访问的是有效的物理内存,只是出现了物理页缺失或者虚拟页和物理页的映射关系未建立的问题。如果应用程序访问的是无效的物理内存的话,还会出现 **无效缺页错误(Invalid Page Fault)** 。
|
||||
|
||||
#### 常见的页面置换算法有哪些?
|
||||
|
||||
当发生硬性页缺失时,如果物理内存中没有空闲的物理页面可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面了。
|
||||
|
||||
用来选择淘汰哪一个物理页的规则叫做 **页面置换算法** ,我们可以把页面置换算法看成是淘汰物物理页的规则。
|
||||
|
||||
页缺失太频繁的发生会非常影响性能,一个好的页面置换算法应该是可以减少页缺失出现的次数。
|
||||
|
||||
常见的页面置换算法有下面这 5 种(其他还有很多页面置换算法都是基于这些算法改进得来的):
|
||||
|
||||

|
||||
|
||||
1. **最佳页面置换算法(OPT,Optimal)** :优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。
|
||||
2. **先进先出页面置换算法(FIFO,First In First Out)** : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可需求。不过,它的性能并不是很好。
|
||||
3. **最近最久未使用页面置换算法(LRU ,Least Recently Used)** :LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。LRU 算法是根据各页之前的访问情况来实现,因此是易于实现的。OPT 算法是根据各页未来的访问情况来实现,因此是不可实现的。
|
||||
4. **最少使用页面置换算法(LFU,Least Frequently Used)** : 和 LRU 算法比较像,不过该置换算法选择的是之前一段时间内使用最少的页面作为淘汰页。
|
||||
5. **时钟页面置换算法(Clock)** :可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。
|
||||
|
||||
**FIFO 页面置换算法性能为何不好?**
|
||||
|
||||
主要原因主要有二:
|
||||
|
||||
1. **经常访问或者需要长期存在的页面会被频繁调入调出** :较早调入的页往往是经常被访问或者需要长期存在的页,这些页会被反复调入和调出。
|
||||
2. **存在 Belady 现象** :被置换的页面并不是进程不会访问的,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。出现该异常的原因是因为 FIFO 算法只考虑了页面进入内存的顺序,而没有考虑页面访问的频率和紧迫性。
|
||||
|
||||
**哪一种页面置换算法实际用的比较多?**
|
||||
|
||||
LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT 的页面置换算法。
|
||||
|
||||
不过,需要注意的是,实际应用中这些算法会被做一些改进,就比如 InnoDB Buffer Pool( InnoDB 缓冲池,MySQL 数据库中用于管理缓存页面的机制)就改进了传统的 LRU 算法,使用了一种称为"Adaptive LRU"的算法(同时结合了 LRU 和 LFU 算法的思想)。
|
||||
|
||||
### 分页机制和分段机制有哪些共同点和区别?
|
||||
|
||||
**共同点** :
|
||||
|
||||
- 都是非连续内存管理的方式。
|
||||
- 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
|
||||
-
|
||||
|
||||
**区别** :
|
||||
|
||||
- 分页机制以页面为单位进行内存管理,而分段机制以段为单位进行内存管理。页的大小是固定的,由操作系统决定,通常为 2 的幂次方。而段的大小不固定,取决于我们当前运行的程序。
|
||||
- 页是物理单位,即操作系统将物理内存划分成固定大小的页面,每个页面的大小通常是 2 的幂次方,例如 4KB、8KB 等等。而段则是逻辑单位,是为了满足程序对内存空间的逻辑需求而设计的,通常根据程序中数据和代码的逻辑结构来划分。
|
||||
- 分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。分页机制解决了外部内存碎片的问题,但仍然可能会出现内部内存碎片。
|
||||
- 分页机制采用了页表来完成虚拟地址到物理地址的映射,页表通过一级页表和二级页表来实现多级映射;而分段机制则采用了段表来完成虚拟地址到物理地址的映射,每个段表项中记录了该段的起始地址和长度信息。
|
||||
- 分页机制对程序没有任何要求,程序只需要按照虚拟地址进行访问即可;而分段机制需要程序员将程序分为多个段,并且显式地使用段寄存器来访问不同的段。
|
||||
|
||||
### 段页机制
|
||||
|
||||
结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
|
||||
|
||||
在段页式机制下,地址翻译的过程分为两个步骤:
|
||||
|
||||
1. 段式地址映射。
|
||||
2. 页式地址映射。
|
||||
|
||||
### 局部性原理
|
||||
|
||||
要想更好地理解虚拟内存技术,必须要知道计算机中著名的 **局部性原理(Locality Principle)**。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。
|
||||
|
||||
局部性原理是指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。其中,时间局部性是指一个数据项或指令在一段时间内被反复使用的特点,空间局部性是指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。
|
||||
|
||||
在分页机制中,页表的作用是将虚拟地址转换为物理地址,从而完成内存访问。在这个过程中,局部性原理的作用体现在两个方面:
|
||||
|
||||
- **时间局部性** :由于程序中存在一定的循环或者重复操作,因此会反复访问同一个页或一些特定的页,这就体现了时间局部性的特点。为了利用时间局部性,分页机制中通常采用缓存机制来提高页面的命中率,即将最近访问过的一些页放入缓存中,如果下一次访问的页已经在缓存中,就不需要再次访问内存,而是直接从缓存中读取。
|
||||
- **空间局部性** :由于程序中数据和指令的访问通常是具有一定的空间连续性的,因此当访问某个页时,往往会顺带访问其相邻的一些页。为了利用空间局部性,分页机制中通常采用预取技术来预先将相邻的一些页读入内存缓存中,以便在未来访问时能够直接使用,从而提高访问速度。
|
||||
|
||||
总之,局部性原理是计算机体系结构设计的重要原则之一,也是许多优化算法的基础。在分页机制中,利用时间局部性和空间局部性,采用缓存和预取技术,可以提高页面的命中率,从而提高内存访问效率
|
||||
|
||||
## 文件系统
|
||||
|
||||
### 文件系统主要做了什么?
|
||||
|
||||
文件系统主要负责管理和组织计算机存储设备上的文件和目录,其功能包括以下几个方面:
|
||||
|
||||
1. **存储管理** :将文件数据存储到物理存储介质中,并且管理空间分配,以确保每个文件都有足够的空间存储,并避免文件之间发生冲突。
|
||||
2. **文件管理** :文件的创建、删除、移动、重命名、压缩、加密、共享等等。
|
||||
3. **目录管理** :目录的创建、删除、移动、重命名等等。
|
||||
4. **文件访问控制** :管理不同用户或进程对文件的访问权限,以确保用户只能访问其被授权访问的文件,以保证文件的安全性和保密性。
|
||||
|
||||
### 硬链接和软链接有什么区别?
|
||||
|
||||
在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种:
|
||||
|
||||
**1、硬链接(Hard Link)**
|
||||
|
||||
- 在 Linux/类 Unix 文件系统中,每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。硬链接通过 inode 节点号建立连接,硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的(可以看作是互为硬链接,源头是同一份文件),删除其中任何一个对另外一个没有影响,可以通过给文件设置硬链接文件来防止重要文件被误删。
|
||||
- 只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。
|
||||
- 硬链接具有一些限制,不能对目录以及不存在的文件创建硬链接,并且,硬链接也不能跨越文件系统。
|
||||
- `ln` 命令用于创建硬链接。
|
||||
|
||||
**2、软链接(Symbolic Link 或 Symlink)**
|
||||
|
||||
- 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。
|
||||
- 源文件删除后,硬链接依然存在,但是指向的是一个无效的文件路径。
|
||||
- 软连接类似于 Windows 系统中的快捷方式。
|
||||
- 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。
|
||||
- `ln -s` 命令用于创建软链接。
|
||||
|
||||
### 硬链接为什么不能跨文件系统?
|
||||
|
||||
我们之前提到过,硬链接是通过 inode 节点号建立连接的,而硬链接和源文件共享相同的 inode 节点号。
|
||||
|
||||
然而,每个文件系统都有自己的独立 inode 表,且每个 inode 表只维护该文件系统内的 inode。如果在不同的文件系统之间创建硬链接,可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。
|
||||
|
||||
### 提高文件系统性能的方式有哪些?
|
||||
|
||||
- **优化硬件** :使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Inexpensive Disks)等技术提高磁盘性能。
|
||||
- **选择合适的文件系统选型** :不同的文件系统具有不同的特性,对于不同的应用场景选择合适的文件系统可以提高系统性能。
|
||||
- **运用缓存** :访问磁盘的效率比较低,可以运用缓存来减少磁盘的访问次数。不过,需要注意缓存命中率,缓存命中率过低的话,效果太差。
|
||||
- **避免磁盘过度使用** :注意磁盘的使用率,避免将磁盘用满,尽量留一些剩余空间,以免对文件系统的性能产生负面影响。
|
||||
- **对磁盘进行合理的分区** :合理的磁盘分区方案,能够使文件系统在不同的区域存储文件,从而减少文件碎片,提高文件读写性能。
|
||||
|
||||
### 常见的磁盘调度算法有哪些?
|
||||
|
||||
磁盘调度算法是操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。
|
||||
|
||||
一次磁盘读写操作的时间由磁盘寻道/寻找时间、延迟时间和传输时间决定。磁盘调度算法可以通过改变到达磁盘请求的处理顺序,减少磁盘寻道时间和延迟时间。
|
||||
|
||||
常见的磁盘调度算法有下面这 6 种(其他还有很多磁盘调度算法都是基于这些算法改进得来的):
|
||||
|
||||

|
||||
|
||||
1. **先来先服务算法(First-Come First-Served,FCFS)** :按照请求到达磁盘调度器的顺序进行处理,先到达的请求的先被服务。FCFS 算法实现起来比较简单,不存在算法开销。不过,由于没有考虑磁头移动的路径和方向,平均寻道时间较长。同时,该算法容易出现饥饿问题,即一些后到的磁盘请求可能需要等待很长时间才能得到服务。
|
||||
2. **最短寻道时间优先算法(Shortest Seek Time First,SSTF)** :也被称为最佳服务优先(Shortest Service Time First,SSTF)算法,优先选择距离当前磁头位置最近的请求进行服务。SSTF 算法能够最小化磁头的寻道时间,但容易出现饥饿问题,即磁头附近的请求不断被服务,远离磁头的请求长时间得不到响应。实际应用中,需要优化一下该算法的实现,避免出现饥饿问题。
|
||||
3. **扫描算法(SCAN)** :也被称为电梯(Elevator)算法,基本思想和电梯非常类似。磁头沿着一个方向扫描磁盘,如果经过的磁道有请求就处理,直到到达磁盘的边界,然后改变移动方向,依此往复。SCAN 算法能够保证所有的请求得到服务,解决了饥饿问题。但是,如果磁头从一个方向刚扫描完,请求才到的话。这个请求就需要等到磁头从相反方向过来之后才能得到处理。
|
||||
4. **循环扫描算法(Circular Scan,C-SCAN)** :SCAN 算法的变体,只在磁盘的一侧进行扫描,并且只按照一个方向扫描,直到到达磁盘边界,然后回到磁盘起点,重新开始循环。
|
||||
5. **边扫描边观察算法(LOOK)** :SCAN 算法中磁头到了磁盘的边界才改变移动方向,这样可能会做很多无用功,因为磁头移动方向上可能已经没有请求需要处理了。LOOK 算法对 SCAN 算法进行了改进,如果磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,依此往复。也就是边扫描边观察指定方向上还有无请求,因此叫 LOOK。
|
||||
6. **均衡循环扫描算法(C-LOOK)** :C-SCAN 只有到达磁盘边界时才能改变磁头移动方向,并且磁头返回时也需要返回到磁盘起点,这样可能会做很多无用功。C-LOOK 算法对 C-SCAN 算法进行了改进,如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。
|
||||
|
||||
## 参考
|
||||
|
||||
- 《计算机操作系统—汤小丹》第四版
|
||||
- 《深入理解计算机系统》
|
||||
- 《重学操作系统》
|
||||
- 《现代操作系统原理与实现》
|
||||
- 王道考研操作系统知识点整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html
|
||||
- 内存管理之伙伴系统与 SLAB:https://blog.csdn.net/qq_44272681/article/details/124199068
|
||||
- 为什么 Linux 需要虚拟内存:https://draveness.me/whys-the-design-os-virtual-memory/
|
||||
- 程序员的自我修养(七):内存缺页错误:https://liam.page/2017/09/01/page-fault/
|
||||
- 虚拟内存的那点事儿:https://juejin.cn/post/6844903507594575886
|
@ -214,7 +214,7 @@ SELECT id FROM table WHERE id=1;
|
||||
|
||||
### 覆盖索引
|
||||
|
||||
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
|
||||
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
|
||||
|
||||
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
|
||||
|
||||
|
@ -517,7 +517,7 @@ SELECT ... FOR UPDATE;
|
||||
- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
|
||||
- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
|
||||
|
||||
**意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。**
|
||||
**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。**
|
||||
|
||||
意向锁之间是互相兼容的。
|
||||
|
||||
|
@ -94,25 +94,23 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编
|
||||
|
||||
可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。
|
||||
|
||||
对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案:
|
||||
首先,2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK。2009 年 Oracle 收购了 Sun 公司,于是自己在 OpenJDK 的基础上搞了一个 Oracle JDK。Oracle JDK 是不开源的,并且刚开始的几个版本(Java8 ~ Java11)还会相比于 OpenJDK 添加一些特有的功能和工具。
|
||||
|
||||
其次,对于 Java 7 而言,OpenJDK 和 Oracle JDK 是十分接近的。 Oracle JDK 是基于 OpenJDK 7 构建的,只添加了一些小功能,由 Oracle 工程师参与维护。
|
||||
|
||||
下面这段话摘自 Oracle 官方在 2012 年发表的一个博客:
|
||||
|
||||
> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?
|
||||
>
|
||||
> 答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些闭源的第三方组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。
|
||||
|
||||
**总结:**(提示:下面括号内的内容是基于原文补充说明的,因为原文太过于晦涩难懂,用人话重新解释了下,如果你看得懂里面的术语,可以忽略括号解释的内容)
|
||||
最后,简单总结一下 Oracle JDK 和 OpenJDK 的区别:
|
||||
|
||||
1. Oracle JDK 大概每 6 个月发一次主要版本(从 2014 年 3 月 JDK 8 LTS 发布到 2017 年 9 月 JDK 9 发布经历了长达 3 年多的时间,所以并不总是 6 个月),而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:[https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence](https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence) 。
|
||||
|
||||
2. OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的;(个人观点:众所周知,JDK 原来是 SUN 公司开发的,后来 SUN 公司又卖给了 Oracle 公司,Oracle 公司以 Oracle 数据库而著名,而 Oracle 数据库又是闭源的,这个时候 Oracle 公司就不想完全开源了,但是原来的 SUN 公司又把 JDK 给开源了,如果这个时候 Oracle 收购回来之后就把他给闭源,必然会引起很多 Java 开发者的不满,导致大家对 Java 失去信心,那 Oracle 公司收购回来不就把 Java 烂在手里了吗!然后,Oracle 公司就想了个骚操作,这样吧,我把一部分核心代码开源出来给你们玩,并且我要和你们自己搞的 JDK 区分下,你们叫 OpenJDK,我叫 Oracle JDK,我发布我的,你们继续玩你们的,要是你们搞出来什么好玩的东西,我后续发布 Oracle JDK 也会拿来用一下,一举两得!)OpenJDK 开源项目:[https://github.com/openjdk/jdk](https://github.com/openjdk/jdk)
|
||||
|
||||
3. Oracle JDK 比 OpenJDK 更稳定(肯定啦,Oracle JDK 由 Oracle 内部团队进行单独研发的,而且发布时间比 OpenJDK 更长,质量更有保障)。OpenJDK 和 Oracle JDK 的代码几乎相同(OpenJDK 的代码是从 Oracle JDK 代码派生出来的,可以理解为在 Oracle JDK 分支上拉了一条新的分支叫 OpenJDK,所以大部分代码相同),但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
|
||||
|
||||
4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能;
|
||||
|
||||
5. Oracle JDK 不会为即将发布的版本提供长期支持(如果是 LTS 长期支持版本的话也会,比如 JDK 8,但并不是每个版本都是 LTS 版本),用户每次都必须通过更新到最新版本获得支持来获取最新版本;
|
||||
|
||||
6. Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。
|
||||
1. **是否开源** :OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是基于 OpenJDK 实现的,并不是完全开源的(个人观点:众所周知,JDK 原来是 SUN 公司开发的,后来 SUN 公司又卖给了 Oracle 公司,Oracle 公司以 Oracle 数据库而著名,而 Oracle 数据库又是闭源的,这个时候 Oracle 公司就不想完全开源了,但是原来的 SUN 公司又把 JDK 给开源了,如果这个时候 Oracle 收购回来之后就把他给闭源,必然会引起很多 Java 开发者的不满,导致大家对 Java 失去信心,那 Oracle 公司收购回来不就把 Java 烂在手里了吗!然后,Oracle 公司就想了个骚操作,这样吧,我把一部分核心代码开源出来给你们玩,并且我要和你们自己搞的 JDK 区分下,你们叫 OpenJDK,我叫 Oracle JDK,我发布我的,你们继续玩你们的,要是你们搞出来什么好玩的东西,我后续发布 Oracle JDK 也会拿来用一下,一举两得!)OpenJDK 开源项目:[https://github.com/openjdk/jdk](https://github.com/openjdk/jdk) 。
|
||||
2. **是否免费** :Oracle JDK 会提供免费版本,但一般有时间限制。JDK17 之后的版本可以免费分发和商用,但是仅有 3 年时间,3 年后无法免费商用。不过,JDK8u221 之前只要不升级可以无限期免费。OpenJDK 是完全免费的。
|
||||
3. **功能性** :Oracle JDK 在 OpenJDK 的基础上添加了一些特有的功能和工具,比如 Java Flight Recorder(JFR,一种监控工具)、Java Mission Control(JMC,一种监控工具)等工具。不过,在 Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致,之前 OracleJDK 中的私有组件大多数也已经被捐赠给开源组织。
|
||||
4. **稳定性** :OpenJDK 不提供 LTS 服务,而 OracleJDK 大概每三年都会推出一个 LTS 版进行长期支持。不过,很多公司都基于 OpenJDK 提供了对应的和 OracleJDK 周期相同的 LTS 版。因此,两者稳定性其实也是差不多的。
|
||||
5. **协议** : Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。
|
||||
|
||||
> 既然 Oracle JDK 这么好,那为什么还要有 OpenJDK?
|
||||
>
|
||||
@ -128,6 +126,10 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编
|
||||
|
||||

|
||||
|
||||
**Oracle JDK 和 OpenJDK 如何选择?**
|
||||
|
||||
建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。
|
||||
|
||||
🌈 拓展一下:
|
||||
|
||||
- BCL 协议(Oracle Binary Code License Agreement): 可以使用 JDK(支持商用),但是不能进行修改。
|
||||
@ -135,8 +137,6 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编
|
||||
|
||||

|
||||
|
||||
相关阅读 👍:[《Differences Between Oracle JDK and OpenJDK》](https://www.baeldung.com/oracle-jdk-vs-openjdk)
|
||||
|
||||
### Java 和 C++ 的区别?
|
||||
|
||||
我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来。
|
||||
@ -876,7 +876,7 @@ System.out.println(y); /* 0.1 */
|
||||
System.out.println(Objects.equals(x, y)); /* true */
|
||||
```
|
||||
|
||||
关于 `BigDecimal` 的详细介绍,可以看看我写的这篇文章:[BigDecimal 详解](./bigdecimal.md)。
|
||||
关于 `BigDecimal` 的详细介绍,可以看看我写的这篇文章:[BigDecimal 详解](https://javaguide.cn/java/basis/bigdecimal.html)。
|
||||
|
||||
### 超过 long 整型的数据应该如何表示?
|
||||
|
||||
|
@ -526,6 +526,8 @@ System.out.println(s);
|
||||
|
||||
如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
|
||||
|
||||
不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 `makeConcatWithConstants()` 来实现,而不是大量的 `StringBuilder` 了。这个改进是 JDK9 的 [JEP 280](https://openjdk.org/jeps/280) 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 [StringBuilder?来重温一下字符串拼接吧](https://juejin.cn/post/7182872058743750715) 。
|
||||
|
||||
#### String#equals() 和 Object#equals() 有何区别?
|
||||
|
||||
`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。
|
||||
|
@ -157,7 +157,7 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中
|
||||
|
||||
相关阅读:[线程的几种状态你真的了解么?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w) 。
|
||||
|
||||
## 什么是上下文切换?
|
||||
## 什么是线程上下文切换?
|
||||
|
||||
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
|
||||
|
||||
|
@ -403,15 +403,16 @@ public class SynchronizedDemo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
|
||||
|
||||

|
||||

|
||||
|
||||
从上面我们可以看出:**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
|
||||
|
||||
上面的字节码中包含一个 `monitorenter` 指令以及两个 `monitorexit` 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。
|
||||
|
||||
当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。
|
||||
|
||||
> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。
|
||||
|
@ -193,8 +193,6 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
|
||||
|
||||

|
||||
|
||||
还没看够?推荐 why 神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!
|
||||
|
||||
如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目:
|
||||
|
||||
- **[Hippo-4](https://github.com/opengoofy/hippo4j)** :一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。
|
||||
|
@ -440,7 +440,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:
|
||||
- **并行与并发**:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
|
||||
- **分代收集**:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
|
||||
- **空间整合**:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
|
||||
- **可预测的停顿**:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
|
||||
- **可预测的停顿**:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
|
||||
|
||||
G1 收集器的运作大致分为以下几个步骤:
|
||||
|
||||
|
@ -252,7 +252,7 @@ JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池
|
||||
|
||||
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 `OutOfMemoryError` 错误出现。
|
||||
|
||||
JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**通道(Channel)**与**缓存区(Buffer)**的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。
|
||||
JDK1.4 中新加入的 **NIO(Non-Blocking I/O,也被称为New I/O)**,引入了一种基于**通道(Channel)**与**缓存区(Buffer)**的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。
|
||||
|
||||
直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
|
||||
|
||||
|
@ -37,6 +37,7 @@ footer: |-
|
||||
- [我曾经也是网瘾少年](./about-the-author/internet-addiction-teenager.md)
|
||||
- [害,毕业三年了!](./about-the-author/my-college-life.md)
|
||||
- [我的知识星球快 3 岁了!](./about-the-author/zhishixingqiu-two-years.md)
|
||||
- [坚持写技术博客六年了](./about-the-author/writing-technology-blog-six-years.md)
|
||||
|
||||
## PDF
|
||||
|
||||
|
@ -31,6 +31,8 @@ tag:
|
||||
|
||||
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
|
||||
|
||||

|
||||
|
||||
```java
|
||||
public class OrdersService {
|
||||
private AccountDao accountDao;
|
||||
@ -57,12 +59,22 @@ public class OrdersService {
|
||||
|
||||
## 事务的特性(ACID)了解么?
|
||||
|
||||
- **原子性(Atomicity):** 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
|
||||
- **一致性(Consistency):** 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
|
||||
- **隔离性(Isolation):** 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
|
||||
- **持久性(Durability):** 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
|
||||
1. **原子性**(`Atomicity`) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
||||
2. **一致性**(`Consistency`): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
|
||||
3. **隔离性**(`Isolation`): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
|
||||
4. **持久性**(`Durability`): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
||||
|
||||
参考 :[https://zh.wikipedia.org/wiki/ACID](https://zh.wikipedia.org/wiki/ACID) 。
|
||||
🌈 这里要额外补充一点:**只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!** 想必大家也和我一样,被 ACID 这个概念被误导了很久! 我也是看周志明老师的公开课[《周志明的软件架构课》](https://time.geekbang.org/opencourse/intro/100064201)才搞清楚的(多看好书!!!)。
|
||||
|
||||

|
||||
|
||||
另外,DDIA 也就是 [《Designing Data-Intensive Application(数据密集型应用系统设计)》](https://book.douban.com/subject/30329536/) 的作者在他的这本书中如是说:
|
||||
|
||||
> Atomicity, isolation, and durability are properties of the database, whereas consis‐ tency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone.
|
||||
>
|
||||
> 翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。
|
||||
|
||||
《Designing Data-Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 Github 开源,地址:[https://github.com/Vonng/ddiaopen in new window](https://github.com/Vonng/ddia) 。
|
||||
|
||||
## 详谈 Spring 对事务的支持
|
||||
|
||||
@ -190,8 +202,8 @@ public interface PlatformTransactionManager {
|
||||
>
|
||||
> 举个例子,我上个项目有发送短信的需求,为此,我们定了一个接口,接口只有两个方法:
|
||||
>
|
||||
> 1.发送短信
|
||||
> 2.处理发送结果的方法。
|
||||
> 1.发送短信
|
||||
> 2.处理发送结果的方法。
|
||||
>
|
||||
> 刚开始我们用的是阿里云短信服务,然后我们实现这个接口完成了一个阿里云短信的服务。后来,我们突然又换到了别的短信服务平台,我们这个时候只需要再实现这个接口即可。这样保证了我们提供给外部的行为不变。几乎不需要改变什么代码,我们就轻松完成了需求的转变,提高了代码的灵活性和可扩展性。
|
||||
>
|
||||
@ -411,7 +423,7 @@ Class B {
|
||||
|
||||
如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说:
|
||||
|
||||
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
|
||||
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
|
||||
- 如果外部方法无事务,则单独开启一个事务,与 `PROPAGATION_REQUIRED` 类似。
|
||||
|
||||
这里还是简单举个例子:如果 `bMethod()` 回滚的话,`aMethod()`不会回滚。如果 `aMethod()` 回滚的话,`bMethod()`会回滚。
|
||||
@ -686,4 +698,3 @@ private void method1() {
|
||||
- Spring 事务的传播特性:[https://github.com/love-somnus/Spring/wiki/Spring 事务的传播特性](https://github.com/love-somnus/Spring/wiki/Spring事务的传播特性)
|
||||
- [Spring 事务传播行为详解](https://segmentfault.com/a/1190000013341344) :[https://segmentfault.com/a/1190000013341344](https://segmentfault.com/a/1190000013341344)
|
||||
- 全面分析 Spring 的编程式事务管理及声明式事务管理:[https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html](https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user