first commit
This commit is contained in:
144
专栏/重学操作系统-完/00开篇词为什么大厂面试必考操作系统?.md
Normal file
144
专栏/重学操作系统-完/00开篇词为什么大厂面试必考操作系统?.md
Normal file
@ -0,0 +1,144 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 为什么大厂面试必考操作系统?
|
||||
你好,发现求知的乐趣,我是林䭽。
|
||||
|
||||
我在阿里巴巴做架构的多年时间里,每天都在和复杂的业务场景斗争着,比如如何应对高并发场景?如何解决系统间的数据一致性问题?如何带给用户更快更爽的体验?
|
||||
|
||||
在处理一个又一个业务架构、系统架构问题的过程中,我愈发地意识到有一块知识非常重要,也就是我们这门课的主题——操作系统。操作系统可以作为一个完整的知识框架,把复杂如高并发、数据一致性的问题,基础到编程语言、计算框架、业务框架的问题都串联起来。
|
||||
|
||||
为什么要学习操作系统?
|
||||
|
||||
操作系统(Operating System)作为一门计算机专业大学必修课,如今已经成为程序员跳槽、涨薪、过面试的必考内容。像面试中高频的考点,比如 Linux 指令、中断、多线程、并发、性能、内存管理、系统稳定性、文件系统、容器和虚拟化等,知识都来源于操作系统。学了操作系统:
|
||||
|
||||
|
||||
你不懂 Java 多线程,也可以回答好 Java 多线程的面试题;
|
||||
你不熟悉 Docker,也可以回答出容器化应该如何做。
|
||||
|
||||
|
||||
|
||||
|
||||
操作系统已不仅仅是一门大学的必修课那么简单,更是计算机领域的本源知识,任何编程语言学下去都会碰到操作系统知识,比如 Java 的虚拟机、Go 语言的协程与通道、Node.js 的 I/O 模型等。任何研发工具学下去也都会碰到操作系统,比如:
|
||||
|
||||
|
||||
MySQL 深入学下去会碰到 InnoDB 文件系统;
|
||||
HBase 深入学下去会有 Hadoop 文件系统(HDFS);
|
||||
Redis 深入学下去会碰到 Linux 的I/O模型;
|
||||
Docker 深入学下去有 Linux 的命名空间等;
|
||||
甚至 Spring 框架,也需要用到线程池和调度算法。
|
||||
|
||||
|
||||
因此,作为面试官,我们需要通过操作系统知识判断求职者的综合能力,你可以将这些语言、开发工具用到什么层次?是能够使用还是理解原理,甚至具备系统改造的能力?
|
||||
|
||||
为什么你要学习我这门操作系统课?
|
||||
|
||||
互联网领域有一个非常重要的分析方法,那就是一件事情如果可以成功,无非就是合适的人,在合适的时间,做合适的事情。接下来,我将结合课程设计思路和自己的人生经历来分析, 你为什么要学习我的这门操作系统课。
|
||||
|
||||
我们可以先从“事到人”的角度入手。从事情上说,我希望做一门学了就能用的课程,所以本课程学习目标有两个:一是帮助你顺利通过面试、跳槽涨薪;另一个是帮助你提升应对实际工作场景的能力,具体包括以下几点。
|
||||
|
||||
|
||||
提升学习和理解能力:比如学习 Redis 可以理解到日志文件系统层面;学习 Java/Python/Node 等语言可以理解到语言最底层。
|
||||
提升应用架构能力:比如可以将操作系统的微内核架构迁移到自己设计的系统中。
|
||||
提升系统稳定性架构能力:比如在多线程设计上更出色,可以帮助同事找到设计漏洞。
|
||||
提升运维能力:做到可以方便地管理集群和分析日志。
|
||||
|
||||
|
||||
下面我再来结合我自身背景和互联网行业特点与你具体聊一聊。
|
||||
|
||||
首先,帮你面试涨薪这块我非常有底气,为什么我敢这样说?
|
||||
|
||||
我曾在 3 家互联网大厂任职架构师(技术专家),而且是技术委员会成员;另一方面,我做了 10 年的面试官,每年都会到拉勾网筛选简历,保守估计按每年面试 100 个人来算,我至少已经面试过上千人了;还有一方面,我有很多朋友在大厂做技术 Leader,也有很多学生在大厂工作,因此我有一个精准的面试圈子。
|
||||
|
||||
我们在面试题、面试技巧方面交流非常多,而且我已经出过几门技术类的在线课程,我的讲课风格也受到了很多学员的认可,他们很喜欢和我交流问题、探讨技术,所以我可以自信地说:我是一个非常懂面试和公司用人标准的老师。
|
||||
|
||||
再者,我有丰富的实际应用场景经历。
|
||||
|
||||
中国互联网系统最主要的设计约束:并发高、数据量大(毕竟中国互联网是以人口红利起家的)。比较巧的是,海量用户的 C 端场景和大数据商业分析场景,我刚好都负责过。而高并发、大数据中的很多知识,又需要从操作系统中获取,加上我本身操作系统方面的知识也比较扎实,所以在实际场景这块我也有丰富的经验。
|
||||
|
||||
接下来再从我的角度来看看“现在要不要学操作系统”,我觉得现在的时机刚刚好。
|
||||
|
||||
首先,目前是一个在线教育的风口,我结合自身背景以及拉勾网在线招聘求职方向的优势,给你带来一门针对工作场景的就业提升类操作系统课程,符合平台调性。
|
||||
|
||||
|
||||
|
||||
再者,云原生架构出现之后,越来越强调“谁开发谁运维”,因此业内对操作系统的需求度在提升、要求也在提高,所以我设计这门针对工作场景的就业提升类操作系统课程符合市场需求。
|
||||
|
||||
|
||||
操作系统的需求量也是急剧增长。
|
||||
随着我国用户量和数据量的爆发式增长,也让互联网的运营成本变高。因此,公司很希望自己的员工熟悉操作系统,可以从系统底层去帮助公司节约成本。
|
||||
|
||||
|
||||
此外,随着我国用户量和数据量的爆发式增长,也让互联网的运营成本变高,一个普普通通千万级用户量的网站,就需要每年上亿的运维成本去 Cover。在这种情况下,公司很希望自己的员工熟悉操作系统,可以从系统底层去帮助公司节约成本。所以市场对于操作系统的需求量也是急剧增长的。
|
||||
|
||||
课程介绍
|
||||
|
||||
接下来我们聊聊课程内容,这门课程对标的是架构师层级的基础能力,看个人的接受程度,学完之后大概会在阿里的 P7 及以上层级。
|
||||
|
||||
课程共分为 8 个模块,合计 39 个课时。具体每个模块的介绍,我将在下一课时“课前必读”中详细讲解。在这里,我先跟你分享一下课程的整体设计思路。
|
||||
|
||||
这门操作系统课程将帮助你系统地解决面试中可能遇到的计算机原理和操作系统类问题,并以大厂面试题作为切入点,引出很多你在实际工作中会遇到的问题和技术难点。同时,每一个模块聚焦操作系统知识的一个方向,每节课的标题就是这个方向最需要掌握的,也是真实出现过的大厂面试题,同时它也代表着一类知识点。而且,我还将结合实战场景帮助你打牢基础知识,向架构师的方向努力。
|
||||
|
||||
说到工作场景,我认为有两个非常重要的问题需要解决。
|
||||
|
||||
第 1 个问题是提高大家在实际工作场景中的实战能力。 除了讲解操作系统的知识结构,还会结合以下 6 个场景深入分析:
|
||||
|
||||
|
||||
架构师必备的高并发、多线程编程技巧;
|
||||
团队 Leader 如何掌握必备的 Linux 运维技巧;
|
||||
程序语言(Java/Go/Node)的内存分配和回收;
|
||||
数据库底层(MySQL/Hadoop)的文件系统;
|
||||
计算机网络和操作系统 I/O 模型结合有哪些注意事项;
|
||||
如何从操作系统底层理看容器化技术:K8s 和 Docker。
|
||||
|
||||
|
||||
这些都是目前国内大厂研发岗位最为关心的问题,也是大厂面试的热门问题,学会和理解这些问题背后的原理,将对你面试和晋升有很大的帮助。
|
||||
|
||||
第 2 个问题是解决面试难题,让你顺利拿到 Offer。我精选了 80 道左右的大厂面试题:
|
||||
|
||||
|
||||
其中 40 道题目会作为课程标题或者课前的提问,在课时中会带着大家分析,以此引出该问题背后的知识点,从点到面一步步把知识点和原理讲清楚。
|
||||
另外 40 道题目将作为课后练习题,帮助你巩固本课时的内容,检测自己的学习成果。此外,这部分问题还会作为精讲习题,在每个模块单独设置一个课时带你梳理解题思路、分析底层原理,结合实战场景帮你分析工作中可能遇到的瓶颈。
|
||||
|
||||
|
||||
学完之后,希望你可以举一反三,这非常有利于提升你的面试成功率。
|
||||
|
||||
其实我在做课程设计之前,花了大量的时间进行市场调研,发现目前市场上专门针对互联网方向,帮助你提升系统架构能力、应对日常使用和高并发场景的操作系统课程是缺失的。
|
||||
|
||||
这也是我做这门课程的初衷:课程中没有大量的源代码,也没有特别的语言依赖。我会把面试要点和工作场景中的重难点内容都覆盖到,彻彻底底、由内而外地给你讲清楚,帮你应对面试、解决工作难题。
|
||||
|
||||
说到这里,我想你可能还有些疑问,比如课程的难度怎么样,适不适合新手学习?或者是课程深度能否满足进阶大厂架构师?
|
||||
|
||||
|
||||
第一个问题,适不适合新手学习?
|
||||
|
||||
|
||||
我的答案是:适合。这门课程对大家的基础知识没做任何假设,比如操作系统唯一依赖的知识——计算机组成原理,我也把它们做成了前置课程,利用模块一的 4 个课时把这块内容讲清楚。
|
||||
|
||||
另一方面,我会从设计者角度出发来给你讲问题。比如讲多线程算法的时候,我会先讲遇到了什么困难,再诱导大家思考如何解决这些困难,然后和你一起分析出几个解题方向,最后循序渐进地给出一个合理的方案。所以这门课程非常适合新手学习。
|
||||
|
||||
|
||||
第二个问题,课程内容的深度能否满足进阶大厂架构师?
|
||||
|
||||
|
||||
我的答案依然是:满足,这门课程的深度足够培养一名架构师。课程内容会涉及多线程编程中 AQS 的原理;数据同步中的乐观锁;Java 新生代用了什么算法、老生代用了什么算法;还有比如高并发时有哪些提升性能的方法?这些都是架构师需要掌握的,也是课程的重点。
|
||||
|
||||
所以,操作系统这门课程既适合新手入门,也适合有经验的开发人员进阶学习,这并不受经验影响,不同的是你们的学习目标和学习收获。
|
||||
|
||||
寄语
|
||||
|
||||
最后,我还想和你说点关于职业发展相关的事情。
|
||||
|
||||
|
||||
|
||||
中国有超过 1000 万程序员,大部分人的年薪小于 30 万。我观察到一个这样的现象:一方面求职者们抱怨市场竞争激烈,大家争抢一两个岗位;另一方面很多优秀团队的高薪岗位招人难,闲置多个空位,求职者很多但是符合岗位要求的却很少。到底是什么原因造成企业招人难,求职者求职难的情况呢?
|
||||
|
||||
其实,拉开个人薪资和团队整体水平差异的分水岭,根本原因就是计算机基础知识的掌握程度。基础好的程序员,学习速度快,愿意花时间去积累知识,提高自身能力,因此涨薪快、跳槽更容易;而基础不好的,学习相对较慢,知识输入少,因此涨薪慢、跳槽难。个人能力的高低决定了收入的水平。
|
||||
|
||||
这个事情很现实,也很不公平。但是反过来想,为什么基础不好的同学,不把时间精力拿出来去填补自己的知识空缺呢?如果你的操作系统知识还是一盘散沙,那么请你现在就开始行动,跟着我一起重学操作系统,把这块知识捡起来。愿正在看这篇文章的你,能通过自己的努力去到更好的团队,拿更高的薪水,进而得到更广阔的发展空间。
|
||||
|
||||
|
||||
|
||||
|
124
专栏/重学操作系统-完/00课前必读构建知识体系,可以这样做!.md
Normal file
124
专栏/重学操作系统-完/00课前必读构建知识体系,可以这样做!.md
Normal file
@ -0,0 +1,124 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 课前必读 构建知识体系,可以这样做!
|
||||
我认为,在学习中有一件非常重要的事情,那就是梳理知识体系,所以在进入操作系统课程的学习之前,我想先给你一份这门课程的知识体系(也是一份学习路径),然后再介绍一套我自己梳理知识体系的方法,帮助你更轻松地学好这门课。
|
||||
|
||||
课程内容&知识体系
|
||||
|
||||
我们先来看下这门课程的知识体系结构,分为 8 个模块,39 个课时,具体如下。
|
||||
|
||||
|
||||
模块一:(前置知识)计算机组成原理。 如果你对计算机的组成原理中涉及的比如内存、寄存器工作原理、CPU 指令、总线都是怎么工作的这些基本问题,没有搞清楚,大概率会影响你后续对操作系统的学习。因此,在课程开始前,我先来给你一份操作系统的前置知识,帮助你更好地理解后续内容。
|
||||
模块二:(初探)Linux 指令入门。 这个模块将介绍一些实用的知识,带你入门 Bash 编程,并通过日志分析、性能监控、集群管理等实战场景深入学习 Linux 指令。这些对于日常开发和运维人员来说,都会非常有帮助。
|
||||
模块三:(总纲)操作系统概述。 这部分帮助你了解操作系统的整体设计,介绍内核、用户空间等基本概念,还会介绍操作系统的分类,以及对比一下市面上的操作系统(如 Windows、Linux、Unix、Android 等),让你对整个操作系统生态能有一个整体的认识。
|
||||
|
||||
|
||||
总的来说,模块四 ~ 模块七是我们这门课程的核心内容,也是面试的重点考区。设置这块内容的目的是借助操作系统的知识,帮你思考如何解决实战问题,比如我们反复提及的高并发、数据一致性、大数据存储和网络问题等。
|
||||
|
||||
|
||||
模块四:(面试重点)进程和线程。 我会针对大家在面试和工作中最常见的并发和数据同步问题,从进程原理、多线程编程、互斥和乐观锁、死锁和饥饿、调度算法、进程通信等多个方面,同时结合一些语言特性(比如 Java 的语言特性)讲解原理、思考方案及对策。
|
||||
模块五:(面试重点)内存管理。 这部分我们是从页表和 MMU、虚拟化、内存的分配和回收、缓存置换、逃逸分析、三色算法、生代算法等方面入手,帮助你了解内存的工作原理,应对高并发带来的内存使用问题。
|
||||
模块六:(面试重点)文件系统。 这部分内容我们将从两个方面入手,一方面是通过学习 Linux 的文件目录结构,了解 Linux 下不同的文件目录的功能和作用,帮助你把 Linux 用好;另一个方面,从文件系统的底层设计入手,帮助你了解文件系统的设计思路和原理,并且通过讲解数据库的文件系统,比如 MySQL 的 InnoDb、B+Tree 以及 Hadoop 的 HDFS,帮你把文件系统的知识应用到处理海量数据的领域。
|
||||
模块七:(面试重点)网络与安全。 这部分讲解面试中常见的互联网协议群、TCP 和 UDP 协议、Linux 的 I/O 模型、公私钥加密体系,以及一些最基本的计算机网络安全知识,帮助你理解操作系统和网络之间的交互,从而更好地利用操作系统知识设计业务系统的网络架构。
|
||||
模块八:(知识拓展)虚拟化和其他。 最后这部分,我们将从操作系统的角度学习容器化应用(比如 Kubernetes 和 Docker),还会深入讨论 Linux 架构及商业操作系统。这些知识一方面能够帮你和面试官产生更多的共鸣,另一方面还能帮你开拓视野、打开思路,看到未来的发展趋势。
|
||||
|
||||
|
||||
接下来,我给大家梳理一下操作系统整体的知识框架,帮你扫除知识盲区。
|
||||
|
||||
从知识结构上来看,操作系统最核心的部分是进程,因为操作系统自己不能提供服务,它要想实现价值,就必须通过安装在系统中的应用程序。而安装好的应用程序,启动后就成了进程,所以说进程处在操作系统知识体系的核心。
|
||||
|
||||
了解了以上内容后,我们围绕进程继续梳理,可以发现:
|
||||
|
||||
|
||||
进程往往要同时做很多事情,比如浏览器同时要处理网络、又要处理鼠标、还要展示内容,因此有了多线程的概念。
|
||||
进程需要执行用的存储空间,比如需要存程序指令、需要堆栈存执行数据,因此需要内存。
|
||||
进程需要将一部分数据持久的存储下来,因此需要文件系统。
|
||||
进程需要和外界通信,其中一种途径就是网络。
|
||||
开发过程中我们希望进程可以单独部署,于是需要容器。
|
||||
操作系统内核本身也是一个程序,可以理解成一个进程,它同样是需要单独研究的。
|
||||
|
||||
|
||||
所以,进程是核心,内核、多线程、内存、文件系统、网络、容器和虚拟是配套的能力。我们要想展开操作系统知识的学习,就要先从它的核心——进程入手,通过进程将操作系统的知识串联起来,然后逐一击破。
|
||||
|
||||
到这里,请你思考一个问题:进程本身是做什么的?
|
||||
|
||||
给你一些提示,进程是程序的执行副本,操作系统用进程来分配资源。这里说的资源,就是 CPU 的计算资源、内存和磁盘的存储资源、I/O 设备的使用权等等。所以我们为了更好地学习操作系统,就需要知道计算机是怎么工作的,因此就需要一门前置课——计算机组成原理,我将在模块一把这部分内容给你讲解清楚。
|
||||
|
||||
最后,操作系统通常提供 3 类用户接口:
|
||||
|
||||
|
||||
给程序员用的 API,比如用 C 语言去调用;
|
||||
给运维人员和管理员用的 Shell;
|
||||
给大众用户的图形界面。
|
||||
|
||||
|
||||
通常用 API 是为了定制操作系统的能力,如果你从事云计算、运维开发、嵌入式方向,那么一定会用到 API 。因为用户界面制作成本最高,能力也最少,所以大部分运维人员和管理人员都用 Shell。从这个角度分析,认真学习 Shell 有助于你快速掌握操作系统的基本功能,而且又不会太难。
|
||||
|
||||
以上就是操作系统课程的知识体系结构,你也可以通过目录,快速了解这个课程的内容设置。
|
||||
|
||||
|
||||
|
||||
我是如何梳理知识体系的?
|
||||
|
||||
介绍完操作系统的知识体系,我还想借此再和你聊一聊我梳理知识体系的方法。
|
||||
|
||||
这是一套我运用多年的学习方法,它可以帮你节省时间成本,快速精准地查询到你需要的资料。这个学习方法有点像顺藤摸瓜,我自己称之为:“追溯源头、回归本质”。经过这样的思考,可以帮助你把知识学扎实,从而逐渐形成自己的知识体系。
|
||||
|
||||
我记得有一个技术大牛曾经说过,程序员最重要的是搜索知识的能力,我非常赞同这个说法,此外,我认为如果你想要长远发展,还应同时具备用结构化的思维去构建知识体系的能力。因为知识成体系后,会形成关联记忆和整体的理解,这种经过深度思考和梳理过的知识才能转化为自己的储备。
|
||||
|
||||
下面请你跟我一起进入到场景中,跟着我的思路把你的大脑运转起来。假设,在工作的过程中,我遇到了一块不懂的知识,其中有一个技术名词我不了解它的作用,比如 ReentrantLockLock,那么我该如何解决自己的问题呢?
|
||||
|
||||
|
||||
注意: 你也可以把它替换成任意一个陌生的或者你不理解的技术名词。
|
||||
|
||||
|
||||
首先我会去查阅它的官方文档,然后发现了以下这些线索:
|
||||
|
||||
|
||||
构造函数上有个参数在配置锁的公平性;
|
||||
ReentrantLockLock 是可重入的;
|
||||
功能类似 synchronized 关键字,但是更灵活;
|
||||
支持 lock、unlock、tryLock 等方法;
|
||||
底层是 AbstractQueuedSynchronizer。
|
||||
|
||||
|
||||
接着,根据我获得的知识,追溯 synchronized 关键字,发现 ReentrantLockLock 都说自己的底层是 AbstractQueuedSynchronizer(AQS),我感觉到 AQS 应该是一个重要的东西。
|
||||
|
||||
然后我会去查资料验证我的猜测。这时候,我又得到了一个新的信息:发现AQS是用来实现信号量、条件变量以及其他锁的一个编程框架。
|
||||
|
||||
假设我还不知道信号量、条件变量和锁是什么,于是我通过搜索资料,发现这些名词通通指向一门科学,也就是操作系统。
|
||||
|
||||
接下来,我会去挑选一门讲操作系统的在线课程或者买一本书来查阅,经过查阅发现这些名词出现在进程和多线程这个部分。然后我翻阅了这两个章节的内容,发现了更多我不知道的知识,比如死锁和饥饿、信号量、竞争条件和临界区、互斥的实现,以及最底层的 CPU 指令。
|
||||
|
||||
经过以上过程的推导,我开始在脑海中梳理这些知识点,然后动笔画出了一幅基于思考过程的思维导图,将这些知识点串联起来,如下图所示:
|
||||
|
||||
|
||||
|
||||
注意,上图梳理出来的知识关系不一定对,但是你一定要敢于去画,这个梳理和探索的过程能够带动你主动思考,锻炼主动解决问题的能力。
|
||||
|
||||
输出思维导图后,我将开始学习上面那些超出我现阶段知识储备的内容,然后进行归类和整理。
|
||||
|
||||
这时候,我发现公平锁、可重入锁其实都是锁的一种实现,而 Java 中实现锁这个机制用的是 AQS,而 AQS 最基本的问题是要解决资源竞争的问题。
|
||||
|
||||
通过学习,我发现资源竞争的问题在操作系统里叫作竞争条件,解决方案是让临界区互斥。让临界区互斥可以用算法的实现,但是为了执行效率,更多的情况是利用 CPU 指令。Java 里用于实现互斥的原子操作 CAS,也是基于 CPU 指令的。
|
||||
|
||||
操作系统在解决了互斥问题的基础上,还提供了解决更复杂问题的数据结构,比如说信号量、竞争条件等;而程序语言也提供了数据结构,比如说可重入锁、公平锁。
|
||||
|
||||
经过一番探索,我终于弄明白了,原来实际应用场景中对锁有各种各样的需求,因此不仅仅需要信号量等数据结构,甚至还需要一个快速实现这种数据结构的框架,这个框架就是 AQS。我们可以用 AQS 实现 ReentrantLockLock 的功能。
|
||||
|
||||
|
||||
|
||||
通过上面的方法,我不仅仅可以把 ReentrantLockLockt 学透,而且顺藤摸瓜找到了所有关联的知识点,比如 AQS 和 CAS。比起理解最初的知识点,更重要的是我通过这种方法形成了自己的一个知识体系;而且,我会发现在这个知识体系中,操作系统是起到支撑作用的骨架。
|
||||
|
||||
与此同时,我还认识到了计算机语言和操作系统之间的联系非常紧密,操作系统知识是学习计算机语言的根基。于是我开始制定学习计划,投入时间学习操作系统。我更偏爱做一次性的时间投入,以防止日后碎片化学习做多次投入,陷入时间黑洞,而这个嗜好让我受益良多。
|
||||
|
||||
寄语
|
||||
|
||||
最后,希望今天的课程和学习方法可以帮助到你;也希望你平时遇到未知的问题,尝试使用我今天介绍的方法,学会构建自己的知识体系,提高自己的学商。你可以在留言区给自己立下 Flag,比如给自己定一个具体的目标,或者是打卡你的学习天数,总之希望你不要一看而过,留下你的思考。经过长期的积累,相信你会得到意想不到的收获。
|
||||
|
||||
|
||||
|
||||
|
147
专栏/重学操作系统-完/01计算机是什么:“如何把程序写好”这个问题是可计算的吗?.md
Normal file
147
专栏/重学操作系统-完/01计算机是什么:“如何把程序写好”这个问题是可计算的吗?.md
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 计算机是什么:“如何把程序写好”这个问题是可计算的吗?
|
||||
我记得自己在面试中遇到过这样一个问题:“可不可以计算一个人程序写得好不好?”
|
||||
|
||||
当时我也没有想明白“计算”这个词是什么意思。但事后分析来看,“计算”不就是写程序吗?
|
||||
|
||||
其实简单理解这个问题就是“可不可以用机器来判断人的程序写得好不好?”如果从这个角度考虑,我是可以和面试官论述一番的。
|
||||
|
||||
后面我查阅了资料,历史上有一个对计算机领域影响颇深的可计算理论,面试官说的“计算”应该就来源于这里。其实继续深挖还能找出很多涉及计算机本源的有趣的知识,比如图灵机、冯诺依曼模型;再比如说 CPU 的构成、程序如何执行、缓存的分级、总线的作用等。
|
||||
|
||||
上面提到的这些内容其实都属于操作系统的前置课程,我会利用第一章 4 个课时和大家探讨一下计算机组成原理,然后我们再正式进入操作系统的学习。其实学习就是这样,追溯源头,回到本质,才能挖掘兴趣、激发思考,否则就变成了死记硬背。接下来我们将从计算能源的角度入手,来展开今天的课程学习。
|
||||
|
||||
芯片:计算能源
|
||||
|
||||
我们知道第一次工业革命出现了蒸汽机,能源是煤炭。第二次工业革命出现了发电机,能源是电。20 世纪四五十年代,又发生了第三次科技革命,革命产物是计算机。而第四次科技革命,就发生在当下,出现了人工智能,能源是数据。
|
||||
|
||||
说到这里,你可能会有个疑问:第三次科技革命的能源是什么呢?
|
||||
|
||||
你的第一反应可能是电,但是细想又觉得不对。前两次工业革命都有带来能源变革,为什么第三次科技革命就没有了能源变革?其实,第三次科技革命的能源是一种数字能量,本质是计算。
|
||||
|
||||
下面我们来看一看这种数字能量是如何产生的。电能供给给芯片,芯片中的一种电子元件晶振(也就是石英晶体)通电后产生震荡,震荡会产生频率稳定的脉冲信号。通常这是一种高频的脉冲信号,每秒可达百万次。然后,我们通过谐振效应发放这个信号,形成方波。再通过电子元件调整这种脉冲的频率,把脉冲信号转换为我们需要的频率,这就形成了驱动芯片工作的时钟信号。这种信号的频率,我们也称作芯片的时钟频率。最后,时钟信号驱动着芯片工作,就像人体的脉搏一样,每一次脉冲到来,都让芯片的状态发生一次变化,用这种方法,最终存储器中的指令被一行行执行。指令被执行,其实就是数据被计算,这就是我说的计算能量。
|
||||
|
||||
芯片普及后,不仅给计算机和手机提供支持,它们还被安装到了航天设备、能源设备、医疗设备及通信设备中,甚至小到电灯、微波炉、咖啡机、热水器里面都有了芯片。有了芯片,设备通电后才可以计算,有了计算,这些设备才能够实现更加复杂而精确的功能。
|
||||
|
||||
摩尔定律:计算能力的发展
|
||||
|
||||
值得一提的是,历史上是先有计算机,后有的芯片。世界上第一个芯片,也被称作集成电路, 1958 年由美国德州仪器公司的工程师杰克·基尔比发明。而世界上第一台通用计算机 ENIAC 则是在 1946 年诞生于美国陆军弹道研究实验室。
|
||||
|
||||
看到这里你可能会有疑问,为什么是先发明计算机再发明芯片呢?
|
||||
|
||||
其实,这个道理就好比很多程序员先实现产品功能,再考虑封装和复用。ENIAC 中负责计算的模块和后来的芯片原理是一样的,都是利用电路实现逻辑运算。只不过在 20 世纪 40 年代人们还没有将这种能力抽象成一个独立的产品,而且也没有办法解决电路体积的问题,ENIAC的体积看上去就像一所学校那么大。
|
||||
|
||||
芯片的计算能力来源于芯片内部的集成电路,集成电路大大减小了电路的体积,所有的元件都是用同一块半导体材料制作而成,也就是把所有的电路都集成到了一个单一的硅片上。为了提高计算性能,集成电路越来越复杂,里面的电子元件也越来越多。从最早拥有 100 个左右晶体管的小型集成电路,发展到 21 世纪初,拥有上亿电子元件的巨大规模集成电路。
|
||||
|
||||
芯片的发展,带来了计算能力的飞跃,ENIAC 只能每秒计算 5000 次加法和 400 次乘法,到 1978 年 8086 芯片已经可以每秒计算百万次了。而今天随便一个芯片不但可以轻轻松松每秒计算数亿次,而且不只有一个核心,是多个核心都能达到这一量级的计算能力。
|
||||
|
||||
在当时那个年代,Intel 的创始人之一摩尔就观察到了这个现象,并提出了摩尔定律:当价格不变时,集成电路中可容纳的晶体管数目约每隔 18~24 个月就会增加一倍,性能也将提升一倍。这一定律揭示了信息技术发展的速度,但到今天,摩尔定律失效了。因为随着芯片越来越小,在尺寸和散热等方面已经挑战了人类的极限,芯片中无法再放入更多的电子元件了。
|
||||
|
||||
但是计算能力又开始以另一种方式发展,比如一个普普通通的 NVIDA 显卡中就拥有了几百个核心,这样就可以进行大量的并发计算;另外,一个分布式的大数据集群,里面就可能有上千个核心。
|
||||
|
||||
展望未来,计算能力还有更多的增长点,不仅有可以无限提高计算能力的量子计算机,还有利用光学元件替代晶体元件的光电集成电路。
|
||||
|
||||
可计算理论:图灵机
|
||||
|
||||
当然,在科学家们尝试发明计算机和芯片之前,他们必须回答一个问题,那就是计算或者程序可以用来做什么?比如:计算可不可以用来做饭?换一个更专业的说法,做饭可不可以被计算?
|
||||
|
||||
生活在数字时代的我们,用着导航、玩着游戏,本能地知道很多问题是可以被计算的,但是生活在 20 世纪初的科学家们,需要在没有计算机和芯片的时代就想清楚这些问题,并不是一件容易的事情。
|
||||
|
||||
公理化体系和不完备性定理
|
||||
|
||||
最早在 19 世纪初,德国著名数学家希尔伯特提出:这个世界可以建立一套完善的公理体系,由少数几个公理出发,推导出所有的定理和推论。这样就可以逐渐通过这种方法将世界上的万事万物都统一到一个体系中。
|
||||
|
||||
当然,这只是一个非常美好的设想,如果万事万物都可以用形式化(简单理解就是程序化规范化)的手段统一到一套体系中,也就意味着计算能力将被无限扩展,只要给定足够的时间和空间,计算机就可以完成任何工作。
|
||||
|
||||
但在不久后,美籍数学家哥德尔就提出了哥德尔不完备性定理,内容是:即便在完善的公理体系中仍然可以找到不能被证明也不能被证伪的命题。
|
||||
|
||||
这让我联想到,一说谎,鼻子就会变长的匹诺曹。如果他说“我说谎了”,那么他的鼻子应该变长还是变短呢?对于人类而言,这个问题可以理解,但是对于计算机来说这个问题是不可以被计算的。
|
||||
|
||||
正是因为世界上存在着大量的这种“公说公有理,婆说婆有理”的问题,才让大家认识到计算不能解决所有问题,所以:计算机能力也是有边界的。哥德尔的不完备性定理,让大家看到了世界上还有大量不可计算的问题。
|
||||
|
||||
图灵机和可计算理论
|
||||
|
||||
于是人们意识到了需要一个理论,专门回答这样的问题:哪些问题可以被计算,哪些不可以被计算,这就是可计算性理论,该理论是计算机科学的理论基础之一。
|
||||
|
||||
1936 年,被誉为人工智能之父的阿兰·图灵提出了图灵机,它是一种不断执行指令的抽象计算机。之所以说抽象,是因为图灵并没有真的造出这台机器,而是把它当成理论去和大家探讨可计算问题。
|
||||
|
||||
图灵发现如果一个问题是可计算的,那么它的解决方案就必须可以被具化成一条条的指令,也就是可以使用图灵机处理。因此,不能使用图灵机处理的问题,都是不可计算的问题。
|
||||
|
||||
比如一个马达的控制程序是可计算的,因为控制过程是可以被抽象成一条条指令的(即可以写程序实现)。比如程序可以先读入传感器的数据,然后根据数据计算出下面要进行加速还是减速。
|
||||
|
||||
不可计算问题
|
||||
|
||||
但当图灵机遇到“素数是不是有无穷多个?”这样的问题时,事情就变得复杂了。虽然,我们可以通过有限的步骤计算出下一个素数。比如可以每次尝试一个更大的数字,然后通过一系列计算过程判断该数字是不是素数,直到找到一个更大的素数。古希腊数学家埃拉托斯特尼就发明了筛选出给定范围内所有素数的方法。
|
||||
|
||||
|
||||
|
||||
如上图所示,我们利用埃拉托斯特尼筛法找到的素数越来越多。但是,我们还是不能回答“素数是不是有无穷多个”这样的问题。因为要回答这样的问题,我们会不停地寻找下一个素数。如果素数是无穷的,那么我们的计算就是无穷无尽的,所以这样的问题不可计算。
|
||||
|
||||
停机问题
|
||||
|
||||
我们也无法实现用一个通用程序去判断另一个程序是否会停止。比如你用运行这段程序来检查一个程序是否会停止时,你会发现不能因为这个程序执行了 1 天,就判定它不会停止,也不能因为这个程序执行了 10 年,从而得出它不会停止的结论。这个问题放到图灵机领域,叫作停机问题,我们无法给出一个判断图灵机是否会停机的通用方法,因此停机问题是一个经典的不可计算问题。
|
||||
|
||||
计算能力的边界在哪里?
|
||||
|
||||
我们可以把世界上想解决的事情都称作问题,解决问题往往需要消耗芯片的计算能力,这通常称作时间开销,另外解决问题还需要消耗内存,称作空间开销。
|
||||
|
||||
问题的分类
|
||||
|
||||
世界上有一类问题,无论我们消耗多少时间和空间也无法解决,这类问题就包括“停机问题”,称作不可计算问题,我们无法用计算机精确地解决这类问题。世界上不可计算问题多,还是可计算问题多,也是一个不可计算问题,但直觉告诉我们一定是不可计算问题更多。
|
||||
|
||||
另外在可计算的问题中,有困难问题,也有简单问题,我们通常用复杂度来衡量,比如:
|
||||
|
||||
|
||||
“求数组第 10 个元素”,计算这种问题,时间开销、空间开销都不会随着问题规模增长,我们记为 O(1);
|
||||
“求数组中的最大值”,计算这种问题,时间开销会随着数组规模线性增大,记作 O(N),N 是问题的规模;
|
||||
还有像“求一个n*n矩阵的和”,如果n是规模,那么时间开销会随着问题规模的平方增长,我们称作 O(N2);
|
||||
当然也有更加复杂的数学模型,比如说O(N3)、O(N4)、O(N100)等。
|
||||
|
||||
|
||||
P 问题 vs NP 问题
|
||||
|
||||
按照摩尔定律所说,人类的计算能力每 18~24 个月翻一倍,我们的计算能力在呈指数形式上升。因此,在所有可以计算的问题中,像 O(N1000)的问题,虽然现在的计算能力不够,但是相信在遥远的未来,我们会拥有能力解决。这种我们有能力解决的问题,统称为多项式时间( Polynomial time)问题。我们今天能解决的问题,都是多项式时间的问题,下面记为 P 类型的问题。
|
||||
|
||||
|
||||
|
||||
另外,还有一类问题复杂度本身也是指数形式的问题,比如 O(2N)的问题。这类型的问题随着规模 N 上升,时间开销的增长速度和人类计算能力增长速度持平甚至更快。因此虽然这类问题可以计算,但是当 N 较大时,因为计算能力不足,最终结果依然无法被解决。
|
||||
|
||||
由此可见,不是所有可以计算的问题都可以被解决,问题如果不能在多项式时间内找到答案,我们记为 NP 问题。
|
||||
|
||||
有一部分 NP 问题可以被转化为 P 问题,比如斐波那契数列求第 N 项,可以用缓存、动态规划等方式转化为 O(N) 的问题。但还有更多的 NP 问题,比如一个集合,找出和为零的子集,就没能找到一个合适的转换方法。其实说这么多,就是想告诉大家:如今还有很多问题无法解决,它的数量远远大于我们可以解决的问题,科学家、工程师们也只能望洋兴叹了。
|
||||
|
||||
人工智能
|
||||
|
||||
此外,包括停机问题、包括 NP 问题在内的很多问题,虽然不能解决,但可以努力让计算机的解决方案超过人类的水平,这就是人工智能。
|
||||
|
||||
比如下围棋,围棋盘是 19*19 的,共有 361!种情况,如果遍历 361!种情况,并进行打分,共有 10 的 170 次方种可能,因此,我们的计算能力是远远不足的。但是如果使用人工智能方法对可能出现的部分情况进行概率判断,在不追求绝对精确的情况下,人工智能就可以超过人类选手。
|
||||
|
||||
AlphaGo 战胜李世石就是利用了基于概率的不完全解法,这种解法已经可以超过部分人类职业选手了,也就是说计算机的解法已经超过了人类。当然,人类的强项在于理解和分析,人有两种思维,归纳和假设,这两种思维都是计算机无法计算的。机器用概率理解围棋,局部来说机器下得更好,但是人可以制造机器,因此,人的感悟更有意义,谈不上孰优孰劣。
|
||||
|
||||
针对这种解决问题的方法,20 世纪中人工智能之父图灵,提出图灵测试,就是在一次人机对话中,随机抽样一部分的实验者和机器对话,如果这部分实验者有较大的百分比判断对面是人而不是机器,那这台机器就通过了图灵测试。在围棋领域,可以说,AI 通过了图灵测试。但围棋的 AI 不能下象棋,这也是 AI 的一个劣势。所以广义的 AI 还没有出现,现在出现的是在某个专业领域的 AI。
|
||||
|
||||
总结
|
||||
|
||||
下面我们进行总结。本课时是一个理解操作系统知识必不可少的计算机原理引导课。
|
||||
|
||||
|
||||
我们学习了芯片,芯片将电能转化为计算能量,计算能量推动程序执行;
|
||||
接着提到了摩尔定律,了解到我们的计算能力仍在飞速发展;
|
||||
还花了篇幅讲了图灵机,从而进一步认识了人工智能之父阿兰·图灵,图灵机具体的设计和构造,这将在02 课时程序的执行部分进一步讨论。
|
||||
最后普及了图灵测试和人工智能的基本概念,带你了解了计算机的能力边界。
|
||||
|
||||
|
||||
下面我们回到最初的问题:“可不可以计算一个人程序写得好不好?”
|
||||
|
||||
这个问题可以这样来思考,如果把问题降级,变成:“可不可以计算一个人写的程序会不会停机?”
|
||||
|
||||
这个问题就如同停机问题,无法计算,因此这是一个不可计算的问题。但是我们通过设立规则,比如检查缩进、检查函数的复用情况、检查类的命名情况,给写程序的人更好的建议。另外,我们也可以通过 AI 技术,让机器在“程序写得好不好”这个问题的判定能力上,达到人类的水平,通过图灵测试。
|
||||
|
||||
综上,从绝对的对错角度去看,这是一个不可计算问题,因为它没有办法被完全解决;但是从图灵测试层面来看,虽然目前无法解决这个问题,但是我们有理由相信,在未来,计算机对这个问题的解决方案,是可以超过人类的。
|
||||
|
||||
|
||||
|
||||
|
192
专栏/重学操作系统-完/02程序的执行:相比32位,64位的优势是什么?(上).md
Normal file
192
专栏/重学操作系统-完/02程序的执行:相比32位,64位的优势是什么?(上).md
Normal file
@ -0,0 +1,192 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 程序的执行:相比 32 位,64 位的优势是什么?(上)
|
||||
本节课给你讲学习操作系统之前的一个前置知识:程序是如何执行的?
|
||||
|
||||
我们先来看一道常规的面试题:相比 32 位,64 位的优势是什么?
|
||||
|
||||
面试官考察这种类型的问题,主要是想看求职者是否有扎实的计算机基础,同时想知道求职者在工作中是否充满好奇,会主动学习、寻根问底,毕竟 32、64 位是经常出现在程序员视野的词汇,常见的东西都弄明白了,那说明这个人学习能力强。
|
||||
|
||||
其实 ,面试官在这里给你挖了一个陷阱,因为他没有说清楚 32、64 位指的是操作系统、是软件、还是 CPU?
|
||||
|
||||
|
||||
如果是软件,那么我们的数据库有 32 位和 64 位版本;
|
||||
如果是操作系统,那么在阿里云上选择 Centos 和 Debian 版本的时候,也会有 32⁄64 版本;
|
||||
如果是 CPU,那么有 32 位 CPU,也有 64 位 CPU。
|
||||
|
||||
|
||||
接下来请你带着问题开始今天的课程学习,本课时的重点是带你学懂程序执行的原理。
|
||||
|
||||
图灵机的构造
|
||||
|
||||
想要学懂程序执行的原理,就要从图灵机说起了。它在计算机科学方面有两个巨大的贡献:
|
||||
|
||||
第一,它清楚地定义了计算机能力的边界,也就是可计算理论;
|
||||
|
||||
第二,它定义了计算机由哪些部分组成,程序又是如何执行的。
|
||||
|
||||
|
||||
|
||||
我们先来看一看图灵机的内部构造:
|
||||
|
||||
|
||||
图灵机拥有一条无限长的纸带,纸带上是一个格子挨着一个格子,格子中可以写字符,你可以把纸带看作内存,而这些字符可以看作是内存中的数据或者程序。
|
||||
图灵机有一个读写头,读写头可以读取任意格子上的字符,也可以改写任意格子的字符。
|
||||
读写头上面的盒子里是一些精密的零件,包括图灵机的存储、控制单元和运算单元。
|
||||
|
||||
|
||||
图灵机如何执行程序
|
||||
|
||||
下面我们来举一个例子,让大家弄清楚图灵机是如何工作的,比如我们要计算 11 + 15 的值,具体的运算步骤如下:
|
||||
|
||||
|
||||
首先,我们将“11、15、+” 分别写入纸带上的 3 个格子(现在纸带上的字符串是11、15、 +),然后将读写头先停在 11 对应的格子上。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
接下来,图灵机通过读写头读入 11 到它的存储设备中(这个存储设备也叫作图灵机的状态)。图灵机没有说读写头为什么可以识别纸带上的字符,而是假定读写头可以做到这点。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
然后读写头向右移动一个格,用同样的方法将 15 读入图灵机的状态中。现在图灵机的状态中有两个连续的数字,11 和 15。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
接下来重复上面的过程,会读到一个+号。下面我详细说一下这个运算流程:
|
||||
|
||||
|
||||
读写头读到一个 + 号 ;
|
||||
然后将 + 号传输给控制单元 ;
|
||||
控制单元发现是一个 + 号,所以没有存入状态中。因为 + 号是一个我们预设的控制符(指令),它的作用是加和目前状态。因此,控制单元识别出是控制符,并通知运算单元工作;
|
||||
运算单元从状态中读入 11、15 并进行计算,将结果 26 存储到状态;
|
||||
运算单元将结果回传给控制单元;
|
||||
控制单元将结果传输给读写头。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
读写头向右移动,将结果 26 写入纸带。
|
||||
|
||||
|
||||
|
||||
|
||||
这样,我们就通过图灵机计算出了 11+15 的值。不知道你有没有发现,图灵机构造的这一台机器,主要功能就是读写纸带然后计算;纸带中有数据、也有控制字符(也就是指令),这个设计和我们今天的计算机是一样的。
|
||||
|
||||
图灵通过数学证明了,一个问题如果可以拆解成图灵机的可执行步骤,那问题就是可计算的。另一方面,图灵机定义了计算机的组成以及工作原理,但是没有给出具体的实现。
|
||||
|
||||
冯诺依曼模型
|
||||
|
||||
|
||||
|
||||
具体的实现是 1945 年冯诺依曼和其他几位科学家在著名的 101 页报告中提出的。报告遵循了图灵机的设计,并提出用电子元件构造计算机,约定了用二进制进行计算和存储,并且将计算机结构分成以下 5 个部分:
|
||||
|
||||
|
||||
输入设备;
|
||||
输出设备;
|
||||
内存;
|
||||
中央处理器;
|
||||
总线。
|
||||
|
||||
|
||||
这个模型也被称为冯诺依曼模型,下面我们具体来看看这 5 部分的作用。
|
||||
|
||||
内存
|
||||
|
||||
在冯诺依曼模型中,程序和数据被存储在一个被称作内存的线性排列存储区域。存储的数据单位是一个二进制位,英文是 bit。最小的存储单位叫作字节,也就是 8 位,英文是 byte,每一个字节都对应一个内存地址。内存地址由 0 开始编号,比如第 1 个地址是 0,第 2 个地址是 1, 然后自增排列,最后一个地址是内存中的字节数减 1。
|
||||
|
||||
我们通常说的内存都是随机存取器,也就是读取任何一个地址数据的速度是一样的,写入任何一个地址数据的速度也是一样的。
|
||||
|
||||
CPU
|
||||
|
||||
冯诺依曼模型中 CPU 负责控制和计算。为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。
|
||||
|
||||
|
||||
如果 CPU 每次可以计算 4 个 byte,那么我们称作 32 位 CPU;
|
||||
如果 CPU 每次可以计算 8 个 byte,那么我们称作 64 位 CPU。
|
||||
|
||||
|
||||
这里的 32 和 64,称作 CPU 的位宽。
|
||||
|
||||
为什么 CPU 要这样设计呢? 因为一个 byte 最大的表示范围就是 0~255。比如要计算 20000*50,就超出了byte 最大的表示范围了。因此,CPU 需要支持多个 byte 一起计算。当然,CPU 位数越大,可以计算的数值就越大。但是在现实生活中不一定需要计算这么大的数值。比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。
|
||||
|
||||
控制单元和逻辑运算单元
|
||||
|
||||
CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。具体的工作原理我们在指令部分给大家分析。
|
||||
|
||||
寄存器
|
||||
|
||||
CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。这种存储就是寄存器。寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。
|
||||
|
||||
|
||||
寄存器中有一部分是可供用户编程用的,比如用来存加和指令的两个参数,是通用寄存器。
|
||||
还有一部分寄存器有特殊的用途,叫作特殊寄存器。比如程序指针,就是一个特殊寄存器。它存储了 CPU 要执行的下一条指令所在的内存地址。注意,程序指针不是存储了下一条要执行的指令,此时指令还在内存中,程序指针只是存储了下一条指令的地址。
|
||||
下一条要执行的指令,会从内存读入到另一个特殊的寄存器中,这个寄存器叫作指令寄存器。指令被执行完成之前,指令都存储在这里。
|
||||
|
||||
|
||||
总线
|
||||
|
||||
CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。总线分成 3 种:
|
||||
|
||||
|
||||
一种是地址总线,专门用来指定 CPU 将要操作的内存地址。
|
||||
还有一种是数据总线,用来读写内存中的数据。
|
||||
|
||||
|
||||
当 CPU 需要读写内存的时候,先要通过地址总线来指定内存地址,再通过数据总线来传输数据。
|
||||
|
||||
|
||||
最后一种总线叫作控制总线,用来发送和接收关键信号,比如后面我们会学到的中断信号,还有设备复位、就绪等信号,都是通过控制总线传输。同样的,CPU 需要对这些信号进行响应,这也需要控制总线。
|
||||
|
||||
|
||||
输入、输出设备
|
||||
|
||||
输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。
|
||||
|
||||
到这里,相信你已经对冯诺依曼模型的构造有了一定的了解。这里我再强调几个问题:
|
||||
|
||||
1. 线路位宽问题
|
||||
|
||||
第一个问题是,你可能会好奇数据如何通过线路传递。其实是通过操作电压,低电压是 0,高电压是 1。
|
||||
|
||||
如果只有一条线路,每次只能传递 1 个信号,因为你必须在 0,1 中选一个。比如你构造高高低低这样的信号,其实就是 1100,相当于你传了一个数字 10 过去。大家注意,这种传递是相当慢的,因为你需要传递 4 次。
|
||||
|
||||
这种一个 bit 一个 bit 发送的方式,我们叫作串行。如果希望每次多传一些数据,就需要增加线路,也就是需要并行。
|
||||
|
||||
如果只有 1 条地址总线,那每次只能表示 0-1 两种情况,所以只能操作 2 个内存地址;如果有 10 条地址总线,一次就可以表示 210 种情况,也就是可以操作 1024 个内存地址;如果你希望操作 4G 的内存,那么就需要 32 条线,因为 232 是 4G。
|
||||
|
||||
到这里,你可能会问,那我串行发送行不行?当然也不是不行,只是速度会很慢,因为每多增加一条线路速度就会翻倍。
|
||||
|
||||
2. 64 位和 32 位的计算
|
||||
|
||||
第二个问题是,CPU 的位宽会对计算造成什么影响?
|
||||
|
||||
我们来看一个具体场景:要用 32 位宽的 CPU,加和两个 64 位的数字。
|
||||
|
||||
32 位宽的 CPU 控制 40 位宽的地址总线、数据总线工作会非常麻烦,需要双方制定协议。 因此通常 32 位宽 CPU 最多操作 32 位宽的地址总线和数据总线。
|
||||
|
||||
因此必须把两个 64 位数字拆成 2 个 32 位数字来计算,这样就需要一个算法,比如用像小时候做加法竖式一样,先加和两个低位的 32 位数字,算出进位,然后加和两个高位的 32 位数字,最后再加上进位。
|
||||
|
||||
而 64 位的 CPU 就可以一次读入 64 位的数字,同时 64 位的 CPU 内部的逻辑计算单元,也支持 64 位的数字进行计算。但是你千万不要仅仅因为位宽的区别,就认为 64 位 CPU 性能比 32 位高很多。
|
||||
|
||||
要知道大部分应用不需要计算超过 32 位的数字,比如你做一个电商网站,用户的金额通常是 10 万以下的,而 32 位有符号整数,最大可以到 20 亿。所以这样的计算在 32 位还是 64 位中没有什么区别。
|
||||
|
||||
还有一点要注意,32 位宽的 CPU 没办法控制超过 32 位的地址总线、数据总线工作。比如说你有一条 40 位的地址总线(其实就是 40 条线),32 位的 CPU 没有办法一次给 40 个信号,因为它最多只有 32 位的寄存器。因此 32 位宽的 CPU 最多操作 232 个内存地址,也就是 4G 内存地址。
|
||||
|
||||
总结
|
||||
|
||||
关于计算机组成和指令部分,我们就先学到这里。这节课我们通过图灵机和冯诺依曼模型学习了计算机的组成、CPU 的工作原理等。此外,我们还顺带讨论了 32 位和 64 位的区别,现在,你可以回答 64 位和 32 位比较有哪些优势了吗?
|
||||
|
||||
|
||||
|
||||
|
202
专栏/重学操作系统-完/03程序的执行:相比32位,64位的优势是什么?(下).md
Normal file
202
专栏/重学操作系统-完/03程序的执行:相比32位,64位的优势是什么?(下).md
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 程序的执行:相比 32 位,64 位的优势是什么?(下)
|
||||
在 02 课时中我们学习了计算机的组成原理,还分析了一些你在工作中可能会遇到的问题。本课时,我们继续深入学习程序执行部分,进一步讨论程序在冯诺依曼模型上如何执行。
|
||||
|
||||
程序的执行过程
|
||||
|
||||
当 CPU 执行程序的时候:
|
||||
|
||||
1.首先,CPU 读取 PC 指针指向的指令,将它导入指令寄存器。具体来说,完成读取指令这件事情有 3 个步骤:
|
||||
|
||||
步骤 1:CPU 的控制单元操作地址总线指定需要访问的内存地址(简单理解,就是把 PC 指针中的值拷贝到地址总线中)。
|
||||
|
||||
步骤 2:CPU 通知内存设备准备数据(内存设备准备好了,就通过数据总线将数据传送给 CPU)。
|
||||
|
||||
步骤 3:CPU 收到内存传来的数据后,将这个数据存入指令寄存器。
|
||||
|
||||
完成以上 3 步,CPU 成功读取了 PC 指针指向指令,存入了指令寄存器。
|
||||
|
||||
2.然后,CPU 分析指令寄存器中的指令,确定指令的类型和参数。
|
||||
3.如果是计算类型的指令,那么就交给逻辑运算单元计算;如果是存储类型的指令,那么由控制单元执行。
|
||||
4.PC 指针自增,并准备获取下一条指令。
|
||||
|
||||
|
||||
比如在 32 位的机器上,指令是 32 位 4 个字节,需要 4 个内存地址存储,因此 PC 指针会自增 4。
|
||||
|
||||
|
||||
|
||||
|
||||
了解了程序的执行过程后,我还有一些问题想和大家一起讨论:
|
||||
|
||||
|
||||
内存虽然是一个随机存取器,但是我们通常不会把指令和数据存在一起,这是为了安全起见。具体的原因我会在模块四进程部分展开讲解,欢迎大家在本课时的留言区讨论起来,我会结合你们留言的内容做后续的课程设计。
|
||||
程序指针也是一个寄存器,64 位的 CPU 会提供 64 位的寄存器,这样就可以使用更多内存地址。特别要说明的是,64 位的寄存器可以寻址的范围非常大,但是也会受到地址总线条数的限制。比如和 64 位 CPU 配套工作的地址总线只有 40 条,那么可以寻址的范围就只有 1T,也就是 240。
|
||||
从 PC 指针读取指令、到执行、再到下一条指令,构成了一个循环,这个不断循环的过程叫作CPU 的指令周期,下面我们会详细讲解这个概念。
|
||||
|
||||
|
||||
详解 a = 11 + 15 的执行过程
|
||||
|
||||
上面我们了解了基本的程序执行过程,接下来我们来看看如果用冯诺依曼模型执行a=11+15是一个怎样的过程。
|
||||
|
||||
我们再 Review 下这个问题:程序员写的程序a=11+15是字符串,CPU 不能执行字符串,只能执行指令。所以这里需要用到一种特殊的程序——编译器。编译器的核心能力是翻译,它把一种程序翻译成另一种程序语言。
|
||||
|
||||
这里,我们需要编译器将程序员写的程序翻译成 CPU 认识的指令(指令我们认为是一种低级语言,我们平时书写的是高级语言)。你可以先跟我完整地学完操作系统,再去深入了解编译原理的内容。
|
||||
|
||||
下面我们来详细阐述 a=11+15 的执行过程:
|
||||
|
||||
1.编译器通过分析,发现 11 和 15 是数据,因此编译好的程序启动时,会在内存中开辟出一个专门的区域存这样的常数,这个专门用来存储常数的区域,就是数据段,如下图所示:
|
||||
|
||||
|
||||
11 被存储到了地址 0x100;
|
||||
15 被存储到了地址 0x104;
|
||||
|
||||
|
||||
|
||||
|
||||
2.编译器将a=11+15转换成了 4 条指令,程序启动后,这些指令被导入了一个专门用来存储指令的区域,也就是正文段。如上图所示,这 4 条指令被存储到了 0x200-0x20c 的区域中:
|
||||
|
||||
0x200 位置的 load 指令将地址 0x100 中的数据 11 导入寄存器 R0;
|
||||
|
||||
0x204 位置的 load 指令将地址 0x104 中的数据 15 导入寄存器 R1;
|
||||
|
||||
0x208 位置的 add 指令将寄存器 R0 和 R1 中的值相加,存入寄存器 R2;
|
||||
|
||||
0x20c 位置的 store 指令将寄存器 R2 中的值存回数据区域中的 0x1108 位置。
|
||||
|
||||
3.具体执行的时候,PC 指针先指向 0x200 位置,然后依次执行这 4 条指令。
|
||||
|
||||
这里还有几个问题要说明一下:
|
||||
|
||||
|
||||
变量 a 实际上是内存中的一个地址,a 是给程序员的助记符。
|
||||
为什么 0x200 中代表加载数据到寄存器的指令是 0x8c000100,我们会在下面详细讨论。
|
||||
不知道细心的同学是否发现,在上面的例子中,我们每次操作 4 个地址,也就是 32 位,这是因为我们在用 32 位宽的 CPU 举例。在 32 位宽的 CPU 中,指令也是 32 位的。但是数据可以小于 32 位,比如可以加和两个 8 位的字节。
|
||||
关于数据段和正文段的内容,会在模块四进程和线程部分继续讲解。
|
||||
|
||||
|
||||
指令
|
||||
|
||||
接下来我会带你具体分析指令的执行过程。
|
||||
|
||||
在上面的例子中,load 指令将内存中的数据导入寄存器,我们写成了 16 进制:0x8c000100,拆分成二进制就是:
|
||||
|
||||
|
||||
这里大家还是看下图,需要看一下才能明白。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
最左边的 6 位,叫作操作码,英文是 OpCode,100011 代表 load 指令;
|
||||
中间的 4 位 0000是寄存器的编号,这里代表寄存器 R0;
|
||||
后面的 22 位代表要读取的地址,也就是 0x100。
|
||||
|
||||
|
||||
所以我们是把操作码、寄存器的编号、要读取的地址合并到了一个 32 位的指令中。
|
||||
|
||||
我们再来看一条求加法运算的 add 指令,16 进制表示是 0x08048000,换算成二进制就是:
|
||||
|
||||
|
||||
|
||||
|
||||
最左边的 6 位是指令编码,代表指令 add;
|
||||
紧接着的 4 位 0000 代表寄存器 R0;
|
||||
然后再接着的 4 位 0001 代表寄存器 R1;
|
||||
再接着的 4 位 0010 代表寄存器 R2;
|
||||
最后剩下的 14 位没有被使用。
|
||||
|
||||
|
||||
构造指令的过程,叫作指令的编码,通常由编译器完成;解析指令的过程,叫作指令的解码,由 CPU 完成。由此可见 CPU 内部有一个循环:
|
||||
|
||||
|
||||
首先 CPU 通过 PC 指针读取对应内存地址的指令,我们将这个步骤叫作 Fetch,就是获取的意思。
|
||||
CPU 对指令进行解码,我们将这个部分叫作 Decode。
|
||||
CPU 执行指令,我们将这个部分叫作 Execution。
|
||||
CPU 将结果存回寄存器或者将寄存器存入内存,我们将这个步骤叫作 Store。
|
||||
|
||||
|
||||
|
||||
|
||||
上面 4 个步骤,我们叫作 CPU 的指令周期。CPU 的工作就是一个周期接着一个周期,周而复始。
|
||||
|
||||
指令的类型
|
||||
|
||||
通过上面的例子,你会发现不同类型(不同 OpCode)的指令、参数个数、每个参数的位宽,都不一样。而参数可以是以下这三种类型:
|
||||
|
||||
|
||||
寄存器;
|
||||
内存地址;
|
||||
数值(一般是整数和浮点)。
|
||||
|
||||
|
||||
当然,无论是寄存器、内存地址还是数值,它们都是数字。
|
||||
|
||||
指令从功能角度来划分,大概有以下 5 类:
|
||||
|
||||
|
||||
I/O 类型的指令,比如处理和内存间数据交换的指令 store/load 等;再比如将一个内存地址的数据转移到另一个内存地址的 mov 指令。
|
||||
计算类型的指令,最多只能处理两个寄存器,比如加减乘除、位运算、比较大小等。
|
||||
跳转类型的指令,用处就是修改 PC 指针。比如编程中大家经常会遇到需要条件判断+跳转的逻辑,比如 if-else,swtich-case、函数调用等。
|
||||
信号类型的指令,比如发送中断的指令 trap。
|
||||
闲置 CPU 的指令 nop,一般 CPU 都有这样一条指令,执行后 CPU 会空转一个周期。
|
||||
|
||||
|
||||
指令还有一个分法,就是寻址模式,比如同样是求和指令,可能会有 2 个版本:
|
||||
|
||||
|
||||
将两个寄存器的值相加的 add 指令。
|
||||
将一个寄存器和一个整数相加的 addi 指令。
|
||||
|
||||
|
||||
另外,同样是加载内存中的数据到寄存器的 load 指令也有不同的寻址模式:
|
||||
|
||||
|
||||
比如直接加载一个内存地址中的数据到寄存器的指令la,叫作直接寻址。
|
||||
直接将一个数值导入寄存器的指令li,叫作寄存器寻址。
|
||||
将一个寄存器中的数值作为地址,然后再去加载这个地址中数据的指令lw,叫作间接寻址。
|
||||
|
||||
|
||||
因此寻址模式是从指令如何获取数据的角度,对指令的一种分类,目的是给编写指令的人更多选择。
|
||||
|
||||
了解了指令的类型后,我再强调几个细节问题:
|
||||
|
||||
|
||||
关于寻址模式和所有的指令,只要你不是嵌入式开发人员,就不需要记忆,理解即可。
|
||||
不同 CPU 的指令和寄存器名称都不一样,因此这些名称也不需要你记忆。
|
||||
有几个寄存器在所有 CPU 里名字都一样,比如 PC 指针、指令寄存器等。
|
||||
|
||||
|
||||
指令的执行速度
|
||||
|
||||
之前我们提到过 CPU 是用石英晶体产生的脉冲转化为时钟信号驱动的,每一次时钟信号高低电平的转换就是一个周期,我们称为时钟周期。CPU 的主频,说的就是时钟信号的频率。比如一个 1GHz 的 CPU,说的是时钟信号的频率是 1G。
|
||||
|
||||
到这里你可能会有疑问:是不是每个时钟周期都可以执行一条指令?其实,不是的,多数指令不能在一个时钟周期完成,通常需要 2 个、4 个、6 个时钟周期。
|
||||
|
||||
总结
|
||||
|
||||
接下来我们来做一个总结。这节课我们深入讨论了指令和指令的分类。接下来,我们来看一看在 02 课时中留下的问题:64 位和 32 位比较有哪些优势?
|
||||
|
||||
还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 其实,这个问题需要分类讨论。
|
||||
|
||||
|
||||
如果说的是 64 位宽 CPU,那么有 2 个优势。
|
||||
|
||||
|
||||
优势 1:64 位 CPU 可以执行更大数字的运算,这个优势在普通应用上不明显,但是对于数值计算较多的应用就非常明显。
|
||||
|
||||
优势 2:64 位 CPU 可以寻址更大的内存空间
|
||||
|
||||
|
||||
如果 32 位/64 位说的是程序,那么说的是指令是 64 位还是 32 位的。32 位指令在 64 位机器上执行,困难不大,可以兼容。 如果是 64 位指令,在 32 位机器上执行就困难了。因为 32 位指令在 64 位机器执行的时候,需要的是一套兼容机制;但是 64 位指令在 32 位机器上执行,32 位的寄存器都存不下指令的参数。
|
||||
操作系统也是一种程序,如果是 64 位操作系统,也就是操作系统中程序的指令都是 64 位指令,因此不能安装在 32 位机器上。
|
||||
|
||||
|
||||
|
||||
|
||||
|
449
专栏/重学操作系统-完/04构造复杂的程序:将一个递归函数转成非递归函数的通用方法.md
Normal file
449
专栏/重学操作系统-完/04构造复杂的程序:将一个递归函数转成非递归函数的通用方法.md
Normal file
@ -0,0 +1,449 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04 构造复杂的程序:将一个递归函数转成非递归函数的通用方法
|
||||
我看到过一道非常不错的面试题:不支持递归的程序语言如何实现递归程序?
|
||||
|
||||
之所以说这道题好,是因为:
|
||||
|
||||
|
||||
首先,它不是纯粹考概念和死记硬背,求职者在回答问题之前需要进行一定的思考;
|
||||
其次,这道题目可以继续深挖,比如可以让求职者具体写一个程序,就变成了一道编程题;
|
||||
最后,这道题目有实战意义,它背后考察的是求职者的编程功底。
|
||||
|
||||
|
||||
为了弄清楚这道题目,你需要对程序有一个更深层次的认识,不仅仅停留在指令的执行层面,而是要灵活使用指令,去实现更加复杂的功能。
|
||||
|
||||
for 循环如何被执行
|
||||
|
||||
首先,我们来看 for 循环是如何实现的。
|
||||
|
||||
下面是一个求 1 加到 100 的 Java 程序,请你思考如何将它转换为指令:
|
||||
|
||||
var i = 1, s = 0;
|
||||
|
||||
for(; i <= 100; i++) {
|
||||
|
||||
s+=i;
|
||||
|
||||
}
|
||||
|
||||
|
||||
指令是简单的,像积木一样,程序是复杂的,像房子一样。我们将简单的事情组合,然后去完成复杂的事情,这就是程序员每天在做的。在这个过程中,你会产生思考,比如如何排列组合,如何搭积木,才能更快更准地完成项目?所以这也是训练思维的一个过程。
|
||||
|
||||
经过思考,如果按照顺序执行上面的程序,则需要很多指令,因为 for 循环可以执行 1 次,也可以执行 100W 次,还可以执行无数次。因此,指令的设计者提供了一种 jump 类型的指令,让你可以在程序间跳跃,比如:
|
||||
|
||||
loop:
|
||||
|
||||
jump loop
|
||||
|
||||
|
||||
这就实现了一个无限循环,程序执行到 jumploop 的时候,就会跳回 loop 标签。
|
||||
|
||||
用这种方法,我们可以将 for 循环用底层的指令实现:
|
||||
|
||||
# var i = 1, s = 0
|
||||
|
||||
# 对应 Java 代码,我们首先将 1 和 0 存储到两个地址
|
||||
|
||||
# 这两个地址我们用 $i 和 $s 表示
|
||||
|
||||
store #1 -> $i // 将数字 1 存入i的地址
|
||||
|
||||
store #0 -> $s // 将数字 0 存入 s 的地址
|
||||
|
||||
# 接下来循环要开始了,我们在这里预留一个 loop 标签
|
||||
|
||||
# loop 是一个自定义标签,它代表指令的相对位置
|
||||
|
||||
# 后续我们可以用 jump 指令跳转回这个位置实现循环
|
||||
|
||||
loop: # 循环标签
|
||||
|
||||
# for ... i <= 100
|
||||
|
||||
# 接下来我们开始实现循环控制
|
||||
|
||||
# 我们先首先 i <= 100的比较
|
||||
|
||||
# 我们先将变量 i 的地址,也就是 $i 导入寄存器 R0
|
||||
|
||||
load $i -> R0
|
||||
|
||||
# 然后我们用 cmp 比较指令 R0 和数字 100
|
||||
|
||||
cmp R0 #100 // 比较 R0 和数字 100
|
||||
|
||||
# 注意指令不会有返回值,它会进行计算,然后改变机器的状态(也就是寄存器)
|
||||
|
||||
# 比较后,有几个特殊的寄存器会保存比较结果
|
||||
|
||||
# 然后我们用 ja(jump above), 如果比较结果 R0 比 100 大
|
||||
|
||||
# 那么我们就跳转到 end 标签,实现循环的跳出
|
||||
|
||||
ja end
|
||||
|
||||
nop
|
||||
|
||||
# 如果 R0<=100,那么ja end 没有生效,这时我们处理 s+=i
|
||||
|
||||
# 首先我们把变量 s 所在地址的数据导入寄存器 R1
|
||||
|
||||
load $s -> R1
|
||||
|
||||
# 然后我们把寄存器R0和R1加和,把结果存储寄存器 R2
|
||||
|
||||
add R0 R1 R2
|
||||
|
||||
# 这时,我们把寄存器 R2 的值存入变量 s 所在的地址
|
||||
|
||||
store R2 -> $s
|
||||
|
||||
# 刚才我们完成了一次循环
|
||||
|
||||
# 我们还需要维护变量 i 的自增
|
||||
|
||||
# 现在 i 的值在 R0 中,我们首先将整数 1 叠加到 R0 上
|
||||
|
||||
add R0 #1 R0
|
||||
|
||||
# 再把 R0 的值存入i所在的内存地址
|
||||
|
||||
store R0 -> $i
|
||||
|
||||
# 这时我们的循环体已经全部执行完成,我们需要调转回上面 loop 标签所在的位置
|
||||
|
||||
# 继续循环
|
||||
|
||||
jump loop
|
||||
|
||||
nop
|
||||
|
||||
end:
|
||||
|
||||
|
||||
通过上面的方法,我们成功将 for 循环的程序转换成了指令,然后再将它们编码成二进制,就可以存储到内存中了。
|
||||
|
||||
讲到这里,我要强调几个事情:
|
||||
|
||||
|
||||
jump 指令直接操作 PC 指针,但是很多 CPU 会抢先执行下一条指令,因此通常我们在 jump 后面要跟随一条 nop 指令,让 CPU 空转一个周期,避免 jump 下面的指令被执行。是不是到了微观世界,和你所认识的程序还不太一样?
|
||||
上面我写指令的时候用到了 add/store 这些指令,它们叫作助记符,是帮助你记忆的。整体这段程序,我们就称作汇编程序。
|
||||
因为不同的机器助记符也不一样,所以你不用太关注我用的是什么汇编语言,也不用去记忆这些指令。当你拿到指定芯片的时候,直接去查阅芯片的说明书就可以了。
|
||||
虽然不同 CPU 的指令不一样,但也是有行业标准的。现在使用比较多的是 RISC(精简指令集)和 CISC(复杂指令集)。比如目前Inte 和 AMD 家族主要使用 CISC 指令集,ARM 和 MIPS 等主要使用RISC 指令集。
|
||||
|
||||
|
||||
条件控制程序
|
||||
|
||||
条件控制程序有两种典型代表,一种是 if-else ,另一种是 switch-case 。 总体来说, if-else 翻译成指令,是比较简单的,你需要用跳转指令和比较指令处理它的跳转逻辑。
|
||||
|
||||
当然,它们的使用场景不同,这块我不展开了。在这里我主要想跟你说说,它们的内部实现是不一样的。if-else 是一个自上向下的执行逻辑, switch-case是一种精确匹配算法。比如你有 1000 个 case,如果用 if-else 你需要一个个比较,最坏情况下需要比较 999 次;而如果用 switch-case ,就不需要一个个比较,通过算法就可以直接定位到对应的case 。
|
||||
|
||||
举个具体的例子,比如一个根据数字返回星期的程序。如果用if-else,那么你需要这样做:
|
||||
|
||||
if(week == 1) {
|
||||
|
||||
return "周一";
|
||||
|
||||
} else if(week == 2) {
|
||||
|
||||
return "周二";
|
||||
|
||||
}
|
||||
|
||||
……
|
||||
|
||||
|
||||
如果用 switch-case 的逻辑,你可能会这样计算:
|
||||
|
||||
跳转位置=当前PC + 4*(week * 2 - 1)
|
||||
|
||||
|
||||
你不用太关心上面的数学关系,我只是举一个例子告诉你, switch-case 实现更多是依赖数学关系,直接算出 case 所在指令的位置,而不是一行行执行和比较。
|
||||
|
||||
函数
|
||||
|
||||
了解了循环和条件判断,我们再来看看函数是如何被执行的。函数的执行过程必须深入到底层,也会涉及一种叫作栈的数据结构。
|
||||
|
||||
下面是一段 C 程序,传入两个参数,然后返回两个参数的和:
|
||||
|
||||
int add(int a, int b){
|
||||
|
||||
return a + b;
|
||||
|
||||
}
|
||||
|
||||
|
||||
这里我先不说具体的解决方案,希望你可以先自己思考。其实到这里,你已经学了不少知识了。下面我们一起分析一下,一种思考的方向是:
|
||||
|
||||
|
||||
通过观察,我们发现函数的参数 a,b 本质是内存中的数据,因此需要给它们分配内存地址。
|
||||
函数返回值也是内存中的数据,也就是返回值也需要分配内存地址。
|
||||
调用函数其实就是跳转到函数体对应的指令所在的位置,因此函数名可以用一个标签,调用时,就用 jump 指令跟这个标签。
|
||||
|
||||
|
||||
比如上面函数进行了a+b的运算,我们可以这样构造指令:
|
||||
|
||||
# 首先我们定义一个叫作add的标签
|
||||
|
||||
add:
|
||||
|
||||
# 然后我们将a和b所在地址中的数据都导入寄存器
|
||||
|
||||
load $a -> R0
|
||||
|
||||
load $b -> R1
|
||||
|
||||
# 然后我们将寄存器求和,并将结果回写到返回地址
|
||||
|
||||
add R0 R1 R2
|
||||
|
||||
store R2 -> $r
|
||||
|
||||
|
||||
当我们需要调用这个函数的时候,我们就构造下面这样的指令:
|
||||
|
||||
jump add
|
||||
|
||||
|
||||
细心的同学可能已经发现,这里有 2 个问题还没有解决:
|
||||
|
||||
|
||||
参数如何传递给函数?
|
||||
返回值如何传递给调用者?
|
||||
|
||||
|
||||
为了解决这 2 个问题,我们就需要用到前面提到的一个叫作栈的数据结构。栈的英文是 Stack,意思是码放整齐的一堆东西。首先在调用方,我们将参数传递给栈;然后在函数执行过程中,我们从栈中取出参数。
|
||||
|
||||
|
||||
|
||||
函数执行过程中,先将执行结果写入栈中,然后在返回前把之前压入的参数出栈,调用方再从栈中取出执行结果。
|
||||
|
||||
|
||||
|
||||
将参数传递给 Stack 的过程,叫作压栈。取出结果的过程,叫作出栈。栈就好像你书桌上的一摞书,压栈就是把参数放到书上面,出栈就是把顶部的书拿下来。
|
||||
|
||||
因为栈中的每个数据大小都一样,所以在函数执行的过程中,我们可以通过参数的个数和参数的序号去计算参数在栈中的位置。
|
||||
|
||||
接下来我们来看看函数执行的整体过程:假设要计算 11 和 15 的和,我们首先在内存中开辟一块单独的空间,也就是栈。
|
||||
|
||||
|
||||
|
||||
就如前面所讲,栈的使用方法是不断往上堆数据,所以需要一个栈指针(Stack Pointer, SP)指向栈顶(也就是下一个可以写入的位置)。每次将数据写入栈时,就把数据写到栈指针指向的位置,然后将 SP 的值增加。
|
||||
|
||||
为了提高效率,我们通常会用一个特殊的寄存器来存储栈指针,这个寄存器就叫作 Stack Pointer,在大多数芯片中都有这个特殊的寄存器。一开始,SP 指向 0x100 位置,而 0x100 位置还没有数据。
|
||||
|
||||
|
||||
压栈参数11
|
||||
|
||||
|
||||
接下来我们开始传参,我们先将 11 压栈,之所以称作压栈( Push),就好像我们把数据 11 堆在内存中一样。模拟压栈的过程是下面两条指令:
|
||||
|
||||
store #11 -> $SP // 将11存入SP指向的地址0x100
|
||||
|
||||
add SP, 4, SP // 栈指针增加4(32位机器)
|
||||
|
||||
|
||||
第一条 store 指令将 SP 寄存器指向的内存地址设置为常数 11。
|
||||
|
||||
第二条指令将栈指针自增 4。
|
||||
|
||||
这里用美元符号代表将 11 存入的是 SP 寄存器指向的内存地址,这是一次间接寻址。存入后,栈指针不是自增 1 而是自增了 4,因为我在这里给你讲解时,用的是一个 32 位宽的 CPU 。如果是 64 位宽的 CPU,那么栈指针就需要自增 8。
|
||||
|
||||
压栈完成后,内存变成下图中所示的样子。11 被写入内存,并且栈指针指向了 0x104 位置。
|
||||
|
||||
|
||||
|
||||
|
||||
压栈参数15
|
||||
|
||||
|
||||
然后我们用同样的方法将参数 15 压栈。
|
||||
|
||||
|
||||
|
||||
压栈后,11 和 15 都被放入了对应的内存位置,并且栈指针指向了 0x108。
|
||||
|
||||
|
||||
将返回值压栈
|
||||
|
||||
|
||||
接下来,我们将返回值压栈。到这里你可能会问,返回值还没有计算呢,怎么就压栈了?其实这相当于一个占位,后面我们会改写这个地址。
|
||||
|
||||
|
||||
|
||||
|
||||
调用函数
|
||||
|
||||
|
||||
当我们完成了上面的压栈后,就开始调用函数,一种简单的做法是用 jump 指令直接跳转到函数的标签,比如:
|
||||
|
||||
jump add
|
||||
|
||||
|
||||
这个时候,要加和在栈中的数据 11 和 15,我们可以利用 SP 指针寻找数据。11 距离当前 SP 指针差 3 个位置,15 距离 SP 指针差 2 个位置。这种寻址方式是一种复合的寻址方式,是间接 + 偏移量寻址。
|
||||
|
||||
我们可以用下面的代码完成将 11 和 15 导入寄存器的过程:
|
||||
|
||||
load $(SP - 12) -> R0
|
||||
|
||||
load $(SP - 8) -> R1
|
||||
|
||||
|
||||
然后进行加和,将结果存入 R2。
|
||||
|
||||
load R0 R1 R2
|
||||
|
||||
|
||||
最后我们可以再次利用数学关系将结果写入返回值所在的位置。
|
||||
|
||||
store R2 -> $(SP-4)
|
||||
|
||||
|
||||
上面我们用到了一种间接寻址的方式来进行加和运算,也就是利用 SP 中的地址做加减法操作内存。
|
||||
|
||||
经过函数调用的结果如下图所示,运算结果 26 已经被写入了返回值的位置:
|
||||
|
||||
|
||||
|
||||
|
||||
发现-解决问题
|
||||
|
||||
|
||||
一个好的解决方案,也会面临问题。现在我们就遇到了麻烦:
|
||||
|
||||
|
||||
函数计算完成,这时应该跳转回去。可是我们没有记录函数调用前 PC 指针的位置,因此这里需要改进,我们需要存储函数调用前的 PC 指针方便调用后恢复。
|
||||
栈不可以被无限使用,11和 15 作为参数,计算出了结果 26,那么它们就可以清空了。如果用调整栈指针的方式去清空,我们就会先清空 26。此时就会出现顺序问题,因此我们需要调整压栈的顺序。
|
||||
|
||||
|
||||
具体顺序你可以看下图。首先,我们将函数参数和返回值换位,这样在清空数据的时候,就会先清空参数,再清空返回值。
|
||||
|
||||
|
||||
|
||||
然后我们在调用函数前,还需要将返回地址压栈。这样在函数计算完成前,就能跳转回对应的返回地址。翻译成指令,就是下面这样:
|
||||
|
||||
## 压栈返回值
|
||||
|
||||
add SP, 4 -> SP
|
||||
|
||||
# 计算返回地址
|
||||
|
||||
# 我们需要跳转到清理堆栈那行,也就是16行
|
||||
|
||||
MOV PC+4*(参数个数*2+1) -> SP
|
||||
|
||||
# 压栈参数的程序
|
||||
|
||||
……
|
||||
|
||||
# 执行函数,计算返回值
|
||||
|
||||
call function
|
||||
|
||||
# 清理堆栈
|
||||
|
||||
add SP, -(参数个数+1)*4, SP
|
||||
|
||||
|
||||
递归函数如何被执行
|
||||
|
||||
我们刚刚使用了栈解决了函数的调用问题。但是这个方案究竟合不合理,还需要用更复杂的情况来验证。
|
||||
|
||||
如下所示,我们给出一个递归函数,请你判断是否可以用上面的方法执行:
|
||||
|
||||
int sum(int n){
|
||||
|
||||
if(n == 1) {return 1;}
|
||||
|
||||
return n + sum(n-1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
递归的时候,我们每次执行函数都形成一个如下所示的栈结构:
|
||||
|
||||
|
||||
|
||||
比如执行 sum(100),我们就会形成一个复杂的栈,第一次调用 n = 100,第二次递归调用 n = 99:
|
||||
|
||||
|
||||
|
||||
它们堆在了一起,就形成了一个很大的栈,简化一下就是这样的一个模型,如下所示:
|
||||
|
||||
|
||||
|
||||
到这里,递归消耗了更多空间,但是也保证了中间计算的独立性。当递归执行到 100 次的时候,就会执行下面的语句:
|
||||
|
||||
if(n == 1) {return 1;}
|
||||
|
||||
|
||||
于是触发第 99 次递归执行:
|
||||
|
||||
return 2 + sum(1) // sum(1) = 1
|
||||
|
||||
|
||||
上面程序等价于return 3,接着再触发第 98 次递归的执行,然后是第 97 次,最终触发到第一次函数调用返回结果。
|
||||
|
||||
由此可见,栈这种结构同样适合递归的计算。事实上,计算机编程语言就是用这种结构来实现递归函数。
|
||||
|
||||
类型(class)如何实现
|
||||
|
||||
按照我们之前已经学习到的知识:
|
||||
|
||||
|
||||
变量是一个内存地址,所以只需要分配内存就好了;
|
||||
循环控制可以用跳转加判断实现;
|
||||
条件控制也可以用跳转加判断实现,只不过如果是 switch-case 还需要一定的数学计算;
|
||||
函数调用需要压栈参数、返回值和返回地址。
|
||||
|
||||
|
||||
最后,我们来说说类型是如何实现的,也就是很多语言都支持的 class 如何被翻译成指令。其实 class 实现非常简单,首先一个 class 会分成两个部分,一部分是数据(也称作属性),另一部分是函数(也称作方法)。
|
||||
|
||||
|
||||
|
||||
class 有一个特殊的方法叫作构造函数,它会为 class 分配内存。构造函数执行的时候,开始扫描类型定义中所有的属性和方法。
|
||||
|
||||
|
||||
如果遇到属性,就为属性分配内存地址;
|
||||
如果遇到方法,方法本身需要存到正文段(也就是程序所在的内存区域),再将方法的值设置为方法指令所在的内存地址。
|
||||
|
||||
|
||||
当我们调用一个 class 方法的时候,本质上是执行了一个函数,因此和函数调用是一致的:
|
||||
|
||||
|
||||
首先把返回值和返回地址压栈;
|
||||
然后压栈参数;
|
||||
最后执行跳转。
|
||||
|
||||
|
||||
这里有一个小问题,有时候 class 的方法会用到this ,这其实并不复杂,你仔细想想, this指针不就是构造函数创建的一个指向 class 实例的地址吗?那么,有一种简单的实现,就是我们可以把 this 作为函数的第一个参数压栈。这样,类型的函数就可以访问类型的成员了,而类型也就可以翻译成指令了。
|
||||
|
||||
总结
|
||||
|
||||
下面我们做一个简单的总结:
|
||||
|
||||
|
||||
我们写的程序需要翻译成指令才能被执行,在 03 课时中我们提到过,这个翻译工具叫作编译器。
|
||||
平时你编程做的事情,用机器指令也能做,所以从计算能力上来说它们是等价的,最终这种计算能力又和图灵机是等价的。如果一个语言的能力和图灵机等价,我们就说这个语言是图灵完备的语言。现在市面上的绝大多数语言都是图灵完备的语言,但也有一些不是,比如 HTML、正则表达式和 SQL 等。
|
||||
我们通过汇编语言构造高级程序;通过高级程序构造自己的业务逻辑,这些都是工程能力的一种体现。
|
||||
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:一个程序语言如果不支持递归函数的话,该如何实现递归算法?
|
||||
|
||||
老规矩,请你先在脑海里思考问题的答案,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 思路如下:
|
||||
|
||||
|
||||
我们需要用到一个栈(其实用数组就可以);
|
||||
我们还需要一个栈指针,支持寄存器的编程语言能够直接用寄存器,而不支持直接用寄存器的编程语言,比如 Java,我们可以用一个变量;
|
||||
然后我们可以实现压栈、出栈的操作,并按照上面学习的函数调用方法操作我们的栈。
|
||||
|
||||
|
||||
|
||||
|
||||
|
182
专栏/重学操作系统-完/05(1)加餐练习题详解(一).md
Normal file
182
专栏/重学操作系统-完/05(1)加餐练习题详解(一).md
Normal file
@ -0,0 +1,182 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 (1) 加餐 练习题详解(一)
|
||||
今天我会带你把《模块一:计算机组成原理》中涉及的课后练习题,逐一讲解,并给出每个课时练习题的解题思路和答案。
|
||||
|
||||
练习题详解
|
||||
|
||||
01 | 计算机是什么:“如何把程序写好”这个问题是可计算的吗?
|
||||
|
||||
【问题】 可不可以构造一段程序证明停机问题无解?如果可以,请用自己熟悉的语言写出这段程序。
|
||||
|
||||
【解析】拿到这道题,我们可以先从问题的抽象入手。
|
||||
|
||||
|
||||
判断一段程序是否会停机的方法可以抽象成一个函数。
|
||||
一段程序,也可以抽象成一个函数。
|
||||
|
||||
|
||||
因此,问题可以转换为:存不存在一个通用函数判断另一个函数是否会停止?
|
||||
|
||||
接下来,再来构造冲突。
|
||||
|
||||
假设存在一个函数 willStop,它只有一个参数 func,willStop 可以判断任意函数 func 是否会停止:
|
||||
|
||||
|
||||
如果会停止,返回 true;
|
||||
如果不会停止返回 false。
|
||||
|
||||
|
||||
willStop 具体如何实现我们无法给出,这里只是做一个假设。
|
||||
|
||||
func willStop(func){
|
||||
|
||||
//...
|
||||
|
||||
}
|
||||
|
||||
|
||||
下面我们构造一组冲突,构造一个叫作wrappedWillStop函数,它调用willStop构造冲突。
|
||||
|
||||
function wrappedWillStop(){
|
||||
|
||||
if( willStop(wrappedWillStop) ) {
|
||||
|
||||
while(true){}
|
||||
|
||||
} else {
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
wrappedWillStop()
|
||||
|
||||
|
||||
wrapped版本构造冲突方法如下:调用willStop并把自己传进去。如果willStop认为wrapped会停止,那么就执行一个死循环。 如果willStop认为wrapped不会停止,就直接返回。
|
||||
|
||||
通过上述的方法,我们就知道willStop这样的函数肯定是无法被实现的;也就是停机问题无解。
|
||||
|
||||
03 | 程序的执行:相比 32 位 64 位的优势是什么?
|
||||
|
||||
【问题】 CPU 中有没有求对数的指令?如果没有那么程序如何去计算?
|
||||
|
||||
【解析】 CPU 中求一个数字的 2 倍,可以通过左移指令。比如 10 代表数字 2,左移 1 位变成 100 就代表数字 4。CPU 提供了乘法指令,所以如果求一个数字的幂,比如 33,可以拿 3*3 再乘以 3,需要计算 2 次。
|
||||
|
||||
但是如果求 3100 次方,就不会去计算 100 次。比如你可以先计算出 325,然后再求 (350)2,就是 3100。所以这样就节省了 1 倍的运算。
|
||||
|
||||
我举例主要是想告诉大家,CPU 没有提供很复杂的指令,但是这里有很多算法可以降低我们的时间开销。
|
||||
|
||||
然后我们来说说求对数,求对数也是没有指令的。因为对数是指数的逆运算,当然我们可以利用乘法运算一点点尝试。比如计算 log_210,我们可以先尝试 32,再尝试 3.12 等等,一直找到以 2 为底 10 的对数。这其实是个近似算法。
|
||||
|
||||
另外,在这个问题上聪明的数学家提出了很多近似算法,提升了计算效率。具体这里比较超纲,面试通常只考到有没有求对数的指令,感兴趣的同学可以学习泰勒级数、牛顿迭代法等。
|
||||
|
||||
比如下面这个泰勒级数可以用来求以e为底的对数,可以进行相似运算。
|
||||
|
||||
|
||||
|
||||
【补充内容】1 位的 CPU 能操作多大的内存空间?
|
||||
|
||||
在 03 课时程序的执行中,有个问题我讲的不是很明白,在这里我们再讨论一下。
|
||||
|
||||
之前提到过 32 位机器只能操作小于 32 位的地址总线,这里其实讲的不太清晰,历史上出现过 32 位操作 40 位地址总线的情况。
|
||||
|
||||
接下来再和你探讨一个极端情况,1 位的 CPU 能操作多大的内存空间。
|
||||
|
||||
答案是:无限大。
|
||||
|
||||
比如说,地址总线 40 位,说明 CPU 上有 40 个引脚接了地址总线。CPU 只有 1 位,因此操作这 40 个引脚可以分成 40 步。每次设置 1 根引脚的电平是 0 还是 1。所以本身 CPU 多少位和能操作多少位地址总线,没有本质联系。但是如果需要分步操作,效率会低,需要多次操作,不如一次完成来得划算。 因此我们今天的设计通常不拿 32 位 CPU 操作 40 位地址总线,而是用 64 位 CPU 操作。
|
||||
|
||||
04 | 构造复杂的程序 : 将一个递归函数转成非递归函数的通用方法?
|
||||
|
||||
【问题】 假设你使用的程序语言不支持递归程序,如果要求用栈来模拟下面这个斐波那契求第 n 项的程序,应该如何转换成等价的基于栈的非递归实现?
|
||||
|
||||
int fib(int n) {
|
||||
|
||||
if(n == 1 || n == 2) { return n; }
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
|
||||
【解析】其实这道题目等同于递归的函数如何非递归表达?改写斐波那契数列第 N 项目。
|
||||
|
||||
下面是我的一个伪代码,需要实现一个 Stack。
|
||||
|
||||
fib(n) {
|
||||
|
||||
stack = new Stack();
|
||||
|
||||
// 构造Stack
|
||||
|
||||
// stack中每一项是一个Record
|
||||
|
||||
// Record第一项是数据(参数或者返回值)
|
||||
|
||||
// Record第二项是递归方向(down=1代表向下,up=2代表向上)
|
||||
|
||||
stack.push((n, down));
|
||||
|
||||
// stack中只有一项的时候递归停止
|
||||
|
||||
while(stack.size() > 1) {
|
||||
|
||||
(n, phase) = stack.pop();
|
||||
|
||||
if(phase == down) {
|
||||
|
||||
if(n == 1 || n == 2) {
|
||||
|
||||
stack.push((1, -))
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
stack.push((n-1, down))
|
||||
|
||||
stack.push((n-1, up))
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
last1 = stack.pop()
|
||||
|
||||
last2 = stack.pop()
|
||||
|
||||
stack.push((last1[0] + last2[0], up))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return stack.pop()[0];
|
||||
|
||||
}
|
||||
|
||||
|
||||
05 | 存储器分级 :SSD、内存和 L1 Cache 相比速度差多少倍?
|
||||
|
||||
【问题】 假设有一个二维数组,总共有 1M 个条目,如果我们要遍历这个二维数组,应该逐行遍历还是逐列遍历?
|
||||
|
||||
【解析】 二维数组本质还是 1 维数组。只不过进行了脚标运算。比如说一个 N 行 M 列的数组,第 y 行第 x 列的坐标是: x + y*M。因此当行坐标增加时,内存空间是跳跃的。列坐标增加时,内存空间是连续的。
|
||||
|
||||
|
||||
|
||||
当 CPU 遍历二维数组的时候,会先从 CPU 缓存中取数据。
|
||||
|
||||
关键因素在于现在的 CPU 设计不是每次读取一个内存地址,而是读取每次读取相邻的多个内存地址(内存速度 200~300 CPU 周期,预读提升效率)。所以这相当于机器和人的约定,如果程序员不按照这个约定,就无法利用预读的优势。
|
||||
|
||||
另一方面当读取内存地址跳跃较大的时候,会触发内存的页面置换,这个知识在“模块五:内存管理”中学习。
|
||||
|
||||
总结
|
||||
|
||||
以上这些练习题你做得怎么样呢?我看到很多同学在留言区写下了练习题答案、思考过程以及课后总结,当然还有很多同学提出了问题。有问题是好事,说明你在认真思考,这也是构建知识体系的一部分。经过长期的积累,相信你会得到意想不到的收获。
|
||||
|
||||
|
||||
|
||||
|
181
专栏/重学操作系统-完/05存储器分级:L1Cache比内存和SSD快多少倍?.md
Normal file
181
专栏/重学操作系统-完/05存储器分级:L1Cache比内存和SSD快多少倍?.md
Normal file
@ -0,0 +1,181 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 存储器分级:L1 Cache 比内存和 SSD 快多少倍?
|
||||
近两年我在面试求职者的时候,喜欢问这样一道面试题:SSD、内存和 L1 Cache 相比速度差多少倍?
|
||||
|
||||
其实比起复杂的技术问题,我更喜欢在面试中提问这种像生活常识一样的简单问题。因为我觉得,复杂的问题是由简单的问题组成的,如果你把简单的问题学扎实了,那么复杂问题也是可以自己推导的。
|
||||
|
||||
如果你不知道 L1 Cache,可能会错误地判断内存执行速度。我们写程序,会用寄存器、内存以及硬盘,所以按照墨菲定律,如果这里有一个认知是错误的,那么最终的结果就会产生问题。
|
||||
|
||||
下面,回到我们今天的问题,这个问题关联的知识点是存储器分级策略。接下来,请你带着问题开始学习今天的内容。
|
||||
|
||||
为什么会有存储器分级策略?
|
||||
|
||||
要想弄清楚存储器分级策略。
|
||||
|
||||
首先,你要弄清楚,“我们希望存储器是什么样子的”,也就是“我们的需求是什么”?
|
||||
|
||||
然后,你要弄清楚,我们的需求有哪些“实现约束”。
|
||||
|
||||
从需求上讲,我们希望存储器速度快、体积小、空间大、能耗低、散热好、断电数据不丢失。但在现实中,我们往往无法把所有需求都实现。
|
||||
|
||||
下面我们举几个例子,带你深入体会一下,比如:
|
||||
|
||||
|
||||
如果一个存储器的体积小,那它存储空间就会受到制约。
|
||||
如果一个存储器电子元件密度很大,那散热就会有问题。因为电子元件都会产生热能,所以电子元件非常集中的 CPU,就需要单独的风扇或者水冷帮助电子元件降温。
|
||||
如果一个存储器离 CPU 较远,那么在传输过程中必然会有延迟,因此传输速度也会下降。
|
||||
|
||||
|
||||
这里你可能会有疑问,因为在大多数人的认知里,光速是很快的,而信号又是以光速传输的。既然光速这么快,那信号的延迟应该很小才对。但事实并不是这样,比如时钟信号是 1GHz 的 CPU,1G 代表 10 个亿,因此时钟信号的一个周期是 1⁄10 亿秒。而光的速度是 3×10 的 8 次方米每秒,就是 3 亿米每秒。所以在一个周期内,光只能前进 30 厘米。
|
||||
|
||||
你看!虽然在宏观世界里光速非常快,但是到计算机世界里,光速并没有像我们认知中的那么快。所以即使元件离 CPU 的距离稍微远了一点,运行速度也会下降得非常明显。
|
||||
|
||||
你可能还会问,那干吗不把内存放到 CPU 里?
|
||||
|
||||
如果你这么做的话,除了整个电路散热和体积会出现问题,服务器也没有办法做定制内存了。也就是说 CPU 在出厂时就决定了它的内存大小,如果你想换更大的内存,就要换 CPU,而组装定制化是你非常重要的诉求,这肯定是不能接受的。
|
||||
|
||||
此外,在相同价格下,一个存储器的速度越快,那么它的能耗通常越高。能耗越高,发热量越大。
|
||||
|
||||
因此,我们上面提到的需求是不可能被全部满足的,除非将来哪天存储技术有颠覆性的突破。
|
||||
|
||||
存储器分级策略
|
||||
|
||||
既然我们不能用一块存储器来解决所有的需求,那就必须把需求分级。
|
||||
|
||||
一种可行的方案,就是根据数据的使用频率使用不同的存储器:高频使用的数据,读写越快越好,因此用最贵的材料,放到离 CPU 最近的位置;使用频率越低的数据,我们放到离 CPU 越远的位置,用越便宜的材料。
|
||||
|
||||
|
||||
|
||||
具体来说,通常我们把存储器分成这么几个级别:
|
||||
|
||||
|
||||
寄存器;
|
||||
L1-Cache;
|
||||
L2-Cache;
|
||||
L3-Cahce;
|
||||
内存;
|
||||
硬盘/SSD。
|
||||
|
||||
|
||||
寄存器(Register)
|
||||
|
||||
寄存器紧挨着 CPU 的控制单元和逻辑计算单元,它所使用的材料速度也是最快的。就像我们前面讲到的,存储器的速度越快、能耗越高、产热越大,而且花费也是最贵的,因此数量不能很多。
|
||||
|
||||
寄存器的数量通常在几十到几百之间,每个寄存器可以用来存储一定字节(byte)的数据。比如:
|
||||
|
||||
|
||||
32 位 CPU 中大多数寄存器可以存储 4 个字节;
|
||||
64 位 CPU 中大多数寄存器可以存储 8 个字节。
|
||||
|
||||
|
||||
寄存机的访问速度非常快,一般要求在半个 CPU 时钟周期内完成读写。比如一条要在 4 个周期内完成的指令,除了读写寄存器,还需要解码指令、控制指令执行和计算。如果寄存器的速度太慢,那 4 个周期就可能无法完成这条指令了。
|
||||
|
||||
L1-Cache
|
||||
|
||||
L1- 缓存在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低。通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。
|
||||
|
||||
L2-Cache
|
||||
|
||||
L2- 缓存也在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远。它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。
|
||||
|
||||
L3-Cache
|
||||
|
||||
L3- 缓存同样在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远。大小通常比 L2-Cache 更大,读写速度在 20~60 个 CPU 周期。L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。
|
||||
|
||||
内存
|
||||
|
||||
内存的主要材料是半导体硅,是插在主板上工作的。因为它的位置距离 CPU 有一段距离,所以需要用总线和 CPU 连接。因为内存有了独立的空间,所以体积更大,造价也比上面提到的存储器低得多。现在有的个人电脑上的内存是 16G,但有些服务器的内存可以到几个 T。内存速度大概在 200~300 个 CPU 周期之间。
|
||||
|
||||
SSD 和硬盘
|
||||
|
||||
SSD 也叫固态硬盘,结构和内存类似,但是它的优点在于断电后数据还在。内存、寄存器、缓存断电后数据就消失了。内存的读写速度比 SSD 大概快 10~1000 倍。以前还有一种物理读写的磁盘,我们也叫作硬盘,它的速度比内存慢 100W 倍左右。因为它的速度太慢,现在已经逐渐被 SSD 替代。
|
||||
|
||||
|
||||
|
||||
当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。
|
||||
|
||||
缓存条目结构
|
||||
|
||||
上面我们介绍了存储器分级结构大概有哪些存储以及它们的特点,接下来还有一些缓存算法和数据结构的设计困难要和你讨论。比如 CPU 想访问一个内存地址,那么如何检查这个数据是否在 L1- 缓存中?换句话说,缓存中的数据结构和算法是怎样的?
|
||||
|
||||
无论是缓存,还是内存,它们都是一个线性存储器,也就是数据一个挨着一个的存储。如果我们把内存想象成一个只有 1 列的表格,那么缓存就是一个多列的表格,这个表格中的每一行叫作一个缓存条目。
|
||||
|
||||
方案 1
|
||||
|
||||
缓存本质上是一个 Key-Value 的存储,它的 Key 是内存地址,值是缓存时刻内存地址中的值。我们先思考一种简单的方案,一个缓存条目设计 2 列:
|
||||
|
||||
|
||||
内存的地址;
|
||||
缓存的值。
|
||||
|
||||
|
||||
CPU 读取到一个内存地址,我们就增加一个条目。当我们要查询一个内存地址的数据在不在 L1- 缓存中的时候,可以遍历每个条目,看条目中的内存地址是否和查询的内存地址相同。如果相同,我们就取出条目中缓存的值。
|
||||
|
||||
这个方法需要遍历缓存中的每个条目,因此计算速度会非常慢,在最坏情况下,算法需要检查所有的条目,所以这不是一个可行的方案。
|
||||
|
||||
方案 2
|
||||
|
||||
其实很多优秀的方案,往往是从最笨的方案改造而来的。现在我们已经拥有了一个方案,但是这个方案无法快速确定一个内存地址缓存在哪一行。因此我们想要找到一个更好的方法,让我们看到一个内存地址,就能够快速知道它在哪一行。
|
||||
|
||||
这里,我们可以用一个数学的方法。比如有 1000 个内存地址,但只有 10 个缓存条目。内存地址的编号是 0、1、2、3,…,999,缓存条目的编号是 0~9。我们思考一个内存编号,比如 701,然后用数学方法把它映射到一个缓存条目,比如 701 整除 10,得到缓存条目 1。
|
||||
|
||||
用这种方法,我们每次拿到一个内存地址,都可以快速确定它的缓存条目;然后再比较缓存条目中的第一列内存地址和查询的内存地址是否相同,就可以确定内存地址有没有被缓存。
|
||||
|
||||
延伸一下,这里用到了一种类似哈希表的方法:地址 % 10,其实就构成了一个简单的哈希函数。
|
||||
|
||||
指令的预读
|
||||
|
||||
接下来我们讨论下指令预读的问题。
|
||||
|
||||
之前我们学过,CPU 顺序执行内存中的指令,CPU 执行指令的速度是非常快的,一般是 2~6 个 CPU 时钟周期;这节课,我们学习了存储器分级策略,发现内存的读写速度其实是非常慢的,大概有 200~300 个时钟周期。
|
||||
|
||||
不知道你发现没有?这也产生了一个非常麻烦的问题:CPU 其实是不能从内存中一条条读取指令再执行的,如果是这样做,那每执行一条指令就需要 200~300 个时钟周期了。
|
||||
|
||||
那么,这个问题如何处理呢?
|
||||
|
||||
这里我再多说一句,你在做业务开发 RPC 调用的时候,其实也会经常碰到这种情况,远程调用拖慢了整体执行效率,下面我们一起讨论这类问题的解决方案。
|
||||
|
||||
一个解决办法就是 CPU 把内存中的指令预读几十条或者上百条到读写速度较快的 L1- 缓存中,因为 L1- 缓存的读写速度只有 2~4 个时钟周期,是可以跟上 CPU 的执行速度的。
|
||||
|
||||
这里又产生了另一个问题:如果数据和指令都存储在 L1- 缓存中,如果数据缓存覆盖了指令缓存,就会产生非常严重的后果。因此,L1- 缓存通常会分成两个区域,一个是指令区,一个是数据区。
|
||||
|
||||
与此同时,又出现了一个问题,L1- 缓存分成了指令区和数据区,那么 L2/L3 需不需要这样分呢?其实,是不需要的。因为 L2 和 L3,不需要协助处理指令预读的问题。
|
||||
|
||||
缓存的命中率
|
||||
|
||||
接下来,还有一个重要的问题需要解决。就是 L1/L2/L3 加起来,缓存的命中率有多少?
|
||||
|
||||
所谓命中就是指在缓存中找到需要的数据。和命中相反的是穿透,也叫 miss,就是一次读取操作没有从缓存中找到对应的数据。
|
||||
|
||||
据统计,L1 缓存的命中率在 80% 左右,L1/L2/L3 加起来的命中率在 95% 左右。因此,CPU 缓存的设计还是相当合理的。只有 5% 的内存读取会穿透到内存,95% 都能读取到缓存。 这也是为什么程序语言逐渐取消了让程序员操作寄存器的语法,因为缓存保证了很高的命中率,多余的优化意义不大,而且很容易出错。
|
||||
|
||||
缓存置换问题
|
||||
|
||||
最后的一个问题,比如现在 L1- 缓存条目已经存满了,接下来 CPU 又读了内存,需要把一个新的条目存到 L1- 缓存中,既然有一个新的条目要进来,那就有一个旧的条目要出去。所以,这个时候我们就需要用一个算法去计算哪个条目应该被置换出去。这个问题叫作缓存置换问题。有关缓存置换问题,我会在 “21 | 进程的调度:进程调度都有哪些方法?”中和你讨论。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们讲到了存储器分级策略,讨论了 L1/L2/L3 缓存的工作原理。本课时学习的内容,是所有缓存知识的源头。所有缓存系统的设计,都是存储资源的分级。我们在设计缓存的时候,除了要关心整体架构外,还需要注意细节,比如:
|
||||
|
||||
|
||||
条目怎么设计?
|
||||
算法怎么设计?
|
||||
命中率怎么统计?
|
||||
缓存怎么置换等?
|
||||
|
||||
|
||||
现在我们来说一下课前提出的问题:SSD、内存和 L1 Cache 相比速度差多少倍?
|
||||
|
||||
还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 因为内存比 SSD 快 10~1000 倍,L1 Cache 比内存快 100 倍左右。因此 L1 Cache 比 SSD 快了 1000~100000 倍。所以你有没有发现 SSD 的潜力很大,好的 SSD 已经接近内存了,只不过造价还略高。
|
||||
|
||||
这个问题告诉我们,不同的存储器之间性能差距很大,构造存储器分级很有意义,分级的目的是要构造缓存体系。
|
||||
|
||||
|
||||
|
||||
|
274
专栏/重学操作系统-完/06目录结构和文件管理指令:rm-rf指令的作用是?.md
Normal file
274
专栏/重学操作系统-完/06目录结构和文件管理指令:rm-rf指令的作用是?.md
Normal file
@ -0,0 +1,274 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 目录结构和文件管理指令:rm -rf 指令的作用是?
|
||||
通过模块一的学习,你应该掌握了计算机组成原理的重点知识,到了模块二,我们开始学习 Linux 指令,它是操作系统的前端,学好这部分内容一方面可以帮助你应对工作场景,另一方面可以让你在学习操作系统底层知识前,对 Linux 有一个大概的了解。
|
||||
|
||||
接下来,我们依然通过一道常见的高频面试题,引出今天的主要内容。面试题如下:请你说说rm / -rf的作用?
|
||||
|
||||
相信 90% 的同学是知道这个指令的。这里先预警一下,你千万不要轻易在服务器上尝试。要想知道这条指令是做什么的,能够帮助我们解决哪些问题,那就请你认真学习今天的内容。在本课时的最后我会公布这道题目的分析过程和答案。
|
||||
|
||||
什么是 Shell
|
||||
|
||||
在我们学习 Linux 指令之前,先来说一下什么是 Shell?Shell 把我们输入的指令,传递给操作系统去执行,所以 Shell 是一个命令行的用户界面。
|
||||
|
||||
早期程序员没有图形界面用,就用 Shell。而且图形界面制作成本较高,不能实现所有功能,因此今天的程序员依然在用 Shell。
|
||||
|
||||
你平时还经常会看到一个词叫作bash(Bourne Again Shell),它是用 Shell 组成的程序。这里的 Bourne 是一个人名,Steve Bourne 是 bash 的发明者。
|
||||
|
||||
我们今天学习的所有指令,不是写死在操作系统中的,而是一个个程序。比如rm指令,你可以用which指令查看它所在的目录。如下图所示,你会发现rm指令在/usr/bin/rm目录中。
|
||||
|
||||
|
||||
|
||||
如上图所示,ramroll是我的英文名字,ubuntu 是我这台机器的名字。我输入了which rm,然后获得了/usr/bin/rm的结果,最终执行这条指令的是操作系统,连接我和操作系统的程序就是 Shell。
|
||||
|
||||
Linux 对文件目录操作的指令就工作在 Shell 上,接下来我们讲讲文件目录操作指令。
|
||||
|
||||
Linux 对文件目录的抽象
|
||||
|
||||
Linux 对文件进行了一个树状的抽象。/代表根目录,每一节目录也用/分开,所以在上图所展示的/usr/bin/rm中,第一级目录是/根目录,第二级目录是usr目录,第三级是bin目录。最后的rm是一个文件。
|
||||
|
||||
路径(path)
|
||||
|
||||
像/usr/bin/rm称为可执行文件rm的路径。路径就是一个文件在文件系统中的地址。如果文件系统是树形结构,那么通常一个文件只有一个地址(路径)。
|
||||
|
||||
目标文件的绝对路径(Absolute path),也叫作完全路径(full path),是从/开始,接下来每一层都是一级子目录,直到定位到目标文件为止。
|
||||
|
||||
如上图所示的例子中,/usr/bin/rm就是一个绝对路径。
|
||||
|
||||
工作目录
|
||||
|
||||
为了方便你工作,Shell 还抽象出了工作目录。当用户打开 Shell 的时候,Shell 就会给用户安排一个工作目录。因此也就产生了相对路径。
|
||||
|
||||
相对路径(Relative path)是以工作目录为基点的路径。比如:
|
||||
|
||||
|
||||
当用户在/usr目录下的时候,rm文件的相对路径就是bin/rm;
|
||||
如果用户在/usr/bin目录下的时候,rm文件的路径就是./rm或者rm,这里用.代表当前目录;
|
||||
如果用户在/usr/bin/somedir下,那么rm的相对路径就是../rm,这里用..代表上一级目录。
|
||||
|
||||
|
||||
我们使用cd(change directory)指令切换工作目录,既可以用绝对路径,也可以用相对路径。 这里我要强调几个注意事项:
|
||||
|
||||
|
||||
输入cd,不带任何参数会切换到用户的家目录,Linux 中通常是/home/{用户名}。以我自己为例,我的家目录是/home/ramroll;
|
||||
输入cd .什么都不会发生,因为.代表当前目录;
|
||||
输入cd..会回退一级目录,因为..代表上级目录。
|
||||
|
||||
|
||||
利用上面这 3 种能力,你就可以方便的构造相对路径了。
|
||||
|
||||
Linux提供了一个指令pwd(Print Working Directory)查看工作目录。下图是我输入pwd的结果。
|
||||
|
||||
|
||||
|
||||
你可以看到我正在/home/ramroll/Documents目录下工作。
|
||||
|
||||
几种常见的文件类型
|
||||
|
||||
另一方面,Linux 下的目录也是一种文件;但是文件也不只有目录和可执行文件两种。常见的文件类型有以下 7 种:
|
||||
|
||||
|
||||
普通文件(比如一个文本文件);
|
||||
目录文件(目录也是一个特殊的文件,它用来存储文件清单,比如/也是一个文件);
|
||||
可执行文件(上面的rm就是一个可执行文件);
|
||||
管道文件(我们会在 07 课时讨论管道文件);
|
||||
Socket 文件(我们会在模块七网络部分讨论 Socket 文件);
|
||||
软链接文件(相当于指向另一个文件所在路径的符号);
|
||||
硬链接文件(相当于指向另一个文件的指针,关于软硬链接我们将在模块六文件系统部分讨论)。
|
||||
|
||||
|
||||
你如果使用ls -F就可以看到当前目录下的文件和它的类型。比如下面这种图:
|
||||
|
||||
|
||||
* 结尾的是可执行文件;
|
||||
= 结尾的是 Socket 文件;
|
||||
@ 结尾的是软链接;
|
||||
| 结尾的管道文件;
|
||||
没有符号结尾的是普通文件;
|
||||
/ 结尾的是目录。
|
||||
|
||||
|
||||
|
||||
|
||||
设备文件
|
||||
|
||||
Socket 是网络插座,是客户端和服务器之间同步数据的接口。其实,Linux 不只把 Socket 抽象成了文件,设备基本也都被抽象成了文件。因为设备需要不断和操作系统交换数据。而交换方式只有两种——读和写。所以设备是可以抽象成文件的,因为文件也支持这两种操作。
|
||||
|
||||
Linux 把所有的设备都抽象成了文件,比如说打印机、USB、显卡等。这让整体的系统设计变得高度统一。
|
||||
|
||||
至此,我们了解了 Linux 对文件目录的抽象,接下来我们看看具体的增删改查指令。
|
||||
|
||||
文件的增删改查
|
||||
|
||||
增加
|
||||
|
||||
创建一个普通文件的方法有很多,最常见的有touch指令。比如下面我们创建了一个 a.txt 文件。
|
||||
|
||||
|
||||
|
||||
touch指令本来是用来更改文件的时间戳的,但是如果文件不存在touch也会帮助创建一个空文件。
|
||||
|
||||
如果你拿到一个指令不知道该怎么用,比如touch,你可以用man touch去获得帮助。man意思是 manual,就是说明书的意思,这里指的是系统的手册。如果你不知道man是什么,也可以使用man man。下图是使用man man的结果:
|
||||
|
||||
|
||||
|
||||
另外如果我们需要增加一个目录,就需要用到mkdir指令( make directory),比如我们创建一个hello目录,如下图所示:
|
||||
|
||||
|
||||
|
||||
查看
|
||||
|
||||
创建之后我们可以用ls指令看到这个文件,ls是 list 的缩写。下面是指令 ‘ls’ 的执行结果。
|
||||
|
||||
|
||||
|
||||
我们看到在当前的目录下有一个a.txt文件,还有一个hello目录。如果你知道当前的工作目录,就可以使用pwd指令。
|
||||
|
||||
如果想看到a.txt更完善的信息,还可以使用ls -l。-l是ls指令的可选参数。下图是ls -l的结果,你可以看到a.txt更详细的描述。
|
||||
|
||||
|
||||
|
||||
如上图所示,我们看到两个ramroll,它们是a.txt所属的用户和所属的用户分组,刚好重名了。Sep 13是日期。 中间有一个0是a.txt的文件大小,目前a.txt中还没有写入内容,因此大小是0。
|
||||
|
||||
另外虽然hello是空的目录,但是目录文件 Linux 上来就分配了4096字节的空间。这是因为目录内需要保存很多文件的描述信息。
|
||||
|
||||
删除
|
||||
|
||||
如果我们想要删除a.txt可以用rm a.txt;如我们要删除hello目录,可以用rm hello。rm是 remove 的缩写。
|
||||
|
||||
|
||||
|
||||
但是当我们输入rm hello的时候,会提示hello是一个目录,不可以删除。因此我们需要增加一个可选项,比如-r即 recursive(递归)。目录是一个递归结构,所以需要用递归删除。最后,你会发现rm hello -r删除了hello目录。
|
||||
|
||||
接下来我们尝试在 hello 目录下新增一个文件,比如相对路径是hello/world/os.txt。需要先创建 hello/world 目录。这种情况会用到mkdir的-p参数,这个参数控制mkdir当发现目标目录的父级目录不存在的时候会递归的创建。以下是我们的执行结果:
|
||||
|
||||
|
||||
|
||||
修改
|
||||
|
||||
如果需要修改一个文件,可以使用nano或者vi编辑器。类似的工具还有很多,但是nano和vi一般是linux自带的。
|
||||
|
||||
这里我不展开讲解了,你可以自己去尝试。在尝试的过程中如果遇到什么问题,可以写在留言区,我会逐一为你解答。
|
||||
|
||||
查阅文件内容
|
||||
|
||||
在了解了文件的增删改查操作后,下面我们来学习查阅文件内容。我们知道,Linux 下查阅文件内容,可以根据不同场景选择不同的指令。
|
||||
|
||||
当文件较小时,比如一个配置文件,想要快速浏览这个文件,可以用cat指令。下面 cat 指令帮助我们快速查看/etc/hosts文件。cat指令将文件连接到标准输出流并打印到屏幕上。
|
||||
|
||||
|
||||
|
||||
标准输出流(Standard Output)也是一种文件,进程可以将要输出的内容写入标准输出流文件,这样就可以在屏幕中打印。
|
||||
|
||||
如果用cat查看大文件,比如一个线上的日志文件,因为动辄有几个 G,控制台打印出所有的内容就要非常久,而且刷屏显示看不到东西。
|
||||
|
||||
而且如果在线上进行查看大文件的操作,会带来不必要的麻烦:
|
||||
|
||||
首先因为我们需要把文件拷贝到输入输出流,这需要花费很长时间,这个过程会占用机器资源;
|
||||
|
||||
其次,本身文件会读取到内存中,这时内存被大量占用,很危险,这可能导致其他应用内存不足。因此我们需要一些不用加载整个文件,就能查看文件内容的指令。
|
||||
|
||||
more
|
||||
|
||||
more可以帮助我们读取文件,但不需要读取整个文件到内存中。本身more的定位是一个阅读过滤器,比如你在more里除了可以向下翻页,还可以输入一段文本进行搜索。
|
||||
|
||||
|
||||
|
||||
如上图所示,我在more查看一个 nginx 日志后,先输入一个/,然后输入192.168看到的结果。more帮我找到了192.168所在的位置,然后又帮我定位到了这个位置。整个过程 more 指令只读取我们需要的部分到内存中。
|
||||
|
||||
less
|
||||
|
||||
less是一个和more功能差不多的工具,打开man能够看到less的介绍上写着自己是more的反义词(opposite of more)。这样你可以看出linux生态其实也是很自由的一个生态,在这里创造工具也可以按照自己的喜好写文档。less支持向上翻页,这个功能more是做不到的。所以现在less用得更多一些。
|
||||
|
||||
head/tail
|
||||
|
||||
head和tail是一组,它们用来读取一个文件的头部 N 行或者尾部 N 行。比如一个线上的大日志文件,当线上出了 bug,服务暂停的时候,我们就可以用tail -n 1000去查看最后的 1000 行日志文件,寻找导致服务异常的原因。
|
||||
|
||||
另一个比较重要的用法是,如果你想看一个实时的nginx日志,可以使用tail -f 文件名,这样你会看到用户的请求不断进来。查一下man,你会发现-f是 follow 的意思,就是文件追加的内容会跟随输出到标准输出流。
|
||||
|
||||
grep
|
||||
|
||||
有时候你需要查看一个指定ip的nginx日志,或者查看一段时间内的nginx日志。如果不想用less和more进入文件中去查看,就可以用grep命令。Linux 的文件命名风格都很短,所以也影响了很多人,比如之前我看到过一个大牛的程序,变量名从来不超过 5 个字母,而且都有意义。
|
||||
|
||||
grep 这个词,我们分成三段来看,是 g|re|p。
|
||||
|
||||
|
||||
g 就是 global,全局;
|
||||
re 就是 regular expression,正则表达式;
|
||||
p 就是 pattern,模式。
|
||||
|
||||
|
||||
所以这个指令的作用是通过正则表达式全局搜索一个文件找到匹配的模式。我觉得这种命名真的很牛,软件命名也是一个世纪难题,grep这个名字不但发音不错,而且很有含义,又避免了名字过长,方便记忆。
|
||||
|
||||
下面我们举两个例子看看 grep 的用法:
|
||||
|
||||
|
||||
例 1:查找 ip 地址
|
||||
|
||||
|
||||
我们可以通过grep命令定位某个ip地址的用户都做了什么事情,如下图所示:
|
||||
|
||||
|
||||
|
||||
|
||||
例 2:查找时间段的日志
|
||||
|
||||
|
||||
我们可以通过 grep 命令查找某个时间段内用户都做了什么事情。如下图所示,你可以看到在某个 5 分钟内所有用户的访问情况。
|
||||
|
||||
|
||||
|
||||
查找文件
|
||||
|
||||
用户经常还会有一种诉求,就是查找文件。
|
||||
|
||||
之前我们使用过一个which指令,这个指令可以查询一个指令文件所在的位置,比如which grep会,你会看到grep指令被安装的位置是/usr/bin。但是我们还需要一个更加通用的指令查找文件,也就是 find 指令。
|
||||
|
||||
find
|
||||
|
||||
find 指令帮助我们在文件系统中查找文件。 比如我们如果想要查找所有.txt 扩展名的文件,可以使用find / -iname "*.txt",-iname这个参数是用来匹配查找的,i 字母代表忽略大小写,这里也可以用-name替代。输入这条指令,你会看到不断查找文件,如下图所示:
|
||||
|
||||
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了很多指令,不知道你记住了多少?最后,我们再一起复习一下。
|
||||
|
||||
|
||||
pwd指令查看工作目录。
|
||||
cd指令切换工作目录。
|
||||
which指令查找一个执行文件所在的路径。
|
||||
ls显示文件信息。
|
||||
rm删除文件。
|
||||
touch修改一个文件的时间戳,如果文件不存在会触发创建文件。
|
||||
vi和nano可以用来编辑文件。
|
||||
cat查看完成的文件适合小型文件。
|
||||
moreless查看一个文件但是只读取用户看到的内容到内存,因此消耗资源较少,适合在服务器上看日志。
|
||||
headtail可以用来看文件的头和尾。
|
||||
grep指令搜索文件内容。
|
||||
find指令全局查找文件。
|
||||
|
||||
|
||||
在这里,我再强调一个指令,即man指令,它是所有指令的手册,所以你一定要多多运用,熟练掌握。另外,一个指令通常有非常多的参数,但都需要用man指令去仔细研究。
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:rm / -rf的作用是?
|
||||
|
||||
老规矩,请你先在脑海里先思考你的答案,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】
|
||||
|
||||
|
||||
/是文件系统根目录;
|
||||
rm是删除指令;
|
||||
-r是 recursive(递归);
|
||||
-f是 force(强制),遇到只读文件也不提示,直接删除。
|
||||
|
||||
|
||||
所以rm -rf /就是删除整个文件系统上的所有文件,而且不用给用户提示。
|
||||
|
||||
|
||||
|
||||
|
273
专栏/重学操作系统-完/07进程、重定向和管道指令:xargs指令的作用是?.md
Normal file
273
专栏/重学操作系统-完/07进程、重定向和管道指令:xargs指令的作用是?.md
Normal file
@ -0,0 +1,273 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 进程、重定向和管道指令:xargs 指令的作用是?
|
||||
在面试中,我们经常会遇到面试官询问 Linux 指令,06 课时中讲到的rm -rf /属于比较简单的题目,相当于小学难度。这节课给你带来一道初中难度的题目:xargs指令的作用是什么?
|
||||
|
||||
通常这个指令是和管道一起使用,因此就引出了这节课的主题:管道。为了理解管道,和学习管道相关的内容,还有一些概念需要你理解,比如:进程、标准流和重定向。好的,接下来请和我一起,把这块知识一网打尽!
|
||||
|
||||
进程
|
||||
|
||||
为了弄清楚这节课程的内容,也就是管道,我们先来讨论一下进程。
|
||||
|
||||
我们知道,应用的可执行文件是放在文件系统里,把可执行文件启动,就会在操作系统里(具体来说是内存中)形成一个应用的副本,这个副本就是进程。
|
||||
|
||||
插一个小知识,以后你再遇到面试题:什么是进程?
|
||||
|
||||
可以回答:进程是应用的执行副本;而不要回答进程是操作系统分配资源的最小单位。前者是定义,后者是作用。
|
||||
|
||||
ps
|
||||
|
||||
如果你要看当前的进程,可以用ps指令。p 代表 processes,也就是进程;s 代表 snapshot,也就是快照。所谓快照,就是像拍照一样。
|
||||
|
||||
|
||||
|
||||
如上图所示,我启动了两个进程,ps和bash。ps 就是我刚刚启动的,被ps自己捕捉到了;bash是因为我开了这个控制台,执行的shell是bash。
|
||||
|
||||
当然操作系统也不可能只有这么几个进程,这是因为不带任何参数的ps指令显示的是同一个电传打字机(TTY上)的进程。TTY 这个概念是一个历史的概念,过去用来传递信息,现在已经被传真、邮件、微信等取代。
|
||||
|
||||
操作系统上的 TTY 是一个输入输出终端的概念,比如用户打开 bash,操作系统就为用户分配了一个输入输出终端。没有加任何参数的ps只显示在同一个 TTY 的进程。
|
||||
|
||||
如果想看到所有的进程,可以用ps -e,-e没有特殊含义,只是为了和-A区分开。我们通常不直接用ps -e而是用ps -ef,这是因为-f可以带上更多的描述字段,如下图所示:
|
||||
|
||||
|
||||
|
||||
|
||||
UID 指进程的所有者;
|
||||
PID 是进程的唯一标识;
|
||||
PPID 是进程的父进程 ID;
|
||||
C 是 CPU 的利用率(就是 CPU 占用);
|
||||
STIME 是开始时间;
|
||||
TTY 是进程所在的 TTY,如果没有 TTY 就是 ?号;
|
||||
TIME;
|
||||
CMD 是进程启动时的命令,如果不是一个 Shell 命令,而是用方括号括起来,那就是系统进程或者内核过程。
|
||||
|
||||
|
||||
另外一个用得比较多的是ps aux,它和ps -ef能力差不多,但是是 BSD 风格的。就是加州伯克利分校研发的 Unix 分支版本的衍生风格,这种风格其实不太好描述,我截了一张图,你可以体会一下:
|
||||
|
||||
|
||||
|
||||
在 BSD 风格中有些字段的叫法和含义变了,如果你感兴趣,可以作为课后延伸学习的内容。
|
||||
|
||||
top
|
||||
|
||||
另外还有一个和ps能力差不多,但是显示的不是快照而是实时更新数据的top指令。因为自带的top显示的内容有点少, 所以我喜欢用一个叫作htop的指令,具体的安装全方法我会在 10 | 软件的安装: 编译安装和包管理器安装有什么优势和劣势?中给你介绍。本课时,我们先看一下使用效果,如下图所示:
|
||||
|
||||
|
||||
|
||||
以上,我们一起把进程学了一个皮毛,更多关于进程的内容我们会在模块四:进程和线程中讨论。
|
||||
|
||||
管道(Pipeline)
|
||||
|
||||
现在你已经掌握了一点点进程的基础,下面我们来学习管道,管道(Pipeline)的作用是在命令和命令之间,传递数据。比如说一个命令的结果,就可以作为另一个命令的输入。我们了解了进程,所以这里说的命令就是进程。更准确地说,管道在进程间传递数据。
|
||||
|
||||
输入输出流
|
||||
|
||||
每个进程拥有自己的标准输入流、标准输出流、标准错误流。
|
||||
|
||||
这几个标准流说起来很复杂,但其实都是文件。
|
||||
|
||||
|
||||
标准输入流(用 0 表示)可以作为进程执行的上下文(进程执行可以从输入流中获取数据)。
|
||||
标准输出流(用 1 表示)中写入的结果会被打印到屏幕上。
|
||||
如果进程在执行过程中发生异常,那么异常信息会被记录到标准错误流(用 2 表示)中。
|
||||
|
||||
|
||||
重定向
|
||||
|
||||
我们执行一个指令,比如ls -l,结果会写入标准输出流,进而被打印。这时可以用重定向符将结果重定向到一个文件,比如说ls -l > out,这样out文件就会有ls -l的结果;而屏幕上也不会再打印ls -l的结果。
|
||||
|
||||
|
||||
|
||||
具体来说>符号叫作覆盖重定向;>>叫作追加重定向。>每次都会把目标文件覆盖,>>会在目标文件中追加。比如你每次启动一个程序日志都写入/var/log/somelogfile中,可以这样操作,如下所示:
|
||||
|
||||
start.sh >> /var/log/somelogfile
|
||||
|
||||
|
||||
经过这样的操作后,每次执行程序日志就不会被覆盖了。
|
||||
|
||||
另外还有一种情况,比如我们输入:
|
||||
|
||||
ls1 > out
|
||||
|
||||
|
||||
结果并不会存入out文件,因为ls1指令是不存在的。结果会输出到标准错误流中,仍然在屏幕上。这里我们可以把标准错误流也重定向到标准输出流,然后再重定向到文件。
|
||||
|
||||
ls1 &> out
|
||||
|
||||
|
||||
这个写法等价于:
|
||||
|
||||
ls1 > out 2>&1
|
||||
|
||||
|
||||
|
||||
|
||||
相当于把ls1的标准输出流重定向到out,因为ls1 > out出错了,所以标准错误流被定向到了标准输出流。&代表一种引用关系,具体代表的是ls1 >out的标准输出流。
|
||||
|
||||
管道的作用和分类
|
||||
|
||||
有了进程和重定向的知识,接下来我们梳理下管道的作用。管道(Pipeline)将一个进程的输出流定向到另一个进程的输入流,就像水管一样,作用就是把这两个文件接起来。如果一个进程输出了一个字符 X,那么另一个进程就会获得 X 这个输入。
|
||||
|
||||
管道和重定向很像,但是管道是一个连接一个进行计算,重定向是将一个文件的内容定向到另一个文件,这二者经常会结合使用。
|
||||
|
||||
Linux 中的管道也是文件,有两种类型的管道:
|
||||
|
||||
|
||||
匿名管道(Unnamed Pipeline),这种管道也在文件系统中,但是它只是一个存储节点,不属于任何一个目录。说白了,就是没有路径。
|
||||
命名管道(Named Pipeline),这种管道就是一个文件,有自己的路径。
|
||||
|
||||
|
||||
FIFO
|
||||
|
||||
管道具有 FIFO(First In First Out),FIFO 和排队场景一样,先排到的先获得。所以先流入管道文件的数据,也会先流出去传递给管道下游的进程。
|
||||
|
||||
使用场景分析
|
||||
|
||||
接下来我们以多个场景举例帮助你深入学习管道。
|
||||
|
||||
排序
|
||||
|
||||
比如我们用ls,希望按照文件名排序倒序,可以使用匿名管道,将ls的结果传递给sort指令去排序。你看,这样ls的开发者就不用关心排序问题了。
|
||||
|
||||
|
||||
|
||||
去重
|
||||
|
||||
另一个比较常见的场景是去重,比如有一个字典文件,里面都是词语。如下所示:
|
||||
|
||||
Apple
|
||||
|
||||
Banana
|
||||
|
||||
Apple
|
||||
|
||||
Banana
|
||||
|
||||
……
|
||||
|
||||
|
||||
如果我们想要去重可以使用uniq指令,uniq指令能够找到文件中相邻的重复行,然后去重。但是我们上面的文件重复行是交替的,所以不可以直接用uniq,因此可以先sort这个文件,然后利用管道将sort的结果重定向到uniq指令。指令如下:
|
||||
|
||||
|
||||
|
||||
筛选
|
||||
|
||||
有时候我们想根据正则模式筛选对应的内容。比如说我们想找到项目文件下所有文件名中含有Spring的文件。就可以利用grep指令,操作如下:
|
||||
|
||||
find ./ | grep Spring
|
||||
|
||||
|
||||
find ./递归列出当前目录下所有目录中的文件。grep从find的输出流中找出含有Spring关键字的行。
|
||||
|
||||
如果我们希望包含Spring但不包含MyBatis就可以这样操作:
|
||||
|
||||
find ./ | grep Spring | grep -v MyBatis
|
||||
|
||||
|
||||
grep -v是匹配不包含 MyBatis 的结果。
|
||||
|
||||
数行数
|
||||
|
||||
还有一个比较常见的场景是数行数。比如你写了一个 Java 文件想知道里面有多少行,就可以使用wc -l指令,如下所示:
|
||||
|
||||
|
||||
|
||||
但是如果你想知道当前目录下有多少个文件,可以用ls | wc -l,如下所示:
|
||||
|
||||
|
||||
|
||||
接下来请你思考一个问题:我们如何知道当前java的项目目录下有多少行代码?
|
||||
|
||||
提示一下。你可以使用下面这个指令:
|
||||
|
||||
find -i ".java" ./ | wc -l
|
||||
|
||||
|
||||
快去自己动手写一写吧,你在尝试的过程中如果遇到什么问题,也可以写在留言区,我会逐一为你解答。
|
||||
|
||||
中间结果
|
||||
|
||||
管道一个接着一个,是一个计算逻辑。有时候我们想要把中间的结果保存下来,这就需要用到tee指令。tee指令从标准输入流中读取数据到标准输出流。
|
||||
|
||||
这时候,你可能会问: 老师, 这不是什么都没做吗?
|
||||
|
||||
别急,tee还有一个能力,就是自己利用这个过程把输入流中读取到的数据存到文件中。比如下面这条指令:
|
||||
|
||||
find ./ -i "*.java" | tee JavaList | grep Spring
|
||||
|
||||
|
||||
这句指令的意思是从当前目录中找到所有含有 Spring 关键字的 Java 文件。tee 本身不影响指令的执行,但是 tee 会把 find 指令的结果保存到 JavaList 文件中。
|
||||
|
||||
tee这个执行就像英文字母中的 T 一样,连通管道两端,下面又开了口。这个开口,在函数式编程里面叫作副作用。
|
||||
|
||||
xargs
|
||||
|
||||
上面我们学习的内容难度,已经由小学 1 年级攀升到了小学 6 年级,最后我们来看看初中难度的xargs指令。
|
||||
|
||||
xargs指令从标准数据流中构造并执行一行行的指令。xargs从输入流获取字符串,然后利用空白、换行符等切割字符串,在这些字符串的基础上构造指令,最后一行行执行这些指令。
|
||||
|
||||
举个例子,如果我们重命名当前目录下的所有 .a 的文件,想在这些文件前面加一个前缀prefix_。比如说x.a文件需要重命名成prefix_x.a,我们就可以用xargs指令构造模块化的指令。
|
||||
|
||||
现在我们有x.ay.az.a三个文件,如下图所示:
|
||||
|
||||
|
||||
|
||||
然后使用下图中的指令构造我们需要的指令:
|
||||
|
||||
|
||||
|
||||
|
||||
我们用ls找到所有的文件;
|
||||
-I参数是查找替换符,这里我们用GG替代ls找到的结果;-I GG后面的字符串 GG 会被替换为x.ax.b或x.z;
|
||||
echo是一个在命令行打印字符串的指令。使用echo主要是为了安全,帮助我们检查指令是否有错误。
|
||||
|
||||
|
||||
我们用xargs构造了 3 条指令。这里我再多讲一个词,叫作样板代码。如果你没有用xargs指令,而是用一条条mv指令去敲,这样就构成了样板代码。
|
||||
|
||||
最后去掉 echo,就是我们想要的结果,如下所示:
|
||||
|
||||
|
||||
|
||||
管道文件
|
||||
|
||||
上面我们花了较长的一段时间讨论匿名管道,用|就可以创造和使用。匿名管道也是利用了文件系统的能力,是一种文件结构。当你学到模块六文件系统的内容,会知道匿名管道拥有一个自己的inode,但不属于任何一个文件夹。
|
||||
|
||||
还有一种管道叫作命名管道(Named Pipeline)。命名管道是要挂到文件夹中的,因此需要创建。用mkfifo指令可以创建一个命名管道,下面我们来创建一个叫作pipe1的命名管道,如下图所示:
|
||||
|
||||
|
||||
|
||||
命名管道和匿名管道能力类似,可以连接一个输出流到另一个输入流,也是 First In First Out。
|
||||
|
||||
当执行cat pipe1的时候,你可以观察到,当前的终端处于等待状态。因为我们cat pipe1的时候pipe1中没有内容。
|
||||
|
||||
如果这个时候我们再找一个终端去写一点东西到pipe中,比如说:
|
||||
|
||||
echo "XXX" > pipe1
|
||||
|
||||
|
||||
这个时候,cat pipe1就会返回,并打印出xxx,如下所示:
|
||||
|
||||
|
||||
|
||||
我们可以像上图那样演示这段程序,在cat pipe1后面增加了一个&符号。这个&符号代表指令在后台执行,不会阻塞用户继续输入。然后我们通过echo指令往pipe1中写入东西,接着就会看到xxx被打印出来。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们为了学习管道,先简单接触了进程的概念,然后学习了重定向。之后我们学习了匿名管道的应用场景,匿名管道帮助我们把 Linux 指令串联起来形成很强的计算能力。特别是xargs指令支持模板化的生成指令,拓展了指令的能力。最后我们还学习了命名管道,命名管道让我们可以真实拿到一个管道文件,让多个程序之间可以方便地进行通信。
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:xargs 的作用了吗?
|
||||
|
||||
老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 xargs 将标准输入流中的字符串分割成一条条子字符串,然后再按照我们自己想要的方式构建成一条条指令,大大拓展了 Linux 指令的能力。
|
||||
|
||||
比如我们可以用来按照某种特定的方式逐个处理一个目录下所有的文件;根据一个 IP 地址列表逐个 ping 这些 IP,收集到每个 IP 地址的延迟等。
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
|
||||
|
279
专栏/重学操作系统-完/08用户和权限管理指令:请简述Linux权限划分的原则?.md
Normal file
279
专栏/重学操作系统-完/08用户和权限管理指令:请简述Linux权限划分的原则?.md
Normal file
@ -0,0 +1,279 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 用户和权限管理指令: 请简述 Linux 权限划分的原则?
|
||||
我看到过这样一道面试题:请简述 Linux 权限划分的原则?
|
||||
|
||||
这种类型的面试题也是我比较喜欢的一种题目,因为它考察的不仅是一个具体的指令,还考察了候选人技术层面的认知。
|
||||
|
||||
如果你对 Linux 权限有较深的认知和理解,那么完全可以通过查资料去完成具体指令的执行。更重要的是,认知清晰的程序员可以把 Linux 权限管理的知识迁移到其他的系统设计中。而且我认为,能够对某个技术形成认知的人, 同样也会热爱思考,善于总结,这样的程序员是所有团队梦寐以求的。
|
||||
|
||||
因此,这次我们就把这道面试题作为引子,开启今天的学习。
|
||||
|
||||
权限抽象
|
||||
|
||||
一个完整的权限管理体系,要有合理的抽象。这里就包括对用户、进程、文件、内存、系统调用等抽象。下面我将带你一一了解。
|
||||
|
||||
首先,我们先来说说用户和组。Linux 是一个多用户平台,允许多个用户同时登录系统工作。Linux 将用户抽象成了账户,账户可以登录系统,比如通过输入登录名 + 密码的方式登录;也可以通过证书的方式登录。
|
||||
|
||||
但为了方便分配每个用户的权限,Linux 还支持组 (Group)账户。组账户是多个账户的集合,组可以为成员们分配某一类权限。每个用户可以在多个组,这样就可以利用组给用户快速分配权限。
|
||||
|
||||
组的概念有点像微信群。一个用户可以在多个群中。比如某个组中分配了 10 个目录的权限,那么新建用户的时候可以将这个用户增加到这个组中,这样新增的用户就不必再去一个个目录分配权限。
|
||||
|
||||
而每一个微信群都有一个群主,Root 账户也叫作超级管理员,就相当于微信群主,它对系统有着完全的掌控。一个超级管理员可以使用系统提供的全部能力。
|
||||
|
||||
此外,Linux 还对文件进行了权限抽象(注意目录也是一种文件)。Linux 中一个文件可以设置下面 3 种权限:
|
||||
|
||||
|
||||
读权限(r):控制读取文件。
|
||||
写权限(w):控制写入文件。
|
||||
执行权限(x):控制将文件执行,比如脚本、应用程序等。
|
||||
|
||||
|
||||
|
||||
|
||||
然后每个文件又可以从 3 个维度去配置上述的 3 种权限:
|
||||
|
||||
|
||||
用户维度。每个文件可以所属 1 个用户,用户维度配置的 rwx 在用户维度生效;
|
||||
组维度。每个文件可以所属 1 个分组,组维度配置的 rwx 在组维度生效;
|
||||
全部用户维度。设置对所有用户的权限。
|
||||
|
||||
|
||||
|
||||
|
||||
因此 Linux 中文件的权限可以用 9 个字符,3 组rwx描述:第一组是用户权限,第二组是组权限,第三组是所有用户的权限。然后用-代表没有权限。比如rwxrwxrwx代表所有维度可以读写执行。rw--wxr-x代表用户维度不可以执行,组维度不可以读取,所有用户维度不可以写入。
|
||||
|
||||
通常情况下,如果用ls -l查看一个文件的权限,会有 10 个字符,这是因为第一个字符代表的是文件类型。我们在 06 课时讲解“几种常见的文件类型”时提到过,有管道文件、目录文件、链接文件等等。-代表普通文件、d代表目录、p代表管道。
|
||||
|
||||
学习了这套机制之后,请你跟着我的节奏一起思考以下 4 个问题。
|
||||
|
||||
|
||||
文件被创建后,初始的权限如何设置?
|
||||
需要全部用户都可以执行的指令,比如ls,它们的权限如何分配?
|
||||
给一个文本文件分配了可执行权限会怎么样?
|
||||
可不可以多个用户都登录root,然后只用root账户?
|
||||
|
||||
|
||||
你可以把以上 4 个问题作为本课时的小测验,把你的思考或者答案写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
问题一:初始权限问题
|
||||
|
||||
一个文件创建后,文件的所属用户会被设置成创建文件的用户。谁创建谁拥有,这个逻辑很顺理成章。但是文件的组又是如何分配的呢?
|
||||
|
||||
这里 Linux 想到了一个很好的办法,就是为每个用户创建一个同名分组。
|
||||
|
||||
比如说zhang这个账户创建时,会创建一个叫作zhang的分组。zhang登录之后,工作分组就会默认使用它的同名分组zhang。如果zhang想要切换工作分组,可以使用newgrp指令切换到另一个工作分组。因此,被创建文件所属的分组是当时用户所在的工作分组,如果没有特别设置,那么就属于用户所在的同名分组。
|
||||
|
||||
再说下文件的权限如何?文件被创建后的权限通常是:
|
||||
|
||||
rw-rw-r--
|
||||
|
||||
|
||||
也就是用户、组维度不可以执行,所有用户可读。
|
||||
|
||||
问题二:公共执行文件的权限
|
||||
|
||||
前面提到过可以用which指令查看ls指令所在的目录,我们发现在/usr/bin中。然后用ls -l查看ls的权限,可以看到下图所示:
|
||||
|
||||
|
||||
|
||||
第一个-代表这是一个普通文件,后面的 rwx 代表用户维度可读写和执行;第二个r-x代表组维度不可以写;第三个r-x代表所有用户可以读和执行。后面的两个root,第一个是所属用户,第二个是所属分组。
|
||||
|
||||
到这里你可能会有一个疑问:如果一个文件设置为不可读,但是可以执行,那么结果会怎样?
|
||||
|
||||
答案当然是不可以执行,无法读取文件内容自然不可以执行。
|
||||
|
||||
问题三:执行文件
|
||||
|
||||
在 Linux 中,如果一个文件可以被执行,则可以直接通过输入文件路径(相对路径或绝对路径)的方式执行。如果想执行一个不可以执行的文件,Linux 则会报错。
|
||||
|
||||
当用户输入一个文件名,如果没有指定完整路径,Linux 就会在一部分目录中查找这个文件。你可以通过echo $PATH看到 Linux 会在哪些目录中查找可执行文件,PATH是 Linux 的环境变量,关于环境变量,我将在 “12 | 高级技巧之集群部署中”和你详细讨论。
|
||||
|
||||
|
||||
|
||||
问题四:可不可以都 root
|
||||
|
||||
最后一个问题是,可不可以都root?
|
||||
|
||||
答案当然是不行!这里先给你留个悬念,具体原因我们会在本课时最后来讨论。
|
||||
|
||||
到这里,用户和组相关权限就介绍完了。接下来说说内核和系统调用权限。 内核是操作系统连接硬件、提供最核心能力的程序。今天我们先简单了解一下,关于内核的详细知识,会在“14 |用户态和内核态:用户态线程和内核态线程有什么区别?”中介绍。
|
||||
|
||||
内核提供操作硬件、磁盘、内存分页、进程等最核心的能力,并拥有直接操作全部内存的权限,因此内核不能把自己的全部能力都提供给用户,而且也不能允许用户通过shell指令进行系统调用。Linux 下内核把部分进程需要的系统调用以 C 语言 API 的形式提供出来。部分系统调用会有权限检查,比如说设置系统时间的系统调用。
|
||||
|
||||
以上我们看到了 Linux 对系统权限的抽象。接下来我们再说说权限架构的思想。
|
||||
|
||||
权限架构思想
|
||||
|
||||
优秀的权限架构主要目标是让系统安全、稳定且用户、程序之间相互制约、相互隔离。这要求权限系统中的权限划分足够清晰,分配权限的成本足够低。
|
||||
|
||||
因此,优秀的架构,应该遵循最小权限原则(Least Privilege)。权限设计需要保证系统的安全和稳定。比如:每一个成员拥有的权限应该足够的小,每一段特权程序执行的过程应该足够的短。对于安全级别较高的时候,还需要成员权限互相牵制。比如金融领域通常登录线上数据库需要两次登录,也就是需要两个密码,分别掌握在两个角色手中。这样即便一个成员出了问题,也可以保证整个系统安全。
|
||||
|
||||
同样的,每个程序也应该减少权限,比如说只拥有少量的目录读写权限,只可以进行少量的系统调用。
|
||||
|
||||
权限划分
|
||||
|
||||
此外,权限架构思想还应遵循一个原则,权限划分边界应该足够清晰,尽量做到相互隔离。Linux 提供了用户和分组。当然 Linux 没有强迫你如何划分权限,这是为了应对更多的场景。通常我们服务器上重要的应用,会由不同的账户执行。比如说 Nginx、Web 服务器、数据库不会执行在一个账户下。现在随着容器化技术的发展,我们甚至希望每个应用独享一个虚拟的空间,就好像运行在一个单独的操作系统中一样,让它们互相不用干扰。
|
||||
|
||||
到这里,你可能会问:为什么不用 root 账户执行程序? 下面我们就来说说 root 的危害。
|
||||
|
||||
举个例子,你有一个 Mysql 进程执行在 root(最大权限)账户上,如果有黑客攻破了你的 Mysql 服务,获得了在 Mysql 上执行 Sql 的权限,那么,你的整个系统就都暴露在黑客眼前了。这会导致非常严重的后果。
|
||||
|
||||
黑客可以利用 Mysql 的 Copy From Prgram 指令为所欲为,比如先备份你的关键文件,然后再删除他们,并要挟你通过指定账户打款。如果执行最小权限原则,那么黑客即便攻破我们的 Mysql 服务,他也只能获得最小的权限。当然,黑客拿到 Mysql 权限也是非常可怕的,但是相比拿到所有权限,这个损失就小多了。
|
||||
|
||||
分级保护
|
||||
|
||||
因为内核可以直接操作内存和 CPU,因此非常危险。驱动程序可以直接控制摄像头、显示屏等核心设备,也需要采取安全措施,比如防止恶意应用开启摄像头盗用隐私。通常操作系统都采取一种环状的保护模式。
|
||||
|
||||
|
||||
|
||||
如上图所示,内核在最里面,也就是 Ring 0。 应用在最外面也就是Ring 3。驱动在中间,也就是 Ring 1 和 Ring 2。对于相邻的两个 Ring,内层 Ring 会拥有较高的权限,可以改变外层的 Ring;而外层的 Ring 想要使用内层 Ring 的资源时,会有专门的程序(或者硬件)进行保护。
|
||||
|
||||
比如说一个 Ring3 的应用需要使用内核,就需要发送一个系统调用给内核。这个系统调用会由内核进行验证,比如验证用户有没有足够的权限,以及这个行为是否安全等等。
|
||||
|
||||
权限包围(Privilege Bracking)
|
||||
|
||||
之前我们讨论过,当 Mysql 跑在 root 权限时,如果 Mysql 被攻破,整个机器就被攻破了。因此我们所有应用都不要跑在 root 上。如果所有应用都跑在普通账户下,那么就会有临时提升权限的场景。比如说安装程序可能需要临时拥有管理员权限,将应用装到/usr/bin目录下。
|
||||
|
||||
Linux 提供了权限包围的能力。比如一个应用,临时需要高级权限,可以利用交互界面(比如让用户输入 root 账户密码)验证身份,然后执行需要高级权限的操作,然后马上恢复到普通权限工作。这样做可以减少应用在高级权限的时间,并做到专权专用,防止被恶意程序利用。
|
||||
|
||||
用户分组指令
|
||||
|
||||
上面我们讨论了 Linux 权限的架构,接下来我们学习一些具体的指令。
|
||||
|
||||
查看
|
||||
|
||||
如果想查看当前用户的分组可以使用groups指令。
|
||||
|
||||
|
||||
|
||||
上面指令列出当前用户的所有分组。第一个是同名的主要分组,后面从adm开始是次级分组。
|
||||
|
||||
我先给你介绍两个分组,其他分组你可以去查资料:
|
||||
|
||||
|
||||
adm 分组用于系统监控,比如/var/log中的部分日志就是 adm 分组。
|
||||
sudo 分组用户可以通过 sudo 指令提升权限。
|
||||
|
||||
|
||||
如果想查看当前用户,可以使用id指令,如下所示:
|
||||
|
||||
|
||||
|
||||
|
||||
uid 是用户 id;
|
||||
gid 是组 id;
|
||||
groups 后面是每个分组和分组的 id。
|
||||
|
||||
|
||||
如果想查看所有的用户,可以直接看/etc/passwd。
|
||||
|
||||
|
||||
|
||||
/etc/passwd这个文件存储了所有的用户信息,如下图所示:
|
||||
|
||||
|
||||
|
||||
创建用户
|
||||
|
||||
创建用户用useradd指令。
|
||||
|
||||
sudo useradd foo
|
||||
|
||||
|
||||
sudo 原意是 superuser do,后来演变成用另一个用户的身份去执行某个指令。如果没有指定需要 sudo 的用户,就像上面那样,就是以超级管理员的身份。因为 useradd 需要管理员身份。这句话执行后,会进行权限提升,并弹出输入管理员密码的输入界面。
|
||||
|
||||
创建分组
|
||||
|
||||
创建分组用groupadd指令。下面指令创建一个叫作hello的分组。
|
||||
|
||||
sudo groupadd hello
|
||||
|
||||
|
||||
为用户增加次级分组
|
||||
|
||||
组分成主要分组(Primary Group)和次级分组(Secondary Group)。主要分组只有 1 个,次级分组可以有多个。如果想为用户添加一个次级分组,可以用usermod指令。下面指令将用户foo添加到sudo分组,从而foo拥有了sudo的权限。
|
||||
|
||||
sudo usermod -a -G sudo foo
|
||||
|
||||
|
||||
-a代表append,-G代表一个次级分组的清单, 最后一个foo是账户名。
|
||||
|
||||
修改用户主要分组
|
||||
|
||||
修改主要分组还是使用usermod指令。只不过参数是小写的-g。
|
||||
|
||||
sudo usermod -g somegroup foo
|
||||
|
||||
|
||||
文件权限管理指令
|
||||
|
||||
接下来我们学习文件管理相关的指令。
|
||||
|
||||
查看
|
||||
|
||||
我们可以用ls -l查看文件的权限,相关内容在本课时前面已经介绍过了。
|
||||
|
||||
修改文件权限
|
||||
|
||||
可以用chmod修改文件权限,chmod( change file mode bits),也就是我们之前学习的 rwx,只不过 rwx 在 Linux 中是用三个连在一起的二进制位来表示。
|
||||
|
||||
# 设置foo可以执行
|
||||
|
||||
chmod +x ./foo
|
||||
|
||||
# 不允许foo执行
|
||||
|
||||
chmod -x ./foo
|
||||
|
||||
# 也可以同时设置多个权限
|
||||
|
||||
chmod +rwx ./foo
|
||||
|
||||
|
||||
因为rwx在 Linux 中用相邻的 3 个位来表示。比如说111代表rwx,101代表r-x。而rwx总共有三组,分别是用户权限、组权限和全部用户权限。也就是可以用1111111119 个 1 代表rwxrwxrwx。又因为11110 进制是 7,因此当需要一次性设置用户权限、组权限和所有用户权限的时候,我们经常用数字表示。
|
||||
|
||||
# 设置rwxrwxrwx (111111111 -> 777)
|
||||
|
||||
chmod 777 ./foo
|
||||
|
||||
# 设置rw-rw-rw-(110110110 -> 666)
|
||||
|
||||
chmod 666 ./foo
|
||||
|
||||
|
||||
修改文件所属用户
|
||||
|
||||
有时候我们需要修改文件所属用户,这个时候会使用chown指令。 下面指令修改foo文件所属的用户为bar。
|
||||
|
||||
chown bar ./foo
|
||||
|
||||
|
||||
还有一些情况下,我们需要同时修改文件所属的用户和分组,比如我们想修改foo的分组位g,用户为u,可以使用:
|
||||
|
||||
chown g.u ./foo
|
||||
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习 Linux 的权限管理的抽象和架构思想。Linux 对用户、组、文件、系统调用等都进行了完善的抽象。之后,我们讨论了最小权限原则。最后我们对用户分组管理和文件权限管理两部分重要的指令进行了系统学习。
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:请简述 Linux 权限划分的原则?
|
||||
|
||||
老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 Linux 遵循最小权限原则。
|
||||
|
||||
|
||||
每个用户掌握的权限应该足够小,每个组掌握的权限也足够小。实际生产过程中,最好管理员权限可以拆分,互相牵制防止问题。
|
||||
每个应用应当尽可能小的使用权限。最理想的是每个应用单独占用一个容器(比如 Docker),这样就不存在互相影响的问题。即便应用被攻破,也无法攻破 Docker 的保护层。
|
||||
尽可能少的root。如果一个用户需要root能力,那么应当进行权限包围——马上提升权限(比如 sudo),处理后马上释放权限。
|
||||
系统层面实现权限分级保护,将系统的权限分成一个个 Ring,外层 Ring 调用内层 Ring 时需要内层 Ring 进行权限校验。
|
||||
|
||||
|
||||
|
||||
|
||||
|
199
专栏/重学操作系统-完/09Linux中的网络指令:如何查看一个域名有哪些NS记录?.md
Normal file
199
专栏/重学操作系统-完/09Linux中的网络指令:如何查看一个域名有哪些NS记录?.md
Normal file
@ -0,0 +1,199 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09 Linux 中的网络指令:如何查看一个域名有哪些 NS 记录?
|
||||
我看到过一道关于 Linux 指令的面试题:如何查看一个域名有哪些 NS 记录?
|
||||
|
||||
这类题目是根据一个场景,考察一件具体的事情如何处理。虽然你可以通过查资料找到解决方案,但是,这类问题在面试中还是有必要穿插一下,用于确定求职者技能是否熟练、经验是否丰富。特别是计算机网络相关的指令,平时在远程操作、开发、联调、Debug 线上问题的时候,会经常用到。
|
||||
|
||||
Linux 中提供了不少网络相关的指令,因为网络指令比较分散,本课时会从下面几个维度给你介绍,帮助你梳理常用的网络指令:
|
||||
|
||||
|
||||
远程操作;
|
||||
查看本地网络状态;
|
||||
网络测试;
|
||||
DNS 查询;
|
||||
HTTP。
|
||||
|
||||
|
||||
这块知识从体系上属于 Linux 指令,同时也关联了很多计算机网络的知识,比如说 TCP/IP 协议、UDP 协议,我会在“模块七”为你简要介绍。
|
||||
|
||||
如果你对这部分指令背后的网络原理有什么困惑,可以在评论区提问。另外,你也可以关注我将在拉勾教育推出的《计算机网络》课程。下面我们开始学习今天的内容。
|
||||
|
||||
远程操作指令
|
||||
|
||||
远程操作指令用得最多的是ssh,ssh指令允许远程登录到目标计算机并进行远程操作和管理。还有一个比较常用的远程指令是scp,scp帮助我们远程传送文件。
|
||||
|
||||
ssh(Secure Shell)
|
||||
|
||||
有一种场景需要远程登录一个 Linux 系统,这时我们会用到ssh指令。比如你想远程登录一台机器,可以使用ssh user@ip的方式,如下图所示:
|
||||
|
||||
|
||||
|
||||
上图中,我在使用ssh指令从机器u1登录我的另一台虚拟机u2。这里u1和u2对应着 IP 地址,是我在/etc/hosts中设置的,如下图所示:
|
||||
|
||||
|
||||
|
||||
/etc/hosts这个文件可以设置 IP 地址对应的域名。我这里是一个小集群,总共有两台机器,因此我设置了方便记忆和操作的名字。
|
||||
|
||||
scp
|
||||
|
||||
另一种场景是我需要拷贝一个文件到远程,这时可以使用scp指令,如下图,我使用scp指令将本地计算机的一个文件拷贝到了 ubuntu 虚拟机用户的家目录中。
|
||||
|
||||
比如从u1拷贝家目录下的文件a.txt到u2。家目录有一个简写,就是用~。具体指令见下图:
|
||||
|
||||
|
||||
|
||||
输入 scp 指令之后会弹出一个提示,要求输入密码,系统验证通过后文件会被成功拷贝。
|
||||
|
||||
查看本地网络状态
|
||||
|
||||
如果你想要了解本地的网络状态,比较常用的网络指令是ifconfig和netstat。
|
||||
|
||||
ifconfig
|
||||
|
||||
当你想知道本地ip以及本地有哪些网络接口时,就可以使用ifconfig指令。你可以把一个网络接口理解成一个网卡,有时候虚拟机会装虚拟网卡,虚拟网卡是用软件模拟的网卡。
|
||||
|
||||
比如:VMware 为每个虚拟机创造一个虚拟网卡,通过虚拟网卡接入虚拟网络。当然物理机也可以接入虚拟网络,它可以通过虚拟网络向虚拟机的虚拟网卡上发送信息。
|
||||
|
||||
下图是我的 ubuntu 虚拟机用 ifconfig 查看网络接口信息。
|
||||
|
||||
|
||||
|
||||
可以看到我的这台 ubuntu 虚拟机一共有 2 个网卡,ens33 和 lo。lo是本地回路(local lookback),发送给lo就相当于发送给本机。ens33是一块连接着真实网络的虚拟网卡。
|
||||
|
||||
netstat
|
||||
|
||||
另一个查看网络状态的场景是想看目前本机的网络使用情况,这个时候可以用netstat。
|
||||
|
||||
默认行为
|
||||
|
||||
不传任何参数的netstat帮助查询所有的本地 socket,下图是netstat | less的结果。
|
||||
|
||||
|
||||
|
||||
如上图,我们看到的是 socket 文件。socket 是网络插槽被抽象成了文件,负责在客户端、服务器之间收发数据。当客户端和服务端发生连接时,客户端和服务端会同时各自生成一个 socket 文件,用于管理这个连接。这里,可以用wc -l数一下有多少个socket。
|
||||
|
||||
|
||||
|
||||
你可以看到一共有 615 个 socket 文件,因为有很多 socket 在解决进程间的通信。就是将两个进程一个想象成客户端,一个想象成服务端。并不是真的有 600 多个连接着互联网的请求。
|
||||
|
||||
查看 TCP 连接
|
||||
|
||||
如果想看有哪些 TCP 连接,可以使用netstat -t。比如下面我通过netstat -t看tcp协议的网络情况:
|
||||
|
||||
|
||||
|
||||
这里没有找到连接中的tcp,因为我们这台虚拟机当时没有发生任何的网络连接。因此我们尝试从机器u2(另一台机器)ssh 登录进u1,再看一次:
|
||||
|
||||
|
||||
|
||||
如上图所示,可以看到有一个 TCP 连接了。
|
||||
|
||||
查看端口占用
|
||||
|
||||
还有一种非常常见的情形,我们想知道某个端口是哪个应用在占用。如下图所示:
|
||||
|
||||
|
||||
|
||||
这里我们看到 22 端口被 sshd,也就是远程登录模块被占用了。-n是将一些特殊的端口号用数字显示,-t是指看 TCP 协议,-l是只显示连接中的连接,-p是显示程序名称。
|
||||
|
||||
网络测试
|
||||
|
||||
当我们需要测试网络延迟、测试服务是否可用时,可能会用到ping和telnet指令。
|
||||
|
||||
ping
|
||||
|
||||
想知道本机到某个网站的网络延迟,就可以使用ping指令。如下图所示:
|
||||
|
||||
|
||||
|
||||
ping一个网站需要使用 ICMP 协议。因此你可以在上图中看到 icmp 序号。 这里的时间time是往返一次的时间。ttl叫作 time to live,是封包的生存时间。就是说,一个封包从发出就开始倒计时,如果途中超过 128ms,这个包就会被丢弃。如果包被丢弃,就会被算进丢包率。
|
||||
|
||||
另外ping还可以帮助我们看到一个网址的 IP 地址。 通过网址获得 IP 地址的过程叫作 DNS Lookup(DNS 查询)。ping利用了 DNS 查询,但是没有显示全部的 DNS 查询结果。
|
||||
|
||||
telnet
|
||||
|
||||
有时候我们想知道本机到某个 IP + 端口的网络是否通畅,也就是想知道对方服务器是否在这个端口上提供了服务。这个时候可以用telnet指令。 如下图所示:
|
||||
|
||||
|
||||
|
||||
telnet 执行后会进入一个交互式的界面,比如这个时候,我们输入下图中的文字就可以发送 HTTP 请求了。如果你对 HTTP 协议还不太了解,建议自学一下 HTTP 协议。如果希望和林老师一起学习,可以等待下我之后的《计算机网络》专栏。
|
||||
|
||||
|
||||
|
||||
如上图所示,第 5 行的GET 和第 6 行的HOST是我输入的。 拉勾网返回了一个 301 永久跳转。这是因为拉勾网尝试把http协议链接重定向到https。
|
||||
|
||||
DNS 查询
|
||||
|
||||
我们排查网络故障时想要进行一次 DNS Lookup,想知道一个网址 DNS 的解析过程。这个时候有多个指令可以用。
|
||||
|
||||
host
|
||||
|
||||
host 就是一个 DNS 查询工具。比如我们查询拉勾网的 DNS,如下图所示:
|
||||
|
||||
|
||||
|
||||
我们看到拉勾网 www.lagou.com 是一个别名,它的原名是 lgmain 开头的一个域名,这说明拉勾网有可能在用 CDN 分发主页(关于 CDN,我们《计算机网络》专栏见)。
|
||||
|
||||
上图中,可以找到 3 个域名对应的 IP 地址。
|
||||
|
||||
如果想追查某种类型的记录,可以使用host -t。比如下图我们追查拉勾的 AAAA 记录,因为拉勾网还没有部署 IPv6,所以没有找到。
|
||||
|
||||
|
||||
|
||||
dig
|
||||
|
||||
dig指令也是一个做 DNS 查询的。不过dig指令显示的内容更详细。下图是dig拉勾网的结果。
|
||||
|
||||
|
||||
|
||||
从结果可以看到www.lagou.com 有一个别名,用 CNAME 记录定义 lgmain 开头的一个域名,然后有 3 条 A 记录,通常这种情况是为了均衡负载或者分发内容。
|
||||
|
||||
HTTP 相关
|
||||
|
||||
最后我们来说说http协议相关的指令。
|
||||
|
||||
curl
|
||||
|
||||
如果要在命令行请求一个网页,或者请求一个接口,可以用curl指令。curl支持很多种协议,比如 LDAP、SMTP、FTP、HTTP 等。
|
||||
|
||||
我们可以直接使用 curl 请求一个网址,获取资源,比如我用 curl 直接获取了拉勾网的主页,如下图所示:
|
||||
|
||||
|
||||
|
||||
如果只想看 HTTP 返回头,可以使用curl -I。
|
||||
|
||||
另外curl还可以执行 POST 请求,比如下面这个语句:
|
||||
|
||||
curl -d '{"x" : 1}' -H "Content-Type: application/json" -X POST http://localhost:3000/api
|
||||
|
||||
|
||||
curl在向localhost:3000发送 POST 请求。-d后面跟着要发送的数据, -X后面是用到的 HTTP 方法,-H是指定自定义的请求头。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了不少网络相关的 Linux 指令,这些指令是将来开发和调试的强大工具。这里再给你复习一下这些指令:
|
||||
|
||||
|
||||
远程登录的 ssh 指令;
|
||||
远程拷贝文件的 scp 指令;
|
||||
查看网络接口的 ifconfig 指令;
|
||||
查看网络状态的 netstat 指令;
|
||||
测试网络延迟的 ping 指令;
|
||||
可以交互式调试和服务端的 telnet 指令;
|
||||
两个 DNS 查询指令 host 和 dig;
|
||||
可以发送各种请求包括 HTTPS 的 curl 指令。
|
||||
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:如何查看一个域名有哪些 NS 记录了吗?
|
||||
|
||||
老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 host 指令提供了一个-t参数指定需要查找的记录类型。我们可以使用host -t ns {网址}。另外 dig 也提供了同样的能力。如果你感兴趣,还可以使用man对系统进行操作。
|
||||
|
||||
|
||||
|
||||
|
249
专栏/重学操作系统-完/10软件的安装:编译安装和包管理器安装有什么优势和劣势?.md
Normal file
249
专栏/重学操作系统-完/10软件的安装:编译安装和包管理器安装有什么优势和劣势?.md
Normal file
@ -0,0 +1,249 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 软件的安装: 编译安装和包管理器安装有什么优势和劣势?
|
||||
今天给你带来的面试题是:编译安装和包管理器安装有什么优势和劣势?为了搞清楚这个问题,就引出了今天的话题,在 Linux 上如何安装程序。
|
||||
|
||||
在 Linux 上安装程序大概有 2 种思路:
|
||||
|
||||
|
||||
直接编译源代码;
|
||||
使用包管理器。
|
||||
|
||||
|
||||
受开源运动影响,Linux 上很多软件都可以拿到源代码,这也是 Linux 能取得成功的一个重要原因。接下来我们先尝试用包管理器安装应用,然后再用一个实战的例子,教你如何编译安装nginx。
|
||||
|
||||
包管理器使用
|
||||
|
||||
Linux 下的应用程序多数以软件包的形式发布,用户拿到对应的包之后,使用包管理器进行安装。说到包管理器,就要提到dpkg和rpm。
|
||||
|
||||
我们先说说包。 Linux 下两大主流的包就是rpm和dpkg。
|
||||
|
||||
dpkg(debian package),是linux一个主流的社区分支开发出来的。社区就是开源社区,有很多世界顶级的程序员会在社区贡献代码,比如 github。一般衍生于debian的 Linux 版本都支持dpkg,比如ubuntu。
|
||||
|
||||
rpm(redhatpackage manager)。在正式讲解之前,我们先来聊聊 RedHat 这家公司。
|
||||
|
||||
RedHat 是一个做 Linux 的公司,你可以把它理解成一家“保险公司”。 很多公司购买红帽的服务,是为了给自己的业务上一个保险。以防万一哪天公司内部搞不定 Linux 底层,或者底层有 Bug,再或者底层不适合当下的业务发展,需要修改等问题,红帽的工程师都可以帮企业解决。
|
||||
|
||||
再比如,RedHat 收购了JBoss,把 JBoss 改名为 WildFly。 像 WildFly 这种工具更多是面向企业级,比如没有大量研发团队的企业会更倾向使用成熟的技术。RedHat 公司也有自己的 Linux,就叫作 RedHat。RedHat 系比较重要的 Linux 有 RedHat/Fedora 等。
|
||||
|
||||
无论是dpkg还是rpm都抽象了自己的包格式,就是以.dpkg或者.rpm结尾的文件。
|
||||
|
||||
dpkg和rpm也都提供了类似的能力:
|
||||
|
||||
|
||||
查询是否已经安装了某个软件包;
|
||||
查询目前安装了什么软件包;
|
||||
给定一个软件包,进行安装;
|
||||
删除一个安装好的软件包。
|
||||
|
||||
|
||||
关于dpkg和rpm的具体用法,你可以用man进行学习。接下来我们聊聊yum和apt。
|
||||
|
||||
自动依赖管理
|
||||
|
||||
Linux 是一个开源生态,因此工具非常多。工具在给用户使用之前,需要先打成dpkg或者rpm包。 有的时候一个包会依赖很多其他的包,而dpkg和rpm不会对这种情况进行管理,有时候为了装一个包需要先装十几个依赖的包,过程非常艰辛!因此现在多数情况都在用yum和apt。
|
||||
|
||||
yum
|
||||
|
||||
你可能会说,我不用yum也不用apt,我只用docker。首先给你一个连击 666,然后我还是要告诉你,如果你做docker镜像,那么还是要用到yum和apt,因此还是有必要学一下。
|
||||
|
||||
yum的全名是 Yellodog Updator,Modified。 看名字就知道它是基于Yellodog Updator这款软件修改而来的一个工具。yum是 Python 开发的,提供的是rpm包,因此只有redhat系的 Linux,比如 Fedora,Centos 支持yum。yum的主要能力就是帮你解决下载和依赖两个问题。
|
||||
|
||||
下载之所以是问题,是因为 Linux 生态非常庞大,有时候用户不知道该去哪里下载一款工具。比如用户想安装vim,只需要输入sudo yum install vim就可以安装了。yum的服务器收集了很多linux软件,因此yum会帮助用户找到vim的包。
|
||||
|
||||
另一方面,yum帮助用户解决了很多依赖,比如用户安装一个软件依赖了 10 个其他的软件,yum会把这 11 个软件一次性的装好。
|
||||
|
||||
关于yum的具体用法,你可以使用man工具进行学习。
|
||||
|
||||
apt
|
||||
|
||||
接下来我们来重点说说apt,然后再一起尝试使用。因为我这次是用ubuntuLinux 给你教学,所以我以 apt 为例子,yum 的用法是差不多的,你可以自己 man 一下。
|
||||
|
||||
apt全名是 Advanced Packaging Tools,是一个debian及其衍生 Linux 系统下的包管理器。由于advanced(先进)是相对于dpkg而言的,因此它也能够提供和yum类似的下载和依赖管理能力。比如在没有vim的机器上,我们可以用下面的指令安装vim。如下图所示:
|
||||
|
||||
|
||||
|
||||
然后用dpkg指令查看 vim 的状态是ii。第一个i代表期望状态是已安装,第二个i代表实际状态是已安装。
|
||||
|
||||
下面我们卸载vim,再通过dpkg查看,如下图所示:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
我们看到 vim 的状态从ii变成了rc,r是期望删除,c是实际上还有配置文件遗留。 如果我们想彻底删除配置文件,可以使用apt purge,就是彻底清除的意思,如下图所示:
|
||||
|
||||
|
||||
|
||||
再使用dpkg -l时,vim已经清除了。
|
||||
|
||||
|
||||
|
||||
期待结果是u就是 unkonw(未知)说明已经没有了。实际结果是n,就是 not-installed(未安装)。
|
||||
|
||||
如果想查询mysql相关的包,可以使用apt serach mysql,这样会看到很多和mysql相关的包,如下图所示:
|
||||
|
||||
|
||||
|
||||
如果我们想精确查找一个叫作mysql-server的包,可以用apt list。
|
||||
|
||||
|
||||
|
||||
这里我们找到了mysql-server包。
|
||||
|
||||
另外有时候国内的apt服务器速度比较慢,你可以尝试使用阿里云的镜像服务器。具体可参考我下面的操作:
|
||||
|
||||
cat /etc/apt/sources.list
|
||||
|
||||
--以下是文件内容--
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
|
||||
|
||||
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
|
||||
|
||||
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
|
||||
|
||||
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
|
||||
|
||||
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
|
||||
|
||||
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
|
||||
|
||||
|
||||
镜像地址可以通过/etc/apt/sources.list配置,注意focal是我用的ubuntu版本,你可以使用sudo lsb_release查看自己的 Ubuntu 版本。如果你想用我上面给出的内容覆盖你的sources.list,只需把版本号改成你自己的。注意,每个ubuntu版本都有自己的代号。
|
||||
|
||||
|
||||
|
||||
通过上面的学习,相信你已经逐渐了解了包管理器的基本概念和使用。如果你是centos或者fedora,需要自己man一下yum。
|
||||
|
||||
编译安装 Nginx
|
||||
|
||||
接下来我们说说编译安装 Nginx(发音是 engine X),是一个家喻户晓的 Web 服务器。 它的发明者是俄国的伊戈尔·赛索耶夫。赛索耶夫 2002 年开始写 Nginx,主要目的是解决同一个互联网节点同时进入大量并发请求的问题。注意,大量并发请求不是大量 QPS 的意思,QPS 是吞吐量大,需要快速响应,而高并发时则需要合理安排任务调度。
|
||||
|
||||
后来塞索耶夫成立了 Nginx 公司, 2018 年估值到达到 4.3 亿美金。现在基本上国内大厂的 Web 服务器都是基于 Nginx,只不过进行了特殊的修改,比如淘宝用 Tengine。
|
||||
|
||||
下面我们再来看看源码安装,在 Linux 上获取nginx源码,可以去搜索 Nginx 官方网站,一般都会提供源码包。
|
||||
|
||||
|
||||
|
||||
如上图所示,可以看到 nginx-1.18.0 的网址是:http://nginx.org/download/nginx-1.19.2.tar.gz。然后我们用 wget 去下载这个包。 wget 是 GNU 项目下的下载工具,GNU 是早期unix项目的一个变种。linux下很多工具都是从unix继承来的,这就是开源的好处,很多工具不用再次开发了。你可能很难想象windows下的命令工具可以在linux下用,但是linux下的工具却可以在任何系统中用。 因此,linux下面的工具发展速度很快,如今已成为最受欢迎的服务器操作系统。
|
||||
|
||||
当然也有同学的机器上没有wget,那么你可以用apt安装一下。
|
||||
|
||||
|
||||
第一步:下载源码。我们使用wget下载nginx源码包:
|
||||
|
||||
|
||||
|
||||
|
||||
可以像我这样使用cd先切换到家目录。
|
||||
|
||||
|
||||
第二步:解压。我们解压下载好的nginx源码包。
|
||||
|
||||
|
||||
|
||||
|
||||
用ls发现包已经存在了,然后使用tar命令解压。
|
||||
|
||||
tar是用来打包和解压用的。之所以叫作tar是有一些历史原因:t代表tape(磁带);ar是 archive(档案)。因为早期的存储介质很小,人们习惯把文件打包然后存储到磁带上,那时候unix用的命令就是tar。因为linux是个开源生态,所以就沿袭下来继续使用tar。
|
||||
|
||||
-x代表 extract(提取)。-z代表gzip,也就是解压gz类型的文件。-v代表 verbose(显示细节),如果你不输入-v,就不会打印解压过程了。-f代表 file,这里指的是要操作文件,而不是磁带。 所以tar解压通常带有x和f,打包通常是c就是 create 的意思。
|
||||
|
||||
|
||||
第三步:配置和解决依赖。解压完,我们进入nginx的目录看一看。 如下图所示:
|
||||
|
||||
|
||||
|
||||
|
||||
可以看到一个叫作configure的文件是绿色的,也就是可执行文件。然后我们执行 configure 文件进行配置,这个配置文件来自一款叫作autoconf的工具,也是 GNU 项目下的,说白了就是bash(Bourne Shell)下的安装打包工具(就是个安装程序)。这个安装程序支持很多配置,你可以用./configure --help看到所有的配置项,如下图所示:
|
||||
|
||||
|
||||
|
||||
这里有几个非常重要的配置项,叫作prefix。prefix配置项决定了软件的安装目录。如果不配置这个配置项,就会使用默认的安装目录。sbin-path决定了nginx的可执行文件的位置。conf-path决定了nginx配置文件的位置。我们都使用默认,然后执行./configure,如下图所示:
|
||||
|
||||
|
||||
|
||||
autoconf进行依赖检查的时候,报了一个错误,cc 没有找到。这是因为机器上没有安装gcc工具,gcc 是家喻户晓的工具套件,全名是 GNU Compiler Collection——里面涵盖了包括 c/c++ 在内的多门语言的编译器。
|
||||
|
||||
我们用包管理器,安装gcc,如下图所示。安装gcc通常是安装build-essential这个包。
|
||||
|
||||
|
||||
|
||||
安装完成之后,再执行./configure,如下图所示:
|
||||
|
||||
|
||||
|
||||
我们看到配置程序开始执行。但是最终报了一个错误,如下图所示:
|
||||
|
||||
|
||||
|
||||
报错的内容是,nginx的HTTP rewrite模块,需要PCRE库。 PCRE 是perl语言的兼容正则表达式库。perl语言一直以支持原生正则表达式,而受到广大编程爱好者的喜爱。我曾经看到过一个 IBM 的朋友用perl加上wget就实现了一个简单的爬虫。接下来,我们开始安装PCRE。
|
||||
|
||||
一般这种依赖库,会叫pcre-dev或者libpcre。用apt查询了一下,然后grep。
|
||||
|
||||
|
||||
|
||||
我们看到有pcre2也有pcre3。这个时候可以考虑试试pcre3。
|
||||
|
||||
|
||||
|
||||
安装完成之后再试试./configure,提示还需要zlib。然后我们用类似的方法解决zlib依赖。
|
||||
|
||||
|
||||
|
||||
zlib包的名字叫zlib1g不太好找,需要查资料才能确定是这个名字。
|
||||
|
||||
我们再尝试配置,终于配置成功了。
|
||||
|
||||
|
||||
|
||||
|
||||
第四步:编译和安装。
|
||||
|
||||
|
||||
通常配置完之后,我们输入make && sudo make install进行编译和安装。make是linux下面一个强大的构建工具。autoconf也就是./configure会在当前目录下生成一个 MakeFile 文件。make会根据MakeFile文件编译整个项目。编译完成后,能够形成和当前操作系统以及 CPU 指令集兼容的二进制可执行文件。然后再用make install安装。&&符号代表执行完make再去执行make installl。
|
||||
|
||||
|
||||
|
||||
你可以看到编译是个非常慢的活。等待了差不多 1 分钟,终于结束了。nginx被安装到了/usr/local/nginx中,如果需要让nginx全局执行,可以设置一个软连接到/usr/local/bin,具体如下:
|
||||
|
||||
ln -sf /usr/local/nginx/sbin/nginx /usr/local/sbin/nginx
|
||||
|
||||
|
||||
为什么会有编译安装?
|
||||
|
||||
学完整个编译安装 Ngnix 过程后,你可能会问,为什么会有编译安装这么复杂的事情。
|
||||
|
||||
原来使用 C/C++ 写的程序存在一个交叉编译的问题。就是写一次程序,在很多个平台执行。而不同指令集的 CPU 指令,还有操作系统的可执行文件格式是不同的。因此,这里有非常多的现实问题需要解决。一般是由操作系统的提供方,比如 RedHat 来牵头解决这些问题。你可以用apt等工具提供给用户已经编译好的包。apt会自动根据用户的平台类型选择不同的包。
|
||||
|
||||
但如果某个包没有在平台侧注册,也没有提供某个 Linux 平台的软件包,我们就需要回退到编译安装,通过源代码直接在某个平台安装。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了在 Linux 上安装软件,简要介绍了dpkg和rpm,然后介绍了能够解决依赖和帮助用户下载的yum和apt。重点带你使用了apt,在这个过程中看到了强大的包管理机制,今天的maven、npm、pip都继承了这样一个特性。最后我们还尝试了一件高难度的事情,就是编译安装nginx。
|
||||
|
||||
那么通过这节课的学习,你现在可以来回答本节关联的面试题目:编译安装和包管理安装有什么优势和劣势了吗?
|
||||
|
||||
老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 包管理安装很方便,但是有两点劣势。
|
||||
|
||||
第一点是需要提前将包编译好,因此有一个发布的过程,如果某个包没有发布版本,或者在某个平台上找不到对应的发布版本,就需要编译安装。
|
||||
|
||||
第二点就是如果一个软件的定制程度很高,可能会在编译阶段传入参数,比如利用configure传入配置参数,这种时候就需要编译安装。
|
||||
|
||||
|
||||
|
||||
|
124
专栏/重学操作系统-完/11高级技巧之日志分析:利用Linux指令分析Web日志.md
Normal file
124
专栏/重学操作系统-完/11高级技巧之日志分析:利用Linux指令分析Web日志.md
Normal file
@ -0,0 +1,124 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 高级技巧之日志分析:利用 Linux 指令分析 Web 日志
|
||||
著名的黑客、自由软件运动的先驱理查德.斯托曼说过,“编程不是科学,编程是手艺”。可见,要想真正搞好编程,除了学习理论知识,还需要在实际的工作场景中进行反复的锤炼。
|
||||
|
||||
所以今天我们将结合实际的工作场景,带你利用 Linux 指令分析 Web 日志,这其中包含很多小技巧,掌握了本课时的内容,将对你将来分析线上日志、了解用户行为和查找问题有非常大地帮助。
|
||||
|
||||
本课时将用到一个大概有 5W 多条记录的nginx日志文件,你可以在 GitHub上下载。 下面就请你和我一起,通过分析这个nginx日志文件,去锤炼我们的手艺。
|
||||
|
||||
第一步:能不能这样做?
|
||||
|
||||
当我们想要分析一个线上文件的时候,首先要思考,能不能这样做? 这里你可以先用htop指令看一下当前的负载。如果你的机器上没有htop,可以考虑用yum或者apt去安装。
|
||||
|
||||
|
||||
|
||||
如上图所示,我的机器上 8 个 CPU 都是 0 负载,2G的内存用了一半多,还有富余。 我们用wget将目标文件下载到本地(如果你没有 wget,可以用yum或者apt安装)。
|
||||
|
||||
wget 某网址(自己替代)
|
||||
|
||||
|
||||
然后我们用ls查看文件大小。发现这只是一个 7M 的文件,因此对线上的影响可以忽略不计。如果文件太大,建议你用scp指令将文件拷贝到闲置服务器再分析。下图中我使用了--block-size让ls以M为单位显示文件大小。
|
||||
|
||||
|
||||
|
||||
确定了当前机器的CPU和内存允许我进行分析后,我们就可以开始第二步操作了。
|
||||
|
||||
第二步:LESS 日志文件
|
||||
|
||||
在分析日志前,给你提个醒,记得要less一下,看看日志里面的内容。之前我们说过,尽量使用less这种不需要读取全部文件的指令,因为在线上执行cat是一件非常危险的事情,这可能导致线上服务器资源不足。
|
||||
|
||||
|
||||
|
||||
如上图所示,我们看到nginx的access_log每一行都是一次用户的访问,从左到右依次是:
|
||||
|
||||
|
||||
IP 地址;
|
||||
时间;
|
||||
HTTP 请求的方法、路径和协议版本、返回的状态码;
|
||||
User Agent。
|
||||
|
||||
|
||||
第三步:PV 分析
|
||||
|
||||
PV(Page View),用户每访问一个页面就是一次Page View。对于nginx的acess_log来说,分析 PV 非常简单,我们直接使用wc -l就可以看到整体的PV。
|
||||
|
||||
|
||||
|
||||
如上图所示:我们看到了一共有 51462 条 PV。
|
||||
|
||||
第四步:PV 分组
|
||||
|
||||
通常一个日志中可能有几天的 PV,为了得到更加直观的数据,有时候需要按天进行分组。为了简化这个问题,我们先来看看日志中都有哪些天的日志。
|
||||
|
||||
使用awk '{print $4}' access.log | less可以看到如下结果。awk是一个处理文本的领域专有语言。这里就牵扯到领域专有语言这个概念,英文是Domain Specific Language。领域专有语言,就是为了处理某个领域专门设计的语言。比如awk是用来分析处理文本的DSL,html是专门用来描述网页的DSL,SQL是专门用来查询数据的DSL……大家还可以根据自己的业务设计某种针对业务的DSL。
|
||||
|
||||
你可以看到我们用$4代表文本的第 4 列,也就是时间所在的这一列,如下图所示:
|
||||
|
||||
|
||||
|
||||
我们想要按天统计,可以利用 awk提供的字符串截取的能力。
|
||||
|
||||
|
||||
|
||||
上图中,我们使用awk的substr函数,数字2代表从第 2 个字符开始,数字11代表截取 11 个字符。
|
||||
|
||||
接下来我们就可以分组统计每天的日志条数了。
|
||||
|
||||
|
||||
|
||||
上图中,使用sort进行排序,然后使用uniq -c进行统计。你可以看到从 2015 年 5 月 17 号一直到 6 月 4 号的日志,还可以看到每天的 PV 量大概是在 2000~3000 之间。
|
||||
|
||||
第五步:分析 UV
|
||||
|
||||
接下来我们分析 UV。UV(Uniq Visitor),也就是统计访问人数。通常确定用户的身份是一个复杂的事情,但是我们可以用 IP 访问来近似统计 UV。
|
||||
|
||||
|
||||
|
||||
上图中,我们使用 awk 去打印$1也就是第一列,接着sort排序,然后用uniq去重,最后用wc -l查看条数。 这样我们就知道日志文件中一共有2660个 IP,也就是2660个 UV。
|
||||
|
||||
第六步:分组分析 UV
|
||||
|
||||
接下来我们尝试按天分组分析每天的 UV 情况。这个情况比较复杂,需要较多的指令,我们先创建一个叫作sum.sh的bash脚本文件,写入如下内容:
|
||||
|
||||
#!/usr/bin/bash
|
||||
|
||||
awk '{print substr($4, 2, 11) " " $1}' access.log |\
|
||||
|
||||
sort | uniq |\
|
||||
|
||||
awk '{uv[$1]++;next}END{for (ip in uv) print ip, uv[ip]}'
|
||||
|
||||
|
||||
具体分析如下。
|
||||
|
||||
|
||||
文件首部我们使用#!,表示我们将使用后面的/usr/bin/bash执行这个文件。
|
||||
第一次awk我们将第 4 列的日期和第 1 列的ip地址拼接在一起。
|
||||
下面的sort是把整个文件进行一次字典序排序,相当于先根据日期排序,再根据 IP 排序。
|
||||
接下来我们用uniq去重,日期 +IP 相同的行就只保留一个。
|
||||
最后的awk我们再根据第 1 列的时间和第 2 列的 IP 进行统计。
|
||||
|
||||
|
||||
为了理解最后这一行描述,我们先来简单了解下awk的原理。
|
||||
|
||||
awk本身是逐行进行处理的。因此我们的next关键字是提醒awk跳转到下一行输入。 对每一行输入,awk会根据第 1 列的字符串(也就是日期)进行累加。之后的END关键字代表一个触发器,就是 END 后面用 {} 括起来的语句会在所有输入都处理完之后执行——当所有输入都执行完,结果被累加到uv中后,通过foreach遍历uv中所有的key,去打印ip和ip对应的数量。
|
||||
|
||||
编写完上面的脚本之后,我们保存退出编辑器。接着执行chmod +x ./sum.sh,给sum.sh增加执行权限。然后我们可以像下图这样执行,获得结果:
|
||||
|
||||
|
||||
|
||||
如上图,IP地址已经按天进行统计好了。
|
||||
|
||||
总结
|
||||
|
||||
今天我们结合一个简单的实战场景——Web 日志分析与统计练习了之前学过的指令,提高熟练程度。此外,我们还一起学习了新知识——功能强大的awk文本处理语言。在实战中,我们对一个nginx的access_log进行了简单的数据分析,直观地获得了这个网站的访问情况。
|
||||
|
||||
我们在日常的工作中会遇到各种各样的日志,除了 nginx 的日志,还有应用日志、前端日志、监控日志等等。你都可以利用今天学习的方法,去做数据分析,然后从中得出结论。
|
||||
|
||||
|
||||
|
||||
|
157
专栏/重学操作系统-完/12(1)加餐练习题详解(二).md
Normal file
157
专栏/重学操作系统-完/12(1)加餐练习题详解(二).md
Normal file
@ -0,0 +1,157 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 (1)加餐 练习题详解(二)
|
||||
今天我会带你把《模块二:Linux 指令》中涉及的课后练习题,逐一讲解,并给出每个课时练习题的解题思路和答案。
|
||||
|
||||
练习题详解
|
||||
|
||||
06 | 目录结构和文件管理指令:rm / -rf 指令的作用是?
|
||||
|
||||
【问题】 搜索文件系统中所有以包含 std字符串且以.h扩展名结尾的文件。
|
||||
|
||||
【解析】 这道题目比较简单,大家也比较活跃,我自己只写了一种方法,没想到留言中有挺多不错的方案,那我们一起来看下。
|
||||
|
||||
下面是我的方案,你学完模块二的内容后,应该知道查看全部文件需要sudo,以管理员身份:
|
||||
|
||||
sudo find / -name "*std*.h"
|
||||
|
||||
|
||||
我在留言中看到有的同学用的是-iname,这样也是可以的,只是忽略了大小写。
|
||||
|
||||
也可以结合 grep 语句, 用管道实现,比如:
|
||||
|
||||
sudo find / -name "*.h" |grep std
|
||||
|
||||
|
||||
07 | 进程、重定向和管道指令:xargs 指令的作用是?
|
||||
|
||||
【问题】 请问下面这段 Shell 程序的作用是什么?
|
||||
|
||||
mkfifo pipe1
|
||||
|
||||
mkfifo pipe2
|
||||
|
||||
echo -n run | cat - pipe1 > pipe2 &
|
||||
|
||||
cat < pipe2 > pipe1
|
||||
|
||||
|
||||
【解析】 这个题目是我在网上看到的一个比较有趣的问题。
|
||||
|
||||
前 2 行代码创建了两个管道文件。
|
||||
|
||||
从第 3 行开始,代码变得复杂。echo -n run就是向输出流中写入一个run字符串(不带回车,所以用-n)。通过管道,将这个结果传递给了cat。cat是 concatenate 的缩写,意思是把文件粘在一起。
|
||||
|
||||
|
||||
当cat用>重定向输出到一个管道文件时,如果没有其他进程从管道文件中读取内容,cat会阻塞。
|
||||
当cat用<读取一个管道内容时,如果管道中没有输入,也会阻塞。
|
||||
|
||||
|
||||
从这个角度来看,总共有 3 次重定向:
|
||||
|
||||
|
||||
将-也就是输入流的内容和pipe1内容合并重定向到pipe2;
|
||||
将pipe2内容重定向到cat;
|
||||
将cat的内容重定向到pipe1。
|
||||
|
||||
|
||||
仔细观察下路径:pipe1->pipe2->pipe1,构成了一个循环。 这样导致管道pipe1管道pipe2中总是有数据(没有数据的时间太短)。于是,就构成了一个无限循环。我们打开执行这个程序后,可以用htop查看当前的 CPU 使用情况,会发现 CPU 占用率很高。
|
||||
|
||||
08 | 用户和权限管理指令: 请简述 Linux 权限划分的原则?
|
||||
|
||||
【问题】 如果一个目录是只读权限,那么这个目录下面的文件还可写吗?
|
||||
|
||||
【解析】 这类问题,你一定要去尝试,观察现象再得到结果。
|
||||
|
||||
|
||||
|
||||
你可以看到上图中,foo 目录不可读了,下面的foo/bar文件还可以写。 即便它不可写了,下面的foo/bar文件还是可以写。
|
||||
|
||||
|
||||
|
||||
但是想要创建新文件就会出现报错,因为创建新文件也需要改目录文件。这个例子说明 Linux 中的文件内容并没有存在目录中,目录中却有文件清单。
|
||||
|
||||
09 | Linux 中的网络指令:如何查看一个域名有哪些 NS 记录?
|
||||
|
||||
【问题】 如何查看正在 TIME_WAIT 状态的连接数量?
|
||||
|
||||
【解析】 注意,这里有个小坑,就是 netstat 会有两行表头,这两行可以用 tail 过滤掉,下面tail -n +3就是告诉你 tail 从第 3 行开始显示。-a代表显示所有的 socket。
|
||||
|
||||
netstat -a | tail -n +3 | wc -l
|
||||
|
||||
|
||||
10 | 软件的安装: 编译安装和包管理器安装有什么优势和劣势?
|
||||
|
||||
【问题】 如果你在编译安装 MySQL 时,发现找不到libcrypt.so ,应该如何处理?
|
||||
|
||||
【解析】 遇到这类问题,首先应该去查资料。 比如查 StackOverflow,搜索关键词:libcrypt.so not found,或者带上自己的操作系统ubuntu。下图是关于 Stackoverflow 的一个解答:
|
||||
|
||||
|
||||
|
||||
在这里我再多说两句,程序员成长最需要的是学习时间,如果在这前面加一个形容词,那就是大量的学习时间;而程序员最需要掌握的技能就是搜索和学习知识的能力。如果你看到今天的这篇内容,说明已经学完了《重学操作系统》专栏两个模块的知识,希望你可以坚持下去!
|
||||
|
||||
11 | 高级技巧之日志分析:利用 Linux 指令分析 Web 日志
|
||||
|
||||
【问题 1 】 根据今天的 access_log 分析出有哪些终端访问了这个网站,并给出分组统计结果。
|
||||
|
||||
【解析】access_log中有Debian和Ubuntu等等。我们可以利用下面的指令看到,第 12 列是终端,如下图所示:
|
||||
|
||||
|
||||
|
||||
我们还可以使用sort和uniq查看有哪些终端,如下图所示:
|
||||
|
||||
|
||||
|
||||
最后需要写一个脚本,进行统计:
|
||||
|
||||
cat nginx_logs.txt |\
|
||||
|
||||
awk '{tms[$12]++;next}END{for (t in tms) print t, tms[t]}'
|
||||
|
||||
|
||||
结果如下:
|
||||
|
||||
|
||||
|
||||
【问题 2】 根据今天的 access_log 分析出访问量 Top 前三的网页。
|
||||
|
||||
如果不需要 Substring 等复杂的处理,也可以使用sort和uniq的组合。如下图所示:
|
||||
|
||||
|
||||
|
||||
12 | 高级技巧之集群部署:利用 Linux 指令同时在多台机器部署程序
|
||||
|
||||
【问题】~/.bashrc ~/.bash_profile, ~/.profile 和 /etc/profile 的区别是什么?
|
||||
|
||||
【解析】 执行一个 shell 的时候分成login shell和non-login shell。顾名思义我们使用了sudosu切换到某个用户身份执行 shell,也就是login shell。还有 ssh 远程执行指令也是 login shell,也就是伴随登录的意思——login shell 会触发很多文件执行,路径如下:
|
||||
|
||||
|
||||
|
||||
如果以当前用户身份正常执行一个 shell,比如说./a.sh,就是一个non-login的模式。 这时候不会触发上述的完整逻辑。
|
||||
|
||||
另外shell还有另一种分法,就是interactive和non-interactive。interactive 是交互式的意思,当用户打开一个终端命令行工具后,会进入一个输入命令得到结果的交互界面,这个时候,就是interactive shell。
|
||||
|
||||
baserc文件通常只在interactive模式下才会执行,这是因为~/.bashrc文件中通常有这样的语句,如下图所示:
|
||||
|
||||
|
||||
|
||||
这个语句通过$-看到当前shell的执行环境,如下图所示:
|
||||
|
||||
|
||||
|
||||
带 i 字符的就是interactive,没有带i字符就不是。
|
||||
|
||||
因此, 如果你需要通过 ssh 远程 shell 执行一个文件,你就不是在 interactive 模式下,bashrc 不会触发。但是因为登录的原因,login shell 都会触发,也就是说 profile 文件依然会执行。
|
||||
|
||||
总结
|
||||
|
||||
这个模块我们学习了 Linux 指令。我带大家入了个门,也和你一起感受了一次 Linux 指令的博大精深。Linux 虽然没有上下五千年的历史,但每次使用,依然让我感受到了它浓郁的历史气息,悠久的文化传承,自由的创造精神。希望这块知识可以陪伴大家,鼓励你成为优秀的程序员。虽然我们已经学了几十个指令,但还是沧海一粟。后续就需要你多查资料,多用man手册,继续深造了。
|
||||
|
||||
好的,Linux 指令部分就告一段落。下一节课,我们将开启操作系统核心知识学习,请和我一起来学习“模块三:操作系统基础知识”吧。
|
||||
|
||||
|
||||
|
||||
|
308
专栏/重学操作系统-完/12高级技巧之集群部署:利用Linux指令同时在多台机器部署程序.md
Normal file
308
专栏/重学操作系统-完/12高级技巧之集群部署:利用Linux指令同时在多台机器部署程序.md
Normal file
@ -0,0 +1,308 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 高级技巧之集群部署:利用 Linux 指令同时在多台机器部署程序
|
||||
Linux 指令是由很多顶级程序员共同设计的,使用 Linux 指令解决问题的过程,就好像在体验一款优秀的产品。每次通过查资料使用 Linux 指令解决问题后,都会让我感到收获满满。在这个过程中,我不仅学会了一条指令,还从中体会到了软件设计的魅力:彼此独立,又互成一体。这就像每个 Linux 指令一样,专注、高效。回想起来,在我第一次看到管道、第一次使用 awk、第一次使用 sort,都曾有过这种感受。
|
||||
|
||||
通过前面的学习,相信你已经掌握了一些基础指令的使用方法,今天我们继续挑战一个更复杂的问题——用 Linux 指令管理一个集群。这属于 Linux 指令的高级技巧,所谓高级技巧并不是我们要学习更多的指令,而是要把之前所学的指令进行排列组合。当你从最初只能写几条指令、执行然后看结果,成长到具备书写一个拥有几十行、甚至上百行的 bash 脚本的能力时,就意味着你具备了解决复杂问题的能力。而最终的目标,是提升你对指令的熟练程度,锻炼工程能力。
|
||||
|
||||
本课时,我将带你朝着这个目标努力,通过把简单的指令组合起来,分层组织成最终的多个脚本文件,解决一个复杂的工程问题:在成百上千的集群中安装一个 Java 环境。接下来,请你带着这个目标,开启今天的学习。
|
||||
|
||||
第一步:搭建学习用的集群
|
||||
|
||||
第一步我们先搭建一个学习用的集群。这里简化一下模型。我在自己的电脑上装一个ubuntu桌面版的虚拟机,然后再装两个ubuntu服务器版的虚拟机。
|
||||
|
||||
相对于桌面版,服务器版对资源的消耗会少很多。我将教学材料中桌面版的ubuntu命名为u1,两个用来被管理的服务器版ubuntu叫作v1和v2。
|
||||
|
||||
用桌面版的原因是:我喜欢ubuntu漂亮的开源字体,这样会让我在给你准备素材的时候拥有一个好心情。如果你对此感兴趣,可以搜索ubuntu mono,尝试把这个字体安装到自己的文本编辑器中。不过我还是觉得在ubuntu中敲代码更有感觉。
|
||||
|
||||
注意,我在这里只用了 3 台服务器,但是接下来我们要写的脚本是可以在很多台服务器之间复用的。
|
||||
|
||||
第二步:循环遍历 IP 列表
|
||||
|
||||
你可以想象一个局域网中有很多服务器需要管理,它们彼此之间网络互通,我们通过一台主服务器对它们进行操作,即通过u1操作v1和v2。
|
||||
|
||||
在主服务器上我们维护一个ip地址的列表,保存成一个文件,如下图所示:
|
||||
|
||||
|
||||
|
||||
目前iplist中只有两项,但是如果我们有足够的机器,可以在里面放成百上千项。接下来,请你思考shell如何遍历这些ip?
|
||||
|
||||
你可以先尝试实现一个最简单的程序,从文件iplist中读出这些ip并尝试用for循环遍历这些ip,具体程序如下:
|
||||
|
||||
#!/usr/bin/bash
|
||||
|
||||
readarray -t ips < iplist
|
||||
|
||||
for ip in ${ips[@]}
|
||||
|
||||
do
|
||||
|
||||
echo $ip
|
||||
|
||||
done
|
||||
|
||||
|
||||
首行的#!叫作 Shebang。Linux 的程序加载器会分析 Shebang 的内容,决定执行脚本的程序。这里我们希望用bash来执行这段程序,因为我们用到的 readarray 指令是bash 4.0后才增加的能力。
|
||||
|
||||
readarray指令将 iplist 文件中的每一行读取到变量ips中。ips是一个数组,可以用echo ${ips[@]}打印其中全部的内容:@代表取数组中的全部内容;$符号是一个求值符号。不带$的话,ips[@]会被认为是一个字符串,而不是表达式。
|
||||
|
||||
for循环遍历数组中的每个ip地址,echo把地址打印到屏幕上。
|
||||
|
||||
如果用shell执行上面的程序会报错,因为readarray是bash 4.0后支持的能力,因此我们用chomd为foreach.sh增加执行权限,然后直接利用shebang的能力用bash执行,如下图所示:
|
||||
|
||||
|
||||
|
||||
第三步:创建集群管理账户
|
||||
|
||||
为了方便集群管理,通常使用统一的用户名管理集群。这个账号在所有的集群中都需要保持命名一致。比如这个集群账号的名字就叫作lagou。
|
||||
|
||||
接下来我们探索一下如何创建这个账户lagou,如下图所示:
|
||||
|
||||
|
||||
|
||||
上面我们创建了lagou账号,然后把lagou加入sudo分组。这样lagou就有了sudo成为root的能力,如下图所示:
|
||||
|
||||
|
||||
|
||||
接下来,我们设置lagou用户的初始化shell是bash,如下图所示:
|
||||
|
||||
|
||||
|
||||
这个时候如果使用命令su lagou,可以切换到lagou账号,但是你会发现命令行没有了颜色。因此我们可以将原来用户下面的.bashrc文件拷贝到/home/lagou目录下,如下图所示:
|
||||
|
||||
|
||||
|
||||
这样,我们就把一些自己平时用的设置拷贝了过去,包括终端颜色的设置。.bashrc是启动bash的时候会默认执行的一个脚本文件。
|
||||
|
||||
接下来,我们编辑一下/etc/sudoers文件,增加一行lagou ALL=(ALL) NOPASSWD:ALL表示lagou账号 sudo 时可以免去密码输入环节,如下图所示:
|
||||
|
||||
|
||||
|
||||
我们可以把上面的完整过程整理成指令文件,create_lagou.sh:
|
||||
|
||||
sudo useradd -m -d /home/lagou lagou
|
||||
|
||||
sudo passwd lagou
|
||||
|
||||
sudo usermod -G sudo lagou
|
||||
|
||||
sudo usermod --shell /bin/bash lagou
|
||||
|
||||
sudo cp ~/.bashrc /home/lagou/
|
||||
|
||||
sudo chown lagou.lagou /home/lagou/.bashrc
|
||||
|
||||
sduo sh -c 'echo "lagou ALL=(ALL) NOPASSWD:ALL">>/etc/sudoers'
|
||||
|
||||
|
||||
你可以删除用户lagou,并清理/etc/sudoers文件最后一行。用指令userdel lagou删除账户,然后执行create_lagou.sh重新创建回lagou账户。如果发现结果一致,就代表create_lagou.sh功能没有问题。
|
||||
|
||||
最后我们想在v1v2上都执行create_logou.sh这个脚本。但是你不要忘记,我们的目标是让程序在成百上千台机器上传播,因此还需要一个脚本将create_lagou.sh拷贝到需要执行的机器上去。
|
||||
|
||||
这里,可以对foreach.sh稍做修改,然后分发create_lagou.sh文件。
|
||||
|
||||
foreach.sh
|
||||
|
||||
#!/usr/bin/bash
|
||||
|
||||
readarray -t ips < iplist
|
||||
|
||||
for ip in ${ips[@]}
|
||||
|
||||
do
|
||||
|
||||
scp ~/remote/create_lagou.sh ramroll@$ip:~/create_lagou.sh
|
||||
|
||||
done
|
||||
|
||||
|
||||
这里,我们在循环中用scp进行文件拷贝,然后分别去每台机器上执行create_lagou.sh。
|
||||
|
||||
如果你的机器非常多,上述过程会变得非常烦琐。你可以先带着这个问题学习下面的Step 4,然后再返回来重新思考这个问题,当然你也可以远程执行脚本。另外,还有一个叫作sshpass的工具,可以帮你把密码传递给要远程执行的指令,如果你对这块内容感兴趣,可以自己研究下这个工具。
|
||||
|
||||
第四步: 打通集群权限
|
||||
|
||||
接下来我们需要打通从主服务器到v1和v2的权限。当然也可以每次都用ssh输入用户名密码的方式登录,但这并不是长久之计。 如果我们有成百上千台服务器,输入用户名密码就成为一件繁重的工作。
|
||||
|
||||
这时候,你可以考虑利用主服务器的公钥在各个服务器间登录,避免输入密码。接下来我们聊聊具体的操作步骤:
|
||||
|
||||
首先,需要在u1上用ssh-keygen生成一个公私钥对,然后把公钥写入需要管理的每一台机器的authorized_keys文件中。如下图所示:我们使用ssh-keygen在主服务器u1中生成公私钥对。
|
||||
|
||||
|
||||
|
||||
然后使用mkdir -p创建~/.ssh目录,-p的优势是当目录不存在时,才需要创建,且不会报错。~代表当前家目录。 如果文件和目录名前面带有一个.,就代表该文件或目录是一个需要隐藏的文件。平时用ls的时候,并不会查看到该文件,通常这种文件拥有特别的含义,比如~/.ssh目录下是对ssh的配置。
|
||||
|
||||
我们用cd切换到.ssh目录,然后执行ssh-keygen。这样会在~/.ssh目录中生成两个文件,id_rsa.pub公钥文件和is_rsa私钥文件。 如下图所示:
|
||||
|
||||
|
||||
|
||||
可以看到id_rsa.pub文件中是加密的字符串,我们可以把这些字符串拷贝到其他机器对应用户的~/.ssh/authorized_keys文件中,当ssh登录其他机器的时候,就不用重新输入密码了。 这个传播公钥的能力,可以用一个shell脚本执行,这里我用transfer_key.sh实现。
|
||||
|
||||
我们修改一下foreach.sh,并写一个transfer_key.sh配合foreach.sh的工作。transfer_key.sh内容如下:
|
||||
|
||||
foreach.sh
|
||||
|
||||
#!/usr/bin/bash
|
||||
|
||||
readarray -t ips < iplist
|
||||
|
||||
for ip in ${ips[@]}
|
||||
|
||||
do
|
||||
|
||||
sh ./transfer_key.sh $ip
|
||||
|
||||
done
|
||||
|
||||
|
||||
tranfer_key.sh
|
||||
|
||||
ip=$1
|
||||
|
||||
pubkey=$(cat ~/.ssh/id_rsa.pub)
|
||||
|
||||
echo "execute on .. $ip"
|
||||
|
||||
ssh lagou@$ip "
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
|
||||
echo $pubkey >> ~/.ssh/authorized_keys
|
||||
|
||||
chmod 700 ~/.ssh
|
||||
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
|
||||
"
|
||||
|
||||
|
||||
在foreach.sh中我们执行 transfer_key.sh,并且将 IP 地址通过参数传递过去。在 transfer_key.sh 中,用$1读出 IP 地址参数, 再将公钥写入变量pubkey,然后登录到对应的服务器,执行多行指令。用mkdir指令检查.ssh目录,如不存在就创建这个目录。最后我们将公钥追加写入目标机器的~/.ssh/authorized_keys中。
|
||||
|
||||
chmod 700和chmod 600是因为某些特定的linux版本需要.ssh的目录为可读写执行,authorized_keys文件的权限为只可读写。而为了保证安全性,组用户、所有用户都不可以访问这个文件。
|
||||
|
||||
此前,我们执行foreach.sh需要输入两次密码。完成上述操作后,我们再登录这两台服务器就不需要输入密码了。
|
||||
|
||||
|
||||
|
||||
接下来,我们尝试一下免密登录,如下图所示:
|
||||
|
||||
|
||||
|
||||
可以发现,我们登录任何一台机器,都不再需要输入用户名和密码了。
|
||||
|
||||
第五步:单机安装 Java 环境
|
||||
|
||||
在远程部署 Java 环境之前,我们先单机完成以下 Java 环境的安装,用来收集需要执行的脚本。
|
||||
|
||||
在ubuntu上安装java环境可以直接用apt。
|
||||
|
||||
我们通过下面几个步骤脚本配置 Java 环境:
|
||||
|
||||
sudo apt install openjdk-11-jdk
|
||||
|
||||
|
||||
经过一番等待我们已经安装好了java,然后执行下面的脚本确认java安装。
|
||||
|
||||
which java
|
||||
|
||||
java --version
|
||||
|
||||
|
||||
|
||||
|
||||
根据最小权限原则,执行 Java 程序我们考虑再创建一个用户ujava。
|
||||
|
||||
sudo useradd -m -d /opt/ujava ujava
|
||||
|
||||
sudo usermod --shell /bin/bash lagou
|
||||
|
||||
|
||||
这个用户可以不设置密码,因为我们不会真的登录到这个用户下去做任何事情。接下来我们为用户配置 Java 环境变量,如下图所示:
|
||||
|
||||
|
||||
|
||||
通过两次 ls 追查,可以发现java可执行文件软连接到/etc/alternatives/java然后再次软连接到/usr/lib/jvm/java-11-openjdk-amd64下。
|
||||
|
||||
这样我们就可以通过下面的语句设置 JAVA_HOME 环境变量了。
|
||||
|
||||
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/
|
||||
|
||||
|
||||
Linux 的环境变量就好比全局可见的数据,这里我们使用 export 设置JAVA_HOME环境变量的指向。如果你想看所有的环境变量的指向,可以使用env指令。
|
||||
|
||||
|
||||
|
||||
其中有一个环境变量比较重要,就是PATH。
|
||||
|
||||
|
||||
|
||||
如上图,我们可以使用shell查看PATH的值,PATH中用:分割,每一个目录都是linux查找执行文件的目录。当用户在命令行输入一个命令,Linux 就会在PATH中寻找对应的执行文件。
|
||||
|
||||
当然我们不希望JAVA_HOME配置后重启一次电脑就消失,因此可以把这个环境变量加入ujava用户的profile中。这样只要发生用户登录,就有这个环境变量。
|
||||
|
||||
sudo sh -c 'echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/" >> /opt/ujava/.bash_profile'
|
||||
|
||||
|
||||
将JAVA_HOME加入bash_profile,这样后续远程执行java指令时就可以使用JAVA_HOME环境变量了。
|
||||
|
||||
最后,我们将上面所有的指令整理起来,形成一个install_java.sh。
|
||||
|
||||
sudo apt -y install openjdk-11-jdk
|
||||
|
||||
sudo useradd -m -d /opt/ujava ujava
|
||||
|
||||
sudo usermod --shell /bin/bash ujava
|
||||
|
||||
sudo sh -c 'echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/" >> /opt/ujava/.bash_profile'
|
||||
|
||||
|
||||
apt后面增了一个-y是为了让执行过程不弹出确认提示。
|
||||
|
||||
第六步:远程安装 Java 环境
|
||||
|
||||
终于到了远程安装 Java 环境这一步,我们又需要用到foreach.sh。为了避免每次修改,你可以考虑允许foreach.sh带一个文件参数,指定需要远程执行的脚本。
|
||||
|
||||
foreach.sh
|
||||
|
||||
#!/usr/bin/bash
|
||||
|
||||
readarray -t ips < iplist
|
||||
|
||||
script=$1
|
||||
|
||||
for ip in ${ips[@]}
|
||||
|
||||
do
|
||||
|
||||
ssh $ip 'bash -s' < $script
|
||||
|
||||
done
|
||||
|
||||
|
||||
改写后的foreach会读取第一个执行参数作为远程执行的脚本文件。 而bash -s会提示使用标准输入流作为命令的输入;< $script负责将脚本文件内容重定向到远程bash的标准输入流。
|
||||
|
||||
然后我们执行foreach.sh install_java.sh,机器等待 1 分钟左右,在执行结束后,可以用下面这个脚本检测两个机器中的安装情况。
|
||||
|
||||
check.sh
|
||||
|
||||
sudo -u ujava -i /bin/bash -c 'echo $JAVA_HOME'
|
||||
|
||||
sudo -u ujava -i java --version
|
||||
|
||||
|
||||
check.sh中我们切换到ujava用户去检查JAVA_HOME环境变量和 Java 版本。执行的结果如下图所示:
|
||||
|
||||
|
||||
|
||||
总结
|
||||
|
||||
这节课我们所讲的场景是自动化运维的一些皮毛。通过这样的场景练习,我们复习了很多之前学过的 Linux 指令。在尝试用脚本文件构建一个又一个小工具的过程中,可以发现复用很重要。
|
||||
|
||||
在工作中,优秀的工程师,总是善于积累和复用,而shell脚本就是积累和复用的利器。如果你第一次安装java环境,可以把今天的安装脚本保存在自己的笔记本中,下次再安装就能自动化完成了。除了积累和总结,另一个非常重要的就是你要尝试自己去查资料,包括使用man工具熟悉各种指令的使用方法,用搜索引擎查阅资料等。
|
||||
|
||||
|
||||
|
||||
|
141
专栏/重学操作系统-完/13操作系统内核:Linux内核和Windows内核有什么区别?.md
Normal file
141
专栏/重学操作系统-完/13操作系统内核:Linux内核和Windows内核有什么区别?.md
Normal file
@ -0,0 +1,141 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 操作系统内核:Linux 内核和 Windows 内核有什么区别?
|
||||
Windows 和 Linux 是当今两款最主流的服务器操作系统产品,都拥有广泛的用户和信徒。Windows 通过强大的商业运作,驱动了大量优秀人才加盟到它的开发团队中;Linux 通过社区产品的魅力吸引着世界上大量的顶级程序员为它贡献源代码、解答问题。两者在服务器市场上竞争激烈,不分伯仲,但也存在互相扶持的关系。
|
||||
|
||||
我觉得,两个操作系统各有千秋。每次学习两个操作系统的技术知识,都让我切实地感受到编程真的是一门艺术,而学习编程就像是在探索艺术。
|
||||
|
||||
今天我们继续从一道面试题目“ Linux 内核和 Windows 内核有什么区别?”入手,去了解这两个操作系统内核的设计,帮助你学习操作系统中最核心的一个概念——内核,并希望这些知识可以伴随你日后的每个系统设计。
|
||||
|
||||
什么是内核?
|
||||
|
||||
说到操作系统,就必须说内核。内核是操作系统中应用连接硬件设备的桥梁。
|
||||
|
||||
内核的能力
|
||||
|
||||
对于一个现代的操作系统来说,它的内核至少应该提供以下 4 种基本能力:
|
||||
|
||||
|
||||
管理进程、线程(决定哪个进程、线程使用 CPU);
|
||||
管理内存(决定内存用来做什么);
|
||||
连接硬件设备(为进程、和设备间提供通信能力);
|
||||
提供系统调用(接收进程发送来的系统调用)。
|
||||
|
||||
|
||||
操作系统分层
|
||||
|
||||
从上面 4 种能力来看操作系统和内核之间的关系,通常可以把操作系统分成 3 层,最底层的硬件设备抽象、中间的内核和最上层的应用。
|
||||
|
||||
|
||||
|
||||
内核是如何工作的?
|
||||
|
||||
为了帮助你理解什么是内核,请你先思考一个问题:进程和内核的关系,是不是像浏览器请求服务端服务?你可以先自己思考,然后在留言区写下你此时此刻对这个问题的认知,等学完“模块三”再反过头来回顾这个知识,相信你定会产生新的理解。
|
||||
|
||||
接下来,我们先一起分析一下这个问题。
|
||||
|
||||
内核权限非常高,它可以管理进程、可以直接访问所有的内存,因此确实需要和进程之间有一定的隔离。这个隔离用类似请求/响应的模型,非常符合常理。
|
||||
|
||||
|
||||
|
||||
但不同的是在浏览器、服务端模型中,浏览器和服务端是用不同的机器在执行,因此不需要共享一个 CPU。但是在进程调用内核的过程中,这里是存在资源共享的。
|
||||
|
||||
|
||||
比如,一个机器有 4 个 CPU,不可能让内核用一个 CPU,其他进程用剩下的 CPU。这样太浪费资源了。
|
||||
再比如,进程向内核请求 100M 的内存,内核把 100M 的数据传回去。 这个模型不可行,因为传输太慢了。
|
||||
|
||||
|
||||
所以,这里多数操作系统的设计都遵循一个原则:进程向内核发起一个请求,然后将 CPU 执行权限让出给内核。内核接手 CPU 执行权限,然后完成请求,再转让出 CPU 执行权限给调用进程。
|
||||
|
||||
关于这块知识,我们会在“ 14 |户态和内核态:用户态线程和内核态线程有什么区别?”中详细讨论。
|
||||
|
||||
Linux 的设计
|
||||
|
||||
Linux 操作系统第一版是1991 年林纳斯托·瓦兹(一个芬兰的小伙子,当时 22 岁)用 C 语音写的。 写完之后他在网络上发布了 Linux 内核的源代码。又经过了 3 年的努力,在 1994 年发布了完整的核心 Version 1.0。
|
||||
|
||||
说到 Linux 内核设计,这里有很多有意思的名词。大多数听起来复杂、专业,但是理解起来其实很简单。接下来我们一一讨论。
|
||||
|
||||
|
||||
Multitask and SMP(Symmetric multiprocessing)
|
||||
|
||||
|
||||
MultiTask 指多任务,Linux 是一个多任务的操作系统。多任务就是多个任务可以同时执行,这里的“同时”并不是要求并发,而是在一段时间内可以执行多个任务。当然 Linux 支持并发。
|
||||
|
||||
SMP 指对称多处理。其实是说 Linux 下每个处理器的地位是相等的,内存对多个处理器来说是共享的,每个处理器都可以访问完整的内存和硬件资源。 这个特点决定了在 Linux 上不会存在一个特定的处理器处理用户程序或者内核程序,它们可以被分配到任何一个处理器上执行。
|
||||
|
||||
|
||||
ELF(Executable and Linkable Format)
|
||||
|
||||
|
||||
|
||||
|
||||
这个名词翻译过来叫作可执行文件链接格式。这是一种从 Unix 继承而来的可执行文件的存储格式。我们可以看到 ELF 中把文件分成了一个个分段(Segment),每个段都有自己的作用。如果想要深入了解这块知识,会涉及部分编译原理的知识,如果你感兴趣可以去网上多查些资料或者去留言区我们一起讨论。
|
||||
|
||||
|
||||
Monolithic Kernel
|
||||
|
||||
|
||||
这个名词翻译过来就是宏内核,宏内核反义词就是 Microkernel ,微内核的意思。Linux 是宏内核架构,这说明 Linux 的内核是一个完整的可执行程序,且内核用最高权限来运行。宏内核的特点就是有很多程序会打包在内核中,比如,文件系统、驱动、内存管理等。当然这并不是说,每次安装驱动都需要重新编译内核,现在 Linux 也可以动态加载内核模块。所以哪些模块在内核层,哪些模块在用户层,这是一种系统层的拆分,并不是很强的物理隔离。
|
||||
|
||||
与宏内核对应,接下来说说微内核,内核只保留最基本的能力。比如进程调度、虚拟内存、中断。多数应用,甚至包括驱动程序、文件系统,是在用户空间管理的。
|
||||
|
||||
|
||||
|
||||
学到这里,你可能会问:在内核层和在用户层有什么区别吗?
|
||||
|
||||
感觉分层其实差不多。 我这里说一个很大的区别,比如说驱动程序是需要频繁调用底层能力的,如果在内核中,性能肯定会好很多。对于微内核设计,驱动在内核外,驱动和硬件设备交互就需要频繁做内核态的切换。
|
||||
|
||||
当然微内核也有它的好处,比如说微内核体积更小、可移植性更强。不过我认为,随着计算能力、存储技术越来越发达,体积小、安装快已经不能算是一个很大的优势了。现在更重要的是如何有效利用硬件设备的性能。
|
||||
|
||||
之所以这么思考,也可能因为我是带着现代的目光回望当时人们对内核的评判,事实上,当时 Linux 团队也因此争论过很长一段时间。 但是我觉得历史往往是螺旋上升的,说不定将来性能发展到了一个新的阶段,像微内核的灵活性、可以提供强大的抽象能力这样的特点,又重新受到人们的重视。
|
||||
|
||||
还有一种就是混合类型内核。 混合类型的特点就是架构像微内核,内核中会有一个最小版本的内核,其他功能会在这个能力上搭建。但是实现的时候,是用宏内核的方式实现的,就是内核被做成了一个完整的程序,大部分功能都包含在内核中。就是在宏内核之内有抽象出了一个微内核。
|
||||
|
||||
上面我们大体介绍了内核几个重要的特性,有关进程、内存、虚拟化等特性,我们会在后面几个模块中逐步讨论。
|
||||
|
||||
Window 设计
|
||||
|
||||
接下来我们说说 Windows 的设计,Windows 和 Linux 的设计有很大程度的相似性。Windows也有内核,它的内核是 C/C++ 写的。准确地说,Windows 有两个内核版本。一个是早期的Windows 9x 内核,早期的 Win95, Win98 都是这个内核。我们今天用的 Windows 7, Windows 10 是另一个内核,叫作 Windows NT。NT 指的是 New Technology。接下来我们讨论的都是 NT 版本的内核。
|
||||
|
||||
下面我找到一张 Windows 内核架构的图片给你一个直观感受。
|
||||
|
||||
|
||||
|
||||
Windows 同样支持 Multitask 和 SMP(对称多处理)。Windows 的内核设计属于混合类型。你可以看到内核中有一个 Microkernel 模块。而整个内核实现又像宏内核一样,含有的能力非常多,是一个完整的整体。
|
||||
|
||||
Windows 下也有自己的可执行文件格式,这个格式叫作 Portable Executable(PE),也就是可移植执行文件,扩展名通常是.exe、.dll、.sys等。
|
||||
|
||||
PE 文件的结构和 ELF 结构有很多相通的地方,我找到了一张图片帮助你更直观地理解。 因为这部分知识涉及编译原理,我这里就不详细介绍了,感兴趣同学可以在留言区和大家一起讨论,或者查阅更多资料。
|
||||
|
||||
|
||||
|
||||
Windows 还有很多独特的能力,比如 Hyper-V 虚拟化技术,有关虚拟化技术我们将在“模块八:虚拟化和其他”中详细讲解。
|
||||
|
||||
总结
|
||||
|
||||
这一讲我们学习了内核的基础知识,包括内核的作用、整体架构以及 3 种内核类型(宏内核、微内核和混合类型内核)。内核很小(微内核)方便移植,因为体积小、安装快;内核大(宏内核),方便优化性能,毕竟内核更了解计算机中的资源。我们还学习了操作系统对执行文件的抽象,但是没有很深入讨论,内核部分有很多知识是需要在后面的几个模块中体现的,比如进程、文件、内存相关的能力等。
|
||||
|
||||
那么通过这一讲的学习,你现在可以来回答本节关联的面试题目:Linux 内核和 Windows 内核有什么区别?
|
||||
|
||||
老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。
|
||||
|
||||
【解析】 Windows 有两个内核,最新的是 NT 内核,目前主流的 Windows 产品都是 NT 内核。NT 内核和 Linux 内核非常相似,没有太大的结构化差异。
|
||||
|
||||
从整体设计上来看,Linux 是宏内核,NT 内核属于混合型内核。和微内核不同,宏内核和混合类型内核从实现上来看是一个完整的程序。只不过混合类型内核内部也抽象出了微内核的概念,从内核内部看混合型内核的架构更像微内核。
|
||||
|
||||
另外 NT 内核和 Linux 内核还存在着许多其他的差异,比如:
|
||||
|
||||
|
||||
Linux 内核是一个开源的内核;
|
||||
它们支持的可执行文件格式不同;
|
||||
它们用到的虚拟化技术不同。
|
||||
|
||||
|
||||
关于这块知识就不展开说了, 我们会在后续的“进程、内存、虚拟化”等模块中仔细讨论。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user