prompt生成网站
文件打开和关闭过程
当用户进程想打开文件,就像向操作系统申请通行证,通过 open() 系统调用,提供文件名和打开模式。
内核拿到请求,首先得验证权限,就像门卫一样,检查进程是否有权访问,比如用户ID和组ID是否匹配,权限位是否允许等。 没权限就返回错误,直接拒绝。
权限过了,内核就要在文件系统里找对应的 inode,就像在档案室里找文件。 如果文件不存在,但打开模式允许创建,就创建一个新的 inode。
找到 inode 后,内核会在进程的文件描述符表中找个空位,文件描述符就像是借阅卡,每个进程都有自己的卡。
然后,内核会创建一个文件对象,也叫文件句柄,维护文件的状态信息,比如读写位置。文件对象是系统级的,可以被多个进程共享。
最后,内核建立连接,将文件描述符指向文件对象,文件对象指向 inode。这样,进程就可以通过文件描述符访问文件了。 open() 成功后,返回文件描述符给用户进程,进程就能用它读写文件了。
关闭文件也很有意思。进程调用 close(),内核首先验证文件描述符是否有效。
有效的话,内核把它从进程的文件描述符表中移除,这样这个描述符就能被下次打开文件重用了。同时,内核减少文件对象的引用计数。因为文件对象可能被多个进程共享,只有当引用计数为零时,才会真正释放。
释放文件对象时,内核会把文件缓冲区的数据写回磁盘,保证数据不丢失,然后释放文件对象占用的内存。 如果这是最后一个指向 inode 的链接,且文件被删除了,内核还会释放 inode 和文件数据块占用的空间。
总的来说,文件的打开和关闭过程保证了文件访问的安全性、效率以及资源的合理利用。
顺序写为什么这么快?
顺序写快的原因有很多。
最重要的一个原因则是寻址问题,也就是要找到写入的磁盘空间,而后将磁头移动到对应的位置。很显然,随机写是每一次写入都要重新寻址,而顺序写则是找到一个位置之后就可以连绵不绝写下去。
除了这个最根源的原因以外,还有两个原因:一个是充分利用写缓冲,这也是局部性原理的一个体现。另外一个则是现代的文件系统会有意识地将偏向并且优化顺序写的性能。
简述
随机写寻址慢,局部性差,文件系统支持不友好
引导
局部性原理
当然,当下广泛使用的 SSD 也有类似的特性,但是原理上有些区别。这其中比较大的一个差异是虽然 SSD 也要寻址,但是没有机械硬盘那么慢,也不需要挪动磁头。
SSD 顺序写快的原因主要是局部性原理的应用,这源自两方面,一个是 SSD 写入是以页为单位的,也就是你写 1B 还是写入 1KB,都是按照页来写入的。另外一个是 SSD 同样会有缓存,顺序写也能更加好的利用这些缓存。
交换区
交换区是硬盘上的一块特殊区域,用于存储那些当前不使用的内存页,这样可以在物理内存紧张时,将这些页换出到硬盘上,从而释放物理内存供其他进程使用。
交换区的作用是扩展系统的可用内存空间,它允许操作系统在物理内存不足时,将不常用的内存页换出到硬盘上,这样就可以在不增加物理内存的情况下,支持更多的进程运行,提高内存的使用效率。
所以,显而易见使用交换区会导致性能变差。因此在性能优化里面,一个常见的措施就是尽可能减少交换区的使用。举个例子来说,在使用 Kafka 之类的中间件的时候,我们会将它的最大内存设置为不大于物理内存。一般都是让中间件使用的内存加上操作系统占用的内存,不大于物理内存。
这样可以确保很少触发换入换出,也就是避免使用交换区。
当然,类似的另外一个手段是调整 Linux 下的 vm.swapness 的值。例如说调整到 1,也就是尽可能规避使用交换区。
这两种手段是可以混合使用的。例如说在 Kafka 的服务器上,同时限制住 Kafka 的堆大小,以及 Kafka 所在 Linux 系统的 vm.swapness 参数到一个极小的值,例如说 1。
页面置换算法
简单的说就是物理内存页淘汰算法
它其实就是我们在虚拟内存里面提到的,当物理内存不够的时候,要将一部分物理页的内容写到交换区中。页面替换算法就是用来计算,究竟哪些页应该写到交换区上。
有很多种算法,每种算法都有自己的特色。
第一种是先进先出(FIFO)算法:这是最简单的页面替换算法。它基于“先进先出”的原则,即最早进入内存的页面将首先被替换。这种算法易于实现,但可能不适合实际的工作负载,因为它不考虑页面的使用频率。
第二种是最近最少使用(LRU)算法:LRU算法认为过去一段时间内最少被使用的页面,在未来的使用概率也相对较低。因此,当需要替换页面时,它会选择最长时间未被使用的页面进行替换。
第三种是最久未使用(LFU)算法:LFU算法是基于页面访问频率来替换页面的。它替换掉访问次数最少的页面,认为这些页面在将来可能也不常被使用。
第四种是最优(OPT)算法:这是一种理想化的算法,但在实际中很难实现。OPT算法在每次页面请求时都会选择将来最长时间内不会被访问的页面进行替换,因此它也被称为“未来导向”的算法。
第五种是时钟(Clock)算法:这是一种简单并且实用的近似算法,用来模拟OPT算法。它通过一个循环的列表来跟踪页面,给每个页面一个“访问位”。当需要替换时,它会检查访问位,如果未被访问过,则替换这个页面;如果已被访问,则重新标记并继续。
第六种是第二次机会(SCR)算法:这是对FIFO算法的一种改进。在FIFO的基础上,每个页面都有一个引用位。如果页面被访问,则设置引用位。在替换页面时,如果页面的引用位是0,则替换;如果是1,则将其置为0并给它“第二次机会”。
第七种是老化(Aging)算法:用于模拟LFU算法,但避免了LFU算法中可能出现的页面饥饿问题。它通过一组位来表示页面的使用情况,并定期右移这些位,以减少旧的使用记录的影响。
第八种是WSClock算法:结合了LRU和Clock算法的特点,使用一个钟表算法的列表来选择可能的页面替换候选,然后检查这些页面的引用位来决定是否替换。
Linux内核使用了多种页面替换算法的组合,主要是基于LRU的变种,同时考虑了文件页面和匿名页面的不同特性。它不是一个固定的算法,而是根据系统负载和内存使用模式动态调整的策略。随着内核版本的更新,页面替换算法也在不断地得到改进和优化。
进一步来说,LRU 算法虽然简单,但是 LRU 其实深刻反应了计算机的时间局部性和空间局部性,所以在计算机里面应用非常广泛。最典型的就是缓存淘汰算法,比如说本地缓存已经满了,但是还需要继续放入内容,那么就需要淘汰一部分缓存,以腾出空间。
临界区
临界区是指一个访问共享资源(如变量、数据结构、文件等)的程序片段,在这个片段中,多个进程或线程不能同时执行,否则可能会导致数据不一致或竞态条件。
具体来说,临界区有四个特点。
第一个是互斥性,即同一时间只能有一个进程或线程进入临界区,其他进程或线程必须等待;
第二个是有限等待,进程或线程在有限时间内能够进入临界区,不会无限期等待;
第三个是让权等待,即如果进程或线程不能立即进入临界区,它应该释放CPU,让其他进程或线程运行。
第三个是空闲让进,即如果没有进程或线程在临界区中执行,那么请求进入临界区的进程或线程应该被允许进入。
和临界区这个概念紧密联系的就是并发编程了,比如说可以站在并发编程的角度重新看这四个特性。
互斥性其实不是必须满足的特性。比如说读写锁就没有严格遵循互斥性,读锁本身是允许多个线程加锁的。
而有限等待更多体现为超时控制。最为典型的例子就是在使用并发队列的时候,入队出队都可以增加超时控制,如果要是在时限内都没有操作成功,则返回错误。
让权等待则是体现为如果要是没有拿到锁之类的,就会阻塞,从而让出了 CPU。当然有一些场景下为了优化性能,会引入自旋机制,看看能不能在自旋的时候就获得锁,或者操作成功。
空闲让进则意味着线程或者协程的调度机制,必须要在锁让出的时候,唤醒阻塞的线程或者协程,进一步执行。
进程调度
从理论上来说,有很多种调度策略。
第一种是先来先服务,也就是按照到达就绪队列的顺序来调度。优点是简单易实现,缺点就是就绪队列尾部的进程可能会出现饥饿。
第二种是短作业优先。也就是优先调度预计执行时间最短的。优点是可以减少平均等待时间,但是缺点是长作业会饥饿。
第三种是优先级调度,也就是优先级高的先调度,显然缺点是优先级低的任务可能会饥饿。
第四种是时间片轮转,也就是说每个进程轮流运行一段时间,到点之后不管有没有结束,都要让出 CPU,显然这种算法公平性比较好。
第五种是多级反馈队列。简单来说就是分成多个队列,每个队列代表一个优先级。操作系统会动态调整进程的优先级,保证进程都能得到调度。这个算法是一个综合性的算法,综合考虑了非常多的因素,所以总体来说调度效率和公平性都比较好。缺点就是实现会比较复杂。
第六种是最短时间优先,也就是优先调度剩余执行时间最短的任务。它和短作业优先的区别是,短作业优先考虑的是整个任务的执行时间,而这个算法考虑的是剩余执行时间。
第七种是保证公平调度,也就是每个用户或者用户组的 CPU 时间是相同的。
第八种事基于需求调度,也就是根据进程需要的资源来执行调度。
大多数操作系统并不会使用单一的调度策略,而是多种策略混合使用。
比如说 Linux 使用的就是所谓的 CFS,完全公平调度策略。它的核心在于确保每个进程都能公平地分享CPU时间。它通过一个叫做虚拟运行时间的东西来决定哪个进程该运行,并且用一个红黑树来管理这些进程。重要的是,它能够动态地调整每个进程的运行时间,确保系统既高效又公平。
简而言之,CFS让每个进程都有机会得到CPU的运行时间,而且还能根据实际情况灵活调整。
CDN
CDN(Content Delivery Network),也就是内容分发网络,其实就是在全球不同地区部署了大量“边缘节点”服务器,把网站上的静态资源(比如图片、视频、CSS、JavaScript 等)预先缓存起来。这样,当用户访问这些内容时,就能就近从最接近的节点获得资源,无须每次都回到源站取数据,整个访问过程会变得又快又稳定。
在具体运作上,用户在访问网站时,首先会通过 DNS 解析域名,CDN 的智能 DNS 根据用户 IP 返回最近的节点地址。
然后,如果该节点本身已经缓存了用户需要的文件,那就会直接把这些文件提供给用户。要是没有缓存,就会去源站拉取资源,保存到节点里,再把文件返回给用户。等到下次访问时,同一资源就可以直接从缓存里取,进一步提升访问速度。
有些 CDN 还会采用负载均衡,把不同的请求分配给不同的服务器,这样能避免个别节点负载过高而导致访问变慢。要进一步提速,CDN 系统往往会做包括智能路由(选择更高效的网络路径)、压缩和优化(例如开启 GZIP 或针对图片做无损压缩)等额外处理,通过尽可能减小数据的体积,或者避开网络访问中的拥堵区,让传输效率提升到更高的水平。除此之外,CDN 常常也会提供一定的安全防护功能,例如抵御 DDoS 攻击、提供 Web 应用防火墙(WAF)等,这样不仅能保证性能,还能守护源站不被恶意攻击击破。
总的来说,CDN通过地理分布的边缘节点、缓存机制和智能调度,把网站和应用的内容有效地送到全球用户手里,既能缩短加载时间,又能抵御突发的大流量冲击,对业务的稳定性和用户体验都有显著的提升效果。
粘包拆包
粘包的意思是,发送方发送的多个数据包在接收方被合并成了一个数据包。比如,发送方分别发送了两条消息“Hello”和“World”,但接收方可能一次性收到“HelloWorld”。而拆包则是相反的情况,发送方发送了一条完整的消息,比如“HelloWorld”,但接收方可能分两次接收到“Hello”和“World”。这两种情况都会导致接收方无法正确解析数据。
TCP 的粘包和拆包问题其实并不是 TCP 本身的缺陷,而是由它的传输特性和应用层协议的交互方式共同导致的。粘包和拆包的产生原因主要有以下几个方面。
首先,TCP 是一个面向字节流的协议,它只负责把数据可靠地传输到对方,但并不关心数据的边界。数据在传输过程中,TCP 会根据网络状况和缓冲区大小动态调整数据包的大小,这就可能导致粘包或拆包。
其次,TCP 的 Nagle 算法也会合并多个小数据包以提高传输效率,这也是粘包的一个常见原因。
还有一种情况是接收方处理数据的速度跟不上发送方的发送速度,导致多个数据包堆积在缓冲区里,一起被读取。
拆包的原因则更多是因为数据包的大小受限,比如 TCP 的 MSS(最大分段大小)或者网络的 MTU(最大传输单元)。如果发送的数据包超过了这些限制,就会被拆分成多个小包传输。
要解决粘包和拆包问题,关键在于在应用层定义清晰的数据边界。常见的解决方法有以下几种:
第一种是消息定长。也就是说,每条消息的长度是固定的,接收方只需要按照固定的长度读取数据就可以了。这种方法实现起来很简单,解析速度也很快,但缺点是灵活性差,无法适应不同长度的消息。
第二种方法是添加分隔符。在每条消息的末尾加一个特定的分隔符,比如换行符 \n 或者空字符 \0,接收方通过识别分隔符来区分消息。这种方法比较灵活,适合不同长度的消息,但需要确保分隔符不会出现在消息内容中,或者对消息内容进行转义处理。
第三种方法是“消息头 + 消息体”。在每条消息的开头加一个固定长度的字段,用来表示消息的总长度。接收方先读取消息头,知道消息体的长度后,再根据这个长度读取完整的消息。这种方法既灵活又高效,能够准确地确定消息边界,但需要设计好消息头的格式和解析逻辑。
总的来说,粘包和拆包是 TCP 编程中非常常见的问题。为了让应用层能够正确解析消息,必须在应用层设计合适的协议机制,比如固定长度、分隔符或者长度字段的方式。具体选择哪种方法,还是要根据实际的应用场景和需求来决定。
这种“消息头+消息体”的协议设计模式方式非常常见,比如我之前参与的DBproxy项目中,需要对接MySQL协议。而MySQL协议大体上也遵循这个协议模式。