强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 16 - 系统管理

16 - 系统管理

16.1 用户与权限管理

用户信息查询

# 当前用户信息
echo "用户名: $(whoami)"
echo "用户ID: $(id -u)"
echo "组ID:   $(id -g)"
echo "所有组: $(id -Gn)"

# 用户列表
awk -F: '{printf "%-20s UID=%-6s GID=%-6s %s\n", $1, $3, $4, $7}' /etc/passwd | head -10

# 登录用户
who
w

# 最近登录
last | head -10

# sudo 日志
journalctl _COMM=sudo --no-pager | tail -20

批量用户管理

#!/bin/bash
# user_mgmt.sh —— 批量用户管理工具
set -euo pipefail

readonly GROUP="developers"

create_user() {
    local username="$1"
    local fullname="$2"
    local shell="${3:-/bin/bash}"

    # 检查用户是否已存在
    if id "$username" &>/dev/null; then
        echo "⚠️  用户 $username 已存在"
        return 0
    fi

    # 生成随机密码
    local password
    password=$(openssl rand -base64 12)

    # 创建用户
    useradd -m -c "$fullname" -s "$shell" -G "$GROUP" "$username"
    echo "$username:$password" | chpasswd

    # 强制首次登录修改密码
    chage -d 0 "$username"

    echo "✅ 创建用户: $username"
    echo "   初始密码: $password"
    echo "   全名: $fullname"
    echo "   组: $GROUP"
}

disable_user() {
    local username="$1"

    if ! id "$username" &>/dev/null; then
        echo "❌ 用户 $username 不存在" >&2
        return 1
    fi

    # 锁定账户
    passwd -l "$username"
    # 禁用登录 Shell
    chsh -s /usr/sbin/nologin "$username"
    # 终止所有进程
    pkill -u "$username" 2>/dev/null || true

    echo "🔒 已禁用用户: $username"
}

delete_user() {
    local username="$1"
    local keep_home="${2:-no}"

    if ! id "$username" &>/dev/null; then
        echo "❌ 用户 $username 不存在" >&2
        return 1
    fi

    # 终止进程
    pkill -u "$username" 2>/dev/null || true
    sleep 1

    if [[ "$keep_home" == "yes" ]]; then
        userdel "$username"
    else
        userdel -r "$username"
    fi

    echo "🗑️  已删除用户: $username"
}

# 从 CSV 文件批量创建
batch_create() {
    local csv_file="$1"
    # CSV 格式: username,fullname,shell

    while IFS=, read -r username fullname shell; do
        [[ -z "$username" || "$username" == \#* ]] && continue
        create_user "$username" "$fullname" "${shell:-/bin/bash}"
    done < "$csv_file"
}

# 用法
case "${1:-help}" in
    create)    create_user "$2" "$3" "${4:-/bin/bash}" ;;
    disable)   disable_user "$2" ;;
    delete)    delete_user "$2" "${3:-no}" ;;
    batch)     batch_create "$2" ;;
    *)
        echo "用法: $0 {create|disable|delete|batch} [参数...]"
        echo "  create <用户名> <全名> [shell]"
        echo "  disable <用户名>"
        echo "  delete <用户名> [保留主目录:yes/no]"
        echo "  batch <csv文件>"
        ;;
esac

16.2 服务管理

#!/bin/bash
# service_mgr.sh —— 服务管理工具
set -euo pipefail

readonly SERVICE_NAME="${1:?用法: $0 <服务名> {start|stop|restart|status|logs|enable|disable}}"

# 检查 systemd 是否可用
if ! command -v systemctl &>/dev/null; then
    echo "系统不使用 systemd" >&2
    exit 1
fi

case "${2:-status}" in
    start)
        echo "启动服务: $SERVICE_NAME"
        systemctl start "$SERVICE_NAME"
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    stop)
        echo "停止服务: $SERVICE_NAME"
        systemctl stop "$SERVICE_NAME"
        ;;
    restart)
        echo "重启服务: $SERVICE_NAME"
        systemctl restart "$SERVICE_NAME"
        sleep 2
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    status)
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    logs)
        journalctl -u "$SERVICE_NAME" -f --no-pager
        ;;
    enable)
        systemctl enable "$SERVICE_NAME"
        echo "✅ 服务已设置为开机自启"
        ;;
    disable)
        systemctl disable "$SERVICE_NAME"
        echo "✅ 服务已取消开机自启"
        ;;
    *)
        echo "未知命令: ${2}" >&2
        exit 1
        ;;
esac

16.3 磁盘与文件系统

# 磁盘使用情况
df -h

# 指定挂载点
df -h / /home /var

# 目录大小
du -sh /var/log

# 最大的文件/目录
du -ah /var 2>/dev/null | sort -rh | head -20

# 查找大文件
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh | head -20

# 查找并清理日志
find /var/log -name "*.log" -mtime +30 -exec ls -lh {} \;
find /var/log -name "*.log" -mtime +30 -delete

# inode 使用情况
df -i

# 挂载点信息
findmnt -t ext4,xfs

# 磁盘健康
smartctl -a /dev/sda 2>/dev/null || echo "需要安装 smartmontools"

16.4 进程管理

# 查看进程
ps aux | head -20

# 按 CPU 排序
ps aux --sort=-%cpu | head -10

# 按内存排序
ps aux --sort=-%mem | head -10

# 查找特定进程
ps aux | grep -E "[n]ginx"  # 使用 [] 技巧排除 grep 自身

# 进程树
pstree -p

# 实时监控
top -bn1 | head -20

# 查看进程的打开文件
lsof -p $$ | head -20

# 查看端口占用
ss -tlnp | head -20
netstat -tlnp 2>/dev/null | head -20

# 杀死进程
kill PID                    # 发送 SIGTERM
kill -9 PID                 # 强制杀死 (SIGKILL)
pkill -f "pattern"          # 按名称杀死

# 查找占用端口的进程
fuser 80/tcp 2>/dev/null
lsof -i :80 2>/dev/null

16.5 系统备份脚本

#!/bin/bash
# backup.sh —— 系统备份脚本
set -euo pipefail

readonly BACKUP_DIR="/backup"
readonly DATE=$(date +%Y%m%d_%H%M%S)
readonly RETENTION_DAYS=30
readonly LOG_FILE="/var/log/backup.log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }

# 创建备份目录
mkdir -p "$BACKUP_DIR"/{daily,weekly,monthly}

# 备份数据库
backup_database() {
    local db_name="$1"
    local backup_file="$BACKUP_DIR/daily/${db_name}_${DATE}.sql.gz"
    
    log "备份数据库: $db_name"
    
    mysqldump --single-transaction --routines --triggers "$db_name" | \
        gzip > "$backup_file"
    
    local size
    size=$(du -h "$backup_file" | cut -f1)
    log "数据库备份完成: $backup_file ($size)"
}

# 备份文件目录
backup_files() {
    local source="$1"
    local name=$(basename "$source")
    local backup_file="$BACKUP_DIR/daily/${name}_${DATE}.tar.gz"
    
    log "备份目录: $source"
    
    tar czf "$backup_file" "$source" 2>/dev/null
    
    local size
    size=$(du -h "$backup_file" | cut -f1)
    log "文件备份完成: $backup_file ($size)"
}

# 清理旧备份
cleanup_old_backups() {
    log "清理 ${RETENTION_DAYS} 天前的备份..."
    
    local count=0
    while IFS= read -r file; do
        rm -f "$file"
        ((count++))
    done < <(find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -name "*.gz")
    
    log "清理完成,删除 $count 个文件"
}

# 验证备份完整性
verify_backup() {
    local file="$1"
    
    if [[ ! -f "$file" ]]; then
        log "❌ 备份文件不存在: $file"
        return 1
    fi
    
    if [[ ! -s "$file" ]]; then
        log "❌ 备份文件为空: $file"
        return 1
    fi
    
    if file "$file" | grep -q "gzip"; then
        if gzip -t "$file" 2>/dev/null; then
            log "✅ 备份完整性验证通过: $file"
            return 0
        fi
    fi
    
    log "❌ 备份文件损坏: $file"
    return 1
}

# 主函数
main() {
    log "========================================"
    log "备份开始"
    log "========================================"
    
    # 执行备份
    backup_database "myapp"
    backup_files "/etc"
    backup_files "/home"
    backup_files "/opt/myapp"
    
    # 验证
    verify_backup "$BACKUP_DIR/daily/myapp_${DATE}.sql.gz"
    
    # 清理
    cleanup_old_backups
    
    log "========================================"
    log "备份完成"
    log "========================================"
}

main "$@"

16.6 cron 集成

cron 基础

# crontab 格式:
# ┌──── 分钟 (0-59)
# │ ┌──── 小时 (0-23)
# │ │ ┌──── 日 (1-31)
# │ │ │ ┌──── 月 (1-12)
# │ │ │ │ ┌──── 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# * * * * * command

# 查看 crontab
crontab -l

# 编辑 crontab
crontab -e

# 常用 cron 表达式
# 每天凌晨 2 点
# 0 2 * * * /opt/scripts/backup.sh

# 每 5 分钟
# */5 * * * * /opt/scripts/monitor.sh

# 工作日每天 9 点
# 0 9 * * 1-5 /opt/scripts/report.sh

# 每月 1 号
# 0 0 1 * * /opt/scripts/monthly_cleanup.sh

cron 脚本模板

#!/bin/bash
# cron_job.sh —— 适配 cron 的脚本模板
set -euo pipefail

# cron 环境变量很少,需要显式设置
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export HOME="$(getent passwd "$(whoami)" | cut -d: -f6)"

readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_DIR="/var/log/cron"
readonly LOCK_DIR="/tmp/cron_locks"

mkdir -p "$LOG_DIR" "$LOCK_DIR"

# 锁文件防重复执行
LOCK_FILE="$LOCK_DIR/${SCRIPT_NAME}.lock"

cleanup() {
    rm -f "$LOCK_FILE"
}
trap cleanup EXIT

# 检查是否已在运行
if [[ -f "$LOCK_FILE" ]]; then
    old_pid=$(cat "$LOCK_FILE")
    if kill -0 "$old_pid" 2>/dev/null; then
        echo "脚本已在运行 (PID: $old_pid),退出" >> "$LOG_DIR/${SCRIPT_NAME}.log"
        exit 0
    fi
fi

echo $$ > "$LOCK_FILE"

# 日志输出(cron 不显示终端)
exec > >(tee -a "$LOG_DIR/${SCRIPT_NAME}.log") 2>&1

echo "=== 任务开始: $(date) ==="

# 业务逻辑
your_main_function

echo "=== 任务结束: $(date) ==="

安装 cron 任务

#!/bin/bash
# install_cron.sh —— 安装 cron 任务
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly CRON_ENTRIES=(
    "0 2 * * * $SCRIPT_DIR/backup.sh >> /var/log/cron/backup.log 2>&1"
    "*/5 * * * * $SCRIPT_DIR/monitor.sh >> /var/log/cron/monitor.log 2>&1"
    "0 9 * * 1-5 $SCRIPT_DIR/report.sh >> /var/log/cron/report.log 2>&1"
)

# 备份现有 crontab
crontab -l > /tmp/crontab_backup_$(date +%Y%m%d) 2>/dev/null || true

# 添加 cron 任务(避免重复)
for entry in "${CRON_ENTRIES[@]}"; do
    if crontab -l 2>/dev/null | grep -qF "${entry%% *}"; then
        echo "⚠️  任务已存在: ${entry%% *}"
    else
        (crontab -l 2>/dev/null; echo "$entry") | crontab -
        echo "✅ 已添加: $entry"
    fi
done

echo ""
echo "当前 cron 任务:"
crontab -l

16.7 日志管理

# 日志轮转配置
cat > /etc/logrotate.d/myapp << 'EOF'
/var/log/myapp/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp myapp
    sharedscripts
    postrotate
        systemctl reload myapp > /dev/null 2>&1 || true
    endscript
}
EOF

# 手动触发日志轮转
logrotate -f /etc/logrotate.d/myapp

# 查看系统日志
journalctl -xe                           # 最近日志
journalctl -u nginx --since "1 hour ago" # 指定服务
journalctl -p err                        # 仅错误级别
journalctl --disk-usage                   # 日志占用空间
journalctl --vacuum-size=1G              # 清理到指定大小

16.8 注意事项

陷阱 说明 解决方案
cron 环境变量缺失 PATH 很短 脚本中显式设置 PATH
cron 日志不可见 没有终端 重定向到文件
重复执行 cron 可能重叠执行 使用锁文件
root 权限滥用 脚本以 root 运行 使用最小权限原则
备份空间不足 备份文件不断增长 设置保留策略

16.9 扩展阅读