Linux-数据展示和信号

背景

在shell中,数据的展示方式有两种,一种是在终端上,还有一种是记录在文件中,这些在Linux中都是借助标准文件描述符实现,本文会对这些标准文件描述符做简要记录。同时在Linux中,是通过信号与进程中运行的程序进行交互,本文也会对此做简要记述。全文参考《Linux命令行与Shell编程大全》

内容

数据展示

在shell中文件描述符是一个非负数整数,并且在一个进程中最多可以有9个文件描述符,分别对应于:0~8,但是由于一些原因,在bash shell中保留了前三个文件描述符:012,分别对应于:标准输入(STDIN)、标准输出(STDOUT)、标准错误输出(STDERR)。

STDIN可以理解为我们常见的shell输入,比如我们直接在shell终端输入,或者通过文件读取。STDOUT可以理解为shell命令的输出,STDERR可以理解为命令运行报错时的输出,STDOUTSTDERR都是一样的输出,但是在Linux中,将其区分开对待。

对于STDIN,默认情况下是通过终端进行输入,但是也可以通过<<<进行修改,比如:

1
2
3
4
5
# 则cat的内容就是直接通过文件读取内容获得
cat < file
cat << EOF
hahaha
EOF

对于STDOUT,默认情况下是通过终端输出,但是也可以通过>>>进行修改,比如:

1
2
3
# 这种就将命令的输出重定向到了file文件中
who > file
who >> file

而对于STDERR,默认情况下也是通过终端输出,如果要修改输出对象,则必须通过:2>,比如:

1
2
# 2和>彼此不可分割
ls wahaha 2> err.log

那么如果将STDERRSTDOUT同时使用呢?也是可以的,比如:

1
2
# 0、1、2和>不可分割
ls wahahah 2> err.log 1> file

如果想将STDERRSTDOUT都重定向输出到同一个文件,也可以使用单独的命令:&>,比如:

1
ls shuai.csv wahahah &> file

但是执行之后,你会获得类似这样STDERR错误一直位于文件顶部的结果:

1
2
ls: wahaha: No such file or directory
3190270 -rw-r--r-- 1 wuxiang staff 56B 12 17 22:33 shuai.csv

这个是因为bash shell自动赋予STDERR更高的优先级,所以将STDERR放在了文件的顶端便于查看。

如果在shell脚本中,我们需要将不同的输出发送给不同的文件描述符时,这个时候就会讲到临时重定向和永久重定向的概念了,对于临时重定向,比如我们想将一个正常的输出传递给STDERR,则可以通过在文件描述符前加上数字2: &2

1
2
# >&2不可分割
echo "who am i" >&2

这种方式可以帮助脚本中输出运行错误的日志。同样,也可以修改shell脚本的STDERR为永久重定向,则需要借助命令: exec

1
2
3
4
5
6
7
# 格式:
exec 文件描述符> 输出对象
exec 文件描述符>> 输出对象

# 比如如下方式就会将所有的标准输出全部打印到file文件中
exec 1> file
echo "log err"

但是这种方式一旦修改以后,再想修改回去就非常麻烦,解决办法是借助其他文件描述符先保存原始数据,待到使用完毕后再修改回去,比如:

1
2
3
4
5
# 4>&1不可分割
exec 4>&1
exec 1> file
command命令
exec 1>&4

同样,还可以修改STDIN的重定向,这样输入的内容就变成了直接从文件中读取:

1
2
3
4
5
6
7
exec 6<&0
exec 0< file
while read line
do
echo $line
done
exec 0<&6

&>符号类似,也可以直接通过<>实现对同一个文件的读写,不过需要注意的是:在shell中,读写文件是通过文件指针读写的,因此当读取文件时,文件指针发生了变化,则再次写入的时候会从变化后的文件指针处开始写入,比如:

1
2
3
4
exec 3<> file
read line <&3
echo $line
echo "new line $0" >&3

则此时会将new line写到file文件的第二行。如下内容:

1
2
3
shuai
new line ./run.sh # 这一行是追加的
n.sh

如果对于自定的文件描述符,我们不再想用了,则可以将其删除

1
2
3
4
5
6
# 格式
# 关闭后的文件描述符不可再用,使用的话会直接报错
exec 文件描述符>&-

# 比如:
exec 3>&-

在使用文件描述符的过程中,有时需要查询当前shell到底使用了哪些文件描述符,此时就可以借助命令:lsof:

1
2
3
4
5
# -p:指定进程ID
# -d:指定要显示的文件描述符标号
# -a:对-p和-d进行and逻辑运算
# $$:表示当前的进程ID
lsof -a -p $$ -d 0,1,2

执行后得到如下结果:

1
2
3
4
COMMAND  PID    USER   FD   TYPE DEVICE  SIZE/OFF NODE NAME
zsh 3663 dudadag 0u CHR 32,8 0t4315965 675 /dev/yyp008
zsh 3663 dudadag 1u CHR 32,8 0t4315965 675 /dev/yyp008
zsh 3663 dudadag 2u CHR 32,8 0t4315965 675 /dev/yyp008

其中的含义如下(这个我也不是很懂,不过FD可以查看):

1
2
3
4
5
6
7
8
9
COMMAND:正在运行的命令的前9个字符
PID:进程PID
USER:进程属主的名字
FD:文件描述符号以及访问类型(r 代表读,w 代表写,u 代表读写)
TYPE:文件的类型(CHR 代表字符型,BLK 代表块型,DIR 代表目录,REG 代表常规文件)
DEVICE:设备的设备号,主设备号和从设备号
SIZE/OFF:表示文件的大小
NODE:本地文件节点号
NAME:文件名

有时候我们并不想查看命令的报错输出,则可以使用Linux下的黑洞文件:/dev/null,它可以将一切写入进去的内容全部丢弃,同时从该文件只能获取空内容,比如:

1
echo "content will be drop" > /dev/null

然后就得说在shell编程中常见一个使用临时文件保存数据的概念,有时候我们需要将一些内容输出到临时文件中,我们可以自主创建,但同时也可以使用命令:mktemp,它会在当前文件夹下创建一个随机命令的临时文件,如下:

1
2
3
4
5
# 注意X是大写的,shell会使用随机字符串替代X的部分,X的数量随自己而定
# 格式:
mktemp devin.XXXXX

# 命令执行的结果会返回创建文件的名称

如果需要在shell脚本中使用,则需要记录这个随机创建的名字

1
file=$( mktemp devni.XXXX )

另外,Linux中一个特殊的文件夹/tmp,它是临时文件夹,随着系统重启,则该文件夹中的内容就会被删除。我们同样可以利用mktemp在文件夹下创建文件,不过需要借助参数-t

1
2
# 它会返回文件绝对路径
mktemp -t devin.XXXX

然后还可以在/tmp下创建临时文件夹,但是需要借助参数:-d

1
mktemp -t -d devin.XXXX

假如我们既需要将STDOUT输出到命令行窗口,又要将内容输出到文件中去,此时就可以借助命令:tee和管道|,比如:

1
2
# 它不仅会将结果输出到控制台,同时还将内容输出到了file文件中
ls /tmp | tee file

信号

以前我们讲过命令kill -9 PID用户杀死进程,其中-9一直不得其意,其实它表示就是Linux中的信号,在Linux中,进程之间的通信都是借助于信号,Linux系统和应用程序可以生成超过30个信号,常见如下:

信号 描述
1 SIGHUP 挂起进程
2 SIGINT 终止进程
3 SIGQUIT 停止进程
9 SIGKILL 无条件终止进程
15 SIGTERM 尽可能终止进程
17 SIGSTOP 无条件停止进程,但不是终止进程
18 SIGTSTP 停止或暂停进程,但不是终止进程
19 SIGCONT 继续运行停止的进程

默认情况下,bash shell忽略信号3和15,但接受1和2。

然后就说到常见的两种信号的生成方式:终止进程(2)、暂停进程(18)

1
2
3
4
5
# 终止正在运行的进程:对应于SIGINT
Ctrl + C

# 暂停正在运行的进程
Ctrl + Z

如果想要查看当前后台运行了多少作业,则可以使用ps命令

1
ps -l

当后台有运行的shell时,此时退出终端,则会出现如下提示:

1
2
3
4
# 如果一定要退出,则再次执行一遍exit命令即可
baqi@host:~$ exit
logout
There are stopped jobs.

在用户没有设定的情况下,传递给shell脚本的信号就会由shell自己进行执行,不过有时候我们希望由该shell脚本针对信号的不同执行不同的命令,此时就需要借助命令:trap,它的格式如下:

1
2
3
# signals:是信号的值,或者是对应的数字皆可
# commands:触发signals信号后执行的命令
trap commands signals

不过当接受到触发信号后,它默认会停止当前正在执行的那一行命令,然后继续执行下一行的命令。举个例子:

1
2
3
4
trap "echo now i catch this signal SIGINT" SIGINT

# 或者也可以如下:
trap "echo now i catch this signal SIGINT" 2

还有就是有时我们希望shell脚本运行结束后能够执行一些命令,这个时候可以将signals部分改为EXIT即可,则其会在shell脚本运行结束后,执行trap后的命令,如下:

1
trap "echo 'this shell script is run over'" EXIT

如果在shell脚本的不同阶段,针对同一个signals要做出不同的响应,则直接在不同的位置针对同样的信号重写trap命令即可。但是如果希望删除用户自定的signals响应,则需要追加参数--

1
2
3
4
5
6
# signals:是信号的值,或者是对应的数字皆可
# 删除后,则该信号值对应的响应就会恢复到系统默认的处理方式上
trap -- signals

# 比如:
trap -- SIGINT

当前所描述的方式都是占用当前shell的窗口的,如果我们想将shell脚本的运行进程放到后台,则可以在尾部追加&

1
2
# 比如:
./run.sh &

运行后,一般会得到类似如下的内容:

1
2
# 2标识作业编号,是shell给定的唯一编号,689是该作业的进程ID
[2] 689

这里有一点需要注意,就是创建的作业是和终端会话绑定在 一起,如果终端会话结束,则创建的作业也会结束。那如果希望即便是退出终端也能够保证shell脚本的正常运行,则我们需要借助nohup命令,方式如下:

1
2
# 当我们退出终端的时候,nohup会直接无视终端发来的SIGHUP信号
nohup ./run.sh &

nohup运行的命令不会在当前终端中输出内容,它会将STDOUTSTDERR内容输出到一个独立的文件: nohup.out,所以可以直接查看该文件的内容即可。

那么如果我们想要查看后台一共运行了多少个作业,则可以借助jobs命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 最简单的就是:
jobs

# -l:列出job的进程ID
jobs -l

# -n:列出shell终端发出命令后,改变了状态的job
jobs -n

# -p:列出job的PID
jobs -p

# -r:列出运行中的job
jobs -r

# -s:列出已停止的作业
jobs -s

运行以后,应该会得出类似如下的输出:

1
2
3
4
shuai@baqi:~$ jobs -l
[1]+ 1363601 Stopped vim run.sh
[3] 1365029 Running sleep 1000 &
[4]- 1365062 Running sleep 1000 &

其中有两个带有+-,其中待+是当前的默认作业,而带-是默认作业结束后的下一个作业。

在bash shell中,可以将已停止的作业作为后台进程/前台进程进行重启,这个需要借助命令:bgfg。其中bg可以将后台已经停止的作业作为后台进程进行重启,格式如下:

1
2
3
4
5
6
7
bg 作业号

# 比如:
bg 3

# 如果要重启的作业正好是当前的默认作业,则可以省略作业号
bg

同样,fg可以将后台已经停止的作业作为前台进程进行重启,并且接管它运行的终端shell,格式如下:

1
2
3
4
5
6
7
fg 作业号

# 比如:
fg 3

# 如果要重启的作业正好是当前的默认作业,则可以省略作业号
fg

不管是bg还是fg,在重启后,都会接管该作业的终端shell。

在多任务的操作系统中,内核负责将CPU时间分配给系统上运行的进程,调度优先级则决定了每个进程占用CPU时间的多少,而在Linux系统中,调度优先级是一个整数值:-20~19,值越小,则调度优先级越高。在Linux中,默认每个进程的调度优先级是一样的,值为0,如果需要修改进程的调度优先级,则可以借助命令:nice

1
2
3
4
5
6
7
8
# -n:指定修改的调度优先级的值
nice -n num command

# 例如:
nice -n 10 ./run.sh

# -n参数也可以直接用-替代,如下:
nice -10 ./run.sh

nice只是修改即将运行的进程的调度优先级,如果需要修改已经运行的进程的调度优先级,则可以借助命令:renice

1
2
# -p:指定修改优先级的进程号
renice -n 10 ./run.sh

不过,对于nicerenice而言,如果是降低进程的调度优先级,则不需要root用户权限,如果是提高进程的调度优先级则需要root用户权限。但是我本地进行实验,却发现无法将进度的优先级调低到0以下,但是可以调到0以上

在Linux中,如果需要在某个指定的时间执行一个定时任务,则可以借助与命令:at,它的格式:

1
2
3
# -f:指定在指定时间需要执行的任务的shell脚本
# time:脚本运行的时间
at -f file time

对于at命令中的time则可以由多种表达方式:

1
2
3
4
5
6
7
8
9
10
11
12
# hour:min
at -f ./run.sh 8:15
at -f ./run.sh 8:15 PM

# MM/DD/YY、MMDDYY、MM.DD.YY
at -f ./run.sh 12/31/20

# now、noon、midnight、teatime
at -f ./run.sh teatime

# Dec 25
at -f ./run.sh Dec 25

但是对于at命令调用的shell脚本,他们都会被加入作业队列中,作业队列用a~z和A~Z表示优先级,字母排序越高则该shell进程的优先级越高。并且该命令的输出不再是STDOUTSTDERR,而是将这些输出直接通过邮件系统发送给运行该shell脚本的用户,如果系统上没有安装send mail程序,则输出将丢失。为了解决这类问题,有两个方案:

  1. 添加参数:-M

    1
    at -M -f ./run.sh teatime 
  2. 在脚本直接重定向输出

    1
    2
    #!/bin/bash
    echo "out to the file" > file

那么,如果想查看正在等待的at命令呢?可以借助命令atq

1
atq

同时也可以删除正在等待的at命令,需要借助:atrm

1
atrm [作业ID]

at命令对于特定时间执行一次命令的需求可以很好的适应,但是对于周期性的任务则无法满足,为此又出现了命令cron,cron是基于时间表的,类似于jenkins,格式如下:

1
min hour day_of_month month day_of_week cmd

其中day_of_week可以使用0~6表示周日到周一,其中0表示周日,6表示周六,如下:

1
2
# 每周一的10:18分执行任务
18 10 * * 1 ./run.sh

同时还可以用:mon、tue、wed、thu、fri、sat、sun来指定周日到周一:

1
2
# 上面的例子还可以写成如下
18 10 * * mon ./run.sh

对于那些已经构建好的cron时间表,如果需要查看,可以借助命令:crontab -l

1
crontab -l

但是执行之后,可能会提示:

1
2
baqi@qwer1234:~/Desktop$ crontab -l
no crontab for wuxiang - using an empty one

这个是因为默认情况下,用户的cron时间表并不存在,如果需要添加任务列表,则需要借助命令:cron

1
2
# 执行后,会提示选择编辑器,编辑选择后,就会出现文本,在文本的最后追加内容即可
crontab -e

当然,如果对运行脚本的时间精确度要求不是很高的话,则可以使用cron预设的cron脚本目录更加方便,这些目录可以通过命令查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ls /etc/cron.*ly

# 一般会得到如下输出,只要将需要执行的shell脚本复制到如下对应的目录下即可
baqi@qwer1234:~/Desktop$ ls /etc/cron.*
/etc/cron.daily: # --> 每天都会执行的目录
0anacron apport bsdmainutils dpkg man-db update-notifier-common
apache2 apt-compat cracklib-runtime logrotate popularity-contest

/etc/cron.hourly: # --> 每小时都会执行的目录

/etc/cron.monthly: # --> 每月都会执行的目录
0anacron

/etc/cron.weekly: # --> 每周都会执行的目录
0anacron man-db update-notifier-common

But,cron也有有缺陷的地方,如果系统异常关机,则cron对于关机期间的任务就不会再去执行,为此又引入了一个命令:anacron,它的作用是去检查cron的任务中是否有未执行完毕的任务,如果有它就会去重新调取执行。它的格式如下:

1
2
3
4
5
6
7
8
# period:定义作业多久运行一次,以天为单位
# delay:表示系统重启后延时多少分钟才开始执行错过的脚本
# identifier:特殊字符串,需要唯一,可以理解为任务的名字,用于表示日志消息和错误邮件中的作业
# command:包含run-parts和一个cron脚本目录名,run-parts会执行cron脚本目录名中传递过来的所有shell脚本
period delay identifier command

# 例如:每天执行cron.daily目录下的shell脚本,并且在重启5五分钟后
1 5 cron.daily run-parts --report /etc/cron.daily

要添加anacrontab的任务,则需要直接编辑其文件,该任务文件时:/etc/anacrontab

1
sudo vim /etc/anacrontab

此处有一点需要说明,那就是anacron命令不会执行/etc/cron.hourly目录下的shell脚本,这个是因为anacron执行的基本单位是天。