shell_awk&grep&pstree&xarg&lsof&find

16 min read

本文介绍几个GNU/linux下的shell指令,可以帮助我们排查问题。

1 grep

日志内容排查当然少不了grepgrep自己就能处理大文件,所以cat 1.log|grep xxx这种管道用法是不合理的。

grep有三种用法,如下:

$ grep [OPTION...] PATTERNS [FILE...]
$ grep [OPTION...] -e PATTERNS ... [FILE...]
$ grep [OPTION...] -f PATTERN_FILE ... [FILE...]

列举几个最常见的用法:

# 从1.log文件中查找包含hello的行
$ grep 'hello' 1.log

# -v 反向查找,找到不包含hello的行
$ grep -v 'hello' 1.log

# -i 忽略大小写 -n 显示行号 --color高亮显示匹配部分
$ grep -i -l 'hello' 1.log --color

# -A 打印匹配行之后的N行,-B 打印匹配行之前的N行
$ grep -A 2 -B 2 'hello' 1.log

# -C 打印匹配行前后各N行,效果同上
$ grep -C 2 'hello' 1.log

# -r 递归搜索目录 -l 只打印匹配的文件名,这样可以找到那些内容含有hello的文件
$ grep -rl "hello" /path/to/directory

上面的用法我们可以快速定位要找的文本,并通过ABC参数可以打印出匹配行的上下文,尤其是对于搜索错误日志非常实用。

同时,grep还支持正则表达式,来模糊匹配,建议用-P来指定正则

# 默认是-G采用基础的正则BER,-P采用perl正则,-E采用扩展的正则,-F则是不用正则,直接匹配字符串

# 基础正则,支持常见的正则
$ grep '[A-Za-z]x*' 1.log # 匹配以字母开头,后面跟着0个或多个字母x

# -E实用扩展正则表达式,与默认差不多,但是默认的有些字符(如+)需要转义 -E则不用多转一次
$ grep -E '[A-Za-z]x+' 1.log # 匹配以字母开头,后面跟着1个或多个字母x

# -P与-E类似,但是是更丰富的Perl正则,例如引入了\d等支持
$ grep -P '\dx+' 1.log

此外对于多个文件场景

# 搜索所有的`.log`后缀的文件这对于有rollingfile配置日志文件目录比较友好。
$ grep 'hello' *.log

# 搜索指定的两个文件
$ grep 'hello' 1.log 2.log

2 find

文件查找,主要用法如下

基于文件名的查找

# 搜索/目录和子目录下所有文件名为hello.txt的文件;-maxdepth指定最大子目录深度
$ find / -maxdepth 4 -name 'hello.txt' 

# 搜索/目录和子目录下所有文件名是hello.开头的文件; -type指定类型f文件,d目录,l链接
$ find / -type f -name 'hello.*'

# 搜索/目录和子目录下的绝对路径中包含/libc的文件,正则匹配;此外-iregex则是忽略大小写
$ find / -regex '.*libc.*'

基于文件其他基础信息

# size>1M 并且上次修改时间是超过10天前的so文件
$ find / -size +1M -mtime +10 -name '*.so'

# size单位: k M G +是超过 -是小于 mtime单位是天,mmin单位则是分钟

基于查到的文件进行操作

# exec后面的表达式中,{}占位符代表找到的文件名,\;是;的转义,分割多个shell执行
$ find / -size +10M -exec du -h {} \;

3 xargs

xargs用来和管道|联合使用,可以进行非常流畅的将内容通过管道输送到后面的shell。

来看以下两者区别

$ ls | grep hello
$ ls | xargs grep hello

ls | grep hello 代表列出当前目录下的文件,然后过滤这些文件中,文件名含有hello的文件。

ls | xargs grep hello 代表累出当前目录下的文件,并过滤文件内容汇总还有hello的行。

为什么会有这么大的区别呢?其实是因为不同的指令对于管道的支持是不同的。一般来说,我们可能希望,|前面指令运行结果,能够作为后面指令的运行参数,但是实际上指令并不一定支持。以ls为例,下面指令我们想的是能得到等价于ls 1.log的效果,但是实际上ls并没有接收管道扔过来的1.log

# 下面运行结果等价于ls
$ echo '1.log' | ls

这时候就需要xargs来帮忙了,xargs让指令和管道配合的更好。

# xargs会将管道传过来的参数,作为后面指令的参数放到后面,等价于 ls 1.log
$ echo '1.log' | xargs ls

通过--verbose参数可以看到xargs的详细执行过程

# 列出所有的`.c`文件,并传给du运行,最终的指令是du -h hello.c main.c t1.c
$ ls *.c  | xargs --verbose du -h
du -h hello.c main.c t1.c 
4.0K	hello.c
4.0K	main.c
4.0K	t1.c

默认情况下xargs以空格为分隔符,即前一句的运行结果会被进行简单的处理,多个空格会合并为一个,然后回车也会被替换为空格

# 这里有回车和多个空格,最后作为ls参数的回车会替换为空格,多个空格会被合并
$ echo -e '2.log\n     3.log' | xargs --verbose ls
ls 2.log 3.log 
2.log  3.log

如果上一级输出有很多空格但是不想被替换,可以用-d参数指定分隔符,

# 手动指定回车分割
$ echo -e '2.log\n3.log' | xargs -d '\n' --verbose ls
ls 2.log 3.log 
2.log  3.log

# 与find的-print0结合(find -print0使结果每行不以回车结尾而是\0), xargs -0则是按照\0分割,将分割后的字符串依次作为ls后的参数
$ find . -name "*.log" -print0 | xargs -0 --verbose ls

默认情况下,xargs会将管道传过来的参数,作为后面指令的参数放到后面,如果我们希望传过来的参数按照空格分开,每个参数都作为后面指令的入参,运行多次.例如echo -e '2.log\n3.log'| xargs --verbose ls等价于ls 2.log 3.log,说白了最后还是一句shell,如果想要拆分多行,每行执行一次ls 2.log然后ls 3.log则可以这样

# -n 指定每个参数都作为ls的参数执行一次,n还可以指定其他比如每两个作为ls的参数运行一次shell
$ echo -e '2.log\n3.log' | xargs -n 1 ls
2.log
3.log

有了xargs,我们就可以熟练掌握管道化的指令了,比如上面find / -size +10M -exec du -h {} \;我们如果不知道-exec的用法,完全可以下面来替换达到一样效果

# find和xargs连用最好用-print0和-0配合,不然文件名可能有奇怪字符被xargs给转掉
find / -size +10M -print0| xargs -0 -n 1 du -h

4 pstree

ps指令可以帮助我们看当前系统下运行的进程情况,而pstree也是gnu/linux下的一个进程查看工具,可以帮助我们查看进程树。

# 直接使用,展示1号进程systemd和由此产生的进程/线程
$ pstree

# 指定PID,展示该进程和产生出来的进程、线程
$ pstree <PID>

# -T 只展示进程,不展示线程
$ pstree -T

# -p展示进程/线程ID,-t展示线程名称
$ pstree -pt <PID>

这是一个示例,展示java进程的线程树,与jstack的线程信息一致。

$ pstree -pt  150693
java(150693)─┬─{C1 CompilerThre}(150707)
             ├─{C2 CompilerThre}(150706)
             ├─{Catalina-utilit}(150716)
             ├─{Catalina-utilit}(150717)
             ├─{Common-Cleaner}(150711)
             ├─{Finalizer}(150702)
             ├─{G1 Conc#0}(150697)
             ├─{G1 Main Marker}(150696)
             ├─{G1 Refine#0}(150698)
             ├─{G1 Service}(150699)
             ├─{GC Thread#0}(150695)
             ├─{GC Thread#1}(150712)
             ├─{HikariPool-1 ho}(150715)
             ├─{Monitor Deflati}(150705)
             ├─{Notification Th}(150709)
             ├─{Reference Handl}(150701)
             ├─{Service Thread}(150704)
             ├─{Signal Dispatch}(150703)
             ├─{Sweeper thread}(150708)
             ├─{VM Periodic Tas}(150710)
             ├─{VM Thread}(150700)
             ├─{container-0}(150718)
             ├─{http-nio-8080-A}(150731)
             ├─{http-nio-8080-P}(150730)
             ├─{http-nio-8080-e}(150720)
             ├─{http-nio-8080-e}(150721)
             ├─{http-nio-8080-e}(150722)
             ├─{http-nio-8080-e}(150723)
             ├─{http-nio-8080-e}(150724)
             ├─{http-nio-8080-e}(150725)
             ├─{http-nio-8080-e}(150726)
             ├─{http-nio-8080-e}(150727)
             ├─{http-nio-8080-e}(150728)
             ├─{http-nio-8080-e}(150729)
             └─{java}(150694)

5 lsof

lsof用来查看当前系统打开的文件,可以帮助我们查看某个进程打开的文件情况,或者查看某个端口被哪个进程占用。因为Linux中一切都是文件,即使是端口也是文件,甚至控制台都是文件。

文件相关(注意这里文件特指文件系统中的文件而不是everything),我们可以用lsof查看文件被哪个进程打开,或者查看文件被哪个进程打开,并打印出进程的PID

# 查看进程打开的文件,一般会有一些进程可执行文件、动态链接库、读写的文件句柄等
$ lsof -p <PID>

# 查看用户打开的文件
$ lsof -u <USER>

# 反过来查看文件被哪些进程打开
$ lsof /usr/lib/x86_64-linux-gnu/libc-2.31.so

端口相关,一般可以用netstat -antp来查看进程监听的端口,但是netstat可能没有内置,可以用apt install net-tools来安装,但是lsof也能查看端口

# -i 是查看端口监听 后面接TCP或UDP可以区分协议,
$ lsof -i <TCP/UDP>

# 可以查看特定的端口 或特定协议的特定端口 被哪个进程占用
$ lsof -i :8080
$ lsof -i TCP:8080

# 占用不一定是监听可以用| grep LISTE来过滤监听 或者-sTCP:LISTEN,过滤tcp套接字,状态为监听
$ lsof -i :8080 -sTCP:LISTEN

6 awk

awk是一种编程语言和工具,它主要用于文本和数据的提取和报告。在UNIX和类UNIX系统中,awk特别适合对结构化文本和字符串进行操作

基本用法如下

$ gawk [ POSIX or GNU style options ] -f program-file [ -- ] file ...
$ gawk [ POSIX or GNU style options ] [ -- ] program-text file ...

awk其实理解起来并不复杂,他就是会对文件的每一行都运行一段函数,我们只需要记住几个常用的函数中的语法即可

# 对1.log每一行文本,运行print $1这段代码,print打印类似echo输出到控制台,$1 $2 ...分别是按照空格拆开的 第1,2...列
$ awk '{print $1}' 1.log

# -F指定每列的分隔符,这里指定分隔符为逗号
$ awk -F ',' '{print $1}' 1.txt

# /正则/ 下面用法用正则匹配行,效果等于grep 'h\w' 1.txt,换句话说大多数场景下awk可以替代grep
$ awk '/h\w/ {print}' 1.txt

image image

常见中可用的内置变量与内置函数:

  • $0整行 $1第一列 $2第二列...
  • NR当前行号(从1开始) NF当前行总列数
  • FS分隔符默认空格-F可以改,RS行分隔符默认回车
  • FILENAME文件名
  • print打印函数,类似shell的echo,用逗号隔开打印多个变量,输出结果会用空格隔开
  • gsub字符串替换
  • -v可以传递变量进去awk -v var="value" '{print var,$1}' filename
  • length(string)
  • index(string, search_string),从1开始,没有则返回0,和编程语言不一样下标从1开始
  • substr(string, start [, length])
  • split(string, array [, fieldsep])
  • toupper(string) tolower(string)
  • sub(regexp, replacement, [,target])在target中查找regexp的第一个匹配项,并用replacement替换它如果没有提供target则默认为整行($0)
  • gsub(regexp, replacement [, target]) 与上面类似,只不过sub是第一个匹配项,gsub是所有匹配项
  • if($1 ~ /正则/) 判断语句,正则匹配的
# 把每行第一列,重新输出到另一个文件,注意双引号
$ awk '{print $1 > "outputfile.txt"}' 1.txt

# BEGIN {} 指在开始之前的时候的准备工作,可以修改一些变量的值,或者赋给变量初值
# END {} 指在最后一行之后执行的收尾工作
# 这里我们给定sum初值为0,分隔符逗号;每一行把列数加到sum中,最后打印sum,多句之间分号隔开类似c语言
$ awk 'BEGIN {FS=","; SUM=0} {SUM = SUM + NF} END {print SUM}'  1.txt

# 执行函数中增加if判断,只有奇数行可以执行sum的增加,sum可以不给初值,默认就为0
# FS如果不是在BEGIN修改而是中间某一句,那么不影响当前行,而是影响下一行。
$ awk 'BEGIN {FS=","} {if (NR % 2 == 1)SUM = SUM + NF} END {print SUM}'  1.txt

# printf函数可以打印格式化的字符串,类似c的printf
$ awk '{printf "第一列为%s\n", $1}' 1.txt

# 把hello替换为hi,输出整行,注意并不修改源文件,只是改了$0变量的值,不产生实际影响
$ awk '{ gsub(/hello/, "hi"); print $0}' filename

实际中我们常常与管道结合使用awk

# ps查看进程信息,grep过滤java进程,awk打印第二列(pid), sort -n按照数字进行从小到大排序
$ ps aux | grep java | awk '{print $2}' | sort -n