The Missing Semester of Your CS Education
前言
The missing semester of your CS education
计算机设计的初衷就是任务自动化,然而学生们却常常陷在大量的重复任务中,或者无法完全发挥出诸如 版本控制、文本编辑器等工具的强大作用。效率低下和浪费时间还是其次,更糟糕的是,这还可能导致数据丢失或 无法完成某些特定任务。
这些主题不是大学课程的一部分:学生一直都不知道如何使用这些工具,或者说,至少是不知道如何高效 地使用,因此浪费了时间和精力在本来可以更简单的任务上。标准的计算机科学课程缺少了这门能让计算变得更简捷的关键课程。
---- from Anish, Jose, and Jon
我也会结合其他资料更加完善地学习。
Shell
sh and bash
有时候远程连接服务器得到的是使用sh的终端,非常不方便,想要换成bash应该如何办呢?
chsh(change shell)是一个用于更改用户登录 Shell 的 Linux 命令。Shell 是一个命令行界面,允许用户与操作系统交互。通过 chsh 命令,用户可以选择切换到不同的 Shell
which bash chsh -s /path/to/bash
Bash Quoting(引号)
Quoting is used to remove the special meaning of certain characters or words to the shell.
Bash中常见的引号有:
-
Escape Character(转义符号)
-
弱去除字符特殊含义的效果:在双引号 ('"') 中会保留引号内所有字符的字面值,但 '$'、'`'、'' 和启用历史扩展时的 '!' 除外。
比如echo "~/tmp/file", 其中
~
并不会保持特殊含义但是例如"$FILENAME"中
$
可以保持特殊含义 -
有强去除字符特殊含义的效果
在程序间创建连接
在 shell 中,程序有两个主要的“流”:它们的输入流和输出流。
当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。
程序与程序之间是单独运行的,一个说明此的现象是:
$ cd /sys/class/backlight/thinkpad_screen $ sudo echo 3 > brightness An error occurred while redirecting file 'brightness' open: Permission denied
sudo
也是一个程序,其执行echo
程序。
shell (权限为当前用户) 会先尝试打开 brightness 文件,但此时操作 shell 的不是根(root)用户,所以系统拒绝了这个打开操作,提示无权限。
更改
echo 3 | sudo tee brightness
tee
- read from standard input and write to standard output and files
-
2>:重定向标准错误 (stderr)
2> 用于将标准错误流重定向到指定的文件或设备。默认情况下,错误信息是输出到屏幕上的,但使用 2> 可以将它们写入到文件或其他地方。
command 2> error.log -
>&2:将输出重定向到标准错误 (stderr)
&2 表示将输出重定向到标准错误流。通常用于将标准输出流 (stdout) 转发到标准错误流 (stderr)。
echo "这是一个错误信息" >&2 -
2>&1(简写为&>):将标准错误流 (stderr) 重定向到标准输出流 (stdout)
2>&1 表示将标准错误流重定向到标准输出流。这意味着错误信息将与正常输出一起发送到同一目标。
command > output.log 2>&1 (or command &> output.log)
xargs
Unix 命令都带有参数,有些命令可以接受"标准输入"(stdin)作为参数。
这些命令可以使用管道不通过xargs
获取参数
$ cat /etc/passwd | grep root
但是大多数命令都不接受标准输入作为参数,只能直接在命令行中写参数,这导致无法用管道命令传递参数。举例来说,echo命令就不接受管道传参。
这个时候需要使用xargs
$ echo "hello world" | xargs echo hello world
难点辨析
>file >2&1
和 2>&1 >file
的区别
在心中可以建立这么个模型:
对于每一个进程在执行时都会打开3个文件,每个文件有对应的文件描述符来方便我们使用:
类型 | 文件描述符 | 默认情况 | 对应文件句柄位置 |
---|---|---|---|
标准输入(standard input) | 0 | 从键盘获得输入 | /proc/slef/fd/0 |
标准输出(standard output) | 1 | 输出到屏幕(即控制台) | /proc/slef/fd/1 |
错误输出(error output) | 2 | 输出到屏幕(即控制台) | /proc/slef/fd/2 |
-
标准输入————> 0
-
1————>标准正确输出
-
2————>标准错误输出
Linux在执行shell命令之前,就会确定好所有的输入输出位置,并且从左到右依次执行重定向的命令。
对于2>&1 >file
,首先执行2>&1
,文件描述符变成了:
1————>标准正确输出 ↑ | | 2
然后再执行>file
:
1————>file 标准正确输出 ↑ | | 2
管道
|
它仅能处理前面一个指令传出的正确输出信息,也就是 standard output 的信息,对于 stdandard error 信息没有直接处理能力。
结合上述知识,这个命令strace echo "Hello" 2>&1 >/dev/null | grep Hello > file2
会将strace echo "Hello"
的正确信息输出到/dev/null
中,错误信息通过管道输出到grep
程序中。
Script
shell 是一个编程环境,所以它具备变量、条件、循环和函数
当你在 shell 中执行命令时,您实际上是在执行一段 shell 可以解释执行的简短代码。
如果你要求 shell 执行某个指令,但是该指令并不是 shell 所了解的编程关键字,那么它会去咨询环境变量 $PATH
'#!'
在计算领域中,Shebang(也称为Hashbang)是一个由井号和叹号构成的字符序列#!,其出现在文本文件的第一行的前两个字符。
在文件中存在Shebang的情况下,类Unix操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有Shebang的文件路径作为该解释器的参数。
脚本并不一定只有用 bash 写才能在终端里调用。比如说,这是一段 Python 脚本,作用是将输入的参数倒序输出:
#!/usr/local/bin/python import sys for arg in reversed(sys.argv[1:]): print(arg)
但是上述写法还是有些问题,因为不同机器Python安装路径是不同的。
在 shebang 行中使用 env 命令是一种好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高了您的脚本的可移植性。
env 会利用PATH 环境变量来进行定位。 例如,使用了 env 的 shebang 看上去是这样的 #!/usr/bin/env python
。
shell 函数和脚本有如下一些不同点:
-
函数只能与 shell 使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
-
函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
-
函数会在当前的 shell 环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
-
与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell 脚本中往往也会包含它们自己的函数定义。
说到这里不得不提一下执行脚本时,source执行脚本和直接执行脚本的区别了:
-
source命令用于在当前shell环境中读取并执行指定文件中的命令。
这说明source命令执行的脚本会对环境变量进行更改。
-
直接执行脚本则如上所述:
#!/bin/bash marco() { export MARCO=$(pwd) } polo() { cd "$MARCO" } 脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
特殊变量
-
空格
shell 脚本中使用空格会起到分割参数的作用
在 bash 中为变量赋值的语法是 foo=bar,访问变量中存储的数值,其语法为 $foo。
需要注意的是,foo = bar (使用空格隔开)是不能正确工作的,因为解释器会调用程序 foo 并将 = 和 bar 作为参数。 -
from this more from this
条件语句
man test
Bash 实现了许多类似的比较操作,您可以查看 test 手册。 在 bash 中进行比较时,尽量使用双方括号 [[ ]] 而不是单方括号 [ ]
if [[ $? -ne 0 ]]; then echo "File $file does not have any foobar, adding one" echo "# foobar" >> "$file" fi
-
同一行的多个命令可以用 ; 分隔。
-
返回值 0 表示正常执行,其他所有非 0 的返回值都表示有错误发生。
-
程序 true 的返回码永远是 0,false 的返回码永远是 1。
false ; echo "This will always run"
我们还可以以脚本的返回值作为条件语句(注意bash中if0才是真)
是的,你完全可以写成 if ! script.sh; then
,因为在 Shell 中,script.sh
就像任何其他命令一样执行,!
操作符会对它的返回状态进行逻辑取反。
-
script.sh
:- 如果你编写了一个名为
script.sh
的脚本,并且该脚本返回一个状态码(通常通过exit
命令或脚本中的最后一条命令的退出状态),那么在调用这个脚本时,Shell 会根据它的返回状态判断是否成功。 - 和所有其他命令一样,如果
script.sh
成功执行,它会返回状态码0
,表示成功;如果执行失败,它会返回非零状态码,表示失败。
- 如果你编写了一个名为
-
使用
!
取反:- 当你使用
if ! script.sh; then
时,!
操作符会对script.sh
的返回状态进行取反:- 如果
script.sh
返回0
(成功),!
将其取反为false
,因此不会进入then
部分。 - 如果
script.sh
返回非零状态(失败),!
将其取反为true
,因此会进入then
部分,执行其后的代码。
- 如果
- 当你使用
假设你有一个简单的脚本 script.sh
,内容如下:
#!/bin/bash # 模拟执行成功或失败 if [ "$1" == "fail" ]; then echo "Script failed!" exit 1 else echo "Script succeeded!" exit 0 fi
这个脚本根据传入的参数决定成功(返回 0
)或失败(返回 1
)。
#!/bin/bash if ! ./script.sh fail; then echo "script.sh failed, handling the error..." fi
- 如果你执行
script.sh fail
,它将返回状态码1
(失败),!
将其取反,条件为真,输出"script.sh failed, handling the error..."
。 - 如果你执行
script.sh
(无参数),它将返回状态码0
(成功),!
将其取反,条件为假,因此不会执行then
块中的代码。
更多的细节
[]
是符合 posix shell 标准的测试命令,[ command ]
等价与 test command
其中更多细节可以使用man test
查看
[[]]
是Bash独有的语法扩展,可以支持正则表达式匹配,&&
,||
等,如:
#!/bin/bash var1=10 var2=20 if [[ $var1 -lt $var2 ]]; then echo "$var1 小于 $var2" else echo "$var1 不小于 $var2" fi # 或者更方便的: str1="apple" str2="banana" if [[ $str1 < $str2 ]]; then echo "$str1 在字典序中位于 $str2 之前" else echo "$str1 在字典序中位于 $str2 之后" fi # 但是不能数值比较,数值比较需要用(()) num1=5 num2=10 if (( num1 < num2 )); then echo "$num1 小于 $num2" fi # 检查字符串是否以 "hello" 开头 if [[ $string == hello* ]]; then echo "字符串以 'hello' 开头" fi # 可以使用 &&(逻辑与)和 ||(逻辑或)进行组合条件判断。 if [[ -f $file && -r $file ]]; then echo "文件存在且可读" fi # 支持使用 =~ 运算符进行正则表达式匹配。 if [[ $string =~ ^[0-9]+$ ]]; then echo "字符串是数字" fi
(())
用于表达式计算,返回0 或 非0,只有0表示真。
#!/bin/bash # 定义变量 a=5 b=3 # 使用 ((...)) 进行算术运算 (( sum = a + b )) (( diff = a - b )) (( prod = a * b )) (( quot = a / b )) # 定义变量 count=0 # 使用 ((...)) 进行自增 (( count++ )) echo "Count after increment: $count" # 输出:Count after increment: 1 # 使用 ((...)) 进行自减 (( count-- )) echo "Count after decrement: $count" # 输出:Count after decrement: 0 a=5 b=3 if (( a > b )); then echo "a is greater than b" else echo "a is not greater than b" fi
识别脚本地址
场景:当我在~/a/b/
目录下写了一个ab.sh,我还在~/a/c/d/
目录下写了一个acd.sh,acd.sh需要作为参数在ab.sh中执行。acd.sh执行时依赖一些~/a/c/d/
目录下的文件file.setting,这个时候acd.sh中可以这么写:
NDIR=$(dirname $0) exec_command '$NDIR/file.setting'
$0
: 在 sh 脚本中,$0 是一个特殊的变量,表示当前脚本的名称(或者调用脚本的路径)。它通常用于获取脚本本身的名字或路径。若有如下测试脚本:
#!/bin/sh echo "The name of this script is: $0"
若你运行该脚本:
$ ./myscript.sh The name of this script is: ./myscript.sh
如果你使用绝对路径调用脚本:
$ /home/user/scripts/myscript.sh The name of this script is: /home/user/scripts/myscript.sh
Bash脚本中子进程变量问题
#!/bin/bash L1="" L2="" FUC1(){ L1="1" L2="2" } FUC1 & PID=$! sleep 1 kill -9 $PID echo "L1: $L1" echo "L2: $L2
最后的输出结果为
L1: L2:
因为 L1 和 L2 是在 主进程 中定义的,而 FUC1 & 是在 子进程(后台进程)中执行的。后台进程的修改不会影响到主进程中的变量。
其中一个解决方法为基于文件进行通信:
#!/bin/bash L1="" L2="" FUC1(){ echo "1" > /tmp/L1 echo "2" > /tmp/L2 } FUC1 & PID=$! sleep 1 kill -9 $PID L1=$(cat /tmp/L1) # 从文件中读取值 L2=$(cat /tmp/L2) echo "L1: $L1" echo "L2: $L2"
tips:
Bash脚本中的函数中可以定义局部变量:使用 local 关键字声明的变量只在函数内部有效,函数结束后,局部变量会自动销毁。
#!/bin/bash my_function() { local my_var="Hello, World!" # 定义局部变量 echo "Inside function: $my_var" } my_function # 调用函数 echo "Outside function: $my_var" # 变量 my_var 在函数外不可访问 # 输出结果: # Inside function: Hello, World! # Outside function:
Bash脚本中字符串变量""问题
echo 输出问题
tmp=$(cat "1 2") echo $tmp # 输出结果会变成:1 2 # 在来个文件中的例子,假设file.txt中有如下数据: # 1 2 3 4 # 5 6 7 8 tmp=$(cat file.txt) echo $tmp # 输出结果会变成:1 2 3 4 5 6 7 8
echo 会自动处理空格和换行符,所以在没有双引号的情况下,Bash 会将变量中的多个空格(或制表符、换行符等空白字符)合并成一个空格,或者将换行符转为一个空格。
所以如果想要看到保留原始格式的输出则要echo "$tmp"
for 循环问题
在bash脚本中以空格分隔的字符串可以用于for循环,如:
tmp="1 2 3 4 5 6" for num in $tmp; do echo $num done # 上述的输出为 # 1 # 2 # 3 # ...
需要注意的是上述写法不能写成for num in "$tmp"
,如果用引号包围变量的话,那么数据就会被看成一个整体,即只循环一次,输出为1 2 3 4 5 6
文件处理(grep sed awk cut tr)
grep [option] pattern file
sed [OPTIONS] 'command' file
awk [options] 'pattern {action}' [file]
上述的file表示文本的来源文件,他们也可以接受从标准输入中获取文本数据。
三者均支持正则表达式,但是对于支持的POSIX 正则表达式风格有所不同需要注意,具体解决方法
同时需要注意awk其可以作为单独一门编程语言使用,在awk的{action}中if
,for
等写法和bash脚本中的有点不一样,同时awk中甚至内置了一些函数。
#!/bin/bash # 初始化 Bash 变量 JPIDS="" JNAMES="" # 使用 jps 列出进程并处理 # -v为传入参数给awk output=$(jps | grep -vE 'Jps' | awk -v JPIDS="$JPIDS" -v JNAMES="$JNAMES" ' { if (index(JPIDS, $1) == 0) { JPIDS = JPIDS " " $1 JNAMES = JNAMES " " $2 } } END { print JPIDS print JNAMES }') # 捕获输出的 JPIDS 和 JNAMES JPIDS=$(echo "$output" | head -n 1) # 获取第一行,即 JPIDS JNAMES=$(echo "$output" | tail -n 1) # 获取第二行,即 JNAMES # 输出最终的 JPIDS 和 JNAMES echo "JPIDS: $JPIDS" echo "JNAMES: $JNAMES"
index() 函数: 返回子串在匹配串上第一次出现的位置,如果没有匹配则返回0
Sed
sed 语法格式
sed [sed 选项] '[sed command1]; [sed command2]; [sed command3]' [输入文件]
sed 选项:
- -n 取消默认的 sed 软件的输出(默认情况下sed会把更改内容打印到终端上,使用-n即代表不要打印到终端),常与 sed 命令的 p 连用(p作用是打印,常用于将范围内的内容打印出来)
- -e 一行命令语句可以执行多条 sed 命令
- -f 选项后面可以接 sed 脚本的文件名
- -r 使用正则拓展表达式,默认情况 sed 只识别基本正则表达式
- -i 直接修改文件内容,而不是输出终端,如果不使用-i 选项 sed 软件只是修改在 内存中的数据,并不影响磁盘上的文件
- -E 开始ERE(扩展正则表达式(Extended Regular Expressions, ERE))
sed command的一般语法格式为:
[范围 操作]
范围包括:
- 空地址,即全文处理。
- 单地址,指定文件某一行。
- /pattern/,被匹配到的每一行。
- 范围区间,如10,20 表示[10,20]行; 10,+5表示第10行向下5行,即[10,15]; 3,$表示3行到最后一行。
- 步长,1~2表示1,3,5,7,9…行。
操作包括:
- a 追加,在指定行后添加一行或多行文本
- c 取代指定的行
- d 删除指定的行
- i 插入 在指定的行前添加一行或多行文本
- s 取代
- p 打印模式空间的内容,通常 p 会与选项-n 一起使用
- …
s 取代操作比较特殊,和其他操作的格式不太一样
具体例子为:
sed '/game/,+1 d' t1.log #删除文件中所有包含game的行,以及它下一行 sed '/game/,$ d' t1.log #删除game这一行到结尾 sed '2~2 d' t1.log #删除偶数行 2,4,6,8 sed ' 11 c OK' t1.log 把11行替换为新数据OK 替换一次 sed 's/替换前字符/替换后字符/' file 全局替换,global 全局替换 sed 's/替换前字符/替换后字符/g' file
Awk
awk 'program' input files
主要是input files,说明可以后面可以接多个文件
Awk程序(program)的结构
awk 程序用单引号包围起来,每个程序由一个单独的 模式–动作 语句 (pattern-action statement) 组成.
每一个 awk 程序(program)都是由一个或多个 模式–动作 语句组成的序列:
pattern { action }
pattern { action }
...
awk 的基本操作是在由输入行组成的序列中, 陆续地扫描每一行, 搜索可以被模式 匹配 (match) 的行.每一个输入行轮流被每一个模式测试. 每匹配一个模式, 对应的动作 (可能包含多个步骤) 就会执行. 然后下一行被读取, 匹配重新开始. 这个过程会一起持续到所有的输入被读取完毕为止.
pattern种类
除 BEGIN, END, expression比较特殊外,下面对其余内容进行解释:
在awk表达式中
!~
和~
是匹配正则表达式的运算符,所以如if ($2 ~ /Aisa/)
是正确的
action种类
语法和C基本一致,放心食用
#倒转数组 awk '{ lines[NR] = $0 } END { i = NR; while (i > 0){ print lines[i]; i = i - 1; } }' # 字符串拼接 awk '{ names = names $1 " " } END { print name }'
内建变量
行和字段
awk依据什么将输入源文本划分为各个输入行(Row),又对每一个输入行划分为字段(Field)呢?
-
RS
(row spilt)定义着输入行的记录分割符,默认为'\n'; 衍生出来NR
(number row), 计数器用于到目前为止读的行数量 -
FS
(field spilt)定义着输入行的字段分割符(若将文本想象成一张表,字段是列上的值),默认为' '; 衍生出来NF
(number field), 记录当前行的字段个数
#!/bin/bash input_file="ps_snapshot.txt" # 将文件按三个换行符分段,并统计每段中包含 'TezChild' 的行数 awk 'BEGIN {RS="\n\n\n"; FS="\n"} { count = 0; columns = ""; # 遍历每一行 for (i = 1; i <= NF; i++) { if ($i ~ /TezChild/) { count++; split($i, fields, " "); if (length(fields) >= 2){ columns = columns fields[2] " "; } } } print count "--" columns }' $input_file > tmp.txt;
内建字符串函数
和C如出一辙
案例
#!/bin/bash # 准备阶段 if [ -z "$1" ]; then echo "请提供一个bash脚本作为参数" exit 1 fi if [ -z "$2" ]; then echo "请提供一个工作目录" exit fi SCRIPT=$1 if [ ! -f "$SCRIPT" ]; then echo "指定脚本不存在: $SCRIPT" exit 1 fi WORKDIR=$2 if [ ! -d "$WORKDIR" ]; then echo "指定工作目录不存在,自动创建: $WORKDIR" mkdir -p "$WORKDIR" if [ $? -ne 0 ]; then echo "创建失败" exit 1 fi fi mkdir -p "$WORKDIR/data" mkdir -p "$WORKDIR/tmp" # 定义临时文件,用于记录已监控的 Java 进程,包含两个字段Java Process PID 和 pidstat PID MONITORED_PROCESSES_FILE="$WORKDIR/tmp/monitored_java_processes.txt" touch $MONITORED_PROCESSES_FILE > $MONITORED_PROCESSES_FILE # 定义文件,用于记录出现过的 Java 进程,包含两个字段Java Process Name 和 Java Process PID JPS_FILE="$WORKDIR/data/jps.csv" touch $JPS_FILE echo "JPID,JNAME" > "$JPS_FILE" # 定义文件,用于记录全部 Java 进程的pidstat PIDSTAT_FILE="$WORKDIR/data/pidstatAll.csv" touch $PIDSTAT_FILE > $PIDSTAT_FILE # 定义函数,用于启动 pidstat 监控 start_monitoring() { local pid=$1 echo "启动 pidstat 监控 Java 进程 PID: $pid" if kill -0 $pid 2>/dev/null; then pidstat -p $pid 1 > "$WORKDIR/tmp/pidstat_$pid.log" & echo "$pid $!" >> $MONITORED_PROCESSES_FILE # 记录 Java 进程和 pidstat 的 PID local name=$(jps | awk -v pid="$pid" '$1 == pid {print $2}') echo "$pid,$name" >> $JPS_FILE # 以csv文件的形式记录出现过的 Java 进程和 Java Name else echo "Java 进程 PID: $pid已结束" fi } # 定义函数,用于停止 pidstat 监控 stop_monitoring() { local pid=$1 local pidstat_pid=$2 echo "停止 pidstat 监控 Java 进程 PID: $pid (pidstat PID: $pidstat_pid)" kill -9 $pidstat_pid } # 定义函数,用于在结束脚本时清除剩余的pidstat clean_monitoring(){ local monitored_pids=$(awk '{print $1}' $MONITORED_PROCESSES_FILE) for pid in $monitored_pids; do local pidstat_pid=$(awk -v pid="$pid" '$1 == pid {print $2}' $MONITORED_PROCESSES_FILE) stop_monitoring $pid $pidstat_pid # 从文件中移除记录 sed -i "/^$pid /d" $MONITORED_PROCESSES_FILE done } # 定义函数,用于轮询是否出现新的 Java 进程 polling_jps(){ # 主循环 while true; do # 获取当前运行的 Java 进程列表 local current_java_pids=$(jps | awk '{print $1}') # 获取已监控的 Java 进程列表 local monitored_pids=$(awk '{print $1}' $MONITORED_PROCESSES_FILE) # 检查是否有新的 Java 进程需要监控 for pid in $current_java_pids; do if ! grep -q "^$pid " $MONITORED_PROCESSES_FILE; then start_monitoring $pid fi done # 检查是否有已结束的 Java 进程,并停止对应的 pidstat for pid in $monitored_pids; do if ! echo "$current_java_pids" | grep -q "^$pid$"; then pidstat_pid=$(awk -v pid="$pid" '$1 == pid {print $2}' $MONITORED_PROCESSES_FILE) stop_monitoring $pid $pidstat_pid # 从文件中移除记录 sed -i "/^$pid /d" $MONITORED_PROCESSES_FILE fi done # 每隔一段时间检查 sleep 1 done } # 采集性能数据 echo "Start sar" sar -o "$WORKDIR/data/sar.dat" 1 >/dev/null 2>&1 & sar_pid=$! echo "Start perf stat" perf stat -e cycles,instructions,branch-misses,L1-dcache-load-misses,L1-icache-load-misses -C 0-127 -A -x , -I 1000 -o "$WORKDIR/data/perf-stat.csv" & perfstat_pid=$! polling_jps & echo "Start polling" polling_pid=$! echo "Start perf record" perf record -e '{cycles,instructions}:S' -g -a -F 99 -o "$WORKDIR/data/perf.data" -- bash "$SCRIPT" # 清除性能采集进程 echo "Kill process" kill -9 $sar_pid kill -9 $perfstat_pid kill -9 $polling_pid clean_monitoring # 处理文件 echo "TIME,UID,PID,%usr,%system,%guest,%wait,%CPU,CPU,Command" > "$PIDSTAT_FILE" PIDSTAT_TMP="$WORKDIR/tmp/pidstatAll.tmp" logs=$(ls "$WORKDIR/tmp" | grep '\.log$') for log in $logs; do cat "$WORKDIR/tmp/$log" >> "$PIDSTAT_TMP" done awk ' /^[0-9]/ && !/UID/ { # 只保留以数字开头的行 $2 = ""; # 删除第二列 $0 = gensub(/^[ \t]+|[ \t]+$/, "", "g"); # 去除行前后多余空格 $0 = gensub(/[ \t]+/, ",", "g"); # 将空格分隔改为逗号分隔 if ($0 !~ /^[,]*$/) print $0; # 过滤掉空行 } ' "$PIDSTAT_TMP" >> "$PIDSTAT_FILE" # 可能存在某些 Java 进程太快结束的现象,所以需要去除掉JPS_FILE中第二列为空的行 JPS_FILE_CLEAN="$WORKDIR/data/jps_clean.csv" awk -F, '$2 != ""' "$JPS_FILE" > "$JPS_FILE_CLEAN"
cut and tr
cut 是一个用于处理文本文件的 Linux 命令行工具,可以用来从每一行中提取指定的列或字段,常用于数据提取和文本处理。它通常与管道(|)结合使用,从标准输入读取数据,并输出结果。
cut 常用选项
- -d:指定分隔符,默认为制表符(Tab),可以指定其他字符,如逗号、空格等。
- -f:指定需要提取的字段(列),字段按分隔符进行划分。
- -c:按字符位置截取,而不是按字段划分。
- -b:按字节位置截取,用于处理二进制数据。
netstat -numeric-ports --numeric-hosts -a --protocol=tcpip | grep tcp #### # 部分输出结果为 # tcp 0 0 127.0.0.1:38166 127.0.0.1:35283 ESTABLISHED # tcp 0 156 192.168.11.128:22 192.168.11.1:55801 ESTABLISHED | cut -c21- | cut -d':' -f2 | cut -d' ' -f1 ##### # 输出结果为 # 38166 # 22
-c21- 表示从第21个字符开始一直取到最后一行
-d':'表示将分割符设置为:
-f2 表示取第2列的元素
- tr 是一个用于转换或删除字符的 Linux 命令行工具。它可以对输入的文本流进行字符替换、删除、压缩、转换大小写等操作,广泛用于文本处理和清理工作。
tr 的常见作用
- 字符替换:将输入中的某些字符替换成其他字符。
echo "ap ple" | tr 'a' 'b'
:bp ple
- 删除字符:删除输入中的指定字符。
echo "ap ple" | tr -d 'a'
:p ple
- 转换大小写:将小写字母转换为大写,或将大写字母转换为小写。
echo "ap ple" | tr 'a-z' 'A-Z'
:AP PLE
- 压缩字符:将重复的字符压缩成一个字符。
echp "ap ple" | tr -s ' '
:ap ple
数组
创建数组
# 定义空的数组 declare -a arr # read 命令在 Bash 中用于从标准输入读取一行数据并将其存储在变量中。如果加上 -a 选项,它会将输入的数据按空格或制表符分隔开来,并将每个值存储到数组中。 # <<< 是 Bash 中的Here String操作符,用于将字符串直接传递给命令的标准输入。它的作用类似于管道,但不同之处在于它直接将右侧的字符串作为标准输入传递给命令。 read -a arr <<< "one two three" # or echo "one two three" | read -a arre
访问数组
# 数组下标从0开始 echo ${arrName[3]} # 打印数组全部内容 echo ${arrName[*]} 或 echo ${arrName[@]} # 打印数组长度 echo ${#arrName[*]}
案例:
#!/bin/bash # 假设 JPIDS 和 JNAMES 已经定义 JPIDS="2479104 2479901 2479373 2482523 2481067 2481354 533235 539064 540647 541580" JNAMES="NameNode SecondaryNameNode DataNode RunJar ResourceManager NodeManager RunJar DAGAppMaster DAGAppMaster DAGAppMaster" # 输出文件 OUTPUT_FILE="file.csv" # 写入表头 echo "JPID,JNAME" > "$OUTPUT_FILE" # 将变量转化为数组 read -a PIDS_ARRAY <<< "$JPIDS" read -a NAMES_ARRAY <<< "$JNAMES" # 遍历数组并写入文件 # for(())是不同于for i in xxx;的另一种写法 for ((i = 0; i < ${#PIDS_ARRAY[@]}; i++)); do echo "${PIDS_ARRAY[i]},${NAMES_ARRAY[i]}" >> "$OUTPUT_FILE" done echo "Data written to $OUTPUT_FILE"
Shell工具
-
手册内容太过详实,让我们难以在其中查找哪些最常用的标记和语法。 TLDR pages 是一个很不错的替代品
sudo apt install tldr tldr -u tldr ls -
编写 bash 脚本有时候会很别扭和反直觉。例如 shellcheck 这样的工具可以帮助你定位 sh/bash 脚本中的错误。
sudo apt install shellcheck
查找
查找文件
- find or fd
查找代码
- grep or rg
查找历史命令
- history or Ctrl+R回溯
命令行编辑器Vim
资料
Vim 剪贴板里面的东西 粘贴到系统粘贴板?
数据整理
-
SSH传输数据
ssh myserver journalctl | grep sshd | grep "Disconnected from" | less or ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less 上述两个写法是有差异的,打了引号的写法表示让命令在ssh连接的远程服务器Shell中先执行,然后将结果传到本地
没有打引号的则是就只执行ssh myserver journalctl
后将结果传到本地 -
正则表达式
捕获组:在正则表达式中位于"()"之中的正则项即为捕获组
-
sed 行流编辑器
sed擅长利用正则表达式匹配行文本,并且对于每行(换行后定义为一行)匹配到的文本进行替换。
-
awk 列流编辑器
awk实际上是一个编程语言,其擅长处理列文本,利用分隔符定义每列,利用正则表达式匹配每列,进行相应操作。
命令行环境
任务控制
大多数情况下,我们可以使用 Ctrl-C 来停止命令的执行。但是它的工作原理是什么呢?为什么有的时候会无法结束进程?
shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。
当我们输入 Ctrl-C 时,shell 会发送一个 SIGINT 信号到进程。
当无法结束进程时,说明可能程序对SIGINT信息进行了异常处理,并不会因为此信号结束程序:
#!/usr/bin/env python import signal, time def handler(signum, time): print("\nI got a SIGINT, but I am not stopping") signal.signal(signal.SIGINT, handler) i = 0 while True: time.sleep(.1) print("\r{}".format(i), end="") i += 1
如上述脚本,捕捉了SIGINT信息。
-
如果要选择最近的一个任务,可以使用 $! 这一特殊参数。
-
后台的进程仍然是您的终端进程的子进程,一旦您关闭终端(会发送另外一个信号 SIGHUP),这些后台的进程也会终止。
为了防止这种情况发生,您可以使用 nohup(一个用来忽略 SIGHUP 的封装)来运行程序。针对已经运行的程序,可以使用 disown 。
或者使用tmux,当我们再远程使用tmux后,即使本地与远程断开了连接,tmux也不会中断运行
配置文件(Dotfiles)
我们应该如何管理这些配置文件呢,它们应该在它们的文件夹下,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。
SSH
SSH免密登入
使用 ssh-keygen 命令可以生成一对密钥:ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519
使用 ssh-keygen 命令来查看密钥是否受密码保护。执行以下命令:ssh-keygen -y -f ~/.ssh/id_ed25519
-
如果密钥文件受密码保护,它会提示你输入密码。
-
如果没有提示密码输入,则密钥文件没有设置密码保护。
为密钥添加密码:ssh-keygen -p -f ~/.ssh/id_ed25519
若希望登入远程服务器无需总输入密码:
cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'
or
ssh-copy-id foobar@remote
将本地公钥添加到服务器 ~/.ssh/authorized_keys
中
若还希望每次使用密钥不要总输入密码,则使用ssh-agent(好吧,使用密钥要输入密码是因为在生成密钥的时候指定了密码,平常使用中生成密钥的时候不指定密码就没有这个问题)
ssh-agent
是一个用于管理 SSH 私钥的程序,它可以在本地系统中存储私钥,并在需要时提供给 SSH 客户端,无需每次使用私钥时手动输入密码。它的主要作用如下:
-
管理私钥:
ssh-agent
会在内存中缓存你添加的私钥。这样你只需要在启动ssh-agent
后一次性输入私钥的密码,之后在当前会话中所有 SSH 连接都可以自动使用这个私钥,而无需重复输入密码。 -
简化 SSH 登录和操作:
如果你频繁连接多个远程服务器,ssh-agent
可以显著减少输入密码的次数,提升效率。尤其是在私钥有密码保护的情况下,它避免了每次使用 SSH 命令时都要输入私钥密码。 -
提供安全性:
ssh-agent
存储在内存中的私钥不会被写入磁盘,因此私钥的安全性得到了保证。你也可以在会话结束后,手动删除私钥或终止ssh-agent
,以确保私钥不会被滥用。
ssh-agent
启动后,会生成一个运行中的后台进程,管理你的 SSH 私钥。- 使用
ssh-add
命令将私钥添加到ssh-agent
,并输入密码解锁私钥。私钥会被缓存,以便以后使用。 - 当你执行 SSH 相关命令(如
ssh
,scp
,rsync
等)时,系统会自动从ssh-agent
获取相应的私钥并进行身份验证。
-
启动
ssh-agent
:
在终端中启动ssh-agent
,并将它的输出结果加入到当前环境中:eval $(ssh-agent) 这样会启动
ssh-agent
进程并设置必要的环境变量。 -
添加私钥到
ssh-agent
:
使用ssh-add
命令将私钥添加到ssh-agent
中进行管理:ssh-add ~/.ssh/id_rsa 你会被要求输入私钥的密码,输入后,
ssh-agent
会缓存解密的私钥。 -
查看已添加的私钥:
你可以使用以下命令查看已经添加到ssh-agent
中的所有私钥:ssh-add -l -
删除私钥:
如果你希望从ssh-agent
中删除所有私钥,可以使用以下命令:ssh-add -D 这会从
ssh-agent
中移除所有缓存的私钥。
端口转发
SSH 端口转发(SSH Port Forwarding),也叫 SSH 隧道(SSH Tunneling),是一种通过 SSH 安全协议将本地或远程端口的数据流重定向到另一台机器的特定端口的技术。
它允许用户通过加密的 SSH 连接来转发网络流量,通常用于访问被防火墙阻止的服务,或者通过加密通道访问不安全的服务。
SSH 端口转发分为三种类型:
-
本地端口转发
-
远程端口转发
-
动态端口转发
运用场景
比如我在远程服务器开了个服务在localhost:8888,我希望在本地我能够用浏览器通过本地9999端口访问这个服务。
ssh -L 9999:localhost:8888 foobar@remote_server
-
ssh
:- SSH 命令,用于通过安全通道连接远程服务器。
-
-L 9999:localhost:8888
:-L
表示本地端口转发。这里设置了本地设备的9999
端口和远程服务器的localhost:8888
之间的转发。9999
是你本地设备上将要监听的端口。localhost:8888
是远程服务器上运行的 Jupyter Notebook 服务的地址和端口。- 这样做的效果是:你在本地访问
localhost:9999
,实际上就是通过 SSH 隧道访问远程服务器上的localhost:8888
。
-
foobar@remote_server
:- 这是连接到远程服务器的 SSH 用户名和服务器地址,表示你使用用户名
foobar
登录远程服务器remote_server
。
- 这是连接到远程服务器的 SSH 用户名和服务器地址,表示你使用用户名
-
建立 SSH 隧道:通过
ssh -L
建立本地端口转发的隧道。 -
本地端口转发:SSH 客户端监听本地端口
9999
,当你访问本地localhost:9999
时,SSH 会把请求通过隧道转发到远程服务器的localhost:8888
端口。 -
访问服务:通过 SSH 隧道,你的浏览器最终访问到远程服务器上运行的 Jupyter Notebook。
实践
我希望本地连接本地虚拟机上的开启的Web服务:
-
python -m http.server 8888 &
在虚拟机上开启Web服务 -
ip a
得知虚拟机上的ip地址为:192.168.32.128
-
在本地Shell上:
ssh -L 9999:localhost:8888 cilinmengye@192.168.32.128
-
在本地浏览器上输入:
http://localhost:9999/
-
访问成功!
SSH通过配置文件简化登入
有时我们通过ssh连接远程服务器的命令很长:ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@remote_server
我们可以通过配置文件简化:
vim ~/.ssh/config Host vm User foobar HostName 172.16.174.141 Port 2222 IdentityFile ~/.ssh/id_ed25519 LocalForward 9999 localhost:8888
我们只需要键入:ssh vm
Windows连接本地Linux虚拟机
ssh cilinmengye@192.168.32.128
然后将公钥添加到Linux虚拟机的~/.ssh/authorized_keys中,避免每次我连接的时候要输入Linux用户密码
我的公钥保存在"C:\Users\次林梦叶.ssh\id_ed25519.pub"
但是我的密钥也有密码,为了避免每次我还要输入密钥密码,需要将秘钥交给ssh-agent保管,在windows上的操作方法是:
-
以管理员身份打开windows终端
-
运行如下命令:
# Get-Service ssh-agent 发现处于Stopping状态 Get-Service -Name ssh-agent | Set-Service -StartupType Manual Start-Service ssh-agent # Get-Service ssh-agent 发现处于Running状态 ssh-add C:\Users\次林梦叶\.ssh\id_ed25519 # 然后输入秘钥密码 # Set-Service -Name ssh-agent -StartupType Automatic 将 ssh-agent 设置为开机自动启动
SFTP (Secure FileTransferProtocol 安全文件传送协议)
SFTP为 SSH的一部分,是一种传输档案至Blogger伺服器的安全方式。
版本控制(Git)
git with github
# 由于git config user.name 和 user.email信息会影响上传者的显示信息,我们已经知道了git config --global,但是我希望在一台机器上对不同的仓库有不同的信息应该怎么办? # 首先,进入该仓库目录,然后设置局部配置: cd /path/to/your/repository git config user.name "Repo-Specific Name" git config user.email "repo_specific_email@example.com"
有时希望使用ssh来访问github,机器中~/.ssh
下已经存在秘钥,但是我并不希望使用这个秘钥,而是再创建个新的秘钥。
ssh-keygen -t rsa -C "your_email@example.com" -f ~/.ssh/id_rsa_github
- 生成密钥时指定用户邮箱: ssh-keygen 可以通过 -C 选项添加一个注释,通常是用户的电子邮件地址。这通常用于标识该密钥属于哪个用户。
- 生成密钥时指定密钥文件名称: 使用 -f 选项,你可以指定保存密钥的路径和文件名。例如,可以将密钥命名为 id_rsa_github,而不是默认的 id_rsa。
然后将公钥上传到github后,使用ssh -T git@github.com
后,还是不起效果。
这绝大多数情况是因为有多个秘钥,无法判断访问github时使用哪个秘钥,解决方法:
- 编辑 SSH 配置文件: 打开或创建 ~/.ssh/config 文件。
- 添加主机配置: 在文件中添加以下内容:
Host github.com HostName github.com IdentityFile ~/.ssh/id_rsa_github IdentitiesOnly yes
或者使用ssh_agent:
eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_rsa_github
资料
基本原理理解
其中的 o 表示一次提交(commit)
在Git中,对象分为数据对象(blob),目录(树tree),提交(commit)
type object = blob | tree | commit type blob = array<byte> // 文件就是一组数据 type tree = map<string, tree | blob> // 一个包含文件和目录的目录 type commit = struct { // 每个提交都包含一个父辈,元数据和顶层树 parents: array<commit> author: string message: string snapshot: tree }
在有向无环图上,父辈是指节点指向的前一个节点
git log --all --graph --decorate --oneline
将commit信息以上述有向无环图的形式打印出来.
HEAD
身为一个'指针',它指向当前正在工作的提交(commit)。具体来说,HEAD 通常指向当前分支的最新提交,也就是你当前检出的分支上的最后一个提交记录。
通常,理解 HEAD 的最简方式,就是将它看做 该分支上的最后一次提交 的快照。
git branch
:git branch name
will create a new branch named "name". Creating branches just creates a new tag pointing to the currently checked out commit.
如上图,当前checked out的提交是在b6995da上,git branch name
会在这个commit上创建出一个名为name的标签
当HEAD与标签分离时(即如上图情况),是不允许git commit
的。
当HEAD与标签在一起时git commit
,那么HEAD与标签一起移动。
撤销操作
重置揭密 全面讲解了git checkout 和 git reset的使用原理和区别
git checkout
-
git checkout <branch-name>
将当前工作目录切换到该分支的最新状态,同时更新 HEAD 指针。
git checkout -b <new-branch-name>
创建并切换到一个新分支, 相当于git branch <new-branch-name>; git checkout <new-branch-name>
-
git checkout <commit-hash>
还可以用于切换到某个特定的提交,这种情况下会进入“分离的 HEAD”状态。
此时你可以查看历史提交,甚至可以修改代码,但这些修改不会影响当前的分支,除非你创建一个新分支。 -
git checkout -- <file-name>
如果你已经将文件的修改 git add 了,但还没有提交,可以使用 git checkout 撤销这些文件的暂存状态,让它们返回到修改前的状态。
即相当于
git add
的反操作
git reset
git reset 的行为主要取决于其选项,Git 提供了三种主要模式来控制它的作用范围:
- --soft:只修改 HEAD,不修改暂存区和工作区。
- --mixed(默认):修改 HEAD 和暂存区,不影响工作区。
- --hard:同时修改 HEAD、暂存区和工作区。
其可视化讲解在上述重置揭密
推荐资料中。
-
git reset --soft <commit>
reset 会移动 HEAD 分支的指向以及更新HEAD(即branch指针和HEAD都会移动且移动相同,checkout只会更新HEAD)
这里的<commit>一般是一次commit操作的Hash值
效果:
HEAD~表示HEAD的父辈,这里的效果相当于只git add了,但是没有git commit
-
git reset --mixed <commit>
这里的效果相当于没有git add了
-
git reset --hard <commit>
这里效果相当于操作没有进行!!!
-
git reset --mixed <commit> <file>
git reset <file>
的简写为git reset --mixed HEAD <file>
- 移动 HEAD 分支的指向 (已跳过)
- 让索引看起来像 HEAD (到此处停止)
所以看起来有
取消暂存文件
的实际效果。
git checkout <commit> <file>
与 git reset --hard <commit> <file>
效果很像
-
一样不会在有向无环图中移动 HEAD。
-
一样用该次提交(commit)中的那个文件来更新索引。
-
它也会覆盖工作目录中对应的文件。
记录每次更新到仓库
-
git commit --amend
有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend 选项的提交命令来重新提交:
-
git rm <file>
在仓库,暂存区,工作区中删除文件。
如果要删除之前修改过或已经放到暂存区的文件,则必须使用强制删除选项-f
-
git rm --cached <file>
在仓库,暂存区删除文件(即取消跟踪),但是在工作区中不删除文件
git patch
元编程
构建系统
make 是最常用的构建系统之一,您会发现它通常被安装到了几乎所有基于 UNIX 的系统中。
make 并不完美,但是对于中小型项目来说,它已经足够好了。当您执行 make 时,它会去参考当前目录下名为 Makefile 的文件。
依赖管理
大多数被其他项目所依赖的项目都会在每次发布新版本时创建一个 版本号。
通常看上去像 8.1.3 或 64.1.20192004。版本号一般是数字构成的,但也并不绝对。版本号有很多用途,其中最重要的作用是保证软件能够运行。
一个相对比较常用的标准是 语义版本号,这种版本号具有不同的语义,它的格式是这样的:主版本号.次版本号.补丁号。相关规则有:
- 如果新的版本没有改变 API,请将补丁号递增;
- 如果您添加了 API 并且该改动是向后兼容的,请将次版本号递增;
- 如果您修改了 API 但是它并不向后兼容,请将主版本号递增。
指定版本要求的方法很多,让我们学习一下Rust 的构建系统的依赖管理。
持续集成系统
持续集成(Continuous integration),或者叫做 CI 是一种雨伞术语(umbrella term,涵盖了一组术语的术语),它指的是那些“当您的代码变动时,自动运行的东西”
案例:
-
Git 可以作为一个简单的 CI 系统来使用,在任何 git 仓库中的 .git/hooks 目录中,您可以找到一些文件(当前处于未激活状态),它们的作用和脚本一样,当某些事件发生时便可以自动执行。
如当某些事件发生时便可以自动执行。编写一个 pre-commit 钩子,它会在提交前执行 make 并在出现构建失败的情况拒绝您的提交。这样做可以避免产生包含不可构建版本的提交信息;
-
GitHub Action
本课程的网站基于 GitHub Pages 构建,这就是一个很好的例子。Pages 在每次 master 有代码更新时,会执行 Jekyll 博客软件,然后使您的站点可以通过某个 GitHub 域名来访问。
对于我们来说这些事情太琐碎了,我现在我们只需要在本地进行修改,然后使用 git 提交代码,发布到远端。CI 会自动帮我们处理后续的事情。
调试与性能分析
调试
-
printf
大法 -
日志/系统日志/第三方日志
日志本质上也是
printf
大法,但是不是临时添加打印语句。日志较普通的打印语句有如下的一些优势:-
您可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
-
日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR 等),这使您可以根据需要过滤日志;
-
对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。
如果您正在构建大型软件系统,您很可能会使用到一些依赖,有些依赖会作为程序单独运行。和这些系统交互的时候,阅读它们的日志是非常必要的,因为仅靠客户端侧的错误信息可能并不足以定位问题。
幸运的是,大多数的程序都会将日志保存在您的系统中的某个地方。
-
-
调试器
很多编程语言都有自己的调试器。Python 的调试器是 pdb.
对于更底层的编程语言,您可能需要了解一下 gdb ( 以及它的改进版 pwndbg) 和 lldb。
-
静态分析器
有些问题是您不需要执行代码就能发现的。如语法错误...
大多数的编辑器和 IDE 都支持在编辑界面显示这些工具的分析结果、高亮有警告和错误的位置。 这个过程通常称为 code linting 。风格检查或安全检查的结果同样也可以进行相应的显示。
-
查看系统调用
即使您需要调试的程序是一个二进制的黑盒程序,仍然有一些工具可以帮助到您。
当您的程序需要执行一些只有操作系统内核才能完成的操作时,它需要使用 系统调用。有一些命令可以帮助您追踪您的程序执行的系统调用。
在 Linux 中可以使用 strace
性能分析
-
计时(类似
printf
大法)如: Python 的
time
模块分别会打印出:- 真实时间 Real - 从程序开始到结束流失掉的真实时间,包括其他进程的执行时间以及阻塞消耗的时间(例如等待 I/O 或网络);
- 用户时间 User - CPU 执行用户代码所花费的时间;
- 系统时间 Sys - CPU 执行系统内核代码所花费的时间。
-
性能分析工具(profilers)
但是上述方法还是通病,到处插入计时太麻烦了,所以有了专门的分析器
- CPU
cpu性能分析工具有两种: 追踪分析器(tracing)及采样分析器(sampling)。- 追踪分析器 会记录程序的每一次函数调用
- 而采样分析器则只会周期性的监测(通常为每毫秒)您的程序并记录程序堆栈。它们使用这些记录来生成统计信息,显示程序在哪些事情上花费了最多的时间。
- 内存
- 事件分析
事件包括如报告不佳的缓存局部性(poor cache locality)、大量的页错误(page faults)或活锁(livelocks)等。
perf 命令将 CPU 的区别进行了抽象,它不会报告时间和内存的消耗,而是报告与您的程序相关的系统事件。 - 可视化
- 火焰图
- 调用图和控制流图
- CPU
-
资源监控
本文作者:次林梦叶
本文链接:https://www.cnblogs.com/cilinmengye/p/18455101
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2023-10-09 JXNU数据库_数据库基本SQL操作