CS 之 书

小林coding OS 篇 阅读笔记

Ref

  • https://xiaolincoding.com/os/

三. 操作系统结构

Ref

  • https://xiaolincoding.com/os/2_os_structure/linux_vs_windows.html

Note

1. windows内核和linux内核区别

windows闭源, linux开源

2. 什么是内核?

"作为应用连接外设的桥梁" 管理外设的中间层,让上层应用程序不需要直接与外设交互,而是可以通过内核提供的接口间接操作外设

3. 内核有哪些能力?

进程调度、内存管理、硬件管理、提供系统调用

4. 内核如何工作?

划分内存空间为用户空间,内核空间。 程序运行状态可以分为: 用户态、内核态。 用户态: 程序使用用户空间的时候 内核态: 程序使用内核空间的时候

交互机制: 用户态程序调用系统调用的时候,会产生一个中断, cpu中断用户程序的执行转而执行内核中断处理程序(进入内核态),处理完成后再主动触发中断, 让用户程序恢复执行

5. Linux的设计理念

Multitask(多任务),SMP(对称多处理), ELF(可执行文件链接格式),Monolithic Kernel(宏内核)

6. windows内核结构

windows7和windows10用的内核设计是混合型内核

7. 内核架构分类

宏内核、微内核、混合内核

内存管理

一. 为什么要有虚拟内存?

1. 为什么要有虚拟内存?

为应用程序开发提供统一的虚拟内存地址空间, 避免让应用程序处理直接操作物理内存可能存在的地址冲突问题。

虚拟地址: 程序使用的内存地址叫做虚拟内存地址 物理地址: 物理内存的地址叫做物理内存地址

单片机开发中程序是直接操作物理内存 linux中程序是操作虚拟内存

2. 虚拟内存工作的硬件依赖

虚拟地址通过cpu中的MMU转换为物理地址 MMU: 内存管理单元

3. 操作系统如何管理虚拟地址和物理地址之间的关系?

内存分段、内存分页

4. 什么是内存分段? (https://xiaolincoding.com/os/3_memory/vmem.html#%E5%86%85%E5%AD%98%E5%88%86%E6%AE%B5)

虚拟内存地址=段选择子+段内偏移,根据段选择子中的段号在段表项中找到对应段描述符(含有段基址和段界限),段内偏移加上段基址就是物理地址,同时能够判断段内偏移是否越界

优点: 不存在内部内存碎片,分配连续的内存空间 缺点: 存在外部内存碎片,内存交换效率低(比如段可能很大,一次换入/换出耗时较长)

5. 什么是内存分页? 什么是简单分页? 为什么要有多级页表?

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。 简单分页是指只用1级页表来管理虚拟地址和物理地址之间的映射关系。 多级页表是为了节省内存空间而设计的。因为简单分页需要为每个虚拟页分配一个物理页框,如果虚拟地址空间很大,可能会浪费大量内存。多级页表用比较小的1级页表来覆盖所有虚拟页,后面的n级(n>=2)页表只在需要的时候才分配,节省了内存空间。

关键词: 页表、页号、页内偏移、多级页表

6. TLB(Translation Lookaside Buffer) 作用是什么?

TLB 是一个硬件缓存,用于存储最近使用的虚拟地址到物理地址的映射,以加速地址转换过程。

7. 什么是段页式内存管理?

先分段再分页,虚拟地址=段号+页号+页内偏移

8. 为什么linux要把所有段的基地址设为0?

关键词: 逻辑地址、线性地址(虚拟地址)、物理地址、Intel处理器发展史

9. linux的虚拟地址空间是如何分布的?

每个程序的内存空间中,从高到低分别是: 内核地址、栈地址、文件映射地址、堆地址、BSS地址、数据段、代码段

每个程序虚拟内存中的内核地址,关联的都是相同的物理内存。

10. 虚拟内存有什么作用?

  • 降低应用程序管理内存复杂性: 每个程序有独立的虚拟内存空间,避免开发者处理物理内存地址冲突问题
  • 提供更大的内存地址空间: 支持进程使用大于物理内存的内存空间
  • 提供更好的内存访问安全性: 比如通过页表项中的权限位来限制进程对内存的访问

11. 什么是内存交换?

内存交换是指把不常用的内存页换出到磁盘上,以释放内存空间。linux中使用swap分区来实现内存交换。

12. 什么是缺页中断? 缺页中断是内存访问的哪一步触发的?

在程序试图访问一个没有物理内存映射的虚拟内存页时,会触发缺页中断。缺页中断发生在虚拟地址转换为物理地址的过程中。 CPU尝试访问内存,MMU查询页表发现该虚拟页没有对应物理页映射,于是触发一个缺页中断(一个硬件中断),操作系统捕获到这个中断, 为这个中断处理程序分配物理页,并更新页表,使得虚拟页映射到新分配的物理页。之后,CPU可以继续执行原来的指令,访问到正确的物理内存。

二. malloc是如何分配内存的?

1. 内核空间和用户空间比例

32位系统中, 总空间4G, 内核空间1G, 用户空间3G 64位系统中, 内核空间和用户空间都是128T

2. malloc是系统调用吗? 底层是什么?

malloc是c库函数,底层调用brk()或mmap()系统调用来分配内存。

  • brk(): 调整堆顶指针的位置,用于分配小块内存
  • mmap(): 映射文件或匿名内存到进程的虚拟地址空间,用于分配大块内存

这里内存的大块小块的划分根据不同glibc的实现而不同, 一般阈值是128kB

3. malloc分配的是物理内存吗?

不,是虚拟内存

4. malloc(1)会分配多大内存?

不是1字节,而是预分配更大的空间做内存池待后面使用。

5. 如何查看进程的内存分配情况?

cat /proc/$pid/maps 查看进程的内存映射情况

6. free释放内存,会归还给操作系统吗?

使用brk()分配的内存,free后不会归还,而是缓存待下次使用; 使用mmap()分配的内存,free后会归还。

最终进程结束的时候,不管之前归没归还的资源都会回收

7. 为什么不全部使用mmap来分配内存?

使用brk()分配内存的内存会放到malloc的内存池中缓存使用,后续再分配的时候可能就不需要再走系统调用,减少系统调用次数。另外,之前分配的虚拟内存地址在页表中的物理地址映射可能还存在,复用之前分配的虚拟内存地址的话,还可能减少缺页中断重新映射物理地址的次数

8. 为什么不全部使用brk()分配内存?

brk分配的内存free的时候不会被释放而是被malloc的内存池管理等待复用,比较容易造成内存碎片,如果要分配大块内存的话很可能无法复用内存碎片的空间,而是需要继续增长brk分配的内存,所以对于大块的内存,使用mmap分配比较好,

9. free只传入一个内存地址,如何知道要释放多大内存?

把分配的内存块大小存放在了内存块地址前面的16字节中

10. malloc内存分配器是如何实现的?

https://mp.weixin.qq.com/s/Flt85kKbDEn_XD83mtYxUA?token=1646973705&lang=zh_CN

三. 内存满了,会发生什么?

1. 内核分配的过程是怎么样的?

malloc分配虚拟内存,虚拟内存实际访问的时候如果发现未映射到物理内存,则触发缺页中断,开始分配物理内存。 如果物理内存充足,则直接分配。如果物理内存不足,则唤醒后台内存回收线程kswapd进行后台内存回收(异步),如果回收速度跟不上进程分配内存的速度,则启动直接内存回收(同步), 如果还是没有足够物理内存,则启动OOMOOM会持续杀死物理内存占用较高的进程,直到释放足够的内存。

2. 哪些内存可以回收?

文件页:

  • 干净页: 直接回收
  • 脏页: 先写回磁盘再回收 匿名页: 通过Swap机制回收

3. 回收内存会带来什么性能影响? 如何优化?

回收内存会造成一些磁盘I/O操作,降低系统性能,其中文件页回收比匿名页回收往往性能影响更大。

优化方法:

  • 设置/proc/sys/vm/swappiness选项,调整文件页和匿名页的回收倾向
  • 尽早触发kswapd线程开启异步回收:通过设置min_free_kbytes

4. SMP架构是什么?SMP架构有什么问题?

SMP: 对称多处理器架构。多CPU平等共享系统资源。也称为UMA(Uniform Memory Access)架构

缺点: 总线带宽压力随着核数增大。

5. NUMA架构如何解决SMP架构总线带宽压力过大的问题?

CPU分组,每组CPU用一个Node表示。 "每个 Node 有自己独立的资源,包括内存、IO 等" 当Node的内存不足的时候,有多种回收模式可以选择, 通过/proc/sys/vm/zone_reclaim_mode控制:

  • 0(默认): 先去其他Node找空闲内存
  • 1: 只回收本地
  • 2: 只回收本地, 可写回脏页
  • 4:

注意: 不同的模式是可以组合使用的

6. 如何保护一个进程不被OOM杀掉?

设置/proc/[pid]/oom_score_adj参数, 取值范围:-1000~1000,越小越不容易被杀掉

四. 4G物理内存上,申请8G内存会怎么样?

32位操作系统上: 8G操过了理论上用户态虚拟内存空间上限3G,会申请失败

64位操作系统上: 8G小于物理内存上限128T,可以申请成功。 但是访问虚拟内存的时候,根据是否开启swap机制会有不同结果:

  • 不开启swap机制: 使用的时候,会因物理空间不足,触发OOM,导致进程被杀掉
  • 开启swap机制: 使用的时候,可以通过swap来回收内存,该书中测试是可以正常访问;书中把分配的虚拟内存增大到64G的时候,使用过程中进程还是被OOM杀掉了,因为系统多次回收内存还是不能够获取足够的内存。

总结:

  1. 如果分配的虚拟内存空间操作系统的用户态虚拟内存空间上限(32位操作系统3G)会申请失败
  2. 分配成功后,使用时如果没有开启swap机制,会因为物理空间不足触发OOM,导致进程被杀掉
  3. 如果开启了swap机制,使用超过物理内存的虚拟内存空间时,会通过swap机制回收内存,如果回收能够获取足够的内存,那么就能够正常使用;如果多次回收内存还是不能够获取足够的内存,那么就会触发OOM,导致进程被杀掉

五. 如何避免预读失效和缓存污染的问题?

普通的LRU缓存淘汰算法存在两个问题: 预读失效: 预先读取的页后续没有被使用,却被缓存了且淘汰了热点页 缓存污染: 某些一次性读取的页面后续不再使用,却占用了缓存空间,导致热点页被淘汰

解决预读失效: 通过改进LRU算法,给予访问的页和预读的页不同的优先级。 linux的LRU实现使用两个链表:active list和inactive list,预读的页先加入inactive list头部,只有访问过的页才能加入active list中; MySQL Innodb使用一个链表,链表划分为两个部分young和old,预读的页先插入old头部,只有访问过的页才加入young中。

解决缓存污染: 提高优先级改变的阈值,避免仅仅因为访问一次就改变优先级。 linux的LRU实现中在预读内存页被访问第二次的时候,才从inactive list移到active list中; MySQL Innodb,不仅预读的内存页要访问到第二次,还要判断两次时间间隔是否超过1s, 超过了才会从old移到young中。

什么是预读? 利用空间局部性原理,为了提高性能,操作系统在缓存某个要访问的页的时候,会预先读取并缓存后续的若干页。

六. 深入理解Linux虚拟内存管理

1. 什么是虚拟内存地址?

一个用于关联到物理地址的数据。

32位虚拟内存地址=页目录项(10)+页表项(10)+页内偏移(12)

每个虚拟内存地址用于定位一个虚拟内存空间中一个特定的字节。

2. 为什么要使用虚拟内存地址访问内存?

  • 提供进程隔离: 每个进程都有独立的虚拟内存空间,避免了不同进程直接使用物理地址需要处理的地址冲突的问题。
  • 统一开发者视图,简化开发难度: 每个进程都有独立的虚拟内存空间,避免了开发者需要处理物理内存地址冲突的问题,而且使用连续的大块的虚拟内存空间的时候,不需要考虑底层的物理内存空间是否是连续的问题。把内存管理的复杂性交给操作系统的内存管理模块处理。
  • 支持内存超售: 虚拟内存允许系统分配比物理内存更多的内存
  • 提高安全性: 通过页表项中的权限位来限制进程对内存的访问

3. 进程虚拟内存空间布局

  • https://xiaolincoding.com/os/3_memory/linux_mem.html#_3-%E8%BF%9B%E7%A8%8B%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4

4. Linux进程虚拟内存空间

七. 深入理解linux物理内存管理

笔者认为最好的回答。

一句话: 大端序高字节低地址, 小端序高字节高地址

在默认 从 左到右地址 增大 的情况下。

大端序是 每个字节 从左到右书写. 小端序是 每个字节 从右到左书写.

比如一个单词 abc,三个字符{a,b,c}分别对应16进制数字:0x61,0x62,0x63 把它作为一个数据,对他来说a、b相对于c是高字节,b、c相对于a来说是低字节

大端序为: 0x61,0x62,0x63 小端序为: 0x63,0x62,0x61

工程开发相关

Dockerfile命令

参考资料

Overview

  • 什么是Dockerfile
  • 如何编写Dockerfile
  • Q&A

一. 什么是Dockerfile

Dockerfile 是 组织 Dockerfile命令的文件, Dockerfile命令用来指导docker build 构建docker 镜像的过程,一般Dockerfile命名为Dockerfile,也有命名为dockerfile的。 一般使用方式如:docker build . -t <target-image-name> 其中. 指示开始构建过程的时候以当前路径为初始构建环境路径,-t <target-image-name指定构建的镜像的名字,比如hello:1.0,比如test,镜像名可能包括版本号

二. 如何编写Dockerfile

1. Dockerfile命令语法格式

2. Dockerfile的结构:

一个Dockerfile至少包含一个FROM命令,一般放在最开头,用来指定镜像的基底,如 FROM ubuntu:latest 指定该镜像的构建基于ubuntu:latest镜像 一般会包含一个CMD命令指定从镜像启动容器的时候执行的程序,如CMD ["echo","Hello World!"] 所以可以编写一个简单的镜像的Dockerfile如下:

FROM ubuntu:latest

CMD ["echo","Hello World!"]

三. Q&A

1. Dockerfile中是否能够使用环境变量? 是否能够使用外部设定的环境变量?

2. Dockerfile中COPY命令使用的参数中是否参数名能够使用环境变量参与组成?

3. Dockerfile中的RUN命令和CMD命令的区别是什么?

4. Dockerfile中

Ref

  • ubuntu24上安装: https://www.sysgeek.cn/install-docker-ubuntu/

Q&A

  1. 问题docker ps 失败, 原因可能是 用户不在docker用户组中,使用如下命令把用户加入用户组 sudo usermod -aG docker $USER 如果使用了该命令还没有解决,可能是docker用户组不存在,cat /etc/group |grep docker确认 尝试如下操作:
sudo groupadd docker
sudo gpasswd -a $USER docker
newgrp docker
  1. 通过 registry 服务分发
  2. 通过打包传输解压镜像文件分发
    docker save -o myimage.tar myimage:tag
    # 从压缩文件恢复镜像
    docker load -i myimage.tar
    

docker compose 使用

Overview

  • 参考资料

  • 简介

  • 常用命令和基础概念

  • docker-compose.yml 与 .env 语法

  • Q&A

一.参考资料

二.简介

docker compose 是一种 命令行工具,能够用来管理多容器的docker应用的配置与执行。

一种获取方式是通过给 docker 加入插件的方式获取,这种方式能够使用docker compose ... 形式命令调用。新版本的docker下载后自带该插件

另一种获取docker compose的方式是下载单独的docker-compose 软件,往往用于老版本。

三.常用命令与基础概念

  1. 配置文件:

    docker compose 使用docker-compose.yml文件配置要创建的容器以及网络,通过.env 文件 配置 docker-compose.yml 中用到的环境变量

    这两个文件要在同一个路径下

  2. 启动与关闭

    docker compose up -d #启动

    docker compose down #关闭
    这两个命令要在配置文件同一路径下输入

  3. 查看是否已经根据配置启动了容器们

docker compose ps

四.docker-compose.yml 与.env 语法

(一)..env 语法

<env> = <value>  
# 使用"#" 作为注释符
# <env> 为一个 字符字面量,可以称环境变量名,在该语句中 与<value>绑定
# <value> 一般为一个数 或者为一个字符串
# 可以在docker-compose.yaml中使用${<env>}的方式引用<value>绑定的值

(二).docker-compose.yml语法

示例:

version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASS}
  • 声明docker compose 的文件格式版本

    version : "3"

  • 定义服务

    services:
        <服务名>:
            image: <镜像名>
            environment:
                <容器内环境变量名>:<变量值>
    

六.Q&A

1. wsl中使用在wsl中下载的linux版本docker,使用docker compose up -d的时候报错http: invalid Host header

原因:未知,怀疑是 docker 本身的问题,

发生问题的版本Docker Compose version v2.17.2 ,Docker version 20.10.24, build 297e128

解决方法,参考Docker-compose ERROR [internal] booting buildkit, http: invalid Host header - Stack Overflow

更新docker版本sudo snap refresh docker --channel=latest/edge , 成功解决!

一.镜像,容器,DockerFile的关系

镜像是对容器配置的描述.DockerFile是对镜像配置的描述

docker能够通过镜像启动容器.

docker通过dockerfile配置结合本地资源和基础镜像 编译出 新镜像.

二.容器与 shell,与虚拟机的区别

容器的功能就像是一个独立的机器,但是实际上它只是独立地拥有一套虚拟的文件系统,一套独立的环境变量:

  • 与shell的区别:除了独立环境变量外,容器还虚拟了一套文件系统

  • 与虚拟机的区别:虚拟机不仅仅模拟了文件系统,而是模拟了整个操作系统,包括内存管理,处理器调度等.

一.如何拉取镜像

docker pull <image-label>

if with no version tag,use latest in default

二.如何从镜像启动容器,如何停止容器

(1). 启动容器

docker run [-it] [--name <container-name>] [-d] <image-name>

参数说明:

  • -d :后台运行容器中命令,启动容器后回到主进程,容器独立运行

(2). 停止容器

docker stop <container-name>|<container-id>

停止后的容器内容信息不会被删除,可以再次启动:

docker start <container-name>|<container-id>

三.如何进入容器

  1. 启动时交互式进入容器 docker exec -it <image-name> <bash-path>

    默认使用/bin/bash

    这种方式进入容器一旦通过exit之类的方式登出,则容器将终止

  2. 交互式进入运行中的容器

    docker exec -it <container-name> <bash-path>

    用这种方式可以多次进入一个容器,在容器中开始的主进程的工作结束前容器都不会因为登出而结束

四.如何查看容器信息,镜像信息

a. 查看所有运行中的容器: docker ps

b. 查看所有未清除的容器: docker ps -a

c. 查看某个容器具体信息 : docker inspect <container-name/id>

d. 查看镜像信息 : docker images

e. 查看指定容器的内存、硬盘、网络、cpu等资源的使用情况

# 使用docker stats

f. 查看指定容器占据的硬盘大小

# 把容器commit成镜像
# 然后查看镜像大小

五.删除镜像,删除容器

  1. 删除镜像: docker rmi <image-name[:version-tag]>

    注意:要成功删除镜像,需要删除使用到该镜像的所有容器先. 其次,删除镜像有时候需要指定具体的tag,如果删除了指定tag的某个镜像, 比如删除了redis:1.0,但是如果该镜像有其他的名字比如redis:1,实际上底层是同一个镜像的话,该镜像的内容并不会从docker的存储区种删除. 只有一个镜像失去了它所有的指向方式(所有标签)的时候才会被实际删除

  2. 删除容器: docker rm <container-name>/<container-id>

    注意:容器要先stop,之后才能删除

六.使用docker build编译Dockerfile为镜像

docker build -t <image-name> .

这里面的.表示以当前路径为打包开始的路径,则docker build执行打包镜像操作的时候,使用的相对地址就是就是用. 指定的基地址 来确定绝对地址

编译镜像的过程中往往需要下下载软件,很多软件下载时往往需要科学上网才能在合理时间内下载成功

七. 把容器commit成镜像

docker commit <container-name> <image-name>

提示词

docker有哪些网络模式

反馈

Docker提供了多种网络模式,用于配置容器之间和容器与外部网络之间的通信。以下是Docker中常用的网络模式:

  1. 桥接模式(Bridge):桥接模式是Docker默认的网络模式。在桥接模式下,Docker会创建一个虚拟的网络桥接接口(bridge),并将容器连接到这个桥接接口上。每个容器都会分配一个IP地址,并可以通过容器名称或IP地址相互通信。

  2. 主机模式(Host):主机模式下,容器与宿主机共享网络命名空间。容器将直接使用宿主机的网络接口,不进行网络地址转换(NAT),也不会创建额外的网络接口。这种模式下容器的网络性能会更好,但容器之间的端口冲突可能会出现。

  3. 无网络模式(None):无网络模式下,容器没有网络接口,完全与外部网络隔离。这种模式适用于那些不需要网络连接的容器,比如批处理任务或与外部网络无关的服务。

  4. 容器模式(Container):容器模式允许多个容器共享一个网络命名空间。这意味着这些容器可以使用相同的网络配置,包括IP地址和端口。这种模式适用于需要共享网络栈的容器,比如微服务架构中的多个容器。

  5. 自定义网络模式(Custom):除了上述内置的网络模式,Docker还提供了自定义网络模式的功能。使用自定义网络模式,可以创建具有自定义网络配置的网络,包括子网、网关和DNS等。这种模式适用于复杂的网络拓扑和多容器应用程序的部署。

通过选择适当的网络模式,可以实现容器之间的通信和与外部网络的连接。不同的网络模式适用于不同的应用场景,根据实际需求选择合适的网络模式可以提供更好的网络性能、安全性和灵活性。

宿主机和容器通信的几种方式

  • 容器可以通过宿主机的IP地址和端口号进行通信
  • 容器中,可以使用特殊的DNS名host.docker.internal来访问宿主机。例如,如果你的应用程序在宿主机的 8080 端口上运行,你可以从容器内部使用 http://host.docker.internal:8080 来访问它。
  • 容器可以通过连接到宿主机的网络来访问宿主机。例如,使用--network=host标志启动容器,容器将连接到宿主机的网络,可以通过宿主机的IP地址和端口号进行通信。

搭建与使用 本地DockerRegistry

参考

  1. official guide

Case

#!/bin/bash

# 本脚本用于搭建一个本地的Docker Registry并展示如何使用它。

# 设置变量
REGISTRY_NAME="local-registry"
REGISTRY_PORT="5000"
CONTAINER_INNER_REGISTRY_PORT="5000"
# 指定镜像存储位置
STORAGE_PATH="/path/to/local/registry/storage"
IMAGE_NAME="my-image"
LOCAL_IMAGE_TAG="localhost:${REGISTRY_PORT}/${IMAGE_NAME}"

# 启动 Docker Registry 容器
echo "启动 Docker Registry..."
docker run -d -p "${REGISTRY_PORT}:${CONTAINER_INNER_REGISTRY_PORT}" --restart=always --name "${REGISTRY_NAME}" \
  -v "${STORAGE_PATH}:/var/lib/registry" \
  registry:2

# 等待几秒确保 Docker Registry 启动
sleep 3

# 检查 Registry 是否成功运行
echo "检查 Docker Registry 状态..."
curl -X GET http://localhost:${REGISTRY_PORT}/v2/_catalog

# 标记本地镜像以便推送到本地 Registry
echo "标记镜像为本地 Registry 格式..."
docker tag "${IMAGE_NAME}" "${LOCAL_IMAGE_TAG}"

# 推送镜像到本地 Registry
echo "推送镜像到本地 Registry..."
docker push "${LOCAL_IMAGE_TAG}"

# 从本地 Registry 拉取镜像
echo "从本地 Registry 拉取镜像..."
docker pull "${LOCAL_IMAGE_TAG}"

# 脚本结束
echo "本地 Docker Registry 配置完成。"

坑点 1: 为一个用户安装了 podman 后,其他用户无法使用

环境:

PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"

安装方式: apt

在某个用户 u 安装了 podman 之后,切换到用户 gitea 执行如下命令报错:

gitea@VM-8-17-ubuntu:~$ podman --version
ERRO[0000] XDG_RUNTIME_DIR directory "/run/user/1003" is not owned by the current user

检查发现 1003 是 用户 u 的 uid, 而 用户 gitea 的 uid 是 1004.

解决方法: 暂无

大道,不必独行。

文学,也不应是少数人的艺术。

文学,也不应该只是痛苦,虐与爽皆有所触,顺与逆皆有所悟