5

前言

最近有个需求是监控linux系统的一些指标,存到文件里面,要求是每10s监控一次指标,每小时换一次文件,最多保留24小时。并且可以作为守护进程。

直接与命令打交道的那肯定是shell脚本莫属。正好之前没有写过shell脚本,于是开始研究。

shell

Shell 这个单词的原意是“外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面

Shell 有很多种,只要能给用户提供命令行环境的程序,都可以看作是 Shell。

历史上,主要的 Shell 有下面这些。

  • Bourne Shell(sh)
  • Bourne Again shell(bash)
  • C Shell(csh)
  • TENEX C Shell(tcsh)
  • Korn shell(ksh)
  • Z Shell(zsh)
  • Friendly Interactive Shell(fish)

Bash 是目前最常用的 Shell, linux 中默认的shell 也是 Bash, 所以我用来编程的也是Bash

下面的命令可以查看当前设备的默认 Shell。

$ echo $SHELL
/bin/bash

一般来说,ps命令结果的倒数第二行是当前 Shell。

$ ps
  PID TTY          TIME CMD
 4467 pts/0    00:00:00 bash
 5379 pts/0    00:00:00 ps

下面的命令可以查看当前的 Linux 系统安装的所有 Shell。
$ cat /etc/shells

第一个 Shell 脚本

编写

打开文本编辑器,新建一个文件,扩展名为sh(sh代表shell)

很多ide都有相关bash的插件,例如语法检查,格式化等,具体可以根据自己使用的ide下载插件

在创建 shell 脚本文件时,必须在文件的第一行指定要使用的 shell:

#!/bin/bash

一般在 shell 脚本中,井号(#)用作注释行,除了第一行是例外。

指定了shell后,就可以编写了。如果我们只是想要执行一些简单的命令,那么其实和我们在终端直接输出命令是一样的

例如:

# 执行命令
echo "Hello World !"

运行

1. 作为可执行程序

将上面的代码保存为 test.sh,

chmod +x ./test.sh  #使脚本具有执行权限
./test.sh  #执行脚本

直接写成 test.sh 是不行的,因为该脚本并没有加入环境变量 PATH,
或者我们可以这么运行,直接运行解释器

/bin/sh test.sh

这种方式运行的脚本,不需要在第一行指定shell

守护进程化

守护进程化意味着进程与终端分离

一般可以使用 setid 命令。

setsid 可以创建一个新的会话(session)并在此会话中运行指定命令的命令,这个命令意味着:

  1. 进程不再是当前终端的一部分,因此它不受当前终端的任何键盘输入影响;
  2. 进程也不会接收到当前终端产生的SIGHUP信号;

忽略SIGHUP信号可以使得当前进程脱离终端

当一个终端退出时,会向该终端下的所有进程发送 SIGHUP 信号,以通知它们终端已经关闭。默认情况下,大多数进程会在收到 SIGHUP 信号后退出,这样就可以避免“孤儿进程”(父进程结束,子进程仍在运行)的出现。

另外,我们还希望进程在后台运行,即 shell不等待该脚本执行完毕后才继续接收新的命令,表示守护进程化后,我们终端可以继续输入别的命令。

总之,我们的命令如下

setsid $0 &

$0 表示当前正在执行的脚本的名称。表示当前脚本的路径和名称。
&: 这个符号将命令放入后台执行,即使终端关闭,命令也会继续执行,而不会等待命令执行完毕。


同时,我们还希望,守护进程化是可选的,例如,我们执行命令的时候 加参数 -d,则可以守护进程化,反之亦然。

执行脚本的参数可以用 $@ 符号获取,或者我们可以用 getopts

#!/bin/bash

DAEMON_MODE=false

for param in "$@"
do
    case $param in
        -d)
            DAEMON_MODE=true
            ;;
        *)
            echo "Unsupported parameter: $param"
            exit 1
            ;;
    esac  #结束标识
done

if [ "$DAEMON_MODE" = true ]; then
   setsid $0 &
   exit 0

while true
    do
       # 脚本逻辑
       sleep 10
    done

exit 0

至此,我们就可以使用-d参数了

输出文件

我们一般会使用一个文件来存放日志输出

代码如下:

# 定义日志文件路径
log_file="/var/log/daemon.log"

exec &>> $LOGFILE

exec 可以重定向标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)

&>> 是组合重定向操作符,它表示将标准输出(文件描述符stdout,默认编号1)和标准错误输出(文件描述符stderr,默认编号2)合并后追加到目标文件

例如,我们 echo "hello world", 那么这个信息就会被记录到文件里

文件管理

需求是每小时换一次日志文件,最多保留24小时,并且可以进行文件压缩

首先是每小时换一次日志文件:

LOG_DIR="/var/log/directory"
LOG_BASENAME="system_monitor"

# 计算当前日志文件名
LOG_FILE="$LOG_DIR/$LOG_BASENAME.$(date +'%Y%m%d-%H').log"

# 函数:切换日志文件
manage_log_file() {
    CURRENT_HOUR=$(date +%H)
    NEW_LOG_FILE="$LOG_DIR/$LOG_BASENAME.$(date +'%Y%m%d-%H').log"

    # 如果当前小时对应的日志文件与当前日志文件不同,则切换
    if [[ "$LOG_FILE" != "$NEW_LOG_FILE" ]]; then
        exec 1>>"$NEW_LOG_FILE" 2>&1
        LOG_FILE="$NEW_LOG_FILE"
    fi
}

最多保留24小时:
可以使用 find命令找出指定的文件,再用-delete删除

`MAX_DAYS=1`

# 清理超过1天的日志文件
find "$LOG_DIR" -name "$LOG_BASENAME.*.log" -type f -mtime +$MAX_DAYS -delete

压缩文件:
很简单,用gzip压缩就行

 gzip "$LOG_FILE"

完整代码

#!/bin/bash

DAEMON_MODE=false

LOG_DIR="/var/log/directory"
LOG_BASENAME="system_monitor"
`MAX_DAYS=1`

# 计算当前日志文件名
LOG_FILE="$LOG_DIR/$LOG_BASENAME.$(date +'%Y%m%d-%H').log"

for param in "$@"
do
    case $param in
        -d)
            DAEMON_MODE=true
            ;;
        *)
            echo "Unsupported parameter: $param"
            exit 1
            ;;
    esac  #结束标识
done

if [ "$DAEMON_MODE" = true ]; then
   setsid $0 &
   exit 0


# 函数:切换日志文件
manage_log_file() {
    # 清理超过1天的日志文件
    find "$LOG_DIR" -name "$LOG_BASENAME.*.log" -type f -mtime +$MAX_DAYS -delete

    CURRENT_HOUR=$(date +%H)
    NEW_LOG_FILE="$LOG_DIR/$LOG_BASENAME.$(date +'%Y%m%d-%H').log"

    # 如果当前小时对应的日志文件与当前日志文件不同,则切换
    if [[ "$LOG_FILE" != "$NEW_LOG_FILE" ]]; then
        # 压缩文件
        gzip "$LOG_FILE"
        exec 1>>"$NEW_LOG_FILE" 2>&1
        LOG_FILE="$NEW_LOG_FILE"
    fi
}

while true
    do
       manage_log_file
       # 脚本逻辑
       sleep 10
    done

exit 0

遇到的坑

一般我们都会认为 0 是 false , 1是 true。

而在shell中则比较复杂,命令语句返回0表示成功,例如我们使用 exit 0 表示正常退出

首先,bash里 条件判断是这么用:

if [ -f "file.txt" ]; then
    echo "file.txt 存在"
fi

方括号[]通常用于条件测试

先看 if [ A ]的情况

1 如果 A 是个数字,则 if [ A ] 始终为真,

if [ 1 ] ; then
    echo "1 is true"
else
    echo "1 is false"
fi

if [ 0 ] ; then
    echo "0 is true"
else
    echo "0 is false"
fi

结果:

1 is true
0 is true

2 如果 A 是个表达式,则用 A 的返回值也就是状态码,来决定 if [ A ] 的真假,不过当状态码为 0 时 if [ A ] 为真,当状态码为非 0 时 if [ A ] 为假

if [ 5 -eq 5 ] ; then
    echo "$? is true"
else
    echo "$? is false"
fi

if [ 5 -eq 6 ] ; then
    echo "$? is true"
else
    echo "$? is false"
fi

结果:

0 is true
1 is false

状态码含义如下:

状态码描述
0命令成功结束
1通用未知错误
2误用 Shell 命令
126命令不可执行
127没找到命令
128无效退出参数
128+nLinux 中信号为 n 的严重错误
130通过 Ctrl + C 退出
255*退出状态码越界

参考:
https://blog.csdn.net/lyndon_li/article/details/120735191
https://www.w3cschool.cn/linux/linux-shell.html
https://blog.csdn.net/w918589859/article/details/108752592


weiweiyi
1k 声望123 粉丝

引用和评论

0 条评论