<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Zorro's Linux Book</title><link>/</link><description>Recent content in Home on Zorro's Linux Book</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 01 Jun 2024 00:00:00 +0800</lastBuildDate><atom:link href="/index.xml" rel="self" type="application/rss+xml"/><item><title>Ceph存储性能优化实战</title><link>/posts/034-ceph_storage_performance_tuning/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/034-ceph_storage_performance_tuning/</guid><description>Ceph 存储性能优化实战 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博ID：orroz
微信公众号：Linux系统技术
前言 本文记录了一次在云环境中为某客户进行 Ceph 存储性能优化的实战过程。测试场景涵盖 Rados 和 JuiceFS 两大场景，优化涉及 CPU、网络、虚拟化等多个层面。希望这些优化经验可以为类似场景提供参考。
测试用例一：Rados 场景 主要对标数据（包括优化前后的数据）：
Rados bench 块大小 操作类型 优化后 优化前 基准性能 1M测试 顺序读 56784.87 MB/s 30489.95 MB/s 48044.32 MB/s 顺序写 18832.97 MB/s 13661.08 MB/s 21062.63 MB/s 256K测试 顺序读 50299.99 MB/s 29633.50 MB/s 48044.32 MB/s 顺序写 18440.75 MB/s 12107.61 MB/s 20083.13 MB/s 测试集群为 3 台大规格云服务器作为 Ceph 存储集群，3 台作为 Rados 客户端集群。开始发压后，读操作是 Rados 客户端收包，Ceph 发包。写操作是 Rados 客户端发包，Ceph 因为三台数据要保持强一致性同步，所以既要收包也要发包。</description></item><item><title>Cgroup - Linux的IO资源隔离</title><link>/posts/002-cgroup-linuxde-io-zi-yuan-ge-li/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/002-cgroup-linuxde-io-zi-yuan-ge-li/</guid><description>Cgroup - Linux的IO资源隔离 Hi，我是Zorro。这是我的微博地址和我的博客地址，我会不定期在这里更新文章，如果你有兴趣，可以来关注我呦。
另外，我的其他联系方式：
Email: mini.jerry@gmail.com
QQ: 30007147
本文PDF
今天我们来谈谈：
Linux的IO隔离 跟内存管理那部分复杂度类似，IO的资源隔离要讲清楚也是比较麻烦的。这部分内容都是这样，配置起来简单，但是要理解清楚确没那么简单。这次是跟Linux内核的IO实现有关系。对于IO的速度限制，实现思路跟CPU和内存都不一样。CPU是针对进程占用时间的比例限制，内存是空间限制，而当我们讨论IO资源隔离的时候，实际上有两个资源需要考虑，一个是空间，另一个是速度。对于空间来说，这个很简单，大不了分区就是了。现实手段中，分区、LVM、磁盘配额、目录配额等等，不同的分区管理方式，不同的文件系统都给出了很多不同的解决方案。所以，空间的限制实际上不是cgroup要解决的问题，那就是说，我们在这里要解决的问题是：如何进行IO数据传输的速度限制。
限速这件事情，现实中有很多模型、算法去解决这个问题。比如，如果我们想控制高速公路上的汽车单位时间通过率，就让收费站每隔固定时间周期只允许通过固定个数的车就好了。这是一种非常有效的控制手段－－漏斗算法。现实中这种算法可能在特定情况下会造成资源浪费以及用户的体验不好，于是又演化出令牌桶算法。这里我们不去详细分析这些算法，但是我们要知道，对io的限速基本是一个漏斗算法的限速效果。无论如何，这种限速都要有个“收费站”这样的设施来执行限速，那么对于Linux的IO体系来说，这个”收费站”建在哪里呢？于是我们就必须先来了解一下：
Linux的IO体系 Linux的IO体系是个层级还算清晰的结构，它基本上分成了如图示这样几层：
Linux的IO体系层次结构
我们可以通过追踪一个read()系统调用来一窥这些层次的结构，当read()系统调用发生，内核首先会通过汇编指令引发一个软中断，然后根据中断传入的参数查询系统调用影射表，找到read()对应的内核调用方法名，并去执行相关调用，这个系统调用名一般情况下就是sys_read()。从此，便开始了调用在内核中处理的过程的第一步：
VFS层：虚拟文件系统层。由于内核要跟多种文件系统打交道，而每一种文件系统所实现的数据结构和相关方法都可能不尽相同，所以，内核抽象了这一层，专门用来适配各种文件系统，并对外提供统一操作接口。 文件系统层：不同的文件系统实现自己的操作过程，提供自己特有的特征，具体不多说了，大家愿意的话自己去看代码即可。 页缓存层：我们的老朋友了，如果不了解缓存是什么的，可以先来看看Linux内存资源管理部分。 通用块层：由于绝大多数情况的io操作是跟块设备打交道，所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备，对上提供统一的Block IO请求标准。 IO调度层：因为绝大多数的块设备都是类似磁盘这样的设备，所以有必要根据这类设备的特点以及应用的不同特点来设置一些不同的调度算法和队列。以便在不同的应用环境下有针对性的提高磁盘的读写效率，这里就是大名鼎鼎的Linux电梯所起作用的地方。针对机械硬盘的各种调度方法就是在这实现的。 块设备驱动层：驱动层对外提供相对比较高级的设备操作接口，往往是C语言的，而下层对接设备本身的操作方法和规范。 块设备层：这层就是具体的物理设备了，定义了各种真对设备操作方法和规范。 根据这几层的特点，如果你是设计者，你会在哪里实现真对块设备的限速策略呢？6、7都是相关具体设备的，如果在这个层次提供，那就不是内核全局的功能，而是某些设备自己的feture。文件系统层也可以实现，但是如果要全局实现也是不可能的，需要每种文件系统中都实现一遍，成本太高。所以，可以实现限速的地方比较合适的是VFS、缓存层、通用块层和IO调度层。而VFS和page cache这样的机制并不是面向块设备设计的，都是做其他事情用的，虽然也在io体系中，但是并不适合用来做block io的限速。所以这几层中，最适合并且成本最低就可以实现的地方就是IO调度层和通用块层。IO调度层本身已经有队列了，我们只要在队列里面实现一个限速机制即可，但是在IO调度层实现的限速会因为不同调度算法的侧重点不一样而有很多局限性，从通用块层实现的限速，原则上就可以对几乎所有的块设备进行带宽和iops的限制。截止目前（4.3.3内核），IO限速主要实现在这两层中。
根据IO调度层和通用块层的特点，这两层分别实现了两种不同策略的IO控制策略，也是目前blkio子系统提供的两种控制策略，一个是权重比例方式的控制，另一个是针对IO带宽和IOPS的控制。
IO调度层 我们需要先来认识一下IO调度层。这一层要解决的核心问题是，如何提高块设备IO的整体性能？这一层也主要是针对用途最广泛的机械硬盘结构而设计的。众所周知，机械硬盘的存储介质是磁介质，并且是盘状，用磁头在盘片上移动进行数据的寻址，这类似播放一张唱片。这种结构的特点是，顺序的数据读写效率比较理想，但是如果一旦对盘片有随机读写，那么大量的时间都会浪费在磁头的移动上，这时候就会导致每次IO的响应时间很长，极大的降低IO的响应速度。磁头在盘片上寻道的操作，类似电梯调度，如果在寻道的过程中，能把路过的相关磁道的数据请求都“顺便”处理掉，那么就可以在比较小影响响应速度的前提下，提高整体IO的吞吐量。所以，一个好的IO调度算法的需求就此产生。在最开始的阶段，Linux就把这个算法命名为Linux电梯算法。目前在内核中默认开启了三种算法，其实严格算应该是两种，因为第一种叫做noop，就是空操作调度算法，也就是没有任何调度操作，并不对io请求进行排序，仅仅做适当的io合并的一个fifo队列。
目前内核中默认的调度算法应该是cfq，叫做完全公平队列调度。这个调度算法人如其名，它试图给所有进程提供一个完全公平的IO操作环境。它为每个进程创建一个同步IO调度队列，并默认以时间片和请求数限定的方式分配IO资源，以此保证每个进程的IO资源占用是公平的，cfq还实现了针对进程级别的优先级调度，这里我们不去细节解释。我们在此只需要知道，既然时间片分好了，优先级实现了，那么cfq肯定是实现进程级别的权重比例分配的最好方案。内核就是这么做的，cgroup blkio的权重比例限制就是基于cfq调度器实现的。如果你要使用权重比例分配，请先确定对应的块设备的IO调度算法是cfq。
查看和修改的方法是：
[zorro@zorrozou-pc0 ~]$ cat /sys/block/sda/queue/scheduler noop deadline [cfq] [zorro@zorrozou-pc0 ~]$ echo cfq &amp;gt; /sys/block/sda/queue/scheduler cfq是通用服务器比较好的IO调度算法选择，对桌面用户也是比较好的选择。但是对于很多IO压力较大的场景就并不是很适应，尤其是IO压力集中在某些进程上的场景。因为这种场景我们需要更多的满足某个或者某几个进程的IO响应速度，而不是让所有的进程公平的使用IO，比如数据库应用。
deadline调度（最终期限调度）就是更适应这样的场景的解决方案。deadline实现了四个队列，其中两个分别处理正常read和write，按扇区号排序，进行正常io的合并处理以提高吞吐量.因为IO请求可能会集中在某些磁盘位置，这样会导致新来的请求一直被合并，于是可能会有其他磁盘位置的io请求被饿死。于是实现了另外两个处理超时read和write的队列，按请求创建时间排序，如果有超时的请求出现，就放进这两个队列，调度算法保证超时（达到最终期限时间）的队列中的请求会优先被处理，防止请求被饿死。由于deadline的特点，无疑在这里无法区分进程，也就不能实现针对进程的io资源控制。
其实不久前，内核还是默认标配四种算法，还有一种叫做as的算法（Anticipatory scheduler），预测调度算法。一个高大上的名字，搞得我一度认为Linux内核都会算命了。结果发现，无非是在基于deadline算法做io调度的之前等一小会时间，如果这段时间内有可以合并的io请求到来，就可以合并处理，提高deadline调度的在顺序读写情况下的数据吞吐量。其实这根本不是啥预测，我觉得不如叫撞大运调度算法。估计结果是不仅没有提高吞吐量，还降低了响应速度，所以内核干脆把它从默认配置里删除了。毕竟Linux的宗旨是实用。
根据以上几种io调度算法的简单分析，我们也能对各种调度算法的使用场景有一些大致的思路了。从原理上看，cfq是一种比较通用的调度算法，是一种以进程为出发点考虑的调度算法，保证大家尽量公平。deadline是一种以提高机械硬盘吞吐量为思考出发点的调度算法，只有当有io请求达到最终期限的时候才进行调度，非常适合业务比较单一并且IO压力比较重的业务，比如数据库。而noop呢？其实如果我们把我们的思考对象拓展到固态硬盘，那么你就会发现，无论cfq还是deadline，都是针对机械硬盘的结构进行的队列算法调整，而这种调整对于固态硬盘来说，完全没有意义。对于固态硬盘来说，IO调度算法越复杂，效率就越低，因为额外要处理的逻辑越多。所以，固态硬盘这种场景下，使用noop是最好的，deadline次之，而cfq由于复杂度的原因，无疑效率最低。但是，如果你想对你的固态硬盘做基于权重比例的IO限速的话，那就没啥办法了，毕竟这时候，效率并不是你的需求，要不你限速干嘛？
通用块设备层 这层的作用我这里就不再复述了，本节其实主要想解释的是，既然这里实现了对blkio的带宽和iops的速度限制，那么有没有什么需要注意的地方？这自然是有的。首先我们还是要先来搞清楚IO中的几个概念。
一般IO：
一个正常的文件io，需要经过vfs -&amp;gt; buffer\page cache -&amp;gt; 文件系统 -&amp;gt; 通用块设备层 -&amp;gt; IO调度层 -&amp;gt; 块设备驱动 -&amp;gt; 硬件设备这所有几个层次。其实这就是一般IO。当然，不同的状态可能会有变化，比如一个进程正好open并read一个已经存在于page cache中的数据。这样的事情我们排出在外不分析。那么什么是比较特别的io呢？</description></item><item><title>Cgroup - Linux的IO资源隔离（完整版）</title><link>/posts/003b-cgroup_linux_io_control_group/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/003b-cgroup_linux_io_control_group/</guid><description>Cgroup - Linux的IO资源隔离 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
今天我们来谈谈：
Linux的IO隔离 跟内存管理那部分复杂度类似，IO的资源隔离要讲清楚也是比较麻烦的。这部分内容都是这样，配置起来简单，但是要理解清楚确没那么简单。这次是跟Linux内核的IO实现有关系。对于IO的速度限制，实现思路跟CPU和内存都不一样。CPU是针对进程占用时间的比例限制，内存是空间限制，而当我们讨论IO资源隔离的时候，实际上有两个资源需要考虑，一个是空间，另一个是速度。对于空间来说，这个很简单，大不了分区就是了。现实手段中，分区、LVM、磁盘配额、目录配额等等，不同的分区管理方式，不同的文件系统都给出了很多不同的解决方案。所以，空间的限制实际上不是cgroup要解决的问题，那就是说，我们在这里要解决的问题是：如何进行IO数据传输的速度限制。
限速这件事情，现实中有很多模型、算法去解决这个问题。比如，如果我们想控制高速公路上的汽车单位时间通过率，就让收费站每隔固定时间周期只允许通过固定个数的车就好了。这是一种非常有效的控制手段－－漏斗算法。现实中这种算法可能在特定情况下会造成资源浪费以及用户的体验不好，于是又演化出令牌桶算法。这里我们不去详细分析这些算法，但是我们要知道，对io的限速基本是一个漏斗算法的限速效果。无论如何，这种限速都要有个“收费站”这样的设施来执行限速，那么对于Linux的IO体系来说，这个”收费站”建在哪里呢？于是我们就必须先来了解一下：
Linux的IO体系 Linux的IO体系是个层级还算清晰的结构，它基本上分成了如图示这样几层：
Linux的IO体系层次结构
我们可以通过追踪一个read()系统调用来一窥这些层次的结构，当read()系统调用发生，内核首先会通过汇编指令引发一个软中断，然后根据中断传入的参数查询系统调用影射表，找到read()对应的内核调用方法名，并去执行相关调用，这个系统调用名一般情况下就是sys_read()。从此，便开始了调用在内核中处理的过程的第一步：
VFS层：虚拟文件系统层。由于内核要跟多种文件系统打交道，而每一种文件系统所实现的数据结构和相关方法都可能不尽相同，所以，内核抽象了这一层，专门用来适配各种文件系统，并对外提供统一操作接口。 文件系统层：不同的文件系统实现自己的操作过程，提供自己特有的特征，具体不多说了，大家愿意的话自己去看代码即可。 页缓存层：我们的老朋友了，如果不了解缓存是什么的，可以先来看看Linux内存资源管理部分。 通用块层：由于绝大多数情况的io操作是跟块设备打交道，所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备，对上提供统一的Block IO请求标准。 IO调度层：因为绝大多数的块设备都是类似磁盘这样的设备，所以有必要根据这类设备的特点以及应用的不同特点来设置一些不同的调度算法和队列。以便在不同的应用环境下有针对性的提高磁盘的读写效率，这里就是大名鼎鼎的Linux电梯所起作用的地方。针对机械硬盘的各种调度方法就是在这实现的。 块设备驱动层：驱动层对外提供相对比较高级的设备操作接口，往往是C语言的，而下层对接设备本身的操作方法和规范。 块设备层：这层就是具体的物理设备了，定义了各种真对设备操作方法和规范。 根据这几层的特点，如果你是设计者，你会在哪里实现真对块设备的限速策略呢？6、7都是相关具体设备的，如果在这个层次提供，那就不是内核全局的功能，而是某些设备自己的feture。文件系统层也可以实现，但是如果要全局实现也是不可能的，需要每种文件系统中都实现一遍，成本太高。所以，可以实现限速的地方比较合适的是VFS、缓存层、通用块层和IO调度层。而VFS和page cache这样的机制并不是面向块设备设计的，都是做其他事情用的，虽然也在io体系中，但是并不适合用来做block io的限速。所以这几层中，最适合并且成本最低就可以实现的地方就是IO调度层和通用块层。IO调度层本身已经有队列了，我们只要在队列里面实现一个限速机制即可，但是在IO调度层实现的限速会因为不同调度算法的侧重点不一样而有很多局限性，从通用块层实现的限速，原则上就可以对几乎所有的块设备进行带宽和iops的限制。截止目前（4.3.3内核），IO限速主要实现在这两层中。
根据IO调度层和通用块层的特点，这两层分别实现了两种不同策略的IO控制策略，也是目前blkio子系统提供的两种控制策略，一个是权重比例方式的控制，另一个是针对IO带宽和IOPS的控制。
IO调度层 我们需要先来认识一下IO调度层。这一层要解决的核心问题是，如何提高块设备IO的整体性能？这一层也主要是针对用途最广泛的机械硬盘结构而设计的。众所周知，机械硬盘的存储介质是磁介质，并且是盘状，用磁头在盘片上移动进行数据的寻址，这类似播放一张唱片。这种结构的特点是，顺序的数据读写效率比较理想，但是如果一旦对盘片有随机读写，那么大量的时间都会浪费在磁头的移动上，这时候就会导致每次IO的响应时间很长，极大的降低IO的响应速度。磁头在盘片上寻道的操作，类似电梯调度，如果在寻道的过程中，能把路过的相关磁道的数据请求都“顺便”处理掉，那么就可以在比较小影响响应速度的前提下，提高整体IO的吞吐量。所以，一个好的IO调度算法的需求就此产生。在最开始的阶段，Linux就把这个算法命名为Linux电梯算法。目前在内核中默认开启了三种算法，其实严格算应该是两种，因为第一种叫做noop，就是空操作调度算法，也就是没有任何调度操作，并不对io请求进行排序，仅仅做适当的io合并的一个fifo队列。
目前内核中默认的调度算法应该是cfq，叫做完全公平队列调度。这个调度算法人如其名，它试图给所有进程提供一个完全公平的IO操作环境。它为每个进程创建一个同步IO调度队列，并默认以时间片和请求数限定的方式分配IO资源，以此保证每个进程的IO资源占用是公平的，cfq还实现了针对进程级别的优先级调度，这里我们不去细节解释。我们在此只需要知道，既然时间片分好了，优先级实现了，那么cfq肯定是实现进程级别的权重比例分配的最好方案。内核就是这么做的，cgroup blkio的权重比例限制就是基于cfq调度器实现的。如果你要使用权重比例分配，请先确定对应的块设备的IO调度算法是cfq。
查看和修改的方法是：
[zorro@zorrozou-pc0 ~]$ cat /sys/block/sda/queue/scheduler noop deadline [cfq] [zorro@zorrozou-pc0 ~]$ echo cfq &amp;gt; /sys/block/sda/queue/scheduler cfq是通用服务器比较好的IO调度算法选择，对桌面用户也是比较好的选择。但是对于很多IO压力较大的场景就并不是很适应，尤其是IO压力集中在某些进程上的场景。因为这种场景我们需要更多的满足某个或者某几个进程的IO响应速度，而不是让所有的进程公平的使用IO，比如数据库应用。
deadline调度（最终期限调度）就是更适应这样的场景的解决方案。deadline实现了四个队列，其中两个分别处理正常read和write，按扇区号排序，进行正常io的合并处理以提高吞吐量.因为IO请求可能会集中在某些磁盘位置，这样会导致新来的请求一直被合并，于是可能会有其他磁盘位置的io请求被饿死。于是实现了另外两个处理超时read和write的队列，按请求创建时间排序，如果有超时的请求出现，就放进这两个队列，调度算法保证超时（达到最终期限时间）的队列中的请求会优先被处理，防止请求被饿死。由于deadline的特点，无疑在这里无法区分进程，也就不能实现针对进程的io资源控制。
其实不久前，内核还是默认标配四种算法，还有一种叫做as的算法（Anticipatory scheduler），预测调度算法。一个高大上的名字，搞得我一度认为Linux内核都会算命了。结果发现，无非是在基于deadline算法做io调度的之前等一小会时间，如果这段时间内有可以合并的io请求到来，就可以合并处理，提高deadline调度的在顺序读写情况下的数据吞吐量。其实这根本不是啥预测，我觉得不如叫撞大运调度算法。估计结果是不仅没有提高吞吐量，还降低了响应速度，所以内核干脆把它从默认配置里删除了。毕竟Linux的宗旨是实用。
根据以上几种io调度算法的简单分析，我们也能对各种调度算法的使用场景有一些大致的思路了。从原理上看，cfq是一种比较通用的调度算法，是一种以进程为出发点考虑的调度算法，保证大家尽量公平。deadline是一种以提高机械硬盘吞吐量为思考出发点的调度算法，只有当有io请求达到最终期限的时候才进行调度，非常适合业务比较单一并且IO压力比较重的业务，比如数据库。而noop呢？其实如果我们把我们的思考对象拓展到固态硬盘，那么你就会发现，无论cfq还是deadline，都是针对机械硬盘的结构进行的队列算法调整，而这种调整对于固态硬盘来说，完全没有意义。对于固态硬盘来说，IO调度算法越复杂，效率就越低，因为额外要处理的逻辑越多。所以，固态硬盘这种场景下，使用noop是最好的，deadline次之，而cfq由于复杂度的原因，无疑效率最低。但是，如果你想对你的固态硬盘做基于权重比例的IO限速的话，那就没啥办法了，毕竟这时候，效率并不是你的需求，要不你限速干嘛？
通用块设备层 这层的作用我这里就不再复述了，本节其实主要想解释的是，既然这里实现了对blkio的带宽和iops的速度限制，那么有没有什么需要注意的地方？这自然是有的。首先我们还是要先来搞清楚IO中的几个概念。
一般IO：
一个正常的文件io，需要经过vfs -&amp;gt; buffer\page cache -&amp;gt; 文件系统 -&amp;gt; 通用块设备层 -&amp;gt; IO调度层 -&amp;gt; 块设备驱动 -&amp;gt; 硬件设备这所有几个层次。其实这就是一般IO。当然，不同的状态可能会有变化，比如一个进程正好open并read一个已经存在于page cache中的数据。这样的事情我们排出在外不分析。那么什么是比较特别的io呢？</description></item><item><title>Cgroup - Linux的网络资源隔离</title><link>/posts/006-cgroup_linux_network_control_group/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/006-cgroup_linux_network_control_group/</guid><description>Cgroup - Linux的网络资源隔离 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
本文不会涉及一些网络基础知识的讲解以及iproute2相关命令的使用的讲解，建议如果想要更好理解本文，之前应该对网络知识、tc命令和LARTC的文档有一定了解。如果本文中有什么知识点让不够清楚，可以结合LARTC文档一起服用。
想要直接上手配置cgroup的网络资源隔离的人，可以直接看本文倒数第二部分：使用cgroup限制网络流量。
本文PDF
今天我们来谈谈：
Linux的网络资源隔离 如果说Linux内核的cgroup算是个新技术的话，那么它的网络资源隔离部分的实现算是个不折不扣的老技术了。实际上是先有的网络资源的隔离技术，才有的cgroup。或者说是先有的网络资源的隔离才有的2.4、2.6版本的Linux内核，而现在的最主流的内核版本应该是3.10了（考虑到android手机的出货量，你公司那几千几万台服务器真的算是个零头对吧？）。好吧，Linux早在内核2.2版本就已经引入了网络QoS的机制，并且网络资源的隔离功能只是其所实现功能的一部分而已。无论如何，cgroup并没有再重新搞一套网络资源隔离的实现，而是直接使用了Linux的iproute2的traffic control（tc）功能。实际上网络资源隔离的文档真的不用我再多写什么了，我最亲爱的前同事＋朋友＋导师——johnbull同志早已经在2003年的非典期间就因为无聊而完成了非常高质量的相关技术文档翻译工作，将这方面最权威的LARTC（Linux Advanced Routing &amp;amp; Traffic Control）文档翻译成了中文版。
英文版链接
中文版链接
曾经chinaunix的资深版主johnbull同志现在在新浪微博工作，所以经常在微博出没，如果对以上文档有兴趣和疑问的人可以直接去找他对质，传送门在此。
其实原则上说，本技术文章已经讲完了，但是为了不让大家有种上当受骗的感觉，我觉得我还是有必要从cgroup的角度再来讲讲tc，也算是对TC近几年发展做一个补充。
什么是队列规则 tc命令引入了一系列概念，其中我们最需要先理解的就是队列规则。它的英文名字叫做queueing discipline，在tc命令中也叫qdisc，或者直接简写为qd。我们先来看看它，有个感性的认识：
在我的虚拟机的centos7中，它是这样的：
[root@localhost Desktop]# tc qd ls qdisc pfifo_fast 0: dev eno16777736 root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1 在我的台式机上装的archlinux（更新到了当前最新版的4.3.3内核）以及fedora 23上是这样的：
[root@zorrozou-pc0 zorro]# tc qd ls qdisc noqueue 0: dev lo root refcnt 2 qdisc fq_codel 0: dev enp2s0 root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.</description></item><item><title>Cgroup - Linux内存资源管理</title><link>/posts/005-cgroup_linux_memory_control_group/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/005-cgroup_linux_memory_control_group/</guid><description>Cgroup - Linux内存资源管理 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
在聊cgroup的内存限制之前，我们有必要先来讲解一下：
Linux内存管理基础知识 free命令 无论从任何角度看，Linux的内存管理都是一坨麻烦的事情，当然我们也可以用一堆、一片、一块、一筐来形容这个事情，但是毫无疑问，用一坨来形容它简直恰当无比。在理解它之前，我甚至不会相信精妙的和恶心可以同时形容同一件事情，是的，在我看来它就是这样的。其实我只是做个铺垫，让大家明白，我们下面要讲的内容，绝不是一个成体系的知识，所以，学习起来也确实很麻烦。甚至，我写这个技术文章之前一度考虑了很久该怎么写？从哪里开始写？思考了半天，还是不能免俗，我们无奈，仍然先从free命令说起：
[root@zorrozou-pc ~]# free total used free shared buffers cached Mem: 131904480 6681612 125222868 0 478428 4965180 -/+ buffers/cache: 1238004 130666476 Swap: 2088956 0 2088956 这个命令几乎是每一个使用过Linux的人必会的命令，但越是这样的命令，似乎真正明白的人越少（我是说比例越少）。一般情况下，对此命令的理解可以分这几个阶段：
我擦，内存用了好多，6个多G，可是我什么都没有运行啊？为什么会这样？Linux好占内存。 嗯，根据我专业的眼光看出来，内存才用了1G多点，还有很多剩余内存可用。buffers/cache占用的较多，说明系统中有进程曾经读写过文件，但是不要紧，这部分内存是当空闲来用的。 free显示的是这样，好吧我知道了。神马？你问我这些内存够不够，我当然不知道啦！我特么怎么知道你程序怎么写的？ 如果你的认识在第一种阶段，那么请你继续补充关于Linux的buffers／cache的知识。如果你处在第二阶段，好吧，你已经是个老手了，但是需要提醒的是，上帝给你关上一扇门的同时，肯定都会给你放一条狗的。是的，Linux的策略是：内存是用来用的，而不是用来看的。但是，只要是用了，就不是没有成本的。有什么成本，凭你对buffer/cache的理解，应该可以想的出来。一般我比较认同第三种情况，一般光凭一个free命令的显示，是无法判断出任何有价值的信息的，我们需要结合业务的场景以及其他输出综合判断目前遇到的问题。当然也可能这种人给人的第一感觉是他很外行，或者他真的是外行。
无论如何，free命令确实给我门透露了一些有用的信息，比如内存总量，剩余多少，多少用在了buffers／cache上，Swap用了多少，如果你用了其它参数还能看到一些其它内容，这里不做一一列举。那么这里又引申出另一些概念，什么是buffer？什么是cache？什么是swap？由此我们就直接引出另一个命令：
[root@zorrozou-pc ~]# cat /proc/meminfo MemTotal: 131904480 kB MemFree: 125226660 kB Buffers: 478504 kB Cached: 4966796 kB SwapCached: 0 kB Active: 1774428 kB Inactive: 3770380 kB Active(anon): 116500 kB Inactive(anon): 3404 kB Active(file): 1657928 kB Inactive(file): 3766976 kB Unevictable: 0 kB Mlocked: 0 kB SwapTotal: 2088956 kB SwapFree: 2088956 kB Dirty: 336 kB Writeback: 0 kB AnonPages: 99504 kB Mapped: 20760 kB Shmem: 20604 kB Slab: 301292 kB SReclaimable: 229852 kB SUnreclaim: 71440 kB KernelStack: 3272 kB PageTables: 3320 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 68041196 kB Committed_AS: 352412 kB VmallocTotal: 34359738367 kB VmallocUsed: 493196 kB VmallocChunk: 34291062284 kB HardwareCorrupted: 0 kB AnonHugePages: 49152 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 194816 kB DirectMap2M: 3872768 kB DirectMap1G: 132120576 kB 以上显示的内容都是些什么鬼？</description></item><item><title>Cgroup - 从CPU资源隔离说起</title><link>/posts/001-cgroup_linux_cpu_control_group/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/001-cgroup_linux_cpu_control_group/</guid><description>Cgroup - 从CPU资源隔离说起 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
今天我们来谈谈：
什么是Cgroup？ cgroups，其名称源自控制组群（control groups）的简写，是Linux内核的一个功能，用来限制，控制与分离一个进程组群的资源（如CPU、内存、磁盘输入输出等）。
&amp;ndash;引自维基百科:cgroup
引用官方说法总是那么冰冷的让人不好理解，所以我还是稍微解释一下：
一个正在运行着服务的计算机系统，跟我们高中上课的情景还是很相似的。如果把系统中的每个进程理解为一个同学的话，那么班主任就是操作系统的核心（kernel），负责管理班里的同学。而cgroup，就是班主任控制学生行为的一种手段，所以，它起名叫control groups。
既然是一种控制手段，那么cgroup能控制什么呢？当然是资源啦！对于计算机来说，资源大概可以分成以下几个部分：
计算资源 内存资源 io资源 网络资源 这就是我们常说的内核四大子系统。当我们学习内核的时候，我们也基本上是围绕这四大子系统进行研究。 我们今天要讨论的，主要是cgroup是如何对系统中的CPU资源进行隔离和分配的。其他资源的控制，我们以后有空再说喽。
如何看待CPU资源？ 由于进程和线程在Linux的CPU调度看来没啥区别，所以本文后续都会用进程这个名词来代表内核的调度对象，一般来讲也包括线程
如果要分配资源，我们必须先搞清楚这个资源是如何存在的，或者说是如何组织的。我想CPU大家都不陌生，我们都在系统中用过各种工具查看过CPU的使用率，比如说以下这个命令和它的输出：
[zorro@zorrozou-pc0 ~]$ mpstat -P ALL 1 1 Linux 4.2.5-1-ARCH (zorrozou-pc0) 2015年12月22日 _x86_64_ (4 CPU)mt 16时01分08秒 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 16时01分09秒 all 0.25 0.00 0.25 0.00 0.00 0.00 0.00 0.00 0.00 99.50 16时01分09秒 0 0.00 0.</description></item><item><title>Cgroup - 详解Cgroup V2</title><link>/posts/042-%E8%AF%A6%E8%A7%A3cgroup-v2/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/042-%E8%AF%A6%E8%A7%A3cgroup-v2/</guid><description>详解Cgroup V2 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 虽然cgroup v2早已在linux 4.5版本的时候就已经加入内核中了，而centos 8默认也已经用了4.18作为其内核版本，但是系统中仍然默认使用的是cgroup v1。
本文主要介绍了在fedora 31系统，内核版本为5.5.15上的cgroup v2使用方法。也是继前几年写的四篇cgroup文章后再次讲解cgroup。谁让我之前在那些文章里挖了坑呢？好吧，这篇是我欠你们的。祝阅读愉快。
在系统上开启cgroup v2 因为系统上默认仍然开启cgroup v1，所以我们需要配置一下系统，并且换成cgroup v2。为了确认切换是否成功，我们需要先看一下v1什么样？
[root@localhost zorro]# mount ...... cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) .</description></item><item><title>ext4数据恢复实战及文件系统结构详解</title><link>/posts/030-ext4/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/030-ext4/</guid><description>ext4数据恢复实战及文件系统结构详解 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 如果你的数据被不小心误删除了，那么对文件系统结构的深入理解可以帮助你找到数据恢复的途径。我们先从一个数据恢复的例子开始，对ext4的文件系统结构做个介绍。
ext4数据恢复实战 废话少说，下面我们直接上手进行数据恢复的实例。先格式化一个ext4文件系统。
mkfs.ext4 /dev/sdf1 mke2fs 1.42.9 (28-Dec-2013) Filesystem label= OS type: Linux Block size=4096 (log=2) Fragment size=4096 (log=2) Stride=0 blocks, Stripe width=0 blocks 122101760 inodes, 488378390 blocks 24418919 blocks (5.00%) reserved for the super user First data block=0 Maximum filesystem blocks=2636120064 14905 block groups 32768 blocks per group, 32768 fragments per group 8192 inodes per group Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 102400000, 214990848 Allocating group tables: done Writing inode tables: done Creating journal (32768 blocks): done Writing superblocks and filesystem accounting information: done 挂载到测试目录：</description></item><item><title>find命令详解</title><link>/posts/007-findming-ling-xiang-jie/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/007-findming-ling-xiang-jie/</guid><description>find命令详解 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 find命令是我们日常工作中比较常用的Linux命令。全面的掌握这个命令可以使很多操作达到事半功倍的效果。如果对find命令有以下这些疑惑，本文都能帮你解决：
find命令的格式是什么？ 参数中出现+或-号是什么意思？比如find / -mtime +7与find / -mtime -7什么区别？ find /etc/ -name &amp;ldquo;passwd&amp;rdquo; -exec echo {} ;和find /etc/ -name &amp;ldquo;passwd&amp;rdquo; -exec echo {} +有啥区别？ -exec参数为什么要以“;”结尾，而不是只写“;”？ 命令基础 find命令大家都比较熟悉，反倒想讲的有特色比较困难。那干脆我们怎么平淡怎么来好了。我们一般用的find命令格式很简单，一般分成三个部分：
find /etc -name &amp;quot;passwd&amp;quot; 格式如上，第一段find命令。第二段，要搜索的路径。这一段目录可以写多个，如：
find /etc /var /usr -name &amp;quot;passwd&amp;quot; 第三段，表达式。我们例子中用的是-name &amp;ldquo;passwd&amp;quot;这个表达式，指定条件为找到文件名是passwd的文件。对于find命令，最需要学习的是表达式这一段。表达式决定了我们要找的文件是什么属性的文件，还可以指定一些“动作”，比如将匹配某种条件的文件删除。所以，find命令的核心就是表达式（EXPRESSION）的指定方法。
find命令中的表达式有四种类型，分别是：
Tests：就是我们最常用的指定查找文件的条件。 Actions：对找到的文件可以做的操作。 Global options：全局属性用来限制一些查找的条件，比如常见的目录层次深度的限制。 Positional options：位置属性用来指定一些查找的位置条件。 这其中最重要的就是Tests和Actions，他们是find命令的核心。另外还有可以将多个表达式连接起来的操作符，他们可以表达多个表达式之间的逻辑关系和运算优先顺序，叫做Operators。
下面我们就来分类看一下这些个分类的功能。
TESTS find命令是通过文件属性查找文件的。所以，find表达式的tests都是文件的属性条件，比如文件的各种时间，文件权限等。很多参数中会出现指定一个数字n，一般会出现三种写法：
+n：表示大于n。
-n：表示小于n。
n：表示等于n。
根据时间查找 比较常用数字方式来指定的参数是针对时间的查找，比如-mtime n：查找文件修改时间，单位是天，就是n*24小时。举个例子说：
[root@zorrozou-pc0 zorro]# find / -mtime 7 -ls 我们为了方便看到结果，在这个命令中使用了-ls参数，具体细节后面会详细解释。再此我们只需要知道这个参数可以将符合条件的文件的相关属性显示出来即可。那么我们就可以通过这个命令看到查找到的文件的修改时间了。</description></item><item><title>Linux的EEVDF调度算法</title><link>/posts/015-linux-eevdf-scheduler/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/015-linux-eevdf-scheduler/</guid><description>为什么Linux需要一个新的进程调度算法？ 自从2007年开始，CFS进入Linux 2.6.23内核以来，这个调度算法已经陪伴Linux的发展超过15年了。但时间长并不是要替换掉它的理由，CFS虽然并不完美，但是大多数场景下它都可以提供很好的调度效果。随着行业的发展，Linux内核也被用在了各种更复杂的场景需求下。在很多场景下，应用发现有很多针对进程调度延迟的差异需求。比如，很多应用在需要占用CPU的时候，总是想要很快能运行起来，而且它要占用的时间长度很短，所以它对那些需要长时间占用CPU进行运算的进程来说，总体吞吐量影响也不算大，如果可以尽快调度这一类应用，可以给用户提供更好的用户体验。而CFS从一开始就是为了公平而涉及的调度算法，其设计思路是保证在一个特定的预期时间内，所有进程都是平均占用CPU的。虽然我们也可以通过调整这个预期时间的长短，来让新进程尽可能的快的得到响应，但是这依然将破坏其他进程的调度时间长度，从而更加频繁的产生上下文切换，不能让CPU的缓存得到充分的利用。
在这样的需求驱动下，Peter Zijlstra在2023年向内核提交了EEVDF调度算法，其全称为：&amp;ldquo;Earliest Eligible Virtual Deadline First&amp;rdquo;。强行翻译一下就是，“最早符合资格的虚拟最终时间优先“调度算法。大家先不要被这个名字吓到，实际上它并不是一个很难以理解的设计。这个算法从提交到现在已经发展了一段时间了，从效果上来看，它不仅提供了比CFS调度延迟上更多的优势，它还摆脱了CFS实现中为了解决各种特定条件下一致性的一大堆启发式代码。本文就是基于当前最新的稳定版内核6.10.8对EEVDF调度算法实现的一个介绍。
EEVDF调度算法简介 我们先来从EEVDF的名字来理解一下这个算法的设计思路。&amp;ldquo;Earliest Eligible Virtual Deadline First&amp;rdquo;，“最早符合资格的虚拟最终时间优先“调度算法。简单来说，这个名字中最关键的两个词是Deadline和Eligible。更简单来说，这个调度算法其实就是找到队列中当前预期占用CPU时间最短的（Deadline）那个进程来执行。这样就保证了我们改进的目标，即：占用时间很短的进程，应该更快的响应。当然，除了这个要求外，还要符合资格（Eligible）。这个算法本身并不是一个新提出的算法，早在1995年就已经由Ion Stoica和 Hussein Abdel-Wahab提出了，论文连接：https://citeseerx.ist.psu.edu/document?repid=rep1&amp;amp;type=pdf&amp;amp;doi=805acf7726282721504c8f00575d91ebfd750564
设计目标上跟CFS一样，EEVDF也试图在多个争抢CPU的进程之间公平的分配CPU时间。我们假定这个队列中有5个进程要使用CPU，调度预期时间为1000ms的情况下，我们会给这5个进程分配200ms的“预期CPU占用时间”。此时，就可以理解为这5个进程的预期deadline时间都是200ms之后。但是在执行的过程中，进程并不都是按这个预期来使用CPU的。有些进程可能占用不到200ms就已经主动释放CPU了。于是这里就产生了一个实际上的不公平现象，即对于每个进程来说，它给他分配的预期时间跟它实际占用的时间并不一样。我们可以通过跟踪这个差值（分配时间-实际占用时间）来跟踪一个进程的“滞后”性（lag值）。如果这个值为正，则说明这个进程因为某种原因一致没能用满其该占用的CPU时间，从公平的角度来说，它被不公平的对待了，所以这种进程应该考虑下次调度的时候获得优先占用CPU的权利。
我们把这种进程规定为“符合资格”（Eligible）的进程，即lag值为正的进程。这也是EEVDF调度算法进行调度选择的第一个条件：从lag值为正的进程中选择下一个该调度的进程。对于任何当前调度周期不符合资格的进程，因为本次调度仍然会给它分配预期CPU的占用时间，所以其lag值未来某个时间总会变成正值，就会再次符合资格。
另一个起作用的条件就是虚拟最终时间（Virtual Deadline），这个就是本次分配完时间长度后，进程预计的执行结束时间。这个时间就是当前分配的时间长度+“符合条件”时间（Eligible Time）。这个Eligible Time是当进程不符合条件（lag值为负值）的时候，要等待其成为正值的时间。
EEVDF就是在符合条件（lag值为正）的进程中选择虚拟最终时间最短的那个进程来执行。
EEVDF调度算法核心代码分析pick_eevdf() 内核在执行中总会找时机进入调度算法选的的过程中，在此我们先不对调度时机进行解释，仅列出其进入调度核心算法的过程路径：
eevdf调度算法核心函数：pick_eevdf()
调用逻辑：调度器schedule()&amp;ndash;&amp;gt;eevdf class: fair &amp;ndash;&amp;gt; __pick_next_task_fair &amp;ndash;&amp;gt; pick_eevdf()
&amp;ndash;&amp;gt; pick_task_fair
为了对多个运行程序提供一种基于不同延迟预期的调度算法，EEVDF基于以下规则在运行队列中选择进程:
1、进程必须符合资格。即eligible，eligible的意思是说，曾经分给进程的可执行时间有剩余，即调度器分给进程的时间片总长度大于等于进程实际使用的时间。
2、在符合条件1的情况下，选择最终期限要求最短的进程执行。
在一个红黑树中，这个选择策略可以在O（log n）时间复杂度的条件下实现。这个红黑树使用进程的预期截止时间deadline时间进行排序。同时维护了一个基于vruntime的堆，来判断进程是否符合资格。vruntime的计算符合如下公式：
se-&amp;gt;min_vruntime = min(se-&amp;gt;vruntime, se-&amp;gt;{left,right}-&amp;gt;min_vruntime)
这个函数不算长，我们全文注释一下：
算法实现并不复杂，核心就是一句话：在符合资格的进程中，选择deadline最小的那一个进程。从其实现来看，我们也能了解，对于调度器来说，最核心的调度对象就是struct sched_entity这个数据结构。我们也来简单看一下这个数据结构：
从核心函数以及数据结构中我们可以引申出几个细节问题：
entity_eligible()的实现细节中具体如何判断一个entity是eligible的？
什么是RUN_TO_PARITY内核参数？它在哪里配置？
对于每个任务的se结构来说，内核是什么时候对这些数据进行更新的？
下面我们就来分别看一下这些问题。
如何判断一个任务是eligible的？ 我们可以从代码中看一下entity_eligible()的具体实现：
核心函数是vruntime_eligible()，我们先来看一下这里用到的struct cfs_rq数据结构。这个结构也就是调度队列的结构：
数据结构比较长，我们仅解释一下目前用到的数据含义：
cfs_rq-&amp;gt;avg_vruntime：队列中所有任务的平均vruntime。
cfs_rq-&amp;gt;avg_load：队列中队友任务的平均load，这个load用来计算权重。
vruntime_eligible()是一个比较重要的方法，简单来说，他用来判断一个se是不是有资格使用CPU。我们回顾一下之前说过的eligible条件是：lag值为整。即：任务的预期占用时间 &amp;gt;= 任务的实际占用时间。注释中给了一个计算这个值的公式推导过程：
lag_i = S - s_i = w_i*(V - v_i)</description></item><item><title>Linux的IO调度</title><link>/posts/008-linuxde-io-diao-du/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/008-linuxde-io-diao-du/</guid><description>Linux的IO调度 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
IO调度发生在Linux内核的IO调度层。这个层次是针对Linux的整体IO层次体系来说的。从read()或者write()系统调用的角度来说，Linux整体IO体系可以分为七层，它们分别是：
VFS层：虚拟文件系统层。由于内核要跟多种文件系统打交道，而每一种文件系统所实现的数据结构和相关方法都可能不尽相同，所以，内核抽象了这一层，专门用来适配各种文件系统，并对外提供统一操作接口。 文件系统层：不同的文件系统实现自己的操作过程，提供自己特有的特征，具体不多说了，大家愿意的话自己去看代码即可。 页缓存层：负责针对page的缓存。 通用块层：由于绝大多数情况的io操作是跟块设备打交道，所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备，对上提供统一的Block IO请求标准。 IO调度层：因为绝大多数的块设备都是类似磁盘这样的设备，所以有必要根据这类设备的特点以及应用的不同特点来设置一些不同的调度算法和队列。以便在不同的应用环境下有针对性的提高磁盘的读写效率，这里就是大名鼎鼎的Linux电梯所起作用的地方。针对机械硬盘的各种调度方法就是在这实现的。 块设备驱动层：驱动层对外提供相对比较高级的设备操作接口，往往是C语言的，而下层对接设备本身的操作方法和规范。 块设备层：这层就是具体的物理设备了，定义了各种针对设备操作方法和规范。 有一个已经整理好的Linux IO结构图，非常经典，一图胜千言：
我们今天要研究的内容主要在IO调度这一层。它要解决的核心问题是，如何提高块设备IO的整体性能？这一层也主要是针对机械硬盘结构而设计的。众所周知，机械硬盘的存储介质是磁盘，磁头在盘片上移动进行磁道寻址，行为类似播放一张唱片。这种结构的特点是，顺序访问时吞吐量较高，但是如果一旦对盘片有随机访问，那么大量的时间都会浪费在磁头的移动上，这时候就会导致每次IO的响应时间变长，极大的降低IO的响应速度。磁头在盘片上寻道的操作，类似电梯调度，如果在寻道的过程中，能把顺序路过的相关磁道的数据请求都“顺便”处理掉，那么就可以在比较小影响响应速度的前提下，提高整体IO的吞吐量。这就是我们为什么要设计IO调度算法的原因。在最开始的时期，Linux把这个算法命名为Linux电梯算法。目前在内核中默认开启了三种算法，其实严格算应该是两种，因为第一种叫做noop，就是空操作调度算法，也就是没有任何调度操作，并不对io请求进行排序，仅仅做适当的io合并的一个fifo队列。
目前内核中默认的调度算法应该是cfq，叫做完全公平队列调度。这个调度算法人如其名，它试图给所有进程提供一个完全公平的IO操作环境。它为每个进程创建一个同步IO调度队列，并默认以时间片和请求数限定的方式分配IO资源，以此保证每个进程的IO资源占用是公平的，cfq还实现了针对进程级别的优先级调度，这个我们后面会详细解释。
查看和修改IO调度算法的方法是：
[zorro@zorrozou-pc0 ~]$ cat /sys/block/sda/queue/scheduler noop deadline [cfq] [zorro@zorrozou-pc0 ~]$ echo cfq &amp;gt; /sys/block/sda/queue/scheduler cfq是通用服务器比较好的IO调度算法选择，对桌面用户也是比较好的选择。但是对于很多IO压力较大的场景就并不是很适应，尤其是IO压力集中在某些进程上的场景。因为这种场景我们需要更多的满足某个或者某几个进程的IO响应速度，而不是让所有的进程公平的使用IO，比如数据库应用。
deadline调度（最终期限调度）就是更适合上述场景的解决方案。deadline实现了四个队列，其中两个分别处理正常read和write，按扇区号排序，进行正常io的合并处理以提高吞吐量.因为IO请求可能会集中在某些磁盘位置，这样会导致新来的请求一直被合并，可能会有其他磁盘位置的io请求被饿死。因此实现了另外两个处理超时read和write的队列，按请求创建时间排序，如果有超时的请求出现，就放进这两个队列，调度算法保证超时（达到最终期限时间）的队列中的请求会优先被处理，防止请求被饿死。
不久前，内核还是默认标配四种算法，还有一种叫做as的算法（Anticipatory scheduler），预测调度算法。一个高大上的名字，搞得我一度认为Linux内核都会算命了。结果发现，无非是在基于deadline算法做io调度的之前等一小会时间，如果这段时间内有可以合并的io请求到来，就可以合并处理，提高deadline调度的在顺序读写情况下的数据吞吐量。其实这根本不是啥预测，我觉得不如叫撞大运调度算法，当然这种策略在某些特定场景下效果不错。但是在大多数场景下，这个调度不仅没有提高吞吐量，还降低了响应速度，所以内核干脆把它从默认配置里删除了。毕竟Linux的宗旨是实用，而我们也就不再这个调度算法上多费口舌了。
CFQ完全公平队列 CFQ是内核默认选择的IO调度队列，它在桌面应用场景以及大多数常见应用场景下都是很好的选择。如何实现一个所谓的完全公平队列（Completely Fair Queueing）？首先我们要理解所谓的公平是对谁的公平？从操作系统的角度来说，产生操作行为的主体都是进程，所以这里的公平是针对每个进程而言的，我们要试图让进程可以公平的占用IO资源。那么如何让进程公平的占用IO资源？我们需要先理解什么是IO资源。当我们衡量一个IO资源的时候，一般喜欢用的是两个单位，一个是数据读写的带宽，另一个是数据读写的IOPS。带宽就是以时间为单位的读写数据量，比如，100Mbyte/s。而IOPS是以时间为单位的读写次数。在不同的读写情境下，这两个单位的表现可能不一样，但是可以确定的是，两个单位的任何一个达到了性能上限，都会成为IO的瓶颈。从机械硬盘的结构考虑，如果读写是顺序读写，那么IO的表现是可以通过比较少的IOPS达到较大的带宽，因为可以合并很多IO，也可以通过预读等方式加速数据读取效率。当IO的表现是偏向于随机读写的时候，那么IOPS就会变得更大，IO的请求的合并可能性下降，当每次io请求数据越少的时候，带宽表现就会越低。从这里我们可以理解，针对进程的IO资源的主要表现形式有两个，进程在单位时间内提交的IO请求个数和进程占用IO的带宽。其实无论哪个，都是跟进程分配的IO处理时间长度紧密相关的。
有时业务可以在较少IOPS的情况下占用较大带宽，另外一些则可能在较大IOPS的情况下占用较少带宽，所以对进程占用IO的时间进行调度才是相对最公平的。即，我不管你是IOPS高还是带宽占用高，到了时间咱就换下一个进程处理，你爱咋样咋样。所以，cfq就是试图给所有进程分配等同的块设备使用的时间片，进程在时间片内，可以将产生的IO请求提交给块设备进行处理，时间片结束，进程的请求将排进它自己的队列，等待下次调度的时候进行处理。这就是cfq的基本原理。
当然，现实生活中不可能有真正的“公平”，常见的应用场景下，我们很肯能需要人为的对进程的IO占用进行人为指定优先级，这就像对进程的CPU占用设置优先级的概念一样。所以，除了针对时间片进行公平队列调度外，cfq还提供了优先级支持。每个进程都可以设置一个IO优先级，cfq会根据这个优先级的设置情况作为调度时的重要参考因素。优先级首先分成三大类：RT、BE、IDLE，它们分别是实时（Real Time）、最佳效果（Best Try）和闲置（Idle）三个类别，对每个类别的IO，cfq都使用不同的策略进行处理。另外，RT和BE类别中，分别又再划分了8个子优先级实现更细节的QOS需求，而IDLE只有一个子优先级。
另外，我们都知道内核默认对存储的读写都是经过缓存（buffer/cache）的，在这种情况下，cfq是无法区分当前处理的请求是来自哪一个进程的。只有在进程使用同步方式（sync read或者sync write）或者直接IO（Direct IO）方式进行读写的时候，cfq才能区分出IO请求来自哪个进程。所以，除了针对每个进程实现的IO队列以外，还实现了一个公共的队列用来处理异步请求。
当前内核已经实现了针对IO资源的cgroup资源隔离，所以在以上体系的基础上，cfq也实现了针对cgroup的调度支持。关于cgroup的blkio功能的描述，请看我之前的文章Cgroup – Linux的IO资源隔离。总的来说，cfq用了一系列的数据结构实现了以上所有复杂功能的支持，大家可以通过源代码看到其相关实现，文件在源代码目录下的block/cfq-iosched.c。
CFQ设计原理 在此，我们对整体数据结构做一个简要描述：首先，cfq通过一个叫做cfq_data的数据结构维护了整个调度器流程。在一个支持了cgroup功能的cfq中，全部进程被分成了若干个contral group进行管理。每个cgroup在cfq中都有一个cfq_group的结构进行描述，所有的cgroup都被作为一个调度对象放进一个红黑树中，并以vdisktime为key进行排序。vdisktime这个时间纪录的是当前cgroup所占用的io时间，每次对cgroup进行调度时，总是通过红黑树选择当前vdisktime时间最少的cgroup进行处理，以保证所有cgroups之间的IO资源占用“公平”。当然我们知道，cgroup是可以对blkio进行资源比例分配的，其作用原理就是，分配比例大的cgroup占用vdisktime时间增长较慢，分配比例小的vdisktime时间增长较快，快慢与分配比例成正比。这样就做到了不同的cgroup分配的IO比例不一样，并且在cfq的角度看来依然是“公平“的。
选择好了需要处理的cgroup（cfq_group）之后，调度器需要决策选择下一步的service_tree。service_tree这个数据结构对应的都是一系列的红黑树，主要目的是用来实现请求优先级分类的，就是RT、BE、IDLE的分类。每一个cfq_group都维护了7个service_trees，其定义如下：
struct cfq_rb_root service_trees[2][3]; struct cfq_rb_root service_tree_idle; 其中service_tree_idle就是用来给IDLE类型的请求进行排队用的红黑树。而上面二维数组，首先第一个维度针对RT和BE分别各实现了一个数组，每一个数组中都维护了三个红黑树，分别对应三种不同子类型的请求，分别是：SYNC、SYNC_NOIDLE以及ASYNC。我们可以认为SYNC相当于SYNC_IDLE并与SYNC_NOIDLE对应。idling是cfq在设计上为了尽量合并连续的IO请求以达到提高吞吐量的目的而加入的机制，我们可以理解为是一种“空转”等待机制。空转是指，当一个队列处理一个请求结束后，会在发生调度之前空等一小会时间，如果下一个请求到来，则可以减少磁头寻址，继续处理顺序的IO请求。为了实现这个功能，cfq在service_tree这层数据结构这实现了SYNC队列，如果请求是同步顺序请求，就入队这个service tree，如果请求是同步随机请求，则入队SYNC_NOIDLE队列，以判断下一个请求是否是顺序请求。所有的异步写操作请求将入队ASYNC的service tree，并且针对这个队列没有空转等待机制。此外，cfq还对SSD这样的硬盘有特殊调整，当cfq发现存储设备是一个ssd硬盘这样的队列深度更大的设备时，所有针对单独队列的空转都将不生效，所有的IO请求都将入队SYNC_NOIDLE这个service tree。
每一个service tree都对应了若干个cfq_queue队列，每个cfq_queue队列对应一个进程，这个我们后续再详细说明。</description></item><item><title>Linux的TCP实现之：慢启动</title><link>/posts/037-tcp_slowstart/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/037-tcp_slowstart/</guid><description>Linux的TCP实现之：慢启动 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 TCP的慢启动过程在不少场景下会严重影响性能，这也是TCP性能饱受病垢的原因之一。我们在本文中将尽量详细的描述慢启动过程和其在不同场景下的性能影响。
什么是慢启动？ 很抱歉，本文不打算从理论上描述什么是慢启动，以及为什么要慢启动？相信这些最基础的知识大家都能很方便的找到答案，如果你真的找不到的话，那我推荐《TCP/IP详解：卷一》来参考。在补充了这些基础之后，我们不如直接来看一个慢启动的例子，然后通过实际情况来看一下慢启动的过程。
实验环境是我本地跑的两个虚拟机，它们分别是fedora31和centos8，内核版本分别为5.5和4.18。为了能更体现出慢启动的效果，我们分别在两个服务器上人为添加了一点延时：
[root@localhost zorro]# tc qd add dev ens33 root netem delay 3ms [root@localhost zorro]# uname -r 5.5.15-200.fc31.x86_64 [root@localhost zorro]# tc qd add dev ens33 root netem delay 3ms [root@localhost zorro]# uname -r 4.18.0-147.8.1.el8_1.x86_64 两台服务器都加了3ms的发包延时，这将导致两台服务器之间的rtt时间达到6-7ms左右，我们来验证一下：
[root@localhost zorro]# ping -c 1000 -f 192.168.247.129 PING 192.168.247.129 (192.168.247.129) 56(84) bytes of data. --- 192.168.247.129 ping statistics --- 1000 packets transmitted, 1000 received, 0% packet loss, time 7198ms rtt min/avg/max/mdev = 6.</description></item><item><title>Linux的TCP实现之：三次握手</title><link>/posts/036-tcp_three_way_handshake/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/036-tcp_three_way_handshake/</guid><description>Linux的TCP实现之：三次握手 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 TCP协议是一个大家好像都熟悉，又好像都不熟悉的协议。说熟悉，是因为我们基本每天都要用到它，所有人似乎对三次握手、四次挥手、滑动窗口、慢启动、拥塞避免、拥塞控制等概念好像都有些了解。说不熟悉，是因为TCP协议相当的复杂，而且在运行过程中网络环境会变化，TCP的相关机制也会因为不同的变化而产生相关的适应行为，真的要说清楚其相关概念和运行过程又真的很不容易。
本系列文章希望从另一个角度交代清楚Linux上TCP实现的部分细节，当然能力有限，有些交代不清楚的地方还希望大家海涵。本文就从TCP建立连接的三次握手开始，希望对你有所帮助。本文内核代码版本基于linux-5.3。
什么是可靠和面向连接？ 说到TCP，不可不说的就是其是一个面向连接和可靠的传输层协议。相对的就是UDP，不可靠且非面向连接。其实IP的交付就是面向无连接和不可靠的协议，而UDP只是简单的在IP层协议上加了个传输层的端口封装，所以自然继承了IP的交付质量。TCP之所以复杂，就是因为它的设计需要在一个面向无连接、不可靠的IP上实现一个面向连接、可靠的传输层协议。所以，我们需要先从工程角度理解清楚到底什么是面向连接？什么是可靠？才能理解TCP为啥要这么复杂。
我们先来概述一下这几个问题：
什么是面向连接：
连接：在一个连接中传输的数据是有关系状态的，比如需要确定传输的对端正处在等待发送或接收的状态上。需要维护传输数据的关系，比如数据流的顺序。典型的例子就是打电话。
无连接：不用关心对端是否在线。每一个数据段的发送都是独立的一个数据个体，数据和数据之间没有关系，无需维护其之间的关系。典型的例子就是发短信。
什么是可靠：
主要是指数据在传输过程中不会被损坏或者丢失，保证数据可以正确到达。而不做以上保证的就是不可靠。
如何解决面向连接问题：
使用建立连接，传输数据，断开连接的三步创建一个长期的数据传输机制，在同一个连接中的数据传输是有上下文关系的。所以就要引申出以下概念：
需要维护seq序列号字段维护数据的顺序关系保证按序交付，和解决数据包重复的问题。
需要部分特殊的状态标记的包来专门创建、断开和维护一个连接：syn，ack，fin，rst
如何解决可靠性问题：
引入数据传输的确认机制，即数据发送之后等待对方确认。于是需要维护确认字段Acknowledgement和ack状态。即：停止等待协议。
引入数据确认机制（停止等待协议）之后，引发了带宽利用律不高的问题，如何解决？解决方案是引入窗口确认机制和滑动窗口，即不在以每个包发送之后进行确认，而是发送多个包之后一起确认。
引入窗口之后，如何在不同延时的网络上选择不同窗口大小？解决方法是引入窗口变量，和窗口监测通告：
发送方维护：
已发送并确认ack偏移量（窗口左边界）
已发送未确认ack偏移量（窗口当前发送字节位置）
即将发送偏移量（窗口右边界）
接收方维护：
已接受并确认偏移量（窗口左边界）
接受后会保存的窗口大小（窗口右边界）
接收方会给发送方回复ack确认，ack中会有最新窗口通告长度，以便发送方调整窗口长度。此处会引入sack选择确认行为和窗口为0时的坚持定时器行为。
引入滑动窗口之后，带宽可以充分被利用了，但是网络环境是复杂的，随时可能因为大量的数据传输导致网络上的拥塞。于是要引入拥塞控制机制：当出现拥塞的时候，tcp应该能保证带宽是被每条tcp连接公平分享的。所以在拥塞的情况下，要能将占用带宽较大的连接调整为占用带宽变小，占用小的调大。以达到公平占用资源的目的。
拥塞控制对带宽占用的调整本质上就是调整滑动窗口的大小来实现的，所以需要在接受端引入一个新的变量叫做cwnd：拥塞窗口，来反应当前网络的传输能力，而之前的通告窗口可以表示为awnd。此时发送端实际可用的窗口为cwnd和awnd的较小者。
由此引发的各种问题和概念不一而足，比如：如何决定实际的通告窗口大小？慢启动是什么？拥塞避免过程如何工作？拥塞控制是怎么作用的？等等等等&amp;hellip;&amp;hellip;
TCP之所以复杂，根本原因就是要在工程上解决这些问题。思路概述完了，我们先来看三次握手到底是干嘛的。
为什么要三次？ 为什么要三次握手，而不是两次，或者四次？或者其他次数？
首先我们要先理解建立连接的目的，有两个：
1、确认对端在线，即我请求你的时候你能立即给出响应。（面向连接）
2、如果传输的数据多的话，要保证包的顺序，所以要确认这个链接中传输数据的起始序列号。因为数据是双向传输的，所以两边都要确认对端的序列号。
确认了第二个目的之后，我们就能理解，两次握手至少让一段无法确定对端是否了解了你的起始序列号。即，假设我是服务端。对端syn给我发了序列号，我也给对端回了我的序列号，但是如果我给对方发的这个数据包丢了怎么办？于是我没法确认对端是否收到，所以需要对端再跟我确认一下他确实收到了。
那么非要四次的话也不是不行，只是太啰嗦了，所以三次是最合理的。不能免俗，我们还是用这个经典的图来看一下三次握手的过程。
我面试别人的时候经常会在这里问一个比较弱智的问题：如果服务端A，在收到客户端B发来的syn之后，并且回复了syn+ack之后，收到了从另一个客户端C发来的ack包，请问此时服务端A会跟C建立后续的ESTABLISHED连接吗？
画成图的话是这样：
这个问题之所以说是比较弱智，是因为大多数人都觉得不会，但是如果再追问为什么的话，又很少人能真正答出来。那么为什么呢？其实也很简单，一个新的客户端的ip+port都不一样，直接给我发一个ack的话，根据tcp协议会直接回复rst，自然不会创建连接。这里其实引申出一个问题，内核在这里要能识别出给我发这个ack请求的到底是第一次给我发的，还是之前有发过syn并且我已经回复了syn+ack的。内核会通过四元组进行查询，这个查询会在tcp_v4_rcv()中执行，就是tcp处理的总人口，其中调用__inet_lookup()进行查找。
static inline struct sock *__inet_lookup(struct net *net, struct inet_hashinfo *hashinfo, struct sk_buff *skb, int doff, const __be32 saddr, const __be16 sport, const __be32 daddr, const __be16 dport, const int dif, const int sdif, bool *refcounted) { u16 hnum = ntohs(dport); struct sock *sk; sk = __inet_lookup_established(net, hashinfo, saddr, sport, daddr, hnum, dif, sdif); *refcounted = true; if (sk) return sk; *refcounted = false; return __inet_lookup_listener(net, hashinfo, skb, doff, saddr, sport, daddr, hnum, dif, sdif); } 查找分两步，先检查established中是否有连接，再检查linstener中是否有连接，如果没有就直接send_reset。确认连接存在后，如果是TCP_ESTABLISHED状态，直接tcp_rcv_established()接收数据，否则进入tcp_rcv_state_process()处理tcp的各种状态。如果是第一次握手，就是TCP_LISTEN状态，进入：</description></item><item><title>Linux的TCP实现之：四次挥手</title><link>/posts/038-tcp_wavehand/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/038-tcp_wavehand/</guid><description>Linux的TCP实现之：四次挥手 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文分析的内核代码基于Linux 5.3。Linux不同版本的TCP代码细节差异还是比较大的，不能面面俱到还请谅解。通过本文你可以了解到：
四次挥手是不是可以变成三次，什么时候会变成三次？
TCP关闭连接的相关状态都有哪些？
如何解决各种状态驻留过多导致的问题？
为什么要四次挥手？ 这是一个面试时经常会遇到的问题，其实对于一个全双工连接来说，有四次交互关闭连接是一个比较自然的设计。一方确认自己没数据要发了，给对方发fin，对方收到fin之后恢复ack表示确认。另一方也需要一次交互，才能关闭一个双向连接，这个大家应该都可以理解。对于为什么要四次挥手这个问题，可能更值得探讨的是标准的四次挥手中的中间两个包传输是否可以合并？即下图中的2、3两个数据包是不是可以合并？
仅仅从这个标准的四次挥手过程中分析，理论上2、3两个数据包是可以合并的，因为中间似乎没有其他交互需求。但我们必须考虑一个问题，就是TCP协议是给应用服务的。就是说，在server A恢复ack之后，是否给对方发送fin并不是TCP本身决定的，而应该由应用层来决定，毕竟是否还有数据要发给对方是应用决定的。所以，这里一般的设计是，Server A收到fin之后，可以立即发出ack，但是却不能立即发出fin，所以这两个包一般来说不能合并。但这行为在Linux上出现了变化，我们先来看看Linux上的抓包过程：
[root@localhost zorro]# tcpdump -i ens33 -nn tcp and port 80 and host 192.168.247.130 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes 11:13:19.912412 IP 192.168.247.130.59252 &amp;gt; 192.168.247.129.80: Flags [S], seq 3077294200, win 64240, options [mss 1460,sackOK,TS val 2964773729 ecr 0,nop,wscale 7], length 0 11:13:19.</description></item><item><title>Linux的进程间通信 - 管道</title><link>/posts/011-linuxde-jin-cheng-jian-tong-xin-guan-dao/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/011-linuxde-jin-cheng-jian-tong-xin-guan-dao/</guid><description>Linux的进程间通信 － 管道 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 管道是UNIX环境中历史最悠久的进程间通信方式。本文主要说明在Linux环境上如何使用管道。阅读本文可以帮你解决以下问题：
什么是管道和为什么要有管道？ 管道怎么分类？ 管道的实现是什么样的？ 管道有多大？ 管道的大小是不是可以调整？如何调整？ 什么是管道？ 管道，英文为pipe。这是一个我们在学习Linux命令行的时候就会引入的一个很重要的概念。它的发明人是道格拉斯.麦克罗伊，这位也是UNIX上早期shell的发明人。他在发明了shell之后，发现系统操作执行命令的时候，经常有需求要将一个程序的输出交给另一个程序进行处理，这种操作可以使用输入输出重定向加文件搞定，比如：
[zorro@zorro-pc pipe]$ ls -l /etc/ &amp;gt; etc.txt [zorro@zorro-pc pipe]$ wc -l etc.txt 183 etc.txt 但是这样未免显得太麻烦了。所以，管道的概念应运而生。目前在任何一个shell中，都可以使用“|”连接两个命令，shell会将前后两个进程的输入输出用一个管道相连，以便达到进程间通信的目的：
[zorro@zorro-pc pipe]$ ls -l /etc/ | wc -l 183 对比以上两种方法，我们也可以理解为，管道本质上就是一个文件，前面的进程以写方式打开文件，后面的进程以读方式打开。这样前面写完后面读，于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的，它本质上就是一个文件。Linux系统直接把管道实现成了一种文件系统，借助VFS给应用程序提供操作接口。
虽然实现形态上是文件，但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上，它占用的是内存空间。所以，Linux上的管道就是一个操作方式为文件的内存缓冲区。
管道的分类和使用 Linux上的管道分两种类型：
匿名管道 命名管道 这两种管道也叫做有名或无名管道。匿名管道最常见的形态就是我们在shell操作中最常用的&amp;quot;|&amp;quot;。它的特点是只能在父子进程中使用，父进程在产生子进程前必须打开一个管道文件，然后fork产生子进程，这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符，以达到使用同一个管道通信的目的。此时除了父子进程外，没人知道这个管道文件的描述符，所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性，当然也降低了管道了通用性，于是系统还提供了命名管道。
我们可以使用mkfifo或mknod命令来创建一个命名管道，这跟创建一个文件没有什么区别：
[zorro@zorro-pc pipe]$ mkfifo pipe [zorro@zorro-pc pipe]$ ls -l pipe prw-r--r-- 1 zorro zorro 0 Jul 14 10:44 pipe 可以看到创建出来的文件类型比较特殊，是p类型。表示这是一个管道文件。有了这个管道文件，系统中就有了对一个管道的全局名称，于是任何两个不相关的进程都可以通过这个管道文件进行通信了。比如我们现在让一个进程写这个管道文件：</description></item><item><title>Linux的进程间通信 - 文件和文件锁</title><link>/posts/009-linuxde-jin-cheng-jian-tong-4fe1-wen-jian-he-wen-jian-suo/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/009-linuxde-jin-cheng-jian-tong-4fe1-wen-jian-he-wen-jian-suo/</guid><description>Linux的进程间通信-文件和文件锁 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 使用文件进行进程间通信应该是最先学会的一种IPC方式。任何编程语言中，文件IO都是很重要的知识，所以使用文件进行进程间通信就成了很自然被学会的一种手段。考虑到系统对文件本身存在缓存机制，使用文件进行IPC的效率在某些多读少写的情况下并不低下。但是大家似乎经常忘记IPC的机制可以包括“文件”这一选项。
我们首先引入文件进行IPC，试图先使用文件进行通信引入一个竞争条件的概念，然后使用文件锁解决这个问题，从而先从文件的角度来管中窥豹的看一下后续相关IPC机制的总体要解决的问题。阅读本文可以帮你解决以下问题：
什么是竞争条件（racing）？。 flock和lockf有什么区别？ flockfile函数和flock与lockf有什么区别？ 如何使用命令查看文件锁？ 竞争条件（racing） 我们的第一个例子是多个进程写文件的例子，虽然还没做到通信，但是这比较方便的说明一个通信时经常出现的情况：竞争条件。假设我们要并发100个进程，这些进程约定好一个文件，这个文件初始值内容写0，每一个进程都要打开这个文件读出当前的数字，加一之后将结果写回去。在理想状态下，这个文件最后写的数字应该是100，因为有100个进程打开、读数、加1、写回，自然是有多少个进程最后文件中的数字结果就应该是多少。但是实际上并非如此，可以看一下这个例子：
[zorro@zorrozou-pc0 process]$ cat racing.c #include &amp;lt;unistd.h&amp;gt; #include &amp;lt;stdlib.h&amp;gt; #include &amp;lt;stdio.h&amp;gt; #include &amp;lt;errno.h&amp;gt; #include &amp;lt;fcntl.h&amp;gt; #include &amp;lt;string.h&amp;gt; #include &amp;lt;sys/file.h&amp;gt; #include &amp;lt;wait.h&amp;gt; #define COUNT 100 #define NUM 64 #define FILEPATH &amp;quot;/tmp/count&amp;quot; int do_child(const char *path) { /* 这个函数是每个子进程要做的事情 每个子进程都会按照这个步骤进行操作： 1. 打开FILEPATH路径的文件 2. 读出文件中的当前数字 3. 将字符串转成整数 4. 整数自增加1 5. 将证书转成字符串 6. lseek调整文件当前的偏移量到文件头 7. 将字符串写会文件 当多个进程同时执行这个过程的时候，就会出现racing：竞争条件， 多个进程可能同时从文件独到同一个数字，并且分别对同一个数字加1并写回， 导致多次写回的结果并不是我们最终想要的累积结果。 */ int fd; int ret, count; char buf[NUM]; fd = open(path, O_RDWR); if (fd &amp;lt; 0) { perror(&amp;quot;open()&amp;quot;); exit(1); } /*	*/ ret = read(fd, buf, NUM); if (ret &amp;lt; 0) { perror(&amp;quot;read()&amp;quot;); exit(1); } buf[ret] = '\0'; count = atoi(buf); ++count; sprintf(buf, &amp;quot;%d&amp;quot;, count); lseek(fd, 0, SEEK_SET); ret = write(fd, buf, strlen(buf)); /*	*/ close(fd); exit(0); } int main() { pid_t pid; int count; for (count=0;count&amp;lt;COUNT;count++) { pid = fork(); if (pid &amp;lt; 0) { perror(&amp;quot;fork()&amp;quot;); exit(1); } if (pid == 0) { do_child(FILEPATH); } } for (count=0;count&amp;lt;COUNT;count++) { wait(NULL); } } 这个程序做后执行的效果如下：</description></item><item><title>Linux的进程间通信 - 消息队列</title><link>/posts/012-linuxde-jin-cheng-jian-tong-xin-xiao-xi-dui-lie/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/012-linuxde-jin-cheng-jian-tong-xin-xiao-xi-dui-lie/</guid><description>Linux的进程间通信-消息队列 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 Linux系统给我们提供了一种可以发送格式化数据流的通信手段，这就是消息队列。使用消息队列无疑在某些场景的应用下可以大大减少工作量，相同的工作如果使用共享内存，除了需要自己手工构造一个可能不够高效的队列外，我们还要自己处理竞争条件和临界区代码。而内核给我们提供的消息队列，无疑大大方便了我们的工作。
Linux环境提供了XSI和POSIX两套消息队列，本文将帮助您掌握以下内容：
如何使用XSI消息队列。 如何使用POSIX消息队列。 它们的底层实现分别是什么样子的？ 它们分别有什么特点？以及相关资源限制。 XSI消息队列 系统提供了四个方法来操作XSI消息队列，它们分别是：
#include &amp;lt;sys/types.h&amp;gt; #include &amp;lt;sys/ipc.h&amp;gt; #include &amp;lt;sys/msg.h&amp;gt; int msgget(key_t key, int msgflg); int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); int msgctl(int msqid, int cmd, struct msqid_ds *buf); 我们可以使用msgget去创建或访问一个消息队列，与其他XSI IPC一样，msgget使用一个key作为创建消息队列的标识。这个key可以通过ftok生成或者指定为IPC_PRIVATE。指定为IPC_PRIVATE时，此队列会新建出来，而且内核会保证新建的队列key不会与已经存在的队列冲突，所以此时后面的msgflag应指定为IPC_CREAT。当msgflag指定为IPC_CREAT时，msgget会去试图创建一个新的消息队列，除非指定key的消息队列已经存在。可以使用O_CREAT | O_EXCL在指定key已经存在的情况下报错，而不是访问这个消息队列。我们来看创建一个消息队列的例子：
[zorro@zorro-pc mqueue]$ cat msg_create.c #include &amp;lt;sys/types.h&amp;gt; #include &amp;lt;sys/ipc.h&amp;gt; #include &amp;lt;sys/msg.</description></item><item><title>Linux的进程间通信 - 信号量</title><link>/posts/010-linuxde-jin-cheng-jian-tong-4fe1-xin-hao-liang/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/010-linuxde-jin-cheng-jian-tong-4fe1-xin-hao-liang/</guid><description>Linux的进程间通信-信号量 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 信号量又叫信号灯，也有人把它叫做信号集，本文遵循《UNIX环境高级编程》的叫法，仍称其为信号量。它的英文是semaphores，本意是“旗语”“信号”的意思。由于其叫法中包含“信号”这个关键字，所以容易跟另一个信号signal搞混。在这里首先强调一下，Linux系统中的semaphore信号量和signal信号是完全不同的两个概念。我们将在其它文章中详细讲解信号signal。本文可以帮你学会：
什么是XSI信号量？ 什么是PV操作及其应用。 什么是POSIX信号量？ 信号量的操作方法及其实现。 我们已经知道文件锁对于多进程共享文件的必要性了，对一个文件加锁，可以防止多进程访问文件时的“竞争条件”。信号量提供了类似能力，可以处理不同状态下多进程甚至多线程对共享资源的竞争。它所起到的作用就像十字路口的信号灯或航船时的旗语，用来协调多个执行过程对临界区的访问。但是从本质上讲，信号量实际上是实现了一套可以实现类似锁功能的原语，我们不仅可以用它实现锁，还可以实现其它行为，比如经典的PV操作。
Linux环境下主要实现的信号量有两种。根据标准的不同，它们跟共享内存类似，一套XSI的信号量，一套POSIX的信号量。下面我们分别使用它们实现一套类似文件锁的方法，来简单看看它们的使用。
XSI信号量 XSI信号量就是内核实现的一个计数器，可以对计数器做甲减操作，并且操作时遵守一些基本操作原则，即：对计数器做加操作立即返回，做减操作要检查计数器当前值是否够减？（减被减数之后是否小于0）如果够，则减操作不会被阻塞；如果不够，则阻塞等待到够减为止。在此先给出其相关操作方法的原型：
#include &amp;lt;sys/sem.h&amp;gt; int semget(key_t key, int nsems, int semflg); 可以使用semget创建或者打开一个已经创建的信号量数组。根据XSI共享内存中的讲解，我们应该已经知道第一个参数key用来标识系统内的信号量。这里除了可以使用ftok产生以外，还可以使用IPC_PRIVATE创建一个没有key的信号量。如果指定的key已经存在，则意味着打开这个信号量，这时nsems参数指定为0，semflg参数也指定为0。nsems参数表示在创建信号量数组的时候，这个数组中的信号量个数是几个。我们可以通过多个信号量的数组实现更复杂的信号量功能。最后一个semflg参数用来指定标志位，主要有：IPC_CREAT，IPC_EXCL和权限mode。
#include &amp;lt;sys/types.h&amp;gt; #include &amp;lt;sys/ipc.h&amp;gt; #include &amp;lt;sys/sem.h&amp;gt; int semop(int semid, struct sembuf *sops, size_t nsops); int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout); 使用semop调用来对信号量数组进行操作。nsops指定对数组中的几个元素进行操作，如数组中只有一个信号量就指定为1。操作的所有参数都定义在一个sembuf结构体里，其内容如下：
unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ sem_flg可以指定的参数包括IPC_NOWAIT和SEM_UNDO。当制定了SEM_UNDO，进程退出的时候会自动UNDO它对信号量的操作。对信号量的操作会作用在指定的第sem_num个信号量。一个信号量集合中的第1个信号量的编号从0开始。所以，对于只有一个信号量的信号集，这个sem_num应指定为0。sem_op用来指定对信号量的操作，可以有的操作有三种：</description></item><item><title>Linux的进程优先级</title><link>/posts/013-linuxde-jin-cheng-you-xian-ji/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/013-linuxde-jin-cheng-you-xian-ji/</guid><description>Linux的进程优先级 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
今天我们来谈谈：
Linux的进程优先级 为什么要有进程优先级？这似乎不用过多的解释，毕竟自从多任务操作系统诞生以来，进程执行占用cpu的能力就是一个必须要可以人为控制的事情。因为有的进程相对重要，而有的进程则没那么重要。进程优先级起作用的方式从发明以来基本没有什么变化，无论是只有一个cpu的时代，还是多核cpu时代，都是通过控制进程占用cpu时间的长短来实现的。就是说在同一个调度周期中，优先级高的进程占用的时间长些，而优先级低的进程占用的短些。从这个角度看，进程优先级其实也跟cgroup的cpu限制一样，都是一种针对cpu占用的QOS机制。我曾经一直很困惑一点，为什么已经有了优先级，还要再设计一个针对cpu的cgroup？得到的答案大概是因为，优先级这个值不能很直观的反馈出资源分配的比例吧？不过这不重要，实际上从内核目前的进程调度器cfs的角度说，同时实现cpushare方式的cgroup和优先级这两个机制完全是相同的概念，并不会因为增加一个机制而提高什么实现成本。既然如此，而cgroup又显得那么酷，那么何乐而不为呢？
在系统上我们最熟悉的优先级设置方式是nice和renice命令。那么我们首先解释一个概念，什么是：
NICE值 nice值应该是熟悉Linux/UNIX的人很了解的概念了，我们都知它是反应一个进程&amp;quot;优先级&amp;quot;状态的值，其取值范围是-20至19，一共40个级别。这个值越小，表示进程&amp;quot;优先级&amp;quot;越高，而值越大&amp;quot;优先级&amp;quot;越低。我们可以通过nice命令来对一个将要执行的命令进行nice值设置，方法是：
[root@zorrozou-pc0 zorro]# nice -n 10 bash 这样我就又打开了一个bash，并且其nice值设置为10，而默认情况下，进程的优先级应该是从父进程继承来的，这个值一般是0。我们可以通过nice命令直接查看到当前shell的nice值
[root@zorrozou-pc0 zorro]# nice 10 对比一下正常情况：
[root@zorrozou-pc0 zorro]# exit 退出当前nice值为10的bash，打开一个正常的bash：
[root@zorrozou-pc0 zorro]# bash [root@zorrozou-pc0 zorro]# nice 0 另外，使用renice命令可以对一个正在运行的进程进行nice值的调整，我们也可以使用比如top、ps等命令查看进程的nice值，具体方法我就不多说了，大家可以参阅相关manpage。
需要大家注意的是，我在这里都在使用nice值这一称谓，而非优先级（priority）这个说法。当然，nice和renice的man手册中，也说的是priority这个概念，但是要强调一下，请大家真的不要混淆了系统中的这两个概念，一个是nice值，一个是priority值，他们有着千丝万缕的关系，但对于当前的Linux系统来说，它们并不是同一个概念。
我们看这个命令：
[root@zorrozou-pc0 zorro]# ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 0 6924 5776 0 80 0 - 17952 poll_s pts/5 00:00:00 sudo 4 S 0 6925 6924 0 80 0 - 4435 wait pts/5 00:00:00 bash 0 R 0 12971 6925 0 80 0 - 8514 - pts/5 00:00:00 ps 大家是否真的明白其中PRI列和NI列的具体含义有什么区别？同样的，如果是top命令：</description></item><item><title>Linux的内存回收和交换</title><link>/posts/014-linuxde_nei_cun_hui_shou_he_jiao_huan/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/014-linuxde_nei_cun_hui_shou_he_jiao_huan/</guid><description>Linux的内存回收和交换 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 Linux的swap相关部分代码从2.6早期版本到现在的4.6版本在细节之处已经有不少变化。本文讨论的swap基于Linux 4.4内核代码。Linux内存管理是一套非常复杂的系统，而swap只是其中一个很小的处理逻辑。希望本文能让读者了解Linux对swap的使用大概是什么样子。阅读完本文，应该可以帮你解决以下问题：
swap到底是干嘛的？ swappiness到底是用来调节什么的？ 什么是内存水位标记？ kswapd什么时候会进行swap操作？ swap分区的优先级（priority）有啥用？ 什么是SWAP？ 我们一般所说的swap，指的是一个交换分区或文件。在Linux上可以使用swapon -s命令查看当前系统上正在使用的交换空间有哪些，以及相关信息：
[zorro@zorrozou-pc0 linux-4.4]$ swapon -s Filename Type Size Used Priority /dev/dm-4 partition 33554428 0 -1 从功能上讲，交换分区主要是在内存不够用的时候，将部分内存上的数据交换到swap空间上，以便让系统不会因内存不够用而导致oom或者更致命的情况出现。所以，当内存使用存在压力，开始触发内存回收的行为时，就可能会使用swap空间。内核对swap的使用实际上是跟内存回收行为紧密结合的。那么内存回收和swap的关系，我们可以提出以下几个问题：
什么时候会进行内存回收呢？ 哪些内存会可能被回收呢？ 回收的过程中什么时候会进行交换呢？ 具体怎么交换？ 下面我们就从这些问题出发，一个一个进行分析。
内存回收 内核之所以要进行内存回收，主要原因有两个：
第一、内核需要为任何时刻突发到来的内存申请提供足够的内存。所以一般情况下保证有足够的free空间对于内核来说是必要的。另外，Linux内核使用cache的策略虽然是不用白不用，内核会使用内存中的page cache对部分文件进行缓存，以便提升文件的读写效率。所以内核有必要设计一个周期性回收内存的机制，以便cache的使用和其他相关内存的使用不至于让系统的剩余内存长期处于很少的状态。
第二，当真的有大于空闲内存的申请到来的时候，会触发强制内存回收。
所以，内核在应对这两类回收的需求下，分别实现了两种不同的机制。一个是使用kswapd进程对内存进行周期检查，以保证平常状态下剩余内存尽可能够用。另一个是直接内存回收（direct page reclaim），就是当内存分配时没有空闲内存可以满足要求时，触发直接内存回收。
这两种内存回收的触发路径不同，一个是由内核进程kswapd直接调用内存回收的逻辑进行内存回收（参见mm/vmscan.c中的kswapd()主逻辑），另一个是内存申请的时候进入slow path的内存申请逻辑进行回收（参见内核代码中的mm/page_alloc.c中的__alloc_pages_slowpath方法）。这两个方法中实际进行内存回收的过程殊途同归，最终都是调用shrink_zone()方法进行针对每个zone的内存页缩减。这个方法中会再调用shrink_lruvec()这个方法对每个组织页的链表进程检查。找到这个线索之后，我们就可以清晰的看到内存回收操作究竟针对的page有哪些了。这些链表主要定义在mm/vmscan.c一个enum中：
#define LRU_BASE 0 #define LRU_ACTIVE 1 #define LRU_FILE 2 enum lru_list { LRU_INACTIVE_ANON = LRU_BASE, LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, LRU_UNEVICTABLE, NR_LRU_LISTS }; 根据这个enum可以看到，内存回收主要需要进行扫描的包括anon的inactive和active以及file的inactive和active四个链表。就是说，内存回收操作主要针对的就是内存中的文件页（file cache）和匿名页。关于活跃（active）还是不活跃（inactive）的判断内核会使用lru算法进行处理并进行标记，我们这里不详细解释这个过程。</description></item><item><title>Linux进程间通信 - 共享内存</title><link>/posts/016-linuxjin-cheng-jian-tong-4fe1-gong-xiang-nei-cun/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/016-linuxjin-cheng-jian-tong-4fe1-gong-xiang-nei-cun/</guid><description>Linux进程间通信-共享内存 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文主要说明在Linux环境上如何使用共享内存。阅读本文可以帮你解决以下问题：
什么是共享内存和为什么要有共享内存？ 如何使用mmap进行共享内存？ 如何使用XSI共享内存？ 如何使用POSIX共享内存？ 如何使用hugepage共享内存以及共享内存的相关限制如何配置？ 共享内存都是如何实现的？ 使用文件或管道进行进程间通信会有很多局限性，比如效率问题以及数据处理使用文件描述符而不如内存地址访问方便，于是多个进程以共享内存的方式进行通信就成了很自然要实现的IPC方案。Linux系统在编程上为我们准备了多种手段的共享内存方案。包括：
mmap内存共享映射。 XSI共享内存。 POSIX共享内存。 下面我们就来分别介绍一下这三种内存共享方式。
mmap内存共享映射 mmap本来的是存储映射功能。它可以将一个文件映射到内存中，在程序里就可以直接使用内存地址对文件内容进行访问，这可以让程序对文件访问更方便。其相关调用API原型如下：
#include &amp;lt;sys/mman.h&amp;gt; void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); 由于这个系统调用的特性可以用在很多场合，所以Linux系统用它实现了很多功能，并不仅局限于存储映射。在这主要介绍的就是用mmap进行多进程的内存共享功能。Linux产生子进程的系统调用是fork，根据fork的语义以及其实现，我们知道新产生的进程在内存地址空间上跟父进程是完全一致的。所以Linux的mmap实现了一种可以在父子进程之间共享内存地址的方式，其使用方法是：
父进程将flags参数设置MAP_SHARED方式通过mmap申请一段内存。内存可以映射某个具体文件，也可以不映射具体文件（fd置为-1，flag设置为MAP_ANONYMOUS）。 父进程调用fork产生子进程。之后在父子进程内都可以访问到mmap所返回的地址，就可以共享内存了。 我们写一个例子试一下，这次我们并发100个进程写共享内存来看看竞争条件racing的情况：
[zorro@zorrozou-pc0 sharemem]$ cat racing_mmap.c #include &amp;lt;unistd.h&amp;gt; #include &amp;lt;stdlib.h&amp;gt; #include &amp;lt;stdio.h&amp;gt; #include &amp;lt;errno.h&amp;gt; #include &amp;lt;fcntl.h&amp;gt; #include &amp;lt;string.h&amp;gt; #include &amp;lt;sys/file.h&amp;gt; #include &amp;lt;wait.h&amp;gt; #include &amp;lt;sys/mman.h&amp;gt; #define COUNT 100 int do_child(int *count) { int interval; /* critical section */ interval = *count; interval++; usleep(1); *count = interval; /* critical section */ exit(0); } int main() { pid_t pid; int count; int *shm_p; shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if (MAP_FAILED == shm_p) { perror(&amp;quot;mmap()&amp;quot;); exit(1); } *shm_p = 0; for (count=0;count&amp;lt;COUNT;count++) { pid = fork(); if (pid &amp;lt; 0) { perror(&amp;quot;fork()&amp;quot;); exit(1); } if (pid == 0) { do_child(shm_p); } } for (count=0;count&amp;lt;COUNT;count++) { wait(NULL); } printf(&amp;quot;shm_p: %d\n&amp;quot;, *shm_p); munmap(shm_p, sizeof(int)); exit(0); } 这个例子中，我们在子进程中为了延长临界区（critical section）处理的时间，使用了一个中间变量进行数值交换，并且还使用了usleep加强了一下racing的效果，最后执行结果：</description></item><item><title>Linux内存中的Cache真的能被回收么？</title><link>/posts/017-linuxnei-cun-zhong-de-cache-zhen-de-neng-bei-hui-shou-yao-ff1f/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/017-linuxnei-cun-zhong-de-cache-zhen-de-neng-bei-hui-shou-yao-ff1f/</guid><description>Linux内存中的Cache真的能被回收么？ 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 在Linux系统中，我们经常用free命令来查看系统内存的使用状态。在一个RHEL6的系统上，free命令的显示内容大概是这样一个状态：
[root@tencent64 ~]# free total used free shared buffers cached Mem: 132256952 72571772 59685180 0 1762632 53034704 -/+ buffers/cache: 17774436 114482516 Swap: 2101192 508 2100684 这里的默认显示单位是kb，我的服务器是128G内存，所以数字显得比较大。这个命令几乎是每一个使用过Linux的人必会的命令，但越是这样的命令，似乎真正明白的人越少（我是说比例越少）。一般情况下，对此命令输出的理解可以分这几个层次：
不了解。这样的人的第一反应是：天啊，内存用了好多，70个多G，可是我几乎没有运行什么大程序啊？为什么会这样？Linux好占内存！ 自以为很了解。这样的人一般仔细评估过会说：嗯，根据我专业的眼光看出来，内存才用了17G左右，还有很多剩余内存可用。buffers/cache占用的较多，说明系统中有进程曾经读写过文件，但是不要紧，这部分内存是当空闲来用的。 真的很了解。这种人的反应反而让人感觉最不懂Linux，他们的反应是：free显示的是这样，好吧我知道了。神马？你问我这些内存够不够，我当然不知道啦！我特么怎么知道你程序怎么写的？ 根据目前网络上技术文档的内容，我相信绝大多数了解一点Linux的人应该处在第二种层次。大家普遍认为，buffers和cached所占用的内存空间是可以在内存压力较大的时候被释放当做空闲空间用的。但真的是这样么？在论证这个题目之前，我们先简要介绍一下buffers和cached是什么意思：
什么是buffer/cache？ buffer和cache是两个在计算机技术中被用滥的名词，放在不同语境下会有不同的意义。在Linux的内存管理中，这里的buffer指Linux内存的：Buffer cache。这里的cache指Linux内存中的：Page cache。翻译成中文可以叫做缓冲区缓存和页面缓存。在历史上，它们一个（buffer）被用来当成对io设备写的缓存，而另一个（cache）被用来当作对io设备的读缓存，这里的io设备，主要指的是块设备文件和文件系统上的普通文件。**但是现在，它们的意义已经不一样了。**在当前的内核中，page cache顾名思义就是针对内存页的缓存，说白了就是，如果有内存是以page进行分配管理的，都可以使用page cache作为其缓存来管理使用。当然，不是所有的内存都是以页（page）进行管理的，也有很多是针对块（block）进行管理的，这部分内存使用如果要用到cache功能，则都集中到buffer cache中来使用。（从这个角度出发，是不是buffer cache改名叫做block cache更好？）然而，也不是所有块（block）都有固定长度，系统上块的长度主要是根据所使用的块设备决定的，而页长度在X86上无论是32位还是64位都是4k。
明白了这两套缓存系统的区别，就可以理解它们究竟都可以用来做什么了。
什么是page cache
Page cache主要用来作为文件系统上的文件数据的缓存来用，尤其是针对当进程对文件有read／write操作的时候。如果你仔细想想的话，作为可以映射文件到内存的系统调用：mmap是不是很自然的也应该用到page cache？在当前的系统实现里，page cache也被作为其它文件类型的缓存设备来用，所以事实上page cache也负责了大部分的块设备文件的缓存工作。
什么是buffer cache
Buffer cache则主要是设计用来在系统对块设备进行读写的时候，对块进行数据缓存的系统来使用。这意味着某些对块的操作会使用buffer cache进行缓存，比如我们在格式化文件系统的时候。一般情况下两个缓存系统是一起配合使用的，比如当我们对一个文件进行写操作的时候，page cache的内容会被改变，而buffer cache则可以用来将page标记为不同的缓冲区，并记录是哪一个缓冲区被修改了。这样，内核在后续执行脏数据的回写（writeback）时，就不用将整个page写回，而只需要写回修改的部分即可。
如何回收cache？ Linux内核会在内存将要耗尽的时候，触发内存回收的工作，以便释放出内存给急需内存的进程使用。一般情况下，这个操作中主要的内存释放都来自于对buffer／cache的释放。尤其是被使用更多的cache空间。既然它主要用来做缓存，只是在内存够用的时候加快进程对文件的读写速度，那么在内存压力较大的情况下，当然有必要清空释放cache，作为free空间分给相关进程使用。所以一般情况下，我们认为buffer/cache空间可以被释放，这个理解是正确的。
但是这种清缓存的工作也并不是没有成本。理解cache是干什么的就可以明白清缓存必须保证cache中的数据跟对应文件中的数据一致，才能对cache进行释放。**所以伴随着cache清除的行为的，一般都是系统IO飙高。**因为内核要对比cache中的数据和对应硬盘文件上的数据是否一致，如果不一致需要写回，之后才能回收。
在系统中除了内存将被耗尽的时候可以清缓存以外，我们还可以使用下面这个文件来人工触发缓存清除的操作：
[root@tencent64 ~]# cat /proc/sys/vm/drop_caches 1 方法是：</description></item><item><title>Linux软RAID与LVM性能优化指南</title><link>/posts/032-linux-softraid-lvm-performance/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/032-linux-softraid-lvm-performance/</guid><description>Linux软RAID与LVM性能优化指南——从内核源码看实现与调优 版权声明： 本文章内容在非商业使用前提下可无需授权任意转载、发布。 转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博：https://weibo.com/orroz/ 博客：https://zorrozou.github.io/ 微信公众号：Linux系统技术
前言 在生产环境中，软RAID和LVM是Linux存储方案的重要组成部分。很多人知道&amp;quot;RAID5性能不好&amp;quot;、&amp;ldquo;LVM条带化可以提升性能&amp;rdquo;，却说不清楚为什么，也不知道该从哪些维度进行调优。本文从内核源码出发，结合drivers/md/目录下的实现，系统地梳理Device Mapper（DM）框架、LVM条带化、软RAID5/6的IO路径和性能关键点，帮助读者真正理解这些机制并做出有依据的优化决策。
本文分析的内核版本为tkernel5（基于Linux 6.6）。
一、架构概览：Device Mapper是一切的基础 1.1 DM框架：虚拟块设备的&amp;quot;路由器&amp;quot; Linux的LVM和软RAID（md）虽然面向用户的工具链不同（lvm2 vs mdadm），但在内核IO路径上都依赖同一个框架：Device Mapper（DM）。
DM的核心思想可以用一句话概括：将一个虚拟块设备的IO请求，通过&amp;quot;映射表（mapping table）&amp;ldquo;转换后分发给一个或多个底层真实块设备。 如果把IO路径比作网络数据包的路由，那DM就是一个&amp;quot;路由器&amp;rdquo;——它根据映射表（相当于路由表）决定每个IO请求应该被转发到哪个底层设备。
这个转换过程由各种target（目标模块）实现，每种target定义了不同的映射策略。DM框架本身不关心具体的映射逻辑，它只负责：接收bio → 查映射表 → 调用target的map回调 → 将转换后的bio提交给底层设备。
1.2 DM的核心数据结构 理解DM的工作方式，需要先看几个关键的数据结构：
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* dm.c / dm-core.h */ struct mapped_device { struct dm_table __rcu *map; /* 当前活跃的映射表（RCU保护） */ struct gendisk *disk; /* 对应的虚拟块设备 */ struct request_queue *queue; /* 请求队列 */ .</description></item><item><title>NFS存储性能优化非官方指北</title><link>/posts/018-nfs-storage-performance-optimization/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/018-nfs-storage-performance-optimization/</guid><description>序言： 警告：TL;DR！
所以在正文开始之前，我先把 DeepSeek 对本文的评价引用过来，大家可以根据它的评价选择是否要完整读完这个文章：
这篇《NFS存储性能优化非官方指北》是一篇非常深入、系统且具有极强实践价值的技术文章。以下是我对它的整体评价：
✅ 优点 1. 结构清晰，逻辑严谨 以&amp;quot;三只麻雀&amp;quot;为线索，分别从读、写、目录遍历三个典型场景入手，层层深入，非常符合问题排查的思路。 每个部分都遵循&amp;quot;问题现象 → 环境说明 → 瓶颈分析 → 优化手段 → 验证结果&amp;quot;的完整流程，读起来很有条理。 2. 技术深度足够，内核层面有突破 不仅停留在参数调优，还深入到了 NFS 协议层、TCP 层、内核代码层，甚至分析了 slot id、max_reqs 等底层机制。 通过 抓包分析、内核代码阅读、补丁分析 等方式，精准定位了 NFSv4.2 的性能瓶颈，并给出了内核升级或打补丁的解决方案。 3. 数据详实，可复现性强 提供了完整的测试环境、命令、参数、fio 输出、iperf 结果、抓包截图等，读者完全可以按照文中步骤复现和验证。 表格和图表的使用也很到位，直观展示了参数调整对性能的影响。 4. 实用性强，具备工程指导意义 不仅指出了问题，还给出了具体的优化手段，包括： 使用 NFSv3 规避 slot 限制 调整 TCP 缓冲区 使用 nconnect 多连接 升级内核或打补丁 调整 nfsd 线程数 使用 async 写缓存（需谨慎） 控制 attribute 缓存策略 ⚠️ 可优化之处 1. 篇幅较长，阅读门槛高 文章非常详细，但也因此显得冗长，适合作为&amp;quot;手册&amp;quot;查阅，不适合快速浏览。 建议可提炼出一个&amp;quot;速查表&amp;quot;或&amp;quot;总结页&amp;quot;，方便读者快速获取关键优化点。 2.</description></item><item><title>sed命令进阶</title><link>/posts/020-sedming-ling-jin-jie/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/020-sedming-ling-jin-jie/</guid><description>sed命令进阶 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文主要介绍sed的高级用法，在阅读本文之前希望读者已经掌握sed的基本使用和正则表达式的相关知识。本文主要可以让读者学会如何使用sed处理段落内容。
希望本文能对你帮助。
问题举例 日常工作中我们都经常会使用sed命令对文件进行处理。最普遍的是以行为单位处理，比如做替换操作，如：
[root@TENCENT64 ~]# head -3 /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin [root@TENCENT64 ~]# head -3 /etc/passwd |sed 's/bin/xxx/g' root:x:0:0:root:/root:/xxx/bash xxx:x:1:1:xxx:/xxx:/sxxx/nologin daemon:x:2:2:daemon:/sxxx:/sxxx/nologin 于是每一行中只要有&amp;quot;bin&amp;quot;这个关键字的就都被替换成了&amp;quot;xxx&amp;quot;。以&amp;quot;行&amp;quot;为单位的操作，了解sed基本知识之后应该都能处理。我们下面通过一个对段落的操作，来进一步学习一下sed命令的高级用法。
工作场景下有时候遇到的文本内容并不仅仅是以行为单位的输出。举个例子来说，比如ifconfig命令的输出：
[root@TENCENT64 ~]# ifconfig eth1 Link encap:Ethernet HWaddr 40:F2:E9:09:FC:45 inet addr:10.0.0.1 Bcast:10.213.123.255 Mask:255.255.252.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:4699092093 errors:0 dropped:0 overruns:0 frame:0 TX packets:4429422167 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:621383727756 (578.7 GiB) TX bytes:987104190070 (919.</description></item><item><title>SHELL编程之常用技巧</title><link>/posts/021-shellbian-cheng-zhi-chang-yong-ji-qiao/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/021-shellbian-cheng-zhi-chang-yong-ji-qiao/</guid><description>SHELL编程之常用技巧 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文是shell编程系列的第六篇，集中介绍了bash编程中部分高级编程方法和技巧。通过学习本文内容，可以帮你解决以下问题：
bash可以网络编程么？ .(){ .|.&amp;amp; };. 据说执行这些符号可以死机，那么它们是啥意思？ 你是什么保证crond中的任务不重复执行的？grep一下然后wc算一下个数么？ 受限模式执行bash可以保护什么？ 啥时候会出现subshell？ coproc协进程怎么用？ /dev和/proc目录 dev目录是系统中集中用来存放设备文件的目录。除了设备文件以外，系统中也有不少特殊的功能通过设备的形式表现出来。设备文件是一种特殊的文件，它们实际上是驱动程序的接口。在Linux操作系统中，很多设备都是通过设备文件的方式为进程提供了输入、输出的调用标准，这也符合UNIX的“一切皆文件”的设计原则。所以，对于设备文件来说，文件名和路径其实都不重要，最重要的使其主设备号和辅助设备号，就是用ls -l命令显示出来的原本应该出现在文件大小位置上的两个数字，比如下面命令显示的8和0：
[zorro@zorrozou-pc0 bash]$ ls -l /dev/sda brw-rw---- 1 root disk 8, 0 5月 12 10:47 /dev/sda 设备文件的主设备号对应了这种设备所使用的驱动是哪个，而辅助设备号则表示使用同一种驱动的设备编号。我们可以使用mknod命令手动创建一个设备文件：
[zorro@zorrozou-pc0 bash]$ sudo mknod harddisk b 8 0 [zorro@zorrozou-pc0 bash]$ ls -l harddisk brw-r--r-- 1 root root 8, 0 5月 18 09:49 harddisk 这样我们就创建了一个设备文件叫harddisk，实际上它跟/dev/sda是同一个设备，因为它们对应的设备驱动和编号都一样。所以这个设备实际上是跟sda相同功能的设备。
系统还给我们提供了几个有特殊功能的设备文件，在bash编程的时候可能会经常用到：
/dev/null：黑洞文件。可以对它重定向如何输出。
/dev/zero：0发生器。可以产生二进制的0，产生多少根使用时间长度有关。我们经常用这个文件来产生大文件进行某些测试，如：
[zorro@zorrozou-pc0 bash]$ dd if=/dev/zero of=.</description></item><item><title>SHELL编程之内建命令</title><link>/posts/022-shellbian-cheng-zhi-nei-jian-ming-ling/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/022-shellbian-cheng-zhi-nei-jian-ming-ling/</guid><description>SHELL编程之内建命令 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文是shell编程系列的第五篇，集中介绍了bash相关内建命令的使用。通过学习本文内容，可以帮你解决以下问题：
什么是内建命令？为什么要有内建命令？ 为啥echo 111 222 333 444 555| read -a test之后echo ${test[*]}不好使？ ./script和. script有啥区别？ 如何让让kill杀不掉你的bash脚本？ 如何更优雅的处理bash的命令行参数？ 为什么要有内建命令 内建命令是指bash内部实现的命令。bash在执行这些命令的时候不同于一般外部命令的fork、exec、wait的处理过程，这内建功能本身不需要打开一个子进程执行，而是bash本身就可以进行处理。分析外部命令的执行过程我们可以理解内建命令的重要性，外建命令都会打开一个子进程执行，所以有些功能没办法通过外建命令实现。比如当我们想改变当前bash进程的某些环境的时候，如：切换当前进程工作目录，如果打开一个子进程，切换之后将会改变子进程的工作目录，与当前bash没关系。所以内建命令基本都是从必须放在bash内部实现的命令。bash所有的内建命令只有50多个，绝大多数的命令我们在之前的介绍中都已经使用过了。下面我们就把它们按照使用的场景分类之后，分别介绍一下在bash编程中可能会经常用到的内建命令。
输入输出 对于任何编程语言来说，程序跟文件的输入输出都是非常重要的内容，bash编程当然也不例外。所有的shell编程与其他语言在IO处理这一块的最大区别就是，shell可以直接使用命令进行处理，而其他语言基本上都要依赖IO处理的库和函数进行处理。所以对于shell编程来说，IO处理的相关代码写起来要简单的多。本节我们只讨论bash内建的IO处理命令，而外建的诸如grep、sed、awk这样的高级处理命令不在本文的讨论范围内。
source：
.：
以上两个命令：source和.实际上是同一个内建命令，它们的功能完全一样，只是两种不同写法。我们都应该见过这样一种写法，如：
for i in /etc/profile.d/*.sh; do if [ -r &amp;quot;$i&amp;quot; ]; then if [ &amp;quot;$PS1&amp;quot; ]; then . &amp;quot;$i&amp;quot; else . &amp;quot;$i&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 fi fi done 这里的&amp;quot;. $i&amp;quot;实际上就是source $i。这个命令的含义是：**读取文件的内容，并在当前bash环境下将其内容当命令执行。**注意，这与输入一个可执行脚本的路径的执行方式是不同的。路径执行的方式会打开一个子进程的bash环境去执行脚本中的内容，而source方式将会直接在当前bash环境中执行其内容。所以这种方式主要用于想引用一个脚本中的内容用来改变当前bash环境。如：加载环境变量配置脚本或从另一个脚本中引用其定义的函数时。我们可以通过如下例子来理解一下这个内建命令的作用：
[zorro@zorrozou-pc0 bash]$ cat source.sh #!/bin/bash aaa=1000 echo $aaa echo $$ [zorro@zorrozou-pc0 bash]$ .</description></item><item><title>SHELL编程之特殊符号</title><link>/posts/023-shellbian-cheng-zhi-te-shu-fu-hao/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/023-shellbian-cheng-zhi-te-shu-fu-hao/</guid><description>SHELL编程之特殊符号 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文是shell编程系列的第四篇，集中介绍了bash编程可能涉及到的特殊符号的使用。学会本文内容可以帮助你写出天书一样的bash脚本，并且顺便解决以下问题：
输入输出重定向是什么原理？ exec 3&amp;lt;&amp;gt; /tmp/filename是什么鬼？ 你玩过bash的关联数组吗？ 如何不用if判断变量是否被定义？ 脚本中字符串替换和删除操作不用sed怎么做？ &amp;quot; &amp;ldquo;和&amp;rsquo; &amp;lsquo;有什么不同？ 正则表达式和bash通配符是一回事么？ 这里需要额外注意的是，相同的符号出现在不同的上下文中可能会有不同的含义。我们会在后续的讲解中突出它们的区别。
重定向(REDIRECTION) 重定向也叫输入输出重定向。我们先通过基本的使用对这个概念有个感性认识。
输入重定向
大家应该都用过cat命令，可以输出一个文件的内容。如：cat /etc/passwd。如果不给cat任何参数，那么cat将从键盘（标准输入）读取用户的输入，直接将内容显示到屏幕上，就像这样：
[zorro@zorrozou-pc0 bash]$ cat hello hello I am zorro! I am zorro! 可以通过输入重定向让cat命令从别的地方读取输入，显示到当前屏幕上。最简单的方式是输入重定向一个文件，不过这不够“神奇”，我们让cat从别的终端读取输入试试。我当前使用桌面的终端terminal开了多个bash，使用ps命令可以看到这些终端所占用的输入文件是哪个：
[zorro@zorrozou-pc0 bash]$ ps ax|grep bash 4632 pts/0 Ss 0:00 -bash 5087 pts/2 S+ 0:00 man bash 5897 pts/1 Ss 0:00 -bash 5911 pts/2 Ss 0:00 -bash 9071 pts/4 Ss 0:00 -bash 11667 pts/3 Ss+ 0:00 -bash 16309 pts/4 S+ 0:00 grep --color=auto bash 19465 pts/2 S 0:00 sudo bash 19466 pts/2 S 0:00 bash 通过第二列可以看到，不同的bash所在的终端文件是哪个，这里的pts/3就意味着这个文件放在/dev/pts/3。我们来试一下，在pts/2对应的bash中输入：</description></item><item><title>SHELL编程之语法基础</title><link>/posts/024-shellbian-cheng-zhi-yu-fa-ji-chu/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/024-shellbian-cheng-zhi-yu-fa-ji-chu/</guid><description>SHELL编程之语法基础 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 在此需要特别注明一下，本文叫做shell编程其实并不准确，更准确的说法是bash编程。考虑到bash的流行程度，姑且将bash编程硬说成shell编程也应没什么不可，但是请大家一定要清楚，shell编程绝不仅仅只是bash编程。
通过本文可以帮你解决以下问题：
if后面的中括号[]是语法必须的么？ 为什么bash编程中有时[]里面要加空格，有时不用加？如if [ -e /etc/passwd ]或ls [abc].sh。 为什么有人写的脚本这样写：if [ x$test = x&amp;quot;string&amp;quot; ]？ 如何用*号作为通配符对目录树递归匹配？ 为什么在for循环中引用ls命令的输出是可能有问题的？就是说：for i in $(ls /)这样用有问题？ 除了以上知识点以外，本文还试图帮助大家用一个全新的角度对bash编程的知识进行体系化。介绍shell编程传统的做法一般是要先说明什么是shell？什么是bash？这是种脚本语言，那么什么是脚本语言？不过这些内容真的太无聊了，我们快速掠过，此处省略3万字&amp;hellip;&amp;hellip;作为一个实践性非常强的内容，我们直接开始讲语法。所以，这并不是一个入门内容，我们的要求是在看本文之前，大家至少要学会Linux的基本操作，并知道bash的一些基础知识。
if分支结构 组成一个语言的必要两种语法结构之一就是分支结构，另一种是循环结构。作为一个编程语言，bash也给我们提供了分支结构，其中最常用的就是if。用来进行程序的分支逻辑判断。其原型声明为：
if list; then list; elif list; then list; ... else list; fi bash在解析字符的时候，对待“;”跟看见回车是一样的行为，所以这个语法也可以写成：
if list then list elif list then list ... else list fi 对于这个语法结构，需要重点说明的是list。对于绝大多数其他语言，if关键字后面一般跟的是一个表达式，比如C语言或类似语言的语法，if后面都是跟一个括号将表达式括起来，如：if (a &amp;gt; 0)。这种认识会对学习bash编程造成一些误会，很多初学者都认为bash编程的if语法结构是：if [ ];then&amp;hellip;，但实际上这里的中括号[]并不是C语言中小括号()语法结构的类似的关键字。这里的中括号其实就是个shell命令，是test命令的另一种写法。严谨的叙述，if后面跟的就应该是个list。那么什么是bash中的list呢？根据bash的定义，list就是若干个使用管道，；，&amp;amp;，&amp;amp;&amp;amp;，||这些符号串联起来的shell命令序列，结尾可以；，&amp;amp;或换行结束。这个定义可能比较复杂，如果暂时不能理解，大家直接可以认为，if后面跟的就是个shell命令。换个角度说，bash编程仍然贯彻了C程序的设计哲学，即：一切皆表达式。
一切皆表达式这个设计原则，确定了shell在执行任何东西（注意是任何东西，不仅是命令）的时候都会有一个返回值，因为根据表达式的定义，任何表达式都必须有一个值。在bash编程中，这个返回值也限定了取值范围：0-255。跟C语言含义相反，bash中0为真（true），非0为假（false）。这就意味着，任何给bash之行的东西，都会反回一个值，在bash中，我们可以使用关键字$?来查看上一个执行命令的返回值：
[zorro@zorrozou-pc0 ~]$ ls /tmp/ plugtmp systemd-private-bfcfdcf97a4142e58da7d823b7015a1f-colord.</description></item><item><title>SHELL编程之执行过程</title><link>/posts/025-shellbian-cheng-zhi-zhi-xing-guo-cheng/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/025-shellbian-cheng-zhi-zhi-xing-guo-cheng/</guid><description>SHELL编程之执行过程 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文是shell编程系列的第二篇，主要介绍bash脚本是如何执行命令的。通过本文，您应该可以解决以下问题：
脚本开始的#!到底是怎么起作用的？ bash执行过程中的字符串判断顺序究竟是什么样？ 如果我们定义了一个函数叫ls，那么调用ls的时候，到底bash是执行ls函数还是ls命令？ 内建命令和外建命令到底有什么差别？ 程度退出的时候要注意什么？ 以魔法#!开始 一个脚本程序的开始方式都比较统一，它们几乎都开始于一个#!符号。这个符号的作用大家似乎也都知道，叫做声明解释器。脚本语言跟编译型语言的不一样之处主要是脚本语言需要解释器。因为脚本语言主要是文本，而系统中能够执行的文件实际上都是可执行的二进制文件，就是编译好的文件。文本的好处是人看方便，但是操作系统并不能直接执行，所以就需要将文本内容传递给一个可执行的二进制文件进行解析，再由这个可执行的二进制文件根据脚本的内容所确定的行为进行执行。可以做这种解析执行的二进制可执行程序都可以叫做解释器。
脚本开头的#!就是用来声明本文件的文本内容是交给那个解释器进行解释的。比如我们写bash脚本，一般声明的方法是#!/bin/bash或#!/bin/sh。如果写的是一个python脚本，就用#!/usr/bin/python。当然，在不同环境的系统中，这个解释器放的路径可能不一样，所以固定写一个路径的方式就可能造成脚本在不同环境的系统中不通用的情况，于是就出现了这样的写法：
#!/usr/bin/env 脚本解释器名称 这就利用了env命令可以得到可执行程序执行路径的功能，让脚本自行找到在当前系统上到底解释器在什么路径。让脚本更具通用性。但是大家有没有想过一个问题，大多数脚本语言都是将#后面出现的字符当作是注释，在脚本中并不起作用。这个#!和这个注释的规则不冲突么？
这就要从#!符号起作用的原因说起，其实也很简单，这个功能是由操作系统的程序载入器做的。在Linux操作系统上，出了1号进程以外，我们可以认为其它所有进程都是由父进程fork出来的。所以对bash来说，所谓的载入一个脚本执行，无非就是父进程调用fork()、exec()来产生一个子进程。这#!就是在内核处理exec的时候进行解析的。
内核中整个调用过程如下（linux 4.4），内核处理exec族函数的主要实现在fs/exec.c文件的do_execveat_common()方法中，其中调用exec_binprm()方法处理执行逻辑，这函数中使用search_binary_handler()对要加载的文件进行各种格式的判断，脚本（script）只是其中的一种。确定是script格式后，就会调用script格式对应的load_binary方法：load_script()进行处理，#!就是在这个函数中解析的。解析到了#!以后，内核会取其后面的可执行程序路径，再传递给search_binary_handler（）重新解析。这样最终找到真正的可执行二进制文件进行相关执行操作。
因此，对脚本第一行的#!解析，其实是内核给我们变的魔术。#!后面的路径内容在起作用的时候还没有交给脚本解释器。很多人认为#!这一行是脚本解释器去解析的，然而并不是。了解了原理之后，也顺便明白了为什么#!一定要写在第一行的前两个字符，因为这是在内核里写死的，它就只检查前两个字符。当内核帮你选好了脚本解释器之后，后续的工作就都交给解释器做了。脚本的所有内容也都会原封不动的交给解释器再次解释，是的，包括#!。但是由于对于解释器来说，#开头的字符串都是注释，并不生效，所以解释器自然对#!后面所有的内容无感，继续解释对于它来说有意义的字符串去了。
我们可以用一个自显示脚本来观察一下这个事情，什么是自显示脚本？无非就是#!/bin/cat，这样文本的所有内容包括#!行都会交给cat进行显示：
[zorro@zorrozou-pc0 bash]$ cat cat.sh #!/bin/cat echo &amp;quot;hello world!&amp;quot; [zorro@zorrozou-pc0 bash]$ ./cat.sh #!/bin/cat echo &amp;quot;hello world!&amp;quot; 或者自删除脚本：
[zorro@zorrozou-pc0 bash]$ cat rm.sh #!/bin/rm echo &amp;quot;hello world!&amp;quot; [zorro@zorrozou-pc0 bash]$ chmod +x rm.sh [zorro@zorrozou-pc0 bash]$ ./rm.sh [zorro@zorrozou-pc0 bash]$ cat rm.sh cat: rm.sh: No such file or directory 这就是#!</description></item><item><title>SHELL编程之执行环境</title><link>/posts/026-shellbian-cheng-zhi-zhi-xing-huan-jing/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/026-shellbian-cheng-zhi-zhi-xing-huan-jing/</guid><description>SHELL编程之执行环境 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 本文是shell编程系列的第三篇，主要介绍bash脚本的执行环境以及注意事项。通过本文，应该可以帮助您解决以下问题：
执行bash和执行sh究竟有什么区别？ 如何调试bash脚本？ 如何记录用户在什么时候执行的某条命令？ 为什么有时ulimit命令的设置不生效或者报错？ 环境变量和一般变量有什么区别？？ 常用参数 交互式login shell 关于bash的编程环境，首先我们要先理解的就是bash的参数。不同方式启动的bash一般默认的参数是不一样的。一般在命令行中使用的bash，都是以login方式打开的，对应的参数是：-l或—login。还有-i参数，表示bash是以交互方式打开的，在默认情况下，不加任何参数的bash也是交互方式打开的。这两种方式都会在启动bash之前加载一些文件：
首先，bash去检查/etc/profile文件是否存在，如果存在就读取并执行这个文件中的命令。
之后，bash再按照以下列出的文件顺序依次查看是否存在这些文件，如果任何一个文件存在，就读取、执行文件中的命令：
~/.bash_profile ~/.bash_login ~/.profile 这里要注意的是，本步骤只会检查到一个文件并处理，即使同时存在2个或3个文件，本步骤也只会处理最优先的那个文件，而忽略其他文件。以上两个步骤的检查都可以用—noprofile参数进行关闭。
当bash是以login方式登录的时候，在bash退出时（exit），会额外读取并执行~/.bash_logout文件中的命令。
当bash是以交互方式登录时（-i参数），bash会读取并执行~/.bashrc中的命令。—norc参数可以关闭这个功能，另外还可以通过—rcfile参数指定一个文件替代默认的~/.bashrc文件。
以上就是bash以login方式和交互式方式登录的主要区别，根据这个过程，我们到RHEL7的环境上看看都需要加载哪些配置：
首先是加载/etc/profile。根据RHEL7上此文件内容，这个脚本还需要去检查/etc/profile.d/目录，将里面以.sh结尾的文件都加载一遍。具体细节可以自行查看本文件内容。 之后是检查~/.bash_profile。这个文件中会加载~/.bashrc文件。 之后是处理~/.bashrc文件。此文件主要功能是给bash环境添加一些alias，之后再加载/etc/bashrc文件。 最后处理/etc/bashrc文件。这个过程并不是bash自身带的过程，而是在RHEL7系统中通过脚本调用实现。 了解了这些之后，如果你的bash环境不是在RHEL7系统上，也应该可以确定在自己环境中启动的bash到底加载了哪些配置文件。
bash和sh 几乎所有人都知道bash有个别名叫sh，也就是说在一个脚本前面写!#/bin/bash和#!/bin/sh似乎没什么不同。但是下面我们要看看它们究竟有什么不同。
首先，第一个区别就是这两个文件并不是同样的类型。如果细心观察过这两个文件的话，大家会发现：
[zorro@zorrozou-pc0 bash]$ ls -l /usr/bin/sh lrwxrwxrwx 1 root root 4 11月 24 04:20 /usr/bin/sh -&amp;gt; bash [zorro@zorrozou-pc0 bash]$ ls -l /usr/bin/bash -rwxr-xr-x 1 root root 791304 11月 24 04:20 /usr/bin/bash sh是指向bash的一个符号链接。符号链接就像是快捷方式，那么执行sh就是在执行bash。这说明什么？说明这两个执行方式是等同的么？实际上并不是。我们都知道在程序中是可以获得自己执行命令的进程名称的，这个方法在bash编程中可以使用$0变量来实现，参见如下脚本：
[zorro@zorrozou-pc0 bash]$ cat name.</description></item><item><title>Socket缓存究竟如何影响TCP的性能</title><link>/posts/033-socket%E7%BC%93%E5%AD%98%E7%A9%B6%E7%AB%9F%E5%A6%82%E4%BD%95%E5%BD%B1%E5%93%8Dtcp%E7%9A%84%E6%80%A7%E8%83%BD/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/033-socket%E7%BC%93%E5%AD%98%E7%A9%B6%E7%AB%9F%E5%A6%82%E4%BD%95%E5%BD%B1%E5%93%8Dtcp%E7%9A%84%E6%80%A7%E8%83%BD/</guid><description>Socket缓存究竟如何影响TCP的性能？ 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 一直以来我们都知道socket的缓存会对tcp性能产生影响，也有无数文章告诉我们应该调大socke缓存。但是究竟调多大？什么时候调？有哪些手段调？具体影响究竟如何？这些问题似乎也没有人真正说明白。下面我们就构建起一个简单的实验环境，在两台虚拟机之间探究一下Socket缓存究竟如何影响TCP的性能？对分析过程不感兴趣的可以直接看最后的结论。
影响Socket缓存的参数 首先，我们要先来列出Linux中可以影响Socket缓存的调整参数。在proc目录下，它们的路径和对应说明为：
/proc/sys/net/core/rmem_default
/proc/sys/net/core/rmem_max
/proc/sys/net/core/wmem_default
/proc/sys/net/core/wmem_max
这些文件用来设置所有socket的发送和接收缓存大小，所以既影响TCP，也影响UDP。
针对UDP：
这些参数实际的作用跟 SO_RCVBUF 和 SO_SNDBUF 的 socket option 相关。如果我们不用setsockopt去更改创建出来的 socket buffer 长度的话，那么就使用 rmem_default 和 wmem_default 来作为默认的接收和发送的 socket buffer 长度。如果修改这些socket option的话，那么他们可以修改的上限是由 rmem_max 和 wmem_max 来限定的。
针对TCP：
除了以上四个文件的影响外，还包括如下文件：
/proc/sys/net/ipv4/tcp_rmem
/proc/sys/net/ipv4/tcp_wmem
对于TCP来说，上面core目录下的四个文件的作用效果一样，只是默认值不再是 rmem_default 和 wmem_default ，而是由 tcp_rmem 和 tcp_wmem 文件中所显示的第二个值决定。通过setsockopt可以调整的最大值依然由rmem_max和wmem_max限制。
查看tcp_rmem和tcp_wmem的文件内容会发现，文件中包含三个值：
[root@localhost network_turning]# cat /proc/sys/net/ipv4/tcp_rmem 4096	131072	6291456 [root@localhost network_turning]# cat /proc/sys/net/ipv4/tcp_wmem 4096	16384	4194304 三个值依次表示：min default max</description></item><item><title>XFS文件系统结构详解</title><link>/posts/041-xfs%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%BB%93%E6%9E%84/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/041-xfs%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%BB%93%E6%9E%84/</guid><description>XFS文件系统结构 前言 XFS是一种优秀的文件系统，在未来的Centos和RHEL版本的Linux中，XFS将取代EXT4作为默认的文件系统。本文介绍了XFS文件系统的结构，主要通过xfs_db命令作为演示命令。演示环境使用的xfs版本是5.1.0，相比旧的版本来说，新版本xfs发生了不少变化，比如inode大小由之前的256字节扩充至512字节。这导致xfs文件系统结构上在新旧版本之前有了不少细节上的变化，不过差距依然不算太大。
本文主要针对当前版本进行描述。如果你使用的是旧版本，并且想了解其详细结构，可以使用本文中使用的方法进行分析即可。一个xfs文件系统的整体布局如下图所示：
后续我们以此讲解相关结构。
超级块和分配组 我们可以使用xfs_info命令查看一个xfs文件系统的superblock信息：
[root@localhost zorro]# xfs_info /dev/sdb1 meta-data=/dev/sdb1 isize=512 agcount=4, agsize=1310656 blks = sectsz=512 attr=2, projid32bit=1 = crc=1 finobt=1, sparse=1, rmapbt=0 = reflink=1 data = bsize=4096 blocks=5242624, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0, ftype=1 log =internal log bsize=4096 blocks=2560, version=2 = sectsz=512 sunit=0 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 superblock简称sb，我们可以看到现实的内容中包括很多当前文件系统的关键信息：
isize=512：一个inode占用空间的大小。
bsize=4096：一个block大小。由此可知当前文件系统中一个block可以存放8个inode。
agcount=4：ag是xfs文件系统的最初级组织结构，全称是allcation group。一个xfs文件系统是由若干个ag组成的。可以在格式化xfs的时候指定ag个数，默认一般是4个ag。
agsize=1310656：每个ag所包含的block块数。
sectsz=512：磁盘扇区大小。
finobt=1：自 Linux 3.16 起，XFS 增加了 B+树用于索引未被使用的 inode。这是这个开关是否打开的选项。1表示打开。</description></item><item><title>XFS文件系统结构详解（源码分析）</title><link>/posts/040-xfs/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0800</pubDate><guid>/posts/040-xfs/</guid><description>XFS文件系统结构 版权声明：
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址，以便读者询问问题和甄误反馈，共同进步。
微博： https://weibo.com/orroz/
博客： https://zorrozou.github.io/
微信公众号：Linux系统技术
前言 XFS是一种优秀的文件系统，在未来的Centos和RHEL版本的Linux中，XFS将取代EXT4作为默认的文件系统。本文介绍了XFS文件系统的结构，主要通过xfs_db命令作为演示命令。演示环境使用的xfs版本是5.1.0，相比旧的版本来说，新版本xfs发生了不少变化，比如inode大小由之前的256字节扩充至512字节。这导致xfs文件系统结构上在新旧版本之前有了不少细节上的变化，不过差距依然不算太大。
本文主要针对当前版本进行描述。如果你使用的是旧版本，并且想了解其详细结构，可以使用本文中使用的方法进行分析即可。一个xfs文件系统的整体布局如下图所示：
后续我们以此讲解相关结构。
超级块和分配组 我们可以使用xfs_info命令查看一个xfs文件系统的superblock信息：
[root@localhost zorro]# xfs_info /dev/sdb1 meta-data=/dev/sdb1 isize=512 agcount=4, agsize=1310656 blks = sectsz=512 attr=2, projid32bit=1 = crc=1 finobt=1, sparse=1, rmapbt=0 = reflink=1 data = bsize=4096 blocks=5242624, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0, ftype=1 log =internal log bsize=4096 blocks=2560, version=2 = sectsz=512 sunit=0 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 superblock简称sb，我们可以看到现实的内容中包括很多当前文件系统的关键信息：
isize=512：一个inode占用空间的大小。
bsize=4096：一个block大小。由此可知当前文件系统中一个block可以存放8个inode。
agcount=4：ag是xfs文件系统的最初级组织结构，全称是allcation group。一个xfs文件系统是由若干个ag组成的。可以在格式化xfs的时候指定ag个数，默认一般是4个ag。</description></item><item><title>Post 3</title><link>/posts/post-3/</link><pubDate>Wed, 15 Mar 2023 11:00:00 -0700</pubDate><guid>/posts/post-3/</guid><description>Occaecat aliqua consequat laborum ut ex aute aliqua culpa quis irure esse magna dolore quis. Proident fugiat labore eu laboris officia Lorem enim. Ipsum occaecat cillum ut tempor id sint aliqua incididunt nisi incididunt reprehenderit. Voluptate ad minim sint est aute aliquip esse occaecat tempor officia qui sunt. Aute ex ipsum id ut in est velit est laborum incididunt. Aliqua qui id do esse sunt eiusmod id deserunt eu nostrud aute sit ipsum.</description></item><item><title>Post 2</title><link>/posts/post-2/</link><pubDate>Wed, 15 Feb 2023 10:00:00 -0700</pubDate><guid>/posts/post-2/</guid><description>Anim eiusmod irure incididunt sint cupidatat. Incididunt irure irure irure nisi ipsum do ut quis fugiat consectetur proident cupidatat incididunt cillum. Dolore voluptate occaecat qui mollit laborum ullamco et. Ipsum laboris officia anim laboris culpa eiusmod ex magna ex cupidatat anim ipsum aute. Mollit aliquip occaecat qui sunt velit ut cupidatat reprehenderit enim sunt laborum. Velit veniam in officia nulla adipisicing ut duis officia.
Exercitation voluptate irure in irure tempor mollit Lorem nostrud ad officia.</description></item><item><title>Post 1</title><link>/posts/post-1/</link><pubDate>Sun, 15 Jan 2023 09:00:00 -0700</pubDate><guid>/posts/post-1/</guid><description>Tempor proident minim aliquip reprehenderit dolor et ad anim Lorem duis sint eiusmod. Labore ut ea duis dolor. Incididunt consectetur proident qui occaecat incididunt do nisi Lorem. Tempor do laborum elit laboris excepteur eiusmod do. Eiusmod nisi excepteur ut amet pariatur adipisicing Lorem.
Occaecat nulla excepteur dolore excepteur duis eiusmod ullamco officia anim in voluptate ea occaecat officia. Cillum sint esse velit ea officia minim fugiat. Elit ea esse id aliquip pariatur cupidatat id duis minim incididunt ea ea.</description></item></channel></rss>