分类 Linux 相关 下的文章

当 CPU 空闲(idle)的时候在干什么

今天学习 Linux schedule 框架的时候, 看到如下的图. 这个图列出了 Linux 内核里面的几个 schedule classes, 以及它们之间的关系.
图片源自于: https://deepdives.medium.com/digging-into-linux-scheduler-47a32ad5a0a8
1_IfRhjQssWLhgJqeItlUAdw.webp

图中各个方块分别表示 Linux 内核的几个 schedule class, 他们按照顺序通过链表的形式连在一起, 当前面的 schedule class 没有需要执行的task的时候, 就去下一个 schedule class 去查找. 当前面所有 schedule class 都没有 task 可以执行的时候, 就去执行 idle 这个虚拟的task.

各个 schedule class 的描述:

  1. Stop Scheduler (SCHED_STOP) - 停止调度器 这个调度类是用于管理特殊的停止任务,这些任务用于在CPU上停止所有其他任务的执行,通常用于实现CPU热插拔和一些系统级别的调试操作。这个调度类的任务具有最高的优先级,可以抢占系统中的任何其他任务。
  1. Deadline Scheduler (SCHED_DEADLINE) - 截止时间调度器 这是一种较新的调度类,它为任务提供了严格的截止时间保证。SCHED_DEADLINE使用了称为“恒定带宽算法”的调度策略,允许任务指定它们的运行周期、运行时间和截止时间。这个调度器试图保证每个任务在其截止时间之前都能完成指定的运行时间,适用于对时间敏感度非常高的任务。
  1. Real-Time Scheduler (RT) - 实时调度器 实时调度类分为两种策略:SCHED_FIFO(先进先出)和SCHED_RR(轮转)。这些策略用于实时任务,这些任务需要快速且可预测的响应时间。在SCHED_FIFO中,只有当运行的实时任务主动放弃CPU或者被更高优先级的实时任务抢占时,它才会停止运行。SCHED_RR与SCHED_FIFO类似,但是它为每个实时任务提供了时间片,使得同一优先级的实时任务可以轮流运行。
  1. Completely Fair Scheduler (CFS) - 完全公平调度器 CFS是Linux内核的默认调度类,用于普通的非实时任务。它的目标是确保所有运行的进程都能获得公平的CPU时间。CFS使用红黑树来跟踪所有可运行的进程,并尽量平等地分配CPU时间。它基于虚拟运行时间的概念,该时间考虑了进程的权重。权重越高的进程获得的CPU时间越多。
  1. Idle Scheduler (SCHED_IDLE) - 空闲调度器 这个调度类用于非常低优先级的任务,只有当系统中没有其他可运行的任务时,这些任务才会被调度执行。它通常用于那些可以在系统空闲时运行的后台任务,例如内存压缩守护进程(kcompactd)。

每个调度类都实现了一组调度器框架定义的通用接口,这些接口包括任务入队、出队、选择下一个运行任务等操作。调度器通过这些接口与调度类交互,以实现不同类型的任务调度需求。

Idle 调度器是如何实现的?

参考这篇文章: https://manybutfinite.com/post/what-does-an-idle-cpu-do/
最终是通过 IA 32 指令集的 HTL(halt) 指令来完成的.
hlt: Halt,使CPU进入halt状态,这是一种低功耗模式。在这种模式下,CPU会停止执行指令,直到下一个中断到来。当系统没有其他任务要执行时,操作系统会执行HLT指令来减少能耗和散热。

查询Linux的最新代码:
https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/irqflags.h#L48
asm volatile("sti": : :"memory");
这段代码的作用是使CPU开始响应外部中断,并且立即进入低功耗的halt状态,直到下一个中断到来。这通常用在系统空闲循环或者其他需要CPU等待事件的场合。由于这段代码会改变CPU的中断使能状态,并且可能影响内存的一致性.

在其他处理器架构中,可能有类似功能但名称不同的指令。例如,在ARM架构中,WFI(Wait for Interrupt)指令和WFE(Wait for Event)指令可以用来让处理器等待外部事件或中断。

Linux 文件特殊权限

最简单的文件看起:
例如:

$ ls -lah
total 948K
drwxrwxr-x  4 supra supra 4.0K Apr 19 21:02 .
drwxrwxr-x 14 supra supra 4.0K Dec  6 21:16 ..
-rw-rw-r--  1 supra supra   69 Nov  6  2022 Dockerfile
-rwxrwxr-x  1 supra supra  16K Apr 19 21:02 a.out
-rwxrwxr-x  1 supra supra  11K Sep 18  2021 hello
-rw-rw-r--  1 supra supra  217 Jun 14  2022 hello.c

读写执行权限

  1. 第一个字符表示文件类型:

    1. -:普通文件
    2. d:目录
    3. l:符号链接

    其他字符表示其他特殊类型的文件,如管道:p、套接字p等。

  2. 接下来的三个字符表示文件所有者(owner)的权限:

    1. r:读权限(可以查看文件内容)
    2. w:写权限(可以修改文件内容)
    3. x:执行权限(可以运行文件作为程序)
  3. 紧接着的三个字符表示文件所属组(group)的权限,含义同上。
  4. 最后三个字符表示其他用户(others)的权限,含义同上。
  5. 如果权限字符被替换为特殊字符,如sS,则表示设置了SUID或SGID位。如果是tT,则表示设置了粘滞位(sticky bit).

在Linux系统中,SUID(Set User ID)是一种特殊的文件权限设置,当应用于可执行文件时,它允许用户以文件所有者的身份运行该文件。通常,当一个程序运行时,它继承了启动它的用户的权限。但是,如果该程序具有SUID权限,那么不管是谁运行它,它都会以文件所有者的权限来执行。

然而,SUID也带来了潜在的安全风险。如果SUID程序存在漏洞,那么它可能被利用来提升权限,因此只有在确实需要时才应该设置SUID权限,并且需要确保相关程序是安全的。系统管理员应该定期审查具有SUID权限的文件,并确保它们来自可信的源并且是最新的。

SUID & SGID

SUID权限通常用八进制代码 4000 表示,或者在 ls -l 命令的输出中,可以看到在文件所有者的执行权限位上有一个 s 字符(例如 -rwsr-xr-x)。

SUID的意义在于它允许用户执行一些通常需要更高权限的操作,而不必给予用户那些权限。这是通过让特定的程序以更高权限(通常是root权限)运行来实现的。

为什么要这么做:

1. 安全性:SUID允许系统管理员授予用户执行特定高权限任务的能力,而不必给用户更广泛的权限。这有助于遵循最小权限原则,减少安全风险。

2. 功能性:某些程序需要访问系统资源或执行任务,这些资源或任务通常只能由系统管理员(root用户)访问或执行。例如,passwd 程序需要修改shadow 文件,该文件包含加密的用户密码,通常只有root用户才有权限修改它。

3. 用户便利性:SUID使得普通用户可以方便地执行某些需要特殊权限的操作,而无需切换到root用户。

Sticky权限

Sticky权限(也称为粘滞位或粘着位)是Linux和类Unix操作系统中的一种特殊权限设置,它可以应用于目录。当一个目录设置了sticky权限,这意味着只有文件的所有者、目录的所有者或者超级用户(root)才能删除或重命名目录中的文件。其他用户即使有该目录的写权限也不能删除或重命名其中不属于他们的文件。

Sticky权限通常用于共享目录,如/tmp,这个目录是所有用户都可以写入的。由于任何用户都可以创建临时文件,没有sticky权限的话,就可能出现一个用户删除或修改了另一个用户的文件的情况。通过设置sticky权限,系统管理员可以防止这种情况发生,确保用户只能管理自己的文件。

设置

有2种: 符号方式和数字方式

符号方式

chmod WhoWhatWhich file | directory
  1. Who - represents identities: u,g,o,a (user, group, other, all)
  2. What - represents actions: +, -, = (add, remove, set exact)
  3. Which - represents access levels: r, w, x, s, t(read, write, execute, SUID/SGID, sticky)

ex:

chmod ug+rw test.txt
chmod g+s 

数字方式

通常使用3位数字分别表示 owner, group, others.

chmod ### file | directory
  1. If the read permission should be set, add 4
  2. If the write permission should be set, add 2
  3. If the execute permission should be set, add 1
特殊权限

使用4位, 第一位代表特殊权限, 后面3位表示 owner, group, others.
SUID = 4
SGID = 2
Sticky = 1

chmod 2770 test.txt

记一段shell 脚本遇到的问题

最近写了一段shell 脚本, 执行过程中遇到一些错误, 这里记录一下.

脚本内容

脚本的用意是: 有 pod 在 kubenetes cluster 里面, 要去 profiling 里面 Java 进程. 所以要先 copy profiler 工具上去, 然后解压缩, 之后找到 Java 进程, 最后 profiling.

下面是改正后的脚本:

#!/bin/bash

#set -x
# Set variables
pod="my_pod_1"
ns="my_ns"
cluster_id="37"
local_profiler="~/work/tools/async-profiler-1.7.1-linux-x64.tar.gz"
base_cmd="/tmp/profiler.sh -e itimer -d 30 -o svg -f /mnt/data/profile_eric.log.html"
# -e alloc | -e lock, -t <tid>, --reverse

# Copy profiler to target pod
echo "Copying profiler to target pod"
kubectl cp "${local_profiler}" "${pod}:/tmp/async-profiler-1.7.1-linux-x64.tar.gz" -n "${ns}" --context="${cluster_id}" -c app

# Unzip the profiler
echo "Unzipping the profiler"
kubectl exec -it "${pod}" -n "${ns}" --context="${cluster_id}" -c app -- tar -xf /tmp/async-profiler-1.7.1-linux-x64.tar.gz -C /tmp/

# Get target pod's Java PID
pid=$(kubectl exec "${pod}" -n "${ns}" --context="${cluster_id}" -c app -- pgrep java)
echo "Target pod Java PID is ${pid}"

# Construct the full command
cmd="${base_cmd} ${pid}"
echo "Command is ${cmd}"

# Run profiling
echo "Running profiling"
kubectl exec "${pod}" -n "${ns}" --context="${cluster_id}" -c app -- ${cmd}

遇到的问题

所有的步骤都能被执行, 但是最后一行代码本应该执行 30 秒, 因为在 profiler 的参数里面设置了 -d 30, 可是每次profiling 工具都是开始执行, 立马结束. 打印执行的命令, 把命令单独执行, 也都没有问题.

问题原因

经过 Shell 脚本大师 Peter Deng 的指点, 加了 set -x 参数, 就可以看出到底执行到了哪一步. 发现在 profiler.sh 里面去 kill -0 $PID 的时候, 发现目标进程不存在, 就结束了. 仔细察看代码, 发现传入的进程号竟然是 $25\r, 但其实应该是25.

在进一步排查为什么是$25\r, 发现最初获取进程号的代码是:

pid=$(kubectl exec -it "${pod}" -n "${ns}" --context="${cluster_id}" -c app -- pgrep java)

里面有 -it, 那么它的返回就是 $25\r, 也就是先是一个命令开始符号, 接着是进程号输出, 然后换行. 这正是一般命令行的输出形式. 所以去掉 -it 就只返回进程号了.

另外一个离奇的是: 虽然变量值是 $25\r, 但是当用 echo 打印的时候, 它只显示 25.

验证

使用下面的代码验证:

#!/bin/bash

num="$34\r"
echo "the num is: ${num}"
echo "the num length is ${#num}"

执行输出的结果:

$sh test.sh
the num is: 4
the num length is 3

获得

  1. 使用 set -x 能帮助很快的去debug
  2. 在 $后面的字符很多都有转义, 要小心注意.

Distroless image 使用经验

最近开始使用 GoogleContainerTools 的 distroless image, 但是遇到了一些问题, 这里记录一下.

distoless image 是啥?

https://github.com/GoogleContainerTools/distroless
GoogleContainer Distroless镜像是一种用于容器化应用程序的轻量级基础镜像,它旨在最小化容器的攻击面和大小。它由Google开发并维护,专为在Kubernetes等容器编排平台上部署的应用程序而设计。

Distroless镜像的一些优点包括:

  1. 最小化攻击面:Distroless镜像只包含应用程序运行所需的最小运行时组件和依赖项,因此减少了潜在的安全风险。它不包含操作系统工具或其他不必要的软件包,使得容器更加安全。
  2. 减小镜像大小:由于Distroless镜像只包含应用程序运行所需的最小组件,因此它们的大小相比包含完整操作系统的基础镜像更小。这可以减少容器的下载时间和存储空间。
  3. 简化部署和维护:Distroless镜像提供了一个简化的部署流程,因为它们不需要管理操作系统的配置或更新。这使得容器化应用程序的部署和维护更加简单和高效。
  4. 与容器编排平台集成:Distroless镜像与Kubernetes等容器编排平台无缝集成,可以轻松地部署和管理大规模的容器化应用程序。

总的来说,Distroless镜像提供了一种安全、高效和简化的方式来容器化应用程序,使开发人员能够更专注于应用程序的开发和部署,而无需担心底层基础设施的细节。

注意: 它默认连shell 都没有, 只有后缀加上 :debug 才有 busybox 的shell. 也没有包管理器, 所以不能安装任何东西.

Dockerfile RUN, CMD, and ENTRYPOINT 指令的 shell 和 exec 形式

https://docs.docker.com/reference/dockerfile/#shell-and-exec-form

RUN, CMD, and ENTRYPOINT 有2种形式: shell 和 exec 形式.
shell 形式: 用字符串形式, 会被shell解释, 例如: RUN echo $HOME
exec 形式: 用数组形式, 不会被shell解释, 例如: RUN ["echo", "$HOME"]

Shell形式依赖于容器内的shell解释器,会增加一定的额外开销,因为每个命令都需要被shell解释器处理。
Exec形式直接将命令传递给Docker守护进程,避免了额外的shell解释器,因此更加高效。

遇到的问题

  1. 由于distroless没有shell, 所以不能用shell形式, 只能用exec形式. 若要使用shell, 要么使用 :debug 版本, 要么自己 copy busybox 到镜像里.
  2. 由于 python 命令和 pip 命令要使用 PYTHONPATH 环境变量, 所以要用shell形式, 但是distroless没有shell, 所以只能用exec形式, 所以只能用 python -mpip install 的形式. 但是这样找不到安装的模块, 要么使用 busybox shell, 要么在 python 代码一开始使用 sys.path 设置需要依赖的文件.
  3. python 版本差异. 由于不能安装东西, 所以只能通过build 环境container安装, 比如安装的东西和最后使用的 distroless image 里面的系统是不是兼容, 要考虑.

Linux 文件系统学习摘要

这是公司内学习 Linux Kernel 的第13章, 关于文件系统的部份.

inode

In Linux and other Unix-like operating systems, an inode (index node) is a data structure that stores information about a file or directory except its name and its actual data. Each file or directory has an inode that contains important metadata about the file or directory.

The information stored in an inode includes:

  1. File size
  2. Device ID
  3. User ID (owner)
  4. Group ID
  5. File permissions
  6. File creation, modification, and access times
  7. Number of links (how many file names point to this inode)
  8. Pointers to the disk blocks that store the file's data

The inode number is a unique identifier for the inode within the filesystem. You can view the inode information of a file or directory using the ls -i or stat command in the terminal.

dentry

In the Linux kernel, a dentry (directory entry) is a data structure that represents a specific inode in the cache. It's a key component of the Virtual File System (VFS) layer, which provides a common interface for all file systems.

The dentry object contains information about the inode, the file name, and pointers to the parent and child dentries, forming a dentry tree that represents the directory hierarchy. This structure allows the kernel to quickly look up files and directories, improving the efficiency of file system operations.

Dentries are stored in a dentry cache (dcache), which keeps track of recently accessed dentries to speed up subsequent file and directory lookups. When a file is accessed, the kernel first checks the dcache. If the dentry is found, the kernel can access the file directly without having to traverse the entire file system, which can significantly improve performance.

stat 命令

stat 命令显示文件, 路径, 文件系统信息. 具体看下面例子.

# 显示 /tmp 目录所在的文件系统信息
$ stat -f /tmp/
File: "/tmp/"
    ID: 7ae4c0a51947813 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 122221576  Free: 73122076   Available: 66895184
Inodes: Total: 31121408   Free: 28709496

例子

当我们新建一个空文件和一个空目录的时候, 可以看到如下:

supra@suprabox:~/Downloads$ touch x
supra@suprabox:~/Downloads$ stat x
  File: x
  Size: 0             Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 10748017    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/   supra)   Gid: ( 1000/   supra)
Access: 2023-12-01 21:05:46.177415933 -0800
Modify: 2023-12-01 21:05:46.177415933 -0800
Change: 2023-12-01 21:05:46.177415933 -0800
 Birth: 2023-12-01 21:05:46.177415933 -0800
supra@suprabox:~/Downloads$ mkdir y
supra@suprabox:~/Downloads$ stat y
  File: y
  Size: 4096          Blocks: 8          IO Block: 4096   directory
Device: fd01h/64769d    Inode: 11153180    Links: 2
Access: (0775/drwxrwxr-x)  Uid: ( 1000/   supra)   Gid: ( 1000/   supra)
Access: 2023-12-01 21:06:28.061716185 -0800
Modify: 2023-12-01 21:06:28.061716185 -0800
Change: 2023-12-01 21:06:28.061716185 -0800
 Birth: 2023-12-01 21:06:28.061716185 -0800

下面是对x这个空文件输出的详细解释:

  1. File: x:文件名是x。
  2. Size: 0:文件大小是0字节,因为你刚创建了这个文件但还没有写入任何内容。
  3. Blocks: 0:文件占用的块数是0,这与文件大小为0是一致的。
  4. IO Block: 4096:文件系统的I/O块大小是4096字节。
  5. regular empty file:这是一个常规的空文件。
  6. Device: fd01h/64769d:文件所在的设备的设备号。
  7. Inode: 10748017:文件的inode号是10748017。
  8. Links: 1:硬链接数是1,表示只有一个文件名指向这个inode。
  9. Access: (0664/-rw-rw-r--):文件的权限是0664,也就是用户(owner)和组(group)有读写权限,其他人(other)只有读权限。
  10. Uid: ( 1000/ supra):文件的所有者的用户ID是1000,用户名是supra。
  11. Gid: ( 1000/ supra):文件的所有者的组ID是1000,组名是supra。
  12. Access: 2023-12-01 21:05:46.177415933 -0800:文件最后一次被访问的时间。
  13. Modify: 2023-12-01 21:05:46.177415933 -0800:文件最后一次被修改的时间。
  14. Change: 2023-12-01 21:05:46.177415933 -0800:文件状态最后一次被改变的时间。
  15. Birth: 2023-12-01 21:05:46.177415933 -0800:文件的创建时间。